Skip to content
This repository was archived by the owner on Sep 3, 2021. It is now read-only.

Commit 24beba5

Browse files
committed
docs: graphql schema stitching w neo4j-graphql-js
1 parent 2a8a8ad commit 24beba5

22 files changed

+8657
-0
lines changed

example/schema-stitching/.env.test

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
JWT_SECRET='supersecret'
2+
NEO4J_USERNAME='neo4j'
3+
NEO4J_PASSWORD='letmein'
4+
NEO4J_PROTOCOL=neo4j
5+
NEO4J_HOST=localhost
6+
NEO4J_DATABASE=neo4j
7+
NEO4J_ENCRYPTION=ENCRYPTION_OFF

example/schema-stitching/.eslintrc.js

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
module.exports = {
2+
plugins: ['jest'],
3+
env: {
4+
es6: true,
5+
node: true,
6+
'jest/globals': true
7+
},
8+
extends: 'airbnb-base',
9+
globals: {
10+
Atomics: 'readonly',
11+
SharedArrayBuffer: 'readonly'
12+
},
13+
parserOptions: {
14+
ecmaVersion: 2018,
15+
sourceType: 'module'
16+
},
17+
rules: {
18+
'jest/no-disabled-tests': 'warn',
19+
'jest/no-focused-tests': 'error',
20+
'jest/no-identical-title': 'error',
21+
'jest/prefer-to-have-length': 'warn',
22+
'jest/valid-expect': 'error',
23+
'import/no-extraneous-dependencies': [
24+
'error',
25+
{ devDependencies: ['db/**/*.js', '**/*.test.js', '**/*.spec.js'] }
26+
]
27+
}
28+
};
+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module.exports = {
2+
presets: [['@babel/preset-env', { targets: { node: 'current' } }]]
3+
};

example/schema-stitching/index.js

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { ApolloServer } from 'apollo-server';
2+
import Server from './src/server';
3+
4+
const playground = {
5+
settings: {
6+
'schema.polling.enable': false
7+
}
8+
};
9+
10+
(async () => {
11+
const server = await Server(ApolloServer, { playground });
12+
const { url } = await server.listen();
13+
// eslint-disable-next-line no-console
14+
console.log(`🚀 Server ready at ${url}`);
15+
})();

example/schema-stitching/package.json

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
{
2+
"name": "neo4j-graphql-js-example-schema-stitching",
3+
"version": "1.0.0",
4+
"main": "index.js",
5+
"license": "MIT",
6+
"scripts": {
7+
"lint": "eslint .",
8+
"test": "jest",
9+
"test:debug": "node inspect node_modules/jest/bin/jest.js",
10+
"dev": "nodemon -r esm index.js",
11+
"dev:debug": "nodemon inspect -r esm index.js",
12+
"db:seed": "node -r esm src/db/seed.js",
13+
"db:clean": "node -r esm src/db/clean.js"
14+
},
15+
"dependencies": {
16+
"@graphql-tools/delegate": "^7.0.7",
17+
"@graphql-tools/graphql-file-loader": "^6.2.6",
18+
"@graphql-tools/load": "^6.2.5",
19+
"@graphql-tools/schema": "^7.1.2",
20+
"@graphql-tools/stitch": "^7.1.4",
21+
"@graphql-tools/wrap": "^7.0.4",
22+
"apollo-datasource": "^0.7.2",
23+
"apollo-server": "^2.19.0",
24+
"bcrypt": "^5.0.0",
25+
"dotenv-flow": "^3.2.0",
26+
"graphql-tools": "^7.0.2",
27+
"jsonwebtoken": "^8.5.1",
28+
"neo4j-driver": "^4.2.1",
29+
"neo4j-graphql-js": "^2.17.1",
30+
"neode": "^0.4.6"
31+
},
32+
"devDependencies": {
33+
"@babel/core": "^7.12.9",
34+
"@babel/preset-env": "^7.12.7",
35+
"apollo-server-testing": "^2.19.0",
36+
"babel-eslint": "^10.1.0",
37+
"babel-jest": "^26.6.3",
38+
"eslint": "^7.14.0",
39+
"eslint-config-airbnb-base": "^14.2.1",
40+
"eslint-plugin-import": "^2.22.1",
41+
"eslint-plugin-jest": "^24.1.3",
42+
"esm": "^3.2.25",
43+
"jest": "^26.6.3",
44+
"nodemon": "^2.0.6"
45+
}
46+
}
+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
require('dotenv-flow').config();
2+
3+
export const { JWT_SECRET, NEO4J_USERNAME, NEO4J_PASSWORD } = process.env;
4+
if (!(JWT_SECRET && NEO4J_USERNAME && NEO4J_PASSWORD)) {
5+
throw new Error(`
6+
7+
Please create a .env file and configure environment variables there.
8+
9+
You could e.g. copy the .env file used for testing:
10+
11+
$ cp .env.text .env
12+
`);
13+
}
14+
export default { JWT_SECRET, NEO4J_USERNAME, NEO4J_PASSWORD };
+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import jwt from 'jsonwebtoken';
2+
import driver from './driver';
3+
import { JWT_SECRET } from './config';
4+
5+
export default function context({ req }) {
6+
let token = req.headers.authorization || '';
7+
token = token.replace('Bearer ', '');
8+
const jwtSign = payload => jwt.sign(payload, JWT_SECRET);
9+
try {
10+
const decoded = jwt.verify(token, JWT_SECRET);
11+
return { ...decoded, jwtSign, driver };
12+
} catch (e) {
13+
return { jwtSign, driver };
14+
}
15+
}
+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import neode from './neode';
2+
3+
(async () => {
4+
await neode.driver
5+
.session()
6+
.writeTransaction(txc => txc.run('MATCH(n) DETACH DELETE n;'));
7+
neode.driver.close();
8+
})();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import bcrypt from 'bcrypt';
2+
import neode from '../neode';
3+
4+
export default class Person {
5+
constructor(data) {
6+
Object.assign(this, data);
7+
}
8+
9+
checkPassword(password) {
10+
return bcrypt.compareSync(password, this.hashedPassword);
11+
}
12+
13+
async save() {
14+
this.hashedPassword = bcrypt.hashSync(this.password, 10);
15+
const node = await neode.create('Person', this);
16+
Object.assign(this, { ...node.properties(), node });
17+
return this;
18+
}
19+
20+
static async first(props) {
21+
const node = await neode.first('Person', props);
22+
if (!node) return null;
23+
return new Person({ ...node.properties(), node });
24+
}
25+
26+
static currentUser(context) {
27+
const { person } = context;
28+
if (!person) return null;
29+
return Person.first({ id: person.id });
30+
}
31+
32+
static async all() {
33+
const nodes = await neode.all('Person');
34+
return nodes.map(node => new Person({ ...node.properties(), node }));
35+
}
36+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import neode from '../neode';
2+
3+
export default class Post {
4+
constructor(data) {
5+
Object.assign(this, data);
6+
}
7+
8+
async save() {
9+
if (!(this.author && this.author.node))
10+
throw new Error('author node is missing!');
11+
const node = await neode.create('Post', this);
12+
await node.relateTo(this.author.node, 'wrote');
13+
Object.assign(this, { ...node.properties(), node });
14+
return this;
15+
}
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
module.exports = {
2+
id: {
3+
type: 'uuid',
4+
primary: true
5+
},
6+
name: {
7+
type: 'string',
8+
required: true
9+
},
10+
email: {
11+
type: 'string',
12+
unique: true,
13+
required: true
14+
},
15+
password: {
16+
type: 'string',
17+
strip: true
18+
},
19+
hashedPassword: {
20+
type: 'string',
21+
required: true
22+
}
23+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
module.exports = {
2+
id: {
3+
type: 'uuid',
4+
primary: true
5+
},
6+
title: {
7+
type: 'string',
8+
required: true
9+
},
10+
text: 'string',
11+
wrote: {
12+
type: 'relationship',
13+
target: 'Person',
14+
relationship: 'WROTE',
15+
direction: 'in'
16+
}
17+
};
+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import Neode from 'neode';
2+
import '../config';
3+
4+
const dir = `${__dirname}/models`;
5+
// eslint-disable-next-line new-cap
6+
const instance = new Neode.fromEnv().withDirectory(dir);
7+
export default instance;
+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import neode from './neode';
2+
import Person from './entities/Person';
3+
import Post from './entities/Post';
4+
5+
const seed = async () => {
6+
const alice = new Person({
7+
name: 'Alice',
8+
email: 'alice@example.org',
9+
password: '1234'
10+
});
11+
const bob = new Person({
12+
name: 'Bob',
13+
email: 'bob@example.org',
14+
password: '4321'
15+
});
16+
await Promise.all([alice, bob].map(p => p.save()));
17+
const posts = [
18+
new Post({ author: alice, title: 'Schema Stitching is cool!' }),
19+
new Post({ author: alice, title: 'Neo4J is a nice graph database!' })
20+
];
21+
await Promise.all(posts.map(post => post.save()));
22+
};
23+
24+
(async () => {
25+
await seed();
26+
await neode.driver.close();
27+
})();
+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import neo4j from 'neo4j-driver';
2+
import { NEO4J_USERNAME, NEO4J_PASSWORD } from './config';
3+
4+
const driver = neo4j.driver(
5+
'bolt://localhost:7687',
6+
neo4j.auth.basic(NEO4J_USERNAME, NEO4J_PASSWORD)
7+
);
8+
export default driver;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { makeAugmentedSchema } from 'neo4j-graphql-js';
2+
import { gql } from 'apollo-server';
3+
4+
const typeDefs = gql`
5+
type Person {
6+
id: ID!
7+
name: String!
8+
email: String
9+
posts: [Post] @relation(name: "WROTE", direction: "OUT")
10+
}
11+
12+
type Post {
13+
id: ID!
14+
title: String!
15+
text: String
16+
author: Person @relation(name: "WROTE", direction: "IN")
17+
}
18+
`;
19+
20+
const schema = makeAugmentedSchema({ typeDefs });
21+
export default schema;
+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { delegateToSchema } from '@graphql-tools/delegate';
2+
import {
3+
AuthenticationError,
4+
UserInputError,
5+
ForbiddenError
6+
} from 'apollo-server';
7+
import Person from './db/entities/Person';
8+
import Post from './db/entities/Post';
9+
10+
export default ({ subschema }) => ({
11+
Query: {
12+
profile: async (_parent, _args, context, info) => {
13+
const [person] = await delegateToSchema({
14+
schema: subschema,
15+
operation: 'query',
16+
fieldName: 'Person',
17+
args: {
18+
id: context.person.id
19+
},
20+
context,
21+
info
22+
});
23+
return person;
24+
}
25+
},
26+
Mutation: {
27+
login: async (_parent, { email, password }, { jwtSign }) => {
28+
const person = await Person.first({ email });
29+
if (person && person.checkPassword(password)) {
30+
return jwtSign({ person: { id: person.id } });
31+
}
32+
throw new AuthenticationError('Wrong email/password combination!');
33+
},
34+
signup: async (_parent, { name, email, password }, { jwtSign }) => {
35+
const existingPerson = await Person.first({ email });
36+
if (existingPerson) throw new UserInputError('email address not unique');
37+
const person = new Person({ name, email, password });
38+
await person.save();
39+
return jwtSign({ person: { id: person.id } });
40+
},
41+
writePost: async (_parent, args, context, info) => {
42+
const currentUser = await Person.currentUser(context);
43+
if (!currentUser)
44+
throw new ForbiddenError('You must be authenticated to write a post!');
45+
const post = new Post({ ...args, author: currentUser });
46+
await post.save();
47+
const [resolvedPost] = await delegateToSchema({
48+
schema: subschema,
49+
operation: 'query',
50+
fieldName: 'Post',
51+
args: { id: post.id },
52+
context,
53+
info
54+
});
55+
return resolvedPost;
56+
}
57+
},
58+
Person: {
59+
email: {
60+
selectionSet: '{ id }',
61+
resolve: (parent, _args, context) => {
62+
const { person } = context;
63+
if (person && person.id === parent.id) return parent.email;
64+
throw new ForbiddenError('E-Mail addresses are private');
65+
}
66+
},
67+
postCount: {
68+
selectionSet: '{ posts { id } }',
69+
resolve: person => person.posts.length
70+
}
71+
}
72+
});

0 commit comments

Comments
 (0)