add nextjs 8 jwt sample app (#1943)

This commit is contained in:
Praveen Durairaj 2019-04-03 19:21:11 +05:30 committed by Shahidh K Muhammed
parent 44d6bb8b7e
commit 39b623f715
36 changed files with 6275 additions and 1 deletions

View File

@ -0,0 +1,130 @@
# nextjs-8-jwt
> Boilerplate to get started with Next.js 8 Serverless Mode and JWT Authentication, Hasura GraphQL engine as CMS and postgres as database.
## Deploy Hasura
- 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 the initial tables
1. Add your heroku URL in `hasura/config.yaml`
```yaml
endpoint: https://<hge-heroku-url>
```
2. Run `hasura migrate apply` and `hasura metadata apply` inside `hasura` directory to create the required tables and permissions for the app.
## Deploy JWT Server
- Deploy the JWT server by following the instructions [here](https://github.com/hasura/graphql-engine/tree/master/community/boilerplates/auth-servers/passportjs-jwt-roles)
*Note* Skip the `knex migrate` step as the required tables have already been created in the above step.
Ensure to configure Hasura GraphQL Engine with the environment variables `HASURA_GRAPHQL_ADMIN_SECRET` and `HASURA_GRAPHQL_JWT_SECRET` as given in the above repo.
## Run the Next.js App
- Clone this repo:
```bash
git clone https://github.com/hasura/graphql-engine
cd graphql-engine/community/sample-apps/nextjs-8-serverless/with-apollo-jwt
```
- Install npm modules:
```bash
cd app
yarn install
```
- Open `lib/init-apollo.js` and configure Hasura's GraphQL Endpoint as follows:
```js
const httpLink = new HttpLink({
uri: 'https://myapp.herokuapp.com/v1alpha1/graphql', // Server URL (must be absolute)
credentials: 'same-origin' // Additional fetch() options like `credentials` or `headers`
})
```
Replace the `uri` with your own Hasura GraphQL endpoint.
We are using an Auth Middleware to inject the Authorization headers into the GraphQL requests.
```js
const authMiddleware = new ApolloLink((operation, forward) => {
// add the authorization to the headers
operation.setContext({
headers: {
authorization: getToken(),
}
})
```
Let's also configure the JWT server URL used for authentication.
Open `server.js` and update the following appropriately.
```
const target = 'http://localhost:8080'
```
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/ArticleList.js`.
```graphql
query {
article {
id
title
}
}
```
- Run the app:
```bash
yarn run build
yarn 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:
```
yarn 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/login.js => .next/serverless/pages/login.js
pages/signup.js => .next/serverless/pages/signup.js
pages/articles.js => .next/serverless/pages/articles.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 `/login`, `/signup` with each one internally being a lambda function which `now` manages.

View File

@ -0,0 +1,75 @@
import { Query } from 'react-apollo'
import gql from 'graphql-tag'
export const articleQuery = gql`
query {
article {
id
title
}
}
`
export default function ArticleList () {
return (
<Query query={articleQuery}>
{({ loading, error, data: { article} }) => {
if (error) return <ErrorMessage message='Error loading articles.' />
if (loading) return <div>Loading</div>
return (
<section>
<ul>
{article.map((a, index) => (
<li key={a.id}>
<div>
<span>{index + 1}. </span>
<a>{a.title}</a>
</div>
</li>
))}
</ul>
<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>
)
}

View File

@ -0,0 +1,63 @@
import Link from 'next/link'
import { logout } from '../utils/auth'
const Header = props => (
<header>
<nav>
<ul>
<li>
<Link href='/'>
<a>Home</a>
</Link>
</li>
<li>
<Link href='/login'>
<a>Login</a>
</Link>
</li>
<li>
<Link href='/signup'>
<a>Signup</a>
</Link>
</li>
<li>
<Link href='/articles'>
<a>Articles</a>
</Link>
</li>
<li>
<button onClick={logout}>Logout</button>
</li>
</ul>
</nav>
<style jsx>{`
ul {
display: flex;
list-style: none;
margin-left: 0;
padding-left: 0;
}
li {
margin-right: 1rem;
}
li:first-child {
margin-left: auto;
}
a {
color: #fff;
text-decoration: none;
}
header {
padding: 0.2rem;
color: #fff;
background-color: #333;
}
`}</style>
</header>
)
export default Header

View File

@ -0,0 +1,40 @@
import React from 'react'
import Head from 'next/head'
import Header from './header'
const Layout = props => (
<React.Fragment>
<Head>
<title>Authentication with Next.js using JWT and Hasura GraphQL Engine</title>
</Head>
<style jsx global>{`
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
margin: 0;
color: #333;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
'Helvetica Neue', Arial, Noto Sans, sans-serif, 'Apple Color Emoji',
'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
}
.container {
max-width: 65rem;
margin: 1.5rem auto;
padding-left: 1rem;
padding-right: 1rem;
}
`}</style>
<Header />
<main>
<div className='container'>{props.children}</div>
</main>
</React.Fragment>
)
export default Layout

View File

@ -0,0 +1,56 @@
import { ApolloClient, InMemoryCache, HttpLink } from 'apollo-boost'
import { ApolloLink, concat } from 'apollo-link'
import fetch from 'isomorphic-unfetch'
import cookie from 'js-cookie'
let apolloClient = null
// Polyfill fetch() on the server (used by apollo-client)
if (!process.browser) {
global.fetch = fetch
}
const getToken = () => {
let token = null;
if (typeof document !== 'undefined') {
token = 'Bearer ' + cookie.get('token')
}
return token
}
function create (initialState) {
const authMiddleware = new ApolloLink((operation, forward) => {
// add the authorization to the headers
operation.setContext({
headers: {
authorization: getToken(),
}
})
return forward(operation);
})
const httpLink = new HttpLink({
uri: 'https://my-app.herokuapp.com/v1alpha1/graphql', // Server URL (must be absolute)
credentials: 'same-origin' // Additional fetch() options like `credentials` or `headers`
})
return new ApolloClient({
connectToDevTools: process.browser,
ssrMode: !process.browser, // Disables forceFetch on the server (so queries are only run once)
link: concat(authMiddleware, httpLink),
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
}

View File

@ -0,0 +1,23 @@
{
"name": "nextjs-jwt-hasura-graphql",
"scripts": {
"dev": "node server.js",
"build": "next build",
"start": "next start"
},
"dependencies": {
"apollo-boost": "^0.3.1",
"encoding": "^0.1.12",
"graphql": "^14.2.1",
"isomorphic-unfetch": "^3.0.0",
"js-cookie": "^2.2.0",
"next": "8.0.3",
"next-cookies": "^1.0.4",
"react": "^16.7.0",
"react-apollo": "^2.5.3",
"react-dom": "^16.7.0"
},
"devDependencies": {
"http-proxy": "^1.17.0"
}
}

View File

@ -0,0 +1,16 @@
import Router from 'next/router'
import fetch from 'isomorphic-unfetch'
import nextCookie from 'next-cookies'
import Layout from '../components/layout'
import ArticleList from '../components/ArticleList'
import { withAuthSync } from '../utils/auth'
const Articles = props => {
return (
<Layout>
<ArticleList />
</Layout>
)
}
export default withAuthSync(Articles)

View File

@ -0,0 +1,29 @@
import React from 'react'
import Layout from '../components/layout'
const Home = props => (
<Layout>
<h1>Authentication example with JWT and Hasura GraphQL</h1>
<p>Steps to test the functionality:</p>
<ol>
<li>Click signup and enter username and password.</li>
<li>
Click home and click articles again, notice how your session is being
used through a token.
</li>
<li>
Click logout and try to go to articles again. You'll get redirected to
the `/login` route.
</li>
</ol>
<style jsx>{`
li {
margin-bottom: 0.5rem;
}
`}</style>
</Layout>
)
export default Home

View File

@ -0,0 +1,135 @@
import { Component } from 'react'
import fetch from 'isomorphic-unfetch'
import Layout from '../components/layout'
import { login } from '../utils/auth'
class Login extends Component {
static getInitialProps ({ req }) {
const protocol = process.env.NODE_ENV === 'production' ? 'https' : 'http'
const apiUrl = process.browser
? `${protocol}://${window.location.host}/login`
: `${protocol}://${req.headers.host}/login`
return { apiUrl }
}
constructor (props) {
super(props)
this.state = { username: '', 'password': '', error: '' }
this.handleChangeUsername = this.handleChangeUsername.bind(this)
this.handleChangePassword = this.handleChangePassword.bind(this)
this.handleSubmit = this.handleSubmit.bind(this)
}
handleChangeUsername (event) {
this.setState({ username: event.target.value })
}
handleChangePassword (event) {
this.setState({ password: event.target.value })
}
async handleSubmit (event) {
event.preventDefault()
this.setState({ error: '' })
const username = this.state.username
const password = this.state.password
const url = this.props.apiUrl
try {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
})
if (response.ok) {
const { token } = await response.json()
login({ token })
} else {
console.log('Login failed.')
// https://github.com/developit/unfetch#caveats
let error = new Error(response.statusText)
error.response = response
throw error
}
} catch (error) {
console.error(
'You have an error in your code or there are Network issues.',
error
)
this.setState({ error: error.message })
}
}
render () {
return (
<Layout>
<div className='login'>
<form onSubmit={this.handleSubmit}>
<label htmlFor='username'>Enter username</label>
<input
type='text'
id='username'
name='username'
value={this.state.username}
onChange={this.handleChangeUsername}
/>
<input
type='text'
id='password'
name='password'
value={this.state.password}
onChange={this.handleChangePassword}
/>
<button type='submit'>Login</button>
<p className={`error ${this.state.error && 'show'}`}>
{this.state.error && `Error: ${this.state.error}`}
</p>
</form>
</div>
<style jsx>{`
.login {
max-width: 340px;
margin: 0 auto;
padding: 1rem;
border: 1px solid #ccc;
border-radius: 4px;
}
form {
display: flex;
flex-flow: column;
}
label {
font-weight: 600;
}
input {
padding: 8px;
margin: 0.3rem 0 1rem;
border: 1px solid #ccc;
border-radius: 4px;
}
.error {
margin: 0.5rem 0 0;
display: none;
color: brown;
}
.error.show {
display: block;
}
`}</style>
</Layout>
)
}
}
export default Login

View File

@ -0,0 +1,150 @@
import { Component } from 'react'
import fetch from 'isomorphic-unfetch'
import Layout from '../components/layout'
import { signup } from '../utils/auth'
class Signup extends Component {
static getInitialProps ({ req }) {
const protocol = process.env.NODE_ENV === 'production' ? 'https' : 'http'
const apiUrl = process.browser
? `${protocol}://${window.location.host}/signup`
: `${protocol}://${req.headers.host}/signup`
return { apiUrl }
}
constructor (props) {
super(props)
this.state = { username: '', password: '', confirmPassword: '', error: '' }
this.handleChangeUsername = this.handleChangeUsername.bind(this)
this.handleChangePassword = this.handleChangePassword.bind(this)
this.handleChangeConfirmPassword = this.handleChangeConfirmPassword.bind(this)
this.handleSubmit = this.handleSubmit.bind(this)
}
handleChangeUsername (event) {
this.setState({ username: event.target.value })
}
handleChangePassword (event) {
this.setState({ password: event.target.value })
}
handleChangeConfirmPassword (event) {
this.setState({ confirmPassword: event.target.value })
}
async handleSubmit (event) {
event.preventDefault()
this.setState({ error: '' })
const username = this.state.username
const password = this.state.password
const confirmPassword = this.state.confirmPassword
const url = this.props.apiUrl
try {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password, confirmPassword })
})
if (response.ok) {
const { token } = await response.json()
signup({ token })
} else {
console.log('Login failed.')
// https://github.com/developit/unfetch#caveats
let error = new Error(response.statusText)
error.response = response
throw error
}
} catch (error) {
console.error(
'You have an error in your code or there are Network issues.',
error
)
this.setState({ error: error.message })
}
}
render () {
return (
<Layout>
<div className='login'>
<form onSubmit={this.handleSubmit}>
<label htmlFor='username'>Enter username</label>
<input
type='text'
id='username'
name='username'
value={this.state.username}
onChange={this.handleChangeUsername}
/>
<label htmlFor='username'>Enter Password</label>
<input
type='text'
id='password'
name='password'
value={this.state.password}
onChange={this.handleChangePassword}
/>
<label htmlFor='username'>Confirm Password</label>
<input
type='text'
id='confirmPassword'
name='confirmPassword'
value={this.state.confirmPassword}
onChange={this.handleChangeConfirmPassword}
/>
<button type='submit'>Signup</button>
<p className={`error ${this.state.error && 'show'}`}>
{this.state.error && `Error: ${this.state.error}`}
</p>
</form>
</div>
<style jsx>{`
.login {
max-width: 340px;
margin: 0 auto;
padding: 1rem;
border: 1px solid #ccc;
border-radius: 4px;
}
form {
display: flex;
flex-flow: column;
}
label {
font-weight: 600;
}
input {
padding: 8px;
margin: 0.3rem 0 1rem;
border: 1px solid #ccc;
border-radius: 4px;
}
.error {
margin: 0.5rem 0 0;
display: none;
color: brown;
}
.error.show {
display: block;
}
`}</style>
</Layout>
)
}
}
export default Signup

View File

@ -0,0 +1,59 @@
const { createServer } = require('http')
const httpProxy = require('http-proxy')
const { parse } = require('url')
const next = require('next')
const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handle = app.getRequestHandler()
const proxy = httpProxy.createProxyServer()
const target = 'http://localhost:8080'
app.prepare().then(() => {
createServer((req, res) => {
const parsedUrl = parse(req.url, true)
const { pathname, query } = parsedUrl
switch (pathname) {
case '/':
app.render(req, res, '/', query)
break
case '/sign-in':
app.render(req, res, '/login', query)
break
case '/sign-up':
app.render(req, res, '/signup', query)
break
case '/login':
proxy.web(req, res, { target }, error => {
console.log('Error!', error)
})
break
case '/signup':
proxy.web(req, res, { target }, error => {
console.log('Error!', error)
})
break
case '/articles':
app.render(req, res, '/articles', query)
break
case '/api/profile.js':
proxy.web(req, res, { target }, error => console.log('Error!', error))
break
default:
handle(req, res, parsedUrl)
break
}
}).listen(3000, err => {
if (err) throw err
console.log('> Ready on http://localhost:3000')
})
})

View File

@ -0,0 +1,91 @@
import { Component } from 'react'
import Router from 'next/router'
import nextCookie from 'next-cookies'
import cookie from 'js-cookie'
export const login = async ({ token }) => {
cookie.set('token', token, { expires: 1 })
// localStorage.setItem('token', token)
Router.push('/articles')
}
export const signup = async ({ token }) => {
cookie.set('token', token, { expires: 1 })
// localStorage.setItem('token', token)
Router.push('/')
}
export const logout = () => {
cookie.remove('token')
// to support logging out from all windows
window.localStorage.setItem('logout', Date.now())
// window.localStorage.removeItem('token')
Router.push('/login')
}
// Gets the display name of a JSX component for dev tools
const getDisplayName = Component =>
Component.displayName || Component.name || 'Component'
export const withAuthSync = WrappedComponent =>
class extends Component {
static displayName = `withAuthSync(${getDisplayName(WrappedComponent)})`
static async getInitialProps (ctx) {
const token = auth(ctx)
const componentProps =
WrappedComponent.getInitialProps &&
(await WrappedComponent.getInitialProps(ctx))
return { ...componentProps, token }
}
constructor (props) {
super(props)
this.syncLogout = this.syncLogout.bind(this)
}
componentDidMount () {
window.addEventListener('storage', this.syncLogout)
}
componentWillUnmount () {
window.removeEventListener('storage', this.syncLogout)
window.localStorage.removeItem('logout')
}
syncLogout (event) {
if (event.key === 'logout') {
console.log('logged out from storage!')
Router.push('/login')
}
}
render () {
return <WrappedComponent {...this.props} />
}
}
export const auth = ctx => {
const { token } = nextCookie(ctx)
/*
* This happens on server only, ctx.req is available means it's being
* rendered on server. If we are on server and token is not available,
* means user is not logged in.
*/
if (ctx.req && !token) {
ctx.res.writeHead(302, { Location: '/login' })
ctx.res.end()
return
}
// We already checked for server. This should only happen on client.
if (!token) {
Router.push('/login')
}
return token
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1 @@
endpoint: http://localhost:8080

View File

@ -0,0 +1,342 @@
--
-- PostgreSQL database dump
--
-- Dumped from database version 11.2 (Debian 11.2-1.pgdg90+1)
-- Dumped by pg_dump version 11.2
-- Started on 2019-04-03 15:23:09 IST
--
-- TOC entry 4 (class 2615 OID 2200)
-- Name: public; Type: SCHEMA; Schema: -; Owner: -
--
SET default_tablespace = '';
SET default_with_oids = false;
--
-- TOC entry 220 (class 1259 OID 16612)
-- Name: user; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public."user" (
id uuid DEFAULT public.gen_random_uuid() NOT NULL,
username character varying(255) NOT NULL,
password character varying(255) NOT NULL,
created_at timestamp with time zone DEFAULT now() NOT NULL,
active boolean DEFAULT true
);
--
-- TOC entry 224 (class 1259 OID 16669)
-- Name: article; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public.article (
id integer NOT NULL,
title text NOT NULL,
content text NOT NULL,
created_at timestamp with time zone DEFAULT now() NOT NULL,
user_id uuid NOT NULL
);
--
-- TOC entry 223 (class 1259 OID 16667)
-- Name: articles_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--
CREATE SEQUENCE public.articles_id_seq
AS integer
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--
-- TOC entry 3042 (class 0 OID 0)
-- Dependencies: 223
-- Name: articles_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
--
ALTER SEQUENCE public.articles_id_seq OWNED BY public.article.id;
--
-- TOC entry 217 (class 1259 OID 16598)
-- Name: knex_migrations; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public.knex_migrations (
id integer NOT NULL,
name character varying(255),
batch integer,
migration_time timestamp with time zone
);
--
-- TOC entry 216 (class 1259 OID 16596)
-- Name: knex_migrations_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--
CREATE SEQUENCE public.knex_migrations_id_seq
AS integer
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--
-- TOC entry 3043 (class 0 OID 0)
-- Dependencies: 216
-- Name: knex_migrations_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
--
ALTER SEQUENCE public.knex_migrations_id_seq OWNED BY public.knex_migrations.id;
--
-- TOC entry 219 (class 1259 OID 16606)
-- Name: knex_migrations_lock; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public.knex_migrations_lock (
index integer NOT NULL,
is_locked integer
);
--
-- TOC entry 218 (class 1259 OID 16604)
-- Name: knex_migrations_lock_index_seq; Type: SEQUENCE; Schema: public; Owner: -
--
CREATE SEQUENCE public.knex_migrations_lock_index_seq
AS integer
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--
-- TOC entry 3044 (class 0 OID 0)
-- Dependencies: 218
-- Name: knex_migrations_lock_index_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
--
ALTER SEQUENCE public.knex_migrations_lock_index_seq OWNED BY public.knex_migrations_lock.index;
--
-- TOC entry 221 (class 1259 OID 16628)
-- Name: role; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public.role (
id uuid DEFAULT public.gen_random_uuid() NOT NULL,
name character varying(255) NOT NULL
);
--
-- TOC entry 222 (class 1259 OID 16638)
-- Name: user_role; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public.user_role (
id uuid DEFAULT public.gen_random_uuid() NOT NULL,
role_id uuid,
user_id uuid
);
--
-- TOC entry 2878 (class 2604 OID 16672)
-- Name: article id; Type: DEFAULT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.article ALTER COLUMN id SET DEFAULT nextval('public.articles_id_seq'::regclass);
--
-- TOC entry 2871 (class 2604 OID 16601)
-- Name: knex_migrations id; Type: DEFAULT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.knex_migrations ALTER COLUMN id SET DEFAULT nextval('public.knex_migrations_id_seq'::regclass);
--
-- TOC entry 2872 (class 2604 OID 16609)
-- Name: knex_migrations_lock index; Type: DEFAULT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.knex_migrations_lock ALTER COLUMN index SET DEFAULT nextval('public.knex_migrations_lock_index_seq'::regclass);
--
-- TOC entry 2904 (class 2606 OID 16678)
-- Name: article articles_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.article
ADD CONSTRAINT articles_pkey PRIMARY KEY (id);
--
-- TOC entry 2883 (class 2606 OID 16611)
-- Name: knex_migrations_lock knex_migrations_lock_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.knex_migrations_lock
ADD CONSTRAINT knex_migrations_lock_pkey PRIMARY KEY (index);
--
-- TOC entry 2881 (class 2606 OID 16603)
-- Name: knex_migrations knex_migrations_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.knex_migrations
ADD CONSTRAINT knex_migrations_pkey PRIMARY KEY (id);
--
-- TOC entry 2892 (class 2606 OID 16635)
-- Name: role role_id_unique; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.role
ADD CONSTRAINT role_id_unique UNIQUE (id);
--
-- TOC entry 2894 (class 2606 OID 16637)
-- Name: role role_name_unique; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.role
ADD CONSTRAINT role_name_unique UNIQUE (name);
--
-- TOC entry 2896 (class 2606 OID 16633)
-- Name: role role_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.role
ADD CONSTRAINT role_pkey PRIMARY KEY (id);
--
-- TOC entry 2886 (class 2606 OID 16624)
-- Name: user user_id_unique; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public."user"
ADD CONSTRAINT user_id_unique UNIQUE (id);
--
-- TOC entry 2888 (class 2606 OID 16622)
-- Name: user user_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public."user"
ADD CONSTRAINT user_pkey PRIMARY KEY (id);
--
-- TOC entry 2898 (class 2606 OID 16645)
-- Name: user_role user_role_id_unique; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.user_role
ADD CONSTRAINT user_role_id_unique UNIQUE (id);
--
-- TOC entry 2900 (class 2606 OID 16643)
-- Name: user_role user_role_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.user_role
ADD CONSTRAINT user_role_pkey PRIMARY KEY (id);
--
-- TOC entry 2890 (class 2606 OID 16626)
-- Name: user user_username_unique; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public."user"
ADD CONSTRAINT user_username_unique UNIQUE (username);
--
-- TOC entry 2884 (class 1259 OID 16627)
-- Name: user_active_index; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX user_active_index ON public."user" USING btree (active);
--
-- TOC entry 2901 (class 1259 OID 16646)
-- Name: user_role_role_id_index; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX user_role_role_id_index ON public.user_role USING btree (role_id);
--
-- TOC entry 2902 (class 1259 OID 16652)
-- Name: user_role_user_id_index; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX user_role_user_id_index ON public.user_role USING btree (user_id);
--
-- TOC entry 2907 (class 2606 OID 16731)
-- Name: article articles_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.article
ADD CONSTRAINT articles_user_id_fkey FOREIGN KEY (user_id) REFERENCES public."user"(id);
--
-- TOC entry 2905 (class 2606 OID 16647)
-- Name: user_role user_role_role_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.user_role
ADD CONSTRAINT user_role_role_id_foreign FOREIGN KEY (role_id) REFERENCES public.role(id);
--
-- TOC entry 2906 (class 2606 OID 16653)
-- Name: user_role user_role_user_id_foreign; Type: FK CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.user_role
ADD CONSTRAINT user_role_user_id_foreign FOREIGN KEY (user_id) REFERENCES public."user"(id);
-- Completed on 2019-04-03 15:23:10 IST
--
-- PostgreSQL database dump complete
--

View File

@ -0,0 +1,88 @@
functions: []
query_templates: []
remote_schemas: []
tables:
- array_relationships: []
delete_permissions: []
event_triggers: []
insert_permissions: []
object_relationships: []
select_permissions:
- comment: null
permission:
allow_aggregations: true
columns:
- id
- title
- content
- created_at
- user_id
filter:
user_id:
_eq: X-Hasura-User-Id
role: user
table: article
update_permissions: []
- array_relationships: []
delete_permissions: []
event_triggers: []
insert_permissions: []
object_relationships: []
select_permissions: []
table: role
update_permissions: []
- array_relationships: []
delete_permissions: []
event_triggers: []
insert_permissions:
- comment: null
permission:
check:
id:
_eq: X-Hasura-User-Id
columns:
- active
- created_at
- id
- password
- username
set: {}
role: user
object_relationships: []
select_permissions:
- comment: null
permission:
allow_aggregations: false
columns:
- id
- username
- password
- created_at
- active
filter:
id:
_eq: X-Hasura-User-Id
role: user
table: user
update_permissions:
- comment: null
permission:
columns:
- active
- created_at
- id
- password
- username
filter:
id:
_eq: X-Hasura-User-Id
set: {}
role: user
- array_relationships: []
delete_permissions: []
event_triggers: []
insert_permissions: []
object_relationships: []
select_permissions: []
table: user_role
update_permissions: []

View 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/

View File

@ -30,7 +30,7 @@
- Clone this repo:
```bash
git clone https://github.com/hasura/graphql-engine
cd graphql-engine/community/sample-apps/nextjs-8-serverless
cd graphql-engine/community/sample-apps/nextjs-8-serverless/with-apollo
```
- Install npm modules:

View File

@ -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} />
}
}
}

View File

@ -0,0 +1,3 @@
module.exports = {
target: "serverless"
}

View File

@ -0,0 +1,6 @@
{
"version": 2,
"builds": [
{ "src": "next.config.js", "use": "@now/next" }
]
}

View 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)