Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
75388d4
Docs update
Liam-Doodson Feb 22, 2023
c0bdf5c
Parse selection sets
Liam-Doodson Feb 22, 2023
66d2e38
Bugfix
Liam-Doodson Feb 22, 2023
ca21dfa
UT fix
Liam-Doodson Feb 22, 2023
8023b0c
UT fix
Liam-Doodson Feb 22, 2023
8332223
UT fix
Liam-Doodson Feb 22, 2023
57f7e53
UT fix
Liam-Doodson Feb 22, 2023
9f9de8d
Updated int tests
Liam-Doodson Feb 22, 2023
f6bf93f
TCK tests
Liam-Doodson Feb 22, 2023
7743399
Changeset
Liam-Doodson Feb 22, 2023
1b8c7a7
Bugfix
Liam-Doodson Feb 23, 2023
ea1a7a6
Merge branch '4.0.0' into change-customResolver-requires-to-a-selecti…
Liam-Doodson Feb 24, 2023
fe71ce5
Merge fix
Liam-Doodson Feb 24, 2023
1fb6318
Test fixes
Liam-Doodson Feb 24, 2023
cba95a8
More test fixes
Liam-Doodson Feb 24, 2023
a62ddea
Removed baseSchema
Liam-Doodson Feb 27, 2023
a09aecf
Re-added validation for customResolver requires
Liam-Doodson Feb 27, 2023
d62b745
Moved validation to within validate document
Liam-Doodson Feb 27, 2023
0dc87b1
Merge branch 'move-requires-validation-to-validate-document' into cha…
Liam-Doodson Feb 27, 2023
cac7597
Apply docs suggestions from code review
Liam-Doodson Feb 27, 2023
ff2c175
Update packages/graphql/src/schema/get-custom-resolver-meta.ts
Liam-Doodson Feb 27, 2023
bfa890f
Merge branch '4.0.0' into change-customResolver-requires-to-a-selecti…
Liam-Doodson Feb 27, 2023
723a084
Docs update
Liam-Doodson Feb 27, 2023
3c894fe
Bugfix
Liam-Doodson Feb 27, 2023
c5a5127
Custom SelectionSet scalar
Liam-Doodson Feb 27, 2023
c0ae612
Removed check for removed directive
Liam-Doodson Feb 27, 2023
409d5f3
PR comments
Liam-Doodson Feb 28, 2023
7462df8
Apply suggestions from code review
Liam-Doodson Feb 28, 2023
ded2b68
PR comments
Liam-Doodson Feb 28, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/old-pigs-float.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@neo4j/graphql": major
---

The `requires` argument of the `@customResolver` directive now accepts a graphql selection set. This means it is now possible to require non-scalar fields such as related types.
125 changes: 111 additions & 14 deletions docs/modules/ROOT/pages/custom-resolvers.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ 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]
Expand All @@ -13,7 +16,7 @@ const typeDefs = `
type User {
firstName: String!
lastName: String!
fullName: String! @customResolver(requires: ["firstName", "lastName"])
fullName: String! @customResolver(requires: "firstName lastName")
}
`;

Expand All @@ -35,14 +38,6 @@ 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.

[[custom-resolver-directive]]
=== `@customResolver`

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.

==== Definition

[source, graphql, indent=0]
Expand All @@ -54,6 +49,112 @@ directive @customResolver(
) on FIELD_DEFINITION
----

==== The `requires` argument

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.

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`:

[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)
}
----

==== 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:
Expand Down Expand Up @@ -107,8 +208,4 @@ const neoSchema = new Neo4jGraphQL({
},
},
})
----

== 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.
----
69 changes: 69 additions & 0 deletions docs/modules/ROOT/pages/guides/v4-migration/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,75 @@ type query {

Additionally, escaping strings is no longer needed.

=== `@customResolver` 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.

Therefore, the following type definitions:

[source, graphql, indent=0]
----
type User {
firstName: String!
lastName: String!
fullName: String! @customResolver(requires: ["firstName", "lastName"])
}
----

Would need to be modified to use a selection set as below:

[source, graphql, indent=0]
----
type User {
firstName: String!
lastName: String!
fullName: String! @customResolver(requires: "firstName lastName")
}
----

Below is a more advanced example showing what the selection set is capable of:

[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)
}
----

Additionally, the requires argument also validates the required selection set against your type definitions.
Therefore, as there is no field called `someFieldThatDoesNotExist`, an error would be thrown on startup if you tried to use the following type definitions:

[source, graphql, indent=0]
----
type User {
firstName: String!
lastName: String!
fullName: String! @customResolver(requires: "firstName someFieldThatDoesNotExist")
}
----


== Miscellaneous changes

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,13 @@ export const typeDefs = gql`
}

type Query {
product(id: ID!): Product @cypher(statement: "MATCH (product:Product) WHERE product.id = $id RETURN product")
product(id: ID!): Product
@cypher(statement: "MATCH (product:Product) WHERE product.id = $id RETURN product", columnName: "product")
deprecatedProduct(sku: String!, package: String!): DeprecatedProduct
@deprecated(reason: "Use product query instead")
@cypher(
statement: "MATCH (product:DeprecatedProduct) WHERE product.sku = $sku AND product.package = $package = $id RETURN product"
columnName: "product"
)
}

Expand Down
31 changes: 14 additions & 17 deletions packages/graphql/src/classes/Neo4jGraphQL.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import type {
Neo4jGraphQLPlugins,
Neo4jGraphQLCallbacks,
Neo4jFeaturesSettings,
StartupValidationConfig
StartupValidationConfig,
} from "../types";
import { makeAugmentedSchema } from "../schema";
import type Node from "./Node";
Expand Down Expand Up @@ -195,7 +195,7 @@ class Neo4jGraphQL {
driver,
driverConfig,
nodes: this.nodes,
options: input.options
options: input.options,
});
}

Expand Down Expand Up @@ -225,7 +225,7 @@ class Neo4jGraphQL {

private async getNeo4jDatabaseInfo(driver: Driver, driverConfig?: DriverConfig): Promise<Neo4jDatabaseInfo> {
const executorConstructorParam: ExecutorConstructorParam = {
executionContext: driver
executionContext: driver,
};

if (driverConfig?.database) {
Expand All @@ -250,13 +250,13 @@ class Neo4jGraphQL {
nodes: this.nodes,
relationships: this.relationships,
schemaModel: this.schemaModel,
plugins: this.plugins
plugins: this.plugins,
};

const resolversComposition = {
"Query.*": [wrapResolver(wrapResolverArgs)],
"Mutation.*": [wrapResolver(wrapResolverArgs)],
"Subscription.*": [wrapSubscription(wrapResolverArgs)]
"Subscription.*": [wrapSubscription(wrapResolverArgs)],
};

// Merge generated and custom resolvers
Expand All @@ -270,17 +270,15 @@ class Neo4jGraphQL {

const { validateTypeDefs, validateResolvers } = this.parseStartupValidationConfig();

if (validateTypeDefs) {
validateDocument(document);
}
validateDocument(document, validateTypeDefs);

const { nodes, relationships, typeDefs, resolvers } = makeAugmentedSchema(document, {
features: this.features,
enableRegex: this.config?.enableRegex,
validateResolvers,
generateSubscriptions: Boolean(this.plugins?.subscriptions),
callbacks: this.config.callbacks,
userCustomResolvers: this.resolvers
userCustomResolvers: this.resolvers,
});

this.schemaModel = generateModel(document);
Expand All @@ -293,7 +291,7 @@ class Neo4jGraphQL {

const schema = makeExecutableSchema({
typeDefs,
resolvers: wrappedResolvers
resolvers: wrappedResolvers,
});

resolve(this.addDefaultFieldResolvers(schema));
Expand All @@ -310,17 +308,16 @@ class Neo4jGraphQL {

const { validateTypeDefs, validateResolvers } = this.parseStartupValidationConfig();

if (validateTypeDefs) {
validateDocument(document, directives, types);
}
validateDocument(document, validateTypeDefs, directives, types);

const { nodes, relationships, typeDefs, resolvers } = makeAugmentedSchema(document, {
features: this.features,
enableRegex: this.config?.enableRegex,
validateResolvers,
generateSubscriptions: Boolean(this.plugins?.subscriptions),
callbacks: this.config.callbacks,
userCustomResolvers: this.resolvers,
subgraph
subgraph,
});

this.schemaModel = generateModel(document);
Expand All @@ -334,7 +331,7 @@ class Neo4jGraphQL {

const schema = subgraph.buildSchema({
typeDefs: subgraphTypeDefs,
resolvers: wrappedResolvers as Record<string, any>
resolvers: wrappedResolvers as Record<string, any>,
});

return this.addDefaultFieldResolvers(schema);
Expand All @@ -350,7 +347,7 @@ class Neo4jGraphQL {
if (this.config?.startupValidation === false) {
return {
validateTypeDefs: false,
validateResolvers: false
validateResolvers: false,
};
}

Expand All @@ -364,7 +361,7 @@ class Neo4jGraphQL {

return {
validateTypeDefs,
validateResolvers
validateResolvers,
};
}

Expand Down
7 changes: 4 additions & 3 deletions packages/graphql/src/graphql/directives/customResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
* limitations under the License.
*/

import { DirectiveLocation, GraphQLDirective, GraphQLList, GraphQLNonNull, GraphQLString } from "graphql";
import { DirectiveLocation, GraphQLDirective } from "graphql";
import { GraphQLSelectionSet } from "../scalars/SelectionSet";

export const customResolverDirective = new GraphQLDirective({
name: "customResolver",
Expand All @@ -27,8 +28,8 @@ export const customResolverDirective = new GraphQLDirective({
args: {
requires: {
description:
"Fields that the custom resolver will depend on. These are passed as an object to the first argument of the custom resolver.",
type: new GraphQLList(new GraphQLNonNull(GraphQLString)),
"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.",
type: GraphQLSelectionSet,
},
},
});
Loading