Skip to content

Commit

Permalink
Add the ability to allow @client fields to be sent to the link chain (
Browse files Browse the repository at this point in the history
#10346)

* Add test to ensure client directive is stripped from the query

* Add option to toggle whether @client directives are removed

* Add tests for the client to prove client fields can be removed via options

* Remove unused import

* Remove client-only fields in HttpLink

* Remove client-only fields in BatchHttpLink

* Minor changes to how test is setup for better readability

* Add changeset

* Fix some code formatting

* Minor change to name of some tests

* Update bundlesize

* Update error message to be less ambiguous

Co-authored-by: Alessia Bellisario <alessia@apollographql.com>
  • Loading branch information
jerelmiller and alessbell authored Dec 21, 2022
1 parent d1f04d9 commit 3bcfc42
Show file tree
Hide file tree
Showing 12 changed files with 411 additions and 12 deletions.
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

0 comments on commit 3bcfc42

Please sign in to comment.