From 1ed16d1df63bbfaac712823b4b264acc5194d22d Mon Sep 17 00:00:00 2001 From: maapteh Date: Sat, 14 Sep 2019 20:22:54 +0200 Subject: [PATCH] feature(): demonstrating graphql modules with complete workflow, apollo and apollo hooks --- .circleci/config.yml | 52 + .circleci/heroku.sh | 25 + .circleci/run-test.sh | 12 + .editorconfig | 9 + .github/workflows/ci.yml | 32 + .gitignore | 23 + .graphqlconfig | 8 + .huskyrc | 5 + .npmrc | 1 + .vscode/settings.json | 7 + LICENSE.md | 21 + README.md | 99 + apollo.config.js | 11 + babel.config.js | 4 + config/nginx/.gitignore | 3 + config/nginx/Dockerfile | 16 + config/nginx/README.md | 13 + config/nginx/localhost.crt | 18 + config/nginx/localhost.key | 28 + config/nginx/package.json | 14 + config/nginx/server.js | 62 + config/prettier.json | 11 + config/prettierignore | 4 + dev.sh | 6 + docker-compose.yml | 53 + lerna.json | 18 + package.json | 46 + packages/app/.babelrc | 26 + packages/app/.eslintignore | 6 + packages/app/.eslintrc | 34 + packages/app/.graphqlconfig | 8 + packages/app/.lintstagedrc | 4 + packages/app/.nmprc | 4 + packages/app/.prettierrc | 10 + packages/app/Dockerfile | 16 + packages/app/README.md | 25 + packages/app/codegen.yml | 35 + packages/app/globals.d.ts | 1 + packages/app/jest.config.js | 24 + packages/app/jest.tsconfig.json | 17 + packages/app/next-env.d.ts | 7 + packages/app/next.config.js | 40 + packages/app/package.json | 97 + packages/app/pages/example.js | 59 + packages/app/pages/index.js | 51 + packages/app/pages/product.js | 1 + packages/app/pages/product/[id].js | 18 + packages/app/pages/products.js | 18 + packages/app/server.js | 25 + packages/app/src/elements/image/image.scss | 18 + packages/app/src/elements/image/image.tsx | 37 + .../app/src/elements/logo-bol/logo-bol.tsx | 10 + .../src/graphql/_generated-fragment-types.ts | 21 + packages/app/src/graphql/_generated-hooks.tsx | 220 + .../app/src/graphql/_generated-schema.graphql | 96 + packages/app/src/graphql/_generated-types.ts | 165 + packages/app/src/graphql/apollo.js | 198 + packages/app/src/graphql/fragment-matcher.js | 6 + .../fragments/product.fragment.graphql.ts | 18 + packages/app/src/modules/App.tsx | 19 + packages/app/src/modules/ErrorMessage.tsx | 1 + packages/app/src/modules/app.scss | 38 + packages/app/src/modules/header/header.scss | 56 + packages/app/src/modules/header/header.tsx | 29 + .../__snapshots__/product.spec.tsx.snap | 32 + .../product-details.spec.tsx.snap | 38 + .../product-details/product-details.scss | 3 + .../product-details/product-details.spec.tsx | 48 + .../product-details/product-details.tsx | 54 + .../product/graphql/get-product.graphql.ts | 13 + .../src/modules/product/product-component.tsx | 31 + .../app/src/modules/product/product.spec.tsx | 84 + packages/app/src/modules/product/product.tsx | 20 + .../elements/link/products-list-link.tsx | 14 + .../graphql/get-products.graphql.ts | 15 + .../modules/products-list/products-list.scss | 3 + .../modules/products-list/products-list.tsx | 45 + packages/app/static/bol.svg | 12 + packages/app/static/favicon.png | Bin 0 -> 2920 bytes packages/app/static/logo.svg | 36 + packages/app/test/__mocks__/fileMock.js | 1 + packages/app/test/visual-regression/index.js | 30 + packages/app/tsconfig.json | 38 + packages/server/.gitignore | 2 + packages/server/.lintstagedrc | 3 + packages/server/.npmrc | 4 + packages/server/.prettierrc.json | 11 + packages/server/Dockerfile | 16 + packages/server/README.md | 22 + packages/server/codegen.yml | 18 + packages/server/jest.config.js | 16 + packages/server/package.json | 92 + packages/server/src/_graphql.d.ts | 359 + packages/server/src/_schema.graphql | 96 + packages/server/src/app.ts | 10 + packages/server/src/config/index.ts | 2 + packages/server/src/config/profile.ts | 2 + packages/server/src/config/server.ts | 9 + packages/server/src/constants/base-urls.ts | 1 + packages/server/src/constants/credentials.ts | 1 + packages/server/src/constants/index.ts | 6 + packages/server/src/global.d.ts | 0 .../helpers/check-status/check-status.spec.ts | 13 + .../src/helpers/check-status/check-status.ts | 10 + packages/server/src/helpers/index.ts | 4 + .../server/src/helpers/is-user-request.ts | 8 + packages/server/src/index.ts | 11 + .../allowed-origin/allowed-origins.ts | 24 + .../middleware/allowed-origin/origins-list.ts | 10 + .../server/src/middleware/cache/no-cache.ts | 16 + packages/server/src/modules/common/index.ts | 21 + packages/server/src/modules/product/index.ts | 16 + .../product/providers/product-data-loader.ts | 19 + .../src/modules/product/providers/product.ts | 38 + .../modules/product/resolvers/resolvers.ts | 30 + .../modules/product/schema/product.graphql | 92 + .../src/modules/product/schema/query.graphql | 11 + packages/server/src/schema.ts | 5 + packages/server/src/server.ts | 77 + packages/server/src/typings.d.ts | 1 + .../server/test/__mocks__/stubs/poducts.ts | 1 + .../stubs/product-9200000111963040.ts | 1 + .../stubs/product-9200000113065845.ts | 1 + .../stubs/product-9200000113944705.ts | 1 + packages/server/test/schema-mock.ts | 29 + packages/server/tsconfig.json | 31 + packages/server/tslint.json | 6 + setup.sh | 10 + yarn.lock | 15290 ++++++++++++++++ 129 files changed, 19022 insertions(+) create mode 100644 .circleci/config.yml create mode 100644 .circleci/heroku.sh create mode 100644 .circleci/run-test.sh create mode 100644 .editorconfig create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 .graphqlconfig create mode 100644 .huskyrc create mode 100644 .npmrc create mode 100644 .vscode/settings.json create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 apollo.config.js create mode 100644 babel.config.js create mode 100644 config/nginx/.gitignore create mode 100644 config/nginx/Dockerfile create mode 100644 config/nginx/README.md create mode 100644 config/nginx/localhost.crt create mode 100644 config/nginx/localhost.key create mode 100644 config/nginx/package.json create mode 100644 config/nginx/server.js create mode 100644 config/prettier.json create mode 100644 config/prettierignore create mode 100644 dev.sh create mode 100644 docker-compose.yml create mode 100644 lerna.json create mode 100644 package.json create mode 100644 packages/app/.babelrc create mode 100644 packages/app/.eslintignore create mode 100644 packages/app/.eslintrc create mode 100644 packages/app/.graphqlconfig create mode 100644 packages/app/.lintstagedrc create mode 100644 packages/app/.nmprc create mode 100644 packages/app/.prettierrc create mode 100644 packages/app/Dockerfile create mode 100644 packages/app/README.md create mode 100644 packages/app/codegen.yml create mode 100644 packages/app/globals.d.ts create mode 100644 packages/app/jest.config.js create mode 100644 packages/app/jest.tsconfig.json create mode 100644 packages/app/next-env.d.ts create mode 100644 packages/app/next.config.js create mode 100644 packages/app/package.json create mode 100644 packages/app/pages/example.js create mode 100644 packages/app/pages/index.js create mode 100644 packages/app/pages/product.js create mode 100644 packages/app/pages/product/[id].js create mode 100644 packages/app/pages/products.js create mode 100644 packages/app/server.js create mode 100644 packages/app/src/elements/image/image.scss create mode 100644 packages/app/src/elements/image/image.tsx create mode 100644 packages/app/src/elements/logo-bol/logo-bol.tsx create mode 100644 packages/app/src/graphql/_generated-fragment-types.ts create mode 100644 packages/app/src/graphql/_generated-hooks.tsx create mode 100644 packages/app/src/graphql/_generated-schema.graphql create mode 100644 packages/app/src/graphql/_generated-types.ts create mode 100644 packages/app/src/graphql/apollo.js create mode 100644 packages/app/src/graphql/fragment-matcher.js create mode 100644 packages/app/src/graphql/fragments/product.fragment.graphql.ts create mode 100644 packages/app/src/modules/App.tsx create mode 100644 packages/app/src/modules/ErrorMessage.tsx create mode 100644 packages/app/src/modules/app.scss create mode 100644 packages/app/src/modules/header/header.scss create mode 100644 packages/app/src/modules/header/header.tsx create mode 100644 packages/app/src/modules/product/__snapshots__/product.spec.tsx.snap create mode 100644 packages/app/src/modules/product/elements/product-details/__snapshots__/product-details.spec.tsx.snap create mode 100644 packages/app/src/modules/product/elements/product-details/product-details.scss create mode 100644 packages/app/src/modules/product/elements/product-details/product-details.spec.tsx create mode 100644 packages/app/src/modules/product/elements/product-details/product-details.tsx create mode 100644 packages/app/src/modules/product/graphql/get-product.graphql.ts create mode 100644 packages/app/src/modules/product/product-component.tsx create mode 100644 packages/app/src/modules/product/product.spec.tsx create mode 100644 packages/app/src/modules/product/product.tsx create mode 100644 packages/app/src/modules/products-list/elements/link/products-list-link.tsx create mode 100644 packages/app/src/modules/products-list/graphql/get-products.graphql.ts create mode 100644 packages/app/src/modules/products-list/products-list.scss create mode 100644 packages/app/src/modules/products-list/products-list.tsx create mode 100644 packages/app/static/bol.svg create mode 100644 packages/app/static/favicon.png create mode 100644 packages/app/static/logo.svg create mode 100644 packages/app/test/__mocks__/fileMock.js create mode 100644 packages/app/test/visual-regression/index.js create mode 100644 packages/app/tsconfig.json create mode 100644 packages/server/.gitignore create mode 100644 packages/server/.lintstagedrc create mode 100644 packages/server/.npmrc create mode 100644 packages/server/.prettierrc.json create mode 100644 packages/server/Dockerfile create mode 100644 packages/server/README.md create mode 100644 packages/server/codegen.yml create mode 100644 packages/server/jest.config.js create mode 100644 packages/server/package.json create mode 100644 packages/server/src/_graphql.d.ts create mode 100644 packages/server/src/_schema.graphql create mode 100644 packages/server/src/app.ts create mode 100644 packages/server/src/config/index.ts create mode 100644 packages/server/src/config/profile.ts create mode 100644 packages/server/src/config/server.ts create mode 100644 packages/server/src/constants/base-urls.ts create mode 100644 packages/server/src/constants/credentials.ts create mode 100644 packages/server/src/constants/index.ts create mode 100644 packages/server/src/global.d.ts create mode 100644 packages/server/src/helpers/check-status/check-status.spec.ts create mode 100644 packages/server/src/helpers/check-status/check-status.ts create mode 100644 packages/server/src/helpers/index.ts create mode 100644 packages/server/src/helpers/is-user-request.ts create mode 100644 packages/server/src/index.ts create mode 100644 packages/server/src/middleware/allowed-origin/allowed-origins.ts create mode 100644 packages/server/src/middleware/allowed-origin/origins-list.ts create mode 100644 packages/server/src/middleware/cache/no-cache.ts create mode 100644 packages/server/src/modules/common/index.ts create mode 100644 packages/server/src/modules/product/index.ts create mode 100644 packages/server/src/modules/product/providers/product-data-loader.ts create mode 100644 packages/server/src/modules/product/providers/product.ts create mode 100644 packages/server/src/modules/product/resolvers/resolvers.ts create mode 100644 packages/server/src/modules/product/schema/product.graphql create mode 100644 packages/server/src/modules/product/schema/query.graphql create mode 100644 packages/server/src/schema.ts create mode 100644 packages/server/src/server.ts create mode 100644 packages/server/src/typings.d.ts create mode 100644 packages/server/test/__mocks__/stubs/poducts.ts create mode 100644 packages/server/test/__mocks__/stubs/product-9200000111963040.ts create mode 100644 packages/server/test/__mocks__/stubs/product-9200000113065845.ts create mode 100644 packages/server/test/__mocks__/stubs/product-9200000113944705.ts create mode 100644 packages/server/test/schema-mock.ts create mode 100644 packages/server/tsconfig.json create mode 100644 packages/server/tslint.json create mode 100644 setup.sh create mode 100644 yarn.lock diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..3c10594 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,52 @@ +--- +defaults: &defaults + docker: + - image: circleci/node:10.16 + +version: 2 +jobs: + build: + <<: *defaults + working_directory: ~/repo + + steps: + - checkout + - restore_cache: + keys: + - dependency-cache-{{ checksum "yarn.lock" }} + - dependency-cache-{{ checksum "packages/app/yarn.lock" }} + - dependency-cache-{{ checksum "packages/server/yarn.lock" }} + - run: yarn + - save_cache: + key: dependency-cache-{{ checksum "./yarn.lock" }} + paths: + - node_modules + - packages/app/node_modules + - packages/server/node_modules + - run: yarn lint + - run: yarn test + # Workflow not working at the moment + # - persist_to_workspace: + # root: ./ + # paths: + # - ./node_modules + # - ./packages/app + # - ./paclages/server + + test: + <<: *defaults + steps: + - checkout + - attach_workspace: + at: ./ + - run: yarn test + - run: yarn lint + +workflows: + version: 2 + build_and_release: + jobs: + - build + # - test: + # requires: + # - build \ No newline at end of file diff --git a/.circleci/heroku.sh b/.circleci/heroku.sh new file mode 100644 index 0000000..b26b052 --- /dev/null +++ b/.circleci/heroku.sh @@ -0,0 +1,25 @@ +#!/bin/sh -e + +usage() { + echo "OVERVIEW: Build apps according to BUILD_ENV value. Meant to be used for Heroku deployment" + exit +} + +if [ "$1" = '-h' ] || [ "$1" = '--help' ]; then + usage +fi + +( + PROJECT_ROOT="$(cd $(dirname $0)/..; pwd)" + + cd $PROJECT_ROOT + + if [ "$BUILD_ENV" = "app" ]; then + yarn workspace graphql-app build + elif [ "$BUILD_ENV" = "server" ]; then + yarn workspace graphql-server build + else + echo "Error: no build config for BUILD_ENV value '$BUILD_ENV'" + exit 1 + fi +) \ No newline at end of file diff --git a/.circleci/run-test.sh b/.circleci/run-test.sh new file mode 100644 index 0000000..f426024 --- /dev/null +++ b/.circleci/run-test.sh @@ -0,0 +1,12 @@ +#!/bin/bash +repo=$1 +branch=`git rev-parse --abbrev-ref HEAD` +if [ "$branch" = "master" ]; then + echo "On branch master. Let's run all tests!" + eval "yarn test" +elif git diff --name-only origin/master...$branch | grep "^${repo}" ; then + echo "Changes detected! Adding ${repo} tests to the queue..." +else + echo "No changes detected. Exiting circle build..." + circleci step halt +fi \ No newline at end of file diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..81eba8c --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +indent_style = space +indent_size = 4 +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true +end_of_line = lf diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..18eb1b2 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,32 @@ + +name: CI + +on: [push] + +jobs: + test: + name: Test on node ${{ matrix.node_version }} + runs-on: ubuntu-latest + strategy: + matrix: + node_version: [10, 12] + + steps: + - uses: actions/checkout@master + + - name: Use Node.js ${{ matrix.node_version }} + uses: actions/setup-node@master + with: + version: ${{ matrix.node_version }} + + - name: Install + run: | + yarn + + - name: Build + run: | + yarn build + + - name: Test + run: | + yarn test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d9a2b13 --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +# fs +.DS_Store +.vscode +!.vscode/settings.json + +coverage +node_modules +.npm +.nvmrc +# we are using yarn, when people update with npm exclude this +package-lock.json +dist + +yarn-error.log +.yarnclean + +.next +.env + +*.scss.d.ts + +# auto generated (leave schema and hooks in repo!) +# packages/app/src/graphql/_generated-types.ts diff --git a/.graphqlconfig b/.graphqlconfig new file mode 100644 index 0000000..98f69fc --- /dev/null +++ b/.graphqlconfig @@ -0,0 +1,8 @@ +{ + "schemaPath": "packages/server/src/_schema.graphql", + "extensions": { + "endpoints": { + "dev": "http://localhost:4000/graphql" + } + } +} \ No newline at end of file diff --git a/.huskyrc b/.huskyrc new file mode 100644 index 0000000..fc68527 --- /dev/null +++ b/.huskyrc @@ -0,0 +1,5 @@ +{ + "hooks": { + "pre-commit": "lerna run --concurrency 1 --stream precommit" + } +} \ No newline at end of file diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..cad5797 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +save-exact = true \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f195174 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "eslint.alwaysShowStatus": true, + "eslint.workingDirectories": [ + "packages/app", "packages/server" + ], + "prettier.tabWidth": 4 +} \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..d7a7c8a --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2018 Maapteh. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..926f374 --- /dev/null +++ b/README.md @@ -0,0 +1,99 @@ +# GraphQL-Modules TypeScript Server & NextJS React application +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. +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/). + +## PRE-REQUISITES +- Node dubnium +- Facebook watchman (only for development) [optional] +- Get your free API key from [bol.com/documentatie/open-api](https://partnerblog.bol.com/documentatie/open-api). Its also possible to run it in mocked mode, no keys needed. + +## INSTALL +1. `yarn` +2. `bash setup.sh` sets correct local .env file with mock mode as default + +## STRUCTURE +``` +. +├── /config/ # some configuration for build scripts +├── /packages/ # 2 applications +│ ├── /app/ # React NextJS isomorphic application +│ └── /server/ # Apollo GraphQL server created with graphql-modules +├── /test/ # end-to-end tests +``` + +## DEVELOPMENT +**Now when you followed the install part you can simply run `yarn start`. It will spin up the GraphQL server and the React application.** Please look at the VSC plugins below for editor happiness. + +## PLAYGROUND +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: + +``` +{ + foo: getProduct(id:"9200000111963040") { + id + title + } + bar:getProduct(id:"9200000111963040") { + id + title + rating + } + shizzle:getProduct(id:"9200000108695538") { + title + rating + shortDescription + } +} +``` + +## PRODUCTION +By default after install the build will take place and the start command is running this build. + +## CONFIGURATION +Environment vars for development (set them in CI for production). + +### '.env' file inside './packages/server': + +*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* + +``` +BOL_API_KEY=*** +NODE_ENV=development +MOCK_API=ON|OFF +ENGINE_KEY=optional-apollo-engine-key-overhere REMOVE WHEN NOT AVAILABLE +ALLOWED_ORIGIN=optional-not-needed-dev-mode REMOVE +``` + +### '.env' file inside './packages/app' +This file is optional, the dev setting is the default. +``` +GRAPHQL_ENDPOINT=endpoint-your-graphql-server-will-run +``` + +## TODO +1) Add more tooling (things like storybook etc etc) +2) Use https://github.com/kamilkisiela/graphql-inspector (also in pipeline, now locally only) +3) `yarn upgrade-interactive --latest` + +## ARTICLES +- [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) +- [Paypal Graphql](https://medium.com/paypal-engineering/graphql-a-success-story-for-paypal-checkout-3482f724fb53) +- [Airbnb luxery homes](https://medium.com/airbnb-engineering/how-airbnb-is-moving-10x-faster-at-scale-with-graphql-and-apollo-aa4ec92d69e2) +- [https://www.graphqlweekly.com/](https://www.graphqlweekly.com/) +- [GraphQL HQ](https://blog.apollographql.com/) + + +## ONLINE DEMO +*Both Heroku containers spin down when no activity, please be patient.* +[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). + + +## VSC +- [vscode-apollo](https://marketplace.visualstudio.com/items?itemName=apollographql.vscode-apollo) for autocomplete in app +- [eslint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) including apollo linting + + + +[![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) + +[![This project is using Percy.io for visual regression testing.](https://percy.io/static/images/percy-badge.svg)](https://percy.io/maas38/graphql-workshop) diff --git a/apollo.config.js b/apollo.config.js new file mode 100644 index 0000000..cc05d9b --- /dev/null +++ b/apollo.config.js @@ -0,0 +1,11 @@ +module.exports = { + client: { + service: { + name: 'maapteh-6450', + localSchemaFile: './packages/server/src/_schema.graphql' + }, + addTypename: false, + excludes: ['**/__tests__/**/*', '**/__mocks__/**/*'], + includes: ['./packages/app/src/**/*.graphql.ts'] + }, +}; diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 0000000..bd84aff --- /dev/null +++ b/babel.config.js @@ -0,0 +1,4 @@ +module.exports = { + // jest: https://github.com/facebook/jest/issues/7359 + babelrcRoots: ['packages/*'], +} \ No newline at end of file diff --git a/config/nginx/.gitignore b/config/nginx/.gitignore new file mode 100644 index 0000000..a23f2ea --- /dev/null +++ b/config/nginx/.gitignore @@ -0,0 +1,3 @@ +local.test+2-key.pem +local.test+2.pem +local.*.pem diff --git a/config/nginx/Dockerfile b/config/nginx/Dockerfile new file mode 100644 index 0000000..c680304 --- /dev/null +++ b/config/nginx/Dockerfile @@ -0,0 +1,16 @@ +# THIS CONTAINER WILL ONLY BE USED DURING DEVELOPMENT +FROM node:dubnium-slim + +ENV NODE_ENV=production +ENV IS_DOCKER=true + +WORKDIR /usr/app + +COPY package.json /usr/app/package.json +COPY yarn.lock /usr/app/yarn.lock + +RUN yarn install + +COPY . /usr/app + +CMD yarn start diff --git a/config/nginx/README.md b/config/nginx/README.md new file mode 100644 index 0000000..cffb7ee --- /dev/null +++ b/config/nginx/README.md @@ -0,0 +1,13 @@ +# Development nginx setup + +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. + +## Create your self signed certificates +The certificates are created using [mkcert](https://github.com/FiloSottile/mkcert) + +- `brew install mkcert` (on Guest network) +- `mkcert -install` +- Create them: `mkcert local.foo.test "*.foo.test" localhost` (in this folder) +- Put certificate in keychain access (cli points where it put cert file) +- *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.* +- *local.foo.test will only work if you add this to your local Host file.* diff --git a/config/nginx/localhost.crt b/config/nginx/localhost.crt new file mode 100644 index 0000000..d48dfa3 --- /dev/null +++ b/config/nginx/localhost.crt @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC5TCCAc2gAwIBAgIJAMPNzGLMR5RLMA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNV +BAMMCWxvY2FsaG9zdDAeFw0xODA3MDIxMDQxNDdaFw0yODA2MjkxMDQxNDdaMBQx +EjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAK7OYJJhvgznP3jcw3b7BsYHFTW7CXlA2b2ra7mrjX8ns/fpT7VQCdT/8vOX +vf8auPtCNakw70qJBm/3fwd3i/buDCxpxjjzlDF5kKrpdw1f1PdNQsGl5c6BDFOi +qQEn0FwEdY6zERXIpUPXLHobdhf2zRiZU93fl/5h/uYVYDPV/2RBz6Y/PfW/8Aug +yXA0S67+7TalNMK/k0CCPv5UR+5LLZlnfKZJhG5hWQjar8CItpheQlaEl7s8kiMN +GZFCsW+zbz6jgcP8YwznhdBzDifGH1Wrl5JOZdH6vHZv08s3Oy+3qvd5598r4INP +XXAFiQvxEbMjgV5rL3bH9D9nMNUCAwEAAaM6MDgwFAYDVR0RBA0wC4IJbG9jYWxo +b3N0MAsGA1UdDwQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDATANBgkqhkiG9w0B +AQsFAAOCAQEATevbPis+ObkJfqW3eKIK8r2UhtC4G+TaTx3Vh5QwR/3EdwSeUyzF +pfDMcLhRuHVqBzzbT5xhHCyt7g8bQSfNsZTLxEQcb88kYARj7WZ579xX+nP0dxAS +hrBo3jiOvZKS9yz7T4BjuAmegLVPUPHFFG3ZNlLfZQKR9D3RQ/2WecFMZQznUEvl +U7IJ/F+pvuR8LUhmL70RFGvX01L5bl3oFUDdYaCe5M35zVt/vMF73arbcR45dhcD +jQ7bl/ro9+C/gSGJNa+FAumzrZmOJYR5ENqJBr2F+R+mRjpYg6g4SPmGjT+7LAID +nkyGwcD7I1QpXVc8tVRiSkKQpLFNyhVriw== +-----END CERTIFICATE----- diff --git a/config/nginx/localhost.key b/config/nginx/localhost.key new file mode 100644 index 0000000..cb5ffeb --- /dev/null +++ b/config/nginx/localhost.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCuzmCSYb4M5z94 +3MN2+wbGBxU1uwl5QNm9q2u5q41/J7P36U+1UAnU//Lzl73/Grj7QjWpMO9KiQZv +938Hd4v27gwsacY485QxeZCq6XcNX9T3TULBpeXOgQxToqkBJ9BcBHWOsxEVyKVD +1yx6G3YX9s0YmVPd35f+Yf7mFWAz1f9kQc+mPz31v/ALoMlwNEuu/u02pTTCv5NA +gj7+VEfuSy2ZZ3ymSYRuYVkI2q/AiLaYXkJWhJe7PJIjDRmRQrFvs28+o4HD/GMM +54XQcw4nxh9Vq5eSTmXR+rx2b9PLNzsvt6r3eeffK+CDT11wBYkL8RGzI4Feay92 +x/Q/ZzDVAgMBAAECggEAcTiatDU6s4DUS6Qxtk7BBGJyCmsqp66pWYA+NfQ3obRF +jL1BM16z/5IH+l6+YQ0d4x/vQbbARraZxMu5K0zzCu0EVX/tM9YQljr2yLyOr8ry +VXtlUafyQN607Tbd4DG5cuAwhEzXNBTRdi9YT36Z7sub6+Ljv0GjYNB4GO6fcPKH +TrhFGGOJusJ3clUAWlnnQgVyWjLM1waZjESYSNZMzMES9xR3qpWQKHHhew0fuZl2 +pj0rV5khGYcXjpc70vhfuv5yrVPeNLTq1sVOKhjgJ42c18MUj6txEefGwoyGtusn +i+ETKSKaqJ/txJ4+f+XwRHIS0OtNbRDZwtbYOl7XwQKBgQDoNZSzC62jpUHFMHva +Odx9eknMx2rbZBsx6VxPE8qel3WUJRSUHRqrWZlMcM5KJKDuHeMP3MjqSWug/q6M +zSDobwQV+jMGXAa9FV7jI9qBy+0/BgtNFtWH+MYG/gjHxBkBpuB95FJJUdi0gM30 +aPOj5zYn826Dm7W5KUG1t+NCfwKBgQDAtzfxwZjTvSsrNovIChYt8/rE9bXR1QUp +Kc6VCKcO7fUno15NYN1wNCE/LPKqFF8lNiPcLj9XEOLiJrFHhJz+Depq2tW7VIua +gpTcSPiYUk7EGisrM5VsrSaqNvWcK3w4PNjrNDe2B2xRhMwUBvJXIywOsC7G/2oP +CAtvu/c6qwKBgGlXnVzYaG570umFBDrM0wUti/tVYFmlAV1UM2dAYEQwC8woQjyr +M2UWoZ/28O7bzRIZBuA0VgVLR4Ni5obDrDEl4+GgfrNc3kW7Qy+iHUeS3s8fi9Lu +D/K+Xf/gENWnVXzVWrRh9x6B/eBtKoG9dwIdKwlWuwUDh543ZDLu+C87AoGBAIgx +9Aua8lLR4exMREU/O6WGQ7dmnvSIQ3lv3ltdHhNjAFrfDgpJZrWhYc2wCl9Avm0h +8f3tgT4a5P1GswsEIZ86Xmzd8ybM/UxY9LMpruaXZKsag1+ouPVw+V5aMQIJiWSF +PBgdczHl1RtXapLMxf/nD3/h620fnOi6mrqAcJy5AoGBAOJvxuKicVnu9mqucwY9 +HFXjkWVXy1ViMpYa2gwSqXVtKo9okZUroCuQ4qmNqWZg2gfpYnVwiyGMqpb4ELi+ +7uceDoTyLm4Wgf9A0QTdudoY7lFiQ81ni+w0p9cFzUxCILU6y/o+44boMJX913/I +xjk2ENxqskDowruz8j1CpyWk +-----END PRIVATE KEY----- diff --git a/config/nginx/package.json b/config/nginx/package.json new file mode 100644 index 0000000..ed4913d --- /dev/null +++ b/config/nginx/package.json @@ -0,0 +1,14 @@ +{ + "name": "graphql-nginx", + "version": "0.0.1", + "license": "UNLICENSED", + "description": "Only used for local development", + "main": "server.js", + "scripts": { + "start": "node server.js" + }, + "dependencies": { + "express": "^4.16.4", + "http-proxy-middleware": "^0.19.1" + } +} diff --git a/config/nginx/server.js b/config/nginx/server.js new file mode 100644 index 0000000..63adc63 --- /dev/null +++ b/config/nginx/server.js @@ -0,0 +1,62 @@ +const fs = require('fs'); +const http = require('http'); +const https = require('https'); +const express = require('express'); +const proxy = require('http-proxy-middleware'); + +let CERT = 'localhost.crt'; +const CERT_SIGNED = 'local.test+2.pem'; +let KEY = 'localhost.key'; +const KEY_SIGNED = 'local.test+2-key.pem'; + +if (fs.existsSync(CERT_SIGNED) && fs.existsSync(CERT_SIGNED)) { + console.log('[PROXY] Own certificates are being used'); + CERT = CERT_SIGNED; + KEY = KEY_SIGNED; +} + +const privateKey = fs.readFileSync(KEY, 'utf8'); +const certificate = fs.readFileSync(CERT, 'utf8'); + +const isDocker = process.env.IS_DOCKER; + +const HTTPS_PORT = 443; +const HTTP_PORT = 3000; + +const WEB = isDocker ? 'web' : 'localhost'; +const API = isDocker ? 'graphql' : 'localhost'; + +const app = express(); + +// Our application: API +app.use( + '/graphql', + proxy({ + target: `http://${API}:4000`, + }), +); + +// Our application: WEB +app.use( + ['/__webpack_hmr', '/'], + proxy({ + target: `http://${WEB}:4001`, + }), +); + +const credentials = { + key: privateKey, + cert: certificate, +}; +const httpServer = http.createServer(app); +const httpsServer = https.createServer(credentials, app); + +console.log(`[PROXY] running for: ${isDocker ? 'docker' : 'localhost'}`); + +httpServer.listen(HTTP_PORT, () => { + console.log(`[PROXY] 🚀 http started on port ${HTTP_PORT}`); +}); + +httpsServer.listen(HTTPS_PORT, () => { + console.log(`[PROXY] 🚀 https started on port ${HTTPS_PORT}`); +}); diff --git a/config/prettier.json b/config/prettier.json new file mode 100644 index 0000000..2edf098 --- /dev/null +++ b/config/prettier.json @@ -0,0 +1,11 @@ +{ + "printWidth": 80, + "tabWidth": 4, + "useTabs": false, + "semi": true, + "singleQuote": true, + "trailingComma": "all", + "bracketSpacing": true, + "jsxBracketSameLine": false, + "arrowParens": "avoid" +} \ No newline at end of file diff --git a/config/prettierignore b/config/prettierignore new file mode 100644 index 0000000..77cab59 --- /dev/null +++ b/config/prettierignore @@ -0,0 +1,4 @@ +*.json +*.snap +_*.ts +_*.tsx diff --git a/dev.sh b/dev.sh new file mode 100644 index 0000000..0125c43 --- /dev/null +++ b/dev.sh @@ -0,0 +1,6 @@ +./node_modules/.bin/concurrently --names "REACT, REACT-COMPONENTS, GRAPHQL, GRAPHQL-TYPES" \ + -c "bgBlue.bold,bgGreen,bgMagenta.bold,bgGreen" \ + "cd packages/app && yarn dev" \ + "cd packages/app && yarn generate:graphqlcodegen -w" \ + "cd packages/server && yarn dev" \ + "cd packages/server && yarn generate:graphqlcodegen -w" \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..d60446a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,53 @@ +version: "2" +services: + graphql: + image: maapteh.com:443/graphql-server + build: + context: ./packages/server + dockerfile: Dockerfile + args: + node_env: development + environment: + - PORT=4000 + ports: + - 4000:4000 + volumes: + - ./packages/server:/usr/app + - /usr/app/node_modules + command: yarn dev + + web: + image: maapteh.com:443/graphql-app + build: + context: ./packages/app + dockerfile: Dockerfile + args: + node_env: development + environment: + - PORT=4001 + - LOG_LEVEL=debug + - GRAPHQL_ENDPOINT=http://localhost:3000/graphql + ports: + - 4001:4001 + volumes: + - ./packages/app:/usr/app + - /usr/app/node_modules + command: yarn dev + + nginx: + image: maapteh.com:443/graphql-nginx + build: + context: ./config/nginx + dockerfile: Dockerfile + args: + node_env: development + IS_DOCKER: "true" + volumes: + - ./config/nginx:/etc/app + - /usr/app/node_modules + links: + - graphql + - web + ports: + - 3000:80 + command: yarn start diff --git a/lerna.json b/lerna.json new file mode 100644 index 0000000..d821e92 --- /dev/null +++ b/lerna.json @@ -0,0 +1,18 @@ +{ + "lerna": "0.0.4", + "npmClient": "yarn", + "useWorkspaces": true, + "packages": ["packages/*"], + "version": "independent", + "hoist": false, + "command": { + "publish": { + "ignoreChanges": ["ignored-file", "*.md"], + "message": "chore(release): publish" + }, + "bootstrap": { + "ignore": "component-*", + "npmClientArgs": ["--no-package-lock"] + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..7b7c7d8 --- /dev/null +++ b/package.json @@ -0,0 +1,46 @@ +{ + "name": "sample-app", + "version": "0.0.4", + "private": true, + "license": "MIT", + "engines": { + "node": "^10.16.x", + "yarn": ">= 1.17.3" + }, + "scripts": { + "postinstall": "lerna exec -- yarn install && lerna run prepare", + "audit": "lerna run audit", + "start": "./node_modules/.bin/concurrently --names 'REACT, react-autogen, GRAPHQL, graphql-autogen' -c 'blue.bold,gray,cyan.bold,gray' 'cd packages/app && yarn dev' 'cd packages/app && yarn generate:graphqlcodegen -w' 'cd packages/server && yarn dev' 'cd packages/server && yarn generate:graphqlcodegen -w'", + "dev": "yarn start", + "build": "lerna run build", + "lint": "lerna run lint", + "test": "lerna run test", + "clean": "find . -name \"node_modules\" -exec rm -rf '{}' + && find . -name \"dist\" -exec rm -rf '{}' +", + "------ playground ------": "----------------------", + "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' ", + "test:coverage": "lerna run test:coverage", + "outdated": "yarn outdated", + "upgrade": "yarn upgrade-interactive --latest" + }, + "workspaces": [ + "packages/app", + "packages/server" + ], + "resolutions": { + "graphql": "14.5.5" + }, + "devDependencies": { + "@graphql-inspector/actions": "latest", + "concurrently": "4.1.0", + "husky": "3.0.5", + "lerna": "^3.13.1", + "typescript": "3.6.3" + }, + "graphql-inspector": { + "diff": true, + "schema": { + "ref": "master", + "path": "packages/server/src/_schema.graphql" + } + } +} diff --git a/packages/app/.babelrc b/packages/app/.babelrc new file mode 100644 index 0000000..d3432b1 --- /dev/null +++ b/packages/app/.babelrc @@ -0,0 +1,26 @@ +{ + "env": { + "development": { + "presets": [ + "next/babel" + ] + }, + "production": { + "presets": [ + "next/babel" + ] + }, + "test": { + "presets": [ + [ + "next/babel", + { + "preset-env": { + "modules": "commonjs" + } + } + ] + ] + } + } +} diff --git a/packages/app/.eslintignore b/packages/app/.eslintignore new file mode 100644 index 0000000..5cf08eb --- /dev/null +++ b/packages/app/.eslintignore @@ -0,0 +1,6 @@ +node_modules/* +next.config.js +*.d.ts +*.spec.ts +*.spec.tsx +src/graphql/_generated-* \ No newline at end of file diff --git a/packages/app/.eslintrc b/packages/app/.eslintrc new file mode 100644 index 0000000..6a24673 --- /dev/null +++ b/packages/app/.eslintrc @@ -0,0 +1,34 @@ +{ + "parser": "babel-eslint", + "env": { + "browser": true, + "node": true + }, + "extends": ["airbnb", "prettier", "prettier/react"], + "plugins": [ + "react-hooks", + "prettier", + "graphql" + ], + "rules": { + "react/jsx-filename-extension": ["error", { "extensions": [".js", ".jsx", ".ts", ".tsx"] }], + "react-hooks/rules-of-hooks": "error", + "react-hooks/exhaustive-deps": "warn", + "import/prefer-default-export": 0, + "react/require-default-props": 0, + "prettier/prettier": ["error"], + "graphql/template-strings": ["error", { + "env": "apollo", + }], + }, + "settings": { + "import/resolver": { + "node": { + "extensions": [".js", ".jsx", ".ts", ".tsx"] + } + } + }, + "globals": { + "React": "writable" + } +} \ No newline at end of file diff --git a/packages/app/.graphqlconfig b/packages/app/.graphqlconfig new file mode 100644 index 0000000..a4e970e --- /dev/null +++ b/packages/app/.graphqlconfig @@ -0,0 +1,8 @@ +{ + "schemaPath": "../server/src/_schema.graphql", + "extensions": { + "endpoints": { + "dev": "http://localhost:4000/graphql" + } + } +} \ No newline at end of file diff --git a/packages/app/.lintstagedrc b/packages/app/.lintstagedrc new file mode 100644 index 0000000..7364d7a --- /dev/null +++ b/packages/app/.lintstagedrc @@ -0,0 +1,4 @@ +{ + "./{src|pages}/**/*.{ts,tsx}": ["yarn lint --fix", "git add"], + "./src/graphql/_generated-schema.graphql": ["yarn inspector:validate"] +} \ No newline at end of file diff --git a/packages/app/.nmprc b/packages/app/.nmprc new file mode 100644 index 0000000..9aad027 --- /dev/null +++ b/packages/app/.nmprc @@ -0,0 +1,4 @@ +loglevel = warn +cache=.npm/cache +tmp=.npm/tmp +save-exact = true diff --git a/packages/app/.prettierrc b/packages/app/.prettierrc new file mode 100644 index 0000000..4385abc --- /dev/null +++ b/packages/app/.prettierrc @@ -0,0 +1,10 @@ +{ + "printWidth": 80, + "tabWidth": 4, + "useTabs": false, + "semi": true, + "singleQuote": true, + "trailingComma": "all", + "bracketSpacing": true, + "jsxBracketSameLine": false +} \ No newline at end of file diff --git a/packages/app/Dockerfile b/packages/app/Dockerfile new file mode 100644 index 0000000..4290691 --- /dev/null +++ b/packages/app/Dockerfile @@ -0,0 +1,16 @@ +FROM node:dubnium-slim + +ENV NODE_ENV=production + +WORKDIR /usr/app + +COPY package.json /usr/app/package.json +COPY yarn.lock /usr/app/yarn.lock + +RUN yarn install + +COPY . /usr/app + +RUN yarn build + +CMD yarn start diff --git a/packages/app/README.md b/packages/app/README.md new file mode 100644 index 0000000..a3bf824 --- /dev/null +++ b/packages/app/README.md @@ -0,0 +1,25 @@ +# NextJS Apollo GraphQL + +## Introduction +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. + +## Pre-requisites +- `yarn` (not needed when you installed from root) + +## DEVELOPMENT +1. Be sure to run the [server](../server/README.md) first +2. tab 1: `generate:graphqlcodegen -w` +3. tab 2: `yarn dev` + + +## GRAPHQL TOOLS + +- `yarn inspector:validate` see if all queries are done correctly +- `yarn inspector:coverage` see what types from the schema are actually used in this application + +## TEST +- `yarn test` + +## VISUAL VALIDATION + +- `yarn snapshots` with the PERCY_TOKEN it uploads the results \ No newline at end of file diff --git a/packages/app/codegen.yml b/packages/app/codegen.yml new file mode 100644 index 0000000..00bd0a7 --- /dev/null +++ b/packages/app/codegen.yml @@ -0,0 +1,35 @@ +schema: + - '../server/src/_schema.graphql' +overwrite: true +documents: [ + './src/modules/**/*.graphql.ts', + './src/components/**/*.graphql.ts', + './src/graphql/fragments/*.graphql.ts' + ] +config: {} +generates: + src/graphql/_generated-fragment-types.ts: + plugins: + - "fragment-matcher" + src/graphql/_generated-types.ts: + plugins: + - add: "/** eslint-disable */\n/** AUTO GENERATED, DO NOT EDIT OVERHERE */" + - typescript + - typescript-operations + src/graphql/_generated-hooks.tsx: + plugins: + - add: "/** eslint-disable */\n/** AUTO GENERATED, DO NOT EDIT OVERHERE */" + - typescript + - typescript-operations + - typescript-react-apollo + config: + reactApolloVersion: 3 + reactApolloImportFrom: '@apollo/react-hooks' + withComponent: false + withHOC: false + withHooks: true + src/graphql/_generated-schema.graphql: + plugins: + - schema-ast +require: + - "ts-node/register/transpile-only" diff --git a/packages/app/globals.d.ts b/packages/app/globals.d.ts new file mode 100644 index 0000000..6a69409 --- /dev/null +++ b/packages/app/globals.d.ts @@ -0,0 +1 @@ +import "@testing-library/jest-dom/extend-expect"; diff --git a/packages/app/jest.config.js b/packages/app/jest.config.js new file mode 100644 index 0000000..3a89107 --- /dev/null +++ b/packages/app/jest.config.js @@ -0,0 +1,24 @@ +module.exports = { + transform: { + '^.+\\.tsx?$': 'ts-jest', + }, + testRegex: '(/(components|modules)/.*(\\.|/)(test|spec))\\.(tsx?|ts?)$', + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], + modulePathIgnorePatterns: ['.next'], + moduleNameMapper: { + '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': + '/test/__mocks__/fileMock.js', + '\\.(css|scss)$': 'identity-obj-proxy', + }, + collectCoverage: true, + coverageReporters: ['json', 'lcov', 'text', 'text-summary'], + setupFilesAfterEnv: ['@testing-library/jest-dom/extend-expect'], + globals: { + 'ts-jest': { + diagnostics: false, + babelConfig: '.babelrc', + tsConfig: './jest.tsconfig.json', + }, + }, + rootDir: process.cwd(), +}; diff --git a/packages/app/jest.tsconfig.json b/packages/app/jest.tsconfig.json new file mode 100644 index 0000000..d88d28e --- /dev/null +++ b/packages/app/jest.tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "esnext", + "jsx": "react", + "sourceMap": false, + "experimentalDecorators": true, + "noImplicitUseStrict": true, + "removeComments": true, + "moduleResolution": "node", + "lib": ["es2017", "dom"], + "typeRoots": ["node_modules/@types", "src/@types"], + "types": ["jest"], + "esModuleInterop": true + }, + "exclude": ["node_modules", "out", ".next"] + } \ No newline at end of file diff --git a/packages/app/next-env.d.ts b/packages/app/next-env.d.ts new file mode 100644 index 0000000..0f3980b --- /dev/null +++ b/packages/app/next-env.d.ts @@ -0,0 +1,7 @@ +/// +/// + +declare module '*.scss' { + const styles: { [className: string]: string }; + export default styles; +} diff --git a/packages/app/next.config.js b/packages/app/next.config.js new file mode 100644 index 0000000..96e4c42 --- /dev/null +++ b/packages/app/next.config.js @@ -0,0 +1,40 @@ +const webpack = require('webpack'); +const withSass = require('@zeit/next-sass'); + +const localIdentName = + process.env.NODE_ENV === 'development' + ? '[local]__[hash:base64:5]' + : '__[hash:base64:5]'; + +module.exports = withSass({ + cssModules: true, + cssLoaderOptions: { + localIdentName, + importLoaders: 1, + }, + webpack: config => { + config.module.rules.map(item => { + const loader = item.use && item.use[0] && item.use[0].loader; + if (loader === 'css-loader/locals') { + item.use.unshift({ + loader: 'dts-css-modules-loader', + options: { + namedExport: true, + banner: '// This file is generated automatically', + }, + }); + } + }); + + config.plugins.push( + new webpack.DefinePlugin({ + 'process.env.GRAPHQL_ENDPOINT': JSON.stringify( + process.env.GRAPHQL_ENDPOINT, + ), + }), + ); + + return config; + }, + generateEtags: false, +}); diff --git a/packages/app/package.json b/packages/app/package.json new file mode 100644 index 0000000..364ff5a --- /dev/null +++ b/packages/app/package.json @@ -0,0 +1,97 @@ +{ + "name": "graphql-app", + "version": "0.0.4", + "author": "", + "license": "MIT", + "repository": { + "type": "git", + "url": "maapteh/graphql-modules-app/packages/graphql-app" + }, + "engines": { + "node": "^10.16.x", + "yarn": ">= 1.17.3" + }, + "scripts": { + "prepare": "yarn build", + "precommit": "lint-staged", + "audit": "yarn audit", + "dev": "next -p 4001", + "build": "tsc -v && next build", + "start": "PORT=$PORT NODE_ENV=production node --optimize_for_size --max_old_space_size=920 --gc_interval=100 server.js", + "start:local": "NODE_ENV=production next start -p 4001", + "test": "jest --config=jest.config.js", + "test:coverage": "yarn test --verbose --coverage", + "test:ci": "yarn run coverage -- --ci --maxWorkers=2 --reporters=default --reporters=jest-junit", + "lint": "eslint './{src,pages}/**/*.{ts,tsx}'", + "clean": "find . -name \"node_modules\" -exec rm -rf '{}' + && find . -name \"dist\" -exec rm -rf '{}' +", + "generate:graphqlcodegen": "graphql-codegen", + "inspector:validate": "graphql-inspector validate src/graphql/_generated-hooks.tsx src/graphql/_generated-schema.graphql --require ts-node/register", + "inspector:coverage": "graphql-inspector coverage src/graphql/_generated-hooks.tsx src/graphql/_generated-schema.graphql --require ts-node/register", + "snapshots": "percy exec -- node test/visual-regression/index.js" + }, + "resolutions": { + "graphql": "14.5.5" + }, + "dependencies": { + "@apollo/react-hooks": "3.1.0", + "@apollo/react-ssr": "3.1.0", + "@apollo/react-testing": "3.1.0", + "@types/react": "16.8.6", + "@zeit/next-css": "1.0.1", + "@zeit/next-sass": "1.0.1", + "apollo": "2.18.1", + "apollo-cache-inmemory": "1.5.1", + "apollo-client": "2.6.4", + "apollo-link": "1.2.8", + "apollo-link-batch-http": "1.2.8", + "apollo-link-error": "1.1.7", + "apollo-link-http": "1.5.11", + "apollo-utilities": "1.3.2", + "classnames": "2.2.6", + "express": "4.16.4", + "graphql": "14.1.1", + "graphql-tag": "2.10.1", + "isomorphic-unfetch": "3.0.0", + "next": "9.0.5", + "node-sass": "4.9.3", + "prettier": "1.18.2", + "prop-types": "15.7.2", + "react": "16.9.0", + "react-dom": "16.9.0", + "react-intersection-observer": "8.24.1", + "ts-node": "8.3.0", + "typescript": "3.2.1", + "webpack": "4.29.6" + }, + "devDependencies": { + "@graphql-codegen/add": "1.7.0", + "@graphql-codegen/cli": "1.7.0", + "@graphql-codegen/fragment-matcher": "1.7.0", + "@graphql-codegen/schema-ast": "1.7.0", + "@graphql-codegen/time": "1.7.0", + "@graphql-codegen/typescript-operations": "1.7.0", + "@graphql-codegen/typescript-react-apollo": "1.7.0", + "@graphql-inspector/cli": "1.24.0", + "@testing-library/jest-dom": "4.1.0", + "@testing-library/react": "9.1.4", + "@testing-library/react-hooks": "2.0.1", + "@types/jest": "24.0.18", + "babel-eslint": "10.0.3", + "babel-jest": "24.9.0", + "dts-css-modules-loader": "1.0.1", + "eslint": "6.3.0", + "eslint-config-airbnb": "18.0.1", + "eslint-config-prettier": "6.2.0", + "eslint-plugin-graphql": "^3.0.3", + "eslint-plugin-import": "2.18.2", + "eslint-plugin-jsx-a11y": "6.2.3", + "eslint-plugin-prettier": "3.1.0", + "eslint-plugin-react": "7.14.3", + "eslint-plugin-react-hooks": "2.0.1", + "identity-obj-proxy": "3.0.0", + "jest": "24.9.0", + "lint-staged": "9.2.5", + "react-test-renderer": "16.9.0", + "ts-jest": "24.0.2" + } +} diff --git a/packages/app/pages/example.js b/packages/app/pages/example.js new file mode 100644 index 0000000..f794d56 --- /dev/null +++ b/packages/app/pages/example.js @@ -0,0 +1,59 @@ +import { App } from '../src/modules/App'; +import { withApollo } from '../src/graphql/apollo'; + +const Index = () => ( + +

Showcase GraphQL

+ +

GraphQL server

+

+ This is why this sample has been setup. It's a showcase for + having real modules in your graphql server. +

+ +

React client

+

+ NextJS is used because it gives a setup in which it's esy to + have an isomorphic application without too much code. This way there + is more focus on the GraphQL part. +

+

+ We only need to provide an Apollo client with the React application + . Now we are able to have control if component needs to be rendered + server or client side and also we can debatch the component (mostly + on components where the data can be utterly slow, for example + reservation airline). +

+ +

Links

+ + + +

+ + This application is using the free open api of{' '} + bol.com at the moment for the + products part. The development version can also be run in mock + mode, no keys required. + +

+
+); + +export default withApollo(Index); diff --git a/packages/app/pages/index.js b/packages/app/pages/index.js new file mode 100644 index 0000000..da7650c --- /dev/null +++ b/packages/app/pages/index.js @@ -0,0 +1,51 @@ +import { App } from '../src/modules/App'; +import { withApollo } from '../src/graphql/apollo'; +import { ProductComponent } from '../src/modules/product/product-component'; + +const Example = () => ( + +

GraphQL Modules

+ +

React client with auto generated Apollo Hooks

+

+ Example with Batched/non-batched queries and non SSR option. On this + demonstration page the first product is rendered with SSR. The + second is rendered in the main client call and the third is + debatched so not in the main client call. +

+

+ When you came from the 'products' page everything is + retrieved from the cache so nothing is retrieved. For the + demonstration effect just reload this page. +

+ + + + + + {/* debatch non ssr test, by default everything is batched but easy to debatch when you expect the server to be slow */} + + +

React server

+

+ Because we have dataloader in place the server will combine both + product calls into one. +

+ +

+ + This application is using the free open api of{' '} + bol.com at the moment for the + products part. Some other parts will have mocked data so without + a key you can still play arround. + +

+
+); + +export default withApollo(Example); diff --git a/packages/app/pages/product.js b/packages/app/pages/product.js new file mode 100644 index 0000000..9550b8d --- /dev/null +++ b/packages/app/pages/product.js @@ -0,0 +1 @@ +export { default } from './product/[id]'; diff --git a/packages/app/pages/product/[id].js b/packages/app/pages/product/[id].js new file mode 100644 index 0000000..a4523c8 --- /dev/null +++ b/packages/app/pages/product/[id].js @@ -0,0 +1,18 @@ +import * as React from 'react'; +import { useRouter } from 'next/router'; +import { withApollo } from '../../src/graphql/apollo'; +import { App } from '../../src/modules/App'; +import { Product } from '../../src/modules/product/product'; + +const ProductPage = () => { + const router = useRouter(); + const { id } = router.query; + + return ( + + + + ); +}; + +export default withApollo(ProductPage); diff --git a/packages/app/pages/products.js b/packages/app/pages/products.js new file mode 100644 index 0000000..84e18df --- /dev/null +++ b/packages/app/pages/products.js @@ -0,0 +1,18 @@ +import { withApollo } from '../src/graphql/apollo'; +import { App } from '../src/modules/App'; +import { ProductsList } from '../src/modules/products-list/products-list'; + +const Products = () => ( + +

+ Example below is using the open API data from bol.com We use + 'apollo-cache-inmemory' in the client when retrieving all + data in list view, so main product details are there allready (for + example image) when switching route. +

+

Products list

+ +
+); + +export default withApollo(Products); diff --git a/packages/app/server.js b/packages/app/server.js new file mode 100644 index 0000000..745cec4 --- /dev/null +++ b/packages/app/server.js @@ -0,0 +1,25 @@ +const express = require('express'); +const next = require('next'); +const dev = process.env.NODE_ENV !== 'production'; +const port = parseInt(process.env.PORT, 10) || 4001; +const app = next({ dev }); +const handler = app.getRequestHandler(); + +app.prepare().then(() => { + const server = express(); + + server.get('/product/:id', (req, res) => { + const actualPage = '/product'; + const queryParams = { id: req.params.id }; + app.render(req, res, actualPage, queryParams); + }); + + server.get('*', (req, res) => { + return handler(req, res); + }); + + server.listen(port, err => { + if (err) throw err; + console.log(`🚀 REACT at http://localhost:${port}`); + }); +}); diff --git a/packages/app/src/elements/image/image.scss b/packages/app/src/elements/image/image.scss new file mode 100644 index 0000000..09c385e --- /dev/null +++ b/packages/app/src/elements/image/image.scss @@ -0,0 +1,18 @@ +.root { + float: left; + overflow: hidden; + box-sizing: content-box; + width: 168px; + height: 209px; + padding: 0 18px 14px 0; + line-height: 0; + opacity:0; + transition: opacity 400ms ease-in; +} +.img { + width: 100%; +} + +.appear { + opacity: 1; +} \ No newline at end of file diff --git a/packages/app/src/elements/image/image.tsx b/packages/app/src/elements/image/image.tsx new file mode 100644 index 0000000..6afd77a --- /dev/null +++ b/packages/app/src/elements/image/image.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import classNames from 'classnames'; +import { useInView } from 'react-intersection-observer'; +import style from './image.scss'; + +// TODO: add aspect ratio's + +type Props = { + url: string; + title?: string; + rootMargin?: string; +}; + +export const Image = ({ url, title = '', rootMargin }: Props) => { + // This set of values serves to grow or shrink each side of the root element's bounding box before computing intersections + const margin = + rootMargin && /((((.\d*)?(px))){4})/.test(rootMargin) + ? rootMargin + : '20px 0px 280px 0px'; + const [image, setImage] = React.useState(); + const [ref, inView] = useInView({ + threshold: 0, + rootMargin: margin, + }); + + if (inView && !image) { + setImage(url); + } + + const css = classNames(style.root, image && `${style.appear}`); + + return ( +
+ {image && {title}} +
+ ); +}; diff --git a/packages/app/src/elements/logo-bol/logo-bol.tsx b/packages/app/src/elements/logo-bol/logo-bol.tsx new file mode 100644 index 0000000..e48bdc4 --- /dev/null +++ b/packages/app/src/elements/logo-bol/logo-bol.tsx @@ -0,0 +1,10 @@ +export const LogoBol = () => { + return ( + BOL.com + ); +}; diff --git a/packages/app/src/graphql/_generated-fragment-types.ts b/packages/app/src/graphql/_generated-fragment-types.ts new file mode 100644 index 0000000..2aa0402 --- /dev/null +++ b/packages/app/src/graphql/_generated-fragment-types.ts @@ -0,0 +1,21 @@ + + export interface IntrospectionResultData { + __schema: { + types: { + kind: string; + name: string; + possibleTypes: { + name: string; + }[]; + }[]; + }; + } + + const result: IntrospectionResultData = { + "__schema": { + "types": [] + } +}; + + export default result; + \ No newline at end of file diff --git a/packages/app/src/graphql/_generated-hooks.tsx b/packages/app/src/graphql/_generated-hooks.tsx new file mode 100644 index 0000000..3d6320a --- /dev/null +++ b/packages/app/src/graphql/_generated-hooks.tsx @@ -0,0 +1,220 @@ +/** eslint-disable */ +/** AUTO GENERATED, DO NOT EDIT OVERHERE */ +import gql from 'graphql-tag'; +import * as ApolloReactCommon from '@apollo/react-common'; +import * as ApolloReactHooks from '@apollo/react-hooks'; +export type Maybe = T | null; +/** All built-in and custom scalars, mapped to their actual values */ +export type Scalars = { + ID: string, + String: string, + Boolean: boolean, + Int: number, + Float: number, +}; + + +export enum CacheControlScope { + Public = 'PUBLIC', + Private = 'PRIVATE' +} + +export type Product = { + __typename?: 'Product', + id: Scalars['String'], + ean?: Maybe, + title: Scalars['String'], + specsTag?: Maybe, + summary?: Maybe, + rating?: Maybe, + shortDescription?: Maybe, + urls?: Maybe>>, + images?: Maybe>>, + offerData?: Maybe, + parentCategoryPaths?: Maybe, +}; + +export type ProductImage = { + __typename?: 'ProductImage', + type?: Maybe, + key?: Maybe, + url?: Maybe, +}; + +export type ProductOfferData = { + __typename?: 'ProductOfferData', + bolCom?: Maybe, + nonProfessionalSellers?: Maybe, + professionalSellers?: Maybe, + offers?: Maybe, +}; + +export type ProductOfferDataOffers = { + __typename?: 'ProductOfferDataOffers', + id?: Maybe, + condition?: Maybe, + price?: Maybe, + listPrice?: Maybe, + availabilityCode?: Maybe, + availabilityDescription?: Maybe, + comment?: Maybe, + seller?: Maybe, + bestOffer?: Maybe, + releaseDate?: Maybe, +}; + +export type ProductParentCategory = { + __typename?: 'ProductParentCategory', + id?: Maybe, + name?: Maybe, +}; + +export type ProductParentCategoryPaths = { + __typename?: 'ProductParentCategoryPaths', + parentCategories?: Maybe, +}; + +/** Products for a specific category, model is taken as is from bol.com */ +export type Products = { + __typename?: 'Products', + products?: Maybe>>, + schemaVersion?: Maybe, + totalResultSize?: Maybe, + originalRequest?: Maybe, +}; + +export type ProductSeller = { + __typename?: 'ProductSeller', + id?: Maybe, + sellerType?: Maybe, + displayName?: Maybe, + url?: Maybe, + topSeller?: Maybe, + useWarrantyRepairConditions?: Maybe, +}; + +export type ProductsOriginalRequest = { + __typename?: 'ProductsOriginalRequest', + category?: Maybe>>, +}; + +export type ProductsOriginalRequestCategory = { + __typename?: 'ProductsOriginalRequestCategory', + id?: Maybe, + name?: Maybe, + productCount?: Maybe, +}; + +export type ProductUrls = { + __typename?: 'ProductUrls', + key?: Maybe, + value?: Maybe, +}; + +export type Query = { + __typename?: 'Query', + /** Get all products for a specific list */ + getProducts?: Maybe, + /** Get single product */ + getProduct?: Maybe, +}; + + +export type QueryGetProductsArgs = { + id: Scalars['String'] +}; + + +export type QueryGetProductArgs = { + id: Scalars['String'] +}; +export type ProductFragment = ( + { __typename?: 'Product' } + & Pick + & { images: Maybe + )>>>, urls: Maybe + )>>> } +); + +export type GetProductQueryVariables = { + id: Scalars['String'] +}; + + +export type GetProductQuery = ( + { __typename?: 'Query' } + & { getProduct: Maybe<{ __typename?: 'Product' } + & ProductFragment + > } +); + +export type GetProductsQueryVariables = { + id: Scalars['String'] +}; + + +export type GetProductsQuery = ( + { __typename?: 'Query' } + & { getProducts: Maybe<( + { __typename?: 'Products' } + & { products: Maybe>> } + )> } +); +export const ProductFragmentDoc = gql` + fragment product on Product { + id + title + rating + shortDescription + images { + key + url + } + urls { + key + value + } +} + `; +export const GetProductDocument = gql` + query getProduct($id: String!) { + getProduct(id: $id) { + ...product + } +} + ${ProductFragmentDoc}`; + + export function useGetProductQuery(baseOptions?: ApolloReactHooks.QueryHookOptions) { + return ApolloReactHooks.useQuery(GetProductDocument, baseOptions); + } + export function useGetProductLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptions) { + return ApolloReactHooks.useLazyQuery(GetProductDocument, baseOptions); + } + +export type GetProductQueryHookResult = ReturnType; +export type GetProductQueryResult = ApolloReactCommon.QueryResult; +export const GetProductsDocument = gql` + query getProducts($id: String!) { + getProducts(id: $id) { + products { + ...product + } + } +} + ${ProductFragmentDoc}`; + + export function useGetProductsQuery(baseOptions?: ApolloReactHooks.QueryHookOptions) { + return ApolloReactHooks.useQuery(GetProductsDocument, baseOptions); + } + export function useGetProductsLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptions) { + return ApolloReactHooks.useLazyQuery(GetProductsDocument, baseOptions); + } + +export type GetProductsQueryHookResult = ReturnType; +export type GetProductsQueryResult = ApolloReactCommon.QueryResult; \ No newline at end of file diff --git a/packages/app/src/graphql/_generated-schema.graphql b/packages/app/src/graphql/_generated-schema.graphql new file mode 100644 index 0000000..c1a129b --- /dev/null +++ b/packages/app/src/graphql/_generated-schema.graphql @@ -0,0 +1,96 @@ +""" auto generated """ +directive @cacheControl(maxAge: Int, scope: CacheControlScope) on OBJECT | FIELD_DEFINITION | INTERFACE + +enum CacheControlScope { + PUBLIC + PRIVATE +} + +type Product { + id: String! + ean: String + title: String! + specsTag: String + summary: String + rating: Int + shortDescription: String + urls: [ProductUrls] + images: [ProductImage] + offerData: ProductOfferData + parentCategoryPaths: ProductParentCategoryPaths +} + +type ProductImage { + type: String + key: String + url: String +} + +type ProductOfferData { + bolCom: Int + nonProfessionalSellers: Int + professionalSellers: Int + offers: ProductOfferDataOffers +} + +type ProductOfferDataOffers { + id: String + condition: String + price: Int + listPrice: Float + availabilityCode: String + availabilityDescription: String + comment: String + seller: ProductSeller + bestOffer: Boolean + releaseDate: String +} + +type ProductParentCategory { + id: String + name: String +} + +type ProductParentCategoryPaths { + parentCategories: ProductParentCategory +} + +"""Products for a specific category, model is taken as is from bol.com""" +type Products { + products: [Product] + schemaVersion: String + totalResultSize: Int + originalRequest: ProductsOriginalRequest +} + +type ProductSeller { + id: String + sellerType: String + displayName: String + url: String + topSeller: Boolean + useWarrantyRepairConditions: Boolean +} + +type ProductsOriginalRequest { + category: [ProductsOriginalRequestCategory] +} + +type ProductsOriginalRequestCategory { + id: Int + name: String + productCount: Int +} + +type ProductUrls { + key: String + value: String +} + +type Query { + """Get all products for a specific list""" + getProducts(id: String!): Products + + """Get single product""" + getProduct(id: String!): Product +} diff --git a/packages/app/src/graphql/_generated-types.ts b/packages/app/src/graphql/_generated-types.ts new file mode 100644 index 0000000..c9c9f43 --- /dev/null +++ b/packages/app/src/graphql/_generated-types.ts @@ -0,0 +1,165 @@ +/** eslint-disable */ +/** AUTO GENERATED, DO NOT EDIT OVERHERE */ +export type Maybe = T | null; +/** All built-in and custom scalars, mapped to their actual values */ +export type Scalars = { + ID: string, + String: string, + Boolean: boolean, + Int: number, + Float: number, +}; + + +export enum CacheControlScope { + Public = 'PUBLIC', + Private = 'PRIVATE' +} + +export type Product = { + __typename?: 'Product', + id: Scalars['String'], + ean?: Maybe, + title: Scalars['String'], + specsTag?: Maybe, + summary?: Maybe, + rating?: Maybe, + shortDescription?: Maybe, + urls?: Maybe>>, + images?: Maybe>>, + offerData?: Maybe, + parentCategoryPaths?: Maybe, +}; + +export type ProductImage = { + __typename?: 'ProductImage', + type?: Maybe, + key?: Maybe, + url?: Maybe, +}; + +export type ProductOfferData = { + __typename?: 'ProductOfferData', + bolCom?: Maybe, + nonProfessionalSellers?: Maybe, + professionalSellers?: Maybe, + offers?: Maybe, +}; + +export type ProductOfferDataOffers = { + __typename?: 'ProductOfferDataOffers', + id?: Maybe, + condition?: Maybe, + price?: Maybe, + listPrice?: Maybe, + availabilityCode?: Maybe, + availabilityDescription?: Maybe, + comment?: Maybe, + seller?: Maybe, + bestOffer?: Maybe, + releaseDate?: Maybe, +}; + +export type ProductParentCategory = { + __typename?: 'ProductParentCategory', + id?: Maybe, + name?: Maybe, +}; + +export type ProductParentCategoryPaths = { + __typename?: 'ProductParentCategoryPaths', + parentCategories?: Maybe, +}; + +/** Products for a specific category, model is taken as is from bol.com */ +export type Products = { + __typename?: 'Products', + products?: Maybe>>, + schemaVersion?: Maybe, + totalResultSize?: Maybe, + originalRequest?: Maybe, +}; + +export type ProductSeller = { + __typename?: 'ProductSeller', + id?: Maybe, + sellerType?: Maybe, + displayName?: Maybe, + url?: Maybe, + topSeller?: Maybe, + useWarrantyRepairConditions?: Maybe, +}; + +export type ProductsOriginalRequest = { + __typename?: 'ProductsOriginalRequest', + category?: Maybe>>, +}; + +export type ProductsOriginalRequestCategory = { + __typename?: 'ProductsOriginalRequestCategory', + id?: Maybe, + name?: Maybe, + productCount?: Maybe, +}; + +export type ProductUrls = { + __typename?: 'ProductUrls', + key?: Maybe, + value?: Maybe, +}; + +export type Query = { + __typename?: 'Query', + /** Get all products for a specific list */ + getProducts?: Maybe, + /** Get single product */ + getProduct?: Maybe, +}; + + +export type QueryGetProductsArgs = { + id: Scalars['String'] +}; + + +export type QueryGetProductArgs = { + id: Scalars['String'] +}; +export type ProductFragment = ( + { __typename?: 'Product' } + & Pick + & { images: Maybe + )>>>, urls: Maybe + )>>> } +); + +export type GetProductQueryVariables = { + id: Scalars['String'] +}; + + +export type GetProductQuery = ( + { __typename?: 'Query' } + & { getProduct: Maybe<{ __typename?: 'Product' } + & ProductFragment + > } +); + +export type GetProductsQueryVariables = { + id: Scalars['String'] +}; + + +export type GetProductsQuery = ( + { __typename?: 'Query' } + & { getProducts: Maybe<( + { __typename?: 'Products' } + & { products: Maybe>> } + )> } +); diff --git a/packages/app/src/graphql/apollo.js b/packages/app/src/graphql/apollo.js new file mode 100644 index 0000000..32a1130 --- /dev/null +++ b/packages/app/src/graphql/apollo.js @@ -0,0 +1,198 @@ +import React, { useMemo } from 'react'; +import Head from 'next/head'; +import fetch from 'isomorphic-unfetch'; +import { ApolloProvider } from '@apollo/react-hooks'; +import { ApolloClient } from 'apollo-client'; +import { InMemoryCache } from 'apollo-cache-inmemory'; + +import { ApolloLink, split } from 'apollo-link'; +import { onError } from 'apollo-link-error'; +import { HttpLink } from 'apollo-link-http'; +import { BatchHttpLink } from 'apollo-link-batch-http'; + +import { toIdValue } from 'apollo-utilities'; +import { fragmentMatcher } from './fragment-matcher'; +import { version } from '../../package.json'; + +let apolloClient = null; + +const uri = process.env.GRAPHQL_ENDPOINT + ? process.env.GRAPHQL_ENDPOINT + : 'http://localhost:4000/graphql'; + +const cache = new InMemoryCache({ + fragmentMatcher, + cacheRedirects: { + Query: { + // Here we map the data we get in product list view with the one for detail view + // see: https://www.apollographql.com/docs/react/features/performance.html + getProduct: (_, args) => + toIdValue( + cache.config.dataIdFromObject({ + __typename: 'Product', + id: args.id, + }), + ), + }, + }, +}); + +const batchHttpLink = new BatchHttpLink({ + uri, + credentials: 'include', // 'same-origin' + headers: { batch: 'true ' }, +}); + +// link to use if not batching +const httpLink = new HttpLink({ + uri, + credentials: 'include', // 'same-origin' +}); + +// Polyfill fetch() on the server (used by apollo-client) +if (!process.browser) { + global.fetch = fetch; +} + +/** + * Creates and provides the apolloContext + * to a next.js PageTree. Use it by wrapping + * your PageComponent via HOC pattern. + * @param {Function|Class} PageComponent + * @param {Object} [config] + * @param {Boolean} [config.ssr=true] + */ +export function withApollo(PageComponent, { ssr = true } = {}) { + const WithApollo = ({ apolloClient, apolloState, ...pageProps }) => { + const client = useMemo( + () => apolloClient || initApolloClient(apolloState), + [apolloClient, apolloState], + ); + return ( + + + + ); + }; + + // Set the correct displayName in development + if (process.env.NODE_ENV !== 'production') { + const displayName = + PageComponent.displayName || PageComponent.name || 'Component'; + + if (displayName === 'App') { + console.warn('This withApollo HOC only works with PageComponents.'); + } + + WithApollo.displayName = `withApollo(${displayName})`; + } + + // Allow Next.js to remove getInitialProps from the browser build + if (typeof window === 'undefined') { + if (ssr) { + WithApollo.getInitialProps = async ctx => { + const { AppTree } = ctx; + + let pageProps = {}; + if (PageComponent.getInitialProps) { + pageProps = await PageComponent.getInitialProps(ctx); + } + + // Run all GraphQL queries in the component tree + // and extract the resulting data + const apolloClient = initApolloClient(); + + try { + // Run all GraphQL queries + const { getDataFromTree } = await import( + '@apollo/react-ssr' + ); + await getDataFromTree( + , + ); + } catch (error) { + // Prevent Apollo Client GraphQL errors from crashing SSR. + // Handle them in components via the data.error prop: + // https://www.apollographql.com/docs/react/api/react-apollo.html#graphql-query-data-error + console.error( + 'Error while running `getDataFromTree`', + error, + ); + } + + // getDataFromTree does not call componentWillUnmount + // head side effect therefore need to be cleared manually + Head.rewind(); + + // Extract query data from the Apollo store + const apolloState = apolloClient.cache.extract(); + + return { + ...pageProps, + apolloState, + }; + }; + } + } + + return WithApollo; +} + +/** + * Always creates a new apollo client on the server + * Creates or reuses apollo client in the browser. + * @param {Object} initialState + */ +function initApolloClient(initialState) { + // Make sure to create a new client for every server-side request so that data + // isn't shared between connections (which would be bad) + if (typeof window === 'undefined') { + return createApolloClient(initialState); + } + + // Reuse client on the client-side + if (!apolloClient) { + apolloClient = createApolloClient(initialState); + } + + return apolloClient; +} + +/** + * Creates and configures the ApolloClient + * @param {Object} [initialState={}] + */ +function createApolloClient(initialState = {}) { + // Check out https://github.com/zeit/next.js/pull/4611 if you want to use the AWSAppSyncClient + const isBrowser = typeof window !== 'undefined'; + return new ApolloClient({ + connectToDevTools: isBrowser, + ssrMode: !isBrowser, // Disables forceFetch on the server (so queries are only run once) + link: ApolloLink.from([ + onError(({ graphQLErrors, networkError }) => { + if (graphQLErrors) + graphQLErrors.map(({ message, locations, path }) => + console.log( + `[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`, + ), + ); + if (networkError) + console.log(`[Network error]: ${networkError}`); + }), + + split( + operation => operation.getContext().important === true, + httpLink, // if the test is true -- debatch + batchHttpLink, // otherwise, batching is fine + ), + ]), + cache: cache.restore(initialState || {}), + name: 'Sample application', + version, + }); +} diff --git a/packages/app/src/graphql/fragment-matcher.js b/packages/app/src/graphql/fragment-matcher.js new file mode 100644 index 0000000..af7f833 --- /dev/null +++ b/packages/app/src/graphql/fragment-matcher.js @@ -0,0 +1,6 @@ +import { IntrospectionFragmentMatcher } from 'apollo-cache-inmemory'; +import introspectionResults from './_generated-fragment-types'; + +export const fragmentMatcher = new IntrospectionFragmentMatcher({ + introspectionQueryResultData: introspectionResults, +}); diff --git a/packages/app/src/graphql/fragments/product.fragment.graphql.ts b/packages/app/src/graphql/fragments/product.fragment.graphql.ts new file mode 100644 index 0000000..20e6507 --- /dev/null +++ b/packages/app/src/graphql/fragments/product.fragment.graphql.ts @@ -0,0 +1,18 @@ +import gql from 'graphql-tag'; + +export const FRAGMENT_PRODUCT = gql` + fragment product on Product { + id + title + rating + shortDescription + images { + key + url + } + urls { + key + value + } + } +`; diff --git a/packages/app/src/modules/App.tsx b/packages/app/src/modules/App.tsx new file mode 100644 index 0000000..11330fe --- /dev/null +++ b/packages/app/src/modules/App.tsx @@ -0,0 +1,19 @@ +import Head from 'next/head'; +import { Header } from './header/header'; +import style from './app.scss'; + +export const App = ({ children, title = 'GraphQL modules example' }: any) => ( +
+ + workshop GRAPHQL - {title} + +