Skip to content

Commit

Permalink
Add cache.writeData to base cache type & DataProxy (#2818)
Browse files Browse the repository at this point in the history
* Add writeData to base cache type & DataProxy

* Add writeData to ApolloClient

* Added changelog, bumped up apollo-cache bundle size

* Add all the tests from link-state

* Fix snapshots
  • Loading branch information
peggyrayzis authored and James Baxley committed Jan 5, 2018
1 parent 54012c7 commit 3373141
Show file tree
Hide file tree
Showing 10 changed files with 505 additions and 4 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
{
"name": "apollo-cache",
"path": "./packages/apollo-cache/lib/bundle.min.js",
"maxSize": "1 kB"
"maxSize": "1.2 kB"
},
{
"name": "apollo-cache-inmemory",
Expand Down
63 changes: 63 additions & 0 deletions packages/apollo-cache/src/__tests__/__snapshots__/utils.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[
`writing data with no query converts a JavaScript object to a query correctly arrays 1`
] = `
"query GeneratedClientQuery {
number
bool
nested {
bool2
undef
nullField
str
}
}
"
`;

exports[
`writing data with no query converts a JavaScript object to a query correctly basic 1`
] = `
"query GeneratedClientQuery {
number
bool
bool2
undef
nullField
str
}
"
`;

exports[
`writing data with no query converts a JavaScript object to a query correctly fragments 1`
] = `
"fragment GeneratedClientQuery on __FakeType {
number
bool
nested {
bool2
undef
nullField
str
}
}
"
`;

exports[
`writing data with no query converts a JavaScript object to a query correctly nested 1`
] = `
"query GeneratedClientQuery {
number
bool
nested {
bool2
undef
nullField
str
}
}
"
`;
76 changes: 76 additions & 0 deletions packages/apollo-cache/src/__tests__/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { print } from 'graphql/language/printer';
import { queryFromPojo, fragmentFromPojo } from '../utils';

describe('writing data with no query', () => {
describe('converts a JavaScript object to a query correctly', () => {
it('basic', () => {
expect(
print(
queryFromPojo({
number: 5,
bool: true,
bool2: false,
undef: undefined,
nullField: null,
str: 'string',
}),
),
).toMatchSnapshot();
});

it('nested', () => {
expect(
print(
queryFromPojo({
number: 5,
bool: true,
nested: {
bool2: false,
undef: undefined,
nullField: null,
str: 'string',
},
}),
),
).toMatchSnapshot();
});

it('arrays', () => {
expect(
print(
queryFromPojo({
number: [5],
bool: [[true]],
nested: [
{
bool2: false,
undef: undefined,
nullField: null,
str: 'string',
},
],
}),
),
).toMatchSnapshot();
});

it('fragments', () => {
expect(
print(
fragmentFromPojo({
number: [5],
bool: [[true]],
nested: [
{
bool2: false,
undef: undefined,
nullField: null,
str: 'string',
},
],
}),
),
).toMatchSnapshot();
});
});
});
35 changes: 35 additions & 0 deletions packages/apollo-cache/src/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { DocumentNode } from 'graphql';
import { getFragmentQueryDocument } from 'apollo-utilities';

import { DataProxy, Cache } from './types';
import { justTypenameQuery, queryFromPojo, fragmentFromPojo } from './utils';

export type Transaction<T> = (c: ApolloCache<T>) => void;

Expand Down Expand Up @@ -99,4 +100,38 @@ export abstract class ApolloCache<TSerialized> implements DataProxy {
query: getFragmentQueryDocument(options.fragment, options.fragmentName),
});
}

public writeData({ id, data }: Cache.WriteDataOptions): void {
if (id) {
let typenameResult = null;
// Since we can't use fragments without having a typename in the store,
// we need to make sure we have one.
// To avoid overwriting an existing typename, we need to read it out first
// and generate a fake one if none exists.
try {
typenameResult = this.read({
rootId: id,
optimistic: false,
query: justTypenameQuery,
});
} catch (e) {
// Do nothing, since an error just means no typename exists
}

// tslint:disable-next-line
const __typename =
(typenameResult && typenameResult.__typename) || '__ClientData';

// Add a type here to satisfy the inmemory cache
const dataToWrite = { __typename, ...data };

this.writeFragment({
id,
fragment: fragmentFromPojo(dataToWrite, __typename),
data: dataToWrite,
});
} else {
this.writeQuery({ query: queryFromPojo(data), data });
}
}
}
1 change: 1 addition & 0 deletions packages/apollo-cache/src/types/Cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,6 @@ export namespace Cache {
export import DiffResult = DataProxy.DiffResult;
export import WriteQueryOptions = DataProxy.WriteQueryOptions;
export import WriteFragmentOptions = DataProxy.WriteFragmentOptions;
export import WriteDataOptions = DataProxy.WriteDataOptions;
export import Fragment = DataProxy.Fragment;
}
24 changes: 21 additions & 3 deletions packages/apollo-cache/src/types/DataProxy.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DocumentNode } from "graphql"; // eslint-disable-line import/no-extraneous-dependencies, import/no-unresolved
import { DocumentNode } from 'graphql'; // eslint-disable-line import/no-extraneous-dependencies, import/no-unresolved

export namespace DataProxy {
export interface Query {
Expand Down Expand Up @@ -58,6 +58,16 @@ export namespace DataProxy {
data: any;
}

export interface WriteDataOptions {
/**
* The data you will be writing to the store.
* It also takes an optional id property.
* The id is used to write a fragment to an existing object in the store.
*/
data: any;
id?: string;
}

export type DiffResult<T> = {
result?: T;
complete?: boolean;
Expand All @@ -76,7 +86,7 @@ export interface DataProxy {
*/
readQuery<QueryType>(
options: DataProxy.Query,
optimistic?: boolean
optimistic?: boolean,
): QueryType | null;

/**
Expand All @@ -86,7 +96,7 @@ export interface DataProxy {
*/
readFragment<FragmentType>(
options: DataProxy.Fragment,
optimistic?: boolean
optimistic?: boolean,
): FragmentType | null;

/**
Expand All @@ -100,4 +110,12 @@ export interface DataProxy {
* provided to select the correct fragment.
*/
writeFragment(options: DataProxy.WriteFragmentOptions): void;

/**
* Sugar for writeQuery & writeFragment.
* Writes data to the store without passing in a query.
* If you supply an id, the data will be written as a fragment to an existing object.
* Otherwise, the data is written to the root of the store.
*/
writeData(options: DataProxy.WriteDataOptions): void;
}
127 changes: 127 additions & 0 deletions packages/apollo-cache/src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import {
DocumentNode,
OperationDefinitionNode,
SelectionSetNode,
FieldNode,
FragmentDefinitionNode,
} from 'graphql';

export function queryFromPojo(obj: any): DocumentNode {
const op: OperationDefinitionNode = {
kind: 'OperationDefinition',
operation: 'query',
name: {
kind: 'Name',
value: 'GeneratedClientQuery',
},
selectionSet: selectionSetFromObj(obj),
};

const out: DocumentNode = {
kind: 'Document',
definitions: [op],
};

return out;
}

export function fragmentFromPojo(obj: any, typename?: string): DocumentNode {
const frag: FragmentDefinitionNode = {
kind: 'FragmentDefinition',
typeCondition: {
kind: 'NamedType',
name: {
kind: 'Name',
value: typename || '__FakeType',
},
},
name: {
kind: 'Name',
value: 'GeneratedClientQuery',
},
selectionSet: selectionSetFromObj(obj),
};

const out: DocumentNode = {
kind: 'Document',
definitions: [frag],
};

return out;
}

function selectionSetFromObj(obj: any): SelectionSetNode {
if (
typeof obj === 'number' ||
typeof obj === 'boolean' ||
typeof obj === 'string' ||
typeof obj === 'undefined' ||
obj === null
) {
// No selection set here
return null;
}

if (Array.isArray(obj)) {
// GraphQL queries don't include arrays
return selectionSetFromObj(obj[0]);
}

// Now we know it's an object
const selections: FieldNode[] = [];

Object.keys(obj).forEach(key => {
const field: FieldNode = {
kind: 'Field',
name: {
kind: 'Name',
value: key,
},
};

// Recurse
const nestedSelSet: SelectionSetNode = selectionSetFromObj(obj[key]);

if (nestedSelSet) {
field.selectionSet = nestedSelSet;
}

selections.push(field);
});

const selectionSet: SelectionSetNode = {
kind: 'SelectionSet',
selections,
};

return selectionSet;
}

export const justTypenameQuery: DocumentNode = {
kind: 'Document',
definitions: [
{
kind: 'OperationDefinition',
operation: 'query',
name: null,
variableDefinitions: null,
directives: [],
selectionSet: {
kind: 'SelectionSet',
selections: [
{
kind: 'Field',
alias: null,
name: {
kind: 'Name',
value: '__typename',
},
arguments: [],
directives: [],
selectionSet: null,
},
],
},
},
],
};
1 change: 1 addition & 0 deletions packages/apollo-client/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

### vNEXT
- include `optimisticResponse` in the context passed to apollo-link for mutations [PR#2704](https://github.com/apollographql/apollo-client/pull/2704)
- Add cache.writeData to base cache type & DataProxy [PR#2818](https://github.com/apollographql/apollo-client/pull/2818)
- Error when invalid `cache-and-network` is provided as `query.fetchPolicy` within `defaultOptions`
- add `onResetStore` method to the client to register callbacks after `client.resetStore` is called [PR#2812](https://github.com/apollographql/apollo-client/pull/2812)

Expand Down
Loading

0 comments on commit 3373141

Please sign in to comment.