prisma1 icon indicating copy to clipboard operation
prisma1 copied to clipboard

Support for an Interface type

Open marktani opened this issue 8 years ago • 71 comments

This feature request serves as central place to discuss how interface types and related operations could look like in Prisma.

marktani avatar Jan 31 '17 14:01 marktani

Use Case: Add a User interface so multiple models can inherit from it and get authentication out of the box

stevewpatterson avatar Feb 27 '17 00:02 stevewpatterson

Thanks @stevewpatterson, this would be useful! Another use case: interface for createdAt/updatedAt

marktani avatar Feb 27 '17 09:02 marktani

The File model could be pulled out as an interface. If you have relations between File and multiple other models, you currently have many null values.

marktani avatar Feb 27 '17 10:02 marktani

Similarly, I want to create Users and Communities which have Slugs and the slug needs to be unique across all of them. Right now when linking User with Slug and Community with Slug, it creates two relations from Slug to Community and from Slug to User. But only exactly one of these relations should have data at any given time. I understand that interface types would fix this issue.

sedubois avatar Apr 27 '17 12:04 sedubois

Another use case: implement a global permissions system across multiple types.

Here's a gist with an example schema: https://gist.github.com/amonks/989c815eca601d7ae63ded8fa5f9d530

I don't know how to see notifications for gist comments, but I'm happy to answer questions here.

amonks avatar May 15 '17 23:05 amonks

I'm working on a CRM style application and need either Union Types or Interfaces to model content. I would need a ContentElement interface that can be implemented by a TextNode or a ImageNode, for example so that I can then build the content of my page based on these different elements.

Hope this is coming really soon, as it is so essential for many more complex use cases.

MrLoh avatar Jun 04 '17 16:06 MrLoh

Am I missing an obvious workaround? It seems like any non-trivial model would need this or unions. For example, take this: https://www.graph.cool/docs/faq/graphcool-relation-tag-in-idl-schemas-jor2ohy6uu/ and try to make it so Users can comment on either Posts or Users:

https://gist.github.com/brandf/c8b06dae80b0fd994f06838d69f5437f

brandf avatar Aug 30 '17 04:08 brandf

+1 this feature request. Our use case is with regards to a Notification model. So an interface would really be helpful to be able to create a relationship between Notification model, and other types of models like Message, Task, Project.

sebasibarguen avatar Sep 21 '17 17:09 sebasibarguen

@marktani if the stage/wip label is being removed, is it being demoted in priority? Where can we expect it in the roadmap? waiting? I understand that there may be no specific timeline (#165) but a feeling of priority would help.

We see interfaces as filling a major requirement for our application and I'd be jumping at scaphold.io for their integration of interfaces if it didn't look like they were dying as a service. Not to play down many of the other killer things you folks are doing! :)

Is there just a difference in philosophy, like graph.cool would prefer users to strictly normalize the types and use relations to connect them back together? Thanks.

ptpaterson avatar Sep 26 '17 02:09 ptpaterson

Hey @ptpaterson, thanks so much for your feedback! This matter absolutely did not decrease in priority and is on our near-future roadmap together with #165. We're in the process of changing the way we communicate and structure our roadmap, and the "stage" labels have become obsolete in the process.

You can now follow along our two-week roadmap in Github projects. The upcoming milestone 1.5 is tracked here: https://github.com/graphcool/graphcool/projects/3. Our immediate focus lies on getting the Resolver + CLI beta out to everyone, as together, they enable completely new workflows and cover a lot of popular feature requests (https://github.com/graphcool/graphcool/issues/213, https://github.com/graphcool/graphcool/issues/39, ...).

Afterwards, I can see us to focus on raw API capabilities (like cascading deletes #47, for example) as well as schema primitives (interfaces, unions, relations, ...), but this is still in flux and remains to be seen 🙂 Rest assured that you are definitely heard though, and I really appreciate your input.

marktani avatar Sep 26 '17 08:09 marktani

@amonks I love the idea of using Interfaces to simplify permission management!

The other major use case is to have a relation to an interface and be able to use the regular relation filters.

There are several different ways to implement interface support on the database level. Each implementation represents a different set of tradeoffs and it is not obvious that one of them is best in all cases. Providing detailed use cases will help us implement interfaces in a way that supports the most use cases.

If you have a use case for interfaces, please take the time to describe it here :-)

I'll list different implementations here so we can can start a more detailed discussion.

Consider the following data model:

interface User {
  id: String
  name: String
}

type Customer implements User {
  customerNumber: Int
}

type Employee implements User {
  department: String
}

Single table

A single table called User contains fields from the User interface as well as both Customer and Employee:

User

  • id
  • name
  • Customer_customerNumber
  • Employee_department

Individual tables for concrete types

Each concrete type has a dedicated table with both interface fields and type fields:

Customer

  • id
  • name
  • customerNumber

Employee

  • id
  • name
  • department

Individual table for Interface and concrete types

Fields on the interface are in a separate table:

User

  • id
  • name

Customer

  • customerNumber

Employee

  • department

sorenbs avatar Sep 26 '17 11:09 sorenbs

  • TLDR:
    • Big problem
    • Need to arbitrary data linked to arbitrary data
    • Need to bootstrap auth, permissions, integrations, etc.
    • Problem well defined with real-life use cases, but solution not set in stone. Base assumptions need challenged

I only after initially posting here, I discovered that Amazon Aurora is sitting on the back-end. The implementation that I am working on has been focusing on some tricks that No-SQL makes easy. I essentially need an entity-attribute-value model so that the user can create arbitrary connections to all sorts of arbitrary data. SQL is not kind to my data, but JSON is nice.

Understanding the backend a bit more makes me skeptical we can get this running on graphcool and still achieve the efficiency and flexibility of configuration we are shooting for.

Maybe custom database is the first feature I need to look out for :) I'd love to still go through the exercise, though.

Here is the blog-post worthy description of my use case.

Thanks!

ptpaterson avatar Sep 27 '17 04:09 ptpaterson

Thanks Paul!

I think a helpful exercise for you would be to define all query paths to better understand where your performance constraint will be.

It might turn out that you will be able to store your flexible configuration in a Json field together with a few values that are required for filtering.

If defining a few fields required for filtering is not possible, maybe allowing filtering on data in a Json field is really what you want: https://github.com/graphcool/graphcool/issues/148 This will allow Graphcool to be used in a similar way as MongoDB, but you loose some of the benefits of the GraphQL type system.

sorenbs avatar Sep 27 '17 08:09 sorenbs

Please let define in interface various directives and all types which are implementing that interface should inherit all directives. For example:

interface Product @model {
  id: ID! @isUnique
  vendor: Vendor! @relation(name: "VendorProducts")
  orders: [Order!]! @relation(name: "OrderProducts")
  name: String!
  description: String
  slug: String! @isUnique
  price: Float! @relation(name: "ProductPrice")
  images: [Image!]! @relation(name: "ProductImages")
  belongsToCollections: [Collection!] @relation(name: "CollectionProducts")
  isPublished: Boolean! @defaultValue(value: false)
  materialInfo: MaterialInfo!
  colorInfo: ColorInfo!
  createdAt: DateTime!
  updatedAt: DateTime!
}

All types implementing interface Productshould be @model types, all isPublished fields in those types should have @defaultValue(value: false), all images fields should have @relation(name: "ProductImages") etc.

welf avatar Oct 12 '17 13:10 welf

@ptpaterson GraphQL is all about a well-defined schema, that can be introspected, and known on the client. You could of course come up with a schema that's generic enough for you to store the information in.

An alternative would be to dynamically create Types and fields based on the needs of the user. I did a small POC with that. Bottom line: if you dynamically create your queries this works, but Relay is out the door, as well as most of Apollo's store management, unless you use middleware to translate your dynamic query results into a generic structure that you can use for your client side store.

So either you have a very generic server, with server-side performance impact, or you have a dynamic server, with client-side performance impact. However, the client-side strategy corresponds mostly to the issues you would also have with that using No-SQL.

The alternative that actually serves you best would be using a Graph database, as it is fundamentally different in the way the the node itself defines the type, not some underlying type definition (much like no-SQL), but querying it is still a walk in the park (using the right query language). As much as I hate advising people not to use GraphQL, it's not a one solution fits all kind of thing, much like any other technology out there.

kbrandwijk avatar Oct 12 '17 13:10 kbrandwijk

Interface types still allow the use of well-defined schema, they just need to be used differently.

I do agree that the data I have would be best served with a graph database, or at least modeled as a graph, which could be done in a relational database or document store. But in order to do that effectively, I either need a schema-less solution or interfaces. 'Effectively' is the key, because I can already use Graphcool to model vertex types and edge types. It's just that each vertex type must explicitly declare separate collections of each possible edge type in advance. If there were interface types then each vertex type could have a collection of abstract edge types, that could then point to another abstract vertex type.

A native graph database doesn't preclude one from using GraphQL. Trying to traverse an abstracted graph like this with GraphQL is imperfect, because GraphQL is indeed not perfect for all solutions, but it's not impossible.

The things that we get out of Graphcool that aren't just Graphql... instant setup, permissions, integrations, serverless setup... to me are worth trying to make work. Unless I have been missing something, the graph database world is seriously lacking in this department - in bootstrapping up a full production application environment. So I can deal with modeling as a graph as opposed to using a native graph db. But I don't see a way to do it without interfaces.

ptpaterson avatar Oct 14 '17 15:10 ptpaterson

@ptpaterson A workaround for using interfaces would be do define the Interface Type as a 'normal' Type, and add a 'concreteType' enum field. Your queries will become a bit more verbose, because you need to filter the client collection based on the concreteType field, but at least you can save it.

Also, if you can define your Types using Interfaces, it's way less dynamic than your original post made it seem. My answer was based on really arbitrary Types, not known at design-time.

kbrandwijk avatar Oct 14 '17 16:10 kbrandwijk

A workaround for using interfaces would be do define the Interface Type as a 'normal' Type, and add a 'concreteType' enum field.

@kbrandwijk Can you post an example of this that works with the current version of Graphcool?

jcheroske avatar Oct 16 '17 19:10 jcheroske

I assume @kbrandwijk is talking about an approach similar to this one: https://www.graph.cool/forum/t/schema-building-with-different-user-types/272/2?u=nilan

marktani avatar Oct 16 '17 19:10 marktani

Ok, I see how we can construct our schema using that approach. Can you show the query and mutation pieces of that pattern? Should I use a nested mutation to insert and update? I'd also like to see the correct way to query data that's structured that way. Do I need to use @include?

jcheroske avatar Oct 16 '17 19:10 jcheroske

It's easy to add interfaces to an existing Graphcool endpoint using the API Gateway pattern. I have created an example here: https://github.com/kbrandwijk/graphcool-gateway-examples/tree/master/interfaces

kbrandwijk avatar Nov 06 '17 00:11 kbrandwijk

I think the API Gateway example shows a valuable lesson of how this could be implemented. The Gateway pattern works like this:

  • Each concrete type (implementation of interface) acts just like a normal type (so has it's own table like any other type)

  • The only difference is on relations. There we benefit from globally unique id's, so there's never a conflict in a collection. The only change I think that is needed is that for queries, the database query should be a join over all concrete types.

  • Mutations also support Interfaces. Here, the same globally unique id's make sure that you can just have 1 id field and one Type field. For vehicleId, the id could be looked up in both concrete Types, and for vehicle, the resolveType should be able to determine the concrete Type. An alternative would be adding a Type field for every concrete Type.

For example:

interface Vehicle { numberOfWheels: Int }
type Car implements Vehicle {
   numberOfWheels: Int
   horsePower: Int
}
type Bike implements Vehicle {
   numberOfWheels: Int
   color: String
}

type User {
  vehicle: Vehicle
}

Now the createUser mutation would look like: createUser(id: ID!, vehicleId: ID, vehicle: Vehicle) or createUser(id: ID!, vehicleId: ID, car: Car, bike: Bike)

kbrandwijk avatar Nov 09 '17 14:11 kbrandwijk

Thanks Kim!

Given your data model, Graphcool should support the following queries:

allUsers(filter: {vehicle: {numberOfWheels: 4}}) { vehicle { ... on Car { .horsePower } } } filter on field on interface ⛔️ allUsers(filter: {vehicle: {horsePower: 42}}) { vehicle { ... on Car { horsePower } } } filter on field not on interface ✅ allUsers(filter: {vehicle: {Car: {horsePower: 4}}}) { vehicle { ... on Car { horsePower } } } filter on field on concrete type. This will only return cars ✅ allUsers(filter: {vehicle: {OR: {Car: {horsePower: 4}, Bike: {}}}}) { vehicle { ... on Car { horsePower } } } filter on field on concrete type. The empty Bike filter will return all bikes ✅ allCars(filter: {horsePower: 42, numberOfWheels}) { horsePower } normal allX query ✅ allVehicles(filter: {numberOfWheels: 4, Car: {horsePower: 42}}) { numberOfWheels ... on Car { horsePower } } allX query for interface ✅ allVehicles(filter: {numberOfWheels: 4, Car: {numberOfWheels: 42}}) { numberOfWheels } filters on interface fields can be specified on the interface and concrete type level. If both are present, they must match

For create mutations, I think your second option is the way to go: createUser(id: ID!, vehicleId: ID, car: Car, bike: Bike) We should return an error if more than one of vehicleId, car, bike is present.

sorenbs avatar Nov 09 '17 15:11 sorenbs

  • Type can be inferred, so I think 'filter on field not on interface' could also work.
  • I would make the filter on type explicit: filter: { type: 'Bike' }. That's easier to build with variables.
  • I don't understand numberOfWheels without value in the allX query.

Also, how would create mutations look when vehicle: Vehicle is vehicles: [Vehicles!]!? And how would the relation attributes look (given you decide to keep them, I created another issue to remove them completely, but I can't find it).

kbrandwijk avatar Nov 09 '17 15:11 kbrandwijk

Type can be inferred, so I think 'filter on field not on interface' could also work.

I don't think this is a good idea. Different types can have the same field without it being part of the interface

Your second point is valid though, and I need to put some more thought into how best to narrow a query to specific types. In my suggestion above, adding a type specific filter would return only nodes of that type. Adding other type filters with an empty filter is a way to add more types, but I don't think that is clear enough.

sorenbs avatar Nov 09 '17 16:11 sorenbs

I think it's not relevant whether a field is part of the interface or not: allUsers(filter: {vehicle: {numberOfWheels: 4}}) {...} allUsers(filter: {vehicle: {horsePower: 42}}) {...} Why should one work, and not the other?

kbrandwijk avatar Nov 09 '17 17:11 kbrandwijk

Can allUsers(filter: {vehicle: {horsePower: 42}}) {...} return bikes?

marktani avatar Nov 09 '17 17:11 marktani

No, but allUsers(filter: {vehicle: {numberOfWheels: 4}}) {...} also won't return bikes.

kbrandwijk avatar Nov 09 '17 17:11 kbrandwijk

So, how does the VehicleFilter look like?

type VehicleFilter {
  id: ID # plus variants like id_contains ...
  horsePower: Int
  color: String
}

What about this:

interface Vehicle { numberOfWheels: Int }
type Car implements Vehicle {
   numberOfWheels: Int
   horsePower: Int
   weight: Float
}
type Bike implements Vehicle {
   numberOfWheels: Int
   color: String
   weight: Int
}

type User {
  vehicle: Vehicle
}

Note weight: Int and weight: Float.

marktani avatar Nov 09 '17 17:11 marktani

Meh... You win 😄 Back to: allUsers(filter: {vehicle: {Car: {horsePower: 42}}}) then for fields that are not part of the interface. I do think that interface fields should also exist on the vehicle.Car type, to filter cars only.

type VehicleFilter {
  id: ID
  numberOfWheels: Int
  Car: VehicleCarFilter
  Bike: VehicleBikeFilter
  type: [String]
}

type VehicleCarFilter {
   id: ID
   numberOfWheels: Int
   horsePower: Float
}

type VehicleBikeFilter {
   id: ID
   numberOfWheels: Int
   color: String
}

kbrandwijk avatar Nov 09 '17 17:11 kbrandwijk