Skip to content
This repository has been archived by the owner on Jul 6, 2020. It is now read-only.

Commit

Permalink
(chore) - improve api surface consistency (#73)
Browse files Browse the repository at this point in the history
* (docs) - update some details

* (chore) - improve store api surface consistency

* (feat) - read fragment in cache

* (tests) - add test cases for new functionality

* (docs) - update docs for our new functionality

* (refactor) - use the readQuery method in updateQuery

* (chore) - update changelog

* (chore) - update changelog entry with danger sign

* (chore) - guard warning when we have a keys config for the type

* (chore) - maintain the bailout when we have no entitykey but only warn when there's no keys entry for the typename

* (chore) - support fragments in updateQuery

* (chore) - use request

* Update readFragment for full keying and rename writeFragment
  • Loading branch information
JoviDeCroock authored and kitten committed Sep 12, 2019
1 parent d754529 commit 2bfbf2a
Show file tree
Hide file tree
Showing 8 changed files with 167 additions and 12 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ If a change is missing an attribution, it may have been made by a Core Contribut

_The format is based on [Keep a Changelog](http://keepachangelog.com/)._

## vNext

- ⚠ rename `writeFragment` to `updateFragment` and introduce `readQuery` and `readFragment` (see [#73](https://github.com/FormidableLabs/urql-exchange-graphcache/pull/73))

## [v1.0.0-rc.7](https://github.com/FormidableLabs/urql-exchange-graphcache/compare/v1.0.0-rc.6...v1.0.0-rc.7)

- ⚠ Fix reexecuted operations due to dependencies not using `cache-first` (see [0bd58f6](https://github.com/FormidableLabs/urql-exchange-graphcache/commit/0bd58f6))
Expand Down
7 changes: 5 additions & 2 deletions docs/optimistic.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,11 @@ Let's see an example.
const myGraphCache = cacheExchange({
optimistic: {
addTodo: (variables, cache, info) => {
console.log(variables); // { id: '1', text: 'optimistic', __typename: 'Todo' }
return variables;
console.log(variables); // { id: '1', text: 'optimistic' }
return {
...variables
__typename: 'Todo', // We still have to let the cache know what entity we are on.
};
},
},
});
Expand Down
30 changes: 29 additions & 1 deletion docs/resolvers.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ A `resolver` gets four arguments:
- `parent` – The original entity in the cache. In the example above, this
would be the full `Todo` object.
- `arguments` – The arguments used in this field.
- `cache` – This is the normalized cache. The cache provides us with a `resolve` method;
- `cache` – This is the normalized cache. The cache provides us with `resolve`, `readQuery` and `readFragment` methods;
see more about this [below](#cache.resolve).
- `info` – This contains the fragments used in the query and the field arguments in the query.

Expand Down Expand Up @@ -82,4 +82,32 @@ console.log(name); // 'Bar'
This can help solve practical use cases like date formatting,
where you would query the date and then convert it in your resolver.

## `cache.readQuery`

Another method the cache allows is to let you read a full query, this method
accepts an object of `query` and optionally `variables`.

```js
const data = cache.readQuery({ query: Todos, variables: { from: 0, limit: 10 } })`
```

This way we'll get the stored data for the `TodosQuery` with given variables.

## `cache.readFragment`

The store allows the user to also read a fragment for a certain entity, this function
accepts a `fragment` and an `id`. This looks like the following.

```js
const data = cache.readFragment(gql`
fragment _ on Todo {
id
text
}
`, '1'`;
```

This way we'll get the Todo with id 1 and the relevant data we are askng for in the
fragment.

[Back](../README.md)
4 changes: 4 additions & 0 deletions docs/updates.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ That's where updates come into play. Analogous to our `resolvers`,
`updates` get arguments, but instead of the `parent` argument we get the
result given from the server due to a subscription trigger or a mutation.

> Note that this result will look like result.data, this means that in
> the example of us adding a todo by means of `addTodo` it will look like
> `{ addTodo: addedTodo }`.
Let's look at three additional methods provided by the cache to enable
updates.

Expand Down
54 changes: 54 additions & 0 deletions src/operations/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
getName,
getFieldArguments,
getFieldAlias,
getFragmentTypeName,
} from '../ast';

import {
Expand All @@ -31,6 +32,7 @@ import {
import { SelectionIterator, isScalar } from './shared';
import { joinKeys, keyOfField } from '../helpers';
import { SchemaPredicates } from '../ast/schemaPredicates';
import { DocumentNode, FragmentDefinitionNode } from 'graphql';

export interface QueryResult {
dependencies: Set<string>;
Expand Down Expand Up @@ -149,6 +151,58 @@ const readRootField = (
}
};

export const readFragment = (
store: Store,
query: DocumentNode,
entity: Data | string
): Data | null => {
const fragments = getFragments(query);
const names = Object.keys(fragments);
const fragment = fragments[names[0]] as FragmentDefinitionNode;
if (fragment === undefined) {
warning(
false,
'readFragment(...) was called with an empty fragment.\n' +
'You have to call it with at least one fragment in your GraphQL document.'
);

return null;
}

const select = getSelectionSet(fragment);
const typename = getFragmentTypeName(fragment);
if (typeof entity !== 'string' && !entity.__typename) {
entity.__typename = typename;
}

const entityKey =
typeof entity !== 'string'
? store.keyOfEntity({ __typename: typename, ...entity } as Data)
: entity;

if (!entityKey) {
warning(
false,
"Can't generate a key for readFragment(...).\n" +
'You have to pass an `id` or `_id` field or create a custom `keys` config for `' +
typename +
'`.'
);

return null;
}

const ctx: Context = {
variables: {},
fragments,
partial: false,
store,
schemaPredicates: store.schemaPredicates,
};

return readSelection(ctx, entityKey, select, Object.create(null)) || null;
};

const readSelection = (
ctx: Context,
entityKey: string,
Expand Down
11 changes: 5 additions & 6 deletions src/operations/write.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,22 +157,21 @@ export const writeFragment = (
}

const select = getSelectionSet(fragment);
const typeName = getFragmentTypeName(fragment);
const writeData = { ...data, __typename: typeName } as Data;

const entityKey = store.keyOfEntity(writeData) as string;
const typename = getFragmentTypeName(fragment);
const writeData = { __typename: typename, ...data } as Data;
const entityKey = store.keyOfEntity(writeData);
if (!entityKey) {
return warning(
false,
"Can't generate a key for writeFragment(...) data.\n" +
'You have to pass an `id` or `_id` field or create a custom `keys` config for `' +
typeName +
typename +
'`.'
);
}

const ctx: Context = {
variables: {}, // TODO: Should we support variables?
variables: {},
fragments,
result: { dependencies: getCurrentDependencies() },
store,
Expand Down
52 changes: 51 additions & 1 deletion src/store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,8 +181,9 @@ describe('Store with OptimisticMutationConfig', () => {
expect(store.getRecord('Appointment:1.info')).toBe(undefined);
});

it('should be able to update a fragment', () => {
it('should be able to write a fragment', () => {
initStoreState(0);

store.writeFragment(
gql`
fragment _ on Todo {
Expand Down Expand Up @@ -218,6 +219,32 @@ describe('Store with OptimisticMutationConfig', () => {
clearStoreState();
});

it('should be able to read a fragment', () => {
initStoreState(0);
const result = store.readFragment(
gql`
fragment _ on Todo {
id
text
complete
}
`,
{ id: '0' }
);

const deps = getCurrentDependencies();
expect(deps).toEqual(new Set(['Todo:0']));

expect(result).toEqual({
id: '0',
text: 'Go to the shops',
complete: false,
__typename: 'Todo',
});

clearStoreState();
});

it('should be able to update a query', () => {
initStoreState(0);
store.updateQuery({ query: Todos }, data => ({
Expand Down Expand Up @@ -303,6 +330,29 @@ describe('Store with OptimisticMutationConfig', () => {
clearStoreState();
});

it('should be able to read a query', () => {
initStoreState(0);
const result = store.readQuery({ query: Todos });

const deps = getCurrentDependencies();
expect(deps).toEqual(
new Set([
'Query.todos',
'Todo:0',
'Todo:1',
'Todo:2',
'Author:0',
'Author:1',
])
);

expect(result).toEqual({
__typename: 'Query',
todos: todosData.todos,
});
clearStoreState();
});

it('should be able to optimistically mutate', () => {
const { dependencies } = writeOptimistic(
store,
Expand Down
17 changes: 15 additions & 2 deletions src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
} from './types';

import { joinKeys, keyOfField } from './helpers';
import { read } from './operations/query';
import { read, readFragment } from './operations/query';
import { writeFragment, startWrite } from './operations/write';
import { invalidate } from './operations/invalidate';
import { SchemaPredicates } from './ast/schemaPredicates';
Expand Down Expand Up @@ -74,6 +74,11 @@ const mapRemove = <T>(map: Pessimism.Map<T>, key: string) => {

type RootField = 'query' | 'mutation' | 'subscription';

interface QueryInput {
query: string | DocumentNode;
variables?: Variables;
}

export class Store {
records: Pessimism.Map<EntityField>;
links: Pessimism.Map<Link>;
Expand Down Expand Up @@ -257,12 +262,20 @@ export class Store {
updater: (data: Data | null) => null | Data
): void {
const request = createRequest(input.query, input.variables);
const output = updater(read(this, request).data);
const output = updater(this.readQuery(request as QueryInput));
if (output !== null) {
startWrite(this, request, output);
}
}

readQuery(input: QueryInput): Data | null {
return read(this, createRequest(input.query, input.variables)).data;
}

readFragment(dataFragment: DocumentNode, entity: string | Data): Data | null {
return readFragment(this, dataFragment, entity);
}

writeFragment(dataFragment: DocumentNode, data: Data): void {
writeFragment(this, dataFragment, data);
}
Expand Down

0 comments on commit 2bfbf2a

Please sign in to comment.