Custom resolvers
The Neo4j GraphQL Library generates query and mutation resolvers, so you don’t need to implement them yourself. However, if you need additional behaviors besides the autogenerated CRUD operations, you can specify custom resolvers for these scenarios. This page describes how to set them up.
@customResolver
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 and define a custom resolver for it.
Take, for instance, this schema:
const typeDefs = `
type User {
firstName: String!
lastName: String!
fullName: String! @customResolver(requires: "firstName lastName")
}
`;
const resolvers = {
User: {
fullName(source) {
return `${source.firstName} ${source.lastName}`;
},
},
};
const neoSchema = new Neo4jGraphQL({
typeDefs,
resolvers,
});
Here fullName
is a value that is resolved from the fields firstName
and lastName
.
Specifying the @customResolver
directive on the field definition keeps fullName
from being included in any Query
or Mutation
fields and hence as a property on the :User
node in the database.
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
"""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
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:
interface UserInterface {
fullName: String! @customResolver
}
type User implements UserInterface {
id: ID!
fullName: String!
}
The following resolvers definition would cause a warning to be logged:
const resolvers = {
UserInterface: {
fullName() {
return "Hello World!";
},
},
};
The following resolvers definition would silence the warning:
const resolvers = {
User: {
fullName() {
return "Hello World!";
},
},
};
Mismatches between the resolver map and @customResolver
directives are always logged to the console as a warning.
The requires
argument
This is how the requires
argument can be used on your schema:
-
Accepts a selection set string.
-
Can be used in any field, as long as it is not another
@customResolver
field. -
Should be used in case the custom resolver depends on any fields. This ensures that, during the Cypher generation process, these properties are selected from the database.
Using a selection set string makes it possible to select fields from related types, as shown in the following example:
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,
});
Here the firstName
, lastName
, address.street
, and address.city
fields are always selected from the database if the fullName
field is selected and is available to the custom resolver.
It is also possible to inline fragments to conditionally select fields from interface/union types:
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)
}
However, 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
:
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)
}