-
Notifications
You must be signed in to change notification settings - Fork 863
Improved API for nested mutations #1280
Comments
I would look at the terminology used compared to that of scalar lists. I'd try to keep it as similar as possible, as for most use cases, it shouldn't really matter if you are working with scalar list, nested types, or relations. In this case, it applies to terminology for scalar list ( Also, the nested structure here really prevents an easy way of feeding an array of elements into the mutation, because they all need to be wrapped by
to:
|
Consider making upserts explicit
|
@sorenbs agree that there should be an explicit upsert operation |
@kbrandwijk I have incorporated your proposal! @btroia I have incorporated explicit |
Hey @sorenbs do you think it will be possible in the near future to store relations with a custom order? |
Connecting additional nodes with the new nested mutation API works fantastic, thanks! |
Has this new API been shipped? I can't seem to get any of the examples above working in the Playground. |
@crtr0 This will be part of Graphcool 1.0. We will release an early beta with partial support in a few days :-) |
Does this mean we will be able to pass in city: { create: and connect: } values so it would essentially create the city if there is none OR connect it if it does? |
@tehfailsafe Currently the spec explicitly states that this is not valid. we can change this decision before the final 1.0 release, so it would be helpful if you can expand on the use case! source:
|
@tehfailsafe, would @sorenbs, the current spec doesn't list createUser(
data: {
name: "Soren"
city: {
upsert: {
create: {
name: "Berlin"
}
update: {
name: "Berlin"
}
}
}
}
) This would
|
@marktani Maybe, but the wording sounds incorrect. I am not trying to update the value for the node, I am trying to connect it to the parent of the nested item if it exists, otherwise create. Upsert sounds like it will find "Berlin" and if I pass other fields it will update those? @sorenbs The use case is quite common: Any time we are dealing with user input and don't know the result set of options and we need to group the data together. Take the city example above. What if we are using Algolia to sync our cities and because of Algolia's pricing model we don't want to pre-populate ALL the cities in the world (there's over 20,000 cities/towns in just the US!). So we allow the user to input a text field for their city. If it doesn't exist, create it so other people can also search for it, otherwise connect the user to the existing one. Coming from rails this is the Maybe there is an easier way I just don't know about, so please let me know :) My particular use case is uploading a CSV file, where a column is a relation and the row entries are results. I need to support uploading multiple CSVs which may or may not have the same column headers. I am using a nested create, something like (names changed to just "rowField and column" to simplify)
Here's the types:
Later I need to query for things like The first time it works but the second time the column name field fails because it has a unique constraint. |
One issue that is not yet covered in the spec is the behavior of the api in case of failure. For example if no node for the where condition can be found, or the nodes are not connected. Here is an example: type top {
id: ID! @unique
uniqueTop: String! @unique
bottom: Bottom @relation(name: "relation")
}
type bottom {
id: ID! @unique
uniqueBottom: String! @unique
top: Top @relation(name: "relation")
} mutation {
deleteBottom(
where: {
id: "NOT A VALID BOTTOM ID"
}
){
uniqueBottom
}
} This will return a '3002' error code and a message that no node with that id exists for 'Bottom'. Currently the new nested mutations will not fail if no item can be found for the where condition mutation {
updateTop(
where: {
id: "TopId"
}
data:{
bottom: {
delete: {id: "NOT A VALID BOTTOM ID"}
}
}
){
id
}
} The same situation can arise if the ids of top and bottom are valid but the two are not connected. This hides failures from the user and necessitates extra checks by the user on the result to determine the outcome of the mutation. Asking for the changed value and checking them against the expected result for example.It also is a difference in behavior from the non-nested case that users might not expect. One potential drawback is that users might expect behavior similar to SQL where this would not error. But we propose, that nested mutations using a where clause should behave from an error-handling perspective as if they were non-nested mutations. Which means in this case erroring and returning a message that the id could not be found. |
Returning an error or not is an independent decision of creating the outer node or node. |
Another common scenario for failure: Consider the case of createUser(
data: {
name: "Soren"
cities: [
{
create: [
{ name: "Berlin" },
{ name: "Wladiwostok" }
]
}
]
}
) Let's say city What happens? Potential outcomes:
|
I would vote for all-succeed or all-fail approach. So I would go for:
|
In my example the outer mutation is an update mutation whose whole effect it would be to delete the inner node. Since the inner mutation fails and the outer one would have no effect at all. In this case I would consider it failing too. We should therefore error on the mutation as a whole when any of the nested parts fails due to an erroneous where clause. Edit: I only saw @marktani's and @kbrandwijk's comments after leaving mine. And I agree with their conclusion to fail completely. |
The shape of
I prefer
This aligns with the shape of conventional |
Added a section on transactional behaviour that should mirror the consensus above. @marktani I don't have a strong opinion on the structure of the upsert mutation. If you (or anyone else) can provide a compelling reason to change it we will. Otherwise it stays as is. |
@marktani shouldn't the new shape then look like this?
And also in the example of nested mutations like in my example further up mutation a{
updateUser(
where: {
name: "Paul"
}
data:{
post: {
delete: {title: "NOT A VALID BOTTOM ID"}
}
}
){
id
}
} It feels a little weird that the outer mutation needs a where while the inner delete does not accept a where but goes straight to the unique fields. A non-nested delete also requires the where. The behavior also differs depending on the nested mutation. For delete and create we infer the data and where level because we can but for update and upsert the where and data levels remain required in the nested case. It would probably be more intuitive if mutations have the same shape independent of whether they are nested or not. The drawback would of course be that the mutation would get one more level of nesting even in cases where we could infer it. |
Was this proposal abandoned and why? Transactional guarantees of some kind for mutations seems undocumented? |
This proposal has been implemented. You can read more about the transactional properties of nested mutations here: https://www.prisma.io/docs/reference/prisma-api/concepts-utee3eiquo#transactional-mutations. |
Nested mutations is a powerful way to manipulate relational data. This proposal will add significant power to the existing nested mutations API.
One-relations
Given the following schema
Create
Update
connect
: Find existing City node by a unique field and connect to the Userupdate
: Update the City node currently connected to the Usercreate
: Create a new City node and connect to the Userupsert
: Update related node if exists, otherwise create. Only available if the relation field is optionaldelete
: Delete the associated node. Only available if the relation field is optionaldisconnect
: remove the associated node from relation. Only available if the relation field is optionalOnly a single argument may be provided at a time. If two or more are provided, and error is returned.
Many-relations
Given the following schema
Create
Create and connect can be provided at the same time. If they are, create mutations will be executed before connect mutations.
Update
connect
: Find existing City nodes by a unique field and connect to the Userupdate
: Update a City node currently connected to the Usercreate
: Create a new City node and connect to the Userupsert
: Update related node if exists, otherwise createdelete
: Delete an associated nodedisconnect
: remove an associated node from relationRelation mutations
With the introduction of more powerful nested mutations we no longer need the relation specific mutations for adding and removing nodes to a relation. Hence, we will remove these mutations to simplify the API.
Transactional Behaviour
Nested mutations are run in a transaction. If any part of the mutation fails the entire transaction is rolled back and the error is returned.
Note on the
connect
actionIf you connect to a node that does not exist, that action will fail and the entire nested mutation will be rolled back.
Further thoughts
The text was updated successfully, but these errors were encountered: