├── .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 |
17 |
18 |
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 |
--------------------------------------------------------------------------------