Giter Club home page Giter Club logo

mst-gql's Introduction

mst-gql

Bindings for mobx-state-tree and GraphQL

Discuss this project on spectrum

CircleCI

🚀 Installation 🚀

Installation: yarn add mobx mobx-state-tree mobx-react react react-dom mst-gql graphql-request

If you want to use graphql tags, also install: yarn add graphql graphql-tag

👩‍🎓 Why 👩‍🎓

Watch the introduction talk @ react-europe 2019: Data models all the way by Michel Weststrate

Both GraphQL and mobx-state-tree are model-first driven approaches, so they have a naturally matching architecture. If you are tired of having your data shapes defined in GraphQL, MobX-state-tree and possible TypeScript as well, this project might be a great help!

Furthermore, this project closes the gap between GraphQL and mobx-state-tree as state management solutions. GraphQL is very transport oriented, while MST is great for client side state management. GraphQL clients like apollo do support some form of client-side state, but that is still quite cumbersome compared to the full model driven power unlocked by MST, where local actions, reactive views, and MobX optimized rendering model be used.

Benefits:

  • Model oriented
  • Type reuse between GraphQL and MobX-state-tree
  • Generates types, queries, mutations and subscription code
  • Strongly typed queries, mutations, result selectors, and hooks! Auto complete all the things!
  • Local views, actions, state and model life-cycles
  • Automatic instance reuse
  • Built-in support for local storage, caching, query caching, subscriptions (over websockets), optimistic updates
  • Idiomatic store organization
  • Incremental scaffolding that preserves changes

👟 Overview & getting started 👟

The mst-gql libraries consists of two parts:

  1. Scaffolding
  2. A runtime library

The scaffolder is a compile-time utility that generates a MST store and models based on the type information provided by your endpoint. This utility doesn't just generate models for all your types, but also query, mutation and subscription code base on the data statically available.

The runtime library is configured by the scaffolder, and provides entry points to use the generated or hand-written queries, React components, and additional utilities you want to mixin to your stores.

Scaffolding

To get started, after installing mst-gql and its dependencies, the first task is to scaffold your store and runtime models based on your graphql endpoint.

To scaffold TypeScript models based on a locally running graphQL endpoint on port 4000, run: yarn mst-gql --format ts http://localhost:4000/graphql. There are several additional args that can be passed to the CLI or put in a config file. Both are detailed below.

Tip: Note that API descriptions found in the graphQL endpoint will generally end up in the generated code, so make sure to write them!

After running the scaffolder, a bunch of files will be generated in the src/models/ directory of your project (or whatever path your provided):

(Files marked ✏ can and should be edited. They won't be overwritten when you scaffold unless you use the force option.)

  • index - A barrel file that exposes all interesting things generated
  • RootStore.base - A mobx-state-tree store that acts as a graphql client. Provides the following:
    • Storage for all "root" types (see below)
    • The .query, .mutate and .subscribe low-level api's to run graphql queries
    • Generated .queryXXX ,.mutateXXX and .subscribeXXX actions based on the query definitions found in your graphQL endpoint
  • RootStore - Extends RootStore.base with any custom logic. This is the version we actually export and use.
  • ModelBase - Extends mst-gql's abstract model type with any custom logic, to be inherited by every concrete model type.
  • XXXModel.base mobx-state-tree types per type found in the graphQL endpoint. These inherit from ModelBase and expose the following things:
    • All fields will have been translated into MST equivalents
    • A xxxPrimitives query fragment, that can be used as selector to obtain all the primitive fields of an object type
    • (TypeScript only) a type that describes the runtime type of a model instance. These are useful to type parameters and react component properties
  • XXXModel - Extends XXXModdel.base with any custom logic. Again, this is the version we actually use.
  • reactUtils. This is a set of utilities to be used in React, exposing the following:
    • StoreContext: a strongly typed React context, that can be used to make the RootStore available through your app
    • useQuery: A react hook that can be used to render queries, mutations etc. It is bound to the StoreContext automatically.

The following graphQL schema will generate the store and message as shown below:

type User {
  id: ID
  name: String!
  avatar: String!
}
type Message {
  id: ID
  user: User!
  text: String!
}
type Query {
  messages: [Message]
  message(id: ID!): Message
  me: User
}
type Subscription {
  newMessages: Message
}
type Mutation {
  changeName(id: ID!, name: String!): User
}

MessageModel.base.ts (shortened):

export const MessageModelBase = ModelBase.named("Message").props({
  __typename: types.optional(types.literal("Message"), "Message"),
  id: types.identifier,
  user: types.union(types.undefined, MSTGQLRef(types.late(() => User))),
  text: types.union(types.undefined, types.string)
})

RootStore.base.ts (shortened):

export const RootStoreBase = MSTGQLStore.named("RootStore")
  .props({
    messages: types.optional(types.map(types.late(() => Message)), {}),
    users: types.optional(types.map(types.late(() => User)), {})
  })
  .actions((self) => ({
    queryMessages(
      variables?: {},
      resultSelector = messagePrimitives,
      options: QueryOptions = {}
    ) {
      // implementation omitted
    },
    mutateChangeName(
      variables: { id: string; name: string },
      resultSelector = userPrimitives,
      optimisticUpdate?: () => void
    ) {
      // implementation omitted
    }
  }))

(Yes, that is a lot of code. A lot of code that you don't have to write 😇)

Note that the mutations and queries are now strongly typed! The parameters will be type checked, and the return types of the query methods are correct. Nonetheless, you will often write wrapper methods around those generated actions, to, for example, define the fragments of the result set that should be retrieved.

Initializing the store

To prepare your app to use the RootStore, it needs to be initialized, which is pretty straight forward, so here is quick example of what an entry file might look like:

// 1
import React from "react"
import * as ReactDOM from "react-dom"
import "./index.css"

import { App } from "./components/App"

// 2
import { createHttpClient } from "mst-gql"
import { RootStore, StoreContext } from "./models"

// 3
const rootStore = RootStore.create(undefined, {
  gqlHttpClient: createHttpClient("http://localhost:4000/graphql")
})

// 4
ReactDOM.render(
  <StoreContext.Provider value={rootStore}>
    <App />
  </StoreContext.Provider>,
  document.getElementById("root")
)

// 5
window.store = rootStore
  1. Typical react stuff, pretty unrelated to this library
  2. Bunch of imports that are related to this lib :)
  3. When starting our client, we initialize a rootStore, which, in typical MST fashion, takes 2 arguments:
    1. The snapshot with the initial state of the client. In this case it is undefined, but one could rehydrate server state here, or pick a snapshot from localStorage, etc.
    2. The transportation of the store. Either gqlHttpClient, gqlWsClient or both need to be provided.
  4. We initialize rendering. Note that we use StoreContext.Provider to make the store available to the rest of the rendering three.
  5. We expose the store on window. This has no practical use, and should be done only in DEV builds. It is a really convenient way to quickly inspect the store, or even fire actions or queries directly from the console of the browser's developer tools. (See this talk for some cool benefits of that)

Loading and rendering your first data

Now, we are ready to write our first React components that use the store! Because the store is a normal MST store, like usual, observer based components can be used to render the contents of the store.

However, mst-gql also provides the useQuery hook that can be used to track the state of an ongoing query or mutation. It can be used in many different ways (see the details below), but here is a quick example:

import React from "react"
import { observer } from "mobx-react"

import { Error, Loading, Message } from "./"
import { useQuery } from "../models/reactUtils"

export const Home = observer(() => {
  const { store, error, loading, data } = useQuery((store) =>
    store.queryMessages()
  )
  if (error) return <Error>{error.message}</Error>
  if (loading) return <Loading />
  return (
    <ul>
      {data.messages.map((message) => (
        <Message key={message.id} message={message} />
      ))}
    </ul>
  )
})

Important: useQuery should always be used in combination with observer from the "mobx-react" or "mobx-react-lite" package! Without that, the component will not re-render automatically!

The useQuery hook is imported from the generated reactUtils, and is bound automatically to the right store context. The first parameter, query, accepts many different types of arguments, but the most convenient one is to give it a callback that invokes one of the query (or your own) methods on the store. The Query object returned from that action will be used to automatically update the rendering. It will also be typed correctly when used in this form.

The useQuery hook component returns, among other things, the store, loading and data fields.

If you just need access to the store, the useContext hook can be used: useContext(StoreContext). The StoreContext can be imported from reactUtils as well.

Mutations

Mutations work very similarly to queries. To render a mutation, the useQuery hook can be used again. Except, this time we start without an initial query parameter. We only set it once a mutation is started. For example the following component uses a custom toggle action that wraps a graphQL mutation:

import * as React from "react"
import { observer } from "mobx-react"

import { useQuery } from "../models/reactUtils"

export const Todo = observer(({ todo }) => {
  const { setQuery, loading, error } = useQuery()
  return (
    <li onClick={() => setQuery(todo.toggle())}>
      <p className={`${todo.complete ? "strikethrough" : ""}`}>{todo.text}</p>
      {error && <span>Failed to update: {error}</span>}
      {loading && <span>(updating)</span>}
    </li>
  )
})

Optimistic updates

The Todo model used in the above component is defined as follows:

export const TodoModel = TodoModelBase.actions((self) => ({
  toggle() {
    return self.store.mutateToggleTodo({ id: self.id }, undefined, () => {
      self.complete = !self.complete
    })
  }
}))

There are few things to notice:

  1. Our toggle action wraps around the generated mutateToggleTodo mutation of the base model, giving us a much more convenient client api.
  2. The Query object created by mutateToggleTodo is returned from our action, so that we can pass it (for example) to the setQuery as done in the previous listing.
  3. We've set the third argument of the mutation, called optimisticUpdate. This function is executed immediately when the mutation is created, without awaiting it's result. So that the change becomes immediately visible in the UI. However, MST will record the patches. If the mutation fails in the future, any changes made inside this optimisticUpdate callback will automatically be rolled back by reverse applying the recorded patches!

Customizing the query result

Mutations and queries take as second argument a result selector, which defines which objects we want to receive back from the backend. Our mutateToggleTodo above leaves it to undefined, which defaults to querying all the shallow, primitive fields of the object (including __typename and id).

However, in the case of toggling a Todo, this is actually overfetching, as we know the text won't change by the mutation. So instead we can provide a selector to indicate that we we are only interested in the complete property: "__typename id complete". Note that we have to include __typename and id so that mst-gql knows to which object the result should be applied!

Children can be retrieved as well by specifying them explicitly in the result selector, for example: "__typename id complete assignee { __typename id name }. Note that for children __typename and id (if applicable) should be selected as well!

It is possible to use gql from the graphql-tag package. This enables highlighting in some IDEs, and potentially enables static analysis.

However, the recommended way to write the result selectors is to use the query builder that mst-gql will generate for you. This querybuilder is entirely strongly typed, provides auto completion and automatically takes care of __typename and id fields. It can be used by passing a function as second argument to a mutation or query. That callback will be invoked with a querybuilder for the type of object that is returned. With the querybuilder, we could write the above mutation as:

export const TodoModel = TodoModelBase.actions((self) => ({
  toggle() {
    return self.store.mutateToggleTodo({ id: self.id }, (todo) => todo.complete)
  }
}))

To select multiple fields, simply keep "dotting", as the query is a fluent interface. For example: user => user.firstname.lastname.avatar selects 3 fields.

Complex children can be selected by calling the field as function, and provide a callback to that field function (which in turn is again a query builder for the appropriate type). So the following example selector selects the timestamp and text of a message. The name and avatar inside the user property, and finally also the likes properties. For the likes no further subselector was specified, which means that only __typename and id will be retrieved.

// prettier-ignore
msg => msg
  .timestamp
  .text
  .user(user => user.name.avatar)
  .likes()
  .toString()

To create reusable query fragments, instead the following syntax can be used:

import { selectFromMessage } from "./MessageModel.base"

// prettier-ignore
export const MESSAGE_FRAGMENT = selectFromMessage()
  .timestamp
  .text
  .user(user => user.name.avatar)
  .likes()
  .toString()

Customizing generated files

You can customize all of the defined mst types: RootStore, ModelBase, and every XXXModel.

However, some files (including but not limited to .base files) should not be touched, as they probably need to be scaffolded again in the future.

Thanks to how MST models compose, this means that you can introduce as many additional views, actions and props as you want to your models, by chaining more calls unto the model definitions. Those actions will often wrap around the generated methods, setting some predefined parameters, or composing the queries into bigger operations.

Example of a generated model, that introduces a toggle action that wraps around one of the generated mutations:

// src/models/TodoModel.js
import { TodoModelBase } from "./TodoModel.base"

export const TodoModel = TodoModelBase.actions((self) => ({
  toggle() {
    return self.store.mutateToggleTodo({ id: self.id })
  }
}))

That's it for the introduction! For the many different ways in which the above can applied in practice, check out the examples

Server side rendering with react

There is an exported function called getDataFromTree which you can use to preload all queries, note that you must set ssr: true as an option in order for this to work

async function preload() {
  const client = RootStore.create(undefined, {
    gqlHttpClient: createHttpClient("http://localhost:4000/graphql"),
    ssr: true
  })
  const html = await getDataFromTree(<App client={client} />, client)
  const initalState = getSnapshot(client)

  return [html, initalState]
}

null vs. undefined

Because you can control what data is fetched for a model in graphql and mst-gql it is possible for a model to have some fields that have not yet been fetched from the server. This can complicate things when we're talking about a field that can also be "empty". To help with this a field in mst-gql will be undefined when it has not been fetched from the server and, following graphql conventions, will be null if the field has been fetched but is in fact empty.


🍿 In-depth store semantics 🍿

mst-gql generates model types for every object type in your graphql definition. (Except for those excluded using the excludes flag). For any query or mutation that is executed by the store, the returned data will be automatically, and recursively parsed into those generated MST models. This means that for any query, you get a 'rich' object back. Finding the right model type is done based on the GraphQL meta field __typename, so make sure to include it in your graphql queries!

The philosophy behind MST / mst-gql is that every 'business concept' should exist only once in the client state, so that there is only one source of truth for every message, usage, order, product etc. that you are holding in memory. To achieve this, it is recommended that every uniquely identifyable concept in your application, does have an id field of the graphQL ID type. By default, any object types for which this is true, is considered to be a "root type".

Root types have few features:

  1. It is guaranteed that any data related to the same id will be updating the very same MST model instance.
  2. All instances of root types are stored on the RootStore, for quick and easy lookups.
  3. If an object is referring to a root type, a true MST types.reference will be used to establish the reference. This means you can use deep fields in the UI, like message.author.name, despite the fact that this data is stored normalized in the store.
  4. Instances of the root types, and all their children, are cached automatically in the root store (until removed manually).

GraphQL has no explicit distinction between compositional and associative relationships between data types. In general, references between graphQL objects are dealt with as follows.

  1. If an object is referring to a root type, a types.reference is used, e.g.: author: types.reference(UserModel)
  2. If an object is not referring to a root type, but a matching MST model type exist, a composition relationship is used, for example: comments: types.array(CommentModel)
  3. If no model type is known for the queried object type, a types.frozen is used, and the data as returned from the query is stored literally.

Dealing with incomplete objects

GraphQL makes it possible to query a subset of the fields of any object. The upside of this is that data traffic can be minimized. The downside is that it cannot be guaranteed that any object is loaded in its 'complete' state. It means that fields might be missing in the client state, even though are defined as being mandatory in the original graphQL object type! To verify which keys are loaded, all models expose the hasLoaded(fieldName:string):boolean view, which keeps track of which fields were received at least once from the back-end.

Query caching

As described above, (root) model instances are kept alive automatically. Beyond that, mst-gql also provides caching on the network level, based on the query string and variables, following the policies of the apollo and urql graphQL clients. The following fetch policies are supported:

  • `"cache-first": Use cache if available, avoid network request if possible
  • `"cache-only": Use cache if available, or error if this request was not made before
  • `"cache-and-network": Use cache, but still send request and update cache in the background
  • `"network-only": Skip cache, but cache the result
  • `"no-cache": Skip cache, and don't cache the response either

The default policy is cache-and-network. This is different from other graphQL clients. But since mst-gql leverages the MobX reactivity system, this means that, possibly stale, results are shown on screen immediately if a response is in cache, and that the screen will automatically update as soon as a new server response arrives.

The query cache is actually stored in MST as well, and can be accessed through store.__queryCache.

Since the query cache is stored in the store, this means that mixins like useLocalStore will serialize them. This will help significantly in building offline-first applications.


🦄 API 🦄

CLI

The mst-gql command currently accepts the following arguments:

  • --format ts|js|mjs The type of files that need to be generated (default: js)

  • --outDir <dir> The output directory of the generated files (default: src/models)

  • --excludes 'type1,type2,typeN' The types that should be omitted during generation, as we are not interested in for this app.

  • --roots 'type1,type2,typeN' The types that should be used as (root types)[#root-types]

  • --modelsOnly Generates only models, but no queries or graphQL capabilities. This is great for backend usage, or if you want to create your own root store

  • --noReact doesn't generate the React related utilities

  • --force When set, exiting files will always be overridden. This will drop all customizations of model classes!

  • --dontRenameModels By default generates model names from graphql schema types that are idiomatic Javascript/Typescript names, ie. type names will be PascalCased and root collection names camelCased. With --dontRenameModels the original names - as provided by the graphql schema - will be used for generating models.

  • --useIdentifierNumber Specifies the use of identifierNumber instead of identifier as the mst type for the generated models IDs. This requires your models to use numbers as their identifiers. See the mobx-state-tree for more information.

  • --fieldOverrides id:uuid:idenfitier,*:ID:identifierNumber Overrides default MST types for matching GraphQL names and types. The format is gqlFieldName:gqlFieldType:mstType. Supports full or partial wildcards for fieldNames, and full wildcards for fieldTypes. Case Sensitive. If multiple matches occur, the match with the least amount of wildcards will be used, followd by the order specified in the arg list if there are still multiple matches. Some examples:

    • *_id:*:string - Matches any GQL type with the field name *_id (like user_id), and uses the MST type types.string

    • *:ID:identifierNumber - Matches any GQL type with any field name and the ID type, and uses the MST type types.identifierNumber

    • User.user_id:ID:number - Matches the user_id field on User with the GQL type ID, and uses the MST type types.number

      Specifying this argument additionaly allows the use of multiple IDs on a type. The best matched ID will be used, setting the other IDs to types.frozen()

    • Book.author_id:ID:identifierNumber - Matches the author_id field on Book with the GQL type ID and uses the MST type types.identifierNumber, and sets any other GQL IDs on Book to types.frozen()

      For TS users, input types and query arguments will only be modified for fieldOverrides with a wildcard for gqlFieldName (*:uuid:identifier). An override like *_id:uuid:identifier will not affect input types.

      The primary use case for this feature is for GQL Servers that don't always do what you want. For example, Hasura does not generate GQL ID types for UUID fields, which causes issues when trying to reference associate types in MST. To overcome this, simply specify --fieldOverrides *:UUID:identifier

    • *.timestamp:*:DateScalar:../scalars - Matches any GQL type with the field name timestamp, and uses the MST type DateScalar imported from file ../scalars. Usually used for graphql custom scalar support with MST type.custom

  • source The last argument is the location at which to find the graphQL definitions. This can be

    • a graphql endpoint, like http://host/graphql
    • a graphql files, like schema.graphql
    • a parsed graphql file, like schema.json

Config

mst-gql also supports cosmiconfig as an alternative to using cli arguments.

A sample config can be found in Example 2.

RootStore

The generated RootStore exposes the following members:

query(query, variables, options): Query

Makes a graphQL request to the backend. The result of the query is by default automatically normalized to model instances as described above. This method is also used by all the automatically scaffolded queries.

  • The query parameter can be a string, or a graphql-tag based query.
  • Variables are the raw JSON data structures that should be send as variable substitutions to the backend. This parameter is optional.
  • Options is an optional QueryOptions object. The defaults are fetchPolicy: "cache-and-network" and noSsr: false
  • The method returns a Query that can be inspected to keep track of the request progress.

Be sure to at least select __typename and id in the result selector, so that mst-gql can normalize the data.

mutate(query, variables, optimisticUpdate): Query

Similar to query, but used for mutations. If an optimisticUpdate thunk is passed in, that function will be immediately executed so that you can optimistically update the model. However, the patches that are generated by modifying the tree will be stored, so that, if the mutation ultimately fails, the changes can be reverted. See the Optimistic updates section for more details.

subscribe(query, variables, onData): () => void

Similar to query, but sets up an websocket based subscription. The gqlWsClient needs to be set during the store creation to make this possible. onData can be provided as callback for when new data arrives.

Example initalization:

import { SubscriptionClient } from "subscriptions-transport-ws"

build a websocket client:

// see: https://www.npmjs.com/package/subscriptions-transport-ws#hybrid-websocket-transport
const gqlWsClient = new SubscriptionClient(constants.graphQlWsUri, {
  reconnect: true,
  connectionParams: {
    headers: { authorization: `Bearer ${tokenWithRoles}` }
  }
})

add the ws client when creating the store:

// see: https://github.com/mobxjs/mst-gql/blob/master/src/MSTGQLStore.ts#L42-L43
const store = RootStore.create(undefined, {
  gqlHttpClient,
  gqlWsClient
})

When using server side rendered tools like gatsby/next/nuxt it is necessary to prevent using subscriptions server side. An error will occur because the server is missing a websocket implementation. See code example for gatsby.

Generated queries, mutations and subscriptions

Based on the queries, mutations and subscriptions defined at the endpoint, mst-gql automatically scaffolds methods for those onto the base root store.

This is very convenient, as you might not need to write any graphQL queries by hand yourself in your application. Beyond that, the queries now become strongly typed. When using TypeScript, both the variables and the return type of the query will be correct.

An example signature of a generated query method is:

queryPokemons(variables: { first: number }, resultSelector = pokemonModelPrimitives, options: QueryOptions = {}): Query<PokemonModelType[]>

All parameters of this query are typically optional (unless some of the variables are requires, like in the above example).

The result selector defines which fields should fetched from the backend. By default mst-gql will fetch __typename, ID and all primitive fields defined in the model, but full free to override this to make more fine tuned queries! For better reuse, consider doing this in a new action on the appropiate model. For example a query to fetch all comments and likes for a message could look like:

import { MessageBaseModel } from "./MessageModel.base"

const MessageModel = MessageBaseModel.actions((self) => ({
  queryCommentsAndLikes(): Query<MessageModelType> {
    return store.queryMessage(
      { id: self.id },
      `
      id
      __typename
      comments {
        id
        __typename
        text
        likes {
          __typename
          author
        }
      }
    `
    )
  }
}))

Other store methods

  • Not a method, but RootStoreType can be used for all places in TypeScript where you need the instance type of the RootStore.
  • rawRequest(query: string, variables: any): Promise. Makes a direct, raw, uncached, request to the graphQL server. Should typically not be needed.
  • __queryCache. See Query caching. Should typically not be needed.
  • merge(data). Merges a raw graphQL response into the store, and returns a new tree with model instances. See In-depth store semantics. Should typically not be needed.

Models

The generated models provide storage place for data returned from GraphQL, as explained above. Beyond that, it is the place where you enrich the models, with client-side only state, actions, derived views, etc.

For convenience, each model does provide two convenience views:

  • hasLoaded(field) returns true if data for the specified field was received from the server
  • store: a strongly typed back-reference to the RootStore that loaded this model

Beyond that, the the following top-level exports are exposed from each model file:

  • xxxPrimitives: A simple string that provides a ready-to-use selector for graphQL queries, that selects all the primitive fields. For example: "__typename id title text done
  • xxxModelType: A TypeScript type definition that can be used in the application if you need to refer to the instance type of this specific model
  • selectFromXXX(): Returns a strongly typed querybuilder that can be used to write graphql result selector fragments more easily. Don't forget to call toString() in the end!

QueryOptions

export interface QueryOptions {
  fetchPolicy?: FetchPolicy
  noSsr?: boolean
}

See Query caching for more details on fetchPolicy. Default: "cache-and-network"

The noSsr field indicates whether the query should be executed during Server Side Rendering, or skipped there and only executed once the page is loaded in the browser. Default: false

createHttpClient(url: string, options: HttpClientOptions = {})

Creates a http client for transportation purposes. For documentation of the options, see: https://github.com/prisma/graphql-request

import { createHttpClient } from "mst-gql"
import { RootStore } from "./models/RootStore"

const gqlHttpClient = createHttpClient("http://localhost:4000/graphql")

const rootStore = RootStore.create(undefined, {
  gqlHttpClient
})

Creating a websocket client

Creating a websocket client can be done by using the subscriptions-transport-ws package, and passing a client to the store as gqlWsClient environment variable:

import { SubscriptionClient } from "subscriptions-transport-ws"

import { RootStore } from "./models/RootStore"

const gqlWsClient = new SubscriptionClient("ws://localhost:4001/graphql", {
  reconnect: true
})

const rootStore = RootStore.create(undefined, {
  gqlWsClient
})

Query object

Query objects capture the state of a specific query. These objects are returned from all query and mutate actions. Query objects are fully reactive, which means that if you use them in observer component, or any other reactive MobX mechanism, such as autorun or when, they can be tracked.

Beyond that, query objects are also then-able, which means that you can use them as a promise. The complete type of a query object is defined as follows:

class Query<T> implements PromiseLike<T> {
  // Whether the Query is currently fetching data from the back-end
  loading: boolean

  // The data that was fetched for this query.
  // Note that data might be available, even when the query object is still loading,
  // depending on the fetchPolicy
  data: T | undefined

  // If any error occurred, it is stored here
  error: any

  // Forces the query to re-executed and make a new roundtrip to the back-end.
  // The returned promise settles once that request is completed
  refetch = (): Promise<T> => {

  // case takes an object that should have the methods `error`, `loading` and `data`.
  // It immediately calls the appropriate handler based on the current query status.
  // Great tool to use in a reactive context, comparable with mobx-utils.fromPromise
  case<R>(handlers: {
    loading(): R
    error(error: any): R
    data(data: T): R
  }): R

  // Returns the promise for the currently ongoing request
  // (note that for example `refetch` will cause a new promise to become the current promise)
  currentPromise()

  // A short-cut to the .then handler of the current promise
  then(onResolve, onError)

StoreContext

In the generated reactUtils you will find the StoreContext, which is a pre-initialized React context that can be used to distribute the RootStore through your application. It's primary benefit is that it is strongly typed, and that Query components will automatically pick up the store distributed by this context.

useQuery hook

The useQuery hook, as found in reactUtils can be used to create and render queries or mutations in React.

The useQuery hook should always be used inside an observer (provided by the mobx-react or mobx-react-lite package) based component!

It accepts zero, one or 2 arguments:

  • query, the query to execute. This parameter can take the following forms:
    • Nothing - the parameter is optional, in case you want to only set the query to be tracked later on using setQuery, for example when a mutation should be tracked.
    • A string, e.g. query messages { allMessages { __typename id message date }}
    • A graphql-tag based template string
    • A Query object
    • A callback, that will receive as first argument the store, and should return a Query object. The callback will be invoked when the component is rendered for the first time, and is a great way to delegate the query logic itself to the store. This is the recommend approach. For example store => store.queryAllMessages()
  • options, an object which can specify further options, such as
    • variables: The variables to be substituted into the graphQL query (only used if the query is specified as graphql tag or string!)
    • fetchPolicy: See fetch policy
    • noSsr: See the noSsr option of queries
    • store: This can be used to customize which store should be used. This can be pretty convenient for testing, as it means that no Provider needs to be used.

The query component takes a render callback, that is rendered based on the current status of the Query objects that is created based on the query property. The callback is also automatically wrapped in MobX-reacts' observer HoC.

The hook returns one object, with the following properties:

  • loading
  • error
  • data
  • store
  • query - the current Query object
  • setQuery - replaces the current query being rendered. This is particularly useful for mutations or loading more data

The useQuery hook is strongly typed; if everything is setup correctly, the type of data should be inferred correctly when using TypeScript.

For examples, see the sections Loading and rendering your first data and Mutations.

localStorageMixin

The localStorageMixin can be used to automatically save the full state of the RootStore. By default the store is saved after every change, but throttle to be saved once per 5 seconds. (The reason for the throttling is that, although snapshotting is cheap, serializing a a snapshot to a string is expensive). If you only want to persist parts of the store you can use the filter option to filter which keys that should be stored.

Options:

  • storage (the storage object to use. Defaults to window.localStorage)
  • throttle (in milliseconds)
  • storageKey (the key to be used to store in the local storage).
  • filter (an optional array of string keys that determines which data that will be stored to local storage)

Example:

models/RootStore.js

const RootStore = RootStoreBase.extend(
  localStorageMixin({
    throttle: 1000,
    storageKey: "appFluff"
    filter: ['todos', 'key.subkey']
  })
)

Use with react-native

To use this mixin with react-native you can pass AsyncStorage to the mixin using the storage option:

Example:

models/RootStore.js

import AsyncStorage from "@react-native-community/async-storage"

const RootStore = RootStoreBase.extend(
  localStorageMixin({
    storage: AsyncStorage,
    throttle: 1000,
    storageKey: "appFluff"
  })
)

🙈 Examples 🙈

This project contains usage exampels in the examples directory showcasing various ways mst-gql can be used.

Running the examples

  1. Make sure to run yarn in the root directory of this project before running an example.
  2. instructuoins for each example can be found in the README.md within the example folder.

Overview of the examples:

1. Getting started

The 1-getting-started example is a very trivial project, that shows how to use mst-gql together with TypeScript and React. Features:

  • React
  • TypeScript
  • Scaffolding
  • Simple query
  • Simple mutation
  • Customizes TodoModel by introduce an toggle action, which uses an optimistic update.
  • Renders loading state

2. Scaffolding

The 2-scaffolding examples generates code for a non trivial projects and runs it through the compiler.

3. Twitter clone

3-twitter-clone Is the most interesting example project. Highlights:

  • Shows a twitter feed using a subscription over websocket
  • A load more button for paging
  • Tweets can be expanded (to show replies) and liked
  • It is possible to compose new tweets
  • The data model has references, such as MessageModel.user and MessageModel.likes.
  • MessageModel.replyTo is field that refers to a MessageModel, so that a tweet tree can be expressed.
  • When changing the name of the currently logged in user, this is properly reflected in the UI, thanks to the normalization and MobX reactivity. There is non need to re-fetch the tweet wall.
  • MessageModel.isLikedByMe introduce a client-only derived view.
  • To store the message order (new messages go in front, messages inserted by loading more data are appended to the end), the RootStore has a property sortedMessages to store local state.
  • All the query logic is abstracted into the models, so that the UI doesn't has as little logic as possible.
  • The twitter example not only scaffolds the client side models, it also scaffolds models to be used on the server!

4. Apollo tutorial

4-apollo-tutorial is a port of the apollo full-stack tutorial. Note that the example doesn't use apollo anymore. See it's readme for specific install instructions.

The examples has a lot of similarities with example 3, and also has

  1. routing
  2. leverages the caching policies in several views, such as switching to specific views, responding initially with cached results until fresh data is fetched
  3. Uses the localStorageMixin so that the app can start without network requests

5. Next.js

5-nextjs an example using next. Highlights:

  1. Server Side Rendering

Tips & tricks

If the result of a query doesn't show up in the store

... you might have forgotten to include __typename or id in the result selector of your string or graphql-tag based queries.

Views is stuck is in loading state

If the view is stuck in loading state, but you can see in the network requests that you did get a proper response, you probably forget to include observer on the component that renders the query

Setup prettier to ignore generated files

If you are using prettier, it is strongly recommended to make sure that the files that are generated over and over again, are not formatted, by setting up a .prettierignore file.

src/models/index.*
src/models/reactUtils.*
src/models/*.base.*
src/models/*Enum.*

Or, alternatively, if you want to properly format the generated files based on your standards, make sure that you always run prettier on those files after scaffolding.

Keep components dumb

In general we recommend to keep the components dumb, and create utility functions in the store or models to perform queries needed for a certain UI component. This encourages reuse of queries between components. Furthermore, it makes testing easier, as it will be possible to test your query methods directly, without depending on rendering components. As is done for example here

Paging, search state or other complex ui states

...are best modelled using separate models, or by introducing additional properties and actions to keep track of paging, offset, search filters, etcetera. This is done for example in the twitter example and the apollo example

Mutations should select the fields they change

Mutation should select the fields they change in the result selection

Using mst-gql with other graphql clients

It is possible to scaffold with the --modelsOnly flag. This generates a RootStore and the model classes, but no code for the queries or React, and hence it is environment and transportation independent. Use this option if you want to use models on the server, or on the client in combination with another graphql client. Use store.merge(data) to merge in query results you get from your graphql client, and get back instantiated model objects.

Stub the transportation layer in unit tests

It is quite easy to stub away the backend and transportation layer, by providing a custom client to the rootStore, as is done here.

mst-gql's People

Contributors

aryk avatar barbalex avatar beepsoft avatar bradenm avatar chrisdrackett avatar dependabot[bot] avatar dpnolte avatar elie222 avatar emckay avatar filippodossena-mozart avatar jesse-savary avatar joaomanke avatar jovidecroock avatar laurenfackler avatar metevier avatar mtsewrs avatar mwarger avatar mweststrate avatar pvpshoot avatar rdewolff avatar rxminus avatar scherler avatar smokku avatar special-character avatar vamshi9666 avatar weglov avatar wtuminski avatar yasinuslu avatar zenflow avatar zpalin avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

mst-gql's Issues

Enable enums to be used in queries

Currently, GraphQL Enum types are converted into MST's types.enumeration. However, there are still some open ends in supporting enums to use them in queries:

  • Enums fields are not added to the ModelSelector/QueryBuilder of the containing model
    • probably require changes in generateFragments in generate.js
  • Enums are not typed when being used as a query variable
    • probably require changes in generateQueryBuilder in generate.js

If anyone would be able to pick this up, great. Otherwise I might do this later.

export enum types when outputting to typescript

for example instead of:

/* This is a mst-gql generated file, don't modify it manually */
/* eslint-disable */
/* tslint:disable */
import { types } from "mobx-state-tree"

/**
* LogType
*/
export const LogTypeEnum = types.enumeration("LogType", [
        "DAILY",
  "MONTHLY",
  "YEARLY",
      ])

output:

/* This is a mst-gql generated file, don't modify it manually */
/* eslint-disable */
/* tslint:disable */
import { types } from "mobx-state-tree"

export type LogEnumType = "DAILY" | "MONTHLY" | "YEARLY"

/**
* LogType
*/
export const LogTypeEnum = types.enumeration("LogType", [
        "DAILY",
  "MONTHLY",
  "YEARLY",
      ])

The name XXXEnumType is terrible and too close to the MST name of XXXTypeEnum. Any suggestions of a better name?

[Bug] Fresh data gets overwritten with stale data from __queryCache

(almost) minimal reproduction repo

https://github.com/zenflow/mst-gql-next/tree/demo-bug-stale-data-reused

I removed almost all of the code unrelated to demonstrating this bug. Pardon the next.js integration bit; it has zero impact here.

The relevant parts are in server/schema.js and pages/index.js

steps to reproduce

  1. Open up the app
    image
  2. Click the "Show" button under the "Done Todos" heading. This fetches todos from a separate query in the graphql schema, doneTodos.
    image
  3. Click the "Hide button. The screen should look the same as in step 1.
  4. Toggle one or more of the todos.
    image
  5. Click the "Show" button under "Done Todos" again.
    image

You can see that the todo items I toggled have been reverted to their original state. 💩

This example is using the default fetchPolicy "cache-and-network", so immediately after clicking "Show", the stale cached query (from when we opened the "Done Todos" the first time) is used for an instant before the freshly fetched query is received, and in using it, (here's the problem), it's model properties/data are written to the "living" model instances in store.todos.

The bug is easier to observe with the "cache-first" fetchPolicy, but I wanted to demonstrate that this is fairly severe bug in the typical/basic usage (i.e. not doing anything special).

I believe mst-gql should simply (?) never copy data from the __queryCache onto root type model instances, since that data will never be more fresh than the data the model instance already has (from other queries, and even optimistic updates). Really it seems that __queryCache shouldn't need to keep properties/data of root types at all, and in the case of query results that are an array of root types, the entry value (in __queryCache) should be just an array of IDs. That would also make things much more efficient when serializing/deserializing state for SSR, localStorage, etc.

Is this likely to be fixed?

BTW, it would be awesome if mst-gql had a model instance for the root graphql type Query {} that worked generally the same as the other graphql models, and thus would:

  • Be able to replace the __queryCache (since the Query model would have the same necessary data from __queryCache)
  • Be part of the normalized data model. No duplicate (stale) data. References are used in place of actual data for other "root types".
  • Allow for optimistic updates to properties of the root graphql Query type! That would mean we can optimistically update all parts of our query results. Not just properties of [other] root type instances, but also which instances are included in query results, and non-root type data in query results. Taking the reproduction repo app for example, in the optimistic update function for toggling a todo, we could (in addition to toggling the done property) add/remove it to/from the store.rootQuery.doneTodos array (of course only if store.rootQuery.hasLoaded('doneTodos')) and then our list of "Done Todos" would stay up to date as we make changes!

Thanks for your work on a super awesome and powerful package. ❤️ 💪

React should be opt-in or opt-out

I think the generation of the code related to React should be an opt-in feature. Store management should be agnostic.

Or at least, it should be possible to give an option to not output React code.

can't resolve enum

I have some enums in my types and after generating my models I get the following error:

/@calm/core/src/models/ItemModel.base.js
Module not found: Can't resolve './ItemTypeEnum.base' in '/@calm/core/src/models'

In ItemModel.base.js I see an unused import:

import { ItemTypeEnumSelector } from './ItemTypeEnum.base'

I wonder if this is suppose to be used below in this file under ItemModelSelector?

Sorry if this is related to #25 I'm still figuring everything out!

[Question] Correct way to update access token in createHTTPClient

I'm using MST-GQL in my app and I have a "currentUser" store where I have a login function which receives and sets the access token. I want to be able to use this access token in the "Auth Bearer" header for all further requests. How do I set the header after the RootStore has been initialised?

Running examples

Some issues running examples. I've tried:

yarn
yarn prepare-examples

But get this error:

"./mst-gql1557842644.tgz": Tarball is not in network and can not be located in cache

I don't understand why this is a dependency in the package.json either:

"mst-gql": "file:./mst-gql1557842644.tgz",

types on `data` returned from useQuery not correct

I think this is related to #47. it seems now that data is returned in the following format given something like queryUser() I now get:

data = {
   user: {
     email: '[email protected]'
   }
}

however data is still typed as an instance of User so doing data.user.email gives me an error:

Property 'user' does not exist on type 'ModelInstanceTypeProps…

seems like we need to update the types for the data bit of the useQuery hook.

optimistic updates with new items

I'm curious if anyone has thoughts around supporting optimistic updates when adding new items to the store. Because items are currently stored under their ID (which won't be available until a response comes back from the server) I'm not sure the best way to handle this case, if it is even possible.

[SSR] getDataFromTree ignores (doesn't wait for) queries triggered after loading first queries

Sometimes we have a component that makes a query then renders some component(s) that makes more queries.

Currently getDataFromTree does not fulfill those queries that are made after the first query(s) are done. The page is server-rendered while all queries beyond the first "layer" are still not loaded. The second layer of queries is actually triggered [server-side] by the second and final rendering of the react element tree, but the results come after the page has been sent.

You can see this issue in action in my branch for #93 . When page opens in browser, the list(s) are populated, but the "Assignee" for each list item is still "Loading...".

This is a problem when you want to take advantage of SSR while taking the approach of using components (or groups of components) that declare their own data dependencies (e.g. PR #93) rather than the approach of depending on data that was fetched in one monolithic (i.e. for every element on the page) request by code somewhere up the hierarchy of react elements. In this [first, components-requesting-their-own-data] case you would typically have multiple "layers" of queries to make. (Hopefully at some point we will be able to opt-in to batching queries together in each layer (#4), or at least have them deduplicated automatically, which seems straightforward & uncontroversial enough.)

This is something that works in apollo, as you can see from inspecting it's getDataFromTree implementation: process() (which includes rendering tree) calls itself recursively while hasPromises().

So in a similar fashion, we should in our getDataFromTree implementation probably be, after awaiting promises, re-rendering and checking for more promises. Something like:

export async function getDataFromTree<STORE extends typeof MSTGQLStore.Type>(
  tree: React.ReactElement<any>,
  client: STORE,
  renderFunction: (
    tree: React.ReactElement<any>
  ) => string = require("react-dom/server").renderToStaticMarkup
): Promise<string> {
  while (true) {
    const html = renderFunction(tree)
    if (client.__promises.size === 0) {
      return html
    }
    await Promise.all(client.__promises)
  }
}

But if you are using the default fetchPolicy "cache-and-network" for your queries, that will cause an infinite loop, since each rendering will cause a network request for every query, including ones that have already been requested and cached.

Apollo deals with this (as well as the next issue I am about to document) by, depending on ssrMode & ssrForceFetchDelay options, forcing fetchPolicy to "cache-only" for queries with one of the fetch policies that always uses network.

The semantics of those options are that ssrMode: true disables network fetch policies for good, while using ssrForceFetchDelay: 100 will disable them for 100ms and then start allowing them. I think we could simplify this:

I propose that when the existing ssr [client constructor] option is true, we disable network fetch policies (by forcing the policy to "cache-first" as needed) only during server-side rendering and initial client-side re-rendering, and, without needing to opt-in via a ssrForceFetchDelay, after that allow use of whatever fetch policy developer wants.

@mattiasewers @chrisdrackett What do you think? Happy to put in a PR for this!

using useQuery with TypeScript

How to use useQuery with typescript.
Folowing code gives me error

Expected 1-3 arguments, but got 0.  TS2554

and VSCODE

(method) queryTodoItems(variables: {
    where: TodoItemWhereInput | undefined;
    orderBy: TodoItemOrderByInput;
    skip: number | undefined;
    after: string | undefined;
    before: string | undefined;
    first: number | undefined;
    last: number | undefined;
}, resultSelector?: string | ((qb: TodoItemModelSelector) => TodoItemModelSelector), options?: QueryOptions): Query<...>
Expected 1-3 arguments, but got 0.ts(2554)
RootStore.base.ts(99, 20): An argument for 'variables' was not provided.
import axios from "axios";
import "./App.css";
import { observer } from "mobx-react";
import { useQuery } from "./models/reactUtils";

interface ItodoItem {
  id: string;
  text: string;
  completed: boolean;
}
interface ItodoItems extends Array<ItodoItem> {}

const App: React.FC = observer(() => {
  const { store, error, loading, data } = useQuery(store =>
    store.queryTodoItems()
  );
  if (error) return <div>{error.message}</div>;
  if (loading) return <div>loading</div>;
  return <ul>git</ul>;
});

export default App;

support returning enums in queries

I'm trying to get the value of an enum in for a given type. given the following schema:

enum ItemType {
  EVENT
  NOTE
  TASK
}

type Item {
  id: ID!
  title: String!
  type: ItemType!
}

type Query {
  item(id: ID!): Item!
  items: [Item!]!
}

I'm trying to do:

  const { error, loading, data: items } = useQuery((store) =>
    store.queryItems({}, (item) => item.type.title),
  )

however it seems that type isn't available in this case. Sorry if this is a known issue with enums!

Support apollo client?

Currently, mst-gql is based on graphql-request. Not using apollo client has a few benefits

  • much smaller footprint
  • easy / simple setup

However, using apollo might have soon benefits as well

  • gradual adoption, if the apollo client can be used as networking layer
  • benefit from features not present in mst-gql, or which are in a more mature state in apollo
  • It could be interesting to research whether the mst-gql RootStore could act as appollo's caching layer, as it is serializable out of the box for example.

Using apollo as transportation layer is possibly already easy to achieve, by creating an { request(query, variables): Promise } object, that wraps around apollo client, and providing that as httpGqlClient when creating a store

curious what to do with datetime or similar

I have a scalar DateTime that I'd love to teach mst-gql to treat as a Date.

So far I've tried editing my models/<item>Model.ts to have:

export const ItemModel = ItemModelBase.props({
  createdAt: types.maybe(types.Date),
})

and that much seems to be working, however I'm not sure where to put the code that takes the string returned from the server and transforms it into a Date.

Looking for maintainers!

I think this project is very promising. However, since I am currently not running any projects that use mobx-state-tree and GraphQL in real life. And beyond that, I'm currently running to many OSS projects already :)

So, consider becoming a maintainer of this project if you want to see this project to be continued!

"RootStoreBase implicitly has type 'any' ..." when generating models for a Hasura schema

Hi,

I'm trying to generate mst-gql models from a Hasura (v1.0.0-beta.4) generated graphql shema.

This is the database schema (basic Todo) I use in postgresql:

create table todo
(
	id bigserial not null
		constraint todo_pkey
			primary key,
	text text not null,
	complete boolean not null
);

Hasura generates this schema for it

schema {
  query: query_root
  mutation: mutation_root
  subscription: subscription_root
}

scalar bigint

# expression to compare columns of type bigint. All fields are combined with logical 'AND'.
input bigint_comparison_exp {
  _eq: bigint
  _gt: bigint
  _gte: bigint
  _in: [bigint]
  _is_null: Boolean
  _lt: bigint
  _lte: bigint
  _neq: bigint
  _nin: [bigint]
}

# expression to compare columns of type boolean. All fields are combined with logical 'AND'.
input boolean_comparison_exp {
  _eq: Boolean
  _gt: Boolean
  _gte: Boolean
  _in: [Boolean]
  _is_null: Boolean
  _lt: Boolean
  _lte: Boolean
  _neq: Boolean
  _nin: [Boolean]
}

# conflict action
enum conflict_action {
  # ignore the insert on this row
  ignore

  # update the row with the given values
  update
}

# mutation root
type mutation_root {
  # delete data from the table: "todo"
  delete_todo(
    # filter the rows which have to be deleted
    where: todo_bool_exp!
  ): todo_mutation_response

  # insert data into the table: "todo"
  insert_todo(
    # the rows to be inserted
    objects: [todo_insert_input!]!

    # on conflict condition
    on_conflict: todo_on_conflict
  ): todo_mutation_response

  # update data of the table: "todo"
  update_todo(
    # increments the integer columns with given value of the filtered values
    _inc: todo_inc_input

    # sets the columns of the filtered rows to the given values
    _set: todo_set_input

    # filter the rows which have to be updated
    where: todo_bool_exp!
  ): todo_mutation_response
}

# column ordering options
enum order_by {
  # in the ascending order, nulls last
  asc

  # in the ascending order, nulls first
  asc_nulls_first

  # in the ascending order, nulls last
  asc_nulls_last

  # in the descending order, nulls first
  desc

  # in the descending order, nulls first
  desc_nulls_first

  # in the descending order, nulls last
  desc_nulls_last
}

# query root
type query_root {
  # fetch data from the table: "todo"
  todo(
    # distinct select on columns
    distinct_on: [todo_select_column!]

    # limit the nuber of rows returned
    limit: Int

    # skip the first n rows. Use only with order_by
    offset: Int

    # sort the rows by one or more columns
    order_by: [todo_order_by!]

    # filter the rows returned
    where: todo_bool_exp
  ): [todo!]!

  # fetch aggregated fields from the table: "todo"
  todo_aggregate(
    # distinct select on columns
    distinct_on: [todo_select_column!]

    # limit the nuber of rows returned
    limit: Int

    # skip the first n rows. Use only with order_by
    offset: Int

    # sort the rows by one or more columns
    order_by: [todo_order_by!]

    # filter the rows returned
    where: todo_bool_exp
  ): todo_aggregate!

  # fetch data from the table: "todo" using primary key columns
  todo_by_pk(id: bigint!): todo
}

# subscription root
type subscription_root {
  # fetch data from the table: "todo"
  todo(
    # distinct select on columns
    distinct_on: [todo_select_column!]

    # limit the nuber of rows returned
    limit: Int

    # skip the first n rows. Use only with order_by
    offset: Int

    # sort the rows by one or more columns
    order_by: [todo_order_by!]

    # filter the rows returned
    where: todo_bool_exp
  ): [todo!]!

  # fetch aggregated fields from the table: "todo"
  todo_aggregate(
    # distinct select on columns
    distinct_on: [todo_select_column!]

    # limit the nuber of rows returned
    limit: Int

    # skip the first n rows. Use only with order_by
    offset: Int

    # sort the rows by one or more columns
    order_by: [todo_order_by!]

    # filter the rows returned
    where: todo_bool_exp
  ): todo_aggregate!

  # fetch data from the table: "todo" using primary key columns
  todo_by_pk(id: bigint!): todo
}

# expression to compare columns of type text. All fields are combined with logical 'AND'.
input text_comparison_exp {
  _eq: String
  _gt: String
  _gte: String
  _ilike: String
  _in: [String]
  _is_null: Boolean
  _like: String
  _lt: String
  _lte: String
  _neq: String
  _nilike: String
  _nin: [String]
  _nlike: String
  _nsimilar: String
  _similar: String
}

# columns and relationships of "todo"
type todo {
  complete: Boolean!
  id: bigint!
  text: String!
}

# aggregated selection of "todo"
type todo_aggregate {
  aggregate: todo_aggregate_fields
  nodes: [todo!]!
}

# aggregate fields of "todo"
type todo_aggregate_fields {
  avg: todo_avg_fields
  count(columns: [todo_select_column!], distinct: Boolean): Int
  max: todo_max_fields
  min: todo_min_fields
  stddev: todo_stddev_fields
  stddev_pop: todo_stddev_pop_fields
  stddev_samp: todo_stddev_samp_fields
  sum: todo_sum_fields
  var_pop: todo_var_pop_fields
  var_samp: todo_var_samp_fields
  variance: todo_variance_fields
}

# order by aggregate values of table "todo"
input todo_aggregate_order_by {
  avg: todo_avg_order_by
  count: order_by
  max: todo_max_order_by
  min: todo_min_order_by
  stddev: todo_stddev_order_by
  stddev_pop: todo_stddev_pop_order_by
  stddev_samp: todo_stddev_samp_order_by
  sum: todo_sum_order_by
  var_pop: todo_var_pop_order_by
  var_samp: todo_var_samp_order_by
  variance: todo_variance_order_by
}

# input type for inserting array relation for remote table "todo"
input todo_arr_rel_insert_input {
  data: [todo_insert_input!]!
  on_conflict: todo_on_conflict
}

# aggregate avg on columns
type todo_avg_fields {
  id: Float
}

# order by avg() on columns of table "todo"
input todo_avg_order_by {
  id: order_by
}

# Boolean expression to filter rows from the table "todo". All fields are combined with a logical 'AND'.
input todo_bool_exp {
  _and: [todo_bool_exp]
  _not: todo_bool_exp
  _or: [todo_bool_exp]
  complete: boolean_comparison_exp
  id: bigint_comparison_exp
  text: text_comparison_exp
}

# unique or primary key constraints on table "todo"
enum todo_constraint {
  # unique or primary key constraint
  todo_pkey
}

# input type for incrementing integer columne in table "todo"
input todo_inc_input {
  id: bigint
}

# input type for inserting data into table "todo"
input todo_insert_input {
  complete: Boolean
  id: bigint
  text: String
}

# aggregate max on columns
type todo_max_fields {
  id: bigint
  text: String
}

# order by max() on columns of table "todo"
input todo_max_order_by {
  id: order_by
  text: order_by
}

# aggregate min on columns
type todo_min_fields {
  id: bigint
  text: String
}

# order by min() on columns of table "todo"
input todo_min_order_by {
  id: order_by
  text: order_by
}

# response of any mutation on the table "todo"
type todo_mutation_response {
  # number of affected rows by the mutation
  affected_rows: Int!

  # data of the affected rows by the mutation
  returning: [todo!]!
}

# input type for inserting object relation for remote table "todo"
input todo_obj_rel_insert_input {
  data: todo_insert_input!
  on_conflict: todo_on_conflict
}

# on conflict condition type for table "todo"
input todo_on_conflict {
  constraint: todo_constraint!
  update_columns: [todo_update_column!]!
}

# ordering options when selecting data from "todo"
input todo_order_by {
  complete: order_by
  id: order_by
  text: order_by
}

# select columns of table "todo"
enum todo_select_column {
  # column name
  complete

  # column name
  id

  # column name
  text
}

# input type for updating data in table "todo"
input todo_set_input {
  complete: Boolean
  id: bigint
  text: String
}

# aggregate stddev on columns
type todo_stddev_fields {
  id: Float
}

# order by stddev() on columns of table "todo"
input todo_stddev_order_by {
  id: order_by
}

# aggregate stddev_pop on columns
type todo_stddev_pop_fields {
  id: Float
}

# order by stddev_pop() on columns of table "todo"
input todo_stddev_pop_order_by {
  id: order_by
}

# aggregate stddev_samp on columns
type todo_stddev_samp_fields {
  id: Float
}

# order by stddev_samp() on columns of table "todo"
input todo_stddev_samp_order_by {
  id: order_by
}

# aggregate sum on columns
type todo_sum_fields {
  id: bigint
}

# order by sum() on columns of table "todo"
input todo_sum_order_by {
  id: order_by
}

# update columns of table "todo"
enum todo_update_column {
  # column name
  complete

  # column name
  id

  # column name
  text
}

# aggregate var_pop on columns
type todo_var_pop_fields {
  id: Float
}

# order by var_pop() on columns of table "todo"
input todo_var_pop_order_by {
  id: order_by
}

# aggregate var_samp on columns
type todo_var_samp_fields {
  id: Float
}

# order by var_samp() on columns of table "todo"
input todo_var_samp_order_by {
  id: order_by
}

# aggregate variance on columns
type todo_variance_fields {
  id: Float
}

# order by variance() on columns of table "todo"
input todo_variance_order_by {
  id: order_by
}

When I run mst-gql --format ts todo-schema.graphql and try to use the generated models, useQuery, etc. I get no code completion because the type of RootStoreBase, RootStore etc. is always 'any'. For these types I get an TS error like:

Error:(51, 14) TS7022: 'RootStoreBase' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer.

Unfortunately I don't know the internals of mst and also not an expert of Typescript, so I could not figure out what causes this problem.

Do you have any Idea?

Note: I started off from a more complex data schema and thought that caused the problems. Then I tried this single table Todo use case and it produced the same errors.

[question] How to refetch data after delete object from model

Ok, i have this mutation on my backend:

mutation: new GraphQLObjectType({
    name: 'Mutation',
    fields: {
      createUser: {
        type: UserType,
        args: {
          name: { type: GraphQLNonNull(GraphQLString) },
          email: { type: GraphQLNonNull(GraphQLString) },
          password: { type: GraphQLNonNull(GraphQLString) },
        },
        resolve: (root, args) => {
          const user = userModel.create(args);
          return user;
        },
      },
      deleteUser: {
        type: UserType,
        args: {
          id: { type: GraphQLNonNull(GraphQLString) },
        },
        resolve: (root, args) => {
          userDbService.deleteUserById(args.id).exec();
          return null;
        },
      },
    },
  }),

after createUser :

// Model actions:
createUser: flow(function* createUser(user) {
    yield self.mutateCreateUser(user);
  }),

new record in RootSore.users appears.

How can i delete record from RootSore.users?

i tried this:

deleteUser: flow(function* deleteUser(id) {
    yield self.mutateDeleteUser({ id });
    yield self.queryUsers();
  }),

-> nothing

this:

yield self.mutateDeleteUser({ id }, undefined, () => {
      self.store.users.delete(id);
    });

-> record drops, but then immediately applied new patch with old data;

  1. Map.length // 2
  2. deleteUser() -> Map.length // 1
  3. Map.length // 2

Automatically updating store upon query/mutation result

In the Twitter example I see this code:

    loadMessages(offset, count, replyTo = undefined) {
      const query = self.queryMessages(
        { offset, count, replyTo },
        MESSAGE_FRAGMENT
      )
      query.then(data => {
        self.sortedMessages.push(...data)
      })
      return query
    }

I assumed that the auto-generated self.queryMessages would update the MST store, but here I see it being done manually, so I take it this is not the case?

If not, why not? Seems like the codebase will be full of these manual updates to the store, whereas a library such as Apollo Client will update its store itself upon each query.

UPDATE: In the README I see the store does auto update after each query. So why does loadMessages in the example above manually add messages to the store?

Circular dependencies between models

I'm running into the following error on all my models that are related to each other:

'TagTypeModelBase' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer.

Here is the file in question:

UserModel.base.ts

...
export const UserModelBase = MSTGQLObject
  .named('User')
  .props({
    __typename: types.optional(types.literal("User"), "User"),
    createdAt: types.maybe(types.frozen()),
    email: types.maybe(types.string),
    id: types.identifier,
    name: types.maybe(types.string),
    projects: types.optional(types.array(MSTGQLRef(types.late((): IAnyModelType => ProjectModel))), []),
    updatedAt: types.maybe(types.frozen()),
  })
...

Project has a link back to the User model. When I first ran into this I found:

https://github.com/mobxjs/mobx-state-tree#handle-circular-dependencies-between-files-and-types-using-late

and thought I could get around this by editing UserModel.ts like the following:

export const UserModel = UserModelBase.props({
  projects: types.optional(
    types.array(MSTGQLRef(types.late((): IAnyModelType => ProjectModel))),
    [],
  ),
})

But I still get the error in typescript. It seems like I need to edit UserModel.base.ts directly to fix this issue, but that is a generated file.

updating the store outside of graphql

I'm looking into using mst-gql on a project I'm working on. I'm really happy so far with the models, queries and mutations!

That being said I'm currently stuck around have live data updates. Our server is currently serverless so we don't currently use GraphQL subscriptions. What we do currently is use pusher to send data from the server to the client and then the client updates the Apollo cache.

What I'm curious about is if something similar is possible in this library. i.e. can I update the local store based on an event from pusher or similar?

relations with arguments not showing up on model

I have a schema that looks like the following:

type User {
  createdAt: DateTime!
  email: String!
  followers(after: String, before: String, first: Int, last: Int, orderBy: UserOrderByInput, skip: Int, where: UserWhereInput): [User!]
  following(after: String, before: String, first: Int, last: Int, orderBy: UserOrderByInput, skip: Int, where: UserWhereInput): [User!]
  id: ID!
  items(after: String, before: String, first: Int, last: Int, orderBy: ItemOrderByInput, skip: Int, where: ItemWhereInput): [Item!]
  itemsCreated(after: String, before: String, first: Int, last: Int, orderBy: ItemOrderByInput, skip: Int, where: ItemWhereInput): [Item!]
  name: String!
  projects(after: String, before: String, first: Int, last: Int, orderBy: ProjectOrderByInput, skip: Int, where: ProjectWhereInput): [Project!]
  updatedAt: DateTime!
}

but after running mst-gql I get the following:

/**
 * UserBase
 * auto generated base class for the model UserModel.
 */
export const UserModelBase = MSTGQLObject
  .named('User')
  .props({
    __typename: types.optional(types.literal("User"), "User"),
    createdAt: types.maybe(types.frozen()),
    email: types.maybe(types.string),
    id: types.identifier,
    name: types.maybe(types.string),
    updatedAt: types.maybe(types.frozen()),
  })
  .views(self => ({
    get store() {
      return self.__getStore<typeof RootStore.Type>()
    }
  }))

export class UserModelSelector extends QueryBuilder {
  get createdAt() { return this.__attr(`createdAt`) }
  get email() { return this.__attr(`email`) }
  get id() { return this.__attr(`id`) }
  get name() { return this.__attr(`name`) }
  get updatedAt() { return this.__attr(`updatedAt`) }
}
export function selectFromUser() {
  return new UserModelSelector()
}

export const userModelPrimitives = selectFromUser().createdAt.email.name.updatedAt

I found that if I remove the args MST picks things up correctly. For example if I change followers to be followers: [User!] I then see it correctly in my store:

followers: types.optional(types.array(MSTGQLRef(types.late((): any => UserModel))), []),

I don't actually use these args, so I can remove them from the generated schema.graphql but I would need to setup a script or similar to do this every time the file is generated.

Bug: Can't read kind of undefined.

Hi. Not sure if this is the right place to put this. I kept trying to run this and kept getting an error can't read .Kind of undefined at interfaceOrUnionType.kind. I ended up cloning this and trying to get it to work locally.

I got it to work by adding a conditional check in the handleInterfaceOrUnionType func in generate.js

const isUnion = interfaceOrUnionType && interfaceOrUnionType.kind === "UNION"

And

interfaceOrUnionType && interfaceOrUnionType.ofTypes.forEach(t => {...

I think it chokes where my Schema has

 {
        "kind": "OBJECT",
        "name": "__Schema",

Adding the check above got it to run and generate properly.

But perhaps that additional check could be added in the repo, so I don't have to use a fork. Thank you.

Alos, there were some refences to './RootStore' in the generated files. But no Rootstore is created, only a small case 'rootStore'. So had to update the paths in 'generate.js' in 3 places that incorrectly referenced uppercase Rootstore file.

I can put in a PR for these changes if you want.

using multi-model fragments within a model

So I have started to define various fragments in my RootStore that work fine there. Recently I wanted to use one of these fragments on a mutation in another store:

ItemModel

export const ItemModel = ItemModelBase.actions((self) => ({
  markComplete() {
    return self.store.mutateUpdateItem(
      {
        ...
      },
      ITEM_FRAGMENT,
    )
  },
}))

The problem is that ITEM_FRAGMENT includes a selectFromOtherModel function from a model that are not available at this point.

I see here: https://github.com/mobxjs/mst-gql/blob/master/examples/3-twitter-clone/src/app/models/MessageModel.ts#L26 that the fragment is just written by hand, I'm curious if there is a way we can support both raw and using selectFromModel functions.

Use a decent code generator or templates

The current generator generate.js, is a pretty straight forward string concatenation tool. it's fast and straight forward, but probably not very maintainable as it grows more complex in the future. Something more solid could be a good idea, I think @birkir had some thoughts about that in the past

Better TypeScript support in query builders

I think it would be useful to infer the types of return values from the query builder. Here's an example from the README:

msg => msg
  .timestamp
  .text
  .user(user => user.name.avatar)
  .likes()

After a quick glance at the source code, I think the builder is not currently used in any way to infer the types of its results. However, I think it would be possible in TS to do that.

I hacked around a little proof of concept that shows how a builder could be written to support type inference:

Playground link

What do you think? Let me know please if it's something you might be interested in in the future.

INPUT_OBJECT in typescript

I tried to use mst-gql with a Prisma generated schema today and ran into the following error:

Error: Not implemented printTsType yet, PR welcome for {
  "kind": "INPUT_OBJECT",
  "name": "ItemWhereUniqueInput",
  "ofType": null
}

when looking at my schema I see:

input ItemWhereUniqueInput {
  id: ID
}

It should be possible to generate non-nullable fields

Depends on #8

Currently, all fields (except id and __typename) are generated to be a types.maybe, as they might never be instantiated.

However, in strict typescript, this will require a lot of non-null assertions.
Also, it can be very good to make it mandatory that queries for a certain type also fetch a certain field.

So it would be could to have an option, for example, that specifies mandatory fields, for example: --mandatoryFields 'User.name,Todo.title'.

Examples with classes vs hooks

Are there any examples that use classes? (and decorators?) vs hooks? Is that possible? I'm looking to take baby steps...

Batching

Is query batching currently supported?

Seems like you could often end up making multiple requests. I believe Apollo Client has this functionality built-in.

If not supported, is this a good feature to be added to the library, or you'd recommend users handle larger queries in a different way?

Generating React hooks specific to operations

Regarding the code generation, I actually do have a custom generator for graphql-code-generator which generates fully typed hooks for each graphql operation. I am not sure yet how it would look like here, but a similar alternative would be really nice.

export function useQResolveLocation(
  variables?: QResolveLocationVariables,
  baseOptions?: Hooks.QueryHookOptions<QResolveLocationVariables>,
) {
  return Hooks.useQuery<QResolveLocationQuery, QResolveLocationVariables>(
    QResolveLocationDocument,
    variables,
    baseOptions,
  )
}

I am using prefixes Q, M, S, F for my *.gql documents and based on that I can distinguish those easily.

Originally posted by @FredyC in #13 (comment)

Failed to convert type

Got following output:

Detected types: 
  - [OBJECT] RootQueryType
  - [SCALAR] UUID
  - [OBJECT] Bot
  - [SCALAR] String
  - [SCALAR] DateTime
  - [SCALAR] Int
  - [OBJECT] BotItemsConnection
  - [OBJECT] BotItemsEdge
  - [OBJECT] BotItem
  - [OBJECT] Media
  - [INTERFACE] User
  - [ENUM] UserBotRelationship
  - [OBJECT] BotsConnection
  - [OBJECT] BotsEdge
  - [OBJECT] PageInfo
  - [SCALAR] Boolean
  - [ENUM] UserContactRelationship
  - [OBJECT] ContactsConnection
  - [OBJECT] ContactsEdge
  - [OBJECT] Hidden
  - [OBJECT] Presence
  - [ENUM] PresenceStatus
  - [SCALAR] Float
  - [ENUM] SubscriptionType
  - [OBJECT] SubscribersConnection
  - [OBJECT] SubscribersEdge
  - [OBJECT] CurrentUser
  - [OBJECT] BlocksConnection
  - [OBJECT] BlocksEdge
  - [OBJECT] Block
  - [OBJECT] BlockedUser
  - [OBJECT] ConversationsConnection
  - [OBJECT] ConversationsEdge
  - [OBJECT] Message
  - [ENUM] MessageDirection
  - [OBJECT] FriendsConnection
  - [OBJECT] FriendsEdge
  - [OBJECT] Contact
  - [OBJECT] LocationEventsConnection
  - [OBJECT] LocationEventsEdge
  - [OBJECT] LocationEvent
  - [ENUM] LocationEventType
  - [OBJECT] Location
  - [OBJECT] UserLocationLiveSharesConnection
  - [OBJECT] UserLocationLiveSharesEdge
  - [OBJECT] UserLocationLiveShare
  - [OBJECT] OtherUser
  - [OBJECT] LocationsConnection
  - [OBJECT] LocationsEdge
  - [OBJECT] MessagesConnection
  - [OBJECT] MessagesEdge
  - [OBJECT] InvitationsConnection
  - [OBJECT] InvitationsEdge
  - [OBJECT] Invitation
  - [INPUT_OBJECT] Point
  - [OBJECT] LocalBots
  - [OBJECT] LocalBotsCluster
  - [OBJECT] BotCluster
  - [SCALAR] AInt
  - [ENUM] NotificationType
  - [OBJECT] NotificationsConnection
  - [OBJECT] NotificationsEdge
  - [OBJECT] Notification
  - [UNION] NotificationData
  - [OBJECT] BotInvitationNotification
  - [OBJECT] BotInvitation
  - [OBJECT] BotInvitationResponseNotification
  - [OBJECT] BotItemNotification
  - [OBJECT] GeofenceEventNotification
  - [ENUM] GeofenceEvent
  - [OBJECT] LocationShareEndNotification
  - [OBJECT] LocationShareNotification
  - [OBJECT] UserInvitationNotification
  - [OBJECT] UserBulkLookupResult
  - [OBJECT] RootMutationType
  - [INPUT_OBJECT] FriendNameInput
  - [OBJECT] FriendNamePayload
  - [OBJECT] ValidationMessage
  - [OBJECT] ValidationOption
  - [OBJECT] UserLocationCancelSharePayload
  - [INPUT_OBJECT] PresenceStatusInput
  - [OBJECT] PresenceStatusPayload
  - [INPUT_OBJECT] UserUnblockInput
  - [OBJECT] UserUnblockPayload
  - [INPUT_OBJECT] UserUpdateInput
  - [INPUT_OBJECT] UserParams
  - [OBJECT] UserUpdatePayload
  - [INPUT_OBJECT] BotDeleteInput
  - [OBJECT] BotDeletePayload
  - [INPUT_OBJECT] BotCreateInput
  - [INPUT_OBJECT] UserLocationUpdateInput
  - [INPUT_OBJECT] BotParams
  - [OBJECT] BotCreatePayload
  - [INPUT_OBJECT] SendMessageInput
  - [OBJECT] SendMessagePayload
  - [INPUT_OBJECT] UserLocationLiveShareInput
  - [OBJECT] UserLocationLiveSharePayload
  - [INPUT_OBJECT] UserBlockInput
  - [OBJECT] UserBlockPayload
  - [INPUT_OBJECT] UserInviteRedeemCodeInput
  - [OBJECT] UserInviteRedeemCodePayload
  - [INPUT_OBJECT] FactoryInsertInput
  - [INPUT_OBJECT] AtomParam
  - [INPUT_OBJECT] BoolParam
  - [INPUT_OBJECT] FloatParam
  - [INPUT_OBJECT] IntParam
  - [INPUT_OBJECT] StringParam
  - [OBJECT] FactoryInsertPayload
  - [INPUT_OBJECT] UserHideInput
  - [OBJECT] UserHidePayload
  - [INPUT_OBJECT] FriendBulkInviteInput
  - [OBJECT] FriendBulkInvitePayload
  - [OBJECT] FriendBulkInviteResult
  - [ENUM] BulkInviteResult
  - [INPUT_OBJECT] MediaUploadParams
  - [OBJECT] MediaUploadPayload
  - [OBJECT] MediaUploadResult
  - [OBJECT] RequestHeader
  - [INPUT_OBJECT] AuthenticateInput
  - [OBJECT] AuthenticatePayload
  - [INPUT_OBJECT] PushNotificationsDisableInput
  - [OBJECT] PushNotificationsDisablePayload
  - [INPUT_OBJECT] BotUpdateInput
  - [OBJECT] BotUpdatePayload
  - [INPUT_OBJECT] UserLocationCancelShareInput
  - [INPUT_OBJECT] MediaDeleteParams
  - [OBJECT] MediaDeletePayload
  - [OBJECT] UserDeletePayload
  - [INPUT_OBJECT] BotInvitationRespondInput
  - [OBJECT] BotInvitationRespondPayload
  - [INPUT_OBJECT] BotUnsubscribeInput
  - [OBJECT] BotUnsubscribePayload
  - [OBJECT] UserInviteMakeCodePayload
  - [INPUT_OBJECT] BotInviteInput
  - [OBJECT] BotInvitePayload
  - [OBJECT] UserLocationGetTokenPayload
  - [INPUT_OBJECT] NotificationDeleteInput
  - [OBJECT] NotificationDeletePayload
  - [INPUT_OBJECT] BotItemDeleteInput
  - [OBJECT] BotItemDeletePayload
  - [INPUT_OBJECT] FollowInput
  - [OBJECT] FollowPayload
  - [INPUT_OBJECT] BotItemPublishInput
  - [OBJECT] BotItemPublishPayload
  - [INPUT_OBJECT] FriendDeleteInput
  - [OBJECT] FriendDeletePayload
  - [INPUT_OBJECT] BotSubscribeInput
  - [OBJECT] BotSubscribePayload
  - [OBJECT] UserLocationUpdatePayload
  - [INPUT_OBJECT] PushNotificationsEnableInput
  - [ENUM] NotificationPlatform
  - [OBJECT] PushNotificationsEnablePayload
  - [INPUT_OBJECT] FriendInviteInput
  - [OBJECT] FriendInvitePayload
  - [OBJECT] RootSubscriptionType
  - [OBJECT] VisitorUpdate
  - [ENUM] VisitorAction
  - [UNION] NotificationUpdate
  - [OBJECT] NotificationDeleted
  - [OBJECT] UserLocationUpdate
  - [OBJECT] __Schema
  - [OBJECT] __Type
  - [ENUM] __TypeKind
  - [OBJECT] __Field
  - [OBJECT] __InputValue
  - [OBJECT] __EnumValue
  - [OBJECT] __Directive
  - [ENUM] __DirectiveLocation
  - [SCALAR] ID
Warning: no root types are configured. Probably --roots should be set. Auto-detected and using the following types as root types: 
/Users/aksonov/Documents/Test/node_modules/mst-gql/generator/generate.js:275
          throw new Error(
          ^

Error: Failed to convert type {"kind":"INTERFACE","name":"User","ofType":null}. PR Welcome!
    at handleFieldType (/Users/aksonov/Documents/Test/node_modules/mst-gql/generator/generate.js:275:17)
    at handleField (/Users/aksonov/Documents/Test/node_modules/mst-gql/generator/generate.js:239:34)
    at type.fields.filter.map.field (/Users/aksonov/Documents/Test/node_modules/mst-gql/generator/generate.js:184:21)
    at Array.map (<anonymous>)
    at handleObjectType (/Users/aksonov/Documents/Test/node_modules/mst-gql/generator/generate.js:184:8)
    at types.filter.forEach.type (/Users/aksonov/Documents/Test/node_modules/mst-gql/generator/generate.js:88:22)
    at Array.forEach (<anonymous>)
    at generateTypes (/Users/aksonov/Documents/Test/node_modules/mst-gql/generator/generate.js:81:8)
    at generate (/Users/aksonov/Documents/Test/node_modules/mst-gql/generator/generate.js:42:3)
    at main (/Users/aksonov/Documents/Test/node_modules/mst-gql/generator/mst-gql-scaffold.js:96:17)
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

issues with `finally` on react-native

I'm attempting to use mst-gql on react native and I've run into the following:

TypeError: new Promise(function (resolve, reject) {
      this$1.onResolve = resolve;
      this$1.onReject = reject;
    }).finally is not a function. (In 'new Promise(function (resolve, reject) {
      this$1.onResolve = resolve;
      this$1.onReject = reject;
    }).finally(function () {
      this$1.store.ssr && this$1.store.unpushPromise(this$1.promise);
    })', 'new Promise(function (resolve, reject) {
      this$1.onResolve = resolve;
      this$1.onReject = reject;
    }).finally' is undefined)

I think this might be related to: facebook/react-native#17972

I'm going to spend a little time on it today, but figured it was worth opening the issue

yarn test and yarn start fails

Hi,

I'm trying to play with the examples in the master branch. As a first step I just tried to run the top level tests for the project but it failed like this:

> yarn
...
> yarn build
...
> yarn test
yarn run v1.7.0
$ jest test && cd examples/2-scaffolding && yarn start
 PASS  tests/generator/generate.test.js
 PASS  tests/lib/todos/todostore.test.js
 PASS  tests/lib/abstractTypes/abstractTypes.test.js

Test Suites: 3 passed, 3 total
Tests:       8 passed, 8 total
Snapshots:   7 passed, 7 total
Time:        1.937s, estimated 4s
Ran all test suites matching /test/i.
warning package.json: No license field
$ yarn scaffold && yarn build
warning package.json: No license field
$ ../../generator/mst-gql-scaffold.js --force --roots 'Pokemon, Attack' --format ts --outDir src/models graphql-schema.json
mst-gql-scaffold.js --format=ts --outDir=<my_path>/mst-gql/examples/2-scaffolding/src/models graphql-schema.json
Detected types: 
  - [OBJECT] Query
  - [SCALAR] Int
  - [OBJECT] Pokemon
  - [SCALAR] ID
  - [SCALAR] String
  - [OBJECT] PokemonDimension
  - [OBJECT] PokemonAttack
  - [OBJECT] Attack
  - [SCALAR] Float
  - [OBJECT] PokemonEvolutionRequirement
  - [OBJECT] __Schema
  - [OBJECT] __Type
  - [ENUM] __TypeKind
  - [SCALAR] Boolean
  - [OBJECT] __Field
  - [OBJECT] __InputValue
  - [OBJECT] __EnumValue
  - [OBJECT] __Directive
  - [ENUM] __DirectiveLocation
Writing file <my_path>/mst-gql/examples/2-scaffolding/src/models/PokemonModel.base.ts
Writing file <my_path>/mst-gql/examples/2-scaffolding/src/models/PokemonModel.ts
Writing file <my_path>/mst-gql/examples/2-scaffolding/src/models/PokemonDimensionModel.base.ts
Writing file <my_path>/mst-gql/examples/2-scaffolding/src/models/PokemonDimensionModel.ts
Writing file <my_path>/mst-gql/examples/2-scaffolding/src/models/PokemonAttackModel.base.ts
Writing file <my_path>/mst-gql/examples/2-scaffolding/src/models/PokemonAttackModel.ts
Writing file <my_path>/mst-gql/examples/2-scaffolding/src/models/AttackModel.base.ts
Writing file <my_path>/mst-gql/examples/2-scaffolding/src/models/AttackModel.ts
Writing file <my_path>/mst-gql/examples/2-scaffolding/src/models/PokemonEvolutionRequirementModel.base.ts
Writing file <my_path>/mst-gql/examples/2-scaffolding/src/models/PokemonEvolutionRequirementModel.ts
Writing file <my_path>/mst-gql/examples/2-scaffolding/src/models/RootStore.ts
Writing file <my_path>/mst-gql/examples/2-scaffolding/src/models/RootStore.base.ts
Writing file <my_path>/mst-gql/examples/2-scaffolding/src/models/reactUtils.ts
Writing file <my_path>/mst-gql/examples/2-scaffolding/src/models/index.ts
warning package.json: No license field
$ tsc
src/models/AttackModel.base.ts:6:44 - error TS2307: Cannot find module 'mst-gql'.

6 import { MSTGQLObject, QueryBuilder } from "mst-gql"
                                             ~~~~~~~~~

src/models/AttackModel.base.ts:27:10 - error TS7006: Parameter 'self' implicitly has an 'any' type.

27   .views(self => ({
            ~~~~

src/models/AttackModel.base.ts:29:14 - error TS2347: Untyped function calls may not accept type arguments.

29       return self.__getStore<typeof RootStore.Type>()
                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

src/models/AttackModel.base.ts:34:28 - error TS2339: Property '__attr' does not exist on type 'AttackModelSelector'.

34   get name() { return this.__attr(`name`) }
                              ~~~~~~

src/models/AttackModel.base.ts:35:28 - error TS2339: Property '__attr' does not exist on type 'AttackModelSelector'.

35   get type() { return this.__attr(`type`) }
                              ~~~~~~

src/models/AttackModel.base.ts:36:30 - error TS2339: Property '__attr' does not exist on type 'AttackModelSelector'.

36   get damage() { return this.__attr(`damage`) }
                                ~~~~~~
...

I also tried to run the 1-getting-started example:

> yarn
...
> yarn start
yarn run v1.17.3
$ run-p start:server start:client
$ sleep 1 && yarn scaffold && webpack-dev-server --hot --inline
$ node ./src/server/index.js
$ ../../generator/mst-gql-scaffold.js --roots 'Todo' --excludes 'CacheControlScope,Query,Subscription' --outDir src/app/models/ --format ts http://localhost:3001/graphql
🚀 Server ready at http://localhost:3001/graphql
🚀 Subscriptions ready at ws://localhost:3001/graphql
mst-gql-scaffold.js --format=ts --outDir=<my_path>/mst-gql/examples/1-getting-started/src/app/models http://localhost:3001/graphql
 ›   Warning: apollo update available from 2.9.0 to 2.16.3.
Detected types: 
  - [OBJECT] Query
  - [OBJECT] Todo
  - [SCALAR] ID
  - [SCALAR] String
  - [SCALAR] Boolean
  - [OBJECT] Mutation
  - [INPUT_OBJECT] CreateTodoInput
  - [OBJECT] __Schema
  - [OBJECT] __Type
  - [ENUM] __TypeKind
  - [OBJECT] __Field
  - [OBJECT] __InputValue
  - [OBJECT] __EnumValue
  - [OBJECT] __Directive
  - [ENUM] __DirectiveLocation
  - [ENUM] CacheControlScope
  - [SCALAR] Upload
  - [SCALAR] Int
  - [SCALAR] Float
Writing file <my_path>/mst-gql/examples/1-getting-started/src/app/models/TodoModel.base.ts
Skipping file <my_path>/mst-gql/examples/1-getting-started/src/app/models/TodoModel.ts
Skipping file <my_path>/mst-gql/examples/1-getting-started/src/app/models/RootStore.ts
Writing file <my_path>/mst-gql/examples/1-getting-started/src/app/models/RootStore.base.ts
Writing file <my_path>/mst-gql/examples/1-getting-started/src/app/models/reactUtils.ts
Writing file <my_path>/mst-gql/examples/1-getting-started/src/app/models/index.ts
ℹ 「atl」: Using [email protected] from typescript
ℹ 「atl」: Using tsconfig.json from <my_path>/mst-gql/examples/1-getting-started/tsconfig.json
ℹ 「atl」: Checking started in a separate process...
✖ 「atl」: Checking finished with 175 errors
✖ 「wdm」: Hash: 3d707c0d5d53f24cee20
Version: webpack 4.29.3
Time: 49130ms
Built at: 08/01/2019 4:00:12 PM
    Asset      Size  Chunks             Chunk Names
bundle.js  6.52 MiB    main  [emitted]  main
Entrypoint main = bundle.js
[0] multi (webpack)-dev-server/client?http://0.0.0.0:3000 (webpack)/hot/dev-server.js ./examples/1-getting-started/src/app/index.tsx 52 bytes {main} [built]
[./examples/1-getting-started/node_modules/loglevel/lib/loglevel.js] 7.68 KiB {main} [built]
[./examples/1-getting-started/node_modules/mst-gql/dist/mst-gql.module.js] 19.9 KiB {main} [built]
[./examples/1-getting-started/node_modules/react-dom/index.js] 1.33 KiB {main} [built]
[./examples/1-getting-started/node_modules/react/index.js] 190 bytes {main} [built]
[./examples/1-getting-started/node_modules/url/url.js] 22.8 KiB {main} [built]
[./examples/1-getting-started/node_modules/webpack-dev-server/client/index.js?http://0.0.0.0:3000] (webpack)-dev-server/client?http://0.0.0.0:3000 7.78 KiB {main} [built]
[./examples/1-getting-started/node_modules/webpack-dev-server/client/overlay.js] (webpack)-dev-server/client/overlay.js 3.58 KiB {main} [built]
[./examples/1-getting-started/node_modules/webpack-dev-server/client/socket.js] (webpack)-dev-server/client/socket.js 1.05 KiB {main} [built]
[./examples/1-getting-started/node_modules/webpack-dev-server/node_modules/strip-ansi/index.js] (webpack)-dev-server/node_modules/strip-ansi/index.js 161 bytes {main} [built]
[./examples/1-getting-started/node_modules/webpack/hot sync ^\.\/log$] (webpack)/hot sync nonrecursive ^\.\/log$ 170 bytes {main} [built]
[./examples/1-getting-started/node_modules/webpack/hot/dev-server.js] (webpack)/hot/dev-server.js 1.61 KiB {main} [built]
[./examples/1-getting-started/node_modules/webpack/hot/emitter.js] (webpack)/hot/emitter.js 75 bytes {main} [built]
[./examples/1-getting-started/node_modules/webpack/hot/log-apply-result.js] (webpack)/hot/log-apply-result.js 1.27 KiB {main} [built]
[./examples/1-getting-started/src/app/index.tsx] 1.3 KiB {main} [built]
    + 168 hidden modules

WARNING in ./examples/1-getting-started/node_modules/graphql-request/dist/src/index.js
Module Warning (from ./examples/1-getting-started/node_modules/source-map-loader/index.js):
(Emitted value instead of an instance of Error) Cannot find source file '../../src/index.ts': Error: Can't resolve '../../src/index.ts' in '<my_path>/mst-gql/examples/1-getting-started/node_modules/graphql-request/dist/src'
 @ ./examples/1-getting-started/node_modules/mst-gql/dist/mst-gql.module.js 3:0-48 478:13-26
 @ ./examples/1-getting-started/src/app/index.tsx
 @ multi (webpack)-dev-server/client?http://0.0.0.0:3000 (webpack)/hot/dev-server.js ./examples/1-getting-started/src/app/index.tsx

WARNING in ./examples/1-getting-started/node_modules/graphql-request/dist/src/types.js
Module Warning (from ./examples/1-getting-started/node_modules/source-map-loader/index.js):
(Emitted value instead of an instance of Error) Cannot find source file '../../src/types.ts': Error: Can't resolve '../../src/types.ts' in '<my_path>/mst-gql/examples/1-getting-started/node_modules/graphql-request/dist/src'
 @ ./examples/1-getting-started/node_modules/graphql-request/dist/src/index.js 55:14-32 56:14-32
 @ ./examples/1-getting-started/node_modules/mst-gql/dist/mst-gql.module.js
 @ ./examples/1-getting-started/src/app/index.tsx
 @ multi (webpack)-dev-server/client?http://0.0.0.0:3000 (webpack)/hot/dev-server.js ./examples/1-getting-started/src/app/index.tsx

ERROR in [at-loader] ./node_modules/@types/react/index.d.ts:2816:14 
    TS2300: Duplicate identifier 'LibraryManagedAttributes'.

ERROR in [at-loader] ./src/app/models/RootStore.base.ts:31:41 
    TS2304: Cannot find name 'CreateTodoInput'.

ERROR in [at-loader] ../../../../../../../../node_modules/@types/react/index.d.ts:2683:14 
    TS2300: Duplicate identifier 'LibraryManagedAttributes'.

ERROR in [at-loader] ../../../../../../../../node_modules/@types/react/index.d.ts:2696:13 
    TS2717: Subsequent property declarations must have the same type.  Property 'a' must be of type 'DetailedHTMLProps<AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>', but here has type 'DetailedHTMLProps<AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>'.

ERROR in [at-loader] ../../../../../../../../node_modules/@types/react/index.d.ts:2697:13 
    TS2717: Subsequent property declarations must have the same type.  Property 'abbr' must be of type 'DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>', but here has type 'DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>'.

ERROR in [at-loader] ../../../../../../../../node_modules/@types/react/index.d.ts:2698:13 
    TS2717: Subsequent property declarations must have the same type.  Property 'address' must be of type 'DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>', but here has type 'DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>'.

ERROR in [at-loader] ../../../../../../../../node_modules/@types/react/index.d.ts:2699:13 
    TS2717: Subsequent property declarations must have the same type.  Property 'area' must be of type 'DetailedHTMLProps<AreaHTMLAttributes<HTMLAreaElement>, HTMLAreaElement>', but here has type 'DetailedHTMLProps<AreaHTMLAttributes<HTMLAreaElement>, HTMLAreaElement>'.

ERROR in [at-loader] ../../../../../../../../node_modules/@types/react/index.d.ts:2700:13 
    TS2717: Subsequent property declarations must have the same type.  Property 'article' must be of type 'DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>', but here has type 'DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>'.

...

I tried with node 10 and 12 as well.

Should these work or am I doing something wrong?

Also, I don't seem to get the nice code completions Michel demonstrated here:

https://youtu.be/Sq2M00vghqY?t=789

For me the store in the useQuery() in Home.tsx in the 1-getting-started example is always of type 'any'.

`realpath` missing when running examples

When running the examples I ran into this issue when running yarn install:

../../scripts/install-mst-gql.sh: line 6: realpath: command not found

Doing this fixed it for me:

npm install --global realpath

realpath-native does get installed in the root node_modules folder. Should the example bash script be updated to rely on the copy of realpath in node_modules?

Fix Example 4

Example 4 is currently not working 100%. I think this might have more to do with the setup instructions than the example itself. Ideally someone with knowledge of Apollo can get it up and running with good docs.

Should this project be used in production?

Hi! Thanks for this project! I know this is experimental software, but is that because there's no maintainers or because it's not ready to be used in production?

I haven't had any issues with it, so I feel comfortable integrating it into my app; let me know if this is a mistake.

[Bug] when generate with --format js

when I ran the command:
$ yarn mst-gql --format js --force http://localhost:4000/graphql

it still generates like this in RootStore.base.js file, seem to be still typescript code with js format

export type PasswordMetaWhereInput = {
  id: string | undefined
  id_not: string | undefined
  id_in: string[]
  id_not_in: string[]
  id_lt: string | undefined
  id_lte: string | undefined
  id_gt: string | undefined
  id_gte: string | undefined
  id_contains: string | undefined
  id_not_contains: string | undefined
  id_starts_with: string | undefined
  id_not_starts_with: string | undefined
  id_ends_with: string | undefined
  id_not_ends_with: string | undefined
  resetToken: string | undefined
  resetToken_not: string | undefined
  resetToken_in: string[]
  resetToken_not_in: string[]
  resetToken_lt: string | undefined
  resetToken_lte: string | undefined
  resetToken_gt: string | undefined
  resetToken_gte: string | undefined
  resetToken_contains: string | undefined
  resetToken_not_contains: string | undefined
  resetToken_starts_with: string | undefined
  resetToken_not_starts_with: string | undefined
  resetToken_ends_with: string | undefined
  resetToken_not_ends_with: string | undefined
  AND: PasswordMetaWhereInput[]
  OR: PasswordMetaWhereInput[]
  NOT: PasswordMetaWhereInput[]
}
export type AwardFamilyWhereUniqueInput = {
  id: string | undefined
  name: string | undefined
  alias: string | undefined
}

Use `maybeNull` instead of `maybe` for default fields

I read the docs on graphql and the default is every field is nullable by default:

https://graphql.org/learn/best-practices/#nullability

I initially ran into this when trying to query a relation type on an ItemList component. The query looks like this:

Screen Shot 2019-07-08 at 5 25 00 PM

The generated mobx model looks like this:

Screen Shot 2019-07-08 at 5 23 28 PM

The issue I ran into is for some items not having a project on them and the type not being accurate when I get project: null back on the query:

Screen Shot 2019-07-08 at 5 24 18 PM

I could be wrong but it feels like the generated models should use maybeNull to match with graphql and allow types to be null? When I manually edit the generated model file to use maybeNull mst-gql correctly loaded my data into the store.

Happy to do a pull request if this makes sense and wasn't intentional!

Alternative way to generate types.identifier/types.identifierNumber for non ID typed identifier fields

The problem

Currently - and very logically - the generator generates types.identifier MST type for graphql fields of type ID. For example:

type Todo {
  id: ID
  ...
}

becomes:

export const TodoModelBase = MSTGQLObject
  .named('Todo')
  .props({
    __typename: types.optional(types.literal("Todo"), "Todo"),
    id: types.identifier,
    ...
  })

Hasura (https://github.com/hasura/graphql-engine), however, generates schemas for PostgreSQL tables matching the type of the columns in the database. So, in case my id in the table is of type bigint, which is quite typical for a primary key, the generated schema will look like this:

scalar bigint

type Todo {
  id: bigint!
  ...
}

And for this mst-gql generates:

export const TodoModelBase = MSTGQLObject
  .named('Todo')
  .props({
    __typename: types.optional(types.literal("Todo"), "Todo"),
    id: types.maybeNull(types.frozen()),
    ...
  })

As you can see the type of id becomes types.maybeNull(types.frozen()). The problem with this is that resolving references in the state tree won't work for identifiers that are not defined as types.identifier (or types.identifierNumber).

What I would need is something like:

export const TodoModelBase = MSTGQLObject
  .named('todo')
  .props({
    __typename: types.optional(types.literal("Todo"), "Todo"),
    id: types.identifierNumber,
    ...
  })

Note that I need types.identifierNumber and not even types.identifier which is what mst-gql is able to generate currently.

Proposal

So, my proposal is to introduce a mechanism, which can recognize identifiers by name/pattern matching in the graphql schema beside those that have the graphql type ID.

Something like:

mst-gql --overrideFieldType "id:bigint:types.identifierNumber,*_id:bigint:types.identifierNumber"

The --overrideFieldType would get a specification of field names to match and the kind of mst type definition to generate for that type instead of the default type. In the above case fields that are called id or fields that end with _id and are of graphql type bigint would be defined as types.identifierNumber MST types.

The override specification may also have the field as Type.field, in which case only the field of that specific type would be overriden:

mst-gql --overrideFieldType "Todo.id:bigint:types.identifierNumber"

The graphqlql type may be omitted or also pattern matched as well:

mst-gql --overrideFieldType "Todo.id:*:types.identifierNumber"

This could work for non id fields as well, of course, although I don't know if such overrides would be necessary beside identifiers.

What do you think?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.