├── .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 | [![CI status](https://circleci.com/gh/neo4j-graphql/neo4j-graphql-js.svg?style=shield&circle-token=d01ffa752fbeb43585631c78370f7dd40528fbd3)](https://circleci.com/gh/neo4j-graphql/neo4j-graphql-js) [![npm version](https://badge.fury.io/js/neo4j-graphql-js.svg)](https://badge.fury.io/js/neo4j-graphql-js) [![Docs link](https://img.shields.io/badge/Docs-GRANDstack.io-brightgreen.svg)](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 | ![Movie graph data model](img/movies.png) 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 | --------------------------------------------------------------------------------