add community boilerplates and examples (#430)

This commit is contained in:
Anon Ray 2018-09-13 06:30:07 +00:00 committed by Shahidh K Muhammed
parent be20a11d37
commit acd62c3bf8
255 changed files with 58710 additions and 18 deletions

View File

@ -3,6 +3,7 @@ LICENSE
scripts/*
assets/*
docs/*
community/*
.circleci/*
.ciignore
.gitignore

1
.gitignore vendored
View File

@ -1 +1,2 @@
npm-debug.log
*.temp

View File

@ -24,6 +24,19 @@ consisting of 3 components. Each have their own contributing guides:
All of the three components have a single version, denoted by either the git
tag, or a combination of branch name and git commit SHA.
### Docs
Contributing guide for docs can be found at [docs/CONTRIBUTING.md](docs/CONTRIBUTING.md).
### Community content
There is no specific contributing guide for community content. Anything that can
help GraphQL Engine community/users can go into the section. We have identified
[boilerplates](community/boilerplates), [examples](community/examples) and
[tools](community/tools) as primary candidates. Feel free to submit a pull
request if you have something to add (not necessarily belonging to the
before-mentioned sections).
## Issues
### Reporting an Issue

24
LICENSE-community Normal file
View File

@ -0,0 +1,24 @@
MIT Licence
Copyright (c) 2018-present, Hasura Technologies Private Limited
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

View File

@ -17,11 +17,15 @@ Read more at [hasura.io](https://hasura.io) and the [docs](https://docs.hasura.i
------------------
![Hasura GraphQL Enigne Demo](assets/demo.gif)
---
------------------
![Hasura GraphQL Engine Readltime Demo](assets/realtime.gif)
-------------------
## Features
* **Make powerful queries**: Built-in filtering, pagination, pattern search, bulk insert, update, delete mutations
* **Realtime**: Convert any GraphQL query to a live query by using subscriptions
* **Trigger webhooks or serverless functions**: On Postgres insert/update/delete events ([read more](event-triggers.md))
@ -33,12 +37,26 @@ Read more at [hasura.io](https://hasura.io) and the [docs](https://docs.hasura.i
Read more at: [https://hasura.io](https://hasura.io) and the [docs](https://docs.hasura.io).
## Demos
## Table of contents
<!-- markdown-toc start - Don't edit this section. Run M-x markdown-toc-refresh-toc -->
**Table of Contents**
* [Add GraphQL to a self-hosted GitLab instance](https://www.youtube.com/watch?v=a2AhxKqd82Q) (*3:44 mins*)
* [Todo app with Auth0 and GraphQL backend](https://www.youtube.com/watch?v=15ITBYnccgc) (*4:00 mins*)
* [GraphQL on GitLab integrated with GitLab auth](https://www.youtube.com/watch?v=m1ChRhRLq7o) (*4:05 mins*)
* [Dashboard for 10million rides with geo-location (PostGIS, Timescale)](https://www.youtube.com/watch?v=tsY573yyGWA) (*3:06 mins*)
- [Quickstart:](#quickstart)
- [One-click deployment on Heroku](#one-click-deployment-on-heroku)
- [Other deployment methods](#other-deployment-methods)
- [Architecture](#architecture)
- [Client-side tooling](#client-side-tooling)
- [Add business logic](#add-business-logic)
- [Custom resolvers](#custom-resolvers)
- [Trigger webhooks on database events](#trigger-webhooks-on-database-events)
- [Demos](#demos)
- [Realtime applications](#realtime-applications)
- [Videos](#videos)
- [Support & Troubleshooting](#support--troubleshooting)
- [Contributing](#contributing)
- [License](#license)
<!-- markdown-toc end -->
## Quickstart:
@ -74,6 +92,51 @@ You can also place the engine behind a central GraphQL proxy that fronts multipl
Hasura works with any GraphQL client. We recommend using [Apollo Client](https://github.com/apollographql/apollo-client). See [awesome-graphql](https://github.com/chentsulin/awesome-graphql) for a list of clients.
## Add business logic
### Custom resolvers
Add custom resolvers in addition to Hasura GraphQL engine. Ideal for delegating
to HTTP APIs, making direct calls to another data-source or writing business
logic in code - [read more](community/boilerplates/custom-resolvers).
### Trigger webhooks on database events
Add asynchronous business logic that is triggered based on database events.
Ideal for notifications, data-pipelines from Postgres or asynchronous
processing - [read more](event-triggers.md).
## Demos
Checkout all the example applications in the
[community/examples](community/examples) directory.
### Realtime applications
- Group Chat application built with React, includes a typing indicator, online users & new
message notifications.
- [Try it out](https://chat-example-trial-roar.herokuapp.com/)
- [Tutorial](community/examples/realtime-chat)
- [Browse APIs](https://hasura-realtime-group-chat.herokuapp.com/)
- Live location tracking app that shows a running vehicle changing current GPS
coordinates moving on a map.
- [Try it out](https://hasura.github.io/realtime-location-app/)
- [Tutorial](community/examples/realtime-location-tracking)
- [Browse APIs](https://realtime-backend.herokuapp.com/)
- A realtime dashboard for data aggregations on continuously changing data.
- [Try it out](https://shahidh.in/hasura-realtime-poll/)
- [Tutorial](community/examples/realtime-poll)
- [Browse APIs](https://hasura-realtime-poll.herokuapp.com/)
### Videos
* [Add GraphQL to a self-hosted GitLab instance](https://www.youtube.com/watch?v=a2AhxKqd82Q) (*3:44 mins*)
* [Todo app with Auth0 and GraphQL backend](https://www.youtube.com/watch?v=15ITBYnccgc) (*4:00 mins*)
* [GraphQL on GitLab integrated with GitLab auth](https://www.youtube.com/watch?v=m1ChRhRLq7o) (*4:05 mins*)
* [Dashboard for 10million rides with geo-location (PostGIS, Timescale)](https://www.youtube.com/watch?v=tsY573yyGWA) (*3:06 mins*)
## Support & Troubleshooting
@ -92,6 +155,17 @@ Check out our [contributing guide](CONTRIBUTING.md) for more details.
## License
GraphQL Engine is available under the [GNU Affero General Public License v3](https://www.gnu.org/licenses/agpl-3.0.en.html) (AGPL-3.0), the same license as [MongoDB](https://www.mongodb.com/community/licensing). We have written more about what you can and cannot do under AGPL [here](https://github.com/hasura/graphql-engine/wiki/License-Explained).
The core GraphQL Engine is available under the [GNU Affero General Public
License v3](https://www.gnu.org/licenses/agpl-3.0.en.html) (AGPL-3.0), the same
license as [MongoDB](https://www.mongodb.com/community/licensing). We have
written more about what you can and cannot do under AGPL
[here](https://github.com/hasura/graphql-engine/wiki/License-Explained).
**Commercial licenses** that bundle the Hasura GraphQL Engine with support, and SLAs are available on request. Please feel free to contact us at build@hasura.io or on our [website chat](https://hasura.io).
**Commercial licenses** that bundle the Hasura GraphQL Engine with support, and
SLAs are available on request. Please feel free to contact us at build@hasura.io
or on our [website chat](https://hasura.io).
All **other contents** (except those in [`server`](server), [`cli`](cli) and
[`console`](console) directories) are under [MIT License](LICENSE-community).
This includes everything in the [`docs`](docs) and [`community`](community)
directories.

16
community/README.md Normal file
View File

@ -0,0 +1,16 @@
# Community
This directory contains community contributed code and content that supplements
Hasura GraphQL Engine. It includes several example applications built using
GraphQL Engine to demonstrate it's features, several boilerplates for users to get
started with auth webhooks, triggers etc. and some community tooling around the Engine.
Feel free to open pull requests to add more content here.
- [Boilerplates](boilerplates)
- [Examples](examples)
- [Tools](tools)
## License
All contents of this directory are under [MIT License](../LICENSE-community).

View File

@ -0,0 +1 @@
*.temp

View File

@ -0,0 +1,12 @@
# Boilerplates for auth webhooks
This directory is a compilation of code samples (boilerplates) that can be used to implement custom auth webhooks for the Hasura GraphQL engine.
## Contributing
- Please fork this repo and submit a PR with your changes
- Please create a directory name, lowercase, hyphenated
- Please add a helpful README that has the following sections:
- Overview
- Deployment instructions
- Creation of any specific environment variables or secrets

View File

@ -0,0 +1,96 @@
# Sample Firebase Cloud Function Auth Webhook for Hasura GraphQL engine
Further reading: [Firebase SDK for Cloud Functions](https://firebase.google.com/docs/functions/)
## Install and deploy
1. Create a Firebase Project using the [Firebase Console](https://console.firebase.google.com).
1. Clone or download this repo and go to `cd firebase-cloud-functions` directory.
1. You must have the Firebase CLI installed. If you don't have it install it with `npm install -g firebase-tools` and then configure it with `firebase login`.
1. Configure the CLI locally by using `firebase init` and select your project in the list.
1. Follow [Add the Firebase Admin SDK to Your Server](https://firebase.google.com/docs/admin/setup) and save it as config.js
1. Copy index.js and config.js to functions folder by: `cp index.js config.js functions/`
1. Install Cloud Functions dependencies locally by running: `cd functions; npm install`
1. Deploy to Firebase Cloud Functions by `firebase deploy`
Once deployed endpoint like this will be created and displayed:
```bash
https://us-central1-xxxxx-auth.cloudfunctions.net/hasuraWebhook
```
## Add webhook endpoint to Hasura GraphQL
Set `--auth-hook` or `HASURA_GRAPHQL_AUTH_HOOK` to the endpoint obtained above.
[GraphQL engine server flags reference](https://docs.hasura.io/1.0/graphql/manual/deployment/graphql-engine-flags/reference.html) for details.
## Create table and set permission
Follow [Common roles and auth examples](https://docs.hasura.io/1.0/graphql/manual/auth/common-roles-auth-examples.html)
on Hasura doc for details of how to setup permission to a table.
Make sure to change id column of user table to TXT type as uid sent from webhook is firebase User UID format (e.g. 0LnvZc7405TjRTbjURhZYYVXPI52)
## How to call webhook from frontend JS code (React, VueJS, Angular etc...)
postAxios.js
```bash
import axiosBase from 'axios'
import * as firebase from 'firebase'
const getIdToken = async () => {
return new Promise((resolve, reject) => {
firebase.auth().onAuthStateChanged(function (user) {
if (user) {
resolve(firebase.auth().currentUser.getIdToken())
} else {
reject(Error('user logged out'))
}
})
})
}
export const postAxios = async (queryString) => {
const idToken = await getIdToken()
const axios = axiosBase.create({
baseURL: 'https://YOURHASURADOMAIN/v1alpha1/graphql',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + idToken
},
responseType: 'json',
method: 'post'
})
return await axios({
data: {
query: queryString
}
}).catch(({response: r}) => console.log(r))
}
```
userService.js
```bash
import { postAxios } from './postAxios'
export default {
async getUsers () {
const queryString = `
query {
user
{
id
name
}
}
 `
const result = await postAxios(queryString)
return result.data.data.user
}
}
```

View File

@ -0,0 +1,55 @@
const functions = require('firebase-functions');
var admin = require('firebase-admin');
var serviceAccount = require('./config.json');
exports.hasuraWebhook = functions.https.onRequest((request, response) => {
var error = null;
if (serviceAccount) {
try {
admin.initializeApp({
credential: admin.credential.cert(serviceAccount)
});
} catch (e) {
error = e;
}
}
var authHeaders = request.get('Authorization');
// Send anonymous role if there are no auth headers
if (!authHeaders) {
response.json({'x-hasura-role': 'anonymous'});
return;
} else {
// Validate the received id_tokenolp;
var idToken = extractToken(authHeaders);
console.log(idToken);
admin.auth().verifyIdToken(idToken)
.then((decodedToken) => {
console.log('decodedToken', decodedToken);
var hasuraVariables = {
'X-Hasura-User-Id': decodedToken.uid,
'X-Hasura-Role': 'user'
};
console.log(hasuraVariables); // For debug
// Send appropriate variables
response.json(hasuraVariables);
return;
})
.catch((e) => {
// Throw authentication error
console.log(e);
response.json({'x-hasura-role': 'anonymous'});
});
}
});
const extractToken = (bearerToken) => {
const regex = /^(Bearer) (.*)$/g;
const match = regex.exec(bearerToken);
if (match && match[2]) {
return match[2];
}
return null;
}

View File

@ -0,0 +1 @@
*node_modules

View File

@ -0,0 +1 @@
web: node server.js

View File

@ -0,0 +1,123 @@
# Sample Auth Webhook for Hasura GraphQL engine
This is a sample auth webhook for authenticating requests to the Hasura GraphQL engine.
It has boilerplate code written for auth0 and firebase auth. There is also a generic sample handler in `server.js` where you can handle your custom auth providers.
## Quick deploy
<!--
### Deploy with Heroku (recommended)
1. Click the following button for deploying to Heroku.
[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/hasura/sample-auth-webhook)
2. Once it is deployed, go to `Manage App > Settings` of your app and set the following environment variables if you want to use the associated providers.
- **AUTH_ZERO_DOMAIN**: Example `test.auth0.com`
- **FIREBASE_CONFIG**: Copy the contents of your serviceAccount JSON file for this field. Example:
```
{
"type": "service_account",
"project_id": "testapp-2222",
"private_key_id": "f02aca08952f702de43ed577b428f405efe2d377",
"private_key": "-----BEGIN PRIVATE KEY-----\n<your-private-key>\n-----END PRIVATE KEY-----\n",
"client_email": "firebase-adminsdk-t4sik@testapp-24a60.iam.gserviceaccount.com",
"client_id": "113608616484852272199",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://accounts.google.com/o/oauth2/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-t4sik%40testapp-22222.iam.gserviceaccount.com"
}
```
If you are not using an auth provider, you need not enter the environment variable associated with it
-->
### Deploy using [Now](https://zeit.co/now)
Run the following commands to deploy using Now.
```bash
git clone https://github.com/hasura/graphql-engine
cd graphql-engine/community/boilerplates/auth-webhooks/nodejs-express
npm install -g now
now -e \
AUTH_ZERO_DOMAIN='test.auth0.com' -e \
FIREBASE_CONFIG='{
"type": "service_account",
"project_id": "testapp-2222",
"private_key_id": "f02aca08952f702de43ed577b428f405efe2d377",
"private_key": "-----BEGIN PRIVATE KEY-----\n<your-private-key>\n-----END PRIVATE KEY-----\n",
"client_email": "firebase-adminsdk-t4sik@testapp-24a60.iam.gserviceaccount.com",
"client_id": "113608616484852272199",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://accounts.google.com/o/oauth2/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-t4sik%40testapp-22222.iam.gserviceaccount.com"
}'
```
If you are not using an auth provider, you need not enter the environment variable associated with it. For example, if you are not using firebase, the command last command you should run is,
```bash
$ now -e \
AUTH_ZERO_DOMAIN='test.auth0.com'
```
### Deploy with Glitch
1. Click the following button to edit on glitch
[![glitch-deploy-button](assets/deploy-glitch.png)](http://glitch.com/edit/#!/import/github/hasura/graphql-engine/community/boilerplates/auth-webhooks/nodejs-express)
2. Add the following environment variables in the `.env` file on glitch.
```env
AUTH_ZERO_DOMAIN='test.auth0.com'
FIREBASE_CONFIG='{
"type": "service_account",
"project_id": "testapp-2222",
"private_key_id": "f02aca08952f702de43ed577b428f405efe2d377",
"private_key": "-----BEGIN PRIVATE KEY-----\n<your-private-key>\n-----END PRIVATE KEY-----\n",
"client_email": "firebase-adminsdk-t4sik@testapp-24a60.iam.gserviceaccount.com",
"client_id": "113608616484852272199",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://accounts.google.com/o/oauth2/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-t4sik%40testapp-22222.iam.gserviceaccount.com"
}'
```
If you are not using an auth provider, you need not enter the environment variable associated with it. For example, if you are not using firebase, the command last command you should run is,
```env
AUTH_ZERO_DOMAIN='test.auth0.com'
```
## Usage with Hasura GraphQL engine
Once you have deployed this webhook, you can use it along with the GraphQL engine. You have to set the webhook URL as an environment variable in the docker container that runs the GraphQL engine.
*[Read the docs](https://docs.hasura.io/1.0/graphql/manual/auth/webhook.html).*
### Auth0
Send the auth0 `access_token` as a header while making queries to the `graphql-engine`.
```JSON
{
"Authorization": "Bearer <access_token>"
}
```
### Firebase
Send the firebase `id_token` as a header while making queries to the `graphql-engine`.
```
{
"Authorization": "Bearer <id_token>"
}
```

View File

@ -0,0 +1,13 @@
{
"name": "Hasura GraphQL Engine Auth Webhook",
"description": "Hasura GraphQL Engine Auth Webhook <> heroku",
"keywords": [
"graphql",
"heroku",
"postgres",
"hasura",
"auth",
"webhook"
],
"repository": "https://github.com/hasura/sample-auth-webhook",
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

View File

@ -0,0 +1,60 @@
var express = require('express');
var auth0Router = express.Router();
var requestClient = require('request');
var auth0Domain = process.env.AUTH_ZERO_DOMAIN;
/*
Auth webhook handler for auth0
Flow:
1) Expects access_token to be sent as 'Authorization: Bearer <access-token>
2) Verified access_token by fetching /userinfo endpoint from auth0
Usage:
1) From your application, when you call Hasura's GraphQL APIs remember to send the access_token from auth0 as an authorization header
2) Replace the url (https://test-hasura.auth0.com/userinfo) in the code below with your own auth0 app url
*/
auth0Router.route('/webhook').get((request, response) => {
// Throw 500 if auth0 domain is not configured
if (!auth0Domain) {
response.status(500).send('Auth0 domain not configured');
return;
}
var token = request.get('Authorization');
if (!token) {
response.json({'x-hasura-role': 'anonymous'});
return;
} else {
// Fetch information about this user from
// auth0 to validate this token
// NOTE: Replace the URL with your own auth0 app url
var options = {
url: `https://${auth0Domain}/userinfo`,
headers: {
Authorization: token,
'Content-Type': 'application/json'
}
};
requestClient(options, (err, res, body) => {
if (!err && res.statusCode == 200) {
var userInfo = JSON.parse(body);
console.log(userInfo); //debug
var hasuraVariables = {
'X-Hasura-User-Id': userInfo.sub,
'X-Hasura-Role': 'user'
};
console.log(hasuraVariables); // For debug
response.json(hasuraVariables);
} else {
// Error response from auth0
console.log(err, res, body);
response.json({'x-hasura-role': 'anonymous'});
return;
}
});
}
});
module.exports = auth0Router;

View File

@ -0,0 +1,3 @@
const config = process.env.FIREBASE_CONFIG;
module.exports = config;

View File

@ -0,0 +1,65 @@
var express = require('express');
var firebaseRouter = express.Router();
var admin = require('firebase-admin');
var serviceAccount = require('./config.js');
var error = null;
// Initialize the Firebase admin SDK with your service account credentials
if (serviceAccount) {
try {
admin.initializeApp({
credential: admin.credential.cert(JSON.parse(serviceAccount))
});
} catch (e) {
error = e;
}
}
firebaseRouter.route("/webhook").get((request, response) => {
// Throw 500 if firebase is not configured
if (!serviceAccount) {
response.status(500).send('Firebase not configured');
return;
}
// Check for errors initializing firebase SDK
if (error) {
response.status(500).send('Invalid firebase configuration');
return;
}
// Get authorization headers
var authHeaders = request.get('Authorization');
// Send anonymous role if there are no auth headers
if (!authHeaders) {
response.json({'x-hasura-role': 'anonymous'});
return;
} else {
// Validate the received id_token
var idToken = extractToken(authHeaders);
console.log(idToken);
admin.auth().verifyIdToken(idToken)
.then((decodedToken) => {
var hasuraVariables = {
'X-Hasura-User-Id': decodedToken.uid,
'X-Hasura-Role': 'user'
};
console.log(hasuraVariables); // For debug
// Send appropriate variables
response.json(hasuraVariables);
})
.catch((e) => {
// Throw authentication error
console.log(e);
response.json({'x-hasura-role': 'anonymous'});
});
}
});
const extractToken = (bearerToken) => {
const regex = /^(Bearer) (.*)$/g;
const match = regex.exec(bearerToken);
if (match && match[2]) {
return match[2];
}
return null;
}
module.exports = firebaseRouter;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,17 @@
{
"name": "sample-auth-webhook",
"version": "1.0.0",
"description": "",
"main": "server.js",
"scripts": {
"start": "node server.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "wawhal",
"license": "ISC",
"dependencies": {
"express": "^4.16.3",
"firebase-admin": "^5.12.1",
"request": "^2.87.0"
}
}

View File

@ -0,0 +1,54 @@
// Sample webhook showing what a hasura auth webhook looks like
// init project
var express = require('express');
var app = express();
var requestClient = require('request');
var port = process.env.PORT || 3000;
/* A simple sample
Flow:
1) Extracts token
2) Fetches userInfo in a mock function
3) Return hasura variables
*/
function fetchUserInfo (token, cb) {
// This function takes a token and then makes an async
// call to the session-cache or database to fetch
// data that is needed for Hasura's access control rules
cb();
}
app.get('/', (req, res) => {
res.send('Webhooks are running');
});
app.get('/simple/webhook', (request, response) => {
// Extract token from request
var token = request.get('Authorization');
// Fetch user_id that is associated with this token
fetchUserInfo(token, (result) => {
// Return appropriate response to Hasura
var hasuraVariables = {
'X-Hasura-Role': 'user', // result.role
'X-Hasura-User-Id': '1' // result.user_id
};
response.json(hasuraVariables);
});
});
// Firebase handler
var firebaseRouter = require('./firebase/firebaseHandler');
app.use('/firebase', firebaseRouter);
// Auth0 handler
var auth0Router = require('./auth0/auth0Handler');
app.use('/auth0', auth0Router);
// listen for requests :)
var listener = app.listen(port, function () {
console.log('Your app is listening on port ' + port);
});

View File

@ -0,0 +1,5 @@
node_modules
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

View File

@ -0,0 +1,11 @@
FROM node:alpine
WORKDIR /server
COPY ./package.json /server/
RUN npm install
COPY . /server/
CMD ["npm", "start"]

View File

@ -0,0 +1 @@
web: npm start

View File

@ -0,0 +1,195 @@
# GraphQL Custom Resolver Example
This is a simple example of using a custom resolver with Hasura's GraphQL API.
## Motivation
Hasura GraphQL Engine provides instant GraphQL APIs over the tables and views of
any Postgres database. It also comes with a fine grained access control layer
that helps you restrict the data that can be consumed.
However, sometimes you might have to write custom resolvers to capture business
logic that is unrelated to the database or needs to execute a custom transaction
or write to the database.
In this example, we illustrate how to write custom resolvers and merge them with
the Hasura GraphQL Engine. We combine Hasura GraphQL Engine's GraphQL API
running at `https://bazookaand.herokuapp.com/v1alpha1/graphql` with the
following custom resolvers:
1. A `hello` query
2. A `count` query (that returns a counter from another data source )
3. A `increment_counter` mutation that increments the value of `count`.
4. A `user_average_age` query that makes directly makes an SQL query to Postgres
using knex.
You can use this as a boilerplate to write custom resolvers with Hasura GraphQL
Engine.
![Custom resolvers with Hasura GraphQL engine](./assets/custom-resolvers-diagram.png)
## Usage
1. Install the required dependencies.
```bash
npm install
```
2. Set appropriate environment variables for the GraphQL Engine URL, the access
key to GraphQL Engine and the Postgres connection string.
```bash
# without the /v1apha1/graphql part
export HASURA_GRAPHQL_ENGINE_URL='https://hge.herokuapp.com'
export X_HASURA_ACCESS_KEY='<access_key>'
# Only required for the direct SQL resolver
export PG_CONNECTION_STRING='<postgres-connection-string>'
```
3. Run the server
```bash
npm start
```
## Deployment
You can deploy this sample boilerplate with:
* Now
* Docker
### Deploy using [Now](https://zeit.co/now)
Run these commands to instantly deploy this boilerplate using Now.
```bash
git clone https://github.com/hasura/graphql-engine
cd community/boilerplates/custom-resolvers
now -e \
HASURA_GRAPHQL_ENGINE_URL='https://hge.herokuapp.com' -e \
X_HASURA_ACCESS_KEY='<access_key>' --npm
```
### Deploy the docker image
This project comes with a [`Dockerfile`](Dockerfile).
## Implementation Details
We will use Apollo's `graphql-tools` library to make a working GraphQL Schema
out of our custom resolvers. Finally, we will merge these resolvers with the
existing Hasura schema so that it can be queried under the same endpoint.
### Writing type definitions
The type definitions are written in standard GraphQL format. We need the
following queries in our custom logic:
```graphql
type Query {
# field hello will return "Hello World" which is a string
hello: String,
# field count will return an Int
count: Int,
# field user_average_age will return a Float
user_average_age: Float
}
type Mutation {
# field "increment_counter" will increment the counter and return type IncrementCounter
increment_counter: IncrementCounter,
# IncrementCounter simply returns the new value of the counter
new_count: Int
}
```
### Writing resolvers
Every resolver is a function that is executed with the following arguments in
the order below:
1. `root`: The root of the current field
2. `args`: The arguments provided in the query
3. `context`: The server context, which also consists of headers
4. `info`: The AST document related to the query made
The resolvers in our case are:
```js
const resolvers = {
// resolvers for queries
Query: {
hello: (root, args, context, info) => {
// return response
return 'Hello world!';
},
count: (root, args, context, info) => {
// return response
return count;
},
user_average_age: async (root, args, context, info) => {
// make SQL query using knex client
const response = await knexClient('user')
.avg('age');
// return response
return response[0].avg;
}
},
// resolvers for mutations
Mutation: {
increment_counter: (root, args, context, info) => {
// return response
return { new_count: ++count };
}
}
};
```
### Making a new schema out of these custom resolvers
Use `makeExecutableSchema()` function from the `graphql-tools` library to make a
schema out of the type definitions and resolvers above.
```js
import { makeExecutableSchema } from 'graphql-tools';
const executableCustomSchema = makeExecutableSchema({
typeDefs,
resolvers,
});
```
### Merging with existing Hasura schema and serving it
Merge these custom resolvers with the Hasura GraphQL Engine by using the
`mergeSchemas()` function from the `graphql-tools` library.
```js
import { mergeSchemas } from 'graphql-tools';
const newSchema = mergeSchemas({
schemas: [
executableCustomSchema,
executableHasuraSchema
]
});
const server = new ApolloServer({
schema: newSchema
});
server.listen().then(({ url }) => {
console.log(`Server running at ${url}`);
});
```
Check [this file](src/index.js) to see how it is done.

View File

@ -0,0 +1,12 @@
{
"name": "Custom Resolvers boilerplate",
"description": "Custom resolvers boilerplate for Hasura GraphQL Engine",
"logo": "https://storage.googleapis.com/hasura-graphql-engine/console/assets/favicon.png",
"keywords": [
"graphql",
"heroku",
"postgres",
"hasura"
],
"repository": "https://github.com/hasura/graphql-engine/community/boilerplates/custom-resolvers"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,25 @@
{
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/hasura/graphql-engine/community/boilerplates/custom-resolvers"
},
"scripts": {
"start": "node -r esm src/index.js"
},
"dependencies": {
"apollo-link-http": "^1.5.4",
"apollo-link-ws": "^1.0.8",
"apollo-server": "^2.0.6",
"apollo-server-express": "^1.3.6",
"apollo-utilities": "^1.0.20",
"esm": "^3.0.61",
"graphql": "^0.13.2",
"graphql-tools-with-subscriptions": "^1.0.0",
"knex": "^0.15.2",
"node-fetch": "^2.1.2",
"pg": "^7.4.3",
"subscriptions-transport-ws": "^0.9.14",
"ws": "^6.0.0"
}
}

View File

@ -0,0 +1,45 @@
import knex from 'knex';
import pg from 'pg';
const { PG_CONNECTION_STRING } = process.env;
// create a knex client to connect to directly connect to postgres
pg.defaults.ssl = true;
const knexClient = knex({
client: 'pg',
connection: PG_CONNECTION_STRING
});
let count = 0;
// custom resolvers
const resolvers = {
// resolvers for queries
Query: {
hello: (root, args, context, info) => {
// return response
return 'Hello world!';
},
count: (root, args, context, info) => {
// return response
return count;
},
user_average_age: async (root, args, context, info) => {
// make SQL query using knex client
const response = await knexClient('user')
.avg('age');
// return response
return response[0].avg;
}
},
// resolvers for mutations
Mutation: {
increment_counter: (root, args, context, info) => {
// return response
return { new_count: ++count };
}
}
};
export default resolvers;

View File

@ -0,0 +1,18 @@
// custom type definitions
const typeDefs = `
type Query {
hello: String,
count: Int,
user_average_age: Float
}
type Mutation {
increment_counter: MutationResp
}
type MutationResp {
new_count: Int
}
`;
export default typeDefs;

View File

@ -0,0 +1,53 @@
import fetch from 'node-fetch';
import { ApolloServer } from 'apollo-server';
import { mergeSchemas, makeExecutableSchema } from 'graphql-tools';
import { getRemoteSchema } from './utils';
import typeDefs from './customTypeDefs';
import resolvers from './customResolvers';
const HASURA_GRAPHQL_ENGINE_URL = process.env.HASURA_GRAPHQL_ENGINE_URL || `https://bazookaand.herokuapp.com`;
const HASURA_GRAPHQL_API_URL = HASURA_GRAPHQL_ENGINE_URL + '/v1alpha1/graphql';
const ACCESS_KEY = process.env.X_HASURA_ACCESS_KEY;
const runServer = async () => {
// make Hasura schema
const executableHasuraSchema = await getRemoteSchema(
HASURA_GRAPHQL_API_URL,
ACCESS_KEY && { 'x-hasura-access-key': ACCESS_KEY }
);
// make executable schema out of custom resolvers and typedefs
const executableCustomSchema = makeExecutableSchema({
typeDefs,
resolvers,
});
// merge custom resolvers with Hasura schema
const finalSchema = mergeSchemas({
schemas: [
executableCustomSchema,
executableHasuraSchema,
]
});
// instantiate a server instance
const server = new ApolloServer({
schema: finalSchema,
introspection: true,
playground: false
});
// run the server
server.listen({
port: process.env.PORT || 4000
}).then(({url}) => {
console.log('Server running. Open ' + url + ' to run queries.');
});
}
try {
runServer();
} catch (e) {
console.log(e, e.message, e.stack);
}

View File

@ -0,0 +1,62 @@
import { HttpLink } from 'apollo-link-http';
import { WebSocketLink } from 'apollo-link-ws';
import { SubscriptionClient } from 'subscriptions-transport-ws';
import ws from 'ws';
import { makeRemoteExecutableSchema, introspectSchema } from 'graphql-tools';
import fetch from 'node-fetch';
import { split } from 'apollo-link';
import { getMainDefinition } from 'apollo-utilities';
const { HASURA_GRAPHQL_ENGINE_AUTH_HOOK } = process.env;
// util function to fetch and create remote schema
export const getRemoteSchema = async (uri, headers) => {
const link = makeHttpAndWsLink(uri, headers);
const schema = await introspectSchema(link);
return makeRemoteExecutableSchema({
schema,
link
});
};
/* create an apollo-link instance that makes
WS connection for subscriptions and
HTTP connection for queries andm utations
*/
const makeHttpAndWsLink = (uri, headers) => {
// Create an http link:
const httpLink = new HttpLink({
uri,
fetch,
headers
});
// Create a WebSocket link:
const wsLink = new WebSocketLink(new SubscriptionClient(
uri,
{
reconnect: true,
connectionParams: {
headers
}
},
ws
));
// chose the link to use based on operation
const link = split(
// split based on operation type
({ query }) => {
const { kind, operation } = getMainDefinition(query);
return kind === 'OperationDefinition' && operation === 'subscription';
},
wsLink,
httpLink,
);
return link;
};

View File

@ -0,0 +1,2 @@
.DS_Store
*.temp

View File

@ -0,0 +1,69 @@
# Event trigger boilerplates for serverless cloud functions
This repository contains boilerplate functions for various use-cases, for different serverless or cloud function platforms. These functions implement sample use-cases of the different types of asynchronous business logic operations that can be triggered by the Hasura GraphQL Engine on database insert, update or delete.
Examples in this repository support the following cloud function platforms:
* AWS Lambda
* Google Cloud Functions
* Microsoft Azure Functions
* Zeit serverless docker
Note: *If you want to add support for other platforms, please submit a PR or create an issue and tag it with `help-wanted`*
## Events Trigger and Serverless functions architecture
![Architecture diagram](assets/basic-event-triggers-arch-diagram.png)
## Setup Postgres + Hasura GraphQL engine
This boilerplate code assumes that you already have a HGE instance running.
If not you can visit the [docs](https://docs.hasura.io/1.0/graphql/manual/getting-started/index.html) and setup Postgres + HGE.
## Deploying the boilerplate examples
Follow the cloud function provider docs for setting these up. Provider specific README also contains CLI instructions.
## Documented examples
* A simple example to echo the trigger payload.
* Make a GraphQL mutation on some data update/insert, from the serverless function (*write related data back into the database*).
* Asynchronously send a FCM/APNS push notification.
* ETL or data transformation: transform the trigger payload and update an algolia index.
Note: Some of the examples have a corresponding `great-first-issue` issue in the repository. Please checkout the checklist in the README in the cloud provider folders for such issues.
## Directory Structure
Boilerplates have been organised into top-level folders for each cloud function platform. Inside each such folder, there's a folder for each language, followed by each use-case. The README for each cloud function provider has a list of available boilerplates.
The following is a representative tree of the folder structure:
.
├── aws-lambda
| |── README.md
| |── nodejs
| | |── echo
| | |── mutation
| |── python
| | ...
|
├── azure-functions
| |── README.md
| |── nodejs
| | |── echo
| | |── mutation
| |── python
| | ...
## Contributing and boilerplate requests
Want to contribute to this repo? Issues marked `good-first-issue` or `help-wanted` are a good place to begin. Please submit a PR for new boilerplates (other use-cases or cloud function providers/platforms like Apache OpenWhisk, etc.). You can also create issues to request new boilerplates.

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@ -0,0 +1,27 @@
# Boilerplates for AWS Lambda serverless functions and Hasura GraphQL Engine's Event Triggers
Sample cloud functions that can be triggered on changes in the database using GraphQL Engine's Event Triggers.
These are organized in language-specific folders.
**NOTE**
Some of the language/platforms are work in progress. We welcome contributions for the WIP langauages. See issues and the following checklist:
| Folder name | Use-case| Node.js(6) | Python | Java | Go | C#
|-------------|---------|:--------:|:------:|:----:|:---:|:---:
| echo | echo the trigger payload | ✅ | ✅ | ❌ | ❌ | ❌
| mutation | insert related data on an insert event using graphql mutation | ✅ | ✅ | ❌ | ❌ | ❌
| push-notification | send push notification on database event | ❌ | ❌ | ❌ | ❌ | ❌
| etl | transform the trigger payload and update an algolia index | ❌ | ❌ | ❌ | ❌ | ❌
## Pre-requisites
1. AWS account with billing enabled
2. Hasura GraphQL Engine
### AWS setup
You need to create a corresponding AWS Lambda for each of these examples.
As the Hasura event system takes webhooks as the event triggers, we need to expose these Lambdas as webhooks. To do that, we need to add the API gateway trigger to each lambda and add an API to it.

View File

@ -0,0 +1,23 @@
# Setup tables
1. Create table:
```
notes:
id: int
note: text
```
# Setup AWS Lambda
Create a lambda function in AWS. This will be our webhook.
1. Create a function.
2. Select Node.js 6 as the runtime.
3. Select "start from scratch".
4. Add API gateway as a trigger.
5. Add an API to API gateway.
6. Add the code in `index.js`. The handler function of your lambda will be the `index.handler`.
# Add the trigger in Hasura GraphQL
1. In events tab, add a trigger
2. Select all insert, update, delete operations for the trigger.
3. Paste the API endpoint of your AWS lambda as the webhook.

View File

@ -0,0 +1,28 @@
// Lambda which just echoes back the event data
exports.handler = (event, context, callback) => {
let request;
try {
request = JSON.parse(event.body);
} catch (e) {
return callback(null, {statusCode: 400, body: "cannot parse hasura event"});
}
let response = {
statusCode: 200,
body: ''
};
console.log(request);
if (request.table.name === "notes" && request.event.op === "INSERT") {
response.body = `New note ${request.event.data.new.id} inserted, with data: ${request.event.data.new.note}`;
}
else if (request.table.name === "notes" && request.event.op === "UPDATE") {
response.body = `Note ${request.event.data.new.id} updated, with data: ${request.event.data.new.note}`;
}
else if (request.table.name === "notes" && request.event.op === "DELETE") {
response.body = `Note ${request.event.data.old.id} deleted, with data: ${request.event.data.old.note}`;
}
callback(null, response);
};

View File

@ -0,0 +1,2 @@
node_modules
*.zip

View File

@ -0,0 +1,40 @@
# Setup tables
1. Create the table using the console:
```
Table name: notes
Columns:
id: Integer auto-increment
note: Text
Table name: note_revision
Columns:
id: Integer auto-increment
note: Text
note_id: Integer (foreign key to notes.id)
update_at: Timestamp, default `now()`
```
# Setup AWS Lambda
Create a lambda function in AWS. This will be our webhook.
1. In this folder, run `npm install`
2. Then create a zip: `zip -r hge-mutation.zip .`
3. Create a Lambda function.
4. Select Node.js 6 as the runtime.
5. Select "start from scratch".
6. Add API gateway as a trigger.
7. Add an API to API gateway.
8. Upload the zip from previous step. The handler function of your lambda will be `index.handler`.
9. Add the following enviroment variables in your lambda config:
1. `ACCESS_KEY`: this is the access key you configured when you setup HGE.
2. `HGE_ENDPOINT`: the URL on which you HGE instance is running.
# Add the trigger in Hasura GraphQL
1. In events tab, add a trigger
2. Select all insert, update, delete operations for the trigger.
3. Paste the API endpoint of your AWS lambda as the webhook.

View File

@ -0,0 +1,44 @@
// Lambda which gets triggered on insert, and in turns performs a mutation
const fetch = require('node-fetch');
const accessKey = process.env.ACCESS_KEY;
const hgeEndpoint = process.env.HGE_ENDPOINT;
const query = `
mutation updateNoteRevision ($noteId: Int!, $data: String!) {
insert_note_revision (objects: [
{
note_id: $noteId,
note: $data
}
]) {
affected_rows
}
}
`;
exports.handler = (event, context, callback) => {
let request;
try {
request = JSON.parse(event.body);
} catch (e) {
return callback(null, {statusCode: 400, body: "cannot parse hasura event"});
}
const response = {
statusCode: 200,
body: "success"
};
const qv = {noteId: request.event.data.old.id, data: request.event.data.old.note};
fetch(hgeEndpoint + '/v1alpha1/graphql', {
method: 'POST',
body: JSON.stringify({query: query, variables: qv}),
headers: {'Content-Type': 'application/json', 'x-hasura-access-key': accessKey},
})
.then(res => res.json())
.then(json => {
console.log(json);
callback(null, response);
});
};

View File

@ -0,0 +1,13 @@
{
"name": "mutation-trigger",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"node-fetch": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.2.0.tgz",
"integrity": "sha512-OayFWziIxiHY8bCUyLX6sTpDH8Jsbp4FfYd1j1f7vZyfgkcOnAyM4oQR16f8a0s7Gl/viMGRey8eScYk4V4EZA=="
}
}
}

View File

@ -0,0 +1,14 @@
{
"name": "mutation-trigger",
"version": "1.0.0",
"description": " sample lambda to react to database updates",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"node-fetch": "^2.2.0"
}
}

View File

@ -0,0 +1,23 @@
# Setup tables
1. Create table:
```
notes:
id: int
note: text
```
# Setup AWS Lambda
Create a lambda function in AWS. This will be our webhook.
1. Create a function.
2. Select Python 3.6 as the runtime.
3. Select "start from scratch".
4. Add API gateway as a trigger.
5. Add an API to API gateway.
6. Add the code in `echo.py`. The handler function of your lambda will be the `echo.lambda_handler`.
# Add the trigger in Hasura GraphQL
1. In events tab, add a trigger
2. Select all insert, update, delete operations for the trigger.
3. Paste the API endpoint of your AWS lambda as the webhook.

View File

@ -0,0 +1,26 @@
import json
def lambda_handler(event, context):
try:
body = json.loads(event['body'])
except:
return {
"statusCode": 400,
"body": json.dumps({'message': 'Unable to parse hasura event'})
}
message = 'Not able to process request'
data = body['event']['data']
if body['table']['name'] == 'notes' and body['event']['op'] == 'INSERT':
message = 'New note {} inserted, with data: {}'.format(data['new']['id'], data['new']['note'])
elif body['table']['name'] == 'notes' and body['event']['op'] == 'UPDATE':
message = 'Note {} updated, with data: {}'.format(data['new']['id'], data['new']['note'])
elif body['table'] == 'notes' and body['op'] == 'DELETE':
message = 'Note {} deleted, with data: {}'.format(data['old']['id'], data['old']['note'])
return {
"statusCode": 200,
"body": json.dumps({'message': message})
}

View File

@ -0,0 +1,36 @@
# Setup tables
1. Create the following tables using the console:
```
Table name: notes
Columns:
id: Integer auto-increment
note: Text
Table name: note_revision
Columns:
id: Integer auto-increment
note: Text
note_id: Integer (foreign key to notes.id)
update_at: Timestamp, default `now()`
```
# Setup AWS Lambda
Create a lambda function in AWS. This will be our webhook.
1. Create a function.
2. Select Python 3.6 as the runtime.
3. Select "start from scratch".
4. Add API gateway as a trigger.
5. Add an API to API gateway.
6. Add the code in `mutation.py`. The handler function of your lambda will be the `mutation.lambda_handler`.
7. Add the following enviroment variables in your lambda config:
1. `ACCESS_KEY`: this is the access key you configured when you setup HGE.
2. `HGE_ENDPOINT`: the URL on which you HGE instance is running.
# Add the trigger in Hasura GraphQL
1. In events tab, add a trigger
2. Select all insert, update, delete operations for the trigger.
3. Paste the API endpoint of your AWS lambda as the webhook.

View File

@ -0,0 +1,45 @@
import os
import json
from botocore.vendored import requests
ACCESS_KEY = os.environ['ACCESS_KEY']
HGE_ENDPOINT = os.environ['HGE_ENDPOINT']
HGE_URL = HGE_ENDPOINT + '/v1alpha1/graphql'
HEADERS = {
'Content-Type': 'application/json',
'X-Hasura-Access-Key': ACCESS_KEY,
}
query = """
mutation updateNoteRevision ($noteId: Int!, $data: String!) {
insert_note_revision (objects: [
{
note_id: $noteId,
note: $data
}
]) {
affected_rows
}
}
"""
def lambda_handler(event, context):
try:
body = json.loads(event['body'])
except:
return {
"statusCode": 400,
"body": json.dumps({'message': 'Unable to parse request body'})
}
data = body['event']['data']
qv = {'noteId': data['old']['id'], 'data': data['old']['note']}
jsonBody = {'query': query, 'variables': qv}
resp = requests.post(HGE_URL, data=json.dumps(jsonBody), headers=HEADERS)
my_json = resp.json()
print(my_json)
return {
"statusCode": 200,
"body": json.dumps({'message': 'success'})
}

View File

@ -0,0 +1,17 @@
# Boilerplates for Azure Cloud Function serverless functions and Hasura GraphQL Engine's Event Triggers
**NOTE**
Some of the language/platforms are work in progress. We welcome contributions for the WIP langauages. See issues and the following checklist:
| Folder name | Use-case| Javascript | Java | C# | F#
|-------------|---------|:--------:|:------:|:----:|:---:
| echo | echo the trigger payload | ✅ | ❌ | ❌ | ❌
| mutation | insert related data on an insert event using graphql mutation | ✅ | ❌ | ❌ | ❌
| push-notification | send push notification on database event | ❌ | ❌ | ❌ | ❌
| etl | transform the trigger payload and update an algolia index | ❌ | ❌ | ❌ | ❌
## Pre-requisites
1. Running instance of Hasura GraphQL
2. You already have a Azure account with billing enabled.
3. Install [azure-cli](https://github.com/Azure/azure-cli)
4. Install [azure-functions-core-tools](https://github.com/Azure/azure-functions-core-tools)

View File

@ -0,0 +1,25 @@
bin
obj
csx
.vs
edge
Publish
*.user
*.suo
*.cscfg
*.Cache
project.lock.json
/packages
/TestResults
/tools/NuGet.exe
/App_Data
/secrets
/data
.secrets
appsettings.json
local.settings.json
node_modules

View File

@ -0,0 +1,5 @@
{
"recommendations": [
"ms-azuretools.vscode-azurefunctions"
]
}

View File

@ -0,0 +1,20 @@
{
"disabled": false,
"bindings": [
{
"authLevel": "anonymous",
"type": "httpTrigger",
"direction": "in",
"name": "req",
"methods": [
"get",
"post"
]
},
{
"type": "http",
"direction": "out",
"name": "res"
}
]
}

View File

@ -0,0 +1,17 @@
module.exports = function (context, req) {
context.log('JavaScript HTTP trigger function processed a request.');
if (req.body && req.body) {
context.res = {
// status: 200, /* Defaults to 200 */
body: req.body
};
}
else {
context.res = {
status: 400,
body: "Please pass a request body"
};
}
context.done();
};

View File

@ -0,0 +1,3 @@
{
"name": "Azure"
}

View File

@ -0,0 +1,35 @@
# Setup tables
1. Create table:
```
notes:
id: int
note: text
```
# Setup Cloud Function
1. Run the following commands to deploy:
```bash
az group create --name 'my-functions-group' --location southindia
az storage account create --name 'myfunctionsstorage' --location southindia --resource-group 'my-functions-group' --sku Standard_LRS
az functionapp create --name 'myfunctionsapp' --storage-account 'myfunctionsstorage' --resource-group 'my-functions-group' --consumption-plan-location southindia
func azure login
func azure subscriptions set 'Free Trial'
func azure functionapp publish 'myfunctionsapp'
```
2. Set Environment variables `ACCESS_KEY` and `HGE_ENDPOINT`
3. Add a X-Function-Key header if Authorization level is enabled
# Running locally
`func host start`
# Check Logs
`func azure functionapp logstream 'myfunctionsapp'`
# Add the trigger in Hasura GraphQL
1. In events tab, add a trigger
2. Select all insert, update, delete operations for the trigger.
3. Paste your function URL as the webhook.

View File

@ -0,0 +1,11 @@
{
"functions": [ "HTTPTrigger" ],
"id": "3adb8c2ca78f4171bab74dfc9c600a2f",
"functionTimeout": "00:00:30",
"http": {
"routePrefix": ""
},
"tracing": {
"consoleLevel": "verbose"
}
}

View File

@ -0,0 +1,25 @@
bin
obj
csx
.vs
edge
Publish
*.user
*.suo
*.cscfg
*.Cache
project.lock.json
/packages
/TestResults
/tools/NuGet.exe
/App_Data
/secrets
/data
.secrets
appsettings.json
local.settings.json
node_modules

View File

@ -0,0 +1,5 @@
{
"recommendations": [
"ms-azuretools.vscode-azurefunctions"
]
}

View File

@ -0,0 +1,20 @@
{
"disabled": false,
"bindings": [
{
"authLevel": "anonymous",
"type": "httpTrigger",
"direction": "in",
"name": "req",
"methods": [
"get",
"post"
]
},
{
"type": "http",
"direction": "out",
"name": "res"
}
]
}

View File

@ -0,0 +1,61 @@
const { query } = require('graphqurl');
const ACCESS_KEY = process.env.ACCESS_KEY;
const HGE_ENDPOINT = process.env.HGE_ENDPOINT;
const MUTATION_NOTE_REVISION = `
mutation updateNoteRevision ($noteId: Int!, $data: String!) {
insert_note_revision (objects: [
{
note_id: $noteId,
note: $data
}
]) {
affected_rows
}
}
`;
module.exports = function (context, req) {
context.log('JavaScript HTTP trigger function processed a request.');
try {
context.log('Inside');
const { event: {op, data}, table } = req.body;
context.log(data);
context.log(data.new.id);
const qv = {noteId: data.old.id, data: data.old.note};
const hgeResponse = query({
query: MUTATION_NOTE_REVISION,
endpoint: HGE_ENDPOINT + '/v1alpha1/graphql',
variables: qv,
headers: {
'x-hasura-access-key': ACCESS_KEY
}
}).then((response) => {
context.log(response);
context.log('After query');
context.res = {
body: {
error: false,
data: response
}
};
context.done();
}).catch((error) => {
console.error(JSON.stringify(error));
context.res = {
status: 500,
body: {
error: true,
data: JSON.stringify(error)
}
};
context.done();
});
} catch(e) {
context.res = {
status: 400,
body: "An error occured."
};
context.done();
}
};

View File

@ -0,0 +1,15 @@
{
"name": "cloudfunction",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"graphqurl": "^0.3.1",
"node-fetch": "^2.2.0"
}
}

View File

@ -0,0 +1,47 @@
# Setup tables
1. Create table:
```
Table name: notes
Columns:
id: Integer auto-increment
note: Text
Table name: note_revision
Columns:
id: Integer auto-increment
note: Text
note_id: Integer (foreign key to notes.id)
update_at: Timestamp, default `now()`
```
# Setup Cloud Function
1. Run the following commands to deploy:
```bash
az group create --name 'my-functions-group' --location southindia
az storage account create --name 'myfunctionsstorage' --location southindia --resource-group 'my-functions-group' --sku Standard_LRS
az functionapp create --name 'myfunctionsapp' --storage-account 'myfunctionsstorage' --resource-group 'my-functions-group' --consumption-plan-location southindia
func azure login
func azure subscriptions set 'Free Trial'
func azure functionapp publish 'myfunctionsapp'
```
2. Set Environment variables `ACCESS_KEY` and `HGE_ENDPOINT`
3. Add a X-Function-Key header if Authorization level is enabled
# Running locally
`func host start`
# Check Logs
`func azure functionapp logstream 'myfunctionsapp'`
# Add the trigger in Hasura GraphQL
1. In events tab, add a trigger
2. Select all insert, update, delete operations for the trigger.
3. Paste your function URL as the webhook.

View File

@ -0,0 +1,14 @@
{
"functions": [ "HTTPTrigger" ],
"id": "3adb8c2ca78f4171bab74dfc9c600a2a",
"functionTimeout": "00:00:30",
"extensions": {
"http": {
"routePrefix": ""
}
},
"logging": {
"consoleLevel": "verbose"
},
"version": "2.0"
}

View File

@ -0,0 +1,25 @@
# Boilerplates for Google Cloud Functions and Hasura GraphQL Engine's Event Triggers
Sample cloud functions that can be triggered on changes in the database using GraphQL Engine's Event Triggers
**NOTE**
Some of the language/platforms are work in progress. We welcome contributions for the WIP langauages. See issues and the following checklist:
| Folder name | Use-case| Node.js(8) | Node.js(6) | Python
|-------------|---------|:--------:|:------:|:----:
| echo | echo the trigger payload | ✅ | ❌ | ❌
| mutation | insert related data on an insert event using graphql mutation | ✅ | ❌ | ❌
| push-notification | send push notification on database event | ❌ | ❌ | ❌
| etl | transform the trigger payload and update an algolia index | ❌ | ❌ | ❌
## Pre-requisites
1. Google cloud account with billing enabled
2. `gcloud` CLI
3. Hasura GraphQL Engine
Get the `gcloud beta` component:
```bash
gcloud components update && gcloud components install beta
```

View File

@ -0,0 +1,20 @@
# This file specifies files that are *not* uploaded to Google Cloud Platform
# using gcloud. It follows the same syntax as .gitignore, with the addition of
# "#!include" directives (which insert the entries of the given .gitignore-style
# file at that point).
#
# For more information, run:
# $ gcloud topic gcloudignore
#
.gcloudignore
# If you would like to upload your .git directory, .gitignore file or files
# from your .gitignore file, remove the corresponding line
# below:
.git
.gitignore
node_modules
.env.yaml
.prod.env.yaml

View File

@ -0,0 +1,2 @@
node_modules
.prod.env.yaml

View File

@ -0,0 +1,41 @@
# Setup table
Create the table using the console:
```
Table name: profile
Columns:
id: Integer auto-increment
name: Text
address: Text
lat: Numeric, Nullable
lng: Numeric, Nullable
```
# Deploy Google Cloud function
Deploy the function:
```bash
gcloud beta functions deploy nodejs-echo \
--runtime nodejs8 \
--trigger-http
```
Get the trigger URL:
```yaml
httpsTrigger:
url: https://us-central1-hasura-test.cloudfunctions.net/nodejs-echo
```
Open Hasura console, goto `Events -> Add Trigger` and create a new trigger:
```
Trigger name: profile_change
Schema/Table: public/profile
Operations: Insert, Update, Delete
Webhook URL: [Trigger URL]
```
Once the trigger is created, goto `Data -> profile -> Insert row` and add a row.

View File

@ -0,0 +1,6 @@
exports.function = (req, res) => {
const { event: {op, data}, table: {name, schema} } = req.body;
const response = {message: 'received event', data: {op, data, name, schema}};
console.log(response);
res.json(response);
};

View File

@ -0,0 +1,2 @@
GMAPS_API_KEY: '[GMAPS_API_KEY]'
HASURA_GRAPHQL_ENGINE_URL: 'http://[HGE_IP]/v1alpha1/graphql'

View File

@ -0,0 +1,19 @@
# This file specifies files that are *not* uploaded to Google Cloud Platform
# using gcloud. It follows the same syntax as .gitignore, with the addition of
# "#!include" directives (which insert the entries of the given .gitignore-style
# file at that point).
#
# For more information, run:
# $ gcloud topic gcloudignore
#
.gcloudignore
# If you would like to upload your .git directory, .gitignore file or files
# from your .gitignore file, remove the corresponding line
# below:
.git
.gitignore
node_modules
.env.yaml
.prod.env.yaml

View File

@ -0,0 +1,2 @@
node_modules
.prod.env.yaml

View File

@ -0,0 +1,71 @@
# Setup table
Create the table using the console:
```
Table name: profile
Columns:
id: Integer auto-increment
name: Text
address: Text
lat: Numeric, Nullable
lng: Numeric, Nullable
```
# Deploy Google Cloud function
We are going to use Google Maps API in our cloud function.
Enable Google Maps API and get an API key (we'll call it `GMAPS_API_KEY`) following [this
guide](https://developers.google.com/maps/documentation/geocoding/start?hl=el#auth)
Check the `Places` box to get access to Geocoding API.
We'll follow [this guide](https://cloud.google.com/functions/docs/quickstart)
and create a Cloud Function with NodeJS 8.
```bash
gcloud components update &&
gcloud components install beta
```
Goto the `cloudfunction` directory:
```bash
cd cloudfunction
```
Edit `.env.yaml` and add values for the following as shown:
```yaml
# .env.yaml
GMAPS_API_KEY: '[GMAPS_API_KEY]'
HASURA_GRAPHQL_ENGINE_URL: 'http://[HGE_IP]/v1alpha1/graphql'
```
```bash
gcloud beta functions deploy trigger \
--runtime nodejs8 \
--trigger-http \
--region asia-south1 \
--env-vars-file .env.yaml
```
Get the trigger URL:
```yaml
httpsTrigger:
url: https://asia-south1-hasura-test.cloudfunctions.net/trigger
```
Goto `HGE_IP` on browser, `Events -> Add Trigger` and create a new trigger:
```
Trigger name: profile_change
Schema/Table: public/profile
Operations: Insert
Webhook URL: [Trigger URL]
```
Once the trigger is created, goto `Data -> profile -> Insert row` and add a new
profile with name and address, save. Goto `Browse rows` tabs to see lat and lng
updated, by the cloud function.

View File

@ -0,0 +1,46 @@
const fetch = require('node-fetch');
const { query } = require('graphqurl');
const MUTATION_UPDATE_LAT_LONG = `
mutation updateLatLong($id: Int!, $lat: numeric! $lng: numeric!) {
update_profile(
where: {id: {_eq: $id}},
_set: {
lat: $lat,
lng: $lng
}
) {
affected_rows
returning {
id
lat
lng
}
}
}`;
exports.trigger = async (req, res) => {
const GMAPS_API_KEY = process.env.GMAPS_API_KEY;
const HASURA_GRAPHQL_ENGINE_URL = process.env.HASURA_GRAPHQL_ENGINE_URL;
const { event: {op, data}, table } = req.body;
if (op === 'INSERT' && table.name === 'profile' && table.schema === 'public') {
const { id, address } = data.new;
const gmaps = await fetch(`https://maps.googleapis.com/maps/api/geocode/json?address=${address}&key=${GMAPS_API_KEY}`);
const gmapsResponse = await gmaps.json();
if (gmapsResponse.results && gmapsResponse.results.length === 0) {
res.json({error: true, data: gmapsResponse, message: `no results for address ${address}`});
return;
}
const { lat, lng } = gmapsResponse.results[0].geometry.location;
const hgeResponse = await query({
query: MUTATION_UPDATE_LAT_LONG,
endpoint: HASURA_GRAPHQL_ENGINE_URL,
variables: { id, lat, lng },
});
res.json({error: false, data: hgeResponse.data});
} else {
res.json({error: false, message: 'ignored event'});
}
};

View File

@ -0,0 +1,15 @@
{
"name": "cloudfunction",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"graphqurl": "^0.3.1",
"node-fetch": "^2.2.0"
}
}

View File

@ -0,0 +1 @@
node_modules

View File

@ -0,0 +1,13 @@
# Boilerplates for Zeit Serverless Docker functions and Hasura GraphQL Engine's Event Triggers
**NOTE**
Some of the language/platforms are work in progress. We welcome contributions for the WIP langauages. See issues.
# Pre-requisite:
1. Running instance of Hasura GraphQL engine
# Setup `now` cli
1. Create zeit account @ https://zeit.co/
2. Download now cli from https://zeit.co/download#now-cli
3. Login using now Login

View File

@ -0,0 +1,4 @@
*
!package.json
!package-lock.json
!index.js

View File

@ -0,0 +1,5 @@
FROM node:10-alpine
COPY package.json package-lock.json ./
RUN npm ci
COPY index.js .
CMD ["node", "node_modules/.bin/micro"]

View File

@ -0,0 +1,22 @@
# Setup our tables
Create table using the console:
```
Table name: notes
Columns:
id: Integer auto-increment
note: Text
```
# Setup echo serverless function
1. cd echo
2. edit now.json to change app and alias name (Note: alias will be the domain for example setting alias to hge-events-zeit-node-echo will provide hge-events-zeit-node-echo.now.sh)
3. Deploy the function by running `now && now alias && now remove <app-name> --safe -y`
# Add events in Hasura GraphQL
1. In events tab, add a trigger
2. Select all insert, update, delete operations for the trigger.
3. Paste the API endpoint as the webhook.

View File

@ -0,0 +1,21 @@
const {json, send} = require('micro');
module.exports = async (req, res) => {
let js;
try {
js = await json(req);
} catch (err) {
send(res, 400, {'error': err.message});
}
let message = 'Not able to process request';
if (js.event.op == 'INSERT' && js.table.name == 'notes') {
message = `New note ${js.event.data.new.id} inserted, with data: ${js.event.data.new.note}`;
} else if (js.event.op == 'UPDATE' && js.table.name == 'notes') {
message = `note ${js.event.data.new.id} updated, with data: ${js.event.data.new.note}`;
} else if (js.event.op == 'DELETE' && js.table.name == 'notes') {
message = `New note ${js.event.data.old.id} deleted, with data: ${js.event.data.old.note}`;
}
send(res, 200, {'message': message});
};

View File

@ -0,0 +1,9 @@
{
"public": true,
"type": "docker",
"features": {
"cloud": "v2"
},
"alias": "hge-events-zeit-node-echo",
"name": "hge-events-zeit-node-echo"
}

View File

@ -0,0 +1,145 @@
{
"name": "hge-events-zeit-node",
"requires": true,
"lockfileVersion": 1,
"dependencies": {
"ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"requires": {
"color-convert": "^1.9.0"
}
},
"arg": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/arg/-/arg-2.0.0.tgz",
"integrity": "sha512-XxNTUzKnz1ctK3ZIcI2XUPlD96wbHP2nGqkPKpvk/HNRlPveYrXIVSTk9m3LcqOgDPg3B1nMvdV/K8wZd7PG4w=="
},
"bytes": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
"integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg="
},
"chalk": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.0.tgz",
"integrity": "sha512-Wr/w0f4o9LuE7K53cD0qmbAMM+2XNLzR29vFn5hqko4sxGlUsyy363NvmyGIyk5tpe9cjTr9SJYbysEyPkRnFw==",
"requires": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
"supports-color": "^5.3.0"
}
},
"color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"requires": {
"color-name": "1.1.3"
}
},
"color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
},
"content-type": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
"integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="
},
"depd": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz",
"integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k="
},
"escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
},
"has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0="
},
"http-errors": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz",
"integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=",
"requires": {
"depd": "1.1.1",
"inherits": "2.0.3",
"setprototypeof": "1.0.3",
"statuses": ">= 1.3.1 < 2"
}
},
"iconv-lite": {
"version": "0.4.19",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz",
"integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ=="
},
"inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
},
"is-stream": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
"integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ="
},
"micro": {
"version": "9.3.3",
"resolved": "https://registry.npmjs.org/micro/-/micro-9.3.3.tgz",
"integrity": "sha512-GbCp4NFQguARch0odX+BuWDja2Kc1pbYZqWfRvEDihGFTJG8U77C0L+Owg2j7TPyhQ5Tc+7z/SxspRqjdiZCjQ==",
"requires": {
"arg": "2.0.0",
"chalk": "2.4.0",
"content-type": "1.0.4",
"is-stream": "1.1.0",
"raw-body": "2.3.2"
}
},
"node-fetch": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.2.0.tgz",
"integrity": "sha512-OayFWziIxiHY8bCUyLX6sTpDH8Jsbp4FfYd1j1f7vZyfgkcOnAyM4oQR16f8a0s7Gl/viMGRey8eScYk4V4EZA=="
},
"raw-body": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz",
"integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=",
"requires": {
"bytes": "3.0.0",
"http-errors": "1.6.2",
"iconv-lite": "0.4.19",
"unpipe": "1.0.0"
}
},
"setprototypeof": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz",
"integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ="
},
"statuses": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
},
"supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"requires": {
"has-flag": "^3.0.0"
}
},
"unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
}
}
}

View File

@ -0,0 +1,8 @@
{
"name": "hge-events-zeit-node-echo",
"dependencies": {
"micro": "9.3.3"
},
"license": "MIT",
"main": "./index.js"
}

View File

@ -0,0 +1,4 @@
*
!package.json
!package-lock.json
!index.js

View File

@ -0,0 +1,5 @@
FROM node:10-alpine
COPY package.json package-lock.json ./
RUN npm ci
COPY index.js .
CMD ["node", "node_modules/.bin/micro"]

View File

@ -0,0 +1,30 @@
# Setup our tables
Create table using the console:
```
Table name: notes
Columns:
id: Integer auto-increment
note: Text
Table name: note_revision
Columns:
id: Integer auto-increment
note: Text
note_id: Integer (foreign key to notes.id)
update_at: Timestamp, default `now()`
```
# Setup echo serverless function
1. cd echo
2. edit now.json to change app and alias name (Note: alias will be the domain for example setting alias to hge-events-zeit-node-echo will provide hge-events-zeit-node-echo.now.sh)
3. Deploy the function by running `now && now alias && now remove <app-name> --safe -y`
# Add events in Hasura GraphQL
1. In events tab, add a trigger
2. Select all insert, update, delete operations for the trigger.
3. Paste the API endpoint as the webhook.

View File

@ -0,0 +1,43 @@
const {json, send} = require('micro');
const { query } = require('graphqurl');
const accessKey = process.env.ACCESS_KEY;
const hgeEndpoint = process.env.HGE_ENDPOINT + '/v1alpha1/graphql';
const q = `
mutation updateNoteRevision ($noteId: Int!, $data: String!) {
insert_note_revisions (objects: [
{
note_id: $noteId,
updated_note: $data
}
]) {
affected_rows
}
}
`;
module.exports = async (req, res) => {
let js;
try {
js = await json(req);
} catch (err) {
send(res, 400, {'error': err.message});
}
query(
{
query: q,
endpoint: hgeEndpoint,
variables: {noteId: js.event.data.old.id, data: js.event.data.old.note},
headers: {
'x-hasura-access-key': accessKey,
'Content-Type': 'application/json'
}
}
).then((response) => {
console.log(response);
send(res, 200, {'message': response});
}).catch((error) => {
send(res, 400, {'message': error});
});
};

View File

@ -0,0 +1,9 @@
{
"public": true,
"type": "docker",
"features": {
"cloud": "v2"
},
"alias": "hge-events-zeit-node-mutation",
"name": "hge-events-zeit-node-mutation"
}

View File

@ -0,0 +1,9 @@
{
"name": "hge-events-zeit-node-echo",
"dependencies": {
"graphqurl": "^0.3.1",
"micro": "9.3.3"
},
"license": "MIT",
"main": "./index.js"
}

View File

@ -0,0 +1,21 @@
# See https://help.github.com/ignore-files/ for more about ignoring files.
# dependencies
/node_modules
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

View File

@ -0,0 +1,9 @@
# Realtime Chat using GraphQL Subscriptions
This is the source code for a fully working group chat app that uses subscriptions in Hasura GraphQL Engine. It is built using React and Apollo.
- [Fully working app](https://chat-example-trial-roar.herokuapp.com)
- [Backend](https://hasura-realtime-group-chat.herokuapp.com)
For complete tutorial about data modelling, [check out this blog](https://medium.com/@rishichandrawawhal/building-a-realtime-chat-app-with-graphql-subscriptions-d68cd33e73f).

View File

@ -0,0 +1,33 @@
{
"name": "react-graphql-boilerplate",
"version": "0.1.0",
"private": true,
"dependencies": {
"apollo-boost": "^0.1.10",
"apollo-client-preset": "^1.0.8",
"apollo-link-schema": "^1.1.0",
"apollo-link-ws": "^1.0.8",
"apollo-utilities": "^1.0.16",
"graphql": "^0.13.2",
"graphql-tag": "^2.9.2",
"graphql-tools": "^3.0.5",
"moment": "^2.22.2",
"react": "^16.4.1",
"react-apollo": "^2.1.9",
"react-dom": "^16.4.1",
"react-router-dom": "^4.3.1",
"react-scripts": "^1.1.1",
"subscriptions-transport-ws": "^0.9.12"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
},
"devDependencies": {
"eslint-plugin-graphql": "^1.5.0",
"eslint": "^5.1.0",
"eslint-plugin-react": "^7.10.0"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -0,0 +1,40 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<!--
manifest.json provides metadata used when your web app is added to the
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

View File

@ -0,0 +1,15 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
}
],
"start_url": "./index.html",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@ -0,0 +1,265 @@
@import url('https://fonts.googleapis.com/css?family=Raleway:400,600');
@import url('https://fonts.googleapis.com/css?family=Open+Sans:300,400');
body
{
font-family: 'Open Sans';
font-size: 16px;
margin: 0;
}
.app {
text-align: left;
}
.messageOdd {
font-size: 18px;
padding-left: 5px;
}
.message {
font-size: 16px;
background-color: #fff;
padding-left: 5px;
margin: 20px;
border-radius: 5px;
width: 50%;
padding: 5px;
}
.selfMessage {
font-size: 16px;
background-color: #eee;
padding-left: 5px;
margin: 20px;
border-radius: 5px;
width: 50%;
padding: 5px;
float: right;
}
.newMessageEven {
font-size: 18px;
background-color: #98FB98;
padding-left: 5px;
}
.newMessageOdd {
font-size: 18px;
background-color: #8FBC8F;
padding-left: 5px;
}
.messageWrapperNew
{
padding-bottom: 75px;
}
.banner {
position: -webkit-sticky;
position: sticky;
top: 0;
align-self: flex-start;
background-color: #20c40f;
font-size: 18px;
cursor: pointer;
font-weight: 400;
padding: 10px 0;
text-align: center;
font-family: 'raleway';
color: #fff;
}
.oldNewSeparator {
font-size: 18px;
text-align: center;
margin-bottom: 15px;
}
#chatbox {
overflow: auto;
height: 100vh;
background-color: #f8f9f9;
}
.textboxWrapper {
text-align: center;
position: fixed;
bottom: 0;
width: 75%;
background-color: #fff;
padding: 1%;
}
.textbox {
}
.sendButton {
width: 20%;
background-color:'green';
}
.login {
text-align: center;
}
.loginTextbox {
font-size: 16px;
height: 43px;
width: 74%;
margin-right: 1%;
font-weight: 300;
border: 1px solid #ececec;
border-radius: 5px;
padding: 0;
padding-left: 10px;
display: inline-block;
}
.typoTextbox
{
font-size: 16px;
height: 43px;
width: 75%;
margin-right: 1%;
font-weight: 300;
border: 1px solid #ececec;
border-radius: 5px;
padding: 0;
padding-left: 10px;
display: inline-block;
background-color: #f6f6f7;
}
.loginTextbox:focus, .typoTextbox:focus
{
outline: none;
border-color: #016d95;
}
.typoTextbox:focus
{
outline: none;
border-color: #bbbdbd;
}
.loginButton {
height: 45px;
width: 21%;
display: inline-block;
border-radius: 5px;
background-color: 'green' !important;
cursor: pointer;
font-size: 16px;
margin-right: 1%;
}
.typoButton
{
height: 45px;
width: 20%;
display: inline-block;
border-radius: 5px;
background-color: #ffca27;
cursor: pointer;
font-size: 16px;
margin-right: 1%;
border: 0;
color: #222;
}
.typoButton:hover
{
background-color: #dba203;
}
.loginButton:focus, .typoButton:focus
{
outline: none;
}
.loginButton:hover
{
background-color: #f6f6f6;
}
.loginHeading {
text-align: center;
font-family: 'Raleway';
margin-top: 0;
padding-bottom: 20px;
}
.loginWrapper
{
width: 450px;
padding: 30px;
margin: 0 auto;
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
border: 1px solid #ececec;
border-radius: 5px;
background-color: #f6f6f6;
}
.errorMessage {
text-align: center;
color: 'red';
padding-bottom: 10px;
}
.wd25 {
width: 25%;
display: inline-block;
}
.wd75 {
width: 75%;
display: inline-block;
}
.onlineUsers {
background-color: #4f5050;
height: 100vh;
overflow: auto;
}
.messageName, .messsageTime
{
width: 49%;
display: inline-block;
}
.messageName
{
color: #1d5d01;
}
.messsageTime
{
text-align: right;
padding-right: 5px;
font-size: 12px;
color: #01999b;
}
.userList {
margin-top: 0;
margin-bottom: 0;
-webkit-padding-start: 0px;
}
.userList li {
padding: 10px;
border-bottom: 1px solid #444;
color: #fff;
}
.chatWrapper {
display: flex;
height: 100vh;
}
.userListHeading {
font-weight: 600;
padding: 15px 10px;
margin-top: 0;
margin-bottom: 0;
background-color: #222;
color: #fff;
}
.typingIndicator {
text-align: left;
padding-bottom: 10px;
padding-left: 1%;
}

View File

@ -0,0 +1,9 @@
import React from 'react';
import Main from './components/Main';
import './App.css' ;
const App = () => {
return <div className="app"> <Main/> </div>;
}
export default App;

View File

@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<App />, div);
ReactDOM.unmountComponentAtNode(div);
});

Some files were not shown because too many files have changed in this diff Show More