diff --git a/antora.yml b/antora.yml index 08f65bde..e1f5be0a 100644 --- a/antora.yml +++ b/antora.yml @@ -1,6 +1,6 @@ name: graphql-manual title: Neo4j GraphQL Library -version: '4.0' +version: '3.0' start_page: ROOT:index.adoc nav: - modules/ROOT/content-nav.adoc diff --git a/modules/ROOT/content-nav.adoc b/modules/ROOT/content-nav.adoc index 83dbafa8..9cd3025e 100644 --- a/modules/ROOT/content-nav.adoc +++ b/modules/ROOT/content-nav.adoc @@ -1,83 +1,108 @@ -* xref:index.adoc[Neo4j GraphQL Library] - -** xref:getting-started/index.adoc[] -*** xref:getting-started/toolbox.adoc[] - -** Type definitions -*** xref:type-definitions/types/index.adoc[] -**** xref:type-definitions/types/scalar.adoc[Scalar] -**** xref:type-definitions/types/temporal.adoc[Temporal] -**** xref:type-definitions/types/spatial.adoc[Spatial] -**** xref:type-definitions/types/interfaces.adoc[Interface] -**** xref:type-definitions/types/unions.adoc[Union] -**** xref:type-definitions/types/relationships.adoc[] -*** xref:type-definitions/directives/index.adoc[] -**** xref:type-definitions/directives/basics.adoc[] -**** xref:type-definitions/directives/autogeneration.adoc[] -**** xref:type-definitions/directives/custom-directives.adoc[] -**** xref:type-definitions/directives/cypher.adoc[] -**** xref:type-definitions/directives/default-values.adoc[] -**** xref:type-definitions/directives/database-mapping.adoc[] -**** xref:type-definitions/directives/indexes-and-constraints.adoc[] - -** xref:schema-configuration/index.adoc[Schema configuration] -*** xref:schema-configuration/type-configuration.adoc[] -*** xref:schema-configuration/global-configuration.adoc[] -*** xref:schema-configuration/field-configuration.adoc[] - -** xref:queries-aggregations/index.adoc[Queries and aggregations] -*** xref:queries-aggregations/queries.adoc[] -*** xref:queries-aggregations/aggregations.adoc[] -*** xref:queries-aggregations/filtering.adoc[] -*** xref:queries-aggregations/sorting.adoc[] -*** xref:queries-aggregations/pagination/index.adoc[] -**** xref:queries-aggregations/pagination/offset-based.adoc[] -**** xref:queries-aggregations/pagination/cursor-based.adoc[] - +* xref:index.adoc[] +** xref:introduction.adoc[] +** xref:getting-started.adoc[] +** xref:type-definitions/index.adoc[] +*** xref:type-definitions/basics.adoc[] +*** xref:type-definitions/types.adoc[] +*** xref:type-definitions/unions.adoc[] +*** xref:type-definitions/interfaces.adoc[] +*** xref:type-definitions/custom-directives.adoc[] +*** xref:type-definitions/relationships.adoc[] +*** xref:type-definitions/schema-configuration/index.adoc[] +**** xref:type-definitions/schema-configuration/type-configuration.adoc[] +**** xref:type-definitions/schema-configuration/global-configuration.adoc[] +**** xref:type-definitions/schema-configuration/field-configuration.adoc[] +*** xref:type-definitions/autogeneration.adoc[] +*** xref:type-definitions/cypher.adoc[] +*** xref:type-definitions/default-values.adoc[] +*** xref:type-definitions/database-mapping.adoc[] +*** xref:type-definitions/indexes-and-constraints.adoc[] +** xref:queries.adoc[] ** xref:mutations/index.adoc[] *** xref:mutations/create.adoc[] *** xref:mutations/update.adoc[] *** xref:mutations/delete.adoc[] - ** xref:subscriptions/index.adoc[] -*** xref:subscriptions/getting-started.adoc[Getting started] -*** xref:subscriptions/events.adoc[Events] +*** xref:subscriptions/getting-started.adoc[Getting Started] +*** Events +**** xref:subscriptions/events/create.adoc[Create] +**** xref:subscriptions/events/update.adoc[Update] +**** xref:subscriptions/events/delete.adoc[Delete] +**** xref:subscriptions/events/create_relationship.adoc[Create Relationship] +**** xref:subscriptions/events/delete_relationship.adoc[Delete Relationship] *** xref:subscriptions/filtering.adoc[] *** xref:subscriptions/scaling.adoc[] -*** xref:subscriptions/engines.adoc[Engines] - +*** xref:subscriptions/plugins/index.adoc[Plugins] +**** xref:subscriptions/plugins/single-instance.adoc[Single Instance] +**** xref:subscriptions/plugins/amqp.adoc[AMQP] +** xref:filtering.adoc[] +** xref:sorting.adoc[] +** xref:pagination/index.adoc[] +*** xref:pagination/offset-based.adoc[] +*** xref:pagination/cursor-based.adoc[] +** xref:mathematical-operators.adoc[Mathematical operators] +** xref:array-methods.adoc[Array methods] ** xref:custom-resolvers.adoc[] - -** xref:authentication-and-authorization/index.adoc[] +** xref:auth/index.adoc[] +*** xref:auth/setup.adoc[] +*** xref:auth/auth-directive.adoc[] +*** xref:auth/global-authentication.adoc[] +*** xref:auth/authentication.adoc[] +*** xref:auth/authorization/index.adoc[] +**** xref:auth/authorization/allow.adoc[] +**** xref:auth/authorization/bind.adoc[] +**** xref:auth/authorization/roles.adoc[] +**** xref:auth/authorization/where.adoc[] +*** xref:auth/subscriptions.adoc[] +** Authentication and Authorization *** xref:authentication-and-authorization/configuration.adoc[] *** xref:authentication-and-authorization/authentication.adoc[] *** xref:authentication-and-authorization/authorization.adoc[] -*** xref:authentication-and-authorization/impersonation-and-user-switching.adoc[] - -** xref:introspector.adoc[Introspector] - -** Migration Guides -*** xref:migration/index.adoc[Migration from `neo4j-graphql-js`] -*** xref:migration/v2-migration.adoc[] -*** xref:migration/v3-migration.adoc[] -*** xref:migration/v4-migration/index.adoc[] -**** xref:migration/v4-migration/authorization.adoc[] -**** xref:migration/v4-migration/ogm.adoc[] - +*** Reference +**** xref:authentication-and-authorization/reference/operations.adoc[] +** xref:directives.adoc[] +** xref:api-reference/index.adoc[] +*** xref:api-reference/neo4jgraphql.adoc[] +*** xref:api-reference/ogm.adoc[] ** xref:ogm/index.adoc[] *** xref:ogm/installation.adoc[] -*** xref:ogm/examples.adoc[] +*** xref:ogm/examples/index.adoc[] +**** xref:ogm/examples/custom-resolvers.adoc[] +**** xref:ogm/examples/rest-api.adoc[] *** xref:ogm/private.adoc[] *** xref:ogm/selection-set.adoc[] *** xref:ogm/type-generation.adoc[] -*** xref:ogm/reference.adoc[] - -*** xref:driver-configuration.adoc[] - -** Frameworks and integrations -*** xref:integrations/apollo-federation.adoc[] -*** xref:integrations/nextjs.adoc[] - -** xref:troubleshooting.adoc[] - +*** xref:ogm/api-reference/index.adoc[] +**** xref:ogm/api-reference/ogm.adoc[] +**** xref:ogm/api-reference/type-generation.adoc[] +**** xref:ogm/api-reference/model/index.adoc[] +***** xref:ogm/api-reference/model/create.adoc[] +***** xref:ogm/api-reference/model/find.adoc[] +***** xref:ogm/api-reference/model/update.adoc[] +***** xref:ogm/api-reference/model/delete.adoc[] +***** xref:ogm/api-reference/model/aggregate.adoc[] +** xref:introspector.adoc[Introspector] +** xref:toolbox.adoc[GraphQL Toolbox] +** xref:driver-configuration.adoc[] +** xref:guides/index.adoc[] +*** xref:guides/apollo-federation.adoc[] +*** xref:guides/frameworks/nextjs.adoc[] +*** xref:guides/migration-guide/index.adoc[] +**** xref:guides/migration-guide/server.adoc[] +**** xref:guides/migration-guide/type-definitions.adoc[] +**** xref:guides/migration-guide/queries.adoc[] +**** xref:guides/migration-guide/mutations.adoc[] +*** xref:guides/v2-migration/index.adoc[] +**** xref:guides/v2-migration/mutations.adoc[] +**** xref:guides/v2-migration/unions.adoc[] +**** xref:guides/v2-migration/miscellaneous.adoc[] +*** xref:guides/v3-migration/index.adoc[] +*** xref:guides/v4-migration/index.adoc[] +**** xref:guides/v4-migration/authorization.adoc[] +** xref:troubleshooting/index.adoc[] +*** xref:troubleshooting/faqs.adoc[] +*** xref:troubleshooting/security.adoc[] +*** xref:troubleshooting/optimizing-create-operations.adoc[] +** xref:appendix/index.adoc[] +*** xref:appendix/preventing-overfetching.adoc[] ** xref:deprecations.adoc[Deprecations] diff --git a/modules/ROOT/examples/example.csv b/modules/ROOT/examples/example.csv deleted file mode 100644 index 1932aa0d..00000000 --- a/modules/ROOT/examples/example.csv +++ /dev/null @@ -1,2 +0,0 @@ -Field1,Field2 -Value1,Value2 \ No newline at end of file diff --git a/modules/ROOT/images/apollo-server-landing-page.png b/modules/ROOT/images/apollo-server-landing-page.png index bc964271..f4af49d8 100644 Binary files a/modules/ROOT/images/apollo-server-landing-page.png and b/modules/ROOT/images/apollo-server-landing-page.png differ diff --git a/modules/ROOT/images/first-mutation.png b/modules/ROOT/images/first-mutation.png new file mode 100644 index 00000000..a223eee3 Binary files /dev/null and b/modules/ROOT/images/first-mutation.png differ diff --git a/modules/ROOT/images/neo4j-aura-dashboard.png b/modules/ROOT/images/neo4j-aura-dashboard.png deleted file mode 100644 index f68cefb6..00000000 Binary files a/modules/ROOT/images/neo4j-aura-dashboard.png and /dev/null differ diff --git a/modules/ROOT/images/subscriptions/diagram1.png b/modules/ROOT/images/subscriptions/diagram1.png new file mode 100644 index 00000000..634c3950 Binary files /dev/null and b/modules/ROOT/images/subscriptions/diagram1.png differ diff --git a/modules/ROOT/images/subscriptions/diagram1.svg b/modules/ROOT/images/subscriptions/diagram1.svg deleted file mode 100644 index 9c918712..00000000 --- a/modules/ROOT/images/subscriptions/diagram1.svg +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/modules/ROOT/images/subscriptions/diagram2.png b/modules/ROOT/images/subscriptions/diagram2.png new file mode 100644 index 00000000..3cb8345e Binary files /dev/null and b/modules/ROOT/images/subscriptions/diagram2.png differ diff --git a/modules/ROOT/images/subscriptions/diagram2.svg b/modules/ROOT/images/subscriptions/diagram2.svg deleted file mode 100644 index 0ce8a6f5..00000000 --- a/modules/ROOT/images/subscriptions/diagram2.svg +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/modules/ROOT/images/subscriptions/diagram3.png b/modules/ROOT/images/subscriptions/diagram3.png new file mode 100644 index 00000000..c47e73c8 Binary files /dev/null and b/modules/ROOT/images/subscriptions/diagram3.png differ diff --git a/modules/ROOT/images/subscriptions/diagram3.svg b/modules/ROOT/images/subscriptions/diagram3.svg deleted file mode 100644 index 28d35606..00000000 --- a/modules/ROOT/images/subscriptions/diagram3.svg +++ /dev/null @@ -1,79 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/modules/ROOT/images/toolbox-editor-view-v2.png b/modules/ROOT/images/toolbox-editor-view-v2.png new file mode 100644 index 00000000..52452089 Binary files /dev/null and b/modules/ROOT/images/toolbox-editor-view-v2.png differ diff --git a/modules/ROOT/images/toolbox-editor-view.png b/modules/ROOT/images/toolbox-editor-view.png deleted file mode 100644 index 784933de..00000000 Binary files a/modules/ROOT/images/toolbox-editor-view.png and /dev/null differ diff --git a/modules/ROOT/images/toolbox-favorite.png b/modules/ROOT/images/toolbox-favorite.png deleted file mode 100644 index f6d3b060..00000000 Binary files a/modules/ROOT/images/toolbox-favorite.png and /dev/null differ diff --git a/modules/ROOT/images/toolbox-schema-view-v2.png b/modules/ROOT/images/toolbox-schema-view-v2.png new file mode 100644 index 00000000..b57c19f0 Binary files /dev/null and b/modules/ROOT/images/toolbox-schema-view-v2.png differ diff --git a/modules/ROOT/images/toolbox-schema-view.png b/modules/ROOT/images/toolbox-schema-view.png deleted file mode 100644 index 5f0985b7..00000000 Binary files a/modules/ROOT/images/toolbox-schema-view.png and /dev/null differ diff --git a/modules/ROOT/pages/api-reference/index.adoc b/modules/ROOT/pages/api-reference/index.adoc new file mode 100644 index 00000000..8f8ba7d3 --- /dev/null +++ b/modules/ROOT/pages/api-reference/index.adoc @@ -0,0 +1,6 @@ +[[api-reference]] += API Reference + +- xref::api-reference/neo4jgraphql.adoc[`Neo4jGraphQL`] +- xref::api-reference/ogm.adoc[`@neo4j/graphql-ogm`] + diff --git a/modules/ROOT/pages/reference/api-reference/neo4jgraphql.adoc b/modules/ROOT/pages/api-reference/neo4jgraphql.adoc similarity index 50% rename from modules/ROOT/pages/reference/api-reference/neo4jgraphql.adoc rename to modules/ROOT/pages/api-reference/neo4jgraphql.adoc index 8cf6cb23..6f185341 100644 --- a/modules/ROOT/pages/reference/api-reference/neo4jgraphql.adoc +++ b/modules/ROOT/pages/api-reference/neo4jgraphql.adoc @@ -29,10 +29,174 @@ Accepts all of the options from https://www.graphql-tools.com/docs/generate-sche Type: https://neo4j.com/docs/javascript-manual/current/[`Driver`] |An instance of a Neo4j driver. +|`config` + + + + Type: xref::api-reference/neo4jgraphql.adoc#api-reference-neo4jgraphql-input-neo4jgraphqlconfig[`Neo4jGraphQLConfig`] +|Additional Neo4j GraphQL configuration options. + |`features` + + - Type: xref::reference/api-reference/neo4jgraphql.adoc#api-reference-neo4jgraphql-input-neo4jfeaturessettings[`Neo4jFeaturesSettings`] + Type: xref::api-reference/neo4jgraphql.adoc#api-reference-neo4jgraphql-input-neo4jfeaturessettings[`Neo4jFeaturesSettings`] |Could be used for configure/enable/disable specific features. + +|`plugins` + + + + Type: xref::api-reference/neo4jgraphql.adoc#api-reference-neo4jgraphql-input-neo4jgraphqlplugins[`Neo4jGraphQLPlugins`] +|Plugins for the Neo4j GraphQL Library. +|=== + +[[api-reference-neo4jgraphql-input-neo4jgraphqlconfig]] +==== `Neo4jGraphQLConfig` + +|=== +|Name and Type |Description + +|`driverConfig` + + + + Type: xref::api-reference/neo4jgraphql.adoc#api-reference-neo4jgraphql-input-neo4jgraphqlconfig-driverconfig[`DriverConfig`] +|Additional driver configuration options. + +|`enableRegex` + + + + Type: `boolean` +|Whether to enable RegEx filters, see xref::filtering.adoc#filtering-regex[RegEx matching] for more information. + +|`queryOptions` + + + + Type: xref::api-reference/neo4jgraphql.adoc#api-reference-neo4jgraphql-input-neo4jgraphqlconfig-cypherqueryoptions[`CypherQueryOptions`] +|Cypher query options, see xref::troubleshooting/index.adoc#troubleshooting-query-tuning[Query Tuning] for more information. + +|`startupValidation` + + + + Type: xref::api-reference/neo4jgraphql.adoc#api-reference-neo4jgraphql-input-neo4jgraphqlconfig-StartupValidationOptions[`StartupValidationOptions`] or `boolean` +|Whether or not startup validation checks should be run. A boolean can be used to enable/disable all startup checks. Alternatively, a `StartupValidationOptions` object can be used for fine grain controls. If nothing is passed, all checks will be run. + +|`skipValidateTypeDefs` + + + + Type: `boolean` +|*`skipValidateTypeDefs` has been deprecated and will be removed in 4.0.0. Please use `startupValidation` instead.* + + + +Can be used to disable strict type definition validation if you are encountering unexpected errors. + +|=== + +[[api-reference-neo4jgraphql-input-neo4jgraphqlconfig-driverconfig]] +===== `DriverConfig` + +|=== +|Name and Type |Description + +|`database` + + + + Type: `string` +|The name of the database within the DBMS to connect to. + +|`bookmarks` + + + + Type: `string` or `Array` +|One or more bookmarks to use for the connection. +|=== + +[[api-reference-neo4jgraphql-input-neo4jgraphqlconfig-cypherqueryoptions]] +===== `CypherQueryOptions` + +All options are enum types imported from `@neo4j/graphql`, for example: + +[source, javascript, indent=0] +---- +const { CypherRuntime } = require("@neo4j/graphql"); +---- + +|=== +|Name and Type |Description + +|`runtime` + + + + Type: `CypherRuntime` +|Possible options: + + + + - `CypherRuntime.INTERPRETED` + + - `CypherRuntime.SLOTTED` + + - `CypherRuntime.PIPELINED` + +|`planner` + + + + Type: `CypherPlanner` +|Possible options: + + + + - `CypherPlanner.COST` + + - `CypherPlanner.IDP` + + - `CypherPlanner.DP` + +|`connectComponentsPlanner` + + + + Type: `CypherConnectComponentsPlanner` +|Possible options: + + + + - `CypherConnectComponentsPlanner.GREEDY` + + - `CypherConnectComponentsPlanner.IDP` + +|`updateStrategy` + + + + Type: `CypherUpdateStrategy` +|Possible options: + + + + - `CypherUpdateStrategy.DEFAULT` + + - `CypherUpdateStrategy.EAGER` + +|`expressionEngine` + + + + Type: `CypherExpressionEngine` +|Possible options: + + + + - `CypherExpressionEngine.DEFAULT` + + - `CypherExpressionEngine.INTERPRETED` + + - `CypherExpressionEngine.COMPILED` + +|`operatorEngine` + + + + Type: `CypherOperatorEngine` +|Possible options: + + + + - `CypherOperatorEngine.DEFAULT` + + - `CypherOperatorEngine.INTERPRETED` + + - `CypherOperatorEngine.COMPILED` + +|`interpretedPipesFallback` + + + + Type: `CypherInterpretedPipesFallback` +|Possible options: + + + + - `CypherInterpretedPipesFallback.DEFAULT` + + - `CypherInterpretedPipesFallback.DISABLED` + + - `CypherInterpretedPipesFallback.WHITELISTED_PLANS_ONLY` + + - `CypherInterpretedPipesFallback.ALL` + +|`replan` + + + + Type: `CypherReplanning` +|Possible options: + + + + - `CypherReplanning.DEFAULT` + + - `CypherReplanning.FORCE` + + - `CypherReplanning.SKIP` +|=== + +[[api-reference-neo4jgraphql-input-neo4jgraphqlconfig-StartupValidationOptions]] +===== `StartupValidationOptions` + +|=== +|Name and Type |Description + +|`typeDefs` + + + + Type: `boolean` +|Can be used to disable strict type definition validation. + +|`resolvers` + + + + Type: `boolean` +|Can be used to disable checks that expected custom resolvers have been provided. |=== [[api-reference-neo4jgraphql-input-neo4jfeaturessettings]] @@ -43,7 +207,7 @@ Accepts all of the options from https://www.graphql-tools.com/docs/generate-sche |`filters` + + - Type: xref::reference/api-reference/neo4jgraphql.adoc#api-reference-neo4jgraphql-input-neo4jfilterssettings[`Neo4jFiltersSettings`] + Type: xref::api-reference/neo4jgraphql.adoc#api-reference-neo4jgraphql-input-neo4jfilterssettings[`Neo4jFiltersSettings`] |Additional configuration for filters. |=== @@ -55,11 +219,11 @@ Accepts all of the options from https://www.graphql-tools.com/docs/generate-sche |`String` + + - Type: xref::reference/api-reference/neo4jgraphql.adoc#api-reference-neo4jgraphql-input-neo4jstringfilterssettings[`Neo4jStringFiltersSettings`] + Type: xref::api-reference/neo4jgraphql.adoc#api-reference-neo4jgraphql-input-neo4jstringfilterssettings[`Neo4jStringFiltersSettings`] |Additional configuration for String filters. |`ID` + + - Type: xref::reference/api-reference/neo4jgraphql.adoc#api-reference-neo4jgraphql-input-neo4jidfilterssettings[`Neo4jIDFiltersSettings`] + Type: xref::api-reference/neo4jgraphql.adoc#api-reference-neo4jgraphql-input-neo4jidfilterssettings[`Neo4jIDFiltersSettings`] |Additional configuration for String filters. |=== @@ -158,7 +322,7 @@ Accepts the arguments below: |`driverConfig` + + - Type: xref::reference/api-reference/neo4jgraphql.adoc#api-reference-checkneo4jcompat-input-driverconfig[`DriverConfig`] + Type: xref::api-reference/neo4jgraphql.adoc#api-reference-checkneo4jcompat-input-driverconfig[`DriverConfig`] |Additional driver configuration options. |=== @@ -234,12 +398,12 @@ Accepts the arguments below: |`driverConfig` + + - Type: xref::reference/api-reference/neo4jgraphql.adoc#api-reference-assertconstraints-input-driverconfig[`DriverConfig`] + Type: xref::api-reference/neo4jgraphql.adoc#api-reference-assertconstraints-input-driverconfig[`DriverConfig`] |Additional driver configuration options. |`options` + + - Type: xref::reference/api-reference/neo4jgraphql.adoc#api-reference-assertconstraints-input-assertconstraintsoptions[`AssertConstraintsOptions`] + Type: xref::api-reference/neo4jgraphql.adoc#api-reference-assertconstraints-input-assertconstraintsoptions[`AssertConstraintsOptions`] |Options for the execution of `assertIndexesAndConstraints`. |=== @@ -271,3 +435,16 @@ Accepts the arguments below: Type: `boolean` |Whether or not to create constraints if they do not yet exist. Disabled by default. |=== + +[[api-reference-neo4jvalidategraphqldocument]] +== `neo4jValidateGraphQLDocument` + +A synchronous method to verify if the type definitions provided are valid for usage with the `@neo4j/graphql` library. Usage example: + +[source, javascript, indent=0] +---- +const neoSchema = new Neo4jGraphQL({ typeDefs: "type User { name: Strin }" }); +const { isValid, validationErrors } = neoSchema.neo4jValidateGraphQLDocument(); +console.log(isValid) // false +console.log(validationErrors) // ['Unknown type "Strin". Did you mean "String"?'] +---- diff --git a/modules/ROOT/pages/reference/api-reference/ogm.adoc b/modules/ROOT/pages/api-reference/ogm.adoc similarity index 50% rename from modules/ROOT/pages/reference/api-reference/ogm.adoc rename to modules/ROOT/pages/api-reference/ogm.adoc index a7b396cf..00b918b0 100644 --- a/modules/ROOT/pages/reference/api-reference/ogm.adoc +++ b/modules/ROOT/pages/api-reference/ogm.adoc @@ -1,4 +1,4 @@ [[api-reference-ogm]] = `@neo4j/graphql-ogm` -See xref::ogm/reference.adoc[`OGM`]. +See xref::ogm/api-reference/ogm.adoc[`OGM`]. diff --git a/modules/ROOT/pages/appendix/index.adoc b/modules/ROOT/pages/appendix/index.adoc new file mode 100644 index 00000000..62c63ffe --- /dev/null +++ b/modules/ROOT/pages/appendix/index.adoc @@ -0,0 +1,4 @@ +[[appendix]] += Appendix + +- xref::appendix/preventing-overfetching.adoc[Preventing overfetching] - details on the steps taken to prevent overfetching when dealing with unions and interfaces. diff --git a/modules/ROOT/pages/appendix/preventing-overfetching.adoc b/modules/ROOT/pages/appendix/preventing-overfetching.adoc new file mode 100644 index 00000000..3d650bab --- /dev/null +++ b/modules/ROOT/pages/appendix/preventing-overfetching.adoc @@ -0,0 +1,47 @@ +[[appendix-preventing-overfetching]] += Preventing overfetching + +When querying for unions and interfaces in Cypher, each union member/interface implementation is broken out into a subquery and joined with `UNION`. For example, using one of the examples above, when we query with no `where` argument, each subquery has a similar structure: + +[source, cypher, indent=0] +---- +CALL { + WITH this + OPTIONAL MATCH (this)-[has_content:HAS_CONTENT]->(blog:Blog) + RETURN { __resolveType: "Blog", title: blog.title } +UNION + WITH this + OPTIONAL MATCH (this)-[has_content:HAS_CONTENT]->(journal:Post) + RETURN { __resolveType: "Post" } +} +---- + +Now if you were to leave both subqueries and add a `WHERE` clause for blogs, it would look like this: + +[source, cypher, indent=0] +---- +CALL { + WITH this + OPTIONAL MATCH (this)-[has_content:HAS_CONTENT]->(blog:Blog) + WHERE blog.title IS NOT NULL + RETURN { __resolveType: "Blog", title: blog.title } +UNION + WITH this + OPTIONAL MATCH (this)-[has_content:HAS_CONTENT]->(journal:Post) + RETURN { __resolveType: "Post" } +} +---- + +As you can see, the subqueries are now "unbalanced", which could result in massive overfetching of `Post` nodes. + +So, when a `where` argument is passed in, only union members which are in the `where` object are fetched, so it is essentially acting as a logical OR gate, different from the rest of the `where` arguments in the schema: + +[source, cypher, indent=0] +---- +CALL { + WITH this + OPTIONAL MATCH (this)-[has_content:HAS_CONTENT]->(blog:Blog) + WHERE blog.title IS NOT NULL + RETURN { __resolveType: "Blog", title: blog.title } +} +---- diff --git a/modules/ROOT/pages/array-methods.adoc b/modules/ROOT/pages/array-methods.adoc new file mode 100644 index 00000000..1004e45d --- /dev/null +++ b/modules/ROOT/pages/array-methods.adoc @@ -0,0 +1,237 @@ +[[array-methods]] += Array methods + +Array methods allow us to modify existing property arrays in Update mutations within these entities: + +* `Node` +* `Nested Nodes` +* `Relationship properties` +* `Interfaces` + +The following operators are available: + +* `_POP` +* `_PUSH` + +The POP operator expects a single Int value as input + +The PUSH operator conforms to the type of input defined in the type definition. + +== Examples + +=== Array pop +Suppose we have the following type definition, a Movie with a property array called tags: + +[source, graphql, indent=0] +---- +type Movie { + title: String + tags: [String] +} +---- + +We can pop from this `tags` property array. + +Before: `['a', 'b', 'c']` + +After: `['a', 'b']` + + +[source, graphql, indent=0] +---- +mutation { + updateMovies (update: { tags_POP: 1 }) { + movies { + title + tags + } + } +} +---- + +Or, for more than one property from the array: + +Before: `['a', 'b', 'c']` + +After: `['a']` + +[source, graphql, indent=0] +---- +mutation { + updateMovies (update: { tags_POP: 2 }) { + movies { + title + tags + } + } +} +---- + +Similarly, you can have multiple array property fields and update them in the same query: + + +[source, graphql, indent=0] +---- +type Movie { + title: String + tags: [String] + moreTags: [String] +} +---- + +We can pop from both the `tags` and `moreTags` property arrays. + + +Before: +``` + tags: ['a', 'b', 'c'] + moreTags: ['x', 'y', 'z'] +``` + +After: +``` + tags: ['a', 'b'] + moreTags: ['x'] +``` + +[source, graphql, indent=0] +---- +mutation { + updateMovies (update: { tags_POP: 1, moreTags_POP: 2 }) { + movies { + title + tags + moreTags + } + } +} +---- + +=== Array push +Suppose we have the following type definition, a Movie with a property array called tags: + +[source, graphql, indent=0] +---- +type Movie { + title: String + tags: [String] +} +---- + +We can push to this `tags` property array. + + +Before: `['some tag']` + +After: `['some tag', 'another tag']` + +[source, graphql, indent=0] +---- +mutation { + updateMovies (update: { tags_PUSH: "another tag" }) { + movies { + title + tags + } + } +} +---- + +Or push multiple elements in a single update: + +Before: `['some tag']` + +After: `['some tag', 'another tag', 'one more tag']` + +[source, graphql, indent=0] +---- +mutation { + updateMovies (update: { tags_PUSH: ["another tag", "one more tag"] }) { + movies { + title + tags + } + } +} +---- + +Similarly, you can have multiple array property fields and update them in the same query: + +[source, graphql, indent=0] +---- +type Movie { + title: String + tags: [String] + moreTags: [String] +} +---- + +We can push to both the `tags` and `moreTags` property arrays. + +Before: +``` + tags: ['some tag'] + moreTags: [] +``` + +After: +``` + tags: ['some tag', 'another tag'] + moreTags ['a different tag'] +``` + +[source, graphql, indent=0] +---- +mutation { + updateMovies (update: { tags_PUSH: "another tag", moreTags_PUSH: "a different tag" }) { + movies { + title + tags + moreTags + } + } +} +---- + +=== Array push and pop in one update + +It is possible to perform both a push and pop operation in one Update mutation. + +Suppose we have the following type definition, a Movie with a property array called tags: + +[source, graphql, indent=0] +---- +type Movie { + title: String + tags: [String] + moreTags: [String] +} +---- + +We can then update both property arrays with either _POP or _PUSH operators. + +Before: +``` + tags: ['some tag'] + moreTags: [] +``` + +After: +``` + tags: [] + moreTags ['a different tag'] +``` + +[source, graphql, indent=0] +---- +mutation { + updateMovies (update: { tags_POP: 1, moreTags_PUSH: "a different tag" }) { + movies { + title + tags + moreTags + } + } +} +---- + diff --git a/modules/ROOT/pages/auth/auth-directive.adoc b/modules/ROOT/pages/auth/auth-directive.adoc new file mode 100644 index 00000000..2a5d9ccb --- /dev/null +++ b/modules/ROOT/pages/auth/auth-directive.adoc @@ -0,0 +1,63 @@ +[[auth-directive]] += `@auth` directive + +WARNING: The `@auth` directive has been replaced by `@authentication` and `@authorization`. `@auth` will be removed in version 4.0.0. +Please see the xref::guides/v4-migration/authorization.adoc[upgrade guide] for details on how to upgrade. + +The `@auth` directive definition is dynamically generated on runtime based on user type definitions. + +== `rules` + +You can have many rules for many operations. Each rule is fallen through until a match is found against the corresponding operation. If no match is found, an error is thrown. You can think of rules as a big `OR`. + +[source, graphql, indent=0] +---- +@auth(rules: [ + { operations: [CREATE, UPDATE], ... }, ## or + { operations: [READ, UPDATE], ...}, ## or + { operations: [DELETE, UPDATE], ... } ## or +]) +---- + +== `operations` + +`operations` is an array which allows you to re-use the same rule for many operations. + +[source, graphql, indent=0] +---- +@auth(rules: [ + { operations: [CREATE, UPDATE, DELETE, CONNECT, DISCONNECT, SUBSCRIBE] }, + { operations: [READ] } +]) +---- + +NOTE: Note that the absence of an `operations` argument will imply _all_ operations. + +Many different operations can be called at once, for example in the following mutation: + +[source, graphql, indent=0] +---- +mutation { + createPosts( + input: [ + { + content: "I like GraphQL", + creator: { connect: { where: { id: "user-01" } } } + } + ] + ) { + posts { + content + } + } +} +---- + +In the above example, there is a `CREATE` operation followed by a `CONNECT`, so the auth rule must allow a user to perform both of these operations. + +== Auth Value Plucking + +When using the `@auth` directive, you use the following prefixes to substitute in their relevant values: + +- `$jwt.` - pulls value from JWT +- `$context.` - pulls value from context diff --git a/modules/ROOT/pages/auth/authentication.adoc b/modules/ROOT/pages/auth/authentication.adoc new file mode 100644 index 00000000..354c54ca --- /dev/null +++ b/modules/ROOT/pages/auth/authentication.adoc @@ -0,0 +1,47 @@ +[[auth-authentication]] += Authentication + +WARNING: The `@auth` directive has been replaced by `@authentication` and `@authorization`. `@auth` will be removed in version 4.0.0. +Please see the xref::guides/v4-migration/authorization.adoc[upgrade guide] for details on how to upgrade. + +The Neo4j GraphQL Library expects an `authorization` header in the request object, which means you can authenticate users however you like. You could have a custom sign-in mutation, integrate with Auth0, or roll your own SSO server. The point here is that it’s just a JWT which the library decodes to make sure it’s valid - but it’s down to the user to issue tokens. + +> The example at xref::ogm/examples/custom-resolvers.adoc[Custom Resolvers] demonstrates a hypothetical sign-up/sign-in flow using the xref::ogm/index.adoc[OGM], which will be a good starting point for inspiration. + +== `isAuthenticated` + +This is the most basic of authentication, used to ensure that there is a valid decoded JWT in the request. The most basic of type definitions could look something like the following, which states you must be authenticated to access `Todo` objects: + +[source, graphql, indent=0] +---- +type Todo { + id: ID + title: String +} + +extend type Todo @auth(rules: [{ isAuthenticated: true }]) +---- + +== `allowUnauthenticated` + +In some cases, you may want to allow unauthenticated requests while also having auth-based rules. You can use the `allowUnauthenticated` parameter to avoid throwing an exception if no auth is present in the context. + +In the example below, only the publisher can see his blog posts if it is not published yet. Once the blog post is published, anyone can see it: + +[source, graphql, indent=0] +---- +type BlogPost + @auth( + rules: [ + { + operations: [READ] + where: { OR: [{ publisher: "$jwt.sub" }, { published: true }] } + allowUnauthenticated: true + } + ] + ) { + id: ID! + publisher: String! + published: Boolean! +} +---- diff --git a/modules/ROOT/pages/auth/authorization/allow.adoc b/modules/ROOT/pages/auth/authorization/allow.adoc new file mode 100644 index 00000000..7dce0fc2 --- /dev/null +++ b/modules/ROOT/pages/auth/authorization/allow.adoc @@ -0,0 +1,144 @@ +[[auth-authorization-allow]] += Allow + +WARNING: The `@auth` directive has been replaced by `@authentication` and `@authorization`. `@auth` will be removed in version 4.0.0. +Please see the xref::guides/v4-migration/authorization.adoc[upgrade guide] for details on how to upgrade. + +Use `allow` to ensure that on matched nodes, there is equality between a value on the JWT and a property on each matched node. Taking a closer look, create two users in a hypothetical empty database: + +[source, cypher, indent=0] +---- +CREATE (:User { id: "user1", name: "one" }) +CREATE (:User { id: "user2", name: "two" }) +---- + +For the label and properties of the nodes created above, the corresponding GraphQL type definition would be: + +[source, graphql, indent=0] +---- +type User { + id: ID! + name: String! +} +---- + +Now that there are two users in the database, and a simple type definition - it might be desirable to restrict `user1` from accessing `user2`. This is where `allow` comes in: + +[source, graphql, indent=0] +---- +type User { + id: ID! + name: String! +} + +extend type User @auth( + rules: [ + { + operations: [READ], + allow: { id: "$jwt.sub" } + } + ] +) +---- + +After a match is made against a node, it is validated that the property `id` on the node is equal to the `jwt.sub` property. + +Given `user1` has the following decoded JWT: + +[source, json, indent=0] +---- +{ + "sub": "user1", + "iat": 1516239022 +} +---- + +If "user1" used this JWT in a request for "user2": + +[source, graphql, indent=0] +---- +query { + users(where: { id: "user2" }) { + name + } +} +---- + +The generated cypher for this query would look like the following and throw you out the operation: + +[source, cypher, indent=0] +---- +MATCH (u:User { id: "user2" }) +CALL apoc.util.validate(NOT (u.id = "user1"), "Forbidden") +RETURN u +---- + +Allow is available on the following operations: + +- `READ` +- `UPDATE` +- `CONNECT` +- `DISCONNECT` +- `DELETE` + +== `allow` across relationships + +There may be a reason where you need to traverse across relationships to satisfy your authorization implementation. One example use case could be "grant update access to all Moderators of a Post": + +[source, graphql, indent=0] +---- +type User { + id: ID + name: String +} + +type Post { + content: String + moderators: [User!]! @relationship(type: "MODERATES_POST", direction: IN) +} + +extend type Post @auth(rules: [ + { operations: [UPDATE], allow: { moderators: { id: "$jwt.sub" } } } +]) +---- + +When you specify allow on a relationship you can select fields on the referenced node. It's worth pointing out that allow on a relationship will perform an `ANY` on the matched nodes to see if there is a match. + +Given the above example - There may be a time when you need to give update access to either the creator of a post or a moderator, you can use `OR` and `AND` inside `allow`: + +[source, graphql, indent=0] +---- +type User { + id: ID + name: String +} + +type Post { + content: String + moderators: [User!]! @relationship(type: "MODERATES_POST", direction: IN) + creator: User! @relationship(type: "HAS_POST", direction: IN) +} + +extend type Post + @auth( + rules: [ + { + operations: [UPDATE], + allow: { OR: [{ moderators: { id: "$jwt.sub" } }, { creator: { id: "$jwt.sub" } }] } + } + ] + ) +---- + +== Field-level `allow` + +`allow` works the same as it does on Types although its context is the Field. So instead of enforcing auth rules when the node is matched and/or modified, it would instead be called when the Field is match and/or modified. Given the following, it is hiding the password to all users but the user themselves: + +[source, graphql, indent=0] +---- +type User { + id: ID! + name: String! + password: String! @auth(rules: [{ allow: { id: "$jwt.sub" } }]) +} +---- diff --git a/modules/ROOT/pages/auth/authorization/bind.adoc b/modules/ROOT/pages/auth/authorization/bind.adoc new file mode 100644 index 00000000..32b036d2 --- /dev/null +++ b/modules/ROOT/pages/auth/authorization/bind.adoc @@ -0,0 +1,119 @@ +[[auth-authorization-bind]] += Bind + +WARNING: The `@auth` directive has been replaced by `@authentication` and `@authorization`. `@auth` will be removed in version 4.0.0. +Please see the xref::guides/v4-migration/authorization.adoc[upgrade guide] for details on how to upgrade. + +Use bind to ensure that on creating or updating nodes, there is equality between a value on the JWT and a property on a matched node. This validation is done after the operation but inside a transaction. Taking a closer look, create a user in your database: + +[source, cypher, indent=0] +---- +CREATE (:User { id:"user1", name: "one" }) +---- + +For the label and properties of the node created above, the corresponding GraphQL type definitions would be: + +[source, graphql, indent=0] +---- +type User { + id: ID! + name: String! +} +---- + +Given the above GraphQL type definition - you could restrict `user1` from changing their own ID: + +[source, graphql, indent=0] +---- +type User { + id: ID! + name: String! +} + +extend type User @auth( + rules: [ + { + operations: [UPDATE], + bind: { id: "$jwt.sub" } + } + ] +) +---- + +After the update or creation of the node, it is validated that the property `id` on the node is equal to the `jwt.sub` property. + +Given `user1` has the following decoded JWT: + +[source, json, indent=0] +---- +{ + "sub": "user1", + "iat": 1516239022 +} +---- + +When the user makes a request using this JWT to change their ID: + +[source, graphql, indent=0] +---- +mutation { + updateUsers(where: { id: "user1" }, update: { id: "user2" }) { + users { + name + } + } +} +---- + +The generated cypher for this query would look like the below, throwing you out of the operation because the `id` property no longer matches. + +[source, cypher, indent=0] +---- +MATCH (u:User { id: "user1" }) +SET u.id = "user2" +CALL apoc.util.validate(NOT (u.id = "user1"), "Forbidden") +RETURN u +---- + +Bind is available for the following operations; + +- `READ` +- `UPDATE` +- `CONNECT` +- `DISCONNECT` +- `DELETE` + +== `bind` across relationships + +There may be a reason where you need to traverse across relationships to satisfy your authorization implementation. One use case could be "ensure that users only create Posts related to themselves": + +[source, graphql, indent=0] +---- +type User { + id: ID + name: String +} + +type Post { + content: String + creator: User! @relationship(type: "HAS_POST", direction: IN) +} + +extend type Post @auth(rules: [ + { operations: [CREATE], bind: { creator: { id: "$jwt.sub" } } } +]) +---- + +When you specify `bind` on a relationship you can select fields on the related node. It's worth pointing out that `bind` on a relationship field will perform an `all` on the matched nodes to see if there is a match, or `any` if the `bindPredicate` option of the plugin has been set to "any". + +=== Field-level `bind` + +You can use `bind` on a field, and the root is still considered the node itself. Taking the example at the start of this chapter, you could do the following to implement the same behaviour: + +[source, graphql, indent=0] +---- +type User { + id: ID! @auth(rules: [{ operations: [UPDATE], bind: { id: "$jwt.sub" } }]) + name: String! +} +---- diff --git a/modules/ROOT/pages/auth/authorization/index.adoc b/modules/ROOT/pages/auth/authorization/index.adoc new file mode 100644 index 00000000..46d1ec20 --- /dev/null +++ b/modules/ROOT/pages/auth/authorization/index.adoc @@ -0,0 +1,14 @@ +[[auth-authorization]] += Authorization + +WARNING: The `@auth` directive has been replaced by `@authentication` and `@authorization`. `@auth` will be removed in version 4.0.0. +Please see the xref::guides/v4-migration/authorization.adoc[upgrade guide] for details on how to upgrade. + +You specify authorization rules inside the `@auth` directive. This section looks at each option available and explains how to use it to implement authorization. + +- xref::auth/authorization/allow.adoc[Allow] +- xref::auth/authorization/bind.adoc[Bind] +- xref::auth/authorization/roles.adoc[Roles] +- xref::auth/authorization/where.adoc[Where] + +NOTE: Note that authorization rules are not supported when applied over nested xref::type-definitions/unions.adoc[unions] or xref::type-definitions/interfaces.adoc[interfaces]. diff --git a/modules/ROOT/pages/auth/authorization/roles.adoc b/modules/ROOT/pages/auth/authorization/roles.adoc new file mode 100644 index 00000000..604a7af7 --- /dev/null +++ b/modules/ROOT/pages/auth/authorization/roles.adoc @@ -0,0 +1,50 @@ +[[auth-authorization-roles]] += Roles + +WARNING: The `@auth` directive has been replaced by `@authentication` and `@authorization`. `@auth` will be removed in version 4.0.0. +Please see the xref::guides/v4-migration/authorization.adoc[upgrade guide] for details on how to upgrade. + +Use the `roles` property to specify the allowed roles for an operation. Use the `Neo4jGraphQL` config option `rolesPath` to specify a object path for JWT roles otherwise defaults to `jwt.roles`. + +The following type definitions show that an admin role is required for all update operations against Users. + +[source, graphql, indent=0] +---- +type User { + id: ID + name: String +} + +extend type User @auth(rules: [{ operations: [UPDATE], roles: ["admin"] }]) +---- + +If there are multiple possible roles you can add more items to the array, of which users only need one to satisfy a rule: + +[source, graphql, indent=0] +---- +extend type User @auth(rules: [{ operations: [UPDATE], roles: ["admin", "super-admin"] }]) +---- + +== RBAC + +Here is an example of RBAC (Role-Based Access Control) using `roles`: + +[source, graphql, indent=0] +---- +type CatalogItem @auth(rules: [{ operations: [READ], roles: ["read:catalog"] }]) { + id: ID + title: String +} + +type Customer @auth(rules: [{ operations: [READ], roles: ["read:customer"] }]) { + id: ID + name: String + password: String @auth(rules: [{ operations: [READ], roles: ["admin"] }]) +} + +type Invoice @auth(rules: [{ operations: [READ], roles: ["read:invoice"] }]) { + id: ID + csv: String + total: Int +} +---- diff --git a/modules/ROOT/pages/auth/authorization/where.adoc b/modules/ROOT/pages/auth/authorization/where.adoc new file mode 100644 index 00000000..88f430dc --- /dev/null +++ b/modules/ROOT/pages/auth/authorization/where.adoc @@ -0,0 +1,102 @@ +[[auth-authorization-where]] += Where + +WARNING: The `@auth` directive has been replaced by `@authentication` and `@authorization`. `@auth` will be removed in version 4.0.0. +Please see the xref::guides/v4-migration/authorization.adoc[upgrade guide] for details on how to upgrade. + +Use the `where` argument on types to conceptually append predicates to the Cypher `WHERE` clause. Given the current user ID is "123" and the following schema: + +[source, graphql, indent=0] +---- +type User { + id: ID + name: String +} + +extend type User @auth(rules: [{ where: { id: "$jwt.id" } }]) +---- + +Then the user executes a GraphQL query for all users: + +[source, graphql, indent=0] +---- +query { + users { + id + name + } +} +---- + +Behind the scenes the user’s ID is conceptually added to the query: + +[source, graphql, indent=0] +---- +query { + users(where: { id: "123" }){ + id + name + } +} +---- + +Where is used on the following operations; + +- `READ` +- `UPDATE` +- `CONNECT` +- `DISCONNECT` +- `DELETE` + + +== Combining `where` with `roles` + +The `where` argument can be combined with `roles` within auth rules to support rule based filtering of results. + +Revising the early example schema, if you update the auth rules to instead provide two sets of rules for two different roles, you can specify that JWTs with the `user` role can only see their own `User` node, as before, but now, those with the `admin` role can see all users: + +[source, graphql, indent=0] +---- +type User { + id: ID + name: String +} + +extend type User @auth(rules: [ + { + roles: ["user"] + where: { id: "$jwt.id" } + } + { + roles: ["admin"] + } +]) +---- + +For those with an admin role, there is a conceptual `where: "*"` rule which is applied, allowing admins to see all `User` nodes. + +These rules can alternatively be expressed using an `OR` within the top parent of a single auth rule, for example: + +[source, graphql, indent=0] +---- +type User { + id: ID + name: String +} + +extend type User @auth(rules: [ + { + OR: [ + { + roles: ["user"] + where: { id: "$jwt.id" } + }, + { + roles: ["admin"] + } + ] + } +]) +---- + +Both ways of expressing the rules are valid, and will return the same results. diff --git a/modules/ROOT/pages/auth/global-authentication.adoc b/modules/ROOT/pages/auth/global-authentication.adoc new file mode 100644 index 00000000..7fd6c8bb --- /dev/null +++ b/modules/ROOT/pages/auth/global-authentication.adoc @@ -0,0 +1,51 @@ +[[auth-global-authentication]] += Global Authentication + +WARNING: The `@auth` directive has been replaced by `@authentication` and `@authorization`. `@auth` will be removed in version 4.0.0. +Please see the xref::guides/v4-migration/authorization.adoc[upgrade guide] for details on how to upgrade. + +For some cases the GraphQL API needs to be secured globally to restrict access to _any_ of the top-level GraphQL types without prior authentication. In the Neo4j GraphQL Library this is referred to as global authentication. It is also known as API-wide authorization. + +== Configuration + +To use the global authentication functionality, it is required to have an instance of an auth plugin for the Neo4j GraphQL Library. For most use cases you will only need to use our provided plugins at `@neo4j/graphql-plugin-auth`. Below is an example configuration enabling global authentication via the `Neo4jGraphQLAuthJWTPlugin` class: + +[source, javascript, indent=0] +---- +import { Neo4jGraphQL } from "@neo4j/graphql"; +import { Neo4jGraphQLAuthJWTPlugin } from "@neo4j/graphql-plugin-auth"; + +const neoSchema = new Neo4jGraphQL({ + typeDefs, + plugins: { + auth: new Neo4jGraphQLAuthJWTPlugin({ + secret: "super-secret", + globalAuthentication: true, + }) + } +}); +---- + +NOTE: Observe that the `Neo4jGraphQLAuthJWTPlugin` class does not accept to enable both `noVerify` and `globalAuthentication` simultaneously. + +If you would like to use JWKS decoding and enable global authentication then use the `Neo4jGraphQLAuthJWKSPlugin` class like so: + +[source, javascript, indent=0] +---- +import { Neo4jGraphQL } from "@neo4j/graphql"; +import { Neo4jGraphQLAuthJWKSPlugin } from "@neo4j/graphql-plugin-auth"; + +const neoSchema = new Neo4jGraphQL({ + typeDefs, + plugins: { + auth: new Neo4jGraphQLAuthJWKSPlugin({ + jwksEndpoint: "https://YOUR_DOMAIN/well-known/jwks.json", + globalAuthentication: true, + }) + } +}); +---- + +== Functionality + +If global authentication is enabled in the auth plugin for the Neo4j GraphQL Library, it is required that _each_ request contains a valid JWT token in the `authorization` header. Otherwise an authentication error will be thrown. diff --git a/modules/ROOT/pages/auth/index.adoc b/modules/ROOT/pages/auth/index.adoc new file mode 100644 index 00000000..2ecdaa5b --- /dev/null +++ b/modules/ROOT/pages/auth/index.adoc @@ -0,0 +1,58 @@ +[[auth]] += Auth (deprecated) + +WARNING: The `@auth` directive has been replaced by `@authentication` and `@authorization`. `@auth` will be removed in version 4.0.0. +Please see the xref::guides/v4-migration/authorization.adoc[upgrade guide] for details on how to upgrade. + +In this chapter you will learn more about how to secure your GraphQL API using the Neo4j GraphQL Library's built-in auth mechanics. + +- xref::auth/setup.adoc[Setup] +- xref::auth/auth-directive.adoc[`@auth` directive] +- xref::auth/global-authentication.adoc[Global authentication] +- xref::auth/authentication.adoc[Authentication] +- xref::auth/authorization/index.adoc[Authorization] +- xref::auth/subscriptions.adoc[Subscriptions] + +== Quickstart examples + +Only authenticated users can create Post nodes: + +[source, graphql, indent=0] +---- +type Post @auth(rules: [ + { operations: [CREATE], isAuthenticated: true } +]) { + title: String! +} +---- + +Use `extend` to avoid large and unwieldy type definitions: + +[source, graphql, indent=0] +---- +type Post { + title: String! +} + +extend type Post @auth(rules: [ + { operations: [CREATE], isAuthenticated: true } +]) +---- + +You can use the directive types as seen in the example above, but you can also apply the directive on any field so as long as it's not decorated with `@relationship`. In the following example, the password field is only accessible to users with role "admin", or the user themselves: + +[source, graphql, indent=0] +---- +type User { + id: ID! + name: String! +} + +extend type User { + password: String! @auth(rules: [ + { + OR: [{ roles: ["admin"] }, { allow: { id: "$jwt.sub" } }] + } + ]) +} +---- diff --git a/modules/ROOT/pages/auth/setup.adoc b/modules/ROOT/pages/auth/setup.adoc new file mode 100644 index 00000000..2515db4c --- /dev/null +++ b/modules/ROOT/pages/auth/setup.adoc @@ -0,0 +1,295 @@ +[[auth-setup]] += Setup + +WARNING: The `@auth` directive has been replaced by `@authentication` and `@authorization`. `@auth` will be removed in version 4.0.0. +Please see the xref::guides/v4-migration/authorization.adoc[upgrade guide] for details on how to upgrade. + +== Configuration + +To get started with auth you need an instance of an auth plugin for the Neo4j GraphQL Library. For most use cases you will only need to use our provided plugins at `@neo4j/graphql-plugin-auth`. Below is a basic example using the `Neo4jGraphQLAuthJWTPlugin` class: + +[source, javascript, indent=0] +---- +import { Neo4jGraphQL } from "@neo4j/graphql"; +import { Neo4jGraphQLAuthJWTPlugin } from "@neo4j/graphql-plugin-auth"; + +const neoSchema = new Neo4jGraphQL({ + typeDefs, + plugins: { + auth: new Neo4jGraphQLAuthJWTPlugin({ + secret: "super-secret" + }) + } +}); +---- + +Or you can initiate the secret with a function which will run to retrieve the secret when the request comes in. + +[source, javascript, indent=0] +---- +import { Neo4jGraphQL } from "@neo4j/graphql"; +import { Neo4jGraphQLAuthJWTPlugin } from "@neo4j/graphql-plugin-auth"; + +const neoSchema = new Neo4jGraphQL({ + typeDefs, + plugins: { + auth: new Neo4jGraphQLAuthJWTPlugin({ + secret: (req) => { + return "super-secret"; + }, + }) + } +}); +---- + +If you would like to use JWKS decoding then use the `Neo4jGraphQLAuthJWKSPlugin` class: + +[source, javascript, indent=0] +---- +import { Neo4jGraphQL } from "@neo4j/graphql"; +import { Neo4jGraphQLAuthJWKSPlugin } from "@neo4j/graphql-plugin-auth"; + +const neoSchema = new Neo4jGraphQL({ + typeDefs, + plugins: { + auth: new Neo4jGraphQLAuthJWKSPlugin({ + jwksEndpoint: "https://YOUR_DOMAIN/well-known/jwks.json", + }) + } +}); +---- + +Or you can pass a function as `jskwsEndpoint` to compute the endpoint when the request comes in. + +[source, javascript, indent=0] +---- +import { Neo4jGraphQL } from "@neo4j/graphql"; +import { Neo4jGraphQLAuthJWKSPlugin } from "@neo4j/graphql-plugin-auth"; + +const neoSchema = new Neo4jGraphQL({ + typeDefs, + plugins: { + auth: new Neo4jGraphQLAuthJWKSPlugin({ + jwksEndpoint: (req) => { + let url = "https://YOUR_DOMAIN/well-known/{file}.json"; + const fileHeader = req.headers["file"]; + url = url.replace("{file}", fileHeader); + return url; + }, + }), + } +}); +---- + +If you need to create your own auth plugin then ensure it adheres to the following interface: + +[source, javascript, indent=0] +---- +interface Neo4jGraphQLAuthPlugin { + rolesPath?: string; + isGlobalAuthenticationEnabled?: boolean; + + decode(token: string | any): Promise; +} +---- + +It is also possible to pass in JWTs which have already been decoded, in which case the `jwt` option is _not necessary_. This is covered in the section xref::auth/setup.adoc#auth-setup-passing-in[Passing in JWTs] below. Note that the plugin's base decode method only supports HS256 and RS256 algorithms. + +=== Optional Constructor Arguments + +Additionally, for both the `Neo4jGraphQLAuthJWTPlugin` and the `Neo4jGraphQLAuthJWKSPlugin` one may optionally specify an `issuer` and an `audience` constructor argument. These reference to the `iss` and the `aud` claim respectively in a JWT token, see https://www.rfc-editor.org/rfc/rfc7519#page-9[JWT RFC]. +If the `issuer` and/or the `audience` arguments are provided, they will be checked as part of the token verification. + +An example on how to provide an `issuer` for the `Neo4jGraphQLAuthJWKSPlugin` is shown below. In this case, any JWT token must contain the `iss` claim `"https://YOUR_DOMAIN"` for the token validation to be successful. +[source, javascript, indent=0] +---- +import { Neo4jGraphQL } from "@neo4j/graphql"; +import { Neo4jGraphQLAuthJWKSPlugin } from "@neo4j/graphql-plugin-auth"; + +const neoSchema = new Neo4jGraphQL({ + typeDefs, + plugins: { + auth: new Neo4jGraphQLAuthJWKSPlugin({ + jwksEndpoint: "https://YOUR_DOMAIN/well-known/jwks.json", + issuer: "https://YOUR_DOMAIN" + }), + }, +}); +---- + +=== Auth Roles Object Paths + +If you are using a 3rd party auth provider such as Auth0 you may find your roles property being nested inside an object: + +[source, json, indent=0] +---- +{ + "https://auth0.mysite.com/claims": { + "https://auth0.mysite.com/claims/roles": ["admin"] + } +} +---- + +In order to make use of this, you must pass it in as a "dot path" into the `rolesPath` option: + +[source, javascript, indent=0] +---- +const neoSchema = new Neo4jGraphQL({ + typeDefs, + plugins: { + auth: new Neo4jGraphQLAuthJWKSPlugin({ + jwksEndpoint: "https://YOUR_DOMAIN/well-known/jwks.json", + rolesPath: "https://auth0\\.mysite\\.com/claims.https://auth0\\.mysite\\.com/claims/roles" + }) + } +}); +---- + +Note that `.` characters within a key of the JWT must be escaped with `\\`, whilst a `.` character indicating traversal into a value must not be escaped. + +=== Cypher predicate used to evaluate `bind` rules + +By default, `bind` rules are evaluated using an `all` predicate in Cypher, which can lead to rules not being satisfied when they perhaps should, for instance only one related user matching the current JWT, rather than all of them. + +To avoid a breaking change to a security-critical feature like authorization, a flag, `bindPredicate`, has been exposed to switch this predicate to `any`, which can be used as follows: + +[source, javascript, indent=0] +---- +import { Neo4jGraphQL } from "@neo4j/graphql"; +import { Neo4jGraphQLAuthJWTPlugin } from "@neo4j/graphql-plugin-auth"; + +const neoSchema = new Neo4jGraphQL({ + typeDefs, + plugins: { + auth: new Neo4jGraphQLAuthJWTPlugin({ + secret: "super-secret", + bindPredicate: "any" + }) + } +}); +---- + +In the next major release, this will become the default behaviour when evaluating `bind` rules. + +[[auth-setup-passing-in]] +== Passing in JWTs + +If you wish to pass in an encoded JWT, this must be included in the `authorization` header of your requests, in the format: + +[source] +---- +POST / HTTP/1.1 +authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJyb2xlcyI6WyJ1c2VyX2FkbWluIiwicG9zdF9hZG1pbiIsImdyb3VwX2FkbWluIl19.IY0LWqgHcjEtOsOw60mqKazhuRFKroSXFQkpCtWpgQI +content-type: application/json +---- + +Note the string "Bearer" before the inclusion of the JWT. + +Then, using Apollo Server as an example, you must include the request in the GraphQL context, as follows (using the `neoSchema` instance from the example above): + +[source, javascript, indent=0] +---- +const server = new ApolloServer({ + schema: await neoSchema.getSchema(), +}); + +await startStandaloneServer(server, { + context: async ({ req }) => ({ req }), +}); +---- + +Note that the request key `req` is appropriate for Express servers, but different middlewares use different keys for request objects. You can more details at https://www.apollographql.com/docs/apollo-server/api/apollo-server/#middleware-specific-context-fields. + +=== Decoded JWTs + +Alternatively, you can pass a key `jwt` of type `JwtPayload` into the context, which has the following definition: + +[source, typescript, indent=0] +---- +// standard claims https://datatracker.ietf.org/doc/html/rfc7519#section-4.1 +interface JwtPayload { + [key: string]: any; + iss?: string | undefined; + sub?: string | undefined; + aud?: string | string[] | undefined; + exp?: number | undefined; + nbf?: number | undefined; + iat?: number | undefined; + jti?: string | undefined; +} +---- + +_Do not_ pass in the header or the signature. + +For example, you might have a function `decodeJWT` which returns a decoded JWT: + +[source, javascript, indent=0] +---- +const decodedJWT = decodeJWT(encodedJWT) + +const server = new ApolloServer({ + schema: await neoSchema.getSchema(), +}); + +await startStandaloneServer(server, { + context: async ({ req }) => ({ req, jwt: decodedJWT.payload }), +}); +---- + +== Auth and Custom Resolvers + +You can't use the `@auth` directive on custom resolvers, however, an auth parameter is injected into the context for use in them. It will be available under the `auth` property. For example, the following custom resolver returns the `sub` field from the JWT: + +[source, javascript, indent=0] +---- +const typeDefs = `#graphql + type Query { + myId: ID! + } +`; + +const resolvers = { + Query: { + myId(_source, _args, context) { + return context.auth.jwt.sub + } + } +}; +---- + +== Auth and `@cypher` fields + +You can put the `@auth` directive on a field alongside the `@cypher` directive. Functionality like `allow` and `bind` will not work but you can still utilize `isAuthenticated` and `roles`. Additionally, you don't need to specify `operations` for `@auth` directives on `@cypher` fields. + +The following example uses the `isAuthenticated` rule to ensure a user is authenticated, before returning the `User` associated with the JWT: + +[source, graphql, indent=0] +---- +type User @exclude { + id: ID + name: String +} + +type Query { + me: User + @cypher(statement: "MATCH (u:User { id: $auth.jwt.sub }) RETURN u") + @auth(rules: [{ isAuthenticated: true }]) +} +---- + +In the following example, the current user must have role "admin" in order to query the `history` field on the type `User`: + +[source, graphql, indent=0] +---- +type History @exclude { + website: String! +} + +type User { + id: ID + name: String + history: [History] + @cypher(statement: "MATCH (this)-[:HAS_HISTORY]->(h:History) RETURN h") + @auth(rules: [{ roles: ["admin"] }]) +} +---- diff --git a/modules/ROOT/pages/auth/subscriptions.adoc b/modules/ROOT/pages/auth/subscriptions.adoc new file mode 100644 index 00000000..7224f594 --- /dev/null +++ b/modules/ROOT/pages/auth/subscriptions.adoc @@ -0,0 +1,32 @@ +[[subscriptions]] += Subscriptions + +WARNING: The `@auth` directive has been replaced by `@authentication` and `@authorization`. `@auth` will be removed in version 4.0.0. +Please see the xref::guides/v4-migration/authorization.adoc[upgrade guide] for details on how to upgrade. + +xref::subscriptions/index.adoc[Subscriptions] can be used along with `@auth`, however, some operations are not supported. To setup rules, +use the `SUBSCRIBE` operation. + +```graphql +type Movie { + title: String! +} + +extend type Movie @auth(rules: [{ isAuthenticated: true, operations: [SUBSCRIBE] }]) +``` + +== Authentication +If the authentication rules `isAuthenticated` and `allowUnauthenticated` are not met, the subscription request will fail and no events will +be sent. + +== Roles +Roles can be set for subscriptions. Only requests matching the roles set will be accepted. + +== Bind +NOTE: Not Supported + +== Where +NOTE: Not Supported + +== Allow +NOTE: Not Supported diff --git a/modules/ROOT/pages/authentication-and-authorization/configuration.adoc b/modules/ROOT/pages/authentication-and-authorization/configuration.adoc index a0741108..8ef502ec 100644 --- a/modules/ROOT/pages/authentication-and-authorization/configuration.adoc +++ b/modules/ROOT/pages/authentication-and-authorization/configuration.adoc @@ -24,6 +24,7 @@ const { url } = await startStandaloneServer(server, { }), }); ---- +Note the `context` function: Specify the `authorization` in the `req.headers` as all lowercase despite the HTTP header's initial capital letter. Optionally, if a custom decoding mechanism is required, that same header can be decoded and the resulting JWT payload put into the `jwt` field of the context. diff --git a/modules/ROOT/pages/authentication-and-authorization/impersonation-and-user-switching.adoc b/modules/ROOT/pages/authentication-and-authorization/impersonation-and-user-switching.adoc deleted file mode 100644 index 5ec4da36..00000000 --- a/modules/ROOT/pages/authentication-and-authorization/impersonation-and-user-switching.adoc +++ /dev/null @@ -1,198 +0,0 @@ -= Impersonation and user switching - -Impersonation and user switching are features of the Neo4j database and driver which allow for query execution in a different context to the initial connection. - -== Impersonation - -Impersonation still authenticates with the database as the original configured user, but runs the query in the context of an impersonated user. -When impersonating a user, the query is run within the complete security context of the impersonated user and not the authenticated user (home database, permissions, etc.). - -An example of how to impersonate a different user per request can be found below. In this example, the user to impersonate is taken from a HTTP header `User`: - -.TypeScript -[%collapsible] -==== -[source, typescript, indent=0] ----- -import { ApolloServer } from "@apollo/server"; -import { startStandaloneServer } from "@apollo/server/standalone"; -import { Neo4jGraphQL, Neo4jGraphQLContext } from "@neo4j/graphql"; -import neo4j from "neo4j-driver"; - -const typeDefs = `#graphql - type Movie { - title: String! - } -`; - -const driver = neo4j.driver( - "neo4j://localhost:7687", - neo4j.auth.basic("neo4j", "password") -); - -const neo4jgraphql = new Neo4jGraphQL({ - typeDefs, - driver, -}); - -const schema = await neo4jgraphql.getSchema(); - -const server = new ApolloServer({ - schema, -}); - -const { url } = await startStandaloneServer(server, { - // Your async context function should async and return an object - context: async ({ req }) => ({ - sessionConfig: { - impersonatedUser: req.headers.user, - }, - }), -}); - -console.log(`🚀 Server ready at: ${url}`); ----- -==== - -.JavaScript -[%collapsible] -==== -[source, javascript, indent=0] ----- -import { ApolloServer } from "@apollo/server"; -import { startStandaloneServer } from "@apollo/server/standalone"; -import { Neo4jGraphQL } from "@neo4j/graphql"; -import neo4j from "neo4j-driver"; - -const typeDefs = `#graphql - type Movie { - title: String! - } -`; - -const driver = neo4j.driver( - "neo4j://localhost:7687", - neo4j.auth.basic("neo4j", "password") -); - -const neo4jgraphql = new Neo4jGraphQL({ - typeDefs, - driver, -}); - -const schema = await neo4jgraphql.getSchema(); - -const server = new ApolloServer({ - schema, -}); - -const { url } = await startStandaloneServer(server, { - // Your async context function should async and return an object - context: async ({ req }) => ({ - sessionConfig: { - impersonatedUser: req.headers.user, - }, - }), -}); - -console.log(`🚀 Server ready at: ${url}`); ----- -==== - -== User switching - -User switching completely switches the user authenticating with the database for the given session, without the performance cost of instantiating an entire new driver instance. - -An example of configuring user switching on a per request basis can be found in the example below. Note that the username and password are provided in HTTP headers `User` and `Password`, but this would not be recommended for production use: - -.TypeScript -[%collapsible] -==== -[source, typescript, indent=0] ----- -import { ApolloServer } from "@apollo/server"; -import { startStandaloneServer } from "@apollo/server/standalone"; -import { Neo4jGraphQL, Neo4jGraphQLContext } from "@neo4j/graphql"; -import neo4j from "neo4j-driver"; - -const typeDefs = `#graphql - type Movie { - title: String! - } -`; - -const driver = neo4j.driver( - "neo4j://localhost:7687", - neo4j.auth.basic("neo4j", "password") -); - -const neo4jgraphql = new Neo4jGraphQL({ - typeDefs, - driver, -}); - -const schema = await neo4jgraphql.getSchema(); - -const server = new ApolloServer({ - schema, -}); - -const { url } = await startStandaloneServer(server, { - // Your async context function should async and return an object - context: async ({ req }) => ({ - sessionConfig: { - auth: neo4j.auth.basic(req.headers.user, req.headers.password), - }, - }), -}); - -console.log(`🚀 Server ready at: ${url}`); ----- -==== - -.JavaScript -[%collapsible] -==== -[source, javascript, indent=0] ----- -import { ApolloServer } from "@apollo/server"; -import { startStandaloneServer } from "@apollo/server/standalone"; -import { Neo4jGraphQL } from "@neo4j/graphql"; -import neo4j from "neo4j-driver"; - -const typeDefs = `#graphql - type Movie { - title: String! - } -`; - -const driver = neo4j.driver( - "neo4j://localhost:7687", - neo4j.auth.basic("neo4j", "password") -); - -const neo4jgraphql = new Neo4jGraphQL({ - typeDefs, - driver, -}); - -const schema = await neo4jgraphql.getSchema(); - -const server = new ApolloServer({ - schema, -}); - -const { url } = await startStandaloneServer(server, { - // Your async context function should async and return an object - context: async ({ req }) => ({ - sessionConfig: { - auth: neo4j.auth.basic(req.headers.user, req.headers.password), - }, - }), -}); - -console.log(`🚀 Server ready at: ${url}`); ----- -==== - - diff --git a/modules/ROOT/pages/authentication-and-authorization/index.adoc b/modules/ROOT/pages/authentication-and-authorization/index.adoc deleted file mode 100644 index 99293536..00000000 --- a/modules/ROOT/pages/authentication-and-authorization/index.adoc +++ /dev/null @@ -1,12 +0,0 @@ -= Authentication and Authorization - -[WARNING] -==== -The `@auth` directive has been replaced by `@authentication` and `@authorization`. -See the xref::migration/v4-migration/authorization.adoc[Migration guide] for details on how to upgrade. -==== - -* xref::authentication-and-authorization/authentication.adoc[Authentication] - Explicit authentication, configured using the `@authentication` directive. -* xref::authentication-and-authorization/authorization.adoc[Authorization] - Authorization rules set using the `@authorization` directive. -* xref::authentication-and-authorization/configuration.adoc[Configuration] - Instructions to set up instantiation. -* xref::authentication-and-authorization/reference/operations.adoc[Operations] - Reference on GraphQL queries and how each location in each query triggers the evaluation of different authentication/authorization rules. \ No newline at end of file diff --git a/modules/ROOT/pages/custom-resolvers.adoc b/modules/ROOT/pages/custom-resolvers.adoc index f38e0b0f..acf9539c 100644 --- a/modules/ROOT/pages/custom-resolvers.adoc +++ b/modules/ROOT/pages/custom-resolvers.adoc @@ -5,9 +5,6 @@ The library will autogenerate query and mutation resolvers, so you don’t need == Custom object type field resolver -[[custom-resolver-directive]] -=== `@customResolver` - If you would like to add a field to an object type which is resolved from existing values in the type, rather than storing new values, you should mark it with the `@customResolver` directive (see below) and define a custom resolver for it. Take for instance a simple schema: [source, javascript, indent=0] @@ -16,7 +13,7 @@ const typeDefs = ` type User { firstName: String! lastName: String! - fullName: String! @customResolver(requires: "firstName lastName") + fullName: String! @customResolver(requires: ["firstName", "lastName"]) } `; @@ -38,123 +35,25 @@ Here `fullName` is a value that is resolved from the fields `firstName` and `las The inclusion of the fields `firstName` and `lastName` in the `requires` argument means that in the definition of the resolver, the properties `firstName` and `lastName` will always be defined on the `source` object. If these fields are not specified, this cannot be guaranteed. -==== Definition - -[source, graphql, indent=0] ----- -"""Informs @neo4j/graphql that a field will be resolved by a custom resolver, and allows specification of any field dependencies.""" -directive @customResolver( - """Selection set of the fields that the custom resolver will depend on. These fields are passed as an object to the first argument of the custom resolver.""" - requires: SelectionSet -) on FIELD_DEFINITION ----- +[[custom-resolver-directive]] +=== `@customResolver` -==== The `requires` argument +This field will essentially be completely ignored during the generation of query and mutation fields, and will require a custom resolver to resolve the field. Any fields that the custom resolver depends on should be passed to the `requires` argument to ensure that during the Cypher generation process those properties are selected from the database. +Allowable fields are any returning a Scalar or Enum type including those defined using the xref::type-definitions/cypher.adoc#type-definitions-cypher[`@cypher`] directive. -Any field can be required, as long as it is not another `@customResolver` field. - -The `requires` argument accepts a selection set string. Using a selection set string makes it possible to select fields from related types as below: - -[source, javascript, indent=0] ----- -const typeDefs = ` - type Address { - houseNumber: Int! - street: String! - city: String! - } - - type User { - id: ID! - firstName: String! - lastName: String! - address: Address! @relationship(type: "LIVES_AT", direction: OUT) - fullName: String - @customResolver(requires: "firstName lastName address { city street }") - } -`; - -const resolvers = { - User: { - fullName({ firstName, lastName, address }) { - return `${firstName} ${lastName} from ${address.street} in ${address.city}`; - }, - }, -}; - -const neoSchema = new Neo4jGraphQL({ - typeDefs, - resolvers, -}); ----- - -In this example, the `firstName`, `lastName`, `address.street` and `address.city` fields will always be selected from the database if the `fullName` field is selected and will be available to the custom resolver. - -It is also possible to inline fragments to conditionally select fields from interface/union types: - -[source, graphql, indent=0] ----- -interface Publication { - publicationYear: Int! -} - -type Author { - name: String! - publications: [Publication!]! @relationship(type: "WROTE", direction: OUT) - publicationsWithAuthor: [String!]! - @customResolver( - requires: "name publications { publicationYear ...on Book { title } ... on Journal { subject } }" - ) -} - -type Book implements Publication { - title: String! - publicationYear: Int! - author: [Author!]! @relationship(type: "WROTE", direction: IN) -} - -type Journal implements Publication { - subject: String! - publicationYear: Int! - author: [Author!]! @relationship(type: "WROTE", direction: IN) -} ----- - -It is **not** possible to require extra fields generated by the library such as aggregations and connections. -For example, the type definitions below would throw an error as they attempt to require the `publicationsAggregate`: +==== Definition [source, graphql, indent=0] ---- -interface Publication { - publicationYear: Int! -} - -type Author { - name: String! - publications: [Publication!]! @relationship(type: "WROTE", direction: OUT) - publicationsWithAuthor: [String!]! - @customResolver( - requires: "name publicationsAggregate { count }" - ) -} - -type Book implements Publication { - title: String! - publicationYear: Int! - author: [Author!]! @relationship(type: "WROTE", direction: IN) -} - -type Journal implements Publication { - subject: String! - publicationYear: Int! - author: [Author!]! @relationship(type: "WROTE", direction: IN) -} +"""Informs @neo4j/graphql that a field will be resolved by a custom resolver, and allows specification of any field dependencies.""" +directive @customResolver( + """Fields that the custom resolver will depend on.""" + requires: [String!] +) on FIELD_DEFINITION ---- -==== Providing custom resolvers - Note that any field marked with the `@customResolver` directive, requires a custom resolver to be defined. If the directive is marked on an interface, any implementation of that interface requires a custom resolver to be defined. Take for example this schema: @@ -171,7 +70,7 @@ type User implements UserInterface { } ---- -The following resolvers definition would cause a warning to be logged: +The following resolvers definition would be invalid: [source, javascript, indent=0] ---- @@ -184,8 +83,7 @@ const resolvers = { }; ---- -The following resolvers definition would silence the warning: - +Instead, the following resolvers definition would be required: [source, javascript, indent=0] ---- const resolvers = { @@ -197,4 +95,41 @@ const resolvers = { }; ---- -Mismatches between the resolver map and `@customResolver` directives will always be logged to the console as a warning. +These checks may not always be required or desirable. If this is the case, they can be disabled using the `startupValidation` config option: + +[source, javascript, indent=0] +---- +const neoSchema = new Neo4jGraphQL({ + typeDefs, + config: { + startupValidation: { + resolvers: false + }, + }, +}) +---- + +[[custom-resolvers-computed]] +=== `@computed` + +NOTE: The `@computed` directive has been deprecated and will be removed in version 4.0. Please use the xref::custom-resolvers.adoc#custom-resolver-directive[`@customResolver` directive] instead. + +This field will essentially be completely ignored during the generation of query and mutation fields, and will require another way to resolve the field, such as through the use of a custom resolver. + +Any fields that the custom resolver depends on should be passed to the `from` argument to ensure that during the Cypher generation process those properties are selected from the database. +Allowable fields are any returning a Scalar or Enum type including those defined using the xref::type-definitions/cypher.adoc#type-definitions-cypher[`@cypher`] directive. + +==== Definition + +[source, graphql, indent=0] +---- +"""Informs @neo4j/graphql that a field will be resolved by a custom resolver, and allows specification of any field dependencies.""" +directive @computed( + """Fields that the custom resolver will depend on.""" + from: [String!] +) on FIELD_DEFINITION +---- + +== Custom query/mutation type field resolver + +You can define additional custom query and mutation fields in your type definitions and provide custom resolvers for them. A prime use case for this is using the xref::ogm/index.adoc[OGM] to manipulate types and fields which are not available through the API. You can find an example of it being used in this capacity in the xref::ogm/examples/custom-resolvers.adoc[Custom Resolvers] example. diff --git a/modules/ROOT/pages/deprecations.adoc b/modules/ROOT/pages/deprecations.adoc index cbfb4152..2895e8e6 100644 --- a/modules/ROOT/pages/deprecations.adoc +++ b/modules/ROOT/pages/deprecations.adoc @@ -1,31 +1,60 @@ [[Deprecations]] -:description: This section lists the products and applications that were deprecated from the Neo4j GraphQL Library. + + = Deprecations The following products and applications are deprecated: -- xref:deprecations.adoc#_grandstack_starter_app[GRANDstack starter app] -- xref:deprecations.adoc#_neo4j_graphql_architect[Neo4j GraphQL Architect] +- GRANDstack starter app +- GraphQL Architect (a https://neo4j.com/developer/graph-apps/[Neo4j Desktop Graph App]) + + +== Why have we deprecated the GRANDstack starter app? + +The GRANDstack starter app has been a marketing and Neo4j Labs effort to drive user adoption of the Neo4j Labs GraphQL library at its early stages. + +Its main purpose was to demonstrate how the Neo4j Labs GraphQL library could be used in the context of a full-stack application using React and Apollo +client. It was extremely useful when building proof of concept applications. + +It was very appealing to developers who didn't want to have additional boiler plate when setting up their application, speeding up development time and helped focussing on building the functionality of the application. + +It was also attractive to junior developers with very little experience in building applications, and it was a great channel to bring a new audience to Neo4j. -== GRANDstack starter app +Finally, it was helpful for users who had an existing front end and needed a new backend. They needed a way to initiate a server, so the value we offered was by providing an opinionated mechanism that can help users get started with Neo4j with GraphQL. -The main purpose of the GRANDstack starter app was to demonstrate how the Neo4j Labs GraphQL library could be used in the context of a full-stack application using React and Apollo client. -It allowed developers to build applications more quickly and with a bigger focus on functionality, while also helping users who already had an existing frontend and needed a new back end. +Over time, the GRANDstack starter grew to support other frameworks such as Flutter and Angular. With hindsight, this massively increased the support burden and technical debt of the product, and it is time to revisit the scope of the product. -Over time, the GRANDstack starter app grew to support other frameworks such as Flutter and Angular, thus the need to revisit its scope. -The intention is to replace this project with a new starter application product, which will focus on the back end and the configuration of the GraphQL library, as well as help developers with their frontend. +The intention for the future is to replace this Labs project with a new starter application product, which will focus on the backend and the configuration of the GraphQL library, and we will consider options on how we might help developers with their front-end. -In the meantime, the `create-grandstack-app` npm package has been marked as deprecated. -It can still be used to skeleton a GRANDstack app, but the user will be warned that the package is deprecated. +== What has happened to the GRANDstack starter app? +From a technical perspective the `create-grandstack-app` npm package has been marked as deprecated - it can still be used to skeleton a GRANDstack app, however the user will be warned that the package is deprecated. The associated GRANDstack starter GitHub repository has been archived, meaning that it is still available as a reference, but is now read-only. The associated npm packages (such as `graphql-auth-directives` and the `grandstack-cli`) have also been deprecated and archived on GitHub. -== Neo4j GraphQL Architect +== Why have we deprecated the GraphQL Architect Graph App? + +The main purpose of the Neo4j GraphQL Architect was to provide users with a Low-Code way to build GraphQL APIs powered by Neo4j. +GraphQL Architect was a Graph App for Neo4j Desktop that enabled developers to build, query, and deploy GraphQL APIs backed by the Neo4j graph database, all from within Neo4j Desktop. + +We decided it was time to give this tool a new purpose and expand its potential to attract our target audience. +Successor of GraphQL Architect Graph App, the new Neo4j GraphQL Toolbox (the name is a placeholder) will be part of the GraphQL for Neo4j holistic product offering. +It is currently available at https://graphql-toolbox.neo4j.io/, however we haven't publicized it yet and we're still writing documentation and marketing pieces on it. + +The vision for this is to build an embeddable tool that we can integrate into the Neo4j workspace which will help onboarding developers into GraphQL. +It's meant to be a Low-Code GraphQL solution to get started for developers who are either new to Neo4j, or GraphQL, or both, and they're seeking a way to get started easily and quickly with their project from scratch. + +We want to make it easily configurable so that it's at developers' fingertips ready and available for users without requiring them to have credentials to access a live Neo4j instance when they are in a prototyping stage. +It's also a great way to discover the GraphQL library capabilities. It's very useful for debugging errors and problems users may encounter using the library so they can use the GraphQL Toolbox to reproduce the problem in GitHub issues. + +We can communicate to the developers market that there is no mismatch between the API model and the data model (in the database). +We are graph from top to bottom. +GraphQL developers already think in graphs about their application data and features, but we need to help them understand that they can store that information in graphs (using a graph DB). + + +== What has happened to the GraphQL Architect? -Neo4j GraphQL Architect was a low-code graph app for Neo4j Desktop that enabled developers to build, query, and deploy GraphQL APIs backed by the Neo4j graph database. -It was deprecated and removed from npm, the https://install.graphapp.io/[Graph App gallery], and Neo4j Desktop. -The associated GitHub repositories have also been archived. +From a technical perspective we have deprecated and removed the GraphQL Architect Graph App from npm and the https://install.graphapp.io/[Graph App gallery], it is no longer available to be installed in Neo4j Desktop. +The associated GitHub repositories have been archived. -This tool was reimagined in the xref:getting-started/toolbox.adoc[Neo4j GraphQL Toolbox], an onboarding, low-code tool that can be integrated to Neo4j. \ No newline at end of file diff --git a/modules/ROOT/pages/directives.adoc b/modules/ROOT/pages/directives.adoc new file mode 100644 index 00000000..5654836f --- /dev/null +++ b/modules/ROOT/pages/directives.adoc @@ -0,0 +1,193 @@ +[[directives]] += Directives + +== `@alias` + +The `@alias` directive will map a GraphQL schema field to a Neo4j property on a node or relationship. + +Reference: xref::type-definitions/database-mapping.adoc#type-definitions-alias[`@alias`] + +== `@auth` + +The `@auth` directive is used to define complex fine-grained and role-based access control for object types and fields. + +Reference: xref::auth/auth-directive.adoc[`@auth` directive] + +== `@callback` label:deprecated[] + +The `@callback` directive has been deprecated and will be removed in version 4.0. Please use the xref::directives.adoc#populated-by-directive[`@populatedBy` directive] instead. + +The `@callback` directive is used to specify a function that will be invoked when updating or creating the properties on a node or relationship. + +Reference: xref::type-definitions/autogeneration.adoc#type-definitions-autogeneration-callback[`@callback`] + +== `@coalesce` + +The `@coalesce` directive exposes a mechanism for querying against non-existent, `null` values on a node. + +Reference: xref::type-definitions/default-values.adoc#type-definitions-default-values-coalesce[`@coalesce`] + +== `@computed` label:deprecated[] + +The `@computed` directive has been deprecated and will be removed in version 4.0. +Please use the xref::directives.adoc#custom-resolver-directive[`@customResolver` directive] instead. + +The `@computed` directive specifies that a field will be resolved by a custom resolver, and allows the specification +of any field dependencies. + +Reference: xref::custom-resolvers.adoc#custom-resolvers-computed[`@computed`] + +[[custom-resolver-directive]] +== `@customResolver` + +The `@customResolver` directive specifies that a field will be resolved by a custom resolver, and allows the specification +of any required fields that will be passed as arguments to the custom resolver. + +Reference: xref::custom-resolvers.adoc#custom-resolver-directive[`@customResolver`] + +== `@cypher` + +The `@cypher` directive overrides field resolution (including `Query` and `Mutation` fields), instead resolving with the specified Cypher. + +Reference: xref::type-definitions/cypher.adoc[`@cypher` directive] + +== `@default` + +The `@default` directive allows for the setting of a default value for a field on object creation. + +Reference: xref::type-definitions/default-values.adoc#type-definitions-default-values-default[`@default`] + +== `@exclude` + +The `@exclude` directive is used on object types to instruct them to be skipped during query, mutation and subscription generation. + +Reference: xref::type-definitions/schema-configuration/index.adoc#_exclude[`@exclude`] + +== `@fulltext` + +The `@fulltext` directive indicates that there should be a Fulltext index inserted into the database for the specified Node and its properties. + +Reference: xref::type-definitions/indexes-and-constraints.adoc#type-definitions-indexes-fulltext[Fulltext indexes] + +== `@id` + +The `@id` directive marks a field as the unique ID for an object type, and allows for autogeneration of IDs. + +Reference: xref::type-definitions/autogeneration.adoc#type-definitions-autogeneration-id[`@id`] + +== `@mutation` + +This directive is used to limit the availability of mutation operations in the library. + +Reference: xref::type-definitions/schema-configuration/type-configuration.adoc#_mutation[`@mutation`] + +== `@node` + +NOTE: The `plural` argument of the `@node` directive has been deprecated and will be removed in version 4.0. +Please use the xref::directives.adoc#plural-directive[`@plural` directive] instead. + +NOTE: The `label` and `additionalLabels` arguments of the `@node` directive have been deprecated and will be removed in version 4.0. +Please use the xref::type-definitions/database-mapping.adoc#_labels[`labels argument`] instead. + +The `@node` directive is used to specify the configuration of a GraphQL object type which represents a Neo4j node. + +Reference: xref::type-definitions/database-mapping.adoc#type-definitions-node[`@node`] + +[[plural-directive]] +== `@plural` + +The `@plural` directive redefines how to compose the plural of the type for the generated operations. +This is particularly useful for types that are not correctly pluralized or are non-English words. + +Reference: xref::type-definitions/database-mapping.adoc#type-definitions-plural[`@plural`] + +[[populated-by-directive]] +== `@populatedBy` + +The `@populatedBy` directive is used to specify a callback function that gets executed during GraphQL query parsing, +to populate fields which have not been provided within the input. + +Reference: xref::type-definitions/autogeneration.adoc#type-definitions-autogeneration-populated-by[`@populatedBy`] + +== `@private` + +The `@private` directive protects fields which should only be available through the xref::ogm/index.adoc[OGM]. + +Reference: xref::ogm/private.adoc[`@private` Directive] + +== `@query` + +This directive is used to limit the availability of query operations in the library. + +Reference: xref::type-definitions/schema-configuration/type-configuration.adoc#_query[`@query`] + +== `@queryOptions` + +The `@queryOptions` is to be used on nodes, where applied will inject values into a query such as the `limit`. + +Reference: xref::type-definitions/default-values.adoc#type-definitions-default-values-queryoptions[`@queryOptions`] + +== `@readonly` label:deprecated[] + +This directive is deprecated. See the xref::type-definitions/schema-configuration/field-configuration.adoc#_settable[`@settable`] directive. + +The `@readonly` directive marks fields as read-only. + +Reference: xref::type-definitions/schema-configuration/field-configuration.adoc#_readonly[`@readonly`] + +== `@relationship` + +The `@relationship` directive is used to configure relationships between object types. + +Reference: xref::type-definitions/relationships.adoc[Relationships], xref::type-definitions/schema-configuration/field-configuration.adoc#_relationship[`@relationship`] + +== `@relationshipProperties` + +Optional syntactic sugar to help you distinguish between interfaces which are used for relationship properties, and otherwise. + +Can only be used on interfaces, as per its definition: + +[source, graphql, indent=0] +---- +"""Syntactic sugar to help differentiate between interfaces for relationship properties, and otherwise.""" +directive @relationshipProperties on INTERFACE +---- + +== `@selectable` + +The `@selectable` directive sets the availability of fields on queries and aggregations. + +Reference: xref::type-definitions/schema-configuration/field-configuration.adoc#_selectable[`@selectable`] + +== `@settable` + +The `@settable` directive sets the availability of fields on the create and update inputs. + +Reference: xref::type-definitions/schema-configuration/field-configuration.adoc#_settable[`@settable`] + +== `@subscription` + +This directive is used to limit subscription operations in the library. + +Reference: xref::type-definitions/schema-configuration/type-configuration.adoc#_subscription[`@subscription`] + +== `@timestamp` + +The `@timestamp` directive flags fields to be used to store timestamps on create/update events. + +Reference: xref::type-definitions/autogeneration.adoc#type-definitions-autogeneration-timestamp[`@timestamp`] + +== `@unique` + +The `@unique` directive indicates that there should be a uniqueness constraint in the database for the fields that it is applied to. + +Reference: xref::type-definitions/indexes-and-constraints.adoc#type-definitions-constraints-unique[Unique node property constraints] + +== `@writeonly` label:deprecated[] + +This directive is deprecated. +Use the xref::type-definitions/schema-configuration/field-configuration.adoc#_selectable[`@selectable`] directive instead. + +The `@writeonly` directive marks fields as write-only. + +Reference: xref::type-definitions/schema-configuration/field-configuration.adoc#_writeonly[`@writeonly`] diff --git a/modules/ROOT/pages/driver-configuration.adoc b/modules/ROOT/pages/driver-configuration.adoc index 1557bb0b..70ba205e 100644 --- a/modules/ROOT/pages/driver-configuration.adoc +++ b/modules/ROOT/pages/driver-configuration.adoc @@ -219,3 +219,95 @@ await startStandaloneServer(server, { context: async ({ req }) => ({ req }), }); ---- + +== Specifying Neo4j version + +When a connection is established, the library automatically detect the version of the Neo4j instance connected. +The version will be then stored and used for the following queries. +It is also possible to specify manually the Neo4j version in the Context. + +=== Context + +[source, javascript, indent=0] +---- +import { ApolloServer } from '@apollo/server'; +import { startStandaloneServer } from '@apollo/server/standalone'; +import { Neo4jDatabaseInfo, Neo4jGraphQL } from "@neo4j/graphql"; +import neo4j from "neo4j-driver"; + +const typeDefs = `#graphql + type User { + name: String + } +`; + +const driver = neo4j.driver( + "bolt://localhost:7687", + neo4j.auth.basic("neo4j", "password") +); + +const neoSchema = new Neo4jGraphQL({ typeDefs, driver }); + +const server = new ApolloServer({ + schema: await neoSchema.getSchema(), +}); + +await startStandaloneServer(server, { + context: async ({ req }) => ({ req, neo4jDatabaseInfo: new Neo4jDatabaseInfo("4.4") }), +}); +---- + +[[driver-configuration-bookmarks]] +== Specifying Neo4j Bookmarks + +You might have a requirement to specify Neo4j bookmarks when executing a query against your GraphQL schema. Primarily you will need to do this to chain transactions to ensure causal consistency if using a causal cluster or Aura Professional. + +You can read more about causal consistency in the https://neo4j.com/docs/operations-manual/current/clustering/introduction/#consistency-explained[clustering chapter of the Neo4j Operations manual], and more about bookmark chaining https://neo4j.com/docs/javascript-manual/current/cypher-workflow/#js-driver-causal-chaining[in the driver manual]. + +You can ask for the bookmark in the selection set from the `info` object of a mutation response. For example, for a type `User`: + +[source, graphql, indent=0] +---- +mutation($name: String!) { + createUsers(input: [{ name: $name }]) { + info { + bookmark + } + users { + name + } + } +} +---- + +You can then pass this bookmark into the context of a subsequent query, like this setup to pass in via a HTTP request header for example: + +[source, javascript, indent=0] +---- +import { ApolloServer } from '@apollo/server'; +import { startStandaloneServer } from '@apollo/server/standalone'; +import { Neo4jGraphQL } from "@neo4j/graphql"; +import neo4j from "neo4j-driver"; + +const typeDefs = `#graphql + type User { + name: String + } +`; + +const driver = neo4j.driver( + "bolt://localhost:7687", + neo4j.auth.basic("neo4j", "password") +); + +const neoSchema = new Neo4jGraphQL({ typeDefs, driver }); + +const server = new ApolloServer({ + schema: await neoSchema.getSchema(), +}); + +await startStandaloneServer(server, { + context: ({ req }) => ({ req, driverConfig: { bookmarks: [req.headers["x-neo4j-bookmark"]] } }), +}); + +---- diff --git a/modules/ROOT/pages/filtering.adoc b/modules/ROOT/pages/filtering.adoc new file mode 100644 index 00000000..34d35085 --- /dev/null +++ b/modules/ROOT/pages/filtering.adoc @@ -0,0 +1,547 @@ +[[filtering]] += Filtering + +== Operators + +When querying for data, a number of operators are available for different types in the `where` argument of a query or mutation. + +=== Equality operators + +All types can be tested for either equality or non-equality. For the `Boolean` type, these are the only available comparison operators. + +[[filtering-numerical-operators]] +=== Numerical operators + +The following comparison operators are available for numeric types (`Int`, `Float`, xref::type-definitions/types.adoc#type-definitions-types-bigint[`BigInt`]), xref::type-definitions/types.adoc#type-definitions-types-temporal[Temporal Types] and xref::type-definitions/types.adoc#type-definitions-types-spatial[Spatial Types]: + +* `_LT` +* `_LTE` +* `_GTE` +* `_GT` + +Filtering of spatial types is different to filtering of numerical types and also offers an additional filter - see xref::type-definitions/types.adoc#type-definitions-types-spatial[Spatial Types]. + +=== String comparison + +The following case-sensitive comparison operators are only available for use on `String` and `ID` types: + +* `_STARTS_WITH` +* `_ENDS_WITH` +* `_CONTAINS` + +The following operators are disabled by default: + +* `_LT` +* `_LTE` +* `_GT` +* `_GTE` + +They can be enabled by explicitly adding them in the features options: + +[source, javascript, indent=0] +---- +const { Neo4jGraphQL } = require("@neo4j/graphql"); +const neo4j = require("neo4j-driver"); + +const typeDefs = ` + type User { + name: String + } +`; + +const driver = neo4j.driver( + "bolt://localhost:7687", + neo4j.auth.basic("neo4j", "password") +); + +const features = { + filters: { + String: { + LT: true, + GT: true, + LTE: true, + GTE: true + } + } +}; + +const neoSchema = new Neo4jGraphQL({ features, typeDefs, driver }); +---- + + +[[filtering-regex]] +==== RegEx matching + +The filter `_MATCHES` is also available for comparison of `String` and `ID` types, which accepts a RegEx string as an argument and returns any matches. +Note that RegEx matching filters are **disabled by default**. + +To enable the inclusion of this filter, set the features configuration object for each. + +For `String`: + +[source, javascript, indent=0] +---- +const features = { + filters: { + String: { + MATCHES: true, + } + } +}; + +const neoSchema = new Neo4jGraphQL({ features, typeDefs, driver }); +---- + +For `ID`: + + +[source, javascript, indent=0] +---- +const features = { + filters: { + String: { + ID: true, + } + } +}; + +const neoSchema = new Neo4jGraphQL({ features, typeDefs, driver }); +---- + +For both `String` and `ID`: + + +[source, javascript, indent=0] +---- +const features = { + filters: { + String: { + MATCHES: true, + }, + ID: { + MATCHES: true, + } + } +}; + +const neoSchema = new Neo4jGraphQL({ features, typeDefs, driver }); +---- + + +Previously to enable to this filter the config option `enableRegex` was used, it has been deprecated. Use the `features` configuration object as described here, as the `enableRegex` will be removed in the future. + +> The nature of RegEx matching means that on an unprotected API, this could potentially be used to execute a ReDoS attack (https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS) against the backing Neo4j database. + +[[array-operators]] +=== Array comparison + +The following two comparison operators are available on non-array fields, and accept an array argument: + +* `_IN` +Conversely, the following operators are available on array fields, and accept a single argument: + +* `_INCLUDES` + +These four operators are available for all types apart from `Boolean`. + +=== AND, OR, NOT operators + +Complex combinations of operators are possible using the `AND`/ `OR` / `NOT` operators. +These are stand-alone operators - that is, they are used as such and not appended to field names, and they accept an array/object argument with items of the same format as the `where` argument. + +== Usage + +Using the type definitions from xref::queries.adoc[Queries], below are some example of how filtering can be applied when querying for data. + +=== At the root of a query + +By using the `where` argument on the query field in question, you can return a User with a particular ID: + +[source, graphql, indent=0] +---- +query { + users(where: { id: "7CF1D9D6-E527-4ACD-9C2A-207AE0F5CB8C" }) { + name + } +} +---- + +=== Combining operators + +All above-mentioned operators can be combined using the `AND`/`OR`/`NOT` operators. +They accept an array argument with items of the same format as the `where` argument, which means they can also be nested to form complex combinations. +As an example, the below query matches all actors by the name of either "Keanu" or not belonging to the "Pantoliano" family, that played in "The Matrix" movie. + +[source, graphql, indent=0] +---- +query { + actors(where: { + AND: [ + { + OR: [ + { name_CONTAINS: "Keanu" }, + { NOT: { name_ENDS_WITH: "Pantoliano" } } + ] + }, + { + movies_SOME: { title: "The Matrix" } + } + ]} + ) { + name + movies { + title + } + } +} +---- + +=== Filtering relationships + +By using the `where` argument on a relationship field, you can filter for a Post with a particular ID across all Users: + +[source, graphql, indent=0] +---- +query { + users { + id + name + posts(where: { id: "2D297425-9BCF-4986-817F-F06EE0A1D9C7" }) { + content + } + } +} +---- + +== Relationship Filtering + +For each relationship field, `field`, a set of filters are available depending on whether the relationship is `n..1` or `n..m`. In the case of `n..1`, filtering is done on equality or inequality of the related node by specifying a filter on `field`, respectively. In the case of `n..m`, filtering is done on the list of related nodes and is based on the https://neo4j.com/docs/cypher-manual/current/functions/predicate/[List Predicates] available in Cypher. + +=== Available Filters + +`n..1`:: + ** `field` - equality + +`n..m`:: + ** `field_ALL` - https://neo4j.com/docs/cypher-manual/current/functions/predicate/#functions-all[all] + ** `field_NONE` - https://neo4j.com/docs/cypher-manual/current/functions/predicate/#functions-none[none] + ** `field_SOME` - https://neo4j.com/docs/cypher-manual/current/functions/predicate/#functions-any[any] + ** `field_SINGLE` - https://neo4j.com/docs/cypher-manual/current/functions/predicate/#functions-single[single] + + +=== Relationship Filtering Usage Examples + +For this section take as type definitions the following: + +[source, graphql, indent=0] +---- +type User { + id: ID! + name: String + posts: [Post!]! @relationship(type: "HAS_POST", direction: OUT) +} + +type Post { + id: ID! + content: String + author: User! @relationship(type: "HAS_POST", direction: IN) + likes: [User!]! @relationship(type: "LIKES", direction: IN) +} +---- +=== `n..1` Relationships +In the above, an `author` represents a `n..1` relationship on `Post` where a given `Post` is authored by one, and only one, `author`. The available filters here will be `author`. + +==== Find all posts by a desired author +[source, graphql, indent=0] +---- +query { + posts(where: { author: { id: "7CF1D9D6-E527-4ACD-9C2A-207AE0F5CB8C" } }) { + content + } +} +---- +==== Find all posts not by an undesired author +[source, graphql, indent=0] +---- +query { + posts(where: { NOT: { author: { id: "7CF1D9D6-E527-4ACD-9C2A-207AE0F5CB8C" } } }) { + content + } +} +---- +=== `n..m` Relationships +In the above, `posts` represents a `n..m` relationship on `User` where a given `User` can have any number of `posts`. + +==== Find all users where all of their posts contain search term: `"neo4j"` +[source, graphql, indent=0] +---- +query { + users(where: { posts_ALL: { content_CONTAINS: "neo4j" } }) { + name + } +} +---- + +==== Find all users where none of their posts contain search term: `"cypher"` +[source, graphql, indent=0] +---- +query { + users(where: { posts_NONE: { content_CONTAINS: "cypher" } }) { + name + } +} +---- + +==== Find all users where some of their posts contain search term: `"graphql"` +[source, graphql, indent=0] +---- +query { + users(where: { posts_SOME: { content_CONTAINS: "graphql" } }) { + name + } +} +---- + +==== Find all users where only one of their posts contain search term: `"graph"` +[source, graphql, indent=0] +---- +query { + users(where: { posts_SINGLE: { content_CONTAINS: "graph" } }) { + name + } +} +---- + +== Aggregation Filtering + +This library offers, for each relationship, an aggregation key inside the where argument. You can use the aggregation key to satisfy questions such as: + +* Find the posts where the number of likes are greater than 5 +* Find flights where the average age of passengers is greater than or equal to 18 +* Find movies where the shortest actor screen time is less than 10 minutes + +You can use this where aggregation on both the `node` and `edge` of a relationship. + + +=== Aggregation Filtering Usage Examples + +==== Find the posts where the number of likes are greater than 5 + +Given the schema: + +[source, graphql, indent=0] +---- +type User { + name: String +} + +type Post { + content: String + likes: [User!]! @relationship(type: "LIKES", direction: IN) +} +---- + +Answering the question: + +[source, graphql, indent=0] +---- +query { + posts(where: { likesAggregate: { count_GT: 5 } }) { + content + } +} +---- + +==== Find flights where the average age of passengers is greater than or equal to 18 + +Given the schema: + +[source, graphql, indent=0] +---- +type Passenger { + name: String + age: Int +} + +type Flight { + code: String + passengers: [Passenger!]! @relationship(type: "FLYING_ON", direction: IN) +} +---- + +Answering the question: + +[source, graphql, indent=0] +---- +query { + flights(where: { passengersAggregate: { node: { age_AVERAGE_GTE: 18 } } }) { + code + } +} +---- + +==== Find movies where the shortest actor screen time is less than 10 minutes + +Given the schema: + +[source, graphql, indent=0] +---- +type Movie { + title: String + actors: [Person!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") +} + +type Person { + name: String +} + +interface ActedIn { + screenTime: Int +} +---- + +Answering the question: + +[source, graphql, indent=0] +---- +query { + movies(where: { actorsAggregate: { edge: { screenTime_MIN_LT: 10 } } }) { + title + } +} +---- + +=== Aggregation Filtering Operators + +Below you will learn more about the autogenerated filters available on the aggregate key and for each type on the `node` and `edge` of the specified relationship. + +==== Count +This is a special 'top level' key inside the where aggregation and will be available for all relationships. This is used to count the amount of relationships the parent node is connected to. +The operators count has are as follows: + +* `count_EQUAL` +* `count_GT` +* `count_GTE` +* `count_LT` +* `count_LTE` + +===== Example + +[source, graphql, indent=0] +---- +query { + posts(where: { likesAggregate: { count_GT: 5 } }) { + content + } +} +---- + +==== `ID` + +No Aggregation filters are available for ID. + +==== `String` + +Fields of type `String` have the following operators: + +* `_AVERAGE_LENGTH_EQUAL` +* `_AVERAGE_LENGTH_GT` +* `_AVERAGE_LENGTH_GTE` +* `_AVERAGE_LENGTH_LT` +* `_AVERAGE_LENGTH_LTE` +* `_SHORTEST_LENGTH_EQUAL` +* `_SHORTEST_LENGTH_GT` +* `_SHORTEST_LENGTH_GTE` +* `_SHORTEST_LENGTH_LT` +* `_SHORTEST_LENGTH_LTE` +* `_LONGEST_LENGTH_EQUAL` +* `_LONGEST_LENGTH_GT` +* `_LONGEST_LENGTH_GTE` +* `_LONGEST_LENGTH_LT` +* `_LONGEST_LENGTH_LTE` + +These operators are calculated against the length of each string. + +===== Example + +[source, graphql, indent=0] +---- +query { + posts(where: { likesAggregate: { node: { name_LONGEST_LENGTH_GT: 5 } } }) { + content + } +} +---- + +==== Numerical Types + +Numerical types include the following: + +* `Int` +* `Float` +* `BigInt` + +The types in the list above have the following operators: + +* `_AVERAGE_EQUAL` +* `_AVERAGE_GT` +* `_AVERAGE_GTE` +* `_AVERAGE_LT` +* `_AVERAGE_LTE` +* `_SUM_EQUAL` +* `_SUM_GT` +* `_SUM_GTE` +* `_SUM_LT` +* `_SUM_LTE` +* `_MIN_EQUAL` +* `_MIN_GT` +* `_MIN_GTE` +* `_MIN_LT` +* `_MIN_LTE` +* `_MAX_EQUAL` +* `_MAX_GT` +* `_MAX_GTE` +* `_MAX_LT` +* `_MAX_LTE` + +===== Example + +[source, graphql, indent=0] +---- +query { + movies(where: { actorsAggregate: { edge: { screenTime_MIN_LT: 10 } } }) { + title + } +} +---- + +==== Temporal Types + +Temporal types include the following: + +* `DateTime` +* `LocalDateTime` +* `LocalTime` +* `Time` +* `Duration` + +The types listed above have the following aggregation operators: + +* `_MIN_EQUAL` +* `_MIN_GT` +* `_MIN_GTE` +* `_MIN_LT` +* `_MIN_LTE` +* `_MAX_EQUAL` +* `_MAX_GT` +* `_MAX_GTE` +* `_MAX_LT` +* `_MAX_LTE` + +Whilst the `Duration` type also has the following additional operators: + +* `_AVERAGE_EQUAL` +* `_AVERAGE_GT` +* `_AVERAGE_GTE` +* `_AVERAGE_LT` +* `_AVERAGE_LTE` diff --git a/modules/ROOT/pages/getting-started.adoc b/modules/ROOT/pages/getting-started.adoc new file mode 100644 index 00000000..b3a65f40 --- /dev/null +++ b/modules/ROOT/pages/getting-started.adoc @@ -0,0 +1,235 @@ +[[getting-started]] += Getting Started + +This tutorial walks you through creating a new project with the Neo4j GraphQL Library. + +If you are not familiar with Neo4j and GraphQL, you can alternatively take the course https://graphacademy.neo4j.com/courses/graphql-basics/?ref=docs[Introduction to Neo4j & GraphQL] in GraphAcademy to learn the fundamentals, how to use the xref:toolbox.adoc[Neo4j GraphQL Toolbox] and the Neo4j GraphQL Library to create GraphQL APIs backed by a Neo4j graph database. + +This tutorial shows you how to: + +- Install the Neo4j GraphQL Library and its dependencies +- Define type definitions that represent the structure of your graph database +- Instantiate an instance of the library, which will generate a GraphQL schema +- Run an instance of a server which will let you execute queries and mutations against your schema + +This tutorial assumes familiarity with the command line and JavaScript, and also that you have a recent version of Node.js installed. These examples will use the default `npm` package manager, but feel free to use your package manager of choice. + +== Create a new project + +. Create a new directory and `cd` into it: ++ +[source, bash, indent=0] +---- +mkdir neo4j-graphql-example +cd neo4j-graphql-example +---- ++ +. Create a new Node.js project (with ESM modules enabled by using the es6 option): ++ +[source, bash, indent=0] +---- +npm init es6 --yes +---- ++ +Whilst you're there, create an empty `index.js` file which will contain all of the code for this example: ++ +[source, bash, indent=0] +---- +touch index.js +---- + +== Install dependencies + +The Neo4j GraphQL Library and it's dependencies must be installed: + +- `@neo4j/graphql` is the official Neo4j GraphQL Library package, which takes your GraphQL type definitions and generates a schema backed by a Neo4j database for you. +- `graphql` is the package used by the Neo4j GraphQL Library to generate a schema and execute queries and mutations. +- `neo4j-driver` is the official Neo4j Driver package for JavaScript, of which an instance must be passed into the Neo4j GraphQL Library. + +Additionally, you will need to install a GraphQL server package which will host your schema and allow you to execute queries and mutations against it. For this example, use the popular https://www.apollographql.com/docs/apollo-server/[Apollo Server] package: + +- `@apollo/server` is the default package for Apollo Server, which you will pass the Neo4j GraphQL Library generated schema into. + +[source, bash, indent=0] +---- +npm install @neo4j/graphql graphql neo4j-driver @apollo/server +---- + +Make sure the database fulfills the requirements stated xref::introduction.adoc#introduction-requirements[here], including the necessary plugins. + +== Define your GraphQL type definitions + +The Neo4j GraphQL Library is primarily driven by type definitions which map to the nodes and relationships in your Neo4j database. To get started, use a simple example with two node types, one with label "Actor" and the other "Movie". + +Open up the previously created `index.js` in your editor of choice and write out your type definitions. You should also add all of the necessary package imports at this stage: + +[source, javascript, indent=0] +---- +import { ApolloServer } from '@apollo/server'; +import { startStandaloneServer } from '@apollo/server/standalone'; +import { Neo4jGraphQL } from "@neo4j/graphql"; +import neo4j from "neo4j-driver"; + +const typeDefs = `#graphql + type Movie { + title: String + actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN) + } + + type Actor { + name: String + movies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT) + } +`; +---- + +These type definitions are incredibly simple, defining the two previously described node labels, and a relationship "ACTED_IN" between the two. When generated, the schema will allow you to execute queries `actors` and `movies` to read data from the database. + +You can also automatically generate type definitions from an existing database by xref::introspector.adoc[introspecting the schema]. + +== Create an instance of `Neo4jGraphQL` + +Now that you have your type definitions, you need to create an instance of the Neo4j GraphQL Library. To do this, you also need a Neo4j driver to connect to your database. For a database located at "bolt://localhost:7687", with a username of "neo4j" and a password of "password", add the following to the bottom of your `index.js` file: + +[source, javascript, indent=0] +---- +const driver = neo4j.driver( + "bolt://localhost:7687", + neo4j.auth.basic("neo4j", "password") +); + +const neoSchema = new Neo4jGraphQL({ typeDefs, driver }); +---- + +== Create an instance of `ApolloServer` + +The final section of code you need to add is to instantiate an Apollo Server instance using the generated schema, which will allow you to execute queries against it. + +Add the following to the bottom of `index.js`: + +[source, javascript, indent=0] +---- +const server = new ApolloServer({ + schema: await neoSchema.getSchema(), +}); + +const { url } = await startStandaloneServer(server, { + context: async ({ req }) => ({ req }), + listen: { port: 4000 }, +}); + +console.log(`🚀 Server ready at ${url}`); +---- + +== Start the server + +Finally, you're ready to start up your GraphQL server! Back in the command line, run the following command: + +[source, bash, indent=0] +---- +node index.js +---- + +All going well, you should see the following output: + +[source, bash, indent=0] +---- +🚀 Server ready at http://localhost:4000/ +---- + +Where http://localhost:4000/ is the default URL which Apollo Server starts at. + +== Create your first nodes in the database + +Now it's time to add some data to your Neo4j database using your GraphQL API! + +Visit http://localhost:4000/ in your web browser and you'll see the following landing page: + +image::apollo-server-landing-page.png[title="Apollo Server Landing Page"] + +Click "Query your server" which will open the Sandbox. + +image::first-mutation.png[title="First mutation"] + +At the moment your database is empty! To get some data in there, you can create a movie and an actor in that movie, all in one mutation. The mutation in the screenshot above can also be found below: + +[source, graphql, indent=0] +---- +mutation { + createMovies( + input: [ + { + title: "Forrest Gump" + actors: { create: [{ node: { name: "Tom Hanks" } }] } + } + ] + ) { + movies { + title + actors { + name + } + } + } +} +---- + +Put this mutation into the Operations panel and hit the blue "Run" button in the top right. When you execute the mutation, you'll receive the following response, confirmation that the data has been created in the database! + +[source, json, indent=0] +---- +{ + "data": { + "createMovies": { + "movies": [ + { + "title": "Forrest Gump", + "actors": [ + { + "name": "Tom Hanks" + } + ] + } + ] + } + } +} +---- + +You can now go back and query the data which you just added: + +image::first-query.png[title="First query"] + +The query in the screenshot above is querying for all movies and their actors in the database: + +[source, graphql, indent=0] +---- +query { + movies { + title + actors { + name + } + } +} +---- + +Of course, you only have the one of each, so you will see the result below: + +[source, json, indent=0] +---- +{ + "data": { + "movies": [ + { + "title": "Forrest Gump", + "actors": [ + { + "name": "Tom Hanks" + } + ] + } + ] + } +} +---- diff --git a/modules/ROOT/pages/getting-started/index.adoc b/modules/ROOT/pages/getting-started/index.adoc deleted file mode 100644 index 84d963a4..00000000 --- a/modules/ROOT/pages/getting-started/index.adoc +++ /dev/null @@ -1,262 +0,0 @@ -[[getting-started]] -:description: This section describes how to get started with the Neo4j GraphQL Library. -= Getting started - -This tutorial walks you through creating a new project with the Neo4j GraphQL Library. - -If you are not familiar with Neo4j and GraphQL, you can alternatively take the course https://graphacademy.neo4j.com/courses/graphql-basics/?ref=docs[Introduction to Neo4j & GraphQL] in GraphAcademy to learn the fundamentals, how to use the xref:getting-started/toolbox.adoc[Neo4j GraphQL Toolbox] and the Neo4j GraphQL Library to create GraphQL APIs backed by a Neo4j graph database. - -This tutorial shows you how to: - -- Install the Neo4j GraphQL Library and its dependencies. -- Set type definitions that represent the structure of your graph database. -- Start an instance of the library to generate a GraphQL schema. -- Run an instance of a server to execute queries and mutations against your schema. - -This tutorial assumes familiarity with command line and JavaScript, and also that you have a recent version of Node.js installed. -The examples use the default `npm` package manager, but you can use another one of choice. - -== Create a new project - -. Create a new directory and `cd` into it: -+ -[source, bash, indent=0] ----- -mkdir neo4j-graphql-example -cd neo4j-graphql-example ----- -+ -. Create a new Node.js project (with ESM modules enabled by using the es6 option): -+ -[source, bash, indent=0] ----- -npm init es6 --yes ----- -+ -. Create an empty `index.js` file which with all of the code for this example: -+ -[source, bash, indent=0] ----- -touch index.js ----- - -== Install dependencies - -. Install the Neo4j GraphQL Library and its dependencies: -+ -.. `@neo4j/graphql`: the official Neo4j GraphQL Library package. -It takes your GraphQL type definitions and generates a schema backed by a Neo4j database. -.. `graphql`: the package used to generate a schema and execute queries and mutations. -.. `neo4j-driver`: the official Neo4j Driver package for JavaScript, of which an instance must be passed into the Neo4j GraphQL Library. - -. Install a GraphQL server package to host your schema and allow the execution of queries and mutations against it. -.. The https://www.apollographql.com/docs/apollo-server/[`@apollo/server`] is the default package for Apollo Server: -+ -[source, bash, indent=0] ----- -npm install @neo4j/graphql graphql neo4j-driver @apollo/server ----- - -. Set up a https://neo4j.com[Neo4j database]. -Make sure it fulfills the xref::index.adoc#_requirements[requirements], including the necessary plugins. - -== Set GraphQL type definitions - -The Neo4j GraphQL Library is primarily driven by type definitions which map to the nodes and relationships in your Neo4j database. -To get started, use a simple example with two node types, one with label "Actor" and the other "Movie": - -. Open the previously created `index.js` in your editor of choice and write your type definitions. -Add all of the necessary package imports: -+ -[source, javascript, indent=0] ----- -import { ApolloServer } from '@apollo/server'; -import { startStandaloneServer } from '@apollo/server/standalone'; -import { Neo4jGraphQL } from "@neo4j/graphql"; -import neo4j from "neo4j-driver"; - -const typeDefs = `#graphql - type Movie { - title: String - actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN) - } - - type Actor { - name: String - movies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT) - } -`; ----- -+ -Note that these type definitions only *define* the node labels "Actor" and "Movie", and a relationship "ACTED_IN" between the two. -When the schema is generated, you can then execute queries for `actors` and `movies` to read data from the database. - -. Alternatively, you can also automatically generate type definitions from an existing database by xref::introspector.adoc[introspecting the schema]. - -== Create an instance of `Neo4jGraphQL` - -To create an instance of the Neo4j GraphQL Library, you need a Neo4j driver to connect to your database. - -=== Using AuraDB - -. For an AuraDB database, https://neo4j.com/cloud/platform/aura-graph-database/?ref=docs-graphql[create an instance]. - -. Make sure to save the generated password and the connection URI provided after the instance is ready and looking similar to this: -+ -image::neo4j-aura-dashboard.png[width=500] - -. Create a new file `.env` with the credentials described in the previous step. -It should look like this: -+ -[source, config] ----- -NEO4J_USER=neo4j -NEO4J_PASSWORD=password -NEO4J_URI=neo4j+s://0083654f.databases.neo4j.io ----- - -=== Using a Neo4j database - -For a database located at the default "bolt://localhost:7687" (see more about https://neo4j.com/docs/operations-manual/current/configuration/ports[port configuration]), with the username "neo4j" and the password "password", add the following to the bottom of your `index.js` file: - -[source, javascript, indent=0] ----- -const driver = neo4j.driver( - "bolt://localhost:7687", - neo4j.auth.basic("neo4j", "password") -); - -const neoSchema = new Neo4jGraphQL({ typeDefs, driver }); ----- - -== Create an instance of `ApolloServer` - -To create an Apollo Server instance using the generated schema, in which you can execute queries against it, add the following to the bottom of `index.js`: - -[source, javascript, indent=0] ----- -const server = new ApolloServer({ - schema: await neoSchema.getSchema(), -}); - -const { url } = await startStandaloneServer(server, { - context: async ({ req }) => ({ req }), - listen: { port: 4000 }, -}); - -console.log(`🚀 Server ready at ${url}`); ----- - -== Start the server - -Finally, you are ready to start up your GraphQL server. -Back in the command line, run: - -[source, bash, indent=0] ----- -node index.js ----- - -If successful, you should see the following output: - -[source, bash, indent=0] ----- -🚀 Server ready at http://localhost:4000/ ----- - -The address http://localhost:4000/ is the default URL in which Apollo Server starts at. - -== Create nodes in the database - -. Visit http://localhost:4000/ in your web browser. -You should get redirected to the Apollo Sandbox: -+ -image::apollo-server-landing-page.png[] - -. At the moment your database is empty. -To start adding data, create a movie and an actor in that movie, all in one mutation to be added into the Operations panel: -+ -[source, graphql, indent=0] ----- -mutation { - createMovies( - input: [ - { - title: "Forrest Gump" - actors: { create: [{ node: { name: "Tom Hanks" } }] } - } - ] - ) { - movies { - title - actors { - name - } - } - } -} ----- - -. Click the "Run" button on the top right. -If successful, you get the following confirmation that the data has been created in the database: -+ -[source, json, indent=0] ----- -{ - "data": { - "createMovies": { - "movies": [ - { - "title": "Forrest Gump", - "actors": [ - { - "name": "Tom Hanks" - } - ] - } - ] - } - } -} ----- - -. Now go back and query the data which you just added: -+ -[source, graphql, indent=0] ----- -query { - movies { - title - actors { - name - } - } -} ----- -+ -Since only one "Movie" and one "Actor" were added, this is the result you should get: -+ -[source, json, indent=0] ----- -{ - "data": { - "movies": [ - { - "title": "Forrest Gump", - "actors": [ - { - "name": "Tom Hanks" - } - ] - } - ] - } -} ----- - -== Conclusion - -This concludes the tutorial. -By now, you should have a GraphQL API connected to a Neo4j database, to which you added two nodes. - -To learn more, keep reading the documentation about xref:queries-aggregations/index.adoc[Queries and aggregations] or alternatively learn how to use the xref:getting-started/toolbox.adoc[Neo4j GraphQL Toolbox]. \ No newline at end of file diff --git a/modules/ROOT/pages/getting-started/toolbox.adoc b/modules/ROOT/pages/getting-started/toolbox.adoc deleted file mode 100644 index e0be25c1..00000000 --- a/modules/ROOT/pages/getting-started/toolbox.adoc +++ /dev/null @@ -1,39 +0,0 @@ -[[Toolbox]] -:description: This page describes the functionalities of the Neo4j GraphQL Toolbox. -= Neo4j GraphQL Toolbox - -The https://graphql-toolbox.neo4j.io[Neo4j GraphQL Toolbox] is an onboarding, low-code tool that can be integrated to Neo4j. -It was created for development and experimentation with Neo4j GraphQL APIs. -With it, you can: - -. Connect to a Neo4j database with valid credentials. -. Define (or introspect) the type definitions. -. Build the Neo4j GraphQL schema. -. Experiment, query, and play. - -== Connect to a Neo4j database - -Before start using the Toolbox, make sure you have followed all xref:index.adoc#requirements[requirements] to run a Neo4j database. -You can use https://neo4j.com/docs/desktop-manual/current/[Neo4j Desktop] or https://neo4j.com/docs/aura/auradb/[Neo4j AuraDB] for that. - -== Set the type definitions - -. Set your type definitions directly in the Toolbox editor or introspect the Neo4j database you connected to. -+ -If you followed the xref:getting-started/index.adoc[Getting started tutorial], you should see something like this: -+ -image::toolbox-schema-view.png[] - -. Click the button "Build schema" and then go to the Query editor tab. - -. Query the Neo4j database using the autogenerated GraphQL queries and mutations from the `@neo4j/graphql` library: -+ -image::toolbox-editor-view.png[] - -== Store type definitions as favorite - -You can store your type definitions as favorites and access a range of settings to work with. - -. Go to the Type definitions tab and click on the star icon to save it as a favorite: -+ -image:toolbox-favorite.png[] diff --git a/modules/ROOT/pages/guides/apollo-federation.adoc b/modules/ROOT/pages/guides/apollo-federation.adoc new file mode 100644 index 00000000..6ce4c8c5 --- /dev/null +++ b/modules/ROOT/pages/guides/apollo-federation.adoc @@ -0,0 +1,175 @@ +[[apollo-federation]] += Apollo Federation - beta + +WARNING: Apollo Federation is currently experimental. There will be missing functionality, and breaking changes may occur in patch and minor releases. It is not recommended to use it in a production environment. + +The Neo4j GraphQL Library can be used to generate a subgraph schema to be used as a part of a federated supergraph using https://www.apollographql.com/apollo-federation/[Apollo Federation]. + +== Usage + +This section will walk through setting up some example subgraphs using the Neo4j GraphQL Library, and composing them into a supergraph schema. + +=== Prerequisites + +Install Rover from Apollo by following their https://www.apollographql.com/docs/rover/getting-started[installation instructions]. + +The example subgraphs below assume that the database URI is stored in an environment variable "NEO4J_URI", and the password in "NEO4J_PASSWORD". + +Create a new directory, and within it, run the following commands to start a new npm project and install the dependencies: + +[source, bash, indent=0] +---- +npm init --yes +npm install @apollo/server @neo4j/graphql neo4j-driver +---- + +=== Example subgraphs + +NOTE: In the two subgraphs below, notice that `@shareable` has been applied to both of the `Location` definitions. +This is a necessary workaround because the Neo4j GraphQL Library generates identically named types for the `Location` definitions, which causes a collision when the Gateway stitches the two federated schemas. + +==== Locations + +Create a file `locations.js` with the following contents: + +[source, javascript, indent=0] +---- +import { ApolloServer } from '@apollo/server'; +import { startStandaloneServer } from '@apollo/server/standalone'; +import { Neo4jGraphQL } from "@neo4j/graphql"; +import neo4j from "neo4j-driver"; + +const typeDefs = `#graphql + extend schema + @link(url: "https://specs.apollo.dev/federation/v2.0", + import: ["@key", "@shareable"]) + + type Location @key(fields: "id") @shareable { + id: ID! + "The name of the location" + name: String + "A short description about the location" + description: String + "The location's main photo as a URL" + photo: String + } +`; + +const driver = neo4j.driver( + process.env.NEO4J_URI, + neo4j.auth.basic("neo4j", process.env.NEO4J_PASSWORD) +); + +const neoSchema = new Neo4jGraphQL({ + typeDefs, + driver, +}); + +const schema = neoSchema.getSubgraphSchema(); + +const server = new ApolloServer({ + schema, +}); + +const { url } = startStandaloneServer(server, { listen: { port: 4001 } }); + +console.log(`🚀 Server ready at ${url}`); +---- + +Start this server by running `node locations.js`. + +==== Reviews + +Create a file `reviews.js` with the following contents: + +[source, javascript, indent=0] +---- +import { ApolloServer } from '@apollo/server'; +import { startStandaloneServer } from '@apollo/server/standalone'; +import { Neo4jGraphQL } from "@neo4j/graphql"; +import neo4j from "neo4j-driver"; + +const typeDefs = `#graphql + extend schema + @link(url: "https://specs.apollo.dev/federation/v2.0", + import: ["@key", "@shareable"]) + + type Location @key(fields: "id") @shareable { + id: ID! + "The calculated overall rating based on all reviews" + overallRating: Float + "All submitted reviews about this location" + reviewsForLocation: [Review]! + } + + type Review { + id: ID! + "Written text" + comment: String + "A number from 1 - 5 with 1 being lowest and 5 being highest" + rating: Int + "The location the review is about" + location: Location! @relationship(type: "HAS_REVIEW", direction: IN) + } +`; + +const driver = neo4j.driver( + process.env.NEO4J_URI, + neo4j.auth.basic("neo4j", process.env.NEO4J_PASSWORD) +); + +const neoSchema = new Neo4jGraphQL({ + typeDefs, + driver, +}); + +const schema = neoSchema.getSubgraphSchema(); + +const server = new ApolloServer({ + schema, +}); + +const { url } = await startStandaloneServer(server, { listen: { port: 4002 } }); + +console.log(`🚀 Server ready at ${url}`); +---- + +Start this server by running `node reviews.js`. + +=== Supergraph composition + +Create a new file, `supergraph.yaml` + +[source, yaml, indent=0] +---- +federation_version: 2 +subgraphs: + locations: + routing_url: http://localhost:4001/ + schema: + subgraph_url: http://localhost:4001/ + reviews: + routing_url: http://localhost:4002/ + schema: + subgraph_url: http://localhost:4002/ +---- + +In the same directory, execute the following command to generate the supergraph schema: + +NOTE: The first time you run this command with a Federation 2 YAML configuration, Rover installs a separate plugin and prompts you to accept the terms and conditions of the ELv2 license. +Find more information in the https://www.apollographql.com/docs/federation/quickstart/local-composition/#2-perform-composition[Apollo Federation docs]. + +[source, bash, indent=0] +---- +rover supergraph compose --config supergraph.yaml > supergraph.graphql +---- + +Finally, execute the following commands to download Apollo Router and start the supergraph server: + +[source, bash, indent=0] +---- +curl -sSL https://router.apollo.dev/download/nix/latest | sh +./router --dev --supergraph supergraph.graphql +---- + +You should now be able to navigate to http://127.0.0.1:4000/ in a web browser to access Apollo Sandbox and query the supergraph. diff --git a/modules/ROOT/pages/guides/frameworks/nextjs.adoc b/modules/ROOT/pages/guides/frameworks/nextjs.adoc new file mode 100644 index 00000000..245632a0 --- /dev/null +++ b/modules/ROOT/pages/guides/frameworks/nextjs.adoc @@ -0,0 +1,5 @@ +[[usage-nextjs]] + += Usage with Next.js + +We maintain an https://github.com/vercel/next.js/tree/canary/examples/with-apollo-neo4j-graphql[example project] in the Next.js repository on how to use `@neo4j/graphql` with Next. The example demonstrates how to use the Neo4j Movie Database with GraphQL and https://www.apollographql.com/docs/apollo-server/[Apollo Server 4]. diff --git a/modules/ROOT/pages/guides/index.adoc b/modules/ROOT/pages/guides/index.adoc new file mode 100644 index 00000000..a3d9e8cc --- /dev/null +++ b/modules/ROOT/pages/guides/index.adoc @@ -0,0 +1,13 @@ +[[guides]] += Guides + +Here you can find a selection of guides to help you work with the Neo4j GraphQL Library. + +== Usage with frameworks +* xref::guides/frameworks/nextjs.adoc[Usage with Next.js] - guide on how to use `@neo4j/graphql` with Next.js. + +== Migration guides +* xref::guides/migration-guide/index.adoc[Migration Guide] - migrating from `neo4j-graphql-js` to `@neo4j/graphql` +* xref::guides/v2-migration/index.adoc[2.0.0 Migration] - migrating from version 1.* of `@neo4j/graphql` to version 2.* for relationship property support +* xref::guides/v3-migration/index.adoc[3.0.0 Migration] - migrating from version 2.* of `@neo4j/graphql` to version 3.* +* xref::guides/v4-migration/index.adoc[4.0.0 Migration] - migrating from version 3.* of `@neo4j/graphql` to version 4.* diff --git a/modules/ROOT/pages/guides/migration-guide/index.adoc b/modules/ROOT/pages/guides/migration-guide/index.adoc new file mode 100644 index 00000000..67ec15f1 --- /dev/null +++ b/modules/ROOT/pages/guides/migration-guide/index.adoc @@ -0,0 +1,23 @@ +[[migration-guide]] += Migration Guide + +`@neo4j/graphql` was never intended to be a drop-in replacement for `neo4j-graphql-js`. However, simple applications should have a fairly trivial migration process. + +== How to Upgrade + +You need to uninstall the old library and install the new one (and its peer dependencies) using npm or your package manager of choice: + +[source, bash, indent=0] +---- +npm uninstall neo4j-graphql-js +npm install @neo4j/graphql graphql neo4j-driver +---- + +From this point on, see each section within this guide: + +1. xref::guides/migration-guide/server.adoc[Server] for how to generate a schema and host it using Apollo Server, including database configuration +2. xref::guides/migration-guide/type-definitions.adoc[Type Definitions] for the changes needed in your type definitions, including GraphQL types and directives +3. xref::guides/migration-guide/queries.adoc[queries] for how you need to change your queries to work with the new schema +4. xref::guides/migration-guide/mutations.adoc[Mutations] for how you need to change your mutations to work with the new schema + +Subscriptions are not supported at this stage. diff --git a/modules/ROOT/pages/guides/migration-guide/mutations.adoc b/modules/ROOT/pages/guides/migration-guide/mutations.adoc new file mode 100644 index 00000000..ed0135b8 --- /dev/null +++ b/modules/ROOT/pages/guides/migration-guide/mutations.adoc @@ -0,0 +1,206 @@ +[[migration-guide-mutations]] += Mutations + +This section will walk through each operation available through GraphQL mutations, and how to migrate each from `neo4j-graphql-js` to `@neo4j/graphql`. + +The examples in this section will be based off the following type definitions (which have been migrated over to `@neo4j/graphql` syntax): + +[source, graphql, indent=0] +---- +type Actor { + name: String! + movies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT) +} + +type Movie { + title: String! + averageRating: Float + actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN) +} +---- + +A summary of migration points is as follows: + +* Mutations which were previously in the singular and in _PascalCase_ are now pluralized and in _camelCase_ - for example `CreateMovie` has become `createMovies` +* Connect and disconnect mutations are no longer present, these operations are accessed through an update mutation +* The object(s) being mutated are returned as a nested field, to allow for metadata about the operation to be added in future +* Mutation arguments are now commonly named between different types, but with different input types - such as `where` and `input` + +> Note that xref::mutations/index.adoc[mutations] in `@neo4j/graphql` are incredibly powerful, and it is well worthwhile reading about them in full. You might find that you can collapse multiple current mutations down into one! + +== Creating + +For creating nodes, the input arguments of `neo4j-graphql` have been moved inside an `input` argument in `@neo4j/graphql`. + +For example, creating a movie using the old library: + +[source, graphql, indent=0] +---- +mutation { + CreateMovie(title: "Forrest Gump") { + title + } +} +---- + +Looks like the following using `@neo4j/graphql`: + +[source, graphql, indent=0] +---- +mutation { + createMovies(input: { title: "Forrest Gump" }) { + movies { + title + } + } +} +---- + +Note that `movies` must also be first selected in the selection set. + +== Updating + +An update mutation using `neo4j-graphql-js` had all of the arguments at the root of the mutation, including the filter and fields to change. + +This has all changed in `@neo4j/graphql`, with a `where` argument to select the node, and then an `update` argument (amongst many others) to select what to update. + +For example, updating the average rating of the Movie Forrest Gump: + +[source, graphql, indent=0] +---- +mutation { + UpdateMovie(title: "Forrest Gump", averageRating: 5.0) { + title + averageRating + } +} +---- + +Will look like the following using the new library: + +[source, graphql, indent=0] +---- +mutation { + updateMovies(where: { title: "Forrest Gump" }, update: { averageRating: 5.0 }) { + movies { + title + averageRating + } + } +} +---- + +== Deleting + +The arguments for selecting which node to delete have now been moved into a `where` argument. + +Additionally, the return value is now a `DeleteInfo` object informing how many nodes and relationships were deleted. + +For example, deleting a movie: + +[source, graphql, indent=0] +---- +mutation { + DeleteMovie(title: "Forrest Gump") { + title + } +} +---- + +Looks like the following using `@neo4j/graphql`: + +[source, graphql, indent=0] +---- +mutation { + deleteMovies(where: { title: "Forrest Gump" }) { + nodesDeleted + relationshipsDeleted + } +} +---- + +== Connecting + +Using `neo4j-graphql-js`, connecting two of the nodes in this example would have been achieved by executing either the `AddMovieActors` or `AddActorMovies` mutation. + +In `@neo4j/graphql`, this is achieved by specifying the `connect` argument on either the `updateMovies` or `updateActors` mutation. + +For example: + +[source, graphql, indent=0] +---- +mutation { + AddMovieActors(from: { name: "Tom Hanks" }, to: { title: "Forrest Gump" }) { + from { + name + } + to { + title + } + } +} +---- + +Would become the following using `@neo4j/graphql`: + +[source, graphql, indent=0] +---- +mutation { + updateMovies( + where: { title: "Forrest Gump" } + connect: { actors: { where: { node: { name: "Tom Hanks" } } } } + ) { + movies { + title + actors { + name + } + } + } +} +---- + +Note, there are many ways to achieve the same goal using the powerful mutation ability of `@neo4j/graphql`, so do what feels best for your data! + +== Disconnecting + +Similarly to connecting, using `neo4j-graphql-js`, disconnecting two of the nodes in this example would have been achieved by executing either the `RemoveMovieActors` or `RemoveActorMovies` mutation. + +In `@neo4j/graphql`, this is achieved by specifying the `disconnect` argument on either the `updateMovies` or `updateActors` mutation. + +For example: + +[source, graphql, indent=0] +---- +mutation { + RemoveMovieActors(from: { name: "Tom Hanks" }, to: { title: "Forrest Gump" }) { + from { + name + } + to { + title + } + } +} +---- + +Would become the following using `@neo4j/graphql`: + +[source, graphql, indent=0] +---- +mutation { + updateMovies( + where: { title: "Forrest Gump" } + disconnect: { actors: { where: { node: { name: "Tom Hanks" } } } } + ) { + movies { + title + actors { + name + } + } + } +} +---- + +In the result field `actors`, Tom Hanks should no longer be present. diff --git a/modules/ROOT/pages/guides/migration-guide/queries.adoc b/modules/ROOT/pages/guides/migration-guide/queries.adoc new file mode 100644 index 00000000..18071826 --- /dev/null +++ b/modules/ROOT/pages/guides/migration-guide/queries.adoc @@ -0,0 +1,195 @@ +[[migration-guide-queries]] += Queries + +Using `neo4j-graphql-js`, all of the arguments for a query were root-level arguments. For example, for the following simple type definition: + +[source, graphql, indent=0] +---- +type Movie { + title: String! + averageRating: Float +} +---- + +The following query would have been generated: + +[source, graphql, indent=0] +---- +type Query { + Movie(title: String, averageRating: Float, first: Int, offset: Int, orderBy: [_MovieOrdering], filter: _MovieFilter): [Movie] +} +---- + +In `@neo4j/graphql`, the ethos has been to simplify the top-level arguments: + +[source, graphql, indent=0] +---- +type Query { + movies(where: MovieWhere, options: MovieOptions): [Movie]! +} +---- + +Changes to note for migration: + +* Query fields were previously named in the singular, and in _PascalCase_ - they are now pluralized and in _camelCase_ +* Query return types were previously in nullable lists of nullable types - they are now non-nullable lists of non-nullable types, _e.g._ `[Movie]` is now `[Movie!]!`; ensuring either an array of defined `Movie` objects or an empty array. +* In this example, the `_MovieFilter` type has essentially been renamed to `MovieWhere`, the `filter` arguments renamed to `where`, and the top-level field arguments `title` and `averageRating` removed - see xref::guides/migration-guide/queries.adoc#migration-guide-queries-filtering[Filtering (`where`)] below +* The `first`, `offset` and `orderBy` have been collapsed into the `MovieOptions` type and renamed `limit`, `offset` and `sort`, respectively - see xref::guides/migration-guide/queries.adoc#migration-guide-queries-options[Sorting and Pagination (`options`)] below + +[[migration-guide-queries-filtering]] +== Filtering (`where`) + +Simple equality fields are no longer available at the root of query fields. As a simple demonstration, a simple query using `neo4j-graphql-js` that looked like: + +[source, graphql, indent=0] +---- +query { + Movie(title: "Forrest Gump") { + averageRating + } +} +---- + +Is now changed to the following using `@neo4j/graphql`: + +[source, graphql, indent=0] +---- +query { + movies(where: { title: "Forrest Gump" }) { + averageRating + } +} +---- + +When discussing how the field `where` of type `MovieWhere` differs to the field `filter` of `_MovieFilter` the following table can be used for guidance: + +.Comparing the fields of the `_MovieFilter` and `MovieWhere` types +[cols="1,1"] +|=== +|`neo4j-graphql-js` |`@neo4j/graphql` + +|`AND: [_MovieFilter!]` +|`AND: [MovieWhere!]` + +|`OR: [_MovieFilter!]` +|`OR: [MovieWhere!]` + +|`NOT: _MovieFilter!` +|`NOT: MovieWhere!` + +|`title: String` +|`title: String` + +|`title_in: [String!]` +|`title_IN: [String!]` + +|`title_contains: String` +|`title_CONTAINS: String` + +|`title_starts_with: String` +|`title_STARTS_WITH: String` + +|`title_ends_with: String` +|`title_ENDS_WITH: String` + +|`title_regexp: String` +|`title_MATCHES: String` + +|`averageRating: Float` +|`averageRating: Float` + +|`averageRating_in: [Float!]` +|`averageRating_IN: [Float]` + +|`averageRating_lt: Float` +|`averageRating_LT: Float` + +|`averageRating_lte: Float` +|`averageRating_LTE: Float` + +|`averageRating_gt: Float` +|`averageRating_GT: Float` + +|`averageRating_gte: Float` +|`averageRating_GTE: Float` +|=== + +For filtering on relationship fields, the `_some`, `_none`, `_single` and `_every` filters are not yet implemented. + +[[migration-guide-queries-options]] +== Sorting and Pagination (`options`) + +=== Sorting + +Sorting has changed somewhat in `@neo4j/graphql`. For the example being used in this page, the `_MovieOrdering` type in `neo4j-graphql-js` was an enum which looked like the following: + +[source, graphql, indent=0] +---- +enum _MovieOrdering { + title_asc + title_desc + averageRating_asc + averageRating_desc +} +---- + +You could then query all movies ordered by title ascending by executing: + +[source, graphql, indent=0] +---- +query { + Movie(orderBy: [title_asc]) { + title + } +} +---- + +In `@neo4j/graphql`, the sorting type `MovieSort` has become an input type with each field as an enum, like follows: + +[source, graphql, indent=0] +---- +enum SortDirection { + ASC + DESC +} + +input MovieSort { + title: SortDirection + averageRating: SortDirection +} +---- + +To fetch all movies sorted by title ascending as per above, you would execute: + +[source, graphql, indent=0] +---- +query { + movies(options: { sort: [{ title: ASC }] }) { + title + } +} +---- + +=== Pagination + +Pagination is broadly similar, with the arguments just renamed and moved a level deeper. For example, to return "page 3, with 10 results per page" using `neo4j-graphql-js` was: + +[source, graphql, indent=0] +---- +query { + Movie(offset: 20, first: 10) { + title + } +} +---- + +Using `@neo4j/graphql`, this will now be: + +[source, graphql, indent=0] +---- +query { + movies(options: { offset: 20, limit: 10 }) { + title + } +} +---- diff --git a/modules/ROOT/pages/guides/migration-guide/server.adoc b/modules/ROOT/pages/guides/migration-guide/server.adoc new file mode 100644 index 00000000..65967052 --- /dev/null +++ b/modules/ROOT/pages/guides/migration-guide/server.adoc @@ -0,0 +1,149 @@ +[[migration-guide-server]] += Server + +[[migration-guide-server-schema-generation]] +== Schema Generation + +In your server codebase, the process of creating an executable schema has changed. For a simple application, what used to be: + +[source, javascript, indent=0] +---- +import { makeAugmentedSchema } from "neo4j-graphql-js"; + +import typeDefs from "./type-definitions"; + +const schema = makeAugmentedSchema({ typeDefs }); +---- + +Has become: + +[source, javascript, indent=0] +---- +import { makeAugmentedSchema } from "neo4j-graphql-js"; + +import typeDefs from "./type-definitions"; + +const neo4jGraphQL = new Neo4jGraphQL({ typeDefs }); + +const schema = await neo4jGraphQL.getSchema(); +---- + +Additionally, the `Neo4jGraphQL` constructor allows you to pass in a driver instance instead of passing one in the context of each request. + +=== Schema Configuration + +The `neo4j-graphql-js` library would allow a user to configure the GraphQL schema via the `makeAugmentedSchema()` function. + +For example, to exclude all queries and all mutations: + +[source, javascript, indent=0] +---- +const schema = makeAugmentedSchema({ + typeDefs, + config: { + query: false, // exclude all queries + mutation: false // exclude all mutations + } +}); +---- + +Or to exclude queries for a specific type like so: + +[source, javascript, indent=0] +---- +const schema = makeAugmentedSchema({ + typeDefs, + config: { + query: { + exclude: ["NameOfTypeToExclude"] // exclude query for type NameOfTypeToExclude + }, + mutation: false // exclude all mutations + } +}); +---- + +To achieve the same behaviour with the new `@neo4j/graphql` library, the GraphQL schema has to be extended instead. Note, no additional configuration or parameters need to be passed to the `getSchema()` function. + +To exclude all mutations for a specific type: +[source, graphql, indent=0] +---- +type NameOfType @exclude(operations: [CREATE, UPDATE, DELETE]) { + name: String +} +---- + +Or to exclude all queries and all mutations for a specific type like so: + +[source, graphql, indent=0] +---- +type NameOfTypeToExclude @exclude { + name: String +} +---- +See more information about the xref::type-definitions/schema-configuration/index.adoc#type-definitions-schema-configuration-exclude[`@exclude`] directive. + + +== Database Configuration + +=== Driver + +Once a schema has been generated, it can be passed into a GraphQL server instance with the driver in the context, identical to using the old library. For example, using Apollo Server and using the schema generated above: + +[source, javascript, indent=0] +---- +import { ApolloServer } from "@apollo/server"; +import { startStandaloneServer } from "@apollo/server/standalone"; + +import driver from "./driver"; +import schema from "./schema"; + +const server = new ApolloServer({ schema }); + +const { url } = await startStandaloneServer(server, { + context: async ({ req }) => ({ driver, token: req.headers.token }), +}); + +console.log(`@neo4j/graphql API ready at ${url}`); +---- + +=== Multiple Databases + +Multiple databases were supported in `neo4j-graphql-js` by passing a context key `neo4jDatabase` with the name of a database, for example for database called "sanmateo": + +[source, javascript, indent=0] +---- +import { ApolloServer } from "@apollo/server"; +import { startStandaloneServer } from "@apollo/server/standalone"; + +import driver from "./driver"; +import schema from "./schema"; + +const server = new ApolloServer({ schema }); + +const { url } = await startStandaloneServer(server, { + context: async ({ req }) => ({ driver, neo4jDatabase: "sanmateo", token: req.headers.token }), +}); + +console.log(`@neo4j/graphql API ready at ${url}`); +---- + +In `@neo4j/graphql`, this has now become: + +[source, javascript, indent=0] +---- +import { ApolloServer } from "@apollo/server"; +import { startStandaloneServer } from "@apollo/server/standalone"; + +import driver from "./driver"; +import schema from "./schema"; + +const server = new ApolloServer({ schema, context: { driver, driverConfig: { database: "sanmateo" } } }); + +const { url } = await startStandaloneServer(server, { + context: async ({ req }) => ({ token: req.headers.token }), +}); + +console.log(`@neo4j/graphql API ready at ${url}`); +---- + +Database bookmarks are also supported. See xref::driver-configuration.adoc[Driver Configuration] for more information. diff --git a/modules/ROOT/pages/guides/migration-guide/type-definitions.adoc b/modules/ROOT/pages/guides/migration-guide/type-definitions.adoc new file mode 100644 index 00000000..04ca67d8 --- /dev/null +++ b/modules/ROOT/pages/guides/migration-guide/type-definitions.adoc @@ -0,0 +1,166 @@ +[[migration-guide-type-definitions]] += Type Definitions + +This page will walk through what needs to change in your type definitions before you can pass them into `@neo4j/graphql`. + +== Directives + +Both `neo4j-graphql-js` and `@neo4j/graphql` are highly driven by GraphQL directives. Each heading in this section will address how/if one or many directives available in `neo4j-graphql-js` can be migrated to `@neo4j/graphql`. + +=== `@relation` + +Migrating this directive is trivial: + +1. Rename `@relation` to `@relationship` +2. Rename the argument `name` to `type` + +For example, `@relation(name: "ACTED_IN", direction: OUT)` becomes `@relationship(type: "ACTED_IN", direction: OUT)`. + +See xref::type-definitions/relationships.adoc[Relationships] for more information on relationships in `@neo4j/graphql`. + +=== Relationship Properties + +If for instance using `neo4j-graphql-js`, you have the following type definitions defining an `ACTED_IN` relationship with a `roles` property: + +[source, graphql, indent=0] +---- +type Actor { + movies: [ActedIn!]! +} + +type Movie { + actors: [ActedIn!]! +} + +type ActedIn @relation(name: "ACTED_IN") { + from: Actor + to: Movie + roles: [String!] +} +---- + +This will need to be refactored to the following in the new library: + +[source, graphql, indent=0] +---- +type Actor { + movies: [Movie!]! @relationship(type: "ACTED_IN", properties: "ActedIn", direction: OUT) +} + +type Movie { + actors: [Actor!]! @relationship(type: "ACTED_IN", properties: "ActedIn", direction: IN) +} + +interface ActedIn @relationshipProperties { + roles: [String!] +} +---- + +Note the following changes to the `ActedIn` type: + +* Changed from `type` to `interface` +* Removed `@relation` directive +* Removed `from` and `to` fields + +And note the following changes to the two node types: + +* Relationship field types changed from the relationship type to the neighbouring node type +* Normal `@relationship` directive added to each relationship field, with an additional `properties` argument pointing to the relationship properties interface + +=== `@cypher` + +No change. See xref::type-definitions/cypher.adoc[`@cypher` directive] for more details on this directive in `@neo4j/graphql`. + +=== `@neo4j_ignore` + +`@neo4j/graphql` offers two directives for skipping autogeneration for specified types/fields: + +* xref::type-definitions/schema-configuration/index.adoc#type-definitions-schema-configuration-exclude[`@exclude`]: Skip generation of specified query/mutation fields for an object type +* xref::custom-resolvers.adoc#custom-resolvers-computed[`@computed`]: Ignore a field, which will need custom logic for resolution + +=== `@isAuthenticated`, `@hasRole` and `@hasScope` + +Will require significant migration, but will be worth the effort! See xref::auth/index.adoc[Auth]. + +=== `@additionalLabels` + +Not supported at this time. + +=== `@id` + +There is an equivalent directive in the new library, but it does not work using database constraints as per the old library. See xref::type-definitions/autogeneration.adoc#type-definitions-autogeneration-id[`@id`]. + +=== `@unique`, `@index` and `@search` + +These all relate to database indexes and constraints, which are not currently supported by `@neo4j/graphql`. + +== Types + +=== Scalar Types + +Supported as you would expect, with additional xref::type-definitions/types.adoc#type-definitions-types-bigint[`BigInt`] support for 64 bit integers. + +=== Temporal Types (`DateTime`, `Date`) + +Temporal Types have been massively simplified in `@neo4j/graphql`, down to `DateTime` and `Date`, which use ISO 8601 and "yyyy-mm-dd" strings respectively for parsing and serialization. + +In terms of migrating from the old library, the `formatted` field of the old `DateTime` type now becomes the value itself. For example, used in a query: + +[source, graphql, indent=0] +---- +{ + Movie(released: { formatted: "1992-10-09T00:00:00Z" }) { + title + } +} +---- + +Has become: + +[source, graphql, indent=0] +---- +{ + Movie(released: "1992-10-09T00:00:00Z") { + title + } +} +---- + +Due to the move to ISO 8601 strings, input types are no longer necessary for temporal instances, so `_Neo4jDateTimeInput` has become `DateTime` and `_Neo4jDateInput` has become `Date` for input. + +See xref::type-definitions/types.adoc#type-definitions-types-temporal[Temporal Types]. + +=== Spatial Types + +The single type in `neo4j-graphql-js`, `Point`, has been split out into two types: + +* xref::type-definitions/types.adoc#type-definitions-types-point[`Point`] +* xref::type-definitions/types.adoc#type-definitions-types-cartesian-point[`CartesianPoint`] + +Correspondingly, `_Neo4jPointInput` has also been split out into two input types: + +* `PointInput` +* `CartesianPointInput` + +Using them in queries and mutations should feel remarkably similar. + +=== Interface Types + +Supported, queryable using inline fragments as per `neo4j-graphql-js`, but can also be created using nested mutations. See xref::type-definitions/unions.adoc#type-definitions-interfaces[Interfaces]. + +=== Union Types + +Supported, queryable using inline fragments as per `neo4j-graphql-js`, but can also be created using nested mutations. See xref::type-definitions/unions.adoc#type-definitions-unions[Unions]. + +== Fields + +=== `_id` + +An `_id` field exposing the underlying node ID is not included in each type by default in `@neo4j/graphql` like it was in `neo4j-graphql-js`. If you require this functionality (however, it should be noted that underlying node IDs should not be relied on because they can be reused), you can include a field definition such as in the following type definition: + +[source, graphql, indent=0] +---- +type ExampleType { + _id: ID! @cypher(statement: "RETURN ID(this)") +} +---- diff --git a/modules/ROOT/pages/guides/v2-migration/index.adoc b/modules/ROOT/pages/guides/v2-migration/index.adoc new file mode 100644 index 00000000..b1447560 --- /dev/null +++ b/modules/ROOT/pages/guides/v2-migration/index.adoc @@ -0,0 +1,19 @@ +[[v2-migration]] += 2.0.0 Migration + +Version 2.0.0 of `@neo4j/graphql` adds support for relationship properties, with some breaking changes to facilitate these new features. All of the required changes will be on the client side, and this guide will walk through what has changed. + +== How to Upgrade + +Simply update `@neo4j/graphql` using npm or your package manager of choice: + +[source, bash, indent=0] +---- +npm update @neo4j/graphql +---- + +From this point on, it is primarily mutations which will form the bulk of the migration: + +1. xref::guides/v2-migration/mutations.adoc[Mutations] for how you need to change your mutations to work with the new schema +2. xref::guides/v2-migration/unions.adoc[Unions] for how querying union fields has changed in version 2.0.0 +3. xref::guides/v2-migration/miscellaneous.adoc[Miscellaneous] for other changes in version 2.0.0 diff --git a/modules/ROOT/pages/guides/v2-migration/miscellaneous.adoc b/modules/ROOT/pages/guides/v2-migration/miscellaneous.adoc new file mode 100644 index 00000000..3e1178ec --- /dev/null +++ b/modules/ROOT/pages/guides/v2-migration/miscellaneous.adoc @@ -0,0 +1,89 @@ +[[v2-migration-miscellaneous]] += Miscellaneous + +== `skip` renamed to `offset` + +In the release of Apollo Client 3.0, it became a bit more opinionated about pagination, favouring `offset` and `limit` over `skip` and `limit`. Acknowledging that the majority of users will be using Apollo Client 3.0, the page-based pagination arguments have been updated to align with this change. + +For example, fetching page 3 of pages of 10 movies would have looked like the following in version `1.x`: + +[source, graphql, indent=0] +---- +query { + movies(options: { skip: 20, limit: 10 }) { + title + } +} +---- + +This will now need to queried as follows: + +[source, graphql, indent=0] +---- +query { + movies(options: { offset: 20, limit: 10 }) { + title + } +} +---- + +== Count queries + +Whilst not a necessary migration step, if you are using page-based pagination, it's important to note the addition of count queries in version 2.0.0. These will allow you to calculate the total number of pages for a particular filter, allowing you to implement much more effective pagination. + +== Schema validation + +In version 2.0.0, there are greater levels of schema validation. However, upon upgrading, you might find that validation is too strict (for example if using certain generated types in your definitions). You can temporarily disable this new validation on construction: + +[source, javascript, indent=0] +---- +const neoSchema = new Neo4jGraphQL({ + typeDefs, + config: { + skipValidateTypeDefs: true, + }, +}) +---- + +If you need to do this, please report the scenario as an issue on GitHub. + +== `_IN` and `_NOT_IN` filters on relationships removed + +There were previously `_IN` and `_NOT_IN` filters for one-to-many and one-to-one relationships, but these were surplus to requirements, and didn't match for all cardinalities (many-to-many relationships don't have `_INCLUDES` and `_NOT_INCLUDES`). These may be added back in the future if and when we look more holistically at distinguishing between different relationship cardinalities. + +You can still achieve identical filters through different routes. For example, if you had the following schema: + +[source, graphql, indent=0] +---- +type Movie { + title: String! + director: Director! @relationship(type: "DIRECTED", direction: IN) +} + +type Director { + name: String! + movies: [Movie!]! @relationship(type: "DIRECTED", direction: OUT) +} +---- + +You would have been able to run the following query: + +[source, graphql, indent=0] +---- +query { + movies(where: { director_IN: [{ name: "A" }, { name: "B" }] }) { + title + } +} +---- + +You can still achieve exactly the same filter with the following: + +[source, graphql, indent=0] +---- +query { + movies(where: { director: { OR: [{ name: "A" }, { name: "B" }]} }) { + title + } +} +---- diff --git a/modules/ROOT/pages/migration/v2-migration.adoc b/modules/ROOT/pages/guides/v2-migration/mutations.adoc similarity index 55% rename from modules/ROOT/pages/migration/v2-migration.adoc rename to modules/ROOT/pages/guides/v2-migration/mutations.adoc index 90e2044a..cc04c1eb 100644 --- a/modules/ROOT/pages/migration/v2-migration.adoc +++ b/modules/ROOT/pages/guides/v2-migration/mutations.adoc @@ -1,23 +1,7 @@ -[[v2-migration]] -= 2.0.0 Migration - -Version 2.0.0 of `@neo4j/graphql` adds support for relationship properties, with some breaking changes to facilitate these new features. All of the required changes will be on the client side, and this guide will walk through what has changed. - -== How to Upgrade - -Simply update `@neo4j/graphql` using npm or your package manager of choice: - -[source, bash, indent=0] ----- -npm update @neo4j/graphql ----- - -From this point on, it is primarily Mutations which will form the bulk of the migration. - [[v2-migration-mutations]] -== Mutations += Mutations -The most broadly affected area of functionality by the 2.0.0 upgrade are the nested operations of Mutations, to facilitate the mutation of and filtering on relationship properties. +The most broadly affected area of functionality by the 2.0.0 upgrade are the nested operations of mutations, to faciliate the mutation of and filtering on relationship properties. The examples in this section will be based off the following type definitions: @@ -37,7 +21,7 @@ type Movie { The theme that you will notice during this section is that as a general rule of thumb, a `node` field will need adding to your inputs where it will also be possible to filter on relationship properties. [[v2-migration-mutations-create]] -=== Create +== Create Focussing on the `createMovies` mutation, notice that the definition of the `createMovies` mutation is unchanged: @@ -132,7 +116,7 @@ mutation { Note the additional level "node" before specifying the actor name for the create operation and in the connect where. This additional level allows for the setting of relationship properties for the new relationship, and filtering on existing relationship properties when looking for the node to connect to. See the page xref::mutations/index.adoc[mutations] for details on this. -=== Update +== Update Focussing on the `updateMovies` mutation, notice that the definition of the `updateMovies` mutation is unchanged: @@ -154,7 +138,7 @@ The `create` and `connect` nested operations are primarily the same as in the `c The `delete` nested operation is primarily the same as in the `deleteMovies` mutation, so please see the <> section for that. -==== Update +=== Update For example, say that you accidentally misspelt Christian Bale's surname and wanted to fix that. In the previous version, you might have achieved that by: @@ -226,7 +210,7 @@ mutation { Note the added layer of abstraction of `node` in both the `where` and `update` clauses. -==== Disconnect +=== Disconnect For example, say you mistakenly put Ben Affleck as playing the role of Batman in "The Dark Knight", and you wanted to disconnect those nodes. In the previous version, this would have looked like: @@ -289,7 +273,7 @@ mutation { ---- [[v2-migration-mutations-delete]] -=== Delete +== Delete Focussing on the `deleteMovies` mutation, notice that the definition of the `deleteMovies` mutation is unchanged: @@ -377,228 +361,3 @@ mutation { ---- Note the additional level "node" before specifying the actor name. - -[[v2-migration-unions]] -== Unions - -In this release, the decision was made to take the opportunity to overhaul the existing support for unions on relationship fields, laying down the foundations for adding top-level union support in the future. - -All examples in this section will be based off the following type definitions: - -[source, graphql, indent=0] ----- -type Actor { - name: String! - actedIn: [Production!]! @relationship(type: "ACTED_IN", direction: OUT) -} - -type Movie { - title: String! - actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN) -} - -type Series { - title: String! - actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN) -} - -union Production = Movie | Series ----- - -=== Input types - -The structure of input types for union queries and mutations have been changed for user friendliness, and a more consistent API. - -Essentially, field names which were previously of template `_` (for example, "actedIn_Movie") are now an object, with the field name at the top, and the member types under it. - -For example, a Mutation which would have previously been: - -[source, graphql, indent=0] ----- -mutation { - createActors( - input: [ - { - name: "Tom Hiddleston" - actedIn_Movie: { - create: [ - { - title: "The Avengers" - } - ] - } - actedIn_Series: { - create: [ - { - title: "Loki" - } - ] - } - } - ] - ) -} ----- - -Will now be: - -[source, graphql, indent=0] ----- -mutation { - createActors( - input: [ - { - name: "Tom Hiddleston" - actedIn: { - Movie: { - create: [ - { - node: { - title: "The Avengers" - } - } - ] - } - Series: { - create: [ - { - node: { - title: "Loki" - } - } - ] - } - } - } - ] - ) -} ----- - -Note the change in structure for union input, but also the additional `node` level which enables the use of relationship properties. These changes are consistent across all operations, including `where`. - -=== Filtering union fields - -There has been a slight change to how you filter union fields, adding a `where` level above each union member. For example, for a query which would have used to have looked like: - -[source, graphql, indent=0] ----- -query { - actors { - name - actedIn(Movie: { "The Avengers" }) { - ... on Movie { - title - } - } - } -} ----- - -This will now be written like: - -[source, graphql, indent=0] ----- -query { - actors { - name - actedIn(where: { Movie: { "The Avengers" }}) { - ... on Movie { - title - } - } - } -} ----- - -Furthermore, the where argument used now dictates which union members are returned from the database, to prevent overfetching. Please see xref::troubleshooting.adoc#appendix-preventing-overfetching[this page] for background and explanation of this decision. - -[[v2-migration-miscellaneous]] -== Miscellaneous - -=== `skip` renamed to `offset` - -In the release of Apollo Client 3.0, it became a bit more opinionated about pagination, favouring `offset` and `limit` over `skip` and `limit`. Acknowledging that the majority of users will be using Apollo Client 3.0, the page-based pagination arguments have been updated to align with this change. - -For example, fetching page 3 of pages of 10 movies would have looked like the following in version `1.x`: - -[source, graphql, indent=0] ----- -query { - movies(options: { skip: 20, limit: 10 }) { - title - } -} ----- - -This will now need to queried as follows: - -[source, graphql, indent=0] ----- -query { - movies(options: { offset: 20, limit: 10 }) { - title - } -} ----- - -=== Count queries - -Whilst not a necessary migration step, if you are using page-based pagination, it's important to note the addition of count queries in version 2.0.0. These will allow you to calculate the total number of pages for a particular filter, allowing you to implement much more effective pagination. - -== Schema validation - -In version 2.0.0, there are greater levels of schema validation. However, upon upgrading, you might find that validation is too strict (for example if using certain generated types in your definitions). You can temporarily disable this new validation on construction: - -[source, javascript, indent=0] ----- -const neoSchema = new Neo4jGraphQL({ - typeDefs, - config: { - skipValidateTypeDefs: true, - }, -}) ----- - -If you need to do this, please report the scenario as an issue on GitHub. - -=== `_IN` and `_NOT_IN` filters on relationships removed - -There were previously `_IN` and `_NOT_IN` filters for one-to-many and one-to-one relationships, but these were surplus to requirements, and didn't match for all cardinalities (many-to-many relationships don't have `_INCLUDES` and `_NOT_INCLUDES`). These may be added back in the future if and when we look more holistically at distinguishing between different relationship cardinalities. - -You can still achieve identical filters through different routes. For example, if you had the following schema: - -[source, graphql, indent=0] ----- -type Movie { - title: String! - director: Director! @relationship(type: "DIRECTED", direction: IN) -} - -type Director { - name: String! - movies: [Movie!]! @relationship(type: "DIRECTED", direction: OUT) -} ----- - -You would have been able to run the following query: - -[source, graphql, indent=0] ----- -query { - movies(where: { director_IN: [{ name: "A" }, { name: "B" }] }) { - title - } -} ----- - -You can still achieve exactly the same filter with the following: - -[source, graphql, indent=0] ----- -query { - movies(where: { director: { OR: [{ name: "A" }, { name: "B" }]} }) { - title - } -} ----- diff --git a/modules/ROOT/pages/guides/v2-migration/unions.adoc b/modules/ROOT/pages/guides/v2-migration/unions.adoc new file mode 100644 index 00000000..2bd59464 --- /dev/null +++ b/modules/ROOT/pages/guides/v2-migration/unions.adoc @@ -0,0 +1,134 @@ +[[v2-migration-unions]] += Unions + +In this release, the decision was made to take the opportunity to overhaul the existing support for unions on relationship fields, laying down the foundations for adding top-level union support in the future. + +All examples in this section will be based off the following type definitions: + +[source, graphql, indent=0] +---- +type Actor { + name: String! + actedIn: [Production!]! @relationship(type: "ACTED_IN", direction: OUT) +} + +type Movie { + title: String! + actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN) +} + +type Series { + title: String! + actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN) +} + +union Production = Movie | Series +---- + +== Input types + +The structure of input types for union queries and mutations have been changed for user friendliness, and a more consistent API. + +Essentially, field names which were previously of template `_` (for example, "actedIn_Movie") are now an object, with the field name at the top, and the member types under it. + +For example, a mutation which would have previously been: + +[source, graphql, indent=0] +---- +mutation { + createActors( + input: [ + { + name: "Tom Hiddleston" + actedIn_Movie: { + create: [ + { + title: "The Avengers" + } + ] + } + actedIn_Series: { + create: [ + { + title: "Loki" + } + ] + } + } + ] + ) +} +---- + +Will now be: + +[source, graphql, indent=0] +---- +mutation { + createActors( + input: [ + { + name: "Tom Hiddleston" + actedIn: { + Movie: { + create: [ + { + node: { + title: "The Avengers" + } + } + ] + } + Series: { + create: [ + { + node: { + title: "Loki" + } + } + ] + } + } + } + ] + ) +} +---- + +Note the change in structure for union input, but also the additional `node` level which enables the use of relationship properties. These changes are consistent across all operations, including `where`. + +== Filtering union fields + +There has been a slight change to how you filter union fields, adding a `where` level above each union member. For example, for a query which would have used to have looked like: + +[source, graphql, indent=0] +---- +query { + actors { + name + actedIn(Movie: { "The Avengers" }) { + ... on Movie { + title + } + } + } +} +---- + +This will now be written like: + +[source, graphql, indent=0] +---- +query { + actors { + name + actedIn(where: { Movie: { "The Avengers" }}) { + ... on Movie { + title + } + } + } +} +---- + +Furthermore, the where argument used now dictates which union members are returned from the database, to prevent overfetching. Please see xref::appendix/preventing-overfetching.adoc[this page] for background and explanation of this decision. diff --git a/modules/ROOT/pages/migration/v3-migration.adoc b/modules/ROOT/pages/guides/v3-migration/index.adoc similarity index 91% rename from modules/ROOT/pages/migration/v3-migration.adoc rename to modules/ROOT/pages/guides/v3-migration/index.adoc index 5050896d..b630085f 100644 --- a/modules/ROOT/pages/migration/v3-migration.adoc +++ b/modules/ROOT/pages/guides/v3-migration/index.adoc @@ -71,7 +71,7 @@ query { } ---- -The same operation, can now be achieved with a xref::queries-aggregations/queries.adoc#_counting_using_aggregation[count aggregation] query: +The same operation, can now be achieved with a xref::queries#_counting_using_aggregation[count aggregation] query: [source, graphql, indent=0] ---- @@ -123,11 +123,9 @@ NOTE: Old queries will still work in this release, but are marked as `@deprecate To better reflect its intended usage, the `@ignore` directive is now named `@computed`. Behaviour is unchanged, so you just need to rename this directive in your schema. -== Auth plugin system -label:deprecated[] - -Auth setup now relies on _plugins_ to setup the configuration. -You'll need to install `@neo4j/graphql-plugin-auth` or a custom plugin. +== Auth plugin system +Auth setup now relies on _plugins_ to setup the configuration. You'll need to install `@neo4j/graphql-plugin-auth` or a custom plugin +as shown in the xref::auth/setup.adoc[auth setup page]. === JWT auth For JWT authorization, instead of the previous configuration: @@ -194,19 +192,19 @@ const neoSchema = new Neo4jGraphQL({ }); ---- -NOTE: Please, refer to xref::authentication-and-authorization/index.adoc[auth setup] before setting up auth. +NOTE: Please, refer to xref::auth/setup.adoc[auth setup] before setting up auth. == Types plurals changes To improve consistency, some automatically generated plurals (e.g. `createActors`) have changed. This may cause issues if your types use conventions such as `snake_case`. Because of this, you may find generated queries and mutations may have different names. If you encounter this problem, -please update your clients to use the new query names or use the `plural` option in the xref::/type-definitions/directives/database-mapping.adoc#type-definitions-node[@node directive] +please update your clients to use the new query names or use the `plural` option in the xref::type-definitions/database-mapping.adoc#_plural[@node directive] to force a custom plural value. == Custom Directives Defining and applying custom directives has changed significantly, if you are using or plan to use custom directives, make -sure to check the up-to-date documentation on xref::/type-definitions/directives/custom-directives.adoc[custom directives]. +sure to check the up-to-date documentation on xref::type-definitions/custom-directives.adoc[custom directives]. == Types changes Some automatically generated types have changed to improve consistency. diff --git a/modules/ROOT/pages/migration/v4-migration/authorization.adoc b/modules/ROOT/pages/guides/v4-migration/authorization.adoc similarity index 87% rename from modules/ROOT/pages/migration/v4-migration/authorization.adoc rename to modules/ROOT/pages/guides/v4-migration/authorization.adoc index fdedf470..df5aeb90 100644 --- a/modules/ROOT/pages/migration/v4-migration/authorization.adoc +++ b/modules/ROOT/pages/guides/v4-migration/authorization.adoc @@ -1,6 +1,6 @@ = Authentication and Authorization -The largest breaking change in version 4.0.0 is the removal of the `@auth` directive, which requires a migration to the new `@authentication`, `@authorization` and `@subscriptionsAuthorization` directives. +The largest breaking change in version 4.0.0 is the removal of the `@auth` directive, which requires a migration to the new `@authentication` and `@authorization` directives. == Instantiation @@ -12,9 +12,7 @@ You should uninstall the previous plugin: npm uninstall @neo4j/graphql-plugin-auth ---- -=== Symmetric secret - -Given an example of instantiation using a symmetric secret with the plugin: +Then, given an example of instantiation using a basic secret with the plugin: [source, typescript, indent=0] ---- @@ -42,38 +40,6 @@ new Neo4jGraphQL({ }) ---- -=== JWKS endpoint - -When using a JWKS endpoint, an example of how this might be configured currently is: - -[source, typescript, indent=0] ----- -new Neo4jGraphQL({ - typeDefs, - plugins: { - auth: new Neo4jGraphQLAuthJWKSPlugin({ - jwksEndpoint: "https://YOUR_DOMAIN/well-known/jwks.json", - }), - } -}) ----- - -In version 4.0.0, delete the import of `Neo4jGraphQLAuthJWKSPlugin`, and change the instantiation to: - -[source, typescript, indent=0] ----- -new Neo4jGraphQL({ - typeDefs, - features: { - authorization: { - key: { - url: "https://YOUR_DOMAIN/well-known/jwks.json", - }, - } - } -}) ----- - == Server Previously, you could pass in the entire request object and the library would find the `Authorization` header: @@ -101,7 +67,7 @@ const server = new ApolloServer({ const { url } = await startStandaloneServer(server, { listen: { port: 4000 }, context: async ({ req }) => ({ - token: req.headers.authorization, + token: req.headers.Authorization, }), }); ---- @@ -288,7 +254,7 @@ The following changes were made for this migration: * A `validate` rule has been used, which will throw an error without the role as per the `roles` argument in the `@auth` directive. This can alternatively be a `filter` rule to just return zero results if a user does not have the required role. -* `roles` has become `roles_INCLUDES`, because the xref::queries-aggregations/filtering.adoc[full filtering capabilities of the library] can now be used within the `@authorization` directive. +* `roles` has become `roles_INCLUDES`, because the xref::filtering.adoc[full filtering capabilities of the library] can now be used within the `@authorization` directive. * `roles` is no longer a top-level rule field, but nested within `where` under `jwt`. Any number of JWT claims can now be compared against, if configured within the type decorated with `@jwt`. diff --git a/modules/ROOT/pages/migration/v4-migration/index.adoc b/modules/ROOT/pages/guides/v4-migration/index.adoc similarity index 54% rename from modules/ROOT/pages/migration/v4-migration/index.adoc rename to modules/ROOT/pages/guides/v4-migration/index.adoc index 98c868b1..2d4b043f 100644 --- a/modules/ROOT/pages/migration/v4-migration/index.adoc +++ b/modules/ROOT/pages/guides/v4-migration/index.adoc @@ -1,6 +1,9 @@ [[v4-migration]] + = 4.0.0 Migration +NOTE: Version 4.0.0 of the library has not yet been released. However, we recommend making these changes early in order to avoid issues in the future. + This document lists all breaking changes from version 3.x.y to 4.0.0 and how to update. == How to upgrade @@ -11,353 +14,10 @@ Simply update `@neo4j/graphql` using npm or your package manager of choice: npm update @neo4j/graphql ---- -== Constructor arguments - -If you were passing any arguments from https://the-guild.dev/graphql/tools/docs/api/interfaces/schema_src.iexecutableschemadefinition[`IExecutableSchemaDefinition`] into the library other than `typeDefs` and `resolvers`, these are no longer supported. - -=== Removal of `config` - -==== `debug` - -The programmatic toggle for debug logging has been moved from `config.enableDebug` to simply `debug`. - -An example of `enableDebug`: - -[source, javascript, indent=0] ----- -const { Neo4jGraphQL } = require("@neo4j/graphql"); -const neo4j = require("neo4j-driver"); -const { ApolloServer } = require("apollo-server"); - -const typeDefs = ` - type Movie { - title: String! - } -`; - -const driver = neo4j.driver( - "bolt://localhost:7687", - neo4j.auth.basic("neo4j", "password") -); - -const neoSchema = new Neo4jGraphQL({ - typeDefs, - driver, - config: { - enableDebug: true, - } -}); ----- - -This now becomes: - -[source, javascript, indent=0] ----- -const { Neo4jGraphQL } = require("@neo4j/graphql"); -const neo4j = require("neo4j-driver"); -const { ApolloServer } = require("apollo-server"); - -const typeDefs = ` - type Movie { - title: String! - } -`; - -const driver = neo4j.driver( - "bolt://localhost:7687", - neo4j.auth.basic("neo4j", "password") -); - -const neoSchema = new Neo4jGraphQL({ - typeDefs, - driver, - debug: true, -}); ----- - -==== `driverConfig` moved to context - -Session configuration is now available only in the context under the `sessionConfig` key. - -This was previously `driverConfig`, available in both the constructor and in the context: - -[source, javascript, indent=0] ----- -const neoSchema = new Neo4jGraphQL({ - typeDefs, - config: { - driverConfig: { - database: "different-db" - }, - }, -}) ----- - -The new `sessionConfig` key is only available in the context: - -[source, javascript, indent=0] ----- -import { ApolloServer } from '@apollo/server'; -import { startStandaloneServer } from '@apollo/server/standalone'; -import { Neo4jGraphQL } from "@neo4j/graphql"; -import neo4j from "neo4j-driver"; - -const typeDefs = `#graphql - type User { - name: String - } -`; - -const driver = neo4j.driver( - "bolt://localhost:7687", - neo4j.auth.basic("neo4j", "password") -); - -const neoSchema = new Neo4jGraphQL({ typeDefs, driver }); - -const server = new ApolloServer({ - schema: await neoSchema.getSchema(), -}); - -await startStandaloneServer(server, { - context: async ({ req }) => ({ sessionConfig: { database: "my-database" }}), -}); ----- - -The `bookmarks` key has been removed because it is no longer needed with the bookmark manager of the newer driver. - -==== `enableRegex` replaced by `MATCHES` in features.filters - -`config.enableRegex` has been replaced by `MATCHES` in features.filters. With this change comes more granularity in the feature configuration. You can now enable the `MATCHES` filter on `String` and `ID` fields separately. - -A direct replacement of the `enableRegex: true` configuration would be as follows: - -[source, javascript, indent=0] ----- -neoSchema = new Neo4jGraphQL({ - typeDefs, - features: { - filters: { - String: { - MATCHES: true, - }, - ID: { - MATCHES: true, - }, - }, - }, -}); ----- - -==== `queryOptions` moved to the context - -If you had a need to pass in Cypher query options for query tuning, this interface has been changed. - -The config option `queryOptions` has now become `cypherQueryOptions` inside the context function, and it now accepts simple strings instead of enums. - -The following is an example before the change: - -[source, javascript, indent=0] ----- -const { Neo4jGraphQL, CypherRuntime } = require("@neo4j/graphql"); -const { ApolloServer } = require("apollo-server"); - -const typeDefs = ` - type Movie { - title: String! - } -`; - -const neoSchema = new Neo4jGraphQL({ - typeDefs, - config: { - queryOptions: { - runtime: CypherRuntime.INTERPRETED, - }, - }, -}); ----- - -This is what is required after the change: - -[source, javascript, indent=0] ----- -const { Neo4jGraphQL } = require("@neo4j/graphql"); -const { ApolloServer } = require("apollo-server"); - -const typeDefs = ` - type Movie { - title: String! - } -`; - -const neoSchema = new Neo4jGraphQL({ - typeDefs, -}); - -const server = new ApolloServer({ - schema: await neoSchema.getSchema(), -}); - -await startStandaloneServer(server, { - context: async ({ req }) => ({ cypherQueryOptions: { runtime: "interpreted" }}), -}); ----- - -This reflects the fact that the Cypher query options are set on a per-request basis. - -[[startup-validation]] -==== `skipValidateTypeDefs` - -The argument `skipValidateTypeDefs` has been moved to the top-level of the constructor input and renamed `validate`, which defaults to `true`. - -To disable type definition validation, the following config option should be used: - -[source, javascript, indent=0] ----- -const neoSchema = new Neo4jGraphQL({ - typeDefs, - validate: false, -}) ----- - -If you started using the `config.startupValidation` option, this has also been rolled into the same `validate` setting for simplicity. -The `resolvers` option of this is now just a warning, and `noDuplicateRelationshipFields` is now a mandatory check rolled into `validate`. - -[subscriptions-options] -=== Subscription options - -Subscriptions are no longer configured as a plugin, but as a feature within the `features` option. - -This means that, instead of: - -[source, javascript] ----- -const neoSchema = new Neo4jGraphQL({ - typeDefs, - plugins: { - subscriptions: plugin, - }, -}); ----- - -Subscriptions are now defined as: - -[source, javascript] ----- -const neoSchema = new Neo4jGraphQL({ - typeDefs, - features: { - subscriptions: plugin, - }, -}); ----- - -==== Default subscriptions - -The class `Neo4jGraphQLSubscriptionsSingleInstancePlugin` is no longer exported. -Instead, the default subscriptions behavior can be enabled by setting the `subscriptions` option to `true` . - -Instead of: - -[source, javascript, indent=0] ----- -const neoSchema = new Neo4jGraphQL({ - typeDefs, - plugin: { - subscriptions: new Neo4jGraphQLSubscriptionsSingleInstancePlugin(), - }, -}); ----- - -The default subscriptions can be enabled with: - -[source, javascript, indent=0] ----- -const neoSchema = new Neo4jGraphQL({ - typeDefs, - features: { - subscriptions: true - }, -}); ----- - -==== Neo4j GraphQL Subscriptions AMQP package - -The name of the interface underlying the Subscriptions system has changed from `Neo4jGraphQLSubscriptionsPlugin` to `Neo4jGraphQLSubscriptionsEngine`. -If you were previously using the `@neo4j/graphql-plugins-subscriptions-amqp` package, this has been changed to `@neo4j/graphql-amqp-subscriptions-engine` to reflect this underlying change. - -Please uninstall the previous package and install the new one: - -[source, bash, indent=0] ----- -npm uninstall @neo4j/graphql-plugins-subscriptions-amqp -npm install @neo4j/graphql-amqp-subscriptions-engine ----- - -Please then update any imports from: - -[source, javascript, indent=0] ----- -import { Neo4jGraphQLSubscriptionsAMQPPlugin } from "@neo4j/graphql-plugins-subscriptions-amqp"; ----- - -To: - -[source, javascript, indent=0] ----- -import { Neo4jGraphQLAMQPSubscriptionsEngine } from "@neo4j/graphql-amqp-subscriptions-engine"; ----- - -Then change the instantiations from: - -[source, javascript, indent=0] ----- -const plugin = new Neo4jGraphQLSubscriptionsAMQPPlugin({ - connection: { - hostname: "localhost", - username: "guest", - password: "guest", - }, -}); ----- - -To: - -[source, javascript, indent=0] ----- -const subscriptionsEngine = new Neo4jGraphQLAMQPSubscriptionsEngine({ - connection: { - hostname: "localhost", - username: "guest", - password: "guest", - }, -}); ----- - -==== Custom Subscription Plugins - -The underlying subscription system has not changed. -Custom behavior can be implemented the same way, by creating a class implementing the interface described in xref::subscriptions/engines.adoc#custom-subscription[Subscriptions Engines]. - -However, if using TypeScript, the exported interface to implement these classes has been renamed from `Neo4jGraphQLSubscriptionsPlugin` to `Neo4jGraphQLSubscriptionsEngine`. - == Updated Directives We have renamed a number of directives and their arguments, in order to make using `@neo4j/graphql` more intuitive. -=== `@alias` values are now automatically escaped - -Properties in the alias directive automatically escaped using backticks. If you were using backticks in the `property` argument of your `@alias` directives, you should now remove the escape strings as this is covered by the library. - -[source, graphql, indent=0] ----- -type User { - id: ID! @id - username: String! @alias(property: "dbUserName") -} ----- - [populatedBy-migration] === `@callback` renamed to `@populatedBy` @@ -371,7 +31,7 @@ Therefore, the following usage of the directive would be invalid: ---- type User { id: ID! @callback(name: "nanoid", operations: [CREATE]) - firstName: String! + firstName: String! surname: String! } ---- @@ -382,7 +42,7 @@ It would instead need to be updated to use the new directive and argument as bel ---- type User { id: ID! @populatedBy(callback: "nanoid", operations: [CREATE]) - firstName: String! + firstName: String! surname: String! } ---- @@ -462,9 +122,9 @@ new Neo4jGraphQL({ ==== `requires` changes In version 4.0.0, it is now possible to require non-scalar fields. This means it is also possible to require fields on related type. -To make this possible, the `requires` argument now accept a graphql selection set instead of a list of strings. +To make this possible, the `requires` argument now accept a GraphQL selection set instead of a list of strings. -Therefore, the following type definitions: +Therefore, given the following type definitions in 3.0.0: [source, graphql, indent=0] ---- @@ -475,7 +135,7 @@ type User { } ---- -Would need to be modified to use a selection set as below: +In 4.0.0, these need to be modified to use a selection set as below: [source, graphql, indent=0] ---- @@ -528,65 +188,87 @@ type User { } ---- -=== `@cypher` changes -The default behaviour of the `@cypher` directive regarding the translation will change: Instead of using https://neo4j.com/labs/apoc/4.0/overview/apoc.cypher/apoc.cypher.runFirstColumnMany/[apoc.cypher.runFirstColumnMany] it will directly wrap the query within a `CALL { }` subquery. This behvaiour has proven to be much more performant for the same queries, however, it may lead to unexpected changes, mainly when using Neo4j 5.x, where the subqueries need to be _aliased_. +[plural-migration] +=== `plural` argument removed from `@node` and replaced with `@plural` -On top of that, to improve performance, it is recommended to pass the returned alias in the property `columnName`, to ensure the subquery is properly integrated into the larger query. +How a type name is pluralised has nothing to do with nodes in the database. As a result, having a `plural` argument on the `@node` directive did not make sense. +As a result, the `plural` argument of `@node` has been removed and replaced with a new `@plural` directive. The `@plural` directive takes the pluralised type name using the `value` argument. -For example: +This means that the following type definition is invalid: -The graphql query: [source, graphql, indent=0] ---- -type query { - test: String! @cypher(statement: "RETURN 'hello'") +type Tech @node(label: "TechDB", plural: "Techs") { + name: String } ---- -Would get translated to: -[source,cypher, indent=0] +It would need to be updated to use the new directive as below: + +[source, graphql, indent=0] ---- -CALL { - RETURN 'hello' +type Tech @node(label: "TechDB") @plural(value: "Techs") { + name: String } -WITH 'hello' AS this -RETURN this ---- -Which is invalid in Neo4j 5.x. +[label-migration] +=== `label` and `additionalLabels` arguments removed from `@node` and replaced with new argument `labels` + +There is no concept of a "main label" in the Neo4j database. As such, keeping these two separate arguments causes a disconnect between the database and the GraphQL library. +As a result, the `label` and `additionalLabels` arguments have been condensed into a single argument `labels` which will accept a list of string labels that used when a node of the given GraphQL type is created. +Please note that defining `labels` means you take control of the database labels of the node. Indexes and constraints in Neo4j only support a single label, for which the first element of the `labels` argument will be used. + +The equivalent of using just the `label` argument is now a list with a single value: -To fix it we just need to ensure the `RETURN` elements are aliased: [source, graphql, indent=0] ---- -type query { - test: String! @cypher(statement: "RETURN 'hello' as result") +type Tech @node(label: "TechDB") { + name: String +} +# becomes +type Tech @node(labels: ["TechDB"]) { + name: String } ---- -This will be a breaking change, but this new behaviour can be used, as an experimental option with the `columnName` flag in the `@cypher` directive: +When creating the equivalent of using just the `additionalLabels` argument now requires the first value in the list to be the GraphQL type name: [source, graphql, indent=0] ---- -type query { - test: String! @cypher(statement: "RETURN 'hello' as result", columnName: "result") +type Tech @node(additionalLabels: ["TechDB"]) { + name: String +} +# becomes +type Tech @node(labels: ["Tech", "TechDB"]) { + name: String } ---- -Additionally, escaping strings is no longer needed. - -=== `@exclude` removed - -The `@exclude` directive has been removed in favor of much more granular configuration directives. +The equivalent of using both deprecated arguments is a list with all the values concatenated: -The new `@query`, `@mutation` and `@subscription` directives instead allow for fully granular configuration for each operation. +[source, graphql, indent=0] +---- +type Tech @node(label: "TechDB", additionalLabels: ["AwesomeTech"]) { + name: String +} +# becomes +type Tech @node(labels: ["TechDB", "AwesomeTech"]) { + name: String +} +---- -As a direct migration, the following usages are equivalent: +As before, providing none of these arguments results in the node label being the same as the GraphQL type name. -* `@exclude` and `@query(read: false, aggregate: false) @mutation(operations: []) @subscription(events: [])`. -* `@exclude(operations: [READ])` and `@query(read: false, aggregate: false)`. -* `@exclude(operation: [CREATE, UPDATE, DELETE])` and `@mutation(operations: [])`. +Please note the implications on constraints. +In the following example, a unique constraint will be asserted for the label `Tech` and the property `name`: -Whilst there is more verbosity, the directives are significantly more powerful and extensible as the library gains features. +[source, graphql, indent=0] +---- +type Tech @node(labels: ["Tech", "TechDB"]) { + name: String @unique +} +---- [full-text-migration] === `@fulltext` changes @@ -747,152 +429,59 @@ type Query { } ---- -=== `@id` changes - -The `@id` directive has been completely pared back in version 4.0.0, with _all_ of its arguments removed. -This has been done to reduce the number of features that this directive was used to toggle, and to ensure that its behaviour is consistent no matter where it is used. - -==== `autogenerate` - -The default value of `autogenerate` was `true`. If this was set to `false`, the `@id` directive was almost a no-op only used to manage a unique node property constraint. Use the `@unique` directive instead. - -==== `global` - -The `global` argument was used to configure the field that would form the global node identifier for Relay. - -This functionality has been moved into its own directive, `@relayId`. The use of `@relayId` will ensure a unique node property constraint for the field. - -==== `unique` - -The `@id` directive used to also manage unique node property constraints for a field. This functionality has now been removed, use the `@unique` directive in combination with `@id` if you want the field to be backed by a constraint. - -=== `@node` changes - -[plural-migration] -==== `plural` argument removed from `@node` and replaced with `@plural` - -How a type name is pluralised has nothing to do with nodes in the database. As a result, having a `plural` argument on the `@node` directive did not make sense. -As a result, the `plural` argument of `@node` has been removed and replaced with a new `@plural` directive. The `@plural` directive takes the pluralised type name using the `value` argument. - -This means that the following type definition is invalid: - -[source, graphql, indent=0] ----- -type Tech @node(label: "TechDB", plural: "Techs") { - name: String -} ----- - -It would need to be updated to use the new directive as below: - -[source, graphql, indent=0] ----- -type Tech @node(label: "TechDB") @plural(value: "Techs") { - name: String -} ----- - -[label-migration] -==== `label` and `additionalLabels` arguments removed from `@node` and replaced with new argument `labels` - -There is no concept of a "main label" in the Neo4j database. As such, keeping these two separate arguments causes a disconnect between the database and the GraphQL library. -As a result, the `label` and `additionalLabels` arguments have been condensed into a single argument `labels` which will accept a list of string labels that used when a node of the given GraphQL type is created. -Please note that defining `labels` means you take control of the database labels of the node. Indexes and constraints in Neo4j only support a single label, for which the first element of the `labels` argument will be used. - -The equivalent of using just the `label` argument is now a list with a single value: - -[source, graphql, indent=0] ----- -type Tech @node(label: "TechDB") { - name: String -} -# becomes -type Tech @node(labels: ["TechDB"]) { - name: String -} ----- - -When creating the equivalent of using just the `additionalLabels` argument now requires the first value in the list to be the GraphQL type name: +=== `@cypher` changes +The default behaviour of the `@cypher` directive regarding the translation will change: Instead of using https://neo4j.com/labs/apoc/4.0/overview/apoc.cypher/apoc.cypher.runFirstColumnMany/[apoc.cypher.runFirstColumnMany] it will directly wrap the query within a `CALL { }` subquery. This behvaiour has proven to be much more performant for the same queries, however, it may lead to unexpected changes, mainly when using Neo4j 5.x, where the subqueries need to be _aliased_. -[source, graphql, indent=0] ----- -type Tech @node(additionalLabels: ["TechDB"]) { - name: String -} -# becomes -type Tech @node(labels: ["Tech", "TechDB"]) { - name: String -} ----- +On top of that, to improve performance, it is recommended to pass the returned alias in the property `columnName`, to ensure the subquery is properly integrated into the larger query. -The equivalent of using both deprecated arguments is a list with all the values concatenated: +For example: +The graphql query: [source, graphql, indent=0] ---- -type Tech @node(label: "TechDB", additionalLabels: ["AwesomeTech"]) { - name: String -} -# becomes -type Tech @node(labels: ["TechDB", "AwesomeTech"]) { - name: String +type query { + test: String! @cypher(statement: "RETURN 'hello'") } ---- -As before, providing none of these arguments results in the node label being the same as the GraphQL type name. - -Please note the implications on constraints. -In the following example, a unique constraint will be asserted for the label `Tech` and the property `name`: - -[source, graphql, indent=0] +Would get translated to: +[source,cypher, indent=0] ---- -type Tech @node(labels: ["Tech", "TechDB"]) { - name: String @unique +CALL { + RETURN 'hello' } +WITH 'hello' AS this +RETURN this ---- -=== `@queryOptions` removed and `limit` argument moved to `@limit` - -If you were using the `@queryOptions` directive to configure the default and max values for limiting the data returned by queries, for instance: +Which is invalid in Neo4j 5.x. +To fix it we just need to ensure the `RETURN` elements are aliased: [source, graphql, indent=0] ---- -type Record @queryOptions(limit: { default: 10, max: 100 }) { - id: ID! +type query { + test: String! @cypher(statement: "RETURN 'hello' as result") } ---- -This is now achieved by using the `@limit` directive: +This will be a breaking change, but this new behaviour can be used, as an experimental option with the `columnName` flag in the `@cypher` directive: [source, graphql, indent=0] ---- -type Record @limit(default: 10, max: 100) { - id: ID! +type query { + test: String! @cypher(statement: "RETURN 'hello' as result", columnName: "result") } ---- -=== `@readonly` and `@writeonly` removed - -The `@readonly` and `@writeonly` directives have been removed in favor of more granular configuration directives. -The new `@selectable` and `@settable` directives can be used to configure not only if fields are readable or writable, but also when they should be readable or writable. - -As a direct migration, the following usages are equivalent: - -* `@readonly` and `@settable(onCreate: false, onUpdate: false)`. -* `@writeonly` and `@selectable(onRead: false, onAggregate: false)`. +Additionally, escaping strings is no longer needed. [relationship-aggregate] === `@relationship` changes -==== Relationship types are now automatically escaped - -Relationship types are now automatically escaped. If you have previously escaped your relationship types using backticks, you must now remove these as this is covered by the library. - -==== `aggregate` argument - In version 4.0.0, the default value of the aggregate argument will be false. This means that aggregation operation fields will no longer be generated by default when a relationship is defined using the `@relationship` directive. -For instance, given the following type definitions: +For instance, the following type definitions [source, graphql, indent=0] ---- @@ -906,9 +495,9 @@ type Actor { } ---- -These will no longer generate `actedInAggregate` for the type `Actor`. +will no longer generate `actedInAggregate` for the type `Actor`. -To enable it, explicitly set the aggregate argument as `true`: +To enable it back, explicitly set the aggregate argument as `true`: [source, graphql, indent=0] ---- @@ -922,77 +511,60 @@ type Actor { } ---- -=== `@relationshipProperties` now mandatory - -Upcoming changes to interfaces require us to distinguish between interfaces that are used to specify relationship properties, and others. Therefore, the `@relationshipProperties` directive is now required on all relationship property interfaces. -If it is not included, an error will be thrown. - -As a result, in version 4.0.0, the following type definitions are invalid: - -[source, graphql, indent=0] ----- -type Person { - name: String! - movies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") -} +== Miscellaneous changes -type Movie { - title: String! - actors: [Person!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") -} +[[startup-validation]] +=== Startup validation -interface ActedIn { - screenTime: Int! -} ----- +In version 4.0.0, startup xref::guides/v4-migration/index.adoc#customResolver-checks[checks for custom resolvers] have been added. As a result, a new configuration option has been added that can disable these checks. +This new option has been combined with the option to `skipValidateTypeDefs`. As a result, `skipValidateTypeDefs` will be removed and replaced by `startupValidation`. -`ActedIn` must be decorated with `@relationshipProperties`: +To only disable strict type definition validation, the following config option should be used: -[source, graphql, indent=0] +[source, javascript, indent=0] ---- -interface ActedIn @relationshipProperties { - screenTime: Int! -} +const neoSchema = new Neo4jGraphQL({ + typeDefs, + config: { + startupValidation: { + typeDefs: false + }, + }, +}) ---- -== Miscellaneous changes - -=== Duplicate relationship fields are now checked for +To only disable checks for custom resolvers, the following config option should be used: -It was possible to define schemas with types that have multiple relationship fields connected by the same type of relationships. Instances of this scenario are now detected during schema generation and an error is thrown so developers are informed to remedy the type definitions. - -An example of what is now considered invalid with these checks: - -[source, graphql, indent=0] +[source, javascript, indent=0] ---- -type Team { - player1: Person! @relationship(type: "PLAYS_IN", direction: IN) - player2: Person! @relationship(type: "PLAYS_IN", direction: IN) - backupPlayers: [Person!]! @relationship(type: "PLAYS_IN", direction: IN) -} - -type Person { - teams: [Team!]! @relationship(type: "PLAYS_IN", direction: OUT) -} +const neoSchema = new Neo4jGraphQL({ + typeDefs, + config: { + startupValidation: { + resolvers: false + }, + }, +}) ---- -In this example, there are multiple fields in the `Team` type which have the same `Person` type, the same `@relationship` type and ("PLAYS_IN") direction (IN). This is an issue when returning data from the database, as there would be no difference between `player1`, `player2` and `backupPlayers`. Selecting these fields would then return the same data. - -These checks can be disabled by disabling all validation in the library, however, this is not recommended unless in production with 100% confidence of type definitions input. +To disable all startup checks, the following config option should be used: [source, javascript, indent=0] ---- const neoSchema = new Neo4jGraphQL({ typeDefs, - validate: false, -}); + config: { + startupValidation: false, + }, +}) ---- [[opt-in-aggregation]] === Opt-in Aggregation Aggregation operations are no longer generated by default. -They can be enabled case by case using the directives xref::/schema-configuration/type-configuration.adoc#_query[`@query`] and xref::/schema-configuration/field-configuration.adoc#_relationship[`@relationship`]. +They can be enabled case by case using the directives xref::type-definitions/schema-configuration/type-configuration.adoc#_query[`@query`] and xref::type-definitions/schema-configuration/field-configuration.adoc#_relationship[`@relationship`]. + You can enable the operation fields `actorsAggregate` and `actedInAggregate` like this: @@ -1008,6 +580,53 @@ type Actor @query(aggregate: true) { } ---- -=== `cypherParams` +=== Session configuration + +Session configuration is now available only in the context under the `sessionConfig` key. + +This was previously `driverConfig`, available in both the constructor and in the context: + +[source, javascript, indent=0] +---- +const neoSchema = new Neo4jGraphQL({ + typeDefs, + config: { + driverConfig: { + database: "different-db" + }, + }, +}) +---- -`cypherParams` is available in the context to provide the ability to pass arbitrary parameters to a custom Cypher query. This functionality remains in 4.0.0, but you no longer have to use the `$cypherParams` prefix to reference these parameters. +The new `sessionConfig` key is only available in the context: + +[source, javascript, indent=0] +---- +import { ApolloServer } from '@apollo/server'; +import { startStandaloneServer } from '@apollo/server/standalone'; +import { Neo4jGraphQL } from "@neo4j/graphql"; +import neo4j from "neo4j-driver"; + +const typeDefs = `#graphql + type User { + name: String + } +`; + +const driver = neo4j.driver( + "bolt://localhost:7687", + neo4j.auth.basic("neo4j", "password") +); + +const neoSchema = new Neo4jGraphQL({ typeDefs, driver }); + +const server = new ApolloServer({ + schema: await neoSchema.getSchema(), +}); + +await startStandaloneServer(server, { + context: async ({ req }) => ({ sessionConfig: { database: "my-database" }}), +}); +---- + +The `bookmarks` key has been removed because it is no longer needed with the bookmark manager of the newer driver. diff --git a/modules/ROOT/pages/migration/v4-migration/ogm.adoc b/modules/ROOT/pages/guides/v4-migration/ogm.adoc similarity index 100% rename from modules/ROOT/pages/migration/v4-migration/ogm.adoc rename to modules/ROOT/pages/guides/v4-migration/ogm.adoc diff --git a/modules/ROOT/pages/index.adoc b/modules/ROOT/pages/index.adoc index 4ee3561d..0a4a30c1 100644 --- a/modules/ROOT/pages/index.adoc +++ b/modules/ROOT/pages/index.adoc @@ -1,83 +1,12 @@ [[index]] -:description: This section describes the Neo4j GraphQL Library. = Neo4j GraphQL Library - -The Neo4j GraphQL Library is a highly flexible, low-code, open source JavaScript library that enables rapid API development for cross-platform and mobile applications by tapping into the power of connected data. - -With Neo4j as the graph database, the GraphQL Library makes it simple for applications to have data treated as a graph natively from the frontend all the way to storage. -This avoids duplicate schema work and ensures flawless integration between frontend and backend developers. - -*If you are new to Neo4j and GraphQL*, take the course https://graphacademy.neo4j.com/courses/graphql-basics/?ref=docs[Introduction to Neo4j & GraphQL] in GraphAcademy to learn the fundamentals, how to use the xref:getting-started/toolbox.adoc[Neo4j GraphQL Toolbox] and the Neo4j GraphQL Library to create GraphQL APIs backed by a Neo4j graph database. - -[NOTE] -==== -The GRANDstack starter app has been deprecated. -For more information, read the section on xref::deprecations.adoc[Deprecations]. -==== - -== How it works - -The Neo4j GraphQL Library requires a set of type definitions that describes the shape of your graph data. -It can generate an entire executable schema with all of the additional types needed to execute queries and mutations to interact with your Neo4j database. - -For every query and mutation that is executed against this generated schema, the Neo4j GraphQL Library generates a single Cypher query which is executed against the database. This eliminates the https://www.google.com/search?q=graphql+n%2B1[N+1 Problem], which can make GraphQL implementations slow and inefficient. - -- Automatic generation of xref::queries-aggregations/queries.adoc[Queries] and xref::mutations/index.adoc[Mutations] for CRUD interactions. -- xref::/type-definitions/types/index.adoc[Types], including temporal and spatial. -- Support for both node and relationship properties. -- Extensibility through the xref::/type-definitions/directives/cypher.adoc[`@cypher` directive] and/or xref::custom-resolvers.adoc[Custom Resolvers]. -- Extensive xref::queries-aggregations/filtering.adoc[Filtering] and xref::queries-aggregations/sorting.adoc[Sorting] options. -- Options for value xref::/type-definitions/directives/autogeneration.adoc[Autogeneration] and xref::/type-definitions/directives/default-values.adoc[Default Values]. -- xref::/queries-aggregations/pagination/index.adoc[Pagination] options. -- xref::authentication-and-authorization/index.adoc[Authentication and authorization options] and additional xref::schema-configuration/index.adoc[Schema Configuration]. -- An xref::ogm/index.adoc[OGM] (Object Graph Mapper) for programmatic interaction with your GraphQL API. -- A xref::getting-started/toolbox.adoc[Toolbox] (UI) to experiment with your Neo4j GraphQL API on Neo4j Desktop. - - -== Interaction - -In the xref::getting-started/index.adoc[Getting Started] guide, Apollo Server is used to host the GraphQL schema, so you can interact directly with your API with no frontend. -In case you prefer to use frontend frameworks, these are some clients that interact with GraphQL APIs: - -- https://reactjs.org/[React] - support through https://www.apollographql.com/docs/react/[Apollo Client] -- https://vuejs.org/[Vue.js] - support through https://apollo.vuejs.org/[Vue Apollo] -- https://angularjs.org/[AngularJS] - support through https://apollo-angular.com/docs/[Apollo Angular]. - -== Deployment - -There are a variety of methods for deploying GraphQL APIs. -In the xref::getting-started/index.adoc[Getting Started] guide, Apollo Server is being used for demonstration. -You can check their own documentation about https://www.apollographql.com/docs/apollo-server/deployment[Deployment] for more details. - -== Versioning - -The Neo4j GraphQL Library uses https://semver.org/[Semantic Versioning]. -Given a version number `MAJOR.MINOR.PATCH`, the increment is based on: - -- `MAJOR` - incompatible API changes compared to the previous `MAJOR` version, for which you will likely have to migrate -- `MINOR` - new features have been added in a backwards compatible manner -- `PATCH` - bug fixes have been added in a backwards compatible manner. - -Additionally, prerelease version numbers may have additional suffixes, for example `MAJOR.MINOR.PATCH-PRERELEASE.NUMBER`, where `PRERELEASE` is one of the following: - -- `alpha` - unstable prerelease artifacts, and the API may change between releases during this phase -- `beta` - feature complete prerelease artifacts, which will be more stable than `alpha` releases but will likely still contain bugs -- `rc` - release candidate including artifacts to be promoted to a stable release, in a last effort to find trailing bugs. - -`NUMBER` in the suffix is simply an incrementing release number in each phase. - -== Requirements - -. https://neo4j.com/[Neo4j Database] version 4.4 and newer with https://neo4j.com/docs/apoc/current/[APOC] plugin. -. https://nodejs.org/en/[Node.js] 16+. - -== Resources - -. https://github.com/neo4j/graphql[GitHub] -. https://github.com/neo4j/graphql/issues[Issue Tracker] -. https://www.npmjs.com/package/@neo4j/graphql[npm package] - -== License +:experimental: +:sectnums: +:chapter-label: +:toc-title: Contents +//:front-cover-image: image::title-page.png[] +:header-title: NEO4J GRAPHQL LIBRARY +:title-page-background-image: image::title-page.png[] ifndef::backend-pdf[] Documentation license: link:{common-license-page-uri}[Creative Commons 4.0] @@ -88,4 +17,39 @@ ifdef::backend-pdf[] Documentation license: <> endif::[] -Source: https://www.apache.org/licenses/LICENSE-2.0[Apache 2.0] + +> This is the documentation for the Neo4j GraphQL Library, authored by the Neo4j GraphQL Team. + +TIP: Are you a GRANDstack user? We have deprecated the GRANDstack starter app, find more information xref::deprecations.adoc[here]. + +This documentation covers the following topics: + +- xref::introduction.adoc[Introduction] - Introduction to the Neo4j GraphQL Library. +- xref::getting-started.adoc[Getting Started] - Start here if you want to quickly get up and running with the library. +- xref::type-definitions/index.adoc[Type Definitions] - Define your nodes and relationships using type definitions as documented here. +- xref::queries.adoc[Queries] - GraphQL queries allow you to read data in your Neo4j database. +- xref::mutations/index.adoc[Mutations] - GraphQL mutations allow you to change data in your Neo4j database. +- xref::subscriptions/index.adoc[Subscriptions] - GraphQL subscriptions for real-time data updates. +- xref::filtering.adoc[Filtering] - This chapter covers how to filter your data in queries and mutations. +- xref::sorting.adoc[Sorting] - This chapter covers how to sort the data being returned. +- xref::pagination/index.adoc[Pagination] - This chapter covers the pagination options offered by the Neo4j GraphQL Library. +- xref::mathematical-operators.adoc[Mathematical operators] - This chapter covers how to use the Mathematical operators. +- xref::array-methods.adoc[Array methods] - This chapter covers how to use the Array methods. +- xref::custom-resolvers.adoc[Custom Resolvers] - Learn how to implement custom functionality accessible through your API. +- xref::auth/index.adoc[Auth] - Covers the authentication and authorization options offered by this library. +- xref::directives.adoc[Directives] - An index of all of the directives offered by the Neo4j GraphQL Library. +- xref::api-reference/index.adoc[API Reference] - API reference for constructing an instance of the library. +- xref::ogm/index.adoc[OGM] - This chapter covers the OGM (Object Graph Mapper), a programmatic way of using your API. +- xref::introspector.adoc[Introspector] - This chapter covers how you can introspect an existing Neo4j database and automatically generate GraphQL type definitions from that. +- xref::toolbox.adoc[GraphQL Toolbox] - Purpose-built Developer UI for your Neo4j GraphQL API. +- xref::driver-configuration.adoc[Driver Configuration] - How to configure a database driver for use with this library. +- xref::guides/index.adoc[Guides] - Guides for usage of the library, including migration guides for moving between versions. +- xref::troubleshooting/index.adoc[Troubleshooting] - Having problems with the library? See if your problem has been found and solved before. +- xref::deprecations.adoc[Deprecations] - Overview and information regarding deprecated products and applications. + +This manual is primarily written for software engineers building an API using the Neo4j GraphQL Library. + +[NOTE] +==== +If you are new to Neo4j and GraphQL, take the course https://graphacademy.neo4j.com/courses/graphql-basics/?ref=docs[Introduction to Neo4j & GraphQL] in GraphAcademy to learn the fundamentals, how to use the xref:toolbox.adoc[Neo4j GraphQL Toolbox] and the Neo4j GraphQL Library to create GraphQL APIs backed by a Neo4j graph database. +==== \ No newline at end of file diff --git a/modules/ROOT/pages/integrations/apollo-federation.adoc b/modules/ROOT/pages/integrations/apollo-federation.adoc deleted file mode 100644 index a4a7682d..00000000 --- a/modules/ROOT/pages/integrations/apollo-federation.adoc +++ /dev/null @@ -1,264 +0,0 @@ -[[apollo-federation]] -:description: This guide shows how to create a subgraph using the Neo4j GraphQL Library, for composition into a supergraph using Apollo Federation. -= Apollo Federation - -This guide shows how to create a subgraph using the Neo4j GraphQL Library, for composition into a supergraph using Apollo Federation. - -The objective is to convert the following monolithic schema into a subgraph schema for use behind a Federation gateway: - -[source, javascript] ----- -import { ApolloServer } from "@apollo/server"; -import { startStandaloneServer } from "@apollo/server/standalone"; -import { Neo4jGraphQL } from "@neo4j/graphql"; -import neo4j from "neo4j-driver"; - -const typeDefs = `#graphql - type User { - id: ID! - name: String! - } -`; - -const { - NEO4J_URI = "neo4j://localhost:7687/neo4j", - NEO4J_USERNAME = "neo4j", - NEO4J_PASSWORD = "password", -} = process.env; - -const driver = neo4j.driver(NEO4J_URI, neo4j.auth.basic(NEO4J_USERNAME, NEO4J_PASSWORD)); - -const neo4jgraphql = new Neo4jGraphQL({ - typeDefs, - driver, -}) - -const schema = await neo4jGraphQL.getSchema(); - -const server = new ApolloServer({ - schema, -}); - -const { url } = await startStandaloneServer(server); -console.log(`🚀 Server ready at ${url}`); ----- - -== Create a new project and install the Apollo Server - -*Create the project directory* - -Create a directory for a new project and `cd` into it: - -[source, bash] ----- -mkdir neo4j-graphql-subgraph-example -cd neo4j-graphql-subgraph-example ----- - -*Initialize the project* - -Initialize a new Node.js project with `npm`: - -[source, bash] ----- -npm init --yes -npm pkg set type="module" ----- - -NOTE: This how-to guide sets up the project using ES Modules, which allows the usage of features such as top-level `await`. - -*Language specific setup* - -Follow the instructions below to set up with either TypeScript or JavaScript: - -.TypeScript -[%collapsible] -==== -. Create a `src` directory with an empty `index.ts` file to contain the entrypoint to your code for this project: -+ -[source, bash] ----- -mkdir src -touch src/index.ts ----- -+ -. Install the development dependencies required for working with a TypeScript project: -+ -[source, bash] ----- -npm install --save-dev typescript @types/node @tsconfig/node-lts ----- -+ -. Create an empty `tsconfig.json` file which will contain the compiler configuration for TypeScript: -+ -[source, bash] ----- -touch tsconfig.json ----- -+ -. Add the following configuration to the `tsconfig.json` file created above: -+ -[source, json] ----- -{ - "extends": "@tsconfig/node-lts/tsconfig.json", - "compilerOptions": { - "rootDir": "src", - "outDir": "dist", - } -} ----- -+ -NOTE: The configuration above extends the https://github.com/tsconfig/bases#node-lts-tsconfigjson[community base for Node.js LTS], provided by the `@tsconfig/node-lts` package installed above. For more information on the available options, see the https://www.typescriptlang.org/tsconfig[TypeScript Compiler docs]. -+ -. Replace the default `scripts` entry in your `package.json` file with the following `scripts` entry: -+ -[source, json] ----- -{ - // ...etc. - "scripts": { - "compile": "tsc", - "start": "npm run compile && node ./dist/index.js" - } - // other dependencies -} ----- -==== - -.JavaScript -[%collapsible] -==== -. Create a `src` directory with an empty `index.js` file to contain the entrypoint to your code for this project: -+ -[source, bash] ----- -mkdir src -touch src/index.js ----- -+ -. Replace the default `scripts` entry in your `package.json` file with the following `scripts` entry: -+ -[source, json] ----- -{ - // ...etc. - "scripts": { - "start": "node index.js" - } - // other dependencies -} ----- -==== - -*Install dependencies* - -The following dependencies are required for this guide: - -* `@apollo/server`, the library for Apollo Server, is used in this guide to host the subgraph. -* `@neo4j/graphql` is the Neo4j GraphQL Library, which translates GraphQL into Cypher and returns the results. -* `graphql` is the reference implementation of the GraphQL specification. It is required for `@neo4j/graphql` to function. -* `neo4j-driver` is the library for the Neo4j driver, which is required to execute Cypher queries against the database. - -Install these dependencies by running the following command: - -[source, bash] ----- -npm install @apollo/server @neo4j/graphql graphql neo4j-driver ----- - -== Opt in to Federation - -For a set of type definitions to be usable as a subgraph for Federation, they must include the following schema extension: - -[source, javascript] ----- -const typeDefs = `#graphql - extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key"]) - - type User { - id: ID! - name: String! - } -`; ----- - -Note that this example only includes the Federation `@key` directive. -To use more https://www.apollographql.com/docs/federation/federated-types/federated-directives[Federation directives], add them to the `import` array. - -== Define an entity - -Defining a type as an https://www.apollographql.com/docs/federation/entities/[entity] allows other subgraphs to contribute with fields to the `Movie` type. -To achieve that, use the `@key` directive to designate a field (or fields) as a key: - -[source, javascript] ----- -const typeDefs = `#graphql - extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key"]) - - type User @key(fields: "id") { - id: ID! - name: String! - } -`; ----- - -Although only the `@key` directive has been added to this example, consider using either the `@id` or the `@unique` directives on the `id` field. -The Federation gateway expects each key to resolve to one result, so it is good practice to ensure that these values are unique in the database. - -== Generate a subgraph schema - -When using the Neo4j GraphQL Library, generating the subgraph schema can be achieved by calling `getSubgraphSchema` instead of `getSchema`. -In other words, only the following line needs to be changed: - -[source, javascript] ----- -const schema = neo4jgraphql.getSubgraphSchema(); ----- - -== Conclusion - -By combining all previous snippets, you should get this: - -[source, javascript] ----- -import { ApolloServer } from "@apollo/server"; -import { startStandaloneServer } from "@apollo/server/standalone"; -import { Neo4jGraphQL } from "@neo4j/graphql"; - -const typeDefs = `#graphql - type User @key(fields: "id") { - id: ID! - name: String! - } -`; - -const { - NEO4J_URI = "neo4j://localhost:7687/neo4j", - NEO4J_USERNAME = "neo4j", - NEO4J_PASSWORD = "password", -} = process.env; - -const driver = neo4j.driver(NEO4J_URI, neo4j.auth.basic(NEO4J_USERNAME, NEO4J_PASSWORD)); - -const neo4jgraphql = new Neo4jGraphQL({ - typeDefs, - driver, -}) - -const schema = await neo4jGraphQL.getSubgraphSchema(); - -const server = new ApolloServer({ - schema, -}); - -const { url } = await startStandaloneServer(server); -console.log(`🚀 Server ready at ${url}`); ----- - - -For further iteration, this subgraph can also be composed into a supergraph. -Check Apollo's guides for more instructions: - -* https://www.apollographql.com/docs/federation/quickstart/studio-composition[Composition in Apollo Studio] -* https://www.apollographql.com/docs/federation/quickstart/local-composition[Local composition] diff --git a/modules/ROOT/pages/integrations/nextjs.adoc b/modules/ROOT/pages/integrations/nextjs.adoc deleted file mode 100644 index 1f5695c7..00000000 --- a/modules/ROOT/pages/integrations/nextjs.adoc +++ /dev/null @@ -1,6 +0,0 @@ -[[next.js]] -:description: This page shows information on how to use the Neo4j GraphQL Library with Next.js -= Next.js - -This https://github.com/vercel/next.js/tree/canary/examples/with-apollo-neo4j-graphql[example project] in the Next.js repository demonstrates how to use Neo4j with https://neo4j.com/docs/getting-started/appendix/example-data/[the "Movies" example], the Neo4j GraphQL Library and https://www.apollographql.com/docs/apollo-server/[Apollo Server]. -See the https://github.com/vercel/next.js/blob/canary/examples/with-apollo-neo4j-graphql/README.md[README] file for more instructions. diff --git a/modules/ROOT/pages/introduction.adoc b/modules/ROOT/pages/introduction.adoc new file mode 100644 index 00000000..c45cebc8 --- /dev/null +++ b/modules/ROOT/pages/introduction.adoc @@ -0,0 +1,83 @@ +[[introduction]] += Introduction + +The Neo4j GraphQL Library is a highly flexible, low-code, open source JavaScript library that enables rapid API development for cross-platform and mobile applications by tapping into the power of connected data. + +With Neo4j as the graph database, the GraphQL Library makes it simple for applications to have application data treated as a graph natively from the front-end all the way to storage, avoiding duplicate schema work and ensuring flawless integration between front-end and backend developers. + +Written in TypeScript, the library's schema-first paradigm lets developers focus on the application data they need, while taking care of the heavy lifting involved in building the API. + +> Just want to get moving with the Neo4j GraphQL Library? Check out the xref::getting-started.adoc[Getting Started] guide! + +== How does it work? + +By supplying the Neo4j GraphQL Library with a set of type definitions describing the shape of your graph data, it can generate an entire executable schema with all of the additional types needed to execute queries and mutations to interact with your Neo4j database. + +For every query and mutation that is executed against this generated schema, the Neo4j GraphQL Library generates a single Cypher query which is executed against the database. This eliminates the infamous https://www.google.com/search?q=graphql+n%2B1[N+1 Problem] which can make GraphQL implementations slow and inefficient. + +== Features + +The Neo4j GraphQL Library presents a large feature set for interacting with a Neo4j database using GraphQL: + +- Automatic generation of xref::queries.adoc[queries] and xref::mutations/index.adoc[mutations] for CRUD interactions +- Various xref::type-definitions/types.adoc[Types], including temporal and spatial types +- Support for both node and relationship properties +- Extensibility through the xref::type-definitions/cypher.adoc[`@cypher` directive] and/or xref::custom-resolvers.adoc[Custom Resolvers] +- Extensive xref::filtering.adoc[Filtering] and xref::sorting.adoc[Sorting] options +- Options for value xref::type-definitions/autogeneration.adoc[Autogeneration] and xref::type-definitions/default-values.adoc[Default Values] +- Multiple xref::pagination/index.adoc[Pagination] options +- Comprehensive authentication and authorization options (xref::auth/index.adoc[Auth]), and additional xref::type-definitions/schema-configuration/index.adoc[Schema Configuration] options +- An xref::ogm/index.adoc[OGM] (Object Graph Mapper) for programmatic interaction with your GraphQL API +- A xref::toolbox.adoc[Toolbox] (UI) to experiment with your Neo4j GraphQL API on Neo4j Desktop. + + +== Interaction + +In the xref::getting-started.adoc[Getting Started] guide, Apollo Server is used to host the GraphQL schema. This bundles Apollo Sandbox which can be used to interact directly with your GraphQL API with no front-end. + +There are a variety of front-end frameworks with clients for interacting with GraphQL APIs: + +- https://reactjs.org/[React] - support through https://www.apollographql.com/docs/react/[Apollo Client] +- https://vuejs.org/[Vue.js] - support through https://apollo.vuejs.org/[Vue Apollo] +- https://angularjs.org/[AngularJS] - support through https://apollo-angular.com/docs/[Apollo Angular] + +== Deployment + +There are a variety of methods for deploying GraphQL APIs, the details of which will not be in this documentation. + +However, Apollo has documented a subset in their https://www.apollographql.com/docs/apollo-server/deployment[Deployment] documentation, which will be a good starting point. + +== Versioning + +The Neo4j GraphQL Library uses https://semver.org/[Semantic Versioning]. Given a version number `MAJOR.MINOR.PATCH`, the increment is based on: + +- `MAJOR` - incompatible API changes compared to the previous `MAJOR` version, for which you will likely have to migrate. +- `MINOR` - new features have been added in a backwards compatible manner. +- `PATCH` - bug fixes have been added in a backwards compatible manner. + +Additionally, prerelease version numbers may have additional suffixes, for example `MAJOR.MINOR.PATCH-PRERELEASE.NUMBER`, where `PRERELEASE` is one of the following: + +- `alpha` - unstable prerelease artifacts, and the API may change between releases during this phase. +- `beta` - feature complete prerelease artifacts, which will be more stable than `alpha` releases but will likely still contain bugs. +- `rc` - release candidate release artifacts where each could be promoted to a stable release, in a last effort to find trailing bugs. + +`NUMBER` in the suffix is simply an incrementing release number in each phase. + +[[introduction-requirements]] +== Requirements + +1. https://neo4j.com/[Neo4j Database] version 4.3 and newer with https://neo4j.com/developer/neo4j-apoc/[APOC] plugin: +2. https://nodejs.org/en/[Node.js] 16+ + +For help installing the APOC plugin, see https://neo4j.com/labs/apoc/4.4/installation/[this page]. + +== Resources + +1. https://github.com/neo4j/graphql[GitHub] +2. https://github.com/neo4j/graphql/issues[Issue Tracker] +3. https://www.npmjs.com/package/@neo4j/graphql[npm package] + +== Licence + +1. Documentation: link:{common-license-page-uri}[Creative Commons 4.0] +2. Source: https://www.apache.org/licenses/LICENSE-2.0[Apache 2.0] diff --git a/modules/ROOT/pages/introspector.adoc b/modules/ROOT/pages/introspector.adoc index 17e1898b..870def5b 100644 --- a/modules/ROOT/pages/introspector.adoc +++ b/modules/ROOT/pages/introspector.adoc @@ -13,7 +13,7 @@ This tool has full support for generating type definitions, including: - `@node` - `label` for mapping where a node label might use a character that's not in the GraphQL supported character set - `additionalLabels` for nodes that has multiple labels -- Generating a read-only version of the GraphQL type definitions +- Generating a read-only version of the GraphQL type definitions, i.e. generate a `@exclude(operations: [CREATE, DELETE, UPDATE])` directive on all node types. == Limitations @@ -21,7 +21,7 @@ If an element property has mixed types through out your graph, that property wil generated type definitions. The reason for this is that your GraphQL server will throw an error if it finds data that doesn't match the specified type. -If any properties are skipped, there will be output in the xref::troubleshooting.adoc[Debug Logging]. +If any properties are skipped, there will be output in the xref::troubleshooting/index.adoc[Debug Logging]. == Usage examples diff --git a/modules/ROOT/pages/mathematical-operators.adoc b/modules/ROOT/pages/mathematical-operators.adoc new file mode 100644 index 00000000..8fccbdc4 --- /dev/null +++ b/modules/ROOT/pages/mathematical-operators.adoc @@ -0,0 +1,93 @@ +[[mathematical-operators]] += Mathematical operators + +Mathematical operators are a handy way to update numerical fields based on their original values in a single DB transaction. + +Specific operators are available on different numerical types (`Int`, `Float`, xref::type-definitions/types.adoc#type-definitions-types-bigint[`BigInt`]). + + +Mathematical operators are supported in update mutations within these entities: + +* `Node` +* `Nested Nodes` +* `Relationship properties` +* `Interfaces` + +== Int and BigInt operators +For `Int` and `BigInt` types, the following operators are available: + +* `_DECREMENT` +* `_INCREMENT` + +== Float operators +For `Float` type, the following operators are available: + +* `_ADD` +* `_SUBTRACT` +* `_MULTIPLY` +* `_DIVIDE` + +== Examples + +=== The social platform schema +In this section, we use the following GraphQL schema: + +[source, graphql, indent=0] +---- +type Video { + id: ID @id + views: Int + ownedBy: User @relationship(type: "OWN_VIDEO", properties: "OwnVideo", direction: IN) +} +type User { + id: ID @id + ownVideo: [Video!]! @relationship(type: "OWN_VIDEO", properties: "OwnVideo", direction: OUT) +} +interface OwnVideo @relationshipProperties { + revenue: Float +} +---- + + +=== Simple increment operation +Let's say that a user views a video, so we want to increment `viewersCount` for that video by 1. +[source, graphql, indent=0] +---- +mutation incrementViewCountMutation { + updateVideos( + where: { id: "VideoID" } + update: { views_INCREMENT: 1 } + ) { + videos { + id + views + } + } +} +---- + +=== Nested example +Now, let's say that the video platform wants to reward the user with 0.01 dollars for viewing the video. +To do that, we have to update the relationship property `revenue`. + +[source, graphql, indent=0] +---- +mutation addRevenueMutation { + updateUsers( + where: { id: "UserID" }, + update: { ownVideo: [{ update: { edge: { revenue_ADD: 0.01 } } }] } + ) { + users { + id + ownVideoConnection { + edges { + revenue + } + } + } + } +} +---- + +== Optional fields +Operators remain available for optional fields, this means that if a mathematical operator has been used in a field not defined then a GraphQL error is raised. diff --git a/modules/ROOT/pages/migration/index.adoc b/modules/ROOT/pages/migration/index.adoc deleted file mode 100644 index 4be46dc5..00000000 --- a/modules/ROOT/pages/migration/index.adoc +++ /dev/null @@ -1,727 +0,0 @@ -[[migration-guide]] -= Migration from `neo4j-graphql-js` - -`@neo4j/graphql` was never intended to be a drop-in replacement for `neo4j-graphql-js`. However, simple applications should have a fairly trivial migration process. - -== How to Upgrade - -You need to uninstall the old library and install the new one (and its peer dependencies) using npm or your package manager of choice: - -[source, bash, indent=0] ----- -npm uninstall neo4j-graphql-js -npm install @neo4j/graphql graphql neo4j-driver ----- - -Subscriptions are not supported at this stage. - -[[migration-guide-server]] -== Server - -[[migration-guide-server-schema-generation]] -=== Schema Generation - -In your server codebase, the process of creating an executable schema has changed. For a simple application, what used to be: - -[source, javascript, indent=0] ----- -const { makeAugmentedSchema } = require("neo4j-graphql-js"); - -const typeDefs = require("./type-definitions"); - -const schema = makeAugmentedSchema({ typeDefs }); ----- - -Has become: - -[source, javascript, indent=0] ----- -const { Neo4jGraphQL } = require("@neo4j/graphql"); - -const typeDefs = require("./type-definitions"); - -const neo4jGraphQL = new Neo4jGraphQL({ typeDefs }); - -const schema = await neo4jGraphQL.getSchema(); ----- - -Additionally, the `Neo4jGraphQL` constructor allows you to pass in a driver instance instead of passing one in the context of each request. - -=== Schema Configuration - -The `neo4j-graphql-js` library would allow a user to configure the GraphQL schema via the `makeAugmentedSchema()` function. - -For example, to exclude all queries and all mutations: - -[source, javascript, indent=0] ----- -const schema = makeAugmentedSchema({ - typeDefs, - config: { - query: false, // exclude all queries - mutation: false // exclude all mutations - } -}); ----- - -Or to exclude queries for a specific type like so: - -[source, javascript, indent=0] ----- -const schema = makeAugmentedSchema({ - typeDefs, - config: { - query: { - exclude: ["NameOfTypeToExclude"] // exclude query for type NameOfTypeToExclude - }, - mutation: false // exclude all mutations - } -}); ----- - -To achieve the same behaviour with the new `@neo4j/graphql` library, the GraphQL schema has to be extended instead. Note, no additional configuration or parameters need to be passed to the `getSchema()` function. - -To exclude all mutations for a specific type: -[source, graphql, indent=0] ----- -type NameOfType @exclude(operations: [CREATE, UPDATE, DELETE]) { - name: String -} ----- - -Or to exclude all queries and all mutations for a specific type like so: - -[source, graphql, indent=0] ----- -type NameOfTypeToExclude @exclude { - name: String -} ----- -For more information regarding the above used `@exclude` directive, see xref::/schema-configuration/type-configuration.adoc#_exclude_deprecated[`@exclude`] - -=== Database Configuration - -==== Driver - -Once a schema has been generated, it can be passed into a GraphQL server instance with the driver in the context, identical to using the old library. For example, using Apollo Server and using the schema generated above: - -[source, javascript, indent=0] ----- -const { ApolloServer } = require("apollo-server"); - -const driver = require("./driver"); -const schema = require("./schema"); - -const server = new ApolloServer({ schema, context: { driver } }); - -server.listen().then(({ url }) => { - console.log(`@neo4j/graphql API ready at ${url}`); -}); ----- - -==== Multiple Databases - -Multiple databases were supported in `neo4j-graphql-js` by passing a context key `neo4jDatabase` with the name of a database, for example for database called "sanmateo": - -[source, javascript, indent=0] ----- -const { ApolloServer } = require("apollo-server"); - -const driver = require("./driver"); -const schema = require("./schema"); - -const server = new ApolloServer({ schema, context: { driver, neo4jDatabase: "sanmateo" } }); - -server.listen().then(({ url }) => { - console.log(`@neo4j/graphql API ready at ${url}`); -}); ----- - -In `@neo4j/graphql`, this has now become: - -[source, javascript, indent=0] ----- -const { ApolloServer } = require("apollo-server"); - -const driver = require("./driver"); -const schema = require("./schema"); - -const server = new ApolloServer({ schema, context: { driver, driverConfig: { database: "sanmateo" } } }); - -server.listen().then(({ url }) => { - console.log(`@neo4j/graphql API ready at ${url}`); -}); ----- - -Database bookmarks are also supported. See xref::driver-configuration.adoc[Driver Configuration] for more information. - -[[migration-guide-type-definitions]] -== Type Definitions - -This page will walk through what needs to change in your type definitions before you can pass them into `@neo4j/graphql`. - -=== Directives - -Both `neo4j-graphql-js` and `@neo4j/graphql` are highly driven by GraphQL directives. Each heading in this section will address how/if one or many directives available in `neo4j-graphql-js` can be migrated to `@neo4j/graphql`. - -==== `@relation` - -Migrating this directive is trivial: - -1. Rename `@relation` to `@relationship` -2. Rename the argument `name` to `type` - -For example, `@relation(name: "ACTED_IN", direction: OUT)` becomes `@relationship(type: "ACTED_IN", direction: OUT)`. - -See xref::/type-definitions/types/relationships.adoc[Relationships] for more information on relationships in `@neo4j/graphql`. - -==== Relationship Properties - -If for instance using `neo4j-graphql-js`, you have the following type definitions defining an `ACTED_IN` relationship with a `roles` property: - -[source, graphql, indent=0] ----- -type Actor { - movies: [ActedIn!]! -} - -type Movie { - actors: [ActedIn!]! -} - -type ActedIn @relation(name: "ACTED_IN") { - from: Actor - to: Movie - roles: [String!] -} ----- - -This will need to be refactored to the following in the new library: - -[source, graphql, indent=0] ----- -type Actor { - movies: [Movie!]! @relationship(type: "ACTED_IN", properties: "ActedIn", direction: OUT) -} - -type Movie { - actors: [Actor!]! @relationship(type: "ACTED_IN", properties: "ActedIn", direction: IN) -} - -interface ActedIn @relationshipProperties { - roles: [String!] -} ----- - -Note the following changes to the `ActedIn` type: - -* Changed from `type` to `interface` -* Removed `@relation` directive -* Removed `from` and `to` fields - -And note the following changes to the two node types: - -* Relationship field types changed from the relationship type to the neighbouring node type -* Normal `@relationship` directive added to each relationship field, with an additional `properties` argument pointing to the relationship properties interface - -=== `@cypher` - -No change. See xref::/type-definitions/directives/cypher.adoc[`@cypher` directive] for more details on this directive in `@neo4j/graphql`. - -==== `@neo4j_ignore` - -`@neo4j/graphql` offers two directives for skipping autogeneration for specified types/fields: - -* xref::/schema-configuration/type-configuration.adoc#_exclude_deprecated[`@exclude`]: Skip generation of specified Query/Mutation fields for an object type -* xref::custom-resolvers.adoc#custom-resolver-directive[`@customResolver`]: Ignore a field, which will need custom logic for resolution - -==== `@isAuthenticated`, `@hasRole` and `@hasScope` - -Will require significant migration, but will be worth the effort! See xref::authentication-and-authorization/index.adoc[Authentication and Authorization]. - -==== `@additionalLabels` - -Not supported at this time. - -==== `@id` - -There is an equivalent directive in the new library, but it does not work using database constraints as per the old library. See xref::/type-definitions/directives/autogeneration.adoc#type-definitions-autogeneration-id[`@id`]. - -==== `@unique`, `@index` and `@search` - -These all relate to database indexes and constraints, which are not currently supported by `@neo4j/graphql`. - -=== Types - -==== Scalar Types - -Supported as you would expect, with additional xref::/type-definitions/types/scalar.adoc[`BigInt`] support for 64 bit integers. - -==== Temporal Types (`DateTime`, `Date`) - -Temporal Types have been massively simplified in `@neo4j/graphql`, down to `DateTime` and `Date`, which use ISO 8601 and "yyyy-mm-dd" strings respectively for parsing and serialization. - -In terms of migrating from the old library, the `formatted` field of the old `DateTime` type now becomes the value itself. For example, used in a query: - -[source, graphql, indent=0] ----- -{ - Movie(released: { formatted: "1992-10-09T00:00:00Z" }) { - title - } -} ----- - -Has become: - -[source, graphql, indent=0] ----- -{ - Movie(released: "1992-10-09T00:00:00Z") { - title - } -} ----- - -Due to the move to ISO 8601 strings, input types are no longer necessary for temporal instances, so `_Neo4jDateTimeInput` has become `DateTime` and `_Neo4jDateInput` has become `Date` for input. - -See xref::/type-definitions/types/temporal.adoc[Temporal Types]. - -==== Spatial Types - -The single type in `neo4j-graphql-js`, `Point`, has been split out into two types: - -* xref::/type-definitions/types/spatial.adoc#_point[`Point`] -* xref::/type-definitions/types/spatial.adoc#_cartesianpoint[`CartesianPoint`] - -Correspondingly, `_Neo4jPointInput` has also been split out into two input types: - -* `PointInput` -* `CartesianPointInput` - -Using them in Queries and Mutations should feel remarkably similar. - -==== Interface Types - -Supported, queryable using inline fragments as per `neo4j-graphql-js`, but can also be created using Nested Mutations. See xref::/type-definitions/types/interfaces.adoc[Interfaces]. - -==== Union Types - -Supported, queryable using inline fragments as per `neo4j-graphql-js`, but can also be created using Nested Mutations. See xref::/type-definitions/types/unions.adoc#type-definitions-unions[Unions]. - -=== Fields - -==== `_id` - -An `_id` field exposing the underlying node ID is not included in each type by default in `@neo4j/graphql` like it was in `neo4j-graphql-js`. If you require this functionality (however, it should be noted that underlying node IDs should not be relied on because they can be reused), you can include a field definition such as in the following type definition: - -[source, graphql, indent=0] ----- -type ExampleType { - _id: ID! @cypher(statement: "RETURN ID(this)") -} ----- - -[[migration-guide-queries]] -== Queries - -Using `neo4j-graphql-js`, all of the arguments for a Query were root-level arguments. For example, for the following simple type definition: - -[source, graphql, indent=0] ----- -type Movie { - title: String! - averageRating: Float -} ----- - -The following Query would have been generated: - -[source, graphql, indent=0] ----- -type Query { - Movie(title: String, averageRating: Float, first: Int, offset: Int, orderBy: [_MovieOrdering], filter: _MovieFilter): [Movie] -} ----- - -In `@neo4j/graphql`, the ethos has been to simplify the top-level arguments: - -[source, graphql, indent=0] ----- -type Query { - movies(where: MovieWhere, options: MovieOptions): [Movie]! -} ----- - -Changes to note for migration: - -* Query fields were previously named in the singular, and in _PascalCase_ - they are now pluralized and in _camelCase_ -* Query return types were previously in nullable lists of nullable types - they are now non-nullable lists of non-nullable types, _e.g._ `[Movie]` is now `[Movie!]!`; ensuring either an array of defined `Movie` objects or an empty array. -* In this example, the `_MovieFilter` type has essentially been renamed to `MovieWhere`, the `filter` arguments renamed to `where`, and the top-level field arguments `title` and `averageRating` removed - see xref::migration/index.adoc#migration-guide-queries-filtering[Filtering (`where`)] below -* The `first`, `offset` and `orderBy` have been collapsed into the `MovieOptions` type and renamed `limit`, `offset` and `sort`, respectively - see xref::migration/index.adoc#migration-guide-queries-options[Sorting and Pagination (`options`)] below. - -[[migration-guide-queries-filtering]] -=== Filtering (`where`) - -Simple equality fields are no longer available at the root of Query fields. As a simple demonstration, a simple query using `neo4j-graphql-js` that looked like: - -[source, graphql, indent=0] ----- -query { - Movie(title: "Forrest Gump") { - averageRating - } -} ----- - -Is now changed to the following using `@neo4j/graphql`: - -[source, graphql, indent=0] ----- -query { - movies(where: { title: "Forrest Gump" }) { - averageRating - } -} ----- - -When discussing how the field `where` of type `MovieWhere` differs to the field `filter` of `_MovieFilter` the following table can be used for guidance: - -.Comparing the fields of the `_MovieFilter` and `MovieWhere` types -[cols="1,1"] -|=== -|`neo4j-graphql-js` |`@neo4j/graphql` - -|`AND: [_MovieFilter!]` -|`AND: [MovieWhere!]` - -|`OR: [_MovieFilter!]` -|`OR: [MovieWhere!]` - -|`NOT: _MovieFilter!` -|`NOT: MovieWhere!` - -|`title: String` -|`title: String` - -|`title_in: [String!]` -|`title_IN: [String!]` - -|`title_contains: String` -|`title_CONTAINS: String` - -|`title_starts_with: String` -|`title_STARTS_WITH: String` - -|`title_ends_with: String` -|`title_ENDS_WITH: String` - -|`title_regexp: String` -|`title_MATCHES: String` - -|`averageRating: Float` -|`averageRating: Float` - -|`averageRating_in: [Float!]` -|`averageRating_IN: [Float]` - -|`averageRating_lt: Float` -|`averageRating_LT: Float` - -|`averageRating_lte: Float` -|`averageRating_LTE: Float` - -|`averageRating_gt: Float` -|`averageRating_GT: Float` - -|`averageRating_gte: Float` -|`averageRating_GTE: Float` -|=== - -For filtering on relationship fields, the `_some`, `_none`, `_single` and `_every` filters are not yet implemented. - -[[migration-guide-queries-options]] -=== Sorting and Pagination (`options`) - -==== Sorting - -Sorting has changed somewhat in `@neo4j/graphql`. For the example being used in this page, the `_MovieOrdering` type in `neo4j-graphql-js` was an enum which looked like the following: - -[source, graphql, indent=0] ----- -enum _MovieOrdering { - title_asc - title_desc - averageRating_asc - averageRating_desc -} ----- - -You could then query all movies ordered by title ascending by executing: - -[source, graphql, indent=0] ----- -query { - Movie(orderBy: [title_asc]) { - title - } -} ----- - -In `@neo4j/graphql`, the sorting type `MovieSort` has become an input type with each field as an enum, like follows: - -[source, graphql, indent=0] ----- -enum SortDirection { - ASC - DESC -} - -input MovieSort { - title: SortDirection - averageRating: SortDirection -} ----- - -To fetch all movies sorted by title ascending as per above, you would execute: - -[source, graphql, indent=0] ----- -query { - movies(options: { sort: [{ title: ASC }] }) { - title - } -} ----- - -==== Pagination - -Pagination is broadly similar, with the arguments just renamed and moved a level deeper. For example, to return "page 3, with 10 results per page" using `neo4j-graphql-js` was: - -[source, graphql, indent=0] ----- -query { - Movie(offset: 20, first: 10) { - title - } -} ----- - -Using `@neo4j/graphql`, this will now be: - -[source, graphql, indent=0] ----- -query { - movies(options: { offset: 20, limit: 10 }) { - title - } -} ----- - -[[migration-guide-mutations]] -== Mutations - -This section will walk through each operation available through GraphQL Mutations, and how to migrate each from `neo4j-graphql-js` to `@neo4j/graphql`. - -The examples in this section will be based off the following type definitions (which have been migrated over to `@neo4j/graphql` syntax): - -[source, graphql, indent=0] ----- -type Actor { - name: String! - movies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT) -} - -type Movie { - title: String! - averageRating: Float - actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN) -} ----- - -A summary of migration points is as follows: - -* Mutations which were previously in the singular and in _PascalCase_ are now pluralized and in _camelCase_ - for example `CreateMovie` has become `createMovies` -* Connect and disconnect Mutations are no longer present, these operations are accessed through an update Mutation -* The object(s) being mutated are returned as a nested field, to allow for metadata about the operation to be added in future -* Mutation arguments are now commonly named between different types, but with different input types - such as `where` and `input` - -> Note that xref::mutations/index.adoc[Mutations] in `@neo4j/graphql` are incredibly powerful, and it is well worthwhile reading about them in full. You might find that you can collapse multiple current mutations down into one! - -=== Creating - -For creating nodes, the input arguments of `neo4j-graphql` have been moved inside an `input` argument in `@neo4j/graphql`. - -For example, creating a movie using the old library: - -[source, graphql, indent=0] ----- -mutation { - CreateMovie(title: "Forrest Gump") { - title - } -} ----- - -Looks like the following using `@neo4j/graphql`: - -[source, graphql, indent=0] ----- -mutation { - createMovies(input: { title: "Forrest Gump" }) { - movies { - title - } - } -} ----- - -Note that `movies` must also be first selected in the selection set. - -=== Updating - -An update Mutation using `neo4j-graphql-js` had all of the arguments at the root of the Mutation, including the filter and fields to change. - -This has all changed in `@neo4j/graphql`, with a `where` argument to select the node, and then an `update` argument (amongst many others) to select what to update. - -For example, updating the average rating of the Movie Forrest Gump: - -[source, graphql, indent=0] ----- -mutation { - UpdateMovie(title: "Forrest Gump", averageRating: 5.0) { - title - averageRating - } -} ----- - -Will look like the following using the new library: - -[source, graphql, indent=0] ----- -mutation { - updateMovies(where: { title: "Forrest Gump" }, update: { averageRating: 5.0 }) { - movies { - title - averageRating - } - } -} ----- - -=== Deleting - -The arguments for selecting which node to delete have now been moved into a `where` argument. - -Additionally, the return value is now a `DeleteInfo` object informing how many nodes and relationships were deleted. - -For example, deleting a movie: - -[source, graphql, indent=0] ----- -mutation { - DeleteMovie(title: "Forrest Gump") { - title - } -} ----- - -Looks like the following using `@neo4j/graphql`: - -[source, graphql, indent=0] ----- -mutation { - deleteMovies(where: { title: "Forrest Gump" }) { - nodesDeleted - relationshipsDeleted - } -} ----- - -=== Connecting - -Using `neo4j-graphql-js`, connecting two of the nodes in this example would have been achieved by executing either the `AddMovieActors` or `AddActorMovies` Mutation. - -In `@neo4j/graphql`, this is achieved by specifying the `connect` argument on either the `updateMovies` or `updateActors` Mutation. - -For example: - -[source, graphql, indent=0] ----- -mutation { - AddMovieActors(from: { name: "Tom Hanks" }, to: { title: "Forrest Gump" }) { - from { - name - } - to { - title - } - } -} ----- - -Would become the following using `@neo4j/graphql`: - -[source, graphql, indent=0] ----- -mutation { - updateMovies( - where: { title: "Forrest Gump" } - connect: { actors: { where: { node: { name: "Tom Hanks" } } } } - ) { - movies { - title - actors { - name - } - } - } -} ----- - -Note, there are many ways to achieve the same goal using the powerful Mutation ability of `@neo4j/graphql`, so do what feels best for your data! - -=== Disconnecting - -Similarly to connecting, using `neo4j-graphql-js`, disconnecting two of the nodes in this example would have been achieved by executing either the `RemoveMovieActors` or `RemoveActorMovies` Mutation. - -In `@neo4j/graphql`, this is achieved by specifying the `disconnect` argument on either the `updateMovies` or `updateActors` Mutation. - -For example: - -[source, graphql, indent=0] ----- -mutation { - RemoveMovieActors(from: { name: "Tom Hanks" }, to: { title: "Forrest Gump" }) { - from { - name - } - to { - title - } - } -} ----- - -Would become the following using `@neo4j/graphql`: - -[source, graphql, indent=0] ----- -mutation { - updateMovies( - where: { title: "Forrest Gump" } - disconnect: { actors: { where: { node: { name: "Tom Hanks" } } } } - ) { - movies { - title - actors { - name - } - } - } -} ----- - -In the result field `actors`, Tom Hanks should no longer be present. - diff --git a/modules/ROOT/pages/mutations/create.adoc b/modules/ROOT/pages/mutations/create.adoc index 8e5c0caf..2df624fb 100644 --- a/modules/ROOT/pages/mutations/create.adoc +++ b/modules/ROOT/pages/mutations/create.adoc @@ -1,8 +1,7 @@ [[mutations-create]] -:description: This page describes how to create nodes through mutations. -= `create` += Create -Using the following type definitions: +Using the following type definitions for these examples: [source, graphql, indent=0] ---- @@ -19,7 +18,7 @@ type User { } ---- -These `create` mutations and response types should be generated: +The following create mutations and response types will be generated for the above type definitions: [source, graphql, indent=0] ---- @@ -37,17 +36,13 @@ type Mutation { } ---- -Note that the `CreateInput` types closely mirror the object type definitions. -This allows you to create not only the type in question, but to recurse down and perform further operations on related types in the same mutation. +The `CreateInput` types closely mirror the object type definitions, allowing you to create not only the type in question, but to recurse down and perform further operations on related types in the same Mutation. -[NOTE] -==== -The `id` field is absent from both `create` input types as the xref::/type-definitions/directives/autogeneration.adoc#type-definitions-autogeneration-id[`@id`] directive has been used. -==== +> The `id` field will be absent from both create input types as the xref::type-definitions/autogeneration.adoc#type-definitions-autogeneration-id[`@id`] directive has been used. -== Single `create` +== Single create -A single `User` can be created by executing the following GraphQL statement: +A single user can be created by executing the following GraphQL statement: [source, graphql, indent=0] ---- @@ -65,11 +60,11 @@ mutation { } ---- -This should create a `User` with name "John Doe", and that name plus the autogenerated ID should be returned. +This will create a User with name "John Doe", and that name plus the autogenerated ID will be returned. -== Nested `create` +== Nested create -A `User` and an initial `Post` can be created by executing the following: +A User and an initial Post can be created by executing the following: [source, graphql, indent=0] ---- @@ -100,19 +95,10 @@ mutation { } ---- -This creates a `User` with name "John Doe" and an introductory post. -Both should be returned with their autogenerated IDs. - -[NOTE] -==== -You can perform similar and complementary actions by using the `update` mutation combined with `create`. -Read about xref:mutations/update.adoc#_connectorcreate_relationships[`update`] for more information. -==== +This will create a User with name "John Doe", an introductory Post, both of which will be returned with their autogenerated IDs. == `connectOrCreate` relationships - -If a related node has a `@unique` or `@id` directive defined, `connectOrCreate` can be used in a nested `create` to perform a `MERGE` operation on the related node. -This should create a new relationship and the related node if it doesn't exist yet. +If a related node has a `@unique` or `@id` directive defined, `connectOrCreate` can be used in a nested create to perform a `MERGE` operation on the related node, creating a new relationship and the related node if it doesn't exist. Consider the following type definitions: @@ -130,8 +116,7 @@ type Movie { } ---- -Because a movie ID is unique, `connectOrCreate` can be used in an `Actor` mutation to ensure the movie exists in the database before connecting. -Note that only `@unique` or `@id` fields can be used in `where`: +Because a movie ID is unique, `connectOrCreate` can be used in an Actor mutation to ensure a movie exists before connecting. Note that only `@unique` or `@id` fields can be used in `where`: [source, graphql, indent=0] ---- @@ -153,14 +138,3 @@ mutation { ---- This will ensure that a movie with ID 1234 exists and it is connected to `"Tom Hanks"`. If the movie does not exist, it will be created with the title `"Forrest Gump"`. Note that if the movie with the given ID already exists, it will be connected to it, regardless of the title. - -== `CREATE` optimization - -With the `create` operations, there is no limit on how many nodes can be created at once. -However, there is a known performance issue for large batch sizes. - -The Neo4j GraphQL Library contains an optimization feature designed to mitigate it, but it does not work in the following scenarios: - -* A field is populated using the directive `@populated_by`. -* The `connect` or `connectOrCreate` operation is used. -* Interface and union types are present in the mutation. \ No newline at end of file diff --git a/modules/ROOT/pages/mutations/delete.adoc b/modules/ROOT/pages/mutations/delete.adoc index 291ff31c..8ffe4a51 100644 --- a/modules/ROOT/pages/mutations/delete.adoc +++ b/modules/ROOT/pages/mutations/delete.adoc @@ -1,8 +1,7 @@ [[mutations-delete]] -:description: This page describes how to delete nodes using mutations. -= `delete` += Delete -Using these type definitions: +Using the following type definitions for these examples: [source, graphql, indent=0] ---- @@ -19,7 +18,7 @@ type User { } ---- -These `delete` mutations and response types should be generated: +The following delete mutations and response type will be generated for the above type definitions: [source, graphql, indent=0] ---- @@ -34,12 +33,9 @@ type Mutation { } ---- -[NOTE] -==== -The `DeleteInfo` type is the common return type for all delete mutations. -==== +Note that the `DeleteInfo` type is the common return type for all delete mutations. -== Single `delete` +== Single Delete A single post can be deleted by executing the following GraphQL statement: @@ -55,25 +51,34 @@ mutation { } ---- -This should delete the post using the autogenerated ID that was returned after that post's creation. -Consequently, `nodesDeleted` should be equal `1` (the post) and `relationshipsDeleted` should also equal `1` as the `HAS_POST` relationship between the `Post` and its author was deleted. +This will delete the post using the autogenerated ID that would have been returned after that post's creation. -== Nested `delete` +`nodesDeleted` would equal 1 (the post) and `relationshipsDeleted` would also equal equal 1 (the `HAS_POST` relationship between the Post and its author). -In case you want to delete a `User` *and* all of their posts, you can use a single nested `delete` mutation: +== Nested Delete + +Say that if when you delete a User, you want to delete _all_ of their Posts as well. This can be achieved using a single nested delete operations: [source, graphql, indent=0] ---- mutation { - deleteUsers(where: { name: "Jane Doe" }, delete: { posts: {} }) { - nodesDeleted - relationshipsDeleted - } + deleteUsers( + where: { + name: "Jane Doe" + }, + delete: { + posts: [ + where: { } + ] + } + ) { + nodesDeleted + relationshipsDeleted + } } ---- -By the time the traversal has reached it, that empty `where` argument has the context of only refer to posts that were created by Jane Doe, as the traversals to those `Post` nodes were from her `User` node. -Essentially, the above query is equivalent to: +You may look at that empty `where` argument and wonder what that's doing. By the time the traversal has reached that argument, it has the context of only posts that were created by Jane Doe, as the traversals to those Post nodes were from her User node. Essentially, the above query is equivalent to: [source, graphql, indent=0] ---- @@ -100,12 +105,4 @@ mutation { } ---- -Note that the output Cypher statement should also have a redundant `WHERE` clause: - -//Please add the cypher statement: - -//[source, cypher, indent=0] -//---- -//DELETE User (name:"Jane Doe") -//WHERE Posts - -//---- +Slightly easier to reason with, but the output Cypher statement will have a redundant `WHERE` clause! diff --git a/modules/ROOT/pages/mutations/index.adoc b/modules/ROOT/pages/mutations/index.adoc index 16cc7055..eb28eb87 100644 --- a/modules/ROOT/pages/mutations/index.adoc +++ b/modules/ROOT/pages/mutations/index.adoc @@ -1,18 +1,18 @@ [[mutations]] -:description: This section describes how to use mutations with the Neo4j GraphQL Library. = Mutations -This section addresses basic examples of the following mutations: +Several mutations are automatically generated for each type defined in type definitions, which are covered in the following chapters: -- xref::mutations/create.adoc[`create`] - create nodes, and recursively create or connect further nodes in the graph. -- xref::mutations/update.adoc[`update`] - update nodes, and recursively perform any operations from there. -- xref::mutations/delete.adoc[`delete`] - delete nodes, and recursively delete or disconnect further nodes in the graph. +- xref::mutations/create.adoc[Create] - create nodes, and recursively create or connect further nodes in the graph +- xref::mutations/update.adoc[Update] - update nodes, and recursively perform any operations from there +- xref::mutations/delete.adoc[Delete] - delete nodes, and recursively delete or disconnect further nodes in the graph -[NOTE] -==== -In order to provide the abstractions available in these mutations, the output Cypher can end up being complex. -This can result in your database throwing out-of-memory errors depending on its configuration. +== A note on nested mutations + +You will see some basic examples of nested mutations in this chapter, which barely scratch the surface of what can be achieved with them. It's encouraged to explore the power of what you can do with them! + +However, it has to be noted that in order to provide the abstractions available in these mutations, the output Cypher can end up being extremely complex, which can result in your database throwing out-of-memory errors depending on its configuration. + +If out-of-memory errors are a regular occurrence, you can adjust the `dbms.memory.heap.max_size` parameter in the DBMS settings. -If this becomes a regular occurrence, you can adjust the link:https://neo4j.com/docs/operations-manual/current/configuration/configuration-settings/#config_server.memory.heap.max_size[`server.memory.heap.max_size`] parameter in the DBMS settings. If you need to perform major data migrations, it may be best to manually write the necessary Cypher and execute this directly in the database. -==== \ No newline at end of file diff --git a/modules/ROOT/pages/mutations/update.adoc b/modules/ROOT/pages/mutations/update.adoc index 146c2ca4..885d6f31 100644 --- a/modules/ROOT/pages/mutations/update.adoc +++ b/modules/ROOT/pages/mutations/update.adoc @@ -1,8 +1,7 @@ [[mutations-update]] -:description: This page describes how to update nodes through mutations. -= `update` += Update -Using these type definitions: +Using the following type definitions for these examples: [source, graphql, indent=0] ---- @@ -19,7 +18,7 @@ type User { } ---- -These `update` mutations and response types should be generated: +The following update mutations and response types will be generated for the above type definitions: [source, graphql, indent=0] ---- @@ -51,14 +50,11 @@ type Mutation { } ---- -[NOTE] -==== -The `id` field cannot be updated as the xref::/type-definitions/directives/autogeneration.adoc#type-definitions-autogeneration-id[`@id`] directive has been used. -==== +> The `id` field not be update-able as the xref::type-definitions/autogeneration.adoc#type-definitions-autogeneration-id[`@id`] directive has been used. -== Single `update` +== Single update -The content of a `Post` can be updated by executing the following GraphQL statement: +Say you wanted to edit the content of a Post: [source, graphql, indent=0] ---- @@ -78,11 +74,9 @@ mutation { } ---- -This should update the post by adding the sentence "Some new content for this Post!". +== Nested create -== Nested `create` using `update` - -Instead of creating a `Post` with the `create` mutation and then connecting it to a `User`, you can update a `User` and `create` a `Post` as part of the mutation: +Instead of creating a Post and connecting it to a User, you could update a User and create a Post as part of the mutation: [source, graphql, indent=0] ---- @@ -107,41 +101,39 @@ mutation { ---- == `connectOrCreate` relationships +If a related node has a `@unique` or `@id` directive defined, `connectOrCreate` can be used in a nested update to perform a `MERGE` operation on the related node, creating a new relationship and the related node if it doesn't exist. -Consider the example provided in the xref:mutations/create.adoc#_connectorcreate_relationships[`create`] page: +Consider the following type definitions: [source, graphql, indent=0] ---- -mutation { - createActors(input: { - name: "Tom Hanks", - movies: { - connectOrCreate: { - where: { node: { id: "1234" } } - onCreate: { node: { title: "Forrest Gump" } } - } - } - }) { - info { - nodesCreated - } - } +type Actor { + name: String! + movies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT) +} + +type Movie { + title: String + id: ID! @id + actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN) } ---- -For `update` operations, `connectOrCreate` can also be used as a top-level input for an equivalent operation: +Because a movie ID is unique, `connectOrCreate` can be used in an Actor mutation to ensure a movie exists before connecting. Note that only `@unique` or `@id` fields can be used in `where`: [source, graphql, indent=0] ---- mutation { updateActors( - connectOrCreate: { + update: { movies: { + connectOrCreate: { where: { node: { id: "1234" } } onCreate: { node: { title: "Forrest Gump" } } + } } - }, - where: { name: "Tom Hanks" } + }, + where: { name: "Tom Hanks" } ) { info { nodesCreated @@ -150,347 +142,27 @@ mutation { } ---- -This operation is equivalent to the previous example. - -== Array methods - -Array methods allow the modification of existing property arrays in `update` mutations within these entities: - -* Node -* Relationship properties -* Interfaces - -For that, the following operators are available: - -* `_POP`: expects a single `Int` value as input. -* `_PUSH`: conforms to the type of input defined in the type definition. - -Consider the following type definitions, a `Movie` with a property array called `tags`: - -[source, graphql, indent=0] ----- -type Movie { - title: String - tags: [String] -} ----- - -You can pop from this `tags` property array: - -[cols="1,1"] -|=== -| Before | After - -a| -``` -tags: ['a', 'b', 'c'] -``` -a| -``` -tags: ['a', 'b'] -``` -|=== - -[source, graphql, indent=0] ----- -mutation { - updateMovies (update: { tags_POP: 1 }) { - movies { - title - tags - } - } -} ----- - -Or, for more than one property from the array: - -[cols="1,1"] -|=== -| Before | After - -a| -``` -tags: ['a', 'b', 'c'] -``` -a| -``` -tags: ['a'] -``` -|=== - -[source, graphql, indent=0] ----- -mutation { - updateMovies (update: { tags_POP: 2 }) { - movies { - title - tags - } - } -} ----- - -Similarly, you can have multiple array property fields and update them in the same query: - -[source, graphql, indent=0] ----- -type Movie { - title: String - tags: [String] - moreTags: [String] -} ----- - -Then, you can `POP` from both the `tags` and `moreTags` property arrays: - -[cols="1,1"] -|=== -| Before | After - -a| -``` -tags: ['a', 'b', 'c'] -moreTags: ['x', 'y', 'z'] -``` -a| -``` - tags: ['a', 'b'] - moreTags: ['x'] -``` -|=== - -[source, graphql, indent=0] ----- -mutation { - updateMovies (update: { tags_POP: 1, moreTags_POP: 2 }) { - movies { - title - tags - moreTags - } - } -} ----- - -=== `_PUSH` - -Using the same type definitions as before, you can push to the `tags` property array: - -[col=1,1] -|==== -| Before | After - -| `['some tag']` - -| `['some tag', 'another tag']` -|==== - -[source, graphql, indent=0] ----- -mutation { - updateMovies (update: { tags_PUSH: "another tag" }) { - movies { - title - tags - } - } -} ----- - -Or push multiple elements in a single update: +In this case, all actors matching `"Tom Hanks"` will be connected to the Movie with id `"1234"`. If the movie with given ID does not exist, it will be created with the title `"Forrest Gump"`. -[col=1,1] -|==== -| Before | After - -| `['some tag']` -| `['some tag', 'another tag', 'one more tag']` -|==== - -[source, graphql, indent=0] ----- -mutation { - updateMovies (update: { tags_PUSH: ["another tag", "one more tag"] }) { - movies { - title - tags - } - } -} ----- - -Similarly, you can have multiple array property fields and update them in the same query: - -[source, graphql, indent=0] ----- -type Movie { - title: String - tags: [String] - moreTags: [String] -} ----- - -You can also push to both the `tags` and `moreTags` property arrays: - -[col=1,1] -|==== -| Before | After - -a| -``` - tags: ['some tag'] - moreTags: [] -``` -a| -``` - tags: ['some tag', 'another tag'] - moreTags ['a different tag'] -``` -|==== - -[source, graphql, indent=0] ----- -mutation { - updateMovies (update: { tags_PUSH: "another tag", moreTags_PUSH: "a different tag" }) { - movies { - title - tags - moreTags - } - } -} ----- - -=== Combined methods - -It is possible to perform both a `_PUSH` and `_POP` operation in one single `update` mutation. - -Consider the following type definitions: - -[source, graphql, indent=0] ----- -type Movie { - title: String - tags: [String] - moreTags: [String] -} ----- - -You can update both property arrays with either `_POP` or `_PUSH` operators at once: - -[col=1,1] -|==== -| Before | After - -a| -``` - tags: ['some tag'] - moreTags: [] -``` - -a| -``` - tags: [] - moreTags ['a different tag'] -``` -|==== +For update operations, `connectOrCreate` can also be used as a top-level input: [source, graphql, indent=0] ---- mutation { - updateMovies (update: { tags_POP: 1, moreTags_PUSH: "a different tag" }) { - movies { - title - tags - moreTags + updateActors( + connectOrCreate: { + movies: { + where: { node: { id: "1234" } } + onCreate: { node: { title: "Forrest Gump" } } } - } -} ----- - -== Mathematical operators - -Mathematical operators can be used to update numerical fields based on their original values in a single DB transaction. -For that, specific operators are available on different numerical types: `Int`, `Float`, xref::/type-definitions/types/scalar.adoc[`BigInt`]. -They are supported within these entities: - -* Nodes -* Relationship properties -* Interfaces - -For `Int` and `BigInt` types, the following operators are available: - -* `_DECREMENT` -* `_INCREMENT` - -For `Float` type, the following operators are available: - -* `_ADD` -* `_SUBTRACT` -* `_MULTIPLY` -* `_DIVIDE` - -[NOTE] -==== -Operators remain available as optional fields. -If a mathematical operator has been used in a field not defined, it will prompt a GraphQL error. -==== - -For example, take the following GraphQL schema for a social video platform: - -[source, graphql, indent=0] ----- -type Video { - id: ID @id - views: Int - ownedBy: User @relationship(type: "OWN_VIDEO", properties: "OwnVideo", direction: IN) -} -type User { - id: ID @id - ownVideo: [Video!]! @relationship(type: "OWN_VIDEO", properties: "OwnVideo", direction: OUT) -} -interface OwnVideo @relationshipProperties { - revenue: Float -} ----- - -Suppose a user viewed a video in this platform, so that you want to increment `viewersCount` for that video by `1`. -Here is how you can do that: - -[source, graphql, indent=0] ----- -mutation incrementViewCountMutation { - updateVideos( - where: { id: "VideoID" } - update: { views_INCREMENT: 1 } + }, + where: { name: "Tom Hanks" } ) { - videos { - id - views + info { + nodesCreated } } } ---- -Now, suppose the social platform wants to reward the user with 0.01 dollars for viewing the video. -To do that, you have to update the relationship property `revenue`: - -[source, graphql, indent=0] ----- -mutation addRevenueMutation { - updateUsers( - where: { id: "UserID" }, - update: { ownVideo: [{ update: { edge: { revenue_ADD: 0.01 } } }] } - ) { - users { - id - ownVideoConnection { - edges { - revenue - } - } - } - } -} ----- +This operation is equivalent to the previous example. diff --git a/modules/ROOT/pages/ogm/api-reference/index.adoc b/modules/ROOT/pages/ogm/api-reference/index.adoc new file mode 100644 index 00000000..6cbd6c8f --- /dev/null +++ b/modules/ROOT/pages/ogm/api-reference/index.adoc @@ -0,0 +1,7 @@ +[[ogm-api-reference]] += API Reference + +- xref::ogm/api-reference/ogm.adoc[`OGM`] +- xref::ogm/api-reference/model/index.adoc[`Model`] +- xref::ogm/api-reference/type-generation.adoc[`Type Generation`] + diff --git a/modules/ROOT/pages/ogm/api-reference/model/aggregate.adoc b/modules/ROOT/pages/ogm/api-reference/model/aggregate.adoc new file mode 100644 index 00000000..98a7c3bf --- /dev/null +++ b/modules/ROOT/pages/ogm/api-reference/model/aggregate.adoc @@ -0,0 +1,50 @@ +[[ogm-api-reference-model-aggregate]] += `aggregate` + +This method can be used to aggregate nodes, and maps to the underlying schema xref::queries.adoc#queries-aggregate[Aggregate]. + +== Example + +Find the longest User name: + +[source, javascript, indent=0] +---- +const User = ogm.model("User"); + +const usersAggregate = await User.aggregate({ + aggregate: { + name: { + longest: true + } + } +}); +---- + +Find the longest User name where name starts with the letter "D": + +[source, javascript, indent=0] +---- +const User = ogm.model("User"); + +const usersAggregate = await User.aggregate({ + where: { + name_STARTS_WITH: "D" + }, + aggregate: { + name: { + longest: true + } + } +}); +---- + +== Arguments + +|=== +|Name and Type |Description + +|`where` + + + + Type: `GraphQLWhereArg` +|A JavaScript object representation of the GraphQL `where` input type used for xref::filtering.adoc[Filtering]. +|=== diff --git a/modules/ROOT/pages/ogm/api-reference/model/create.adoc b/modules/ROOT/pages/ogm/api-reference/model/create.adoc new file mode 100644 index 00000000..6108e80c --- /dev/null +++ b/modules/ROOT/pages/ogm/api-reference/model/create.adoc @@ -0,0 +1,48 @@ +[[ogm-api-reference-model-create]] += `create` + +This method can be used to update nodes, and maps to the underlying xref::mutations/create.adoc[Create] mutation. + +Returns a `Promise` that resolves to the equivalent of the mutation response for this operation. + +== Example + +To create a Movie with title "The Matrix": + +[source, javascript, indent=0] +---- +const Movie = ogm.model("Movie"); + +await Movie.create({ input: [{ title: "The Matrix" }] }) +---- + +== Arguments + +|=== +|Name and Type |Description + +|`input` + + + + Type: `any` +|JavaScript object representation of the GraphQL `input` input type used for xref::mutations/create.adoc[Create] mutations. + +|`selectionSet` + + + + Type: `string` or `DocumentNode` or `SelectionSetNode` +|Selection set for the mutation, see xref::ogm/selection-set.adoc[Selection Set] for more information. + +|`args` + + + + Type: `any` +|The `args` value for the GraphQL mutation. + +|`context` + + + + Type: `any` +|The `context` value for the GraphQL mutation. A `driver`, `session` or `transaction` can be passed into the `executionContext` field of the `context`. See https://neo4j.com/docs/javascript-manual/current/transactions/[Transactions] for more details on running transactions. + +|`rootValue` + + + + Type: `any` +|The `rootValue` value for the GraphQL mutation. +|=== diff --git a/modules/ROOT/pages/ogm/api-reference/model/delete.adoc b/modules/ROOT/pages/ogm/api-reference/model/delete.adoc new file mode 100644 index 00000000..c46a75a7 --- /dev/null +++ b/modules/ROOT/pages/ogm/api-reference/model/delete.adoc @@ -0,0 +1,58 @@ +[[ogm-api-reference-model-delete]] += `delete` + +This method can be used to delete nodes, and maps to the underlying xref::mutations/delete.adoc[Delete] mutation. + +Returns a `Promise` which resolvers to a `DeleteInfo` object: + +|=== +|Name and Type |Description + +|`nodesDeleted` + + + + Type: `number` +|The number of nodes deleted. + +|`relationshipsDeleted` + + + + Type: `number` +|The number of relationships deleted. +|=== + +== Example + +To delete all User nodes where the name is "Dan": + +[source, javascript, indent=0] +---- +const User = ogm.model("User"); + +await User.delete({ where: { name: "Dan" }}); +---- + +== Arguments + +|=== +|Name and Type |Description + +|`where` + + + + Type: `GraphQLWhereArg` +|A JavaScript object representation of the GraphQL `where` input type used for xref::filtering.adoc[Filtering]. + +|`delete` + + + + Type: `string` or `DocumentNode` or `SelectionSetNode` +|A JavaScript object representation of the GraphQL `delete` input type used for xref::mutations/delete.adoc[Delete] mutations. + +|`context` + + + + Type: `any` +|The `context` value for the GraphQL mutation. A `driver`, `session` or `transaction` can be passed into the `executionContext` field of the `context`. See https://neo4j.com/docs/javascript-manual/current/transactions/[Transactions] for more details on running transactions. + + +|`rootValue` + + + + Type: `any` +|The `rootValue` value for the GraphQL mutation. +|=== diff --git a/modules/ROOT/pages/ogm/api-reference/model/find.adoc b/modules/ROOT/pages/ogm/api-reference/model/find.adoc new file mode 100644 index 00000000..557ac0fe --- /dev/null +++ b/modules/ROOT/pages/ogm/api-reference/model/find.adoc @@ -0,0 +1,62 @@ +[[ogm-api-reference-model-find]] += `find` + +This method can be used to find nodes, and maps to the underlying schema xref::queries.adoc[queries]. + +Returns a `Promise` which resolvers to an array of objects matching the type of the Model. + +== Example + +To find all user nodes in the database: + +[source, javascript, indent=0] +---- +const User = ogm.model("User"); + +const users = await User.find(); +---- + +To find users with name "Jane Smith": + +[source, javascript, indent=0] +---- +const User = ogm.model("User"); + +const users = await User.find({ where: { name: "Jane Smith" }}); +---- + +== Arguments + +|=== +|Name and Type |Description + +|`where` + + + + Type: `GraphQLWhereArg` +|A JavaScript object representation of the GraphQL `where` input type used for xref::filtering.adoc[Filtering]. + +|`options` + + + + Type: `GraphQLOptionsArg` +|A JavaScript object representation of the GraphQL `options` input type used for xref::sorting.adoc[Sorting] and xref::pagination/index.adoc[Pagination]. + +|`selectionSet` + + + + Type: `string` or `DocumentNode` or `SelectionSetNode` +|Selection set for the mutation, see xref::ogm/selection-set.adoc[Selection Set] for more information. + +|`args` + + + + Type: `any` +|The `args` value for the GraphQL mutation. + +|`context` + + + + Type: `any` +|The `context` value for the GraphQL mutation. + +|`rootValue` + + + + Type: `any` +|The `rootValue` value for the GraphQL mutation. +|=== diff --git a/modules/ROOT/pages/ogm/api-reference/model/index.adoc b/modules/ROOT/pages/ogm/api-reference/model/index.adoc new file mode 100644 index 00000000..114ccc24 --- /dev/null +++ b/modules/ROOT/pages/ogm/api-reference/model/index.adoc @@ -0,0 +1,22 @@ +[[ogm-api-reference-model]] += `Model` + +== `create` + +See xref::ogm/api-reference/model/create.adoc[`create`]. + +== `find` + +See xref::ogm/api-reference/model/find.adoc[`find`]. + +== `update` + +See xref::ogm/api-reference/model/update.adoc[`update`]. + +== `delete` + +See xref::ogm/api-reference/model/delete.adoc[`delete`]. + +== `aggregate` + +See xref::ogm/api-reference/model/aggregate.adoc[`aggregate`]. diff --git a/modules/ROOT/pages/ogm/api-reference/model/update.adoc b/modules/ROOT/pages/ogm/api-reference/model/update.adoc new file mode 100644 index 00000000..5a37faf8 --- /dev/null +++ b/modules/ROOT/pages/ogm/api-reference/model/update.adoc @@ -0,0 +1,77 @@ +[[ogm-api-reference-model-update]] += `update` + +This method can be used to update nodes, and maps to the underlying xref::mutations/update.adoc[Update] mutation. + +Returns a `Promise` that resolves to the equivalent of the mutation response for this operation. + +== Example + +For the User with name "John", update their name to be "Jane": + +[source, javascript, indent=0] +---- +const User = ogm.model("User"); + +const { users } = await User.update({ + where: { name: "John" }, + update: { name: "Jane" }, +}); +---- + +== Arguments + +|=== +|Name and Type |Description + +|`where` + + + + Type: `GraphQLWhereArg` +|A JavaScript object representation of the GraphQL `where` input type used for xref::filtering.adoc[Filtering]. + +|`update` + + + + Type: `any` +|A JavaScript object representation of the GraphQL `update` input type used for xref::mutations/update.adoc[Update] mutations. + +|`connect` + + + + Type: `any` +|A JavaScript object representation of the GraphQL `connect` input type used for xref::mutations/update.adoc[Update] mutations. + +|`disconnect` + + + + Type: `any` +|A JavaScript object representation of the GraphQL `disconnect` input type used for xref::mutations/update.adoc[Update] mutations. + +|`create` + + + + Type: `any` +|A JavaScript object representation of the GraphQL `create` input type used for xref::mutations/update.adoc[Update] mutations. + +|`options` + + + + Type: `GraphQLOptionsArg` +|A JavaScript object representation of the GraphQL `options` input type used for xref::sorting.adoc[Sorting] and xref::pagination/index.adoc[Pagination]. + +|`selectionSet` + + + + Type: `string` or `DocumentNode` or `SelectionSetNode` +|Selection set for the mutation, see xref::ogm/selection-set.adoc[Selection Set] for more information. + +|`args` + + + + Type: `any` +|The `args` value for the GraphQL mutation. + +|`context` + + + + Type: `any` +|The `context` value for the GraphQL mutation. A `driver`, `session` or `transaction` can be passed into the `executionContext` field of the `context`. See https://neo4j.com/docs/javascript-manual/current/transactions/[Transactions] for more details on running transactions. + + +|`rootValue` + + + + Type: `any` +|The `rootValue` value for the GraphQL mutation. +|=== diff --git a/modules/ROOT/pages/ogm/api-reference/ogm.adoc b/modules/ROOT/pages/ogm/api-reference/ogm.adoc new file mode 100644 index 00000000..9c34cf31 --- /dev/null +++ b/modules/ROOT/pages/ogm/api-reference/ogm.adoc @@ -0,0 +1,122 @@ +[[ogm-api-reference-ogm]] += `OGM` + +== `constructor` + +Returns an `OGM` instance. + +Takes an `input` object as a parameter, which is then passed to the `Neo4jGraphQL` constructor. Supported options are listed in the documentation for xref::api-reference/neo4jgraphql.adoc[`Neo4jGraphQL`]. + +=== Example + +[source, javascript, indent=0] +---- +const ogm = new OGM({ + typeDefs, +}); +---- + +== `init` + +Asynchronous method to initialize the OGM. Internally, calls xref::api-reference/neo4jgraphql.adoc#api-reference-getschema[`Neo4jGraphQL.getSchema()`] to generate a GraphQL schema, and stores the result. Initializes any models which have been created before this execution, and will throw an error if any of them are invalid. + +== `model` + +Returns a `Model` instance matching the passed in name, or (if the OGM has been initialized) throws an `Error` if one can't be found. + +Accepts a single argument `name` of type `string`. + +=== Example + +For the following type definitions: + +[source, graphql, indent=0] +---- +type User { + username: String! +} +---- + +The following would successfully return a `Model` instance: + +[source, javascript, indent=0] +---- +const User = ogm.model("User"); +---- + +The following would throw an `Error`: + +[source, javascript, indent=0] +---- +const User = ogm.model("NotFound"); +---- + + +[[ogm-api-reference-assertconstraints]] +== `assertIndexesAndConstraints` + +Asynchronous method to assert the existence of database constraints, that either resolves to `void` in a successful scenario, or throws an error if the necessary constraints do not exist following its execution. + +Takes an `input` object as a parameter, the supported fields of which are described below. + +=== Example + +Given the following type definitions saved to the variable `typeDefs` and a valid driver instance saved to the variable `driver`: + +[source, graphql, indent=0] +---- +type Book { + isbn: String! @unique +} +---- + +And the construction and initialisation of an `OGM`, using: + +[source, javascript, indent=0] +---- +const ogm = new OGM({ + typeDefs, +}); +await ogm.init(); +---- + +The following will check whether a unique node property constraint exists for label "Book" and property "isbn", and throw an error if it does not: + +[source, javascript, indent=0] +---- +await ogm.assertIndexesAndConstraints(); +---- + +The next example will create the constraint if it does not exist: + +[source, javascript, indent=0] +---- +await ogm.assertIndexesAndConstraints({ options: { create: true } }); +---- + +[[ogm-api-reference-assertconstraints-input]] +=== Input + +Accepts the arguments below: + +[cols="1,2,3"] +|=== +|Name |Type |Description + +|`options` +|xref::ogm/api-reference/ogm.adoc#ogm-api-reference-assertconstraints-input-assertconstraintsoptions[`AssertConstraintsOptions`] +|Options for the execution of `assertIndexesAndConstraints`. +|=== + + +[[ogm-api-reference-assertconstraints-input-assertconstraintsoptions]] +==== `AssertConstraintsOptions` + +[cols="1,2,3"] +|=== +|Name |Type |Description + +|`create` +|`boolean` +|Whether or not to create constraints if they do not yet exist. Disabled by default. +|=== diff --git a/modules/ROOT/pages/ogm/api-reference/type-generation.adoc b/modules/ROOT/pages/ogm/api-reference/type-generation.adoc new file mode 100644 index 00000000..2d6672f7 --- /dev/null +++ b/modules/ROOT/pages/ogm/api-reference/type-generation.adoc @@ -0,0 +1,66 @@ +[[ogm-api-type-generation]] += `generate` + + +Either writes to specified `outFile` or returns a string - if `noWrite` is set. + + +== Example with outFile + +Will write to outFile: + +[source, typescript, indent=0] +---- +import { OGM, generate } from "@neo4j/graphql-ogm"; + +const typeDefs = ` + type Movie { + id: ID + name: String + } +`; + +const driver = neo4j.driver( + "bolt://localhost:7687", + neo4j.auth.basic("admin", "password") +); + +const ogm = new OGM({ typeDefs, driver }); + +await generate({ + ogm, + outFile: "path/to/my/file.ts", +}); + +console.log("Types Generated"); +---- + +== Example with noWrite + +Will return a string: + +[source, typescript, indent=0] +---- +import { OGM, generate } from "@neo4j/graphql-ogm"; + +const typeDefs = ` + type Movie { + id: ID + name: String + } +`; + +const driver = neo4j.driver( + "bolt://localhost:7687", + neo4j.auth.basic("admin", "password") +); + +const ogm = new OGM({ typeDefs, driver }); + +const source = await generate({ + ogm, + noWrite: true, +}); + +console.log("Types Generated ", source); +---- \ No newline at end of file diff --git a/modules/ROOT/pages/ogm/examples.adoc b/modules/ROOT/pages/ogm/examples/custom-resolvers.adoc similarity index 66% rename from modules/ROOT/pages/ogm/examples.adoc rename to modules/ROOT/pages/ogm/examples/custom-resolvers.adoc index e11b1b49..422ce753 100644 --- a/modules/ROOT/pages/ogm/examples.adoc +++ b/modules/ROOT/pages/ogm/examples/custom-resolvers.adoc @@ -1,13 +1,5 @@ -[[ogm-examples]] -= Examples - -This chapter runs through some examples of how you might take advantage of the OGM. - -- xref::ogm/examples.adoc#ogm-examples-custom-resolvers[Custom Resolvers] - using the OGM in custom resolvers within your Neo4j GraphQL API -- xref::ogm/examples.adoc#ogm-examples-rest-api[REST API] - exposing your Neo4j GraphQL API through a REST API, using the OGM - [[ogm-examples-custom-resolvers]] -== Custom Resolvers += Custom Resolvers A common case for using the OGM will be within custom resolvers inside a Neo4j GraphQL instance (very meta!), due to the fact that it has access to some fields which the Neo4j GraphQL Library may not. A common use case might be to have a `password` field marked with directive `@private`, and a custom resolver for creating users with passwords. @@ -184,93 +176,4 @@ You should see the following output: 🚀 Server ready at http://localhost:4000/ ---- -You can execute the `signUp` Mutation against this GraphQL API to sign up, but when you go to query the user through the same API, the password field will not be available. - -[[ogm-examples-rest-api]] -== REST API - -This example demonstrates how you might use the OGM without exposing a Neo4j GraphQL API endpoint. The example starts an https://expressjs.com/[Express] server and uses the OGM to interact with the Neo4j GraphQL Library, exposed over a REST endpoint. - -First, create your example application directory, create a new project and also the file which will contain yur application code: - -[source, bash, indent=0] ----- -mkdir ogm-rest-example -cd ogm-rest-example -npm init --yes -touch index.js ----- - -Then you need to install your dependencies: - -[source, bash, indent=0] ----- -npm install @neo4j/graphql-ogm graphql neo4j-driver express ----- - -Assuming a running Neo4j database at "bolt://localhost:7687" with username "neo4j" and password "password", in your empty `index.js` file, add the following code: - -[source, javascript, indent=0] ----- -const express = require("express"); -const { OGM } = require("@neo4j/graphql-ogm"); -const neo4j = require("neo4j-driver"); - -const driver = neo4j.driver( - "bolt://localhost:7687", - neo4j.auth.basic("neo4j", "password") -); - -const typeDefs = ` - type User { - id: ID - name: String - } -`; - -const ogm = new OGM({ typeDefs, driver }); -const User = ogm.model("User"); - -const app = express(); - -app.get("/users", async (req, res) => { - const { search, offset, limit, sort } = req.query; - - const regex = search ? `(?i).*${search}.*` : null; - - const users = await User.find({ - where: { name_REGEX: regex }, - options: { - offset, - limit, - sort - } - }); - - return res.json(users).end(); -}); - -const port = 4000; - -ogm.init().then(() => { - app.listen(port, () => { - console.log("Example app listening at http://localhost:${port}") - }); -}); ----- - -In your application directory, you can run this application: - -[source, bash, indent=0] ----- -node index.js ----- - -Which should output: - -[source, bash, indent=0] ----- -Example app listening at http://localhost:4000 ----- - -The REST API should now be ready to accept requests at the URL logged. +You can execute the `signUp` mutation against this GraphQL API to sign up, but when you go to query the user through the same API, the password field will not be available. diff --git a/modules/ROOT/pages/ogm/examples/index.adoc b/modules/ROOT/pages/ogm/examples/index.adoc new file mode 100644 index 00000000..bc6db86e --- /dev/null +++ b/modules/ROOT/pages/ogm/examples/index.adoc @@ -0,0 +1,7 @@ +[[ogm-examples]] += Examples + +This chapter runs through some examples of how you might take advantage of the OGM. + +- xref::ogm/examples/custom-resolvers.adoc[Custom Resolvers] - using the OGM in custom resolvers within your Neo4j GraphQL API +- xref::ogm/examples/rest-api.adoc[REST API] - exposing your Neo4j GraphQL API through a REST API, using the OGM diff --git a/modules/ROOT/pages/ogm/examples/rest-api.adoc b/modules/ROOT/pages/ogm/examples/rest-api.adoc new file mode 100644 index 00000000..3250d567 --- /dev/null +++ b/modules/ROOT/pages/ogm/examples/rest-api.adoc @@ -0,0 +1,88 @@ +[[ogm-examples-rest-api]] += REST API + +This example demonstrates how you might use the OGM without exposing a Neo4j GraphQL API endpoint. The example starts an https://expressjs.com/[Express] server and uses the OGM to interact with the Neo4j GraphQL Library, exposed over a REST endpoint. + +First, create your example application directory, create a new project and also the file which will contain yur application code: + +[source, bash, indent=0] +---- +mkdir ogm-rest-example +cd ogm-rest-example +npm init --yes +touch index.js +---- + +Then you need to install your dependencies: + +[source, bash, indent=0] +---- +npm install @neo4j/graphql-ogm graphql neo4j-driver express +---- + +Assuming a running Neo4j database at "bolt://localhost:7687" with username "neo4j" and password "password", in your empty `index.js` file, add the following code: + +[source, javascript, indent=0] +---- +const express = require("express"); +const { OGM } = require("@neo4j/graphql-ogm"); +const neo4j = require("neo4j-driver"); + +const driver = neo4j.driver( + "bolt://localhost:7687", + neo4j.auth.basic("neo4j", "password") +); + +const typeDefs = ` + type User { + id: ID + name: String + } +`; + +const ogm = new OGM({ typeDefs, driver }); +const User = ogm.model("User"); + +const app = express(); + +app.get("/users", async (req, res) => { + const { search, offset, limit, sort } = req.query; + + const regex = search ? `(?i).*${search}.*` : null; + + const users = await User.find({ + where: { name_REGEX: regex }, + options: { + offset, + limit, + sort + } + }); + + return res.json(users).end(); +}); + +const port = 4000; + +ogm.init().then(() => { + app.listen(port, () => { + console.log("Example app listening at http://localhost:${port}") + }); +}); +---- + +In your application directory, you can run this application: + +[source, bash, indent=0] +---- +node index.js +---- + +Which should output: + +[source, bash, indent=0] +---- +Example app listening at http://localhost:4000 +---- + +The REST API should now be ready to accept requests at the URL logged. diff --git a/modules/ROOT/pages/ogm/index.adoc b/modules/ROOT/pages/ogm/index.adoc index 63886eee..09b544df 100644 --- a/modules/ROOT/pages/ogm/index.adoc +++ b/modules/ROOT/pages/ogm/index.adoc @@ -4,53 +4,24 @@ Most applications won't just expose a single GraphQL API. There may also be scheduled jobs, authentication and migrations keeping an application ticking over. The OGM (Object Graph Mapper) can be used to programmatically interact with your Neo4j GraphQL API, which may help with achieving these goals. - xref::ogm/installation.adoc[Installation] -- xref::ogm/examples.adoc[Examples] +- xref::ogm/examples/index.adoc[Examples] - xref::ogm/private.adoc[`@private` Directive] - xref::ogm/selection-set.adoc[Selection Set] - xref::ogm/type-generation.adoc[TypeScript Type Generation] -- xref::ogm/reference.adoc[API Reference] +- xref::ogm/api-reference/index.adoc[API Reference] -> Before diving into the OGM, it's important to have a good understanding of the Neo4j GraphQL Library first. It's recommended to at least work through the xref::getting-started/index.adoc[Getting Started] guide. +> Before diving into the OGM, it's important to have a good understanding of the Neo4j GraphQL Library first. It's recommended to at least work through the xref::getting-started.adoc[Getting Started] guide. == Excluded directives The following directives are excluded from the OGM's schema: -- `@authentication` -- `@authorization` -- `@subscriptionsAuthorization` +- `@auth` +- `@exclude` - `@private` -- `@query` -- `@mutation` -- `@subscription` -- `@filterable` -- `@selectable` -- `@settable` +- `@readonly` +- `@writeonly` This is because the OGM is only ever used programmatically, as opposed to an exposed API which needs these security measures. See also: xref::ogm/private.adoc[`@private` Directive] - -[[ogm-installation]] -== Installation - -The OGM is very easy to install into a new or existing Node.js project. However it does have a couple of dependencies. The OGM depends on the Neo4j GraphQL Library, which will be installed when you install the OGM, so you will require the following dependencies: - -- `@neo4j/graphql-ogm` is the OGM package. -- `graphql` is the package used by the Neo4j GraphQL Library to generate a schema and execute queries and mutations. -- `neo4j-driver` is the official Neo4j Driver package for JavaScript, necessary for interacting with the database. - -[source, bash, indent=0] ----- -npm install @neo4j/graphql-ogm graphql neo4j-driver ----- - -To use the OGM, it will need to be imported wherever you want to use it: - -[source, javascript, indent=0] ----- -const { OGM } = require("@neo4j/graphql-ogm"); ----- - -It's recommended to check out the xref::ogm/examples.adoc[Examples] to see where you might go from here. - diff --git a/modules/ROOT/pages/ogm/installation.adoc b/modules/ROOT/pages/ogm/installation.adoc index e028a7d5..f2d7c262 100644 --- a/modules/ROOT/pages/ogm/installation.adoc +++ b/modules/ROOT/pages/ogm/installation.adoc @@ -19,4 +19,4 @@ To use the OGM, it will need to be imported wherever you want to use it: const { OGM } = require("@neo4j/graphql-ogm"); ---- -It's recommended to check out the xref::ogm/examples.adoc[Examples] to see where you might go from here. +It's recommended to check out the xref::ogm/examples/index.adoc[Examples] to see where you might go from here. diff --git a/modules/ROOT/pages/ogm/reference.adoc b/modules/ROOT/pages/ogm/reference.adoc deleted file mode 100644 index a8aa0979..00000000 --- a/modules/ROOT/pages/ogm/reference.adoc +++ /dev/null @@ -1,491 +0,0 @@ -[[ogm-api-reference]] -= API Reference - -[[ogm-api-reference-ogm]] -== `OGM` - -=== `constructor` - -Returns an `OGM` instance. - -Takes an `input` object as a parameter, which is then passed to the `Neo4jGraphQL` constructor. Supported options are listed in the documentation for xref::reference/api-reference/neo4jgraphql.adoc[`Neo4jGraphQL`]. - -==== Example - -[source, javascript, indent=0] ----- -const ogm = new OGM({ - typeDefs, -}); ----- - -=== `init` - -Asynchronous method to initialize the OGM. Internally, calls xref::reference/api-reference/neo4jgraphql.adoc#api-reference-getschema[`Neo4jGraphQL.getSchema()`] to generate a GraphQL schema, and stores the result. Initializes any models which have been created before this execution, and will throw an error if any of them are invalid. - -=== `model` - -Returns a `Model` instance matching the passed in name, or (if the OGM has been initialized) throws an `Error` if one can't be found. - -Accepts a single argument `name` of type `string`. - -==== Example - -For the following type definitions: - -[source, graphql, indent=0] ----- -type User { - username: String! -} ----- - -The following would successfully return a `Model` instance: - -[source, javascript, indent=0] ----- -const User = ogm.model("User"); ----- - -The following would throw an `Error`: - -[source, javascript, indent=0] ----- -const User = ogm.model("NotFound"); ----- - -[[ogm-api-type-generation]] -=== `generate` - -Either writes to specified `outFile` or returns a string - if `noWrite` is set. - -==== Example with outFile - -Will write to outFile: - -[source, typescript, indent=0] ----- -import { OGM, generate } from "@neo4j/graphql-ogm"; - -const typeDefs = ` - type Movie { - id: ID - name: String - } -`; - -const driver = neo4j.driver( - "bolt://localhost:7687", - neo4j.auth.basic("admin", "password") -); - -const ogm = new OGM({ typeDefs, driver }); - -await generate({ - ogm, - outFile: "path/to/my/file.ts", -}); - -console.log("Types Generated"); ----- - -==== Example with noWrite - -Will return a string: - -[source, typescript, indent=0] ----- -import { OGM, generate } from "@neo4j/graphql-ogm"; - -const typeDefs = ` - type Movie { - id: ID - name: String - } -`; - -const driver = neo4j.driver( - "bolt://localhost:7687", - neo4j.auth.basic("admin", "password") -); - -const ogm = new OGM({ typeDefs, driver }); - -const source = await generate({ - ogm, - noWrite: true, -}); - -console.log("Types Generated ", source); ----- - - -[[ogm-api-reference-assertconstraints]] -=== `assertIndexesAndConstraints` - -Asynchronous method to assert the existence of database constraints, that either resolves to `void` in a successful scenario, or throws an error if the necessary constraints do not exist following its execution. - -Takes an `input` object as a parameter, the supported fields of which are described below. - -==== Example - -Given the following type definitions saved to the variable `typeDefs` and a valid driver instance saved to the variable `driver`: - -[source, graphql, indent=0] ----- -type Book { - isbn: String! @unique -} ----- - -And the construction and initialisation of an `OGM`, using: - -[source, javascript, indent=0] ----- -const ogm = new OGM({ - typeDefs, -}); -await ogm.init(); ----- - -The following will check whether a unique node property constraint exists for label "Book" and property "isbn", and throw an error if it does not: - -[source, javascript, indent=0] ----- -await ogm.assertIndexesAndConstraints(); ----- - -The next example will create the constraint if it does not exist: - -[source, javascript, indent=0] ----- -await ogm.assertIndexesAndConstraints({ options: { create: true } }); ----- - -[[ogm-api-reference-assertconstraints-input]] -==== Input - -Accepts the arguments below: - -[cols="1,2,3"] -|=== -|Name |Type |Description - -|`options` -|xref::ogm/reference.adoc#ogm-api-reference-assertconstraints-input-assertconstraintsoptions[`AssertConstraintsOptions`] -|Options for the execution of `assertIndexesAndConstraints`. -|=== - - -[[ogm-api-reference-assertconstraints-input-assertconstraintsoptions]] -===== `AssertConstraintsOptions` - -[cols="1,2,3"] -|=== -|Name |Type |Description - -|`create` -|`boolean` -|Whether or not to create constraints if they do not yet exist. Disabled by default. -|=== - -[[ogm-model]] -== Model - -[[ogm-api-reference-model-aggregate]] -=== `aggregate` - -This method can be used to aggregate nodes, and maps to the underlying schema xref::queries-aggregations/queries.adoc#queries-aggregate[Aggregate]. - -==== Example - -Find the longest User name: - -[source, javascript, indent=0] ----- -const User = ogm.model("User"); - -const usersAggregate = await User.aggregate({ - aggregate: { - name: { - longest: true - } - } -}); ----- - -Find the longest User name where name starts with the letter "D": - -[source, javascript, indent=0] ----- -const User = ogm.model("User"); - -const usersAggregate = await User.aggregate({ - where: { - name_STARTS_WITH: "D" - }, - aggregate: { - name: { - longest: true - } - } -}); ----- - -==== Arguments - -|=== -|Name and Type |Description - -|`where` + - + - Type: `GraphQLWhereArg` -|A JavaScript object representation of the GraphQL `where` input type used for xref::queries-aggregations/filtering.adoc[Filtering]. -|=== - -[[ogm-api-reference-model-create]] -=== `create` - -This method can be used to update nodes, and maps to the underlying xref::mutations/create.adoc[Create] Mutation. - -Returns a `Promise` that resolves to the equivalent of the Mutation response for this operation. - -==== Example - -To create a Movie with title "The Matrix": - -[source, javascript, indent=0] ----- -const Movie = ogm.model("Movie"); - -await Movie.create({ input: [{ title: "The Matrix" }] }) ----- - -==== Arguments - -|=== -|Name and Type |Description - -|`input` + - + - Type: `any` -|JavaScript object representation of the GraphQL `input` input type used for xref::mutations/create.adoc[Create] mutations. - -|`selectionSet` + - + - Type: `string` or `DocumentNode` or `SelectionSetNode` -|Selection set for the Mutation, see xref::ogm/selection-set.adoc[Selection Set] for more information. - -|`args` + - + - Type: `any` -|The `args` value for the GraphQL Mutation. - -|`context` + - + - Type: `any` -|The `context` value for the GraphQL Mutation. - -|`rootValue` + - + - Type: `any` -|The `rootValue` value for the GraphQL Mutation. -|=== - -[[ogm-api-reference-model-delete]] -=== `delete` - -This method can be used to delete nodes, and maps to the underlying xref::mutations/delete.adoc[Delete] Mutation. - -Returns a `Promise` which resolvers to a `DeleteInfo` object: - -|=== -|Name and Type |Description - -|`nodesDeleted` + - + - Type: `number` -|The number of nodes deleted. - -|`relationshipsDeleted` + - + - Type: `number` -|The number of relationships deleted. -|=== - -==== Example - -To delete all User nodes where the name is "Dan": - -[source, javascript, indent=0] ----- -const User = ogm.model("User"); - -await User.delete({ where: { name: "Dan" }}); ----- - -==== Arguments - -|=== -|Name and Type |Description - -|`where` + - + - Type: `GraphQLWhereArg` -|A JavaScript object representation of the GraphQL `where` input type used for xref::queries-aggregations/filtering.adoc[Filtering]. - -|`delete` + - + - Type: `string` or `DocumentNode` or `SelectionSetNode` -|A JavaScript object representation of the GraphQL `delete` input type used for xref::mutations/delete.adoc[Delete] Mutations. - -|`context` + - + - Type: `any` -|The `context` value for the GraphQL Mutation. - -|`rootValue` + - + - Type: `any` -|The `rootValue` value for the GraphQL Mutation. -|=== - -[[ogm-api-reference-model-find]] -=== `find` - -This method can be used to find nodes, and maps to the underlying schema xref::queries-aggregations/queries.adoc[Queries]. - -Returns a `Promise` which resolvers to an array of objects matching the type of the Model. - -==== Example - -To find all user nodes in the database: - -[source, javascript, indent=0] ----- -const User = ogm.model("User"); - -const users = await User.find(); ----- - -To find users with name "Jane Smith": - -[source, javascript, indent=0] ----- -const User = ogm.model("User"); - -const users = await User.find({ where: { name: "Jane Smith" }}); ----- - -==== Arguments - -|=== -|Name and Type |Description - -|`where` + - + - Type: `GraphQLWhereArg` -|A JavaScript object representation of the GraphQL `where` input type used for xref::queries-aggregations/filtering.adoc[Filtering]. - -|`options` + - + - Type: `GraphQLOptionsArg` -|A JavaScript object representation of the GraphQL `options` input type used for xref::queries-aggregations/sorting.adoc[Sorting] and xref::/queries-aggregations/pagination/index.adoc[Pagination]. - -|`selectionSet` + - + - Type: `string` or `DocumentNode` or `SelectionSetNode` -|Selection set for the Mutation, see xref::ogm/selection-set.adoc[Selection Set] for more information. - -|`args` + - + - Type: `any` -|The `args` value for the GraphQL Mutation. - -|`context` + - + - Type: `any` -|The `context` value for the GraphQL Mutation. - -|`rootValue` + - + - Type: `any` -|The `rootValue` value for the GraphQL Mutation. -|=== - -[[ogm-api-reference-model-update]] -=== `update` - -This method can be used to update nodes, and maps to the underlying xref::mutations/update.adoc[Update] Mutation. - -Returns a `Promise` that resolves to the equivalent of the Mutation response for this operation. - -==== Example - -For the User with name "John", update their name to be "Jane": - -[source, javascript, indent=0] ----- -const User = ogm.model("User"); - -const { users } = await User.update({ - where: { name: "John" }, - update: { name: "Jane" }, -}); ----- - -==== Arguments - -|=== -|Name and Type |Description - -|`where` + - + - Type: `GraphQLWhereArg` -|A JavaScript object representation of the GraphQL `where` input type used for xref::queries-aggregations/filtering.adoc[Filtering]. - -|`update` + - + - Type: `any` -|A JavaScript object representation of the GraphQL `update` input type used for xref::mutations/update.adoc[Update] Mutations. - -|`connect` + - + - Type: `any` -|A JavaScript object representation of the GraphQL `connect` input type used for xref::mutations/update.adoc[Update] Mutations. - -|`disconnect` + - + - Type: `any` -|A JavaScript object representation of the GraphQL `disconnect` input type used for xref::mutations/update.adoc[Update] Mutations. - -|`create` + - + - Type: `any` -|A JavaScript object representation of the GraphQL `create` input type used for xref::mutations/update.adoc[Update] Mutations. - -|`options` + - + - Type: `GraphQLOptionsArg` -|A JavaScript object representation of the GraphQL `options` input type used for xref::queries-aggregations/sorting.adoc[Sorting] and xref::/queries-aggregations/pagination/index.adoc[Pagination]. - -|`selectionSet` + - + - Type: `string` or `DocumentNode` or `SelectionSetNode` -|Selection set for the Mutation, see xref::ogm/selection-set.adoc[Selection Set] for more information. - -|`args` + - + - Type: `any` -|The `args` value for the GraphQL Mutation. - -|`context` + - + - Type: `any` -|The `context` value for the GraphQL Mutation. - -|`rootValue` + - + - Type: `any` -|The `rootValue` value for the GraphQL Mutation. -|=== diff --git a/modules/ROOT/pages/queries-aggregations/pagination/cursor-based.adoc b/modules/ROOT/pages/pagination/cursor-based.adoc similarity index 100% rename from modules/ROOT/pages/queries-aggregations/pagination/cursor-based.adoc rename to modules/ROOT/pages/pagination/cursor-based.adoc diff --git a/modules/ROOT/pages/pagination/index.adoc b/modules/ROOT/pages/pagination/index.adoc new file mode 100644 index 00000000..30c86bf1 --- /dev/null +++ b/modules/ROOT/pages/pagination/index.adoc @@ -0,0 +1,7 @@ +[[pagination]] += Pagination + +The Neo4j GraphQL Library offers two mechanisms for pagination: + +- xref::pagination/offset-based.adoc[Offset-based pagination] - Pagination based on offsets, often associated with navigation via pages. +- xref::pagination/cursor-based.adoc[Cursor-based pagination] - Pagination based on cursors, often associated with infinitely-scrolling applications. diff --git a/modules/ROOT/pages/queries-aggregations/pagination/offset-based.adoc b/modules/ROOT/pages/pagination/offset-based.adoc similarity index 94% rename from modules/ROOT/pages/queries-aggregations/pagination/offset-based.adoc rename to modules/ROOT/pages/pagination/offset-based.adoc index d0430e86..387206ab 100644 --- a/modules/ROOT/pages/queries-aggregations/pagination/offset-based.adoc +++ b/modules/ROOT/pages/pagination/offset-based.adoc @@ -61,7 +61,7 @@ And so on, so forth. You can fetch the total number of records for a certain type using its count query, and then divide that number by your entries per page in order to calculate the total number of pages. This will allow to to determine what the last page is, and whether there is a next page. -See xref::queries-aggregations/queries.adoc[Count] queries for details on how to execute these queries. +See xref::queries.adoc#queries-count[Count] queries for details on how to execute these queries. == Paginating relationship fields diff --git a/modules/ROOT/pages/queries-aggregations/aggregations.adoc b/modules/ROOT/pages/queries-aggregations/aggregations.adoc deleted file mode 100644 index ade63ec9..00000000 --- a/modules/ROOT/pages/queries-aggregations/aggregations.adoc +++ /dev/null @@ -1,153 +0,0 @@ -[[aggregations]] -:description: This page describes aggregations supported by Neo4j GraphQL. -= Aggregations - -Based on the given <>, here is a list of fields that accept aggregations supported by Neo4j GraphQL: - -[cols="1,1,2"] -|=== -| Type | Aggregating function | Example - -| String (e.g. `ID`, `String`) -| `shortest`, `longest` -a| -.Longest User name -[source, graphql, indent=0] ----- -query { - usersAggregate { - name { - longest - } - } -} ----- - -| Numeric (e.g. `Int`, `Float`, `BigInt`) -| `min`, `max`, `average`, `sum` -a| -.Example query -[source, graphql, indent=0] ----- -query { - usersAggregate { - - } -} ----- - -| Temporal (e.g. `DateTime`, `Time`, `LocalTime`, `LocalDateTime`, `Duration`) -| `min`, `max` -a| -.First Post date -[source, graphql, indent=0] ----- -query { - postsAggregate { - createdAt { - min - } - } -} ----- -|=== - -[NOTE] -==== -The argument `where` can also be used in aggregation queries for xref::queries-aggregations/filtering.adoc[filtering] data. -==== - -== Aggregate related nodes - -Related nodes can also be aggregated within a query by accessing the aggregation fields in the node. -In these fields, you can **count**, aggregate the **nodes** or **edges** fields. - -The same selections and types as before are available in relationship aggregations. - -.Counting User nodes -[source, graphql, indent=0] ----- -query { - usersAggregate { - count - } -} ----- - -.Counting User nodes where name starts with "J" -[source, graphql, indent=0] ----- -query { - usersAggregate(where: { name_STARTS_WITH: "J" }) { - count - } -} ----- - -.Counting all posts per User -[source, graphql, indent=0] ----- -query { - users { - id - postsAggregate { - count - } - } -} ----- - -By using the `node` field, related nodes properties can be aggregated: - -.Finding longest post per User -[source, graphql, indent=0] ----- -query { - users { - name - postsAggregate { - node { - content { - longest - } - } - } - } -} ----- - -== Aggregate relationships - -Relationship properties can be aggregated as well by using the `edge` field: - -.Querying what User nodes posted up to a date -[source, graphql, indent=0] ----- -query { - users { - name - postsAggregate { - edge { - date { - max - } - } - } - } -} ----- - -When performing an aggregation on related nodes, the query against the relationship -can be defined as "undirected" by using the argument `directed: false`: - -[source, graphql, indent=0] ----- -query { - users { - id - postsAggregate(directed: false) { - count - } - } -} ----- diff --git a/modules/ROOT/pages/queries-aggregations/filtering.adoc b/modules/ROOT/pages/queries-aggregations/filtering.adoc deleted file mode 100644 index 7efaa971..00000000 --- a/modules/ROOT/pages/queries-aggregations/filtering.adoc +++ /dev/null @@ -1,497 +0,0 @@ -[[filtering]] -:description: This page describes filtering operators. -= Filtering - -When querying for data, a number of operators are available for different types in the `where` argument of a query or mutation. - -== Equality operators - -All types can be tested for either equality (`_eq`) or non-equality (`_ne`). -For example: - -.Filtering all Users named John -[source, graphql, indent=0] ----- -query { - users(where: {name: { _eq: "John" }}) - id - name - } ----- - -[NOTE] -==== -For the `Boolean` type, equality operators are the only ones available. -==== - -== Numerical operators - -These are the operators available for numeric (`Int`, `Float`, xref::/type-definitions/types/scalar.adoc[`BigInt`]), xref::/type-definitions/types/temporal.adoc[temporal] and xref::/type-definitions/types/spatial.adoc[spatial] types: - -* `_LT` -* `_LTE` -* `_GT` -* `_GTE` - -Here is an example of how to use them: - -.Filtering Users younger than 50 years old -[source, graphql, indent=0] ----- -query { - users(where: {age: { _lt: 50 }}) { - id - name - age - } -} ----- - -Spatial types use numerical filtering differently and they also have additional options. -See xref::/type-definitions/types/spatial.adoc#_filtering[Spatial types filtering] for more information. - -These same operators are disabled by default in the case of String comparisons. -To enable, explicitly add them in the features options: - -[source, javascript, indent=0] ----- -const { Neo4jGraphQL } = require("@neo4j/graphql"); -const neo4j = require("neo4j-driver"); - -const typeDefs = ` - type User { - name: String - } -`; - -const driver = neo4j.driver( - "bolt://localhost:7687", - neo4j.auth.basic("neo4j", "password") -); - -const features = { - filters: { - String: { - LT: true, - GT: true, - LTE: true, - GTE: true - } - } -}; - -const neoSchema = new Neo4jGraphQL({ features, typeDefs, driver }); ----- - - -== String comparison - -The following case-sensitive comparison operators are only available for use on `String` and `ID` types: - -* `_STARTS_WITH` -* `_ENDS_WITH` -* `_CONTAINS` - -Here is an example of how to use them: - -.Filtering Users with name starting with "J" -[source, graphql, indent=0] ----- -query { - users(where: { name_STARTS_WITH: "J" }) { - id - name - } -} ----- - -== RegEx matching - -The filter `_MATCHES` is also available for comparison of `String` and `ID` types. -It accepts RegEx strings as an argument and returns any matches. - - -Note that RegEx matching filters are **disabled by default**. -This is because, on an unprotected API, they could potentially be used to execute a https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS[ReDoS attack^] against the backing Neo4j database. - -If you want to enable them, set the features configuration object for each: - -[source, javascript, indent=0] ----- -const features = { - filters: { - String: { - MATCHES: true, - } - } -}; - -const neoSchema = new Neo4jGraphQL({ features, typeDefs, driver }); ----- - -For `ID`: - - -[source, javascript, indent=0] ----- -const features = { - filters: { - String: { - ID: true, - } - } -}; - -const neoSchema = new Neo4jGraphQL({ features, typeDefs, driver }); ----- - -For both `String` and `ID`: - - -[source, javascript, indent=0] ----- -const features = { - filters: { - String: { - MATCHES: true, - }, - ID: { - MATCHES: true, - } - } -}; - -const neoSchema = new Neo4jGraphQL({ features, typeDefs, driver }); ----- - -== Array comparison - -The following operator is available on non-array fields, and accepts an array argument: - -* `_IN` - -Conversely, the following operator is available on array fields, and accepts a single argument: - -* `_INCLUDES` - -These operators are available for all types apart from `Boolean`. - -== Combining operators - -All operators can be combined using the `AND`, `OR`, and `NOT` operators. -They can also be stand-alone operators, which means that they can be used as such and not be appended to field names. - -These operators accept an array argument with items of the same format as the `where` argument, which means they can also be nested to form complex combinations. - -For example, if you want to match all actors by the name of either "Keanu" or not belonging to the "Pantoliano" family, that played in "The Matrix" movie, here is how you can query that: - -[source, graphql, indent=0] ----- -query { - actors(where: { - AND: [ - { - OR: [ - { name_CONTAINS: "Keanu" }, - { NOT: { name_ENDS_WITH: "Pantoliano" } } - ] - }, - { - movies_SOME: { title: "The Matrix" } - } - ]} - ) { - name - movies { - title - } - } -} ----- - -== Relationship filtering - -Relationship filtering depends on the type of relationship that you have: - -* `n..1`: filtering done on equality or inequality of the related nodes by specifying a filter on `field`. -* `n..m`: filtering is done on the list of related nodes and is based on the https://neo4j.com/docs/cypher-manual/current/functions/predicate/[list predicates] available in Cypher: -** `field_ALL` - https://neo4j.com/docs/cypher-manual/current/functions/predicate/#functions-all[all] -** `field_NONE` - https://neo4j.com/docs/cypher-manual/current/functions/predicate/#functions-none[none] -** `field_SOME` - https://neo4j.com/docs/cypher-manual/current/functions/predicate/#functions-any[any] -** `field_SINGLE` - https://neo4j.com/docs/cypher-manual/current/functions/predicate/#functions-single[single] - -As an example, take these type definitions: - -[source, graphql, indent=0] ----- -type User { - id: ID! - name: String - posts: [Post!]! @relationship(type: "HAS_POST", direction: OUT) -} - -type Post { - id: ID! - content: String - author: User! @relationship(type: "HAS_POST", direction: IN) - likes: [User!]! @relationship(type: "LIKES", direction: IN) -} ----- -=== In the case of `n..1` relationships - -An `author` represents an `n..1` relationship on `Post`, where a given `Post` is authored by one, and only one, `author`. -The available filters here will be `author`. -For example: - -.Find all posts by a desired author -[source, graphql, indent=0] ----- -query { - posts(where: { author: { id: "7CF1D9D6-E527-4ACD-9C2A-207AE0F5CB8C" } }) { - content - } -} ----- - -.Find all posts `NOT` by an undesired author -[source, graphql, indent=0] ----- -query { - posts(where: { NOT: { author: { id: "7CF1D9D6-E527-4ACD-9C2A-207AE0F5CB8C" } } }) { - content - } -} ----- - -=== In the case of `n..m` relationships - -In the previous example, `posts` represents a `n..m` relationship on `User`, where a given `User` can have any number of `posts`. -Here are some query examples: - -.Find all Users where all of their posts contain search term: `"neo4j"` -[source, graphql, indent=0] ----- -query { - users(where: { posts_ALL: { content_CONTAINS: "neo4j" } }) { - name - } -} ----- - -.Find all Users where none of their posts contains search term: `"cypher"` -[source, graphql, indent=0] ----- -query { - users(where: { posts_NONE: { content_CONTAINS: "cypher" } }) { - name - } -} ----- - -.Find all users where some of their posts contain search term: `"graphql"` -[source, graphql, indent=0] ----- -query { - users(where: { posts_SOME: { content_CONTAINS: "graphql" } }) { - name - } -} ----- - -.Find all users where only one of their posts contain search term: `"graph"` -[source, graphql, indent=0] ----- -query { - users(where: { posts_SINGLE: { content_CONTAINS: "graph" } }) { - name - } -} ----- - -== Aggregation filtering - -This library offers, for each relationship, an aggregation key inside the `where` argument. -It can be used both on the `node` and `edge` of a relationship. - -Here are some examples on how to apply this kind of filtering: - -. *Find posts where the number of likes are greater than 5* -+ -.Schema example -[source, graphql, indent=0] ----- -type User { - name: String -} - -type Post { - content: String - likes: [User!]! @relationship(type: "LIKES", direction: IN) -} ----- -+ -.Query -[source, graphql, indent=0] ----- -query { - posts(where: { likesAggregate: { count_GT: 5 } }) { - content - } -} ----- - -. *Find flights where the average age of passengers is greater than or equal to 18* -+ -.Schema example -[source, graphql, indent=0] ----- -type Passenger { - name: String - age: Int -} - -type Flight { - code: String - passengers: [Passenger!]! @relationship(type: "FLYING_ON", direction: IN) -} ----- -+ -.Query -[source, graphql, indent=0] ----- -query { - flights(where: { passengersAggregate: { node: { age_AVERAGE_GTE: 18 } } }) { - code - } -} ----- - -. *Find movies where the shortest actor screen time is less than 10 minutes* -+ -.Schema example -[source, graphql, indent=0] ----- -type Movie { - title: String - actors: [Person!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") -} - -type Person { - name: String -} - -interface ActedIn @relationshipProperties { - screenTime: Int -} ----- -+ -.Query -[source, graphql, indent=0] ----- -query { - movies(where: { actorsAggregate: { edge: { screenTime_MIN_LT: 10 } } }) { - title - } -} ----- - -=== Operators - -Aggregation filtering can also be done with operators. -They provide autogenerated filters available for each type on the `node` and `edge` of the specified relationship. - -[cols="1,2,2,2"] -|=== -| Field type | Description | Operators | Example - -| `count` -| A special 'top level' key inside the `where` aggregation and will be available for all relationships. This is used to count the amount of relationships the parent node is connected to. -| `count_EQUAL`, `count_GT`, `count_GTE`, `count_LT`, `count_LTE` -a| -[source, graphql, indent=0] ----- -query { - posts(where: { likesAggregate: { count_GT: 5 } }) { - content - } -} ----- - -| `String` -| These operators are calculated against the length of each string. -| `_AVERAGE_LENGTH_EQUAL` `_AVERAGE_LENGTH_GT` `_AVERAGE_LENGTH_GTE` `_AVERAGE_LENGTH_LT` `_AVERAGE_LENGTH_LTE` `_SHORTEST_LENGTH_EQUAL` `_SHORTEST_LENGTH_GT` `_SHORTEST_LENGTH_GTE` `_SHORTEST_LENGTH_LT` `_SHORTEST_LENGTH_LTE` `_LONGEST_LENGTH_EQUAL` `_LONGEST_LENGTH_GT` `_LONGEST_LENGTH_GTE` `_LONGEST_LENGTH_LT` `_LONGEST_LENGTH_LTE` -a| -[source, graphql, indent=0] ----- -query { - posts(where: { likesAggregate: { node: { name_LONGEST_LENGTH_GT: 5 } } }) { - content - } -} ----- - -| `Numerical` -| Used in the case of `Int`, `Float`, and `BigInt`. -| `_AVERAGE_EQUAL`, `_AVERAGE_GT`, `_AVERAGE_GTE`, `_AVERAGE_LT`, `_AVERAGE_LTE`, `_SUM_EQUAL`, `_SUM_GT`, `_SUM_GTE`, `_SUM_LT`, `_SUM_LTE`, `_MIN_EQUAL`, `_MIN_GT`, `_MIN_GTE`, `_MIN_LT`, `_MIN_LTE`, `_MAX_EQUAL`, `_MAX_GT`, `_MAX_GTE`, `_MAX_LT`, `_MAX_LTE` -a| -[source, graphql, indent=0] ----- -query { - movies(where: { actorsAggregate: { edge: { screenTime_MIN_LT: 10 } } }) { - title - } -} ----- - -| `Temporal` -| Used in the case of `DateTime`, `LocalDateTime`, `LocalTime`, `Time`, and `Duration`. -| `_MIN_EQUAL`, `_MIN_GT`, `_MIN_GTE`, `_MIN_LT`, `_MIN_LTE`, `_MAX_EQUAL`, `_MAX_GT`, `_MAX_GTE`, `_MAX_LT`, `_MAX_LTE` -a| -.Type definitions -[source, graphql, indent=0] ----- -type Event { - title: String! - startTime: DateTime! -} ----- - -.Query -[source, graphql, indent=0] ----- -query EventsAggregate { - users(where: { eventsAggregate: { node: { startTime_GT: "2022-08-14T15:00:00Z" } } }) { - name - } -} ----- - -| `Duration` -| Description. -| `_AVERAGE_EQUAL`, `_AVERAGE_GT`, `_AVERAGE_GTE`, `_AVERAGE_LT`, `_AVERAGE_LTE` -a| -.Type definitions -[source, graphql, indent=0] ----- -type Event { - title: String! - duration: Duration! -} ----- - -.Query -[source, graphql, indent=0] ----- -query EventsAggregate { - users(where: { eventsAggregate: { node: { duration_AVERAGE_LT: "PT2H" } } }) { - name - } -} ----- - -| `ID` -| No aggregation filters are available for ID. -| - -| - - -|=== \ No newline at end of file diff --git a/modules/ROOT/pages/queries-aggregations/index.adoc b/modules/ROOT/pages/queries-aggregations/index.adoc deleted file mode 100644 index 337b1a6d..00000000 --- a/modules/ROOT/pages/queries-aggregations/index.adoc +++ /dev/null @@ -1,50 +0,0 @@ -[queries-aggregations] -:description: This section describes queries and aggregations. -= Queries and aggregations - -Each node defined in type definitions has two query fields generated for it: one for *querying* data and another one for *aggregating* it. -Each of these fields, by their part, accepts two arguments used for *filtering*, *sorting*, and *pagination*. - -This section addresses the following topics: - -* xref:queries-aggregations/queries.adoc[Queries] - How to read or fetch values. -* xref:queries-aggregations/aggregations.adoc[Aggregations] - How to combine lists of types from different sources into a single list. -* xref:queries-aggregations/filtering.adoc[Filtering] - How to filter query results to find objects. -* xref:queries-aggregations/sorting.adoc[Sorting] - How to sort query results by each individual field. - -[#examples-reference] -*All examples featured in this section use the following type definitions:* - -[source, graphql, indent=0] ----- -type Post { - id: ID! @id - content: String! - creator: User! @relationship(type: "HAS_POST", direction: IN, properties: "PostedAt") - createdAt: DateTime! -} - -type User { - id: ID! @id - name: String! - posts: [Post!]! @relationship(type: "HAS_POST", direction: OUT, properties: "PostedAt") - friends: [User!]! @relationship(type: "FRIENDS_WITH", direction: OUT) -} - -interface PostedAt @relationshipProperties { - date: DateTime -} ----- - -For which the following query fields are generated: - -[source, graphql, indent=0] ----- -type Query { - posts(where: PostWhere, options: PostOptions): [Post!]! - postsAggregate(where: PostWhere): PostAggregationSelection! - - users(where: UserWhere, options: UserOptions): [User!]! - usersAggregate(where: UserWhere): UserAggregationSelection! -} ----- \ No newline at end of file diff --git a/modules/ROOT/pages/queries-aggregations/pagination/index.adoc b/modules/ROOT/pages/queries-aggregations/pagination/index.adoc deleted file mode 100644 index 9377e39f..00000000 --- a/modules/ROOT/pages/queries-aggregations/pagination/index.adoc +++ /dev/null @@ -1,7 +0,0 @@ -[[pagination]] -= Pagination - -The Neo4j GraphQL Library offers two mechanisms for pagination: - -- xref::/queries-aggregations/pagination/offset-based.adoc[Offset-based pagination] - Pagination based on offsets, often associated with navigation via pages. -- xref::/queries-aggregations/pagination/cursor-based.adoc[Cursor-based pagination] - Pagination based on cursors, often associated with infinitely-scrolling applications. diff --git a/modules/ROOT/pages/queries-aggregations/queries.adoc b/modules/ROOT/pages/queries-aggregations/queries.adoc deleted file mode 100644 index 0f0c2845..00000000 --- a/modules/ROOT/pages/queries-aggregations/queries.adoc +++ /dev/null @@ -1,71 +0,0 @@ -[[queries]] -:description: This page describes how to read or fetch values in the Neo4j GraphQL Library. -= Queries - -Based on the given <>, here are two examples of how to write queries for reading or fetching values: - -.Return all User nodes from their ID and name -[source, graphql, indent=0] ----- -query { - users { - id - name - } -} ----- - -.Query User with name "Jane Smith" and their posts -[source, graphql, indent=0] ----- -query { - users(where: { name: "Jane Smith" }) { - id - name - posts { - content - } - } -} ----- - -== Undirected queries - -All xref::/type-definitions/types/relationships.adoc[relationships] are created with a direction from one node to another. -By default, all queries follow the direction defined in the relationship. -However, in some cases it is necessary to query for all related nodes, regardless of the direction of the relationship. -This can be achieved with the argument `directed: false`. - -For example, the following query should return all User friends, regardless of the direction of the relationship `"FRIENDS_WITH"`: - -[source, graphql, indent=0] ----- -query { - users { - name - friends: friends(directed: false) { - name - } - } -} ----- - -In addition, undirected relationships can also be used in the same fashion with connections: - -[source, graphql, indent=0] ----- -query Query { - users { - friendsConnection(directed: false) { - edges { - node { - name - } - } - } - } -} ----- - -Keep in mind that *undirected relationships are only supported in queries*. -The xref::/type-definitions/types/relationships.adoc#_querydirection[type definitions] for a relationship may define a different behavior, so the `directed` option may not be available in some cases. \ No newline at end of file diff --git a/modules/ROOT/pages/queries.adoc b/modules/ROOT/pages/queries.adoc new file mode 100644 index 00000000..135a7016 --- /dev/null +++ b/modules/ROOT/pages/queries.adoc @@ -0,0 +1,288 @@ +[[queries]] += Queries + +Each node defined in type definitions will have two query fields generated for it: + +1. One for querying data +2. One for aggregating data + +The examples in this chapter will use the following type definitions: + +[source, graphql, indent=0] +---- +type Post { + id: ID! @id + content: String! + creator: User! @relationship(type: "HAS_POST", direction: IN, properties: "PostedAt") + createdAt: DateTime! +} + +type User { + id: ID! @id + name: String! + posts: [Post!]! @relationship(type: "HAS_POST", direction: OUT, properties: "PostedAt") + friends: [User!]! @relationship(type: "FRIENDS_WITH", direction: OUT) +} + +interface PostedAt { + date: DateTime +} +---- + +For which the following query fields will be generated: + +[source, graphql, indent=0] +---- +type Query { + posts(where: PostWhere, options: PostOptions): [Post!]! + postsAggregate(where: PostWhere): PostAggregationSelection! + + users(where: UserWhere, options: UserOptions): [User!]! + usersAggregate(where: UserWhere): UserAggregationSelection! +} +---- + +== Query + +Each field for querying data accepts two arguments: + +- `where` - used for xref::filtering.adoc[Filtering] data +- `options` - used to specify xref::sorting.adoc[Sorting] and xref::pagination/index.adoc[Pagination] options + +=== Querying for all User nodes + +The following query will return all User nodes, returning their ID and name. + +[source, graphql, indent=0] +---- +query { + users { + id + name + } +} +---- + +=== Query for user with name "Jane Smith" and their posts + +The following query will return all Users, returning the content which they have posted. + +[source, graphql, indent=0] +---- +query { + users(where: { name: "Jane Smith" }) { + id + name + posts { + content + } + } +} +---- + +=== Undirected queries + +All xref:type-definitions/relationships.adoc[relationships] are created with a _direction_ from one **node** to another. +By default, all queries follow the direction defined in the relationship, however, in some cases we may need to query for +all related nodes, regardless of the direction of the relationship. This can be achieved with the argument `directed: false`. + +For example, the following query: + +[source, graphql, indent=0] +---- +query { + users { + name + friends: friends(directed: false) { + name + } + } +} +---- + +Will return all user friends, regardless on the direction of the relationship `"FRIENDS_WITH"`. + +Undirected relationships can also be used in the same fashion with connections: + +[source, graphql, indent=0] +---- +query Query { + users { + friendsConnection(directed: false) { + edges { + node { + name + } + } + } + } +} +---- + +Note that _undirected relationships_ are only supported in queries. + +The xref:type-definitions/relationships.adoc#_querydirection[type definitions] for a relationship may define +a different behaviour, so the `directed` option may not be available in some cases. + +[[queries-aggregate]] +== Aggregate + +Neo4j GraphQL supports aggregations on fields with type: + +1. `ID`- String +2. `String` - String +3. `Int` - Numerical +4. `Float` - Numerical +5. `BigInt` - Numerical +6. `DateTime` +7. `Time` +8. `LocalTime` +9. `LocalDateTime` +10. `Duration` + +Numerical Fields will expose the following aggregation selections: + +1. `min` +2. `max` +3. `average` +4. `sum` + +String fields will expose: + +1. `shortest` +2. `longest` + +The rest will only expose: + +1. `min` +2. `max` + + +Aggregation queries accepts a `where` argument for xref::filtering.adoc[filtering] data. + +=== Counting Using aggregation + +The following query will count all User nodes: + +[source, graphql, indent=0] +---- +query { + usersAggregate { + count + } +} +---- + +=== Counting User nodes where name starts with "J" + +[source, graphql, indent=0] +---- +query { + usersAggregate(where: { name_STARTS_WITH: "J" }) { + count + } +} +---- + +=== Querying for the longest User name + +[source, graphql, indent=0] +---- +query { + usersAggregate { + name { + longest + } + } +} +---- + +=== Querying for first Post date + +[source, graphql, indent=0] +---- +query { + postsAggregate { + createdAt { + min + } + } +} +---- + +[[queries-aggregate-related-nodes]] +== Aggregate related nodes + +Related nodes can also be aggregated within a query by accessing the aggregation fields in the node. +In these fields, you can **count**, aggregate the **nodes** or **edges** fields. + +The same selections and types as before are available in relationship aggregations. + +=== Counting all posts per users + +[source, graphql, indent=0] +---- +query { + users { + id + postsAggregate { + count + } + } +} +---- + +=== Finding longest post per user +By using the `node` field, related nodes properties can be aggregated. + +[source, graphql, indent=0] +---- +query { + users { + name + postsAggregate { + node { + content { + longest + } + } + } + } +} +---- + +=== Aggregate relationships +Relationship properties can be aggregated as well by using the `edge` field. + +[source, graphql, indent=0] +---- +query { + users { + name + postsAggregate { + edge { + date { + max + } + } + } + } +} +---- + +=== Undirected aggregation queries + +When performing an aggregation on related nodes, the query against the relationship +can be defined as an xref::queries.adoc#_undirected_queries[undirected] using the argument `directed: false`: + +[source, graphql, indent=0] +---- +query { + users { + id + postsAggregate(directed: false) { + count + } + } +} +---- diff --git a/modules/ROOT/pages/reference/api-reference/index.adoc b/modules/ROOT/pages/reference/api-reference/index.adoc deleted file mode 100644 index d752a737..00000000 --- a/modules/ROOT/pages/reference/api-reference/index.adoc +++ /dev/null @@ -1,6 +0,0 @@ -[[api-reference]] -= API Reference - -- xref::reference/api-reference/neo4jgraphql.adoc[`Neo4jGraphQL`] -- xref::reference/api-reference/ogm.adoc[`@neo4j/graphql-ogm`] - diff --git a/modules/ROOT/pages/reference/directives/index.adoc b/modules/ROOT/pages/reference/directives/index.adoc deleted file mode 100644 index d35bba0c..00000000 --- a/modules/ROOT/pages/reference/directives/index.adoc +++ /dev/null @@ -1,160 +0,0 @@ -[[directives]] -= Directives - -== `@alias` - -The `@alias` directive will map a GraphQL schema field to a Neo4j property on a node or relationship. - -Reference: xref::/type-definitions/directives/database-mapping.adoc#type-definitions-alias[`@alias`] - -== `@coalesce` - -The `@coalesce` directive exposes a mechanism for querying against non-existent, `null` values on a node. - -Reference: xref::/type-definitions/directives/default-values.adoc#type-definitions-default-values-coalesce[`@coalesce`] - -[[custom-resolver-directive]] -== `@customResolver` - -The `@customResolver` directive specifies that a field will be resolved by a custom resolver, and allows the specification -of any required fields that will be passed as arguments to the custom resolver. - -Reference: xref::custom-resolvers.adoc#custom-resolver-directive[`@customResolver`] - -== `@cypher` - -The `@cypher` directive overrides field resolution (including `Query` and `Mutation` fields), instead resolving with the specified Cypher. - -Reference: xref::/type-definitions/directives/cypher.adoc[`@cypher` directive] - -== `@default` - -The `@default` directive allows for the setting of a default value for a field on object creation. - -Reference: xref::/type-definitions/directives/default-values.adoc#type-definitions-default-values-default[`@default`] - -== `@exclude` label:deprecated[] - -This directive is deprecated. - -Use the xref:schema-configuration/type-configuration.adoc#_query[`@query`], xref:schema-configuration/type-configuration.adoc#_mutation[`@mutation`] and the xref:schema-configuration/type-configuration.adoc#_subscription[`@subscription`] directives instead. - -The `@exclude` directive is used on object types to instruct them to be skipped during Query, Mutation and Subscription generation. - -Reference: xref::schema-configuration/type-configuration.adoc#_exclude_deprecated[`@exclude`] - -== `@filterable` - -The `@filterable` directive defines the filters generated for a field. - -Reference: xref:schema-configuration/field-configuration.adoc#_filterable[`@filterable`] - -== `@fulltext` - -The `@fulltext` directive indicates that there should be a Fulltext index inserted into the database for the specified Node and its properties. - -Reference: xref::/type-definitions/directives/indexes-and-constraints.adoc#type-definitions-indexes-fulltext[Fulltext indexes] - -== `@id` - -The `@id` directive marks a field as the unique ID for an object type, and allows for autogeneration of IDs. - -Reference: xref::/type-definitions/directives/autogeneration.adoc#type-definitions-autogeneration-id[`@id`] - -== `@limit` - -The `@limit` is to be used on nodes, and when applied will inject values into Cypher `LIMIT` clauses. - -Reference: xref::/type-definitions/directives/default-values.adoc#type-definitions-default-values-limit[`@limit`] - -== `@mutation` - -This directive is used to limit the availability of Mutation operations in the library. - -Reference: xref:schema-configuration/type-configuration.adoc#_mutation[`@mutation`] - -== `@node` - -The `@node` directive is used to specify the configuration of a GraphQL object type which represents a Neo4j node. - -Reference: xref::/type-definitions/directives/database-mapping.adoc#type-definitions-node[`@node`] - -[[plural-directive]] -== `@plural` - -The `@plural` directive redefines how to compose the plural of the type for the generated operations. -This is particularly useful for types that are not correctly pluralized or are non-English words. - -Reference: xref::/type-definitions/directives/database-mapping.adoc#type-definitions-plural[`@plural`] - -[[populated-by-directive]] -== `@populatedBy` - -The `@populatedBy` directive is used to specify a callback function that gets executed during GraphQL query parsing, -to populate fields which have not been provided within the input. - -Reference: xref::/type-definitions/directives/autogeneration.adoc#type-definitions-autogeneration-populated-by[`@populatedBy`] - -== `@private` - -The `@private` directive protects fields which should only be available through the xref::ogm/index.adoc[OGM]. - -Reference: xref::ogm/private.adoc[`@private` Directive] - -== `@query` - -This directive is used to limit the availability of Query operations in the library. - -Reference: xref:schema-configuration/type-configuration.adoc#_query[`@query`] - -== `@relationship` - -The `@relationship` directive is used to configure relationships between object types. - -Reference: xref::/type-definitions/types/relationships.adoc[Relationships], xref::schema-configuration/field-configuration.adoc#_relationship[`@relationship`] - -== `@relationshipProperties` - -Required to help you distinguish between interfaces which are used for relationship properties, and otherwise. - -Can only be used on interfaces, as per its definition: - -[source, graphql, indent=0] ----- -"""Required to differentiate between interfaces for relationship properties, and otherwise.""" -directive @relationshipProperties on INTERFACE ----- - -== `@relayId` - -The `@relayId` directive can be used on object type fields, to flag which field should be used as the global node identifier for Relay. Can be used once per type. The use of the `@relayId` directive ensures a unique node property constraint for the field. - -== `@selectable` - -The `@selectable` directive sets the availability of fields on queries and aggregations. - -Reference: xref:schema-configuration/field-configuration.adoc#_selectable[`@selectable`] - -== `@settable` - -The `@settable` directive sets the availability of fields on the create and update inputs. - -Reference: xref:schema-configuration/field-configuration.adoc#_settable[`@settable`] - -== `@subscription` - -This directive is used to limit Subscription events available in the library. - -Reference: xref:schema-configuration/type-configuration.adoc#_subscription[`@subscription`] - -== `@timestamp` - -The `@timestamp` directive flags fields to be used to store timestamps on create/update events. - -Reference: xref::/type-definitions/directives/autogeneration.adoc#type-definitions-autogeneration-timestamp[`@timestamp`] - -== `@unique` - -The `@unique` directive indicates that there should be a uniqueness constraint in the database for the fields that it is applied to. - -Reference: xref::/type-definitions/directives/indexes-and-constraints.adoc#type-definitions-constraints-unique[Unique node property constraints] diff --git a/modules/ROOT/pages/reference/type-definitions/interfaces.adoc b/modules/ROOT/pages/reference/type-definitions/interfaces.adoc deleted file mode 100644 index 6b6d01e6..00000000 --- a/modules/ROOT/pages/reference/type-definitions/interfaces.adoc +++ /dev/null @@ -1,134 +0,0 @@ -[[schema-configuration-type-configuration]] -= Type Configuration - -When representing a Neo4j Node, a GraphQL Object type produces multiple operation fields in the `Query`, `Mutation`, and `Subscription` types. -For example: - -[source, graphql, indent=0] ----- -type Movie { - title: String - length: Int -} ----- - -From these type definitions, the library generates the following operation fields: - -**Query**: - - * `movies` - * `moviesAggregate` - * `moviesConnection` - -**Mutation**: - - * `createMovies` - * `deleteMovies` - * `updateMovies` - -**Subscription**: - - * `movieCreated` - * `movieUpdated` - * `movieDeleted` -. - -This section explains how to reduce the operation fields produced using the directives `@query`, `@mutation`, and `@subscription`. - -== `@query` - -This directive is used to limit the availability of query operations in the library. - -=== Definition - -[source, graphql, indent=0] ----- -directive @query(read: Boolean! = true, aggregate: Boolean! = false) on OBJECT | SCHEMA ----- - -[NOTE] -==== -Aggregations will no longer be generated by default in the 4.0.0 version of the library. -See xref:migration/v4-migration/index.adoc#opt-in-aggregation[`Opt-in Aggregation`] for more information. -==== - -[[type-definitions-interfaced-types-querying]] -== Querying an interface - -Which implementations are returned by a query are dictated by the `where` filter applied. - -For example, the following will return all productions with title starting "The " for every actor: - -[source, graphql, indent=0] ----- -enum MutationFields { - CREATE - UPDATE - DELETE -} - -directive @mutation(operations: [MutationFields!]! = [CREATE, UPDATE, DELETE]) on OBJECT | SCHEMA ----- - -=== Usage - -==== Disable Create, Delete, and Update operations for _Movie_ - -[source, graphql, indent=0] ----- -type Movie @mutation(operations: []) { - title: String - length: Int -} ----- - -==== Enable only Create operations for _Movie_ - -[source, graphql, indent=0] ----- -type Movie @mutation(operations: [CREATE]) { - title: String - length: Int -} ----- - -== `@subscription` - -This directive is used to limit Subscription operations in the library. - -=== Definition - -[source, graphql, indent=0] ----- -enum SubscriptionFields { - CREATE - UPDATE - DELETE - CREATE_RELATIONSHIP - DELETE_RELATIONSHIP -} - -directive @subscription(operations: [SubscriptionFields!]! = [CREATE, UPDATE, DELETE, CREATE_RELATIONSHIP, DELETE_RELATIONSHIP]) on OBJECT | SCHEMA ----- - -=== Usage - -==== Disable subscriptions for _Movie_ - -[source, graphql, indent=0] ----- -type Movie @subscription(operations: []) { - title: String - length: Int -} ----- - -==== Enable only _movieCreated_ subscription for _Movie_ - -[source, graphql, indent=0] ----- -type Movie @subscription(operations: [CREATE]) { - title: String - length: Int -} ----- \ No newline at end of file diff --git a/modules/ROOT/pages/schema-configuration/index.adoc b/modules/ROOT/pages/schema-configuration/index.adoc deleted file mode 100644 index b271b1a9..00000000 --- a/modules/ROOT/pages/schema-configuration/index.adoc +++ /dev/null @@ -1,14 +0,0 @@ -[[type-definitions-schema-configuration]] -:description: This section describes configurations that can be set to a schema in Neo4j GraphQL. -= Schema configuration - -Neo4j GraphQL Library supports several functionalities such as CRUD operations, aggregation, filtering, and others. -To make them work, a large amount of GraphQL types are generated. -However, in some cases, it may be advisable to reduce the scope of the API produced. - -This section provides information on how to limit access to unwanted operations and reduce the size of the schema which can improve the performance: - -- xref::schema-configuration/index.adoc[Schema configuration] - How to restrict access to certain types or fields. -- xref::schema-configuration/field-configuration.adoc[Field configuration] - How to remove fields from a GraphQL Object Type or a GraphQL Input Object Type. -- xref::schema-configuration/global-configuration.adoc[Global configuration] - How to globally disable specific types of operation. -- xref::schema-configuration/type-configuration.adoc[Type configuration] - How to set up `Query`, `Mutation`, and `Subscription` types. \ No newline at end of file diff --git a/modules/ROOT/pages/schema-configuration/type-configuration.adoc b/modules/ROOT/pages/schema-configuration/type-configuration.adoc deleted file mode 100644 index 46eb8077..00000000 --- a/modules/ROOT/pages/schema-configuration/type-configuration.adoc +++ /dev/null @@ -1,150 +0,0 @@ -[[schema-configuration-type-configuration]] -:description: This page describes how to reduce the operation fields produced using the directives @query, @mutation, and @subscription. -= Type configuration - -When representing a Neo4j node, a GraphQL Object Type produces multiple operation fields in the `Query`, `Mutation`, and `Subscription` types. -For example: - -[source, graphql, indent=0] ----- -type Movie { - title: String - length: Int -} ----- - -From these type definitions, the library generates the following operation fields: - -**Query**: - - * `movies` - * `moviesAggregate` - * `moviesConnection` - -**Mutation**: - - * `createMovies` - * `deleteMovies` - * `updateMovies` - -**Subscription**: - - * `movieCreated` - * `movieUpdated` - * `movieDeleted` -. - -This page describes how to reduce the operation fields produced using the directives `@query`, `@mutation`, and `@subscription`. - -== `@query` - -This directive is used to limit the availability of query operations in the library. - -=== Definition - -[source, graphql, indent=0] ----- -directive @query(read: Boolean! = true, aggregate: Boolean! = false) on OBJECT | SCHEMA ----- - -[NOTE] -==== -Aggregations are no longer generated by default in the 4.0.0 version of the library. -See xref:migration/v4-migration/index.adoc#opt-in-aggregation[Opt-in aggregation] for more information. -==== - -=== Usage - -.Disable _movies_ and _moviesConnection_ operations -[source, graphql, indent=0] ----- -type Movie @query(read: false, aggregate: true) { - title: String - length: Int -} ----- - -.Disable _moviesAggregate_ operations -[source, graphql, indent=0] ----- -type Movie @query(read: true, aggregate: false) { - title: String - length: Int -} ----- - -== `@mutation` - -This directive is used to limit the availability of mutation operations in the library. - -=== Definition - -[source, graphql, indent=0] ----- -enum MutationFields { - CREATE - UPDATE - DELETE -} - -directive @mutation(operations: [MutationFields!]! = [CREATE, UPDATE, DELETE]) on OBJECT | SCHEMA ----- - -=== Usage - -.Disable Create, Delete, and Update operations for _Movie_ -[source, graphql, indent=0] ----- -type Movie @mutation(operations: []) { - title: String - length: Int -} ----- - -.Enable only Create operations for _Movie_ -[source, graphql, indent=0] ----- -type Movie @mutation(operations: [CREATE]) { - title: String - length: Int -} ----- - -== `@subscription` - -This directive is used to limit subscription operations in the library. - -=== Definition - -[source, graphql, indent=0] ----- -enum SubscriptionFields { - CREATE - UPDATE - DELETE - CREATE_RELATIONSHIP - DELETE_RELATIONSHIP -} - -directive @subscription(operations: [SubscriptionFields!]! = [CREATE, UPDATE, DELETE, CREATE_RELATIONSHIP, DELETE_RELATIONSHIP]) on OBJECT | SCHEMA ----- - -=== Usage - -.Disable subscriptions for _Movie_ -[source, graphql, indent=0] ----- -type Movie @subscription(operations: []) { - title: String - length: Int -} ----- - -.Enable only _movieCreated_ subscription for _Movie_ -[source, graphql, indent=0] ----- -type Movie @subscription(operations: [CREATE]) { - title: String - length: Int -} ----- diff --git a/modules/ROOT/pages/queries-aggregations/sorting.adoc b/modules/ROOT/pages/sorting.adoc similarity index 69% rename from modules/ROOT/pages/queries-aggregations/sorting.adoc rename to modules/ROOT/pages/sorting.adoc index aa5b93bc..923d2391 100644 --- a/modules/ROOT/pages/queries-aggregations/sorting.adoc +++ b/modules/ROOT/pages/sorting.adoc @@ -1,11 +1,9 @@ [[sorting]] -:description: This page describes how to use sorting input types. = Sorting -A sorting input type is generated for every object type defined in your type definitions. -It allows for query results to be sorted by each individual field. +A sorting input type is generated for every object type defined in your type definitions, allowing for query results to be sorted by each individual field. -Using this example type definition: +Using the following example type definition: [source, graphql, indent=0] ---- @@ -15,7 +13,7 @@ type Movie { } ---- -The following sorting input type and query should be generated: +The following sorting input type and query will be generated: [source, graphql, indent=0] ---- @@ -46,7 +44,7 @@ type Query { } ---- -The resulting query should then allow fetching all movies sorted by runtime in ascending order: +The following query would then allow you to fetch all movies sorted by runtime in ascending order: [source, graphql, indent=0] ---- @@ -64,7 +62,7 @@ query { } ---- -Additionally, in case there was a relationship between the `Movie` and an `Actor` type, sorting can also be applied when fetching the `actors` field: +Additionally, say there was a relationship between the `Movie` and an `Actor` type, sorting can also be applied when fetching the `actors` field: [source, graphql, indent=0] ---- diff --git a/modules/ROOT/pages/subscriptions/engines.adoc b/modules/ROOT/pages/subscriptions/engines.adoc deleted file mode 100644 index b4f77f46..00000000 --- a/modules/ROOT/pages/subscriptions/engines.adoc +++ /dev/null @@ -1,169 +0,0 @@ -[[subscription-engines]] -:description: This page describes how a GraphQL subscription may be set along with a @neo4j/graphql server. -= Subscription engines - -This page describes different ways to set up a GraphQL subscription along with a `@neo4j/graphql` server. - -== Default - -The default behavior is automatically set if the `subscriptions` feature is set to `true`, as described in xref::subscriptions/getting-started.adoc[Getting Started]: - -[source, javascript, indent=0] ----- -new Neo4jGraphQL({ - typeDefs, - driver, - features: { - subscriptions: true - }, -}); ----- - -This behavior enables a simple subscription system that works on a single instance. -It is ideal for development, testing, and servers that do not require horizontal scaling. - -[[amqp]] -== AMQP - -Using subscriptions on a server with multiple instances can be complex, as described in xref::subscriptions/scaling.adoc[Horizontal scaling]. -Therefore, the recommended approach is to use a PubSub system, which can be achieved with an AMQP broker such as link:https://www.rabbitmq.com/[RabbitMQ]. -This is supported by the link:https://www.npmjs.com/package/@neo4j/graphql-amqp-subscriptions-engine[@neo4j/graphql-amqp-subscriptions-engine] package. - -The `@neo4j/graphql-amqp-subscriptions-engine` plugin connects to message brokers through the `AMQP 0-9-1` protocol to distribute subscription events across all server instances. - -Some brokers supporting this protocol are: - -* link:https://www.rabbitmq.com/[RabbitMQ] -* link:https://qpid.apache.org/[Apache Qpid] -* link:https://activemq.apache.org/[Apache ActiveMQ] - -The plugin can be installed with `npm`: - -[source, sh, indent=0] ----- -npm install @neo4j/graphql-amqp-subscriptions-engine ----- - -[NOTE] -==== -AMQP 1.0 is **not** supported by this plugin. -==== - -=== Usage - -The AMQP plugin should be instanced and passed to the `subscription` field in features. -This automatically enables the subscriptions with the AMQP broker as a message queue: - -[souce, javascript, indent=0] ----- -const { Neo4jGraphQLAMQPSubscriptionsEngine } = require("@neo4j/graphql-amqp-subscriptions-engine"); - -const amqpSubscription = new Neo4jGraphQLAMQPSubscriptionsEngine({ - connection: { - hostname: "localhost", - username: "guest", - password: "guest", - } -}); - -const neoSchema = new Neo4jGraphQL({ - typeDefs, - driver, - features: { - subscriptions: amqpSubscription, - }, -}); ----- - -=== API -The following options can be passed to the constructor: - -* **connection**: an AMQP uri as a string or a configuration object. -** **hostname**: hostname to be used. -Defaults to `localhost`. -** **username**: defaults to `guest`. -** **password**: defaults to `guest`. -** **port**: port of the AMQP broker. -Defaults to `5672`. -* **exchange**: the exchange to be used in the broker. -Defaults to `neo4j.graphql.subscriptions.fx`. -* **version**: the AMQP version to be used. -Currently only `0-9-1` is supported. - -Additionally, any option supported by link:https://www.npmjs.com/package/amqplib[amqplib] can be passed to `connection`. -To set these configurations up, use the following method: - -* **close(): Promise**: Closes the connection and channel created, and unbinds the event emitter. - -[[custom-subscription]] -== Custom subscription engine - -If none of the existing engines is valid for your use case, you can create a new engine to connect to any broker you may need. -For that, you need to create a new class defining your messaging behavior and it must contain: - -* An `EventEmitter` property called `events` that should emit an event every time the broker sends a message. -* A `publish` method that should publish a new event to the broker. -* Optionally, an `init` method returning a promise that should be called on `getSchema`. -This is useful for setting up the connection to a broker. - -In case you want to handle subscriptions using link:https://redis.io/[redis]: - -[souce, javascript, indent=0] ----- -// Note: This is an example of a custom subscription behavior and not a production ready redis implementation. -class CustomRedisSubscriptionEngine { - constructor(redisClient) { - this.client = redisClient; - this.events = new EventEmitter(); - } - - // This method connects to Redis and sends messages to the eventEmitter when receiving events. - async init(){ - await this.client.connect(); - this.subscriber = this.client.duplicate() - this.publisher = this.client.duplicate(); - await this.subscriber.connect(); - await this.publisher.connect(); - - await this.subscriber.subscribe("graphql-subscriptions", (message) => { - const eventMeta = JSON.parse(message); - this.events.emit(eventMeta.event, eventMeta); // Emits a new event when receiving a new message from redis - }); - } - - async publish(eventMeta) { - await this.publisher.publish("graphql-subscriptions", JSON.stringify(eventMeta)); // Sends a message to redis - } -} - -const client = createClient(); // From https://www.npmjs.com/package/redis -const redisSubscriptions = new CustomRedisSubscriptionEngine(client) - -const neoSchema = new Neo4jGraphQL({ - typeDefs, - driver, - features: { - subscriptions: redisSubscriptions, - }, -}); ----- - -Note that extra properties and methods are often needed to handle the connection to the broker. -However, as long as the messages are sent to the broker in the `publish` method and that these messages are received and then emitted through the `events` property, the subscriptions are properly handled. - -=== Using Typescript - -If using Typescript, you may import the interface `Neo4jGraphQLSubscriptionsEngine` to implement your own class. -Ensure the API is correctly defined: - -[source, typescript] ----- -class CustomRedisEngine implements Neo4jGraphQLSubscriptionsEngine {} ----- - -[NOTE] -==== -Events are sent in order to the class. -However, order is not guaranteed once these events have been broadcasted through a broker. -For cases when ordering is important, you must set up the field `timestamp` in the subscriptions payload. -==== diff --git a/modules/ROOT/pages/subscriptions/events.adoc b/modules/ROOT/pages/subscriptions/events.adoc deleted file mode 100644 index e1e548bd..00000000 --- a/modules/ROOT/pages/subscriptions/events.adoc +++ /dev/null @@ -1,1859 +0,0 @@ -[[subscription-events]] -:description: This page covers a variety of subscription options offered by the Neo4j GraphQL Library. -= Subscription events - -This page covers a variety of subscription options offered by the Neo4j GraphQL Library. - -[NOTE] -=== -Only changes made through `@neo4j/graphql` should trigger the events here described. -Changes made directly to the database or using the xref::reference/directives/cypher.adoc[`@cypher` directive] will **not** trigger any event. -=== - -== `CREATE` - -Subscriptions to `CREATE` events listen *only* to newly created nodes, not new relationships. -In this occasion, a new event is triggered for each new node, containing its properties. - -This action is performed with the top-level subscription `[type]Created`, which contains the following fields: - -* `event`: the event triggering this subscription (in this case, `CREATE`). -* `created`: top-level properties of the newly created node, without relationships. -* `timestamp`: the timestamp in which the mutation was made. -If a same query triggers multiple events, they should have the same timestamp. - -As an example, consider the following type definitions: - -[source,graphql,indent=0] ----- -type Movie { - title: String - genre: String -} ----- - -Note, however, that only changes made through `@neo4j/graphql` should trigger events. -Changes made directly to the database or using the xref::reference/directives/cypher.adoc[`@cypher` directive] will **not** trigger any event. - -== `CREATE` - -Subscriptions to `CREATE` events listen *only* to newly created nodes, not new relationships. -In this occasion, a new event is triggered for each new node, containing its properties. - -This action is performed with the top-level subscription `[type]Created`, which contains the following fields: - -* `event`: the event triggering this subscription (in this case, `CREATE`). -* `created`: top-level properties of the newly created node, without relationships. -* `timestamp`: the timestamp in which the mutation was made. -If a same query triggers multiple events, they should have the same timestamp. - -As an example, consider the following type definitions: - -[source,graphql,indent=0] ----- -type Movie { - title: String - genre: String -} ----- - -A subscription to any newly created node of the `Movie` type should look like this: - -[source,graphql,indent=0] ----- -subscription { - movieCreated { - createdMovie { - title - genre - } - event - timestamp - } -} ----- - -[[update]] -== `UPDATE` - -Subscriptions to `UPDATE` events listen *only* to node properties changes, not updates to other fields. -In this occasion, a new event is triggered for each mutation that modifies the node top-level properties. - -This action is performed with the top-level subscription `[type]Updated`, which contains the following fields: - -* `event`: the event triggering this subscription (in this case, `UPDATE`). -* `updated`: top-level properties of the updated node, without relationships. -* `previousState`: the previous top-level properties of the node, before the `UPDATE` event. -* `timestamp`: the timestamp in which the mutation was made. -If a same query triggers multiple events, they should have the same timestamp. - -As an example, consider the following type definitions: - -[source,graphql,indent=0] ----- -type Movie { - title: String - genre: String -} ----- - -A subscription to any node of the `Movie` type with its properties recently updated should look like this: - -[source,graphql,indent=0] ----- -subscription MovieUpdated { - movieUpdated { - event - previousState { - title - genre - } - updatedMovie { - title - } - timestamp - } -} ----- - -== `DELETE` - -Subscriptions to `DELETE` events listen *only* to nodes being deleted, not deleted relationships. -This action is performed with the top-level subscription `[type]Deleted`, which contains the following fields: - -* `event`: the event triggering this subscription (in this case, `DELETE`). -* `deleted`: top-level properties of the deleted node, without relationships. -* `timestamp`: the timestamp in which the mutation was made. -If a same query triggers multiple events, they should have the same timestamp. - -As an example, consider the following type definitions: - -[source,graphql,indent=0] ----- -type Movie { - title: String - genre: String -} ----- - -A subscription to any deleted nodes of the `Movie` type should look like this: - -[source,graphql,indent=0] ----- -subscription { - movieDeleted { - deletedMovie { - title - } - event - timestamp - } -} ----- - -[[create_relationship]] -== `CREATE_RELATIONSHIP` - -Subscriptions to `CREATE_RELATIONSHIP` events listen for newly created relationships to a node of the specified type. - -[NOTE] -=== -This subscription operation is **only** available for types that define relationship fields. -=== - -== `CREATE_RELATIONSHIP` - -Subscriptions to `CREATE_RELATIONSHIP` events listen to new relationships being created and contain information about the connected nodes. -These events: - -* Contain relationship-specific information, such as the relationship field name and the object containing all relationship field names of the specified type. -* Trigger an equivalent number of events compared to the relationships created, in case a new relationship is created following a mutation and the type targeted is responsible for defining two or more relationships in the schema. -* Contain the relationships object populated with the newly created relationship properties for one single relationship name only (all other relationship names should have a null value). -* Contain the properties of the nodes connected through the relationship, as well as the properties of the new relationship, if any. - -[NOTE] -=== -Connected nodes that may or may not have previously existed are not covered by this subscription. -To subscribe to these nodes' updates, use the xref:subscriptions/events.adoc#_create[`CREATE`] or the xref:subscriptions/events.adoc#_update[`UPDATE`] subscription. -=== - -Subscriptions to `CREATE_RELATIONSHIP` events can be made with the top-level subscription `[type]RelationshipCreated`, which contains the following fields: - -* `event`: the event triggering this subscription (in this case, `CREATE_RELATIONSHIP`). -* `timestamp`: the timestamp in which the mutation was made. -If a same query triggers multiple events, they should have the same timestamp. -* ``: top-level properties of the targeted nodes, without relationships, before the `CREATE_RELATIONSHIP` operation was triggered. -* `relationshipFieldName`: the field name of the newly created relationship. -* `createdRelationship`: an object having all field names of the nodes affected by the newly created relationships. -While any event unrelated to `relationshipFieldName` should be `null`, the ones which are related should contain the relationship properties, if defined, and a `node` key containing the properties of the node on the other side of the relationship. -Only top-level properties, without relationships, are available and they are the properties that already existed before the `CREATE_RELATIONSHIP` operation took place. - -[NOTE] -=== -Irrespective of the relationship direction in the database, the `CREATE_RELATIONSHIP` event is bound to the type targeted for the subscription. -Consequently, if types A and B have xref:subscriptions/events.adoc#create-non-reciprocal-relationships[non-reciprocal relationships] and a GraphQL operation creates a relationship between them (despite being already previously connected in the database), the `CREATE_RELATIONSHIP` event should only return the subscription to the type A. -=== - -As an example, consider the following type definitions: - -[source,graphql,indent=0] ----- -type Movie { - title: String - genre: String - actors: [Actor] @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") - reviewers: [Reviewer] @relationship(type: "REVIEWED", direction: IN, properties: "Reviewed") -} - -type Actor { - name: String -} - -interface ActedIn @relationshipProperties { - screenTime: Int! -} - -type Reviewer { - name: String - reputation: Int -} - -interface Reviewed @relationshipProperties { - score: Int! -} ----- - -Now consider a mutation creating an `Actor` named `Tom Hardy` and a `Reviewer` named `Jane` is connected through a relationship to a `Movie` titled `Inception`. -A `CREATE_RELATIONSHIP` subscription in this case should receive the following events: - -[source,graphql,indent=0] ----- -{ - # 1 - relationship type `ACTED_IN` - event: "CREATE_RELATIONSHIP", - timestamp, - movie: { - title: "Inception", - genre: "Adventure" - }, - relationshipFieldName: "actors", # notice the field name specified here is populated below in the `createdRelationship` object - createdRelationship: { - actors: { - screenTime: 1000, # relationship properties for the relationship type `ACTED_IN` - node: { # top-level properties of the node at the other end of the relationship, in this case `Actor` type - name: "Tom Hardy" - } - }, - reviewers: null # relationship declared by this field name is not covered by this event, check out the following... - } -} -{ - # 2 - relationship type `REVIEWED` - event: "CREATE_RELATIONSHIP", - timestamp, - movie: { - title: "Inception", - genre: "Adventure" - }, - relationshipFieldName: "reviewers", # this event covers the relationship declared by this field name - createdRelationship: { - actors: null, # relationship declared by this field name is not covered by this event - reviewers: { # field name equal to `relationshipFieldName` - score: 8, - node: { - name: "Jane", - reputation: 9 - } - } - } -} ----- - -=== Standard types - -For another example, this time creating a relationship with standard types, consider the following type definitions: - -[source,graphql,indent=0] ----- -type Movie { - title: String - genre: String - actors: [Actor] @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") -} - -type Actor { - name: String -} - -interface ActedIn @relationshipProperties { - screenTime: Int! -} ----- - -A subscription to any `Movie` with newly created relationships should look like this: - -[source,graphql,indent=0] ----- -subscription { - movieRelationshipCreated { - event - timestamp - movie { - title - genre - } - relationshipFieldName - createdRelationship { - actors { - screenTime - node { - name - } - } - } - } -} ----- - -=== Abstract types - -When using abstract types with relationships, you need to specify one or more of the corresponding concrete types when performing the subscription operation. - -These types are generated by the library and conform to the format `[type]EventPayload`, where `[type]` is a concrete type. - -As an example, consider the following type definitions: - -[source,graphql,indent=0] ----- -type Movie { - title: String - genre: String - directors: [Director!]! @relationship(type: "DIRECTED", properties: "Directed", direction: IN) -} - -union Director = Person | Actor - -type Actor { - name: String -} - -type Person { - name: String - reputation: Int -} - -interface Directed @relationshipProperties { - year: Int! -} ----- - -A subscription to any `Movie` newly created relationships should look like this: - -[source,graphql,indent=0] ----- -subscription { - movieRelationshipCreated { - event - timestamp - movie { - title - genre - } - relationshipFieldName - createdRelationship { - directors { - year - node { - ... on PersonEventPayload { # generated type - name - reputation - } - ... on ActorEventPayload { # generated type - name - } - } - } - } - } -} ----- - -=== Interface - -For an example in which a relationship is created with an interface, consider the following type definitions: - -[source,graphql,indent=0] ----- -type Movie { - title: String - genre: String - reviewers: [Reviewer!]! @relationship(type: "REVIEWED", properties: "Review", direction: IN) -} - -interface Reviewer { - reputation: Int! -} - -type Magazine implements Reviewer { - title: String - reputation: Int! -} - -type Influencer implements Reviewer { - name: String - reputation: Int! -} - -interface Review @relationshipProperties { - score: Int! -} ----- - -A subscription to any `Movie` newly created relationships should look like this: - -[source,graphql,indent=0] ----- -subscription { - movieRelationshipCreated { - event - timestamp - movie { - title - genre - } - relationshipFieldName - createdRelationship { - reviewers { - score - node { - reputation - ... on MagazineEventPayload { # generated type - title - reputation - } - ... on InfluencerEventPayload { # generated type - name - reputation - } - } - } - } - } -} ----- - -=== Non-reciprocal relationships - -Non-reciprocal relationships can be described, for example, as when a type A and a type B hold a relationship, but, in the GraphQL schema, type A is the one defining the relationship to B, while B does not define a relationship to A. - -To illustrate that, consider the following type definitions: - -[source,graphql,indent=0] ----- -type Movie { - title: String - genre: String - actors: [Actor] @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") - directors: [Director!]! @relationship(type: "DIRECTED", properties: "Directed", direction: IN) -} - -type Actor { - name: String - movies: [Movie!]! @relationship(type: "ACTED_IN", properties: "ActedIn", direction: OUT) -} - -type Person { - name: String - reputation: Int -} - -union Director = Person | Actor - -interface ActedIn @relationshipProperties { - screenTime: Int! -} - -interface Directed @relationshipProperties { - year: Int! -} ----- - -Note that the type definitions contain two relationships: - -* `ACTED_IN`, which has a corresponding field defined in both the `Movie` and `Actor` types and, as such, can be considered a reciprocal relationship. -* `DIRECTED`, which is only defined in the `Movie` type. -The `Director` type does not define a matching field and, as such, it can be considered a non-reciprocal relationship. - -Considering the three types previously described (`Movie`, `Actor`, and `Person`), subscribing to `CREATE_RELATIONSHIP` is *not* possible only in the case of the `Person` type, for it does not define any relationships. -For the other two types, here is how to subscribe: - -.`Movie` type -[source,graphql,indent=0] ----- -subscription { - movieRelationshipCreated { - event - timestamp - movie { - title - genre - } - relationshipFieldName - createdRelationship { - actors { # corresponds to the `ACTED_IN` relationship type - screenTime - node { - name - } - } - directors { # corresponds to the `DIRECTED` relationship type - year - node { - ... on PersonEventPayload { - name - reputation - } - ... on ActorEventPayload { - name - } - } - } - } - } -} ----- - -.`Actor` type -[source,graphql,indent=0] ----- -subscription { - actorRelationshipCreated { - event - timestamp - actor { - name - } - relationshipFieldName - createdRelationship { - movies { # corresponds to the `ACTED_IN` relationship type - screenTime - node { - title - genre - } - } - # no other field corresponding to the `DIRECTED` relationship type - } - } -} ----- - -The presence of the `Movie` field inside of `createdRelationship` for the `actorRelationshipCreated` subscription reflects the fact that the `ACTED_IN`-typed relationship is reciprocal. - -Therefore, when a new relationship of this type is created, such as by running this mutation: - -[source,graphql,indent=0] ----- -mutation { - createMovies( - input: [ - { - actors: { - create: [ - { - node: { - name: "Keanu Reeves" - }, - edge: { - screenTime: 420 - } - } - ] - }, - title: "John Wick", - genre: "Action" - } - ] - ) { - movies { - title - genre - } - } -} ----- - -Should prompt two events, in case you have subscribed to `CREATE_RELATIONSHIP` events on both types: - -[source,graphql,indent=0] ----- -{ - # from `movieRelationshipCreated` - event: "CREATE_RELATIONSHIP" - timestamp - movie { - title: "John Wick", - genre: "Action" - } - relationshipFieldName: "actors", - createdRelationship { - actors: { - screenTime: 420, - node: { - name: "Keanu Reeves" - } - }, - directors: null - } -}, -{ - # from `actorRelationshipCreated` - event: "CREATE_RELATIONSHIP" - timestamp - actor { - name: "Keanu Reeves" - } - relationshipFieldName: "movies", - createdRelationship { - movies: { - screenTime: 420, - node: { - title: "John Wick", - genre: "Action" - } - } - } -} ----- - -Now, since the `DIRECTED` relationship between types `Movie` and `Director` is *not* reciprocal, executing this mutation: - -[source,graphql,indent=0] ----- -mutation { - createMovies( - input: [ - { - directors: { - Actor: { # relationship 1 - create: [ - { - node: { - name: "Woody Allen" - }, - edge: { - year: 1989 - } - } - ] - }, - Person: { # relationship 2 - create: [ - { - node: { - name: "Francis Ford Coppola", - reputation: 100 - }, - edge: { - year: 1989 - } - } - ] - } - }, - title: "New York Stories", - genre: "Comedy" - } - ] - ) { - movies { - title - genre - } - } -} ----- - -Should prompt two events, in case you have subscribed to `CREATE_RELATIONSHIP` events on the `Movie` type: - -[source,graphql,indent=0] ----- -{ - # relationship 1 - from `movieRelationshipCreated` - event: "CREATE_RELATIONSHIP" - timestamp - movie { - title: "New York Stories", - genre: "Comedy" - } - relationshipFieldName: "directors", - createdRelationship { - actors: null, - directors: { - year: 1989, - node: { - name: "Woody Allen" - } - } - } -}, -{ - # relationship 2 - from `movieRelationshipCreated` - event: "CREATE_RELATIONSHIP" - timestamp - movie { - title: "New York Stories", - genre: "Comedy" - } - relationshipFieldName: "directors", - createdRelationship { - actors: null, - directors: { - year: 1989, - node: { - name: "Francis Ford Coppola", - reputation: 100 - } - } - } -} ----- - -=== Types using the same Neo4j label - -One scenario to be considered is when Neo4j labels are overriden by a specific GraphQL type. -This can be achieved using the `@node` directive, by specifying the `label` argument. -However, in the majority of cases, this is *not* the recommended approach to design your API. - -As an example, consider these type definitions: - -[source,graphql,indent=0] ----- -type Actor @node(label: "Person") { - name: String - movies: [Movie!]! @relationship(type: "PART_OF", direction: OUT) -} - -typePerson { - name: String - movies: [Movie!]! @relationship(type: "PART_OF", direction: OUT) -} - -type Movie { - title: String - genre: String - people: [Person!]! @relationship(type: "PART_OF", direction: IN) - actors: [Actor!]! @relationship(type: "PART_OF", direction: IN) -} ----- - -Although the example features 3 GraphQL types, in Neo4j there should only ever be 2 types of nodes: labeled `Movie` or labeled `Person`. - -At the database level there is no distinction between `Actor` and `Person`. -Therefore, when creating a new relationship of type `PART_OF`, there should be one event for each of the 2 types. - -Considering the following subscriptions: - -[source,graphql,indent=0] ----- -subscription { - movieRelationshipCreated { - event - timestamp - movie { - title - genre - } - relationshipFieldName - createdRelationship { - people { # corresponds to the `PART_OF` relationship type - node { - name - } - } - actors { # corresponds to the `PART_OF` relationship type - node { - name - } - } - } - } -} - -subscription { - actorRelationshipCreated { - event - timestamp - actor { - name - } - relationshipFieldName - createdRelationship { - movies { # corresponds to the `PART_OF` relationship type - node { - title - genre - } - } - } - } -} ----- - -Running a mutation such as: - -[source,graphql,indent=0] ----- -mutation { - createMovies( - input: [ - { - people: { # relationship 1 - create: [ - { - node: { - name: "John Logan" - } - } - ] - }, - actors: { # relationship 2 - create: [ - { - node: { - name: "Johnny Depp" - } - } - ] - }, - title: "Sweeney Todd", - genre: "Horror" - } - ] - ) { - movies { - title - genre - } - } -} ----- - -Should result in this: - -[source,graphql,indent=0] ----- -{ - # relationship 1 `people` - for GraphQL types `Movie`, `Person` - event: "CREATE_RELATIONSHIP" - timestamp - movie { - title: "Sweeney Todd", - genre: "Horror" - } - relationshipFieldName: "people", - createdRelationship { - people: { - node: { - name: "John Logan" - } - }, - actors: null - } -}, -{ - # relationship 1 `people` - for GraphQL types `Movie`, `Actor` - event: "CREATE_RELATIONSHIP" - timestamp - movie { - title: "Sweeney Todd", - genre: "Horror" - } - relationshipFieldName: "actors", - createdRelationship { - people: null, - actors: { - node: { - name: "John Logan" - } - } - } -}, -{ - # relationship 1 `movies` - for GraphQL types `Actor`, `Movie` - event: "CREATE_RELATIONSHIP" - timestamp - actor { - name: "John Logan" - } - relationshipFieldName: "movies", - createdRelationship { - movies: { - node: { - title: "Sweeney Todd", - genre: "Horror" - } - } - } -}, -{ - # relationship 2 `actors` - for GraphQL types `Movie`,`Person` - event: "CREATE_RELATIONSHIP" - timestamp - movie { - title: "Sweeney Todd", - genre: "Horror" - } - relationshipFieldName: "people", - createdRelationship { - people: { - node: { - name: "Johnny Depp" - } - }, - actors: null - } -}, -{ - # relationship 2 `actors` - for GraphQL types `Movie`, `Actor` - event: "CREATE_RELATIONSHIP" - timestamp - movie { - title: "Sweeney Todd", - genre: "Horror" - } - relationshipFieldName: "actors", - createdRelationship { - people: null, - actors: { - node: { - name: "Johnny Depp" - } - } - } -}, -{ - # relationship 2 `movies` - for GraphQL types `Actor`, `Movie` - event: "CREATE_RELATIONSHIP" - timestamp - actor { - name: "Johnny Depp" - } - relationshipFieldName: "movies", - createdRelationship { - movies: { - node: { - title: "Sweeney Todd", - genre: "Horror" - } - } - } -}, ----- - -In case you have subscribed to `Person` as well, you should receive two more events: - -[source,graphql,indent=0] ----- -{ - # relationship 1 `movies` - for GraphQL types `Person`, `Movie` - event: "CREATE_RELATIONSHIP" - timestamp - actor { - name: "John Logan" - } - relationshipFieldName: "movies", - createdRelationship { - movies: { - node: { - title: "Sweeney Todd", - genre: "Horror" - } - } - } -}, -{ - # relationship 2 `movies` - for GraphQL types `Person`, `Movie` - event: "CREATE_RELATIONSHIP" - timestamp - actor { - name: "Johnny Depp" - } - relationshipFieldName: "movies", - createdRelationship { - movies: { - node: { - title: "Sweeney Todd", - genre: "Horror" - } - } - } -}, ----- - -== `DELETE_RELATIONSHIP` - -Subscriptions to `DELETE_RELATIONSHIP` events listen to relationships being deleted and contain information about the previously connected nodes of a specified type. -This kind of subscription: - -* Is only available for types that define relationship fields. -* Contains relationship-specific information, such as the relationship field name and the object containing all relationship field names of the specified type. -This object should be populated with properties according to the deleted relationship. -* Triggers an equivalent number of events compared to relationships deleted, in case a relationship is deleted following a mutation and the type targeted is responsible for defining two or more relationships in the schema. -* Contains the relationships object populated with the newly deleted relationship properties for one single relationship name only (all other relationship names should have a null value). -* Contains the properties of the nodes connected through the relationship, as well as the properties of the newly deleted relationship, if any. - -[NOTE] -=== -Disconnected nodes that may or may not have been deleted in the process are not covered by this subscription. -To subscribe to these nodes' updates, use the `DELETE` subscriptions. -=== - -Subscriptions to `DELETE_RELATIONSHIP` events can be made with the top-level subscription `[type]RelationshipDeleted`, which contains the following fields: - -* `event`: the event triggering this subscription (in this case, `DELETE_RELATIONSHIP`). -* `timestamp`: the timestamp in which the mutation was made. If a same query triggers multiple events, they should have the same timestamp. -* ``: top-level properties of the targeted nodes, without relationships, before the `DELETE_RELATIONSHIP` operation was triggered. -* `relationshipFieldName`: the field name of the newly deleted relationship. -* `deletedRelationship`: an object having all field names of the nodes affected by the newly deleted relationships. -While any event unrelated to `relationshipFieldName` should be `null`, the ones which are related should contain the relationship properties, if defined, and a node key containing the properties of the node on the other side of the relationship. -Only top-level properties, without relationships, are available and they are the properties that already existed before the `DELETE_RELATIONSHIP` operation took place. - -[NOTE] -=== -Irrespective of the relationship direction in the database, the `DELETE_RELATIONSHIP` event is bound to the type targeted for the subscription. -Consequently, if types A and B have xref:subscriptions/events.adoc#delete-non-reciprocal-relationships[non-reciprocal relationships] and a GraphQL operation deletes a relationship between them (despite being already previously diconnected in the database), the `DELETE_RELATIONSHIP` event should only return the subscription to the type A. -=== - -As an example, consider these type definitions: - -[source,graphql,indent=0] ----- -type Movie { - title: String - genre: String - actors: [Actor] @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") - reviewers: [Reviewer] @relationship(type: "REVIEWED", direction: IN, properties: "Reviewed") -} - -type Actor {s - name: String -} - -interface ActedIn @relationshipProperties { - screenTime: Int! -} - -type Reviewer { - name: String - reputation: Int -} - -interface Reviewed @relationshipProperties { - score: Int! -} ----- - -Now consider a mutation deleting the `Actor` named `Tom Hardy` and the `Reviewer` named `Jane`, which are connected through a relationship to a `Movie` titled `Inception`. -A `DELETE_RELATIONSHIP` subscription in this case should receive the following events: - -[source,graphql,indent=0] ----- -{ - # 1 - relationship type `ACTED_IN` - event: "DELETE_RELATIONSHIP", - timestamp, - movie: { - title: "Inception", - genre: "Adventure" - }, - relationshipFieldName: "actors", # notice the field name specified here is populated below in the `createdRelationship` object - deletedRelationship: { - actors: { - screenTime: 1000, # relationship properties for the relationship type `ACTED_IN` that was deleted - node: { # top-level properties of the node at the other end of the relationship, in this case `Actor` type, before the delete occured - name: "Tom Hardy" - } - }, - reviewers: null # relationship declared by this field name is not covered by this event, check out the following... - } -} -{ - # 2 - relationship type `REVIEWED` - event: "DELETE_RELATIONSHIP", - timestamp, - movie: { - title: "Inception", - genre: "Adventure" - }, - relationshipFieldName: "reviewers", # this event covers the relationship declared by this field name - deletedRelationship: { - actors: null, # relationship declared by this field name is not covered by this event - reviewers: { # field name equal to `relationshipFieldName` - score: 8, - node: { - name: "Jane", - reputation: 9 - } - } - } -} ----- - -=== Standard types - -As an example, consider these type definitions: - -[source,graphql,indent=0] ----- -type Movie { - title: String - genre: String - actors: [Actor] @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") -} - -type Actor { - name: String -} - -interface ActedIn @relationshipProperties { - screenTime: Int! -} ----- - -A subscription to any `Movie` deleted relationships would look like: - -[source, graphql] ----- -subscription { - movieRelationshipDeleted { - event - timestamp - movie { - title - genre - } - relationshipFieldName - deletedRelationship { - actors { - screenTime - node { - name - } - } - } - } -} ----- - -==== Delete Relationship with Abstract Types - -When using Abstract Types with relationships, you will need to specify one or more of the corresponding Concrete Types when performing the subscription operation. - -These types are generated by the library and conform to the format `[type]EventPayload`, where `[type]` is a **Concrete Type**. - -===== Union Example -Considering the following type definitions: - -[source, graphql] ----- -type Movie { - title: String - genre: String - directors: [Director!]! @relationship(type: "DIRECTED", properties: "Directed", direction: IN) -} - -union Director = Person | Actor - -type Actor { - name: String -} - -type Person { - name: String - reputation: Int -} - -interface Directed @relationshipProperties { - year: Int! -} ----- - -A subscription to `Movie` deleted relationships would look like: - -[source, graphql] ----- -subscription { - movieRelationshipDeleted { - event - timestamp - movie { - title - genre - } - relationshipFieldName - deletedRelationship { - directors { - year - node { - ... on PersonEventPayload { # generated type - name - reputation - } - ... on ActorEventPayload { # generated type - name - } - } - } - } - } -} ----- - -===== Interface Example -Considering the following type definitions: - -[source, graphql] ----- -type Movie { - title: String - genre: String - reviewers: [Reviewer!]! @relationship(type: "REVIEWED", properties: "Review", direction: IN) -} - -interface Reviewer { - reputation: Int! -} - -type Magazine implements Reviewer { - title: String - reputation: Int! -} - -type Influencer implements Reviewer { - name: String - reputation: Int! -} - -interface Review @relationshipProperties { - score: Int! -} ----- - -A subscription to `Movie` deleted relationships would look like: - -[source, graphql] ----- -subscription { - movieRelationshipDeleted { - event - timestamp - movie { - title - genre - } - relationshipFieldName - deletedRelationship { - reviewers { - score - node { - reputation - ... on MagazineEventPayload { # generated type - title - reputation - } - ... on InfluencerEventPayload { # generated type - name - reputation - } - } - } - } - } -} ----- - -[[delete-non-reciprocal-relationships]] -==== Non-reciprocal relationships - -Considering the following type definitions: - -[source, graphql] ----- -type Movie { - title: String - genre: String - actors: [Actor] @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") - directors: [Director!]! @relationship(type: "DIRECTED", properties: "Directed", direction: IN) -} - -type Actor { - name: String - movies: [Movie!]! @relationship(type: "ACTED_IN", properties: "ActedIn", direction: OUT) -} - -type Person { - name: String - reputation: Int -} - -union Director = Person | Actor - -interface ActedIn @relationshipProperties { - screenTime: Int! -} - -interface Directed @relationshipProperties { - year: Int! -} ----- - -The type definitions contain 2 relationships: types `ACTED_IN` and `DIRECTED`. - -It can be observed that the `ACTED_IN` relationship has a corresponding field defined in both the `Movie` and `Actor` types. As such, we can say that `ACTED_IN` is a reciprocal relationship. - -`DIRECTED` on the other hand is only defined in the `Movie` type. The `Director` type does not define a matching field. As such, we can say `DIRECTED` is **not** a reciprocal relationship. - -Let us now take a look at how we can subscribe to deleted relationships for the 3 types defined above: - -===== Movie - -[source, graphql] ----- -subscription { - movieRelationshipDeleted { - event - timestamp - movie { - title - genre - } - relationshipFieldName - deletedRelationship { - actors { # corresponds to the `ACTED_IN` relationship type - screenTime - node { - name - } - } - directors { # corresponds to the `DIRECTED` relationship type - year - node { - ... on PersonEventPayload { - name - reputation - } - ... on ActorEventPayload { - name - } - } - } - } - } -} ----- - -===== Person - -As the `Person` type does not define any relationships, it is **not** possible to subscribe to `DELETE_RELATIONSHIP` events for this type. - -===== Actor - -[source, graphql] ----- -subscription { - actorRelationshipDeleted { - event - timestamp - actor { - name - } - relationshipFieldName - deletedRelationship { - movies { # corresponds to the `ACTED_IN` relationship type - screenTime - node { - title - genre - } - } - # no other field corresponding to the `DIRECTED` relationship type - } - } -} ----- - -The presence of the `movie` field inside of `deletedRelationship` for the `actorRelationshipDeleted` subscription reflects the fact that the `ACTED_IN` typed relationship is reciprocal. - -Therefore, when a relationship of this type is deleted, such as by running the following mutations: - -[source, graphql] ----- -mutation { - createMovies( - input: [ - { - actors: { - create: [ - { - node: { - name: "Keanu Reeves" - }, - edge: { - screenTime: 420 - } - } - ] - }, - title: "John Wick", - genre: "Action" - } - ] - ) { - movies { - title - genre - } - } -} - -mutation { - deleteMovies( - where: { - title: "John Wick" - } - ) { - nodesDeleted - } -} ----- - -Two events will be published (given that we subscribed to `DELETE_RELATIONSHIP` events on both types): - -[source, graphql] ----- -{ - # from `movieRelationshipDeleted` - event: "DELETE_RELATIONSHIP" - timestamp - movie { - title: "John Wick", - genre: "Action" - } - relationshipFieldName: "actors", - deletedRelationship { - actors: { - screenTime: 420, - node: { - name: "Keanu Reeves" - } - }, - directors: null - } -}, -{ - # from `actorRelationshipDeleted` - event: "DELETE_RELATIONSHIP" - timestamp - actor { - name: "Keanu Reeves" - } - relationshipFieldName: "movies", - deletedRelationship { - movies: { - screenTime: 420, - node: { - title: "John Wick", - genre: "Action" - } - } - } -} ----- - -Since the `DIRECTED` relationship between types `Movie` and `Director` is **not** reciprocal, executing the following mutations: - -[source, graphql] ----- -mutation { - createMovies( - input: [ - { - directors: { - Actor: { # relationship 1 - create: [ - { - node: { - name: "Woody Allen" - }, - edge: { - year: 1989 - } - } - ] - }, - Person: { # relationship 2 - create: [ - { - node: { - name: "Francis Ford Coppola", - reputation: 100 - }, - edge: { - year: 1989 - } - } - ] - } - }, - title: "New York Stories", - genre: "Comedy" - } - ] - ) { - movies { - title - genre - } - } -} - -mutation { - deleteMovies( - where: { - title: "New York Stories" - } - ) { - nodesDeleted - } -} ----- - -Two events will be published (given that we subscribed to `DELETE_RELATIONSHIP` events on the `Movie` type): - -[source, graphql] ----- -{ - # relationship 1 - from `movieRelationshipDeleted` - event: "DELETE_RELATIONSHIP" - timestamp - movie { - title: "New York Stories", - genre: "Comedy" - } - relationshipFieldName: "directors", - deletedRelationship { - actors: null, - directors: { - year: 1989, - node: { - name: "Woody Allen" - } - } - } -}, -{ - # relationship 2 - from `movieRelationshipDeleted` - event: "DELETE_RELATIONSHIP" - timestamp - movie { - title: "New York Stories", - genre: "Comedy" - } - relationshipFieldName: "directors", - deletedRelationship { - actors: null, - directors: { - year: 1989, - node: { - name: "Francis Ford Coppola", - reputation: 100 - } - } - } -} ----- - -=== Special Considerations - -==== Types using the same Neo4j label - -One case that deserves special consideration is overriding the label in Neo4j for a specific GraphQL type. -This can be achieved using the `@node` directive, by specifying the `label` argument. - -NOTE: While this section serves an informative purpose, it should be mentioned that, in the majority of cases, this is not the recommended approach of designing your API. - -Consider the following type definitions: - -[source, graphql] ----- -type Actor @node(label: "Person") { - name: String - movies: [Movie!]! @relationship(type: "PART_OF", direction: OUT) -} - -typePerson { - name: String - movies: [Movie!]! @relationship(type: "PART_OF", direction: OUT) -} - -type Movie { - title: String - genre: String - people: [Person!]! @relationship(type: "PART_OF", direction: IN) - actors: [Actor!]! @relationship(type: "PART_OF", direction: IN) -} ----- - -Although we have 3 GraphQL types, in Neo4j there will only ever be 2 types of nodes: labeled `Movie` or labeled `Person`. - -At the database level there is no distinction between `Actor` and `Person`. Therefore, when deleting a relationship of type `PART_OF`, there will be one event for each of the 2 types. - -Considering the following subscriptions: - -[source, graphql] ----- -subscription { - movieRelationshipDeleted { - event - timestamp - movie { - title - genre - } - relationshipFieldName - deletedRelationship { - people { # corresponds to the `PART_OF` relationship type - node { - name - } - } - actors { # corresponds to the `PART_OF` relationship type - node { - name - } - } - } - } -} - -subscription { - actorRelationshipDeleted { - event - timestamp - actor { - name - } - relationshipFieldName - deletedRelationship { - movies { # corresponds to the `PART_OF` relationship type - node { - title - genre - } - } - } - } -} ----- - -Running the following mutations: - -[source, graphql] ----- -mutation { - createMovies( - input: [ - { - people: { # relationship 1 - create: [ - { - node: { - name: "John Logan" - } - } - ] - }, - actors: { # relationship 2 - create: [ - { - node: { - name: "Johnny Depp" - } - } - ] - }, - title: "Sweeney Todd", - genre: "Horror" - } - ] - ) { - movies { - title - genre - } - } -} - -mutation { - deleteMovies( - where: { - title: "Sweeney Todd" - } - ) { - nodesDeleted - } -} ----- - -Result in the following events: - -[source, graphql] ----- -{ - # relationship 1 `people` - for GraphQL types `Movie`, `Person` - event: "DELETE_RELATIONSHIP" - timestamp - movie { - title: "Sweeney Todd", - genre: "Horror" - } - relationshipFieldName: "people", - deletedRelationship { - people: { - node: { - name: "John Logan" - } - }, - actors: null - } -}, -{ - # relationship 1 `people` - for GraphQL types `Movie`, `Actor` - event: "DELETE_RELATIONSHIP" - timestamp - movie { - title: "Sweeney Todd", - genre: "Horror" - } - relationshipFieldName: "actors", - deletedRelationship { - people: null, - actors: { - node: { - name: "John Logan" - } - } - } -}, -{ - # relationship 1 `movies` - for GraphQL types `Actor`, `Movie` - event: "DELETE_RELATIONSHIP" - timestamp - actor { - name: "John Logan" - } - relationshipFieldName: "movies", - deletedRelationship { - movies: { - node: { - title: "Sweeney Todd", - genre: "Horror" - } - } - } -}, -{ - # relationship 2 `actors` - for GraphQL types `Movie`,`Person` - event: "DELETE_RELATIONSHIP" - timestamp - movie { - title: "Sweeney Todd", - genre: "Horror" - } - relationshipFieldName: "people", - deletedRelationship { - people: { - node: { - name: "Johnny Depp" - } - }, - actors: null - } -}, -{ - # relationship 2 `actors` - for GraphQL types `Movie`, `Actor` - event: "DELETE_RELATIONSHIP" - timestamp - movie { - title: "Sweeney Todd", - genre: "Horror" - } - relationshipFieldName: "actors", - deletedRelationship { - people: null, - actors: { - node: { - name: "Johnny Depp" - } - } - } -}, -{ - # relationship 2 `movies` - for GraphQL types `Actor`, `Movie` - event: "DELETE_RELATIONSHIP" - timestamp - actor { - name: "Johnny Depp" - } - relationshipFieldName: "movies", - deletedRelationship { - movies: { - node: { - title: "Sweeney Todd", - genre: "Horror" - } - } - } -}, ----- - -Had we subscribed to `Person` as well, we would have received two more events: - -[source, graphql] ----- -{ - # relationship 1 `movies` - for GraphQL types `Person`, `Movie` - event: "DELETE_RELATIONSHIP" - timestamp - actor { - name: "John Logan" - } - relationshipFieldName: "movies", - deletedRelationship { - movies: { - node: { - title: "Sweeney Todd", - genre: "Horror" - } - } - } -}, -{ - # relationship 2 `movies` - for GraphQL types `Person`, `Movie` - event: "DELETE_RELATIONSHIP" - timestamp - actor { - name: "Johnny Depp" - } - relationshipFieldName: "movies", - deletedRelationship { - movies: { - node: { - title: "Sweeney Todd", - genre: "Horror" - } - } - } -}, ----- diff --git a/modules/ROOT/pages/subscriptions/events/create.adoc b/modules/ROOT/pages/subscriptions/events/create.adoc new file mode 100644 index 00000000..b26636fe --- /dev/null +++ b/modules/ROOT/pages/subscriptions/events/create.adoc @@ -0,0 +1,38 @@ +[[create]] += Create subscriptions + +Subscriptions to `CREATE` events will listen to newly created nodes. A new event will be triggered for each new node. +The event will contain the node's properties. + +NOTE: Only nodes created will trigger this event, new relationships will **not** trigger the event. + +== `CREATE` event + +A subscription to a type can be made with the top-level subscription `[type]Created`. The subscription will contain the following fields: + +* `event`: The event triggering this subscription, in this case it will always be `"CREATE"`. +* `created`: The properties of the newly created node, only top-level properties, without relationships, are available. +* `timestamp`: The timestamp in which the mutation was made. Note that multiple events may come with the same timestamp if triggered by the same query. + +=== Example +Considering the following type definitions: +```graphql +type Movie { + title: String + genre: String +} +``` + +A subscription to any `Movie` created would look like: +```graphql +subscription { + movieCreated { + createdMovie { + title + genre + } + event + timestamp + } +} +``` diff --git a/modules/ROOT/pages/subscriptions/events/create_relationship.adoc b/modules/ROOT/pages/subscriptions/events/create_relationship.adoc new file mode 100644 index 00000000..f7ecd6da --- /dev/null +++ b/modules/ROOT/pages/subscriptions/events/create_relationship.adoc @@ -0,0 +1,796 @@ +[[create_relationship]] += Create relationship subscriptions + +Subscriptions to `CREATE_RELATIONSHIP` events will listen for newly created relationships to a node of the specified type. + +NOTE: This subscription operation is **only** available for types that define relationship fields. + +As relationship-specific information, the event will contain the relationship field name, as well as an object containing all relationship field names of the specified type. This object will be populated with properties according to the newly created relationship. + +NOTE: A new event will be triggered for each new relationship. + +This means that, if the type targeted for the subscriptions defines two or more relationships in the schema and one of each relationships are created following a mutation operation, the number of events triggered will be equivalent to the number of relationships created. + +Each event will have the relationships object populated with the created relationship's properties for one single relationship name only - all other relationship names will have a null value. + +The event will also contain the properties of the nodes at both ends of the relationship, as well as the properties of the new relationship, if any. + +NOTE: The `CREATE_RELATIONSHIP` events represent new relationships being created and contain information about the nodes at each end of the new relationship. However, the connected nodes may or may not have previously existed. To subscribe to the node's updates, you need to use one of the `CREATE` or `UPDATE` subscriptions. + +== `CREATE_RELATIONSHIP` event + +A subscription to a type can be made with the top-level subscription `[type]RelationshipCreated`. The subscription will contain the following fields: + +* `event`: The event triggering this subscription, in this case it will always be `"CREATE_RELATIONSHIP"`. +* `timestamp`: The timestamp in which the mutation was made. Note that multiple events may come with the same timestamp if triggered by the same query. +* ``: The properties of the node target to the subscription. Only top-level properties, without relationships, are available. Note these are the properties before the operation that triggered the `CREATE_RELATIONSHIP` took place. +* `relationshipFieldName`: The field name of the newly created relationship, as part of the node target to the subscription. +* `createdRelationship`: An object having as keys all field names of the node target to the subscription which represent its relationships. For any given event, all field names except the one corresponding to `relationshipFieldName` will be null. The field name equal to `relationshipFieldName` will contain the relationship properties if defined, and a `node` key containing the properties of the node on the other side of the relationship. Only top-level properties, without relationships, are available. Note these are the properties before the operation that triggered the `CREATE_RELATIONSHIP` took place. + +NOTE: Irrespective of the relationship direction in the database, the `CREATE_RELATIONSHIP` event is bound to the type targeted for the subscription. The consequence is that - given a relationship between types A and B that is not reciprocal (that is, in the GraphQL schema type A defines a relationship to B but B does **not** define a relationship to A) and a GraphQL operation that creates the relationship between them - even though the two nodes will be connected in the database, the `CREATE_RELATIONSHIP` event will only be returned to the subscription to type A. Check out the xref:subscriptions/events/create_relationship.adoc#non-reciprocal-relationships[Non-reciprocal Relationships] section below for more details. + +For example, considering the following type definitions: +```graphql +type Movie { + title: String + genre: String + actors: [Actor] @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") + reviewers: [Reviewer] @relationship(type: "REVIEWED", direction: IN, properties: "Reviewed") +} + +type Actor { + name: String +} + +interface ActedIn @relationshipProperties { + screenTime: Int! +} + +type Reviewer { + name: String + reputation: Int +} + +interface Reviewed @relationshipProperties { + score: Int! +} +``` + +An ongoing subscription to created relationships on the `Movie` type, upon a mutation creating an `Actor` named `Tom Hardy` and a `Reviewer` named `Jane` to a `Movie` titled `Inception` would receive the following events: +```graphql +{ + # 1 - relationship type `ACTED_IN` + event: "CREATE_RELATIONSHIP", + timestamp, + movie: { + title: "Inception", + genre: "Adventure" + }, + relationshipFieldName: "actors", # notice the field name specified here is populated below in the `createdRelationship` object + createdRelationship: { + actors: { + screenTime: 1000, # relationship properties for the relationship type `ACTED_IN` + node: { # top-level properties of the node at the other end of the relationship, in this case `Actor` type + name: "Tom Hardy" + } + }, + reviewers: null # relationship declared by this field name is not covered by this event, check out the following... + } +} +{ + # 2 - relationship type `REVIEWED` + event: "CREATE_RELATIONSHIP", + timestamp, + movie: { + title: "Inception", + genre: "Adventure" + }, + relationshipFieldName: "reviewers", # this event covers the relationship declared by this field name + createdRelationship: { + actors: null, # relationship declared by this field name is not covered by this event + reviewers: { # field name equal to `relationshipFieldName` + score: 8, + node: { + name: "Jane", + reputation: 9 + } + } + } +} +``` + +== Examples + +=== Create Relationship with Standard Types + +For example, considering the following type definitions: +```graphql +type Movie { + title: String + genre: String + actors: [Actor] @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") +} + +type Actor { + name: String +} + +interface ActedIn @relationshipProperties { + screenTime: Int! +} +``` + +A subscription to any `Movie` created relationships would look like: +```graphql +subscription { + movieRelationshipCreated { + event + timestamp + movie { + title + genre + } + relationshipFieldName + createdRelationship { + actors { + screenTime + node { + name + } + } + } + } +} +``` + +=== Create Relationship with Abstract Types + +When using Abstract Types with relationships, you will need to specify one or more of the corresponding Concrete Types when performing the subscription operation. + +These types are generated by the library and conform to the format `[type]EventPayload`, where `[type]` is a **Concrete Type**. + +==== Union Example +Considering the following type definitions: +```graphql +type Movie { + title: String + genre: String + directors: [Director!]! @relationship(type: "DIRECTED", properties: "Directed", direction: IN) +} + +union Director = Person | Actor + +type Actor { + name: String +} + +type Person { + name: String + reputation: Int +} + +interface Directed @relationshipProperties { + year: Int! +} +``` + +A subscription to any `Movie` created relationships would look like: +```graphql +subscription { + movieRelationshipCreated { + event + timestamp + movie { + title + genre + } + relationshipFieldName + createdRelationship { + directors { + year + node { + ... on PersonEventPayload { # generated type + name + reputation + } + ... on ActorEventPayload { # generated type + name + } + } + } + } + } +} +``` + +==== Interface Example +Considering the following type definitions: +```graphql +type Movie { + title: String + genre: String + reviewers: [Reviewer!]! @relationship(type: "REVIEWED", properties: "Review", direction: IN) +} + +interface Reviewer { + reputation: Int! +} + +type Magazine implements Reviewer { + title: String + reputation: Int! +} + +type Influencer implements Reviewer { + name: String + reputation: Int! +} + +interface Review { + score: Int! +} +``` + +A subscription to any `Movie` created relationships would look like: +```graphql +subscription { + movieRelationshipCreated { + event + timestamp + movie { + title + genre + } + relationshipFieldName + createdRelationship { + reviewers { + score + node { + reputation + ... on MagazineEventPayload { # generated type + title + reputation + } + ... on InfluencerEventPayload { # generated type + name + reputation + } + } + } + } + } +} +``` + +[[non-reciprocal-relationships]] +=== Non-reciprocal relationships + +Considering the following type definitions: +```graphql +type Movie { + title: String + genre: String + actors: [Actor] @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") + directors: [Director!]! @relationship(type: "DIRECTED", properties: "Directed", direction: IN) +} + +type Actor { + name: String + movies: [Movie!]! @relationship(type: "ACTED_IN", properties: "ActedIn", direction: OUT) +} + +type Person { + name: String + reputation: Int +} + +union Director = Person | Actor + +interface ActedIn @relationshipProperties { + screenTime: Int! +} + +interface Directed @relationshipProperties { + year: Int! +} +``` + +The type definitions contain 2 relationships: types `ACTED_IN` and `DIRECTED`. + +It can be observed that the `ACTED_IN` relationship has a corresponding field defined in both the `Movie` and `Actor` types. As such, we can say that `ACTED_IN` is a reciprocal relationship. + +`DIRECTED` on the other hand is only defined in the `Movie` type. The `Director` type does not define a matching field. As such, we can say `DIRECTED` is **not** a reciprocal relationship. + +Let us now take a look at how we can subscribe to created relationships for the 3 types defined above: + +==== Movie +```graphql +subscription { + movieRelationshipCreated { + event + timestamp + movie { + title + genre + } + relationshipFieldName + createdRelationship { + actors { # corresponds to the `ACTED_IN` relationship type + screenTime + node { + name + } + } + directors { # corresponds to the `DIRECTED` relationship type + year + node { + ... on PersonEventPayload { + name + reputation + } + ... on ActorEventPayload { + name + } + } + } + } + } +} +``` + +==== Person + +As the `Person` type does not define any relationships, it is **not** possible to subscribe to `CREATE_RELATIONSHIP` events for this type. + +==== Actor +```graphql +subscription { + actorRelationshipCreated { + event + timestamp + actor { + name + } + relationshipFieldName + createdRelationship { + movies { # corresponds to the `ACTED_IN` relationship type + screenTime + node { + title + genre + } + } + # no other field corresponding to the `DIRECTED` relationship type + } + } +} +``` + +The presence of the `movie` field inside of `createdRelationship` for the `actorRelationshipCreated` subscription reflects the fact that the `ACTED_IN` typed relationship is reciprocal. + +Therefore, when a new relationship of this type is made, such as by running a mutation as follows: +```graphql +mutation { + createMovies( + input: [ + { + actors: { + create: [ + { + node: { + name: "Keanu Reeves" + }, + edge: { + screenTime: 420 + } + } + ] + }, + title: "John Wick", + genre: "Action" + } + ] + ) { + movies { + title + genre + } + } +} +``` + +Two events will be published (given that we subscribed to `CREATE_RELATIONSHIP` events on both types): +```graphql +{ + # from `movieRelationshipCreated` + event: "CREATE_RELATIONSHIP" + timestamp + movie { + title: "John Wick", + genre: "Action" + } + relationshipFieldName: "actors", + createdRelationship { + actors: { + screenTime: 420, + node: { + name: "Keanu Reeves" + } + }, + directors: null + } +}, +{ + # from `actorRelationshipCreated` + event: "CREATE_RELATIONSHIP" + timestamp + actor { + name: "Keanu Reeves" + } + relationshipFieldName: "movies", + createdRelationship { + movies: { + screenTime: 420, + node: { + title: "John Wick", + genre: "Action" + } + } + } +} +``` + +Since the `DIRECTED` relationship between types `Movie` and `Director` is **not** reciprocal, executing a mutation as follows: +```graphql +mutation { + createMovies( + input: [ + { + directors: { + Actor: { # relationship 1 + create: [ + { + node: { + name: "Woody Allen" + }, + edge: { + year: 1989 + } + } + ] + }, + Person: { # relationship 2 + create: [ + { + node: { + name: "Francis Ford Coppola", + reputation: 100 + }, + edge: { + year: 1989 + } + } + ] + } + }, + title: "New York Stories", + genre: "Comedy" + } + ] + ) { + movies { + title + genre + } + } +} +``` + +Two events will be published (given that we subscribed to `CREATE_RELATIONSHIP` events on the `Movie` type): +```graphql +{ + # relationship 1 - from `movieRelationshipCreated` + event: "CREATE_RELATIONSHIP" + timestamp + movie { + title: "New York Stories", + genre: "Comedy" + } + relationshipFieldName: "directors", + createdRelationship { + actors: null, + directors: { + year: 1989, + node: { + name: "Woody Allen" + } + } + } +}, +{ + # relationship 2 - from `movieRelationshipCreated` + event: "CREATE_RELATIONSHIP" + timestamp + movie { + title: "New York Stories", + genre: "Comedy" + } + relationshipFieldName: "directors", + createdRelationship { + actors: null, + directors: { + year: 1989, + node: { + name: "Francis Ford Coppola", + reputation: 100 + } + } + } +} +``` + +== Special Considerations + +[[connect-same-label]] +=== Types using the same Neo4j label + +One case that deserves special consideration is overriding the label in Neo4j for a specific GraphQL type. +This can be achieved using the `@node` directive, by specifying the `label` argument. + +NOTE: While this section serves an informative purpose, it should be mentioned that, in the majority of cases, this is not the recommended approach of designing your API. + +Consider the following type definitions: +```graphql +type Actor @node(label: "Person") { + name: String + movies: [Movie!]! @relationship(type: "PART_OF", direction: OUT) +} + +typePerson { + name: String + movies: [Movie!]! @relationship(type: "PART_OF", direction: OUT) +} + +type Movie { + title: String + genre: String + people: [Person!]! @relationship(type: "PART_OF", direction: IN) + actors: [Actor!]! @relationship(type: "PART_OF", direction: IN) +} +``` + +Although we have 3 GraphQL types, in Neo4j there will only ever be 2 types of nodes: labeled `Movie` or labeled `Person`. + +At the database level there is no distinction between `Actor` and `Person`. Therefore, when creating a new relationship of type `PART_OF`, there will be one event for each of the 2 types. + +Considering the following subscriptions: + +```graphql +subscription { + movieRelationshipCreated { + event + timestamp + movie { + title + genre + } + relationshipFieldName + createdRelationship { + people { # corresponds to the `PART_OF` relationship type + node { + name + } + } + actors { # corresponds to the `PART_OF` relationship type + node { + name + } + } + } + } +} + +subscription { + actorRelationshipCreated { + event + timestamp + actor { + name + } + relationshipFieldName + createdRelationship { + movies { # corresponds to the `PART_OF` relationship type + node { + title + genre + } + } + } + } +} +``` + +Running a mutation as follows: +```graphql +mutation { + createMovies( + input: [ + { + people: { # relationship 1 + create: [ + { + node: { + name: "John Logan" + } + } + ] + }, + actors: { # relationship 2 + create: [ + { + node: { + name: "Johnny Depp" + } + } + ] + }, + title: "Sweeney Todd", + genre: "Horror" + } + ] + ) { + movies { + title + genre + } + } +} +``` + +Results in the following events: +```graphql +{ + # relationship 1 `people` - for GraphQL types `Movie`, `Person` + event: "CREATE_RELATIONSHIP" + timestamp + movie { + title: "Sweeney Todd", + genre: "Horror" + } + relationshipFieldName: "people", + createdRelationship { + people: { + node: { + name: "John Logan" + } + }, + actors: null + } +}, +{ + # relationship 1 `people` - for GraphQL types `Movie`, `Actor` + event: "CREATE_RELATIONSHIP" + timestamp + movie { + title: "Sweeney Todd", + genre: "Horror" + } + relationshipFieldName: "actors", + createdRelationship { + people: null, + actors: { + node: { + name: "John Logan" + } + } + } +}, +{ + # relationship 1 `movies` - for GraphQL types `Actor`, `Movie` + event: "CREATE_RELATIONSHIP" + timestamp + actor { + name: "John Logan" + } + relationshipFieldName: "movies", + createdRelationship { + movies: { + node: { + title: "Sweeney Todd", + genre: "Horror" + } + } + } +}, +{ + # relationship 2 `actors` - for GraphQL types `Movie`,`Person` + event: "CREATE_RELATIONSHIP" + timestamp + movie { + title: "Sweeney Todd", + genre: "Horror" + } + relationshipFieldName: "people", + createdRelationship { + people: { + node: { + name: "Johnny Depp" + } + }, + actors: null + } +}, +{ + # relationship 2 `actors` - for GraphQL types `Movie`, `Actor` + event: "CREATE_RELATIONSHIP" + timestamp + movie { + title: "Sweeney Todd", + genre: "Horror" + } + relationshipFieldName: "actors", + createdRelationship { + people: null, + actors: { + node: { + name: "Johnny Depp" + } + } + } +}, +{ + # relationship 2 `movies` - for GraphQL types `Actor`, `Movie` + event: "CREATE_RELATIONSHIP" + timestamp + actor { + name: "Johnny Depp" + } + relationshipFieldName: "movies", + createdRelationship { + movies: { + node: { + title: "Sweeney Todd", + genre: "Horror" + } + } + } +}, +``` + +Had we subscribed to `Person` as well, we would have received two more events: +```graphql +{ + # relationship 1 `movies` - for GraphQL types `Person`, `Movie` + event: "CREATE_RELATIONSHIP" + timestamp + actor { + name: "John Logan" + } + relationshipFieldName: "movies", + createdRelationship { + movies: { + node: { + title: "Sweeney Todd", + genre: "Horror" + } + } + } +}, +{ + # relationship 2 `movies` - for GraphQL types `Person`, `Movie` + event: "CREATE_RELATIONSHIP" + timestamp + actor { + name: "Johnny Depp" + } + relationshipFieldName: "movies", + createdRelationship { + movies: { + node: { + title: "Sweeney Todd", + genre: "Horror" + } + } + } +}, +``` diff --git a/modules/ROOT/pages/subscriptions/events/delete.adoc b/modules/ROOT/pages/subscriptions/events/delete.adoc new file mode 100644 index 00000000..17ef73dd --- /dev/null +++ b/modules/ROOT/pages/subscriptions/events/delete.adoc @@ -0,0 +1,35 @@ +[[delete]] += Delete subscriptions + +Subscriptions to `DELETE` events will trigger on deleted nodes. + +NOTE: Only deleted nodes will trigger this event, deleted relationships will **not** trigger any event. + +== `DELETE` event +A subscription to a type can be made with the top-level subscription `[type]Deleted`. The subscription will contain the following fields: + +* `event`: The event triggering this subscription, in this case it will always be `"DELETE"`. +* `deleted`: The top-level properties of the deleted node, these will be the properties right before the node was deleted. Relationships are not available. +* `timestamp`: The timestamp in which the mutation was made. Note that multiple events may come with the same timestamp if triggered by the same query. + +=== Example +Considering the following type definitions: +```graphql +type Movie { + title: String + genre: String +} +``` + +A subscription to any deleted `Movie` would look like: +```graphql +subscription { + movieDeleted { + deletedMovie { + title + } + event + timestamp + } +} +``` diff --git a/modules/ROOT/pages/subscriptions/events/delete_relationship.adoc b/modules/ROOT/pages/subscriptions/events/delete_relationship.adoc new file mode 100644 index 00000000..8431ae63 --- /dev/null +++ b/modules/ROOT/pages/subscriptions/events/delete_relationship.adoc @@ -0,0 +1,826 @@ +[[delete_relationship]] += Delete relationship subscriptions + +Subscriptions to `DELETE_RELATIONSHIP` events will listen for relationships to a node of the specified type that have been deleted. + +NOTE: This subscription operation is **only** available for types that define relationship fields. + +As relationship-specific information, the event will contain the relationship field name, as well as an object containing all relationship field names of the specified type. This object will be populated with properties according to the deleted relationship. + +NOTE: A new event will be triggered for each deleted relationship. + +This means that, if the type targeted for the subscriptions defines two or more relationships in the schema and one of each relationships are deleted following a mutation operation, the number of events triggered will be equivalent to the number of relationships deleted. + +Each event will have the relationships object populated with the deleted relationship's properties for one single relationship name only - all other relationship names will have a null value. + +The event will also contain the properties of the nodes at both ends of the relationship, as well as the properties of the new relationship, if any. + +NOTE: The `DELETE_RELATIONSHIP` events represent relationships being deleted and contain information about the nodes at each end of the new relationship. However, the disconnected nodes may or may not have been deleted in the process. To subscribe to the node's updates, you need to use the `DELETE` subscriptions. + +== `DELETE_RELATIONSHIP` event + +A subscription to a type can be made with the top-level subscription `[type]RelationshipDeleted`. The subscription will contain the following fields: + +* `event`: The event triggering this subscription, in this case it will always be `"DELETE_RELATIONSHIP"`. +* `timestamp`: The timestamp in which the mutation was made. Note that multiple events may come with the same timestamp if triggered by the same query. +* ``: The properties of the node target to the subscription. Only top-level properties, without relationships, are available. Note these are the properties before the operation that triggered the `DELETE_RELATIONSHIP` took place. +* `relationshipFieldName`: The field name of the deleted relationship, as part of the node target to the subscription. +* `deletedRelationship`: An object having as keys all field names of the node target to the subscription which represent its relationships. For any given event, all field names except the one corresponding to `relationshipFieldName` will be null. The field name equal to `relationshipFieldName` will contain the relationship properties if defined, and a `node` key containing the properties of the node on the other side of the relationship. Only top-level properties, without relationships, are available. Note these are the properties before the operation that triggered the `DELETE_RELATIONSHIP` took place. + +NOTE: Irrespective of the relationship direction in the database, the `DELETE_RELATIONSHIP` event is bound to the type targeted for the subscription. The consequence is that - given a relationship between types A and B that is not reciprocal (that is, in the GraphQL schema type A defines a relationship to B but B does **not** define a relationship to A) and a GraphQL operation that deletes the relationship between them - even though the two nodes will be disconnected in the database, the `DELETE_RELATIONSHIP` event will only be returned to the subscription to type A. Check out the xref:subscriptions/events/delete_relationship.adoc#non-reciprocal-relationships[Non-reciprocal Relationships] section below for more details. + +For example, considering the following type definitions: +```graphql +type Movie { + title: String + genre: String + actors: [Actor] @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") + reviewers: [Reviewer] @relationship(type: "REVIEWED", direction: IN, properties: "Reviewed") +} + +type Actor { + name: String +} + +interface ActedIn @relationshipProperties { + screenTime: Int! +} + +type Reviewer { + name: String + reputation: Int +} + +interface Reviewed @relationshipProperties { + score: Int! +} +``` + +An ongoing subscription to deleted relationships from the `Movie` type, upon a mutation deleting then `Actor` named `Tom Hardy` and the `Reviewer` named `Jane` from a `Movie` titled `Inception` would receive the following events: +```graphql +{ + # 1 - relationship type `ACTED_IN` + event: "DELETE_RELATIONSHIP", + timestamp, + movie: { + title: "Inception", + genre: "Adventure" + }, + relationshipFieldName: "actors", # notice the field name specified here is populated below in the `createdRelationship` object + deletedRelationship: { + actors: { + screenTime: 1000, # relationship properties for the relationship type `ACTED_IN` that was deleted + node: { # top-level properties of the node at the other end of the relationship, in this case `Actor` type, before the delete occured + name: "Tom Hardy" + } + }, + reviewers: null # relationship declared by this field name is not covered by this event, check out the following... + } +} +{ + # 2 - relationship type `REVIEWED` + event: "DELETE_RELATIONSHIP", + timestamp, + movie: { + title: "Inception", + genre: "Adventure" + }, + relationshipFieldName: "reviewers", # this event covers the relationship declared by this field name + deletedRelationship: { + actors: null, # relationship declared by this field name is not covered by this event + reviewers: { # field name equal to `relationshipFieldName` + score: 8, + node: { + name: "Jane", + reputation: 9 + } + } + } +} +``` + +== Examples + +=== Delete Relationships with Standard Types + +For example, considering the following type definitions: +```graphql +type Movie { + title: String + genre: String + actors: [Actor] @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") +} + +type Actor { + name: String +} + +interface ActedIn @relationshipProperties { + screenTime: Int! +} +``` + +A subscription to any `Movie` deleted relationships would look like: +```graphql +subscription { + movieRelationshipDeleted { + event + timestamp + movie { + title + genre + } + relationshipFieldName + deletedRelationship { + actors { + screenTime + node { + name + } + } + } + } +} +``` + +=== Delete Relationship with Abstract Types + +When using Abstract Types with relationships, you will need to specify one or more of the corresponding Concrete Types when performing the subscription operation. + +These types are generated by the library and conform to the format `[type]EventPayload`, where `[type]` is a **Concrete Type**. + +==== Union Example +Considering the following type definitions: +```graphql +type Movie { + title: String + genre: String + directors: [Director!]! @relationship(type: "DIRECTED", properties: "Directed", direction: IN) +} + +union Director = Person | Actor + +type Actor { + name: String +} + +type Person { + name: String + reputation: Int +} + +interface Directed @relationshipProperties { + year: Int! +} +``` + +A subscription to `Movie` deleted relationships would look like: +```graphql +subscription { + movieRelationshipDeleted { + event + timestamp + movie { + title + genre + } + relationshipFieldName + deletedRelationship { + directors { + year + node { + ... on PersonEventPayload { # generated type + name + reputation + } + ... on ActorEventPayload { # generated type + name + } + } + } + } + } +} +``` + +==== Interface Example +Considering the following type definitions: +```graphql +type Movie { + title: String + genre: String + reviewers: [Reviewer!]! @relationship(type: "REVIEWED", properties: "Review", direction: IN) +} + +interface Reviewer { + reputation: Int! +} + +type Magazine implements Reviewer { + title: String + reputation: Int! +} + +type Influencer implements Reviewer { + name: String + reputation: Int! +} + +interface Review { + score: Int! +} +``` + +A subscription to `Movie` deleted relationships would look like: +```graphql +subscription { + movieRelationshipDeleted { + event + timestamp + movie { + title + genre + } + relationshipFieldName + deletedRelationship { + reviewers { + score + node { + reputation + ... on MagazineEventPayload { # generated type + title + reputation + } + ... on InfluencerEventPayload { # generated type + name + reputation + } + } + } + } + } +} +``` + +[[non-reciprocal-relationships]] +=== Non-reciprocal relationships + +Considering the following type definitions: +```graphql +type Movie { + title: String + genre: String + actors: [Actor] @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") + directors: [Director!]! @relationship(type: "DIRECTED", properties: "Directed", direction: IN) +} + +type Actor { + name: String + movies: [Movie!]! @relationship(type: "ACTED_IN", properties: "ActedIn", direction: OUT) +} + +type Person { + name: String + reputation: Int +} + +union Director = Person | Actor + +interface ActedIn @relationshipProperties { + screenTime: Int! +} + +interface Directed @relationshipProperties { + year: Int! +} +``` + +The type definitions contain 2 relationships: types `ACTED_IN` and `DIRECTED`. + +It can be observed that the `ACTED_IN` relationship has a corresponding field defined in both the `Movie` and `Actor` types. As such, we can say that `ACTED_IN` is a reciprocal relationship. + +`DIRECTED` on the other hand is only defined in the `Movie` type. The `Director` type does not define a matching field. As such, we can say `DIRECTED` is **not** a reciprocal relationship. + +Let us now take a look at how we can subscribe to deleted relationships for the 3 types defined above: + +==== Movie +```graphql +subscription { + movieRelationshipDeleted { + event + timestamp + movie { + title + genre + } + relationshipFieldName + deletedRelationship { + actors { # corresponds to the `ACTED_IN` relationship type + screenTime + node { + name + } + } + directors { # corresponds to the `DIRECTED` relationship type + year + node { + ... on PersonEventPayload { + name + reputation + } + ... on ActorEventPayload { + name + } + } + } + } + } +} +``` + +==== Person + +As the `Person` type does not define any relationships, it is **not** possible to subscribe to `DELETE_RELATIONSHIP` events for this type. + +==== Actor +```graphql +subscription { + actorRelationshipDeleted { + event + timestamp + actor { + name + } + relationshipFieldName + deletedRelationship { + movies { # corresponds to the `ACTED_IN` relationship type + screenTime + node { + title + genre + } + } + # no other field corresponding to the `DIRECTED` relationship type + } + } +} +``` + +The presence of the `movie` field inside of `deletedRelationship` for the `actorRelationshipDeleted` subscription reflects the fact that the `ACTED_IN` typed relationship is reciprocal. + +Therefore, when a relationship of this type is deleted, such as by running the following mutations: +```graphql +mutation { + createMovies( + input: [ + { + actors: { + create: [ + { + node: { + name: "Keanu Reeves" + }, + edge: { + screenTime: 420 + } + } + ] + }, + title: "John Wick", + genre: "Action" + } + ] + ) { + movies { + title + genre + } + } +} + +mutation { + deleteMovies( + where: { + title: "John Wick" + } + ) { + nodesDeleted + } +} +``` + +Two events will be published (given that we subscribed to `DELETE_RELATIONSHIP` events on both types): +```graphql +{ + # from `movieRelationshipDeleted` + event: "DELETE_RELATIONSHIP" + timestamp + movie { + title: "John Wick", + genre: "Action" + } + relationshipFieldName: "actors", + deletedRelationship { + actors: { + screenTime: 420, + node: { + name: "Keanu Reeves" + } + }, + directors: null + } +}, +{ + # from `actorRelationshipDeleted` + event: "DELETE_RELATIONSHIP" + timestamp + actor { + name: "Keanu Reeves" + } + relationshipFieldName: "movies", + deletedRelationship { + movies: { + screenTime: 420, + node: { + title: "John Wick", + genre: "Action" + } + } + } +} +``` + +Since the `DIRECTED` relationship between types `Movie` and `Director` is **not** reciprocal, executing the following mutations: +```graphql +mutation { + createMovies( + input: [ + { + directors: { + Actor: { # relationship 1 + create: [ + { + node: { + name: "Woody Allen" + }, + edge: { + year: 1989 + } + } + ] + }, + Person: { # relationship 2 + create: [ + { + node: { + name: "Francis Ford Coppola", + reputation: 100 + }, + edge: { + year: 1989 + } + } + ] + } + }, + title: "New York Stories", + genre: "Comedy" + } + ] + ) { + movies { + title + genre + } + } +} + +mutation { + deleteMovies( + where: { + title: "New York Stories" + } + ) { + nodesDeleted + } +} +``` + +Two events will be published (given that we subscribed to `DELETE_RELATIONSHIP` events on the `Movie` type): +```graphql +{ + # relationship 1 - from `movieRelationshipDeleted` + event: "DELETE_RELATIONSHIP" + timestamp + movie { + title: "New York Stories", + genre: "Comedy" + } + relationshipFieldName: "directors", + deletedRelationship { + actors: null, + directors: { + year: 1989, + node: { + name: "Woody Allen" + } + } + } +}, +{ + # relationship 2 - from `movieRelationshipDeleted` + event: "DELETE_RELATIONSHIP" + timestamp + movie { + title: "New York Stories", + genre: "Comedy" + } + relationshipFieldName: "directors", + deletedRelationship { + actors: null, + directors: { + year: 1989, + node: { + name: "Francis Ford Coppola", + reputation: 100 + } + } + } +} +``` + +== Special Considerations + +[[connect-same-label]] +=== Types using the same Neo4j label + +One case that deserves special consideration is overriding the label in Neo4j for a specific GraphQL type. +This can be achieved using the `@node` directive, by specifying the `label` argument. + +NOTE: While this section serves an informative purpose, it should be mentioned that, in the majority of cases, this is not the recommended approach of designing your API. + +Consider the following type definitions: +```graphql +type Actor @node(label: "Person") { + name: String + movies: [Movie!]! @relationship(type: "PART_OF", direction: OUT) +} + +typePerson { + name: String + movies: [Movie!]! @relationship(type: "PART_OF", direction: OUT) +} + +type Movie { + title: String + genre: String + people: [Person!]! @relationship(type: "PART_OF", direction: IN) + actors: [Actor!]! @relationship(type: "PART_OF", direction: IN) +} +``` + +Although we have 3 GraphQL types, in Neo4j there will only ever be 2 types of nodes: labeled `Movie` or labeled `Person`. + +At the database level there is no distinction between `Actor` and `Person`. Therefore, when deleting a relationship of type `PART_OF`, there will be one event for each of the 2 types. + +Considering the following subscriptions: + +```graphql +subscription { + movieRelationshipDeleted { + event + timestamp + movie { + title + genre + } + relationshipFieldName + deletedRelationship { + people { # corresponds to the `PART_OF` relationship type + node { + name + } + } + actors { # corresponds to the `PART_OF` relationship type + node { + name + } + } + } + } +} + +subscription { + actorRelationshipDeleted { + event + timestamp + actor { + name + } + relationshipFieldName + deletedRelationship { + movies { # corresponds to the `PART_OF` relationship type + node { + title + genre + } + } + } + } +} +``` + +Running the following mutations: +```graphql +mutation { + createMovies( + input: [ + { + people: { # relationship 1 + create: [ + { + node: { + name: "John Logan" + } + } + ] + }, + actors: { # relationship 2 + create: [ + { + node: { + name: "Johnny Depp" + } + } + ] + }, + title: "Sweeney Todd", + genre: "Horror" + } + ] + ) { + movies { + title + genre + } + } +} + +mutation { + deleteMovies( + where: { + title: "Sweeney Todd" + } + ) { + nodesDeleted + } +} +``` + +Result in the following events: +```graphql +{ + # relationship 1 `people` - for GraphQL types `Movie`, `Person` + event: "DELETE_RELATIONSHIP" + timestamp + movie { + title: "Sweeney Todd", + genre: "Horror" + } + relationshipFieldName: "people", + deletedRelationship { + people: { + node: { + name: "John Logan" + } + }, + actors: null + } +}, +{ + # relationship 1 `people` - for GraphQL types `Movie`, `Actor` + event: "DELETE_RELATIONSHIP" + timestamp + movie { + title: "Sweeney Todd", + genre: "Horror" + } + relationshipFieldName: "actors", + deletedRelationship { + people: null, + actors: { + node: { + name: "John Logan" + } + } + } +}, +{ + # relationship 1 `movies` - for GraphQL types `Actor`, `Movie` + event: "DELETE_RELATIONSHIP" + timestamp + actor { + name: "John Logan" + } + relationshipFieldName: "movies", + deletedRelationship { + movies: { + node: { + title: "Sweeney Todd", + genre: "Horror" + } + } + } +}, +{ + # relationship 2 `actors` - for GraphQL types `Movie`,`Person` + event: "DELETE_RELATIONSHIP" + timestamp + movie { + title: "Sweeney Todd", + genre: "Horror" + } + relationshipFieldName: "people", + deletedRelationship { + people: { + node: { + name: "Johnny Depp" + } + }, + actors: null + } +}, +{ + # relationship 2 `actors` - for GraphQL types `Movie`, `Actor` + event: "DELETE_RELATIONSHIP" + timestamp + movie { + title: "Sweeney Todd", + genre: "Horror" + } + relationshipFieldName: "actors", + deletedRelationship { + people: null, + actors: { + node: { + name: "Johnny Depp" + } + } + } +}, +{ + # relationship 2 `movies` - for GraphQL types `Actor`, `Movie` + event: "DELETE_RELATIONSHIP" + timestamp + actor { + name: "Johnny Depp" + } + relationshipFieldName: "movies", + deletedRelationship { + movies: { + node: { + title: "Sweeney Todd", + genre: "Horror" + } + } + } +}, +``` + +Had we subscribed to `Person` as well, we would have received two more events: +```graphql +{ + # relationship 1 `movies` - for GraphQL types `Person`, `Movie` + event: "DELETE_RELATIONSHIP" + timestamp + actor { + name: "John Logan" + } + relationshipFieldName: "movies", + deletedRelationship { + movies: { + node: { + title: "Sweeney Todd", + genre: "Horror" + } + } + } +}, +{ + # relationship 2 `movies` - for GraphQL types `Person`, `Movie` + event: "DELETE_RELATIONSHIP" + timestamp + actor { + name: "Johnny Depp" + } + relationshipFieldName: "movies", + deletedRelationship { + movies: { + node: { + title: "Sweeney Todd", + genre: "Horror" + } + } + } +}, +``` diff --git a/modules/ROOT/pages/subscriptions/events/update.adoc b/modules/ROOT/pages/subscriptions/events/update.adoc new file mode 100644 index 00000000..34d58859 --- /dev/null +++ b/modules/ROOT/pages/subscriptions/events/update.adoc @@ -0,0 +1,42 @@ +[[update]] += Update subscriptions + +Subscription to `UPDATE` events will listen to node properties changes. A new event will be triggered for each mutation that modifies the node top-level properties. + +NOTE: Update events will only be triggered if any of the properties have changed. An update that doesn't modify the properties will be ignored. + +== `UPDATE` event + +A subscription to a type can be made with the top-level subscription `[type]Updated`. The subscription will contain the following fields: + +* `event`: The event triggering this subscription, in this case it will always be `"UPDATE"`. +* `updated`: The properties of the updated node, after modification. Only top-level properties, without relationships, are available. +* `previousState`: The old properties of the node, right before the update event. Only top-level properties are available. +* `timestamp`: The timestamp in which the mutation was made. Note that multiple events may come with the same timestamp if triggered by the same query. + + +=== Example +Considering the following type definitions: +```graphql +type Movie { + title: String + genre: String +} +``` + +A subscription to any `Movie` updated could look like: +```graphql +subscription MovieUpdated { + movieUpdated { + event + previousState { + title + genre + } + updatedMovie { + title + } + timestamp + } +} +``` diff --git a/modules/ROOT/pages/subscriptions/filtering.adoc b/modules/ROOT/pages/subscriptions/filtering.adoc index 9c311ec2..619976f7 100644 --- a/modules/ROOT/pages/subscriptions/filtering.adoc +++ b/modules/ROOT/pages/subscriptions/filtering.adoc @@ -1,14 +1,11 @@ -[[filtering]] -:description: This page covers how to apply filters to subscriptions in the Neo4j GraphQL Library. +[[create]] = Filtering -This page covers how to apply filters to subscriptions in the Neo4j GraphQL Library. -Note, however, that: +Filtering can only be applied at the root of the Subscription operation. -* Filtering can only be applied at the root of the subscription operation. -* Aggregations are not supported on subscription types and there is currently no available method. +NOTE: Aggregations are not supported on subscription types, so there is currently no way to apply filter on these fields. -A subscription can be created to target the changes to a node (`create`/`update`/`delete``) or to a relationship (`create`/`delete``). +A subscription can be created to target the changes to a node (Create/Update/Delete) or to a relationship (Create/Delete). While the format slightly differs depending on whether the subscription targets a node or a relationship, providing a `where` argument allows for filtering on the events that are returned to the subscription. @@ -18,23 +15,19 @@ When creating a subscription, a number of operators are available for different === Equality operators -All types can be tested for either equality or non-equality. -For the `Boolean` type, these are the only available comparison operators. +All types can be tested for either equality or non-equality. For the `Boolean` type, these are the only available comparison operators. [[filtering-numerical-operators]] === Numerical operators -The following comparison operators are available for numeric types (`Int`, `Float`, xref::type-definitions/types/index.adoc[`BigInt`]): +The following comparison operators are available for numeric types (`Int`, `Float`, xref::type-definitions/types.adoc#type-definitions-types-bigint[`BigInt`]) * `_LT` * `_LTE` * `_GTE` * `_GT` -[NOTE] -==== -Filtering on xref::type-definitions/types/temporal.adoc[Temporal types] and xref::type-definitions/types/spatial.adoc[Spatial types] is not currently supported. -==== +NOTE: Filtering on xref::type-definitions/types.adoc#type-definitions-types-temporal[Temporal Types] and xref::type-definitions/types.adoc#type-definitions-types-spatial[Spatial Types]: is not yet supported. === String comparison @@ -46,31 +39,31 @@ The following case-sensitive comparison operators are only available for use on === Array comparison -The following operator is available on non-array fields, and accepts an array argument: +The following two comparison operators are available on non-array fields, and accept an array argument: * `_IN` -Conversely, the following operator is available on array fields, and accepts a single argument: +Conversely, the following operators are available on array fields, and accept a single argument: * `_INCLUDES` -These operators are available for all types apart from `Boolean`. +These four operators are available for all types apart from `Boolean`. -=== `AND`/`OR` operators +=== AND, OR operators Complex combinations of operators are possible using the `AND`/ `OR` operators. -They accept as argument an array of items of the same format as the `where` argument. -Check xref:subscriptions/filtering.adoc#combining-operators[Combining operators] for an example. +`AND`/`OR` operators accept as argument an array of items of the same format as the `where` argument. + +Check out a usage example in the xref:subscriptions/filtering.adoc#combining-operators[Combining operators] section below. [[node-events-usage]] == Subscribing to node events -The `where` argument allows specifying filters on top-level properties of the targeted nodes. -Only events matching these properties and type are returned to the subscription. - -As an example, consider the following type definitions: +The `where` argument allows for specifying filters on top-level properties of the targeted nodes. +Only events matching these properties and type will be returned to the subscription. +Considering the following type definitions: [source, graphql, indent=0] ---- type Movie { @@ -80,12 +73,10 @@ type Movie { releasedIn: Int } ---- +Below are some example of how filtering can be applied when creating a subscription. -Now, here is how filtering can be applied when creating a subscription: - -=== `CREATE` - -To filter subscriptions to `create` operations for movies by their genre, here is how to do it: +=== Create +We can filter our movies by their genre: [source, graphql, indent=0] ---- @@ -98,16 +89,12 @@ subscription { } ---- -This way, only newly created movies with the genre `"Drama"` trigger events to this subscription. - -[NOTE] -==== -The `where` argument only filters by properties set at the moment of creation. -==== +This way, only newly created movies with the genre `"Drama"` will trigger events to this subscription. -=== `UPDATE` +NOTE: `where` will only filter by properties set at the moment of creation. -Here is how to filter subscription to `update` operations in movies with average rating bigger than 8: +=== Update +We can filter our movies with the average rating bigger than 8: [source, graphql, indent=0] ---- @@ -120,14 +107,9 @@ subscription { } ---- -This way, you should only receive events triggered by movies with the average rating bigger than 8 when they are modified. +This way, we will only receive events triggered by movies with the average rating bigger than 8 being modified. -[NOTE] -==== -The `where` argument only filters properties that already existed *before* the update. -==== - -This is how these events may look like: +NOTE: `Where` will only filter by existing properties before the update. [source, graphql, indent=0] ---- @@ -158,11 +140,10 @@ mutation { } ---- -Therefore, given the previously described subscription, these GraphQL operations should only triggered for `"The Matrix"` movie. - -=== `DELETE` +Therefore, given the above subscription, these GraphQL operations will only be triggered for `"The Matrix"` movie. -Here is how to filter subscription to `delete` operations in movies by their genre, using the `NOT` filter: +=== Delete +we can filter our movies by their genre with the `NOT` filter: [source, graphql, indent=0] ---- @@ -175,21 +156,18 @@ subscription { } ---- -This way, only deleted movies of all genres except for `"Comedy"` should trigger events to this subscription. +This way, only deleted movies of all genres except for `"Comedy"` will trigger events to this subscription. -[NOTE] -==== -The `where` argument only filters properties that already existed before the deletion process. -==== +NOTE: `Where` will only filter by existing properties right before deletion. [[combining-operators]] === Combining operators -All previously mentioned operators can be combined using the `AND`, `OR`, and `NOT` operators. +All above-mentioned operators can be combined using the `AND`/`OR`/`NOT` operators. They accept an array argument with items of the same format as the `where` argument, which means they can also be nested to form complex combinations. -As an exampke, consider a user who likes comedy movies, but not romantic comedies from early 2000, and who has the Matrix Trilogy as their favorite titles. -They could subscribe to any updates that cover this particular set of interests as follows: +Say we like comedy movies except for romantic comedies from early 2000, although our favorite movies are ones from the Matrix Trilogy. +We could subscribe to any updates that we are interested in as follows: [source, graphql, indent=0] ---- @@ -215,21 +193,13 @@ subscription { == Subscribing to relationship events -When subscribing to relationship events, the `where` argument still allows specifying filters on the top-level properties of the targeted nodes. -It also supports specifying filters on the relationship properties (`edge`) and on the top-level properties (`node`) of the nodes at the other end of the relationship. -This is done by using the operators previously described, and the usage is very similar to xref:subscriptions/filtering.adoc#node-events-usage[subscribing to node events]. - -However, filtering by relationship events is an even more powerful logic. -This is because these filters can also express the expected relationship field, or the expected concrete type at the other end of the relationship, provided that they are connected to abstract types. +When subscribing to relationship events, the `where` argument still allows for specifying filters on the top-level properties of the targeted nodes, and also supports specifying filters on the relationship properties (`edge`) and on the top-level properties (`node`) of the nodes at the other end of the relationship. This is done by using the operators described above, and the usage is very similar to the one in xref:subscriptions/filtering.adoc#node-events-usage[Subscribing to node events]. -Note that each relationship field specified is combined with the others using a xref:subscriptions/filtering.adoc#filter-logical-or[logical `OR`]. -Only events matching these relationship field names are returned in the subscription. +The relationship-related filtering logic is even more powerful, as filters can also express the expected relationship field, or the expected concrete type at the other end of the relationship when having relationships to Abstract Types. Note that each relationship field specified is combined with the others using a xref:subscriptions/filtering.adoc#filter-logical-or[logical `OR`]. Only events matching these relationship field names will be returned in the subscription. -You can further filter each relationship field by node and relationship properties. -These fields are combined in the resulting filter with a xref:subscriptions/filtering.adoc#filter-logical-and[logical `AND`]. - -As an example, in the following type definitions: +You can further filter each relationship field by node and relationship properties. As per usual, these fields are combined in the resulting filter with a xref:subscriptions/filtering.adoc#filter-logical-and[logical `AND`]. +Considering the following type definitions: [source, graphql, indent=0] ---- type Movie { @@ -248,7 +218,6 @@ type Actor { ---- The format of the `where` argument is: - [source, graphql, indent=0] ---- { @@ -271,11 +240,10 @@ The format of the `where` argument is: } ---- -The following sections feature examples of how filtering can be applied when creating a subscription to relationship events. - -=== Newly created relationship +Below are some example of how filtering can be applied when creating a subscription to relationship events. -The following example filters the subscriptions to newly created relationships that are connecting a `Movie` from genres other than "Drama", and to an `Actor` with a screen time bigger than 10 minutes: +=== Create Relationship +The following example filters the subscriptions to newly created relationships that are connecting a `Movie` from genres other than "Drama", to an `Actor` with a screen time bigger than 10: [source, graphql, indent=0] ---- @@ -296,14 +264,10 @@ subscription { } ---- -[NOTE] -==== -The `where` argument only filters already existing properties at the moment of the relationship creation. -==== - -=== Newly deleted relationship +NOTE: `where` will only filter by properties set at the moment of creation. -The following example filters the subscriptions to deleted relationships that were connecting a `Movie` of the genre `"Comedy"` or `"Adventure"` to an `Actor` named `"Jim Carrey"`: +=== Delete Relationship +The following example filters the subscriptions to deleted relationships that were connecting a `Movie` of genre Comedy or Adventure to an `Actor` named "Jim Carrey": [source, graphql, indent=0] ---- @@ -324,21 +288,15 @@ subscription { } ---- -[NOTE] -==== -The `where` argument only filters properties that already existed before the relationship deletion. -==== +NOTE: `Where` will only filter by existing properties right before deletion. -=== Relationship-related filters -In addition to filtering node or relationship properties, the relationship-related filtering logic is even more powerful. -This is because these filters can also express the expected relationship field, or the expected concrete type at the other end of the relationship, provided that they are connected to abstract types. +=== Relationship-related filters +In addition to filtering on node or relationship properties, the relationship-related filtering logic is even more powerful, as filters can also express the expected relationship field, or the expected concrete type at the other end of the relationship when having relationships to Abstract Types. -The following examples are valid for both `CREATE_RELATIONSHIP` and `DELETE_RELATIONSHIP` events. -Their purpose is to illustrate the various ways in which a subscription to a relationship event can be filtered. +The following examples are valid for both `CREATE_RELATIONSHIP`/`DELETE_RELATIONSHIP` events. Their purpose is to illustrate the various ways in which a subscription to a relationship event can be filtered in a variety of ways. Considering the following type definitions: - [source, graphql, indent=0] ---- type Movie { @@ -377,7 +335,7 @@ type Magazine implements Reviewer { reputation: Int! } -interface Review @relationshipProperties { +interface Review { score: Int! } ---- @@ -429,17 +387,15 @@ subscription MovieRelationshipDeleted($where: MovieRelationshipDeletedSubscripti } ---- -You can use the following `where` inputs in the GraphQL variable values to get different results: - -==== Filtering via implicit/explicit declaration +Given the above subscription, you can use the following where inputs in the GraphQL variable values to get different results. -Implicit or explicit declaration is used to filter specific relationship types that are expected to be returned to a subscription. +=== Filtering via implicit/explicit declaration -For example, when subscribing to created or deleted relationships to a `Movie`, a user might only be interested in the relationship of type `ACTED_IN`, but indifferent to the properties of the `Actor` node or the other relationships connected to it. -In this case, the corresponding field name of this relationship is `actors`. +Implicit or explicit declaration is used to filter on the specific relationship types that are expected to be returned to a subscription. -By explicitly specifying the `actors` field name, you can filter-out events to other relationship properties: +For example, when subscribing to created or deleted relationships to a `Movie` we might only be interested in the relationship of type `ACTED_IN`, indifferent to the properties of the `Actor` node or of the relationship to it. Note that the corresponding field name of this relationship is `actors`. +By explicitly specifying the `actors` field name, we filter-out events to other relationship properties: [source, graphql, indent=0] ---- { @@ -451,8 +407,7 @@ By explicitly specifying the `actors` field name, you can filter-out events to o } ---- -In case you are interested in `Actor` nodes conforming to some filters, for example with the name starting with the letter "A", the procedure is no different than xref:subscriptions/filtering.adoc#node-events-usage[subscribing to node events]: - +If we were interested in `Actor` nodes conforming to some filters, for example with the name starting with the letter "A", it is no different than when xref:subscriptions/filtering.adoc#node-events-usage[Subscribing to node events]: [source, graphql, indent=0] ---- { @@ -468,8 +423,7 @@ In case you are interested in `Actor` nodes conforming to some filters, for exam } ---- -If you are also interested in the relationship itself conforming to some filters, such as the `Actor` having spent no more than 40 minutes in the `Movie`, this is how the query may look: - +Or we could also be interested in the relationship itself conforming to some filters, like the `Actor` to have spent no more than 40 minutes in the `Movie`: [source, graphql, indent=0] ---- { @@ -488,9 +442,7 @@ If you are also interested in the relationship itself conforming to some filters } ---- -Multiple relationship types can also be included in the returned subscriptions by explicitly specifying the corresponding field names. -For instance: - +Multiple relationship types can be included in the returned subscriptions by explicitly specifying the corresponding field names like so: [source, graphql, indent=0] ---- { @@ -504,8 +456,7 @@ For instance: } ---- -Now, if you are interested in all relationship types, you can either express this implicitly by not specifying any: - +In case we are interested in all relationship types, we can either express this implicitly by not specifying any: [source, graphql, indent=0] ---- { @@ -514,9 +465,7 @@ Now, if you are interested in all relationship types, you can either express thi } } ---- - Or explicitly by specifying the field names of all the relationships connected to the type targeted for the subscription: - [source, graphql, indent=0] ---- { @@ -532,10 +481,9 @@ Or explicitly by specifying the field names of all the relationships connected t } ---- -Note, however, that as **any** filters are applied to **any** of the relationships, explicitly including those that you are interested in subscribing to is a **mandatory** step. - -For example, if all relationships should be returned, but you want to filter-out the `REVIEWED` ones which have a score lower than 7, this is how your query may look like: +NOTE: As soon as we want to apply **any** filter to **any** of the relationships, explicitly including those that we are interested in is **mandatory** +For example if all relationships should be returned, but we want to filter-out the `REVIEWED` ones with a score less than 7: [source, graphql, indent=0] ---- { @@ -553,9 +501,7 @@ For example, if all relationships should be returned, but you want to filter-out } ---- -Different filters can also be applied to different relationships without any constraints. -For example: - +Different filters can be applied to the different relationships without any constraints: [source, graphql, indent=0] ---- { @@ -581,28 +527,15 @@ For example: ---- [[filter-logical-or]] - -[NOTE] -==== -In the previous example, there is an implicit logical `OR` between the `actors`, `directors`, and `reviewers` relationship fields. -This is to say that a relationship of **either** type `ACTED_IN` **or** of type `DIRECTED` **or** of type `REVIEWED` should trigger the previously described subscription. -==== - +NOTE: Note that in the above, there is an implicit logical `OR` between the `actors`, `directors` and `reviewers`, relationship fields. I.e. a relationship of **either** type `ACTED_IN` **or** of type `DIRECTED` **or** of type `REVIEWED` will trigger the subscription above. [[filter-logical-and]] -[NOTE] -==== -There is an implicit logical `AND` between the `edge` and `node` fields inside of the `actors` relationship field. -In other words, the relationship of type `ACTED_IN` with the property `screenTime` less than 60 **and** a target node with name in `["Tom Hardy", "George Clooney"]` should trigger the subscription. -==== - -=== Abstract types +NOTE: Note that there is an implicit logical `AND` between the `edge` and `node` fields inside of the `actors` relationship field. I.e. a relationship of type `ACTED_IN` with the property `screenTime` less than 60 **and** a target node with name in ["Tom Hardy", "George Clooney"] will trigger the subscription. -The following sections describe how to filter subscriptions using abstract types. +=== Abstract Types -==== Union type - -This example illustrates how to filter the node at the other end of the relationship when it is of a union type: +==== Union Type +The following example illustrates how to filter on the node at the other end of the relationship when it is of a Union type: [source, graphql, indent=0] ---- { @@ -632,13 +565,12 @@ This example illustrates how to filter the node at the other end of the relation } ---- -The result is that only relationships of type `DIRECTED` are returned to the subscription, where the `Director` is a `Person` named `"John Doe"`, who directed the movie after 2010, **or** where the `Director` is an `Actor` named `"Tom Hardy"` who directed the movie before 2005. +The result is that only relationships of type `DIRECTED` are returned to the subscription, where the `Director` is a `Person` named `John Doe` who directed the movie after 2010, or where the `Director` is an `Actor` named `Tom Hardy` who directed the movie before 2005. -Note that the relationship field name is split into multiple sections, one for each of the concrete types that make up the union type. -The relationship properties do not exist outside the confines of one of these sections, even though the properties are the same. +NOTE: Note that the relationship field name is split into multiple sections, one for each of the Concrete types that make up the Union type. The relationship properties do not exist outside the confines of one of these sections, even though the properties are the same. -Now, take the other example that did not explicitly specify the concrete types: +What about the example above that did not explicitly specify the Concrete types? [source, graphql, indent=0] ---- { @@ -650,11 +582,8 @@ Now, take the other example that did not explicitly specify the concrete types: } ---- -Following the same logic as for the relationship field names: when nothing is explicitly provided, then all is accepted. -Thus relationships of type `DIRECTED`, established between a `Movie` and any of the concrete types that make up the union type `Director` are returned to the subscription. - -It is equivalent to the following: - +Following the same logic as for the relationship field names, when nothing is explicitly provided then all is accepted. Thus relationships of type `DIRECTED` between a `Movie` and any of the Concrete types that make up the Union type `Director` will be returned to the subscription. +It is therefore equivalent to the following: [source, graphql, indent=0] ---- { @@ -669,8 +598,7 @@ It is equivalent to the following: } ---- -Note that explicitly specifying a concrete type excludes the others from the returned events: - +Of course, it follows that explicitly specifying a Concrete type will exclude the other from the returned events: [source, graphql, indent=0] ---- { @@ -684,11 +612,9 @@ Note that explicitly specifying a concrete type excludes the others from the ret } ---- -In this case, only relationships of type `DIRECTED` between a `Movie` and an `Actor` are returned to the subscription. -Those between a `Movie` and a `Person` are filtered out. +In this case, only relationships of type `DIRECTED` between a `Movie` and an `Actor` will be returned to the subscription, those between a `Movie` and a `Person` being filtered out. One reason why this might be done is to include some filters on the `Actor` type: - [source, graphql, indent=0] ---- { @@ -706,8 +632,7 @@ One reason why this might be done is to include some filters on the `Actor` type } ---- -To include filters on the `Actor` type, but also include the `Person` type in the result, you need to make the intent explicit: - +To include filters on the `Actor` type but also include `Person` type in the result, we need to make the intent explicit: [source, graphql, indent=0] ---- { @@ -727,10 +652,9 @@ To include filters on the `Actor` type, but also include the `Person` type in th ---- -==== Interface type - -The following example illustrates how to filter the node at the other end of the relationship when it is of an interface type: +==== Interface Type +The following example illustrates how to filter on the node at the other end of the relationship when it is of an Interface type: [source, graphql, indent=0] ---- { @@ -761,20 +685,12 @@ The following example illustrates how to filter the node at the other end of the } ---- -This example returns events for relationships between the type `Movie` and `Reviewer`, where the score is higher than 7, and the `Reviewer` is a `Person` named "Jane Doe", with a reputation greater or equal to 7, or the `Reviewer` is a `Magazine` with the reputation of 8. +The above will return events for relationships between the type `Movie` and `Reviewer`, where the score is greater than 7 and the `Reviewer` is a Person named "Jane Doe" with a reputation greater or equal to 7, or the `Reviewer` is a Magazine with the reputation of 8. -[NOTE] -==== -Note how the reputation field is part of the interface type, and can thus be specified in 3 ways: inside the `node` key, inside each concrete type, or in both places. +NOTE: Notice how the reputation field is part of the Interface type, and can thus be specified in 3 ways: inside the `node` key, inside each Concrete type, or in both places. When specified in both places, the filter is composed with a logical `AND`. Type `Person` overrides the `reputation_GTE` operator so the final filter is `reputation_GTE: 7`, while type `Magazine` composes the original operator so the final filter is the interval `reputation_GTE: 8 && reputation_LT: 9`. -When specified in both places, however, the filter is composed with a logical `AND`. -Type `Person` then overrides the `reputation_GTE` operator, so the final filter is `reputation_GTE: 7`. -Likewise, type `Magazine` composes the original operator so the final filter is the interval `reputation_GTE: 8 && reputation_LT: 9`. -==== - - -To get all relationships of type `REVIEWED` with a certain score returned, you can use implicit filtering, such as: +To get all relationships of type `REVIEWED` with a certain score returned, we can make use of the implicit filtering like so: [source, graphql, indent=0] ---- { @@ -790,8 +706,7 @@ To get all relationships of type `REVIEWED` with a certain score returned, you c } ---- -Implicit filtering can still be used, even for relationships of type `REVIEWED` to a `Reviewer` of a specific reputation: - +Even for relationships of type `REVIEWED` to a `Reviewer` of a specific reputation, we can still make use of the implicit filtering: [source, graphql, indent=0] ---- { @@ -807,8 +722,7 @@ Implicit filtering can still be used, even for relationships of type `REVIEWED` } ---- -It is only when a specific concrete type needs to be filtered that you need to be explicit in the concrete types that you are interested in: - +It is only when a specific Concrete type needs to be filtered that we need to be explicit in the Concrete types that we are interested in: [source, graphql, indent=0] ---- { @@ -829,9 +743,7 @@ It is only when a specific concrete type needs to be filtered that you need to b } ---- -This example does not include relationships of type `REVIEWED` to the `Magazine` type. -You can do that by making them explicit: - +The above will not include relationships of type `REVIEWED` to the `Magazine` type. We can include them by making the intent explicit: [source, graphql, indent=0] ---- { diff --git a/modules/ROOT/pages/subscriptions/getting-started.adoc b/modules/ROOT/pages/subscriptions/getting-started.adoc index d091fdc6..abc4503e 100644 --- a/modules/ROOT/pages/subscriptions/getting-started.adoc +++ b/modules/ROOT/pages/subscriptions/getting-started.adoc @@ -1,44 +1,21 @@ [[getting-started]] -:description: This page shows how to start using subscriptions on a GraphQL server. -= Getting started with subscriptions += Getting Started with Subscriptions -This guide shows how to start using subscription capabilities on a GraphQL server. +To get started with subscriptions you need a GraphQL server with subscription capabilities. -[NOTE] -==== -If you use link:https://studio.apollographql.com/[Apollo Studio], make sure to select the link:https://www.npmjs.com/package/graphql-ws[graphql-ws] implementation in the connection settings. -==== +== Example using Apollo and WebSockets +For this example, we will use link:https://www.apollographql.com/[Apollo] and link:https://github.com/enisdenjo/graphql-ws[graphql-ws]. -== Enable subscription capabilities +=== Setting up the server -Before using subscriptions on a GraphQL server, you must enable them by passing the `subscriptions` feature to `Neo4jGraphQL`: - -[source, javascript] ----- -new Neo4jGraphQL({ - typeDefs, - driver, - features: { - subscriptions: true - }, -}); ----- - -== Install dependencies - -Then, the next step is to install the following dependencies: - -[source, bash] ----- +Install the following dependencies: +```bash npm i --save ws graphql-ws neo4j-driver @neo4j/graphql express @apollo/server body-parser cors ----- - -== Set up an `@apollo/server` server +``` -Add the following code to your `index.js` file to implement a simple `@apollo/server` server with subscriptions (for more options, see link:https://www.apollographql.com/docs/apollo-server/data/subscriptions/[Apollo's documentation]): - -[source, javascript, indent=no] ----- +The following code implements a simple `@apollo/server` server with subscriptions. You can find more examples and documentation +on subscriptions in link:https://www.apollographql.com/docs/apollo-server/data/subscriptions/[Apollo's documentation]. +```javascript import { ApolloServer } from "@apollo/server"; import { expressMiddleware } from "@apollo/server/express4"; import { ApolloServerPluginDrainHttpServer } from "@apollo/server/plugin/drainHttpServer"; @@ -46,7 +23,7 @@ import bodyParser from 'body-parser'; import cors from "cors"; import { createServer } from "http"; import neo4j from 'neo4j-driver'; -import { Neo4jGraphQL } from '@neo4j/graphql'; +import { Neo4jGraphQL, Neo4jGraphQLSubscriptionsSingleInstancePlugin } from '@neo4j/graphql'; import { WebSocketServer } from "ws"; import { useServer } from "graphql-ws/lib/use/ws"; import express from 'express'; @@ -66,8 +43,8 @@ const driver = neo4j.driver("bolt://localhost:7687", neo4j.auth.basic("neo4j", " const neoSchema = new Neo4jGraphQL({ typeDefs, driver, - features: { - subscriptions: true + plugins: { + subscriptions: new Neo4jGraphQLSubscriptionsSingleInstancePlugin(), }, }); @@ -128,22 +105,13 @@ async function main() { } main(); ----- - -[NOTE] -==== -This setup uses the default subscriptions mechanism suitable only for development, testing, and single instance servers. -If you need to scale horizontally for a production-ready application, see xref::subscriptions/scaling.adoc[Horizontal scaling]. -==== +``` -== GraphQL subscriptions +NOTE: The example above uses the **xref::subscriptions/plugins/single-instance.adoc[single instance plugin]** and cannot scale horizontally. -With the previous server running, the available subscriptions are set for `Movie` and `Actor`. -You can subscribe to new movies created with the following statement: - -[source, graphql, indent=0] ----- -graphql +=== GraphQL subscriptions +With the previous server running, we have subscriptions available for `Movie` and `Actor`. We can subscribe to new movies created with the following statement: +```graphql subscription { movieCreated(where: { title: "The Matrix" }) { createdMovie { @@ -151,13 +119,10 @@ subscription { } } } ----- - -With that, any new movie created with the matching title will trigger a subscription. -You can try this with the following query: +``` -[source, graphql, indent=0] ----- +Any new movie created with the matching title will trigger a subscription. You can try this with the following query: +```graphql mutation { createMovies(input: [{ title: "The Matrix" }]) { movies { @@ -165,8 +130,7 @@ mutation { } } } ----- - -== Further reading +``` -Keep reading this section on xref:subscriptions/index.adoc[Subscriptions] for more information and advanced examples. \ No newline at end of file +NOTE: This example uses the link:https://www.npmjs.com/package/graphql-ws[graphql-ws] implementation, if you are using Apollo Studio, make sure +to select "graphql-ws" implementation in connection settings. diff --git a/modules/ROOT/pages/subscriptions/index.adoc b/modules/ROOT/pages/subscriptions/index.adoc index c37d2318..a1db59bc 100644 --- a/modules/ROOT/pages/subscriptions/index.adoc +++ b/modules/ROOT/pages/subscriptions/index.adoc @@ -1,13 +1,20 @@ [[subscriptions]] -:description: This section covers how to use subscriptions with the Neo4j GraphQL Library. = Subscriptions -GraphQL subscriptions add real-time capabilities to an API by allowing a server to send data to its clients when a specific event happens. +GraphQL subscriptions add real-time capabilities to your API. Subscriptions allow to listen for changes on the database. -This section covers: +The following subscriptions are available when using `@neo4j/graphql`: -* xref:subscriptions/getting-started.adoc[Getting started] - How to get started with subscriptions by setting up a basic example. -* xref:subscriptions/events.adoc[Events] - How to trigger events when using `@neo4j/graphql`. -* xref:subscriptions/filtering.adoc[Filtering] - How to apply filters to subscriptions. -* xref:subscriptions/scaling.adoc[Horizontal scaling] - How to perform horizontal scaling. -* xref:subscriptions/engines.adoc[Subscriptions engines] - How to set up a GraphQL subscription along with a `@neo4j/graphql` server. +* **xref::subscriptions/events/create.adoc[Create]** - Listen for newly created nodes. +* **xref::subscriptions/events/update.adoc[Update]** - Listen for changes to existing nodes. +* **xref::subscriptions/events/delete.adoc[Delete]** - Listen for deleted nodes. +* **xref::subscriptions/events/create_relationship.adoc[Create Relationship]** - Listen for newly created relationships. +* **xref::subscriptions/events/delete_relationship.adoc[Delete Relationship]** - Listen for deleted relationships. + +All events will be triggered individually, even in events caused by the same mutation. Events are fired on the creation or deletion of nodes and relationships, and on the update of nodes. + +NOTE: Only changes made through `@neo4j/graphql` will trigger events. Changes made directly to the database or using the xref::type-definitions/cypher.adoc[`@cypher` directive] +will **not** trigger any event. + +== Subscriptions with Auth +Some `auth` clauses can be used with subscriptions. How these work is documented xref::auth/subscriptions.adoc[here]. diff --git a/modules/ROOT/pages/subscriptions/plugins/amqp.adoc b/modules/ROOT/pages/subscriptions/plugins/amqp.adoc new file mode 100644 index 00000000..84a4a492 --- /dev/null +++ b/modules/ROOT/pages/subscriptions/plugins/amqp.adoc @@ -0,0 +1,55 @@ +[[amqp]] += AMQP Plugin + +The `@neo4j/graphql-plugin-subscriptions-amqp` plugin connects to brokers through `AMQP 0-9-1` protocol such as: + +* link:https://www.rabbitmq.com/[RabbitMQ] +* link:https://qpid.apache.org/[Apache Qpid] +* link:https://activemq.apache.org/[Apache ActiveMQ] + +It can be installed with npm: + +```sh +npm install @neo4j/graphql-plugin-subscriptions-amqp +``` + +NOTE: AMQP 1.0 is **not** supported by this plugin. + +== Usage + +```javascript +const { Neo4jGraphQLSubscriptionsAMQPPlugin } = require("@neo4j/graphql-plugin-subscriptions-amqp"); + +const plugin = new Neo4jGraphQLSubscriptionsAMQPPlugin({ + connection: { + hostname: "localhost", + username: "guest", + password: "guest", + } +}); + +const neoSchema = new Neo4jGraphQL({ + typeDefs, + driver, + plugins: { + subscriptions: plugin, + }, +}); +``` + +== API +The following options can be passed to the contructor: + +* connection: An AMQP uri as a string or a configuration object: + * hostname: Hostname to be used, defaults to `localhost`. + * username: defaults to `guest`. + * password: defaults to `guest`. + * port: Port of the AMQP broker, defaults to `5672`. +* exchange: The exchange to be used in the broker. Defaults to `neo4j.graphql.subscriptions.fx`. +* version: The AMQP version to be used. Currently only `0-9-1` is supported. + +Additionally, any option supported by link:https://www.npmjs.com/package/amqplib[amqplib] can be passed to `connection`. + + +=== close(): Promise +Closes the connection and channel created and unbinds the event emitter. diff --git a/modules/ROOT/pages/subscriptions/plugins/index.adoc b/modules/ROOT/pages/subscriptions/plugins/index.adoc new file mode 100644 index 00000000..997151f4 --- /dev/null +++ b/modules/ROOT/pages/subscriptions/plugins/index.adoc @@ -0,0 +1,76 @@ +[[plugins]] += Subscription Plugins + +Plugins are used to enable subscriptions in `@neo4j/graphql` and to allow easy connection to different message brokers. This is required to run a production +system with xref::subscriptions/scaling.adoc[horizontal scaling]. + + +== Official plugins +The following plugins are supported officially by `@neo4j/graphql`: + +* **xref::subscriptions/plugins/single-instance.adoc[Single Instance]** - A plugin for use within a single instance of the GraphQL library, exported directly from the library. +* **xref::subscriptions/plugins/amqp.adoc[AMQP]** - Connect to AMQP brokers such as link:https://www.rabbitmq.com/[RabbitMQ]. + +== Create a custom plugin +If none of the existing plugins is valid for your use case, you can create a new plugin to connect to any broker you may need. To do this +you need to create a new class defining your plugin. This class needs to contain the following: + +* An `EventEmitter` property called `events` that should emit an event everytime the broker sends a message. +* A `publish` method that will publish a new event to the broker. +* Optionally, a `init` method returning a promise, that will be called on `getSchema`. This is useful for setting up the connection to a broker. + +For instance, if we wanted to subscribe using link:https://redis.io/[redis]: + +```javascript +// Note: This is an example of a custom plugin and not a production ready redis plugin +class CustomRedisPlugin { + constructor(redisClient) { + this.client = redisClient; + this.events = new EventEmitter(); + } + + // This method connects to Redis and sends messages to the eventEmitter when receiving events. + async init(){ + await this.client.connect(); + this.subscriber = this.client.duplicate() + this.publisher = this.client.duplicate(); + await this.subscriber.connect(); + await this.publisher.connect(); + + await this.subscriber.subscribe("graphql-subscriptions", (message) => { + const eventMeta = JSON.parse(message); + this.events.emit(eventMeta.event, eventMeta); // Emits a new event when receiving a new message from redis + }); + } + + async publish(eventMeta) { + await this.publisher.publish("graphql-subscriptions", JSON.stringify(eventMeta)); // Sends a message to redis + } +} + +const client = createClient(); // From https://www.npmjs.com/package/redis +const redisSubscriptions = new CustomRedisPlugin(client) + +const neoSchema = new Neo4jGraphQL({ + typeDefs, + driver, + plugins: { + subscriptions: redisSubscriptions, + }, +}); +``` + +Note that, like in the previous example, extra properties and methods are often needed to handle the connection to the broker. As long as the messages +are sent to the broker in the `publish` method and that these messages are received and then emitted through the `events` property, the subscriptions +will be properly handled. + +== Using Typescript +If using Typescript, you may import the interface `Neo4jGraphQLSubscriptionsPlugin` to implement your own plugin: + +```typescript +class CustomRedisPlugin implements Neo4jGraphQLSubscriptionsPlugin {} +``` + +== Note about event orders +Events are sent to the plugin in order, however, order is not guaranteed once these events have been broadcasted through a broker. +For cases when ordering is important, the subscriptions payload contains a field `timestamp`. diff --git a/modules/ROOT/pages/subscriptions/plugins/single-instance.adoc b/modules/ROOT/pages/subscriptions/plugins/single-instance.adoc new file mode 100644 index 00000000..4acfa93d --- /dev/null +++ b/modules/ROOT/pages/subscriptions/plugins/single-instance.adoc @@ -0,0 +1,21 @@ +[[single-instance]] += Single Instance Plugin + +The `@neo4j/graphql` library provides a zero-dependency plugin to be used during development to quickly deploy a subscriptions server locally in a single instance. +If can be imported directly: + +```javascript +const { Neo4jGraphQL, Neo4jGraphQLSubscriptionsSingleInstancePlugin } = require('@neo4j/graphql'); + +const neoSchema = new Neo4jGraphQL({ + typeDefs, + driver, + plugins: { + subscriptions: new Neo4jGraphQLSubscriptionsSingleInstancePlugin(), + }, +}); +``` + +For a full example, check the xref::subscriptions/getting-started.adoc[Getting started] guide. + +WARNING: This plugin is not recommended for production environments unless running a single instance. diff --git a/modules/ROOT/pages/subscriptions/scaling.adoc b/modules/ROOT/pages/subscriptions/scaling.adoc index 96bbbeaf..25bca430 100644 --- a/modules/ROOT/pages/subscriptions/scaling.adoc +++ b/modules/ROOT/pages/subscriptions/scaling.adoc @@ -1,48 +1,45 @@ [[horizontal-scaling]] -:description: This page describes horizontal scaling in Neo4j GraphQL. -= Horizontal scaling += Horizontal Scaling -Horizontally scaling any real time system can be complex, especially when dealing with long lived connections such as WebSockets. -Consider the following example, in which Client A is subscribed to a certain event that is triggered by Client B: +== The Problem +Horizontal scaling of any real time system can be tricky. Especially when dealing with long lived connections such as WebSockets. +Consider the following example, in which _Client A_ is subscribed to a certain event, that is triggered by _Client B_: -image::subscriptions/diagram1.svg[title="Basic subscriptions setup example"] +image::subscriptions/diagram1.png[title="Simple subscriptions setup"] -The server running the GraphQL service does the following: -. Receives the mutation by Client B. -. Runs the Cypher query on Neo4j. -. Triggers the subscription event to Client A. +In the previous example, the server running the GraphQL service, will do the following: -This setup works for a single instance of a `@neo4j/graphql` server. -However, when trying to scale horizontally by adding more GraphQL servers, you may encounter the following situation: +1. Receive the _mutation_ by _Client B_. +2. Run the Cypher Query on Neo4j. +3. Trigger the subscription event to _Client A_. -image::subscriptions/diagram2.svg[title="Subscriptions with 2 servers"] +This setup works fine for a single instance of `@neo4j/graphql`. However, when trying to scale horizontally, by adding more GraphQL servers, +we may encounter the following situation: -In this case, Client B is subscribed to one server. -However, when Client A triggers the mutation, it may query a different server. +image::subscriptions/diagram2.png[title="Subscriptions with 2 servers"] -The change happens successfully in the database, and any client connected to the same server receives the subscription event. -However, Client A does **not** receive any update, as the server it's connected to does not get notified of any mutation. +In this case, _Client B_ is subscribed to one server, however, when _Client A_ triggers the mutation, it queries a different server. -This is the default behavior of the subscription engine provided by the library, making it unsuitable for use in a horizontally scaled environment. +The change happens successfully in the database, and any client connected to the same server will receive the subscription event, however, _Client A_ +will **not** receive any update, as the server it's connected to will not get notified of any mutation. -== Using PubSub +This is the behaviour of the xref::subscriptions/plugins/single-instance.adoc[single instance plugin] provided by the library, so it is not suitable for use in a horizontally scaled environment. -One solution to this problem (as well as how `@neo4j/graphql` is intended to work) is to use a PubSub pattern with an external broker to broadcast events through multiple instances. -This can be achieved through different xref::subscriptions/engines.adoc[Subscription engines]. +## Using PubSub +One solution to this problem, and how `@neo4j/graphql` is intended to work, is to use a PubSub pattern with an external broker to broadcast events through multiple +instances. This can be achieved through xref::subscriptions/plugins/index.adoc[subscription plugins]. -Following the previous example, using an intermediate broker to broadcast the events across all instances, the infrastructure would look like this: +Following the previous example, using an intermediate broker to broadcast the events across all instances, the infrastructure would look like: -image::subscriptions/diagram3.svg[title="Subscriptions with 2 servers and a message broker"] +image::subscriptions/diagram3.png[title="Subscriptions with 2 servers and message broker"] -The events are as follow: +In this example, the events are as follow: -. Client B queries the first server. -. The server performs the mutation in the database. -. The same server sends an event to the broker. -. The broker then notifies every server (broadcast), including the server that originally triggered the event. -. Both servers receive the notification and trigger any event to their subscribed clients. +1. _Client B_ will query the first server. +2. The server performs the mutation in the database. +3. The same server sends an event to the broker. +4. The broker will then notify every server (broadcast), including the server that originally triggered the event. +5. Both servers will receive the notification and will trigger any event to their subscribed clients. -== Further reading - -You can find more examples of this type of pattern with `@neo4j/graphql` on xref::subscriptions/engines.adoc[Subscription engines]. +NOTE: You can find examples of usage with supported brokers in xref::subscriptions/plugins/index.adoc[plugins]. diff --git a/modules/ROOT/pages/toolbox.adoc b/modules/ROOT/pages/toolbox.adoc new file mode 100644 index 00000000..629a107f --- /dev/null +++ b/modules/ROOT/pages/toolbox.adoc @@ -0,0 +1,37 @@ +[[Toolbox]] + += Neo4j GraphQL Toolbox + +Experiment with your Neo4j GraphQL API using the purpose-built web IDE. + +> This tool is for development and experimentation purposes only! + +== Link + +Access the Neo4j GraphQL Toolbox under https://graphql-toolbox.neo4j.io + +== Usage + +1. Connect to a Neo4j database with valid credentials +2. Define (or introspect) the type definitions +3. Build the Neo4j GraphQL schema +4. Experiment, query and play + +== Requirements + +1. A Neo4j database, see https://neo4j.com/docs/desktop-manual/current/[Neo4j Desktop] or use a https://neo4j.com/cloud/[Neo4j AuraDB] +2. Make sure the Neo4j database fulfills xref::introduction.adoc#introduction-requirements[these requirements], including the necessary plugins + +== Example + +Define your type definitions directly in the editor or introspect the Neo4j database you connected to. + +image::toolbox-schema-view-v2.png[title="Type definitions view"] + +Then query the Neo4j database using the autogenerated GraphQL queries and mutations from the `@neo4j/graphql` library. + +image::toolbox-editor-view-v2.png[title="Query editor view"] + +Note that you can store your type definitions as favorites and that a range of settings are available to work with. + +Got curious? Try it out right now under https://graphql-toolbox.neo4j.io! diff --git a/modules/ROOT/pages/troubleshooting.adoc b/modules/ROOT/pages/troubleshooting.adoc deleted file mode 100644 index cf38707d..00000000 --- a/modules/ROOT/pages/troubleshooting.adoc +++ /dev/null @@ -1,267 +0,0 @@ -[[troubleshooting]] -= Troubleshooting - -This chapter contains common troubleshooting steps. Additionally, there is a section for xref::troubleshooting.adoc#troubleshooting-faqs[FAQs] (Frequently Asked Questions) where you might find answers to your problems. - -[[troubleshooting-debug-logging]] -== Debug Logging - -=== For `@neo4j/graphql` - -`@neo4j/graphql` uses the https://www.npmjs.com/package/debug[`debug`] library for debug-level logging. You can turn on all debug logging by setting the environment variable `DEBUG` to `@neo4j/graphql:*` when running. For example: - -==== Command line - -[source, bash, indent=0] ----- -DEBUG=@neo4j/graphql:* node src/index.js ----- - -Alternatively, if you are debugging a particular functionality, you can specify a number of namespaces to isolate certain log lines: - -1. `@neo4j/graphql:*` - Logs all -2. `@neo4j/graphql:auth` - Logs the status of authorization header and token extraction, and decoding of JWT -3. `@neo4j/graphql:graphql` - Logs the GraphQL query and variables -4. `@neo4j/graphql:execute` - Logs the Cypher and Cypher paramaters before execution, and summary of execution - -==== Constructor - -You can also enable all debug logging in the library by setting the `debug` argument to `true` in the constructor. - -[source, javascript, indent=0] ----- -const { Neo4jGraphQL } = require("@neo4j/graphql"); -const neo4j = require("neo4j-driver"); -const { ApolloServer } = require("apollo-server"); - -const typeDefs = ` - type Movie { - title: String! - } -`; - -const driver = neo4j.driver( - "bolt://localhost:7687", - neo4j.auth.basic("neo4j", "password") -); - -const neoSchema = new Neo4jGraphQL({ - typeDefs, - driver, - debug: true, -}); ----- - -=== For `@neo4j/introspector` - -`@neo4j/introspector` has its own debug logging namespace and you can turn on logging for it with: - -[source, bash, indent=0] ----- -DEBUG=@neo4j/introspector node src/index.js ----- - -Read more about the xref::introspector.adoc[introspector]. - -[[troubleshooting-query-tuning]] -== Query Tuning - -Hopefully you won't need to perform any query tuning, but if you do, the Neo4j GraphQL Library allows you to set the full array of query options in the request context. - -You can read more about the available query options at https://neo4j.com/docs/cypher-manual/current/query-tuning/query-options/#cypher-query-options[Cypher Manual -> Query Options]. - -_Please only set these options if you know what you are doing._ - -For example, to set the "runtime" option to "interpreted": - -[source, javascript, indent=0] ----- -const { Neo4jGraphQL } = require("@neo4j/graphql"); -const neo4j = require("neo4j-driver"); -const { ApolloServer } = require("apollo-server"); - -const typeDefs = ` - type Movie { - title: String! - } -`; - -const driver = neo4j.driver( - "bolt://localhost:7687", - neo4j.auth.basic("neo4j", "password") -); - -const neoSchema = new Neo4jGraphQL({ - typeDefs, - driver, -}); - -neoSchema.getSchema().then((schema) => { - const server = new ApolloServer({ - schema, - context: ({ req }) => ({ - req, - cypherQueryOptions: { - runtime: "interpreted", - }, - }), - }); - - server.listen().then(({ url }) => { - console.log(`Server ready at ${url}`); - }); -}); ----- - -[[troubleshooting-faqs]] -== FAQs - -This chapter contains commonly asked questions and their solutions. - -=== I've upgraded from <1.1.0 and my `DateTime` fields aren't sorting as expected - -Due to a bug in versions less than 1.1.0, there is a chance that your `DateTime` fields are stored in the database as strings instead of temporal values. You should perform a rewrite of those properties in your database using a Cypher query. For an example where the affected node has label "Movie" and the affected property is "timestamp", you can do this using the following Cypher: - -[source, javascript, indent=0] ----- -MATCH (m:Movie) -WHERE apoc.meta.type(m.timestamp) = "STRING" -SET m.timestamp = datetime(m.timestamp) -RETURN m ----- - -=== I've created some data and then gone to query it, but it's not there - -If you use a causal cluster or an Aura Professional instance, there is a chance that the created data is not yet present on the server which gets connected to on the next GraphQL query. - -You can ensure that the data is available to query by passing a bookmark into your request - see xref::driver-configuration.adoc#driver-configuration-bookmarks[Specifying Neo4j Bookmarks] for more information. - -=== What is `_emptyInput` in my update and create inputs? - -`_emptyInput` will appear in your update and create inputs if you define a type with only auto-generated and/or relationship properties. It is a placeholder property and therefore giving it a value in neither update nor create will give it a value on the node. `_emptyInput` will be removed if you add a user-provided property. - -The following example will create inputs with `_emptyInput`: - -[source, graphql] ----- -type Cookie { - id: ID! @id - owner: Owner! @relationship(type: "HAS_OWNER", direction: OUT) - # f: String # If you don't want _emptyInput, uncomment this line. -} ----- - -=== Relationship nullability isn't being enforced in my graph - -Currently, and given the typeDefs below, Neo4j GraphQL will enforce cardinality when creating and updating a one-one relationship such as the movie director field below: - -[source, graphql, indent=0] ----- -type Movie { - title: String! - director: Person! @relationship(type: "DIRECTED", direction: IN) - actors: [Person!]! @relationship(type: "ACTED_IN", direction: IN) -} - -type Person { - name: String! -} ----- - -However, at this point, there is no mechanism to support validating the actors relationship. Furthermore, there is a known limitation given if you were create a movie and a director in one mutation: - -[source, graphql, indent=0] ----- -mutation { - createMovies( - input: [ - { - title: "Forrest Gump" - director: { create: { node: { name: "Robert Zemeckis" } } } - } - ] - ) { - movies { - title - director { - name - } - } - } -} ----- - -Then delete the director node: - -[source, graphql, indent=0] ----- -mutation { - deletePeople(where: { name: "Robert Zemeckis" }) { - nodesDeleted - } -} ----- - -No error is thrown, even though the schema states that all movies must have a director thus technically rendering the movie node invalid. - -Finally, we do not enforce relationship cardinality on union or interface relationships. - -[[security]] -== Security - -This section describes security considerations and known issues. - -=== Authorization not triggered for empty match - -If a query yields no results, the xref::authentication-and-authorization/authorization.adoc[Authorization] process will not be triggered. -This means that the result will be empty, instead of throwing an authentication error. Unauthorized users may -then discern whether or not a certain type exists in the database, even if data itself cannot be accessed. - -[[appendix-preventing-overfetching]] -== Preventing overfetching - -When querying for unions and interfaces in Cypher, each union member/interface implementation is broken out into a subquery and joined with `UNION`. For example, using one of the examples above, when we query with no `where` argument, each subquery has a similar structure: - -[source, cypher, indent=0] ----- -CALL { - WITH this - OPTIONAL MATCH (this)-[has_content:HAS_CONTENT]->(blog:Blog) - RETURN { __resolveType: "Blog", title: blog.title } -UNION - WITH this - OPTIONAL MATCH (this)-[has_content:HAS_CONTENT]->(journal:Post) - RETURN { __resolveType: "Post" } -} ----- - -Now if you were to leave both subqueries and add a `WHERE` clause for blogs, it would look like this: - -[source, cypher, indent=0] ----- -CALL { - WITH this - OPTIONAL MATCH (this)-[has_content:HAS_CONTENT]->(blog:Blog) - WHERE blog.title IS NOT NULL - RETURN { __resolveType: "Blog", title: blog.title } -UNION - WITH this - OPTIONAL MATCH (this)-[has_content:HAS_CONTENT]->(journal:Post) - RETURN { __resolveType: "Post" } -} ----- - -As you can see, the subqueries are now "unbalanced", which could result in massive overfetching of `Post` nodes. - -So, when a `where` argument is passed in, only union members which are in the `where` object are fetched, so it is essentially acting as a logical OR gate, different from the rest of the `where` arguments in the schema: - -[source, cypher, indent=0] ----- -CALL { - WITH this - OPTIONAL MATCH (this)-[has_content:HAS_CONTENT]->(blog:Blog) - WHERE blog.title IS NOT NULL - RETURN { __resolveType: "Blog", title: blog.title } -} ----- - diff --git a/modules/ROOT/pages/troubleshooting/faqs.adoc b/modules/ROOT/pages/troubleshooting/faqs.adoc new file mode 100644 index 00000000..75f8aca1 --- /dev/null +++ b/modules/ROOT/pages/troubleshooting/faqs.adoc @@ -0,0 +1,91 @@ +[[troubleshooting-faqs]] += FAQs + +This chapter contains commonly asked questions and their solutions. + +== I've upgraded from <1.1.0 and my `DateTime` fields aren't sorting as expected + +Due to a bug in versions less than 1.1.0, there is a chance that your `DateTime` fields are stored in the database as strings instead of temporal values. You should perform a rewrite of those properties in your database using a Cypher query. For an example where the affected node has label "Movie" and the affected property is "timestamp", you can do this using the following Cypher: + +[source, javascript, indent=0] +---- +MATCH (m:Movie) +WHERE apoc.meta.type(m.timestamp) = "STRING" +SET m.timestamp = datetime(m.timestamp) +RETURN m +---- + +== I've created some data and then gone to query it, but it's not there + +If you use a causal cluster or an Aura Professional instance, there is a chance that the created data is not yet present on the server which gets connected to on the next GraphQL query. + +You can ensure that the data is available to query by passing a bookmark into your request - see xref::driver-configuration.adoc[Specifying Neo4j Bookmarks] for more information. + +== What is `_emptyInput` in my update and create inputs? + +`_emptyInput` will appear in your update and create inputs if you define a type with only auto-generated and/or relationship properties. It is a placeholder property and therefore giving it a value in neither update nor create will give it a value on the node. `_emptyInput` will be removed if you add a user-provided property. + +The following example will create inputs with `_emptyInput`: + +```graphql +type Cookie { + id: ID! @id + owner: Owner! @relationship(type: "HAS_OWNER", direction: OUT) + # f: String # If you don't want _emptyInput, uncomment this line. +} +``` + +== Relationship nullability isn't being enforced in my graph + +Currently, and given the typeDefs below, Neo4j GraphQL will enforce cardinality when creating and updating a one-one relationship such as the movie director field below: + +[source, graphql, indent=0] +---- +type Movie { + title: String! + director: Person! @relationship(type: "DIRECTED", direction: IN) + actors: [Person!]! @relationship(type: "ACTED_IN", direction: IN) +} + +type Person { + name: String! +} +---- + +However, at this point, there is no mechanism to support validating the actors relationship. Furthermore, there is a known limitation given if you were create a movie and a director in one mutation: + +[source, graphql, indent=0] +---- +mutation { + createMovies( + input: [ + { + title: "Forrest Gump" + director: { create: { node: { name: "Robert Zemeckis" } } } + } + ] + ) { + movies { + title + director { + name + } + } + } +} +---- + +Then delete the director node: + +[source, graphql, indent=0] +---- +mutation { + deletePeople(where: { name: "Robert Zemeckis" }) { + nodesDeleted + } +} +---- + +No error is thrown, even though the schema states that all movies must have a director thus technically rendering the movie node invalid. + +Finally, we do not enforce relationship cardinality on union or interface relationships. \ No newline at end of file diff --git a/modules/ROOT/pages/troubleshooting/index.adoc b/modules/ROOT/pages/troubleshooting/index.adoc new file mode 100644 index 00000000..3c4b4130 --- /dev/null +++ b/modules/ROOT/pages/troubleshooting/index.adoc @@ -0,0 +1,83 @@ +[[troubleshooting]] += Troubleshooting + +This chapter contains common troubleshooting steps. Additionally, there is a section for xref::troubleshooting/faqs.adoc[FAQs] (Frequently Asked Questions) where you might find answers to your problems. + +[[troubleshooting-debug-logging]] +== Debug Logging + +=== For `@neo4j/graphql` + +`@neo4j/graphql` uses the https://www.npmjs.com/package/debug[`debug`] library for debug-level logging. You can turn on all debug logging by setting the environment variable `DEBUG` to `@neo4j/graphql:*` when running. For example: + +[source, bash, indent=0] +---- +DEBUG=@neo4j/graphql:* node src/index.js +---- + +Alternatively, if you are debugging a particular functionality, you can specify a number of namespaces to isolate certain log lines: + +1. `@neo4j/graphql:*` - Logs all +2. `@neo4j/graphql:auth` - Logs the status of authorization header and token extraction, and decoding of JWT +3. `@neo4j/graphql:graphql` - Logs the GraphQL query and variables +4. `@neo4j/graphql:execute` - Logs the Cypher and Cypher paramaters before execution, and summary of execution + +=== For `@neo4j/introspector` + +`@neo4j/introspector` has its own debug logging namespace and you can turn on logging for it with: + +[source, bash, indent=0] +---- +DEBUG=@neo4j/introspector node src/index.js +---- + +Read more about the xref::introspector.adoc[introspector]. + +[[troubleshooting-query-tuning]] +== Query Tuning + +Hopefully you won't need to perform any query tuning, but if you do, the Neo4j GraphQL Library allows you to set the full array of query options on construction of the library. + +You can read more about the available query options at https://neo4j.com/docs/cypher-manual/current/query-tuning/query-options/#cypher-query-options. + +_Please only set these options if you know what you are doing._ + +For example, in order to set the Cypher runtime to "interpreted": + +[source, javascript, indent=0] +---- +import { ApolloServer } from "@apollo/server"; +import { startStandaloneServer } from "@apollo/server/standalone"; +import { Neo4jGraphQL, CypherRuntime } from "@neo4j/graphql"; +import neo4j from "neo4j-driver"; + +const typeDefs = `#graphql + type Movie { + title: String! + } +`; + +const driver = neo4j.driver( + "bolt://localhost:7687", + neo4j.auth.basic("neo4j", "password") +); + +const neoSchema = new Neo4jGraphQL({ + typeDefs, + driver, + config: { + queryOptions: { + runtime: CypherRuntime.INTERPRETED, + }, + }, +}); + +const server = new ApolloServer({ + schema: await neoSchema.getSchema(), +}); + +await startStandaloneServer(server, { + context: async ({ req }) => ({ req }), +}); + +---- diff --git a/modules/ROOT/pages/troubleshooting/optimizing-create-operations.adoc b/modules/ROOT/pages/troubleshooting/optimizing-create-operations.adoc new file mode 100644 index 00000000..3d9d94b9 --- /dev/null +++ b/modules/ROOT/pages/troubleshooting/optimizing-create-operations.adoc @@ -0,0 +1,19 @@ +[[optimizing-create-operations]] += Optimizing create operations + +It's possible to use the Neo4jGraphQL library to create several nodes and relationships in a single mutation, however, +it's well known that performance issues are present in performing this task. +A solution has been implemented that doesn't require any changes from the user. +However, there are still several situations where this kind of optimization is not achievable. + +== Subscriptions enabled + +No optimizations are available if a Subscription plugin it's being used. + +== `@populated_by` + +No optimizations are available if a Node affected by the mutation has a field with the directive `@populated_by`. + +== `connect` and `connectOrCreate` operations + +No optimizations are available if the GraphQL input contains the `connect` or `connectOrCreate` operation. diff --git a/modules/ROOT/pages/troubleshooting/security.adoc b/modules/ROOT/pages/troubleshooting/security.adoc new file mode 100644 index 00000000..7c664d84 --- /dev/null +++ b/modules/ROOT/pages/troubleshooting/security.adoc @@ -0,0 +1,10 @@ +[[security]] += Security + +This chapter describes security considerations and known issues. + +== Authorization not triggered for empty match + +If a query yields no results, the xref::auth/auth-directive.adoc[Authorization] process will not be triggered. +This means that the result will be empty, instead of throwing an authentication error. Unauthorized users may +then discern whether or not a certain type exists in the database, even if data itself cannot be accessed. diff --git a/modules/ROOT/pages/type-definitions/directives/autogeneration.adoc b/modules/ROOT/pages/type-definitions/autogeneration.adoc similarity index 50% rename from modules/ROOT/pages/type-definitions/directives/autogeneration.adoc rename to modules/ROOT/pages/type-definitions/autogeneration.adoc index 0c34cb8a..e4e66d06 100644 --- a/modules/ROOT/pages/type-definitions/directives/autogeneration.adoc +++ b/modules/ROOT/pages/type-definitions/autogeneration.adoc @@ -1,31 +1,31 @@ [[type-definitions-autogeneration]] -:description: This page describes directives used for autogeneration. = Autogeneration -This page describes directives used for autogeneration: - [[type-definitions-autogeneration-id]] == `@id` -This directive marks a field as an identifier for an object type. -This enables autogeneration of IDs for the field. +This directive marks a field as the unique identifier for an object type, and by default; enables autogeneration of IDs for the field and implies that a unique node property constraint should exist for the property. The format of each generated ID is a UUID generated by https://neo4j.com/docs/cypher-manual/current/functions/scalar/#functions-randomuuid[randomUUID() function]. -The field will not be present in input types for mutations. -It is recommended to use xref::/type-definitions/directives/indexes-and-constraints.adoc#type-definitions-constraints-unique[`@unique`] in conjunction with this to add a unique node property constraint. +If autogeneration for an ID field is enabled, the field will not be present in input types for mutations. + +See xref::type-definitions/indexes-and-constraints.adoc#type-definitions-constraints-unique[Unique node property constraints] for details on how to assert the existence of the necessary database constraints for relevant fields. === Definition [source, graphql, indent=0] ---- -"""Indicates that the field is an identifier for the object type.""" -directive @id on FIELD_DEFINITION +"""Indicates that the field is an identifier for the object type. By default; autogenerated, and has a unique node property constraint in the database.""" +directive @id( + autogenerate: Boolean! = true + unique: Boolean! = true +) on FIELD_DEFINITION ---- === Usage -The following type definition specifies the `id` field as an autogenerated value: +The following two type definitions are equivalent in that they both specify an ID which will benefit from autogeneration: [source, graphql, indent=0] ---- @@ -35,16 +35,42 @@ type User { } ---- +[source, graphql, indent=0] +---- +type User { + id: ID! @id(autogenerate: true) + username: String! +} +---- + +The following type definition is currently a no-op, as the `@id` directive only provides autogeneration as it stands: + +[source, graphql, indent=0] +---- +type User { + id: ID! @id(autogenerate: false) + username: String! +} +---- + +You can disable the mapping of the `@id` directive to a unique node property constraint by setting the `unique` argument to `false`: + +[source, graphql, indent=0] +---- +type User { + id: ID! @id(unique: false) + username: String! +} +---- + [[type-definitions-autogeneration-timestamp]] == `@timestamp` -This directive marks a field as a timestamp field, which can be used to store timestamps of when particular events happen through the GraphQL API. +=== Timestamps -[NOTE] -==== -These events are triggered and stored at the GraphQL API layer. -Events happening in your database through other routes do not trigger updates of these timestamps. -==== +This directive marks a field as a timestamp field, which will be used to store timestamps of when particular events happen through the GraphQL API. + +> These events are triggered and stored at the GraphQL API layer. Events happening in your database through other routes will not trigger updates of these timestamps. === Definition @@ -64,7 +90,7 @@ directive @timestamp( === Usage -The following type definition has two individual fields to store the timestamps of create and update events: +The following type definition has two individual fields to store the timestamps of create and update events. [source, graphql, indent=0] ---- @@ -74,7 +100,7 @@ type User { } ---- -The following two equivalent type definitions have a single field storing the event timestamp of the last `create` or `update`: +The following two equivalent type definitions have a single field storing the event timestamp of the last create or update: [source, graphql, indent=0] ---- @@ -89,14 +115,13 @@ type User { lastModified: DateTime! @timestamp(operations: [CREATE, UPDATE]) } ---- - [[type-definitions-autogeneration-populated-by]] == `@populatedBy` -This directive is used to specify a callback function that gets executed during GraphQL query parsing, +The `@populatedBy` directive is used to specify a callback function that gets executed during GraphQL query parsing, to populate fields which have not been provided within the input. -For non-required values, callbacks may return `undefined` (meaning that nothing is changed or added to the property) or `null` (meaning that the property will be removed). +For non-required values, callbacks may return `undefined` (meaning that nothing will be changed or added to the property) or `null` (meaning that the property will be removed). === Definition @@ -118,7 +143,7 @@ directive @populatedBy( === Usage -Type definitions: +Type Definitions: [source, graphql, indent=0] ---- @@ -128,7 +153,9 @@ type Product { } ---- -Schema construction (note that the callback is asynchronous): +Schema Construction: + +> Note that the callback is asynchronous! [source, javascript, indent=0] ---- @@ -149,12 +176,11 @@ new Neo4jGraphQL({ }) ---- -== Context values +==== Using GraphQL context values -The GraphQL context for the request is available as the third argument in a callback. -This maps to the argument pattern for GraphQL resolvers. +The GraphQL context for the request is available as the third argument in a callback. This maps to the argument pattern for GraphQL resolvers. -For example, if you want a field `modifiedBy`: +For example, if you wanted a field `modifiedBy`: [source, graphql, indent=0] ---- @@ -185,7 +211,17 @@ new Neo4jGraphQL({ }) ---- -Note that the second positional argument, in this case `_args`, has a type of `Record`, and as such it will always be an empty object. +NOTE: The second positional argument, in this case `_args`, has a type of `Record`, and as such will always be an empty object. + + +[[type-definitions-autogeneration-callback]] +== `@callback` + +NOTE: The `@callback` directive has been deprecated and will be removed in version 4.0. Please use xref::type-definitions/autogeneration.adoc#type-definitions-autogeneration-populated-by[`@populatedBy`] instead. + +The `@callback` directive is used to specify a function that will be invoked when updating or creating the properties on a node or relationship. + +For non-required values, callbacks may return `undefined` (meaning that nothing will be changed or added to the property) or `null` (meaning that the property will be removed). === Definition @@ -207,7 +243,7 @@ directive @callback( === Usage -Type definitions: +Type Definitions: [source, graphql, indent=0] ---- @@ -217,7 +253,9 @@ type Product { } ---- -Schema construction (note that the callback is asynchronous): +Schema Construction: + +> Note that the callback is asynchronous! [source, javascript, indent=0] ---- @@ -236,4 +274,41 @@ new Neo4jGraphQL({ } } }) ----- \ No newline at end of file +---- + +==== Using GraphQL context values + +The GraphQL context for the request is available as the third argument in a callback. This maps to the argument pattern for GraphQL resolvers. + +For example, if you wanted a field `modifiedBy`: + +[source, graphql, indent=0] +---- +type Record { + content: String! + modifiedBy: @callback(operations: [CREATE, UPDATE], name: "modifiedBy") +} +---- + +If the username is located in `context.username`, you could define a callback such as: + +[source, javascript, indent=0] +---- +const modifiedByCallback = async (_parent, _args, context) => { + return context.username; +} + +new Neo4jGraphQL({ + typeDefs, + driver, + features: { + populatedBy: { + callbacks: { + modifiedBy: modifiedByCallback + } + } + } +}) +---- + +NOTE: The second positional argument, in this case `_args`, has a type of `Record`, and as such will always be an empty object. diff --git a/modules/ROOT/pages/type-definitions/basics.adoc b/modules/ROOT/pages/type-definitions/basics.adoc new file mode 100644 index 00000000..0f8e005e --- /dev/null +++ b/modules/ROOT/pages/type-definitions/basics.adoc @@ -0,0 +1,61 @@ +[[type-definitions-basics]] += Basics + +Each type in your GraphQL type definitions can be mapped to an entity in your Neo4j database. + +== Nodes + +The most basic mapping is of GraphQL types to Neo4j nodes, where the GraphQL type name maps to the Neo4j node label. + +For example, to represent a node with label "Movie" and a single property "title" of type string: + +[source, graphql, indent=0] +---- +type Movie { + title: String +} +---- + +== Relationships + +Relationships are represented by marking particular fields with a directive. This directive, `@relationship`, defines the relationship type in the database, as well as which direction that relationship goes in. + +Add a second node type, "Actor", and connect the two together: + +[source, graphql, indent=0] +---- +type Movie { + title: String + actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN) +} + +type Actor { + name: String + movies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT) +} +---- + +Note there is a directive on each "end" of the relationship in this case, but it is not essential. + +=== Relationship properties + +In order to add relationship properties to a relationship, you need to add a new type to your type definitions, but this time it will be of type `interface`. For example, for your "ACTED_IN" relationship, add a property "roles": + +[source, graphql, indent=0] +---- +type Movie { + title: String + actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") +} + +type Actor { + name: String + movies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") +} + +interface ActedIn @relationshipProperties { + roles: [String] +} +---- + +Note that in addition to this new interface type, there is an added a key `properties` in the existing `@relationship` directives. diff --git a/modules/ROOT/pages/type-definitions/directives/custom-directives.adoc b/modules/ROOT/pages/type-definitions/custom-directives.adoc similarity index 73% rename from modules/ROOT/pages/type-definitions/directives/custom-directives.adoc rename to modules/ROOT/pages/type-definitions/custom-directives.adoc index a221fab6..6427741b 100644 --- a/modules/ROOT/pages/type-definitions/directives/custom-directives.adoc +++ b/modules/ROOT/pages/type-definitions/custom-directives.adoc @@ -1,11 +1,18 @@ [[type-definitions-custom-directives]] -:description: This page describes how to use custom directives in the Neo4j GraphQL Library. -= Custom directives += Custom Directives -As of https://www.graphql-tools.com/docs/schema-directives[`@graphql-tools`] version 8, the mechanism for defining and applying custom directives has changed significantly, and this is reflected in the Neo4j GraphQL Library. +As of `@graphql-tools` version 8, the mechanism for defining and applying +custom directives has changed significantly, and this is reflected in the +Neo4j GraphQL Library. -To understand the changes, consider the following example. -It works with the implementation of a field directive to uppercase field values, using the following definition: +It comes highly recommended to read and follow the +https://www.graphql-tools.com/docs/schema-directives[`@graphql-tools` documentation on schema directives] +for comprehensive documentation on this feature. + +== Example + +This example will work towards the implementation of a field directive to +uppercase field values, with the following definition: [source, graphql, indent=0] ---- @@ -41,14 +48,16 @@ function upperDirective(directiveName: string) { } ---- -On calling the function, the type definition and the transformer returns for the directive: +On calling the function, the type definition and the transformer will be +returned for the directive: [source, typescript, indent=0] ---- const { upperDirectiveTypeDefs, upperDirectiveTransformer } = upperDirective("uppercase"); ---- -On construction of a `Neo4jGraphQL` instance, the directive definition can be passed into the `typeDefs` array alongside the rest of the type definitions: +On construction of a `Neo4jGraphQL` instance, the directive definition can be +passed into the `typeDefs` array alongside the rest of the type definitions: [source, typescript, indent=0] ---- @@ -65,11 +74,13 @@ const neoSchema = new Neo4jGraphQL({ }); ---- -Finally, the Neo4j GraphQL schema must be transformed using the transformer returned from the directive function: +Finally, the Neo4j GraphQL schema must be transformed using the transformer +returned from the directive function: [source, typescript, indent=0] ---- const schema = upperDirectiveTransformer(await neoSchema.getSchema()); ---- -Note that this `schema` object is an instance of a `GraphQLSchema` which can be used in any GraphQL tools, such as in Apollo Server. +This `schema` object is an instance of a `GraphQLSchema` which can be used in +any GraphQL tools, such as in Apollo Server. diff --git a/modules/ROOT/pages/reference/directives/cypher.adoc b/modules/ROOT/pages/type-definitions/cypher.adoc similarity index 67% rename from modules/ROOT/pages/reference/directives/cypher.adoc rename to modules/ROOT/pages/type-definitions/cypher.adoc index 458e3aad..cb6d8f9f 100644 --- a/modules/ROOT/pages/reference/directives/cypher.adoc +++ b/modules/ROOT/pages/type-definitions/cypher.adoc @@ -3,8 +3,6 @@ The `@cypher` directive binds a GraphQL field to the result(s) of a Cypher query. -This directive can be used both for properties in a type or as top level queries: - == Definition [source, graphql, indent=0] @@ -13,11 +11,37 @@ This directive can be used both for properties in a type or as top level queries directive @cypher( """The Cypher statement to run which returns a value of the same type composition as the field definition on which the directive is applied.""" statement: String!, - """Name of the returned variable from the Cypher statement.""" - columnName: String! + """[Experimental] Name of the returned variable from the Cypher statement, if provided, the query will be optimized to improve performance.""" + columnName: String ) on FIELD_DEFINITION ---- +== Character Escaping + +All double quotes must be _double escaped_ when used in a @cypher directive - once for GraphQL and once for the function in which the Cypher is wrapped. For example, at its simplest: + +[source, graphql, indent=0] +---- +type Example { + string: String! + @cypher( + statement: """ + RETURN \\"field-level string\\" + """ + ) +} + +type Query { + string: String! + @cypher( + statement: """ + RETURN \\"Query-level string\\" + """ + ) +} +---- + +Note the double-backslash (`\\`) before each double quote (`"`). == Globals @@ -27,7 +51,7 @@ Global variables are available for use within the Cypher statement. The value `this` is a reference to the currently resolved node, and it can be used to traverse the graph. -This can be seen in the usage example xref::/type-definitions/directives/cypher.adoc#type-definitions-cypher-object-usage[On an object type field] below. +This can be seen in the usage example xref::type-definitions/cypher.adoc#type-definitions-cypher-object-usage[On an object type field] below. === `auth` @@ -53,10 +77,9 @@ type User { type Query { me: User @cypher( statement: """ - MATCH (user:User {id: $jwt.sub}) + MATCH (user:User {id: $auth.jwt.sub}) RETURN user - """, - columnName: "user" + """ ) } ---- @@ -84,9 +107,9 @@ Use in cypher query: ---- type Query { userPosts: [Post] @cypher(statement: """ - MATCH (:User {id: $userId})-[:POSTED]->(p:Post) + MATCH (:User {id: $cypherParams.userId})-[:POSTED]->(p:Post) RETURN p - """, columnName: "p") + """) } ---- @@ -94,8 +117,6 @@ type Query { The return value of the Cypher statement must be of the same type to which the directive is applied. -The variable should also be aliased with a name that must be the same as the named passed to `columnName`, this can be the name of a node or relationship query or an alias in the `RETURN` statement of the cypher. - === Scalar values The Cypher statement must return a value which matches the scalar type to which the directive was applied. @@ -103,7 +124,7 @@ The Cypher statement must return a value which matches the scalar type to which [source, graphql, indent=0] ---- type Query { - randomNumber: Int @cypher(statement: "RETURN rand() as result", columnName: "result") + randomNumber: Int @cypher(statement: "RETURN rand()") } ---- @@ -123,8 +144,7 @@ type Query { statement: """ MATCH (u:User) RETURN u - """, - columnName: "u" + """ ) } ---- @@ -140,13 +160,42 @@ type Query { MATCH (u:User) RETURN { id: u.id - } as result - """, columnName: "result") + } + """) } ---- The downside of the latter approach is that you will need to adjust the return object as you change your object type definition. +== columnName + +The `columName` argument will change the translation of custom Cypher. Instead of using https://neo4j.com/labs/apoc/4.0/overview/apoc.cypher/apoc.cypher.runFirstColumnMany/[apoc.cypher.runFirstColumnMany] it will directly wrap the query within a `CALL { }` subquery. This behvaiour has proven to be much more performant for the same queries. + + +`columnName` should be the name of the returned variable to be used in the rest of the query. For example: + +The graphql query: +[source, graphql, indent=0] +---- +type query { + test: String! @cypher(statement: "MATCH(m:Movie) RETURN m", columnName: "m") +} +---- + +Would get translated to: +[source,cypher, indent=0] +---- +CALL { + MATCH(m:Movie) RETURN m +} +WITH m AS this +RETURN this +---- + +Additionally, escaping strings is no longer needed when `columName` is set. + +NOTE: This alternative behaviour may lead to unexpected changes, mainly if using Neo4j 5.x, where subqueries need to be _aliased_. + == Usage examples [[type-definitions-cypher-object-usage]] @@ -175,8 +224,7 @@ type Movie { MATCH (this)<-[:ACTED_IN]-(:Actor)-[:ACTED_IN]->(rec:Movie) WITH rec, COUNT(*) AS score ORDER BY score DESC RETURN rec LIMIT $limit - """, - columnName: "rec" + """ ) } ---- @@ -198,8 +246,7 @@ type Query { statement: """ MATCH (a:Actor) RETURN a - """, - columnName: "a" + """ ) } ---- @@ -221,8 +268,7 @@ type Mutation { statement: """ CREATE (a:Actor {name: $name}) RETURN a - """, - columnName: "a" + """ ) } ----- \ No newline at end of file +---- diff --git a/modules/ROOT/pages/type-definitions/database-mapping.adoc b/modules/ROOT/pages/type-definitions/database-mapping.adoc new file mode 100644 index 00000000..4cdf94d2 --- /dev/null +++ b/modules/ROOT/pages/type-definitions/database-mapping.adoc @@ -0,0 +1,338 @@ +[[type-definitions-database-mapping]] += Database Mapping + +[[type-definitions-alias]] +== `@alias` + +This directive maps a GraphQL field to a Neo4j property on a node or relationship. + +This can be used on any fields that are not `@cypher` or `@relationship` fields. + +=== Definition + +[source, graphql, indent=0] +---- +"""Indicates that the field is to be mapped to the underlying Neo4j under a different property name.""" +directive @alias(property: String!) on FIELD_DEFINITION +---- + +=== Usage + +[source, graphql, indent=0] +---- +type User { + id: ID! @id @alias(property: "dbId") + username: String! +} +---- + +[source, graphql, indent=0] +---- +type User { + id: ID! @id + username: String! @alias(property: "dbUserName") + livesIn: [City!]! @relationship(direction: OUT, type: "LIVES_IN", properties: "UserLivesInProperties") +} + +type City { + name: String +} + +interface UserLivesInProperties @relationshipProperties { + since: DateTime @alias(property: "moveInDate") +} +---- + +[[type-definitions-plural]] +== `@plural` + +The `@plural` directive redefines how to compose the plural of the type for the generated operations. +This is particularly useful for types that are not correctly pluralized or are non-English words. + +[source, graphql, indent=0] +---- +type Tech @plural(value: "Techs") { + name: String +} +---- + +This way, instead of the wrongly generated `teches`, the type is properly written as `techs`: + +[source, graphql, indent=0] +---- +{ + techs { + title + } +} +---- + +The same is applied to other operations such as `createTechs`. Note that database labels will not change. + +[[type-definitions-node]] +== `@node` + +NOTE: The plural argument has been deprecated and will be removed in version 4.0. +Please use the xref::type-definitions/database-mapping.adoc#type-definitions-plural[`@plural` directive] instead. + +NOTE: The label and additionalLabels arguments of the `@node` directive have been deprecated and will be removed in version 4.0. +Please use the xref::type-definitions/database-mapping.adoc#_labels[`labels argument`] instead. + +The `@node` directive is used to specify the configuration of a GraphQL object type which represents a Neo4j node. + +=== Definition + +[source, graphql, indent=0] +---- +"""Informs @neo4j/graphql of node metadata""" +directive @node( + """Map the GraphQL type to match Neo4j node labels""" + labels: [String] + """Map the GraphQL type to a custom Neo4j node label""" + label: String + """Map the GraphQL type to match additional Neo4j node labels""" + additionalLabels: [String] + """Allows for the specification of the plural of the type name.""" + plural: String +) on OBJECT +---- + +=== Usage +`@node` can be used with the following optional parameters. + +==== `labels` +The parameter `labels` defines the list of label to be used in Neo4j instead of the GraphQL type name: + +[source, graphql, indent=0] +---- +type Dog @node(labels: ["K9"]) { + name: String! +} +---- + +This way, the following query: + +[source, graphql, indent=0] +---- +{ + dogs { + name + } +} +---- + +Generates the cypher query: + +[source, cypher, indent=0] +---- +MATCH (this: K9) +RETURN this { .name } as name +---- + +If the GraphQL type name should still be used as a label, it needs to be specified as well: + +[source, graphql, indent=0] +---- +type Dog @node(labels: ["Dog", "K9"]) { + name: String! +} +---- + +This way, the following query: + +[source, graphql, indent=0] +---- +{ + dogs { + name + } +} +---- + +Generates the cypher query: + +[source, cypher, indent=0] +---- +MATCH (this:Dog:K9) +RETURN this { .name } as this +---- + +Please note that defining `labels` means you take control of the database labels of the node. Indexes and constraints in Neo4j only support a single label, for which the first element of the `labels` argument will be used. + +The following example results in a unique constraint be asserted for the label `K9` and the property `name`: + +[source, graphql, indent=0] +---- +type Dog @node(labels: ["K9", "Dog"]) { + name: String! @unique +} +---- + +==== `label` + +NOTE: The label and additionalLabels arguments of the `@node` directive have been deprecated and will be removed in version 4.0. +Please use the xref::type-definitions/database-mapping.adoc#_labels[`labels argument`] instead. + +The parameter `label` defines the label to be used in Neo4j instead of the GraphQL type name: + +[source, graphql, indent=0] +---- +type Movie @node(label: "Film") { + title: String! +} +---- + +This way, the following query: + +[source, graphql, indent=0] +---- +{ + movies { + title + } +} +---- + +Generates the cypher query: + +[source, cypher, indent=0] +---- +MATCH (this: Film) +RETURN this { .title } as this +---- + +===== Using `$jwt` and `$context` +In some cases, we may want to generate dynamic labels depending on the user requesting. In these cases, we can use the variable `$jwt` to define a custom label define in the JWT (similarly to how it is used in the xref::auth/index.adoc[`@auth` directive]): + +[source, graphql, indent=0] +---- +type User @node(label: "$jwt.username") { + name: String! +} +---- + +The following query will yield a different cypher query depending on the user JWT: + +[source, graphql, indent=0] +---- +{ + users { + name + } +} +---- + +Assuming an user with the value `"username": "arthur"` in JWT, the Cypher query will look like: + +[source, cypher, indent=0] +---- +MATCH (this:arthur) +RETURN this { .name } as this +---- + +Similarly, context values can be passed directly: + +[source, graphql, indent=0] +---- +type User @node(label: "$context.appId") { + name: String! +} +---- + +When running the server with Apollo: + +[source, js, indent=0] +---- +const server = new ApolloServer({ + schema: await neoSchema.getSchema(), +}); + +await startStandaloneServer(server, { + context: async ({ req }) => ({ req, appId: "myApp" }), +}); +---- + +==== `additionalLabels` + +NOTE: The label and additionalLabels arguments of the `@node` directive have been deprecated and will be removed in version 4.0. +Please use the xref::type-definitions/database-mapping.adoc#_labels[`labels argument`] instead. + +`additionalLabels` lets you define extra Neo4j labels that need to exist on the node for that GraphQL type. + +[source, graphql, indent=0] +---- +type Actor @node(additionalLabels: ["Person", "User"]) { + name: String! +} +---- + +The following query: + +[source, graphql, indent=0] +---- +{ + Actor { + name + } +} +---- + +Generates the following cypher query, with the labels `Actor`, `Person` and `User`: + +[source, cypher, indent=0] +---- +MATCH (this:Actor:Person:User) +RETURN this { .name } as this +---- + +Note that `additionalLabels` can be used along with `label`: + +[source, graphql, indent=0] +---- +type Actor @node(label: "ActorDB", additionalLabels: ["Person"]) { + name: String! +} +---- + +In this case, the resulting Cypher query will use the labels `ActorDB` and `Person` instead of `Actor`: + +---- +MATCH (this:ActorDB:Person) +RETURN this { .name } as this +---- +<<#_using_jwt_and_context,Context and JWT variables>> can be used with `additionalLabels` in the same fashion as in `label`: + +[source, graphql, indent=0] +---- +type User @node(additionalLabels: ["$jwt.username"]) { + name: String! +} +---- + +==== `plural` + +NOTE: The plural argument has been deprecated and will be removed in version 4.0. +Please use the xref::type-definitions/database-mapping.adoc#type-definitions-plural[`@plural` directive] instead. + +The parameter `plural` redefines how to compose the plural of the type for the generated operations. This is particularly +useful for types that are not correctly pluralized or are non-English words. + +[source, graphql, indent=0] +---- +type Tech @node(plural: "Techs") { + name: String +} +---- + +This way, instead of the wrongly generated `teches`, the type is properly written as `techs`: + +[source, graphql, indent=0] +---- +{ + techs { + title + } +} +---- + +The same is applied to other operations such as `createTechs`. Note that database labels will not change. diff --git a/modules/ROOT/pages/reference/directives/default-values.adoc b/modules/ROOT/pages/type-definitions/default-values.adoc similarity index 86% rename from modules/ROOT/pages/reference/directives/default-values.adoc rename to modules/ROOT/pages/type-definitions/default-values.adoc index 97c72d29..2620c009 100644 --- a/modules/ROOT/pages/reference/directives/default-values.adoc +++ b/modules/ROOT/pages/type-definitions/default-values.adoc @@ -74,23 +74,27 @@ type Movie { } ---- -[[type-definitions-default-values-limit]] -== `@limit` +[[type-definitions-default-values-queryoptions]] +== `@queryOptions` -The `@limit` is to be used on nodes, where applied will inject values into a query such as the `limit`. +The `@queryOptions` is to be used on nodes, where applied will inject values into a query such as the `limit`. === Definition [source, graphql, indent=0] ---- -"""The `@limit` is to be used on nodes, where applied will inject values into a query such as the `limit`.""" -directive @limit( - default: Int - max: Int +"""The `@queryOptions` is to be used on nodes, where applied will inject values into a query such as the `limit`.""" +directive @queryOptions( + """If no limit argument is supplied on query will fallback to this value.""" + limit: { + default: Int + max: Int + } ) on OBJECT ---- -The directive has two arguments: +=== Limit +Limit has 2 arguments: * `default` - If no `limit` argument is passed to the query, the default limit will be used. The query may still pass a higher or lower `limit`. -* `max` - Defines the maximum limit to be passed to the query. If a higher value is passed, this will be used instead. If no `default` value is set, `max` will be used for queries without limit. \ No newline at end of file +* `max` - Defines the maximum limit to be passed to the query. If a higher value is passed, this will be used instead. If no `default` value is set, `max` will be used for queries without limit. diff --git a/modules/ROOT/pages/type-definitions/directives/basics.adoc b/modules/ROOT/pages/type-definitions/directives/basics.adoc deleted file mode 100644 index 51381a44..00000000 --- a/modules/ROOT/pages/type-definitions/directives/basics.adoc +++ /dev/null @@ -1,67 +0,0 @@ -[[type-definitions-basics]] -:description: This page describes basic notions about how to use directives with the Neo4j GraphQL Library. -= Basics - -Each type in your GraphQL type definitions can be mapped to an entity in your Neo4j database, such as nodes, relationships, and relationship properties. -This page describes how that can be done. - -== Nodes - -The most basic mapping, it uses GraphQL type names to map to the Neo4j node label. -For example, to represent a node with label "Movie" and a single property "title" of type string: - -[source, graphql, indent=0] ----- -type Movie { - title: String -} ----- - -== Relationships - -Relationships are represented by marking particular fields with a directive -- in this case, `@relationship`. -It defines the relationship type in the database, as well as which direction that relationship goes in. - -To add a second node type, "Actor", and connect the two together, you should do the following: - -[source, graphql, indent=0] ----- -type Movie { - title: String - actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN) -} - -type Actor { - name: String - movies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT) -} ----- - -Note that, in this case, there is a directive on each "end" of the relationship, but it is not essential. - -=== Relationship properties - -In order to add properties to a relationship, you need to add a new type to your type definitions. -This time, however, it should be an interface which must be decorated with the `@relationshipProperties` directive. - -For example, for the "ACTED_IN" relationship, add a property "roles": - -[source, graphql, indent=0] ----- -type Movie { - title: String - actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") -} - -type Actor { - name: String - movies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") -} - -interface ActedIn @relationshipProperties { - roles: [String] -} ----- - -Note that in addition to this interface type, there is an added a key `properties` in the existing `@relationship` directives. -For more information, see xref::/type-definitions/types/relationships.adoc[Type definitions -> Relationships]. diff --git a/modules/ROOT/pages/type-definitions/directives/cypher.adoc b/modules/ROOT/pages/type-definitions/directives/cypher.adoc deleted file mode 100644 index 67cafe7a..00000000 --- a/modules/ROOT/pages/type-definitions/directives/cypher.adoc +++ /dev/null @@ -1,236 +0,0 @@ -[[type-definitions-cypher]] -:description: This page describes how to use the @cypher directive in the Neo4j GraphQL Library. -= `@cypher` - -The `@cypher` directive binds a GraphQL field to the results of a Cypher query. -This directive can be used both for properties in a type or as top level queries. - -== Global variables - -Global variables are available for use within the Cypher statement and can be applied to the `@cypher` directive. - -[cols="1,2,2"] -|=== -| Variable | Description | Example - -| `this` -| This value is a reference to the currently resolved node, and it can be used to traverse the graph. -a| -[source, graphql, indent=0] ----- -{ - Movie { - title - actors: ACTED_IN @this { - role - actor { - name - } - } - directors: DIRECTED @this { - director { - name - } - } - } -} ----- - -| `auth` -a| This value is represented by the following TypeScript interface definition: -[source, typescript, indent=0] ----- -interface Auth { - isAuthenticated: boolean; - roles?: string[]; - jwt: any; -} ----- -a| You can use the JWT in the request to return the value of the currently logged in User: -[source, graphql, indent=0] ----- -type User { - id: String -} - -type Query { - me: User @cypher( - statement: """ - MATCH (user:User {id: $jwt.sub}) - RETURN user - """, - columnName: "user" - ) -} ----- - -| `cypherParams` -| Use it to inject values into the Cypher query from the GraphQL context function. -a| Inject into context: -[source, typescript, indent=0] ----- -const server = new ApolloServer({ - typeDefs, -}); - -await startStandaloneServer(server, { - context: async ({ req }) => ({ cypherParams: { userId: "user-id-01" } }), -}); ----- - -Use in Cypher query: - -[source, graphql, indent=0] ----- -type Query { - userPosts: [Post] @cypher(statement: """ - MATCH (:User {id: $userId})-[:POSTED]->(p:Post) - RETURN p - """, columnName: "p") -} ----- -|=== - - -== Return values - -The return value of Cypher statements must always be of the same type to which the directive is applied. - -The variable must also be aliased with a name that is the same as the one passed to `columnName`. -This can be the name of a node, relationship query or an alias in the `RETURN` statement of the Cypher statement. - -=== Scalar values - -Cypher statements must return a value which matches the scalar type to which the directive was applied. -For example: - -[source, graphql, indent=0] ----- -type Query { - randomNumber: Int @cypher(statement: "RETURN rand() as result", columnName: "result") -} ----- - -=== Object types - -When returning an object type, all fields of the type must be available in the Cypher return value. -This can be achieved by either returning the entire object from the Cypher query, or returning a map of the fields which are required for the object type. -Both approaches are demonstrated below: - -[source, graphql, indent=0] ----- -type User { - id -} - -type Query { - users: [User] - @cypher( - statement: """ - MATCH (u:User) - RETURN u - """, - columnName: "u" - ) -} ----- - -[source, graphql, indent=0] ----- -type User { - id -} - -type Query { - users: [User] @cypher(statement: """ - MATCH (u:User) - RETURN { - id: u.id - } as result - """, columnName: "result") -} ----- - -The downside of the latter approach is that you need to adjust the return object as you change your object type definition. - -== Usage examples - -The `@cypher` directive can be used in different contexts, such as the ones described in this section. - -[[type-definitions-cypher-object-usage]] -=== On an object type field - -In the example below, a field `similarMovies` is bound to the `Movie` type, to find other movies with an overlap of actors: - -[source, graphql, indent=0] ----- -type Actor { - actorId: ID! - name: String - movies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT) -} - -type Movie { - movieId: ID! - title: String - description: String - year: Int - actors(limit: Int = 10): [Actor!]! - @relationship(type: "ACTED_IN", direction: IN) - similarMovies(limit: Int = 10): [Movie] - @cypher( - statement: """ - MATCH (this)<-[:ACTED_IN]-(:Actor)-[:ACTED_IN]->(rec:Movie) - WITH rec, COUNT(*) AS score ORDER BY score DESC - RETURN rec LIMIT $limit - """, - columnName: "rec" - ) -} ----- - -=== On a query type field - -The following example demonstrates a query to return all of the actors in the database: - -[source, graphql, indent=0] ----- -type Actor { - actorId: ID! - name: String -} - -type Query { - allActors: [Actor] - @cypher( - statement: """ - MATCH (a:Actor) - RETURN a - """, - columnName: "a" - ) -} ----- - -=== On a mutation type field - -The following example demonstrates a mutation using a Cypher query to insert a single actor with the specified name argument: - -[source, graphql, indent=0] ----- -type Actor { - actorId: ID! - name: String -} - -type Mutation { - createActor(name: String!): Actor - @cypher( - statement: """ - CREATE (a:Actor {name: $name}) - RETURN a - """, - columnName: "a" - ) -} ----- \ No newline at end of file diff --git a/modules/ROOT/pages/type-definitions/directives/database-mapping.adoc b/modules/ROOT/pages/type-definitions/directives/database-mapping.adoc deleted file mode 100644 index 78bef11b..00000000 --- a/modules/ROOT/pages/type-definitions/directives/database-mapping.adoc +++ /dev/null @@ -1,202 +0,0 @@ -[[type-definitions-database-mapping]] -:description: This page describes how to use directives for database mapping. -= Database mapping - -This page describes how to use directives for database mapping. - -[[type-definitions-alias]] -== `@alias` - -This directive maps a GraphQL field to a Neo4j property on a node or relationship. -It can be used on any fields that are not `@cypher` or `@relationship` fields. - -For example: - -[source, graphql, indent=0] ----- -type User { - id: ID! @id @alias(property: "dbId") - username: String! -} ----- - -[source, graphql, indent=0] ----- -type User { - id: ID! @id - username: String! @alias(property: "dbUserName") - livesIn: [City!]! @relationship(direction: OUT, type: "LIVES_IN", properties: "UserLivesInProperties") -} - -type City { - name: String -} - -interface UserLivesInProperties @relationshipProperties { - since: DateTime @alias(property: "moveInDate") -} ----- - -Note that the property in aliases are automatically escaped (wrapped with backticks ``), so there is no need to add escape characters around them. - -[[type-definitions-plural]] -== `@plural` - -This directive redefines how to compose the plural of the type for the generated operations. -This is particularly useful for types that are not correctly pluralized or are non-English words. -Take this type definition as an example: - -[source, graphql, indent=0] ----- -type Tech @plural(value: "Techs") { - name: String -} ----- - -This way, instead of the wrongly generated `teches`, the type is properly written as `techs`: - -[source, graphql, indent=0] ----- -{ - techs { - title - } -} ----- - -The same is applied to other operations such as `createTechs`. -However, keep in mind that database labels are not changed with this directive. - -[[type-definitions-node]] -== `@node` - -This directive is used to specify the configuration of a GraphQL object type which represents a Neo4j node. -It can be appended with the following optional parameters: - -[discrete] -=== `labels` - -This parameter defines the list of label to be used in Neo4j instead of the GraphQL type name: - -[source, graphql, indent=0] ----- -type Dog @node(labels: ["K9"]) { - name: String! -} ----- - -This way, the following query: - -[source, graphql, indent=0] ----- -{ - dogs { - name - } -} ----- - -Generates the Cypher query: - -[source, cypher, indent=0] ----- -MATCH (this: K9) -RETURN this { .name } as name ----- - -If the GraphQL type name should still be used as a label, it needs to be specified as well: - -[source, graphql, indent=0] ----- -type Dog @node(labels: ["Dog", "K9"]) { - name: String! -} ----- - -This way, the following query: - -[source, graphql, indent=0] ----- -{ - dogs { - name - } -} ----- - -Generates the Cypher query: - -[source, cypher, indent=0] ----- -MATCH (this:Dog:K9) -RETURN this { .name } as this ----- - -[NOTE] -==== -Defining `labels` means you take control of the database labels of the node. -Indexes and constraints in Neo4j only support a single label, for which the first element of the `labels` argument is used. -==== - -The following example results in a unique constraint to be asserted for the label `K9` and the property `name`: - -[source, graphql, indent=0] ----- -type Dog @node(labels: ["K9", "Dog"]) { - name: String! @unique -} ----- - -[discrete] -=== Using `$jwt` and `$context` - -In some cases, you may want to generate dynamic labels depending on the user requesting. -For that, you can use the variable `$jwt` to define a custom label in the JWT: - -[source, graphql, indent=0] ----- -type User @node(labels: ["$jwt.username"]) { - name: String! -} ----- - -The following query yielda a different Cypher query depending on the user JWT: - -[source, graphql, indent=0] ----- -{ - users { - name - } -} ----- - -Assuming there is a user with the value `"username": "arthur"` in JWT, the Cypher query looks like: - -[source, cypher, indent=0] ----- -MATCH (this:arthur) -RETURN this { .name } as this ----- - -Similarly, context values can be passed directly: - -[source, graphql, indent=0] ----- -type User @node(label: ["$context.appId"]) { - name: String! -} ----- - -When running the server with Apollo: - -[source, js, indent=0] ----- -const server = new ApolloServer({ - schema: await neoSchema.getSchema(), -}); - -await startStandaloneServer(server, { - context: async ({ req }) => ({ req, appId: "myApp" }), -}); ----- diff --git a/modules/ROOT/pages/type-definitions/directives/default-values.adoc b/modules/ROOT/pages/type-definitions/directives/default-values.adoc deleted file mode 100644 index 91b6b072..00000000 --- a/modules/ROOT/pages/type-definitions/directives/default-values.adoc +++ /dev/null @@ -1,117 +0,0 @@ -[[type-definitions-default-values]] -:description: This page describes how to use default values in the Neo4j GraphQL Library. -= Default values - -This page describes how to use default values in the Neo4j GraphQL Library. - -[[type-definitions-default-values-default]] -== `@default` - -When generating the input type for the `create`` mutation, the value specified in this directive is used as the default value for this field. - -=== Definition - -[source, graphql, indent=0] ----- -"""Int | Float | String | Boolean | ID | DateTime | Enum""" -scalar Scalar - -"""Instructs @neo4j/graphql to set the specified value as the default value in the CreateInput type for the object type in which this directive is used.""" -directive @default( - """The default value to use. Must be a scalar type and must match the type of the field with which this directive decorates.""" - value: Scalar!, -) on FIELD_DEFINITION ----- - -=== Usage - -`@default` may be used with enums. -When setting the default value for an enum field, it must be one of the enumerated enum values: - -[source, graphql, indent=0] ----- -enum Location { - HERE - THERE - EVERYWHERE -} - -type SomeType { - firstLocation: Location! @default(value: HERE) # valid usage - secondLocation: Location! @default(value: ELSEWHERE) # invalid usage, will throw an error -} ----- - -[[type-definitions-default-values-coalesce]] -== `@coalesce` - -When translating from GraphQL to Cypher, any instances of fields to which this directive is applied will be wrapped in a `coalesce()` function in the WHERE clause (see https://neo4j.com/developer/kb/understanding-non-existent-properties-and-null-values/#_use_coalesce_to_use_a_default_for_a_null_value). - -This directive helps querying against non-existent properties in a database. -However, it is encouraged to populate these properties with meaningful values if this is becoming the norm. The `@coalesce`directive is a primitive implementation of the function which only takes a static default value as opposed to using another property in a node or a Cypher expression. - -=== Definition - -[source, graphql, indent=0] ----- -"""Int | Float | String | Boolean | ID | DateTime | Enum""" -scalar ScalarOrEnum - -"""Instructs @neo4j/graphql to wrap the property in a coalesce() function during queries, using the single value specified.""" -directive @coalesce( - """The value to use in the coalesce() function. Must be a scalar type and must match the type of the field with which this directive decorates.""" - value: Scalar!, -) on FIELD_DEFINITION ----- - -=== Usage - -`@coalesce` may be used with enums. -When setting the default value for an enum field, it must be one of the enumerated enum values: - -[source, graphql, indent=0] ----- -enum Status { - ACTIVE - INACTIVE -} -type Movie { - status: Status @coalesce(value: ACTIVE) -} ----- - -[[type-definitions-default-values-limit]] -== `@limit` - -Available on nodes, this directive injects values into a query such as the `limit`. - -=== Definition - -[source, graphql, indent=0] ----- -"""The `@limit` is to be used on nodes, where applied will inject values into a query such as the `limit`.""" -directive @limit( - default: Int - max: Int -) on OBJECT ----- - -=== Usage - -The directive has two arguments: - -* `default` - if no `limit` argument is passed to the query, the default limit is used. -The query may still pass a higher or lower `limit`. -* `max` - defines the maximum limit to be passed to the query. -If a higher value is passed, it is used instead. -If no `default` value is set, `max` is used for queries without limit. - -[source, graphql, indent=0] ----- -{ - Movie @limit(amount: 5) { - title - year - } -} ----- diff --git a/modules/ROOT/pages/type-definitions/directives/index.adoc b/modules/ROOT/pages/type-definitions/directives/index.adoc deleted file mode 100644 index 9eb167ff..00000000 --- a/modules/ROOT/pages/type-definitions/directives/index.adoc +++ /dev/null @@ -1,98 +0,0 @@ -[[directives]] -:description: This page lists all directives available in the Neo4j GraphQL Library. -= Directives - -The Neo4j GraphQL Library provides the following directives to be used whilst defining types: - -[cols="1,3"] -|=== -| Directive | Description - -| xref::/type-definitions/directives/database-mapping.adoc#type-definitions-alias[`@alias`] -| Maps a GraphQL schema field to a Neo4j property on a node or relationship. - -| xref::/authentication-and-authorization/authentication.adoc[`@authentication`] -| Requires authentication checks when accessing the type. - -| xref::/authentication-and-authorization/authorization.adoc[`@authorization`] -| Specifies authorization rules for queries and mutations on the type. - -| xref::/type-definitions/directives/default-values.adoc#type-definitions-default-values-coalesce[`@coalesce`] -| Exposes a mechanism for querying against non-existent, `null` values on a node. - -| xref::custom-resolvers.adoc#custom-resolver-directive[`@customResolver`] -| Specifies that a field is resolved by a custom resolver, and allows the specification -of any required fields that is passed as arguments to the custom resolver. - -| xref::/type-definitions/directives/cypher.adoc[`@cypher`] -| Overrides field resolution (including `Query` and `Mutation` fields), instead resolving with the specified Cypher. - -| xref::/type-definitions/directives/default-values.adoc#type-definitions-default-values-default[`@default`] -| Allows for the setting of a default value for a field on object creation. - -| xref:/schema-configuration/field-configuration.adoc#_filterable[`@filterable`] -| Defines the filters generated for a field. - -| xref::/type-definitions/directives/indexes-and-constraints.adoc#type-definitions-indexes-fulltext[`@fulltext`] -| Indicates that there should be a fulltext index inserted into the database for the specified Node and its properties. - -| xref::/type-definitions/directives/autogeneration.adoc#type-definitions-autogeneration-id[`@id`] -| Marks a field as the unique ID for an object type, and allows for autogeneration of IDs. - -| xref::/type-definitions/directives/default-values.adoc#type-definitions-default-values-limit[`@limit`] -| Used on nodes to inject values into Cypher `LIMIT` clauses. - -| xref:/schema-configuration/type-configuration.adoc#_mutation[`@mutation`] -| Limits the availability of Mutation operations in the library. - -| xref::/type-definitions/directives/database-mapping.adoc#type-definitions-node[`@node`] -| Specifies the configuration of a GraphQL object type which represents a Neo4j node. - -| xref::/type-definitions/directives/database-mapping.adoc#type-definitions-plural[`@plural`] -| Redefines how to compose the plural of the type for the generated operations. -This is particularly useful for types that are not correctly pluralized or are non-English words. - -| xref::/type-definitions/directives/autogeneration.adoc#type-definitions-autogeneration-populated-by[`@populatedBy`] -| Specifies a callback function that gets executed during GraphQL query parsing, -to populate fields which have not been provided within the input. - -| xref::ogm/private.adoc[`@private`] -| Protects fields which should only be available through the xref::ogm/index.adoc[OGM]. - -| xref:/schema-configuration/type-configuration.adoc#_query[`@query`] -| Limits the availability of Query operations in the library. - -| xref::/schema-configuration/field-configuration.adoc#_relationship[`@relationship`] -| Configure xref::/type-definitions/types/relationships.adoc[relationships] between object types. - -| *`@relationshipProperties`* -a| Required to help you distinguish between interfaces which are used for relationship properties, and otherwise. -Can only be used on interfaces, as per its definition: -[source, graphql, indent=0] ----- -"""Required to differentiate between interfaces for relationship properties, and otherwise.""" -directive @relationshipProperties on INTERFACE ----- - -| `@relayId` -| Specifies that the field should be used as the global node identifier for Relay. - -| xref:/schema-configuration/field-configuration.adoc#_selectable[`@selectable`] -| Sets the availability of fields on queries and aggregations. - -| xref:/schema-configuration/field-configuration.adoc#_settable[`@settable`] -| Sets the availability of fields on the create and update inputs. - -| xref:/schema-configuration/type-configuration.adoc#_subscription[`@subscription`] -| Limits subscription operations in the library. - -| `@subscriptionsAuthorization` -| Specifies authorization rules for subscriptions on the type. - -| xref::/type-definitions/directives/autogeneration.adoc#type-definitions-autogeneration-timestamp[`@timestamp`] -| Flags fields to be used to store timestamps on create/update events. - -| xref::/type-definitions/directives/indexes-and-constraints.adoc#type-definitions-constraints-unique[`@unique`] -| Indicates that there should be a uniqueness constraint in the database for the fields that it is applied to. - -|=== \ No newline at end of file diff --git a/modules/ROOT/pages/type-definitions/index.adoc b/modules/ROOT/pages/type-definitions/index.adoc new file mode 100644 index 00000000..fe26e53f --- /dev/null +++ b/modules/ROOT/pages/type-definitions/index.adoc @@ -0,0 +1,15 @@ +[[type-definitions]] += Type Definitions + +- xref::type-definitions/basics.adoc[Basics] - Learn how to define your nodes and relationships using GraphQL type definitions. +- xref::type-definitions/types.adoc[Types] - Learn about the various data types available in the Neo4j GraphQL Library. +- xref::type-definitions/unions.adoc[Unions] - Learn about GraphQL unions and how they map to the Neo4j database. +- xref::type-definitions/interfaces.adoc[Interfaces] - Learn about GraphQL interfaces and how they map to the Neo4j database. +- xref::type-definitions/relationships.adoc[Relationships] - Learn more about defining relationships using the Neo4j GraphQL Library. +- xref::type-definitions/schema-configuration/index.adoc[Schema Configuration] - Learn about how to restrict access to certain types or fields. +- xref::type-definitions/autogeneration.adoc[Autogeneration] - Learn about certain types which you can enable autogeneration of values for. +- xref::type-definitions/cypher.adoc[`@cypher` directive] - Learn about how to add custom Cypher to your type definitions. +- xref::type-definitions/default-values.adoc[Default Values] - Learn about different ways of setting default values for particular fields. +- xref::type-definitions/database-mapping.adoc[Database Mapping] - Learn how to map the GraphQL Schema fields to custom Neo4j node and relationship properties. +- xref::type-definitions/indexes-and-constraints.adoc[Indexes and Constraints] - Learn how to use schema directives to add indexes and constraints to your Neo4j database. +- xref::introspector.adoc[Infer GraphQL Type Definitions] - If you have an existing database, you can learn how to automatically generate the type definition file from that. diff --git a/modules/ROOT/pages/type-definitions/directives/indexes-and-constraints.adoc b/modules/ROOT/pages/type-definitions/indexes-and-constraints.adoc similarity index 59% rename from modules/ROOT/pages/type-definitions/directives/indexes-and-constraints.adoc rename to modules/ROOT/pages/type-definitions/indexes-and-constraints.adoc index 754d75c3..f465de9d 100644 --- a/modules/ROOT/pages/type-definitions/directives/indexes-and-constraints.adoc +++ b/modules/ROOT/pages/type-definitions/indexes-and-constraints.adoc @@ -1,9 +1,7 @@ [[type-definitions-indexes-and-constraints]] -:description: This page describes how to use indexes and constraints in the Neo4j GraphQL Library. -= Indexes and constraints - -This page describes how to use indexes and constraints in the Neo4j GraphQL Library. += Indexes and Constraints +[[type-definitions-constraints-unique]] == Unique node property constraints Unique node property constraints map to `@unique` directives used in your type definitions, which has the following definition: @@ -17,16 +15,15 @@ directive @unique( ) on FIELD_DEFINITION ---- -Additionally, the usage of the xref::/type-definitions/directives/autogeneration.adoc#type-definitions-autogeneration-id[`@id`] directive by default implies that there should be a unique node property constraint in the database for that property. +Additionally, the usage of the xref::type-definitions/autogeneration.adoc#type-definitions-autogeneration-id[`@id`] directive by default implies that there should be a unique node property constraint in the database for that property. -Using this directive does not automatically ensure the existence of these constraints, and you will need to run a function on server startup. -See the section xref::/type-definitions/directives/indexes-and-constraints.adoc#_asserting_constraints[Asserting constraints] for details. +Using this directive does not automatically ensure the existence of these constraints, and you will need to run a function on server startup. See the section xref::type-definitions/indexes-and-constraints.adoc#type-definitions-indexes-and-constraints-asserting[Asserting constraints] below for details. -=== Usage +=== `@unique` directive usage -`@unique` directives can only be used in GraphQL object types representing nodes, and they are only applicable for the "main" label for the node. +`@unique` directives can only be used in GraphQL object types representing nodes, and will only be applied for the "main" label for the node. You can find some examples below. -In the following example, a unique constraint is asserted for the label `Colour` and the property `hexadecimal`: +In the following example, a unique constraint will be asserted for the label `Colour` and the property `hexadecimal`: [source, graphql, indent=0] ---- @@ -35,7 +32,7 @@ type Colour { } ---- -In the next example, a unique constraint with name `unique_colour` is asserted for the label `Colour` and the property `hexadecimal`: +In the next example, a unique constraint with name `unique_colour` will be asserted for the label `Colour` and the property `hexadecimal`: [source, graphql, indent=0] ---- @@ -44,7 +41,7 @@ type Colour { } ---- -The `@node` directive is used to change the database label mapping in this next example, so a unique constraint is asserted for the first label in the list, `Color`, and the property `hexadecimal`: +The `@node` directive is used to change the database label mapping in this next example, so a unique constraint will be asserted for the first label in the list, `Color`, and the property `hexadecimal`: [source, graphql, indent=0] ---- @@ -54,7 +51,7 @@ type Colour @node(labels: ["Color"]) { ---- In the following example, all labels specified in the `labels` argument of the `@node` directive are also checked when asserting constraints. -If there is a unique constraint specified for the `hexadecimal` property of nodes with the `Hue` label, but not the `Color` label, no error is thrown and no new constraints are created when running `assertIndexesAndConstraints`. +If there is a unique constraint specified for the `hexadecimal` property of nodes with the `Hue` label, but not the `Color` label, no error will be thrown and no new constraints created when running `assertIndexesAndConstraints`. [source, graphql, indent=0] ---- @@ -63,10 +60,10 @@ type Colour @node(labels: ["Color", "Hue"]) { } ---- +[[type-definitions-indexes-fulltext]] == Fulltext indexes You can use the `@fulltext` directive to add a https://neo4j.com/docs/cypher-manual/current/indexes-for-full-text-search/[Full text index] inside Neo4j. -For example: [source, graphql, indent=0] ---- @@ -74,6 +71,7 @@ input FullTextInput { indexName: String queryName: String fields: [String]! + name: String # Deprecated and will be removed in version 4.0. of the library. Use indexName instead. } """ @@ -82,14 +80,11 @@ Informs @neo4j/graphql that there should be a fulltext index in the database, al directive @fulltext(indexes: [FullTextInput]!) on OBJECT ---- -Using this directive does not automatically ensure the existence of these indexes. -You need to run a function on server startup. -See the section xref::/type-definitions/directives/indexes-and-constraints.adoc#_asserting_constraints[Asserting constraints] for details. +Using this directive does not automatically ensure the existence of these indexes, and you will need to run a function on server startup. See the section xref::type-definitions/indexes-and-constraints.adoc#type-definitions-indexes-and-constraints-asserting[Asserting constraints] below for details. -=== Specifying +=== Specifying the `@fulltext` directive -The `@fulltext` directive can be used on nodes. -In this example, a `Fulltext` index called "ProductName", for the name `field`, on the `Product` node, is added: +The directive can be used on nodes. Here we add a Fulltext index, called 'ProductName', for the name field, on the Product node. [source, graphql, indent=0] ---- @@ -99,17 +94,17 @@ type Product @fulltext(indexes: [{ indexName: "ProductName", fields: ["name"] }] } ---- -When you run xref::/type-definitions/directives/indexes-and-constraints.adoc#_asserting_constraints[Asserting constraints], they create the index like so: +When you run xref::type-definitions/indexes-and-constraints.adoc#type-definitions-indexes-and-constraints-asserting[Asserting constraints] this shall do the index creation like so: [source, cypher, indent=0] ---- CALL db.index.fulltext.createNodeIndex("ProductName", ["Product"], ["name"]) ---- -=== Usage +=== Using the `@fulltext` directive -For every index specified, a new top level query is generated by the library. -For example, for the previous type definitions, the following query and types are generated: +For every index specified, a new top level query will be generated by the library. For example, for the type definitions above, +the following query and types would be generated: [source, graphql, indent=0] ---- @@ -143,8 +138,7 @@ input FloatWhere { } ---- -This query can then be used to perform a https://lucene.apache.org/[Lucene full-text query] to match and return products. -Here is an example of this: +This query can then be used to perform a https://lucene.apache.org/[Lucene full-text query] to match and return products. Below is an example of this: [source, graphql, indent=0] ---- @@ -158,7 +152,7 @@ query { } ---- -This query produces results in the following format: +The above query would produce results in the following format: [source, json, indent=0] ---- @@ -188,7 +182,7 @@ This query produces results in the following format: } ---- -Additionally, it is possible to define a custom query name as part of the `@fulltext` directive by using the `queryName` argument: +Additionally, it is possible to define a custom query name as part of the `@fulltext` directive, using the `queryName` argument as shown below: [source, graphql, indent=0] ---- @@ -198,7 +192,7 @@ type Product @fulltext(indexes: [{ queryName: "CustomProductFulltextQuery", inde } ---- -This produces the following top-level query: +This would then produce the following top-level query: [source, graphql, indent=0] ---- @@ -208,7 +202,7 @@ type Query { } ---- -This query can then be used like this: +This query can then be used as shown below: [source, graphql, indent=0] ---- @@ -222,11 +216,35 @@ query { } ---- +==== Deprecated usage + + +NOTE: Querying full-text indexes in the following ways has been deprecated and will be removed in version 4.0. + +Once you specify the index, you will now gain a 'Top Level' `fulltext` key on the following operations: + +1. read +2. count +3. aggregate + +Here we use the `fulltext` key, and the phrase is using https://lucene.apache.org/[Lucene’s full-text query language] to match and return Products: + +[source, graphql, indent=0] +---- +query { + products(fulltext: { ProductName: { phrase: "beer OR cerveza" } }) { + name + } +} +---- + +> Note that you can only query one Fulltext index at once and that the fulltext key is only available on 'Top Level' queries. + + +[[type-definitions-indexes-and-constraints-asserting]] == Asserting constraints -In order to ensure that the specified constraints exist in the database, you need to run the function `assertIndexesAndConstraints` (see more details in xref::reference/api-reference/neo4jgraphql.adoc#api-reference-assertconstraints[API reference]). -A simple example to create the necessary constraints might look like the following, assuming a valid driver instance in the variable `driver`. -This creates two constraints, one for each field decorated with `@id` and `@unique`, and apply the indexes specified in `@fulltext`: +In order to ensure that the specified constraints exist in the database, you will need to run the function `assertIndexesAndConstraints`, the full details of which can be found in the xref::api-reference/neo4jgraphql.adoc#api-reference-assertconstraints[API reference]. A simple example to create the necessary constraints might look like the following, assuming a valid driver instance in the variable `driver`. This will create two constraints, one for each field decorated with `@id`, `@unique` and apply the indexes specified in `@fulltext`: [source, javascript, indent=0] ---- diff --git a/modules/ROOT/pages/type-definitions/types/interfaces.adoc b/modules/ROOT/pages/type-definitions/interfaces.adoc similarity index 60% rename from modules/ROOT/pages/type-definitions/types/interfaces.adoc rename to modules/ROOT/pages/type-definitions/interfaces.adoc index 299820e1..de4c8174 100644 --- a/modules/ROOT/pages/type-definitions/types/interfaces.adoc +++ b/modules/ROOT/pages/type-definitions/interfaces.adoc @@ -1,12 +1,11 @@ [[type-definitions-interfaces]] -:description: This page describes how to use and define interfaces on relationship fields. -= Interface types += Interface Types -This page describes how to use and define interfaces on relationship fields. +The Neo4j GraphQL Library supports the use of interfaces on relationship fields. This chapter will walk through their definition and usage. -== Creating an interface field +== Type definitions -The following schema defines a `Actor` type, that has a relationship `ACTED_IN`, of type `[Production!]!`. `Production` is an interface type with `Movie` and `Series` implementations. Note in this example that relationship properties have also been used with the `@relationshipProperties` directive, so that interfaces representing relationship properties can be easily distinguished. +The following schema defines a `Actor` type, that has a relationship `ACTED_IN`, of type `[Production!]!`. `Production` is an interface type with `Movie` and `Series` implementations. Note in this example that relationship properties have also been used with the `@relationshipProperties` directive as syntactic sugar, so that interfaces representing relationship properties can be easily distinguished. [source, graphql, indent=0] ---- @@ -72,6 +71,48 @@ type Actor { } ---- +==== Overriding + +In addition to inheritance, directives can be overridden on a per-implementation basis. Say you had an interface defining some `Content`, with some basic authorization rules: + +[source, graphql, indent=0] +---- +interface Content + @auth(rules: [{ operations: [CREATE, UPDATE, DELETE], allow: { author: { username: "$jwt.sub" } } }]) { + title: String! + author: [Author!]! @relationship(type: "HAS_CONTENT", direction: IN) +} + +type User { + username: String! + content: [Content!]! @relationship(type: "HAS_CONTENT", direction: OUT) +} +---- + +You might implement this once for public content and once for private content which has additional rules in place: + +[source, graphql, indent=0] +---- +type PublicContent implements Content { + title: String! + author: [Author!]! +} + +type PrivateContent implements Content + @auth(rules: [{ operations: [CREATE, READ, UPDATE, DELETE], allow: { author: { username: "$jwt.sub" } } }]) { + title: String! + author: [Author!]! +} +---- + +The `PublicContent` type will inherit the auth rules from the `Content` interface, whilst the `PrivateContent` type will use the auth rules specified there. + +In summary, there are three choices for the application of directives when using interfaces: + +- Directives specified on the interface and inherited by all implementing types when the directives for every type are the same. +- Directives specified on the interface and overridden by certain implementing types when directives are broadly the same with a few discrepancies. +- Directives specified on implementing types alone when there is very little commonality between types, or certain types need a directive and others don't. + [[type-definitions-interfaced-types-querying]] == Querying an interface @@ -114,7 +155,7 @@ query GetMoviesStartingWithThe { } ---- -This is to prevent overfetching, and you can find an explanation of this xref::troubleshooting.adoc#appendix-preventing-overfetching[here]. +This is to prevent overfetching, and you can find an explanation of this xref::appendix/preventing-overfetching.adoc[here]. Alternatively, these implementation specific filters can be used to override filtering for a specific implementation. For example, if you wanted all productions with title starting with "The ", but movies with title starting with "A ", you could achieve this using the following: @@ -199,8 +240,7 @@ mutation CreateActorAndProductions { == Nested interface operations -Operations on interfaces are abstract until you instruct them not to be. -Take the following example: +Operations on interfaces are abstract until you instruct them not to be. Take the following example: [source, graphql, indent=0] ---- @@ -224,14 +264,13 @@ mutation CreateActorAndProductions { } ---- -The above mutation: +The above mutation will: -. Finds any `Actor` nodes with the name "Woody Harrelson". -. Connects the "Woody Harrelson" node to a `Production` node with the title "Zombieland". -. Connects the connected `Production` node to any `Actor` nodes with the name "Emma Stone". +1. Find any `Actor` nodes with the name "Woody Harrelson" +2. Connect those nodes to any `Production` (`Movie` or `Series`) nodes with the title "Zombieland" +3. Connect the connected `Production` nodes to any `Actor` nodes with the name "Emma Stone" -This query, however, is fully abstract. -If you want to only connect `Movie` nodes to `Actor` nodes with name "Emma Stone", you could instead do: +As you can see, this is abstract all the way down. If you wanted to only connect `Movie` nodes to `Actor` nodes with name "Emma Stone", you could instead do: [source, graphql, indent=0] ---- @@ -255,7 +294,7 @@ mutation CreateActorAndProductions { } ---- -Alternatively, you can also make sure you only connect to `Movie` nodes with title "Zombieland": +Likewise, you could move this up a level to make sure you only connect to `Movie` nodes with title "Zombieland": [source, graphql, indent=0] ---- @@ -278,179 +317,3 @@ mutation CreateActorAndProductions { } } ---- - -== Directive inheritance - -For the next example, consider the following schema. -It defines an `Actor` type, that has a relationship `ACTED_IN`, of type `[Production!]!`. -`Production` is an interface type with `Movie` and `Series` implementations. -In this example, relationship properties have also been used with the `@relationshipProperties` directive, so that interfaces representing relationship properties can be easily distinguished: - -[source, graphql, indent=0] ----- -interface Production { - title: String! - actors: [Actor!]! -} - -type Movie implements Production { - title: String! - actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") - runtime: Int! -} - -type Series implements Production { - title: String! - actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") - episodes: Int! -} - -interface ActedIn @relationshipProperties { - role: String! -} - -type Actor { - name: String! - actedIn: [Production!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") -} ----- - -Now, considering that any xref::/type-definitions/directives/index.adoc[directives] present on an interface or its fields are "inherited" by any object types implementing it, the example schema could be refactored to have the `@relationship` directive on the `actors` field in the `Production` interface instead of on each implementing type as it is currently. -That is how it would look like: - -[source, graphql, indent=0] ----- -interface Production { - title: String! - actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") -} - -type Movie implements Production { - title: String! - actors: [Actor!]! - runtime: Int! -} - -type Series implements Production { - title: String! - actors: [Actor!]! - episodes: Int! -} - -interface ActedIn @relationshipProperties { - role: String! -} - -type Actor { - name: String! - actedIn: [Production!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") -} ----- - -=== Overriding - -In addition to inheritance, directives can be overridden on a per-implementation basis. -Say you had an interface defining some `Content`, with some basic authorization rules, such as: - -[source, graphql, indent=0] ----- -interface Content - @auth(rules: [{ operations: [CREATE, UPDATE, DELETE], allow: { author: { username: "$jwt.sub" } } }]) { - title: String! - author: [Author!]! @relationship(type: "HAS_CONTENT", direction: IN) -} - -type User { - username: String! - content: [Content!]! @relationship(type: "HAS_CONTENT", direction: OUT) -} ----- - -You might implement this once for public content and once for private content which has additional rules in place: - -[source, graphql, indent=0] ----- -type PublicContent implements Content { - title: String! - author: [Author!]! -} - -type PrivateContent implements Content - @auth(rules: [{ operations: [CREATE, READ, UPDATE, DELETE], allow: { author: { username: "$jwt.sub" } } }]) { - title: String! - author: [Author!]! -} ----- - -The `PublicContent` type inherits the auth rules from the `Content` interface, while the `PrivateContent` type uses the auth rules specified there. - -In summary, there are three choices for the application of directives when using interfaces: - -* Directives specified on the interface and inherited by all implementing types when the directives for every type are the same. -* Directives specified on the interface and overridden by certain implementing types when directives are broadly the same with a few discrepancies. -* Directives specified on implementing types alone when there is very little commonality between types, or certain types need a directive and others don't. - -== Querying an interface - -In order to set which implementations are returned by a query, a filter `where` needs to be applied. -For example, the following query returns all productions (`movies` and `series`) with title starting "The " for every actor: - -[source, graphql, indent=0] ----- -query GetProductionsStartingWithThe { - actors { - name - actedIn(where: { node: { title_STARTS_WITH: "The " } }) { - title - ... on Movie { - runtime - } - ... on Series { - episodes - } - } - } -} ----- - -This query, on the other hand, only returns the movies with title starting with "The" for each actor: - -[source, graphql, indent=0] ----- -query GetMoviesStartingWithThe { - actors { - name - actedIn(where: { node: { _on: { Movie: { title_STARTS_WITH: "The " } } } }) { - title - ... on Movie { - runtime - } - } - } -} ----- - -This approach aims to prevent overfetching. -For more information, read the page xref::troubleshooting.adoc#appendix-preventing-overfetching[Troubleshooting -> Preventing overfetching]. - -Alternatively, these specific filters can also be used to override filtering for a specific implementation. -For example, if you want to fetch all `series` with title starting with "The " and `movies` with title starting with "A ", you can do it like that: - -[source, graphql, indent=0] ----- -query GetProductionsStartingWith { - actors { - name - actedIn(where: { node: { title_STARTS_WITH: "The ", _on: { Movie: { title_STARTS_WITH: "A " } } } }) { - title - ... on Movie { - runtime - } - ... on Series { - episodes - } - } - } -} ----- - diff --git a/modules/ROOT/pages/type-definitions/types/relationships.adoc b/modules/ROOT/pages/type-definitions/relationships.adoc similarity index 57% rename from modules/ROOT/pages/type-definitions/types/relationships.adoc rename to modules/ROOT/pages/type-definitions/relationships.adoc index e28f9214..27411820 100644 --- a/modules/ROOT/pages/type-definitions/types/relationships.adoc +++ b/modules/ROOT/pages/type-definitions/relationships.adoc @@ -1,19 +1,17 @@ [[type-definitions-relationships]] -:description: This page describes how to write type definitions for a simple connected model, inserting data through the schema, and then querying it. = Relationships -Without relationships, your type definitions work rather as a collection of disconnected nodes, with little value. -Adding relationships into your data model gives your data the context that it needs to run complex queries across wide sections of your graph. +Without relationships, your type definitions are simply a collection of disconnected nodes, with little value. Adding relationships into your data model gives your data the context that it needs to run complex queries across wide sections of your graph. This section will run through writing some type definitions for a simple connected model, inserting some data through the schema, and then querying it. -This page describes how to write type definitions for a simple connected model, inserting data through the schema, and then querying it. +== Example graph -== Type definitions - -Take the following graph as an example in which a `Person` type has two different relationship types, which can connect it to a `Movie` type. +The following graph will be used in this example, where a Person type has two different relationship types which can connect it to a Movie type. image::relationships.svg[title="Example graph"] -To create that graph using the Neo4j GraphQL Library, first you need to define the nodes and define the two distinct types in this model: +== Type definitions + +First, to define the nodes, you should define the two distinct types in this model: [source, graphql, indent=0] ---- @@ -47,23 +45,21 @@ type Movie { } ---- -Note that, in this query: +The following should be noted about the fields you just added: -* A `Person` can _act in_ or _direct_ multiple movies, and a `Movie` can have multiple actors. -However, it is rare for a `Movie` to have more than one director, so you can model this cardinality in your type definitions, to ensure accuracy of your data. -* A `Movie` isn't really a `Movie` without a director, and this has been signified by marking the `director` field as non-nullable. -This means that a `Movie` *must* have a `DIRECTED` relationship coming into it to be valid. -* To figure out whether the `direction` argument of the `@relationship` directive should be `IN` or `OUT`, visualize your relationships like in the diagram above, then model out the direction of the arrows. -* The `@relationship` directive is a reference to Neo4j relationships, whereas in the schema, the phrase `edge(s)` is used to be consistent with the general API language used by Relay. +* A Person can act in or direct multiple movies, and a Movie can have multiple actors. However, it is exceedingly rare for a Movie to have more than one director, and you can model this cardinality in your type definitions, to ensure accuracy of your data. +* A Movie isn't really a Movie without a director, and this has been signified by marking the `director` field as non-nullable, meaning that a Movie must have a `DIRECTED` relationship coming into it. +* To figure out whether the `direction` argument of the `@relationship` directive should be `IN` or `OUT`, visualise your relationships like in the diagram above, and model out the direction of the arrows. +* The @relationship directive is a reference to Neo4j relationships, whereas in the schema, the phrase edge(s) is used to be consistent with the general API language used by Relay. -=== Relationship properties +=== Relationship Properties -You can add relationship properties to the example in two steps: +Relationship properties can be added to the above type definitions in two steps: -. Add an interface definition decorated with the `@relationshipProperties` directive and containing the desired relationship properties. -. Add a `properties` argument to both "sides" (or just one side, if you prefer) of the `@relationship` directive which points to the newly defined interface. +1. Add an interface definition containing the desired relationship properties +2. Add a `properties` argument to both "sides" (or just one side, if you prefer) of the `@relationship` directive which points to the newly defined interface -For example, suppose you want to distinguish which roles an actor played in a movie: +For example, to distinguish which roles an actor played in a movie: [source, graphql, indent=0] ---- @@ -86,11 +82,9 @@ interface ActedIn @relationshipProperties { } ---- -=== `queryDirection` - -All relationships have a direction. -However, when querying them, it is possible to perform xref::queries-aggregations/queries.adoc#_undirected_queries[undirected queries]. -To set the default behavior of a relationship when it is queried, you can use the argument `queryDirection`: +=== QueryDirection +All relationships have a direction, however, when querying it is possible to perform xref:queries.adoc#_undirected_queries[undirected queries]. +When defining a relationship you can define the default behaviour when performing queries over this relationship with the argument `queryDirection`: [source, graphql, indent=0] ---- @@ -103,17 +97,14 @@ type Person { `queryDirection` can have the following values: -* `DEFAULT_DIRECTED` (default): all queries are **directed** by default, but users may perform undirected queries. -* `DEFAULT_UNDIRECTED`: all queries are **undirected** by default, but users may perform directed queries. -* `DIRECTED_ONLY`: only directed queries can be performed on this relationship. -* `UNDIRECTED_ONLY`: only undirected queries can be performed on this relationship. +* `DEFAULT_DIRECTED` (default): All queries will be **directed** by default, but users may perform undirected queries. +* `DEFAULT_UNDIRECTED`: All queries will be **undirected** by default, but users may perform directed queries. +* `DIRECTED_ONLY`: Only directed queries can be perform on this relationship. +* `UNDIRECTED_ONLY`: Only undirected queries can be perform on this relationship. == Inserting data -Nested mutations mean that there are many ways in which you can insert data into your database through the GraphQL schema. -Consider the previously mentioned rule that a `Movie` node cannot be created without adding a director. -You can, however, create a director node first and then create and connect it to a `Movie`. -Another option is to create both `Movie` and `Director` in the same mutation, for example: +Nested mutations mean that there are many ways in which you can insert data into your database through the GraphQL schema. You can't create a Movie without adding a director, and you can do that by either creating the director first and then creating and connecting the movie, or you can create both the Movie and the director in the same mutation. With the latter approach: [source, graphql, indent=0] ---- @@ -144,7 +135,7 @@ mutation CreateMovieAndDirector { } ---- -You then need to create the actor in this example, and connect them to the new `Movie` node, also specifying which roles they played: +You then need to create the actor in this example, and connect them to the new Movie node, also specifying which roles they played: [source, graphql, indent=0] ---- @@ -188,8 +179,7 @@ mutation CreateActor { Note the selection of the `actorsConnection` field in order to query the `roles` relationship property. -Also observe that, in the second mutation, the entire graph was returned. -That is not necessary, since you can compress down these mutations into one single operation that inserts all of the data needed: +As you can see, these nested mutations are very powerful, and in the second mutation you ran, you were able to return the entire graph which was created in this example. In fact, these mutations can actually be compressed down into a single mutation which inserts all of the data needed: [source, graphql, indent=0] ---- @@ -242,11 +232,11 @@ mutation CreateMovieDirectorAndActor { } ---- -Acknowledging this helps you create bigger sub-graphs in one mutation at once and, therefore, more efficiently. +Once you get your head around this, you'll be creating giant sub-graphs in one mutation in no time! == Fetching your data -Now that you have the `Movie` information in your database, you can query everything altogether as follows: +Now that you have the Movie information in your database, you can query all of the information which you just inserted as follows: [source, graphql, indent=0] ---- @@ -273,8 +263,7 @@ query { == Cardinality -The Neo4j GraphQL Library has type definition requirements for "many" relationship. -For example: +The Neo4j GraphQL Library has type definition requirements for "many" relationships, so given this example: [source, graphql, indent=0] ---- @@ -288,5 +277,4 @@ type Post { } ---- -The relationship at `User.posts` is considered a "many" relationship, which means it should always be of type `NonNullListType` and `NonNullNamedType`. -In other words, both the array and the type inside of a "many" relationship should have a `!`. \ No newline at end of file +The relationship at `User.posts` is considered a "many" relationship. Relationships such as the one above should always be of type `NonNullListType` and `NonNullNamedType`, meaning both the array and the type inside of it should have a `!`. diff --git a/modules/ROOT/pages/schema-configuration/field-configuration.adoc b/modules/ROOT/pages/type-definitions/schema-configuration/field-configuration.adoc similarity index 53% rename from modules/ROOT/pages/schema-configuration/field-configuration.adoc rename to modules/ROOT/pages/type-definitions/schema-configuration/field-configuration.adoc index ec652924..8deb0ae0 100644 --- a/modules/ROOT/pages/schema-configuration/field-configuration.adoc +++ b/modules/ROOT/pages/type-definitions/schema-configuration/field-configuration.adoc @@ -1,6 +1,5 @@ [[schema-configuration-field-configuration]] -:description: This page describes how to use the directives @selectable, @settable, @filterable and `@relationship` to control how fields are exposed. -= Field configuration += Field Configuration In case you need to remove fields from a GraphQL Object Type or a GraphQL Input Object Type, consider the following type definitions: @@ -18,7 +17,7 @@ type Actor { } ---- -It generates the type `Actor`: +It will generate the type `Actor`: [source, graphql, indent=0] ---- @@ -31,7 +30,7 @@ type Actor { } ---- -By using the directives `@selectable`, `@settable`, `@filterable`, and `@relationship` it is possible to control how these fields are exposed. +By using the directives `@selectable`, `@settable`, and `@relationship` it is possible to control how these fields are exposed. For instance, to hide the field `age` in the Selection Set, you can use the `@selectable` directive: [source, graphql, indent=0] @@ -48,7 +47,7 @@ type Actor { } ---- -Now the type `Actor` looks like this: +Now the type `Actor` will look like this: [source, graphql, indent=0] ---- @@ -62,13 +61,12 @@ type Actor { == `@relationship` -There are several nested operations available for every field created using the `@relationship` directive. These are `create`, `connect`, `disconnect`, `connectOrCreate`, and `delete`. +There are several nested operations available for every field created using the `@relationship` directive. These are `create`, `connect`, `disconnect`, `connectOrCreate`, `delete`. However, these operations are not always needed. The `@relationship` directive allows you to define which operations should be available for a relationship by using the argument `nestedOperations`. -Additionally, the `@relationship` directive causes fields to be added for nested aggregations. -These can be disabled using the `aggregate` argument. +Additionally, the `@relationship` directive causes fields to be added for nested aggregations. These can be disabled using the `aggregate` argument. === Definition @@ -96,12 +94,12 @@ directive @relationship( [NOTE] ==== In version 4.0.0, `aggregate` is `false` as default. -See xref:migration/v4-migration/index.adoc#_relationship_changes[`@relationship changes`] for more information. +See xref::guides/v4-migration/index.adoc#_relationship_changes[`@relationship changes`] for more information. ==== === Usage -*Configure aggregation* +==== Configure aggregation From the previous type definitions, the type `Actor` produced is: @@ -115,8 +113,7 @@ type Actor { } ---- -Note that the relationship field `actedIn` produces the operation field `actedInAggregate`, which allows aggregations on that relationship. -It is possible to configure this behavior by passing the argument aggregate on the `@relationship` directive: +As it's visible, the relationship field `actedIn` produces the operation field `actedInAggregate` which allows aggregations on that relationship. It's possible to configure this behavior by passing the argument aggregate on the `@relationship` directive: [source, graphql, indent=0] ---- @@ -132,7 +129,7 @@ type Actor { } ---- -In this case, as the argument `aggregate` was passed as false, the type `Actor` produced is: +In this case, as we passed the argument `aggregate` as false, the type `Actor` produced is: [source, graphql, indent=0] ---- @@ -144,10 +141,9 @@ type Actor { } ---- -*Configure nested operations* +=== Configure nested operations -A large part of the schema produced by the library is needed to support nested operations. -These are enabled by the directive `@relationship` as described in xref::/type-definitions/types/relationships.adoc#_inserting_data[`@relationship` -> Inserting data]. +A large part of the schema produced by the library is needed to support nested operations. These are enabled by the directive `@relationship` as described in xref:type-definitions/relationships.adoc#_inserting_data[@relationship Inserting Data]. The nested operations available are: @@ -163,10 +159,9 @@ enum NestedOperations { } ---- -By default, `@relationship` enables all of them when defined. -To enable only some of them, you have to pass the argument `nestedOperations` specifying the operations required. +By default, the `@relationship` enables all of them when defined. To enable only some of them, you have to pass the argument `nestedOperations` specifying the operations required. -*Disable nested create* +**Disable nested create** To disable the nested `CREATE` operation, change the initial type definitions to: @@ -184,11 +179,11 @@ type Actor { } ---- -As the `CREATE` operation is not present in the `nestedOperations` argument array, it is no longer possible to create movies starting from the `Actor` type. +As the `CREATE` operation is not present in the `nestedOperations` argument array, it will be no longer possible to create movies starting from the `Actor` type. -*Disable all nested operations* +**Disable all nested operations** -If instead, no nested operations are required, it is possible to disable all the nested operations by passing an empty array: +If instead, no nested operations are required, it's possible to disable all the nested operations by passing an empty array, as: [source, graphql, indent=0] ---- @@ -209,8 +204,8 @@ type Actor { This directive sets the availability of fields on queries and aggregations. It has two arguments: -* **onRead**: if disabled, this field is not available on queries and subscriptions. -* **onAggregate**: if disabled, aggregations is not available for this field. +* **onRead**: If disabled, this field will not be available on queries and subscriptions. +* **onAggregate**: If disabled, aggregations will not be available for this field. === Definition @@ -232,7 +227,7 @@ type Movie { } ---- -The type `Movie` in the resulting schema looks like this: +The type `Movie` in the resulting schema will look like this: [source, graphql, indent=0] ---- @@ -253,7 +248,7 @@ type MovieAggregateSelection { } ---- -In case you want to remove the `description` field from `MovieAggregateSelection`, you need to change the `onAggregate` value to `false`: +In case we wanted to remove the `description` field from `MovieAggregateSelection`, what we needed to do is change the `onAggregate` value to `false`, as follow: [source, graphql, indent=0] ---- @@ -263,7 +258,7 @@ type Movie { } ---- -*`@selectable` with relationships* +=== `@selectable` with Relationships This directive can be used along with relationship fields. @@ -279,9 +274,9 @@ type Actor { } ---- -This means that the `actedIn` field can be queried from the homonymous generated field `actedIn` and the field `actedInConnection`. -To avoid that, it is required to use the directive `@selectable`. -For instance, these type definitions: +This means that the `actedIn` field is queryable from the homonymous generated field `actedIn` and the field `actedInConnection`, to avoid that, it's required to use the directive `@selectable`. + +For instance: [source, graphql, indent=0] ---- @@ -298,7 +293,7 @@ type Actor { } ---- -Generate the type `Actor`: +It will generate the type type `Actor`: [source, graphql, indent=0] ---- @@ -308,16 +303,18 @@ type Actor { } ---- -Note how `actedInAggregate` is not affected by the argument `onAggregate`. -To disable the generation of `actedInAggregate`, see the `aggregate` argument of the directive xref::/schema-configuration/field-configuration.adoc#_relationship[`@relationship`]. +[NOTE] +==== +Please note how the `actedInAggregate` is not affected by the argument `onAggregate`. To disable the generation of `actedInAggregate` see the `aggregate` argument of the directive xref::type-definitions/schema-configuration/field-configuration.adoc#_relationship[`@relationship`] +==== == `@settable` This directive sets the availability of the input field on creation and update mutations. It has two arguments: -* **onCreate**: if disabled, this field is not available on creation operations. -* **onUpdate**: if disabled, this field is not available on update operations. +* **onCreate**: If disabled, this field will not be available on creation operations. +* **onUpdate**: If disabled, this field will not be available on update operations. === Definition @@ -327,7 +324,7 @@ It has two arguments: directive @settable(onCreate: Boolean! = true, onUpdate: Boolean! = true) on FIELD_DEFINITION ---- -=== Usage +==== Usage With this definition: @@ -345,7 +342,7 @@ type Actor { } ---- -The following input fields are generated: +The following input fields will be generated: [source, graphql, indent=0] ---- @@ -359,12 +356,12 @@ input MovieUpdateInput { } ---- -This means the description can be set on creation, but it is not available on update operations. +This means the description can be set on creation, but it will not be available on update operations. -*`@settable` with relationships* +=== `@settable` with Relationships This directive can be used along with relationship fields. -When an operation on a field is disabled this way, that relationship is no longer available on top-level operations. +When an operation on a field is disabled this way, that relationship will not be available on top-level operations. For example: [source, graphql, indent=0] @@ -382,7 +379,7 @@ type Actor { } ---- -The following input fields are generated: +The following input fields will be generated: [source, graphql, indent=0] ---- @@ -396,148 +393,44 @@ input ActorUpdateInput { } ---- -This means `actedIn` can be updated on an update, but it is no longer available on `create`` operations. +This means the `actedIn` can be updated on an update, but it will not be available on create operations. -== `@filterable` +== `@readonly` label:deprecated[] -This directive defines the filters generated for the field to which this directive is applied. -It has two arguments: +With this directive, fields will only be featured in mutations for creating and in object types for querying. +It is not mutable after creation. + +[NOTE] +==== +This directive is deprecated. +Use the xref::type-definitions/schema-configuration/field-configuration.adoc#_settable[`@settable`] directive instead. +==== -* **byValue**: if disabled, this field does not generate value filters. -* **byAggregate**: if disabled, this field does not generate aggregation filters. === Definition [source, graphql, indent=0] ---- -"""Instructs @neo4j/graphql to generate filters for this field.""" -directive @filterable(byValue: Boolean! = true, byAggregate: Boolean! = true) on FIELD_DEFINITION +"""Instructs @neo4j/graphql to only include a field in generated input type for creating, and in the object type within which the directive is applied.""" +directive @readonly on FIELD_DEFINITION ---- -=== Usage -With this definition: +== `@writeonly` label:deprecated[] -[source, graphql, indent=0] ----- -type Movie { - title: String! - description: String @filterable(byValue: false, byAggregate: false) - actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN) -} +With this directive, fields will only be featured in input types and will not be available for querying the object type through a Query or through a mutation response. -type Actor { - name: String! - actedIn: [Movie!]! - @relationship(type: "ACTED_IN", direction: OUT) -} ----- - -The following input fields are generated: +[NOTE] +==== +This directive is deprecated. +Use the xref::type-definitions/schema-configuration/field-configuration.adoc#_selectable[`@selectable`] directive instead. +==== -[source, graphql, indent=0] ----- -input MovieWhere { - OR: [MovieWhere!] - AND: [MovieWhere!] - NOT: MovieWhere - title: String - title_IN: [String!] - title_CONTAINS: String - title_STARTS_WITH: String - title_ENDS_WITH: String - actorsAggregate: MovieActorsAggregateInput - actors_ALL: ActorWhere - actors_NONE: ActorWhere - actors_SINGLE: ActorWhere - actors_SOME: ActorWhere - actorsConnection_ALL: MovieActorsConnectionWhere - actorsConnection_NONE: MovieActorsConnectionWhere - actorsConnection_SINGLE: MovieActorsConnectionWhere - actorsConnection_SOME: MovieActorsConnectionWhere -} - -input ActorActedInNodeAggregationWhereInput { - AND: [ActorActedInNodeAggregationWhereInput!] - OR: [ActorActedInNodeAggregationWhereInput!] - NOT: ActorActedInNodeAggregationWhereInput - title_AVERAGE_LENGTH_EQUAL: Float - title_LONGEST_LENGTH_EQUAL: Int - title_SHORTEST_LENGTH_EQUAL: Int - title_AVERAGE_LENGTH_GT: Float - title_LONGEST_LENGTH_GT: Int - title_SHORTEST_LENGTH_GT: Int - title_AVERAGE_LENGTH_GTE: Float - title_LONGEST_LENGTH_GTE: Int - title_SHORTEST_LENGTH_GTE: Int - title_AVERAGE_LENGTH_LT: Float - title_LONGEST_LENGTH_LT: Int - title_SHORTEST_LENGTH_LT: Int - title_AVERAGE_LENGTH_LTE: Float - title_LONGEST_LENGTH_LTE: Int - title_SHORTEST_LENGTH_LTE: Int -} ----- - -As shown by the generated input fields, the `description` field is not available for filtering on both value and aggregation filters. - -*`@filterable` with relationships* -This directive can be used along with relationship fields. -When an operation on a field is disabled this way, that relationship is no longer available on top-level operations. -For example: +=== Definition [source, graphql, indent=0] ---- -type Movie { - title: String! - description: String @filterable(byValue: false, byAggregate: false) - actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN) @filterable(byValue: false, byAggregate: false) -} - -type Actor { - name: String! - actedIn: [Movie!]! - @relationship(type: "ACTED_IN", direction: OUT) - -} ----- - -The following input fields are generated: - -[source, graphql, indent=0] +"""Instructs @neo4j/graphql to only include a field in the generated input types for the object type within which the directive is applied, but exclude it from the object type itself.""" +directive @writeonly on FIELD_DEFINITION ---- -input MovieWhere { - OR: [MovieWhere!] - AND: [MovieWhere!] - NOT: MovieWhere - title: String - title_IN: [String!] - title_CONTAINS: String - title_STARTS_WITH: String - title_ENDS_WITH: String -} - -input ActorActedInNodeAggregationWhereInput { - AND: [ActorActedInNodeAggregationWhereInput!] - OR: [ActorActedInNodeAggregationWhereInput!] - NOT: ActorActedInNodeAggregationWhereInput - title_AVERAGE_LENGTH_EQUAL: Float - title_LONGEST_LENGTH_EQUAL: Int - title_SHORTEST_LENGTH_EQUAL: Int - title_AVERAGE_LENGTH_GT: Float - title_LONGEST_LENGTH_GT: Int - title_SHORTEST_LENGTH_GT: Int - title_AVERAGE_LENGTH_GTE: Float - title_LONGEST_LENGTH_GTE: Int - title_SHORTEST_LENGTH_GTE: Int - title_AVERAGE_LENGTH_LT: Float - title_LONGEST_LENGTH_LT: Int - title_SHORTEST_LENGTH_LT: Int - title_AVERAGE_LENGTH_LTE: Float - title_LONGEST_LENGTH_LTE: Int - title_SHORTEST_LENGTH_LTE: Int -} ----- - -As shown by the previous inputs fields, the `actors` field is not available for filtering on both value and aggregation filters. \ No newline at end of file diff --git a/modules/ROOT/pages/schema-configuration/global-configuration.adoc b/modules/ROOT/pages/type-definitions/schema-configuration/global-configuration.adoc similarity index 69% rename from modules/ROOT/pages/schema-configuration/global-configuration.adoc rename to modules/ROOT/pages/type-definitions/schema-configuration/global-configuration.adoc index 79a8ef13..d1ef708a 100644 --- a/modules/ROOT/pages/schema-configuration/global-configuration.adoc +++ b/modules/ROOT/pages/type-definitions/schema-configuration/global-configuration.adoc @@ -1,11 +1,9 @@ [[schema-configuration-global-configuration]] -:description: This page describes how to globally disable specific types of operations. -= Global configuration += Global Configuration Through the schema configuration, it is possible to globally disable specific types of operation. -To set up operations individually, refer to xref:/schema-configuration/type-configuration.adoc[Type Configuration]. - -For instance, if you want *to disable all top-level aggregation operations at once*, the Neo4j GraphQL Library offers this option through schema extensions with `@query`: +To set up operations individually, refer to xref:type-definitions/schema-configuration/type-configuration.adoc[Type Configuration]. +For instance, if you want to disable all top-level aggregation operations at once, Neo4j GraphQL Library offers this option through schema extensions with `@query`: [source, graphql, indent=0] ---- @@ -34,8 +32,8 @@ extend schema @query(read: true, aggregate: false) **Invalid schema usage** The same schema configuration directive cannot be applied to both Schema and Object. -Take the following type definitions as example: +Take the following type definitions as example: [source, graphql, indent=0] ---- type Movie { @@ -50,4 +48,4 @@ type Actor @query(read: false, aggregate: true) { extend schema @query(read: true, aggregate: false) ---- -Such configuration prompts the error `"@query directive already defined at the schema location"`. +Such configuration will prompt the error `"@query directive already defined at the schema location"`. diff --git a/modules/ROOT/pages/type-definitions/schema-configuration/index.adoc b/modules/ROOT/pages/type-definitions/schema-configuration/index.adoc new file mode 100644 index 00000000..09a5d10b --- /dev/null +++ b/modules/ROOT/pages/type-definitions/schema-configuration/index.adoc @@ -0,0 +1,39 @@ +[[type-definitions-schema-configuration]] += Schema Configuration + +Neo4j GraphQL Library supports several functionalities such as CRUD operations, aggregation, filtering, and others. +To make them work, a large amount of GraphQL types are generated. + +In some cases, it may be advisable to reduce the scope of the API produced. +This section provides information on how to limit access to unwanted operations and reduce the size of the schema which can improve the performance. + +For instance, starting from the type definitions: + +[source, graphql, indent=0] +---- +type Movie { + title: String + length: Int +} +---- + +If you want to prevent users from querying `movies` in the Neo4j Database, you need to make the following the changes: + +[source, graphql, indent=0] +---- +type Movie @query(read: false, aggregate: false) { + title: String + length: Int +} +---- + +With this change, the library does **not** generate the following operation fields from the `Query` type: + +* `movies` +* `moviesAggregate` +* `moviesConnection` + +[NOTE] +==== +Aggregations will no longer be generated by default in the 4.0.0 version of the library. See xref::guides/v4-migration/index.adoc#opt-in-aggregation[`Opt-in Aggregation`] for more information. +==== diff --git a/modules/ROOT/pages/reference/directives/schema-configuration/type-configuration.adoc b/modules/ROOT/pages/type-definitions/schema-configuration/type-configuration.adoc similarity index 51% rename from modules/ROOT/pages/reference/directives/schema-configuration/type-configuration.adoc rename to modules/ROOT/pages/type-definitions/schema-configuration/type-configuration.adoc index 774ec765..c1978a5a 100644 --- a/modules/ROOT/pages/reference/directives/schema-configuration/type-configuration.adoc +++ b/modules/ROOT/pages/type-definitions/schema-configuration/type-configuration.adoc @@ -49,7 +49,7 @@ directive @query(read: Boolean! = true, aggregate: Boolean! = false) on OBJECT | [NOTE] ==== Aggregations will no longer be generated by default in the 4.0.0 version of the library. -See xref:migration/v4-migration/index.adoc#opt-in-aggregation[`Opt-in Aggregation`] for more information. +See xref::guides/v4-migration/index.adoc#opt-in-aggregation[`Opt-in Aggregation`] for more information. ==== === Usage @@ -121,15 +121,15 @@ This directive is used to limit Subscription operations in the library. [source, graphql, indent=0] ---- -enum SubscriptionEvent { - CREATED - UPDATED - DELETED - RELATIONSHIP_CREATED - RELATIONSHIP_DELETED +enum SubscriptionFields { + CREATE + UPDATE + DELETE + CREATE_RELATIONSHIP + DELETE_RELATIONSHIP } -directive @subscription(events: [SubscriptionEvent!]! = [CREATED, UPDATED, DELETED, RELATIONSHIP_CREATED, RELATIONSHIP_DELETED]) on OBJECT | SCHEMA +directive @subscription(operations: [SubscriptionFields!]! = [CREATE, UPDATE, DELETE, CREATE_RELATIONSHIP, DELETE_RELATIONSHIP]) on OBJECT | SCHEMA ---- === Usage @@ -138,7 +138,7 @@ directive @subscription(events: [SubscriptionEvent!]! = [CREATED, UPDATED, DELET [source, graphql, indent=0] ---- -type Movie @subscription(events: []) { +type Movie @subscription(operations: []) { title: String length: Int } @@ -148,8 +148,93 @@ type Movie @subscription(events: []) { [source, graphql, indent=0] ---- -type Movie @subscription(events: [CREATED]) { +type Movie @subscription(operations: [CREATE]) { title: String length: Int } ---- + +== `@exclude` label:deprecated[] + +This directive skips the generation of queries and/or subscriptions and/or particular/all mutations for the specified type. + +[NOTE] +==== +This directive is deprecated. + +Use the xref:/type-definitions/schema-configuration/type-configuration.adoc#_query[`@query`], xref:/type-definitions/schema-configuration/type-configuration.adoc#_mutation[`@mutation`] and the xref:/type-definitions/schema-configuration/type-configuration.adoc#_subscription[`@subscription`] directives instead. +==== + +=== Definition + +[source, graphql, indent=0] +---- +enum ExcludeOperation { + CREATE + READ + UPDATE + DELETE + SUBSCRIBE +} + +"""Instructs @neo4j/graphql to exclude the specified operations from query, mutation and subscription generation. If used without an argument, no queries, mutations or subscriptions will be generated for this type.""" +directive @exclude( + operations: [ExcludeOperation!]! = [CREATE, READ, UPDATE, DELETE, SUBSCRIBE] +) on OBJECT +---- + +=== Usage + +==== Disable Query field generation + +[source, graphql, indent=0] +---- +type User @exclude(operations: [READ]) { + name: String +} +---- + +==== Disable single Mutation field generation + +[source, graphql, indent=0] +---- +type User @exclude(operations: [CREATE]) { + name: String +} +---- + +==== Disable multiple Mutation field generation + +[source, graphql, indent=0] +---- +type User @exclude(operations: [CREATE, DELETE]) { + name: String +} +---- + +==== Disable Subscription field generation + +[source, graphql, indent=0] +---- +type User @exclude(operations: [SUBSCRIBE]) { + name: String +} +---- + +==== Disable all Query, Mutation, and Subscription field generation + +The following two type definitions are equivalent in the sense that no queries, mutations, or subscriptions will be generated for either of them: + +[source, graphql, indent=0] +---- +type User @exclude { + name: String +} +---- + +[source, graphql, indent=0] +---- +type User @exclude(operations: [CREATE, READ, UPDATE, DELETE, SUBSCRIBE]) { + name: String +} +---- \ No newline at end of file diff --git a/modules/ROOT/pages/type-definitions/types.adoc b/modules/ROOT/pages/type-definitions/types.adoc new file mode 100644 index 00000000..4d17d374 --- /dev/null +++ b/modules/ROOT/pages/type-definitions/types.adoc @@ -0,0 +1,301 @@ +[[type-definitions-types]] += Types + +Neo4j GraphQL supports all of the default GraphQL https://graphql.org/learn/schema/#scalar-types[scalar types] as well as additional scalar and object types specific to the Neo4j database. + +== Int + +One of the default GraphQL scalar types. Supports up to 53-bit values - see xref::type-definitions/types.adoc#type-definitions-types-bigint[`BigInt`] for 64-bit value support. + +== Float + +One of the default GraphQL scalar types. + +== String + +One of the default GraphQL scalar types. + +== Boolean + +One of the default GraphQL scalar types. + +== ID + +One of the default GraphQL scalar types. Stored as a string in the database and always returned as a string. + +[[type-definitions-types-bigint]] +== `BigInt` + +Supports up to 64 bit integers, serialized as strings in variables and in data responses. Shares the same xref::filtering.adoc#filtering-numerical-operators[Numerical operators] as the other numeric types. + +[source, graphql, indent=0] +---- +type File { + size: BigInt +} +---- + +Can be passed as a number (does not need quotes) when used directly in a query or mutation: + +[source, graphql, indent=0] +---- +query { + files(where: { size: 9223372036854775807 }) { + size + } +} +---- + +[[type-definitions-types-temporal]] +== Temporal Types + +=== `DateTime` + +ISO datetime string stored as a https://neo4j.com/docs/cypher-manual/current/values-and-types/temporal/#_temporal_value_types[datetime] temporal type. + +[source, graphql, indent=0] +---- +type User { + createdAt: DateTime +} +---- + +=== `Date` + +"yyyy-mm-dd" date string stored as a https://neo4j.com/docs/cypher-manual/current/values-and-types/temporal/#_temporal_value_types[date] temporal type. + +[source, graphql, indent=0] +---- +type Movie { + releaseDate: Date +} +---- + +=== `Duration` + +ISO 8601 duration string stored as a https://neo4j.com/docs/cypher-manual/current/values-and-types/temporal/#cypher-temporal-durations[duration] type. + +[source, graphql, indent=0] +---- +type Movie { + runningTime: Duration! +} +---- + +_Note:_ + +- Decimal values are not currently accepted on `[YMWD]` +- Comparisons are made according to the https://neo4j.com/developer/cypher/dates-datetimes-durations/#comparing-filtering-values[Cypher Developer Guide] + +=== `LocalDateTime` + +"YYYY-MM-DDTHH:MM:SS" datetime string stored as a https://neo4j.com/docs/cypher-manual/current/values-and-types/temporal/#_temporal_value_types[LocalDateTime] temporal type. + +[source, graphql, indent=0] +---- +type Movie { + nextShowing: LocalDateTime +} +---- + +=== `Time` + +RFC3339 time string stored as a https://neo4j.com/docs/cypher-manual/current/values-and-types/temporal/#_temporal_value_types[Time] temporal type. + +[source, graphql, indent=0] +---- +type Movie { + nextShowing: Time +} +---- + +=== `LocalTime` + +"HH:MM:SS[.sss+]" time string stored as a https://neo4j.com/docs/cypher-manual/current/values-and-types/temporal/#_temporal_value_types[LocalTime] temporal type. + +[source, graphql, indent=0] +---- +type Movie { + nextShowing: LocalTime +} +---- + +[[type-definitions-types-spatial]] +== Spatial Types + +Neo4j GraphQL spatial types translate to spatial values stored using https://neo4j.com/docs/cypher-manual/current/values-and-types/spatial/[point] in the database. The use of either of these types in a GraphQL schema will automatically introduce the types needed to run queries and mutations relevant to these spatial types. + +[[type-definitions-types-point]] +=== `Point` + +The `Point` type is used to describe the two https://neo4j.com/docs/cypher-manual/current/values-and-types/spatial/#spatial-values-crs-geographic[Geographic coordinate reference systems] supported by Neo4j. + +In order to use it in your schema, you quite simply add a field with a type `Point` to any type or types in schema, like the following: + +[source, graphql, indent=0] +---- +type TypeWithPoint { + location: Point! +} +---- + +Once this has been done, the `Point` type will be automatically added to your schema, in addition to all of the input and output types you will need to query and manipulate spatial types through your API. + +The rest of the documentation under this heading is documenting all of those automatically generated types and how to use them. + +==== Type definition + +[source, graphql, indent=0] +---- +type Point { + latitude: Float! + longitude: Float! + height: Float +} +---- + +==== Queries and mutations + +Due to the fact that `Point` is an object type, it has an additional type for input in queries and mutations. However, this input type has the same shape as the object type: + +[source, graphql, indent=0] +---- +input PointInput { + latitude: Float! + longitude: Float! + height: Float +} +---- + +===== Query + +For example, you can query for a `User` with an exact location: + +[source, graphql, indent=0] +---- +query Users($longitude: Float!, $latitude: Float!) { + users(where: { location: { longitude: $longitude, latitude: $latitude } }) { + name + location { + longitude + latitude + } + } +} +---- + +===== Mutation + +An example of creating a `User` with a location is as follows: + +[source, graphql, indent=0] +---- +mutation CreateUsers($name: String!, $longitude: Float!, $latitude: Float!) { + createUsers(input: [{ name: $name, location: { longitude: $longitude, latitude: $latitude } }]) { + users { + name + location { + longitude + latitude + } + } + } +} +---- + +==== Filtering + +In addition to the xref::filtering.adoc#filtering-numerical-operators[Numerical operators], the `Point` type has an additional `_DISTANCE` filter. All of the filters take the following type as an argument: + +[source, graphql, indent=0] +---- +input PointDistance { + point: Point! + distance: Float! +} +---- + +In essence, each of the filters mean the following: + +* `_LT`: Checks that the specified `point` field is less than the `distance` away in meters from the `Point` being compared against. +* `_LTE`: Checks that the specified `point` field is less than or equal to the `distance` away in meters from the `Point` being compared against. +* `_DISTANCE`: Checks that the specified `point` field is the exact `distance` away in meters from the `Point` being compared against. +* `_GTE`: Checks that the specified `point` field is greater than the `distance` away in meters from the `Point` being compared against. +* `_GT`: Checks that the specified `point` field is greater than or equal to the `distance` away in meters from the `Point` being compared against. + +In practice, you can construct queries such as the following which will find all users within a 5km (5000m) radius of a `Point`: + +[source, graphql, indent=0] +---- +query CloseByUsers($longitude: Float!, $latitude: Float!) { + users(where: { location_LTE: { point: { longitude: $longitude, latitude: $latitude }, distance: 5000 } }) { + name + location { + longitude + latitude + } + } +} +---- + +[[type-definitions-types-cartesian-point]] +=== `CartesianPoint` + +The `CartesianPoint` type is used to describe the two https://neo4j.com/docs/cypher-manual/current/values-and-types/spatial/#spatial-values-crs-cartesian[Cartesian coordinate reference systems] supported by Neo4j. + +In order to use it in your schema, you quite simply add a field with a type `CartesianPoint` to any type or types in schema, like the following: + +[source, graphql, indent=0] +---- +type TypeWithCartesianPoint { + location: CartesianPoint! +} +---- + +Once this has been done, the `CartesianPoint` type will be automatically added to your schema, in addition to all of the input and output types you will need to query and manipulate spatial types through your API. + +The rest of the documentation under this heading is documenting all of those automatically generated types and how to use them. + +==== Type definition + +[source, graphql, indent=0] +---- +type CartesianPoint { + x: Float! + y: Float! + z: Float +} +---- + +==== Queries and mutations + +Due to the fact that `CartesianPoint` is an object type, it has an additional type for input in queries and mutations. However, this input type has the same shape as the object type: + +[source, graphql, indent=0] +---- +input CartesianPointInput { + x: Float! + y: Float! + z: Float +} +---- + +==== Filtering + +In addition to the xref::filtering.adoc#filtering-numerical-operators[Numerical operators], the `CartesianPoint` type has an additional `_DISTANCE` filter. All of the filters take the following type as an argument: + +[source, graphql, indent=0] +---- +input CartesianPointDistance { + point: CartesianPoint! + distance: Float! +} +---- + +In essence, each of the filters mean the following: + +* `_LT`: Checks that the specified `point` field is less than the `distance` away from the `CartesianPoint` being compared against, in the units used to specify the points. +* `_LTE`: Checks that the specified `point` field is less than or equal to the `distance` away from the `CartesianPoint` being compared against, in the units used to specify the points. +* `_DISTANCE`: Checks that the specified `point` field is the exact `distance` away from the `CartesianPoint` being compared against, in the units used to specify the points. +* `_GTE`: Checks that the specified `point` field is greater than the `distance` away from the `CartesianPoint` being compared against, in the units used to specify the points. +* `_GT`: Checks that the specified `point` field is greater than or equal to the `distance` away from the `CartesianPoint` being compared against, in the units used to specify the points. diff --git a/modules/ROOT/pages/type-definitions/types/index.adoc b/modules/ROOT/pages/type-definitions/types/index.adoc deleted file mode 100644 index b674d16f..00000000 --- a/modules/ROOT/pages/type-definitions/types/index.adoc +++ /dev/null @@ -1,53 +0,0 @@ -[[types]] -:description: This page lists all types available in the Neo4j GraphQL Library. -= Types - -The Neo4j GraphQL Library provides the following types: - -[cols="1,3"] -|=== -| Type | Description - -| xref::/type-definitions/types/scalar.adoc[`Int`] -| The `Int` scalar type represents a signed 32-bit numeric non-fractional value. - -| xref::/type-definitions/types/scalar.adoc[`Float`] -| The `Float` scalar type represents a signed double-precision fractional value. - -| xref::/type-definitions/types/scalar.adoc[`String`] -| The `String` scalar type represents textual data, represented as UTF-8 character sequences. - -| xref::/type-definitions/types/scalar.adoc[`Boolean`] -| The `Boolean` scalar type represents `true` or `false`. - -| xref::/type-definitions/types/scalar.adoc[`ID`] -| The `ID` scalar type represents a unique identifier, often used to refetch an object. - -| xref::/type-definitions/types/scalar.adoc[`BigInt`] -| The `BigInt` scalar type represents a signed 64-bit numeric non-fractional value. - -| xref:/type-definitions/types/temporal.adoc[`Date`] -| The `Date` scalar type is a ISO 8601 date mapping to the Neo4j `DATE` type. - -| xref::/type-definitions/types/temporal.adoc[`Time`] -| The `Time` scalar type is a ISO 8601 time of day and timezone mapping to the Neo4j `ZONED TIME` type. - -| xref::/type-definitions/types/temporal.adoc[`LocalTime`] -| The `LocalTime` scalar type is a ISO 8601 time of day mapping to the Neo4j `LOCAL TIME` type. - -| xref::/type-definitions/types/temporal.adoc[`DateTime`] -| The `DateTime` scalar type is a ISO 8601 date, time of day and timezone mapping to the Neo4j `ZONED DATETIME` type. - -| xref:/type-definitions/types/temporal.adoc[`LocalDateTime`] -| The `LocalDateTime` scalar type is a ISO 8601 date and time of day mapping to the Neo4j `LOCAL DATETIME` type. - -| xref::/type-definitions/types/temporal.adoc[`Duration`] -| The `Duration` scalar type is a ISO 8601 duration mapping to the Neo4j `DURATION` type. - -| xref::/type-definitions/types/spatial.adoc#point[`Point`] -| The `Point` object type is a WGS 84 3D geographic point. - -| xref::/type-definitions/types/spatial.adoc#cartesian-point[`CartesianPoint`] -| The `CartesianPoint` object type is a Cartesian 3D point. - -|=== \ No newline at end of file diff --git a/modules/ROOT/pages/type-definitions/types/scalar.adoc b/modules/ROOT/pages/type-definitions/types/scalar.adoc deleted file mode 100644 index 67315112..00000000 --- a/modules/ROOT/pages/type-definitions/types/scalar.adoc +++ /dev/null @@ -1,83 +0,0 @@ -[[type-definitions-types]] -:description: This page lists the default types available in the Neo4j GraphQL Library. -= Scalar types - -Neo4j GraphQL supports all of the built-in GraphQL https://graphql.org/learn/schema/#scalar-types[scalar types] as well as additional scalar and object types specific to the Neo4j database. - -== Scalar types - -[cols="1,2,2"] -|=== -| Type | Description | Example - -| `Int` -| Supports up to 53-bit values. -a| -[source, graphql, indent=0] ----- -type Person { - age: Int! -} ----- - -.2+| `BigInt` -| Supports up to 64 bit integers, serialized as strings in variables and in data responses. -Shares the same xref::queries-aggregations/filtering.adoc#filtering-numerical-operators[Numerical operators] as the other numeric types. -a| -[source, graphql, indent=0] ----- -type File { - size: BigInt -} ----- - -| Can be passed as a number (does not need quotes) when used directly in a query or mutation. -a| -[source, graphql, indent=0] ----- -query { - files(where: { size: 9223372036854775807 }) { - size - } -} ----- - -| `Float` -| Placeholder text to make the table prettier. -a| -[source, graphql, indent=0] ----- -type Product { - price: Float! -} ----- - -| `String` -| Stored as a string in the database and always returned as a string. -a| -[source, graphql, indent=0] ----- -type Product { - name: String! -} ----- - -| `Boolean` -| Placeholder text to make the table prettier. -a| -[source, graphql, indent=0] ----- -type Product { - inStock: Boolean! -} ----- - -| `ID` -| Placeholder text to make the table prettier. -[source, graphql, indent=0] ----- -type Product { - id: ID! -} ----- -|=== \ No newline at end of file diff --git a/modules/ROOT/pages/type-definitions/types/spatial.adoc b/modules/ROOT/pages/type-definitions/types/spatial.adoc deleted file mode 100644 index 6f0b6958..00000000 --- a/modules/ROOT/pages/type-definitions/types/spatial.adoc +++ /dev/null @@ -1,185 +0,0 @@ - -[[type-definitions-spatial-types]] -:description: This page lists the spatial types available in the Neo4j GraphQL Library. -= Temporal types - -== Spatial Types - -Neo4j GraphQL spatial types translate to spatial values stored using https://neo4j.com/docs/cypher-manual/current/values-and-types/spatial/[`Point`] in the database. -The use of either of these types in a GraphQL schema automatically introduces the types needed to run queries and mutations relevant to these spatial types. - -[[point]] -=== `Point` - -The `Point` type is used to describe the two https://neo4j.com/docs/cypher-manual/current/values-and-types/spatial/#spatial-values-crs-geographic[Geographic coordinate reference systems] supported by Neo4j. - -In order to use it in your schema, you quite simply add a field with a type `Point` to any type or types in schema, like the following: - -[source, graphql, indent=0] ----- -type TypeWithPoint { - location: Point! -} ----- - -Once this has been done, the `Point` type is automatically added to your schema, in addition to all of the input and output types you need to query and manipulate spatial types through your API. - -These are the automatically generated types and how to use them: - -==== Type definition - -[source, graphql, indent=0] ----- -type Point { - latitude: Float! - longitude: Float! - height: Float -} ----- - -==== Queries and mutations - -Due to the fact that `Point` is an object type, it has an additional type for input in queries and mutations. -However, this input type has the same shape as the object type: - -[source, graphql, indent=0] ----- -input PointInput { - latitude: Float! - longitude: Float! - height: Float -} ----- - -For example, you can query for a `User` with an exact location: - -[source, graphql, indent=0] ----- -query Users($longitude: Float!, $latitude: Float!) { - users(where: { location: { longitude: $longitude, latitude: $latitude } }) { - name - location { - longitude - latitude - } - } -} ----- - -Or you can create a `User` with a location as follows: - -[source, graphql, indent=0] ----- -mutation CreateUsers($name: String!, $longitude: Float!, $latitude: Float!) { - createUsers(input: [{ name: $name, location: { longitude: $longitude, latitude: $latitude } }]) { - users { - name - location { - longitude - latitude - } - } - } -} ----- - -==== Filtering - -Besides the xref::queries-aggregations/filtering.adoc#filtering-numerical-operators[Numerical operators], the `Point` type has an additional `_DISTANCE` filter. -Here is a list of what each filter does: - -* `_LT`: checks that the specified `Point` field is less than the `distance` away in meters from the `Point` being compared against. -* `_LTE`: checks that the specified `Point` field is less than or equal to the `distance` away in meters from the `Point` being compared against. -* `_DISTANCE`: checks that the specified `Point` field is the exact `distance` away in meters from the `Point` being compared against. -* `_GTE`: checks that the specified `Point` field is greater than the `distance` away in meters from the `Point` being compared against. -* `_GT`: checks that the specified `Point` field is greater than or equal to the `distance` away in meters from the `Point` being compared against. - -All of the filters take the following type as an argument: - -[source, graphql, indent=0] ----- -input PointDistance { - point: Point! - distance: Float! -} ----- - -In practice, you can construct queries such as the following which can find all users within a 5km (5000m) radius of a `Point`: - -[source, graphql, indent=0] ----- -query CloseByUsers($longitude: Float!, $latitude: Float!) { - users(where: { location_LTE: { point: { longitude: $longitude, latitude: $latitude }, distance: 5000 } }) { - name - location { - longitude - latitude - } - } -} ----- - -[[cartesian-point]] -=== `CartesianPoint` - -The `CartesianPoint` type is used to describe the two https://neo4j.com/docs/cypher-manual/current/values-and-types/spatial/#spatial-values-crs-cartesian[Cartesian coordinate reference systems] supported by Neo4j. - -To use it in the schema, add a field with a type `CartesianPoint` to any type or types, such as in this example: - -[source, graphql, indent=0] ----- -type TypeWithCartesianPoint { - location: CartesianPoint! -} ----- - -Once this has been done, the `CartesianPoint` type is automatically added to your schema, in addition to all of the input and output types you will need to query and manipulate spatial types through your API. - -These are the automatically generated types and how to use them: - -==== Type definition - -[source, graphql, indent=0] ----- -type CartesianPoint { - x: Float! - y: Float! - z: Float -} ----- - -==== Queries and mutations - -Due to the fact that `CartesianPoint` is an object type, it has an additional type for input in queries and mutations. -However, this input type has the same shape as the object type: - -[source, graphql, indent=0] ----- -input CartesianPointInput { - x: Float! - y: Float! - z: Float -} ----- - -==== Filtering - -Besides the xref::queries-aggregations/filtering.adoc#filtering-numerical-operators[Numerical operators], the `CartesianPoint` type has an additional `_DISTANCE` filter. - -Here is a list of what each filter does: - -* `_LT`: checks that the specified `Point` field is less than the `distance` away from the `CartesianPoint` being compared against, in the units used to specify the points. -* `_LTE`: checks that the specified `Point` field is less than or equal to the `distance` away from the `CartesianPoint` being compared against, in the units used to specify the points. -* `_DISTANCE`: checks that the specified `Point` field is the exact `distance` away from the `CartesianPoint` being compared against, in the units used to specify the points. -* `_GTE`: checks that the specified `Point` field is greater than the `distance` away from the `CartesianPoint` being compared against, in the units used to specify the points. -* `_GT`: checks that the specified `Point` field is greater than or equal to the `distance` away from the `CartesianPoint` being compared against, in the units used to specify the points. - -All of the filters take the following type as an argument: - -[source, graphql, indent=0] ----- -input CartesianPointDistance { - point: CartesianPoint! - distance: Float! -} ----- \ No newline at end of file diff --git a/modules/ROOT/pages/type-definitions/types/temporal.adoc b/modules/ROOT/pages/type-definitions/types/temporal.adoc deleted file mode 100644 index 2d62387c..00000000 --- a/modules/ROOT/pages/type-definitions/types/temporal.adoc +++ /dev/null @@ -1,77 +0,0 @@ -[[type-definitions-temporal-types]] -:description: This page lists the temporal types available in the Neo4j GraphQL Library. -= Temporal types - -Neo4j GraphQL supports all of the default GraphQL https://graphql.org/learn/schema/#scalar-types[scalar types] as well as additional scalar and object types specific to the Neo4j database, such as the temporal types. - -== Temporal types - -[cols="1,2,2"] -|=== -| Type | Description | Example - -| `DateTime` -| ISO datetime string stored as a https://neo4j.com/docs/cypher-manual/current/values-and-types/temporal/#_temporal_value_types[datetime] temporal type. -a| -[source, graphql, indent=0] ----- -type User { - createdAt: DateTime -} ----- - -| `Date` -| "YYYY-MM-DD" date string stored as a https://neo4j.com/docs/cypher-manual/current/values-and-types/temporal/#_temporal_value_types[date] temporal type. -a| -[source, graphql, indent=0] ----- -type Movie { - releaseDate: Date -} ----- - -| `Duration` -a| ISO 8601 duration string stored as a https://neo4j.com/docs/cypher-manual/current/values-and-types/temporal/#cypher-temporal-durations[duration] type. -[NOTE] -==== -Decimal values are not currently accepted on `[YMWD]`. -Comparisons are made according to the https://neo4j.com/developer/cypher/dates-datetimes-durations/#comparing-filtering-values[Cypher Developer Guide]. -==== -a| -[source, graphql, indent=0] ----- -type Movie { - runningTime: Duration! -} ----- - -| `LocalDateTime` -| "YYYY-MM-DDTHH:MM:SS" datetime string stored as a https://neo4j.com/docs/cypher-manual/current/values-and-types/temporal/#_temporal_value_types[LocalDateTime] temporal type. -a| -[source, graphql, indent=0] ----- -type Movie { - nextShowing: LocalDateTime -} ----- - -| `Time` -| RFC3339 time string stored as a https://neo4j.com/docs/cypher-manual/current/values-and-types/temporal/#_temporal_value_types[Time] temporal type. -a| -[source, graphql, indent=0] ----- -type Movie { - nextShowing: Time -} ----- - -| `LocalTime` -| "HH:MM:SS[.sss+]" time string stored as a https://neo4j.com/docs/cypher-manual/current/values-and-types/temporal/#_temporal_value_types[LocalTime] temporal type. -a| -[source, graphql, indent=0] ----- -type Movie { - nextShowing: LocalTime -} ----- -|=== \ No newline at end of file diff --git a/modules/ROOT/pages/type-definitions/types/unions.adoc b/modules/ROOT/pages/type-definitions/unions.adoc similarity index 67% rename from modules/ROOT/pages/type-definitions/types/unions.adoc rename to modules/ROOT/pages/type-definitions/unions.adoc index 06e8d219..18ba82e7 100644 --- a/modules/ROOT/pages/type-definitions/types/unions.adoc +++ b/modules/ROOT/pages/type-definitions/unions.adoc @@ -1,12 +1,7 @@ [[type-definitions-unions]] -:description: This page describes how to use unions on relatinship fields with the Neo4j GraphQL Library. -= Union types += Union Types -The Neo4j GraphQL Library supports the use of unions on relationship fields. - -As an example, consider the following schema. -It defines a `User` type that has a relationship `HAS_CONTENT`, of type `[Content!]!`. -`Content` is of type `union`, representing either a `Blog` or a `Post`: +The Neo4j GraphQL Library supports the use of unions on relationship fields. For example, the following schema defines a `User` type, that has a relationship `HAS_CONTENT`, of type `[Content!]!`. `Content` is of type `union` representing either a `Blog` or a `Post`. [source, graphql, indent=0] ---- @@ -27,9 +22,50 @@ type User { } ---- +Below you can find some examples of how queries and mutations work with this example. + +[[type-definitions-unions-querying]] +== Querying a union + +Which union members are returned by a Query are dictated by the `where` filter applied. + +For example, the following will return all user content, and you will specifically get the title of each blog. + +[source, graphql, indent=0] +---- +query GetUsersWithBlogs { + users { + name + content { + ... on Blog { + title + } + } + } +} +---- + +Whilst the query below will only return blogs. You could for instance use a filter to check that the title is not null to essentially return all blogs: + +[source, graphql, indent=0] +---- +query GetUsersWithAllContent { + users { + name + content(where: { Blog: { NOT: { title: null } }}) { + ... on Blog { + title + } + } + } +} +---- + +This is to prevent overfetching, and you can find an explanation of this xref::appendix/preventing-overfetching.adoc[here]. + == Creating a union -To create the union featured in the example, you need to do this mutation: +The below mutation creates the user and their content: [source, graphql, indent=0] ---- @@ -67,42 +103,3 @@ mutation CreateUserAndContent { } } ---- - -== Querying a union - -Which union members are returned by a query are dictated by the `where` filter applied to the query. -The following example returns all user content, more specifically the title of each blog: - -[source, graphql, indent=0] ----- -query GetUsersWithBlogs { - users { - name - content { - ... on Blog { - title - } - } - } -} ----- - -While this particular query only returns blogs, you could for instance use a filter to check that the title is not null when the list of blogs is returned: - -[source, graphql, indent=0] ----- -query GetUsersWithAllContent { - users { - name - content(where: { Blog: { NOT: { title: null } }}) { - ... on Blog { - title - } - } - } -} ----- - -This also helps with preventing overfetching. -See more information in the xref::troubleshooting.adoc#appendix-preventing-overfetching[Troubleshooting] section. - diff --git a/modules/ROOT/partials/reusing-content.adoc b/modules/ROOT/partials/reusing-content.adoc deleted file mode 100644 index 464c8ddc..00000000 --- a/modules/ROOT/partials/reusing-content.adoc +++ /dev/null @@ -1 +0,0 @@ -This paragraph can be used anywhere with the syntax shown in xref:content-types.adoc#_partials[]. \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 3c45d985..4ab45a14 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "graphql", - "version": "4.0.0", + "version": "3.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "graphql", - "version": "4.0.0", + "version": "3.0.0", "license": "ISC", "dependencies": { "@antora/cli": "^3.1.0", diff --git a/package.json b/package.json index 14fbecea..9c487273 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "graphql", - "version": "4.0.0", + "version": "3.0.0", "description": "Neo4j GraphQL Library", "main": "server.js", "scripts": { diff --git a/preview.yml b/preview.yml index 4ae4529d..7a1b4f53 100644 --- a/preview.yml +++ b/preview.yml @@ -24,7 +24,7 @@ urls: antora: extensions: - require: "@neo4j-antora/antora-modify-sitemaps" - sitemap_version: '4.0' + sitemap_version: '3.0' sitemap_loc_version: 'current' move_sitemaps_to_components: true diff --git a/publish.yml b/publish.yml index c9e2857c..e35afc8d 100644 --- a/publish.yml +++ b/publish.yml @@ -7,7 +7,7 @@ site: content: sources: - url: https://github.com/neo4j/docs-graphql.git - branches: ['main'] + branches: ['3.x'] exclude: - '!**/_includes/*' - '!**/readme.adoc' @@ -25,7 +25,7 @@ urls: antora: extensions: - require: "@neo4j-antora/antora-modify-sitemaps" - sitemap_version: '4.0' + sitemap_version: '3.0' sitemap_loc_version: 'current' move_sitemaps_to_components: true