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

Add Schema Stitching Example #550

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file modified .babelrc
100755 → 100644
Empty file.
Empty file modified README.md
100755 → 100644
Empty file.
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/entities/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/entities/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 './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();
})();
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/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
}
}
});
Loading