neo4j / graphql

A GraphQL to Cypher query execution layer for Neo4j and JavaScript GraphQL implementations.
https://neo4j.com/docs/graphql-manual/current/
Apache License 2.0
504 stars 149 forks source link

Custom resolver not found after migration from @computed to @customResolver #2717

Closed lukasbals closed 1 year ago

lukasbals commented 1 year ago

Describe the bug After the migration from the @computed directive to the @customResolver directive it throws an error Error: Custom resolver for status has not been provided.

The only change is from status: InvitationStatus! @computed(from: ["createdAt", "isAccepted"]) to status: InvitationStatus! @customResolver(requires: ["createdAt", "isAccepted"]).

Type definitions

Part of the type definition:

const invitation = gql`
  type Invitation @exclude {
    id: ID! @id
    invitedEmail: String!
    isAccepted: Boolean! @default(value: false) @readonly
    isInvited: [User!]! @relationship(type: "IS_INVITED", direction: IN)
    invitedBy: User! @relationship(type: "INVITED_BY", direction: IN) @private
    invitedTo: Project @relationship(type: "INVITED_TO", direction: IN)

    status: InvitationStatus! @customResolver(requires: ["createdAt", "isAccepted"])

    createdAt: DateTime! @timestamp(operations: [CREATE])
    updatedAt: DateTime! @timestamp(operations: [CREATE, UPDATE])
  }

  enum InvitationStatus {
    PENDING
    ACCEPTED
    EXPIRED
  }
`;

Part of the resolvers:

const resolvers = {
  ...

  Invitation: { status },
};

To Reproduce Steps to reproduce the behavior:

  1. Run a server with the typeDefs and resolvers above (resolver function is not included in the code snippets)
  2. The following error message is thrown:
/Users/user/code/api/node_modules/@neo4j/graphql/dist/schema/get-custom-resolver-meta.js:44
        throw new Error(`Custom resolver for ${field.name.value} has not been provided`);
              ^

Error: Custom resolver for status has not been provided
    at getCustomResolverMeta (/Users/user/code/api/node_modules/@neo4j/graphql/dist/schema/get-custom-resolver-meta.js:44:15)
    at obj.fields.reduce.relationFields (/Users/user/code/api/node_modules/@neo4j/graphql/dist/schema/get-obj-field-meta.js:59:75)
    at Array.reduce (<anonymous>)
    at getObjFieldMeta (/Users/user/code/api/node_modules/@neo4j/graphql/dist/schema/get-obj-field-meta.js:44:25)
    at /Users/user/code/api/node_modules/@neo4j/graphql/dist/schema/get-nodes.js:82:61
    at Array.map (<anonymous>)
    at getNodes (/Users/user/code/api/node_modules/@neo4j/graphql/dist/schema/get-nodes.js:39:47)
    at makeAugmentedSchema (/Users/user/code/api/node_modules/@neo4j/graphql/dist/schema/make-augmented-schema.js:128:52)
    at /Users/user/code/api/node_modules/@neo4j/graphql/dist/classes/Neo4jGraphQL.js:155:100
    at new Promise (<anonymous>)

Expected behavior The app runs without throwing the error.

Screenshots Not needed.

System (please complete the following information):

neo4j-team-graphql commented 1 year ago

Many thanks for raising this bug report @lukasbals. :bug: We will now attempt to reproduce the bug based on the steps you have provided.

Please ensure that you've provided the necessary information for a minimal reproduction, including but not limited to:

If you have a support agreement with Neo4j, please link this GitHub issue to a new or existing Zendesk ticket.

Thanks again! :pray:

neo4j-team-graphql commented 1 year ago

Many thanks for raising this bug report @lukasbals. :bug: We will now attempt to reproduce the bug based on the steps you have provided.

Please ensure that you've provided the necessary information for a minimal reproduction, including but not limited to:

If you have a support agreement with Neo4j, please link this GitHub issue to a new or existing Zendesk ticket.

Thanks again! :pray:

tbwiss commented 1 year ago

Hi @lukasbals! Thanks for reaching out. When I tried to reproduce this bug I actually got a different error message Error: "Invitation" defined in resolvers, but not in schema which is due to the @exclude directive of the type Invitation. If I remove the @exclude directive no error gets thrown and the server starts up. Do you experience the same when you try it on your end?

lukasbals commented 1 year ago

Hi @lukasbals! Thanks for reaching out. When I tried to reproduce this bug I actually got a different error message Error: "Invitation" defined in resolvers, but not in schema which is due to the @exclude directive of the type Invitation. If I remove the @exclude directive no error gets thrown and the server starts up. Do you experience the same when you try it on your end?

Hi @tbwiss,

thanks for reproducing it. I already tried to remove the @exlude and it doesn't change anything for me.

One interesting thing: If I log the customResolvers in the node_modules/@neo4j/graphql/dist/schema/get-custom-resolver-meta.js file in the getCustomResolverMeta function I get undefined which seems to be wrong ... and the reason why the error is thrown.

tbwiss commented 1 year ago

Hmm yes, that's interesting @lukasbals. That could be the reason why you get the error.

Just for completion's sake, here is the full code I used to reproduce the reported bug:

import { Neo4jGraphQL } from "@neo4j/graphql";
import * as neo4j from "neo4j-driver";
import { ApolloServer, gql } from "apollo-server";

const typeDefs = gql`
    type Invitation @exclude {
        id: ID! @id
        invitedEmail: String!
        isAccepted: Boolean! @default(value: false) @readonly
        isInvited: [User!]! @relationship(type: "IS_INVITED", direction: IN)
        invitedBy: User! @relationship(type: "INVITED_BY", direction: IN) @private
        invitedTo: Project @relationship(type: "INVITED_TO", direction: IN)

        status: InvitationStatus! @customResolver(requires: ["createdAt", "isAccepted"])

        createdAt: DateTime! @timestamp(operations: [CREATE])
        updatedAt: DateTime! @timestamp(operations: [CREATE, UPDATE])
    }

    type User {
        name: String
    }

    type Project {
        name: String
    }

    enum InvitationStatus {
        PENDING
        ACCEPTED
        EXPIRED
    }
`;

async function run() {
    const driver = neo4j.driver(
        "neo4j://localhost:7687",
        neo4j.auth.basic("XXXXX", "XXXXXXXX")
    );

    const resolvers = {
        Invitation: { status: () => "text" },
    };

    const neoSchema = new Neo4jGraphQL({
        typeDefs: typeDefs,
        driver,
        resolvers,
    });

    const schema = await neoSchema.getSchema();

    const server = new ApolloServer({
        schema,
        sandbox: false,
        context: ({ req }) => ({ req }),
    });

    server.listen().then(() => console.log("Online in port 4000"));
}
run();

With these dependencies (same as you posted above):

  "dependencies": {
    "@neo4j/graphql": "^3.0.3",
    "apollo-server": "^3.11.1",
    "neo4j-driver": "^5.0.1"
  }
lukasbals commented 1 year ago

Hi @tbwiss,

one thing I found out: If I uncomment the if in line 42 of the node_modules/@neo4j/graphql/dist/schema/get-custom-resolver-meta.js file the server starts up and works fine. If I log the customResolvers then they are not undefined (at least in the second log that appears).

See image and test output below:

Screenshot 2023-01-12 at 17 18 50

Logs when running the tests:

 PASS  test/integration/invitation/invitations.test.ts
  ● Console

    console.log
      CR:  { status: [Function: status] }

      at getCustomResolverMeta (node_modules/@neo4j/graphql/src/schema/get-custom-resolver-meta.ts:65:5)
          at Array.reduce (<anonymous>)
          at Array.map (<anonymous>)

    console.log
      CR:  undefined

      at getCustomResolverMeta (node_modules/@neo4j/graphql/src/schema/get-custom-resolver-meta.ts:65:5)
          at Array.reduce (<anonymous>)
          at Array.map (<anonymous>)

So for me it seems like this getCustomResolverMeta function is called multiple times and the first time it is called on startup the customResolvers are missing (undefined). What do you think?

One more thing that comes to my mind: We are using apollo-server-express and not apollo-server ... but I assume this shouldn't be a problem 🤔

tbwiss commented 1 year ago

Okay @lukasbals. To debug this some more, could you check/log if the customResolvers variable contains any data here in this get-nodes.ts file: https://github.com/neo4j/graphql/blob/dev/packages/graphql/src/schema/get-nodes.ts#L124 ?

Also, if possible, could you please share more details of your server code? I mean the code that in my snippet above is located in the run() function. To double check if there is something there that may cause your problem.

lukasbals commented 1 year ago

Hi @tbwiss,

thanks for your investigations!

This is the output if I log the customResolvers in the get-nodes.js file:

Determining test suites to run...Custom resolvers in get-nodes:  undefined
Custom resolvers in get-custom-resolver:  undefined
Custom resolvers in get-nodes:  undefined
Custom resolvers in get-nodes:  undefined
Custom resolvers in get-nodes:  undefined
Custom resolvers in get-nodes:  undefined
Custom resolvers in get-nodes:  undefined
Custom resolvers in get-nodes:  undefined
Custom resolvers in get-nodes:  undefined
Custom resolvers in get-nodes:  undefined
Custom resolvers in get-nodes:  undefined
Custom resolvers in get-nodes:  undefined
Custom resolvers in get-nodes:  undefined
  console.log
    Custom resolvers in get-nodes:  undefined

      at node_modules/@neo4j/graphql/src/schema/get-nodes.ts:139:9
          at Array.map (<anonymous>)

  console.log
    Custom resolvers in get-custom-resolver:  { status: [Function: status] }

      at getCustomResolverMeta (node_modules/@neo4j/graphql/src/schema/get-custom-resolver-meta.ts:65:5)
          at Array.reduce (<anonymous>)
          at Array.map (<anonymous>)

  console.log
    Custom resolvers in get-nodes:  { status: [Function: status] }

      at node_modules/@neo4j/graphql/src/schema/get-nodes.ts:139:9
          at Array.map (<anonymous>)

  console.log
    Custom resolvers in get-nodes:  undefined

      at node_modules/@neo4j/graphql/src/schema/get-nodes.ts:139:9
          at Array.map (<anonymous>)

  console.log
    Custom resolvers in get-nodes:  undefined

      at node_modules/@neo4j/graphql/src/schema/get-nodes.ts:139:9
          at Array.map (<anonymous>)

  console.log
    Custom resolvers in get-nodes:  undefined

      at node_modules/@neo4j/graphql/src/schema/get-nodes.ts:139:9
          at Array.map (<anonymous>)

  console.log
    Custom resolvers in get-nodes:  undefined

      at node_modules/@neo4j/graphql/src/schema/get-nodes.ts:139:9
          at Array.map (<anonymous>)

  console.log
    Custom resolvers in get-nodes:  undefined

      at node_modules/@neo4j/graphql/src/schema/get-nodes.ts:139:9
          at Array.map (<anonymous>)

  console.log
    Custom resolvers in get-nodes:  undefined

      at node_modules/@neo4j/graphql/src/schema/get-nodes.ts:139:9
          at Array.map (<anonymous>)

  console.log
    Custom resolvers in get-nodes:  undefined

      at node_modules/@neo4j/graphql/src/schema/get-nodes.ts:139:9
          at Array.map (<anonymous>)

  console.log
    Custom resolvers in get-nodes:  undefined

      at node_modules/@neo4j/graphql/src/schema/get-nodes.ts:139:9
          at Array.map (<anonymous>)

  console.log
    Custom resolvers in get-nodes:  undefined

      at node_modules/@neo4j/graphql/src/schema/get-nodes.ts:139:9
          at Array.map (<anonymous>)

  console.log
    Custom resolvers in get-nodes:  undefined

      at node_modules/@neo4j/graphql/src/schema/get-nodes.ts:139:9
          at Array.map (<anonymous>)

  console.log
    Custom resolvers in get-nodes:  undefined

      at node_modules/@neo4j/graphql/src/schema/get-nodes.ts:139:9
          at Array.map (<anonymous>)

  console.log
    Custom resolvers in get-custom-resolver:  undefined

      at getCustomResolverMeta (node_modules/@neo4j/graphql/src/schema/get-custom-resolver-meta.ts:65:5)
          at Array.reduce (<anonymous>)
          at Array.map (<anonymous>)

  console.log
    Custom resolvers in get-nodes:  undefined

      at node_modules/@neo4j/graphql/src/schema/get-nodes.ts:139:9
          at Array.map (<anonymous>)

  console.log
    Custom resolvers in get-nodes:  undefined

      at node_modules/@neo4j/graphql/src/schema/get-nodes.ts:139:9
          at Array.map (<anonymous>)

  console.log
    Custom resolvers in get-nodes:  undefined

      at node_modules/@neo4j/graphql/src/schema/get-nodes.ts:139:9
          at Array.map (<anonymous>)

  console.log
    Custom resolvers in get-nodes:  undefined

      at node_modules/@neo4j/graphql/src/schema/get-nodes.ts:139:9
          at Array.map (<anonymous>)

  console.log
    Custom resolvers in get-nodes:  undefined

      at node_modules/@neo4j/graphql/src/schema/get-nodes.ts:139:9
          at Array.map (<anonymous>)

  console.log
    Custom resolvers in get-nodes:  undefined

      at node_modules/@neo4j/graphql/src/schema/get-nodes.ts:139:9
          at Array.map (<anonymous>)

  console.log
    Custom resolvers in get-nodes:  undefined

      at node_modules/@neo4j/graphql/src/schema/get-nodes.ts:139:9
          at Array.map (<anonymous>)

  console.log
    Custom resolvers in get-nodes:  undefined

      at node_modules/@neo4j/graphql/src/schema/get-nodes.ts:139:9
          at Array.map (<anonymous>)

  console.log
    Custom resolvers in get-nodes:  undefined

      at node_modules/@neo4j/graphql/src/schema/get-nodes.ts:139:9
          at Array.map (<anonymous>)

  console.log
    Custom resolvers in get-nodes:  undefined

      at node_modules/@neo4j/graphql/src/schema/get-nodes.ts:139:9
          at Array.map (<anonymous>)

  console.log
    Custom resolvers in get-nodes:  undefined

      at node_modules/@neo4j/graphql/src/schema/get-nodes.ts:139:9
          at Array.map (<anonymous>)

Here our server.ts file:

import * as dotenv from 'dotenv';
dotenv.config();

import { Neo4jGraphQL } from '@neo4j/graphql';
import neo4j, { Driver } from 'neo4j-driver';
import typeDefs from './typeDefs';
import resolvers from './resolvers';
import { generate, OGM } from '@neo4j/graphql-ogm';
import express, { Request, Response } from 'express';
import { ApolloServer } from 'apollo-server-express';
import compression from 'compression';
import cors from 'cors';
import helmet from 'helmet';
import path from 'path';
import { createUser, createUserValidator } from './webhooks/users';
import { Neo4jGraphQLAuthJWTPlugin } from '@neo4j/graphql-plugin-auth';
import authMiddleware from './middleware/authMiddleware';
import { downloadDocument } from './downloads/documents';
import { ModelMap } from './types/generated';
import { setupGoogleDriveClient } from './integrations/googleDrive';

const app = express();
app.use(cors());
app.use(helmet({ contentSecurityPolicy: false }));
app.use(compression());
app.use(express.json());

const driver: Driver = neo4j.driver(
  process.env.NEO4J_URI || 'bolt://localhost:7687',
  neo4j.auth.basic(process.env.NEO4J_USER || 'neo4j', process.env.NEO4J_PASSWORD || 'letmein')
);

const auth = new Neo4jGraphQLAuthJWTPlugin({
  // When testing the token should not ve verified because the token for testing is expired and
  // we don't want to be forced to set the AUTH0_SECRET for testing
  noVerify: process.env.NODE_ENV === 'test',
  secret: process.env.AUTH0_SECRET ? process.env.AUTH0_SECRET.replace(/\\n/g, '\n') : '',
  rolesPath: 'permissions',
});
const drive = setupGoogleDriveClient();
const ogm = new OGM<ModelMap>({ typeDefs, driver });

const neoSchema: Neo4jGraphQL = new Neo4jGraphQL({
  typeDefs,
  resolvers,
  driver,
  plugins: { auth },
});

app.get('/.well-known/apollo/server-ready', (req: Request, res: Response) => {
  res.json({ status: 'pass' });
});

export const startApolloServer = async (): Promise<ApolloServer> => {
  const schema = await neoSchema.getSchema();
  const server = new ApolloServer({
    schema: schema,
    context: ({ req }) => ({ req }),
    cache: 'bounded',
  });

  await ogm.init();
  await server.start();

  server.applyMiddleware({
    app,
    path: '/',
    onHealthCheck: async () => {
      try {
        await driver.verifyConnectivity();
      } catch (error) {
        console.error('Health check failed');
        throw new Error('Unable to verify connection.');
      }
    },
  });

  const port: number = parseInt(process.env.GRAPHQL_LISTEN_PORT || '4001');

  if (process.env.NODE_ENV !== 'test' && !process.env.GENERATE) {
    app.listen({ port }, (): void => console.log(`🚀 Server ready at localhost:${port}`));
  }

  return server;
};

export { driver, ogm, auth, drive };

if (process.env.NODE_ENV !== 'test') {
  app.post('/webhooks/users', createUserValidator, createUser);
  app.get('/downloads/documents/:document', authMiddleware, downloadDocument);
  startApolloServer();
}
tbwiss commented 1 year ago

Thank you @lukasbals. There is nothing in your server code that strikes me as wrong.

One more thing you can check, is the this.schemaDefinition.resolvers variable in this file https://github.com/neo4j/graphql/blob/dev/packages/graphql/src/classes/Neo4jGraphQL.ts#L236 is undefined as well?

If this.schemaDefinition.resolvers is undefined it means the resolvers passed in here:

const neoSchema: Neo4jGraphQL = new Neo4jGraphQL({
  typeDefs,
  resolvers,
  driver,
  plugins: { auth },
});

are undefined.

lukasbals commented 1 year ago

hi @tbwiss,

I added a console.log in the generateSchema method of the Neo4jGraphQL class and got the following result:

Determining test suites to run...Custom resolvers in the Neo4jGraphQL class genereateSchema:  undefined
Custom resolvers in get-custom-resolver:  undefined
  console.log
    Custom resolvers in the Neo4jGraphQL class genereateSchema:  {
      Mutation: {
        claimPlots: [AsyncFunction: claimPlots],
        createReport: [AsyncFunction: createNewReport],
        unClaimPlots: [AsyncFunction: unClaimPlots],
        acceptInvitation: [AsyncFunction: acceptInvitation],
        inviteUserToProject: [AsyncFunction: inviteUserToProject],
        downloadDocument: [AsyncFunction: downloadDocument],
        getInTouch: [AsyncFunction: getInTouch]
      },
      Query: {
        documents: [AsyncFunction: documents],
        checkPlot: [AsyncFunction: checkPlot],
        plotsDetail: [AsyncFunction: plotsDetail],
        searchPlots: [AsyncFunction: searchPlots],
        invitations: [AsyncFunction: invitations],
        getReports: [AsyncFunction: getReports]
      },
      Invitation: { status: [Function: status] }
    }

      at Neo4jGraphQL.generateSchema (node_modules/@neo4j/graphql/src/classes/Neo4jGraphQL.ts:227:16)

  console.log
    Custom resolvers in get-custom-resolver:  { status: [Function: status] }

      at getCustomResolverMeta (node_modules/@neo4j/graphql/src/schema/get-custom-resolver-meta.ts:65:5)
          at Array.reduce (<anonymous>)
          at Array.map (<anonymous>)

  console.log
    Custom resolvers in the Neo4jGraphQL class genereateSchema:  undefined

      at Neo4jGraphQL.generateSchema (node_modules/@neo4j/graphql/src/classes/Neo4jGraphQL.ts:227:16)

  console.log
    Custom resolvers in get-custom-resolver:  undefined

      at getCustomResolverMeta (node_modules/@neo4j/graphql/src/schema/get-custom-resolver-meta.ts:65:5)
          at Array.reduce (<anonymous>)
          at Array.map (<anonymous>)

It's also undefined on the first call but at some point, it's defined 🤔

tbwiss commented 1 year ago

@lukasbals I might have found the issue. In your server code above, you use the @neo4j/graphql OGM. Could you try passing the resolvers there too? Like:

const ogm = new OGM<ModelMap>({ typeDefs, driver, resolvers });

Background: The OGM uses the Neo4jGraphQL class under the hood which may cause the error you experience.

lukasbals commented 1 year ago

Hi @tbwiss,

thanks for the hint! It seems like it helps. The app starts and runs, but unfortunately, it leads to a new error that makes no sense to me. When calling a query that has a custom resolver and in the custom resolver I use the OGM to query something I get an Unauthenticated error:

@neo4j/graphql:auth Could not get .req or .request from context +0ms
Error: Unauthenticated
    at Model.find (/Users/lukasbals/treely/api/node_modules/@neo4j/graphql-ogm/src/classes/Model.ts:149:19)
    at processTicksAndRejections (node:internal/process/task_queues:96:5)
    at async invitations (/Users/lukasbals/treely/api/src/resolvers/invitations/invitations.ts:37:25)

Do you have any idea what's the problem here?

tbwiss commented 1 year ago

Great! Hmm that's interesting @lukasbals. I see in your server code above that you already pass the request in the context of the ApolloServer:

const server = new ApolloServer({
    schema: schema,
    context: ({ req }) => ({ req }),
    cache: 'bounded',
  });

That's usually what gets forgotten and causes a similar error.

However, I see that it errs in the Model.find() which is in the OGM. Is it possible, haven't tried that myself, that you can pass the request in the context of the Model.find() method (ref: https://github.com/neo4j/graphql/blob/dev/packages/ogm/src/classes/Model.ts#L90-L98)?

lukasbals commented 1 year ago

Thanks @tbwiss!

I tried to pass the context that I get in the custom resolver to the Model.find() call. This resulted in an infinite loop.

How can I pass just the request to the Model.find() call in a custom resolver? Can you please give me an example?

tbwiss commented 1 year ago

@lukasbals, okay! Does any of this here help: https://github.com/neo4j/graphql/blob/dev/packages/ogm/tests/integration/additional-labels.int.test.ts#L67-L88 ?

lukasbals commented 1 year ago

Hi @tbwiss,

thanks!

I think I know what the problem is.

If I pass the resolvers to the OGM and I use the OGM inside a custom resolver I run into an infinite loop since the custom resolver calls itself again and again. The solution I found, for now, is to exclude the custom Query and Mutation resolvers when passing the resolvers to the OGM. See an example here:

const ogm = new OGM<ModelMap>({
  typeDefs,
  resolvers: { ...resolvers, Query: {}, Mutation: {} },
  driver,
});

What do you think? Should that maybe be part of the OGM library, to exclude the Query and Mutation resolvers?

tbwiss commented 1 year ago

Hi @lukasbals, glad you found a solution!

To be honest I'm not entirely sure as I'm not too familiar with the OGM but I'll bring this up with the team.

Thanks!

tbwiss commented 1 year ago

@lukasbals, we're currently working on a feature that may mitigate or address this: https://github.com/neo4j/graphql/pull/2761 We'll also look into the infinite loop issue as part of that linked feature.

lukasbals commented 1 year ago

@tbwiss This would fix the issue. But I would rather skip the validation on the ORM initialization and not on the Neo4jGraphql initialization.

tbwiss commented 1 year ago

You should be able to do something along the line of:

const ogm = new OGM<ModelMap>({
  typeDefs,
  resolvers,
  driver,
  config: {
      startupValidation: {
          typeDefs: true,
          resolvers: false,
      },
  },
});

when https://github.com/neo4j/graphql/pull/2761 is released.

tbwiss commented 1 year ago

Closing this bug report as this can be addressed with the changes in https://github.com/neo4j/graphql/pull/2761. Please feel free to re-open this issue otherwise.