diff --git a/README.md b/README.md index 39deb2b7..064d45d9 100755 --- a/README.md +++ b/README.md @@ -1,13 +1,15 @@ -[![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) [![codecov](https://codecov.io/gh/neo4j-graphql/neo4j-graphql-js/branch/master/graph/badge.svg)](https://codecov.io/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)](http://grandstack.io/docs/neo4j-graphql-js.html) +> ⚠️ NOTE: This project is no longer actively maintained. Please consider using the [official Neo4j GraphQL Library.](https://neo4j.com/docs/graphql-manual/current/) + +[![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) # neo4j-graphql.js A GraphQL to Cypher query execution layer for Neo4j and JavaScript GraphQL implementations. -- [Read the docs](https://grandstack.io/docs/neo4j-graphql-js.html) +- [Read the docs](https://github.com/neo4j-graphql/neo4j-graphql-js/tree/master/docs) - [Read the changelog](https://github.com/neo4j-graphql/neo4j-graphql-js/blob/master/CHANGELOG.md) -> NOTE: neo4j-graphql.js is facilitated by [Neo4j Labs](https://neo4j.com/labs/). Work has begun on an official Neo4j GraphQL library ([`@neo4j/graphql`](https://www.npmjs.com/package/@neo4j/graphql)), which is currently available as an alpha release on [npm](https://www.npmjs.com/package/@neo4j/graphql). You can find more information and documentation for the `@neo4j/graphql` library [here.](https://github.com/neo4j/graphql-tracker-temp) +neo4j-graphql.js is facilitated by [Neo4j Labs](https://neo4j.com/labs/). ## Installation and usage @@ -107,9 +109,9 @@ See our [detailed contribution guidelines](./CONTRIBUTING.md). See [/examples](https://github.com/neo4j-graphql/neo4j-graphql-js/tree/master/example/apollo-server) -## [Documentation](http://grandstack.io/docs/neo4j-graphql-js.html) +## [Documentation](docs/) -Full docs can be found on [GRANDstack.io/docs](http://grandstack.io/docs/neo4j-graphql-js.html) +Full docs can be found in [docs/](docs/) ## Debugging and Tuning diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..bec9fe10 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,17 @@ +# neo4j-graphql-js Documentation + +- [Quickstart](neo4j-graphql-js-quickstart.md) +- [User Guide](neo4j-graphql-js.md) +- [Schema Generation And Augmentation](graphql-schema-generation-augmentation.md) +- [Filtering With GraphQL](graphql-filtering.md) +- [Relationship Types](graphql-relationship-types.md) +- [Temporal Types (DateTime)](graphql-temporal-types-datetime.md) +- [Spatial Types](graphql-spatial-types.md) +- [Interface and Union Types](graphql-interface-union-types.md) +- [Authorization / Middleware](neo4j-graphql-js-middleware-authorization.md) +- [Multiple Databases](neo4j-multiple-database-graphql.md) +- [GraphQL Schema Directives](graphql-schema-directives.md) +- [API Reference](neo4j-graphql-js-api.md) +- [Adding Custom Logic](graphql-custom-logic.md) +- [Infer GraphQL Schema](infer-graphql-schema-database.md) +- [Apollo Federation And Gateway](apollo-federation.md) diff --git a/docs/apollo-federation.md b/docs/apollo-federation.md new file mode 100644 index 00000000..f9b5b1dd --- /dev/null +++ b/docs/apollo-federation.md @@ -0,0 +1,689 @@ +# Using Apollo Federation and Gateway With Neo4j + +## Introduction and Motivation + +[Apollo Gateway](https://www.apollographql.com/docs/apollo-server/federation/gateway/) composes federated schemas into a single schema, with each receiving appropriately delegated operations while running as a service. An [implementing service](https://www.apollographql.com/docs/apollo-server/federation/implementing-services/) is a schema that conforms to the [Apollo Federation specification](https://www.apollographql.com/docs/apollo-server/federation/federation-spec/), which itself is complaint with the [GraphQL specification](http://spec.graphql.org/June2018/). This approach exposes a [single data graph](https://principledgraphql.com/integrity#1-one-graph) while enabling [concern-based separation](https://www.apollographql.com/docs/apollo-server/federation/introduction/#concern-based-separation) of types _and fields_ across services, ensuring that the data graph remains simple to consume. + +One way to think of Apollo Federation is that it enables microservices for GraphQL: combining multiple GraphQL services together into a single composed GraphQL gateway. + +The following guide will overview this [example](https://github.com/neo4j-graphql/neo4j-graphql-js/tree/master/example/apollo-federation) found in the `neo4j-graphql.js` Github repo, based on the Apollo Federation [demo](https://github.com/apollographql/federation-demo), to demonstrate current behavior with `neo4j-graphql-js`. + +## Setup + +As with the Federation demo [services](https://github.com/apollographql/federation-demo/tree/master/services), we use [buildFederatedSchema](https://www.apollographql.com/docs/apollo-server/api/apollo-federation/) to build four schema: + +- [Accounts](https://github.com/neo4j-graphql/neo4j-graphql-js/blob/master/example/apollo-federation/services/accounts/index.js) +- [Reviews](https://github.com/neo4j-graphql/neo4j-graphql-js/blob/master/example/apollo-federation/services/reviews/index.js) +- [Products](https://github.com/neo4j-graphql/neo4j-graphql-js/blob/master/example/apollo-federation/services/products/index.js) +- [Inventory](https://github.com/neo4j-graphql/neo4j-graphql-js/blob/master/example/apollo-federation/services/inventory/index.js) + +Each federated schema is then exposed as an individual, implementing GraphQL service using [ApolloServer](https://www.apollographql.com/docs/apollo-server/). Finally, the service names and URLs of those servers are provided to [ApolloGateway](https://www.apollographql.com/docs/apollo-server/api/apollo-gateway/), starting a server for a single API based on the composition of the federated schema. + +- [Gateway](https://github.com/neo4j-graphql/neo4j-graphql-js/blob/master/example/apollo-federation/gateway.js) + +You can follow these steps to run the example: + +- Clone or download the [neo4j-graphql-js](https://github.com/neo4j-graphql/neo4j-graphql-js) repository + +Run these Npm scripts to install dependencies and start the gateway: + +- `npm run install` +- `npm run start-gateway` + +Upon successful startup you should see: + +```shell +πŸš€ Accounts ready at http://localhost:4001/ +πŸš€ Reviews ready at http://localhost:4002/ +πŸš€ Products ready at http://localhost:4003/ +πŸš€ Inventory ready at http://localhost:4004/ +πŸš€ Apollo Gateway ready at http://localhost:4000/ +``` + +The following mutation can then be run to merge example data into your Neo4j database: + +```graphql +mutation { + MergeSeedData +} +``` + +![Image of example data in Neo4j Bloom](img/exampleDataGraph.png) + +_Image of example data in Neo4j Bloom_ +

+ +With your Neo4j database active and the gateway and services running, you can run current [integration tests](https://github.com/neo4j-graphql/neo4j-graphql-js/blob/master/test/integration/gateway.test.js). The below Npm script merges example data, runs tests, then deletes the data. + +- `npm run test-gateway` + +## Walkthrough + +Let's consider a reduced version of the example schema: + +```js +import { ApolloServer } from 'apollo-server'; +import { buildFederatedSchema } from '@apollo/federation'; +import { ApolloGateway } from '@apollo/gateway'; +import { neo4jgraphql, makeAugmentedSchema, cypher } from 'neo4j-graphql-js'; +import neo4j from 'neo4j-driver'; + +const driver = neo4j.driver( + process.env.NEO4J_URI || 'bolt://localhost:7687', + neo4j.auth.basic( + process.env.NEO4J_USER || 'neo4j', + process.env.NEO4J_PASSWORD || 'letmein' + ) +); + +const augmentedAccountsSchema = makeAugmentedSchema({ + typeDefs: gql` + type Account @key(fields: "id") { + id: ID! + name: String + username: String + } + `, + config: { + isFederated: true + } +}); + +const accountsService = new ApolloServer({ + schema: buildFederatedSchema([augmentedAccountsSchema]), + context: ({ req }) => { + return { + driver, + req + }; + } +}); + +const reviewsService = new ApolloServer({ + schema: buildFederatedSchema([ + makeAugmentedSchema({ + typeDefs: gql` + extend type Account @key(fields: "id") { + id: ID! @external + + # Example: A reference to an entity defined in -this service- + # as the type of a field added to an entity defined + # in -another service- + reviews(body: String): [Review] + @relation(name: "AUTHOR_OF", direction: OUT) + } + + type Review @key(fields: "id") { + id: ID! + body: String + + # Example: A reference to an entity defined in -another service- + # as the type of a field added to an entity defined + # in -this service- + author: Account @relation(name: "AUTHOR_OF", direction: IN) + product: Product @relation(name: "REVIEW_OF", direction: OUT) + } + + extend type Product @key(fields: "upc") { + upc: ID! @external + + # Same case as Account.reviews + reviews(body: String): [Review] + @relation(name: "REVIEW_OF", direction: IN) + } + `, + config: { + isFederated: true + } + }) + ]), + context: ({ req }) => { + return { + driver, + req + }; + } +}); + +const productService = new ApolloServer({ + schema: buildFederatedSchema([ + makeAugmentedSchema({ + typeDefs: gql` + type Product @key(fields: "upc") { + upc: String! + name: String + price: Int + weight: Int + } + `, + config: { + isFederated: true + } + }) + ]), + context: ({ req }) => { + return { + driver, + req + }; + } +}); + +const inventoryService = new ApolloServer({ + schema: buildFederatedSchema([ + makeAugmentedSchema({ + typeDefs: gql` + extend type Product @key(fields: "upc") { + upc: String! @external + weight: Int @external + price: Int @external + inStock: Boolean + shippingEstimate: Int + @requires(fields: "weight price") + @cypher( + statement: """ + CALL apoc.when($price > 900, + // free for expensive items + 'RETURN 0 AS value', + // estimate is based on weight + 'RETURN $weight * 0.5 AS value', + { + price: $price, + weight: $weight + }) + YIELD value + RETURN value.value + """ + ) + } + `, + config: { + isFederated: true + } + }) + ]), + context: ({ req }) => { + return { + driver, + req + }; + } +}); + +// Start implementing services +accountsService.listen({ port: 4001 }).then(({ url }) => { + console.log(`πŸš€ Accounts ready at ${url}`); +}); + +reviewsService.listen({ port: 4002 }).then(({ url }) => { + console.log(`πŸš€ Reviews ready at ${url}`); +}); + +productsService.listen({ port: 4003 }).then(({ url }) => { + console.log(`πŸš€ Products ready at ${url}`); +}); + +inventoryService.listen({ port: 4003 }).then(({ url }) => { + console.log(`πŸš€ Products ready at ${url}`); +}); + +// Configure gateway +const gateway = new ApolloGateway({ + serviceList: [ + { name: 'accounts', url: 'http://localhost:4001/graphql' }, + { name: 'reviews', url: 'http://localhost:4002/graphql' }, + { name: 'products', url: 'http://localhost:4003/graphql' }, + { name: 'inventory', url: 'http://localhost:4004/graphql' } + ] +}); + +// Start gateway +(async () => { + const server = new ApolloServer({ + gateway + }); + server.listen({ port: 4000 }).then(({ url }) => { + console.log(`πŸš€ Apollo Gateway ready at ${url}`); + }); +})(); +``` + +### Data Sources + +All services in this example use `neo4j-graphql-js` and the same Neo4j database as a data source. This is only for demonstration and testing. If you have an existing monolithic GraphQL server, Gateway could compose your schema with another schema that uses `neo4j-graphql-js`, enabling [incremental adoption](https://www.apollographql.com/docs/apollo-server/federation/introduction/#incremental-adoption). + +### Schema Augmentation + +To support using [schema augmentation](graphql-schema-generation-augmentation.mdx) with Federation, the `isFederated` configuration option can be set to `true`. For now, this does two things. + +- Ensures the return format is a schema module - an object containing `typeDefs` and `resolvers`. A schema module is the expected argument format for `buildFederatedSchema`. +

+- The `isFederated` configuration flag is not required for supporting the use of `neo4jgraphql` to resolve a federated operation. However, two new kinds of resolvers are now generated during schema augmentation to handle entity [references](#resolving-entity-references) and [extensions](#resolving-entity-extension-fields). + +## Defining An Entity + +### `@key` Directive + +An [entity](https://www.apollographql.com/docs/apollo-server/federation/entities) is an object type with its primary keys [defined](https://www.apollographql.com/docs/apollo-server/federation/entities/#defining) using a new [@key](https://www.apollographql.com/docs/apollo-server/federation/federation-spec/#key) type directive provided by Federation. When a service defines an object type entity, another service can [extend](http://spec.graphql.org/June2018/#ObjectTypeExtension) it, allowing that service to [reference](https://www.apollographql.com/docs/apollo-server/federation/entities/#referencing) it on fields and to [add fields](https://www.apollographql.com/docs/graphql-tools/generate-schema/#extending-types) to it that the service will be responsible for resolving. + +To define an object type as an entity, its primary key fields are provided to the `fields` argument of the `@key` type directive. This allows Apollo Gateway to identify `Account` type data between services. In the below schema, the `id` field of the `Account` type is specified as a key. + +###### _Accounts_ + +```graphql +type Account @key(fields: "id") { + id: ID! + name: String + username: String +} +``` + +###### _Reviews_ + +```graphql +type Review @key(fields: "id") { + id: ID! + body: String + authorID: ID +} +``` + +###### _Products_ + +```graphql +type Product @key(fields: "upc") { + upc: String! + name: String! + price: Int +} +``` + +An entity can define [multiple keys](https://www.apollographql.com/docs/apollo-server/federation/entities/#defining-multiple-primary-keys) and its relationship fields can be used as [compound keys](https://www.apollographql.com/docs/apollo-server/federation/entities/#defining-multiple-primary-keys). When a compound key is provided in a representation, `neo4jgraphql` will generate a Cypher translation that selects only entity nodes with relationships to other nodes of the key field type that have property values matching those provided for the compound key. This translation is generated using current support for translating a [relationship field](https://github.com/neo4j-graphql/neo4j-graphql-js/blob/master/test/helpers/tck/filterTck.md#all-related-nodes-matching-filter) query [filtering](graphql-filtering.mdx) argument for the exact name of the relationship field used as a compound key. + +## Referencing An Entity + +A reference to an entity defined in a given service occurs when that entity is used as the type of a field added to an entity defined in another service. + +With the above entities defined throughout three services, an entity is introduced from one service into another by using the GraphQL `extend` keyword. This allows for that entity to be referenced on fields of entities defined by the extending service. + +For example, we can extend the `Account` type in the reviews service and reference it as the type of a new `author` field on `Review`: + +###### _Reviews schema_ + +```graphql +extend type Account @key(fields: "id") { + id: ID! @external +} + +type Review @key(fields: "id") { + id: ID! + body: String + authorID: ID + author: Account +} +``` + +### `@external` Directive + +Another new directive provided by Federation is the [@external](https://www.apollographql.com/docs/apollo-server/federation/federation-spec/#external) field directive. When a service extends an entity from another service, the `fields` of the `@key` directive for the entity must be marked as `@external` so field types can be known during runtime when Gateway builds a query plan. + +### Resolving Entity References + +When the `Review` entity is the root field type of a query, the federated operation begins with the reviews service. If [neo4jgraphql](neo4j-graphql-js-api.mdx#neo4jgraphqlobject-params-context-resolveinfo-debug-executionresult-https-graphqlorg-graphql-js-execution-execute) is used to resolve the query, it will be translated under normal conditions: + +###### _Reviews query_ + +```graphql +query { + Review { + id + body + author { + id + } + } +} +``` + +###### _Reviews resolver_ + +```js +Query: { + async Review(object, params, context, resolveInfo) { + return await neo4jgraphql(object, params, context, resolveInfo); + } +} +``` + +This would result in querying `Account` type data in the reviews service to obtain the `id` key field for the `author` of each queried `Review`. In a federated schema, this `Account` type data for a `Review` `author` is called a **representation**. Field resolvers for fields of the entity type being represented provide representations to other services in order to fetch additional data for the entity, given its specified keys. + +Here is an example of a resolver for the `author` field of the `Review` type in the reviews service, providing a representation of an `Account` entity. + +```js +Review: { + author(review, context, resolveInfo) { + return { __typename: "Account", id: "1" }; + } +} +``` + +Because a query with `neo4jgraphql` resolves at the root field, all representations appropriate for the data the reviews service is responsible for are obtained and provided by the root field translation. When executing a federated query, `neo4jgraphql` decides a default `__typename`. The result is that field resolvers do not normally need to be written to provide representations. + +In this example, we have not yet selected an _external_ field of the `Account` entity which is _not_ a key. We have only selected the field of an `Account` key the reviews service can provide. So there is no need to query the accounts service. If we were to select additional fields of the `Account` entity, such as `name`, which the reviews service is not responsible for resolving, it would result in the following: + +###### _Query_ + +```graphql +query { + Review { + id + body + author { + name + } + } +} +``` + +###### _Reviews query_ + +Gateway would delegate the following selections to the reviews service: + +```graphql +query { + Review { + id + body + author { + id + } + } +} +``` + +The `Account` entity representation data resolved for the `author` field would then be provided to a [new kind of resolver](https://www.apollographql.com/docs/apollo-server/federation/entities/#resolving) defined with a [\_\_resolveReference](https://www.apollographql.com/docs/apollo-server/api/apollo-federation/#__resolvereference) function in the `Account` type resolvers of the accounts service. + +> An implementing service uses reference resolvers for providing data for the fields it resolves for the entities it defines. These reference resolvers are generated during schema augmentation. + +###### _Accounts resolver_ + +```js +Account: { + async __resolveReference(object, context, resolveInfo) { + return await neo4jgraphql(object, {}, context, resolveInfo); + } +} +``` + +###### _Accounts query_ + +In this case, Gateway delegates resolving the following selections to the accounts service. + +```graphql +query { + Account { + name + } +} +``` + +When deciding how to use `neo4jgraphql` to resolve the `author` field, there are a few possibilities we can consider. We may use a property on the `Review` type in a [@cypher](neo4j-graphql-js.mdx#cypher-directive) directive on its `author` field to select related `Account` data. + +###### _Reviews schema_ + +```graphql +type Review @key(fields: "id") { + id: ID! + body: String + authorID: ID + author: Account + @cypher( + statement: """ + MATCH (a:Account { + id: this.authorID + }) + RETURN a + """ + ) +} +``` + +If `Review` and `Account` data are stored in Neo4j as a [property graph](https://neo4j.com/developer/graph-database/#property-graph), we can use the [@relation](neo4j-graphql-js.md#start-with-a-graphql-schema) field directive to support generated translation to [Cypher](https://neo4j.com/developer/cypher-query-language/) that selects the related `Account` through [relationships](https://neo4j.com/docs/getting-started/current/cypher-intro/patterns/#cypher-intro-patterns-relationship-syntax). + +```graphql +type Review @key(fields: "id") { + id: ID! + body: String + author: Account @relation(name: "AUTHOR_OF", direction: IN) +} +``` + +In both cases, the `id` field from each selected `Account`, related to each resolved `Review` in some way, is provided from the reviews service to the accounts service. These representations are then used by the accounts service to select `Account` entities and return a value for the `name` field of each. + +The situation is similar with the `product` field on the `Review` entity in our example. + +```graphql +extend type Account @key(fields: "id") { + id: ID! @external +} + +extend type Product @key(fields: "upc") { + upc: ID! @external +} + +type Review @key(fields: "id") { + id: ID! + body: String + author: Account + @cypher( + statement: """ + MATCH (a:Account { + id: this.authorID + }) + RETURN a + """ + ) + product: Product @relation(name: "REVIEW_OF", direction: OUT) +} +``` + +In this case, the products service would need to use a reference resolver for the `Product` entity, in order to provide its `name`, `price`, or `weight` fields when selected through the `product` field referencing it. + +###### _Products resolver_ + +```js +Product: { + async __resolveReference(object, context, resolveInfo) { + return await neo4jgraphql(object, {}, context, resolveInfo); + } +} +``` + +## Extending An Entity + +An [extension](https://www.apollographql.com/docs/graphql-tools/generate-schema/#extending-types) of an entity defined in a given service occurs when a field is added to it by another service. This enables concern-based separation of types and fields across services. Expanding on the example schema of the reviews service, a `reviews` field is added to the type extension of the `Account` entity below. + +###### _Reviews schema_ + +```graphql +extend type Account @key(fields: "id") { + id: ID! @external + name: String @external + reviews(body: String): [Review] @relation(name: "AUTHOR_OF", direction: OUT) +} + +type Review @key(fields: "id") { + id: ID! + body: String + author: Account @relation(name: "AUTHOR_OF", direction: IN) + product: Product @relation(name: "REVIEW_OF", direction: OUT) +} + +extend type Product @key(fields: "upc") { + upc: ID! @external + reviews(body: String): [Review] @relation(name: "REVIEW_OF", direction: IN) +} +``` + +### Resolving Entity Extension Fields + +Any other service that can reference the `Account` type can now select the `reviews` field added by the reviews service. The `Review` entity may or may not be the root field type of a query that selects the `reviews` field, so the reviews service may not receive the root operation. When it does, as with the following query, the normal type resolver for the `Review` field of the `Query` type would be called. + +###### _Reviews query_ + +```graphql +query { + Review { + id + body + author { + id + name + reviews + } + } +} +``` + +The root field type of a query that selects the `reviews` field may or may not be `Account`. The Gateway query plan may begin at the accounts service, or at some other service selecting the `reviews` field through a reference to the `Account` entity when it is used as the type of a field on another entity defined by the other service. In the below example, the accounts service initially receives the query. + +###### _Accounts query_ + +```graphql +query { + Account { + id + name + reviews + } +} +``` + +In any case, after the service that receives the root query resolves data for any selected `Account` fields it's responsible for (keys, etc.), the query plan will send any obtained `Account` representations to the reviews service for use in resolving the `reviews` field selection set its responsible for, such as `body`, from appropriately related `Review` entity data. + +Resolving fields added to entities from other services also requires the new [\_\_resolveReference](https://www.apollographql.com/docs/apollo-server/api/apollo-federation/#__resolvereference) type resolver. In the case of the `reviews` field added to the `Account` extension, the reviews service must provide a reference resolver for the `Account` entity in order to support resolving `Review` entity data selected through the `reviews` field. + +> An implementing service uses reference resolvers for providing data for the fields it resolves for its extensions of entities defined by other services. These reference resolvers are generated during schema augmentation. + +###### _Accounts resolver_ + +```js +Account: { + async __resolveReference(object, context, resolveInfo) { + const entityData = await neo4jgraphql(object, {}, context, resolveInfo); + return { + // Entity data possibly previously resolved from other services + ...object, + // Entity data now resolved for any selected fields this service is responsible for + ...entityData + }; + } +} +``` + +### `@requires` Directive + +Federation also provides a [@requires](https://www.apollographql.com/docs/apollo-server/federation/entities/#extending-an-entity-with-computed-fields-advanced) field directive that a service can use to define `@external` fields of an extended entity from another service as necessary for resolving the _requiring_ field. This new directive also takes a `fields` argument, used to define the _required_ fields. + +The resolution of a entity extension field with a `@requires` directive waits on its required fields to be resolved from the services responsible for them. If the required fields are not selected in a given query that selects the requiring field, they will still be requested from the service that resolves them and provided in representations for resolving the requiring field. + +#### Resolving a `@requires` field + +When using `neo4jgraphql` to resolve a `@requires` field, generated translation uses any values resolved for its `fields` as additional selection keys for the entity defining the field. + +Let's take a look at the inventory service from the Federation demo. The `Product` entity defined by the products service is extended to provide additional fields when it is queried. + +##### _Inventory schema (demo)_ + +```graphql +extend type Product @key(fields: "upc") { + upc: String! @external + weight: Int @external + price: Int @external + inStock: Boolean + shippingEstimate: Int @requires(fields: "weight price") +} +``` + +In the demo, the execution of a [shippingEstimate](https://github.com/apollographql/federation-demo/blob/1a73e0f6f34b57ac4a555568034e83abe461d16e/services/inventory/index.js#L22) field resolver for the `Product` entity in the inventory service waits on the products service to resolve the required `@external` fields, `weight` and `price`, to be provided as a representation used in conditional logic. + +##### _Inventory resolver (demo)_ + +```js +Product: { + shippingEstimate(object) { + // free for expensive items + if (object.price > 1000) return 0; + // estimate is based on weight + return object.weight * 0.5; + } +} +``` + +### When `@cypher` `@requires` + +In this case, our example uses a `@cypher` directive in combination with a `@requires` directive in order to support translating it with custom Cypher after the required `Product` data has been resolved. Instead of using a field resolver, the above conditional logic can be expressed in Cypher using a `CALL` to the [apoc.when](https://markhneedham.com/blog/2019/07/31/neo4j-conditional-where-query-apoc/) Neo4j database [procedure](https://neo4j.com/docs/java-reference/current/extending-neo4j/procedures-and-functions/procedures/) from [APOC](https://neo4j.com/labs/apoc/) - a database plugin already used in supporting some other aspects of translation in `neo4j-graphql-js`. + +Now we can finally take a look at our example inventory schema. + +##### _Inventory schema_ + +```graphql +extend type Product @key(fields: "upc") { + upc: String! @external + weight: Int @external + price: Int @external + inStock: Boolean + shippingEstimate: Int + @requires(fields: "weight price") + @cypher( + statement: """ + CALL apoc.when($price > 900, + // free for expensive items + 'RETURN 0 AS value', + // estimate is based on weight + 'RETURN $weight * 0.5 AS value', + { + price: $price, + weight: $weight + }) + YIELD value + RETURN value.value + """ + ) +} +``` + +### `@provides` Directive + +As an optional optimization, the Federation [@provides](https://www.apollographql.com/docs/apollo-server/federation/entities/#resolving-another-services-field-advanced) directive can be used when both a service defining an entity and another service extending it can access the same data source to resolve its fields. This directive also takes a `fields` argument, used by a given service to define which fields of an extended entity it's responsible for resolving, given those fields could be resolved by either service. + +## Resources + +Here are some additional resources to learn more about Apollo Federation and using GraphQL with Neo4j. + +### Videos + +- [Introducing Apollo Federation](https://www.youtube.com/watch?v=WIeoBYRbprQ) + As part of [Apollo Day Seattle 2019](https://www.youtube.com/playlist?list=PLpi1lPB6opQznIY72BAmWtGm50D-WkYxv), this video overviews why Apollo developed Federation & Gateway as the evolution of [schema stitching](https://www.apollographql.com/docs/apollo-server/features/schema-stitching/) and demonstrates features by explaining the schema for the `accounts`, `products`, and `reviews` services of the [Federation demo](https://github.com/apollographql/federation-demo). +

+- [Your First Federated Schema with Apollo Server](https://youtu.be/v_1bn2sHdk4) + As part of the [Apollo Space Camp](https://www.eventbrite.com/e/apollo-space-camp-tickets-101566159116#) online event, part two of this video demonstrates using Gateway with services for `astronauts` and `missions`, and a [repository](https://github.com/mandiwise/space-camp-federation-demo) you can clone to try it out. + +### Articles + +- [Apollo Federation Introduction](https://www.apollographql.com/blog/apollo-federation-f260cf525d21) - An [Apollo Blog](https://www.apollographql.com/blog/) article introducing Federation by explaining the services used in the Federation demo. +

+- [Schema stitching guide](https://www.apollographql.com/docs/apollo-server/federation/migrating-from-stitching/) - Apollo provides a guide for migrating from schema stitching to using federated schemas. + +### Libraries + +- [@apollo/federation](https://www.npmjs.com/package/@apollo/federation) - This package provides utilities for creating GraphQL microservices, which can be combined into a single endpoint through tools like Apollo Gateway. +

+- [@apollo/gateway](https://www.npmjs.com/package/@apollo/gateway) - A GraphQL to Cypher query execution layer for Neo4j and JavaScript GraphQL implementations. +

+- [neo4j-graphql-js](https://www.npmjs.com/package/neo4j-graphql-js) - A GraphQL to Cypher query execution layer for Neo4j and JavaScript GraphQL implementations. +

+- [neo4j-driver](https://www.npmjs.com/package/neo4j-driver) - A database driver for Neo4j 3.0.0+. +

+- [Awesome Procedures On Cypher (APOC)](https://neo4j.com/labs/apoc/) - APOC is an add-on library for Neo4j that provides hundreds of procedures and functions adding a lot of useful functionality. diff --git a/docs/graphql-custom-logic.md b/docs/graphql-custom-logic.md new file mode 100644 index 00000000..55a41d64 --- /dev/null +++ b/docs/graphql-custom-logic.md @@ -0,0 +1,711 @@ +# Adding Custom Logic To Our GraphQL API + +Adding custom logic to our GraphQL API is necessary any time our application requires logic beyond simple CRUD operations (which are auto-generated by `makeAugmentedSchema`). + +There are two options for adding custom logic to your API using neo4j-graphql.js: + +1. Using the `@cypher` GraphQL schema directive to express your custom logic using Cypher, or +2. By implementing custom resolvers and attaching them to the GraphQL schema + +## The `@cypher` GraphQL Schema Directive + +We expose 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. The `@cypher` directive takes a single argument `statement` which is a Cypher statement. Parameters are passed into this query at runtime, including `this` which is the currently resolved node as well as any field-level arguments defined in the GraphQL type definition. + +> The `@cypher` directive feature used in the Query API requires the use of the APOC standard library plugin. Be sure you've followed the steps to install APOC in the Project Setup section of this chapter. + +### Computed Scalar Fields + +We can use the `@cypher` directive to define a custom scalar field, defining a computed field in our schema. Here we add an `averageStars` field to the `Business` type which calculates the average stars of all reviews for the business using the `this` variable. + +```graphql +type Business { + businessId: ID! + averageStars: Float! + @cypher( + statement: "MATCH (this)<-[:REVIEWS]-(r:Review) RETURN avg(r.stars)" + ) + name: String! + city: String! + state: String! + address: String! + location: Point! + reviews: [Review] @relation(name: "REVIEWS", direction: IN) + categories: [Category] @relation(name: "IN_CATEGORY", direction: OUT) +} +``` + +Now we can include the `averageStars` field in our GraphQL query: + +```graphql +{ + Business { + name + averageStars + } +} +``` + +And we see in the results that the computed value for `averageStars` is now included. + +```json +{ + "data": { + "Business": [ + { + "name": "Hanabi", + "averageStars": 5 + }, + { + "name": "Zootown Brew", + "averageStars": 5 + }, + { + "name": "Ninja Mike's", + "averageStars": 4.5 + } + ] + } +} +``` + +The generated Cypher query includes the annotated Cypher query as a sub-query, preserving the single database call to resolve the GraphQL request. + +### Computed Object And Array Fields + +We can also use the `@cypher` schema directive to resolve object and array fields. Let's add a recommended business field to the `Business` type. We'll use a simple Cypher query to find common businesses that other users reviewed. For example, if a user likes "Market on Front", we could recommend other businesses that users who reviewed "Market on Front" also reviewed. + +```cypher +MATCH (b:Business {name: "Market on Front"})<-[:REVIEWS]-(:Review)<-[:WROTE]-(:User)-[:WROTE]->(:Review)-[:REVIEWS]->(rec:Business) +WITH rec, COUNT(*) AS score +RETURN rec ORDER BY score DESC +``` + +We can make use of this Cypher query in our GraphQL schema by including it in a `@cypher` directive on the `recommended` field in our `Business` type definition. + +```graphql +type Business { + businessId: ID! + averageStars: Float! + @cypher( + statement: "MATCH (this)<-[:REVIEWS]-(r:Review) RETURN avg(r.stars)" + ) + recommended(first: Int = 1): [Business] + @cypher( + statement: """ + MATCH (this)<-[:REVIEWS]-(:Review)<-[:WROTE]-(:User)-[:WROTE]->(:Review)-[:REVIEWS]->(rec:Business) + WITH rec, COUNT(*) AS score + RETURN rec ORDER BY score DESC LIMIT $first + """ + ) + name: String! + city: String! + state: String! + address: String! + location: Point! + reviews: [Review] @relation(name: "REVIEWS", direction: IN) + categories: [Category] @relation(name: "IN_CATEGORY", direction: OUT) +} +``` + +We also define a `first` field argument, which is passed to the Cypher query included in the `@cypher` directive and acts as a limit on the number of recommended businesses returned. + +### Custom Top-Level Query Fields + +Another helpful way to use the `@cypher` directive is as a custom query or mutation field. For example, let's see how we can add full-text query support to search for businesses. Applications often use full-text search to correct for things like misspellings in user input using fuzzy matching. + +In Neo4j we can use full-text search by first creating a full-text index. + +```cypher +CALL db.index.fulltext.createNodeIndex("businessNameIndex", ["Business"],["name"]) +``` + +Then to query the index, in this case we misspell "coffee" but including the `~` character enables fuzzy matching, ensuring we still find what we're looking for. + +```cypher +CALL db.index.fulltext.queryNodes("businessNameIndex", "cofee~") +``` + +Wouldn't it be nice to include this fuzzy matching full-text search in our GraphQL API? To do that let's create a Query field called `fuzzyBusinessByName` that takes a search string and searches for businesses. + +```graphql +type Query { + fuzzyBusinessByName(searchString: String): [Business] + @cypher( + statement: """ + CALL db.index.fulltext.queryNodes( 'businessNameIndex', $searchString+'~') + YIELD node RETURN node; + """ + ) +} +``` + +We can now search for business names using this fuzzy matching. + +```graphql +{ + fuzzyBusinessByName(searchString: "libary") { + name + } +} +``` + +Since we are using full-text search, even though we spell "library" incorrectly, we still find matching results. + +```json +{ + "data": { + "fuzzyBusinessByName": [ + { + "name": "Missoula Public Library" + } + ] + } +} +``` + +The `@cypher` schema directive is a powerful way to add custom logic and advanced functionality to our GraphQL API. We can also use the `@cypher` directive for authorization features, accessing values such as authorization tokens from the request object, a pattern that is discussed in [the GraphQL authorization page.](neo4j-graphql-js-middleware-authorization.mdx#cypher-parameters-from-context) + +## Custom Nested Mutations + +Nested mutations can be used by adding input object type arguments when overwriting generated node mutations or when using a custom mutation with a `@cypher` directive on a `Mutation` type field. The `@cypher` directive can be used on the fields of nested input object arguments to provide Cypher statements to execute after generated translation. + +> This feature requires a Neo4j database version that supports Cypher 4.1 [correlated subqueries](https://neo4j.com/docs/cypher-manual/current/clauses/call-subquery/). + +Consider the below example schema: + +- The `MergeA` mutation generated by `makeAugmentedSchema` for the `A` node type is overwritten. +- The `CustomMergeA` defines a `@cypher` mutation that provides custom logic for merging a single `A` type node. +- The `BatchMergeA` defines a `@cypher` mutation that provides custom logic for merging many `A` type nodes. + +```graphql +type Mutation { + MergeA(id: ID!, b: [ABMutation!]): A! + CustomMergeA(data: AInput!): A! + @cypher( + statement: """ + MERGE (a: A { + id: $data.id + }) + RETURN a + """ + ) + BatchMergeA(data: [AInput!]!): [A!]! + @cypher( + statement: """ + UNWIND $data AS AInput + MERGE (a: A { + id: AInput.id + }) + RETURN a + """ + ) +} + +type A { + id: ID! @id + b: [B] @relation(name: "AB", direction: OUT) +} + +input AInput { + id: ID! + b: ABMutation +} + +input ABMutation { + merge: [BInput] + @cypher( + statement: """ + WITH a + MERGE (b: B { + id: BInput.id + }) + MERGE (a)-[:AB]->(b) + WITH b + """ + ) +} + +type B { + id: ID! @id + c: [C] @relation(name: "BC", direction: OUT) +} + +input BInput { + id: ID! + c: BCMutation +} + +input BCMutation { + merge: [CInput] + @cypher( + statement: """ + MERGE (c: C { + id: CInput.id + }) + MERGE (b)-[:BC]->(c) + """ + ) +} + +type C { + id: ID! @id + a: [A] @relation(name: "CA", direction: OUT) +} + +input CInput { + id: ID! + a: CAMutation +} + +input CAMutation { + merge: [AInput] + @cypher( + statement: """ + MERGE (a: A { + id: AInput.id + }) + MERGE (c)-[:CA]->(a) + """ + ) +} +``` + +### Generated Mutations + +The [generated api](https://grandstack.io/docs/graphql-schema-generation-augmentation#generated-mutations) for `Create`, `Merge`, `Update`, and `Delete` node mutation fields can be overwritten to customize their arguments. If a `@cypher` directive is not used when authoring the mutation field yourself, then a generated translation is still used. When argument values are provided for nested `@cypher` input fields, their Cypher statements are executed after the generated translation. This also works when authoring your own `data` arguments in the format of the [experimental](https://grandstack.io/docs/graphql-schema-generation-augmentation#experimental-api) node mutation API. + +The `MergeA` mutation field first has an argument for the expected primary key in order to match the generated format. A list argument named `b` is then added to make it possible to provide an argument value for the nested `@cypher` field named `merge` on the `ABMutation` input object. + +This pattern continues with further nested input objects in the below example mutation: + +```graphql +MergeA(id: ID!, b: [ABMutation!]): A! +``` + +```graphql +mutation { + MergeA( + id: "a" + b: [ + { + merge: { id: "b", c: { merge: { id: "c", a: { merge: { id: "a" } } } } } + } + ] + ) { + id + b { + id + c { + id + a { + id + } + } + } + } +} +``` + +#### Importing Variables + +Explicitly declaring which [Cypher variables](https://neo4j.com/docs/cypher-manual/current/syntax/variables/index.html) to continue with helps prevent naming conflicts when moving from one nested `@cypher` statement to the next. Variables can be imported into or exported out of nested `@cypher` statements using the Cypher [WITH](https://neo4j.com/docs/cypher-manual/current/clauses/with/) clause. + +When using a `WITH` clause to import variables into a nested `@cypher` statement, any variables not declared can be reused. If no clause is provided, all variables in scope are imported by default. + +In the statement for the `merge` field on the `ABMutation` input object, the `a` variable is explicitly imported. This excludes `b` from its variable scope to prevent a naming conflict between the existing `b` argument on the `MergeA` mutation and naming a new variable `b` when merging `B` type nodes. + +Without the `WITH a` clause, the mutation would fail with the Cypher error: `"Variable b already declared"`. + +```graphql +input ABMutation { + merge: [BInput] + @cypher( + statement: """ + WITH a + MERGE (b: B { + id: BInput.id + }) + MERGE (a)-[:AB]->(b) + WITH b + """ + ) +} +``` + +#### Data Variables + +Input object fields using the `@cypher` directive are supported by generated [UNWIND](https://neo4j.com/docs/cypher-manual/current/clauses/unwind/) clauses within nested subqueries. Because of this, a Cypher variable matching the name of the input object is always available. In the above Cypher statement for the `merge` field on the `ABMutation` input object, a generated `UNWIND` clause declares the `BInput` variable for accessing parameter data provided to the `merge` argument. + +#### Exporting Variables + +When exporting variables out of a nested `@cypher` statement, any variables not exported can be reused in proceeding nested `@cypher` statements. Similar to importing variables, if no exporting `WITH` clause is provided, all variables in scope are exported. + +Proceeding with the nested `@cypher` fields, the `BCMutation` input object imports and exports all variables in scope: + +```graphql +input BCMutation { + merge: [CInput] + @cypher( + statement: """ + MERGE (c: C { + id: CInput.id + }) + MERGE (b)-[:BC]->(c) + """ + ) +} +``` + +But in the case of the proceeding `CAMutation` input object, the mutation would fail with the Cypher error `"Variable a already declared"` without the `WITH b` clause exporting only `b`. By default, both the `a` and `b` variables would be exported, but the existing `a` node variable set by the generated translation would conflict with naming a new variable `a` when merging `A` type nodes: + +```graphql +input CAMutation { + merge: [AInput] + @cypher( + statement: """ + MERGE (a: A { + id: AInput.id + }) + MERGE (c)-[:CA]->(a) + """ + ) +} +``` + +With no variable naming conflicts, the successful execution of the `MergeA` mutation results in merging and relating an `A` node with a `B` node, the `B` node with a `C` node, and the `C` node with the initially merged `A` node, resulting in the below graph: + +![MergeA Graph Data](img/MergeAGraphData.png) + +
+ Cypher Translation + +```js +// Generated translation of MergeA +MERGE (`a`:`A`{id: $params.id}) +// Continues with all variables in scope +WITH * +CALL { + WITH * + // Generated UNWIND clauses to progressively unwind + // nested @cypher argument parameters + UNWIND $params.b AS _b + UNWIND _b.merge as BInput + // Begin: ABMutation.merge @cypher + // Augmented importing WITH clause to persist + // unwound parameter, iff clause provided + WITH BInput, a + MERGE (b: B { + id: BInput.id + }) + MERGE (a)-[:AB]->(b) + // Augmented exporting WITH clause with parameter alias + // to allow for input type reuse + WITH BInput AS _BInput, b + // End: ABMutation.merge @cypher + CALL { + WITH * + UNWIND _BInput.c.merge AS CInput + MERGE (c: C { + id: CInput.id + }) + MERGE (b)-[:BC]->(c) + WITH *, CInput AS _CInput + CALL { + WITH * + UNWIND _CInput.a.merge AS AInput + MERGE (a: A { + id: AInput.id + }) + MERGE (c)-[:CA]->(a) + RETURN COUNT(*) AS _a_merge_ + } + RETURN COUNT(*) AS _c_merge_ + } + // Generated closure of variable scope for + // RETURN clause required by subqueries + RETURN COUNT(*) AS _b_merge_ +} +// Generated translation of selection set +RETURN `a` { + .id, + b: [(`a`)-[:`AB`]->(`a_b`:`B`) | `a_b` { + .id, + c: [(`a_b`)-[:`BC`]->(`a_b_c`:`C`) | `a_b_c` { + .id, + a: [(`a_b_c`)-[:`CA`]->(`a_b_c_a`:`A`) | `a_b_c_a` { + .id + }] + }] + }] +} AS `a` +``` + +
+ +
+ Data + +```json +{ + "data": { + "MergeA": { + "id": "a", + "b": [ + { + "id": "b", + "c": [ + { + "id": "c", + "a": [ + { + "id": "a" + } + ] + } + ] + } + ] + } + } +} +``` + +
+ +### Custom Mutations + +Similar to overwriting generated node mutations and adding custom arguments, nested input objects with `@cypher` fields can be used to provide additional operations to execute after the `@cypher` statement of a custom mutation: + +```graphql +CustomMergeA(data: AInput!): A! @cypher(statement: """ + MERGE (a: A { + id: $data.id + }) + RETURN a +""") +``` + +```graphql +mutation { + CustomMergeA( + data: { + id: "a" + b: { + merge: { id: "b", c: { merge: { id: "c", a: { merge: { id: "a" } } } } } + } + } + ) { + id + b { + id + c { + id + a { + id + } + } + } + } +} +``` + +![MergeA Graph Data](img/MergeAGraphData.png) + +#### Custom Batch Mutations + +If a custom mutation uses an [UNWIND](https://neo4j.com/docs/cypher-manual/current/clauses/unwind/) clause on a list argument of input objects, then the Cypher variable must match the type name of the argument for its nested `@cypher` fields to process in the same iterative scope. List arguments of input objects are otherwise handled by generated `UNWIND` clauses, processing independently. In the below example, the `data` list argument of type `AInput` is unwound to a variable named `AInput`, following the naming convention of the variable set by generated `UNWIND` clauses: + +```graphql +BatchMergeA(data: [AInput!]!): [A!]! @cypher(statement: """ + UNWIND $data AS AInput + MERGE (a: A { + id: AInput.id + }) + RETURN a +""") +``` + +```graphql +mutation { + BatchMergeA( + data: [ + { + id: "a" + b: { + merge: [ + { + id: "b" + c: { merge: [{ id: "c", a: { merge: [{ id: "a" }] } }] } + } + ] + } + } + { + id: "x" + b: { + merge: [ + { + id: "y" + c: { + merge: [{ id: "z", a: { merge: [{ id: "x" }, { id: "a" }] } }] + } + } + ] + } + } + ] + ) { + id + b { + id + c { + id + a { + id + } + } + } + } +} +``` + +![BatchMergeA Graph Data](img/BatchMergeAGraphData.png) + +
+ Data + +```json +{ + "data": { + "BatchMergeA": [ + { + "id": "a", + "b": [ + { + "id": "b", + "c": [ + { + "id": "c", + "a": [ + { + "id": "a" + } + ] + } + ] + } + ] + }, + { + "id": "x", + "b": [ + { + "id": "y", + "c": [ + { + "id": "z", + "a": [ + { + "id": "a" + }, + { + "id": "x" + } + ] + } + ] + } + ] + } + ] + } +} +``` + +
+ +## Implementing Custom Resolvers + +While the `@cypher` directive is one way to add custom logic, in some cases we may need to implement custom resolvers that implement logic not able to be expressed in Cypher. For example, we may need to fetch data from another system, or apply some custom validation rules. In these cases we can implement a custom resolver and attach it to the GraphQL schema so that resolver is called to resolve our custom field instead of relying on the generated Cypher query by neo4j-graphql.js to resolve the field. + +In our example let's imagine there is an external system that can be used to determine current wait times at businesses. We want to add an additional `waitTime` field to the `Business` type in our schema and implement the resolver logic for this field to use this external system. + +To do this, we first add the field to our schema, adding the `@neo4j_ignore` directive to ensure the field is excluded from the generated Cypher query. This is our way of telling neo4j-graphql.js that a custom resolver will be responsible for resolving this field and we don't expect it to be fetched from the database automatically. + +```graphql +type Business { + businessId: ID! + waitTime: Int! @neo4j_ignore + averageStars: Float! + @cypher( + statement: "MATCH (this)<-[:REVIEWS]-(r:Review) RETURN avg(r.stars)" + ) + name: String! + city: String! + state: String! + address: String! + location: Point! + reviews: [Review] @relation(name: "REVIEWS", direction: IN) + categories: [Category] @relation(name: "IN_CATEGORY", direction: OUT) +} +``` + +Next we create a resolver map with our custom resolver. We didn't have to create this previously because neo4j-graphql.js generated our resolvers for us. Our wait time calculation will be just selecting a value at random, but we could implement any custom logic here to determine the `waitTime` value, such as making a request to a 3rd party API. + +```js +const resolvers = { + Business: { + waitTime: (obj, args, context, info) => { + const options = [0, 5, 10, 15, 30, 45]; + return options[Math.floor(Math.random() * options.length)]; + } + } +}; +``` + +Then we add this resolver map to the parameters passed to `makeAugmentedSchema`. + +```js +const schema = makeAugmentedSchema({ + typeDefs, + resolvers +}); +``` + +Now, let's search for restaurants and see what their wait times are by including the `waitTime` field in the selection set. + +```graphql +{ + Business(filter: { categories_some: { name: "Restaurant" } }) { + name + waitTime + } +} +``` + +In the results we now see a value for the wait time. Your results will of course vary since the value is randomized. + +```json +{ + "data": { + "Business": [ + { + "name": "Ninja Mike's", + "waitTime": 5 + }, + { + "name": "Market on Front", + "waitTime": 45 + }, + { + "name": "Hanabi", + "waitTime": 45 + } + ] + } +} +``` + +## Resources + +- [Using Neo4j’s Full-Text Search With GraphQL](https://blog.grandstack.io/using-neo4js-full-text-search-with-graphql-e3fa484de2ea) Defining Custom Query Fields Using The Cypher GraphQL Schema Directive diff --git a/docs/graphql-filtering.md b/docs/graphql-filtering.md new file mode 100644 index 00000000..c19ee435 --- /dev/null +++ b/docs/graphql-filtering.md @@ -0,0 +1,246 @@ +# Complex GraphQL Filtering + +A `filter` argument is added to field arguments, as well as input types used to support them. + +> Filtering is currently supported for scalar fields, enums, `@relation` fields and types. Filtering on `@cypher` directive fields is not yet supported. + +## `filter` Argument + +The auto-generated `filter` argument is used to support complex filtering in queries. For example, to filter for Movies released before 1920: + +```graphql +{ + Movie(filter: { year_lt: 1920 }) { + title + year + } +} +``` + +## Nested Filter + +To filter based on the results of nested fields applied to the root, simply nest the filters used. For example, to search for movies whose title starts with "River" and has at least one actor whose name starts with "Brad": + +```graphql +{ + Movie( + filter: { + title_starts_with: "River" + actors_some: { name_contains: "Brad" } + } + ) { + title + } +} +``` + +## Logical Operators: `AND`, `OR` + +Filters can be wrapped in logical operations `AND` and `OR`. For example, to find movies that were released before 1920 or have a title that contains "River Runs": + +```graphql +{ + Movie(filter: { OR: [{ year_lt: 1920 }, { title_contains: "River Runs" }] }) { + title + year + } +} +``` + +These logical operators can be nested as well. For example, find movies that there were released before 1920 or have a title that contains "River" and belong to the genre "Drama": + +```graphql +{ + Movie( + filter: { + OR: [ + { year_lt: 1920 } + { + AND: [{ title_contains: "River" }, { genres_some: { name: "Drama" } }] + } + ] + } + ) { + title + year + genres { + name + } + } +} +``` + +## Regular Expressions: `regexp` + +The Cypher [regular expression](https://neo4j.com/docs/cypher-manual/current/clauses/where/#query-where-regex) filter is generated for `ID` and `String` type fields. The value provided should follow the [Java syntax](https://docs.oracle.com/javase/7/docs/api/java/util/regex/Pattern.html) for regular expressions. + +For example, to find movies where the `title` property has the value `river` (case-insensitive): + +```graphql +{ + Movie(filter: { title_regexp: "(?i)river.*" }) { + title + year + } +} +``` + +## Filtering In Selections + +Filters can be used in not only the root query argument, but also throughout the selection set. For example, search for all movies that contain the string "River", and when returning the genres of the these movies only return genres with the name "Drama": + +```graphql +{ + Movie(filter: { title_contains: "River" }) { + title + genres(filter: { name: "Drama" }) { + name + } + } +} +``` + +## DateTime Filtering + +Filtering can be applied to GraphQL temporal fields, using the temporal input types described in the [Temporal Types (DateTime) section](graphql-temporal-types-datetime.mdx#using-temporal-fields-in-mutations). For example, filter for reviews created before January 1, 2015: + +```graphql +{ + User(first: 1) { + rated(filter: { created_lt: { year: 2015, month: 1, day: 1 } }) { + rating + created { + formatted + } + } + } +} +``` + +## Spatial Filtering + +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? + +For points using the Geographic coordinate reference system (latitude and longitude) `distance` is measured in meters. + +## Filter Criteria + +The filter criteria available depends on the type of the field and are added to the generated input type prefixed by the name of the field and suffixed with the criteria. For example, given the following type definitions: + +```graphql +enum RATING { + G + PG + PG13 + R +} + +type Movie { + movieId: ID! + title: String + year: Int + rating: RATING + available: Boolean + actors: [Actor] @relation(name: "ACTED_IN", direction: IN) + reviews: [UserReview] +} + +type Actor { + name: String +} + +type User { + name: String + rated: UserReview +} + +type UserReview @relation(name: "RATED") { + from: User + to: Movie + rating: Float + createdAt: DateTime +} + +type Business { + id: ID! + name: String + location: Point +} +``` + +the following filtering criteria is available, through the generated `filter` input type. + +_This table shows the fields available on the generated `filter` input type, and a brief explanation of each filter criteria._ + +| | Field | Type | Explanation | +| ----------------------- | ----------------------- | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Logical operators** | | | | +| | `AND` | `[_MovieFilter]` | Use to apply logical AND to a list of filters, typically used when nested with OR operators | +| | `OR` | `[_MovieFilter]` | Use to apply logical OR to a list of filters. | +| **ID fields** | | | | +| | `movieId` | `ID` | Matches nodes when value is an exact match | +| | `movieId_not` | `ID` | Matches nodes when value is not an exact match | +| | `movieId_in` | `[ID!]` | Matches nodes based on equality of at least one value in list of values | +| | `movieId_not_in` | `[ID!]` | Matches nodes based on inequality of all values in list of values | +| | `movieId_regexp` | `String` | Matches nodes given provided [regular expression](https://neo4j.com/docs/cypher-manual/current/clauses/where/#query-where-regex) | +| **String fields** | | | | +| | `title` | `String` | Matches nodes based on equality of value | +| | `title_not` | `String` | Matches nodes based on inequality of value | +| | `title_in` | `[String!]` | Matches nodes based on equality of at least one value in list | +| | `title_not_in` | `[String!]` | Matches nodes based on inequality of all values in list | +| | `title_regexp` | `String` | Matches nodes given provided [regular expression](https://neo4j.com/docs/cypher-manual/current/clauses/where/#query-where-regex) | +| | `title_contains` | `String` | Matches nodes when value contains given substring | +| | `title_not_contains` | `String` | Matches nodes when value does not contain given substring | +| | `title_starts_with` | `String` | Matches nodes when value starts with given substring | +| | `title_not_starts_with` | `String` | Matches nodes when value does not start with given substring | +| | `title_ends_with` | `String` | Matches nodes when value ends with given substring | +| | `title_not_ends_with` | `String` | Matches nodes when value does not end with given substring | +| **Numeric fields** | | | _Similar behavior for float fields_ | +| | `year` | `Int` | Matches nodes when value is an exact match | +| | `year_not` | `Int` | Matches nodes based on inequality of value | +| | `year_in` | `[Int!]` | Matches nodes based on equality of at least one value in list | +| | `year_not_in` | `[Int!]` | Matches nodes based on inequality of all values in list | +| | `year_lt` | `Int` | Matches nodes when value is less than given integer | +| | `year_lte` | `Int` | Matches nodes when value is less than or equal to given integer | +| | `year_gt` | `Int` | Matches nodes when value is greater than given integer | +| | `year_gte` | `Int` | Matches nodes when value is greater than or equal to given integer | +| **Enum fields** | | | | +| | `rating` | `RATING_ENUM` | Matches nodes based on enum value | +| | `rating_not` | `RATING_ENUM` | Matches nodes based on inequality of enum value | +| | `rating_in` | `[RATING_ENUM!]` | Matches nodes based on equality of at least one enum value in list | +| | `rating_not_in` | `[RATING_ENUM!]` | Matches nodes based on inequality of all values in list | +| **Boolean fields** | | | +| | `available` | `Boolean` | Matches nodes based on value | +| | `available_not` | `Boolean` | Matches nodes base on inequality of value | +| **Relationship fields** | | | _Use a relationship field filter to apply a nested filter to matches at the root level_ | +| | `actors` | `_ActorFilter` | Matches nodes based on a filter of the related node | +| | `actors_not` | `_ActorFilter` | Matches nodes when a filter of the related node is not a match | +| | `actors_in` | `[_ActorFilter!]` | Matches nodes when the filter matches at least one of the related nodes | +| | `actors_not_in` | `[_ActorFilter!]` | Matches nodes when the filter matches none of the related nodes | +| | `actors_some` | `_ActorFilter` | Matches nodes when at least one of the related nodes is a match | +| | `actors_none` | `_ActorFilter` | Matches nodes when none of the related nodes are a match | +| | `actors_single` | `_ActorFilter` | Matches nodes when exactly one of the related nodes is a match | +| | `actors_every` | `_ActorFilter` | Matches nodes when all related nodes are a match | +| **Temporal fields** | | | _Temporal filters use the temporal inputs described in the [Temporal Types (DateTime) section](graphql-temporal-types-datetime.mdx#using-temporal-fields-in-mutations)_ | +| | `createdAt` | `_Neo4jDateTimeInput` | Matches when date is an exact match | +| | `createdAt_not` | `_Neo4jDateTimeInput` | Matches based on inequality of value | +| | `createdAt_in` | `[_Neo4jDateTimeInput]` | Matches based on equality of at least one value in list | +| | `createdAt_not_in` | `[_Neo4jDateTimeInput]` | Matches based on inequality of all values in list | +| | `createdAt_lt` | `_Neo4jDateTimeInput` | Matches when value is less than given DateTime | +| | `createdAt_lte` | `_Neo4jDateTimeInput` | Matches when value is less than or equal to given DateTime | +| | `createdAt_gt` | `_Neo4jDateTimeInput` | Matches when value is greater than given DateTime | +| | `cratedAt_gte` | `_Neo4jDateTimeInput` | Matches when value is greater than or equal to given DateTime | +| **Spatial fields** | | | _Spatial filers use the point inputs described in the [Spatial Types section](graphql-spatial-types.mdx)_ | +| | `location` | `_Neo4jPointInput` | Matches point property exactly | +| | `location_not` | `_Neo4jPointInput` | Matches based on inequality of point values | +| | `location_distance` | `_Neo4jPointDistanceFilter` | Matches based on computed distance of location to provided point | +| | `location_distance_lt` | `_Neo4jPointDistanceFilter` | Matches when computed distance of location to provided point is less than distance specified | +| | `location_distance_lte` | `_Neo4jPointDistanceFilter` | Matches when computed distance of location to provided point is less than or equal to distance specified | +| | `location_distance_gt` | `_Neo4jPointDistanceFilter` | Matches when computed distance of location to provided point is greater than distance specified | +| | `location_distance_gte` | `_Neo4jPointDistanceFilter` | Matches when computed distance of location to provided point is greater than or equal to distance specified | + +See the [filtering tests](https://github.com/neo4j-graphql/neo4j-graphql-js/blob/master/test/helpers/tck/filterTck.md) for more examples of the use of filters. + +## Resources + +- Blog post: [Complex GraphQL Filtering With neo4j-graphql.js](https://blog.grandstack.io/complex-graphql-filtering-with-neo4j-graphql-js-aef19ad06c3e) - Use filtering in your GraphQL queries without writing any resolvers diff --git a/docs/graphql-interface-union-types.md b/docs/graphql-interface-union-types.md new file mode 100644 index 00000000..ee3c1e43 --- /dev/null +++ b/docs/graphql-interface-union-types.md @@ -0,0 +1,444 @@ +# Using GraphQL Interface And Union Types + +## Overview + +This page describes how interface and union types can be used with neo4j-graphql.js. + +GraphQL supports two kinds of abstract types: interfaces and unions. Interfaces are abstract types that include a set of fields that all implementing types must include. A union type indicates that a field can return one of several object types, but doesn't specify any fields that must be included in the implementing types of the union. See the GraphQL documentation to learn more about [interface](https://graphql.org/learn/schema/#interfaces) and [union](https://graphql.org/learn/schema/#union-types) types. + +## Interface Types + +Interface types are supported in neo4j-graphql.js through the use of multiple labels in Neo4j. For example, consider the following GraphQL type definitions: + +```graphql +interface Person { + id: ID! + name: String +} + +type User implements Person { + id: ID! + name: String + screenName: String + reviews: [Review] @relation(name: "WROTE", direction: OUT) +} + +type Actor implements Person { + id: ID! + name: String + movies: [Movie] @relation(name: "ACTED_IN", direction: OUT) +} + +type Movie { + movieId: ID! + title: String +} + +type Review { + rating: Int + created: DateTime + movie: Movie @relation(name: "REVIEWS", direction: OUT) +} +``` + +The above GraphQL type definitions would define the following property graph model using neo4j-graphql.js: + +![Property graph model](img/interface-model.png) + + + +Note that the label `Person` (which represents the interface type) is added to each node of a type implementing the `Person` interface (`User` and `Actor`), + +### Interface Mutations + +When an interface type is included in the GraphQL type definitions, the generated create mutations will add the additional label for the interface type to any nodes of an implementing type when creating data. For example consider the following mutations. + +```graphql +mutation { + u1: CreateUser(name: "Bob", screenName: "bobbyTables", id: "u1") { + id + } + a1: CreateActor(name: "Brad Pitt", id: "a1") { + id + } + m1: CreateMovie(title: "River Runs Through It, A", movieId: "m1") { + movieId + } + am1: AddActorMovies(from: { id: "a1" }, to: { movieId: "m1" }) { + from { + id + } + } +} +``` + +This creates the following graph in Neo4j (note the use of multiple labels): + +![Simple movie graph with interface](img/interface-data.png) + + + +### Interface Queries + +#### Query field + +A query field is added to the generated `Query` type for each interface. For example, querying using our `Person` interface. + +```graphql +query { + Person { + name + } +} +``` + +```json +{ + "data": { + "Person": [ + { + "name": "Bob" + }, + { + "name": "Brad Pitt" + } + ] + } +} +``` + +#### \_\_typename introspection field + +The `__typename` introspection field can be added to the selection set to determine the concrete type of the object. + +```graphql +query { + Person { + name + __typename + } +} +``` + +```json +{ + "data": { + "Person": [ + { + "name": "Bob", + "__typename": "User" + }, + { + "name": "Brad Pitt", + "__typename": "Actor" + } + ] + } +} +``` + +#### Inline fragments + +[Inline fragments](https://graphql.org/learn/queries/#inline-fragments) can be used to access fields of the concrete types in the selection set. + +```graphql +query { + Person { + name + __typename + ... on Actor { + movies { + title + } + } + + ... on User { + screenName + } + } +} +``` + +```json +{ + "data": { + "Person": [ + { + "name": "Bob", + "__typename": "User", + "screenName": "bobbyTables" + }, + { + "name": "Brad Pitt", + "__typename": "Actor", + "movies": [ + { + "title": "River Runs Through It, A" + } + ] + } + ] + } +} +``` + +#### Filtering With Interfaces + +The generated filter arguments can be used for interface types. Note however that only fields in the interface definition are included in the generated filter arguments as those apply to all concrete types. + +```graphql +query { + Person(filter: { name_contains: "Brad" }) { + name + __typename + ... on Actor { + movies { + title + } + } + + ... on User { + screenName + } + } +} +``` + +```json +{ + "data": { + "Person": [ + { + "name": "Brad Pitt", + "__typename": "Actor", + "movies": [ + { + "title": "River Runs Through It, A" + } + ] + } + ] + } +} +``` + +#### Interface Relationship Fields + +We can also use interfaces when defining relationship fields. For example: + +```graphql + friends: [Person] @relation(name: "FRIEND_OF", direction: OUT) + +``` + +## Union Types + +> Note that using union types for relationship types is not yet supported by neo4j-graphql.js. Unions can however be used on relationship fields. + +Union types are abstract types that do not specify any fields that must be included in the implementing types of the union, therefore it cannot be assumed that the concrete types of a union include any overlapping fields. Similar to interface types, in neo4j-graphql.js an additional label is added to nodes to represent the union type. + +For example, consider the following GraphQL type definitions: + +```graphql +union SearchResult = Blog | Movie + +type Blog { + blogId: ID! + created: DateTime + content: String +} + +type Movie { + movieId: ID! + title: String +} +``` + +### Union Mutations + +Using the generated mutations to create the following data: + +```graphql +mutation { + b1: CreateBlog( + blogId: "b1" + created: { year: 2020, month: 3, day: 7 } + content: "Neo4j GraphQL is the best!" + ) { + blogId + } + m1: CreateMovie(movieId: "m1", title: "River Runs Through It, A") { + movieId + } +} +``` + +The above mutations create the following data in Neo4j. Note the use of multiple node labels. + +![Union data in Neo4j](img/union-data.png) + + + +### Union Queries + +#### Query Field + +A query field is added to the Query type for each union type defined in the schema. + +```graphql +{ + SearchResult { + __typename + } +} +``` + +```json +{ + "data": { + "SearchResult": [ + { + "__typename": "Blog" + }, + { + "__typename": "Movie" + } + ] + } +} +``` + +#### Inline Fragments + +Inline fragments are used in the selection set to access fields of the concrete type. + +```graphql +{ + SearchResult { + __typename + ... on Blog { + created { + formatted + } + content + } + + ... on Movie { + title + } + } +} +``` + +```json +{ + "data": { + "SearchResult": [ + { + "__typename": "Blog", + "created": { + "formatted": "2020-03-07T00:00:00Z" + }, + "content": "Neo4j GraphQL is the bedst!" + }, + { + "__typename": "Movie", + "title": "River Runs Through It, A" + } + ] + } +} +``` + +#### Using With @cypher Directive Query Fields + +We can also use unions with `@cypher` directive fields. Unions are often useful in the context of search results, where the result object might be one of several types. In order to support this usecase full text indexes can be used to search across multiple node labels and properties. + +First, let's create a full text index in Neo4j. This index will include the `:Blog(content)` and `:Movie(title)` properties. + +```cypher +CALL db.index.fulltext.createNodeIndex("searchIndex", ["Blog","Movie"],["content", "title"]) +``` + +Now we can add a `search` field to the Query type that searches the full text index. + +```graphql +type Query { + search(searchString: String!): [SearchResult] @cypher(statement:"CALL db.index.fulltext.queryNodes("searchIndex", $searchString) YIELD node RETURN node") +} +``` + +Now we can query the `search` field, leveraging the full text index. + +```graphql +{ + search(searchString: "river") { + __typename + ... on Movie { + title + } + ... on Blog { + created { + formatted + } + content + } + } +} +``` + +```json +{ + "data": { + "search": [ + { + "__typename": "Movie", + "title": "River Runs Through It, A" + } + ] + } +} +``` + +## Resources + +- [Using Neo4j’s Full-Text Search With GraphQL](https://blog.grandstack.io/using-neo4js-full-text-search-with-graphql-e3fa484de2ea) -- Defining Custom Query Fields Using The Cypher GraphQL Schema Directive diff --git a/docs/graphql-relationship-types.md b/docs/graphql-relationship-types.md new file mode 100644 index 00000000..37ea0a68 --- /dev/null +++ b/docs/graphql-relationship-types.md @@ -0,0 +1,126 @@ +# GraphQL Relationship Types + +## Defining relationships in SDL + +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: + +```graphql +type Movie { + title: String + year: Int + genres: [Genre] @relation(name: "IN_GENRE", direction: OUT) +} + +type Genre { + name: String + movies: [Movie] @relation(name: "IN_GENRE", direction: IN) +} +``` + +### Querying Relationship Fields + +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: + +
+ +
+ +## Relationships with properties + +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: + +```graphql +type Movie { + title: String + year: Int + ratings: [Rated] +} + +type User { + userId: ID + name: String + rated: [Rated] +} + +type Rated @relation(name: "RATED") { + from: User + to: Movie + rating: Float + created: DateTime +} +``` + +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. + +### Querying Relationship Types + +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: + +```graphql +type _MovieRatings { + created: _Neo4jDateTime + rating: Float + User: User +} +``` + +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. + +
+ +
+ +## Field names for related nodes + +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. + +```graphql +type Rated @relation(name: "RATED", from: "user", to: "movie") { + user: User + movie: Movie + rating: Float + created: DateTime +} +``` + +## Default relationship name + +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. + +```graphql +type UserRated + @relation(from: "user", to: "movie") { # name: "USER_RATED" + user: User + movie: Movie + rating: Float + created: DateTime +} +``` + +## Relationship mutations + +See the [generated mutations](graphql-schema-generation-augmentation.mdx#generated-mutations) section for information on the mutations generated for relationship types. diff --git a/docs/graphql-schema-directives.md b/docs/graphql-schema-directives.md new file mode 100644 index 00000000..cd8e785d --- /dev/null +++ b/docs/graphql-schema-directives.md @@ -0,0 +1,31 @@ +# GraphQL Schema Directives + +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. + +## What Are GraphQL Schema Directives + +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. + +In neo4j-graphql.js we use schema directives to: + +- help describe our data model (`@relation`, `@id`, `@unique`, `@index`) +- implement custom logic in our GraphQL service (`@cypher`, `@neo4j_ignore`) +- help implement authorization logic (`@additionalLabel`, `@isAuthenticated`, `@hasRole`, `@hasScope`) + +## Neo4j GraphQL Schema Directives + +The following GraphQL schema directives are declared during the schema augmentation process and can be used in the type definitions passed to `makeAugmentedSchema`. + +| Directive | Arguments | Description | Notes | +| ------------------- | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `@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) | +| `@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) | +| `@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) | +| `@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) | +| `@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) | +| `@search` | `index` | Used on fields to set full-text search indexes. | See [API Reference](https://grandstack.io/docs/neo4j-graphql-js-api#searchschemaoptionsnull) | +| `@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) | +| `@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) | +| `@isAuthenticated` | | Protects fields and types by requiring a valid signed JWT | See [GraphQL Authorization](neo4j-graphql-js-middleware-authorization.mdx#isauthenticated) | +| `@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) | +| `@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) | diff --git a/docs/graphql-schema-generation-augmentation.md b/docs/graphql-schema-generation-augmentation.md new file mode 100644 index 00000000..36c8d3b0 --- /dev/null +++ b/docs/graphql-schema-generation-augmentation.md @@ -0,0 +1,679 @@ +# GraphQL Schema Generation And Augmentation + +`neo4j-graphql.js` can create an executable GraphQL schema from GraphQL type definitions or augment an existing GraphQL schema, adding + +- auto-generated mutations and queries (including resolvers) +- ordering and pagination fields +- filter fields + +## Usage + +To add these augmentations to the schema use either the [`augmentSchema`](neo4j-graphql-js-api.mdx#augmentschemaschema-graphqlschema) or [`makeAugmentedSchema`](neo4j-graphql-js-api.mdx#makeaugmentedschemaoptions-graphqlschema) functions exported from `neo4j-graphql-js`. + +**`makeAugmentedSchema`** - _generate executable schema from GraphQL type definitions only_ + +```js +import { makeAugmentedSchema } from 'neo4j-graphql-js'; + +const typeDefs = ` +type Movie { + movieId: ID! + title: String @search + year: Int + imdbRating: Float + genres: [Genre] @relation(name: "IN_GENRE", direction: OUT) + similar: [Movie] @cypher( + statement: """MATCH (this)<-[:RATED]-(:User)-[:RATED]->(s:Movie) + WITH s, COUNT(*) AS score + RETURN s ORDER BY score DESC LIMIT {first}""") +} + +type Genre { + name: String + movies: [Movie] @relation(name: "IN_GENRE", direction: IN) +}`; + +const schema = makeAugmentedSchema({ typeDefs }); +``` + +**`augmentSchema`** - _when you already have a GraphQL schema object_ + +```js +import { augmentSchema } from 'neo4j-graphql-js'; +import { makeExecutableSchema } from 'apollo-server'; +import { typeDefs, resolvers } from './movies-schema'; + +const schema = makeExecutableSchema({ + typeDefs, + resolvers +}); + +const augmentedSchema = augmentSchema(schema); +``` + +## Generated Queries + +Based on the type definitions provided, fields are added to the Query type for each type defined. For example, the following queries are added based on the type definitions above: + +```graphql +Movie( + movieID: ID! + title: String + year: Int + imdbRating: Float + _id: Int + first: Int + offset: Int + orderBy: _MovieOrdering +): [Movie] +``` + +```graphql +Genre( + name: String + _id: Int + first: Int + offset: Int + orderBy: _GenreOrdering +): [Genre] +``` + +## Generated Mutations + +Create, update, delete, and add relationship mutations are also generated for each type. For example: + +### Create + +```graphql +CreateMovie( + movieId: ID! + title: String + year: Int + imdbRating: Float +): Movie +``` + +> If an `ID` typed field is specified in the type definition, but not provided when the create mutation is executed then a random UUID will be generated and stored in the database. + +### Update + +```graphql +UpdateMovie( + movieId: ID! + title: String! + year: Int + imdbRating: Float +): Movie +``` + +### Delete + +```graphql +DeleteMovie( + movieId: ID! +): Movie +``` + +### Merge + +> In Neo4j, the `MERGE` clause ensures that a pattern exists in the graph. Either the pattern already exists, or it needs to be created. See the [Cypher manual](https://neo4j.com/docs/cypher-manual/current/clauses/merge/) for more information. + +```graphql +MergeMovie( + movieId: ID! + title: String + year: Int + imdbRating: Float +) +``` + +### Add / Remove Relationship + +Input types are used for relationship mutations. + +_Add a relationship with no properties:_ + +```graphql +AddMovieGenres( + from: _MovieInput! + to: _GenreInput! +): _AddMovieGenresPayload +``` + +and return a special payload type specific to the relationship: + +```graphql +type _AddMovieGenresPayload { + from: Movie + to: Genre +} +``` + +Relationship types with properties have an additional `data` parameter for specifying relationship properties: + +```graphql +AddMovieRatings( + from: _UserInput! + to: _MovieInput! + data: _RatedInput! +): _AddMovieRatingsPayload + +type _RatedInput { + timestamp: Int + rating: Float +} +``` + +### Remove relationship: + +```graphql +RemoveMovieGenres( + from: _MovieInput! + to: _GenreInput! +): _RemoveMovieGenresPayload +``` + +### Merge relationship: + +```graphql +MergeMovieGenres( + from: _MovieInput! + to: _GenreInput! +): _MergeMovieGenresPayload +``` + +### Update relationship + +Used to update properties on a relationship type. + +```graphql +UpdateUserRated( + from: _UserInput! + to: _MovieInput! + data: _RatedInput! +): _UpdateUserRatedPayload +``` + +> See [the relationship types](#relationship-types) section for more information, including how to declare these types in the schema and the relationship type query API. + +### Experimental API + +When the `config.experimental` boolean flag is true, [input objects](https://spec.graphql.org/June2018/#sec-Input-Objects) are generated for node property selection and input. + +```js +config: { + experimental: true; +} +``` + +For the following variant of the above schema, using the `@id`, `@unique`, and `@index` directives on the `Movie` type: + +```graphql +type Movie { + movieId: ID! @id + title: String! @unique + year: Int @index + imdbRating: Float + genres: [Genre] @relation(name: "IN_GENRE", direction: OUT) + similar: [Movie] + @cypher( + statement: """ + MATCH (this)<-[:RATED]-(:User)-[:RATED]->(s:Movie) + WITH s, COUNT(*) AS score + RETURN s ORDER BY score DESC LIMIT {first} + """ + ) +} + +type Genre { + name: String @id + movies: [Movie] @relation(name: "IN_GENRE", direction: IN) +} +``` + +This alternative API would be generated for the `Movie` type: + +```graphql +type Mutation { + # Node mutations + CreateMovie(data: _MovieCreate!): Movie + UpdateMovie(where: _MovieWhere!, data: _MovieUpdate!): Movie + DeleteMovie(where: _MovieWhere!): Movie + # Relationship mutations + AddMovieGenres(from: _MovieWhere!, to: _GenreWhere!): _AddMovieGenresPayload + RemoveMovieGenres( + from: _MovieWhere! + to: _GenreWhere! + ): _RemoveMovieGenresPayload + MergeMovieGenres( + from: _MovieWhere! + to: _GenreWhere! + ): _MergeMovieGenresPayload +} +``` + +For a node type such as `Movie`, this API design generates an input object for a node selection `where` argument and an input object for a `data` node property input argument. Complex [filtering arguments](https://grandstack.io/docs/graphql-filtering), similar to those used for the `filter` argument in the query API, are generated for each key field (`@id`, `@unique`, and `@index`) on the `Movie` type: + +#### Property Selection + +```graphql +input _MovieWhere { + AND: [_MovieWhere!] + OR: [_MovieWhere!] + movieId: ID + movieId_not: ID + movieId_in: [ID!] + movieId_not_in: [ID!] + movieId_contains: ID + movieId_not_contains: ID + movieId_starts_with: ID + movieId_not_starts_with: ID + movieId_ends_with: ID + movieId_not_ends_with: ID + title: String + title_not: String + title_in: [String!] + title_not_in: [String!] + title_contains: String + title_not_contains: String + title_starts_with: String + title_not_starts_with: String + title_ends_with: String + title_not_ends_with: String + year: Int + year_not: Int + year_in: [Int!] + year_not_in: [Int!] + year_lt: Int + year_lte: Int + year_gt: Int + year_gte: Int +} +``` + +#### Property Creation + +```graphql +input _MovieCreate { + movieId: ID + title: String! + year: Int + imdbRating: Float! +} +``` + +#### Create + +Similar to non-experimental API, when no value is provided for the `@id` field of a created node type, that field recieves an auto-generated value using [apoc.create.uuid()](https://neo4j.com/labs/apoc/4.1/graph-updates/uuid/#manual-uuids): + +```graphql +mutation { + CreateMovie(data: { title: "abc", imdbRating: 10, year: 2020 }) { + movieId + } +} +``` + +```js +{ + "data": { + "CreateMovie": { + "movieId": "1a2afaa0-5c74-436f-90be-57c4cbb791b0" + } + } +} +``` + +#### Property Update + +```graphql +input _MovieUpdate { + movieId: ID + title: String + year: Int + imdbRating: Float +} +``` + +#### Update + +This mutation API allows for updating key field values: + +```graphql +mutation { + UpdateMovie(where: { title: "abc", year: 2020 }, data: { year: 2021 }) { + movieId + } +} +``` + +#### Delete + +```graphql +mutation { + DeleteMovie(where: { year: 2020 }) { + movieId + } +} +``` + +#### Merge + +Because the Cypher `MERGE` clause cannot be combined with `WHERE`, node merge operations can use multiple key fields for node selection, but do not have complex filtering options: + +```graphql +type Mutation { + MergeMovie(where: _MovieKeys!, data: _MovieCreate!): Movie +} +``` + +```graphql +input _MovieKeys { + movieId: ID + title: String + year: Int +} +``` + +```graphql +mutation { + MergeMovie( + where: { movieId: "123" } + data: { title: "abc", imdbRating: 10, year: 2021 } + ) { + movieId + } +} +``` + +In the above `MergeMovie` mutation, a value is provided for the `movieId` argument, which is an `@id` key field on the `Movie` type. Similar to node creation, the `apoc.create.uuid` procedure is used to generate a value for an `@id` key, but only when first creating a node (using the Cypher `ON CREATE` clause of `MERGE`) and if no value is provided in both the `where` and `data` arguments: + +```graphql +mutation { + MergeMovie(where: { year: 2021 }, data: { imdbRating: 10, title: "abc" }) { + movieId + } +} +``` + +```js +{ + "data": { + "MergeMovie": { + "movieId": "fd44cd00-1ba1-4da8-894d-d38ba8e5513b" + } + } +} +``` + +## Ordering + +`neo4j-graphql-js` supports ordering results through the use of an `orderBy` parameter. The augment schema process will add `orderBy` to fields as well as appropriate ordering enum types (where values are a combination of each field and `_asc` for ascending order and `_desc` for descending order). For example: + +```graphql +enum _MovieOrdering { + title_asc + title_desc + year_asc + year_desc + imdbRating_asc + imdbRating_desc + _id_asc + _id_desc +} +``` + +## Pagination + +`neo4j-graphql-js` support pagination through the use of `first` and `offset` parameters. These parameters are added to the appropriate fields as part of the schema augmentation process. + +## Filtering + +The auto-generated `filter` argument is used to support complex field level filtering in queries. + +See [the Complex GraphQL Filtering section](graphql-filtering.mdx) for details. + +## Full-text Search + +The auto-generated `search` argument is used to support using [full-text search](https://neo4j.com/docs/cypher-manual/current/administration/indexes-for-full-text-search/#administration-indexes-fulltext-search-introduction) indexes set using [searchSchema](https://grandstack.io/docs/neo4j-graphql-js-api#searchschemaoptionsnull) with [@search directive](https://grandstack.io/docs/graphql-schema-directives) fields. + +In our example schema, no value is provided to the `index` argument of the `@search` directive on the `title` field of the `Movie` node type. So a default name of `MovieSearch` is used. + +The below example would query the `MovieSearch` search index for the value `river` (case-insensitive) on the `title` property of `Movie` type nodes. Only matching nodes with a score at or above the `threshold` argument would be returned. + +```graphql +query { + Movie(search: { MovieSearch: "river", threshold: 97.5 }) { + title + } +} +``` + +When the `search` argument is used, the query selects from the results of calling the [db.index.fulltext.queryNodes](https://neo4j.com/docs/cypher-manual/current/administration/indexes-for-full-text-search/#administration-indexes-fulltext-search-query) procedure: + +```js +CALL db.index.fulltext.queryNodes("MovieSearch", "river") +YIELD node AS movie, score WHERE score >= 97.5 +``` + +The remaining translation of the query is then applied to the yielded nodes. If a value for the `Float` type `threshold` argument is provided, only matching nodes with a resulting `score` at or above it will be returned. + +The `search` argument is not yet available on relationship fields and using multiple named search index arguments at once is not supported. + +## Type Extensions + +The GraphQL [specification](https://spec.graphql.org/June2018/#sec-Type-Extensions) describes using the `extend` keyword to represent a type which has been extended from another type. The following subsections describe the available behaviors, such as extending an object type to represent additional fields. When using schema augmentation, type extensions are applied when building the fields and types used for the generated Query and Mutation API. + +### Schema + +The [schema](https://spec.graphql.org/June2018/#sec-Schema-Extension) type can be extended with operation types. + +```graphql +schema { + query: Query +} +extend schema { + mutation: Mutation +} +``` + +### Scalars + +[Scalar](https://spec.graphql.org/June2018/#ScalarTypeExtension) types can be extended with additional directives. + +```graphql +scalar myScalar + +extend scalar myScalar @myDirective +``` + +### Objects & Interfaces + +[Object](https://spec.graphql.org/June2018/#ObjectTypeExtension) and [interface](https://spec.graphql.org/June2018/#InterfaceTypeExtension) types can be extended with additional fields and directives. Objects can also be extended to implement interfaces. + +##### Fields + +```graphql +type Movie { + movieId: ID! + title: String + year: Int + imdbRating: Float +} + +extend type Movie { + genres: [Genre] @relation(name: "IN_GENRE", direction: OUT) + similar: [Movie] + @cypher( + statement: """ + MATCH (this)<-[:RATED]-(:User)-[:RATED]->(s:Movie) + WITH s, COUNT(*) AS score + RETURN s ORDER BY score DESC LIMIT {first} + """ + ) +} +``` + +##### Directives + +```graphql +type Movie { + movieId: ID! +} + +extend type Movie @additionalLabels(labels: ["newMovieLabel"]) +``` + +##### Operation types + +```graphql +type Query { + Movie: [Movie] +} + +extend type Query { + customMovie: Movie +} +``` + +##### Implementing interfaces + +```graphql +interface Person { + userId: ID! + name: String +} + +type Actor { + userId: ID! + name: String +} + +extend type Actor implements Person +``` + +### Unions + +A [union](https://spec.graphql.org/June2018/#sec-Union-Extensions) type can be extended with additional member types or directives. + +```graphql +union MovieSearch = Movie | Genre | Book + +extend union MovieSearch = Actor | OldCamera +``` + +### Enums + +[Enum](https://spec.graphql.org/June2018/#EnumTypeExtension) types can be extended with additional values or directives. + +```graphql +enum BookGenre { + Mystery + Science +} + +extend enum BookGenre { + Math +} +``` + +### Input Objects + +[Input object](https://spec.graphql.org/June2018/#InputObjectTypeExtension) types can be extended with additional input fields or directives. + +```graphql +input CustomMutationInput { + title: String +} + +extend input CustomMutationInput { + year: Int + imdbRating: Float +} +``` + +## Configuring Schema Augmentation + +You may not want to generate Query and Mutation fields for all types included in your type definitions, or you may not want to generate a Mutation type at all. Both `augmentSchema` and `makeAugmentedSchema` can be passed an optional configuration object to specify which types should be included in queries and mutations. + +### Disabling Auto-generated Queries and Mutations + +By default, both Query and Mutation types are auto-generated from type definitions and will include fields for all types in the schema. An optional `config` object can be passed to disable generating either the Query or Mutation type. + +Using `makeAugmentedSchema`, disable generating the Mutation type: + +```js +import { makeAugmentedSchema } from "neo4j-graphql-js"; + +const schema = makeAugmentedSchema({ + typeDefs, + config: { + query: true, // default + mutation: false + } +} +``` + +Using `augmentSchema`, disable auto-generating mutations: + +```js +import { augmentSchema } from 'neo4j-graphql-js'; + +const augmentedSchema = augmentSchema(schema, { + query: true, //default + mutation: false +}); +``` + +### Excluding Types + +To exclude specific types from being included in the generated Query and Mutation types, pass those type names in to the config object under `exclude`. For example: + +```js +import { makeAugmentedSchema } from 'neo4j-graphql-js'; + +const schema = makeAugmentedSchema({ + typeDefs, + config: { + query: { + exclude: ['MyCustomPayload'] + }, + mutation: { + exclude: ['MyCustomPayload'] + } + } +}); +``` + +See the API Reference for [`augmentSchema`](neo4j-graphql-js-api.mdx#augmentschemaschema-graphqlschema) and [`makeAugmentedSchema`](neo4j-graphql-js-api.mdx#makeaugmentedschemaoptions-graphqlschema) for more information. + +### Excluding relationships + +To exclude specific relationships between types from being resolved using the generated neo4j resolver, use the `@neo4j_ignore` directive. This is useful when combining other data sources with your neo4j graph. Used alongside excluding types from augmentation, it allows data related to graph nodes to be blended with eth neo4j result. For example: + +```graphql +type IMDBReview { + rating: Int + text: String +} + +extend type Movie { + imdbUrl: String + imdbReviews: [IMDBReview] @neo4j_ignore +} +``` + +```js +const schema = makeAugmentedSchema({ + resolvers: { + Movie: { + imdbReviews: ({imdbURL}) => // fetch data from IMDB and return JSON result + } + } + config: {query: {exclude: ['IMDBReview']}} +]) +``` + +## Resources + +- Blog post: [GraphQL API Configuration With neo4j-graphql.js](https://blog.grandstack.io/graphql-api-configuration-with-neo4j-graphql-js-bf7a1331c793) - Excluding Types From The Auto-Generated GraphQL Schema diff --git a/docs/graphql-spatial-types.md b/docs/graphql-spatial-types.md new file mode 100644 index 00000000..ca8f4bca --- /dev/null +++ b/docs/graphql-spatial-types.md @@ -0,0 +1,189 @@ +# GraphQL Spatial Types + +> 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. + +## Spatial `Point` type in SDL + +neo4j-graphql.js makes available the `Point` type for use in your GraphQL type definitions. You can use it like this: + +```graphql +type Business { + id: ID! + name: String + location: Point +} +``` + +The GraphQL [schema augmentation process](graphql-schema-generation-augmentation.mdx) will translate the `location` field to a `_Neo4jPoint` type in the augmented schema. + +## Using `Point` In Queries + +The `Point` object type exposes the following fields that can be used in the query selection set: + +- `x`: `Float` +- `y`: `Float` +- `z`: `Float` +- `longitude`: `Float` +- `latitude`: `Float` +- `height`: `Float` +- `crs`: `String` +- `srid`: `Int` + +For example: + +_GraphQL query_ + +```graphql +query { + Business(first: 2) { + name + location { + latitude + longitude + } + } +} +``` + +_GraphQL result_ + +```json +{ + "data": { + "Business": [ + { + "name": "Missoula Public Library", + "location": { + "latitude": 46.870035, + "longitude": -113.990976 + } + }, + { + "name": "Ninja Mike's", + "location": { + "latitude": 46.874029, + "longitude": -113.995057 + } + } + ] + } +} +``` + +### `Point` Query Arguments + +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: + +_GraphQL query_ + +```graphql +query { + Business(location: { latitude: 46.870035, longitude: -113.990976 }) { + name + location { + latitude + longitude + } + } +} +``` + +_GraphQL result_ + +```json +{ + "data": { + "Business": [ + { + "name": "Missoula Public Library", + "location": { + "latitude": 46.870035, + "longitude": -113.990976 + } + } + ] + } +} +``` + +However, with Point data the auto-generated filters are likely to be more useful, especially when we consider arbitrary precision. + +### `Point` Query Filter + +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: + +_GraphQL query_ + +```graphql +{ + Business( + filter: { + location_distance_lt: { + point: { latitude: 46.859924, longitude: -113.985402 } + distance: 1500 + } + } + ) { + name + location { + latitude + longitude + } + } +} +``` + +_GraphQL result_ + +```json +{ + "data": { + "Business": [ + { + "name": "Missoula Public Library", + "location": { + "latitude": 46.870035, + "longitude": -113.990976 + } + }, + { + "name": "Market on Front", + "location": { + "latitude": 46.869824, + "longitude": -113.993633 + } + } + ] + } +} +``` + +For points using the Geographic coordinate reference system (latitude and longitude) `distance` is measured in meters. + +## Using `Point` In Mutations + +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. + +For example, to create a new Business node and set the value of the location field: + +```graphql +mutation { + CreateBusiness( + name: "University of Montana" + location: { latitude: 46.859924, longitude: -113.985402 } + ) { + name + } +} +``` + +Note that not all fields of the `_Neo4jPointInput` type need to specified. In general, you have the choice of: + +- **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. +- **Number of dimensions** You can specify `height` along with `longitude` and `latitude` for 3D, or `z` along with `x` and `y`. + +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. + +## Resources + +- 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 diff --git a/docs/graphql-temporal-types-datetime.md b/docs/graphql-temporal-types-datetime.md new file mode 100644 index 00000000..7c2a00d2 --- /dev/null +++ b/docs/graphql-temporal-types-datetime.md @@ -0,0 +1,162 @@ +# GraphQL Temporal Types (DateTime) + +> 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/). + +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. + +## Temporal Types In SDL + +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: + +```graphql +type Movie { + id: ID! + title: String + published: DateTime +} +``` + +## Using Temporal Fields In Queries + +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: + +_GraphQL query_ + +```graphql +{ + Movie(title: "River Runs Through It, A") { + title + published { + day + month + year + hour + minute + second + formatted + } + } +} +``` + +_GraphQL result_ + +```json +{ + "data": { + "Movie": [ + { + "title": "River Runs Through It, A", + "published": { + "day": 9, + "month": 10, + "year": 1992, + "hour": 0, + "minute": 0, + "second": 0, + "formatted": "1992-10-09T00:00:00Z" + } + } + ] + } +} +``` + +### Temporal Query Arguments + +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: + +```graphql +type Movie { + movieId: ID! + title: String + released: Date +} +``` + +the following query will be generated for the `Movie` type: + +```graphql +Movie ( + movieId: ID! + title: String + released: _Neo4jDate + _id: String + first: Int + offset: Int + orderBy: _MovieOrdering +) +``` + +and the type `_Neo4jDateInput` added to the schema: + +```graphql +type _Neo4jDateTimeInput { + year: Int + month: Int + day: Int + formatted: String +} +``` + +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: + +```graphql +{ + Movie(released: { year: 1992, month: 10, day: 9 }) { + title + } +} +``` + +or equivalently: + +```graphql +{ + Movie(released: { formatted: "1992-10-09" }) { + title + } +} +``` + +## Using Temporal Fields In Mutations + +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. + +For example, this mutation: + +```graphql +mutation { + CreateMovie( + title: "River Runs Through It, A" + published: { year: 1992, month: 10, day: 9 } + ) { + title + published { + formatted + } + } +} +``` + +is equivalent to this version, using the `formatted` field instead + +```graphql +mutation { + CreateMovie( + title: "River Runs Through It, A" + published: { formatted: "1992-10-09T00:00:00Z" } + ) { + title + published { + formatted + } + } +} +``` + +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). + +## Resources + +- 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 diff --git a/docs/img/BatchMergeAGraphData.png b/docs/img/BatchMergeAGraphData.png new file mode 100644 index 00000000..4b97370a Binary files /dev/null and b/docs/img/BatchMergeAGraphData.png differ diff --git a/docs/img/MergeAGraphData.png b/docs/img/MergeAGraphData.png new file mode 100644 index 00000000..009cad32 Binary files /dev/null and b/docs/img/MergeAGraphData.png differ diff --git a/docs/img/exampleDataGraph.png b/docs/img/exampleDataGraph.png new file mode 100644 index 00000000..ca294c17 Binary files /dev/null and b/docs/img/exampleDataGraph.png differ diff --git a/docs/img/interface-data.png b/docs/img/interface-data.png new file mode 100644 index 00000000..a20fe0c4 Binary files /dev/null and b/docs/img/interface-data.png differ diff --git a/docs/img/interface-model.png b/docs/img/interface-model.png new file mode 100644 index 00000000..8304c8b3 Binary files /dev/null and b/docs/img/interface-model.png differ diff --git a/docs/img/movies.png b/docs/img/movies.png new file mode 100644 index 00000000..27c46a39 Binary files /dev/null and b/docs/img/movies.png differ diff --git a/docs/img/union-data.png b/docs/img/union-data.png new file mode 100644 index 00000000..163a6147 Binary files /dev/null and b/docs/img/union-data.png differ diff --git a/docs/infer-graphql-schema-database.md b/docs/infer-graphql-schema-database.md new file mode 100644 index 00000000..841e2332 --- /dev/null +++ b/docs/infer-graphql-schema-database.md @@ -0,0 +1,173 @@ +# Inferring GraphQL Schema From An Existing Database + +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. + +Let's use the [Neo4j Sandbox movies recommendation dataset](https://neo4j.com/sandbox?usecase=recommendations) to auto-generate GraphQL type definitions using `inferSchema`. + +This dataset has information about movies, actors, directors, and user ratings of movies, modeled as a graph: + +![Movie graph data model](img/movies.png) + +## How To Use `inferSchema` + +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. + +```js +const { inferSchema } = require('neo4j-graphql-js'); +const neo4j = require('neo4j-driver'); + +const driver = neo4j.driver( + 'bolt://localhost:7687', + neo4j.auth.basic('neo4j', 'letmein') +); +inferSchema(driver).then(result => { + console.log(result.typeDefs); +}); +``` + +Running this on [the movies dataset](https://neo4j.com/sandbox?usecase=recommendations) would produce the following GraphQL type definitions: + +```graphql +type Movie { + _id: Long! + countries: [String] + imdbId: String! + imdbRating: Float + imdbVotes: Int + languages: [String] + movieId: String! + plot: String + poster: String + released: String + runtime: Int + title: String! + tmdbId: String + year: Int + in_genre: [Genre] @relation(name: "IN_GENRE", direction: OUT) + users: [User] @relation(name: "RATED", direction: IN) + actors: [Actor] @relation(name: "ACTED_IN", direction: IN) + directors: [Director] @relation(name: "DIRECTED", direction: IN) +} + +type RATED @relation(name: "RATED") { + from: User! + to: Movie! + created: DateTime! + rating: Float! + timestamp: Int! +} + +type User { + _id: Long! + name: String! + userId: String! + rated: [Movie] @relation(name: "RATED", direction: OUT) + RATED_rel: [RATED] +} + +type Actor { + _id: Long! + name: String! + acted_in: [Movie] @relation(name: "ACTED_IN", direction: OUT) +} + +type Director { + _id: Long! + name: String! + directed: [Movie] @relation(name: "DIRECTED", direction: OUT) +} + +type Genre { + _id: Long! + name: String! + movies: [Movie] @relation(name: "IN_GENRE", direction: IN) +} +``` + +## Using `inferSchema` With `makeAugmentedSchema` + +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`. + +Here’s a full example: + +```js +const { makeAugmentedSchema, inferSchema } = require('neo4j-graphql-js'); +const { ApolloServer } = require('apollo-server'); +const neo4j = require('neo4j-driver'); + +// Create Neo4j driver instance +const driver = neo4j.driver( + process.env.NEO4J_URI || 'bolt://localhost:7687', + neo4j.auth.basic( + process.env.NEO4J_USER || 'neo4j', + process.env.NEO4J_PASSWORD || 'letmein' + ) +); + +// Connect to existing Neo4j instance, infer GraphQL typedefs +// generate CRUD GraphQL API using makeAugmentedSchema +const inferAugmentedSchema = driver => { + return inferSchema(driver).then(result => { + return makeAugmentedSchema({ + typeDefs: result.typeDefs + }); + }); +}; + +// Spin up GraphQL server using auto-generated GraphQL schema object +const createServer = schema => + new ApolloServer({ + schema + context: { driver } + } + }); + +inferAugmentedSchema(driver) + .then(createServer) + .then(server => server.listen(3000, '0.0.0.0')) + .then(({ url }) => { + console.log(`GraphQL API ready at ${url}`); + }) + .catch(err => console.error(err)); +``` + +## Persisting The Inferred Schema + +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: + +```js +const neo4j = require('neo4j-driver'); +const { inferSchema } = require('neo4j-graphql-js'); +const fs = require('fs'); + +const driver = neo4j.driver( + 'bolt://localhost:7687', + neo4j.auth.basic('neo4j', 'letmein') +); + +const schemaInferenceOptions = { + alwaysIncludeRelationships: false +}; + +inferSchema(driver, schemaInferenceOptions).then(result => { + fs.writeFile('schema.graphql', result.typeDefs, err => { + if (err) throw err; + console.log('Updated schema.graphql'); + process.exit(0); + }); +}); +``` + +Then we can load this schema.graphql file and pass the type definitions into `makeAugmentedSchema`. + +```js +// Load GraphQL type definitions from schema.graphql file +const typeDefs = fs + .readFileSync(path.join(__dirname, 'schema.graphql')) + .toString('utf-8'); +``` + +## Resources + +- [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 +- [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. diff --git a/docs/neo4j-graphql-js-api.md b/docs/neo4j-graphql-js-api.md new file mode 100644 index 00000000..3f957eaa --- /dev/null +++ b/docs/neo4j-graphql-js-api.md @@ -0,0 +1,255 @@ +# neo4j-graphql.js API Reference + +This reference documents the exports from `neo4j-graphql-js`: + +## Exports + +### `makeAugmentedSchema(options)`: <`GraphQLSchema`> + +Wraps [`makeExecutableSchema`](https://www.apollographql.com/docs/apollo-server/api/apollo-server.html#makeExecutableSchema) to create a GraphQL schema from GraphQL type definitions (SDL). Will generate Query and Mutation types for the provided type definitions and attach `neo4jgraphql` as the resolver for these queries and mutations. Either a schema or typeDefs must be provided. `resolvers` can optionally be implemented to override any of the generated Query/Mutation fields. Additional options are passed through to `makeExecutableSchema`. + +#### Parameters + +- `options`: <`Object`> + - `schema`: <`GraphQLSchema`> + - `typeDefs`: <`String`> + - `resolvers`: <`Object`> + - `logger`: <`Object`> + - `allowUndefinedInResolve` = false + - `resolverValidationOptions` = {} + - `directiveResolvers` = null + - `schemaDirectives` = null + - `parseOptions` = {} + - `inheritResolversFromInterfaces` = false + - `config`: <`Object`> = {} + +#### config + +`config` is an object that can contain several optional keys as detailed in the table below. + +| Key | Description | Default | Options | Example | +| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------- | --------------------- | ----------------------------------------------------------------------------------- | +| `query` | Configure the autogenerated Query fields. Can be enabled/disabled for all types or a list of individual types to exclude can be passed. Commonly used to exclude payload types. | `true` | `Boolean` or `Object` | `{query: false}`



`{query: {exclude: ["MyPayloadType"]}` | +| `mutation` | Configure the autogenerated Mutation fields. Can be enabled/disabled for all types or a list of individual types to exclude can be passed. Commonly used to exclude payload types. | `true` | `Boolean` or `Object` | `{mutation: false}`



`{query: {exclude: ["MyPayloadType"]}` | +| `debug` | Enable/disable logging of generated Cypher queries and parameters. | `True` | `Boolean` | `{debug: false}` | +| `auth` | Used to enable authorization schema directives (`@isAuthenticated`, `@hasRole`, `@hasScope`). If enabled, directives from the [graphql-auth-directives](https://www.npmjs.com/package/graphql-auth-directives) are declared and can be used in the schema. If `@hasScope` is enabled it is automatically added to all generated query and mutation fields. See the authorization guide for more information. | `false` | `Boolean` or `Object` | `{auth: true}`



`{auth: {isAuthenticated: true}, {hasRole: true}` | + +For example: + +```js +const schema = makeAugmentedSchema({ + typeDefs, + config: { + query: true, //default + mutation: false + } +}); +``` + +or + +```js +const schema = makeAugmentedSchema({ + typeDefs, + config: { + query: { + exclude: ['MyPayloadType'] + }, + mutation: { + exclude: ['MyPayloadType'] + } + } +}); +``` + +#### Returns + +`GraphQLSchema` + +
+ +### `assertSchema(options)`:`null` + +This function uses the `@id`, `@unique`, and `@index` schema directives present in the GraphQL type definitions, along with [apoc.schema.assert()](https://neo4j.com/labs/apoc/4.0/overview/apoc.schema/apoc.schema.assert/), to add any database constraints and indexes. + +#### Parameters + +- `options`: <`Object`> + + - `schema`: <`GraphQLSchema`> + - `driver`: <`Neo4jDriver`> + - `debug`: <`Bool`> = false + - `dropExisting`: <`Bool`> = true + +#### Example + +```js +import { + makeAugmentedSchema, + assertSchema +} from 'neo4j-graphql-js'; + +const driver = neo4j.driver(...); + +const schema = makeAugmentedSchema(...); + +assertSchema({ schema, driver, debug: true }); +``` + +
+ +### `searchSchema(options)`:`null` + +This function uses the `@search` schema directive present in the GraphQL type definitions to add any [full-text search indexes](https://neo4j.com/docs/cypher-manual/current/administration/indexes-for-full-text-search/). + +#### Parameters + +- `options`: <`Object`> + + - `schema`: <`GraphQLSchema`> + - `driver`: <`Neo4jDriver`> + - `debug`: <`Bool`> = false + +#### Example + +```js +import { + makeAugmentedSchema, + searchSchema +} from 'neo4j-graphql-js'; + +const driver = neo4j.driver(...); + +const schema = makeAugmentedSchema(...); + +searchSchema({ schema, driver, debug: true }); +``` + +
+ +### `neo4jgraphql(object, params, context, resolveInfo, debug)`: <`ExecutionResult`> + +This function's signature matches that of [GraphQL resolver functions](https://graphql.org/learn/execution/#root-fields-resolvers). and thus the parameters match the parameters passed into `resolve` by GraphQL implementations like graphql-js. + +It can be called within a resolver to generate a Cypher query and handle the database call to Neo4j to completely resolve the GraphQL request. Alternatively, use `cypherQuery` or `cypherMutation` within a resolver to only generate the Cypher query and handle the database call yourself. + +#### Parameters + +- `object`: <`Object`> + +The previous object being resolved. Rarely used for a field on the root Query type. + +- `params`: <`Object`> + +The arguments provided to the field in the GraphQL query. + +- `context`: <`Object`> + +Value provided to every resolver and hold contextual information about the request, such as the currently logged in user, or access to a database. _`neo4j-graphql-js` assumes a `neo4j-javascript-driver` instance exists in this object, under the key `driver`._ + +- `resolveInfo`: <`GraphQLResolveInfo`> + +Holds field-specific infomation relevant to the current query as well as the GraphQL schema. + +- `debug`: `Boolean` _(default: `true`)_ + +Specifies whether to log the generated Cypher queries for each GraphQL request. Logging is enabled by default. + +#### Returns + +[ExecutionResult](https://graphql.org/graphql-js/execution/#execute) + +
+ +### `augmentSchema(schema, config)`: <`GraphQLSchema`> + +Takes an existing GraphQL schema object and adds neo4j-graphql-js specific enhancements, including auto-generated mutations and queries, and ordering and pagination fields. See [this guide](neo4j-graphql-js.mdx) for more information. + +> NOTE: Only use `augmentSchema` if you are working with an existing GraphQLSchema object. In most cases you should use [`makeAugmentedSchema`](#makeaugmentedschemaoptions-graphqlschema) which can construct the GraphQLSchema object from type definitions. + +#### Parameters + +- `schema`: <`GraphQLSchema`> +- `config`: <`Object`> + +`config` is an object that can contain several optional keys. See the [details in the table below for `makeAugmentedSchema`](#config) + +For example: + +```js +const augmentedSchema = augmentSchema(schema, { + query: true, //default + mutation: false +}); +``` + +or + +```js +const augmentedSchema = augmentSchema(schema, { + query: { + exclude: ['MyPayloadType'] + }, + mutation: { + exclude: ['MyPayloadType'] + } +}); +``` + +#### Returns + +`GraphQLSchema` + +
+ +### `cypherQuery(params, context, resolveInfo)` + +Generates a Cypher query (and associated parameters) to resolve a given GraphQL request (for a Query). Use this function when you want to handle the database call yourself, use `neo4jgraphql` for automated database call support. + +#### Parameters + +- `params`: <`Object`> +- `context`: <`Object`> +- `resolveInfo`: <`GraphQLResolveInfo`> + +#### Returns + +`[`<`String`>, <`Object`>`]` + +Returns an array where the first element is the genereated Cypher query and the second element is an object with the parameters for the generated Cypher query. + +
+ +### `cypherMutation(params, context, resolveInfo` + +Similar to `cypherQuery`, but for mutations. Generates a Cypher query (and associated parameters) to resolve a given GraphQL request (for a Mutation). Use this function when you want to handle the database call yourself, use `neo4jgraphql` for automated database call support. + +#### Parameters + +- `params`: <`Object`> +- `context`: <`Object`> +- `resolveInfo`: <`GraphQLResolveInfo`> + +#### Returns + +`[`<`String`>, <`Object`>`]` + +Returns an array where the first element is the genereated Cypher query and the second element is an object with the parameters for the generated Cypher query. + +
+ +### `inferSchema(driver, [options])`: <`Promise`> + +Used to generate GraphQL type definitions from an existing Neo4j database by inspecting the data stored in the database. When used in combination with `makeAugmentedSchema` this can be used to generate a GraphQL CRUD API on top of an existing Neo4j database without writing any resolvers or GraphQL type definitions. See [example/autogenerated/autogen.js](https://github.com/neo4j-graphql/neo4j-graphql-js/blob/master/example/autogenerated/autogen.js) for an example of using `inferSchema` and `makeAugmentedSchema` with Apollo Server. + +#### Parameters + +- `driver`: <`Neo4jDriver`> +- `options`: <`Object`> + - `alwaysIncludeRelationships`: `Boolean` - specifies whether relationships should always be included in the type definitions as [relationship types](neo4j-graphql-js.mdx#relationship-types), even if the relationships do not have properties. + +#### Returns + +`Promise` that resolves to an object that contains: + +- `typeDefs`: `String` - a string representation of the generated GraphQL type definitions in Schema Definition Language (SDL) format, inferred from the existing Neo4j database. diff --git a/docs/neo4j-graphql-js-middleware-authorization.md b/docs/neo4j-graphql-js-middleware-authorization.md new file mode 100644 index 00000000..f5183f76 --- /dev/null +++ b/docs/neo4j-graphql-js-middleware-authorization.md @@ -0,0 +1,363 @@ +# GraphQL Authorization And Middleware + +This guide discusses some of the ways to address authentication and authorization when using `neo4j-graphql-js` and will evolve as new auth-specific features are added. + +## GraphQL Authorization Schema Directives + +Schema directives can be used to define authorization logic. By default, we use the [`graphql-auth-directives`](https://www.npmjs.com/package/graphql-auth-directives) library to add authorization schema directives that can then be used in the schema. `graphql-auth-directives` work with JSON Web Tokens (JWT), and assumes a JWT is included in the GraphQL request header. The claims contained in the JWT (roles, scopes, etc) are used to validate the GraphQL request, protecting resources in the following ways: + +### `isAuthenticated` + +The `isAuthenticated` schema directive can be used on types or fields. The request must be authenticated to access the resource (in other words, the request must contain a valid signed JWT). `@isAuthenticated` can be used on a type definition, applying the authorization rule to the entire object, for example: + +```graphql +type Movie @isAuthenticated { + movieId: ID! + title: String + plot: String +} +``` + +We could also annotate individual fields, in this case restricting only the `plot` field: + +```graphql +type Movie { + movieId: ID! + title: String + plot: String @isAuthenticated + views: Int +} +``` + +### `hasRole` + +The `hasRole` schema directive can be used on types or fields and indicates that: + +1. a request must contain a valid signed JWT, and +1. the `roles` claim in the JWT must include the role specified in the schema directive. + +Valid roles should be defined in a GraphQL enum. For example: + +```graphql +enum Role { + reader + user + admin +} + +type Movie @hasRole(roles: [admin]) { + movieId: ID! + title: String + plot: String + views: Int +} +``` + +### `hasScope` + +The `hasScope` schema directive can be used on Query or Mutation fields and indicates that + +1. a request must contain a valid signed JWT, and +1. the `scope` claim in the JWT includes at least one of the required scopes in the directive + +```graphql +type Mutation { + CreateMovie(movieId: ID!, title: String, plot: String, views: Int): Movie + @hasScope(scopes: ["Movie:Create"]) +} +``` + +The `hasScope` directive can be used on custom queries and mutations with `@cypher` directive, as well as on the auto-generated queries and mutations (see next section) + +```graphql +type Mutation { + IncrementView(movieId: ID!): Movie + @hasScope(scopes: ["Movie:Update"]) + @cypher( + statement: """ + MATCH (m:Movie {movieId: $movieId}) + SET m.views = m.views + 1 + """ + ) +} +``` + +### Configuring schema directives + +To make use of the directives from `graphql-auth-directives` you must + +1. Set the `JWT_SECRET` environment variable +1. Enable the auth directives in the config object passed to `makeAugmentedSchema` or `augmentSchema` + +#### Environment variables used to configure JWT + +You must set the `JWT_SECRET` environment variable: + +```shell +export JWT_SECRET= +``` + +By default `@hasRole` will validate the `roles`, `role`, `Roles`, or `Role` claim (whichever is found first). You can override this by setting `AUTH_DIRECTIVES_ROLE_KEY` environment variable. For example, if your role claim is stored in the JWT like this + +```json +"https://grandstack.io/roles": [ + "admin" +] +``` + +then declare a value for `AUTH_DIRECTIVES_ROLE_KEY` environment variable: + +```shell +export AUTH_DIRECTIVES_ROLE_KEY=https://grandstack.io/roles +``` + +#### Enabling Auth Directives + +By default the auth directives are disabled and must be explicitly enabled in the config object passed to `makeAugmentedSchema` or `augmenteSchema`. + +If enabled, authorization directives are declared and can be used in the schema. If `@hasScope` is enabled it is automatically added to all generated query and mutation fields. To enable authorization schema directives (`@isAuthenticated`, `@hasRole`, `@hasScope`), pass values for the `auth` key in the `config` object. For example: + +```js +import { makeAugmentedSchema } from 'neo4j-graphql-js'; + +const schema = makeAugmentedSchema({ + tyepDefs, + config: { + auth: { + isAuthenticated: true, + hasRole: true + } + } +}); +``` + +With this configuration, the `isAuthenticated` and `hasRole` directives will be available to be used in the schema, but not the `hasScope` directive. + +#### Attaching Directives To Auto-Generated Queries and Mutations + +Since neo4j-graphql.js automatically adds Query and Mutation types to the schema, these auto-generated fields cannot be annotated by the user with directives. To enable authorization on the auto-generated queries and mutations, simply enable the `hasScope` directive and it will be added to the generated CRUD API with the appropriate scope for each operation. For example: + +```js +import { makeAugmentedSchema } from 'neo4j-graphql-js'; + +const schema = makeAugmentedSchema({ + tyepDefs, + config: { + auth: { + hasScope: true + } + } +}); +``` + +#### Attaching Custom Authorization Schema Directives + +In some use cases, different authentication and authorization logic is required to that provided by `graphql-auth-directives`. For example, you may use middleware to validate bearer tokens and insert scopes into the context obect. It is possible to leverage the schema augmentation functionality afforded by the `config.auth` options whilst providing your own directives to attach. First create your own directive (the default [`graphql-auth-directives` repository](https://github.com/grand-stack/graphql-auth-directives) offers a working template) and then include them in the config object passed to `makeAugmentedSchema` or `augmentSchema`. For example, to override the `@hasScope` directive: + +```js +import { makeAugmentedSchema } from 'neo4j-graphql-js'; +import { MyHasScopeDirective } from './my-directives'; + +const schema = makeAugmentedSchema({ + tyepDefs, + config: { + auth: { + hasScope: true + } + }, + schemaDirectives: { + hasScope: MyHasScopeDirective + } +}); +``` + +## Cypher Parameters From Context + +Another approach to implementing authorization logic is to access values from the context object in a Cypher query used in a `@cypher` directive. This is useful for example, to access authenticated user information that may be stored in a request token or added to the request object via middleware. Any parameters in the `cypherParams` object in the context are passed with the Cypher query and can be used as Cypher parameters. + +For example: + +```graphql +type Query { + currentUser: User + @cypher( + statement: """ + MATCH (u:User {id: $cypherParams.currentUserId}) + RETURN u + """ + ) +} +``` + +Here is an example of how to add values to the `cypherParams` in the context using ApolloServer: + +```js +const server = new ApolloServer({ + context: ({ req }) => ({ + driver, + cypherParams: { + currentUserId: req.user.id + } + }) +}); +``` + +## Inspect Context In Resolver + +A common pattern for dealing with authentication / authorization in GraphQL is to inspect an authorization token or a user object injected into the context in a resolver function to ensure the authenticated user is appropirately authorized to request the data. This can be done in `neo4j-graphql-js` by implementing your own resolver function(s) and calling [`neo4jgraphql`](neo4j-graphql-js-api.mdx#neo4jgraphqlobject-params-context-resolveinfo-debug-executionresult-https-graphqlorg-graphql-js-execution-execute) after inspecting the token / user object. + +First, ensure the appropriate data is injected into the context object. In this case we inject the entire `request` object, which in our case will contain a `user` object (which comes from some authorization middleware in our application, such as passport.js): + +```js +const server = new ApolloServer({ + schema: augmentedSchema, + context: ({ req }) => { + return { + driver, + req + }; + } +}); +``` + +Then in our resolver, we check for the existence of our user object. If `req.user` is not present then we return an error as the request is not authenticated, if `req.user` is present then we know the request is authenticated and resolve the data with a call to `neo4jgraphql`: + +```js +const resolvers = { + // root entry point to GraphQL service + Query: { + Movie(object, params, ctx, resolveInfo) { + if (!ctx.req.user) { + throw new Error('request not authenticated'); + } else { + return neo4jgraphql(object, params, ctx, resolveInfo); + } + } + } +}; +``` + +This resolver object can then be attached to the GraphQL schema using [`makeAugmentedSchema`](neo4j-graphql-js-api.mdx#makeaugmentedschemaoptions-graphqlschema) + +We can apply this same strategy to check for user scopes, inspect scopes on a JWT, etc. + +## Aditional Schema Directives + +### `@additionalLabels` + +The `additionalLabels` schema directive can only be used on types for adding additional labels on the nodes. Use this if you need extra labels on you nodes or if you want to implement a kind of "multi-tenancy" graph that isolates the subgraph with different labels. The directive accept an array of strings that can be combined with `cypherParams` variables. + +Adding 2 labels to the Movie type; 1 static label and 1 dynamic label that uses fields from `cypherParams`. For example: + +```graphql +type Movie + @additionalLabels( + labels: ["u_<%= $cypherParams.userId %>", "newMovieLabel"] + ) { + movieId: ID! + title: String + plot: String + views: Int +} +``` + +This will add the labels "newMovieLabel" and "u_1234" on the query when creating/updating/querying the database. This does not work if there exist a `@cypher` directive on the type. + +## Middleware + +Middleware is often useful for features such as authentication / authorization. You can use middleware with neo4j-graphql-js by injecting the request object after middleware has been applied into the context. For example: + +```js +const server = new ApolloServer({ + schema: augmentedSchema, + // inject the request object into the context to support middleware + // inject the Neo4j driver instance to handle database call + context: ({ req }) => { + return { + driver, + req + }; + } +}); +``` + +This request object will then be available inside your GraphQL resolver function. You can inspect the context/request object in your resolver to verify auth before calling `neo4jgraphql`. Also, `neo4jgraphql` will check for the existence of: + +- `context.req.error` +- `context.error` + +and will throw an error if any of the above are defined. + +Full example: + +```js +import { makeAugmentedSchema } from 'neo4j-graphql-js'; +import { ApolloServer } from 'apollo-server-express'; +import express from 'express'; +import bodyParser from 'body-parser'; +import { makeExecutableSchema } from 'apollo-server'; +import { v1 as neo4j } from 'neo4j-driver'; +import { typeDefs } from './movies-schema'; + +const schema = makeAugmentedSchema({ + typeDefs +}); + +// Add auto-generated mutations +const schema = augmentSchema(schema); + +const driver = neo4j.driver( + process.env.NEO4J_URI || 'bolt://localhost:7687', + neo4j.auth.basic( + process.env.NEO4J_USER || 'neo4j', + process.env.NEO4J_PASSWORD || 'letmein' + ) +); + +const app = express(); +app.use(bodyParser.json()); + +const checkErrorHeaderMiddleware = async (req, res, next) => { + req.error = req.headers['x-error']; + next(); +}; + +app.use('*', checkErrorHeaderMiddleware); + +const server = new ApolloServer({ + schema: schema, + // inject the request object into the context to support middleware + // inject the Neo4j driver instance to handle database call + context: ({ req }) => { + return { + driver, + req + }; + } +}); + +server.applyMiddleware({ app, path: '/' }); +app.listen(3000, '0.0.0.0'); +``` + +## Passing A Neo4j Driver Bookmark + +To support [causal consistency chaining](https://neo4j.com/docs/operations-manual/4.1/clustering/introduction/) for Neo4j clusters where a system other than neo4j-graphql.js has been sending writes to the database cluster, optionally a neo4j driver bookmark may be added to the context object in the `neo4jBooksmarks` key. + +For example, to set the context.neo4jBookmarks value from a request header (in this case headers.neo4jbookmarks) using Apollo Server: + +```js +const server = new ApolloServer({ + schema, + context: ({ req }) => { + return { + driver, + neo4jBookmarks: req.headers['neo4jbookmark'] + }; + } +}); +``` + +## Resources + +- Blog post: [Authorization In GraphQL Using Custom Schema Directives](https://blog.grandstack.io/authorization-in-graphql-using-custom-schema-directives-eafa6f5b4658) - With Apollo’s graphql-tools, Auth0 JWTs, and neo4j-graphql.js diff --git a/docs/neo4j-graphql-js-quickstart.md b/docs/neo4j-graphql-js-quickstart.md new file mode 100644 index 00000000..aa2cb712 --- /dev/null +++ b/docs/neo4j-graphql-js-quickstart.md @@ -0,0 +1,94 @@ +# neo4j-graphql.js Quickstart + +A GraphQL to Cypher query execution layer for Neo4j and JavaScript GraphQL implementations. + +## Installation and usage + +### Install + +```shell +npm install --save neo4j-graphql-js +``` + +### Usage + +Start with GraphQL type definitions: + +```js +const typeDefs = ` +type Movie { + title: String + year: Int + imdbRating: Float + genres: [Genre] @relation(name: "IN_GENRE", direction: OUT) +} +type Genre { + name: String + movies: [Movie] @relation(name: "IN_GENRE", direction: IN) +} +`; +``` + +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: + +```js +import { makeAugmentedSchema } from 'neo4j-graphql-js'; + +const schema = makeAugmentedSchema({ typeDefs }); +``` + +Create a neo4j-javascript-driver instance: + +```js +import neo4j from 'neo4j-driver'; + +const driver = neo4j.driver( + 'bolt://localhost:7687', + neo4j.auth.basic('neo4j', 'letmein') +); +``` + +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: + +```js +import { ApolloServer } from 'apollo-server'; + +const server = new ApolloServer({ schema, context: { driver } }); + +server.listen(3003, '0.0.0.0').then(({ url }) => { + console.log(`GraphQL API ready at ${url}`); +}); +``` + +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. + +```js +import { neo4jgraphql } from 'neo4j-graphql-js'; + +const resolvers = { + Query: { + Movie(object, params, ctx, resolveInfo) { + return neo4jgraphql(object, params, ctx, resolveInfo); + } + } +}; +``` + +## Benefits + +- Send a single query to the database +- No need to write queries for each resolver +- Exposes the power of the Cypher query language through GraphQL + +## Features + +- [x] Translate basic GraphQL queries to Cypher +- [x] `first` and `offset` arguments for pagination +- [x] `@cypher` schema directive for exposing Cypher through GraphQL +- [x] Handle fragments +- [x] Ordering +- [x] Filtering +- [x] Handle interface types +- [x] Handle inline fragments +- [x] Native database temporal types (Date, DateTime, LocalDateTime) +- [x] Native Point database type diff --git a/docs/neo4j-graphql-js.md b/docs/neo4j-graphql-js.md new file mode 100644 index 00000000..495ee961 --- /dev/null +++ b/docs/neo4j-graphql-js.md @@ -0,0 +1,218 @@ +# neo4j-graphql.js User Guide + +## What is `neo4j-graphql-js` + +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. + +### Goals of neo4j-graphql.js + +- Translate GraphQL queries to Cypher to simplify the process of writing GraphQL resolvers +- Allow for custom logic by overriding of any resolver function +- Work with `graphql-tools`, `graphql-js`, and `apollo-server` +- Support GraphQL servers that need to resolve data from multiple data services/databases +- Expose the power of Cypher through GraphQL via the `@cypher` directive + +## How it works + +`neo4j-graphql-js` aims to simplify the process of building GraphQL APIs backed by Neo4j, embracing the paradigm of GraphQL First Development. Specifically, + +- The Neo4j datamodel is defined by a GraphQL schema. +- 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. +- Any resolver can be overridden by a custom resolver function implementation to allow for custom logic +- Optionally, GraphQL fields can be resolved by a user defined Cypher query through the use of the `@cypher` schema directive. + +### Start with a GraphQL schema + +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: + +```js +const typeDefs = ` +type Movie { + movieId: ID! + title: String + year: Int + plot: String + poster: String + imdbRating: Float + similar(first: Int = 3, offset: Int = 0): [Movie] @cypher(statement: "MATCH (this)-[:IN_GENRE]->(:Genre)<-[:IN_GENRE]-(o:Movie) RETURN o") + degree: Int @cypher(statement: "RETURN SIZE((this)-->())") + actors(first: Int = 3, offset: Int = 0): [Actor] @relation(name: "ACTED_IN", direction:IN) +} + +type Actor { + id: ID! + name: String + movies: [Movie] +} + + +type Query { + Movie(id: ID, title: String, year: Int, imdbRating: Float, first: Int, offset: Int): [Movie] +} +`; +``` + +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` + +- `@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. +- `@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. + +### Translate GraphQL To Cypher + +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. + +```js +import { neo4jgraphql } from 'neo4j-graphql-js'; + +const resolvers = { + // entry point to GraphQL service + Query: { + Movie(object, params, ctx, resolveInfo) { + return neo4jgraphql(object, params, ctx, resolveInfo); + } + } +}; +``` + +GraphQL to Cypher translation works by inspecting the GraphQL schema, the GraphQL query and arguments. For example, this simple GraphQL query + +```graphql +{ + Movie(title: "River Runs Through It, A") { + title + year + imdbRating + } +} +``` + +is translated into the Cypher query + +```cypher +MATCH (movie:Movie {title:"River Runs Through It, A"}) +RETURN movie { .title , .year , .imdbRating } AS movie +SKIP 0 +``` + +A slightly more complicated traversal + +```graphql +{ + Movie(title: "River Runs Through It, A") { + title + year + imdbRating + actors { + name + } + } +} +``` + +becomes + +```cypher +MATCH (movie:Movie {title:"River Runs Through It, A"}) +RETURN movie { .title , .year , .imdbRating, + actors: [(movie)<-[ACTED_IN]-(movie_actors:Actor) | movie_actors { .name }] } +AS movie +SKIP 0 +``` + +## `@cypher` directive + +> 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). + +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: + +```graphql +type Movie { + movieId: ID! + title: String + year: Int + plot: String + similar(first: Int = 3, offset: Int = 0): [Movie] + @cypher( + statement: "MATCH (this)-[:IN_GENRE]->(:Genre)<-[:IN_GENRE]-(o:Movie) RETURN o ORDER BY COUNT(*) DESC" + ) +} +``` + +The field `similar` will be resolved using the Cypher query + +```cypher +MATCH (this)-[:IN_GENRE]->(:Genre)<-[:IN_GENRE]-(o:Movie) RETURN o ORDER BY COUNT(*) DESC +``` + +to find movies with overlapping Genres. + +Querying a GraphQL field marked with a `@cypher` directive executes that query as a subquery: + +_GraphQL:_ + +```graphql +{ + Movie(title: "River Runs Through It, A") { + title + year + imdbRating + actors { + name + } + similar(first: 3) { + title + } + } +} +``` + +_Cypher:_ + +```cypher +MATCH (movie:Movie {title:"River Runs Through It, A"}) +RETURN movie { .title , .year , .imdbRating, + actors: [(movie)<-[ACTED_IN]-(movie_actors:Actor) | movie_actors { .name }], + similar: [ x IN apoc.cypher.runFirstColumn(" + WITH {this} AS this + MATCH (this)-[:IN_GENRE]->(:Genre)<-[:IN_GENRE]-(o:Movie) + RETURN o", + {this: movie}, true) | x { .title }][..3] +} AS movie +SKIP 0 +``` + +> This means that the entire GraphQL request is still resolved with a single Cypher query, and thus a single round trip to the database. + +## Query Neo4j + +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. + +```js +let driver; + +function context(headers, secrets) { + if (!driver) { + driver = neo4j.driver( + 'bolt://localhost:7687', + neo4j.auth.basic('neo4j', 'letmein') + ); + } + return { driver }; +} +``` + +```js +server.use( + '/graphql', + bodyParser.json(), + graphqlExpress(request => ({ + schema, + rootValue, + context: context(request.headers, process.env) + })) +); +``` + +## Resources + +- 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 diff --git a/docs/neo4j-multiple-database-graphql.md b/docs/neo4j-multiple-database-graphql.md new file mode 100644 index 00000000..12b835a7 --- /dev/null +++ b/docs/neo4j-multiple-database-graphql.md @@ -0,0 +1,65 @@ +# Using Multiple Neo4j Databases + +> This section describes how to use multiple Neo4j databases with neo4j-graphql.js. Multiple active databases is a feature available in Neo4j v4.x + +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`) + +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/) + +## Specifying The Neo4j Database + +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. + +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. + +For example, with Apollo Server, here we provide the database name `sanmateo`: + +```js +const neo4j = require('neo4j-driver'); +const { ApolloServer } = require('apollo-server'); + +const driver = neo4j.driver( + 'neo4j://localhost:7687', + neo4j.auth.basic('neo4j', 'letmein') +); + +const server = new ApolloServer({ + schema, + context: { driver, neo4jDatabase: 'sanmateo' } +}); + +server.listen(3004, '0.0.0.0').then(({ url }) => { + console.log(`GraphQL API ready at ${url}`); +}); +``` + +## Specifying The Neo4j Database In A Request Header + +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. + +Here we use the value of the request header `x-database` for the Neo4j database: + +```js +const neo4j = require('neo4j-driver'); +const { ApolloServer } = require('apollo-server'); + +const driver = neo4j.driver( + 'neo4j://localhost:7687', + neo4j.auth.basic('neo4j', 'letmein') +); + +const server = new ApolloServer({ + schema, + context: ({ req }) => { + return { driver, neo4jDatabase: req.header['x-database'] }; + } +}); + +server.listen(3004, '0.0.0.0').then(({ url }) => { + console.log(`GraphQL API ready at ${url}`); +}); +``` + +## Resources + +- [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 diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 00000000..156f1bac --- /dev/null +++ b/index.d.ts @@ -0,0 +1,213 @@ +// Type definitions for neo4j-graphql-js +// Project: https://github.com/neo4j-graphql/neo4j-graphql-js +// Definitions by: Nopzen +// Definitions: https://github.com/neo4j-graphql/neo4j-graphql-js.git +// TypeScript Version: 3.8 + +/* eslint no-unused-vars: "off" */ + +declare module 'neo4j-graphql-js' { + import { Driver } from 'neo4j-driver'; + import { GraphQLSchema, GraphQLFieldResolver, GraphQLResolveInfo, DocumentNode } from 'graphql'; + import { IResolvers } from 'graphql-tools'; + + /** + * makeAugmentedSchema + * @description Wraps {@link https://www.apollographql.com/docs/apollo-server/api/apollo-server/#makeExecutableSchema|makeExecutableSchema} to create a GraphQL schema from GraphQL type definitions (SDL). Will generate Query and Mutation types for the provided type definitions and attach `neo4jgraphql` as the resolver for these queries and mutations. Either a schema or typeDefs must be provided. `resolvers` can optionally be implemented to override any of the generated Query/Mutation fields. Additional options are passed through to `makeExecutableSchema`. + * + * @param {makeAugmentedSchemaOptions} options + */ + export function makeAugmentedSchema(options: makeAugmentedSchemaOptions): GraphQLSchema; + + /** + * neo4jgraphql + * @async + * @description This function's signature matches that of {@link https://graphql.org/learn/execution/#root-fields-resolvers|GraphQL resolver functions}. and thus the parameters match the parameters passed into resolve by GraphQL implementations like graphql-js. + * + * It can be called within a resolver to generate a Cypher query and handle the database call to Neo4j to completely resolve the GraphQL request. Alternatively, use `cypherQuery` or `cypherMutation` within a resolver to only generate the Cypher query and handle the database call yourself. + * @param {object} object The previous object being resolved. Rarely used for a field on the root Query type. + * @param {RequestArguments} args The arguments provided to the field in the GraphQL query. + * @param {Neo4jContext} context Value provided to every resolver and hold contextual information about the request, such as the currently logged in user, or access to a database. neo4j-graphql-js assumes a neo4j-javascript-driver instance exists in this object, under the key driver. + * @param {GraphQLResolveInfo} resolveInfo Holds field-specific information relevant to the current query as well as the GraphQL schema. + * @param {boolean} debug Specifies whether to log the generated Cypher queries for each GraphQL request. Logging is enabled by default. + */ + export function neo4jgraphql( + object: TData, + args: RequestArguments, + context: Neo4jContext, + resolveInfo: GraphQLResolveInfo, + debug?: boolean, + ): Promise; + + /** + * augmentSchema + * @description Takes an existing GraphQL schema object and adds neo4j-graphql-js specific enhancements, including auto-generated mutations and queries, and ordering and pagination fields. {@link https://grandstack.io/docs/neo4j-graphql-js|See this guide} for more information. + * + * @param {GraphQLSchema} schema + * @param {AugmentSchemaConfig} config + */ + export function augmentSchema(schema: GraphQLSchema, config: AugmentSchemaConfig): GraphQLSchema; + + type AssertSchemaOptions = { + schema: GraphQLSchema, + driver: Driver, + debug?: boolean + dropExisting?: boolean + } + + /** + * assertSchema + * @description This function uses the `@id`, `@unique` and `@index` schema directives present in the Graphql type definitions, along with `apoc.schema.assert()`, to add any database constraints and indexes. + * @param {AssertSchemaOptions} options + */ + export function assertSchema(options: AssertSchemaOptions): void; + + /** + * cypherQuery + * @description Generates a Cypher query (and associated parameters) to resolve a given GraphQL request (for a Query). Use this function when you want to handle the database call yourself, use neo4jgraphql for automated database call support. + * + * @param {RequestArguments} args + * @param {object} context + * @param {GraphQLResolveInfo} resolveInfo + */ + + export function cypherQuery(args: RequestArguments, context: any, resolveInfo: GraphQLResolveInfo): CypherResult; + + /** + * cypherMutation + * @description Similar to `cypherQuery`, but for mutations. Generates a Cypher query (and associated parameters) to resolve a given GraphQL request (for a Mutation). Use this function when you want to handle the database call yourself, use neo4jgraphql for automated database call support. + * + * @param {RequestArguments} args + * @param {object} context + * @param {GraphQLResolveInfo} resolveInfo + */ + export function cypherMutation(args: RequestArguments, context: any, resolveInfo: GraphQLResolveInfo): CypherResult; + + /** + * inferrerSchema + * @description Used to generate GraphQL type definitions from an existing Neo4j database by inspecting the data stored in the database. When used in combination with makeAugmentedSchema this can be used to generate a GraphQL CRUD API on top of an existing Neo4j database without writing any resolvers or GraphQL type definitions. See {@link https://github.com/neo4j-graphql/neo4j-graphql-js/blob/master/example/autogenerated/autogen.js|example/autogenerated/autogen.js} for an example of using `inferSchema` and `makeAugmentedSchema` with Apollo Server. + * + * @param {Driver} driver A neo4j js driver + * @param {InferSchemaOptions} options + */ + export function inferSchema(driver: Driver, options: InferSchemaOptions): Promise; + + type Neo4jContext> = T & { + driver: Driver; + }; + + /** + * InferrerSchemaOptions + * @param {boolean} alwaysIncludeRelationships specifies whether relationships should always be included in the type definitions as {@link https://grandstack.io/docs/neo4j-graphql-js#relationship-types|relationship types}, even if the relationships do not have properties. + */ + interface InferSchemaOptions { + alwaysIncludeRelationships: boolean; + } + + /** + * InferSchemaPromise + * @param {string} typeDefs a string representation of the generated GraphQL type definitions in Schema Definition Language (SDL) format, inferred from the existing Neo4j database. + */ + interface InferSchemaPromise { + typeDefs: string; + } + + type CypherResult = [string, { [key: string]: any }]; + + interface RequestArguments { + [key: string]: any; + } + + interface AugmentSchemaResolvers { + [key: string]: GraphQLFieldResolver; + } + + interface AugmentSchemaLogger { + log: (msg: string) => void; + } + + interface AugmentSchemaParseOptions { + [key: string]: any; + } + + /** + * AugmentSchemaResolverValidationOptions + * @param {boolean} requireResolversForArgs will cause `makeExecutableSchema` to throw an error if no resolver is defined for a field that has arguments. + * @param {boolean} requireResolversForNonScalar will cause makeExecutableSchema to throw an error if a non-scalar field has no resolver defined. Setting this to `true` can be helpful in catching errors, but defaults to `false` to avoid confusing behavior for those coming from other GraphQL libraries. + * @param {boolean} requireResolversForAllFields asserts that _all_ fields have valid resolvers. + * @param {boolean} requireResolversForResolveType will require a _resolveType()_ method for Interface and Union types. This can be passed in with the field resolvers as *__resolveType()*. False to disable the warning. + * @param {boolean} allowResolversNotInSchema turns off the functionality which throws errors when resolvers are found which are not present in the schema. Defaults to `false`, to help catch common errors. + */ + interface AugmentSchemaResolverValidationOptions { + requireResolversForArgs: boolean; + requireResolversForNonScalar: boolean; + requireResolversForAllFields: boolean; + requireResolversForResolveType: boolean; + allowResolversNotInSchema: boolean; + } + + type AugmentSchemaTransform = (schema: GraphQLSchema) => GraphQLSchema + interface AugmentSchemaDirectives { + [key: string]: (next: Promise, src: any, args: RequestArguments, context: any) => Promise; + } + + type DirectiveResolvers = Record any>; + + /** + * AugmentSchemaAuthConfig + * @param {boolean} isAuthenticated enables `@isAuthenticated` directive, **Optional, defaults to true** + * @param {boolean} hasRole enables `@hasRole` directive, **Optional, defaults to true** + * @param {boolean} hasScope enables `@hasScope` directive, **Optional, defaults to true** + */ + interface AugmentSchemaAuthConfig { + isAuthenticated?: boolean; + hasRole?: boolean; + hasScope?: boolean; + } + + /** + * AugmentSchemaConfig + * + * @param {boolean|object} query Configure the autogenerated Query fields. Can be enabled/disabled for all types or a list of individual types to exclude can be passed. Commonly used to exclude payload types. **Optional defaults to `true`** + * @param {boolean|object} mutation Configure the autogenerated Mutation fields. Can be enabled/disabled for all types or a list of individual types to exclude can be passed. Commonly used to exclude payload types. **Optional, defaults to `true`** + * @param {boolean} debug Enable/disable logging of generated Cypher queries and parameters. **Optional, defaults to `true`** + * @param {boolean} auth Used to enable authorization schema directives (@isAuthenticated, @hasRole, @hasScope). If enabled, directives from the graphql-auth-directives are declared and can be used in the schema. If @hasScope is enabled it is automatically added to all generated query and mutation fields. See the authorization guide for more information. **Optional, defaults to `false`** + * @param {boolean} experimental When the config.experimental boolean flag is true, input objects are generated for node property selection and input. + */ + interface AugmentSchemaConfig { + query?: boolean | { exclude: string[] }; + mutation?: boolean | { exclude: string[] }; + debug?: boolean; + auth?: boolean | AugmentSchemaAuthConfig; + experimental?: boolean + } + + /** + * makeAugmentedSchemaOptions + * @param {GraphQLSchema} schema __optional__ argument, predefined schema takes presidence over a `typeDefs` & `resolvers` combination + * @param {string} typeDefs __required__ argument, and should be an GraphQL schema language string or array of GraphQL schema language strings or a function that takes no arguments and returns an array of GraphQL schema language strings. The order of the strings in the array is not important, but it must include a schema definition. + * @param {object} resolvers __optional__ argument, _(empty object by default)_ and should be an object or an array of objects that follow the pattern explained in {@link https://www.graphql-tools.com/docs/resolvers/|article on resolvers} + * @param {object} logger __optional__ argument, which can be used to print errors to the server console that are usually swallowed by GraphQL. The logger argument should be an object with a log function, eg. `const logger = { log: e => console.log(e) }` + * @param {object} parseOptions __optional__ argument, which allows customization of parse when specifying `typeDefs` as a string. + * @param {boolean} allowUndefinedInResolve __optional__ argument, which is `true` by default. When set to `false`, causes your resolver to throw errors if they return `undefined`, which can help make debugging easier. + * @param {object} resolverValidationOptions __optional__ argument, see: _AugmentSchemaResolverValidationOptions_ + * @param {object} directiveResolvers __optional__ argument, _(null by default)_ and should be an object that follows the pattern explained in this {@link https://www.graphql-tools.com/docs/directive-resolvers /|article on directive resolvers} + * @param {object} schemaDirectives __optional__ argument, (empty object by default) and can be used to specify the {@link https://www.graphql-tools.com/docs/legacy-schema-directives/|earlier class-based implementation of schema directives} + * @param {AugmentSchemaTransform[]} schemaTransforms __optional__ argument, (empty array by default) Suport for newer functional `schemaDirectives` see the docs {@link https://www.graphql-tools.com/docs/schema-directives/#at-least-two-strategies/|(At least two strategies)} + * @param {boolean} inheritResolversFromInterfaces __optional__ argument, (false by default) GraphQL Objects that implement interfaces will inherit missing resolvers from their interface types defined in the resolvers object. + */ + interface makeAugmentedSchemaOptions { + schema?: GraphQLSchema; + typeDefs: DocumentNode | string; + resolvers?: AugmentSchemaResolvers | IResolvers; + logger?: AugmentSchemaLogger; + parseOptions?: AugmentSchemaParseOptions; + config?: AugmentSchemaConfig; + allowUndefinedInResolve?: boolean; + resolverValidationOptions?: AugmentSchemaResolverValidationOptions; + directiveResolvers?: DirectiveResolvers; + schemaDirectives?: AugmentSchemaDirectives; + schemaTransforms?: AugmentSchemaTransform[]; + inheritResolversFromInterfaces?: boolean; + } +} diff --git a/package.json b/package.json index 2dc16262..deefb211 100755 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "2.19.8", "description": "A GraphQL to Cypher query execution layer for Neo4j. ", "main": "./dist/index.js", + "types": "./index.d.ts", "scripts": { "start": "nodemon ./example/apollo-server/movies.js --exec babel-node -e js", "autogen": "nodemon ./example/autogenerated/autogen.js --exec babel-node -e js",