mattkrick / cashay

:moneybag: Relay for the rest of us :moneybag:
MIT License
453 stars 28 forks source link

npm version Build Status Coverage Status Gitter

Cashay

Relay for the rest of us

Installation

npm i -S cashay

How's it different?

This is my honest comparison. If I'm leaving out any features out, make a PR!

Cashay Apollo Relay
Uses Redux Yes Yes No
Local state & domain state in the same store Yes Yes No
Uses your GraphQL client schema Yes No No
Supports the full GraphQL spec No Yes No
No big changes to your GraphQL server Yes Yes No
Writes your mutations for you Yes No No
Allows for more than append/prepend mutations Yes Yes No
Works with all frontends Yes Yes No
Aggregates queries from child routes No No Yes
Supports Subscriptions Yes Yes No
Supports local transforms like sort, filter Yes No No
Caches denormalized result for fast renders Yes No ?
Supports Query Batching No Yes Yes
Built-in SSR No Yes No

Usage

Creating the client schema

Cashay uses a client-safe portion of your GraphQL schema, similar to GraphiQL, but way smaller.

Since schemas change rapidly during development, Cashay includes a Webpack loader to automatically refresh the schema on startup. You can include the schema on your client (likely near the instantiation of your Cashay singleton) by using a require() statement:

const cashaySchema = require('cashay!../server/utils/getCashaySchema.js');
///            cashay-loader ^^^     ^^^ returns function for promise for schema

Note: cashay-loader is automatically included with Cashay, just use it!

All the loader needs is a module which exports a function that will return a Promise for a schema that's been transformed by the Cashay's transformSchema() convenience function.

Ours looks like this:

// getCashaySchema.js

require('babel-register');
require('babel-polyfill');
const {transformSchema} = require('cashay');
const graphql = require('graphql').graphql;
const rootSchema = require('../graphql/rootSchema');
const r = require('../database/rethinkDriver');
module.exports = (params) => {
  if (params === '?exitRethink') {
    // optional pool draining if your schema starts a DB connection pool
    r.getPoolMaster().drain();
  }
  return transformSchema(rootSchema, graphql);
}

If you cannot use webpack, see the cashay-schema recipe.

Adding the reducer

Cashay is just like any other redux reducer:

import {createStore, compose, combineReducers} from 'redux'
import {cashayReducer} from 'cashay';
const rootReducer = combineReducers({cashay: cashayReducer});
const store = createStore(rootReducer, {});

Creating the singleton

Cashay is front-end agnostic, so instead of passing it through React context or making you replace react-redux with something non-vanilla, you can just import the singleton. This means it works well in SSR apps, too.

// in your client index.js
const clientSchema = require('cashay!../server/utils/getCashaySchema.js');
import {cashay} from 'cashay';
cashay.create(paramsObject);

// in a Component.js
import {cashay} from 'cashay';
cashay.query(...);

The params that you can pass into the create method are as follows (*required):

Now, whenever you need to query or mutate some data, just import your shiny new singleton!

API

Queries

const {data, setVariables, status} = cashay.query(queryString, options)

Options:

Each option below is an object full of field names for keys and functions for values.

Mutation Handlers

mutationHandler(optimisticVariables, queryResponse, currentResponse, getEntities, invalidate)

A mutation handler is called twice per mutation: once with optimisticVariables (for optimistic updates), and again with serverData when the mutation response comes back.

If a return value is provided, it will be normalized & merged with the state. If there is no return value, the state won't change.

For this example, we'll use React and react-redux:

const mapStateToProps = (state, props) => {
  return {
    response: cashay.query(queryString, options)
  }
};

Following the example above, this.props.response will be an object that has the following:

Setting variables

Cashay gives you a function to make setting variables dead simple. It gives you your op's variables that are currently in the store, and then it's up to you to give it back a new variables object:

const {setVariables} = this.props.cashay;
    setVariables(currentVariables => {
      return {
        count: currentVariables.count + 2
      }
    })

Mutations

Cashay mutations are pretty darn simple, too:

await cashay.mutate(mutationName, options)

Cashay is smart. By default, it will go through all the mutationHandlers that are currently active, looking for any handlers for mutationName. Then, it intersects your mutation payload schema with the corresponding queries to automatically fetch all the fields you need. No fat queries, no mutation fragments in your queries, no problems. If two different queries need the same field but with different arguments (eg. Query1 needs profilePic(size:SMALL) and Query2 needs profilePic(size:LARGE), it'll take care of that, too. This method conveniently returns a Promise so you can trigger side effects like redirects and localStorage caching, too. Note: if you return a scalar variable at the highest level of your mutation payload schema, make sure the name in the mutation payload schema matches the name in the query to give Cashay a hint to grab it.

The options are as follows:

In this example, we just want to call the mutationHandler for the comments op where key === postId. If you wanted to delete Comment #3 (where key = 3), you'd want to trigger the mutationHandler for {comments: 3} and not bother wasting CPU cycles checking {comments: 1} and {comments: 2}. Additionally, we call the mutationHandler for post if the value is true. This might be common if the post query includes a commentCount that should decrement when a comment is deleted. This logic makes Cashay super efficient by default, while still being flexible enough to write multiple mutations that have the same mutationName, but affect different queries. For example, you might have a mutation called deleteSomething that accepts a tableName and id variable. Then, a good practice to hardcode tableName to Posts that op. In doing so, you reduce the # of mutations in your schema (since deleteSomething can delete any doc in your db). Additionally, because you hardcoded in the tableName, you don't have to pass that variable down via this.props.

const {postId} = this.props;
const mutationAffectsPostOp = true;
const ops = {
  comments: postId,
  post: mutationAffectsPostOp
}
cashay.mutate('deleteComment', {variables: {commentId: postId}, ops})

Subscriptions

Subscriptions are hard, don't let anyone tell you different. Cashay makes them simpler by allowing you to inline them with the @live directive and manage them with your custom subscriber callback that you write yourself. Cashay doesn't dictate your socket package, your server, or your message protocol (DDP or otherwise) because doing so would tightly couple your front end to your server. That's not cool.

For examples, see the subscriber and @live recipes.

cashay.unsubscribe(channel, key = '')

Calls the result of your subscriber.

Cashay also provides a lower level subscribe API for advanced use cases. If you need to ensure that a component is subscribed, or need to subscribe without supplying data to the view layer, this is for you.

const {data, status, unsubscribe} = cashay.subscribe(channel, key, subscriber)

Recipes

See recipes

Examples (PR to list yours)

Contributing

Cashay is a young project, so there are sure to be plenty of bugs to squash and edge cases to capture. Bugs will be fixed with the following priority:

Roadmap to 1.0

Deviations from the GraphQL spec

The following edge cases are valid per the GraphQL spec, but are not supported in Cashay:

License

MIT