├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .vscode └── settings.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.txt ├── NPM.md ├── README.md ├── aws ├── .babelrc ├── .eslintrc.json ├── LICENSE.txt ├── README.md ├── jest.config.js ├── package-lock.json ├── package.json ├── pipelines │ ├── ci.yml │ ├── publish-prerelease.yml │ └── publish-release.yml ├── src │ ├── awsContext.test.ts │ ├── awsContext.ts │ ├── awsModule.test.ts │ ├── awsModule.ts │ ├── awsRequest.test.ts │ ├── awsRequest.ts │ ├── awsResponse.test.ts │ ├── awsResponse.ts │ ├── index.ts │ ├── middleware │ │ ├── index.ts │ │ ├── kinesisMiddleware.test.ts │ │ ├── kinesisMiddleware.ts │ │ ├── simpleQueueMiddleware.test.ts │ │ ├── simpleQueueMiddleware.ts │ │ ├── simpleStorageMiddleware.ts │ │ └── simpleStorageMidleware.test.ts │ ├── models │ │ └── awsLamda.ts │ ├── services │ │ ├── S3Storage.test.ts │ │ ├── S3Storage.ts │ │ ├── index.ts │ │ ├── lambdaCloudService.test.ts │ │ └── lambdaCloudService.ts │ └── test │ │ └── events │ │ └── defaultAwsEvent.json └── tsconfig.json ├── azure-pipelines.yml ├── azure ├── .babelrc ├── .eslintrc.json ├── LICENSE.txt ├── README.md ├── jest.config.js ├── package-lock.json ├── package.json ├── pipelines │ ├── ci.yml │ ├── publish-prerelease.yml │ └── publish-release.yml ├── src │ ├── azureContext.test.ts │ ├── azureContext.ts │ ├── azureModule.test.ts │ ├── azureModule.ts │ ├── azureRequest.test.ts │ ├── azureRequest.ts │ ├── azureResponse.test.ts │ ├── azureResponse.ts │ ├── index.ts │ ├── middleware │ │ ├── eventHubMiddleware.test.ts │ │ ├── eventHubMiddleware.ts │ │ ├── index.ts │ │ ├── serviceBusMiddleware.test.ts │ │ ├── serviceBusMiddleware.ts │ │ ├── storageBlob.test.ts │ │ ├── storageBlob.ts │ │ ├── storageQueueMiddleware.test.ts │ │ └── storageQueueMiddleware.ts │ ├── models │ │ └── azureFunctions.ts │ └── services │ │ ├── azureBlobStorage.test.ts │ │ ├── azureBlobStorage.ts │ │ ├── azureFunctionCloudService.test.ts │ │ ├── azureFunctionCloudService.ts │ │ └── index.ts └── tsconfig.json ├── core ├── .babelrc ├── .eslintrc.json ├── LICENSE.txt ├── README.md ├── jest.config.js ├── package-lock.json ├── package.json ├── pipelines │ ├── ci.yml │ ├── publish-prerelease.yml │ └── publish-release.yml ├── src │ ├── app.test.ts │ ├── app.ts │ ├── builders │ │ ├── mockBuilder.test.ts │ │ ├── mockBuilder.ts │ │ ├── mockedService.ts │ │ └── service │ │ │ └── testService.ts │ ├── cloudContainer.test.ts │ ├── cloudContainer.ts │ ├── cloudContext.ts │ ├── cloudMessage.ts │ ├── cloudRequest.ts │ ├── cloudResponse.ts │ ├── common │ │ ├── guard.test.ts │ │ ├── guard.ts │ │ ├── stringParams.test.ts │ │ ├── stringParams.ts │ │ ├── util.test.ts │ │ └── util.ts │ ├── index.ts │ ├── middleware │ │ ├── exceptionMiddleware.test.ts │ │ ├── exceptionMiddleware.ts │ │ ├── httpBindingMiddleware.test.ts │ │ ├── httpBindingMiddleware.ts │ │ ├── index.ts │ │ ├── loggingServiceMiddleware.test.ts │ │ ├── loggingServiceMiddleware.ts │ │ ├── performanceMiddleware.test.ts │ │ ├── performanceMiddleware.ts │ │ ├── requestLoggingMiddleware.test.ts │ │ ├── requestLoggingMiddleware.ts │ │ ├── serviceMiddleware.test.ts │ │ ├── serviceMiddleware.ts │ │ ├── storageMiddleware.test.ts │ │ ├── storageMiddleware.ts │ │ ├── telemetryMiddleware.test.ts │ │ ├── telemetryMiddleware.ts │ │ ├── validationMiddleware.test.ts │ │ └── validationMiddleware.ts │ ├── services │ │ ├── cloudService.ts │ │ ├── cloudStorage.ts │ │ ├── consoleLogger.test.ts │ │ ├── consoleLogger.ts │ │ ├── index.ts │ │ ├── logger.ts │ │ └── telemetry.ts │ └── test │ │ ├── cloudContextBuilder.test.ts │ │ ├── cloudContextBuilder.ts │ │ ├── index.ts │ │ ├── mockFactory.ts │ │ ├── testCloudService.ts │ │ ├── testCloudStorage.ts │ │ ├── testContext.ts │ │ ├── testModule.ts │ │ ├── testRequest.ts │ │ └── testResponse.ts └── tsconfig.json ├── docs ├── assets │ ├── ESLintInstall.gif │ ├── editor-config-config.gif │ ├── editor-config-install.png │ ├── editor-config-setup.png │ ├── editor-config-troubleshoot-delete.png │ ├── editor-config-troubleshoot-vsc.gif │ └── editor-config-troubleshoot.gif ├── editor-config.md ├── eslint.md ├── structure.md └── typescript.md ├── samples ├── aws-storage │ ├── .eslintrc.json │ ├── package-lock.json │ ├── package.json │ ├── src │ │ └── index.ts │ └── tsconfig.json ├── azure-cloud-request │ ├── .babelrc │ ├── .funcignore │ ├── .gitignore │ ├── .npmrc │ ├── host.json │ ├── jest.config.js │ ├── local.settings.json │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── function.json │ │ ├── index.ts │ │ └── middleware │ │ │ ├── authorizationMiddleware.test.ts │ │ │ ├── authorizationMiddleware.ts │ │ │ ├── index.ts │ │ │ ├── joiSupport.test.ts │ │ │ └── joiSupport.ts │ ├── test.http │ └── tsconfig.json ├── azure-http-middleware │ ├── .babelrc │ ├── .funcignore │ ├── .gitignore │ ├── .npmrc │ ├── host.json │ ├── jest.config.js │ ├── local.settings.json │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── function.json │ │ └── index.ts │ ├── test.http │ └── tsconfig.json ├── azure-storage │ ├── .eslintrc.json │ ├── package.json │ ├── src │ │ └── index.ts │ └── tsconfig.json ├── cloud-calls │ ├── .gitignore │ ├── .npmrc │ ├── call │ │ ├── function.json │ │ └── index.js │ ├── host.json │ ├── package-lock.json │ ├── package.json │ └── test.http ├── mongoose-connection │ ├── .babelrc │ ├── .funcignore │ ├── .gitignore │ ├── .npmrc │ ├── host.json │ ├── jest.config.js │ ├── local.settings.json │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── function.json │ │ ├── index.ts │ │ ├── middleware │ │ │ ├── databaseMiddleware.test.ts │ │ │ ├── databaseMiddleware.ts │ │ │ └── index.ts │ │ └── types │ │ │ └── custom.d.ts │ └── tsconfig.json └── sls-telemetry-middleware │ ├── .gitignore │ ├── .npmrc │ ├── hello │ ├── function.json │ └── handler.js │ ├── host.json │ ├── local.settings.json │ ├── package-lock.json │ ├── package.json │ ├── serverless-aws.yml │ ├── serverless-azure.yml │ └── webpack.config.js ├── scripts ├── build.sh ├── publish.sh └── version.sh └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | --- 5 | 6 | 14 | 15 | # This is a Bug Report 16 | 17 | ## Description 18 | 19 | - What went wrong? 20 | - What did you expect should have happened? 21 | - What was the config you used? 22 | - What stacktrace or error message from your provider did you see? 23 | 24 | Similar or dependent issues: 25 | 26 | - #12345 27 | 28 | ## Additional Data 29 | 30 | - **_Serverless Framework Version you're using_**: 31 | - **_Operating System_**: 32 | - **_Stack Trace_**: 33 | - **_Provider Error messages_**: 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for serverless framework 4 | --- 5 | 6 | 14 | 15 | # This is a Feature Proposal 16 | 17 | ## Description 18 | 19 | - What is the use case that should be solved. The more detail you describe this in the easier it is to understand for us. 20 | - If there is additional config how would it look 21 | 22 | Similar or dependent issues: 23 | 24 | - #12345 25 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | ## What did you implement: 8 | 9 | Closes #XXXXX 10 | 11 | 14 | 15 | ## How did you implement it: 16 | 17 | 20 | 21 | ## How can we verify it: 22 | 23 | 34 | 35 | ## Todos: 36 | 37 | _**Note: Run `npm run test-ci` to run all validation checks on proposed changes**_ 38 | 39 | - [ ] Write tests and confirm existing functionality is not broken. 40 | **Validate via `npm test`** 41 | - [ ] Write documentation 42 | - [ ] Ensure there are no lint errors. 43 | **Validate via `npm run lint-updated`** 44 | _Note: Some reported issues can be automatically fixed by running `npm run lint:fix`_ 45 | - [ ] Ensure introduced changes match Prettier formatting. 46 | **Validate via `npm run prettier-check-updated`** 47 | _Note: All reported issues can be automatically fixed by running `npm run prettify-updated`_ 48 | - [ ] Make sure code coverage hasn't dropped 49 | - [ ] Provide verification config / commands / resources 50 | - [ ] Enable "Allow edits from maintainers" for this PR 51 | - [ ] Update the messages below 52 | 53 | **_Is this ready for review?:_** NO 54 | **_Is it a breaking change?:_** NO 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/node,visualstudiocode 2 | # Edit at https://www.gitignore.io/?templates=node,visualstudiocode 3 | 4 | ### Node ### 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | 28 | # lib folder 29 | */lib/**/* 30 | 31 | # nyc test coverage 32 | .nyc_output 33 | 34 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 35 | .grunt 36 | 37 | # Bower dependency directory (https://bower.io/) 38 | bower_components 39 | 40 | # node-waf configuration 41 | .lock-wscript 42 | 43 | # Compiled binary addons (https://nodejs.org/api/addons.html) 44 | build/Release 45 | lib 46 | 47 | # Dependency directories 48 | node_modules/ 49 | jspm_packages/ 50 | 51 | # TypeScript v1 declaration files 52 | typings/ 53 | 54 | # Optional npm cache directory 55 | .npm 56 | 57 | # Optional eslint cache 58 | .eslintcache 59 | 60 | # Optional REPL history 61 | .node_repl_history 62 | 63 | # Output of 'npm pack' 64 | *.tgz 65 | 66 | # Yarn Integrity file 67 | .yarn-integrity 68 | 69 | # dotenv environment variables file 70 | .env 71 | .env.test 72 | 73 | # parcel-bundler cache (https://parceljs.org/) 74 | .cache 75 | 76 | # next.js build output 77 | .next 78 | 79 | # nuxt.js build output 80 | .nuxt 81 | 82 | # vuepress build output 83 | .vuepress/dist 84 | 85 | # Serverless directories 86 | .serverless/ 87 | 88 | # FuseBox cache 89 | .fusebox/ 90 | 91 | # DynamoDB Local files 92 | .dynamodb/ 93 | 94 | ### VisualStudioCode ### 95 | .vscode/* 96 | !.vscode/settings.json 97 | !.vscode/tasks.json 98 | !.vscode/launch.json 99 | !.vscode/extensions.json 100 | 101 | ### VisualStudioCode Patch ### 102 | # Ignore all local history of files 103 | .history 104 | 105 | # other 106 | ignore 107 | bin 108 | obj 109 | 110 | # End of https://www.gitignore.io/api/node,visualstudiocode 111 | 112 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.validate": [ 3 | "javascript", 4 | "typescript" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Serverless, Inc. http://www.serverless.com 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /NPM.md: -------------------------------------------------------------------------------- 1 | # Configure NPM 2 | 3 | The middleware and common serverless components are currently published to a private NPM repository, hosted on [Azure DevOps](https://dev.azure.com/711digital/ServerlessApps/_packaging?_a=feed&feed=common-packages). 4 | 5 | ## Common Packages 6 | The following scoped common packages are available: 7 | 8 | ```json 9 | "dependencies": { 10 | "@multicloud/sls-core": "latest", 11 | "@multicloud/sls-aws": "latest", 12 | "@multicloud/sls-azure": "latest, 13 | } 14 | ``` 15 | 16 | In order to fetch any common packages via `npm i`, you must first provision and configure a token. 17 | 18 | ## NPM Authentication Token 19 | In order to access (read or write) to/from this NPM reposistory, you will need to: 20 | 21 | 1. Provision a [Personal Access Token (PAT)](https://docs.microsoft.com/en-us/azure/devops/artifacts/npm/npmrc?view=azure-devops&tabs=windows) on the [Azure DevOps portal](https://dev.azure.com/711digital/_usersSettings/tokens), under Profile --> Security --> Personal access tokens 22 | * Tokens only require `Packaging (read & write)` scope 23 | * Tokens have a max expiry of one year (there is no support for non-expiring tokens) 24 | 2. Prior to running any NPM commands, be sure to set `NPM_TOKEN` as an environment variable, using the base64 encoded PAT token that was previously created. 25 | 26 | ## FAQ 27 | **Q:** I'm getting this error: `Error: Failed to replace env in config: ${NPM_TOKEN}` 28 | 29 | **A**: You will see this error if you run _any_ NPM command without first setting the `NPM_TOKEN` environment variable 30 | 31 | ----------- 32 | 33 | **Q:** I'm getting a `401 (Unauthorized)` when running NPM commands, with the `NPM_TOKEN` environment variable set. 34 | 35 | **A:** Make sure you successfully provisioned your Personal Access Token (PAT) on the Azure DevOps portal and make sure environment variable was set and base64 encoded. 36 | -------------------------------------------------------------------------------- /aws/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "babel-plugin-transform-typescript-metadata" 4 | ], 5 | "presets": [ 6 | [ 7 | "react-app", 8 | { 9 | "flow": false, 10 | "typescript": true 11 | } 12 | ] 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /aws/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "project": "./tsconfig.json" 5 | }, 6 | "plugins": ["@typescript-eslint"], 7 | "extends": ["plugin:@typescript-eslint/recommended"], 8 | "rules": { 9 | "quotes": ["error", "double"], 10 | "@typescript-eslint/indent": ["error", 2], 11 | "@typescript-eslint/no-explicit-any": 0, 12 | "@typescript-eslint/explicit-function-return-type": 0, 13 | "@typescript-eslint/no-parameter-properties": 0, 14 | "@typescript-eslint/no-use-before-define": 0, 15 | "@typescript-eslint/no-object-literal-type-assertion": 0 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /aws/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Serverless, Inc. http://www.serverless.com 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /aws/README.md: -------------------------------------------------------------------------------- 1 | # Serverless Multicloud Library for Amazon Web Services 2 | [![Build Status](https://dev.azure.com/serverless-inc/multicloud/_apis/build/status/CI/%5Bsls-aws%5D%20ci?branchName=dev)](https://dev.azure.com/serverless-inc/multicloud/_build/latest?definitionId=1&branchName=dev) 3 | [![npm (scoped)](https://img.shields.io/npm/v/@multicloud/sls-aws)](https://www.npmjs.com/package/@multicloud/sls-aws) 4 | 5 | The Serverless @multicloud library provides an easy way to build Serverless handlers in NodeJS using a cloud agnostic library that can then be deployed to support cloud providers. 6 | 7 | In addition to a normalized API the @multicloud library supports reusable middleware pipeline similar to the Express framework 8 | 9 | ## Installation 10 | ```bash 11 | npm install @multicloud/sls-aws --save 12 | ``` 13 | 14 | ## Example 15 | ```javascript 16 | const { App } = require("@multicloud/sls-core"); 17 | const { AwsModule } = require("@multicloud/sls-aws"); 18 | const app = new App(new AwsModule()); 19 | 20 | module.exports.handler = app.use([], async (context) => { 21 | const { req } = context; 22 | const name = req.query.get("name"); 23 | 24 | if (name) { 25 | context.send(`Hello ${name}`, 200); 26 | } 27 | else { 28 | context.send("Please pass a name on the query string or in the request body", 400); 29 | } 30 | }); 31 | ``` 32 | 33 | ## Contributing 34 | 35 | ### Testing 36 | Run Jest unit tests 37 | ```bash 38 | npm run test 39 | ``` 40 | 41 | ### Building 42 | Runs the TypeScript compiler and ouputs to the `lib` folder 43 | ```bash 44 | npm run build 45 | ``` 46 | 47 | ## Contributing 48 | ### [Code of Conduct](../CODE_OF_CONDUCT.md) 49 | In the interest of fostering an open and welcoming environment, we as 50 | contributors and maintainers pledge to making participation in our project and 51 | our community a harassment-free experience for everyone, regardless of age, body 52 | size, disability, ethnicity, gender identity and expression, level of experience, 53 | nationality, personal appearance, race, religion, or sexual identity and 54 | orientation. 55 | 56 | ### [Contriubtion Guidelines](../CONTRIBUTING.md) 57 | Welcome, and thanks in advance for your help! Please follow these simple guidelines :+1: 58 | 59 | ## Licensing 60 | 61 | Serverless is licensed under the [MIT License](./LICENSE.txt). 62 | 63 | All files located in the node_modules and external directories are externally maintained libraries used by this software which have their own licenses; we recommend you read them, as their terms may differ from the terms in the MIT License. 64 | -------------------------------------------------------------------------------- /aws/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "collectCoverageFrom": [ 3 | "src/**/*.{js,jsx,ts,tsx}", 4 | "!src/**/*.d.ts" 5 | ], 6 | "testEnvironment": "node", 7 | "transform": { 8 | "^.+\\.(js|jsx|ts|tsx)$": "babel-jest" 9 | }, 10 | "testPathIgnorePatterns": [ 11 | "./lib", 12 | "./node_modules" 13 | ], 14 | "transformIgnorePatterns": [ 15 | "./lib", 16 | "./node_modules" 17 | ], 18 | "moduleFileExtensions": [ 19 | "js", 20 | "ts", 21 | "tsx", 22 | "json", 23 | "jsx", 24 | "node" 25 | ] 26 | }; -------------------------------------------------------------------------------- /aws/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@multicloud/sls-aws", 3 | "version": "0.1.1", 4 | "description": "Amazon AWS specific middleware and components for Serverless @multicloud.", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/serverless/multicloud" 8 | }, 9 | "keywords": [ 10 | "serverless", 11 | "sls", 12 | "serverless framework", 13 | "multicloud", 14 | "aws", 15 | "aws lambda" 16 | ], 17 | "main": "lib/index.js", 18 | "types": "lib/index.d.ts", 19 | "scripts": { 20 | "start": "npm run test -- --watch", 21 | "lint": "eslint src/**/*.ts", 22 | "lint:fix": "npm run lint -- --fix", 23 | "pretest": "npm run lint", 24 | "test": "jest", 25 | "test:ci": "npm run test -- --ci", 26 | "test:coverage": "npm run test -- --coverage", 27 | "prebuild": "shx rm -rf lib/ && npm run test", 28 | "build": "npx tsc" 29 | }, 30 | "files": [ 31 | "lib/" 32 | ], 33 | "author": "Microsoft Corporation, 7-Eleven & Serverless Inc", 34 | "license": "MIT", 35 | "dependencies": { 36 | "@multicloud/sls-core": "^0.1.1-13", 37 | "aws-sdk": "2.476.0", 38 | "inversify": "5.0.1", 39 | "reflect-metadata": "0.1.13" 40 | }, 41 | "devDependencies": { 42 | "@types/jest": "^24.0.13", 43 | "@types/node": "12.0.8", 44 | "@typescript-eslint/eslint-plugin": "^1.9.0", 45 | "@typescript-eslint/parser": "^1.9.0", 46 | "babel-jest": "^24.8.0", 47 | "babel-plugin-transform-typescript-metadata": "0.2.2", 48 | "babel-preset-react-app": "^9.0.0", 49 | "eslint": "^5.16.0", 50 | "jest": "^24.8.0", 51 | "shx": "^0.3.2", 52 | "typescript": "^3.5.2" 53 | }, 54 | "engines": { 55 | "node": ">=8.16.0", 56 | "npm": ">=6.4.1" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /aws/pipelines/ci.yml: -------------------------------------------------------------------------------- 1 | # Node.js 2 | # Build a general Node.js project with npm. 3 | # Add steps that analyze code, save build artifacts, deploy, and more: 4 | # https://docs.microsoft.com/azure/devops/pipelines/languages/javascript 5 | 6 | # CI should only run when PRs are opened against dev/master. When integrating AZDo repos with 7 | # AZDo pipelines, the only way to do this is through `build validations` - branch-specific 8 | # integrations that can only be configured via AzDO UI. 9 | trigger: none 10 | 11 | pr: 12 | - master 13 | - dev 14 | 15 | pool: 16 | vmImage: 'ubuntu-latest' 17 | 18 | steps: 19 | - task: NodeTool@0 20 | displayName: 'Use Node 10.x' 21 | inputs: 22 | versionSpec: 10.x 23 | 24 | - task: Bash@3 25 | displayName: 'Build sls-aws' 26 | inputs: 27 | targetType: filePath 28 | filePath: ./scripts/build.sh 29 | workingDirectory: aws 30 | env: 31 | NPM_TOKEN: $(NPM_TOKEN) 32 | -------------------------------------------------------------------------------- /aws/pipelines/publish-prerelease.yml: -------------------------------------------------------------------------------- 1 | # Node.js 2 | # Build a general Node.js project with npm. 3 | # Add steps that analyze code, save build artifacts, deploy, and more: 4 | # https://docs.microsoft.com/azure/devops/pipelines/languages/javascript 5 | 6 | # Only publish to AzDO NPM when a tag is pushed to master. 7 | # NOTE: a PR needs to be opened from dev --> master, which includes version bump. 8 | # If needs be, we can eventually automate. 9 | trigger: 10 | branches: 11 | include: 12 | - dev 13 | paths: 14 | include: 15 | - aws/* 16 | 17 | pr: none 18 | 19 | pool: 20 | vmImage: 'ubuntu-latest' 21 | 22 | variables: 23 | - group: GitHub-Deploy-Creds 24 | - group: npm-release-credentials 25 | 26 | steps: 27 | - task: NodeTool@0 28 | displayName: 'Use Node 10.x' 29 | inputs: 30 | versionSpec: 10.x 31 | 32 | # Download secure file 33 | # Download a secure file to the agent machine 34 | - task: DownloadSecureFile@1 35 | # name: sshKey # The name with which to reference the secure file's path on the agent, like $(mySecureFile.secureFilePath) 36 | inputs: 37 | secureFile: multicloud_id_rsa 38 | 39 | # Install an SSH key prior to a build or deployment 40 | - task: InstallSSHKey@0 # https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/utility/install-ssh-key?view=azure-devops 41 | inputs: 42 | knownHostsEntry: $(KNOWN_HOSTS_ENTRY) 43 | sshPublicKey: $(SSH_PUBLIC_KEY) 44 | #sshPassphrase: $(SSH_PASS_PHRASE) 45 | sshKeySecureFile: multicloud_id_rsa 46 | env: 47 | KNOWN_HOSTS_ENTRY: $(KNOWN_HOSTS_ENTRY) 48 | SSH_PUBLIC_KEY: $(SSH_PUBLIC_KEY) # map to the right format (camelCase) that Azure credentials understand 49 | 50 | - task: Bash@3 51 | name: BumpNpmVersion 52 | displayName: Bump NPM Prerelease Version 53 | inputs: 54 | targetType: filePath 55 | filePath: ./scripts/version.sh 56 | arguments: '@multicloud/sls-aws' 57 | workingDirectory: aws 58 | env: 59 | SOURCE_BRANCH: $(Build.SourceBranch) 60 | 61 | - task: Bash@3 62 | displayName: 'Publish sls-aws to NPM' 63 | inputs: 64 | targetType: filePath 65 | filePath: ./scripts/publish.sh 66 | arguments: 'prerelease' 67 | workingDirectory: aws 68 | env: 69 | NPM_TOKEN: $(NPM_TOKEN) 70 | -------------------------------------------------------------------------------- /aws/pipelines/publish-release.yml: -------------------------------------------------------------------------------- 1 | # Node.js 2 | # Build a general Node.js project with npm. 3 | # Add steps that analyze code, save build artifacts, deploy, and more: 4 | # https://docs.microsoft.com/azure/devops/pipelines/languages/javascript 5 | 6 | # Only publish to AzDO NPM when a tag is pushed to master. 7 | # NOTE: a PR needs to be opened from dev --> master, which includes version bump. 8 | # If needs be, we can eventually automate. 9 | trigger: none 10 | 11 | pr: none 12 | 13 | pool: 14 | vmImage: 'ubuntu-latest' 15 | 16 | variables: 17 | - group: GitHub-Deploy-Creds 18 | - group: npm-release-credentials 19 | 20 | steps: 21 | - task: NodeTool@0 22 | displayName: 'Use Node 10.x' 23 | inputs: 24 | versionSpec: 10.x 25 | 26 | # Download secure file 27 | # Download a secure file to the agent machine 28 | - task: DownloadSecureFile@1 29 | # name: sshKey # The name with which to reference the secure file's path on the agent, like $(mySecureFile.secureFilePath) 30 | inputs: 31 | secureFile: multicloud_id_rsa 32 | 33 | # Install an SSH key prior to a build or deployment 34 | - task: InstallSSHKey@0 # https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/utility/install-ssh-key?view=azure-devops 35 | inputs: 36 | knownHostsEntry: $(KNOWN_HOSTS_ENTRY) 37 | sshPublicKey: $(SSH_PUBLIC_KEY) 38 | #sshPassphrase: $(SSH_PASS_PHRASE) 39 | sshKeySecureFile: multicloud_id_rsa 40 | env: 41 | KNOWN_HOSTS_ENTRY: $(KNOWN_HOSTS_ENTRY) 42 | SSH_PUBLIC_KEY: $(SSH_PUBLIC_KEY) # map to the right format (camelCase) that Azure credentials understand 43 | 44 | - task: Bash@3 45 | name: BumpNpmVersion 46 | displayName: Bump NPM Prerelease Version 47 | inputs: 48 | targetType: filePath 49 | filePath: ./scripts/version.sh 50 | arguments: '@multicloud/sls-aws $(NPM_RELEASE_TYPE)' 51 | workingDirectory: aws 52 | env: 53 | SOURCE_BRANCH: $(Build.SourceBranch) 54 | 55 | - task: Bash@3 56 | displayName: 'Publish sls-aws to NPM' 57 | inputs: 58 | targetType: filePath 59 | filePath: ./scripts/publish.sh 60 | workingDirectory: aws 61 | env: 62 | NPM_TOKEN: $(NPM_TOKEN) 63 | -------------------------------------------------------------------------------- /aws/src/awsContext.test.ts: -------------------------------------------------------------------------------- 1 | import { AwsContext } from "."; 2 | import { AwsResponse } from "./awsResponse"; 3 | import awsEvent from "./test/events/defaultAwsEvent.json"; 4 | 5 | 6 | describe("AWS context", () => { 7 | const awsContext = { 8 | awsRequestId: "12345", 9 | req: {}, 10 | res: {}, 11 | }; 12 | 13 | function createAwsContext(event, context, callback = jest.fn()) { 14 | var awsContext = new AwsContext([event, context, callback]); 15 | awsContext.res = new AwsResponse(awsContext); 16 | awsContext.done = jest.fn(); 17 | 18 | return awsContext; 19 | } 20 | 21 | it("context id should be set", async () => { 22 | const emptyAWSEvent = {}; 23 | const sut = createAwsContext(emptyAWSEvent, awsContext); 24 | expect(sut.id).toEqual(awsContext.awsRequestId); 25 | }); 26 | 27 | it("send() calls response.send() on httpTrigger", () => { 28 | const body = { message: "Hello World" }; 29 | const context = createAwsContext(awsEvent, awsContext); 30 | context.res = new AwsResponse(context); 31 | context.res.send = jest.fn(); 32 | context.send(body); 33 | 34 | expect(context.res.send).toBeCalledWith(body, 200); 35 | }); 36 | 37 | it("send() calls response.send() on httpTrigger with custom status", () => { 38 | const body = { message: "oh Crap!" }; 39 | const context = createAwsContext(awsEvent, awsContext); 40 | context.res = new AwsResponse(context); 41 | context.res.send = jest.fn(); 42 | context.send(body, 400); 43 | 44 | expect(context.res.send).toHaveBeenCalledWith(body, 400); 45 | }); 46 | 47 | it("send() calls context.done() to signal handler is complete", () => { 48 | const context = createAwsContext(awsEvent, awsContext); 49 | context.send("test", 200); 50 | 51 | expect(context.done).toBeCalled(); 52 | }); 53 | 54 | it("flush() calls response.flush() to call final AWS callback", () => { 55 | const context = createAwsContext(awsEvent, awsContext); 56 | const flushSpy = jest.spyOn(context.res, "flush"); 57 | context.send("test", 200); 58 | context.flush(); 59 | 60 | expect(flushSpy).toBeCalled(); 61 | }); 62 | 63 | it("binds event property to incoming event argument", () => { 64 | const context = createAwsContext(awsEvent, awsContext); 65 | expect(context.event).toBe(awsEvent); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /aws/src/awsContext.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | import { AwsRequest, AwsResponse } from "."; 3 | import { CloudContext, ComponentType } from "@multicloud/sls-core"; 4 | import { injectable, inject } from "inversify"; 5 | import { AwsLambdaRuntime } from "./models/awsLamda"; 6 | 7 | /** 8 | * Implementation of Cloud Context for AWS Lambda 9 | */ 10 | @injectable() 11 | export class AwsContext implements CloudContext { 12 | /** 13 | * Initializes new AwsContext, injects runtime arguments of AWS Lambda. 14 | * Sets runtime parameters from original arguments 15 | * @param args Runtime arguments for AWS Lambda 16 | */ 17 | public constructor(@inject(ComponentType.RuntimeArgs) args: any[]) { 18 | this.providerType = "aws"; 19 | 20 | this.runtime = { 21 | event: args[0], 22 | context: args[1], 23 | callback: args[2], 24 | }; 25 | 26 | this.id = this.runtime.context.awsRequestId; 27 | 28 | // AWS has a single incoming event source 29 | this.event = this.runtime.event; 30 | } 31 | 32 | /** "aws" */ 33 | public providerType: string; 34 | /** Unique identifier for request */ 35 | public id: string; 36 | /** The incoming event source */ 37 | public event: any; 38 | /** HTTP Request */ 39 | public req: AwsRequest; 40 | /** HTTP Response */ 41 | public res: AwsResponse; 42 | /** Original runtime arguments for AWS Lambda */ 43 | public runtime: AwsLambdaRuntime; 44 | /** Signals to the framework that the request is complete */ 45 | public done: () => void; 46 | 47 | /** 48 | * Send response from AWS Lambda 49 | * @param body Body of response 50 | * @param status Status code of response 51 | */ 52 | public send(body: any, status: number = 200): void { 53 | if (this.res) { 54 | this.res.send(body, status); 55 | } 56 | 57 | this.done(); 58 | } 59 | 60 | public flush() { 61 | if (this.res) { 62 | this.res.flush(); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /aws/src/awsModule.ts: -------------------------------------------------------------------------------- 1 | import { CloudModule, CloudContext, CloudRequest, CloudResponse, ComponentType, CloudStorage, CloudService } from "@multicloud/sls-core"; 2 | import { ContainerModule, interfaces } from "inversify"; 3 | import { AwsContext, AwsRequest, AwsResponse, S3Storage } from "."; 4 | import { LambdaCloudService } from "./services"; 5 | 6 | /** 7 | * AWS Module that can be registered in IoC container 8 | */ 9 | export class AwsModule implements CloudModule { 10 | 11 | /** 12 | * Determines whether or not the incoming request is an AWS request 13 | * @param req The IoC resolution request 14 | */ 15 | private isAwsRequest(req: interfaces.Request) { 16 | const runtimeArgs = req.parentContext.container.get(ComponentType.RuntimeArgs); 17 | return runtimeArgs && runtimeArgs[1].awsRequestId; 18 | }; 19 | 20 | /** 21 | * Creates the inversify container module 22 | */ 23 | public create() { 24 | return new ContainerModule((bind) => { 25 | bind(ComponentType.CloudContext) 26 | .to(AwsContext) 27 | .inSingletonScope() 28 | .when(this.isAwsRequest); 29 | 30 | bind(ComponentType.CloudRequest) 31 | .to(AwsRequest) 32 | .when(this.isAwsRequest); 33 | 34 | bind(ComponentType.CloudResponse) 35 | .to(AwsResponse) 36 | .when(this.isAwsRequest); 37 | 38 | bind(ComponentType.CloudService) 39 | .to(LambdaCloudService) 40 | .when(this.isAwsRequest); 41 | 42 | bind(ComponentType.CloudStorage) 43 | .to(S3Storage) 44 | .when(this.isAwsRequest); 45 | }); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /aws/src/awsRequest.test.ts: -------------------------------------------------------------------------------- 1 | import { AwsContext, AwsRequest } from "."; 2 | import awsEvent from "./test/events/defaultAwsEvent.json"; 3 | import { StringParams } from "@multicloud/sls-core"; 4 | 5 | describe("test of request", () => { 6 | const context = { 7 | requestId: "12345", 8 | req: {}, 9 | res: {} 10 | } 11 | 12 | it("should pass-through event values without modifications", () => { 13 | awsEvent.body = JSON.stringify(awsEvent.body); 14 | const request = new AwsRequest(new AwsContext([awsEvent, context, null])); 15 | expect(request.method).toEqual(awsEvent.httpMethod); 16 | expect(request.headers).toEqual(new StringParams(awsEvent.headers)); 17 | expect(request.query).toEqual(new StringParams(awsEvent.queryStringParameters)); 18 | expect(request.body).toEqual(JSON.parse(awsEvent.body)); 19 | }); 20 | 21 | it("should use default value for body if not provided", () => { 22 | const noBodyEvent = Object.assign({}, awsEvent); 23 | delete noBodyEvent.body; 24 | const request = new AwsRequest(new AwsContext([noBodyEvent, context, null])); 25 | expect(request.method).toEqual(awsEvent.httpMethod); 26 | expect(request.headers).toEqual(new StringParams(awsEvent.headers)); 27 | expect(request.query).toEqual(new StringParams(awsEvent.queryStringParameters)); 28 | expect(request.body).toEqual(null); 29 | }); 30 | 31 | it("should use default value for headers if not provided", () => { 32 | const noHeadersEvent = Object.assign({}, awsEvent); 33 | delete noHeadersEvent.headers; 34 | noHeadersEvent.body = JSON.stringify(noHeadersEvent.body); 35 | const request = new AwsRequest(new AwsContext([noHeadersEvent, context, null])); 36 | expect(request.method).toEqual(awsEvent.httpMethod); 37 | expect(request.headers).toEqual(new StringParams()); 38 | expect(request.query).toEqual(new StringParams(awsEvent.queryStringParameters)); 39 | expect(request.body).toEqual(JSON.parse(noHeadersEvent.body)); 40 | }); 41 | 42 | it("should use default value for query if not provided", () => { 43 | const noQueryEvent = Object.assign({}, awsEvent); 44 | delete noQueryEvent.queryStringParameters; 45 | noQueryEvent.body = JSON.stringify(noQueryEvent.body); 46 | const request = new AwsRequest(new AwsContext([noQueryEvent, context, null])); 47 | expect(request.method).toEqual(awsEvent.httpMethod); 48 | expect(request.headers).toEqual(new StringParams(awsEvent.headers)); 49 | expect(request.query).toEqual(new StringParams()); 50 | expect(request.body).toEqual(JSON.parse(noQueryEvent.body)); 51 | }); 52 | 53 | it("should set context defaults if context content are empty objects", () => { 54 | const emptyParams = new StringParams(); 55 | const emptyAWSEvent = { 56 | httpMethod: "GET" 57 | }; 58 | 59 | const request = new AwsRequest(new AwsContext([emptyAWSEvent, context, null])); 60 | 61 | expect(request.method).toEqual("GET"); 62 | expect(request.headers).toEqual(emptyParams); 63 | expect(request.query).toEqual(emptyParams); 64 | expect(request.body).toEqual(null); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /aws/src/awsRequest.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | import { CloudRequest, ComponentType, StringParams } from "@multicloud/sls-core"; 3 | import { AwsContext } from "."; 4 | import { injectable, inject } from "inversify"; 5 | 6 | /** 7 | * Implementation of Cloud Request for AWS Lambda 8 | */ 9 | @injectable() 10 | export class AwsRequest implements CloudRequest { 11 | /** Body of HTTP Request */ 12 | public body?: any; 13 | /** Headers of HTTP Request */ 14 | public headers?: StringParams; 15 | /** HTTP method of request */ 16 | public method: string; 17 | /** Query params of HTTP request */ 18 | public query?: StringParams; 19 | /** Path params of HTTP Request */ 20 | public pathParams?: StringParams; 21 | 22 | /** 23 | * Initialize new AWS Request, injecting cloud context 24 | * @param context Current CloudContext 25 | */ 26 | public constructor(@inject(ComponentType.CloudContext) context: AwsContext) { 27 | const body = typeof (context.runtime.event.body) === "string" 28 | ? JSON.parse(context.runtime.event.body) 29 | : context.runtime.event.body; 30 | 31 | this.method = context.runtime.event.httpMethod; 32 | this.body = body || null; 33 | this.headers = new StringParams(context.runtime.event.headers); 34 | this.query = new StringParams(context.runtime.event.queryStringParameters); 35 | this.pathParams = new StringParams(context.runtime.event.pathParameters); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /aws/src/awsResponse.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | import { 3 | CloudResponse, 4 | ComponentType, 5 | ProviderType, 6 | CloudProviderResponseHeader, 7 | StringParams 8 | } from "@multicloud/sls-core"; 9 | import { AwsContext } from "."; 10 | import { injectable, inject } from "inversify"; 11 | 12 | /** 13 | * Implementation of Cloud Response for AWS Lambda 14 | */ 15 | @injectable() 16 | export class AwsResponse implements CloudResponse { 17 | /** The AWS runtime callback */ 18 | private callback: Function; 19 | 20 | /** The HTTP response body */ 21 | public body: any; 22 | 23 | /** The HTTP response status code */ 24 | public status: number = 200; 25 | 26 | /** Headers of HTTP Response */ 27 | public headers?: StringParams = new StringParams(); 28 | 29 | /** 30 | * Initialize new AWS Response, injecting Cloud Context 31 | * @param context Current CloudContext 32 | */ 33 | public constructor(@inject(ComponentType.CloudContext) context: AwsContext) { 34 | this.headers.set(CloudProviderResponseHeader, ProviderType.AWS); 35 | this.callback = context.runtime.callback; 36 | } 37 | 38 | /** 39 | * Send HTTP response via provided callback 40 | * @param body Body of HTTP response 41 | * @param status Status code of HTTP response 42 | * @param callback Callback function to call with response 43 | */ 44 | public send(body: any, status: number = 200): void { 45 | const responseBody = typeof (body) !== "string" 46 | ? JSON.stringify(body) 47 | : body; 48 | 49 | this.body = responseBody; 50 | this.status = status; 51 | 52 | if (!body) { 53 | return; 54 | } 55 | 56 | const bodyType = body.constructor.name; 57 | 58 | if (["Object", "Array"].includes(bodyType)) { 59 | this.headers.set("Content-Type", "application/json"); 60 | } 61 | 62 | if (["String"].includes(bodyType)) { 63 | this.headers.set("Content-Type", "text/html"); 64 | } 65 | } 66 | 67 | public flush(): void { 68 | this.callback(null, { 69 | headers: this.headers.toJSON(), 70 | body: this.body, 71 | statusCode: this.status || 200, 72 | }); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /aws/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./awsModule"; 2 | export * from "./awsContext"; 3 | export * from "./awsRequest"; 4 | export * from "./awsResponse"; 5 | export * from "./services"; 6 | export * from "./middleware"; 7 | export * from "./models/awsLamda"; 8 | -------------------------------------------------------------------------------- /aws/src/middleware/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./simpleQueueMiddleware"; 2 | export * from "./simpleStorageMiddleware"; 3 | export * from "./kinesisMiddleware"; 4 | -------------------------------------------------------------------------------- /aws/src/middleware/kinesisMiddleware.test.ts: -------------------------------------------------------------------------------- 1 | import { CloudContext, Middleware } from "@multicloud/sls-core"; 2 | import { AwsContext } from "../awsContext"; 3 | import { KinesisMiddleware } from "."; 4 | 5 | describe("Kinesis Middleware", () => { 6 | let middleware: Middleware; 7 | 8 | beforeEach(() => { 9 | middleware = KinesisMiddleware(); 10 | }); 11 | 12 | it("only runs for AWS requests", async () => { 13 | const next = jest.fn(); 14 | const originalEvent = {}; 15 | const context: CloudContext = { 16 | id: "abc123", 17 | providerType: "azure", 18 | event: originalEvent, 19 | done: jest.fn(), 20 | send: jest.fn(), 21 | flush: jest.fn(), 22 | }; 23 | 24 | await middleware(context, next); 25 | 26 | expect(context.event).toBe(originalEvent); 27 | expect(next).toBeCalled(); 28 | }); 29 | 30 | it("transforms AWS events into normalized Cloud Messages", async () => { 31 | const originalEvent: any = { 32 | Records: [ 33 | { 34 | kinesis: { 35 | partitionKey: "partitionKey-03", 36 | kinesisSchemaVersion: "1.0", 37 | data: "SGVsbG8sIHRoaXMgaXMgYSB0ZXN0IDEyMy4=", 38 | sequenceNumber: "49545115243490985018280067714973144582180062593244200961", 39 | approximateArrivalTimestamp: 1428537600 40 | }, 41 | eventSource: "aws:kinesis", 42 | eventID: "shardId-000000000000:49545115243490985018280067714973144582180062593244200961", 43 | invokeIdentityArn: "arn:aws:iam::EXAMPLE", 44 | eventVersion: "1.0", 45 | eventName: "aws:kinesis:record", 46 | eventSourceARN: "arn:aws:kinesis:EXAMPLE", 47 | awsRegion: "us-east-1" 48 | } 49 | ] 50 | }; 51 | const runtimeArgs = [ 52 | originalEvent, 53 | { 54 | awsRequestId: "ID123", 55 | }, 56 | ]; 57 | 58 | const next = jest.fn(); 59 | const context = new AwsContext(runtimeArgs); 60 | await middleware(context, next); 61 | 62 | expect(context.event).toEqual({ 63 | records: [{ 64 | id: originalEvent.Records[0].eventID, 65 | body: originalEvent.Records[0].kinesis.data, 66 | partitionKey: originalEvent.Records[0].kinesis.partitionKey, 67 | sequenceNumber: originalEvent.Records[0].kinesis.sequenceNumber, 68 | eventSourceARN: originalEvent.Records[0].kinesis.eventSourceARN, 69 | timestamp: expect.any(Date), 70 | eventSource: "aws:kinesis", 71 | }] 72 | }) 73 | expect(next).toBeCalled(); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /aws/src/middleware/kinesisMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { CloudMessage, Middleware } from "@multicloud/sls-core"; 2 | import { AwsContext } from ".."; 3 | 4 | /** 5 | * Normalizes SQS messages into a generic records array 6 | */ 7 | export const KinesisMiddleware = (): Middleware => async (context: AwsContext, next: () => Promise) => { 8 | if (context instanceof AwsContext) { 9 | const records: CloudMessage[] = context.runtime.event.Records.map((message) => ({ 10 | id: message.eventID, 11 | body: message.kinesis.data, 12 | sequenceNumber: message.kinesis.sequenceNumber, 13 | partitionKey: message.kinesis.partitionKey, 14 | timestamp: new Date(message.kinesis.approximateArrivalTimestamp), 15 | eventSource: "aws:kinesis", 16 | eventSourceARN: message.kinesis.eventSourceARN 17 | })); 18 | 19 | context.event = { records }; 20 | } 21 | 22 | await next(); 23 | }; 24 | -------------------------------------------------------------------------------- /aws/src/middleware/simpleQueueMiddleware.test.ts: -------------------------------------------------------------------------------- 1 | import { CloudContext, Middleware } from "@multicloud/sls-core"; 2 | import { SimpleQueueMiddleware } from "."; 3 | import { AwsContext } from "../awsContext"; 4 | 5 | describe("Simple Queue Middleware", () => { 6 | let middleware: Middleware; 7 | 8 | beforeEach(() => { 9 | middleware = SimpleQueueMiddleware(); 10 | }); 11 | 12 | it("only runs for AWS requests", async () => { 13 | const next = jest.fn(); 14 | const originalEvent = {}; 15 | const context: CloudContext = { 16 | id: "abc123", 17 | providerType: "azure", 18 | event: originalEvent, 19 | done: jest.fn(), 20 | send: jest.fn(), 21 | flush: jest.fn(), 22 | }; 23 | 24 | await middleware(context, next); 25 | 26 | expect(context.event).toBe(originalEvent); 27 | expect(next).toBeCalled(); 28 | }); 29 | 30 | it("transforms AWS events into normalized Cloud Messages", async () => { 31 | const originalEvent = { 32 | Records: [ 33 | { 34 | messageId: "19dd0b57-b21e-4ac1-bd88-01bbb068cb78", 35 | receiptHandle: "MessageReceiptHandle", 36 | body: "Hello from SQS!", 37 | attributes: { 38 | ApproximateReceiveCount: "1", 39 | SentTimestamp: "1523232000000", 40 | SenderId: "123456789012", 41 | ApproximateFirstReceiveTimestamp: "1523232000001" 42 | }, 43 | messageAttributes: {}, 44 | md5OfBody: "7b270e59b47ff90a553787216d55d91d", 45 | eventSource: "aws:sqs", 46 | eventSourceARN: "arn:aws:sqs:us-east-1:123456789012:MyQueue", 47 | awsRegion: "us-east-1" 48 | } 49 | ] 50 | }; 51 | const runtimeArgs = [ 52 | originalEvent, 53 | { 54 | awsRequestId: "ID123", 55 | }, 56 | ]; 57 | 58 | const next = jest.fn(); 59 | const context = new AwsContext(runtimeArgs); 60 | await middleware(context, next); 61 | 62 | expect(context.event).toEqual({ 63 | records: [{ 64 | id: originalEvent.Records[0].messageId, 65 | body: originalEvent.Records[0].body, 66 | timestamp: expect.any(Date), 67 | eventSource: "aws:sqs", 68 | }] 69 | }) 70 | expect(next).toBeCalled(); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /aws/src/middleware/simpleQueueMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { CloudMessage, Middleware } from "@multicloud/sls-core"; 2 | import { AwsContext } from ".."; 3 | 4 | /** 5 | * Normalizes SQS messages into a generic records array 6 | */ 7 | export const SimpleQueueMiddleware = (): Middleware => async (context: AwsContext, next: () => Promise) => { 8 | if (context instanceof AwsContext) { 9 | const records: CloudMessage[] = context.runtime.event.Records.map((message) => ({ 10 | id: message.messageId, 11 | body: message.body, 12 | timestamp: new Date(message.attributes.SentTimestamp), 13 | eventSource: "aws:sqs", 14 | })); 15 | 16 | context.event = { records }; 17 | } 18 | 19 | await next(); 20 | }; 21 | -------------------------------------------------------------------------------- /aws/src/middleware/simpleStorageMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { CloudMessage, Middleware, CloudContext, ConsoleLogger } from "@multicloud/sls-core"; 2 | import { Stream } from "stream"; 3 | 4 | /** 5 | * Normalizes S3 messages into a generic records array 6 | */ 7 | export const SimpleStorageMiddleware = (): Middleware => async (context: CloudContext, next: () => Promise) => { 8 | if (context.providerType === "aws") { 9 | const logger = context.logger || new ConsoleLogger(); 10 | 11 | if (!context.storage) { 12 | logger.error("Storage API missing from CloudContext. Ensure the CloudStorage middleware has been registered before the SimpleStorageMiddleware"); 13 | } 14 | 15 | const tasks: Promise[] = context.runtime.event.Records.map(async (message) => { 16 | let stream: Stream; 17 | 18 | try { 19 | stream = await context.storage.read({ 20 | container: message.s3.bucket.name, 21 | path: message.s3.object.key, 22 | }); 23 | } catch (e) { 24 | logger.warn(`Error reading object, container: ${message.s3.bucket.name}, path: ${message.s3.object.key}`); 25 | logger.error(e); 26 | stream = null; 27 | } 28 | 29 | const cloudMessage = { 30 | id: `${message.s3.bucket.name}/${message.s3.object.key}`, 31 | body: stream, 32 | timestamp: new Date(message.eventTime), 33 | eventName: message.eventName, 34 | eventSource: "aws:s3", 35 | } 36 | 37 | return cloudMessage; 38 | }); 39 | 40 | const records = await Promise.all(tasks); 41 | context.event = { records }; 42 | } 43 | 44 | await next(); 45 | }; 46 | -------------------------------------------------------------------------------- /aws/src/models/awsLamda.ts: -------------------------------------------------------------------------------- 1 | import { CloudProviderRuntime } from "@multicloud/sls-core"; 2 | 3 | export interface AwsLambdaRuntime extends CloudProviderRuntime { 4 | event: any; 5 | callback: (err, response) => void; 6 | context: { 7 | awsRequestId: string; 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /aws/src/services/S3Storage.ts: -------------------------------------------------------------------------------- 1 | import AWS from "aws-sdk"; 2 | import { CloudStorage, ReadBlobOptions, WriteBlobOptions, convertToStream, Guard, WriteBlobOutput } from "@multicloud/sls-core"; 3 | import { Stream } from "stream"; 4 | import { injectable } from "inversify"; 5 | import "reflect-metadata"; 6 | 7 | /** 8 | * Implementation of CloudStorage for AWS S3 Storage 9 | */ 10 | @injectable() 11 | export class S3Storage implements CloudStorage { 12 | 13 | private s3: AWS.S3; 14 | 15 | /** 16 | * Initialize new AWS S3 service 17 | */ 18 | public constructor() { 19 | this.s3 = new AWS.S3(); 20 | } 21 | 22 | /** 23 | * Read an object from an S3 bucket 24 | * @param opts Container (bucket) and blob (object) to read from in S3 25 | */ 26 | public async read(opts: ReadBlobOptions): Promise { 27 | const params = { 28 | Bucket: opts.container, 29 | Key: opts.path 30 | }; 31 | const result = await this.s3.getObject(params).promise() 32 | return result.Body as Stream 33 | } 34 | 35 | /** 36 | * Write an object to an S3 bucket 37 | * @param opts Container (bucket), blob (object) and body to write to S3 38 | */ 39 | public async write(opts: WriteBlobOptions): Promise { 40 | Guard.empty(opts.container, "container"); 41 | Guard.empty(opts.path, "path"); 42 | Guard.null(opts.body, "body"); 43 | 44 | const streamBody = convertToStream(opts.body); 45 | const params = { 46 | ...opts.options, 47 | Bucket: opts.container, 48 | Key: opts.path, 49 | Body: streamBody, 50 | ContentLength: streamBody.readableLength 51 | }; 52 | const result = await this.s3.putObject(params).promise(); 53 | 54 | return { 55 | version: result.VersionId, 56 | eTag: result.ETag 57 | }; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /aws/src/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./lambdaCloudService"; 2 | export * from "./S3Storage"; 3 | -------------------------------------------------------------------------------- /aws/src/services/lambdaCloudService.ts: -------------------------------------------------------------------------------- 1 | import AWS from "aws-sdk"; 2 | import { CloudService, ContainerResolver, CloudServiceOptions, CloudContext } from "@multicloud/sls-core"; 3 | import { ComponentType } from "@multicloud/sls-core"; 4 | import { injectable, inject } from "inversify"; 5 | 6 | /** 7 | * Type of invocation for AWS Lambda 8 | */ 9 | export enum AWSInvokeType { 10 | /** Wait for response */ 11 | fireAndWait = "RequestResponse", 12 | /** Don't wait for response */ 13 | fireAndForget = "Event" 14 | } 15 | 16 | /** 17 | * Options for invocation of AWS Lambda 18 | */ 19 | export interface AWSCloudServiceOptions extends CloudServiceOptions { 20 | /** Name of Lambda function to invoke */ 21 | name: string; 22 | /** Unique resource identifier for Lambda function */ 23 | arn: string; 24 | /** AWS Region containing lambda function */ 25 | region: string; 26 | } 27 | 28 | /** 29 | * Implementation of Cloud Service for AWS Lambda. Invokes Lambda Functions 30 | * with exposed HTTP endpoints via API Gateway 31 | */ 32 | @injectable() 33 | export class LambdaCloudService implements CloudService { 34 | public constructor(@inject(ComponentType.CloudContext) context: CloudContext) { 35 | this.containerResolver = context.container; 36 | } 37 | 38 | public containerResolver: ContainerResolver; 39 | 40 | /** 41 | * 42 | * @param name Name of Lambda function to invoke 43 | * @param fireAndForget Wait for response if false (default behavior) 44 | * @param payload Body of HTTP request 45 | */ 46 | public invoke(name: string, fireAndForget = false, payload: any) { 47 | if (!name || name.length === 0) { 48 | throw Error("Name is needed"); 49 | } 50 | const context = this.containerResolver.resolve(name); 51 | 52 | if (!context.region || !context.arn) { 53 | throw Error("Region and ARN are needed for Lambda calls"); 54 | } 55 | const lambda = new AWS.Lambda({ region: context.region }); 56 | 57 | return (lambda 58 | .invoke({ 59 | FunctionName: context.arn, 60 | Payload: payload, 61 | InvocationType: fireAndForget 62 | ? AWSInvokeType.fireAndForget 63 | : AWSInvokeType.fireAndWait 64 | }) 65 | .promise() as unknown) as T; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /aws/src/test/events/defaultAwsEvent.json: -------------------------------------------------------------------------------- 1 | { 2 | "body": "eyJ0ZXN0IjoiYm9keSJ9", 3 | "resource": "/{proxy+}", 4 | "path": "/path/to/resource", 5 | "httpMethod": "POST", 6 | "isBase64Encoded": true, 7 | "queryStringParameters": { 8 | "foo": "bar" 9 | }, 10 | "pathParameters": { 11 | "proxy": "/path/to/resource" 12 | }, 13 | "stageVariables": { 14 | "baz": "qux" 15 | }, 16 | "headers": { 17 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", 18 | "Accept-Encoding": "gzip, deflate, sdch", 19 | "Accept-Language": "en-US,en;q=0.8", 20 | "Cache-Control": "max-age=0", 21 | "CloudFront-Forwarded-Proto": "https", 22 | "CloudFront-Is-Desktop-Viewer": "true", 23 | "CloudFront-Is-Mobile-Viewer": "false", 24 | "CloudFront-Is-SmartTV-Viewer": "false", 25 | "CloudFront-Is-Tablet-Viewer": "false", 26 | "CloudFront-Viewer-Country": "US", 27 | "Host": "1234567890.execute-api.us-east-1.amazonaws.com", 28 | "Upgrade-Insecure-Requests": "1", 29 | "User-Agent": "Custom User Agent String", 30 | "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", 31 | "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", 32 | "X-Forwarded-For": "127.0.0.1, 127.0.0.2", 33 | "X-Forwarded-Port": "443", 34 | "X-Forwarded-Proto": "https" 35 | }, 36 | "requestContext": { 37 | "accountId": "123456789012", 38 | "resourceId": "123456", 39 | "stage": "prod", 40 | "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", 41 | "requestTime": "09/Apr/2015:12:34:56 +0000", 42 | "requestTimeEpoch": 1428582896000, 43 | "identity": { 44 | "cognitoIdentityPoolId": null, 45 | "accountId": null, 46 | "cognitoIdentityId": null, 47 | "caller": null, 48 | "accessKey": null, 49 | "sourceIp": "127.0.0.1", 50 | "cognitoAuthenticationType": null, 51 | "cognitoAuthenticationProvider": null, 52 | "userArn": null, 53 | "userAgent": "Custom User Agent String", 54 | "user": null 55 | }, 56 | "path": "/prod/path/to/resource", 57 | "resourcePath": "/{proxy+}", 58 | "httpMethod": "POST", 59 | "apiId": "1234567890", 60 | "protocol": "HTTP/1.1" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /aws/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "experimentalDecorators": true, 5 | "rootDir": "src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 6 | "outDir": "lib", /* Redirect output structure to the directory. */ 7 | }, 8 | "include": [ 9 | "./src" 10 | ], 11 | "exclude": [ 12 | "**/*.test.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | # Starter pipeline 2 | # Start with a minimal pipeline that you can customize to build and deploy your code. 3 | # Add steps that build, run tests, deploy, and more: 4 | # https://aka.ms/yaml 5 | 6 | trigger: 7 | - master 8 | 9 | pool: 10 | vmImage: 'ubuntu-latest' 11 | 12 | steps: 13 | - script: echo Hello, world! 14 | displayName: 'Run a one-line script' 15 | 16 | - script: | 17 | echo Add other tasks to build, test, and deploy your project. 18 | echo See https://aka.ms/yaml 19 | displayName: 'Run a multi-line script' 20 | -------------------------------------------------------------------------------- /azure/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "babel-plugin-transform-typescript-metadata" 4 | ], 5 | "presets": [ 6 | [ 7 | "react-app", 8 | { 9 | "flow": false, 10 | "typescript": true 11 | } 12 | ] 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /azure/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "project": "./tsconfig.json" 5 | }, 6 | "plugins": ["@typescript-eslint"], 7 | "extends": ["plugin:@typescript-eslint/recommended"], 8 | "rules": { 9 | "quotes": ["error", "double"], 10 | "@typescript-eslint/indent": [ 11 | "error", 12 | 2 13 | ], 14 | "@typescript-eslint/no-explicit-any": 0, 15 | "@typescript-eslint/explicit-function-return-type": 0, 16 | "@typescript-eslint/no-parameter-properties": 0, 17 | "@typescript-eslint/no-use-before-define": 0, 18 | "@typescript-eslint/no-object-literal-type-assertion": 0 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /azure/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Serverless, Inc. http://www.serverless.com 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /azure/README.md: -------------------------------------------------------------------------------- 1 | # Serverless Multicloud Library for Microsoft Azure 2 | [![Build Status](https://dev.azure.com/serverless-inc/multicloud/_apis/build/status/CI/%5Bsls-azure%5D%20ci?branchName=dev)](https://dev.azure.com/serverless-inc/multicloud/_build/latest?definitionId=2&branchName=dev) 3 | [![npm (scoped)](https://img.shields.io/npm/v/@multicloud/sls-azure)](https://www.npmjs.com/package/@multicloud/sls-azure) 4 | 5 | The Serverless @multicloud library provides an easy way to build Serverless handlers in NodeJS using a cloud agnostic library that can then be deployed to support cloud providers. 6 | 7 | In addition to a normalized API the @multicloud library supports reusable middleware pipeline similar to the Express framework 8 | 9 | ## Installation 10 | ```bash 11 | npm install @multicloud/sls-azure --save 12 | ``` 13 | 14 | ## Example 15 | ```javascript 16 | const { App } = require("@multicloud/sls-core"); 17 | const { AzureModule } = require("@multicloud/sls-azure"); 18 | const app = new App(new AzureModule()); 19 | 20 | module.exports.handler = app.use([], async (context) => { 21 | const { req } = context; 22 | const name = req.query.get("name"); 23 | 24 | if (name) { 25 | context.send(`Hello ${name}`, 200); 26 | } 27 | else { 28 | context.send("Please pass a name on the query string or in the request body", 400); 29 | } 30 | }); 31 | ``` 32 | 33 | ## Contributing 34 | 35 | ### Testing 36 | Run Jest unit tests 37 | ```bash 38 | npm run test 39 | ``` 40 | 41 | ### Building 42 | Runs the TypeScript compiler and ouputs to the `lib` folder 43 | ```bash 44 | npm run build 45 | ``` 46 | 47 | ## Contributing 48 | ### [Code of Conduct](../CODE_OF_CONDUCT.md) 49 | In the interest of fostering an open and welcoming environment, we as 50 | contributors and maintainers pledge to making participation in our project and 51 | our community a harassment-free experience for everyone, regardless of age, body 52 | size, disability, ethnicity, gender identity and expression, level of experience, 53 | nationality, personal appearance, race, religion, or sexual identity and 54 | orientation. 55 | 56 | ### [Contriubtion Guidelines](../CONTRIBUTING.md) 57 | Welcome, and thanks in advance for your help! Please follow these simple guidelines :+1: 58 | 59 | ## Licensing 60 | 61 | Serverless is licensed under the [MIT License](./LICENSE.txt). 62 | 63 | All files located in the node_modules and external directories are externally maintained libraries used by this software which have their own licenses; we recommend you read them, as their terms may differ from the terms in the MIT License. 64 | -------------------------------------------------------------------------------- /azure/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "collectCoverageFrom": [ 3 | "src/**/*.{js,jsx,ts,tsx}", 4 | "!src/**/*.d.ts" 5 | ], 6 | "testEnvironment": "node", 7 | "transform": { 8 | "^.+\\.(js|jsx|ts|tsx)$": "babel-jest" 9 | }, 10 | "testPathIgnorePatterns": [ 11 | "./lib", 12 | "./node_modules" 13 | ], 14 | "transformIgnorePatterns": [ 15 | "./lib", 16 | "./node_modules" 17 | ], 18 | "moduleFileExtensions": [ 19 | "js", 20 | "ts", 21 | "tsx", 22 | "json", 23 | "jsx", 24 | "node" 25 | ] 26 | }; 27 | -------------------------------------------------------------------------------- /azure/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@multicloud/sls-azure", 3 | "version": "0.1.1", 4 | "description": "Microsoft Azure specific middleware and components for Serverless @multicloud.", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/serverless/multicloud" 8 | }, 9 | "keywords": [ 10 | "serverless", 11 | "sls", 12 | "serverless framework", 13 | "multicloud", 14 | "azure", 15 | "azure functions" 16 | ], 17 | "main": "lib/index.js", 18 | "types": "lib/index.d.ts", 19 | "scripts": { 20 | "start": "npm run test -- --watch", 21 | "lint": "eslint src/**/*.ts", 22 | "lint:fix": "npm run lint -- --fix", 23 | "pretest": "npm run lint", 24 | "test": "jest", 25 | "test:ci": "npm run test -- --ci", 26 | "test:coverage": "npm run test -- --coverage", 27 | "prebuild": "shx rm -rf lib/ && npm run test", 28 | "build": "npx tsc" 29 | }, 30 | "files": [ 31 | "lib/" 32 | ], 33 | "author": "Microsoft Corporation, 7-Eleven & Serverless Inc", 34 | "license": "MIT", 35 | "dependencies": { 36 | "@azure/storage-blob": "10.3.0", 37 | "@multicloud/sls-core": "^0.1.1-13", 38 | "inversify": "5.0.1", 39 | "reflect-metadata": "0.1.13", 40 | "streamifier": "0.1.1" 41 | }, 42 | "devDependencies": { 43 | "@types/jest": "^24.0.13", 44 | "@types/node": "12.0.8", 45 | "@typescript-eslint/eslint-plugin": "^1.9.0", 46 | "@typescript-eslint/parser": "^1.9.0", 47 | "babel-jest": "^24.8.0", 48 | "babel-preset-react-app": "^9.0.0", 49 | "babel-plugin-transform-typescript-metadata": "0.2.2", 50 | "eslint": "^5.16.0", 51 | "jest": "^24.8.0", 52 | "shx": "^0.3.2", 53 | "typescript": "^3.5.2" 54 | }, 55 | "engines": { 56 | "node": ">=8.16.0", 57 | "npm": ">=6.4.1" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /azure/pipelines/ci.yml: -------------------------------------------------------------------------------- 1 | # Node.js 2 | # Build a general Node.js project with npm. 3 | # Add steps that analyze code, save build artifacts, deploy, and more: 4 | # https://docs.microsoft.com/azure/devops/pipelines/languages/javascript 5 | 6 | # CI should only run when PRs are opened against dev/master. When integrating AZDo repos with 7 | # AZDo pipelines, the only way to do this is through `build validations` - branch-specific 8 | # integrations that can only be configured via AzDO UI. 9 | trigger: none 10 | 11 | pr: 12 | - master 13 | - dev 14 | 15 | pool: 16 | vmImage: 'ubuntu-latest' 17 | 18 | steps: 19 | - task: NodeTool@0 20 | displayName: 'Use Node 10.x' 21 | inputs: 22 | versionSpec: 10.x 23 | 24 | - task: Bash@3 25 | displayName: 'Build sls-azure' 26 | inputs: 27 | targetType: filePath 28 | filePath: ./scripts/build.sh 29 | workingDirectory: azure 30 | env: 31 | NPM_TOKEN: $(NPM_TOKEN) 32 | -------------------------------------------------------------------------------- /azure/pipelines/publish-prerelease.yml: -------------------------------------------------------------------------------- 1 | # Node.js 2 | # Build a general Node.js project with npm. 3 | # Add steps that analyze code, save build artifacts, deploy, and more: 4 | # https://docs.microsoft.com/azure/devops/pipelines/languages/javascript 5 | 6 | # Only publish to AzDO NPM when a tag is pushed to master. 7 | # NOTE: a PR needs to be opened from dev --> master, which includes version bump. 8 | # If needs be, we can eventually automate. 9 | trigger: 10 | branches: 11 | include: 12 | - dev 13 | paths: 14 | include: 15 | - azure/* 16 | 17 | pr: none 18 | 19 | pool: 20 | vmImage: 'ubuntu-latest' 21 | 22 | variables: 23 | - group: GitHub-Deploy-Creds 24 | - group: npm-release-credentials 25 | 26 | steps: 27 | - task: NodeTool@0 28 | displayName: 'Use Node 10.x' 29 | inputs: 30 | versionSpec: 10.x 31 | 32 | # Download secure file 33 | # Download a secure file to the agent machine 34 | - task: DownloadSecureFile@1 35 | # name: sshKey # The name with which to reference the secure file's path on the agent, like $(mySecureFile.secureFilePath) 36 | inputs: 37 | secureFile: multicloud_id_rsa 38 | 39 | # Install an SSH key prior to a build or deployment 40 | - task: InstallSSHKey@0 # https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/utility/install-ssh-key?view=azure-devops 41 | inputs: 42 | knownHostsEntry: $(KNOWN_HOSTS_ENTRY) 43 | sshPublicKey: $(SSH_PUBLIC_KEY) 44 | #sshPassphrase: $(SSH_PASS_PHRASE) 45 | sshKeySecureFile: multicloud_id_rsa 46 | env: 47 | KNOWN_HOSTS_ENTRY: $(KNOWN_HOSTS_ENTRY) 48 | SSH_PUBLIC_KEY: $(SSH_PUBLIC_KEY) # map to the right format (camelCase) that Azure credentials understand 49 | 50 | - task: Bash@3 51 | name: BumpNpmVersion 52 | displayName: Bump NPM Prerelease Version 53 | inputs: 54 | targetType: filePath 55 | filePath: ./scripts/version.sh 56 | arguments: '@multicloud/sls-azure' 57 | workingDirectory: azure 58 | env: 59 | SOURCE_BRANCH: $(Build.SourceBranch) 60 | 61 | - task: Bash@3 62 | displayName: 'Publish sls-azure to NPM' 63 | inputs: 64 | targetType: filePath 65 | filePath: ./scripts/publish.sh 66 | arguments: 'prerelease' 67 | workingDirectory: azure 68 | env: 69 | NPM_TOKEN: $(NPM_TOKEN) 70 | -------------------------------------------------------------------------------- /azure/pipelines/publish-release.yml: -------------------------------------------------------------------------------- 1 | # Node.js 2 | # Build a general Node.js project with npm. 3 | # Add steps that analyze code, save build artifacts, deploy, and more: 4 | # https://docs.microsoft.com/azure/devops/pipelines/languages/javascript 5 | 6 | # Only publish to AzDO NPM when a tag is pushed to master. 7 | # NOTE: a PR needs to be opened from dev --> master, which includes version bump. 8 | # If needs be, we can eventually automate. 9 | trigger: none 10 | 11 | pr: none 12 | 13 | pool: 14 | vmImage: 'ubuntu-latest' 15 | 16 | variables: 17 | - group: GitHub-Deploy-Creds 18 | - group: npm-release-credentials 19 | 20 | steps: 21 | - task: NodeTool@0 22 | displayName: 'Use Node 10.x' 23 | inputs: 24 | versionSpec: 10.x 25 | 26 | # Download secure file 27 | # Download a secure file to the agent machine 28 | - task: DownloadSecureFile@1 29 | # name: sshKey # The name with which to reference the secure file's path on the agent, like $(mySecureFile.secureFilePath) 30 | inputs: 31 | secureFile: multicloud_id_rsa 32 | 33 | # Install an SSH key prior to a build or deployment 34 | - task: InstallSSHKey@0 # https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/utility/install-ssh-key?view=azure-devops 35 | inputs: 36 | knownHostsEntry: $(KNOWN_HOSTS_ENTRY) 37 | sshPublicKey: $(SSH_PUBLIC_KEY) 38 | #sshPassphrase: $(SSH_PASS_PHRASE) 39 | sshKeySecureFile: multicloud_id_rsa 40 | env: 41 | KNOWN_HOSTS_ENTRY: $(KNOWN_HOSTS_ENTRY) 42 | SSH_PUBLIC_KEY: $(SSH_PUBLIC_KEY) # map to the right format (camelCase) that Azure credentials understand 43 | 44 | - task: Bash@3 45 | name: BumpNpmVersion 46 | displayName: Bump NPM Prerelease Version 47 | inputs: 48 | targetType: filePath 49 | filePath: ./scripts/version.sh 50 | arguments: '@multicloud/sls-azure $(NPM_RELEASE_TYPE)' 51 | workingDirectory: azure 52 | env: 53 | SOURCE_BRANCH: $(Build.SourceBranch) 54 | 55 | - task: Bash@3 56 | displayName: 'Publish sls-azure to NPM' 57 | inputs: 58 | targetType: filePath 59 | filePath: ./scripts/publish.sh 60 | workingDirectory: azure 61 | env: 62 | NPM_TOKEN: $(NPM_TOKEN) 63 | -------------------------------------------------------------------------------- /azure/src/azureModule.ts: -------------------------------------------------------------------------------- 1 | import { ContainerModule, interfaces } from "inversify"; 2 | import { CloudModule, CloudContext, CloudRequest, CloudResponse, ComponentType, CloudService, CloudStorage } from "@multicloud/sls-core"; 3 | import { AzureContext, AzureRequest, AzureResponse, AzureBlobStorage } from "."; 4 | import { AzureFunctionCloudService } from "./services"; 5 | 6 | /** 7 | * Azure Module that can be registered in IoC container 8 | */ 9 | export class AzureModule implements CloudModule { 10 | 11 | /** 12 | * Determines whether or not the incoming request is an Azure request 13 | * @param req The IoC resolution request 14 | */ 15 | private isAzureRequest(req: interfaces.Request) { 16 | const runtimeArgs = req.parentContext.container.get(ComponentType.RuntimeArgs); 17 | return runtimeArgs && runtimeArgs[0].invocationId; 18 | }; 19 | 20 | /** 21 | * Creates the inversify container module 22 | */ 23 | public create() { 24 | return new ContainerModule((bind) => { 25 | bind(ComponentType.CloudContext) 26 | .to(AzureContext) 27 | .inSingletonScope() 28 | .when(this.isAzureRequest); 29 | 30 | bind(ComponentType.CloudRequest) 31 | .to(AzureRequest) 32 | .when(this.isAzureRequest); 33 | 34 | bind(ComponentType.CloudResponse) 35 | .to(AzureResponse) 36 | .when(this.isAzureRequest); 37 | 38 | bind(ComponentType.CloudService) 39 | .to(AzureFunctionCloudService) 40 | .when(this.isAzureRequest); 41 | 42 | bind(ComponentType.CloudStorage) 43 | .to(AzureBlobStorage) 44 | .when(this.isAzureRequest); 45 | }); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /azure/src/azureRequest.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | import { inject, injectable } from "inversify"; 3 | import { CloudRequest, ComponentType, StringParams } from "@multicloud/sls-core"; 4 | import { AzureContext } from "./azureContext"; 5 | 6 | /** 7 | * Implementation of Cloud Request for Azure Functions 8 | */ 9 | @injectable() 10 | export class AzureRequest implements CloudRequest { 11 | /** Body of HTTP request */ 12 | public body?: any; 13 | /** Headers of HTTP request */ 14 | public headers?: StringParams; 15 | /** HTTP method of request */ 16 | public method: string; 17 | /** Query params of HTTP request */ 18 | public query?: StringParams; 19 | /** Path params of HTTP request */ 20 | public pathParams?: StringParams; 21 | 22 | /** 23 | * Initialize new Azure Request, injecting Cloud Context 24 | * @param context Current CloudContext 25 | */ 26 | public constructor(@inject(ComponentType.CloudContext) context: AzureContext) { 27 | const req = context.runtime.event || context.runtime.context.req; 28 | 29 | this.body = req.body || null; 30 | this.headers = new StringParams(req.headers); 31 | this.method = req.method || ""; 32 | this.query = new StringParams(req.query); 33 | this.pathParams = new StringParams(req.params); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /azure/src/azureResponse.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | import { inject, injectable } from "inversify"; 3 | import { 4 | CloudResponse, 5 | ComponentType, 6 | ProviderType, 7 | CloudProviderResponseHeader, 8 | StringParams 9 | } from "@multicloud/sls-core"; 10 | import { AzureContext } from "./azureContext"; 11 | 12 | /** 13 | * Implementation of Cloud Response for Azure Functions 14 | */ 15 | @injectable() 16 | export class AzureResponse implements CloudResponse { 17 | /** Original runtime from Azure Function context */ 18 | public runtime: any; 19 | 20 | /** The HTTP response body */ 21 | public body: any; 22 | 23 | /** The HTTP response status code */ 24 | public status: number = 200; 25 | 26 | /** Headers of HTTP Response */ 27 | public headers?: StringParams; 28 | 29 | /** 30 | * Initialize new Azure Response, injecting Cloud Context 31 | * @param context Current CloudContext 32 | */ 33 | public constructor( 34 | @inject(ComponentType.CloudContext) context: AzureContext 35 | ) { 36 | this.runtime = context.runtime; 37 | this.headers = new StringParams(this.runtime.context.res.headers); 38 | this.headers.set(CloudProviderResponseHeader, ProviderType.Azure); 39 | } 40 | 41 | /** 42 | * Send HTTP response 43 | * @param body Body of HTTP response 44 | * @param status Status code of HTTP response 45 | */ 46 | public send(body: any = null, status: number = 200): void { 47 | // If body was left as `undefined` vs `null` the azure functions runtime 48 | // incorrectly returns the full `response` object as the `body` of the response object 49 | this.body = body; 50 | this.status = status; 51 | 52 | if (!body) { 53 | return; 54 | } 55 | 56 | const bodyType = body.constructor.name; 57 | 58 | if (["Object", "Array"].includes(bodyType)) { 59 | this.headers.set("Content-Type", "application/json"); 60 | } 61 | 62 | if (["String"].includes(bodyType)) { 63 | this.headers.set("Content-Type", "text/html"); 64 | } 65 | } 66 | 67 | public flush() { 68 | const response = { 69 | status: this.status, 70 | body: this.body, 71 | headers: this.headers.toJSON(), 72 | }; 73 | 74 | // Find the registered output binding for the function 75 | const outputBinding = this.runtime.context.bindingDefinitions 76 | .find((binding) => binding.direction === "out" && binding.type === "http"); 77 | 78 | // If an output binding has been defined and it is not the 79 | // default $return binding then set the "res" on the runtime 80 | // The Azure functions framework will then set the output bindings to the value of runtime.res 81 | if (outputBinding && outputBinding.name !== "$return") { 82 | this.runtime.context.res = response; 83 | this.runtime.context.done(); 84 | } else { // Otherwise call the done callback with the response 85 | this.runtime.context.done(null, response); 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /azure/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./azureContext"; 2 | export * from "./azureRequest"; 3 | export * from "./azureResponse"; 4 | export * from "./azureModule"; 5 | export * from "./services"; 6 | export * from "./middleware"; 7 | export * from "./models/azureFunctions"; 8 | -------------------------------------------------------------------------------- /azure/src/middleware/eventHubMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { Middleware, CloudMessage } from "@multicloud/sls-core"; 2 | import { AzureContext } from "../azureContext"; 3 | 4 | export const EventHubMiddleware = (): Middleware => async (context: AzureContext, next: () => Promise) => { 5 | if (context instanceof AzureContext) { 6 | const bindingData = context.runtime.context.bindingData; 7 | 8 | const message: CloudMessage = { 9 | id: bindingData.sequenceNumber, 10 | partitionKey: bindingData.partitionKey, 11 | offset: bindingData.offset, 12 | body: context.event, 13 | timestamp: new Date(bindingData.enqueuedTimeUtc), 14 | eventSource: "azure:eventHub", 15 | properties: bindingData.properties, 16 | }; 17 | 18 | context.event = { 19 | records: [message] 20 | }; 21 | } 22 | 23 | await next(); 24 | } 25 | -------------------------------------------------------------------------------- /azure/src/middleware/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./storageQueueMiddleware"; 2 | export * from "./serviceBusMiddleware"; 3 | export * from "./eventHubMiddleware"; 4 | export * from "./storageBlob"; 5 | -------------------------------------------------------------------------------- /azure/src/middleware/serviceBusMiddleware.test.ts: -------------------------------------------------------------------------------- 1 | import { ServiceBusMiddleware } from "."; 2 | import { Middleware, CloudContext } from "@multicloud/sls-core"; 3 | import { AzureContext } from ".."; 4 | 5 | describe("Service Bus Middleware", () => { 6 | let middleware: Middleware; 7 | 8 | beforeEach(() => { 9 | middleware = ServiceBusMiddleware(); 10 | }); 11 | 12 | it("only runs during azure requests", async () => { 13 | const next = jest.fn(); 14 | const originalEvent = {}; 15 | const context: CloudContext = { 16 | id: "abc123", 17 | providerType: "unknown", 18 | event: originalEvent, 19 | done: jest.fn(), 20 | send: jest.fn(), 21 | flush: jest.fn(), 22 | }; 23 | 24 | await middleware(context, next); 25 | 26 | expect(context.event).toBe(originalEvent); 27 | expect(next).toBeCalled(); 28 | }); 29 | 30 | it("transforms the azure event into a generic cloud message", async () => { 31 | const originalEvent = "test message"; 32 | const runtimeArgs: any[] = [ 33 | { 34 | invocationID: "ID123", 35 | log: {}, 36 | bindingData: { 37 | invocationId: "db9752bd-8380-4e18-9b71-469b66921e67", 38 | deliveryCount: 1, 39 | lockToken: "6a4b609e-5987-445c-b0e2-74fc00c671ca", 40 | expiresAtUtc: "2019-08-16T22:41:33.403Z", 41 | enqueuedTimeUtc: "2019-08-02T22:41:33.403Z", 42 | messageId: "e91fb2da-2da9-4286-8ae6-c2f6fa110353", 43 | sequenceNumber: 7, 44 | label: "Service Bus Explorer", 45 | userProperties: 46 | { 47 | machineName: "XYZ123", 48 | userName: "somebody", 49 | "x-opt-enqueue-sequence-number": 0 50 | }, 51 | messageReceiver: 52 | { 53 | registeredPlugins: [], 54 | receiveMode: 0, 55 | prefetchCount: 0, 56 | lastPeekedSequenceNumber: 0, 57 | path: "sample-queue", 58 | operationTimeout: "00:01:00", 59 | serviceBusConnection: [Object], 60 | isClosedOrClosing: false, 61 | clientId: "MessageReceiver5sample-queue", 62 | retryPolicy: [Object] 63 | }, 64 | sys: 65 | { 66 | methodName: "serviceBusQueueHandler", 67 | utcNow: "2019-08-02T22:41:33.6505599Z", 68 | randGuid: "36e00190-8916-46bf-8d08-767bfceac1bb" 69 | } 70 | }, 71 | bindingDefinitions: [], 72 | }, 73 | originalEvent, 74 | ]; 75 | 76 | const next = jest.fn(); 77 | const context = new AzureContext(runtimeArgs); 78 | await middleware(context, next); 79 | 80 | expect(context.event).toEqual({ 81 | records: [{ 82 | id: runtimeArgs[0].bindingData.messageId, 83 | body: originalEvent, 84 | timestamp: expect.any(Date), 85 | eventSource: "azure:serviceBus", 86 | }] 87 | }) 88 | expect(next).toBeCalled(); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /azure/src/middleware/serviceBusMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { Middleware, CloudMessage } from "@multicloud/sls-core"; 2 | import { AzureContext } from "../azureContext"; 3 | 4 | export const ServiceBusMiddleware = (): Middleware => async (context: AzureContext, next: () => Promise) => { 5 | if (context instanceof AzureContext) { 6 | const bindingData = context.runtime.context.bindingData; 7 | 8 | const message: CloudMessage = { 9 | id: bindingData.messageId, 10 | body: context.event, 11 | timestamp: new Date(bindingData.enqueuedTimeUtc), 12 | properties: bindingData.properties, 13 | eventSource: "azure:serviceBus", 14 | }; 15 | 16 | context.event = { 17 | records: [message] 18 | }; 19 | } 20 | 21 | await next(); 22 | } 23 | -------------------------------------------------------------------------------- /azure/src/middleware/storageBlob.test.ts: -------------------------------------------------------------------------------- 1 | import { StorageBlobMiddleware } from "."; 2 | import { Middleware, CloudContext } from "@multicloud/sls-core"; 3 | import { AzureContext } from ".."; 4 | import { Stream } from "stream"; 5 | 6 | describe("Storage Queue Middleware", () => { 7 | let middleware: Middleware; 8 | 9 | beforeEach(() => { 10 | middleware = StorageBlobMiddleware(); 11 | }); 12 | 13 | it("only runs during azure requests", async () => { 14 | const next = jest.fn(); 15 | const originalEvent = {}; 16 | const context: CloudContext = { 17 | id: "abc123", 18 | providerType: "unknown", 19 | event: originalEvent, 20 | done: jest.fn(), 21 | send: jest.fn(), 22 | flush: jest.fn(), 23 | }; 24 | 25 | await middleware(context, next); 26 | 27 | expect(context.event).toBe(originalEvent); 28 | expect(next).toBeCalled(); 29 | }); 30 | 31 | it("transforms the azure event into a generic cloud message", async () => { 32 | const testMessage = "this is a test"; 33 | const originalEvent = Buffer.from(testMessage); 34 | const runtimeArgs: any[] = [ 35 | { 36 | invocationID: "ID123", 37 | log: {}, 38 | bindingData: { 39 | id: "ABC123", 40 | blobTrigger: "container/item.txt", 41 | insertionTime: new Date().toUTCString(), 42 | properties: { 43 | contentType: "text/plain", 44 | length: 123, 45 | lastModified: "2019-08-05T23:58:10+00:00" 46 | } 47 | }, 48 | bindingDefinitions: [], 49 | }, 50 | originalEvent, 51 | ]; 52 | 53 | const next = jest.fn(); 54 | const context = new AzureContext(runtimeArgs); 55 | await middleware(context, next); 56 | 57 | const actualBody = await streamToString(context.event.records[0].body); 58 | 59 | expect(actualBody).toEqual(testMessage); 60 | expect(context.event).toEqual({ 61 | records: [{ 62 | id: runtimeArgs[0].bindingData.blobTrigger, 63 | contentType: runtimeArgs[0].bindingData.properties.contentType, 64 | length: runtimeArgs[0].bindingData.properties.length, 65 | properties: runtimeArgs[0].bindingData.properties, 66 | body: expect.anything(), 67 | timestamp: expect.any(Date), 68 | eventSource: "azure:storageBlob", 69 | }] 70 | }) 71 | expect(next).toBeCalled(); 72 | }); 73 | }); 74 | 75 | function streamToString(stream: Stream) { 76 | const chunks = []; 77 | return new Promise((resolve, reject) => { 78 | stream.on("data", (chunk) => chunks.push(chunk)); 79 | stream.on("error", reject); 80 | stream.on("end", () => resolve(Buffer.concat(chunks).toString("utf8"))); 81 | }); 82 | } 83 | -------------------------------------------------------------------------------- /azure/src/middleware/storageBlob.ts: -------------------------------------------------------------------------------- 1 | import { CloudMessage, Middleware } from "@multicloud/sls-core"; 2 | import { AzureContext } from ".."; 3 | import { createReadStream } from "streamifier"; 4 | 5 | 6 | /** 7 | * Normalizes an Azure Storage Blob message into an array of records 8 | */ 9 | export const StorageBlobMiddleware = (): Middleware => async (context: AzureContext, next: () => Promise) => { 10 | if (context instanceof AzureContext) { 11 | const bindingData = context.runtime.context.bindingData; 12 | const buffer = context.event; 13 | 14 | const message: CloudMessage = { 15 | id: bindingData.blobTrigger, 16 | contentType: bindingData.properties.contentType, 17 | length: bindingData.properties.length, 18 | body: createReadStream(buffer), 19 | timestamp: new Date(bindingData.properties.lastModified), 20 | properties: bindingData.properties, 21 | eventSource: "azure:storageBlob", 22 | }; 23 | 24 | context.event = { 25 | records: [message] 26 | }; 27 | } 28 | 29 | await next(); 30 | } 31 | -------------------------------------------------------------------------------- /azure/src/middleware/storageQueueMiddleware.test.ts: -------------------------------------------------------------------------------- 1 | import { StorageQueueMiddleware } from "."; 2 | import { Middleware, CloudContext } from "@multicloud/sls-core"; 3 | import { AzureContext } from "../"; 4 | 5 | describe("Storage Queue Middleware", () => { 6 | let middleware: Middleware; 7 | 8 | beforeEach(() => { 9 | middleware = StorageQueueMiddleware(); 10 | }); 11 | 12 | it("only runs during azure requests", async () => { 13 | const next = jest.fn(); 14 | const originalEvent = {}; 15 | const context: CloudContext = { 16 | id: "abc123", 17 | providerType: "unknown", 18 | event: originalEvent, 19 | done: jest.fn(), 20 | send: jest.fn(), 21 | flush: jest.fn(), 22 | }; 23 | 24 | await middleware(context, next); 25 | 26 | expect(context.event).toBe(originalEvent); 27 | expect(next).toBeCalled(); 28 | }); 29 | 30 | it("transforms the azure event into a generic cloud message", async () => { 31 | const originalEvent = "test message"; 32 | const runtimeArgs: any[] = [ 33 | { 34 | invocationID: "ID123", 35 | log: {}, 36 | bindingData: { 37 | id: "ABC123", 38 | insertionTime: new Date().toUTCString(), 39 | }, 40 | bindingDefinitions: [], 41 | }, 42 | originalEvent, 43 | ]; 44 | 45 | const next = jest.fn(); 46 | const context = new AzureContext(runtimeArgs); 47 | await middleware(context, next); 48 | 49 | expect(context.event).toEqual({ 50 | records: [{ 51 | id: runtimeArgs[0].bindingData.id, 52 | body: originalEvent, 53 | timestamp: expect.any(Date), 54 | eventSource: "azure:storageQueue", 55 | }] 56 | }) 57 | expect(next).toBeCalled(); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /azure/src/middleware/storageQueueMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { CloudMessage, Middleware } from "@multicloud/sls-core"; 2 | import { AzureContext } from ".."; 3 | 4 | /** 5 | * Normalizes an Azure Storage Queue message into an array of records 6 | */ 7 | export const StorageQueueMiddleware = (): Middleware => async (context: AzureContext, next: () => Promise) => { 8 | if (context instanceof AzureContext) { 9 | const bindingData = context.runtime.context.bindingData; 10 | 11 | const message: CloudMessage = { 12 | id: bindingData.id, 13 | body: context.event, 14 | timestamp: new Date(bindingData.insertionTime), 15 | eventSource: "azure:storageQueue", 16 | }; 17 | 18 | context.event = { 19 | records: [message] 20 | }; 21 | } 22 | 23 | await next(); 24 | } 25 | -------------------------------------------------------------------------------- /azure/src/models/azureFunctions.ts: -------------------------------------------------------------------------------- 1 | import { CloudProviderRuntime } from "@multicloud/sls-core"; 2 | 3 | /** 4 | * The Azure functions runtime context 5 | */ 6 | export interface AzureFunctionsRuntime extends CloudProviderRuntime { 7 | event: any; 8 | context: { 9 | bindingData: any; 10 | bindingDefinitions: BindingDefinition[]; 11 | invocationId: string; 12 | log: AzureLog; 13 | done: (err: any, response: any) => void; 14 | req?: any; 15 | res?: any; 16 | }; 17 | } 18 | 19 | /** 20 | * Azure JavaScript logging function definition. 21 | */ 22 | export type LogFunction = (message: string) => void; 23 | 24 | /** 25 | * Azure logging interface, as per documentation: 26 | * https://docs.microsoft.com/en-us/azure/azure-functions/functions-reference-node#context-object 27 | */ 28 | export type AzureLog = LogFunction & { 29 | verbose: LogFunction; 30 | info: LogFunction; 31 | warn: LogFunction; 32 | error: LogFunction; 33 | } 34 | 35 | /** 36 | * The azure functions binding definition 37 | */ 38 | export interface BindingDefinition { 39 | name: string; 40 | type: string; 41 | direction: BindingDirection; 42 | } 43 | 44 | /** 45 | * The Azure functions binding direction 46 | */ 47 | export enum BindingDirection { 48 | In = "in", 49 | Out = "out", 50 | } 51 | -------------------------------------------------------------------------------- /azure/src/services/azureBlobStorage.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Aborter, 3 | BlobURL, 4 | ServiceURL, 5 | StorageURL, 6 | SharedKeyCredential, 7 | ContainerURL, 8 | BlockBlobURL, 9 | uploadStreamToBlockBlob 10 | } from "@azure/storage-blob"; 11 | import { CloudStorage, ReadBlobOptions, WriteBlobOptions, Guard, convertToStream, WriteBlobOutput } from "@multicloud/sls-core"; 12 | import { injectable } from "inversify"; 13 | import { Stream } from "stream"; 14 | import "reflect-metadata"; 15 | 16 | /** 17 | * Implementation of CloudStorage for Azure Blob Storage 18 | */ 19 | @injectable() 20 | export class AzureBlobStorage implements CloudStorage { 21 | 22 | private service: ServiceURL; 23 | 24 | /** 25 | * Initialize new Azure Blob Storage service 26 | */ 27 | public constructor() { 28 | const sharedKeyCredential = new SharedKeyCredential( 29 | process.env.azAccount, 30 | process.env.azAccountKey 31 | ); 32 | const pipeline = StorageURL.newPipeline(sharedKeyCredential); 33 | 34 | this.service = new ServiceURL( 35 | `https://${process.env.azAccount}.blob.core.windows.net`, 36 | pipeline 37 | ); 38 | } 39 | 40 | /** 41 | * Read a blob from Azure Storage account 42 | * @param opts Specifies container and blob for read 43 | */ 44 | public async read(opts: ReadBlobOptions): Promise { 45 | const containerURL = ContainerURL.fromServiceURL(this.service, opts.container); 46 | const blobURL = BlobURL.fromContainerURL(containerURL, opts.path); 47 | 48 | const stream = await blobURL.download(Aborter.none, 0) 49 | return stream.readableStreamBody 50 | } 51 | 52 | /** 53 | * Write a blob to Azure Storage account 54 | * @param opts Specifies container and blob to write 55 | */ 56 | public async write(opts: WriteBlobOptions): Promise { 57 | Guard.empty(opts.container); 58 | Guard.empty(opts.path); 59 | Guard.null(opts.body); 60 | 61 | const containerURL = ContainerURL.fromServiceURL(this.service, opts.container); 62 | const blockBlobURL = BlockBlobURL.fromContainerURL(containerURL, opts.path); 63 | 64 | const stream = convertToStream(opts.body); 65 | 66 | const bufferSize = 4*1024*1024; 67 | const maxBuffers = 5; 68 | const result = await uploadStreamToBlockBlob(Aborter.none, stream, blockBlobURL, bufferSize, maxBuffers); 69 | 70 | return { 71 | eTag: result.eTag, 72 | version: result.version, 73 | }; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /azure/src/services/azureFunctionCloudService.ts: -------------------------------------------------------------------------------- 1 | import { CloudService, ContainerResolver, CloudServiceOptions, CloudContext } from "@multicloud/sls-core"; 2 | import axios, { AxiosRequestConfig } from "axios"; 3 | import { ComponentType } from "@multicloud/sls-core"; 4 | import { injectable, inject } from "inversify"; 5 | 6 | /** 7 | * Options for Azure Function invocation 8 | */ 9 | export interface AzureCloudServiceOptions extends CloudServiceOptions { 10 | /** Name of function to invoke */ 11 | name: string; 12 | /** HTTP method of invocation */ 13 | method: string; 14 | /** URL for invocation */ 15 | http: string; 16 | } 17 | 18 | /** 19 | * Implementation of Cloud Service for Azure Functions. Invokes HTTP Azure Functions 20 | */ 21 | @injectable() 22 | export class AzureFunctionCloudService implements CloudService { 23 | 24 | /** 25 | * Initialize a new Azure Function Cloud Service with the IoC container 26 | * @param containerResolver IoC container for service resolution 27 | */ 28 | public constructor(@inject(ComponentType.CloudContext) context: CloudContext) { 29 | this.containerResolver = context.container; 30 | } 31 | 32 | public containerResolver: ContainerResolver; 33 | 34 | /** 35 | * 36 | * @param name Name of function to invoke 37 | * @param fireAndForget Wait for response if false (default behavior) 38 | * @param payload Body of HTTP request 39 | */ 40 | public async invoke(name: string, fireAndForget = false, payload: any = null) { 41 | if (!name || name.length === 0) { 42 | throw Error("Name is needed"); 43 | } 44 | 45 | const context = this.containerResolver.resolve(name); 46 | if (!context.method || !context.http) { 47 | throw Error("Missing Data"); 48 | } 49 | 50 | const axiosRequestConfig: AxiosRequestConfig = { 51 | url: context.http, 52 | method: context.method, 53 | data: payload, 54 | }; 55 | 56 | if (fireAndForget) { 57 | axios.request(axiosRequestConfig) 58 | return Promise.resolve(undefined); 59 | } else { 60 | return await axios.request(axiosRequestConfig); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /azure/src/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./azureBlobStorage"; 2 | export * from "./azureFunctionCloudService"; 3 | -------------------------------------------------------------------------------- /azure/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 5 | "outDir": "lib", 6 | "lib": [ 7 | "es6", 8 | "dom" 9 | ], /* Redirect output structure to the directory. */ 10 | "experimentalDecorators": true, 11 | "emitDecoratorMetadata": true 12 | }, 13 | "include": [ 14 | "./src" 15 | ], 16 | "exclude": [ 17 | "**/*.test.ts" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /core/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "babel-plugin-transform-typescript-metadata" 4 | ], 5 | "presets": [ 6 | [ 7 | "react-app", 8 | { 9 | "flow": false, 10 | "typescript": true 11 | } 12 | ] 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /core/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "project": "./tsconfig.json" 5 | }, 6 | "plugins": ["@typescript-eslint"], 7 | "extends": ["plugin:@typescript-eslint/recommended"], 8 | "rules": { 9 | "quotes": ["error", "double"], 10 | "@typescript-eslint/indent": [ 11 | "error", 12 | 2 13 | ], 14 | "@typescript-eslint/no-explicit-any": 0, 15 | "@typescript-eslint/explicit-function-return-type": 0, 16 | "@typescript-eslint/no-parameter-properties": 0, 17 | "@typescript-eslint/no-use-before-define": 0, 18 | "@typescript-eslint/no-object-literal-type-assertion": 0 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /core/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Serverless, Inc. http://www.serverless.com 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /core/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "collectCoverageFrom": [ 3 | "src/**/*.{js,jsx,ts,tsx}", 4 | "!src/**/*.d.ts" 5 | ], 6 | "testEnvironment": "node", 7 | "transform": { 8 | "^.+\\.(js|jsx|ts|tsx)$": "babel-jest" 9 | }, 10 | "testPathIgnorePatterns": [ 11 | "./lib", 12 | "./node_modules" 13 | ], 14 | "transformIgnorePatterns": [ 15 | "./lib", 16 | "./node_modules" 17 | ], 18 | "moduleFileExtensions": [ 19 | "js", 20 | "ts", 21 | "tsx", 22 | "json", 23 | "jsx", 24 | "node" 25 | ] 26 | }; -------------------------------------------------------------------------------- /core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@multicloud/sls-core", 3 | "version": "0.1.1", 4 | "description": "Core middleware and components for Serverless @multicloud.", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/serverless/multicloud" 8 | }, 9 | "keywords": [ 10 | "serverless", 11 | "sls", 12 | "serverless framework", 13 | "multicloud" 14 | ], 15 | "main": "lib/index.js", 16 | "types": "lib/index.d.ts", 17 | "scripts": { 18 | "start": "npm run test -- --watch", 19 | "lint": "eslint src/**/*.ts", 20 | "lint:fix": "npm run lint -- --fix", 21 | "pretest": "npm run lint", 22 | "test": "jest", 23 | "test:ci": "npm run test -- --ci", 24 | "test:coverage": "npm run test -- --coverage", 25 | "prebuild": "shx rm -rf lib/ && npm run test", 26 | "build": "npx tsc" 27 | }, 28 | "files": [ 29 | "lib/" 30 | ], 31 | "author": "Microsoft Corporation, 7-Eleven & Serverless Inc", 32 | "license": "MIT", 33 | "dependencies": { 34 | "inversify": "5.0.1", 35 | "reflect-metadata": "0.1.13" 36 | }, 37 | "devDependencies": { 38 | "@types/jest": "^24.0.15", 39 | "@types/node": "12.0.8", 40 | "@typescript-eslint/eslint-plugin": "^1.9.0", 41 | "@typescript-eslint/parser": "^1.9.0", 42 | "babel-jest": "^24.8.0", 43 | "babel-plugin-transform-typescript-metadata": "0.2.2", 44 | "babel-preset-react-app": "^9.0.0", 45 | "eslint": "^5.16.0", 46 | "jest": "^24.8.0", 47 | "shx": "^0.3.2", 48 | "typescript": "^3.5.2" 49 | }, 50 | "engines": { 51 | "node": ">=8.16.0", 52 | "npm": ">=6.4.1" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /core/pipelines/ci.yml: -------------------------------------------------------------------------------- 1 | # Node.js 2 | # Build a general Node.js project with npm. 3 | # Add steps that analyze code, save build artifacts, deploy, and more: 4 | # https://docs.microsoft.com/azure/devops/pipelines/languages/javascript 5 | 6 | # CI should only run when PRs are opened against dev/master. When integrating AZDo repos with 7 | # AZDo pipelines, the only way to do this is through `build validations` - branch-specific 8 | # integrations that can only be configured via AzDO UI. 9 | trigger: none 10 | 11 | pr: 12 | - master 13 | - dev 14 | 15 | pool: 16 | vmImage: 'ubuntu-latest' 17 | 18 | steps: 19 | - task: NodeTool@0 20 | displayName: 'Use Node 10.x' 21 | inputs: 22 | versionSpec: 10.x 23 | 24 | - task: Bash@3 25 | displayName: 'Build sls-core' 26 | inputs: 27 | targetType: filePath 28 | filePath: ./scripts/build.sh 29 | workingDirectory: core 30 | env: 31 | NPM_TOKEN: $(NPM_TOKEN) 32 | -------------------------------------------------------------------------------- /core/pipelines/publish-prerelease.yml: -------------------------------------------------------------------------------- 1 | # Node.js 2 | # Build a general Node.js project with npm. 3 | # Add steps that analyze code, save build artifacts, deploy, and more: 4 | # https://docs.microsoft.com/azure/devops/pipelines/languages/javascript 5 | 6 | # Only publish to AzDO NPM when a tag is pushed to master. 7 | # NOTE: a PR needs to be opened from dev --> master, which includes version bump. 8 | # If needs be, we can eventually automate. 9 | trigger: 10 | branches: 11 | include: 12 | - dev 13 | paths: 14 | include: 15 | - core/* 16 | 17 | pr: none 18 | 19 | pool: 20 | vmImage: 'ubuntu-latest' 21 | 22 | variables: 23 | - group: GitHub-Deploy-Creds 24 | - group: npm-release-credentials 25 | 26 | steps: 27 | - task: NodeTool@0 28 | displayName: 'Use Node 10.x' 29 | inputs: 30 | versionSpec: 10.x 31 | 32 | # Download secure file 33 | # Download a secure file to the agent machine 34 | - task: DownloadSecureFile@1 35 | # name: sshKey # The name with which to reference the secure file's path on the agent, like $(mySecureFile.secureFilePath) 36 | inputs: 37 | secureFile: multicloud_id_rsa 38 | 39 | # Install an SSH key prior to a build or deployment 40 | - task: InstallSSHKey@0 # https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/utility/install-ssh-key?view=azure-devops 41 | inputs: 42 | knownHostsEntry: $(KNOWN_HOSTS_ENTRY) 43 | sshPublicKey: $(SSH_PUBLIC_KEY) 44 | #sshPassphrase: $(SSH_PASS_PHRASE) 45 | sshKeySecureFile: multicloud_id_rsa 46 | env: 47 | KNOWN_HOSTS_ENTRY: $(KNOWN_HOSTS_ENTRY) 48 | SSH_PUBLIC_KEY: $(SSH_PUBLIC_KEY) # map to the right format (camelCase) that Azure credentials understand 49 | 50 | - task: Bash@3 51 | name: BumpNpmVersion 52 | displayName: Bump NPM Prerelease Version 53 | inputs: 54 | targetType: filePath 55 | filePath: ./scripts/version.sh 56 | arguments: '@multicloud/sls-core' 57 | workingDirectory: core 58 | env: 59 | SOURCE_BRANCH: $(Build.SourceBranch) 60 | 61 | - task: Bash@3 62 | displayName: 'Publish sls-core to NPM' 63 | inputs: 64 | targetType: filePath 65 | filePath: ./scripts/publish.sh 66 | arguments: 'prerelease' 67 | workingDirectory: core 68 | env: 69 | NPM_TOKEN: $(NPM_TOKEN) 70 | -------------------------------------------------------------------------------- /core/pipelines/publish-release.yml: -------------------------------------------------------------------------------- 1 | # Node.js 2 | # Build a general Node.js project with npm. 3 | # Add steps that analyze code, save build artifacts, deploy, and more: 4 | # https://docs.microsoft.com/azure/devops/pipelines/languages/javascript 5 | 6 | # Only publish to AzDO NPM when a tag is pushed to master. 7 | # NOTE: a PR needs to be opened from dev --> master, which includes version bump. 8 | # If needs be, we can eventually automate. 9 | trigger: none 10 | 11 | pr: none 12 | 13 | pool: 14 | vmImage: 'ubuntu-latest' 15 | 16 | variables: 17 | - group: GitHub-Deploy-Creds 18 | - group: npm-release-credentials 19 | 20 | steps: 21 | - task: NodeTool@0 22 | displayName: 'Use Node 10.x' 23 | inputs: 24 | versionSpec: 10.x 25 | 26 | # Download secure file 27 | # Download a secure file to the agent machine 28 | - task: DownloadSecureFile@1 29 | # name: sshKey # The name with which to reference the secure file's path on the agent, like $(mySecureFile.secureFilePath) 30 | inputs: 31 | secureFile: multicloud_id_rsa 32 | 33 | # Install an SSH key prior to a build or deployment 34 | - task: InstallSSHKey@0 # https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/utility/install-ssh-key?view=azure-devops 35 | inputs: 36 | knownHostsEntry: $(KNOWN_HOSTS_ENTRY) 37 | sshPublicKey: $(SSH_PUBLIC_KEY) 38 | #sshPassphrase: $(SSH_PASS_PHRASE) 39 | sshKeySecureFile: multicloud_id_rsa 40 | env: 41 | KNOWN_HOSTS_ENTRY: $(KNOWN_HOSTS_ENTRY) 42 | SSH_PUBLIC_KEY: $(SSH_PUBLIC_KEY) # map to the right format (camelCase) that Azure credentials understand 43 | 44 | - task: Bash@3 45 | name: BumpNpmVersion 46 | displayName: Bump NPM Prerelease Version 47 | inputs: 48 | targetType: filePath 49 | filePath: ./scripts/version.sh 50 | arguments: '@multicloud/sls-core $(NPM_RELEASE_TYPE)' 51 | workingDirectory: core 52 | env: 53 | SOURCE_BRANCH: $(Build.SourceBranch) 54 | 55 | - task: Bash@3 56 | displayName: 'Publish sls-core to NPM' 57 | inputs: 58 | targetType: filePath 59 | filePath: ./scripts/publish.sh 60 | workingDirectory: core 61 | env: 62 | NPM_TOKEN: $(NPM_TOKEN) 63 | -------------------------------------------------------------------------------- /core/src/builders/mockBuilder.ts: -------------------------------------------------------------------------------- 1 | import { MockedService } from "./mockedService"; 2 | 3 | export class MockBuilder { 4 | public service: any = null; 5 | public method: string; 6 | public withCallback: boolean; 7 | public result: any = null; 8 | public error: any = null; 9 | 10 | /** 11 | * Set property withCallback 12 | * @return param withCallback set to true 13 | */ 14 | public makeCallback() { 15 | this.withCallback = true; 16 | return this; 17 | } 18 | 19 | /** 20 | * Set property service 21 | * @param service service to mock 22 | * @return param service set to the service to mock 23 | */ 24 | public setService(service: any): MockBuilder { 25 | this.service = service; 26 | return this; 27 | } 28 | 29 | /** 30 | * Set property mtehod 31 | * @param method method to mock 32 | * @return param method set to the method to mock 33 | */ 34 | public setMethod(method: string): MockBuilder { 35 | this.method = method; 36 | return this; 37 | } 38 | 39 | /** 40 | * Set property result 41 | * @param result result to mock 42 | * @return param result set to the result to mock 43 | */ 44 | public setResult(result: any): MockBuilder { 45 | this.result = result; 46 | this.error = null; 47 | return this; 48 | } 49 | 50 | /** 51 | * Set property error 52 | * @param error error to mock 53 | * @return param error set to the error to mock 54 | */ 55 | public setError(error: any): MockBuilder { 56 | this.error = error; 57 | this.result = null; 58 | return this; 59 | } 60 | 61 | /** 62 | * Method for build the mock 63 | * @return Module mocked with all params set 64 | */ 65 | public build(): MockedService { 66 | const mock = new MockedService(this).getMock(); 67 | this.reset(); 68 | return mock; 69 | } 70 | 71 | /** 72 | * Get withCallback property 73 | * @return withCallack value 74 | */ 75 | public isHavingCallback(): boolean { 76 | return this.withCallback; 77 | } 78 | 79 | /** 80 | * Get service property 81 | * @return service value 82 | */ 83 | public getService(): any { 84 | return this.service; 85 | } 86 | 87 | /** 88 | * Get method property 89 | * @return method value 90 | */ 91 | public getMethod(): string { 92 | return this.method; 93 | } 94 | 95 | /** 96 | * Get result property 97 | * @return result value 98 | */ 99 | public getResult(): any { 100 | return this.result; 101 | } 102 | 103 | /** 104 | * Get error property 105 | * @return error value 106 | */ 107 | public getError(): any { 108 | return this.error; 109 | } 110 | 111 | /** 112 | * Reset all params to the initial values 113 | * @return service, method, withCallback, result and error set to initial values 114 | */ 115 | private reset() { 116 | this.service = null; 117 | this.method = ""; 118 | this.withCallback = false; 119 | this.result = null; 120 | this.error = null; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /core/src/builders/mockedService.ts: -------------------------------------------------------------------------------- 1 | import { MockBuilder } from "./mockBuilder"; 2 | 3 | export class MockedService { 4 | public mock: any; 5 | public service: Function; 6 | public method: string; 7 | public builder: Function; 8 | public result: Function; 9 | public error: Error; 10 | public isHavingCallback: boolean; 11 | 12 | public constructor(builder: MockBuilder) { 13 | this.mock = null; 14 | this.service = builder.getService(); 15 | this.method = builder.getMethod(); 16 | this.result = builder.getResult(); 17 | this.error = builder.getError(); 18 | this.isHavingCallback = builder.isHavingCallback(); 19 | 20 | this.mockFunction(this.service, this.method, this.error, this.result); 21 | 22 | } 23 | 24 | /** 25 | * Returns the mock to be used in the unit tests 26 | * @return jest mock object 27 | */ 28 | public getMock(): MockedService { 29 | return this.mock; 30 | } 31 | 32 | /** 33 | * Creates a mock function for services to used in the unit tests 34 | * @param service Service to mock 35 | * @param methodName Method name to mock 36 | * @param error Error value 37 | * @param result Result value 38 | */ 39 | public mockFunction(service: any, methodName: string, error: Error, result: any) { 40 | let mock; 41 | 42 | if (this.isHavingCallback) { 43 | mock = jest.fn((_, callback) => { 44 | callback(error, result); 45 | }); 46 | } 47 | else { 48 | mock = jest.fn(() => { 49 | if (error) { 50 | throw error; 51 | } 52 | return result; 53 | }); 54 | } 55 | 56 | if (service.mockImplementation) { 57 | service.mockImplementation(() => ({ 58 | [methodName]: mock 59 | })); 60 | } 61 | else { 62 | service[methodName] = mock; 63 | } 64 | 65 | this.mock = mock; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /core/src/builders/service/testService.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This is a simple test service created with the goal of explain how to use the mockBuilder 3 | */ 4 | export class TestService { 5 | private message: string; 6 | 7 | public getMessage(message: string, callback: any) { 8 | this.message = message; 9 | callback(null, this.message); 10 | } 11 | 12 | public returnHello(name: string) { 13 | return `hello ${name}` 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /core/src/cloudContext.ts: -------------------------------------------------------------------------------- 1 | import { CloudContainer } from "./cloudContainer"; 2 | import { CloudRequest } from "./cloudRequest"; 3 | import { CloudResponse } from "./cloudResponse"; 4 | import { CloudStorage } from "./services/cloudStorage"; 5 | import { Logger } from "./services/logger"; 6 | import { CloudService } from "./services/cloudService"; 7 | import { TelemetryService } from "./services/telemetry"; 8 | 9 | /** 10 | * Cloud Provider Runtime 11 | */ 12 | export interface CloudProviderRuntime { 13 | context: any; 14 | event: any; 15 | } 16 | 17 | /** 18 | * Common context for Serverless functions 19 | */ 20 | export interface CloudContext { 21 | /** Cloud provider type */ 22 | providerType: string; 23 | /** Request ID */ 24 | id: string; 25 | /** Incoming request */ 26 | event: any; 27 | /** Container for Cloud Services */ 28 | container?: CloudContainer; 29 | /** Common Request for Serverless Functions */ 30 | req?: CloudRequest; 31 | /** Common Response for Serverless Functions */ 32 | res?: CloudResponse; 33 | /** Storage Service */ 34 | storage?: CloudStorage; 35 | /** Logging Service */ 36 | logger?: Logger; 37 | /** Invocation Service */ 38 | service?: CloudService; 39 | /** Original cloud-specific event context */ 40 | runtime?: CloudProviderRuntime; 41 | /** Telemetry Service */ 42 | telemetry?: TelemetryService; 43 | /** Send response */ 44 | send: (body: any, status: number) => void; 45 | /** Signals the runtime that handler has completed */ 46 | done: () => void; 47 | /** Flushes the final response to the cloud providers */ 48 | flush: () => void; 49 | } 50 | 51 | /** 52 | * Currently supported cloud provider types. 53 | */ 54 | export enum ProviderType { 55 | Azure = "azure", 56 | AWS = "aws" 57 | } 58 | -------------------------------------------------------------------------------- /core/src/cloudMessage.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Normalized format for all messages published from pub/sub systems 3 | */ 4 | export interface CloudMessage { 5 | id: string; 6 | body: any; 7 | timestamp: Date; 8 | eventSource: string; 9 | [key: string]: any; 10 | } 11 | -------------------------------------------------------------------------------- /core/src/cloudRequest.ts: -------------------------------------------------------------------------------- 1 | import { StringParams } from "./common/stringParams"; 2 | 3 | /** 4 | * Common HTTP Request for Serverless functions 5 | */ 6 | export interface CloudRequest { 7 | /** Body of request */ 8 | body?: any; 9 | /** Headers of request */ 10 | headers?: StringParams; 11 | /** HTTP Method */ 12 | method: string; 13 | /** Query parameters */ 14 | query?: StringParams; 15 | /** Path parameters */ 16 | pathParams?: StringParams; 17 | } 18 | -------------------------------------------------------------------------------- /core/src/cloudResponse.ts: -------------------------------------------------------------------------------- 1 | import { StringParams } from "./common/stringParams"; 2 | 3 | /** 4 | * Common HTTP Response for Serverless functions 5 | */ 6 | export interface CloudResponse { 7 | /** Headers of response */ 8 | headers?: StringParams; 9 | body: string; 10 | status: number; 11 | 12 | /** 13 | * Send response 14 | * @param body Body of response 15 | * @param status Status code for response 16 | * @param callback Callback to call with response 17 | */ 18 | send: (body: any, status: number, callback?: Function) => void; 19 | 20 | /** 21 | * Flushes final response and signals cloud provider runtime that request is complete 22 | */ 23 | flush: () => void; 24 | } 25 | 26 | /** 27 | * Cloud provider response header name. 28 | */ 29 | export const CloudProviderResponseHeader = "x-sls-cloud-provider"; 30 | -------------------------------------------------------------------------------- /core/src/common/guard.test.ts: -------------------------------------------------------------------------------- 1 | import { Guard } from "./guard"; 2 | 3 | describe("Guard", () => { 4 | function methodWithRequiredName(name: string) { 5 | Guard.empty(name); 6 | } 7 | 8 | function methodWithRequiredNameWithParam(name: string) { 9 | Guard.empty(name, "name", "Name is required"); 10 | } 11 | 12 | function methodWithRequiredObject(options: any) { 13 | Guard.null(options); 14 | } 15 | 16 | function methodWithRequiredExpression(value: number) { 17 | Guard.expression(value, (num) => num > 0 && num < 100); 18 | } 19 | 20 | describe("empty", () => { 21 | it("throws error on null value", () => { 22 | expect(() => methodWithRequiredName(null)).toThrowError(); 23 | }); 24 | 25 | it("throws error on empty value", () => { 26 | expect(() => methodWithRequiredName("")).toThrowError(); 27 | }); 28 | 29 | it("throw error on whitespace", () => { 30 | expect(() => methodWithRequiredName(" ")).toThrowError(); 31 | }); 32 | 33 | it("does not throw error on valid value", () => { 34 | expect(() => methodWithRequiredName("valid")).not.toThrowError(); 35 | }); 36 | 37 | it("throws specific error message", () => { 38 | expect(() => methodWithRequiredNameWithParam(null)).toThrowError("Name is required"); 39 | }); 40 | }); 41 | 42 | describe("null", () => { 43 | it("throws error on null value", () => { 44 | expect(() => methodWithRequiredObject(null)).toThrowError(); 45 | }); 46 | 47 | it("does not throw error on valid value", () => { 48 | expect(() => methodWithRequiredObject({})).not.toThrowError(); 49 | }); 50 | }); 51 | 52 | describe("expression", () => { 53 | it("throws error on invalide value", () => { 54 | expect(() => methodWithRequiredExpression(0)).toThrowError(); 55 | }); 56 | 57 | it("does not throw error on valid value", () => { 58 | expect(() => methodWithRequiredExpression(1)).not.toThrowError(); 59 | }); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /core/src/common/guard.ts: -------------------------------------------------------------------------------- 1 | export class Guard { 2 | /** 3 | * Validates the string express is not null or empty, otherwise throws an exception 4 | * @param value - The value to validate 5 | * @param paramName - The name of the parameter to validate 6 | * @param message - The error message to return on invalid value 7 | */ 8 | public static empty(value: string, paramName?: string, message?: string) { 9 | if ((!!value === false || value.trim().length === 0)) { 10 | message = message || (`'${paramName || "value"}' cannot be null or empty`); 11 | throw new Error(message); 12 | } 13 | } 14 | 15 | /** 16 | * Validates the value is not null, otherwise throw an exception 17 | * @param value - The value to validate 18 | * @param paramName - The name of the parameter to validate 19 | * @param message - The error message to return on invalid value 20 | */ 21 | public static null(value: any, paramName?: string, message?: string) { 22 | if ((!!value === false)) { 23 | message = message || (`'${paramName || "value"}' cannot be null or undefined`); 24 | throw new Error(message); 25 | } 26 | } 27 | 28 | /** 29 | * Validates the value meets the specified expectation, otherwise throws an exception 30 | * @param value - The value to validate 31 | * @param predicate - The predicate used for validation 32 | * @param paramName - The name of the parameter to validate 33 | * @param message - The error message to return on invalid value 34 | */ 35 | public static expression(value: T, predicate: (value: T) => boolean, paramName?: string, message?: string) { 36 | if (!!value === false || !predicate(value)) { 37 | message = message || (`'${paramName || "value"}' is not a valid value`); 38 | throw new Error(message); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /core/src/common/stringParams.ts: -------------------------------------------------------------------------------- 1 | import { Guard } from "./guard"; 2 | 3 | /** 4 | * Create a case insensitive string parameter map 5 | * Used in HTTP request / response for headers, query strings & path params 6 | */ 7 | export class StringParams extends Map { 8 | 9 | public constructor(entries?: Iterable<[string, T]> | any) { 10 | if (entries && entries.constructor && entries.constructor.name === "Object") { 11 | entries = Object.keys(entries).map((key) => [key, entries[key]]); 12 | } 13 | 14 | super(entries) 15 | } 16 | 17 | /** 18 | * Sets a value at the specified key 19 | * @param key The key to set 20 | * @param value The value of the key 21 | */ 22 | public set(key: string, value: T): this { 23 | Guard.empty(key); 24 | return super.set(this.normalizeKey(key), value); 25 | } 26 | 27 | /** 28 | * Gets the value for the specified key 29 | * @param key The key to get 30 | */ 31 | public get(key: string): T | undefined { 32 | Guard.empty(key); 33 | return super.get(this.normalizeKey(key)); 34 | } 35 | 36 | /** 37 | * Checks whether a key exists within the map 38 | * @param key The key to check 39 | */ 40 | public has(key: string): boolean { 41 | Guard.empty(key); 42 | return super.has(this.normalizeKey(key)); 43 | } 44 | 45 | /** 46 | * Delete the key with the specified value 47 | * @param key The key to delete 48 | */ 49 | public delete(key: string): boolean { 50 | Guard.empty(key); 51 | return super.delete(this.normalizeKey(key)); 52 | } 53 | 54 | /** 55 | * Serializes the map to a plain javascript object for use with JSON.stringify 56 | */ 57 | public toJSON() { 58 | const output = {}; 59 | 60 | this.forEach((value, key) => { 61 | output[key] = value; 62 | }); 63 | 64 | return output; 65 | } 66 | 67 | /** 68 | * Normalizes the specified key to lower case 69 | * @param key The key to normalize 70 | */ 71 | private normalizeKey(key: string) { 72 | return key.toLowerCase(); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /core/src/common/util.test.ts: -------------------------------------------------------------------------------- 1 | import { ensurePromise, convertToStream } from "./util"; 2 | import { Readable } from "stream"; 3 | 4 | describe("util", () => { 5 | describe("ensurePromise", () => { 6 | it("returns the promise if value is a promise", async () => { 7 | const expected = "value"; 8 | const promise = Promise.resolve(expected); 9 | const actual = await ensurePromise(promise); 10 | 11 | expect(actual).toBe(expected); 12 | }); 13 | 14 | it("returns a wrapped promise if the value is a static value", async () => { 15 | const expected = "value"; 16 | const actual = await ensurePromise(expected); 17 | 18 | expect(actual).toBe(expected); 19 | }); 20 | }); 21 | describe("convertToStream", () => { 22 | const input = "foo"; 23 | 24 | const streamToString = (stream, cb) => { 25 | const chunks = []; 26 | stream.on("data", (chunk) => { 27 | chunks.push(chunk.toString()); 28 | }); 29 | stream.on("end", () => { 30 | cb(chunks.join("")); 31 | }); 32 | 33 | return chunks; 34 | } 35 | 36 | it("receives a string and returns a stream", (done) => { 37 | const actualStream = convertToStream(input); 38 | streamToString(actualStream, (result) => { 39 | expect(result).toEqual(input); 40 | expect(actualStream).toBeInstanceOf(Readable); 41 | done(); 42 | }); 43 | }); 44 | 45 | it("receives a Buffer and returns a stream", (done) => { 46 | const actualStream = convertToStream(Buffer.from(input)); 47 | streamToString(actualStream, (result) => { 48 | expect(result).toEqual(input); 49 | expect(actualStream).toBeInstanceOf(Readable); 50 | done(); 51 | }); 52 | }); 53 | 54 | it("receives and returns a Stream", (done) => { 55 | let inputStream = new Readable; 56 | inputStream.push(input); 57 | inputStream.push(null); 58 | const actualStream = convertToStream(inputStream); 59 | streamToString(actualStream, (result) => { 60 | expect(result).toEqual(input); 61 | expect(actualStream).toBeInstanceOf(Readable); 62 | done(); 63 | }); 64 | }); 65 | 66 | it("throws an exception when input value is not string/Buffer/Stream", (done) => { 67 | const value = 100 68 | expect(() => convertToStream(value as unknown as string)).toThrowError(); 69 | done(); 70 | }); 71 | 72 | it("throws an exception if input is null", (done) => { 73 | expect(() => convertToStream(null)).toThrowError(); 74 | done(); 75 | }); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /core/src/common/util.ts: -------------------------------------------------------------------------------- 1 | import { Stream, Readable } from "stream"; 2 | import { Guard } from "./guard"; 3 | 4 | /** 5 | * Ensures that the specified value is wrapped as a promise 6 | * @param value The value to evaluate 7 | */ 8 | export function ensurePromise(value: T | Promise) { 9 | const promise = value as Promise; 10 | if (promise && promise.then && promise.catch) { 11 | return promise; 12 | } 13 | 14 | return Promise.resolve(value); 15 | } 16 | 17 | /** 18 | * Converts the input to Stream 19 | * @param input Data to be converted to Stream 20 | */ 21 | export function convertToStream(input: string | Buffer | Stream): Readable { 22 | Guard.null(input, "input"); 23 | 24 | let readable; 25 | 26 | if (input instanceof Stream.Readable) { 27 | readable = input; 28 | } else if (input instanceof Buffer || typeof input === "string") { 29 | readable = new Readable(); 30 | readable.push(input); 31 | readable.push(null); 32 | } else { 33 | throw new Error("input type not supported"); 34 | } 35 | 36 | return readable; 37 | } 38 | -------------------------------------------------------------------------------- /core/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./app"; 2 | export * from "./common/guard"; 3 | export * from "./common/stringParams"; 4 | export * from "./common/util"; 5 | export * from "./cloudContext"; 6 | export * from "./cloudRequest"; 7 | export * from "./cloudResponse"; 8 | export * from "./cloudContainer"; 9 | export * from "./cloudMessage"; 10 | export * from "./middleware"; 11 | export * from "./services"; 12 | export * from "./test"; 13 | -------------------------------------------------------------------------------- /core/src/middleware/exceptionMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { Middleware } from "../app"; 2 | import { CloudContext } from "../cloudContext"; 3 | 4 | /** 5 | * Options for handling exceptions 6 | */ 7 | export interface ExceptionOptions { 8 | /** Log error message */ 9 | log: (error: string) => Promise; 10 | } 11 | 12 | /** 13 | * Middleware for handling exceptions. Returns async function that accepts the 14 | * CloudContext and the `next` Function in the middleware chain 15 | * @param options Options for handling exceptions 16 | */ 17 | export const ExceptionMiddleware = (options: ExceptionOptions): Middleware => 18 | (context: CloudContext, next: () => Promise): Promise => { 19 | function onError(err) { 20 | options.log(err); 21 | 22 | const result = { 23 | requestId: context.id, 24 | message: err.toString(), 25 | timestamp: new Date() 26 | }; 27 | 28 | context.send(result, 500); 29 | } 30 | 31 | try { 32 | return next().catch(onError); 33 | } catch (err) { 34 | onError(err); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /core/src/middleware/httpBindingMiddleware.test.ts: -------------------------------------------------------------------------------- 1 | import { MockFactory } from "../test/mockFactory"; 2 | import { App, Handler } from "../app"; 3 | import { CloudContext } from "../cloudContext"; 4 | import { HTTPBindingMiddleware } from "./httpBindingMiddleware"; 5 | 6 | describe("HTTPBindingMiddleware should", () => { 7 | let app: App; 8 | let handler: Handler; 9 | 10 | beforeEach(() => { 11 | app = new App(); 12 | handler = MockFactory.createMockHandler(); 13 | }) 14 | 15 | it("call next handler", async () => { 16 | await app.use([HTTPBindingMiddleware()], handler)(); 17 | 18 | expect(handler).toBeCalled(); 19 | }); 20 | 21 | it("calls the next middleware in the chain", async () => { 22 | const mockMiddleware = MockFactory.createMockMiddleware(); 23 | 24 | await app.use([HTTPBindingMiddleware(), mockMiddleware], handler)(); 25 | expect(mockMiddleware).toHaveBeenCalled(); 26 | expect(handler).toHaveBeenCalled(); 27 | }); 28 | 29 | it("sets the cloudRequest and cloudResponse during HTTP requests", async () => { 30 | const testMiddleware = MockFactory.createMockMiddleware(async (context: CloudContext, next: Function) => { 31 | expect(context.req).not.toBeNull(); 32 | expect(context.res).not.toBeNull(); 33 | 34 | await next(); 35 | }); 36 | 37 | await app.use([HTTPBindingMiddleware(), testMiddleware], handler)({}, { method: "GET" }); 38 | expect(testMiddleware).toBeCalled(); 39 | expect(handler).toBeCalled(); 40 | }); 41 | 42 | it("does not set the cloudRequest and cloudResponse and call next if the eventType is not HTTP", async () => { 43 | const testMiddleware = MockFactory.createMockMiddleware(async (context: CloudContext, next: Function) => { 44 | expect(context.req).toBeNull(); 45 | expect(context.res).toBeNull(); 46 | 47 | await next(); 48 | }); 49 | 50 | await app.use([HTTPBindingMiddleware(), testMiddleware], handler)(); 51 | expect(testMiddleware).toBeCalled(); 52 | expect(handler).toBeCalled(); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /core/src/middleware/httpBindingMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { Middleware } from "../app"; 2 | import { CloudContext } from "../cloudContext"; 3 | import { CloudRequest } from "../cloudRequest"; 4 | import { ComponentType } from "../cloudContainer"; 5 | import { CloudResponse } from "../cloudResponse"; 6 | 7 | /** 8 | * Middleware for HTTP bindings. Returns async function that accepts the 9 | * CloudContext and the `next` Function in the middleware chain 10 | */ 11 | export const HTTPBindingMiddleware = (): Middleware => 12 | async (context: CloudContext, next: () => Promise): Promise => { 13 | context.req = context.container.resolve(ComponentType.CloudRequest); 14 | context.res = context.container.resolve(ComponentType.CloudResponse); 15 | await next(); 16 | }; 17 | -------------------------------------------------------------------------------- /core/src/middleware/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./exceptionMiddleware"; 2 | export * from "./httpBindingMiddleware"; 3 | export * from "./loggingServiceMiddleware"; 4 | export * from "./performanceMiddleware"; 5 | export * from "./requestLoggingMiddleware"; 6 | export * from "./validationMiddleware"; 7 | export * from "./telemetryMiddleware"; 8 | export * from "./serviceMiddleware"; 9 | export * from "./storageMiddleware"; 10 | -------------------------------------------------------------------------------- /core/src/middleware/loggingServiceMiddleware.test.ts: -------------------------------------------------------------------------------- 1 | import { CloudContext } from "../cloudContext"; 2 | import { TestContext } from "../test/testContext"; 3 | import { LoggingServiceMiddleware } from "./loggingServiceMiddleware"; 4 | import { Logger, LogLevel } from "../services/logger"; 5 | import { ConsoleLogger } from "../services/consoleLogger"; 6 | 7 | describe("LoggingServiceMiddleware should", () => { 8 | class TestLogger implements Logger { 9 | public constructor(logLevel: LogLevel) { 10 | this.logLevel = logLevel; 11 | } 12 | 13 | public logLevel: LogLevel; 14 | public log = jest.fn(); 15 | public info = jest.fn(); 16 | public error = jest.fn(); 17 | public warn = jest.fn(); 18 | public debug = jest.fn(); 19 | public trace = jest.fn(); 20 | } 21 | 22 | let context: CloudContext; 23 | const logger = new TestLogger(LogLevel.VERBOSE); 24 | 25 | beforeEach(() => { 26 | context = new TestContext(); 27 | jest.clearAllMocks(); 28 | }); 29 | 30 | it("save the Logger in context.logger", async () => { 31 | const next = jest.fn(); 32 | await LoggingServiceMiddleware(logger)(context, next); 33 | expect(context.logger).toEqual(logger); 34 | }); 35 | 36 | it("save the defaultLogger in context.logger", async () => { 37 | const next = jest.fn(); 38 | await LoggingServiceMiddleware(null)(context, next); 39 | expect(context.logger).toBeInstanceOf(ConsoleLogger); 40 | }); 41 | 42 | it("call next middleware", async () => { 43 | const next = jest.fn(); 44 | await LoggingServiceMiddleware(logger)(context, next); 45 | expect(next).toBeCalled(); 46 | }); 47 | 48 | it("save the logger in context and be used from the next middleware", async () => { 49 | const next = jest.fn(); 50 | await LoggingServiceMiddleware(logger)(context, next); 51 | expect(context.logger).toEqual(logger); 52 | expect(next).toBeCalled(); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /core/src/middleware/loggingServiceMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from "../services/logger"; 2 | import { Middleware } from "../app"; 3 | import { CloudContext } from "../cloudContext"; 4 | import { ConsoleLogger } from "../services/consoleLogger"; 5 | 6 | /** 7 | * Middleware for logging. Returns async function that accepts the 8 | * CloudContext and the `next` Function in the middleware chain 9 | * @param logger Logging Service 10 | */ 11 | export const LoggingServiceMiddleware = (logger: Logger): Middleware => 12 | async (context: CloudContext, next: () => Promise): Promise => { 13 | context.logger = logger || new ConsoleLogger(); 14 | await next(); 15 | }; 16 | -------------------------------------------------------------------------------- /core/src/middleware/performanceMiddleware.test.ts: -------------------------------------------------------------------------------- 1 | import { TestContext } from "../test/testContext"; 2 | import { TestResponse } from "../test/testResponse"; 3 | import { ConsoleLogger } from "../services/consoleLogger"; 4 | import { PerformanceMiddleware, DurationResponseHeader, RequestIdResponseHeader } from "./performanceMiddleware" 5 | 6 | describe("PerformanceMiddleware should", () => { 7 | let context; 8 | 9 | beforeEach(() => { 10 | context = new TestContext(); 11 | context.res = new TestResponse(); 12 | context.logger = new ConsoleLogger(); 13 | context.logger.info = jest.fn(); 14 | }) 15 | 16 | afterEach(() => { 17 | jest.clearAllMocks(); 18 | }); 19 | 20 | it("collect and log performance metrics", async () => { 21 | const next = jest.fn(); 22 | await PerformanceMiddleware()(context, next); 23 | 24 | expect(context.logger.info).toBeCalledTimes(2); 25 | expect(context.res.headers.has(RequestIdResponseHeader)).toBe(true); 26 | expect(context.res.headers.has(DurationResponseHeader)).toBe(true); 27 | }); 28 | 29 | it("collect and log performance metrics, even if an exception is thrown", async () => { 30 | const failNext = () => { 31 | throw new Error("Unexpected Exception"); 32 | }; 33 | 34 | await expect(PerformanceMiddleware()(context, failNext)).rejects.toThrow(); 35 | expect(context.logger.info).toBeCalledTimes(2); 36 | expect(context.res.headers.has(RequestIdResponseHeader)).toBe(true); 37 | expect(context.res.headers.has(DurationResponseHeader)).toBe(true); 38 | }); 39 | 40 | it("call the next middleware when using App", async () => { 41 | const next = jest.fn(); 42 | await PerformanceMiddleware()(context, next); 43 | 44 | expect(next).toBeCalled(); 45 | expect(context.res.headers.has(RequestIdResponseHeader)).toBe(true); 46 | expect(context.res.headers.has(DurationResponseHeader)).toBe(true); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /core/src/middleware/performanceMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { performance, PerformanceObserver } from "perf_hooks"; 2 | import { Middleware } from "../app"; 3 | import { CloudContext } from "../cloudContext"; 4 | import { ConsoleLogger } from "../services/consoleLogger"; 5 | 6 | /** 7 | * Request ID response header name. 8 | */ 9 | export const RequestIdResponseHeader = "x-sls-request-id"; 10 | 11 | /** 12 | * Performance response header name. 13 | */ 14 | export const DurationResponseHeader = "x-sls-perf-duration"; 15 | 16 | /** 17 | * Middleware for logging performance of Serverless function. Returns 18 | * async function that accepts the CloudContext and the `next` Function 19 | * in the middleware chain 20 | */ 21 | export const PerformanceMiddleware = (): Middleware => 22 | async (context: CloudContext, next: () => Promise): Promise => { 23 | // NOTE: if the context provides a logger, use it, otherwise use the default console logger 24 | const logger = context.logger ? context.logger : new ConsoleLogger(); 25 | const start = `Function Start: ${context.id}`; 26 | const end = `Function End: ${context.id}`; 27 | 28 | try { 29 | const observer = new PerformanceObserver((list, innerObserver) => { 30 | const perfEntries = list.getEntriesByName(context.id); 31 | if (perfEntries && perfEntries.length) { 32 | const entry = perfEntries[0]; 33 | logger.info(`Function End, Request ID: ${context.id}, took ${entry.duration}ms`); 34 | 35 | if (context.res) { 36 | context.res.headers.set(RequestIdResponseHeader, context.id); 37 | context.res.headers.set(DurationResponseHeader, entry.duration.toString()); 38 | } 39 | } 40 | innerObserver.disconnect(); 41 | }); 42 | 43 | // fire performance observer events for all measure calls 44 | observer.observe({ entryTypes: ["measure"] }); 45 | 46 | performance.mark(start); 47 | logger.info(`Function Start, Request ID: ${context.id}`); 48 | await next(); 49 | } finally { 50 | performance.mark(end); 51 | performance.measure(context.id, start, end); 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /core/src/middleware/requestLoggingMiddleware.test.ts: -------------------------------------------------------------------------------- 1 | import { MockFactory } from "../test/mockFactory"; 2 | import { TestContext } from "../test/testContext"; 3 | import { Logger, LogLevel } from "../services/logger"; 4 | import { RequestLoggingMiddleware, LoggingOptions } from "./requestLoggingMiddleware"; 5 | import { App } from "../app"; 6 | 7 | describe("requestLoggingServiceMiddleware should", () => { 8 | class TestLogger implements Logger { 9 | public constructor(logLevel: LogLevel) { 10 | this.logLevel = logLevel; 11 | } 12 | 13 | public logLevel: LogLevel; 14 | public log = jest.fn(); 15 | public info = jest.fn(); 16 | public error = jest.fn(); 17 | public warn = jest.fn(); 18 | public debug = jest.fn(); 19 | public trace = jest.fn(); 20 | } 21 | 22 | const loggingOptions: LoggingOptions = { 23 | logger: new TestLogger(LogLevel.VERBOSE), 24 | handlerName: "GET myAPI", 25 | } 26 | 27 | const handler = MockFactory.createMockHandler(); 28 | let context; 29 | 30 | beforeEach(() => { 31 | jest.clearAllMocks(); 32 | context = new TestContext(); 33 | }); 34 | 35 | it("should call log twice with provided messages", async () => { 36 | const next = jest.fn(); 37 | await RequestLoggingMiddleware(loggingOptions)(context, next); 38 | expect(loggingOptions.logger.log).toBeCalledTimes(2); 39 | expect(loggingOptions.logger.log).toHaveBeenNthCalledWith(1, `Starting request for handler: ${loggingOptions.handlerName}`); 40 | expect(loggingOptions.logger.log).toHaveBeenNthCalledWith(2, `Finished request for handler: ${loggingOptions.handlerName}`); 41 | }); 42 | 43 | it("call next middleware", async () => { 44 | const next = jest.fn(); 45 | await RequestLoggingMiddleware(loggingOptions)(context, next); 46 | expect(next).toHaveBeenCalled(); 47 | }); 48 | 49 | it("call next middleware after requestLoggingMiddleware using App", async () => { 50 | const mockMiddleware = MockFactory.createMockMiddleware(); 51 | 52 | const sut = new App(); 53 | await sut.use([RequestLoggingMiddleware(loggingOptions), mockMiddleware], handler)(); 54 | expect(mockMiddleware).toHaveBeenCalled(); 55 | expect(handler).toHaveBeenCalled(); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /core/src/middleware/requestLoggingMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from "../services/logger"; 2 | import { Middleware } from "../app"; 3 | import { CloudContext } from "../cloudContext"; 4 | import { ConsoleLogger } from "../services/consoleLogger"; 5 | 6 | /** 7 | * Options for Request Logging 8 | */ 9 | export interface LoggingOptions { 10 | /** Logging Service */ 11 | logger: Logger; 12 | /** Name of handler for which to log messages */ 13 | handlerName: string; 14 | } 15 | 16 | /** 17 | * Middleware for logging start and stop of function handler. Returns 18 | * async function that accepts the CloudContext and the `next` Function 19 | * in the middleware chain 20 | * @param options Options for logging request 21 | */ 22 | export const RequestLoggingMiddleware = (options: LoggingOptions): Middleware => 23 | async (_: CloudContext, next: () => Promise): Promise => { 24 | const logger: Logger = options.logger || new ConsoleLogger(); 25 | 26 | logger.log(`Starting request for handler: ${options.handlerName}`); 27 | await next(); 28 | logger.log(`Finished request for handler: ${options.handlerName}`); 29 | }; 30 | -------------------------------------------------------------------------------- /core/src/middleware/serviceMiddleware.test.ts: -------------------------------------------------------------------------------- 1 | import { CloudContext } from "../cloudContext"; 2 | import { CloudContainer } from "../cloudContainer"; 3 | import { TestModule } from "../test/testModule"; 4 | import { TestCloudService } from "../test/testCloudService"; 5 | import { TestContext } from "../test/testContext"; 6 | import { ServiceMiddleware } from "./serviceMiddleware"; 7 | 8 | describe("ServiceMiddleware should", () => { 9 | let context: CloudContext; 10 | 11 | beforeEach(() => { 12 | context = new TestContext(); 13 | 14 | const container = new CloudContainer(); 15 | container.registerModule(new TestModule()); 16 | context.container = container; 17 | }) 18 | 19 | it("calls the next middleware in the chain", async () => { 20 | const next = jest.fn(); 21 | await ServiceMiddleware()(context, next); 22 | 23 | expect(next).toHaveBeenCalled(); 24 | }); 25 | 26 | it("sets the cloudService on the context", async () => { 27 | const next = jest.fn(); 28 | await ServiceMiddleware()(context, next); 29 | expect(context.service).toBeInstanceOf(TestCloudService); 30 | expect(next).toBeCalled(); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /core/src/middleware/serviceMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { Middleware } from "../app"; 2 | import { CloudContext } from "../cloudContext"; 3 | import { CloudService } from "../services/cloudService"; 4 | import { ComponentType } from "../cloudContainer"; 5 | 6 | /** 7 | * Middleware for Service binding. Returns async function that accepts the 8 | * CloudContext and the `next` Function in the middleware chain 9 | */ 10 | export const ServiceMiddleware = (): Middleware => 11 | async (context: CloudContext, next: () => Promise): Promise => { 12 | context.service = context.container.resolve(ComponentType.CloudService); 13 | await next(); 14 | }; 15 | -------------------------------------------------------------------------------- /core/src/middleware/storageMiddleware.test.ts: -------------------------------------------------------------------------------- 1 | import { MockFactory } from "../test/mockFactory"; 2 | import { App, Handler } from "../app"; 3 | import { CloudContext } from "../cloudContext"; 4 | import { StorageMiddleware } from "./storageMiddleware"; 5 | 6 | describe("StorageMiddleware should", () => { 7 | let app: App; 8 | let handler: Handler; 9 | 10 | beforeEach(() => { 11 | app = new App(); 12 | handler = MockFactory.createMockHandler(); 13 | }); 14 | 15 | it("call next handler", async () => { 16 | await app.use([StorageMiddleware()], handler)(); 17 | 18 | expect(handler).toBeCalled(); 19 | }); 20 | 21 | it("calls the next middleware in the chain", async () => { 22 | const mockMiddleware = MockFactory.createMockMiddleware(); 23 | 24 | await app.use([StorageMiddleware(), mockMiddleware], handler)(); 25 | expect(mockMiddleware).toHaveBeenCalled(); 26 | expect(handler).toHaveBeenCalled(); 27 | }); 28 | 29 | it("sets the cloudStorage during HTTP requests", async () => { 30 | const testMiddleware = MockFactory.createMockMiddleware(async (context: CloudContext, next: Function) => { 31 | expect(context.storage).not.toBeNull(); 32 | 33 | await next(); 34 | }); 35 | 36 | await app.use([StorageMiddleware(), testMiddleware], handler)({}, { method: "GET" }); 37 | expect(testMiddleware).toBeCalled(); 38 | expect(handler).toBeCalled(); 39 | }); 40 | 41 | it("does not set the cloudStorage and call next if the eventType is not HTTP", async () => { 42 | const testMiddleware = MockFactory.createMockMiddleware(async (context: CloudContext, next: Function) => { 43 | expect(context.storage).not.toBeNull(); 44 | 45 | await next(); 46 | }); 47 | 48 | await app.use([StorageMiddleware(), testMiddleware], handler)(); 49 | expect(testMiddleware).toBeCalled(); 50 | expect(handler).toBeCalled(); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /core/src/middleware/storageMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { Middleware } from "../app"; 2 | import { CloudContext } from "../cloudContext"; 3 | import { CloudStorage } from "../services/cloudStorage"; 4 | import { ComponentType } from "../cloudContainer"; 5 | 6 | /** 7 | * Middleware for adding the storage service to the context object. 8 | * It will allow to read, upload and delete files in the cloud. 9 | */ 10 | export const StorageMiddleware = (): Middleware => 11 | async (context: CloudContext, next: () => Promise): Promise => { 12 | context.storage = context.container.resolve(ComponentType.CloudStorage); 13 | await next(); 14 | }; 15 | -------------------------------------------------------------------------------- /core/src/middleware/telemetryMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { cpus, totalmem, freemem } from "os"; 2 | import { TelemetryOptions } from "../services/telemetry"; 3 | import { Middleware } from "../app"; 4 | import { CloudContext } from "../cloudContext"; 5 | 6 | /** 7 | * 8 | * @param options Options for Telemetry 9 | */ 10 | export const TelemetryServiceMiddleware = (options: TelemetryOptions): Middleware => 11 | async (context: CloudContext, next: () => Promise): Promise => { 12 | const initialCpuAverage = CpuAverage(); 13 | const usedMemBeforeChain = GetUsedMemory(); 14 | 15 | context.telemetry = options.telemetryService; 16 | await next(); 17 | 18 | const finalCpuAverage = CpuAverage(); 19 | const consumeCpuIdle = finalCpuAverage.idle - initialCpuAverage.idle; 20 | const consumeCpuTick = finalCpuAverage.tick - initialCpuAverage.tick; 21 | const memoryConsume = GetUsedMemory() - usedMemBeforeChain; 22 | 23 | const stats = { 24 | consumeCpuIdle, 25 | consumeCpuTick, 26 | memoryConsume 27 | }; 28 | 29 | context.telemetry.collect("stats", stats); 30 | 31 | if (options.shouldFlush) { 32 | context.telemetry.flush(); 33 | } 34 | }; 35 | 36 | const CpuAverage = () => { 37 | //Initialise sum of idle and time of cores and fetch CPU info 38 | var totalIdle = 0, 39 | totalTick = 0; 40 | var myCpus = cpus(); 41 | 42 | //Loop through CPU cores 43 | for (var i = 0, len = myCpus.length; i < len; i++) { 44 | //Select CPU core 45 | var cpu = myCpus[i]; 46 | 47 | //Total up the time in the cores tick 48 | totalTick += cpu.times.user; 49 | totalTick += cpu.times.nice; 50 | totalTick += cpu.times.sys; 51 | totalTick += cpu.times.idle; 52 | totalTick += cpu.times.irq; 53 | 54 | //Total up the idle time of the core 55 | totalIdle += cpu.times.idle; 56 | } 57 | 58 | //Return the average Idle and Tick times 59 | return { idle: totalIdle / myCpus.length, tick: totalTick / myCpus.length }; 60 | }; 61 | 62 | const GetUsedMemory = () => { 63 | return totalmem() - freemem(); 64 | }; 65 | -------------------------------------------------------------------------------- /core/src/middleware/validationMiddleware.test.ts: -------------------------------------------------------------------------------- 1 | import { CloudContext } from "../cloudContext"; 2 | import { Middleware } from "../app"; 3 | import { TestContext } from "../test/testContext"; 4 | import { createValidationMiddleware, ValidationResult, ValidationOptions } from "./validationMiddleware"; 5 | 6 | describe("Validation Middleware", () => { 7 | const successValidation: ValidationResult = { 8 | hasError: jest.fn().mockReturnValue(false), 9 | send: jest.fn() 10 | }; 11 | 12 | const failValidation: ValidationResult = { 13 | hasError: jest.fn().mockReturnValue(true), 14 | send: jest.fn() 15 | }; 16 | 17 | const next = jest.fn(); 18 | 19 | const options: ValidationOptions = { 20 | validate: jest.fn() 21 | }; 22 | 23 | let context: CloudContext; 24 | 25 | let middleware: Middleware = undefined; 26 | beforeEach(() => { 27 | jest.clearAllMocks(); 28 | middleware = createValidationMiddleware(options); 29 | context = new TestContext(); 30 | }); 31 | 32 | it("call validate", async () => { 33 | options.validate = jest.fn().mockResolvedValue(successValidation); 34 | await middleware(context, next); 35 | expect(options.validate).toBeCalledWith(context); 36 | }); 37 | 38 | it("call send on error", async () => { 39 | options.validate = jest.fn().mockResolvedValue(failValidation); 40 | await middleware(context, next); 41 | expect(failValidation.send).toBeCalled(); 42 | }); 43 | 44 | it("don't call next on error", async () => { 45 | options.validate = jest.fn().mockResolvedValue(failValidation); 46 | await middleware(context, next); 47 | expect(next).not.toBeCalled(); 48 | }); 49 | 50 | it("when success call next", async () => { 51 | options.validate = jest.fn().mockResolvedValue(successValidation); 52 | await middleware(context, next); 53 | expect(successValidation.send).not.toBeCalled(); 54 | expect(next).toBeCalled(); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /core/src/middleware/validationMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { CloudContext } from "../cloudContext"; 2 | import { Middleware } from "../app"; 3 | 4 | /** 5 | * Result from validation execution 6 | */ 7 | export interface ValidationResult { 8 | /** True if validation failed */ 9 | hasError(): boolean; 10 | /** Send result of validation */ 11 | send(): Promise; 12 | } 13 | 14 | /** 15 | * Options for Validation Middleware 16 | */ 17 | export interface ValidationOptions { 18 | /** Validate Cloud Context */ 19 | validate: (context: CloudContext) => Promise; 20 | }; 21 | 22 | /** 23 | * Create validation middleware 24 | * @param options Options for Validation Middleware 25 | */ 26 | export const createValidationMiddleware = (options: ValidationOptions): Middleware => 27 | async (context: CloudContext, next: () => Promise): Promise => { 28 | const result = await options.validate(context); 29 | 30 | if (result.hasError()) { 31 | await result.send(); 32 | return; 33 | } 34 | return next(); 35 | } 36 | -------------------------------------------------------------------------------- /core/src/services/cloudService.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Options for Cloud Service invocation 3 | */ 4 | export interface CloudServiceOptions { 5 | name: string; 6 | } 7 | 8 | /** 9 | * Service to invoke a Serverless function 10 | */ 11 | export interface CloudService { 12 | /** 13 | * Invoke a deployed Serverless function 14 | * @param name Name of function to forget 15 | * @param fireAndForget Don't listen for response if true 16 | * @param payload Payload to send in invocation 17 | */ 18 | invoke(name: string, fireAndForget: boolean, payload?: any): Promise; 19 | } 20 | -------------------------------------------------------------------------------- /core/src/services/cloudStorage.ts: -------------------------------------------------------------------------------- 1 | import Stream from "stream"; 2 | 3 | /** 4 | * Options for reading blob 5 | */ 6 | export interface ReadBlobOptions { 7 | /** Container containing blob to read */ 8 | container: string; 9 | /** Path of blob within container */ 10 | path: string; 11 | } 12 | 13 | /** 14 | * Options for writing blob 15 | */ 16 | export interface WriteBlobOptions { 17 | /** Container containing blob to read */ 18 | container: string; 19 | /** Path of blob within container */ 20 | path: string; 21 | /** Stringified body of the blob to write */ 22 | body: string | Buffer | Stream; 23 | /** Object containing extra parameters to pass */ 24 | options?: object; 25 | } 26 | 27 | /** 28 | * Output when uploading blob 29 | */ 30 | export interface WriteBlobOutput { 31 | /** Entity tag of the object */ 32 | eTag: string; 33 | /** Version of the object */ 34 | version: string; 35 | } 36 | 37 | /** 38 | * Service for Cloud Storage account 39 | */ 40 | export interface CloudStorage { 41 | /** Read a stream from a blob within the storage account */ 42 | read: (opts: ReadBlobOptions) => Promise; 43 | /** Write a blob to the storage account */ 44 | write: (opts: WriteBlobOptions) => Promise; 45 | } 46 | -------------------------------------------------------------------------------- /core/src/services/consoleLogger.ts: -------------------------------------------------------------------------------- 1 | import { Logger, LogLevel } from "./logger"; 2 | 3 | /** 4 | * Console implementation of Logger interface 5 | */ 6 | export class ConsoleLogger implements Logger { 7 | /** Creates a new Logger, with the specified LogLevel. */ 8 | public constructor(private logLevel?: LogLevel) { 9 | if (logLevel === LogLevel.NONE) { 10 | this.logLevel = logLevel; 11 | } else { 12 | this.logLevel = logLevel || parseInt(process.env.LOG_LEVEL) || LogLevel.INFO; 13 | } 14 | } 15 | 16 | /** Log message with the current stack trace */ 17 | public trace(...message: string[]) { 18 | if (this.logLevel && this.logLevel === LogLevel.VERBOSE) { 19 | console.trace("[TRACE] ", ...message); 20 | } 21 | } 22 | 23 | /** Log message as debug */ 24 | public debug(...message: string[]) { 25 | if (this.logLevel && this.logLevel === LogLevel.VERBOSE) { 26 | console.debug("[DEBUG] ", ...message); 27 | } 28 | } 29 | 30 | /** Log message */ 31 | public log(...message: string[]) { 32 | if (this.logLevel && this.logLevel === LogLevel.VERBOSE) { 33 | console.log("[VERBOSE] ", ...message); 34 | } 35 | } 36 | 37 | /** Log message as info */ 38 | public info(...message: string[]) { 39 | if (this.logLevel && this.logLevel <= LogLevel.INFO) { 40 | console.info("[INFO] ", ...message); 41 | } 42 | } 43 | 44 | /** Log message as warning */ 45 | public warn(...message: string[]) { 46 | if (this.logLevel && this.logLevel <= LogLevel.WARN) { 47 | console.warn("[WARN] ", ...message); 48 | } 49 | } 50 | 51 | /** Log message as error */ 52 | public error(...message: string[]) { 53 | if (this.logLevel && this.logLevel <= LogLevel.ERROR) { 54 | console.error("[ERROR] ", ...message); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /core/src/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./cloudService"; 2 | export * from "./cloudStorage"; 3 | export * from "./logger"; 4 | export * from "./consoleLogger"; 5 | export * from "./telemetry"; 6 | -------------------------------------------------------------------------------- /core/src/services/logger.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Level of verbosity for logging 3 | */ 4 | export enum LogLevel { 5 | /** Disable logging */ 6 | NONE = 0, 7 | /** Log everyting */ 8 | VERBOSE = 1, 9 | /** Only log info, errors and warnings */ 10 | INFO = 2, 11 | /** Only log errors and warnings */ 12 | WARN = 3, 13 | /** Only log errors */ 14 | ERROR = 4, 15 | } 16 | 17 | /** 18 | * Logging service 19 | */ 20 | export interface Logger { 21 | /** Log message */ 22 | log: (...message: string[]) => void; 23 | /** Log message as info */ 24 | info: (...message: string[]) => void; 25 | /** Log message as error */ 26 | error: (...message: string[]) => void; 27 | /** Log message as warning */ 28 | warn: (...message: string[]) => void; 29 | /** Log message as debug */ 30 | debug: (...message: string[]) => void; 31 | /** Log message with the current stack trace */ 32 | trace: (...message: string[]) => void; 33 | } 34 | -------------------------------------------------------------------------------- /core/src/services/telemetry.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Options for telemetry 3 | */ 4 | export interface TelemetryOptions { 5 | /** Service for sending telemetry */ 6 | telemetryService: TelemetryService; 7 | /** Service should flush if true */ 8 | shouldFlush: boolean; 9 | } 10 | 11 | /** 12 | * Service for sending telemetry 13 | */ 14 | export interface TelemetryService { 15 | /** 16 | * Collect telemetry 17 | * @param key Authentication for telemetry service 18 | * @param data Message to log in telemetry 19 | */ 20 | collect: (key: string, data: object) => Promise; 21 | /** Flush all messages in service */ 22 | flush: () => Promise; 23 | } 24 | -------------------------------------------------------------------------------- /core/src/test/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./testContext"; 2 | export * from "./testRequest"; 3 | export * from "./testResponse"; 4 | export * from "./testModule" 5 | export * from "./testCloudService"; 6 | export * from "./testCloudStorage"; 7 | export * from "./cloudContextBuilder"; 8 | export * from "./mockFactory"; 9 | -------------------------------------------------------------------------------- /core/src/test/testCloudService.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | import { injectable } from "inversify"; 3 | import { CloudService } from "../services/cloudService"; 4 | 5 | @injectable() 6 | export class TestCloudService implements CloudService { 7 | public invoke(): Promise { 8 | return Promise.resolve(null); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /core/src/test/testCloudStorage.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | import { injectable } from "inversify"; 3 | import { Stream } from "stream"; 4 | import { CloudStorage, WriteBlobOutput } from "../services/cloudStorage"; 5 | 6 | @injectable() 7 | export class TestCloudStorage implements CloudStorage { 8 | public read(): Promise { 9 | return Promise.resolve(null); 10 | }; 11 | public write(): Promise { 12 | return Promise.resolve(null); 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /core/src/test/testContext.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | import { injectable, inject } from "inversify"; 3 | import { CloudRequest } from "../cloudRequest"; 4 | import { CloudResponse } from "../cloudResponse"; 5 | import { CloudContext, CloudProviderRuntime } from "../cloudContext"; 6 | import { ComponentType } from "../cloudContainer"; 7 | 8 | @injectable() 9 | export class TestContext implements CloudContext { 10 | public constructor(@inject(ComponentType.RuntimeArgs) args?: any[]) { 11 | if (args && args.length) { 12 | this.runtime.context = args[0]; 13 | this.runtime.event = args[1]; 14 | } 15 | 16 | this.id = this.runtime.context.id || Math.random().toString(36).substring(7) 17 | this.event = this.runtime.event; 18 | } 19 | 20 | public runtime: CloudProviderRuntime = { 21 | context: {}, 22 | event: {} 23 | }; 24 | 25 | public providerType: string = "test"; 26 | public id: string; 27 | public event: any; 28 | public container?; 29 | public req?: CloudRequest; 30 | public res?: CloudResponse; 31 | public storage?; 32 | public logger?; 33 | public service?; 34 | public telemetry?; 35 | 36 | public send(body: any, status: number) { 37 | if (this.res) { 38 | this.res.send(body, status); 39 | } 40 | 41 | this.done(); 42 | }; 43 | 44 | public done: () => void; 45 | 46 | public flush() { 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /core/src/test/testModule.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | import { interfaces, ContainerModule } from "inversify"; 3 | import { CloudModule, ComponentType } from "../cloudContainer"; 4 | import { CloudContext } from "../cloudContext"; 5 | import { TestContext } from "./testContext"; 6 | import { CloudRequest } from "../cloudRequest"; 7 | import { TestRequest } from "./testRequest"; 8 | import { CloudResponse } from "../cloudResponse"; 9 | import { TestResponse } from "./testResponse"; 10 | import { CloudService } from "../services/cloudService"; 11 | import { TestCloudService } from "./testCloudService"; 12 | import { CloudStorage } from "../services/cloudStorage"; 13 | import { TestCloudStorage } from "./testCloudStorage"; 14 | 15 | export class TestModule implements CloudModule { 16 | private static isTestEnvironment(): boolean { 17 | return process.env.NODE_ENV === "test"; 18 | } 19 | 20 | private static isHttpRequest(req: interfaces.Request): boolean { 21 | const runtimeArgs = req.parentContext.container.get(ComponentType.RuntimeArgs); 22 | return runtimeArgs && runtimeArgs[1] && runtimeArgs[1].method; 23 | } 24 | 25 | public create() { 26 | return new ContainerModule((bind) => { 27 | bind(ComponentType.CloudContext) 28 | .to(TestContext) 29 | .inSingletonScope() 30 | .when(TestModule.isTestEnvironment); 31 | 32 | bind(ComponentType.CloudRequest) 33 | .to(TestRequest) 34 | .when((req) => TestModule.isTestEnvironment() && TestModule.isHttpRequest(req)); 35 | 36 | bind(ComponentType.CloudResponse) 37 | .to(TestResponse) 38 | .when((req) => TestModule.isTestEnvironment() && TestModule.isHttpRequest(req)); 39 | 40 | bind(ComponentType.CloudService) 41 | .to(TestCloudService) 42 | .when(TestModule.isTestEnvironment); 43 | 44 | bind(ComponentType.CloudStorage) 45 | .to(TestCloudStorage) 46 | .when(TestModule.isTestEnvironment); 47 | }); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /core/src/test/testRequest.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | import { injectable, inject } from "inversify"; 3 | import { CloudRequest } from "../cloudRequest"; 4 | import { ComponentType } from "../cloudContainer"; 5 | import { CloudContext } from "../cloudContext"; 6 | import { StringParams } from "../common/stringParams"; 7 | 8 | @injectable() 9 | export class TestRequest implements CloudRequest { 10 | public constructor(@inject(ComponentType.CloudContext) private context: CloudContext) { 11 | this.method = this.context.runtime.event.method; 12 | this.headers = new StringParams(this.context.runtime.event.headers); 13 | this.query = new StringParams(this.context.runtime.event.query); 14 | this.pathParams = new StringParams(this.context.runtime.event.pathParams); 15 | this.body = this.context.runtime.event.body; 16 | } 17 | 18 | public body: any; 19 | public method: string; 20 | public headers: StringParams; 21 | public query: StringParams; 22 | public pathParams: StringParams; 23 | } 24 | -------------------------------------------------------------------------------- /core/src/test/testResponse.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | import { injectable } from "inversify"; 3 | import { CloudResponse } from "../cloudResponse"; 4 | import { StringParams } from "../common/stringParams"; 5 | 6 | @injectable() 7 | export class TestResponse implements CloudResponse { 8 | public body: string; 9 | public status: number; 10 | public headers: StringParams = new StringParams(); 11 | 12 | public send(body: any, status: number) { 13 | this.body = body; 14 | this.status = status; 15 | } 16 | 17 | public flush() { 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */, 5 | "outDir": "lib", 6 | "lib": ["es6"] /* Redirect output structure to the directory. */, 7 | "experimentalDecorators": true, 8 | "emitDecoratorMetadata": true 9 | }, 10 | "include": ["./src"], 11 | "exclude": ["**/*.test.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /docs/assets/ESLintInstall.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless/multicloud/fe68e4c0ed5d0453e2c9e437913a7effad52b5ba/docs/assets/ESLintInstall.gif -------------------------------------------------------------------------------- /docs/assets/editor-config-config.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless/multicloud/fe68e4c0ed5d0453e2c9e437913a7effad52b5ba/docs/assets/editor-config-config.gif -------------------------------------------------------------------------------- /docs/assets/editor-config-install.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless/multicloud/fe68e4c0ed5d0453e2c9e437913a7effad52b5ba/docs/assets/editor-config-install.png -------------------------------------------------------------------------------- /docs/assets/editor-config-setup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless/multicloud/fe68e4c0ed5d0453e2c9e437913a7effad52b5ba/docs/assets/editor-config-setup.png -------------------------------------------------------------------------------- /docs/assets/editor-config-troubleshoot-delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless/multicloud/fe68e4c0ed5d0453e2c9e437913a7effad52b5ba/docs/assets/editor-config-troubleshoot-delete.png -------------------------------------------------------------------------------- /docs/assets/editor-config-troubleshoot-vsc.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless/multicloud/fe68e4c0ed5d0453e2c9e437913a7effad52b5ba/docs/assets/editor-config-troubleshoot-vsc.gif -------------------------------------------------------------------------------- /docs/assets/editor-config-troubleshoot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serverless/multicloud/fe68e4c0ed5d0453e2c9e437913a7effad52b5ba/docs/assets/editor-config-troubleshoot.gif -------------------------------------------------------------------------------- /docs/eslint.md: -------------------------------------------------------------------------------- 1 | # ESLint 2 | 3 | ## Installation 4 | 5 | For **Visual Studio Code** just download the ESLint extension as the following: 6 | 7 | ![ESLint Setup](assets/ESLintInstall.gif) 8 | 9 | **Note:** make sure that the ESLint dependencies are installed, use `npm install` instead. 10 | 11 | ## Configurations 12 | 13 | The linter configurations are set using the recommended rules from [eslint](https://eslint.org/docs/rules/), [jest](https://github.com/jest-community/eslint-plugin-jest) and [node](https://github.com/mysticatea/eslint-plugin-node). Additionally, there are some specific rules set and each one of them is set to throw an error: 14 | 15 | - **indent:** set to `2`, enforces a consistent indentation style. [more details](https://eslint.org/docs/rules/indent) 16 | - **linebreak-style:** set to `unix`, enforces consistent line endings independent of operating system, VCS, or editor used across your codebase. [more details](https://eslint.org/docs/rules/linebreak-style) 17 | - **quotes:** set to `single`, enforces the consistent use of either backticks, double, or single quotes. [more details](https://eslint.org/docs/rules/quotes) 18 | - **semi:** set to `always`, require or disallow semicolons [more details](https://eslint.org/docs/rules/semi) 19 | -------------------------------------------------------------------------------- /docs/structure.md: -------------------------------------------------------------------------------- 1 | ## Project Structure 2 | 3 | ``` 4 | ├── aws 5 | | ├── src 6 | | | ├── storage.test.js 7 | | | └── storage.js 8 | | ├── package.json 9 | | └── tsconfig.json 10 | ├── azure 11 | | ├── src 12 | | | ├── storage.test.js 13 | | | └── storage.js 14 | | ├── package.json 15 | | └── tsconfig.json 16 | ├── core 17 | | ├── src 18 | | ├── package.json 19 | | └── tsconfig.json 20 | ├── readme.md 21 | └── tsconfig.json 22 | ``` 23 | 24 | - **aws** folder: contains the general library files which adapt AWS lambda provider to a generic provider to make business services agnostic from the cloud provider. The utils folder contains the adapters to read request input and write a response as output. 25 | 26 | - **azure** folder: contains the general library files which adapt Azure Functions provider to a generic provider to make the business services agnostic from the cloud provider. The utils folder contains the adapters to read request input and write a response as output. 27 | 28 | - **core** folder: contains the general library files which adapt any provider to a generic provider to make the business services agnostic from the cloud provider. The utils folder contains the adapters to read request input and write a response as output. 29 | -------------------------------------------------------------------------------- /docs/typescript.md: -------------------------------------------------------------------------------- 1 | # TypeScript 2 | 3 | TypeScript is an object-oriented programming language developed and maintained by the Microsoft Corporation. It is a superset of JavaScript and contains all of its elements. [More Info](https://www.typescriptlang.org/) 4 | 5 | For working with it we need a configuration file which will build the javascript files coded in typescript. 6 | 7 | # How to run typescript 8 | 9 | Exucute the following command: 10 | 11 | ``` 12 | npm run tsc 13 | ``` 14 | 15 | ## Configurations 16 | 17 | ### tsconfig.json [more info](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html) 18 | 19 | We are using the following configuration in `tsconfig.json` file in **aws**, **azure** and **core** folders. 20 | 21 | ``` 22 | { 23 | "extends": "../tsconfig.json", 24 | "compilerOptions": { 25 | "rootDir": "src" 26 | }, 27 | "include": [ 28 | "./src" 29 | ] 30 | } 31 | ``` 32 | 33 | We are using the following configuration in `tsconfig.json` file at **root** level. 34 | 35 | ``` 36 | { 37 | "compilerOptions": { 38 | "target": "es6", 39 | "module": "commonjs", 40 | "declaration": true, 41 | "outDir": "lib", 42 | "downlevelIteration": true, 43 | "strict": false, 44 | "moduleResolution": "node", 45 | "esModuleInterop": true, 46 | "resolveJsonModule": true 47 | } 48 | } 49 | ``` 50 | -------------------------------------------------------------------------------- /samples/aws-storage/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "project": "./tsconfig.json" 5 | }, 6 | "plugins": ["@typescript-eslint"], 7 | "extends": ["plugin:@typescript-eslint/recommended"], 8 | "rules": { 9 | "quotes": ["error", "double"], 10 | "@typescript-eslint/indent": [ 11 | "error", 12 | 2 13 | ], 14 | "@typescript-eslint/no-explicit-any": 0, 15 | "@typescript-eslint/explicit-function-return-type": 0, 16 | "@typescript-eslint/no-parameter-properties": 0, 17 | "@typescript-eslint/no-use-before-define": 0, 18 | "@typescript-eslint/no-object-literal-type-assertion": 0 19 | } 20 | } -------------------------------------------------------------------------------- /samples/aws-storage/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aws-storage", 3 | "version": "1.0.0", 4 | "description": "aws middleware", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "npm run test -- --watch", 8 | "lint": "eslint src/**/*.ts", 9 | "lint:fix": "npm run lint -- --fix", 10 | "pretest": "npm run lint", 11 | "test": "jest --passWithNoTests", 12 | "test:ci": "npm run test -- --ci", 13 | "test:coverage": "npm run test -- --coverage", 14 | "prebuild": "shx rm -rf lib/ && npm run test", 15 | "build": "tsc" 16 | }, 17 | "author": "", 18 | "license": "ISC", 19 | "dependencies": { 20 | "aws": "file:../../aws" 21 | }, 22 | "devDependencies": { 23 | "@types/jest": "^24.0.13", 24 | "@types/node": "12.0.8", 25 | "@typescript-eslint/eslint-plugin": "^1.9.0", 26 | "@typescript-eslint/parser": "^1.9.0", 27 | "babel-jest": "^24.8.0", 28 | "babel-preset-react-app": "^9.0.0", 29 | "eslint": "^5.16.0", 30 | "jest": "^24.8.0", 31 | "shx": "^0.3.2", 32 | "typescript": "^3.4.5" 33 | }, 34 | "engines": { 35 | "node": ">= 6.5.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /samples/aws-storage/src/index.ts: -------------------------------------------------------------------------------- 1 | import Storage from "aws/lib/S3Storage"; 2 | 3 | const storage = new Storage(); 4 | const options = { 5 | container: "foo", 6 | path: "bar" 7 | }; 8 | 9 | const readFileFromStorage = async () => { 10 | const result = await storage.read(options) 11 | console.log(result) 12 | 13 | } 14 | readFileFromStorage() 15 | -------------------------------------------------------------------------------- /samples/aws-storage/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "esModuleInterop": true, 5 | "target": "es6", 6 | "moduleResolution": "node", 7 | "sourceMap": true, 8 | "outDir": "./lib" 9 | }, 10 | "include": ["./"], 11 | "lib": ["es2015"] 12 | } 13 | -------------------------------------------------------------------------------- /samples/azure-cloud-request/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "react-app", 5 | { 6 | "flow": false, 7 | "typescript": true 8 | } 9 | ] 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /samples/azure-cloud-request/.funcignore: -------------------------------------------------------------------------------- 1 | *.js.map 2 | *.ts 3 | .git* 4 | .vscode 5 | local.settings.json 6 | test 7 | tsconfig.json -------------------------------------------------------------------------------- /samples/azure-cloud-request/.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/node,visualstudiocode 2 | # Edit at https://www.gitignore.io/?templates=node,visualstudiocode 3 | 4 | ### Node ### 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | 28 | # lib folder 29 | */lib/**/* 30 | 31 | # nyc test coverage 32 | .nyc_output 33 | 34 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 35 | .grunt 36 | 37 | # Bower dependency directory (https://bower.io/) 38 | bower_components 39 | 40 | # node-waf configuration 41 | .lock-wscript 42 | 43 | # Compiled binary addons (https://nodejs.org/api/addons.html) 44 | build/Release 45 | lib 46 | 47 | # Dependency directories 48 | node_modules/ 49 | jspm_packages/ 50 | 51 | # TypeScript v1 declaration files 52 | typings/ 53 | 54 | # Optional npm cache directory 55 | .npm 56 | 57 | # Optional eslint cache 58 | .eslintcache 59 | 60 | # Optional REPL history 61 | .node_repl_history 62 | 63 | # Output of 'npm pack' 64 | *.tgz 65 | 66 | # Yarn Integrity file 67 | .yarn-integrity 68 | 69 | # dotenv environment variables file 70 | .env 71 | .env.test 72 | 73 | # parcel-bundler cache (https://parceljs.org/) 74 | .cache 75 | 76 | # next.js build output 77 | .next 78 | 79 | # nuxt.js build output 80 | .nuxt 81 | 82 | # vuepress build output 83 | .vuepress/dist 84 | 85 | # Serverless directories 86 | .serverless/ 87 | 88 | # FuseBox cache 89 | .fusebox/ 90 | 91 | # DynamoDB Local files 92 | .dynamodb/ 93 | 94 | ### VisualStudioCode ### 95 | .vscode/* 96 | !.vscode/settings.json 97 | !.vscode/tasks.json 98 | !.vscode/launch.json 99 | !.vscode/extensions.json 100 | 101 | ### VisualStudioCode Patch ### 102 | # Ignore all local history of files 103 | .history 104 | 105 | # other 106 | ignore 107 | 108 | # End of https://www.gitignore.io/api/node,visualstudiocode 109 | -------------------------------------------------------------------------------- /samples/azure-cloud-request/.npmrc: -------------------------------------------------------------------------------- 1 | @multicloud:registry=https://pkgs.dev.azure.com/711digital/_packaging/common-packages/npm/registry/ 2 | engine-strict=true 3 | save-exact=true 4 | always-auth=true 5 | 6 | ; begin auth token 7 | //pkgs.dev.azure.com/711digital/_packaging/common-packages/npm/registry/:username=common-packages 8 | //pkgs.dev.azure.com/711digital/_packaging/common-packages/npm/registry/:_password=${NPM_TOKEN} 9 | //pkgs.dev.azure.com/711digital/_packaging/common-packages/npm/registry/:email=common-packages 10 | ; end auth token 11 | -------------------------------------------------------------------------------- /samples/azure-cloud-request/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0" 3 | } -------------------------------------------------------------------------------- /samples/azure-cloud-request/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "collectCoverageFrom": [ 3 | "src/**/*.{js,jsx,ts,tsx}", 4 | "!src/**/*.d.ts" 5 | ], 6 | "testEnvironment": "node", 7 | "transform": { 8 | "^.+\\.(js|jsx|ts|tsx)$": "babel-jest" 9 | }, 10 | "testPathIgnorePatterns": [ 11 | "./lib", 12 | "./node_modules" 13 | ], 14 | "transformIgnorePatterns": [ 15 | "./lib", 16 | "./node_modules" 17 | ], 18 | "moduleFileExtensions": [ 19 | "js", 20 | "ts", 21 | "tsx", 22 | "json", 23 | "jsx", 24 | "node" 25 | ] 26 | }; -------------------------------------------------------------------------------- /samples/azure-cloud-request/local.settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "IsEncrypted": false, 3 | "Values": { 4 | "FUNCTIONS_WORKER_RUNTIME": "node", 5 | "AzureWebJobsStorage": "{AzureWebJobsStorage}" 6 | } 7 | } -------------------------------------------------------------------------------- /samples/azure-cloud-request/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "azure-function-handler", 3 | "version": "", 4 | "scripts": { 5 | "build": "tsc", 6 | "build:production": "npm run prestart && npm prune --production", 7 | "watch": "tsc --w", 8 | "prestart": "npm run build && func extensions install", 9 | "start:host": "func start", 10 | "start": "npm-run-all --parallel start:host watch", 11 | "test": "jest" 12 | }, 13 | "description": "", 14 | "dependencies": { 15 | "@hapi/joi": "15.1.0", 16 | "@multicloud/sls-azure": "0.0.2", 17 | "@multicloud/sls-core": "0.0.5" 18 | }, 19 | "devDependencies": { 20 | "@azure/functions": "^1.0.1-beta1", 21 | "@types/jest": "24.0.15", 22 | "@types/joi": "14.3.3", 23 | "@types/node": "10.0.3", 24 | "npm-run-all": "^4.1.5", 25 | "typescript": "^3.3.3", 26 | "babel-jest": "^24.8.0", 27 | "babel-preset-react-app": "^9.0.0" 28 | }, 29 | "engines": { 30 | "node": ">= 6.5.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /samples/azure-cloud-request/src/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "bindings": [ 3 | { 4 | "authLevel": "function", 5 | "type": "httpTrigger", 6 | "direction": "in", 7 | "name": "req", 8 | "methods": ["get", "post"] 9 | }, 10 | { 11 | "type": "http", 12 | "direction": "out", 13 | "name": "res" 14 | } 15 | ], 16 | "scriptFile": "../lib/index.js" 17 | } 18 | -------------------------------------------------------------------------------- /samples/azure-cloud-request/src/index.ts: -------------------------------------------------------------------------------- 1 | import { AzureModule } from "@multicloud/sls-azure/lib/AzureModule"; 2 | import { CloudContainer, App, Handler, CloudContext } from "@multicloud/sls-core"; 3 | console.log('hello'); 4 | const container = new CloudContainer(); 5 | container.registerModule(new AzureModule()); 6 | 7 | const handler: Handler = (context: CloudContext) => { 8 | context.send("Foo, Bar", 200); 9 | }; 10 | 11 | const app = new App(container); 12 | 13 | export default app.use([], handler); 14 | -------------------------------------------------------------------------------- /samples/azure-cloud-request/src/middleware/authorizationMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { ValidationOptions, ValidationResult } from "@multicloud/sls-core"; 2 | import { CloudContext, Middleware } from "@multicloud/sls-core"; 3 | 4 | export class AuthorizeValidationResult implements ValidationResult { 5 | public constructor( 6 | public authorized: boolean, 7 | private context: CloudContext, 8 | public message: string 9 | ) {} 10 | 11 | public hasError(): boolean { 12 | return !this.authorized; 13 | } 14 | 15 | public send(): Promise { 16 | return Promise.resolve(this.context.send(this.message, 401)); 17 | } 18 | } 19 | 20 | export class AuthorizationValidationOptions implements ValidationOptions { 21 | public constructor( 22 | public header: string, 23 | public message: string, 24 | public compare?: any 25 | ) {} 26 | 27 | public validate = (context: CloudContext): Promise => { 28 | const value = context.req.headers[this.header]; 29 | const result = 30 | !!value && (this.compare === undefined || value === this.compare); 31 | 32 | return Promise.resolve( 33 | new AuthorizeValidationResult(result, context, this.message) 34 | ); 35 | }; 36 | } 37 | 38 | export const createAuthorizationMiddleware = ( 39 | options: AuthorizationValidationOptions 40 | ): Middleware => async ( 41 | context: CloudContext, 42 | next: Function 43 | ): Promise => { 44 | const result = await options.validate(context); 45 | 46 | if (result.hasError()) { 47 | await result.send(); 48 | return; 49 | } 50 | return next(); 51 | }; 52 | -------------------------------------------------------------------------------- /samples/azure-cloud-request/src/middleware/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./joiSupport"; 2 | export * from "./authorizationMiddleware" 3 | -------------------------------------------------------------------------------- /samples/azure-cloud-request/src/middleware/joiSupport.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createJoiBodyValidationOptions, 3 | createJoiQueryValidationOptions, 4 | InternalValidationResult 5 | } from "./joiSupport"; 6 | import { CloudContext,CloudRequest } from "@multicloud/sls-core"; 7 | import * as Joi from "@hapi/joi"; 8 | 9 | describe("Joi validation option", () => { 10 | const schema: Joi.AnySchema = Joi.object({ 11 | foo: Joi.string().required(), 12 | bar: Joi.string().required() 13 | }); 14 | 15 | const baseContext: CloudContext = { 16 | providerType: "test", 17 | send: jest.fn() 18 | }; 19 | 20 | it("invalid body should has error", async () => { 21 | const context = { 22 | ...baseContext, 23 | req: { 24 | body: {}, 25 | method: "POST" 26 | } 27 | }; 28 | 29 | const result = await createJoiBodyValidationOptions(schema).validate( 30 | context 31 | ); 32 | expect(result.hasError()).toBe(true); 33 | }); 34 | 35 | it("valid body shouldnt has error", async () => { 36 | const context = { 37 | ...baseContext, 38 | req: { 39 | body: { 40 | foo: "foo", 41 | bar: "bar" 42 | }, 43 | method: "POST" 44 | } 45 | }; 46 | 47 | const result = await createJoiBodyValidationOptions(schema).validate( 48 | context 49 | ); 50 | expect(result.hasError()).toBe(false); 51 | }); 52 | 53 | it("invalid query should has error", async () => { 54 | const context = { 55 | ...baseContext, 56 | req: { 57 | query: {}, 58 | method: "GET" 59 | } 60 | }; 61 | 62 | const result = await createJoiQueryValidationOptions(schema).validate( 63 | context 64 | ); 65 | expect(result.hasError()).toBe(true); 66 | }); 67 | 68 | it("valid query shouldnt has error", async () => { 69 | const context = { 70 | ...baseContext, 71 | req: { 72 | query: { 73 | foo: "foo", 74 | bar: "bar" 75 | }, 76 | method: "GET" 77 | } 78 | }; 79 | const result = await createJoiQueryValidationOptions(schema).validate( 80 | context 81 | ); 82 | expect(result.hasError()).toBe(false); 83 | }); 84 | 85 | it("when fail send should has internal error", async () => { 86 | const context = { 87 | ...baseContext, 88 | req: { 89 | query: { 90 | }, 91 | method: "GET" 92 | } 93 | }; 94 | const result = await createJoiQueryValidationOptions(schema).validate( 95 | context 96 | ) as InternalValidationResult; 97 | result.send() 98 | expect(context.send).toHaveBeenCalledWith(result.result.error, 400); 99 | }); 100 | }); 101 | -------------------------------------------------------------------------------- /samples/azure-cloud-request/src/middleware/joiSupport.ts: -------------------------------------------------------------------------------- 1 | import { ValidationOptions, ValidationResult } from "@multicloud/sls-core"; 2 | import { CloudContext } from "@multicloud/sls-core"; 3 | import * as Joi from "joi"; 4 | 5 | export class InternalValidationResult implements ValidationResult { 6 | private error: boolean = false; 7 | public constructor( 8 | public result: Joi.ValidationResult, 9 | private context: CloudContext 10 | ) { 11 | this.error = result.error !== null; 12 | } 13 | 14 | public hasError(): boolean { 15 | return this.error; 16 | } 17 | public send(): Promise { 18 | return Promise.resolve(this.context.send(this.result.error,400)); 19 | } 20 | } 21 | 22 | type CloudContextSelector = (context: CloudContext) => any; 23 | 24 | const createJoiValidationOptions = (selector: CloudContextSelector) => ( 25 | schema: Joi.AnySchema 26 | ): ValidationOptions => { 27 | return { 28 | validate: (context: CloudContext) => { 29 | const value = selector(context); 30 | const result = schema.validate(value); 31 | return Promise.resolve(new InternalValidationResult(result, context)); 32 | } 33 | }; 34 | }; 35 | 36 | export const createJoiBodyValidationOptions = createJoiValidationOptions(x => { 37 | return x.req.body; 38 | }); 39 | 40 | export const createJoiQueryValidationOptions = createJoiValidationOptions(x => { 41 | return x.req.query; 42 | }); 43 | -------------------------------------------------------------------------------- /samples/azure-cloud-request/test.http: -------------------------------------------------------------------------------- 1 | GET http://localhost:7071/api/azure-function-handler?hi=Gab 2 | -------------------------------------------------------------------------------- /samples/azure-cloud-request/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "esModuleInterop": true, 5 | "target": "es6", 6 | "moduleResolution": "node", 7 | "sourceMap": true, 8 | "outDir": "./lib" 9 | }, 10 | "include": ["./"], 11 | "lib": ["es2015"] 12 | } 13 | -------------------------------------------------------------------------------- /samples/azure-http-middleware/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "react-app", 5 | { 6 | "flow": false, 7 | "typescript": true 8 | } 9 | ] 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /samples/azure-http-middleware/.funcignore: -------------------------------------------------------------------------------- 1 | *.js.map 2 | *.ts 3 | .git* 4 | .vscode 5 | local.settings.json 6 | test 7 | tsconfig.json -------------------------------------------------------------------------------- /samples/azure-http-middleware/.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/node,visualstudiocode 2 | # Edit at https://www.gitignore.io/?templates=node,visualstudiocode 3 | 4 | ### Node ### 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | 28 | # lib folder 29 | */lib/**/* 30 | 31 | # nyc test coverage 32 | .nyc_output 33 | 34 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 35 | .grunt 36 | 37 | # Bower dependency directory (https://bower.io/) 38 | bower_components 39 | 40 | # node-waf configuration 41 | .lock-wscript 42 | 43 | # Compiled binary addons (https://nodejs.org/api/addons.html) 44 | build/Release 45 | lib 46 | 47 | # Dependency directories 48 | node_modules/ 49 | jspm_packages/ 50 | 51 | # TypeScript v1 declaration files 52 | typings/ 53 | 54 | # Optional npm cache directory 55 | .npm 56 | 57 | # Optional eslint cache 58 | .eslintcache 59 | 60 | # Optional REPL history 61 | .node_repl_history 62 | 63 | # Output of 'npm pack' 64 | *.tgz 65 | 66 | # Yarn Integrity file 67 | .yarn-integrity 68 | 69 | # dotenv environment variables file 70 | .env 71 | .env.test 72 | 73 | # parcel-bundler cache (https://parceljs.org/) 74 | .cache 75 | 76 | # next.js build output 77 | .next 78 | 79 | # nuxt.js build output 80 | .nuxt 81 | 82 | # vuepress build output 83 | .vuepress/dist 84 | 85 | # Serverless directories 86 | .serverless/ 87 | 88 | # FuseBox cache 89 | .fusebox/ 90 | 91 | # DynamoDB Local files 92 | .dynamodb/ 93 | 94 | ### VisualStudioCode ### 95 | .vscode/* 96 | !.vscode/settings.json 97 | !.vscode/tasks.json 98 | !.vscode/launch.json 99 | !.vscode/extensions.json 100 | 101 | ### VisualStudioCode Patch ### 102 | # Ignore all local history of files 103 | .history 104 | 105 | # other 106 | ignore 107 | 108 | # End of https://www.gitignore.io/api/node,visualstudiocode 109 | -------------------------------------------------------------------------------- /samples/azure-http-middleware/.npmrc: -------------------------------------------------------------------------------- 1 | @multicloud:registry=https://pkgs.dev.azure.com/711digital/_packaging/common-packages/npm/registry/ 2 | engine-strict=true 3 | save-exact=true 4 | always-auth=true 5 | 6 | ; begin auth token 7 | //pkgs.dev.azure.com/711digital/_packaging/common-packages/npm/registry/:username=common-packages 8 | //pkgs.dev.azure.com/711digital/_packaging/common-packages/npm/registry/:_password=${NPM_TOKEN} 9 | //pkgs.dev.azure.com/711digital/_packaging/common-packages/npm/registry/:email=common-packages 10 | ; end auth token 11 | -------------------------------------------------------------------------------- /samples/azure-http-middleware/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0" 3 | } -------------------------------------------------------------------------------- /samples/azure-http-middleware/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "collectCoverageFrom": [ 3 | "src/**/*.{js,jsx,ts,tsx}", 4 | "!src/**/*.d.ts" 5 | ], 6 | "testEnvironment": "node", 7 | "transform": { 8 | "^.+\\.(js|jsx|ts|tsx)$": "babel-jest" 9 | }, 10 | "testPathIgnorePatterns": [ 11 | "./lib", 12 | "./node_modules" 13 | ], 14 | "transformIgnorePatterns": [ 15 | "./lib", 16 | "./node_modules" 17 | ], 18 | "moduleFileExtensions": [ 19 | "js", 20 | "ts", 21 | "tsx", 22 | "json", 23 | "jsx", 24 | "node" 25 | ] 26 | }; -------------------------------------------------------------------------------- /samples/azure-http-middleware/local.settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "IsEncrypted": false, 3 | "Values": { 4 | "FUNCTIONS_WORKER_RUNTIME": "node", 5 | "AzureWebJobsStorage": "{AzureWebJobsStorage}" 6 | } 7 | } -------------------------------------------------------------------------------- /samples/azure-http-middleware/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "azure-function-handler", 3 | "version": "", 4 | "scripts": { 5 | "build": "tsc", 6 | "build:production": "npm run prestart && npm prune --production", 7 | "watch": "tsc --w", 8 | "prestart": "npm run build && func extensions install", 9 | "start:host": "func start", 10 | "start": "npm-run-all --parallel start:host watch", 11 | "test": "jest" 12 | }, 13 | "description": "", 14 | "dependencies": { 15 | "@multicloud/sls-azure": "0.0.8-1", 16 | "@multicloud/sls-core": "0.0.5" 17 | }, 18 | "devDependencies": { 19 | "@azure/functions": "^1.0.1-beta1", 20 | "@types/jest": "24.0.15", 21 | "@types/node": "10.0.3", 22 | "npm-run-all": "^4.1.5", 23 | "typescript": "^3.3.3", 24 | "babel-jest": "^24.8.0", 25 | "babel-preset-react-app": "^9.0.0" 26 | }, 27 | "engines": { 28 | "node": ">= 6.5.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /samples/azure-http-middleware/src/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "bindings": [ 3 | { 4 | "authLevel": "function", 5 | "type": "httpTrigger", 6 | "direction": "in", 7 | "name": "req", 8 | "methods": ["get", "post"] 9 | }, 10 | { 11 | "type": "http", 12 | "direction": "out", 13 | "name": "res" 14 | } 15 | ], 16 | "scriptFile": "../lib/index.js" 17 | } 18 | -------------------------------------------------------------------------------- /samples/azure-http-middleware/src/index.ts: -------------------------------------------------------------------------------- 1 | import { AzureModule } from "../../../azure/lib/azureModule"; 2 | import { CloudContainer, App, Handler, CloudContext } from "@multicloud/sls-core/lib"; 3 | import { HTTPBindingMiddleware } from "../../../core/lib/httpBindingMiddleware" 4 | 5 | const container = new CloudContainer(); 6 | container.registerModule(new AzureModule()); 7 | 8 | const handler: Handler = async (context: CloudContext) => { 9 | context.res.send("HTTPMiddleware is working", 200); 10 | }; 11 | 12 | const app = new App(container); 13 | 14 | export default app.use([HTTPBindingMiddleware(container)], handler); 15 | -------------------------------------------------------------------------------- /samples/azure-http-middleware/test.http: -------------------------------------------------------------------------------- 1 | GET http://localhost:7071/api/azure-function-handler?hi=Gab 2 | -------------------------------------------------------------------------------- /samples/azure-http-middleware/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "esModuleInterop": true, 5 | "target": "es6", 6 | "moduleResolution": "node", 7 | "sourceMap": true, 8 | "outDir": "./lib" 9 | }, 10 | "include": ["./"], 11 | "lib": ["es2015"] 12 | } 13 | -------------------------------------------------------------------------------- /samples/azure-storage/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "project": "./tsconfig.json" 5 | }, 6 | "plugins": ["@typescript-eslint"], 7 | "extends": ["plugin:@typescript-eslint/recommended"], 8 | "rules": { 9 | "quotes": ["error", "double"], 10 | "@typescript-eslint/indent": [ 11 | "error", 12 | 2 13 | ], 14 | "@typescript-eslint/no-explicit-any": 0, 15 | "@typescript-eslint/explicit-function-return-type": 0, 16 | "@typescript-eslint/no-parameter-properties": 0, 17 | "@typescript-eslint/no-use-before-define": 0, 18 | "@typescript-eslint/no-object-literal-type-assertion": 0 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /samples/azure-storage/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "azure-storage", 3 | "version": "1.0.0", 4 | "description": "azure middleware", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "npm run test -- --watch", 8 | "lint": "eslint src/**/*.ts", 9 | "lint:fix": "npm run lint -- --fix", 10 | "pretest": "npm run lint", 11 | "test": "jest --passWithNoTests", 12 | "test:ci": "npm run test -- --ci", 13 | "test:coverage": "npm run test -- --coverage", 14 | "prebuild": "shx rm -rf lib/ && npm run test", 15 | "build": "tsc" 16 | }, 17 | "author": "", 18 | "license": "ISC", 19 | "dependencies": { 20 | "azure": "file:../../azure" 21 | }, 22 | "devDependencies": { 23 | "@types/jest": "^24.0.13", 24 | "@types/node": "12.0.8", 25 | "@typescript-eslint/eslint-plugin": "^1.9.0", 26 | "@typescript-eslint/parser": "^1.9.0", 27 | "babel-jest": "^24.8.0", 28 | "babel-preset-react-app": "^9.0.0", 29 | "eslint": "^5.16.0", 30 | "jest": "^24.8.0", 31 | "shx": "^0.3.2", 32 | "typescript": "^3.4.5" 33 | }, 34 | "engines": { 35 | "node": ">= 6.5.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /samples/azure-storage/src/index.ts: -------------------------------------------------------------------------------- 1 | import AzureBlobStorage from "azure/lib/AzureBlobStorage"; 2 | 3 | const settings = { 4 | account: "foo", 5 | accountKey: "bar" 6 | }; 7 | 8 | const options = { 9 | container: "foo", 10 | path: "bar" 11 | }; 12 | 13 | const storage = new AzureBlobStorage(settings); 14 | storage.read(options).then(console.log); 15 | -------------------------------------------------------------------------------- /samples/azure-storage/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ 5 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 6 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 7 | "outDir": "lib", /* Redirect output structure to the directory. */ 8 | "rootDir": "src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 9 | "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 10 | /* Strict Type-Checking Options */ 11 | "strict": false, /* Enable all strict type-checking options. */ 12 | /* Module Resolution Options */ 13 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 14 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 15 | /* Experimental Options */ 16 | "resolveJsonModule": true 17 | }, 18 | "include": [ 19 | "./src" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /samples/cloud-calls/.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | obj 3 | csx 4 | .vs 5 | edge 6 | Publish 7 | 8 | *.user 9 | *.suo 10 | *.cscfg 11 | *.Cache 12 | project.lock.json 13 | 14 | /packages 15 | /TestResults 16 | 17 | /tools/NuGet.exe 18 | /App_Data 19 | /secrets 20 | /data 21 | .secrets 22 | appsettings.json 23 | local.settings.json 24 | 25 | node_modules 26 | dist 27 | 28 | # Local python packages 29 | .python_packages/ 30 | 31 | # Python Environments 32 | .env 33 | .venv 34 | env/ 35 | venv/ 36 | ENV/ 37 | env.bak/ 38 | venv.bak/ 39 | 40 | # Byte-compiled / optimized / DLL files 41 | __pycache__/ 42 | *.py[cod] 43 | *$py.class -------------------------------------------------------------------------------- /samples/cloud-calls/.npmrc: -------------------------------------------------------------------------------- 1 | @multicloud:registry=https://pkgs.dev.azure.com/711digital/_packaging/common-packages/npm/registry/ 2 | engine-strict=true 3 | save-exact=true 4 | always-auth=true 5 | 6 | ; begin auth token 7 | //pkgs.dev.azure.com/711digital/_packaging/common-packages/npm/registry/:username=common-packages 8 | //pkgs.dev.azure.com/711digital/_packaging/common-packages/npm/registry/:_password=${NPM_TOKEN} 9 | //pkgs.dev.azure.com/711digital/_packaging/common-packages/npm/registry/:email=common-packages 10 | ; end auth token 11 | -------------------------------------------------------------------------------- /samples/cloud-calls/call/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "bindings": [ 3 | { 4 | "authLevel": "function", 5 | "type": "httpTrigger", 6 | "direction": "in", 7 | "name": "req", 8 | "methods": [ 9 | "get", 10 | "post" 11 | ] 12 | }, 13 | { 14 | "type": "http", 15 | "direction": "out", 16 | "name": "res" 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /samples/cloud-calls/call/index.js: -------------------------------------------------------------------------------- 1 | const { App, CloudContainer } = require("@multicloud/sls-core"); 2 | const { 3 | AzureModule, 4 | AzureFunctionCloudService 5 | } = require("@multicloud/sls-azure"); 6 | 7 | 8 | const container = new CloudContainer(); 9 | container.registerModule(new AzureModule()); 10 | container.registerModule({ 11 | init: container => { 12 | container.bind("google").toConstantValue({ 13 | name: "google", 14 | method: "get", 15 | http: "http://www.google.com" 16 | }); 17 | } 18 | }); 19 | 20 | const app = new App(container); 21 | const caller = new AzureFunctionCloudService(container) 22 | 23 | const handler = async (context) => { 24 | const result = await caller.invoke("google"); 25 | context.send(result.data); 26 | }; 27 | 28 | module.exports = app.use([], handler); 29 | -------------------------------------------------------------------------------- /samples/cloud-calls/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0" 3 | } -------------------------------------------------------------------------------- /samples/cloud-calls/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "version": "", 4 | "description": "", 5 | "scripts": { 6 | "start": "func host start --build" 7 | }, 8 | "author": "", 9 | "dependencies": { 10 | "@multicloud/sls-azure": "0.0.8-2", 11 | "@multicloud/sls-core": "0.0.11-3" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /samples/cloud-calls/test.http: -------------------------------------------------------------------------------- 1 | GET http://localhost:7071/api/call 2 | -------------------------------------------------------------------------------- /samples/mongoose-connection/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "react-app", 5 | { 6 | "flow": false, 7 | "typescript": true 8 | } 9 | ] 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /samples/mongoose-connection/.funcignore: -------------------------------------------------------------------------------- 1 | *.js.map 2 | *.ts 3 | .git* 4 | .vscode 5 | local.settings.json 6 | test 7 | tsconfig.json -------------------------------------------------------------------------------- /samples/mongoose-connection/.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/node,visualstudiocode 2 | # Edit at https://www.gitignore.io/?templates=node,visualstudiocode 3 | 4 | ### Node ### 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | 28 | # lib folder 29 | */lib/**/* 30 | 31 | # nyc test coverage 32 | .nyc_output 33 | 34 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 35 | .grunt 36 | 37 | # Bower dependency directory (https://bower.io/) 38 | bower_components 39 | 40 | # node-waf configuration 41 | .lock-wscript 42 | 43 | # Compiled binary addons (https://nodejs.org/api/addons.html) 44 | build/Release 45 | lib 46 | 47 | # Dependency directories 48 | node_modules/ 49 | jspm_packages/ 50 | 51 | # TypeScript v1 declaration files 52 | typings/ 53 | 54 | # Optional npm cache directory 55 | .npm 56 | 57 | # Optional eslint cache 58 | .eslintcache 59 | 60 | # Optional REPL history 61 | .node_repl_history 62 | 63 | # Output of 'npm pack' 64 | *.tgz 65 | 66 | # Yarn Integrity file 67 | .yarn-integrity 68 | 69 | # dotenv environment variables file 70 | .env 71 | .env.test 72 | 73 | # parcel-bundler cache (https://parceljs.org/) 74 | .cache 75 | 76 | # next.js build output 77 | .next 78 | 79 | # nuxt.js build output 80 | .nuxt 81 | 82 | # vuepress build output 83 | .vuepress/dist 84 | 85 | # Serverless directories 86 | .serverless/ 87 | 88 | # FuseBox cache 89 | .fusebox/ 90 | 91 | # DynamoDB Local files 92 | .dynamodb/ 93 | 94 | ### VisualStudioCode ### 95 | .vscode/* 96 | !.vscode/settings.json 97 | !.vscode/tasks.json 98 | !.vscode/launch.json 99 | !.vscode/extensions.json 100 | 101 | ### VisualStudioCode Patch ### 102 | # Ignore all local history of files 103 | .history 104 | 105 | # other 106 | ignore 107 | 108 | # End of https://www.gitignore.io/api/node,visualstudiocode 109 | -------------------------------------------------------------------------------- /samples/mongoose-connection/.npmrc: -------------------------------------------------------------------------------- 1 | @multicloud:registry=https://pkgs.dev.azure.com/711digital/_packaging/common-packages/npm/registry/ 2 | engine-strict=true 3 | save-exact=true 4 | always-auth=true 5 | 6 | ; begin auth token 7 | //pkgs.dev.azure.com/711digital/_packaging/common-packages/npm/registry/:username=common-packages 8 | //pkgs.dev.azure.com/711digital/_packaging/common-packages/npm/registry/:_password=${NPM_TOKEN} 9 | //pkgs.dev.azure.com/711digital/_packaging/common-packages/npm/registry/:email=common-packages 10 | ; end auth token 11 | -------------------------------------------------------------------------------- /samples/mongoose-connection/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0" 3 | } -------------------------------------------------------------------------------- /samples/mongoose-connection/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "collectCoverageFrom": [ 3 | "src/**/*.{js,jsx,ts,tsx}", 4 | "!src/**/*.d.ts" 5 | ], 6 | "testEnvironment": "node", 7 | "transform": { 8 | "^.+\\.(js|jsx|ts|tsx)$": "babel-jest" 9 | }, 10 | "testPathIgnorePatterns": [ 11 | "./lib", 12 | "./node_modules" 13 | ], 14 | "transformIgnorePatterns": [ 15 | "./lib", 16 | "./node_modules" 17 | ], 18 | "moduleFileExtensions": [ 19 | "js", 20 | "ts", 21 | "tsx", 22 | "json", 23 | "jsx", 24 | "node" 25 | ] 26 | }; -------------------------------------------------------------------------------- /samples/mongoose-connection/local.settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "IsEncrypted": false, 3 | "Values": { 4 | "FUNCTIONS_WORKER_RUNTIME": "node", 5 | "AzureWebJobsStorage": "{AzureWebJobsStorage}" 6 | } 7 | } -------------------------------------------------------------------------------- /samples/mongoose-connection/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "azure-function-handler", 3 | "version": "", 4 | "scripts": { 5 | "build": "tsc", 6 | "build:production": "npm run prestart && npm prune --production", 7 | "watch": "tsc --w", 8 | "prestart": "npm run build && func extensions install", 9 | "start:host": "func start", 10 | "start": "npm-run-all --parallel start:host watch", 11 | "test": "jest" 12 | }, 13 | "description": "", 14 | "dependencies": { 15 | "@hapi/joi": "15.1.0", 16 | "@multicloud/sls-azure": "0.0.8-6", 17 | "@multicloud/sls-core": "0.0.11-7", 18 | "mongoose": "5.6.4" 19 | }, 20 | "devDependencies": { 21 | "@azure/functions": "^1.0.1-beta1", 22 | "@types/jest": "24.0.15", 23 | "@types/joi": "14.3.3", 24 | "@types/node": "10.0.3", 25 | "babel-jest": "^24.8.0", 26 | "babel-preset-react-app": "^9.0.0", 27 | "jest": "24.8.0", 28 | "npm-run-all": "^4.1.5", 29 | "typescript": "^3.5.2" 30 | }, 31 | "engines": { 32 | "node": ">=8.16.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /samples/mongoose-connection/src/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "bindings": [ 3 | { 4 | "authLevel": "function", 5 | "type": "httpTrigger", 6 | "direction": "in", 7 | "name": "req", 8 | "methods": ["get", "post"] 9 | }, 10 | { 11 | "type": "http", 12 | "direction": "out", 13 | "name": "res" 14 | } 15 | ], 16 | "scriptFile": "../lib/index.js" 17 | } 18 | -------------------------------------------------------------------------------- /samples/mongoose-connection/src/index.ts: -------------------------------------------------------------------------------- 1 | import { AzureModule } from "@multicloud/sls-azure"; 2 | import { App, HTTPBindingMiddleware } from "@multicloud/sls-core"; 3 | import { DatabaseOptions, DatabaseMiddleware } from "./middleware/databaseMiddleware"; 4 | 5 | class DbOptions implements DatabaseOptions { 6 | public constructor(closeConnection: boolean){ 7 | this.closeConnection = closeConnection; 8 | } 9 | public url: string = "mongodb://localhost:27017"; 10 | public params: any = { 11 | poolSize: 1, 12 | connectTimeoutMS: 30000, 13 | socketTimeoutMS: 30000, 14 | keepAlive: 120 15 | }; 16 | public closeConnection: boolean = true; 17 | } 18 | 19 | const dbMiddleware1 = DatabaseMiddleware(new DbOptions(false)); 20 | const dbMiddleware2 = DatabaseMiddleware(new DbOptions(false)); 21 | const httpMiddleware = HTTPBindingMiddleware(); 22 | 23 | const app = new App(new AzureModule()); 24 | const middlewares = [httpMiddleware, dbMiddleware1, dbMiddleware2]; 25 | 26 | const handler = (context) => { 27 | context.res.send(`Number of active connections: ${global.numberOfConnections}. Number of connections tried to open: ${global.numberOfDbMiddlewares}`, 200); 28 | } 29 | 30 | module.exports = app.use(middlewares, handler); 31 | -------------------------------------------------------------------------------- /samples/mongoose-connection/src/middleware/databaseMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { Middleware } from "@multicloud/sls-core"; 2 | import { CloudContext } from "@multicloud/sls-core"; 3 | import mongoose from "mongoose"; 4 | 5 | export interface DatabaseOptions { 6 | url: string; 7 | params?: any; 8 | closeConnection: boolean; 9 | } 10 | 11 | export const DatabaseMiddleware = ( 12 | options: DatabaseOptions 13 | ): Middleware => async ( 14 | context: CloudContext, 15 | next: Function 16 | ): Promise => { 17 | const { url, params, closeConnection } = options; 18 | //Added global.numberOfDbMiddlewares variable to check the ammount of times this is instantiated. It can be removed. 19 | isNaN(global.numberOfDbMiddlewares) ? global.numberOfDbMiddlewares = 1 : global.numberOfDbMiddlewares++; 20 | if ( 21 | !global || 22 | !global.cachedConnection || 23 | !global.cachedConnection.readyState || 24 | !(global.cachedConnection.readyState === 1) 25 | ) { 26 | let connectionPromise = mongoose.connect(url,params); 27 | global.connectionPromise = connectionPromise; 28 | await connectionPromise.then( 29 | () => { 30 | if (mongoose.connection) { 31 | let connection = mongoose.connection; 32 | global.cachedConnection = connection; 33 | //Added global.numberOfConnections variable to check the ammount of connections actually created. It can be removed. 34 | isNaN(global.numberOfConnections) ? global.numberOfConnections = 1 : global.numberOfConnections++; 35 | 36 | connection.on("error", () => { 37 | global.cachedConnection = null; 38 | }); 39 | 40 | connection.on("disconnected", () => { 41 | global.cachedConnection = null; 42 | }); 43 | 44 | connection.on("close", () => { 45 | global.cachedConnection = null; 46 | }); 47 | 48 | process.on("SIGINT", () => { 49 | connection.close(() => { 50 | process.exit(0); 51 | }); 52 | }); 53 | } else { 54 | global.cachedConnection = null; 55 | } 56 | }, 57 | reason => { 58 | console.log("reason ====> ", reason); 59 | const error = "DB Connection FAILURE ".concat(reason); 60 | global.cachedConnection = null; 61 | throw Error(error); 62 | } 63 | ); 64 | } 65 | 66 | await next(); 67 | if (closeConnection) { 68 | await mongoose.disconnect(); 69 | } 70 | }; 71 | -------------------------------------------------------------------------------- /samples/mongoose-connection/src/middleware/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./databaseMiddleware"; 2 | -------------------------------------------------------------------------------- /samples/mongoose-connection/src/types/custom.d.ts: -------------------------------------------------------------------------------- 1 | import { Connection } from "mongoose"; 2 | 3 | declare global { 4 | namespace NodeJS { 5 | interface Global { 6 | connectionPromise: Promise; 7 | cachedConnection: Connection; 8 | //These next two variables are just to check the number of connections v. number of instantiations. They can be removed. 9 | numberOfConnections: number; 10 | numberOfDbMiddlewares: number; 11 | } 12 | } 13 | } 14 | export = global 15 | -------------------------------------------------------------------------------- /samples/mongoose-connection/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "esModuleInterop": true, 5 | "target": "es6", 6 | "moduleResolution": "node", 7 | "sourceMap": true, 8 | "outDir": "./lib" 9 | }, 10 | "files": [ 11 | "src/types/custom.d.ts" 12 | ], 13 | "include": ["./"], 14 | "lib": ["es2015"] 15 | } 16 | -------------------------------------------------------------------------------- /samples/sls-telemetry-middleware/.gitignore: -------------------------------------------------------------------------------- 1 | # package directories 2 | node_modules 3 | jspm_packages 4 | .webpack 5 | 6 | # Serverless directories 7 | .serverless 8 | -------------------------------------------------------------------------------- /samples/sls-telemetry-middleware/.npmrc: -------------------------------------------------------------------------------- 1 | @multicloud:registry=https://pkgs.dev.azure.com/711digital/_packaging/common-packages/npm/registry/ 2 | engine-strict=true 3 | save-exact=true 4 | always-auth=true 5 | 6 | ; begin auth token 7 | //pkgs.dev.azure.com/711digital/_packaging/common-packages/npm/registry/:username=common-packages 8 | //pkgs.dev.azure.com/711digital/_packaging/common-packages/npm/registry/:_password=${NPM_TOKEN} 9 | //pkgs.dev.azure.com/711digital/_packaging/common-packages/npm/registry/:email=common-packages 10 | ; end auth token 11 | -------------------------------------------------------------------------------- /samples/sls-telemetry-middleware/hello/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "disabled": false, 3 | "bindings": [ 4 | { 5 | "type": "httpTrigger", 6 | "direction": "in", 7 | "name": "req", 8 | "authLevel": "function", 9 | "methods": [ 10 | "GET" 11 | ] 12 | }, 13 | { 14 | "type": "http", 15 | "direction": "out", 16 | "name": "res" 17 | } 18 | ], 19 | "entryPoint": "hello", 20 | "scriptFile": "handler.js" 21 | } -------------------------------------------------------------------------------- /samples/sls-telemetry-middleware/hello/handler.js: -------------------------------------------------------------------------------- 1 | const { 2 | App, 3 | TelemetryServiceMiddleware, 4 | HTTPBindingMiddleware 5 | } = require("@multicloud/sls-core"); 6 | 7 | const { AzureModule } = require("@multicloud/sls-azure"); 8 | const { AwsModule } = require("@multicloud/sls-aws"); 9 | 10 | const analyticsData = {}; 11 | const telemetryService = { 12 | collect: (key, data) => { 13 | analyticsData[key] = data; 14 | return Promise.resolve(); 15 | }, 16 | 17 | flush: () => { 18 | Object.keys(analyticsData).forEach(key => { 19 | console.log(analyticsData[key]); 20 | }); 21 | return Promise.resolve(); 22 | } 23 | }; 24 | 25 | const options = { 26 | telemetryService, 27 | shouldFlush: true 28 | }; 29 | 30 | const app = new App(new AzureModule(), new AwsModule()); 31 | 32 | const handler = context => { 33 | context.telemetry.collect("custom-event", { value: 32 }); 34 | context.send("sucesess"); 35 | }; 36 | const middlewareWithTimeout = () => async (context, next) => { 37 | function sleep(ms) { 38 | return new Promise(resolve => { 39 | setTimeout(resolve, ms); 40 | }); 41 | } 42 | 43 | await sleep(3000); 44 | await next(); 45 | }; 46 | 47 | module.exports.hello = app.use( 48 | [ 49 | HTTPBindingMiddleware(), 50 | TelemetryServiceMiddleware(options), 51 | middlewareWithTimeout() 52 | ], 53 | handler 54 | ); 55 | -------------------------------------------------------------------------------- /samples/sls-telemetry-middleware/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0" 3 | } -------------------------------------------------------------------------------- /samples/sls-telemetry-middleware/local.settings.json: -------------------------------------------------------------------------------- 1 | {"IsEncrypted":false,"Values":{"AzureWebJobsStorage":"","FUNCTIONS_WORKER_RUNTIME":"node"}} -------------------------------------------------------------------------------- /samples/sls-telemetry-middleware/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "azure-telemetry-middleware", 3 | "version": "", 4 | "scripts": { 5 | "build": "tsc", 6 | "build:production": "npm run prestart && npm prune --production", 7 | "watch": "tsc --w", 8 | "prestart": "npm run build && func extensions install", 9 | "start:host": "func start", 10 | "start": "npm-run-all --parallel start:host watch", 11 | "test": "jest" 12 | }, 13 | "description": "", 14 | "dependencies": { 15 | "@multicloud/sls-aws": "0.0.7-13", 16 | "@multicloud/sls-azure": "0.0.8-13", 17 | "@multicloud/sls-core": "0.0.11-16" 18 | }, 19 | "devDependencies": { 20 | "@azure/functions": "^1.0.1-beta1", 21 | "@types/jest": "24.0.15", 22 | "@types/node": "10.0.3", 23 | "babel-jest": "^24.8.0", 24 | "babel-preset-react-app": "^9.0.0", 25 | "npm-run-all": "^4.1.5", 26 | "serverless-azure-functions": "^1.0.0-7", 27 | "serverless-offline": "^5.5.0", 28 | "serverless-webpack": "^5.3.1", 29 | "webpack": "^4.33.0", 30 | "webpack-cli": "^3.3.3", 31 | "typescript": "^3.3.3" 32 | }, 33 | "engines": { 34 | "node": ">= 6.5.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /samples/sls-telemetry-middleware/serverless-aws.yml: -------------------------------------------------------------------------------- 1 | # Welcome to Serverless! 2 | # 3 | # This file is the main config file for your service. 4 | # It's very minimal at this point and uses default values. 5 | # You can always add more config options for more control. 6 | # We've included some commented out config examples here. 7 | # Just uncomment any of them to get that config option. 8 | # 9 | # For full config options, check the docs: 10 | # docs.serverless.com 11 | # 12 | # Happy Coding! 13 | 14 | service: sls-aws # NOTE: update this with your service name 15 | #app: your-app-name 16 | #tenant: your-tenant-name 17 | 18 | # You can pin your service to only deploy with a specific Serverless version 19 | # Check out our docs for more details 20 | # frameworkVersion: "=X.X.X" 21 | 22 | provider: 23 | name: aws 24 | runtime: nodejs10.x 25 | 26 | custom: 27 | webpack: 28 | keepOutputDirectory: true 29 | 30 | plugins: 31 | - serverless-offline 32 | - serverless-webpack 33 | 34 | functions: 35 | hello: 36 | handler: hello/handler.hello 37 | events: 38 | - http: 39 | method: GET 40 | path: /hello 41 | -------------------------------------------------------------------------------- /samples/sls-telemetry-middleware/serverless-azure.yml: -------------------------------------------------------------------------------- 1 | # Welcome to Serverless! 2 | # 3 | # This file is the main config file for your service. 4 | # It's very minimal at this point and uses default values. 5 | # You can always add more config options for more control. 6 | # We've included some commented out config examples here. 7 | # Just uncomment any of them to get that config option. 8 | # 9 | # For full config options, check the docs: 10 | # docs.serverless.com 11 | # 12 | # Happy Coding! 13 | 14 | service: v1 15 | 16 | # You can pin your service to only deploy with a specific Serverless version 17 | # Check out our docs for more details 18 | # frameworkVersion: "=X.X.X" 19 | 20 | custom: 21 | webpack: 22 | keepOutputDirectory: true 23 | 24 | provider: 25 | prefix: sls-sample 26 | name: azure 27 | region: ${opt:region, 'westus'} 28 | stage: ${opt:stage, 'dev'} 29 | type: premium 30 | apim: 31 | api: 32 | name: v1 33 | subscriptionRequired: false 34 | displayName: v1 35 | description: V1 sample app APIs 36 | protocols: 37 | - https 38 | path: v1 39 | tags: 40 | - tag1 41 | - tag2 42 | authorization: none 43 | cors: 44 | allowCredentials: false 45 | allowedOrigins: 46 | - "*" 47 | allowedMethods: 48 | - GET 49 | - POST 50 | - PUT 51 | - DELETE 52 | - PATCH 53 | allowedHeaders: 54 | - "*" 55 | exposeHeaders: 56 | - "*" 57 | 58 | plugins: 59 | - serverless-azure-functions 60 | - serverless-webpack 61 | 62 | functions: 63 | hello: 64 | handler: hello/handler.hello 65 | apim: 66 | operations: 67 | - method: get 68 | urlTemplate: /hello 69 | displayName: Hello 70 | events: 71 | - http: true 72 | x-azure-settings: 73 | methods: 74 | - GET 75 | authLevel: function 76 | - http: true 77 | x-azure-settings: 78 | direction: out 79 | name: res 80 | -------------------------------------------------------------------------------- /samples/sls-telemetry-middleware/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const slsw = require("serverless-webpack"); 3 | 4 | module.exports = { 5 | entry: slsw.lib.entries, 6 | target: "node", 7 | output: { 8 | libraryTarget: "commonjs2", 9 | library: "index", 10 | path: path.resolve(__dirname, ".webpack"), 11 | filename: "[name].js" 12 | }, 13 | plugins: [] 14 | }; 15 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | npm ci 5 | # NOTE: build will also invoke Node prebuild, pretest and test lifecycle scripts 6 | npm run build 7 | -------------------------------------------------------------------------------- /scripts/publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eo pipefail 3 | 4 | # NOTE: build and publish are always executed in a package (e.g core) working directory 5 | $(pwd)/../scripts/build.sh 6 | 7 | # set up .npmrc to authenticate with the provided token 8 | echo "Set up .npmrc ..." 9 | echo "//registry.npmjs.org/:_authToken=\${NPM_TOKEN}" > .npmrc 10 | 11 | # NOTE: auth is taken care of via AzDO `npm auth` task. ENV vars would work as well. 12 | if [ -z "$1" ]; then 13 | echo "Publishing 'latest' to NPM..."; 14 | npm publish --access public 15 | else 16 | echo "Publishing 'prerelease' to NPM..."; 17 | npm publish --tag=beta --access public 18 | fi 19 | -------------------------------------------------------------------------------- /scripts/version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | PACKAGE_NAME=$1 5 | NPM_RELEASE_TYPE=${2-"prerelease"} 6 | 7 | # Get full branch name excluding refs/head from the env var SOURCE_BRANCH 8 | SOURCE_BRANCH_NAME=${SOURCE_BRANCH/refs\/heads\/} 9 | 10 | export GIT_SSH_COMMAND="ssh -vv -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no" 11 | 12 | # Configure git to commit as Azure Dev Ops 13 | git config --local --add url."git@github.com:".insteadOf "https://github.com/" 14 | git config --local user.email "Multicloud Admin" 15 | git config --local user.name "multicloud@serverless.com" 16 | 17 | git pull origin ${SOURCE_BRANCH_NAME} 18 | git checkout ${SOURCE_BRANCH_NAME} 19 | echo Checked out branch: ${SOURCE_BRANCH_NAME} 20 | 21 | NPM_VERSION=`npm version ${NPM_RELEASE_TYPE}` 22 | echo Set NPM version to: ${NPM_VERSION} 23 | 24 | # Stage update to package.json files 25 | git add package.json 26 | git add package-lock.json 27 | 28 | # Since there isn't a package.json at the root of repo 29 | # and we have multiple packages within same repo 30 | # we need to manually commit and tag in order to create unique tag names 31 | git commit -m "release: Bumping NPM package ${PACKAGE_NAME} ${NPM_RELEASE_TYPE} to version ${NPM_VERSION} ***NO_CI***" 32 | SHA=`git rev-parse HEAD` 33 | 34 | git tag ${PACKAGE_NAME}-${NPM_VERSION} 35 | 36 | git remote add authOrigin git@github.com:serverless/multicloud.git 37 | git push authOrigin ${SOURCE_BRANCH_NAME} --tags 38 | 39 | echo Pushed new tag: ${PACKAGE_NAME}-${NPM_VERSION} @ SHA: ${SHA:0:8} 40 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ 5 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 6 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 7 | "outDir": "lib", /* Redirect output structure to the directory. */ 8 | "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 9 | /* Strict Type-Checking Options */ 10 | "strict": false, /* Enable all strict type-checking options. */ 11 | /* Module Resolution Options */ 12 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 13 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 14 | /* Experimental Options */ 15 | "resolveJsonModule": true, 16 | "experimentalDecorators": true 17 | } 18 | } 19 | --------------------------------------------------------------------------------