initial commit

This commit is contained in:
Grzegorz Leoniec 2018-10-05 13:31:45 +02:00
commit 5fcceda30f
12 changed files with 4032 additions and 0 deletions

1
.babelrc Normal file
View File

@ -0,0 +1 @@
{ "presets": ["env"] }

6
.env Normal file
View File

@ -0,0 +1,6 @@
NEO4J_URI=bolt://localhost:7687
NEO4J_USER=neo4j
NEO4J_PASSWORD=letmein
GRAPHQL_LISTEN_PORT=4000
GRAPHQL_URI=http://localhost:4000
MOCK=false

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
node_modules/

3
.graphqlconfig Normal file
View File

@ -0,0 +1,3 @@
{
"schemaPath": "./src/schema.graphql"
}

93
README.md Normal file
View File

@ -0,0 +1,93 @@
# Human-Connection - GraphQL API - Prototype
> This Prototype tries to resolve the biggest hurdle of connecting
> our services together. This is not possible in a sane way using
> our current approach.
>
> With this Prototype we can explore using the combination of
> GraphQL and the Neo4j Graph Database for achieving the connected
> nature of a social graph with better development experience as we
> do not need to connect data by our own any more through weird table
> structures etc.
>
> #### Advantages:
> - easer data structure
> - better connected data
> - easy to achieve "recommendations" based on actions (relations)
> - more performant and better to understand API
> - better API client that uses caching
>
> We still need to evaluate the drawbacks and estimate the development
> cost of such an approach
## Quick Start
Install dependencies:
```bash
yarn install
# -or-
npm install
```
Start the GraphQL service:
```bash
yarn start
# -or-
npm start
```
This will start the GraphQL service (by default on localhost:4000)
where you can issue GraphQL requests or access GraphQL Playground in the browser:
![GraphQL Playground](img/graphql-playground.png)
## Configure
Set your Neo4j connection string and credentials in `.env`.
For example:
_.env_
```yaml
NEO4J_URI=bolt://localhost:7687
NEO4J_USER=neo4j
NEO4J_PASSWORD=letmein
```
Note that grand-stack-starter does not currently bundle a distribution
of Neo4j. You can download [Neo4j Desktop](https://neo4j.com/download/)
and run locally for development, spin up a [hosted Neo4j Sandbox instance](https://neo4j.com/download/),
run Neo4j in one of the [many cloud options](https://neo4j.com/developer/guide-cloud-deployment/),
or [spin up Neo4j in a Docker container](https://neo4j.com/developer/docker/).
Just be sure to update the Neo4j connection string and credentials accordingly in `.env`.
## Deployment
You can deploy to any service that hosts Node.js apps, but [Zeit Now](https://zeit.co/now)
is a great easy to use service for hosting your app that has an easy to use free plan for small projects.
To deploy your GraphQL service on Zeit Now, first install [Now Desktop](https://zeit.co/download) -
you'll need to provide an email address. Then run
```
now
```
to deploy your GraphQL service on Zeit Now. Once deployed you'll be given
a fresh URL that represents the current state of your application where you
can access your GraphQL endpoint and GraphQL Playgound.
For example: https://hc-graph-api-prototype-sdga96ad7.now.sh/
## Seeding The Database
Optionally you can seed the GraphQL service by executing mutations that
will write sample data to the database:
```bash
yarn seedDb
# -or-
npm run seedDb
```

32
package.json Normal file
View File

@ -0,0 +1,32 @@
{
"name": "hc-graph-api-prototype",
"version": "0.0.1",
"description": "Graph API Protype for Human Connection",
"main": "src/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "./node_modules/.bin/nodemon --exec babel-node src/index.js",
"seedDb": "./node_modules/.bin/babel-node src/seed/seed-db.js"
},
"author": "Grzegorz Leoniec",
"license": "MIT",
"dependencies": {
"apollo-boost": "^0.1.10",
"apollo-cache-inmemory": "^1.2.5",
"apollo-client": "^2.3.2",
"apollo-link-http": "^1.5.4",
"apollo-server": "^2.0.4",
"dotenv": "^6.0.0",
"faker": "^4.1.0",
"graphql-custom-directives": "^0.2.13",
"graphql-tag": "^2.9.2",
"neo4j-driver": "^1.6.1",
"neo4j-graphql-js": "^1.0.2",
"node-fetch": "^2.1.2"
},
"devDependencies": {
"babel-cli": "^6.26.0",
"babel-preset-env": "^1.7.0",
"nodemon": "^1.17.5"
}
}

17
src/graphql-schema.js Normal file
View File

@ -0,0 +1,17 @@
// import { neo4jgraphql } from "neo4j-graphql-js"
import fs from 'fs'
import path from 'path'
export const typeDefs =
fs.readFileSync(process.env.GRAPHQL_SCHEMA || path.join(__dirname, "schema.graphql"))
.toString('utf-8')
export const resolvers = {
// Query: {
// usersBySubstring: neo4jgraphql
// }
}
export const mutations = {
}

64
src/index.js Normal file
View File

@ -0,0 +1,64 @@
// import { GraphQLServer } from 'graphql-yoga'
import { ApolloServer, makeExecutableSchema } from 'apollo-server'
import { augmentSchema } from 'neo4j-graphql-js'
import { typeDefs, resolvers } from './graphql-schema'
import { v1 as neo4j } from 'neo4j-driver'
import dotenv from 'dotenv'
import {
GraphQLLowerCaseDirective,
GraphQLTrimDirective,
GraphQLDefaultToDirective
} from 'graphql-custom-directives';
import faker from 'faker'
dotenv.config()
const schema = makeExecutableSchema({
typeDefs,
resolvers
})
// augmentSchema will add auto generated mutations based on types in schema
const augmentedSchema = augmentSchema(schema)
// add custom directives
const directives = [
GraphQLLowerCaseDirective,
GraphQLTrimDirective,
GraphQLDefaultToDirective
]
augmentedSchema._directives.push.apply(augmentedSchema._directives, directives)
const driver = neo4j.driver(
process.env.NEO4J_URI || 'bolt://localhost:7687',
neo4j.auth.basic(
process.env.NEO4J_USER || 'neo4j',
process.env.NEO4J_PASSWORD || 'neo4j'
)
)
const MOCK = (process.env.MOCK === 'true')
console.log('MOCK:', MOCK)
const server = new ApolloServer({
context: {
driver
},
tracing: true,
schema: augmentedSchema,
mocks: MOCK ? {
User: () => ({
name: () => `${faker.name.firstName()} ${faker.name.lastName()}`,
email: () => `${faker.internet.email()}`
}),
Post: () => ({
title: () => faker.lorem.lines(1),
slug: () => faker.lorem.slug(3),
content: () => faker.lorem.paragraphs(5),
contentExcerpt: () => faker.lorem.paragraphs(1)
})
} : false
})
server.listen().then(({ url }) => {
console.log(`Server ready at ${url} 🚀`);
})

107
src/schema.graphql Normal file
View File

@ -0,0 +1,107 @@
enum VisibilityEnum {
Public
Friends
Private
}
enum UserGroupEnum {
Admin
Moderator
User
}
type WrittenPost @relation(name: "WROTE") {
from: User
to: Post
timestamp: Int
}
type WrittenComment @relation(name: "WROTE") {
from: User
to: Comment
timestamp: Int
}
type User {
id: ID!
name: String!
email: String
disabled: Boolean @default(to: false)
role: UserGroupEnum
friends: [User]! @relation(name: "FRIENDS", direction: "BOTH")
friendsCount: Int! @cypher(statement: "MATCH (this)<-[:FRIENDS]->(r:User) RETURN COUNT(r)")
following: [User]! @relation(name: "FOLLOWING", direction: "OUT")
followingCount: Int! @cypher(statement: "MATCH (this)-[:FOLLOWING]->(r:User) RETURN COUNT(r)")
followedBy: [User]! @relation(name: "FOLLOWING", direction: "IN")
followedByCount: Int! @cypher(statement: "MATCH (this)<-[:FOLLOWING]-(r:User) RETURN COUNT(r)")
contributions: [WrittenPost]!
contributionsCount: Int! @cypher(statement: "MATCH (this)-[:WROTE]->(r:Post) RETURN COUNT(r)")
comments: [WrittenComment]!
commentsCount: Int! @cypher(statement: "MATCH (this)-[:WROTE]->(r:Comment) RETURN COUNT(r)")
shouted: [Post]! @relation(name: "SHOUTED", direction: "OUT")
organizationsCreated: [Organization] @relation(name: "CREATED_ORGA", direction: "OUT")
organizationsOwned: [Organization] @relation(name: "OWNING_ORGA", direction: "OUT")
}
type Post {
id: ID!
author: WrittenPost
title: String!
slug: String!
content: String!
contentExcerpt: String!
visibility: VisibilityEnum
disabled: Boolean @default(to: false)
tags: [Tag]! @relation(name: "TAGGED", direction: "OUT")
categories: [Category]! @relation(name: "CATEGORIZED", direction: "OUT")
comments: [Comment]! @relation(name: "COMMENT", direction: "IN")
commentsCount: Int! @cypher(statement: "MATCH (this)<-[:COMMENT]-(r:Comment) RETURN COUNT(r)")
shoutedBy: [User]! @relation(name: "SHOUTED", direction: "IN")
shoutedCount: Int! @cypher(statement: "MATCH (this)<-[:SHOUTED]-(r:User) RETURN COUNT(r)")
}
type Comment {
id: ID!
author: WrittenComment
content: String!
contentExcerpt: String!
post: Post @relation(name: "COMMENT", direction: "OUT")
disabled: Boolean @default(to: false)
}
type Category {
id: ID!
name: String!
slug: String!
icon: String!
}
type Organization {
id: ID!
createdBy: User @relation(name: "CREATED_ORGA", direction: "IN")
ownedBy: [User] @relation(name: "OWNING_ORGA", direction: "IN")
name: String!
slug: String!
disabled: Boolean @default(to: false)
tags: [Tag]! @relation(name: "TAGGED", direction: "OUT")
categories: [Category]! @relation(name: "CATEGORIZED", direction: "OUT")
}
type Tag {
id: ID!
name: String!
taggedPosts: [Post]! @relation(name: "TAGGED", direction: "IN")
taggedOrganizations: [Organization]! @relation(name: "TAGGED", direction: "IN")
taggedCount: Int! @cypher(statement: "MATCH (this)<-[:TAGGED]-(r) RETURN COUNT(r)")
disabled: Boolean @default(to: false)
}

21
src/seed/seed-db.js Normal file
View File

@ -0,0 +1,21 @@
import ApolloClient from "apollo-client";
import dotenv from "dotenv";
import gql from 'graphql-tag'
import seedMutations from "./seed-mutations";
import fetch from "node-fetch";
import { HttpLink } from "apollo-link-http";
import { InMemoryCache } from "apollo-cache-inmemory";
dotenv.config();
const client = new ApolloClient({
link: new HttpLink({ uri: process.env.GRAPHQL_URI, fetch }),
cache: new InMemoryCache()
});
client
.mutate({
mutation: gql(seedMutations)
})
.then(data => console.log(data))
.catch(error => console.error(error));

321
src/seed/seed-mutations.js Normal file
View File

@ -0,0 +1,321 @@
export default `
mutation {
u1: CreateUser(id: "u1", name: "Will Blast", email: "Will-Blast@Yahoo.com", role: Admin) {
id
name
email
role
}
u2: CreateUser(id: "u2", name: "Bob der Bausmeister", email: "Bob-der-Bausmeister@yahoo.com", role: Moderator) {
id
name
email
role
}
u3: CreateUser(id: "u3", name: "Jenny Rostock", email: "JennyRostock@yahoo.com", role: Admin) {
id
name
email
role
}
u4: CreateUser(id: "u4", name: "Angie Banjie", email: "Angie_Banjie@yahoo.com", role: User) {
id
name
email
role
}
cat1: CreateCategory( id: "cat1", name: "Just For Fun", slug: "justforfun", icon: "categories-justforfun" ) { name }
cat2: CreateCategory( id: "cat2", name: "Happyness & Values", slug: "happyness-values", icon: "categories-luck" ) { name }
cat3: CreateCategory( id: "cat3", name: "Health & Wellbeing", slug: "health-wellbeing", icon: "categories-health" ) { name }
cat4: CreateCategory( id: "cat4", name: "Environment & Nature", slug: "environment-nature", icon: "categories-environment" ) { name }
cat5: CreateCategory( id: "cat5", name: "Animal Protection", slug: "animalprotection", icon: "categories-animal-justice" ) { name }
cat6: CreateCategory( id: "cat6", name: "Humanrights Justice", slug: "humanrights-justice", icon: "categories-human-rights" ) { name }
cat7: CreateCategory( id: "cat7", name: "Education & Sciences", slug: "education-sciences", icon: "categories-education" ) { name }
cat8: CreateCategory( id: "cat8", name: "Cooperation & Development", slug: "cooperation-development", icon: "categories-cooperation" ) { name }
cat9: CreateCategory( id: "cat9", name: "Democracy & Politics", slug: "democracy-politics", icon: "categories-politics" ) { name }
cat10: CreateCategory( id: "cat10", name: "Economy & Finances", slug: "economy-finances", icon: "categories-economy" ) { name }
cat11: CreateCategory( id: "cat11", name: "Energy & Technology", slug: "energy-technology", icon: "categories-technology" ) { name }
cat12: CreateCategory( id: "cat12", name: "IT, Internet & Data Privacy", slug: "it-internet-dataprivacy", icon: "categories-internet" ) { name }
cat13: CreateCategory( id: "cat13", name: "Art, Curlure & Sport", slug: "art-culture-sport", icon: "categories-art" ) { name }
cat14: CreateCategory( id: "cat14", name: "Freedom of Speech", slug: "freedomofspeech", icon: "categories-freedom-of-speech" ) { name }
cat15: CreateCategory( id: "cat15", name: "Consumption & Sustainability", slug: "consumption-sustainability", icon: "categories-sustainability" ) { name }
cat16: CreateCategory( id: "cat16", name: "Global Peace & Nonviolence", slug: "globalpeace-nonviolence", icon: "categories-peace" ) { name }
p1: CreatePost(
id: "p1",
title: "Gedanken eines Polizisten zum Einsatz im Hambacher Forst",
slug: "gedanken-eines-polizisten-zum-einsatz-im-hambacher-forst",
content: "# 1 This is my content 1",
contentExcerpt: "# 1 This is my content 1",
visibility: Public
) { title }
p1_cat1: AddPostCategories(from: {id: "p1"}, to: {id: "cat1"}) { from { id } }
p1_cat2: AddPostCategories(from: {id: "p1"}, to: {id: "cat2"}) { from { id } }
p2: CreatePost(
id: "p2",
title: "Julian Assange",
slug: "julian-assange",
content: "#2 This is my content 2",
contentExcerpt: "#2 This is my content 2",
visibility: Public
) { title }
p2_cat1: AddPostCategories(from: {id: "p2"}, to: {id: "cat1"}) { from { id } }
p2_cat16: AddPostCategories(from: {id: "p2"}, to: {id: "cat16"}) { from { id } }
p3: CreatePost(
id: "p3",
title: "Hacker, Freaks und Funktionäre...Der CCC",
slug: "hacker-freaks-und-funktionäre-der-ccc",
content: "#3 This is my content 3",
contentExcerpt: "#3 This is my content 3",
visibility: Public
) { title }
p3_cat1: AddPostCategories(from: {id: "p3"}, to: {id: "cat1"}) { from { id } }
p3_cat3: AddPostCategories(from: {id: "p3"}, to: {id: "cat3"}) { from { id } }
p3_cat14: AddPostCategories(from: {id: "p3"}, to: {id: "cat14"}) { from { id } }
p4: CreatePost(
id: "p4",
title: "Lebensmittel (?)",
slug: "lebensmittel",
content: "#4 This is my content 4",
contentExcerpt: "#4 This is my content 4",
visibility: Public
) { title }
p4_cat1: AddPostCategories(from: {id: "p4"}, to: {id: "cat1"}) { from { id } }
p4_cat9: AddPostCategories(from: {id: "p4"}, to: {id: "cat9"}) { from { id } }
p4_cat4: AddPostCategories(from: {id: "p4"}, to: {id: "cat4"}) { from { id } }
c1: CreateComment(
id: "c1",
content: "# 1 This is my comment 1",
contentExcerpt: "# 1 This is my..."
) { id }
c2: CreateComment(
id: "c2",
content: "# 2 This is my comment 2",
contentExcerpt: "# 2 This is my..."
) { id }
c3: CreateComment(
id: "c3",
content: "# 3 This is my comment 3",
contentExcerpt: "# 3 This is my..."
) { id }
c4: CreateComment(
id: "c4",
content: "# 4 This is my comment 4",
contentExcerpt: "# 4 This is my..."
) { id }
c5: CreateComment(
id: "c5",
content: "# 5 This is my comment 5",
contentExcerpt: "# 5 This is my..."
) { id }
c1_u1: AddCommentAuthor(
from: { id: "u1" },
to: { id: "c1" },
data: { timestamp: 1538655020 }
) { from { id } }
c2_u1: AddCommentAuthor(
from: { id: "u1" },
to: { id: "c2" },
data: { timestamp: 1538655020 }
) { from { id } }
c3_u2: AddCommentAuthor(
from: { id: "u2" },
to: { id: "c3" },
data: { timestamp: 1538655020 }
) { from { id } }
c4_u3: AddCommentAuthor(
from: { id: "u3" },
to: { id: "c4" },
data: { timestamp: 1538655020 }
) { from { id } }
c5_u4: AddCommentAuthor(
from: { id: "u4" },
to: { id: "c5" },
data: { timestamp: 1538655020 }
) { from { id } }
c1_p1: AddCommentPost(
from: { id: "c1" },
to: { id: "p1" }
) { from { id } }
c2_p1: AddCommentPost(
from: { id: "c2" },
to: { id: "p1" }
) { from { id } }
c3_p2: AddCommentPost(
from: { id: "c3" },
to: { id: "p2" }
) { from { id } }
c4_p3: AddCommentPost(
from: { id: "c4" },
to: { id: "p3" }
) { from { id } }
c5_p4: AddCommentPost(
from: { id: "c5" },
to: { id: "p4" }
) { from { id } }
t1: CreateTag(
id: "t1",
name: "Umwelt"
) { name }
t2: CreateTag(
id: "t2",
name: "Naturschutz"
) { name }
t3: CreateTag(
id: "t3",
name: "Demokratie"
) { name }
t4: CreateTag(
id: "t4",
name: "Freiheit"
) { name }
p1_t1: AddPostTags(
from: { id: "p1" }
to: { id: "t1" }
) { from { id } }
p1_t2: AddPostTags(
from: { id: "p1" }
to: { id: "t2" }
) { from { id } }
p1_t3: AddPostTags(
from: { id: "p1" }
to: { id: "t3" }
) { from { id } }
p2_t4: AddPostTags(
from: { id: "p2" }
to: { id: "t4" }
) { from { id } }
p3_t2: AddPostTags(
from: { id: "p3" }
to: { id: "t2" }
) { from { id } }
p3_t4: AddPostTags(
from: { id: "p3" }
to: { id: "t4" }
) { from { id } }
o1: CreateOrganization(
id: "o1",
name: "Democracy Deutschland",
slug: "democracy-deutschland"
) { name }
o2: CreateOrganization(
id: "o2",
name: "Human-Connection",
slug: "human-connection"
) { name }
o3: CreateOrganization(
id: "o3",
name: "Pro Veg",
slug: "pro-veg"
) { name }
o4: CreateOrganization(
id: "o4",
name: "Greenpeace",
slug: "greenpeace"
) { name }
u1_c_o1: AddOrganizationCreatedBy(
from: { id: "u1" },
to: { id: "o1" }
) { from { id } }
u1_c_o2: AddOrganizationCreatedBy(
from: { id: "u1" },
to: { id: "o2" }
) { from { id } }
u2_o_o1: AddOrganizationOwnedBy(
from: { id: "u2" },
to: { id: "o2" }
) { from { id } }
u2_c_o3: AddOrganizationOwnedBy(
from: { id: "u2" },
to: { id: "o3" }
) { from { id } }
u1_friends_u2: AddUserFriends(
from: { id: "u1" },
to: { id: "u2" }
) { from { id } }
u1_friends_u3: AddUserFriends(
from: { id: "u1" },
to: { id: "u3" }
) { from { id } }
u1_follow_u2: AddUserFollowing(
from: { id: "u1" },
to: { id: "u2" }
) { from { id } }
u2_follow_u1: AddUserFollowing(
from: { id: "u2" },
to: { id: "u1" }
) { from { id } }
u2_follow_u3: AddUserFollowing(
from: { id: "u2" },
to: { id: "u3" }
) { from { id } }
u2_follow_u4: AddUserFollowing(
from: { id: "u2" },
to: { id: "u4" }
) { from { id } }
u4_follow_u2: AddUserFollowing(
from: { id: "u4" },
to: { id: "u2" }
) { from { id } }
ur1: AddUserContributions(
from: { id: "u1" },
to: { id: "p1" },
data: { timestamp: 1538655020 }
) { from { id } }
ur2: AddUserContributions(
from: { id: "u2" },
to: { id: "p2" },
data: { timestamp: 1538655120 }
) { from { id } }
ur3: AddUserContributions(
from: { id: "u3" },
to: { id: "p3" },
data: { timestamp: 1538653120 }
) { from { id } }
ur4: AddUserContributions(
from: { id: "u4" },
to: { id: "p4" },
data: { timestamp: 1538615120 }
) { from { id } }
u1s2: AddUserShouted(
from: { id: "u1" },
to: { id: "p2" }
) { from { id } }
u1s3: AddUserShouted(
from: { id: "u1" },
to: { id: "p3" }
) { from { id } }
u2s1: AddUserShouted(
from: { id: "u2" },
to: { id: "p1" }
) { from { id } }
u3s1: AddUserShouted(
from: { id: "u3" },
to: { id: "p1" }
) { from { id } }
u3s4: AddUserShouted(
from: { id: "u3" },
to: { id: "p4" }
) { from { id } }
u4s1: AddUserShouted(
from: { id: "u4" },
to: { id: "p1" }
) { from { id } }
}
`

3366
yarn.lock Normal file

File diff suppressed because it is too large Load Diff