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

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