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
609 stars 147 forks source link

Potential bug adding {undefined} to @cypher final return clause #443

Open alexluna7 opened 4 years ago

alexluna7 commented 4 years ago

I have two mutations in GraphQL using @cypher custom code that are enclosed by additional code cypher code from neo4-graphql-js that is adding an {undefined} map to the final RETURN clause leading to an error.

When I run the same code (my part) directly in cypher-shell it runs smoothly.

Below is the output of node's console for both mutations:

1. neo4j-graphql-js CALL apoc.cypher.doIt("WITH $input as input FOREACH (language IN input | MERGE (l:Language{code:language.code}) FOREACH (name IN language.name | MERGE (nl:Language{code:name.language}) MERGE (l)-[:NAME_TRANSLATION]->(t:Translation)-[:IN_LANGUAGE]->(nl) ON CREATE SET t.id=apoc.create.uuid() SET t.text=name.text ) FOREACH (root IN language.root | MERGE (r:Language{code:root.code}) MERGE (l)-[:ROOT_LANGUAGE]->(r) ) ) WITH input UNWIND input AS language MATCH (l:Language{code:language.code})-[:NAME_TRANSLATION]->(t:Translation)-[:IN_LANGUAGE]->(nl:Language) OPTIONAL MATCH (l)-[:ROOT_LANGUAGE]->(r:Language) RETURN l", {input:$input, first:$first, offset:$offset}) YIELD value WITH apoc.map.values(value, [keys(value)[0]])[0] AS languagePayload RETURN languagePayload {undefined} AS languagePayload +0ms

2. neo4j-graphql-js CALL apoc.cypher.doIt("WITH $input as input MERGE (r:Root) ON CREATE SET r.id=apoc.create.uuid() FOREACH (country IN input | MERGE (c:Country{code:country.code}) FOREACH (name IN country.name | MERGE (l:Language{code:name.language}) MERGE (c)-[:NAME_TRANSLATION]->(t:Translation)-[:IN_LANGUAGE]->(l) ON CREATE SET t.id=apoc.create.uuid() SET t.text=name.text ) FOREACH (nid IN country.nid | MERGE (c)-[:HAS_NID{code:nid.code}]->(comp:Component)-[:CURRENT]->(ver:Version:Unique)-[:VERSION_OF]->(comp) ON CREATE SET comp.id=apoc.create.uuid(), ver.id=apoc.create.uuid() MERGE (comp)<-[:IS_AUTHOR]-(r) FOREACH (nname IN nid.name | MERGE (nl:Language{code:nname.language}) MERGE (comp)-[:NAME_TRANSLATION]->(nt:Translation)-[:IN_LANGUAGE]->(nl) ON CREATE SET nt.id=apoc.create.uuid() SET nt.text=nname.text ) ) FOREACH (official IN country.official | MERGE (ol:Language{code:official.code}) MERGE (c)-[:OFFICIAL_LANGUAGE]->(ol) ) FOREACH (supported IN country.supported | MERGE (sl:Language{code:supported.code}) MERGE (c)-[:SUPPORTED_LANGUAGE]->(sl) ) ) WITH input UNWIND input AS country MATCH (c:Country{code:country.code}) RETURN c", {input:$input, first:$first, offset:$offset}) YIELD value WITH apoc.map.values(value, [keys(value)[0]])[0] AS countryPayload RETURN countryPayload {undefined} AS countryPayload +159ms

Is this a bug?

johnymontana commented 4 years ago

Hi @alexluna7 can you please share the relevant bits of your GraphQL type defintions, including the @cypher schema directive and the GraphQL query that produces the error so we can try to replicate the problem>

jnterry commented 4 years ago

@johnymontana - I've also ran in this bug using the following GraphQL typedefs:

interface Entity {
    name: String!
}

type Place implements Entity {
    name: String!
    visitors: [Visited]
}

type Person implements Entity {
    name: String!
    visited: [Visited]
}

type Visited @relation(name: "VISITED") {
    from: Person
    to: Place
        # List of sentence IDs which include this action
    sentences: [String]
}

type Sentence {
    id: String!
    relations: [EntEntRel] @cypher(statement:"""
        MATCH (source:Entity)-[r]->(target:Entity)
        WHERE size([x IN r.sentences WHERE x = this.id ]) > 0
        RETURN r {
                     .*, type: type(r),
                     source: (source { .*, FRAGMENT_TYPE: head([ x IN labels(source) WHERE x <> 'Entity' ]) } ),
             target: (target { .*, FRAGMENT_TYPE: head([ x IN labels(target) WHERE x <> 'Entity' ]) } )
               }
    """)
}

# Fake type artificially constructed by cypher query
type EntEntRel {
    type: String!
    source: Entity
    target: Entity
    sentence: [String]
}

The following GraphQL query produces the error:

query {
  Sentence {
    id
    relations {
      type
      source { name }
      target { name }
    }
  }
}

Using DEBUG=neo4j-graphql-js shows that the generated Cypher query is:

MATCH (`sentence`:`Sentence`) RETURN `sentence` { .id ,relations: [ sentence_relations IN apoc.cypher.runFirstColumn("MATCH (source:Entity)-[r]->(target:Entity)
WHERE size([x IN r.sentences WHERE x = this.id ]) > 0
RETURN r {
       .*, type: type(r),
       source: (source { .*, FRAGMENT_TYPE: head([ x IN labels(source) WHERE x <> 'Entity' ]) } ),
       target: (target { .*, FRAGMENT_TYPE: head([ x IN labels(target) WHERE x <> 'Entity' ]) } )
  }", {this: sentence}, true) | sentence_relations { undefined }] } AS `sentence`

I've managed to track down the cause of this bug to the variable subQuery being undefined on this line: https://github.com/neo4j-graphql/neo4j-graphql-js/blob/master/src/translate.js#L1363

Updating the line from:

mapProjection = `${safeVariableName} {${subQuery}}`;

to

mapProjection = `${safeVariableName} {${subQuery || ' .* '}}`;

causes the graphql query to work as expected - but I don't know if this an acceptable fix (I don't have context on how this repo works to know why subQuery is undefined here

alexluna7 commented 4 years ago

Hi @alexluna7 can you please share the relevant bits of your GraphQL type defintions, including the @cypher schema directive and the GraphQL query that produces the error so we can try to replicate the problem>

@johnymontana You will find below the requested details:

The following mutations correspond to the node console output in my first post, the 1 is SetupLanguage and the 2 is SetupCountry.

extend type Mutation { SetupLanguage(input: [LanguageInput!]!): [LanguageResponse!]! @cypher( statement: """ WITH $input as input FOREACH (language IN input | MERGE (l:Language{code:language.code}) FOREACH (name IN language.name | MERGE (nl:Language{code:name.language}) MERGE (l)-[:NAME_TRANSLATION]->(t:Translation)-[:IN_LANGUAGE]->(nl) ON CREATE SET t.id=apoc.create.uuid() SET t.text=name.text ) FOREACH (root IN language.root | MERGE (r:Language{code:root.code}) MERGE (l)-[:ROOT_LANGUAGE]->(r) ) ) WITH input UNWIND input AS language MATCH (l:Language{code:language.code})-[:NAME_TRANSLATION]->(t:Translation)-[:IN_LANGUAGE]->(nl:Language) OPTIONAL MATCH (l)-[:ROOT_LANGUAGE]->(r:Language) RETURN l { .code, name: t { .id, language: nl.code, .text }, root: r.code } AS languageResponse """ ) SetupCountry(input: [CountryInput!]!): [CountryResponse!]! @cypher( statement: """ WITH $input as input MERGE (r:Root) ON CREATE SET r.id=apoc.create.uuid() FOREACH (country IN input | MERGE (c:Country{code:country.code}) FOREACH (name IN country.name | MERGE (l:Language{code:name.language}) MERGE (c)-[:NAME_TRANSLATION]->(t:Translation)-[:IN_LANGUAGE]->(l) ON CREATE SET t.id=apoc.create.uuid() SET t.text=name.text ) FOREACH (nid IN country.nid | MERGE (c)-[:HAS_NID{code:nid.code}]->(comp:Component)-[:CURRENT]->(ver:Version:Unique)-[:VERSION_OF]->(comp) ON CREATE SET comp.id=apoc.create.uuid(), ver.id=apoc.create.uuid() SET comp.alias=nid.code MERGE (comp)<-[:IS_AUTHOR]-(r) FOREACH (nname IN nid.name | MERGE (nl:Language{code:nname.language}) MERGE (comp)-[:NAME_TRANSLATION]->(nt:Translation)-[:IN_LANGUAGE]->(nl) ON CREATE SET nt.id=apoc.create.uuid() SET nt.text=nname.text ) ) FOREACH (official IN country.official | MERGE (ol:Language{code:official.code}) MERGE (c)-[:OFFICIAL_LANGUAGE]->(ol) ) FOREACH (supported IN country.supported | MERGE (sl:Language{code:supported.code}) MERGE (c)-[:SUPPORTED_LANGUAGE]->(sl) ) ) WITH input UNWIND input AS country MATCH (c:Country{code:country.code}) RETURN c { .code, name: [(c)-[:NAME_TRANSLATION]->(t:Translation)-[:IN_LANGUAGE]->(l:Language) | t { .id, language: l.code, .text }], nid: [(c)-[rel:HAS_NID]->(comp:Component)-[:CURRENT]->(ver:Version:Unique) | rel { .code, name: [(comp)-[:NAME_TRANSLATION]->(t:Translation)-[:IN_LANGUAGE]->(l:Language) | t { .id, language: l.code, .text }], component: comp { .id, .alias, type: 'Unique', current: ver.id, author: [(comp)<-[:IS_AUTHOR]-(a) | a.id ]} }], official: [(c)-[OFFICIAL_LANGUAGE]->(l:Language)-[:NAME_TRANSLATION]->(t:Translation)-[:IN_LANGUAGE]->(nl:Language) | l { .code, name: t { .id, language: nl.code, .text }, root: r.code }], supported: [(c)-[SUPPORTED_LANGUAGE]->(l:Language)-[:NAME_TRANSLATION]->(t:Translation)-[:IN_LANGUAGE]->(nl:Language) | l { .code, name: t { .id, language: nl.code, .text }, root: r.code }] } AS countryResponse """ ) }

And these are all the type definitions related to these mutations:

type Translation { id: ID! language: Language! @relation(name: "IN_LANGUAGE", direction: "OUT") text: String! }

input TranslationInput { language: String! text: String! }

type TranslationResponse { id: ID! language: String! text: String! }

type Language { code: String! name: [Translation!]! @relation(name: "NAME_TRANSLATION", direction: "OUT") root: [Language!] @relation(name: "ROOT_LANGUAGE", direction: "OUT") official: [Country!] @relation(name: "OFFICIAL_LANGUAGE", direction: "IN") supported: [Country!] @relation(name: "SUPPORTED_LANGUAGE", direction: "IN") translations: [Translation!] @relation(name: "IN_LANGUAGE", direction: "IN") }

input LanguageInput { code: String! name: [TranslationInput!]! root: LanguageInput }

type LanguageResponse { code: String! name: [TranslationResponse!]! root: String }

input LanguageNameTranslationInput { code: String! language: String! name: String! }

type Country { code: String! name: [Translation!]! @relation(name: "NAME_TRANSLATION", direction: "OUT") nid: [Component!]! @relation(name: "HAS_NID", direction: "OUT") official: [Language!]! @relation(name: "OFFICIAL_LANGUAGE", direction: "OUT") supported: [Language!] @relation(name: "SUPPORTED_LANGUAGE", direction: "OUT") }

type HasNid @relation(name: "HAS_NID") { code: String! }

input NidInput { code: String! name: [TranslationInput!]! }

type NidResponse { code: String! name: [TranslationResponse!]! component: ComponentResponse! }

input CountryInput { code: String! name: [TranslationInput!]! nid: [NidInput!]! official: [LanguageInput!]! supported: [LanguageInput!] }

type CountryResponse { code: String! name: [TranslationResponse]! nid: [NidResponse!]! official: [LanguageResponse!]! supported: [LanguageResponse!] }

Let me know if you need anything else.

jnterry commented 4 years ago

I've ran into this issue again, with a different minimal example, which you can find here: https://community.neo4j.com/t/reusing-custom-cypher-logic-between-all-instances-of-an-interface/23613/3?u=jnterry

alexluna7 commented 4 years ago

Hi @johnymontana, were you able to reproduce the undefined issue or do you need additional data?

As I'm hitting this issue multiple times while working in my project, I've tried to create a very simple scenario to reproduce it. However, I've ended up hitting a different error, which I will reproduce below. I believe that both issues may, somewhat, be connected. It seems to me that neo4j-graphql-js, in some scenarios, doesn't deal well when a @cypher query/mutation has a field that returns an object (therefore a subquery).

As an additional information, I've tried @jnterry code fix suggestion above, but making the change in the dist code installed by npm (version 2.16.0), and it works in the case where I was getting the undefined error.

Please let me know if I am doing something wrong or what I can do to help you go through this issue.

GraphQL schema:

The nidData and nidConfigData fields are there only to help exploring the issue, my use case is more complex than that.

` type UTCountry { code: String! @id name: String! nationalID: UTNid! @relation(name: "UT_HAS_NID", direction: "OUT") nidData: UTNid! @cypher( statement: """ MATCH (this)-[:UT_HAS_NID]->(n:UTNid)-[:UT_NID_CONFIG]->(c:UTConfig) RETURN n { .code, .name, config: c} AS UTNid """ ) nidConfigData: UTConfig @cypher( statement: """ MATCH (this)-[:UT_HAS_NID]->(n:UTNid)-[:UT_NID_CONFIG]->(c:UTConfig) RETURN c AS UTConfig """ ) }

type UTNid { code: String! @id name: String! config: UTConfig @relation(name: "UT_NID_CONFIG", direction: "OUT") }

type UTConfig { id: ID! @id required: Boolean! min: Int max: Int length: Int regexp: String } `

First playground query with result:

` { UTCountry { nidData { code name config { id required min max length regexp } } } }

{ "errors": [ { "message": "Expected to find a node at 'uTCountry_nidData' but found Map{name -> String(\"CPF\"), code -> String(\"BR_cpf\"), config -> (18)} instead", "locations": [ { "line": 2, "column": 3 } ], "path": [ "UTCountry" ], "extensions": { "code": "INTERNAL_SERVER_ERROR", "exception": { "code": "Neo.ClientError.Statement.TypeError", "name": "Neo4jError", "stacktrace": [ "Neo4jError: Expected to find a node at 'uTCountry_nidData' but found Map{name -> String(\"CPF\"), code -> String(\"BR_cpf\"), config -> (18)} instead", ": ", " at captureStacktrace (/home/app/node_modules/neo4j-driver/lib/result.js:263:15)", " at new Result (/home/app/node_modules/neo4j-driver/lib/result.js:68:19)", " at newCompletedResult (/home/app/node_modules/neo4j-driver/lib/transaction.js:449:10)", " at Object.run (/home/app/node_modules/neo4j-driver/lib/transaction.js:287:14)", " at Transaction.run (/home/app/node_modules/neo4j-driver/lib/transaction.js:123:32)", " at /home/app/node_modules/neo4j-graphql-js/dist/index.js:192:25", " at TransactionExecutor._safeExecuteTransactionWork (/home/app/node_modules/neo4j-driver/lib/internal/transaction-executor.js:134:22)", " at TransactionExecutor._executeTransactionInsidePromise (/home/app/node_modules/neo4j-driver/lib/internal/transaction-executor.js:122:32)", " at /home/app/node_modules/neo4j-driver/lib/internal/transaction-executor.js:61:15", " at new Promise ()", " at TransactionExecutor.execute (/home/app/node_modules/neo4j-driver/lib/internal/transaction-executor.js:60:14)", " at Session._runTransaction (/home/app/node_modules/neo4j-driver/lib/session.js:302:40)", " at Session.readTransaction (/home/app/node_modules/neo4j-driver/lib/session.js:274:19)", " at _callee$ (/home/app/node_modules/neo4j-graphql-js/dist/index.js:191:28)", " at tryCatch (/home/app/node_modules/regenerator-runtime/runtime.js:63:40)", " at Generator.invoke [as _invoke] (/home/app/node_modules/regenerator-runtime/runtime.js:293:22)" ] } } } ], "data": { "UTCountry": null } } `

The first query cypher code from neo4j-graphql-js as outputted in node's console:

neo4j-graphql-js MATCH (uTCountry:UTCountry) RETURNuTCountry{nidData: head([ uTCountry_nidData IN apoc.cypher.runFirstColumn("MATCH (this)-[:UT_HAS_NID]->(n:UTNid)-[:UT_NID_CONFIG]->(c:UTConfig) RETURN n { .code, .name, config: c} AS UTNid", {this: uTCountry}, true) | uTCountry_nidData { .code , .name ,config: head([(uTCountry_nidData)-[:UT_NID_CONFIG]->(uTCountry_nidData_config:UTConfig) |uTCountry_nidData_config{ .id , .required , .min , .max , .length , .regexp }]) }]) } ASuTCountry+0ms neo4j-graphql-js { "offset": 0, "first": -1 } +7ms

The first query result when run from cypher-shell:

` neo4j@neo4j> MATCH (this:UTCountry{code:$country}) MATCH (this)-[:UT_HAS_NID]->(n:UTNid)-[:UT_NID_CONFIG]->(c:UTConfig) RETURN n { .code, .name, config: c} AS UTNid ; +------------------------------------------------------------------------------------------------------------------------------------------------------+ | UTNid | +------------------------------------------------------------------------------------------------------------------------------------------------------+ | {name: "CPF", code: "BR_cpf", config: (:UTConfig {length: 11.0, regexp: "([0-9]{11})", id: "59059aec-9936-4dfc-be51-0f2e154aede2", required: TRUE})} | +------------------------------------------------------------------------------------------------------------------------------------------------------+

1 row available after 44 ms, consumed after another 1 ms `

Second playground query with result:

` { UTCountry { nidConfigData { id required min max length regexp } } }

{ "data": { "UTCountry": [ { "nidConfigData": { "id": "59059aec-9936-4dfc-be51-0f2e154aede2", "required": true, "min": null, "max": null, "length": 11, "regexp": "([0-9]{11})" } } ] } } `

The second query cypher code from neo4j-graphql-js as outputted in node's console:

neo4j-graphql-js MATCH (uTCountry:UTCountry) RETURNuTCountry{nidConfigData: head([ uTCountry_nidConfigData IN apoc.cypher.runFirstColumn("MATCH (this)-[:UT_HAS_NID]->(n:UTNid)-[:UT_NID_CONFIG]->(c:UTConfig) RETURN c AS UTConfig", {this: uTCountry}, true) | uTCountry_nidConfigData { .id , .required , .min , .max , .length , .regexp }]) } ASuTCountry+1h neo4j-graphql-js { "offset": 0, "first": -1 } +1ms

The second query result when run from cypher-shell:

` neo4j@neo4j> MATCH (this:UTCountry{code:$country}) MATCH (this)-[:UT_HAS_NID]->(n:UTNid)-[:UT_NID_CONFIG]->(c:UTConfig) RETURN c AS UTConfig ; +---------------------------------------------------------------------------------------------------------------+ | UTConfig | +---------------------------------------------------------------------------------------------------------------+ | (:UTConfig {length: 11.0, regexp: "([0-9]{11})", id: "59059aec-9936-4dfc-be51-0f2e154aede2", required: TRUE}) | +---------------------------------------------------------------------------------------------------------------+

1 row available after 88 ms, consumed after another 1 ms `

alexluna7 commented 4 years ago

The following issue may be connected as well:

https://community.neo4j.com/t/nested-query-is-undefined/16507

loicmarie commented 4 years ago

Anything new about this ?

amoe commented 3 years ago

Hitting this error on the following toy example that attempts to return node-links style data:

CREATE (a:Node {id: "Alice"}), (b:Node {id: "Bob"}), (c:Node {id: "Carol"}), (a)-[:KNOWS]->(b), (b)-[:KNOWS]->(c);
type NodeObject {
    id: ID!
}

type LinkObject {
    source: ID!
    target: ID!
}

type DaveGraph {
    nodes: [NodeObject]
    links: [LinkObject]
}

type Query {
    daveGraph: DaveGraph @cypher(
        statement: """
            MATCH (n), (a)-[r]->(b)
            WITH COLLECT(DISTINCT n) AS nodes, COLLECT(DISTINCT {source: a.id, target: b.id}) AS links
            RETURN {
                nodes: nodes,
                links: links
            }
        """
    )
}

Example query:

{
    daveGraph {
    nodes {
      id
    }
    links {
      source
      target
    }
  }
}

Here's the error:

{
  "errors": [
    {
      "message": "Variable `undefined` not defined (line 6, column 92 (offset: 283))\n\"}\", {offset:$offset, first:$first}, True) AS x UNWIND x AS `daveGraph` RETURN `daveGraph` {undefined} AS `daveGraph`\"\n                                                                                            ^",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": [
        "daveGraph"
      ],
      "extensions": {
        "code": "INTERNAL_SERVER_ERROR",
        "exception": {
          "code": "Neo.ClientError.Statement.SyntaxError",
          "name": "Neo4jError",
          "stacktrace": [
            "Neo4jError: Variable `undefined` not defined (line 6, column 92 (offset: 283))",
            "\"}\", {offset:$offset, first:$first}, True) AS x UNWIND x AS `daveGraph` RETURN `daveGraph` {undefined} AS `daveGraph`\"",
            "                                                                                            ^",
            "",
            "    at captureStacktrace (/home/amoe/dev/lysander-graphql-server/node_modules/neo4j-driver/lib/result.js:275:15)",
            "    at new Result (/home/amoe/dev/lysander-graphql-server/node_modules/neo4j-driver/lib/result.js:66:19)",
            "    at newCompletedResult (/home/amoe/dev/lysander-graphql-server/node_modules/neo4j-driver/lib/transaction.js:446:10)",
            "    at Object.run (/home/amoe/dev/lysander-graphql-server/node_modules/neo4j-driver/lib/transaction.js:285:14)",
            "    at Transaction.run (/home/amoe/dev/lysander-graphql-server/node_modules/neo4j-driver/lib/transaction.js:121:32)",
            "    at /home/amoe/dev/lysander-graphql-server/node_modules/neo4j-graphql-js/dist/index.js:190:25",
            "    at TransactionExecutor._safeExecuteTransactionWork (/home/amoe/dev/lysander-graphql-server/node_modules/neo4j-driver/lib/internal/transaction-executor.js:132:22)",
            "    at TransactionExecutor._executeTransactionInsidePromise (/home/amoe/dev/lysander-graphql-server/node_modules/neo4j-driver/lib/internal/transaction-executor.js:120:32)",
            "    at /home/amoe/dev/lysander-graphql-server/node_modules/neo4j-driver/lib/internal/transaction-executor.js:59:15",
            "    at new Promise (<anonymous>)"
          ]
        }
      }
    }
  ],
  "data": {
    "daveGraph": null
  }
}
michaeldgraham commented 3 years ago

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