├── .editorconfig ├── .env.example ├── .eslintrc.json ├── .github ├── dependabot.yml └── workflows │ ├── cleanup.yml │ └── main.yml ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── docker-compose.yml ├── esbuild-plugins.js ├── jest.config.ts ├── jest.preset.js ├── libs ├── .gitkeep ├── aws │ ├── .babelrc │ ├── .eslintrc.json │ ├── README.md │ ├── console.ts │ ├── jest.config.ts │ ├── project.json │ ├── src │ │ ├── index.ts │ │ ├── lambda │ │ │ ├── client.test.ts │ │ │ ├── client.ts │ │ │ ├── index.ts │ │ │ ├── invoke.test.ts │ │ │ └── invoke.ts │ │ └── sqs │ │ │ ├── client.test.ts │ │ │ ├── client.ts │ │ │ ├── getQueueUrl.test.ts │ │ │ ├── getQueueUrl.ts │ │ │ ├── index.ts │ │ │ ├── sendMessage.test.ts │ │ │ └── sendMessage.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ └── tsconfig.spec.json └── utils │ ├── .babelrc │ ├── .eslintrc.json │ ├── README.md │ ├── console.ts │ ├── jest.config.ts │ ├── project.json │ ├── src │ ├── downcase-keys.test.ts │ ├── downcase-keys.ts │ ├── index.ts │ ├── invert.test.ts │ ├── invert.ts │ ├── is-offline.test.ts │ └── is-offline.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ └── tsconfig.spec.json ├── nx.json ├── package.json ├── serverless.common.ts ├── services ├── background-jobs │ ├── .eslintrc.json │ ├── README.md │ ├── handlers │ │ ├── demo.test.ts │ │ └── demo.ts │ ├── jest.config.ts │ ├── project.json │ ├── serverless.ts │ ├── tsconfig.app.json │ ├── tsconfig.json │ └── tsconfig.spec.json ├── example-service │ ├── .eslintrc.json │ ├── README.md │ ├── handlers │ │ ├── generateDemoJob.test.ts │ │ ├── generateDemoJob.ts │ │ ├── hello.test.ts │ │ └── hello.ts │ ├── jest.config.ts │ ├── project.json │ ├── serverless.ts │ ├── tsconfig.app.json │ ├── tsconfig.json │ └── tsconfig.spec.json └── public-api │ ├── .eslintrc.json │ ├── README.md │ ├── codegen.yml │ ├── jest.config.ts │ ├── project.json │ ├── serverless.ts │ ├── src │ ├── handlers │ │ ├── graphql.test.ts │ │ ├── graphql.ts │ │ ├── healthcheck.test.ts │ │ └── healthcheck.ts │ ├── schema │ │ ├── graphql.schema.json │ │ ├── hello │ │ │ ├── hello.resolvers.ts │ │ │ ├── hello.test.ts │ │ │ ├── hello.typedefs.ts │ │ │ └── index.ts │ │ ├── index.ts │ │ └── schema.graphql │ ├── types │ │ └── graphql.ts │ └── utils │ │ └── apolloTest.ts │ ├── tsconfig.app.json │ ├── tsconfig.json │ └── tsconfig.spec.json ├── tools ├── executors │ └── workspace │ │ ├── executor.json │ │ ├── impl.js │ │ ├── impl.ts │ │ ├── package.json │ │ └── schema.json ├── generators │ ├── lib │ │ ├── files │ │ │ ├── console.ts__tmpl__ │ │ │ └── src │ │ │ │ └── index.ts__tmpl__ │ │ ├── index.ts │ │ └── schema.json │ └── service │ │ ├── files │ │ ├── .eslintrc.json │ │ ├── README.md │ │ ├── handler.test.ts__tmpl__ │ │ ├── handler.ts__tmpl__ │ │ ├── serverless.ts__tmpl__ │ │ ├── tsconfig.app.json │ │ ├── tsconfig.json │ │ └── tsconfig.spec.json │ │ ├── index.ts │ │ ├── jest-config.ts │ │ ├── schema.json │ │ ├── schema.ts │ │ ├── serverless-common.ts │ │ └── workspace-config.ts └── tsconfig.tools.json ├── tsconfig.base.json ├── workspace.json └── yarn.lock /.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 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | REGION=us-east-1 2 | SLS_STAGE=dev 3 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": ["**/*"], 4 | "plugins": ["@nrwl/nx"], 5 | "overrides": [ 6 | { 7 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 8 | "rules": { 9 | "@nrwl/nx/enforce-module-boundaries": [ 10 | "error", 11 | { 12 | "enforceBuildableLibDependency": true, 13 | "allow": [], 14 | "depConstraints": [ 15 | { 16 | "sourceTag": "service", 17 | "onlyDependOnLibsWithTags": ["lib"] 18 | }, 19 | { 20 | "sourceTag": "lib", 21 | "onlyDependOnLibsWithTags": ["lib"] 22 | } 23 | ] 24 | } 25 | ] 26 | } 27 | }, 28 | { 29 | "files": ["*.ts", "*.tsx"], 30 | "extends": ["plugin:@nrwl/nx/typescript"], 31 | "rules": {} 32 | }, 33 | { 34 | "files": ["*.js", "*.jsx"], 35 | "extends": ["plugin:@nrwl/nx/javascript"], 36 | "rules": {} 37 | } 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: '/' 5 | schedule: 6 | interval: 'weekly' 7 | day: 'friday' 8 | time: '10:00' 9 | open-pull-requests-limit: 10 10 | reviewers: 11 | - dustinsgoodman 12 | labels: 13 | - dependency 14 | ignore: 15 | # @nrwl deps should always be updated by running `npx nx migrate @nrwl/workspace` 16 | - dependency-name: '@nrwl/*' 17 | - dependency-name: 'nx' 18 | -------------------------------------------------------------------------------- /.github/workflows/cleanup.yml: -------------------------------------------------------------------------------- 1 | name: Preview Deploy Cleanup 2 | 3 | # When pull requests are closed the associated 4 | # branch will be removed from SLS deploy 5 | on: 6 | pull_request: 7 | types: [closed] 8 | 9 | jobs: 10 | tear-down: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | node-version: [16.x] 15 | steps: 16 | - name: Checkout Repo 17 | uses: actions/checkout@v3 18 | 19 | - name: Configure Node 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: '16.x' 23 | cache: 'yarn' 24 | 25 | - name: Install packages 26 | run: yarn install --frozen-lockfile 27 | 28 | # - name: Create AWS profile 29 | # uses: Fooji/create-aws-profile-action@v1 30 | # with: 31 | # profile: default 32 | # region: us-east-1 33 | # key: ${{ secrets.AWS_ACCESS_KEY }} 34 | # secret: ${{ secrets.AWS_SECRET }} 35 | 36 | # - name: Deploy SLS 37 | # run: yarn remove:all --stage=${GITHUB_HEAD_REF} 38 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI/CD 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | workflow_dispatch: 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | node-version: [16.x] 15 | steps: 16 | - name: Checkout Repo 17 | uses: actions/checkout@v3 18 | 19 | - name: Configure Node 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: '16.x' 23 | cache: 'yarn' 24 | 25 | - name: Install packages 26 | run: yarn install --frozen-lockfile 27 | 28 | - name: Create AWS profile 29 | uses: Fooji/create-aws-profile-action@v1 30 | with: 31 | profile: default 32 | region: us-east-1 33 | key: ${{ secrets.AWS_ACCESS_KEY }} 34 | secret: ${{ secrets.AWS_SECRET }} 35 | 36 | - name: Create .env file for testing 37 | run: cp .env.example .env.test 38 | 39 | - name: Generate service 40 | run: yarn workspace-generator service test-service 41 | 42 | - name: Generate library 43 | run: yarn workspace-generator lib test-lib 44 | 45 | - name: Linting 46 | run: yarn lint:all 47 | 48 | - name: Run tests 49 | run: yarn test:all 50 | 51 | - name: Upload Code Coverage 52 | uses: codecov/codecov-action@v3 53 | with: 54 | files: ./coverage/libs/aws/coverage-final.json,./coverage/libs/utils/coverage-final.json,./coverage/libs/aws/coverage-final.json,./coverage/services/background-jobs/coverage-final.json,./coverage/services/example-service/coverage-final.json,./coverage/services/public-api/coverage-final.json 55 | 56 | - name: Create .env file for deployment 57 | run: | 58 | touch .env 59 | echo IS_OFFLINE${{ secrets.IS_OFFLINE }} >> .env 60 | echo SLS_STAGE=${{ secrets.SLS_STAGE }} >> .env 61 | cat .env 62 | 63 | - name: Run sls package 64 | run: yarn build:all 65 | 66 | # - name: Deploy SLS Preview 67 | # if: github.ref != 'refs/heads/main' 68 | # run: npx nx run deploy:all --stage=${GITHUB_HEAD_REF} 69 | # - name: Deploy SLS Dev 70 | # if: github.ref == 'refs/heads/main' 71 | # run: npx nx run deploy:all --stage=dev 72 | -------------------------------------------------------------------------------- /.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 | .node_repl_history 37 | 38 | # System Files 39 | .DS_Store 40 | Thumbs.db 41 | 42 | # packaging 43 | .serverless 44 | .vscode 45 | .dynamodb 46 | .webpack 47 | .esbuild 48 | 49 | # Secrets 50 | .env 51 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn lint-staged --concurrent 5 5 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | lts/gallium 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Add files here to ignore them from prettier formatting 2 | 3 | /dist 4 | /coverage 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "useTabs": true 4 | } 5 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | education, socio-economic status, nationality, personal appearance, race, 10 | religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting [dustin.s.goodman@gmail.com](mailto:dustin.s.goodman@gmail.com). All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Serverless Micoservices w/ GraphQL Template 2 | 3 | Please [read the Code of Conduct](CODE_OF_CONDUCT.md). By contributing to this repository, you are agreeing to its rules. 4 | 5 | Contents 6 | 7 | - [Submitting a Pull Request (PR)](#submitting-a-pull-request-pr) 8 | - [Code Guidelines](#code-guidelines) 9 | 10 | --- 11 | 12 | ## Submitting a Pull Request (PR) 13 | 14 | Before you submit your Pull Request (PR) consider the following guidelines: 15 | 16 | - Search [the repo pull requests](https://github.com/dustinsgoodman/serverless-microservices-graphql-template/pulls) for an open or closed PR that relates to your submission. This will help reduce redundancies. 17 | 18 | - Make your changes in your forked repository as a new git branch: 19 | 20 | ```shell 21 | git checkout -b my-fix-branch main 22 | ``` 23 | 24 | - Create your change following the [code](#code-guidelines) and/or [content guidelines](#content-guidelines) as appropriate. 25 | 26 | - Commit your changes using a descriptive commit message. This repo does not having any commit linting but attempts to follow [conventional commits guidelines](https://www.conventionalcommits.org/en/v1.0.0/) for consistency. 27 | 28 | - Push your branch to GitHub: 29 | 30 | ```shell 31 | git push origin my-fix-branch 32 | ``` 33 | 34 | - In GitHub, send a pull request to `dustinsgoodman/serverless-microservices-graphql-template:main`. 35 | 36 | - If suggestions or changes are requested, please make the require updates. 37 | 38 | - If your branch has merge conflicts or is out of sync with upstream, please rebase your branch and resolve the conflicts. This will require a force push. 39 | 40 | - When updating your feature branch with the requested changes, please do not overwrite the commit history, but rather contain the changes in new commits. This is for the sake of a clearer and easier review process. 41 | 42 | That's it! Thank you for your contribution! 43 | 44 | ## Code Guidelines 45 | 46 | Consistency is important on a project, where many developers will work on this codebase over long periods of time. Therefore, we expect all developers to adhere to a common set of coding practices and expectations. This section will be updated as the team decides on new standards. 47 | 48 | - **Formatting** – Please follow the conventions as outlined in our prettier configuration and eslint settings. 49 | - **Developer Testing** – Developers are responsible for thoroughly testing their code prior to putting it up for review. It is NOT the responsibility of the code reviewer to execute the code and test it (though the reviewer might run the code to aid their review if they want). The repository's test runner expects 100% coverage so your change should include the relevant tests. 50 | - **Minimal Pull Requests** – Do not commit changes to files where there was not a new feature added or an existing feature altered. Files altered only to remove unused imports or change formatting should not be included in pull requests. Code authors are expected to review the files in each pull request and revert files that were only incidentally changed. Please make sure you also update documentation as features get changed. 51 | - **Code Comments** – We're not following a strict code commenting pattern (like js-doc), but developers are encouraged to use comments liberally where it may aid understandability and readability of the code (especially for new contributors). Comments that merely explain what a line of code does are not necessary. Instead, comments should indicate the intent of the author. It could mention assumptions, constraints, intent, algorithm design, etc. 52 | - **Commit/Pull Request Comments** – Code authors are strongly recommended to communicate the reason for the code changes, the nature of the changes, and the intent of the changes in their git commit messages or PR descriptions. Additionally, while not strictly required, we recommend that code authors make comments in their pull requests where useful to help code reviewers understand the background/intent for some less obvious changes. 53 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Dustin Goodman 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 | # Serverless Micoservices w/ GraphQL Template 2 | 3 | [![serverless](http://public.serverless.com/badges/v3.svg)](http://www.serverless.com) 4 | [![Nx monorepo](https://img.shields.io/badge/monorepo-Nx-blue)](https://nx.dev/) 5 | [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/dustinsgoodman/serverless-microservices-graphql-template/blob/main/LICENSE.md) 6 | [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/dustinsgodman/serverless-template) 7 | [![codecov](https://codecov.io/gh/dustinsgoodman/serverless-microservices-graphql-template/branch/main/graph/badge.svg?token=HZCVZ0DPWD)](https://codecov.io/gh/dustinsgoodman/serverless-microservices-graphql-template) 8 | 9 | A template for a Serverless Framework microservices architecture based on the [nx-serverless-template by sudokar](https://github.com/sudokar/nx-serverless). 10 | 11 | ## Table of contents 12 | 13 | - [Whats Included](#whats-included) 14 | - [Template Layout](#template-layout) 15 | - [Prerequisites](#prerequisites) 16 | - [Usage](#usage) 17 | - [Further help](#further-help) 18 | - [Contribution](#contribution) 19 | - [Support](#support) 20 | - [Maintainer](#maintainer) 21 | - [License](#license) 22 | 23 | ## Whats Included 24 | 25 | - A template project layout using latest version of Nx and Servrless Framework 26 | - An easy to use workspace generator to generate a template/service with Serverless Framework files and related Nx configuration 27 | - Configured with a basic AWS provider that can be easily adopted to any cloud provider 28 | - Serverless Offline microservices architecture support 29 | 30 | ### How does this differ from the original template? 31 | 32 | The original template is phenomenal, but I was aiming for some additional customizations and different core libraries. Specifically: 33 | 34 | - This version uses esbuild instead of webpack 35 | - This version provides some common utility functions for a microservices architecture 36 | - Upstream used Nx Cloud and some other 3rd party tools where this version uses just GitHub Actions 37 | - This version provides opinionated public-api and background-jobs services for consumption. 38 | 39 | ## Template Layout 40 | 41 | ```shell 42 | . 43 | ├── services/ # each serverless configuration/template and its associated files 44 | ├── libs/ # shared libraries 45 | ├── tools/ 46 | ├── README.md 47 | ├── jest.config.ts 48 | ├── jest.preset.ts 49 | ├── nx.json 50 | ├── package.json 51 | ├── serverless.common.yml # shared serverless configuration 52 | ├── tsconfig.base.json 53 | ├── workspace.json 54 | ├── .editorconfig 55 | ├── .eslintrc.json 56 | ├── .gitignore 57 | ├── .husky # git hooks 58 | ├── .nvmrc 59 | ├── .prettierignore 60 | ├── .prettierrc 61 | ├── docker-compose.yml 62 | ``` 63 | 64 | ## Prerequisites 65 | 66 | - [Node.js 16](https://nodejs.org/) - [nvm](https://github.com/nvm-sh/nvm) recommended 67 | - [Yarn](https://yarnpkg.com) 68 | - [Serverless Framework v3](https://serverless.com/) 69 | - 💅 Code format plugins 70 | - [Eslint](https://eslint.org/) 71 | - [Prettier](https://prettier.io/) 72 | - [EditorConfig](https://editorconfig.org/) 73 | - [Jest](https://jestjs.io/) for testing 74 | 75 | ## Usage 76 | 77 | **Install project dependencies** 78 | 79 | ```shell 80 | yarn 81 | ``` 82 | 83 | **Setup and run infrastructure** 84 | 85 | All needed services, such as databases, are managed via Docker. When working in this project, it is 86 | recommended that you start all the infrastructure. When you're not working on the project, you should 87 | stop all the infrastructure. 88 | 89 | ```shell 90 | # Creates the docker container and starts it. 91 | yarn infrastructure:setup 92 | 93 | # Stops the docker container and deletes it. 94 | yarn infrastructure:teardown 95 | 96 | # Starts the docker container. Requires the container to exist. 97 | yarn infrastructure:start 98 | 99 | # Stops the docker container. Requires the container to exist. 100 | yarn infrastructure:stop 101 | ``` 102 | 103 | **Generate a new service** 104 | 105 | ```shell 106 | yarn workspace-generator service 107 | ``` 108 | 109 | **Generate a new library** 110 | 111 | ```shell 112 | yarn workspace-generator lib 113 | ``` 114 | 115 | **Packaging services** 116 | 117 | ```shell 118 | # Package a single service 119 | yarn build 120 | 121 | 122 | # Package all services afffected by a change 123 | yarn affected:build 124 | 125 | 126 | # Package all services 127 | yarn all:build 128 | ``` 129 | 130 | Pass the `--stage=` flag if you're creating a build for a specific environment. 131 | 132 | **Deploying services** 133 | 134 | ```shell 135 | # Deploy a single service to a stage 136 | yarn deploy --stage= 137 | 138 | # Deploy all services to a stage 139 | yarn deploy:all --stage= 140 | ``` 141 | 142 | **Removing deployed service** 143 | 144 | ```shell 145 | # Remove a single service from a stage 146 | yarn remove --stage= 147 | 148 | # Remove all services for a stage 149 | yarn remove:all --stage= 150 | ``` 151 | 152 | **Run tests** 153 | 154 | ```shell 155 | # Run tests for a single service or library 156 | yarn test 157 | 158 | # Run tests for all services or libraries affected by a change 159 | yarn test:affected 160 | 161 | # Run all tests 162 | yarn test:all 163 | ``` 164 | 165 | **Analyze function bundles** 166 | 167 | When building serverless applications, it's important to understand your memory footprint due to Lambda's memory settings as you can experience unexpected errors. As such, the following script can be used to understand the memory footprint of your individual functions: 168 | 169 | ```shell 170 | yarn analyze --function= 171 | ``` 172 | 173 | This will open the results in a new tab in your browser with the results using [esbuild visualizer](https://www.npmjs.com/package/esbuild-visualizer). 174 | 175 | **Run offline / locally** 176 | 177 | - To run a single service 178 | 179 | ```shell 180 | yarn serve 181 | ``` 182 | 183 | **Understand your workspace** 184 | 185 | ``` 186 | yarn dep-graph 187 | ``` 188 | 189 | ## Further help 190 | 191 | - Visit [Serverless Documentation](https://www.serverless.com/framework/docs/) to learn more about Serverless framework 192 | - Visit [Nx Documentation](https://nx.dev) to learn more about Nx dev toolkit 193 | 194 | ## Contribution 195 | 196 | Found an issue? Feel free to raise an issue with information to reproduce. Pull requests are welcome to improve. 197 | 198 | ## Support 199 | 200 | If you like this template, please go support [sudokar](https://github.com/sudokar) as this template would not have been possible without their original work. However, you can always leave this version a star ⭐. 😄 201 | 202 | ## Maintainer 203 | 204 | This version of the template is authored and maintained by [dustinsgoodman](https://github.com/dustinsgoodman) 205 | 206 | ## License 207 | 208 | MIT 209 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | sqs: 4 | image: softwaremill/elasticmq:1.1.1 5 | ports: 6 | - 9324:9324 7 | - 9325:9325 8 | -------------------------------------------------------------------------------- /esbuild-plugins.js: -------------------------------------------------------------------------------- 1 | const { nodeExternalsPlugin } = require('esbuild-node-externals'); 2 | 3 | // default export should be an array of plugins 4 | module.exports = [nodeExternalsPlugin()]; 5 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | projects: [ 3 | '/services/public-api', 4 | '/services/background-jobs', 5 | '/services/example-service', 6 | '/libs/aws', 7 | '/libs/utils', 8 | ], 9 | }; 10 | -------------------------------------------------------------------------------- /jest.preset.js: -------------------------------------------------------------------------------- 1 | const nxPreset = require('@nrwl/jest/preset').default; 2 | 3 | module.exports = { ...nxPreset }; 4 | -------------------------------------------------------------------------------- /libs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dustinsgoodman/serverless-microservices-graphql-template/169943744a8955eb6bd59e000d86573dff96dba2/libs/.gitkeep -------------------------------------------------------------------------------- /libs/aws/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["@nrwl/web/babel", { "useBuiltIns": "usage" }]] 3 | } 4 | -------------------------------------------------------------------------------- /libs/aws/.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/aws/README.md: -------------------------------------------------------------------------------- 1 | # aws 2 | 3 | This library was generated with [Nx](https://nx.dev). 4 | 5 | ## Running unit tests 6 | 7 | Run `nx test aws` to execute the unit tests via [Jest](https://jestjs.io). 8 | -------------------------------------------------------------------------------- /libs/aws/console.ts: -------------------------------------------------------------------------------- 1 | (async () => { 2 | const repl = await import('repl'); 3 | let functionsMap = await import('./src'); 4 | 5 | const replServer = repl.start({ 6 | prompt: 'app > ', 7 | useColors: true, 8 | }); 9 | 10 | replServer.setupHistory('./.node_repl_history', (err) => { 11 | if (err) { 12 | console.error(err); 13 | } 14 | }); 15 | 16 | Object.entries(functionsMap).forEach(([key, value]) => { 17 | replServer.context[key] = value; 18 | }); 19 | 20 | replServer.defineCommand('re', { 21 | help: 'Reload the models without resetting the environment', 22 | async action() { 23 | // bust require cache 24 | Object.keys(require.cache).forEach((key) => { 25 | delete require.cache[key]; 26 | }); 27 | 28 | // fetch map of functions to reload 29 | try { 30 | // import * as functionsMap from './src'; 31 | functionsMap = await import('./src'); 32 | } catch (err) { 33 | console.error(err); 34 | } 35 | Object.entries(functionsMap).forEach(([key, value]) => { 36 | replServer.context[key] = value; 37 | }); 38 | 39 | // inform user that reload is complete 40 | console.log('reloaded!'); 41 | 42 | // reset the prompt 43 | this.displayPrompt(); 44 | }, 45 | }); 46 | })(); 47 | -------------------------------------------------------------------------------- /libs/aws/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'aws', 4 | 5 | globals: { 6 | 'ts-jest': { 7 | tsconfig: '/tsconfig.spec.json', 8 | }, 9 | }, 10 | testEnvironment: 'node', 11 | collectCoverage: true, 12 | coverageDirectory: '../../coverage/libs/aws', 13 | coverageThreshold: { 14 | global: { 15 | branches: 100, 16 | functions: 100, 17 | lines: 100, 18 | statements: 100, 19 | }, 20 | }, 21 | coverageReporters: ['json'], 22 | preset: '../../jest.preset.js', 23 | }; 24 | -------------------------------------------------------------------------------- /libs/aws/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aws", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "libs/aws/src", 5 | "projectType": "library", 6 | "targets": { 7 | "lint": { 8 | "executor": "@nrwl/linter:eslint", 9 | "outputs": ["{options.outputFile}"], 10 | "options": { 11 | "lintFilePatterns": ["libs/aws/**/*.ts"] 12 | } 13 | }, 14 | "test": { 15 | "executor": "@nrwl/jest:jest", 16 | "outputs": ["{workspaceRoot}/coverage/libs/aws"], 17 | "options": { 18 | "jestConfig": "libs/aws/jest.config.ts", 19 | "passWithNoTests": true 20 | } 21 | }, 22 | "console": { 23 | "executor": "./tools/executors/workspace:run-command", 24 | "options": { 25 | "cwd": "libs/aws", 26 | "color": true, 27 | "command": "node --experimental-repl-await -r ts-node/register -r tsconfig-paths/register ./console.ts" 28 | } 29 | } 30 | }, 31 | "tags": ["lib"] 32 | } 33 | -------------------------------------------------------------------------------- /libs/aws/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lambda'; 2 | export * from './sqs'; 3 | -------------------------------------------------------------------------------- /libs/aws/src/lambda/client.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | import type { LambdaClient } from '@aws-sdk/client-lambda'; 3 | 4 | describe('getClient', () => { 5 | const OLD_ENV = process.env; 6 | let subject; 7 | let LambdaClient: LambdaClient; 8 | 9 | describe('when IS_OFFLINE is true', () => { 10 | beforeAll(() => { 11 | jest.resetModules(); 12 | LambdaClient = require('@aws-sdk/client-lambda').LambdaClient; 13 | const { getClient } = require('./client'); 14 | process.env = { 15 | ...OLD_ENV, 16 | IS_OFFLINE: 'true', 17 | }; 18 | subject = getClient('public-api'); 19 | }); 20 | 21 | afterAll(() => { 22 | process.env = OLD_ENV; 23 | }); 24 | 25 | it('returns an LambdaClient', () => { 26 | expect(subject).toEqual(expect.any(LambdaClient)); 27 | }); 28 | 29 | it('sets the endpoint to localhost', async () => { 30 | expect(subject.config.isCustomEndpoint).toBe(true); 31 | const { hostname } = await subject.config.endpoint(); 32 | expect(hostname).toMatch(/localhost/); 33 | }); 34 | }); 35 | 36 | describe('when IS_OFFLINE is false', () => { 37 | beforeAll(() => { 38 | jest.resetModules(); 39 | LambdaClient = require('@aws-sdk/client-lambda').LambdaClient; 40 | const { getClient } = require('./client'); 41 | process.env = { 42 | ...OLD_ENV, 43 | IS_OFFLINE: 'false', 44 | }; 45 | subject = getClient('public-api'); 46 | }); 47 | 48 | afterAll(() => { 49 | process.env = OLD_ENV; 50 | }); 51 | 52 | it('returns an LambdaClient', () => { 53 | expect(subject).toEqual(expect.any(LambdaClient)); 54 | }); 55 | 56 | it('uses the default AWS Lambda endpoint', async () => { 57 | expect(subject.config.isCustomEndpoint).toBe(false); 58 | }); 59 | }); 60 | 61 | describe('when called twice', () => { 62 | let client; 63 | 64 | beforeAll(() => { 65 | jest.resetModules(); 66 | 67 | client = require('@aws-sdk/client-lambda'); 68 | jest.doMock('@aws-sdk/client-lambda', () => ({ 69 | LambdaClient: jest 70 | .fn() 71 | .mockImplementation(() => new client.LambdaClient({})), 72 | })); 73 | LambdaClient = require('@aws-sdk/client-lambda').LambdaClient; 74 | 75 | const { getClient } = require('./client'); 76 | process.env = { 77 | ...OLD_ENV, 78 | IS_OFFLINE: 'true', 79 | }; 80 | getClient('public-api'); 81 | subject = getClient('public-api'); 82 | }); 83 | 84 | afterAll(() => { 85 | process.env = OLD_ENV; 86 | }); 87 | 88 | it('runs the constructor once', () => { 89 | expect(LambdaClient).toHaveBeenCalledTimes(1); 90 | }); 91 | 92 | it('returns a cached client', () => { 93 | expect(subject).toEqual(expect.any(client.LambdaClient)); 94 | }); 95 | }); 96 | }); 97 | -------------------------------------------------------------------------------- /libs/aws/src/lambda/client.ts: -------------------------------------------------------------------------------- 1 | import { LambdaClient, LambdaClientConfig } from '@aws-sdk/client-lambda'; 2 | import type { Port, Service } from '@serverless-template/serverless-common'; 3 | import { PORTS } from '@serverless-template/serverless-common'; 4 | import { isOffline } from '@serverless-template/utils'; 5 | 6 | let cachedClient: LambdaClient | null = null; 7 | 8 | export const getClient = (serviceName: Service) => { 9 | if (cachedClient) { 10 | return cachedClient; 11 | } 12 | 13 | const { REGION } = process.env; 14 | 15 | const config: LambdaClientConfig = { 16 | apiVersion: '2031', 17 | region: REGION, 18 | }; 19 | 20 | if (isOffline()) { 21 | const port = (PORTS[serviceName] as Port).lambdaPort; 22 | config.endpoint = `http://localhost:${port}`; 23 | } 24 | 25 | cachedClient = new LambdaClient(config); 26 | return cachedClient; 27 | }; 28 | -------------------------------------------------------------------------------- /libs/aws/src/lambda/index.ts: -------------------------------------------------------------------------------- 1 | export { invoke } from './invoke'; 2 | -------------------------------------------------------------------------------- /libs/aws/src/lambda/invoke.test.ts: -------------------------------------------------------------------------------- 1 | import { mockClient } from 'aws-sdk-client-mock'; 2 | import { TextEncoder } from 'util'; 3 | import { LambdaClient, InvokeCommand } from '@aws-sdk/client-lambda'; 4 | import { Context, invoke } from './invoke'; 5 | 6 | describe('.invoke', () => { 7 | let subject; 8 | let invokeMock; 9 | let payload; 10 | 11 | beforeAll(() => { 12 | invokeMock = mockClient(LambdaClient); 13 | }); 14 | 15 | afterAll(() => { 16 | invokeMock.restore(); 17 | }); 18 | 19 | describe('when context not provided', () => { 20 | beforeAll(async () => { 21 | payload = new TextEncoder().encode( 22 | JSON.stringify({ 23 | body: JSON.stringify({ a: 1, b: 2 }), 24 | }) 25 | ); 26 | invokeMock 27 | .on(InvokeCommand, { 28 | ClientContext: 'e30=', 29 | FunctionName: 'public-api-dev-testFn', 30 | InvocationType: 'RequestResponse', 31 | Payload: payload, 32 | LogType: 'Tail', 33 | }) 34 | .resolves({ 35 | Payload: new TextEncoder().encode( 36 | JSON.stringify({ 37 | code: 'OK', 38 | message: 'test successful', 39 | statusCode: 200, 40 | }) 41 | ), 42 | }); 43 | 44 | subject = await invoke({ 45 | serviceName: 'public-api', 46 | functionName: 'testFn', 47 | payload: { a: 1, b: 2 }, 48 | }); 49 | }); 50 | 51 | it('returns response from lambda', () => { 52 | expect(subject).toEqual({ 53 | code: 'OK', 54 | message: 'test successful', 55 | statusCode: 200, 56 | }); 57 | }); 58 | }); 59 | 60 | describe('when context provided', () => { 61 | beforeAll(async () => { 62 | payload = new TextEncoder().encode( 63 | JSON.stringify({ 64 | headers: { 65 | authorization: 'Bearer test', 66 | }, 67 | body: JSON.stringify({ a: 1, b: 2 }), 68 | }) 69 | ); 70 | invokeMock 71 | .on(InvokeCommand, { 72 | ClientContext: 'e30=', 73 | FunctionName: 'public-api-dev-testFn', 74 | InvocationType: 'RequestResponse', 75 | Payload: payload, 76 | LogType: 'Tail', 77 | }) 78 | .resolves({ 79 | Payload: new TextEncoder().encode( 80 | JSON.stringify({ 81 | code: 'OK', 82 | message: 'test successful', 83 | statusCode: 200, 84 | }) 85 | ), 86 | }); 87 | 88 | subject = await invoke({ 89 | serviceName: 'public-api', 90 | functionName: 'testFn', 91 | payload: { a: 1, b: 2 }, 92 | context: { 93 | event: { 94 | headers: { 95 | Authorization: 'Bearer test', 96 | }, 97 | }, 98 | } as unknown as Context, 99 | }); 100 | }); 101 | 102 | it('returns response from lambda', () => { 103 | expect(subject).toEqual({ 104 | code: 'OK', 105 | message: 'test successful', 106 | statusCode: 200, 107 | }); 108 | }); 109 | }); 110 | 111 | describe('when FunctionError returned', () => { 112 | beforeAll(async () => { 113 | payload = new TextEncoder().encode( 114 | JSON.stringify({ 115 | headers: { 116 | authorization: 'Bearer test', 117 | }, 118 | body: JSON.stringify({ a: 1, b: 2 }), 119 | }) 120 | ); 121 | invokeMock.resolves({ 122 | Payload: new TextEncoder().encode( 123 | JSON.stringify({ 124 | code: 'OK', 125 | message: 'test successful', 126 | statusCode: 200, 127 | }) 128 | ), 129 | FunctionError: 'misc function error', 130 | }); 131 | 132 | subject = invoke({ 133 | serviceName: 'public-api', 134 | functionName: 'testFn', 135 | payload: { a: 1, b: 2 }, 136 | context: { 137 | event: { 138 | headers: { 139 | Authorization: 'Bearer test', 140 | }, 141 | }, 142 | } as unknown as Context, 143 | }); 144 | }); 145 | 146 | it('returns response from lambda', async () => { 147 | await expect(subject).rejects.toThrow('misc function error'); 148 | }); 149 | }); 150 | 151 | describe('when empty Payload returned', () => { 152 | beforeAll(async () => { 153 | invokeMock.resolves({ 154 | Payload: null, 155 | }); 156 | 157 | subject = await invoke({ 158 | serviceName: 'public-api', 159 | functionName: 'testFn', 160 | payload: { a: 1, b: 2 }, 161 | context: { 162 | event: { 163 | headers: { 164 | Authorization: 'Bearer test', 165 | }, 166 | }, 167 | } as unknown as Context, 168 | }); 169 | }); 170 | 171 | it('returns null', () => { 172 | expect(subject).toBeNull(); 173 | }); 174 | }); 175 | }); 176 | -------------------------------------------------------------------------------- /libs/aws/src/lambda/invoke.ts: -------------------------------------------------------------------------------- 1 | import { 2 | APIGatewayProxyEvent, 3 | Context as LambdaContext, 4 | APIGatewayProxyEventHeaders, 5 | } from 'aws-lambda'; 6 | import { InvokeCommand } from '@aws-sdk/client-lambda'; 7 | import { TextEncoder, TextDecoder } from 'util'; 8 | import type { Service } from '@serverless-template/serverless-common'; 9 | import { downcaseKeys } from '@serverless-template/utils'; 10 | import { getClient } from './client'; 11 | 12 | export type Context = { 13 | event?: APIGatewayProxyEvent; 14 | context?: LambdaContext; 15 | userId?: string | number; 16 | }; 17 | 18 | type InvokeParams = { 19 | serviceName: Service; 20 | functionName: string; 21 | payload?: object; 22 | context?: Context; 23 | invocationType?: 'RequestResponse' | 'Event'; 24 | }; 25 | 26 | /** 27 | * 28 | * @param {Service} serviceName - The name of the Service the function you wish to invoke belongs to, e.g. User-API 29 | * @param {String} functionName - The name of the Lambda function, version or alias - excluding the service name and stage. See: https://docs.aws.amazon.com/lambda/latest/dg/API_Invoke.html#API_Invoke_RequestSyntax 30 | * @param {Object} payload - The data to be provided to the Lambda as an input - will be JSON.stringified before being passed to Lambda 31 | * @param {Base64} context=null - The context object to be passed from Apollo Resolver 32 | * @param {String} [invocationType='RequestResponse'] - The type of invocation to carry out. See: https://docs.aws.amazon.com/lambda/latest/dg/API_Invoke.html#API_Invoke_RequestSyntax 33 | * @returns {Object} payload - The JSON data that is returned from the Lambda invocation 34 | */ 35 | export const invoke = async ({ 36 | serviceName, 37 | functionName, 38 | payload, 39 | context = {}, 40 | invocationType = 'RequestResponse', 41 | }: InvokeParams): Promise => { 42 | const combinedPayload = { 43 | ...context.event, 44 | body: JSON.stringify(payload), 45 | }; 46 | 47 | // forcing all headers for case insensitivity 48 | if (combinedPayload.headers) { 49 | combinedPayload.headers = downcaseKeys( 50 | combinedPayload.headers 51 | ) as APIGatewayProxyEventHeaders; 52 | } 53 | 54 | const combinedContext = { 55 | ...context.context, 56 | // insert other key context to pass along like userId 57 | // userId: context.userId, 58 | }; 59 | 60 | const stringifiedContext = JSON.stringify(combinedContext); 61 | const processedContext = Buffer.from(stringifiedContext).toString('base64'); 62 | const params = { 63 | ClientContext: processedContext, 64 | FunctionName: `${serviceName}-${process.env.SLS_STAGE}-${functionName}`, 65 | InvocationType: invocationType, 66 | Payload: new TextEncoder().encode(JSON.stringify(combinedPayload)), 67 | LogType: 'Tail', 68 | }; 69 | 70 | const command = new InvokeCommand(params); 71 | const { Payload, FunctionError } = await getClient(serviceName).send(command); 72 | 73 | if (FunctionError) { 74 | throw new Error(FunctionError); 75 | } 76 | 77 | if (Payload) { 78 | return JSON.parse(new TextDecoder().decode(Payload)); 79 | } 80 | 81 | return null; 82 | }; 83 | -------------------------------------------------------------------------------- /libs/aws/src/sqs/client.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | import type { SQSClient } from '@aws-sdk/client-sqs'; 3 | 4 | describe('getClient', () => { 5 | const OLD_ENV = process.env; 6 | let subject; 7 | let SQSClient: SQSClient; 8 | 9 | describe('when IS_OFFLINE is true', () => { 10 | beforeAll(() => { 11 | jest.resetModules(); 12 | SQSClient = require('@aws-sdk/client-sqs').SQSClient; 13 | const { getClient } = require('./client'); 14 | process.env = { 15 | ...OLD_ENV, 16 | IS_OFFLINE: 'true', 17 | }; 18 | subject = getClient(); 19 | }); 20 | 21 | afterAll(() => { 22 | process.env = OLD_ENV; 23 | }); 24 | 25 | it('returns an SQSClient', () => { 26 | expect(subject).toEqual(expect.any(SQSClient)); 27 | }); 28 | 29 | it('sets the endpoint to localhost', async () => { 30 | expect(subject.config.isCustomEndpoint).toBe(true); 31 | const { hostname } = await subject.config.endpoint(); 32 | expect(hostname).toMatch(/localhost/); 33 | }); 34 | }); 35 | 36 | describe('when IS_OFFLINE is false', () => { 37 | beforeAll(() => { 38 | jest.resetModules(); 39 | SQSClient = require('@aws-sdk/client-sqs').SQSClient; 40 | const { getClient } = require('./client'); 41 | process.env = { 42 | ...OLD_ENV, 43 | IS_OFFLINE: 'false', 44 | }; 45 | subject = getClient(); 46 | }); 47 | 48 | afterAll(() => { 49 | process.env = OLD_ENV; 50 | }); 51 | 52 | it('returns an SQSClient', () => { 53 | expect(subject).toEqual(expect.any(SQSClient)); 54 | }); 55 | 56 | it('uses the default AWS SQS endpoint', async () => { 57 | expect(subject.config.isCustomEndpoint).toBe(false); 58 | }); 59 | }); 60 | 61 | describe('when called twice', () => { 62 | let client; 63 | 64 | beforeAll(() => { 65 | jest.resetModules(); 66 | 67 | client = require('@aws-sdk/client-sqs'); 68 | jest.doMock('@aws-sdk/client-sqs', () => ({ 69 | SQSClient: jest.fn().mockImplementation(() => new client.SQSClient({})), 70 | })); 71 | SQSClient = require('@aws-sdk/client-sqs').SQSClient; 72 | 73 | const { getClient } = require('./client'); 74 | process.env = { 75 | ...OLD_ENV, 76 | IS_OFFLINE: 'true', 77 | }; 78 | getClient(); 79 | subject = getClient(); 80 | }); 81 | 82 | afterAll(() => { 83 | process.env = OLD_ENV; 84 | }); 85 | 86 | it('runs the constructor once', () => { 87 | expect(SQSClient).toHaveBeenCalledTimes(1); 88 | }); 89 | 90 | it('returns a cached client', () => { 91 | expect(subject).toEqual(expect.any(client.SQSClient)); 92 | }); 93 | }); 94 | }); 95 | -------------------------------------------------------------------------------- /libs/aws/src/sqs/client.ts: -------------------------------------------------------------------------------- 1 | import { SQSClient, SQSClientConfig } from '@aws-sdk/client-sqs'; 2 | import { isOffline } from '@serverless-template/utils'; 3 | 4 | export type QueueName = 'DemoQueue'; 5 | 6 | let cachedClient: SQSClient | null = null; 7 | 8 | export const getClient = (): SQSClient => { 9 | if (cachedClient) { 10 | return cachedClient; 11 | } 12 | 13 | const config: SQSClientConfig = {}; 14 | if (isOffline()) { 15 | config.endpoint = 'http://localhost:9324'; 16 | } 17 | 18 | cachedClient = new SQSClient(config); 19 | return cachedClient; 20 | }; 21 | -------------------------------------------------------------------------------- /libs/aws/src/sqs/getQueueUrl.test.ts: -------------------------------------------------------------------------------- 1 | import { mockClient } from 'aws-sdk-client-mock'; 2 | import { SQSClient, GetQueueUrlCommand } from '@aws-sdk/client-sqs'; 3 | import { getQueueUrl } from './getQueueUrl'; 4 | 5 | describe('getQueueUrl', () => { 6 | let subject; 7 | let sqsMock; 8 | 9 | beforeAll(() => { 10 | sqsMock = mockClient(SQSClient); 11 | }); 12 | 13 | afterAll(() => { 14 | sqsMock.restore(); 15 | }); 16 | 17 | describe('when valid params are provided', () => { 18 | beforeAll(async () => { 19 | sqsMock.on(GetQueueUrlCommand).resolves({ 20 | QueueUrl: 'https://sqs.us-east-1.amazonaws.com/123456789012/DemoQueue', 21 | }); 22 | subject = await getQueueUrl('DemoQueue'); 23 | }); 24 | 25 | it('returns QueueUrl', () => { 26 | expect(subject).toEqual( 27 | 'https://sqs.us-east-1.amazonaws.com/123456789012/DemoQueue' 28 | ); 29 | }); 30 | }); 31 | 32 | describe('when invalid params are provided', () => { 33 | beforeAll(() => { 34 | sqsMock.on(GetQueueUrlCommand).rejects('mocked rejection'); 35 | subject = getQueueUrl('DemoQueue'); 36 | }); 37 | 38 | it('throws an exception with the error message', async () => { 39 | await expect(subject).rejects.toThrow('mocked rejection'); 40 | }); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /libs/aws/src/sqs/getQueueUrl.ts: -------------------------------------------------------------------------------- 1 | import { GetQueueUrlCommand } from '@aws-sdk/client-sqs'; 2 | import { getClient } from './client'; 3 | import type { QueueName } from './client'; 4 | 5 | export const getQueueUrl = async (queue: QueueName): Promise => { 6 | const command = new GetQueueUrlCommand({ 7 | QueueName: queue, 8 | }); 9 | 10 | try { 11 | const { QueueUrl } = await getClient().send(command); 12 | return QueueUrl; 13 | } catch (error) { 14 | throw new Error(error.message); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /libs/aws/src/sqs/index.ts: -------------------------------------------------------------------------------- 1 | export type { QueueName } from './client'; 2 | export { sendMessage } from './sendMessage'; 3 | -------------------------------------------------------------------------------- /libs/aws/src/sqs/sendMessage.test.ts: -------------------------------------------------------------------------------- 1 | import { mockClient } from 'aws-sdk-client-mock'; 2 | import { 3 | SQSClient, 4 | SendMessageCommand, 5 | GetQueueUrlCommand, 6 | } from '@aws-sdk/client-sqs'; 7 | import { sendMessage } from './sendMessage'; 8 | 9 | describe('sendMessage', () => { 10 | let subject; 11 | let sqsMock; 12 | 13 | beforeAll(() => { 14 | sqsMock = mockClient(SQSClient); 15 | sqsMock.on(GetQueueUrlCommand).resolves({ 16 | QueueUrl: 'https://sqs.us-east-1.amazonaws.com/123456789012/test-queue', 17 | }); 18 | }); 19 | 20 | afterAll(() => { 21 | sqsMock.restore(); 22 | }); 23 | 24 | describe('when valid params are provided', () => { 25 | let messageId; 26 | 27 | beforeAll(async () => { 28 | messageId = '12345678-1111-2222-3333-111122223333'; 29 | sqsMock.on(SendMessageCommand).resolves({ 30 | MessageId: messageId, 31 | }); 32 | subject = await sendMessage('DemoQueue', { 33 | message: 'Hello World', 34 | }); 35 | }); 36 | 37 | it('sends message to queue', () => { 38 | expect(subject).toEqual({ 39 | success: true, 40 | data: { 41 | MessageId: messageId, 42 | }, 43 | }); 44 | }); 45 | }); 46 | 47 | describe('when invalid params are provided', () => { 48 | beforeAll(async () => { 49 | sqsMock.on(SendMessageCommand).rejects('mocked rejection'); 50 | subject = await sendMessage('DemoQueue', { 51 | message: 'Hello World', 52 | }); 53 | }); 54 | 55 | it('does not send the message to queue', () => { 56 | expect(subject).toEqual({ 57 | success: false, 58 | data: 'mocked rejection', 59 | }); 60 | }); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /libs/aws/src/sqs/sendMessage.ts: -------------------------------------------------------------------------------- 1 | import { SendMessageCommand } from '@aws-sdk/client-sqs'; 2 | import { getClient } from './client'; 3 | import type { QueueName } from './client'; 4 | import { getQueueUrl } from './getQueueUrl'; 5 | 6 | type Message = { 7 | [key: string]: string | number | boolean | null; 8 | }; 9 | 10 | export const sendMessage = async (queue: QueueName, message: Message) => { 11 | const client = getClient(); 12 | const queueUrl = await getQueueUrl(queue); 13 | const command = new SendMessageCommand({ 14 | QueueUrl: queueUrl, 15 | MessageBody: JSON.stringify(message), 16 | }); 17 | 18 | try { 19 | const data = await client.send(command); 20 | return { 21 | success: true, 22 | data, 23 | }; 24 | } catch (error) { 25 | return { 26 | success: false, 27 | data: error.message, 28 | }; 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /libs/aws/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.lib.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /libs/aws/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", "jest.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /libs/aws/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": [ 9 | "**/*.test.ts", 10 | "**/*.spec.ts", 11 | "**/*.test.tsx", 12 | "**/*.spec.tsx", 13 | "**/*.test.js", 14 | "**/*.spec.js", 15 | "**/*.test.jsx", 16 | "**/*.spec.jsx", 17 | "**/*.d.ts", 18 | "jest.config.ts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /libs/utils/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["@nrwl/web/babel", { "useBuiltIns": "usage" }]] 3 | } 4 | -------------------------------------------------------------------------------- /libs/utils/.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/utils/README.md: -------------------------------------------------------------------------------- 1 | # utils 2 | 3 | This library was generated with [Nx](https://nx.dev). 4 | 5 | ## Running unit tests 6 | 7 | Run `nx test utils` to execute the unit tests via [Jest](https://jestjs.io). 8 | -------------------------------------------------------------------------------- /libs/utils/console.ts: -------------------------------------------------------------------------------- 1 | (async () => { 2 | const repl = await import('repl'); 3 | let functionsMap = await import('./src'); 4 | 5 | const replServer = repl.start({ 6 | prompt: 'app > ', 7 | useColors: true, 8 | }); 9 | 10 | replServer.setupHistory('./.node_repl_history', (err) => { 11 | if (err) { 12 | console.error(err); 13 | } 14 | }); 15 | 16 | Object.entries(functionsMap).forEach(([key, value]) => { 17 | replServer.context[key] = value; 18 | }); 19 | 20 | replServer.defineCommand('re', { 21 | help: 'Reload the models without resetting the environment', 22 | async action() { 23 | // bust require cache 24 | Object.keys(require.cache).forEach((key) => { 25 | delete require.cache[key]; 26 | }); 27 | 28 | // fetch map of functions to reload 29 | try { 30 | functionsMap = await import('./src'); 31 | } catch (err) { 32 | console.error(err); 33 | } 34 | Object.entries(functionsMap).forEach(([key, value]) => { 35 | replServer.context[key] = value; 36 | }); 37 | 38 | // inform user that reload is complete 39 | console.log('reloaded!'); 40 | 41 | // reset the prompt 42 | this.displayPrompt(); 43 | }, 44 | }); 45 | })(); 46 | -------------------------------------------------------------------------------- /libs/utils/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'utils', 4 | 5 | globals: { 6 | 'ts-jest': { 7 | tsconfig: '/tsconfig.spec.json', 8 | }, 9 | }, 10 | testEnvironment: 'node', 11 | collectCoverage: true, 12 | coverageDirectory: '../../coverage/libs/utils', 13 | coverageThreshold: { 14 | global: { 15 | branches: 100, 16 | functions: 100, 17 | lines: 100, 18 | statements: 100, 19 | }, 20 | }, 21 | coverageReporters: ['json'], 22 | preset: '../../jest.preset.js', 23 | }; 24 | -------------------------------------------------------------------------------- /libs/utils/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "utils", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "libs/utils/src", 5 | "projectType": "library", 6 | "targets": { 7 | "lint": { 8 | "executor": "@nrwl/linter:eslint", 9 | "outputs": ["{options.outputFile}"], 10 | "options": { 11 | "lintFilePatterns": ["libs/utils/**/*.ts"] 12 | } 13 | }, 14 | "test": { 15 | "executor": "@nrwl/jest:jest", 16 | "outputs": ["{workspaceRoot}/coverage/libs/utils"], 17 | "options": { 18 | "jestConfig": "libs/utils/jest.config.ts", 19 | "passWithNoTests": true 20 | } 21 | }, 22 | "console": { 23 | "executor": "./tools/executors/workspace:run-command", 24 | "options": { 25 | "cwd": "libs/utils", 26 | "color": true, 27 | "command": "node --experimental-repl-await -r ts-node/register -r tsconfig-paths/register ./console.ts" 28 | } 29 | } 30 | }, 31 | "tags": ["lib"] 32 | } 33 | -------------------------------------------------------------------------------- /libs/utils/src/downcase-keys.test.ts: -------------------------------------------------------------------------------- 1 | import { downcaseKeys } from './downcase-keys'; 2 | 3 | describe('.downcaseKeys', () => { 4 | let subject; 5 | 6 | describe('when object has mixed case keys', () => { 7 | beforeAll(() => { 8 | subject = downcaseKeys({ 9 | bar: 'bar', 10 | bAz: 'baz', 11 | FOO: 'foo', 12 | fooBar: 'foobar', 13 | foo_bar: 'foobar', 14 | FooBar: 'foobar', 15 | _foo_BAR_: 'foobar', 16 | }); 17 | }); 18 | 19 | test('returns all keys downcased', () => { 20 | expect(subject).toEqual({ 21 | bar: 'bar', 22 | baz: 'baz', 23 | foo: 'foo', 24 | foobar: 'foobar', 25 | foo_bar: 'foobar', 26 | _foo_bar_: 'foobar', 27 | }); 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /libs/utils/src/downcase-keys.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Converts all object keys to lower case versions for case insensitive 3 | * operations like header checking 4 | */ 5 | export const downcaseKeys = (obj: object): object => 6 | Object.entries(obj).reduce((acc, [key, value]) => { 7 | acc[key.toLowerCase()] = value; 8 | return acc; 9 | }, {}); 10 | -------------------------------------------------------------------------------- /libs/utils/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './downcase-keys'; 2 | export * from './invert'; 3 | export * from './is-offline'; 4 | -------------------------------------------------------------------------------- /libs/utils/src/invert.test.ts: -------------------------------------------------------------------------------- 1 | import { invert } from './invert'; 2 | 3 | describe('.invert', () => { 4 | let subject; 5 | 6 | describe('when object is simple', () => { 7 | beforeAll(() => { 8 | subject = invert({ a: 1, b: 2 }); 9 | }); 10 | 11 | test('returns inverted object', async () => { 12 | expect(subject).toEqual({ 1: 'a', 2: 'b' }); 13 | }); 14 | }); 15 | 16 | describe('when object has duplicating values', () => { 17 | beforeAll(() => { 18 | subject = invert({ a: 1, b: 2, c: 2 }); 19 | }); 20 | 21 | test('returns inverted object keeping last match', async () => { 22 | expect(subject).toEqual({ 1: 'a', 2: 'c' }); 23 | }); 24 | }); 25 | 26 | describe('when object has complex values', () => { 27 | beforeAll(() => { 28 | subject = invert({ a: 1, b: [2, 3], c: { d: 4, e: 5 } }); 29 | }); 30 | 31 | test('returns the values as toStringed keys', async () => { 32 | expect(subject).toEqual({ '1': 'a', '2,3': 'b', '[object Object]': 'c' }); 33 | }); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /libs/utils/src/invert.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Takes an object and inverts the keys and values, e.g. { a: 1, b: 2 } 3 | * would become { 1: a, 2: b } 4 | */ 5 | export const invert = (obj: object): object => { 6 | return Object.entries(obj).reduce((acc, [key, value]) => { 7 | acc[value] = key; 8 | return acc; 9 | }, {}); 10 | }; 11 | -------------------------------------------------------------------------------- /libs/utils/src/is-offline.test.ts: -------------------------------------------------------------------------------- 1 | import { isOffline } from './is-offline'; 2 | 3 | describe('isOffline ', () => { 4 | const env = process.env; 5 | 6 | beforeEach(() => { 7 | jest.resetModules(); 8 | process.env = { ...env }; 9 | }); 10 | 11 | afterEach(() => { 12 | process.env = env; 13 | }); 14 | 15 | // run each of the 3 possible scenarios 16 | const cases = [ 17 | ['true', true], 18 | ['false', false], 19 | ['undefined', false], 20 | ]; 21 | test.each(cases)( 22 | `when process.env.IS_OFFLINE set to %p, returns %p`, 23 | (a, b) => { 24 | // set env to test case 25 | process.env.IS_OFFLINE = String(a); 26 | expect(isOffline()).toEqual(b); 27 | } 28 | ); 29 | }); 30 | -------------------------------------------------------------------------------- /libs/utils/src/is-offline.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Utility function for checking where functions are being run locally via serverless offline 3 | * or if they're running on infrastructure. Helpful for detecting which connection string to use. 4 | * 5 | * @returns boolean are we running locally or on infra? 6 | */ 7 | export const isOffline = (): boolean => process.env.IS_OFFLINE === 'true'; 8 | -------------------------------------------------------------------------------- /libs/utils/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.lib.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /libs/utils/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", "jest.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /libs/utils/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": [ 9 | "**/*.test.ts", 10 | "**/*.spec.ts", 11 | "**/*.test.tsx", 12 | "**/*.spec.tsx", 13 | "**/*.test.js", 14 | "**/*.spec.js", 15 | "**/*.test.jsx", 16 | "**/*.spec.jsx", 17 | "**/*.d.ts", 18 | "jest.config.ts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- 1 | { 2 | "npmScope": "serverless-template", 3 | "affected": { 4 | "defaultBase": "main" 5 | }, 6 | "tasksRunnerOptions": { 7 | "default": { 8 | "runner": "@nrwl/workspace/tasks-runners/default", 9 | "options": { 10 | "cacheableOperations": ["build", "lint", "test", "e2e"] 11 | } 12 | } 13 | }, 14 | "workspaceLayout": { 15 | "appsDir": "services" 16 | }, 17 | "$schema": "./node_modules/nx/schemas/nx-schema.json", 18 | "namedInputs": { 19 | "default": ["{projectRoot}/**/*", "sharedGlobals"], 20 | "sharedGlobals": [ 21 | "{workspaceRoot}/workspace.json", 22 | "{workspaceRoot}/tsconfig.base.json", 23 | "{workspaceRoot}/tslint.json", 24 | "{workspaceRoot}/nx.json" 25 | ], 26 | "production": [ 27 | "default", 28 | "!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)", 29 | "!{projectRoot}/tsconfig.spec.json", 30 | "!{projectRoot}/jest.config.[jt]s", 31 | "!{projectRoot}/.eslintrc.json" 32 | ] 33 | }, 34 | "targetDefaults": { 35 | "build": { 36 | "inputs": ["production", "^production"] 37 | }, 38 | "test": { 39 | "inputs": ["default", "^production", "{workspaceRoot}/jest.preset.js"] 40 | }, 41 | "lint": { 42 | "inputs": ["default", "{workspaceRoot}/.eslintrc.json"] 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-template", 3 | "version": "1.0.0", 4 | "license": "MIT", 5 | "private": true, 6 | "sideEffects": [ 7 | "libs/aws/**/client.ts" 8 | ], 9 | "scripts": { 10 | "nx": "nx", 11 | "build": "nx build", 12 | "deploy": "nx deploy", 13 | "remove": "nx remove", 14 | "analyze": "nx analyze", 15 | "serve": "nx serve", 16 | "test": "nx test", 17 | "lint": "nx workspace-lint && nx lint", 18 | "e2e": "nx e2e", 19 | "dep-graph": "nx dep-graph", 20 | "infrastructure:build": "docker compose up -d", 21 | "infrastructure:remove": "docker compose down", 22 | "infrastructure:start": "docker compose start", 23 | "infrastructure:stop": "docker compose stop", 24 | "services:affected": "nx affected:apps", 25 | "libs:affected": "nx affected:libs", 26 | "build:affected": "nx affected:build", 27 | "e2e:affected": "nx affected:e2e", 28 | "test:affected": "nx affected:test", 29 | "lint:affected": "nx affected:lint", 30 | "dep-graph:affected": "nx affected:dep-graph", 31 | "affected": "nx affected", 32 | "build:all": "nx run-many --target=build --all", 33 | "deploy:all": "nx run-many --target=deploy --all", 34 | "remove:all": "nx run-many --target=remove-dev --all", 35 | "test:all": "nx run-many --target=test --all", 36 | "lint:all": "nx run-many --target=lint --all", 37 | "help": "nx help", 38 | "format": "nx format:write", 39 | "format:write": "nx format:write", 40 | "format:check": "nx format:check", 41 | "prepare": "husky install", 42 | "codegen": "nx codegen", 43 | "console": "nx console", 44 | "update": "nx migrate latest", 45 | "workspace-generator": "nx workspace-generator" 46 | }, 47 | "dependencies": { 48 | "@aws-sdk/client-lambda": "^3.245.0", 49 | "@aws-sdk/client-sqs": "^3.254.0", 50 | "@graphql-tools/merge": "^8.3.7", 51 | "@graphql-tools/schema": "^9.0.13", 52 | "apollo-server-lambda": "^3.11.1", 53 | "graphql": "^16.6.0", 54 | "tslib": "^2.4.1" 55 | }, 56 | "devDependencies": { 57 | "@aws-sdk/types": "^3.198.0", 58 | "@babel/generator": "^7.20.7", 59 | "@babel/parser": "^7.20.7", 60 | "@babel/types": "^7.19.0", 61 | "@graphql-codegen/cli": "^2.16.4", 62 | "@graphql-codegen/introspection": "^2.2.3", 63 | "@graphql-codegen/schema-ast": "^2.5.1", 64 | "@graphql-codegen/typescript": "^2.8.7", 65 | "@graphql-codegen/typescript-operations": "^2.5.12", 66 | "@graphql-codegen/typescript-resolvers": "^2.7.12", 67 | "@jest/transform": "^29.3.1", 68 | "@nrwl/cli": "15.5.3", 69 | "@nrwl/devkit": "15.5.3", 70 | "@nrwl/eslint-plugin-nx": "15.5.3", 71 | "@nrwl/jest": "15.5.3", 72 | "@nrwl/linter": "15.5.3", 73 | "@nrwl/node": "15.5.3", 74 | "@nrwl/workspace": "15.5.3", 75 | "@types/aws-lambda": "^8.10.109", 76 | "@types/jest": "28.1.8", 77 | "@types/node": "18.11.18", 78 | "@types/serverless": "^3.12.9", 79 | "@typescript-eslint/eslint-plugin": "5.48.2", 80 | "@typescript-eslint/parser": "5.48.2", 81 | "aws-sdk-client-mock": "^2.0.1", 82 | "aws-sdk-client-mock-jest": "^2.0.1", 83 | "esbuild": "^0.15.18", 84 | "esbuild-node-externals": "^1.6.0", 85 | "esbuild-visualizer": "^0.3.1", 86 | "eslint": "8.32.0", 87 | "eslint-config-prettier": "^8.6.0", 88 | "graphql-schema-linter": "^3.0.1", 89 | "husky": "^8.0.3", 90 | "jest": "28.1.3", 91 | "lint-staged": "^13.0.3", 92 | "nx": "15.5.3", 93 | "prettier": "2.8.3", 94 | "serverless": "^3.26.0", 95 | "serverless-analyze-bundle-plugin": "^1.2.1", 96 | "serverless-esbuild": "^1.37.2", 97 | "serverless-offline": "^11.6.0", 98 | "serverless-offline-sqs": "^7.3.2", 99 | "ts-jest": "28.0.8", 100 | "ts-loader": "^9.4.2", 101 | "ts-node": "10.9.1", 102 | "tsconfig-paths": "^4.1.2", 103 | "typescript": "4.9.4" 104 | }, 105 | "engines": { 106 | "node": ">=16 <17" 107 | }, 108 | "lint-staged": { 109 | "*.{js,jsx,ts,tsx}": [ 110 | "eslint --fix", 111 | "prettier --write" 112 | ], 113 | "*.{md,json,yml,yaml,html}": [ 114 | "prettier --write" 115 | ], 116 | "*.graphql": [ 117 | "graphql-schema-linter src/**/*.graphql" 118 | ] 119 | }, 120 | "graphql-schema-linter": { 121 | "rules": [ 122 | "deprecations-have-a-reason", 123 | "descriptions-are-capitalized", 124 | "enum-values-all-caps", 125 | "enum-values-have-descriptions", 126 | "enum-values-sorted-alphabetically", 127 | "fields-are-camel-cased", 128 | "fields-have-descriptions", 129 | "input-object-fields-sorted-alphabetically", 130 | "input-object-values-are-camel-cased", 131 | "input-object-values-have-descriptions", 132 | "type-fields-sorted-alphabetically", 133 | "types-are-capitalized", 134 | "types-have-descriptions" 135 | ], 136 | "ignore": { 137 | "types-have-descriptions": [ 138 | "Query", 139 | "Mutation", 140 | "Subscription" 141 | ] 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /serverless.common.ts: -------------------------------------------------------------------------------- 1 | import type { Serverless } from 'serverless/aws'; 2 | 3 | export type Service = 'public-api' | 'background-jobs' | 'example-service'; 4 | export type Port = { 5 | httpPort: number; 6 | lambdaPort: number; 7 | }; 8 | type PortConfig = { 9 | [k in Service]: Port; 10 | }; 11 | 12 | export const PORTS: PortConfig = { 13 | 'public-api': { 14 | httpPort: 3000, 15 | lambdaPort: 3002, 16 | }, 17 | 'background-jobs': { 18 | httpPort: 3004, 19 | lambdaPort: 3006, 20 | }, 21 | 'example-service': { 22 | httpPort: 3008, 23 | lambdaPort: 3010, 24 | }, 25 | }; 26 | 27 | export const getCustomConfig = ( 28 | serviceName: Service 29 | ): Serverless['custom'] => ({ 30 | 'serverless-offline': { 31 | httpPort: PORTS[serviceName].httpPort, 32 | lambdaPort: PORTS[serviceName].lambdaPort, 33 | }, 34 | esbuild: { 35 | packager: 'yarn', 36 | plugins: '../../esbuild-plugins.js', 37 | bundle: true, 38 | minify: true, 39 | sourcemap: true, 40 | }, 41 | 'serverless-offline-sqs': { 42 | autoCreate: true, 43 | apiVersion: '2012-11-05', 44 | endpoint: 'http://0.0.0.0:9324', 45 | region: 'us-east-1', 46 | accessKeyId: 'root', 47 | secretAccessKey: 'root', 48 | skipCacheInvalidation: false, 49 | }, 50 | }); 51 | -------------------------------------------------------------------------------- /services/background-jobs/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../.eslintrc.json", 3 | "ignorePatterns": ["!**/*"], 4 | "rules": {} 5 | } 6 | -------------------------------------------------------------------------------- /services/background-jobs/README.md: -------------------------------------------------------------------------------- 1 | # background-jobs Service 2 | 3 | This service will act as the background job runner for the application. 4 | 5 | ## Architecture 6 | 7 | Background jobs will be run using AWS SQS to queue up jobs and execute them via Lambda worker processes. 8 | 9 | Because each function is packaged individually and due to the local testing of this feature relying on ElasticMQ, it is best to isolate all SQS related runtime to this service. We follow [Serverless Framework's recommendations for SQS setup](https://www.serverless.com/framework/docs/providers/aws/events/sqs) and the [serverless-offline-sqs plugin](https://www.npmjs.com/package/serverless-offline-sqs). 10 | 11 | You can view local queue stats at [http://localhost:9325/](http://localhost:9325/). 12 | 13 | When adding new queues, you must register the queue resource to a Lambda event for serverless-offline-sqs to generate the queue. Hence, you have to start this service to initialize new queues before pushing messages onto them via other queues. To add a new queue, you must do the following: 14 | 15 | 1. Register the new queue to the [Queues file](../../libs/aws/src/sqs/queues.ts) 16 | 2. Setup a new environment variable to reference the local queue and configure it in the serverless.yml 17 | -------------------------------------------------------------------------------- /services/background-jobs/handlers/demo.test.ts: -------------------------------------------------------------------------------- 1 | import { Context, Callback, SQSEvent } from 'aws-lambda'; 2 | import { handler } from './demo'; 3 | 4 | describe('demo', () => { 5 | let subject; 6 | let logMock; 7 | 8 | beforeAll(async () => { 9 | logMock = jest.spyOn(console, 'log').mockImplementation(() => ({})); 10 | subject = await handler( 11 | { 12 | Records: [ 13 | { 14 | body: 'Hello', 15 | }, 16 | { 17 | body: 'World', 18 | }, 19 | ], 20 | } as SQSEvent, 21 | {} as Context, 22 | {} as Callback 23 | ); 24 | }); 25 | 26 | afterAll(() => { 27 | logMock.mockReset(); 28 | }); 29 | 30 | it('processes both messages', () => { 31 | expect(console.log).toHaveBeenCalledWith('"Hello"'); 32 | expect(console.log).toHaveBeenCalledWith('"World"'); 33 | }); 34 | 35 | it('returns nothing', () => { 36 | expect(subject).toBeUndefined(); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /services/background-jobs/handlers/demo.ts: -------------------------------------------------------------------------------- 1 | import { SQSHandler } from 'aws-lambda'; 2 | 3 | export const handler: SQSHandler = async (event) => { 4 | const recordHandler = async (record) => { 5 | console.log(JSON.stringify(record.body)); 6 | }; 7 | 8 | // Ensuring we await on all the promises is super important to avoid 9 | // accidentally killing the lambda prior to processing being completed. 10 | await Promise.all(event.Records.map(recordHandler)); 11 | }; 12 | -------------------------------------------------------------------------------- /services/background-jobs/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'background-jobs', 4 | 5 | globals: { 6 | 'ts-jest': { 7 | tsconfig: '/tsconfig.spec.json', 8 | }, 9 | }, 10 | testEnvironment: 'node', 11 | collectCoverage: true, 12 | coverageDirectory: '../../coverage/services/background-jobs', 13 | coverageThreshold: { 14 | global: { 15 | branches: 100, 16 | functions: 100, 17 | lines: 100, 18 | statements: 100, 19 | }, 20 | }, 21 | coverageReporters: ['json'], 22 | preset: '../../jest.preset.js', 23 | }; 24 | -------------------------------------------------------------------------------- /services/background-jobs/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "background-jobs", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "projectType": "application", 5 | "sourceRoot": "services/background-jobs/src", 6 | "targets": { 7 | "build": { 8 | "executor": "./tools/executors/workspace:run-command", 9 | "options": { 10 | "cwd": "services/background-jobs", 11 | "color": true, 12 | "command": "sls package" 13 | } 14 | }, 15 | "serve": { 16 | "executor": "./tools/executors/workspace:run-command", 17 | "options": { 18 | "cwd": "services/background-jobs", 19 | "color": true, 20 | "command": "sls offline start" 21 | } 22 | }, 23 | "deploy": { 24 | "executor": "./tools/executors/workspace:run-command", 25 | "options": { 26 | "cwd": "services/background-jobs", 27 | "color": true, 28 | "command": "sls deploy --stage {args.stage}" 29 | } 30 | }, 31 | "remove": { 32 | "executor": "./tools/executors/workspace:run-command", 33 | "options": { 34 | "cwd": "services/background-jobs", 35 | "color": true, 36 | "command": "sls remove --stage {args.stage}" 37 | } 38 | }, 39 | "analyze": { 40 | "executor": "./tools/executors/workspace:run-command", 41 | "options": { 42 | "cwd": "services/background-jobs", 43 | "color": true, 44 | "command": "sls package --analyze {args.function}" 45 | } 46 | }, 47 | "lint": { 48 | "executor": "@nrwl/linter:eslint", 49 | "options": { 50 | "lintFilePatterns": ["services/background-jobs/**/*.ts"] 51 | } 52 | }, 53 | "test": { 54 | "executor": "@nrwl/jest:jest", 55 | "outputs": ["{workspaceRoot}/coverage/services/background-jobs"], 56 | "options": { 57 | "jestConfig": "services/background-jobs/jest.config.ts", 58 | "passWithNoTests": true 59 | } 60 | } 61 | }, 62 | "tags": ["service"] 63 | } 64 | -------------------------------------------------------------------------------- /services/background-jobs/serverless.ts: -------------------------------------------------------------------------------- 1 | import type { Serverless } from 'serverless/aws'; 2 | import { getCustomConfig } from '../../serverless.common'; 3 | 4 | const serviceName = 'background-jobs'; 5 | 6 | const serverlessConfiguration: Serverless = { 7 | service: serviceName, 8 | frameworkVersion: '3', 9 | plugins: [ 10 | 'serverless-esbuild', 11 | 'serverless-analyze-bundle-plugin', 12 | 'serverless-offline-sqs', 13 | 'serverless-offline', 14 | ], 15 | useDotenv: true, 16 | custom: getCustomConfig(serviceName), 17 | package: { 18 | individually: true, 19 | patterns: ['handler.js', '!node_modules/**'], 20 | excludeDevDependencies: true, 21 | }, 22 | provider: { 23 | name: 'aws', 24 | runtime: 'nodejs16.x', 25 | // profile: '', 26 | stage: "${opt:stage, 'dev'}", 27 | region: "${opt:region, 'us-east-1'}", 28 | memorySize: 512, // default: 1024MB 29 | timeout: 900, // 15 minutes - this is the maximum allowed by Lambda 30 | environment: { 31 | REGION: '${aws:region}', 32 | SLS_STAGE: '${sls:stage}', 33 | }, 34 | iam: { 35 | role: { 36 | statements: [ 37 | { 38 | Effect: 'Allow', 39 | Action: ['sqs:CreateQueue'], 40 | Resource: 'arn:aws:sqs:*:*:*', 41 | }, 42 | ], 43 | }, 44 | }, 45 | }, 46 | functions: { 47 | demo: { 48 | handler: 'handlers/demo.handler', 49 | events: [ 50 | { 51 | sqs: { 52 | arn: { 53 | 'Fn::GetAtt': ['DemoQueue', 'Arn'], 54 | }, 55 | }, 56 | }, 57 | ], 58 | }, 59 | }, 60 | resources: { 61 | Resources: { 62 | DemoQueue: { 63 | Type: 'AWS::SQS::Queue', 64 | Properties: { 65 | QueueName: 'DemoQueue', 66 | }, 67 | }, 68 | }, 69 | }, 70 | }; 71 | 72 | module.exports = serverlessConfiguration; 73 | -------------------------------------------------------------------------------- /services/background-jobs/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "types": ["node"] 6 | }, 7 | "exclude": ["**/*.spec.ts", "jest.config.ts"], 8 | "include": ["**/*.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /services/background-jobs/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/background-jobs/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", "jest.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /services/example-service/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../.eslintrc.json", 3 | "ignorePatterns": ["!**/*"], 4 | "rules": {} 5 | } 6 | -------------------------------------------------------------------------------- /services/example-service/README.md: -------------------------------------------------------------------------------- 1 | # example-service Service 2 | 3 | This service is used to demonstrate how you might interface with the main services, e.g. `public-api` 4 | and `background-jobs`. It provides an example of how to send messages to an SQS queue for processing 5 | as well as a lambda for generating data for a resolver. 6 | -------------------------------------------------------------------------------- /services/example-service/handlers/generateDemoJob.test.ts: -------------------------------------------------------------------------------- 1 | import { APIGatewayProxyEvent, Context, Callback } from 'aws-lambda'; 2 | import { sendMessage } from '@serverless-template/aws'; 3 | import { handler } from './generateDemoJob'; 4 | 5 | jest.mock('@serverless-template/aws'); 6 | 7 | describe('generateDemoJob', () => { 8 | let subject; 9 | let mathRandomMock; 10 | 11 | const sendMessageMock = jest.mocked(sendMessage, true); 12 | 13 | beforeAll(() => { 14 | mathRandomMock = jest.spyOn(global.Math, 'random').mockReturnValue(0.11); 15 | }); 16 | 17 | afterAll(() => { 18 | mathRandomMock.mockRestore(); 19 | jest.clearAllMocks(); 20 | }); 21 | 22 | describe('when the message is sent sucessfully', () => { 23 | beforeAll(async () => { 24 | sendMessageMock.mockResolvedValue({ 25 | success: true, 26 | data: { 27 | MessageId: '123456789', 28 | }, 29 | }); 30 | subject = await handler( 31 | {} as APIGatewayProxyEvent, 32 | {} as Context, 33 | {} as Callback 34 | ); 35 | }); 36 | 37 | afterAll(() => { 38 | sendMessageMock.mockClear(); 39 | }); 40 | 41 | it('returns a 200 status code', () => { 42 | expect(subject.statusCode).toBe(200); 43 | }); 44 | 45 | it('returns the returned messaged', () => { 46 | expect(JSON.parse(subject.body)).toEqual({ 47 | MessageId: '123456789', 48 | }); 49 | }); 50 | }); 51 | 52 | describe('when the message is not sent sucessfully', () => { 53 | beforeAll(async () => { 54 | sendMessageMock.mockResolvedValue({ 55 | success: false, 56 | data: 'bad request', 57 | }); 58 | subject = await handler( 59 | {} as APIGatewayProxyEvent, 60 | {} as Context, 61 | {} as Callback 62 | ); 63 | }); 64 | 65 | afterAll(() => { 66 | sendMessageMock.mockClear(); 67 | }); 68 | 69 | it('returns a 400 status code', () => { 70 | expect(subject.statusCode).toBe(400); 71 | }); 72 | 73 | it('returns the returned messaged', () => { 74 | expect(JSON.parse(subject.body)).toEqual('bad request'); 75 | }); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /services/example-service/handlers/generateDemoJob.ts: -------------------------------------------------------------------------------- 1 | import { APIGatewayProxyHandler } from 'aws-lambda'; 2 | import { sendMessage } from '@serverless-template/aws'; 3 | 4 | export const handler: APIGatewayProxyHandler = async () => { 5 | const resp = await sendMessage('DemoQueue', { 6 | id: Math.ceil(Math.random() * 100), 7 | message: 'Hello World!', 8 | }); 9 | 10 | return { 11 | statusCode: resp.success ? 200 : 400, 12 | body: JSON.stringify(resp.data), 13 | }; 14 | }; 15 | -------------------------------------------------------------------------------- /services/example-service/handlers/hello.test.ts: -------------------------------------------------------------------------------- 1 | import { handler } from './hello'; 2 | 3 | describe('hello', () => { 4 | let subject; 5 | 6 | beforeAll(async () => { 7 | subject = await handler({ 8 | body: JSON.stringify({ greeting: 'world' }), 9 | }); 10 | }); 11 | 12 | it('returns a greeting message', () => { 13 | expect(subject).toEqual('Hello, world'); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /services/example-service/handlers/hello.ts: -------------------------------------------------------------------------------- 1 | export const handler = async (event: { body: string }): Promise => { 2 | const { greeting } = JSON.parse(event.body); 3 | return `Hello, ${greeting}`; 4 | }; 5 | -------------------------------------------------------------------------------- /services/example-service/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'example-service', 4 | 5 | globals: { 6 | 'ts-jest': { 7 | tsconfig: '/tsconfig.spec.json', 8 | }, 9 | }, 10 | testEnvironment: 'node', 11 | collectCoverage: true, 12 | coverageDirectory: '../../coverage/services/example-service', 13 | coverageThreshold: { 14 | global: { 15 | branches: 100, 16 | functions: 100, 17 | lines: 100, 18 | statements: 100, 19 | }, 20 | }, 21 | coverageReporters: ['json'], 22 | preset: '../../jest.preset.js', 23 | }; 24 | -------------------------------------------------------------------------------- /services/example-service/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-service", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "projectType": "application", 5 | "sourceRoot": "services/example-service/src", 6 | "targets": { 7 | "build": { 8 | "executor": "./tools/executors/workspace:run-command", 9 | "options": { 10 | "cwd": "services/example-service", 11 | "color": true, 12 | "command": "sls package" 13 | } 14 | }, 15 | "serve": { 16 | "executor": "./tools/executors/workspace:run-command", 17 | "options": { 18 | "cwd": "services/example-service", 19 | "color": true, 20 | "command": "sls offline start" 21 | } 22 | }, 23 | "deploy": { 24 | "executor": "./tools/executors/workspace:run-command", 25 | "options": { 26 | "cwd": "services/example-service", 27 | "color": true, 28 | "command": "sls deploy --stage {args.stage}" 29 | } 30 | }, 31 | "remove": { 32 | "executor": "./tools/executors/workspace:run-command", 33 | "options": { 34 | "cwd": "services/example-service", 35 | "color": true, 36 | "command": "sls remove --stage {args.stage}" 37 | } 38 | }, 39 | "analyze": { 40 | "executor": "./tools/executors/workspace:run-command", 41 | "options": { 42 | "cwd": "services/example-service", 43 | "color": true, 44 | "command": "sls package --analyze {args.function}" 45 | } 46 | }, 47 | "lint": { 48 | "executor": "@nrwl/linter:eslint", 49 | "options": { 50 | "lintFilePatterns": ["services/example-service/**/*.ts"] 51 | } 52 | }, 53 | "test": { 54 | "executor": "@nrwl/jest:jest", 55 | "outputs": ["{workspaceRoot}/coverage/services/example-service"], 56 | "options": { 57 | "jestConfig": "services/example-service/jest.config.ts", 58 | "passWithNoTests": true 59 | } 60 | } 61 | }, 62 | "tags": ["service"] 63 | } 64 | -------------------------------------------------------------------------------- /services/example-service/serverless.ts: -------------------------------------------------------------------------------- 1 | import type { Serverless } from 'serverless/aws'; 2 | import { getCustomConfig } from '../../serverless.common'; 3 | 4 | const serviceName = 'example-service'; 5 | 6 | const serverlessConfiguration: Serverless = { 7 | service: serviceName, 8 | frameworkVersion: '3', 9 | plugins: [ 10 | 'serverless-esbuild', 11 | 'serverless-analyze-bundle-plugin', 12 | 'serverless-offline', 13 | ], 14 | useDotenv: true, 15 | custom: getCustomConfig(serviceName), 16 | package: { 17 | individually: true, 18 | patterns: ['handler.js', '!node_modules/**'], 19 | excludeDevDependencies: true, 20 | }, 21 | provider: { 22 | name: 'aws', 23 | runtime: 'nodejs16.x', 24 | // profile: '', 25 | stage: "${opt:stage, 'dev'}", 26 | region: "${opt:region, 'us-east-1'}", 27 | memorySize: 512, // default: 1024MB 28 | timeout: 29, // default: max allowable for Gateway 29 | environment: { 30 | REGION: '${aws:region}', 31 | SLS_STAGE: '${sls:stage}', 32 | }, 33 | iam: { 34 | role: { 35 | statements: [ 36 | { 37 | Effect: 'Allow', 38 | Action: ['sqs:SendMessage', 'sqs:GetQueueUrl'], 39 | Resource: 'arn:aws:sqs:*:*:*', 40 | }, 41 | ], 42 | }, 43 | }, 44 | }, 45 | functions: { 46 | hello: { 47 | handler: 'handlers/hello.handler', 48 | }, 49 | generateDemoJobs: { 50 | handler: 'handlers/generateDemoJob.handler', 51 | events: [ 52 | { 53 | httpApi: { 54 | method: 'get', 55 | path: '/generateDemoJob', 56 | }, 57 | }, 58 | ], 59 | }, 60 | }, 61 | }; 62 | 63 | module.exports = serverlessConfiguration; 64 | -------------------------------------------------------------------------------- /services/example-service/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "types": ["node"] 6 | }, 7 | "exclude": ["**/*.spec.ts", "jest.config.ts"], 8 | "include": ["**/*.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /services/example-service/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/example-service/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", "jest.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /services/public-api/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../.eslintrc.json", 3 | "ignorePatterns": ["!**/*", "src/types/graphql.ts"], 4 | "rules": {} 5 | } 6 | -------------------------------------------------------------------------------- /services/public-api/README.md: -------------------------------------------------------------------------------- 1 | # public-api Service 2 | 3 | This service acts as the public facing API for the entire application. 4 | 5 | ## Architecture 6 | 7 | This service provides a GraphQL API built with [Apollo Server 3](https://www.apollographql.com/docs/apollo-server/) by default that will act as a central gateway for all requests. 8 | 9 | If you need to provide REST Endpoints or callbacks endpoints, you can register them in the serverless.yml as well. 10 | 11 | ### Folder Structure 12 | 13 | All Lambda handlers should be in the `handlers` folder with colocated test files. The `healtcheck` function provides an example of how to structure a standard handler with test. The `graphql` function provides the Apollo Server. You should only need to modify this file if you're changing core Apollo Server features. 14 | 15 | Your GraphQL schema type definitions and resolvers should live in the `schema` folder. Each domain should have its own folder and contain the following files: 16 | 17 | - `domain.typedefs.ts` 18 | - `domain.resolvers.ts` 19 | - `domain.test.ts` 20 | 21 | The domain typedef and resolver should then be imported into `schema/index.ts` and registered to their respective export so they are properly loaded into the Apollo Server. 22 | 23 | The `types` folder contains all the types needed for this service. A `graphql.d.ts` is autogenerated using [GraphQL CodeGen](https://www.graphql-code-generator.com/). 24 | 25 | This service will host the primary API Gateway for the rest of the application so all public facing endpoints should be added to this service. 26 | 27 | ### Invoking other services in resolvers 28 | 29 | Most of your resolvers will probably invoke lambdas in other services. You can achieve this by using the `invoke` helper function from the `aws` lib. Your invocation will take the shape of: 30 | 31 | ```ts 32 | const data = await invoke({ 33 | serviceName: 'name-of-serivce', 34 | functionName: 'function-you-want-to-invoke', 35 | payload: { 36 | // payload to send to the invoked function 37 | }, 38 | context, // from the current request's context 39 | }); 40 | ``` 41 | 42 | ## Restricting Endpoints 43 | 44 | By default, all endpoints are public and unprotected from CORS. Before deploying to production or infrastucture, please read https://www.serverless.com/framework/docs/providers/aws/events/http-api/#cors-setup and set up CORS appropriately. 45 | 46 | ## Domain Configuration 47 | 48 | TBD 49 | 50 | ## Scripts 51 | 52 | `yarn deploy public-api` 53 | 54 | This will run `sls deploy` for the service and will use the defaults assigned in your serverless.yml. If you need to send overrides to the deploy command, you can pass arguments to the command as follows: 55 | 56 | ``` 57 | yarn deploy public-api --args="-region us-east-1 -stage prod" 58 | ``` 59 | -------------------------------------------------------------------------------- /services/public-api/codegen.yml: -------------------------------------------------------------------------------- 1 | overwrite: true 2 | schema: './src/schema/index.ts' 3 | documents: null 4 | generates: 5 | ./src/types/graphql.ts: 6 | plugins: 7 | - typescript 8 | - typescript-operations 9 | - typescript-resolvers 10 | ./src/schema/graphql.schema.json: 11 | plugins: 12 | - 'introspection' 13 | ./src/schema/schema.graphql: 14 | plugins: 15 | - schema-ast 16 | config: 17 | includeDirectives: true 18 | commentDescriptions: true 19 | sort: true 20 | require: 21 | - ts-node/register 22 | - tsconfig-paths/register 23 | -------------------------------------------------------------------------------- /services/public-api/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'public-api', 4 | 5 | globals: { 6 | 'ts-jest': { 7 | tsconfig: '/tsconfig.spec.json', 8 | }, 9 | }, 10 | testEnvironment: 'node', 11 | collectCoverage: true, 12 | coverageDirectory: '../../coverage/services/public-api', 13 | coverageThreshold: { 14 | global: { 15 | branches: 100, 16 | functions: 100, 17 | lines: 100, 18 | statements: 100, 19 | }, 20 | }, 21 | coverageReporters: ['json'], 22 | preset: '../../jest.preset.js', 23 | }; 24 | -------------------------------------------------------------------------------- /services/public-api/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "public-api", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "projectType": "application", 5 | "sourceRoot": "services/public-api/src", 6 | "targets": { 7 | "build": { 8 | "executor": "./tools/executors/workspace:run-command", 9 | "options": { 10 | "cwd": "services/public-api", 11 | "color": true, 12 | "command": "sls package" 13 | } 14 | }, 15 | "serve": { 16 | "executor": "./tools/executors/workspace:run-command", 17 | "options": { 18 | "cwd": "services/public-api", 19 | "color": true, 20 | "command": "sls offline start" 21 | }, 22 | "dependsOn": [ 23 | { 24 | "target": "codegen", 25 | "projects": "self" 26 | } 27 | ] 28 | }, 29 | "deploy": { 30 | "executor": "./tools/executors/workspace:run-command", 31 | "options": { 32 | "cwd": "services/public-api", 33 | "color": true, 34 | "command": "sls deploy --stage {args.stage}" 35 | } 36 | }, 37 | "remove": { 38 | "executor": "./tools/executors/workspace:run-command", 39 | "options": { 40 | "cwd": "services/public-api", 41 | "color": true, 42 | "command": "sls remove --stage {args.stage}" 43 | } 44 | }, 45 | "analyze": { 46 | "executor": "./tools/executors/workspace:run-command", 47 | "options": { 48 | "cwd": "services/public-api", 49 | "color": true, 50 | "command": "sls package --analyze {args.function}" 51 | } 52 | }, 53 | "lint": { 54 | "executor": "@nrwl/linter:eslint", 55 | "options": { 56 | "lintFilePatterns": ["services/public-api/**/*.ts"] 57 | } 58 | }, 59 | "test": { 60 | "executor": "@nrwl/jest:jest", 61 | "outputs": ["{workspaceRoot}/coverage/services/public-api"], 62 | "options": { 63 | "jestConfig": "services/public-api/jest.config.ts", 64 | "passWithNoTests": true 65 | }, 66 | "dependsOn": [ 67 | { 68 | "target": "codegen", 69 | "projects": "self" 70 | } 71 | ] 72 | }, 73 | "codegen": { 74 | "executor": "./tools/executors/workspace:run-command", 75 | "options": { 76 | "cwd": "services/public-api", 77 | "color": true, 78 | "command": "graphql-codegen; yarn format" 79 | } 80 | } 81 | }, 82 | "tags": ["service"] 83 | } 84 | -------------------------------------------------------------------------------- /services/public-api/serverless.ts: -------------------------------------------------------------------------------- 1 | import type { Serverless } from 'serverless/aws'; 2 | import { getCustomConfig } from '../../serverless.common'; 3 | 4 | const serviceName = 'public-api'; 5 | 6 | const serverlessConfiguration: Serverless = { 7 | service: serviceName, 8 | frameworkVersion: '3', 9 | plugins: [ 10 | 'serverless-esbuild', 11 | 'serverless-analyze-bundle-plugin', 12 | 'serverless-offline', 13 | ], 14 | useDotenv: true, 15 | custom: getCustomConfig(serviceName), 16 | package: { 17 | individually: true, 18 | patterns: ['handler.js', '!node_modules/**'], 19 | excludeDevDependencies: true, 20 | }, 21 | provider: { 22 | name: 'aws', 23 | runtime: 'nodejs16.x', 24 | // profile: '', 25 | stage: "${opt:stage, 'dev'}", 26 | region: "${opt:region, 'us-east-1'}", 27 | memorySize: 512, // default: 1024MB 28 | timeout: 29, // default: max allowable for Gateway 29 | httpApi: { 30 | // TODO: update to be more restrictive for real apps: https://www.serverless.com/framework/docs/providers/aws/events/http-api/#cors-setup 31 | cors: true, 32 | }, 33 | environment: { 34 | REGION: '${aws:region}', 35 | SLS_STAGE: '${sls:stage}', 36 | }, 37 | 38 | iam: { 39 | role: { 40 | statements: [ 41 | { 42 | Effect: 'Allow', 43 | Action: ['lambda:InvokeFunction'], 44 | Resource: 'arn:aws:lambda:*:*:*', 45 | }, 46 | ], 47 | }, 48 | }, 49 | tracing: { 50 | apiGateway: true, 51 | lambda: true, 52 | }, 53 | }, 54 | functions: { 55 | graphql: { 56 | handler: 'src/handlers/graphql.server', 57 | events: [ 58 | { 59 | httpApi: { 60 | method: 'post', 61 | path: '/graphql', 62 | }, 63 | }, 64 | { 65 | httpApi: { 66 | method: 'get', 67 | path: '/graphql', 68 | }, 69 | }, 70 | ], 71 | }, 72 | healthcheck: { 73 | handler: 'src/handlers/healthcheck.handler', 74 | events: [ 75 | { 76 | httpApi: { 77 | path: '/healthcheck', 78 | method: 'get', 79 | }, 80 | }, 81 | ], 82 | }, 83 | }, 84 | }; 85 | 86 | module.exports = serverlessConfiguration; 87 | -------------------------------------------------------------------------------- /services/public-api/src/handlers/graphql.test.ts: -------------------------------------------------------------------------------- 1 | import { server } from './graphql'; 2 | 3 | describe('apollo server', () => { 4 | test('returns graphql server handler', () => { 5 | expect(server).toBeInstanceOf(Function); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /services/public-api/src/handlers/graphql.ts: -------------------------------------------------------------------------------- 1 | import { ApolloServer } from 'apollo-server-lambda'; 2 | import { 3 | Context, 4 | APIGatewayProxyEventHeaders, 5 | APIGatewayProxyEvent, 6 | } from 'aws-lambda'; 7 | import { schema } from '../schema'; 8 | 9 | export type ApolloServerContext = { 10 | headers: APIGatewayProxyEventHeaders; 11 | functionName: string; 12 | event: APIGatewayProxyEvent; 13 | context: Context; 14 | }; 15 | 16 | // export for use in tests 17 | export const apolloServer = new ApolloServer({ 18 | schema, 19 | dataSources: () => ({}), 20 | context: async ({ event, context }: ApolloServerContext) => ({ 21 | headers: event.headers, 22 | functionName: context.functionName, 23 | event: event, 24 | context: context, 25 | }), 26 | cache: 'bounded', 27 | }); 28 | 29 | export const server = apolloServer.createHandler({ 30 | expressGetMiddlewareOptions: { 31 | cors: { 32 | origin: true, 33 | credentials: true, 34 | }, 35 | }, 36 | }); 37 | -------------------------------------------------------------------------------- /services/public-api/src/handlers/healthcheck.test.ts: -------------------------------------------------------------------------------- 1 | import { APIGatewayProxyEvent, Context, Callback } from 'aws-lambda'; 2 | import { handler } from './healthcheck'; 3 | 4 | describe('healtcheck', () => { 5 | let subject; 6 | 7 | beforeAll(async () => { 8 | subject = await handler( 9 | {} as APIGatewayProxyEvent, 10 | {} as Context, 11 | {} as Callback 12 | ); 13 | }); 14 | 15 | it('returns a 200 statusCode', () => { 16 | expect(subject.statusCode).toBe(200); 17 | }); 18 | 19 | it('returns a working message', () => { 20 | expect(subject.body).toEqual('public-api is working!'); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /services/public-api/src/handlers/healthcheck.ts: -------------------------------------------------------------------------------- 1 | import { APIGatewayProxyHandler } from 'aws-lambda'; 2 | 3 | export const handler: APIGatewayProxyHandler = 4 | async (/* _event, _context */) => { 5 | return { 6 | statusCode: 200, 7 | body: 'public-api is working!', 8 | }; 9 | }; 10 | -------------------------------------------------------------------------------- /services/public-api/src/schema/graphql.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "__schema": { 3 | "queryType": { 4 | "name": "Query" 5 | }, 6 | "mutationType": null, 7 | "subscriptionType": null, 8 | "types": [ 9 | { 10 | "kind": "SCALAR", 11 | "name": "Boolean", 12 | "description": "The `Boolean` scalar type represents `true` or `false`.", 13 | "fields": null, 14 | "inputFields": null, 15 | "interfaces": null, 16 | "enumValues": null, 17 | "possibleTypes": null 18 | }, 19 | { 20 | "kind": "OBJECT", 21 | "name": "Query", 22 | "description": null, 23 | "fields": [ 24 | { 25 | "name": "hello", 26 | "description": "Simple hello world query that accepts a greeting", 27 | "args": [ 28 | { 29 | "name": "greeting", 30 | "description": null, 31 | "type": { 32 | "kind": "NON_NULL", 33 | "name": null, 34 | "ofType": { 35 | "kind": "SCALAR", 36 | "name": "String", 37 | "ofType": null 38 | } 39 | }, 40 | "defaultValue": null, 41 | "isDeprecated": false, 42 | "deprecationReason": null 43 | } 44 | ], 45 | "type": { 46 | "kind": "NON_NULL", 47 | "name": null, 48 | "ofType": { 49 | "kind": "SCALAR", 50 | "name": "String", 51 | "ofType": null 52 | } 53 | }, 54 | "isDeprecated": false, 55 | "deprecationReason": null 56 | } 57 | ], 58 | "inputFields": null, 59 | "interfaces": [], 60 | "enumValues": null, 61 | "possibleTypes": null 62 | }, 63 | { 64 | "kind": "SCALAR", 65 | "name": "String", 66 | "description": "The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.", 67 | "fields": null, 68 | "inputFields": null, 69 | "interfaces": null, 70 | "enumValues": null, 71 | "possibleTypes": null 72 | }, 73 | { 74 | "kind": "OBJECT", 75 | "name": "__Directive", 76 | "description": "A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document.\n\nIn some cases, you need to provide options to alter GraphQL's execution behavior in ways field arguments will not suffice, such as conditionally including or skipping a field. Directives provide this by describing additional information to the executor.", 77 | "fields": [ 78 | { 79 | "name": "name", 80 | "description": null, 81 | "args": [], 82 | "type": { 83 | "kind": "NON_NULL", 84 | "name": null, 85 | "ofType": { 86 | "kind": "SCALAR", 87 | "name": "String", 88 | "ofType": null 89 | } 90 | }, 91 | "isDeprecated": false, 92 | "deprecationReason": null 93 | }, 94 | { 95 | "name": "description", 96 | "description": null, 97 | "args": [], 98 | "type": { 99 | "kind": "SCALAR", 100 | "name": "String", 101 | "ofType": null 102 | }, 103 | "isDeprecated": false, 104 | "deprecationReason": null 105 | }, 106 | { 107 | "name": "isRepeatable", 108 | "description": null, 109 | "args": [], 110 | "type": { 111 | "kind": "NON_NULL", 112 | "name": null, 113 | "ofType": { 114 | "kind": "SCALAR", 115 | "name": "Boolean", 116 | "ofType": null 117 | } 118 | }, 119 | "isDeprecated": false, 120 | "deprecationReason": null 121 | }, 122 | { 123 | "name": "locations", 124 | "description": null, 125 | "args": [], 126 | "type": { 127 | "kind": "NON_NULL", 128 | "name": null, 129 | "ofType": { 130 | "kind": "LIST", 131 | "name": null, 132 | "ofType": { 133 | "kind": "NON_NULL", 134 | "name": null, 135 | "ofType": { 136 | "kind": "ENUM", 137 | "name": "__DirectiveLocation", 138 | "ofType": null 139 | } 140 | } 141 | } 142 | }, 143 | "isDeprecated": false, 144 | "deprecationReason": null 145 | }, 146 | { 147 | "name": "args", 148 | "description": null, 149 | "args": [ 150 | { 151 | "name": "includeDeprecated", 152 | "description": null, 153 | "type": { 154 | "kind": "SCALAR", 155 | "name": "Boolean", 156 | "ofType": null 157 | }, 158 | "defaultValue": "false", 159 | "isDeprecated": false, 160 | "deprecationReason": null 161 | } 162 | ], 163 | "type": { 164 | "kind": "NON_NULL", 165 | "name": null, 166 | "ofType": { 167 | "kind": "LIST", 168 | "name": null, 169 | "ofType": { 170 | "kind": "NON_NULL", 171 | "name": null, 172 | "ofType": { 173 | "kind": "OBJECT", 174 | "name": "__InputValue", 175 | "ofType": null 176 | } 177 | } 178 | } 179 | }, 180 | "isDeprecated": false, 181 | "deprecationReason": null 182 | } 183 | ], 184 | "inputFields": null, 185 | "interfaces": [], 186 | "enumValues": null, 187 | "possibleTypes": null 188 | }, 189 | { 190 | "kind": "ENUM", 191 | "name": "__DirectiveLocation", 192 | "description": "A Directive can be adjacent to many parts of the GraphQL language, a __DirectiveLocation describes one such possible adjacencies.", 193 | "fields": null, 194 | "inputFields": null, 195 | "interfaces": null, 196 | "enumValues": [ 197 | { 198 | "name": "QUERY", 199 | "description": "Location adjacent to a query operation.", 200 | "isDeprecated": false, 201 | "deprecationReason": null 202 | }, 203 | { 204 | "name": "MUTATION", 205 | "description": "Location adjacent to a mutation operation.", 206 | "isDeprecated": false, 207 | "deprecationReason": null 208 | }, 209 | { 210 | "name": "SUBSCRIPTION", 211 | "description": "Location adjacent to a subscription operation.", 212 | "isDeprecated": false, 213 | "deprecationReason": null 214 | }, 215 | { 216 | "name": "FIELD", 217 | "description": "Location adjacent to a field.", 218 | "isDeprecated": false, 219 | "deprecationReason": null 220 | }, 221 | { 222 | "name": "FRAGMENT_DEFINITION", 223 | "description": "Location adjacent to a fragment definition.", 224 | "isDeprecated": false, 225 | "deprecationReason": null 226 | }, 227 | { 228 | "name": "FRAGMENT_SPREAD", 229 | "description": "Location adjacent to a fragment spread.", 230 | "isDeprecated": false, 231 | "deprecationReason": null 232 | }, 233 | { 234 | "name": "INLINE_FRAGMENT", 235 | "description": "Location adjacent to an inline fragment.", 236 | "isDeprecated": false, 237 | "deprecationReason": null 238 | }, 239 | { 240 | "name": "VARIABLE_DEFINITION", 241 | "description": "Location adjacent to a variable definition.", 242 | "isDeprecated": false, 243 | "deprecationReason": null 244 | }, 245 | { 246 | "name": "SCHEMA", 247 | "description": "Location adjacent to a schema definition.", 248 | "isDeprecated": false, 249 | "deprecationReason": null 250 | }, 251 | { 252 | "name": "SCALAR", 253 | "description": "Location adjacent to a scalar definition.", 254 | "isDeprecated": false, 255 | "deprecationReason": null 256 | }, 257 | { 258 | "name": "OBJECT", 259 | "description": "Location adjacent to an object type definition.", 260 | "isDeprecated": false, 261 | "deprecationReason": null 262 | }, 263 | { 264 | "name": "FIELD_DEFINITION", 265 | "description": "Location adjacent to a field definition.", 266 | "isDeprecated": false, 267 | "deprecationReason": null 268 | }, 269 | { 270 | "name": "ARGUMENT_DEFINITION", 271 | "description": "Location adjacent to an argument definition.", 272 | "isDeprecated": false, 273 | "deprecationReason": null 274 | }, 275 | { 276 | "name": "INTERFACE", 277 | "description": "Location adjacent to an interface definition.", 278 | "isDeprecated": false, 279 | "deprecationReason": null 280 | }, 281 | { 282 | "name": "UNION", 283 | "description": "Location adjacent to a union definition.", 284 | "isDeprecated": false, 285 | "deprecationReason": null 286 | }, 287 | { 288 | "name": "ENUM", 289 | "description": "Location adjacent to an enum definition.", 290 | "isDeprecated": false, 291 | "deprecationReason": null 292 | }, 293 | { 294 | "name": "ENUM_VALUE", 295 | "description": "Location adjacent to an enum value definition.", 296 | "isDeprecated": false, 297 | "deprecationReason": null 298 | }, 299 | { 300 | "name": "INPUT_OBJECT", 301 | "description": "Location adjacent to an input object type definition.", 302 | "isDeprecated": false, 303 | "deprecationReason": null 304 | }, 305 | { 306 | "name": "INPUT_FIELD_DEFINITION", 307 | "description": "Location adjacent to an input object field definition.", 308 | "isDeprecated": false, 309 | "deprecationReason": null 310 | } 311 | ], 312 | "possibleTypes": null 313 | }, 314 | { 315 | "kind": "OBJECT", 316 | "name": "__EnumValue", 317 | "description": "One possible value for a given Enum. Enum values are unique values, not a placeholder for a string or numeric value. However an Enum value is returned in a JSON response as a string.", 318 | "fields": [ 319 | { 320 | "name": "name", 321 | "description": null, 322 | "args": [], 323 | "type": { 324 | "kind": "NON_NULL", 325 | "name": null, 326 | "ofType": { 327 | "kind": "SCALAR", 328 | "name": "String", 329 | "ofType": null 330 | } 331 | }, 332 | "isDeprecated": false, 333 | "deprecationReason": null 334 | }, 335 | { 336 | "name": "description", 337 | "description": null, 338 | "args": [], 339 | "type": { 340 | "kind": "SCALAR", 341 | "name": "String", 342 | "ofType": null 343 | }, 344 | "isDeprecated": false, 345 | "deprecationReason": null 346 | }, 347 | { 348 | "name": "isDeprecated", 349 | "description": null, 350 | "args": [], 351 | "type": { 352 | "kind": "NON_NULL", 353 | "name": null, 354 | "ofType": { 355 | "kind": "SCALAR", 356 | "name": "Boolean", 357 | "ofType": null 358 | } 359 | }, 360 | "isDeprecated": false, 361 | "deprecationReason": null 362 | }, 363 | { 364 | "name": "deprecationReason", 365 | "description": null, 366 | "args": [], 367 | "type": { 368 | "kind": "SCALAR", 369 | "name": "String", 370 | "ofType": null 371 | }, 372 | "isDeprecated": false, 373 | "deprecationReason": null 374 | } 375 | ], 376 | "inputFields": null, 377 | "interfaces": [], 378 | "enumValues": null, 379 | "possibleTypes": null 380 | }, 381 | { 382 | "kind": "OBJECT", 383 | "name": "__Field", 384 | "description": "Object and Interface types are described by a list of Fields, each of which has a name, potentially a list of arguments, and a return type.", 385 | "fields": [ 386 | { 387 | "name": "name", 388 | "description": null, 389 | "args": [], 390 | "type": { 391 | "kind": "NON_NULL", 392 | "name": null, 393 | "ofType": { 394 | "kind": "SCALAR", 395 | "name": "String", 396 | "ofType": null 397 | } 398 | }, 399 | "isDeprecated": false, 400 | "deprecationReason": null 401 | }, 402 | { 403 | "name": "description", 404 | "description": null, 405 | "args": [], 406 | "type": { 407 | "kind": "SCALAR", 408 | "name": "String", 409 | "ofType": null 410 | }, 411 | "isDeprecated": false, 412 | "deprecationReason": null 413 | }, 414 | { 415 | "name": "args", 416 | "description": null, 417 | "args": [ 418 | { 419 | "name": "includeDeprecated", 420 | "description": null, 421 | "type": { 422 | "kind": "SCALAR", 423 | "name": "Boolean", 424 | "ofType": null 425 | }, 426 | "defaultValue": "false", 427 | "isDeprecated": false, 428 | "deprecationReason": null 429 | } 430 | ], 431 | "type": { 432 | "kind": "NON_NULL", 433 | "name": null, 434 | "ofType": { 435 | "kind": "LIST", 436 | "name": null, 437 | "ofType": { 438 | "kind": "NON_NULL", 439 | "name": null, 440 | "ofType": { 441 | "kind": "OBJECT", 442 | "name": "__InputValue", 443 | "ofType": null 444 | } 445 | } 446 | } 447 | }, 448 | "isDeprecated": false, 449 | "deprecationReason": null 450 | }, 451 | { 452 | "name": "type", 453 | "description": null, 454 | "args": [], 455 | "type": { 456 | "kind": "NON_NULL", 457 | "name": null, 458 | "ofType": { 459 | "kind": "OBJECT", 460 | "name": "__Type", 461 | "ofType": null 462 | } 463 | }, 464 | "isDeprecated": false, 465 | "deprecationReason": null 466 | }, 467 | { 468 | "name": "isDeprecated", 469 | "description": null, 470 | "args": [], 471 | "type": { 472 | "kind": "NON_NULL", 473 | "name": null, 474 | "ofType": { 475 | "kind": "SCALAR", 476 | "name": "Boolean", 477 | "ofType": null 478 | } 479 | }, 480 | "isDeprecated": false, 481 | "deprecationReason": null 482 | }, 483 | { 484 | "name": "deprecationReason", 485 | "description": null, 486 | "args": [], 487 | "type": { 488 | "kind": "SCALAR", 489 | "name": "String", 490 | "ofType": null 491 | }, 492 | "isDeprecated": false, 493 | "deprecationReason": null 494 | } 495 | ], 496 | "inputFields": null, 497 | "interfaces": [], 498 | "enumValues": null, 499 | "possibleTypes": null 500 | }, 501 | { 502 | "kind": "OBJECT", 503 | "name": "__InputValue", 504 | "description": "Arguments provided to Fields or Directives and the input fields of an InputObject are represented as Input Values which describe their type and optionally a default value.", 505 | "fields": [ 506 | { 507 | "name": "name", 508 | "description": null, 509 | "args": [], 510 | "type": { 511 | "kind": "NON_NULL", 512 | "name": null, 513 | "ofType": { 514 | "kind": "SCALAR", 515 | "name": "String", 516 | "ofType": null 517 | } 518 | }, 519 | "isDeprecated": false, 520 | "deprecationReason": null 521 | }, 522 | { 523 | "name": "description", 524 | "description": null, 525 | "args": [], 526 | "type": { 527 | "kind": "SCALAR", 528 | "name": "String", 529 | "ofType": null 530 | }, 531 | "isDeprecated": false, 532 | "deprecationReason": null 533 | }, 534 | { 535 | "name": "type", 536 | "description": null, 537 | "args": [], 538 | "type": { 539 | "kind": "NON_NULL", 540 | "name": null, 541 | "ofType": { 542 | "kind": "OBJECT", 543 | "name": "__Type", 544 | "ofType": null 545 | } 546 | }, 547 | "isDeprecated": false, 548 | "deprecationReason": null 549 | }, 550 | { 551 | "name": "defaultValue", 552 | "description": "A GraphQL-formatted string representing the default value for this input value.", 553 | "args": [], 554 | "type": { 555 | "kind": "SCALAR", 556 | "name": "String", 557 | "ofType": null 558 | }, 559 | "isDeprecated": false, 560 | "deprecationReason": null 561 | }, 562 | { 563 | "name": "isDeprecated", 564 | "description": null, 565 | "args": [], 566 | "type": { 567 | "kind": "NON_NULL", 568 | "name": null, 569 | "ofType": { 570 | "kind": "SCALAR", 571 | "name": "Boolean", 572 | "ofType": null 573 | } 574 | }, 575 | "isDeprecated": false, 576 | "deprecationReason": null 577 | }, 578 | { 579 | "name": "deprecationReason", 580 | "description": null, 581 | "args": [], 582 | "type": { 583 | "kind": "SCALAR", 584 | "name": "String", 585 | "ofType": null 586 | }, 587 | "isDeprecated": false, 588 | "deprecationReason": null 589 | } 590 | ], 591 | "inputFields": null, 592 | "interfaces": [], 593 | "enumValues": null, 594 | "possibleTypes": null 595 | }, 596 | { 597 | "kind": "OBJECT", 598 | "name": "__Schema", 599 | "description": "A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations.", 600 | "fields": [ 601 | { 602 | "name": "description", 603 | "description": null, 604 | "args": [], 605 | "type": { 606 | "kind": "SCALAR", 607 | "name": "String", 608 | "ofType": null 609 | }, 610 | "isDeprecated": false, 611 | "deprecationReason": null 612 | }, 613 | { 614 | "name": "types", 615 | "description": "A list of all types supported by this server.", 616 | "args": [], 617 | "type": { 618 | "kind": "NON_NULL", 619 | "name": null, 620 | "ofType": { 621 | "kind": "LIST", 622 | "name": null, 623 | "ofType": { 624 | "kind": "NON_NULL", 625 | "name": null, 626 | "ofType": { 627 | "kind": "OBJECT", 628 | "name": "__Type", 629 | "ofType": null 630 | } 631 | } 632 | } 633 | }, 634 | "isDeprecated": false, 635 | "deprecationReason": null 636 | }, 637 | { 638 | "name": "queryType", 639 | "description": "The type that query operations will be rooted at.", 640 | "args": [], 641 | "type": { 642 | "kind": "NON_NULL", 643 | "name": null, 644 | "ofType": { 645 | "kind": "OBJECT", 646 | "name": "__Type", 647 | "ofType": null 648 | } 649 | }, 650 | "isDeprecated": false, 651 | "deprecationReason": null 652 | }, 653 | { 654 | "name": "mutationType", 655 | "description": "If this server supports mutation, the type that mutation operations will be rooted at.", 656 | "args": [], 657 | "type": { 658 | "kind": "OBJECT", 659 | "name": "__Type", 660 | "ofType": null 661 | }, 662 | "isDeprecated": false, 663 | "deprecationReason": null 664 | }, 665 | { 666 | "name": "subscriptionType", 667 | "description": "If this server support subscription, the type that subscription operations will be rooted at.", 668 | "args": [], 669 | "type": { 670 | "kind": "OBJECT", 671 | "name": "__Type", 672 | "ofType": null 673 | }, 674 | "isDeprecated": false, 675 | "deprecationReason": null 676 | }, 677 | { 678 | "name": "directives", 679 | "description": "A list of all directives supported by this server.", 680 | "args": [], 681 | "type": { 682 | "kind": "NON_NULL", 683 | "name": null, 684 | "ofType": { 685 | "kind": "LIST", 686 | "name": null, 687 | "ofType": { 688 | "kind": "NON_NULL", 689 | "name": null, 690 | "ofType": { 691 | "kind": "OBJECT", 692 | "name": "__Directive", 693 | "ofType": null 694 | } 695 | } 696 | } 697 | }, 698 | "isDeprecated": false, 699 | "deprecationReason": null 700 | } 701 | ], 702 | "inputFields": null, 703 | "interfaces": [], 704 | "enumValues": null, 705 | "possibleTypes": null 706 | }, 707 | { 708 | "kind": "OBJECT", 709 | "name": "__Type", 710 | "description": "The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the `__TypeKind` enum.\n\nDepending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name, description and optional `specifiedByURL`, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types.", 711 | "fields": [ 712 | { 713 | "name": "kind", 714 | "description": null, 715 | "args": [], 716 | "type": { 717 | "kind": "NON_NULL", 718 | "name": null, 719 | "ofType": { 720 | "kind": "ENUM", 721 | "name": "__TypeKind", 722 | "ofType": null 723 | } 724 | }, 725 | "isDeprecated": false, 726 | "deprecationReason": null 727 | }, 728 | { 729 | "name": "name", 730 | "description": null, 731 | "args": [], 732 | "type": { 733 | "kind": "SCALAR", 734 | "name": "String", 735 | "ofType": null 736 | }, 737 | "isDeprecated": false, 738 | "deprecationReason": null 739 | }, 740 | { 741 | "name": "description", 742 | "description": null, 743 | "args": [], 744 | "type": { 745 | "kind": "SCALAR", 746 | "name": "String", 747 | "ofType": null 748 | }, 749 | "isDeprecated": false, 750 | "deprecationReason": null 751 | }, 752 | { 753 | "name": "specifiedByURL", 754 | "description": null, 755 | "args": [], 756 | "type": { 757 | "kind": "SCALAR", 758 | "name": "String", 759 | "ofType": null 760 | }, 761 | "isDeprecated": false, 762 | "deprecationReason": null 763 | }, 764 | { 765 | "name": "fields", 766 | "description": null, 767 | "args": [ 768 | { 769 | "name": "includeDeprecated", 770 | "description": null, 771 | "type": { 772 | "kind": "SCALAR", 773 | "name": "Boolean", 774 | "ofType": null 775 | }, 776 | "defaultValue": "false", 777 | "isDeprecated": false, 778 | "deprecationReason": null 779 | } 780 | ], 781 | "type": { 782 | "kind": "LIST", 783 | "name": null, 784 | "ofType": { 785 | "kind": "NON_NULL", 786 | "name": null, 787 | "ofType": { 788 | "kind": "OBJECT", 789 | "name": "__Field", 790 | "ofType": null 791 | } 792 | } 793 | }, 794 | "isDeprecated": false, 795 | "deprecationReason": null 796 | }, 797 | { 798 | "name": "interfaces", 799 | "description": null, 800 | "args": [], 801 | "type": { 802 | "kind": "LIST", 803 | "name": null, 804 | "ofType": { 805 | "kind": "NON_NULL", 806 | "name": null, 807 | "ofType": { 808 | "kind": "OBJECT", 809 | "name": "__Type", 810 | "ofType": null 811 | } 812 | } 813 | }, 814 | "isDeprecated": false, 815 | "deprecationReason": null 816 | }, 817 | { 818 | "name": "possibleTypes", 819 | "description": null, 820 | "args": [], 821 | "type": { 822 | "kind": "LIST", 823 | "name": null, 824 | "ofType": { 825 | "kind": "NON_NULL", 826 | "name": null, 827 | "ofType": { 828 | "kind": "OBJECT", 829 | "name": "__Type", 830 | "ofType": null 831 | } 832 | } 833 | }, 834 | "isDeprecated": false, 835 | "deprecationReason": null 836 | }, 837 | { 838 | "name": "enumValues", 839 | "description": null, 840 | "args": [ 841 | { 842 | "name": "includeDeprecated", 843 | "description": null, 844 | "type": { 845 | "kind": "SCALAR", 846 | "name": "Boolean", 847 | "ofType": null 848 | }, 849 | "defaultValue": "false", 850 | "isDeprecated": false, 851 | "deprecationReason": null 852 | } 853 | ], 854 | "type": { 855 | "kind": "LIST", 856 | "name": null, 857 | "ofType": { 858 | "kind": "NON_NULL", 859 | "name": null, 860 | "ofType": { 861 | "kind": "OBJECT", 862 | "name": "__EnumValue", 863 | "ofType": null 864 | } 865 | } 866 | }, 867 | "isDeprecated": false, 868 | "deprecationReason": null 869 | }, 870 | { 871 | "name": "inputFields", 872 | "description": null, 873 | "args": [ 874 | { 875 | "name": "includeDeprecated", 876 | "description": null, 877 | "type": { 878 | "kind": "SCALAR", 879 | "name": "Boolean", 880 | "ofType": null 881 | }, 882 | "defaultValue": "false", 883 | "isDeprecated": false, 884 | "deprecationReason": null 885 | } 886 | ], 887 | "type": { 888 | "kind": "LIST", 889 | "name": null, 890 | "ofType": { 891 | "kind": "NON_NULL", 892 | "name": null, 893 | "ofType": { 894 | "kind": "OBJECT", 895 | "name": "__InputValue", 896 | "ofType": null 897 | } 898 | } 899 | }, 900 | "isDeprecated": false, 901 | "deprecationReason": null 902 | }, 903 | { 904 | "name": "ofType", 905 | "description": null, 906 | "args": [], 907 | "type": { 908 | "kind": "OBJECT", 909 | "name": "__Type", 910 | "ofType": null 911 | }, 912 | "isDeprecated": false, 913 | "deprecationReason": null 914 | } 915 | ], 916 | "inputFields": null, 917 | "interfaces": [], 918 | "enumValues": null, 919 | "possibleTypes": null 920 | }, 921 | { 922 | "kind": "ENUM", 923 | "name": "__TypeKind", 924 | "description": "An enum describing what kind of type a given `__Type` is.", 925 | "fields": null, 926 | "inputFields": null, 927 | "interfaces": null, 928 | "enumValues": [ 929 | { 930 | "name": "SCALAR", 931 | "description": "Indicates this type is a scalar.", 932 | "isDeprecated": false, 933 | "deprecationReason": null 934 | }, 935 | { 936 | "name": "OBJECT", 937 | "description": "Indicates this type is an object. `fields` and `interfaces` are valid fields.", 938 | "isDeprecated": false, 939 | "deprecationReason": null 940 | }, 941 | { 942 | "name": "INTERFACE", 943 | "description": "Indicates this type is an interface. `fields`, `interfaces`, and `possibleTypes` are valid fields.", 944 | "isDeprecated": false, 945 | "deprecationReason": null 946 | }, 947 | { 948 | "name": "UNION", 949 | "description": "Indicates this type is a union. `possibleTypes` is a valid field.", 950 | "isDeprecated": false, 951 | "deprecationReason": null 952 | }, 953 | { 954 | "name": "ENUM", 955 | "description": "Indicates this type is an enum. `enumValues` is a valid field.", 956 | "isDeprecated": false, 957 | "deprecationReason": null 958 | }, 959 | { 960 | "name": "INPUT_OBJECT", 961 | "description": "Indicates this type is an input object. `inputFields` is a valid field.", 962 | "isDeprecated": false, 963 | "deprecationReason": null 964 | }, 965 | { 966 | "name": "LIST", 967 | "description": "Indicates this type is a list. `ofType` is a valid field.", 968 | "isDeprecated": false, 969 | "deprecationReason": null 970 | }, 971 | { 972 | "name": "NON_NULL", 973 | "description": "Indicates this type is a non-null. `ofType` is a valid field.", 974 | "isDeprecated": false, 975 | "deprecationReason": null 976 | } 977 | ], 978 | "possibleTypes": null 979 | } 980 | ], 981 | "directives": [ 982 | { 983 | "name": "deprecated", 984 | "description": "Marks an element of a GraphQL schema as no longer supported.", 985 | "isRepeatable": false, 986 | "locations": [ 987 | "ARGUMENT_DEFINITION", 988 | "ENUM_VALUE", 989 | "FIELD_DEFINITION", 990 | "INPUT_FIELD_DEFINITION" 991 | ], 992 | "args": [ 993 | { 994 | "name": "reason", 995 | "description": "Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted using the Markdown syntax, as specified by [CommonMark](https://commonmark.org/).", 996 | "type": { 997 | "kind": "SCALAR", 998 | "name": "String", 999 | "ofType": null 1000 | }, 1001 | "defaultValue": "\"No longer supported\"", 1002 | "isDeprecated": false, 1003 | "deprecationReason": null 1004 | } 1005 | ] 1006 | }, 1007 | { 1008 | "name": "include", 1009 | "description": "Directs the executor to include this field or fragment only when the `if` argument is true.", 1010 | "isRepeatable": false, 1011 | "locations": ["FIELD", "FRAGMENT_SPREAD", "INLINE_FRAGMENT"], 1012 | "args": [ 1013 | { 1014 | "name": "if", 1015 | "description": "Included when true.", 1016 | "type": { 1017 | "kind": "NON_NULL", 1018 | "name": null, 1019 | "ofType": { 1020 | "kind": "SCALAR", 1021 | "name": "Boolean", 1022 | "ofType": null 1023 | } 1024 | }, 1025 | "defaultValue": null, 1026 | "isDeprecated": false, 1027 | "deprecationReason": null 1028 | } 1029 | ] 1030 | }, 1031 | { 1032 | "name": "skip", 1033 | "description": "Directs the executor to skip this field or fragment when the `if` argument is true.", 1034 | "isRepeatable": false, 1035 | "locations": ["FIELD", "FRAGMENT_SPREAD", "INLINE_FRAGMENT"], 1036 | "args": [ 1037 | { 1038 | "name": "if", 1039 | "description": "Skipped when true.", 1040 | "type": { 1041 | "kind": "NON_NULL", 1042 | "name": null, 1043 | "ofType": { 1044 | "kind": "SCALAR", 1045 | "name": "Boolean", 1046 | "ofType": null 1047 | } 1048 | }, 1049 | "defaultValue": null, 1050 | "isDeprecated": false, 1051 | "deprecationReason": null 1052 | } 1053 | ] 1054 | }, 1055 | { 1056 | "name": "specifiedBy", 1057 | "description": "Exposes a URL that specifies the behavior of this scalar.", 1058 | "isRepeatable": false, 1059 | "locations": ["SCALAR"], 1060 | "args": [ 1061 | { 1062 | "name": "url", 1063 | "description": "The URL that specifies the behavior of this scalar.", 1064 | "type": { 1065 | "kind": "NON_NULL", 1066 | "name": null, 1067 | "ofType": { 1068 | "kind": "SCALAR", 1069 | "name": "String", 1070 | "ofType": null 1071 | } 1072 | }, 1073 | "defaultValue": null, 1074 | "isDeprecated": false, 1075 | "deprecationReason": null 1076 | } 1077 | ] 1078 | } 1079 | ] 1080 | } 1081 | } 1082 | -------------------------------------------------------------------------------- /services/public-api/src/schema/hello/hello.resolvers.ts: -------------------------------------------------------------------------------- 1 | import { Resolvers } from '../../types/graphql'; 2 | import { invoke } from '@serverless-template/aws'; 3 | 4 | export const helloResolvers: Resolvers = { 5 | Query: { 6 | hello: async (_parent, { greeting }, context): Promise => { 7 | const resp = await invoke({ 8 | serviceName: 'example-service', 9 | functionName: 'hello', 10 | payload: { greeting }, 11 | context, 12 | }); 13 | return resp; 14 | }, 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /services/public-api/src/schema/hello/hello.test.ts: -------------------------------------------------------------------------------- 1 | import { gql } from 'apollo-server-lambda'; 2 | import { invoke } from '@serverless-template/aws'; 3 | import { apolloServerExecute } from '../../utils/apolloTest'; 4 | 5 | jest.mock('@serverless-template/aws'); 6 | 7 | describe('hello query', () => { 8 | let subject; 9 | let greeting: string; 10 | 11 | const invokeMock = jest.mocked(invoke, true); 12 | 13 | beforeAll(async () => { 14 | greeting = 'World!'; 15 | const QUERY = gql` 16 | query HelloWorldQuery($greeting: String!) { 17 | hello(greeting: $greeting) 18 | } 19 | `; 20 | invokeMock.mockResolvedValue('Hello, World!'); 21 | subject = await apolloServerExecute({ 22 | query: QUERY, 23 | variables: { greeting }, 24 | }); 25 | }); 26 | 27 | afterAll(() => { 28 | jest.clearAllMocks(); 29 | }); 30 | 31 | it('calls the example-service hello function', () => { 32 | expect(invokeMock).toHaveBeenCalledWith({ 33 | serviceName: 'example-service', 34 | functionName: 'hello', 35 | payload: { greeting }, 36 | context: expect.any(Object), 37 | }); 38 | }); 39 | 40 | it('returns the salutation concatenated with the greeting', () => { 41 | expect(subject.data).toEqual({ 42 | hello: `Hello, ${greeting}`, 43 | }); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /services/public-api/src/schema/hello/hello.typedefs.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | export const helloTypeDefs = gql` 4 | type Query { 5 | "Simple hello world query that accepts a greeting" 6 | hello(greeting: String!): String! 7 | } 8 | `; 9 | -------------------------------------------------------------------------------- /services/public-api/src/schema/hello/index.ts: -------------------------------------------------------------------------------- 1 | export { helloTypeDefs } from './hello.typedefs'; 2 | export { helloResolvers } from './hello.resolvers'; 3 | -------------------------------------------------------------------------------- /services/public-api/src/schema/index.ts: -------------------------------------------------------------------------------- 1 | import { mergeResolvers, mergeTypeDefs } from '@graphql-tools/merge'; 2 | import { makeExecutableSchema } from '@graphql-tools/schema'; 3 | import { helloTypeDefs, helloResolvers } from './hello'; 4 | 5 | const typeDefs = mergeTypeDefs([helloTypeDefs]); 6 | 7 | const resolvers = mergeResolvers([helloResolvers]); 8 | 9 | export const schema = makeExecutableSchema({ typeDefs, resolvers }); 10 | -------------------------------------------------------------------------------- /services/public-api/src/schema/schema.graphql: -------------------------------------------------------------------------------- 1 | schema { 2 | query: Query 3 | } 4 | 5 | type Query { 6 | "Simple hello world query that accepts a greeting" 7 | hello(greeting: String!): String! 8 | } 9 | -------------------------------------------------------------------------------- /services/public-api/src/types/graphql.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLResolveInfo } from 'graphql'; 2 | export type Maybe = T | null; 3 | export type InputMaybe = Maybe; 4 | export type Exact = { 5 | [K in keyof T]: T[K]; 6 | }; 7 | export type MakeOptional = Omit & { 8 | [SubKey in K]?: Maybe; 9 | }; 10 | export type MakeMaybe = Omit & { 11 | [SubKey in K]: Maybe; 12 | }; 13 | export type RequireFields = Omit & { 14 | [P in K]-?: NonNullable; 15 | }; 16 | /** All built-in and custom scalars, mapped to their actual values */ 17 | export type Scalars = { 18 | ID: string; 19 | String: string; 20 | Boolean: boolean; 21 | Int: number; 22 | Float: number; 23 | }; 24 | 25 | export type Query = { 26 | __typename?: 'Query'; 27 | /** Simple hello world query that accepts a greeting */ 28 | hello: Scalars['String']; 29 | }; 30 | 31 | export type QueryHelloArgs = { 32 | greeting: Scalars['String']; 33 | }; 34 | 35 | export type ResolverTypeWrapper = Promise | T; 36 | 37 | export type ResolverWithResolve = { 38 | resolve: ResolverFn; 39 | }; 40 | export type Resolver = 41 | | ResolverFn 42 | | ResolverWithResolve; 43 | 44 | export type ResolverFn = ( 45 | parent: TParent, 46 | args: TArgs, 47 | context: TContext, 48 | info: GraphQLResolveInfo 49 | ) => Promise | TResult; 50 | 51 | export type SubscriptionSubscribeFn = ( 52 | parent: TParent, 53 | args: TArgs, 54 | context: TContext, 55 | info: GraphQLResolveInfo 56 | ) => AsyncIterable | Promise>; 57 | 58 | export type SubscriptionResolveFn = ( 59 | parent: TParent, 60 | args: TArgs, 61 | context: TContext, 62 | info: GraphQLResolveInfo 63 | ) => TResult | Promise; 64 | 65 | export interface SubscriptionSubscriberObject< 66 | TResult, 67 | TKey extends string, 68 | TParent, 69 | TContext, 70 | TArgs 71 | > { 72 | subscribe: SubscriptionSubscribeFn< 73 | { [key in TKey]: TResult }, 74 | TParent, 75 | TContext, 76 | TArgs 77 | >; 78 | resolve?: SubscriptionResolveFn< 79 | TResult, 80 | { [key in TKey]: TResult }, 81 | TContext, 82 | TArgs 83 | >; 84 | } 85 | 86 | export interface SubscriptionResolverObject { 87 | subscribe: SubscriptionSubscribeFn; 88 | resolve: SubscriptionResolveFn; 89 | } 90 | 91 | export type SubscriptionObject< 92 | TResult, 93 | TKey extends string, 94 | TParent, 95 | TContext, 96 | TArgs 97 | > = 98 | | SubscriptionSubscriberObject 99 | | SubscriptionResolverObject; 100 | 101 | export type SubscriptionResolver< 102 | TResult, 103 | TKey extends string, 104 | TParent = {}, 105 | TContext = {}, 106 | TArgs = {} 107 | > = 108 | | (( 109 | ...args: any[] 110 | ) => SubscriptionObject) 111 | | SubscriptionObject; 112 | 113 | export type TypeResolveFn = ( 114 | parent: TParent, 115 | context: TContext, 116 | info: GraphQLResolveInfo 117 | ) => Maybe | Promise>; 118 | 119 | export type IsTypeOfResolverFn = ( 120 | obj: T, 121 | context: TContext, 122 | info: GraphQLResolveInfo 123 | ) => boolean | Promise; 124 | 125 | export type NextResolverFn = () => Promise; 126 | 127 | export type DirectiveResolverFn< 128 | TResult = {}, 129 | TParent = {}, 130 | TContext = {}, 131 | TArgs = {} 132 | > = ( 133 | next: NextResolverFn, 134 | parent: TParent, 135 | args: TArgs, 136 | context: TContext, 137 | info: GraphQLResolveInfo 138 | ) => TResult | Promise; 139 | 140 | /** Mapping between all available schema types and the resolvers types */ 141 | export type ResolversTypes = { 142 | Boolean: ResolverTypeWrapper; 143 | Query: ResolverTypeWrapper<{}>; 144 | String: ResolverTypeWrapper; 145 | }; 146 | 147 | /** Mapping between all available schema types and the resolvers parents */ 148 | export type ResolversParentTypes = { 149 | Boolean: Scalars['Boolean']; 150 | Query: {}; 151 | String: Scalars['String']; 152 | }; 153 | 154 | export type QueryResolvers< 155 | ContextType = any, 156 | ParentType extends ResolversParentTypes['Query'] = ResolversParentTypes['Query'] 157 | > = { 158 | hello?: Resolver< 159 | ResolversTypes['String'], 160 | ParentType, 161 | ContextType, 162 | RequireFields 163 | >; 164 | }; 165 | 166 | export type Resolvers = { 167 | Query?: QueryResolvers; 168 | }; 169 | -------------------------------------------------------------------------------- /services/public-api/src/utils/apolloTest.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLRequest } from 'apollo-server-types'; 2 | import { DocumentNode } from 'graphql'; 3 | import { apolloServer } from '../handlers/graphql'; 4 | import { LambdaContextFunctionParams } from 'apollo-server-lambda/dist/ApolloServer'; 5 | 6 | const defaultContextParams: LambdaContextFunctionParams = { 7 | event: { headers: {}, random: '123' }, 8 | context: {}, 9 | express: undefined, 10 | }; 11 | 12 | export const apolloServerExecute = ( 13 | request: Omit & { 14 | query?: string | DocumentNode; 15 | }, 16 | contextParams: LambdaContextFunctionParams = defaultContextParams 17 | ) => { 18 | return apolloServer.executeOperation( 19 | request, 20 | Object.assign(defaultContextParams, contextParams) 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /services/public-api/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "types": ["node"] 6 | }, 7 | "exclude": ["**/*.spec.ts", "jest.config.ts"], 8 | "include": ["**/*.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /services/public-api/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/public-api/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", "jest.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /tools/executors/workspace/executor.json: -------------------------------------------------------------------------------- 1 | { 2 | "executors": { 3 | "run-command": { 4 | "implementation": "./impl", 5 | "schema": "./schema.json", 6 | "description": "Runs a command without capturing stdin/stdout/stderr" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tools/executors/workspace/impl.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var __assign = 3 | (this && this.__assign) || 4 | function () { 5 | __assign = 6 | Object.assign || 7 | function (t) { 8 | for (var s, i = 1, n = arguments.length; i < n; i++) { 9 | s = arguments[i]; 10 | for (var p in s) 11 | if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; 12 | } 13 | return t; 14 | }; 15 | return __assign.apply(this, arguments); 16 | }; 17 | var __awaiter = 18 | (this && this.__awaiter) || 19 | function (thisArg, _arguments, P, generator) { 20 | function adopt(value) { 21 | return value instanceof P 22 | ? value 23 | : new P(function (resolve) { 24 | resolve(value); 25 | }); 26 | } 27 | return new (P || (P = Promise))(function (resolve, reject) { 28 | function fulfilled(value) { 29 | try { 30 | step(generator.next(value)); 31 | } catch (e) { 32 | reject(e); 33 | } 34 | } 35 | function rejected(value) { 36 | try { 37 | step(generator['throw'](value)); 38 | } catch (e) { 39 | reject(e); 40 | } 41 | } 42 | function step(result) { 43 | result.done 44 | ? resolve(result.value) 45 | : adopt(result.value).then(fulfilled, rejected); 46 | } 47 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 48 | }); 49 | }; 50 | var __generator = 51 | (this && this.__generator) || 52 | function (thisArg, body) { 53 | var _ = { 54 | label: 0, 55 | sent: function () { 56 | if (t[0] & 1) throw t[1]; 57 | return t[1]; 58 | }, 59 | trys: [], 60 | ops: [], 61 | }, 62 | f, 63 | y, 64 | t, 65 | g; 66 | return ( 67 | (g = { next: verb(0), throw: verb(1), return: verb(2) }), 68 | typeof Symbol === 'function' && 69 | (g[Symbol.iterator] = function () { 70 | return this; 71 | }), 72 | g 73 | ); 74 | function verb(n) { 75 | return function (v) { 76 | return step([n, v]); 77 | }; 78 | } 79 | function step(op) { 80 | if (f) throw new TypeError('Generator is already executing.'); 81 | while (_) 82 | try { 83 | if ( 84 | ((f = 1), 85 | y && 86 | (t = 87 | op[0] & 2 88 | ? y['return'] 89 | : op[0] 90 | ? y['throw'] || ((t = y['return']) && t.call(y), 0) 91 | : y.next) && 92 | !(t = t.call(y, op[1])).done) 93 | ) 94 | return t; 95 | if (((y = 0), t)) op = [op[0] & 2, t.value]; 96 | switch (op[0]) { 97 | case 0: 98 | case 1: 99 | t = op; 100 | break; 101 | case 4: 102 | _.label++; 103 | return { value: op[1], done: false }; 104 | case 5: 105 | _.label++; 106 | y = op[1]; 107 | op = [0]; 108 | continue; 109 | case 7: 110 | op = _.ops.pop(); 111 | _.trys.pop(); 112 | continue; 113 | default: 114 | if ( 115 | !((t = _.trys), (t = t.length > 0 && t[t.length - 1])) && 116 | (op[0] === 6 || op[0] === 2) 117 | ) { 118 | _ = 0; 119 | continue; 120 | } 121 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { 122 | _.label = op[1]; 123 | break; 124 | } 125 | if (op[0] === 6 && _.label < t[1]) { 126 | _.label = t[1]; 127 | t = op; 128 | break; 129 | } 130 | if (t && _.label < t[2]) { 131 | _.label = t[2]; 132 | _.ops.push(op); 133 | break; 134 | } 135 | if (t[2]) _.ops.pop(); 136 | _.trys.pop(); 137 | continue; 138 | } 139 | op = body.call(thisArg, _); 140 | } catch (e) { 141 | op = [6, e]; 142 | y = 0; 143 | } finally { 144 | f = t = 0; 145 | } 146 | if (op[0] & 5) throw op[1]; 147 | return { value: op[0] ? op[1] : void 0, done: true }; 148 | } 149 | }; 150 | exports.__esModule = true; 151 | exports.LARGE_BUFFER = void 0; 152 | var child_process_1 = require('child_process'); 153 | var path = require('path'); 154 | var yargsParser = require('yargs-parser'); 155 | var npm_run_path_1 = require('npm-run-path'); 156 | exports.LARGE_BUFFER = 1024 * 1000000; 157 | function loadEnvVars(path) { 158 | return __awaiter(this, void 0, void 0, function () { 159 | var result, _a; 160 | return __generator(this, function (_b) { 161 | switch (_b.label) { 162 | case 0: 163 | if (!path) return [3 /*break*/, 2]; 164 | return [ 165 | 4 /*yield*/, 166 | Promise.resolve().then(function () { 167 | return require('dotenv'); 168 | }), 169 | ]; 170 | case 1: 171 | result = _b.sent().config({ path: path }); 172 | if (result.error) { 173 | throw result.error; 174 | } 175 | return [3 /*break*/, 5]; 176 | case 2: 177 | _b.trys.push([2, 4, , 5]); 178 | return [ 179 | 4 /*yield*/, 180 | Promise.resolve().then(function () { 181 | return require('dotenv'); 182 | }), 183 | ]; 184 | case 3: 185 | _b.sent().config(); 186 | return [3 /*break*/, 5]; 187 | case 4: 188 | _a = _b.sent(); 189 | return [3 /*break*/, 5]; 190 | case 5: 191 | return [2 /*return*/]; 192 | } 193 | }); 194 | }); 195 | } 196 | var propKeys = [ 197 | 'command', 198 | 'commands', 199 | 'color', 200 | 'parallel', 201 | 'readyWhen', 202 | 'cwd', 203 | 'args', 204 | 'envFile', 205 | 'outputPath', 206 | ]; 207 | function default_1(options, context) { 208 | return __awaiter(this, void 0, void 0, function () { 209 | var normalized, success, _a, e_1; 210 | return __generator(this, function (_b) { 211 | switch (_b.label) { 212 | case 0: 213 | return [4 /*yield*/, loadEnvVars(options.envFile)]; 214 | case 1: 215 | _b.sent(); 216 | normalized = normalizeOptions(options); 217 | if (options.readyWhen && !options.parallel) { 218 | throw new Error( 219 | 'ERROR: Bad executor config for @nrwl/run-commands - "readyWhen" can only be used when "parallel=true".' 220 | ); 221 | } 222 | _b.label = 2; 223 | case 2: 224 | _b.trys.push([2, 7, , 8]); 225 | if (!options.parallel) return [3 /*break*/, 4]; 226 | return [4 /*yield*/, runInParallel(normalized, context)]; 227 | case 3: 228 | _a = _b.sent(); 229 | return [3 /*break*/, 6]; 230 | case 4: 231 | return [4 /*yield*/, runSerially(normalized, context)]; 232 | case 5: 233 | _a = _b.sent(); 234 | _b.label = 6; 235 | case 6: 236 | success = _a; 237 | return [2 /*return*/, { success: success }]; 238 | case 7: 239 | e_1 = _b.sent(); 240 | if (process.env.NX_VERBOSE_LOGGING === 'true') { 241 | console.error(e_1); 242 | } 243 | throw new Error( 244 | 'ERROR: Something went wrong in @nrwl/run-commands - '.concat( 245 | e_1.message 246 | ) 247 | ); 248 | case 8: 249 | return [2 /*return*/]; 250 | } 251 | }); 252 | }); 253 | } 254 | exports['default'] = default_1; 255 | function runInParallel(options, context) { 256 | return __awaiter(this, void 0, void 0, function () { 257 | var procs, r, r, failed; 258 | return __generator(this, function (_a) { 259 | switch (_a.label) { 260 | case 0: 261 | procs = options.commands.map(function (c) { 262 | return createProcess( 263 | c.command, 264 | options.readyWhen, 265 | options.color, 266 | calculateCwd(options.cwd, context) 267 | ).then(function (result) { 268 | return { 269 | result: result, 270 | command: c.command, 271 | }; 272 | }); 273 | }); 274 | if (!options.readyWhen) return [3 /*break*/, 2]; 275 | return [4 /*yield*/, Promise.race(procs)]; 276 | case 1: 277 | r = _a.sent(); 278 | if (!r.result) { 279 | process.stderr.write( 280 | 'Warning: @nrwl/run-commands command "'.concat( 281 | r.command, 282 | '" exited with non-zero status code' 283 | ) 284 | ); 285 | return [2 /*return*/, false]; 286 | } else { 287 | return [2 /*return*/, true]; 288 | } 289 | return [3 /*break*/, 4]; 290 | case 2: 291 | return [4 /*yield*/, Promise.all(procs)]; 292 | case 3: 293 | r = _a.sent(); 294 | failed = r.filter(function (v) { 295 | return !v.result; 296 | }); 297 | if (failed.length > 0) { 298 | failed.forEach(function (f) { 299 | process.stderr.write( 300 | 'Warning: @nrwl/run-commands command "'.concat( 301 | f.command, 302 | '" exited with non-zero status code' 303 | ) 304 | ); 305 | }); 306 | return [2 /*return*/, false]; 307 | } else { 308 | return [2 /*return*/, true]; 309 | } 310 | _a.label = 4; 311 | case 4: 312 | return [2 /*return*/]; 313 | } 314 | }); 315 | }); 316 | } 317 | function normalizeOptions(options) { 318 | options.parsedArgs = parseArgs(options); 319 | if (options.command) { 320 | options.commands = [{ command: options.command }]; 321 | options.parallel = !!options.readyWhen; 322 | } else { 323 | options.commands = options.commands.map(function (c) { 324 | return typeof c === 'string' ? { command: c } : c; 325 | }); 326 | } 327 | options.commands.forEach(function (c) { 328 | var _a; 329 | c.command = transformCommand( 330 | c.command, 331 | options.parsedArgs, 332 | (_a = c.forwardAllArgs) !== null && _a !== void 0 ? _a : true 333 | ); 334 | }); 335 | return options; 336 | } 337 | function runSerially(options, context) { 338 | return __awaiter(this, void 0, void 0, function () { 339 | var _i, _a, c; 340 | return __generator(this, function (_b) { 341 | for (_i = 0, _a = options.commands; _i < _a.length; _i++) { 342 | c = _a[_i]; 343 | createSyncProcess( 344 | c.command, 345 | options.color, 346 | calculateCwd(options.cwd, context) 347 | ); 348 | } 349 | return [2 /*return*/, true]; 350 | }); 351 | }); 352 | } 353 | function createProcess(command, readyWhen, color, cwd) { 354 | return new Promise(function (res) { 355 | var childProcess = (0, child_process_1.exec)(command, { 356 | maxBuffer: exports.LARGE_BUFFER, 357 | env: processEnv(color), 358 | cwd: cwd, 359 | }); 360 | /** 361 | * Ensure the child process is killed when the parent exits 362 | */ 363 | var processExitListener = function () { 364 | return childProcess.kill(); 365 | }; 366 | process.on('exit', processExitListener); 367 | process.on('SIGTERM', processExitListener); 368 | childProcess.stdout.on('data', function (data) { 369 | process.stdout.write(data); 370 | if (readyWhen && data.toString().indexOf(readyWhen) > -1) { 371 | res(true); 372 | } 373 | }); 374 | childProcess.stderr.on('data', function (err) { 375 | process.stderr.write(err); 376 | if (readyWhen && err.toString().indexOf(readyWhen) > -1) { 377 | res(true); 378 | } 379 | }); 380 | childProcess.on('exit', function (code) { 381 | if (!readyWhen) { 382 | res(code === 0); 383 | } 384 | }); 385 | }); 386 | } 387 | function createSyncProcess(command, color, cwd) { 388 | (0, child_process_1.execSync)(command, { 389 | env: processEnv(color), 390 | stdio: [process.stdin, process.stdout, process.stderr], 391 | maxBuffer: exports.LARGE_BUFFER, 392 | cwd: cwd, 393 | }); 394 | } 395 | function calculateCwd(cwd, context) { 396 | if (!cwd) return context.root; 397 | if (path.isAbsolute(cwd)) return cwd; 398 | return path.join(context.root, cwd); 399 | } 400 | function processEnv(color) { 401 | var env = __assign(__assign({}, process.env), (0, npm_run_path_1.env)()); 402 | if (color) { 403 | env.FORCE_COLOR = ''.concat(color); 404 | } 405 | return env; 406 | } 407 | function transformCommand(command, args, forwardAllArgs) { 408 | if (command.indexOf('{args.') > -1) { 409 | var regex = /{args\.([^}]+)}/g; 410 | return command.replace(regex, function (_, group) { 411 | return args[camelCase(group)]; 412 | }); 413 | } else if (Object.keys(args).length > 0 && forwardAllArgs) { 414 | var stringifiedArgs = Object.keys(args) 415 | .map(function (a) { 416 | return typeof args[a] === 'string' && args[a].includes(' ') 417 | ? '--'.concat(a, '="').concat(args[a].replace(/"/g, '"'), '"') 418 | : '--'.concat(a, '=').concat(args[a]); 419 | }) 420 | .join(' '); 421 | return ''.concat(command, ' ').concat(stringifiedArgs); 422 | } else { 423 | return command; 424 | } 425 | } 426 | function parseArgs(options) { 427 | var args = options.args; 428 | if (!args) { 429 | var unknownOptionsTreatedAsArgs = Object.keys(options) 430 | .filter(function (p) { 431 | return propKeys.indexOf(p) === -1; 432 | }) 433 | .reduce(function (m, c) { 434 | return (m[c] = options[c]), m; 435 | }, {}); 436 | return unknownOptionsTreatedAsArgs; 437 | } 438 | return yargsParser(args.replace(/(^"|"$)/g, ''), { 439 | configuration: { 'camel-case-expansion': true }, 440 | }); 441 | } 442 | function camelCase(input) { 443 | if (input.indexOf('-') > 1) { 444 | return input.toLowerCase().replace(/-(.)/g, function (match, group1) { 445 | return group1.toUpperCase(); 446 | }); 447 | } else { 448 | return input; 449 | } 450 | } 451 | -------------------------------------------------------------------------------- /tools/executors/workspace/impl.ts: -------------------------------------------------------------------------------- 1 | import { ExecutorContext } from '@nrwl/devkit'; 2 | import { exec, execSync } from 'child_process'; 3 | import * as path from 'path'; 4 | import * as yargsParser from 'yargs-parser'; 5 | import { env as appendLocalEnv } from 'npm-run-path'; 6 | 7 | export const LARGE_BUFFER = 1024 * 1000000; 8 | 9 | async function loadEnvVars(path?: string) { 10 | if (path) { 11 | const result = (await import('dotenv')).config({ path }); 12 | if (result.error) { 13 | throw result.error; 14 | } 15 | } else { 16 | try { 17 | (await import('dotenv')).config(); 18 | } catch {} 19 | } 20 | } 21 | 22 | export type Json = { [k: string]: any }; 23 | export interface RunCommandsBuilderOptions extends Json { 24 | command?: string; 25 | commands?: ( 26 | | { 27 | command: string; 28 | forwardAllArgs?: boolean; 29 | /** 30 | * description was added to allow users to document their commands inline, 31 | * it is not intended to be used as part of the execution of the command. 32 | */ 33 | description?: string; 34 | } 35 | | string 36 | )[]; 37 | color?: boolean; 38 | parallel?: boolean; 39 | readyWhen?: string; 40 | cwd?: string; 41 | args?: string; 42 | envFile?: string; 43 | outputPath?: string; 44 | } 45 | 46 | const propKeys = [ 47 | 'command', 48 | 'commands', 49 | 'color', 50 | 'parallel', 51 | 'readyWhen', 52 | 'cwd', 53 | 'args', 54 | 'envFile', 55 | 'outputPath', 56 | ]; 57 | 58 | export interface NormalizedRunCommandsBuilderOptions 59 | extends RunCommandsBuilderOptions { 60 | commands: { 61 | command: string; 62 | forwardAllArgs?: boolean; 63 | }[]; 64 | parsedArgs: { [k: string]: any }; 65 | } 66 | 67 | export default async function ( 68 | options: RunCommandsBuilderOptions, 69 | context: ExecutorContext 70 | ): Promise<{ success: boolean }> { 71 | await loadEnvVars(options.envFile); 72 | const normalized = normalizeOptions(options); 73 | 74 | if (options.readyWhen && !options.parallel) { 75 | throw new Error( 76 | 'ERROR: Bad executor config for @nrwl/run-commands - "readyWhen" can only be used when "parallel=true".' 77 | ); 78 | } 79 | 80 | try { 81 | const success = options.parallel 82 | ? await runInParallel(normalized, context) 83 | : await runSerially(normalized, context); 84 | return { success }; 85 | } catch (e) { 86 | if (process.env.NX_VERBOSE_LOGGING === 'true') { 87 | console.error(e); 88 | } 89 | throw new Error( 90 | `ERROR: Something went wrong in @nrwl/run-commands - ${e.message}` 91 | ); 92 | } 93 | } 94 | 95 | async function runInParallel( 96 | options: NormalizedRunCommandsBuilderOptions, 97 | context: ExecutorContext 98 | ) { 99 | const procs = options.commands.map((c) => 100 | createProcess( 101 | c.command, 102 | options.readyWhen, 103 | options.color, 104 | calculateCwd(options.cwd, context) 105 | ).then((result) => ({ 106 | result, 107 | command: c.command, 108 | })) 109 | ); 110 | 111 | if (options.readyWhen) { 112 | const r = await Promise.race(procs); 113 | if (!r.result) { 114 | process.stderr.write( 115 | `Warning: @nrwl/run-commands command "${r.command}" exited with non-zero status code` 116 | ); 117 | return false; 118 | } else { 119 | return true; 120 | } 121 | } else { 122 | const r = await Promise.all(procs); 123 | const failed = r.filter((v) => !v.result); 124 | if (failed.length > 0) { 125 | failed.forEach((f) => { 126 | process.stderr.write( 127 | `Warning: @nrwl/run-commands command "${f.command}" exited with non-zero status code` 128 | ); 129 | }); 130 | return false; 131 | } else { 132 | return true; 133 | } 134 | } 135 | } 136 | 137 | function normalizeOptions( 138 | options: RunCommandsBuilderOptions 139 | ): NormalizedRunCommandsBuilderOptions { 140 | options.parsedArgs = parseArgs(options); 141 | 142 | if (options.command) { 143 | options.commands = [{ command: options.command }]; 144 | options.parallel = !!options.readyWhen; 145 | } else { 146 | options.commands = options.commands.map((c) => 147 | typeof c === 'string' ? { command: c } : c 148 | ); 149 | } 150 | (options as NormalizedRunCommandsBuilderOptions).commands.forEach((c) => { 151 | c.command = transformCommand( 152 | c.command, 153 | (options as NormalizedRunCommandsBuilderOptions).parsedArgs, 154 | c.forwardAllArgs ?? true 155 | ); 156 | }); 157 | return options as any; 158 | } 159 | 160 | async function runSerially( 161 | options: NormalizedRunCommandsBuilderOptions, 162 | context: ExecutorContext 163 | ) { 164 | for (const c of options.commands) { 165 | createSyncProcess( 166 | c.command, 167 | options.color, 168 | calculateCwd(options.cwd, context) 169 | ); 170 | } 171 | return true; 172 | } 173 | 174 | function createProcess( 175 | command: string, 176 | readyWhen: string, 177 | color: boolean, 178 | cwd: string 179 | ): Promise { 180 | return new Promise((res) => { 181 | const childProcess = exec(command, { 182 | maxBuffer: LARGE_BUFFER, 183 | env: processEnv(color), 184 | cwd, 185 | }); 186 | /** 187 | * Ensure the child process is killed when the parent exits 188 | */ 189 | const processExitListener = () => childProcess.kill(); 190 | process.on('exit', processExitListener); 191 | process.on('SIGTERM', processExitListener); 192 | childProcess.stdout.on('data', (data) => { 193 | process.stdout.write(data); 194 | if (readyWhen && data.toString().indexOf(readyWhen) > -1) { 195 | res(true); 196 | } 197 | }); 198 | childProcess.stderr.on('data', (err) => { 199 | process.stderr.write(err); 200 | if (readyWhen && err.toString().indexOf(readyWhen) > -1) { 201 | res(true); 202 | } 203 | }); 204 | childProcess.on('exit', (code) => { 205 | if (!readyWhen) { 206 | res(code === 0); 207 | } 208 | }); 209 | }); 210 | } 211 | 212 | function createSyncProcess(command: string, color: boolean, cwd: string) { 213 | execSync(command, { 214 | env: processEnv(color), 215 | stdio: [process.stdin, process.stdout, process.stderr], 216 | maxBuffer: LARGE_BUFFER, 217 | cwd, 218 | }); 219 | } 220 | 221 | function calculateCwd( 222 | cwd: string | undefined, 223 | context: ExecutorContext 224 | ): string { 225 | if (!cwd) return context.root; 226 | if (path.isAbsolute(cwd)) return cwd; 227 | return path.join(context.root, cwd); 228 | } 229 | 230 | function processEnv(color: boolean) { 231 | const env = { 232 | ...process.env, 233 | ...appendLocalEnv(), 234 | }; 235 | 236 | if (color) { 237 | env.FORCE_COLOR = `${color}`; 238 | } 239 | return env; 240 | } 241 | 242 | function transformCommand( 243 | command: string, 244 | args: { [key: string]: string }, 245 | forwardAllArgs: boolean 246 | ) { 247 | if (command.indexOf('{args.') > -1) { 248 | const regex = /{args\.([^}]+)}/g; 249 | return command.replace(regex, (_, group: string) => args[camelCase(group)]); 250 | } else if (Object.keys(args).length > 0 && forwardAllArgs) { 251 | const stringifiedArgs = Object.keys(args) 252 | .map((a) => 253 | typeof args[a] === 'string' && args[a].includes(' ') 254 | ? `--${a}="${args[a].replace(/"/g, '"')}"` 255 | : `--${a}=${args[a]}` 256 | ) 257 | .join(' '); 258 | return `${command} ${stringifiedArgs}`; 259 | } else { 260 | return command; 261 | } 262 | } 263 | 264 | function parseArgs(options: RunCommandsBuilderOptions) { 265 | const args = options.args; 266 | if (!args) { 267 | const unknownOptionsTreatedAsArgs = Object.keys(options) 268 | .filter((p) => propKeys.indexOf(p) === -1) 269 | .reduce((m, c) => ((m[c] = options[c]), m), {}); 270 | return unknownOptionsTreatedAsArgs; 271 | } 272 | return yargsParser(args.replace(/(^"|"$)/g, ''), { 273 | configuration: { 'camel-case-expansion': true }, 274 | }); 275 | } 276 | 277 | function camelCase(input) { 278 | if (input.indexOf('-') > 1) { 279 | return input 280 | .toLowerCase() 281 | .replace(/-(.)/g, (match, group1) => group1.toUpperCase()); 282 | } else { 283 | return input; 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /tools/executors/workspace/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "executors": "./executor.json" 3 | } 4 | -------------------------------------------------------------------------------- /tools/executors/workspace/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/schema", 3 | "type": "object", 4 | "cli": "nx", 5 | "properties": { 6 | "command": { 7 | "type": "string", 8 | "description": "The command to run" 9 | }, 10 | "cwd": { 11 | "type": "string", 12 | "description": "The working directory to run the command in" 13 | } 14 | }, 15 | "required": ["command"] 16 | } 17 | -------------------------------------------------------------------------------- /tools/generators/lib/files/console.ts__tmpl__: -------------------------------------------------------------------------------- 1 | (async () => { 2 | const repl = await import('repl'); 3 | let functionsMap = await import('./src'); 4 | 5 | const replServer = repl.start({ 6 | prompt: 'app > ', 7 | useColors: true, 8 | }); 9 | 10 | replServer.setupHistory('./.node_repl_history', (err) => { 11 | if (err) { 12 | console.error(err); 13 | } 14 | }); 15 | 16 | Object.entries(functionsMap).forEach(([key, value]) => { 17 | replServer.context[key] = value; 18 | }); 19 | 20 | replServer.defineCommand('re', { 21 | help: 'Reload the models without resetting the environment', 22 | async action() { 23 | // bust require cache 24 | Object.keys(require.cache).forEach((key) => { 25 | delete require.cache[key]; 26 | }); 27 | 28 | // fetch map of functions to reload 29 | try { 30 | // import * as functionsMap from './src'; 31 | functionsMap = await import('./src'); 32 | } catch (err) { 33 | console.error(err); 34 | } 35 | Object.entries(functionsMap).forEach(([key, value]) => { 36 | replServer.context[key] = value; 37 | }); 38 | 39 | // inform user that reload is complete 40 | console.log('reloaded!'); 41 | 42 | // reset the prompt 43 | this.displayPrompt(); 44 | }, 45 | }); 46 | })(); 47 | -------------------------------------------------------------------------------- /tools/generators/lib/files/src/index.ts__tmpl__: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /tools/generators/lib/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Tree, 3 | formatFiles, 4 | installPackagesTask, 5 | readProjectConfiguration, 6 | generateFiles, 7 | joinPathFragments, 8 | updateProjectConfiguration, 9 | } from '@nrwl/devkit'; 10 | import { libraryGenerator } from '@nrwl/workspace/generators'; 11 | import { addPropertyToJestConfig } from '@nrwl/jest'; 12 | 13 | type Schema = { 14 | readonly name: string; 15 | }; 16 | 17 | export default async function (tree: Tree, schema: Schema) { 18 | await libraryGenerator(tree, { name: schema.name }); 19 | const libraryRoot = readProjectConfiguration(tree, schema.name).root; 20 | 21 | generateFiles(tree, joinPathFragments(__dirname, './files'), libraryRoot, { 22 | ...schema, 23 | tmpl: '', 24 | }); 25 | 26 | updateProject(tree, schema, libraryRoot); 27 | 28 | addPropertyToJestConfig( 29 | tree, 30 | `${libraryRoot}/jest.config.ts`, 31 | 'collectCoverage', 32 | true 33 | ); 34 | addPropertyToJestConfig( 35 | tree, 36 | `${libraryRoot}/jest.config.ts`, 37 | 'coverageThreshold', 38 | { 39 | global: { 40 | branches: 100, 41 | functions: 100, 42 | lines: 100, 43 | statements: 100, 44 | }, 45 | } 46 | ); 47 | 48 | await formatFiles(tree); 49 | return () => { 50 | installPackagesTask(tree); 51 | }; 52 | } 53 | 54 | // See https://github.com/nrwl/nx/blob/efedd2eff78700a72bcc30bdf7450656860a4ffb/packages/node/src/generators/library/library.ts#L130-L156 55 | function updateProject(tree: Tree, options: Schema, libraryRoot: string) { 56 | const project = readProjectConfiguration(tree, options.name); 57 | 58 | project.targets = project.targets || {}; 59 | project.targets.console = { 60 | executor: './tools/executors/workspace:run-command', 61 | options: { 62 | cwd: libraryRoot, 63 | color: true, 64 | command: 65 | 'node --experimental-repl-await -r ts-node/register -r tsconfig-paths/register ./console.ts', 66 | }, 67 | }; 68 | project.tags = ['lib']; 69 | 70 | updateProjectConfiguration(tree, options.name, project); 71 | } 72 | -------------------------------------------------------------------------------- /tools/generators/lib/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/schema", 3 | "cli": "nx", 4 | "$id": "lib", 5 | "type": "object", 6 | "properties": { 7 | "name": { 8 | "type": "string", 9 | "description": "Library name", 10 | "$default": { 11 | "$source": "argv", 12 | "index": 0 13 | } 14 | } 15 | }, 16 | "required": ["name"] 17 | } 18 | -------------------------------------------------------------------------------- /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 %> Service 2 | 3 | What is the purpose of this service? 4 | 5 | ## Architecture 6 | 7 | - What belongs in this service? 8 | - Any particular file structures or rules to follow for contribution? 9 | - Other important architectural decisions 10 | 11 | ## Scripts 12 | 13 | List important scripts here 14 | -------------------------------------------------------------------------------- /tools/generators/service/files/handler.test.ts__tmpl__: -------------------------------------------------------------------------------- 1 | import { APIGatewayProxyEvent, Context, Callback } from 'aws-lambda'; 2 | import { healthcheck } from './handler'; 3 | 4 | describe('healtcheck', () => { 5 | let subject; 6 | 7 | beforeAll(async () => { 8 | subject = await healthcheck( 9 | {} as APIGatewayProxyEvent, 10 | {} as Context, 11 | {} as Callback 12 | ); 13 | }); 14 | 15 | it('returns a 200 statusCode', () => { 16 | expect(subject.statusCode).toBe(200); 17 | }); 18 | 19 | it('returns a working message', () => { 20 | expect(subject.body).toEqual('<%= name %> is working!'); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /tools/generators/service/files/handler.ts__tmpl__: -------------------------------------------------------------------------------- 1 | import { APIGatewayProxyHandler } from 'aws-lambda'; 2 | 3 | export const healthcheck: APIGatewayProxyHandler = async (_event, _context) => { 4 | return { 5 | statusCode: 200, 6 | body: '<%= name %> is working!', 7 | }; 8 | }; 9 | -------------------------------------------------------------------------------- /tools/generators/service/files/serverless.ts__tmpl__: -------------------------------------------------------------------------------- 1 | import type { Serverless } from 'serverless/aws'; 2 | import { getCustomConfig } from '../../serverless.common'; 3 | 4 | const serviceName = '<%= name %>'; 5 | 6 | const serverlessConfiguration: Serverless = { 7 | service: serviceName, 8 | frameworkVersion: '3', 9 | plugins: [ 10 | 'serverless-esbuild', 11 | 'serverless-analyze-bundle-plugin', 12 | 'serverless-offline', 13 | ], 14 | useDotenv: true, 15 | custom: getCustomConfig(serviceName), 16 | package: { 17 | individually: true, 18 | patterns: ['handler.js', '!node_modules/**'], 19 | excludeDevDependencies: true, 20 | }, 21 | provider: { 22 | name: 'aws', 23 | runtime: 'nodejs16.x', 24 | // profile: '', 25 | stage: "${opt:stage, 'dev'}", 26 | region: "${opt:region, 'us-east-1'}", 27 | memorySize: 512, // default: 1024MB 28 | timeout: 29, // default: max allowable for Gateway 29 | environment: { 30 | REGION: '${aws:region}', 31 | SLS_STAGE: '${sls:stage}', 32 | }, 33 | iam: { 34 | role: { 35 | statements: [], 36 | }, 37 | }, 38 | }, 39 | functions: { 40 | healthcheck: { 41 | handler: 'handler.healthcheck', 42 | events: [ 43 | { 44 | httpApi: { 45 | method: 'get', 46 | path: '/healthcheck', 47 | }, 48 | }, 49 | ], 50 | }, 51 | }, 52 | }; 53 | 54 | module.exports = serverlessConfiguration; 55 | -------------------------------------------------------------------------------- /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 | Tree, 7 | readProjectConfiguration, 8 | updateProjectConfiguration, 9 | } from '@nrwl/devkit'; 10 | import { Schema } from './schema'; 11 | import { addJest } from './jest-config'; 12 | import { addWorkspaceConfig } from './workspace-config'; 13 | import { updateServerlessCommon } from './serverless-common'; 14 | 15 | export default async (host: Tree, schema: Schema) => { 16 | const serviceRoot = `services/${schema.name}`; 17 | 18 | updateServerlessCommon(schema.name); 19 | 20 | generateFiles( 21 | host, // the virtual file system 22 | joinPathFragments(__dirname, './files'), // path to the file templates 23 | serviceRoot, // destination path of the files 24 | { ...schema, tmpl: '' } // config object to replace variable in file templates 25 | ); 26 | 27 | addWorkspaceConfig(host, schema.name, serviceRoot); 28 | 29 | await addJest(host, schema.name, serviceRoot); 30 | 31 | updateProject(host, schema, serviceRoot); 32 | 33 | await formatFiles(host); 34 | 35 | return () => { 36 | installPackagesTask(host); 37 | }; 38 | }; 39 | 40 | // See https://github.com/nrwl/nx/blob/efedd2eff78700a72bcc30bdf7450656860a4ffb/packages/node/src/generators/library/library.ts#L130-L156 41 | function updateProject(tree: Tree, options: Schema, serviceRoot: string) { 42 | const project = readProjectConfiguration(tree, options.name); 43 | 44 | project.targets = project.targets || {}; 45 | 46 | project.tags = ['service']; 47 | 48 | updateProjectConfiguration(tree, options.name, project); 49 | } 50 | -------------------------------------------------------------------------------- /tools/generators/service/jest-config.ts: -------------------------------------------------------------------------------- 1 | import { Tree } from '@nrwl/devkit'; 2 | import { jestProjectGenerator, addPropertyToJestConfig } from '@nrwl/jest'; 3 | import { JestProjectSchema } from '@nrwl/jest/src/generators/jest-project/schema'; 4 | 5 | export const addJest = async ( 6 | host: Tree, 7 | projectName: string, 8 | serviceRoot: string 9 | ) => { 10 | await jestProjectGenerator(host, { 11 | project: projectName, 12 | setupFile: 'none', 13 | testEnvironment: 'node', 14 | skipSerializers: false, 15 | skipSetupFile: false, 16 | supportTsx: false, 17 | babelJest: false, 18 | skipFormat: true, 19 | }); 20 | 21 | addPropertyToJestConfig( 22 | host, 23 | `${serviceRoot}/jest.config.ts`, 24 | 'collectCoverage', 25 | true 26 | ); 27 | addPropertyToJestConfig( 28 | host, 29 | `${serviceRoot}/jest.config.ts`, 30 | 'coverageThreshold', 31 | { 32 | global: { 33 | branches: 100, 34 | functions: 100, 35 | lines: 100, 36 | statements: 100, 37 | }, 38 | } 39 | ); 40 | addPropertyToJestConfig( 41 | host, 42 | `${serviceRoot}/jest.config.ts`, 43 | 'coverageReporters', 44 | ['json'] 45 | ); 46 | }; 47 | -------------------------------------------------------------------------------- /tools/generators/service/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "cli": "nx", 3 | "id": "serverless", 4 | "type": "object", 5 | "properties": { 6 | "name": { 7 | "type": "string", 8 | "description": "Library name", 9 | "$default": { 10 | "$source": "argv", 11 | "index": 0 12 | } 13 | } 14 | }, 15 | "required": ["name"] 16 | } 17 | -------------------------------------------------------------------------------- /tools/generators/service/schema.ts: -------------------------------------------------------------------------------- 1 | export type Schema = { 2 | readonly name: string; 3 | }; 4 | -------------------------------------------------------------------------------- /tools/generators/service/serverless-common.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import { parse, ParserOptions } from '@babel/parser'; 3 | import generate from '@babel/generator'; 4 | import { 5 | TSTypeAliasDeclaration, 6 | TSUnionType, 7 | tSLiteralType, 8 | stringLiteral, 9 | ExportNamedDeclaration, 10 | VariableDeclaration, 11 | ObjectExpression, 12 | objectProperty, 13 | objectExpression, 14 | numericLiteral, 15 | ObjectProperty, 16 | isObjectProperty, 17 | NumericLiteral, 18 | } from '@babel/types'; 19 | 20 | export interface ConversionConfig { 21 | isInitFile: boolean; 22 | } 23 | 24 | const DEFAULT_BABEL_OPTIONS: ParserOptions = { 25 | sourceType: 'unambiguous', 26 | plugins: ['jsx', 'typescript', 'classProperties'], 27 | }; 28 | 29 | function getNextPort(portConfig: ObjectExpression): number { 30 | if (!portConfig) { 31 | return 3000; 32 | } 33 | 34 | const usedPorts = portConfig.properties 35 | .filter((node): node is ObjectProperty => isObjectProperty(node)) 36 | .flatMap((property: ObjectProperty) => { 37 | return (property.value as ObjectExpression).properties 38 | .filter((node): node is ObjectProperty => isObjectProperty(node)) 39 | .map((innerProp) => (innerProp.value as NumericLiteral).value); 40 | }); 41 | 42 | return Math.max(...usedPorts) + 2; 43 | } 44 | 45 | function getNewPort(port: number) { 46 | return { 47 | httpPort: port, 48 | lambdaPort: port + 2, 49 | }; 50 | } 51 | 52 | export const updateServerlessCommon = async (serviceName: string) => { 53 | try { 54 | const filePath = './serverless.common.ts'; 55 | const doc = fs.readFileSync(filePath).toString(); 56 | 57 | const babelASTFile = parse(doc, DEFAULT_BABEL_OPTIONS); 58 | 59 | // push new service name into service type definition 60 | const serviceDeclaration = babelASTFile.program 61 | .body[1] as ExportNamedDeclaration; 62 | const serviceVariableDeclaration = 63 | serviceDeclaration.declaration as TSTypeAliasDeclaration; 64 | const serviceTypeUnion = 65 | serviceVariableDeclaration.typeAnnotation as TSUnionType; 66 | serviceTypeUnion.types.push(tSLiteralType(stringLiteral(serviceName))); 67 | 68 | // push new ports into port config 69 | const portsDeclaration = babelASTFile.program 70 | .body[4] as ExportNamedDeclaration; 71 | const portsVaraibleDeclaration = 72 | portsDeclaration.declaration as VariableDeclaration; 73 | const portsObject = portsVaraibleDeclaration.declarations[0] 74 | .init as ObjectExpression; 75 | const nextPort = getNextPort(portsObject); 76 | const newPort = getNewPort(nextPort); 77 | portsObject.properties.push( 78 | objectProperty( 79 | stringLiteral(serviceName), 80 | objectExpression([ 81 | objectProperty( 82 | stringLiteral('httpPort'), 83 | numericLiteral(newPort.httpPort) 84 | ), 85 | objectProperty( 86 | stringLiteral('lambdaPort'), 87 | numericLiteral(newPort.lambdaPort) 88 | ), 89 | ]) 90 | ) 91 | ); 92 | 93 | // generate output code and write to file 94 | // @ts-ignore: upstream types issue 95 | const updatedConfig = generate(babelASTFile).code; 96 | fs.writeFileSync('./serverless.common.ts', updatedConfig); 97 | } catch (e) { 98 | console.log(e); 99 | } 100 | }; 101 | -------------------------------------------------------------------------------- /tools/generators/service/workspace-config.ts: -------------------------------------------------------------------------------- 1 | import { addProjectConfiguration, Tree } from '@nrwl/devkit'; 2 | 3 | const buildRunCommandConfig = (dir: string, command: string) => ({ 4 | executor: './tools/executors/workspace:run-command', 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( 30 | serviceRoot, 31 | 'sls deploy --stage {args.stage}' 32 | ), 33 | }, 34 | remove: { 35 | ...buildRunCommandConfig( 36 | serviceRoot, 37 | 'sls remove --stage {args.stage}' 38 | ), 39 | }, 40 | analyze: { 41 | ...buildRunCommandConfig( 42 | serviceRoot, 43 | 'sls package --analyze {args.function}' 44 | ), 45 | }, 46 | lint: { 47 | executor: '@nrwl/linter:eslint', 48 | options: { 49 | lintFilePatterns: [serviceRoot + '/**/*.ts'], 50 | }, 51 | }, 52 | }, 53 | tags: ['service'], 54 | }); 55 | }; 56 | -------------------------------------------------------------------------------- /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": "es5", 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": "node16", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "importHelpers": false, 11 | "target": "ES2018", 12 | "module": "node16", 13 | "skipLibCheck": true, 14 | "skipDefaultLibCheck": true, 15 | "baseUrl": ".", 16 | "paths": { 17 | "@serverless-template/aws": ["libs/aws/src/index.ts"], 18 | "@serverless-template/serverless-common": ["serverless.common.ts"], 19 | "@serverless-template/utils": ["libs/utils/src/index.ts"] 20 | } 21 | }, 22 | "exclude": ["node_modules", "tmp"] 23 | } 24 | -------------------------------------------------------------------------------- /workspace.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "projects": { 4 | "aws": "libs/aws", 5 | "background-jobs": "services/background-jobs", 6 | "example-service": "services/example-service", 7 | "public-api": "services/public-api", 8 | "utils": "libs/utils" 9 | }, 10 | "$schema": "./node_modules/nx/schemas/workspace-schema.json" 11 | } 12 | --------------------------------------------------------------------------------