├── .gitignore ├── book-suggestion ├── .gitignore ├── .prettierrc ├── README.md ├── jest-e2e.json ├── nest-cli.json ├── nodemon-debug.json ├── nodemon.json ├── package-lock.json ├── package.json ├── serverless.yml ├── src │ ├── app.controller.spec.ts │ ├── app.controller.ts │ ├── app.module.ts │ ├── app.service.ts │ ├── main.ts │ └── suggestions │ │ ├── ibook.ts │ │ ├── suggestions.controller.spec.ts │ │ ├── suggestions.controller.ts │ │ ├── suggestions.module.ts │ │ ├── suggestions.service.spec.ts │ │ └── suggestions.service.ts ├── test │ ├── app.e2e-spec.ts │ └── jest-e2e.json ├── tsconfig.build.json ├── tsconfig.json ├── tsconfig.spec.json └── tslint.json ├── bookcategory-upload ├── .gitignore ├── README.md ├── csv │ └── book-categories.csv ├── handler.ts ├── package-lock.json ├── package.json ├── serverless.yml ├── src │ ├── category.ts │ ├── csv-record-import-service.ts │ ├── data-repository.ts │ └── processor.ts ├── tsconfig.json ├── tslint.json └── webpack.config.js ├── ddb-entity-store ├── .gitignore ├── .prettierrc ├── README.md ├── iam │ └── OnlineLibraryIAM.yml ├── jest-e2e.json ├── nest-cli.json ├── nodemon-debug.json ├── nodemon.json ├── package-lock.json ├── package.json ├── serverless.yml ├── src │ ├── app.controller.spec.ts │ ├── app.controller.ts │ ├── app.module.ts │ ├── app.service.ts │ ├── books │ │ ├── author.ts │ │ ├── authorBook.entity.ts │ │ ├── authorBook.ts │ │ ├── book.entity.ts │ │ ├── book.ts │ │ ├── books.controller.spec.ts │ │ ├── books.controller.ts │ │ ├── books.repository.spec.ts │ │ ├── books.repository.ts │ │ ├── books.service.spec.ts │ │ └── books.service.ts │ ├── main.ts │ └── utils │ │ └── ExceptionsLogFilter.ts ├── test │ ├── app.e2e-spec.ts │ └── jest-e2e.json ├── tsconfig.build.json ├── tsconfig.json ├── tsconfig.spec.json └── tslint.json ├── online-library-cloudwatch-emf ├── .gitignore ├── .prettierrc ├── README.md ├── iam │ └── OnlineLibraryIAM.yml ├── package-lock.json ├── package.json ├── serverless.yml ├── src │ ├── app.module.ts │ ├── books │ │ ├── books.controller.spec.ts │ │ ├── books.controller.ts │ │ ├── books.repository.spec.ts │ │ ├── books.repository.ts │ │ ├── books.service.spec.ts │ │ ├── books.service.ts │ │ ├── iauthor.ts │ │ └── ibook.ts │ ├── main.ts │ └── utils │ │ └── ExceptionsLogFilter.ts ├── test │ ├── app.e2e-spec.ts │ └── jest-e2e.json ├── tsconfig.build.json ├── tsconfig.json ├── tsconfig.spec.json └── tslint.json ├── online-library-java ├── .gitignore ├── build.gradle ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── src │ └── main │ │ ├── java │ │ └── com │ │ │ └── serverlesscorner │ │ │ └── onlinelibrary │ │ │ ├── ApplicationConfig.java │ │ │ ├── Author.java │ │ │ ├── Book.java │ │ │ ├── LoggingInterceptor.java │ │ │ ├── OnlineLibraryController.java │ │ │ ├── OnlineLibraryService.java │ │ │ └── StreamLambdaHandler.java │ │ └── resources │ │ ├── application.properties │ │ └── log4j2.xml └── template.yaml ├── online-library-monitor ├── .gitignore ├── README.md ├── handler.ts ├── package-lock.json ├── package.json ├── serverless.yml ├── src │ └── processor.ts ├── tsconfig.json ├── tslint.json └── webpack.config.js ├── online-library-oss ├── .gitignore ├── .prettierrc ├── README.md ├── jest-e2e.json ├── nest-cli.json ├── nodemon-debug.json ├── nodemon.json ├── package-lock.json ├── package.json ├── serverless.yml ├── src │ ├── app.controller.spec.ts │ ├── app.controller.ts │ ├── app.module.ts │ ├── app.service.ts │ └── main.ts ├── test │ ├── app.e2e-spec.ts │ └── jest-e2e.json ├── tsconfig.build.json ├── tsconfig.json ├── tsconfig.spec.json └── tslint.json ├── online-library-stream-filter ├── .gitignore ├── README.md ├── handlerAuthors.ts ├── handlerBooks.ts ├── package-lock.json ├── package.json ├── serverless.yml ├── src │ ├── export-search.service.ts │ ├── model │ │ ├── author.ts │ │ └── book.ts │ ├── processorAuthors.ts │ └── processorBooks.ts ├── tsconfig.json ├── tslint.json └── webpack.config.js ├── online-library-stream ├── .gitignore ├── README.md ├── handler.ts ├── package-lock.json ├── package.json ├── serverless.yml ├── src │ ├── export-search.service.ts │ ├── model │ │ └── book.ts │ └── processor.ts ├── tsconfig.json ├── tslint.json └── webpack.config.js ├── online-library ├── .gitignore ├── .prettierrc ├── README.md ├── iam │ └── OnlineLibraryIAM.yml ├── jest-e2e.json ├── nest-cli.json ├── nodemon-debug.json ├── nodemon.json ├── online-library.iml ├── package-lock.json ├── package.json ├── serverless.yml ├── src │ ├── app.controller.spec.ts │ ├── app.controller.ts │ ├── app.module.ts │ ├── app.service.ts │ ├── books │ │ ├── books.controller.spec.ts │ │ ├── books.controller.ts │ │ ├── books.repository.spec.ts │ │ ├── books.repository.ts │ │ ├── books.service.spec.ts │ │ ├── books.service.ts │ │ ├── iauthor.ts │ │ └── ibook.ts │ ├── main.ts │ └── utils │ │ └── ExceptionsLogFilter.ts ├── test │ ├── app.e2e-spec.ts │ └── jest-e2e.json ├── tsconfig.build.json ├── tsconfig.json ├── tsconfig.spec.json └── tslint.json └── quotes-restapi-service ├── .gitignore ├── .prettierrc ├── README.md ├── jest-e2e.json ├── nest-cli.json ├── nodemon-debug.json ├── nodemon.json ├── package-lock.json ├── package.json ├── serverless.yml ├── src ├── app.controller.spec.ts ├── app.controller.ts ├── app.module.ts ├── app.service.ts ├── main.ts └── quotes │ ├── quotes.controller.spec.ts │ ├── quotes.controller.ts │ ├── quotes.model.spec.ts │ ├── quotes.model.ts │ ├── quotes.service.spec.ts │ └── quotes.service.ts ├── test ├── app.e2e-spec.ts └── jest-e2e.json ├── tsconfig.build.json ├── tsconfig.json ├── tsconfig.spec.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | ddb-entity-store/online-library.iml 3 | -------------------------------------------------------------------------------- /book-suggestion/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /.build/ 3 | /.serverless/ 4 | /_warmup/ 5 | /.idea/ 6 | -------------------------------------------------------------------------------- /book-suggestion/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } 5 | -------------------------------------------------------------------------------- /book-suggestion/README.md: -------------------------------------------------------------------------------- 1 | 9 | # Serverless NestJS REST API template 10 | 11 | This serverless template enables you to run a NestJS REST API with AWS Lambda using Serverless and TypeScript. 12 | 13 | ## Use Cases 14 | - REST API. 15 | 16 | ## Setup 17 | 1. Install serverless - ```npm install -g serverless``` 18 | 2. Use serverless to create a project with template-url. For example 19 | ```serverless create --template-url https://github.com/cyberworkz/serverless-templates/tree/main/aws-nodejs-typescript-restapi-nest --path myService``` 20 | 21 | 22 | ## Examples 23 | See the following article for a step-to-step guide how to use this template https://medium.com/dev-genius/how-to-build-a-serverless-rest-api-with-nestjs-and-dynamodb-7b58b5b59bf6 24 | 25 | 26 | -------------------------------------------------------------------------------- /book-suggestion/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /book-suggestion/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "ts", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src" 5 | } 6 | -------------------------------------------------------------------------------- /book-suggestion/nodemon-debug.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src"], 3 | "ext": "ts", 4 | "ignore": ["src/**/*.spec.ts"], 5 | "exec": "node --inspect-brk -r ts-node/register -r tsconfig-paths/register src/main.ts" 6 | } 7 | -------------------------------------------------------------------------------- /book-suggestion/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src"], 3 | "ext": "ts", 4 | "ignore": ["src/**/*.spec.ts"], 5 | "exec": "ts-node -r tsconfig-paths/register src/main.ts" 6 | } 7 | -------------------------------------------------------------------------------- /book-suggestion/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "book-suggestion", 3 | "version": "1.0.0", 4 | "description": "Serverless REST API service", 5 | "author": "cyberworkz", 6 | "scripts": { 7 | "build": "tsc -p tsconfig.build.json", 8 | "format": "prettier --write \"src/**/*.ts\"", 9 | "start": "sls offline start", 10 | "lint": "tslint -p tsconfig.json -c tslint.json", 11 | "test": "jest", 12 | "test:watch": "jest --watch", 13 | "test:cov": "jest --coverage", 14 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 15 | "test:e2e": "jest --config ./test/jest-e2e.json", 16 | "deploy": "sls deploy --stage dev" 17 | }, 18 | "dependencies": { 19 | "@nestjs/common": "^8.2.1", 20 | "@nestjs/core": "^8.2.1", 21 | "@nestjs/platform-express": "^8.2.1", 22 | "aws-sdk": "^2.789.0", 23 | "aws-serverless-express": "^3.3.5", 24 | "reflect-metadata": "^0.1.12", 25 | "rimraf": "^2.6.2", 26 | "rxjs": "^6.2.2" 27 | }, 28 | "devDependencies": { 29 | "@nestjs/testing": "^5.1.0", 30 | "@types/aws-lambda": "^8.10.15", 31 | "@types/aws-serverless-express": "^3.3.5", 32 | "@types/express": "^4.16.0", 33 | "@types/jest": "^23.3.1", 34 | "@types/node": "^10.7.1", 35 | "@types/supertest": "^2.0.5", 36 | "acorn": "^8.0.5", 37 | "jest": "^23.5.0", 38 | "nodemon": "^1.18.3", 39 | "prettier": "^1.14.2", 40 | "serverless-offline": "^6.8.0", 41 | "serverless-plugin-optimize": "^4.0.2-rc.1", 42 | "serverless-plugin-typescript": "1.1.7", 43 | "serverless-stage-manager": "^1.0.5", 44 | "supertest": "^3.1.0", 45 | "ts-jest": "^23.1.3", 46 | "ts-loader": "^4.4.2", 47 | "ts-node": "^7.0.1", 48 | "tsconfig-paths": "^3.5.0", 49 | "tslint": "5.11.0", 50 | "typescript": "^4.1.5" 51 | }, 52 | "jest": { 53 | "moduleFileExtensions": [ 54 | "js", 55 | "json", 56 | "ts" 57 | ], 58 | "rootDir": "src", 59 | "testRegex": ".spec.ts$", 60 | "transform": { 61 | "^.+\\.(t|j)s$": "ts-jest" 62 | }, 63 | "coverageDirectory": "../coverage", 64 | "testEnvironment": "node" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /book-suggestion/serverless.yml: -------------------------------------------------------------------------------- 1 | service: book-suggestion 2 | 3 | plugins: 4 | - serverless-plugin-typescript 5 | - serverless-plugin-optimize 6 | - serverless-offline 7 | - serverless-stage-manager 8 | provider: 9 | name: aws 10 | tracing: 11 | apiGateway: true 12 | runtime: nodejs14.x 13 | memorySize: 128 # optional, in MB, default is 1024 14 | timeout: 10 # optional, in seconds, default is 6 15 | stage: ${opt:stage, 'dev'} 16 | region: eu-west-1 17 | 18 | 19 | package: 20 | exclude: 21 | - .gitignore 22 | - README.md 23 | - serverless.yml 24 | - nest-cli.json 25 | - .prettierrc 26 | excludeDevDependencies: true 27 | individually: true 28 | 29 | functions: 30 | main: 31 | handler: src/main.handler 32 | events: 33 | - http: 34 | method: any 35 | path: /{proxy+} 36 | cors: true 37 | 38 | custom: 39 | stages: 40 | - dev 41 | - test 42 | - acc 43 | - prod 44 | 45 | 46 | -------------------------------------------------------------------------------- /book-suggestion/src/app.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | 5 | describe('AppController', () => { 6 | let app: TestingModule; 7 | 8 | beforeAll(async () => { 9 | app = await Test.createTestingModule({ 10 | controllers: [AppController], 11 | providers: [AppService], 12 | }).compile(); 13 | }); 14 | 15 | describe('root', () => { 16 | it('should return "Hello World!"', () => { 17 | const appController = app.get(AppController); 18 | expect(appController.getHello()).toBe('Hello World!'); 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /book-suggestion/src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { AppService } from './app.service'; 3 | 4 | @Controller() 5 | export class AppController { 6 | constructor(private readonly appService: AppService) {} 7 | 8 | @Get('hello') 9 | getHello(): string { 10 | return this.appService.getHello(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /book-suggestion/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | import { SuggestionsModule } from './suggestions/suggestions.module'; 5 | 6 | @Module({ 7 | imports: [SuggestionsModule], 8 | controllers: [AppController], 9 | providers: [AppService], 10 | }) 11 | export class AppModule {} 12 | -------------------------------------------------------------------------------- /book-suggestion/src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService { 5 | getHello(): string { 6 | return 'Hello World!'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /book-suggestion/src/main.ts: -------------------------------------------------------------------------------- 1 | import { Handler, Context } from 'aws-lambda'; 2 | import { Server } from 'http'; 3 | import { createServer, proxy } from 'aws-serverless-express'; 4 | import { eventContext } from 'aws-serverless-express/middleware'; 5 | 6 | import { ExpressAdapter } from '@nestjs/platform-express'; 7 | import { NestFactory } from '@nestjs/core'; 8 | import { AppModule } from './app.module'; 9 | 10 | import * as express from 'express'; 11 | 12 | // NOTE: If you get ERR_CONTENT_DECODING_FAILED in your browser, this is likely 13 | // due to a compressed response (e.g. gzip) which has not been handled correctly 14 | // by aws-serverless-express and/or API Gateway. Add the necessary MIME types to 15 | // binaryMimeTypes below 16 | const binaryMimeTypes: string[] = []; 17 | 18 | let cachedServer: Server; 19 | 20 | process.on('unhandledRejection', (reason) => { 21 | // tslint:disable-next-line:no-console 22 | console.error(reason); 23 | }); 24 | 25 | process.on('uncaughtException', (reason) => { 26 | // tslint:disable-next-line:no-console 27 | console.error(reason); 28 | }); 29 | 30 | async function bootstrapServer(): Promise { 31 | if (!cachedServer) { 32 | try { 33 | const expressApp = express(); 34 | const nestApp = await NestFactory.create(AppModule, new ExpressAdapter(expressApp)); 35 | nestApp.enableCors(); 36 | nestApp.use(eventContext()); 37 | await nestApp.init(); 38 | cachedServer = createServer(expressApp, undefined, binaryMimeTypes); 39 | } 40 | catch (error) { 41 | return Promise.reject(error); 42 | } 43 | } 44 | return Promise.resolve(cachedServer); 45 | } 46 | 47 | export const handler: Handler = async (event: any, context: Context) => { 48 | cachedServer = await bootstrapServer(); 49 | return proxy(cachedServer, event, context, 'PROMISE').promise; 50 | } 51 | -------------------------------------------------------------------------------- /book-suggestion/src/suggestions/ibook.ts: -------------------------------------------------------------------------------- 1 | export interface IBook { 2 | 3 | isbn: number; 4 | title: string; 5 | author: string; 6 | } 7 | -------------------------------------------------------------------------------- /book-suggestion/src/suggestions/suggestions.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { SuggestionsController } from './suggestions.controller'; 3 | 4 | describe('SuggestionsController', () => { 5 | let controller: SuggestionsController; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | controllers: [SuggestionsController], 10 | }).compile(); 11 | 12 | controller = module.get(SuggestionsController); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(controller).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /book-suggestion/src/suggestions/suggestions.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, HttpStatus, Param, Res } from '@nestjs/common'; 2 | import { IBook } from './ibook'; 3 | import { SuggestionsService } from './suggestions.service'; 4 | 5 | @Controller('suggestions') 6 | export class SuggestionsController { 7 | 8 | constructor(private suggestionService: SuggestionsService){} 9 | 10 | @Get("/:isbn") 11 | async getSuggestionsByISBN(@Param('isbn') isbn:number, @Res() res: any){ 12 | const suggestions: IBook[] = await this.suggestionService.getSuggestions(isbn); 13 | return res.status(HttpStatus.OK).json(suggestions); 14 | } 15 | 16 | } 17 | 18 | -------------------------------------------------------------------------------- /book-suggestion/src/suggestions/suggestions.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { SuggestionsController } from './suggestions.controller'; 3 | import { SuggestionsService } from './suggestions.service'; 4 | 5 | @Module({ 6 | controllers: [SuggestionsController], 7 | providers: [SuggestionsService] 8 | }) 9 | export class SuggestionsModule {} 10 | -------------------------------------------------------------------------------- /book-suggestion/src/suggestions/suggestions.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { SuggestionsService } from './suggestions.service'; 3 | 4 | describe('SuggestionsService', () => { 5 | let service: SuggestionsService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [SuggestionsService], 10 | }).compile(); 11 | 12 | service = module.get(SuggestionsService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /book-suggestion/src/suggestions/suggestions.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { IBook } from './ibook'; 3 | 4 | @Injectable() 5 | export class SuggestionsService { 6 | getSuggestions(isbn: number): IBook[] { 7 | const books:IBook[] = []; 8 | 9 | books.push({ 10 | title: 'Harry Potter - The Creature Vault', 11 | isbn: 123456, 12 | author: 'Jody Revenson' 13 | }); 14 | 15 | books.push({ 16 | title: 'The Silmarillion', 17 | isbn: 345216, 18 | author: 'J.R.R. Tolkien' 19 | }); 20 | 21 | books.push({ 22 | title: 'The Fall of Gondolin', 23 | isbn: 234516, 24 | author: 'J.R.R. Tolkien' 25 | }); 26 | 27 | return books; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /book-suggestion/test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { INestApplication } from '@nestjs/common'; 2 | import { Test } from '@nestjs/testing'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeAll(async () => { 10 | const moduleFixture = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/hello (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/hello') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /book-suggestion/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /book-suggestion/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src/**/*"], 4 | "exclude": ["node_modules", "**/*.spec.ts"] 5 | } 6 | -------------------------------------------------------------------------------- /book-suggestion/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "noImplicitAny": false, 6 | "removeComments": true, 7 | "noLib": false, 8 | "allowSyntheticDefaultImports": true, 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "target": "es6", 12 | "sourceMap": true, 13 | "outDir": "./dist", 14 | "baseUrl": "./" 15 | }, 16 | "exclude": ["node_modules"] 17 | } 18 | -------------------------------------------------------------------------------- /book-suggestion/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "types": ["jest", "node"] 5 | }, 6 | "include": ["**/*.spec.ts", "**/*.d.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /book-suggestion/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": ["tslint:recommended"], 4 | "jsRules": { 5 | "no-unused-expression": true 6 | }, 7 | "rules": { 8 | "quotemark": [true, "single"], 9 | "member-access": [false], 10 | "ordered-imports": [false], 11 | "max-line-length": [true, 150], 12 | "member-ordering": [false], 13 | "interface-name": [false], 14 | "arrow-parens": false, 15 | "object-literal-sort-keys": false 16 | }, 17 | "rulesDirectory": [] 18 | } 19 | -------------------------------------------------------------------------------- /bookcategory-upload/.gitignore: -------------------------------------------------------------------------------- 1 | # package directories 2 | node_modules 3 | jspm_packages 4 | 5 | # Serverless directories 6 | .serverless 7 | 8 | # Webpack directories 9 | .webpack 10 | /email-service.iml 11 | /.idea/ 12 | -------------------------------------------------------------------------------- /bookcategory-upload/README.md: -------------------------------------------------------------------------------- 1 | 9 | # Serverless S3 CSV Upload 10 | 11 | This serverless example demonstrates how to handle S3 events with AWS Lambda using Serverless and TypeScript. 12 | 13 | ## Use Case 14 | - import CSV files into DynamoDB 15 | 16 | ## Article 17 | See https://blog.devgenius.io/s3-trigger-serverless-csv-upload-into-dynamodb-8877c770fb32 for the story belonging to the code. 18 | 19 | -------------------------------------------------------------------------------- /bookcategory-upload/csv/book-categories.csv: -------------------------------------------------------------------------------- 1 | PK;SK;name;code 2 | CAT#001;CAT#FANT;Fantasy;FANT 3 | CAT#001;CAT#SCI;Sci-Fi;SCI 4 | CAT#001;CAT#THR;Thriller;THR 5 | CAT#001;CAT#ROM;Romance;ROM 6 | CAT#001;CAT#SELF;Self-Help;SELF 7 | CAT#001;CAT#HIST;History;HIST -------------------------------------------------------------------------------- /bookcategory-upload/handler.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; // for TypeDI 2 | 3 | export { default as processor} from './src/processor'; 4 | -------------------------------------------------------------------------------- /bookcategory-upload/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bookcategory-upload", 3 | "version": "0.0.0", 4 | "description": "Serverless example AWS S3 event with TypeScript", 5 | "main": "handler.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "lint": "tslint -p tsconfig.json -c tslint.json", 9 | "deploy-dev": "sls deploy --stage dev" 10 | }, 11 | "dependencies": { 12 | "@types/lambda-log": "^2.2.0", 13 | "aws-lambda": "^0.1.2", 14 | "aws-sdk": "^2.859.0", 15 | "lambda": "^0.10.3", 16 | "log": "^6.0.0", 17 | "source-map-support": "^0.5.10", 18 | "@fast-csv/parse": "^4.3.6", 19 | "typedi": "^0.10.0" 20 | }, 21 | "devDependencies": { 22 | "@types/aws-lambda": "^8.10.72", 23 | "@types/node": "^10.17.55", 24 | "eslint": "^6.6.0", 25 | "serverless-webpack": "^5.2.0", 26 | "ts-loader": "^5.3.3", 27 | "tslint": "^5.20.0", 28 | "tslint-config-airbnb": "^5.11.2", 29 | "typescript": "^3.9.9", 30 | "webpack": "^4.46.0" 31 | }, 32 | "author": "Haiko van der Schaaf (https://github.com/cyberworkz)", 33 | "license": "MIT" 34 | } 35 | -------------------------------------------------------------------------------- /bookcategory-upload/serverless.yml: -------------------------------------------------------------------------------- 1 | service: bookcategory-upload 2 | 3 | plugins: 4 | - serverless-webpack 5 | 6 | provider: 7 | name: aws 8 | runtime: nodejs14.x 9 | environment: 10 | DB_NAME: ${self:custom.DB_NAME.dev} 11 | memorySize: 128 # optional, in MB, default is 1024 12 | timeout: 10 # optional, in seconds, default is 6 13 | stage: ${opt:stage, 'dev'} 14 | region: eu-west-1 15 | iamRoleStatements: 16 | - Effect: Allow 17 | Action: 18 | - s3:* 19 | Resource: "*" 20 | - Effect: Allow 21 | Action: 22 | - dynamodb:PutItem 23 | Resource: 24 | - arn:aws:dynamodb:eu-west-1::table/online-library 25 | 26 | 27 | functions: 28 | bookCategoryS3EventProcessor: 29 | name: bookCategoryImporter 30 | handler: handler.processor 31 | events: 32 | - s3: 33 | bucket: ${self:custom.bucket} 34 | event: s3:ObjectCreated:* 35 | rules: 36 | - suffix: .csv 37 | 38 | custom: 39 | bucket: bookcategories 40 | stages: 41 | - dev 42 | DB_NAME: 43 | dev: online-library 44 | 45 | -------------------------------------------------------------------------------- /bookcategory-upload/src/category.ts: -------------------------------------------------------------------------------- 1 | export class Category { 2 | PK: string; 3 | SK: string; 4 | code: string; 5 | name: string; 6 | } -------------------------------------------------------------------------------- /bookcategory-upload/src/csv-record-import-service.ts: -------------------------------------------------------------------------------- 1 | import { Service } from 'typedi'; 2 | import { DataRepository } from './data-repository'; 3 | import * as stream from 'stream'; 4 | import * as csv from '@fast-csv/parse'; 5 | 6 | @Service() 7 | export class CsvRecordImportService { 8 | 9 | constructor(private dataRepo: DataRepository) { 10 | } 11 | 12 | /** 13 | * Process stream of csv file. 14 | * 15 | * @param csvStream 16 | */ 17 | async import(csvStream: stream.Readable) { 18 | return new Promise((resolve, reject) => { 19 | const parsedData = []; 20 | csvStream.pipe(csv.parse({ headers: true, delimiter: ';' })) 21 | .on('error', function (data) { 22 | console.error(`Got an error: ${data}`); 23 | reject("Error parsing \n" + data); 24 | }) 25 | .on('data', (data) => { 26 | parsedData.push(data); 27 | }) 28 | .on('end', async () => { 29 | if (parsedData.length > 0) { 30 | await this.dataRepo.upload(parsedData); 31 | } else { 32 | console.log('No parsed data to upload'); 33 | } 34 | resolve('done importing'); 35 | }); 36 | }); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /bookcategory-upload/src/data-repository.ts: -------------------------------------------------------------------------------- 1 | import { Service } from 'typedi'; 2 | import { Category } from './category'; 3 | import { DocumentClient } from 'aws-sdk/clients/dynamodb'; 4 | import * as AWS from 'aws-sdk'; 5 | 6 | @Service() 7 | export class DataRepository { 8 | 9 | protected tableName: string; 10 | protected db: DocumentClient; 11 | 12 | constructor() { 13 | this.tableName = process.env.DB_NAME; 14 | this.db = new AWS.DynamoDB.DocumentClient(); 15 | } 16 | 17 | async upload(parsedData: any[]) { 18 | console.log('uploading data'); 19 | 20 | for (const item of parsedData) { 21 | if (item.PK.includes('CAT#')) { 22 | await this.handleCategoryItem(item); 23 | } 24 | } 25 | } 26 | 27 | async handleCategoryItem(item: any) { 28 | let category = this.getCategory(item); 29 | try { 30 | await this.db 31 | .put({ 32 | TableName: this.tableName, 33 | Item: category, 34 | ConditionExpression: 'attribute_not_exists(#code)', 35 | ExpressionAttributeNames: { 36 | '#code': 'code', 37 | }, 38 | }, function (err, data) { 39 | if (err) { 40 | console.log('Error adding item to database: ', err); 41 | } 42 | else { 43 | console.log(data); 44 | } 45 | }).promise(); 46 | } catch (error) { 47 | if (error.code === 'ConditionalCheckFailedException') { 48 | console.log('insert cancelled for category with code ' + item.code, error); 49 | } 50 | } 51 | } 52 | 53 | getCategory(item: any) { 54 | let category = new Category(); 55 | category.PK = item.PK; 56 | category.SK = item.SK; 57 | category.code = item.code; 58 | category.name = item.name; 59 | 60 | console.log(category); 61 | return category; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /bookcategory-upload/src/processor.ts: -------------------------------------------------------------------------------- 1 | import { S3Event, S3Handler } from 'aws-lambda'; 2 | 3 | import S3 from 'aws-sdk/clients/s3'; 4 | import { Container } from 'typedi'; 5 | import { CsvRecordImportService } from './csv-record-import-service'; 6 | 7 | const processor: S3Handler = async (event: S3Event) => { 8 | // tslint:disable-next-line:prefer-template 9 | console.log('received event on ' + new Date().toDateString()); 10 | 11 | const csvImportService = Container.get(CsvRecordImportService); 12 | 13 | for (const record of event.Records) { 14 | const key: string = record.s3.object.key; 15 | console.log(' key--> ', key); 16 | console.log(record); 17 | 18 | const s3: S3 = new S3({ apiVersion: '2006-03-01', region: 'eu-west-1' }); 19 | 20 | await s3.getObject({ Bucket: record.s3.bucket.name, Key: record.s3.object.key }).promise().then((data) => { 21 | console.log(data.Body.toString()); 22 | }, 23 | ); 24 | 25 | const stream = await s3.getObject({ 26 | Bucket: record.s3.bucket.name, 27 | Key: record.s3.object.key, 28 | }).createReadStream(); 29 | await csvImportService.import(stream); 30 | } 31 | }; 32 | 33 | export default processor; 34 | -------------------------------------------------------------------------------- /bookcategory-upload/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "esModuleInterop": true, 5 | "lib": ["esnext"], 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "noUnusedLocals": true, 9 | "noUnusedParameters": true, 10 | "outDir": "lib", 11 | "sourceMap": true, 12 | "target": "esnext", 13 | "experimentalDecorators": true, 14 | "emitDecoratorMetadata": true 15 | }, 16 | "exclude": ["node_modules"] 17 | } 18 | -------------------------------------------------------------------------------- /bookcategory-upload/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint-config-airbnb"] 3 | } 4 | -------------------------------------------------------------------------------- /bookcategory-upload/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const slsw = require('serverless-webpack'); 3 | 4 | module.exports = { 5 | mode: slsw.lib.webpack.isLocal ? 'development' : 'production', 6 | entry: slsw.lib.entries, 7 | devtool: 'source-map', 8 | resolve: { 9 | extensions: ['.js', '.jsx', '.json', '.ts', '.tsx'], 10 | }, 11 | output: { 12 | libraryTarget: 'commonjs', 13 | path: path.join(__dirname, '.webpack'), 14 | filename: '[name].js', 15 | }, 16 | target: 'node', 17 | module: { 18 | rules: [ 19 | // all files with a `.ts` or `.tsx` extension will be handled by `ts-loader` 20 | { test: /\.tsx?$/, loader: 'ts-loader' }, 21 | ], 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /ddb-entity-store/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /.build/ 3 | /.serverless/ 4 | /_warmup/ 5 | /.idea/ 6 | -------------------------------------------------------------------------------- /ddb-entity-store/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } 5 | -------------------------------------------------------------------------------- /ddb-entity-store/README.md: -------------------------------------------------------------------------------- 1 | 9 | # Serverless NestJS REST API + DynamoDB example 10 | This example demonstrates you how to run a NestJS REST API with AWS Lambda using Serverless and TypeScript with DynamoDB as datastore. See the story on [Medium](https://serverlesscorner.com/how-to-build-a-serverless-rest-api-with-nestjs-and-dynamodb-7b58b5b59bf6) for more explanation. 11 | 12 | ## Use Cases 13 | - REST API + DynamDB 14 | -------------------------------------------------------------------------------- /ddb-entity-store/iam/OnlineLibraryIAM.yml: -------------------------------------------------------------------------------- 1 | OnlineLibraryService: 2 | - Effect: Allow 3 | Action: 4 | - dynamodb:PutItem 5 | - dynamodb:Scan 6 | - dynamodb:GetItem 7 | - dynamodb:UpdateItem 8 | - dynamodb:Query 9 | - dynamodb:ConditionCheckItem 10 | Resource: 11 | - arn:aws:dynamodb:eu-west-1:184690513648:table/online-library -------------------------------------------------------------------------------- /ddb-entity-store/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /ddb-entity-store/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "ts", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src" 5 | } 6 | -------------------------------------------------------------------------------- /ddb-entity-store/nodemon-debug.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src"], 3 | "ext": "ts", 4 | "ignore": ["src/**/*.spec.ts"], 5 | "exec": "node --inspect-brk -r ts-node/register -r tsconfig-paths/register src/main.ts" 6 | } 7 | -------------------------------------------------------------------------------- /ddb-entity-store/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src"], 3 | "ext": "ts", 4 | "ignore": ["src/**/*.spec.ts"], 5 | "exec": "ts-node -r tsconfig-paths/register src/main.ts" 6 | } 7 | -------------------------------------------------------------------------------- /ddb-entity-store/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ddb-entity-store", 3 | "version": "1.0.0", 4 | "description": "Online library service with DynamoDB Entity Store", 5 | "author": "cyberworkz", 6 | "scripts": { 7 | "build": "tsc -p tsconfig.build.json", 8 | "format": "prettier --write \"src/**/*.ts\"", 9 | "start": "sls offline start", 10 | "lint": "tslint -p tsconfig.json -c tslint.json", 11 | "test": "jest", 12 | "test:watch": "jest --watch", 13 | "test:cov": "jest --coverage", 14 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 15 | "test:e2e": "jest --config ./test/jest-e2e.json", 16 | "deploy": "sls deploy --stage dev" 17 | }, 18 | "dependencies": { 19 | "@nestjs/common": "^10.2.7", 20 | "@nestjs/core": "^10.2.7", 21 | "@nestjs/platform-express": "^10.2.7", 22 | "@symphoniacloud/dynamodb-entity-store": "^1.0.0", 23 | "@vendia/serverless-express": "^4.10.4", 24 | "reflect-metadata": "^0.1.12", 25 | "rimraf": "^2.6.2", 26 | "rxjs": "^7.8.1" 27 | }, 28 | "devDependencies": { 29 | "@nestjs/testing": "^10.2.7", 30 | "@types/aws-lambda": "^8.10.15", 31 | "@types/aws-serverless-express": "^3.3.5", 32 | "@types/express": "^4.16.0", 33 | "@types/jest": "^23.3.1", 34 | "@types/node": "^10.7.1", 35 | "@types/supertest": "^2.0.5", 36 | "acorn": "^8.0.5", 37 | "jest": "^23.5.0", 38 | "nodemon": "^1.18.3", 39 | "prettier": "^1.14.2", 40 | "serverless-offline": "^13.2.1", 41 | "serverless-plugin-typescript": "2.1.5", 42 | "serverless-stage-manager": "^1.0.5", 43 | "supertest": "^3.1.0", 44 | "ts-jest": "^23.1.3", 45 | "ts-loader": "^4.4.2", 46 | "ts-node": "^7.0.1", 47 | "tsconfig-paths": "^3.5.0", 48 | "tslint": "5.11.0", 49 | "typescript": "^4.1.5" 50 | }, 51 | "jest": { 52 | "moduleFileExtensions": [ 53 | "js", 54 | "json", 55 | "ts" 56 | ], 57 | "rootDir": "src", 58 | "testRegex": ".spec.ts$", 59 | "transform": { 60 | "^.+\\.(t|j)s$": "ts-jest" 61 | }, 62 | "coverageDirectory": "../coverage", 63 | "testEnvironment": "node" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /ddb-entity-store/serverless.yml: -------------------------------------------------------------------------------- 1 | service: online-library 2 | 3 | plugins: 4 | - serverless-plugin-typescript 5 | - serverless-offline 6 | - serverless-stage-manager 7 | provider: 8 | name: aws 9 | tracing: 10 | apiGateway: true 11 | runtime: nodejs18.x 12 | stage: ${opt:stage, 'dev'} 13 | region: eu-west-1 14 | iamRoleStatements: 15 | ${file(iam/OnlineLibraryIAM.yml):OnlineLibraryService} 16 | 17 | 18 | 19 | package: 20 | exclude: 21 | - .gitignore 22 | - README.md 23 | - serverless.yml 24 | - nest-cli.json 25 | - .prettierrc 26 | excludeDevDependencies: true 27 | individually: true 28 | 29 | functions: 30 | main: 31 | handler: src/main.handler 32 | events: 33 | - http: 34 | method: any 35 | path: /{proxy+} 36 | cors: true 37 | 38 | custom: 39 | stages: 40 | - dev 41 | - test 42 | - acc 43 | - prod 44 | -------------------------------------------------------------------------------- /ddb-entity-store/src/app.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | 5 | describe('AppController', () => { 6 | let app: TestingModule; 7 | 8 | beforeAll(async () => { 9 | app = await Test.createTestingModule({ 10 | controllers: [AppController], 11 | providers: [AppService], 12 | }).compile(); 13 | }); 14 | 15 | describe('root', () => { 16 | it('should return "Hello World!"', () => { 17 | const appController = app.get(AppController); 18 | expect(appController.getHello()).toBe('Hello World!'); 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /ddb-entity-store/src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { AppService } from './app.service'; 3 | 4 | @Controller() 5 | export class AppController { 6 | constructor(private readonly appService: AppService) {} 7 | 8 | @Get('hello') 9 | getHello(): string { 10 | return this.appService.getHello(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /ddb-entity-store/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | import { BooksController } from './books/books.controller'; 5 | import { BooksRepository } from './books/books.repository'; 6 | import { BooksService } from './books/books.service'; 7 | import { ExceptionsLoggerFilter } from './utils/ExceptionsLogFilter'; 8 | import { APP_FILTER } from '@nestjs/core'; 9 | 10 | @Module({ 11 | imports: [], 12 | controllers: [AppController, BooksController], 13 | providers: [AppService, BooksService, BooksRepository, 14 | { provide: APP_FILTER, 15 | useClass: ExceptionsLoggerFilter, 16 | } 17 | ], 18 | }) 19 | export class AppModule {} 20 | -------------------------------------------------------------------------------- /ddb-entity-store/src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService { 5 | getHello(): string { 6 | return 'Hello World!'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /ddb-entity-store/src/books/author.ts: -------------------------------------------------------------------------------- 1 | export interface Author { 2 | firstName: string; 3 | lastName: string; 4 | } 5 | -------------------------------------------------------------------------------- /ddb-entity-store/src/books/authorBook.entity.ts: -------------------------------------------------------------------------------- 1 | import { DynamoDBValues, createEntity } from '@symphoniacloud/dynamodb-entity-store'; 2 | import { Book } from './book'; 3 | import {Author} from './author'; 4 | import {AuthorBook} from './authorBook'; 5 | 6 | const isAuthorBook = (x: DynamoDBValues): x is AuthorBook => { 7 | const candidate = x as AuthorBook; 8 | return candidate.Title !== undefined 9 | && candidate.firstName !== undefined 10 | && candidate.lastName !== undefined 11 | && candidate.isbn !== undefined; 12 | }; 13 | 14 | export const AUTHOR_BOOK_ENTITY = createEntity( 15 | 'AUTHOR_BOOK', 16 | isAuthorBook, 17 | ({firstName, lastName}: Pick) => 'AUTH#' + lastName.toUpperCase() + '_' + firstName.toUpperCase(), 18 | ({isbn}: Pick) => 'BOOK#' + isbn, 19 | ); 20 | -------------------------------------------------------------------------------- /ddb-entity-store/src/books/authorBook.ts: -------------------------------------------------------------------------------- 1 | export interface AuthorBook { 2 | firstName: string; 3 | lastName: string; 4 | Title: string; 5 | isbn: number; 6 | } 7 | -------------------------------------------------------------------------------- /ddb-entity-store/src/books/book.entity.ts: -------------------------------------------------------------------------------- 1 | import { DynamoDBValues, createEntity } from '@symphoniacloud/dynamodb-entity-store'; 2 | import { Book } from './book'; 3 | 4 | const isBook = (x: DynamoDBValues): x is Book => { 5 | const candidate = x as Book; 6 | return candidate.Author !== undefined && candidate.Title !== undefined; 7 | }; 8 | 9 | export const BOOK_ENTITY = createEntity( 10 | 'BOOK', 11 | isBook, 12 | ({isbn}: Pick) => 'BOOK#' + isbn, 13 | ({isbn}: Pick) => 'BOOK#' + isbn, 14 | ); 15 | -------------------------------------------------------------------------------- /ddb-entity-store/src/books/book.ts: -------------------------------------------------------------------------------- 1 | import {Author} from './author'; 2 | 3 | export interface Book { 4 | isbn: number; 5 | Title: string; 6 | Author: string; 7 | category: string; 8 | lend: boolean; 9 | lendDate: Date; 10 | returnDate: Date; 11 | TYPE: string; 12 | } 13 | -------------------------------------------------------------------------------- /ddb-entity-store/src/books/books.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { BooksController } from './books.controller'; 3 | 4 | describe('BooksController', () => { 5 | let controller: BooksController; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | controllers: [BooksController], 10 | }).compile(); 11 | 12 | controller = module.get(BooksController); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(controller).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /ddb-entity-store/src/books/books.controller.ts: -------------------------------------------------------------------------------- 1 | import {Body, Controller, Get, HttpStatus, Param, Post, Res} from '@nestjs/common'; 2 | import {BooksService} from './books.service'; 3 | import { Book } from './book'; 4 | 5 | @Controller('books') 6 | export class BooksController { 7 | 8 | constructor(private bookService: BooksService) {} 9 | 10 | @Get('/:isbn') 11 | async getBookByISBN(@Param('isbn') isbn: number, @Res() res: any) { 12 | const book: object = await this.bookService.getBook(isbn); 13 | return res.status(HttpStatus.OK).json(book); 14 | } 15 | 16 | @Get('/author/:lastName/:firstName') 17 | async getBooksByAuthor(@Param('lastName')lastName: string, @Param('firstName')firstName: string, @Res() res: any) { 18 | const books: any[] = await this.bookService.getAuthorBooks(lastName, firstName); 19 | return res.status(HttpStatus.OK).json(books); 20 | } 21 | 22 | @Post() 23 | async addBook(@Body() newBook: Book, @Res() res: any) { 24 | let book = this.bookService.addBookToLibrary(newBook); 25 | return res.status(HttpStatus.CREATED).json(book); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /ddb-entity-store/src/books/books.repository.spec.ts: -------------------------------------------------------------------------------- 1 | import { BooksRepository } from './books.repository'; 2 | 3 | describe('BooksRepository', () => { 4 | it('should be defined', () => { 5 | expect(new BooksRepository()).toBeDefined(); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /ddb-entity-store/src/books/books.repository.ts: -------------------------------------------------------------------------------- 1 | import {Injectable, InternalServerErrorException, NotFoundException} from '@nestjs/common'; 2 | import { 3 | TableConfig, 4 | AllEntitiesStore, 5 | SingleEntityOperations, 6 | createStandardSingleTableConfig, 7 | createStore, 8 | rangeWhereSkBeginsWith, consoleLogger, createStoreContext 9 | } from '@symphoniacloud/dynamodb-entity-store'; 10 | import {BOOK_ENTITY} from './book.entity'; 11 | import {Book} from './book'; 12 | import {AuthorBook} from './authorBook'; 13 | import {AUTHOR_BOOK_ENTITY} from './authorBook.entity'; 14 | 15 | @Injectable() 16 | export class BooksRepository { 17 | 18 | private readonly tableName: string; 19 | private store: AllEntitiesStore; 20 | private bookOps: SingleEntityOperations, Pick>; 21 | private authorBookOps: SingleEntityOperations, Pick>; 22 | private bookPrefix = 'BOOK#'; 23 | 24 | constructor() { 25 | this.tableName = 'online-library'; 26 | const storeContext = createStoreContext({ logger: consoleLogger }); 27 | 28 | let tableConfig: TableConfig = createStandardSingleTableConfig(this.tableName); 29 | tableConfig.metaAttributeNames.entityType = 'TYPE'; 30 | 31 | this.store = createStore(tableConfig, storeContext); 32 | this.bookOps = this.store.for(BOOK_ENTITY); 33 | this.authorBookOps = this.store.for(AUTHOR_BOOK_ENTITY); 34 | } 35 | 36 | async getBook(isbn: number) { 37 | let book: Book; 38 | 39 | try { 40 | book = await this.bookOps.getOrThrow({isbn}); 41 | } catch (error) { 42 | throw new InternalServerErrorException(error); 43 | } 44 | 45 | if (!book) { 46 | throw new NotFoundException(`Book with ISBN "${isbn}" not found`); 47 | } 48 | 49 | return book; 50 | } 51 | 52 | async getBooksByAuthor(lastName: string, firstName: string) { 53 | let books: AuthorBook[] = []; 54 | 55 | books = await this.authorBookOps.queryOnePageByPkAndSk({firstName, lastName}, 56 | rangeWhereSkBeginsWith(this.bookPrefix), {scanIndexForward: false}); 57 | 58 | // tslint:disable-next-line:no-console 59 | console.log(books); 60 | return books; 61 | } 62 | 63 | async addBook(newBook: Book) { 64 | await this.bookOps.put(newBook); 65 | return newBook; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /ddb-entity-store/src/books/books.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { BooksService } from './books.service'; 3 | 4 | describe('BooksService', () => { 5 | let service: BooksService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [BooksService], 10 | }).compile(); 11 | 12 | service = module.get(BooksService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /ddb-entity-store/src/books/books.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import {BooksRepository} from './books.repository'; 3 | import { Book } from './book'; 4 | 5 | @Injectable() 6 | export class BooksService { 7 | 8 | constructor(private bookRepo: BooksRepository) {} 9 | 10 | async getBook(isbn: number) { 11 | return await this.bookRepo.getBook(isbn); 12 | } 13 | 14 | async getAuthorBooks(lastName: string, firstName: string) { 15 | return await this.bookRepo.getBooksByAuthor(lastName, firstName); 16 | } 17 | 18 | async addBookToLibrary(newBook: Book) { 19 | newBook.lend = false; 20 | return await this.bookRepo.addBook(newBook); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ddb-entity-store/src/main.ts: -------------------------------------------------------------------------------- 1 | import { Handler, Context } from 'aws-lambda'; 2 | 3 | import {configure as serverlessExpress} from '@vendia/serverless-express'; 4 | 5 | import { NestFactory } from '@nestjs/core'; 6 | import { AppModule } from './app.module'; 7 | 8 | let cachedServer: Handler; 9 | 10 | process.on('unhandledRejection', (reason) => { 11 | // tslint:disable-next-line:no-console 12 | console.error(reason); 13 | }); 14 | 15 | process.on('uncaughtException', (reason) => { 16 | // tslint:disable-next-line:no-console 17 | console.error(reason); 18 | }); 19 | 20 | async function bootstrapServer() { 21 | if (!cachedServer) { 22 | const nestApp = await NestFactory.create(AppModule); 23 | nestApp.init(); 24 | nestApp.enableCors(); 25 | cachedServer = serverlessExpress({ app: nestApp.getHttpAdapter().getInstance()}); 26 | } 27 | return cachedServer; 28 | } 29 | 30 | export const handler = async (event: any, context: Context, callback: any) => { 31 | const server = await bootstrapServer(); 32 | return server(event, context, callback); 33 | }; 34 | -------------------------------------------------------------------------------- /ddb-entity-store/src/utils/ExceptionsLogFilter.ts: -------------------------------------------------------------------------------- 1 | import { Catch, ArgumentsHost } from '@nestjs/common'; 2 | import { BaseExceptionFilter } from '@nestjs/core'; 3 | 4 | @Catch() 5 | export class ExceptionsLoggerFilter extends BaseExceptionFilter { 6 | catch(exception: unknown, host: ArgumentsHost) { 7 | console.log('ERROR', exception); 8 | super.catch(exception, host); 9 | } 10 | } -------------------------------------------------------------------------------- /ddb-entity-store/test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { INestApplication } from '@nestjs/common'; 2 | import { Test } from '@nestjs/testing'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeAll(async () => { 10 | const moduleFixture = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/hello (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/hello') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /ddb-entity-store/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /ddb-entity-store/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src/**/*"], 4 | "exclude": ["node_modules", "**/*.spec.ts"] 5 | } 6 | -------------------------------------------------------------------------------- /ddb-entity-store/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "noImplicitAny": false, 6 | "removeComments": true, 7 | "noLib": false, 8 | "allowSyntheticDefaultImports": true, 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "target": "es6", 12 | "sourceMap": true, 13 | "outDir": "./dist", 14 | "baseUrl": "./" 15 | }, 16 | "exclude": ["node_modules"] 17 | } 18 | -------------------------------------------------------------------------------- /ddb-entity-store/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "types": ["jest", "node"] 5 | }, 6 | "include": ["**/*.spec.ts", "**/*.d.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /ddb-entity-store/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": ["tslint:recommended"], 4 | "jsRules": { 5 | "no-unused-expression": true 6 | }, 7 | "rules": { 8 | "quotemark": [true, "single"], 9 | "member-access": [false], 10 | "ordered-imports": [false], 11 | "max-line-length": [true, 150], 12 | "member-ordering": [false], 13 | "interface-name": [false], 14 | "arrow-parens": false, 15 | "object-literal-sort-keys": false 16 | }, 17 | "rulesDirectory": [] 18 | } 19 | -------------------------------------------------------------------------------- /online-library-cloudwatch-emf/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /.build/ 3 | /.serverless/ 4 | /_warmup/ 5 | /.idea/ 6 | -------------------------------------------------------------------------------- /online-library-cloudwatch-emf/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } 5 | -------------------------------------------------------------------------------- /online-library-cloudwatch-emf/README.md: -------------------------------------------------------------------------------- 1 | 9 | # Cloudwatch EMF example 10 | This example demonstrates you how to use Cloudwatch EMF. See the story on [Medium]() for more explanation. 11 | 12 | ## Use Cases 13 | - Cloudwatch EMF 14 | -------------------------------------------------------------------------------- /online-library-cloudwatch-emf/iam/OnlineLibraryIAM.yml: -------------------------------------------------------------------------------- 1 | OnlineLibraryService: 2 | - Effect: Allow 3 | Action: 4 | - dynamodb:PutItem 5 | - dynamodb:Scan 6 | - dynamodb:GetItem 7 | - dynamodb:UpdateItem 8 | - dynamodb:Query 9 | - dynamodb:ConditionCheckItem 10 | Resource: 11 | - arn:aws:dynamodb:eu-west-1:184690513648:table/online-library -------------------------------------------------------------------------------- /online-library-cloudwatch-emf/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "online-library-cloudwatch-emf", 3 | "version": "1.0.0", 4 | "description": "Serverless REST API service with Cloudwatch EMF example", 5 | "author": "cyberworkz", 6 | "scripts": { 7 | "build": "tsc -p tsconfig.build.json", 8 | "format": "prettier --write \"src/**/*.ts\"", 9 | "start": "sls offline start", 10 | "lint": "tslint -p tsconfig.json -c tslint.json", 11 | "test": "jest", 12 | "test:watch": "jest --watch", 13 | "test:cov": "jest --coverage", 14 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 15 | "test:e2e": "jest --config ./test/jest-e2e.json", 16 | "deploy": "sls deploy --stage dev" 17 | }, 18 | "dependencies": { 19 | "@nestjs/common": "^8.2.1", 20 | "@nestjs/core": "^8.2.1", 21 | "@nestjs/platform-express": "^8.2.1", 22 | "aws-sdk": "^2.789.0", 23 | "aws-serverless-express": "^3.3.5", 24 | "reflect-metadata": "^0.1.12", 25 | "rimraf": "^2.6.2", 26 | "rxjs": "^6.2.2" 27 | }, 28 | "devDependencies": { 29 | "@aws-lambda-powertools/metrics": "^1.5.1", 30 | "@nestjs/testing": "^5.1.0", 31 | "@types/aws-lambda": "^8.10.15", 32 | "@types/aws-serverless-express": "^3.3.5", 33 | "@types/express": "^4.16.0", 34 | "@types/jest": "^23.3.1", 35 | "@types/node": "^10.7.1", 36 | "@types/supertest": "^2.0.5", 37 | "acorn": "^8.0.5", 38 | "jest": "^23.5.0", 39 | "nodemon": "^1.18.3", 40 | "prettier": "^1.14.2", 41 | "serverless-offline": "^6.8.0", 42 | "serverless-plugin-typescript": "1.1.7", 43 | "serverless-stage-manager": "^1.0.5", 44 | "supertest": "^3.1.0", 45 | "ts-jest": "^23.1.3", 46 | "ts-loader": "^4.4.2", 47 | "ts-node": "^7.0.1", 48 | "tsconfig-paths": "^3.5.0", 49 | "tslint": "5.11.0", 50 | "typescript": "^4.1.5" 51 | }, 52 | "jest": { 53 | "moduleFileExtensions": [ 54 | "js", 55 | "json", 56 | "ts" 57 | ], 58 | "rootDir": "src", 59 | "testRegex": ".spec.ts$", 60 | "transform": { 61 | "^.+\\.(t|j)s$": "ts-jest" 62 | }, 63 | "coverageDirectory": "../coverage", 64 | "testEnvironment": "node" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /online-library-cloudwatch-emf/serverless.yml: -------------------------------------------------------------------------------- 1 | service: online-library-cloudwatch-emf 2 | 3 | plugins: 4 | - serverless-plugin-typescript 5 | - serverless-offline 6 | - serverless-stage-manager 7 | provider: 8 | name: aws 9 | tracing: 10 | apiGateway: true 11 | runtime: nodejs14.x 12 | memorySize: 128 # optional, in MB, default is 1024 13 | timeout: 10 # optional, in seconds, default is 6 14 | stage: ${opt:stage, 'dev'} 15 | region: eu-west-1 16 | iam: 17 | role: 18 | statements: ${file(iam/OnlineLibraryIAM.yml):OnlineLibraryService} 19 | 20 | package: 21 | patterns: 22 | - '!README.md' 23 | - '!erverless.yml' 24 | - '!nest-cli.json' 25 | - '!.prettierrc' 26 | excludeDevDependencies: true 27 | 28 | functions: 29 | main: 30 | handler: src/main.handler 31 | layers: 32 | - arn:aws:lambda:eu-west-1:094274105915:layer:AWSLambdaPowertoolsTypeScript:7 33 | events: 34 | - http: 35 | method: any 36 | path: /{proxy+} 37 | cors: true 38 | 39 | custom: 40 | stages: 41 | - dev 42 | - test 43 | - acc 44 | - prod 45 | -------------------------------------------------------------------------------- /online-library-cloudwatch-emf/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { BooksController } from './books/books.controller'; 3 | import { BooksRepository } from './books/books.repository'; 4 | import { BooksService } from './books/books.service'; 5 | import { ExceptionsLoggerFilter } from './utils/ExceptionsLogFilter'; 6 | import { APP_FILTER } from '@nestjs/core'; 7 | 8 | @Module({ 9 | imports: [], 10 | controllers: [ BooksController], 11 | providers: [BooksService, BooksRepository, 12 | { provide: APP_FILTER, 13 | useClass: ExceptionsLoggerFilter, 14 | }, 15 | ], 16 | }) 17 | export class AppModule {} 18 | -------------------------------------------------------------------------------- /online-library-cloudwatch-emf/src/books/books.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { BooksController } from './books.controller'; 3 | 4 | describe('BooksController', () => { 5 | let controller: BooksController; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | controllers: [BooksController], 10 | }).compile(); 11 | 12 | controller = module.get(BooksController); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(controller).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /online-library-cloudwatch-emf/src/books/books.controller.ts: -------------------------------------------------------------------------------- 1 | import {Controller, Get, HttpStatus, Param, Put, Res} from '@nestjs/common'; 2 | import {BooksService} from './books.service'; 3 | 4 | @Controller('books') 5 | export class BooksController { 6 | 7 | constructor(private bookService: BooksService){} 8 | 9 | @Get('/:isbn') 10 | async getBookByISBN(@Param('isbn') isbn:number, @Res() res: any){ 11 | const book: object = await this.bookService.getBook(isbn); 12 | return res.status(HttpStatus.OK).json(book); 13 | } 14 | 15 | @Get('/author/:lastName/:firstName') 16 | async getBooksByAuthor(@Param('lastName')lastName: string, @Param('firstName')firstName: string, @Res() res: any) { 17 | const books: any[] = await this.bookService.getAuthorBooks(lastName, firstName); 18 | return res.status(HttpStatus.OK).json(books); 19 | } 20 | 21 | @Put('/:isbn/lend') 22 | async lendBook(@Param('isbn') isbn:number, @Res() res: any) { 23 | const book = await this.bookService.lendBook(isbn); 24 | return res.status(HttpStatus.OK).json(book); 25 | } 26 | 27 | @Put('/:isbn/return') 28 | async returnBook(@Param('isbn') isbn:number, @Res() res: any) { 29 | const book = await this.bookService.returnBook(isbn); 30 | return res.status(HttpStatus.OK).json(book); 31 | } 32 | } -------------------------------------------------------------------------------- /online-library-cloudwatch-emf/src/books/books.repository.spec.ts: -------------------------------------------------------------------------------- 1 | import { BooksRepository } from './books.repository'; 2 | 3 | describe('BooksRepository', () => { 4 | it('should be defined', () => { 5 | expect(new BooksRepository()).toBeDefined(); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /online-library-cloudwatch-emf/src/books/books.repository.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, InternalServerErrorException, Logger, NotFoundException } from '@nestjs/common'; 2 | import { DocumentClient } from 'aws-sdk/clients/dynamodb'; 3 | import * as AWS from 'aws-sdk'; 4 | 5 | @Injectable() 6 | export class BooksRepository { 7 | 8 | private readonly logger = new Logger(BooksRepository.name); 9 | 10 | private tableName: string; 11 | private db: DocumentClient; 12 | 13 | private bookPrefix = 'BOOK#'; 14 | private authorPrefix = 'AUTH#'; 15 | 16 | constructor() { 17 | this.tableName = 'online-library'; 18 | this.db = new AWS.DynamoDB.DocumentClient(); 19 | } 20 | 21 | async getBook(isbn: number) { 22 | let book: object; 23 | 24 | try { 25 | const result = await this.db 26 | .get({ 27 | TableName: this.tableName, 28 | Key: { PK: this.bookPrefix.concat(String(isbn)), 29 | SK: this.bookPrefix.concat(String(isbn))}, 30 | }) 31 | .promise(); 32 | 33 | book = result.Item; 34 | } catch (error) { 35 | throw new InternalServerErrorException(error); 36 | } 37 | 38 | if (!book) { 39 | throw new NotFoundException(`Book with ISBN "${isbn}" not found`); 40 | } 41 | 42 | return book; 43 | } 44 | 45 | async getBooksByAuthor(lastName: string, firstName: string) { 46 | let books = []; 47 | 48 | try { 49 | const result = await this.db 50 | .query({ 51 | TableName: this.tableName, 52 | KeyConditionExpression: '#PK=:PK AND begins_with(#SK, :SK)', 53 | ExpressionAttributeNames: { 54 | '#PK': 'PK', 55 | '#SK': 'SK', 56 | }, 57 | ExpressionAttributeValues: { 58 | ':PK': this.authorPrefix.concat(lastName.toUpperCase()).concat('_').concat(firstName.toUpperCase()), 59 | ':SK': this.bookPrefix, 60 | }, 61 | ScanIndexForward: false, 62 | Limit: 100, 63 | }) 64 | .promise(); 65 | books = result.Items; 66 | } catch (error) { 67 | throw new InternalServerErrorException(error); 68 | } 69 | 70 | return books; 71 | } 72 | 73 | async lendBook(isbn: number) { 74 | let result: {}; 75 | try { 76 | this.logger.log('lending book: ' + isbn); 77 | result = await this.db 78 | .update({ 79 | TableName: this.tableName, 80 | Key: { 81 | PK: this.bookPrefix.concat(String(isbn)), 82 | SK: this.bookPrefix.concat(String(isbn)), 83 | }, 84 | UpdateExpression: 'set #lend = :lendNew, ' + 85 | 'lendDate = :lendDate', 86 | ConditionExpression: '#lend = :lendOpen', 87 | ExpressionAttributeNames: { 88 | '#lend': 'lend', 89 | }, 90 | ExpressionAttributeValues: { 91 | ':lendNew': true, 92 | ':lendDate': new Date().toISOString(), 93 | ':lendOpen': false, 94 | }, 95 | ReturnValues: 'ALL_NEW', 96 | }) 97 | .promise(); 98 | } catch (error) { 99 | this.logger.log(error); 100 | if (error.code === 'ConditionalCheckFailedException') { 101 | this.logger.warn('book already lend out: ' + isbn, error); 102 | return {ok: false, data: 'book already lend out: ' + isbn}; 103 | } 104 | throw new InternalServerErrorException(error); 105 | } 106 | 107 | this.logger.log(result); 108 | 109 | // @ts-ignore 110 | return {ok: true, data: result.Attributes} 111 | } 112 | 113 | async returnBook(isbn: number) { 114 | let result: {}; 115 | try { 116 | this.logger.log('returning book: ' + isbn); 117 | result = await this.db 118 | .update({ 119 | TableName: this.tableName, 120 | Key: { 121 | PK: this.bookPrefix.concat(String(isbn)), 122 | SK: this.bookPrefix.concat(String(isbn)), 123 | }, 124 | UpdateExpression: 'set #lend = :lendNew, ' + 125 | 'returnDate = :returnDate', 126 | ConditionExpression: '#lend = :lendOpen', 127 | ExpressionAttributeNames: { 128 | '#lend': 'lend', 129 | }, 130 | ExpressionAttributeValues: { 131 | ':lendNew': false, 132 | ':returnDate': new Date().toISOString(), 133 | ':lendOpen': true, 134 | }, 135 | ReturnValues: 'ALL_NEW', 136 | }) 137 | .promise(); 138 | } catch (error) { 139 | this.logger.log(error); 140 | if (error.code === 'ConditionalCheckFailedException') { 141 | this.logger.warn('book not lend out: ' + isbn, error); 142 | return {ok: false, data: 'book not lend out: ' + isbn}; 143 | } 144 | throw new InternalServerErrorException(error); 145 | } 146 | 147 | this.logger.log(result); 148 | 149 | // @ts-ignore 150 | return {ok: true, data: result.Attributes} 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /online-library-cloudwatch-emf/src/books/books.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { BooksService } from './books.service'; 3 | 4 | describe('BooksService', () => { 5 | let service: BooksService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [BooksService], 10 | }).compile(); 11 | 12 | service = module.get(BooksService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /online-library-cloudwatch-emf/src/books/books.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { metrics } from '../main'; 3 | import { BooksRepository } from "./books.repository"; 4 | 5 | import { MetricUnits} from '@aws-lambda-powertools/metrics'; 6 | 7 | 8 | @Injectable() 9 | export class BooksService { 10 | 11 | 12 | constructor(private bookRepo: BooksRepository) { } 13 | 14 | async getBook(isbn: number) { 15 | return await this.bookRepo.getBook(isbn); 16 | } 17 | 18 | async getAuthorBooks(lastName: string, firstName: string) { 19 | return await this.bookRepo.getBooksByAuthor(lastName, firstName); 20 | } 21 | 22 | async lendBook(isbn: number) { 23 | let bookResponse = await this.bookRepo.lendBook(isbn); 24 | 25 | if(bookResponse.ok){ 26 | // measure books lend per category 27 | metrics.addMetric(bookResponse.data.category, MetricUnits.Count, 1); 28 | metrics.addMetadata('lendDate', bookResponse.data.lendDate) 29 | 30 | // publish metrics 31 | metrics.publishStoredMetrics(); 32 | } 33 | 34 | return bookResponse; 35 | } 36 | 37 | async returnBook(isbn: number) { 38 | let bookResponse = await this.bookRepo.returnBook(isbn); 39 | return bookResponse; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /online-library-cloudwatch-emf/src/books/iauthor.ts: -------------------------------------------------------------------------------- 1 | export interface IAuthor { 2 | 3 | firstName: string; 4 | lastName: string; 5 | } 6 | -------------------------------------------------------------------------------- /online-library-cloudwatch-emf/src/books/ibook.ts: -------------------------------------------------------------------------------- 1 | import {IAuthor} from "./iauthor"; 2 | 3 | export interface IBook { 4 | 5 | isbn: number; 6 | title: string; 7 | author: IAuthor; 8 | category: string; 9 | reserved: boolean; 10 | edition: number; 11 | publisher: string; 12 | lend: boolean; 13 | lendDate: Date; 14 | returnDate: Date; 15 | 16 | } 17 | -------------------------------------------------------------------------------- /online-library-cloudwatch-emf/src/main.ts: -------------------------------------------------------------------------------- 1 | import { Handler, Context } from 'aws-lambda'; 2 | import { Server } from 'http'; 3 | import { createServer, proxy } from 'aws-serverless-express'; 4 | import { eventContext } from 'aws-serverless-express/middleware'; 5 | 6 | import { ExpressAdapter } from '@nestjs/platform-express'; 7 | import { NestFactory } from '@nestjs/core'; 8 | import { AppModule } from './app.module'; 9 | 10 | import * as express from 'express'; 11 | 12 | // bootstrap Metrics outside Lambda handler 13 | import { Metrics } from '@aws-lambda-powertools/metrics'; 14 | export const metrics = new Metrics({ namespace: 'library-metrics', serviceName: 'bookloans' }); 15 | 16 | // NOTE: If you get ERR_CONTENT_DECODING_FAILED in your browser, this is likely 17 | // due to a compressed response (e.g. gzip) which has not been handled correctly 18 | // by aws-serverless-express and/or API Gateway. Add the necessary MIME types to 19 | // binaryMimeTypes below 20 | const binaryMimeTypes: string[] = []; 21 | 22 | let cachedServer: Server; 23 | 24 | process.on('unhandledRejection', (reason) => { 25 | // tslint:disable-next-line:no-console 26 | console.error(reason); 27 | }); 28 | 29 | process.on('uncaughtException', (reason) => { 30 | // tslint:disable-next-line:no-console 31 | console.error(reason); 32 | }); 33 | 34 | async function bootstrapServer(): Promise { 35 | if (!cachedServer) { 36 | try { 37 | const expressApp = express(); 38 | const nestApp = await NestFactory.create(AppModule, new ExpressAdapter(expressApp)); 39 | nestApp.enableCors(); 40 | nestApp.use(eventContext()); 41 | await nestApp.init(); 42 | cachedServer = createServer(expressApp, undefined, binaryMimeTypes); 43 | } catch (error) { 44 | return Promise.reject(error); 45 | } 46 | } 47 | return Promise.resolve(cachedServer); 48 | } 49 | 50 | export const handler: Handler = async (event: any, context: Context) => { 51 | cachedServer = await bootstrapServer(); 52 | return proxy(cachedServer, event, context, 'PROMISE').promise; 53 | } 54 | -------------------------------------------------------------------------------- /online-library-cloudwatch-emf/src/utils/ExceptionsLogFilter.ts: -------------------------------------------------------------------------------- 1 | import { Catch, ArgumentsHost } from '@nestjs/common'; 2 | import { BaseExceptionFilter } from '@nestjs/core'; 3 | 4 | @Catch() 5 | export class ExceptionsLoggerFilter extends BaseExceptionFilter { 6 | catch(exception: unknown, host: ArgumentsHost) { 7 | console.log('ERROR', exception); 8 | super.catch(exception, host); 9 | } 10 | } -------------------------------------------------------------------------------- /online-library-cloudwatch-emf/test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { INestApplication } from '@nestjs/common'; 2 | import { Test } from '@nestjs/testing'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeAll(async () => { 10 | const moduleFixture = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/hello (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/hello') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /online-library-cloudwatch-emf/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /online-library-cloudwatch-emf/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src/**/*"], 4 | "exclude": ["node_modules", "**/*.spec.ts"] 5 | } 6 | -------------------------------------------------------------------------------- /online-library-cloudwatch-emf/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "esModuleInterop": false, 5 | "lib": ["esnext"], 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "noUnusedLocals": true, 9 | "noUnusedParameters": true, 10 | "outDir": "lib", 11 | "sourceMap": true, 12 | "target": "esnext", 13 | "experimentalDecorators": true, 14 | "emitDecoratorMetadata": true 15 | }, 16 | "exclude": ["node_modules"] 17 | } 18 | -------------------------------------------------------------------------------- /online-library-cloudwatch-emf/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "types": ["jest", "node"] 5 | }, 6 | "include": ["**/*.spec.ts", "**/*.d.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /online-library-cloudwatch-emf/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": ["tslint:recommended"], 4 | "jsRules": { 5 | "no-unused-expression": true 6 | }, 7 | "rules": { 8 | "quotemark": [true, "single"], 9 | "member-access": [false], 10 | "ordered-imports": [false], 11 | "max-line-length": [true, 150], 12 | "member-ordering": [false], 13 | "interface-name": [false], 14 | "arrow-parens": false, 15 | "object-literal-sort-keys": false 16 | }, 17 | "rulesDirectory": [] 18 | } 19 | -------------------------------------------------------------------------------- /online-library-java/.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | .gradle 3 | /build/ 4 | /bin/ 5 | /.settings/ 6 | .project 7 | .classpath 8 | 9 | # Package Files 10 | *.jar 11 | *.war 12 | *.ear 13 | 14 | # Serverless directories 15 | .serverless 16 | 17 | !/gradle/wrapper/gradle-wrapper.jar 18 | -------------------------------------------------------------------------------- /online-library-java/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.github.johnrengelman.shadow' version '6.1.0' 3 | id 'java' 4 | } 5 | 6 | repositories { 7 | mavenCentral() 8 | } 9 | 10 | sourceCompatibility = 1.11 11 | targetCompatibility = 1.11 12 | 13 | dependencies { 14 | 15 | // AWS Lambda 16 | implementation 'com.amazonaws:aws-lambda-java-events:3.11.0' 17 | implementation 'com.amazonaws:aws-lambda-java-core:1.2.1' 18 | implementation 'com.amazonaws:aws-lambda-java-log4j2:1.5.1' 19 | implementation 'com.amazonaws.serverless:aws-serverless-java-container-spring:1.8.2' 20 | 21 | 22 | // Jackson 23 | implementation 'com.fasterxml.jackson.core:jackson-core:2.13.3' 24 | implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.3' 25 | implementation 'com.fasterxml.jackson.core:jackson-annotations:2.13.3' 26 | 27 | // spring 28 | implementation 'org.springframework:spring-webmvc:5.3.20' 29 | implementation 'org.springframework:spring-context:5.3.20' 30 | 31 | // logging 32 | implementation 'org.apache.logging.log4j:log4j-core:2.17.2' 33 | implementation 'org.apache.logging.log4j:log4j-api:2.17.2' 34 | implementation 'org.apache.logging.log4j:log4j-slf4j18-impl:2.17.2' 35 | 36 | 37 | } 38 | 39 | shadowJar { 40 | exclude 'META-INF/**/pom.xml' 41 | exclude 'META-INF/**/pom.properties' 42 | exclude 'META-INF/spring*' 43 | exclude '**/Log4j2Plugins.dat' 44 | } 45 | 46 | -------------------------------------------------------------------------------- /online-library-java/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyberworkz/examples/58db18f73b55167eafe05360c5efbac03fdd8353/online-library-java/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /online-library-java/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.5.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /online-library-java/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /online-library-java/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto init 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto init 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :init 68 | @rem Get command-line arguments, handling Windows variants 69 | 70 | if not "%OS%" == "Windows_NT" goto win9xME_args 71 | 72 | :win9xME_args 73 | @rem Slurp the command line arguments. 74 | set CMD_LINE_ARGS= 75 | set _SKIP=2 76 | 77 | :win9xME_args_slurp 78 | if "x%~1" == "x" goto execute 79 | 80 | set CMD_LINE_ARGS=%* 81 | 82 | :execute 83 | @rem Setup the command line 84 | 85 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 86 | 87 | 88 | @rem Execute Gradle 89 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 90 | 91 | :end 92 | @rem End local scope for the variables with windows NT shell 93 | if "%ERRORLEVEL%"=="0" goto mainEnd 94 | 95 | :fail 96 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 97 | rem the _cmd.exe /c_ return code! 98 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 99 | exit /b 1 100 | 101 | :mainEnd 102 | if "%OS%"=="Windows_NT" endlocal 103 | 104 | :omega 105 | -------------------------------------------------------------------------------- /online-library-java/src/main/java/com/serverlesscorner/onlinelibrary/ApplicationConfig.java: -------------------------------------------------------------------------------- 1 | package com.serverlesscorner.onlinelibrary; 2 | 3 | 4 | import java.util.List; 5 | 6 | import org.springframework.context.annotation.*; 7 | import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; 8 | import org.springframework.http.converter.HttpMessageConverter; 9 | import org.springframework.http.converter.StringHttpMessageConverter; 10 | import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; 11 | import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; 12 | import org.springframework.web.cors.CorsConfiguration; 13 | import org.springframework.web.cors.UrlBasedCorsConfigurationSource; 14 | import org.springframework.web.filter.CorsFilter; 15 | import org.springframework.web.multipart.MultipartResolver; 16 | import org.springframework.web.multipart.commons.CommonsMultipartResolver; 17 | import org.springframework.web.servlet.HandlerAdapter; 18 | import org.springframework.web.servlet.HandlerExceptionResolver; 19 | import org.springframework.web.servlet.HandlerMapping; 20 | import org.springframework.web.servlet.ModelAndView; 21 | import org.springframework.web.servlet.config.annotation.EnableWebMvc; 22 | import org.springframework.web.servlet.config.annotation.InterceptorRegistry; 23 | 24 | import com.fasterxml.jackson.databind.ObjectMapper; 25 | import com.fasterxml.jackson.databind.SerializationFeature; 26 | import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; 27 | import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; 28 | 29 | import javax.servlet.http.HttpServletRequest; 30 | import javax.servlet.http.HttpServletResponse; 31 | 32 | 33 | @EnableWebMvc 34 | @Configuration 35 | @Import(OnlineLibraryController.class) 36 | @PropertySource("classpath:application.properties") 37 | public class ApplicationConfig { 38 | 39 | private static final long MAX_UPLOAD_SIZE = 125_829_120L; 40 | 41 | 42 | @Bean 43 | public CorsFilter corsFilter() { 44 | UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); 45 | CorsConfiguration config = new CorsConfiguration(); 46 | config.addAllowedOrigin("*"); 47 | config.addAllowedHeader("*"); 48 | config.addAllowedMethod("OPTIONS"); 49 | config.addAllowedMethod("HEAD"); 50 | config.addAllowedMethod("GET"); 51 | config.addAllowedMethod("PUT"); 52 | config.addAllowedMethod("POST"); 53 | config.addAllowedMethod("DELETE"); 54 | config.addAllowedMethod("PATCH"); 55 | source.registerCorsConfiguration("/**", config); 56 | return new CorsFilter(source); 57 | } 58 | 59 | 60 | @Bean 61 | @Primary 62 | public ObjectMapper objectMapper() { 63 | ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json().build(); 64 | objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); 65 | objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); 66 | 67 | return objectMapper; 68 | } 69 | 70 | @Bean 71 | public static PropertySourcesPlaceholderConfigurer properties() { 72 | PropertySourcesPlaceholderConfigurer configurer = new PropertySourcesPlaceholderConfigurer(); 73 | return configurer; 74 | } 75 | 76 | @DependsOn("objectMapper") 77 | public void configureMessageConverters(List> converters) { 78 | converters.add(new MappingJackson2HttpMessageConverter(objectMapper())); 79 | converters.add(new StringHttpMessageConverter()); 80 | } 81 | 82 | @Bean 83 | public MultipartResolver multipartResolver() { 84 | CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver(); 85 | multipartResolver.setMaxUploadSize(MAX_UPLOAD_SIZE); 86 | return multipartResolver; 87 | } 88 | 89 | 90 | public void addInterceptors(InterceptorRegistry registry) { 91 | registry.addInterceptor(new LoggingInterceptor()); 92 | } 93 | 94 | /* 95 | * Create required HandlerMapping, to avoid several default HandlerMapping instances being created 96 | */ 97 | @Bean 98 | public HandlerMapping handlerMapping() { 99 | return new RequestMappingHandlerMapping(); 100 | } 101 | 102 | /* 103 | * Create required HandlerAdapter, to avoid several default HandlerAdapter instances being created 104 | */ 105 | @Bean 106 | public HandlerAdapter handlerAdapter() { 107 | return new RequestMappingHandlerAdapter(); 108 | } 109 | 110 | /* 111 | * optimization - avoids creating default exception resolvers; not required as the serverless container handles 112 | * all exceptions 113 | * 114 | * By default, an ExceptionHandlerExceptionResolver is created which creates many dependent object, including 115 | * an expensive ObjectMapper instance. 116 | * 117 | * To enable custom @ControllerAdvice classes remove this bean. 118 | */ 119 | @Bean 120 | public HandlerExceptionResolver handlerExceptionResolver() { 121 | return new HandlerExceptionResolver() { 122 | 123 | @Override 124 | public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { 125 | return null; 126 | } 127 | }; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /online-library-java/src/main/java/com/serverlesscorner/onlinelibrary/Author.java: -------------------------------------------------------------------------------- 1 | package com.serverlesscorner.onlinelibrary; 2 | 3 | import java.util.Objects; 4 | 5 | public class Author { 6 | 7 | private String firstName; 8 | 9 | private String lastName; 10 | 11 | public Author(String firstName, String lastName) { 12 | this.firstName = firstName; 13 | this.lastName = lastName; 14 | } 15 | 16 | public String getFirstName() { 17 | return firstName; 18 | } 19 | 20 | public void setFirstName(String firstName) { 21 | this.firstName = firstName; 22 | } 23 | 24 | public String getLastName() { 25 | return lastName; 26 | } 27 | 28 | public void setLastName(String lastName) { 29 | this.lastName = lastName; 30 | } 31 | 32 | @Override 33 | public boolean equals(Object o) { 34 | if (this == o) return true; 35 | if (o == null || getClass() != o.getClass()) return false; 36 | Author author = (Author) o; 37 | return firstName.equals(author.firstName) && lastName.equals(author.lastName); 38 | } 39 | 40 | @Override 41 | public int hashCode() { 42 | return Objects.hash(firstName, lastName); 43 | } 44 | 45 | @Override 46 | public String toString() { 47 | return "Author{" + 48 | "firstName='" + firstName + '\'' + 49 | ", lastName='" + lastName + '\'' + 50 | '}'; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /online-library-java/src/main/java/com/serverlesscorner/onlinelibrary/Book.java: -------------------------------------------------------------------------------- 1 | package com.serverlesscorner.onlinelibrary; 2 | 3 | import java.util.Objects; 4 | 5 | public class Book { 6 | 7 | private Integer isbn; 8 | 9 | private String title; 10 | 11 | private Author author; 12 | 13 | public Book(Integer isbn, String title, Author author) { 14 | this.isbn = isbn; 15 | this.title = title; 16 | this.author = author; 17 | } 18 | 19 | public Integer getIsbn() { 20 | return isbn; 21 | } 22 | 23 | public void setIsbn(Integer isbn) { 24 | this.isbn = isbn; 25 | } 26 | 27 | public String getTitle() { 28 | return title; 29 | } 30 | 31 | public void setTitle(String title) { 32 | this.title = title; 33 | } 34 | 35 | public Author getAuthor() { 36 | return author; 37 | } 38 | 39 | public void setAuthor(Author author) { 40 | this.author = author; 41 | } 42 | 43 | @Override 44 | public boolean equals(Object o) { 45 | if (this == o) return true; 46 | if (o == null || getClass() != o.getClass()) return false; 47 | Book book = (Book) o; 48 | return isbn.equals(book.isbn); 49 | } 50 | 51 | @Override 52 | public int hashCode() { 53 | return Objects.hash(isbn); 54 | } 55 | 56 | @Override 57 | public String toString() { 58 | return "Book{" + 59 | "isbn=" + isbn + 60 | ", title='" + title + '\'' + 61 | ", author=" + author + 62 | '}'; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /online-library-java/src/main/java/com/serverlesscorner/onlinelibrary/LoggingInterceptor.java: -------------------------------------------------------------------------------- 1 | package com.serverlesscorner.onlinelibrary; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.web.servlet.HandlerInterceptor; 6 | import org.springframework.web.servlet.ModelAndView; 7 | 8 | import javax.servlet.http.HttpServletRequest; 9 | import javax.servlet.http.HttpServletResponse; 10 | import java.util.Collection; 11 | 12 | public class LoggingInterceptor implements HandlerInterceptor { 13 | private static final Logger logger = LoggerFactory.getLogger(LoggingInterceptor.class); 14 | 15 | @Override 16 | public boolean preHandle(HttpServletRequest req, HttpServletResponse response, Object handler) 17 | throws Exception { 18 | logger.info("adr:{}; req:\"{} {} {}\";", req.getRemoteAddr(), req.getMethod(), req.getRequestURL(), req.getProtocol()); 19 | 20 | return true; 21 | } 22 | 23 | @Override 24 | public void postHandle( HttpServletRequest request, HttpServletResponse response, 25 | Object handler, ModelAndView modelAndView) throws Exception { 26 | Collection responseHeaders = response.getHeaderNames(); 27 | for(String name: responseHeaders){ 28 | logger.info("header:" + name + ":" + response.getHeader(name)); 29 | } 30 | } 31 | 32 | @Override 33 | public void afterCompletion(HttpServletRequest request, HttpServletResponse response, 34 | Object handler, Exception ex) throws Exception { 35 | logger.info("Request completed with Status({})", response.getStatus()); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /online-library-java/src/main/java/com/serverlesscorner/onlinelibrary/OnlineLibraryController.java: -------------------------------------------------------------------------------- 1 | package com.serverlesscorner.onlinelibrary; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.context.annotation.Import; 6 | import org.springframework.http.HttpStatus; 7 | import org.springframework.http.ResponseEntity; 8 | import org.springframework.web.bind.annotation.GetMapping; 9 | import org.springframework.web.bind.annotation.PathVariable; 10 | import org.springframework.web.bind.annotation.RequestMapping; 11 | import org.springframework.web.bind.annotation.RestController; 12 | import org.springframework.web.servlet.config.annotation.EnableWebMvc; 13 | 14 | import java.util.List; 15 | 16 | 17 | @RestController 18 | @EnableWebMvc 19 | @RequestMapping(value = "/books") 20 | @Import(OnlineLibraryService.class) 21 | public class OnlineLibraryController { 22 | 23 | private final OnlineLibraryService service; 24 | 25 | private static final Logger LOG = LoggerFactory.getLogger(OnlineLibraryController.class); 26 | 27 | public OnlineLibraryController(OnlineLibraryService service) { 28 | this.service = service; 29 | } 30 | 31 | @GetMapping(value = "/{isbn}", produces = "application/json") 32 | public ResponseEntity getBookByIsbn(@PathVariable("isbn") Integer isbn){ 33 | Book book = service.getBookByIsbn(isbn); 34 | 35 | if(book != null){ 36 | return new ResponseEntity<>(book, HttpStatus.OK); 37 | } else { 38 | LOG.info(" book not found"); 39 | return new ResponseEntity<>(null, HttpStatus.NOT_FOUND); 40 | } 41 | } 42 | 43 | 44 | @GetMapping("/author/{lastName}/{firstName}") 45 | public ResponseEntity> getBooksByAuthor(@PathVariable("lastName") String lastName, @PathVariable("firstName") String firstName) { 46 | List booksFromAuthor = service.getBooksFromAuthor(new Author(firstName, lastName)); 47 | 48 | return new ResponseEntity<>(booksFromAuthor, HttpStatus.OK); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /online-library-java/src/main/java/com/serverlesscorner/onlinelibrary/OnlineLibraryService.java: -------------------------------------------------------------------------------- 1 | package com.serverlesscorner.onlinelibrary; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.stereotype.Service; 6 | 7 | import java.util.ArrayList; 8 | import java.util.HashMap; 9 | import java.util.List; 10 | import java.util.Map; 11 | 12 | @Service 13 | public class OnlineLibraryService { 14 | 15 | private static final Logger LOG = LoggerFactory.getLogger(OnlineLibraryService.class); 16 | 17 | private static Map booksByIsbnMap = new HashMap<>(); 18 | 19 | private static Map> booksByAuthor = new HashMap(); 20 | 21 | static { 22 | 23 | Author tolkien = new Author("JRR", "Tolkien"); 24 | Author rowling = new Author("JK", "Rowling"); 25 | 26 | Book hobbit = new Book(1234, "The Hobbit", tolkien); 27 | Book twoTowers = new Book(1224, "Two Towers", tolkien); 28 | Book philosophersStone = new Book(4556, "Harry Potter and the Philosopher's Stone", rowling); 29 | Book deathlyHallows = new Book(4875, "Harry Potter and the Deathly Hallows", rowling); 30 | 31 | // fill maps 32 | booksByIsbnMap.put(hobbit.getIsbn(), hobbit); 33 | booksByIsbnMap.put(twoTowers.getIsbn(), twoTowers); 34 | booksByIsbnMap.put(philosophersStone.getIsbn(), philosophersStone); 35 | booksByIsbnMap.put(deathlyHallows.getIsbn(), deathlyHallows); 36 | 37 | booksByAuthor.put(tolkien, List.of(hobbit, twoTowers)); 38 | booksByAuthor.put(rowling, List.of(philosophersStone, deathlyHallows)); 39 | 40 | } 41 | 42 | public Book getBookByIsbn(Integer isbn) { 43 | return booksByIsbnMap.get(isbn); 44 | } 45 | 46 | public List getBooksFromAuthor(Author author) { 47 | LOG.debug(author.toString()); 48 | return booksByAuthor.get(author) != null ? booksByAuthor.get(author): new ArrayList<>(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /online-library-java/src/main/java/com/serverlesscorner/onlinelibrary/StreamLambdaHandler.java: -------------------------------------------------------------------------------- 1 | package com.serverlesscorner.onlinelibrary; 2 | 3 | import com.amazonaws.serverless.exceptions.ContainerInitializationException; 4 | import com.amazonaws.serverless.proxy.model.AwsProxyRequest; 5 | import com.amazonaws.serverless.proxy.model.AwsProxyResponse; 6 | import com.amazonaws.serverless.proxy.spring.SpringLambdaContainerHandler; 7 | import com.amazonaws.services.lambda.runtime.Context; 8 | import com.amazonaws.services.lambda.runtime.RequestStreamHandler; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | 12 | import java.io.IOException; 13 | import java.io.InputStream; 14 | import java.io.OutputStream; 15 | 16 | /** 17 | * @author haiko 18 | * 19 | */ 20 | public class StreamLambdaHandler implements RequestStreamHandler { 21 | 22 | private static final Logger LOG = LoggerFactory.getLogger(StreamLambdaHandler.class); 23 | 24 | private static SpringLambdaContainerHandler handler; 25 | 26 | static { 27 | try { 28 | LOG.info("start lambda container init"); 29 | 30 | handler = SpringLambdaContainerHandler.getAwsProxyHandler(ApplicationConfig.class); 31 | 32 | LOG.info("endlambda container init"); 33 | } catch (ContainerInitializationException e) { 34 | // if we fail here. We re-throw the exception to force another cold start 35 | e.printStackTrace(); 36 | throw new RuntimeException("Could not initialize Spring framework", e); 37 | } 38 | } 39 | 40 | @Override 41 | public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context) 42 | throws IOException { 43 | handler.proxyStream(inputStream, outputStream, context); 44 | } 45 | } -------------------------------------------------------------------------------- /online-library-java/src/main/resources/application.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyberworkz/examples/58db18f73b55167eafe05360c5efbac03fdd8353/online-library-java/src/main/resources/application.properties -------------------------------------------------------------------------------- /online-library-java/src/main/resources/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %d{yyyy-MM-dd HH:mm:ss} %X{AWSRequestId} %-5p %c{1}:%L - %m%n 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /online-library-java/template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: > 4 | online-library-java 5 | 6 | SAM Template for online-library-java 7 | # More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst 8 | Globals: 9 | Function: 10 | Timeout: 20 11 | Tracing: Active 12 | Api: 13 | TracingEnabled: True 14 | 15 | Resources: 16 | OnlineLibraryJavaAPI: 17 | Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction 18 | Properties: 19 | CodeUri: ./build/libs/online-library-java-all.jar 20 | Handler: com.serverlesscorner.onlinelibrary.StreamLambdaHandler 21 | Runtime: java11 22 | Architectures: 23 | - x86_64 24 | Policies: AWSLambdaBasicExecutionRole 25 | Timeout: 60 26 | MemorySize: 512 27 | Environment: # More info about Env Vars: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#environment-object 28 | Variables: 29 | PARAM1: VALUE 30 | JAVA_TOOL_OPTIONS: -XX:+TieredCompilation -XX:TieredStopAtLevel=1 # More info about tiered compilation https://aws.amazon.com/blogs/compute/optimizing-aws-lambda-function-performance-for-java/ 31 | Events: 32 | LibraryAPI: 33 | Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api 34 | Properties: 35 | Path: /{proxy+} 36 | Method: any 37 | 38 | ConfigLambdaPermission: 39 | Type: "AWS::Lambda::Permission" 40 | DependsOn: 41 | - OnlineLibraryJavaAPI 42 | Properties: 43 | Action: lambda:InvokeFunction 44 | FunctionName: !Ref OnlineLibraryJavaAPI 45 | Principal: apigateway.amazonaws.com 46 | -------------------------------------------------------------------------------- /online-library-monitor/.gitignore: -------------------------------------------------------------------------------- 1 | # package directories 2 | node_modules 3 | jspm_packages 4 | 5 | # Serverless directories 6 | .serverless 7 | 8 | # Webpack directories 9 | .webpack 10 | /email-service.iml 11 | /.idea/ 12 | -------------------------------------------------------------------------------- /online-library-monitor/README.md: -------------------------------------------------------------------------------- 1 | 9 | # Serverless AWS Lambda Cloudwatch Log Template 10 | 11 | This serverless template enables you to handle Cloudwatch Log events with AWS Lambda using Serverless and TypeScript. 12 | 13 | ## Use Cases 14 | - Process Cloudwatch logs to trigger on errors or warnings 15 | 16 | ## Setup 17 | 1. Install serverless - ```npm install -g serverless``` 18 | 2. Use serverless to create a project with template-url. For example 19 | ```serverless create --template-url https://github.com/cyberworkz/serverless-templates/tree/main/aws-nodejs-typescript-cloudwatch-log --path myService``` 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /online-library-monitor/handler.ts: -------------------------------------------------------------------------------- 1 | export { default as processor} from './src/processor'; 2 | -------------------------------------------------------------------------------- /online-library-monitor/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "online-library-monitor", 3 | "version": "0.0.0", 4 | "description": "Serverless example AWS S3 event with TypeScript", 5 | "main": "handler.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "lint": "tslint -p tsconfig.json -c tslint.json", 9 | "deploy-dev": "sls deploy --stage dev" 10 | }, 11 | "dependencies": { 12 | "@slack/web-api": "^6.7.1", 13 | "@types/lambda-log": "^2.2.0", 14 | "algoliasearch": "^4.13.1", 15 | "aws-lambda": "^0.1.2", 16 | "aws-sdk": "^2.859.0", 17 | "lambda": "^0.10.3", 18 | "log": "^6.0.0", 19 | "source-map-support": "^0.5.10" 20 | }, 21 | "devDependencies": { 22 | "@types/aws-lambda": "^8.10.72", 23 | "@types/node": "^12.12.6", 24 | "eslint": "^6.6.0", 25 | "serverless-webpack": "^5.2.0", 26 | "ts-loader": "^5.3.3", 27 | "tslint": "^5.20.0", 28 | "tslint-config-airbnb": "^5.11.2", 29 | "typescript": "^3.9.9", 30 | "webpack": "^4.46.0" 31 | }, 32 | "author": "Haiko van der Schaaf (https://github.com/cyberworkz)", 33 | "license": "MIT" 34 | } 35 | -------------------------------------------------------------------------------- /online-library-monitor/serverless.yml: -------------------------------------------------------------------------------- 1 | service: online-library-monitor 2 | 3 | plugins: 4 | - serverless-webpack 5 | 6 | provider: 7 | name: aws 8 | runtime: nodejs14.x 9 | memorySize: 128 # optional, in MB, default is 1024 10 | timeout: 10 # optional, in seconds, default is 6 11 | stage: ${opt:stage, 'dev'} 12 | region: eu-west-1 13 | 14 | 15 | functions: 16 | processor: 17 | handler: handler.processor 18 | events: 19 | - cloudwatchLog: 20 | logGroup: ${self:custom.logGroup} 21 | filter: '?ERROR ?Exception' 22 | 23 | 24 | # custom variables 25 | custom: 26 | logGroup: /aws/lambda/online-library-dev-main 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /online-library-monitor/src/processor.ts: -------------------------------------------------------------------------------- 1 | import { ErrorCode, WebClient } from '@slack/web-api'; 2 | import { 3 | CloudWatchLogsDecodedData, 4 | CloudWatchLogsEvent, 5 | CloudWatchLogsEventData, 6 | CloudWatchLogsHandler, 7 | } from 'aws-lambda'; 8 | import * as zlib from "zlib"; 9 | 10 | // Read a token from the environment variables 11 | const token = process.env.SLACK_TOKEN; 12 | const web = new WebClient(token); 13 | 14 | /** 15 | * Note: Cloudwatch log events are gzip-compressed and base64-encoded 16 | */ 17 | const processor: CloudWatchLogsHandler = async (event: CloudWatchLogsEvent) => { 18 | 19 | console.log('received log event on ' + new Date().toDateString()); 20 | 21 | const compressedData: CloudWatchLogsEventData = event.awslogs; 22 | 23 | if(compressedData){ 24 | const payload = Buffer.from(compressedData.data, 'base64') 25 | const decodedData: CloudWatchLogsDecodedData = JSON.parse(zlib.unzipSync(payload).toString()) as CloudWatchLogsDecodedData; 26 | 27 | console.log("Log group:" + decodedData.logGroup); 28 | 29 | for (const logEvent of decodedData.logEvents) { 30 | 31 | console.log(decodedData.logGroup + " " + logEvent.message); 32 | 33 | // message Slack 34 | try { 35 | const result = await web.chat.postMessage({ 36 | text: logEvent.message, 37 | channel: '#general' 38 | }); 39 | console.log(result); 40 | } 41 | catch (error) { 42 | // Check the code property, and when its a PlatformError, log the whole response. 43 | if (error.code === ErrorCode.PlatformError) { 44 | console.log(error.data); 45 | } else { 46 | // Some other error, oh no! 47 | console.log('Well, that was unexpected.'); 48 | } 49 | } 50 | } 51 | } 52 | }; 53 | 54 | export default processor; 55 | -------------------------------------------------------------------------------- /online-library-monitor/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "esModuleInterop": true, 5 | "lib": ["esnext"], 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "noUnusedLocals": true, 9 | "noUnusedParameters": true, 10 | "outDir": "lib", 11 | "sourceMap": true, 12 | "target": "esnext", 13 | "experimentalDecorators": true, 14 | "emitDecoratorMetadata": true 15 | }, 16 | "exclude": ["node_modules"] 17 | } 18 | -------------------------------------------------------------------------------- /online-library-monitor/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint-config-airbnb"] 3 | } 4 | -------------------------------------------------------------------------------- /online-library-monitor/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const slsw = require('serverless-webpack'); 3 | 4 | module.exports = { 5 | mode: slsw.lib.webpack.isLocal ? 'development' : 'production', 6 | entry: slsw.lib.entries, 7 | devtool: 'source-map', 8 | resolve: { 9 | extensions: ['.js', '.jsx', '.json', '.ts', '.tsx'], 10 | }, 11 | output: { 12 | libraryTarget: 'commonjs', 13 | path: path.join(__dirname, '.webpack'), 14 | filename: '[name].js', 15 | }, 16 | target: 'node', 17 | module: { 18 | rules: [ 19 | // all files with a `.ts` or `.tsx` extension will be handled by `ts-loader` 20 | { test: /\.tsx?$/, loader: 'ts-loader' }, 21 | ], 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /online-library-oss/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /.build/ 3 | /.serverless/ 4 | /_warmup/ 5 | /.idea/ 6 | -------------------------------------------------------------------------------- /online-library-oss/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } 5 | -------------------------------------------------------------------------------- /online-library-oss/README.md: -------------------------------------------------------------------------------- 1 | 9 | # Serverless NestJS REST API template 10 | 11 | This serverless template enables you to run a NestJS REST API with AWS Lambda using Serverless and TypeScript. 12 | 13 | ## Use Cases 14 | - REST API. 15 | 16 | ## Setup 17 | 1. Install serverless - ```npm install -g serverless``` 18 | 2. Use serverless to create a project with template-url. For example 19 | ```serverless create --template-url https://github.com/cyberworkz/serverless-templates/tree/main/aws-nodejs-typescript-restapi-nest --path myService``` 20 | 21 | 22 | ## Examples 23 | See the following article for a step-to-step guide how to use this template https://medium.com/dev-genius/how-to-build-a-serverless-rest-api-with-nestjs-and-dynamodb-7b58b5b59bf6 24 | 25 | 26 | -------------------------------------------------------------------------------- /online-library-oss/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /online-library-oss/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "ts", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src" 5 | } 6 | -------------------------------------------------------------------------------- /online-library-oss/nodemon-debug.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src"], 3 | "ext": "ts", 4 | "ignore": ["src/**/*.spec.ts"], 5 | "exec": "node --inspect-brk -r ts-node/register -r tsconfig-paths/register src/main.ts" 6 | } 7 | -------------------------------------------------------------------------------- /online-library-oss/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src"], 3 | "ext": "ts", 4 | "ignore": ["src/**/*.spec.ts"], 5 | "exec": "ts-node -r tsconfig-paths/register src/main.ts" 6 | } 7 | -------------------------------------------------------------------------------- /online-library-oss/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "online-library-oss", 3 | "version": "1.0.0", 4 | "description": "Serverless REST API service with OSS- https://github.com/oss-serverless/serverless", 5 | "author": "cyberworkz", 6 | "scripts": { 7 | "build": "tsc -p tsconfig.build.json", 8 | "format": "prettier --write \"src/**/*.ts\"", 9 | "start": "sls offline start", 10 | "lint": "tslint -p tsconfig.json -c tslint.json", 11 | "test": "jest", 12 | "test:watch": "jest --watch", 13 | "test:cov": "jest --coverage", 14 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 15 | "test:e2e": "jest --config ./test/jest-e2e.json", 16 | "deploy": "sls deploy --stage dev" 17 | }, 18 | "dependencies": { 19 | "@codegenie/serverless-express": "^4.13.0", 20 | "@nestjs/common": "^10.2.7", 21 | "@nestjs/core": "^10.2.7", 22 | "@nestjs/passport": "^10.0.3", 23 | "@nestjs/platform-express": "^10.2.7", 24 | "@types/uuid": "^8.3.0", 25 | "reflect-metadata": "^0.1.12", 26 | "rimraf": "^2.6.2", 27 | "rxjs": "^7.8.1", 28 | "supertest": "^6.3.3", 29 | "typescript-collections": "^1.3.3" 30 | }, 31 | "devDependencies": { 32 | "@nestjs/testing": "^10.2.7", 33 | "@types/aws-lambda": "^8.10.72", 34 | "@types/express": "^4.17.8", 35 | "@types/jest": "^23.3.1", 36 | "@types/node": "^10.7.1", 37 | "acorn": "^8.0.5", 38 | "jest": "^23.5.0", 39 | "nodemon": "^1.18.3", 40 | "prettier": "^1.14.2", 41 | "serverless-offline": "^13.3.2", 42 | "serverless-plugin-optimize": "^4.0.2-rc.1", 43 | "serverless-plugin-typescript": "2.1.5", 44 | "serverless-prune-plugin": "^2.0.1", 45 | "serverless-stage-manager": "^1.0.5", 46 | "ts-jest": "^23.1.3", 47 | "ts-loader": "^8.0.17", 48 | "ts-node": "^9.0.0", 49 | "tsconfig-paths": "^3.9.0", 50 | "typescript": "^4.1.5" 51 | }, 52 | "jest": { 53 | "moduleFileExtensions": [ 54 | "js", 55 | "json", 56 | "ts" 57 | ], 58 | "rootDir": "src", 59 | "testRegex": ".spec.ts$", 60 | "transform": { 61 | "^.+\\.(t|j)s$": "ts-jest" 62 | }, 63 | "coverageDirectory": "../coverage", 64 | "testEnvironment": "node" 65 | } 66 | } -------------------------------------------------------------------------------- /online-library-oss/serverless.yml: -------------------------------------------------------------------------------- 1 | service: online-library-oss 2 | 3 | plugins: 4 | - serverless-plugin-typescript 5 | - serverless-plugin-optimize 6 | - serverless-offline 7 | - serverless-stage-manager 8 | provider: 9 | name: aws 10 | tracing: 11 | apiGateway: true 12 | runtime: nodejs18.x 13 | memorySize: 128 # optional, in MB, default is 1024 14 | stage: ${opt:stage, 'dev'} 15 | region: eu-west-1 16 | # Valid values: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-logs-loggroup.html 17 | logRetentionInDays: 365 18 | 19 | 20 | package: 21 | exclude: 22 | - .gitignore 23 | - README.md 24 | - serverless.yml 25 | - nest-cli.json 26 | - .prettierrc 27 | excludeDevDependencies: true 28 | individually: true 29 | 30 | functions: 31 | main: 32 | handler: src/main.handler 33 | reservedConcurrency: 5 # default, change according to your needs 34 | layers: # AWS Powertools layer for EMF metrics 35 | - arn:aws:lambda:eu-west-1:094274105915:layer:AWSLambdaPowertoolsTypeScript:28 36 | events: 37 | - http: 38 | method: any 39 | path: /{proxy+} 40 | cors: true 41 | 42 | custom: 43 | stages: 44 | - dev 45 | - test 46 | - acc 47 | - prod 48 | 49 | 50 | -------------------------------------------------------------------------------- /online-library-oss/src/app.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | 5 | describe('AppController', () => { 6 | let app: TestingModule; 7 | 8 | beforeAll(async () => { 9 | app = await Test.createTestingModule({ 10 | controllers: [AppController], 11 | providers: [AppService], 12 | }).compile(); 13 | }); 14 | 15 | describe('root', () => { 16 | it('should return "Hello World!"', () => { 17 | const appController = app.get(AppController); 18 | expect(appController.getHello()).toBe('Hello World!'); 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /online-library-oss/src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { AppService } from './app.service'; 3 | 4 | @Controller() 5 | export class AppController { 6 | constructor(private readonly appService: AppService) {} 7 | 8 | @Get('hello') 9 | getHello(): string { 10 | return this.appService.getHello(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /online-library-oss/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | 5 | @Module({ 6 | imports: [], 7 | controllers: [AppController], 8 | providers: [AppService], 9 | }) 10 | export class AppModule {} 11 | -------------------------------------------------------------------------------- /online-library-oss/src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService { 5 | getHello(): string { 6 | return 'Hello World!'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /online-library-oss/src/main.ts: -------------------------------------------------------------------------------- 1 | import { Handler, Context } from 'aws-lambda'; 2 | 3 | import {configure as serverlessExpress} from '@codegenie/serverless-express'; 4 | 5 | import { NestFactory } from '@nestjs/core'; 6 | import { AppModule } from './app.module'; 7 | 8 | let cachedServer: Handler; 9 | 10 | process.on('unhandledRejection', (reason) => { 11 | // tslint:disable-next-line:no-console 12 | console.error(reason); 13 | }); 14 | 15 | process.on('uncaughtException', (reason) => { 16 | // tslint:disable-next-line:no-console 17 | console.error(reason); 18 | }); 19 | 20 | async function bootstrapServer() { 21 | // if (!cachedServer) { 22 | const nestApp = await NestFactory.create(AppModule, { 23 | logger: ['debug'], 24 | }); 25 | 26 | nestApp.enableCors(); 27 | await nestApp.init(); 28 | cachedServer = serverlessExpress({ app: nestApp.getHttpAdapter().getInstance()}); 29 | //} 30 | return cachedServer; 31 | } 32 | 33 | export const handler = async (event: any, context: Context, callback: any) => { 34 | const server = await bootstrapServer(); 35 | return server(event, context, callback); 36 | }; 37 | -------------------------------------------------------------------------------- /online-library-oss/test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { INestApplication } from '@nestjs/common'; 2 | import { Test } from '@nestjs/testing'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeAll(async () => { 10 | const moduleFixture = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/hello (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/hello') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /online-library-oss/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /online-library-oss/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src/**/*"], 4 | "exclude": ["node_modules", "**/*.spec.ts"] 5 | } 6 | -------------------------------------------------------------------------------- /online-library-oss/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "noImplicitAny": false, 6 | "removeComments": true, 7 | "noLib": false, 8 | "allowSyntheticDefaultImports": true, 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "target": "es6", 12 | "sourceMap": true, 13 | "outDir": "./dist", 14 | "baseUrl": "./" 15 | }, 16 | "exclude": ["node_modules"] 17 | } 18 | -------------------------------------------------------------------------------- /online-library-oss/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "types": ["jest", "node"] 5 | }, 6 | "include": ["**/*.spec.ts", "**/*.d.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /online-library-oss/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": ["tslint:recommended"], 4 | "jsRules": { 5 | "no-unused-expression": true 6 | }, 7 | "rules": { 8 | "quotemark": [true, "single"], 9 | "member-access": [false], 10 | "ordered-imports": [false], 11 | "max-line-length": [true, 150], 12 | "member-ordering": [false], 13 | "interface-name": [false], 14 | "arrow-parens": false, 15 | "object-literal-sort-keys": false 16 | }, 17 | "rulesDirectory": [] 18 | } 19 | -------------------------------------------------------------------------------- /online-library-stream-filter/.gitignore: -------------------------------------------------------------------------------- 1 | # package directories 2 | node_modules 3 | jspm_packages 4 | 5 | # Serverless directories 6 | .serverless 7 | 8 | # Webpack directories 9 | .webpack 10 | /email-service.iml 11 | /.idea/ 12 | -------------------------------------------------------------------------------- /online-library-stream-filter/README.md: -------------------------------------------------------------------------------- 1 | 9 | # Serverless DynamoDB Streams template 10 | 11 | This serverless template enables you to handle DynamoDB Stream events with AWS Lambda using Serverless and TypeScript. 12 | 13 | ## Use Cases 14 | - Side effects on INSERT or UPDATE of a DynamoDB record. 15 | - Audit events. 16 | - Communicate DynamoDB data changes to other external services. 17 | 18 | ## Setup 19 | 1. Install serverless - ```npm install -g serverless``` 20 | 2. Use serverless to create a project with template-url. For example 21 | ```serverless create --template-url https://github.com/cyberworkz/serverless-templates/tree/main/aws-nodejs-typescript-dynamodb-streams --path myService``` 22 | 23 | 24 | -------------------------------------------------------------------------------- /online-library-stream-filter/handlerAuthors.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; // for TypeDI 2 | 3 | export { default as processorAuthors} from './src/processorAuthors'; 4 | -------------------------------------------------------------------------------- /online-library-stream-filter/handlerBooks.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; // for TypeDI 2 | 3 | export { default as processorBooks} from './src/processorBooks'; 4 | -------------------------------------------------------------------------------- /online-library-stream-filter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "online-library-stream", 3 | "version": "0.0.0", 4 | "description": "Serverless example AWS DynamoDB Streams event with TypeScript", 5 | "main": "handler.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "lint": "tslint -p tsconfig.json -c tslint.json", 9 | "deploy-dev": "sls deploy --stage dev" 10 | }, 11 | "dependencies": { 12 | "@types/lambda-log": "^2.2.0", 13 | "algoliasearch": "^4.13.1", 14 | "aws-lambda": "^0.1.2", 15 | "aws-sdk": "^2.859.0", 16 | "lambda": "^0.10.3", 17 | "log": "^6.0.0", 18 | "source-map-support": "^0.5.10", 19 | "typedi": "^0.10.0" 20 | }, 21 | "devDependencies": { 22 | "@types/aws-lambda": "^8.10.72", 23 | "@types/node": "^10.17.55", 24 | "eslint": "^6.6.0", 25 | "serverless-webpack": "^5.2.0", 26 | "ts-loader": "^5.3.3", 27 | "tslint": "^5.20.0", 28 | "tslint-config-airbnb": "^5.11.2", 29 | "typescript": "^3.9.9", 30 | "webpack": "^4.46.0" 31 | }, 32 | "author": "Haiko van der Schaaf (https://github.com/cyberworkz)", 33 | "license": "MIT" 34 | } 35 | -------------------------------------------------------------------------------- /online-library-stream-filter/serverless.yml: -------------------------------------------------------------------------------- 1 | service: online-library-stream-filter 2 | 3 | plugins: 4 | - serverless-webpack 5 | 6 | provider: 7 | name: aws 8 | runtime: nodejs14.x 9 | memorySize: 128 # optional, in MB, default is 1024 10 | timeout: 10 # optional, in seconds, default is 6 11 | stage: ${opt:stage, 'dev'} 12 | region: eu-west-1 13 | 14 | functions: 15 | bookExporter: 16 | handler: handlerBooks.processorBooks 17 | events: 18 | - stream: 19 | type: dynamodb 20 | arn: ${self:custom.stream_arn} 21 | maximumRetryAttempts: 1 22 | filterPatterns: 23 | - eventName: [INSERT, MODIFY] 24 | dynamodb: 25 | NewImage: 26 | TYPE: 27 | S: [BOOK] 28 | authorExporter: 29 | handler: handlerAuthors.processorAuthors 30 | events: 31 | - stream: 32 | type: dynamodb 33 | arn: ${self:custom.stream_arn} 34 | maximumRetryAttempts: 1 35 | filterPatterns: 36 | - eventName: [INSERT, MODIFY] 37 | dynamodb: 38 | NewImage: 39 | TYPE: 40 | S: [AUTHOR] 41 | 42 | custom: 43 | stream_arn: [arn-stream] 44 | stages: 45 | - dev 46 | - test 47 | - acc 48 | - prod 49 | -------------------------------------------------------------------------------- /online-library-stream-filter/src/export-search.service.ts: -------------------------------------------------------------------------------- 1 | import {Service} from "typedi"; 2 | import algoliasearch, {SearchClient} from "algoliasearch"; 3 | import { Book } from "./model/book"; 4 | import { Author } from "./model/author"; 5 | 6 | const applicationId = process.env.APPLICATION_ID; 7 | const adminAPIKey = process.env.ADMIN_API_KEY; 8 | 9 | @Service() 10 | export class ExportService { 11 | 12 | searchClient: SearchClient; 13 | 14 | constructor() { 15 | this.searchClient = algoliasearch(applicationId, adminAPIKey); 16 | } 17 | 18 | async processBook(book: Book) { 19 | 20 | console.log('adding book'); 21 | book.objectID = book.PK; 22 | 23 | let bookIndex = this.searchClient.initIndex("dev_online_library"); 24 | 25 | await bookIndex.saveObject(book).then((objectID) => { 26 | console.log(objectID); 27 | }) 28 | } 29 | 30 | async processAuthor(author: Author) { 31 | 32 | console.log('adding author'); 33 | author.objectID = author.PK; 34 | 35 | let authorIndex = this.searchClient.initIndex("dev_online_library_author"); 36 | 37 | await authorIndex.saveObject(author).then((objectID) => { 38 | console.log(objectID); 39 | }) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /online-library-stream-filter/src/model/author.ts: -------------------------------------------------------------------------------- 1 | export interface Author { 2 | objectID: string; 3 | PK: string; 4 | SK: string; 5 | firstName: string; 6 | lastName: string; 7 | TYPE: string; 8 | } -------------------------------------------------------------------------------- /online-library-stream-filter/src/model/book.ts: -------------------------------------------------------------------------------- 1 | export interface Book { 2 | objectID: string; 3 | PK: string; 4 | SK: string; 5 | Title: string; 6 | Author: string; 7 | TYPE: string; 8 | } -------------------------------------------------------------------------------- /online-library-stream-filter/src/processorAuthors.ts: -------------------------------------------------------------------------------- 1 | import {DynamoDBStreamEvent, DynamoDBStreamHandler} from 'aws-lambda'; 2 | import {Container} from "typedi"; 3 | import { ExportService } from './export-search.service'; 4 | import {Converter} from "aws-sdk/clients/dynamodb"; 5 | import { Author } from './model/author'; 6 | 7 | const processor: DynamoDBStreamHandler= async (event: DynamoDBStreamEvent) => { 8 | // tslint:disable-next-line:prefer-template 9 | console.log('received event on ' + new Date().toDateString()); 10 | 11 | const exportService = Container.get(ExportService); 12 | 13 | try { 14 | for (const record of event.Records) { 15 | const source: string = record.eventSource; 16 | const newImage = record.dynamodb.NewImage 17 | console.log(' source--> ', source); 18 | console.log('new image \n' + newImage) 19 | console.log('keys \n' + record.dynamodb.Keys); 20 | 21 | let author: Author = Converter.unmarshall(record.dynamodb.NewImage) as Author; 22 | await exportService.processAuthor(author); 23 | } 24 | } catch (error) { 25 | console.log(error); 26 | } 27 | }; 28 | 29 | export default processor; 30 | -------------------------------------------------------------------------------- /online-library-stream-filter/src/processorBooks.ts: -------------------------------------------------------------------------------- 1 | import {DynamoDBStreamEvent, DynamoDBStreamHandler} from 'aws-lambda'; 2 | import {Container} from "typedi"; 3 | import { ExportService } from './export-search.service'; 4 | import {Converter} from "aws-sdk/clients/dynamodb"; 5 | import { Book } from './model/book'; 6 | 7 | const processor: DynamoDBStreamHandler= async (event: DynamoDBStreamEvent) => { 8 | // tslint:disable-next-line:prefer-template 9 | console.log('received event on ' + new Date().toDateString()); 10 | 11 | const exportService = Container.get(ExportService); 12 | 13 | try { 14 | for (const record of event.Records) { 15 | const source: string = record.eventSource; 16 | const newImage = record.dynamodb.NewImage 17 | console.log(' source--> ', source); 18 | console.log('new image \n' + newImage) 19 | console.log('keys \n' + record.dynamodb.Keys); 20 | 21 | let book: Book = Converter.unmarshall(record.dynamodb.NewImage) as Book; 22 | 23 | await exportService.processBook(book); 24 | } 25 | } catch (error) { 26 | console.log(error); 27 | } 28 | }; 29 | 30 | export default processor; 31 | -------------------------------------------------------------------------------- /online-library-stream-filter/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "esModuleInterop": true, 5 | "lib": ["esnext"], 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "noUnusedLocals": true, 9 | "noUnusedParameters": true, 10 | "outDir": "lib", 11 | "sourceMap": true, 12 | "target": "esnext", 13 | "experimentalDecorators": true, 14 | "emitDecoratorMetadata": true 15 | }, 16 | "exclude": ["node_modules"] 17 | } 18 | -------------------------------------------------------------------------------- /online-library-stream-filter/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint-config-airbnb"] 3 | } 4 | -------------------------------------------------------------------------------- /online-library-stream-filter/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const slsw = require('serverless-webpack'); 3 | 4 | module.exports = { 5 | mode: slsw.lib.webpack.isLocal ? 'development' : 'production', 6 | entry: slsw.lib.entries, 7 | devtool: 'source-map', 8 | resolve: { 9 | extensions: ['.js', '.jsx', '.json', '.ts', '.tsx'], 10 | }, 11 | output: { 12 | libraryTarget: 'commonjs', 13 | path: path.join(__dirname, '.webpack'), 14 | filename: '[name].js', 15 | }, 16 | target: 'node', 17 | module: { 18 | rules: [ 19 | // all files with a `.ts` or `.tsx` extension will be handled by `ts-loader` 20 | { test: /\.tsx?$/, loader: 'ts-loader' }, 21 | ], 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /online-library-stream/.gitignore: -------------------------------------------------------------------------------- 1 | # package directories 2 | node_modules 3 | jspm_packages 4 | 5 | # Serverless directories 6 | .serverless 7 | 8 | # Webpack directories 9 | .webpack 10 | /email-service.iml 11 | /.idea/ 12 | -------------------------------------------------------------------------------- /online-library-stream/README.md: -------------------------------------------------------------------------------- 1 | 9 | # Serverless DynamoDB Streams template 10 | 11 | This serverless template enables you to handle DynamoDB Stream events with AWS Lambda using Serverless and TypeScript. 12 | 13 | ## Use Cases 14 | - Side effects on INSERT or UPDATE of a DynamoDB record. 15 | - Audit events. 16 | - Communicate DynamoDB data changes to other external services. 17 | 18 | ## Setup 19 | 1. Install serverless - ```npm install -g serverless``` 20 | 2. Use serverless to create a project with template-url. For example 21 | ```serverless create --template-url https://github.com/cyberworkz/serverless-templates/tree/main/aws-nodejs-typescript-dynamodb-streams --path myService``` 22 | 23 | 24 | -------------------------------------------------------------------------------- /online-library-stream/handler.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; // for TypeDI 2 | 3 | export { default as processor} from './src/processor'; 4 | -------------------------------------------------------------------------------- /online-library-stream/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "online-library-stream", 3 | "version": "0.0.0", 4 | "description": "Serverless example AWS DynamoDB Streams event with TypeScript", 5 | "main": "handler.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "lint": "tslint -p tsconfig.json -c tslint.json", 9 | "deploy-dev": "sls deploy --stage dev" 10 | }, 11 | "dependencies": { 12 | "@types/lambda-log": "^2.2.0", 13 | "algoliasearch": "^4.13.1", 14 | "aws-lambda": "^0.1.2", 15 | "aws-sdk": "^2.859.0", 16 | "lambda": "^0.10.3", 17 | "log": "^6.0.0", 18 | "source-map-support": "^0.5.10", 19 | "typedi": "^0.10.0" 20 | }, 21 | "devDependencies": { 22 | "@types/aws-lambda": "^8.10.72", 23 | "@types/node": "^10.17.55", 24 | "eslint": "^6.6.0", 25 | "serverless-webpack": "^5.2.0", 26 | "ts-loader": "^5.3.3", 27 | "tslint": "^5.20.0", 28 | "tslint-config-airbnb": "^5.11.2", 29 | "typescript": "^3.9.9", 30 | "webpack": "^4.46.0" 31 | }, 32 | "author": "Haiko van der Schaaf (https://github.com/cyberworkz)", 33 | "license": "MIT" 34 | } 35 | -------------------------------------------------------------------------------- /online-library-stream/serverless.yml: -------------------------------------------------------------------------------- 1 | service: online-library-stream 2 | 3 | plugins: 4 | - serverless-webpack 5 | 6 | provider: 7 | name: aws 8 | runtime: nodejs14.x 9 | memorySize: 128 # optional, in MB, default is 1024 10 | timeout: 10 # optional, in seconds, default is 6 11 | stage: ${opt:stage, 'dev'} 12 | region: eu-west-1 13 | 14 | functions: 15 | bookExporter: 16 | handler: handler.processor 17 | events: 18 | - stream: 19 | type: dynamodb 20 | arn: ${self:custom.stream_arn} 21 | 22 | custom: 23 | stream_arn: [arn-db-stream] 24 | stages: 25 | - dev 26 | - test 27 | - acc 28 | - prod 29 | -------------------------------------------------------------------------------- /online-library-stream/src/export-search.service.ts: -------------------------------------------------------------------------------- 1 | import {Service} from "typedi"; 2 | import {DynamoDBRecord} from "aws-lambda"; 3 | import {Converter} from "aws-sdk/clients/dynamodb"; 4 | import algoliasearch, {SearchClient} from "algoliasearch"; 5 | import { Book } from "./model/book"; 6 | 7 | const applicationId = process.env.APPLICATION_ID; 8 | const adminAPIKey = process.env.ADMIN_API_KEY; 9 | 10 | @Service() 11 | export class ExportService { 12 | 13 | searchClient: SearchClient; 14 | 15 | constructor() { 16 | this.searchClient = algoliasearch(applicationId, adminAPIKey); 17 | } 18 | 19 | async processRecord(record: DynamoDBRecord) { 20 | console.log("incoming record", record); 21 | let book: Book = Converter.unmarshall(record.dynamodb.NewImage) as Book; 22 | 23 | console.log('adding book'); 24 | book.objectID = book.PK; 25 | 26 | let bookIndex = this.searchClient.initIndex("dev_online_library"); 27 | 28 | await bookIndex.saveObject(book).then((objectID) => { 29 | console.log(objectID); 30 | }) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /online-library-stream/src/model/book.ts: -------------------------------------------------------------------------------- 1 | export interface Book { 2 | objectID: string; 3 | PK: string; 4 | Title: string; 5 | Author: string; 6 | } -------------------------------------------------------------------------------- /online-library-stream/src/processor.ts: -------------------------------------------------------------------------------- 1 | import {DynamoDBStreamEvent, DynamoDBStreamHandler} from 'aws-lambda'; 2 | import {Container} from "typedi"; 3 | import { ExportService } from './export-search.service'; 4 | 5 | const processor: DynamoDBStreamHandler= async (event: DynamoDBStreamEvent) => { 6 | // tslint:disable-next-line:prefer-template 7 | console.log('received event on ' + new Date().toDateString()); 8 | 9 | const exportService = Container.get(ExportService); 10 | 11 | try { 12 | for (const record of event.Records) { 13 | const source: string = record.eventSource; 14 | const newImage = record.dynamodb.NewImage 15 | console.log(' source--> ', source); 16 | console.log('new image \n' + newImage) 17 | 18 | await exportService.processRecord(record); 19 | } 20 | } catch (error) { 21 | console.log(error); 22 | } 23 | }; 24 | 25 | export default processor; 26 | -------------------------------------------------------------------------------- /online-library-stream/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "esModuleInterop": true, 5 | "lib": ["esnext"], 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "noUnusedLocals": true, 9 | "noUnusedParameters": true, 10 | "outDir": "lib", 11 | "sourceMap": true, 12 | "target": "esnext", 13 | "experimentalDecorators": true, 14 | "emitDecoratorMetadata": true 15 | }, 16 | "exclude": ["node_modules"] 17 | } 18 | -------------------------------------------------------------------------------- /online-library-stream/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint-config-airbnb"] 3 | } 4 | -------------------------------------------------------------------------------- /online-library-stream/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const slsw = require('serverless-webpack'); 3 | 4 | module.exports = { 5 | mode: slsw.lib.webpack.isLocal ? 'development' : 'production', 6 | entry: slsw.lib.entries, 7 | devtool: 'source-map', 8 | resolve: { 9 | extensions: ['.js', '.jsx', '.json', '.ts', '.tsx'], 10 | }, 11 | output: { 12 | libraryTarget: 'commonjs', 13 | path: path.join(__dirname, '.webpack'), 14 | filename: '[name].js', 15 | }, 16 | target: 'node', 17 | module: { 18 | rules: [ 19 | // all files with a `.ts` or `.tsx` extension will be handled by `ts-loader` 20 | { test: /\.tsx?$/, loader: 'ts-loader' }, 21 | ], 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /online-library/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /.build/ 3 | /.serverless/ 4 | /_warmup/ 5 | /.idea/ 6 | -------------------------------------------------------------------------------- /online-library/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } 5 | -------------------------------------------------------------------------------- /online-library/README.md: -------------------------------------------------------------------------------- 1 | 9 | # Serverless NestJS REST API + DynamoDB example 10 | This example demonstrates you how to run a NestJS REST API with AWS Lambda using Serverless and TypeScript with DynamoDB as datastore. See the story on [Medium](https://serverlesscorner.com/how-to-build-a-serverless-rest-api-with-nestjs-and-dynamodb-7b58b5b59bf6) for more explanation. 11 | 12 | ## Use Cases 13 | - REST API + DynamDB 14 | -------------------------------------------------------------------------------- /online-library/iam/OnlineLibraryIAM.yml: -------------------------------------------------------------------------------- 1 | OnlineLibraryService: 2 | - Effect: Allow 3 | Action: 4 | - dynamodb:PutItem 5 | - dynamodb:Scan 6 | - dynamodb:GetItem 7 | - dynamodb:UpdateItem 8 | - dynamodb:Query 9 | - dynamodb:ConditionCheckItem 10 | Resource: 11 | - arn:aws:dynamodb:eu-west-1:184690513648:table/online-library -------------------------------------------------------------------------------- /online-library/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /online-library/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "ts", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src" 5 | } 6 | -------------------------------------------------------------------------------- /online-library/nodemon-debug.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src"], 3 | "ext": "ts", 4 | "ignore": ["src/**/*.spec.ts"], 5 | "exec": "node --inspect-brk -r ts-node/register -r tsconfig-paths/register src/main.ts" 6 | } 7 | -------------------------------------------------------------------------------- /online-library/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src"], 3 | "ext": "ts", 4 | "ignore": ["src/**/*.spec.ts"], 5 | "exec": "ts-node -r tsconfig-paths/register src/main.ts" 6 | } 7 | -------------------------------------------------------------------------------- /online-library/online-library.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /online-library/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "online-library", 3 | "version": "1.0.0", 4 | "description": "Serverless REST API service", 5 | "author": "cyberworkz", 6 | "scripts": { 7 | "build": "tsc -p tsconfig.build.json", 8 | "format": "prettier --write \"src/**/*.ts\"", 9 | "start": "sls offline start", 10 | "lint": "tslint -p tsconfig.json -c tslint.json", 11 | "test": "jest", 12 | "test:watch": "jest --watch", 13 | "test:cov": "jest --coverage", 14 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 15 | "test:e2e": "jest --config ./test/jest-e2e.json", 16 | "deploy": "sls deploy --stage dev" 17 | }, 18 | "dependencies": { 19 | "@nestjs/common": "^8.2.1", 20 | "@nestjs/core": "^8.2.1", 21 | "@nestjs/platform-express": "^8.2.1", 22 | "aws-sdk": "^2.789.0", 23 | "aws-serverless-express": "^3.3.5", 24 | "reflect-metadata": "^0.1.12", 25 | "rimraf": "^2.6.2", 26 | "rxjs": "^6.2.2" 27 | }, 28 | "devDependencies": { 29 | "@nestjs/testing": "^5.1.0", 30 | "@types/aws-lambda": "^8.10.15", 31 | "@types/aws-serverless-express": "^3.3.5", 32 | "@types/express": "^4.16.0", 33 | "@types/jest": "^23.3.1", 34 | "@types/node": "^10.7.1", 35 | "@types/supertest": "^2.0.5", 36 | "acorn": "^8.0.5", 37 | "jest": "^23.5.0", 38 | "nodemon": "^1.18.3", 39 | "prettier": "^1.14.2", 40 | "serverless-offline": "^6.8.0", 41 | "serverless-plugin-typescript": "1.1.7", 42 | "serverless-stage-manager": "^1.0.5", 43 | "supertest": "^3.1.0", 44 | "ts-jest": "^23.1.3", 45 | "ts-loader": "^4.4.2", 46 | "ts-node": "^7.0.1", 47 | "tsconfig-paths": "^3.5.0", 48 | "tslint": "5.11.0", 49 | "typescript": "^4.1.5" 50 | }, 51 | "jest": { 52 | "moduleFileExtensions": [ 53 | "js", 54 | "json", 55 | "ts" 56 | ], 57 | "rootDir": "src", 58 | "testRegex": ".spec.ts$", 59 | "transform": { 60 | "^.+\\.(t|j)s$": "ts-jest" 61 | }, 62 | "coverageDirectory": "../coverage", 63 | "testEnvironment": "node" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /online-library/serverless.yml: -------------------------------------------------------------------------------- 1 | service: online-library 2 | 3 | plugins: 4 | - serverless-plugin-typescript 5 | - serverless-offline 6 | - serverless-stage-manager 7 | provider: 8 | name: aws 9 | tracing: 10 | apiGateway: true 11 | runtime: nodejs14.x 12 | stage: ${opt:stage, 'dev'} 13 | region: eu-west-1 14 | iamRoleStatements: 15 | ${file(iam/OnlineLibraryIAM.yml):OnlineLibraryService} 16 | 17 | 18 | 19 | package: 20 | exclude: 21 | - .gitignore 22 | - README.md 23 | - serverless.yml 24 | - nest-cli.json 25 | - .prettierrc 26 | excludeDevDependencies: true 27 | individually: true 28 | 29 | functions: 30 | main: 31 | handler: src/main.handler 32 | events: 33 | - http: 34 | method: any 35 | path: /{proxy+} 36 | cors: true 37 | 38 | custom: 39 | stages: 40 | - dev 41 | - test 42 | - acc 43 | - prod 44 | -------------------------------------------------------------------------------- /online-library/src/app.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | 5 | describe('AppController', () => { 6 | let app: TestingModule; 7 | 8 | beforeAll(async () => { 9 | app = await Test.createTestingModule({ 10 | controllers: [AppController], 11 | providers: [AppService], 12 | }).compile(); 13 | }); 14 | 15 | describe('root', () => { 16 | it('should return "Hello World!"', () => { 17 | const appController = app.get(AppController); 18 | expect(appController.getHello()).toBe('Hello World!'); 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /online-library/src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { AppService } from './app.service'; 3 | 4 | @Controller() 5 | export class AppController { 6 | constructor(private readonly appService: AppService) {} 7 | 8 | @Get('hello') 9 | getHello(): string { 10 | return this.appService.getHello(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /online-library/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | import { BooksController } from './books/books.controller'; 5 | import { BooksRepository } from './books/books.repository'; 6 | import { BooksService } from './books/books.service'; 7 | import { ExceptionsLoggerFilter } from './utils/ExceptionsLogFilter'; 8 | import { APP_FILTER } from '@nestjs/core'; 9 | 10 | @Module({ 11 | imports: [], 12 | controllers: [AppController, BooksController], 13 | providers: [AppService, BooksService, BooksRepository, 14 | { provide: APP_FILTER, 15 | useClass: ExceptionsLoggerFilter, 16 | } 17 | ], 18 | }) 19 | export class AppModule {} 20 | -------------------------------------------------------------------------------- /online-library/src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService { 5 | getHello(): string { 6 | return 'Hello World!'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /online-library/src/books/books.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { BooksController } from './books.controller'; 3 | 4 | describe('BooksController', () => { 5 | let controller: BooksController; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | controllers: [BooksController], 10 | }).compile(); 11 | 12 | controller = module.get(BooksController); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(controller).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /online-library/src/books/books.controller.ts: -------------------------------------------------------------------------------- 1 | import {Controller, Get, HttpStatus, Param, Res} from '@nestjs/common'; 2 | import {BooksService} from "./books.service"; 3 | import {IBook} from "./ibook"; 4 | 5 | @Controller('books') 6 | export class BooksController { 7 | 8 | constructor(private bookService: BooksService){} 9 | 10 | @Get("/:isbn") 11 | async getBookByISBN(@Param('isbn') isbn:number, @Res() res: any){ 12 | const book: object = await this.bookService.getBook(isbn); 13 | return res.status(HttpStatus.OK).json(book); 14 | } 15 | 16 | @Get("/author/:lastName/:firstName") 17 | async getBooksByAuthor(@Param('lastName')lastName: string, @Param('firstName')firstName: string, @Res() res: any) { 18 | const books: any[] = await this.bookService.getAuthorBooks(lastName, firstName); 19 | return res.status(HttpStatus.OK).json(books); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /online-library/src/books/books.repository.spec.ts: -------------------------------------------------------------------------------- 1 | import { BooksRepository } from './books.repository'; 2 | 3 | describe('BooksRepository', () => { 4 | it('should be defined', () => { 5 | expect(new BooksRepository()).toBeDefined(); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /online-library/src/books/books.repository.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, InternalServerErrorException, NotFoundException } from "@nestjs/common"; 2 | import { DocumentClient } from "aws-sdk/clients/dynamodb"; 3 | import * as AWS from 'aws-sdk'; 4 | 5 | @Injectable() 6 | export class BooksRepository { 7 | 8 | private tableName: string; 9 | private db: DocumentClient; 10 | 11 | private bookPrefix = 'BOOK#'; 12 | private authorPrefix = 'AUTH#' 13 | 14 | 15 | constructor() { 16 | this.tableName = 'online-library'; 17 | this.db = new AWS.DynamoDB.DocumentClient(); 18 | } 19 | 20 | 21 | async getBook(isbn: number) { 22 | let book: object; 23 | 24 | try { 25 | const result = await this.db 26 | .get({ 27 | TableName: this.tableName, 28 | Key: { PK: this.bookPrefix.concat(String(isbn)), 29 | SK: this.bookPrefix.concat(String(isbn))}, 30 | }) 31 | .promise(); 32 | 33 | book = result.Item; 34 | } catch (error) { 35 | throw new InternalServerErrorException(error); 36 | } 37 | 38 | if (!book) { 39 | throw new NotFoundException(`Book with ISBN "${isbn}" not found`); 40 | } 41 | 42 | return book; 43 | } 44 | 45 | async getBooksByAuthor(lastName: string, firstName: string) { 46 | let books = []; 47 | 48 | try { 49 | const result = await this.db 50 | .query({ 51 | TableName: this.tableName, 52 | KeyConditionExpression: '#PK=:PK AND begins_with(#SK, :SK)', 53 | ExpressionAttributeNames: { 54 | '#PK': 'PK', 55 | '#SK': 'SK' 56 | }, 57 | ExpressionAttributeValues: { 58 | ':PK': this.authorPrefix.concat(lastName.toUpperCase()).concat("_").concat(firstName.toUpperCase()), 59 | ':SK': this.bookPrefix 60 | }, 61 | ScanIndexForward: false, 62 | Limit: 100 63 | }) 64 | .promise(); 65 | books = result.Items; 66 | } catch (error) { 67 | throw new InternalServerErrorException(error); 68 | } 69 | 70 | return books; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /online-library/src/books/books.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { BooksService } from './books.service'; 3 | 4 | describe('BooksService', () => { 5 | let service: BooksService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [BooksService], 10 | }).compile(); 11 | 12 | service = module.get(BooksService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /online-library/src/books/books.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import {BooksRepository} from "./books.repository"; 3 | 4 | @Injectable() 5 | export class BooksService { 6 | 7 | constructor(private bookRepo: BooksRepository) {} 8 | 9 | async getBook(isbn: number) { 10 | return await this.bookRepo.getBook(isbn); 11 | } 12 | 13 | async getAuthorBooks(lastName: string, firstName: string) { 14 | return await this.bookRepo.getBooksByAuthor(lastName, firstName); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /online-library/src/books/iauthor.ts: -------------------------------------------------------------------------------- 1 | export interface IAuthor { 2 | 3 | firstName: string; 4 | lastName: string; 5 | } 6 | -------------------------------------------------------------------------------- /online-library/src/books/ibook.ts: -------------------------------------------------------------------------------- 1 | import {IAuthor} from "./iauthor"; 2 | 3 | export interface IBook { 4 | 5 | isbn: number; 6 | title: string; 7 | author: IAuthor; 8 | reserved: boolean; 9 | edition: number; 10 | publisher: string 11 | 12 | } 13 | -------------------------------------------------------------------------------- /online-library/src/main.ts: -------------------------------------------------------------------------------- 1 | import { Handler, Context } from 'aws-lambda'; 2 | import { Server } from 'http'; 3 | import { createServer, proxy } from 'aws-serverless-express'; 4 | import { eventContext } from 'aws-serverless-express/middleware'; 5 | 6 | import { ExpressAdapter } from '@nestjs/platform-express'; 7 | import { NestFactory } from '@nestjs/core'; 8 | import { AppModule } from './app.module'; 9 | 10 | import * as express from 'express'; 11 | 12 | // NOTE: If you get ERR_CONTENT_DECODING_FAILED in your browser, this is likely 13 | // due to a compressed response (e.g. gzip) which has not been handled correctly 14 | // by aws-serverless-express and/or API Gateway. Add the necessary MIME types to 15 | // binaryMimeTypes below 16 | const binaryMimeTypes: string[] = []; 17 | 18 | let cachedServer: Server; 19 | 20 | process.on('unhandledRejection', (reason) => { 21 | // tslint:disable-next-line:no-console 22 | console.error(reason); 23 | }); 24 | 25 | process.on('uncaughtException', (reason) => { 26 | // tslint:disable-next-line:no-console 27 | console.error(reason); 28 | }); 29 | 30 | async function bootstrapServer(): Promise { 31 | if (!cachedServer) { 32 | try { 33 | const expressApp = express(); 34 | const nestApp = await NestFactory.create(AppModule, new ExpressAdapter(expressApp)); 35 | nestApp.enableCors(); 36 | nestApp.use(eventContext()); 37 | await nestApp.init(); 38 | cachedServer = createServer(expressApp, undefined, binaryMimeTypes); 39 | } 40 | catch (error) { 41 | return Promise.reject(error); 42 | } 43 | } 44 | return Promise.resolve(cachedServer); 45 | } 46 | 47 | export const handler: Handler = async (event: any, context: Context) => { 48 | cachedServer = await bootstrapServer(); 49 | return proxy(cachedServer, event, context, 'PROMISE').promise; 50 | } 51 | -------------------------------------------------------------------------------- /online-library/src/utils/ExceptionsLogFilter.ts: -------------------------------------------------------------------------------- 1 | import { Catch, ArgumentsHost } from '@nestjs/common'; 2 | import { BaseExceptionFilter } from '@nestjs/core'; 3 | 4 | @Catch() 5 | export class ExceptionsLoggerFilter extends BaseExceptionFilter { 6 | catch(exception: unknown, host: ArgumentsHost) { 7 | console.log('ERROR', exception); 8 | super.catch(exception, host); 9 | } 10 | } -------------------------------------------------------------------------------- /online-library/test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { INestApplication } from '@nestjs/common'; 2 | import { Test } from '@nestjs/testing'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeAll(async () => { 10 | const moduleFixture = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/hello (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/hello') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /online-library/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /online-library/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src/**/*"], 4 | "exclude": ["node_modules", "**/*.spec.ts"] 5 | } 6 | -------------------------------------------------------------------------------- /online-library/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "noImplicitAny": false, 6 | "removeComments": true, 7 | "noLib": false, 8 | "allowSyntheticDefaultImports": true, 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "target": "es6", 12 | "sourceMap": true, 13 | "outDir": "./dist", 14 | "baseUrl": "./" 15 | }, 16 | "exclude": ["node_modules"] 17 | } 18 | -------------------------------------------------------------------------------- /online-library/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "types": ["jest", "node"] 5 | }, 6 | "include": ["**/*.spec.ts", "**/*.d.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /online-library/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": ["tslint:recommended"], 4 | "jsRules": { 5 | "no-unused-expression": true 6 | }, 7 | "rules": { 8 | "quotemark": [true, "single"], 9 | "member-access": [false], 10 | "ordered-imports": [false], 11 | "max-line-length": [true, 150], 12 | "member-ordering": [false], 13 | "interface-name": [false], 14 | "arrow-parens": false, 15 | "object-literal-sort-keys": false 16 | }, 17 | "rulesDirectory": [] 18 | } 19 | -------------------------------------------------------------------------------- /quotes-restapi-service/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /.build/ 3 | /.serverless/ 4 | /_warmup/ 5 | /.idea/ 6 | -------------------------------------------------------------------------------- /quotes-restapi-service/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } 5 | -------------------------------------------------------------------------------- /quotes-restapi-service/README.md: -------------------------------------------------------------------------------- 1 | 9 | # Serverless NestJS REST API template 10 | 11 | This serverless template enables you to run a NestJS REST API with AWS Lambda using Serverless and TypeScript. 12 | 13 | ## Use Cases 14 | - REST API. 15 | 16 | ## Setup 17 | 1. Install serverless - ```npm install -g serverless``` 18 | 2. Use serverless to create a project with template-url. For example 19 | ```serverless create --template-url https://github.com/cyberworkz/serverless-templates/tree/main/aws-nodejs-typescript-restapi-nest --path myService``` 20 | 21 | 22 | -------------------------------------------------------------------------------- /quotes-restapi-service/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /quotes-restapi-service/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "ts", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src" 5 | } 6 | -------------------------------------------------------------------------------- /quotes-restapi-service/nodemon-debug.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src"], 3 | "ext": "ts", 4 | "ignore": ["src/**/*.spec.ts"], 5 | "exec": "node --inspect-brk -r ts-node/register -r tsconfig-paths/register src/main.ts" 6 | } 7 | -------------------------------------------------------------------------------- /quotes-restapi-service/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src"], 3 | "ext": "ts", 4 | "ignore": ["src/**/*.spec.ts"], 5 | "exec": "ts-node -r tsconfig-paths/register src/main.ts" 6 | } 7 | -------------------------------------------------------------------------------- /quotes-restapi-service/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "quotes-restapi-service", 3 | "version": "1.0.0", 4 | "description": "Serverless REST API service", 5 | "author": "cyberworkz", 6 | "scripts": { 7 | "build": "tsc -p tsconfig.build.json", 8 | "format": "prettier --write \"src/**/*.ts\"", 9 | "start": "sls offline start", 10 | "lint": "tslint -p tsconfig.json -c tslint.json", 11 | "test": "jest", 12 | "test:watch": "jest --watch", 13 | "test:cov": "jest --coverage", 14 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 15 | "test:e2e": "jest --config ./test/jest-e2e.json", 16 | "deploy": "sls deploy --stage dev" 17 | }, 18 | "dependencies": { 19 | "@nestjs/common": "^8.2.1", 20 | "@nestjs/core": "^8.2.1", 21 | "@nestjs/platform-express": "^8.2.1", 22 | "aws-sdk": "^2.789.0", 23 | "aws-serverless-express": "^3.3.5", 24 | "reflect-metadata": "^0.1.12", 25 | "rimraf": "^2.6.2", 26 | "rxjs": "^6.2.2" 27 | }, 28 | "devDependencies": { 29 | "@nestjs/testing": "^5.1.0", 30 | "@types/aws-lambda": "^8.10.15", 31 | "@types/aws-serverless-express": "^3.3.5", 32 | "@types/express": "^4.16.0", 33 | "@types/jest": "^23.3.1", 34 | "@types/node": "^10.7.1", 35 | "@types/supertest": "^2.0.5", 36 | "acorn": "^8.0.5", 37 | "jest": "^23.5.0", 38 | "nodemon": "^1.18.3", 39 | "prettier": "^1.14.2", 40 | "serverless-offline": "^6.8.0", 41 | "serverless-plugin-optimize": "^4.0.2-rc.1", 42 | "serverless-plugin-typescript": "1.1.7", 43 | "serverless-stage-manager": "^1.0.5", 44 | "supertest": "^3.1.0", 45 | "ts-jest": "^23.1.3", 46 | "ts-loader": "^4.4.2", 47 | "ts-node": "^7.0.1", 48 | "tsconfig-paths": "^3.5.0", 49 | "tslint": "5.11.0", 50 | "typescript": "^4.1.5" 51 | }, 52 | "jest": { 53 | "moduleFileExtensions": [ 54 | "js", 55 | "json", 56 | "ts" 57 | ], 58 | "rootDir": "src", 59 | "testRegex": ".spec.ts$", 60 | "transform": { 61 | "^.+\\.(t|j)s$": "ts-jest" 62 | }, 63 | "coverageDirectory": "../coverage", 64 | "testEnvironment": "node" 65 | } 66 | } -------------------------------------------------------------------------------- /quotes-restapi-service/serverless.yml: -------------------------------------------------------------------------------- 1 | service: quotes-restapi-service 2 | 3 | plugins: 4 | - serverless-plugin-typescript 5 | - serverless-plugin-optimize 6 | - serverless-offline 7 | - serverless-stage-manager 8 | provider: 9 | name: aws 10 | tracing: 11 | apiGateway: true 12 | runtime: nodejs14.x 13 | stage: ${opt:stage, 'dev'} 14 | region: eu-west-1 15 | 16 | 17 | package: 18 | exclude: 19 | - .gitignore 20 | - README.md 21 | - serverless.yml 22 | - nest-cli.json 23 | - .prettierrc 24 | excludeDevDependencies: true 25 | individually: true 26 | 27 | functions: 28 | main: 29 | handler: src/main.handler 30 | events: 31 | - http: 32 | method: any 33 | path: /{proxy+} 34 | cors: true 35 | 36 | custom: 37 | stages: 38 | - dev 39 | - test 40 | - acc 41 | - prod 42 | 43 | 44 | -------------------------------------------------------------------------------- /quotes-restapi-service/src/app.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | 5 | describe('AppController', () => { 6 | let app: TestingModule; 7 | 8 | beforeAll(async () => { 9 | app = await Test.createTestingModule({ 10 | controllers: [AppController], 11 | providers: [AppService], 12 | }).compile(); 13 | }); 14 | 15 | describe('root', () => { 16 | it('should return "Hello World!"', () => { 17 | const appController = app.get(AppController); 18 | expect(appController.getHello()).toBe('Hello World!'); 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /quotes-restapi-service/src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import {Controller, Get, HttpCode, HttpStatus, Post, Res} from '@nestjs/common'; 2 | import { AppService } from './app.service'; 3 | 4 | @Controller('hello') 5 | export class AppController { 6 | constructor(private readonly appService: AppService) {} 7 | 8 | @Get() 9 | getHello(): string { 10 | return this.appService.getHello(); 11 | } 12 | 13 | @Post() 14 | postHello(@Res() res: any) { 15 | return res.status(HttpStatus.CREATED).json({message: 'POST Hello World'}); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /quotes-restapi-service/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | import { QuotesService } from './quotes/quotes.service'; 5 | import { QuotesController } from './quotes/quotes.controller'; 6 | 7 | @Module({ 8 | imports: [], 9 | controllers: [AppController, QuotesController], 10 | providers: [AppService, QuotesService], 11 | }) 12 | export class AppModule {} 13 | -------------------------------------------------------------------------------- /quotes-restapi-service/src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService { 5 | getHello(): string { 6 | return 'Hello World!'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /quotes-restapi-service/src/main.ts: -------------------------------------------------------------------------------- 1 | import { Handler, Context } from 'aws-lambda'; 2 | import { Server } from 'http'; 3 | import { createServer, proxy } from 'aws-serverless-express'; 4 | import { eventContext } from 'aws-serverless-express/middleware'; 5 | 6 | import { ExpressAdapter } from '@nestjs/platform-express'; 7 | import { NestFactory } from '@nestjs/core'; 8 | import { AppModule } from './app.module'; 9 | 10 | import * as express from 'express'; 11 | 12 | // NOTE: If you get ERR_CONTENT_DECODING_FAILED in your browser, this is likely 13 | // due to a compressed response (e.g. gzip) which has not been handled correctly 14 | // by aws-serverless-express and/or API Gateway. Add the necessary MIME types to 15 | // binaryMimeTypes below 16 | const binaryMimeTypes: string[] = []; 17 | 18 | let cachedServer: Server; 19 | 20 | process.on('unhandledRejection', (reason) => { 21 | // tslint:disable-next-line:no-console 22 | console.error(reason); 23 | }); 24 | 25 | process.on('uncaughtException', (reason) => { 26 | // tslint:disable-next-line:no-console 27 | console.error(reason); 28 | }); 29 | 30 | async function bootstrapServer(): Promise { 31 | if (!cachedServer) { 32 | try { 33 | const expressApp = express(); 34 | const nestApp = await NestFactory.create(AppModule, new ExpressAdapter(expressApp)); 35 | nestApp.enableCors(); 36 | nestApp.use(eventContext()); 37 | await nestApp.init(); 38 | cachedServer = createServer(expressApp, undefined, binaryMimeTypes); 39 | } 40 | catch (error) { 41 | return Promise.reject(error); 42 | } 43 | } 44 | return Promise.resolve(cachedServer); 45 | } 46 | 47 | export const handler: Handler = async (event: any, context: Context) => { 48 | cachedServer = await bootstrapServer(); 49 | return proxy(cachedServer, event, context, 'PROMISE').promise; 50 | } 51 | -------------------------------------------------------------------------------- /quotes-restapi-service/src/quotes/quotes.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { QuotesController } from './quotes.controller'; 3 | 4 | describe('QuotesController', () => { 5 | let controller: QuotesController; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | controllers: [QuotesController], 10 | }).compile(); 11 | 12 | controller = module.get(QuotesController); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(controller).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /quotes-restapi-service/src/quotes/quotes.controller.ts: -------------------------------------------------------------------------------- 1 | import {Body, Controller, Get, HttpStatus, Post, Res} from '@nestjs/common'; 2 | import {QuotesService} from './quotes.service'; 3 | import {QuotesModel} from './quotes.model'; 4 | 5 | @Controller('quotes') 6 | export class QuotesController { 7 | 8 | constructor(private readonly quotesService: QuotesService) {} 9 | 10 | @Get() 11 | getQuotes(@Res() res) { 12 | const quotes = this.quotesService.getAllQuotes(); 13 | return res.status(HttpStatus.OK).json(Array.from(quotes.entries())); 14 | } 15 | 16 | @Post() 17 | addQuote(@Body() quote: QuotesModel, @Res() res: any) { 18 | const quotes = this.quotesService.addQuote(quote.quote, quote.author); 19 | return res.status(HttpStatus.CREATED).json(Array.from(quotes.entries())); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /quotes-restapi-service/src/quotes/quotes.model.spec.ts: -------------------------------------------------------------------------------- 1 | import { QuotesModel } from './quotes.model'; 2 | 3 | describe('QuotesModel', () => { 4 | it('should be defined', () => { 5 | expect(new QuotesModel()).toBeDefined(); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /quotes-restapi-service/src/quotes/quotes.model.ts: -------------------------------------------------------------------------------- 1 | export class QuotesModel { 2 | quote: string; 3 | author: string; 4 | } 5 | -------------------------------------------------------------------------------- /quotes-restapi-service/src/quotes/quotes.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { QuotesService } from './quotes.service'; 3 | 4 | describe('QuotesService', () => { 5 | let service: QuotesService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [QuotesService], 10 | }).compile(); 11 | 12 | service = module.get(QuotesService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /quotes-restapi-service/src/quotes/quotes.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class QuotesService { 5 | 6 | private readonly quotes: Map; 7 | 8 | constructor() { 9 | this.quotes = new Map(); 10 | this.quotes.set('Eleanor Roosevelt', ['If life were predictable it would cease to be life, and be without flavor.']); 11 | this.quotes.set('Oprah Winfrey', ['If you look at what you have in life, you\'ll always have more. If you look at ' + 12 | 'what you don\'t have in life, you\'ll never have enough.']); 13 | } 14 | 15 | addQuote(quote: string, author: string): Map { 16 | if (this.quotes.has(author)) { 17 | const authorQoutes = this.quotes.get(author); 18 | authorQoutes.push(quote); 19 | } else { 20 | const quoteArray = new Array(); 21 | quoteArray.push(quote); 22 | this.quotes.set(author, quoteArray); 23 | } 24 | return this.quotes; 25 | } 26 | 27 | getAllQuotes() { 28 | return this.quotes; 29 | } 30 | 31 | getAuthorQuotes(author: string) { 32 | return this.quotes.get(author); 33 | } 34 | 35 | // tslint:disable-next-line:no-empty 36 | async clear() { 37 | 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /quotes-restapi-service/test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { INestApplication } from '@nestjs/common'; 2 | import { Test } from '@nestjs/testing'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeAll(async () => { 10 | const moduleFixture = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/hello (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/hello') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /quotes-restapi-service/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /quotes-restapi-service/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src/**/*"], 4 | "exclude": ["node_modules", "**/*.spec.ts"] 5 | } 6 | -------------------------------------------------------------------------------- /quotes-restapi-service/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "noImplicitAny": false, 6 | "removeComments": true, 7 | "noLib": false, 8 | "allowSyntheticDefaultImports": true, 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "target": "es6", 12 | "sourceMap": true, 13 | "outDir": "./dist", 14 | "baseUrl": "./" 15 | }, 16 | "exclude": ["node_modules"] 17 | } 18 | -------------------------------------------------------------------------------- /quotes-restapi-service/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "types": ["jest", "node"] 5 | }, 6 | "include": ["**/*.spec.ts", "**/*.d.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /quotes-restapi-service/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": ["tslint:recommended"], 4 | "jsRules": { 5 | "no-unused-expression": true 6 | }, 7 | "rules": { 8 | "quotemark": [true, "single"], 9 | "member-access": [false], 10 | "ordered-imports": [false], 11 | "max-line-length": [true, 150], 12 | "member-ordering": [false], 13 | "interface-name": [false], 14 | "arrow-parens": false, 15 | "object-literal-sort-keys": false 16 | }, 17 | "rulesDirectory": [] 18 | } 19 | --------------------------------------------------------------------------------