graphql icon indicating copy to clipboard operation
graphql copied to clipboard

Unions / Interfaces at top level

Open smkhalsa opened this issue 4 years ago • 29 comments

As mentioned, unions no longer work at the top level, and interfaces no longer work at the top level or on relationships.

This is a major issue for me, and I hope that we can find a way to maintain this functionality in the new library. I'd like to highlight several use cases that are relevant to me:

1. Unions for Search

Search is the canonical example for unions, and it's not clear how we can replicate this behavior without them.

union SearchResult =
    Album
  | Song
  | Artist
  | UserPlaylist
  | FeaturedPlaylist
  | Podcast

type Query {
  Search(string: String!): [SearchResult!]!
    @cypher(
      statement: """
      CALL db.index.fulltext.queryNodes('names', $string+'~') YIELD node, score
      RETURN node
      """
    )
}

2. Interfaces on Relationships for Activity Feed

Assuming the below schema excerpt, we may want to query for all of a user's activity, ordered by createdAt time

interface Activity {
  id: ID!
  createdAt: _Neo4jDateTime!
  user: User! @relation(name: "BY_USER", direction: "OUT")
}

type AudioListen implements Activity {
  id: ID!
  createdAt: _Neo4jDateTime!
  user: User! @relation(name: "BY_USER", direction: "OUT")
  audio: Audio! @relation(name: "AUDIO_LISTENED", direction: "OUT")
}

type VideoView implements Activity {
  id: ID!
  createdAt: _Neo4jDateTime!
  user: User! @relation(name: "BY_USER", direction: "OUT")
  video: Video! @relation(name: "VIDEO_VIEWED", direction: "OUT")
}

type User {
  id: ID!
  name: String
  createdAt: _Neo4jDateTime
  activity: [Activity!]! 
    @cypher(
      statement: """
        // get all activity, sorted by createdAt field
      """
    )
}

3. Top-Level Interface to Query Across Related Types

Assuming the below schema excerpt, we may wish to query across all Audio types.

interface Audio {
  id: ID!
  name: String!
  url: String!
  listens: [AudioListen!]! @relation(name: "AUDIO_LISTENED", direction: "IN")
}

type Song implements Audio {
  id: ID!
  name: String!
  url: String!
  artists: [Artist!]! @relation(name: "BY_ARTIST", direction: "OUT")
  albums: [Album!]! @relation(name: "HAS_SONG", direction: "IN")
  listens: [AudioListen!]! @relation(name: "AUDIO_LISTENED", direction: "IN")
}

type Podcast implements Audio {
  id: ID!
  name: String!
  url: String!
  host: User!
  listens: [AudioListen!]! @relation(name: "AUDIO_LISTENED", direction: "IN")
}

Each of these works fine with the existing neo4j-graphql-js package. I would advocate strongly for ensuring that these and similar use cases can be easily implemented with the new package.

smkhalsa avatar Jan 15 '21 03:01 smkhalsa

Hello. Thank you for sharing your usage here. We realize that not supporting unions and interfaces the same way is causing some issues & will get back to you with a solution.

danstarns avatar Jan 15 '21 14:01 danstarns

@danstarns Just checking in on this.

Is there any update on how the new package will handle these features?

smkhalsa avatar Mar 19 '21 11:03 smkhalsa

Hey @smkhalsa ! The state of this had not changed and we still don't have a solution. Apologies for the time this is taking.

danstarns avatar Mar 19 '21 14:03 danstarns

@danstarns @smkhalsa

Is it possible to use additional labels on nodes to facilitate this approach? A proposal could be to use a directive on an interface declaration as in:

interface Activity @label {
  id: ID!
  createdAt: DateTime!
  user: User! @relationship(type: "BY_USER", direction: OUT)
}
...

Whenever creating a node, any implementing interfaces that have the @label directive attached to it are affixed with an additional label corresponding to the name of the interface. In @smkhalsa's example every :VideoView node would become a :VideoView:Activity node. In this way the additional labels are coupled to the GraphQL schema with no naming conflicts if valid.

Any interface using the @relationship directive must have the @label directive on its declaration. When returning the activity on the User Object

...
type User {
  id: ID!
  name: String!
  createdAt: DateTime!
  activity: [Activity!]! @relationship(type: "BY_USER", direction: IN)
}

it could generate a cypher query of (node:Activity)-[:BY_USER]->(this:User) . Each returned node should have a list of labels that includes a label that corresponds to an Object type which will be what the node will resolve to, e.g. a returned node with labels ["VideoView", "Activity", ...] would resolve to a VideoView GraphQL Object. create-ing Activity through the createUsers and updateUsers would have to be disabled, unless a way of creating specific Activity i.e. VideoView or AudioListen through the api can be found.

This could be applied to top-level interface querying. Again citing @smkhalsa's example:

interface Audio @label {
  id: ID!
  name: String!
  url: String!
  listens: [AudioListen!]! @relationship(type: "AUDIO_LISTENED", direction: IN)
}

type Song implements Audio {
  id: ID!
  name: String!
  url: String!
  artists: [Artist!]! @relationship(type: "BY_ARTIST", direction: OUT)
  albums: [Album!]! @relationship(type: "HAS_SONG", direction: IN)
  listens: [AudioListen!]! @relationship(type: "AUDIO_LISTENED", direction: IN)
}

type Podcast implements Audio {
  id: ID!
  name: String!
  url: String!
  host: User!
  listens: [AudioListen!]! @relationship(type: "AUDIO_LISTENED", direction: IN)
}

Top-level queries with the exception of createAudio could be generated for the Audio interface since it has the @label directive. (createAudios would be impossible to implement since every node must have a label corresponding to some GraphQL Object type and ostensibly createSongs and createPodcasts exists). In this way query { audio(where: {}) { ... } } could return nodes with :Podcast:Audio or :Song:Audio labels that can be resolved to the appropriate Object types.

Without understanding the approach it is difficult to know if this is a useful implementation or not. I, and I think others, would be happy to hear what the current thinking on this topic is. I really appreciate all of the work that is going into this project. Thanks again.

dmoree avatar Mar 20 '21 09:03 dmoree

Hi @dmoree . Thank you for taking the time to write this up! additionalLabels is something we have not yet ported over from neo4j-graphql-js but of course, something we need to look into more.

Our current thinking in regards to Unions and Interfaces is; Firstly, we found problems with the way they were treated... that being, things broke when users plugged the library into an existing dataset, and secondly, multiple labels is a more advanced use case and lead to an harder to reason about implementation, and then we have the Relay RFC here neo4j/graphql-tracker-temp#7 where we want to leave interfaces and unions alone till we come to a final and implemented solution for Relay, and finally, we have to consider how unions and interfaces would work with are newer features such as @auth, The OGM and Nested Mutations.

As it stands, on fields, you can use Unions and I will say; the given examples from both @smkhalsa & @dmoree for sure highlights the need for top-level union queries.

Once again thank you both for expressing your concern and we do realise the missing functionally is making it difficult to implement particular use cases. My best suggestion for you, for the time being, would be to use the @cypher directive.

danstarns avatar Mar 22 '21 08:03 danstarns

@danstarns Any update on this? I see that v1.0 is now live, and this is one of the few issues preventing me from migrating from the old lib.

smkhalsa avatar May 03 '21 06:05 smkhalsa

Hi @smkhalsa. Still no change, sorry to hear it's blocking your migration.

danstarns avatar May 04 '21 05:05 danstarns

@danstarns is this actively being worked on? Can you give an estimated timeline?

smkhalsa avatar May 04 '21 07:05 smkhalsa

@smkhalsa We are not actively working on this, it's being put aside until we come to a solution for relationship properties and relay, as we think there is going to be overlap with interfaces.

danstarns avatar May 04 '21 09:05 danstarns

@danstarns you stated "As it stands, on fields, you can use Unions" -- can you please point to a working example of such a union on a field? Then at least there is a way to work around this issue.

Pitchlab avatar May 14 '21 15:05 Pitchlab

@danstarns you stated "As it stands, on fields, you can use Unions" -- can you please point to a working example of such a union on a field? Then at least there is a way to work around this issue.

Hi. Specifically, you can use Unions on a @relationship field. There is an example here https://neo4j.com/docs/graphql-manual/current/type-definitions/unions-and-interfaces/. Sorry, there is not a solution and we realise the need for the requested. Once relationship properties are in we can dive deeper into this.

danstarns avatar May 17 '21 07:05 danstarns

Hi, I have a use case where it’s practical to group classes/nodes into a super-class or even within a hierarchical tree. However, I have seen that multiple labels are not recommended for such relationships and even « is-a » relationship with caution (https://community.neo4j.com/t/graph-modeling-labels/27690 ). The author prefers unions (https://neo4j.com/docs/graphql-manual/current/type-definitions/unions-and-interfaces/ ), which is also advised by Danstarns in the current thread. But you don’t always remember all members of a union. According to what I see in this last post, there was no attribute inheritance in the former « Interface » anyway. Danstarns also suggests to use the @cypher directive. Could you please point to an example ? Besides, in a webinar dedicated to data science this afternoon, I saw a « part-of-group » relationship. Could it better fit than the former options ? More over, there are classification algorithms in GDS. How does Neo4J handle the resulting classes ?

MarcH1000 avatar May 28 '21 15:05 MarcH1000

Sorry, there is not a solution and we realise the need for the requested. Once relationship properties are in we can dive deeper into this.

@danstarns Now that relationship properties have shipped (awesome work!) in v2, can you provide any update on using interfaces at the top level / in relationships?

smkhalsa avatar Aug 17 '21 00:08 smkhalsa

Hey @smkhalsa, we're about to shape up the work for some or all of:

  • Top-level unions
  • Top-level interfaces
  • Relationship interface fields

Unlikely we'll start work on all of these straight away, so we're going to have to make a judgement call on what's going to be most valuable at this stage. 🙂

darrellwarde avatar Aug 17 '21 08:08 darrellwarde

@darrellwarde that's great news 🎉. Thank you!

My vote would be to start with relationship interface fields.

smkhalsa avatar Aug 17 '21 14:08 smkhalsa

@darrellwarde If you’re looking for community feedback on this I would vote along with @smkhalsa for relationship interface fields

dmoree avatar Aug 17 '21 15:08 dmoree

100% keen for community feedback on this - I encourage all to voice their opinion!

darrellwarde avatar Aug 17 '21 16:08 darrellwarde

@darrellwarde any updates on the status of top-level unions/interfaces? They feel necessary for returning search results that span multiple node types. I'm eager to migrate away from neo4j-graphql-js, but I don't think I'll be able to until they're supported.

adley-kim avatar Oct 11 '21 20:10 adley-kim

I also wonder if it wouldn’t be more optimal to type the observables (Rxjs) with interfaces and to sort or map the implementation classes/types in parallel or a posteriori rather than issuing as many queries as classes/types corresponding to components (Angular) ?

MarcH1000 avatar Oct 12 '21 14:10 MarcH1000

@darrellwarde any updates on the status of top-level unions/interfaces? They feel necessary for returning search results that span multiple node types. I'm eager to migrate away from neo4j-graphql-js, but I don't think I'll be able to until they're supported.

We're focussing on interface relationship fields first (#525) as this was what was called for by the community. Top-level interface and union queries will follow hopefully not too far after!

darrellwarde avatar Oct 13 '21 12:10 darrellwarde

I am halfway into my migration to find this bug, its a deal-breaker for us

fnfbraga avatar Oct 27 '21 09:10 fnfbraga

I am halfway into my migration to find this bug, its a deal-breaker for us

Top-level unions and interfaces won't be trailing too far behind field-level interfaces, which will be released soon.

darrellwarde avatar Oct 28 '21 12:10 darrellwarde

Field level interfaces have been released in 2.4.0 and are available on npm. Top-level is something we are still planning and shaping.

danstarns avatar Nov 11 '21 13:11 danstarns

Hey folks! I was wondering if there's been any update on top-level interfaces? We've got a few use cases that I can't figure out good workarounds to, and it would be super helpful if we had autogenerated queries and mutations for them. For example, given this schema:

interface Content {
  id: ID!
  name: String @ignore
}

type Image implements Content {
  id: ID!
  name: String @ignore(dependsOn: ["altText"])
  url: String
  altText: String
}

type Post implements Content {
  id: ID!
  name: String @ignore(dependsOn: ["title"])
  text: String
  title: String
}

Currently, there are no queries or mutations that are generated for them, so we'd have to query or mutate Image or Post records separately. This requires quite a bit of application code, especially once you have more than 3 or 4 different types of Content. I looked into writing my own queries using @cypher or using OGM directly, and both have downsides.

  • @cypher - like others have mentioned, these are hardcoded queries, so we wouldn't be able to handle different selection sets in different requests. We could try and fetch everything, but our data model has many-to-many Content relations that might have arbitrary depth, and I'm not sure how to handle that with this approach (plus, even if it's possible, that's a whole lot of data on every request, especially if the actual selection set ends up being small)
  • OGM - we can't call ogm.model() on interfaces (which makes sense, I think), but that means doing a query against every possible Content type. Even so, you can pass a request's selection set into an OGM model's .find() method, until you try to fetch a field with @ignore(dependsOn) or @cypher in the schema, which throws an error (I think because OGM doesn't know about all of the stuff @neo4j/graphql knows about)

nelsonpecora avatar Feb 14 '22 19:02 nelsonpecora

@nelsonpecora I saw your comment and looked at it again. I didn't look at it for too long, but the initial work is here: https://github.com/neo4j/graphql/compare/dev...dmoree:feature/top-level-union-interface

Those are minimal changes to get queries working where full text in unions can be used on those that have them, but quickly you'll realize that interfaces are treated differently. They are only relevant if they are used in a @relationship. Maybe I'm missing something, but you can take a look.

Also, how would you envision mutations working? Queries I think can be readily done if things were reworked, interfaces in particular.

dmoree avatar Feb 16 '22 06:02 dmoree

Any updates or workarounds for top-level interface queries?

bjoernpy avatar Jul 02 '22 11:07 bjoernpy

Any updates for top-level interface queries? I see multiple branches industriously developed by @a-alle and some PRs merged, such as #4061. Would it be released soon? 🥰🥰

ZhongYic00 avatar Oct 20 '23 04:10 ZhongYic00

As you've seen, feature development is well underway! They are currently hidden under an experimental flag that we've introduced, so not ready for production use yet, but you can play around with them if you like! Feedback is appreciated.

darrellwarde avatar Oct 23 '23 14:10 darrellwarde