├── .editorconfig ├── .eslintrc.json ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── actions.ts ├── demo.gif ├── docker-compose.yaml ├── dynamo.png ├── environments ├── environment.prod.ts ├── environment.serverless.ts ├── environment.stg.ts ├── environment.ts └── environment.types.ts ├── jest.config.js ├── jest.preset.js ├── libs ├── .gitkeep ├── db │ ├── .eslintrc.json │ ├── jest.config.js │ ├── project.json │ ├── src │ │ └── lib │ │ │ ├── client.ts │ │ │ ├── errors.ts │ │ │ ├── item.ts │ │ │ ├── operations.ts │ │ │ └── transaction.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ └── tsconfig.spec.json └── http │ ├── .eslintrc.json │ ├── jest.config.js │ ├── project.json │ ├── src │ └── lib │ │ ├── auth.middleware.ts │ │ ├── handlers.ts │ │ ├── response.ts │ │ ├── schema-validator.middleware.ts │ │ └── types.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ └── tsconfig.spec.json ├── logo.png ├── migrations.json ├── nx.json ├── package-lock.json ├── package.json ├── plugins.js ├── serverless.base.ts ├── services ├── .gitkeep ├── auth │ ├── .eslintrc.json │ ├── README.md │ ├── jest.config.js │ ├── project.json │ ├── serverless.ts │ ├── src │ │ ├── auth.utils.ts │ │ └── sign-up │ │ │ ├── sign-in-handler.spec.ts │ │ │ └── sign-up-handler.ts │ ├── tsconfig.app.json │ ├── tsconfig.json │ └── tsconfig.spec.json ├── core │ ├── .eslintrc.json │ ├── jest.config.js │ ├── project.json │ ├── serverless.ts │ ├── tsconfig.app.json │ ├── tsconfig.json │ └── tsconfig.spec.json ├── todos │ ├── .eslintrc.json │ ├── README.md │ ├── jest.config.js │ ├── project.json │ ├── serverless.ts │ ├── src │ │ ├── create-todo │ │ │ ├── create-todo-handler.spec.ts │ │ │ └── create-todo-handler.ts │ │ ├── get-todo │ │ │ ├── get-todo-handler.spec.ts │ │ │ └── get-todo-handler.ts │ │ ├── get-todos │ │ │ ├── get-todos-handler.spec.ts │ │ │ └── get-todos-handler.ts │ │ ├── todo.model.ts │ │ └── update-todo │ │ │ ├── update-todo-handler.spec.ts │ │ │ └── update-todo-handler.ts │ ├── tsconfig.app.json │ ├── tsconfig.json │ └── tsconfig.spec.json └── users │ ├── .eslintrc.json │ ├── README.md │ ├── jest.config.js │ ├── project.json │ ├── serverless.ts │ ├── src │ ├── get-user │ │ ├── get-user-handler.spec.ts │ │ └── get-user-handler.ts │ ├── user.model.ts │ └── users.types.ts │ ├── tsconfig.app.json │ ├── tsconfig.json │ └── tsconfig.spec.json ├── tools ├── generators │ ├── handler │ │ ├── files │ │ │ └── __fileName__ │ │ │ │ ├── __fileName__-handler.spec.ts__tmpl__ │ │ │ │ └── __fileName__-handler.ts__tmpl__ │ │ ├── index.ts │ │ └── schema.json │ ├── http-handler │ │ ├── files │ │ │ └── __fileName__ │ │ │ │ ├── __fileName__-handler.spec.ts__tmpl__ │ │ │ │ └── __fileName__-handler.ts__tmpl__ │ │ ├── index.ts │ │ └── schema.json │ ├── model │ │ ├── files │ │ │ └── __fileName__.model.ts__tmpl__ │ │ ├── index.ts │ │ └── schema.json │ └── service │ │ ├── files │ │ ├── .eslintrc.json │ │ ├── README.md │ │ ├── serverless.ts__tmpl__ │ │ ├── src │ │ │ ├── __fileName__.types.ts__tmpl__ │ │ │ └── __fileName__.utils.ts__tmpl__ │ │ ├── tsconfig.app.json │ │ ├── tsconfig.json │ │ └── tsconfig.spec.json │ │ ├── index.ts │ │ ├── jest-config.ts │ │ ├── schema.json │ │ ├── schema.ts │ │ └── workspace-config.ts └── tsconfig.tools.json ├── tsconfig.base.json └── workspace.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": ["**/*"], 4 | "plugins": ["@nrwl/nx"], 5 | "overrides": [ 6 | { 7 | "files": ["*.ts", "*.tsx"], 8 | "extends": ["plugin:@nrwl/nx/typescript"], 9 | "rules": { 10 | "@typescript-eslint/no-explicit-any": ["off"] 11 | } 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "extends": ["plugin:@nrwl/nx/javascript"], 16 | "rules": {} 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | #name: Lint,Test,Deploy 2 | 3 | #on: 4 | # pull_request: 5 | # branches: [ main ] 6 | # push: 7 | # branches: [ main ] 8 | 9 | #env: 10 | # AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 11 | # AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 12 | 13 | #jobs: 14 | # lint_test_deploy: 15 | # runs-on: ubuntu-latest 16 | 17 | # steps: 18 | # - name: Checkout repo 19 | # uses: actions/checkout@v3 20 | # with: 21 | # fetch-depth: 0 22 | 23 | # - name: Node setup 24 | # uses: actions/setup-node@v3 25 | # with: 26 | # node-version: 16 27 | # cache: 'npm' 28 | # - name: install dependencies 29 | # run: npm ci 30 | 31 | # - name: lint 32 | # run: npm run lint:affected 33 | 34 | # - name: test 35 | # run: npm run test:affected 36 | 37 | # - name: deploy to staging 38 | # if: contains(github.head_ref , 'stg') 39 | # env: 40 | # NODE_ENV: stg 41 | # run: npm run deploy:affected 42 | 43 | # - name: deploy to prod 44 | # if: contains(github.head_ref , 'prod') 45 | # env: 46 | # NODE_ENV: prod 47 | # run: npm run deploy:affected 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | /node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | 20 | # IDE - VSCode 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/tasks.json 24 | !.vscode/launch.json 25 | !.vscode/extensions.json 26 | 27 | # misc 28 | /.sass-cache 29 | /connect.lock 30 | /coverage 31 | /libpeerconnection.log 32 | npm-debug.log 33 | yarn-error.log 34 | testem.log 35 | /typings 36 | 37 | # System Files 38 | .DS_Store 39 | Thumbs.db 40 | 41 | .serverless 42 | .vscode 43 | .dynamodb 44 | .webpack 45 | .esbuild 46 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run lint-staged --concurrent 5 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Add files here to ignore them from prettier formatting 2 | 3 | /dist 4 | /coverage 5 | .esbuild -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Netanel Basal 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | 4 |

5 | 6 |

Nx Serverless

7 | 8 | > The Ultimate Monorepo Starter for Node.js Serverless Applications 9 | 10 | ✅  First-Class Typescript Support
11 | ✅  DynamoDB Single Table Design
12 | ✅  Shared API Gateway
13 | ✅  Environments Configuration
14 | ✅  CORS
15 | ✅  JWT Auth Middleware
16 | ✅  Http Params Validation
17 | ✅  Typed Proxy Handlers
18 | ✅  Auto Generators
19 | ✅  Localstack
20 | ✅  ESLint
21 | ✅  Jest
22 | ✅  Github Actions 23 | 24 |
25 | 26 | [![serverless](http://public.serverless.com/badges/v3.svg)](http://www.serverless.com) 27 | [![](https://img.shields.io/badge/monorepo-Nx-blue)](https://nx.dev/) 28 | ![esbuild](https://badges.aleen42.com/src/esbuild.svg) 29 | ![npm peer dependency version (scoped)](https://img.shields.io/npm/dependency-version/eslint-config-prettier/peer/eslint) 30 | ![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square) 31 | [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/sudokar/nx-serverless/blob/master/LICENSE) 32 | [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/sudokar/nx-serverless) 33 | ![Maintained](https://img.shields.io/maintenance/yes/2022.svg) 34 | 35 | 36 | ## Prerequisites 37 | 38 | - Docker 39 | - Node.js 40 | 41 | ## Getting Started 42 | 43 | - Run git clone https://github.com/ngneat/nx-serverless.git your-app-name 44 | - Run `npm install` 45 | - Run `npm run localstack` ( Check that it works by going to http://localhost:4566/health) 46 | - Run `npx nx deploy core --stage local` to create the table 47 | - Update the `environment` files based on your configuration 48 | - Run `npm run serve` 49 | 50 | ## About the App 51 | 52 | The application contains three services: 53 | 54 | #### Auth Service: 55 | 56 | The auth service is responsible for authentication. It exposes one route for signing up: 57 | 58 | ```bash 59 | curl --request POST 'http://localhost:3001/dev/auth/sign-up' \ 60 | --data-raw '{ 61 | "email": "netanel@gmail.com", 62 | "name": "Netanel Basal" 63 | }' 64 | ``` 65 | 66 | The request returns a JWT, which is used for accessing protected routes. 67 | 68 | #### Users Service: 69 | 70 | The users service is responsible for managing users. It exposes one route: 71 | 72 | ```bash 73 | curl 'http://localhost:3003/dev/user' --header 'Authorization: token TOKEN' 74 | ``` 75 | 76 | The request returns the logged-in user. 77 | 78 | #### Todos Service: 79 | 80 | The todos service is responsible for managing todos. A user has many todos. It exposes CRUD routes: 81 | 82 | ```bash 83 | // Get user todos 84 | curl 'http://localhost:3005/dev/todos' --header 'Authorization: token TOKEN' 85 | 86 | // Get a single todo 87 | curl 'http://localhost:3005/dev/todos/:id' --header 'Authorization: token TOKEN' 88 | 89 | // Create a todo 90 | curl --request POST 'http://localhost:3005/dev/todos' \ 91 | --header 'Authorization: token TOKEN' 92 | --data-raw '{ 93 | "title": "Learn Serverless" 94 | }' 95 | 96 | // Update a todo 97 | curl --request PUT 'http://localhost:3005/dev/todos/:id' \ 98 | --header 'Authorization: token TOKEN' \ 99 | --data-raw '{ 100 | "completed": true 101 | }' 102 | ``` 103 | 104 | ## DynamoDB GUI 105 | [Download](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/workbench.settingup.html) NoSQL Workbench for DynamoDB and connect to `http://localhost:4566`. 106 | 107 |

108 | 109 |

110 | 111 | ## Commands 112 | 113 | ```bash 114 | nx serve 115 | nx deploy 116 | nx remove 117 | nx build 118 | nx lint 119 | nx test 120 | 121 | // Use different enviroment 122 | NODE_ENV=prod nx deploy 123 | NODE_ENV=stg nx deploy 124 | 125 | // Run only affected 126 | nx affected:test 127 | nx affected:deploy 128 | ``` 129 | 130 | ## Generators 131 | 132 | ```bash 133 | // Generate a service 134 | yarn g:service tags 135 | 136 | // Generate handler 137 | yarn g:handler handler-name 138 | 139 | // Generate http handler 140 | yarn g:http-handler create-tag 141 | 142 | // Generate a model 143 | yarn g:model tag 144 | ``` 145 | 146 | 147 | 148 |
149 | 150 | ## CI/CD Pipeline with Github Actions 151 | 152 | The pipeline has been configured to run everytime a push/pull_request is made to the `main` branch. You should uncomment the `ci.yml` workflow. 153 | 154 | #### Workflow Steps 155 | 156 | - Checkout: The `checkout` action is used to checkout the source code. 157 | 158 | - Node setup: The `setup-node` action is used to optionally download and cache distribution of the requested Node.js version. 159 | 160 | - lint and test: The `lint` and `test` runs only on affected projects. 161 | 162 | - Configure AWS credentials: The credentials needed are `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` and should be set as Github __secrets__. 163 | 164 | - Each branch should be prefixed with the `environment` name. For example, if we have a `stg-feature-name` branch and open a pull request to the `main` branch, it will set `NODE_ENV` to `stg` and deploy to this environment. 165 | 166 | By merging the pull request to the `main` branch, `NODE_ENV` is set to `prod`, and the deployment is done to production. 167 | 168 | The workflow file can have as many environments as you need. 169 | 170 | ## Further help 171 | 172 | - Visit [Serverless Documentation](https://www.serverless.com/framework/docs/) to learn more about Serverless framework 173 | - Visit [Nx Documentation](https://nx.dev) to learn more about Nx dev toolkit 174 | - Visit [LocalStack](https://localstack.cloud/) to learn more about it 175 | ## Contribution 176 | 177 | Found an issue? feel free to raise an issue with information to reproduce. 178 | 179 | Pull requests are welcome to improve. 180 | 181 | ## License 182 | 183 | MIT 184 | 185 | This project is a fork of [nx-serverless](https://github.com/sudokar/nx-serverless) 186 | 187 | Monster icons created by Smashicons - Flaticon 188 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngneat/nx-serverless/34ded7c4083c2962bad956cafa0772a59899b21e/demo.gif -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | localstack: 5 | container_name: '${LOCALSTACK_DOCKER_NAME-localstack_main}' 6 | image: localstack/localstack 7 | network_mode: bridge 8 | ports: 9 | - '127.0.0.1:4510-4559:4510-4559' # external service port range 10 | - '127.0.0.1:4566:4566' # LocalStack Edge Proxy 11 | environment: 12 | - AWS_DEFAULT_REGION=eu-west-1 13 | - DEBUG=1 14 | - DATA_DIR=/tmp/localstack/data 15 | volumes: 16 | - '/private/tmp/localstack:/tmp/localstack' 17 | - '/var/run/docker.sock:/var/run/docker.sock' 18 | -------------------------------------------------------------------------------- /dynamo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngneat/nx-serverless/34ded7c4083c2962bad956cafa0772a59899b21e/dynamo.png -------------------------------------------------------------------------------- /environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | import type { Environment } from './environment.types'; 2 | 3 | export const env: Environment = { 4 | name: 'prod', 5 | profile: '', 6 | jwtSecret: '', 7 | dynamo: { 8 | tableName: `prod-AppTable`, 9 | }, 10 | region: 'eu-west-1', 11 | }; 12 | -------------------------------------------------------------------------------- /environments/environment.serverless.ts: -------------------------------------------------------------------------------- 1 | // Serverless configuration files don't work with esbuild, 2 | // so this workaround must be used when environment variables are needed in the serverless configuration files. 3 | export const envName = process.env.NODE_ENV; 4 | const envFile = process.env.NODE_ENV ? `.${process.env.NODE_ENV}` : ''; 5 | 6 | export const { env } = require(`./environment${envFile}.ts`); 7 | 8 | export const tableResource = `arn:aws:dynamodb:${env.region}:*:table/${env.dynamo.tableName}`; 9 | -------------------------------------------------------------------------------- /environments/environment.stg.ts: -------------------------------------------------------------------------------- 1 | import type { Environment } from './environment.types'; 2 | 3 | export const env: Environment = { 4 | name: 'stg', 5 | profile: '', 6 | jwtSecret: 'secret', 7 | dynamo: { 8 | tableName: `stg-AppTable`, 9 | }, 10 | region: 'eu-west-1', 11 | }; 12 | -------------------------------------------------------------------------------- /environments/environment.ts: -------------------------------------------------------------------------------- 1 | import type { Environment } from './environment.types'; 2 | 3 | export const env: Environment = { 4 | name: 'dev', 5 | region: 'eu-west-1', 6 | profile: 'local', 7 | jwtSecret: 'secret', 8 | dynamo: { 9 | endpoint: 'http://localhost:4566', 10 | tableName: `dev-AppTable`, 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /environments/environment.types.ts: -------------------------------------------------------------------------------- 1 | export interface Environment { 2 | name: 'dev' | 'stg' | 'prod'; 3 | region: string; 4 | profile: string; 5 | jwtSecret: string; 6 | dynamo: { 7 | endpoint?: string; 8 | tableName: string; 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | projects: [ 3 | '/services/todos', 4 | '/libs/http', 5 | '/services/core', 6 | '/services/auth', 7 | '/libs/db', 8 | '/services/users', 9 | ], 10 | }; 11 | -------------------------------------------------------------------------------- /jest.preset.js: -------------------------------------------------------------------------------- 1 | const nxPreset = require('@nrwl/jest/preset'); 2 | 3 | module.exports = { ...nxPreset }; 4 | -------------------------------------------------------------------------------- /libs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngneat/nx-serverless/34ded7c4083c2962bad956cafa0772a59899b21e/libs/.gitkeep -------------------------------------------------------------------------------- /libs/db/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /libs/db/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | displayName: 'db', 3 | preset: '../../jest.preset.js', 4 | globals: { 5 | 'ts-jest': { 6 | tsconfig: '/tsconfig.spec.json', 7 | }, 8 | }, 9 | testEnvironment: 'node', 10 | transform: { 11 | '^.+\\.[tj]s$': 'ts-jest', 12 | }, 13 | moduleFileExtensions: ['ts', 'js', 'html'], 14 | coverageDirectory: '../../coverage/libs/db', 15 | }; 16 | -------------------------------------------------------------------------------- /libs/db/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": "libs/db", 3 | "sourceRoot": "libs/db/src", 4 | "targets": { 5 | "build": { 6 | "executor": "@nrwl/js:tsc", 7 | "outputs": ["{options.outputPath}"], 8 | "options": { 9 | "outputPath": "dist/libs/db", 10 | "main": "libs/db/src/index.ts", 11 | "tsConfig": "libs/db/tsconfig.lib.json", 12 | "assets": ["libs/db/*.md"] 13 | } 14 | }, 15 | "lint": { 16 | "executor": "@nrwl/linter:eslint", 17 | "outputs": ["{options.outputFile}"], 18 | "options": { 19 | "lintFilePatterns": ["libs/db/**/*.ts"] 20 | } 21 | }, 22 | "test": { 23 | "executor": "@nrwl/jest:jest", 24 | "outputs": ["coverage/libs/db"], 25 | "options": { 26 | "jestConfig": "libs/db/jest.config.js", 27 | "passWithNoTests": true 28 | } 29 | } 30 | }, 31 | "tags": [] 32 | } 33 | -------------------------------------------------------------------------------- /libs/db/src/lib/client.ts: -------------------------------------------------------------------------------- 1 | import AWS, { DynamoDB } from 'aws-sdk'; 2 | import { env } from '@app/env'; 3 | 4 | AWS.config.update({ 5 | region: env.region, 6 | }); 7 | 8 | let client: DynamoDB | null = null; 9 | 10 | export function getClient() { 11 | if (!client) { 12 | const options = { 13 | endpoint: env.dynamo.endpoint, 14 | httpOptions: { 15 | connectTimeout: 1000, 16 | timeout: 1000, 17 | }, 18 | }; 19 | 20 | client = new DynamoDB(env.name === 'dev' ? options : undefined); 21 | } 22 | 23 | return { 24 | db: client, 25 | TableName: env.dynamo.tableName, 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /libs/db/src/lib/errors.ts: -------------------------------------------------------------------------------- 1 | // We don't want to expose these errors to the client. 2 | // We only log them to see them in our debug/monitor tool. 3 | export function dbErrorLogger(err: any) { 4 | if (!err) { 5 | console.log('Encountered error object was empty'); 6 | return; 7 | } 8 | 9 | if (!err.code) { 10 | console.log( 11 | `An exception occurred, investigate and configure retry strategy. Error: ${JSON.stringify( 12 | err 13 | )}` 14 | ); 15 | 16 | return; 17 | } 18 | 19 | switch (err.code) { 20 | case 'ConditionalCheckFailedException': 21 | console.log( 22 | `Condition check specified in the operation failed, review and update the condition check before retrying. Error: ${err.message}` 23 | ); 24 | return; 25 | case 'TransactionConflictException': 26 | console.log(`Operation was rejected because there is an ongoing transaction for the item, generally safe to retry ' + 27 | 'with exponential back-off. Error: ${err.message}`); 28 | return; 29 | case 'ItemCollectionSizeLimitExceededException': 30 | console.log( 31 | `An item collection is too large, you're using Local Secondary Index and exceeded size limit of` + 32 | `items per partition key. Consider using Global Secondary Index instead. Error: ${err.message}` 33 | ); 34 | return; 35 | case 'InternalServerError': 36 | console.log( 37 | `Internal Server Error, generally safe to retry with exponential back-off. Error: ${err.message}` 38 | ); 39 | return; 40 | case 'ProvisionedThroughputExceededException': 41 | console.log( 42 | `Request rate is too high. If you're using a custom retry strategy make sure to retry with exponential back-off. ` + 43 | `Otherwise consider reducing frequency of requests or increasing provisioned capacity for your table or secondary index. Error: ${err.message}` 44 | ); 45 | return; 46 | case 'ResourceNotFoundException': 47 | console.log( 48 | `One of the tables was not found, verify table exists before retrying. Error: ${err.message}` 49 | ); 50 | return; 51 | case 'ServiceUnavailable': 52 | console.log( 53 | `Had trouble reaching DynamoDB. generally safe to retry with exponential back-off. Error: ${err.message}` 54 | ); 55 | return; 56 | case 'ThrottlingException': 57 | console.log( 58 | `Request denied due to throttling, generally safe to retry with exponential back-off. Error: ${err.message}` 59 | ); 60 | return; 61 | case 'UnrecognizedClientException': 62 | console.log( 63 | `The request signature is incorrect most likely due to an invalid AWS access key ID or secret key, fix before retrying. ` + 64 | `Error: ${err.message}` 65 | ); 66 | return; 67 | case 'ValidationException': 68 | console.log( 69 | `The input fails to satisfy the constraints specified by DynamoDB, ` + 70 | `fix input before retrying. Error: ${err.message}` 71 | ); 72 | return; 73 | case 'RequestLimitExceeded': 74 | console.log( 75 | `Throughput exceeds the current throughput limit for your account, ` + 76 | `increase account level throughput before retrying. Error: ${err.message}` 77 | ); 78 | return; 79 | default: 80 | console.log( 81 | `An exception occurred, investigate and configure retry strategy. Error: ${err.message}` 82 | ); 83 | return; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /libs/db/src/lib/item.ts: -------------------------------------------------------------------------------- 1 | import { DynamoDB } from 'aws-sdk'; 2 | import { 3 | marshall, 4 | unmarshall, 5 | marshallOptions, 6 | unmarshallOptions, 7 | } from '@aws-sdk/util-dynamodb'; 8 | 9 | export interface BaseModel { 10 | PK: string; 11 | SK: string; 12 | createdAt: string; 13 | entityType: string; 14 | } 15 | 16 | export abstract class ItemKeys { 17 | static ENTITY_TYPE: string; 18 | abstract get pk(): string; 19 | abstract get sk(): string; 20 | 21 | fromItem() { 22 | return { 23 | PK: this.pk, 24 | SK: this.sk, 25 | }; 26 | } 27 | 28 | toItem() { 29 | return { 30 | PK: { S: this.pk }, 31 | SK: { S: this.sk }, 32 | }; 33 | } 34 | } 35 | 36 | export abstract class Item> { 37 | abstract get keys(): ItemKeys; 38 | abstract toItem(): DynamoDB.AttributeMap; 39 | 40 | static fromItem( 41 | attributeMap: DynamoDB.AttributeMap, 42 | options?: unmarshallOptions 43 | ) { 44 | return unmarshall(attributeMap, options); 45 | } 46 | 47 | private withKeys() { 48 | return { 49 | ...this.keys.fromItem(), 50 | entityType: (this.keys.constructor as any).ENTITY_TYPE, 51 | createdAt: new Date().toISOString(), 52 | }; 53 | } 54 | 55 | marshall( 56 | model: Omit, 57 | options?: marshallOptions 58 | ): DynamoDB.AttributeMap { 59 | return marshall( 60 | { ...model, ...this.withKeys() }, 61 | { removeUndefinedValues: true, convertEmptyValues: true, ...options } 62 | ); 63 | } 64 | 65 | unmarshall( 66 | attributeMap: DynamoDB.AttributeMap, 67 | options?: unmarshallOptions 68 | ): T { 69 | return unmarshall(attributeMap, options) as T; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /libs/db/src/lib/operations.ts: -------------------------------------------------------------------------------- 1 | import { getClient } from './client'; 2 | import { Item, ItemKeys } from './item'; 3 | import { DynamoDB } from 'aws-sdk'; 4 | import { dbErrorLogger } from './errors'; 5 | 6 | export async function createItem>( 7 | item: T, 8 | options?: Omit 9 | ) { 10 | const { TableName, db } = getClient(); 11 | 12 | try { 13 | return await db 14 | .putItem({ 15 | TableName, 16 | Item: item.toItem(), 17 | ConditionExpression: 'attribute_not_exists(SK)', 18 | ...options, 19 | }) 20 | .promise(); 21 | } catch (e) { 22 | dbErrorLogger(e); 23 | 24 | throw { 25 | success: false, 26 | }; 27 | } 28 | } 29 | 30 | export async function updateItem( 31 | keys: ItemKeys, 32 | options?: Omit 33 | ) { 34 | const { TableName, db } = getClient(); 35 | 36 | try { 37 | return await db 38 | .updateItem({ 39 | TableName, 40 | Key: keys.toItem(), 41 | ...options, 42 | }) 43 | .promise(); 44 | } catch (e) { 45 | dbErrorLogger(e); 46 | 47 | throw { 48 | success: false, 49 | }; 50 | } 51 | } 52 | 53 | export async function deleteItem( 54 | keys: ItemKeys, 55 | options?: Omit 56 | ) { 57 | const { TableName, db } = getClient(); 58 | 59 | try { 60 | await db 61 | .deleteItem({ 62 | TableName, 63 | Key: keys.toItem(), 64 | ...options, 65 | }) 66 | .promise(); 67 | } catch (e) { 68 | dbErrorLogger(e); 69 | 70 | throw { 71 | success: false, 72 | }; 73 | } 74 | } 75 | 76 | export async function query(options: Omit) { 77 | const { TableName, db } = getClient(); 78 | 79 | try { 80 | return await db 81 | .query({ 82 | TableName, 83 | ...options, 84 | }) 85 | .promise(); 86 | } catch (e) { 87 | dbErrorLogger(e); 88 | 89 | throw { 90 | success: false, 91 | }; 92 | } 93 | } 94 | 95 | export async function getItem( 96 | keys: ItemKeys, 97 | options?: Omit 98 | ) { 99 | const { TableName, db } = getClient(); 100 | 101 | try { 102 | return await db 103 | .getItem({ 104 | TableName, 105 | Key: keys.toItem(), 106 | ...options, 107 | }) 108 | .promise(); 109 | } catch (e) { 110 | dbErrorLogger(e); 111 | 112 | throw { 113 | success: false, 114 | }; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /libs/db/src/lib/transaction.ts: -------------------------------------------------------------------------------- 1 | import { DynamoDB } from 'aws-sdk'; 2 | 3 | type executeTransactWriteInput = { 4 | client: DynamoDB; 5 | params: DynamoDB.Types.TransactWriteItemsInput; 6 | }; 7 | 8 | export async function executeTransactWrite({ 9 | client, 10 | params, 11 | }: executeTransactWriteInput) { 12 | const transactionRequest = client.transactWriteItems(params); 13 | 14 | let cancellationReasons: any; 15 | 16 | transactionRequest.on('extractError', (response) => { 17 | try { 18 | cancellationReasons = JSON.parse( 19 | response.httpResponse.body.toString() 20 | ).CancellationReasons; 21 | } catch (err) { 22 | // suppress this just in case some types of errors aren't JSON parseable 23 | console.error('Error extracting cancellation error', err); 24 | } 25 | }); 26 | 27 | return new Promise((resolve, reject) => { 28 | transactionRequest.send((err, response) => { 29 | if (err) { 30 | /* tslint:disable-next-line */ 31 | (err as any).cancellationReasons = cancellationReasons; 32 | return reject(err); 33 | } 34 | return resolve(response); 35 | }); 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /libs/db/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "forceConsistentCasingInFileNames": true, 6 | "strict": true, 7 | "noImplicitOverride": true, 8 | "noPropertyAccessFromIndexSignature": true, 9 | "noImplicitReturns": true, 10 | "noFallthroughCasesInSwitch": true 11 | }, 12 | "files": [], 13 | "include": [], 14 | "references": [ 15 | { 16 | "path": "./tsconfig.lib.json" 17 | }, 18 | { 19 | "path": "./tsconfig.spec.json" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /libs/db/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "declaration": true, 6 | "types": [] 7 | }, 8 | "include": ["**/*.ts"], 9 | "exclude": ["**/*.spec.ts", "**/*.test.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /libs/db/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": ["**/*.test.ts", "**/*.spec.ts", "**/*.d.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /libs/http/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /libs/http/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | displayName: 'http', 3 | preset: '../../jest.preset.js', 4 | globals: { 5 | 'ts-jest': { 6 | tsconfig: '/tsconfig.spec.json', 7 | }, 8 | }, 9 | testEnvironment: 'node', 10 | transform: { 11 | '^.+\\.[tj]s$': 'ts-jest', 12 | }, 13 | moduleFileExtensions: ['ts', 'js', 'html'], 14 | coverageDirectory: '../../coverage/libs/http', 15 | }; 16 | -------------------------------------------------------------------------------- /libs/http/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": "libs/http", 3 | "sourceRoot": "libs/http/src", 4 | "targets": { 5 | "build": { 6 | "executor": "@nrwl/js:tsc", 7 | "outputs": ["{options.outputPath}"], 8 | "options": { 9 | "outputPath": "dist/libs/http", 10 | "main": "libs/http/src/index.ts", 11 | "tsConfig": "libs/http/tsconfig.lib.json", 12 | "assets": ["libs/http/*.md"] 13 | } 14 | }, 15 | "lint": { 16 | "executor": "@nrwl/linter:eslint", 17 | "outputs": ["{options.outputFile}"], 18 | "options": { 19 | "lintFilePatterns": ["libs/http/**/*.ts"] 20 | } 21 | }, 22 | "test": { 23 | "executor": "@nrwl/jest:jest", 24 | "outputs": ["coverage/libs/http"], 25 | "options": { 26 | "jestConfig": "libs/http/jest.config.js", 27 | "passWithNoTests": true 28 | } 29 | } 30 | }, 31 | "tags": [] 32 | } 33 | -------------------------------------------------------------------------------- /libs/http/src/lib/auth.middleware.ts: -------------------------------------------------------------------------------- 1 | import middy from '@middy/core'; 2 | import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'; 3 | import jwt from 'jsonwebtoken'; 4 | import { env } from '@app/env'; 5 | import { httpError } from './response'; 6 | import { Handler, UserContext } from './types'; 7 | 8 | export function authMiddleware(): middy.MiddlewareObj< 9 | Parameters>[0], 10 | APIGatewayProxyResult 11 | > { 12 | const before: middy.MiddlewareFn< 13 | APIGatewayProxyEvent, 14 | APIGatewayProxyResult 15 | > = async (request) => { 16 | const authHeader = request.event.headers['Authorization']; 17 | 18 | if (authHeader) { 19 | const token = authHeader.split(' ')[1]; 20 | 21 | try { 22 | const data = jwt.verify(token, env.jwtSecret); 23 | (request.context as unknown as UserContext).user = 24 | data as UserContext['user']; 25 | 26 | return Promise.resolve(); 27 | } catch (error) { 28 | return httpError(error, { statusCode: 401 }); 29 | } 30 | } 31 | 32 | return httpError('Missing token', { statusCode: 401 }); 33 | }; 34 | 35 | return { 36 | before, 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /libs/http/src/lib/handlers.ts: -------------------------------------------------------------------------------- 1 | import middy from '@middy/core'; 2 | import middyJsonBodyParser from '@middy/http-json-body-parser'; 3 | import { authMiddleware } from './auth.middleware'; 4 | import { EventParams, Handler } from './types'; 5 | 6 | export function createHandler< 7 | P extends EventParams, 8 | isProtected extends boolean = false 9 | >(handler: Handler) { 10 | return middy(handler).use(middyJsonBodyParser()); 11 | } 12 | 13 | export function createProtectedHandler

( 14 | handler: Handler

15 | ) { 16 | return createHandler(handler).use(authMiddleware()); 17 | } 18 | -------------------------------------------------------------------------------- /libs/http/src/lib/response.ts: -------------------------------------------------------------------------------- 1 | import { APIGatewayProxyResult } from 'aws-lambda'; 2 | 3 | const corsHeaders = { 4 | // Change this to your domains 5 | 'Access-Control-Allow-Origin': '*', 6 | // Change this to your headers 7 | 'Access-Control-Allow-Headers': '*', 8 | 'Access-Control-Max-Age': 86400, 9 | } 10 | 11 | export function httpResponse( 12 | data: Record, 13 | { statusCode = 200, ...rest }: Omit = { 14 | statusCode: 200, 15 | } 16 | ): APIGatewayProxyResult { 17 | return { 18 | body: JSON.stringify({ data }), 19 | statusCode, 20 | ...rest, 21 | headers: { 22 | ...rest.headers, 23 | ...corsHeaders 24 | }, 25 | }; 26 | } 27 | 28 | export function httpError( 29 | error: any, 30 | { statusCode = 400, ...rest }: Omit = { 31 | statusCode: 200, 32 | } 33 | ): APIGatewayProxyResult { 34 | return { 35 | body: JSON.stringify({ error }), 36 | statusCode, 37 | ...rest, 38 | headers: { 39 | ...rest.headers, 40 | ...corsHeaders 41 | }, 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /libs/http/src/lib/schema-validator.middleware.ts: -------------------------------------------------------------------------------- 1 | import { BaseSchema, ValidationError } from 'yup'; 2 | import middy from '@middy/core'; 3 | import { APIGatewayProxyResult } from 'aws-lambda'; 4 | import { httpError } from './response'; 5 | import { BodyParams, EventParams, Handler, QueryParams } from './types'; 6 | 7 | export function schemaValidator

(schema: { 8 | body?: BaseSchema

; 9 | queryStringParameters?: BaseSchema< 10 | BaseSchema

11 | >; 12 | }): middy.MiddlewareObj>[0], APIGatewayProxyResult> { 13 | const before: middy.MiddlewareFn< 14 | Parameters>[0], 15 | APIGatewayProxyResult 16 | > = async (request) => { 17 | try { 18 | const { body, queryStringParameters } = request.event; 19 | 20 | if (schema.body) { 21 | schema.body.validateSync(body); 22 | } 23 | 24 | if (schema.queryStringParameters) { 25 | schema.queryStringParameters.validateSync(queryStringParameters ?? {}); 26 | } 27 | 28 | return Promise.resolve(); 29 | } catch (e) { 30 | return httpError(e instanceof ValidationError ? e.errors : []); 31 | } 32 | }; 33 | 34 | return { 35 | before, 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /libs/http/src/lib/types.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | APIGatewayProxyEvent, 3 | APIGatewayProxyResult, 4 | Callback, 5 | Context, 6 | } from 'aws-lambda'; 7 | 8 | export interface BodyParams< 9 | T extends Record = Record 10 | > { 11 | body: T; 12 | } 13 | 14 | export interface QueryParams< 15 | T extends Record = Record 16 | > { 17 | queryStringParameters: T; 18 | } 19 | 20 | export interface PathParams< 21 | T extends Record = Record 22 | > { 23 | pathParameters: T; 24 | } 25 | 26 | export type EventParams = BodyParams | QueryParams | PathParams; 27 | 28 | export type Handler< 29 | P extends EventParams, 30 | isProtected extends boolean = true 31 | > = ( 32 | event: Omit< 33 | APIGatewayProxyEvent, 34 | 'body' | 'pathParameters' | 'queryStringParameters' 35 | > & { 36 | body: P extends BodyParams ? P['body'] : null; 37 | pathParameters: P extends PathParams ? P['pathParameters'] : null; 38 | queryStringParameters: P extends QueryParams 39 | ? P['queryStringParameters'] 40 | : null; 41 | }, 42 | context: isProtected extends true ? Context & UserContext : Context, 43 | callback: Callback 44 | ) => void | Promise; 45 | 46 | export type UserContext = { 47 | user: { 48 | id: string; 49 | }; 50 | }; 51 | -------------------------------------------------------------------------------- /libs/http/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "forceConsistentCasingInFileNames": true, 6 | "strict": true, 7 | "noImplicitOverride": true, 8 | "noPropertyAccessFromIndexSignature": true, 9 | "noImplicitReturns": true, 10 | "noFallthroughCasesInSwitch": true 11 | }, 12 | "files": [], 13 | "include": [], 14 | "references": [ 15 | { 16 | "path": "./tsconfig.lib.json" 17 | }, 18 | { 19 | "path": "./tsconfig.spec.json" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /libs/http/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "declaration": true, 6 | "types": [] 7 | }, 8 | "include": ["**/*.ts"], 9 | "exclude": ["**/*.spec.ts", "**/*.test.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /libs/http/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": ["**/*.test.ts", "**/*.spec.ts", "**/*.d.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngneat/nx-serverless/34ded7c4083c2962bad956cafa0772a59899b21e/logo.png -------------------------------------------------------------------------------- /migrations.json: -------------------------------------------------------------------------------- 1 | { 2 | "migrations": [ 3 | { 4 | "cli": "nx", 5 | "version": "14.0.6", 6 | "description": "Remove root property from project.json files", 7 | "factory": "./src/migrations/update-14-0-6/remove-roots", 8 | "package": "nx", 9 | "name": "14-0-6-remove-root" 10 | }, 11 | { 12 | "version": "14.0.0-beta.0", 13 | "description": "Changes the presets in nx.json to come from the nx package", 14 | "cli": "nx", 15 | "implementation": "./src/migrations/update-14-0-0/change-nx-json-presets", 16 | "package": "@nrwl/workspace", 17 | "name": "14-0-0-change-nx-json-presets" 18 | }, 19 | { 20 | "version": "14.0.0-beta.0", 21 | "description": "Migrates from @nrwl/workspace:run-script to nx:run-script", 22 | "cli": "nx", 23 | "implementation": "./src/migrations/update-14-0-0/change-npm-script-executor", 24 | "package": "@nrwl/workspace", 25 | "name": "14-0-0-change-npm-script-executor" 26 | }, 27 | { 28 | "version": "14.0.0-beta.2", 29 | "cli": "nx", 30 | "description": "Update move jest config files to .ts files.", 31 | "factory": "./src/migrations/update-14-0-0/update-jest-config-ext", 32 | "package": "@nrwl/jest", 33 | "name": "update-jest-config-extensions" 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- 1 | { 2 | "npmScope": "app", 3 | "affected": { 4 | "defaultBase": "main" 5 | }, 6 | "implicitDependencies": { 7 | "workspace.json": "*", 8 | "package.json": { 9 | "dependencies": "*", 10 | "devDependencies": "*" 11 | }, 12 | "tsconfig.base.json": "*", 13 | "tslint.json": "*", 14 | ".eslintrc.json": "*", 15 | "nx.json": "*" 16 | }, 17 | "tasksRunnerOptions": { 18 | "default": { 19 | "runner": "@nrwl/workspace/tasks-runners/default", 20 | "options": { 21 | "cacheableOperations": ["build", "lint", "test"] 22 | } 23 | } 24 | }, 25 | "workspaceLayout": { 26 | "appsDir": "services" 27 | }, 28 | "cli": { 29 | "defaultCollection": "@nrwl/node" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nx-serverless", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "nx": "nx", 7 | "localstack": "docker-compose up", 8 | "build": "nx build", 9 | "serve": "nx run-many --target=serve --all", 10 | "test": "nx run-many --target=test --all", 11 | "lint": "nx run-many --target=lint --all", 12 | "deploy": "nx run-many --target=deploy --all", 13 | "affected": "nx affected", 14 | "format": "nx format:write", 15 | "update": "nx migrate latest", 16 | "workspace-generator": "nx workspace-generator", 17 | "test:affected": "nx affected:test --base=origin/main --head=HEAD", 18 | "deploy:affected": "nx affected --target=deploy --all", 19 | "lint:affected": "nx affected:lint --base=origin/main --head=HEAD", 20 | "g:http-handler": " npx nx workspace-generator http-handler", 21 | "g:handler": " npx nx workspace-generator handler", 22 | "g:service": " npx nx workspace-generator service", 23 | "g:model": " npx nx workspace-generator model" 24 | }, 25 | "dependencies": { 26 | "@aws-sdk/util-dynamodb": "3.80.0", 27 | "@middy/core": "2.5.7", 28 | "@middy/http-json-body-parser": "2.5.7", 29 | "jsonwebtoken": "8.5.1", 30 | "middy": "0.36.0", 31 | "ulid": "2.3.0", 32 | "yup": "0.32.11", 33 | "tslib": "2.0.0" 34 | }, 35 | "devDependencies": { 36 | "ts-morph": "15.0.0", 37 | "@aws-sdk/types": "3.55.0", 38 | "@jest/transform": "27.5.1", 39 | "@nrwl/cli": "14.1.2", 40 | "@nrwl/devkit": "14.1.2", 41 | "@nrwl/eslint-plugin-nx": "14.1.2", 42 | "@nrwl/jest": "14.1.2", 43 | "@nrwl/linter": "14.1.2", 44 | "@nrwl/node": "14.1.2", 45 | "@nrwl/nx-cloud": "14.0.3", 46 | "@nrwl/tao": "14.1.2", 47 | "@nrwl/workspace": "14.1.2", 48 | "@types/aws-lambda": "8.10.93", 49 | "@types/jest": "27.4.1", 50 | "@types/jsonwebtoken": "8.5.8", 51 | "@types/node": "15.12.4", 52 | "@types/serverless": "3.0.2", 53 | "@types/terser-webpack-plugin": "5.0.4", 54 | "@types/webpack": "5.28.0", 55 | "@typescript-eslint/eslint-plugin": "5.19.0", 56 | "@typescript-eslint/parser": "5.19.0", 57 | "esbuild": "0.14.36", 58 | "eslint": "8.13.0", 59 | "eslint-config-prettier": "8.5.0", 60 | "husky": "7.0.4", 61 | "jest": "27.5.1", 62 | "lambda-event-mock": "1.5.0", 63 | "lint-staged": "12.3.8", 64 | "prettier": "2.6.2", 65 | "serverless": "3.18.1", 66 | "serverless-esbuild": "1.27.1", 67 | "serverless-jest-plugin": "0.4.0", 68 | "serverless-localstack": "0.4.36", 69 | "serverless-offline": "8.8.0", 70 | "ts-jest": "27.1.4", 71 | "ts-loader": "9.2.8", 72 | "ts-node": "9.1.1", 73 | "typescript": "4.6.3" 74 | }, 75 | "engines": { 76 | "node": ">=16" 77 | }, 78 | "lint-staged": { 79 | "*.{js,jsx,ts,tsx}": [ 80 | "eslint --fix", 81 | "prettier --write" 82 | ], 83 | "*.{md,json,yml,yaml,html}": [ 84 | "prettier --write" 85 | ] 86 | } 87 | } -------------------------------------------------------------------------------- /plugins.js: -------------------------------------------------------------------------------- 1 | const { readFileSync } = require('fs'); 2 | 3 | const envPlugin = { 4 | name: 'env', 5 | setup(build) { 6 | build.onResolve({ filter: /@app\/env$/ }, (args) => { 7 | return { 8 | path: args.path, 9 | namespace: 'env-ns', 10 | }; 11 | }); 12 | 13 | build.onLoad({ filter: /.*/, namespace: 'env-ns' }, () => { 14 | const envFile = process.env.NODE_ENV ? `.${process.env.NODE_ENV}` : ''; 15 | const content = readFileSync( 16 | `../../environments/environment${envFile}.ts`, 17 | 'utf8' 18 | ); 19 | 20 | return { 21 | contents: content, 22 | resolveDir: '../../environments', 23 | loader: 'ts', 24 | }; 25 | }); 26 | }, 27 | }; 28 | 29 | module.exports = [envPlugin]; 30 | -------------------------------------------------------------------------------- /serverless.base.ts: -------------------------------------------------------------------------------- 1 | import type { Serverless } from 'serverless/aws'; 2 | import { env, envName } from './environments/environment.serverless'; 3 | 4 | console.log(`-------------- USING ENV: ${env.name} ----------------`); 5 | 6 | export const baseServerlessConfigProvider: Serverless['provider'] = { 7 | name: 'aws', 8 | runtime: 'nodejs16.x', 9 | memorySize: 128, 10 | profile: env.profile, 11 | stage: env.name, 12 | environment: { 13 | NODE_ENV: envName, 14 | AWS_NODEJS_CONNECTION_REUSE_ENABLED: '1', 15 | }, 16 | region: env.region, 17 | }; 18 | 19 | export const baseServerlessConfig: Partial = { 20 | frameworkVersion: '3', 21 | service: 'base', 22 | package: { 23 | individually: true, 24 | excludeDevDependencies: true, 25 | }, 26 | plugins: ['serverless-esbuild', 'serverless-offline'], 27 | custom: { 28 | esbuild: { 29 | bundle: true, 30 | minify: env.name !== 'dev', 31 | target: ['es2020'], 32 | sourcemap: env.name !== 'dev', 33 | sourcesContent: false, 34 | plugins: '../../plugins.js', 35 | define: { 'require.resolve': undefined }, 36 | }, 37 | }, 38 | provider: { 39 | ...baseServerlessConfigProvider, 40 | apiGateway: { 41 | minimumCompressionSize: 1024, 42 | // @ts-ignore 43 | restApiId: { 44 | 'Fn::ImportValue': `${env.name}-AppApiGW-restApiId`, 45 | }, 46 | // @ts-ignore 47 | restApiRootResourceId: { 48 | 'Fn::ImportValue': `${env.name}-AppApiGW-rootResourceId`, 49 | }, 50 | }, 51 | }, 52 | }; 53 | -------------------------------------------------------------------------------- /services/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngneat/nx-serverless/34ded7c4083c2962bad956cafa0772a59899b21e/services/.gitkeep -------------------------------------------------------------------------------- /services/auth/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../.eslintrc.json", 3 | "ignorePatterns": ["!**/*"], 4 | "rules": {} 5 | } 6 | -------------------------------------------------------------------------------- /services/auth/README.md: -------------------------------------------------------------------------------- 1 | # auth 2 | 3 | This stack was generated with [Nx](https://nx.dev). 4 | 5 | ## Running unit tests 6 | 7 | Run `nx test auth` to execute the unit tests via [Jest](https://jestjs.io). 8 | -------------------------------------------------------------------------------- /services/auth/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | displayName: 'auth', 3 | preset: '../../jest.preset.js', 4 | globals: { 5 | 'ts-jest': { 6 | tsconfig: '/tsconfig.spec.json', 7 | }, 8 | }, 9 | testEnvironment: 'node', 10 | coverageDirectory: '../../coverage/services/auth', 11 | }; 12 | -------------------------------------------------------------------------------- /services/auth/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": "services/auth", 3 | "projectType": "application", 4 | "sourceRoot": "services/auth/src", 5 | "targets": { 6 | "build": { 7 | "executor": "@nrwl/workspace:run-commands", 8 | "options": { 9 | "cwd": "services/auth", 10 | "color": true, 11 | "command": "sls package" 12 | } 13 | }, 14 | "serve": { 15 | "executor": "@nrwl/workspace:run-commands", 16 | "options": { 17 | "cwd": "services/auth", 18 | "color": true, 19 | "command": "sls offline start" 20 | } 21 | }, 22 | "deploy": { 23 | "executor": "@nrwl/workspace:run-commands", 24 | "options": { 25 | "cwd": "services/auth", 26 | "color": true, 27 | "command": "sls deploy --verbose" 28 | }, 29 | "dependsOn": [ 30 | { 31 | "target": "deploy", 32 | "projects": "dependencies" 33 | } 34 | ] 35 | }, 36 | "remove": { 37 | "executor": "@nrwl/workspace:run-commands", 38 | "options": { 39 | "cwd": "services/auth", 40 | "color": true, 41 | "command": "sls remove" 42 | } 43 | }, 44 | "lint": { 45 | "executor": "@nrwl/linter:eslint", 46 | "options": { 47 | "lintFilePatterns": ["services/auth/**/*.ts"] 48 | } 49 | }, 50 | "test": { 51 | "executor": "@nrwl/jest:jest", 52 | "outputs": ["coverage/services/auth"], 53 | "options": { 54 | "jestConfig": "services/auth/jest.config.js", 55 | "passWithNoTests": true 56 | } 57 | } 58 | }, 59 | "tags": ["service"], 60 | "implicitDependencies": ["core"] 61 | } 62 | -------------------------------------------------------------------------------- /services/auth/serverless.ts: -------------------------------------------------------------------------------- 1 | import type { Serverless } from 'serverless/aws'; 2 | import { baseServerlessConfig } from '../../serverless.base'; 3 | import { tableResource } from '../../environments/environment.serverless'; 4 | 5 | const serverlessConfig: Partial = { 6 | ...baseServerlessConfig, 7 | service: 'auth', 8 | provider: { 9 | ...baseServerlessConfig.provider, 10 | iam: { 11 | role: { 12 | statements: [ 13 | { 14 | Effect: 'Allow', 15 | Action: ['dynamodb:PutItem'], 16 | Resource: tableResource, 17 | }, 18 | ], 19 | }, 20 | }, 21 | }, 22 | custom: { 23 | ...baseServerlessConfig.custom, 24 | 'serverless-offline': { 25 | lambdaPort: 3000, 26 | httpPort: 3001, 27 | }, 28 | }, 29 | functions: { 30 | 'sign-up': { 31 | handler: 'src/sign-up/sign-up-handler.main', 32 | events: [ 33 | { 34 | http: { 35 | method: 'post', 36 | path: 'auth/sign-up', 37 | }, 38 | }, 39 | ], 40 | }, 41 | }, 42 | }; 43 | 44 | module.exports = serverlessConfig; 45 | -------------------------------------------------------------------------------- /services/auth/src/auth.utils.ts: -------------------------------------------------------------------------------- 1 | import { sign } from 'jsonwebtoken'; 2 | import { env } from '@app/env'; 3 | 4 | export function createJWT(id: string) { 5 | return sign( 6 | { 7 | id, 8 | }, 9 | env.jwtSecret, 10 | { expiresIn: '5d' } 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /services/auth/src/sign-up/sign-in-handler.spec.ts: -------------------------------------------------------------------------------- 1 | describe('sign-up', () => { 2 | it('should do something useful', async () => { 3 | expect(true).toBeTruthy(); 4 | }); 5 | }); 6 | -------------------------------------------------------------------------------- /services/auth/src/sign-up/sign-up-handler.ts: -------------------------------------------------------------------------------- 1 | import { object, string } from 'yup'; 2 | import { BodyParams } from '@app/http/types'; 3 | import { createHandler } from '@app/http/handlers'; 4 | import { httpResponse, httpError } from '@app/http/response'; 5 | import { schemaValidator } from '@app/http/schema-validator.middleware'; 6 | import { createJWT } from '../auth.utils'; 7 | import { createUser, User } from '@app/users/user.model'; 8 | 9 | type Params = BodyParams<{ email: string; name: string }>; 10 | 11 | export const main = createHandler(async (event) => { 12 | const { email, name } = event.body; 13 | 14 | try { 15 | await createUser(new User({ email, name })); 16 | 17 | return httpResponse({ 18 | token: createJWT(email), 19 | }); 20 | } catch (error) { 21 | return httpError(error); 22 | } 23 | }); 24 | 25 | main.use([ 26 | schemaValidator({ 27 | body: object({ 28 | email: string().email().required(), 29 | name: string().required(), 30 | }), 31 | }), 32 | ]); 33 | -------------------------------------------------------------------------------- /services/auth/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "types": ["node"] 6 | }, 7 | "exclude": ["**/*.spec.ts"], 8 | "include": ["**/*.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /services/auth/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.app.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | }, 12 | { 13 | "path": "./tsconfig.spec.json" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /services/auth/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": ["**/*.test.ts", "**/*.spec.ts", "**/*.d.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /services/core/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../.eslintrc.json", 3 | "ignorePatterns": ["!**/*"], 4 | "rules": {} 5 | } 6 | -------------------------------------------------------------------------------- /services/core/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | displayName: 'core', 3 | preset: '../../jest.preset.js', 4 | globals: { 5 | 'ts-jest': { 6 | tsconfig: '/tsconfig.spec.json', 7 | }, 8 | }, 9 | testEnvironment: 'node', 10 | coverageDirectory: '../../coverage/stacks/api', 11 | }; 12 | -------------------------------------------------------------------------------- /services/core/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": "services/core", 3 | "projectType": "application", 4 | "sourceRoot": "services/core/src", 5 | "targets": { 6 | "deploy": { 7 | "executor": "@nrwl/workspace:run-commands", 8 | "options": { 9 | "cwd": "services/core", 10 | "color": true, 11 | "command": "sls deploy --verbose" 12 | } 13 | }, 14 | "remove": { 15 | "executor": "@nrwl/workspace:run-commands", 16 | "options": { 17 | "cwd": "services/core", 18 | "color": true, 19 | "command": "sls remove" 20 | } 21 | } 22 | }, 23 | "tags": ["services"] 24 | } 25 | -------------------------------------------------------------------------------- /services/core/serverless.ts: -------------------------------------------------------------------------------- 1 | import { env } from '../../environments/environment.serverless'; 2 | import type { Serverless } from 'serverless/aws'; 3 | import { baseServerlessConfigProvider } from '../../serverless.base'; 4 | 5 | const serverlessConfig: Partial = { 6 | provider: baseServerlessConfigProvider, 7 | plugins: ['serverless-localstack'], 8 | service: 'core', 9 | custom: { 10 | localstack: { 11 | stages: ['local'], 12 | lambda: { 13 | mountCode: 'True', 14 | }, 15 | }, 16 | }, 17 | resources: { 18 | Resources: { 19 | AppApiGW: { 20 | Type: 'AWS::ApiGateway::RestApi', 21 | Properties: { 22 | Name: `${env.name}-AppApiGW`, 23 | }, 24 | }, 25 | AppTable: { 26 | Type: 'AWS::DynamoDB::Table', 27 | Properties: { 28 | TableName: env.dynamo.tableName, 29 | AttributeDefinitions: [ 30 | { 31 | AttributeName: 'PK', 32 | AttributeType: 'S', 33 | }, 34 | { 35 | AttributeName: 'SK', 36 | AttributeType: 'S', 37 | }, 38 | ], 39 | KeySchema: [ 40 | { 41 | AttributeName: 'PK', 42 | KeyType: 'HASH', 43 | }, 44 | { 45 | AttributeName: 'SK', 46 | KeyType: 'RANGE', 47 | }, 48 | ], 49 | ProvisionedThroughput: { 50 | ReadCapacityUnits: 1, 51 | WriteCapacityUnits: 1, 52 | }, 53 | }, 54 | }, 55 | }, 56 | Outputs: { 57 | ApiGatewayRestApiId: { 58 | Value: { 59 | Ref: 'AppApiGW', 60 | }, 61 | Export: { 62 | Name: `${env.name}-AppApiGW-restApiId`, 63 | }, 64 | }, 65 | ApiGatewayRestApiRootResourceId: { 66 | Value: { 67 | 'Fn::GetAtt': ['AppApiGW', 'RootResourceId'], 68 | }, 69 | Export: { 70 | Name: `${env.name}-AppApiGW-rootResourceId`, 71 | }, 72 | }, 73 | }, 74 | }, 75 | }; 76 | 77 | module.exports = serverlessConfig; 78 | -------------------------------------------------------------------------------- /services/core/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "types": ["node"] 6 | }, 7 | "exclude": ["**/*.spec.ts"], 8 | "include": ["**/*.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /services/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.app.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | }, 12 | { 13 | "path": "./tsconfig.spec.json" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /services/core/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": ["**/*.test.ts", "**/*.spec.ts", "**/*.d.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /services/todos/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../.eslintrc.json", 3 | "ignorePatterns": ["!**/*"], 4 | "rules": {} 5 | } 6 | -------------------------------------------------------------------------------- /services/todos/README.md: -------------------------------------------------------------------------------- 1 | # todos 2 | 3 | This stack was generated with [Nx](https://nx.dev). 4 | 5 | ## Running unit tests 6 | 7 | Run `nx test todos` to execute the unit tests via [Jest](https://jestjs.io). 8 | -------------------------------------------------------------------------------- /services/todos/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | displayName: 'todos', 3 | preset: '../../jest.preset.js', 4 | globals: { 5 | 'ts-jest': { 6 | tsconfig: '/tsconfig.spec.json', 7 | }, 8 | }, 9 | testEnvironment: 'node', 10 | coverageDirectory: '../../coverage/services/todos', 11 | }; 12 | -------------------------------------------------------------------------------- /services/todos/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": "services/todos", 3 | "projectType": "application", 4 | "sourceRoot": "services/todos/src", 5 | "targets": { 6 | "build": { 7 | "executor": "@nrwl/workspace:run-commands", 8 | "options": { 9 | "cwd": "services/todos", 10 | "color": true, 11 | "command": "sls package" 12 | } 13 | }, 14 | "serve": { 15 | "executor": "@nrwl/workspace:run-commands", 16 | "options": { 17 | "cwd": "services/todos", 18 | "color": true, 19 | "command": "sls offline start" 20 | } 21 | }, 22 | "deploy": { 23 | "executor": "@nrwl/workspace:run-commands", 24 | "options": { 25 | "cwd": "services/todos", 26 | "color": true, 27 | "command": "sls deploy --verbose" 28 | }, 29 | "dependsOn": [ 30 | { 31 | "target": "deploy", 32 | "projects": "dependencies" 33 | } 34 | ] 35 | }, 36 | "remove": { 37 | "executor": "@nrwl/workspace:run-commands", 38 | "options": { 39 | "cwd": "services/todos", 40 | "color": true, 41 | "command": "sls remove" 42 | } 43 | }, 44 | "lint": { 45 | "executor": "@nrwl/linter:eslint", 46 | "options": { 47 | "lintFilePatterns": ["services/todos/**/*.ts"] 48 | } 49 | }, 50 | "test": { 51 | "executor": "@nrwl/jest:jest", 52 | "outputs": ["coverage/services/todos"], 53 | "options": { 54 | "jestConfig": "services/todos/jest.config.js", 55 | "passWithNoTests": true 56 | } 57 | } 58 | }, 59 | "tags": ["service"], 60 | "implicitDependencies": ["core"] 61 | } 62 | -------------------------------------------------------------------------------- /services/todos/serverless.ts: -------------------------------------------------------------------------------- 1 | import { tableResource } from '../../environments/environment.serverless'; 2 | import type { Serverless } from 'serverless/aws'; 3 | import { baseServerlessConfig } from '../../serverless.base'; 4 | 5 | const serverlessConfig: Partial = { 6 | ...baseServerlessConfig, 7 | service: `todos`, 8 | provider: { 9 | ...baseServerlessConfig.provider, 10 | iam: { 11 | role: { 12 | statements: [ 13 | { 14 | Effect: 'Allow', 15 | Action: [ 16 | 'dynamodb:Query', 17 | 'dynamodb:GetItem', 18 | 'dynamodb:PutItem', 19 | 'dynamodb:UpdateItem', 20 | ], 21 | Resource: tableResource, 22 | }, 23 | ], 24 | }, 25 | }, 26 | }, 27 | custom: { 28 | ...baseServerlessConfig.custom, 29 | 'serverless-offline': { 30 | lambdaPort: 3004, 31 | httpPort: 3005, 32 | }, 33 | }, 34 | functions: { 35 | 'get-todos': { 36 | handler: 'src/get-todos/get-todos-handler.main', 37 | events: [ 38 | { 39 | http: { 40 | method: 'get', 41 | path: 'todos', 42 | }, 43 | }, 44 | ], 45 | }, 46 | 'get-todo': { 47 | handler: 'src/get-todo/get-todo-handler.main', 48 | events: [ 49 | { 50 | http: { 51 | method: 'get', 52 | path: 'todos/{id}', 53 | }, 54 | }, 55 | ], 56 | }, 57 | 'create-todo': { 58 | handler: 'src/create-todo/create-todo-handler.main', 59 | events: [ 60 | { 61 | http: { 62 | method: 'post', 63 | path: 'todos', 64 | }, 65 | }, 66 | ], 67 | }, 68 | 'update-todo': { 69 | handler: 'src/update-todo/update-todo-handler.main', 70 | events: [ 71 | { 72 | http: { 73 | method: 'put', 74 | path: 'todos/{id}', 75 | }, 76 | }, 77 | ], 78 | }, 79 | }, 80 | }; 81 | 82 | module.exports = serverlessConfig; 83 | -------------------------------------------------------------------------------- /services/todos/src/create-todo/create-todo-handler.spec.ts: -------------------------------------------------------------------------------- 1 | describe('create-todo', () => { 2 | it('should do something useful', async () => { 3 | expect(true).toBe(true); 4 | }); 5 | }); 6 | -------------------------------------------------------------------------------- /services/todos/src/create-todo/create-todo-handler.ts: -------------------------------------------------------------------------------- 1 | import { BodyParams } from '@app/http/types'; 2 | import { createProtectedHandler } from '@app/http/handlers'; 3 | import { httpError, httpResponse } from '@app/http/response'; 4 | import { schemaValidator } from '@app/http/schema-validator.middleware'; 5 | import { createTodo, Todo } from '../todo.model'; 6 | import { ulid } from 'ulid'; 7 | import { UserKeys } from '@app/users/user.model'; 8 | import { object, string } from 'yup'; 9 | 10 | type Params = BodyParams<{ title: string }>; 11 | 12 | export const main = createProtectedHandler(async (event, context) => { 13 | const userKeys = new UserKeys(context.user.id); 14 | 15 | const todo = new Todo( 16 | { id: ulid(), completed: false, title: event.body.title }, 17 | userKeys 18 | ); 19 | 20 | try { 21 | const newTodo = await createTodo(todo); 22 | 23 | return httpResponse({ 24 | todo: newTodo, 25 | }); 26 | } catch (e) { 27 | return httpError(e); 28 | } 29 | }); 30 | 31 | main.use([ 32 | schemaValidator({ 33 | body: object({ 34 | title: string().required(), 35 | }), 36 | }), 37 | ]); 38 | -------------------------------------------------------------------------------- /services/todos/src/get-todo/get-todo-handler.spec.ts: -------------------------------------------------------------------------------- 1 | describe('get-todo', () => { 2 | it('should do something useful', async () => { 3 | expect(true).toBe(true); 4 | }); 5 | }); 6 | -------------------------------------------------------------------------------- /services/todos/src/get-todo/get-todo-handler.ts: -------------------------------------------------------------------------------- 1 | import { PathParams } from '@app/http/types'; 2 | import { createProtectedHandler } from '@app/http/handlers'; 3 | import { httpError, httpResponse } from '@app/http/response'; 4 | import { UserKeys } from '@app/users/user.model'; 5 | import { getTodo, TodoKeys } from '../todo.model'; 6 | 7 | type Params = PathParams<{ id: string }>; 8 | 9 | export const main = createProtectedHandler(async (event, context) => { 10 | const userKeys = new UserKeys(context.user.id); 11 | const todoKeys = new TodoKeys(event.pathParameters.id, userKeys); 12 | 13 | try { 14 | const todo = await getTodo(todoKeys); 15 | 16 | return httpResponse({ 17 | todo, 18 | }); 19 | } catch (e) { 20 | return httpError(e); 21 | } 22 | }); 23 | -------------------------------------------------------------------------------- /services/todos/src/get-todos/get-todos-handler.spec.ts: -------------------------------------------------------------------------------- 1 | describe('get-todos', () => { 2 | it('should do something useful', async () => { 3 | expect(true).toBe(true); 4 | }); 5 | }); 6 | -------------------------------------------------------------------------------- /services/todos/src/get-todos/get-todos-handler.ts: -------------------------------------------------------------------------------- 1 | import { QueryParams } from '@app/http/types'; 2 | import { createProtectedHandler } from '@app/http/handlers'; 3 | import { httpError, httpResponse } from '@app/http/response'; 4 | import { UserKeys } from '@app/users/user.model'; 5 | import { getTodos } from '../todo.model'; 6 | 7 | type Params = QueryParams<{ searchTerm: string }>; 8 | 9 | export const main = createProtectedHandler(async (event, context) => { 10 | const userKeys = new UserKeys(context.user.id); 11 | 12 | try { 13 | const todos = await getTodos(userKeys); 14 | 15 | return httpResponse({ 16 | todos, 17 | }); 18 | } catch (e) { 19 | return httpError(e); 20 | } 21 | }); 22 | -------------------------------------------------------------------------------- /services/todos/src/todo.model.ts: -------------------------------------------------------------------------------- 1 | import { createItem, query, updateItem, getItem } from '@app/db/operations'; 2 | import { Item, ItemKeys } from '@app/db/item'; 3 | import { DynamoDB } from 'aws-sdk'; 4 | import { UserKeys } from '@app/users/user.model'; 5 | 6 | export interface TodoModel { 7 | id: string; 8 | title: string; 9 | completed: boolean; 10 | } 11 | 12 | export class TodoKeys extends ItemKeys { 13 | static ENTITY_TYPE = 'TODO'; 14 | 15 | constructor(private todoId: string, private userKeys: UserKeys) { 16 | super(); 17 | } 18 | 19 | get pk() { 20 | return this.userKeys.pk; 21 | } 22 | 23 | get sk() { 24 | return `${TodoKeys.ENTITY_TYPE}#${this.todoId}`; 25 | } 26 | } 27 | 28 | export class Todo extends Item { 29 | constructor(private todo: TodoModel, private userKeys: UserKeys) { 30 | super(); 31 | } 32 | 33 | get keys() { 34 | return new TodoKeys(this.todo.id, this.userKeys); 35 | } 36 | 37 | static fromItem(attributeMap: DynamoDB.AttributeMap): TodoModel { 38 | return { 39 | id: attributeMap.id.S, 40 | title: attributeMap.title.S, 41 | completed: attributeMap.completed.BOOL, 42 | }; 43 | } 44 | 45 | toItem() { 46 | return this.marshall(this.todo); 47 | } 48 | } 49 | 50 | export async function createTodo(todo: Todo): Promise { 51 | await createItem(todo); 52 | 53 | return Todo.fromItem(todo.toItem()); 54 | } 55 | 56 | export async function getTodo(todoKeys: TodoKeys) { 57 | const result = await getItem(todoKeys); 58 | 59 | return Todo.fromItem(result.Item); 60 | } 61 | 62 | export async function updateTodo( 63 | todoKeys: TodoKeys, 64 | completed: TodoModel['completed'] 65 | ) { 66 | await updateItem(todoKeys, { 67 | UpdateExpression: 'SET #completed = :completed', 68 | ExpressionAttributeValues: { 69 | ':completed': { BOOL: completed }, 70 | }, 71 | ExpressionAttributeNames: { 72 | '#completed': 'completed', 73 | }, 74 | }); 75 | 76 | return { success: true }; 77 | } 78 | 79 | export async function getTodos(userKeys: UserKeys) { 80 | const result = await query({ 81 | KeyConditionExpression: `PK = :PK AND begins_with(SK, :SK)`, 82 | ExpressionAttributeValues: { 83 | ':PK': { S: userKeys.pk }, 84 | ':SK': { S: TodoKeys.ENTITY_TYPE }, 85 | }, 86 | }); 87 | 88 | return result.Items.map(Todo.fromItem); 89 | } 90 | -------------------------------------------------------------------------------- /services/todos/src/update-todo/update-todo-handler.spec.ts: -------------------------------------------------------------------------------- 1 | describe('update-todo', () => { 2 | it('should do something useful', async () => { 3 | expect(true).toBe(true); 4 | }); 5 | }); 6 | -------------------------------------------------------------------------------- /services/todos/src/update-todo/update-todo-handler.ts: -------------------------------------------------------------------------------- 1 | import { PathParams, BodyParams } from '@app/http/types'; 2 | import { createProtectedHandler } from '@app/http/handlers'; 3 | import { httpError, httpResponse } from '@app/http/response'; 4 | import { UserKeys } from '@app/users/user.model'; 5 | import { TodoKeys, TodoModel, updateTodo } from '../todo.model'; 6 | 7 | type Params = PathParams<{ id: string }> & 8 | BodyParams<{ completed: TodoModel['completed'] }>; 9 | 10 | export const main = createProtectedHandler(async (event, context) => { 11 | const userKeys = new UserKeys(context.user.id); 12 | const todoKeys = new TodoKeys(event.pathParameters.id, userKeys); 13 | 14 | try { 15 | const result = await updateTodo(todoKeys, event.body.completed); 16 | 17 | return httpResponse(result); 18 | } catch (e) { 19 | return httpError(e); 20 | } 21 | }); 22 | -------------------------------------------------------------------------------- /services/todos/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "types": ["node"] 6 | }, 7 | "exclude": ["**/*.spec.ts"], 8 | "include": ["**/*.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /services/todos/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.app.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | }, 12 | { 13 | "path": "./tsconfig.spec.json" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /services/todos/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": ["**/*.test.ts", "**/*.spec.ts", "**/*.d.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /services/users/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../.eslintrc.json", 3 | "ignorePatterns": ["!**/*"], 4 | "rules": {} 5 | } 6 | -------------------------------------------------------------------------------- /services/users/README.md: -------------------------------------------------------------------------------- 1 | # users 2 | 3 | This stack was generated with [Nx](https://nx.dev). 4 | 5 | ## Running unit tests 6 | 7 | Run `nx test users` to execute the unit tests via [Jest](https://jestjs.io). 8 | -------------------------------------------------------------------------------- /services/users/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | displayName: 'users', 3 | preset: '../../jest.preset.js', 4 | globals: { 5 | 'ts-jest': { 6 | tsconfig: '/tsconfig.spec.json', 7 | }, 8 | }, 9 | testEnvironment: 'node', 10 | coverageDirectory: '../../coverage/services/users', 11 | }; 12 | -------------------------------------------------------------------------------- /services/users/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": "services/users", 3 | "projectType": "application", 4 | "sourceRoot": "services/users/src", 5 | "targets": { 6 | "build": { 7 | "executor": "@nrwl/workspace:run-commands", 8 | "options": { 9 | "cwd": "services/users", 10 | "color": true, 11 | "command": "sls package" 12 | } 13 | }, 14 | "serve": { 15 | "executor": "@nrwl/workspace:run-commands", 16 | "options": { 17 | "cwd": "services/users", 18 | "color": true, 19 | "command": "sls offline start" 20 | } 21 | }, 22 | "deploy": { 23 | "executor": "@nrwl/workspace:run-commands", 24 | "options": { 25 | "cwd": "services/users", 26 | "color": true, 27 | "command": "sls deploy --verbose" 28 | }, 29 | "dependsOn": [ 30 | { 31 | "target": "deploy", 32 | "projects": "dependencies" 33 | } 34 | ] 35 | }, 36 | "remove": { 37 | "executor": "@nrwl/workspace:run-commands", 38 | "options": { 39 | "cwd": "services/users", 40 | "color": true, 41 | "command": "sls remove" 42 | } 43 | }, 44 | "lint": { 45 | "executor": "@nrwl/linter:eslint", 46 | "options": { 47 | "lintFilePatterns": ["services/users/**/*.ts"] 48 | } 49 | }, 50 | "test": { 51 | "executor": "@nrwl/jest:jest", 52 | "outputs": ["coverage/services/users"], 53 | "options": { 54 | "jestConfig": "services/users/jest.config.js", 55 | "passWithNoTests": true 56 | } 57 | } 58 | }, 59 | "tags": ["service"], 60 | "implicitDependencies": ["core"] 61 | } 62 | -------------------------------------------------------------------------------- /services/users/serverless.ts: -------------------------------------------------------------------------------- 1 | import type { Serverless } from 'serverless/aws'; 2 | import { baseServerlessConfig } from '../../serverless.base'; 3 | import { tableResource } from '../../environments/environment.serverless'; 4 | 5 | const serverlessConfig: Partial = { 6 | ...baseServerlessConfig, 7 | service: 'users', 8 | provider: { 9 | ...baseServerlessConfig.provider, 10 | iam: { 11 | role: { 12 | statements: [ 13 | { 14 | Effect: 'Allow', 15 | Action: ['dynamodb:GetItem'], 16 | Resource: tableResource, 17 | }, 18 | ], 19 | }, 20 | }, 21 | }, 22 | custom: { 23 | ...baseServerlessConfig.custom, 24 | 'serverless-offline': { 25 | lambdaPort: 3002, 26 | httpPort: 3003, 27 | }, 28 | }, 29 | functions: { 30 | 'get-user': { 31 | handler: 'src/get-user/get-user-handler.main', 32 | events: [ 33 | { 34 | http: { 35 | method: 'get', 36 | path: 'user', 37 | }, 38 | }, 39 | ], 40 | }, 41 | }, 42 | }; 43 | 44 | module.exports = serverlessConfig; 45 | -------------------------------------------------------------------------------- /services/users/src/get-user/get-user-handler.spec.ts: -------------------------------------------------------------------------------- 1 | describe('get-user', () => { 2 | it('should do something useful', async () => { 3 | expect(true).toBe(true); 4 | }); 5 | }); 6 | -------------------------------------------------------------------------------- /services/users/src/get-user/get-user-handler.ts: -------------------------------------------------------------------------------- 1 | import { createProtectedHandler } from '@app/http/handlers'; 2 | import { httpError, httpResponse } from '@app/http/response'; 3 | 4 | import { getUser, UserKeys } from '../user.model'; 5 | 6 | export const main = createProtectedHandler(async (_, context) => { 7 | try { 8 | const user = await getUser(new UserKeys(context.user.id)); 9 | 10 | return httpResponse({ 11 | user, 12 | }); 13 | } catch (e) { 14 | return httpError(e); 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /services/users/src/user.model.ts: -------------------------------------------------------------------------------- 1 | import { createItem, getItem } from '@app/db/operations'; 2 | import { Item, ItemKeys } from '@app/db/item'; 3 | import { DynamoDB } from 'aws-sdk'; 4 | 5 | export interface UserModel { 6 | email: string; 7 | name: string; 8 | } 9 | 10 | export class UserKeys extends ItemKeys { 11 | static ENTITY_TYPE = 'USER'; 12 | 13 | constructor(private email: string) { 14 | super(); 15 | } 16 | 17 | get pk() { 18 | return `${UserKeys.ENTITY_TYPE}#${this.email}`; 19 | } 20 | 21 | get sk() { 22 | return this.pk; 23 | } 24 | } 25 | 26 | export class User extends Item { 27 | constructor(private user: UserModel) { 28 | super(); 29 | } 30 | 31 | static fromItem(attributeMap: DynamoDB.AttributeMap): UserModel { 32 | return { 33 | email: attributeMap.email.S, 34 | name: attributeMap.name.S, 35 | }; 36 | } 37 | 38 | get keys() { 39 | return new UserKeys(this.user.email); 40 | } 41 | 42 | toItem() { 43 | return this.marshall(this.user); 44 | } 45 | } 46 | 47 | export async function createUser(user: User): Promise { 48 | await createItem(user); 49 | 50 | return User.fromItem(user.toItem()); 51 | } 52 | 53 | export async function getUser(userKeys: UserKeys): Promise { 54 | const result = await getItem(userKeys); 55 | 56 | return User.fromItem(result.Item); 57 | } 58 | -------------------------------------------------------------------------------- /services/users/src/users.types.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngneat/nx-serverless/34ded7c4083c2962bad956cafa0772a59899b21e/services/users/src/users.types.ts -------------------------------------------------------------------------------- /services/users/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "types": ["node"] 6 | }, 7 | "exclude": ["**/*.spec.ts"], 8 | "include": ["**/*.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /services/users/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.app.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | }, 12 | { 13 | "path": "./tsconfig.spec.json" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /services/users/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": ["**/*.test.ts", "**/*.spec.ts", "**/*.d.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /tools/generators/handler/files/__fileName__/__fileName__-handler.spec.ts__tmpl__: -------------------------------------------------------------------------------- 1 | import { main } from './<%= name %>-handler'; 2 | 3 | describe('<%= name %>', () => { 4 | 5 | it('should do something useful', async () => { 6 | expect(main).toBe(true); 7 | }) 8 | 9 | }); 10 | -------------------------------------------------------------------------------- /tools/generators/handler/files/__fileName__/__fileName__-handler.ts__tmpl__: -------------------------------------------------------------------------------- 1 | export const main = async (event) => { 2 | 3 | }; 4 | -------------------------------------------------------------------------------- /tools/generators/handler/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Tree, 3 | formatFiles, 4 | installPackagesTask, 5 | names, 6 | generateFiles, 7 | joinPathFragments, 8 | } from '@nrwl/devkit'; 9 | 10 | interface Schema { 11 | name: string; 12 | project: string; 13 | } 14 | 15 | export default async function (tree: Tree, schema: Schema) { 16 | const serviceRoot = `services/${schema.project}/src`; 17 | 18 | const { fileName } = names(schema.name); 19 | 20 | generateFiles(tree, joinPathFragments(__dirname, './files'), serviceRoot, { 21 | ...schema, 22 | tmpl: '', 23 | fileName, 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /tools/generators/handler/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/schema", 3 | "cli": "nx", 4 | "$id": "handler", 5 | "type": "object", 6 | "properties": { 7 | "name": { 8 | "type": "string", 9 | "description": "Handler name", 10 | "x-prompt": "What is the name of the handler?", 11 | "$default": { 12 | "$source": "argv", 13 | "index": 0 14 | } 15 | }, 16 | "project": { 17 | "type": "string", 18 | "description": "Project name", 19 | "x-prompt": "What is the name of the project?" 20 | } 21 | }, 22 | "required": [ 23 | "name", 24 | "project" 25 | ] 26 | } -------------------------------------------------------------------------------- /tools/generators/http-handler/files/__fileName__/__fileName__-handler.spec.ts__tmpl__: -------------------------------------------------------------------------------- 1 | import lambdaEventMock from 'lambda-event-mock'; 2 | import { Context } from 'aws-lambda'; 3 | import { main } from './<%= name %>-handler'; 4 | 5 | describe('<%= name %>', () => { 6 | 7 | it('should do something useful', async () => { 8 | 9 | const event = lambdaEventMock.apiGateway() 10 | .path('/todos') 11 | .queryStringParameters({ 12 | searchTerm: 'foo', 13 | }) 14 | .method('GET') 15 | .build(); 16 | 17 | const { body } = await main(event, {} as any); 18 | 19 | expect(JSON.parse(body).data.searchTerm).toEqual('foo') 20 | }) 21 | 22 | }); 23 | -------------------------------------------------------------------------------- /tools/generators/http-handler/files/__fileName__/__fileName__-handler.ts__tmpl__: -------------------------------------------------------------------------------- 1 | import type { QueryParams } from '@app/http/types'; 2 | import { createProtectedHandler } from '@app/http/handlers'; 3 | import { httpResponse } from '@app/http/response'; 4 | 5 | type Params = QueryParams<{ searchTerm: string }>; 6 | 7 | export const main = createProtectedHandler(async (event) => { 8 | 9 | return httpResponse({ 10 | searchTerm: event.queryStringParameters?.searchTerm, 11 | }) 12 | }); 13 | -------------------------------------------------------------------------------- /tools/generators/http-handler/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Tree, 3 | names, 4 | generateFiles, 5 | joinPathFragments, 6 | getProjects, 7 | logger 8 | } from '@nrwl/devkit'; 9 | 10 | import { 11 | Project, 12 | ScriptTarget, 13 | SyntaxKind, 14 | ObjectLiteralExpression, 15 | Writers, 16 | PropertyAssignment, 17 | } from 'ts-morph'; 18 | 19 | const project = new Project({ 20 | compilerOptions: { 21 | target: ScriptTarget.ES2020, 22 | }, 23 | }); 24 | 25 | interface Schema { 26 | name: string; 27 | project: string; 28 | method: string; 29 | path: string; 30 | } 31 | 32 | export default async function (tree: Tree, schema: Schema) { 33 | if (!getProjects(tree).has(schema.project)) { 34 | logger.error(`Project ${schema.project} does not exist.`); 35 | 36 | return; 37 | } 38 | 39 | const root = `services/${schema.project}`; 40 | const serviceSource = joinPathFragments(root, 'src'); 41 | 42 | const n = names(schema.name); 43 | const serverlessPath = joinPathFragments(`services/${schema.project}`, 'serverless.ts'); 44 | const serverless = tree.read(serverlessPath).toString() 45 | 46 | generateFiles(tree, joinPathFragments(__dirname, './files'), serviceSource, { 47 | ...schema, 48 | tmpl: '', 49 | fileName: n.fileName, 50 | }); 51 | 52 | const sourceFile = project.createSourceFile('serverless.ts', serverless); 53 | const dec = sourceFile.getVariableDeclaration('serverlessConfig'); 54 | const objectLiteralExpression = dec!.getInitializerIfKindOrThrow( 55 | SyntaxKind.ObjectLiteralExpression 56 | ) as ObjectLiteralExpression; 57 | 58 | const funcProp = objectLiteralExpression.getProperty( 59 | 'functions' 60 | ) as PropertyAssignment; 61 | 62 | const funcValue = funcProp.getInitializer() as ObjectLiteralExpression; 63 | 64 | funcValue.addPropertyAssignment({ 65 | initializer: (writer) => { 66 | return Writers.object({ 67 | handler: `'src/${n.fileName}/${n.fileName}-handler.main'`, 68 | events: (writer) => { 69 | writer.write('['); 70 | Writers.object({ 71 | http: Writers.object({ 72 | method: `'${schema.method.toLowerCase()}'`, 73 | path: `'${schema.path}'`, 74 | }), 75 | })(writer); 76 | writer.write(']'); 77 | }, 78 | })(writer); 79 | }, 80 | name: `'${n.fileName}'`, 81 | }); 82 | 83 | sourceFile.formatText({ indentSize: 2 }); 84 | 85 | tree.write(serverlessPath, sourceFile.getText()); 86 | } 87 | -------------------------------------------------------------------------------- /tools/generators/http-handler/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/schema", 3 | "cli": "nx", 4 | "$id": "http-handler", 5 | "type": "object", 6 | "properties": { 7 | "name": { 8 | "type": "string", 9 | "description": "Handler name", 10 | "x-prompt": "What is the name of the handler?", 11 | "$default": { 12 | "$source": "argv", 13 | "index": 0 14 | } 15 | }, 16 | "project": { 17 | "type": "string", 18 | "description": "Project name", 19 | "x-prompt": "What is the name of the project?" 20 | }, 21 | "method": { 22 | "description": "The HTTP method", 23 | "type": "string", 24 | "default": "GET", 25 | "enum": [ 26 | "GET", 27 | "POST", 28 | "PUT", 29 | "DELETE", 30 | "PATCH", 31 | "HEAD", 32 | "OPTIONS" 33 | ], 34 | "x-prompt": { 35 | "message": "Which HTTP method you want to use?", 36 | "type": "list", 37 | "items": [ 38 | { 39 | "value": "GET", 40 | "label": "GET" 41 | }, 42 | { 43 | "value": "POST", 44 | "label": "POST" 45 | }, 46 | { 47 | "value": "PUT", 48 | "label": "PUT" 49 | }, 50 | { 51 | "value": "DELETE", 52 | "label": "DELETE" 53 | }, 54 | { 55 | "value": "PATCH", 56 | "label": "PATCH" 57 | }, 58 | { 59 | "value": "HEAD", 60 | "label": "HEAD" 61 | }, 62 | { 63 | "value": "OPTIONS", 64 | "label": "OPTIONS" 65 | } 66 | ] 67 | } 68 | }, 69 | "path": { 70 | "type": "string", 71 | "description": "Path of the handler", 72 | "x-prompt": "What is the handler path?" 73 | } 74 | }, 75 | "required": [ 76 | "name", 77 | "project", 78 | "path", 79 | "method" 80 | ] 81 | } -------------------------------------------------------------------------------- /tools/generators/model/files/__fileName__.model.ts__tmpl__: -------------------------------------------------------------------------------- 1 | import { 2 | createItem, 3 | getItem, 4 | } from '@app/db/operations'; 5 | import { Item, ItemKeys } from '@app/db/item'; 6 | import { DynamoDB } from 'aws-sdk'; 7 | 8 | export interface <%= className %>Model { 9 | id: string; 10 | } 11 | 12 | export class <%= className %>Keys extends ItemKeys { 13 | static ENTITY_TYPE = '<%= constantName %>'; 14 | 15 | constructor(private id: string) { 16 | super(); 17 | } 18 | 19 | get pk() { 20 | return `${<%= className %>Keys.ENTITY_TYPE}#${this.id}`; 21 | } 22 | 23 | get sk() { 24 | return this.pk; 25 | } 26 | } 27 | 28 | export class <%= className %> extends Item<<%= className %>Model> { 29 | constructor(public <%= propertyName %>: <%= className %>Model) { 30 | super(); 31 | } 32 | 33 | get keys() { 34 | return new <%= className %>Keys(this.<%= propertyName %>.id); 35 | } 36 | 37 | static fromItem(attributeMap: DynamoDB.AttributeMap): <%= className %>Model { 38 | return { 39 | id: attributeMap.id.S 40 | }; 41 | } 42 | 43 | toItem() { 44 | return this.marshall(this.<%= propertyName %>); 45 | } 46 | } 47 | 48 | 49 | export async function create<%= className %>(<%= propertyName %>: <%= className %>): Promise<<%= className %>Model> { 50 | await createItem(<%= propertyName %>); 51 | 52 | return <%= className %>.fromItem(<%= propertyName %>.toItem()); 53 | } 54 | 55 | export async function get<%= className %>(<%= propertyName %>Keys: <%= className %>Keys) { 56 | const result = await getItem(<%= propertyName %>Keys); 57 | 58 | return <%= className %>.fromItem(result.Item); 59 | } 60 | -------------------------------------------------------------------------------- /tools/generators/model/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Tree, 3 | formatFiles, 4 | installPackagesTask, 5 | names, 6 | generateFiles, 7 | joinPathFragments, 8 | } from '@nrwl/devkit'; 9 | 10 | interface Schema { 11 | name: string; 12 | project: string; 13 | } 14 | 15 | export default async function (tree: Tree, schema: Schema) { 16 | const serviceRoot = `services/${schema.project}/src`; 17 | 18 | generateFiles(tree, joinPathFragments(__dirname, './files'), serviceRoot, { 19 | ...schema, 20 | tmpl: '', 21 | ...names(schema.name), 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /tools/generators/model/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/schema", 3 | "cli": "nx", 4 | "$id": "model", 5 | "type": "object", 6 | "properties": { 7 | "name": { 8 | "type": "string", 9 | "description": "Model name", 10 | "x-prompt": "What is the name of the model?", 11 | "$default": { 12 | "$source": "argv", 13 | "index": 0 14 | } 15 | }, 16 | "project": { 17 | "type": "string", 18 | "description": "Project name", 19 | "x-prompt": "What is the name of the project?" 20 | } 21 | }, 22 | "required": [ 23 | "name", 24 | "project" 25 | ] 26 | } -------------------------------------------------------------------------------- /tools/generators/service/files/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../.eslintrc.json", 3 | "ignorePatterns": ["!**/*"], 4 | "rules": {} 5 | } 6 | -------------------------------------------------------------------------------- /tools/generators/service/files/README.md: -------------------------------------------------------------------------------- 1 | # <%= name %> 2 | 3 | This stack was generated with [Nx](https://nx.dev). 4 | 5 | ## Running unit tests 6 | 7 | Run `nx test <%= name %>` to execute the unit tests via [Jest](https://jestjs.io). 8 | -------------------------------------------------------------------------------- /tools/generators/service/files/serverless.ts__tmpl__: -------------------------------------------------------------------------------- 1 | import type { Serverless } from 'serverless/aws'; 2 | import { baseServerlessConfig } from '../../serverless.base'; 3 | 4 | const serverlessConfig: Partial = { 5 | ...baseServerlessConfig, 6 | service: '<%= name %>', 7 | custom: { 8 | ...baseServerlessConfig.custom, 9 | 'serverless-offline': { 10 | lambdaPort: 3005, 11 | httpPort: 3006, 12 | }, 13 | }, 14 | functions: {}, 15 | } 16 | 17 | module.exports = serverlessConfig; -------------------------------------------------------------------------------- /tools/generators/service/files/src/__fileName__.types.ts__tmpl__: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngneat/nx-serverless/34ded7c4083c2962bad956cafa0772a59899b21e/tools/generators/service/files/src/__fileName__.types.ts__tmpl__ -------------------------------------------------------------------------------- /tools/generators/service/files/src/__fileName__.utils.ts__tmpl__: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngneat/nx-serverless/34ded7c4083c2962bad956cafa0772a59899b21e/tools/generators/service/files/src/__fileName__.utils.ts__tmpl__ -------------------------------------------------------------------------------- /tools/generators/service/files/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "types": ["node"] 6 | }, 7 | "exclude": ["**/*.spec.ts"], 8 | "include": ["**/*.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /tools/generators/service/files/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.app.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /tools/generators/service/files/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": ["**/*.spec.ts", "**/*.d.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /tools/generators/service/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | formatFiles, 3 | generateFiles, 4 | installPackagesTask, 5 | joinPathFragments, 6 | names, 7 | Tree, 8 | } from '@nrwl/devkit'; 9 | import { Schema } from './schema'; 10 | import { addJest } from './jest-config'; 11 | import { addWorkspaceConfig } from './workspace-config'; 12 | 13 | export default async (host: Tree, schema: Schema) => { 14 | const serviceRoot = `services/${schema.name}`; 15 | 16 | const { fileName } = names(schema.name); 17 | 18 | generateFiles(host, joinPathFragments(__dirname, './files'), serviceRoot, { 19 | ...schema, 20 | tmpl: '', 21 | fileName, 22 | }); 23 | 24 | addWorkspaceConfig(host, schema.name, serviceRoot); 25 | 26 | await addJest(host, schema.name); 27 | 28 | await formatFiles(host); 29 | 30 | return () => { 31 | installPackagesTask(host); 32 | }; 33 | }; 34 | -------------------------------------------------------------------------------- /tools/generators/service/jest-config.ts: -------------------------------------------------------------------------------- 1 | import { Tree } from '@nrwl/devkit'; 2 | import { jestProjectGenerator } from '@nrwl/jest'; 3 | import { JestProjectSchema } from '@nrwl/jest/src/generators/jest-project/schema'; 4 | 5 | export const addJest = async (host: Tree, projectName: string) => { 6 | await jestProjectGenerator(host, { 7 | project: projectName, 8 | setupFile: 'none', 9 | testEnvironment: 'node', 10 | skipSerializers: false, 11 | skipSetupFile: false, 12 | supportTsx: false, 13 | babelJest: false, 14 | skipFormat: true, 15 | }); 16 | }; 17 | -------------------------------------------------------------------------------- /tools/generators/service/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "cli": "nx", 3 | "id": "service", 4 | "type": "object", 5 | "properties": { 6 | "name": { 7 | "type": "string", 8 | "description": "Service name", 9 | "x-prompt": "What is the name of the service?", 10 | "$default": { 11 | "$source": "argv", 12 | "index": 0 13 | } 14 | } 15 | }, 16 | "required": [ 17 | "name" 18 | ] 19 | } -------------------------------------------------------------------------------- /tools/generators/service/schema.ts: -------------------------------------------------------------------------------- 1 | export type Schema = { 2 | readonly name: string; 3 | }; 4 | -------------------------------------------------------------------------------- /tools/generators/service/workspace-config.ts: -------------------------------------------------------------------------------- 1 | import { addProjectConfiguration, Tree } from '@nrwl/devkit'; 2 | 3 | const buildRunCommandConfig = (dir: string, command: string) => ({ 4 | executor: '@nrwl/workspace:run-commands', 5 | options: { 6 | cwd: dir, 7 | color: true, 8 | command: command, 9 | }, 10 | }); 11 | 12 | export const addWorkspaceConfig = ( 13 | host: Tree, 14 | projectName: string, 15 | serviceRoot: string 16 | ) => { 17 | addProjectConfiguration(host, projectName, { 18 | root: serviceRoot, 19 | projectType: 'application', 20 | sourceRoot: `${serviceRoot}/src`, 21 | targets: { 22 | build: { 23 | ...buildRunCommandConfig(serviceRoot, 'sls package'), 24 | }, 25 | serve: { 26 | ...buildRunCommandConfig(serviceRoot, 'sls offline start'), 27 | }, 28 | deploy: { 29 | ...buildRunCommandConfig(serviceRoot, 'sls deploy --verbose'), 30 | dependsOn: [ 31 | { 32 | target: 'deploy', 33 | projects: 'dependencies', 34 | }, 35 | ], 36 | }, 37 | remove: { 38 | ...buildRunCommandConfig(serviceRoot, 'sls remove'), 39 | }, 40 | lint: { 41 | executor: '@nrwl/linter:eslint', 42 | options: { 43 | lintFilePatterns: [`${serviceRoot}/**/*.ts`], 44 | }, 45 | }, 46 | }, 47 | tags: ['service'], 48 | implicitDependencies: ['core'], 49 | }); 50 | }; 51 | -------------------------------------------------------------------------------- /tools/tsconfig.tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "../dist/out-tsc/tools", 5 | "rootDir": ".", 6 | "module": "commonjs", 7 | "target": "ES2020", 8 | "types": ["node"], 9 | "importHelpers": false 10 | }, 11 | "include": ["**/*.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "sourceMap": true, 6 | "declaration": false, 7 | "moduleResolution": "node", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "allowSyntheticDefaultImports": true, 11 | "esModuleInterop": true, 12 | "importHelpers": false, 13 | "target": "ES2020", 14 | "module": "commonjs", 15 | "lib": ["esnext"], 16 | "skipLibCheck": true, 17 | "skipDefaultLibCheck": true, 18 | "baseUrl": ".", 19 | "paths": { 20 | "@app/auth/*": ["libs/auth/src/lib/*"], 21 | "@app/db/*": ["libs/db/src/lib/*"], 22 | "@app/env": ["environments/environment.ts"], 23 | "@app/http/*": ["libs/http/src/lib/*"], 24 | "@app/users/*": ["services/users/src/*"] 25 | } 26 | }, 27 | "exclude": ["node_modules", "tmp"] 28 | } 29 | -------------------------------------------------------------------------------- /workspace.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "projects": { 4 | "auth": "services/auth", 5 | "core": "services/core", 6 | "db": "libs/db", 7 | "http": "libs/http", 8 | "todos": "services/todos", 9 | "users": "services/users" 10 | } 11 | } 12 | --------------------------------------------------------------------------------