Skip to content

Commit

Permalink
Merge 3994f33 into 75c2202
Browse files Browse the repository at this point in the history
  • Loading branch information
mattkrick committed Jun 24, 2016
2 parents 75c2202 + 3994f33 commit e5122a4
Show file tree
Hide file tree
Showing 10 changed files with 190 additions and 31 deletions.
28 changes: 24 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,29 @@ const transport = new HTTPTransport('/myEndpoint', {headers: {Authorization}});
If you'd like to replace the global `cashay.transport`, you can call just call `cashay.create({transport: newTransport})`.
This is useful if you use custom `fetchOptions` that include an authorization token and you need to change it.

### ServerSideTransport

```js
new ServerSideTransport(graphQLHandler, errorHandler)
```

- *`graphQLHandler({query, variables})`: a Promise that takes a `query` and `variables` prop, and returns the output from
a `graphql` call.
- `errorHandler`: A custom function that takes any GraphQL `errors` and converts them into a single `error` for your Redux state

Example:
```js
import {graphql} from 'graphql';
import Schema from './rootSchema';
import {ServerSideTransport} from 'cashay';
const graphQLHandler = function({query, variables}) => {
return graphql(Schema, query, null, null, variables);
}
const transport = new ServerSideTransport(graphQLHandler);
```
This is useful if you use your server for both graphql and server-side rendering
and don't want cashay to make an HTTP roundtrip to itself.

### Queries

```js
Expand Down Expand Up @@ -227,9 +250,7 @@ cashay.mutate('deleteComment', {variables: {commentId: postId}, components})

## Recipes

- [Pagination](./recipes/pagination.md)
- [Multi-part queries](./recipes/multi-part.queries.md)
- [Schema (without webpack)](./recipes/cashay-schema.md)
[See recipes](./recipes/index.md)

## Examples (PR to list yours)

Expand All @@ -249,7 +270,6 @@ Bugs will be fixed with the following priority:
- Subscriptions
- Fixing `getEntites` in the `mutationHandler`
- Test coverage at 95%
- Recipe for server-side rendering
- Persisted data and TTL on documents
- Support directives

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "cashay",
"version": "0.10.2",
"version": "0.11.0",
"description": "relay for the rest of us",
"main": "lib/index.js",
"bin": {
Expand Down
44 changes: 44 additions & 0 deletions recipes/SSR.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Server-side rending

Cashay supports server-side rending out of the box.

First, you'll need to create a redux store:
```
const store = createStore(reducer, {});
```

Second, you need to create the Cashay singleton:
```
import {cashay, ServerSideTransport} from 'cashay';
const cashaySchema = require('cashay!./utils/getCashaySchema.js');
cashay.create({
store,
schema: cashaySchema,
transport: new ServerSideTransport(...)
});
```
_Note: if you use a bundler like webpack, make sure that this file is included in the bundle.
You'll want the `cashay` that you `import` here
to be the same `cashay` that you use in your components. (singletons are no fun like that)_

Third, you'll want to stringify your state to send it down the wire:
```
const initialState = `window.__INITIAL_STATE__ = ${JSON.stringify(store.getState())}`;
// assume you use a react jsx template for SSR
<html>
<body>
...
<script dangerouslySetInnerHTML={{__html: initialState}} />
</body>
</html>
```

Finally, when it arrives on the client, you'll want to rehydrate the state:

```
const initialState = window.__INITIAL_STATE__;
store = createStore(reducer, initialState);
```

And there you have it!
7 changes: 7 additions & 0 deletions recipes/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Recipes

- [Multi-part queries](./multi-part.queries.md)
- [Pagination](./pagination.md)
- [Persisted state](./persisted-state.md)
- [Schema (without webpack)](./cashay-schema.md)
- [Server-side rendering](./SSR.md)
67 changes: 67 additions & 0 deletions recipes/persisted-state.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Persisted state

Cashay is just a reducer, so it doesn't need any extra logic to persist data locally.
Any tool you use to persist your redux store will work for persisting your domain state, too!

Personally, I like using [redux-persist](https://github.com/rt2zz/redux-persist).
Redux-persist works by storing each reducer in in a key-value store like
`localStorage`, `localForage`, or React-native storage.
After each action is dispatched, it will update your locally-stored copy, too.
When your app starts up, you can rehydrate the persisted data back into your redux state.

A production setup might look like this:

```
persistStore(store, {transforms: [cashayPersistTransform]}, () => {
cashay.create({
store,
schema: cashaySchema,
transport: new HTTPTransport(...);
});
render(
<AppContainer>
<Root store={store}/>
</AppContainer>,
document.getElementById('root')
);
});
```

A more advanced feature is removing stale data during rehydration, such as old query data.
That's what the `cashayPersistTransform` does above.
For example, if you use Cashay for authorization,
you might want to remove an expired JWT from a `getUserWithAuthToken` query:

```
import {createTransform} from 'redux-persist';
import jwtDecode from 'jwt-decode';
const cashayDeserializer = outboundState => {
const auth = outboundState.data.result.getUserWithAuthToken;
if (auth) {
const authObj = auth[''];
const {authToken} = authObj;
if (authToken) {
const authTokenObj = jwtDecode(authToken);
if (authTokenObj.exp < Date.now() / 1000) {
authObj.authToken = null;
}
}
}
return outboundState;
};
export default createTransform(
state => state,
cashayDeserializer,
{whitelist: ['cashay']}
);
```
Note: Cashay stores data using the following pattern:
```
cashay.data.result[queryName][?arguments][?pagination]
```
You can verify this path by cracking open your friendly `redux-devtools`.

In the future, when time-to-live (TTL) metadata is supported,
Cashay will include a transfrom that will automatically delete all expired documents.
12 changes: 9 additions & 3 deletions src/Cashay.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {printMinimalQuery} from './query/printMinimalQuery';
import {shortenNormalizedResponse, invalidateMutationsOnNewQuery, equalPendingQueries} from './query/queryHelpers';
import {checkMutationInSchema} from './utils';
import mergeStores from './normalize/mergeStores';
import {CachedMutation, CachedQuery} from './helperClasses';
import {CachedMutation, CachedQuery, CachedSubscription} from './helperClasses';
import flushDependencies from './query/flushDependencies';
import {parse, buildExecutionContext, getVariables, clone} from './utils';
import namespaceMutation from './mutate/namespaceMutation';
Expand Down Expand Up @@ -99,6 +99,7 @@ class Cashay {
// }
this.normalizedDeps = {};

// a Set of minimized query strings. Identical strings are ignored
this.pendingQueries = new Set();
}

Expand Down Expand Up @@ -580,17 +581,22 @@ class Cashay {
*/
subscribe(subscriptionString, subscriber, options) {
const component = options.component || subscriptionString;
if (!this.cachedSubscriptions[component]) {
const fastCachedSub = this.cachedSubscriptions[component];
// TODO add support for keys
if (fastCachedSub) {
return fastCachedSub.response;
} else {
this.cachedSubscriptions[component] = new CachedSubscription(subscriptionString);
}
const cachedSubscription = this.cachedSubscriptions[component];
const handlers = {
add: this.subscriptionAdd,
update: this.subscriptionUpdate,
remove: this.subscriptionRemove,
error: this.subscriptionError
};
const cashayDataState = this.getState().data;
// const variables = getVariables(options.variables, cashayDataState.variables[component]);
const variables = getVariables(options.variables, cashayDataState, component, key, cachedSubscription.response);
return subscriber(subscriptionString, handlers, variables);
}

Expand Down
3 changes: 2 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export cashayReducer from './normalize/duck';
export cashay from './Cashay';
export HTTPTransport from './HTTPTransport';
export HTTPTransport from './transports/HTTPTransport';
export ServerSideTransport from './transports/ServerSideTransport';
export transformSchema from './schema/transformSchema';
23 changes: 1 addition & 22 deletions src/HTTPTransport.js → src/transports/HTTPTransport.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import fetch from 'isomorphic-fetch'
import defaultHandleErrors from './defaultHandleErrors';

export default class HTTPTransport {
constructor(uri = '/graphql', init = {}, handleErrors = defaultHandleErrors) {
Expand Down Expand Up @@ -35,25 +36,3 @@ export default class HTTPTransport {

}
}

const tryParse = str => {
let obj;
try {
obj = JSON.parse(str);
} catch (e) {
return false;
}
return obj;
};

const defaultHandleErrors = (request, errors, duckField = '_error') => {
if (!errors) return;
// expect a human to put an end-user-safe message in the message field
const firstErrorMessage = errors[0].message;
if (!firstErrorMessage) return {errors};
const parsedError = tryParse(firstErrorMessage);
if (parsedError && parsedError.hasOwnProperty(duckField)) {
return parsedError;
}
return {errors};
};
14 changes: 14 additions & 0 deletions src/transports/ServerSideTransport.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import defaultHandleErrors from './defaultHandleErrors';

export default class ServerSideTransport {
constructor(graphQLHandler, handleErrors = defaultHandleErrors) {
this.graphQLHandler = graphQLHandler;
this.handleErrors = handleErrors;
}

async handleQuery(request) {
const {data, errors} = await this.graphQLHandler(request);
const error = this.handleErrors(request, errors);
return error ? {data, error} : {data};
}
}
21 changes: 21 additions & 0 deletions src/transports/defaultHandleErrors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
const tryParse = str => {
let obj;
try {
obj = JSON.parse(str);
} catch (e) {
return false;
}
return obj;
};

export default function defaultHandleErrors(request, errors, duckField = '_error') {
if (!errors) return;
// expect a human to put an end-user-safe message in the message field
const firstErrorMessage = errors[0].message;
if (!firstErrorMessage) return {errors};
const parsedError = tryParse(firstErrorMessage);
if (parsedError && parsedError.hasOwnProperty(duckField)) {
return parsedError;
}
return {errors};
};

0 comments on commit e5122a4

Please sign in to comment.