UPDATE: We will implement the proposal given here
In certain scenarios you want to compose types without using relations (via @relation
). Here is a simple example:
type User @model {
id: ID! @isUnique
name: String!
location: Location
friends: [User!]! @relation(name: "Friends")
}
type Location {
lat: Float!
lng: Float!
city: City
}
type City {
name: String!
}
This means that...
allLocations
query generatedLocation
to a User
via GraphQLUser
is deleted, the Location
node is deleted as wellid
fieldcreatedAt
/ updatedAt
fields should be supportedProvide the ability to create a type A that is fully owned by another type B. This means that:
With these goals in mind we can improve the developer experience in some key areas:
id: ID! @isUnique
@relation
directive
This is no longer valid
Each embedded type is stored in a separate table. The above schema results in these data tables:
User
| id | name |
|---|-------|
User_Location
| id | lat | lng |
|---|----|----|
User_Location_City
| id | name |
|---|-------|
and these relation tables:
UserLocationRelation
| id | A | B |
|---|---|--|
LocationCityRelation
| id | A | B |
|---|---|--|
Embedded types are stored the same way as normal model types with relations. This enables us to seamlessly migrate an embedded type to a model type and allows us to reuse the existing data fetching mechanism.
Note that embedded types have a table for each type they appear in. Above this is indicated by a prefix, but in practice we have to use an opaque identifier as some SQL engines have strict limits on column name length.
In the future we will likely add a configuration option to pick between different storage strategies optimised for specific use cases. Two obvious alternative strategies:
The three fields id
, createdAt
, updatedAt
are created and maintained internally. They can be exposed in the API by simply including them in the definition of the embedded type.
In the future we will support easy non-destructive migration between model and embedded type.
The following before and after schema illustrates this:
before:
type User @model {
id: ID! @isUnique
name: String!
location: Location
friends: [User!]! @relation(name: "Friends")
}
type Location {
lat: Float!
lng: Float!
}
after:
type User @model {
id: ID! @isUnique
name: String!
location: Location @relation(name: "UserLocation")
friends: [User!]! @relation(name: "Friends")
}
type Location @model {
id: ID! @isUnique
user: User! @relation(name: "UserLocation")
lat: Float!
lng: Float!
}
The data structure will be the same before and after, so this migration can be done quickly and in a non-destructive way. The only change is the exposed API
The default storage strategy supports normal relation filters:
allUsers(
filter: {
location: {
lat_gt: 30
city: {
name_not: "Copenhagen"
}
}
}
) { ... }
note that embedded types does not get a top-level query. The only way to access a Location is by traversing from a User:
{ allUsers { location { lat } } }
embedded types can be created in a deeply nested fashion. Unlike model types, it is not possible to reference an existing embedded type by id.
If a required field in any embedded types is not provided, the entire create mutation fails
mutation {
createUser(data: {
name: "Karl"
location: {
lat: 1
lng: 2
}
})
}
When updating an embedded type, the entire existing object tree is kept, and only the provided keys are updated. In this example lng is updated, and lat retains its old value:
mutation {
updateUser(id: "", data: {
location: {
lng: 3
}
})
}
If an update sets a key in a deeply nested embedded object structure, where the higher-level objects didn't exist before, Graphcool will attempt to create these objects. If any of these objects has required fields that are not being set, the entire update mutation will fail.
When deleting a model type, all its embedded types are deleted as well. This is a feature of the embedded type, and there is no way to change this behaviour.
mutation {
deleteUser(id: "")
}
See the proposal given here
So basically, strongly typed JSON? If this is supported, then is there still a use case left for @relation
? And for mandatory two-way relationships? I think this proposal should take that into account as well. Because I think everything could function exactly like it is doing now without that attribute.
Also related, I think types don't need to have a mandatory id
field. I imagine every node having a unique id in the database, but whether or not you want to expose it in the schema should be up to the developer. There might be another field with @isUnique
, and if there are none, then the User
query (for example) would not be generated at all. This aligns with #257 for nested mutations.
In general, which queries/mutations to generate for a Type could be part of the service definition (graphcool.yml
).
Also related, and probably closeable in favor of this one: https://github.com/graphcool/framework/issues/308
Topic to consider is filtering by those fields. In your example @schickling - would I be able to filter Users by their location on query level like
allUsers(
filter: {
location: {
lat_gt: 30
}
}
) {
}
I think it'd be very popular use case (at least in my case), while it'll propably not support any native database optimisations (unless you use postgress jsonp format etc)
Great input @pie6k and @kbrandwijk! I have documented all aspects of this proposal I am aware of. Would love for you to provide feedback on areas not yet covered or design decisions you disagree with or don't understand.
I have a couple of open questions:
locations: [Location!]!
?id
for embedded data?Should I create a separate issue for my remark about deprecating the relation directive alltogether?
@kbrandwijk I don't really understand what/how/why it would mean to deprecate the @relation directive, so I think a separate issue is a good idea :-)
@sorenbs I was referring to my argumentation in a previous comment: https://github.com/graphcool/framework/issues/1160#issuecomment-341853424
@marktani:
Can you refer to a list of embedded types: locations: [Location!]!?
in the first implementation not, later yes
I think it's important to 'get this right' in one go, and try to minimize on the limitations. This reminds too much of the type restrictions in resolver functions.
UPDATE: This proposal will be part of Graphcool 1.0
With the stated goals for embedded types in mind, I'd like to explore an alternative way to achieve these goals without introducing embedded types as a new first-class concept.
@kbrandwijk I think this goes in the direction of what you were talking about in your comment above?
This alternative introduces some fundamental changes to the way Graphcool works, so brace yourself :-)
@model
directiveWe can assume that all types are stored in database. If we introduce different kinds of types in the future, we can find a way to differentiate them, possibly with a new directive.
id: ID! @unique
field optionalWhen the id
field is not present:
When there is only one relation between two types, we don't need a relation name to match it up:
type Post {
id: ID! @unique
comments: [Comment!]! @relation(name: "Comments")
}
becomes:
type Post {
id: ID! @unique
comments: [Comment!]!
}
Currently relations are required to have fields on both sides:
type Post {
id: ID! @unique
comments: [Comment!]! @relation(name: "Comments")
}
type Comment {
id: ID! @unique
post: Post! @relation(name: "Comments")
}
Allowing one-way relations would remove clutter in some cases:
type Post {
id: ID! @unique
comments: [Comment!]! @relation(name: "Comments")
}
type Comment {
id: ID! @unique
}
type User @model {
id: ID! @unique
name: String!
location: Location
friends: [User!]! @relation(name: "Friends")
}
type Location {
lat: Float!
lng: Float!
city: City
}
type City {
name: String!
}
type User {
id: ID! @unique
name: String!
location: Location @relation(onDelete: CASCADE)
friends: [User!]!
}
type Location {
lat: Float!
lng: Float!
city: City @relation(onDelete: CASCADE)
}
type City {
name: String!
}
I'd love to get some opinions on this idea. Especially from @schickling and @mavilein who brought it up at our internal review and @kbrandwijk who brought up a similar idea in the comments above.
Perfect 鉂わ笍
Another thing to consider is migration from embedded
data to @models
and vice-versa.
Let's say you have embedded data already, but you want Location
to become database entity. What would happen with existing data when @model
will be added to Location
?
@pie6k with @sorenbs' new proposal these would be the same, so migrations are trivial.
Our current understanding is that we should not implement special functionality for embedded types
and instead implement the changes described in https://github.com/graphcool/framework/issues/1160#issuecomment-345373277 A few people have upvoted the proposal already, but I would like to hear from anyone who see any potential issues in the proposed changes.
Personally I am not fund of:
Make the @relation directive optional
Make back-relations optional
I fear hard-to-resolve-bugs arising from this?
@JensMadsen Why would that result in bugs?
In cases where the @relation
is not used (because is optional) or is not given a name, what happens with relation permissions?
@jvbianchi In Graphcool 1.0 permissions are implemented in the GraphQL server, adding a lot of extra flexibility. You can already take a look at this approach in https://github.com/graphcool/graphql-server-example
@sorenbs Where in this example you control user permission?
Having this is the only thing that is preventing me from decision about using graph.cool as backend for my product and it's kind of urgent for me. Are you able to provide any information about progress and plans of implementing it?
@jvbianchi There is a very simple example in https://github.com/graphcool/graphql-server-example/blob/master/src/resolvers/Viewer.ts where the query is limited to the scope of the current user.
@nikolasburk is working on much more extensive documentation :-)
@pie6k This will be included in an early beta later this month. We expect to release 1.0 in January.
Embedded types, unidirection relations, and optional relation directives have been implemented in Prisma 1.0.
As we are starting to work on the MongoDB connector a new perspective is reviving the discussion on special handling for embedded types. I would appreciate any and all input on this proposal #2836 - especially from @schickling, @kbrandwijk, @mavilein who we instrumental in shaping the proposal outlined in https://github.com/prismagraphql/prisma/issues/1160#issuecomment-345373277 that is currently implemented in Prisma.
Most helpful comment
With the stated goals for embedded types in mind, I'd like to explore an alternative way to achieve these goals without introducing embedded types as a new first-class concept.
@kbrandwijk I think this goes in the direction of what you were talking about in your comment above?
This alternative introduces some fundamental changes to the way Graphcool works, so brace yourself :-)
remove the
@model
directiveWe can assume that all types are stored in database. If we introduce different kinds of types in the future, we can find a way to differentiate them, possibly with a new directive.
Make the
id: ID! @unique
field optionalWhen the
id
field is not present:Make the @relation directive optional
When there is only one relation between two types, we don't need a relation name to match it up:
becomes:
Make back-relations optional
Currently relations are required to have fields on both sides:
Allowing one-way relations would remove clutter in some cases:
Compromises compared to embedded types proposal
Adjusted proposal example
Proposal
Alternate version
I'd love to get some opinions on this idea. Especially from @schickling and @mavilein who brought it up at our internal review and @kbrandwijk who brought up a similar idea in the comments above.