Most AWS Amplify/AppSync tutorials and examples explain how conflict resolution is usually automatic with this tech stack, and in extreme examples show how to custom handle a conflict when saving an instance of a single Model. What happens when you need to enforce more complicated relationships between your Models, and something goes wrong "down stream"?
(Contrived) Example:
Imagine a number of easter eggs holding some number of jelly beans each, and a group of children trying to empty them into their easter baskets.
type Child @model {
id: ID!
name: String!
jellybeans: Int!
}
type Egg @model {
id: ID!
jellybeans: Int!
}
Jellybeans are a conserved quantity - if we add up all the jellybeans across the children and the eggs it should be constant. Additionally, neither children nor eggs can have "negative jellybeans" - so a CRDT model is not possible.
As the children find eggs, they add them to their baskets:
async function findEgg(childId, eggId) {
const child = await DataStore.query(Child, childId);
const egg = await DataStore.query(Egg, eggId);
if (!child || !egg) {
return;
}
const count = egg.jellybeans;
// Call 1
await DataStore.save(
Child.copyOf(child, updated => {
updated.jellybeans += count;
})
);
// Call 2
await DataStore.save(
Egg.copyOf(egg, updated => {
updated.jellybeans -= count;
})
);
}
John, in offline mode, finds egg #ABC, and his app calls findEgg()
- he got 13 jellybeans! However, unfortunately, Suzie actually had already found egg #ABC while John was offline, and called findEgg()
, and AppSync GraphQL had already given her the 13 jellybeans.
I'm not sure what happens here, but I don't think I like it under any circumstances:
- John and Suzie both tried to set #ABC to a count of 0. AppSync may say "no conflict", while letting John and Suzie both increment their own jellybean count, ruining conservation of jelly beans.
- A conflict was identified and "successfully" resolved as #ABC now has -13 jellybeans. I can imagine how to identify this and prevent it at the resolver level, so let's skip discussion of that.
- AppSync GraphQL rejects John's Call 2 attempt to update #ABC when John later comes online. Presumably Call 1 already resolved without issue, though, which is a problem.
Question Restatement So - how do you structure the models and/or the code in this contrived example, so that AppSync not only notices an issue, but then also backs out mutations on other models that should not succeed in isolation?
What I've tried:
Or rather, not tried, but wondered (because I can't find good examples of this), would be to create something like:
type JellybeanTransfer @model {
child: Child
egg: Egg
}
and then try to save a "new" one of these. The GraphQL resolver doesn't do a braindead CRUD operation, but instead either (A) saves one of these in addition to updating Child and Egg, all via some sort of BEGIN TRANSACTION / END TRANSACTION sql; or (B) doesn't even create a JellybeanTransfer
instance anywhere and treats it as a sort of meta/ephemeral concept. It updates the Child and the Egg, and if it can't do both, backs out the change and rejects the new JellybeanTransfer
.