julienvincent / modelizr

Generate GraphQL queries from models that can be mocked and normalized.
http://julienvincent.github.io/modelizr
176 stars 6 forks source link

How to improve working with models #8

Closed julienvincent closed 7 years ago

julienvincent commented 8 years ago

The Problem

The current implementation of models aren't flexible enough for use in real world applications. As soon as queries require models with different keys, things start to feel hacky and, well, wrong. Take for example the following case:

import { model, query } from 'modelizr'

const user = model('users', { ... })

query(
   user()
)

This makes sense and is easy to use, however things can quickly become messy:

query(
   user(
      book(
         user().as('author')
      )
   ).as('user'),

   book().as('book')
)

Introducing the alias() util and proposal #2 helped clean this up a bit to look like this:

const author = alias(user)

query(
   user('user',
      book(
         author()
      )
   ),

   book('book')
)

However this still feels wrong to me.

As a side effect, specifying whether or not a top level models in a query should be mocked as a collection of or single entity is hard to do. The current implementation is to pass in the models primary key as a parameter, but this won't be possible if querying a type that takes no parameters and returns a singular entity.

Some Alternatives

Here are some alternatives that have crossed my mind. I will add more as I think of them.

Allowing models to infer child model keys

When creating models, part of the process is defining their relationships with other models

user.define({
   textbooks: [book]
})

book.define(
   author: user
)

We could use these definitions to infer what the key should be inside a query

query(
   user(
      book()
   ),

   book(
      user()
   )
)

/*
{
  users {
    ...,
    textbooks {
      ...
    }
  },

  books {
    ...,
    author {
      ...
    }
  }
}
*/

This is already a lot cleaner, but there are some problems.

  1. Model definitions with two relationships using the same model will not be inferable
  2. Doesn't work on top level models
  3. Doesn't solve the problem of single entity mocking for top level models.

Adding a define modifier to our query tools would make this option more viable

query(
   user(
      book()
   ),

   book(
      user()
   )
).define({
  user: user,
  textbooks: arrayOf(book)
})

This will solve problems 2 and 3, albeit messily, but not problem 1.

Create singular and collection models separately

When creating a model, specify the entity key and query key separately. Define collection models by aliasing singular models and wrapping them with arrayOf or valuesOf.

import { model, alias, arrayOf } from 'modelizr'

const user = model('user', 'users', { ... })
const users = arrayOf(alias(user, 'users'))

Although we would still need to use .as in our queries, it would be much clearer as to what is going on, and this would solve the mocks problem

query(
   users(
       book(
           user('author')
       )
   ),

   user()
)

or

const author = alias(user, 'author')

query(
   users(
       book(
           author()
       )
   ),

   user()
)

Much more readable! But it still doesn't feel flexible to me. It's almost as if the confusion that was queries has just moved over to the model creation process.

Input

I hope to get some feedback from other people using or interested in modelizr.

julienvincent commented 7 years ago

The coming V1 should solve the complexity issues described here.