Support for an Interface type
This feature request serves as central place to discuss how interface types and related operations could look like in Prisma.
Use Case: Add a User interface so multiple models can inherit from it and get authentication out of the box
Thanks @stevewpatterson, this would be useful! Another use case: interface for createdAt/updatedAt
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.
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.
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.
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.
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
+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.
@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.
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.
@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
- 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!
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.
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.
@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.
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 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.
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?
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
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?
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
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 forvehicle, theresolveTypeshould 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)
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.
- 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
numberOfWheelswithout value in theallXquery.
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).
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.
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?
Can allUsers(filter: {vehicle: {horsePower: 42}}) {...} return bikes?
No, but allUsers(filter: {vehicle: {numberOfWheels: 4}}) {...} also won't return bikes.
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.
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
}