diff --git a/CHANGELOG.md b/CHANGELOG.md index f5f4df9..d735e98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)) diff --git a/docs/optimistic.md b/docs/optimistic.md index 362345d..d0ce65a 100644 --- a/docs/optimistic.md +++ b/docs/optimistic.md @@ -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. + }; }, }, }); diff --git a/docs/resolvers.md b/docs/resolvers.md index 42f308b..6a62700 100644 --- a/docs/resolvers.md +++ b/docs/resolvers.md @@ -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. @@ -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) diff --git a/docs/updates.md b/docs/updates.md index b787554..f5dd2bd 100644 --- a/docs/updates.md +++ b/docs/updates.md @@ -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. diff --git a/src/operations/query.ts b/src/operations/query.ts index ec2df34..6bc5b57 100644 --- a/src/operations/query.ts +++ b/src/operations/query.ts @@ -7,6 +7,7 @@ import { getName, getFieldArguments, getFieldAlias, + getFragmentTypeName, } from '../ast'; import { @@ -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; @@ -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, diff --git a/src/operations/write.ts b/src/operations/write.ts index d2a48ff..6589722 100644 --- a/src/operations/write.ts +++ b/src/operations/write.ts @@ -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, diff --git a/src/store.test.ts b/src/store.test.ts index a309550..69c9337 100644 --- a/src/store.test.ts +++ b/src/store.test.ts @@ -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 { @@ -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 => ({ @@ -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, diff --git a/src/store.ts b/src/store.ts index 3ec8ad2..70d84b8 100644 --- a/src/store.ts +++ b/src/store.ts @@ -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'; @@ -74,6 +74,11 @@ const mapRemove = (map: Pessimism.Map, key: string) => { type RootField = 'query' | 'mutation' | 'subscription'; +interface QueryInput { + query: string | DocumentNode; + variables?: Variables; +} + export class Store { records: Pessimism.Map; links: Pessimism.Map; @@ -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); }