├── .gitignore ├── README.md ├── assets ├── no-server.png └── server.png └── examples ├── graphql-wrapper-rest ├── .eslintrc.json ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ └── manifest.json ├── src │ ├── App.css │ ├── App.js │ ├── App.test.js │ ├── AppRouter.js │ ├── components │ │ └── QueryExample.js │ ├── graphql │ │ ├── resolvers.js │ │ └── typeDefs.js │ ├── index.css │ ├── index.js │ ├── logo.svg │ └── registerServiceWorker.js └── yarn.lock └── schema-stitching ├── .eslintrc.json ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json ├── src ├── App.css ├── App.js ├── App.test.js ├── AppRouter.js ├── components │ └── QueryExample.js ├── graphql │ ├── linkTypeDefs.js │ ├── resolvers.js │ ├── schemaStitchingResolvers.js │ └── typeDefs.js ├── index.css ├── index.js ├── logo.svg └── registerServiceWorker.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | *node_modules 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GraphQL without a server 2 | 3 | ## TL;DR 4 | 5 | - You do not need a GraphQL server to use the GraphQL APIs on the client. ([Example](https://github.com/hasura/client-side-graphql/tree/master/examples/graphql-wrapper-rest)) 6 | - You do not need a separate GraphQL server to stitch the schemas of two existing GraphQL APIs. ([Example](https://github.com/hasura/client-side-graphql/tree/master/examples/schema-stitching)) 7 | 8 | ## Introduction 9 | 10 | GraphQL is essentially a syntax that describes how to ask for data. A GraphQL API is something that accepts queries in GraphQL syntax and resolves them to whatever is asked by a query. Traditionally, this are written on a server and exposed on a single endpoint. However, since the growth of [Apollo Client](https://www.apollographql.com/client/), and the tools around it, this GraphQL wrapper can be written on the client as well. 11 | 12 | ## Analogy 13 | 14 | ![traditional](https://raw.githubusercontent.com/hasura/client-side-graphql/master/assets/server.png) 15 | 16 | ![no-server](https://raw.githubusercontent.com/hasura/client-side-graphql/master/assets/no-server.png) 17 | 18 | ## Examples 19 | 20 | ### Simple GraphQL schema 21 | 22 | This example makes a simple hello-world GraphQL schema. 23 | 24 | ```js 25 | import { makeExecutableSchema } from 'graphql-tools'; 26 | import { SchemaLink } from 'apollo-link-schema'; 27 | import { InMemoryCache } from 'apollo-cache-inmemory'; 28 | import ApolloClient from 'apollo-client'; 29 | 30 | const typeDefs = ` 31 | type Hello { 32 | message: String 33 | } 34 | `; 35 | 36 | const resolvers = { 37 | Query: { 38 | message: (root, args, context, info) => "hello-world" 39 | } 40 | }; 41 | 42 | const schema = makeExecutableSchema({ 43 | typeDefs, 44 | resolvers 45 | }); 46 | 47 | const client = new ApolloClient({ 48 | link: new SchemaLink({ schema }), 49 | cache: new InMemoryCache() 50 | }) 51 | 52 | // You can use this client in your app and it will work like any other GraphQL server 53 | 54 | ``` 55 | 56 | Check out [this example](https://github.com/hasura/client-side-graphql/tree/master/examples/graphql-wrapper-rest) where we have written a GraphQL wrapper over the [Meta weather REST API](https://www.metaweather.com/). 57 | 58 | ### Schema Stitching 59 | 60 | This is an example of stitching two remote GraphQL schemas. 61 | 62 | ```js 63 | import { makeRemoteExecutableSchema, introspectSchema, mergeSchemas } from 'graphql-tools'; 64 | import { SchemaLink } from 'apollo-link-schema'; 65 | import ApolloClient from 'apollo-client'; 66 | 67 | const uri1 = 'https://server1.com/graphql'; 68 | const uri2 = 'https://server2.com/graphql'; 69 | 70 | const getRemoteExecutableSchema = async (uri) => { 71 | const httpLink = new HttpLink({ uri }); 72 | const remoteSchema = await introspectSchema(httpLink); 73 | return makeRemoteExecutableSchema({ schema: remoteSchema, link: httpLink }); 74 | } 75 | 76 | const executableSchema1 = await getRemoteExecutableSchema(uri1); 77 | const executableSchema2 = await getRemoteExecutableSchema(uri2); 78 | 79 | const newSchema = mergeSchemas({ 80 | schemas: [ 81 | executableSchema1, 82 | executableSchema2 83 | ] 84 | }); 85 | 86 | const client = new ApolloClient({ 87 | link: new SchemaLink({ schema: newSchema }), 88 | cache: new InMemoryCache() 89 | }); 90 | 91 | // You can use this client in your app and it will work like any other GraphQL server 92 | ``` 93 | 94 | You can also have custom resolvers if you want to link your schemas in some way. You just have to add them to the resolvers field in the `mergeSchemas` function. 95 | 96 | ```js 97 | 98 | const resolvers = { 99 | Query: { 100 | ... 101 | }, 102 | Mutation: { 103 | ... 104 | } 105 | } 106 | 107 | const newSchema = mergeSchemas({ 108 | schemas: [ 109 | executableSchema1, 110 | executableSchema2 111 | ], 112 | resolvers: resolvers 113 | }); 114 | ``` 115 | 116 | Check out [this example](https://github.com/hasura/client-side-graphql/tree/master/examples/schema-stitching) where we stitch a remote GraphQL schema with a local GraphQL schema. 117 | -------------------------------------------------------------------------------- /assets/no-server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hasura/client-side-graphql/8608faf48bfe89b0bc8b6ab5dcc0e6e8142fc64c/assets/no-server.png -------------------------------------------------------------------------------- /assets/server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hasura/client-side-graphql/8608faf48bfe89b0bc8b6ab5dcc0e6e8142fc64c/assets/server.png -------------------------------------------------------------------------------- /examples/graphql-wrapper-rest/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true 5 | }, 6 | "extends": "eslint:recommended", 7 | "parserOptions": { 8 | "ecmaFeatures": { 9 | "jsx": true 10 | }, 11 | "ecmaVersion": 2018, 12 | "sourceType": "module" 13 | }, 14 | "plugins": [ 15 | "react" 16 | ], 17 | "rules": { 18 | "indent": [ 19 | "error", 20 | 4 21 | ], 22 | "linebreak-style": [ 23 | "error", 24 | "unix" 25 | ], 26 | "quotes": [ 27 | "error", 28 | "single" 29 | ], 30 | "semi": [ 31 | "error", 32 | "always" 33 | ] 34 | } 35 | } -------------------------------------------------------------------------------- /examples/graphql-wrapper-rest/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /examples/graphql-wrapper-rest/README.md: -------------------------------------------------------------------------------- 1 | To run this example project, please run: 2 | 3 | ```bash 4 | $ npm install 5 | $ npm start 6 | ``` 7 | 8 | Check [this file](https://github.com/hasura/client-side-graphql/blob/master/examples/graphql-wrapper-rest/src/App.js) to see how the GraphQL to REST mapping is done. 9 | -------------------------------------------------------------------------------- /examples/graphql-wrapper-rest/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-wrapper-rest", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "apollo-boost": "^0.1.10", 7 | "apollo-link-schema": "^1.1.0", 8 | "graphql": "^0.13.2", 9 | "graphql-tools": "^3.0.5", 10 | "react": "^16.4.1", 11 | "react-apollo": "^2.1.9", 12 | "react-dom": "^16.4.1", 13 | "react-router-dom": "^4.3.1", 14 | "react-scripts": "^1.1.1" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts start", 18 | "build": "react-scripts build", 19 | "test": "react-scripts test --env=jsdom", 20 | "eject": "react-scripts eject" 21 | }, 22 | "devDependencies": { 23 | "eslint-plugin-graphql": "^1.5.0", 24 | "eslint": "^5.1.0", 25 | "eslint-plugin-react": "^7.10.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /examples/graphql-wrapper-rest/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hasura/client-side-graphql/8608faf48bfe89b0bc8b6ab5dcc0e6e8142fc64c/examples/graphql-wrapper-rest/public/favicon.ico -------------------------------------------------------------------------------- /examples/graphql-wrapper-rest/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | React App 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /examples/graphql-wrapper-rest/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /examples/graphql-wrapper-rest/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .Row { 6 | display: flex; 7 | flex-direction: row; 8 | } 9 | -------------------------------------------------------------------------------- /examples/graphql-wrapper-rest/src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ApolloProvider } from 'react-apollo'; 3 | import ApolloClient from 'apollo-client'; 4 | import { SchemaLink } from 'apollo-link-schema'; 5 | import { InMemoryCache } from 'apollo-cache-inmemory'; 6 | import { makeExecutableSchema } from 'graphql-tools'; 7 | import AppRouter from './AppRouter'; 8 | import typeDefs from './graphql/typeDefs'; 9 | import resolvers from './graphql/resolvers'; 10 | 11 | 12 | const initApollo = () => { 13 | const schema = makeExecutableSchema({ 14 | typeDefs, 15 | resolvers 16 | }); 17 | 18 | const client = new ApolloClient({ 19 | link: new SchemaLink({ schema }), 20 | cache: new InMemoryCache({ 21 | addTypename: false 22 | }) 23 | }); 24 | 25 | return client; 26 | }; 27 | 28 | class App extends React.Component { 29 | state = { 30 | client: null, 31 | } 32 | 33 | componentWillMount() { 34 | const client = initApollo(); 35 | this.setState({ client }); 36 | } 37 | 38 | render() { 39 | if (!this.state.client) { 40 | return "Loading ..."; 41 | } 42 | return ( 43 | 44 | 45 | 46 | ); 47 | } 48 | } 49 | 50 | export default App; 51 | -------------------------------------------------------------------------------- /examples/graphql-wrapper-rest/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /examples/graphql-wrapper-rest/src/AppRouter.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './App.css'; 3 | import QueryExample from './components/QueryExample'; 4 | 5 | import { 6 | BrowserRouter as Router, 7 | Route, 8 | Link 9 | } from 'react-router-dom'; 10 | 11 | const Home = () => ( 12 |
13 |

Home

14 |
15 | ); 16 | 17 | const AppRouter = (props) => { 18 | return ( 19 | 20 |
21 |
    22 |
  • Home
  • 23 |
  • Example
  • 24 |
25 | 26 |
27 | 28 | 29 | 30 |
31 |
32 | ); 33 | } 34 | 35 | export default AppRouter; 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /examples/graphql-wrapper-rest/src/components/QueryExample.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Query } from 'react-apollo'; 3 | import gql from 'graphql-tag'; 4 | 5 | const queryText = gql` 6 | query ($cityName: String!){ 7 | cityWeather (city_name: $cityName) { 8 | temp 9 | max_temp 10 | min_temp 11 | } 12 | } 13 | `; 14 | 15 | class QueryComponent extends React.Component { 16 | 17 | render() { 18 | if (!this.props.cityName) { 19 | return "Please type a city to get its temperature"; 20 | } 21 | return ( 22 | 26 | {({ data, loading, error }) => { 27 | if (loading) { 28 | return (

Loading

); 29 | } 30 | if (error) { 31 | return (

{JSON.stringify(error)}

); 32 | } 33 | if (!data.cityWeather) { 34 | return ("No info found for this city"); 35 | } 36 | const { temp, max_temp, min_temp } = data.cityWeather; 37 | return ( 38 |
39 |

Temp: {temp}

40 |

Max temp: {max_temp}

41 |

Min temp: {min_temp}

42 |
43 | ); 44 | }} 45 |
46 | ) 47 | } 48 | } 49 | 50 | class CityTemp extends React.Component { 51 | 52 | constructor (props) { 53 | super(props); 54 | this.state = { 55 | text: '' 56 | } 57 | } 58 | 59 | handleTextChange = (text) => { 60 | this.setState({ ...this.state, text }) 61 | } 62 | 63 | setFinalValue = () => { 64 | this.setState({ finalText: this.state.text}); 65 | } 66 | 67 | render() { 68 | return ( 69 |
70 |
71 | 72 |
73 |
74 | this.handleTextChange(e.target.value)} 78 | placeholder="Enter a city name" 79 | /> 80 | 86 |
87 |
88 | ) 89 | } 90 | } 91 | 92 | export default CityTemp; 93 | -------------------------------------------------------------------------------- /examples/graphql-wrapper-rest/src/graphql/resolvers.js: -------------------------------------------------------------------------------- 1 | const METAWEATHER_API_URL = "https://www.metaweather.com/api/location/"; 2 | 3 | const getWeather = (data) => { 4 | return fetch(METAWEATHER_API_URL + data.woeid) 5 | .then(response => response.json()) 6 | }; 7 | 8 | // get woeid (where on earth id) using city name 9 | const getWoeid = (place) => { 10 | return fetch(`${METAWEATHER_API_URL}search/?query=${place}`) 11 | .then(response => response.json()) 12 | .then(jsonResponse => jsonResponse[0]) 13 | }; 14 | 15 | // resolvers -> get where on earth id -> get consolidated_weather data and return 16 | const resolvers = { 17 | Query: { 18 | cityWeather: (root, args, context, info) => { 19 | return getWoeid(args.city_name).then( (response) => { 20 | if (!response) { 21 | return null; 22 | } 23 | return getWeather(response).then( (weather) => { 24 | if (!weather) { 25 | return null; 26 | } 27 | let consolidated_weather = weather.consolidated_weather; 28 | // check for args applicable_date to apply filter 29 | consolidated_weather = args.applicable_date ? consolidated_weather.find(item => item.applicable_date === args.applicable_date) : consolidated_weather[0]; 30 | const respObj = {'temp': consolidated_weather.the_temp.toString(), 'min_temp': consolidated_weather.min_temp.toString(), 'max_temp': consolidated_weather.max_temp.toString(), 'city_name': weather.title, 'applicable_date': consolidated_weather.applicable_date}; 31 | return respObj; 32 | }); 33 | }); 34 | } 35 | }, 36 | }; 37 | 38 | export default resolvers; 39 | -------------------------------------------------------------------------------- /examples/graphql-wrapper-rest/src/graphql/typeDefs.js: -------------------------------------------------------------------------------- 1 | const typeDefs = ` 2 | type CityWeather { 3 | temp: String 4 | min_temp: String 5 | max_temp: String 6 | city_name: String! 7 | applicable_date: String! 8 | } 9 | 10 | type Query { 11 | cityWeather(city_name: String! applicable_date: String): CityWeather 12 | } 13 | `; 14 | 15 | export default typeDefs; 16 | -------------------------------------------------------------------------------- /examples/graphql-wrapper-rest/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /examples/graphql-wrapper-rest/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | ReactDOM.render( 6 | , 7 | document.getElementById('root') 8 | ); 9 | -------------------------------------------------------------------------------- /examples/graphql-wrapper-rest/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /examples/graphql-wrapper-rest/src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | const isLocalhost = Boolean( 12 | window.location.hostname === 'localhost' || 13 | // [::1] is the IPv6 localhost address. 14 | window.location.hostname === '[::1]' || 15 | // 127.0.0.1/8 is considered localhost for IPv4. 16 | window.location.hostname.match( 17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 18 | ) 19 | ); 20 | 21 | export default function register() { 22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 23 | // The URL constructor is available in all browsers that support SW. 24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location); 25 | if (publicUrl.origin !== window.location.origin) { 26 | // Our service worker won't work if PUBLIC_URL is on a different origin 27 | // from what our page is served on. This might happen if a CDN is used to 28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 29 | return; 30 | } 31 | 32 | window.addEventListener('load', () => { 33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 34 | 35 | if (isLocalhost) { 36 | // This is running on localhost. Lets check if a service worker still exists or not. 37 | checkValidServiceWorker(swUrl); 38 | 39 | // Add some additional logging to localhost, pointing developers to the 40 | // service worker/PWA documentation. 41 | navigator.serviceWorker.ready.then(() => { 42 | console.log( 43 | 'This web app is being served cache-first by a service ' + 44 | 'worker. To learn more, visit https://goo.gl/SC7cgQ' 45 | ); 46 | }); 47 | } else { 48 | // Is not local host. Just register service worker 49 | registerValidSW(swUrl); 50 | } 51 | }); 52 | } 53 | } 54 | 55 | function registerValidSW(swUrl) { 56 | navigator.serviceWorker 57 | .register(swUrl) 58 | .then(registration => { 59 | registration.onupdatefound = () => { 60 | const installingWorker = registration.installing; 61 | installingWorker.onstatechange = () => { 62 | if (installingWorker.state === 'installed') { 63 | if (navigator.serviceWorker.controller) { 64 | // At this point, the old content will have been purged and 65 | // the fresh content will have been added to the cache. 66 | // It's the perfect time to display a "New content is 67 | // available; please refresh." message in your web app. 68 | console.log('New content is available; please refresh.'); 69 | } else { 70 | // At this point, everything has been precached. 71 | // It's the perfect time to display a 72 | // "Content is cached for offline use." message. 73 | console.log('Content is cached for offline use.'); 74 | } 75 | } 76 | }; 77 | }; 78 | }) 79 | .catch(error => { 80 | console.error('Error during service worker registration:', error); 81 | }); 82 | } 83 | 84 | function checkValidServiceWorker(swUrl) { 85 | // Check if the service worker can be found. If it can't reload the page. 86 | fetch(swUrl) 87 | .then(response => { 88 | // Ensure service worker exists, and that we really are getting a JS file. 89 | if ( 90 | response.status === 404 || 91 | response.headers.get('content-type').indexOf('javascript') === -1 92 | ) { 93 | // No service worker found. Probably a different app. Reload the page. 94 | navigator.serviceWorker.ready.then(registration => { 95 | registration.unregister().then(() => { 96 | window.location.reload(); 97 | }); 98 | }); 99 | } else { 100 | // Service worker found. Proceed as normal. 101 | registerValidSW(swUrl); 102 | } 103 | }) 104 | .catch(() => { 105 | console.log( 106 | 'No internet connection found. App is running in offline mode.' 107 | ); 108 | }); 109 | } 110 | 111 | export function unregister() { 112 | if ('serviceWorker' in navigator) { 113 | navigator.serviceWorker.ready.then(registration => { 114 | registration.unregister(); 115 | }); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /examples/schema-stitching/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true 5 | }, 6 | "extends": "eslint:recommended", 7 | "parserOptions": { 8 | "ecmaFeatures": { 9 | "jsx": true 10 | }, 11 | "ecmaVersion": 2018, 12 | "sourceType": "module" 13 | }, 14 | "plugins": [ 15 | "react" 16 | ], 17 | "rules": { 18 | "accessor-pairs": "error", 19 | "array-bracket-newline": "error", 20 | "array-bracket-spacing": "error", 21 | "array-callback-return": "error", 22 | "array-element-newline": "error", 23 | "arrow-body-style": "error", 24 | "arrow-parens": [ 25 | "error", 26 | "as-needed" 27 | ], 28 | "arrow-spacing": [ 29 | "error", 30 | { 31 | "after": true, 32 | "before": true 33 | } 34 | ], 35 | "block-scoped-var": "error", 36 | "block-spacing": "error", 37 | "brace-style": [ 38 | "error", 39 | "1tbs" 40 | ], 41 | "callback-return": "error", 42 | "camelcase": "error", 43 | "capitalized-comments": "off", 44 | "class-methods-use-this": "off", 45 | "comma-dangle": "error", 46 | "comma-spacing": [ 47 | "error", 48 | { 49 | "after": true, 50 | "before": false 51 | } 52 | ], 53 | "comma-style": [ 54 | "error", 55 | "last" 56 | ], 57 | "complexity": "error", 58 | "computed-property-spacing": "error", 59 | "consistent-return": "error", 60 | "consistent-this": "error", 61 | "curly": "error", 62 | "default-case": "error", 63 | "dot-location": [ 64 | "error", 65 | "property" 66 | ], 67 | "dot-notation": [ 68 | "error", 69 | { 70 | "allowKeywords": true 71 | } 72 | ], 73 | "eol-last": "error", 74 | "eqeqeq": "error", 75 | "func-call-spacing": "error", 76 | "func-name-matching": "error", 77 | "func-names": "error", 78 | "func-style": [ 79 | "error", 80 | "declaration" 81 | ], 82 | "function-paren-newline": "off", 83 | "generator-star-spacing": "error", 84 | "global-require": "error", 85 | "guard-for-in": "error", 86 | "handle-callback-err": "error", 87 | "id-blacklist": "error", 88 | "id-length": "error", 89 | "id-match": "error", 90 | "implicit-arrow-linebreak": [ 91 | "error", 92 | "beside" 93 | ], 94 | "indent": "off", 95 | "indent-legacy": "off", 96 | "init-declarations": "error", 97 | "jsx-quotes": [ 98 | "error", 99 | "prefer-double" 100 | ], 101 | "key-spacing": "error", 102 | "keyword-spacing": [ 103 | "error", 104 | { 105 | "after": true, 106 | "before": true 107 | } 108 | ], 109 | "line-comment-position": "error", 110 | "linebreak-style": [ 111 | "error", 112 | "unix" 113 | ], 114 | "lines-around-comment": "error", 115 | "lines-around-directive": "error", 116 | "lines-between-class-members": "error", 117 | "max-classes-per-file": "error", 118 | "max-depth": "error", 119 | "max-len": "off", 120 | "max-lines": "error", 121 | "max-lines-per-function": "error", 122 | "max-nested-callbacks": "error", 123 | "max-params": "error", 124 | "max-statements": "error", 125 | "max-statements-per-line": "error", 126 | "multiline-comment-style": [ 127 | "error", 128 | "separate-lines" 129 | ], 130 | "multiline-ternary": "error", 131 | "new-cap": "error", 132 | "new-parens": "error", 133 | "newline-after-var": "off", 134 | "newline-before-return": "error", 135 | "newline-per-chained-call": "error", 136 | "no-alert": "error", 137 | "no-array-constructor": "error", 138 | "no-await-in-loop": "error", 139 | "no-bitwise": "error", 140 | "no-buffer-constructor": "error", 141 | "no-caller": "error", 142 | "no-catch-shadow": "error", 143 | "no-confusing-arrow": "error", 144 | "no-continue": "error", 145 | "no-div-regex": "error", 146 | "no-duplicate-imports": "error", 147 | "no-else-return": "error", 148 | "no-empty-function": "error", 149 | "no-eq-null": "error", 150 | "no-eval": "error", 151 | "no-extend-native": "error", 152 | "no-extra-bind": "error", 153 | "no-extra-label": "error", 154 | "no-extra-parens": "error", 155 | "no-floating-decimal": "error", 156 | "no-implicit-coercion": "error", 157 | "no-implicit-globals": "error", 158 | "no-implied-eval": "error", 159 | "no-inline-comments": "error", 160 | "no-invalid-this": "error", 161 | "no-iterator": "error", 162 | "no-label-var": "error", 163 | "no-labels": "error", 164 | "no-lone-blocks": "error", 165 | "no-lonely-if": "error", 166 | "no-loop-func": "error", 167 | "no-magic-numbers": "off", 168 | "no-mixed-operators": "error", 169 | "no-mixed-requires": "error", 170 | "no-multi-assign": "error", 171 | "no-multi-spaces": "error", 172 | "no-multi-str": "error", 173 | "no-multiple-empty-lines": "error", 174 | "no-native-reassign": "error", 175 | "no-negated-condition": "error", 176 | "no-negated-in-lhs": "error", 177 | "no-nested-ternary": "error", 178 | "no-new": "error", 179 | "no-new-func": "error", 180 | "no-new-object": "error", 181 | "no-new-require": "error", 182 | "no-new-wrappers": "error", 183 | "no-octal-escape": "error", 184 | "no-param-reassign": "error", 185 | "no-path-concat": "error", 186 | "no-plusplus": "error", 187 | "no-process-env": "off", 188 | "no-process-exit": "error", 189 | "no-proto": "error", 190 | "no-prototype-builtins": "error", 191 | "no-restricted-globals": "error", 192 | "no-restricted-imports": "error", 193 | "no-restricted-modules": "error", 194 | "no-restricted-properties": "error", 195 | "no-restricted-syntax": "error", 196 | "no-return-assign": "error", 197 | "no-return-await": "error", 198 | "no-script-url": "error", 199 | "no-self-compare": "error", 200 | "no-sequences": "error", 201 | "no-shadow": "error", 202 | "no-shadow-restricted-names": "error", 203 | "no-spaced-func": "error", 204 | "no-sync": "error", 205 | "no-tabs": "error", 206 | "no-template-curly-in-string": "error", 207 | "no-ternary": "error", 208 | "no-throw-literal": "error", 209 | "no-trailing-spaces": "error", 210 | "no-undef-init": "error", 211 | "no-undefined": "error", 212 | "no-underscore-dangle": "error", 213 | "no-unmodified-loop-condition": "error", 214 | "no-unneeded-ternary": "error", 215 | "no-unused-expressions": "error", 216 | "no-use-before-define": "off", 217 | "no-useless-call": "error", 218 | "no-useless-computed-key": "error", 219 | "no-useless-concat": "error", 220 | "no-useless-constructor": "error", 221 | "no-useless-rename": "error", 222 | "no-useless-return": "error", 223 | "no-var": "error", 224 | "no-void": "error", 225 | "no-warning-comments": "error", 226 | "no-whitespace-before-property": "error", 227 | "no-with": "error", 228 | "nonblock-statement-body-position": "error", 229 | "object-curly-newline": "error", 230 | "object-curly-spacing": [ 231 | "error", 232 | "always" 233 | ], 234 | "object-property-newline": "error", 235 | "object-shorthand": "error", 236 | "one-var": "error", 237 | "one-var-declaration-per-line": "error", 238 | "operator-assignment": "error", 239 | "operator-linebreak": "error", 240 | "padded-blocks": "off", 241 | "padding-line-between-statements": "error", 242 | "prefer-arrow-callback": "error", 243 | "prefer-const": "error", 244 | "prefer-destructuring": "error", 245 | "prefer-numeric-literals": "error", 246 | "prefer-object-spread": "error", 247 | "prefer-promise-reject-errors": "error", 248 | "prefer-reflect": "error", 249 | "prefer-rest-params": "error", 250 | "prefer-spread": "error", 251 | "prefer-template": "error", 252 | "quote-props": "off", 253 | "quotes": [ 254 | "error", 255 | "single" 256 | ], 257 | "radix": "error", 258 | "require-await": "error", 259 | "require-jsdoc": "off", 260 | "rest-spread-spacing": "error", 261 | "semi": "error", 262 | "semi-spacing": "error", 263 | "semi-style": [ 264 | "error", 265 | "last" 266 | ], 267 | "sort-imports": "off", 268 | "sort-keys": [ 269 | "error", 270 | "desc" 271 | ], 272 | "sort-vars": "error", 273 | "space-before-blocks": "error", 274 | "space-before-function-paren": "off", 275 | "space-in-parens": [ 276 | "error", 277 | "never" 278 | ], 279 | "space-infix-ops": "error", 280 | "space-unary-ops": "error", 281 | "spaced-comment": [ 282 | "error", 283 | "always" 284 | ], 285 | "strict": "error", 286 | "switch-colon-spacing": "error", 287 | "symbol-description": "error", 288 | "template-curly-spacing": [ 289 | "error", 290 | "never" 291 | ], 292 | "template-tag-spacing": "error", 293 | "unicode-bom": [ 294 | "error", 295 | "never" 296 | ], 297 | "valid-jsdoc": "error", 298 | "vars-on-top": "error", 299 | "wrap-iife": "error", 300 | "wrap-regex": "error", 301 | "yield-star-spacing": "error", 302 | "yoda": [ 303 | "error", 304 | "never" 305 | ] 306 | } 307 | } -------------------------------------------------------------------------------- /examples/schema-stitching/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /examples/schema-stitching/README.md: -------------------------------------------------------------------------------- 1 | To run this example project, please run: 2 | 3 | ```bash 4 | $ npm install 5 | $ npm start 6 | ``` 7 | 8 | Check [this file](https://github.com/hasura/client-side-graphql/blob/master/examples/schema-stitching/src/App.js) to see how schema stitching is being done. 9 | -------------------------------------------------------------------------------- /examples/schema-stitching/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-graphql-boilerplate", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "apollo-boost": "^0.1.10", 7 | "apollo-link-schema": "^1.1.0", 8 | "graphql": "^0.13.2", 9 | "graphql-tools": "^3.0.5", 10 | "react": "^16.4.1", 11 | "react-apollo": "^2.1.9", 12 | "react-dom": "^16.4.1", 13 | "react-router-dom": "^4.3.1", 14 | "react-scripts": "^1.1.1" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts start", 18 | "build": "react-scripts build", 19 | "test": "react-scripts test --env=jsdom", 20 | "eject": "react-scripts eject" 21 | }, 22 | "devDependencies": { 23 | "eslint-plugin-graphql": "^1.5.0", 24 | "eslint": "^5.1.0", 25 | "eslint-plugin-react": "^7.10.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /examples/schema-stitching/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hasura/client-side-graphql/8608faf48bfe89b0bc8b6ab5dcc0e6e8142fc64c/examples/schema-stitching/public/favicon.ico -------------------------------------------------------------------------------- /examples/schema-stitching/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | React App 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /examples/schema-stitching/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /examples/schema-stitching/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | animation: App-logo-spin infinite 20s linear; 7 | height: 80px; 8 | } 9 | 10 | .App-header { 11 | background-color: #222; 12 | height: 150px; 13 | padding: 20px; 14 | color: white; 15 | } 16 | 17 | .App-title { 18 | font-size: 1.5em; 19 | } 20 | 21 | .App-intro { 22 | font-size: large; 23 | } 24 | 25 | @keyframes App-logo-spin { 26 | from { transform: rotate(0deg); } 27 | to { transform: rotate(360deg); } 28 | } 29 | -------------------------------------------------------------------------------- /examples/schema-stitching/src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ApolloProvider } from 'react-apollo'; 3 | import AppRouter from './AppRouter'; 4 | import ApolloClient from 'apollo-client'; 5 | import { SchemaLink } from 'apollo-link-schema'; 6 | import { HttpLink } from 'apollo-link-http'; 7 | import { InMemoryCache } from 'apollo-cache-inmemory'; 8 | import { 9 | makeExecutableSchema, 10 | makeRemoteExecutableSchema, 11 | introspectSchema, 12 | mergeSchemas 13 | } from 'graphql-tools'; 14 | import weatherTypeDefs from './graphql/typeDefs'; 15 | import weatherResolvers from './graphql/resolvers'; 16 | import personWeatherTypeExtensions from './graphql/linkTypeDefs'; 17 | import schemaStitchingResolvers from './graphql/schemaStitchingResolvers'; 18 | 19 | const personGraphQLUri = 'https://bazookaand.herokuapp.com/v1alpha1/graphql'; 20 | 21 | const initApollo = async () => { 22 | const link = new HttpLink({uri: personGraphQLUri}); 23 | const personSchema = await introspectSchema(link); 24 | const executablePersonSchema = makeRemoteExecutableSchema({ 25 | schema: personSchema, 26 | link 27 | }); 28 | const executableWeatherSchema = makeExecutableSchema({ 29 | typeDefs: weatherTypeDefs, 30 | resolvers: weatherResolvers 31 | }); 32 | 33 | const newSchema = mergeSchemas({ 34 | schemas: [ 35 | executableWeatherSchema, 36 | executablePersonSchema, 37 | personWeatherTypeExtensions 38 | ], 39 | resolvers: schemaStitchingResolvers({ 40 | weather: executableWeatherSchema, 41 | person: executablePersonSchema 42 | }) 43 | }); 44 | 45 | const client = new ApolloClient({ 46 | link: new SchemaLink({ schema: newSchema }), 47 | cache: new InMemoryCache({ 48 | addTypename: false 49 | }), 50 | connectToDevTools: true 51 | }); 52 | 53 | return client; 54 | }; 55 | 56 | class App extends React.Component { 57 | state = { 58 | client: null, 59 | } 60 | 61 | async componentWillMount() { 62 | const client = await initApollo(); 63 | this.setState({ client }); 64 | } 65 | 66 | render() { 67 | if (!this.state.client) { 68 | return "Loading ..."; 69 | } 70 | return ( 71 | 72 | 73 | 74 | ); 75 | } 76 | } 77 | 78 | export default App; 79 | -------------------------------------------------------------------------------- /examples/schema-stitching/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /examples/schema-stitching/src/AppRouter.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './App.css'; 3 | import QueryExample from './components/QueryExample.js'; 4 | 5 | import { 6 | BrowserRouter as Router, 7 | Route, 8 | Link 9 | } from 'react-router-dom'; 10 | 11 | const Home = () => ( 12 |
13 |

Hello world

14 |
15 | ); 16 | 17 | const AppRouter = (props) => { 18 | console.log(props); 19 | return ( 20 | 21 |
22 |
    23 |
  • Home
  • 24 |
  • Example
  • 25 |
26 | 27 |
28 | }/> 29 | 30 |
31 |
32 | ); 33 | } 34 | 35 | export default AppRouter; 36 | -------------------------------------------------------------------------------- /examples/schema-stitching/src/components/QueryExample.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Query } from 'react-apollo'; 3 | import gql from 'graphql-tag'; 4 | 5 | const queryText = gql` 6 | query ($personName: String!){ 7 | person (where: { name: { _eq: $personName }}) { 8 | name 9 | age 10 | city 11 | city_weather { 12 | temp 13 | max_temp 14 | min_temp 15 | } 16 | } 17 | } 18 | `; 19 | 20 | class QueryComponent extends React.Component { 21 | 22 | render() { 23 | if (!this.props.personName) { 24 | return "Type an person's name"; 25 | } 26 | return ( 27 | 31 | {({ data, loading, error }) => { 32 | if (loading) { 33 | return (

Loading

); 34 | } 35 | if (error) { 36 | return (

{JSON.stringify(error)}

); 37 | } 38 | if (data.person.length === 0) { 39 | return "Name not found in the database. Try something else"; 40 | } 41 | const { name, city, age, city_weather } = data.person[0] ; 42 | const renderWeather = () => { 43 | if (city_weather) { 44 | return ( 45 |
46 |

City Temp: {city_weather.temp}

47 |

City Max temp: {city_weather.max_temp}

48 |

City Min temp: {city_weather.min_temp}

49 |
50 | ); 51 | } 52 | return

City Weather: No information available

53 | } 54 | return ( 55 |
56 |

Name: {name}

57 |

Age: {age}

58 |

City: {city}

59 | {renderWeather()} 60 |
61 | ); 62 | }} 63 |
64 | ) 65 | } 66 | } 67 | 68 | class CityTemp extends React.Component { 69 | 70 | constructor (props) { 71 | super(props); 72 | this.state = { 73 | text: '' 74 | } 75 | } 76 | 77 | handleTextChange = (text) => { 78 | this.setState({ ...this.state, text }) 79 | } 80 | 81 | setFinalValue = () => { 82 | this.setState({ finalText: this.state.text}); 83 | } 84 | 85 | render() { 86 | return ( 87 |
88 |
89 | 90 |
91 |
92 | this.handleTextChange(e.target.value)} 96 | placeholder="Name" 97 | /> 98 | 104 |
105 |
106 | ) 107 | } 108 | } 109 | 110 | export default CityTemp; 111 | -------------------------------------------------------------------------------- /examples/schema-stitching/src/graphql/linkTypeDefs.js: -------------------------------------------------------------------------------- 1 | const linkTypeDefs = ` 2 | extend type person { 3 | city_weather: CityWeather, 4 | } 5 | `; 6 | 7 | export default linkTypeDefs; -------------------------------------------------------------------------------- /examples/schema-stitching/src/graphql/resolvers.js: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch'; 2 | 3 | const METAWEATHER_API_URL = "https://www.metaweather.com/api/location/"; 4 | 5 | const getWeather = (data) => { 6 | return fetch(METAWEATHER_API_URL + data.woeid) 7 | .then(response => response.json()) 8 | }; 9 | 10 | // get woeid (where on earth id) using city name 11 | const getWoeid = (place) => { 12 | return fetch(`${METAWEATHER_API_URL}search/?query=${place}`) 13 | .then(response => response.json()) 14 | .then(jsonResponse => jsonResponse[0]) 15 | }; 16 | 17 | // resolvers -> get where on earth id -> get consolidated_weather data and return 18 | const resolvers = { 19 | Query: { 20 | cityWeather: (root, args, context, info) => { 21 | return getWoeid(args.city_name).then( (response) => { 22 | if (!response) { 23 | return null; 24 | } 25 | return getWeather(response).then( (weather) => { 26 | if (!weather) { 27 | return null; 28 | } 29 | let consolidated_weather = weather.consolidated_weather; 30 | // check for args applicable_date to apply filter 31 | consolidated_weather = args.applicable_date ? consolidated_weather.find(item => item.applicable_date === args.applicable_date) : consolidated_weather[0]; 32 | const respObj = {'temp': consolidated_weather.the_temp.toString(), 'min_temp': consolidated_weather.min_temp.toString(), 'max_temp': consolidated_weather.max_temp.toString(), 'city_name': weather.title, 'applicable_date': consolidated_weather.applicable_date}; 33 | return respObj; 34 | }); 35 | }); 36 | } 37 | }, 38 | }; 39 | 40 | export default resolvers; 41 | -------------------------------------------------------------------------------- /examples/schema-stitching/src/graphql/schemaStitchingResolvers.js: -------------------------------------------------------------------------------- 1 | const schemaStitchingResolvers = (executableSchemas) => ({ 2 | person: { 3 | city_weather : { 4 | resolve(parent, args, context, info) { 5 | return info.mergeInfo.delegateToSchema({ 6 | schema: executableSchemas.weather, 7 | operation: 'query', 8 | fieldName: 'cityWeather', 9 | args: { 10 | city_name: parent.city, 11 | }, 12 | context, 13 | info, 14 | }); 15 | }, 16 | }, 17 | }, 18 | }); 19 | 20 | export default schemaStitchingResolvers; -------------------------------------------------------------------------------- /examples/schema-stitching/src/graphql/typeDefs.js: -------------------------------------------------------------------------------- 1 | const typeDefs = ` 2 | type CityWeather { 3 | temp: String 4 | min_temp: String 5 | max_temp: String 6 | city_name: String! 7 | applicable_date: String! 8 | } 9 | 10 | type Query { 11 | cityWeather(city_name: String! applicable_date: String): CityWeather 12 | } 13 | `; 14 | 15 | export default typeDefs; 16 | -------------------------------------------------------------------------------- /examples/schema-stitching/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /examples/schema-stitching/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | ReactDOM.render( 6 | , 7 | document.getElementById('root') 8 | ); 9 | -------------------------------------------------------------------------------- /examples/schema-stitching/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /examples/schema-stitching/src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | const isLocalhost = Boolean( 12 | window.location.hostname === 'localhost' || 13 | // [::1] is the IPv6 localhost address. 14 | window.location.hostname === '[::1]' || 15 | // 127.0.0.1/8 is considered localhost for IPv4. 16 | window.location.hostname.match( 17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 18 | ) 19 | ); 20 | 21 | export default function register() { 22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 23 | // The URL constructor is available in all browsers that support SW. 24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location); 25 | if (publicUrl.origin !== window.location.origin) { 26 | // Our service worker won't work if PUBLIC_URL is on a different origin 27 | // from what our page is served on. This might happen if a CDN is used to 28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 29 | return; 30 | } 31 | 32 | window.addEventListener('load', () => { 33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 34 | 35 | if (isLocalhost) { 36 | // This is running on localhost. Lets check if a service worker still exists or not. 37 | checkValidServiceWorker(swUrl); 38 | 39 | // Add some additional logging to localhost, pointing developers to the 40 | // service worker/PWA documentation. 41 | navigator.serviceWorker.ready.then(() => { 42 | console.log( 43 | 'This web app is being served cache-first by a service ' + 44 | 'worker. To learn more, visit https://goo.gl/SC7cgQ' 45 | ); 46 | }); 47 | } else { 48 | // Is not local host. Just register service worker 49 | registerValidSW(swUrl); 50 | } 51 | }); 52 | } 53 | } 54 | 55 | function registerValidSW(swUrl) { 56 | navigator.serviceWorker 57 | .register(swUrl) 58 | .then(registration => { 59 | registration.onupdatefound = () => { 60 | const installingWorker = registration.installing; 61 | installingWorker.onstatechange = () => { 62 | if (installingWorker.state === 'installed') { 63 | if (navigator.serviceWorker.controller) { 64 | // At this point, the old content will have been purged and 65 | // the fresh content will have been added to the cache. 66 | // It's the perfect time to display a "New content is 67 | // available; please refresh." message in your web app. 68 | console.log('New content is available; please refresh.'); 69 | } else { 70 | // At this point, everything has been precached. 71 | // It's the perfect time to display a 72 | // "Content is cached for offline use." message. 73 | console.log('Content is cached for offline use.'); 74 | } 75 | } 76 | }; 77 | }; 78 | }) 79 | .catch(error => { 80 | console.error('Error during service worker registration:', error); 81 | }); 82 | } 83 | 84 | function checkValidServiceWorker(swUrl) { 85 | // Check if the service worker can be found. If it can't reload the page. 86 | fetch(swUrl) 87 | .then(response => { 88 | // Ensure service worker exists, and that we really are getting a JS file. 89 | if ( 90 | response.status === 404 || 91 | response.headers.get('content-type').indexOf('javascript') === -1 92 | ) { 93 | // No service worker found. Probably a different app. Reload the page. 94 | navigator.serviceWorker.ready.then(registration => { 95 | registration.unregister().then(() => { 96 | window.location.reload(); 97 | }); 98 | }); 99 | } else { 100 | // Service worker found. Proceed as normal. 101 | registerValidSW(swUrl); 102 | } 103 | }) 104 | .catch(() => { 105 | console.log( 106 | 'No internet connection found. App is running in offline mode.' 107 | ); 108 | }); 109 | } 110 | 111 | export function unregister() { 112 | if ('serviceWorker' in navigator) { 113 | navigator.serviceWorker.ready.then(registration => { 114 | registration.unregister(); 115 | }); 116 | } 117 | } 118 | --------------------------------------------------------------------------------