├── .babelrc
├── .circleci
└── config.yml
├── .gitignore
├── .npmignore
├── .prettierignore
├── .vscode
└── launch.json
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── docs
├── README.md
├── apollo-federation.md
├── graphql-custom-logic.md
├── graphql-filtering.md
├── graphql-interface-union-types.md
├── graphql-relationship-types.md
├── graphql-schema-directives.md
├── graphql-schema-generation-augmentation.md
├── graphql-spatial-types.md
├── graphql-temporal-types-datetime.md
├── img
│ ├── BatchMergeAGraphData.png
│ ├── MergeAGraphData.png
│ ├── exampleDataGraph.png
│ ├── interface-data.png
│ ├── interface-model.png
│ ├── movies.png
│ └── union-data.png
├── infer-graphql-schema-database.md
├── neo4j-graphql-js-api.md
├── neo4j-graphql-js-middleware-authorization.md
├── neo4j-graphql-js-quickstart.md
├── neo4j-graphql-js.md
└── neo4j-multiple-database-graphql.md
├── example
├── apollo-federation
│ ├── gateway.js
│ ├── seed-data.js
│ └── services
│ │ ├── accounts
│ │ └── index.js
│ │ ├── inventory
│ │ └── index.js
│ │ ├── products
│ │ └── index.js
│ │ └── reviews
│ │ └── index.js
├── apollo-server
│ ├── authScopes.js
│ ├── bookmarks.js
│ ├── interface-union-example.js
│ ├── movies-middleware.js
│ ├── movies-schema.js
│ ├── movies-typedefs.js
│ ├── movies.js
│ └── multidatabase.js
└── autogenerated
│ ├── autogen-middleware.js
│ └── autogen.js
├── index.d.ts
├── package-lock.json
├── package.json
├── scripts
├── install-neo4j.sh
├── start-neo4j.sh
├── stop-and-clear-neo4j.sh
└── wait-for-graphql.sh
├── src
├── augment
│ ├── ast.js
│ ├── augment.js
│ ├── directives.js
│ ├── fields.js
│ ├── input-values.js
│ ├── resolvers.js
│ └── types
│ │ ├── node
│ │ ├── mutation.js
│ │ ├── node.js
│ │ ├── query.js
│ │ └── selection.js
│ │ ├── relationship
│ │ ├── mutation.js
│ │ ├── query.js
│ │ └── relationship.js
│ │ ├── spatial.js
│ │ ├── temporal.js
│ │ └── types.js
├── auth.js
├── federation.js
├── index.js
├── inferSchema.js
├── neo4j-schema
│ ├── Neo4jSchemaTree.js
│ ├── entities.js
│ ├── graphQLMapper.js
│ └── types.js
├── schemaAssert.js
├── schemaSearch.js
├── selections.js
├── translate
│ ├── mutation.js
│ └── translate.js
└── utils.js
└── test
├── helpers
├── configTestHelpers.js
├── custom
│ ├── customSchemaTest.js
│ └── testSchema.js
├── cypherTestHelpers.js
├── driverFakes.js
├── experimental
│ ├── augmentSchemaTest.js
│ ├── custom
│ │ ├── customSchemaTest.js
│ │ └── testSchema.js
│ └── testSchema.js
├── filterTestHelpers.js
├── tck
│ ├── filterTck.md
│ ├── parseTck.js
│ └── parser.js
└── testSchema.js
├── integration
├── gateway.test.js
├── integration.test.js
└── test-middleware.test.js
├── tck
└── .gitkeep
└── unit
├── assertSchema.test.js
├── augmentSchemaTest.test.js
├── configTest.test.js
├── custom
└── cypherTest.test.js
├── cypherTest.test.js
├── experimental
├── augmentSchemaTest.test.js
├── custom
│ └── cypherTest.test.js
└── cypherTest.test.js
├── filterTest.test.js
├── neo4j-schema
├── Neo4jSchemaTreeTest.test.js
├── entitiesTest.test.js
├── graphQLMapperTest.test.js
└── typesTest.test.js
└── searchSchema.test.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "@babel/preset-env"
4 | ],
5 | "plugins": [
6 | [
7 | "@babel/plugin-transform-runtime",
8 | {
9 | "corejs": 2
10 | }
11 | ],
12 | "@babel/plugin-proposal-async-generator-functions",
13 | "@babel/plugin-proposal-object-rest-spread"
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (https://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # Typescript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # Yarn Integrity file
55 | .yarn-integrity
56 |
57 | # dotenv environment variables file
58 | .env
59 |
60 | .idea
61 |
62 | .DS_Store
63 |
64 | dist/
65 |
66 | node-version
67 | neo4j-version
68 |
69 | test/tck/*
70 | !test/tck/.gitkeep
71 |
72 | .history
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | src
2 | test
3 | .DS_Store
4 | .idea
5 | scripts
6 | .circleci
7 | .nyc_output
8 | .vscode
9 | coverage
10 | example
11 | .babelrc
12 | .prettierignore
13 | coverage.lcov
14 | scratch.md
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | package.json
2 | package-lock.json
3 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "type": "node",
6 | "request": "launch",
7 | "name": "Launch debug via NPM",
8 | "runtimeExecutable": "npm",
9 | "runtimeArgs": ["run-script", "debug"],
10 | "port": 9229
11 | },
12 | {
13 | "type": "node",
14 | "request": "launch",
15 | "name": "Movie-typedefs debug",
16 | "runtimeExecutable": "npm",
17 | "runtimeArgs": ["run-script", "debug-typedefs"],
18 | "port": 9229
19 | },
20 | {
21 | "type": "node",
22 | "request": "launch",
23 | "name": "interface errors",
24 | "runtimeExecutable": "npm",
25 | "runtimeArgs": ["run-script", "debug-interface"],
26 | "port": 9229
27 | }
28 | ]
29 | }
30 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | Thanks so much for thinking of contributing to `neo4j-graphql-js`, we really
4 | appreciate it! :heart:
5 |
6 | ## Get in Touch
7 |
8 | There's an active [mailing list](https://groups.google.com/forum/#!forum/neo4j)
9 | and [Slack channel](https://neo4j.com/slack) where we work directly with the
10 | community.
11 | If you're not already a member, sign up!
12 |
13 | We love our community and wouldn't be where we are without you.
14 |
15 | ## Getting Set Up
16 |
17 | Clone the repository, install dependencies and build the project:
18 |
19 | ```
20 | git clone git@github.com:neo4j-graphql/neo4j-graphql-js.git
21 | cd neo4j-graphql-js
22 | npm install
23 | npm run build
24 | ```
25 |
26 | ### Testing
27 |
28 | We use the `ava` test runner. Run the tests with:
29 |
30 | ```
31 | npm run test
32 | ```
33 |
34 | The `npm test` script will run unit tests that check GraphQL -> Cypher
35 | translation and the schema augmentation features and can be easily run locally
36 | without any extra dependencies.
37 |
38 | Full integration tests can be found in `/test` and are
39 | [run on CircleCI](https://circleci.com/gh/neo4j-graphql/neo4j-graphql-js) as
40 | part of the CI process.
41 |
42 | #### Integration Testing
43 |
44 | If you want to run integration tests locally, make sure your setup meets the
45 | following requirements:
46 |
47 | - A local Neo4J instance with username `neo4j` and password `letmein`
48 | - APOC plugin installed (see instructions [here](https://github.com/neo4j-contrib/neo4j-apoc-procedures#installation-with-neo4j-desktop))
49 | - Your Neo4J instance runs on [this database](https://s3.amazonaws.com/neo4j-sandbox-usecase-datastores/v3_5/recommendations.db.zip)
50 |
51 | In order to import the database, you can download the zipped files and extract
52 | it to the databases folder of your Neo4J instance. Restart the database on the
53 | new data.
54 |
55 | Once you're done with that:
56 |
57 | ```
58 | npm run start-middleware
59 | # open another terminal and run
60 | npm run parse-tck
61 | npm run test-all
62 | ```
63 |
64 | Note that `npm run test-all` will fail on consecutive runs! Some of the
65 | integration tests create data and get in the way of other tests. Running the
66 | whole test suite twice will result in some failing tests. There is [an issue
67 | for it](https://github.com/neo4j-graphql/neo4j-graphql-js/issues/252), check if
68 | it is still active. Your best option for now is to re-import the data after each
69 | test run.
70 |
71 | ### Local Development
72 |
73 | If you include this library inside your project and you want point the
74 | dependency to the files on your local machine, you will probably run into the
75 | following error:
76 |
77 | ```
78 | Error: Cannot use GraphQLObjectType "Query" from another module or realm.
79 |
80 | Ensure that there is only one instance of "graphql" in the node_modules
81 | directory. If different versions of "graphql" are the dependencies of other
82 | relied on modules, use "resolutions" to ensure only one version is installed.
83 | ```
84 |
85 | This is because we currently don't have `graphql` as a peer dependency. See if
86 | [this issue](https://github.com/neo4j-graphql/neo4j-graphql-js/issues/249) still
87 | exists. Until this is fixed a possible workaround is to overwrite the target
88 | folder of `npm run build`.
89 |
90 | Open `package.json` and simply replace `dist/` with the path to the
91 | `node_modules/` folder of your project:
92 |
93 | ```
94 | {
95 | ...
96 | "scripts": {
97 | ...
98 | "build": "babel src --presets babel-preset-env --out-dir /path/to/your/projects/node_modules/",
99 | ..
100 | }
101 | ..
102 | }
103 |
104 | ```
105 |
106 | If you run `npm run build` now, it will be build right into your project and you
107 | should not face the error above.
108 |
109 | ## Spread the love
110 |
111 | If you want to merge back your changes into the main repository, it would be
112 | best if you could [fork the repository](https://help.github.com/en/articles/fork-a-repo)
113 | on Github. Add the fork as a separate remote, push your branch and create a pull
114 | request:
115 |
116 | ```sh
117 | git remote add your-fork git@github.com:your-username/neo4j-graphql-js.git
118 | git push your-fork your-branch
119 | # now go to Github and create a pull request
120 | ```
121 |
122 | We :heart: you.
123 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Licensed under the Apache License, Version 2.0 (the "License");
2 | you may not use this file except in compliance with the License.
3 | You may obtain a copy of the License at
4 |
5 | http://www.apache.org/licenses/LICENSE-2.0
6 |
7 | Unless required by applicable law or agreed to in writing, software
8 | distributed under the License is distributed on an "AS IS" BASIS,
9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | See the License for the specific language governing permissions and
11 | limitations under the License.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | > ⚠️ NOTE: This project is no longer actively maintained. Please consider using the [official Neo4j GraphQL Library.](https://neo4j.com/docs/graphql-manual/current/)
2 |
3 | [](https://circleci.com/gh/neo4j-graphql/neo4j-graphql-js) [](https://badge.fury.io/js/neo4j-graphql-js) [](https://github.com/neo4j-graphql/neo4j-graphql-js/tree/master/docs)
4 |
5 | # neo4j-graphql.js
6 |
7 | A GraphQL to Cypher query execution layer for Neo4j and JavaScript GraphQL implementations.
8 |
9 | - [Read the docs](https://github.com/neo4j-graphql/neo4j-graphql-js/tree/master/docs)
10 | - [Read the changelog](https://github.com/neo4j-graphql/neo4j-graphql-js/blob/master/CHANGELOG.md)
11 |
12 | neo4j-graphql.js is facilitated by [Neo4j Labs](https://neo4j.com/labs/).
13 |
14 | ## Installation and usage
15 |
16 | Install
17 |
18 | ```
19 | npm install --save neo4j-graphql-js
20 | ```
21 |
22 | ### Usage
23 |
24 | Start with GraphQL type definitions:
25 |
26 | ```javascript
27 | const typeDefs = `
28 | type Movie {
29 | title: String
30 | year: Int
31 | imdbRating: Float
32 | genres: [Genre] @relation(name: "IN_GENRE", direction: "OUT")
33 | }
34 | type Genre {
35 | name: String
36 | movies: [Movie] @relation(name: "IN_GENRE", direction: "IN")
37 | }
38 | `;
39 | ```
40 |
41 | Create an executable schema with auto-generated resolvers for Query and Mutation types, ordering, pagination, and support for computed fields defined using the `@cypher` GraphQL schema directive:
42 |
43 | ```javascript
44 | import { makeAugmentedSchema } from 'neo4j-graphql-js';
45 |
46 | const schema = makeAugmentedSchema({ typeDefs });
47 | ```
48 |
49 | Create a neo4j-javascript-driver instance:
50 |
51 | ```javascript
52 | import { v1 as neo4j } from 'neo4j-driver';
53 |
54 | const driver = neo4j.driver(
55 | 'bolt://localhost:7687',
56 | neo4j.auth.basic('neo4j', 'letmein')
57 | );
58 | ```
59 |
60 | Use your favorite JavaScript GraphQL server implementation to serve your GraphQL schema, injecting the Neo4j driver instance into the context so your data can be resolved in Neo4j:
61 |
62 | ```javascript
63 | import { ApolloServer } from 'apollo-server';
64 |
65 | const server = new ApolloServer({ schema, context: { driver } });
66 |
67 | server.listen(3003, '0.0.0.0').then(({ url }) => {
68 | console.log(`GraphQL API ready at ${url}`);
69 | });
70 | ```
71 |
72 | If you don't want auto-generated resolvers, you can also call `neo4jgraphql()` in your GraphQL resolver. Your GraphQL query will be translated to Cypher and the query passed to Neo4j.
73 |
74 | ```js
75 | import { neo4jgraphql } from 'neo4j-graphql-js';
76 |
77 | const resolvers = {
78 | Query: {
79 | Movie(object, params, ctx, resolveInfo) {
80 | return neo4jgraphql(object, params, ctx, resolveInfo);
81 | }
82 | }
83 | };
84 | ```
85 |
86 | ## What is `neo4j-graphql.js`
87 |
88 | A package to make it easier to use GraphQL and [Neo4j](https://neo4j.com/) together. `neo4j-graphql.js` translates GraphQL queries to a single [Cypher](https://neo4j.com/developer/cypher/) query, eliminating the need to write queries in GraphQL resolvers and for batching queries. It also exposes the Cypher query language through GraphQL via the `@cypher` schema directive.
89 |
90 | ### Goals
91 |
92 | - Translate GraphQL queries to Cypher to simplify the process of writing GraphQL resolvers
93 | - Allow for custom logic by overriding of any resolver function
94 | - Work with `graphql-tools`, `graphql-js`, and `apollo-server`
95 | - Support GraphQL servers that need to resolve data from multiple data services/databases
96 | - Expose the power of Cypher through GraphQL via the `@cypher` directive
97 |
98 | ## Benefits
99 |
100 | - Send a single query to the database
101 | - No need to write queries for each resolver
102 | - Exposes the power of the Cypher query language through GraphQL
103 |
104 | ## Contributing
105 |
106 | See our [detailed contribution guidelines](./CONTRIBUTING.md).
107 |
108 | ## Examples
109 |
110 | See [/examples](https://github.com/neo4j-graphql/neo4j-graphql-js/tree/master/example/apollo-server)
111 |
112 | ## [Documentation](docs/)
113 |
114 | Full docs can be found in [docs/](docs/)
115 |
116 | ## Debugging and Tuning
117 |
118 | You can log out the generated cypher statements with an environment variable:
119 |
120 | ```
121 | DEBUG=neo4j-graphql-js node yourcode.js
122 | ```
123 |
124 | This helps to debug and optimize your database statements. E.g. visit your Neo4J
125 | browser console at http://localhost:7474/browser/ and paste the following:
126 |
127 | ```
128 | :params :params { offset: 0, first: 12, filter: {}, cypherParams: { currentUserId: '42' } }
129 | ```
130 |
131 | and now profile the generated query:
132 |
133 | ```
134 | EXPLAIN MATCH (`post`:`Post`) WITH `post` ORDER BY post.createdAt DESC RETURN `post` { .id , .title } AS `post`
135 | ```
136 |
137 | You can learn more by typing:
138 |
139 | ```
140 | :help EXPLAIN
141 | :help params
142 | ```
143 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # neo4j-graphql-js Documentation
2 |
3 | - [Quickstart](neo4j-graphql-js-quickstart.md)
4 | - [User Guide](neo4j-graphql-js.md)
5 | - [Schema Generation And Augmentation](graphql-schema-generation-augmentation.md)
6 | - [Filtering With GraphQL](graphql-filtering.md)
7 | - [Relationship Types](graphql-relationship-types.md)
8 | - [Temporal Types (DateTime)](graphql-temporal-types-datetime.md)
9 | - [Spatial Types](graphql-spatial-types.md)
10 | - [Interface and Union Types](graphql-interface-union-types.md)
11 | - [Authorization / Middleware](neo4j-graphql-js-middleware-authorization.md)
12 | - [Multiple Databases](neo4j-multiple-database-graphql.md)
13 | - [GraphQL Schema Directives](graphql-schema-directives.md)
14 | - [API Reference](neo4j-graphql-js-api.md)
15 | - [Adding Custom Logic](graphql-custom-logic.md)
16 | - [Infer GraphQL Schema](infer-graphql-schema-database.md)
17 | - [Apollo Federation And Gateway](apollo-federation.md)
18 |
--------------------------------------------------------------------------------
/docs/graphql-relationship-types.md:
--------------------------------------------------------------------------------
1 | # GraphQL Relationship Types
2 |
3 | ## Defining relationships in SDL
4 |
5 | GraphQL types can reference other types. When defining your schema, use the `@relation` GraphQL schema directive on the fields that reference other types. For example:
6 |
7 | ```graphql
8 | type Movie {
9 | title: String
10 | year: Int
11 | genres: [Genre] @relation(name: "IN_GENRE", direction: OUT)
12 | }
13 |
14 | type Genre {
15 | name: String
16 | movies: [Movie] @relation(name: "IN_GENRE", direction: IN)
17 | }
18 | ```
19 |
20 | ### Querying Relationship Fields
21 |
22 | Relationship fields can be queried as object fields in GraphQL by including the fields in the selection set. For example, here we query genres connected to a movie node:
23 |
24 |
25 |
35 |
36 |
37 | ## Relationships with properties
38 |
39 | The above example (annotating a field with `@relation`) works for simple relationships without properties, but does not allow for modeling relationship properties. Imagine that we have users who can rate movies, and we want to store their rating and timestamp as a property on a relationship connecting the user and movie. We can represent this by promoting the relationship to a type and moving the `@relation` directive to annotate this new type:
40 |
41 | ```graphql
42 | type Movie {
43 | title: String
44 | year: Int
45 | ratings: [Rated]
46 | }
47 |
48 | type User {
49 | userId: ID
50 | name: String
51 | rated: [Rated]
52 | }
53 |
54 | type Rated @relation(name: "RATED") {
55 | from: User
56 | to: Movie
57 | rating: Float
58 | created: DateTime
59 | }
60 | ```
61 |
62 | This approach of an optional relationship type allows for keeping the schema simple when we don't need relationship properties, but having the flexibility of handling relationship properties when we want to model them.
63 |
64 | ### Querying Relationship Types
65 |
66 | When queries are generated (through [`augmentSchema`](neo4j-graphql-js-api.mdx#augmentschemaschema-graphqlschema) or [`makeAugmentedSchema`](neo4j-graphql-js-api.mdx#makeaugmentedschemaoptions-graphqlschema)) fields referencing a relationship type are replaced with a special payload type that contains the relationship properties and the type reference. For example:
67 |
68 | ```graphql
69 | type _MovieRatings {
70 | created: _Neo4jDateTime
71 | rating: Float
72 | User: User
73 | }
74 | ```
75 |
76 | Here we query for a user and their movie ratings, selecting the `rating` and `created` fields from the relationship type, as well as the movie node connected to the relationship.
77 |
78 |
79 |
95 |
96 |
97 | ## Field names for related nodes
98 |
99 | There are two valid ways to express which fields of a `@relation` type refer to its [source and target node](https://neo4j.com/docs/getting-started/current/graphdb-concepts/#graphdb-relationship-types) types. The `Rated` relationship type above defines `from` and `to` fields. Semantically specific names can be provided for the source and target node fields to the `from` and `to` arguments of the `@relation` type directive.
100 |
101 | ```graphql
102 | type Rated @relation(name: "RATED", from: "user", to: "movie") {
103 | user: User
104 | movie: Movie
105 | rating: Float
106 | created: DateTime
107 | }
108 | ```
109 |
110 | ## Default relationship name
111 |
112 | If the `name` argument of the `@relation` type directive is not provided, then its default is generated during schema augmentation to be the conversion of the type name to Snake case.
113 |
114 | ```graphql
115 | type UserRated
116 | @relation(from: "user", to: "movie") { # name: "USER_RATED"
117 | user: User
118 | movie: Movie
119 | rating: Float
120 | created: DateTime
121 | }
122 | ```
123 |
124 | ## Relationship mutations
125 |
126 | See the [generated mutations](graphql-schema-generation-augmentation.mdx#generated-mutations) section for information on the mutations generated for relationship types.
127 |
--------------------------------------------------------------------------------
/docs/graphql-schema-directives.md:
--------------------------------------------------------------------------------
1 | # GraphQL Schema Directives
2 |
3 | This page provides an overview of the various GraphQL schema directives made available in neo4j-graphql.js. See the links in the table below for full documentation of each directive.
4 |
5 | ## What Are GraphQL Schema Directives
6 |
7 | GraphQL schema directives are a powerful feature of GraphQL that can be used in the type definitions of a GraphQL schema to indicate non-default logic and can be applied to either fields on types. Think of a schema directive as a way to indicate custom logic that should be executed on the GraphQL server.
8 |
9 | In neo4j-graphql.js we use schema directives to:
10 |
11 | - help describe our data model (`@relation`, `@id`, `@unique`, `@index`)
12 | - implement custom logic in our GraphQL service (`@cypher`, `@neo4j_ignore`)
13 | - help implement authorization logic (`@additionalLabel`, `@isAuthenticated`, `@hasRole`, `@hasScope`)
14 |
15 | ## Neo4j GraphQL Schema Directives
16 |
17 | The following GraphQL schema directives are declared during the schema augmentation process and can be used in the type definitions passed to `makeAugmentedSchema`.
18 |
19 | | Directive | Arguments | Description | Notes |
20 | | ------------------- | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
21 | | `@relation` | `name`, `direction` | Used to define relationship fields and types in the GraphQL schema. | See [Designing Your GraphQL Schema Guide](guide-graphql-schema-design.mdx) |
22 | | `@id` | | Used on fields to (optionally) specify the field to be used as the ID for the type. | See [API Reference](https://grandstack.io/docs/neo4j-graphql-js-api#assertschemaoptionsnull) |
23 | | `@unique` | | Used on fields to (optionally) specify fields that should have a uniqueness constraint. | See [API Reference](https://grandstack.io/docs/neo4j-graphql-js-api#assertschemaoptionsnull) |
24 | | `@index` | | Used on fields to indicate an index should be created on this field. | See [API Reference](https://grandstack.io/docs/neo4j-graphql-js-api#assertschemaoptionsnull) |
25 | | `@cypher` | `statement` | Used to define custom logic using Cypher. | See [Defining Custom Logic](graphql-custom-logic.mdx#the-cypher-graphql-schema-directive), and [Designing Your GraphQL Schema](guide-graphql-schema-design.mdx#defining-custom-logic-with-cypher-schema-directives) |
26 | | `@search` | `index` | Used on fields to set full-text search indexes. | See [API Reference](https://grandstack.io/docs/neo4j-graphql-js-api#searchschemaoptionsnull) |
27 | | `@neo4j_ignore` | | Used to exclude fields or types from the Cypher query generation process. Use when implementing a custom resolver. | See [Defining Custom Logic](graphql-custom-logic.mdx#implementing-custom-resolvers) |
28 | | `@additionalLabels` | `labels` | Used for adding additional node labels to types. Can be useful for multi-tenant scenarios where an additional node label is used per tenant. | See [GraphQL Authorization](neo4j-graphql-js-middleware-authorization.mdx#additionallabels) |
29 | | `@isAuthenticated` | | Protects fields and types by requiring a valid signed JWT | See [GraphQL Authorization](neo4j-graphql-js-middleware-authorization.mdx#isauthenticated) |
30 | | `@hasRole` | `roles` | Protects fields and types by limiting access to only requests with valid roles | See [GraphQL Authorization](neo4j-graphql-js-middleware-authorization.mdx#hasrole) |
31 | | `@hasScope` | `scopes` | Protects fields and types by limiting access to only requests with valid scopes | See [GraphQL Authorization](neo4j-graphql-js-middleware-authorization.mdx#hasscope) |
32 |
--------------------------------------------------------------------------------
/docs/graphql-spatial-types.md:
--------------------------------------------------------------------------------
1 | # GraphQL Spatial Types
2 |
3 | > Neo4j currently supports the spatial `Point` type, which can represent both 2D (such as latitude and longitude) and 3D (such as x,y,z or latitude, longitude, height) points. Read more about the [Point type](https://neo4j.com/docs/cypher-manual/3.5/syntax/spatial/) and associated [functions, such as the index-backed distance function](https://neo4j.com/docs/cypher-manual/current/functions/spatial/) in the Neo4j docs.
4 |
5 | ## Spatial `Point` type in SDL
6 |
7 | neo4j-graphql.js makes available the `Point` type for use in your GraphQL type definitions. You can use it like this:
8 |
9 | ```graphql
10 | type Business {
11 | id: ID!
12 | name: String
13 | location: Point
14 | }
15 | ```
16 |
17 | The GraphQL [schema augmentation process](graphql-schema-generation-augmentation.mdx) will translate the `location` field to a `_Neo4jPoint` type in the augmented schema.
18 |
19 | ## Using `Point` In Queries
20 |
21 | The `Point` object type exposes the following fields that can be used in the query selection set:
22 |
23 | - `x`: `Float`
24 | - `y`: `Float`
25 | - `z`: `Float`
26 | - `longitude`: `Float`
27 | - `latitude`: `Float`
28 | - `height`: `Float`
29 | - `crs`: `String`
30 | - `srid`: `Int`
31 |
32 | For example:
33 |
34 | _GraphQL query_
35 |
36 | ```graphql
37 | query {
38 | Business(first: 2) {
39 | name
40 | location {
41 | latitude
42 | longitude
43 | }
44 | }
45 | }
46 | ```
47 |
48 | _GraphQL result_
49 |
50 | ```json
51 | {
52 | "data": {
53 | "Business": [
54 | {
55 | "name": "Missoula Public Library",
56 | "location": {
57 | "latitude": 46.870035,
58 | "longitude": -113.990976
59 | }
60 | },
61 | {
62 | "name": "Ninja Mike's",
63 | "location": {
64 | "latitude": 46.874029,
65 | "longitude": -113.995057
66 | }
67 | }
68 | ]
69 | }
70 | }
71 | ```
72 |
73 | ### `Point` Query Arguments
74 |
75 | As part of the GraphQL [schema augmentation process](graphql-schema-generation-augmentation.mdx) point input types are added to the schema and can be used as field arguments. For example if I wanted to find businesses with exact values of longitude and latitude:
76 |
77 | _GraphQL query_
78 |
79 | ```graphql
80 | query {
81 | Business(location: { latitude: 46.870035, longitude: -113.990976 }) {
82 | name
83 | location {
84 | latitude
85 | longitude
86 | }
87 | }
88 | }
89 | ```
90 |
91 | _GraphQL result_
92 |
93 | ```json
94 | {
95 | "data": {
96 | "Business": [
97 | {
98 | "name": "Missoula Public Library",
99 | "location": {
100 | "latitude": 46.870035,
101 | "longitude": -113.990976
102 | }
103 | }
104 | ]
105 | }
106 | }
107 | ```
108 |
109 | However, with Point data the auto-generated filters are likely to be more useful, especially when we consider arbitrary precision.
110 |
111 | ### `Point` Query Filter
112 |
113 | When querying using point data, often we want to find things that are close to other things. For example, what businesses are within 1.5km of me? We can accomplish this using the [auto-generated filter argument](graphql-filtering.mdx). For example:
114 |
115 | _GraphQL query_
116 |
117 | ```graphql
118 | {
119 | Business(
120 | filter: {
121 | location_distance_lt: {
122 | point: { latitude: 46.859924, longitude: -113.985402 }
123 | distance: 1500
124 | }
125 | }
126 | ) {
127 | name
128 | location {
129 | latitude
130 | longitude
131 | }
132 | }
133 | }
134 | ```
135 |
136 | _GraphQL result_
137 |
138 | ```json
139 | {
140 | "data": {
141 | "Business": [
142 | {
143 | "name": "Missoula Public Library",
144 | "location": {
145 | "latitude": 46.870035,
146 | "longitude": -113.990976
147 | }
148 | },
149 | {
150 | "name": "Market on Front",
151 | "location": {
152 | "latitude": 46.869824,
153 | "longitude": -113.993633
154 | }
155 | }
156 | ]
157 | }
158 | }
159 | ```
160 |
161 | For points using the Geographic coordinate reference system (latitude and longitude) `distance` is measured in meters.
162 |
163 | ## Using `Point` In Mutations
164 |
165 | The schema augmentation process adds mutations for creating, updating, and deleting nodes and relationships, including for setting values for `Point` fields using the `_Neo4jPointInput` type.
166 |
167 | For example, to create a new Business node and set the value of the location field:
168 |
169 | ```graphql
170 | mutation {
171 | CreateBusiness(
172 | name: "University of Montana"
173 | location: { latitude: 46.859924, longitude: -113.985402 }
174 | ) {
175 | name
176 | }
177 | }
178 | ```
179 |
180 | Note that not all fields of the `_Neo4jPointInput` type need to specified. In general, you have the choice of:
181 |
182 | - **Fields (latitude,longitude or x,y)** If the coordinate is specified using the fields `latitude` and `longitude` then the Geographic coordinate reference system will be used. If instead `x` and `y` fields are used then the coordinate reference system would be Cartesian.
183 | - **Number of dimensions** You can specify `height` along with `longitude` and `latitude` for 3D, or `z` along with `x` and `y`.
184 |
185 | See the [Neo4j Cypher docs for more details](https://neo4j.com/docs/cypher-manual/current/syntax/spatial/#cypher-spatial-specifying-spatial-instants) on the spatial point type.
186 |
187 | ## Resources
188 |
189 | - Blog post: [Working With Spatial Data In Neo4j GraphQL In The Cloud](https://blog.grandstack.io/working-with-spatial-data-in-neo4j-graphql-in-the-cloud-eee2bf1afad) - Serverless GraphQL, Neo4j Aura, and GRANDstack
190 |
--------------------------------------------------------------------------------
/docs/graphql-temporal-types-datetime.md:
--------------------------------------------------------------------------------
1 | # GraphQL Temporal Types (DateTime)
2 |
3 | > Temporal types are available in Neo4j v3.4+ Read more about [using temporal types](https://neo4j.com/docs/cypher-manual/current/syntax/temporal/) and [functions](https://neo4j.com/docs/cypher-manual/current/functions/temporal/) in Neo4j in the docs and [in this post](https://www.adamcowley.co.uk/neo4j/temporal-native-dates/).
4 |
5 | Neo4j supports native temporal types as properties on nodes and relationships. These types include Date, DateTime, and LocalDateTime. With neo4j-graphql.js you can use these temporal types in your GraphQL schema. Just use them in your SDL type definitions.
6 |
7 | ## Temporal Types In SDL
8 |
9 | neo4j-graphql.js makes available the following temporal types for use in your GraphQL type definitions: `Date`, `DateTime`, and `LocalDateTime`. You can use the temporal types in a field definition in your GraphQL type like this:
10 |
11 | ```graphql
12 | type Movie {
13 | id: ID!
14 | title: String
15 | published: DateTime
16 | }
17 | ```
18 |
19 | ## Using Temporal Fields In Queries
20 |
21 | Temporal types expose their date components (such as day, month, year, hour, etc) as fields, as well as a `formatted` field which is the [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) string representation of the temporal value. The specific fields available vary depending on which temporal is used, but generally conform to [those specified here](https://neo4j.com/docs/cypher-manual/current/syntax/temporal/). For example:
22 |
23 | _GraphQL query_
24 |
25 | ```graphql
26 | {
27 | Movie(title: "River Runs Through It, A") {
28 | title
29 | published {
30 | day
31 | month
32 | year
33 | hour
34 | minute
35 | second
36 | formatted
37 | }
38 | }
39 | }
40 | ```
41 |
42 | _GraphQL result_
43 |
44 | ```json
45 | {
46 | "data": {
47 | "Movie": [
48 | {
49 | "title": "River Runs Through It, A",
50 | "published": {
51 | "day": 9,
52 | "month": 10,
53 | "year": 1992,
54 | "hour": 0,
55 | "minute": 0,
56 | "second": 0,
57 | "formatted": "1992-10-09T00:00:00Z"
58 | }
59 | }
60 | ]
61 | }
62 | }
63 | ```
64 |
65 | ### Temporal Query Arguments
66 |
67 | As part of the [schema augmentation process](graphql-schema-generation-augmentation.mdx) temporal input types are added to the schema and can be used as query arguments. For example, given the type definition:
68 |
69 | ```graphql
70 | type Movie {
71 | movieId: ID!
72 | title: String
73 | released: Date
74 | }
75 | ```
76 |
77 | the following query will be generated for the `Movie` type:
78 |
79 | ```graphql
80 | Movie (
81 | movieId: ID!
82 | title: String
83 | released: _Neo4jDate
84 | _id: String
85 | first: Int
86 | offset: Int
87 | orderBy: _MovieOrdering
88 | )
89 | ```
90 |
91 | and the type `_Neo4jDateInput` added to the schema:
92 |
93 | ```graphql
94 | type _Neo4jDateTimeInput {
95 | year: Int
96 | month: Int
97 | day: Int
98 | formatted: String
99 | }
100 | ```
101 |
102 | At query time, either specify the individual components (year, month, day, etc) or the `formatted` field, which is the [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) representation. For example, to query for all movies with a release date of October 10th, 1992:
103 |
104 | ```graphql
105 | {
106 | Movie(released: { year: 1992, month: 10, day: 9 }) {
107 | title
108 | }
109 | }
110 | ```
111 |
112 | or equivalently:
113 |
114 | ```graphql
115 | {
116 | Movie(released: { formatted: "1992-10-09" }) {
117 | title
118 | }
119 | }
120 | ```
121 |
122 | ## Using Temporal Fields In Mutations
123 |
124 | As part of the [schema augmentation process](#schema-augmentation) temporal input types are created and used for the auto-generated create, update, delete mutations using the type definitions specified for the GraphQL schema. These temporal input types also include fields for each component of the temporal type (day, month, year, hour, etc) as well as `formatted`, the [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) representation. When used in a mutation, specify either the individual components **or** the `formatted` field, but not both.
125 |
126 | For example, this mutation:
127 |
128 | ```graphql
129 | mutation {
130 | CreateMovie(
131 | title: "River Runs Through It, A"
132 | published: { year: 1992, month: 10, day: 9 }
133 | ) {
134 | title
135 | published {
136 | formatted
137 | }
138 | }
139 | }
140 | ```
141 |
142 | is equivalent to this version, using the `formatted` field instead
143 |
144 | ```graphql
145 | mutation {
146 | CreateMovie(
147 | title: "River Runs Through It, A"
148 | published: { formatted: "1992-10-09T00:00:00Z" }
149 | ) {
150 | title
151 | published {
152 | formatted
153 | }
154 | }
155 | }
156 | ```
157 |
158 | The input types for temporals generally correspond to the fields used for specifying temporal instants in Neo4j [described here](https://neo4j.com/docs/cypher-manual/current/syntax/temporal/#cypher-temporal-specifying-temporal-instants).
159 |
160 | ## Resources
161 |
162 | - Blog post: [Using Native DateTime Types With GRANDstack](https://blog.grandstack.io/using-native-datetime-types-with-grandstack-e126728fb2a0) - Leverage Neo4j’s Temporal Types In Your GraphQL Schema With neo4j-graphql.js
163 |
--------------------------------------------------------------------------------
/docs/img/BatchMergeAGraphData.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neo4j-graphql/neo4j-graphql-js/381ef0302bbd11ecd0f94f978045cdbc61c39b8e/docs/img/BatchMergeAGraphData.png
--------------------------------------------------------------------------------
/docs/img/MergeAGraphData.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neo4j-graphql/neo4j-graphql-js/381ef0302bbd11ecd0f94f978045cdbc61c39b8e/docs/img/MergeAGraphData.png
--------------------------------------------------------------------------------
/docs/img/exampleDataGraph.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neo4j-graphql/neo4j-graphql-js/381ef0302bbd11ecd0f94f978045cdbc61c39b8e/docs/img/exampleDataGraph.png
--------------------------------------------------------------------------------
/docs/img/interface-data.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neo4j-graphql/neo4j-graphql-js/381ef0302bbd11ecd0f94f978045cdbc61c39b8e/docs/img/interface-data.png
--------------------------------------------------------------------------------
/docs/img/interface-model.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neo4j-graphql/neo4j-graphql-js/381ef0302bbd11ecd0f94f978045cdbc61c39b8e/docs/img/interface-model.png
--------------------------------------------------------------------------------
/docs/img/movies.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neo4j-graphql/neo4j-graphql-js/381ef0302bbd11ecd0f94f978045cdbc61c39b8e/docs/img/movies.png
--------------------------------------------------------------------------------
/docs/img/union-data.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neo4j-graphql/neo4j-graphql-js/381ef0302bbd11ecd0f94f978045cdbc61c39b8e/docs/img/union-data.png
--------------------------------------------------------------------------------
/docs/infer-graphql-schema-database.md:
--------------------------------------------------------------------------------
1 | # Inferring GraphQL Schema From An Existing Database
2 |
3 | Typically when we start a new application, we don't have an existing database and follow the GraphQL-First development paradigm by starting with type definitions. However, in some cases we may have an existing Neo4j database populated with data. In those cases, it can be convenient to generate GraphQL type definitions based on the existing database that can then be fed into `makeAugmentedSchema` to generate a GraphQL API for the existing database. We can do this with the use of the `inferSchema` functionality in neo4j-graphql.js.
4 |
5 | Let's use the [Neo4j Sandbox movies recommendation dataset](https://neo4j.com/sandbox?usecase=recommendations) to auto-generate GraphQL type definitions using `inferSchema`.
6 |
7 | This dataset has information about movies, actors, directors, and user ratings of movies, modeled as a graph:
8 |
9 | 
10 |
11 | ## How To Use `inferSchema`
12 |
13 | The `inferSchema` function takes a Neo4j JavaScript driver instance and returns a Promise that evaluates to our GraphQL type definitions, defined using GraphQL Schema Definition Language (SDL). Here’s a simple example that will inspect our local Neo4j database and log the GraphQL type definitions to the console.
14 |
15 | ```js
16 | const { inferSchema } = require('neo4j-graphql-js');
17 | const neo4j = require('neo4j-driver');
18 |
19 | const driver = neo4j.driver(
20 | 'bolt://localhost:7687',
21 | neo4j.auth.basic('neo4j', 'letmein')
22 | );
23 | inferSchema(driver).then(result => {
24 | console.log(result.typeDefs);
25 | });
26 | ```
27 |
28 | Running this on [the movies dataset](https://neo4j.com/sandbox?usecase=recommendations) would produce the following GraphQL type definitions:
29 |
30 | ```graphql
31 | type Movie {
32 | _id: Long!
33 | countries: [String]
34 | imdbId: String!
35 | imdbRating: Float
36 | imdbVotes: Int
37 | languages: [String]
38 | movieId: String!
39 | plot: String
40 | poster: String
41 | released: String
42 | runtime: Int
43 | title: String!
44 | tmdbId: String
45 | year: Int
46 | in_genre: [Genre] @relation(name: "IN_GENRE", direction: OUT)
47 | users: [User] @relation(name: "RATED", direction: IN)
48 | actors: [Actor] @relation(name: "ACTED_IN", direction: IN)
49 | directors: [Director] @relation(name: "DIRECTED", direction: IN)
50 | }
51 |
52 | type RATED @relation(name: "RATED") {
53 | from: User!
54 | to: Movie!
55 | created: DateTime!
56 | rating: Float!
57 | timestamp: Int!
58 | }
59 |
60 | type User {
61 | _id: Long!
62 | name: String!
63 | userId: String!
64 | rated: [Movie] @relation(name: "RATED", direction: OUT)
65 | RATED_rel: [RATED]
66 | }
67 |
68 | type Actor {
69 | _id: Long!
70 | name: String!
71 | acted_in: [Movie] @relation(name: "ACTED_IN", direction: OUT)
72 | }
73 |
74 | type Director {
75 | _id: Long!
76 | name: String!
77 | directed: [Movie] @relation(name: "DIRECTED", direction: OUT)
78 | }
79 |
80 | type Genre {
81 | _id: Long!
82 | name: String!
83 | movies: [Movie] @relation(name: "IN_GENRE", direction: IN)
84 | }
85 | ```
86 |
87 | ## Using `inferSchema` With `makeAugmentedSchema`
88 |
89 | The real power of `inferSchema` comes when used in combination with `makeAugmentedSchema` to create a GraphQL API from only the database. Since `makeAugmentedSchema` handles generating our Query/Mutation fields and resolvers, that means creating a GraphQL API on top of Neo4j is as simple as passing our typedefs from `inferSchema` into `makeAugmentedSchema`.
90 |
91 | Here’s a full example:
92 |
93 | ```js
94 | const { makeAugmentedSchema, inferSchema } = require('neo4j-graphql-js');
95 | const { ApolloServer } = require('apollo-server');
96 | const neo4j = require('neo4j-driver');
97 |
98 | // Create Neo4j driver instance
99 | const driver = neo4j.driver(
100 | process.env.NEO4J_URI || 'bolt://localhost:7687',
101 | neo4j.auth.basic(
102 | process.env.NEO4J_USER || 'neo4j',
103 | process.env.NEO4J_PASSWORD || 'letmein'
104 | )
105 | );
106 |
107 | // Connect to existing Neo4j instance, infer GraphQL typedefs
108 | // generate CRUD GraphQL API using makeAugmentedSchema
109 | const inferAugmentedSchema = driver => {
110 | return inferSchema(driver).then(result => {
111 | return makeAugmentedSchema({
112 | typeDefs: result.typeDefs
113 | });
114 | });
115 | };
116 |
117 | // Spin up GraphQL server using auto-generated GraphQL schema object
118 | const createServer = schema =>
119 | new ApolloServer({
120 | schema
121 | context: { driver }
122 | }
123 | });
124 |
125 | inferAugmentedSchema(driver)
126 | .then(createServer)
127 | .then(server => server.listen(3000, '0.0.0.0'))
128 | .then(({ url }) => {
129 | console.log(`GraphQL API ready at ${url}`);
130 | })
131 | .catch(err => console.error(err));
132 | ```
133 |
134 | ## Persisting The Inferred Schema
135 |
136 | Often it is helpful to generate the inferred schema as a starting point, persist it to a file, and then adjust the generated type definitions as necessary (such as adding custom logic with the use of `@cypher` directives). Here we generate GraphQL type definitions for our database, saving them to a file named schema.graphql:
137 |
138 | ```js
139 | const neo4j = require('neo4j-driver');
140 | const { inferSchema } = require('neo4j-graphql-js');
141 | const fs = require('fs');
142 |
143 | const driver = neo4j.driver(
144 | 'bolt://localhost:7687',
145 | neo4j.auth.basic('neo4j', 'letmein')
146 | );
147 |
148 | const schemaInferenceOptions = {
149 | alwaysIncludeRelationships: false
150 | };
151 |
152 | inferSchema(driver, schemaInferenceOptions).then(result => {
153 | fs.writeFile('schema.graphql', result.typeDefs, err => {
154 | if (err) throw err;
155 | console.log('Updated schema.graphql');
156 | process.exit(0);
157 | });
158 | });
159 | ```
160 |
161 | Then we can load this schema.graphql file and pass the type definitions into `makeAugmentedSchema`.
162 |
163 | ```js
164 | // Load GraphQL type definitions from schema.graphql file
165 | const typeDefs = fs
166 | .readFileSync(path.join(__dirname, 'schema.graphql'))
167 | .toString('utf-8');
168 | ```
169 |
170 | ## Resources
171 |
172 | - [Inferring GraphQL Type Definitions From An Existing Neo4j Database](https://blog.grandstack.io/inferring-graphql-type-definitions-from-an-existing-neo4j-database-dadca2138b25) - Create a GraphQL API Without Writing Resolvers Or TypeDefs
173 | - [Schema auto-generation example in neo4j-graphql-js Github repository](https://github.com/neo4j-graphql/neo4j-graphql-js/blob/master/example/autogenerated/autogen.js) - example code showing how to use `inferSchema` and `makeAugmentedSchema` together.
174 |
--------------------------------------------------------------------------------
/docs/neo4j-graphql-js-quickstart.md:
--------------------------------------------------------------------------------
1 | # neo4j-graphql.js Quickstart
2 |
3 | A GraphQL to Cypher query execution layer for Neo4j and JavaScript GraphQL implementations.
4 |
5 | ## Installation and usage
6 |
7 | ### Install
8 |
9 | ```shell
10 | npm install --save neo4j-graphql-js
11 | ```
12 |
13 | ### Usage
14 |
15 | Start with GraphQL type definitions:
16 |
17 | ```js
18 | const typeDefs = `
19 | type Movie {
20 | title: String
21 | year: Int
22 | imdbRating: Float
23 | genres: [Genre] @relation(name: "IN_GENRE", direction: OUT)
24 | }
25 | type Genre {
26 | name: String
27 | movies: [Movie] @relation(name: "IN_GENRE", direction: IN)
28 | }
29 | `;
30 | ```
31 |
32 | Create an executable GraphQL schema with auto-generated resolvers for Query and Mutation types, ordering, pagination, and support for computed fields defined using the `@cypher` GraphQL schema directive:
33 |
34 | ```js
35 | import { makeAugmentedSchema } from 'neo4j-graphql-js';
36 |
37 | const schema = makeAugmentedSchema({ typeDefs });
38 | ```
39 |
40 | Create a neo4j-javascript-driver instance:
41 |
42 | ```js
43 | import neo4j from 'neo4j-driver';
44 |
45 | const driver = neo4j.driver(
46 | 'bolt://localhost:7687',
47 | neo4j.auth.basic('neo4j', 'letmein')
48 | );
49 | ```
50 |
51 | Use your favorite JavaScript GraphQL server implementation to serve your GraphQL schema, injecting the Neo4j driver instance into the context so your data can be resolved in Neo4j:
52 |
53 | ```js
54 | import { ApolloServer } from 'apollo-server';
55 |
56 | const server = new ApolloServer({ schema, context: { driver } });
57 |
58 | server.listen(3003, '0.0.0.0').then(({ url }) => {
59 | console.log(`GraphQL API ready at ${url}`);
60 | });
61 | ```
62 |
63 | If you don't want auto-generated resolvers, you can also call `neo4jgraphql()` in your GraphQL resolver. Your GraphQL query will be translated to Cypher and the query passed to Neo4j.
64 |
65 | ```js
66 | import { neo4jgraphql } from 'neo4j-graphql-js';
67 |
68 | const resolvers = {
69 | Query: {
70 | Movie(object, params, ctx, resolveInfo) {
71 | return neo4jgraphql(object, params, ctx, resolveInfo);
72 | }
73 | }
74 | };
75 | ```
76 |
77 | ## Benefits
78 |
79 | - Send a single query to the database
80 | - No need to write queries for each resolver
81 | - Exposes the power of the Cypher query language through GraphQL
82 |
83 | ## Features
84 |
85 | - [x] Translate basic GraphQL queries to Cypher
86 | - [x] `first` and `offset` arguments for pagination
87 | - [x] `@cypher` schema directive for exposing Cypher through GraphQL
88 | - [x] Handle fragments
89 | - [x] Ordering
90 | - [x] Filtering
91 | - [x] Handle interface types
92 | - [x] Handle inline fragments
93 | - [x] Native database temporal types (Date, DateTime, LocalDateTime)
94 | - [x] Native Point database type
95 |
--------------------------------------------------------------------------------
/docs/neo4j-graphql-js.md:
--------------------------------------------------------------------------------
1 | # neo4j-graphql.js User Guide
2 |
3 | ## What is `neo4j-graphql-js`
4 |
5 | A package to make it easier to use GraphQL and [Neo4j](https://neo4j.com/) together. `neo4j-graphql-js` translates GraphQL queries to a single [Cypher](https://neo4j.com/developer/cypher/) query, eliminating the need to write queries in GraphQL resolvers and for batching queries. It also exposes the Cypher query language through GraphQL via the `@cypher` schema directive.
6 |
7 | ### Goals of neo4j-graphql.js
8 |
9 | - Translate GraphQL queries to Cypher to simplify the process of writing GraphQL resolvers
10 | - Allow for custom logic by overriding of any resolver function
11 | - Work with `graphql-tools`, `graphql-js`, and `apollo-server`
12 | - Support GraphQL servers that need to resolve data from multiple data services/databases
13 | - Expose the power of Cypher through GraphQL via the `@cypher` directive
14 |
15 | ## How it works
16 |
17 | `neo4j-graphql-js` aims to simplify the process of building GraphQL APIs backed by Neo4j, embracing the paradigm of GraphQL First Development. Specifically,
18 |
19 | - The Neo4j datamodel is defined by a GraphQL schema.
20 | - Inside resolver functions, GraphQL queries are translated to Cypher queries and can be sent to a Neo4j database by including a Neo4j driver instance in the context object of the GraphQL request.
21 | - Any resolver can be overridden by a custom resolver function implementation to allow for custom logic
22 | - Optionally, GraphQL fields can be resolved by a user defined Cypher query through the use of the `@cypher` schema directive.
23 |
24 | ### Start with a GraphQL schema
25 |
26 | GraphQL First Development is all about starting with a well defined GraphQL schema. Here we'll use the GraphQL schema IDL syntax, compatible with graphql-tools (and other libraries) to define a simple schema:
27 |
28 | ```js
29 | const typeDefs = `
30 | type Movie {
31 | movieId: ID!
32 | title: String
33 | year: Int
34 | plot: String
35 | poster: String
36 | imdbRating: Float
37 | similar(first: Int = 3, offset: Int = 0): [Movie] @cypher(statement: "MATCH (this)-[:IN_GENRE]->(:Genre)<-[:IN_GENRE]-(o:Movie) RETURN o")
38 | degree: Int @cypher(statement: "RETURN SIZE((this)-->())")
39 | actors(first: Int = 3, offset: Int = 0): [Actor] @relation(name: "ACTED_IN", direction:IN)
40 | }
41 |
42 | type Actor {
43 | id: ID!
44 | name: String
45 | movies: [Movie]
46 | }
47 |
48 |
49 | type Query {
50 | Movie(id: ID, title: String, year: Int, imdbRating: Float, first: Int, offset: Int): [Movie]
51 | }
52 | `;
53 | ```
54 |
55 | We define two types, `Movie` and `Actor` as well as a top level Query `Movie` which becomes our entry point. This looks like a standard GraphQL schema, except for the use of two directives `@relation` and `@cypher`. In GraphQL directives allow us to annotate fields and provide an extension point for GraphQL. See [GraphQL Schema Directive](graphql-schema-directives.mdx) for an overview of all GraphQL schema directives exposed in `neo4j-graphql.js`
56 |
57 | - `@cypher` directive - maps the specified Cypher query to the value of the field. In the Cypher query, `this` is bound to the current object being resolved. See [Adding Custom Logic](graphql-custom-logic.mdx#the-cypher-graphql-schema-directive) for more information and examples of the `@cypher` GraphQL schema directive.
58 | - `@relation` directive - used to indicate relationships in the data model. The `name` argument specifies the relationship type, and `direction` indicates the direction of the relationship (`IN` for incoming relationships, `OUT` for outgoing relationships, or `BOTH` to match both directions). See the [GraphQL Schema Design Guide](guide-graphql-schema-design.mdx) for more information and examples.
59 |
60 | ### Translate GraphQL To Cypher
61 |
62 | Inside each resolver, use `neo4j-graphql()` to generate the Cypher required to resolve the GraphQL query, passing through the query arguments, context and resolveInfo objects.
63 |
64 | ```js
65 | import { neo4jgraphql } from 'neo4j-graphql-js';
66 |
67 | const resolvers = {
68 | // entry point to GraphQL service
69 | Query: {
70 | Movie(object, params, ctx, resolveInfo) {
71 | return neo4jgraphql(object, params, ctx, resolveInfo);
72 | }
73 | }
74 | };
75 | ```
76 |
77 | GraphQL to Cypher translation works by inspecting the GraphQL schema, the GraphQL query and arguments. For example, this simple GraphQL query
78 |
79 | ```graphql
80 | {
81 | Movie(title: "River Runs Through It, A") {
82 | title
83 | year
84 | imdbRating
85 | }
86 | }
87 | ```
88 |
89 | is translated into the Cypher query
90 |
91 | ```cypher
92 | MATCH (movie:Movie {title:"River Runs Through It, A"})
93 | RETURN movie { .title , .year , .imdbRating } AS movie
94 | SKIP 0
95 | ```
96 |
97 | A slightly more complicated traversal
98 |
99 | ```graphql
100 | {
101 | Movie(title: "River Runs Through It, A") {
102 | title
103 | year
104 | imdbRating
105 | actors {
106 | name
107 | }
108 | }
109 | }
110 | ```
111 |
112 | becomes
113 |
114 | ```cypher
115 | MATCH (movie:Movie {title:"River Runs Through It, A"})
116 | RETURN movie { .title , .year , .imdbRating,
117 | actors: [(movie)<-[ACTED_IN]-(movie_actors:Actor) | movie_actors { .name }] }
118 | AS movie
119 | SKIP 0
120 | ```
121 |
122 | ## `@cypher` directive
123 |
124 | > The `@cypher` directive feature has a dependency on the APOC procedure library, to enable subqueries. If you'd like to make use of the `@cypher` feature you'll need to install the [APOC procedure library](https://github.com/neo4j-contrib/neo4j-apoc-procedures#installation-with-neo4j-desktop).
125 |
126 | GraphQL is fairly limited when it comes to expressing complex queries such as filtering, or aggregations. We expose the graph querying language Cypher through GraphQL via the `@cypher` directive. Annotate a field in your schema with the `@cypher` directive to map the results of that query to the annotated GraphQL field. For example:
127 |
128 | ```graphql
129 | type Movie {
130 | movieId: ID!
131 | title: String
132 | year: Int
133 | plot: String
134 | similar(first: Int = 3, offset: Int = 0): [Movie]
135 | @cypher(
136 | statement: "MATCH (this)-[:IN_GENRE]->(:Genre)<-[:IN_GENRE]-(o:Movie) RETURN o ORDER BY COUNT(*) DESC"
137 | )
138 | }
139 | ```
140 |
141 | The field `similar` will be resolved using the Cypher query
142 |
143 | ```cypher
144 | MATCH (this)-[:IN_GENRE]->(:Genre)<-[:IN_GENRE]-(o:Movie) RETURN o ORDER BY COUNT(*) DESC
145 | ```
146 |
147 | to find movies with overlapping Genres.
148 |
149 | Querying a GraphQL field marked with a `@cypher` directive executes that query as a subquery:
150 |
151 | _GraphQL:_
152 |
153 | ```graphql
154 | {
155 | Movie(title: "River Runs Through It, A") {
156 | title
157 | year
158 | imdbRating
159 | actors {
160 | name
161 | }
162 | similar(first: 3) {
163 | title
164 | }
165 | }
166 | }
167 | ```
168 |
169 | _Cypher:_
170 |
171 | ```cypher
172 | MATCH (movie:Movie {title:"River Runs Through It, A"})
173 | RETURN movie { .title , .year , .imdbRating,
174 | actors: [(movie)<-[ACTED_IN]-(movie_actors:Actor) | movie_actors { .name }],
175 | similar: [ x IN apoc.cypher.runFirstColumn("
176 | WITH {this} AS this
177 | MATCH (this)-[:IN_GENRE]->(:Genre)<-[:IN_GENRE]-(o:Movie)
178 | RETURN o",
179 | {this: movie}, true) | x { .title }][..3]
180 | } AS movie
181 | SKIP 0
182 | ```
183 |
184 | > This means that the entire GraphQL request is still resolved with a single Cypher query, and thus a single round trip to the database.
185 |
186 | ## Query Neo4j
187 |
188 | Inject a Neo4j driver instance in the context of each GraphQL request and `neo4j-graphql-js` will query the Neo4j database and return the results to resolve the GraphQL query.
189 |
190 | ```js
191 | let driver;
192 |
193 | function context(headers, secrets) {
194 | if (!driver) {
195 | driver = neo4j.driver(
196 | 'bolt://localhost:7687',
197 | neo4j.auth.basic('neo4j', 'letmein')
198 | );
199 | }
200 | return { driver };
201 | }
202 | ```
203 |
204 | ```js
205 | server.use(
206 | '/graphql',
207 | bodyParser.json(),
208 | graphqlExpress(request => ({
209 | schema,
210 | rootValue,
211 | context: context(request.headers, process.env)
212 | }))
213 | );
214 | ```
215 |
216 | ## Resources
217 |
218 | - Blog post: [Five Common GraphQL Problems and How Neo4j-GraphQL Aims To Solve Them](https://blog.grandstack.io/five-common-graphql-problems-and-how-neo4j-graphql-aims-to-solve-them-e9a8999c8d43) - Digging Into the Goals of A Neo4j-GraphQL Integration
219 |
--------------------------------------------------------------------------------
/docs/neo4j-multiple-database-graphql.md:
--------------------------------------------------------------------------------
1 | # Using Multiple Neo4j Databases
2 |
3 | > This section describes how to use multiple Neo4j databases with neo4j-graphql.js. Multiple active databases is a feature available in Neo4j v4.x
4 |
5 | Neo4j supports multiple active databases. This feature is often used to support multi-tenancy use cases. Multiple databases can be used with neo4j-graphql.js by specifying a value in the GraphQL resolver context. If no value is specified for `context.neo4jDatabase` then the default database is used (as specified in `neo4j.conf`)
6 |
7 | You can read more about managing and working with multiple databases in Neo4j in the manual [here.](https://neo4j.com/docs/operations-manual/current/manage-databases/introduction/)
8 |
9 | ## Specifying The Neo4j Database
10 |
11 | The Neo4j database to be used is specified in the GraphQL resolver context object. The context object is passed to each resolver and neo4j-graphql.js at a minimum expects a Neo4j JavaScript driver instance under the `driver` key.
12 |
13 | To specify the Neo4j database to be used, provide a value in the context object, under the key `neo4jDatabase` that evaluates to a string representing the desired database. If no value is provided then the default Neo4j database will be used.
14 |
15 | For example, with Apollo Server, here we provide the database name `sanmateo`:
16 |
17 | ```js
18 | const neo4j = require('neo4j-driver');
19 | const { ApolloServer } = require('apollo-server');
20 |
21 | const driver = neo4j.driver(
22 | 'neo4j://localhost:7687',
23 | neo4j.auth.basic('neo4j', 'letmein')
24 | );
25 |
26 | const server = new ApolloServer({
27 | schema,
28 | context: { driver, neo4jDatabase: 'sanmateo' }
29 | });
30 |
31 | server.listen(3004, '0.0.0.0').then(({ url }) => {
32 | console.log(`GraphQL API ready at ${url}`);
33 | });
34 | ```
35 |
36 | ## Specifying The Neo4j Database In A Request Header
37 |
38 | We can also use a function to define the context object. This allows us to use a value from the request header or some middleware process to specify the Neo4j database.
39 |
40 | Here we use the value of the request header `x-database` for the Neo4j database:
41 |
42 | ```js
43 | const neo4j = require('neo4j-driver');
44 | const { ApolloServer } = require('apollo-server');
45 |
46 | const driver = neo4j.driver(
47 | 'neo4j://localhost:7687',
48 | neo4j.auth.basic('neo4j', 'letmein')
49 | );
50 |
51 | const server = new ApolloServer({
52 | schema,
53 | context: ({ req }) => {
54 | return { driver, neo4jDatabase: req.header['x-database'] };
55 | }
56 | });
57 |
58 | server.listen(3004, '0.0.0.0').then(({ url }) => {
59 | console.log(`GraphQL API ready at ${url}`);
60 | });
61 | ```
62 |
63 | ## Resources
64 |
65 | - [Multi-Tenant GraphQL With Neo4j 4.0](https://blog.grandstack.io/multitenant-graphql-with-neo4j-4-0-4a1b2b4dada4) A Look At Using Neo4j 4.0 Multidatabase With neo4j-graphql.js
66 |
--------------------------------------------------------------------------------
/example/apollo-federation/gateway.js:
--------------------------------------------------------------------------------
1 | import { ApolloServer } from 'apollo-server';
2 | import { ApolloGateway } from '@apollo/gateway';
3 | import { accountsSchema } from './services/accounts';
4 | import { inventorySchema } from './services/inventory';
5 | import { productsSchema } from './services/products';
6 | import { reviewsSchema } from './services/reviews';
7 | import neo4j from 'neo4j-driver';
8 |
9 | // The schema and seed data are based on the Apollo Federation demo
10 | // See: https://github.com/apollographql/federation-demo
11 |
12 | const driver = neo4j.driver(
13 | process.env.NEO4J_URI || 'bolt://localhost:7687',
14 | neo4j.auth.basic(
15 | process.env.NEO4J_USER || 'neo4j',
16 | process.env.NEO4J_PASSWORD || 'letmein'
17 | )
18 | );
19 |
20 | // Start Accounts
21 | const accountsService = new ApolloServer({
22 | schema: accountsSchema,
23 | context: ({ req }) => {
24 | return {
25 | driver,
26 | req,
27 | cypherParams: {
28 | userId: 'user-id'
29 | }
30 | };
31 | }
32 | });
33 | accountsService.listen({ port: 4001 }).then(({ url }) => {
34 | console.log(`🚀 Accounts ready at ${url}`);
35 | });
36 |
37 | // Start Reviews
38 | const reviewsService = new ApolloServer({
39 | schema: reviewsSchema,
40 | context: ({ req }) => {
41 | return {
42 | driver,
43 | req,
44 | cypherParams: {
45 | userId: 'user-id'
46 | }
47 | };
48 | }
49 | });
50 | reviewsService.listen({ port: 4002 }).then(({ url }) => {
51 | console.log(`🚀 Reviews ready at ${url}`);
52 | });
53 |
54 | // Start Products
55 | const productsService = new ApolloServer({
56 | schema: productsSchema,
57 | context: ({ req }) => {
58 | return {
59 | driver,
60 | req,
61 | cypherParams: {
62 | userId: 'user-id'
63 | }
64 | };
65 | }
66 | });
67 | productsService.listen({ port: 4003 }).then(({ url }) => {
68 | console.log(`🚀 Products ready at ${url}`);
69 | });
70 |
71 | // Start Inventory
72 | const inventoryService = new ApolloServer({
73 | schema: inventorySchema,
74 | context: ({ req }) => {
75 | return {
76 | driver,
77 | req,
78 | cypherParams: {
79 | userId: 'user-id'
80 | }
81 | };
82 | }
83 | });
84 | inventoryService.listen({ port: 4004 }).then(({ url }) => {
85 | console.log(`🚀 Inventory ready at ${url}`);
86 | });
87 |
88 | const gateway = new ApolloGateway({
89 | serviceList: [
90 | { name: 'accounts', url: 'http://localhost:4001/graphql' },
91 | { name: 'reviews', url: 'http://localhost:4002/graphql' },
92 | { name: 'products', url: 'http://localhost:4003/graphql' },
93 | { name: 'inventory', url: 'http://localhost:4004/graphql' }
94 | ],
95 | // Experimental: Enabling this enables the query plan view in Playground.
96 | __exposeQueryPlanExperimental: true
97 | });
98 |
99 | (async () => {
100 | const server = new ApolloServer({
101 | gateway,
102 |
103 | // Apollo Graph Manager (previously known as Apollo Engine)
104 | // When enabled and an `ENGINE_API_KEY` is set in the environment,
105 | // provides metrics, schema management and trace reporting.
106 | engine: false,
107 |
108 | // Subscriptions are unsupported but planned for a future Gateway version.
109 | subscriptions: false
110 | });
111 |
112 | server.listen({ port: 4000 }).then(({ url }) => {
113 | console.log(`🚀 Apollo Gateway ready at ${url}`);
114 | });
115 | })();
116 |
--------------------------------------------------------------------------------
/example/apollo-federation/seed-data.js:
--------------------------------------------------------------------------------
1 | export const seedData = {
2 | data: {
3 | Review: [
4 | {
5 | id: '1',
6 | body: 'Love it!',
7 | rating: 9.9,
8 | product: {
9 | upc: '1',
10 | name: 'Table',
11 | price: 899,
12 | weight: 100,
13 | inStock: true,
14 | metrics: [
15 | {
16 | id: '100',
17 | metric: 1,
18 | data: 2
19 | }
20 | ],
21 | objectCompoundKey: {
22 | id: '100',
23 | metric: 1,
24 | data: 2
25 | },
26 | listCompoundKey: [
27 | {
28 | id: '100',
29 | metric: 1,
30 | data: 2
31 | }
32 | ],
33 | value: 1
34 | },
35 | authorID: '1',
36 | author: {
37 | id: '1',
38 | name: 'Ada Lovelace',
39 | username: '@ada',
40 | numberOfReviews: 2
41 | }
42 | },
43 | {
44 | id: '2',
45 | body: 'Too expensive.',
46 | rating: 5.5,
47 | product: {
48 | upc: '2',
49 | name: 'Couch',
50 | price: 1299,
51 | weight: 1000,
52 | inStock: false,
53 | metrics: [],
54 | objectCompoundKey: null,
55 | listCompoundKey: [],
56 | value: 2
57 | },
58 | authorID: '1',
59 | author: {
60 | id: '1',
61 | name: 'Ada Lovelace',
62 | username: '@ada',
63 | numberOfReviews: 2
64 | }
65 | },
66 | {
67 | id: '3',
68 | body: 'Could be better.',
69 | rating: 3.8,
70 | product: {
71 | upc: '3',
72 | name: 'Chair',
73 | price: 54,
74 | weight: 50,
75 | inStock: true,
76 | metrics: [],
77 | objectCompoundKey: null,
78 | listCompoundKey: [],
79 | value: 3
80 | },
81 | authorID: '2',
82 | author: {
83 | id: '2',
84 | name: 'Alan Turing',
85 | username: '@complete',
86 | numberOfReviews: 2
87 | }
88 | },
89 | {
90 | id: '4',
91 | body: 'Prefer something else.',
92 | rating: 5.0,
93 | product: {
94 | upc: '1',
95 | name: 'Table',
96 | price: 899,
97 | weight: 100,
98 | inStock: true,
99 | metrics: [
100 | {
101 | id: '100',
102 | metric: 1,
103 | data: 2
104 | }
105 | ],
106 | objectCompoundKey: {
107 | id: '100',
108 | metric: 1,
109 | data: 2
110 | },
111 | listCompoundKey: [
112 | {
113 | id: '100',
114 | metric: 1,
115 | data: 2
116 | }
117 | ],
118 | value: 4
119 | },
120 | authorID: '2',
121 | author: {
122 | id: '2',
123 | name: 'Alan Turing',
124 | username: '@complete',
125 | numberOfReviews: 2
126 | }
127 | }
128 | ]
129 | }
130 | };
131 |
--------------------------------------------------------------------------------
/example/apollo-federation/services/accounts/index.js:
--------------------------------------------------------------------------------
1 | import { gql } from 'apollo-server';
2 | import { buildFederatedSchema } from '@apollo/federation';
3 | import { neo4jgraphql, augmentTypeDefs, cypher } from '../../../../src';
4 |
5 | // Example: without schema augmentation
6 | export const accountsSchema = buildFederatedSchema([
7 | {
8 | // Used to add support for neo4j-graphql directives
9 | // (@cypher / @relation) and types (temporal / spatial)
10 | typeDefs: augmentTypeDefs(
11 | gql`
12 | extend type Query {
13 | me: Account @cypher(${cypher`
14 | MATCH (account: Account {
15 | id: '1'
16 | })
17 | RETURN account
18 | `})
19 | Account: [Account] @cypher(${cypher`
20 | MATCH (account: Account)
21 | RETURN account
22 | `})
23 | }
24 |
25 | type Account @key(fields: "id") {
26 | id: ID!
27 | name: String
28 | username: String
29 | }
30 | `,
31 | {
32 | isFederated: true
33 | }
34 | ),
35 | resolvers: {
36 | Query: {
37 | async me(object, params, context, resolveInfo) {
38 | return await neo4jgraphql(object, params, context, resolveInfo);
39 | },
40 | async Account(object, params, context, resolveInfo) {
41 | return await neo4jgraphql(object, params, context, resolveInfo);
42 | }
43 | },
44 | Account: {
45 | // Base type reference resolver
46 | async __resolveReference(object, context, resolveInfo) {
47 | return await neo4jgraphql(object, {}, context, resolveInfo);
48 | }
49 | }
50 | }
51 | }
52 | ]);
53 |
54 | export const accounts = [
55 | {
56 | id: '1',
57 | name: 'Ada Lovelace',
58 | birthDate: '1815-12-10',
59 | username: '@ada'
60 | },
61 | {
62 | id: '2',
63 | name: 'Alan Turing',
64 | birthDate: '1912-06-23',
65 | username: '@complete'
66 | }
67 | ];
68 |
--------------------------------------------------------------------------------
/example/apollo-federation/services/inventory/index.js:
--------------------------------------------------------------------------------
1 | import { gql } from 'apollo-server';
2 | import { buildFederatedSchema } from '@apollo/federation';
3 | import { makeAugmentedSchema, cypher } from '../../../../src';
4 |
5 | export const inventorySchema = buildFederatedSchema([
6 | makeAugmentedSchema({
7 | typeDefs: gql`
8 | extend type Product @key(fields: "upc listCompoundKey { id } objectCompoundKey { id } nullKey") {
9 | upc: String! @external
10 | weight: Int @external
11 | price: Int @external
12 | nullKey: String @external
13 | inStock: Boolean
14 | shippingEstimate: Int
15 | @requires(fields: "weight price")
16 | @cypher(${cypher`
17 | CALL apoc.when($price > 900,
18 | // free for expensive items
19 | 'RETURN 0 AS value',
20 | // estimate is based on weight
21 | 'RETURN $weight * 0.5 AS value',
22 | {
23 | price: $price,
24 | weight: $weight
25 | })
26 | YIELD value
27 | RETURN value.value
28 | `})
29 | metrics: [Metric]
30 | @requires(fields: "price")
31 | @relation(name: "METRIC_OF", direction: OUT)
32 | objectCompoundKey: Metric
33 | @external
34 | @relation(name: "METRIC_OF", direction: OUT)
35 | listCompoundKey: [Metric]
36 | @external
37 | @relation(name: "METRIC_OF", direction: OUT)
38 | }
39 |
40 | extend type Metric @key(fields: "id") {
41 | id: ID @external
42 | metric: Int @external
43 | data: Int
44 | @requires(fields: "metric")
45 | @cypher(${cypher`
46 | RETURN $metric + 1
47 | `})
48 | }
49 |
50 | `,
51 | resolvers: {
52 | Metric: {
53 | // Generated
54 | // async __resolveReference(object, context, resolveInfo) {
55 | // const data = await neo4jgraphql(object, {}, context, resolveInfo);
56 | // return {
57 | // ...object,
58 | // ...data
59 | // };
60 | // },
61 | }
62 | // Generated
63 | // Product: {
64 | // async __resolveReference(object, context, resolveInfo) {
65 | // const data = await neo4jgraphql(object, {}, context, resolveInfo);
66 | // return {
67 | // ...object,
68 | // ...data
69 | // };
70 | // }
71 | // }
72 | },
73 | config: {
74 | isFederated: true
75 | // debug: true
76 | }
77 | })
78 | ]);
79 |
80 | export const inventory = [
81 | { upc: '1', inStock: true },
82 | { upc: '2', inStock: false },
83 | { upc: '3', inStock: true }
84 | ];
85 |
--------------------------------------------------------------------------------
/example/apollo-federation/services/products/index.js:
--------------------------------------------------------------------------------
1 | import { gql } from 'apollo-server';
2 | import { buildFederatedSchema } from '@apollo/federation';
3 | import { makeAugmentedSchema } from '../../../../src';
4 |
5 | export const productsSchema = buildFederatedSchema([
6 | makeAugmentedSchema({
7 | typeDefs: gql`
8 | extend type Query {
9 | Product: [Product]
10 | topProducts(first: Int = 5): [Product]
11 | }
12 |
13 | type Product
14 | @key(
15 | fields: "upc listCompoundKey { id } objectCompoundKey { id } nullKey"
16 | ) {
17 | upc: String!
18 | name: String
19 | price: Int
20 | weight: Int
21 | nullKey: String
22 | objectCompoundKey: Metric @relation(name: "METRIC_OF", direction: OUT)
23 | listCompoundKey: [Metric] @relation(name: "METRIC_OF", direction: OUT)
24 | }
25 |
26 | type Metric @key(fields: "id") {
27 | id: ID
28 | metric: Int
29 | }
30 | `,
31 | config: {
32 | isFederated: true
33 | // debug: true
34 | }
35 | })
36 | ]);
37 |
38 | export const products = [
39 | {
40 | upc: '1',
41 | name: 'Table',
42 | price: 899,
43 | weight: 100
44 | },
45 | {
46 | upc: '2',
47 | name: 'Couch',
48 | price: 1299,
49 | weight: 1000
50 | },
51 | {
52 | upc: '3',
53 | name: 'Chair',
54 | price: 54,
55 | weight: 50
56 | }
57 | ];
58 |
--------------------------------------------------------------------------------
/example/apollo-federation/services/reviews/index.js:
--------------------------------------------------------------------------------
1 | import { gql } from 'apollo-server';
2 | import { buildFederatedSchema } from '@apollo/federation';
3 | import { makeAugmentedSchema, neo4jgraphql, cypher } from '../../../../src';
4 | import { seedData } from '../../seed-data';
5 |
6 | export const reviewsSchema = buildFederatedSchema([
7 | makeAugmentedSchema({
8 | typeDefs: gql`
9 | type Review @key(fields: "id authorID") {
10 | id: ID!
11 | body: String
12 | authorID: ID
13 | # Scalar property to lookup associate node
14 | author: Account
15 | @cypher(${cypher`
16 | MATCH (account:Account {id: this.authorID})
17 | RETURN account
18 | `})
19 | # Normal use of @relation field directive
20 | product: Product
21 | @relation(name: "REVIEW_OF", direction: OUT)
22 | ratings: [Rating]
23 | }
24 |
25 | extend type Account @key(fields: "id") {
26 | id: ID! @external
27 | # Object list @relation field added to nonlocal type for local type
28 | reviews(body: String): [Review]
29 | @relation(name: "AUTHOR_OF", direction: OUT)
30 | # Scalar @cypher field added to nonlocal type for local type
31 | numberOfReviews: Int
32 | @cypher(${cypher`
33 | MATCH (this)-[:AUTHOR_OF]->(review:Review)
34 | RETURN count(review)
35 | `})
36 | product: [Product] @relation(name: "PRODUCT_ACCOUNT", direction: IN)
37 | entityRelationship: [EntityRelationship]
38 | }
39 |
40 | extend type Product @key(fields: "upc") {
41 | upc: String! @external
42 | reviews(body: String): [Review]
43 | @relation(name: "REVIEW_OF", direction: IN)
44 | ratings: [Rating]
45 | account(filter: LocalAccountFilter): [Account] @relation(name: "PRODUCT_ACCOUNT", direction: OUT)
46 | entityRelationship: [EntityRelationship]
47 | }
48 |
49 | input LocalAccountFilter {
50 | id_not: ID
51 | }
52 |
53 | type Rating @relation(name: "REVIEW_OF") {
54 | from: Review
55 | rating: Float
56 | to: Product
57 | }
58 |
59 | type EntityRelationship @relation(name: "PRODUCT_ACCOUNT") {
60 | from: Product
61 | value: Int
62 | to: Account
63 | }
64 |
65 | # Used in testing and for example of nested merge import
66 | input MergeReviewsInput {
67 | id: ID!
68 | body: String
69 | product: MergeProductInput
70 | author: MergeAccountInput
71 | }
72 |
73 | input MergeProductInput {
74 | upc: String!
75 | name: String
76 | price: Int
77 | weight: Int
78 | inStock: Boolean
79 | metrics: [MergeMetricInput]
80 | objectCompoundKey: MergeMetricInput
81 | listCompoundKey: [MergeMetricInput]
82 | }
83 |
84 | input MergeMetricInput {
85 | id: ID!
86 | metric: String
87 | }
88 |
89 | input MergeAccountInput {
90 | id: ID!
91 | name: String
92 | username: String
93 | }
94 |
95 | extend type Mutation {
96 | MergeSeedData(data: MergeReviewsInput): Boolean @cypher(${cypher`
97 | UNWIND $data AS review
98 | MERGE (r:Review {
99 | id: review.id
100 | })
101 | SET r += {
102 | body: review.body,
103 | authorID: review.authorID
104 | }
105 | WITH *
106 |
107 | // Merge Review.author
108 | UNWIND review.author AS account
109 | MERGE (a:Account {
110 | id: account.id
111 | })
112 | ON CREATE SET a += {
113 | name: account.name,
114 | username: account.username
115 | }
116 | MERGE (r)<-[:AUTHOR_OF]-(a)
117 | // Resets processing context for unwound sibling relationship data
118 | WITH COUNT(*) AS SCOPE
119 |
120 | // Unwind second sibling, Review.product
121 | UNWIND $data AS review
122 | MATCH (r:Review {
123 | id: review.id
124 | })
125 | // Merge Review.product
126 | UNWIND review.product AS product
127 | MERGE (p:Product {
128 | upc: product.upc
129 | })
130 | ON CREATE SET p += {
131 | name: product.name,
132 | price: product.price,
133 | weight: product.weight,
134 | inStock: product.inStock
135 | }
136 | MERGE (p)<-[:REVIEW_OF {
137 | rating: review.rating
138 | }]-(r)
139 | WITH *
140 | UNWIND review.author AS account
141 | MATCH (a:Account {
142 | id: account.id
143 | })
144 | MERGE (p)-[:PRODUCT_ACCOUNT {
145 | value: product.value
146 | }]->(a)
147 | WITH *
148 | // Merge Review.product.metrics / .objectCompoundKey / .listCompoundKey
149 | UNWIND product.metrics AS metric
150 | MERGE (m:Metric {
151 | id: metric.id
152 | })
153 | ON CREATE SET m += {
154 | metric: metric.metric
155 | }
156 | MERGE (p)-[:METRIC_OF]->(m)
157 | // End processing scope for Review.product
158 | WITH COUNT(*) AS SCOPE
159 |
160 | RETURN true
161 | `})
162 | DeleteSeedData: Boolean @cypher(${cypher`
163 | MATCH (account: Account)
164 | MATCH (product: Product)
165 | MATCH (review: Review)
166 | MATCH (metric: Metric)
167 | DETACH DELETE account, product, review, metric
168 | RETURN TRUE
169 | `})
170 | }
171 |
172 | `,
173 | resolvers: {
174 | Mutation: {
175 | async MergeSeedData(object, params, context, resolveInfo) {
176 | const data = seedData.data['Review'];
177 | return await neo4jgraphql(object, { data }, context, resolveInfo);
178 | }
179 | },
180 | Account: {
181 | // Generated
182 | // async __resolveReference(object, context, resolveInfo) {
183 | // const data = await neo4jgraphql(object, {}, context, resolveInfo);
184 | // return {
185 | // ...object,
186 | // ...data
187 | // };
188 | // }
189 | },
190 | Product: {
191 | // Generated
192 | // async __resolveReference(object, context, resolveInfo) {
193 | // const data = await neo4jgraphql(object, {}, context, resolveInfo);
194 | // return {
195 | // ...object,
196 | // ...data
197 | // };
198 | // }
199 | }
200 | },
201 | config: {
202 | isFederated: true
203 | // debug: true
204 | }
205 | })
206 | ]);
207 |
208 | export const reviews = [
209 | {
210 | id: '1',
211 | authorID: '1',
212 | product: { upc: '1' },
213 | body: 'Love it!'
214 | },
215 | {
216 | id: '2',
217 | authorID: '1',
218 | product: { upc: '2' },
219 | body: 'Too expensive.'
220 | },
221 | {
222 | id: '3',
223 | authorID: '2',
224 | product: { upc: '3' },
225 | body: 'Could be better.'
226 | },
227 | {
228 | id: '4',
229 | authorID: '2',
230 | product: { upc: '1' },
231 | body: 'Prefer something else.'
232 | }
233 | ];
234 |
--------------------------------------------------------------------------------
/example/apollo-server/authScopes.js:
--------------------------------------------------------------------------------
1 | import { makeAugmentedSchema } from '../../src/index';
2 | import { ApolloServer } from 'apollo-server';
3 | import neo4j from 'neo4j-driver';
4 |
5 | // JWT
6 | // eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJzY29wZXMiOlsicmVhZDp1c2VyIiwiY3JlYXRlOnVzZXIiXX0.jCidMhYKk_0s8aQpXojYwZYz00eIG9lD_DbeXRKj4vA
7 |
8 | // scopes
9 | // "scopes": ["read:user", "create:user"]
10 |
11 | // JWT_SECRET
12 | // oqldBPU1yMXcrTwcha1a9PGi9RHlPVzQ
13 |
14 | const typeDefs = `
15 | type User {
16 | userId: ID!
17 | name: String
18 | }
19 |
20 | type Business {
21 | name: String
22 | }
23 | `;
24 |
25 | const schema = makeAugmentedSchema({
26 | typeDefs,
27 | config: { auth: { hasScope: true } }
28 | });
29 |
30 | const driver = neo4j.driver(
31 | 'bolt://localhost:7687',
32 | neo4j.auth.basic('neo4j', 'letmein')
33 | );
34 |
35 | const server = new ApolloServer({
36 | schema,
37 | context: ({ req }) => {
38 | return {
39 | req,
40 | driver
41 | };
42 | }
43 | });
44 |
45 | server.listen().then(({ url }) => {
46 | console.log(`GraphQL API ready at ${url}`);
47 | });
48 |
--------------------------------------------------------------------------------
/example/apollo-server/bookmarks.js:
--------------------------------------------------------------------------------
1 | import { makeAugmentedSchema } from '../../src/index';
2 | import { ApolloServer } from 'apollo-server';
3 | import neo4j from 'neo4j-driver';
4 |
5 | const typeDefs = `
6 | type Person {
7 | _id: Long!
8 | born: Int
9 | name: String!
10 | acted_in: [Movie] @relation(name: "ACTED_IN", direction: "OUT")
11 | ACTED_IN_rel: [ACTED_IN]
12 | directed: [Movie] @relation(name: "DIRECTED", direction: "OUT")
13 | produced: [Movie] @relation(name: "PRODUCED", direction: "OUT")
14 | wrote: [Movie] @relation(name: "WROTE", direction: "OUT")
15 | follows: [Person] @relation(name: "FOLLOWS", direction: "OUT")
16 | reviewed: [Movie] @relation(name: "REVIEWED", direction: "OUT")
17 | REVIEWED_rel: [REVIEWED]
18 | }
19 |
20 | type Movie {
21 | _id: Long!
22 | released: Int!
23 | tagline: String
24 | title: String!
25 | persons_acted_in: [Person] @relation(name: "ACTED_IN", direction: "IN")
26 | persons_directed: [Person] @relation(name: "DIRECTED", direction: "IN")
27 | persons_produced: [Person] @relation(name: "PRODUCED", direction: "IN")
28 | persons_wrote: [Person] @relation(name: "WROTE", direction: "IN")
29 | persons_reviewed: [Person] @relation(name: "REVIEWED", direction: "IN")
30 | }
31 |
32 | type ACTED_IN @relation(name: "ACTED_IN") {
33 | from: Person!
34 | to: Movie!
35 | roles: [String]!
36 | }
37 |
38 | type REVIEWED @relation(name: "REVIEWED") {
39 | from: Person!
40 | to: Movie!
41 | rating: Int!
42 | summary: String!
43 | }
44 |
45 | `;
46 |
47 | const schema = makeAugmentedSchema({ typeDefs });
48 |
49 | const driver = neo4j.driver(
50 | 'bolt://localhost:7687',
51 | neo4j.auth.basic('neo4j', 'letmein')
52 | );
53 |
54 | const server = new ApolloServer({
55 | schema,
56 | context: ({ req }) => {
57 | return {
58 | driver,
59 | neo4jBookmarks: req.headers['neo4jbookmark']
60 | };
61 | }
62 | });
63 |
64 | server.listen(3003, '0.0.0.0').then(({ url }) => {
65 | console.log(`GraphQL API ready at ${url}`);
66 | });
67 |
--------------------------------------------------------------------------------
/example/apollo-server/interface-union-example.js:
--------------------------------------------------------------------------------
1 | import { makeAugmentedSchema } from '../../src/index';
2 | const { ApolloServer } = require('apollo-server');
3 | const neo4j = require('neo4j-driver');
4 |
5 | const __unionTypeDefs = `
6 | union SearchResult = Blog | Movie
7 |
8 | type Blog {
9 | blogId: ID!
10 | created: DateTime
11 | content: String
12 | }
13 |
14 | type Movie {
15 | movieId: ID!
16 | title: String
17 | }
18 |
19 | type Query {
20 | search(searchString: String!): [SearchResult] @cypher(statement:"CALL db.index.fulltext.queryNodes('searchIndex', $searchString) YIELD node RETURN node")
21 | }
22 | `;
23 |
24 | const __interfaceTypeDefs = `
25 | interface Person {
26 | id: ID!
27 | name: String
28 | }
29 |
30 | type User implements Person {
31 | id: ID!
32 | name: String
33 | screenName: String
34 | reviews: [Review] @relation(name: "WROTE", direction: OUT)
35 | }
36 |
37 | type Actor implements Person {
38 | id: ID!
39 | name: String
40 | movies: [Movie] @relation(name: "ACTED_IN", direction: OUT)
41 | }
42 |
43 | type Movie {
44 | movieId: ID!
45 | title: String
46 | }
47 |
48 | type Review {
49 | rating: Int
50 | created: DateTime
51 | movie: Movie @relation(name: "REVIEWS", direction: OUT)
52 | }
53 | `;
54 |
55 | const typeDefs = /* GraphQL*/ `
56 |
57 | union SearchResult = Review | Actor | Movie
58 |
59 | interface Person {
60 | id: ID!
61 | name: String
62 | friends: [Person] @relation(name: "FRIEND_OF", direction: OUT)
63 | }
64 |
65 | type User implements Person {
66 | id: ID!
67 | name: String
68 | screenName: String
69 | reviews: [Review] @relation(name: "WROTE", direction: OUT)
70 | friends: [Person] @relation(name: "FRIEND_OF", direction: OUT)
71 | searched: [SearchResult]
72 | }
73 |
74 | type Actor implements Person {
75 | id: ID!
76 | name: String
77 | movies: [Movie] @relation(name: "ACTED_IN", direction: OUT)
78 | friends: [Person] @relation(name: "FRIEND_OF", direction: OUT)
79 | }
80 |
81 | type Movie {
82 | movieId: ID!
83 | title: String
84 | }
85 |
86 | type Review {
87 | rating: Int
88 | movie: Movie @relation(name: "REVIEWS", direction: OUT)
89 | }
90 |
91 | `;
92 |
93 | const driver = neo4j.driver(
94 | 'neo4j://localhost:7687',
95 | neo4j.auth.basic('neo4j', 'letmein'),
96 | { encrypted: false }
97 | );
98 |
99 | const server = new ApolloServer({
100 | schema: makeAugmentedSchema({ typeDefs }),
101 | context: ({ req }) => {
102 | return { driver };
103 | }
104 | });
105 |
106 | server.listen(3003, '0.0.0.0').then(({ url }) => {
107 | console.log(`GraphQL API ready at ${url}`);
108 | });
109 |
--------------------------------------------------------------------------------
/example/apollo-server/movies-middleware.js:
--------------------------------------------------------------------------------
1 | import { makeAugmentedSchema } from '../../src/index';
2 | import { ApolloServer } from 'apollo-server-express';
3 | import express from 'express';
4 | import bodyParser from 'body-parser';
5 | import neo4j from 'neo4j-driver';
6 | import { typeDefs, resolvers } from './movies-schema';
7 |
8 | const schema = makeAugmentedSchema({
9 | typeDefs,
10 | resolvers
11 | });
12 |
13 | const driver = neo4j.driver(
14 | process.env.NEO4J_URI || 'bolt://localhost:7687',
15 | neo4j.auth.basic(
16 | process.env.NEO4J_USER || 'neo4j',
17 | process.env.NEO4J_PASSWORD || 'letmein'
18 | )
19 | );
20 |
21 | const app = express();
22 | app.use(bodyParser.json());
23 |
24 | const checkErrorHeaderMiddleware = async (req, res, next) => {
25 | req.error = req.headers['x-error'];
26 | next();
27 | };
28 |
29 | app.use('*', checkErrorHeaderMiddleware);
30 |
31 | const server = new ApolloServer({
32 | schema,
33 | // inject the request object into the context to support middleware
34 | // inject the Neo4j driver instance to handle database call
35 | context: ({ req }) => {
36 | return {
37 | driver,
38 | req
39 | };
40 | }
41 | });
42 |
43 | server.applyMiddleware({ app, path: '/' });
44 | app.listen(3000, '0.0.0.0');
45 |
--------------------------------------------------------------------------------
/example/apollo-server/movies-schema.js:
--------------------------------------------------------------------------------
1 | import { neo4jgraphql } from '../../src/index';
2 |
3 | export const typeDefs = `
4 | type Movie {
5 | movieId: ID!
6 | title: String
7 | someprefix_title_with_underscores: String
8 | year: Int
9 | dateTime: DateTime
10 | localDateTime: LocalDateTime
11 | date: Date
12 | plot: String
13 | poster: String
14 | imdbRating: Float
15 | ratings: [Rated]
16 | genres: [Genre] @relation(name: "IN_GENRE", direction: "OUT")
17 | similar(first: Int = 3, offset: Int = 0, limit: Int = 5): [Movie] @cypher(statement: "MATCH (this)--(:Genre)--(o:Movie) RETURN o LIMIT $limit")
18 | mostSimilar: Movie @cypher(statement: "RETURN this")
19 | degree: Int @cypher(statement: "RETURN SIZE((this)--())")
20 | actors(first: Int = 3, offset: Int = 0): [Actor] @relation(name: "ACTED_IN", direction:"IN")
21 | avgStars: Float
22 | filmedIn: State @relation(name: "FILMED_IN", direction: "OUT")
23 | location: Point
24 | locations: [Point]
25 | scaleRating(scale: Int = 3): Float @cypher(statement: "RETURN $scale * this.imdbRating")
26 | scaleRatingFloat(scale: Float = 1.5): Float @cypher(statement: "RETURN $scale * this.imdbRating")
27 | _id: ID
28 | }
29 |
30 | type Genre {
31 | name: String
32 | movies(first: Int = 3, offset: Int = 0): [Movie] @relation(name: "IN_GENRE", direction: "IN")
33 | highestRatedMovie: Movie @cypher(statement: "MATCH (m:Movie)-[:IN_GENRE]->(this) RETURN m ORDER BY m.imdbRating DESC LIMIT 1")
34 | }
35 |
36 | type State {
37 | name: String
38 | }
39 |
40 | interface Person {
41 | userId: ID!
42 | name: String
43 | }
44 |
45 | type Actor {
46 | id: ID!
47 | name: String
48 | movies: [Movie] @relation(name: "ACTED_IN", direction: "OUT")
49 | knows: [Person] @relation(name: "KNOWS", direction: "OUT")
50 | }
51 |
52 | type User implements Person {
53 | userId: ID!
54 | name: String
55 | rated: [Rated]
56 | }
57 |
58 | type Rated @relation(name:"RATED") {
59 | from: User
60 | to: Movie
61 | timestamp: Int
62 | date: Date
63 | rating: Float
64 | }
65 | enum BookGenre {
66 | Mystery,
67 | Science,
68 | Math
69 | }
70 |
71 | type OnlyDate {
72 | date: Date
73 | }
74 |
75 | type SpatialNode {
76 | id: ID!
77 | point: Point
78 | spatialNodes(point: Point): [SpatialNode]
79 | @relation(name: "SPATIAL", direction: OUT)
80 | }
81 |
82 | type Book {
83 | genre: BookGenre
84 | }
85 |
86 | interface Camera {
87 | id: ID!
88 | type: String
89 | make: String
90 | weight: Int
91 | operators: [Person] @relation(name: "cameras", direction: IN)
92 | computedOperators(name: String): [Person] @cypher(statement: "MATCH (this)<-[:cameras]-(p:Person) RETURN p")
93 | }
94 |
95 | type OldCamera implements Camera {
96 | id: ID!
97 | type: String
98 | make: String
99 | weight: Int
100 | smell: String
101 | operators: [Person] @relation(name: "cameras", direction: IN)
102 | computedOperators(name: String): [Person] @cypher(statement: "MATCH (this)<-[:cameras]-(p:Person) RETURN p")
103 | }
104 |
105 | type NewCamera implements Camera {
106 | id: ID!
107 | type: String
108 | make: String
109 | weight: Int
110 | features: [String]
111 | operators: [Person] @relation(name: "cameras", direction: IN)
112 | computedOperators(name: String): [Person] @cypher(statement: "MATCH (this)<-[:cameras]-(p:Person) RETURN p")
113 | }
114 |
115 | type CameraMan implements Person {
116 | userId: ID!
117 | name: String
118 | favoriteCamera: Camera @relation(name: "favoriteCamera", direction: "OUT")
119 | heaviestCamera: [Camera] @cypher(statement: "MATCH (c: Camera)--(this) RETURN c ORDER BY c.weight DESC LIMIT 1")
120 | cameras: [Camera!]! @relation(name: "cameras", direction: "OUT")
121 | cameraBuddy: Person @relation(name: "cameraBuddy", direction: "OUT")
122 | }
123 |
124 | union MovieSearch = Movie | Genre | Book | User | OldCamera
125 |
126 | type Query {
127 | Movie(movieId: ID, title: String, year: Int, plot: String, poster: String, imdbRating: Float): [Movie]
128 | MoviesByYear(year: Int, first: Int = 10, offset: Int = 0): [Movie]
129 | AllMovies: [Movie]
130 | MovieById(movieId: ID!): Movie
131 | GenresBySubstring(substring: String): [Genre] @cypher(statement: "MATCH (g:Genre) WHERE toLower(g.name) CONTAINS toLower($substring) RETURN g")
132 | Books: [Book]
133 | CustomCameras: [Camera] @cypher(statement: "MATCH (c:Camera) RETURN c")
134 | CustomCamera: Camera @cypher(statement: "MATCH (c:Camera) RETURN c")
135 | }
136 |
137 | type Mutation {
138 | CustomCamera: Camera @cypher(statement: "CREATE (newCamera:Camera:NewCamera {id: apoc.create.uuid(), type: 'macro'}) RETURN newCamera")
139 | CustomCameras: [Camera] @cypher(statement: "CREATE (newCamera:Camera:NewCamera {id: apoc.create.uuid(), type: 'macro', features: ['selfie', 'zoom']}) CREATE (oldCamera:Camera:OldCamera {id: apoc.create.uuid(), type: 'floating', smell: 'rusty' }) RETURN [newCamera, oldCamera]")
140 | }
141 | `;
142 |
143 | export const resolvers = {
144 | // root entry point to GraphQL service
145 | Query: {
146 | Movie(object, params, ctx, resolveInfo) {
147 | return neo4jgraphql(object, params, ctx, resolveInfo, true);
148 | },
149 | MoviesByYear(object, params, ctx, resolveInfo) {
150 | return neo4jgraphql(object, params, ctx, resolveInfo, true);
151 | },
152 | AllMovies(object, params, ctx, resolveInfo) {
153 | return neo4jgraphql(object, params, ctx, resolveInfo, true);
154 | },
155 | MovieById(object, params, ctx, resolveInfo) {
156 | return neo4jgraphql(object, params, ctx, resolveInfo, true);
157 | },
158 | GenresBySubstring(object, params, ctx, resolveInfo) {
159 | return neo4jgraphql(object, params, ctx, resolveInfo, true);
160 | },
161 | Books(object, params, ctx, resolveInfo) {
162 | return neo4jgraphql(object, params, ctx, resolveInfo, true);
163 | }
164 | }
165 | };
166 |
--------------------------------------------------------------------------------
/example/apollo-server/movies-typedefs.js:
--------------------------------------------------------------------------------
1 | import { makeAugmentedSchema } from '../../src/index';
2 | import { ApolloServer } from 'apollo-server';
3 | import { v1 as neo4j } from 'neo4j-driver';
4 |
5 | const typeDefs = `
6 | type Movie {
7 | movieId: ID!
8 | title: String
9 | year: Int
10 | plot: String
11 | poster: String
12 | imdbRating: Float
13 | ratings: [Rated]
14 | genres: [Genre] @relation(name: "IN_GENRE", direction: "OUT")
15 | actors: [Actor] @relation(name: "ACTED_IN", direction: "IN")
16 | }
17 | type Genre {
18 | name: String
19 | movies: [Movie] @relation(name: "IN_GENRE", direction: "IN")
20 | }
21 | type Actor {
22 | id: ID!
23 | name: String
24 | movies: [Movie] @relation(name: "ACTED_IN", direction: "OUT")
25 | }
26 | type User {
27 | userId: ID!
28 | name: String
29 | rated: [Rated]
30 | recommendedMovies: [Movie]
31 | @cypher(
32 | statement: """
33 | MATCH (this)-[:RATED]->(:Movie)<-[:RATED]-(:User)-[:RATED]->(rec:Movie)
34 | WITH rec, COUNT(*) AS score ORDER BY score DESC LIMIT 10
35 | RETURN rec
36 | """
37 | )
38 | }
39 | type Rated @relation(name: "RATED") {
40 | from: User
41 | to: Movie
42 | rating: Float
43 | created: DateTime
44 | }
45 | `;
46 |
47 | const schema = makeAugmentedSchema({ typeDefs });
48 |
49 | const driver = neo4j.driver(
50 | 'bolt://localhost:7687',
51 | neo4j.auth.basic('neo4j', 'letmein')
52 | );
53 |
54 | const server = new ApolloServer({ schema, context: { driver } });
55 |
56 | server.listen(3003, '0.0.0.0').then(({ url }) => {
57 | console.log(`GraphQL API ready at ${url}`);
58 | });
59 |
--------------------------------------------------------------------------------
/example/apollo-server/movies.js:
--------------------------------------------------------------------------------
1 | import { augmentTypeDefs, augmentSchema } from '../../src/index';
2 | import { ApolloServer, gql, makeExecutableSchema } from 'apollo-server';
3 | import neo4j from 'neo4j-driver';
4 | import { typeDefs, resolvers } from './movies-schema';
5 |
6 | const schema = makeExecutableSchema({
7 | typeDefs: augmentTypeDefs(typeDefs),
8 | resolverValidationOptions: {
9 | requireResolversForResolveType: false
10 | },
11 | resolvers
12 | });
13 |
14 | // Add auto-generated mutations
15 | const augmentedSchema = augmentSchema(schema);
16 |
17 | const driver = neo4j.driver(
18 | process.env.NEO4J_URI || 'bolt://localhost:7687',
19 | neo4j.auth.basic(
20 | process.env.NEO4J_USER || 'neo4j',
21 | process.env.NEO4J_PASSWORD || 'letmein'
22 | )
23 | );
24 |
25 | const server = new ApolloServer({
26 | schema: augmentedSchema,
27 | // inject the request object into the context to support middleware
28 | // inject the Neo4j driver instance to handle database call
29 | context: ({ req }) => {
30 | return {
31 | driver,
32 | req
33 | };
34 | }
35 | });
36 |
37 | server
38 | .listen(process.env.GRAPHQL_LISTEN_PORT || 3000, '0.0.0.0')
39 | .then(({ url }) => {
40 | console.log(`GraphQL API ready at ${url}`);
41 | });
42 |
--------------------------------------------------------------------------------
/example/apollo-server/multidatabase.js:
--------------------------------------------------------------------------------
1 | import { makeAugmentedSchema } from '../../src/index';
2 | import { ApolloServer } from 'apollo-server';
3 | import neo4j from 'neo4j-driver';
4 |
5 | const typeDefs = `
6 |
7 | type User {
8 | name: String!
9 | wrote: [Review] @relation(name: "WROTE", direction: "OUT")
10 | }
11 | type Review {
12 | date: Date!
13 | reviewId: String!
14 | stars: Float!
15 | text: String
16 | reviews: [Business] @relation(name: "REVIEWS", direction: "OUT")
17 | users: [User] @relation(name: "WROTE", direction: "IN")
18 | }
19 | type Category {
20 | name: String!
21 | business: [Business] @relation(name: "IN_CATEGORY", direction: "IN")
22 | }
23 | type Business {
24 | address: String!
25 | city: String!
26 | location: Point!
27 | name: String!
28 | state: String!
29 | in_category: [Category] @relation(name: "IN_CATEGORY", direction: "OUT")
30 | reviews: [Review] @relation(name: "REVIEWS", direction: "IN")
31 | }
32 | `;
33 |
34 | const schema = makeAugmentedSchema({ typeDefs });
35 |
36 | const driver = neo4j.driver(
37 | 'neo4j://localhost:7687',
38 | neo4j.auth.basic('neo4j', 'letmein'),
39 | { encrypted: false }
40 | );
41 |
42 | // Create two separate servers by hardcoding value for context.neo4jDatabase
43 | // const sanmateoServer = new ApolloServer({
44 | // schema,
45 | // context: { driver, neo4jDatabase: 'sanmateo' }
46 | // });
47 |
48 | // sanmateoServer.listen(3003, '0.0.0.0').then(({ url }) => {
49 | // console.log(`San Mateo GraphQL API ready at ${url}`);
50 | // });
51 |
52 | // const missoulaServer = new ApolloServer({
53 | // schema,
54 | // context: { driver, neo4jDatabase: 'missoula' }
55 | // });
56 |
57 | // missoulaServer.listen(3004, '0.0.0.0').then(({ url }) => {
58 | // console.log(`Missoula GraphQL API ready at ${url}`);
59 | // });
60 |
61 | // Or we can add a header to the request
62 | const server = new ApolloServer({
63 | schema,
64 | context: ({ req }) => {
65 | return { driver, neo4jDatabase: req.headers['x-database'] };
66 | }
67 | });
68 |
69 | server.listen(3003, '0.0.0.0').then(({ url }) => {
70 | console.log(`GraphQL API ready at ${url}`);
71 | });
72 |
--------------------------------------------------------------------------------
/example/autogenerated/autogen-middleware.js:
--------------------------------------------------------------------------------
1 | import { makeAugmentedSchema } from '../../src/index';
2 | import { ApolloServer } from 'apollo-server-express';
3 | import express from 'express';
4 | import bodyParser from 'body-parser';
5 | import { v1 as neo4j } from 'neo4j-driver';
6 | import { typeDefs, resolvers } from './movies-schema';
7 |
8 | const schema = makeAugmentedSchema({
9 | typeDefs,
10 | resolvers,
11 | resolverValidationOptions: {
12 | requireResolversForResolveType: false
13 | }
14 | });
15 |
16 | // Add auto-generated mutations
17 | //const augmentedSchema = augmentSchema(schema);
18 |
19 | const driver = neo4j.driver(
20 | process.env.NEO4J_URI || 'bolt://localhost:7687',
21 | neo4j.auth.basic(
22 | process.env.NEO4J_USER || 'neo4j',
23 | process.env.NEO4J_PASSWORD || 'letmein'
24 | )
25 | );
26 |
27 | const app = express();
28 | app.use(bodyParser.json());
29 |
30 | const checkErrorHeaderMiddleware = async (req, res, next) => {
31 | req.error = req.headers['x-error'];
32 | next();
33 | };
34 |
35 | app.use('*', checkErrorHeaderMiddleware);
36 |
37 | const server = new ApolloServer({
38 | schema,
39 | // inject the request object into the context to support middleware
40 | // inject the Neo4j driver instance to handle database call
41 | context: ({ req }) => {
42 | return {
43 | driver,
44 | req
45 | };
46 | }
47 | });
48 |
49 | server.applyMiddleware({ app, path: '/' });
50 | app.listen(3000, '0.0.0.0');
51 |
--------------------------------------------------------------------------------
/example/autogenerated/autogen.js:
--------------------------------------------------------------------------------
1 | import { makeAugmentedSchema, inferSchema } from '../../src/index';
2 | import { ApolloServer } from 'apollo-server';
3 | import neo4j from 'neo4j-driver';
4 |
5 | const driver = neo4j.driver(
6 | process.env.NEO4J_URI || 'bolt://localhost:7687',
7 | neo4j.auth.basic(
8 | process.env.NEO4J_USER || 'neo4j',
9 | process.env.NEO4J_PASSWORD || 'letmein'
10 | )
11 | );
12 |
13 | const schemaInferenceOptions = {
14 | alwaysIncludeRelationships: false
15 | };
16 |
17 | const inferAugmentedSchema = driver => {
18 | return inferSchema(driver, schemaInferenceOptions).then(result => {
19 | console.log('TYPEDEFS:');
20 | console.log(result.typeDefs);
21 |
22 | return makeAugmentedSchema({
23 | typeDefs: result.typeDefs
24 | });
25 | });
26 | };
27 |
28 | const createServer = augmentedSchema =>
29 | new ApolloServer({
30 | schema: augmentedSchema,
31 | // inject the request object into the context to support middleware
32 | // inject the Neo4j driver instance to handle database call
33 | context: ({ req }) => {
34 | return {
35 | driver,
36 | req
37 | };
38 | }
39 | });
40 |
41 | const port = process.env.GRAPHQL_LISTEN_PORT || 3000;
42 |
43 | inferAugmentedSchema(driver)
44 | .then(createServer)
45 | .then(server => server.listen(port, '0.0.0.0'))
46 | .then(({ url }) => {
47 | console.log(`GraphQL API ready at ${url}`);
48 | })
49 | .catch(err => console.error(err));
50 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "neo4j-graphql-js",
3 | "version": "2.19.4",
4 | "description": "A GraphQL to Cypher query execution layer for Neo4j. ",
5 | "main": "./dist/index.js",
6 | "types": "./index.d.ts",
7 | "scripts": {
8 | "start": "nodemon ./example/apollo-server/movies.js --exec babel-node -e js",
9 | "autogen": "nodemon ./example/autogenerated/autogen.js --exec babel-node -e js",
10 | "start-middleware": "nodemon ./example/apollo-server/movies-middleware.js --exec babel-node -e js",
11 | "start-typedefs": "nodemon ./example/apollo-server/movies-typedefs.js --exec babel-node -e js",
12 | "start-interface": "DEBUG=neo4j-graphql.js nodemon ./example/apollo-server/interface-union-example.js --exec babel-node -e js",
13 | "start-gateway": "nodemon ./example/apollo-federation/gateway.js --exec babel-node -e js",
14 | "start-bookmark-example": "nodemon ./example/apollo-server/bookmarks.js --exec babel-node -e js",
15 | "start-auth-example": "JWT_SECRET=oqldBPU1yMXcrTwcha1a9PGi9RHlPVzQ nodemon ./example/apollo-server/authScopes.js --exec babel-node -e js",
16 | "build": "babel src --presets @babel/preset-env --out-dir dist",
17 | "build-with-sourcemaps": "babel src --presets @babel/preset-env --out-dir dist --source-maps",
18 | "precommit": "lint-staged",
19 | "prepare": "npm run build",
20 | "test": "nyc --reporter=lcov ava test/unit/augmentSchemaTest.test.js test/unit/configTest.test.js test/unit/assertSchema.test.js test/unit/searchSchema.test.js test/unit/cypherTest.test.js test/unit/filterTest.test.js test/unit/filterTests.test.js test/unit/custom/cypherTest.test.js test/unit/experimental/augmentSchemaTest.test.js test/unit/experimental/cypherTest.test.js test/unit/experimental/custom/cypherTest.test.js",
21 | "parse-tck": "babel-node test/helpers/tck/parseTck.js",
22 | "test-tck": "nyc ava --fail-fast test/unit/filterTests.test.js",
23 | "report-coverage": "nyc report --reporter=text-lcov > coverage.lcov && codecov",
24 | "test-all": "nyc ava --verbose",
25 | "test-isolated": "nyc ava test/**/*.test.js --verbose --match='!*not-isolated*'",
26 | "test-gateway": "nyc --reporter=lcov ava --fail-fast test/integration/gateway.test.js",
27 | "test-infer": "nyc --reporter=lcov ava --fail-fast test/unit/neo4j-schema/*.js",
28 | "debug": "nodemon ./example/apollo-server/movies.js --exec babel-node --inspect-brk=9229 --nolazy",
29 | "debug-typedefs": "nodemon ./example/apollo-server/movies-typedefs.js --exec babel-node --inspect-brk=9229 --nolazy",
30 | "debug-interface": "nodemon ./example/apollo-server/interfaceError.js --exec babel-node --inspect-brk=9229 --nolazy",
31 | "debug-gateway": "nodemon ./example/apollo-federation/gateway.js --exec babel-node --inspect-brk=9229 --nolazy"
32 | },
33 | "engines": {
34 | "node": ">=8"
35 | },
36 | "author": "William Lyon",
37 | "license": "Apache-2.0",
38 | "repository": {
39 | "type": "git",
40 | "url": "git+https://github.com/neo4j-graphql/neo4j-graphql-js"
41 | },
42 | "devDependencies": {
43 | "@apollo/federation": "^0.20.7",
44 | "@apollo/gateway": "^0.14.1",
45 | "@babel/cli": "^7.0.0",
46 | "@babel/core": "^7.0.0",
47 | "@babel/node": "^7.0.0",
48 | "@babel/plugin-proposal-async-generator-functions": "^7.0.0",
49 | "@babel/plugin-proposal-object-rest-spread": "^7.0.0",
50 | "@babel/plugin-transform-runtime": "^7.0.0",
51 | "@babel/preset-env": "^7.10.4",
52 | "@graphql-inspector/core": "^2.3.0",
53 | "apollo-cache-inmemory": "^1.6.3",
54 | "apollo-client": "^2.6.10",
55 | "apollo-link-http": "^1.5.16",
56 | "apollo-server": "^2.19.0",
57 | "apollo-server-express": "^2.19.0",
58 | "ava": "^2.2.0",
59 | "body-parser": "^1.18.3",
60 | "express": "^4.17.1",
61 | "graphql-tag": "^2.10.1",
62 | "husky": "^0.14.3",
63 | "lint-staged": "^7.2.0",
64 | "node-fetch": "^2.3.0",
65 | "nodemon": "^1.18.11",
66 | "nyc": "^14.1.1",
67 | "prettier": "^1.17.0",
68 | "sinon": "^7.3.1"
69 | },
70 | "dependencies": {
71 | "@babel/runtime": "^7.5.5",
72 | "@babel/runtime-corejs2": "^7.5.5",
73 | "apollo-server-errors": "^2.4.1",
74 | "debug": "^4.1.1",
75 | "graphql-auth-directives": "^2.2.2",
76 | "lodash": "^4.17.19",
77 | "neo4j-driver": "^4.2.1",
78 | "graphql": "^15.4.0",
79 | "graphql-tools": "^7.0.2"
80 | },
81 | "ava": {
82 | "require": [
83 | "@babel/register"
84 | ],
85 | "files": [
86 | "!test/helpers",
87 | "!test/integration/gateway.test.js"
88 | ]
89 | },
90 | "prettier": {
91 | "singleQuote": true
92 | },
93 | "lint-staged": {
94 | "*.{js,json,css,md}": [
95 | "prettier --write",
96 | "git add"
97 | ]
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/scripts/install-neo4j.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -xe
4 |
5 | if [ ! -d "neo4j/data/databases/graph.db" ]; then
6 | mkdir -p neo4j
7 | wget dist.neo4j.org/neo4j-$NEO4J_DIST-$NEO4J_VERSION-unix.tar.gz
8 | tar -xzf neo4j-$NEO4J_DIST-$NEO4J_VERSION-unix.tar.gz -C neo4j --strip-components 1
9 | neo4j/bin/neo4j-admin set-initial-password letmein
10 | curl -L https://github.com/neo4j-contrib/neo4j-apoc-procedures/releases/download/$APOC_VERSION/apoc-$APOC_VERSION-all.jar > ./neo4j/plugins/apoc-$APOC_VERSION-all.jar
11 | wget https://datastores.s3.amazonaws.com/recommendations/v$DATASTORE_VERSION/recommendations.db.zip
12 | sudo apt-get install unzip
13 | unzip recommendations.db.zip
14 | mv recommendations.db neo4j/data/databases/graph.db
15 | else
16 | echo "Database is already installed, skipping"
17 | fi
18 |
--------------------------------------------------------------------------------
/scripts/start-neo4j.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | BOLT_PORT=7687
4 |
5 | if [ ! -d "neo4j/data/databases/graph.db" ]; then
6 | echo "Neo4j not installed correctly, run ./scripts/install_neo4j"
7 | exit 1
8 | else
9 | echo "dbms.allow_upgrade=true" >> ./neo4j/conf/neo4j.conf
10 | echo "dbms.recovery.fail_on_missing_files=false" >> ./neo4j/conf/neo4j.conf
11 | # Set initial and max heap to workaround JVM in docker issues
12 | dbms_memory_heap_initial_size="2048m" dbms_memory_heap_max_size="2048m" ./neo4j/bin/neo4j start
13 | echo "Waiting up to 2 minutes for neo4j bolt port ($BOLT_PORT)"
14 |
15 | for i in {1..120};
16 | do
17 | nc -z 127.0.0.1 $BOLT_PORT
18 | is_up=$?
19 | if [ $is_up -eq 0 ]; then
20 | echo
21 | echo "Successfully started, neo4j bolt available on $BOLT_PORT"
22 | break
23 | fi
24 | sleep 1
25 | echo -n "."
26 | done
27 | echo
28 | # Wait a further 5 seconds after the port is available
29 | sleep 5
30 | fi
--------------------------------------------------------------------------------
/scripts/stop-and-clear-neo4j.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | BOLT_PORT=7687
4 |
5 | ./neo4j/bin/neo4j stop
6 | rm -r neo4j/data/databases/graph.db
7 | ./neo4j/bin/neo4j start
8 |
9 | echo "Waiting up to 2 minutes for neo4j bolt port ($BOLT_PORT)"
10 |
11 | for i in {1..120};
12 | do
13 | nc -z 127.0.0.1 $BOLT_PORT
14 | is_up=$?
15 | if [ $is_up -eq 0 ]; then
16 | echo
17 | echo "Successfully started, neo4j bolt available on $BOLT_PORT"
18 | break
19 | fi
20 | sleep 1
21 | echo -n "."
22 | done
23 | echo
24 | # Wait a further 5 seconds after the port is available
25 | sleep 5
--------------------------------------------------------------------------------
/scripts/wait-for-graphql.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | HTTP_PORT=3000
4 |
5 | echo "Waiting up to 2 minutes for graphql http port ($HTTP_PORT)"
6 |
7 | for i in {1..120};
8 | do
9 | nc -z localhost $HTTP_PORT
10 | is_up=$?
11 | if [ $is_up -eq 0 ]; then
12 | echo
13 | echo "Successfully started, graphql http available on $HTTP_PORT"
14 | break
15 | fi
16 | sleep 1
17 | echo -n "."
18 | done
19 | echo
--------------------------------------------------------------------------------
/src/augment/ast.js:
--------------------------------------------------------------------------------
1 | import { Kind } from 'graphql';
2 | import { TypeWrappers } from './fields';
3 |
4 | /**
5 | * Builds the AST definition for a Name
6 | */
7 | export const buildName = ({ name = {} }) => ({
8 | kind: Kind.NAME,
9 | value: name
10 | });
11 |
12 | /**
13 | * Builds the AST definition for a Document
14 | */
15 | export const buildDocument = ({ definitions = [] }) => ({
16 | kind: Kind.DOCUMENT,
17 | definitions
18 | });
19 |
20 | /**
21 | * Builds the AST definition for a Directive Argument
22 | */
23 | export const buildDirectiveArgument = ({ name = {}, value }) =>
24 | buildArgument({
25 | name,
26 | value
27 | });
28 |
29 | /**
30 | * Builds the AST definition for a Directive instance
31 | */
32 | export const buildDirective = ({ name = {}, args = [] }) => ({
33 | kind: Kind.DIRECTIVE,
34 | name,
35 | arguments: args
36 | });
37 |
38 | /**
39 | * Builds the AST definition for a type
40 | */
41 | export const buildNamedType = ({ name = {}, wrappers = {} }) => {
42 | let type = {
43 | kind: Kind.NAMED_TYPE,
44 | name: buildName({ name })
45 | };
46 | if (wrappers[TypeWrappers.NON_NULL_NAMED_TYPE]) {
47 | type = {
48 | kind: Kind.NON_NULL_TYPE,
49 | type: type
50 | };
51 | }
52 | if (wrappers[TypeWrappers.LIST_TYPE]) {
53 | type = {
54 | kind: Kind.LIST_TYPE,
55 | type: type
56 | };
57 | }
58 | if (wrappers[TypeWrappers.NON_NULL_LIST_TYPE]) {
59 | type = {
60 | kind: Kind.NON_NULL_TYPE,
61 | type: type
62 | };
63 | }
64 | return type;
65 | };
66 |
67 | /**
68 | * Builds the AST definition for a schema type
69 | */
70 | export const buildSchemaDefinition = ({ operationTypes = [] }) => ({
71 | kind: Kind.SCHEMA_DEFINITION,
72 | operationTypes
73 | });
74 |
75 | /**
76 | * Builds the AST definition for an operation type on
77 | * the schema type
78 | */
79 | export const buildOperationType = ({ operation = '', type = {} }) => ({
80 | kind: Kind.OPERATION_TYPE_DEFINITION,
81 | operation,
82 | type
83 | });
84 |
85 | /**
86 | * Builds the AST definition for an Object type
87 | */
88 | export const buildObjectType = ({
89 | name = {},
90 | fields = [],
91 | directives = [],
92 | description
93 | }) => ({
94 | kind: Kind.OBJECT_TYPE_DEFINITION,
95 | name,
96 | fields,
97 | directives,
98 | description
99 | });
100 |
101 | /**
102 | * Builds the AST definition for a Field
103 | */
104 | export const buildField = ({
105 | name = {},
106 | type = {},
107 | args = [],
108 | directives = [],
109 | description
110 | }) => ({
111 | kind: Kind.FIELD_DEFINITION,
112 | name,
113 | type,
114 | arguments: args,
115 | directives,
116 | description
117 | });
118 |
119 | /**
120 | * Builds the AST definition for an Input Value,
121 | * used for both field arguments and input object types
122 | */
123 | export const buildInputValue = ({
124 | name = {},
125 | type = {},
126 | directives = [],
127 | defaultValue,
128 | description
129 | }) => {
130 | return {
131 | kind: Kind.INPUT_VALUE_DEFINITION,
132 | name,
133 | type,
134 | directives,
135 | defaultValue,
136 | description
137 | };
138 | };
139 |
140 | /**
141 | * Builds the AST definition for an Enum type
142 | */
143 | export const buildEnumType = ({ name = {}, values = [], description }) => ({
144 | kind: Kind.ENUM_TYPE_DEFINITION,
145 | name,
146 | values,
147 | description
148 | });
149 |
150 | /**
151 | * Builds the AST definition for an Enum type value
152 | */
153 | export const buildEnumValue = ({ name = {}, description }) => ({
154 | kind: Kind.ENUM_VALUE_DEFINITION,
155 | name,
156 | description
157 | });
158 |
159 | /**
160 | * Builds the AST definition for an Input Object type
161 | */
162 | export const buildInputObjectType = ({
163 | name = {},
164 | fields = [],
165 | directives = [],
166 | description
167 | }) => ({
168 | kind: Kind.INPUT_OBJECT_TYPE_DEFINITION,
169 | name,
170 | fields,
171 | directives,
172 | description
173 | });
174 |
175 | /**
176 | * Builds the AST definition for a Directive definition
177 | */
178 | export const buildDirectiveDefinition = ({
179 | name = {},
180 | args = [],
181 | locations = [],
182 | description,
183 | isRepeatable = false
184 | }) => {
185 | return {
186 | kind: Kind.DIRECTIVE_DEFINITION,
187 | name,
188 | arguments: args,
189 | locations,
190 | description,
191 | isRepeatable
192 | };
193 | };
194 |
195 | export const buildDescription = ({ value, block = false, config = {} }) => {
196 | // If boolean and not false, then default is to generate documentation
197 | if (
198 | typeof config.documentation !== 'boolean' ||
199 | config.documentation !== false
200 | ) {
201 | return {
202 | kind: Kind.STRING,
203 | value,
204 | block
205 | };
206 | }
207 | };
208 |
209 | export const buildSelectionSet = ({ selections = [] }) => {
210 | return {
211 | kind: Kind.SELECTION_SET,
212 | selections
213 | };
214 | };
215 |
216 | export const buildFieldSelection = ({
217 | args = [],
218 | directives = [],
219 | name = {},
220 | selectionSet = {
221 | kind: Kind.SELECTION_SET,
222 | selections: []
223 | }
224 | }) => {
225 | return {
226 | kind: Kind.FIELD,
227 | arguments: args,
228 | directives,
229 | name,
230 | selectionSet
231 | };
232 | };
233 |
234 | export const buildArgument = ({ name = {}, value }) => {
235 | return {
236 | kind: Kind.ARGUMENT,
237 | name,
238 | value
239 | };
240 | };
241 |
242 | export const buildVariableDefinition = ({ variable = {}, type = {} }) => {
243 | return {
244 | kind: Kind.VARIABLE_DEFINITION,
245 | variable,
246 | type
247 | };
248 | };
249 |
250 | export const buildVariable = ({ name = {} }) => {
251 | return {
252 | kind: Kind.VARIABLE,
253 | name
254 | };
255 | };
256 |
257 | export const buildOperationDefinition = ({
258 | operation = '',
259 | name = {},
260 | selectionSet = {},
261 | variableDefinitions = []
262 | }) => {
263 | return {
264 | kind: Kind.OPERATION_DEFINITION,
265 | name,
266 | operation,
267 | selectionSet,
268 | variableDefinitions
269 | };
270 | };
271 |
--------------------------------------------------------------------------------
/src/augment/resolvers.js:
--------------------------------------------------------------------------------
1 | import { neo4jgraphql } from '../index';
2 | import { OperationType } from '../augment/types/types';
3 | import {
4 | generateBaseTypeReferenceResolvers,
5 | generateNonLocalTypeExtensionReferenceResolvers
6 | } from '../federation';
7 |
8 | /**
9 | * The main export for the generation of resolvers for the
10 | * Query and Mutation API. Prevent overwriting.
11 | */
12 | export const augmentResolvers = ({
13 | generatedTypeMap,
14 | operationTypeMap,
15 | typeExtensionDefinitionMap,
16 | resolvers,
17 | config = {}
18 | }) => {
19 | const isFederated = config.isFederated;
20 | // Persist and generate Query resolvers
21 | let queryTypeName = OperationType.QUERY;
22 | let mutationTypeName = OperationType.MUTATION;
23 | let subscriptionTypeName = OperationType.SUBSCRIPTION;
24 |
25 | const queryType = operationTypeMap[queryTypeName];
26 | if (queryType) queryTypeName = queryType.name.value;
27 | const queryTypeExtensions = typeExtensionDefinitionMap[queryTypeName];
28 | if (queryType || (queryTypeExtensions && queryTypeExtensions.length)) {
29 | let queryResolvers =
30 | resolvers && resolvers[queryTypeName] ? resolvers[queryTypeName] : {};
31 | queryResolvers = possiblyAddResolvers({
32 | operationType: queryType,
33 | operationTypeExtensions: queryTypeExtensions,
34 | resolvers: queryResolvers,
35 | config,
36 | isFederated
37 | });
38 |
39 | if (Object.keys(queryResolvers).length) {
40 | resolvers[queryTypeName] = queryResolvers;
41 | if (isFederated) {
42 | resolvers = generateBaseTypeReferenceResolvers({
43 | queryResolvers,
44 | resolvers,
45 | config
46 | });
47 | }
48 | }
49 | }
50 |
51 | if (Object.values(typeExtensionDefinitionMap).length) {
52 | if (isFederated) {
53 | resolvers = generateNonLocalTypeExtensionReferenceResolvers({
54 | resolvers,
55 | generatedTypeMap,
56 | typeExtensionDefinitionMap,
57 | queryTypeName,
58 | mutationTypeName,
59 | subscriptionTypeName,
60 | config
61 | });
62 | }
63 | }
64 |
65 | // Persist and generate Mutation resolvers
66 | const mutationType = operationTypeMap[mutationTypeName];
67 | if (mutationType) mutationTypeName = mutationType.name.value;
68 | const mutationTypeExtensions = typeExtensionDefinitionMap[mutationTypeName];
69 | if (
70 | mutationType ||
71 | (mutationTypeExtensions && mutationTypeExtensions.length)
72 | ) {
73 | let mutationResolvers =
74 | resolvers && resolvers[mutationTypeName]
75 | ? resolvers[mutationTypeName]
76 | : {};
77 | mutationResolvers = possiblyAddResolvers({
78 | operationType: mutationType,
79 | operationTypeExtensions: mutationTypeExtensions,
80 | resolvers: mutationResolvers,
81 | config
82 | });
83 | if (Object.keys(mutationResolvers).length > 0) {
84 | resolvers[mutationTypeName] = mutationResolvers;
85 | }
86 | }
87 |
88 | // Persist Subscription resolvers
89 | const subscriptionType = operationTypeMap[subscriptionTypeName];
90 | if (subscriptionType) {
91 | subscriptionTypeName = subscriptionType.name.value;
92 | let subscriptionResolvers =
93 | resolvers && resolvers[subscriptionTypeName]
94 | ? resolvers[subscriptionTypeName]
95 | : {};
96 | if (Object.keys(subscriptionResolvers).length > 0) {
97 | resolvers[subscriptionTypeName] = subscriptionResolvers;
98 | }
99 | }
100 |
101 | // must implement __resolveInfo for every Interface type
102 | // we use "FRAGMENT_TYPE" key to identify the Interface implementation
103 | // type at runtime, so grab this value
104 | const derivedTypes = Object.keys(generatedTypeMap).filter(
105 | e =>
106 | generatedTypeMap[e].kind === 'InterfaceTypeDefinition' ||
107 | generatedTypeMap[e].kind === 'UnionTypeDefinition'
108 | );
109 | derivedTypes.map(e => {
110 | resolvers[e] = {};
111 |
112 | resolvers[e]['__resolveType'] = (obj, context, info) => {
113 | return obj['FRAGMENT_TYPE'];
114 | };
115 | });
116 |
117 | return resolvers;
118 | };
119 |
120 | const getOperationFieldMap = ({ operationType, operationTypeExtensions }) => {
121 | const fieldMap = {};
122 | const fields = operationType ? operationType.fields : [];
123 | fields.forEach(field => {
124 | fieldMap[field.name.value] = true;
125 | });
126 | operationTypeExtensions.forEach(extension => {
127 | extension.fields.forEach(field => {
128 | fieldMap[field.name.value] = true;
129 | });
130 | });
131 | return fieldMap;
132 | };
133 |
134 | /**
135 | * Generates resolvers for a given operation type, if
136 | * any fields exist, for any resolver not provided
137 | */
138 | const possiblyAddResolvers = ({
139 | operationType,
140 | operationTypeExtensions = [],
141 | resolvers,
142 | config
143 | }) => {
144 | const fieldMap = getOperationFieldMap({
145 | operationType,
146 | operationTypeExtensions
147 | });
148 | Object.keys(fieldMap).forEach(name => {
149 | // If not provided
150 | if (resolvers[name] === undefined) {
151 | resolvers[name] = async function(...args) {
152 | return await neo4jgraphql(...args, config.debug);
153 | };
154 | }
155 | });
156 | return resolvers;
157 | };
158 |
159 | /**
160 | * Extracts resolvers from a schema
161 | */
162 | export const extractResolversFromSchema = schema => {
163 | const _typeMap = schema && schema._typeMap ? schema._typeMap : {};
164 | const types = Object.keys(_typeMap);
165 | let type = {};
166 | let schemaTypeResolvers = {};
167 | return types.reduce((acc, t) => {
168 | // prevent extraction from schema introspection system keys
169 | if (
170 | t !== '__Schema' &&
171 | t !== '__Type' &&
172 | t !== '__TypeKind' &&
173 | t !== '__Field' &&
174 | t !== '__InputValue' &&
175 | t !== '__EnumValue' &&
176 | t !== '__Directive'
177 | ) {
178 | type = _typeMap[t];
179 | // resolvers are stored on the field level at a .resolve key
180 | schemaTypeResolvers = extractFieldResolversFromSchemaType(type);
181 | // do not add unless there exists at least one field resolver for type
182 | if (schemaTypeResolvers) {
183 | acc[t] = schemaTypeResolvers;
184 | }
185 | }
186 | return acc;
187 | }, {});
188 | };
189 |
190 | /**
191 | * Extracts field resolvers from a given type taken
192 | * from a schema
193 | */
194 | const extractFieldResolversFromSchemaType = type => {
195 | const fields = type._fields;
196 | const fieldKeys = fields ? Object.keys(fields) : [];
197 | const fieldResolvers =
198 | fieldKeys.length > 0
199 | ? fieldKeys.reduce((acc, t) => {
200 | // do not add entry for this field unless it has resolver
201 | if (fields[t].resolve !== undefined) {
202 | acc[t] = fields[t].resolve;
203 | }
204 | return acc;
205 | }, {})
206 | : undefined;
207 | // do not return value unless there exists at least 1 field resolver
208 | return fieldResolvers && Object.keys(fieldResolvers).length > 0
209 | ? fieldResolvers
210 | : undefined;
211 | };
212 |
--------------------------------------------------------------------------------
/src/augment/types/spatial.js:
--------------------------------------------------------------------------------
1 | import { GraphQLInt, GraphQLString, GraphQLFloat } from 'graphql';
2 | import { buildNeo4jTypes, Neo4jTypeName } from '../types/types';
3 | import {
4 | buildName,
5 | buildNamedType,
6 | buildInputValue,
7 | buildInputObjectType
8 | } from '../ast';
9 | import { TypeWrappers } from '../fields';
10 |
11 | /**
12 | * An enum describing the name of the Neo4j Point type
13 | */
14 | export const SpatialType = {
15 | POINT: 'Point'
16 | };
17 |
18 | /**
19 | * An enum describing the property names of the Neo4j Point type
20 | * See: https://neo4j.com/docs/cypher-manual/current/syntax/spatial/#cypher-spatial-instants
21 | */
22 | const Neo4jPointField = {
23 | X: 'x',
24 | Y: 'y',
25 | Z: 'z',
26 | LONGITUDE: 'longitude',
27 | LATITUDE: 'latitude',
28 | HEIGHT: 'height',
29 | CRS: 'crs',
30 | SRID: 'srid'
31 | };
32 |
33 | /**
34 | * A map of the Neo4j Temporal Time type fields to their respective
35 | * GraphQL types
36 | */
37 | export const Neo4jPoint = {
38 | [Neo4jPointField.X]: GraphQLFloat.name,
39 | [Neo4jPointField.Y]: GraphQLFloat.name,
40 | [Neo4jPointField.Z]: GraphQLFloat.name,
41 | [Neo4jPointField.LONGITUDE]: GraphQLFloat.name,
42 | [Neo4jPointField.LATITUDE]: GraphQLFloat.name,
43 | [Neo4jPointField.HEIGHT]: GraphQLFloat.name,
44 | [Neo4jPointField.CRS]: GraphQLString.name,
45 | [Neo4jPointField.SRID]: GraphQLInt.name
46 | };
47 |
48 | export const Neo4jPointDistanceFilter = {
49 | DISTANCE: 'distance',
50 | DISTANCE_LESS_THAN: 'distance_lt',
51 | DISTANCE_LESS_THAN_OR_EQUAL: 'distance_lte',
52 | DISTANCE_GREATER_THAN: 'distance_gt',
53 | DISTANCE_GREATER_THAN_OR_EQUAL: 'distance_gte'
54 | };
55 |
56 | export const Neo4jPointDistanceArgument = {
57 | POINT: 'point',
58 | DISTANCE: 'distance'
59 | };
60 |
61 | /**
62 | * The main export for building the GraphQL input and output type definitions
63 | * for Neo4j Temporal property types
64 | */
65 | export const augmentSpatialTypes = ({ typeMap, config = {} }) => {
66 | config.spatial = decideSpatialConfig({ config });
67 | typeMap = buildSpatialDistanceFilterInputType({
68 | typeMap,
69 | config
70 | });
71 | return buildNeo4jTypes({
72 | typeMap,
73 | neo4jTypes: SpatialType,
74 | config
75 | });
76 | };
77 |
78 | /**
79 | * A helper function for ensuring a fine-grained spatial
80 | * configmration
81 | */
82 | const decideSpatialConfig = ({ config }) => {
83 | let defaultConfig = {
84 | point: true
85 | };
86 | const providedConfig = config ? config.spatial : defaultConfig;
87 | if (typeof providedConfig === 'boolean') {
88 | if (providedConfig === false) {
89 | defaultConfig.point = false;
90 | }
91 | } else if (typeof providedConfig === 'object') {
92 | defaultConfig = providedConfig;
93 | }
94 | return defaultConfig;
95 | };
96 |
97 | /**
98 | * Builds the AST for the input object definition used for
99 | * providing arguments to the spatial filters that use the
100 | * distance Cypher function
101 | */
102 | const buildSpatialDistanceFilterInputType = ({ typeMap = {}, config }) => {
103 | if (config.spatial.point) {
104 | const typeName = `${Neo4jTypeName}${SpatialType.POINT}DistanceFilter`;
105 | // Overwrite
106 | typeMap[typeName] = buildInputObjectType({
107 | name: buildName({ name: typeName }),
108 | fields: [
109 | buildInputValue({
110 | name: buildName({ name: Neo4jPointDistanceArgument.POINT }),
111 | type: buildNamedType({
112 | name: `${Neo4jTypeName}${SpatialType.POINT}Input`,
113 | wrappers: {
114 | [TypeWrappers.NON_NULL_NAMED_TYPE]: true
115 | }
116 | })
117 | }),
118 | buildInputValue({
119 | name: buildName({ name: Neo4jPointDistanceArgument.DISTANCE }),
120 | type: buildNamedType({
121 | name: GraphQLFloat.name,
122 | wrappers: {
123 | [TypeWrappers.NON_NULL_NAMED_TYPE]: true
124 | }
125 | })
126 | })
127 | ]
128 | });
129 | }
130 | return typeMap;
131 | };
132 |
--------------------------------------------------------------------------------
/src/augment/types/temporal.js:
--------------------------------------------------------------------------------
1 | import { GraphQLInt, GraphQLString } from 'graphql';
2 | import { buildNeo4jTypes } from '../types/types';
3 |
4 | /**
5 | * An enum describing the names of Neo4j Temporal types
6 | * See: https://neo4j.com/docs/cypher-manual/current/syntax/temporal/#cypher-temporal-instants
7 | */
8 | export const TemporalType = {
9 | TIME: 'Time',
10 | DATE: 'Date',
11 | DATETIME: 'DateTime',
12 | LOCALTIME: 'LocalTime',
13 | LOCALDATETIME: 'LocalDateTime'
14 | };
15 |
16 | /**
17 | * An enum describing the property names of the Neo4j Time type
18 | */
19 | export const Neo4jTimeField = {
20 | HOUR: 'hour',
21 | MINUTE: 'minute',
22 | SECOND: 'second',
23 | MILLISECOND: 'millisecond',
24 | MICROSECOND: 'microsecond',
25 | NANOSECOND: 'nanosecond',
26 | TIMEZONE: 'timezone'
27 | };
28 |
29 | /**
30 | * An enum describing the property names of the Neo4j Date type
31 | */
32 | export const Neo4jDateField = {
33 | YEAR: 'year',
34 | MONTH: 'month',
35 | DAY: 'day'
36 | };
37 |
38 | /**
39 | * A map of the Neo4j Temporal Time type fields to their respective
40 | * GraphQL types
41 | */
42 | export const Neo4jTime = {
43 | [Neo4jTimeField.HOUR]: GraphQLInt.name,
44 | [Neo4jTimeField.MINUTE]: GraphQLInt.name,
45 | [Neo4jTimeField.SECOND]: GraphQLInt.name,
46 | [Neo4jTimeField.MILLISECOND]: GraphQLInt.name,
47 | [Neo4jTimeField.MICROSECOND]: GraphQLInt.name,
48 | [Neo4jTimeField.NANOSECOND]: GraphQLInt.name,
49 | [Neo4jTimeField.TIMEZONE]: GraphQLString.name
50 | };
51 |
52 | /**
53 | * A map of the Neo4j Temporal Date type fields to their respective
54 | * GraphQL types
55 | */
56 | export const Neo4jDate = {
57 | [Neo4jDateField.YEAR]: GraphQLInt.name,
58 | [Neo4jDateField.MONTH]: GraphQLInt.name,
59 | [Neo4jDateField.DAY]: GraphQLInt.name
60 | };
61 |
62 | /**
63 | * The main export for building the GraphQL input and output type definitions
64 | * for Neo4j Temporal property types. Each TemporalType can be constructed
65 | * using either or both of the Time and Date type fields
66 | */
67 | export const augmentTemporalTypes = ({ typeMap, config = {} }) => {
68 | config.temporal = decideTemporalConfig({ config });
69 | return buildNeo4jTypes({
70 | typeMap,
71 | neo4jTypes: TemporalType,
72 | config
73 | });
74 | };
75 |
76 | /**
77 | * A helper function for ensuring a fine-grained temporal
78 | * configmration, used to simplify checking it
79 | * throughout the augmnetation process
80 | */
81 | const decideTemporalConfig = ({ config }) => {
82 | let defaultConfig = {
83 | time: true,
84 | date: true,
85 | datetime: true,
86 | localtime: true,
87 | localdatetime: true
88 | };
89 | const providedConfig = config ? config.temporal : defaultConfig;
90 | if (typeof providedConfig === 'boolean') {
91 | if (providedConfig === false) {
92 | defaultConfig.time = false;
93 | defaultConfig.date = false;
94 | defaultConfig.datetime = false;
95 | defaultConfig.localtime = false;
96 | defaultConfig.localdatetime = false;
97 | }
98 | } else if (typeof providedConfig === 'object') {
99 | Object.keys(defaultConfig).forEach(e => {
100 | if (providedConfig[e] === undefined) {
101 | providedConfig[e] = defaultConfig[e];
102 | }
103 | });
104 | defaultConfig = providedConfig;
105 | }
106 | return defaultConfig;
107 | };
108 |
--------------------------------------------------------------------------------
/src/auth.js:
--------------------------------------------------------------------------------
1 | // Initial support for checking auth
2 | import {
3 | IsAuthenticatedDirective,
4 | HasRoleDirective,
5 | HasScopeDirective
6 | } from 'graphql-auth-directives';
7 | /*
8 | * Check is context.req.error or context.error
9 | * have been defined.
10 | */
11 | export const checkRequestError = context => {
12 | if (context && context.req && context.req.error) {
13 | return context.req.error;
14 | } else if (context && context.error) {
15 | return context.error;
16 | } else {
17 | return false;
18 | }
19 | };
20 |
21 | const shouldAddAuthDirective = (config, authDirective) => {
22 | if (config && typeof config === 'object') {
23 | return (
24 | config.auth === true ||
25 | (config &&
26 | typeof config.auth === 'object' &&
27 | config.auth[authDirective] === true)
28 | );
29 | }
30 | return false;
31 | };
32 |
33 | export const addAuthDirectiveImplementations = (
34 | schemaDirectives,
35 | typeMap,
36 | config
37 | ) => {
38 | if (shouldAddAuthDirective(config, 'isAuthenticated')) {
39 | if (!schemaDirectives['isAuthenticated']) {
40 | schemaDirectives['isAuthenticated'] = IsAuthenticatedDirective;
41 | }
42 | }
43 | if (shouldAddAuthDirective(config, 'hasRole')) {
44 | getRoleType(typeMap); // ensure Role enum specified in typedefs
45 | if (!schemaDirectives['hasRole']) {
46 | schemaDirectives['hasRole'] = HasRoleDirective;
47 | }
48 | }
49 | if (shouldAddAuthDirective(config, 'hasScope')) {
50 | if (!schemaDirectives['hasScope']) {
51 | schemaDirectives['hasScope'] = HasScopeDirective;
52 | }
53 | }
54 | return schemaDirectives;
55 | };
56 |
57 | const getRoleType = typeMap => {
58 | const roleType = typeMap['Role'];
59 | if (!roleType) {
60 | throw new Error(
61 | `A Role enum type is required for the @hasRole auth directive.`
62 | );
63 | }
64 | return roleType;
65 | };
66 |
--------------------------------------------------------------------------------
/src/inferSchema.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 |
3 | // OKAPI formats it as ':`Foo`' and we want 'Foo'
4 | const extractRelationshipType = relTypeName =>
5 | relTypeName.substring(2, relTypeName.length - 1);
6 |
7 | const generateGraphQLTypeForTreeEntry = (tree, key) => {
8 | const entry = tree.getNode(key);
9 | const propNames = Object.keys(entry);
10 | const graphqlTypeName = key.replace(/:/g, '_');
11 |
12 | const typeDeclaration = `type ${graphqlTypeName} {\n`;
13 |
14 | const propertyDeclarations = propNames.map(
15 | propName => ` ${propName}: ${entry[propName].graphQLType}\n`
16 | );
17 |
18 | const labels = key.split(/:/);
19 |
20 | // For these labels, figure out which rels are outbound from any member label.
21 | // That is, if your node is :Foo:Bar, any rel outbound from just Foo counts.
22 | const relDeclarations = _.flatten(
23 | labels.map(label => {
24 | const inbound = lookupInboundRels(tree, label);
25 | const outbound = lookupOutboundRels(tree, label);
26 | const relIds = _.uniq(inbound.concat(outbound));
27 |
28 | return relIds.map(relId => {
29 | // Create a copy of the links to/from this label.
30 | const links = _.cloneDeep(
31 | tree.rels[relId].links.filter(
32 | link => link.from.indexOf(label) > -1 || link.to.indexOf(label) > -1
33 | )
34 | ).map(link => {
35 | if (link.from.indexOf(label) > -1) {
36 | _.set(link, 'direction', 'OUT');
37 | } else {
38 | _.set(link, 'direction', 'IN');
39 | }
40 | });
41 |
42 | // OUT relationships first. Get their 'to' labels and generate.
43 | const allTargetLabels = _.uniq(
44 | _.flatten(
45 | links.filter(l => l.direction === 'OUT').map(link => link.to)
46 | )
47 | );
48 | if (allTargetLabels.length > 1) {
49 | // If a relationship (:A)-[:relType]->(x) where
50 | // x has multiple different labels, we can't express this as a type in
51 | // GraphQL.
52 | console.warn(
53 | `RelID ${relId} for label ${label} has more than one outbound type (${allTargetLabels}); skipping`
54 | );
55 | return null;
56 | }
57 |
58 | const tag = `@relation(name: "${extractRelationshipType(
59 | relId
60 | )}", direction: OUT)`;
61 | const targetTypeName = allTargetLabels[0];
62 |
63 | return ` ${targetTypeName.toLowerCase()}s: [${targetTypeName}] ${tag}\n`;
64 | });
65 | })
66 | );
67 |
68 | return (
69 | typeDeclaration +
70 | propertyDeclarations.join('') +
71 | relDeclarations.join('') +
72 | '}\n'
73 | );
74 | };
75 |
76 | /**
77 | * Determine which relationships are outbound from a label under a schema tree.
78 | * @param {*} tree a schema tree
79 | * @param {*} label a graph label
80 | * @returns {Array} of relationship IDs
81 | */
82 | const lookupOutboundRels = (tree, label) =>
83 | Object.keys(tree.rels).filter(
84 | relId =>
85 | tree.rels[relId].links &&
86 | tree.rels[relId].links.filter(link => link.from.indexOf(label) !== -1)
87 | .length > 0
88 | );
89 |
90 | const lookupInboundRels = (tree, label) =>
91 | Object.keys(tree.rels).filter(
92 | relId =>
93 | tree.rels[relId].links &&
94 | tree.rels[relId].links.filter(link => link.to.indexOf(label) !== -1)
95 | .length > 0
96 | );
97 |
98 | const schemaTreeToGraphQLSchema = tree => {
99 | console.log('TREE ', JSON.stringify(tree.toJSON(), null, 2));
100 | const nodeTypes = Object.keys(tree.nodes).map(key =>
101 | generateGraphQLTypeForTreeEntry(tree, key)
102 | );
103 |
104 | const schema = nodeTypes.join('\n');
105 | return schema;
106 | };
107 |
--------------------------------------------------------------------------------
/src/neo4j-schema/Neo4jSchemaTree.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 | import schema from './entities';
3 | import neo4jTypes from './types';
4 |
5 | const extractRelationshipType = relTypeName =>
6 | relTypeName.substring(2, relTypeName.length - 1);
7 |
8 | const withSession = (driver, database, f) => {
9 | let s;
10 | if (database) {
11 | s = driver.session({ database });
12 | } else {
13 | s = driver.session();
14 | }
15 |
16 | return f(s).finally(() => s.close());
17 | };
18 |
19 | /**
20 | * This object harvests Neo4j schema information out of a running instance and organizes
21 | * it into a tree structure.
22 | *
23 | * Currently, it does this by using built-in Neo4j procedures (db.schema.nodeTypeProperties())
24 | * This approach has the drawback that it scans the entire database to make sure that the
25 | * resulting schema is complete and accurate, which can increase startup times and churn the
26 | * page cache, but guarantees 100% accurate results.
27 | *
28 | * TODO - in a future version, we will make the schema harvesting swappable for an APOC
29 | * approach that is based on sampling.
30 | */
31 | export default class Neo4jSchemaTree {
32 | // TODO: config is where method of generating metadata can be passed
33 | constructor(driver, config = {}) {
34 | this.driver = driver;
35 | this.nodes = {};
36 | this.rels = {};
37 | this.config = config;
38 | }
39 |
40 | toJSON() {
41 | return {
42 | nodes: this.nodes,
43 | rels: this.rels
44 | };
45 | }
46 |
47 | initialize() {
48 | const nodeTypeProperties = session =>
49 | session
50 | .run(
51 | `CALL db.schema.nodeTypeProperties()
52 | YIELD nodeType, nodeLabels, propertyName, propertyTypes, mandatory
53 | WITH *
54 | WHERE propertyName =~ "[_A-Za-z][_0-9A-Za-z]*"
55 | AND all(x IN nodeLabels WHERE (x =~ "[A-Za-z][_0-9A-Za-z]*"))
56 | RETURN *`
57 | )
58 | .then(results => results.records.map(rec => rec.toObject()));
59 |
60 | const relTypeProperties = session =>
61 | session
62 | .run(
63 | `CALL db.schema.relTypeProperties()
64 | YIELD relType, propertyName, propertyTypes, mandatory
65 | WITH * WHERE propertyName =~ "[_A-Za-z][_0-9A-Za-z]*" OR propertyName IS NULL
66 | RETURN *`
67 | )
68 | .then(results => results.records.map(rec => rec.toObject()));
69 |
70 | console.log('Initializing your Neo4j Schema');
71 | console.log('This may take a few moments depending on the size of your DB');
72 | return Promise.all([
73 | withSession(this.driver, this.config.database, nodeTypeProperties),
74 | withSession(this.driver, this.config.database, relTypeProperties)
75 | ])
76 | .then(([nodeTypes, relTypes]) => this._populate(nodeTypes, relTypes))
77 | .then(() => this._populateRelationshipLinkTypes())
78 | .then(() => this);
79 | }
80 |
81 | _populateRelationshipLinkTypes() {
82 | // console.log('Getting from/to relationship metadata');
83 |
84 | const okapiIds = Object.keys(this.rels);
85 |
86 | const promises = okapiIds.map(okapiId => {
87 | const q = `
88 | MATCH (n)-[r${okapiId}]->(m)
89 | WITH n, r, m LIMIT 100
90 | WITH DISTINCT labels(n) AS from, labels(m) AS to
91 | WITH [x IN from WHERE x =~ "[A-Za-z][_0-9A-Za-z]*"] AS from, [x IN to WHERE x =~ "[A-Za-z][_0-9A-Za-z]*"] AS to
92 | WITH from, to WHERE SIZE(from) > 0 AND SIZE(to) > 0
93 | RETURN from, to
94 | `;
95 |
96 | return withSession(this.driver, this.config.database, s =>
97 | s.run(q).then(results => results.records.map(r => r.toObject()))
98 | ).then(rows => {
99 | this.getRel(okapiId).relType = extractRelationshipType(okapiId);
100 | this.getRel(okapiId).links = rows;
101 | });
102 | });
103 |
104 | return Promise.all(promises).then(() => this);
105 | }
106 |
107 | getNode(id) {
108 | return this.nodes[id];
109 | }
110 |
111 | getNodes() {
112 | return Object.values(this.nodes);
113 | }
114 |
115 | /**
116 | * @param {Array[String]} labels a set of labels
117 | * @returns {Neo4jNode} if it exists, null otherwise.
118 | */
119 | getNodeByLabels(labels) {
120 | const lookingFor = _.uniq(labels);
121 | const total = lookingFor.length;
122 |
123 | return this.getNodes().filter(n => {
124 | const here = n.getLabels();
125 |
126 | const matches = here.filter(label => lookingFor.indexOf(label) > -1)
127 | .length;
128 | return matches === total;
129 | })[0];
130 | }
131 |
132 | getRel(id) {
133 | return this.rels[id];
134 | }
135 |
136 | getRels() {
137 | return Object.values(this.rels);
138 | }
139 |
140 | _populate(nodeTypes, relTypes) {
141 | // Process node types first
142 | _.uniq(nodeTypes.map(n => n.nodeType)).forEach(nodeType => {
143 | // A node type is an OKAPI node type label, looks like ":`Event`"
144 | // Not terribly meaningful, but a grouping ID
145 | const labelCombos = _.uniq(
146 | nodeTypes.filter(i => i.nodeType === nodeType)
147 | );
148 |
149 | labelCombos.forEach(item => {
150 | const combo = item.nodeLabels;
151 | // A label combination is an array of strings ["X", "Y"] which indicates
152 | // that some nodes ":X:Y" exist in the graph.
153 | const id = combo.join(':');
154 | const entity = this.nodes[id] || new schema.Neo4jNode(id);
155 | this.nodes[id] = entity;
156 |
157 | // Pick out only the property data for this label combination.
158 | nodeTypes
159 | .filter(i => i.nodeLabels === combo)
160 | .map(i => _.pick(i, ['propertyName', 'propertyTypes', 'mandatory']))
161 | .forEach(propDetail => {
162 | // console.log(schema);
163 | if (_.isNil(propDetail.propertyName)) {
164 | return;
165 | }
166 |
167 | propDetail.graphQLType = neo4jTypes.chooseGraphQLType(propDetail);
168 | entity.addProperty(propDetail.propertyName, propDetail);
169 | });
170 | });
171 | });
172 |
173 | // Rel types
174 | _.uniq(relTypes.map(r => r.relType)).forEach(relType => {
175 | const id = relType;
176 | const entity = this.rels[id] || new schema.Neo4jRelationship(id);
177 | this.rels[id] = entity;
178 |
179 | relTypes
180 | .filter(r => r.relType === relType)
181 | .map(r => _.pick(r, ['propertyName', 'propertyTypes', 'mandatory']))
182 | .forEach(propDetail => {
183 | if (_.isNil(propDetail.propertyName)) {
184 | return;
185 | }
186 |
187 | propDetail.graphQLType = neo4jTypes.chooseGraphQLType(propDetail);
188 | entity.addProperty(propDetail.propertyName, propDetail);
189 | });
190 | });
191 | }
192 | }
193 |
--------------------------------------------------------------------------------
/src/neo4j-schema/entities.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 |
3 | /**
4 | * Base class for a schema entity derived from Neo4j
5 | */
6 | class Neo4jSchemaEntity {
7 | constructor(id, type, properties = {}) {
8 | this.id = id;
9 | this.type = type;
10 | this.properties = properties;
11 | }
12 |
13 | asJSON() {
14 | return {
15 | id: this.id,
16 | type: this.type,
17 | properties: this.properties
18 | };
19 | }
20 |
21 | getGraphQLTypeName() {
22 | throw new Error('Override me in subclass');
23 | }
24 |
25 | getPropertyNames() {
26 | return Object.keys(this.properties).sort();
27 | }
28 |
29 | hasProperties() {
30 | return this.getPropertyNames().length > 0;
31 | }
32 |
33 | getProperty(name) {
34 | return this.properties[name];
35 | }
36 |
37 | addProperty(name, details) {
38 | if (_.isNil(name) || _.isNil(details)) {
39 | throw new Error('Property must have both name and details');
40 | }
41 |
42 | _.set(this.properties, name, details);
43 | return this;
44 | }
45 | }
46 |
47 | class Neo4jNode extends Neo4jSchemaEntity {
48 | constructor(id) {
49 | super(id, 'node', {});
50 | }
51 |
52 | getGraphQLTypeName() {
53 | // Make sure to guarantee alphabetic consistent ordering.
54 | const parts = this.getLabels();
55 | return parts.join('_').replace(/ /g, '_');
56 | }
57 |
58 | getLabels() {
59 | return this.id.split(/:/g).sort();
60 | }
61 | }
62 |
63 | class Neo4jRelationship extends Neo4jSchemaEntity {
64 | constructor(id) {
65 | super(id, 'relationship', {});
66 | }
67 |
68 | getRelationshipType() {
69 | // OKAPI returns okapi IDs as :`TYPENAME`
70 | return this.id.substring(2, this.id.length - 1);
71 | }
72 |
73 | getGraphQLTypeName() {
74 | return this.getRelationshipType().replace(/ /g, '_');
75 | }
76 |
77 | /**
78 | * A univalent relationship is one that connects exactly one type of node label to exactly one type of
79 | * other node label. Imagine you have (:Customer)-[:BUYS]->(:Product). In this case, BUYS is univalent
80 | * because it always connects from:Customer to:Product.
81 | *
82 | * If you had a graph which was (:Customer)-[:BUYS]->(:Product), (:Company)-[:BUYS]->(:Product) then
83 | * the BUYS relationship would be multivalent because it connects [Customer, Company] -> [Product].
84 | *
85 | * Important note, since nodes can have multiple labels, you could end up in a situation where
86 | * (:A:B)-[:WHATEVER]->(:C:D). This is still univalent, because WHATEVER always connects things which
87 | * are all of :A:B to those that are all of :C:D. If you had this situation:
88 | * (:A:B)-[:WHATEVER]->(:C:D) and then (:A)-[:WHATEVER]->(:C) this is not univalent.
89 | */
90 | isUnivalent() {
91 | return (
92 | this.links && this.links.length === 1
93 | // Length of links[0].from and to doesn't matter, as label combinations may be in use.
94 | );
95 | }
96 |
97 | isInboundTo(label) {
98 | const comparisonSet = this._setify(label);
99 |
100 | const linksToThisLabel = this.links.filter(link => {
101 | const hereToSet = new Set(link.to);
102 | const intersection = this._setIntersection(comparisonSet, hereToSet);
103 | return intersection.size === comparisonSet.size;
104 | });
105 | return linksToThisLabel.length > 0;
106 | }
107 |
108 | _setify(thing) {
109 | return new Set(_.isArray(thing) ? thing : [thing]);
110 | }
111 |
112 | _setIntersection(a, b) {
113 | return new Set([...a].filter(x => b.has(x)));
114 | }
115 |
116 | /**
117 | * Returns true if the relationship is outbound from a label or set of labels.
118 | * @param {*} label a single label or array of labels.
119 | */
120 | isOutboundFrom(label) {
121 | const comparisonSet = this._setify(label);
122 |
123 | const linksFromThisLabelSet = this.links.filter(link => {
124 | const hereFromSet = new Set(link.from);
125 | const intersection = this._setIntersection(comparisonSet, hereFromSet);
126 | return intersection.size === comparisonSet.size;
127 | });
128 | return linksFromThisLabelSet.length > 0;
129 | }
130 |
131 | getToLabels() {
132 | return _.uniq(_.flatten(this.links.map(l => l.to))).sort();
133 | }
134 |
135 | getFromLabels() {
136 | return _.uniq(_.flatten(this.links.map(l => l.from))).sort();
137 | }
138 | }
139 |
140 | export default {
141 | Neo4jSchemaEntity,
142 | Neo4jNode,
143 | Neo4jRelationship
144 | };
145 |
--------------------------------------------------------------------------------
/src/neo4j-schema/types.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 |
3 | /**
4 | * Choose a single property type given an array of possible values. In the simplest
5 | * and usual case, a property only has one possible type, and so it will get assigned
6 | * that. But we have to handle situations where a property can be a long or an int,
7 | * depending on value, etc.
8 | * @param {Object} property a property object from OKAPI schema information
9 | * @returns String a single type name.
10 | */
11 | const chooseGraphQLType = property => {
12 | const options = _.get(property, 'propertyTypes');
13 | const mandatoryModifier = _.get(property, 'mandatory') ? '!' : '';
14 |
15 | // This stores and performs the mapping of a primitive Neo4j type t
16 | // to a GraphQL primitive type (the output)
17 | const mapSingleType = t => {
18 | const mapping = {
19 | // Primitives
20 | Long: 'Int',
21 | Float: 'Float',
22 | Double: 'Float',
23 | Integer: 'Int',
24 | String: 'String',
25 | Boolean: 'Boolean',
26 | Date: 'Date',
27 | DateTime: 'DateTime',
28 | LocalTime: 'LocalTime',
29 | LocalDateTime: 'LocalDateTime',
30 | Time: 'Time',
31 | Point: 'Point',
32 |
33 | // Array types
34 | LongArray: '[Int]',
35 | DoubleArray: '[Float]',
36 | FloatArray: '[Float]',
37 | IntegerArray: '[Int]',
38 | BooleanArray: '[Boolean]',
39 | StringArray: '[String]',
40 | DateArray: '[Date]',
41 | DateTimeArray: '[DateTime]',
42 | TimeArray: '[Time]',
43 | LocalTimeArray: '[LocalTime]',
44 | LocalDateTimeArray: '[LocalDateTime]',
45 | PointArray: '[Point]'
46 | };
47 |
48 | return mapping[t] || 'String';
49 | };
50 |
51 | if (!options || options.length === 0) {
52 | return 'String' + mandatoryModifier;
53 | }
54 | if (options.length === 1) {
55 | return mapSingleType(options[0]) + mandatoryModifier;
56 | }
57 |
58 | const has = (set, item) => set.indexOf(item) !== -1;
59 |
60 | return (
61 | mapSingleType(
62 | options
63 | .filter(a => a)
64 | .reduce((a, b) => {
65 | // Comparator function: always pick the broader of the two types.
66 | if (!a || !b) {
67 | return a || b;
68 | }
69 | if (a === b) {
70 | return a;
71 | }
72 |
73 | const set = [a, b];
74 |
75 | // String's generality dominates everything else.
76 | if (has(set, 'String')) {
77 | return 'String';
78 | }
79 |
80 | // Types form a partial ordering/lattice. Some combinations are
81 | // nonsense and aren't specified, for example Long vs. Boolean.
82 | // In the nonsense cases, you get String at the bottom.
83 | // Basically, inconsistently typed neo4j properties are a **problem**,
84 | // and you shouldn't have them.
85 | // Only a few pairwise combinations make sense...
86 | if (has(set, 'Long') && has(set, 'Integer')) {
87 | return 'Long';
88 | }
89 | if (has(set, 'Integer') && has(set, 'Float')) {
90 | return 'Float';
91 | }
92 |
93 | return 'String';
94 | }, null)
95 | ) + mandatoryModifier
96 | );
97 | };
98 |
99 | const label2GraphQLType = label => {
100 | if (_.isNil(label)) {
101 | throw new Error('Cannot convert nil label to GraphQL type');
102 | }
103 |
104 | return label.replace(/[ :]/g, '_');
105 | };
106 |
107 | export default {
108 | chooseGraphQLType,
109 | label2GraphQLType
110 | };
111 |
--------------------------------------------------------------------------------
/src/schemaAssert.js:
--------------------------------------------------------------------------------
1 | import { getFieldDirective } from './utils';
2 | import { DirectiveDefinition } from './augment/directives';
3 | import { isNodeType, isUnionTypeDefinition } from './augment/types/types';
4 | import { getKeyFields } from './augment/types/node/selection';
5 |
6 | export const schemaAssert = ({
7 | schema,
8 | indexLabels,
9 | constraintLabels,
10 | dropExisting = true
11 | }) => {
12 | if (!indexLabels) indexLabels = `{}`;
13 | if (!constraintLabels) constraintLabels = `{}`;
14 | if (schema) {
15 | const indexFieldTypeMap = buildKeyTypeMap({
16 | schema,
17 | directives: [DirectiveDefinition.INDEX]
18 | });
19 | indexLabels = cypherMap({
20 | typeMap: indexFieldTypeMap
21 | });
22 | const uniqueFieldTypeMap = buildKeyTypeMap({
23 | schema,
24 | directives: [DirectiveDefinition.ID, DirectiveDefinition.UNIQUE]
25 | });
26 | constraintLabels = cypherMap({
27 | typeMap: uniqueFieldTypeMap
28 | });
29 | }
30 | return `CALL apoc.schema.assert(${indexLabels}, ${constraintLabels}${
31 | dropExisting === false ? `, ${dropExisting}` : ''
32 | })`;
33 | };
34 |
35 | const buildKeyTypeMap = ({ schema, directives = [] }) => {
36 | const typeMap = schema ? schema.getTypeMap() : {};
37 | return Object.entries(typeMap).reduce(
38 | (mapped, [typeName, { astNode: definition }]) => {
39 | if (
40 | isNodeType({ definition }) &&
41 | !isUnionTypeDefinition({ definition })
42 | ) {
43 | const type = schema.getType(typeName);
44 | const fieldMap = type.getFields();
45 | const fields = Object.values(fieldMap).map(field => field.astNode);
46 | const keyFields = getKeyFields({ fields });
47 | if (keyFields.length && directives.length) {
48 | const directiveFields = keyFields.filter(field => {
49 | // there exists at least one directive on this field
50 | // matching a directive we want to map
51 | return directives.some(directive =>
52 | getFieldDirective(field, directive)
53 | );
54 | });
55 | if (directiveFields.length) {
56 | mapped[typeName] = {
57 | ...definition,
58 | fields: directiveFields
59 | };
60 | }
61 | }
62 | }
63 | return mapped;
64 | },
65 | {}
66 | );
67 | };
68 |
69 | const cypherMap = ({ typeMap = {} }) => {
70 | // The format of a Cypher map is close to JSON but does not quote keys
71 | const cypherMapFormat = Object.entries(typeMap).map(([typeName, astNode]) => {
72 | const fields = astNode.fields || [];
73 | const fieldNames = fields.map(field => field.name.value);
74 | const assertions = JSON.stringify(fieldNames);
75 | return `${typeName}:${assertions}`;
76 | });
77 | return `{${cypherMapFormat}}`;
78 | };
79 |
--------------------------------------------------------------------------------
/src/schemaSearch.js:
--------------------------------------------------------------------------------
1 | import { getFieldDirective } from './utils';
2 | import {
3 | DirectiveDefinition,
4 | getDirective,
5 | getDirectiveArgument
6 | } from './augment/directives';
7 | import { isNodeType, isUnionTypeDefinition } from './augment/types/types';
8 | import { TypeWrappers, unwrapNamedType } from './augment/fields';
9 | import { GraphQLID, GraphQLString } from 'graphql';
10 | import { ApolloError } from 'apollo-server-errors';
11 |
12 | const CREATE_NODE_INDEX = `CALL db.index.fulltext.createNodeIndex`;
13 |
14 | export const schemaSearch = ({ schema }) => {
15 | let statement = '';
16 | let statements = [];
17 | if (schema) {
18 | const searchFieldTypeMap = mapSearchDirectives({
19 | schema
20 | });
21 | statements = Object.entries(searchFieldTypeMap).map(([name, config]) => {
22 | const { labelMap, properties } = config;
23 | const labels = Object.keys(labelMap);
24 | const labelVariable = JSON.stringify(labels);
25 | const propertyVariable = JSON.stringify(properties);
26 | // create the index anew
27 | return ` ${CREATE_NODE_INDEX}("${name}",${labelVariable},${propertyVariable})`;
28 | });
29 | }
30 | if (statements.length) {
31 | statement = `${statements.join('\n')}
32 | RETURN TRUE`;
33 | }
34 | return statement;
35 | };
36 |
37 | export const mapSearchDirectives = ({ schema }) => {
38 | const typeMap = schema ? schema.getTypeMap() : {};
39 | return Object.entries(typeMap).reduce(
40 | (mapped, [typeName, { astNode: definition }]) => {
41 | if (
42 | isNodeType({ definition }) &&
43 | !isUnionTypeDefinition({ definition })
44 | ) {
45 | const type = schema.getType(typeName);
46 | const fieldMap = type.getFields();
47 | Object.entries(fieldMap).forEach(([name, field]) => {
48 | const { astNode } = field;
49 | if (astNode) {
50 | const unwrappedType = unwrapNamedType({ type: astNode.type });
51 | const fieldTypeName = unwrappedType.name;
52 | const fieldTypeWrappers = unwrappedType.wrappers;
53 | const directives = astNode.directives;
54 | const directive = getDirective({
55 | directives,
56 | name: DirectiveDefinition.SEARCH
57 | });
58 | if (directive) {
59 | const isStringType = fieldTypeName === GraphQLString.name;
60 | const isIDType = fieldTypeName === GraphQLID.name;
61 | const isListField = fieldTypeWrappers[TypeWrappers.LIST_TYPE];
62 | if (isIDType || isStringType) {
63 | if (!isListField) {
64 | let searchIndexName = getDirectiveArgument({
65 | directive,
66 | name: 'index'
67 | });
68 | if (!searchIndexName) searchIndexName = `${typeName}Search`;
69 | if (!mapped[searchIndexName]) {
70 | mapped[searchIndexName] = {
71 | labelMap: {
72 | [typeName]: true
73 | },
74 | properties: [name]
75 | };
76 | } else {
77 | const indexEntry = mapped[searchIndexName];
78 | const labelMap = indexEntry.labelMap;
79 | const firstLabel = Object.keys(labelMap)[0];
80 | if (labelMap[typeName]) {
81 | mapped[searchIndexName].properties.push(name);
82 | } else {
83 | throw new ApolloError(
84 | `The ${searchIndexName} index on the ${firstLabel} type cannot be used on the ${name} field of the ${typeName} type, because composite search indexes are not yet supported.`
85 | );
86 | }
87 | }
88 | } else {
89 | throw new ApolloError(
90 | `The @search directive on the ${name} field of the ${typeName} type is invalid, because search indexes cannot currently be set for list type fields.`
91 | );
92 | }
93 | } else {
94 | throw new ApolloError(
95 | `The @search directive on the ${name} field of the ${typeName} type is invalid, because search indexes can only be set for String and ID type fields.`
96 | );
97 | }
98 | }
99 | }
100 | });
101 | }
102 | return mapped;
103 | },
104 | {}
105 | );
106 | };
107 |
--------------------------------------------------------------------------------
/test/helpers/configTestHelpers.js:
--------------------------------------------------------------------------------
1 | export const typeDefs = `
2 | type Tweet {
3 | id: ID!
4 | timestamp: DateTime
5 | text: String
6 | hashtags: [Hashtag] @relation(name: "HAS_TAG", direction: "OUT")
7 | user: User @relation(name: "POSTED", direction: "IN")
8 | }
9 |
10 | type User {
11 | id: ID!
12 | screen_name: String
13 | tweets: [Tweet] @relation(name: "POSTED", direction: "OUT")
14 | }
15 |
16 | type Hashtag {
17 | name: String
18 | }
19 |
20 | scalar DateTime
21 | `;
22 |
23 | export const EXPECTED_SCHEMA_NO_QUERIES_NO_MUTATIONS = `directive @cypher(statement: String) on FIELD_DEFINITION
24 |
25 | directive @relation(name: String, direction: _RelationDirections, from: String, to: String) on FIELD_DEFINITION | OBJECT
26 |
27 | directive @MutationMeta(relationship: String, from: String, to: String) on FIELD_DEFINITION
28 |
29 | enum _RelationDirections {
30 | IN
31 | OUT
32 | }
33 |
34 | type Hashtag {
35 | name: String
36 | _id: String
37 | }
38 |
39 | type Tweet {
40 | id: ID!
41 | timestamp: _Neo4jDateTime
42 | text: String
43 | hashtags: [Hashtag]
44 | user: User
45 | _id: String
46 | }
47 |
48 | type User {
49 | id: ID!
50 | screen_name: String
51 | tweets: [Tweet]
52 | _id: String
53 | }
54 | `;
55 |
56 | export const EXPECTED_SCHEMA_ENABLE_QUERIES_NO_MUTATIONS = `directive @cypher(statement: String) on FIELD_DEFINITION
57 |
58 | directive @relation(name: String, direction: _RelationDirections, from: String, to: String) on FIELD_DEFINITION | OBJECT
59 |
60 | directive @MutationMeta(relationship: String, from: String, to: String) on FIELD_DEFINITION
61 |
62 | enum _HashtagOrdering {
63 | name_asc
64 | name_desc
65 | _id_asc
66 | _id_desc
67 | }
68 |
69 | enum _RelationDirections {
70 | IN
71 | OUT
72 | }
73 |
74 | enum _TweetOrdering {
75 | id_asc
76 | id_desc
77 | text_asc
78 | text_desc
79 | _id_asc
80 | _id_desc
81 | }
82 |
83 | enum _UserOrdering {
84 | id_asc
85 | id_desc
86 | screen_name_asc
87 | screen_name_desc
88 | _id_asc
89 | _id_desc
90 | }
91 |
92 | type Hashtag {
93 | name: String
94 | _id: String
95 | }
96 |
97 | type Query {
98 | Tweet(id: ID, text: String, _id: String, first: Int, offset: Int, orderBy: _TweetOrdering): [Tweet]
99 | User(id: ID, screen_name: String, _id: String, first: Int, offset: Int, orderBy: _UserOrdering): [User]
100 | Hashtag(name: String, _id: String, first: Int, offset: Int, orderBy: _HashtagOrdering): [Hashtag]
101 | }
102 |
103 | type Tweet {
104 | id: ID!
105 | timestamp: _Neo4jDateTime
106 | text: String
107 | hashtags(first: Int, offset: Int, orderBy: _HashtagOrdering): [Hashtag]
108 | user: User
109 | _id: String
110 | }
111 |
112 | type User {
113 | id: ID!
114 | screen_name: String
115 | tweets(first: Int, offset: Int, orderBy: _TweetOrdering): [Tweet]
116 | _id: String
117 | }
118 | `;
119 |
120 | export const EXPECTED_SCHEMA_ENABLE_QUERIES_ENABLE_MUTATIONS = `directive @cypher(statement: String) on FIELD_DEFINITION
121 |
122 | directive @relation(name: String, direction: _RelationDirections, from: String, to: String) on FIELD_DEFINITION | OBJECT
123 |
124 | directive @MutationMeta(relationship: String, from: String, to: String) on FIELD_DEFINITION
125 |
126 | type _AddTweetHashtagsPayload {
127 | from: Tweet
128 | to: Hashtag
129 | }
130 |
131 | type _AddTweetUserPayload {
132 | from: User
133 | to: Tweet
134 | }
135 |
136 | type _AddUserTweetsPayload {
137 | from: User
138 | to: Tweet
139 | }
140 |
141 | input _HashtagInput {
142 | name: String!
143 | }
144 |
145 | enum _HashtagOrdering {
146 | name_asc
147 | name_desc
148 | _id_asc
149 | _id_desc
150 | }
151 |
152 | enum _RelationDirections {
153 | IN
154 | OUT
155 | }
156 |
157 | type _RemoveTweetHashtagsPayload {
158 | from: Tweet
159 | to: Hashtag
160 | }
161 |
162 | type _RemoveTweetUserPayload {
163 | from: User
164 | to: Tweet
165 | }
166 |
167 | type _RemoveUserTweetsPayload {
168 | from: User
169 | to: Tweet
170 | }
171 |
172 | input _TweetInput {
173 | id: ID!
174 | }
175 |
176 | enum _TweetOrdering {
177 | id_asc
178 | id_desc
179 | text_asc
180 | text_desc
181 | _id_asc
182 | _id_desc
183 | }
184 |
185 | input _UserInput {
186 | id: ID!
187 | }
188 |
189 | enum _UserOrdering {
190 | id_asc
191 | id_desc
192 | screen_name_asc
193 | screen_name_desc
194 | _id_asc
195 | _id_desc
196 | }
197 |
198 | type Hashtag {
199 | name: String
200 | _id: String
201 | }
202 |
203 | type Mutation {
204 | CreateTweet(id: ID, text: String): Tweet
205 | UpdateTweet(id: ID!, text: String): Tweet
206 | DeleteTweet(id: ID!): Tweet
207 | AddTweetHashtags(from: _TweetInput!, to: _HashtagInput!): _AddTweetHashtagsPayload
208 | RemoveTweetHashtags(from: _TweetInput!, to: _HashtagInput!): _RemoveTweetHashtagsPayload
209 | AddTweetUser(from: _UserInput!, to: _TweetInput!): _AddTweetUserPayload
210 | RemoveTweetUser(from: _UserInput!, to: _TweetInput!): _RemoveTweetUserPayload
211 | CreateUser(id: ID, screen_name: String): User
212 | UpdateUser(id: ID!, screen_name: String): User
213 | DeleteUser(id: ID!): User
214 | AddUserTweets(from: _UserInput!, to: _TweetInput!): _AddUserTweetsPayload
215 | RemoveUserTweets(from: _UserInput!, to: _TweetInput!): _RemoveUserTweetsPayload
216 | CreateHashtag(name: String): Hashtag
217 | DeleteHashtag(name: String!): Hashtag
218 | }
219 |
220 | type Query {
221 | Tweet(id: ID, text: String, _id: String, first: Int, offset: Int, orderBy: _TweetOrdering): [Tweet]
222 | User(id: ID, screen_name: String, _id: String, first: Int, offset: Int, orderBy: _UserOrdering): [User]
223 | Hashtag(name: String, _id: String, first: Int, offset: Int, orderBy: _HashtagOrdering): [Hashtag]
224 | }
225 |
226 | type Tweet {
227 | id: ID!
228 | timestamp: _Neo4jDateTime
229 | text: String
230 | hashtags(first: Int, offset: Int, orderBy: _HashtagOrdering): [Hashtag]
231 | user: User
232 | _id: String
233 | }
234 |
235 | type User {
236 | id: ID!
237 | screen_name: String
238 | tweets(first: Int, offset: Int, orderBy: _TweetOrdering): [Tweet]
239 | _id: String
240 | }
241 | `;
242 |
--------------------------------------------------------------------------------
/test/helpers/custom/customSchemaTest.js:
--------------------------------------------------------------------------------
1 | import { graphql } from 'graphql';
2 | import { makeExecutableSchema } from 'graphql-tools';
3 | import _ from 'lodash';
4 | import {
5 | cypherQuery,
6 | cypherMutation,
7 | makeAugmentedSchema,
8 | augmentTypeDefs
9 | } from '../../../src/index';
10 | import { printSchemaDocument } from '../../../src/augment/augment';
11 | import { testSchema } from './testSchema';
12 |
13 | export function cypherTestRunner(
14 | t,
15 | graphqlQuery,
16 | graphqlParams,
17 | expectedCypherQuery,
18 | expectedCypherParams
19 | ) {
20 | const checkCypherQuery = (object, params, ctx, resolveInfo) => {
21 | const [query, queryParams] = cypherQuery(params, ctx, resolveInfo);
22 | t.is(query, expectedCypherQuery);
23 | const deserializedParams = JSON.parse(JSON.stringify(queryParams));
24 | t.deepEqual(deserializedParams, expectedCypherParams);
25 | };
26 |
27 | const checkCypherMutation = (object, params, ctx, resolveInfo) => {
28 | const [query, queryParams] = cypherMutation(params, ctx, resolveInfo);
29 | t.is(query, expectedCypherQuery);
30 | const deserializedParams = JSON.parse(JSON.stringify(queryParams));
31 | t.deepEqual(deserializedParams, expectedCypherParams);
32 | };
33 |
34 | const resolvers = {
35 | Mutation: {
36 | CreateUser: checkCypherMutation,
37 | DeleteUser: checkCypherMutation,
38 | MergeUser: checkCypherMutation,
39 | Custom: checkCypherMutation
40 | }
41 | };
42 | let augmentedTypeDefs = augmentTypeDefs(testSchema, {
43 | auth: true
44 | });
45 | const schema = makeExecutableSchema({
46 | typeDefs: augmentedTypeDefs,
47 | resolvers,
48 | resolverValidationOptions: {
49 | requireResolversForResolveType: false
50 | }
51 | });
52 |
53 | // query the test schema with the test query, assertion is in the resolver
54 | return graphql(
55 | schema,
56 | graphqlQuery,
57 | null,
58 | {
59 | cypherParams: {
60 | userId: 'user-id'
61 | }
62 | },
63 | graphqlParams
64 | );
65 | }
66 |
67 | export function augmentedSchemaCypherTestRunner(
68 | t,
69 | graphqlQuery,
70 | graphqlParams,
71 | expectedCypherQuery,
72 | expectedCypherParams
73 | ) {
74 | const checkCypherQuery = (object, params, ctx, resolveInfo) => {
75 | const [query, queryParams] = cypherQuery(params, ctx, resolveInfo);
76 | t.is(query, expectedCypherQuery);
77 | const deserializedParams = JSON.parse(JSON.stringify(queryParams));
78 | t.deepEqual(deserializedParams, expectedCypherParams);
79 | };
80 | const checkCypherMutation = (object, params, ctx, resolveInfo) => {
81 | const [query, queryParams] = cypherMutation(params, ctx, resolveInfo);
82 | t.is(query, expectedCypherQuery);
83 | const deserializedParams = JSON.parse(JSON.stringify(queryParams));
84 | t.deepEqual(deserializedParams, expectedCypherParams);
85 | };
86 |
87 | const resolvers = {
88 | Mutation: {
89 | CreateUser: checkCypherMutation,
90 | DeleteUser: checkCypherMutation,
91 | MergeUser: checkCypherMutation,
92 | Custom: checkCypherMutation
93 | }
94 | };
95 |
96 | const cypherTestTypeDefs = printSchemaDocument({
97 | schema: makeAugmentedSchema({
98 | typeDefs: testSchema,
99 | resolvers: {},
100 | config: {
101 | auth: true
102 | }
103 | })
104 | });
105 |
106 | const augmentedSchema = makeExecutableSchema({
107 | typeDefs: cypherTestTypeDefs,
108 | resolvers,
109 | resolverValidationOptions: {
110 | requireResolversForResolveType: false
111 | }
112 | });
113 |
114 | return graphql(
115 | augmentedSchema,
116 | graphqlQuery,
117 | null,
118 | {
119 | cypherParams: {
120 | userId: 'user-id'
121 | }
122 | },
123 | graphqlParams
124 | );
125 | }
126 |
--------------------------------------------------------------------------------
/test/helpers/custom/testSchema.js:
--------------------------------------------------------------------------------
1 | import { gql } from 'apollo-server';
2 | import { cypher } from '../../../src/index';
3 |
4 | export const testSchema = gql`
5 | type User {
6 | idField: ID! @id
7 | name: String
8 | names: [String]
9 | birthday: DateTime
10 | liked: [Movie!] @relation(name: "RATING", direction: OUT)
11 | uniqueString: String @unique
12 | createdAt: DateTime
13 | modifiedAt: DateTime
14 | }
15 |
16 | type Movie {
17 | id: ID! @id
18 | title: String! @unique
19 | likedBy: [User!] @relation(name: "RATING", direction: IN)
20 | custom: String
21 | }
22 |
23 | type Query {
24 | User: [User!]
25 | Movie: [Movie!]
26 | }
27 |
28 | type Mutation {
29 | CreateUser(idField: ID, name: String, names: [String], birthday: DateTime, uniqueString: String, liked: UserLiked, sideEffects: OnUserCreate, sideEffectList: [OnUserCreate!]): User
30 | MergeUser(idField: ID!, name: String, names: [String], birthday: DateTime, uniqueString: String, liked: UserLiked, sideEffects: OnUserMerge): User
31 | DeleteUser(idField: ID!, liked: UserLiked): User
32 | Custom(id: ID!, sideEffects: CustomSideEffects, computed: CustomComputed): Custom @cypher(${cypher`
33 | MERGE (custom: Custom {
34 | id: $id
35 | })
36 | RETURN custom
37 | `})
38 | }
39 |
40 | type Custom {
41 | id: ID! @id
42 | computed: Int
43 | nested: [Custom] @relation(name: "RELATED", direction: OUT)
44 | }
45 |
46 | input CustomData {
47 | id: ID!
48 | nested: CustomSideEffects
49 | }
50 |
51 | input CustomComputed {
52 | computed: ComputeComputed
53 | }
54 |
55 | input CustomComputedInput {
56 | value: Int!
57 | }
58 |
59 | input ComputeComputed {
60 | multiply: CustomComputedInput @cypher(${cypher`
61 | WITH custom
62 | SET custom.computed = CustomComputedInput.value * 10
63 | `})
64 | }
65 |
66 | input CustomSideEffects {
67 | create: [CustomData] @cypher(${cypher`
68 | WITH custom
69 | MERGE (subCustom: Custom {
70 | id: CustomData.id
71 | })
72 | MERGE (custom)-[:RELATED]->(subCustom)
73 | WITH subCustom AS custom
74 | `})
75 | }
76 |
77 | input UserWhere {
78 | idField: ID
79 | }
80 |
81 | input UserCreate {
82 | idField: ID
83 | name: String
84 | names: [String]
85 | birthday: DateTime
86 | uniqueString: String
87 | liked: UserLiked
88 | }
89 |
90 | input OnUserCreate {
91 | createdAt: CreatedAt @cypher(${cypher`
92 | WITH user
93 | SET user.createdAt = datetime(CreatedAt.datetime)
94 | `})
95 | }
96 |
97 | input OnUserMerge {
98 | mergedAt: CreatedAt @cypher(${cypher`
99 | WITH user
100 | SET user.modifiedAt = datetime(CreatedAt.datetime)
101 | `})
102 | }
103 |
104 | input CreatedAt {
105 | datetime: DateTime!
106 | }
107 |
108 | input UserMerge {
109 | where: UserWhere
110 | data: UserCreate
111 | }
112 |
113 | input UserLiked {
114 | create: [MovieCreate!] @cypher(${cypher`
115 | WITH user
116 | CREATE (user)-[:RATING]->(movie: Movie {
117 | id: MovieCreate.id,
118 | title: MovieCreate.title
119 | })
120 | WITH movie
121 | `})
122 | nestedCreate: [MovieCreate!] @cypher(${cypher`
123 | WITH user
124 | CREATE (user)-[:RATING]->(movie: Movie {
125 | id: MovieCreate.customLayer.movie.id,
126 | title: MovieCreate.customLayer.movie.title,
127 | custom: MovieCreate.customLayer.custom
128 | })
129 | WITH movie
130 | `})
131 | merge: [MovieMerge!] @cypher(${cypher`
132 | WITH user
133 | MERGE (movie: Movie {
134 | id: MovieMerge.where.id
135 | })
136 | ON CREATE
137 | SET movie.title = MovieMerge.data.title
138 | MERGE (user)-[:RATING]->(movie)
139 | WITH movie
140 | `})
141 | delete: [MovieWhere!] @cypher(${cypher`
142 | WITH user
143 | MATCH (user)-[:RATING]->(movie: Movie { id: MovieWhere.id })
144 | DETACH DELETE movie
145 | `})
146 | }
147 |
148 | input MovieMerge {
149 | where: MovieWhere
150 | data: MovieCreate
151 | }
152 |
153 | input MovieWhere {
154 | id: ID!
155 | }
156 |
157 | input MovieCreate {
158 | id: ID
159 | title: String
160 | likedBy: MovieLikedBy
161 | customLayer: MovieCreateParamLayer
162 | }
163 |
164 | input MovieCreateParamLayer {
165 | custom: String
166 | movie: MovieCreate
167 | }
168 |
169 | input MovieLikedBy {
170 | create: [UserCreate!] @cypher(${cypher`
171 | WITH movie
172 | CREATE (movie)<-[:RATING]-(user:User {
173 | name: UserCreate.name,
174 | uniqueString: UserCreate.uniqueString
175 | })
176 | `})
177 | merge: [UserMerge!] @cypher(${cypher`
178 | WITH movie
179 | MERGE (user: User {
180 | idField: UserMerge.where.idField
181 | })
182 | ON CREATE
183 | SET user.name = UserMerge.data.name,
184 | user.uniqueString = UserMerge.data.uniqueString
185 | MERGE (movie)<-[:RATING]-(user)
186 | `})
187 | }
188 |
189 | enum Role {
190 | reader
191 | user
192 | admin
193 | }
194 | `;
195 |
--------------------------------------------------------------------------------
/test/helpers/driverFakes.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This module serves to fake the minimum of the Neo4j driver API so that tests
3 | * can impersonate live driver connections.
4 | *
5 | * Use "Fake Tables" like this:
6 | *
7 | * Driver({
8 | * "MATCH (n) RETURN count(n)": [ { n: 100 } ],
9 | * /CREATE.*:Foo.*RETURN true/: [ { value: true } ],
10 | * })
11 | *
12 | * This makes a fake driver which responds in those matching conditions, and
13 | * answers all other queries with []
14 | */
15 | import sinon from 'sinon';
16 | import _ from 'lodash';
17 |
18 | let i = 0;
19 |
20 | const record = (data = { value: 1 }) => {
21 | return {
22 | get: field => {
23 | if (field in data) {
24 | return data[field];
25 | }
26 | throw new Error(
27 | `Missing field in FakeRecord caller expected ${field} in ${JSON.stringify(
28 | data
29 | )}`
30 | );
31 | },
32 | has: field => !_.isNil(_.get(data, field)),
33 | toObject: () => data
34 | };
35 | };
36 |
37 | const results = (results = []) => ({
38 | records: results.map(record)
39 | });
40 |
41 | // Fake table is an object with keys that are either queries or regular expressions
42 | // Values are the results to return when you see those.
43 | const fakeRun = fakeTable => (query, params) => {
44 | let foundFakes = [];
45 |
46 | Object.keys(fakeTable).forEach(fakePossibility => {
47 | if (
48 | query === fakePossibility ||
49 | query.match(new RegExp(fakePossibility, 'igm'))
50 | ) {
51 | foundFakes = fakeTable[fakePossibility];
52 | }
53 | });
54 |
55 | return Promise.resolve(results(foundFakes));
56 | };
57 |
58 | const Session = fakeTable => {
59 | return {
60 | id: Math.random(),
61 | run: fakeRun(fakeTable),
62 | close: sinon.fake.returns(true)
63 | };
64 | };
65 |
66 | const Driver = fakeTable => {
67 | return {
68 | id: Math.random(),
69 | session: sinon.fake.returns(Session(fakeTable))
70 | };
71 | };
72 |
73 | export default {
74 | results,
75 | record,
76 | Driver,
77 | Session
78 | };
79 |
--------------------------------------------------------------------------------
/test/helpers/experimental/augmentSchemaTest.js:
--------------------------------------------------------------------------------
1 | import { graphql } from 'graphql';
2 | import { makeExecutableSchema } from 'graphql-tools';
3 | import _ from 'lodash';
4 | import {
5 | cypherQuery,
6 | cypherMutation,
7 | makeAugmentedSchema,
8 | augmentTypeDefs
9 | } from '../../../src/index';
10 | import { printSchemaDocument } from '../../../src/augment/augment';
11 | import { testSchema } from './testSchema';
12 |
13 | // Optimization to prevent schema augmentation from running for every test
14 | const cypherTestTypeDefs = printSchemaDocument({
15 | schema: makeAugmentedSchema({
16 | typeDefs: testSchema,
17 | resolvers: {},
18 | config: {
19 | auth: true,
20 | experimental: true
21 | }
22 | })
23 | });
24 |
25 | export function cypherTestRunner(
26 | t,
27 | graphqlQuery,
28 | graphqlParams,
29 | expectedCypherQuery,
30 | expectedCypherParams
31 | ) {
32 | const testMovieSchema =
33 | testSchema +
34 | `
35 | type Mutation {
36 | CreateUser(data: _UserCreate!): User @hasScope(scopes: ["User: Create", "create:user"])
37 | UpdateUser(where: _UserWhere!, data: _UserUpdate!): User @hasScope(scopes: ["User: Update", "update:user"])
38 | DeleteUser(where: _UserWhere!): User @hasScope(scopes: ["User: Delete", "delete:user"])
39 | MergeUser(where: _UserKeys!, data: _UserCreate!): User @hasScope(scopes: ["User: Merge", "merge:user"])
40 | }
41 |
42 | type Query {
43 | User: [User] @hasScope(scopes: ["User: Read", "read:user"])
44 | }
45 |
46 | input _UserCreate {
47 | idField: ID
48 | name: String
49 | names: [String]
50 | birthday: _Neo4jDateTimeInput
51 | birthdays: [_Neo4jDateTimeInput]
52 | uniqueString: String!
53 | indexedInt: Int
54 | extensionString: String!
55 | }
56 |
57 | input _UserUpdate {
58 | idField: ID
59 | name: String
60 | names: [String]
61 | birthday: _Neo4jDateTimeInput
62 | birthdays: [_Neo4jDateTimeInput]
63 | uniqueString: String
64 | indexedInt: Int
65 | extensionString: String
66 | }
67 |
68 | input _UserWhere {
69 | AND: [_UserWhere!]
70 | OR: [_UserWhere!]
71 | idField: ID
72 | idField_not: ID
73 | idField_in: [ID!]
74 | idField_not_in: [ID!]
75 | idField_contains: ID
76 | idField_not_contains: ID
77 | idField_starts_with: ID
78 | idField_not_starts_with: ID
79 | idField_ends_with: ID
80 | idField_not_ends_with: ID
81 | uniqueString: String
82 | uniqueString_not: String
83 | uniqueString_in: [String!]
84 | uniqueString_not_in: [String!]
85 | uniqueString_contains: String
86 | uniqueString_not_contains: String
87 | uniqueString_starts_with: String
88 | uniqueString_not_starts_with: String
89 | uniqueString_ends_with: String
90 | uniqueString_not_ends_with: String
91 | indexedInt: Int
92 | indexedInt_not: Int
93 | indexedInt_in: [Int!]
94 | indexedInt_not_in: [Int!]
95 | indexedInt_lt: Int
96 | indexedInt_lte: Int
97 | indexedInt_gt: Int
98 | indexedInt_gte: Int
99 | }
100 |
101 | input _UserKeys {
102 | idField: ID
103 | uniqueString: String
104 | indexedInt: Int
105 | }
106 |
107 | `;
108 |
109 | const checkCypherQuery = (object, params, ctx, resolveInfo) => {
110 | const [query, queryParams] = cypherQuery(params, ctx, resolveInfo);
111 | t.is(query, expectedCypherQuery);
112 | const deserializedParams = JSON.parse(JSON.stringify(queryParams));
113 | t.deepEqual(deserializedParams, expectedCypherParams);
114 | };
115 |
116 | const checkCypherMutation = (object, params, ctx, resolveInfo) => {
117 | const [query, queryParams] = cypherMutation(params, ctx, resolveInfo);
118 | t.is(query, expectedCypherQuery);
119 | const deserializedParams = JSON.parse(JSON.stringify(queryParams));
120 | t.deepEqual(deserializedParams, expectedCypherParams);
121 | };
122 |
123 | const resolvers = {
124 | Mutation: {
125 | CreateUser: checkCypherMutation,
126 | UpdateUser: checkCypherMutation,
127 | DeleteUser: checkCypherMutation,
128 | MergeUser: checkCypherMutation
129 | }
130 | };
131 | let augmentedTypeDefs = augmentTypeDefs(testMovieSchema, {
132 | auth: true,
133 | experimental: true
134 | });
135 | const schema = makeExecutableSchema({
136 | typeDefs: augmentedTypeDefs,
137 | resolvers,
138 | resolverValidationOptions: {
139 | requireResolversForResolveType: false
140 | }
141 | });
142 |
143 | // query the test schema with the test query, assertion is in the resolver
144 | return graphql(
145 | schema,
146 | graphqlQuery,
147 | null,
148 | {
149 | cypherParams: {
150 | userId: 'user-id'
151 | }
152 | },
153 | graphqlParams
154 | );
155 | }
156 |
157 | export function augmentedSchemaCypherTestRunner(
158 | t,
159 | graphqlQuery,
160 | graphqlParams,
161 | expectedCypherQuery,
162 | expectedCypherParams
163 | ) {
164 | const checkCypherQuery = (object, params, ctx, resolveInfo) => {
165 | const [query, queryParams] = cypherQuery(params, ctx, resolveInfo);
166 | t.is(query, expectedCypherQuery);
167 | const deserializedParams = JSON.parse(JSON.stringify(queryParams));
168 | t.deepEqual(deserializedParams, expectedCypherParams);
169 | };
170 | const checkCypherMutation = (object, params, ctx, resolveInfo) => {
171 | const [query, queryParams] = cypherMutation(params, ctx, resolveInfo);
172 | t.is(query, expectedCypherQuery);
173 | const deserializedParams = JSON.parse(JSON.stringify(queryParams));
174 | t.deepEqual(deserializedParams, expectedCypherParams);
175 | };
176 |
177 | const resolvers = {
178 | Mutation: {
179 | CreateUser: checkCypherMutation,
180 | UpdateUser: checkCypherMutation,
181 | DeleteUser: checkCypherMutation,
182 | MergeUser: checkCypherMutation,
183 | AddUserRated: checkCypherMutation,
184 | UpdateUserRated: checkCypherMutation,
185 | RemoveUserRated: checkCypherMutation,
186 | MergeUserRated: checkCypherMutation
187 | }
188 | };
189 |
190 | const augmentedSchema = makeExecutableSchema({
191 | typeDefs: cypherTestTypeDefs,
192 | resolvers,
193 | resolverValidationOptions: {
194 | requireResolversForResolveType: false
195 | }
196 | });
197 |
198 | return graphql(
199 | augmentedSchema,
200 | graphqlQuery,
201 | null,
202 | {
203 | cypherParams: {
204 | userId: 'user-id'
205 | }
206 | },
207 | graphqlParams
208 | );
209 | }
210 |
--------------------------------------------------------------------------------
/test/helpers/experimental/custom/customSchemaTest.js:
--------------------------------------------------------------------------------
1 | import { graphql } from 'graphql';
2 | import { makeExecutableSchema } from 'graphql-tools';
3 | import _ from 'lodash';
4 | import {
5 | cypherQuery,
6 | cypherMutation,
7 | makeAugmentedSchema,
8 | augmentTypeDefs
9 | } from '../../../../src/index';
10 | import { printSchemaDocument } from '../../../../src/augment/augment';
11 | import { testSchema } from './testSchema';
12 |
13 | export function cypherTestRunner(
14 | t,
15 | graphqlQuery,
16 | graphqlParams,
17 | expectedCypherQuery,
18 | expectedCypherParams
19 | ) {
20 | const checkCypherQuery = (object, params, ctx, resolveInfo) => {
21 | const [query, queryParams] = cypherQuery(params, ctx, resolveInfo);
22 | t.is(query, expectedCypherQuery);
23 | const deserializedParams = JSON.parse(JSON.stringify(queryParams));
24 | t.deepEqual(deserializedParams, expectedCypherParams);
25 | };
26 |
27 | const checkCypherMutation = (object, params, ctx, resolveInfo) => {
28 | const [query, queryParams] = cypherMutation(params, ctx, resolveInfo);
29 | t.is(query, expectedCypherQuery);
30 | const deserializedParams = JSON.parse(JSON.stringify(queryParams));
31 | t.deepEqual(deserializedParams, expectedCypherParams);
32 | };
33 |
34 | const resolvers = {
35 | Mutation: {
36 | CreateUser: checkCypherMutation,
37 | DeleteUser: checkCypherMutation,
38 | MergeUser: checkCypherMutation,
39 | Custom: checkCypherMutation,
40 | MergeCustoms: checkCypherMutation,
41 | MergeLayeredNetwork: checkCypherMutation,
42 | MergeLayeredNetwork2: checkCypherMutation,
43 | MergeCustomsWithoutReturnOrWithClause: checkCypherMutation
44 | }
45 | };
46 | let augmentedTypeDefs = augmentTypeDefs(testSchema, {
47 | auth: true,
48 | experimental: true
49 | });
50 | const schema = makeExecutableSchema({
51 | typeDefs: augmentedTypeDefs,
52 | resolvers,
53 | resolverValidationOptions: {
54 | requireResolversForResolveType: false
55 | }
56 | });
57 |
58 | // query the test schema with the test query, assertion is in the resolver
59 | return graphql(
60 | schema,
61 | graphqlQuery,
62 | null,
63 | {
64 | cypherParams: {
65 | userId: 'user-id'
66 | }
67 | },
68 | graphqlParams
69 | );
70 | }
71 |
72 | export function augmentedSchemaCypherTestRunner(
73 | t,
74 | graphqlQuery,
75 | graphqlParams,
76 | expectedCypherQuery,
77 | expectedCypherParams
78 | ) {
79 | const checkCypherQuery = (object, params, ctx, resolveInfo) => {
80 | const [query, queryParams] = cypherQuery(params, ctx, resolveInfo);
81 | t.is(query, expectedCypherQuery);
82 | const deserializedParams = JSON.parse(JSON.stringify(queryParams));
83 | t.deepEqual(deserializedParams, expectedCypherParams);
84 | };
85 | const checkCypherMutation = (object, params, ctx, resolveInfo) => {
86 | const [query, queryParams] = cypherMutation(params, ctx, resolveInfo);
87 | t.is(query, expectedCypherQuery);
88 | const deserializedParams = JSON.parse(JSON.stringify(queryParams));
89 | t.deepEqual(deserializedParams, expectedCypherParams);
90 | };
91 |
92 | const resolvers = {
93 | Mutation: {
94 | CreateUser: checkCypherMutation,
95 | DeleteUser: checkCypherMutation,
96 | MergeUser: checkCypherMutation,
97 | Custom: checkCypherMutation,
98 | MergeCustoms: checkCypherMutation,
99 | MergeLayeredNetwork: checkCypherMutation,
100 | MergeLayeredNetwork2: checkCypherMutation,
101 | MergeCustomsWithoutReturnOrWithClause: checkCypherMutation
102 | }
103 | };
104 |
105 | const cypherTestTypeDefs = printSchemaDocument({
106 | schema: makeAugmentedSchema({
107 | typeDefs: testSchema,
108 | resolvers: {},
109 | config: {
110 | auth: true,
111 | experimental: true
112 | }
113 | })
114 | });
115 |
116 | const augmentedSchema = makeExecutableSchema({
117 | typeDefs: cypherTestTypeDefs,
118 | resolvers,
119 | resolverValidationOptions: {
120 | requireResolversForResolveType: false
121 | }
122 | });
123 |
124 | return graphql(
125 | augmentedSchema,
126 | graphqlQuery,
127 | null,
128 | {
129 | cypherParams: {
130 | userId: 'user-id'
131 | }
132 | },
133 | graphqlParams
134 | );
135 | }
136 |
--------------------------------------------------------------------------------
/test/helpers/experimental/testSchema.js:
--------------------------------------------------------------------------------
1 | import { gql } from 'apollo-server';
2 |
3 | export const testSchema = `
4 | type User {
5 | idField: ID! @id
6 | name: String
7 | names: [String]
8 | birthday: DateTime
9 | birthdays: [DateTime]
10 | uniqueString: String! @unique
11 | indexedInt: Int @index
12 | liked: [Movie!]! @relation(
13 | name: "RATING",
14 | direction: OUT
15 | )
16 | rated: [Rating]
17 | }
18 |
19 | extend type User {
20 | extensionString: String!
21 | }
22 |
23 | type Rating @relation(from: "user", to: "movie") {
24 | user: User
25 | rating: Int!
26 | movie: Movie
27 | }
28 |
29 | type Movie {
30 | id: ID! @id
31 | title: String! @unique
32 | genre: MovieGenre @index
33 | likedBy: [User!]! @relation(
34 | name: "RATING",
35 | direction: IN
36 | )
37 | ratedBy: [Rating]
38 | }
39 |
40 | enum MovieGenre {
41 | Action
42 | Mystery
43 | Scary
44 | }
45 |
46 | enum Role {
47 | reader
48 | user
49 | admin
50 | }
51 | `;
52 |
--------------------------------------------------------------------------------
/test/helpers/filterTestHelpers.js:
--------------------------------------------------------------------------------
1 | import { makeExecutableSchema } from 'graphql-tools';
2 | import { graphql } from 'graphql';
3 | import { cypherQuery, augmentTypeDefs } from '../../src/index';
4 |
5 | export const filterTestRunner = (
6 | t,
7 | typeDefs,
8 | graphqlQuery,
9 | graphqlParams,
10 | expectedCypherQuery,
11 | expectedCypherParams
12 | ) => {
13 | const resolvers = {
14 | Query: {
15 | person(object, params, ctx, resolveInfo) {
16 | const [query, queryParams] = cypherQuery(params, ctx, resolveInfo);
17 | t.is(query, expectedCypherQuery);
18 | // need to turn neo4j Integers (used for temporal params) back to just JSON
19 | const deserializedParams = JSON.parse(JSON.stringify(queryParams));
20 | t.deepEqual(deserializedParams, expectedCypherParams);
21 | },
22 | Company(object, params, ctx, resolveInfo) {
23 | const [query, queryParams] = cypherQuery(params, ctx, resolveInfo);
24 | t.is(query, expectedCypherQuery);
25 | const deserializedParams = JSON.parse(JSON.stringify(queryParams));
26 | t.deepEqual(deserializedParams, expectedCypherParams);
27 | }
28 | }
29 | };
30 | const schema = makeExecutableSchema({
31 | typeDefs: augmentTypeDefs(typeDefs),
32 | resolvers,
33 | resolverValidationOptions: {
34 | requireResolversForResolveType: false
35 | }
36 | });
37 | return graphql(schema, graphqlQuery, null, {}, graphqlParams);
38 | };
39 |
--------------------------------------------------------------------------------
/test/helpers/tck/parseTck.js:
--------------------------------------------------------------------------------
1 | import { generateTestFile } from './parser';
2 |
3 | const TCK_FILE = './filterTck.md';
4 | const TEST_FILE = './test/unit/filterTests.test.js';
5 |
6 | generateTestFile(TCK_FILE, TEST_FILE);
7 |
--------------------------------------------------------------------------------
/test/helpers/tck/parser.js:
--------------------------------------------------------------------------------
1 | import { makeExecutableSchema } from 'graphql-tools';
2 | import { parse, print, graphql } from 'graphql';
3 | import { readFileSync, createWriteStream } from 'fs';
4 | import { augmentTypeDefs, cypherQuery } from '../../../src/index';
5 | import path from 'path';
6 | import { EOL } from 'os';
7 |
8 | export const generateTestFile = async (tckFile, testFile, extractionLimit) => {
9 | const tck = await extractTck(tckFile, testFile);
10 | const testDeclarations = buildTestDeclarations(tck, extractionLimit);
11 | writeTestFile(testFile, tck, testDeclarations);
12 | };
13 |
14 | const extractTck = fileName => {
15 | return new Promise((resolve, reject) => {
16 | try {
17 | let lines = readFileSync(path.join(__dirname, fileName)).toString(
18 | 'utf-8'
19 | );
20 | lines = lines.split(`${EOL}`);
21 | // extract array elements for typeDefs
22 | const typeDefs = extractBlock('schema', lines);
23 | if (typeDefs) {
24 | resolve({
25 | typeDefs: typeDefs.join(`${EOL} `),
26 | tests: extractTestBlocks(lines)
27 | });
28 | }
29 | } catch (err) {
30 | reject(err);
31 | }
32 | });
33 | };
34 |
35 | const extractBlock = (type, lines, index = 0) => {
36 | const startTag = '```' + type;
37 | const endTag = '```';
38 | const typeBlockIndex = lines.indexOf(startTag, index);
39 | let extracted = undefined;
40 | if (typeBlockIndex !== -1) {
41 | const endIndex = lines.indexOf(endTag, typeBlockIndex);
42 | if (type === 'params') {
43 | const beforeTagIndex = lines.indexOf('```cypher', index);
44 | // reject if the next params block occurs after the next cypher block
45 | // in order to prevent getting unassociated params block
46 | if (typeBlockIndex > beforeTagIndex) {
47 | return extracted;
48 | }
49 | }
50 | // offset by 1 to skip the startingTag
51 | extracted = lines.slice(typeBlockIndex + 1, endIndex);
52 | if (extracted.length === 0) {
53 | extracted = lines.slice(typeBlockIndex + 1);
54 | }
55 | }
56 | return extracted;
57 | };
58 |
59 | const extractTestBlocks = data => {
60 | let lastTitle = '';
61 | return data.reduce((acc, line, index, lines) => {
62 | if (line.startsWith('###')) {
63 | lastTitle = line.substring(3).trim();
64 | }
65 | // beginning at every ```graphql line
66 | if (line === '```graphql') {
67 | // extract the array elements of this graphql block
68 | const graphqlBlock = extractBlock('graphql', lines, index);
69 | if (graphqlBlock) {
70 | acc.push({
71 | test: lastTitle,
72 | graphql: graphqlBlock,
73 | params: extractBlock('params', lines, index),
74 | cypher: extractBlock('cypher', lines, index)
75 | });
76 | }
77 | }
78 | return acc;
79 | }, []);
80 | };
81 |
82 | const buildTestDeclarations = (tck, extractionLimit) => {
83 | const schema = makeTestDataSchema(tck);
84 | const testData = buildTestData(schema, tck);
85 | return testData
86 | .reduce((acc, test) => {
87 | // escape " so that we can wrap the cypher in "s
88 | const cypherStatement = test.cypher.replace(/"/g, '\\"');
89 | // ava test string template
90 | acc.push(`test("${test.name}", t => {
91 | const graphQLQuery = \`${test.graphql}\`;
92 | const expectedCypherQuery = "${cypherStatement}";
93 | t.plan(2);
94 | filterTestRunner(t, typeDefs, graphQLQuery, ${JSON.stringify(
95 | test.params
96 | )}, expectedCypherQuery, ${JSON.stringify(test.expectedCypherParams)});
97 | });\r\n`);
98 | return acc;
99 | }, [])
100 | .join(`${EOL}`);
101 | };
102 |
103 | const makeTestDataSchema = tck => {
104 | const typeDefs = tck.typeDefs;
105 | const augmentedTypeDefs = augmentTypeDefs(typeDefs);
106 | const resolvers = buildTestDataResolvers(augmentedTypeDefs);
107 | return makeExecutableSchema({
108 | typeDefs: augmentedTypeDefs,
109 | resolvers,
110 | resolverValidationOptions: {
111 | requireResolversForResolveType: false
112 | }
113 | });
114 | };
115 |
116 | const buildTestData = (schema, tck) => {
117 | const extractedTckTestData = tck.tests;
118 | return extractedTckTestData.reduce((acc, testBlocks) => {
119 | const testName = testBlocks.test;
120 | // graphql
121 | let testGraphql = testBlocks.graphql.join(`${EOL}`);
122 | // validation and formatting through parse -> print
123 | testGraphql = parse(testGraphql);
124 | testGraphql = print(testGraphql);
125 | // graphql variables
126 | let testParams = {};
127 | if (testBlocks.params) {
128 | testParams = testBlocks.params.join(`${EOL}`);
129 | testParams = JSON.parse(testParams);
130 | }
131 | const testCypher = testBlocks.cypher.join(' ');
132 | // get expected params from within resolver
133 | graphql(
134 | schema,
135 | testGraphql,
136 | null,
137 | {
138 | testData: acc,
139 | name: testName,
140 | graphql: testGraphql,
141 | params: testParams,
142 | cypher: testCypher
143 | },
144 | testParams
145 | ).then(data => {
146 | const errors = data['errors'];
147 | if (errors) {
148 | const error = errors[0];
149 | const message = error.message;
150 | console.log(`
151 | Parse Error:
152 | testName: ${testName}
153 | message: ${message}
154 | `);
155 | }
156 | });
157 | return acc;
158 | }, []);
159 | };
160 |
161 | const buildTestDataResolvers = augmentedTypeDefs => {
162 | const definitions = parse(augmentedTypeDefs).definitions;
163 | const resolvers = {};
164 | const queryType = definitions.find(
165 | definition => definition.name && definition.name.value === 'Query'
166 | );
167 | if (queryType) {
168 | const queryMap = queryType.fields.reduce((acc, t) => {
169 | acc[t.name.value] = t;
170 | return acc;
171 | }, {});
172 | resolvers['Query'] = buildResolvers(queryMap);
173 | }
174 | const mutationType = definitions.find(
175 | definition => definition.name && definition.name.value === 'Mutation'
176 | );
177 | if (mutationType) {
178 | const mutationMap = mutationType.fields.reduce((acc, t) => {
179 | acc[t.name.value] = t;
180 | return acc;
181 | }, {});
182 | resolvers['Mutation'] = buildResolvers(mutationMap);
183 | }
184 | return resolvers;
185 | };
186 |
187 | const buildResolvers = fieldMap => {
188 | return Object.keys(fieldMap).reduce((acc, t) => {
189 | acc[t] = (object, params, ctx, resolveInfo) => {
190 | const [query, cypherParams] = cypherQuery(params, ctx, resolveInfo);
191 | ctx.testData.push({
192 | name: ctx.name,
193 | graphql: ctx.graphql,
194 | params: ctx.params,
195 | cypher: ctx.cypher,
196 | expectedCypherParams: cypherParams
197 | });
198 | };
199 | return acc;
200 | }, {});
201 | };
202 |
203 | const writeTestFile = (testFile, tck, testDeclarations) => {
204 | const typeDefs = tck.typeDefs;
205 | const writeStream = createWriteStream(testFile);
206 | writeStream.write(`// Generated by test/helpers/tck/parseTck.js
207 | import test from 'ava';
208 | import { filterTestRunner } from '../helpers/filterTestHelpers';
209 |
210 | const typeDefs = \`
211 | ${typeDefs}
212 | \`;
213 |
214 | ${testDeclarations}`);
215 | };
216 |
--------------------------------------------------------------------------------
/test/integration/test-middleware.test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava';
2 |
3 | import { ApolloClient } from 'apollo-client';
4 | import { HttpLink } from 'apollo-link-http';
5 | import { InMemoryCache } from 'apollo-cache-inmemory';
6 |
7 | import gql from 'graphql-tag';
8 | import fetch from 'node-fetch';
9 |
10 | let client;
11 |
12 | const headers = {
13 | 'x-error': 'Middleware error'
14 | };
15 |
16 | test.before(() => {
17 | client = new ApolloClient({
18 | link: new HttpLink({ uri: 'http://localhost:3000', fetch, headers }),
19 | cache: new InMemoryCache()
20 | });
21 | });
22 |
23 | test('Middleware fail on req.error', async t => {
24 | t.plan(1);
25 |
26 | await client
27 | .query({
28 | query: gql`
29 | {
30 | Movie(title: "River Runs Through It, A") {
31 | title
32 | }
33 | }
34 | `
35 | })
36 | .then(data => {
37 | t.fail('Error should be thrown.');
38 | })
39 | .catch(error => {
40 | t.pass();
41 | });
42 | });
43 |
--------------------------------------------------------------------------------
/test/tck/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neo4j-graphql/neo4j-graphql-js/381ef0302bbd11ecd0f94f978045cdbc61c39b8e/test/tck/.gitkeep
--------------------------------------------------------------------------------
/test/unit/configTest.test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava';
2 | import { printSchema } from 'graphql';
3 | import {
4 | augmentSchema,
5 | makeAugmentedSchema,
6 | augmentTypeDefs
7 | } from '../../src/index';
8 | import { typeDefs } from '../helpers/configTestHelpers';
9 | import { makeExecutableSchema } from 'graphql-tools';
10 |
11 | test.cb('Config - makeAugmentedSchema - no queries, no mutations', t => {
12 | const testSchema = makeAugmentedSchema({
13 | typeDefs,
14 | config: {
15 | query: false,
16 | mutation: false,
17 | auth: false
18 | }
19 | });
20 |
21 | t.is(printSchema(testSchema).includes('type Mutation'), false);
22 | t.is(printSchema(testSchema).includes('type Query'), false);
23 | t.end();
24 | });
25 |
26 | test.cb('Config - augmentSchema - no queries, no mutations', t => {
27 | const schema = makeExecutableSchema({
28 | typeDefs: augmentTypeDefs(typeDefs, { auth: false })
29 | });
30 |
31 | const augmentedSchema = augmentSchema(schema, {
32 | query: false,
33 | mutation: false
34 | });
35 |
36 | t.is(printSchema(augmentedSchema).includes('type Mutation'), false);
37 | t.is(printSchema(augmentedSchema).includes('type Query'), false);
38 | t.end();
39 | });
40 |
41 | test.cb('Config - makeAugmentedSchema - enable queries, no mutations', t => {
42 | const testSchema = makeAugmentedSchema({
43 | typeDefs,
44 | config: {
45 | query: true,
46 | mutation: false
47 | }
48 | });
49 |
50 | t.is(printSchema(testSchema).includes('type Mutation'), false);
51 | t.is(printSchema(testSchema).includes('type Query'), true);
52 | t.end();
53 | });
54 |
55 | test.cb('Config - augmentSchema - enable queries, no mutations', t => {
56 | const schema = makeExecutableSchema({
57 | typeDefs: augmentTypeDefs(typeDefs, { auth: false })
58 | });
59 |
60 | const augmentedSchema = augmentSchema(schema, {
61 | query: true,
62 | mutation: false
63 | });
64 |
65 | t.is(printSchema(augmentedSchema).includes('type Mutation'), false);
66 | t.is(printSchema(augmentedSchema).includes('type Query'), true);
67 | t.end();
68 | });
69 |
70 | test.cb(
71 | 'Config - makeAugmentedSchema - enable queries, enable mutations',
72 | t => {
73 | const testSchema = makeAugmentedSchema({
74 | typeDefs,
75 | config: {
76 | query: true,
77 | mutation: true
78 | }
79 | });
80 |
81 | t.is(printSchema(testSchema).includes('type Mutation'), true);
82 | t.is(printSchema(testSchema).includes('type Query'), true);
83 | t.end();
84 | }
85 | );
86 |
87 | test.cb('Config - augmentSchema - enable queries, enable mutations', t => {
88 | const schema = makeExecutableSchema({
89 | typeDefs: augmentTypeDefs(typeDefs, { auth: false })
90 | });
91 |
92 | const augmentedSchema = augmentSchema(schema, {
93 | query: true,
94 | mutation: true
95 | });
96 |
97 | t.is(printSchema(augmentedSchema).includes('type Mutation'), true);
98 | t.is(printSchema(augmentedSchema).includes('type Query'), true);
99 | t.end();
100 | });
101 |
102 | test.cb(
103 | 'Config - makeAugmentedSchema - specify types to exclude for mutation',
104 | t => {
105 | const testSchema = makeAugmentedSchema({
106 | typeDefs,
107 | config: {
108 | mutation: {
109 | exclude: ['User', 'Hashtag']
110 | }
111 | }
112 | });
113 |
114 | t.is(printSchema(testSchema).includes('CreateUser'), false);
115 | t.is(printSchema(testSchema).includes('DeleteHashtag'), false);
116 | t.end();
117 | }
118 | );
119 |
120 | test.cb('Config - augmentSchema - specify types to exclude for mutation', t => {
121 | const schema = makeExecutableSchema({
122 | typeDefs: augmentTypeDefs(typeDefs, { auth: false })
123 | });
124 |
125 | const augmentedSchema = augmentSchema(schema, {
126 | mutation: {
127 | exclude: ['User', 'Hashtag']
128 | }
129 | });
130 |
131 | t.is(printSchema(augmentedSchema).includes('CreateUser'), false);
132 | t.is(printSchema(augmentedSchema).includes('DeleteHashtag'), false);
133 | t.end();
134 | });
135 |
136 | test.cb(
137 | 'Config - makeAugmentedSchema - specify types to exclude for query',
138 | t => {
139 | const testSchema = makeAugmentedSchema({
140 | typeDefs,
141 | config: {
142 | query: {
143 | exclude: ['User', 'Hashtag']
144 | }
145 | }
146 | });
147 |
148 | const queryType = `
149 | type Query {
150 | """
151 | [Generated query](https://grandstack.io/docs/graphql-schema-generation-augmentation#generated-queries) for Tweet type nodes.
152 | """
153 | Tweet(id: ID, timestamp: _Neo4jDateTimeInput, text: String, _id: String, first: Int, offset: Int, orderBy: [_TweetOrdering], filter: _TweetFilter): [Tweet]
154 | }
155 | `;
156 |
157 | t.is(printSchema(testSchema).includes(queryType), true);
158 | t.end();
159 | }
160 | );
161 |
162 | test.cb('Config - augmentSchema - specify types to exclude for query', t => {
163 | const schema = makeExecutableSchema({
164 | typeDefs: augmentTypeDefs(typeDefs, { auth: false })
165 | });
166 |
167 | const augmentedSchema = augmentSchema(schema, {
168 | query: {
169 | exclude: ['User', 'Hashtag']
170 | }
171 | });
172 |
173 | const queryType = `
174 | type Query {
175 | """
176 | [Generated query](https://grandstack.io/docs/graphql-schema-generation-augmentation#generated-queries) for Tweet type nodes.
177 | """
178 | Tweet(id: ID, timestamp: _Neo4jDateTimeInput, text: String, _id: String, first: Int, offset: Int, orderBy: [_TweetOrdering], filter: _TweetFilter): [Tweet]
179 | }
180 | `;
181 |
182 | t.is(printSchema(augmentedSchema).includes(queryType), true);
183 | t.end();
184 | });
185 |
186 | test.cb('Config - temporal - disable temporal schema augmentation', t => {
187 | const schema = makeAugmentedSchema({
188 | typeDefs,
189 | config: {
190 | temporal: false
191 | }
192 | });
193 |
194 | t.is(printSchema(schema).includes('_Neo4jDateTime'), false);
195 | t.is(printSchema(schema).includes('_Neo4jDateTimeInput'), false);
196 | t.end();
197 | });
198 |
199 | test.cb(
200 | 'Config - temporal - disable temporal schema augmentation (type specific)',
201 | t => {
202 | const schema = makeAugmentedSchema({
203 | typeDefs,
204 | config: {
205 | temporal: {
206 | time: false,
207 | date: false,
208 | datetime: false,
209 | localtime: false
210 | }
211 | }
212 | });
213 |
214 | t.is(printSchema(schema).includes('_Neo4jDateTime'), false);
215 | t.is(printSchema(schema).includes('_Neo4jDateTimeInput'), false);
216 | t.end();
217 | }
218 | );
219 |
220 | test.cb('Config - spatial - disable spatial schema augmentation', t => {
221 | const schema = makeAugmentedSchema({
222 | typeDefs,
223 | config: {
224 | spatial: false
225 | }
226 | });
227 | t.is(printSchema(schema).includes('_Neo4jPoint'), false);
228 | t.is(printSchema(schema).includes('_Neo4jPointInput'), false);
229 | t.end();
230 | });
231 |
232 | test.cb(
233 | 'Config - spatial - disable spatial schema augmentation (type specific)',
234 | t => {
235 | const schema = makeAugmentedSchema({
236 | typeDefs,
237 | config: {
238 | spatial: {
239 | point: false
240 | }
241 | }
242 | });
243 | t.is(printSchema(schema).includes('_Neo4jPoint'), false);
244 | t.is(printSchema(schema).includes('_Neo4jPointInput'), false);
245 | t.is(printSchema(schema).includes('_Neo4jDistanceFilterInput'), false);
246 | t.end();
247 | }
248 | );
249 |
250 | test.cb('Config - default configuration persistence', t => {
251 | const schema = makeAugmentedSchema({
252 | typeDefs,
253 | config: {
254 | temporal: false
255 | }
256 | });
257 | t.is(printSchema(schema).includes('Query'), true);
258 | t.is(printSchema(schema).includes('Mutation'), true);
259 | t.end();
260 | });
261 |
--------------------------------------------------------------------------------
/test/unit/filterTest.test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava';
2 | import { makeAugmentedSchema, cypherQuery } from '../../src/index.js';
3 | import { graphql } from 'graphql';
4 |
5 | test('Filters with unfiltered parents, nested relationship types', async t => {
6 | const typeDefs = /* GraphQL */ `
7 | type A_B_Relation @relation(name: "A_TO_B") {
8 | from: A
9 | to: B
10 | }
11 | type B_C_Relation @relation(name: "B_TO_C") {
12 | from: B
13 | to: C
14 | active: Boolean!
15 | }
16 | type A {
17 | bArray: [A_B_Relation!]!
18 | }
19 | type B {
20 | cArray: [B_C_Relation!]!
21 | }
22 | type C {
23 | id: ID!
24 | }
25 | type Query {
26 | A: [A]
27 | }
28 | `;
29 |
30 | const graphqlQuery = /* GraphQL */ `
31 | {
32 | A {
33 | bArray {
34 | B {
35 | filteredCArray: cArray(filter: { active: true }) {
36 | C {
37 | id
38 | }
39 | }
40 | }
41 | }
42 | }
43 | }
44 | `;
45 |
46 | const expectedCypherQuery =
47 | 'MATCH (`a`:`A`) RETURN `a` {bArray: [(`a`)-[`a_bArray_relation`:`A_TO_B`]->(:`B`) | a_bArray_relation {B: head([(:`A`)-[`a_bArray_relation`]->(`a_bArray_B`:`B`) | a_bArray_B {cArray: [(`a_bArray_B`)-[`a_bArray_B_cArray_relation`:`B_TO_C`]->(:`C`) WHERE (`a_bArray_B_cArray_relation`.active = $`1_filter`.active) | a_bArray_B_cArray_relation {C: head([(:`B`)-[`a_bArray_B_cArray_relation`]->(`a_bArray_B_cArray_C`:`C`) | a_bArray_B_cArray_C { .id }]) }] }]) }] } AS `a`';
48 | const expectedCypherParams = {
49 | '1_filter': { active: true },
50 | first: -1,
51 | offset: 0
52 | };
53 |
54 | const resolvers = {
55 | Query: {
56 | A(object, params, ctx, resolveInfo) {
57 | const [query, queryParams] = cypherQuery(params, ctx, resolveInfo);
58 | t.is(query, expectedCypherQuery);
59 | t.deepEqual(queryParams, expectedCypherParams);
60 | return [];
61 | }
62 | }
63 | };
64 |
65 | const schema = makeAugmentedSchema({
66 | typeDefs,
67 | resolvers,
68 | config: { mutation: false }
69 | });
70 |
71 | // query the test schema with the test query, assertion is in the resolver
72 | const resp = await graphql(schema, graphqlQuery);
73 |
74 | t.deepEqual(resp.data.A, []);
75 |
76 | return;
77 | });
78 |
--------------------------------------------------------------------------------
/test/unit/neo4j-schema/Neo4jSchemaTreeTest.test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava';
2 | import Neo4jSchemaTree from '../../../src/neo4j-schema/Neo4jSchemaTree';
3 | import fakes from '../../helpers/driverFakes';
4 | import _ from 'lodash';
5 |
6 | let tree;
7 | let driver;
8 |
9 | const fakeResponses = {
10 | 'CALL db.schema.nodeTypeProperties()': [
11 | {
12 | nodeLabels: ['A', 'B'],
13 | propertyName: 'prop1',
14 | propertyTypes: ['String'],
15 | mandatory: true
16 | },
17 | {
18 | nodeLabels: ['A', 'B'],
19 | propertyName: 'prop2',
20 | propertyTypes: ['Float'],
21 | mandatory: false
22 | }
23 | ],
24 | 'CALL db.schema.relTypeProperties()': [
25 | {
26 | relType: ':`REL`',
27 | propertyName: null,
28 | propertyTypes: null,
29 | mandatory: null
30 | },
31 | {
32 | relType: ':`WITH_PROPS`',
33 | propertyName: 'a',
34 | propertyTypes: ['String'],
35 | mandatory: true
36 | },
37 | {
38 | relType: ':`WITH_PROPS`',
39 | propertyName: 'b',
40 | propertyTypes: ['String'],
41 | mandatory: true
42 | }
43 | ],
44 | // Regex match for triggering _populateRelationshipTypeLinks
45 | '.*as from,.*as to': [{ from: ['A', 'B'], to: ['A', 'B'] }]
46 | };
47 |
48 | test.beforeEach(t => {
49 | driver = fakes.Driver(fakeResponses);
50 |
51 | tree = new Neo4jSchemaTree(driver);
52 | });
53 |
54 | test('Driver ownership', t => {
55 | t.is(tree.driver, driver);
56 | });
57 |
58 | test('Initialize', t => {
59 | return tree.initialize().then(() => {
60 | const nodes = tree.getNodes();
61 | t.is(nodes.length, 1);
62 | const n = nodes[0];
63 |
64 | t.is(n.id, 'A:B');
65 |
66 | // Schema tree should assign graphQLTypes to properties
67 | t.is(n.getProperty('prop1').graphQLType, 'String!');
68 |
69 | const rels = tree.getRels();
70 | t.is(rels.length, 2);
71 | const rel = rels.filter(r => r.getRelationshipType() === 'REL')[0];
72 | const withProps = rels.filter(
73 | r => r.getRelationshipType() === 'WITH_PROPS'
74 | )[0];
75 |
76 | t.deepEqual(rel.getPropertyNames(), []);
77 | t.deepEqual(withProps.getPropertyNames(), ['a', 'b']);
78 |
79 | t.is(tree.getNodeByLabels(['B', 'A']), n);
80 | t.is(tree.getNodeByLabels(['Nonexistant']), undefined);
81 | });
82 | });
83 |
84 | test('Link Establishment', t => {
85 | return tree.initialize().then(() => {
86 | const rels = tree.getRels();
87 | const rel = rels.filter(r => r.getRelationshipType() === 'REL')[0];
88 | const withProps = rels.filter(
89 | r => r.getRelationshipType() === 'WITH_PROPS'
90 | )[0];
91 |
92 | // They're both from A,B -> A,B
93 |
94 | t.true(rel.isOutboundFrom('A'));
95 | t.true(rel.isOutboundFrom('B'));
96 | t.true(withProps.isOutboundFrom('A'));
97 | t.true(withProps.isOutboundFrom('B'));
98 | });
99 | });
100 |
101 | test('toJSON', t => {
102 | return tree.initialize().then(() => {
103 | const obj = tree.toJSON();
104 | t.true(_.isObject(obj.nodes) && !_.isNil(obj.nodes));
105 | t.true(_.isObject(obj.rels) && !_.isNil(obj.rels));
106 | });
107 | });
108 |
--------------------------------------------------------------------------------
/test/unit/neo4j-schema/entitiesTest.test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava';
2 | import schema from '../../../src/neo4j-schema/entities';
3 | import _ from 'lodash';
4 |
5 | let n;
6 | let r;
7 |
8 | const nodeOkapiID = 'Foo:Bar';
9 | const relOkapiID = ':`REL WITH SPACE`';
10 |
11 | test.before(t => {
12 | n = new schema.Neo4jNode(nodeOkapiID);
13 | r = new schema.Neo4jRelationship(relOkapiID);
14 | });
15 |
16 | test('Neo4jNode basics', t => {
17 | t.is(n.type, 'node');
18 | t.is(n.id, nodeOkapiID);
19 | });
20 |
21 | test('SchemaEntity properties', t => {
22 | const fooProp = { foo: 'bar' };
23 | const barProp = { bar: 'foo' };
24 | n.addProperty('foo', fooProp);
25 | n.addProperty('bar', barProp);
26 |
27 | t.deepEqual(n.getPropertyNames(), ['bar', 'foo']);
28 | t.deepEqual(n.getProperty('foo'), fooProp);
29 | t.deepEqual(n.getProperty('bar'), barProp);
30 |
31 | t.true(_.isNil(n.getProperty('nonexistant')));
32 | });
33 |
34 | test('Neo4jNode labels', t => t.deepEqual(n.getLabels(), ['Bar', 'Foo']));
35 | test('Neo4jNode graphQLType', t => t.is(n.getGraphQLTypeName(), 'Bar_Foo'));
36 |
37 | test('Neo4jRelationship basics', t => {
38 | t.is(r.type, 'relationship');
39 | t.is(r.id, relOkapiID);
40 | });
41 |
42 | test('Neo4jRelationship type', t =>
43 | t.is(r.getRelationshipType(), 'REL WITH SPACE'));
44 |
45 | test('Neo4jRelationship graphQLTypeName', t =>
46 | t.is(r.getGraphQLTypeName(), 'REL_WITH_SPACE'));
47 |
48 | test('Neo4j Relationship Links', t => {
49 | const links = [
50 | { from: ['A', 'B'], to: ['C', 'D'] },
51 | { from: ['E'], to: ['F'] }
52 | ];
53 |
54 | r.links = links;
55 |
56 | ['A', 'B', 'E'].forEach(label => {
57 | t.true(r.isOutboundFrom(label));
58 | t.false(r.isInboundTo(label));
59 | });
60 |
61 | ['C', 'D', 'F'].forEach(label => {
62 | t.true(r.isInboundTo(label));
63 | t.false(r.isOutboundFrom(label));
64 | });
65 |
66 | // isOutboundFrom/isInboundTo should also work on sets.
67 | t.true(r.isOutboundFrom(['B', 'A']));
68 | t.true(r.isInboundTo(['C', 'D']));
69 |
70 | t.deepEqual(r.getToLabels(), ['C', 'D', 'F']);
71 | t.deepEqual(r.getFromLabels(), ['A', 'B', 'E']);
72 |
73 | t.false(r.isUnivalent());
74 | });
75 |
76 | test('Neo4j Univalent/Multivalent Relationships', t => {
77 | const univalentLinks = [{ from: ['A', 'B'], to: ['C', 'D'] }];
78 | const multiValentLinks = [
79 | { from: ['A', 'B'], to: ['C', 'D'] },
80 | { from: ['E'], to: ['F'] }
81 | ];
82 |
83 | r.links = univalentLinks;
84 | t.true(r.isUnivalent());
85 | r.links = multiValentLinks;
86 | t.false(r.isUnivalent());
87 | });
88 |
--------------------------------------------------------------------------------
/test/unit/neo4j-schema/graphQLMapperTest.test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava';
2 | import schema from '../../../src/neo4j-schema/entities';
3 | import graphQLMapper from '../../../src/neo4j-schema/graphQLMapper';
4 | import Neo4jSchemaTree from '../../../src/neo4j-schema/Neo4jSchemaTree';
5 | import driverFakes from '../../helpers/driverFakes';
6 | import assert from 'assert';
7 | import _ from 'lodash';
8 |
9 | let tree;
10 | let driver;
11 | let typeDefs;
12 | let result;
13 | let resolvers;
14 |
15 | const fakeOkapiProperty = (labels, name, graphQLType, mandatory = false) => ({
16 | nodeLabels: labels,
17 | graphQLType,
18 | mandatory
19 | });
20 |
21 | const fakeOkapiRelProperty = (
22 | relType,
23 | name,
24 | graphQLType,
25 | mandatory = false
26 | ) => ({
27 | relType,
28 | propertyName: name,
29 | graphQLType,
30 | mandatory
31 | });
32 |
33 | test.before(t => {
34 | driver = driverFakes.Driver();
35 |
36 | // Create a fake tree.
37 | tree = new Neo4jSchemaTree(driver);
38 |
39 | const customer = new schema.Neo4jNode('Customer');
40 | const product = new schema.Neo4jNode('Product');
41 | const state = new schema.Neo4jNode('State');
42 |
43 | const buys = new schema.Neo4jRelationship(':`BUYS`');
44 | buys.links = [{ from: ['Customer'], to: ['Product'] }];
45 | const reviewed = new schema.Neo4jRelationship(':`REVIEWED`');
46 | reviewed.links = [{ from: ['Customer'], to: ['Product'] }];
47 | const livesIn = new schema.Neo4jRelationship(':`LIVES_IN`');
48 | livesIn.links = [{ from: ['Customer'], to: ['State'] }];
49 |
50 | customer.addProperty(
51 | 'name',
52 | fakeOkapiProperty(['Customer'], 'name', 'String!')
53 | );
54 | customer.addProperty(
55 | 'age',
56 | fakeOkapiProperty(['Customer'], 'age', 'Integer')
57 | );
58 | product.addProperty(
59 | 'sku',
60 | fakeOkapiProperty(['Product'], 'sku', 'String!', true)
61 | );
62 | state.addProperty(
63 | 'name',
64 | fakeOkapiProperty(['State'], 'name', 'String!', true)
65 | );
66 |
67 | reviewed.addProperty(
68 | 'stars',
69 | fakeOkapiRelProperty(':`REVIEWED`', 'stars', 'Integer', true)
70 | );
71 |
72 | tree.nodes[customer.id] = customer;
73 | tree.nodes[product.id] = product;
74 | tree.nodes[state.id] = state;
75 |
76 | tree.rels[buys.id] = buys;
77 | tree.rels[reviewed.id] = reviewed;
78 | tree.rels[livesIn.id] = livesIn;
79 |
80 | result = graphQLMapper(tree);
81 | typeDefs = result.typeDefs;
82 | resolvers = result.resolvers;
83 | });
84 |
85 | test('Basic Mapping Result Structure', t => {
86 | t.true(typeof result === 'object');
87 | t.true(typeof typeDefs === 'string');
88 | t.true(typeof resolvers === 'object');
89 | });
90 |
91 | test('Defines a GraphQL type per node', t => {
92 | t.true(typeDefs.indexOf('type Customer {') > -1);
93 | t.true(typeDefs.indexOf('type Product {') > -1);
94 | });
95 |
96 | test('All nodes get an _id property to permit propertyless-node labels to work', t => {
97 | t.true(typeDefs.indexOf('_id: Long!') > -1);
98 | });
99 |
100 | test('Defines properties with correct types', t => {
101 | console.log(typeDefs);
102 | t.true(typeDefs.indexOf('age: Integer') > -1);
103 | t.true(typeDefs.indexOf('name: String!') > -1);
104 | t.true(typeDefs.indexOf('sku: String!') > -1);
105 | });
106 |
107 | test('Defines relationships BOTH WAYS with right order and @relation directive', t => {
108 | t.true(
109 | typeDefs.indexOf(
110 | 'lives_in: [State] @relation(name: "LIVES_IN", direction: OUT)'
111 | ) > -1
112 | );
113 | t.true(
114 | typeDefs.indexOf(
115 | 'customers: [Customer] @relation(name: "LIVES_IN", direction: IN)'
116 | ) > -1
117 | );
118 | });
119 |
120 | test('Deconflicts names for multi-targeted relationships by using relationship label', t => {
121 | // From customer, we have both rels REVIEWED and BUYS going out to Product. This means
122 | // that on "Product" the field can't be called "customers" because there would be a naming
123 | // conflict. This tests that the module has worked around this.
124 | t.true(
125 | typeDefs.indexOf(
126 | 'customers_buys: [Customer] @relation(name: "BUYS", direction: IN)'
127 | ) > -1
128 | );
129 |
130 | t.true(
131 | typeDefs.indexOf(
132 | 'customers_reviewed: [Customer] @relation(name: "REVIEWED", direction: IN)'
133 | ) > -1
134 | );
135 | });
136 |
137 | test('Defines relationship types with properties', t => {
138 | console.log(typeDefs);
139 | t.true(typeDefs.indexOf('type REVIEWED @relation(name: "REVIEWED")') > -1);
140 | });
141 |
--------------------------------------------------------------------------------
/test/unit/neo4j-schema/typesTest.test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava';
2 | import types from '../../../src/neo4j-schema/types';
3 | import _ from 'lodash';
4 |
5 | test('label2GraphQLType', t => {
6 | t.is(types.label2GraphQLType('Foo'), 'Foo');
7 | t.is(types.label2GraphQLType('Hello World'), 'Hello_World');
8 | t.is(types.label2GraphQLType('A:B:C'), 'A_B_C');
9 | });
10 |
11 | test('chooseGraphQLType', t => {
12 | const prop = (types, mandatory = false) => ({
13 | propertyTypes: types,
14 | mandatory
15 | });
16 |
17 | t.is(types.chooseGraphQLType(null), 'String');
18 | t.is(types.chooseGraphQLType(prop(['String'], true)), 'String!');
19 | t.is(types.chooseGraphQLType(prop(['Long', 'String'], true)), 'String!');
20 |
21 | // Types which are the same between both.
22 | const sameTypes = [
23 | 'Float',
24 | 'String',
25 | 'Boolean',
26 | 'Date',
27 | 'DateTime',
28 | 'LocalTime',
29 | 'LocalDateTime',
30 | 'Time'
31 | ];
32 | sameTypes.forEach(typeName => {
33 | t.is(types.chooseGraphQLType(prop([typeName], false)), typeName);
34 |
35 | // Repeated types always resolve to the same thing.
36 | t.is(
37 | types.chooseGraphQLType(prop([typeName, typeName, typeName], false)),
38 | typeName
39 | );
40 | });
41 |
42 | // String dominates all other types.
43 | const lotsOfTypes = _.cloneDeep(sameTypes);
44 | t.is(types.chooseGraphQLType(prop(lotsOfTypes, false)), 'String');
45 |
46 | // Array types map to [Type]
47 | sameTypes.forEach(typeName => {
48 | const arrayNeo4jTypeName = `${typeName}Array`;
49 | t.is(
50 | types.chooseGraphQLType(prop([arrayNeo4jTypeName], false)),
51 | '[' + typeName + ']'
52 | );
53 | });
54 |
55 | const mappedTypes = [
56 | { neo4j: 'Long', graphQL: 'Int' },
57 | { neo4j: 'Double', graphQL: 'Float' },
58 | { neo4j: 'Integer', graphQL: 'Int' }
59 | ];
60 |
61 | mappedTypes.forEach(mt => {
62 | t.is(types.chooseGraphQLType(prop([mt.neo4j], false)), mt.graphQL);
63 | });
64 |
65 | // Arrays of mapped types.
66 | mappedTypes.forEach(mt => {
67 | const arrayNeo4jTypeName = `${mt.neo4j}Array`;
68 | t.is(
69 | types.chooseGraphQLType(prop([arrayNeo4jTypeName], false)),
70 | '[' + mt.graphQL + ']'
71 | );
72 | });
73 |
74 | // Domination relationships:
75 | // Long is wider than integer, but in the end, that maps to Int in GraphQL
76 | t.is(types.chooseGraphQLType(prop(['Long', 'Integer'], false)), 'Int');
77 |
78 | // Float is wider than Integer
79 | t.is(types.chooseGraphQLType(prop(['Integer', 'Float'], false)), 'Float');
80 | });
81 |
--------------------------------------------------------------------------------
/test/unit/searchSchema.test.js:
--------------------------------------------------------------------------------
1 | import test from 'ava';
2 | import { makeAugmentedSchema } from '../../src/index';
3 | import { mapSearchDirectives } from '../../src/schemaSearch';
4 | import { gql } from 'apollo-server';
5 | import { ApolloError } from 'apollo-server-errors';
6 |
7 | test('Throws error if named @search index is used on more than one type', t => {
8 | const error = t.throws(
9 | () => {
10 | const schema = makeAugmentedSchema({
11 | typeDefs: gql`
12 | type Movie {
13 | movieId: ID! @id
14 | title: String @search(index: "MoviePersonSearch")
15 | }
16 | type Person {
17 | id: ID! @id
18 | name: String! @search(index: "MoviePersonSearch")
19 | }
20 | `
21 | });
22 | mapSearchDirectives({ schema });
23 | },
24 | {
25 | instanceOf: ApolloError
26 | }
27 | );
28 | t.is(
29 | error.message,
30 | `The MoviePersonSearch index on the Movie type cannot be used on the name field of the Person type, because composite search indexes are not yet supported.`
31 | );
32 | });
33 |
34 | test('Throws error if @search directive is used on list type field', t => {
35 | const error = t.throws(
36 | () => {
37 | const schema = makeAugmentedSchema({
38 | typeDefs: gql`
39 | type Movie {
40 | movieId: ID! @id
41 | titles: [String] @search
42 | }
43 | `
44 | });
45 | mapSearchDirectives({ schema });
46 | },
47 | {
48 | instanceOf: ApolloError
49 | }
50 | );
51 | t.is(
52 | error.message,
53 | `The @search directive on the titles field of the Movie type is invalid, because search indexes cannot currently be set for list type fields.`
54 | );
55 | });
56 |
57 | test('Throws error if @search directive is used on type other than String', t => {
58 | const error = t.throws(
59 | () => {
60 | const schema = makeAugmentedSchema({
61 | typeDefs: gql`
62 | type Movie {
63 | movieId: ID! @id
64 | year: Int @search
65 | }
66 | `
67 | });
68 | mapSearchDirectives({ schema });
69 | },
70 | {
71 | instanceOf: ApolloError
72 | }
73 | );
74 | t.is(
75 | error.message,
76 | `The @search directive on the year field of the Movie type is invalid, because search indexes can only be set for String and ID type fields.`
77 | );
78 | });
79 |
--------------------------------------------------------------------------------