Skip to content

Commit

Permalink
[Data masking] Add @unmask directive with field access warnings (#1…
Browse files Browse the repository at this point in the history
  • Loading branch information
jerelmiller authored Jul 24, 2024
1 parent 03d84ef commit b8a0d02
Show file tree
Hide file tree
Showing 30 changed files with 2,741 additions and 813 deletions.
2 changes: 1 addition & 1 deletion .api-reports/api-report-cache.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export abstract class ApolloCache<TSerialized> implements DataProxy {
// (undocumented)
identify(object: StoreObject | Reference): string | undefined;
// (undocumented)
maskDocument<TData = unknown>(document: DocumentNode, data: TData): TData;
maskOperation<TData = unknown>(document: DocumentNode, data: TData): TData;
// (undocumented)
modify<Entity extends Record<string, any> = Record<string, any>>(options: Cache_2.ModifyOptions<Entity>): boolean;
// (undocumented)
Expand Down
2 changes: 1 addition & 1 deletion .api-reports/api-report-core.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export abstract class ApolloCache<TSerialized> implements DataProxy {
// (undocumented)
identify(object: StoreObject | Reference): string | undefined;
// (undocumented)
maskDocument<TData = unknown>(document: DocumentNode, data: TData): TData;
maskOperation<TData = unknown>(document: DocumentNode, data: TData): TData;
// (undocumented)
modify<Entity extends Record<string, any> = Record<string, any>>(options: Cache_2.ModifyOptions<Entity>): boolean;
// (undocumented)
Expand Down
2 changes: 1 addition & 1 deletion .api-reports/api-report-react.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ abstract class ApolloCache<TSerialized> implements DataProxy {
// (undocumented)
identify(object: StoreObject | Reference): string | undefined;
// (undocumented)
maskDocument<TData = unknown>(document: DocumentNode, data: TData): TData;
maskOperation<TData = unknown>(document: DocumentNode, data: TData): TData;
// (undocumented)
modify<Entity extends Record<string, any> = Record<string, any>>(options: Cache_2.ModifyOptions<Entity>): boolean;
// Warning: (ae-forgotten-export) The symbol "Transaction" needs to be exported by the entry point index.d.ts
Expand Down
2 changes: 1 addition & 1 deletion .api-reports/api-report-react_components.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ abstract class ApolloCache<TSerialized> implements DataProxy {
// (undocumented)
identify(object: StoreObject | Reference): string | undefined;
// (undocumented)
maskDocument<TData = unknown>(document: DocumentNode, data: TData): TData;
maskOperation<TData = unknown>(document: DocumentNode, data: TData): TData;
// (undocumented)
modify<Entity extends Record<string, any> = Record<string, any>>(options: Cache_2.ModifyOptions<Entity>): boolean;
// Warning: (ae-forgotten-export) The symbol "Transaction" needs to be exported by the entry point index.d.ts
Expand Down
2 changes: 1 addition & 1 deletion .api-reports/api-report-react_context.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ abstract class ApolloCache<TSerialized> implements DataProxy {
// (undocumented)
identify(object: StoreObject | Reference): string | undefined;
// (undocumented)
maskDocument<TData = unknown>(document: DocumentNode, data: TData): TData;
maskOperation<TData = unknown>(document: DocumentNode, data: TData): TData;
// (undocumented)
modify<Entity extends Record<string, any> = Record<string, any>>(options: Cache_2.ModifyOptions<Entity>): boolean;
// Warning: (ae-forgotten-export) The symbol "Transaction" needs to be exported by the entry point index.d.ts
Expand Down
2 changes: 1 addition & 1 deletion .api-reports/api-report-react_hoc.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ abstract class ApolloCache<TSerialized> implements DataProxy {
// (undocumented)
identify(object: StoreObject | Reference): string | undefined;
// (undocumented)
maskDocument<TData = unknown>(document: DocumentNode, data: TData): TData;
maskOperation<TData = unknown>(document: DocumentNode, data: TData): TData;
// (undocumented)
modify<Entity extends Record<string, any> = Record<string, any>>(options: Cache_2.ModifyOptions<Entity>): boolean;
// Warning: (ae-forgotten-export) The symbol "Transaction" needs to be exported by the entry point index.d.ts
Expand Down
2 changes: 1 addition & 1 deletion .api-reports/api-report-react_hooks.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ abstract class ApolloCache<TSerialized> implements DataProxy {
// (undocumented)
identify(object: StoreObject | Reference): string | undefined;
// (undocumented)
maskDocument<TData = unknown>(document: DocumentNode, data: TData): TData;
maskOperation<TData = unknown>(document: DocumentNode, data: TData): TData;
// (undocumented)
modify<Entity extends Record<string, any> = Record<string, any>>(options: Cache_2.ModifyOptions<Entity>): boolean;
// Warning: (ae-forgotten-export) The symbol "Transaction" needs to be exported by the entry point index.d.ts
Expand Down
2 changes: 1 addition & 1 deletion .api-reports/api-report-react_internal.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ abstract class ApolloCache<TSerialized> implements DataProxy {
// (undocumented)
identify(object: StoreObject | Reference): string | undefined;
// (undocumented)
maskDocument<TData = unknown>(document: DocumentNode, data: TData): TData;
maskOperation<TData = unknown>(document: DocumentNode, data: TData): TData;
// (undocumented)
modify<Entity extends Record<string, any> = Record<string, any>>(options: Cache_2.ModifyOptions<Entity>): boolean;
// Warning: (ae-forgotten-export) The symbol "Transaction" needs to be exported by the entry point index.d.ts
Expand Down
2 changes: 1 addition & 1 deletion .api-reports/api-report-react_ssr.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ abstract class ApolloCache<TSerialized> implements DataProxy {
// (undocumented)
identify(object: StoreObject | Reference): string | undefined;
// (undocumented)
maskDocument<TData = unknown>(document: DocumentNode, data: TData): TData;
maskOperation<TData = unknown>(document: DocumentNode, data: TData): TData;
// (undocumented)
modify<Entity extends Record<string, any> = Record<string, any>>(options: Cache_2.ModifyOptions<Entity>): boolean;
// Warning: (ae-forgotten-export) The symbol "Transaction" needs to be exported by the entry point index.d.ts
Expand Down
2 changes: 1 addition & 1 deletion .api-reports/api-report-testing.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ abstract class ApolloCache<TSerialized> implements DataProxy {
// (undocumented)
identify(object: StoreObject | Reference): string | undefined;
// (undocumented)
maskDocument<TData = unknown>(document: DocumentNode, data: TData): TData;
maskOperation<TData = unknown>(document: DocumentNode, data: TData): TData;
// (undocumented)
modify<Entity extends Record<string, any> = Record<string, any>>(options: Cache_2.ModifyOptions<Entity>): boolean;
// Warning: (ae-forgotten-export) The symbol "Transaction" needs to be exported by the entry point index.d.ts
Expand Down
2 changes: 1 addition & 1 deletion .api-reports/api-report-testing_core.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ abstract class ApolloCache<TSerialized> implements DataProxy {
// (undocumented)
identify(object: StoreObject | Reference): string | undefined;
// (undocumented)
maskDocument<TData = unknown>(document: DocumentNode, data: TData): TData;
maskOperation<TData = unknown>(document: DocumentNode, data: TData): TData;
// (undocumented)
modify<Entity extends Record<string, any> = Record<string, any>>(options: Cache_2.ModifyOptions<Entity>): boolean;
// Warning: (ae-forgotten-export) The symbol "Transaction" needs to be exported by the entry point index.d.ts
Expand Down
5 changes: 4 additions & 1 deletion .api-reports/api-report-utilities.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ abstract class ApolloCache<TSerialized> implements DataProxy {
// (undocumented)
identify(object: StoreObject | Reference): string | undefined;
// (undocumented)
maskDocument<TData = unknown>(document: DocumentNode, data: TData): TData;
maskOperation<TData = unknown>(document: DocumentNode, data: TData): TData;
// (undocumented)
modify<Entity extends Record<string, any> = Record<string, any>>(options: Cache_2.ModifyOptions<Entity>): boolean;
// Warning: (ae-forgotten-export) The symbol "Transaction" needs to be exported by the entry point index.d.ts
Expand Down Expand Up @@ -1151,6 +1151,9 @@ export function getFragmentDefinitions(doc: DocumentNode): FragmentDefinitionNod
// @public (undocumented)
export function getFragmentFromSelection(selection: SelectionNode, fragmentMap?: FragmentMap | FragmentMapFunction): InlineFragmentNode | FragmentDefinitionNode | null;

// @internal (undocumented)
export function getFragmentMaskMode(fragment: FragmentSpreadNode): "mask" | "migrate" | "unmask";

// @public
export function getFragmentQueryDocument(document: DocumentNode, fragmentName?: string): DocumentNode;

Expand Down
2 changes: 1 addition & 1 deletion .api-reports/api-report.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export abstract class ApolloCache<TSerialized> implements DataProxy {
// (undocumented)
identify(object: StoreObject | Reference): string | undefined;
// (undocumented)
maskDocument<TData = unknown>(document: DocumentNode, data: TData): TData;
maskOperation<TData = unknown>(document: DocumentNode, data: TData): TData;
// (undocumented)
modify<Entity extends Record<string, any> = Record<string, any>>(options: Cache_2.ModifyOptions<Entity>): boolean;
// (undocumented)
Expand Down
3 changes: 0 additions & 3 deletions .github/workflows/compare-build-output.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
name: Compare Build Output
on:
pull_request:
branches:
- main
- release-*

concurrency: ${{ github.workflow }}-${{ github.ref }}

Expand Down
4 changes: 2 additions & 2 deletions .size-limits.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"dist/apollo-client.min.cjs": 39923,
"import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 33211
"dist/apollo-client.min.cjs": 40154,
"import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 33429
}
1 change: 1 addition & 0 deletions src/__tests__/__snapshots__/exports.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,7 @@ Array [
"getFragmentDefinition",
"getFragmentDefinitions",
"getFragmentFromSelection",
"getFragmentMaskMode",
"getFragmentQueryDocument",
"getGraphQLErrorsFromResult",
"getInclusionDirectives",
Expand Down
220 changes: 220 additions & 0 deletions src/__tests__/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
Kind,
print,
visit,
FragmentSpreadNode,
} from "graphql";
import gql from "graphql-tag";

Expand Down Expand Up @@ -6599,6 +6600,149 @@ describe("data masking", () => {
}
});

it("does not mask fragments marked with @unmask", async () => {
interface Query {
currentUser: {
__typename: "User";
id: number;
name: string;
};
}

const query: TypedDocumentNode<Query, never> = gql`
query UnmaskedQuery {
currentUser {
id
name
...UserFields @unmask
}
}

fragment UserFields on User {
age
}
`;

const mocks = [
{
request: { query },
result: {
data: {
currentUser: {
__typename: "User",
id: 1,
name: "Test User",
age: 30,
},
},
},
},
];

const client = new ApolloClient({
dataMasking: true,
cache: new InMemoryCache(),
link: new MockLink(mocks),
});

const observable = client.watchQuery({ query });

const stream = new ObservableStream(observable);

{
const { data } = await stream.takeNext();

expect(data).toEqual({
currentUser: {
__typename: "User",
id: 1,
name: "Test User",
age: 30,
},
});
}
});

it("does not mask fragments marked with @unmask added by document transforms", async () => {
const documentTransform = new DocumentTransform((document) => {
return visit(document, {
FragmentSpread(node) {
return {
...node,
directives: [
{
kind: Kind.DIRECTIVE,
name: { kind: Kind.NAME, value: "unmask" },
},
],
} satisfies FragmentSpreadNode;
},
});
});

interface Query {
currentUser: {
__typename: "User";
id: number;
name: string;
};
}

const query: TypedDocumentNode<Query, never> = gql`
query UnmaskedQuery {
currentUser {
id
name
...UserFields
}
}

fragment UserFields on User {
age
}
`;

const mocks = [
{
request: { query },
result: {
data: {
currentUser: {
__typename: "User",
id: 1,
name: "Test User",
age: 30,
},
},
},
},
];

const client = new ApolloClient({
dataMasking: true,
cache: new InMemoryCache(),
link: new MockLink(mocks),
documentTransform,
});

const observable = client.watchQuery({ query });

const stream = new ObservableStream(observable);

{
const { data } = await stream.takeNext();

expect(data).toEqual({
currentUser: {
__typename: "User",
id: 1,
name: "Test User",
age: 30,
},
});
}
});

it("does not mask query when using a cache that does not support it", async () => {
using _ = spyOnConsole("warn");

Expand Down Expand Up @@ -7252,6 +7396,82 @@ describe("data masking", () => {
}
}
);

it("warns when accessing a unmasked field while using @unmask with mode: 'migrate'", async () => {
using consoleSpy = spyOnConsole("warn");

interface Query {
currentUser: {
__typename: "User";
id: number;
name: string;
age: number;
};
}

const query: TypedDocumentNode<Query, never> = gql`
query UnmaskedQuery {
currentUser {
id
name
...UserFields @unmask(mode: "migrate")
}
}

fragment UserFields on User {
age
name
}
`;

const mocks = [
{
request: { query },
result: {
data: {
currentUser: {
__typename: "User",
id: 1,
name: "Test User",
age: 34,
},
},
},
delay: 20,
},
];

const client = new ApolloClient({
dataMasking: true,
cache: new InMemoryCache(),
link: new MockLink(mocks),
});

const observable = client.watchQuery({ query });
const stream = new ObservableStream(observable);

{
const { data } = await stream.takeNext();
data.currentUser.__typename;
data.currentUser.id;
data.currentUser.name;

expect(consoleSpy.warn).not.toHaveBeenCalled();

data.currentUser.age;

expect(consoleSpy.warn).toHaveBeenCalledTimes(1);
expect(consoleSpy.warn).toHaveBeenCalledWith(
"Accessing unmasked field on %s at path '%s'. This field will not be available when masking is enabled. Please read the field from the fragment instead.",
"query 'UnmaskedQuery'",
"currentUser.age"
);

// Ensure we only warn once
data.currentUser.age;
expect(consoleSpy.warn).toHaveBeenCalledTimes(1);
}
});
});

function clientRoundtrip(
Expand Down
Loading

0 comments on commit b8a0d02

Please sign in to comment.