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

Provide possibility to mask the __typename in client results #533

Merged
merged 20 commits into from
Feb 13, 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
7 changes: 7 additions & 0 deletions .changeset/modern-queens-run.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@urql/core': minor
---

Adds the `maskTypename` export to urql-core, this deeply masks typenames from the given payload.
Masking `__typename` properties is also available as a `maskTypename` option on the `Client`. Setting this to true will
strip typenames from results.
1 change: 1 addition & 0 deletions packages/core/src/__snapshots__/client.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Client {
"executeSubscription": [Function],
"fetch": undefined,
"fetchOptions": undefined,
"maskTypename": false,
"operations$": [Function],
"preferGetMethod": false,
"reexecuteOperation": [Function],
Expand Down
19 changes: 17 additions & 2 deletions packages/core/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
switchMap,
publish,
subscribe,
map,
} from 'wonka';

import {
Expand All @@ -35,7 +36,7 @@ import {
PromisifiedSource,
} from './types';

import { createRequest, toSuspenseSource, withPromise } from './utils';
import { createRequest, toSuspenseSource, withPromise, maskTypename } from './utils';
import { DocumentNode } from 'graphql';

/** Options for configuring the URQL [client]{@link Client}. */
Expand All @@ -54,6 +55,8 @@ export interface ClientOptions {
requestPolicy?: RequestPolicy;
/** Use HTTP GET for queries. */
preferGetMethod?: boolean;
/** Mask __typename from results. */
maskTypename?: boolean;
}

interface ActiveOperations {
Expand All @@ -72,6 +75,7 @@ export class Client {
suspense: boolean;
preferGetMethod: boolean;
requestPolicy: RequestPolicy;
maskTypename: boolean;

// These are internals to be used to keep track of operations
dispatchOperation: (operation: Operation) => void;
Expand All @@ -90,6 +94,7 @@ export class Client {
this.suspense = !!opts.suspense;
this.requestPolicy = opts.requestPolicy || 'cache-first';
this.preferGetMethod = !!opts.preferGetMethod;
this.maskTypename = !!opts.maskTypename;

// This subject forms the input of operations; executeOperation may be
// called to dispatch a new operation on the subject
Expand Down Expand Up @@ -182,11 +187,21 @@ export class Client {
/** Executes an Operation by sending it through the exchange pipeline It returns an observable that emits all related exchange results and keeps track of this observable's subscribers. A teardown signal will be emitted when no subscribers are listening anymore. */
executeRequestOperation(operation: Operation): Source<OperationResult> {
const { key, operationName } = operation;
const operationResults$ = pipe(
let operationResults$ = pipe(
this.results$,
filter((res: OperationResult) => res.operation.key === key)
);

if (this.maskTypename) {
operationResults$ = pipe(
operationResults$,
map(res => {
res.data = maskTypename(res.data);
return res;
}),
);
}

if (operationName === 'mutation') {
// A mutation is always limited to just a single result and is never shared
return pipe(
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ export {
makeResult,
makeErrorResult,
formatDocument,
maskTypename,
} from './utils';
1 change: 1 addition & 0 deletions packages/core/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export * from './result';
export * from './typenames';
export * from './toSuspenseSource';
export * from './stringifyVariables';
export * from './maskTypename';
export * from './withPromise';

export const noop = () => {
Expand Down
55 changes: 55 additions & 0 deletions packages/core/src/utils/maskTypename.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { maskTypename } from './maskTypename';

it('strips typename from flat objects', () => {
expect(
maskTypename({ __typename: 'Todo', id: 1 })
).toEqual({ id: 1 });
});

it('strips typename from flat objects containing dates', () => {
const date = new Date();
expect(
maskTypename({ __typename: 'Todo', id: 1, date })
).toEqual({ id: 1, date });
});

it('strips typename from nested objects', () => {
expect(
maskTypename({
__typename: 'Todo',
id: 1,
author: {
id: 2,
__typename: 'Author'
}
})
).toEqual({ id: 1, author: { id: 2 } });
});

it('strips typename from nested objects with arrays', () => {
expect(
maskTypename({
__typename: 'Todo',
id: 1,
author: {
id: 2,
__typename: 'Author',
books: [
{ id: 3, __typename: 'Book', review: { id: 8, __typename: 'Review' } },
{ id: 4, __typename: 'Book' },
{ id: 5, __typename: 'Book' },
]
}
})
).toEqual({
id: 1,
author: {
id: 2,
books: [
{ id: 3, review: { id: 8 } },
{ id: 4 },
{ id: 5 },
]
}
});
});
21 changes: 21 additions & 0 deletions packages/core/src/utils/maskTypename.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export const maskTypename = (data: any): any => {
if (!data || typeof data !== 'object') return data;

return Object.keys(data).reduce((acc, key: string) => {
const value = data[key];
if (key === '__typename') {
Object.defineProperty(acc, '__typename', {
enumerable: false,
value,
});
} else if (Array.isArray(value)) {
acc[key] = value.map(maskTypename);
} else if (typeof value === 'object' && '__typename' in value) {
acc[key] = maskTypename(value);
} else {
acc[key] = value;
}

return acc;
}, {});
}
7 changes: 4 additions & 3 deletions packages/preact-urql/src/hooks/useMutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,11 @@ export const useMutation = <T = any, V = object>(
fetching: true,
});

const request = createRequest(query, variables as any);

return pipe(
client.executeMutation(request, context || {}),
client.executeMutation(
createRequest(query, variables as any),
context || {},
),
toPromise
).then(result => {
setState({
Expand Down
9 changes: 1 addition & 8 deletions packages/preact-urql/src/hooks/useQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,14 +80,7 @@ export const useQuery = <T = any, V = object>(
);
unsubscribe.current = result.unsubscribe;
},
[
args.context,
args.requestPolicy,
args.pollInterval,
client,
request,
setState,
]
[setState, client, request, args.requestPolicy, args.pollInterval, args.context]
);

useImmediateEffect(() => {
Expand Down
4 changes: 4 additions & 0 deletions packages/react-urql/src/__snapshots__/context.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Object {
"executeSubscription": [Function],
"fetch": undefined,
"fetchOptions": undefined,
"maskTypename": false,
"operations$": [Function],
"preferGetMethod": false,
"reexecuteOperation": [Function],
Expand All @@ -44,6 +45,7 @@ Object {
"executeSubscription": [Function],
"fetch": undefined,
"fetchOptions": undefined,
"maskTypename": false,
"operations$": [Function],
"preferGetMethod": false,
"reexecuteOperation": [Function],
Expand Down Expand Up @@ -82,6 +84,7 @@ Object {
"executeSubscription": [Function],
"fetch": undefined,
"fetchOptions": undefined,
"maskTypename": false,
"operations$": [Function],
"preferGetMethod": false,
"reexecuteOperation": [Function],
Expand All @@ -101,6 +104,7 @@ Object {
"executeSubscription": [Function],
"fetch": undefined,
"fetchOptions": undefined,
"maskTypename": false,
"operations$": [Function],
"preferGetMethod": false,
"reexecuteOperation": [Function],
Expand Down
9 changes: 5 additions & 4 deletions packages/react-urql/src/hooks/useMutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
OperationResult,
OperationContext,
CombinedError,
createRequest
createRequest,
} from '@urql/core';

import { useClient } from '../context';
Expand Down Expand Up @@ -39,10 +39,11 @@ export const useMutation = <T = any, V = object>(
(variables?: V, context?: Partial<OperationContext>) => {
setState({ ...initialState, fetching: true });

const request = createRequest(query, variables as any);

return pipe(
client.executeMutation(request, context || {}),
client.executeMutation(
createRequest(query, variables as any),
context || {},
),
toPromise
).then(result => {
setState({
Expand Down