Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: added support for automatic type inference with typed-document-node #6720

Merged
merged 7 commits into from
Aug 4, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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