diff --git a/website/docs/graphql-custom-logic.mdx b/website/docs/graphql-custom-logic.mdx index e01fee6..1a6c62b 100644 --- a/website/docs/graphql-custom-logic.mdx +++ b/website/docs/graphql-custom-logic.mdx @@ -4,6 +4,7 @@ title: Adding Custom Logic To Our GraphQL API sidebar_label: Adding Custom Logic --- +import useBaseUrl from '@docusaurus/useBaseUrl'; import FeedbackWidget from "../components/FeedbackWidget"; 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`). @@ -17,7 +18,7 @@ There are two options for adding custom logic to your API using neo4j-graphql.js 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 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. +> 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 @@ -159,6 +160,488 @@ Since we are using full-text search, even though we spell "library" incorrectly, 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 +
+ +
+ 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 mutations: + +```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 +
+ +#### 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 +
+ +
+ 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. diff --git a/website/docs/graphql-filtering.mdx b/website/docs/graphql-filtering.mdx index 4aee559..ada24aa 100644 --- a/website/docs/graphql-filtering.mdx +++ b/website/docs/graphql-filtering.mdx @@ -292,11 +292,13 @@ _This table shows the fields available on the generated `filter` input type, and | | `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 | diff --git a/website/docs/graphql-schema-generation-augmentation.mdx b/website/docs/graphql-schema-generation-augmentation.mdx index 0a30d97..0baa4c5 100644 --- a/website/docs/graphql-schema-generation-augmentation.mdx +++ b/website/docs/graphql-schema-generation-augmentation.mdx @@ -214,9 +214,9 @@ For the following variant of the above schema, using the `@id`, `@unique`, and ` ```graphql type Movie { movieId: ID! @id - title: String @unique + title: String! @unique year: Int @index - imdbRating: Float + imdbRating: Float! genres: [Genre] @relation(name: "IN_GENRE", direction: "OUT") similar: [Movie] @cypher(statement: """ MATCH (this)<-[:RATED]-(:User)-[:RATED]->(s:Movie) @@ -235,8 +235,8 @@ This alternative API would be generated for the `Movie` type: ```graphql type Mutation { # Node mutations - CreateMovie(data: _MovieData!): Movie - UpdateMovie(where: _MovieWhere!, data: _MovieData!): Movie + CreateMovie(data: _MovieCreate!): Movie + UpdateMovie(where: _MovieWhere!, data: _MovieUpdate!): Movie DeleteMovie(where: _MovieWhere!): Movie # Relationship mutations AddMovieGenres(from: _MovieWhere!, to: _GenreWhere!): _AddMovieGenresPayload @@ -281,21 +281,23 @@ input _MovieWhere { year_gte: Int } ``` -#### Property Input +#### Property Creation ```graphql -input _MovieData { +input _MovieCreate { movieId: ID - title: String + title: String! year: Int - imdbRating: Float + 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). +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 } ) { @@ -312,12 +314,22 @@ mutation { } } ``` +#### 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: { @@ -344,7 +356,7 @@ mutation { 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: _MovieData!): Movie + MergeMovie(where: _MovieKeys!, data: _MovieCreate!): Movie } ``` ```graphql @@ -358,16 +370,43 @@ input _MovieKeys { mutation { MergeMovie( where: { - year: 2021 + 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 diff --git a/website/docs/guide-graphql-schema-design.mdx b/website/docs/guide-graphql-schema-design.mdx index 31ea33d..e6722ea 100644 --- a/website/docs/guide-graphql-schema-design.mdx +++ b/website/docs/guide-graphql-schema-design.mdx @@ -293,7 +293,7 @@ type Query { } ``` -> This approach can be used to define custom logic for Mutation fields as well. +This approach can be used to define custom logic for Mutation fields as well. See the [guide for custom logic](https://grandstack.io/docs/graphql-custom-logic) for further examples of the `@cypher` directive. ### Implementing Custom Resolvers diff --git a/website/docs/neo4j-graphql-js-api.mdx b/website/docs/neo4j-graphql-js-api.mdx index 09dbf92..7c22468 100644 --- a/website/docs/neo4j-graphql-js-api.mdx +++ b/website/docs/neo4j-graphql-js-api.mdx @@ -76,7 +76,7 @@ const schema = makeAugmentedSchema({ ### `assertSchema(options)`:`null` -This function uses the `@id`, `@unique`, and `@index` schema directives present in the GraphQL type definitions to add any database constraints and indexes. +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 @@ -103,6 +103,34 @@ 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. diff --git a/website/static/assets/img/BatchMergeAGraphData.png b/website/static/assets/img/BatchMergeAGraphData.png new file mode 100644 index 0000000..4b97370 Binary files /dev/null and b/website/static/assets/img/BatchMergeAGraphData.png differ diff --git a/website/static/assets/img/MergeAGraphData.png b/website/static/assets/img/MergeAGraphData.png new file mode 100644 index 0000000..009cad3 Binary files /dev/null and b/website/static/assets/img/MergeAGraphData.png differ