├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc.js ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── backend-go ├── .air.toml ├── .dockerignore ├── .env.template ├── .gitignore ├── Dockerfile ├── Makefile ├── cmd │ └── main.go ├── db │ ├── interface.go │ ├── postgres.go │ └── postgres_test.go ├── docker-compose.yml ├── go.mod ├── go.sum ├── gqlgen.yml ├── graphql │ ├── generated │ │ ├── models.go │ │ └── types.go │ ├── graphql.go │ ├── graphql_test.go │ ├── resolvers │ │ ├── post.resolvers.go │ │ ├── resolver.go │ │ └── user.resolvers.go │ └── schema │ │ ├── post.graphql │ │ └── user.graphql └── scripts │ └── db │ ├── dump.sql │ └── init.sh ├── backend ├── .dockerignore ├── .env.template ├── Dockerfile ├── config │ └── index.ts ├── docker-compose.yml ├── index.ts ├── jest.config.js ├── nodemon.json ├── package.json ├── packages │ ├── db │ │ ├── README.md │ │ ├── index.ts │ │ ├── package.json │ │ └── prisma │ │ │ ├── index.ts │ │ │ ├── migrations │ │ │ ├── 20211104122622_initial_migration │ │ │ │ └── migration.sql │ │ │ └── migration_lock.toml │ │ │ └── schema.prisma │ ├── graphql │ │ ├── README.md │ │ ├── api │ │ │ ├── index.ts │ │ │ └── queries │ │ │ │ ├── post │ │ │ │ └── getPost.ts │ │ │ │ └── user │ │ │ │ └── getUser.ts │ │ ├── codegen.yml │ │ ├── index.ts │ │ ├── package.json │ │ ├── schema │ │ │ ├── index.ts │ │ │ ├── post │ │ │ │ ├── index.ts │ │ │ │ ├── queries.graphql │ │ │ │ ├── resolvers │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── queries │ │ │ │ │ │ ├── getPost.ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ └── types │ │ │ │ │ │ ├── Post.ts │ │ │ │ │ │ └── index.ts │ │ │ │ └── types.graphql │ │ │ └── user │ │ │ │ ├── index.ts │ │ │ │ ├── queries.graphql │ │ │ │ ├── resolvers │ │ │ │ ├── index.ts │ │ │ │ ├── queries │ │ │ │ │ ├── getUser.ts │ │ │ │ │ └── index.ts │ │ │ │ └── types │ │ │ │ │ ├── User.ts │ │ │ │ │ └── index.ts │ │ │ │ └── types.graphql │ │ └── types │ │ │ ├── index.ts │ │ │ └── resolvers.ts │ └── utils │ │ ├── index.ts │ │ ├── logger.ts │ │ └── package.json ├── tests │ ├── db │ │ └── db.test.ts │ ├── graphql │ │ └── graphql.test.ts │ └── utils │ │ └── index.ts ├── tsconfig.json ├── types │ └── index.d.ts ├── webpack.config.js └── yarn.lock ├── cypress.json ├── cypress ├── fixtures │ └── example.json ├── integration │ └── home.spec.ts ├── plugins │ └── index.js ├── support │ ├── commands.js │ └── index.js ├── tsconfig.json └── utils.ts ├── package.json ├── web ├── .dockerignore ├── .env.template ├── .eslintignore ├── .gitignore ├── Dockerfile ├── README.md ├── craco.config.js ├── docker-compose.yml ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ ├── robots.txt │ └── worker.js ├── src │ ├── assets │ │ └── images │ │ │ └── logo.png │ ├── config │ │ ├── index.ts │ │ └── test.selectors.ts │ ├── constants │ │ └── index.ts │ ├── global │ │ └── root.css │ ├── graphql │ │ └── index.ts │ ├── index.tsx │ ├── layout │ │ ├── Footer │ │ │ └── index.tsx │ │ └── index.ts │ ├── pages │ │ ├── Home │ │ │ ├── components │ │ │ │ └── Welcome │ │ │ │ │ └── index.tsx │ │ │ └── index.tsx │ │ └── index.ts │ ├── react-app-env.d.ts │ ├── serviceWorker.ts │ ├── tests │ │ └── App.test.tsx │ ├── theme │ │ ├── index.ts │ │ ├── palette.ts │ │ └── typography.ts │ └── utils │ │ └── index.ts ├── tsconfig.json ├── tsconfig.paths.json └── yarn.lock └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | **/dist/ 3 | **/coverage/ 4 | **/build/ 5 | **/.webpack/ 6 | *.json 7 | *.md 8 | *.yml 9 | web/src/graphql/operations.tsx 10 | backend/packages/graphql/schema.graphql 11 | backend/packages/graphql/types/schema.ts 12 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | ecmaVersion: 2020, 5 | sourceType: 'module', 6 | ecmaFeatures: { 7 | jsx: true 8 | } 9 | }, 10 | settings: { 11 | react: { 12 | version: '14.18' 13 | }, 14 | 'import/parsers': { 15 | '@typescript-eslint/parser': ['.ts', '.tsx'] 16 | }, 17 | 'import/resolver': { 18 | node: { 19 | extensions: ['.ts', '.tsx'] 20 | }, 21 | alias: { 22 | map: [ 23 | ['@backend/db', './backend/packages/db'], 24 | ['@backend/graphql', './backend/packages/graphql'], 25 | ['@backend/utils', './backend/packages/utils'], 26 | ['@backend/config', './backend/config/index.ts'], 27 | ['@web/assets', './web/src/assets'], 28 | ['@web/config', './web/src/config'], 29 | ['@web/constants', './web/src/constants'], 30 | ['@web/global', './web/src/global'], 31 | ['@web/layout', './web/src/layout'], 32 | ['@web/graphql', './web/src/graphql'], 33 | ['@web/pages', './web/src/pages'], 34 | ['@web/theme', './web/src/theme'], 35 | ['@web/utils', './web/src/utils'] 36 | ], 37 | extensions: ['.ts', '.tsx'] 38 | } 39 | } 40 | }, 41 | env: { 42 | es6: true, 43 | browser: true, 44 | node: true 45 | }, 46 | plugins: ['react', '@typescript-eslint', 'prettier'], 47 | extends: [ 48 | 'eslint:recommended', 49 | 'plugin:@typescript-eslint/recommended', 50 | 'plugin:react/recommended', 51 | 'plugin:import/errors', 52 | 'plugin:import/warnings', 53 | 'plugin:import/typescript' 54 | ], 55 | rules: { 56 | // JS/TS RULES 57 | quotes: ['error', 'single'], 58 | camelcase: 'error', 59 | 'prefer-const': 'error', 60 | 'no-var': 'error', 61 | 'no-dupe-else-if': 'off', 62 | 'no-setter-return': 'off', 63 | 'import/no-unresolved': 'off', 64 | 'import/newline-after-import': ['error', { count: 1 }], 65 | '@typescript-eslint/no-inferrable-types': 'off', 66 | '@typescript-eslint/semi': ['error', 'always'], 67 | '@typescript-eslint/indent': ['error', 2], 68 | '@typescript-eslint/brace-style': ['error', '1tbs', { allowSingleLine: true }], 69 | 'no-multiple-empty-lines': ['error', { max: 2, maxBOF: 0, maxEOF: 1 }], 70 | 'no-trailing-spaces': 'error', 71 | '@typescript-eslint/type-annotation-spacing': ['error'], 72 | 'object-curly-spacing': ['error', 'always'], 73 | 'key-spacing': ['error', { beforeColon: false }], 74 | 'object-shorthand': ['error', 'always'], 75 | '@typescript-eslint/no-explicit-any': 'off', 76 | '@typescript-eslint/ban-types': 'off', 77 | '@typescript-eslint/no-var-requires': 'off', 78 | // JSX RULES 79 | 'jsx-quotes': ['error', 'prefer-single'], 80 | 'react/jsx-boolean-value': 'error', 81 | 'react/jsx-closing-bracket-location': 'error', 82 | 'react/jsx-equals-spacing': 'error', 83 | 'react/jsx-indent-props': ['error', 2], 84 | 'react/jsx-indent': ['error', 2], 85 | 'react/jsx-max-props-per-line': ['error', { maximum: 4 }], 86 | 'react/jsx-no-bind': 'error', 87 | 'react/jsx-no-literals': 'off', 88 | 'react/jsx-tag-spacing': ['error', { beforeSelfClosing: 'always' }] 89 | } 90 | }; 91 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - develop 8 | pull_request: 9 | branches: 10 | - master 11 | - develop 12 | 13 | env: 14 | DATABASE_URL: postgres://user:password@127.0.0.1:5432/database 15 | POSTGRES_USER: user 16 | POSTGRES_PASSWORD: password 17 | POSTGRES_DB: database 18 | POSTGRES_HOST: 127.0.0.1 19 | 20 | jobs: 21 | web: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - name: Checkout code 25 | uses: actions/checkout@v2 26 | 27 | - name: Setup node 28 | uses: actions/setup-node@v1 29 | with: 30 | node-version: 14.18.0 31 | 32 | - name: Install 33 | run: yarn install --frozen-lockfile 34 | 35 | - name: Lint 36 | run: yarn lint 37 | 38 | - name: Test 39 | run: cd web && yarn test 40 | 41 | - name: Build 42 | run: cd web && yarn build 43 | 44 | backend: 45 | runs-on: ubuntu-latest 46 | services: 47 | postgres: 48 | image: postgres:12 49 | env: 50 | POSTGRES_USER: user 51 | POSTGRES_PASSWORD: password 52 | POSTGRES_DB: database 53 | ports: 54 | - 5432:5432 55 | options: >- 56 | --health-cmd pg_isready 57 | --health-interval 10s 58 | --health-timeout 5s 59 | --health-retries 5 60 | steps: 61 | - name: Checkout code 62 | uses: actions/checkout@v2 63 | 64 | - name: Setup node 65 | uses: actions/setup-node@v1 66 | with: 67 | node-version: 14.18.0 68 | 69 | - name: Install 70 | run: yarn install --frozen-lockfile 71 | 72 | - name: Lint 73 | run: yarn lint 74 | 75 | - name: Test 76 | run: cd backend && yarn migrate && yarn test 77 | 78 | - name: Build 79 | run: cd backend && yarn build 80 | 81 | backend-go: 82 | runs-on: ubuntu-latest 83 | defaults: 84 | run: 85 | working-directory: backend-go 86 | services: 87 | postgres: 88 | image: postgres:12 89 | env: 90 | POSTGRES_USER: user 91 | POSTGRES_PASSWORD: password 92 | POSTGRES_DB: database 93 | ports: 94 | - 5432:5432 95 | options: >- 96 | --health-cmd pg_isready 97 | --health-interval 10s 98 | --health-timeout 5s 99 | --health-retries 5 100 | steps: 101 | - name: Checkout code 102 | uses: actions/checkout@v2 103 | 104 | - uses: actions/setup-go@v2 105 | with: 106 | go-version: 1.17.2 107 | 108 | - name: Setup DB 109 | run: psql -h ${{ env.POSTGRES_HOST }} -U ${{ env.POSTGRES_USER }} -d ${{ env.POSTGRES_DB }} -a -f scripts/db/dump.sql 110 | env: 111 | PGPASSWORD: ${{ env.POSTGRES_PASSWORD }} 112 | 113 | - name: Install 114 | run: make install && make generate 115 | 116 | - name: Test 117 | run: make test 118 | 119 | - name: Build 120 | run: make build 121 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | ### VisualStudioCode ### 107 | .vscode/* 108 | 109 | ### Builds ### 110 | build 111 | 112 | # External tools 113 | .husky 114 | 115 | # Codegen 116 | web/src/graphql/operations.tsx 117 | backend/packages/graphql/schema.graphql 118 | backend/packages/graphql/types/schema.ts 119 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact = true 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | **/dist/ 3 | **/coverage/ 4 | **/build/ 5 | **/.webpack/ 6 | *.json 7 | *.md 8 | *.yml -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | printWidth: 120, 4 | tabWidth: 2, 5 | trailingComma: 'none', 6 | jsxBracketSameLine: true, 7 | jsxSingleQuote: true, 8 | arrowParens: 'avoid' 9 | }; 10 | -------------------------------------------------------------------------------- /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 | nationality, personal appearance, race, religion, or sexual identity and 10 | 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 the project team at contact@karanpratapsingh.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://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: https://contributor-covenant.org 74 | [version]: https://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Karan Pratap Singh 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![fullstack-starterkit](https://user-images.githubusercontent.com/29705703/86912939-7b0e7e00-c13b-11ea-950b-200a4529ae6b.png) 2 | 3 |

4 | 5 | 6 | PRs welcome! 7 | 8 | Twitter: karan_6864 9 | 10 | ### Motivation :star: 11 | 12 | Setting up boiler plates when starting new projects is tedious sometimes and I often found myself setting it up from scratch 🥱 13 | 14 | Hence I made this starterkit following some of the best patterns and practices I learnt from some of the larger codebase and fantastic developers I've had a chance to work with 🙌 15 | 16 | The main purpose of this repository is to provide a scalable "batteries included" full stack starterkit which follows good architecture patterns (might be opinionated) and code decoupling which becomes significant as the project grows or new developers are onboarded 17 | 18 | #### Features 19 | 20 | - **All in Typescript** 21 | Because TypeScript is awesome, and types are important 😃 22 | 23 | - **GraphQL First** 24 | This starterkit is built with graphql first approach using the [Apollo](https://www.apollographql.com/) platform 25 | 26 | - **Includes CI** 27 | CI is integral part of any project. This starterkit includes `Github Actions` by default. PR's for integration with any other providers are welcome 🙌 28 | 29 | - **Docker Support** 30 | You can also use docker to develop and run your applications 31 | 32 | - **Testing Focused** 33 | This project uses [Jest](https://jestjs.io/) for testing framework and comes with sample tests which are easy to extend 34 | 35 | - **Prisma** 36 | [Prisma](https://www.prisma.io/) is the ORM being used for [PostgreSQL](https://www.postgresql.org/). Feel free to submit a PR for any other ORM or drivers you'd like to see here 😁 37 | 38 | - **PWA Support** 39 | This starterkit comes with out of the box PWA support 40 | 41 | 42 | **Please leave a :star: as motivation if you liked the idea :smile:** 43 | 44 | ### :rocket: Technologies Used 45 | 46 | technologies 47 | 48 | ### 📖 Contents 49 | - [Architecture](#architecture) 50 | - [Backend](#backend) 51 | - [Web](#web) 52 | - [Getting started](#getting-started) 53 | - [How to Contribute](#how-to-contribute) 54 | - [License](#license) 55 | 56 | ### Video Overview 57 | 58 | Here's a brief video overview of this project, hope it helps. 59 | 60 | [![Full Stack Starterkit Overview](http://img.youtube.com/vi/TgtUhEnW8O4/0.jpg)](http://www.youtube.com/watch?v=TgtUhEnW8O4 "Full Stack Starterkit Overview") 61 | 62 | Big thanks to [@mikestaub](https://twitter.com/mikestaub) for mentoring me on the lot of the ideas you will come across in this repository. Checkout how he's changing social media with [Peapods](https://peapods.com) 63 | 64 | ### 🏭 Architecture 65 | 66 | #### Backend 67 | 68 | Here is the folder structure for `backend`, it is using `yarn workspaces` which helps us split our monorepo into packages such as DB, GraphQL. Which if required can be made into their own micro services. 69 | 70 | ``` 71 | backend 72 | ├── build 73 | ├── config 74 | ├── logs 75 | ├── packages 76 | │ ├── db 77 | │ │ └──prisma 78 | │ ├── graphql 79 | │ │ ├── api 80 | │ │ ├── schema 81 | │ │ └── types 82 | │ └── utils 83 | ├── tests 84 | │ ├── db 85 | │ └── graphql 86 | ├── index.ts 87 | └── package.json 88 | ``` 89 | 90 | ##### DB 91 | 92 | This workspace package contains the database abstractions. The database stack is [PostgreSQL](https://www.postgresql.org/) as relational database and [Prisma](https://www.prisma.io/) as an ORM, read more about DB package [here](./backend/packages/db/README.md) 93 | 94 | ##### GraphQL 95 | 96 | The GraphQL package is organized as below: 97 | ``` 98 | graphql 99 | ├── schema 100 | │ └── user <---- some entity 101 | │ ├── resolvers 102 | │ │ ├── types <---- type resolvers 103 | │ │ ├── queries <---- query resolvers 104 | │ │ └── mutations <---- mutation resolvers 105 | │ ├── queries.graphql 106 | │ ├── mutations.graphql 107 | │ └── types.graphql 108 | ├── api 109 | │ ├── queries 110 | │ └── mutations 111 | ├── types <---- graphql types 112 | │ ├── schema 113 | │ └── resolvers 114 | └── index.json 115 | ``` 116 | 117 | The schema splits each entity into it's own set of schema to modularize the codebase. The graphql package uses [schema stitching](https://www.apollographql.com/docs/apollo-server/features/schema-stitching) and [code generators](https://graphql-code-generator.com/) to construct the whole schema. 118 | 119 | It is organized so because if you choose to split graphql into it's own set of microservices later, it should be relatively easier to do so as this should be easy to integrate with [Apollo Federation](https://www.apollographql.com/docs/apollo-server/federation/introduction) 120 | 121 | Read more about GraphQL package [here](./backend/packages/graphql/README.md) 122 | 123 | #### Web 124 | Here is the folder structure for `web`, it is a standard [create-react-app](https://create-react-app.dev/) using [craco](https://github.com/gsoft-inc/craco) to override configs without ejecting 125 | 126 | Web package uses [Material UI](https://material-ui.com/) heavily as it makes theming and customization very easy. PR's for any other UI kit are welcome 😃 127 | 128 | ``` 129 | web 130 | ├── build 131 | ├── public 132 | ├── src 133 | │ ├── assets 134 | │ ├── config 135 | │ ├── constants 136 | │ ├── global 137 | │ ├── tests 138 | │ ├── layout <---- controls, pure components 139 | │ ├── theme <---- theme config 140 | │ ├── graphql 141 | │ │ └── operations.tsx <---- generated graphql operations and types 142 | │ ├── pages 143 | │ │ └── Home <---- page component 144 | │ │ ├── components <---- page specific components 145 | │ │ └── hooks <---- page specific custom hooks 146 | │ └── utils 147 | ├── tests 148 | │ ├── db 149 | │ └── graphql 150 | ├── index.ts 151 | └── package.json 152 | ``` 153 | 154 | 155 | ### 🏃 Getting Started 156 | 157 | **Setting up environment variables** 158 | 159 | Before getting started, create `.env` files at both `backend/.env` as well as `web/.env` following the `.env.template` files located in those directories. 160 | 161 | **Install dependencies** 162 | 163 | I recommend using `yarn` instead of `npm` as this project heavily uses `yarn workspaces` 164 | 165 | Install [volta](https://docs.volta.sh/guide/getting-started), which should automatically install correct `node` and `yarn` version when you checkout the repository (check the root package.json for config) 166 | 167 | ``` 168 | yarn 169 | ``` 170 | 171 | To install dependencies for `web` and `backend` automatically, a postinstall script has been added in the main `package.json` 172 | 173 | **Running web** 174 | 175 | - Docker (recommended) 176 | 177 | ``` 178 | $ cd web 179 | $ yarn dev 180 | ``` 181 | 182 | Once you're done working, use `yarn dev:down` command to stop the docker containers. 183 | 184 | - Locally 185 | 186 | ``` 187 | $ cd web 188 | $ yarn start:web 189 | ``` 190 | 191 | **Running backend** 192 | 193 | - Docker (recommended) 194 | 195 | ``` 196 | $ cd backend 197 | $ yarn dev 198 | ``` 199 | 200 | Once the container starts, you'll be inside the backend image. Now, simply migrate the db (only first time) and start the development server. 201 | 202 | ``` 203 | $ yarn migrate 204 | $ yarn start 205 | ``` 206 | 207 | Once you're done working, exit out from the container and use `yarn dev:down` command to stop the docker containers. 208 | 209 | - Locally 210 | 211 | ``` 212 | $ cd backend 213 | $ yarn start 214 | ``` 215 | 216 | _Note: When running locally, you'll be required to run your own instance of postgres._ 217 | 218 | **Running backend-go** 219 | 220 | If you don't have [`make`](https://en.wikipedia.org/wiki/Make_(software)) installed, commands are available in `Makefile`. 221 | 222 | ``` 223 | $ cd backend-go 224 | $ make dev 225 | ``` 226 | 227 | Now from inside the container, you can run the tests or application like below: 228 | 229 | ``` 230 | $ make test 231 | $ make run 232 | ``` 233 | 234 | 235 | Feel free to open a new issue if you're facing any problem 🙋 236 | 237 | 238 | **Codegen** 239 | 240 | This starterkit uses [graphql-code-generator](https://github.com/dotansimha/graphql-code-generator) to codegen lot of things like TypeScript types, React Apollo hooks and queries, GraphQL Schema AST etc. 241 | 242 | ``` 243 | cd backend 244 | yarn generate:graphql 245 | ``` 246 | 247 | 248 | Codegen is also executed in yarn postinstall hook 249 | 250 | 251 | 252 | ### 👏 How to Contribute 253 | 254 | Contributions are welcome as always, before submitting a new PR please make sure to open a new 255 | issue so community members can discuss. 256 | 257 | Additionally you might find existing open issues which can helps with improvements. 258 | 259 | This project follows standard [code of conduct](/CODE_OF_CONDUCT.md) so that you can understand what actions will and will not be tolerated. 260 | 261 | ### 📄 License 262 | 263 | This project is MIT licensed, as found in the [LICENSE](/LICENSE) 264 | 265 |

266 |

267 | Built and maintained with 🌮 by Karan 268 |

269 |

270 | 💼 Hire Me | 271 | 🍺 Donate 272 |

273 |

274 | -------------------------------------------------------------------------------- /backend-go/.air.toml: -------------------------------------------------------------------------------- 1 | root = "." 2 | tmp_dir = "tmp" 3 | 4 | [build] 5 | bin = "./tmp/main" 6 | cmd = "go build -o ./tmp/main ./cmd/main.go" 7 | delay = 1000 8 | exclude_dir = ["assets", "tmp", "vendor"] 9 | exclude_file = [] 10 | exclude_regex = [] 11 | exclude_unchanged = false 12 | follow_symlink = false 13 | full_bin = "" 14 | include_dir = [] 15 | include_ext = ["go", "tpl", "tmpl", "html"] 16 | kill_delay = "0s" 17 | log = "build-errors.log" 18 | send_interrupt = false 19 | stop_on_error = true 20 | 21 | [color] 22 | app = "" 23 | build = "yellow" 24 | main = "magenta" 25 | runner = "green" 26 | watcher = "cyan" 27 | 28 | [log] 29 | time = false 30 | 31 | [misc] 32 | clean_on_exit = false 33 | -------------------------------------------------------------------------------- /backend-go/.dockerignore: -------------------------------------------------------------------------------- 1 | tmp 2 | -------------------------------------------------------------------------------- /backend-go/.env.template: -------------------------------------------------------------------------------- 1 | DATABASE_URL=postgres://user:password@db:5432/database 2 | -------------------------------------------------------------------------------- /backend-go/.gitignore: -------------------------------------------------------------------------------- 1 | tmp 2 | -------------------------------------------------------------------------------- /backend-go/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM postgres:12 as db 2 | WORKDIR /app 3 | COPY ./scripts/db/init.sh /docker-entrypoint-initdb.d 4 | COPY ./scripts/db/dump.sql ./scripts/db/dump.sql 5 | 6 | # Ref: https://dev.to/karanpratapsingh/dockerize-your-go-app-46pp 7 | FROM golang:1.17.2 as app 8 | WORKDIR /app 9 | COPY . . 10 | RUN curl -sSfL https://raw.githubusercontent.com/cosmtrek/air/master/install.sh | sh -s -- -b $(go env GOPATH)/bin 11 | RUN make install 12 | RUN make generate 13 | CMD make run 14 | -------------------------------------------------------------------------------- /backend-go/Makefile: -------------------------------------------------------------------------------- 1 | install: 2 | go install ./cmd 3 | go mod tidy 4 | 5 | generate: 6 | go run github.com/99designs/gqlgen generate 7 | 8 | run: 9 | air 10 | 11 | dev: 12 | docker compose up -d 13 | docker exec -it backend /bin/bash 14 | 15 | dev-down: 16 | docker compose down 17 | 18 | test: 19 | go test ./db ./graphql -count=1 20 | 21 | build: 22 | go build cmd/main.go 23 | -------------------------------------------------------------------------------- /backend-go/cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "backend/db" 5 | "backend/graphql" 6 | "fmt" 7 | "net/http" 8 | 9 | _ "github.com/99designs/gqlgen/cmd" 10 | "github.com/gorilla/mux" 11 | ) 12 | 13 | var port = ":4000" 14 | 15 | func main() { 16 | router := mux.NewRouter() 17 | 18 | postgres := db.NewPostgres() 19 | defer postgres.GetPool().Close() 20 | 21 | graphql.Run(router, postgres) 22 | 23 | fmt.Println("Server is running") 24 | http.ListenAndServe(port, router) 25 | } 26 | -------------------------------------------------------------------------------- /backend-go/db/interface.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "backend/graphql/generated" 5 | "context" 6 | ) 7 | 8 | type DB interface { 9 | GetUser(ctx context.Context, id string) (*generated.User, error) 10 | GetPost(ctx context.Context, id string) (*generated.Post, error) 11 | GetPosts(ctx context.Context, userID string) ([]*generated.Post, error) 12 | } 13 | -------------------------------------------------------------------------------- /backend-go/db/postgres.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "backend/graphql/generated" 5 | "context" 6 | "fmt" 7 | "os" 8 | 9 | "github.com/jackc/pgx/v4/pgxpool" 10 | ) 11 | 12 | type Postgres struct { 13 | pool *pgxpool.Pool 14 | } 15 | 16 | func NewPostgres() Postgres { 17 | dbURL := os.Getenv("DATABASE_URL") 18 | pool, err := pgxpool.Connect(context.Background(), dbURL) 19 | 20 | if err != nil { 21 | fmt.Fprintf(os.Stderr, "Unable to connect to database: %v\n", err) 22 | os.Exit(1) 23 | } 24 | 25 | return Postgres{pool} 26 | } 27 | 28 | func (db Postgres) GetPool() *pgxpool.Pool { 29 | return db.pool 30 | } 31 | 32 | func (db Postgres) GetUser(ctx context.Context, id string) (*generated.User, error) { 33 | var user generated.User 34 | 35 | query := "SELECT id, name, email FROM users WHERE id=$1" 36 | err := db.pool.QueryRow(ctx, query, &id).Scan(&user.ID, &user.Name, &user.Email) 37 | 38 | if err != nil { 39 | return nil, fmt.Errorf(`no user found for id: %s %s`, id, err.Error()) 40 | } 41 | 42 | return &user, nil 43 | } 44 | 45 | func (db Postgres) GetPost(ctx context.Context, id string) (*generated.Post, error) { 46 | var post generated.Post 47 | var author generated.User 48 | 49 | query := "SELECT id, title, content, published, author_id FROM posts WHERE id=$1" 50 | err := db.pool.QueryRow(ctx, query, &id).Scan(&post.ID, &post.Title, &post.Content, &post.Published, &author.ID) 51 | 52 | if err != nil { 53 | return nil, fmt.Errorf(`no post found for id: %s %s`, id, err.Error()) 54 | } 55 | 56 | post.Author = &author 57 | 58 | return &post, nil 59 | } 60 | 61 | func (db Postgres) GetPosts(ctx context.Context, userID string) ([]*generated.Post, error) { 62 | var posts []*generated.Post 63 | 64 | query := "SELECT id, title, content, published, author_id FROM posts WHERE author_id=$1" 65 | rows, err := db.pool.Query(ctx, query, &userID) 66 | 67 | if err != nil { 68 | return nil, fmt.Errorf(`no posts found for user: %s %s`, userID, err.Error()) 69 | } 70 | 71 | for rows.Next() { 72 | var post generated.Post 73 | var author generated.User 74 | 75 | err := rows.Scan(&post.ID, &post.Title, &post.Content, &post.Published, &author.ID) 76 | 77 | if err != nil { 78 | return nil, fmt.Errorf(`error retrieving posts for user: %s %s`, userID, err.Error()) 79 | } 80 | 81 | post.Author = &author 82 | posts = append(posts, &post) 83 | } 84 | 85 | return posts, nil 86 | } 87 | -------------------------------------------------------------------------------- /backend-go/db/postgres_test.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestDatabase(t *testing.T) { 11 | db := NewPostgres() 12 | defer db.GetPool().Close() 13 | 14 | t.Run("GetUser: Should retrieve a user", func(t *testing.T) { 15 | ctx := context.Background() 16 | 17 | ID := "userid" 18 | user, err := db.GetUser(ctx, ID) 19 | 20 | assert.Equal(t, user.ID, ID, "User ID should be equal to expected ID") 21 | assert.Equal(t, err, nil, "Error should be nil") 22 | }) 23 | 24 | t.Run("GetUser: Should not retrieve any user", func(t *testing.T) { 25 | ctx := context.Background() 26 | 27 | ID := "unknown-id" 28 | _, err := db.GetUser(ctx, ID) 29 | 30 | assert.NotEqual(t, err, nil, "Error should not be nil") 31 | }) 32 | 33 | t.Run("GetPost: Should retrieve a post", func(t *testing.T) { 34 | ctx := context.Background() 35 | 36 | ID := "postid" 37 | post, err := db.GetPost(ctx, ID) 38 | 39 | assert.Equal(t, post.ID, ID, "Post ID should be equal to expected ID") 40 | assert.Equal(t, err, nil, "Error should be nil") 41 | }) 42 | 43 | t.Run("GetPost: Should not retrieve any post", func(t *testing.T) { 44 | ctx := context.Background() 45 | 46 | ID := "unknown-id" 47 | _, err := db.GetPost(ctx, ID) 48 | 49 | assert.NotEqual(t, err, nil, "Error should not be nil") 50 | }) 51 | 52 | t.Run("GetPosts: Should retrieve posts for an user", func(t *testing.T) { 53 | ctx := context.Background() 54 | 55 | userID := "userid" 56 | posts, err := db.GetPosts(ctx, userID) 57 | 58 | expected := 1 59 | 60 | assert.GreaterOrEqual(t, len(posts), expected, "Posts should contain atleast one element") 61 | assert.Equal(t, err, nil, "Error should be nil") 62 | }) 63 | 64 | t.Run("GetPosts: Should not retrieve posts for an unknown user", func(t *testing.T) { 65 | ctx := context.Background() 66 | 67 | ID := "unknown-id" 68 | posts, _ := db.GetPosts(ctx, ID) 69 | 70 | expected := 0 71 | 72 | assert.Equal(t, len(posts), expected, "Posts should contain no element") 73 | }) 74 | } 75 | -------------------------------------------------------------------------------- /backend-go/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | 3 | services: 4 | backend: 5 | container_name: backend 6 | image: backend-go 7 | restart: always 8 | build: 9 | context: . 10 | target: app 11 | volumes: 12 | - .:/app 13 | env_file: 14 | - .env 15 | ports: 16 | - 4000:4000 17 | depends_on: 18 | - db 19 | 20 | db: 21 | image: db 22 | container_name: db 23 | build: 24 | context: . 25 | target: db 26 | environment: 27 | - POSTGRES_USER=user 28 | - POSTGRES_PASSWORD=password 29 | - POSTGRES_DB=database 30 | ports: 31 | - 5432:5432 32 | volumes: 33 | - postgres:/var/lib/postgresql/data 34 | 35 | volumes: 36 | postgres: 37 | -------------------------------------------------------------------------------- /backend-go/go.mod: -------------------------------------------------------------------------------- 1 | module backend 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/99designs/gqlgen v0.14.0 7 | github.com/gorilla/mux v1.8.0 8 | github.com/jackc/pgx/v4 v4.13.0 9 | github.com/stretchr/testify v1.7.0 10 | github.com/vektah/gqlparser/v2 v2.2.0 11 | ) 12 | 13 | require ( 14 | github.com/agnivade/levenshtein v1.1.1 // indirect 15 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d // indirect 16 | github.com/davecgh/go-spew v1.1.1 // indirect 17 | github.com/gorilla/websocket v1.4.2 // indirect 18 | github.com/hashicorp/golang-lru v0.5.4 // indirect 19 | github.com/jackc/chunkreader/v2 v2.0.1 // indirect 20 | github.com/jackc/pgconn v1.10.0 // indirect 21 | github.com/jackc/pgio v1.0.0 // indirect 22 | github.com/jackc/pgpassfile v1.0.0 // indirect 23 | github.com/jackc/pgproto3/v2 v2.1.1 // indirect 24 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect 25 | github.com/jackc/pgtype v1.8.1 // indirect 26 | github.com/jackc/puddle v1.1.3 // indirect 27 | github.com/mitchellh/mapstructure v1.4.2 // indirect 28 | github.com/pkg/errors v0.9.1 // indirect 29 | github.com/pmezard/go-difflib v1.0.0 // indirect 30 | github.com/russross/blackfriday/v2 v2.0.1 // indirect 31 | github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect 32 | github.com/urfave/cli/v2 v2.1.1 // indirect 33 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect 34 | golang.org/x/mod v0.3.0 // indirect 35 | golang.org/x/text v0.3.7 // indirect 36 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a // indirect 37 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect 38 | gopkg.in/yaml.v2 v2.4.0 // indirect 39 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect 40 | ) 41 | -------------------------------------------------------------------------------- /backend-go/go.sum: -------------------------------------------------------------------------------- 1 | github.com/99designs/gqlgen v0.14.0 h1:Wg8aNYQUjMR/4v+W3xD+7SizOy6lSvVeQ06AobNQAXI= 2 | github.com/99designs/gqlgen v0.14.0/go.mod h1:S7z4boV+Nx4VvzMUpVrY/YuHjFX4n7rDyuTqvAkuoRE= 3 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 4 | github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= 5 | github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= 6 | github.com/agnivade/levenshtein v1.1.0/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo= 7 | github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8= 8 | github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo= 9 | github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= 10 | github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= 11 | github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= 12 | github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= 13 | github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= 14 | github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= 15 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 16 | github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 17 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= 18 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 19 | github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= 20 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 21 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 22 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 23 | github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g= 24 | github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= 25 | github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= 26 | github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= 27 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 28 | github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= 29 | github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= 30 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 31 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 32 | github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= 33 | github.com/gorilla/mux v1.6.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= 34 | github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= 35 | github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 36 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 37 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 38 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 39 | github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= 40 | github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= 41 | github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0= 42 | github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= 43 | github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= 44 | github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= 45 | github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= 46 | github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= 47 | github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= 48 | github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= 49 | github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= 50 | github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= 51 | github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= 52 | github.com/jackc/pgconn v1.10.0 h1:4EYhlDVEMsJ30nNj0mmgwIUXoq7e9sMJrVC2ED6QlCU= 53 | github.com/jackc/pgconn v1.10.0/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= 54 | github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= 55 | github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= 56 | github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= 57 | github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= 58 | github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc= 59 | github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= 60 | github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 61 | github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 62 | github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A= 63 | github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= 64 | github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= 65 | github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= 66 | github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= 67 | github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= 68 | github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 69 | github.com/jackc/pgproto3/v2 v2.1.1 h1:7PQ/4gLoqnl87ZxL7xjO0DR5gYuviDCZxQJsUlFW1eI= 70 | github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 71 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg= 72 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= 73 | github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= 74 | github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= 75 | github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= 76 | github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= 77 | github.com/jackc/pgtype v1.8.1 h1:9k0IXtdJXHJbyAWQgbWr1lU+MEhPXZz6RIXxfR5oxXs= 78 | github.com/jackc/pgtype v1.8.1/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= 79 | github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= 80 | github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= 81 | github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= 82 | github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= 83 | github.com/jackc/pgx/v4 v4.13.0 h1:JCjhT5vmhMAf/YwBHLvrBn4OGdIQBiFG6ym8Zmdx570= 84 | github.com/jackc/pgx/v4 v4.13.0/go.mod h1:9P4X524sErlaxj0XSGZk7s+LD0eOyu1ZDUrrpznYDF0= 85 | github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 86 | github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 87 | github.com/jackc/puddle v1.1.3 h1:JnPg/5Q9xVJGfjsO5CPUOjnJps1JaRUm8I9FXVCFK94= 88 | github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 89 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 90 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 91 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 92 | github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 93 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 94 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 95 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 96 | github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= 97 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 98 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 99 | github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 100 | github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 101 | github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 102 | github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8= 103 | github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 104 | github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= 105 | github.com/matryer/moq v0.0.0-20200106131100-75d0ddfc0007/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ= 106 | github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= 107 | github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 108 | github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 109 | github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 110 | github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 111 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 112 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 113 | github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 114 | github.com/mitchellh/mapstructure v1.4.2 h1:6h7AQ0yhTcIsmFmnAwQls75jp2Gzs4iB8W7pjMO+rqo= 115 | github.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 116 | github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= 117 | github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= 118 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 119 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 120 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 121 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 122 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 123 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 124 | github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= 125 | github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= 126 | github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= 127 | github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= 128 | github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= 129 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 130 | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 131 | github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= 132 | github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= 133 | github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= 134 | github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= 135 | github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= 136 | github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= 137 | github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= 138 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 139 | github.com/shurcooL/vfsgen v0.0.0-20180121065927-ffb13db8def0/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw= 140 | github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= 141 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 142 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 143 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 144 | github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= 145 | github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 146 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 147 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 148 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 149 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 150 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 151 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 152 | github.com/urfave/cli/v2 v2.1.1 h1:Qt8FeAtxE/vfdrLmR3rxR6JRE0RoVmbXu8+6kZtYU4k= 153 | github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= 154 | github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e/go.mod h1:/HUdMve7rvxZma+2ZELQeNh88+003LL7Pf/CZ089j8U= 155 | github.com/vektah/gqlparser/v2 v2.2.0 h1:bAc3slekAAJW6sZTi07aGq0OrfaCjj4jxARAaC7g2EM= 156 | github.com/vektah/gqlparser/v2 v2.2.0/go.mod h1:i3mQIGIrbK2PD1RrCeMTlVbkF2FJ6WkU1KJlJlC+3F4= 157 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 158 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 159 | github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= 160 | go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 161 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 162 | go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 163 | go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 164 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 165 | go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= 166 | go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= 167 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 168 | go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 169 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 170 | go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= 171 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 172 | golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= 173 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 174 | golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 175 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 176 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 177 | golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= 178 | golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 179 | golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 180 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg= 181 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 182 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 183 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 184 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 185 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 186 | golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= 187 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 188 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 189 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 190 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 191 | golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 192 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 193 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 194 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 195 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 196 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 197 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 198 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 199 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 200 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 201 | golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 202 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 203 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 204 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 205 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 206 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 207 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 208 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 209 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 210 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 211 | golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= 212 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 213 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 214 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 215 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 216 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 217 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 218 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= 219 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 220 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 221 | golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 222 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 223 | golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 224 | golang.org/x/tools v0.0.0-20190515012406-7d7faa4812bd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 225 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 226 | golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 227 | golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 228 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 229 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 230 | golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 231 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 232 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a h1:CB3a9Nez8M13wwlr/E2YtwoU+qYHKfC+JrDa45RXXoQ= 233 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 234 | golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 235 | golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 236 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 237 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 238 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 239 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 240 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 241 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 242 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 243 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 244 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 245 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 246 | gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= 247 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 248 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 249 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 250 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 251 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 252 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 253 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 254 | sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= 255 | sourcegraph.com/sourcegraph/appdash-data v0.0.0-20151005221446-73f23eafcf67/go.mod h1:L5q+DGLGOQFpo1snNEkLOJT2d1YTW66rWNzatr3He1k= 256 | -------------------------------------------------------------------------------- /backend-go/gqlgen.yml: -------------------------------------------------------------------------------- 1 | schema: 2 | - graphql/schema/*.graphql 3 | 4 | exec: 5 | filename: graphql/generated/types.go 6 | package: generated 7 | 8 | model: 9 | filename: graphql/generated/models.go 10 | package: generated 11 | 12 | resolver: 13 | layout: follow-schema 14 | dir: graphql/resolvers 15 | package: resolvers 16 | 17 | autobind: [] 18 | 19 | models: 20 | User: 21 | fields: 22 | posts: 23 | resolver: true 24 | Post: 25 | fields: 26 | author: 27 | resolver: true 28 | ID: 29 | model: 30 | - github.com/99designs/gqlgen/graphql.ID 31 | - github.com/99designs/gqlgen/graphql.Int 32 | - github.com/99designs/gqlgen/graphql.Int64 33 | - github.com/99designs/gqlgen/graphql.Int32 34 | Int: 35 | model: 36 | - github.com/99designs/gqlgen/graphql.Int 37 | - github.com/99designs/gqlgen/graphql.Int64 38 | - github.com/99designs/gqlgen/graphql.Int32 39 | -------------------------------------------------------------------------------- /backend-go/graphql/generated/models.go: -------------------------------------------------------------------------------- 1 | // Code generated by github.com/99designs/gqlgen, DO NOT EDIT. 2 | 3 | package generated 4 | 5 | type GetPostInput struct { 6 | ID string `json:"id"` 7 | } 8 | 9 | type GetPostResult struct { 10 | Post *Post `json:"post"` 11 | } 12 | 13 | type GetUserInput struct { 14 | ID string `json:"id"` 15 | } 16 | 17 | type GetUserResult struct { 18 | User *User `json:"user"` 19 | } 20 | 21 | type Post struct { 22 | ID string `json:"id"` 23 | Title string `json:"title"` 24 | Content *string `json:"content"` 25 | Published bool `json:"published"` 26 | Author *User `json:"author"` 27 | } 28 | 29 | type User struct { 30 | ID string `json:"id"` 31 | Name string `json:"name"` 32 | Email string `json:"email"` 33 | Posts []*Post `json:"posts"` 34 | } 35 | -------------------------------------------------------------------------------- /backend-go/graphql/graphql.go: -------------------------------------------------------------------------------- 1 | package graphql 2 | 3 | import ( 4 | "backend/db" 5 | "backend/graphql/generated" 6 | "backend/graphql/resolvers" 7 | "net/http" 8 | 9 | "github.com/99designs/gqlgen/graphql" 10 | "github.com/99designs/gqlgen/graphql/handler" 11 | "github.com/99designs/gqlgen/graphql/playground" 12 | "github.com/gorilla/mux" 13 | ) 14 | 15 | const endpoint = "/graphql" 16 | 17 | func Run(r *mux.Router, db db.DB) { 18 | graphql, playground := getHandlers(db) 19 | 20 | r.HandleFunc(endpoint, playground).Methods(http.MethodGet) 21 | r.Handle(endpoint, graphql).Methods(http.MethodPost) 22 | } 23 | 24 | func getHandlers(db db.DB) (*handler.Server, http.HandlerFunc) { 25 | schema := getSchema(db) 26 | 27 | graphql := handler.NewDefaultServer(schema) 28 | playground := playground.Handler("GraphQL Playground", endpoint) 29 | 30 | return graphql, playground 31 | } 32 | 33 | func getSchema(db db.DB) graphql.ExecutableSchema { 34 | config := generated.Config{ 35 | Resolvers: &resolvers.Resolver{DB: db}, 36 | } 37 | return generated.NewExecutableSchema(config) 38 | } 39 | -------------------------------------------------------------------------------- /backend-go/graphql/graphql_test.go: -------------------------------------------------------------------------------- 1 | package graphql 2 | 3 | import ( 4 | "backend/db" 5 | "backend/graphql/generated" 6 | "testing" 7 | 8 | "github.com/99designs/gqlgen/client" 9 | "github.com/99designs/gqlgen/graphql/handler" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestGraphQLResolvers(t *testing.T) { 14 | postgres := db.NewPostgres() 15 | defer postgres.GetPool().Close() 16 | 17 | schema := getSchema(postgres) 18 | c := client.New(handler.NewDefaultServer(schema)) 19 | 20 | t.Run("Query GetUser", func(t *testing.T) { 21 | var response struct { 22 | GetUser generated.GetUserResult 23 | } 24 | 25 | query := ` 26 | query GetUser($input: GetUserInput!) { 27 | getUser(input: $input) { 28 | user { 29 | id 30 | } 31 | } 32 | } 33 | ` 34 | 35 | expected := "userid" 36 | 37 | input := generated.GetPostInput{ID: expected} 38 | c.MustPost(query, &response, client.Var("input", input)) 39 | 40 | assert.Equal(t, response.GetUser.User.ID, expected, "User ID should be equal to expected ID") 41 | }) 42 | 43 | t.Run("Query GetPost", func(t *testing.T) { 44 | var response struct { 45 | GetPost generated.GetPostResult 46 | } 47 | 48 | query := ` 49 | query GetPost($input: GetPostInput!) { 50 | getPost(input: $input) { 51 | post { 52 | id 53 | } 54 | } 55 | } 56 | ` 57 | 58 | expected := "postid" 59 | 60 | input := generated.GetPostInput{ID: expected} 61 | c.MustPost(query, &response, client.Var("input", input)) 62 | 63 | assert.Equal(t, response.GetPost.Post.ID, expected, "Post ID should be equal to expected ID") 64 | }) 65 | } 66 | -------------------------------------------------------------------------------- /backend-go/graphql/resolvers/post.resolvers.go: -------------------------------------------------------------------------------- 1 | package resolvers 2 | 3 | // This file will be automatically regenerated based on the schema, any resolver implementations 4 | // will be copied through when generating and any unknown code will be moved to the end. 5 | 6 | import ( 7 | "backend/graphql/generated" 8 | "context" 9 | ) 10 | 11 | func (r *postResolver) Author(ctx context.Context, obj *generated.Post) (*generated.User, error) { 12 | user, err := r.DB.GetUser(ctx, obj.Author.ID) 13 | 14 | return user, err 15 | } 16 | 17 | func (r *queryResolver) GetPost(ctx context.Context, input generated.GetPostInput) (*generated.GetPostResult, error) { 18 | post, err := r.DB.GetPost(ctx, input.ID) 19 | 20 | result := &generated.GetPostResult{ 21 | Post: post, 22 | } 23 | 24 | return result, err 25 | } 26 | 27 | // Post returns generated.PostResolver implementation. 28 | func (r *Resolver) Post() generated.PostResolver { return &postResolver{r} } 29 | 30 | // Query returns generated.QueryResolver implementation. 31 | func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} } 32 | 33 | type postResolver struct{ *Resolver } 34 | type queryResolver struct{ *Resolver } 35 | -------------------------------------------------------------------------------- /backend-go/graphql/resolvers/resolver.go: -------------------------------------------------------------------------------- 1 | package resolvers 2 | 3 | import "backend/db" 4 | 5 | // This file will not be regenerated automatically. 6 | // 7 | // It serves as dependency injection for your app, add any dependencies you require here. 8 | 9 | type Resolver struct { 10 | DB db.DB 11 | } 12 | -------------------------------------------------------------------------------- /backend-go/graphql/resolvers/user.resolvers.go: -------------------------------------------------------------------------------- 1 | package resolvers 2 | 3 | // This file will be automatically regenerated based on the schema, any resolver implementations 4 | // will be copied through when generating and any unknown code will be moved to the end. 5 | 6 | import ( 7 | "backend/graphql/generated" 8 | "context" 9 | ) 10 | 11 | func (r *queryResolver) GetUser(ctx context.Context, input generated.GetUserInput) (*generated.GetUserResult, error) { 12 | user, err := r.DB.GetUser(ctx, input.ID) 13 | 14 | result := &generated.GetUserResult{ 15 | User: user, 16 | } 17 | 18 | return result, err 19 | } 20 | 21 | func (r *userResolver) Posts(ctx context.Context, obj *generated.User) ([]*generated.Post, error) { 22 | posts, err := r.DB.GetPosts(ctx, obj.ID) 23 | 24 | return posts, err 25 | } 26 | 27 | // User returns generated.UserResolver implementation. 28 | func (r *Resolver) User() generated.UserResolver { return &userResolver{r} } 29 | 30 | type userResolver struct{ *Resolver } 31 | -------------------------------------------------------------------------------- /backend-go/graphql/schema/post.graphql: -------------------------------------------------------------------------------- 1 | type Post { 2 | id: ID! 3 | title: String! 4 | content: String 5 | published: Boolean! 6 | author: User 7 | } 8 | 9 | extend type Query { 10 | getPost(input: GetPostInput!): GetPostResult! 11 | } 12 | 13 | input GetPostInput { 14 | id: ID! 15 | } 16 | 17 | type GetPostResult { 18 | post: Post 19 | } 20 | -------------------------------------------------------------------------------- /backend-go/graphql/schema/user.graphql: -------------------------------------------------------------------------------- 1 | type User { 2 | id: ID! 3 | name: String! 4 | email: String! 5 | posts: [Post!] 6 | } 7 | 8 | extend type Query { 9 | getUser(input: GetUserInput!): GetUserResult! 10 | } 11 | 12 | input GetUserInput { 13 | id: ID! 14 | } 15 | 16 | type GetUserResult { 17 | user: User 18 | } 19 | -------------------------------------------------------------------------------- /backend-go/scripts/db/dump.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE IF NOT EXISTS users ( 3 | "id" TEXT NOT NULL, 4 | "name" TEXT NOT NULL, 5 | "email" TEXT NOT NULL, 6 | 7 | PRIMARY KEY ("id") 8 | ); 9 | 10 | -- CreateTable 11 | CREATE TABLE IF NOT EXISTS posts ( 12 | "id" TEXT NOT NULL, 13 | "title" TEXT NOT NULL, 14 | "content" TEXT, 15 | "published" BOOLEAN NOT NULL DEFAULT false, 16 | "author_id" TEXT NOT NULL, 17 | 18 | PRIMARY KEY ("id") 19 | ); 20 | 21 | -- CreateIndex 22 | CREATE UNIQUE INDEX email_unique ON users("email"); 23 | 24 | -- AddForeignKey 25 | ALTER TABLE posts ADD FOREIGN KEY ("author_id") REFERENCES users("id") ON DELETE SET NULL ON UPDATE CASCADE; 26 | 27 | -- Seed 28 | INSERT INTO users (id, name, email) VALUES ('userid', 'Gopher', 'hello@gopher.com'); 29 | INSERT INTO posts (id, title, content, published, author_id) VALUES ('postid', 'Why go is awesome!', 'something something', TRUE, 'userid'); 30 | -------------------------------------------------------------------------------- /backend-go/scripts/db/init.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | psql -U $POSTGRES_USER -d $POSTGRES_DB -a -f /app/scripts/db/dump.sql 4 | -------------------------------------------------------------------------------- /backend/.dockerignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | -------------------------------------------------------------------------------- /backend/.env.template: -------------------------------------------------------------------------------- 1 | # Default environment variables for backend 2 | NODE_ENV=development | test | production 3 | DATABASE_URL=postgres://user:password@db:5432/database 4 | -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | # Ref: https://dev.to/karanpratapsingh/dockerize-node-application-222k 2 | FROM node:14.18-alpine 3 | WORKDIR /app 4 | COPY . . 5 | RUN yarn install 6 | EXPOSE 4000 7 | CMD tail -f /dev/null 8 | -------------------------------------------------------------------------------- /backend/config/index.ts: -------------------------------------------------------------------------------- 1 | type ConfigType = { 2 | logPath: string; 3 | defaultPort: number; 4 | }; 5 | 6 | const Config: ConfigType = { 7 | logPath: './logs', 8 | defaultPort: 4000 9 | }; 10 | 11 | export default Config; 12 | -------------------------------------------------------------------------------- /backend/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | 3 | services: 4 | backend: 5 | container_name: backend 6 | image: backend-node 7 | build: 8 | context: . 9 | volumes: 10 | - .:/app 11 | - dependencies:/app/node_modules 12 | env_file: 13 | - .env 14 | ports: 15 | - 4000:4000 16 | depends_on: 17 | - db 18 | 19 | db: 20 | image: postgres:12 21 | container_name: db 22 | environment: 23 | - POSTGRES_USER=user 24 | - POSTGRES_PASSWORD=password 25 | - POSTGRES_DB=database 26 | ports: 27 | - 5432:5432 28 | volumes: 29 | - postgres:/var/lib/postgresql/data 30 | 31 | volumes: 32 | dependencies: 33 | postgres: 34 | -------------------------------------------------------------------------------- /backend/index.ts: -------------------------------------------------------------------------------- 1 | import http from 'http'; 2 | import express, { Express } from 'express'; 3 | import cors from 'cors'; 4 | import { ApolloServer } from 'apollo-server-express'; 5 | import { GraphQLServerOptions } from '@backend/graphql'; 6 | import { logger } from '@backend/utils'; 7 | import Config from '@backend/config'; 8 | 9 | const app: Express = express(); 10 | app.use(cors()); 11 | 12 | const PORT: string | number = process.env.PORT || Config.defaultPort; 13 | const server: ApolloServer = new ApolloServer(GraphQLServerOptions); 14 | server.applyMiddleware({ app }); 15 | 16 | const httpServer: http.Server = http.createServer(app); 17 | server.installSubscriptionHandlers(httpServer); 18 | 19 | httpServer.listen(PORT, () => { 20 | const { graphqlPath, subscriptionsPath } = server; 21 | logger.info(`🚀 GraphQL Server is running at http://localhost:${PORT}${graphqlPath}`); 22 | logger.info(`🚀 Subscriptions ready at ws://localhost:${PORT}${subscriptionsPath}`); 23 | }); 24 | -------------------------------------------------------------------------------- /backend/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transform: { 3 | '\\.(gql|graphql)$': 'jest-transform-graphql' 4 | }, 5 | preset: 'ts-jest', 6 | testEnvironment: 'node', 7 | collectCoverage: true, 8 | testMatch: ['**/tests/**/?(*.)+(test).ts'], 9 | testPathIgnorePatterns: ['/node_modules/', '/build/'], 10 | moduleNameMapper: { 11 | '@backend/config': '/config', 12 | '@backend/(.*)$': '/packages/$1' 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /backend/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignore": ["node_modules/*", "tests/*"], 3 | "ext": "ts,tsx,js,jsx,json,gql" 4 | } 5 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "project-backend", 3 | "version": "0.1.0", 4 | "main": "index.ts", 5 | "author": { 6 | "name": "Karan Pratap Singh", 7 | "email": "contact@karanpratapsingh.com", 8 | "url": "https://karanpratapsingh.com" 9 | }, 10 | "private": true, 11 | "license": "MIT", 12 | "description": "Full Stack starterkit backend", 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/karanpratapsingh/fullstack-starterkit.git" 16 | }, 17 | "bugs": { 18 | "url": "https://github.com/karanpratapsingh/fullstack-starterkit/issues" 19 | }, 20 | "scripts": { 21 | "dev": "docker compose up -d && yarn dev:exec", 22 | "dev:exec": "docker exec -it backend /bin/sh", 23 | "dev:down": "docker compose down", 24 | "start": "yarn build --watch --hot", 25 | "test": "yarn jest --passWithNoTests --detectOpenHandles --verbose", 26 | "build": "webpack --color", 27 | "build:clean": "rm -rf build && yarn build", 28 | "generate:graphql": "yarn workspace @project-backend/graphql run generate", 29 | "generate:prisma": "yarn workspace @project-backend/db run generate", 30 | "migrate": "yarn workspace @project-backend/db run migrate", 31 | "postinstall": "yarn generate:graphql && yarn generate:prisma" 32 | }, 33 | "dependencies": { 34 | "@types/express": "4.17.12", 35 | "@types/lodash": "4.14.170", 36 | "@types/node": "15.9.0", 37 | "apollo-server-express": "2.25.0", 38 | "cors": "2.8.5", 39 | "dotenv": "10.0.0", 40 | "express": "4.17.1", 41 | "lodash": "4.17.21", 42 | "typescript": "4.3.2" 43 | }, 44 | "devDependencies": { 45 | "@types/jest": "^27.0.2", 46 | "chalk": "4.1.1", 47 | "dotenv-webpack": "7.0.3", 48 | "esbuild-loader": "2.13.1", 49 | "fork-ts-checker-webpack-plugin": "6.2.10", 50 | "jest": "27.0.4", 51 | "jest-transform-graphql": "2.1.0", 52 | "nodemon-webpack-plugin": "4.5.2", 53 | "ts-jest": "27.0.2", 54 | "webpack": "5.38.1", 55 | "webpack-cli": "4.7.0" 56 | }, 57 | "workspaces": { 58 | "nohoist": [], 59 | "packages": [ 60 | "packages/*" 61 | ] 62 | }, 63 | "volta": { 64 | "node": "14.18.0", 65 | "yarn": "1.22.10" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /backend/packages/db/README.md: -------------------------------------------------------------------------------- 1 | ## Database package 2 | 3 | This workspace package contains the database abstractions. The database stack is [PostgreSQL](https://www.postgresql.org/) as relational database and [Prisma](https://www.prisma.io/) as an ORM 4 | 5 | **Commands** 6 | 7 | Following npm scripts are available for convenience 8 | 9 | * [yarn introspect](https://www.prisma.io/docs/reference/tools-and-interfaces/introspection) 10 | 11 | * [yarn generate](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-cli) 12 | 13 | * [yarn migrate:save](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-migrate) 14 | 15 | * [yarn migrate:up](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-migrate) 16 | 17 | * [yarn studio](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-studio) 18 | 19 | **Note** 20 | On using migration commands `migrations` folder will be created, make sure to add it to your version control, prisma requires that folder to check if migrations matches or not. If the folder was accidentally removed make sure to drop the `_Migration` table in your postgres instance 21 | -------------------------------------------------------------------------------- /backend/packages/db/index.ts: -------------------------------------------------------------------------------- 1 | export * from './prisma'; 2 | -------------------------------------------------------------------------------- /backend/packages/db/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@project-backend/db", 3 | "version": "0.1.0", 4 | "main": "index.ts", 5 | "author": { 6 | "name": "Karan Pratap Singh", 7 | "email": "contact@karanpratapsingh.com", 8 | "url": "https://karanpratapsingh.com" 9 | }, 10 | "license": "MIT", 11 | "description": "Database package for full starterkit backend", 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/karanpratapsingh/fullstack-starterkit.git" 15 | }, 16 | "bugs": { 17 | "url": "https://github.com/karanpratapsingh/fullstack-starterkit/issues" 18 | }, 19 | "scripts": { 20 | "push": "yarn db push --schema=./prisma/schema.prisma", 21 | "generate": "yarn prisma generate --schema=./prisma/schema.prisma", 22 | "introspect": "yarn prisma introspect --schema=./prisma/schema.prisma", 23 | "studio": "yarn prisma studio --schema=./prisma/schema.prisma", 24 | "migrate": "yarn prisma migrate dev --schema=./prisma/schema.prisma" 25 | }, 26 | "dependencies": { 27 | "@prisma/client": "2.20.0", 28 | "prisma": "2.20.0" 29 | }, 30 | "volta": { 31 | "node": "14.18.0", 32 | "yarn": "1.22.10" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /backend/packages/db/prisma/index.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient, Prisma as PRISMA } from '@prisma/client'; 2 | 3 | class Prisma extends PrismaClient { 4 | private static instance: Prisma; 5 | 6 | private constructor() { 7 | super(); 8 | } 9 | 10 | static getInstance(): Prisma { 11 | if (!Prisma.instance) { 12 | Prisma.instance = new Prisma(); 13 | } 14 | 15 | return Prisma.instance; 16 | } 17 | } 18 | 19 | const prisma = Prisma.getInstance(); 20 | 21 | export { PRISMA, Prisma, prisma }; 22 | -------------------------------------------------------------------------------- /backend/packages/db/prisma/migrations/20211104122622_initial_migration/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "User" ( 3 | "id" TEXT NOT NULL, 4 | "email" TEXT NOT NULL, 5 | "name" TEXT NOT NULL, 6 | 7 | PRIMARY KEY ("id") 8 | ); 9 | 10 | -- CreateTable 11 | CREATE TABLE "Post" ( 12 | "id" TEXT NOT NULL, 13 | "authorId" TEXT, 14 | "content" TEXT, 15 | "published" BOOLEAN NOT NULL DEFAULT false, 16 | "title" TEXT NOT NULL, 17 | 18 | PRIMARY KEY ("id") 19 | ); 20 | 21 | -- CreateIndex 22 | CREATE UNIQUE INDEX "User.email_unique" ON "User"("email"); 23 | 24 | -- AddForeignKey 25 | ALTER TABLE "Post" ADD FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; 26 | -------------------------------------------------------------------------------- /backend/packages/db/prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /backend/packages/db/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | generator client { 2 | provider = "prisma-client-js" 3 | binaryTargets = ["native", "linux-musl"] 4 | } 5 | 6 | datasource db { 7 | provider = "postgresql" 8 | url = env("DATABASE_URL") 9 | } 10 | 11 | model User { 12 | id String @default(cuid()) @id 13 | email String @unique 14 | name String 15 | posts Post[] 16 | } 17 | 18 | model Post { 19 | id String @default(cuid()) @id 20 | authorId String? 21 | content String? 22 | published Boolean @default(false) 23 | title String 24 | author User? @relation(fields: [authorId], references: [id]) 25 | } 26 | -------------------------------------------------------------------------------- /backend/packages/graphql/README.md: -------------------------------------------------------------------------------- 1 | ## GraphQL package 2 | 3 | This workspace package contains the graphql abstractions and serves it using [Apollo](https://www.apollographql.com/docs/apollo-server/getting-started/) 4 | 5 | **Commands** 6 | 7 | Following npm scripts are available for convenience 8 | 9 | * `yarn generate` uses [graphql-codegen](https://graphql-code-generator.com/) to generate types and a stitched `schema.graphql` 10 | -------------------------------------------------------------------------------- /backend/packages/graphql/api/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * GraphQLApi 3 | * This helps with seamless access to GraphQL api in a non-graphql 4 | * context such as custom REST routes or testing environments 5 | */ 6 | import getUser from './queries/user/getUser'; 7 | import getPost from './queries/post/getPost'; 8 | 9 | const allQueries = { 10 | getUser, 11 | getPost 12 | }; 13 | 14 | import { Context } from '../types/resolvers'; 15 | import schema from '../schema'; 16 | import { graphql, print, DocumentNode } from 'graphql'; 17 | import { GetUserInput, GetUserResult, GetPostInput, GetPostResult } from '../types'; 18 | 19 | type GraphQLApiArgs = { 20 | context: Context; 21 | }; 22 | 23 | type GraphQLRequest = { 24 | query?: string; 25 | variables: any; 26 | context?: Context; 27 | operationName: string; 28 | }; 29 | 30 | class GraphQLApi { 31 | private context: Context; 32 | 33 | constructor({ context }: GraphQLApiArgs) { 34 | this.context = context; 35 | } 36 | 37 | async getUser(input: GetUserInput, context?: Context): Promise { 38 | const operationName = 'getUser'; 39 | const variables = { input }; 40 | return this.graphqlRequest({ 41 | operationName, 42 | variables, 43 | context 44 | }); 45 | } 46 | 47 | async getPost(input: GetPostInput, context?: Context): Promise { 48 | const operationName = 'getPost'; 49 | const variables = { input }; 50 | return this.graphqlRequest({ 51 | operationName, 52 | variables, 53 | context 54 | }); 55 | } 56 | 57 | private async graphqlRequest({ query, variables, context, operationName }: GraphQLRequest): Promise { 58 | const queryNode: DocumentNode = allQueries[operationName]; 59 | const queryNodeString: string = print(queryNode); 60 | const source: string = query || queryNodeString; 61 | 62 | const contextValue = (context = context ? { ...this.context, ...context } : this.context); 63 | const { data, errors } = await graphql({ schema, source, variableValues: variables, contextValue }); 64 | 65 | if (errors && errors.length) { 66 | throw errors[0]; 67 | } 68 | 69 | if (!data) { 70 | throw new Error(`Invalid query ${operationName}.`); 71 | } 72 | 73 | return data[operationName]; 74 | } 75 | } 76 | 77 | export { GraphQLApi as default, GraphQLApiArgs, allQueries }; 78 | -------------------------------------------------------------------------------- /backend/packages/graphql/api/queries/post/getPost.ts: -------------------------------------------------------------------------------- 1 | import { gql } from 'apollo-server-express'; 2 | 3 | const getPost = gql` 4 | query getPost($input: GetPostInput!) { 5 | getPost(input: $input) { 6 | post { 7 | id 8 | content 9 | published 10 | title 11 | author { 12 | id 13 | } 14 | } 15 | } 16 | } 17 | `; 18 | 19 | export default getPost; 20 | -------------------------------------------------------------------------------- /backend/packages/graphql/api/queries/user/getUser.ts: -------------------------------------------------------------------------------- 1 | import { gql } from 'apollo-server-express'; 2 | 3 | const getUser = gql` 4 | query getUser($input: GetUserInput!) { 5 | getUser(input: $input) { 6 | user { 7 | id 8 | email 9 | name 10 | posts { 11 | id 12 | } 13 | } 14 | } 15 | } 16 | `; 17 | 18 | export default getUser; 19 | -------------------------------------------------------------------------------- /backend/packages/graphql/codegen.yml: -------------------------------------------------------------------------------- 1 | overwrite: true 2 | schema: ["./schema/**/types.graphql", "./schema/**/queries.graphql", "./schema/**/mutations.graphql"] 3 | documents: ["./api/queries/**/**.ts", "./api/mutations/**/**.ts"] 4 | generates: 5 | ./schema.graphql: 6 | plugins: 7 | - schema-ast 8 | ./types/schema.ts: 9 | plugins: 10 | - typescript 11 | ../../../web/src/graphql/operations.tsx: 12 | plugins: 13 | - typescript 14 | - typescript-operations 15 | - typescript-react-apollo 16 | -------------------------------------------------------------------------------- /backend/packages/graphql/index.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from '@backend/db'; 2 | import { logger } from '@backend/utils'; 3 | import { ApolloServerExpressConfig, ExpressContext } from 'apollo-server-express'; 4 | import GraphQLApi, { GraphQLApiArgs } from './api'; 5 | import schema from './schema'; 6 | 7 | const GraphQLServerOptions: ApolloServerExpressConfig = { 8 | schema, 9 | context: (context: ExpressContext) => ({ 10 | ...context, 11 | prisma, 12 | logger 13 | }) 14 | }; 15 | 16 | export { GraphQLApi, GraphQLApiArgs, schema, GraphQLServerOptions }; 17 | -------------------------------------------------------------------------------- /backend/packages/graphql/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@project-backend/graphql", 3 | "version": "0.1.0", 4 | "main": "index.ts", 5 | "author": { 6 | "name": "Karan Pratap Singh", 7 | "email": "contact@karanpratapsingh.com", 8 | "url": "https://karanpratapsingh.com" 9 | }, 10 | "license": "MIT", 11 | "description": "GraphQL package for full starterkit backend", 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/karanpratapsingh/fullstack-starterkit.git" 15 | }, 16 | "bugs": { 17 | "url": "https://github.com/karanpratapsingh/fullstack-starterkit/issues" 18 | }, 19 | "scripts": { 20 | "generate": "yarn graphql-codegen --config codegen.yml" 21 | }, 22 | "dependencies": { 23 | "@graphql-tools/merge": "6.2.14", 24 | "@graphql-tools/schema": "7.1.5", 25 | "graphql": "15.5.0", 26 | "graphql-tag": "2.12.4" 27 | }, 28 | "devDependencies": { 29 | "@graphql-codegen/cli": "1.21.5", 30 | "@graphql-codegen/schema-ast": "1.18.3", 31 | "@graphql-codegen/typescript": "1.22.1", 32 | "@graphql-codegen/typescript-operations": "1.18.0", 33 | "@graphql-codegen/typescript-react-apollo": "2.2.5" 34 | }, 35 | "volta": { 36 | "node": "14.18.0", 37 | "yarn": "1.22.10" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /backend/packages/graphql/schema/index.ts: -------------------------------------------------------------------------------- 1 | import { DocumentNode, GraphQLSchema } from 'graphql'; 2 | import flatten from 'lodash/flatten'; 3 | 4 | import { mergeResolvers, mergeTypeDefs } from '@graphql-tools/merge'; 5 | import { makeExecutableSchema } from '@graphql-tools/schema'; 6 | 7 | import { userTypes, userQueries, userResolvers } from './user'; 8 | import { postTypes, postQueries, postResolvers } from './post'; 9 | 10 | const resolvers = mergeResolvers([userResolvers, postResolvers]); 11 | 12 | const types: DocumentNode[] = [userTypes, postTypes]; 13 | const queries: DocumentNode[] = [userQueries, postQueries]; 14 | const mutations: DocumentNode[] = []; 15 | 16 | const typeDefs = mergeTypeDefs(flatten([types, queries, mutations])); 17 | 18 | const schema: GraphQLSchema = makeExecutableSchema({ typeDefs, resolvers }); 19 | 20 | export default schema; 21 | -------------------------------------------------------------------------------- /backend/packages/graphql/schema/post/index.ts: -------------------------------------------------------------------------------- 1 | import postTypes from './types.graphql'; 2 | import postQueries from './queries.graphql'; 3 | import postResolvers from './resolvers'; 4 | 5 | export { postTypes, postQueries, postResolvers }; 6 | -------------------------------------------------------------------------------- /backend/packages/graphql/schema/post/queries.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | getPost(input: GetPostInput!): GetPostResult! 3 | } 4 | 5 | input GetPostInput { 6 | id: ID! 7 | } 8 | 9 | type GetPostResult { 10 | post: Post 11 | } 12 | -------------------------------------------------------------------------------- /backend/packages/graphql/schema/post/resolvers/index.ts: -------------------------------------------------------------------------------- 1 | import { Post } from './types'; 2 | import { getPost } from './queries'; 3 | 4 | const resolvers = { 5 | Query: { 6 | getPost 7 | }, 8 | Post 9 | }; 10 | 11 | export default resolvers; 12 | -------------------------------------------------------------------------------- /backend/packages/graphql/schema/post/resolvers/queries/getPost.ts: -------------------------------------------------------------------------------- 1 | import { Post } from '@prisma/client'; 2 | import { Parent, Context, QueryGetPostArgs, GetPostInput, GetPostResult } from '../../../../types'; 3 | 4 | async function getPost(_: Parent, args: QueryGetPostArgs, context: Context): Promise { 5 | const { prisma } = context; 6 | const { input } = args; 7 | const { id }: GetPostInput = input; 8 | 9 | const post: Post | null = await prisma.post.findUnique({ where: { id } }); 10 | 11 | return { post }; 12 | } 13 | 14 | export default getPost; 15 | -------------------------------------------------------------------------------- /backend/packages/graphql/schema/post/resolvers/queries/index.ts: -------------------------------------------------------------------------------- 1 | import getPost from './getPost'; 2 | 3 | export { getPost }; 4 | -------------------------------------------------------------------------------- /backend/packages/graphql/schema/post/resolvers/types/Post.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Post type resolvers 3 | */ 4 | import { User } from '@prisma/client'; 5 | import { Parent, Args, Context } from '../../../../types'; 6 | 7 | const Post = { 8 | author: async (parent: Parent, _: Args, context: Context): Promise => { 9 | const { id } = parent; 10 | const { prisma } = context; 11 | 12 | return prisma.post.findUnique({ where: { id } }).author(); 13 | } 14 | }; 15 | 16 | export default Post; 17 | -------------------------------------------------------------------------------- /backend/packages/graphql/schema/post/resolvers/types/index.ts: -------------------------------------------------------------------------------- 1 | import Post from './Post'; 2 | 3 | export { Post }; 4 | -------------------------------------------------------------------------------- /backend/packages/graphql/schema/post/types.graphql: -------------------------------------------------------------------------------- 1 | type Post { 2 | id: ID! 3 | title: String! 4 | content: String 5 | published: Boolean! 6 | author: User 7 | } 8 | -------------------------------------------------------------------------------- /backend/packages/graphql/schema/user/index.ts: -------------------------------------------------------------------------------- 1 | import userTypes from './types.graphql'; 2 | import userQueries from './queries.graphql'; 3 | import userResolvers from './resolvers'; 4 | 5 | export { userTypes, userQueries, userResolvers }; 6 | -------------------------------------------------------------------------------- /backend/packages/graphql/schema/user/queries.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | getUser(input: GetUserInput!): GetUserResult! 3 | } 4 | 5 | input GetUserInput { 6 | id: ID! 7 | } 8 | 9 | type GetUserResult { 10 | user: User 11 | } 12 | -------------------------------------------------------------------------------- /backend/packages/graphql/schema/user/resolvers/index.ts: -------------------------------------------------------------------------------- 1 | import { User } from './types'; 2 | import { getUser } from './queries'; 3 | 4 | const resolvers = { 5 | Query: { 6 | getUser 7 | }, 8 | User 9 | }; 10 | 11 | export default resolvers; 12 | -------------------------------------------------------------------------------- /backend/packages/graphql/schema/user/resolvers/queries/getUser.ts: -------------------------------------------------------------------------------- 1 | import { User } from '@prisma/client'; 2 | import { Parent, Context, QueryGetUserArgs, GetUserInput, GetUserResult } from '../../../../types'; 3 | 4 | async function getUser(_: Parent, args: QueryGetUserArgs, context: Context): Promise { 5 | const { prisma } = context; 6 | const { input } = args; 7 | const { id }: GetUserInput = input; 8 | 9 | const user: User | null = await prisma.user.findUnique({ where: { id } }); 10 | 11 | return { user }; 12 | } 13 | 14 | export default getUser; 15 | -------------------------------------------------------------------------------- /backend/packages/graphql/schema/user/resolvers/queries/index.ts: -------------------------------------------------------------------------------- 1 | import getUser from './getUser'; 2 | 3 | export { getUser }; 4 | -------------------------------------------------------------------------------- /backend/packages/graphql/schema/user/resolvers/types/User.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * User type resolvers 3 | */ 4 | import { Post } from '@prisma/client'; 5 | import { Parent, Args, Context } from '../../../../types'; 6 | 7 | const User = { 8 | posts: async (parent: Parent, _: Args, context: Context): Promise => { 9 | const { id } = parent; 10 | const { prisma } = context; 11 | 12 | return prisma.user.findUnique({ where: { id } }).posts(); 13 | } 14 | }; 15 | 16 | export default User; 17 | -------------------------------------------------------------------------------- /backend/packages/graphql/schema/user/resolvers/types/index.ts: -------------------------------------------------------------------------------- 1 | import User from './User'; 2 | 3 | export { User }; 4 | -------------------------------------------------------------------------------- /backend/packages/graphql/schema/user/types.graphql: -------------------------------------------------------------------------------- 1 | type User { 2 | id: ID! 3 | name: String! 4 | email: String! 5 | posts: [Post!] 6 | } 7 | -------------------------------------------------------------------------------- /backend/packages/graphql/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './schema'; 2 | export * from './resolvers'; 3 | -------------------------------------------------------------------------------- /backend/packages/graphql/types/resolvers.ts: -------------------------------------------------------------------------------- 1 | import { Prisma } from '@backend/db'; 2 | import { Logger } from '@backend/utils'; 3 | import { GraphQLResolveInfo } from 'graphql'; 4 | import { Scalars } from './schema'; 5 | 6 | export type Parent = { 7 | id: Scalars['ID']; 8 | }; 9 | 10 | export type Args = any; 11 | 12 | export type Context = { 13 | prisma: Prisma; 14 | logger: Logger; 15 | }; 16 | 17 | export type Info = GraphQLResolveInfo; 18 | -------------------------------------------------------------------------------- /backend/packages/utils/index.ts: -------------------------------------------------------------------------------- 1 | import cuid from 'cuid'; 2 | 3 | function generateId(prefix?: string): string { 4 | const generatedId = cuid(); 5 | return `${prefix}${generatedId}`; 6 | } 7 | 8 | export { generateId }; 9 | export * from './logger'; 10 | -------------------------------------------------------------------------------- /backend/packages/utils/logger.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Logger 3 | * 4 | * This utility helps with logging. 5 | * 6 | * Checkout transports available for winston: 7 | * https://github.com/winstonjs/winston/blob/master/docs/transports.md 8 | */ 9 | import winston, { Logger, Logform } from 'winston'; 10 | import { join } from 'path'; 11 | import Config from '@backend/config'; 12 | 13 | const { logPath } = Config; 14 | 15 | class LoggerUtils { 16 | private static instance: Logger; 17 | 18 | private getLogger(): Logger { 19 | const logger = winston.createLogger({ 20 | level: 'info', 21 | format: winston.format.combine( 22 | winston.format.errors({ stack: true }), 23 | winston.format.timestamp(), 24 | winston.format.colorize(), 25 | winston.format.json() 26 | ), 27 | transports: [ 28 | new winston.transports.File({ filename: join(logPath, 'combined.log') }), 29 | new winston.transports.File({ filename: join(logPath, 'error.log'), level: 'error' }) 30 | ] 31 | }); 32 | 33 | logger.add( 34 | new winston.transports.Console({ 35 | format: winston.format.combine( 36 | winston.format.errors({ stack: true }), 37 | winston.format.timestamp(), 38 | winston.format.colorize(), 39 | winston.format.printf(this.logTransform) 40 | ) 41 | }) 42 | ); 43 | 44 | return logger; 45 | } 46 | 47 | static getInstance(): Logger { 48 | if (!LoggerUtils.instance) { 49 | const loggerUtils = new LoggerUtils(); 50 | LoggerUtils.instance = loggerUtils.getLogger(); 51 | } 52 | 53 | return LoggerUtils.instance; 54 | } 55 | 56 | private logTransform = (info: Logform.TransformableInfo): string => { 57 | const { level, message } = info; 58 | return `${level} ${message}`; 59 | }; 60 | } 61 | 62 | const logger = LoggerUtils.getInstance(); 63 | 64 | export { Logger, logger }; 65 | -------------------------------------------------------------------------------- /backend/packages/utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@project-backend/utils", 3 | "version": "0.1.0", 4 | "main": "index.ts", 5 | "author": { 6 | "name": "Karan Pratap Singh", 7 | "email": "contact@karanpratapsingh.com", 8 | "url": "https://karanpratapsingh.com" 9 | }, 10 | "license": "MIT", 11 | "description": "Utils package for full starterkit backend", 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/karanpratapsingh/fullstack-starterkit.git" 15 | }, 16 | "bugs": { 17 | "url": "https://github.com/karanpratapsingh/fullstack-starterkit/issues" 18 | }, 19 | "dependencies": { 20 | "cuid": "2.1.8", 21 | "winston": "3.3.3" 22 | }, 23 | "volta": { 24 | "node": "14.18.0", 25 | "yarn": "1.22.10" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /backend/tests/db/db.test.ts: -------------------------------------------------------------------------------- 1 | import TestSuiteUtils, { TestSuite, TestSuiteType } from '../utils'; 2 | import { User, Post } from '@prisma/client'; 3 | import { Prisma } from '../../packages/db'; 4 | 5 | const utils: TestSuite = new TestSuiteUtils(TestSuiteType.DB); 6 | 7 | let prisma: Prisma; 8 | let createUserInput; 9 | let createPostInput; 10 | 11 | beforeAll(() => { 12 | prisma = utils.prisma; 13 | createUserInput = utils.createUserInput; 14 | createPostInput = utils.createPostInput; 15 | }); 16 | 17 | describe('DB Test Suite', () => { 18 | test('User should be created', async () => { 19 | const input: User = createUserInput(); 20 | const user = await prisma.user.create({ data: input }); 21 | expect(user.id).toBe(input.id); 22 | }); 23 | 24 | test('Post should be created', async () => { 25 | const input: Post = createPostInput(); 26 | const post = await prisma.post.create({ data: input }); 27 | expect(post.id).toBe(input.id); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /backend/tests/graphql/graphql.test.ts: -------------------------------------------------------------------------------- 1 | import TestSuiteUtils, { TestSuite, TestSuiteType } from '../utils'; 2 | import { GraphQLApi } from '../../packages/graphql'; 3 | 4 | const utils: TestSuite = new TestSuiteUtils(TestSuiteType.GRAPHQL); 5 | 6 | let graphQLApi: GraphQLApi; 7 | 8 | beforeAll(() => { 9 | graphQLApi = utils.graphQLApi; 10 | }); 11 | 12 | describe('GraphQL Test Suite', () => { 13 | describe('getUser query', () => { 14 | test('works with valid input', async () => { 15 | const id = 'demo_user_id'; 16 | const { user } = await graphQLApi.getUser({ id }); 17 | 18 | if (user) { 19 | expect(user.id).toBe(id); 20 | } else { 21 | expect(user).toBe(null); 22 | } 23 | }); 24 | }); 25 | 26 | describe('getPost query', () => { 27 | test('works with valid input', async () => { 28 | const id = 'demo_post_id'; 29 | const { post } = await graphQLApi.getPost({ id }); 30 | 31 | if (post) { 32 | expect(post.id).toBe(id); 33 | } else { 34 | expect(post).toBe(null); 35 | } 36 | }); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /backend/tests/utils/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * TestSuiteUtils 3 | * This test suite utils helps with setting up jest environment 4 | * for testing database and graphql api 5 | */ 6 | 7 | import { PRISMA, Prisma, prisma } from '../../packages/db'; 8 | import { GraphQLApi, GraphQLApiArgs } from '../../packages/graphql'; 9 | import { Post, User } from '../../packages/graphql/types'; 10 | import { generateId, Logger, logger } from '../../packages/utils'; 11 | 12 | enum TestSuiteType { 13 | DB, 14 | GRAPHQL 15 | } 16 | 17 | interface TestSuite { 18 | prisma: Prisma; 19 | graphQLApi: GraphQLApi; 20 | logger: Logger; 21 | createUserInput: (user?: User) => User; 22 | createPostInput: (post?: Post) => Post; 23 | } 24 | 25 | class TestSuiteUtils implements TestSuite { 26 | prisma: Prisma = prisma; 27 | graphQLApi!: GraphQLApi; 28 | logger: Logger = logger; 29 | 30 | private testIdPrefix: string = '_test_id'; 31 | 32 | constructor(type: TestSuiteType) { 33 | if (type === TestSuiteType.GRAPHQL) { 34 | const graphQLApiArgs: GraphQLApiArgs = { 35 | context: { 36 | prisma: this.prisma, 37 | logger: this.logger 38 | } 39 | }; 40 | this.graphQLApi = new GraphQLApi(graphQLApiArgs); 41 | } 42 | this.setupJest(); 43 | } 44 | 45 | private setupJest = (): void => { 46 | jest.setTimeout(20_000); 47 | global.afterAll(done => { 48 | this.afterAll(done); 49 | }); 50 | }; 51 | 52 | private afterAll = async (done: jest.DoneCallback) => { 53 | await this.cleanup(); 54 | await this.prisma.$disconnect(); 55 | done(); 56 | }; 57 | 58 | private cleanup = async (): Promise => { 59 | this.logger.info('Running DB Cleanup'); 60 | const input = { 61 | where: { 62 | id: { 63 | contains: this.testIdPrefix 64 | } 65 | } 66 | }; 67 | 68 | const users: PRISMA.BatchPayload = await this.prisma.user.deleteMany(input); 69 | const posts: PRISMA.BatchPayload = await this.prisma.post.deleteMany(input); 70 | 71 | this.logger.info(`Cleaned ${users.count} user(s)`); 72 | this.logger.info(`Cleaned ${posts.count} post(s)`); 73 | }; 74 | 75 | createUserInput = (user?: User): User => { 76 | const id = generateId(this.testIdPrefix); 77 | const uniqueEmail = generateId(); 78 | return { 79 | id, 80 | name: 'test user', 81 | email: `${uniqueEmail}@email.com`, 82 | ...user 83 | }; 84 | }; 85 | 86 | createPostInput = (post?: Post): Post => { 87 | const id = generateId(this.testIdPrefix); 88 | return { 89 | id, 90 | title: 'test title', 91 | published: true, 92 | ...post 93 | }; 94 | }; 95 | } 96 | 97 | export { TestSuiteUtils as default, TestSuite, TestSuiteType }; 98 | -------------------------------------------------------------------------------- /backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "esModuleInterop": true, 6 | "moduleResolution": "node", 7 | "rootDir": ".", 8 | "outDir": "build", 9 | "declaration": true, 10 | "sourceMap": true, 11 | "removeComments": true, 12 | "strict": true, 13 | "noImplicitAny": false, 14 | "strictNullChecks": true, 15 | "strictFunctionTypes": true, 16 | "noImplicitThis": true, 17 | "alwaysStrict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noImplicitReturns": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "baseUrl": ".", 23 | "paths": { 24 | "@backend/db": [ 25 | "packages/db" 26 | ], 27 | "@backend/graphql": [ 28 | "packages/graphql" 29 | ], 30 | "@backend/utils": [ 31 | "packages/utils" 32 | ], 33 | "@backend/config": [ 34 | "config" 35 | ] 36 | }, 37 | }, 38 | "include": [ 39 | "**/*", 40 | ], 41 | "exclude": [ 42 | "**/node_modules", 43 | "**/build", 44 | "**/tests" 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /backend/types/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.json' { 2 | const value: any; 3 | export default value; 4 | } 5 | 6 | declare module '*.graphql' { 7 | import { DocumentNode } from 'graphql'; 8 | 9 | const content: DocumentNode; 10 | export default content; 11 | } 12 | -------------------------------------------------------------------------------- /backend/webpack.config.js: -------------------------------------------------------------------------------- 1 | // External modules 2 | require('dotenv').config(); 3 | const path = require('path'); 4 | const chalk = require('chalk'); 5 | 6 | // Webpack plugin modules 7 | const DotEnvPlugin = require('dotenv-webpack'); 8 | const NodemonPlugin = require('nodemon-webpack-plugin'); 9 | const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); 10 | 11 | // Environment config 12 | const isDevelopment = process.env.NODE_ENV === 'development'; 13 | const mode = isDevelopment ? 'development' : 'production'; 14 | 15 | console.log(chalk.blue.bold(`Webpack bundling for ${mode} environment`)); 16 | 17 | // Bundle config options 18 | const BUNDLE = { 19 | entry: './index.ts', 20 | output: { 21 | filename: 'app.js', 22 | path: path.resolve(__dirname, 'build') 23 | } 24 | }; 25 | 26 | module.exports = { 27 | mode, 28 | target: 'node', 29 | entry: BUNDLE.entry, 30 | stats: 'errors-only', 31 | node: { 32 | __dirname: true, 33 | __filename: true 34 | }, 35 | module: getLoaders(), 36 | plugins: getPlugins(), 37 | resolve: { 38 | extensions: ['.tsx', '.ts', '.js', '.json'], 39 | alias: { 40 | '@backend/config': path.resolve(__dirname, 'config'), 41 | '@backend/graphql': path.resolve(__dirname, 'packages/graphql'), 42 | '@backend/utils': path.resolve(__dirname, 'packages/utils'), 43 | '@backend/db': path.resolve(__dirname, 'packages/db'), 44 | graphql$: path.resolve(__dirname, 'node_modules/graphql/index.js') 45 | } 46 | }, 47 | output: BUNDLE.output 48 | }; 49 | 50 | /** 51 | * Loaders used by the application. 52 | */ 53 | function getLoaders() { 54 | const graphql = { 55 | test: /\.(graphql|gql)$/, 56 | exclude: /node_modules/, 57 | loader: 'graphql-tag/loader' 58 | }; 59 | 60 | const esbuild = { 61 | test: /\.(js|jsx|ts|tsx)?$/, 62 | loader: 'esbuild-loader', 63 | options: { 64 | loader: 'tsx', 65 | target: 'es2015' 66 | }, 67 | exclude: /node_modules/ 68 | }; 69 | 70 | const loaders = { 71 | rules: [graphql, esbuild] 72 | }; 73 | 74 | return loaders; 75 | } 76 | 77 | /** 78 | * Plugins 79 | */ 80 | function getPlugins() { 81 | const dotEnv = new DotEnvPlugin(); 82 | const nodemon = new NodemonPlugin(); 83 | const typescriptChecker = new ForkTsCheckerWebpackPlugin({ 84 | async: true, 85 | typescript: { 86 | memoryLimit: 8192, 87 | diagnosticOptions: { 88 | semantic: true, 89 | syntactic: true 90 | } 91 | } 92 | }); 93 | // Order matters! 94 | return [dotEnv, typescriptChecker, nodemon]; 95 | } 96 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } -------------------------------------------------------------------------------- /cypress/integration/home.spec.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { withID } from '../utils'; 4 | import Selectors from '../../web/src/config/test.selectors'; 5 | 6 | context('Actions', () => { 7 | beforeEach(() => { 8 | cy.visit('http://localhost:3000'); 9 | }); 10 | 11 | it('Should load the Home page', () => { 12 | cy.get(withID(Selectors.title)) 13 | .should('be.visible') 14 | .should(element => { 15 | expect(element).to.have.length(1); 16 | expect(element.first().text()).to.be.eq('Full Stack Starterkit'); 17 | }); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | /// 2 | // *********************************************************** 3 | // This example plugins/index.js can be used to load plugins 4 | // 5 | // You can change the location of this file or turn off loading 6 | // the plugins file with the 'pluginsFile' configuration option. 7 | // 8 | // You can read more here: 9 | // https://on.cypress.io/plugins-guide 10 | // *********************************************************** 11 | 12 | // This function is called when a project is opened or re-opened (e.g. due to 13 | // the project's config changing) 14 | 15 | /** 16 | * @type {Cypress.PluginConfig} 17 | */ 18 | module.exports = () => { 19 | // `on` is used to hook into various events Cypress emits 20 | // `config` is the resolved Cypress config 21 | }; 22 | -------------------------------------------------------------------------------- /cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add("login", (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This will overwrite an existing command -- 25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 26 | -------------------------------------------------------------------------------- /cypress/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands'; 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /cypress/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "es5", 6 | "dom" 7 | ], 8 | "types": [ 9 | "cypress" 10 | ] 11 | }, 12 | "include": [ 13 | "**/*.spec.ts" 14 | ], 15 | "exclude": [] 16 | } -------------------------------------------------------------------------------- /cypress/utils.ts: -------------------------------------------------------------------------------- 1 | export function withID(selector: string): string { 2 | return `#${selector}`; 3 | } 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fullstack-starterkit", 3 | "version": "0.1.0", 4 | "author": { 5 | "name": "Karan Pratap Singh", 6 | "email": "contact@karanpratapsingh.com", 7 | "url": "https://karanpratapsingh.com" 8 | }, 9 | "private": true, 10 | "license": "MIT", 11 | "description": "Starter kit for Full Stack projects that scales", 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/karanpratapsingh/fullstack-starterkit.git" 15 | }, 16 | "bugs": { 17 | "url": "https://github.com/karanpratapsingh/fullstack-starterkit/issues" 18 | }, 19 | "scripts": { 20 | "lint": "yarn eslint --ext .js,.jsx,.ts,.tsx .", 21 | "lint:fix": "yarn prettier --write . && yarn lint --fix", 22 | "e2e": "yarn cypress open", 23 | "postinstall": "cd web && yarn && cd ../backend && yarn && cd .." 24 | }, 25 | "devDependencies": { 26 | "@types/jest": "26.0.23", 27 | "@typescript-eslint/eslint-plugin": "4.26.0", 28 | "@typescript-eslint/parser": "4.26.0", 29 | "cypress": "7.4.0", 30 | "eslint": "7.27.0", 31 | "eslint-import-resolver-alias": "1.1.2", 32 | "eslint-import-resolver-typescript": "2.4.0", 33 | "eslint-plugin-import": "2.23.4", 34 | "eslint-plugin-prettier": "3.4.0", 35 | "eslint-plugin-react": "7.24.0", 36 | "husky": "6.0.0", 37 | "prettier": "2.3.0", 38 | "typescript": "4.3.2" 39 | }, 40 | "husky": { 41 | "hooks": { 42 | "pre-commit": "yarn lint", 43 | "pre-push": "yarn lint:fix" 44 | } 45 | }, 46 | "volta": { 47 | "node": "14.18.0", 48 | "yarn": "1.22.10" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /web/.dockerignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | -------------------------------------------------------------------------------- /web/.env.template: -------------------------------------------------------------------------------- 1 | # Default environment variables for web 2 | REACT_APP_ENDPOINT_HTTPS=http://localhost:4000/graphql 3 | REACT_APP_ENDPOINT_WSS=ws://localhost:4000/graphql -------------------------------------------------------------------------------- /web/.eslintignore: -------------------------------------------------------------------------------- 1 | src/graphql/operations.tsx 2 | -------------------------------------------------------------------------------- /web/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /web/Dockerfile: -------------------------------------------------------------------------------- 1 | # Ref: https://dev.to/karanpratapsingh/dockerize-your-react-app-4j2e 2 | FROM node:14.18-alpine 3 | ENV NODE_ENV development 4 | ENV TSC_WATCHFILE UseFsEvents 5 | WORKDIR /app 6 | COPY package.json . 7 | COPY yarn.lock . 8 | RUN yarn install 9 | COPY . . 10 | EXPOSE 3000 11 | CMD tail -f /dev/null 12 | -------------------------------------------------------------------------------- /web/README.md: -------------------------------------------------------------------------------- 1 | ## Available Scripts 2 | 3 | In the project directory, you can run: 4 | 5 | ### `yarn start` 6 | 7 | Runs the app in the development mode.
8 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 9 | 10 | The page will reload if you make edits.
11 | You will also see any lint errors in the console. 12 | 13 | ### `yarn test` 14 | 15 | Launches the test runner in the interactive watch mode.
16 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 17 | 18 | ### `yarn build` 19 | 20 | Builds the app for production to the `build` folder.
21 | It correctly bundles React in production mode and optimizes the build for the best performance. 22 | 23 | The build is minified and the filenames include the hashes.
24 | Your app is ready to be deployed! 25 | 26 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 27 | 28 | ### `yarn eject` 29 | 30 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 31 | 32 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 33 | 34 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 35 | 36 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 37 | 38 | ## Learn More 39 | 40 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 41 | 42 | To learn React, check out the [React documentation](https://reactjs.org/). 43 | -------------------------------------------------------------------------------- /web/craco.config.js: -------------------------------------------------------------------------------- 1 | const CracoAlias = require('craco-alias'); 2 | 3 | module.exports = { 4 | plugins: [ 5 | { 6 | plugin: CracoAlias, 7 | options: { 8 | source: 'tsconfig', 9 | baseUrl: '.', 10 | tsConfigPath: './tsconfig.paths.json' 11 | } 12 | } 13 | ] 14 | }; 15 | -------------------------------------------------------------------------------- /web/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | 3 | services: 4 | web: 5 | container_name: web 6 | image: web 7 | restart: always 8 | build: 9 | context: . 10 | volumes: 11 | - .:/app 12 | - dependencies:/app/node_modules 13 | env_file: 14 | - .env 15 | ports: 16 | - 3000:3000 17 | 18 | volumes: 19 | dependencies: 20 | -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "project-web", 3 | "version": "0.1.0", 4 | "author": { 5 | "name": "Karan Pratap Singh", 6 | "email": "contact@karanpratapsingh.com", 7 | "url": "https://karanpratapsingh.com" 8 | }, 9 | "private": true, 10 | "license": "MIT", 11 | "description": "Full Stack starterkit web", 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/karanpratapsingh/fullstack-starterkit.git" 15 | }, 16 | "bugs": { 17 | "url": "https://github.com/karanpratapsingh/fullstack-starterkit/issues" 18 | }, 19 | "scripts": { 20 | "dev": "docker compose up", 21 | "dev:down": "docker compose down", 22 | "start": "craco start", 23 | "build": "craco build", 24 | "test": "yarn test:clean && craco test --watchAll=false", 25 | "test:clean": "yarn jest --passWithNoTests clean", 26 | "eject": "craco eject" 27 | }, 28 | "dependencies": { 29 | "@apollo/client": "3.3.19", 30 | "@craco/craco": "6.1.2", 31 | "@material-ui/core": "4.11.4", 32 | "@material-ui/icons": "4.11.2", 33 | "@testing-library/jest-dom": "5.12.0", 34 | "@testing-library/react": "11.2.7", 35 | "@testing-library/user-event": "13.1.9", 36 | "@types/jest": "26.0.23", 37 | "@types/material-ui": "0.21.8", 38 | "@types/node": "15.9.0", 39 | "@types/react": "17.0.9", 40 | "@types/react-dom": "17.0.6", 41 | "@types/react-router": "5.1.14", 42 | "@types/react-router-dom": "5.1.7", 43 | "graphql": "15.5.0", 44 | "react": "17.0.2", 45 | "react-dom": "17.0.2", 46 | "react-router": "5.2.0", 47 | "react-router-dom": "5.2.0", 48 | "react-scripts": "4.0.3", 49 | "subscriptions-transport-ws": "0.9.18", 50 | "typescript": "4.3.2" 51 | }, 52 | "devDependencies": { 53 | "craco-alias": "3.0.1" 54 | }, 55 | "jest": { 56 | "moduleNameMapper": { 57 | "@web/(.*)$": "/src/$1" 58 | } 59 | }, 60 | "browserslist": { 61 | "production": [ 62 | ">0.2%", 63 | "not dead", 64 | "not op_mini all" 65 | ], 66 | "development": [ 67 | "last 1 chrome version", 68 | "last 1 firefox version", 69 | "last 1 safari version" 70 | ] 71 | }, 72 | "volta": { 73 | "node": "14.18.0", 74 | "yarn": "1.22.10" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /web/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karanpratapsingh/fullstack-starterkit/c6552ad748fa70e52dacd7ea96850617389bda86/web/public/favicon.ico -------------------------------------------------------------------------------- /web/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 19 | 20 | 21 | 30 | Full Stack starterkit 31 | 32 | 33 | 34 |
35 | 43 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /web/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karanpratapsingh/fullstack-starterkit/c6552ad748fa70e52dacd7ea96850617389bda86/web/public/logo192.png -------------------------------------------------------------------------------- /web/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karanpratapsingh/fullstack-starterkit/c6552ad748fa70e52dacd7ea96850617389bda86/web/public/logo512.png -------------------------------------------------------------------------------- /web/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Full Stack App", 3 | "name": "Full Stack starterkit", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /web/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /web/public/worker.js: -------------------------------------------------------------------------------- 1 | const CACHE_NAME = 'FullStack Starterkit'; 2 | 3 | // Add the routes you want to cache in an offline PWA 4 | const urlsToCache = ['/']; 5 | 6 | // Install a service worker 7 | self.addEventListener('install', event => { 8 | // Perform install steps 9 | event.waitUntil( 10 | caches.open(CACHE_NAME).then(cache => { 11 | console.log('Opened cache'); 12 | return cache.addAll(urlsToCache); 13 | }) 14 | ); 15 | }); 16 | 17 | // Cache and return requests 18 | self.addEventListener('fetch', event => { 19 | event.respondWith( 20 | caches.match(event.request).then(response => { 21 | // Cache hit - return response 22 | if (response) { 23 | return response; 24 | } 25 | return fetch(event.request); 26 | }) 27 | ); 28 | }); 29 | 30 | // Update a service worker 31 | self.addEventListener('activate', event => { 32 | const cacheWhitelist = ['Full Stack Starterkit']; 33 | event.waitUntil( 34 | caches.keys().then(cacheNames => { 35 | return Promise.all( 36 | cacheNames.map(cacheName => { 37 | if (cacheWhitelist.indexOf(cacheName) === -1) { 38 | return caches.delete(cacheName); 39 | } 40 | }) 41 | ); 42 | }) 43 | ); 44 | }); 45 | -------------------------------------------------------------------------------- /web/src/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karanpratapsingh/fullstack-starterkit/c6552ad748fa70e52dacd7ea96850617389bda86/web/src/assets/images/logo.png -------------------------------------------------------------------------------- /web/src/config/index.ts: -------------------------------------------------------------------------------- 1 | import { required } from '@web/utils'; 2 | import Selectors from './test.selectors'; 3 | 4 | const { version } = require('../../package.json'); 5 | 6 | type Endpoint = { 7 | https: string; 8 | wss: string; 9 | }; 10 | 11 | class Config { 12 | version: string; 13 | endpoints: Endpoint; 14 | 15 | constructor(env: NodeJS.ProcessEnv) { 16 | this.version = version; 17 | this.endpoints = { 18 | https: required(env.REACT_APP_ENDPOINT_HTTPS), 19 | wss: required(env.REACT_APP_ENDPOINT_WSS) 20 | }; 21 | } 22 | } 23 | 24 | export { Config as default, Selectors }; 25 | -------------------------------------------------------------------------------- /web/src/config/test.selectors.ts: -------------------------------------------------------------------------------- 1 | const Selectors: Record = { 2 | title: 'title-id' 3 | }; 4 | 5 | export default Selectors; 6 | -------------------------------------------------------------------------------- /web/src/constants/index.ts: -------------------------------------------------------------------------------- 1 | export enum Routes { 2 | HOME = '/' 3 | } 4 | -------------------------------------------------------------------------------- /web/src/global/root.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 4 | 'Droid Sans', 'Helvetica Neue', sans-serif; 5 | -webkit-font-smoothing: antialiased; 6 | -moz-osx-font-smoothing: grayscale; 7 | } 8 | 9 | code { 10 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; 11 | } 12 | 13 | #root { 14 | display: flex; 15 | flex: 1; 16 | min-height: 100vh; 17 | width: 100vw; 18 | flex-direction: column; 19 | } 20 | -------------------------------------------------------------------------------- /web/src/graphql/index.ts: -------------------------------------------------------------------------------- 1 | import { ApolloClient, InMemoryCache, NormalizedCacheObject, HttpLink, split, ApolloLink } from '@apollo/client'; 2 | import { WebSocketLink } from '@apollo/client/link/ws'; 3 | import { onError } from '@apollo/client/link/error'; 4 | import { getMainDefinition } from '@apollo/client/utilities'; 5 | import Config from '@web/config'; 6 | 7 | function configureApolloClient(config: Config): ApolloClient { 8 | const httpLink = new HttpLink({ uri: config.endpoints.https }); 9 | 10 | const wsLink = new WebSocketLink({ 11 | uri: config.endpoints.wss, 12 | options: { reconnect: true } 13 | }); 14 | 15 | const link = split( 16 | ({ query }) => { 17 | const definition = getMainDefinition(query); 18 | return definition.kind === 'OperationDefinition' && definition.operation === 'subscription'; 19 | }, 20 | wsLink, 21 | httpLink 22 | ); 23 | 24 | const client = new ApolloClient({ 25 | link: ApolloLink.from([ 26 | onError(({ graphQLErrors, networkError }) => { 27 | if (graphQLErrors) { 28 | graphQLErrors.forEach(({ message, locations, path }) => 29 | console.log(`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`) 30 | ); 31 | } 32 | 33 | if (networkError) { 34 | console.log(`[Network error]: ${networkError}`); 35 | } 36 | }), 37 | link 38 | ]), 39 | cache: new InMemoryCache() 40 | }); 41 | 42 | return client; 43 | } 44 | 45 | export { configureApolloClient }; 46 | export * from './operations'; 47 | -------------------------------------------------------------------------------- /web/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { ThemeProvider } from '@material-ui/core'; 2 | import { Routes } from '@web/constants'; 3 | import '@web/global/root.css'; 4 | import { Home } from '@web/pages'; 5 | import theme from '@web/theme'; 6 | import React from 'react'; 7 | import ReactDOM from 'react-dom'; 8 | import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'; 9 | import { ApolloProvider } from '@apollo/client'; 10 | import { configureApolloClient } from '@web/graphql'; 11 | import Config from '@web/config'; 12 | import { Footer } from '@web/layout'; 13 | import * as serviceWorker from './serviceWorker'; 14 | 15 | const config = new Config(process.env); 16 | const client = configureApolloClient(config); 17 | 18 | function App(): React.ReactElement { 19 | return ( 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | ); 31 | } 32 | 33 | ReactDOM.render( 34 | 35 | 36 | , 37 | document.getElementById('root') 38 | ); 39 | 40 | // If you want your app to work offline and load faster, you can change 41 | // unregister() to register() below. Note this comes with some pitfalls. 42 | // Learn more about service workers: https://bit.ly/CRA-PWA 43 | serviceWorker.register(); 44 | -------------------------------------------------------------------------------- /web/src/layout/Footer/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Container, Typography } from '@material-ui/core'; 3 | import { makeStyles } from '@material-ui/core/styles'; 4 | 5 | const useStyles = makeStyles(({ palette, typography, spacing }) => ({ 6 | container: { 7 | display: 'flex', 8 | alignItems: 'center', 9 | justifyContent: 'center', 10 | flexDirection: 'column', 11 | padding: `${spacing(2)}px 0px`, 12 | backgroundColor: palette.grey[100] 13 | }, 14 | body: { 15 | fontSize: 14, 16 | color: palette.grey[600], 17 | fontWeight: typography.fontWeightLight, 18 | marginTop: spacing(1) 19 | } 20 | })); 21 | 22 | function Footer(): React.ReactElement<{}> { 23 | const classes = useStyles(); 24 | const year = new Date().getFullYear(); 25 | 26 | return ( 27 | 28 | 29 | Made with React and GraphQL | Copyright {year} 30 | 31 | 32 | ); 33 | } 34 | 35 | export default Footer; 36 | -------------------------------------------------------------------------------- /web/src/layout/index.ts: -------------------------------------------------------------------------------- 1 | import Footer from './Footer'; 2 | 3 | export { Footer }; 4 | -------------------------------------------------------------------------------- /web/src/pages/Home/components/Welcome/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactLogo from '@web/assets/images/logo.png'; 3 | import { Container, Typography } from '@material-ui/core'; 4 | import { makeStyles } from '@material-ui/core/styles'; 5 | import { Selectors } from '@web/config'; 6 | 7 | const useStyles = makeStyles(({ palette, typography, spacing, breakpoints }) => ({ 8 | container: { 9 | display: 'flex', 10 | alignItems: 'center', 11 | justifyContent: 'center', 12 | flexDirection: 'column' 13 | }, 14 | logo: { 15 | width: '40%', 16 | [breakpoints.down('sm')]: { 17 | width: '60%' 18 | }, 19 | [breakpoints.down('xs')]: { 20 | width: '80%' 21 | } 22 | }, 23 | title: { 24 | color: palette.grey[800], 25 | fontWeight: typography.fontWeightRegular, 26 | marginTop: spacing(4) 27 | }, 28 | subtitle: { 29 | color: palette.grey[700], 30 | fontWeight: typography.fontWeightLight, 31 | marginTop: spacing(2) 32 | }, 33 | body: { 34 | color: palette.grey[700], 35 | fontWeight: typography.fontWeightLight, 36 | marginTop: spacing(1) 37 | } 38 | })); 39 | 40 | function Welcome(): React.ReactElement<{}> { 41 | const classes = useStyles(); 42 | 43 | return ( 44 | 45 | starterkit 46 | 47 | Full Stack Starterkit 48 | 49 | 50 | GraphQL first starter kit that scales 51 | 52 | 53 | powered by TypeScript 54 | 55 | 56 | PWA Support now added! 57 | 58 | 59 | ); 60 | } 61 | 62 | export default Welcome; 63 | -------------------------------------------------------------------------------- /web/src/pages/Home/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Container } from '@material-ui/core'; 3 | import { makeStyles } from '@material-ui/core/styles'; 4 | import Welcome from './components/Welcome'; 5 | 6 | const useStyles = makeStyles(() => ({ 7 | container: { 8 | display: 'flex', 9 | flex: 1, 10 | alignItems: 'center', 11 | justifyContent: 'center', 12 | flexDirection: 'column' 13 | } 14 | })); 15 | 16 | function Home(): React.ReactElement<{}> { 17 | const classes = useStyles(); 18 | 19 | return ( 20 | 21 | 22 | 23 | ); 24 | } 25 | 26 | export default Home; 27 | -------------------------------------------------------------------------------- /web/src/pages/index.ts: -------------------------------------------------------------------------------- 1 | import Home from './Home'; 2 | 3 | export { Home }; 4 | -------------------------------------------------------------------------------- /web/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /web/src/serviceWorker.ts: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/) 19 | ); 20 | 21 | type Config = { 22 | onSuccess?: (registration: ServiceWorkerRegistration) => void; 23 | onUpdate?: (registration: ServiceWorkerRegistration) => void; 24 | }; 25 | 26 | function checkValidServiceWorker(swUrl: string, config?: Config) { 27 | // Check if the service worker can be found. If it can't reload the page. 28 | fetch(swUrl, { 29 | headers: { 'Service-Worker': 'script' } 30 | }) 31 | .then(response => { 32 | // Ensure service worker exists, and that we really are getting a JS file. 33 | const contentType = response.headers.get('content-type'); 34 | if (response.status === 404 || (contentType !== null && contentType.indexOf('javascript') === -1)) { 35 | // No service worker found. Probably a different app. Reload the page. 36 | navigator.serviceWorker.ready.then(registration => { 37 | registration.unregister().then(() => { 38 | window.location.reload(); 39 | }); 40 | }); 41 | } else { 42 | // Service worker found. Proceed as normal. 43 | registerValidSW(swUrl, config); 44 | } 45 | }) 46 | .catch(() => { 47 | console.log('No internet connection found. App is running in offline mode.'); 48 | }); 49 | } 50 | 51 | function registerValidSW(swUrl: string, config?: Config) { 52 | navigator.serviceWorker 53 | .register(swUrl) 54 | .then(registration => { 55 | registration.onupdatefound = () => { 56 | const installingWorker = registration.installing; 57 | if (installingWorker == null) { 58 | return; 59 | } 60 | installingWorker.onstatechange = () => { 61 | if (installingWorker.state === 'installed') { 62 | if (navigator.serviceWorker.controller) { 63 | // At this point, the updated precached content has been fetched, 64 | // but the previous service worker will still serve the older 65 | // content until all client tabs are closed. 66 | console.log( 67 | 'New content is available and will be used when all ' + 68 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 69 | ); 70 | 71 | // Execute callback 72 | if (config && config.onUpdate) { 73 | config.onUpdate(registration); 74 | } 75 | } else { 76 | // At this point, everything has been precached. 77 | // It's the perfect time to display a 78 | // "Content is cached for offline use." message. 79 | console.log('Content is cached for offline use.'); 80 | 81 | // Execute callback 82 | if (config && config.onSuccess) { 83 | config.onSuccess(registration); 84 | } 85 | } 86 | } 87 | }; 88 | }; 89 | }) 90 | .catch(error => { 91 | console.error('Error during service worker registration:', error); 92 | }); 93 | } 94 | 95 | export function register(config?: Config): void { 96 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 97 | // The URL constructor is available in all browsers that support SW. 98 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 99 | if (publicUrl.origin !== window.location.origin) { 100 | // Our service worker won't work if PUBLIC_URL is on a different origin 101 | // from what our page is served on. This might happen if a CDN is used to 102 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 103 | return; 104 | } 105 | 106 | window.addEventListener('load', () => { 107 | const swUrl = `${process.env.PUBLIC_URL}/worker.js`; 108 | 109 | if (isLocalhost) { 110 | // This is running on localhost. Let's check if a service worker still exists or not. 111 | checkValidServiceWorker(swUrl, config); 112 | 113 | // Add some additional logging to localhost, pointing developers to the 114 | // service worker/PWA documentation. 115 | navigator.serviceWorker.ready.then(() => { 116 | console.log( 117 | 'This web app is being served cache-first by a service ' + 118 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 119 | ); 120 | }); 121 | } else { 122 | // Is not localhost. Just register service worker 123 | registerValidSW(swUrl, config); 124 | } 125 | }); 126 | } 127 | } 128 | 129 | export function unregister(): void { 130 | if ('serviceWorker' in navigator) { 131 | navigator.serviceWorker.ready.then(registration => { 132 | registration.unregister(); 133 | }); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /web/src/tests/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import { Home } from '../pages'; 4 | 5 | describe('Web test suite', () => { 6 | test('Renders home page correctly', () => { 7 | const { getByText } = render(); 8 | const linkElement = getByText(/Full Stack Starterkit/i); 9 | expect(linkElement).toBeTruthy(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /web/src/theme/index.ts: -------------------------------------------------------------------------------- 1 | import { createMuiTheme, Theme } from '@material-ui/core/styles'; 2 | import createBreakpoints from '@material-ui/core/styles/createBreakpoints'; 3 | import palette from './palette'; 4 | import typography, { createTypographyOverrides } from './typography'; 5 | 6 | const breakpoints = createBreakpoints({}); 7 | 8 | const overrides = createTypographyOverrides(breakpoints); 9 | 10 | const theme: Theme = createMuiTheme({ palette, typography, breakpoints, overrides }); 11 | 12 | export default theme; 13 | -------------------------------------------------------------------------------- /web/src/theme/palette.ts: -------------------------------------------------------------------------------- 1 | // https://material-ui.com/customization/palette/ 2 | const palette = { 3 | primary: { 4 | light: '#D1C4E9', 5 | main: '#673AB7', 6 | dark: '#512DA8' 7 | }, 8 | secondary: { 9 | main: '#00BCD4' 10 | }, 11 | text: { 12 | primary: '#212121', 13 | secondary: '#757575' 14 | } 15 | }; 16 | 17 | export default palette; 18 | -------------------------------------------------------------------------------- /web/src/theme/typography.ts: -------------------------------------------------------------------------------- 1 | import { Breakpoints } from '@material-ui/core/styles/createBreakpoints'; 2 | import { Overrides } from '@material-ui/core/styles/overrides'; 3 | import { pxToRem } from '@web/utils'; 4 | 5 | const typography = { 6 | fontFamily: 'Montserrat, sans-serif' 7 | }; 8 | 9 | // https://material-ui.com/customization/breakpoints/ 10 | 11 | function createTypographyOverrides(breakpoints: Breakpoints): Overrides { 12 | return { 13 | MuiTypography: { 14 | h4: { 15 | fontSize: pxToRem(40), 16 | [breakpoints.down('xs')]: { 17 | fontSize: pxToRem(30) 18 | } 19 | }, 20 | subtitle1: { 21 | fontSize: pxToRem(22), 22 | [breakpoints.down('xs')]: { 23 | fontSize: pxToRem(20) 24 | } 25 | }, 26 | subtitle2: { 27 | fontSize: pxToRem(24), 28 | [breakpoints.down('xs')]: { 29 | fontSize: pxToRem(22) 30 | } 31 | }, 32 | body1: { 33 | fontSize: pxToRem(18), 34 | [breakpoints.down('xs')]: { 35 | fontSize: pxToRem(16) 36 | } 37 | }, 38 | body2: { 39 | fontSize: pxToRem(20), 40 | [breakpoints.down('xs')]: { 41 | fontSize: pxToRem(18) 42 | } 43 | }, 44 | caption: { 45 | fontSize: pxToRem(16), 46 | [breakpoints.down('xs')]: { 47 | fontSize: pxToRem(14) 48 | } 49 | } 50 | } 51 | }; 52 | } 53 | 54 | export { typography as default, createTypographyOverrides }; 55 | -------------------------------------------------------------------------------- /web/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export function pxToRem(value: number): string { 2 | return `${value / 16}rem`; 3 | } 4 | 5 | export function required(value: T | undefined): T { 6 | if (!value) { 7 | throw new Error('value is required'); 8 | } 9 | 10 | return value; 11 | } 12 | -------------------------------------------------------------------------------- /web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.paths.json", 3 | "compilerOptions": { 4 | "target": "es5", 5 | "lib": [ 6 | "dom", 7 | "dom.iterable", 8 | "esnext" 9 | ], 10 | "allowJs": true, 11 | "skipLibCheck": true, 12 | "esModuleInterop": true, 13 | "allowSyntheticDefaultImports": true, 14 | "strict": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx", 22 | "sourceMap": true, 23 | "removeComments": true, 24 | "noImplicitAny": false, 25 | "strictNullChecks": true, 26 | "strictFunctionTypes": true, 27 | "noImplicitThis": true, 28 | "alwaysStrict": true, 29 | "noUnusedLocals": true, 30 | "noUnusedParameters": true, 31 | "noImplicitReturns": true, 32 | "noFallthroughCasesInSwitch": true 33 | }, 34 | "include": [ 35 | "src" 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /web/tsconfig.paths.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "@web/assets": [ 6 | "src/assets" 7 | ], 8 | "@web/config": [ 9 | "src/config" 10 | ], 11 | "@web/constants": [ 12 | "src/constants" 13 | ], 14 | "@web/global": [ 15 | "src/global" 16 | ], 17 | "@web/layout": [ 18 | "src/layout" 19 | ], 20 | "@web/graphql": [ 21 | "src/graphql" 22 | ], 23 | "@web/pages": [ 24 | "src/pages" 25 | ], 26 | "@web/theme": [ 27 | "src/theme" 28 | ], 29 | "@web/utils": [ 30 | "src/utils" 31 | ] 32 | } 33 | } 34 | } --------------------------------------------------------------------------------