This repository has been archived by the owner on Sep 3, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 147
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
docs: graphql schema stitching w neo4j-graphql-js
- Loading branch information
1 parent
2a8a8ad
commit 24beba5
Showing
22 changed files
with
8,657 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
JWT_SECRET='supersecret' | ||
NEO4J_USERNAME='neo4j' | ||
NEO4J_PASSWORD='letmein' | ||
NEO4J_PROTOCOL=neo4j | ||
NEO4J_HOST=localhost | ||
NEO4J_DATABASE=neo4j | ||
NEO4J_ENCRYPTION=ENCRYPTION_OFF |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
module.exports = { | ||
plugins: ['jest'], | ||
env: { | ||
es6: true, | ||
node: true, | ||
'jest/globals': true | ||
}, | ||
extends: 'airbnb-base', | ||
globals: { | ||
Atomics: 'readonly', | ||
SharedArrayBuffer: 'readonly' | ||
}, | ||
parserOptions: { | ||
ecmaVersion: 2018, | ||
sourceType: 'module' | ||
}, | ||
rules: { | ||
'jest/no-disabled-tests': 'warn', | ||
'jest/no-focused-tests': 'error', | ||
'jest/no-identical-title': 'error', | ||
'jest/prefer-to-have-length': 'warn', | ||
'jest/valid-expect': 'error', | ||
'import/no-extraneous-dependencies': [ | ||
'error', | ||
{ devDependencies: ['db/**/*.js', '**/*.test.js', '**/*.spec.js'] } | ||
] | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
module.exports = { | ||
presets: [['@babel/preset-env', { targets: { node: 'current' } }]] | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import { ApolloServer } from 'apollo-server'; | ||
import Server from './src/server'; | ||
|
||
const playground = { | ||
settings: { | ||
'schema.polling.enable': false | ||
} | ||
}; | ||
|
||
(async () => { | ||
const server = await Server(ApolloServer, { playground }); | ||
const { url } = await server.listen(); | ||
// eslint-disable-next-line no-console | ||
console.log(`🚀 Server ready at ${url}`); | ||
})(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
{ | ||
"name": "neo4j-graphql-js-example-schema-stitching", | ||
"version": "1.0.0", | ||
"main": "index.js", | ||
"license": "MIT", | ||
"scripts": { | ||
"lint": "eslint .", | ||
"test": "jest", | ||
"test:debug": "node inspect node_modules/jest/bin/jest.js", | ||
"dev": "nodemon -r esm index.js", | ||
"dev:debug": "nodemon inspect -r esm index.js", | ||
"db:seed": "node -r esm src/db/seed.js", | ||
"db:clean": "node -r esm src/db/clean.js" | ||
}, | ||
"dependencies": { | ||
"@graphql-tools/delegate": "^7.0.7", | ||
"@graphql-tools/graphql-file-loader": "^6.2.6", | ||
"@graphql-tools/load": "^6.2.5", | ||
"@graphql-tools/schema": "^7.1.2", | ||
"@graphql-tools/stitch": "^7.1.4", | ||
"@graphql-tools/wrap": "^7.0.4", | ||
"apollo-datasource": "^0.7.2", | ||
"apollo-server": "^2.19.0", | ||
"bcrypt": "^5.0.0", | ||
"dotenv-flow": "^3.2.0", | ||
"graphql-tools": "^7.0.2", | ||
"jsonwebtoken": "^8.5.1", | ||
"neo4j-driver": "^4.2.1", | ||
"neo4j-graphql-js": "^2.17.1", | ||
"neode": "^0.4.6" | ||
}, | ||
"devDependencies": { | ||
"@babel/core": "^7.12.9", | ||
"@babel/preset-env": "^7.12.7", | ||
"apollo-server-testing": "^2.19.0", | ||
"babel-eslint": "^10.1.0", | ||
"babel-jest": "^26.6.3", | ||
"eslint": "^7.14.0", | ||
"eslint-config-airbnb-base": "^14.2.1", | ||
"eslint-plugin-import": "^2.22.1", | ||
"eslint-plugin-jest": "^24.1.3", | ||
"esm": "^3.2.25", | ||
"jest": "^26.6.3", | ||
"nodemon": "^2.0.6" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
require('dotenv-flow').config(); | ||
|
||
export const { JWT_SECRET, NEO4J_USERNAME, NEO4J_PASSWORD } = process.env; | ||
if (!(JWT_SECRET && NEO4J_USERNAME && NEO4J_PASSWORD)) { | ||
throw new Error(` | ||
Please create a .env file and configure environment variables there. | ||
You could e.g. copy the .env file used for testing: | ||
$ cp .env.text .env | ||
`); | ||
} | ||
export default { JWT_SECRET, NEO4J_USERNAME, NEO4J_PASSWORD }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import jwt from 'jsonwebtoken'; | ||
import driver from './driver'; | ||
import { JWT_SECRET } from './config'; | ||
|
||
export default function context({ req }) { | ||
let token = req.headers.authorization || ''; | ||
token = token.replace('Bearer ', ''); | ||
const jwtSign = payload => jwt.sign(payload, JWT_SECRET); | ||
try { | ||
const decoded = jwt.verify(token, JWT_SECRET); | ||
return { ...decoded, jwtSign, driver }; | ||
} catch (e) { | ||
return { jwtSign, driver }; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import neode from './neode'; | ||
|
||
(async () => { | ||
await neode.driver | ||
.session() | ||
.writeTransaction(txc => txc.run('MATCH(n) DETACH DELETE n;')); | ||
neode.driver.close(); | ||
})(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import bcrypt from 'bcrypt'; | ||
import neode from '../neode'; | ||
|
||
export default class Person { | ||
constructor(data) { | ||
Object.assign(this, data); | ||
} | ||
|
||
checkPassword(password) { | ||
return bcrypt.compareSync(password, this.hashedPassword); | ||
} | ||
|
||
async save() { | ||
this.hashedPassword = bcrypt.hashSync(this.password, 10); | ||
const node = await neode.create('Person', this); | ||
Object.assign(this, { ...node.properties(), node }); | ||
return this; | ||
} | ||
|
||
static async first(props) { | ||
const node = await neode.first('Person', props); | ||
if (!node) return null; | ||
return new Person({ ...node.properties(), node }); | ||
} | ||
|
||
static currentUser(context) { | ||
const { person } = context; | ||
if (!person) return null; | ||
return Person.first({ id: person.id }); | ||
} | ||
|
||
static async all() { | ||
const nodes = await neode.all('Person'); | ||
return nodes.map(node => new Person({ ...node.properties(), node })); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import neode from '../neode'; | ||
|
||
export default class Post { | ||
constructor(data) { | ||
Object.assign(this, data); | ||
} | ||
|
||
async save() { | ||
if (!(this.author && this.author.node)) | ||
throw new Error('author node is missing!'); | ||
const node = await neode.create('Post', this); | ||
await node.relateTo(this.author.node, 'wrote'); | ||
Object.assign(this, { ...node.properties(), node }); | ||
return this; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
module.exports = { | ||
id: { | ||
type: 'uuid', | ||
primary: true | ||
}, | ||
name: { | ||
type: 'string', | ||
required: true | ||
}, | ||
email: { | ||
type: 'string', | ||
unique: true, | ||
required: true | ||
}, | ||
password: { | ||
type: 'string', | ||
strip: true | ||
}, | ||
hashedPassword: { | ||
type: 'string', | ||
required: true | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
module.exports = { | ||
id: { | ||
type: 'uuid', | ||
primary: true | ||
}, | ||
title: { | ||
type: 'string', | ||
required: true | ||
}, | ||
text: 'string', | ||
wrote: { | ||
type: 'relationship', | ||
target: 'Person', | ||
relationship: 'WROTE', | ||
direction: 'in' | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import Neode from 'neode'; | ||
import '../config'; | ||
|
||
const dir = `${__dirname}/models`; | ||
// eslint-disable-next-line new-cap | ||
const instance = new Neode.fromEnv().withDirectory(dir); | ||
export default instance; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import neode from './neode'; | ||
import Person from './entities/Person'; | ||
import Post from './entities/Post'; | ||
|
||
const seed = async () => { | ||
const alice = new Person({ | ||
name: 'Alice', | ||
email: '[email protected]', | ||
password: '1234' | ||
}); | ||
const bob = new Person({ | ||
name: 'Bob', | ||
email: '[email protected]', | ||
password: '4321' | ||
}); | ||
await Promise.all([alice, bob].map(p => p.save())); | ||
const posts = [ | ||
new Post({ author: alice, title: 'Schema Stitching is cool!' }), | ||
new Post({ author: alice, title: 'Neo4J is a nice graph database!' }) | ||
]; | ||
await Promise.all(posts.map(post => post.save())); | ||
}; | ||
|
||
(async () => { | ||
await seed(); | ||
await neode.driver.close(); | ||
})(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import neo4j from 'neo4j-driver'; | ||
import { NEO4J_USERNAME, NEO4J_PASSWORD } from './config'; | ||
|
||
const driver = neo4j.driver( | ||
'bolt://localhost:7687', | ||
neo4j.auth.basic(NEO4J_USERNAME, NEO4J_PASSWORD) | ||
); | ||
export default driver; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import { makeAugmentedSchema } from 'neo4j-graphql-js'; | ||
import { gql } from 'apollo-server'; | ||
|
||
const typeDefs = gql` | ||
type Person { | ||
id: ID! | ||
name: String! | ||
email: String | ||
posts: [Post] @relation(name: "WROTE", direction: "OUT") | ||
} | ||
type Post { | ||
id: ID! | ||
title: String! | ||
text: String | ||
author: Person @relation(name: "WROTE", direction: "IN") | ||
} | ||
`; | ||
|
||
const schema = makeAugmentedSchema({ typeDefs }); | ||
export default schema; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
import { delegateToSchema } from '@graphql-tools/delegate'; | ||
import { | ||
AuthenticationError, | ||
UserInputError, | ||
ForbiddenError | ||
} from 'apollo-server'; | ||
import Person from './db/entities/Person'; | ||
import Post from './db/entities/Post'; | ||
|
||
export default ({ subschema }) => ({ | ||
Query: { | ||
profile: async (_parent, _args, context, info) => { | ||
const [person] = await delegateToSchema({ | ||
schema: subschema, | ||
operation: 'query', | ||
fieldName: 'Person', | ||
args: { | ||
id: context.person.id | ||
}, | ||
context, | ||
info | ||
}); | ||
return person; | ||
} | ||
}, | ||
Mutation: { | ||
login: async (_parent, { email, password }, { jwtSign }) => { | ||
const person = await Person.first({ email }); | ||
if (person && person.checkPassword(password)) { | ||
return jwtSign({ person: { id: person.id } }); | ||
} | ||
throw new AuthenticationError('Wrong email/password combination!'); | ||
}, | ||
signup: async (_parent, { name, email, password }, { jwtSign }) => { | ||
const existingPerson = await Person.first({ email }); | ||
if (existingPerson) throw new UserInputError('email address not unique'); | ||
const person = new Person({ name, email, password }); | ||
await person.save(); | ||
return jwtSign({ person: { id: person.id } }); | ||
}, | ||
writePost: async (_parent, args, context, info) => { | ||
const currentUser = await Person.currentUser(context); | ||
if (!currentUser) | ||
throw new ForbiddenError('You must be authenticated to write a post!'); | ||
const post = new Post({ ...args, author: currentUser }); | ||
await post.save(); | ||
const [resolvedPost] = await delegateToSchema({ | ||
schema: subschema, | ||
operation: 'query', | ||
fieldName: 'Post', | ||
args: { id: post.id }, | ||
context, | ||
info | ||
}); | ||
return resolvedPost; | ||
} | ||
}, | ||
Person: { | ||
email: { | ||
selectionSet: '{ id }', | ||
resolve: (parent, _args, context) => { | ||
const { person } = context; | ||
if (person && person.id === parent.id) return parent.email; | ||
throw new ForbiddenError('E-Mail addresses are private'); | ||
} | ||
}, | ||
postCount: { | ||
selectionSet: '{ posts { id } }', | ||
resolve: person => person.posts.length | ||
} | ||
} | ||
}); |
Oops, something went wrong.