Skip to content
Snippets Groups Projects
Commit e51c0743 authored by Emmanuel Salomon's avatar Emmanuel Salomon :fist:
Browse files

First commit :seedling:

parents
No related branches found
No related tags found
No related merge requests found
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# 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
*.lcov
# 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/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# 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 / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and *not* Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
\ No newline at end of file
# 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 (http://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
# next.js build output
.next
# 0x
.__browserify_string_empty.js
profile-*
# JetBrains IntelliJ IDEA
.idea/
*.iml
# VS Code
.vscode/
# lock files
package-lock.json
yarn.lock
.travis.yml
.editorconfig
.gitattributes
.gitignore
.github
\ No newline at end of file
LICENSE 0 → 100644
MIT License
Copyright (c) 2021 Emmanuel Salomon (ManUtopiK)
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.
\ No newline at end of file
README.md 0 → 100644
![fastify-hasura.png](https://i.postimg.cc/6qhHHw8K/fastify-hasura.png)
# fastify-hasura
[![NPM version](https://img.shields.io/npm/v/fastify-hasura.svg?style=flat)](https://www.npmjs.com/package/fastify-hasura)
[![Coverage Status](https://coveralls.io/repos/github/ManUtopiK/fastify-hasura/badge.svg?branch=master)](https://coveralls.io/github/ManUtopiK/fastify-hasura?branch=master)
A [Fastify](https://github.com/fastify/fastify) plugin to have fun with [Hasura](https://github.com/hasura/graphql-engine).
## Features
- Fastify decorator over [graphql-request](https://github.com/prisma/graphql-request) to easily request Hasura graphql endpoint.
- Provide routes for Hasura events, actions and cron jobs.
- Secure requests coming from Hasura.
- Easily register handler for Hasura events, actions and cron jobs.
## Install
1. Install fastify-hasura with:
```sh
yarn add fastify-hasura # or npm i --save fastify-hasura
```
2. Register the plugin:
```js
fastify.register(require('fastify-hasura'), {
endpoint: 'yourHasuraGraphqlEndpoint',
admin_secret: 'yourAdminSecret'
})
```
## Usage
**Example request on Hasura Graphql Endpoint**
```js
const userId = 'yourUserUUID'
const fetchUser = `#graphql
query fetchUser($id: uuid!) {
user: user_by_pk(id: $id) {
password
}
}
`
const { user } = await fastify.hasura.graphql(fetchUser, {
id: userId
})
```
**Example registering event and action:**
```js
// Register new_user hasura event
fastify.hasura.registerEvent('new_user', (request, reply) => {
const user = request.event.getNewData()
console.log(user)
})
// Register login hasura action
fastify.hasura.registerAction('login', async (request, reply) => {
const data = request.action.getData('id', 'type', 'user')
console.log(data)
const response = await yourAsyncCustomBusinessLogic(data)
reply.send(response)
})
```
_**Note:** Requests for events and actions are decorated with [hasura-parser](https://github.com/resultdoo/hasura-parser). So, you can easily retrieve data in routes with `request.event` and `request.action`._
### Options
- `endpoint` **[ required ]**: Your Hasura Graphql Endpoint.
- `admin_secret` **[ required ]**: Your Hasura admin secret.
- `api_secret` **[ optional ]**: _Highly recommended._ Provide an api secret if you want to secure requests from your Hasura instance to your Fastify app. You must configure `x-hasura-from-env` headers of all Hasura events, actions and cron jobs with this api secret.
- `routes_prefix` **[ optional ]**: By default, this plugin build root routes for `/events`, `/actions` and `/crons`. Use this option if you want to prefix this routes. Eg: `/hasura` will build routes `/hasura/events` and so on...
**All options:**
```js
fastify.register(require('fastify-hasura'), {
endpoint: 'yourHasuraGraphqlEndpoint',
admin_secret: 'yourAdminSecret',
api_secret: 'yourApiSecret',
routes_prefix: '/hasura'
})
```
## More documentation
- [Hasura GraphQL Engine Documentation](https://hasura.io/docs/latest/graphql/core/index.html)
- [graphql-request](https://www.npmjs.com/package/graphql-request)
- [hasura-parser](https://github.com/resultdoo/hasura-parser)
## Contributions
If you would like to make any contribution you are welcome to do so.
## License
Licensed under [MIT](https://github.com/ManUtopiK/fastify-hasura/blob/master/LICENSE)
@api = http://localhost:{{$dotenv PORT}}
### test enpoint
GET http://localhost:8080/healthz HTTP/1.1
### send mail
POST {{api}}/events HTTP/1.1
Content-Type: application/json
X-Hasura-From-Env: {{$dotenv API_SECRET}}
{
"event": {
"session_variables": {
"x-hasura-role": "admin",
"x-hasura-user-id": "ac118364-159a-4e27-bcad-ea1075595a79"
},
"op": "MANUAL",
"data": {
"old": null,
"new": {
"subject": "Test 97PX",
"status": "pending",
"createdAt": "2020-01-31T20:13:48.153043+00:00",
"data": {
"intro": "Bonjour M. Xavier\nJe vous remercie pour votre prise de contact avec l'agence 97PX, je vois que vous avez tenté de déposer quelques clichés sur le serveur. Pour que nous puissions valider les photos, il faudrait déposer des fichiers HD ( nous ne proposons pas de fichier moyenne définition ), et bien insérer un titre pour la photo (la légende n'est pas indispensable). Par ailleurs, ce serait bien si vous pouviez remplir les informations de votre profil photographe. Si vous voulez échanger avec vous par téléphone pour mieux comprendre notre démarche, n'hésitez pas ( avec les limites du décalage horaire, nous sommes en Guyane)\nA bientôt\nPierre-Olivier Jay\n97PX\n05 94 31 57 97",
"name": "Xavier"
},
"to": "emmanuel.salomon@gmail.com",
"userId": "c8269a12-1927-44fd-a226-07fdec95e34d",
"id": "b7063074-a97e-45ba-98b8-ed7d853ba3ad"
}
}
},
"created_at": "2020-01-31T20:13:48.153043Z",
"id": "67d7365f-97dc-463f-a3ee-6c290651f94f",
"delivery_info": {
"max_retries": 0,
"current_retry": 0
},
"trigger": {
"name": "send_mail"
},
"table": {
"schema": "public",
"name": "mail"
}
}
{
"compilerOptions": {
"module": "es2015"
}
}
{
"name": "fastify-hasura",
"version": "0.0.1",
"description": "A fastify plugin to have fun with Hasura.",
"main": "plugin.js",
"type": "module",
"scripts": {
"test": "tap test/*.test.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/ManUtopiK/fastify-hasura.git"
},
"keywords": [
"hasura",
"graphql",
"fastify",
"fastify-plugin",
"graphql"
],
"author": "Emmanuel Salomon <emmanuel.salomon@gmail.com> (https://github.com/ManUtopiK)",
"license": "MIT",
"bugs": {
"url": "https://github.com/ManUtopiK/fastify-hasura/issues"
},
"homepage": "https://github.com/ManUtopiK/fastify-hasura#readme",
"engines": {
"node": ">=10"
},
"dependencies": {
"fastify-plugin": "^3.0.0",
"graphql-request": "^3.4.0"
},
"devDependencies": {
"tap": "^14.11.0"
}
}
plugin.js 0 → 100644
import fp from 'fastify-plugin'
import { GraphQLClient } from 'graphql-request'
import hasuraRoutes from './routes.js'
function fastifyHasura(fastify, options, done) {
// Register fastify-sensible for handling errors in routes.
fastify.register(import('fastify-sensible'))
// Check options
if (!options.endpoint) throw new Error('You must provide hasura endpoint')
if (!options.admin_secret)
throw new Error('You must provide hasura admin secret')
if (!options.routes_prefix) options.routes_prefix = ''
// Provide fake graphql Hasura endpoint for testing
// if (fastify.config.NODE_ENV === 'test' && env.HASURA_GRAPHQL_TEST_ENDPOINT)
// env.HASURA_GRAPHQL_ENDPOINT = env.HASURA_GRAPHQL_TEST_ENDPOINT
// Ugly error come from graphql-request! See here https://github.com/prisma-labs/graphql-request/blob/777cc55f3f772f5b527df4b7b4ae5f66006b30e9/src/types.ts#L29
// TODO Implement formatter ? https://github.com/mercurius-js/mercurius/blob/master/lib/errors.js
class ErrorHasura extends Error {
constructor({ message }) {
const err = message.split(': {"resp')
const { response, request } = JSON.parse(
`{"resp${err[1]}`
.replace(/\n/g, '')
.replace(/field "(.*)" not/, "field '$1' not")
)
super(err[0])
this.query = request.query
this.variables = request.variables
this.extensions = response.errors[0].extensions
this.statusCode = 200
}
}
// Create graphql client.
const graphql_client = new GraphQLClient(options.endpoint, {
headers: {
'Content-Type': 'application/json',
'x-hasura-admin-secret': options.admin_secret
}
})
fastify.decorate('hasura', {
/**
* Provide `fastify.hasura.graphql` function to make request on Hasura graphql endpoint.
* @param {String} query Graphql query in AST format. Eg: gql`query...`
* @param {Object} variables Variables passed to the query.
* @return Object Response from Hasura instance.
*/
graphql: async (query, variables) => {
try {
const hasura_data = await graphql_client.request(query, variables)
// check if we got any response back
if (hasura_data.length === 0) {
throw new Error('Invalid request')
}
return hasura_data
} catch (err) {
throw new ErrorHasura(err)
}
},
/**
* Register events
*/
events: {},
registerEvent: (event, func) => {
fastify.hasura.events[event] = func
},
/**
* Register actions
*/
actions: {},
registerAction: (action, func) => {
fastify.hasura.actions[action] = func
},
/**
* Register crons
*/
crons: {},
registerCron: (cron, func) => {
fastify.hasura.crons[cron] = func
}
})
/**
* Register routes `/events`, `/actions` and `/crons`
*/
fastify.register(hasuraRoutes, options)
done()
}
export default fp(fastifyHasura, {
fastify: '>=2.11.0',
name: 'fastify-hasura'
})
import { EventParser, ActionParser } from '@result/hasura-parser'
async function hasuraRoutes(fastify, options) {
/**
* Provide a way to protect all routes with `x-hasura-from-env` header.
* If you provide the `api_secret` key in options plugin, all routes will be protected.
* When protected, you must configure headers of all Hasura events, actions and crons to pass this hook.
*/
fastify.addHook('onRequest', async request => {
// Verify api_secret header
if (
options.api_secret &&
request.headers['x-hasura-from-env'] !== options.api_secret
) {
throw fastify.httpErrors.networkAuthenticationRequired(
"Bad header 'x-hasura-from-env'"
)
}
})
/**
* Decorate events and actions requests with hasura-parser.
* So, you can retrieve and use hasura-parser in routes with `request.event` and `request.action`.
*/
fastify.decorateRequest('event', null)
fastify.decorateRequest('action', null)
// Update properties
fastify.addHook('preHandler', (request, reply, done) => {
if (request.body.hasOwnProperty('event'))
request.event = new EventParser(request.body)
if (request.body.hasOwnProperty('action'))
request.action = new ActionParser(request.body)
done()
})
/**
* Declare events, actions and crons routes.
*/
fastify.post(`${options.routes_prefix}/events`, (request, reply) => {
const eventName = request.event.getTriggerName()
if (fastify.hasura.events[eventName])
return fastify.hasura.events[eventName](request, reply)
reply.notFound(`Event ${eventName} not registered`)
})
fastify.post(`${options.routes_prefix}/actions`, (request, reply) => {
const actionName = request.action.getActionName()
if (fastify.hasura.actions[actionName])
return fastify.hasura.actions[actionName](request, reply)
reply.notFound(`Action ${actionName} not registered`)
})
fastify.post(`${options.routes_prefix}/crons`, (request, reply) => {
const cronName = request.body.name
if (fastify.hasura.crons[cronName])
return fastify.hasura.crons[cronName](request, reply)
reply.notFound(`Cron ${cronName} not registered`)
})
}
export default hasuraRoutes
import tap from 'tap'
import axios from 'axios'
import buildFastify from '../../../app.js'
tap.test('hasura', async t => {
const fastify = buildFastify()
await fastify.ready()
// Test hasura plugin correctly loaded
t.type(
fastify.hasura.graphql,
'function',
'fastify.hasura.graphql is function'
)
// Test hasura endpoint health response
const endpoint = fastify.configHasura.HASURA_GRAPHQL_ENDPOINT
const { data } = await axios.get(endpoint.replace('v1/graphql', 'healthz'))
t.equal(data, 'OK', `${endpoint} healthy`)
// Throw nice error
try {
await fastify.hasura.graphql(
`#graphql
query test {
test
}
`
)
} catch (err) {
t.equal(err.statusCode, 400, 'Return statusCode 400')
t.equal(
err.message,
`field "test" not found in type: 'query_root'`,
'Throw nice error'
)
}
await fastify.close()
})
This diff is collapsed.
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment