graphql: Something with interfaces generates invalid cypher

Describe the bug I’m not sure what actually triggers this error, but it triggers given the following schema

Type definitions

type SomeNode {
    id: ID! @id
    other: OtherNode! @relationship(type: "HAS_OTHER_NODES", direction: OUT)
}

type OtherNode {
    id: ID! @id
    interfaceField: MyInterface! @relationship(type: "HAS_INTERFACE_NODES", direction: OUT)
}

interface MyInterface {
    id: ID! @id
}

type MyImplementation implements MyInterface {
    id: ID! @id
}

To Reproduce Run the following query:

query {
  someNodes {
    id
    other {
      interfaceField {
        id
      }
    }
  }
}

and you should get an error, similar to #1535

Variable `interfaceField` not defined (line 2, column 117 (offset: 138))\n\"RETURN this { .id, other: head([ (this)-[:HAS_OTHER_NODES]->(this_other:OtherNode)   | this_other { interfaceField: interfaceField } ]) } as this\"\n                                                                                                                     ^

System (please complete the following information):

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Reactions: 1
  • Comments: 17 (8 by maintainers)

Most upvoted comments

@chrisdostert We are planning on a release this week, so hopefully you’ll be unblocked pretty soon.

Thanks for the fork, @litewarp 👏

After some recent changes in the cypher projections, fixing this should indeed be easier. As @litewarp mentions, the logic is similar to unions, and in fact to normal relationships now that those are subqueries (#1971 and #1918)

I’ll be making a PR soon that should fix this issue, thanks for all the info and sorry for the long time this has been on triage

So I could be way off base here, but I think this fix might be easier than once thought. Or at least it should be theoretically.

If I understand the spec correctly, the only difference between an interface and union is where you can query the types - in an interface, you can group them together; in a union, you have to list them individually. In other words, this is really a concern for how the client queries the graphql server, but it doesn’t necessarily affect the query to the database.

To take the star wars example the difference between querying an interface and a union is about where you place the inline fragment.

# Example 1 - With interfaces

interface Character {
  id: ID!
  name: String!
}

type Human implements Character {
  id: ID!
  name: String!
  hasForce: Boolean!
}

type Droid implements Character {
  id: ID!
  name: String!
  hasRestrainingBolt: Boolean!
}

query {
  character {
    id
    name
    ... on Human {
       hasForce
    }
    ... on Droid {
      hasRestrainingBolt
    }
  }
}

----

# Example 2 - With unions

union Character = Human | Droid

type Human {
  id: ID!
  name: String!
  hasForce: Boolean!
}

type Droid {
  id: ID!
  name: String!
  hasRestrainingBolt: Boolean!
}

query {
  character {
    ... on Human {
      id
      name
      hasForce
    }
    ... on Droid {
      id
      name
      hasRestrainingBolt
    }
  }
}

In both cases, the database has to search for Droids that have id, name, and hasRestrainingBolt, and for Humans, id, name, and hasForce.

The way that create-projection-and-params currently handles unions is that it finds the nodes that comprise the union, and then filters out the ones that either don’t have any fields projected or are filtered by the where clause and then constructs parallel subqueries for them joined by the UNION statement.

See, e.g.

            "MATCH (this:\`Movie\`)
            WHERE this.title = $param0
            CALL {
                WITH this
                CALL {
                    WITH this
                    MATCH (this)-[thisthis0:SEARCH]->(this_search:\`Genre\`)
                    WHERE (this_search.name = $thisparam0 AND apoc.util.validatePredicate(NOT ((this_search.name IS NOT NULL AND this_search.name = $thisparam1)), \\"@neo4j/graphql/FORBIDDEN\\", [0]))
                    WITH this_search  { __resolveType: \\"Genre\\",  .name } AS this_search
                    RETURN this_search AS this_search
                    UNION # <--------
                    WITH this
                    MATCH (this)-[thisthis1:SEARCH]->(this_search:\`Movie\`)
                    WHERE this_search.title = $thisparam2
                    WITH this_search  { __resolveType: \\"Movie\\",  .title } AS this_search
                    RETURN this_search AS this_search
                }
                WITH this_search
                SKIP 1
                LIMIT 10
                RETURN collect(this_search) AS this_search
            }
            RETURN this { search: this_search } as this"

However, since both abstract classes expect the same data returned, you could basically use the cypher built for unions to run the interface query (echoing a comment made waaaaay back in the day).

Most of the heavy lifting can be pulled from the relationField.union implementation linked above. You’d just have to change the logic to find all the Nodes that implement the Interface before passing it to the same subquery builder as used for unions. Then the resolver should accept the same shape of the returned data, but process it as an interface before returning it to the client.

If I had the time, I’d give it a try. But hope this helps someone else get started.

@chrisdostert, sorry to hear that! That’s on me that the bug still has a high-priority label, I simply forgot to remove it. Given your input and some changes in the library in the meantime, I’ll also make sure that we in the team discuss and re-evaluate this bug report again, likely next week.