├── .eslintignore ├── .eslintrc.json ├── .github └── workflows │ ├── deploy-dev.yml │ ├── deploy-main.yml │ ├── format-lint.yml │ ├── test.yml │ └── version.yml ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .npmignore ├── .npmrc ├── .prettierrc ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── ci.sh ├── docs └── design.md ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── EventPattern.ts ├── ExpressBridge.ts ├── Telemetry.ts ├── index.ts └── tests │ ├── eventPattern.data.ts │ ├── eventPattern.test.ts │ ├── expressBridge.data.ts │ ├── expressBridge.test.ts │ └── telemetry.test.ts └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root":true, 3 | "env": { 4 | "es2021": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/recommended", 10 | "prettier" 11 | ], 12 | "parser": "@typescript-eslint/parser", 13 | "parserOptions": { 14 | "ecmaVersion": "latest", 15 | "sourceType": "module" 16 | }, 17 | "plugins": [ 18 | "@typescript-eslint" 19 | ], 20 | "rules": { 21 | "import/extensions": "off", 22 | "@typescript-eslint/no-explicit-any": "off" 23 | }, 24 | "overrides": [ 25 | { 26 | "files": [ 27 | "src/tests/**/*.ts" 28 | ], 29 | "env": { 30 | "jest": true 31 | } 32 | } 33 | ], 34 | "settings": { 35 | "import/resolver": { 36 | "node": { 37 | "extensions": [".js", ".ts"] 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /.github/workflows/deploy-dev.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: DEV CI 4 | 5 | # Controls when the workflow will run 6 | on: 7 | push: 8 | branches: [ dev ] 9 | 10 | # Allows you to run this workflow manually from the Actions tab 11 | workflow_dispatch: 12 | 13 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 14 | jobs: 15 | deploy: 16 | runs-on: ubuntu-latest 17 | # Steps represent a sequence of tasks that will be executed as part of the job 18 | steps: 19 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 20 | - uses: actions/checkout@v2 21 | 22 | # Install dependencies 23 | - name: Install 24 | run: npm install 25 | 26 | # Build package 27 | - name: Build 28 | run: npm run build 29 | 30 | - name: NPM Publish to github package repo 31 | uses: JS-DevTools/npm-publish@v1 32 | with: 33 | token: ${{ secrets.GITHUB_TOKEN }} 34 | registry: https://npm.pkg.github.com/ 35 | -------------------------------------------------------------------------------- /.github/workflows/deploy-main.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: PROD CI 4 | 5 | # Controls when the workflow will run 6 | on: 7 | push: 8 | tags: 9 | - '*' 10 | 11 | # Allows you to run this workflow manually from the Actions tab 12 | workflow_dispatch: 13 | 14 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 15 | jobs: 16 | deploy: 17 | runs-on: ubuntu-latest 18 | # Steps represent a sequence of tasks that will be executed as part of the job 19 | steps: 20 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 21 | - uses: actions/checkout@v2 22 | 23 | - name: Prepare 24 | run: npm run prepare:ci -- ${{ github.ref }} 25 | 26 | # Install dependencies 27 | - name: Install 28 | run: npm install 29 | 30 | # Build package 31 | - name: Build 32 | run: npm run build 33 | 34 | - name: NPM Publish to github package repo 35 | uses: JS-DevTools/npm-publish@v1 36 | with: 37 | token: ${{ secrets.NPM_TOKEN }} 38 | registry: https://registry.npmjs.org/ 39 | # access level for scoped packages -- by 1.0.0 we may unscope from @oslabs-beta 40 | access: restricted 41 | # just see if it works -- don't actually publish 42 | dry-run: true 43 | # only run if published version number differs from the one in package.json 44 | check-version: true 45 | -------------------------------------------------------------------------------- /.github/workflows/format-lint.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | name: DEV CI 3 | 4 | # Controls when the workflow will run 5 | on: 6 | pull_request: 7 | branches: [ dev ] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 13 | jobs: 14 | # This workflow contains a single job called "format" 15 | format-lint: 16 | # The type of runner that the job will run on 17 | runs-on: ubuntu-latest 18 | 19 | # Steps represent a sequence of tasks that will be executed as part of the job 20 | steps: 21 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 22 | - uses: actions/checkout@v2 23 | 24 | # Install dependencies 25 | - name: Install 26 | run: npm install 27 | 28 | # Format project 29 | - name: Format project 30 | run: npm run format 31 | 32 | # Lint project 33 | - name: Lint project 34 | run: npm run lint 35 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: DEV CI 4 | 5 | # Controls when the workflow will run 6 | on: 7 | pull_request: 8 | branches: [ dev ] 9 | 10 | # Allows you to run this workflow manually from the Actions tab 11 | workflow_dispatch: 12 | 13 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 14 | jobs: 15 | test: 16 | runs-on: ubuntu-latest 17 | # Steps represent a sequence of tasks that will be executed as part of the job 18 | steps: 19 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 20 | - uses: actions/checkout@v2 21 | 22 | # Install dependencies 23 | - name: Install 24 | run: npm install 25 | 26 | - name: Test 27 | run: npm test 28 | 29 | -------------------------------------------------------------------------------- /.github/workflows/version.yml: -------------------------------------------------------------------------------- 1 | name: Dev Version Increment 2 | 3 | on: 4 | push: 5 | branches: 6 | - dev 7 | 8 | jobs: 9 | version: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | with: 14 | token: ${{ secrets.GITHUB_TOKEN }} 15 | - run: git config --global user.name 'PandaWhale CI' 16 | - run: git config --global user.email 'ci@pandawhale.it' 17 | - run: npm version patch -m "[DEV] %s" 18 | - run: git push 19 | 20 | 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm test 5 | npx lint-staged 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | **/* 2 | !dist/**/* 3 | !package.json 4 | !package-lock.json 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | @oslabs-beta:registry=https://npm.pkg.github.com -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "trailingComma": "es5" 5 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "jest.jestCommandLine": "EB_TELEMETRY=true npm t --" 3 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 OSLabs Beta 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

3 | ExpressBridge v1.0 4 |

5 |

6 | 7 | A tiny microservice framework for Nodejs. 🚀 8 | 9 |

10 | 11 |

12 | ExpressBridge is a free and open source microservice framework for Nodejs-based microservices. It is appropriate for use in any microservice or FaaS environment and across all cloud vendors. 13 |

14 |

15 | 16 | Gatsby is released under the MIT license. 17 | 18 | GitHub Workflow Status 19 |

20 | 21 | ## What's in this document 22 | - Overview 23 | - Quickstart Guide 24 | - Learning ExpressBridge 25 | - Sample Project 26 | - Release Notes 27 | - Documentation Site 28 | - How to Contribute 29 | 30 | ## Overview: ExpressBridge Fundamentals 31 | ExpressBridge provides a fluid, easy to use API that enables developers to stand up new microservices that handle, process, and dispatch messages with ease. The API surface is small and straightforward. Consider the following example: 32 | 33 | ```ts 34 | const handler = (message) => { 35 | const expressBridge = new ExpressBridge({ 36 | alwaysRunHooks: true, 37 | }); 38 | 39 | expressBridge.pre((message) => { 40 | // ... pre-process message 41 | }); 42 | 43 | expressBridge.post((message) => { 44 | // ... post-process message 45 | }); 46 | 47 | // respond to some generic AWS events 48 | const awsEventHandler = (message) => { /* ... write to db, log event, dispatch message, etc */ } 49 | expressBridge.use({ source: 'aws.*' }, awsEventHandler, (err) => { console.log(err) }) 50 | 51 | expressBridge.process(message); 52 | } 53 | ``` 54 | 55 | The above example represents the entire API surface of the framework in its simplest implementation. However, ExpressBridge is highly configurable so as to be suitable for a wide range of scenarios. Understanding this configurability will enable you to fully leverage the power of this framework. -------------------------------------------------------------------------------- /ci.sh: -------------------------------------------------------------------------------- 1 | sed -i "s/@oslabs-beta\\/expressbridge/expressbridge/g" package.json 2 | # @oslabs-beta/expressbridge -> expresbridge 3 | 4 | # specify delimiter 5 | IFS='/'; 6 | 7 | # read in github.ref property 8 | read -ra ref <<< $1 9 | 10 | TAG_VERSION = $ref[3] 11 | 12 | echo $TAG_VERSION 13 | 14 | sed -i "s/\"version.*/\"version\": \"$TAG_VERSION\",/g" package.json 15 | -------------------------------------------------------------------------------- /docs/design.md: -------------------------------------------------------------------------------- 1 | # Design document 2 | 3 | ## Objective 4 | Provide an easy-to-use abstraction for consuming Amazon Event Bridge events, exposed as a configurable express middleware. 5 | 6 | ## Requirements 7 | Specific requirements needed. How to judge if complete 8 | An implementation of an express middleware that accepts an event pattern to handle. 9 | 10 | 11 | 12 | ## What is out of scope 13 | 14 | 15 | ## Background to the problem domain -- what is it, why is it important 16 | 17 | ## Overview -- readable by a generic engineer -- with diagrams. 18 | 19 | ## Infrastructure needed 20 | 21 | ## Success criteria 22 | - Functional + non functional requirements 23 | 24 | ```ts 25 | const expressBridge = require(‘expressbridge’); 26 | app.use(expressBridge.configure()); 27 | 28 | Const evtPattern = { 29 | “Source”: “aws.ec2”, 30 | “Detail-type”: “EC2 Instance State-change Notification” 31 | “Detail”: {“state”: “terminated”, “product_source”: /vendor$/i} 32 | }; 33 | 34 | expressBridge.use({evtPattern}, handler); 35 | 36 | incomingEvent.has().key(‘source’).withValue(‘aws.ec2’) 37 | 38 | 39 | ##### possible bit string discussion ##### 40 | 41 | Const keyMappingNumberTable = { 42 | Source: 0, 43 | Detail: 1, 44 | Detail-Type: 2, 45 | ... 46 | } 47 | 48 | Function createBitStr(eventObj, keyMappingNumberTable) { 49 | Const bitArray = new Array(Object.keys(keyMappingNumberTable).length).fill(0); 50 | For (let key in eventObj) { 51 | bitArray[keyMappingNumberTable[key]] = 1 52 | } 53 | Return bitArray.join(‘’); 54 | } 55 | 56 | Function findMathcingEventPattern(eventBitStr, patternBitStrObj) { 57 | For (let key in patternBitStrObj) { 58 | // number xor with itself equals 0 59 | If (key ^ eventBitStr === 0) {return patternBitStrObj[key]} 60 | } 61 | } 62 | 63 | Const reverseHashMap = {}; 64 | Function createEventHashKey(eventObj) { 65 | Const keys = Object.keys(eventObj); 66 | keys.sort(); 67 | Let str = ‘’; 68 | For (let key of keys) { 69 | str+= key + ‘$’ + eventObj[key] 70 | } 71 | reverseHashMap[str] = eventObj; 72 | } 73 | 74 | ##### possible bit string discussion ##### 75 | 76 | Const matcher = (event) => { 77 | If (event.detail.state === ‘terminated’) return true; 78 | }; 79 | 80 | expressBridge.use(matcher, handler); 81 | 82 | 83 | { 84 | "version": "0", 85 | "id": "6a7e8feb-b491-4cf7-a9f1-bf3703467718", 86 | "detail-type": "EC2 Instance State-change Notification", 87 | "source": "aws.ec2", 88 | "account": "111122223333", 89 | "time": "2017-12-22T18:43:48Z", 90 | "region": "us-west-1", 91 | "resources": [ 92 | "arn:aws:ec2:us-west-1:123456789012:instance/i-1234567890abcdef0" 93 | ], 94 | "detail": { 95 | "instance-id": " i-1234567890abcdef0", 96 | "state": "terminated" 97 | } 98 | } 99 | 100 | Const evtPattern1 = { 101 | “Source”: “aws.ec2”, 102 | “Detail-type”: “EC2 Instance State-change Notification” 103 | “Detail”: {“state”: “terminated”} 104 | }; 105 | 106 | Const evtPattern2 = { 107 | “Source”: “aws.ec2”, 108 | “Detail-type”: “EC2 Instance State-change Notification” 109 | “Detail”: {“state”: “started”} 110 | }; 111 | 112 | expressBridge.use({evtPattern1}, handler); 113 | expressBridge.use({evtPattern2}, handler); 114 | 115 | Object.keys(eventPatternList).forEach(() => { 116 | 117 | }) 118 | 119 | // for each handler 120 | Look at each key in the pattern 121 | If all key/val matches → run handler function 122 | Else… move to next handler registered 123 | 124 | ``` -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ 2 | module.exports = { 3 | roots: ['./src'], 4 | preset: 'ts-jest', 5 | testEnvironment: 'node', 6 | moduleFileExtensions: ['js', 'ts'] 7 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@oslabs-beta/expressbridge", 3 | "version": "0.0.29", 4 | "description": "expressbridge is a event-driven microservice framework for Node.js", 5 | "main": "./dist/index.js", 6 | "types": "./dist/index.d.ts", 7 | "scripts": { 8 | "build": "tsc", 9 | "start": "tsc & node .", 10 | "dev": "tsc -w & nodemon .", 11 | "test": "EB_TELEMETRY=true jest", 12 | "format": "prettier -c src/*", 13 | "prepare": "husky install", 14 | "lint": "eslint src/**/*.ts", 15 | "prepare:ci": "./ci.sh", 16 | "format:fix": "prettier -c src/* --write", 17 | "lint:fix": "eslint src/**/*.ts --fix" 18 | }, 19 | "devDependencies": { 20 | "@types/express": "^4.17.13", 21 | "@types/jest": "^27.4.0", 22 | "@types/node": "^16.11.17", 23 | "@types/uuid": "^8.3.4", 24 | "@typescript-eslint/eslint-plugin": "^5.3.1", 25 | "@typescript-eslint/parser": "^5.3.1", 26 | "eslint": "^8.2.0", 27 | "eslint-config-prettier": "^8.3.0", 28 | "eslint-plugin-import": "^2.25.3", 29 | "husky": "^7.0.0", 30 | "jest": "^27.3.1", 31 | "lint-staged": "^11.2.6", 32 | "nodemon": "^2.0.14", 33 | "prettier": "^2.4.1", 34 | "ts-jest": "^27.0.7", 35 | "ts-node": "^10.4.0", 36 | "typescript": "^4.4.4" 37 | }, 38 | "repository": { 39 | "type": "git", 40 | "url": "git+https://github.com/oslabs-beta/expressbridge.git" 41 | }, 42 | "author": "", 43 | "license": "ISC", 44 | "bugs": { 45 | "url": "https://github.com/oslabs-beta/expressbridge/issues" 46 | }, 47 | "publishConfig": { 48 | "registry": "https://npm.pkg.github.com" 49 | }, 50 | "homepage": "https://github.com/oslabs-beta/expressbridge#readme", 51 | "lint-staged": { 52 | "src/**/*.ts": [ 53 | "npm run format", 54 | "npm run lint" 55 | ] 56 | }, 57 | "dependencies": { 58 | "axios": "^0.24.0", 59 | "uuid": "^8.3.2" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/EventPattern.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export type handlerType = (context: any) => any; 3 | export type errorHandlerType = (exception: unknown) => never; 4 | 5 | export class EventPattern { 6 | constructor( 7 | private pattern: Partial, 8 | private handlers: handlerType[], 9 | private errorHandler: errorHandlerType 10 | ) {} 11 | 12 | public getPattern(): Partial { 13 | return this.pattern; 14 | } 15 | 16 | public getHandlers(): handlerType[] { 17 | return this.handlers; 18 | } 19 | 20 | public getErrorHandler(): errorHandlerType { 21 | return this.errorHandler; 22 | } 23 | 24 | public setPattern(pattern: Partial): void { 25 | this.pattern = pattern; 26 | } 27 | 28 | public setHandlers(handlers: handlerType[]): void { 29 | this.handlers = handlers; 30 | } 31 | 32 | public setErrorHandler(errorHandler: errorHandlerType): void { 33 | this.errorHandler = errorHandler; 34 | } 35 | 36 | public test(incomingEvent: Record): boolean { 37 | const keysPresent = Object.keys(this.pattern).every( 38 | (key) => !!incomingEvent[key] 39 | ); 40 | let result = false; 41 | if (keysPresent) { 42 | result = Object.entries(this.pattern).reduce( 43 | (acc: boolean, [key, value]: [string, unknown]) => { 44 | return acc && testValue(value, incomingEvent[key]); 45 | }, 46 | true 47 | ); 48 | } 49 | 50 | return result; 51 | } 52 | } 53 | 54 | function testValue(expected: unknown, actual: unknown): boolean { 55 | if (!expected || !actual) return false; 56 | let result = true; 57 | 58 | // if Object, iterate through keys and recursively validate each 59 | if (expected instanceof Object) { 60 | let key: keyof typeof expected; 61 | for (key in expected) { 62 | result = result && testValue(expected[key], actual[key]); 63 | } 64 | } 65 | 66 | // validate 67 | if (typeof expected === 'string') { 68 | // support simple wildcard-based matching 69 | const wildcard = expected.indexOf('*'); 70 | if (wildcard === expected.length - 1) { 71 | result = 72 | result && (actual as string).includes(expected.substring(0, wildcard)); 73 | } else if (wildcard === 0) { 74 | result = 75 | result && 76 | (actual as string).includes( 77 | expected.substring(wildcard + 1, expected.length) 78 | ); 79 | } else if (wildcard > 0 && wildcard < expected.length - 1) { 80 | result = 81 | (actual as string).startsWith(expected.substring(0, wildcard)) && 82 | (actual as string).endsWith( 83 | expected.substring(wildcard + 1, expected.length) 84 | ); 85 | } else { 86 | result = expected === actual; 87 | } 88 | } else if (typeof expected === 'number' || typeof expected === 'boolean') { 89 | // TODO: Match numbers/booleans 90 | } else if (expected instanceof RegExp) { 91 | // TODO: Match via RegEx 92 | } else { 93 | // TODO: This may not be necessary. Might also be ok to throw exception here. 94 | } 95 | 96 | return result; 97 | } 98 | -------------------------------------------------------------------------------- /src/ExpressBridge.ts: -------------------------------------------------------------------------------- 1 | import type { handlerType, errorHandlerType } from './EventPattern'; 2 | import type { TelemetryConfig } from './Telemetry'; 3 | import { EventPattern } from './EventPattern'; 4 | import { Telemetry } from './Telemetry'; 5 | 6 | type EventType = Record; 7 | 8 | interface ExpressBridgeOptions { 9 | alwaysRunHooks?: boolean; 10 | telemetry?: TelemetryConfig; 11 | } 12 | export class ExpressBridge { 13 | private comparableCollection: EventPattern[] = []; 14 | 15 | private preHandlers: handlerType[] = []; 16 | 17 | private postHandlers: handlerType[] = []; 18 | 19 | private telemetry: Telemetry; 20 | 21 | public constructor(public options: ExpressBridgeOptions) {} 22 | 23 | public use( 24 | pattern: Record, 25 | handlers: handlerType[], 26 | errorHandler: errorHandlerType 27 | ): void { 28 | const patternInstance = new EventPattern( 29 | pattern, 30 | handlers, 31 | errorHandler 32 | ); 33 | this.comparableCollection.push(patternInstance); 34 | } 35 | 36 | public async process(incomingEvent: EventType): Promise { 37 | try { 38 | // if telemetry is defined, set uuid and call beacon 39 | console.log('Telemetry enabled: ', !!process.env.EB_TELEMETRY); 40 | if (process.env.EB_TELEMETRY && this.options.telemetry) { 41 | this.telemetry = new Telemetry(this.options.telemetry); 42 | this.telemetry.tagEvent(incomingEvent); 43 | } 44 | 45 | await this.telemetry?.beacon('EB-PROCESS', { 46 | description: 'Process function called. Generating process ID.', 47 | data: { 48 | event: incomingEvent, 49 | }, 50 | }); 51 | 52 | const matchedPatterns = this.match(incomingEvent); 53 | 54 | if (matchedPatterns.length > 0) { 55 | await this.telemetry?.beacon('EB-MATCH', { 56 | description: 'Patterns matched for event. Calling assigned handlers.', 57 | data: { 58 | matchedPatterns, 59 | }, 60 | }); 61 | 62 | // run pre hook 63 | const output = await pipeline(incomingEvent, ...this.preHandlers); 64 | 65 | await this.telemetry?.beacon('EB-PRE', { 66 | description: 'Pre hooks running', 67 | data: output, 68 | }); 69 | 70 | // run pattern handlers 71 | for (const pattern of matchedPatterns) { 72 | try { 73 | await pipeline(output, ...pattern.getHandlers()); 74 | } catch (err) { 75 | pattern.getErrorHandler()(err); 76 | } 77 | } 78 | 79 | await this.telemetry?.beacon('EB-HANDLERS', { 80 | description: 'Handlers running', 81 | data: output, 82 | }); 83 | 84 | // run post handlers 85 | if (this.postHandlers) pipeline(output, ...this.postHandlers); 86 | 87 | await this.telemetry?.beacon('EB-POST', { 88 | description: 'Post hooks running', 89 | data: incomingEvent, 90 | }); 91 | } else if ( 92 | this.options.alwaysRunHooks && 93 | (this.preHandlers || this.postHandlers) 94 | ) { 95 | await pipeline( 96 | incomingEvent, 97 | ...this.preHandlers, 98 | ...this.postHandlers 99 | ); 100 | } 101 | } catch (err: unknown) { 102 | console.log('Error occurred processing event: ', err); 103 | await this.telemetry?.beacon('EB-ERROR', { 104 | description: 'Error occurred in processing', 105 | data: err, 106 | }); 107 | } 108 | } 109 | 110 | public pre(...handlers: handlerType[]): void { 111 | this.preHandlers.push(...handlers); 112 | } 113 | 114 | public post(...handlers: handlerType[]): void { 115 | this.postHandlers.push(...handlers); 116 | } 117 | /* */ 118 | private match( 119 | incomingEvent: Record 120 | ): EventPattern[] { 121 | return this.comparableCollection.filter( 122 | (eventPattern: EventPattern>) => { 123 | return eventPattern.test(incomingEvent as any); 124 | } 125 | ); 126 | } 127 | } 128 | 129 | function pipeline(message: EventType, ...functions: handlerType[]): EventType { 130 | const reduced = functions.reduce(async (acc, func) => { 131 | return func(await acc); 132 | }, message); 133 | 134 | return reduced; 135 | } 136 | -------------------------------------------------------------------------------- /src/Telemetry.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { v4 } from 'uuid'; 3 | import { AxiosRequestConfig } from 'axios'; 4 | 5 | export type TelemetryConfig = Partial & { 6 | serviceName: string; 7 | }; 8 | 9 | export class Telemetry { 10 | private eb_event_id: string; 11 | 12 | constructor(private requestConfig: TelemetryConfig) {} 13 | 14 | public async beacon( 15 | tag: string, 16 | message: Record 17 | ): Promise { 18 | try { 19 | const { serviceName, ...requestConfig } = this.requestConfig; 20 | await axios({ 21 | ...requestConfig, 22 | data: { 23 | tag, 24 | eb_event_id: this.eb_event_id, 25 | serviceName, 26 | message, 27 | }, 28 | }); 29 | } catch (err) { 30 | console.log('Error calling telemetry beacon', err); 31 | } 32 | } 33 | 34 | public tagEvent(event: Record): string { 35 | const { body, detail, Records } = event; 36 | let payload = body ?? detail ?? Records; 37 | 38 | let tag = v4(); 39 | if (Array.isArray(payload)) { 40 | for (const record of payload) { 41 | tag = record.eb_event_id || tag; 42 | record.eb_event_id = tag; 43 | } 44 | } else if (payload) { 45 | payload = typeof payload === 'string' ? JSON.parse(payload) : payload; 46 | tag = payload.eb_event_id || tag; 47 | payload.eb_event_id = tag; 48 | event[body ? 'body' : 'detail'] = payload; 49 | } else { 50 | tag = event.eb_event_id || tag; 51 | event.eb_event_id = tag; 52 | } 53 | 54 | this.eb_event_id = tag; 55 | 56 | return tag; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ExpressBridge'; 2 | -------------------------------------------------------------------------------- /src/tests/eventPattern.data.ts: -------------------------------------------------------------------------------- 1 | export const stringScenarios = [ 2 | ['market*', 'marketplaceOffers', true], 3 | ['aws.*', 'aws.ec2', true], 4 | ['*.postfix', 'service.postfix', true], 5 | ['*.postfix', 'service.postmix', false], 6 | ['market*', 'elephant', false], 7 | ['*narwhal', 'goose', false], 8 | ['cool*beans', 'coolxbeans', true], 9 | ['cool*beans', 'coolxbeanz', false], 10 | ['*beans', 'coolxbeans', true], 11 | ['beans', 'coolxbeans', false], 12 | ]; 13 | 14 | export const nestedScenarios = [ 15 | [ 16 | { requestContext: { http: { method: 'POST' } } }, 17 | { requestContext: { http: { method: 'POST' } } }, 18 | true, 19 | ], 20 | [ 21 | { requestContext: { http: { method: '*' } } }, 22 | { requestContext: { http: { method: 'POST' } } }, 23 | true, 24 | ], 25 | ]; 26 | -------------------------------------------------------------------------------- /src/tests/eventPattern.test.ts: -------------------------------------------------------------------------------- 1 | import { stringScenarios, nestedScenarios } from './eventPattern.data'; 2 | import { EventPattern } from '../EventPattern'; 3 | 4 | describe('Test EventPattern.test', () => { 5 | test.each(stringScenarios)( 6 | 'should produce correct test results given string pattern', 7 | (patternString, actual, output) => { 8 | const basePattern = { 9 | source: patternString, //'orders.*' 10 | }; 11 | const pattern = new EventPattern( 12 | basePattern, 13 | [(input) => input], 14 | (err) => { 15 | throw err; 16 | } 17 | ); 18 | 19 | const result = pattern.test({ 20 | // incoming eventm 21 | source: actual, // 'orders.canada' 22 | }); 23 | 24 | expect(result).toBe(output); 25 | } 26 | ); 27 | 28 | test.each(nestedScenarios)( 29 | 'should correctly match nested objects', 30 | (patternObj, actual, output) => { 31 | const pattern = new EventPattern( 32 | patternObj as any, 33 | [(input) => input], 34 | (err) => { 35 | throw err; 36 | } 37 | ); 38 | 39 | const result = pattern.test(actual as any); 40 | 41 | expect(result).toBe(output); 42 | } 43 | ); 44 | }); 45 | -------------------------------------------------------------------------------- /src/tests/expressBridge.data.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | // message 1 3 | instanceTerminatedMessage: { 4 | version: '0', 5 | id: '6a7e8feb-b491-4cf7-a9f1-bf3703467718', 6 | 'detail-type': 'EC2 Instance State-change Notification', 7 | source: 'aws.ec2', 8 | account: '111122223333', 9 | time: '2017-12-22T18:43:48Z', 10 | region: 'us-west-1', 11 | resources: [ 12 | 'arn:aws:ec2:us-west-1:123456789012:instance/i-1234567890abcdef0', 13 | ], 14 | detail: { 15 | 'instance-id': ' i-1234567890abcdef0', 16 | state: 'terminated', 17 | }, 18 | }, 19 | // message 2 20 | basicMessage: { 21 | exampleValue: 'test', 22 | }, 23 | // message 3 24 | httpMessage: { 25 | body: { 26 | id: 1234, 27 | userId: 'user@example.com', 28 | price: 10.0, 29 | serviceTier: 'premium', 30 | address: '2965 S Sierra Heights, Mesa, AZ 85212', 31 | createdAt: 1000001231234, 32 | eb_event_id: '', 33 | }, 34 | method: 'POST', 35 | principalId: '', 36 | stage: 'dev', 37 | headers: { 38 | Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 39 | 'Accept-Encoding': 'gzip, deflate', 40 | 'Accept-Language': 'en-us', 41 | 'CloudFront-Forwarded-Proto': 'https', 42 | 'CloudFront-Is-Desktop-Viewer': 'true', 43 | 'CloudFront-Is-Mobile-Viewer': 'false', 44 | 'CloudFront-Is-SmartTV-Viewer': 'false', 45 | 'CloudFront-Is-Tablet-Viewer': 'false', 46 | 'CloudFront-Viewer-Country': 'US', 47 | Cookie: 48 | '__gads=ID=d51d609e5753330d:T=1443694116:S=ALNI_MbjWKzLwdEpWZ5wR5WXRI2dtjIpHw; __qca=P0-179798513-1443694132017; _ga=GA1.2.344061584.1441769647', 49 | Host: 'xxx.execute-api.us-east-1.amazonaws.com', 50 | 'User-Agent': 51 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_5) AppleWebKit/601.6.17 (KHTML, like Gecko) Version/9.1.1 Safari/601.6.17', 52 | Via: '1.1 c8a5bb0e20655459eaam174e5c41443b.cloudfront.net (CloudFront)', 53 | 'X-Amz-Cf-Id': 'z7Ds7oXaY8hgUn7lcedZjoIoxyvnzF6ycVzBdQmhn3QnOPEjJz4BrQ==', 54 | 'X-Forwarded-For': '221.24.103.21, 54.242.148.216', 55 | 'X-Forwarded-Port': '443', 56 | 'X-Forwarded-Proto': 'https', 57 | }, 58 | query: {}, 59 | path: {}, 60 | identity: { 61 | cognitoIdentityPoolId: '', 62 | accountId: '', 63 | cognitoIdentityId: '', 64 | caller: '', 65 | apiKey: '', 66 | sourceIp: '221.24.103.21', 67 | cognitoAuthenticationType: '', 68 | cognitoAuthenticationProvider: '', 69 | userArn: '', 70 | userAgent: 71 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_5) AppleWebKit/601.6.17 (KHTML, like Gecko) Version/9.1.1 Safari/601.6.17', 72 | user: '', 73 | }, 74 | stageVariables: {}, 75 | }, 76 | eventBridgeMessage: { 77 | version: '0', 78 | id: '301a585b-1fc4-00bb-f5b2-a4f887684942', 79 | 'detail-type': 'message', 80 | source: 'acme.payments', 81 | account: '018345072091', 82 | time: '2022-01-29T05:57:47Z', 83 | region: 'us-east-1', 84 | resources: [], 85 | detail: { 86 | id: 1234, 87 | userId: 'user@example.com', 88 | price: 10, 89 | serviceTier: 'premium', 90 | address: '2965 S Sierra Heights, Mesa, AZ 85212', 91 | createdAt: 1000001231234, 92 | eb_event_id: '20e1b1ba-05da-40b9-aa98-6d81eca40041', 93 | }, 94 | }, 95 | }; 96 | -------------------------------------------------------------------------------- /src/tests/expressBridge.test.ts: -------------------------------------------------------------------------------- 1 | import { ExpressBridge } from '../ExpressBridge'; 2 | import type { handlerType } from '../EventPattern'; 3 | import messages from './expressBridge.data'; 4 | 5 | jest.mock('../Telemetry', () => { 6 | return { 7 | Telemetry: jest.fn().mockImplementation(() => { 8 | return { 9 | beacon: () => Promise.resolve({}), 10 | }; 11 | }), 12 | }; 13 | }); 14 | 15 | const basePattern = { 16 | source: 'aws.*', 17 | }; 18 | 19 | describe('Test ExpressBridge', () => { 20 | test('should always run hooks when option is set', async () => { 21 | const preHook = jest.fn((event) => Promise.resolve(event)); 22 | const postHook = jest.fn((event) => Promise.resolve(event)); 23 | const expressBridge = new ExpressBridge({ alwaysRunHooks: true }); 24 | 25 | const handler = jest.fn((event) => Promise.resolve(event)); 26 | const errorHandler = jest.fn((err) => { 27 | console.log(err); 28 | throw err; 29 | }); 30 | 31 | expressBridge.pre(preHook as handlerType); 32 | expressBridge.post(postHook as handlerType); 33 | 34 | expressBridge.use(basePattern, [handler as handlerType], errorHandler); 35 | 36 | await expressBridge.process(messages.instanceTerminatedMessage); 37 | 38 | expect(preHook).toHaveBeenCalledWith(messages.instanceTerminatedMessage); 39 | expect(postHook).toHaveBeenCalledWith(messages.instanceTerminatedMessage); 40 | }); 41 | 42 | test("shouldn't run hooks when option 'always run hooks' option isn't set", async () => { 43 | const preHook = jest.fn((event) => Promise.resolve(event)); 44 | const postHook = jest.fn((event) => Promise.resolve(event)); 45 | const expressBridge = new ExpressBridge({ alwaysRunHooks: false }); 46 | 47 | const handler = jest.fn((event) => Promise.resolve(event)); 48 | const errorHandler = jest.fn((err) => { 49 | console.log(err); 50 | throw err; 51 | }); 52 | 53 | expressBridge.pre(preHook as handlerType); 54 | expressBridge.post(postHook as handlerType); 55 | 56 | expressBridge.use( 57 | messages.basicMessage, 58 | [handler as handlerType], 59 | errorHandler 60 | ); 61 | 62 | await expressBridge.process(messages.instanceTerminatedMessage); 63 | 64 | expect(preHook).toHaveBeenCalledTimes(0); 65 | expect(postHook).toHaveBeenCalledTimes(0); 66 | //expect(true).toBe(true); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /src/tests/telemetry.test.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { Telemetry } from '../Telemetry'; 3 | 4 | jest.mock('axios', () => ({ 5 | __esModule: true, 6 | default: jest.fn(() => { 7 | Promise.resolve(); 8 | }), 9 | })); 10 | 11 | describe('test telemetry functionality', () => { 12 | beforeEach(() => { 13 | jest.clearAllMocks(); 14 | }); 15 | 16 | test('should exercise telemetry functionality under correct conditions', async () => { 17 | expect(process.env.EB_TELEMETRY).toBe('true'); 18 | 19 | const telemetry = new Telemetry({ 20 | url: 'foo.com/telemetry', 21 | method: 'post', 22 | headers: { 23 | Authorization: 'bearer foo', 24 | }, 25 | serviceName: 'orders', 26 | }); 27 | 28 | const message = { 29 | name: 'Johnny Appleseed', 30 | item: 'Moleskine Notebooks', 31 | quantity: 2, 32 | price: 19.99, 33 | address: { 34 | street: '1000 Pennsylvania Ave.', 35 | city: 'Olympia', 36 | state: 'WA', 37 | zip: '98512', 38 | }, 39 | }; 40 | 41 | const tag = telemetry.tagEvent(message); 42 | telemetry.beacon('EB-TEST', message); 43 | 44 | expect(axios).toHaveBeenCalledWith( 45 | expect.objectContaining({ 46 | data: { 47 | tag: 'EB-TEST', 48 | message, 49 | eb_event_id: tag, 50 | serviceName: 'orders', 51 | }, 52 | }) 53 | ); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "esModuleInterop": true, 5 | "target": "es6", 6 | "noImplicitAny": true, 7 | "moduleResolution": "node", 8 | "sourceMap": true, 9 | "outDir": "dist", 10 | "declaration": true, 11 | "declarationDir": "dist", 12 | "baseUrl": ".", 13 | "paths": { 14 | "*": ["node_modules/*"] 15 | }, 16 | "types": ["node", "jest"], 17 | "typeRoots": ["./types", "node_modules/@types"], 18 | "lib": [ 19 | "es2020", 20 | ] 21 | }, 22 | "include": ["src/*"], 23 | "exclude": ["dist", "node_modules", "src/tests"] 24 | } 25 | --------------------------------------------------------------------------------