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

Add the ability to allow @client fields to be sent to the link chain #10346

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
5 changes: 5 additions & 0 deletions .changeset/rude-mayflies-scream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@apollo/client': minor
---

Add the ability to allow `@client` fields to be sent to the link chain.
2 changes: 1 addition & 1 deletion config/bundlesize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { join } from "path";
import { gzipSync } from "zlib";
import bytes from "bytes";

const gzipBundleByteLengthLimit = bytes("33.01KB");
const gzipBundleByteLengthLimit = bytes("33.17KB");
const minFile = join("dist", "apollo-client.min.cjs");
const minPath = join(__dirname, "..", minFile);
const gzipByteLen = gzipSync(readFileSync(minPath)).byteLength;
Expand Down
95 changes: 94 additions & 1 deletion src/__tests__/client.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { cloneDeep, assign } from 'lodash';
import { GraphQLError, ExecutionResult, DocumentNode } from 'graphql';
import { GraphQLError, ExecutionResult, DocumentNode, print } from 'graphql';
import gql from 'graphql-tag';

import {
Expand All @@ -8,6 +8,7 @@ import {
WatchQueryFetchPolicy,
QueryOptions,
ObservableQuery,
Operation,
TypedDocumentNode,
} from '../core';

Expand Down Expand Up @@ -940,6 +941,98 @@ describe('client', () => {
.then(resolve, reject);
});

it('removes @client fields from the query before it reaches the link', async () => {
const result: { current: Operation | undefined } = {
current: undefined
}

const query = gql`
query {
author {
firstName
lastName
isInCollection @client
}
}
`;

const transformedQuery = gql`
query {
author {
firstName
lastName
}
}
`;

const link = new ApolloLink((operation) => {
result.current = operation;

return Observable.of({
data: {
author: {
firstName: 'John',
lastName: 'Smith',
__typename: 'Author',
}
}
});
});

const client = new ApolloClient({
link,
cache: new InMemoryCache({ addTypename: false }),
});

await client.query({ query });

expect(print(result.current!.query)).toEqual(print(transformedQuery));
});

it('sends @client fields to the link when defaultOptions.transformQuery.removeClientFields is `false`', async () => {
const result: { current: Operation | undefined } = {
current: undefined
};

const query = gql`
query {
author {
firstName
lastName
isInCollection @client
}
}
`;

const link = new ApolloLink((operation) => {
result.current = operation

return Observable.of({
data: {
author: {
firstName: 'John',
lastName: 'Smith',
__typename: 'Author',
}
}
});
});

const client = new ApolloClient({
link,
cache: new InMemoryCache({ addTypename: false }),
defaultOptions: {
transformQuery: {
removeClientFields: false,
}
}
});

await client.query({ query });

expect(print(result.current!.query)).toEqual(print(query));
});

itAsync('should handle named fragments on mutations', (resolve, reject) => {
const mutation = gql`
mutation {
Expand Down
2 changes: 2 additions & 0 deletions src/core/ApolloClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
RefetchQueriesResult,
InternalRefetchQueriesResult,
RefetchQueriesInclude,
TransformQueryOptions,
} from './types';

import {
Expand All @@ -39,6 +40,7 @@ export interface DefaultOptions {
watchQuery?: Partial<WatchQueryOptions<any, any>>;
query?: Partial<QueryOptions<any, any>>;
mutate?: Partial<MutationOptions<any, any, any>>;
transformQuery?: Partial<TransformQueryOptions>;
}

let hasSuggestedDevtools = false;
Expand Down
13 changes: 10 additions & 3 deletions src/core/LocalState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,9 +171,16 @@ export class LocalState<TCacheShape> {
return null;
}

// Server queries are stripped of all @client based selection sets.
public serverQuery(document: DocumentNode) {
return removeClientSetsFromDocument(document);
// Server queries by default are stripped of all @client based selection sets.
public serverQuery(
document: DocumentNode,
options: { removeClientFields?: boolean } = Object.create(null)
) {
const { removeClientFields = true } = options;

return removeClientFields
? removeClientSetsFromDocument(document)
: document;
}

public prepareContext(context?: Record<string, any>) {
Expand Down
7 changes: 6 additions & 1 deletion src/core/QueryManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -607,12 +607,17 @@ export class QueryManager<TStore> {

public transform(document: DocumentNode) {
const { transformCache } = this;
const {
removeClientFields = true
} = this.defaultOptions.transformQuery || Object.create(null);

if (!transformCache.has(document)) {
const transformed = this.cache.transformDocument(document);
const noConnection = removeConnectionDirectiveFromDocument(transformed);
const clientQuery = this.localState.clientQuery(transformed);
const serverQuery = noConnection && this.localState.serverQuery(noConnection);
const serverQuery =
noConnection &&
this.localState.serverQuery(noConnection, { removeClientFields });

const cacheEntry: TransformCacheEntry = {
document: transformed,
Expand Down
101 changes: 101 additions & 0 deletions src/core/__tests__/QueryManager/links.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// externals
import gql from 'graphql-tag';
import { print } from 'graphql'

import { Observable, ObservableSubscription } from '../../../utilities/observables/Observable';
import { ApolloLink } from '../../../link/core';
Expand Down Expand Up @@ -360,4 +361,104 @@ describe('Link interactions', () => {
});
});
});

it('removes @client fields from the query before it reaches the link', async () => {
const result: { current: Operation | undefined } = {
current: undefined
};

const query = gql`
query {
books {
id
title
isRead @client
}
}
`;

const expectedQuery = gql`
query {
books {
id
title
}
}
`;

const link = new ApolloLink((operation) => {
result.current = operation;

return Observable.of({
data: {
books: [
{ id: 1, title: 'Woo', __typename: 'Book' },
{ id: 2, title: 'Foo', __typename: 'Book' },
],
}
});
});

const queryManager = new QueryManager({
link,
cache: new InMemoryCache({ addTypename: false }),
});

await queryManager.query({ query });

expect(print(result.current!.query)).toEqual(print(expectedQuery))
});

it('sends @client fields to the link when defaultOptions.transformQuery.removeClientFields is false', async () => {
const result: { current: Operation | undefined } = {
current: undefined
};

const query = gql`
query {
books {
id
title
isRead @client
}
}
`;

const expectedQuery = gql`
query {
books {
id
title
isRead @client
}
}
`;

const link = new ApolloLink((operation) => {
result.current = operation;

return Observable.of({
data: {
books: [
{ id: 1, title: 'Woo', __typename: 'Book' },
{ id: 2, title: 'Foo', __typename: 'Book' },
],
}
});
});

const queryManager = new QueryManager({
link,
cache: new InMemoryCache({ addTypename: false }),
defaultOptions: {
transformQuery: {
removeClientFields: false
}
}
});

await queryManager.query({ query });

expect(print(result.current!.query)).toEqual(print(expectedQuery))
});
});
10 changes: 9 additions & 1 deletion src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ export type ApolloQueryResult<T> = {
*/
errors?: ReadonlyArray<GraphQLError>;
/**
* The single Error object that is passed to onError and useQuery hooks, and is often thrown during manual `client.query` calls.
* The single Error object that is passed to onError and useQuery hooks, and is often thrown during manual `client.query` calls.
* This will contain both a NetworkError field and any GraphQLErrors.
* See https://www.apollographql.com/docs/react/data/error-handling/ for more information.
*/
Expand Down Expand Up @@ -194,3 +194,11 @@ export interface Resolvers {
[ field: string ]: Resolver;
};
}

export interface TransformQueryOptions {
/**
* Determines whether fields using the `@client` directive should be removed
* from the query before it is sent through the link chain. Defaults to `true`.
*/
removeClientFields?: boolean
}
Loading