mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-15 01:12:56 +03:00
add nextjs-8-serverless sample app (#1640)
This commit is contained in:
parent
d3b994885e
commit
5fc2df2766
83
community/sample-apps/nextjs-8-serverless/.gitignore
vendored
Normal file
83
community/sample-apps/nextjs-8-serverless/.gitignore
vendored
Normal file
@ -0,0 +1,83 @@
|
||||
|
||||
### Node ###
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
# next.js build output
|
||||
.next
|
||||
|
||||
# nuxt.js build output
|
||||
.nuxt
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
125
community/sample-apps/nextjs-8-serverless/README.md
Normal file
125
community/sample-apps/nextjs-8-serverless/README.md
Normal file
@ -0,0 +1,125 @@
|
||||
# nextjs-8-serverless
|
||||
|
||||
> Boilerplate to get started with Next.js 8 Serverless Mode, Hasura GraphQL engine as CMS and postgres as database.
|
||||
|
||||
# Tutorial
|
||||
|
||||
- Deploy Postgres and GraphQL Engine on Heroku:
|
||||
|
||||
[![Deploy to heroku](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/hasura/graphql-engine-heroku)
|
||||
|
||||
Please checkout our [docs](https://docs.hasura.io/1.0/graphql/manual/deployment/index.html) for other deployment methods
|
||||
|
||||
- Get the Heroku app URL (say `my-app.herokuapp.com`)
|
||||
|
||||
- Create `author` table:
|
||||
|
||||
Open Hasura console: visit https://my-app.herokuapp.com on a browser
|
||||
Navigate to `Data` section in the top nav bar and create a table as follows:
|
||||
|
||||
![Create author table](../gatsby-postgres-graphql/assets/add_table.jpg)
|
||||
|
||||
- Insert sample data into `author` table:
|
||||
|
||||
![Insert data into author table](../gatsby-postgres-graphql/assets/insert_data.jpg)
|
||||
|
||||
Verify if the row is inserted successfully
|
||||
|
||||
![Insert data into author table](../gatsby-postgres-graphql/assets/browse_rows.jpg)
|
||||
|
||||
- Clone this repo:
|
||||
```bash
|
||||
git clone https://github.com/hasura/graphql-engine
|
||||
cd graphql-engine/community/sample-apps/nextjs-8-serverless
|
||||
```
|
||||
|
||||
- Install npm modules:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
- Open `lib/init-apollo.js` and configure Hasura's GraphQL Endpoint as follows:
|
||||
|
||||
```js
|
||||
|
||||
function create (initialState) {
|
||||
return new ApolloClient({
|
||||
connectToDevTools: process.browser,
|
||||
ssrMode: !process.browser, // Disables forceFetch on the server (so queries are only run once)
|
||||
link: new HttpLink({
|
||||
uri: 'https://myapp.herokuapp.com/v1alpha1/graphql', // Server URL (must be absolute)
|
||||
credentials: 'same-origin' // Additional fetch() options like `credentials` or `headers`
|
||||
}),
|
||||
cache: new InMemoryCache().restore(initialState || {})
|
||||
})
|
||||
}
|
||||
|
||||
```
|
||||
Replace the `uri` with your own Hasura GraphQL endpoint.
|
||||
|
||||
In this example, we integrate Apollo with Next by wrapping our *pages/_app.js* inside a higher-order component HOC. Using the HOC pattern we're able to pass down a central store of query result data created by Apollo into our React component hierarchy defined inside each page of our Next application.
|
||||
|
||||
On initial page load, while on the server and inside `getInitialProps`, we invoke the Apollo method, [`getDataFromTree`](https://www.apollographql.com/docs/react/features/server-side-rendering.html#getDataFromTree). This method returns a promise; at the point in which the promise resolves, our Apollo Client store is completely initialized.
|
||||
|
||||
- We have defined the graphql query in `components/AuthorList.js`.
|
||||
|
||||
```graphql
|
||||
|
||||
query author($skip: Int!) {
|
||||
author(offset: $skip, limit: 5) {
|
||||
id
|
||||
name
|
||||
}
|
||||
author_aggregate {
|
||||
aggregate {
|
||||
count
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
- Run the app:
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
- Test the app
|
||||
Visit [http://localhost:3000](http://localhost:3000) to view the app
|
||||
|
||||
# Serverless Mode
|
||||
|
||||
With Next.js 8, each page in the `pages` directory becomes a serverless lambda. To enable `serverless` mode, we add the `serverless` build `target` in `next.config.js`.
|
||||
|
||||
```
|
||||
module.exports = {
|
||||
target: "serverless",
|
||||
};
|
||||
```
|
||||
|
||||
That's it! Now build the serverless app by running the following command:
|
||||
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
|
||||
In the `.next` folder, you will see a `serverless` folder generated after the build. Inside that there is a `pages` folder, which will have outputs of lambda per page.
|
||||
|
||||
```
|
||||
pages/index.js => .next/serverless/pages/index.js
|
||||
pages/about.js => .next/serverless/pages/about.js
|
||||
```
|
||||
|
||||
# Deploy to now.sh
|
||||
|
||||
Deploy it to the cloud with [now](https://zeit.co/now) ([download](https://zeit.co/download)):
|
||||
|
||||
```bash
|
||||
npm install -g now
|
||||
now
|
||||
```
|
||||
Note: Older versions of now-cli doesn't support serverless mode.
|
||||
Once the deployment is successful, you will be able to navigate to pages `/` and `/about`, with each one internally being a lambda function which `now` manages.
|
||||
|
||||
|
||||
|
||||
|
42
community/sample-apps/nextjs-8-serverless/components/App.js
Normal file
42
community/sample-apps/nextjs-8-serverless/components/App.js
Normal file
@ -0,0 +1,42 @@
|
||||
export default ({ children }) => (
|
||||
<main>
|
||||
{children}
|
||||
<style jsx global>{`
|
||||
* {
|
||||
font-family: Menlo, Monaco, 'Lucida Console', 'Liberation Mono',
|
||||
'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', 'Courier New',
|
||||
monospace, serif;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 25px 50px;
|
||||
}
|
||||
a {
|
||||
color: #22bad9;
|
||||
}
|
||||
p {
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
}
|
||||
article {
|
||||
margin: 0 auto;
|
||||
max-width: 650px;
|
||||
}
|
||||
button {
|
||||
align-items: center;
|
||||
background-color: #22bad9;
|
||||
border: 0;
|
||||
color: white;
|
||||
display: flex;
|
||||
padding: 5px 7px;
|
||||
}
|
||||
button:active {
|
||||
background-color: #1b9db7;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
button:focus {
|
||||
outline: none;
|
||||
}
|
||||
`}</style>
|
||||
</main>
|
||||
)
|
@ -0,0 +1,109 @@
|
||||
import { Query } from 'react-apollo'
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const authorQuery = gql`
|
||||
query author($skip: Int!) {
|
||||
author(offset: $skip, limit: 5) {
|
||||
id
|
||||
name
|
||||
}
|
||||
author_aggregate {
|
||||
aggregate {
|
||||
count
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
export const authorQueryVars = {
|
||||
skip: 0,
|
||||
}
|
||||
|
||||
export default function AuthorList () {
|
||||
return (
|
||||
<Query query={authorQuery} variables={authorQueryVars}>
|
||||
{({ loading, error, data: { author, author_aggregate }, fetchMore }) => {
|
||||
if (error) return <ErrorMessage message='Error loading authors.' />
|
||||
if (loading) return <div>Loading</div>
|
||||
|
||||
const areMoreAuthors = author.length < author_aggregate.aggregate.count
|
||||
return (
|
||||
<section>
|
||||
<ul>
|
||||
{author.map((a, index) => (
|
||||
<li key={a.id}>
|
||||
<div>
|
||||
<span>{index + 1}. </span>
|
||||
<a>{a.name}</a>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{areMoreAuthors ? (
|
||||
<button onClick={() => loadMoreAuthors(author, fetchMore)}>
|
||||
{' '}
|
||||
{loading ? 'Loading...' : 'Show More'}{' '}
|
||||
</button>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
<style jsx>{`
|
||||
section {
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
li {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
div {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
a {
|
||||
font-size: 14px;
|
||||
margin-right: 10px;
|
||||
text-decoration: none;
|
||||
padding-bottom: 0;
|
||||
border: 0;
|
||||
}
|
||||
span {
|
||||
font-size: 14px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
button:before {
|
||||
align-self: center;
|
||||
border-style: solid;
|
||||
border-width: 6px 4px 0 4px;
|
||||
border-color: #ffffff transparent transparent transparent;
|
||||
content: '';
|
||||
height: 0;
|
||||
margin-right: 5px;
|
||||
width: 0;
|
||||
}
|
||||
`}</style>
|
||||
</section>
|
||||
)
|
||||
}}
|
||||
</Query>
|
||||
)
|
||||
}
|
||||
|
||||
function loadMoreAuthors (author, fetchMore) {
|
||||
fetchMore({
|
||||
variables: {
|
||||
skip: author.length
|
||||
},
|
||||
updateQuery: (previousResult, { fetchMoreResult }) => {
|
||||
if (!fetchMoreResult) {
|
||||
return previousResult
|
||||
}
|
||||
return Object.assign({}, previousResult, {
|
||||
// Append the new results to the old one
|
||||
author: [...previousResult.author, ...fetchMoreResult.author]
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
export default ({ message }) => (
|
||||
<aside>
|
||||
{message}
|
||||
<style jsx>{`
|
||||
aside {
|
||||
padding: 1.5em;
|
||||
font-size: 14px;
|
||||
color: white;
|
||||
background-color: red;
|
||||
}
|
||||
`}</style>
|
||||
</aside>
|
||||
)
|
@ -0,0 +1,28 @@
|
||||
import Link from 'next/link'
|
||||
import { withRouter } from 'next/router'
|
||||
|
||||
const Header = ({ router: { pathname } }) => (
|
||||
<header>
|
||||
<Link prefetch href='/'>
|
||||
<a className={pathname === '/' ? 'is-active' : ''}>Home</a>
|
||||
</Link>
|
||||
<Link prefetch href='/about'>
|
||||
<a className={pathname === '/about' ? 'is-active' : ''}>About</a>
|
||||
</Link>
|
||||
<style jsx>{`
|
||||
header {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
a {
|
||||
font-size: 14px;
|
||||
margin-right: 15px;
|
||||
text-decoration: none;
|
||||
}
|
||||
.is-active {
|
||||
text-decoration: underline;
|
||||
}
|
||||
`}</style>
|
||||
</header>
|
||||
)
|
||||
|
||||
export default withRouter(Header)
|
36
community/sample-apps/nextjs-8-serverless/lib/init-apollo.js
Normal file
36
community/sample-apps/nextjs-8-serverless/lib/init-apollo.js
Normal file
@ -0,0 +1,36 @@
|
||||
import { ApolloClient, InMemoryCache, HttpLink } from 'apollo-boost'
|
||||
import fetch from 'isomorphic-unfetch'
|
||||
|
||||
let apolloClient = null
|
||||
|
||||
// Polyfill fetch() on the server (used by apollo-client)
|
||||
if (!process.browser) {
|
||||
global.fetch = fetch
|
||||
}
|
||||
|
||||
function create (initialState) {
|
||||
return new ApolloClient({
|
||||
connectToDevTools: process.browser,
|
||||
ssrMode: !process.browser, // Disables forceFetch on the server (so queries are only run once)
|
||||
link: new HttpLink({
|
||||
uri: 'https://myapp.herokuapp.com/v1alpha1/graphql', // Server URL (must be absolute)
|
||||
credentials: 'same-origin' // Additional fetch() options like `credentials` or `headers`
|
||||
}),
|
||||
cache: new InMemoryCache().restore(initialState || {})
|
||||
})
|
||||
}
|
||||
|
||||
export default function initApollo (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 (!process.browser) {
|
||||
return create(initialState)
|
||||
}
|
||||
|
||||
// Reuse client on the client-side
|
||||
if (!apolloClient) {
|
||||
apolloClient = create(initialState)
|
||||
}
|
||||
|
||||
return apolloClient
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
import React from 'react'
|
||||
import initApollo from './init-apollo'
|
||||
import Head from 'next/head'
|
||||
import { getDataFromTree } from 'react-apollo'
|
||||
|
||||
export default App => {
|
||||
return class Apollo extends React.Component {
|
||||
static displayName = 'withApollo(App)'
|
||||
static async getInitialProps (ctx) {
|
||||
const { Component, router } = ctx
|
||||
|
||||
let appProps = {}
|
||||
if (App.getInitialProps) {
|
||||
appProps = await App.getInitialProps(ctx)
|
||||
}
|
||||
|
||||
// Run all GraphQL queries in the component tree
|
||||
// and extract the resulting data
|
||||
const apollo = initApollo()
|
||||
if (!process.browser) {
|
||||
try {
|
||||
// Run all GraphQL queries
|
||||
await getDataFromTree(
|
||||
<App
|
||||
{...appProps}
|
||||
Component={Component}
|
||||
router={router}
|
||||
apolloClient={apollo}
|
||||
/>
|
||||
)
|
||||
} 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 = apollo.cache.extract()
|
||||
|
||||
return {
|
||||
...appProps,
|
||||
apolloState
|
||||
}
|
||||
}
|
||||
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.apolloClient = initApollo(props.apolloState)
|
||||
}
|
||||
|
||||
render () {
|
||||
return <App {...this.props} apolloClient={this.apolloClient} />
|
||||
}
|
||||
}
|
||||
}
|
3
community/sample-apps/nextjs-8-serverless/next.config.js
Normal file
3
community/sample-apps/nextjs-8-serverless/next.config.js
Normal file
@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
target: "serverless"
|
||||
}
|
6
community/sample-apps/nextjs-8-serverless/now.json
Normal file
6
community/sample-apps/nextjs-8-serverless/now.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"version": 2,
|
||||
"builds": [
|
||||
{ "src": "next.config.js", "use": "@now/next" }
|
||||
]
|
||||
}
|
6322
community/sample-apps/nextjs-8-serverless/package-lock.json
generated
Normal file
6322
community/sample-apps/nextjs-8-serverless/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
24
community/sample-apps/nextjs-8-serverless/package.json
Normal file
24
community/sample-apps/nextjs-8-serverless/package.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "hasura-nextjs-serverless",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"now-build": "next build",
|
||||
"dev": "next",
|
||||
"build": "next build",
|
||||
"start": "next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"apollo-boost": "^0.1.16",
|
||||
"express": "^4.16.4",
|
||||
"graphql": "^14.0.2",
|
||||
"isomorphic-unfetch": "^3.0.0",
|
||||
"next": "latest",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^16.7.0",
|
||||
"react-apollo": "^2.1.11",
|
||||
"react-dom": "^16.7.0",
|
||||
"serverless-http": "^1.9.0"
|
||||
},
|
||||
"author": "Praveen <praveen@hasura.io>",
|
||||
"license": "MIT"
|
||||
}
|
19
community/sample-apps/nextjs-8-serverless/pages/_app.js
Normal file
19
community/sample-apps/nextjs-8-serverless/pages/_app.js
Normal file
@ -0,0 +1,19 @@
|
||||
import App, { Container } from 'next/app'
|
||||
import React from 'react'
|
||||
import withApolloClient from '../lib/with-apollo-client'
|
||||
import { ApolloProvider } from 'react-apollo'
|
||||
|
||||
class MyApp extends App {
|
||||
render () {
|
||||
const { Component, pageProps, apolloClient } = this.props
|
||||
return (
|
||||
<Container>
|
||||
<ApolloProvider client={apolloClient}>
|
||||
<Component {...pageProps} />
|
||||
</ApolloProvider>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default withApolloClient(MyApp)
|
39
community/sample-apps/nextjs-8-serverless/pages/about.js
Normal file
39
community/sample-apps/nextjs-8-serverless/pages/about.js
Normal file
@ -0,0 +1,39 @@
|
||||
import App from '../components/App'
|
||||
import Header from '../components/Header'
|
||||
|
||||
export default () => (
|
||||
<App>
|
||||
<Header />
|
||||
<article>
|
||||
<h1>The Idea Behind This Example</h1>
|
||||
<p>
|
||||
<a href='https://www.apollographql.com/client/'>Apollo</a> is a GraphQL
|
||||
client that allows you to easily query the exact data you need from a
|
||||
GraphQL server. In addition to fetching and mutating data, Apollo
|
||||
analyzes your queries and their results to construct a client-side cache
|
||||
of your data, which is kept up to date as further queries and mutations
|
||||
are run, fetching more results from the server.
|
||||
</p>
|
||||
<p>
|
||||
In this simple example, we integrate Apollo seamlessly with{' '}
|
||||
<a href='https://github.com/zeit/next.js'>Next</a> by wrapping our pages
|
||||
inside a{' '}
|
||||
<a href='https://facebook.github.io/react/docs/higher-order-components.html'>
|
||||
higher-order component (HOC)
|
||||
</a>
|
||||
. Using the HOC pattern we're able to pass down a central store of query
|
||||
result data created by Apollo into our React component hierarchy defined
|
||||
inside each page of our Next application.
|
||||
</p>
|
||||
<p>
|
||||
On initial page load, while on the server and inside getInitialProps, we
|
||||
invoke the Apollo method,{' '}
|
||||
<a href='https://www.apollographql.com/docs/react/features/server-side-rendering.html#getDataFromTree'>
|
||||
getDataFromTree
|
||||
</a>
|
||||
. This method returns a promise; at the point in which the promise
|
||||
resolves, our Apollo Client store is completely initialized.
|
||||
</p>
|
||||
</article>
|
||||
</App>
|
||||
)
|
10
community/sample-apps/nextjs-8-serverless/pages/index.js
Normal file
10
community/sample-apps/nextjs-8-serverless/pages/index.js
Normal file
@ -0,0 +1,10 @@
|
||||
import App from '../components/App'
|
||||
import Header from '../components/Header'
|
||||
import AuthorList from '../components/AuthorList'
|
||||
|
||||
export default () => (
|
||||
<App>
|
||||
<Header />
|
||||
<AuthorList />
|
||||
</App>
|
||||
)
|
Loading…
Reference in New Issue
Block a user