Skip to content

Commit

Permalink
Merge pull request #6720 from dotansimha/feature/typed-document-node
Browse files Browse the repository at this point in the history
feature: added support for automatic type inference with `typed-document-node`
  • Loading branch information
benjamn committed Aug 4, 2020
2 parents dce4170 + 02528dd commit 081e604
Show file tree
Hide file tree
Showing 23 changed files with 625 additions and 260 deletions.
495 changes: 308 additions & 187 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
}
},
"dependencies": {
"@graphql-typed-document-node/core": "^2.1.0",
"@types/zen-observable": "^0.8.0",
"@wry/context": "^0.5.2",
"@wry/equality": "^0.2.0",
Expand Down
17 changes: 17 additions & 0 deletions src/__tests__/ApolloClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { ApolloLink } from '../link/core';
import { HttpLink } from '../link/http';
import { InMemoryCache } from '../cache';
import { stripSymbols } from '../testing';
import { TypedDocumentNode } from '@graphql-typed-document-node/core';

describe('ApolloClient', () => {
describe('constructor', () => {
Expand Down Expand Up @@ -1210,6 +1211,22 @@ describe('ApolloClient', () => {
}

describe('using writeQuery', () => {
it('with TypedDocumentNode', async () => {
const client = newClient();

// This is defined manually for the purpose of the test, but
// eventually this could be generated with graphql-code-generator
const typedQuery: TypedDocumentNode<Data, { testVar: string }> = query;

// The result and variables are being typed automatically, based on the query object we pass,
// and type inference is done based on the TypeDocumentNode object.
const result = await client.query({ query: typedQuery, variables: { testVar: 'foo' } });

// Just try to access it, if something will break, TS will throw an error
// during the test
result.data?.people.friends[0].id;
});

it('with a replacement of nested array (wq)', done => {
let count = 0;
const client = newClient();
Expand Down
6 changes: 3 additions & 3 deletions src/cache/core/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export abstract class ApolloCache<TSerialized> implements DataProxy {
// required to implement
// core API
public abstract read<T, TVariables = any>(
query: Cache.ReadOptions<TVariables>,
query: Cache.ReadOptions<TVariables, T>,
): T | null;
public abstract write<TResult = any, TVariables = any>(
write: Cache.WriteOptions<TResult, TVariables>,
Expand Down Expand Up @@ -104,7 +104,7 @@ export abstract class ApolloCache<TSerialized> implements DataProxy {
* @param optimistic
*/
public readQuery<QueryType, TVariables = any>(
options: DataProxy.Query<TVariables>,
options: DataProxy.Query<TVariables, QueryType>,
optimistic: boolean = false,
): QueryType | null {
return this.read({
Expand All @@ -120,7 +120,7 @@ export abstract class ApolloCache<TSerialized> implements DataProxy {
private getFragmentDoc = wrap(getFragmentQueryDocument);

public readFragment<FragmentType, TVariables = any>(
options: DataProxy.Fragment<TVariables>,
options: DataProxy.Fragment<TVariables, FragmentType>,
optimistic: boolean = false,
): FragmentType | null {
return this.read({
Expand Down
6 changes: 3 additions & 3 deletions src/cache/core/types/Cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ import { Modifier, Modifiers } from './common';
export namespace Cache {
export type WatchCallback = (diff: Cache.DiffResult<any>) => void;

export interface ReadOptions<TVariables = any>
extends DataProxy.Query<TVariables> {
export interface ReadOptions<TVariables = any, TData = any>
extends DataProxy.Query<TVariables, TData> {
rootId?: string;
previousResult?: any;
optimistic: boolean;
}

export interface WriteOptions<TResult = any, TVariables = any>
extends DataProxy.Query<TVariables> {
extends DataProxy.Query<TVariables, TResult> {
dataId?: string;
result: TResult;
broadcast?: boolean;
Expand Down
17 changes: 9 additions & 8 deletions src/cache/core/types/DataProxy.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { DocumentNode } from 'graphql'; // eslint-disable-line import/no-extraneous-dependencies, import/no-unresolved
import { TypedDocumentNode } from '@graphql-typed-document-node/core';

import { MissingFieldError } from './common';

export namespace DataProxy {
export interface Query<TVariables> {
export interface Query<TVariables, TData> {
/**
* The GraphQL query shape to be used constructed using the `gql` template
* string tag from `graphql-tag`. The query will be used to determine the
* shape of the data to be read.
*/
query: DocumentNode;
query: DocumentNode | TypedDocumentNode<TData, TVariables>;

/**
* Any variables that the GraphQL query may depend on.
Expand All @@ -24,7 +25,7 @@ export namespace DataProxy {
id?: string;
}

export interface Fragment<TVariables> {
export interface Fragment<TVariables, TData> {
/**
* The root id to be used. This id should take the same form as the
* value returned by your `dataIdFromObject` function. If a value with your
Expand All @@ -38,7 +39,7 @@ export namespace DataProxy {
* the shape of data to read. If you provide more than one fragment in this
* document then you must also specify `fragmentName` to select a single.
*/
fragment: DocumentNode;
fragment: DocumentNode | TypedDocumentNode<TData, TVariables>;

/**
* The name of the fragment in your GraphQL document to be used. If you do
Expand All @@ -54,7 +55,7 @@ export namespace DataProxy {
}

export interface WriteQueryOptions<TData, TVariables>
extends Query<TVariables> {
extends Query<TVariables, TData> {
/**
* The data you will be writing to the store.
*/
Expand All @@ -66,7 +67,7 @@ export namespace DataProxy {
}

export interface WriteFragmentOptions<TData, TVariables>
extends Fragment<TVariables> {
extends Fragment<TVariables, TData> {
/**
* The data you will be writing to the store.
*/
Expand Down Expand Up @@ -95,7 +96,7 @@ export interface DataProxy {
* Reads a GraphQL query from the root query id.
*/
readQuery<QueryType, TVariables = any>(
options: DataProxy.Query<TVariables>,
options: DataProxy.Query<TVariables, QueryType>,
optimistic?: boolean,
): QueryType | null;

Expand All @@ -105,7 +106,7 @@ export interface DataProxy {
* provided to select the correct fragment.
*/
readFragment<FragmentType, TVariables = any>(
options: DataProxy.Fragment<TVariables>,
options: DataProxy.Fragment<TVariables, FragmentType>,
optimistic?: boolean,
): FragmentType | null;

Expand Down
58 changes: 58 additions & 0 deletions src/cache/inmemory/__tests__/__snapshots__/cache.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -227,3 +227,61 @@ Object {
},
}
`;

exports[`TypedDocumentNode<Data, Variables> should determine Data and Variables types of {write,read}{Query,Fragment} 1`] = `
Object {
"Author:{\\"name\\":\\"John C. Mitchell\\"}": Object {
"__typename": "Author",
"name": "John C. Mitchell",
},
"Book:{\\"isbn\\":\\"0262133210\\"}": Object {
"__typename": "Book",
"author": Object {
"__ref": "Author:{\\"name\\":\\"John C. Mitchell\\"}",
},
"isbn": "0262133210",
"title": "Foundations for Programming Languages",
},
"ROOT_QUERY": Object {
"__typename": "Query",
"book({\\"isbn\\":\\"0262133210\\"})": Object {
"__ref": "Book:{\\"isbn\\":\\"0262133210\\"}",
},
},
}
`;
exports[`TypedDocumentNode<Data, Variables> should determine Data and Variables types of {write,read}{Query,Fragment} 2`] = `
Object {
"Author:{\\"name\\":\\"Harold Abelson\\"}": Object {
"__typename": "Author",
"name": "Harold Abelson",
},
"Author:{\\"name\\":\\"John C. Mitchell\\"}": Object {
"__typename": "Author",
"name": "John C. Mitchell",
},
"Book:{\\"isbn\\":\\"0262133210\\"}": Object {
"__typename": "Book",
"author": Object {
"__ref": "Author:{\\"name\\":\\"John C. Mitchell\\"}",
},
"isbn": "0262133210",
"title": "Foundations for Programming Languages",
},
"Book:{\\"isbn\\":\\"0262510871\\"}": Object {
"__typename": "Book",
"author": Object {
"__ref": "Author:{\\"name\\":\\"Harold Abelson\\"}",
},
"isbn": "0262510871",
"title": "Structure and Interpretation of Computer Programs",
},
"ROOT_QUERY": Object {
"__typename": "Query",
"book({\\"isbn\\":\\"0262133210\\"})": Object {
"__ref": "Book:{\\"isbn\\":\\"0262133210\\"}",
},
},
}
`;
158 changes: 157 additions & 1 deletion src/cache/inmemory/__tests__/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import gql, { disableFragmentWarnings } from 'graphql-tag';

import { stripSymbols } from '../../../utilities/testing/stripSymbols';
import { cloneDeep } from '../../../utilities/common/cloneDeep';
import { makeReference, Reference, makeVar } from '../../../core';
import { makeReference, Reference, makeVar, TypedDocumentNode, isReference } from '../../../core';
import { InMemoryCache, InMemoryCacheConfig } from '../inMemoryCache';

disableFragmentWarnings();
Expand Down Expand Up @@ -2496,3 +2496,159 @@ describe("ReactiveVar and makeVar", () => {
});
});
});

describe('TypedDocumentNode<Data, Variables>', () => {
type Book = {
isbn?: string;
title: string;
author: {
name: string;
};
};

const query: TypedDocumentNode<
{ book: Book },
{ isbn: string }
> = gql`query GetBook($isbn: String!) {
book(isbn: $isbn) {
title
author {
name
}
}
}`;

const fragment: TypedDocumentNode<Book> = gql`
fragment TitleAndAuthor on Book {
title
isbn
author {
name
}
}
`;

it('should determine Data and Variables types of {write,read}{Query,Fragment}', () => {
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
book(existing, { args, toReference }) {
return existing ?? (args && toReference({
__typename: "Book",
isbn: args.isbn,
}));
}
}
},

Book: {
keyFields: ["isbn"],
},

Author: {
keyFields: ["name"],
},
},
});

// We need to define these objects separately from calling writeQuery,
// because passing them directly to writeQuery will trigger excess property
// warnings due to the extra __typename and isbn fields. Internally, we
// almost never pass object literals to writeQuery or writeFragment, so
// excess property checks should not be a problem in practice.
const jcmAuthor = {
__typename: "Author",
name: "John C. Mitchell",
};

const ffplBook = {
__typename: "Book",
isbn: "0262133210",
title: "Foundations for Programming Languages",
author: jcmAuthor,
};

const ffplVariables = {
isbn: "0262133210",
};

cache.writeQuery({
query,
variables: ffplVariables,
data: {
book: ffplBook,
},
});

expect(cache.extract()).toMatchSnapshot();

const ffplQueryResult = cache.readQuery({
query,
variables: ffplVariables,
});

if (ffplQueryResult === null) throw new Error("null result");
expect(ffplQueryResult.book.isbn).toBeUndefined();
expect(ffplQueryResult.book.author.name).toBe(jcmAuthor.name);
expect(ffplQueryResult).toEqual({
book: {
__typename: "Book",
title: "Foundations for Programming Languages",
author: {
__typename: "Author",
name: "John C. Mitchell",
},
},
});

const sicpBook = {
__typename: "Book",
isbn: "0262510871",
title: "Structure and Interpretation of Computer Programs",
author: {
__typename: "Author",
name: "Harold Abelson",
},
};

const sicpRef = cache.writeFragment({
fragment,
data: sicpBook,
});

expect(isReference(sicpRef)).toBe(true);
expect(cache.extract()).toMatchSnapshot();

const ffplFragmentResult = cache.readFragment({
fragment,
id: cache.identify(ffplBook),
});
if (ffplFragmentResult === null) throw new Error("null result");
expect(ffplFragmentResult.title).toBe(ffplBook.title);
expect(ffplFragmentResult.author.name).toBe(ffplBook.author.name);
expect(ffplFragmentResult).toEqual(ffplBook);

// This uses the read function for the Query.book field.
const sicpReadResult = cache.readQuery({
query,
variables: {
isbn: sicpBook.isbn,
},
});
if (sicpReadResult === null) throw new Error("null result");
expect(sicpReadResult.book.isbn).toBeUndefined();
expect(sicpReadResult.book.title).toBe(sicpBook.title);
expect(sicpReadResult.book.author.name).toBe(sicpBook.author.name);
expect(sicpReadResult).toEqual({
book: {
__typename: "Book",
title: "Structure and Interpretation of Computer Programs",
author: {
__typename: "Author",
name: "Harold Abelson",
},
},
});
});
});
Loading

0 comments on commit 081e604

Please sign in to comment.