Map projection and nested type

Hello, could someone please help, it's rather urgent. I am using an old version of neo4j-graphql-js (with grandstack) but I imagine it could be the same problem with neo4j-graphql. My graph is (Client)-[:HAS_SPONSOR]->(Client) Client type in schema:

type Client {
   id: String!
   login: String!
   level: Int!
   name: String!
   sponsor: Client @relation(name: "HAS_SPONSOR", direction: OUT)
}

`
and Query Type in schema

  downline(login: String!): [Client] @cypher(statement: """
      
      MATCH (me:Client) WHERE me.login=$login 
      WITH  me
      MATCH client:Client)-[hs:HAS_SPONSOR*0..2]->(me)
         WITH SIZE(hs) AS level
      RETURN client{.*, level:level} ORDER BY level
   """)

Note that I am adding a calculated level field in map projection This query works great:

 {
 downline(login: "c4") {
   id
   name
   level
 }
 }

but I need to get the sponsor as well and I am not able to. The query:

{
 downline(login: "c4") {
   id
   name
   sponsor {
    id
    name
  }
 }
 }

error:

Expected to find a node at '  client@901' but found Map

I am getting the same if I return Client node instead of Map projection in cypher (custom query). Do you have any idea how I can get sponsor data (if specified in graphql query)?

You'd need to project out the sponsor object in the @cypher query as well, instead of returning the node object.

Something like this if I understand your model:

downline(login: String!): [Client] @cypher(statement: """
      
      MATCH (me:Client) WHERE me.login=$login 
      WITH  me
      MATCH (client:Client)-[hs:HAS_SPONSOR*0..2]->(me)
         WITH SIZE(hs) AS level
      RETURN client {.*, level: level, sponsor: me {.*}} ORDER BY level
   """)

Yes, I tried that. I am getting the same error:

Expected to find a node at '  client@891' but found Map instead

for the query:

 downline(login: "c4" ) {
   id
   name
   sponsor {
    id
    name
  }
 }
 }

I also find the very similar question posted here, without an answer:

Any other idea? Is this doable at all?
Thanks!

@michaeldgraham, @lyonwj
I feel I am stuck and really need help. I have debugged the queries generated by neo4j-graphql-js (latest version 2.19.4) with neo4j-driver 4.2.2 against neo4j-community-server 4.3.3.

For completeness, here is my schema.graphql

type Client {
   id: String!
   login: String!
   name: String!
   sponsor: Client @relation(name: "HAS_SPONSOR", direction: OUT)
   type: String!
   level: Int
   typeLevel: String
}

type Query {
   team(login: String!): [Client] @cypher(statement: """
      MATCH (me:Client) WHERE me.login=$login
      WITH  me
      MATCH (client:Client)-[hs:HAS_SPONSOR*0..2]->(me)
      WITH me, SIZE(hs) AS level, client, (SIZE(hs) + client.type) AS typeLevel
      RETURN client{.*, level:level, typeLevel: typeLevel, sponsor: me {.*}} ORDER BY level
   """)
}

typeLevel and level are the calculated fields where the calculation happens in custom cypher query during sponsorship tree traversal.

When I fire a generic (non-custom query) for Client:

query{
    Client (filter: {login: "c4"}) {
       id
       name
       sponsor {
         id
         name
      }
    }
}

Generated query by neo4j-graphql-js in the console is:

MATCH (`client`:`Client`) WHERE (`client`.login = 'c4') RETURN `client` { .id , .name ,sponsor: [(`client`)-[:`HAS_SPONSOR`]->(`client_sponsor`:`Client`) | `client_sponsor` { .id , .name }] } AS `client`

and it works ok.

But since I need to calculate fields: typeLevel and level, I am using custom query team.

And for the graphql query:

query{
    team (login: "c4") {
       id
       name
       sponsor {
         id
         name
      }
    }
}

I am getting error:

Expected to find a node at '  client@369' but found Map

Generated query by neo4j-graphql-js in the console is:

WITH apoc.cypher.runFirstColumn("MATCH (me:Client) WHERE me.login='c4'
WITH  me
MATCH (client:Client)-[hs:HAS_SPONSOR*0..2]->(me)
WITH me, SIZE(hs) AS level, client, (SIZE(hs) + client.type) AS typeLevel
RETURN client{.*, level:level, typeLevel: typeLevel} ORDER BY level", {}) AS x 
UNWIND x AS `client` RETURN `client` { .id , .name ,sponsor: head([(`client`)-[:`HAS_SPONSOR`]->(`client_sponsor`:`Client`) | `client_sponsor` { .id , .name }]) } AS client    

I can see that the result of UNWIND is not the Client node, but Map which seems like a cause of the error I am getting.

The return statement in custom query:

RETURN client{.*, level:level, typeLevel: typeLevel, sponsor: me {.*}} ORDER BY level

is per @lyonwj advice in this thread.
I have also tried

RETURN client{.*, level:level, typeLevel: typeLevel} ORDER BY level

and I am getting the same error.
Again, the result of the UNWIND is a Map not Client node.

I have been browsing the github issues and community forums and found the similar problem reported in 2 Open issues:

without any possible solution mentioned.

Could you please tell me what options I have (very tight deadline is in front of me).

Many thanks in advance,
Milan

Rather than using the Cypher directive on the Query field, could you move the custom Cypher to the level and typeLevel fields? So something like

type Client {
  id: String!
  login: String!
  name: String!
  sponsor: Client @relation(name: "HAS_SPONSOR", direction: OUT)
  type: String!
  level: Int @cypher(statement: "RETURN SIZE( (:Client)-[:HAS_SPONSOR*0..2]->(this)  )"
  typeLevel: String @cypher(statement: "MATCH (c:Client)-[hs:HAS_SPONSOR*0..2]->(this) RETURN (SIZE(hs) + c.type)"
}

and then query using the generated Client query field and filtering for login?

{
  Client(login: "foo") {
    level
    typeLevel
    ...
  }
}

Thanks @lyonwj, interesting solution, but I am afraid, it won't work for me.
In order to calculate level like you suggest, I would need 2 things:

  1. a reference to the traversal root node (Client id or login)
  2. additional field, e.g recruits
recruits: [Client] @relation(name: "HAS_SPONSOR", direction: IN)

2 is not a problem, although it is somehow redundant, since I need only sponsor field for mutations
but 1 is a problem, since apparently I cannot pass $login to the cypher query attached to level and typeLevel field. The only node I can pass is this.

It would be much more convenient/efficient if there is a way to return Client node + additional fields in the custom query posted above:

type Query {
   team(login: String!): [Client] @cypher(statement: """
      MATCH (me:Client) WHERE me.login=$login
      WITH  me
      MATCH (client:Client)-[hs:HAS_SPONSOR*0..2]->(me)
      WITH me, SIZE(hs) AS level, client, (SIZE(hs) + client.type) AS typeLevel
      RETURN client{.*, level:level, typeLevel: typeLevel, sponsor: me {.*}} ORDER BY level
   """)
}

so that in graphql request I can specify sponsor and get its data through mapped relationship.
Is there any trick to make that work?
Is it a bug/limitation or I am doing something wrong here?

Thanks again for your time and help.

but 1 is a problem, since apparently I cannot pass $login to the cypher query attached to level and typeLevel field. The only node I can pass is this .

Any field arguments defined in a Cypher directive field are passed to the Cypher statement as Cypher parameters. So if you wanted a reference to $login in the Cypher query you could add it to the field definition and pass the login value at query time:

type Client {
  ...
  level(login: String!): Int @cypher(statement: "RETURN SIZE ( (:Client {login: $login})-[:HAS_SPONSOR*0..2]->(this) )
}

The Cypher directive was originally intended for usecases like computed fields and custom mutations, so there may be some cases with returning a nested projected object that aren't properly handeled in neo4j-graphql.js.

Another option might be to implement a custom resolver that uses the Neo4j JavaScript driver directly to execute the Cypher query (the driver object will be available in the context object).

Have you tried this with the official Neo4j GraphQL Library? I haven't tested this specifically yet, but you may have better results with this and the new library.

Hi, sorry for the late response.
Cypher directive on a field doesn't seem to be an option, since I would need to traverse the whole sponsorship tree on both level and typeLevel.
Custom resolver, too, since I need a portable solution (we are using spring boot with neo4j-graphql-java in prod, and neo4j-graphql-js with grandstack for development only).

I haven't tried official neo4j-graphql js library recently. The problem with that is incompatibility with neo4j-graphql-java (which dev sadly seems to be stopped).

I ended up extracting fields from connected nodes and using map projection in data field on Client node. This way I cannot specify in graphql request which fields from connected nodes I want (all of them are returned when data is specified in the request) but I simply could not find any other way.

Thanks for trying to help!