├── .circleci ├── config.yml ├── heroku.sh └── run-test.sh ├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── ci.yml ├── .gitignore ├── .graphqlconfig ├── .huskyrc ├── .npmrc ├── .vscode ├── launch.json └── settings.json ├── LICENSE.md ├── README.md ├── apollo.config.js ├── babel.config.js ├── config ├── nginx │ ├── .gitignore │ ├── Dockerfile │ ├── README.md │ ├── localhost.crt │ ├── localhost.key │ ├── package.json │ └── server.js ├── prettier.json └── prettierignore ├── dev.sh ├── docker-compose.yml ├── lerna.json ├── package.json ├── packages ├── app │ ├── .babelrc │ ├── .eslintignore │ ├── .eslintrc │ ├── .graphqlconfig │ ├── .lintstagedrc │ ├── .nmprc │ ├── .prettierrc │ ├── Dockerfile │ ├── README.md │ ├── codegen.yml │ ├── globals.d.ts │ ├── jest.config.js │ ├── jest.tsconfig.json │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── public │ │ └── static │ │ │ ├── bol.svg │ │ │ ├── favicon.png │ │ │ └── logo.svg │ ├── server.js │ ├── src │ │ ├── elements │ │ │ ├── image │ │ │ │ ├── image.scss │ │ │ │ └── image.tsx │ │ │ └── logo-bol │ │ │ │ └── logo-bol.tsx │ │ ├── graphql │ │ │ ├── _generated-fragment-types.ts │ │ │ ├── _generated-hooks.tsx │ │ │ ├── _generated-schema.graphql │ │ │ ├── _generated-types.ts │ │ │ ├── apollo.js │ │ │ ├── fragment-matcher.js │ │ │ └── fragments │ │ │ │ └── product.fragment.graphql.ts │ │ ├── modules │ │ │ ├── App.tsx │ │ │ ├── ErrorMessage.tsx │ │ │ ├── app.scss │ │ │ ├── header │ │ │ │ ├── header.scss │ │ │ │ └── header.tsx │ │ │ ├── lazy.module.scss │ │ │ ├── offer │ │ │ │ ├── graphql │ │ │ │ │ └── get-offer.graphql.ts │ │ │ │ ├── offer.scss │ │ │ │ └── offer.tsx │ │ │ ├── product │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── product.spec.tsx.snap │ │ │ │ ├── elements │ │ │ │ │ └── product-details │ │ │ │ │ │ ├── __snapshots__ │ │ │ │ │ │ └── product-details.spec.tsx.snap │ │ │ │ │ │ ├── product-details.scss │ │ │ │ │ │ ├── product-details.spec.tsx │ │ │ │ │ │ ├── product-details.tsx │ │ │ │ │ │ └── product-placeholder.tsx │ │ │ │ ├── graphql │ │ │ │ │ └── get-product.graphql.ts │ │ │ │ ├── product-component.tsx │ │ │ │ ├── product.spec.tsx │ │ │ │ └── product.tsx │ │ │ └── products-list │ │ │ │ ├── elements │ │ │ │ └── link │ │ │ │ │ └── products-list-link.tsx │ │ │ │ ├── graphql │ │ │ │ └── get-products.graphql.ts │ │ │ │ ├── products-list.scss │ │ │ │ └── products-list.tsx │ │ └── pages │ │ │ ├── debug.js │ │ │ ├── example.js │ │ │ ├── index.js │ │ │ ├── lazy.js │ │ │ ├── product.js │ │ │ ├── product │ │ │ └── [id].js │ │ │ ├── products.js │ │ │ └── sample.js │ ├── test │ │ ├── __mocks__ │ │ │ └── fileMock.js │ │ └── visual-regression │ │ │ └── index.js │ └── tsconfig.json └── server │ ├── .gitignore │ ├── .lintstagedrc │ ├── .npmrc │ ├── .prettierrc.json │ ├── Dockerfile │ ├── README.md │ ├── codegen.yml │ ├── jest.config.js │ ├── package.json │ ├── src │ ├── _graphql.d.ts │ ├── _schema.graphql │ ├── app.ts │ ├── config │ │ ├── index.ts │ │ ├── profile.ts │ │ └── server.ts │ ├── constants │ │ ├── base-urls.ts │ │ ├── credentials.ts │ │ └── index.ts │ ├── global.d.ts │ ├── helpers │ │ ├── check-status │ │ │ ├── check-status.spec.ts │ │ │ └── check-status.ts │ │ ├── index.ts │ │ └── is-user-request.ts │ ├── index.ts │ ├── middleware │ │ ├── allowed-origin │ │ │ ├── allowed-origins.ts │ │ │ └── origins-list.ts │ │ └── cache │ │ │ └── no-cache.ts │ ├── modules │ │ ├── common │ │ │ └── index.ts │ │ ├── offer │ │ │ ├── index.ts │ │ │ ├── providers │ │ │ │ └── offer.ts │ │ │ ├── resolvers │ │ │ │ └── resolvers.ts │ │ │ └── schema │ │ │ │ ├── offer.graphql │ │ │ │ └── query.graphql │ │ └── product │ │ │ ├── index.ts │ │ │ ├── providers │ │ │ ├── product-data-loader.ts │ │ │ └── product.ts │ │ │ ├── resolvers │ │ │ └── resolvers.ts │ │ │ └── schema │ │ │ ├── product.graphql │ │ │ └── query.graphql │ ├── schema.ts │ ├── server.ts │ └── typings.d.ts │ ├── swagger.yml │ ├── test │ ├── __mocks__ │ │ └── stubs │ │ │ ├── poducts.ts │ │ │ ├── product-9200000111963040.ts │ │ │ ├── product-9200000113065845.ts │ │ │ └── product-9200000113944705.ts │ └── schema-mock.ts │ ├── tsconfig.json │ └── tslint.json ├── setup.sh └── yarn.lock /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | defaults: &defaults 3 | docker: 4 | - image: circleci/node:10.16 5 | 6 | version: 2 7 | jobs: 8 | build: 9 | <<: *defaults 10 | working_directory: ~/repo 11 | 12 | steps: 13 | - checkout 14 | - restore_cache: 15 | keys: 16 | - dependency-cache-{{ checksum "yarn.lock" }} 17 | - dependency-cache-{{ checksum "packages/app/yarn.lock" }} 18 | - dependency-cache-{{ checksum "packages/server/yarn.lock" }} 19 | - run: yarn 20 | - save_cache: 21 | key: dependency-cache-{{ checksum "./yarn.lock" }} 22 | paths: 23 | - node_modules 24 | - packages/app/node_modules 25 | - packages/server/node_modules 26 | - run: yarn lint 27 | - run: yarn test 28 | # Workflow not working at the moment 29 | # - persist_to_workspace: 30 | # root: ./ 31 | # paths: 32 | # - ./node_modules 33 | # - ./packages/app 34 | # - ./paclages/server 35 | 36 | test: 37 | <<: *defaults 38 | steps: 39 | - checkout 40 | - attach_workspace: 41 | at: ./ 42 | - run: yarn test 43 | - run: yarn lint 44 | 45 | workflows: 46 | version: 2 47 | build_and_release: 48 | jobs: 49 | - build 50 | # - test: 51 | # requires: 52 | # - build -------------------------------------------------------------------------------- /.circleci/heroku.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | usage() { 4 | echo "OVERVIEW: Build apps according to BUILD_ENV value. Meant to be used for Heroku deployment" 5 | exit 6 | } 7 | 8 | if [ "$1" = '-h' ] || [ "$1" = '--help' ]; then 9 | usage 10 | fi 11 | 12 | ( 13 | PROJECT_ROOT="$(cd $(dirname $0)/..; pwd)" 14 | 15 | cd $PROJECT_ROOT 16 | 17 | if [ "$BUILD_ENV" = "app" ]; then 18 | yarn workspace graphql-app build 19 | elif [ "$BUILD_ENV" = "server" ]; then 20 | yarn workspace graphql-server build 21 | else 22 | echo "Error: no build config for BUILD_ENV value '$BUILD_ENV'" 23 | exit 1 24 | fi 25 | ) -------------------------------------------------------------------------------- /.circleci/run-test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | repo=$1 3 | branch=`git rev-parse --abbrev-ref HEAD` 4 | if [ "$branch" = "master" ]; then 5 | echo "On branch master. Let's run all tests!" 6 | eval "yarn test" 7 | elif git diff --name-only origin/master...$branch | grep "^${repo}" ; then 8 | echo "Changes detected! Adding ${repo} tests to the queue..." 9 | else 10 | echo "No changes detected. Exiting circle build..." 11 | circleci step halt 12 | fi -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | end_of_line = lf 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | 2 | name: CI 3 | 4 | on: [push] 5 | 6 | jobs: 7 | test: 8 | name: Test on node ${{ matrix.node_version }} 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | node_version: [10, 12] 13 | 14 | steps: 15 | - uses: actions/checkout@master 16 | 17 | - name: Use Node.js ${{ matrix.node_version }} 18 | uses: actions/setup-node@master 19 | with: 20 | version: ${{ matrix.node_version }} 21 | 22 | - name: Install 23 | run: | 24 | yarn 25 | 26 | - name: Build 27 | run: | 28 | yarn build 29 | 30 | - name: Test 31 | run: | 32 | yarn test 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # fs 2 | .DS_Store 3 | #.vscode 4 | !.vscode/settings.json 5 | !.vscode/launch.json 6 | 7 | coverage 8 | node_modules 9 | .npm 10 | .nvmrc 11 | .clinic 12 | # we are using yarn, when people update with npm exclude this 13 | package-lock.json 14 | dist 15 | 16 | yarn-error.log 17 | .yarnclean 18 | 19 | lerna-debug.log 20 | 21 | .next 22 | .env 23 | 24 | *.scss.d.ts 25 | 26 | # auto generated (leave schema and hooks in repo!) 27 | # packages/app/src/graphql/_generated-types.ts 28 | 29 | node_trace.* -------------------------------------------------------------------------------- /.graphqlconfig: -------------------------------------------------------------------------------- 1 | { 2 | "schemaPath": "packages/server/src/_schema.graphql", 3 | "extensions": { 4 | "endpoints": { 5 | "dev": "http://localhost:4000/graphql" 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /.huskyrc: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | "pre-commit": "lerna run --concurrency 1 --stream precommit" 4 | } 5 | } -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact = true -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "attach", 10 | "name": "GRAPHQL server", 11 | "protocol": "inspector", 12 | "port": 7001, 13 | "restart": true, 14 | "localRoot": "${workspaceRoot}", 15 | "remoteRoot": "." 16 | }, 17 | /*{ 18 | "type": "node", 19 | "request": "attach", 20 | "name": "WEB", 21 | "protocol": "inspector", 22 | "port": 7000, 23 | "restart": true, 24 | "localRoot": "${workspaceRoot}", 25 | "remoteRoot": "." 26 | }*/ 27 | ], 28 | // run `yarn start` in vscode console, then attach this one 29 | // still problems running nextjs correctly with inspect flag, it keeps re-attaching 30 | "compounds": [ 31 | { 32 | "name": "Full", 33 | "configurations": [ 34 | "GRAPHQL server" 35 | ] 36 | } 37 | ] 38 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.alwaysShowStatus": true, 3 | "eslint.workingDirectories": [ 4 | "packages/app", "packages/server" 5 | ], 6 | "prettier.tabWidth": 4 7 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Maapteh. 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 | # GraphQL-Modules TypeScript Server & NextJS React application 2 | > Demonstration application for showcase utilizing [Graphql-modules](https://graphql-modules.com/) which is using data from BOL.com Open Api for the server (also complete mocked version is available). You will find a sample with products and dataloader. 3 | The React web application is using [NextJS](https://nextjs.org/), [GraphQL Codegen by Dotan](https://graphql-code-generator.com) and [Apollo hooks](https://www.apollographql.com/docs/react/api/react-hooks/). _More background information about this app is in the [wiki](../../wiki)._ I would like to thank [The Guild](https://the-guild.dev) for their awesome GraphQL toolchain. 4 | 5 | ## PRE-REQUISITES 6 | - Node dubnium (required) or higher 7 | - Facebook watchman (only for development) (optional) 8 | - Get your free API key from [open api bol.com](https://partnerblog.bol.com/documentatie/open-api) (optional) 9 | 10 | ## INSTALL 11 | 1. `yarn` 12 | 2. `bash setup.sh` sets correct local .env file for server part with _mock mode as default_ (it is possible to set your bol.com api key there as well (then also set MOCK_API=off), the only difference will be using a real datasource or not!) 13 | 3. Apollo [vsc extension](https://marketplace.visualstudio.com/items?itemName=apollographql.vscode-apollo) (optional) 14 | 15 | ## ONLINE DEMO 16 | *Both Heroku containers spin down when no activity, please be patient.* 17 | [graphql-schiphol.herokuapp.com/](https://graphql-schiphol.herokuapp.com) which points to the graphql endpoint at [graphql-server](https://graphql-server-schiphol.herokuapp.com/graphql) - [interactive graph](https://graphql-server-schiphol.herokuapp.com/voyager). 18 | 19 | ## STRUCTURE 20 | ``` 21 | . 22 | ├── /config/ # some configuration for build scripts 23 | ├── /packages/ # 2 applications 24 | │ ├── /app/ # React NextJS isomorphic application 25 | │ └── /server/ # Apollo GraphQL server created with graphql-modules 26 | ├── /test/ # end-to-end tests 27 | ``` 28 | 29 | ## DEVELOPMENT 30 | **Now when you followed the install part (and looked at point 2, its nicer with a real datasource) you can simply run `yarn start`. It will spin up the GraphQL server and the React application.** 31 | Please look at the Vscode plugins below for editor happiness. 32 | 33 | It is also possible to just play with only the server part with `MOCK_API=on yarn start:dev:server`, which spins up the graphql server in mocked mode. 34 | Or `BOL_API_KEY=*** yarn start:dev:server` which spins it up in non-mocked mode when you have an open api bol.com key. 35 | 36 | For Vscode you can attach `GRAPHQL server` to the debug panel which will make it possible to have breakpoints while running the whole application. For other editors just whatch port 7001 for inspection results. 37 | 38 | ## PLAYGROUND 39 | At [local-server](http://localhost:400) or [demo-server heroku](https://graphql-server-schiphol.herokuapp.com/graphql) you will see [dataloader](./packages/server/src/modules/product/providers/product-data-loader.ts) taking care of eventually requesting two products from the API in one single call. Using the following query: 40 | 41 | ``` 42 | { 43 | foo: getProduct(id:"9200000111963040") { 44 | id 45 | title 46 | } 47 | bar:getProduct(id:"9200000111963040") { 48 | id 49 | title 50 | rating 51 | } 52 | shizzle:getProduct(id:"9200000108695538") { 53 | title 54 | rating 55 | shortDescription 56 | } 57 | } 58 | ``` 59 | 60 | You can find product and product category id's on the real [bol.com](https://bol.com) website to play further. 61 | 62 | ## CODE DEMONSTRATION 63 | Product is explained in: 64 | - [graphql module](packages/server/src/modules/product) the injectable Product module 65 | - [frontend module](packages/app/src/modules/product) the client component 66 | 67 | ## PRODUCTION 68 | By default after install the build will take place. 69 | *Please look carefully at ./packages/server/src/server.ts#37 and put comment back when really deploying for production, then you need to turn off introspection and interactive graph, they are now only turned on for the demonstration effect.* 70 | 71 | ## CONFIGURATION 72 | Environment vars for development (set them in CI for production). 73 | 74 | ### '.env' file inside './packages/server': 75 | 76 | *Important: You can set MOCK_API to 'on' in case you don't have access to bol.com api. Then the server will use stub data* 77 | 78 | ``` 79 | BOL_API_KEY=*** 80 | NODE_ENV=development 81 | MOCK_API=on|off 82 | ENGINE_KEY=optional-apollo-engine-key-overhere REMOVE WHEN NOT AVAILABLE 83 | ALLOWED_ORIGIN=optional-not-needed-dev-mode REMOVE 84 | ``` 85 | 86 | ### '.env' file inside './packages/app' 87 | This file is optional, the dev setting is the default. 88 | ``` 89 | GRAPHQL_ENDPOINT=endpoint-your-graphql-server-will-run 90 | ``` 91 | 92 | ## TODO 93 | 1) Add more tooling (things like storybook etc etc) 94 | 2) `yarn upgrade-interactive --latest` 95 | 3) ask apollo team if graphiql can be offline too, now it loads resources from cdn.jsdelivr.net 96 | 97 | ## ARTICLES 98 | - [Graphql explained high level](https://www.youtube.com/watch?v=Oh5oC98ztvI) 99 | - [Paypal Graphql](https://medium.com/paypal-engineering/graphql-a-success-story-for-paypal-checkout-3482f724fb53) 100 | - [Airbnb luxery homes](https://medium.com/airbnb-engineering/how-airbnb-is-moving-10x-faster-at-scale-with-graphql-and-apollo-aa4ec92d69e2) 101 | - [WhatsApp-Clone-server](https://github.com/Urigo/WhatsApp-Clone-server), [WhatsApp-Clone-Client-React](https://github.com/Urigo/WhatsApp-Clone-Client-React) and [tutorial](https://tortilla.academy/tutorial/whatsapp-react/step/1) 102 | - [https://www.graphqlweekly.com/](https://www.graphqlweekly.com/) 103 | - [GraphQL HQ](https://blog.apollographql.com/) 104 | 105 | 106 | ## NON-BELIEVERS 107 | There are always teams resistent to pickup "new" technologies. If they want they are still able to consume us as rest endpoints with the same codebase behind it. 108 | 109 | For example our application also gives the following endpoint: 110 | [locally: /api/get-product/9200000111963040](http://localhost:4000/api/get-product/9200000111963040) or [online demo](https://graphql-server-schiphol.herokuapp.com/api/get-product/9200000111963040) 111 | 112 | See [open-api](./packages/server/swagger.yml) its auto generated with help of [SOFA](https://github.com/Urigo/SOFA) 113 | 114 | 115 | ## VSC plugins 116 | - [vscode-apollo](https://marketplace.visualstudio.com/items?itemName=apollographql.vscode-apollo) for autocomplete in app 117 | - [eslint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) including apollo linting 118 | 119 | 120 | [![Codeship Status for maapteh/graphql-modules-app](https://app.codeship.com/projects/3bf47d90-d61c-0136-0edf-1a5c0fb66462/status?branch=master)](https://graphql-schiphol.herokuapp.com) 121 | 122 | [![This project is using Percy.io for visual regression testing.](https://percy.io/static/images/percy-badge.svg)](https://percy.io/maas38/graphql-workshop) 123 | -------------------------------------------------------------------------------- /apollo.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | client: { 3 | service: { 4 | name: 'maapteh-6450', 5 | localSchemaFile: './packages/server/src/_schema.graphql' 6 | }, 7 | addTypename: false, 8 | excludes: ['**/__tests__/**/*', '**/__mocks__/**/*'], 9 | includes: ['./packages/app/src/**/*.graphql.ts'] 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // jest: https://github.com/facebook/jest/issues/7359 3 | babelrcRoots: ['packages/*'], 4 | } -------------------------------------------------------------------------------- /config/nginx/.gitignore: -------------------------------------------------------------------------------- 1 | local.test+2-key.pem 2 | local.test+2.pem 3 | local.*.pem 4 | -------------------------------------------------------------------------------- /config/nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | # THIS CONTAINER WILL ONLY BE USED DURING DEVELOPMENT 2 | FROM node:dubnium-slim 3 | 4 | ENV NODE_ENV=production 5 | ENV IS_DOCKER=true 6 | 7 | WORKDIR /usr/app 8 | 9 | COPY package.json /usr/app/package.json 10 | COPY yarn.lock /usr/app/yarn.lock 11 | 12 | RUN yarn install 13 | 14 | COPY . /usr/app 15 | 16 | CMD yarn start 17 | -------------------------------------------------------------------------------- /config/nginx/README.md: -------------------------------------------------------------------------------- 1 | # Development nginx setup 2 | 3 | This repository comes with already generated certificates. You can use these or create your own self signed certificates. This nginx will not be used on production. 4 | 5 | ## Create your self signed certificates 6 | The certificates are created using [mkcert](https://github.com/FiloSottile/mkcert) 7 | 8 | - `brew install mkcert` (on Guest network) 9 | - `mkcert -install` 10 | - Create them: `mkcert local.foo.test "*.foo.test" localhost` (in this folder) 11 | - Put certificate in keychain access (cli points where it put cert file) 12 | - *Make sure the name of the certificates are the same as in .gitignore (default names of mkcert). The proxy will take precedence when these files exist.* 13 | - *local.foo.test will only work if you add this to your local Host file.* 14 | -------------------------------------------------------------------------------- /config/nginx/localhost.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC5TCCAc2gAwIBAgIJAMPNzGLMR5RLMA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNV 3 | BAMMCWxvY2FsaG9zdDAeFw0xODA3MDIxMDQxNDdaFw0yODA2MjkxMDQxNDdaMBQx 4 | EjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC 5 | ggEBAK7OYJJhvgznP3jcw3b7BsYHFTW7CXlA2b2ra7mrjX8ns/fpT7VQCdT/8vOX 6 | vf8auPtCNakw70qJBm/3fwd3i/buDCxpxjjzlDF5kKrpdw1f1PdNQsGl5c6BDFOi 7 | qQEn0FwEdY6zERXIpUPXLHobdhf2zRiZU93fl/5h/uYVYDPV/2RBz6Y/PfW/8Aug 8 | yXA0S67+7TalNMK/k0CCPv5UR+5LLZlnfKZJhG5hWQjar8CItpheQlaEl7s8kiMN 9 | GZFCsW+zbz6jgcP8YwznhdBzDifGH1Wrl5JOZdH6vHZv08s3Oy+3qvd5598r4INP 10 | XXAFiQvxEbMjgV5rL3bH9D9nMNUCAwEAAaM6MDgwFAYDVR0RBA0wC4IJbG9jYWxo 11 | b3N0MAsGA1UdDwQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDATANBgkqhkiG9w0B 12 | AQsFAAOCAQEATevbPis+ObkJfqW3eKIK8r2UhtC4G+TaTx3Vh5QwR/3EdwSeUyzF 13 | pfDMcLhRuHVqBzzbT5xhHCyt7g8bQSfNsZTLxEQcb88kYARj7WZ579xX+nP0dxAS 14 | hrBo3jiOvZKS9yz7T4BjuAmegLVPUPHFFG3ZNlLfZQKR9D3RQ/2WecFMZQznUEvl 15 | U7IJ/F+pvuR8LUhmL70RFGvX01L5bl3oFUDdYaCe5M35zVt/vMF73arbcR45dhcD 16 | jQ7bl/ro9+C/gSGJNa+FAumzrZmOJYR5ENqJBr2F+R+mRjpYg6g4SPmGjT+7LAID 17 | nkyGwcD7I1QpXVc8tVRiSkKQpLFNyhVriw== 18 | -----END CERTIFICATE----- 19 | -------------------------------------------------------------------------------- /config/nginx/localhost.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCuzmCSYb4M5z94 3 | 3MN2+wbGBxU1uwl5QNm9q2u5q41/J7P36U+1UAnU//Lzl73/Grj7QjWpMO9KiQZv 4 | 938Hd4v27gwsacY485QxeZCq6XcNX9T3TULBpeXOgQxToqkBJ9BcBHWOsxEVyKVD 5 | 1yx6G3YX9s0YmVPd35f+Yf7mFWAz1f9kQc+mPz31v/ALoMlwNEuu/u02pTTCv5NA 6 | gj7+VEfuSy2ZZ3ymSYRuYVkI2q/AiLaYXkJWhJe7PJIjDRmRQrFvs28+o4HD/GMM 7 | 54XQcw4nxh9Vq5eSTmXR+rx2b9PLNzsvt6r3eeffK+CDT11wBYkL8RGzI4Feay92 8 | x/Q/ZzDVAgMBAAECggEAcTiatDU6s4DUS6Qxtk7BBGJyCmsqp66pWYA+NfQ3obRF 9 | jL1BM16z/5IH+l6+YQ0d4x/vQbbARraZxMu5K0zzCu0EVX/tM9YQljr2yLyOr8ry 10 | VXtlUafyQN607Tbd4DG5cuAwhEzXNBTRdi9YT36Z7sub6+Ljv0GjYNB4GO6fcPKH 11 | TrhFGGOJusJ3clUAWlnnQgVyWjLM1waZjESYSNZMzMES9xR3qpWQKHHhew0fuZl2 12 | pj0rV5khGYcXjpc70vhfuv5yrVPeNLTq1sVOKhjgJ42c18MUj6txEefGwoyGtusn 13 | i+ETKSKaqJ/txJ4+f+XwRHIS0OtNbRDZwtbYOl7XwQKBgQDoNZSzC62jpUHFMHva 14 | Odx9eknMx2rbZBsx6VxPE8qel3WUJRSUHRqrWZlMcM5KJKDuHeMP3MjqSWug/q6M 15 | zSDobwQV+jMGXAa9FV7jI9qBy+0/BgtNFtWH+MYG/gjHxBkBpuB95FJJUdi0gM30 16 | aPOj5zYn826Dm7W5KUG1t+NCfwKBgQDAtzfxwZjTvSsrNovIChYt8/rE9bXR1QUp 17 | Kc6VCKcO7fUno15NYN1wNCE/LPKqFF8lNiPcLj9XEOLiJrFHhJz+Depq2tW7VIua 18 | gpTcSPiYUk7EGisrM5VsrSaqNvWcK3w4PNjrNDe2B2xRhMwUBvJXIywOsC7G/2oP 19 | CAtvu/c6qwKBgGlXnVzYaG570umFBDrM0wUti/tVYFmlAV1UM2dAYEQwC8woQjyr 20 | M2UWoZ/28O7bzRIZBuA0VgVLR4Ni5obDrDEl4+GgfrNc3kW7Qy+iHUeS3s8fi9Lu 21 | D/K+Xf/gENWnVXzVWrRh9x6B/eBtKoG9dwIdKwlWuwUDh543ZDLu+C87AoGBAIgx 22 | 9Aua8lLR4exMREU/O6WGQ7dmnvSIQ3lv3ltdHhNjAFrfDgpJZrWhYc2wCl9Avm0h 23 | 8f3tgT4a5P1GswsEIZ86Xmzd8ybM/UxY9LMpruaXZKsag1+ouPVw+V5aMQIJiWSF 24 | PBgdczHl1RtXapLMxf/nD3/h620fnOi6mrqAcJy5AoGBAOJvxuKicVnu9mqucwY9 25 | HFXjkWVXy1ViMpYa2gwSqXVtKo9okZUroCuQ4qmNqWZg2gfpYnVwiyGMqpb4ELi+ 26 | 7uceDoTyLm4Wgf9A0QTdudoY7lFiQ81ni+w0p9cFzUxCILU6y/o+44boMJX913/I 27 | xjk2ENxqskDowruz8j1CpyWk 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /config/nginx/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-nginx", 3 | "version": "0.0.1", 4 | "license": "UNLICENSED", 5 | "description": "Only used for local development", 6 | "main": "server.js", 7 | "scripts": { 8 | "start": "node server.js" 9 | }, 10 | "dependencies": { 11 | "express": "^4.16.4", 12 | "http-proxy-middleware": "^0.19.1" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /config/nginx/server.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const http = require('http'); 3 | const https = require('https'); 4 | const express = require('express'); 5 | const proxy = require('http-proxy-middleware'); 6 | 7 | let CERT = 'localhost.crt'; 8 | const CERT_SIGNED = 'local.test+2.pem'; 9 | let KEY = 'localhost.key'; 10 | const KEY_SIGNED = 'local.test+2-key.pem'; 11 | 12 | if (fs.existsSync(CERT_SIGNED) && fs.existsSync(CERT_SIGNED)) { 13 | console.log('[PROXY] Own certificates are being used'); 14 | CERT = CERT_SIGNED; 15 | KEY = KEY_SIGNED; 16 | } 17 | 18 | const privateKey = fs.readFileSync(KEY, 'utf8'); 19 | const certificate = fs.readFileSync(CERT, 'utf8'); 20 | 21 | const isDocker = process.env.IS_DOCKER; 22 | 23 | const HTTPS_PORT = 443; 24 | const HTTP_PORT = 3000; 25 | 26 | const WEB = isDocker ? 'web' : 'localhost'; 27 | const API = isDocker ? 'graphql' : 'localhost'; 28 | 29 | const app = express(); 30 | 31 | // Our application: API 32 | app.use( 33 | '/graphql', 34 | proxy({ 35 | target: `http://${API}:4000`, 36 | }), 37 | ); 38 | 39 | // Our application: WEB 40 | app.use( 41 | ['/__webpack_hmr', '/'], 42 | proxy({ 43 | target: `http://${WEB}:4001`, 44 | }), 45 | ); 46 | 47 | const credentials = { 48 | key: privateKey, 49 | cert: certificate, 50 | }; 51 | const httpServer = http.createServer(app); 52 | const httpsServer = https.createServer(credentials, app); 53 | 54 | console.log(`[PROXY] running for: ${isDocker ? 'docker' : 'localhost'}`); 55 | 56 | httpServer.listen(HTTP_PORT, () => { 57 | console.log(`[PROXY] 🚀 http started on port ${HTTP_PORT}`); 58 | }); 59 | 60 | httpsServer.listen(HTTPS_PORT, () => { 61 | console.log(`[PROXY] 🚀 https started on port ${HTTPS_PORT}`); 62 | }); 63 | -------------------------------------------------------------------------------- /config/prettier.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 4, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": true, 7 | "trailingComma": "all", 8 | "bracketSpacing": true, 9 | "jsxBracketSameLine": false, 10 | "arrowParens": "avoid" 11 | } -------------------------------------------------------------------------------- /config/prettierignore: -------------------------------------------------------------------------------- 1 | *.json 2 | *.snap 3 | _*.ts 4 | _*.tsx 5 | -------------------------------------------------------------------------------- /dev.sh: -------------------------------------------------------------------------------- 1 | ./node_modules/.bin/concurrently --names "REACT, REACT-COMPONENTS, GRAPHQL, GRAPHQL-TYPES" \ 2 | -c "blue.bold,gray,cyan.bold,gray" \ 3 | "cd packages/app && yarn dev" \ 4 | "cd packages/app && yarn generate:graphqlcodegen -w" \ 5 | "cd packages/server && yarn dev" \ 6 | "cd packages/server && yarn generate:graphqlcodegen -w" -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | services: 3 | graphql: 4 | image: maapteh.com:443/graphql-server 5 | build: 6 | context: ./packages/server 7 | dockerfile: Dockerfile 8 | args: 9 | node_env: development 10 | environment: 11 | - PORT=4000 12 | ports: 13 | - 4000:4000 14 | volumes: 15 | - ./packages/server:/usr/app 16 | - /usr/app/node_modules 17 | command: yarn dev 18 | 19 | web: 20 | image: maapteh.com:443/graphql-app 21 | build: 22 | context: ./packages/app 23 | dockerfile: Dockerfile 24 | args: 25 | node_env: development 26 | environment: 27 | - PORT=4001 28 | - LOG_LEVEL=debug 29 | - GRAPHQL_ENDPOINT=http://localhost:3000/graphql 30 | ports: 31 | - 4001:4001 32 | volumes: 33 | - ./packages/app:/usr/app 34 | - /usr/app/node_modules 35 | command: yarn dev 36 | 37 | nginx: 38 | image: maapteh.com:443/graphql-nginx 39 | build: 40 | context: ./config/nginx 41 | dockerfile: Dockerfile 42 | args: 43 | node_env: development 44 | IS_DOCKER: "true" 45 | volumes: 46 | - ./config/nginx:/etc/app 47 | - /usr/app/node_modules 48 | links: 49 | - graphql 50 | - web 51 | ports: 52 | - 3000:80 53 | command: yarn start 54 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "lerna": "0.0.4", 3 | "npmClient": "yarn", 4 | "useWorkspaces": true, 5 | "packages": ["packages/*"], 6 | "version": "independent", 7 | "command": { 8 | "publish": { 9 | "ignoreChanges": ["ignored-file", "*.md"], 10 | "message": "chore(release): publish" 11 | }, 12 | "bootstrap": { 13 | "ignore": "component-*", 14 | "npmClientArgs": ["--no-package-lock"] 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sample/app", 3 | "version": "0.0.4", 4 | "private": true, 5 | "license": "MIT", 6 | "engines": { 7 | "node": ">= 10.16.x", 8 | "yarn": ">= 1.16.0" 9 | }, 10 | "scripts": { 11 | "postinstall": "lerna exec -- yarn install && lerna run prepare", 12 | "audit": "lerna run audit", 13 | "start": "./node_modules/.bin/concurrently --names 'REACT, react-autogen, GRAPHQL, graphql-autogen' -c 'blue.bold,gray,cyan.bold,gray' 'NODE_ENV=development yarn workspace @sample/app dev' 'yarn workspace @sample/app generate:graphqlcodegen -w' 'NODE_ENV=development yarn workspace @sample/server dev' 'yarn workspace @sample/server generate:graphqlcodegen -w'", 14 | "dev": "yarn start", 15 | "build": "lerna run build", 16 | "lint": "lerna run lint", 17 | "test": "lerna run test", 18 | "clean": "lerna run clean", 19 | "------ playground ------": "----------------------", 20 | "start:mock-prod": "yarn build && ./node_modules/.bin/concurrently --names 'REACT, GRAPHQL' -c 'blue.bold,cyan.bold' 'NODE_ENV=production cd packages/app && yarn start' 'NODE_ENV=production MOCK_API=on cd packages/server && yarn start' ", 21 | "start:prod": "./node_modules/.bin/concurrently --names 'REACT, GRAPHQL' -c 'blue.bold,cyan.bold' 'yarn workspace @sample/app clean && NODE_ENV=production yarn workspace @sample/app build && NODE_ENV=production yarn workspace @sample/app start' 'NODE_ENV=production yarn workspace @sample/server start'", 22 | "start:clinic": "./node_modules/.bin/concurrently --names 'REACT, GRAPHQL' -c 'blue.bold,cyan.bold' 'NODE_ENV=development yarn workspace @sample/app dev' 'NODE_ENV=production yarn workspace @sample/server start:clinic'", 23 | "test:coverage": "lerna run test:coverage", 24 | "outdated": "yarn outdated", 25 | "upgrade": "yarn upgrade-interactive --latest", 26 | "start:dev:server": "yarn workspace @sample/server start" 27 | }, 28 | "workspaces": [ 29 | "packages/app", 30 | "packages/server" 31 | ], 32 | "resolutions": { 33 | "graphql": "14.5.8" 34 | }, 35 | "devDependencies": { 36 | "@graphql-inspector/actions": "1.26.0", 37 | "concurrently": "5.0.0", 38 | "husky": "3.0.9", 39 | "lerna": "^3.18.4", 40 | "typescript": "3.7.2" 41 | }, 42 | "graphql-inspector": { 43 | "diff": true, 44 | "schema": { 45 | "ref": "master", 46 | "path": "packages/server/src/_schema.graphql" 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/app/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "development": { 4 | "presets": [ 5 | "next/babel" 6 | ] 7 | }, 8 | "production": { 9 | "presets": [ 10 | "next/babel" 11 | ] 12 | }, 13 | "test": { 14 | "presets": [ 15 | [ 16 | "next/babel", 17 | { 18 | "preset-env": { 19 | "modules": "commonjs" 20 | } 21 | } 22 | ] 23 | ] 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/app/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | next.config.js 3 | *.d.ts 4 | *.spec.ts 5 | *.spec.tsx 6 | src/graphql/_generated-* -------------------------------------------------------------------------------- /packages/app/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "env": { 4 | "browser": true, 5 | "node": true 6 | }, 7 | "extends": ["airbnb", "prettier", "prettier/react"], 8 | "plugins": [ 9 | "react-hooks", 10 | "prettier", 11 | "graphql" 12 | ], 13 | "rules": { 14 | "react/jsx-filename-extension": ["error", { "extensions": [".js", ".jsx", ".ts", ".tsx"] }], 15 | "react-hooks/rules-of-hooks": "error", 16 | "react-hooks/exhaustive-deps": "warn", 17 | "import/prefer-default-export": 0, 18 | "react/require-default-props": 0, 19 | "prettier/prettier": ["error"], 20 | "graphql/template-strings": ["error", { 21 | "env": "apollo", 22 | }], 23 | }, 24 | "settings": { 25 | "import/resolver": { 26 | "node": { 27 | "extensions": [".js", ".jsx", ".ts", ".tsx"] 28 | } 29 | } 30 | }, 31 | "globals": { 32 | "React": "writable" 33 | } 34 | } -------------------------------------------------------------------------------- /packages/app/.graphqlconfig: -------------------------------------------------------------------------------- 1 | { 2 | "schemaPath": "../server/src/_schema.graphql", 3 | "extensions": { 4 | "endpoints": { 5 | "dev": "http://localhost:4000/graphql" 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /packages/app/.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "./{src|pages}/**/*.{ts,tsx}": ["yarn lint --fix", "git add"], 3 | "./src/graphql/_generated-schema.graphql": ["yarn inspector:validate"] 4 | } -------------------------------------------------------------------------------- /packages/app/.nmprc: -------------------------------------------------------------------------------- 1 | loglevel = warn 2 | cache=.npm/cache 3 | tmp=.npm/tmp 4 | save-exact = true 5 | -------------------------------------------------------------------------------- /packages/app/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 4, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": true, 7 | "trailingComma": "all", 8 | "bracketSpacing": true, 9 | "jsxBracketSameLine": false 10 | } -------------------------------------------------------------------------------- /packages/app/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:dubnium-slim 2 | 3 | ENV NODE_ENV=production 4 | 5 | WORKDIR /usr/app 6 | 7 | COPY package.json /usr/app/package.json 8 | COPY yarn.lock /usr/app/yarn.lock 9 | 10 | RUN yarn install 11 | 12 | COPY . /usr/app 13 | 14 | RUN yarn build 15 | 16 | CMD yarn start 17 | -------------------------------------------------------------------------------- /packages/app/README.md: -------------------------------------------------------------------------------- 1 | # NextJS Apollo GraphQL 2 | 3 | ## Introduction 4 | This simple application is created to show the complete setup of our Apollo GraphQL [endpoint](../server/README.md) created with GraphQL-Modules, with this isomorphic application consuming it. 5 | 6 | ## Pre-requisites 7 | - `yarn` (not needed when you installed from root) 8 | 9 | ## DEVELOPMENT 10 | 1. Be sure to run the [server](../server/README.md) first 11 | 2. tab 1: `generate:graphqlcodegen -w` 12 | 3. tab 2: `yarn dev` 13 | 14 | 15 | ## GRAPHQL TOOLS 16 | 17 | - `yarn inspector:validate` see if all queries are done correctly 18 | - `yarn inspector:coverage` see what types from the schema are actually used in this application 19 | 20 | ## TEST 21 | - `yarn test` 22 | 23 | ## VISUAL VALIDATION 24 | - add '@percy/script' to the repo and try it out. Export your PERCY_TOKEN before running the command below 25 | - `yarn snapshots` with the PERCY_TOKEN it uploads the results -------------------------------------------------------------------------------- /packages/app/codegen.yml: -------------------------------------------------------------------------------- 1 | schema: 2 | - '../server/src/_schema.graphql' 3 | overwrite: true 4 | documents: [ 5 | './src/modules/**/*.graphql.ts', 6 | './src/components/**/*.graphql.ts', 7 | './src/graphql/fragments/*.graphql.ts' 8 | ] 9 | config: {} 10 | generates: 11 | src/graphql/_generated-fragment-types.ts: 12 | plugins: 13 | - "fragment-matcher" 14 | src/graphql/_generated-types.ts: 15 | plugins: 16 | - add: "/** eslint-disable */\n/** AUTO GENERATED, DO NOT EDIT OVERHERE */" 17 | - typescript 18 | - typescript-operations 19 | src/graphql/_generated-hooks.tsx: 20 | plugins: 21 | - add: "/** eslint-disable */\n/** AUTO GENERATED, DO NOT EDIT OVERHERE */" 22 | - typescript 23 | - typescript-operations 24 | - typescript-react-apollo 25 | config: 26 | reactApolloVersion: 3 27 | reactApolloImportFrom: '@apollo/react-hooks' 28 | withComponent: false 29 | withHOC: false 30 | withHooks: true 31 | src/graphql/_generated-schema.graphql: 32 | plugins: 33 | - schema-ast 34 | require: 35 | - "ts-node/register/transpile-only" 36 | -------------------------------------------------------------------------------- /packages/app/globals.d.ts: -------------------------------------------------------------------------------- 1 | import "@testing-library/jest-dom/extend-expect"; 2 | 3 | declare module NodeJS { 4 | export interface Global { 5 | document: Document; 6 | window: Window; 7 | fetch: any; 8 | } 9 | } 10 | 11 | declare global { 12 | namespace NodeJS { 13 | interface Global { 14 | document: Document; 15 | window: Window; 16 | fetch: any; 17 | navigator: Navigator; 18 | } 19 | } 20 | } 21 | 22 | declare var global: Global; -------------------------------------------------------------------------------- /packages/app/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transform: { 3 | '^.+\\.tsx?$': 'ts-jest', 4 | }, 5 | testRegex: '(/(components|modules)/.*(\\.|/)(test|spec))\\.(tsx?|ts?)$', 6 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], 7 | modulePathIgnorePatterns: ['.next'], 8 | moduleNameMapper: { 9 | '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': 10 | '/test/__mocks__/fileMock.js', 11 | '\\.(css|scss)$': 'identity-obj-proxy', 12 | }, 13 | collectCoverage: true, 14 | coverageReporters: ['json', 'lcov', 'text', 'text-summary'], 15 | setupFilesAfterEnv: ['@testing-library/jest-dom/extend-expect'], 16 | globals: { 17 | 'ts-jest': { 18 | diagnostics: false, 19 | babelConfig: '.babelrc', 20 | tsConfig: './jest.tsconfig.json', 21 | }, 22 | }, 23 | rootDir: process.cwd(), 24 | }; 25 | -------------------------------------------------------------------------------- /packages/app/jest.tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "esnext", 5 | "jsx": "react", 6 | "sourceMap": false, 7 | "experimentalDecorators": true, 8 | "noImplicitUseStrict": true, 9 | "removeComments": true, 10 | "moduleResolution": "node", 11 | "lib": ["es2017", "dom"], 12 | "typeRoots": ["node_modules/@types", "src/@types"], 13 | "types": ["jest"], 14 | "esModuleInterop": true 15 | }, 16 | "exclude": ["node_modules", "out", ".next"] 17 | } -------------------------------------------------------------------------------- /packages/app/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | declare module '*.scss' { 5 | const styles: { [className: string]: string }; 6 | export default styles; 7 | } 8 | -------------------------------------------------------------------------------- /packages/app/next.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const withSass = require('@zeit/next-sass'); 3 | 4 | const localIdentName = 5 | process.env.NODE_ENV === 'development' 6 | ? '[local]__[hash:base64:5]' 7 | : '__[hash:base64:5]'; 8 | 9 | module.exports = withSass({ 10 | cssModules: true, 11 | cssLoaderOptions: { 12 | localIdentName, 13 | importLoaders: 1, 14 | }, 15 | webpack: config => { 16 | config.module.rules.map(item => { 17 | const loader = item.use && item.use[0] && item.use[0].loader; 18 | if (loader === 'css-loader/locals') { 19 | item.use.unshift({ 20 | loader: 'dts-css-modules-loader', 21 | options: { 22 | namedExport: true, 23 | banner: '// This file is generated automatically', 24 | }, 25 | }); 26 | } 27 | }); 28 | 29 | config.plugins.push( 30 | new webpack.DefinePlugin({ 31 | 'process.env.GRAPHQL_ENDPOINT': JSON.stringify( 32 | process.env.GRAPHQL_ENDPOINT, 33 | ), 34 | 'process.env.npm_package_version': JSON.stringify( 35 | process.env.npm_package_version, 36 | ), 37 | }), 38 | ); 39 | 40 | return config; 41 | }, 42 | generateEtags: false, 43 | }); 44 | -------------------------------------------------------------------------------- /packages/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sample/app", 3 | "version": "0.0.4", 4 | "author": "", 5 | "license": "MIT", 6 | "main": "src/index.ts", 7 | "repository": { 8 | "type": "git", 9 | "url": "maapteh/graphql-modules-app/packages/graphql-app" 10 | }, 11 | "engines": { 12 | "node": ">= 10.16.x", 13 | "yarn": ">= 1.16.0" 14 | }, 15 | "scripts": { 16 | "prepare": "yarn build", 17 | "precommit": "lint-staged", 18 | "audit": "yarn audit", 19 | "dev": "next -p 4001", 20 | "build": "tsc -v && next build", 21 | "start": "PORT=$PORT NODE_ENV=production node --optimize_for_size --max_old_space_size=920 --gc_interval=100 server.js", 22 | "start:local": "NODE_ENV=production next start -p 4001", 23 | "test": "jest --config=jest.config.js", 24 | "test:coverage": "yarn test --verbose --coverage", 25 | "test:ci": "yarn run coverage -- --ci --maxWorkers=2 --reporters=default --reporters=jest-junit", 26 | "lint": "eslint './{src,pages}/**/*.{ts,tsx}'", 27 | "clean": "rm -rf 'dist' && rm -rf '.next'", 28 | "generate:graphqlcodegen": "graphql-codegen", 29 | "inspector:validate": "graphql-inspector validate src/graphql/_generated-hooks.tsx src/graphql/_generated-schema.graphql --require ts-node/register", 30 | "inspector:coverage": "graphql-inspector coverage src/graphql/_generated-hooks.tsx src/graphql/_generated-schema.graphql --require ts-node/register", 31 | "snapshots": "percy exec -- node test/visual-regression/index.js" 32 | }, 33 | "resolutions": { 34 | "graphql": "14.5.8" 35 | }, 36 | "dependencies": { 37 | "@apollo/react-hooks": "3.1.3", 38 | "@apollo/react-ssr": "3.1.3", 39 | "@apollo/react-testing": "3.1.3", 40 | "@mpth/react-in-view": "1.0.1", 41 | "@mpth/react-no-ssr": "1.0.0", 42 | "@types/react": "16.9.11", 43 | "@zeit/next-css": "1.0.1", 44 | "@zeit/next-sass": "1.0.1", 45 | "apollo-cache-inmemory": "1.6.3", 46 | "apollo-client": "2.6.4", 47 | "apollo-link": "1.2.13", 48 | "apollo-link-batch-http": "1.2.13", 49 | "apollo-link-error": "1.1.12", 50 | "apollo-link-http": "1.5.16", 51 | "apollo-utilities": "1.3.2", 52 | "classnames": "2.2.6", 53 | "express": "4.17.1", 54 | "graphql": "14.5.8", 55 | "graphql-tag": "2.10.1", 56 | "isomorphic-unfetch": "3.0.0", 57 | "next": "9.1.3", 58 | "node-sass": "4.13.0", 59 | "prettier": "1.19.1", 60 | "prop-types": "15.7.2", 61 | "react": "16.11.0", 62 | "react-content-loader": "4.3.3", 63 | "react-dom": "16.11.0", 64 | "react-intersection-observer": "8.25.1", 65 | "ts-node": "8.4.1", 66 | "typescript": "3.7.2", 67 | "webpack": "4.41.2" 68 | }, 69 | "devDependencies": { 70 | "@graphql-codegen/add": "1.8.3", 71 | "@graphql-codegen/cli": "1.8.3", 72 | "@graphql-codegen/fragment-matcher": "1.8.3", 73 | "@graphql-codegen/schema-ast": "1.8.3", 74 | "@graphql-codegen/time": "1.8.3", 75 | "@graphql-codegen/typescript-operations": "1.8.3", 76 | "@graphql-codegen/typescript-react-apollo": "1.8.3", 77 | "@graphql-inspector/cli": "1.26.0", 78 | "@testing-library/jest-dom": "4.2.3", 79 | "@testing-library/react": "9.3.2", 80 | "@testing-library/react-hooks": "3.2.1", 81 | "@types/jest": "24.0.22", 82 | "babel-eslint": "10.0.3", 83 | "babel-jest": "24.9.0", 84 | "dts-css-modules-loader": "1.0.1", 85 | "eslint": "6.6.0", 86 | "eslint-config-airbnb": "18.0.1", 87 | "eslint-config-prettier": "6.5.0", 88 | "eslint-plugin-graphql": "^3.0.3", 89 | "eslint-plugin-import": "2.18.2", 90 | "eslint-plugin-jsx-a11y": "6.2.3", 91 | "eslint-plugin-prettier": "3.1.1", 92 | "eslint-plugin-react": "7.16.0", 93 | "eslint-plugin-react-hooks": "2.2.0", 94 | "identity-obj-proxy": "3.0.0", 95 | "jest": "24.9.0", 96 | "lint-staged": "9.4.2", 97 | "mq-polyfill": "1.1.8", 98 | "react-test-renderer": "16.11.0", 99 | "ts-jest": "24.1.0" 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /packages/app/public/static/bol.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /packages/app/public/static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maapteh/graphql-modules-app/e5da8544bbaaaefa04f6bd2609bc708e6a1ced8e/packages/app/public/static/favicon.png -------------------------------------------------------------------------------- /packages/app/public/static/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 8 | 9 | 10 | 14 | 18 | 21 | 22 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /packages/app/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const next = require('next'); 3 | const dev = process.env.NODE_ENV !== 'production'; 4 | const port = parseInt(process.env.PORT, 10) || 4001; 5 | const app = next({ dev }); 6 | const handler = app.getRequestHandler(); 7 | 8 | app.prepare().then(() => { 9 | const server = express(); 10 | 11 | server.get('/product/:id', (req, res) => { 12 | const actualPage = '/product'; 13 | const queryParams = { id: req.params.id }; 14 | app.render(req, res, actualPage, queryParams); 15 | }); 16 | 17 | server.get('*', (req, res) => { 18 | return handler(req, res); 19 | }); 20 | 21 | server.listen(port, err => { 22 | if (err) throw err; 23 | console.log(`🚀 REACT at http://localhost:${port}`); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /packages/app/src/elements/image/image.scss: -------------------------------------------------------------------------------- 1 | .block { 2 | float: left; 3 | overflow: hidden; 4 | box-sizing: content-box; 5 | width: 168px; 6 | height: 209px; 7 | padding: 0 18px 14px 0; 8 | line-height: 0; 9 | } 10 | 11 | .hide { 12 | opacity:0; 13 | transition: opacity 200ms ease-in; 14 | } 15 | 16 | .img { 17 | width: 100%; 18 | } 19 | 20 | .appear { 21 | opacity: 1; 22 | } -------------------------------------------------------------------------------- /packages/app/src/elements/image/image.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | import { useInView } from 'react-intersection-observer'; 4 | import style from './image.scss'; 5 | 6 | // TODO: add aspect ratio's 7 | 8 | type Props = { 9 | url: string; 10 | title?: string; 11 | rootMargin?: string; 12 | instantImage?: boolean; 13 | }; 14 | 15 | export const Image = ({ 16 | url, 17 | title = '', 18 | rootMargin, 19 | instantImage = false, 20 | }: Props) => { 21 | // This set of values serves to grow or shrink each side of the root element's bounding box before computing intersections 22 | const margin = 23 | rootMargin && /((((.\d*)?(px))){4})/.test(rootMargin) 24 | ? rootMargin 25 | : '20px 0px 280px 0px'; 26 | const [image, setImage] = React.useState(); 27 | const [ref, inView] = useInView({ 28 | threshold: 0, 29 | rootMargin: margin, 30 | }); 31 | 32 | const css = classNames(style.block, style.hide, image && `${style.appear}`); 33 | const imageComponent = {title}; 34 | 35 | if (instantImage) { 36 | return
{imageComponent}
; 37 | } 38 | 39 | if (inView && !image) { 40 | setImage(url); 41 | } 42 | 43 | return ( 44 |
45 | {image && imageComponent} 46 |
47 | ); 48 | }; 49 | -------------------------------------------------------------------------------- /packages/app/src/elements/logo-bol/logo-bol.tsx: -------------------------------------------------------------------------------- 1 | export const LogoBol = () => { 2 | return ( 3 | BOL.com 9 | ); 10 | }; 11 | -------------------------------------------------------------------------------- /packages/app/src/graphql/_generated-fragment-types.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface IntrospectionResultData { 3 | __schema: { 4 | types: { 5 | kind: string; 6 | name: string; 7 | possibleTypes: { 8 | name: string; 9 | }[]; 10 | }[]; 11 | }; 12 | } 13 | const result: IntrospectionResultData = { 14 | "__schema": { 15 | "types": [] 16 | } 17 | }; 18 | export default result; 19 | -------------------------------------------------------------------------------- /packages/app/src/graphql/_generated-hooks.tsx: -------------------------------------------------------------------------------- 1 | /** eslint-disable */ 2 | /** AUTO GENERATED, DO NOT EDIT OVERHERE */ 3 | import gql from 'graphql-tag'; 4 | import * as ApolloReactCommon from '@apollo/react-common'; 5 | import * as ApolloReactHooks from '@apollo/react-hooks'; 6 | export type Maybe = T | null; 7 | 8 | /** All built-in and custom scalars, mapped to their actual values */ 9 | export type Scalars = { 10 | ID: string, 11 | String: string, 12 | Boolean: boolean, 13 | Int: number, 14 | Float: number, 15 | }; 16 | 17 | 18 | export enum CacheControlScope { 19 | Public = 'PUBLIC', 20 | Private = 'PRIVATE' 21 | } 22 | 23 | /** Show Offer for a Product */ 24 | export type Offer = { 25 | __typename?: 'Offer', 26 | id: Scalars['String'], 27 | condition: Scalars['String'], 28 | price: Scalars['Float'], 29 | availabilityCode: Scalars['String'], 30 | availabilityDescription: Scalars['String'], 31 | seller: OfferSeller, 32 | }; 33 | 34 | export type OfferSeller = { 35 | __typename?: 'OfferSeller', 36 | id: Scalars['String'], 37 | sellerType: Scalars['String'], 38 | displayName: Scalars['String'], 39 | }; 40 | 41 | export type Product = { 42 | __typename?: 'Product', 43 | offer?: Maybe, 44 | id: Scalars['String'], 45 | ean?: Maybe, 46 | title: Scalars['String'], 47 | specsTag?: Maybe, 48 | summary?: Maybe, 49 | rating?: Maybe, 50 | shortDescription?: Maybe, 51 | urls?: Maybe>>, 52 | images?: Maybe>>, 53 | /** TODO: question does 'offerData' belong here? */ 54 | offerData?: Maybe, 55 | parentCategoryPaths?: Maybe, 56 | }; 57 | 58 | export type ProductImage = { 59 | __typename?: 'ProductImage', 60 | type?: Maybe, 61 | key?: Maybe, 62 | url?: Maybe, 63 | }; 64 | 65 | export type ProductOfferData = { 66 | __typename?: 'ProductOfferData', 67 | bolCom?: Maybe, 68 | nonProfessionalSellers?: Maybe, 69 | professionalSellers?: Maybe, 70 | offers?: Maybe>>, 71 | }; 72 | 73 | /** TODO: is this Offer? Schema usage validator should pick this up */ 74 | export type ProductOfferDataOffer = { 75 | __typename?: 'ProductOfferDataOffer', 76 | id: Scalars['String'], 77 | condition: Scalars['String'], 78 | price: Scalars['Float'], 79 | listPrice?: Maybe, 80 | availabilityCode: Scalars['String'], 81 | availabilityDescription: Scalars['String'], 82 | comment: Scalars['String'], 83 | seller: ProductSeller, 84 | bestOffer?: Maybe, 85 | releaseDate?: Maybe, 86 | }; 87 | 88 | export type ProductParentCategory = { 89 | __typename?: 'ProductParentCategory', 90 | id?: Maybe, 91 | name?: Maybe, 92 | }; 93 | 94 | export type ProductParentCategoryPaths = { 95 | __typename?: 'ProductParentCategoryPaths', 96 | parentCategories?: Maybe, 97 | }; 98 | 99 | /** Products for a specific category, model is taken as is from bol.com */ 100 | export type Products = { 101 | __typename?: 'Products', 102 | products?: Maybe>>, 103 | schemaVersion?: Maybe, 104 | totalResultSize?: Maybe, 105 | originalRequest?: Maybe, 106 | }; 107 | 108 | export type ProductSeller = { 109 | __typename?: 'ProductSeller', 110 | id: Scalars['String'], 111 | sellerType: Scalars['String'], 112 | displayName: Scalars['String'], 113 | url?: Maybe, 114 | topSeller: Scalars['Boolean'], 115 | useWarrantyRepairConditions: Scalars['Boolean'], 116 | }; 117 | 118 | export type ProductsOriginalRequest = { 119 | __typename?: 'ProductsOriginalRequest', 120 | category?: Maybe>>, 121 | }; 122 | 123 | export type ProductsOriginalRequestCategory = { 124 | __typename?: 'ProductsOriginalRequestCategory', 125 | id?: Maybe, 126 | name?: Maybe, 127 | productCount?: Maybe, 128 | }; 129 | 130 | export type ProductUrls = { 131 | __typename?: 'ProductUrls', 132 | key?: Maybe, 133 | value?: Maybe, 134 | }; 135 | 136 | export type Query = { 137 | __typename?: 'Query', 138 | /** Get best offer for specific product */ 139 | getOffer?: Maybe, 140 | /** Get all products for a specific list */ 141 | getProducts?: Maybe, 142 | /** Get single product */ 143 | getProduct?: Maybe, 144 | }; 145 | 146 | 147 | export type QueryGetOfferArgs = { 148 | id: Scalars['String'] 149 | }; 150 | 151 | 152 | export type QueryGetProductsArgs = { 153 | id: Scalars['String'] 154 | }; 155 | 156 | 157 | export type QueryGetProductArgs = { 158 | id: Scalars['String'] 159 | }; 160 | 161 | export type ProductFragment = ( 162 | { __typename?: 'Product' } 163 | & Pick 164 | & { images: Maybe 167 | )>>>, urls: Maybe 170 | )>>> } 171 | ); 172 | 173 | export type GetOfferQueryVariables = { 174 | id: Scalars['String'] 175 | }; 176 | 177 | 178 | export type GetOfferQuery = ( 179 | { __typename?: 'Query' } 180 | & { getOffer: Maybe<( 181 | { __typename?: 'Offer' } 182 | & Pick 183 | & { seller: ( 184 | { __typename?: 'OfferSeller' } 185 | & Pick 186 | ) } 187 | )> } 188 | ); 189 | 190 | export type GetProductQueryVariables = { 191 | id: Scalars['String'] 192 | }; 193 | 194 | 195 | export type GetProductQuery = ( 196 | { __typename?: 'Query' } 197 | & { getProduct: Maybe<( 198 | { __typename?: 'Product' } 199 | & ProductFragment 200 | )> } 201 | ); 202 | 203 | export type GetProductsQueryVariables = { 204 | id: Scalars['String'] 205 | }; 206 | 207 | 208 | export type GetProductsQuery = ( 209 | { __typename?: 'Query' } 210 | & { getProducts: Maybe<( 211 | { __typename?: 'Products' } 212 | & { products: Maybe>> } 216 | )> } 217 | ); 218 | 219 | export const ProductFragmentDoc = gql` 220 | fragment product on Product { 221 | id 222 | title 223 | rating 224 | shortDescription 225 | images { 226 | key 227 | url 228 | } 229 | urls { 230 | key 231 | value 232 | } 233 | } 234 | `; 235 | export const GetOfferDocument = gql` 236 | query getOffer($id: String!) { 237 | getOffer(id: $id) { 238 | id 239 | price 240 | availabilityDescription 241 | seller { 242 | displayName 243 | } 244 | } 245 | } 246 | `; 247 | 248 | /** 249 | * __useGetOfferQuery__ 250 | * 251 | * To run a query within a React component, call `useGetOfferQuery` and pass it any options that fit your needs. 252 | * When your component renders, `useGetOfferQuery` returns an object from Apollo Client that contains loading, error, and data properties 253 | * you can use to render your UI. 254 | * 255 | * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; 256 | * 257 | * @example 258 | * const { data, loading, error } = useGetOfferQuery({ 259 | * variables: { 260 | * id: // value for 'id' 261 | * }, 262 | * }); 263 | */ 264 | export function useGetOfferQuery(baseOptions?: ApolloReactHooks.QueryHookOptions) { 265 | return ApolloReactHooks.useQuery(GetOfferDocument, baseOptions); 266 | } 267 | export function useGetOfferLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptions) { 268 | return ApolloReactHooks.useLazyQuery(GetOfferDocument, baseOptions); 269 | } 270 | export type GetOfferQueryHookResult = ReturnType; 271 | export type GetOfferLazyQueryHookResult = ReturnType; 272 | export type GetOfferQueryResult = ApolloReactCommon.QueryResult; 273 | export const GetProductDocument = gql` 274 | query getProduct($id: String!) { 275 | getProduct(id: $id) { 276 | ...product 277 | } 278 | } 279 | ${ProductFragmentDoc}`; 280 | 281 | /** 282 | * __useGetProductQuery__ 283 | * 284 | * To run a query within a React component, call `useGetProductQuery` and pass it any options that fit your needs. 285 | * When your component renders, `useGetProductQuery` returns an object from Apollo Client that contains loading, error, and data properties 286 | * you can use to render your UI. 287 | * 288 | * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; 289 | * 290 | * @example 291 | * const { data, loading, error } = useGetProductQuery({ 292 | * variables: { 293 | * id: // value for 'id' 294 | * }, 295 | * }); 296 | */ 297 | export function useGetProductQuery(baseOptions?: ApolloReactHooks.QueryHookOptions) { 298 | return ApolloReactHooks.useQuery(GetProductDocument, baseOptions); 299 | } 300 | export function useGetProductLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptions) { 301 | return ApolloReactHooks.useLazyQuery(GetProductDocument, baseOptions); 302 | } 303 | export type GetProductQueryHookResult = ReturnType; 304 | export type GetProductLazyQueryHookResult = ReturnType; 305 | export type GetProductQueryResult = ApolloReactCommon.QueryResult; 306 | export const GetProductsDocument = gql` 307 | query getProducts($id: String!) { 308 | getProducts(id: $id) { 309 | products { 310 | ...product 311 | } 312 | } 313 | } 314 | ${ProductFragmentDoc}`; 315 | 316 | /** 317 | * __useGetProductsQuery__ 318 | * 319 | * To run a query within a React component, call `useGetProductsQuery` and pass it any options that fit your needs. 320 | * When your component renders, `useGetProductsQuery` returns an object from Apollo Client that contains loading, error, and data properties 321 | * you can use to render your UI. 322 | * 323 | * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; 324 | * 325 | * @example 326 | * const { data, loading, error } = useGetProductsQuery({ 327 | * variables: { 328 | * id: // value for 'id' 329 | * }, 330 | * }); 331 | */ 332 | export function useGetProductsQuery(baseOptions?: ApolloReactHooks.QueryHookOptions) { 333 | return ApolloReactHooks.useQuery(GetProductsDocument, baseOptions); 334 | } 335 | export function useGetProductsLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptions) { 336 | return ApolloReactHooks.useLazyQuery(GetProductsDocument, baseOptions); 337 | } 338 | export type GetProductsQueryHookResult = ReturnType; 339 | export type GetProductsLazyQueryHookResult = ReturnType; 340 | export type GetProductsQueryResult = ApolloReactCommon.QueryResult; -------------------------------------------------------------------------------- /packages/app/src/graphql/_generated-schema.graphql: -------------------------------------------------------------------------------- 1 | """ auto generated """ 2 | directive @cacheControl(maxAge: Int, scope: CacheControlScope) on OBJECT | FIELD_DEFINITION | INTERFACE 3 | 4 | enum CacheControlScope { 5 | PUBLIC 6 | PRIVATE 7 | } 8 | 9 | """Show Offer for a Product""" 10 | type Offer { 11 | id: String! 12 | condition: String! 13 | price: Float! 14 | availabilityCode: String! 15 | availabilityDescription: String! 16 | seller: OfferSeller! 17 | } 18 | 19 | type OfferSeller { 20 | id: String! 21 | sellerType: String! 22 | displayName: String! 23 | } 24 | 25 | type Product { 26 | offer: Offer 27 | id: String! 28 | ean: String 29 | title: String! 30 | specsTag: String 31 | summary: String 32 | rating: Int 33 | shortDescription: String 34 | urls: [ProductUrls] 35 | images: [ProductImage] 36 | 37 | """TODO: question does 'offerData' belong here?""" 38 | offerData: ProductOfferData 39 | parentCategoryPaths: ProductParentCategoryPaths 40 | } 41 | 42 | type ProductImage { 43 | type: String 44 | key: String 45 | url: String 46 | } 47 | 48 | type ProductOfferData { 49 | bolCom: Int 50 | nonProfessionalSellers: Int 51 | professionalSellers: Int 52 | offers: [ProductOfferDataOffer] 53 | } 54 | 55 | """TODO: is this Offer? Schema usage validator should pick this up""" 56 | type ProductOfferDataOffer { 57 | id: String! 58 | condition: String! 59 | price: Float! 60 | listPrice: Float 61 | availabilityCode: String! 62 | availabilityDescription: String! 63 | comment: String! 64 | seller: ProductSeller! 65 | bestOffer: Boolean 66 | releaseDate: String 67 | } 68 | 69 | type ProductParentCategory { 70 | id: String 71 | name: String 72 | } 73 | 74 | type ProductParentCategoryPaths { 75 | parentCategories: ProductParentCategory 76 | } 77 | 78 | """Products for a specific category, model is taken as is from bol.com""" 79 | type Products { 80 | products: [Product] 81 | schemaVersion: String 82 | totalResultSize: Int 83 | originalRequest: ProductsOriginalRequest 84 | } 85 | 86 | type ProductSeller { 87 | id: String! 88 | sellerType: String! 89 | displayName: String! 90 | url: String 91 | topSeller: Boolean! 92 | useWarrantyRepairConditions: Boolean! 93 | } 94 | 95 | type ProductsOriginalRequest { 96 | category: [ProductsOriginalRequestCategory] 97 | } 98 | 99 | type ProductsOriginalRequestCategory { 100 | id: Int 101 | name: String 102 | productCount: Int 103 | } 104 | 105 | type ProductUrls { 106 | key: String 107 | value: String 108 | } 109 | 110 | type Query { 111 | """Get best offer for specific product""" 112 | getOffer(id: String!): Offer 113 | 114 | """Get all products for a specific list""" 115 | getProducts(id: String!): Products 116 | 117 | """Get single product""" 118 | getProduct(id: String!): Product 119 | } 120 | -------------------------------------------------------------------------------- /packages/app/src/graphql/_generated-types.ts: -------------------------------------------------------------------------------- 1 | /** eslint-disable */ 2 | /** AUTO GENERATED, DO NOT EDIT OVERHERE */ 3 | export type Maybe = T | null; 4 | 5 | /** All built-in and custom scalars, mapped to their actual values */ 6 | export type Scalars = { 7 | ID: string, 8 | String: string, 9 | Boolean: boolean, 10 | Int: number, 11 | Float: number, 12 | }; 13 | 14 | 15 | export enum CacheControlScope { 16 | Public = 'PUBLIC', 17 | Private = 'PRIVATE' 18 | } 19 | 20 | /** Show Offer for a Product */ 21 | export type Offer = { 22 | __typename?: 'Offer', 23 | id: Scalars['String'], 24 | condition: Scalars['String'], 25 | price: Scalars['Float'], 26 | availabilityCode: Scalars['String'], 27 | availabilityDescription: Scalars['String'], 28 | seller: OfferSeller, 29 | }; 30 | 31 | export type OfferSeller = { 32 | __typename?: 'OfferSeller', 33 | id: Scalars['String'], 34 | sellerType: Scalars['String'], 35 | displayName: Scalars['String'], 36 | }; 37 | 38 | export type Product = { 39 | __typename?: 'Product', 40 | offer?: Maybe, 41 | id: Scalars['String'], 42 | ean?: Maybe, 43 | title: Scalars['String'], 44 | specsTag?: Maybe, 45 | summary?: Maybe, 46 | rating?: Maybe, 47 | shortDescription?: Maybe, 48 | urls?: Maybe>>, 49 | images?: Maybe>>, 50 | /** TODO: question does 'offerData' belong here? */ 51 | offerData?: Maybe, 52 | parentCategoryPaths?: Maybe, 53 | }; 54 | 55 | export type ProductImage = { 56 | __typename?: 'ProductImage', 57 | type?: Maybe, 58 | key?: Maybe, 59 | url?: Maybe, 60 | }; 61 | 62 | export type ProductOfferData = { 63 | __typename?: 'ProductOfferData', 64 | bolCom?: Maybe, 65 | nonProfessionalSellers?: Maybe, 66 | professionalSellers?: Maybe, 67 | offers?: Maybe>>, 68 | }; 69 | 70 | /** TODO: is this Offer? Schema usage validator should pick this up */ 71 | export type ProductOfferDataOffer = { 72 | __typename?: 'ProductOfferDataOffer', 73 | id: Scalars['String'], 74 | condition: Scalars['String'], 75 | price: Scalars['Float'], 76 | listPrice?: Maybe, 77 | availabilityCode: Scalars['String'], 78 | availabilityDescription: Scalars['String'], 79 | comment: Scalars['String'], 80 | seller: ProductSeller, 81 | bestOffer?: Maybe, 82 | releaseDate?: Maybe, 83 | }; 84 | 85 | export type ProductParentCategory = { 86 | __typename?: 'ProductParentCategory', 87 | id?: Maybe, 88 | name?: Maybe, 89 | }; 90 | 91 | export type ProductParentCategoryPaths = { 92 | __typename?: 'ProductParentCategoryPaths', 93 | parentCategories?: Maybe, 94 | }; 95 | 96 | /** Products for a specific category, model is taken as is from bol.com */ 97 | export type Products = { 98 | __typename?: 'Products', 99 | products?: Maybe>>, 100 | schemaVersion?: Maybe, 101 | totalResultSize?: Maybe, 102 | originalRequest?: Maybe, 103 | }; 104 | 105 | export type ProductSeller = { 106 | __typename?: 'ProductSeller', 107 | id: Scalars['String'], 108 | sellerType: Scalars['String'], 109 | displayName: Scalars['String'], 110 | url?: Maybe, 111 | topSeller: Scalars['Boolean'], 112 | useWarrantyRepairConditions: Scalars['Boolean'], 113 | }; 114 | 115 | export type ProductsOriginalRequest = { 116 | __typename?: 'ProductsOriginalRequest', 117 | category?: Maybe>>, 118 | }; 119 | 120 | export type ProductsOriginalRequestCategory = { 121 | __typename?: 'ProductsOriginalRequestCategory', 122 | id?: Maybe, 123 | name?: Maybe, 124 | productCount?: Maybe, 125 | }; 126 | 127 | export type ProductUrls = { 128 | __typename?: 'ProductUrls', 129 | key?: Maybe, 130 | value?: Maybe, 131 | }; 132 | 133 | export type Query = { 134 | __typename?: 'Query', 135 | /** Get best offer for specific product */ 136 | getOffer?: Maybe, 137 | /** Get all products for a specific list */ 138 | getProducts?: Maybe, 139 | /** Get single product */ 140 | getProduct?: Maybe, 141 | }; 142 | 143 | 144 | export type QueryGetOfferArgs = { 145 | id: Scalars['String'] 146 | }; 147 | 148 | 149 | export type QueryGetProductsArgs = { 150 | id: Scalars['String'] 151 | }; 152 | 153 | 154 | export type QueryGetProductArgs = { 155 | id: Scalars['String'] 156 | }; 157 | 158 | export type ProductFragment = ( 159 | { __typename?: 'Product' } 160 | & Pick 161 | & { images: Maybe 164 | )>>>, urls: Maybe 167 | )>>> } 168 | ); 169 | 170 | export type GetOfferQueryVariables = { 171 | id: Scalars['String'] 172 | }; 173 | 174 | 175 | export type GetOfferQuery = ( 176 | { __typename?: 'Query' } 177 | & { getOffer: Maybe<( 178 | { __typename?: 'Offer' } 179 | & Pick 180 | & { seller: ( 181 | { __typename?: 'OfferSeller' } 182 | & Pick 183 | ) } 184 | )> } 185 | ); 186 | 187 | export type GetProductQueryVariables = { 188 | id: Scalars['String'] 189 | }; 190 | 191 | 192 | export type GetProductQuery = ( 193 | { __typename?: 'Query' } 194 | & { getProduct: Maybe<( 195 | { __typename?: 'Product' } 196 | & ProductFragment 197 | )> } 198 | ); 199 | 200 | export type GetProductsQueryVariables = { 201 | id: Scalars['String'] 202 | }; 203 | 204 | 205 | export type GetProductsQuery = ( 206 | { __typename?: 'Query' } 207 | & { getProducts: Maybe<( 208 | { __typename?: 'Products' } 209 | & { products: Maybe>> } 213 | )> } 214 | ); 215 | -------------------------------------------------------------------------------- /packages/app/src/graphql/apollo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Head from 'next/head'; 3 | import fetch from 'isomorphic-unfetch'; 4 | import { ApolloProvider } from '@apollo/react-hooks'; 5 | import { ApolloClient } from 'apollo-client'; 6 | import { InMemoryCache } from 'apollo-cache-inmemory'; 7 | 8 | import { ApolloLink, split } from 'apollo-link'; 9 | import { onError } from 'apollo-link-error'; 10 | import { HttpLink } from 'apollo-link-http'; 11 | import { BatchHttpLink } from 'apollo-link-batch-http'; 12 | 13 | import { toIdValue } from 'apollo-utilities'; 14 | import { fragmentMatcher } from './fragment-matcher'; 15 | import { version } from '../../package.json'; 16 | 17 | const uri = process.env.GRAPHQL_ENDPOINT 18 | ? process.env.GRAPHQL_ENDPOINT 19 | : 'http://localhost:4000/graphql'; 20 | 21 | const cache = new InMemoryCache({ 22 | fragmentMatcher, 23 | cacheRedirects: { 24 | Query: { 25 | // Here we map the data we get in product list view with the one for detail view 26 | // see: https://www.apollographql.com/docs/react/features/performance.html 27 | getProduct: (_, args) => 28 | toIdValue( 29 | cache.config.dataIdFromObject({ 30 | __typename: 'Product', 31 | id: args.id, 32 | }), 33 | ), 34 | }, 35 | }, 36 | resultCaching: false, 37 | }); 38 | 39 | const batchHttpLink = new BatchHttpLink({ 40 | uri, 41 | credentials: 'include', // 'same-origin' 42 | headers: { batch: 'true ' }, 43 | batchInterval: 10, 44 | fetch, 45 | }); 46 | 47 | // link to use if not batching 48 | const httpLink = new HttpLink({ 49 | uri, 50 | credentials: 'include', // 'same-origin' 51 | fetch, 52 | }); 53 | 54 | // Polyfill fetch() on the server (used by apollo-client) 55 | if (!process.browser) { 56 | global.fetch = fetch; 57 | } 58 | 59 | let apolloClient = null; 60 | 61 | /** 62 | * Creates and provides the apolloContext 63 | * to a next.js PageTree. Use it by wrapping 64 | * your PageComponent via HOC pattern. 65 | * @param {Function|Class} PageComponent 66 | * @param {Object} [config] 67 | * @param {Boolean} [config.ssr=true] 68 | */ 69 | export function withApollo(PageComponent, { ssr = true } = {}) { 70 | const WithApollo = ({ 71 | apolloClient, 72 | apolloState, 73 | ssrComplete, 74 | ...pageProps 75 | }) => { 76 | const client = apolloClient || initApolloClient(apolloState); 77 | 78 | return typeof window !== 'undefined' || (ssr && !ssrComplete) ? ( 79 | 80 | 81 | 82 | ) : null; 83 | }; 84 | 85 | // Set the correct displayName in development 86 | if (process.env.NODE_ENV !== 'production') { 87 | const displayName = 88 | PageComponent.displayName || PageComponent.name || 'Component'; 89 | 90 | if (displayName === 'App') { 91 | console.warn('This withApollo HOC only works with PageComponents.'); 92 | } 93 | 94 | WithApollo.displayName = `withApollo(${displayName})`; 95 | } 96 | 97 | if (ssr || PageComponent.getInitialProps) { 98 | WithApollo.getInitialProps = async ctx => { 99 | const { AppTree } = ctx; 100 | 101 | // Initialize ApolloClient, add it to the ctx object so 102 | // we can use it in `PageComponent.getInitialProp`. 103 | ctx.apolloClient = initApolloClient(); 104 | 105 | // Run wrapped getInitialProps methods 106 | let pageProps = {}; 107 | if (PageComponent.getInitialProps) { 108 | pageProps = await PageComponent.getInitialProps(ctx); 109 | } 110 | 111 | // Only on the server: 112 | if (typeof window === 'undefined') { 113 | // When redirecting, the response is finished. 114 | // No point in continuing to render 115 | if (ctx.res && ctx.res.finished) { 116 | return pageProps; 117 | } 118 | 119 | // Only if ssr is enabled 120 | if (ssr) { 121 | try { 122 | // Run all GraphQL queries 123 | const { getDataFromTree } = await import( 124 | '@apollo/react-ssr' 125 | ); 126 | await getDataFromTree( 127 | , 133 | ); 134 | } catch (error) { 135 | // Prevent Apollo Client GraphQL errors from crashing SSR. 136 | // Handle them in components via the data.error prop: 137 | // https://www.apollographql.com/docs/react/api/react-apollo.html#graphql-query-data-error 138 | console.error( 139 | 'Error while running `getDataFromTree`', 140 | error, 141 | ); 142 | } 143 | 144 | // getDataFromTree does not call componentWillUnmount 145 | // head side effect therefore need to be cleared manually 146 | Head.rewind(); 147 | } 148 | } 149 | 150 | // Extract query data from the Apollo store 151 | const apolloState = ctx.apolloClient.cache.extract(); 152 | 153 | return { 154 | ...pageProps, 155 | apolloState, 156 | ssrComplete: true, 157 | }; 158 | }; 159 | } 160 | 161 | return WithApollo; 162 | } 163 | 164 | /** 165 | * Always creates a new apollo client on the server 166 | * Creates or reuses apollo client in the browser. 167 | * @param {Object} initialState 168 | */ 169 | function initApolloClient(initialState) { 170 | // Make sure to create a new client for every server-side request so that data 171 | // isn't shared between connections (which would be bad) 172 | if (typeof window === 'undefined') { 173 | return createApolloClient(initialState); 174 | } 175 | 176 | // Reuse client on the client-side 177 | if (!apolloClient) { 178 | apolloClient = createApolloClient(initialState); 179 | } 180 | 181 | return apolloClient; 182 | } 183 | 184 | /** 185 | * Creates and configures the ApolloClient 186 | * @param {Object} [initialState={}] 187 | */ 188 | function createApolloClient(initialState = {}) { 189 | return new ApolloClient({ 190 | ssrMode: typeof window === 'undefined', // Disables forceFetch on the server (so queries are only run once) 191 | link: ApolloLink.from([ 192 | onError(({ graphQLErrors, networkError }) => { 193 | if (graphQLErrors) 194 | graphQLErrors.map(({ message, locations, path }) => 195 | console.log( 196 | `[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`, 197 | ), 198 | ); 199 | if (networkError) 200 | console.log(`[Network error]: ${networkError}`); 201 | }), 202 | 203 | split( 204 | operation => operation.getContext().important === true, 205 | httpLink, // if the test is true -- debatch 206 | batchHttpLink, // otherwise, batching is fine 207 | ), 208 | ]), 209 | cache: cache.restore(initialState), 210 | name: 'Sample application', 211 | version, 212 | }); 213 | } 214 | -------------------------------------------------------------------------------- /packages/app/src/graphql/fragment-matcher.js: -------------------------------------------------------------------------------- 1 | import { IntrospectionFragmentMatcher } from 'apollo-cache-inmemory'; 2 | import introspectionResults from './_generated-fragment-types'; 3 | 4 | export const fragmentMatcher = new IntrospectionFragmentMatcher({ 5 | introspectionQueryResultData: introspectionResults, 6 | }); 7 | -------------------------------------------------------------------------------- /packages/app/src/graphql/fragments/product.fragment.graphql.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | export const FRAGMENT_PRODUCT = gql` 4 | fragment product on Product { 5 | id 6 | title 7 | rating 8 | shortDescription 9 | images { 10 | key 11 | url 12 | } 13 | urls { 14 | key 15 | value 16 | } 17 | } 18 | `; 19 | -------------------------------------------------------------------------------- /packages/app/src/modules/App.tsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head'; 2 | import { Header } from './header/header'; 3 | import style from './app.scss'; 4 | 5 | export const App = ({ children, title = 'GraphQL modules example' }: any) => ( 6 |
7 | 8 | workshop GRAPHQL - {title} 9 | 13 |