neo4j-graphql / neo4j-graphql-js

NOTE: This project is no longer actively maintained. Please consider using the official Neo4j GraphQL Library (linked in README).
Other
608 stars 148 forks source link

How to use @cypher directive to return DateTime type? #473

Open steezeburger opened 4 years ago

steezeburger commented 4 years ago

I may have found a bug.

imagine SomeType with property:

startDateTime: DateTime @cypher(statement:"""
   WITH datetime('2020-01-25') as startDateTime
   RETURN startDateTime
""")

for the following query:

{
  SomeType {
    startDateTime {
      year
      month
      day
      formatted
    }
  }
}

I receive the following error:


{
  "errors": [
    {
      "message": "2020-01-25T00:00:00Z (of class org.neo4j.values.storable.DateTimeValue)",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": [
        "SomeType"
      ],
      "extensions": {
        "code": "INTERNAL_SERVER_ERROR",
        "exception": {
          "code": "Neo.DatabaseError.General.UnknownError",
          "name": "Neo4jError",
          "stacktrace": [
            "Neo4jError: 2020-01-25T00:00:00Z (of class org.neo4j.values.storable.DateTimeValue)",
            "",
            "    at captureStacktrace (/Users/code/proj/node_modules/neo4j-driver/lib/result.js:275:15)",
            "    at new Result (/Users/code/proj/node_modules/neo4j-driver/lib/result.js:66:19)",
            "    at newCompletedResult (/Users/code/proj/node_modules/neo4j-driver/lib/transaction.js:446:10)",
            "    at Object.run (/Users/code/proj/node_modules/neo4j-driver/lib/transaction.js:285:14)",
            "    at Transaction.run (/Users/code/proj/node_modules/neo4j-driver/lib/transaction.js:121:32)",
            "    at /Users/code/proj/packages/api/node_modules/neo4j-graphql-js/dist/index.js:204:29",
            "    at TransactionExecutor._safeExecuteTransactionWork (/Users/code/proj/node_modules/neo4j-driver/lib/internal/transaction-executor.js:132:22)",
            "    at TransactionExecutor._executeTransactionInsidePromise (/Users/code/proj/node_modules/neo4j-driver/lib/internal/transaction-executor.js:120:32)",
            "    at /Users/code/proj/node_modules/neo4j-driver/lib/internal/transaction-executor.js:59:15",
            "    at new Promise (<anonymous>)",
            "    at TransactionExecutor.execute (/Users/code/proj/node_modules/neo4j-driver/lib/internal/transaction-executor.js:58:14)",
            "    at Session._runTransaction (/Users/code/proj/node_modules/neo4j-driver/lib/session.js:300:40)",
            "    at Session.readTransaction (/Users/code/proj/node_modules/neo4j-driver/lib/session.js:272:19)",
            "    at _callee$ (/Users/code/proj/packages/api/node_modules/neo4j-graphql-js/dist/index.js:203:32)",
            "    at tryCatch (/Users/code/proj/node_modules/regenerator-runtime/runtime.js:45:40)",
            "    at Generator.invoke [as _invoke] (/Users/code/proj/node_modules/regenerator-runtime/runtime.js:274:22)"
          ]
        }
      }
    }
  ],
  "data": {
    "SomeType": null
  }
}

Is the full error getting cut off? It doesn't even make sense to me haha.

I've also tried:

startDateTime: DateTime @cypher(statement:"""
   WITH datetime('2020-01-25') as startDateTime
   RETURN { 
     year: startDateTime.year,
     month: startDateTime.month,
     day: startDateTime.day,
     hour: startDateTime.hour,
     minute: startDateTime.minute,
     second: startDateTime.second,
     millisecond: startDateTime.millisecond,
     formatted: startDateTime
   }
""")

but I get null for every value:

{
  "startDateTime": {
    "year": null,
    "month": null,
    "day": null,
    "formatted": null
  }
}

I'm at a loss for how to use a @cypher directive to return a DateTime type. Any ideas?

steezeburger commented 4 years ago

I'm trying to implement a workaround, and custom resolvers for fields with @neo4j_ignore directives are receiving arguments of object = {}, ... so I'm not even able to build the datetime in javascript because I don't have the parent object data available in the custom resolver.

export const resolvers = {
  Mutation: {
    createSomeType,
    updateSomeType,
  },
  SessionRule: {
    startDateTime: async (object, params, context, info) => {
      const startDateTime = await runCypherQuery(
        context.driver,
        'MATCH (sr:SomeType {id: $id})-[:IN]->(s:SomeOtherType) RETURN s.start',
        { id: object.id },
      );

      const momentDate = moment.utc(startDateTime);
      const dateObj = momentDate.toObject();

      return {
        year: dateObj.years,
        month: dateObj.months,
        day: dateObj.date,
        hour: dateObj.hours,
        minute: dateObj.minutes,
        second: dateObj.seconds,
        millisecond: dateObj.milliseconds,
        formatted: momentDate.toISOString(),
      };
    },

  },
};

object.id is always undefined for queries. object is actually the parent object when the field is resolved for a mutation.

steezeburger commented 4 years ago

FYI, the following works properly, so I know it's not an issue with my cypher.

    startDateTime: Int @cypher(statement:"""
       MATCH (this)-[:IN]->(s:SomeOtherType)
       WITH s.start as startDateTime
       RETURN startDateTime.year
    """)
dmoree commented 4 years ago

I am having the same issue. I could not use the cypher directive to achieve this; I am getting the same error. One could implement a utility function that takes a cypher fragment and resolves to a _Neo4jDateTime object. An example would be:

const neo4jDateTimeResolver = ({
  fragment, // A fragment of a cypher query such as  MATCH (this)-[:IN]->(s:SomeOtherType)
  field, // such as 's.start'
  parameters = {},
  order = 'DESC',
}) => async (node, _, { cypherParams, driver }) => {
  try {
    const session = driver.session()
    const neo4jDateTime = await session.readTransaction(async txc => {
      const { records } = await txc.run(`
        MATCH (this:Node {id: $_nodeId})
        ${fragment}
        WITH ${field} as dt
        RETURN dt {.year, .month, .day, .hour, .minute, .second, .millisecond, .microsecond, .nanosecond, .timezone, formatted: apoc.date.toISO8601(dt.epochMillis)}
        ORDER BY dt ${order}
        LIMIT 1
      `,
        { _nodeId: node.id, cypherParams, ...parameters },
      )
      return records.map(record => record.get('dt'))[0]
    })
    await session.close()
    // neo4jDateTime will be of the form { year: Integer, month: Integer, ..., formatted: string} or undefined
    // Integer coming from the neo4j-driver here:
    // https://github.com/neo4j/neo4j-javascript-driver/blob/88e646d4664943e337aecb7d61c187407926cc46/src/integer.js#L39
    if (neo4jDateTime) {
      let object = {}
      // map this to a javascript object with integers using Integer.toInt()
      // https://github.com/neo4j/neo4j-javascript-driver/blob/88e646d4664943e337aecb7d61c187407926cc46/src/integer.js#L85
      Object.entries(neo4jDateTime).forEach(([key, value]) => {
        if (value.constructor.name === 'Integer') {
          object[key] = value.toInt() 
        } else {
          object[key] = value
        }
      })
      return object
    } else {
      return null
    }
  } catch (error) {
    // Do something with error
    return null
  }
}

First notice before the fragment I am matching a :Node with indexed id field. In the graph that I am using this is true for all types. The parent object node is an object that consists of the fields that were queried for. The id field must be queried along with the DateTime fields in order for this to work, but the client is always querying the id field for caching. Second, the matched node is called this to help with the fragment. It is not constructed the same way this library constructs this; it just reads the same. The cypherParams are passed as well.

In your example

type SessionRule {
  startDateTime: DateTime
}

then in resolvers use:

export const resolvers = {
  Mutation: {
    createSomeType,
    updateSomeType,
  },
  SessionRule: {
    startDateTime: neo4jDateTimeResolver({
      fragment: `MATCH (this)-[:IN]->(s:SomeOtherType)`,
      field: 's.start'
    }),
  },
};

I understand that the issue is asking a question about returning a DateTime object using the cypher directive. This is one way that you can return the DateTime object. If the schema is flexible maybe return a scalar like String in the form of an ISO8601 string using apoc. Something like:

type SessionRule {
  startDateTimeISO: String @cypher(statement:"""
    MATCH (this)-[:IN]->(s:SomeOtherType)
    WITH s.start as dt
    RETURN apoc.date.toISO8601(dt.epochMillis)
  """)
}

This is obviously only a workaround until the bug is worked out and my use cases may be simpler than yours, but I hope this helps.

dmoree commented 3 years ago

@michaeldgraham @johnymontana Has this been identified as a bug?

I am still experiencing this issue on

neo4j-driver: 4.2.1
neo4j-graphql-js: 2.19.1
michaeldgraham commented 3 years ago

https://github.com/neo4j-graphql/neo4j-graphql-js/issues/608