vulcan-npm
vulcan-npm copied to clipboard
Helper for virtual relations
Is your feature request related to a problem? Please describe. Imagine you have Todo and User models. A User can have multiple Todos. A Todo can have only one User, its owner.
To create a relationship from Todo to User, you need to a have a field Todo.userId
. You then use the hasOne
helper to create a resolver able to fetch User related to a Todo.
But if you want the reverse, you have 2 patterns:
1. Duplicating ids
You add a field User.todoIds
. You use hasMany
on this field. Limit: it is quite difficult to keep both data in sync. You have to keep track of the Todo userId
but also update the User todoIds
on each mutation.
We technically are already able to code this with hasOne
hasMany
and callbacks.
2. Resolver only
You add User.todos
and create a resolver that can fetch the todos based on Todo.userId
. Limit: it is more costly, as you need to find all users todos among all possible todos. You probably need to set an index on Todo.userId
in your Mongo db to make this efficient.
For this pattern, we currently don't have any helper.
Describe the solution you'd like
A way to define hasOne
or hasMany
relationship WITHOUT creating a new field containing ids, in order to keep a single source of truth.
Note: even if we use Mongo, those are relational patterns. We don't really use the NoSQL approach of Mongo in Vulcan, so that's fine to use patterns you would find traditionally in SQL DBMS.
In Hasura, you can select the reference schema of a relationship. It can be either the current schema, which is equivalent to pattern "1.". But it can also be a different schema, which is equivalent to pattern "2.".
Describe alternatives you've considered
For pattern 1., the current syntax is fine, but you need to define the callbacks carefully. We can keep it as is.
For pattern 2., we could provide some relation
syntax.
Multiple syntaxes are possible, for example:
todos: {
...
relation: {
model: Todo, // model where to look for data
from: "_id", // field in the current schema to compare
to: "userId", // field in the reference schema
kind: 'hasMany'
}
The generated resolver will simply run a query on Todo
collection to find relevant data.
We could also use the normal resolveAs
syntax but provide an helper to create the resolver:
todos: {
resolveAs: {
resolver: arrayRelationResolver({from: "_id", to: "userId", targetModel: Todo})
For pattern 2, I prefer first option, much more simple. Another option
createOneToMany({
from: User
fromField: "_id",
fromResolverField: "todos",
to: Todo,
toField: "userId",
toResolverField: "user"
})
or createRelationship with an option kind: 'hasMany', 'hasOne'...
This function would have inside addField functions to create the needed resolvers/db fields. Advantage : single source of truth (if we delete the function, we delete the relationship), simplicity, out of the box Inconvinients :
- Specifying the field properties (maybe a fieldProps option to say canRead, canUpdate...)
- Code comprehension. Since we declare the relationship only in one place (let's say in users collection) it will be hard to find where the userId relationship has been declared etc.