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

Commit

Permalink
docs: graphql schema stitching w neo4j-graphql-js
Browse files Browse the repository at this point in the history
  • Loading branch information
roschaefer committed Dec 15, 2020
1 parent 2a8a8ad commit ef1b6c9
Show file tree
Hide file tree
Showing 22 changed files with 8,657 additions and 0 deletions.
7 changes: 7 additions & 0 deletions example/schema-stitching/.env.test
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
28 changes: 28 additions & 0 deletions example/schema-stitching/.eslintrc.js
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'] }
]
}
};
3 changes: 3 additions & 0 deletions example/schema-stitching/babel.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
presets: [['@babel/preset-env', { targets: { node: 'current' } }]]
};
15 changes: 15 additions & 0 deletions example/schema-stitching/index.js
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}`);
})();
46 changes: 46 additions & 0 deletions example/schema-stitching/package.json
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"
}
}
14 changes: 14 additions & 0 deletions example/schema-stitching/src/config.js
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 };
15 changes: 15 additions & 0 deletions example/schema-stitching/src/context.js
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 };
}
}
8 changes: 8 additions & 0 deletions example/schema-stitching/src/db/clean.js
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();
})();
36 changes: 36 additions & 0 deletions example/schema-stitching/src/db/entitites/Person.js
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 }));
}
}
16 changes: 16 additions & 0 deletions example/schema-stitching/src/db/entitites/Post.js
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;
}
}
23 changes: 23 additions & 0 deletions example/schema-stitching/src/db/models/Person.js
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
}
};
17 changes: 17 additions & 0 deletions example/schema-stitching/src/db/models/Post.js
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'
}
};
7 changes: 7 additions & 0 deletions example/schema-stitching/src/db/neode.js
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;
27 changes: 27 additions & 0 deletions example/schema-stitching/src/db/seed.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import neode from './neode';
import Person from './entitites/Person';
import Post from './entitites/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();
})();
8 changes: 8 additions & 0 deletions example/schema-stitching/src/driver.js
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;
21 changes: 21 additions & 0 deletions example/schema-stitching/src/neo4j-graphql-js/schema.js
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;
72 changes: 72 additions & 0 deletions example/schema-stitching/src/resolvers.js
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/entitites/Person';
import Post from './db/entitites/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
}
}
});
Loading

0 comments on commit ef1b6c9

Please sign in to comment.