diff --git a/{{cookiecutter.project_slug}}/frontend/package.json b/{{cookiecutter.project_slug}}/frontend/package.json index 5c19366c7..3a3ceeecb 100644 --- a/{{cookiecutter.project_slug}}/frontend/package.json +++ b/{{cookiecutter.project_slug}}/frontend/package.json @@ -18,9 +18,22 @@ "react-scripts": "3.1.1" }, "devDependencies": { + "@apollo/react-common": "3.1.2", + "@apollo/react-components": "3.1.2", + "@apollo/react-hooks": "3.1.2", + "@apollo/react-testing": "3.1.2", + "apollo-boost": "0.4.4", + "apollo-cache-persist": "0.1.1", + "apollo-link-context": "1.0.19", + "apollo-link-rest": "0.7.3", + "apollo-link-schema": "1.2.4", + "apollo-link-state": "0.4.2", + "apollo-upload-client": "11.0.0", "axios": "0.19.0", "node-sass": "4.12.0", "raven-js": "3.27.2", + "react-alert": "5.5.0", + "react-alert-template-basic": "1.0.0", "react-document-title": "2.0.3", "react-router-dom": "5.1.2" }, diff --git a/{{cookiecutter.project_slug}}/frontend/src/apollo/cache.js b/{{cookiecutter.project_slug}}/frontend/src/apollo/cache.js new file mode 100644 index 000000000..ae62da7cc --- /dev/null +++ b/{{cookiecutter.project_slug}}/frontend/src/apollo/cache.js @@ -0,0 +1,9 @@ +import { InMemoryCache } from 'apollo-cache-inmemory' +import { persistCache } from 'apollo-cache-persist' +import localForage from 'localforage' + +const cache = new InMemoryCache() + +persistCache({cache, storage: localForage}) + +export default cache diff --git a/{{cookiecutter.project_slug}}/frontend/src/apollo/client.js b/{{cookiecutter.project_slug}}/frontend/src/apollo/client.js new file mode 100644 index 000000000..ab83ff397 --- /dev/null +++ b/{{cookiecutter.project_slug}}/frontend/src/apollo/client.js @@ -0,0 +1,36 @@ +import { ApolloClient } from 'apollo-client' +import { ApolloLink } from 'apollo-link' + +import { authLink, restLink, stateLink, uploadLink } from './links' +import cache from './cache' +import { mockLink } from './mocks' + +const defaultOptions = { + watchQuery: { + fetchPolicy: 'cache-and-network', + errorPolicy: 'ignore', + }, + query: { + fetchPolicy: 'cache-and-network', + errorPolicy: 'all', + }, + mutate: { + // NOTE: Using 'none' will allow Apollo to recognize errors even if the response + // includes {data: null} in it (graphene does this with each unsuccessful mutation!) + errorPolicy: 'none', + }, +} + +const link = ApolloLink.from([stateLink, restLink, authLink.concat(uploadLink)]) + +const client = new ApolloClient({ + link: + process.env.NODE_ENV === 'production' + ? link // never use mock for production + : ApolloLink.split(operation => operation.getContext().mock, mockLink, link), + cache, + connectToDevTools: process.env.NODE_ENV === 'production' ? false : true, + defaultOptions, +}) + +export default client diff --git a/{{cookiecutter.project_slug}}/frontend/src/apollo/links.js b/{{cookiecutter.project_slug}}/frontend/src/apollo/links.js new file mode 100644 index 000000000..ae0ba77bd --- /dev/null +++ b/{{cookiecutter.project_slug}}/frontend/src/apollo/links.js @@ -0,0 +1,43 @@ +import cookie from 'react-cookies' +import { setContext } from 'apollo-link-context' +import { createHttpLink } from 'apollo-link-http' +import { RestLink } from 'apollo-link-rest' +import { withClientState } from 'apollo-link-state' +import { createUploadLink } from 'apollo-upload-client' +import { merge } from 'lodash' + +import cache from './cache' + +// docs: https://www.apollographql.com/docs/link/links/http.html +export const httpLink = createHttpLink({ + uri: '/graphql/', + credentials: 'same-origin', +}) + +// docs: https://github.com/jaydenseric/apollo-upload-client +export const uploadLink = createUploadLink({ + uri: '/graphql/', + credentials: 'same-origin', +}) + +// docs: https://www.apollographql.com/docs/link/links/rest.html +export const restLink = new RestLink({ + uri: '/api/', + endpoints: {}, +}) + +// docs: https://www.apollographql.com/docs/link/links/state.html +export const stateLink = withClientState({ + cache, + ...merge({}), +}) + +export const authLink = setContext((_, { headers }) => { + return { + headers: { + ...headers, + 'X-CSRFToken': cookie.load('csrftoken'), + Authorization: `JWT ${cookie.load('jwt')}`, + }, + } +}) diff --git a/{{cookiecutter.project_slug}}/frontend/src/apollo/mocks/index.js b/{{cookiecutter.project_slug}}/frontend/src/apollo/mocks/index.js new file mode 100644 index 000000000..d845518ef --- /dev/null +++ b/{{cookiecutter.project_slug}}/frontend/src/apollo/mocks/index.js @@ -0,0 +1,19 @@ +import { SchemaLink } from 'apollo-link-schema' +import { addMockFunctionsToSchema } from 'graphql-tools' +import { assign } from 'lodash' + +import { mockSchema } from '../schema-parser' + +export const baseMocks = {} + +// Use this devMocks object for your development mock override +// Make sure to empty this part before your pull request +const devMocks = {} + +const combinedMocks = assign({}, baseMocks, devMocks) + +const schema = mockSchema() + +addMockFunctionsToSchema({ schema, mocks: combinedMocks }) + +export const mockLink = new SchemaLink({ schema }) diff --git a/{{cookiecutter.project_slug}}/frontend/src/apollo/schema-parser.js b/{{cookiecutter.project_slug}}/frontend/src/apollo/schema-parser.js new file mode 100644 index 000000000..39c32f507 --- /dev/null +++ b/{{cookiecutter.project_slug}}/frontend/src/apollo/schema-parser.js @@ -0,0 +1,24 @@ +import { findIndex, remove } from 'lodash' +import { buildClientSchema } from 'graphql' + +import * as jsonFile from './schema' + +export const mockSchema = () => { + // new json loader update adds a `defulat` parent level to json + const x = jsonFile.default + + // Apollo won't accept introspection that has any '__debug' values in json file, so the parser should remove them first + // To read more: https://github.research.chop.edu/DGD/nexus/issues/1318#issuecomment-18281 + + const queryTypeIndex = findIndex(x.data.__schema.types, ['name', 'Query']) + const mutationTypeIndex = findIndex(x.data.__schema.types, ['name', 'Mutation']) + + // remove the fields: + + remove(x.data.__schema.types[queryTypeIndex].fields, field => field.name === '__debug') + remove(x.data.__schema.types[mutationTypeIndex].fields, field => field.name === '__debug') + + const schema = buildClientSchema(x.data) + + return schema +} diff --git a/{{cookiecutter.project_slug}}/frontend/src/apollo/test-helpers.js b/{{cookiecutter.project_slug}}/frontend/src/apollo/test-helpers.js new file mode 100644 index 000000000..83a454566 --- /dev/null +++ b/{{cookiecutter.project_slug}}/frontend/src/apollo/test-helpers.js @@ -0,0 +1,28 @@ +import { assign } from 'lodash' +import { baseMocks } from './mocks' +import { graphql } from 'graphql' +import { addMockFunctionsToSchema } from 'graphql-tools' +import { mockSchema } from './schema-parser' +import { print as gqlToString } from 'graphql/language' + + +export const mockQuery = ({query, mocks, variables = { id: 'id' }, log = false}) => { + // Arguments: + // (required) QUERY + // (optional) Mock object : to override base mocks + // (optional) Variables : If not passed, it will use a fake id only (most frequently used) + // (optional) Log : In case you want the query result to be shown with test results. + + /// MOCKING SCHEMA: + const schema = mockSchema() + const combinedMocks = mocks ? assign({}, baseMocks, mocks) : baseMocks + addMockFunctionsToSchema({ schema, mocks: combinedMocks }) + + // need the first 'return' so that the final output is the promise result + return graphql(schema, gqlToString(query), null, null, variables).then(result => { + if (log) console.log('mockQuery result', result) + const { data, errors } = result + expect(errors).toBe() + return data + }) +} diff --git a/{{cookiecutter.project_slug}}/frontend/src/root.js b/{{cookiecutter.project_slug}}/frontend/src/root.js index b19501a51..5f4d7f65e 100644 --- a/{{cookiecutter.project_slug}}/frontend/src/root.js +++ b/{{cookiecutter.project_slug}}/frontend/src/root.js @@ -1,8 +1,12 @@ import React from 'react' +import { Provider as AlertProvider } from 'react-alert' +import AlertTemplate from 'react-alert-template-basic' import { Router } from 'react-router-dom' +import { ApolloProvider } from '@apollo/react-hooks' import { createBrowserHistory as createHistory } from 'history' import Raven from 'raven-js' +import client from 'apollo/client' import Routes from './routes' if (process.env.NODE_ENV === 'production') { @@ -12,9 +16,13 @@ if (process.env.NODE_ENV === 'production') { const history = createHistory() const Root = () => ( - - - + + + + + + + ) export default Root