From 4f8f0debbee1b766aa7c49d8212f97bc1d66ae05 Mon Sep 17 00:00:00 2001 From: Grzegorz Leoniec Date: Wed, 13 Feb 2019 16:02:25 +0100 Subject: [PATCH 01/26] Added findPost query --- src/schema.graphql | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/schema.graphql b/src/schema.graphql index 55f23d5ca..95e7b3d48 100644 --- a/src/schema.graphql +++ b/src/schema.graphql @@ -1,6 +1,16 @@ type Query { isLoggedIn: Boolean! statistics: Statistics! + findPosts(filter: String!, limit: Int = 10): [Post]! @cypher( + statement: """ + CALL db.index.fulltext.queryNodes( + 'postTitleAndContent', $filter+'~') + YIELD node AS node + RETURN node + ORDER BY node.createdAt DESC + LIMIT $limit + """ + ) } type Mutation { login(email: String!, password: String!): LoggedInUser From 56936c403831b9b624711ca60c6a1a94d6121b35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Thu, 14 Feb 2019 16:05:30 +0100 Subject: [PATCH 02/26] Setup a routine how to create indices initially In order to create the indices programmatically we need to change the default password for security concerns. To create the user we need to start the neo4j database. So I decided to provide a bash script that let us do it once the container are started. In production we must change the NEO4J_PASSWORD. --- README.md | 2 +- docker-compose.override.yml | 7 +++++++ docker-compose.prod.yml | 9 +++++++++ docker-compose.yml | 7 ------- neo4j/Dockerfile | 1 + neo4j/migrate.sh | 4 ++++ src/bootstrap/neo4j.js | 2 +- src/graphql-schema.js | 2 +- src/schema.graphql | 2 +- 9 files changed, 25 insertions(+), 11 deletions(-) create mode 100644 docker-compose.prod.yml create mode 100755 neo4j/migrate.sh diff --git a/README.md b/README.md index 1b12562d2..b1363a293 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ _.env_ ```yaml NEO4J_URI=bolt://localhost:7687 -NEO4J_USER=neo4j +NEO4J_USERNAME=neo4j NEO4J_PASSWORD=letmein ``` diff --git a/docker-compose.override.yml b/docker-compose.override.yml index ef7d52c7e..b972c31f6 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -11,6 +11,13 @@ services: - /nitro-backend/node_modules command: yarn run dev neo4j: + environment: + - NEO4J_AUTH=none ports: - 7687:7687 - 7474:7474 + volumes: + - neo4j-data:/data + +volumes: + neo4j-data: diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 000000000..c4f5dc4f5 --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,9 @@ +version: "3.7" + +services: + neo4j: + environment: + - NEO4J_PASSWORD=letmein + backend: + environment: + - NEO4J_PASSWORD=letmein diff --git a/docker-compose.yml b/docker-compose.yml index 6905bb893..1e8c9158c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -27,14 +27,7 @@ services: context: neo4j networks: - hc-network - volumes: - - neo4j-data:/data - environment: - - NEO4J_AUTH=none networks: hc-network: name: hc-network - -volumes: - neo4j-data: diff --git a/neo4j/Dockerfile b/neo4j/Dockerfile index cb7fd228f..f6e71811b 100644 --- a/neo4j/Dockerfile +++ b/neo4j/Dockerfile @@ -1,2 +1,3 @@ FROM neo4j:3.5.0 RUN wget https://github.com/neo4j-contrib/neo4j-apoc-procedures/releases/download/3.5.0.1/apoc-3.5.0.1-all.jar -P plugins/ +COPY migrate.sh /usr/local/bin/migrate diff --git a/neo4j/migrate.sh b/neo4j/migrate.sh new file mode 100755 index 000000000..30a58c306 --- /dev/null +++ b/neo4j/migrate.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -e +echo "CALL dbms.security.changePassword('${NEO4J_PASSWORD}');" | cypher-shell --username neo4j --password neo4j +echo 'CALL db.index.fulltext.createNodeIndex("full_text_search",["Post"],["title", "content"]);' | cypher-shell --username neo4j --password $NEO4J_PASSWORD diff --git a/src/bootstrap/neo4j.js b/src/bootstrap/neo4j.js index 766c12065..929e62f23 100644 --- a/src/bootstrap/neo4j.js +++ b/src/bootstrap/neo4j.js @@ -9,7 +9,7 @@ export default function () { driver = neo4j.driver( process.env.NEO4J_URI || 'bolt://localhost:7687', neo4j.auth.basic( - process.env.NEO4J_USER || 'neo4j', + process.env.NEO4J_USERNAME || 'neo4j', process.env.NEO4J_PASSWORD || 'neo4j' ) ) diff --git a/src/graphql-schema.js b/src/graphql-schema.js index 8b5f369e0..c525d67d7 100644 --- a/src/graphql-schema.js +++ b/src/graphql-schema.js @@ -34,7 +34,7 @@ export const query = (cypher, session) => { }) }) } -const queryOne = (cypher, session) => { +export const queryOne = (cypher, session) => { return new Promise((resolve, reject) => { query(cypher, session) .then(res => { diff --git a/src/schema.graphql b/src/schema.graphql index 95e7b3d48..472b345d7 100644 --- a/src/schema.graphql +++ b/src/schema.graphql @@ -4,7 +4,7 @@ type Query { findPosts(filter: String!, limit: Int = 10): [Post]! @cypher( statement: """ CALL db.index.fulltext.queryNodes( - 'postTitleAndContent', $filter+'~') + 'full_text_search', $filter+'~') YIELD node AS node RETURN node ORDER BY node.createdAt DESC From d579d19ad24764f0de3f6ab87da809a0b2e8bbef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Thu, 14 Feb 2019 17:30:17 +0100 Subject: [PATCH 03/26] Modify migrate for development environment --- neo4j/migrate.sh | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/neo4j/migrate.sh b/neo4j/migrate.sh index 30a58c306..cdb9ec46c 100755 --- a/neo4j/migrate.sh +++ b/neo4j/migrate.sh @@ -1,4 +1,8 @@ #!/usr/bin/env bash set -e -echo "CALL dbms.security.changePassword('${NEO4J_PASSWORD}');" | cypher-shell --username neo4j --password neo4j -echo 'CALL db.index.fulltext.createNodeIndex("full_text_search",["Post"],["title", "content"]);' | cypher-shell --username neo4j --password $NEO4J_PASSWORD +if [[ -z "${NEO4J_PASSWORD}" ]]; then + echo 'CALL db.index.fulltext.createNodeIndex("full_text_search",["Post"],["title", "content"]);' | cypher-shell +else + echo "CALL dbms.security.changePassword('${NEO4J_PASSWORD}');" | cypher-shell --username neo4j --password neo4j + echo 'CALL db.index.fulltext.createNodeIndex("full_text_search",["Post"],["title", "content"]);' | cypher-shell --username neo4j --password $NEO4J_PASSWORD +fi From e87dae4efdb2b7b473239d03ff85802063cd50bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Fri, 15 Feb 2019 00:16:43 +0100 Subject: [PATCH 04/26] Provide README --- README.md | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index b1363a293..7f4676a6c 100644 --- a/README.md +++ b/README.md @@ -3,15 +3,15 @@ > 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 +> 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 @@ -19,10 +19,10 @@ > - more performant and better to understand API > - better API client that uses caching > -> We still need to evaluate the drawbacks and estimate the development +> We still need to evaluate the drawbacks and estimate the development > cost of such an approach -## How to get in touch +## How to get in touch Connect with other developers over [Discord](https://discord.gg/6ub73U3) ## Quick Start @@ -35,6 +35,10 @@ Before you start, fork the repository using the fork button above, then clone it Run: ```sh docker-compose up + +# create indices etc. +docker-compose exec neo4j migrate + # if you want seed data # open another terminal and run docker-compose exec backend yarn run db:seed @@ -116,7 +120,7 @@ Just set `MOCK=true` inside `.env` or pass it on application start. ## Seed and Reset the Database -Optionally you can seed the GraphQL service by executing mutations that +Optionally you can seed the GraphQL service by executing mutations that will write sample data to the database: ```bash @@ -152,5 +156,5 @@ npm run test - [x] check if sorting is working - [x] check if pagination is working - [ ] check if upload is working (using graphql-yoga?) -- [x] evaluate middleware +- [x] evaluate middleware - [ ] ignore Posts and Comments by blacklisted Users From 2715f940b5e787c2e4e3f46b057ad8412b1631ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Fri, 15 Feb 2019 00:50:37 +0100 Subject: [PATCH 05/26] Sketch a test --- src/schema.graphql.spec.js | 51 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 src/schema.graphql.spec.js diff --git a/src/schema.graphql.spec.js b/src/schema.graphql.spec.js new file mode 100644 index 000000000..304fdf557 --- /dev/null +++ b/src/schema.graphql.spec.js @@ -0,0 +1,51 @@ +import { request } from 'graphql-request' +import { create, cleanDatabase } from './seed/factories' +import { host } from './jest/helpers' + +describe('filter for searchQuery', () => { + const query = (searchQuery) => { + return ` + { + findPosts(filter: "${searchQuery}", limit: 10) { + title + } + } + ` + } + + describe('given some posts', () => { + beforeEach(async () => { + await create('post', { + title: 'Hamlet', + content: 'To be, or not to be: that is the question' + }) + await create('post', { + title: 'Threepenny Opera', + content: 'And the shark, it has teeth, And it wears them in the face.' + }) + }) + + afterEach(async () => { + await cleanDatabase() + }) + + describe('result set', () => { + describe('includes posts if search term', () => { + it('matches title', async () => { + const data = await request(host, query('Hamlet')) + expect(data).toEqual({findPosts: [{title: 'Hamlet'}]}) + }) + + it('matches a part of the title', async () => { + const data = await request(host, query('let')) + expect(data).toEqual({findPosts: [{title: 'Hamlet'}]}) + }) + + it('matches a part of the content', async () => { + const data = await request(host, query('shark')) + expect(data).toEqual({findPosts: [{title: 'Threepenny Opera'}]}) + }) + }) + }) + }) +}) From 6b5d329759fc6958a05311796fda02decb3a5cf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Fri, 15 Feb 2019 01:46:33 +0100 Subject: [PATCH 06/26] Implement test for search @appinteractive could you have a look if sanitization of search queries work? I created a test and I see "unterminated string" exceptions. This is not what we want! All user input should be escaped. --- src/schema.graphql | 3 +-- src/schema.graphql.spec.js | 18 +++++++++++++++--- src/seed/factories/index.js | 3 ++- src/seed/factories/posts.js | 22 ++++++++++++++++++++++ 4 files changed, 40 insertions(+), 6 deletions(-) create mode 100644 src/seed/factories/posts.js diff --git a/src/schema.graphql b/src/schema.graphql index 472b345d7..ee519fff3 100644 --- a/src/schema.graphql +++ b/src/schema.graphql @@ -3,8 +3,7 @@ type Query { statistics: Statistics! findPosts(filter: String!, limit: Int = 10): [Post]! @cypher( statement: """ - CALL db.index.fulltext.queryNodes( - 'full_text_search', $filter+'~') + CALL db.index.fulltext.queryNodes('full_text_search', $filter+'~') YIELD node AS node RETURN node ORDER BY node.createdAt DESC diff --git a/src/schema.graphql.spec.js b/src/schema.graphql.spec.js index 304fdf557..5fdc67d0c 100644 --- a/src/schema.graphql.spec.js +++ b/src/schema.graphql.spec.js @@ -21,7 +21,7 @@ describe('filter for searchQuery', () => { }) await create('post', { title: 'Threepenny Opera', - content: 'And the shark, it has teeth, And it wears them in the face.' + content: 'And the shark, it has teeth, And it wears them in the face.' }) }) @@ -29,6 +29,18 @@ describe('filter for searchQuery', () => { await cleanDatabase() }) + describe('sanitization', () => { + it('escapes cypher statement', async () => { + await request(host, query(`''); + MATCH (n) OPTIONAL MATCH (n)-[r]-() DELETE n,r; + CALL db.index.fulltext.queryNodes('full_text_search', '' + `)) + console.log(data) + const data = await request(host, query('the')) + expect(data).toEqual({findPosts: [{title: 'Hamlet'}, {title: 'Threepenny Opera'}]}) + }) + }) + describe('result set', () => { describe('includes posts if search term', () => { it('matches title', async () => { @@ -36,8 +48,8 @@ describe('filter for searchQuery', () => { expect(data).toEqual({findPosts: [{title: 'Hamlet'}]}) }) - it('matches a part of the title', async () => { - const data = await request(host, query('let')) + it('matches mistyped title', async () => { + const data = await request(host, query('amlet')) expect(data).toEqual({findPosts: [{title: 'Hamlet'}]}) }) diff --git a/src/seed/factories/index.js b/src/seed/factories/index.js index e62e98869..9c6d4116c 100644 --- a/src/seed/factories/index.js +++ b/src/seed/factories/index.js @@ -20,7 +20,8 @@ const client = new ApolloClient({ const driver = neo4j().getDriver() const builders = { - 'user': require('./users.js').default + 'user': require('./users.js').default, + 'post': require('./posts.js').default } const buildMutation = (model, parameters) => { diff --git a/src/seed/factories/posts.js b/src/seed/factories/posts.js new file mode 100644 index 000000000..366b64155 --- /dev/null +++ b/src/seed/factories/posts.js @@ -0,0 +1,22 @@ +import faker from 'faker' + +export default function (params) { + const { + id = Array.from({length: 3}, () => faker.lorem.word()).join(''), + title = faker.lorem.sentence(), + content = Array.from({length: 10}, () => faker.lorem.sentence()).join(' ') + } = params + return ` + mutation { + ${id}: CreatePost( + id: "${id}", + title: "${title}", + content: "${content}", + image: "https://picsum.photos/1280/1024?image=424", + visibility: public, + disabled: false, + deleted: false + ) { title } + } + ` +} From cadb309ecfadfa671fb1befd47f1eaf661d7c6c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Fri, 15 Feb 2019 02:05:08 +0100 Subject: [PATCH 07/26] Fix lint --- src/schema.graphql.spec.js | 13 ++++++------- src/seed/factories/posts.js | 4 ++-- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/schema.graphql.spec.js b/src/schema.graphql.spec.js index 5fdc67d0c..12abdd3c2 100644 --- a/src/schema.graphql.spec.js +++ b/src/schema.graphql.spec.js @@ -16,11 +16,11 @@ describe('filter for searchQuery', () => { describe('given some posts', () => { beforeEach(async () => { await create('post', { - title: 'Hamlet', + title: 'Hamlet', content: 'To be, or not to be: that is the question' }) await create('post', { - title: 'Threepenny Opera', + title: 'Threepenny Opera', content: 'And the shark, it has teeth, And it wears them in the face.' }) }) @@ -35,9 +35,8 @@ describe('filter for searchQuery', () => { MATCH (n) OPTIONAL MATCH (n)-[r]-() DELETE n,r; CALL db.index.fulltext.queryNodes('full_text_search', '' `)) - console.log(data) const data = await request(host, query('the')) - expect(data).toEqual({findPosts: [{title: 'Hamlet'}, {title: 'Threepenny Opera'}]}) + expect(data).toEqual({ findPosts: [{ title: 'Hamlet' }, { title: 'Threepenny Opera' }] }) }) }) @@ -45,17 +44,17 @@ describe('filter for searchQuery', () => { describe('includes posts if search term', () => { it('matches title', async () => { const data = await request(host, query('Hamlet')) - expect(data).toEqual({findPosts: [{title: 'Hamlet'}]}) + expect(data).toEqual({ findPosts: [{ title: 'Hamlet' }] }) }) it('matches mistyped title', async () => { const data = await request(host, query('amlet')) - expect(data).toEqual({findPosts: [{title: 'Hamlet'}]}) + expect(data).toEqual({ findPosts: [{ title: 'Hamlet' }] }) }) it('matches a part of the content', async () => { const data = await request(host, query('shark')) - expect(data).toEqual({findPosts: [{title: 'Threepenny Opera'}]}) + expect(data).toEqual({ findPosts: [{ title: 'Threepenny Opera' }] }) }) }) }) diff --git a/src/seed/factories/posts.js b/src/seed/factories/posts.js index 366b64155..d3cd46c9a 100644 --- a/src/seed/factories/posts.js +++ b/src/seed/factories/posts.js @@ -2,9 +2,9 @@ import faker from 'faker' export default function (params) { const { - id = Array.from({length: 3}, () => faker.lorem.word()).join(''), + id = Array.from({ length: 3 }, () => faker.lorem.word()).join(''), title = faker.lorem.sentence(), - content = Array.from({length: 10}, () => faker.lorem.sentence()).join(' ') + content = Array.from({ length: 10 }, () => faker.lorem.sentence()).join(' ') } = params return ` mutation { From d3a925cc3b1ec5560256df5437cb0faadeba3c12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Fri, 15 Feb 2019 02:10:30 +0100 Subject: [PATCH 08/26] Create indices before testing --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index f4a01b147..f106ed5e8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,10 +16,12 @@ before_install: - curl -L https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` > docker-compose - chmod +x docker-compose - sudo mv docker-compose /usr/local/bin + - yarn global add wait-on install: - docker build --build-arg BUILD_COMMIT=$TRAVIS_COMMIT --target production -t humanconnection/nitro-backend:latest . - docker-compose -f docker-compose.yml -f docker-compose.travis.yml up -d + - wait-on tcp:7687 && docker-compose exec neo4j migrate script: - docker-compose exec backend yarn run lint From af536fac61faf96ee4470a04e52d8a0c9cb7023b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Fri, 15 Feb 2019 02:20:28 +0100 Subject: [PATCH 09/26] On Travis we have to expose the port ...to be able to wait for it. --- docker-compose.travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docker-compose.travis.yml b/docker-compose.travis.yml index 761a2aa64..bc26a2a34 100644 --- a/docker-compose.travis.yml +++ b/docker-compose.travis.yml @@ -1,6 +1,9 @@ version: "3.7" services: + neo4j: + ports: + - 7687:7687 backend: image: humanconnection/nitro-backend:builder build: From 3cc19800c9b10085182b1a039107b34aa87a6bb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Fri, 15 Feb 2019 02:29:09 +0100 Subject: [PATCH 10/26] Do we need to wait until the web client is up? --- .travis.yml | 2 +- docker-compose.travis.yml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index f106ed5e8..e699197cf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,7 +21,7 @@ before_install: install: - docker build --build-arg BUILD_COMMIT=$TRAVIS_COMMIT --target production -t humanconnection/nitro-backend:latest . - docker-compose -f docker-compose.yml -f docker-compose.travis.yml up -d - - wait-on tcp:7687 && docker-compose exec neo4j migrate + - wait-on http://localhost:7474 && docker-compose exec neo4j migrate script: - docker-compose exec backend yarn run lint diff --git a/docker-compose.travis.yml b/docker-compose.travis.yml index bc26a2a34..1553347fa 100644 --- a/docker-compose.travis.yml +++ b/docker-compose.travis.yml @@ -4,6 +4,7 @@ services: neo4j: ports: - 7687:7687 + - 7474:7474 backend: image: humanconnection/nitro-backend:builder build: From 55ee112dfa11096a8aa61aa9331e39c1869f7692 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Fri, 15 Feb 2019 02:36:07 +0100 Subject: [PATCH 11/26] Disable authentication on Travis --- docker-compose.travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker-compose.travis.yml b/docker-compose.travis.yml index 1553347fa..e1998f6dd 100644 --- a/docker-compose.travis.yml +++ b/docker-compose.travis.yml @@ -2,6 +2,8 @@ version: "3.7" services: neo4j: + environment: + - NEO4J_AUTH=none ports: - 7687:7687 - 7474:7474 From da804e26ae44b34383e0eb096a49983b165c6f12 Mon Sep 17 00:00:00 2001 From: Grzegorz Leoniec Date: Sun, 17 Feb 2019 17:32:14 +0100 Subject: [PATCH 12/26] Fix search --- src/schema.graphql | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/schema.graphql b/src/schema.graphql index ee519fff3..e9b5d5106 100644 --- a/src/schema.graphql +++ b/src/schema.graphql @@ -3,10 +3,9 @@ type Query { statistics: Statistics! findPosts(filter: String!, limit: Int = 10): [Post]! @cypher( statement: """ - CALL db.index.fulltext.queryNodes('full_text_search', $filter+'~') - YIELD node AS node + CALL db.index.fulltext.queryNodes('full_text_search', $filter) + YIELD node RETURN node - ORDER BY node.createdAt DESC LIMIT $limit """ ) From cbbd33301f04c2052d0faf2a3545e294bf8be1da Mon Sep 17 00:00:00 2001 From: Grzegorz Leoniec Date: Mon, 18 Feb 2019 16:57:17 +0100 Subject: [PATCH 13/26] Improved search query --- src/schema.graphql | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/schema.graphql b/src/schema.graphql index e9b5d5106..1e09768cb 100644 --- a/src/schema.graphql +++ b/src/schema.graphql @@ -4,8 +4,12 @@ type Query { findPosts(filter: String!, limit: Int = 10): [Post]! @cypher( statement: """ CALL db.index.fulltext.queryNodes('full_text_search', $filter) - YIELD node - RETURN node + YIELD node as post, score + MATCH (post)<-[:WROTE]-(user:User) + WHERE score >= 0.2 + AND NOT user.deleted = true AND NOT user.disabled = true + AND NOT post.deleted = true AND NOT post.disabled = true + RETURN post LIMIT $limit """ ) From 832a778ca1294c532cf6a3d67cd54efe84e39578 Mon Sep 17 00:00:00 2001 From: Grzegorz Leoniec Date: Wed, 13 Feb 2019 16:02:25 +0100 Subject: [PATCH 14/26] Added findPost query --- src/schema.graphql | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/schema.graphql b/src/schema.graphql index 55f23d5ca..95e7b3d48 100644 --- a/src/schema.graphql +++ b/src/schema.graphql @@ -1,6 +1,16 @@ type Query { isLoggedIn: Boolean! statistics: Statistics! + findPosts(filter: String!, limit: Int = 10): [Post]! @cypher( + statement: """ + CALL db.index.fulltext.queryNodes( + 'postTitleAndContent', $filter+'~') + YIELD node AS node + RETURN node + ORDER BY node.createdAt DESC + LIMIT $limit + """ + ) } type Mutation { login(email: String!, password: String!): LoggedInUser From 46436ca9b10a20cc80b9de2f5ff0304375ac6b81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Thu, 14 Feb 2019 16:05:30 +0100 Subject: [PATCH 15/26] Setup a routine how to create indices initially In order to create the indices programmatically we need to change the default password for security concerns. To create the user we need to start the neo4j database. So I decided to provide a bash script that let us do it once the container are started. In production we must change the NEO4J_PASSWORD. --- README.md | 2 +- docker-compose.override.yml | 7 +++++++ docker-compose.prod.yml | 9 +++++++++ docker-compose.yml | 7 ------- neo4j/Dockerfile | 1 + neo4j/migrate.sh | 4 ++++ src/bootstrap/neo4j.js | 22 +++++++++++----------- src/graphql-schema.js | 2 +- src/schema.graphql | 2 +- 9 files changed, 35 insertions(+), 21 deletions(-) create mode 100644 docker-compose.prod.yml create mode 100755 neo4j/migrate.sh diff --git a/README.md b/README.md index b3b4764b7..b9b90b0a8 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,7 @@ _.env_ ```yaml NEO4J_URI=bolt://localhost:7687 -NEO4J_USER=neo4j +NEO4J_USERNAME=neo4j NEO4J_PASSWORD=letmein ``` diff --git a/docker-compose.override.yml b/docker-compose.override.yml index ef7d52c7e..b972c31f6 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -11,6 +11,13 @@ services: - /nitro-backend/node_modules command: yarn run dev neo4j: + environment: + - NEO4J_AUTH=none ports: - 7687:7687 - 7474:7474 + volumes: + - neo4j-data:/data + +volumes: + neo4j-data: diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 000000000..c4f5dc4f5 --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,9 @@ +version: "3.7" + +services: + neo4j: + environment: + - NEO4J_PASSWORD=letmein + backend: + environment: + - NEO4J_PASSWORD=letmein diff --git a/docker-compose.yml b/docker-compose.yml index 6905bb893..1e8c9158c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -27,14 +27,7 @@ services: context: neo4j networks: - hc-network - volumes: - - neo4j-data:/data - environment: - - NEO4J_AUTH=none networks: hc-network: name: hc-network - -volumes: - neo4j-data: diff --git a/neo4j/Dockerfile b/neo4j/Dockerfile index cb7fd228f..f6e71811b 100644 --- a/neo4j/Dockerfile +++ b/neo4j/Dockerfile @@ -1,2 +1,3 @@ FROM neo4j:3.5.0 RUN wget https://github.com/neo4j-contrib/neo4j-apoc-procedures/releases/download/3.5.0.1/apoc-3.5.0.1-all.jar -P plugins/ +COPY migrate.sh /usr/local/bin/migrate diff --git a/neo4j/migrate.sh b/neo4j/migrate.sh new file mode 100755 index 000000000..30a58c306 --- /dev/null +++ b/neo4j/migrate.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -e +echo "CALL dbms.security.changePassword('${NEO4J_PASSWORD}');" | cypher-shell --username neo4j --password neo4j +echo 'CALL db.index.fulltext.createNodeIndex("full_text_search",["Post"],["title", "content"]);' | cypher-shell --username neo4j --password $NEO4J_PASSWORD diff --git a/src/bootstrap/neo4j.js b/src/bootstrap/neo4j.js index 935449a0a..62f5f326d 100644 --- a/src/bootstrap/neo4j.js +++ b/src/bootstrap/neo4j.js @@ -1,18 +1,18 @@ -import { v1 as neo4j } from 'neo4j-driver' -import dotenv from 'dotenv' +import { v1 as neo4j } from "neo4j-driver"; +import dotenv from "dotenv"; -dotenv.config() +dotenv.config(); -let driver +let driver; -export function getDriver (options = {}) { +export function getDriver(options = {}) { const { - uri = process.env.NEO4J_URI || 'bolt://localhost:7687', - username = process.env.NEO4J_USERNAME || 'neo4j', - password = process.env.NEO4J_PASSWORD || 'neo4j' - } = options + uri = process.env.NEO4J_URI || "bolt://localhost:7687", + username = process.env.NEO4J_USERNAME || "neo4j", + password = process.env.NEO4J_PASSWORD || "neo4j" + } = options; if (!driver) { - driver = neo4j.driver(uri, neo4j.auth.basic(username, password)) + driver = neo4j.driver(uri, neo4j.auth.basic(username, password)); } - return driver + return driver; } diff --git a/src/graphql-schema.js b/src/graphql-schema.js index 8b5f369e0..c525d67d7 100644 --- a/src/graphql-schema.js +++ b/src/graphql-schema.js @@ -34,7 +34,7 @@ export const query = (cypher, session) => { }) }) } -const queryOne = (cypher, session) => { +export const queryOne = (cypher, session) => { return new Promise((resolve, reject) => { query(cypher, session) .then(res => { diff --git a/src/schema.graphql b/src/schema.graphql index 95e7b3d48..472b345d7 100644 --- a/src/schema.graphql +++ b/src/schema.graphql @@ -4,7 +4,7 @@ type Query { findPosts(filter: String!, limit: Int = 10): [Post]! @cypher( statement: """ CALL db.index.fulltext.queryNodes( - 'postTitleAndContent', $filter+'~') + 'full_text_search', $filter+'~') YIELD node AS node RETURN node ORDER BY node.createdAt DESC From 34ecd07b1bf746211fbe34b777f5ca810fa690e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Thu, 14 Feb 2019 17:30:17 +0100 Subject: [PATCH 16/26] Modify migrate for development environment --- neo4j/migrate.sh | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/neo4j/migrate.sh b/neo4j/migrate.sh index 30a58c306..cdb9ec46c 100755 --- a/neo4j/migrate.sh +++ b/neo4j/migrate.sh @@ -1,4 +1,8 @@ #!/usr/bin/env bash set -e -echo "CALL dbms.security.changePassword('${NEO4J_PASSWORD}');" | cypher-shell --username neo4j --password neo4j -echo 'CALL db.index.fulltext.createNodeIndex("full_text_search",["Post"],["title", "content"]);' | cypher-shell --username neo4j --password $NEO4J_PASSWORD +if [[ -z "${NEO4J_PASSWORD}" ]]; then + echo 'CALL db.index.fulltext.createNodeIndex("full_text_search",["Post"],["title", "content"]);' | cypher-shell +else + echo "CALL dbms.security.changePassword('${NEO4J_PASSWORD}');" | cypher-shell --username neo4j --password neo4j + echo 'CALL db.index.fulltext.createNodeIndex("full_text_search",["Post"],["title", "content"]);' | cypher-shell --username neo4j --password $NEO4J_PASSWORD +fi From 151bb2842985c7d1475ef7bd070ce4b1547e77bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Fri, 15 Feb 2019 00:16:43 +0100 Subject: [PATCH 17/26] Provide README --- README.md | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index b9b90b0a8..2717e7ed5 100644 --- a/README.md +++ b/README.md @@ -10,15 +10,15 @@ > 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 +> 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 @@ -26,10 +26,10 @@ > - more performant and better to understand API > - better API client that uses caching > -> We still need to evaluate the drawbacks and estimate the development +> We still need to evaluate the drawbacks and estimate the development > cost of such an approach -## How to get in touch +## How to get in touch Connect with other developers over [Discord](https://discord.gg/6ub73U3) ## Quick Start @@ -42,6 +42,10 @@ Before you start, fork the repository using the fork button above, then clone it Run: ```sh docker-compose up + +# create indices etc. +docker-compose exec neo4j migrate + # if you want seed data # open another terminal and run docker-compose exec backend yarn run db:seed @@ -123,7 +127,7 @@ Just set `MOCK=true` inside `.env` or pass it on application start. ## Seed and Reset the Database -Optionally you can seed the GraphQL service by executing mutations that +Optionally you can seed the GraphQL service by executing mutations that will write sample data to the database: ```bash @@ -159,7 +163,7 @@ npm run test - [x] check if sorting is working - [x] check if pagination is working - [ ] check if upload is working (using graphql-yoga?) -- [x] evaluate middleware +- [x] evaluate middleware - [ ] ignore Posts and Comments by blacklisted Users From 5230099e6b3d76542acc48d3054d596c1f70c4d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Fri, 15 Feb 2019 00:50:37 +0100 Subject: [PATCH 18/26] Sketch a test --- src/schema.graphql.spec.js | 51 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 src/schema.graphql.spec.js diff --git a/src/schema.graphql.spec.js b/src/schema.graphql.spec.js new file mode 100644 index 000000000..304fdf557 --- /dev/null +++ b/src/schema.graphql.spec.js @@ -0,0 +1,51 @@ +import { request } from 'graphql-request' +import { create, cleanDatabase } from './seed/factories' +import { host } from './jest/helpers' + +describe('filter for searchQuery', () => { + const query = (searchQuery) => { + return ` + { + findPosts(filter: "${searchQuery}", limit: 10) { + title + } + } + ` + } + + describe('given some posts', () => { + beforeEach(async () => { + await create('post', { + title: 'Hamlet', + content: 'To be, or not to be: that is the question' + }) + await create('post', { + title: 'Threepenny Opera', + content: 'And the shark, it has teeth, And it wears them in the face.' + }) + }) + + afterEach(async () => { + await cleanDatabase() + }) + + describe('result set', () => { + describe('includes posts if search term', () => { + it('matches title', async () => { + const data = await request(host, query('Hamlet')) + expect(data).toEqual({findPosts: [{title: 'Hamlet'}]}) + }) + + it('matches a part of the title', async () => { + const data = await request(host, query('let')) + expect(data).toEqual({findPosts: [{title: 'Hamlet'}]}) + }) + + it('matches a part of the content', async () => { + const data = await request(host, query('shark')) + expect(data).toEqual({findPosts: [{title: 'Threepenny Opera'}]}) + }) + }) + }) + }) +}) From 5a995f9f86f2d484794faa883de23461d85b8014 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Fri, 15 Feb 2019 01:46:33 +0100 Subject: [PATCH 19/26] Implement test for search @appinteractive could you have a look if sanitization of search queries work? I created a test and I see "unterminated string" exceptions. This is not what we want! All user input should be escaped. --- src/schema.graphql | 3 +- src/schema.graphql.spec.js | 18 ++++- src/seed/factories/index.js | 141 ++++++++++++++++++------------------ src/seed/factories/posts.js | 20 ++--- 4 files changed, 96 insertions(+), 86 deletions(-) diff --git a/src/schema.graphql b/src/schema.graphql index 472b345d7..ee519fff3 100644 --- a/src/schema.graphql +++ b/src/schema.graphql @@ -3,8 +3,7 @@ type Query { statistics: Statistics! findPosts(filter: String!, limit: Int = 10): [Post]! @cypher( statement: """ - CALL db.index.fulltext.queryNodes( - 'full_text_search', $filter+'~') + CALL db.index.fulltext.queryNodes('full_text_search', $filter+'~') YIELD node AS node RETURN node ORDER BY node.createdAt DESC diff --git a/src/schema.graphql.spec.js b/src/schema.graphql.spec.js index 304fdf557..5fdc67d0c 100644 --- a/src/schema.graphql.spec.js +++ b/src/schema.graphql.spec.js @@ -21,7 +21,7 @@ describe('filter for searchQuery', () => { }) await create('post', { title: 'Threepenny Opera', - content: 'And the shark, it has teeth, And it wears them in the face.' + content: 'And the shark, it has teeth, And it wears them in the face.' }) }) @@ -29,6 +29,18 @@ describe('filter for searchQuery', () => { await cleanDatabase() }) + describe('sanitization', () => { + it('escapes cypher statement', async () => { + await request(host, query(`''); + MATCH (n) OPTIONAL MATCH (n)-[r]-() DELETE n,r; + CALL db.index.fulltext.queryNodes('full_text_search', '' + `)) + console.log(data) + const data = await request(host, query('the')) + expect(data).toEqual({findPosts: [{title: 'Hamlet'}, {title: 'Threepenny Opera'}]}) + }) + }) + describe('result set', () => { describe('includes posts if search term', () => { it('matches title', async () => { @@ -36,8 +48,8 @@ describe('filter for searchQuery', () => { expect(data).toEqual({findPosts: [{title: 'Hamlet'}]}) }) - it('matches a part of the title', async () => { - const data = await request(host, query('let')) + it('matches mistyped title', async () => { + const data = await request(host, query('amlet')) expect(data).toEqual({findPosts: [{title: 'Hamlet'}]}) }) diff --git a/src/seed/factories/index.js b/src/seed/factories/index.js index a107fc6b7..fb59281fc 100644 --- a/src/seed/factories/index.js +++ b/src/seed/factories/index.js @@ -1,7 +1,16 @@ -import { GraphQLClient, request } from 'graphql-request' -import { getDriver } from '../../bootstrap/neo4j' +import { GraphQLClient, request } from "graphql-request"; +import { getDriver } from "../../bootstrap/neo4j"; -export const seedServerHost = 'http://127.0.0.1:4001' +import createBadge from "./badges.js"; +import createUser from "./users.js"; +import createOrganization from "./organizations.js"; +import createPost from "./posts.js"; +import createComment from "./comments.js"; +import createCategory from "./categories.js"; +import createTag from "./tags.js"; +import createReport from "./reports.js"; + +export const seedServerHost = "http://127.0.0.1:4001"; const authenticatedHeaders = async ({ email, password }, host) => { const mutation = ` @@ -9,95 +18,85 @@ const authenticatedHeaders = async ({ email, password }, host) => { login(email:"${email}", password:"${password}"){ token } - }` - const response = await request(host, mutation) + }`; + const response = await request(host, mutation); return { authorization: `Bearer ${response.login.token}` - } -} - + }; +}; const factories = { - 'badge': require('./badges.js').default, - 'user': require('./users.js').default, - 'organization': require('./organizations.js').default, - 'post': require('./posts.js').default, - 'comment': require('./comments.js').default, - 'category': require('./categories.js').default, - 'tag': require('./tags.js').default, - 'report': require('./reports.js').default -} - -const relationFactories = { - 'user': require('./users.js').relate, - 'organization': require('./organizations.js').relate, - 'post': require('./posts.js').relate, - 'comment': require('./comments.js').relate -} - -export const create = (model, parameters, options) => { - const graphQLClient = new GraphQLClient(seedServerHost, options) - const mutation = factories[model](parameters) - return graphQLClient.request(mutation) -} - -export const relate = (model, type, parameters, options) => { - const graphQLClient = new GraphQLClient(seedServerHost, options) - const mutation = relationFactories[model](type, parameters) - return graphQLClient.request(mutation) -} + Badge: createBadge, + User: createUser, + Organization: createOrganization, + Post: createPost, + Comment: createComment, + Category: createCategory, + Tag: createTag, + Report: createReport +}; export const cleanDatabase = async (options = {}) => { - const { - driver = getDriver() - } = options - const session = driver.session() - const cypher = 'MATCH (n) DETACH DELETE n' + const { driver = getDriver() } = options; + const session = driver.session(); + const cypher = "MATCH (n) DETACH DELETE n"; try { - return await session.run(cypher) + return await session.run(cypher); } catch (error) { - throw (error) + throw error; } finally { - session.close() + session.close(); } -} +}; -export default function Factory (options = {}) { +export default function Factory(options = {}) { const { neo4jDriver = getDriver(), - seedServerHost = 'http://127.0.0.1:4001' - } = options + seedServerHost = "http://127.0.0.1:4001" + } = options; - const graphQLClient = new GraphQLClient(seedServerHost) + const graphQLClient = new GraphQLClient(seedServerHost); const result = { neo4jDriver, seedServerHost, graphQLClient, + factories, lastResponse: null, - async authenticateAs ({ email, password }) { - const headers = await authenticatedHeaders({ email, password }, seedServerHost) - this.lastResponse = headers - this.graphQLClient = new GraphQLClient(seedServerHost, { headers }) - return this + async authenticateAs({ email, password }) { + const headers = await authenticatedHeaders( + { email, password }, + seedServerHost + ); + this.lastResponse = headers; + this.graphQLClient = new GraphQLClient(seedServerHost, { headers }); + return this; }, - async create (node, properties) { - const mutation = factories[node](properties) - this.lastResponse = await this.graphQLClient.request(mutation) - return this + async create(node, properties) { + const mutation = this.factories[node](properties); + this.lastResponse = await this.graphQLClient.request(mutation); + return this; }, - async relate (node, relationship, properties) { - const mutation = relationFactories[node](relationship, properties) - this.lastResponse = await this.graphQLClient.request(mutation) - return this + async relate(node, relationship, properties) { + const { from, to } = properties; + const mutation = ` + mutation { + Add${node}${relationship}( + from: { id: "${from}" }, + to: { id: "${to}" } + ) { from { id } } + } + `; + this.lastResponse = await this.graphQLClient.request(mutation); + return this; }, - async cleanDatabase () { - this.lastResponse = await cleanDatabase({ driver: this.neo4jDriver }) - return this + async cleanDatabase() { + this.lastResponse = await cleanDatabase({ driver: this.neo4jDriver }); + return this; } - } - result.authenticateAs.bind(result) - result.create.bind(result) - result.relate.bind(result) - result.cleanDatabase.bind(result) - return result + }; + result.authenticateAs.bind(result); + result.create.bind(result); + result.relate.bind(result); + result.cleanDatabase.bind(result); + return result; } diff --git a/src/seed/factories/posts.js b/src/seed/factories/posts.js index 80f5e289d..179f17a9e 100644 --- a/src/seed/factories/posts.js +++ b/src/seed/factories/posts.js @@ -1,7 +1,7 @@ -import faker from 'faker' -import uuid from 'uuid/v4' +import faker from "faker"; +import uuid from "uuid/v4"; -export default function (params) { +export default function(params) { const { id = uuid(), title = faker.lorem.sentence(), @@ -11,12 +11,12 @@ export default function (params) { faker.lorem.sentence(), faker.lorem.sentence(), faker.lorem.sentence() - ].join('. '), + ].join(". "), image = faker.image.image(), - visibility = 'public', + visibility = "public", disabled = false, deleted = false - } = params + } = params; return ` mutation { @@ -30,11 +30,11 @@ export default function (params) { deleted: ${deleted} ) { title, content } } - ` + `; } -export function relate (type, params) { - const { from, to } = params +export function relate(type, params) { + const { from, to } = params; return ` mutation { ${from}_${type}_${to}: AddPost${type}( @@ -42,5 +42,5 @@ export function relate (type, params) { to: { id: "${to}" } ) { from { id } } } - ` + `; } From 63845681b744d5fef94f50a9dd56979c8be0df3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Fri, 15 Feb 2019 02:05:08 +0100 Subject: [PATCH 20/26] Fix lint --- src/schema.graphql.spec.js | 13 ++++++------- src/seed/factories/posts.js | 12 ------------ 2 files changed, 6 insertions(+), 19 deletions(-) diff --git a/src/schema.graphql.spec.js b/src/schema.graphql.spec.js index 5fdc67d0c..12abdd3c2 100644 --- a/src/schema.graphql.spec.js +++ b/src/schema.graphql.spec.js @@ -16,11 +16,11 @@ describe('filter for searchQuery', () => { describe('given some posts', () => { beforeEach(async () => { await create('post', { - title: 'Hamlet', + title: 'Hamlet', content: 'To be, or not to be: that is the question' }) await create('post', { - title: 'Threepenny Opera', + title: 'Threepenny Opera', content: 'And the shark, it has teeth, And it wears them in the face.' }) }) @@ -35,9 +35,8 @@ describe('filter for searchQuery', () => { MATCH (n) OPTIONAL MATCH (n)-[r]-() DELETE n,r; CALL db.index.fulltext.queryNodes('full_text_search', '' `)) - console.log(data) const data = await request(host, query('the')) - expect(data).toEqual({findPosts: [{title: 'Hamlet'}, {title: 'Threepenny Opera'}]}) + expect(data).toEqual({ findPosts: [{ title: 'Hamlet' }, { title: 'Threepenny Opera' }] }) }) }) @@ -45,17 +44,17 @@ describe('filter for searchQuery', () => { describe('includes posts if search term', () => { it('matches title', async () => { const data = await request(host, query('Hamlet')) - expect(data).toEqual({findPosts: [{title: 'Hamlet'}]}) + expect(data).toEqual({ findPosts: [{ title: 'Hamlet' }] }) }) it('matches mistyped title', async () => { const data = await request(host, query('amlet')) - expect(data).toEqual({findPosts: [{title: 'Hamlet'}]}) + expect(data).toEqual({ findPosts: [{ title: 'Hamlet' }] }) }) it('matches a part of the content', async () => { const data = await request(host, query('shark')) - expect(data).toEqual({findPosts: [{title: 'Threepenny Opera'}]}) + expect(data).toEqual({ findPosts: [{ title: 'Threepenny Opera' }] }) }) }) }) diff --git a/src/seed/factories/posts.js b/src/seed/factories/posts.js index 179f17a9e..d77a7cad0 100644 --- a/src/seed/factories/posts.js +++ b/src/seed/factories/posts.js @@ -32,15 +32,3 @@ export default function(params) { } `; } - -export function relate(type, params) { - const { from, to } = params; - return ` - mutation { - ${from}_${type}_${to}: AddPost${type}( - from: { id: "${from}" }, - to: { id: "${to}" } - ) { from { id } } - } - `; -} From 14c61b3bf820db42363b4573396faee86067112c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Fri, 15 Feb 2019 02:10:30 +0100 Subject: [PATCH 21/26] Create indices before testing --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index f4a01b147..f106ed5e8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,10 +16,12 @@ before_install: - curl -L https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` > docker-compose - chmod +x docker-compose - sudo mv docker-compose /usr/local/bin + - yarn global add wait-on install: - docker build --build-arg BUILD_COMMIT=$TRAVIS_COMMIT --target production -t humanconnection/nitro-backend:latest . - docker-compose -f docker-compose.yml -f docker-compose.travis.yml up -d + - wait-on tcp:7687 && docker-compose exec neo4j migrate script: - docker-compose exec backend yarn run lint From a41063a60aeaa3b099d53f4230a7ff69ed701696 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Fri, 15 Feb 2019 02:20:28 +0100 Subject: [PATCH 22/26] On Travis we have to expose the port ...to be able to wait for it. --- docker-compose.travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docker-compose.travis.yml b/docker-compose.travis.yml index 761a2aa64..bc26a2a34 100644 --- a/docker-compose.travis.yml +++ b/docker-compose.travis.yml @@ -1,6 +1,9 @@ version: "3.7" services: + neo4j: + ports: + - 7687:7687 backend: image: humanconnection/nitro-backend:builder build: From 8453d95f68d3107e321072b68b26db331456c2e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Fri, 15 Feb 2019 02:29:09 +0100 Subject: [PATCH 23/26] Do we need to wait until the web client is up? --- .travis.yml | 2 +- docker-compose.travis.yml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index f106ed5e8..e699197cf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,7 +21,7 @@ before_install: install: - docker build --build-arg BUILD_COMMIT=$TRAVIS_COMMIT --target production -t humanconnection/nitro-backend:latest . - docker-compose -f docker-compose.yml -f docker-compose.travis.yml up -d - - wait-on tcp:7687 && docker-compose exec neo4j migrate + - wait-on http://localhost:7474 && docker-compose exec neo4j migrate script: - docker-compose exec backend yarn run lint diff --git a/docker-compose.travis.yml b/docker-compose.travis.yml index bc26a2a34..1553347fa 100644 --- a/docker-compose.travis.yml +++ b/docker-compose.travis.yml @@ -4,6 +4,7 @@ services: neo4j: ports: - 7687:7687 + - 7474:7474 backend: image: humanconnection/nitro-backend:builder build: From e7c01d8877644b8920690de5bc045a6e68b5d406 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Fri, 15 Feb 2019 02:36:07 +0100 Subject: [PATCH 24/26] Disable authentication on Travis --- docker-compose.travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker-compose.travis.yml b/docker-compose.travis.yml index 1553347fa..e1998f6dd 100644 --- a/docker-compose.travis.yml +++ b/docker-compose.travis.yml @@ -2,6 +2,8 @@ version: "3.7" services: neo4j: + environment: + - NEO4J_AUTH=none ports: - 7687:7687 - 7474:7474 From 360acdb141ece84fa5cda7f9341848d20bebd04d Mon Sep 17 00:00:00 2001 From: Grzegorz Leoniec Date: Sun, 17 Feb 2019 17:32:14 +0100 Subject: [PATCH 25/26] Fix search --- src/schema.graphql | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/schema.graphql b/src/schema.graphql index ee519fff3..e9b5d5106 100644 --- a/src/schema.graphql +++ b/src/schema.graphql @@ -3,10 +3,9 @@ type Query { statistics: Statistics! findPosts(filter: String!, limit: Int = 10): [Post]! @cypher( statement: """ - CALL db.index.fulltext.queryNodes('full_text_search', $filter+'~') - YIELD node AS node + CALL db.index.fulltext.queryNodes('full_text_search', $filter) + YIELD node RETURN node - ORDER BY node.createdAt DESC LIMIT $limit """ ) From 6a146aa699da7752a280a34d0372c8712caa6d38 Mon Sep 17 00:00:00 2001 From: Grzegorz Leoniec Date: Mon, 18 Feb 2019 16:57:17 +0100 Subject: [PATCH 26/26] Improved search query --- src/schema.graphql | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/schema.graphql b/src/schema.graphql index e9b5d5106..1e09768cb 100644 --- a/src/schema.graphql +++ b/src/schema.graphql @@ -4,8 +4,12 @@ type Query { findPosts(filter: String!, limit: Int = 10): [Post]! @cypher( statement: """ CALL db.index.fulltext.queryNodes('full_text_search', $filter) - YIELD node - RETURN node + YIELD node as post, score + MATCH (post)<-[:WROTE]-(user:User) + WHERE score >= 0.2 + AND NOT user.deleted = true AND NOT user.disabled = true + AND NOT post.deleted = true AND NOT post.disabled = true + RETURN post LIMIT $limit """ )