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

Automatically strip __typename for input data in variables #10724

Merged
merged 26 commits into from
Apr 13, 2023
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
79110fc
Add function to determine if a document is a mutation.
jerelmiller Apr 4, 2023
670ade3
Add type helpers to deep omit a property
jerelmiller Apr 4, 2023
d516ecb
Create an omitDeep helper to deep omit keys from an object.
jerelmiller Apr 5, 2023
6754afd
Create a helper to strip typename
jerelmiller Apr 5, 2023
38d9f3a
Strip __typename when sending variables
jerelmiller Apr 5, 2023
3188741
Ensure omitDeep handles circular references
jerelmiller Apr 5, 2023
e1e5110
Add test to validate __typename is stripped in selectHttpOptionsAndBody
jerelmiller Apr 5, 2023
02e45ab
Remove unused isMutation helper
jerelmiller Apr 5, 2023
5967b3f
Add changeset
jerelmiller Apr 5, 2023
e62d787
Increase bundlesize
jerelmiller Apr 5, 2023
f8f07c3
Strip __typename from variables sent to subscriptions
jerelmiller Apr 5, 2023
317ea7a
Move Primitive type over to DeepOmit
jerelmiller Apr 5, 2023
9719d91
Export omitDeep, stripTypename, and DeepOmit from utilities/index to …
jerelmiller Apr 5, 2023
e57ae31
Update snapshot test with newly exported utils
jerelmiller Apr 5, 2023
72c7d98
Return unmodified subtrees when key is not found
jerelmiller Apr 5, 2023
e0bd4a9
Update the wording on the changeset to be more clear on when the `__t…
jerelmiller Apr 5, 2023
f8e71ef
Update bundlesize
jerelmiller Apr 5, 2023
c741fbf
Fix issue with omitDeep when key is not found on first level of object.
jerelmiller Apr 5, 2023
80f6f2f
Ignore class instances when omitting properties via omitDeep
jerelmiller Apr 12, 2023
7849aae
Add isPlainObject export to snapshot test
jerelmiller Apr 12, 2023
bbb31a1
Increase bundlesize
jerelmiller Apr 12, 2023
fc1c040
Merge branch 'release-3.8' into strip-typename-in-mutations
alessbell Apr 13, 2023
cadf7c8
bumps bundlesize to 34.96KB (was 34.82KB)
alessbell Apr 13, 2023
0acf112
Merge branch 'release-3.8' into strip-typename-in-mutations
alessbell Apr 13, 2023
32ff5e3
bumps bundlesize to 34.98KB (was 34.96KB)
alessbell Apr 13, 2023
12d4dae
Merge branch 'strip-typename-in-mutations' of github.com:apollographq…
alessbell Apr 13, 2023
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/cyan-insects-love.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@apollo/client': patch
---

Automatically strips `__typename` fields from `variables` sent to the server when using [`HttpLink`](https://www.apollographql.com/docs/react/api/link/apollo-link-http), [`BatchHttpLink`](https://www.apollographql.com/docs/react/api/link/apollo-link-batch-http), or [`GraphQLWsLink`](https://www.apollographql.com/docs/react/api/link/apollo-link-subscriptions). This allows GraphQL data returned from a query to be used as an argument to a subsequent GraphQL operation without the need to strip the `__typename` in user-space.
11 changes: 11 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,17 @@ src/react/*
!src/utilities/
src/utilities/*
!src/utilities/promises/
!src/utilities/types/
src/utilities/types/*
!src/utilities/types/DeepOmit.ts
!src/utilities/common
src/utilities/common/*
!src/utilities/common/stripTypename.ts
!src/utilities/common/omitDeep.ts
!src/utilities/common/__tests__/
src/utilities/common/__tests__/*
!src/utilities/common/__tests__/omitDeep.ts
!src/utilities/common/__tests__/stripTypename.ts

## Allowed React Hooks
!src/react/hooks/
Expand Down
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("34.5KB");
const gzipBundleByteLengthLimit = bytes("34.60KB");
const minFile = join("dist", "apollo-client.min.cjs");
const minPath = join(__dirname, "..", minFile);
const gzipByteLen = gzipSync(readFileSync(minPath)).byteLength;
Expand Down
2 changes: 2 additions & 0 deletions src/__tests__/__snapshots__/exports.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,7 @@ Array [
"mergeIncrementalData",
"mergeOptions",
"offsetLimitPagination",
"omitDeep",
"relayStylePagination",
"removeArgumentsFromDocument",
"removeClientSetsFromDocument",
Expand All @@ -427,6 +428,7 @@ Array [
"shouldInclude",
"storeKeyNameFromField",
"stringifyForDisplay",
"stripTypename",
"valueToObjectRepresentation",
]
`;
Expand Down
258 changes: 257 additions & 1 deletion src/link/http/__tests__/HttpLink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1083,7 +1083,263 @@ describe('HttpLink', () => {
expect(errorHandler).toHaveBeenCalledWith(
new Error('HttpLink: Trying to send a client-only query to the server. To send to the server, ensure a non-client field is added to the query or set the `transformOptions.removeClientFields` option to `true`.')
);
})
});

it('strips __typename from object argument when sending a mutation', async () => {
fetchMock.mock('https://example.com/graphql', {
status: 200,
body: JSON.stringify({
data: {
__typename: 'Mutation',
updateTodo: {
__typename: 'Todo',
id: 1,
name: 'Take out trash',
completed: true
}
}
}),
headers: { 'content-type': 'application/json' }
});

const query = gql`
mutation UpdateTodo($todo: TodoInput!) {
updateTodo(todo: $todo) {
id
name
completed
}
}
`;

const link = createHttpLink({ uri: 'https://example.com/graphql' });

const todo = {
__typename: 'Todo',
id: 1,
name: 'Take out trash',
completed: true,
}

await new Promise((resolve, reject) => {
execute(link, { query, variables: { todo } }).subscribe({
next: resolve,
error: reject
});
});

const [, options] = fetchMock.lastCall()!;
const { body } = options!

expect(JSON.parse(body!.toString())).toEqual({
operationName: 'UpdateTodo',
query: print(query),
variables: {
todo: {
id: 1,
name: 'Take out trash',
completed: true,
}
}
});
});

it('strips __typename from array argument when sending a mutation', async () => {
fetchMock.mock('https://example.com/graphql', {
status: 200,
body: JSON.stringify({
data: {
__typename: 'Mutation',
updateTodos: [
{
__typename: 'Todo',
id: 1,
name: 'Take out trash',
completed: true
},
{
__typename: 'Todo',
id: 2,
name: 'Clean room',
completed: true
},
]
}
}),
headers: { 'content-type': 'application/json' }
});

const query = gql`
mutation UpdateTodos($todos: [TodoInput!]!) {
updateTodos(todos: $todos) {
id
name
completed
}
}
`;

const link = createHttpLink({ uri: 'https://example.com/graphql' });

const todos = [
{
__typename: 'Todo',
id: 1,
name: 'Take out trash',
completed: true,
},
{
__typename: 'Todo',
id: 2,
name: 'Clean room',
completed: true,
},
];

await new Promise((resolve, reject) => {
execute(link, { query, variables: { todos } }).subscribe({
next: resolve,
error: reject
});
});

const [, options] = fetchMock.lastCall()!;
const { body } = options!

expect(JSON.parse(body!.toString())).toEqual({
operationName: 'UpdateTodos',
query: print(query),
variables: {
todos: [
{
id: 1,
name: 'Take out trash',
completed: true,
},
{
id: 2,
name: 'Clean room',
completed: true,
},
]
}
});
});

it('strips __typename from mixed argument when sending a mutation', async () => {
fetchMock.mock('https://example.com/graphql', {
status: 200,
body: JSON.stringify({
data: {
__typename: 'Mutation',
updateProfile: {
__typename: 'Profile',
id: 1,
},
}
}),
headers: { 'content-type': 'application/json' }
});

const query = gql`
mutation UpdateProfile($profile: ProfileInput!) {
updateProfile(profile: $profile) {
id
}
}
`;

const link = createHttpLink({ uri: 'https://example.com/graphql' });

const profile = {
__typename: 'Profile',
id: 1,
interests: [
{ __typename: 'Interest', name: 'Hiking' },
{ __typename: 'Interest', name: 'Nature' }
],
avatar: {
__typename: 'Avatar',
url: 'https://example.com/avatar.jpg',
}
};

await new Promise((resolve, reject) => {
execute(link, { query, variables: { profile } }).subscribe({
next: resolve,
error: reject
});
});

const [, options] = fetchMock.lastCall()!;
const { body } = options!

expect(JSON.parse(body!.toString())).toEqual({
operationName: 'UpdateProfile',
query: print(query),
variables: {
profile: {
id: 1,
interests: [
{ name: 'Hiking' },
{ name: 'Nature' }
],
avatar: {
url: 'https://example.com/avatar.jpg',
},
},
}
});
});
});

it('strips __typename when sending a query', async () => {
fetchMock.mock('https://example.com/graphql', {
status: 200,
body: JSON.stringify({
data: {
__typename: 'Query',
searchTodos: []
}
}),
headers: { 'content-type': 'application/json' }
});

const query = gql`
query SearchTodos($filter: TodoFilter!) {
searchTodos(filter: $filter) {
id
name
}
}
`;

const link = createHttpLink({ uri: 'https://example.com/graphql' });

const filter = {
__typename: 'Filter',
completed: true,
};

await new Promise((resolve, reject) => {
execute(link, { query, variables: { filter } }).subscribe({
next: resolve,
error: reject
});
});

const [, options] = fetchMock.lastCall()!;
const { body } = options!

expect(JSON.parse(body!.toString())).toEqual({
operationName: 'SearchTodos',
query: print(query),
variables: {
filter: {
completed: true,
},
},
});
});

describe('Dev warnings', () => {
Expand Down
21 changes: 21 additions & 0 deletions src/link/http/__tests__/selectHttpOptionsAndBody.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,25 @@ describe('selectHttpOptionsAndBody', () => {

expect(body.query).toBe('query SampleQuery{stub{id}}');
});

it('strips __typename from variables', () => {
const operation = createOperation(
{},
{
query,
variables: {
__typename: 'Test',
nested: { __typename: 'Nested', foo: 'bar' },
array: [{ __typename: 'Item', baz: 'foo' }]
},
}
);

const { body } = selectHttpOptionsAndBody(operation, {});

expect(body.variables).toEqual({
nested: { foo: 'bar' },
array: [{ baz: 'foo' }],
});
})
});
3 changes: 2 additions & 1 deletion src/link/http/selectHttpOptionsAndBody.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ASTNode, print } from 'graphql';
import { stripTypename } from '../../utilities';

import { Operation } from '../core';

Expand Down Expand Up @@ -179,7 +180,7 @@ export function selectHttpOptionsAndBodyInternal(

//The body depends on the http options
const { operationName, extensions, variables, query } = operation;
const body: Body = { operationName, variables };
const body: Body = { operationName, variables: stripTypename(variables) };

if (http.includeExtensions) (body as any).extensions = extensions;

Expand Down
8 changes: 6 additions & 2 deletions src/link/subscriptions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import { print } from "graphql";
import type { Client } from "graphql-ws";

import { ApolloLink, Operation, FetchResult } from "../core";
import { isNonNullObject, Observable } from "../../utilities";
import { isNonNullObject, stripTypename, Observable } from "../../utilities";
import { ApolloError } from "../../errors";

interface LikeCloseEvent {
Expand All @@ -55,7 +55,11 @@ export class GraphQLWsLink extends ApolloLink {
public request(operation: Operation): Observable<FetchResult> {
return new Observable((observer) => {
return this.client.subscribe<FetchResult>(
{ ...operation, query: print(operation.query) },
{
...operation,
query: print(operation.query),
variables: stripTypename(operation.variables)
},
{
next: observer.next.bind(observer),
complete: observer.complete.bind(observer),
Expand Down
Loading