VulcanJS / vulcan-npm

The full-stack JavaScript App Framework
https://vulcan-docs.vercel.app
MIT License
31 stars 8 forks source link

Helper for virtual relations #18

Open eric-burel opened 3 years ago

eric-burel commented 3 years ago

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.".

image

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})
EloyID commented 3 years ago

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 :