diff --git a/.api-reports/api-report-cache.api.md b/.api-reports/api-report-cache.api.md index 533a64c6689..b539bec11c8 100644 --- a/.api-reports/api-report-cache.api.md +++ b/.api-reports/api-report-cache.api.md @@ -30,6 +30,8 @@ export abstract class ApolloCache implements DataProxy { abstract evict(options: Cache_2.EvictOptions): boolean; abstract extract(optimistic?: boolean): TSerialized; // (undocumented) + fragmentMatches?(fragment: InlineFragmentNode, typename: string): boolean; + // (undocumented) gc(): string[]; // Warning: (ae-forgotten-export) The symbol "getApolloCacheMemoryInternals" needs to be exported by the entry point index.d.ts // @@ -38,15 +40,19 @@ export abstract class ApolloCache implements DataProxy { // (undocumented) identify(object: StoreObject | Reference): string | undefined; // (undocumented) + lookupFragment(fragmentName: string): FragmentDefinitionNode | null; + // (undocumented) modify = Record>(options: Cache_2.ModifyOptions): boolean; // (undocumented) abstract performTransaction(transaction: Transaction, optimisticId?: string | null): void; + // Warning: (ae-forgotten-export) The symbol "Unmasked" needs to be exported by the entry point index.d.ts + // // (undocumented) - abstract read(query: Cache_2.ReadOptions): TData | null; + abstract read(query: Cache_2.ReadOptions): Unmasked | null; // (undocumented) - readFragment(options: Cache_2.ReadFragmentOptions, optimistic?: boolean): FragmentType | null; + readFragment(options: Cache_2.ReadFragmentOptions, optimistic?: boolean): Unmasked | null; // (undocumented) - readQuery(options: Cache_2.ReadQueryOptions, optimistic?: boolean): QueryType | null; + readQuery(options: Cache_2.ReadQueryOptions, optimistic?: boolean): Unmasked | null; // (undocumented) recordOptimisticTransaction(transaction: Transaction, optimisticId: string): void; // (undocumented) @@ -59,9 +65,9 @@ export abstract class ApolloCache implements DataProxy { // (undocumented) transformForLink(document: DocumentNode): DocumentNode; // (undocumented) - updateFragment(options: Cache_2.UpdateFragmentOptions, update: (data: TData | null) => TData | null | void): TData | null; + updateFragment(options: Cache_2.UpdateFragmentOptions, update: (data: Unmasked | null) => Unmasked | null | void): Unmasked | null; // (undocumented) - updateQuery(options: Cache_2.UpdateQueryOptions, update: (data: TData | null) => TData | null | void): TData | null; + updateQuery(options: Cache_2.UpdateQueryOptions, update: (data: Unmasked | null) => Unmasked | null | void): Unmasked | null; // (undocumented) abstract watch(watch: Cache_2.WatchOptions): () => void; // Warning: (ae-forgotten-export) The symbol "OperationVariables" needs to be exported by the entry point index.d.ts @@ -166,7 +172,7 @@ namespace Cache_2 { // (undocumented) dataId?: string; // (undocumented) - result: TResult; + result: Unmasked; } import DiffResult = DataProxy.DiffResult; import ReadQueryOptions = DataProxy.ReadQueryOptions; @@ -210,9 +216,22 @@ export const canonicalStringify: ((value: any) => string) & { // @public (undocumented) type CanReadFunction = (value: StoreValue) => boolean; +// Warning: (ae-forgotten-export) The symbol "UnionToIntersection" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "UnwrapFragmentRefs" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "RemoveFragmentName" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type CombineFragmentRefs> = UnionToIntersection<{ + [K in keyof FragmentRefs]-?: UnwrapFragmentRefs>; +}[keyof FragmentRefs]>; + // @public (undocumented) export function createFragmentRegistry(...fragments: DocumentNode[]): FragmentRegistryAPI; +// @public (undocumented) +interface DataMasking { +} + // @public (undocumented) export namespace DataProxy { // (undocumented) @@ -261,7 +280,7 @@ export namespace DataProxy { // (undocumented) export interface WriteOptions { broadcast?: boolean; - data: TData; + data: Unmasked; overwrite?: boolean; } // (undocumented) @@ -271,8 +290,8 @@ export namespace DataProxy { // @public export interface DataProxy { - readFragment(options: DataProxy.ReadFragmentOptions, optimistic?: boolean): FragmentType | null; - readQuery(options: DataProxy.ReadQueryOptions, optimistic?: boolean): QueryType | null; + readFragment(options: DataProxy.ReadFragmentOptions, optimistic?: boolean): Unmasked | null; + readQuery(options: DataProxy.ReadQueryOptions, optimistic?: boolean): Unmasked | null; writeFragment(options: DataProxy.WriteFragmentOptions): Reference | undefined; writeQuery(options: DataProxy.WriteQueryOptions): Reference | undefined; } @@ -517,6 +536,17 @@ export interface FragmentRegistryAPI { transform(document: D): D; } +// @public (undocumented) +type FragmentType = [ +TData +] extends [{ + " $fragmentName"?: infer TKey; +}] ? TKey extends string ? { + " $fragmentRefs"?: { + [key in TKey]: TData; + }; +} : never : never; + // @internal const getApolloCacheMemoryInternals: (() => { cache: { @@ -577,6 +607,8 @@ export class InMemoryCache extends ApolloCache { // (undocumented) extract(optimistic?: boolean): NormalizedCacheObject; // (undocumented) + fragmentMatches(fragment: InlineFragmentNode, typename: string): boolean; + // (undocumented) gc(options?: { resetResultCache?: boolean; resetResultIdentities?: boolean; @@ -588,6 +620,8 @@ export class InMemoryCache extends ApolloCache { // (undocumented) identify(object: StoreObject | Reference): string | undefined; // (undocumented) + lookupFragment(fragmentName: string): FragmentDefinitionNode | null; + // (undocumented) readonly makeVar: typeof makeVar; // (undocumented) modify = Record>(options: Cache_2.ModifyOptions): boolean; @@ -696,6 +730,17 @@ export function makeReference(id: string): Reference; // @public (undocumented) export function makeVar(value: T): ReactiveVar; +// Warning: (ae-forgotten-export) The symbol "Prettify" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "RemoveMaskedMarker" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "DataMasking" needs to be exported by the entry point index.d.ts +// +// @public +type MaybeMasked = TData extends { + __masked?: true; +} ? Prettify> : DataMasking extends { + enabled: true; +} ? TData : Unmasked; + // @public (undocumented) export interface MergeInfo { // (undocumented) @@ -761,6 +806,9 @@ export type Modifiers = Record> = [FieldName in keyof T]: Modifier>>; }>; +// @public +type NoInfer_2 = [T][T extends any ? 0 : never]; + // @public export interface NormalizedCache { // (undocumented) @@ -859,6 +907,11 @@ export type PossibleTypesMap = { [supertype: string]: string[]; }; +// @public (undocumented) +type Prettify = { + [K in keyof T]: T[K]; +} & {}; + // @public (undocumented) type Primitive = null | undefined | string | number | boolean | symbol | bigint; @@ -920,6 +973,12 @@ export interface Reference { readonly __ref: string; } +// @public (undocumented) +type RemoveFragmentName = T extends any ? Omit : T; + +// @public (undocumented) +type RemoveMaskedMarker = Omit; + // @public (undocumented) type SafeReadonly = T extends object ? Readonly : T; @@ -976,22 +1035,41 @@ export type TypePolicy = { }; }; +// @public (undocumented) +type UnionToIntersection = (U extends any ? (x: U) => void : never) extends (x: infer I) => void ? I : never; + +// @public +type Unmasked = TData extends object ? UnwrapFragmentRefs>> : TData; + +// Warning: (ae-forgotten-export) The symbol "CombineFragmentRefs" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type UnwrapFragmentRefs = string extends keyof NonNullable ? TData : " $fragmentRefs" extends keyof NonNullable ? TData extends { + " $fragmentRefs"?: infer FragmentRefs extends object; +} ? Prettify<{ + [K in keyof TData as K extends " $fragmentRefs" ? never : K]: UnwrapFragmentRefs; +} & CombineFragmentRefs> : never : TData extends object ? { + [K in keyof TData]: UnwrapFragmentRefs; +} : TData; + // @public export interface WatchFragmentOptions { fragment: DocumentNode | TypedDocumentNode; fragmentName?: string; - from: StoreObject | Reference | string; + // Warning: (ae-forgotten-export) The symbol "FragmentType" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "NoInfer_2" needs to be exported by the entry point index.d.ts + from: StoreObject | Reference | FragmentType> | string; optimistic?: boolean; variables?: TVars; } // @public export type WatchFragmentResult = { - data: TData; + data: MaybeMasked; complete: true; missing?: never; } | { - data: DeepPartial; + data: DeepPartial>; complete: false; missing: MissingTree; }; @@ -1030,6 +1108,7 @@ interface WriteContext extends ReadMergeModifyContext { // Warnings were encountered during analysis: // +// src/cache/core/cache.ts:92:7 - (ae-forgotten-export) The symbol "MaybeMasked" needs to be exported by the entry point index.d.ts // src/cache/inmemory/policies.ts:92:3 - (ae-forgotten-export) The symbol "FragmentMap" needs to be exported by the entry point index.d.ts // src/cache/inmemory/policies.ts:161:3 - (ae-forgotten-export) The symbol "KeySpecifier" needs to be exported by the entry point index.d.ts // src/cache/inmemory/policies.ts:161:3 - (ae-forgotten-export) The symbol "KeyArgsFunction" needs to be exported by the entry point index.d.ts diff --git a/.api-reports/api-report-core.api.md b/.api-reports/api-report-core.api.md index 6014990bc2d..c77201045c1 100644 --- a/.api-reports/api-report-core.api.md +++ b/.api-reports/api-report-core.api.md @@ -45,6 +45,8 @@ export abstract class ApolloCache implements DataProxy { abstract evict(options: Cache_2.EvictOptions): boolean; abstract extract(optimistic?: boolean): TSerialized; // (undocumented) + fragmentMatches?(fragment: InlineFragmentNode, typename: string): boolean; + // (undocumented) gc(): string[]; // Warning: (ae-forgotten-export) The symbol "getApolloCacheMemoryInternals" needs to be exported by the entry point index.d.ts // @@ -53,15 +55,19 @@ export abstract class ApolloCache implements DataProxy { // (undocumented) identify(object: StoreObject | Reference): string | undefined; // (undocumented) + lookupFragment(fragmentName: string): FragmentDefinitionNode | null; + // (undocumented) modify = Record>(options: Cache_2.ModifyOptions): boolean; // (undocumented) abstract performTransaction(transaction: Transaction, optimisticId?: string | null): void; + // Warning: (ae-forgotten-export) The symbol "Unmasked" needs to be exported by the entry point index.d.ts + // // (undocumented) - abstract read(query: Cache_2.ReadOptions): TData | null; + abstract read(query: Cache_2.ReadOptions): Unmasked | null; // (undocumented) - readFragment(options: Cache_2.ReadFragmentOptions, optimistic?: boolean): FragmentType | null; + readFragment(options: Cache_2.ReadFragmentOptions, optimistic?: boolean): Unmasked | null; // (undocumented) - readQuery(options: Cache_2.ReadQueryOptions, optimistic?: boolean): QueryType | null; + readQuery(options: Cache_2.ReadQueryOptions, optimistic?: boolean): Unmasked | null; // (undocumented) recordOptimisticTransaction(transaction: Transaction, optimisticId: string): void; // (undocumented) @@ -74,9 +80,9 @@ export abstract class ApolloCache implements DataProxy { // (undocumented) transformForLink(document: DocumentNode): DocumentNode; // (undocumented) - updateFragment(options: Cache_2.UpdateFragmentOptions, update: (data: TData | null) => TData | null | void): TData | null; + updateFragment(options: Cache_2.UpdateFragmentOptions, update: (data: Unmasked | null) => Unmasked | null | void): Unmasked | null; // (undocumented) - updateQuery(options: Cache_2.UpdateQueryOptions, update: (data: TData | null) => TData | null | void): TData | null; + updateQuery(options: Cache_2.UpdateQueryOptions, update: (data: Unmasked | null) => Unmasked | null | void): Unmasked | null; // (undocumented) abstract watch(watch: Cache_2.WatchOptions): () => void; watchFragment(options: WatchFragmentOptions): Observable>; @@ -117,14 +123,15 @@ export class ApolloClient implements DataProxy { getResolvers(): Resolvers; // (undocumented) link: ApolloLink; - mutate = DefaultContext, TCache extends ApolloCache = ApolloCache>(options: MutationOptions): Promise>; + mutate = DefaultContext, TCache extends ApolloCache = ApolloCache>(options: MutationOptions): Promise>>; onClearStore(cb: () => Promise): () => void; onResetStore(cb: () => Promise): () => void; - query(options: QueryOptions): Promise>; + // Warning: (ae-forgotten-export) The symbol "MaybeMasked" needs to be exported by the entry point index.d.ts + query(options: QueryOptions): Promise>>; // (undocumented) queryDeduplication: boolean; - readFragment(options: DataProxy.Fragment, optimistic?: boolean): T | null; - readQuery(options: DataProxy.Query, optimistic?: boolean): T | null; + readFragment(options: DataProxy.Fragment, optimistic?: boolean): Unmasked | null; + readQuery(options: DataProxy.Query, optimistic?: boolean): Unmasked | null; reFetchObservableQueries(includeStandby?: boolean): Promise[]>; refetchQueries = ApolloCache, TResult = Promise>>(options: RefetchQueriesOptions): RefetchQueriesResult; resetStore(): Promise[] | null>; @@ -133,7 +140,7 @@ export class ApolloClient implements DataProxy { setLocalStateFragmentMatcher(fragmentMatcher: FragmentMatcher): void; setResolvers(resolvers: Resolvers | Resolvers[]): void; stop(): void; - subscribe(options: SubscriptionOptions): Observable>; + subscribe(options: SubscriptionOptions): Observable>>; // (undocumented) readonly typeDefs: ApolloClientOptions["typeDefs"]; // (undocumented) @@ -152,6 +159,7 @@ export interface ApolloClientOptions { connectToDevTools?: boolean; // (undocumented) credentials?: string; + dataMasking?: boolean; // (undocumented) defaultContext?: Partial; defaultOptions?: DefaultOptions; @@ -378,7 +386,7 @@ namespace Cache_2 { // (undocumented) dataId?: string; // (undocumented) - result: TResult; + result: Unmasked; } import DiffResult = DataProxy.DiffResult; import ReadQueryOptions = DataProxy.ReadQueryOptions; @@ -427,6 +435,15 @@ export type ClientParseError = InvariantError & { parseError: Error; }; +// Warning: (ae-forgotten-export) The symbol "UnionToIntersection" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "UnwrapFragmentRefs" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "RemoveFragmentName" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type CombineFragmentRefs> = UnionToIntersection<{ + [K in keyof FragmentRefs]-?: UnwrapFragmentRefs>; +}[keyof FragmentRefs]>; + // @public (undocumented) class Concast extends Observable { // Warning: (ae-forgotten-export) The symbol "MaybeAsync" needs to be exported by the entry point index.d.ts @@ -466,6 +483,10 @@ export const createSignalIfSupported: () => { signal: AbortSignal; }; +// @public (undocumented) +interface DataMasking { +} + // @public (undocumented) export namespace DataProxy { // (undocumented) @@ -514,7 +535,7 @@ export namespace DataProxy { // (undocumented) export interface WriteOptions { broadcast?: boolean; - data: TData; + data: Unmasked; overwrite?: boolean; } // (undocumented) @@ -524,8 +545,8 @@ export namespace DataProxy { // @public export interface DataProxy { - readFragment(options: DataProxy.ReadFragmentOptions, optimistic?: boolean): FragmentType | null; - readQuery(options: DataProxy.ReadQueryOptions, optimistic?: boolean): QueryType | null; + readFragment(options: DataProxy.ReadFragmentOptions, optimistic?: boolean): Unmasked | null; + readQuery(options: DataProxy.ReadQueryOptions, optimistic?: boolean): Unmasked | null; writeFragment(options: DataProxy.WriteFragmentOptions): Reference | undefined; writeQuery(options: DataProxy.WriteQueryOptions): Reference | undefined; } @@ -918,6 +939,17 @@ interface FragmentRegistryAPI { transform(document: D): D; } +// @public (undocumented) +type FragmentType = [ +TData +] extends [{ + " $fragmentName"?: infer TKey; +}] ? TKey extends string ? { + " $fragmentRefs"?: { + [key in TKey]: TData; + }; +} : never : never; + // @public (undocumented) export const from: typeof ApolloLink.from; @@ -1104,6 +1136,8 @@ export class InMemoryCache extends ApolloCache { // (undocumented) extract(optimistic?: boolean): NormalizedCacheObject; // (undocumented) + fragmentMatches(fragment: InlineFragmentNode, typename: string): boolean; + // (undocumented) gc(options?: { resetResultCache?: boolean; resetResultIdentities?: boolean; @@ -1115,6 +1149,8 @@ export class InMemoryCache extends ApolloCache { // (undocumented) identify(object: StoreObject | Reference): string | undefined; // (undocumented) + lookupFragment(fragmentName: string): FragmentDefinitionNode | null; + // (undocumented) readonly makeVar: typeof makeVar; // (undocumented) modify = Record>(options: Cache_2.ModifyOptions): boolean; @@ -1200,11 +1236,11 @@ export function isNetworkRequestSettled(networkStatus?: NetworkStatus): boolean; // @public (undocumented) export function isReference(obj: any): obj is Reference; -// Warning: (ae-forgotten-export) The symbol "UnionToIntersection" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "UnionToIntersection_2" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "UnionForAny" needs to be exported by the entry point index.d.ts // // @public (undocumented) -type IsStrictlyAny = UnionToIntersection> extends never ? true : false; +type IsStrictlyAny = UnionToIntersection_2> extends never ? true : false; // @public (undocumented) type KeyArgsFunction = (args: Record | null, context: { @@ -1304,9 +1340,38 @@ export function makeReference(id: string): Reference; // @public (undocumented) export function makeVar(value: T): ReactiveVar; +// @public (undocumented) +interface MaskFragmentOptions { + // (undocumented) + data: TData; + // (undocumented) + fragment: DocumentNode; + // (undocumented) + fragmentName?: string; +} + +// @public (undocumented) +interface MaskOperationOptions { + // (undocumented) + data: TData; + // (undocumented) + document: DocumentNode; +} + // @public (undocumented) type MaybeAsync = T | PromiseLike; +// Warning: (ae-forgotten-export) The symbol "Prettify" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "RemoveMaskedMarker" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "DataMasking" needs to be exported by the entry point index.d.ts +// +// @public +type MaybeMasked = TData extends { + __masked?: true; +} ? Prettify> : DataMasking extends { + enabled: true; +} ? TData : Unmasked; + // @public (undocumented) export interface MergeInfo { // (undocumented) @@ -1391,11 +1456,12 @@ interface MutationBaseOptions; + // Warning: (ae-forgotten-export) The symbol "NoInfer_2" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts - optimisticResponse?: TData | ((vars: TVariables, { IGNORE }: { + optimisticResponse?: Unmasked> | ((vars: TVariables, { IGNORE }: { IGNORE: IgnoreModifier; - }) => TData | IgnoreModifier); - refetchQueries?: ((result: FetchResult) => InternalRefetchQueriesInclude) | InternalRefetchQueriesInclude; + }) => Unmasked> | IgnoreModifier); + refetchQueries?: ((result: FetchResult>) => InternalRefetchQueriesInclude) | InternalRefetchQueriesInclude; update?: MutationUpdaterFunction; updateQueries?: MutationQueryReducersMap; variables?: TVariables; @@ -1413,7 +1479,7 @@ export interface MutationOptions = (previousResult: Record, options: { - mutationResult: FetchResult; + mutationResult: FetchResult>; queryName: string | undefined; queryVariables: Record; }) => Record; @@ -1451,7 +1517,7 @@ export type MutationUpdaterFn = (cache: ApolloCache, mutationResult: FetchResult) => void; // @public (undocumented) -export type MutationUpdaterFunction> = (cache: TCache, result: Omit, "context">, options: { +export type MutationUpdaterFunction> = (cache: TCache, result: Omit>, "context">, options: { context?: TContext; variables?: TVariables; }) => void; @@ -1485,6 +1551,9 @@ export type NextLink = (operation: Operation) => Observable; // @public (undocumented) type NextResultListener = (method: "next" | "error" | "complete", arg?: any) => any; +// @public +type NoInfer_2 = [T][T extends any ? 0 : never]; + // @public export interface NormalizedCache { // (undocumented) @@ -1531,20 +1600,20 @@ export interface NormalizedCacheObject { export { Observable } // @public (undocumented) -export class ObservableQuery extends Observable> { +export class ObservableQuery extends Observable>> { constructor({ queryManager, queryInfo, options, }: { queryManager: QueryManager; queryInfo: QueryInfo; options: WatchQueryOptions; }); fetchMore(fetchMoreOptions: FetchMoreQueryOptions & { - updateQuery?: (previousQueryResult: TData, options: { - fetchMoreResult: TFetchData; + updateQuery?: (previousQueryResult: Unmasked, options: { + fetchMoreResult: Unmasked; variables: TFetchVars; - }) => TData; - }): Promise>; + }) => Unmasked; + }): Promise>>; // (undocumented) - getCurrentResult(saveAsLastResult?: boolean): ApolloQueryResult; + getCurrentResult(saveAsLastResult?: boolean): ApolloQueryResult>; // (undocumented) getLastError(variablesMustMatch?: boolean): ApolloError | undefined; // (undocumented) @@ -1561,9 +1630,9 @@ export class ObservableQuery): Promise>; + refetch(variables?: Partial): Promise>>; // (undocumented) - reobserve(newOptions?: Partial>, newNetworkStatus?: NetworkStatus): Promise>; + reobserve(newOptions?: Partial>, newNetworkStatus?: NetworkStatus): Promise>>; // Warning: (ae-forgotten-export) The symbol "Concast" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1575,20 +1644,20 @@ export class ObservableQuery) => void, onError?: (error: any) => void, onComplete?: () => void): ObservableSubscription; + resubscribeAfterError(onNext: (value: ApolloQueryResult>) => void, onError?: (error: any) => void, onComplete?: () => void): ObservableSubscription; // (undocumented) resubscribeAfterError(observer: Observer>): ObservableSubscription; // (undocumented) - result(): Promise>; + result(): Promise>>; // (undocumented) - setOptions(newOptions: Partial>): Promise>; - setVariables(variables: TVariables): Promise | void>; + setOptions(newOptions: Partial>): Promise>>; + setVariables(variables: TVariables): Promise> | void>; // (undocumented) silentSetOptions(newOptions: Partial>): void; startPolling(pollInterval: number): void; stopPolling(): void; subscribeToMore(options: SubscribeToMoreOptions): () => void; - updateQuery(mapFn: (previousQueryResult: TData, options: Pick, "variables">) => TData): void; + updateQuery(mapFn: (previousQueryResult: Unmasked, options: Pick, "variables">) => Unmasked): void; get variables(): TVariables | undefined; } @@ -1684,6 +1753,11 @@ export type PossibleTypesMap = { [supertype: string]: string[]; }; +// @public (undocumented) +type Prettify = { + [K in keyof T]: T[K]; +} & {}; + // @public (undocumented) type Primitive = null | undefined | string | number | boolean | symbol | bigint; @@ -1773,6 +1847,8 @@ class QueryManager { // (undocumented) clearStore(options?: Cache_2.ResetOptions): Promise; // (undocumented) + readonly dataMasking: boolean; + // (undocumented) readonly defaultContext: Partial; // (undocumented) defaultOptions: DefaultOptions; @@ -1837,14 +1913,22 @@ class QueryManager { onQueryUpdated?: OnQueryUpdated; keepRootFields?: boolean; }, cache?: ApolloCache): Promise>; + // Warning: (ae-forgotten-export) The symbol "MaskFragmentOptions" needs to be exported by the entry point index.d.ts + // + // (undocumented) + maskFragment(options: MaskFragmentOptions): TData; + // Warning: (ae-forgotten-export) The symbol "MaskOperationOptions" needs to be exported by the entry point index.d.ts + // + // (undocumented) + maskOperation(options: MaskOperationOptions): MaybeMasked; // (undocumented) - mutate, TCache extends ApolloCache>({ mutation, variables, optimisticResponse, updateQueries, refetchQueries, awaitRefetchQueries, update: updateWithProxyFn, onQueryUpdated, fetchPolicy, errorPolicy, keepRootFields, context, }: MutationOptions): Promise>; + mutate, TCache extends ApolloCache>({ mutation, variables, optimisticResponse, updateQueries, refetchQueries, awaitRefetchQueries, update: updateWithProxyFn, onQueryUpdated, fetchPolicy, errorPolicy, keepRootFields, context, }: MutationOptions): Promise>>; // (undocumented) mutationStore?: { [mutationId: string]: MutationStoreValue; }; // (undocumented) - query(options: QueryOptions, queryId?: string): Promise>; + query(options: QueryOptions, queryId?: string): Promise>>; // (undocumented) reFetchObservableQueries(includeStandby?: boolean): Promise[]>; // (undocumented) @@ -1858,7 +1942,7 @@ class QueryManager { // (undocumented) readonly ssrMode: boolean; // (undocumented) - startGraphQLSubscription({ query, fetchPolicy, errorPolicy, variables, context, extensions, }: SubscriptionOptions): Observable>; + startGraphQLSubscription(options: SubscriptionOptions): Observable>; stop(): void; // (undocumented) stopQuery(queryId: string): void; @@ -1879,6 +1963,8 @@ interface QueryManagerOptions { // (undocumented) clientAwareness: Record; // (undocumented) + dataMasking: boolean; + // (undocumented) defaultContext: Partial | undefined; // (undocumented) defaultOptions: DefaultOptions; @@ -2012,6 +2098,12 @@ export type RefetchQueryDescriptor = string | DocumentNode; // @public (undocumented) export type RefetchWritePolicy = "merge" | "overwrite"; +// @public (undocumented) +type RemoveFragmentName = T extends any ? Omit : T; + +// @public (undocumented) +type RemoveMaskedMarker = Omit; + // @public (undocumented) export type RequestHandler = (operation: Operation, forward: NextLink) => Observable | null; @@ -2195,6 +2287,8 @@ interface TransformCacheEntry { // (undocumented) hasNonreactiveDirective: boolean; // (undocumented) + nonReactiveQuery: DocumentNode; + // (undocumented) serverQuery: DocumentNode | null; } @@ -2224,18 +2318,35 @@ export type TypePolicy = { type UnionForAny = T extends never ? "a" : 1; // @public (undocumented) -type UnionToIntersection = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never; +type UnionToIntersection = (U extends any ? (x: U) => void : never) extends (x: infer I) => void ? I : never; + +// @public (undocumented) +type UnionToIntersection_2 = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never; + +// @public +type Unmasked = TData extends object ? UnwrapFragmentRefs>> : TData; + +// Warning: (ae-forgotten-export) The symbol "CombineFragmentRefs" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type UnwrapFragmentRefs = string extends keyof NonNullable ? TData : " $fragmentRefs" extends keyof NonNullable ? TData extends { + " $fragmentRefs"?: infer FragmentRefs extends object; +} ? Prettify<{ + [K in keyof TData as K extends " $fragmentRefs" ? never : K]: UnwrapFragmentRefs; +} & CombineFragmentRefs> : never : TData extends object ? { + [K in keyof TData]: UnwrapFragmentRefs; +} : TData; // @public (undocumented) type UpdateQueries = MutationOptions["updateQueries"]; // @public (undocumented) -type UpdateQueryFn = (previousQueryResult: TData, options: { +type UpdateQueryFn = (previousQueryResult: Unmasked, options: { subscriptionData: { - data: TSubscriptionData; + data: Unmasked; }; variables?: TSubscriptionVariables; -}) => TData; +}) => Unmasked; // @public (undocumented) export interface UpdateQueryOptions { @@ -2253,18 +2364,19 @@ export interface UriFunction { export interface WatchFragmentOptions { fragment: DocumentNode | TypedDocumentNode; fragmentName?: string; - from: StoreObject | Reference | string; + // Warning: (ae-forgotten-export) The symbol "FragmentType" needs to be exported by the entry point index.d.ts + from: StoreObject | Reference | FragmentType> | string; optimistic?: boolean; variables?: TVars; } // @public export type WatchFragmentResult = { - data: TData; + data: MaybeMasked; complete: true; missing?: never; } | { - data: DeepPartial; + data: DeepPartial>; complete: false; missing: MissingTree; }; @@ -2317,11 +2429,11 @@ interface WriteContext extends ReadMergeModifyContext { // src/cache/inmemory/policies.ts:161:3 - (ae-forgotten-export) The symbol "KeySpecifier" needs to be exported by the entry point index.d.ts // src/cache/inmemory/policies.ts:161:3 - (ae-forgotten-export) The symbol "KeyArgsFunction" needs to be exported by the entry point index.d.ts // src/cache/inmemory/types.ts:139:3 - (ae-forgotten-export) The symbol "KeyFieldsFunction" needs to be exported by the entry point index.d.ts -// src/core/ObservableQuery.ts:116:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts -// src/core/ObservableQuery.ts:117:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:138:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:382:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:275:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:120:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:121:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:155:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:408:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:277:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts // src/link/http/selectHttpOptionsAndBody.ts:128:32 - (ae-forgotten-export) The symbol "HttpQueryOptions" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/.api-reports/api-report-masking.api.md b/.api-reports/api-report-masking.api.md new file mode 100644 index 00000000000..4468e51a049 --- /dev/null +++ b/.api-reports/api-report-masking.api.md @@ -0,0 +1,86 @@ +## API Report File for "@apollo/client" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { TypedDocumentNode } from '@graphql-typed-document-node/core'; + +// Warning: (ae-forgotten-export) The symbol "UnionToIntersection" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "UnwrapFragmentRefs" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "RemoveFragmentName" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type CombineFragmentRefs> = UnionToIntersection<{ + [K in keyof FragmentRefs]-?: UnwrapFragmentRefs>; +}[keyof FragmentRefs]>; + +// @public (undocumented) +interface DataMasking { +} + +// @public (undocumented) +export type FragmentType = [ +TData +] extends [{ + " $fragmentName"?: infer TKey; +}] ? TKey extends string ? { + " $fragmentRefs"?: { + [key in TKey]: TData; + }; +} : never : never; + +// @public +export type Masked = TData & { + __masked?: true; +}; + +// @public +export type MaskedDocumentNode = TypedDocumentNode, TVariables>; + +// Warning: (ae-forgotten-export) The symbol "Prettify" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "RemoveMaskedMarker" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "DataMasking" needs to be exported by the entry point index.d.ts +// +// @public +export type MaybeMasked = TData extends { + __masked?: true; +} ? Prettify> : DataMasking extends { + enabled: true; +} ? TData : Unmasked; + +// @public (undocumented) +type Prettify = { + [K in keyof T]: T[K]; +} & {}; + +// @public (undocumented) +type RemoveFragmentName = T extends any ? Omit : T; + +// @public (undocumented) +type RemoveMaskedMarker = Omit; + +// @public (undocumented) +type UnionToIntersection = (U extends any ? (x: U) => void : never) extends (x: infer I) => void ? I : never; + +// @public +export type Unmasked = TData extends object ? UnwrapFragmentRefs>> : TData; + +// Warning: (ae-forgotten-export) The symbol "CombineFragmentRefs" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type UnwrapFragmentRefs = string extends keyof NonNullable ? TData : " $fragmentRefs" extends keyof NonNullable ? TData extends { + " $fragmentRefs"?: infer FragmentRefs extends object; +} ? Prettify<{ + [K in keyof TData as K extends " $fragmentRefs" ? never : K]: UnwrapFragmentRefs; +} & CombineFragmentRefs> : never : TData extends object ? { + [K in keyof TData]: UnwrapFragmentRefs; +} : TData; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/.api-reports/api-report-react.api.md b/.api-reports/api-report-react.api.md index dabdf2dbd1d..818587e9537 100644 --- a/.api-reports/api-report-react.api.md +++ b/.api-reports/api-report-react.api.md @@ -11,6 +11,7 @@ import type { FormattedExecutionResult } from 'graphql'; import type { FragmentDefinitionNode } from 'graphql'; import type { GraphQLErrorExtensions } from 'graphql'; import type { GraphQLFormattedError } from 'graphql'; +import type { InlineFragmentNode } from 'graphql'; import { Observable } from 'zen-observable-ts'; import type { Observer } from 'zen-observable-ts'; import type * as ReactTypes from 'react'; @@ -40,6 +41,8 @@ abstract class ApolloCache implements DataProxy { abstract evict(options: Cache_2.EvictOptions): boolean; abstract extract(optimistic?: boolean): TSerialized; // (undocumented) + fragmentMatches?(fragment: InlineFragmentNode, typename: string): boolean; + // (undocumented) gc(): string[]; // Warning: (ae-forgotten-export) The symbol "getApolloCacheMemoryInternals" needs to be exported by the entry point index.d.ts // @@ -50,19 +53,22 @@ abstract class ApolloCache implements DataProxy { // (undocumented) identify(object: StoreObject | Reference): string | undefined; // (undocumented) + lookupFragment(fragmentName: string): FragmentDefinitionNode | null; + // (undocumented) modify = Record>(options: Cache_2.ModifyOptions): boolean; // Warning: (ae-forgotten-export) The symbol "Transaction" needs to be exported by the entry point index.d.ts // // (undocumented) abstract performTransaction(transaction: Transaction, optimisticId?: string | null): void; // Warning: (ae-forgotten-export) The symbol "Cache_2" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "Unmasked" needs to be exported by the entry point index.d.ts // // (undocumented) - abstract read(query: Cache_2.ReadOptions): TData | null; + abstract read(query: Cache_2.ReadOptions): Unmasked | null; // (undocumented) - readFragment(options: Cache_2.ReadFragmentOptions, optimistic?: boolean): FragmentType | null; + readFragment(options: Cache_2.ReadFragmentOptions, optimistic?: boolean): Unmasked | null; // (undocumented) - readQuery(options: Cache_2.ReadQueryOptions, optimistic?: boolean): QueryType | null; + readQuery(options: Cache_2.ReadQueryOptions, optimistic?: boolean): Unmasked | null; // (undocumented) recordOptimisticTransaction(transaction: Transaction, optimisticId: string): void; // (undocumented) @@ -75,9 +81,9 @@ abstract class ApolloCache implements DataProxy { // (undocumented) transformForLink(document: DocumentNode): DocumentNode; // (undocumented) - updateFragment(options: Cache_2.UpdateFragmentOptions, update: (data: TData | null) => TData | null | void): TData | null; + updateFragment(options: Cache_2.UpdateFragmentOptions, update: (data: Unmasked | null) => Unmasked | null | void): Unmasked | null; // (undocumented) - updateQuery(options: Cache_2.UpdateQueryOptions, update: (data: TData | null) => TData | null | void): TData | null; + updateQuery(options: Cache_2.UpdateQueryOptions, update: (data: Unmasked | null) => Unmasked | null | void): Unmasked | null; // (undocumented) abstract watch(watch: Cache_2.WatchOptions): () => void; // Warning: (ae-forgotten-export) The symbol "OperationVariables" needs to be exported by the entry point index.d.ts @@ -134,16 +140,17 @@ class ApolloClient implements DataProxy { link: ApolloLink; // Warning: (ae-forgotten-export) The symbol "MutationOptions" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "FetchResult" needs to be exported by the entry point index.d.ts - mutate = Context, TCache extends ApolloCache = ApolloCache>(options: MutationOptions): Promise>; + mutate = Context, TCache extends ApolloCache = ApolloCache>(options: MutationOptions): Promise>>; onClearStore(cb: () => Promise): () => void; onResetStore(cb: () => Promise): () => void; // Warning: (ae-forgotten-export) The symbol "QueryOptions" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "ApolloQueryResult" needs to be exported by the entry point index.d.ts - query(options: QueryOptions): Promise>; + // Warning: (ae-forgotten-export) The symbol "MaybeMasked" needs to be exported by the entry point index.d.ts + query(options: QueryOptions): Promise>>; // (undocumented) queryDeduplication: boolean; - readFragment(options: DataProxy.Fragment, optimistic?: boolean): T | null; - readQuery(options: DataProxy.Query, optimistic?: boolean): T | null; + readFragment(options: DataProxy.Fragment, optimistic?: boolean): Unmasked | null; + readQuery(options: DataProxy.Query, optimistic?: boolean): Unmasked | null; reFetchObservableQueries(includeStandby?: boolean): Promise[]>; // Warning: (ae-forgotten-export) The symbol "RefetchQueriesOptions" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "RefetchQueriesResult" needs to be exported by the entry point index.d.ts @@ -156,7 +163,7 @@ class ApolloClient implements DataProxy { setResolvers(resolvers: Resolvers | Resolvers[]): void; stop(): void; // Warning: (ae-forgotten-export) The symbol "SubscriptionOptions" needs to be exported by the entry point index.d.ts - subscribe(options: SubscriptionOptions): Observable>; + subscribe(options: SubscriptionOptions): Observable>>; // Warning: (ae-forgotten-export) The symbol "ApolloClientOptions" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -179,6 +186,7 @@ interface ApolloClientOptions { connectToDevTools?: boolean; // (undocumented) credentials?: string; + dataMasking?: boolean; // (undocumented) defaultContext?: Partial; defaultOptions?: DefaultOptions; @@ -373,7 +381,7 @@ export interface BaseMutationOptions; ignoreResults?: boolean; notifyOnNetworkStatusChange?: boolean; - onCompleted?: (data: TData, clientOptions?: BaseMutationOptions) => void; + onCompleted?: (data: MaybeMasked, clientOptions?: BaseMutationOptions) => void; onError?: (error: ApolloError, clientOptions?: BaseMutationOptions) => void; } @@ -493,7 +501,7 @@ namespace Cache_2 { // (undocumented) dataId?: string; // (undocumented) - result: TResult; + result: Unmasked; } import DiffResult = DataProxy.DiffResult; import ReadQueryOptions = DataProxy.ReadQueryOptions; @@ -520,6 +528,15 @@ const enum CacheWriteBehavior { // @public (undocumented) type CanReadFunction = (value: StoreValue) => boolean; +// Warning: (ae-forgotten-export) The symbol "UnionToIntersection" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "UnwrapFragmentRefs" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "RemoveFragmentName" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type CombineFragmentRefs> = UnionToIntersection<{ + [K in keyof FragmentRefs]-?: UnwrapFragmentRefs>; +}[keyof FragmentRefs]>; + // @public (undocumented) export type CommonOptions = TOptions & { client?: ApolloClient; @@ -556,6 +573,10 @@ export interface Context extends Record { // @public export function createQueryPreloader(client: ApolloClient): PreloadQueryFunction; +// @public (undocumented) +interface DataMasking { +} + // @public (undocumented) namespace DataProxy { // (undocumented) @@ -614,7 +635,7 @@ namespace DataProxy { // (undocumented) interface WriteOptions { broadcast?: boolean; - data: TData; + data: Unmasked; overwrite?: boolean; } // Warning: (ae-forgotten-export) The symbol "DataProxy" needs to be exported by the entry point index.d.ts @@ -626,8 +647,8 @@ namespace DataProxy { // @public interface DataProxy { - readFragment(options: DataProxy.ReadFragmentOptions, optimistic?: boolean): FragmentType | null; - readQuery(options: DataProxy.ReadQueryOptions, optimistic?: boolean): QueryType | null; + readFragment(options: DataProxy.ReadFragmentOptions, optimistic?: boolean): Unmasked | null; + readQuery(options: DataProxy.ReadQueryOptions, optimistic?: boolean): Unmasked | null; writeFragment(options: DataProxy.WriteFragmentOptions): Reference | undefined; writeQuery(options: DataProxy.WriteQueryOptions): Reference | undefined; } @@ -782,11 +803,11 @@ interface ExecutionPatchResultBase { // // @public (undocumented) type FetchMoreFunction = (fetchMoreOptions: FetchMoreQueryOptions & { - updateQuery?: (previousQueryResult: TData, options: { - fetchMoreResult: TData; + updateQuery?: (previousQueryResult: Unmasked, options: { + fetchMoreResult: Unmasked; variables: TVariables; - }) => TData; -}) => Promise>; + }) => Unmasked; +}) => Promise>>; // @public (undocumented) interface FetchMoreQueryOptions { @@ -828,6 +849,17 @@ interface FragmentMap { // @public (undocumented) type FragmentMatcher = (rootValue: any, typeCondition: string, context: any) => boolean; +// @public (undocumented) +type FragmentType = [ +TData +] extends [{ + " $fragmentName"?: infer TKey; +}] ? TKey extends string ? { + " $fragmentRefs"?: { + [key in TKey]: TData; + }; +} : never : never; + // @internal const getApolloCacheMemoryInternals: (() => { cache: { @@ -963,11 +995,11 @@ const _invalidateModifier: unique symbol; // @public (undocumented) function isReference(obj: any): obj is Reference; -// Warning: (ae-forgotten-export) The symbol "UnionToIntersection" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "UnionToIntersection_2" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "UnionForAny" needs to be exported by the entry point index.d.ts // // @public (undocumented) -type IsStrictlyAny = UnionToIntersection> extends never ? true : false; +type IsStrictlyAny = UnionToIntersection_2> extends never ? true : false; // @public (undocumented) export type LazyQueryExecFunction = (options?: Partial>) => Promise>; @@ -982,7 +1014,7 @@ export interface LazyQueryHookExecOptions extends BaseQueryOptions { // @internal (undocumented) defaultOptions?: Partial>; - onCompleted?: (data: TData) => void; + onCompleted?: (data: MaybeMasked) => void; onError?: (error: ApolloError) => void; } @@ -1062,9 +1094,38 @@ type LocalStateOptions = { fragmentMatcher?: FragmentMatcher; }; +// @public (undocumented) +interface MaskFragmentOptions { + // (undocumented) + data: TData; + // (undocumented) + fragment: DocumentNode; + // (undocumented) + fragmentName?: string; +} + +// @public (undocumented) +interface MaskOperationOptions { + // (undocumented) + data: TData; + // (undocumented) + document: DocumentNode; +} + // @public (undocumented) type MaybeAsync = T | PromiseLike; +// Warning: (ae-forgotten-export) The symbol "Prettify" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "RemoveMaskedMarker" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "DataMasking" needs to be exported by the entry point index.d.ts +// +// @public +type MaybeMasked = TData extends { + __masked?: true; +} ? Prettify> : DataMasking extends { + enabled: true; +} ? TData : Unmasked; + // @public (undocumented) class MissingFieldError extends Error { constructor(message: string, path: MissingTree | Array, query: DocumentNode, variables?: Record | undefined); @@ -1120,10 +1181,10 @@ interface MutationBaseOptions; // Warning: (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts - optimisticResponse?: TData | ((vars: TVariables, { IGNORE }: { + optimisticResponse?: Unmasked> | ((vars: TVariables, { IGNORE }: { IGNORE: IgnoreModifier; - }) => TData | IgnoreModifier); - refetchQueries?: ((result: FetchResult) => InternalRefetchQueriesInclude) | InternalRefetchQueriesInclude; + }) => Unmasked> | IgnoreModifier); + refetchQueries?: ((result: FetchResult>) => InternalRefetchQueriesInclude) | InternalRefetchQueriesInclude; // Warning: (ae-forgotten-export) The symbol "MutationUpdaterFunction" needs to be exported by the entry point index.d.ts update?: MutationUpdaterFunction; // Warning: (ae-forgotten-export) The symbol "MutationQueryReducersMap" needs to be exported by the entry point index.d.ts @@ -1141,7 +1202,7 @@ export interface MutationDataOptions; // @public (undocumented) -export type MutationFunction = ApolloCache> = (options?: MutationFunctionOptions) => Promise>; +export type MutationFunction = ApolloCache> = (options?: MutationFunctionOptions) => Promise>>; // @public (undocumented) export interface MutationFunctionOptions = ApolloCache> extends BaseMutationOptions { @@ -1159,7 +1220,7 @@ interface MutationOptions = (previousResult: Record, options: { - mutationResult: FetchResult; + mutationResult: FetchResult>; queryName: string | undefined; queryVariables: Record; }) => Record; @@ -1175,7 +1236,7 @@ type MutationQueryReducersMap { called: boolean; client: ApolloClient; - data?: TData | null; + data?: MaybeMasked | null; error?: ApolloError; loading: boolean; reset: () => void; @@ -1204,12 +1265,12 @@ interface MutationStoreValue { // @public (undocumented) export type MutationTuple = ApolloCache> = [ -mutate: (options?: MutationFunctionOptions) => Promise>, +mutate: (options?: MutationFunctionOptions) => Promise>>, result: MutationResult ]; // @public (undocumented) -type MutationUpdaterFunction> = (cache: TCache, result: Omit, "context">, options: { +type MutationUpdaterFunction> = (cache: TCache, result: Omit>, "context">, options: { context?: TContext; variables?: TVariables; }) => void; @@ -1248,20 +1309,20 @@ type NoInfer_2 = [T][T extends any ? 0 : never]; export { NoInfer_2 as NoInfer } // @public (undocumented) -class ObservableQuery extends Observable> { +class ObservableQuery extends Observable>> { constructor({ queryManager, queryInfo, options, }: { queryManager: QueryManager; queryInfo: QueryInfo; options: WatchQueryOptions; }); fetchMore(fetchMoreOptions: FetchMoreQueryOptions & { - updateQuery?: (previousQueryResult: TData, options: { - fetchMoreResult: TFetchData; + updateQuery?: (previousQueryResult: Unmasked, options: { + fetchMoreResult: Unmasked; variables: TFetchVars; - }) => TData; - }): Promise>; + }) => Unmasked; + }): Promise>>; // (undocumented) - getCurrentResult(saveAsLastResult?: boolean): ApolloQueryResult; + getCurrentResult(saveAsLastResult?: boolean): ApolloQueryResult>; // (undocumented) getLastError(variablesMustMatch?: boolean): ApolloError | undefined; // (undocumented) @@ -1278,9 +1339,9 @@ class ObservableQuery): Promise>; + refetch(variables?: Partial): Promise>>; // (undocumented) - reobserve(newOptions?: Partial>, newNetworkStatus?: NetworkStatus): Promise>; + reobserve(newOptions?: Partial>, newNetworkStatus?: NetworkStatus): Promise>>; // Warning: (ae-forgotten-export) The symbol "Concast" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1292,39 +1353,39 @@ class ObservableQuery) => void, onError?: (error: any) => void, onComplete?: () => void): Subscription; + resubscribeAfterError(onNext: (value: ApolloQueryResult>) => void, onError?: (error: any) => void, onComplete?: () => void): Subscription; // (undocumented) resubscribeAfterError(observer: Observer>): Subscription; // (undocumented) - result(): Promise>; + result(): Promise>>; // (undocumented) - setOptions(newOptions: Partial>): Promise>; - setVariables(variables: TVariables): Promise | void>; + setOptions(newOptions: Partial>): Promise>>; + setVariables(variables: TVariables): Promise> | void>; // (undocumented) silentSetOptions(newOptions: Partial>): void; startPolling(pollInterval: number): void; stopPolling(): void; // Warning: (ae-forgotten-export) The symbol "SubscribeToMoreOptions" needs to be exported by the entry point index.d.ts subscribeToMore(options: SubscribeToMoreOptions): () => void; - updateQuery(mapFn: (previousQueryResult: TData, options: Pick, "variables">) => TData): void; + updateQuery(mapFn: (previousQueryResult: Unmasked, options: Pick, "variables">) => Unmasked): void; get variables(): TVariables | undefined; } // @public (undocumented) export interface ObservableQueryFields { fetchMore: (fetchMoreOptions: FetchMoreQueryOptions & { - updateQuery?: (previousQueryResult: TData, options: { - fetchMoreResult: TFetchData; + updateQuery?: (previousQueryResult: Unmasked, options: { + fetchMoreResult: Unmasked; variables: TFetchVars; - }) => TData; - }) => Promise>; - refetch: (variables?: Partial) => Promise>; + }) => Unmasked; + }) => Promise>>; + refetch: (variables?: Partial) => Promise>>; // @internal (undocumented) - reobserve: (newOptions?: Partial>, newNetworkStatus?: NetworkStatus) => Promise>; + reobserve: (newOptions?: Partial>, newNetworkStatus?: NetworkStatus) => Promise>>; startPolling: (pollInterval: number) => void; stopPolling: () => void; subscribeToMore: (options: SubscribeToMoreOptions) => () => void; - updateQuery: (mapFn: (previousQueryResult: TData, options: Pick, "variables">) => TData) => void; + updateQuery: (mapFn: (previousQueryResult: Unmasked, options: Pick, "variables">) => Unmasked) => void; variables: TVariables | undefined; } @@ -1435,6 +1496,11 @@ options?: PreloadQueryOptions> & Omit> & Omit ]; +// @public (undocumented) +type Prettify = { + [K in keyof T]: T[K]; +} & {}; + // @public (undocumented) type Primitive = null | undefined | string | number | boolean | symbol | bigint; @@ -1460,7 +1526,7 @@ export interface QueryDataOptions extends BaseQueryOptions { // @internal (undocumented) defaultOptions?: Partial>; - onCompleted?: (data: TData) => void; + onCompleted?: (data: MaybeMasked) => void; onError?: (error: ApolloError) => void; skip?: boolean; } @@ -1550,6 +1616,8 @@ class QueryManager { // (undocumented) clearStore(options?: Cache_2.ResetOptions): Promise; // (undocumented) + readonly dataMasking: boolean; + // (undocumented) readonly defaultContext: Partial; // Warning: (ae-forgotten-export) The symbol "DefaultOptions" needs to be exported by the entry point index.d.ts // @@ -1616,14 +1684,22 @@ class QueryManager { onQueryUpdated?: OnQueryUpdated; keepRootFields?: boolean; }, cache?: ApolloCache): Promise>; + // Warning: (ae-forgotten-export) The symbol "MaskFragmentOptions" needs to be exported by the entry point index.d.ts + // // (undocumented) - mutate, TCache extends ApolloCache>({ mutation, variables, optimisticResponse, updateQueries, refetchQueries, awaitRefetchQueries, update: updateWithProxyFn, onQueryUpdated, fetchPolicy, errorPolicy, keepRootFields, context, }: MutationOptions): Promise>; + maskFragment(options: MaskFragmentOptions): TData; + // Warning: (ae-forgotten-export) The symbol "MaskOperationOptions" needs to be exported by the entry point index.d.ts + // + // (undocumented) + maskOperation(options: MaskOperationOptions): MaybeMasked; + // (undocumented) + mutate, TCache extends ApolloCache>({ mutation, variables, optimisticResponse, updateQueries, refetchQueries, awaitRefetchQueries, update: updateWithProxyFn, onQueryUpdated, fetchPolicy, errorPolicy, keepRootFields, context, }: MutationOptions): Promise>>; // (undocumented) mutationStore?: { [mutationId: string]: MutationStoreValue; }; // (undocumented) - query(options: QueryOptions, queryId?: string): Promise>; + query(options: QueryOptions, queryId?: string): Promise>>; // (undocumented) reFetchObservableQueries(includeStandby?: boolean): Promise[]>; // Warning: (ae-forgotten-export) The symbol "InternalRefetchQueriesOptions" needs to be exported by the entry point index.d.ts @@ -1640,7 +1716,7 @@ class QueryManager { // (undocumented) readonly ssrMode: boolean; // (undocumented) - startGraphQLSubscription({ query, fetchPolicy, errorPolicy, variables, context, extensions, }: SubscriptionOptions): Observable>; + startGraphQLSubscription(options: SubscriptionOptions): Observable>; stop(): void; // (undocumented) stopQuery(queryId: string): void; @@ -1661,6 +1737,8 @@ interface QueryManagerOptions { // (undocumented) clientAwareness: Record; // (undocumented) + dataMasking: boolean; + // (undocumented) defaultContext: Partial | undefined; // (undocumented) defaultOptions: DefaultOptions; @@ -1710,14 +1788,14 @@ export interface QueryReference extends Q export interface QueryResult extends ObservableQueryFields { called: boolean; client: ApolloClient; - data: TData | undefined; + data: MaybeMasked | undefined; error?: ApolloError; // @deprecated (undocumented) errors?: ReadonlyArray; loading: boolean; networkStatus: NetworkStatus; observable: ObservableQuery; - previousData?: TData; + previousData?: MaybeMasked; } // @public (undocumented) @@ -1813,6 +1891,12 @@ type RefetchQueryDescriptor = string | DocumentNode; // @public (undocumented) type RefetchWritePolicy = "merge" | "overwrite"; +// @public (undocumented) +type RemoveFragmentName = T extends any ? Omit : T; + +// @public (undocumented) +type RemoveMaskedMarker = Omit; + // @public (undocumented) class RenderPromises { // (undocumented) @@ -1979,7 +2063,7 @@ interface SubscriptionOptions { // @public (undocumented) export interface SubscriptionResult { - data?: TData; + data?: MaybeMasked; error?: ApolloError; loading: boolean; // @internal (undocumented) @@ -2026,6 +2110,8 @@ interface TransformCacheEntry { // (undocumented) hasNonreactiveDirective: boolean; // (undocumented) + nonReactiveQuery: DocumentNode; + // (undocumented) serverQuery: DocumentNode | null; } @@ -2036,18 +2122,35 @@ type TransformFn = (document: DocumentNode) => DocumentNode; type UnionForAny = T extends never ? "a" : 1; // @public (undocumented) -type UnionToIntersection = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never; +type UnionToIntersection = (U extends any ? (x: U) => void : never) extends (x: infer I) => void ? I : never; + +// @public (undocumented) +type UnionToIntersection_2 = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never; + +// @public +type Unmasked = TData extends object ? UnwrapFragmentRefs>> : TData; + +// Warning: (ae-forgotten-export) The symbol "CombineFragmentRefs" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type UnwrapFragmentRefs = string extends keyof NonNullable ? TData : " $fragmentRefs" extends keyof NonNullable ? TData extends { + " $fragmentRefs"?: infer FragmentRefs extends object; +} ? Prettify<{ + [K in keyof TData as K extends " $fragmentRefs" ? never : K]: UnwrapFragmentRefs; +} & CombineFragmentRefs> : never : TData extends object ? { + [K in keyof TData]: UnwrapFragmentRefs; +} : TData; // @public (undocumented) type UpdateQueries = MutationOptions["updateQueries"]; // @public (undocumented) -type UpdateQueryFn = (previousQueryResult: TData, options: { +type UpdateQueryFn = (previousQueryResult: Unmasked, options: { subscriptionData: { - data: TSubscriptionData; + data: Unmasked; }; variables?: TSubscriptionVariables; -}) => TData; +}) => Unmasked; // @public (undocumented) interface UriFunction { @@ -2141,19 +2244,21 @@ export function useFragment(options: Us // @public (undocumented) export interface UseFragmentOptions extends Omit, NoInfer_2>, "id" | "query" | "optimistic" | "previousResult" | "returnPartialData">, Omit, "id" | "variables" | "returnPartialData"> { client?: ApolloClient; + // Warning: (ae-forgotten-export) The symbol "FragmentType" needs to be exported by the entry point index.d.ts + // // (undocumented) - from: StoreObject | Reference | string; + from: StoreObject | Reference | FragmentType> | string; // (undocumented) optimistic?: boolean; } // @public (undocumented) export type UseFragmentResult = { - data: TData; + data: MaybeMasked; complete: true; missing?: never; } | { - data: DeepPartial; + data: DeepPartial>; complete: false; missing?: MissingTree; }; @@ -2221,7 +2326,7 @@ export function useReadQuery(queryRef: QueryRef): UseReadQueryResu // @public (undocumented) export interface UseReadQueryResult { - data: TData; + data: MaybeMasked; error: ApolloError | undefined; networkStatus: NetworkStatus; } @@ -2230,7 +2335,7 @@ export interface UseReadQueryResult { export function useSubscription(subscription: DocumentNode | TypedDocumentNode, options?: SubscriptionHookOptions, NoInfer_2>): { restart: () => void; loading: boolean; - data?: TData | undefined; + data?: MaybeMasked | undefined; error?: ApolloError; variables?: TVariables | undefined; }; @@ -2281,7 +2386,7 @@ export interface UseSuspenseQueryResult; // (undocumented) - data: TData; + data: MaybeMasked; // (undocumented) error: ApolloError | undefined; // (undocumented) @@ -2309,18 +2414,18 @@ TVariables interface WatchFragmentOptions { fragment: DocumentNode | TypedDocumentNode; fragmentName?: string; - from: StoreObject | Reference | string; + from: StoreObject | Reference | FragmentType> | string; optimistic?: boolean; variables?: TVars; } // @public type WatchFragmentResult = { - data: TData; + data: MaybeMasked; complete: true; missing?: never; } | { - data: DeepPartial; + data: DeepPartial>; complete: false; missing: MissingTree; }; @@ -2335,20 +2440,20 @@ interface WatchQueryOptions implements DataProxy { abstract evict(options: Cache_2.EvictOptions): boolean; abstract extract(optimistic?: boolean): TSerialized; // (undocumented) + fragmentMatches?(fragment: InlineFragmentNode, typename: string): boolean; + // (undocumented) gc(): string[]; // Warning: (ae-forgotten-export) The symbol "getApolloCacheMemoryInternals" needs to be exported by the entry point index.d.ts // @@ -50,19 +53,22 @@ abstract class ApolloCache implements DataProxy { // (undocumented) identify(object: StoreObject | Reference): string | undefined; // (undocumented) + lookupFragment(fragmentName: string): FragmentDefinitionNode | null; + // (undocumented) modify = Record>(options: Cache_2.ModifyOptions): boolean; // Warning: (ae-forgotten-export) The symbol "Transaction" needs to be exported by the entry point index.d.ts // // (undocumented) abstract performTransaction(transaction: Transaction, optimisticId?: string | null): void; // Warning: (ae-forgotten-export) The symbol "Cache_2" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "Unmasked" needs to be exported by the entry point index.d.ts // // (undocumented) - abstract read(query: Cache_2.ReadOptions): TData | null; + abstract read(query: Cache_2.ReadOptions): Unmasked | null; // (undocumented) - readFragment(options: Cache_2.ReadFragmentOptions, optimistic?: boolean): FragmentType | null; + readFragment(options: Cache_2.ReadFragmentOptions, optimistic?: boolean): Unmasked | null; // (undocumented) - readQuery(options: Cache_2.ReadQueryOptions, optimistic?: boolean): QueryType | null; + readQuery(options: Cache_2.ReadQueryOptions, optimistic?: boolean): Unmasked | null; // (undocumented) recordOptimisticTransaction(transaction: Transaction, optimisticId: string): void; // (undocumented) @@ -75,9 +81,9 @@ abstract class ApolloCache implements DataProxy { // (undocumented) transformForLink(document: DocumentNode): DocumentNode; // (undocumented) - updateFragment(options: Cache_2.UpdateFragmentOptions, update: (data: TData | null) => TData | null | void): TData | null; + updateFragment(options: Cache_2.UpdateFragmentOptions, update: (data: Unmasked | null) => Unmasked | null | void): Unmasked | null; // (undocumented) - updateQuery(options: Cache_2.UpdateQueryOptions, update: (data: TData | null) => TData | null | void): TData | null; + updateQuery(options: Cache_2.UpdateQueryOptions, update: (data: Unmasked | null) => Unmasked | null | void): Unmasked | null; // (undocumented) abstract watch(watch: Cache_2.WatchOptions): () => void; // Warning: (ae-forgotten-export) The symbol "OperationVariables" needs to be exported by the entry point index.d.ts @@ -135,16 +141,17 @@ class ApolloClient implements DataProxy { // Warning: (ae-forgotten-export) The symbol "DefaultContext" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "MutationOptions" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "FetchResult" needs to be exported by the entry point index.d.ts - mutate = DefaultContext, TCache extends ApolloCache = ApolloCache>(options: MutationOptions): Promise>; + mutate = DefaultContext, TCache extends ApolloCache = ApolloCache>(options: MutationOptions): Promise>>; onClearStore(cb: () => Promise): () => void; onResetStore(cb: () => Promise): () => void; // Warning: (ae-forgotten-export) The symbol "QueryOptions" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "ApolloQueryResult" needs to be exported by the entry point index.d.ts - query(options: QueryOptions): Promise>; + // Warning: (ae-forgotten-export) The symbol "MaybeMasked" needs to be exported by the entry point index.d.ts + query(options: QueryOptions): Promise>>; // (undocumented) queryDeduplication: boolean; - readFragment(options: DataProxy.Fragment, optimistic?: boolean): T | null; - readQuery(options: DataProxy.Query, optimistic?: boolean): T | null; + readFragment(options: DataProxy.Fragment, optimistic?: boolean): Unmasked | null; + readQuery(options: DataProxy.Query, optimistic?: boolean): Unmasked | null; reFetchObservableQueries(includeStandby?: boolean): Promise[]>; // Warning: (ae-forgotten-export) The symbol "RefetchQueriesOptions" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "RefetchQueriesResult" needs to be exported by the entry point index.d.ts @@ -157,7 +164,7 @@ class ApolloClient implements DataProxy { setResolvers(resolvers: Resolvers | Resolvers[]): void; stop(): void; // Warning: (ae-forgotten-export) The symbol "SubscriptionOptions" needs to be exported by the entry point index.d.ts - subscribe(options: SubscriptionOptions): Observable>; + subscribe(options: SubscriptionOptions): Observable>>; // Warning: (ae-forgotten-export) The symbol "ApolloClientOptions" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -180,6 +187,7 @@ interface ApolloClientOptions { connectToDevTools?: boolean; // (undocumented) credentials?: string; + dataMasking?: boolean; // (undocumented) defaultContext?: Partial; defaultOptions?: DefaultOptions; @@ -321,7 +329,7 @@ interface BaseMutationOptions; ignoreResults?: boolean; notifyOnNetworkStatusChange?: boolean; - onCompleted?: (data: TData, clientOptions?: BaseMutationOptions) => void; + onCompleted?: (data: MaybeMasked, clientOptions?: BaseMutationOptions) => void; onError?: (error: ApolloError, clientOptions?: BaseMutationOptions) => void; } @@ -444,7 +452,7 @@ namespace Cache_2 { // (undocumented) dataId?: string; // (undocumented) - result: TResult; + result: Unmasked; } import DiffResult = DataProxy.DiffResult; import ReadQueryOptions = DataProxy.ReadQueryOptions; @@ -471,6 +479,15 @@ const enum CacheWriteBehavior { // @public (undocumented) type CanReadFunction = (value: StoreValue) => boolean; +// Warning: (ae-forgotten-export) The symbol "UnionToIntersection" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "UnwrapFragmentRefs" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "RemoveFragmentName" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type CombineFragmentRefs> = UnionToIntersection<{ + [K in keyof FragmentRefs]-?: UnwrapFragmentRefs>; +}[keyof FragmentRefs]>; + // @public (undocumented) class Concast extends Observable { // Warning: (ae-forgotten-export) The symbol "MaybeAsync" needs to be exported by the entry point index.d.ts @@ -495,6 +512,10 @@ class Concast extends Observable { // @public (undocumented) type ConcastSourcesIterable = Iterable>; +// @public (undocumented) +interface DataMasking { +} + // @public (undocumented) namespace DataProxy { // (undocumented) @@ -553,7 +574,7 @@ namespace DataProxy { // (undocumented) interface WriteOptions { broadcast?: boolean; - data: TData; + data: Unmasked; overwrite?: boolean; } // Warning: (ae-forgotten-export) The symbol "DataProxy" needs to be exported by the entry point index.d.ts @@ -565,8 +586,8 @@ namespace DataProxy { // @public interface DataProxy { - readFragment(options: DataProxy.ReadFragmentOptions, optimistic?: boolean): FragmentType | null; - readQuery(options: DataProxy.ReadQueryOptions, optimistic?: boolean): QueryType | null; + readFragment(options: DataProxy.ReadFragmentOptions, optimistic?: boolean): Unmasked | null; + readQuery(options: DataProxy.ReadQueryOptions, optimistic?: boolean): Unmasked | null; writeFragment(options: DataProxy.WriteFragmentOptions): Reference | undefined; writeQuery(options: DataProxy.WriteQueryOptions): Reference | undefined; } @@ -750,6 +771,17 @@ interface FragmentMap { // @public (undocumented) type FragmentMatcher = (rootValue: any, typeCondition: string, context: any) => boolean; +// @public (undocumented) +type FragmentType = [ +TData +] extends [{ + " $fragmentName"?: infer TKey; +}] ? TKey extends string ? { + " $fragmentRefs"?: { + [key in TKey]: TData; + }; +} : never : never; + // @internal const getApolloCacheMemoryInternals: (() => { cache: { @@ -872,11 +904,11 @@ const _invalidateModifier: unique symbol; // @public (undocumented) function isReference(obj: any): obj is Reference; -// Warning: (ae-forgotten-export) The symbol "UnionToIntersection" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "UnionToIntersection_2" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "UnionForAny" needs to be exported by the entry point index.d.ts // // @public (undocumented) -type IsStrictlyAny = UnionToIntersection> extends never ? true : false; +type IsStrictlyAny = UnionToIntersection_2> extends never ? true : false; // @public (undocumented) class LocalState { @@ -923,9 +955,38 @@ type LocalStateOptions = { fragmentMatcher?: FragmentMatcher; }; +// @public (undocumented) +interface MaskFragmentOptions { + // (undocumented) + data: TData; + // (undocumented) + fragment: DocumentNode; + // (undocumented) + fragmentName?: string; +} + +// @public (undocumented) +interface MaskOperationOptions { + // (undocumented) + data: TData; + // (undocumented) + document: DocumentNode; +} + // @public (undocumented) type MaybeAsync = T | PromiseLike; +// Warning: (ae-forgotten-export) The symbol "Prettify" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "RemoveMaskedMarker" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "DataMasking" needs to be exported by the entry point index.d.ts +// +// @public +type MaybeMasked = TData extends { + __masked?: true; +} ? Prettify> : DataMasking extends { + enabled: true; +} ? TData : Unmasked; + // @public (undocumented) class MissingFieldError extends Error { constructor(message: string, path: MissingTree | Array, query: DocumentNode, variables?: Record | undefined); @@ -995,11 +1056,12 @@ interface MutationBaseOptions; + // Warning: (ae-forgotten-export) The symbol "NoInfer_2" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts - optimisticResponse?: TData | ((vars: TVariables, { IGNORE }: { + optimisticResponse?: Unmasked> | ((vars: TVariables, { IGNORE }: { IGNORE: IgnoreModifier; - }) => TData | IgnoreModifier); - refetchQueries?: ((result: FetchResult) => InternalRefetchQueriesInclude) | InternalRefetchQueriesInclude; + }) => Unmasked> | IgnoreModifier); + refetchQueries?: ((result: FetchResult>) => InternalRefetchQueriesInclude) | InternalRefetchQueriesInclude; // Warning: (ae-forgotten-export) The symbol "MutationUpdaterFunction" needs to be exported by the entry point index.d.ts update?: MutationUpdaterFunction; // Warning: (ae-forgotten-export) The symbol "MutationQueryReducersMap" needs to be exported by the entry point index.d.ts @@ -1026,7 +1088,7 @@ type MutationFetchPolicy = Extract; // Warning: (ae-forgotten-export) The symbol "MutationFunctionOptions" needs to be exported by the entry point index.d.ts // // @public (undocumented) -type MutationFunction = ApolloCache> = (options?: MutationFunctionOptions) => Promise>; +type MutationFunction = ApolloCache> = (options?: MutationFunctionOptions) => Promise>>; // @public (undocumented) interface MutationFunctionOptions = ApolloCache> extends BaseMutationOptions { @@ -1040,7 +1102,7 @@ interface MutationOptions = (previousResult: Record, options: { - mutationResult: FetchResult; + mutationResult: FetchResult>; queryName: string | undefined; queryVariables: Record; }) => Record; @@ -1056,7 +1118,7 @@ type MutationQueryReducersMap { called: boolean; client: ApolloClient; - data?: TData | null; + data?: MaybeMasked | null; error?: ApolloError; loading: boolean; reset: () => void; @@ -1084,7 +1146,7 @@ interface MutationStoreValue { } // @public (undocumented) -type MutationUpdaterFunction> = (cache: TCache, result: Omit, "context">, options: { +type MutationUpdaterFunction> = (cache: TCache, result: Omit>, "context">, options: { context?: TContext; variables?: TVariables; }) => void; @@ -1120,8 +1182,11 @@ type NextLink = (operation: Operation) => Observable; // @public (undocumented) type NextResultListener = (method: "next" | "error" | "complete", arg?: any) => any; +// @public +type NoInfer_2 = [T][T extends any ? 0 : never]; + // @public (undocumented) -class ObservableQuery extends Observable> { +class ObservableQuery extends Observable>> { constructor({ queryManager, queryInfo, options, }: { queryManager: QueryManager; queryInfo: QueryInfo; @@ -1129,13 +1194,13 @@ class ObservableQuery(fetchMoreOptions: FetchMoreQueryOptions & { - updateQuery?: (previousQueryResult: TData, options: { - fetchMoreResult: TFetchData; + updateQuery?: (previousQueryResult: Unmasked, options: { + fetchMoreResult: Unmasked; variables: TFetchVars; - }) => TData; - }): Promise>; + }) => Unmasked; + }): Promise>>; // (undocumented) - getCurrentResult(saveAsLastResult?: boolean): ApolloQueryResult; + getCurrentResult(saveAsLastResult?: boolean): ApolloQueryResult>; // (undocumented) getLastError(variablesMustMatch?: boolean): ApolloError | undefined; // (undocumented) @@ -1152,9 +1217,9 @@ class ObservableQuery): Promise>; + refetch(variables?: Partial): Promise>>; // (undocumented) - reobserve(newOptions?: Partial>, newNetworkStatus?: NetworkStatus): Promise>; + reobserve(newOptions?: Partial>, newNetworkStatus?: NetworkStatus): Promise>>; // Warning: (ae-forgotten-export) The symbol "Concast" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1166,39 +1231,39 @@ class ObservableQuery) => void, onError?: (error: any) => void, onComplete?: () => void): Subscription_2; + resubscribeAfterError(onNext: (value: ApolloQueryResult>) => void, onError?: (error: any) => void, onComplete?: () => void): Subscription_2; // (undocumented) resubscribeAfterError(observer: Observer>): Subscription_2; // (undocumented) - result(): Promise>; + result(): Promise>>; // (undocumented) - setOptions(newOptions: Partial>): Promise>; - setVariables(variables: TVariables): Promise | void>; + setOptions(newOptions: Partial>): Promise>>; + setVariables(variables: TVariables): Promise> | void>; // (undocumented) silentSetOptions(newOptions: Partial>): void; startPolling(pollInterval: number): void; stopPolling(): void; // Warning: (ae-forgotten-export) The symbol "SubscribeToMoreOptions" needs to be exported by the entry point index.d.ts subscribeToMore(options: SubscribeToMoreOptions): () => void; - updateQuery(mapFn: (previousQueryResult: TData, options: Pick, "variables">) => TData): void; + updateQuery(mapFn: (previousQueryResult: Unmasked, options: Pick, "variables">) => Unmasked): void; get variables(): TVariables | undefined; } // @public (undocumented) interface ObservableQueryFields { fetchMore: (fetchMoreOptions: FetchMoreQueryOptions & { - updateQuery?: (previousQueryResult: TData, options: { - fetchMoreResult: TFetchData; + updateQuery?: (previousQueryResult: Unmasked, options: { + fetchMoreResult: Unmasked; variables: TFetchVars; - }) => TData; - }) => Promise>; - refetch: (variables?: Partial) => Promise>; + }) => Unmasked; + }) => Promise>>; + refetch: (variables?: Partial) => Promise>>; // @internal (undocumented) - reobserve: (newOptions?: Partial>, newNetworkStatus?: NetworkStatus) => Promise>; + reobserve: (newOptions?: Partial>, newNetworkStatus?: NetworkStatus) => Promise>>; startPolling: (pollInterval: number) => void; stopPolling: () => void; subscribeToMore: (options: SubscribeToMoreOptions) => () => void; - updateQuery: (mapFn: (previousQueryResult: TData, options: Pick, "variables">) => TData) => void; + updateQuery: (mapFn: (previousQueryResult: Unmasked, options: Pick, "variables">) => Unmasked) => void; variables: TVariables | undefined; } @@ -1248,6 +1313,11 @@ type OperationVariables = Record; // @public (undocumented) type Path = ReadonlyArray; +// @public (undocumented) +type Prettify = { + [K in keyof T]: T[K]; +} & {}; + // @public (undocumented) type Primitive = null | undefined | string | number | boolean | symbol | bigint; @@ -1284,7 +1354,7 @@ export interface QueryComponentOptions extends BaseQueryOptions { // @internal (undocumented) defaultOptions?: Partial>; - onCompleted?: (data: TData) => void; + onCompleted?: (data: MaybeMasked) => void; onError?: (error: ApolloError) => void; skip?: boolean; } @@ -1364,6 +1434,8 @@ class QueryManager { // (undocumented) clearStore(options?: Cache_2.ResetOptions): Promise; // (undocumented) + readonly dataMasking: boolean; + // (undocumented) readonly defaultContext: Partial; // Warning: (ae-forgotten-export) The symbol "DefaultOptions" needs to be exported by the entry point index.d.ts // @@ -1430,14 +1502,22 @@ class QueryManager { onQueryUpdated?: OnQueryUpdated; keepRootFields?: boolean; }, cache?: ApolloCache): Promise>; + // Warning: (ae-forgotten-export) The symbol "MaskFragmentOptions" needs to be exported by the entry point index.d.ts + // // (undocumented) - mutate, TCache extends ApolloCache>({ mutation, variables, optimisticResponse, updateQueries, refetchQueries, awaitRefetchQueries, update: updateWithProxyFn, onQueryUpdated, fetchPolicy, errorPolicy, keepRootFields, context, }: MutationOptions): Promise>; + maskFragment(options: MaskFragmentOptions): TData; + // Warning: (ae-forgotten-export) The symbol "MaskOperationOptions" needs to be exported by the entry point index.d.ts + // + // (undocumented) + maskOperation(options: MaskOperationOptions): MaybeMasked; + // (undocumented) + mutate, TCache extends ApolloCache>({ mutation, variables, optimisticResponse, updateQueries, refetchQueries, awaitRefetchQueries, update: updateWithProxyFn, onQueryUpdated, fetchPolicy, errorPolicy, keepRootFields, context, }: MutationOptions): Promise>>; // (undocumented) mutationStore?: { [mutationId: string]: MutationStoreValue; }; // (undocumented) - query(options: QueryOptions, queryId?: string): Promise>; + query(options: QueryOptions, queryId?: string): Promise>>; // (undocumented) reFetchObservableQueries(includeStandby?: boolean): Promise[]>; // Warning: (ae-forgotten-export) The symbol "InternalRefetchQueriesOptions" needs to be exported by the entry point index.d.ts @@ -1454,7 +1534,7 @@ class QueryManager { // (undocumented) readonly ssrMode: boolean; // (undocumented) - startGraphQLSubscription({ query, fetchPolicy, errorPolicy, variables, context, extensions, }: SubscriptionOptions): Observable>; + startGraphQLSubscription(options: SubscriptionOptions): Observable>; stop(): void; // (undocumented) stopQuery(queryId: string): void; @@ -1475,6 +1555,8 @@ interface QueryManagerOptions { // (undocumented) clientAwareness: Record; // (undocumented) + dataMasking: boolean; + // (undocumented) defaultContext: Partial | undefined; // (undocumented) defaultOptions: DefaultOptions; @@ -1514,14 +1596,14 @@ interface QueryOptions { interface QueryResult extends ObservableQueryFields { called: boolean; client: ApolloClient; - data: TData | undefined; + data: MaybeMasked | undefined; error?: ApolloError; // @deprecated (undocumented) errors?: ReadonlyArray; loading: boolean; networkStatus: NetworkStatus; observable: ObservableQuery; - previousData?: TData; + previousData?: MaybeMasked; } // @public (undocumented) @@ -1591,6 +1673,12 @@ type RefetchQueryDescriptor = string | DocumentNode; // @public (undocumented) type RefetchWritePolicy = "merge" | "overwrite"; +// @public (undocumented) +type RemoveFragmentName = T extends any ? Omit : T; + +// @public (undocumented) +type RemoveMaskedMarker = Omit; + // @public (undocumented) type RequestHandler = (operation: Operation, forward: NextLink) => Observable | null; @@ -1727,7 +1815,7 @@ interface SubscriptionOptions { // @public (undocumented) interface SubscriptionResult { - data?: TData; + data?: MaybeMasked; error?: ApolloError; loading: boolean; // @internal (undocumented) @@ -1755,6 +1843,8 @@ interface TransformCacheEntry { // (undocumented) hasNonreactiveDirective: boolean; // (undocumented) + nonReactiveQuery: DocumentNode; + // (undocumented) serverQuery: DocumentNode | null; } @@ -1765,18 +1855,35 @@ type TransformFn = (document: DocumentNode) => DocumentNode; type UnionForAny = T extends never ? "a" : 1; // @public (undocumented) -type UnionToIntersection = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never; +type UnionToIntersection = (U extends any ? (x: U) => void : never) extends (x: infer I) => void ? I : never; + +// @public (undocumented) +type UnionToIntersection_2 = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never; + +// @public +type Unmasked = TData extends object ? UnwrapFragmentRefs>> : TData; + +// Warning: (ae-forgotten-export) The symbol "CombineFragmentRefs" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type UnwrapFragmentRefs = string extends keyof NonNullable ? TData : " $fragmentRefs" extends keyof NonNullable ? TData extends { + " $fragmentRefs"?: infer FragmentRefs extends object; +} ? Prettify<{ + [K in keyof TData as K extends " $fragmentRefs" ? never : K]: UnwrapFragmentRefs; +} & CombineFragmentRefs> : never : TData extends object ? { + [K in keyof TData]: UnwrapFragmentRefs; +} : TData; // @public (undocumented) type UpdateQueries = MutationOptions["updateQueries"]; // @public (undocumented) -type UpdateQueryFn = (previousQueryResult: TData, options: { +type UpdateQueryFn = (previousQueryResult: Unmasked, options: { subscriptionData: { - data: TSubscriptionData; + data: Unmasked; }; variables?: TSubscriptionVariables; -}) => TData; +}) => Unmasked; // @public (undocumented) interface UriFunction { @@ -1788,18 +1895,19 @@ interface UriFunction { interface WatchFragmentOptions { fragment: DocumentNode | TypedDocumentNode; fragmentName?: string; - from: StoreObject | Reference | string; + // Warning: (ae-forgotten-export) The symbol "FragmentType" needs to be exported by the entry point index.d.ts + from: StoreObject | Reference | FragmentType> | string; optimistic?: boolean; variables?: TVars; } // @public type WatchFragmentResult = { - data: TData; + data: MaybeMasked; complete: true; missing?: never; } | { - data: DeepPartial; + data: DeepPartial>; complete: false; missing: MissingTree; }; @@ -1814,20 +1922,20 @@ interface WatchQueryOptions implements DataProxy { abstract evict(options: Cache_2.EvictOptions): boolean; abstract extract(optimistic?: boolean): TSerialized; // (undocumented) + fragmentMatches?(fragment: InlineFragmentNode, typename: string): boolean; + // (undocumented) gc(): string[]; // Warning: (ae-forgotten-export) The symbol "getApolloCacheMemoryInternals" needs to be exported by the entry point index.d.ts // @@ -49,19 +52,22 @@ abstract class ApolloCache implements DataProxy { // (undocumented) identify(object: StoreObject | Reference): string | undefined; // (undocumented) + lookupFragment(fragmentName: string): FragmentDefinitionNode | null; + // (undocumented) modify = Record>(options: Cache_2.ModifyOptions): boolean; // Warning: (ae-forgotten-export) The symbol "Transaction" needs to be exported by the entry point index.d.ts // // (undocumented) abstract performTransaction(transaction: Transaction, optimisticId?: string | null): void; // Warning: (ae-forgotten-export) The symbol "Cache_2" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "Unmasked" needs to be exported by the entry point index.d.ts // // (undocumented) - abstract read(query: Cache_2.ReadOptions): TData | null; + abstract read(query: Cache_2.ReadOptions): Unmasked | null; // (undocumented) - readFragment(options: Cache_2.ReadFragmentOptions, optimistic?: boolean): FragmentType | null; + readFragment(options: Cache_2.ReadFragmentOptions, optimistic?: boolean): Unmasked | null; // (undocumented) - readQuery(options: Cache_2.ReadQueryOptions, optimistic?: boolean): QueryType | null; + readQuery(options: Cache_2.ReadQueryOptions, optimistic?: boolean): Unmasked | null; // (undocumented) recordOptimisticTransaction(transaction: Transaction, optimisticId: string): void; // (undocumented) @@ -74,9 +80,9 @@ abstract class ApolloCache implements DataProxy { // (undocumented) transformForLink(document: DocumentNode): DocumentNode; // (undocumented) - updateFragment(options: Cache_2.UpdateFragmentOptions, update: (data: TData | null) => TData | null | void): TData | null; + updateFragment(options: Cache_2.UpdateFragmentOptions, update: (data: Unmasked | null) => Unmasked | null | void): Unmasked | null; // (undocumented) - updateQuery(options: Cache_2.UpdateQueryOptions, update: (data: TData | null) => TData | null | void): TData | null; + updateQuery(options: Cache_2.UpdateQueryOptions, update: (data: Unmasked | null) => Unmasked | null | void): Unmasked | null; // (undocumented) abstract watch(watch: Cache_2.WatchOptions): () => void; // Warning: (ae-forgotten-export) The symbol "OperationVariables" needs to be exported by the entry point index.d.ts @@ -134,16 +140,17 @@ class ApolloClient implements DataProxy { // Warning: (ae-forgotten-export) The symbol "DefaultContext" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "MutationOptions" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "FetchResult" needs to be exported by the entry point index.d.ts - mutate = DefaultContext, TCache extends ApolloCache = ApolloCache>(options: MutationOptions): Promise>; + mutate = DefaultContext, TCache extends ApolloCache = ApolloCache>(options: MutationOptions): Promise>>; onClearStore(cb: () => Promise): () => void; onResetStore(cb: () => Promise): () => void; // Warning: (ae-forgotten-export) The symbol "QueryOptions" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "ApolloQueryResult" needs to be exported by the entry point index.d.ts - query(options: QueryOptions): Promise>; + // Warning: (ae-forgotten-export) The symbol "MaybeMasked" needs to be exported by the entry point index.d.ts + query(options: QueryOptions): Promise>>; // (undocumented) queryDeduplication: boolean; - readFragment(options: DataProxy.Fragment, optimistic?: boolean): T | null; - readQuery(options: DataProxy.Query, optimistic?: boolean): T | null; + readFragment(options: DataProxy.Fragment, optimistic?: boolean): Unmasked | null; + readQuery(options: DataProxy.Query, optimistic?: boolean): Unmasked | null; reFetchObservableQueries(includeStandby?: boolean): Promise[]>; // Warning: (ae-forgotten-export) The symbol "RefetchQueriesOptions" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "RefetchQueriesResult" needs to be exported by the entry point index.d.ts @@ -156,7 +163,7 @@ class ApolloClient implements DataProxy { setResolvers(resolvers: Resolvers | Resolvers[]): void; stop(): void; // Warning: (ae-forgotten-export) The symbol "SubscriptionOptions" needs to be exported by the entry point index.d.ts - subscribe(options: SubscriptionOptions): Observable>; + subscribe(options: SubscriptionOptions): Observable>>; // Warning: (ae-forgotten-export) The symbol "ApolloClientOptions" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -179,6 +186,7 @@ interface ApolloClientOptions { connectToDevTools?: boolean; // (undocumented) credentials?: string; + dataMasking?: boolean; // (undocumented) defaultContext?: Partial; defaultOptions?: DefaultOptions; @@ -438,7 +446,7 @@ namespace Cache_2 { // (undocumented) dataId?: string; // (undocumented) - result: TResult; + result: Unmasked; } import DiffResult = DataProxy.DiffResult; import ReadQueryOptions = DataProxy.ReadQueryOptions; @@ -465,6 +473,15 @@ const enum CacheWriteBehavior { // @public (undocumented) type CanReadFunction = (value: StoreValue) => boolean; +// Warning: (ae-forgotten-export) The symbol "UnionToIntersection" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "UnwrapFragmentRefs" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "RemoveFragmentName" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type CombineFragmentRefs> = UnionToIntersection<{ + [K in keyof FragmentRefs]-?: UnwrapFragmentRefs>; +}[keyof FragmentRefs]>; + // @public (undocumented) class Concast extends Observable { // Warning: (ae-forgotten-export) The symbol "MaybeAsync" needs to be exported by the entry point index.d.ts @@ -489,6 +506,10 @@ class Concast extends Observable { // @public (undocumented) type ConcastSourcesIterable = Iterable>; +// @public (undocumented) +interface DataMasking { +} + // @public (undocumented) namespace DataProxy { // (undocumented) @@ -547,7 +568,7 @@ namespace DataProxy { // (undocumented) interface WriteOptions { broadcast?: boolean; - data: TData; + data: Unmasked; overwrite?: boolean; } // Warning: (ae-forgotten-export) The symbol "DataProxy" needs to be exported by the entry point index.d.ts @@ -559,8 +580,8 @@ namespace DataProxy { // @public interface DataProxy { - readFragment(options: DataProxy.ReadFragmentOptions, optimistic?: boolean): FragmentType | null; - readQuery(options: DataProxy.ReadQueryOptions, optimistic?: boolean): QueryType | null; + readFragment(options: DataProxy.ReadFragmentOptions, optimistic?: boolean): Unmasked | null; + readQuery(options: DataProxy.ReadQueryOptions, optimistic?: boolean): Unmasked | null; writeFragment(options: DataProxy.WriteFragmentOptions): Reference | undefined; writeQuery(options: DataProxy.WriteQueryOptions): Reference | undefined; } @@ -744,6 +765,17 @@ interface FragmentMap { // @public (undocumented) type FragmentMatcher = (rootValue: any, typeCondition: string, context: any) => boolean; +// @public (undocumented) +type FragmentType = [ +TData +] extends [{ + " $fragmentName"?: infer TKey; +}] ? TKey extends string ? { + " $fragmentRefs"?: { + [key in TKey]: TData; + }; +} : never : never; + // @internal const getApolloCacheMemoryInternals: (() => { cache: { @@ -869,11 +901,11 @@ const _invalidateModifier: unique symbol; // @public (undocumented) function isReference(obj: any): obj is Reference; -// Warning: (ae-forgotten-export) The symbol "UnionToIntersection" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "UnionToIntersection_2" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "UnionForAny" needs to be exported by the entry point index.d.ts // // @public (undocumented) -type IsStrictlyAny = UnionToIntersection> extends never ? true : false; +type IsStrictlyAny = UnionToIntersection_2> extends never ? true : false; // @public (undocumented) class LocalState { @@ -920,9 +952,38 @@ type LocalStateOptions = { fragmentMatcher?: FragmentMatcher; }; +// @public (undocumented) +interface MaskFragmentOptions { + // (undocumented) + data: TData; + // (undocumented) + fragment: DocumentNode; + // (undocumented) + fragmentName?: string; +} + +// @public (undocumented) +interface MaskOperationOptions { + // (undocumented) + data: TData; + // (undocumented) + document: DocumentNode; +} + // @public (undocumented) type MaybeAsync = T | PromiseLike; +// Warning: (ae-forgotten-export) The symbol "Prettify" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "RemoveMaskedMarker" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "DataMasking" needs to be exported by the entry point index.d.ts +// +// @public +type MaybeMasked = TData extends { + __masked?: true; +} ? Prettify> : DataMasking extends { + enabled: true; +} ? TData : Unmasked; + // @public (undocumented) class MissingFieldError extends Error { constructor(message: string, path: MissingTree | Array, query: DocumentNode, variables?: Record | undefined); @@ -978,11 +1039,12 @@ interface MutationBaseOptions; + // Warning: (ae-forgotten-export) The symbol "NoInfer_2" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts - optimisticResponse?: TData | ((vars: TVariables, { IGNORE }: { + optimisticResponse?: Unmasked> | ((vars: TVariables, { IGNORE }: { IGNORE: IgnoreModifier; - }) => TData | IgnoreModifier); - refetchQueries?: ((result: FetchResult) => InternalRefetchQueriesInclude) | InternalRefetchQueriesInclude; + }) => Unmasked> | IgnoreModifier); + refetchQueries?: ((result: FetchResult>) => InternalRefetchQueriesInclude) | InternalRefetchQueriesInclude; // Warning: (ae-forgotten-export) The symbol "MutationUpdaterFunction" needs to be exported by the entry point index.d.ts update?: MutationUpdaterFunction; // Warning: (ae-forgotten-export) The symbol "MutationQueryReducersMap" needs to be exported by the entry point index.d.ts @@ -1004,7 +1066,7 @@ interface MutationOptions = (previousResult: Record, options: { - mutationResult: FetchResult; + mutationResult: FetchResult>; queryName: string | undefined; queryVariables: Record; }) => Record; @@ -1038,7 +1100,7 @@ interface MutationStoreValue { } // @public (undocumented) -type MutationUpdaterFunction> = (cache: TCache, result: Omit, "context">, options: { +type MutationUpdaterFunction> = (cache: TCache, result: Omit>, "context">, options: { context?: TContext; variables?: TVariables; }) => void; @@ -1074,8 +1136,11 @@ type NextLink = (operation: Operation) => Observable; // @public (undocumented) type NextResultListener = (method: "next" | "error" | "complete", arg?: any) => any; +// @public +type NoInfer_2 = [T][T extends any ? 0 : never]; + // @public (undocumented) -class ObservableQuery extends Observable> { +class ObservableQuery extends Observable>> { constructor({ queryManager, queryInfo, options, }: { queryManager: QueryManager; queryInfo: QueryInfo; @@ -1083,13 +1148,13 @@ class ObservableQuery(fetchMoreOptions: FetchMoreQueryOptions & { - updateQuery?: (previousQueryResult: TData, options: { - fetchMoreResult: TFetchData; + updateQuery?: (previousQueryResult: Unmasked, options: { + fetchMoreResult: Unmasked; variables: TFetchVars; - }) => TData; - }): Promise>; + }) => Unmasked; + }): Promise>>; // (undocumented) - getCurrentResult(saveAsLastResult?: boolean): ApolloQueryResult; + getCurrentResult(saveAsLastResult?: boolean): ApolloQueryResult>; // (undocumented) getLastError(variablesMustMatch?: boolean): ApolloError | undefined; // (undocumented) @@ -1106,9 +1171,9 @@ class ObservableQuery): Promise>; + refetch(variables?: Partial): Promise>>; // (undocumented) - reobserve(newOptions?: Partial>, newNetworkStatus?: NetworkStatus): Promise>; + reobserve(newOptions?: Partial>, newNetworkStatus?: NetworkStatus): Promise>>; // Warning: (ae-forgotten-export) The symbol "Concast" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1120,39 +1185,39 @@ class ObservableQuery) => void, onError?: (error: any) => void, onComplete?: () => void): Subscription; + resubscribeAfterError(onNext: (value: ApolloQueryResult>) => void, onError?: (error: any) => void, onComplete?: () => void): Subscription; // (undocumented) resubscribeAfterError(observer: Observer>): Subscription; // (undocumented) - result(): Promise>; + result(): Promise>>; // (undocumented) - setOptions(newOptions: Partial>): Promise>; - setVariables(variables: TVariables): Promise | void>; + setOptions(newOptions: Partial>): Promise>>; + setVariables(variables: TVariables): Promise> | void>; // (undocumented) silentSetOptions(newOptions: Partial>): void; startPolling(pollInterval: number): void; stopPolling(): void; // Warning: (ae-forgotten-export) The symbol "SubscribeToMoreOptions" needs to be exported by the entry point index.d.ts subscribeToMore(options: SubscribeToMoreOptions): () => void; - updateQuery(mapFn: (previousQueryResult: TData, options: Pick, "variables">) => TData): void; + updateQuery(mapFn: (previousQueryResult: Unmasked, options: Pick, "variables">) => Unmasked): void; get variables(): TVariables | undefined; } // @public (undocumented) interface ObservableQueryFields { fetchMore: (fetchMoreOptions: FetchMoreQueryOptions & { - updateQuery?: (previousQueryResult: TData, options: { - fetchMoreResult: TFetchData; + updateQuery?: (previousQueryResult: Unmasked, options: { + fetchMoreResult: Unmasked; variables: TFetchVars; - }) => TData; - }) => Promise>; - refetch: (variables?: Partial) => Promise>; + }) => Unmasked; + }) => Promise>>; + refetch: (variables?: Partial) => Promise>>; // @internal (undocumented) - reobserve: (newOptions?: Partial>, newNetworkStatus?: NetworkStatus) => Promise>; + reobserve: (newOptions?: Partial>, newNetworkStatus?: NetworkStatus) => Promise>>; startPolling: (pollInterval: number) => void; stopPolling: () => void; subscribeToMore: (options: SubscribeToMoreOptions) => () => void; - updateQuery: (mapFn: (previousQueryResult: TData, options: Pick, "variables">) => TData) => void; + updateQuery: (mapFn: (previousQueryResult: Unmasked, options: Pick, "variables">) => Unmasked) => void; variables: TVariables | undefined; } @@ -1184,6 +1249,11 @@ type OperationVariables = Record; // @public (undocumented) type Path = ReadonlyArray; +// @public (undocumented) +type Prettify = { + [K in keyof T]: T[K]; +} & {}; + // @public (undocumented) type Primitive = null | undefined | string | number | boolean | symbol | bigint; @@ -1212,7 +1282,7 @@ interface QueryDataOptions extends BaseQueryOptions { // @internal (undocumented) defaultOptions?: Partial>; - onCompleted?: (data: TData) => void; + onCompleted?: (data: MaybeMasked) => void; onError?: (error: ApolloError) => void; skip?: boolean; } @@ -1292,6 +1362,8 @@ class QueryManager { // (undocumented) clearStore(options?: Cache_2.ResetOptions): Promise; // (undocumented) + readonly dataMasking: boolean; + // (undocumented) readonly defaultContext: Partial; // Warning: (ae-forgotten-export) The symbol "DefaultOptions" needs to be exported by the entry point index.d.ts // @@ -1358,14 +1430,22 @@ class QueryManager { onQueryUpdated?: OnQueryUpdated; keepRootFields?: boolean; }, cache?: ApolloCache): Promise>; + // Warning: (ae-forgotten-export) The symbol "MaskFragmentOptions" needs to be exported by the entry point index.d.ts + // // (undocumented) - mutate, TCache extends ApolloCache>({ mutation, variables, optimisticResponse, updateQueries, refetchQueries, awaitRefetchQueries, update: updateWithProxyFn, onQueryUpdated, fetchPolicy, errorPolicy, keepRootFields, context, }: MutationOptions): Promise>; + maskFragment(options: MaskFragmentOptions): TData; + // Warning: (ae-forgotten-export) The symbol "MaskOperationOptions" needs to be exported by the entry point index.d.ts + // + // (undocumented) + maskOperation(options: MaskOperationOptions): MaybeMasked; + // (undocumented) + mutate, TCache extends ApolloCache>({ mutation, variables, optimisticResponse, updateQueries, refetchQueries, awaitRefetchQueries, update: updateWithProxyFn, onQueryUpdated, fetchPolicy, errorPolicy, keepRootFields, context, }: MutationOptions): Promise>>; // (undocumented) mutationStore?: { [mutationId: string]: MutationStoreValue; }; // (undocumented) - query(options: QueryOptions, queryId?: string): Promise>; + query(options: QueryOptions, queryId?: string): Promise>>; // (undocumented) reFetchObservableQueries(includeStandby?: boolean): Promise[]>; // Warning: (ae-forgotten-export) The symbol "InternalRefetchQueriesOptions" needs to be exported by the entry point index.d.ts @@ -1382,7 +1462,7 @@ class QueryManager { // (undocumented) readonly ssrMode: boolean; // (undocumented) - startGraphQLSubscription({ query, fetchPolicy, errorPolicy, variables, context, extensions, }: SubscriptionOptions): Observable>; + startGraphQLSubscription(options: SubscriptionOptions): Observable>; stop(): void; // (undocumented) stopQuery(queryId: string): void; @@ -1403,6 +1483,8 @@ interface QueryManagerOptions { // (undocumented) clientAwareness: Record; // (undocumented) + dataMasking: boolean; + // (undocumented) defaultContext: Partial | undefined; // (undocumented) defaultOptions: DefaultOptions; @@ -1442,14 +1524,14 @@ interface QueryOptions { interface QueryResult extends ObservableQueryFields { called: boolean; client: ApolloClient; - data: TData | undefined; + data: MaybeMasked | undefined; error?: ApolloError; // @deprecated (undocumented) errors?: ReadonlyArray; loading: boolean; networkStatus: NetworkStatus; observable: ObservableQuery; - previousData?: TData; + previousData?: MaybeMasked; } // @public (undocumented) @@ -1519,6 +1601,12 @@ type RefetchQueryDescriptor = string | DocumentNode; // @public (undocumented) type RefetchWritePolicy = "merge" | "overwrite"; +// @public (undocumented) +type RemoveFragmentName = T extends any ? Omit : T; + +// @public (undocumented) +type RemoveMaskedMarker = Omit; + // @public (undocumented) class RenderPromises { // (undocumented) @@ -1675,6 +1763,8 @@ interface TransformCacheEntry { // (undocumented) hasNonreactiveDirective: boolean; // (undocumented) + nonReactiveQuery: DocumentNode; + // (undocumented) serverQuery: DocumentNode | null; } @@ -1685,18 +1775,35 @@ type TransformFn = (document: DocumentNode) => DocumentNode; type UnionForAny = T extends never ? "a" : 1; // @public (undocumented) -type UnionToIntersection = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never; +type UnionToIntersection = (U extends any ? (x: U) => void : never) extends (x: infer I) => void ? I : never; + +// @public (undocumented) +type UnionToIntersection_2 = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never; + +// @public +type Unmasked = TData extends object ? UnwrapFragmentRefs>> : TData; + +// Warning: (ae-forgotten-export) The symbol "CombineFragmentRefs" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type UnwrapFragmentRefs = string extends keyof NonNullable ? TData : " $fragmentRefs" extends keyof NonNullable ? TData extends { + " $fragmentRefs"?: infer FragmentRefs extends object; +} ? Prettify<{ + [K in keyof TData as K extends " $fragmentRefs" ? never : K]: UnwrapFragmentRefs; +} & CombineFragmentRefs> : never : TData extends object ? { + [K in keyof TData]: UnwrapFragmentRefs; +} : TData; // @public (undocumented) type UpdateQueries = MutationOptions["updateQueries"]; // @public (undocumented) -type UpdateQueryFn = (previousQueryResult: TData, options: { +type UpdateQueryFn = (previousQueryResult: Unmasked, options: { subscriptionData: { - data: TSubscriptionData; + data: Unmasked; }; variables?: TSubscriptionVariables; -}) => TData; +}) => Unmasked; // @public (undocumented) interface UriFunction { @@ -1708,18 +1815,19 @@ interface UriFunction { interface WatchFragmentOptions { fragment: DocumentNode | TypedDocumentNode; fragmentName?: string; - from: StoreObject | Reference | string; + // Warning: (ae-forgotten-export) The symbol "FragmentType" needs to be exported by the entry point index.d.ts + from: StoreObject | Reference | FragmentType> | string; optimistic?: boolean; variables?: TVars; } // @public type WatchFragmentResult = { - data: TData; + data: MaybeMasked; complete: true; missing?: never; } | { - data: DeepPartial; + data: DeepPartial>; complete: false; missing: MissingTree; }; @@ -1734,20 +1842,20 @@ interface WatchQueryOptions implements DataProxy { abstract evict(options: Cache_2.EvictOptions): boolean; abstract extract(optimistic?: boolean): TSerialized; // (undocumented) + fragmentMatches?(fragment: InlineFragmentNode, typename: string): boolean; + // (undocumented) gc(): string[]; // Warning: (ae-forgotten-export) The symbol "getApolloCacheMemoryInternals" needs to be exported by the entry point index.d.ts // @@ -49,19 +52,22 @@ abstract class ApolloCache implements DataProxy { // (undocumented) identify(object: StoreObject | Reference): string | undefined; // (undocumented) + lookupFragment(fragmentName: string): FragmentDefinitionNode | null; + // (undocumented) modify = Record>(options: Cache_2.ModifyOptions): boolean; // Warning: (ae-forgotten-export) The symbol "Transaction" needs to be exported by the entry point index.d.ts // // (undocumented) abstract performTransaction(transaction: Transaction, optimisticId?: string | null): void; // Warning: (ae-forgotten-export) The symbol "Cache_2" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "Unmasked" needs to be exported by the entry point index.d.ts // // (undocumented) - abstract read(query: Cache_2.ReadOptions): TData | null; + abstract read(query: Cache_2.ReadOptions): Unmasked | null; // (undocumented) - readFragment(options: Cache_2.ReadFragmentOptions, optimistic?: boolean): FragmentType | null; + readFragment(options: Cache_2.ReadFragmentOptions, optimistic?: boolean): Unmasked | null; // (undocumented) - readQuery(options: Cache_2.ReadQueryOptions, optimistic?: boolean): QueryType | null; + readQuery(options: Cache_2.ReadQueryOptions, optimistic?: boolean): Unmasked | null; // (undocumented) recordOptimisticTransaction(transaction: Transaction, optimisticId: string): void; // (undocumented) @@ -74,9 +80,9 @@ abstract class ApolloCache implements DataProxy { // (undocumented) transformForLink(document: DocumentNode): DocumentNode; // (undocumented) - updateFragment(options: Cache_2.UpdateFragmentOptions, update: (data: TData | null) => TData | null | void): TData | null; + updateFragment(options: Cache_2.UpdateFragmentOptions, update: (data: Unmasked | null) => Unmasked | null | void): Unmasked | null; // (undocumented) - updateQuery(options: Cache_2.UpdateQueryOptions, update: (data: TData | null) => TData | null | void): TData | null; + updateQuery(options: Cache_2.UpdateQueryOptions, update: (data: Unmasked | null) => Unmasked | null | void): Unmasked | null; // (undocumented) abstract watch(watch: Cache_2.WatchOptions): () => void; // Warning: (ae-forgotten-export) The symbol "OperationVariables" needs to be exported by the entry point index.d.ts @@ -134,16 +140,17 @@ class ApolloClient implements DataProxy { // Warning: (ae-forgotten-export) The symbol "DefaultContext" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "MutationOptions" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "FetchResult" needs to be exported by the entry point index.d.ts - mutate = DefaultContext, TCache extends ApolloCache = ApolloCache>(options: MutationOptions): Promise>; + mutate = DefaultContext, TCache extends ApolloCache = ApolloCache>(options: MutationOptions): Promise>>; onClearStore(cb: () => Promise): () => void; onResetStore(cb: () => Promise): () => void; // Warning: (ae-forgotten-export) The symbol "QueryOptions" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "ApolloQueryResult" needs to be exported by the entry point index.d.ts - query(options: QueryOptions): Promise>; + // Warning: (ae-forgotten-export) The symbol "MaybeMasked" needs to be exported by the entry point index.d.ts + query(options: QueryOptions): Promise>>; // (undocumented) queryDeduplication: boolean; - readFragment(options: DataProxy.Fragment, optimistic?: boolean): T | null; - readQuery(options: DataProxy.Query, optimistic?: boolean): T | null; + readFragment(options: DataProxy.Fragment, optimistic?: boolean): Unmasked | null; + readQuery(options: DataProxy.Query, optimistic?: boolean): Unmasked | null; reFetchObservableQueries(includeStandby?: boolean): Promise[]>; // Warning: (ae-forgotten-export) The symbol "RefetchQueriesOptions" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "RefetchQueriesResult" needs to be exported by the entry point index.d.ts @@ -156,7 +163,7 @@ class ApolloClient implements DataProxy { setResolvers(resolvers: Resolvers | Resolvers[]): void; stop(): void; // Warning: (ae-forgotten-export) The symbol "SubscriptionOptions" needs to be exported by the entry point index.d.ts - subscribe(options: SubscriptionOptions): Observable>; + subscribe(options: SubscriptionOptions): Observable>>; // Warning: (ae-forgotten-export) The symbol "ApolloClientOptions" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -179,6 +186,7 @@ interface ApolloClientOptions { connectToDevTools?: boolean; // (undocumented) credentials?: string; + dataMasking?: boolean; // (undocumented) defaultContext?: Partial; defaultOptions?: DefaultOptions; @@ -320,7 +328,7 @@ interface BaseMutationOptions; ignoreResults?: boolean; notifyOnNetworkStatusChange?: boolean; - onCompleted?: (data: TData, clientOptions?: BaseMutationOptions) => void; + onCompleted?: (data: MaybeMasked, clientOptions?: BaseMutationOptions) => void; onError?: (error: ApolloError, clientOptions?: BaseMutationOptions) => void; } @@ -418,7 +426,7 @@ namespace Cache_2 { // (undocumented) dataId?: string; // (undocumented) - result: TResult; + result: Unmasked; } import DiffResult = DataProxy.DiffResult; import ReadQueryOptions = DataProxy.ReadQueryOptions; @@ -454,6 +462,15 @@ export type ChildMutateProps = TProps & Partial> & Partial>; +// Warning: (ae-forgotten-export) The symbol "UnionToIntersection" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "UnwrapFragmentRefs" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "RemoveFragmentName" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type CombineFragmentRefs> = UnionToIntersection<{ + [K in keyof FragmentRefs]-?: UnwrapFragmentRefs>; +}[keyof FragmentRefs]>; + // @public (undocumented) class Concast extends Observable { // Warning: (ae-forgotten-export) The symbol "MaybeAsync" needs to be exported by the entry point index.d.ts @@ -478,6 +495,10 @@ class Concast extends Observable { // @public (undocumented) type ConcastSourcesIterable = Iterable>; +// @public (undocumented) +interface DataMasking { +} + // @public (undocumented) export interface DataProps { // (undocumented) @@ -542,7 +563,7 @@ namespace DataProxy { // (undocumented) interface WriteOptions { broadcast?: boolean; - data: TData; + data: Unmasked; overwrite?: boolean; } // Warning: (ae-forgotten-export) The symbol "DataProxy" needs to be exported by the entry point index.d.ts @@ -554,8 +575,8 @@ namespace DataProxy { // @public interface DataProxy { - readFragment(options: DataProxy.ReadFragmentOptions, optimistic?: boolean): FragmentType | null; - readQuery(options: DataProxy.ReadQueryOptions, optimistic?: boolean): QueryType | null; + readFragment(options: DataProxy.ReadFragmentOptions, optimistic?: boolean): Unmasked | null; + readQuery(options: DataProxy.ReadQueryOptions, optimistic?: boolean): Unmasked | null; writeFragment(options: DataProxy.WriteFragmentOptions): Reference | undefined; writeQuery(options: DataProxy.WriteQueryOptions): Reference | undefined; } @@ -751,6 +772,17 @@ interface FragmentMap { // @public (undocumented) type FragmentMatcher = (rootValue: any, typeCondition: string, context: any) => boolean; +// @public (undocumented) +type FragmentType = [ +TData +] extends [{ + " $fragmentName"?: infer TKey; +}] ? TKey extends string ? { + " $fragmentRefs"?: { + [key in TKey]: TData; + }; +} : never : never; + // @internal const getApolloCacheMemoryInternals: (() => { cache: { @@ -876,11 +908,11 @@ const _invalidateModifier: unique symbol; // @public (undocumented) function isReference(obj: any): obj is Reference; -// Warning: (ae-forgotten-export) The symbol "UnionToIntersection" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "UnionToIntersection_2" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "UnionForAny" needs to be exported by the entry point index.d.ts // // @public (undocumented) -type IsStrictlyAny = UnionToIntersection> extends never ? true : false; +type IsStrictlyAny = UnionToIntersection_2> extends never ? true : false; // @public (undocumented) class LocalState { @@ -927,9 +959,38 @@ type LocalStateOptions = { fragmentMatcher?: FragmentMatcher; }; +// @public (undocumented) +interface MaskFragmentOptions { + // (undocumented) + data: TData; + // (undocumented) + fragment: DocumentNode; + // (undocumented) + fragmentName?: string; +} + +// @public (undocumented) +interface MaskOperationOptions { + // (undocumented) + data: TData; + // (undocumented) + document: DocumentNode; +} + // @public (undocumented) type MaybeAsync = T | PromiseLike; +// Warning: (ae-forgotten-export) The symbol "Prettify" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "RemoveMaskedMarker" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "DataMasking" needs to be exported by the entry point index.d.ts +// +// @public +type MaybeMasked = TData extends { + __masked?: true; +} ? Prettify> : DataMasking extends { + enabled: true; +} ? TData : Unmasked; + // @public (undocumented) class MissingFieldError extends Error { constructor(message: string, path: MissingTree | Array, query: DocumentNode, variables?: Record | undefined); @@ -997,11 +1058,12 @@ interface MutationBaseOptions; + // Warning: (ae-forgotten-export) The symbol "NoInfer_2" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts - optimisticResponse?: TData | ((vars: TVariables, { IGNORE }: { + optimisticResponse?: Unmasked> | ((vars: TVariables, { IGNORE }: { IGNORE: IgnoreModifier; - }) => TData | IgnoreModifier); - refetchQueries?: ((result: FetchResult) => InternalRefetchQueriesInclude) | InternalRefetchQueriesInclude; + }) => Unmasked> | IgnoreModifier); + refetchQueries?: ((result: FetchResult>) => InternalRefetchQueriesInclude) | InternalRefetchQueriesInclude; // Warning: (ae-forgotten-export) The symbol "MutationUpdaterFunction" needs to be exported by the entry point index.d.ts update?: MutationUpdaterFunction; // Warning: (ae-forgotten-export) The symbol "MutationQueryReducersMap" needs to be exported by the entry point index.d.ts @@ -1017,7 +1079,7 @@ type MutationFetchPolicy = Extract; // Warning: (ae-forgotten-export) The symbol "MutationFunctionOptions" needs to be exported by the entry point index.d.ts // // @public (undocumented) -type MutationFunction = ApolloCache> = (options?: MutationFunctionOptions) => Promise>; +type MutationFunction = ApolloCache> = (options?: MutationFunctionOptions) => Promise>>; // Warning: (ae-forgotten-export) The symbol "BaseMutationOptions" needs to be exported by the entry point index.d.ts // @@ -1033,7 +1095,7 @@ interface MutationOptions = (previousResult: Record, options: { - mutationResult: FetchResult; + mutationResult: FetchResult>; queryName: string | undefined; queryVariables: Record; }) => Record; @@ -1049,7 +1111,7 @@ type MutationQueryReducersMap { called: boolean; client: ApolloClient; - data?: TData | null; + data?: MaybeMasked | null; error?: ApolloError; loading: boolean; reset: () => void; @@ -1077,7 +1139,7 @@ interface MutationStoreValue { } // @public (undocumented) -type MutationUpdaterFunction> = (cache: TCache, result: Omit, "context">, options: { +type MutationUpdaterFunction> = (cache: TCache, result: Omit>, "context">, options: { context?: TContext; variables?: TVariables; }) => void; @@ -1113,8 +1175,11 @@ type NextLink = (operation: Operation) => Observable; // @public (undocumented) type NextResultListener = (method: "next" | "error" | "complete", arg?: any) => any; +// @public +type NoInfer_2 = [T][T extends any ? 0 : never]; + // @public (undocumented) -class ObservableQuery extends Observable> { +class ObservableQuery extends Observable>> { constructor({ queryManager, queryInfo, options, }: { queryManager: QueryManager; queryInfo: QueryInfo; @@ -1122,13 +1187,13 @@ class ObservableQuery(fetchMoreOptions: FetchMoreQueryOptions & { - updateQuery?: (previousQueryResult: TData, options: { - fetchMoreResult: TFetchData; + updateQuery?: (previousQueryResult: Unmasked, options: { + fetchMoreResult: Unmasked; variables: TFetchVars; - }) => TData; - }): Promise>; + }) => Unmasked; + }): Promise>>; // (undocumented) - getCurrentResult(saveAsLastResult?: boolean): ApolloQueryResult; + getCurrentResult(saveAsLastResult?: boolean): ApolloQueryResult>; // (undocumented) getLastError(variablesMustMatch?: boolean): ApolloError | undefined; // (undocumented) @@ -1145,9 +1210,9 @@ class ObservableQuery): Promise>; + refetch(variables?: Partial): Promise>>; // (undocumented) - reobserve(newOptions?: Partial>, newNetworkStatus?: NetworkStatus): Promise>; + reobserve(newOptions?: Partial>, newNetworkStatus?: NetworkStatus): Promise>>; // Warning: (ae-forgotten-export) The symbol "Concast" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1159,21 +1224,21 @@ class ObservableQuery) => void, onError?: (error: any) => void, onComplete?: () => void): Subscription; + resubscribeAfterError(onNext: (value: ApolloQueryResult>) => void, onError?: (error: any) => void, onComplete?: () => void): Subscription; // (undocumented) resubscribeAfterError(observer: Observer>): Subscription; // (undocumented) - result(): Promise>; + result(): Promise>>; // (undocumented) - setOptions(newOptions: Partial>): Promise>; - setVariables(variables: TVariables): Promise | void>; + setOptions(newOptions: Partial>): Promise>>; + setVariables(variables: TVariables): Promise> | void>; // (undocumented) silentSetOptions(newOptions: Partial>): void; startPolling(pollInterval: number): void; stopPolling(): void; // Warning: (ae-forgotten-export) The symbol "SubscribeToMoreOptions" needs to be exported by the entry point index.d.ts subscribeToMore(options: SubscribeToMoreOptions): () => void; - updateQuery(mapFn: (previousQueryResult: TData, options: Pick, "variables">) => TData): void; + updateQuery(mapFn: (previousQueryResult: Unmasked, options: Pick, "variables">) => Unmasked): void; get variables(): TVariables | undefined; } @@ -1231,6 +1296,11 @@ export interface OptionProps; +// @public (undocumented) +type Prettify = { + [K in keyof T]: T[K]; +} & {}; + // @public (undocumented) type Primitive = null | undefined | string | number | boolean | symbol | bigint; @@ -1337,6 +1407,8 @@ class QueryManager { // (undocumented) clearStore(options?: Cache_2.ResetOptions): Promise; // (undocumented) + readonly dataMasking: boolean; + // (undocumented) readonly defaultContext: Partial; // Warning: (ae-forgotten-export) The symbol "DefaultOptions" needs to be exported by the entry point index.d.ts // @@ -1403,14 +1475,22 @@ class QueryManager { onQueryUpdated?: OnQueryUpdated; keepRootFields?: boolean; }, cache?: ApolloCache): Promise>; + // Warning: (ae-forgotten-export) The symbol "MaskFragmentOptions" needs to be exported by the entry point index.d.ts + // // (undocumented) - mutate, TCache extends ApolloCache>({ mutation, variables, optimisticResponse, updateQueries, refetchQueries, awaitRefetchQueries, update: updateWithProxyFn, onQueryUpdated, fetchPolicy, errorPolicy, keepRootFields, context, }: MutationOptions): Promise>; + maskFragment(options: MaskFragmentOptions): TData; + // Warning: (ae-forgotten-export) The symbol "MaskOperationOptions" needs to be exported by the entry point index.d.ts + // + // (undocumented) + maskOperation(options: MaskOperationOptions): MaybeMasked; + // (undocumented) + mutate, TCache extends ApolloCache>({ mutation, variables, optimisticResponse, updateQueries, refetchQueries, awaitRefetchQueries, update: updateWithProxyFn, onQueryUpdated, fetchPolicy, errorPolicy, keepRootFields, context, }: MutationOptions): Promise>>; // (undocumented) mutationStore?: { [mutationId: string]: MutationStoreValue; }; // (undocumented) - query(options: QueryOptions, queryId?: string): Promise>; + query(options: QueryOptions, queryId?: string): Promise>>; // (undocumented) reFetchObservableQueries(includeStandby?: boolean): Promise[]>; // Warning: (ae-forgotten-export) The symbol "InternalRefetchQueriesOptions" needs to be exported by the entry point index.d.ts @@ -1427,7 +1507,7 @@ class QueryManager { // (undocumented) readonly ssrMode: boolean; // (undocumented) - startGraphQLSubscription({ query, fetchPolicy, errorPolicy, variables, context, extensions, }: SubscriptionOptions): Observable>; + startGraphQLSubscription(options: SubscriptionOptions): Observable>; stop(): void; // (undocumented) stopQuery(queryId: string): void; @@ -1448,6 +1528,8 @@ interface QueryManagerOptions { // (undocumented) clientAwareness: Record; // (undocumented) + dataMasking: boolean; + // (undocumented) defaultContext: Partial | undefined; // (undocumented) defaultOptions: DefaultOptions; @@ -1548,6 +1630,12 @@ type RefetchQueryDescriptor = string | DocumentNode; // @public (undocumented) type RefetchWritePolicy = "merge" | "overwrite"; +// @public (undocumented) +type RemoveFragmentName = T extends any ? Omit : T; + +// @public (undocumented) +type RemoveMaskedMarker = Omit; + // @public (undocumented) type RequestHandler = (operation: Operation, forward: NextLink) => Observable | null; @@ -1679,6 +1767,8 @@ interface TransformCacheEntry { // (undocumented) hasNonreactiveDirective: boolean; // (undocumented) + nonReactiveQuery: DocumentNode; + // (undocumented) serverQuery: DocumentNode | null; } @@ -1689,18 +1779,35 @@ type TransformFn = (document: DocumentNode) => DocumentNode; type UnionForAny = T extends never ? "a" : 1; // @public (undocumented) -type UnionToIntersection = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never; +type UnionToIntersection = (U extends any ? (x: U) => void : never) extends (x: infer I) => void ? I : never; + +// @public (undocumented) +type UnionToIntersection_2 = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never; + +// @public +type Unmasked = TData extends object ? UnwrapFragmentRefs>> : TData; + +// Warning: (ae-forgotten-export) The symbol "CombineFragmentRefs" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type UnwrapFragmentRefs = string extends keyof NonNullable ? TData : " $fragmentRefs" extends keyof NonNullable ? TData extends { + " $fragmentRefs"?: infer FragmentRefs extends object; +} ? Prettify<{ + [K in keyof TData as K extends " $fragmentRefs" ? never : K]: UnwrapFragmentRefs; +} & CombineFragmentRefs> : never : TData extends object ? { + [K in keyof TData]: UnwrapFragmentRefs; +} : TData; // @public (undocumented) type UpdateQueries = MutationOptions["updateQueries"]; // @public (undocumented) -type UpdateQueryFn = (previousQueryResult: TData, options: { +type UpdateQueryFn = (previousQueryResult: Unmasked, options: { subscriptionData: { - data: TSubscriptionData; + data: Unmasked; }; variables?: TSubscriptionVariables; -}) => TData; +}) => Unmasked; // @public (undocumented) interface UpdateQueryOptions { @@ -1718,18 +1825,19 @@ interface UriFunction { interface WatchFragmentOptions { fragment: DocumentNode | TypedDocumentNode; fragmentName?: string; - from: StoreObject | Reference | string; + // Warning: (ae-forgotten-export) The symbol "FragmentType" needs to be exported by the entry point index.d.ts + from: StoreObject | Reference | FragmentType> | string; optimistic?: boolean; variables?: TVars; } // @public type WatchFragmentResult = { - data: TData; + data: MaybeMasked; complete: true; missing?: never; } | { - data: DeepPartial; + data: DeepPartial>; complete: false; missing: MissingTree; }; @@ -1761,20 +1869,20 @@ export function withSubscription implements DataProxy { abstract evict(options: Cache_2.EvictOptions): boolean; abstract extract(optimistic?: boolean): TSerialized; // (undocumented) + fragmentMatches?(fragment: InlineFragmentNode, typename: string): boolean; + // (undocumented) gc(): string[]; // Warning: (ae-forgotten-export) The symbol "getApolloCacheMemoryInternals" needs to be exported by the entry point index.d.ts // @@ -48,19 +51,22 @@ abstract class ApolloCache implements DataProxy { // (undocumented) identify(object: StoreObject | Reference): string | undefined; // (undocumented) + lookupFragment(fragmentName: string): FragmentDefinitionNode | null; + // (undocumented) modify = Record>(options: Cache_2.ModifyOptions): boolean; // Warning: (ae-forgotten-export) The symbol "Transaction" needs to be exported by the entry point index.d.ts // // (undocumented) abstract performTransaction(transaction: Transaction, optimisticId?: string | null): void; // Warning: (ae-forgotten-export) The symbol "Cache_2" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "Unmasked" needs to be exported by the entry point index.d.ts // // (undocumented) - abstract read(query: Cache_2.ReadOptions): TData | null; + abstract read(query: Cache_2.ReadOptions): Unmasked | null; // (undocumented) - readFragment(options: Cache_2.ReadFragmentOptions, optimistic?: boolean): FragmentType | null; + readFragment(options: Cache_2.ReadFragmentOptions, optimistic?: boolean): Unmasked | null; // (undocumented) - readQuery(options: Cache_2.ReadQueryOptions, optimistic?: boolean): QueryType | null; + readQuery(options: Cache_2.ReadQueryOptions, optimistic?: boolean): Unmasked | null; // (undocumented) recordOptimisticTransaction(transaction: Transaction, optimisticId: string): void; // (undocumented) @@ -73,9 +79,9 @@ abstract class ApolloCache implements DataProxy { // (undocumented) transformForLink(document: DocumentNode): DocumentNode; // (undocumented) - updateFragment(options: Cache_2.UpdateFragmentOptions, update: (data: TData | null) => TData | null | void): TData | null; + updateFragment(options: Cache_2.UpdateFragmentOptions, update: (data: Unmasked | null) => Unmasked | null | void): Unmasked | null; // (undocumented) - updateQuery(options: Cache_2.UpdateQueryOptions, update: (data: TData | null) => TData | null | void): TData | null; + updateQuery(options: Cache_2.UpdateQueryOptions, update: (data: Unmasked | null) => Unmasked | null | void): Unmasked | null; // (undocumented) abstract watch(watch: Cache_2.WatchOptions): () => void; // Warning: (ae-forgotten-export) The symbol "OperationVariables" needs to be exported by the entry point index.d.ts @@ -133,16 +139,17 @@ class ApolloClient implements DataProxy { // Warning: (ae-forgotten-export) The symbol "DefaultContext" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "MutationOptions" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "FetchResult" needs to be exported by the entry point index.d.ts - mutate = DefaultContext, TCache extends ApolloCache = ApolloCache>(options: MutationOptions): Promise>; + mutate = DefaultContext, TCache extends ApolloCache = ApolloCache>(options: MutationOptions): Promise>>; onClearStore(cb: () => Promise): () => void; onResetStore(cb: () => Promise): () => void; // Warning: (ae-forgotten-export) The symbol "QueryOptions" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "ApolloQueryResult" needs to be exported by the entry point index.d.ts - query(options: QueryOptions): Promise>; + // Warning: (ae-forgotten-export) The symbol "MaybeMasked" needs to be exported by the entry point index.d.ts + query(options: QueryOptions): Promise>>; // (undocumented) queryDeduplication: boolean; - readFragment(options: DataProxy.Fragment, optimistic?: boolean): T | null; - readQuery(options: DataProxy.Query, optimistic?: boolean): T | null; + readFragment(options: DataProxy.Fragment, optimistic?: boolean): Unmasked | null; + readQuery(options: DataProxy.Query, optimistic?: boolean): Unmasked | null; reFetchObservableQueries(includeStandby?: boolean): Promise[]>; // Warning: (ae-forgotten-export) The symbol "RefetchQueriesOptions" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "RefetchQueriesResult" needs to be exported by the entry point index.d.ts @@ -155,7 +162,7 @@ class ApolloClient implements DataProxy { setResolvers(resolvers: Resolvers | Resolvers[]): void; stop(): void; // Warning: (ae-forgotten-export) The symbol "SubscriptionOptions" needs to be exported by the entry point index.d.ts - subscribe(options: SubscriptionOptions): Observable>; + subscribe(options: SubscriptionOptions): Observable>>; // Warning: (ae-forgotten-export) The symbol "ApolloClientOptions" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -178,6 +185,7 @@ interface ApolloClientOptions { connectToDevTools?: boolean; // (undocumented) credentials?: string; + dataMasking?: boolean; // (undocumented) defaultContext?: Partial; defaultOptions?: DefaultOptions; @@ -344,7 +352,7 @@ interface BaseMutationOptions; ignoreResults?: boolean; notifyOnNetworkStatusChange?: boolean; - onCompleted?: (data: TData, clientOptions?: BaseMutationOptions) => void; + onCompleted?: (data: MaybeMasked, clientOptions?: BaseMutationOptions) => void; onError?: (error: ApolloError, clientOptions?: BaseMutationOptions) => void; } @@ -467,7 +475,7 @@ namespace Cache_2 { // (undocumented) dataId?: string; // (undocumented) - result: TResult; + result: Unmasked; } import DiffResult = DataProxy.DiffResult; import ReadQueryOptions = DataProxy.ReadQueryOptions; @@ -494,6 +502,15 @@ const enum CacheWriteBehavior { // @public (undocumented) type CanReadFunction = (value: StoreValue) => boolean; +// Warning: (ae-forgotten-export) The symbol "UnionToIntersection" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "UnwrapFragmentRefs" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "RemoveFragmentName" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type CombineFragmentRefs> = UnionToIntersection<{ + [K in keyof FragmentRefs]-?: UnwrapFragmentRefs>; +}[keyof FragmentRefs]>; + // @public (undocumented) class Concast extends Observable { // Warning: (ae-forgotten-export) The symbol "MaybeAsync" needs to be exported by the entry point index.d.ts @@ -518,6 +535,10 @@ class Concast extends Observable { // @public (undocumented) type ConcastSourcesIterable = Iterable>; +// @public (undocumented) +interface DataMasking { +} + // @public (undocumented) namespace DataProxy { // (undocumented) @@ -576,7 +597,7 @@ namespace DataProxy { // (undocumented) interface WriteOptions { broadcast?: boolean; - data: TData; + data: Unmasked; overwrite?: boolean; } // Warning: (ae-forgotten-export) The symbol "DataProxy" needs to be exported by the entry point index.d.ts @@ -588,8 +609,8 @@ namespace DataProxy { // @public interface DataProxy { - readFragment(options: DataProxy.ReadFragmentOptions, optimistic?: boolean): FragmentType | null; - readQuery(options: DataProxy.ReadQueryOptions, optimistic?: boolean): QueryType | null; + readFragment(options: DataProxy.ReadFragmentOptions, optimistic?: boolean): Unmasked | null; + readQuery(options: DataProxy.ReadQueryOptions, optimistic?: boolean): Unmasked | null; writeFragment(options: DataProxy.WriteFragmentOptions): Reference | undefined; writeQuery(options: DataProxy.WriteQueryOptions): Reference | undefined; } @@ -737,11 +758,11 @@ interface ExecutionPatchResultBase { // // @public (undocumented) type FetchMoreFunction = (fetchMoreOptions: FetchMoreQueryOptions & { - updateQuery?: (previousQueryResult: TData, options: { - fetchMoreResult: TData; + updateQuery?: (previousQueryResult: Unmasked, options: { + fetchMoreResult: Unmasked; variables: TVariables; - }) => TData; -}) => Promise>; + }) => Unmasked; +}) => Promise>>; // @public (undocumented) interface FetchMoreQueryOptions { @@ -783,6 +804,17 @@ interface FragmentMap { // @public (undocumented) type FragmentMatcher = (rootValue: any, typeCondition: string, context: any) => boolean; +// @public (undocumented) +type FragmentType = [ +TData +] extends [{ + " $fragmentName"?: infer TKey; +}] ? TKey extends string ? { + " $fragmentRefs"?: { + [key in TKey]: TData; + }; +} : never : never; + // @internal const getApolloCacheMemoryInternals: (() => { cache: { @@ -905,11 +937,11 @@ const _invalidateModifier: unique symbol; // @public (undocumented) function isReference(obj: any): obj is Reference; -// Warning: (ae-forgotten-export) The symbol "UnionToIntersection" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "UnionToIntersection_2" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "UnionForAny" needs to be exported by the entry point index.d.ts // // @public (undocumented) -type IsStrictlyAny = UnionToIntersection> extends never ? true : false; +type IsStrictlyAny = UnionToIntersection_2> extends never ? true : false; // Warning: (ae-forgotten-export) The symbol "LazyQueryHookExecOptions" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "QueryResult" needs to be exported by the entry point index.d.ts @@ -931,7 +963,7 @@ interface LazyQueryHookExecOptions extends BaseQueryOptions { // @internal (undocumented) defaultOptions?: Partial>; - onCompleted?: (data: TData) => void; + onCompleted?: (data: MaybeMasked) => void; onError?: (error: ApolloError) => void; } @@ -1011,9 +1043,38 @@ type LocalStateOptions = { fragmentMatcher?: FragmentMatcher; }; +// @public (undocumented) +interface MaskFragmentOptions { + // (undocumented) + data: TData; + // (undocumented) + fragment: DocumentNode; + // (undocumented) + fragmentName?: string; +} + +// @public (undocumented) +interface MaskOperationOptions { + // (undocumented) + data: TData; + // (undocumented) + document: DocumentNode; +} + // @public (undocumented) type MaybeAsync = T | PromiseLike; +// Warning: (ae-forgotten-export) The symbol "Prettify" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "RemoveMaskedMarker" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "DataMasking" needs to be exported by the entry point index.d.ts +// +// @public +type MaybeMasked = TData extends { + __masked?: true; +} ? Prettify> : DataMasking extends { + enabled: true; +} ? TData : Unmasked; + // @public (undocumented) class MissingFieldError extends Error { constructor(message: string, path: MissingTree | Array, query: DocumentNode, variables?: Record | undefined); @@ -1069,10 +1130,10 @@ interface MutationBaseOptions; // Warning: (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts - optimisticResponse?: TData | ((vars: TVariables, { IGNORE }: { + optimisticResponse?: Unmasked> | ((vars: TVariables, { IGNORE }: { IGNORE: IgnoreModifier; - }) => TData | IgnoreModifier); - refetchQueries?: ((result: FetchResult) => InternalRefetchQueriesInclude) | InternalRefetchQueriesInclude; + }) => Unmasked> | IgnoreModifier); + refetchQueries?: ((result: FetchResult>) => InternalRefetchQueriesInclude) | InternalRefetchQueriesInclude; // Warning: (ae-forgotten-export) The symbol "MutationUpdaterFunction" needs to be exported by the entry point index.d.ts update?: MutationUpdaterFunction; // Warning: (ae-forgotten-export) The symbol "MutationQueryReducersMap" needs to be exported by the entry point index.d.ts @@ -1101,7 +1162,7 @@ interface MutationOptions = (previousResult: Record, options: { - mutationResult: FetchResult; + mutationResult: FetchResult>; queryName: string | undefined; queryVariables: Record; }) => Record; @@ -1117,7 +1178,7 @@ type MutationQueryReducersMap { called: boolean; client: ApolloClient; - data?: TData | null; + data?: MaybeMasked | null; error?: ApolloError; loading: boolean; reset: () => void; @@ -1149,12 +1210,12 @@ interface MutationStoreValue { // // @public (undocumented) type MutationTuple = ApolloCache> = [ -mutate: (options?: MutationFunctionOptions) => Promise>, +mutate: (options?: MutationFunctionOptions) => Promise>>, result: MutationResult ]; // @public (undocumented) -type MutationUpdaterFunction> = (cache: TCache, result: Omit, "context">, options: { +type MutationUpdaterFunction> = (cache: TCache, result: Omit>, "context">, options: { context?: TContext; variables?: TVariables; }) => void; @@ -1192,20 +1253,20 @@ type NextResultListener = (method: "next" | "error" | "complete", arg?: any) => type NoInfer_2 = [T][T extends any ? 0 : never]; // @public (undocumented) -class ObservableQuery extends Observable> { +class ObservableQuery extends Observable>> { constructor({ queryManager, queryInfo, options, }: { queryManager: QueryManager; queryInfo: QueryInfo; options: WatchQueryOptions; }); fetchMore(fetchMoreOptions: FetchMoreQueryOptions & { - updateQuery?: (previousQueryResult: TData, options: { - fetchMoreResult: TFetchData; + updateQuery?: (previousQueryResult: Unmasked, options: { + fetchMoreResult: Unmasked; variables: TFetchVars; - }) => TData; - }): Promise>; + }) => Unmasked; + }): Promise>>; // (undocumented) - getCurrentResult(saveAsLastResult?: boolean): ApolloQueryResult; + getCurrentResult(saveAsLastResult?: boolean): ApolloQueryResult>; // (undocumented) getLastError(variablesMustMatch?: boolean): ApolloError | undefined; // (undocumented) @@ -1222,9 +1283,9 @@ class ObservableQuery): Promise>; + refetch(variables?: Partial): Promise>>; // (undocumented) - reobserve(newOptions?: Partial>, newNetworkStatus?: NetworkStatus): Promise>; + reobserve(newOptions?: Partial>, newNetworkStatus?: NetworkStatus): Promise>>; // Warning: (ae-forgotten-export) The symbol "Concast" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1236,39 +1297,39 @@ class ObservableQuery) => void, onError?: (error: any) => void, onComplete?: () => void): Subscription; + resubscribeAfterError(onNext: (value: ApolloQueryResult>) => void, onError?: (error: any) => void, onComplete?: () => void): Subscription; // (undocumented) resubscribeAfterError(observer: Observer>): Subscription; // (undocumented) - result(): Promise>; + result(): Promise>>; // (undocumented) - setOptions(newOptions: Partial>): Promise>; - setVariables(variables: TVariables): Promise | void>; + setOptions(newOptions: Partial>): Promise>>; + setVariables(variables: TVariables): Promise> | void>; // (undocumented) silentSetOptions(newOptions: Partial>): void; startPolling(pollInterval: number): void; stopPolling(): void; // Warning: (ae-forgotten-export) The symbol "SubscribeToMoreOptions" needs to be exported by the entry point index.d.ts subscribeToMore(options: SubscribeToMoreOptions): () => void; - updateQuery(mapFn: (previousQueryResult: TData, options: Pick, "variables">) => TData): void; + updateQuery(mapFn: (previousQueryResult: Unmasked, options: Pick, "variables">) => Unmasked): void; get variables(): TVariables | undefined; } // @public (undocumented) interface ObservableQueryFields { fetchMore: (fetchMoreOptions: FetchMoreQueryOptions & { - updateQuery?: (previousQueryResult: TData, options: { - fetchMoreResult: TFetchData; + updateQuery?: (previousQueryResult: Unmasked, options: { + fetchMoreResult: Unmasked; variables: TFetchVars; - }) => TData; - }) => Promise>; - refetch: (variables?: Partial) => Promise>; + }) => Unmasked; + }) => Promise>>; + refetch: (variables?: Partial) => Promise>>; // @internal (undocumented) - reobserve: (newOptions?: Partial>, newNetworkStatus?: NetworkStatus) => Promise>; + reobserve: (newOptions?: Partial>, newNetworkStatus?: NetworkStatus) => Promise>>; startPolling: (pollInterval: number) => void; stopPolling: () => void; subscribeToMore: (options: SubscribeToMoreOptions) => () => void; - updateQuery: (mapFn: (previousQueryResult: TData, options: Pick, "variables">) => TData) => void; + updateQuery: (mapFn: (previousQueryResult: Unmasked, options: Pick, "variables">) => Unmasked) => void; variables: TVariables | undefined; } @@ -1323,6 +1384,11 @@ type OperationVariables = Record; // @public (undocumented) type Path = ReadonlyArray; +// @public (undocumented) +type Prettify = { + [K in keyof T]: T[K]; +} & {}; + // @public (undocumented) type Primitive = null | undefined | string | number | boolean | symbol | bigint; @@ -1333,7 +1399,7 @@ const QUERY_REF_BRAND: unique symbol; interface QueryFunctionOptions extends BaseQueryOptions { // @internal (undocumented) defaultOptions?: Partial>; - onCompleted?: (data: TData) => void; + onCompleted?: (data: MaybeMasked) => void; onError?: (error: ApolloError) => void; skip?: boolean; } @@ -1419,6 +1485,8 @@ class QueryManager { // (undocumented) clearStore(options?: Cache_2.ResetOptions): Promise; // (undocumented) + readonly dataMasking: boolean; + // (undocumented) readonly defaultContext: Partial; // Warning: (ae-forgotten-export) The symbol "DefaultOptions" needs to be exported by the entry point index.d.ts // @@ -1485,14 +1553,22 @@ class QueryManager { onQueryUpdated?: OnQueryUpdated; keepRootFields?: boolean; }, cache?: ApolloCache): Promise>; + // Warning: (ae-forgotten-export) The symbol "MaskFragmentOptions" needs to be exported by the entry point index.d.ts + // // (undocumented) - mutate, TCache extends ApolloCache>({ mutation, variables, optimisticResponse, updateQueries, refetchQueries, awaitRefetchQueries, update: updateWithProxyFn, onQueryUpdated, fetchPolicy, errorPolicy, keepRootFields, context, }: MutationOptions): Promise>; + maskFragment(options: MaskFragmentOptions): TData; + // Warning: (ae-forgotten-export) The symbol "MaskOperationOptions" needs to be exported by the entry point index.d.ts + // + // (undocumented) + maskOperation(options: MaskOperationOptions): MaybeMasked; + // (undocumented) + mutate, TCache extends ApolloCache>({ mutation, variables, optimisticResponse, updateQueries, refetchQueries, awaitRefetchQueries, update: updateWithProxyFn, onQueryUpdated, fetchPolicy, errorPolicy, keepRootFields, context, }: MutationOptions): Promise>>; // (undocumented) mutationStore?: { [mutationId: string]: MutationStoreValue; }; // (undocumented) - query(options: QueryOptions, queryId?: string): Promise>; + query(options: QueryOptions, queryId?: string): Promise>>; // (undocumented) reFetchObservableQueries(includeStandby?: boolean): Promise[]>; // Warning: (ae-forgotten-export) The symbol "InternalRefetchQueriesOptions" needs to be exported by the entry point index.d.ts @@ -1509,7 +1585,7 @@ class QueryManager { // (undocumented) readonly ssrMode: boolean; // (undocumented) - startGraphQLSubscription({ query, fetchPolicy, errorPolicy, variables, context, extensions, }: SubscriptionOptions): Observable>; + startGraphQLSubscription(options: SubscriptionOptions): Observable>; stop(): void; // (undocumented) stopQuery(queryId: string): void; @@ -1530,6 +1606,8 @@ interface QueryManagerOptions { // (undocumented) clientAwareness: Record; // (undocumented) + dataMasking: boolean; + // (undocumented) defaultContext: Partial | undefined; // (undocumented) defaultOptions: DefaultOptions; @@ -1575,14 +1653,14 @@ interface QueryRef { interface QueryResult extends ObservableQueryFields { called: boolean; client: ApolloClient; - data: TData | undefined; + data: MaybeMasked | undefined; error?: ApolloError; // @deprecated (undocumented) errors?: ReadonlyArray; loading: boolean; networkStatus: NetworkStatus; observable: ObservableQuery; - previousData?: TData; + previousData?: MaybeMasked; } // @public (undocumented) @@ -1672,6 +1750,12 @@ type RefetchQueryDescriptor = string | DocumentNode; // @public (undocumented) type RefetchWritePolicy = "merge" | "overwrite"; +// @public (undocumented) +type RemoveFragmentName = T extends any ? Omit : T; + +// @public (undocumented) +type RemoveMaskedMarker = Omit; + // @public (undocumented) type RequestHandler = (operation: Operation, forward: NextLink) => Observable | null; @@ -1801,7 +1885,7 @@ interface SubscriptionOptions { // @public (undocumented) interface SubscriptionResult { - data?: TData; + data?: MaybeMasked; error?: ApolloError; loading: boolean; // @internal (undocumented) @@ -1849,6 +1933,8 @@ interface TransformCacheEntry { // (undocumented) hasNonreactiveDirective: boolean; // (undocumented) + nonReactiveQuery: DocumentNode; + // (undocumented) serverQuery: DocumentNode | null; } @@ -1859,18 +1945,35 @@ type TransformFn = (document: DocumentNode) => DocumentNode; type UnionForAny = T extends never ? "a" : 1; // @public (undocumented) -type UnionToIntersection = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never; +type UnionToIntersection = (U extends any ? (x: U) => void : never) extends (x: infer I) => void ? I : never; + +// @public (undocumented) +type UnionToIntersection_2 = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never; + +// @public +type Unmasked = TData extends object ? UnwrapFragmentRefs>> : TData; + +// Warning: (ae-forgotten-export) The symbol "CombineFragmentRefs" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type UnwrapFragmentRefs = string extends keyof NonNullable ? TData : " $fragmentRefs" extends keyof NonNullable ? TData extends { + " $fragmentRefs"?: infer FragmentRefs extends object; +} ? Prettify<{ + [K in keyof TData as K extends " $fragmentRefs" ? never : K]: UnwrapFragmentRefs; +} & CombineFragmentRefs> : never : TData extends object ? { + [K in keyof TData]: UnwrapFragmentRefs; +} : TData; // @public (undocumented) type UpdateQueries = MutationOptions["updateQueries"]; // @public (undocumented) -type UpdateQueryFn = (previousQueryResult: TData, options: { +type UpdateQueryFn = (previousQueryResult: Unmasked, options: { subscriptionData: { - data: TSubscriptionData; + data: Unmasked; }; variables?: TSubscriptionVariables; -}) => TData; +}) => Unmasked; // @public (undocumented) interface UriFunction { @@ -1965,19 +2068,21 @@ export function useFragment(options: Us // @public (undocumented) export interface UseFragmentOptions extends Omit, NoInfer_2>, "id" | "query" | "optimistic" | "previousResult" | "returnPartialData">, Omit, "id" | "variables" | "returnPartialData"> { client?: ApolloClient; + // Warning: (ae-forgotten-export) The symbol "FragmentType" needs to be exported by the entry point index.d.ts + // // (undocumented) - from: StoreObject | Reference | string; + from: StoreObject | Reference | FragmentType> | string; // (undocumented) optimistic?: boolean; } // @public (undocumented) export type UseFragmentResult = { - data: TData; + data: MaybeMasked; complete: true; missing?: never; } | { - data: DeepPartial; + data: DeepPartial>; complete: false; missing?: MissingTree; }; @@ -2052,7 +2157,7 @@ export function useReadQuery(queryRef: QueryRef): UseReadQueryResu // @public (undocumented) export interface UseReadQueryResult { - data: TData; + data: MaybeMasked; error: ApolloError | undefined; networkStatus: NetworkStatus; } @@ -2063,7 +2168,7 @@ export interface UseReadQueryResult { export function useSubscription(subscription: DocumentNode | TypedDocumentNode, options?: SubscriptionHookOptions, NoInfer_2>): { restart: () => void; loading: boolean; - data?: TData | undefined; + data?: MaybeMasked | undefined; error?: ApolloError; variables?: TVariables | undefined; }; @@ -2116,7 +2221,7 @@ export interface UseSuspenseQueryResult; // (undocumented) - data: TData; + data: MaybeMasked; // (undocumented) error: ApolloError | undefined; // (undocumented) @@ -2133,18 +2238,18 @@ export interface UseSuspenseQueryResult { fragment: DocumentNode | TypedDocumentNode; fragmentName?: string; - from: StoreObject | Reference | string; + from: StoreObject | Reference | FragmentType> | string; optimistic?: boolean; variables?: TVars; } // @public type WatchFragmentResult = { - data: TData; + data: MaybeMasked; complete: true; missing?: never; } | { - data: DeepPartial; + data: DeepPartial>; complete: false; missing: MissingTree; }; @@ -2159,20 +2264,20 @@ interface WatchQueryOptions implements DataProxy { abstract evict(options: Cache_2.EvictOptions): boolean; abstract extract(optimistic?: boolean): TSerialized; // (undocumented) + fragmentMatches?(fragment: InlineFragmentNode, typename: string): boolean; + // (undocumented) gc(): string[]; // Warning: (ae-forgotten-export) The symbol "getApolloCacheMemoryInternals" needs to be exported by the entry point index.d.ts // @@ -48,19 +51,22 @@ abstract class ApolloCache implements DataProxy { // (undocumented) identify(object: StoreObject | Reference): string | undefined; // (undocumented) + lookupFragment(fragmentName: string): FragmentDefinitionNode | null; + // (undocumented) modify = Record>(options: Cache_2.ModifyOptions): boolean; // Warning: (ae-forgotten-export) The symbol "Transaction" needs to be exported by the entry point index.d.ts // // (undocumented) abstract performTransaction(transaction: Transaction, optimisticId?: string | null): void; // Warning: (ae-forgotten-export) The symbol "Cache_2" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "Unmasked" needs to be exported by the entry point index.d.ts // // (undocumented) - abstract read(query: Cache_2.ReadOptions): TData | null; + abstract read(query: Cache_2.ReadOptions): Unmasked | null; // (undocumented) - readFragment(options: Cache_2.ReadFragmentOptions, optimistic?: boolean): FragmentType | null; + readFragment(options: Cache_2.ReadFragmentOptions, optimistic?: boolean): Unmasked | null; // (undocumented) - readQuery(options: Cache_2.ReadQueryOptions, optimistic?: boolean): QueryType | null; + readQuery(options: Cache_2.ReadQueryOptions, optimistic?: boolean): Unmasked | null; // (undocumented) recordOptimisticTransaction(transaction: Transaction, optimisticId: string): void; // (undocumented) @@ -73,9 +79,9 @@ abstract class ApolloCache implements DataProxy { // (undocumented) transformForLink(document: DocumentNode): DocumentNode; // (undocumented) - updateFragment(options: Cache_2.UpdateFragmentOptions, update: (data: TData | null) => TData | null | void): TData | null; + updateFragment(options: Cache_2.UpdateFragmentOptions, update: (data: Unmasked | null) => Unmasked | null | void): Unmasked | null; // (undocumented) - updateQuery(options: Cache_2.UpdateQueryOptions, update: (data: TData | null) => TData | null | void): TData | null; + updateQuery(options: Cache_2.UpdateQueryOptions, update: (data: Unmasked | null) => Unmasked | null | void): Unmasked | null; // (undocumented) abstract watch(watch: Cache_2.WatchOptions): () => void; // Warning: (ae-forgotten-export) The symbol "OperationVariables" needs to be exported by the entry point index.d.ts @@ -133,16 +139,17 @@ class ApolloClient implements DataProxy { // Warning: (ae-forgotten-export) The symbol "DefaultContext" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "MutationOptions" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "FetchResult" needs to be exported by the entry point index.d.ts - mutate = DefaultContext, TCache extends ApolloCache = ApolloCache>(options: MutationOptions): Promise>; + mutate = DefaultContext, TCache extends ApolloCache = ApolloCache>(options: MutationOptions): Promise>>; onClearStore(cb: () => Promise): () => void; onResetStore(cb: () => Promise): () => void; // Warning: (ae-forgotten-export) The symbol "QueryOptions" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "ApolloQueryResult" needs to be exported by the entry point index.d.ts - query(options: QueryOptions): Promise>; + // Warning: (ae-forgotten-export) The symbol "MaybeMasked" needs to be exported by the entry point index.d.ts + query(options: QueryOptions): Promise>>; // (undocumented) queryDeduplication: boolean; - readFragment(options: DataProxy.Fragment, optimistic?: boolean): T | null; - readQuery(options: DataProxy.Query, optimistic?: boolean): T | null; + readFragment(options: DataProxy.Fragment, optimistic?: boolean): Unmasked | null; + readQuery(options: DataProxy.Query, optimistic?: boolean): Unmasked | null; reFetchObservableQueries(includeStandby?: boolean): Promise[]>; // Warning: (ae-forgotten-export) The symbol "RefetchQueriesOptions" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "RefetchQueriesResult" needs to be exported by the entry point index.d.ts @@ -155,7 +162,7 @@ class ApolloClient implements DataProxy { setResolvers(resolvers: Resolvers | Resolvers[]): void; stop(): void; // Warning: (ae-forgotten-export) The symbol "SubscriptionOptions" needs to be exported by the entry point index.d.ts - subscribe(options: SubscriptionOptions): Observable>; + subscribe(options: SubscriptionOptions): Observable>>; // Warning: (ae-forgotten-export) The symbol "ApolloClientOptions" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -178,6 +185,7 @@ interface ApolloClientOptions { connectToDevTools?: boolean; // (undocumented) credentials?: string; + dataMasking?: boolean; // (undocumented) defaultContext?: Partial; defaultOptions?: DefaultOptions; @@ -439,7 +447,7 @@ namespace Cache_2 { // (undocumented) dataId?: string; // (undocumented) - result: TResult; + result: Unmasked; } import DiffResult = DataProxy.DiffResult; import ReadQueryOptions = DataProxy.ReadQueryOptions; @@ -473,6 +481,15 @@ const enum CacheWriteBehavior { // @public (undocumented) type CanReadFunction = (value: StoreValue) => boolean; +// Warning: (ae-forgotten-export) The symbol "UnionToIntersection" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "UnwrapFragmentRefs" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "RemoveFragmentName" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type CombineFragmentRefs> = UnionToIntersection<{ + [K in keyof FragmentRefs]-?: UnwrapFragmentRefs>; +}[keyof FragmentRefs]>; + // @public (undocumented) class Concast extends Observable { // Warning: (ae-forgotten-export) The symbol "MaybeAsync" needs to be exported by the entry point index.d.ts @@ -502,6 +519,10 @@ type ConcastSourcesIterable = Iterable>; // @public function createQueryPreloader(client: ApolloClient): PreloadQueryFunction; +// @public (undocumented) +interface DataMasking { +} + // @public (undocumented) namespace DataProxy { // (undocumented) @@ -560,7 +581,7 @@ namespace DataProxy { // (undocumented) interface WriteOptions { broadcast?: boolean; - data: TData; + data: Unmasked; overwrite?: boolean; } // Warning: (ae-forgotten-export) The symbol "DataProxy" needs to be exported by the entry point index.d.ts @@ -572,8 +593,8 @@ namespace DataProxy { // @public interface DataProxy { - readFragment(options: DataProxy.ReadFragmentOptions, optimistic?: boolean): FragmentType | null; - readQuery(options: DataProxy.ReadQueryOptions, optimistic?: boolean): QueryType | null; + readFragment(options: DataProxy.ReadFragmentOptions, optimistic?: boolean): Unmasked | null; + readQuery(options: DataProxy.ReadQueryOptions, optimistic?: boolean): Unmasked | null; writeFragment(options: DataProxy.WriteFragmentOptions): Reference | undefined; writeQuery(options: DataProxy.WriteQueryOptions): Reference | undefined; } @@ -721,11 +742,11 @@ interface ExecutionPatchResultBase { // // @public (undocumented) type FetchMoreFunction = (fetchMoreOptions: FetchMoreQueryOptions & { - updateQuery?: (previousQueryResult: TData, options: { - fetchMoreResult: TData; + updateQuery?: (previousQueryResult: Unmasked, options: { + fetchMoreResult: Unmasked; variables: TVariables; - }) => TData; -}) => Promise>; + }) => Unmasked; +}) => Promise>>; // @public (undocumented) type FetchMoreOptions = Parameters["fetchMore"]>[0]; @@ -770,6 +791,17 @@ interface FragmentMap { // @public (undocumented) type FragmentMatcher = (rootValue: any, typeCondition: string, context: any) => boolean; +// @public (undocumented) +type FragmentType = [ +TData +] extends [{ + " $fragmentName"?: infer TKey; +}] ? TKey extends string ? { + " $fragmentRefs"?: { + [key in TKey]: TData; + }; +} : never : never; + // @public (undocumented) interface FulfilledPromise extends Promise { // (undocumented) @@ -893,7 +925,7 @@ export class InternalQueryReference { // Warning: (ae-forgotten-export) The symbol "FetchMoreOptions" needs to be exported by the entry point index.d.ts // // (undocumented) - fetchMore(options: FetchMoreOptions): Promise>; + fetchMore(options: FetchMoreOptions): Promise>>; // (undocumented) readonly key: QueryKey; // Warning: (ae-forgotten-export) The symbol "Listener" needs to be exported by the entry point index.d.ts @@ -905,11 +937,11 @@ export class InternalQueryReference { // (undocumented) promise: QueryRefPromise; // (undocumented) - refetch(variables: OperationVariables | undefined): Promise>; + refetch(variables: OperationVariables | undefined): Promise>>; // (undocumented) reinitialize(): void; // (undocumented) - result: ApolloQueryResult; + result: ApolloQueryResult>; // (undocumented) retain(): () => void; // (undocumented) @@ -967,11 +999,11 @@ const _invalidateModifier: unique symbol; // @public (undocumented) function isReference(obj: any): obj is Reference; -// Warning: (ae-forgotten-export) The symbol "UnionToIntersection" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "UnionToIntersection_2" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "UnionForAny" needs to be exported by the entry point index.d.ts // // @public (undocumented) -type IsStrictlyAny = UnionToIntersection> extends never ? true : false; +type IsStrictlyAny = UnionToIntersection_2> extends never ? true : false; // @public (undocumented) type Listener = (promise: QueryRefPromise) => void; @@ -1021,9 +1053,38 @@ type LocalStateOptions = { fragmentMatcher?: FragmentMatcher; }; +// @public (undocumented) +interface MaskFragmentOptions { + // (undocumented) + data: TData; + // (undocumented) + fragment: DocumentNode; + // (undocumented) + fragmentName?: string; +} + +// @public (undocumented) +interface MaskOperationOptions { + // (undocumented) + data: TData; + // (undocumented) + document: DocumentNode; +} + // @public (undocumented) type MaybeAsync = T | PromiseLike; +// Warning: (ae-forgotten-export) The symbol "Prettify" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "RemoveMaskedMarker" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "DataMasking" needs to be exported by the entry point index.d.ts +// +// @public +type MaybeMasked = TData extends { + __masked?: true; +} ? Prettify> : DataMasking extends { + enabled: true; +} ? TData : Unmasked; + // @public (undocumented) class MissingFieldError extends Error { constructor(message: string, path: MissingTree | Array, query: DocumentNode, variables?: Record | undefined); @@ -1080,10 +1141,10 @@ interface MutationBaseOptions; // Warning: (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts - optimisticResponse?: TData | ((vars: TVariables, { IGNORE }: { + optimisticResponse?: Unmasked> | ((vars: TVariables, { IGNORE }: { IGNORE: IgnoreModifier; - }) => TData | IgnoreModifier); - refetchQueries?: ((result: FetchResult) => InternalRefetchQueriesInclude) | InternalRefetchQueriesInclude; + }) => Unmasked> | IgnoreModifier); + refetchQueries?: ((result: FetchResult>) => InternalRefetchQueriesInclude) | InternalRefetchQueriesInclude; // Warning: (ae-forgotten-export) The symbol "MutationUpdaterFunction" needs to be exported by the entry point index.d.ts update?: MutationUpdaterFunction; // Warning: (ae-forgotten-export) The symbol "MutationQueryReducersMap" needs to be exported by the entry point index.d.ts @@ -1105,7 +1166,7 @@ interface MutationOptions = (previousResult: Record, options: { - mutationResult: FetchResult; + mutationResult: FetchResult>; queryName: string | undefined; queryVariables: Record; }) => Record; @@ -1139,7 +1200,7 @@ interface MutationStoreValue { } // @public (undocumented) -type MutationUpdaterFunction> = (cache: TCache, result: Omit, "context">, options: { +type MutationUpdaterFunction> = (cache: TCache, result: Omit>, "context">, options: { context?: TContext; variables?: TVariables; }) => void; @@ -1177,20 +1238,20 @@ type NextResultListener = (method: "next" | "error" | "complete", arg?: any) => type NoInfer_2 = [T][T extends any ? 0 : never]; // @public (undocumented) -class ObservableQuery extends Observable> { +class ObservableQuery extends Observable>> { constructor({ queryManager, queryInfo, options, }: { queryManager: QueryManager; queryInfo: QueryInfo; options: WatchQueryOptions; }); fetchMore(fetchMoreOptions: FetchMoreQueryOptions & { - updateQuery?: (previousQueryResult: TData, options: { - fetchMoreResult: TFetchData; + updateQuery?: (previousQueryResult: Unmasked, options: { + fetchMoreResult: Unmasked; variables: TFetchVars; - }) => TData; - }): Promise>; + }) => Unmasked; + }): Promise>>; // (undocumented) - getCurrentResult(saveAsLastResult?: boolean): ApolloQueryResult; + getCurrentResult(saveAsLastResult?: boolean): ApolloQueryResult>; // (undocumented) getLastError(variablesMustMatch?: boolean): ApolloError | undefined; // (undocumented) @@ -1207,9 +1268,9 @@ class ObservableQuery): Promise>; + refetch(variables?: Partial): Promise>>; // (undocumented) - reobserve(newOptions?: Partial>, newNetworkStatus?: NetworkStatus): Promise>; + reobserve(newOptions?: Partial>, newNetworkStatus?: NetworkStatus): Promise>>; // Warning: (ae-forgotten-export) The symbol "Concast" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1221,39 +1282,39 @@ class ObservableQuery) => void, onError?: (error: any) => void, onComplete?: () => void): Subscription; + resubscribeAfterError(onNext: (value: ApolloQueryResult>) => void, onError?: (error: any) => void, onComplete?: () => void): Subscription; // (undocumented) resubscribeAfterError(observer: Observer>): Subscription; // (undocumented) - result(): Promise>; + result(): Promise>>; // (undocumented) - setOptions(newOptions: Partial>): Promise>; - setVariables(variables: TVariables): Promise | void>; + setOptions(newOptions: Partial>): Promise>>; + setVariables(variables: TVariables): Promise> | void>; // (undocumented) silentSetOptions(newOptions: Partial>): void; startPolling(pollInterval: number): void; stopPolling(): void; // Warning: (ae-forgotten-export) The symbol "SubscribeToMoreOptions" needs to be exported by the entry point index.d.ts subscribeToMore(options: SubscribeToMoreOptions): () => void; - updateQuery(mapFn: (previousQueryResult: TData, options: Pick, "variables">) => TData): void; + updateQuery(mapFn: (previousQueryResult: Unmasked, options: Pick, "variables">) => Unmasked): void; get variables(): TVariables | undefined; } // @public (undocumented) interface ObservableQueryFields { fetchMore: (fetchMoreOptions: FetchMoreQueryOptions & { - updateQuery?: (previousQueryResult: TData, options: { - fetchMoreResult: TFetchData; + updateQuery?: (previousQueryResult: Unmasked, options: { + fetchMoreResult: Unmasked; variables: TFetchVars; - }) => TData; - }) => Promise>; - refetch: (variables?: Partial) => Promise>; + }) => Unmasked; + }) => Promise>>; + refetch: (variables?: Partial) => Promise>>; // @internal (undocumented) - reobserve: (newOptions?: Partial>, newNetworkStatus?: NetworkStatus) => Promise>; + reobserve: (newOptions?: Partial>, newNetworkStatus?: NetworkStatus) => Promise>>; startPolling: (pollInterval: number) => void; stopPolling: () => void; subscribeToMore: (options: SubscribeToMoreOptions) => () => void; - updateQuery: (mapFn: (previousQueryResult: TData, options: Pick, "variables">) => TData) => void; + updateQuery: (mapFn: (previousQueryResult: Unmasked, options: Pick, "variables">) => Unmasked) => void; variables: TVariables | undefined; } @@ -1353,6 +1414,11 @@ options?: PreloadQueryOptions> & Omit> & Omit ]; +// @public (undocumented) +type Prettify = { + [K in keyof T]: T[K]; +} & {}; + // @public (undocumented) type Primitive = null | undefined | string | number | boolean | symbol | bigint; @@ -1378,7 +1444,7 @@ const QUERY_REFERENCE_SYMBOL: unique symbol; interface QueryFunctionOptions extends BaseQueryOptions { // @internal (undocumented) defaultOptions?: Partial>; - onCompleted?: (data: TData) => void; + onCompleted?: (data: MaybeMasked) => void; onError?: (error: ApolloError) => void; skip?: boolean; } @@ -1470,6 +1536,8 @@ class QueryManager { // (undocumented) clearStore(options?: Cache_2.ResetOptions): Promise; // (undocumented) + readonly dataMasking: boolean; + // (undocumented) readonly defaultContext: Partial; // Warning: (ae-forgotten-export) The symbol "DefaultOptions" needs to be exported by the entry point index.d.ts // @@ -1536,14 +1604,22 @@ class QueryManager { onQueryUpdated?: OnQueryUpdated; keepRootFields?: boolean; }, cache?: ApolloCache): Promise>; + // Warning: (ae-forgotten-export) The symbol "MaskFragmentOptions" needs to be exported by the entry point index.d.ts + // // (undocumented) - mutate, TCache extends ApolloCache>({ mutation, variables, optimisticResponse, updateQueries, refetchQueries, awaitRefetchQueries, update: updateWithProxyFn, onQueryUpdated, fetchPolicy, errorPolicy, keepRootFields, context, }: MutationOptions): Promise>; + maskFragment(options: MaskFragmentOptions): TData; + // Warning: (ae-forgotten-export) The symbol "MaskOperationOptions" needs to be exported by the entry point index.d.ts + // + // (undocumented) + maskOperation(options: MaskOperationOptions): MaybeMasked; + // (undocumented) + mutate, TCache extends ApolloCache>({ mutation, variables, optimisticResponse, updateQueries, refetchQueries, awaitRefetchQueries, update: updateWithProxyFn, onQueryUpdated, fetchPolicy, errorPolicy, keepRootFields, context, }: MutationOptions): Promise>>; // (undocumented) mutationStore?: { [mutationId: string]: MutationStoreValue; }; // (undocumented) - query(options: QueryOptions, queryId?: string): Promise>; + query(options: QueryOptions, queryId?: string): Promise>>; // (undocumented) reFetchObservableQueries(includeStandby?: boolean): Promise[]>; // Warning: (ae-forgotten-export) The symbol "InternalRefetchQueriesOptions" needs to be exported by the entry point index.d.ts @@ -1560,7 +1636,7 @@ class QueryManager { // (undocumented) readonly ssrMode: boolean; // (undocumented) - startGraphQLSubscription({ query, fetchPolicy, errorPolicy, variables, context, extensions, }: SubscriptionOptions): Observable>; + startGraphQLSubscription(options: SubscriptionOptions): Observable>; stop(): void; // (undocumented) stopQuery(queryId: string): void; @@ -1581,6 +1657,8 @@ interface QueryManagerOptions { // (undocumented) clientAwareness: Record; // (undocumented) + dataMasking: boolean; + // (undocumented) defaultContext: Partial | undefined; // (undocumented) defaultOptions: DefaultOptions; @@ -1629,7 +1707,7 @@ export interface QueryReference extends Q // Warning: (ae-forgotten-export) The symbol "PromiseWithState" needs to be exported by the entry point index.d.ts // // @public (undocumented) -type QueryRefPromise = PromiseWithState>; +type QueryRefPromise = PromiseWithState>>; // Warning: (ae-forgotten-export) The symbol "ObservableQueryFields" needs to be exported by the entry point index.d.ts // @@ -1637,14 +1715,14 @@ type QueryRefPromise = PromiseWithState>; interface QueryResult extends ObservableQueryFields { called: boolean; client: ApolloClient; - data: TData | undefined; + data: MaybeMasked | undefined; error?: ApolloError; // @deprecated (undocumented) errors?: ReadonlyArray; loading: boolean; networkStatus: NetworkStatus; observable: ObservableQuery; - previousData?: TData; + previousData?: MaybeMasked; } // @public (undocumented) @@ -1725,6 +1803,12 @@ interface RejectedPromise extends Promise { status: "rejected"; } +// @public (undocumented) +type RemoveFragmentName = T extends any ? Omit : T; + +// @public (undocumented) +type RemoveMaskedMarker = Omit; + // @public (undocumented) type RequestHandler = (operation: Operation, forward: NextLink) => Observable | null; @@ -1901,6 +1985,8 @@ interface TransformCacheEntry { // (undocumented) hasNonreactiveDirective: boolean; // (undocumented) + nonReactiveQuery: DocumentNode; + // (undocumented) serverQuery: DocumentNode | null; } @@ -1911,7 +1997,24 @@ type TransformFn = (document: DocumentNode) => DocumentNode; type UnionForAny = T extends never ? "a" : 1; // @public (undocumented) -type UnionToIntersection = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never; +type UnionToIntersection = (U extends any ? (x: U) => void : never) extends (x: infer I) => void ? I : never; + +// @public (undocumented) +type UnionToIntersection_2 = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never; + +// @public +type Unmasked = TData extends object ? UnwrapFragmentRefs>> : TData; + +// Warning: (ae-forgotten-export) The symbol "CombineFragmentRefs" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type UnwrapFragmentRefs = string extends keyof NonNullable ? TData : " $fragmentRefs" extends keyof NonNullable ? TData extends { + " $fragmentRefs"?: infer FragmentRefs extends object; +} ? Prettify<{ + [K in keyof TData as K extends " $fragmentRefs" ? never : K]: UnwrapFragmentRefs; +} & CombineFragmentRefs> : never : TData extends object ? { + [K in keyof TData]: UnwrapFragmentRefs; +} : TData; // @public (undocumented) export function unwrapQueryRef(queryRef: WrappedQueryRef): InternalQueryReference; @@ -1923,12 +2026,12 @@ export function unwrapQueryRef(queryRef: Partial>) type UpdateQueries = MutationOptions["updateQueries"]; // @public (undocumented) -type UpdateQueryFn = (previousQueryResult: TData, options: { +type UpdateQueryFn = (previousQueryResult: Unmasked, options: { subscriptionData: { - data: TSubscriptionData; + data: Unmasked; }; variables?: TSubscriptionVariables; -}) => TData; +}) => Unmasked; // @public (undocumented) export function updateWrappedQueryRef(queryRef: WrappedQueryRef, promise: QueryRefPromise): void; @@ -2028,19 +2131,21 @@ function useFragment(options: UseFragme // @public (undocumented) interface UseFragmentOptions extends Omit, NoInfer_2>, "id" | "query" | "optimistic" | "previousResult" | "returnPartialData">, Omit, "id" | "variables" | "returnPartialData"> { client?: ApolloClient; + // Warning: (ae-forgotten-export) The symbol "FragmentType" needs to be exported by the entry point index.d.ts + // // (undocumented) - from: StoreObject | Reference | string; + from: StoreObject | Reference | FragmentType> | string; // (undocumented) optimistic?: boolean; } // @public (undocumented) type UseFragmentResult = { - data: TData; + data: MaybeMasked; complete: true; missing?: never; } | { - data: DeepPartial; + data: DeepPartial>; complete: false; missing?: MissingTree; }; @@ -2069,7 +2174,7 @@ function useReadQuery(queryRef: QueryRef): UseReadQueryResult { - data: TData; + data: MaybeMasked; error: ApolloError | undefined; networkStatus: NetworkStatus; } @@ -2123,7 +2228,7 @@ interface UseSuspenseQueryResult; // (undocumented) - data: TData; + data: MaybeMasked; // (undocumented) error: ApolloError | undefined; // (undocumented) @@ -2151,18 +2256,18 @@ TVariables interface WatchFragmentOptions { fragment: DocumentNode | TypedDocumentNode; fragmentName?: string; - from: StoreObject | Reference | string; + from: StoreObject | Reference | FragmentType> | string; optimistic?: boolean; variables?: TVars; } // @public type WatchFragmentResult = { - data: TData; + data: MaybeMasked; complete: true; missing?: never; } | { - data: DeepPartial; + data: DeepPartial>; complete: false; missing: MissingTree; }; @@ -2222,20 +2327,20 @@ export function wrapQueryRef(inter // Warnings were encountered during analysis: // -// src/cache/core/types/DataProxy.ts:146:7 - (ae-forgotten-export) The symbol "MissingFieldError" needs to be exported by the entry point index.d.ts +// src/cache/core/types/DataProxy.ts:147:7 - (ae-forgotten-export) The symbol "MissingFieldError" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:101:3 - (ae-forgotten-export) The symbol "ReadFieldFunction" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:102:3 - (ae-forgotten-export) The symbol "CanReadFunction" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:103:3 - (ae-forgotten-export) The symbol "isReference" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:104:3 - (ae-forgotten-export) The symbol "ToReferenceFunction" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:105:3 - (ae-forgotten-export) The symbol "StorageType" needs to be exported by the entry point index.d.ts // src/core/LocalState.ts:46:5 - (ae-forgotten-export) The symbol "FragmentMap" needs to be exported by the entry point index.d.ts -// src/core/ObservableQuery.ts:116:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts -// src/core/ObservableQuery.ts:117:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:138:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:382:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts -// src/core/types.ts:174:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts -// src/core/types.ts:203:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:275:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:120:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:121:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:155:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:408:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts +// src/core/types.ts:175:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts +// src/core/types.ts:204:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:277:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts // src/react/hooks/useBackgroundQuery.ts:38:3 - (ae-forgotten-export) The symbol "SubscribeToMoreFunction" needs to be exported by the entry point index.d.ts // src/react/hooks/useBackgroundQuery.ts:54:3 - (ae-forgotten-export) The symbol "FetchMoreFunction" needs to be exported by the entry point index.d.ts // src/react/hooks/useBackgroundQuery.ts:78:4 - (ae-forgotten-export) The symbol "RefetchFunction" needs to be exported by the entry point index.d.ts diff --git a/.api-reports/api-report-react_ssr.api.md b/.api-reports/api-report-react_ssr.api.md index 69841fbd241..95c4d46cd9f 100644 --- a/.api-reports/api-report-react_ssr.api.md +++ b/.api-reports/api-report-react_ssr.api.md @@ -11,6 +11,7 @@ import type { FormattedExecutionResult } from 'graphql'; import type { FragmentDefinitionNode } from 'graphql'; import type { GraphQLErrorExtensions } from 'graphql'; import type { GraphQLFormattedError } from 'graphql'; +import type { InlineFragmentNode } from 'graphql'; import { Observable } from 'zen-observable-ts'; import type { Observer } from 'zen-observable-ts'; import type * as ReactTypes from 'react'; @@ -39,6 +40,8 @@ abstract class ApolloCache implements DataProxy { abstract evict(options: Cache_2.EvictOptions): boolean; abstract extract(optimistic?: boolean): TSerialized; // (undocumented) + fragmentMatches?(fragment: InlineFragmentNode, typename: string): boolean; + // (undocumented) gc(): string[]; // Warning: (ae-forgotten-export) The symbol "getApolloCacheMemoryInternals" needs to be exported by the entry point index.d.ts // @@ -49,19 +52,22 @@ abstract class ApolloCache implements DataProxy { // (undocumented) identify(object: StoreObject | Reference): string | undefined; // (undocumented) + lookupFragment(fragmentName: string): FragmentDefinitionNode | null; + // (undocumented) modify = Record>(options: Cache_2.ModifyOptions): boolean; // Warning: (ae-forgotten-export) The symbol "Transaction" needs to be exported by the entry point index.d.ts // // (undocumented) abstract performTransaction(transaction: Transaction, optimisticId?: string | null): void; // Warning: (ae-forgotten-export) The symbol "Cache_2" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "Unmasked" needs to be exported by the entry point index.d.ts // // (undocumented) - abstract read(query: Cache_2.ReadOptions): TData | null; + abstract read(query: Cache_2.ReadOptions): Unmasked | null; // (undocumented) - readFragment(options: Cache_2.ReadFragmentOptions, optimistic?: boolean): FragmentType | null; + readFragment(options: Cache_2.ReadFragmentOptions, optimistic?: boolean): Unmasked | null; // (undocumented) - readQuery(options: Cache_2.ReadQueryOptions, optimistic?: boolean): QueryType | null; + readQuery(options: Cache_2.ReadQueryOptions, optimistic?: boolean): Unmasked | null; // (undocumented) recordOptimisticTransaction(transaction: Transaction, optimisticId: string): void; // (undocumented) @@ -74,9 +80,9 @@ abstract class ApolloCache implements DataProxy { // (undocumented) transformForLink(document: DocumentNode): DocumentNode; // (undocumented) - updateFragment(options: Cache_2.UpdateFragmentOptions, update: (data: TData | null) => TData | null | void): TData | null; + updateFragment(options: Cache_2.UpdateFragmentOptions, update: (data: Unmasked | null) => Unmasked | null | void): Unmasked | null; // (undocumented) - updateQuery(options: Cache_2.UpdateQueryOptions, update: (data: TData | null) => TData | null | void): TData | null; + updateQuery(options: Cache_2.UpdateQueryOptions, update: (data: Unmasked | null) => Unmasked | null | void): Unmasked | null; // (undocumented) abstract watch(watch: Cache_2.WatchOptions): () => void; // Warning: (ae-forgotten-export) The symbol "OperationVariables" needs to be exported by the entry point index.d.ts @@ -134,16 +140,17 @@ class ApolloClient implements DataProxy { // Warning: (ae-forgotten-export) The symbol "DefaultContext" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "MutationOptions" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "FetchResult" needs to be exported by the entry point index.d.ts - mutate = DefaultContext, TCache extends ApolloCache = ApolloCache>(options: MutationOptions): Promise>; + mutate = DefaultContext, TCache extends ApolloCache = ApolloCache>(options: MutationOptions): Promise>>; onClearStore(cb: () => Promise): () => void; onResetStore(cb: () => Promise): () => void; // Warning: (ae-forgotten-export) The symbol "QueryOptions" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "ApolloQueryResult" needs to be exported by the entry point index.d.ts - query(options: QueryOptions): Promise>; + // Warning: (ae-forgotten-export) The symbol "MaybeMasked" needs to be exported by the entry point index.d.ts + query(options: QueryOptions): Promise>>; // (undocumented) queryDeduplication: boolean; - readFragment(options: DataProxy.Fragment, optimistic?: boolean): T | null; - readQuery(options: DataProxy.Query, optimistic?: boolean): T | null; + readFragment(options: DataProxy.Fragment, optimistic?: boolean): Unmasked | null; + readQuery(options: DataProxy.Query, optimistic?: boolean): Unmasked | null; reFetchObservableQueries(includeStandby?: boolean): Promise[]>; // Warning: (ae-forgotten-export) The symbol "RefetchQueriesOptions" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "RefetchQueriesResult" needs to be exported by the entry point index.d.ts @@ -156,7 +163,7 @@ class ApolloClient implements DataProxy { setResolvers(resolvers: Resolvers | Resolvers[]): void; stop(): void; // Warning: (ae-forgotten-export) The symbol "SubscriptionOptions" needs to be exported by the entry point index.d.ts - subscribe(options: SubscriptionOptions): Observable>; + subscribe(options: SubscriptionOptions): Observable>>; // Warning: (ae-forgotten-export) The symbol "ApolloClientOptions" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -179,6 +186,7 @@ interface ApolloClientOptions { connectToDevTools?: boolean; // (undocumented) credentials?: string; + dataMasking?: boolean; // (undocumented) defaultContext?: Partial; defaultOptions?: DefaultOptions; @@ -407,7 +415,7 @@ namespace Cache_2 { // (undocumented) dataId?: string; // (undocumented) - result: TResult; + result: Unmasked; } import DiffResult = DataProxy.DiffResult; import ReadQueryOptions = DataProxy.ReadQueryOptions; @@ -434,6 +442,15 @@ const enum CacheWriteBehavior { // @public (undocumented) type CanReadFunction = (value: StoreValue) => boolean; +// Warning: (ae-forgotten-export) The symbol "UnionToIntersection" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "UnwrapFragmentRefs" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "RemoveFragmentName" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type CombineFragmentRefs> = UnionToIntersection<{ + [K in keyof FragmentRefs]-?: UnwrapFragmentRefs>; +}[keyof FragmentRefs]>; + // @public (undocumented) class Concast extends Observable { // Warning: (ae-forgotten-export) The symbol "MaybeAsync" needs to be exported by the entry point index.d.ts @@ -458,6 +475,10 @@ class Concast extends Observable { // @public (undocumented) type ConcastSourcesIterable = Iterable>; +// @public (undocumented) +interface DataMasking { +} + // @public (undocumented) namespace DataProxy { // (undocumented) @@ -516,7 +537,7 @@ namespace DataProxy { // (undocumented) interface WriteOptions { broadcast?: boolean; - data: TData; + data: Unmasked; overwrite?: boolean; } // Warning: (ae-forgotten-export) The symbol "DataProxy" needs to be exported by the entry point index.d.ts @@ -528,8 +549,8 @@ namespace DataProxy { // @public interface DataProxy { - readFragment(options: DataProxy.ReadFragmentOptions, optimistic?: boolean): FragmentType | null; - readQuery(options: DataProxy.ReadQueryOptions, optimistic?: boolean): QueryType | null; + readFragment(options: DataProxy.ReadFragmentOptions, optimistic?: boolean): Unmasked | null; + readQuery(options: DataProxy.ReadQueryOptions, optimistic?: boolean): Unmasked | null; writeFragment(options: DataProxy.WriteFragmentOptions): Reference | undefined; writeQuery(options: DataProxy.WriteQueryOptions): Reference | undefined; } @@ -713,6 +734,17 @@ interface FragmentMap { // @public (undocumented) type FragmentMatcher = (rootValue: any, typeCondition: string, context: any) => boolean; +// @public (undocumented) +type FragmentType = [ +TData +] extends [{ + " $fragmentName"?: infer TKey; +}] ? TKey extends string ? { + " $fragmentRefs"?: { + [key in TKey]: TData; + }; +} : never : never; + // @internal const getApolloCacheMemoryInternals: (() => { cache: { @@ -854,11 +886,11 @@ const _invalidateModifier: unique symbol; // @public (undocumented) function isReference(obj: any): obj is Reference; -// Warning: (ae-forgotten-export) The symbol "UnionToIntersection" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "UnionToIntersection_2" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "UnionForAny" needs to be exported by the entry point index.d.ts // // @public (undocumented) -type IsStrictlyAny = UnionToIntersection> extends never ? true : false; +type IsStrictlyAny = UnionToIntersection_2> extends never ? true : false; // @public (undocumented) class LocalState { @@ -905,9 +937,38 @@ type LocalStateOptions = { fragmentMatcher?: FragmentMatcher; }; +// @public (undocumented) +interface MaskFragmentOptions { + // (undocumented) + data: TData; + // (undocumented) + fragment: DocumentNode; + // (undocumented) + fragmentName?: string; +} + +// @public (undocumented) +interface MaskOperationOptions { + // (undocumented) + data: TData; + // (undocumented) + document: DocumentNode; +} + // @public (undocumented) type MaybeAsync = T | PromiseLike; +// Warning: (ae-forgotten-export) The symbol "Prettify" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "RemoveMaskedMarker" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "DataMasking" needs to be exported by the entry point index.d.ts +// +// @public +type MaybeMasked = TData extends { + __masked?: true; +} ? Prettify> : DataMasking extends { + enabled: true; +} ? TData : Unmasked; + // @public (undocumented) class MissingFieldError extends Error { constructor(message: string, path: MissingTree | Array, query: DocumentNode, variables?: Record | undefined); @@ -963,11 +1024,12 @@ interface MutationBaseOptions; + // Warning: (ae-forgotten-export) The symbol "NoInfer_2" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts - optimisticResponse?: TData | ((vars: TVariables, { IGNORE }: { + optimisticResponse?: Unmasked> | ((vars: TVariables, { IGNORE }: { IGNORE: IgnoreModifier; - }) => TData | IgnoreModifier); - refetchQueries?: ((result: FetchResult) => InternalRefetchQueriesInclude) | InternalRefetchQueriesInclude; + }) => Unmasked> | IgnoreModifier); + refetchQueries?: ((result: FetchResult>) => InternalRefetchQueriesInclude) | InternalRefetchQueriesInclude; // Warning: (ae-forgotten-export) The symbol "MutationUpdaterFunction" needs to be exported by the entry point index.d.ts update?: MutationUpdaterFunction; // Warning: (ae-forgotten-export) The symbol "MutationQueryReducersMap" needs to be exported by the entry point index.d.ts @@ -989,7 +1051,7 @@ interface MutationOptions = (previousResult: Record, options: { - mutationResult: FetchResult; + mutationResult: FetchResult>; queryName: string | undefined; queryVariables: Record; }) => Record; @@ -1023,7 +1085,7 @@ interface MutationStoreValue { } // @public (undocumented) -type MutationUpdaterFunction> = (cache: TCache, result: Omit, "context">, options: { +type MutationUpdaterFunction> = (cache: TCache, result: Omit>, "context">, options: { context?: TContext; variables?: TVariables; }) => void; @@ -1059,8 +1121,11 @@ type NextLink = (operation: Operation) => Observable; // @public (undocumented) type NextResultListener = (method: "next" | "error" | "complete", arg?: any) => any; +// @public +type NoInfer_2 = [T][T extends any ? 0 : never]; + // @public (undocumented) -class ObservableQuery extends Observable> { +class ObservableQuery extends Observable>> { constructor({ queryManager, queryInfo, options, }: { queryManager: QueryManager; queryInfo: QueryInfo; @@ -1068,13 +1133,13 @@ class ObservableQuery(fetchMoreOptions: FetchMoreQueryOptions & { - updateQuery?: (previousQueryResult: TData, options: { - fetchMoreResult: TFetchData; + updateQuery?: (previousQueryResult: Unmasked, options: { + fetchMoreResult: Unmasked; variables: TFetchVars; - }) => TData; - }): Promise>; + }) => Unmasked; + }): Promise>>; // (undocumented) - getCurrentResult(saveAsLastResult?: boolean): ApolloQueryResult; + getCurrentResult(saveAsLastResult?: boolean): ApolloQueryResult>; // (undocumented) getLastError(variablesMustMatch?: boolean): ApolloError | undefined; // (undocumented) @@ -1091,9 +1156,9 @@ class ObservableQuery): Promise>; + refetch(variables?: Partial): Promise>>; // (undocumented) - reobserve(newOptions?: Partial>, newNetworkStatus?: NetworkStatus): Promise>; + reobserve(newOptions?: Partial>, newNetworkStatus?: NetworkStatus): Promise>>; // Warning: (ae-forgotten-export) The symbol "Concast" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1105,39 +1170,39 @@ class ObservableQuery) => void, onError?: (error: any) => void, onComplete?: () => void): Subscription; + resubscribeAfterError(onNext: (value: ApolloQueryResult>) => void, onError?: (error: any) => void, onComplete?: () => void): Subscription; // (undocumented) resubscribeAfterError(observer: Observer>): Subscription; // (undocumented) - result(): Promise>; + result(): Promise>>; // (undocumented) - setOptions(newOptions: Partial>): Promise>; - setVariables(variables: TVariables): Promise | void>; + setOptions(newOptions: Partial>): Promise>>; + setVariables(variables: TVariables): Promise> | void>; // (undocumented) silentSetOptions(newOptions: Partial>): void; startPolling(pollInterval: number): void; stopPolling(): void; // Warning: (ae-forgotten-export) The symbol "SubscribeToMoreOptions" needs to be exported by the entry point index.d.ts subscribeToMore(options: SubscribeToMoreOptions): () => void; - updateQuery(mapFn: (previousQueryResult: TData, options: Pick, "variables">) => TData): void; + updateQuery(mapFn: (previousQueryResult: Unmasked, options: Pick, "variables">) => Unmasked): void; get variables(): TVariables | undefined; } // @public (undocumented) interface ObservableQueryFields { fetchMore: (fetchMoreOptions: FetchMoreQueryOptions & { - updateQuery?: (previousQueryResult: TData, options: { - fetchMoreResult: TFetchData; + updateQuery?: (previousQueryResult: Unmasked, options: { + fetchMoreResult: Unmasked; variables: TFetchVars; - }) => TData; - }) => Promise>; - refetch: (variables?: Partial) => Promise>; + }) => Unmasked; + }) => Promise>>; + refetch: (variables?: Partial) => Promise>>; // @internal (undocumented) - reobserve: (newOptions?: Partial>, newNetworkStatus?: NetworkStatus) => Promise>; + reobserve: (newOptions?: Partial>, newNetworkStatus?: NetworkStatus) => Promise>>; startPolling: (pollInterval: number) => void; stopPolling: () => void; subscribeToMore: (options: SubscribeToMoreOptions) => () => void; - updateQuery: (mapFn: (previousQueryResult: TData, options: Pick, "variables">) => TData) => void; + updateQuery: (mapFn: (previousQueryResult: Unmasked, options: Pick, "variables">) => Unmasked) => void; variables: TVariables | undefined; } @@ -1169,6 +1234,11 @@ type OperationVariables = Record; // @public (undocumented) type Path = ReadonlyArray; +// @public (undocumented) +type Prettify = { + [K in keyof T]: T[K]; +} & {}; + // @public (undocumented) type Primitive = null | undefined | string | number | boolean | symbol | bigint; @@ -1197,7 +1267,7 @@ interface QueryDataOptions extends BaseQueryOptions { // @internal (undocumented) defaultOptions?: Partial>; - onCompleted?: (data: TData) => void; + onCompleted?: (data: MaybeMasked) => void; onError?: (error: ApolloError) => void; skip?: boolean; } @@ -1277,6 +1347,8 @@ class QueryManager { // (undocumented) clearStore(options?: Cache_2.ResetOptions): Promise; // (undocumented) + readonly dataMasking: boolean; + // (undocumented) readonly defaultContext: Partial; // Warning: (ae-forgotten-export) The symbol "DefaultOptions" needs to be exported by the entry point index.d.ts // @@ -1343,14 +1415,22 @@ class QueryManager { onQueryUpdated?: OnQueryUpdated; keepRootFields?: boolean; }, cache?: ApolloCache): Promise>; + // Warning: (ae-forgotten-export) The symbol "MaskFragmentOptions" needs to be exported by the entry point index.d.ts + // // (undocumented) - mutate, TCache extends ApolloCache>({ mutation, variables, optimisticResponse, updateQueries, refetchQueries, awaitRefetchQueries, update: updateWithProxyFn, onQueryUpdated, fetchPolicy, errorPolicy, keepRootFields, context, }: MutationOptions): Promise>; + maskFragment(options: MaskFragmentOptions): TData; + // Warning: (ae-forgotten-export) The symbol "MaskOperationOptions" needs to be exported by the entry point index.d.ts + // + // (undocumented) + maskOperation(options: MaskOperationOptions): MaybeMasked; + // (undocumented) + mutate, TCache extends ApolloCache>({ mutation, variables, optimisticResponse, updateQueries, refetchQueries, awaitRefetchQueries, update: updateWithProxyFn, onQueryUpdated, fetchPolicy, errorPolicy, keepRootFields, context, }: MutationOptions): Promise>>; // (undocumented) mutationStore?: { [mutationId: string]: MutationStoreValue; }; // (undocumented) - query(options: QueryOptions, queryId?: string): Promise>; + query(options: QueryOptions, queryId?: string): Promise>>; // (undocumented) reFetchObservableQueries(includeStandby?: boolean): Promise[]>; // Warning: (ae-forgotten-export) The symbol "InternalRefetchQueriesOptions" needs to be exported by the entry point index.d.ts @@ -1367,7 +1447,7 @@ class QueryManager { // (undocumented) readonly ssrMode: boolean; // (undocumented) - startGraphQLSubscription({ query, fetchPolicy, errorPolicy, variables, context, extensions, }: SubscriptionOptions): Observable>; + startGraphQLSubscription(options: SubscriptionOptions): Observable>; stop(): void; // (undocumented) stopQuery(queryId: string): void; @@ -1388,6 +1468,8 @@ interface QueryManagerOptions { // (undocumented) clientAwareness: Record; // (undocumented) + dataMasking: boolean; + // (undocumented) defaultContext: Partial | undefined; // (undocumented) defaultOptions: DefaultOptions; @@ -1427,14 +1509,14 @@ interface QueryOptions { interface QueryResult extends ObservableQueryFields { called: boolean; client: ApolloClient; - data: TData | undefined; + data: MaybeMasked | undefined; error?: ApolloError; // @deprecated (undocumented) errors?: ReadonlyArray; loading: boolean; networkStatus: NetworkStatus; observable: ObservableQuery; - previousData?: TData; + previousData?: MaybeMasked; } // @public (undocumented) @@ -1504,6 +1586,12 @@ type RefetchQueryDescriptor = string | DocumentNode; // @public (undocumented) type RefetchWritePolicy = "merge" | "overwrite"; +// @public (undocumented) +type RemoveFragmentName = T extends any ? Omit : T; + +// @public (undocumented) +type RemoveMaskedMarker = Omit; + // @public (undocumented) export class RenderPromises { // (undocumented) @@ -1660,6 +1748,8 @@ interface TransformCacheEntry { // (undocumented) hasNonreactiveDirective: boolean; // (undocumented) + nonReactiveQuery: DocumentNode; + // (undocumented) serverQuery: DocumentNode | null; } @@ -1670,18 +1760,35 @@ type TransformFn = (document: DocumentNode) => DocumentNode; type UnionForAny = T extends never ? "a" : 1; // @public (undocumented) -type UnionToIntersection = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never; +type UnionToIntersection = (U extends any ? (x: U) => void : never) extends (x: infer I) => void ? I : never; + +// @public (undocumented) +type UnionToIntersection_2 = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never; + +// @public +type Unmasked = TData extends object ? UnwrapFragmentRefs>> : TData; + +// Warning: (ae-forgotten-export) The symbol "CombineFragmentRefs" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type UnwrapFragmentRefs = string extends keyof NonNullable ? TData : " $fragmentRefs" extends keyof NonNullable ? TData extends { + " $fragmentRefs"?: infer FragmentRefs extends object; +} ? Prettify<{ + [K in keyof TData as K extends " $fragmentRefs" ? never : K]: UnwrapFragmentRefs; +} & CombineFragmentRefs> : never : TData extends object ? { + [K in keyof TData]: UnwrapFragmentRefs; +} : TData; // @public (undocumented) type UpdateQueries = MutationOptions["updateQueries"]; // @public (undocumented) -type UpdateQueryFn = (previousQueryResult: TData, options: { +type UpdateQueryFn = (previousQueryResult: Unmasked, options: { subscriptionData: { - data: TSubscriptionData; + data: Unmasked; }; variables?: TSubscriptionVariables; -}) => TData; +}) => Unmasked; // @public (undocumented) interface UriFunction { @@ -1693,18 +1800,19 @@ interface UriFunction { interface WatchFragmentOptions { fragment: DocumentNode | TypedDocumentNode; fragmentName?: string; - from: StoreObject | Reference | string; + // Warning: (ae-forgotten-export) The symbol "FragmentType" needs to be exported by the entry point index.d.ts + from: StoreObject | Reference | FragmentType> | string; optimistic?: boolean; variables?: TVars; } // @public type WatchFragmentResult = { - data: TData; + data: MaybeMasked; complete: true; missing?: never; } | { - data: DeepPartial; + data: DeepPartial>; complete: false; missing: MissingTree; }; @@ -1719,20 +1827,20 @@ interface WatchQueryOptions implements DataProxy { abstract evict(options: Cache_2.EvictOptions): boolean; abstract extract(optimistic?: boolean): TSerialized; // (undocumented) + fragmentMatches?(fragment: InlineFragmentNode, typename: string): boolean; + // (undocumented) gc(): string[]; // Warning: (ae-forgotten-export) The symbol "getApolloCacheMemoryInternals" needs to be exported by the entry point index.d.ts // @@ -49,19 +52,22 @@ abstract class ApolloCache implements DataProxy { // (undocumented) identify(object: StoreObject | Reference): string | undefined; // (undocumented) + lookupFragment(fragmentName: string): FragmentDefinitionNode | null; + // (undocumented) modify = Record>(options: Cache_2.ModifyOptions): boolean; // Warning: (ae-forgotten-export) The symbol "Transaction" needs to be exported by the entry point index.d.ts // // (undocumented) abstract performTransaction(transaction: Transaction, optimisticId?: string | null): void; // Warning: (ae-forgotten-export) The symbol "Cache_2" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "Unmasked" needs to be exported by the entry point index.d.ts // // (undocumented) - abstract read(query: Cache_2.ReadOptions): TData | null; + abstract read(query: Cache_2.ReadOptions): Unmasked | null; // (undocumented) - readFragment(options: Cache_2.ReadFragmentOptions, optimistic?: boolean): FragmentType | null; + readFragment(options: Cache_2.ReadFragmentOptions, optimistic?: boolean): Unmasked | null; // (undocumented) - readQuery(options: Cache_2.ReadQueryOptions, optimistic?: boolean): QueryType | null; + readQuery(options: Cache_2.ReadQueryOptions, optimistic?: boolean): Unmasked | null; // (undocumented) recordOptimisticTransaction(transaction: Transaction, optimisticId: string): void; // (undocumented) @@ -74,9 +80,9 @@ abstract class ApolloCache implements DataProxy { // (undocumented) transformForLink(document: DocumentNode): DocumentNode; // (undocumented) - updateFragment(options: Cache_2.UpdateFragmentOptions, update: (data: TData | null) => TData | null | void): TData | null; + updateFragment(options: Cache_2.UpdateFragmentOptions, update: (data: Unmasked | null) => Unmasked | null | void): Unmasked | null; // (undocumented) - updateQuery(options: Cache_2.UpdateQueryOptions, update: (data: TData | null) => TData | null | void): TData | null; + updateQuery(options: Cache_2.UpdateQueryOptions, update: (data: Unmasked | null) => Unmasked | null | void): Unmasked | null; // (undocumented) abstract watch(watch: Cache_2.WatchOptions): () => void; // Warning: (ae-forgotten-export) The symbol "OperationVariables" needs to be exported by the entry point index.d.ts @@ -134,16 +140,17 @@ class ApolloClient implements DataProxy { // Warning: (ae-forgotten-export) The symbol "DefaultContext" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "MutationOptions" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "FetchResult" needs to be exported by the entry point index.d.ts - mutate = DefaultContext, TCache extends ApolloCache = ApolloCache>(options: MutationOptions): Promise>; + mutate = DefaultContext, TCache extends ApolloCache = ApolloCache>(options: MutationOptions): Promise>>; onClearStore(cb: () => Promise): () => void; onResetStore(cb: () => Promise): () => void; // Warning: (ae-forgotten-export) The symbol "QueryOptions" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "ApolloQueryResult" needs to be exported by the entry point index.d.ts - query(options: QueryOptions): Promise>; + // Warning: (ae-forgotten-export) The symbol "MaybeMasked" needs to be exported by the entry point index.d.ts + query(options: QueryOptions): Promise>>; // (undocumented) queryDeduplication: boolean; - readFragment(options: DataProxy.Fragment, optimistic?: boolean): T | null; - readQuery(options: DataProxy.Query, optimistic?: boolean): T | null; + readFragment(options: DataProxy.Fragment, optimistic?: boolean): Unmasked | null; + readQuery(options: DataProxy.Query, optimistic?: boolean): Unmasked | null; reFetchObservableQueries(includeStandby?: boolean): Promise[]>; // Warning: (ae-forgotten-export) The symbol "RefetchQueriesOptions" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "RefetchQueriesResult" needs to be exported by the entry point index.d.ts @@ -156,7 +163,7 @@ class ApolloClient implements DataProxy { setResolvers(resolvers: Resolvers | Resolvers[]): void; stop(): void; // Warning: (ae-forgotten-export) The symbol "SubscriptionOptions" needs to be exported by the entry point index.d.ts - subscribe(options: SubscriptionOptions): Observable>; + subscribe(options: SubscriptionOptions): Observable>>; // Warning: (ae-forgotten-export) The symbol "ApolloClientOptions" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -179,6 +186,7 @@ interface ApolloClientOptions { connectToDevTools?: boolean; // (undocumented) credentials?: string; + dataMasking?: boolean; // (undocumented) defaultContext?: Partial; defaultOptions?: DefaultOptions; @@ -397,7 +405,7 @@ namespace Cache_2 { // (undocumented) dataId?: string; // (undocumented) - result: TResult; + result: Unmasked; } import DiffResult = DataProxy.DiffResult; import ReadQueryOptions = DataProxy.ReadQueryOptions; @@ -424,6 +432,15 @@ const enum CacheWriteBehavior { // @public (undocumented) type CanReadFunction = (value: StoreValue) => boolean; +// Warning: (ae-forgotten-export) The symbol "UnionToIntersection" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "UnwrapFragmentRefs" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "RemoveFragmentName" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type CombineFragmentRefs> = UnionToIntersection<{ + [K in keyof FragmentRefs]-?: UnwrapFragmentRefs>; +}[keyof FragmentRefs]>; + // @public (undocumented) class Concast extends Observable { // Warning: (ae-forgotten-export) The symbol "MaybeAsync" needs to be exported by the entry point index.d.ts @@ -459,6 +476,10 @@ type CovariantUnaryFunction = { // @public (undocumented) export function createMockClient(data: TData, query: DocumentNode, variables?: {}): ApolloClient; +// @public (undocumented) +interface DataMasking { +} + // @public (undocumented) namespace DataProxy { // (undocumented) @@ -517,7 +538,7 @@ namespace DataProxy { // (undocumented) interface WriteOptions { broadcast?: boolean; - data: TData; + data: Unmasked; overwrite?: boolean; } // Warning: (ae-forgotten-export) The symbol "DataProxy" needs to be exported by the entry point index.d.ts @@ -529,8 +550,8 @@ namespace DataProxy { // @public interface DataProxy { - readFragment(options: DataProxy.ReadFragmentOptions, optimistic?: boolean): FragmentType | null; - readQuery(options: DataProxy.ReadQueryOptions, optimistic?: boolean): QueryType | null; + readFragment(options: DataProxy.ReadFragmentOptions, optimistic?: boolean): Unmasked | null; + readQuery(options: DataProxy.ReadQueryOptions, optimistic?: boolean): Unmasked | null; writeFragment(options: DataProxy.WriteFragmentOptions): Reference | undefined; writeQuery(options: DataProxy.WriteQueryOptions): Reference | undefined; } @@ -714,6 +735,17 @@ interface FragmentMap { // @public (undocumented) type FragmentMatcher = (rootValue: any, typeCondition: string, context: any) => boolean; +// @public (undocumented) +type FragmentType = [ +TData +] extends [{ + " $fragmentName"?: infer TKey; +}] ? TKey extends string ? { + " $fragmentRefs"?: { + [key in TKey]: TData; + }; +} : never : never; + // @internal const getApolloCacheMemoryInternals: (() => { cache: { @@ -836,11 +868,11 @@ const _invalidateModifier: unique symbol; // @public (undocumented) function isReference(obj: any): obj is Reference; -// Warning: (ae-forgotten-export) The symbol "UnionToIntersection" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "UnionToIntersection_2" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "UnionForAny" needs to be exported by the entry point index.d.ts // // @public (undocumented) -type IsStrictlyAny = UnionToIntersection> extends never ? true : false; +type IsStrictlyAny = UnionToIntersection_2> extends never ? true : false; // @public (undocumented) export const itAsync: ((this: unknown, message: string, callback: (resolve: (result?: any) => void, reject: (reason?: any) => void) => any, timeout?: number | undefined) => void) & { @@ -894,9 +926,38 @@ type LocalStateOptions = { fragmentMatcher?: FragmentMatcher; }; +// @public (undocumented) +interface MaskFragmentOptions { + // (undocumented) + data: TData; + // (undocumented) + fragment: DocumentNode; + // (undocumented) + fragmentName?: string; +} + +// @public (undocumented) +interface MaskOperationOptions { + // (undocumented) + data: TData; + // (undocumented) + document: DocumentNode; +} + // @public (undocumented) type MaybeAsync = T | PromiseLike; +// Warning: (ae-forgotten-export) The symbol "Prettify" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "RemoveMaskedMarker" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "DataMasking" needs to be exported by the entry point index.d.ts +// +// @public +type MaybeMasked = TData extends { + __masked?: true; +} ? Prettify> : DataMasking extends { + enabled: true; +} ? TData : Unmasked; + // @public (undocumented) class MissingFieldError extends Error { constructor(message: string, path: MissingTree | Array, query: DocumentNode, variables?: Record | undefined); @@ -978,11 +1039,11 @@ export interface MockedResponse, out TVariables // (undocumented) maxUsageCount?: number; // (undocumented) - newData?: ResultFunction, TVariables>; + newData?: ResultFunction>, TVariables>; // (undocumented) request: GraphQLRequest; // (undocumented) - result?: FetchResult | ResultFunction, TVariables>; + result?: FetchResult> | ResultFunction>, TVariables>; // Warning: (ae-forgotten-export) The symbol "VariableMatcher" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1084,11 +1145,12 @@ interface MutationBaseOptions; + // Warning: (ae-forgotten-export) The symbol "NoInfer_2" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts - optimisticResponse?: TData | ((vars: TVariables, { IGNORE }: { + optimisticResponse?: Unmasked> | ((vars: TVariables, { IGNORE }: { IGNORE: IgnoreModifier; - }) => TData | IgnoreModifier); - refetchQueries?: ((result: FetchResult) => InternalRefetchQueriesInclude) | InternalRefetchQueriesInclude; + }) => Unmasked> | IgnoreModifier); + refetchQueries?: ((result: FetchResult>) => InternalRefetchQueriesInclude) | InternalRefetchQueriesInclude; // Warning: (ae-forgotten-export) The symbol "MutationUpdaterFunction" needs to be exported by the entry point index.d.ts update?: MutationUpdaterFunction; // Warning: (ae-forgotten-export) The symbol "MutationQueryReducersMap" needs to be exported by the entry point index.d.ts @@ -1110,7 +1172,7 @@ interface MutationOptions = (previousResult: Record, options: { - mutationResult: FetchResult; + mutationResult: FetchResult>; queryName: string | undefined; queryVariables: Record; }) => Record; @@ -1144,7 +1206,7 @@ interface MutationStoreValue { } // @public (undocumented) -type MutationUpdaterFunction> = (cache: TCache, result: Omit, "context">, options: { +type MutationUpdaterFunction> = (cache: TCache, result: Omit>, "context">, options: { context?: TContext; variables?: TVariables; }) => void; @@ -1180,6 +1242,9 @@ type NextLink = (operation: Operation) => Observable; // @public (undocumented) type NextResultListener = (method: "next" | "error" | "complete", arg?: any) => any; +// @public +type NoInfer_2 = [T][T extends any ? 0 : never]; + // @public interface NormalizedCacheObject { // (undocumented) @@ -1191,7 +1256,7 @@ interface NormalizedCacheObject { } // @public (undocumented) -class ObservableQuery extends Observable> { +class ObservableQuery extends Observable>> { constructor({ queryManager, queryInfo, options, }: { queryManager: QueryManager; queryInfo: QueryInfo; @@ -1199,13 +1264,13 @@ class ObservableQuery(fetchMoreOptions: FetchMoreQueryOptions & { - updateQuery?: (previousQueryResult: TData, options: { - fetchMoreResult: TFetchData; + updateQuery?: (previousQueryResult: Unmasked, options: { + fetchMoreResult: Unmasked; variables: TFetchVars; - }) => TData; - }): Promise>; + }) => Unmasked; + }): Promise>>; // (undocumented) - getCurrentResult(saveAsLastResult?: boolean): ApolloQueryResult; + getCurrentResult(saveAsLastResult?: boolean): ApolloQueryResult>; // (undocumented) getLastError(variablesMustMatch?: boolean): ApolloError | undefined; // (undocumented) @@ -1222,9 +1287,9 @@ class ObservableQuery): Promise>; + refetch(variables?: Partial): Promise>>; // (undocumented) - reobserve(newOptions?: Partial>, newNetworkStatus?: NetworkStatus): Promise>; + reobserve(newOptions?: Partial>, newNetworkStatus?: NetworkStatus): Promise>>; // Warning: (ae-forgotten-export) The symbol "Concast" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1236,21 +1301,21 @@ class ObservableQuery) => void, onError?: (error: any) => void, onComplete?: () => void): Subscription; + resubscribeAfterError(onNext: (value: ApolloQueryResult>) => void, onError?: (error: any) => void, onComplete?: () => void): Subscription; // (undocumented) resubscribeAfterError(observer: Observer>): Subscription; // (undocumented) - result(): Promise>; + result(): Promise>>; // (undocumented) - setOptions(newOptions: Partial>): Promise>; - setVariables(variables: TVariables): Promise | void>; + setOptions(newOptions: Partial>): Promise>>; + setVariables(variables: TVariables): Promise> | void>; // (undocumented) silentSetOptions(newOptions: Partial>): void; startPolling(pollInterval: number): void; stopPolling(): void; // Warning: (ae-forgotten-export) The symbol "SubscribeToMoreOptions" needs to be exported by the entry point index.d.ts subscribeToMore(options: SubscribeToMoreOptions): () => void; - updateQuery(mapFn: (previousQueryResult: TData, options: Pick, "variables">) => TData): void; + updateQuery(mapFn: (previousQueryResult: Unmasked, options: Pick, "variables">) => Unmasked): void; get variables(): TVariables | undefined; } @@ -1282,6 +1347,11 @@ type OperationVariables = Record; // @public (undocumented) type Path = ReadonlyArray; +// @public (undocumented) +type Prettify = { + [K in keyof T]: T[K]; +} & {}; + // @public (undocumented) type Primitive = null | undefined | string | number | boolean | symbol | bigint; @@ -1360,6 +1430,8 @@ class QueryManager { // (undocumented) clearStore(options?: Cache_2.ResetOptions): Promise; // (undocumented) + readonly dataMasking: boolean; + // (undocumented) readonly defaultContext: Partial; // (undocumented) defaultOptions: DefaultOptions; @@ -1424,14 +1496,22 @@ class QueryManager { onQueryUpdated?: OnQueryUpdated; keepRootFields?: boolean; }, cache?: ApolloCache): Promise>; + // Warning: (ae-forgotten-export) The symbol "MaskFragmentOptions" needs to be exported by the entry point index.d.ts + // // (undocumented) - mutate, TCache extends ApolloCache>({ mutation, variables, optimisticResponse, updateQueries, refetchQueries, awaitRefetchQueries, update: updateWithProxyFn, onQueryUpdated, fetchPolicy, errorPolicy, keepRootFields, context, }: MutationOptions): Promise>; + maskFragment(options: MaskFragmentOptions): TData; + // Warning: (ae-forgotten-export) The symbol "MaskOperationOptions" needs to be exported by the entry point index.d.ts + // + // (undocumented) + maskOperation(options: MaskOperationOptions): MaybeMasked; + // (undocumented) + mutate, TCache extends ApolloCache>({ mutation, variables, optimisticResponse, updateQueries, refetchQueries, awaitRefetchQueries, update: updateWithProxyFn, onQueryUpdated, fetchPolicy, errorPolicy, keepRootFields, context, }: MutationOptions): Promise>>; // (undocumented) mutationStore?: { [mutationId: string]: MutationStoreValue; }; // (undocumented) - query(options: QueryOptions, queryId?: string): Promise>; + query(options: QueryOptions, queryId?: string): Promise>>; // (undocumented) reFetchObservableQueries(includeStandby?: boolean): Promise[]>; // Warning: (ae-forgotten-export) The symbol "InternalRefetchQueriesOptions" needs to be exported by the entry point index.d.ts @@ -1448,7 +1528,7 @@ class QueryManager { // (undocumented) readonly ssrMode: boolean; // (undocumented) - startGraphQLSubscription({ query, fetchPolicy, errorPolicy, variables, context, extensions, }: SubscriptionOptions): Observable>; + startGraphQLSubscription(options: SubscriptionOptions): Observable>; stop(): void; // (undocumented) stopQuery(queryId: string): void; @@ -1469,6 +1549,8 @@ interface QueryManagerOptions { // (undocumented) clientAwareness: Record; // (undocumented) + dataMasking: boolean; + // (undocumented) defaultContext: Partial | undefined; // (undocumented) defaultOptions: DefaultOptions; @@ -1569,6 +1651,12 @@ type RefetchQueryDescriptor = string | DocumentNode; // @public (undocumented) type RefetchWritePolicy = "merge" | "overwrite"; +// @public (undocumented) +type RemoveFragmentName = T extends any ? Omit : T; + +// @public (undocumented) +type RemoveMaskedMarker = Omit; + // @public (undocumented) type RequestHandler = (operation: Operation, forward: NextLink) => Observable | null; @@ -1711,6 +1799,8 @@ interface TransformCacheEntry { // (undocumented) hasNonreactiveDirective: boolean; // (undocumented) + nonReactiveQuery: DocumentNode; + // (undocumented) serverQuery: DocumentNode | null; } @@ -1721,18 +1811,35 @@ type TransformFn = (document: DocumentNode) => DocumentNode; type UnionForAny = T extends never ? "a" : 1; // @public (undocumented) -type UnionToIntersection = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never; +type UnionToIntersection = (U extends any ? (x: U) => void : never) extends (x: infer I) => void ? I : never; + +// @public (undocumented) +type UnionToIntersection_2 = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never; + +// @public +type Unmasked = TData extends object ? UnwrapFragmentRefs>> : TData; + +// Warning: (ae-forgotten-export) The symbol "CombineFragmentRefs" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type UnwrapFragmentRefs = string extends keyof NonNullable ? TData : " $fragmentRefs" extends keyof NonNullable ? TData extends { + " $fragmentRefs"?: infer FragmentRefs extends object; +} ? Prettify<{ + [K in keyof TData as K extends " $fragmentRefs" ? never : K]: UnwrapFragmentRefs; +} & CombineFragmentRefs> : never : TData extends object ? { + [K in keyof TData]: UnwrapFragmentRefs; +} : TData; // @public (undocumented) type UpdateQueries = MutationOptions["updateQueries"]; // @public (undocumented) -type UpdateQueryFn = (previousQueryResult: TData, options: { +type UpdateQueryFn = (previousQueryResult: Unmasked, options: { subscriptionData: { - data: TSubscriptionData; + data: Unmasked; }; variables?: TSubscriptionVariables; -}) => TData; +}) => Unmasked; // @public (undocumented) interface UriFunction { @@ -1750,18 +1857,19 @@ export function wait(ms: number): Promise; interface WatchFragmentOptions { fragment: DocumentNode | TypedDocumentNode; fragmentName?: string; - from: StoreObject | Reference | string; + // Warning: (ae-forgotten-export) The symbol "FragmentType" needs to be exported by the entry point index.d.ts + from: StoreObject | Reference | FragmentType> | string; optimistic?: boolean; variables?: TVars; } // @public type WatchFragmentResult = { - data: TData; + data: MaybeMasked; complete: true; missing?: never; } | { - data: DeepPartial; + data: DeepPartial>; complete: false; missing: MissingTree; }; @@ -1787,20 +1895,20 @@ export function withWarningSpy(it: (...args: TArgs // Warnings were encountered during analysis: // -// src/cache/core/types/DataProxy.ts:146:7 - (ae-forgotten-export) The symbol "MissingFieldError" needs to be exported by the entry point index.d.ts +// src/cache/core/types/DataProxy.ts:147:7 - (ae-forgotten-export) The symbol "MissingFieldError" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:101:3 - (ae-forgotten-export) The symbol "ReadFieldFunction" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:102:3 - (ae-forgotten-export) The symbol "CanReadFunction" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:103:3 - (ae-forgotten-export) The symbol "isReference" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:104:3 - (ae-forgotten-export) The symbol "ToReferenceFunction" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:105:3 - (ae-forgotten-export) The symbol "StorageType" needs to be exported by the entry point index.d.ts // src/core/LocalState.ts:46:5 - (ae-forgotten-export) The symbol "FragmentMap" needs to be exported by the entry point index.d.ts -// src/core/ObservableQuery.ts:116:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts -// src/core/ObservableQuery.ts:117:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:138:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:382:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts -// src/core/types.ts:174:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts -// src/core/types.ts:203:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:275:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:120:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:121:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:155:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:408:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts +// src/core/types.ts:175:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts +// src/core/types.ts:204:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:277:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/.api-reports/api-report-testing_core.api.md b/.api-reports/api-report-testing_core.api.md index 36c9b9abc92..621bb36846b 100644 --- a/.api-reports/api-report-testing_core.api.md +++ b/.api-reports/api-report-testing_core.api.md @@ -11,6 +11,7 @@ import type { FormattedExecutionResult } from 'graphql'; import type { FragmentDefinitionNode } from 'graphql'; import type { GraphQLErrorExtensions } from 'graphql'; import type { GraphQLFormattedError } from 'graphql'; +import type { InlineFragmentNode } from 'graphql'; import { Observable } from 'zen-observable-ts'; import type { Observer } from 'zen-observable-ts'; import type { Subscriber } from 'zen-observable-ts'; @@ -38,6 +39,8 @@ abstract class ApolloCache implements DataProxy { abstract evict(options: Cache_2.EvictOptions): boolean; abstract extract(optimistic?: boolean): TSerialized; // (undocumented) + fragmentMatches?(fragment: InlineFragmentNode, typename: string): boolean; + // (undocumented) gc(): string[]; // Warning: (ae-forgotten-export) The symbol "getApolloCacheMemoryInternals" needs to be exported by the entry point index.d.ts // @@ -48,19 +51,22 @@ abstract class ApolloCache implements DataProxy { // (undocumented) identify(object: StoreObject | Reference): string | undefined; // (undocumented) + lookupFragment(fragmentName: string): FragmentDefinitionNode | null; + // (undocumented) modify = Record>(options: Cache_2.ModifyOptions): boolean; // Warning: (ae-forgotten-export) The symbol "Transaction" needs to be exported by the entry point index.d.ts // // (undocumented) abstract performTransaction(transaction: Transaction, optimisticId?: string | null): void; // Warning: (ae-forgotten-export) The symbol "Cache_2" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "Unmasked" needs to be exported by the entry point index.d.ts // // (undocumented) - abstract read(query: Cache_2.ReadOptions): TData | null; + abstract read(query: Cache_2.ReadOptions): Unmasked | null; // (undocumented) - readFragment(options: Cache_2.ReadFragmentOptions, optimistic?: boolean): FragmentType | null; + readFragment(options: Cache_2.ReadFragmentOptions, optimistic?: boolean): Unmasked | null; // (undocumented) - readQuery(options: Cache_2.ReadQueryOptions, optimistic?: boolean): QueryType | null; + readQuery(options: Cache_2.ReadQueryOptions, optimistic?: boolean): Unmasked | null; // (undocumented) recordOptimisticTransaction(transaction: Transaction, optimisticId: string): void; // (undocumented) @@ -73,9 +79,9 @@ abstract class ApolloCache implements DataProxy { // (undocumented) transformForLink(document: DocumentNode): DocumentNode; // (undocumented) - updateFragment(options: Cache_2.UpdateFragmentOptions, update: (data: TData | null) => TData | null | void): TData | null; + updateFragment(options: Cache_2.UpdateFragmentOptions, update: (data: Unmasked | null) => Unmasked | null | void): Unmasked | null; // (undocumented) - updateQuery(options: Cache_2.UpdateQueryOptions, update: (data: TData | null) => TData | null | void): TData | null; + updateQuery(options: Cache_2.UpdateQueryOptions, update: (data: Unmasked | null) => Unmasked | null | void): Unmasked | null; // (undocumented) abstract watch(watch: Cache_2.WatchOptions): () => void; // Warning: (ae-forgotten-export) The symbol "OperationVariables" needs to be exported by the entry point index.d.ts @@ -133,16 +139,17 @@ class ApolloClient implements DataProxy { // Warning: (ae-forgotten-export) The symbol "DefaultContext" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "MutationOptions" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "FetchResult" needs to be exported by the entry point index.d.ts - mutate = DefaultContext, TCache extends ApolloCache = ApolloCache>(options: MutationOptions): Promise>; + mutate = DefaultContext, TCache extends ApolloCache = ApolloCache>(options: MutationOptions): Promise>>; onClearStore(cb: () => Promise): () => void; onResetStore(cb: () => Promise): () => void; // Warning: (ae-forgotten-export) The symbol "QueryOptions" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "ApolloQueryResult" needs to be exported by the entry point index.d.ts - query(options: QueryOptions): Promise>; + // Warning: (ae-forgotten-export) The symbol "MaybeMasked" needs to be exported by the entry point index.d.ts + query(options: QueryOptions): Promise>>; // (undocumented) queryDeduplication: boolean; - readFragment(options: DataProxy.Fragment, optimistic?: boolean): T | null; - readQuery(options: DataProxy.Query, optimistic?: boolean): T | null; + readFragment(options: DataProxy.Fragment, optimistic?: boolean): Unmasked | null; + readQuery(options: DataProxy.Query, optimistic?: boolean): Unmasked | null; reFetchObservableQueries(includeStandby?: boolean): Promise[]>; // Warning: (ae-forgotten-export) The symbol "RefetchQueriesOptions" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "RefetchQueriesResult" needs to be exported by the entry point index.d.ts @@ -155,7 +162,7 @@ class ApolloClient implements DataProxy { setResolvers(resolvers: Resolvers | Resolvers[]): void; stop(): void; // Warning: (ae-forgotten-export) The symbol "SubscriptionOptions" needs to be exported by the entry point index.d.ts - subscribe(options: SubscriptionOptions): Observable>; + subscribe(options: SubscriptionOptions): Observable>>; // Warning: (ae-forgotten-export) The symbol "ApolloClientOptions" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -178,6 +185,7 @@ interface ApolloClientOptions { connectToDevTools?: boolean; // (undocumented) credentials?: string; + dataMasking?: boolean; // (undocumented) defaultContext?: Partial; defaultOptions?: DefaultOptions; @@ -396,7 +404,7 @@ namespace Cache_2 { // (undocumented) dataId?: string; // (undocumented) - result: TResult; + result: Unmasked; } import DiffResult = DataProxy.DiffResult; import ReadQueryOptions = DataProxy.ReadQueryOptions; @@ -423,6 +431,15 @@ const enum CacheWriteBehavior { // @public (undocumented) type CanReadFunction = (value: StoreValue) => boolean; +// Warning: (ae-forgotten-export) The symbol "UnionToIntersection" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "UnwrapFragmentRefs" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "RemoveFragmentName" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type CombineFragmentRefs> = UnionToIntersection<{ + [K in keyof FragmentRefs]-?: UnwrapFragmentRefs>; +}[keyof FragmentRefs]>; + // @public (undocumented) class Concast extends Observable { // Warning: (ae-forgotten-export) The symbol "MaybeAsync" needs to be exported by the entry point index.d.ts @@ -458,6 +475,10 @@ type CovariantUnaryFunction = { // @public (undocumented) export function createMockClient(data: TData, query: DocumentNode, variables?: {}): ApolloClient; +// @public (undocumented) +interface DataMasking { +} + // @public (undocumented) namespace DataProxy { // (undocumented) @@ -516,7 +537,7 @@ namespace DataProxy { // (undocumented) interface WriteOptions { broadcast?: boolean; - data: TData; + data: Unmasked; overwrite?: boolean; } // Warning: (ae-forgotten-export) The symbol "DataProxy" needs to be exported by the entry point index.d.ts @@ -528,8 +549,8 @@ namespace DataProxy { // @public interface DataProxy { - readFragment(options: DataProxy.ReadFragmentOptions, optimistic?: boolean): FragmentType | null; - readQuery(options: DataProxy.ReadQueryOptions, optimistic?: boolean): QueryType | null; + readFragment(options: DataProxy.ReadFragmentOptions, optimistic?: boolean): Unmasked | null; + readQuery(options: DataProxy.ReadQueryOptions, optimistic?: boolean): Unmasked | null; writeFragment(options: DataProxy.WriteFragmentOptions): Reference | undefined; writeQuery(options: DataProxy.WriteQueryOptions): Reference | undefined; } @@ -713,6 +734,17 @@ interface FragmentMap { // @public (undocumented) type FragmentMatcher = (rootValue: any, typeCondition: string, context: any) => boolean; +// @public (undocumented) +type FragmentType = [ +TData +] extends [{ + " $fragmentName"?: infer TKey; +}] ? TKey extends string ? { + " $fragmentRefs"?: { + [key in TKey]: TData; + }; +} : never : never; + // @internal const getApolloCacheMemoryInternals: (() => { cache: { @@ -835,11 +867,11 @@ const _invalidateModifier: unique symbol; // @public (undocumented) function isReference(obj: any): obj is Reference; -// Warning: (ae-forgotten-export) The symbol "UnionToIntersection" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "UnionToIntersection_2" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "UnionForAny" needs to be exported by the entry point index.d.ts // // @public (undocumented) -type IsStrictlyAny = UnionToIntersection> extends never ? true : false; +type IsStrictlyAny = UnionToIntersection_2> extends never ? true : false; // @public (undocumented) export const itAsync: ((this: unknown, message: string, callback: (resolve: (result?: any) => void, reject: (reason?: any) => void) => any, timeout?: number | undefined) => void) & { @@ -893,9 +925,38 @@ type LocalStateOptions = { fragmentMatcher?: FragmentMatcher; }; +// @public (undocumented) +interface MaskFragmentOptions { + // (undocumented) + data: TData; + // (undocumented) + fragment: DocumentNode; + // (undocumented) + fragmentName?: string; +} + +// @public (undocumented) +interface MaskOperationOptions { + // (undocumented) + data: TData; + // (undocumented) + document: DocumentNode; +} + // @public (undocumented) type MaybeAsync = T | PromiseLike; +// Warning: (ae-forgotten-export) The symbol "Prettify" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "RemoveMaskedMarker" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "DataMasking" needs to be exported by the entry point index.d.ts +// +// @public +type MaybeMasked = TData extends { + __masked?: true; +} ? Prettify> : DataMasking extends { + enabled: true; +} ? TData : Unmasked; + // @public (undocumented) class MissingFieldError extends Error { constructor(message: string, path: MissingTree | Array, query: DocumentNode, variables?: Record | undefined); @@ -933,11 +994,11 @@ export interface MockedResponse, out TVariables // (undocumented) maxUsageCount?: number; // (undocumented) - newData?: ResultFunction, TVariables>; + newData?: ResultFunction>, TVariables>; // (undocumented) request: GraphQLRequest; // (undocumented) - result?: FetchResult | ResultFunction, TVariables>; + result?: FetchResult> | ResultFunction>, TVariables>; // Warning: (ae-forgotten-export) The symbol "VariableMatcher" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1039,11 +1100,12 @@ interface MutationBaseOptions; + // Warning: (ae-forgotten-export) The symbol "NoInfer_2" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts - optimisticResponse?: TData | ((vars: TVariables, { IGNORE }: { + optimisticResponse?: Unmasked> | ((vars: TVariables, { IGNORE }: { IGNORE: IgnoreModifier; - }) => TData | IgnoreModifier); - refetchQueries?: ((result: FetchResult) => InternalRefetchQueriesInclude) | InternalRefetchQueriesInclude; + }) => Unmasked> | IgnoreModifier); + refetchQueries?: ((result: FetchResult>) => InternalRefetchQueriesInclude) | InternalRefetchQueriesInclude; // Warning: (ae-forgotten-export) The symbol "MutationUpdaterFunction" needs to be exported by the entry point index.d.ts update?: MutationUpdaterFunction; // Warning: (ae-forgotten-export) The symbol "MutationQueryReducersMap" needs to be exported by the entry point index.d.ts @@ -1065,7 +1127,7 @@ interface MutationOptions = (previousResult: Record, options: { - mutationResult: FetchResult; + mutationResult: FetchResult>; queryName: string | undefined; queryVariables: Record; }) => Record; @@ -1099,7 +1161,7 @@ interface MutationStoreValue { } // @public (undocumented) -type MutationUpdaterFunction> = (cache: TCache, result: Omit, "context">, options: { +type MutationUpdaterFunction> = (cache: TCache, result: Omit>, "context">, options: { context?: TContext; variables?: TVariables; }) => void; @@ -1135,6 +1197,9 @@ type NextLink = (operation: Operation) => Observable; // @public (undocumented) type NextResultListener = (method: "next" | "error" | "complete", arg?: any) => any; +// @public +type NoInfer_2 = [T][T extends any ? 0 : never]; + // @public interface NormalizedCacheObject { // (undocumented) @@ -1146,7 +1211,7 @@ interface NormalizedCacheObject { } // @public (undocumented) -class ObservableQuery extends Observable> { +class ObservableQuery extends Observable>> { constructor({ queryManager, queryInfo, options, }: { queryManager: QueryManager; queryInfo: QueryInfo; @@ -1154,13 +1219,13 @@ class ObservableQuery(fetchMoreOptions: FetchMoreQueryOptions & { - updateQuery?: (previousQueryResult: TData, options: { - fetchMoreResult: TFetchData; + updateQuery?: (previousQueryResult: Unmasked, options: { + fetchMoreResult: Unmasked; variables: TFetchVars; - }) => TData; - }): Promise>; + }) => Unmasked; + }): Promise>>; // (undocumented) - getCurrentResult(saveAsLastResult?: boolean): ApolloQueryResult; + getCurrentResult(saveAsLastResult?: boolean): ApolloQueryResult>; // (undocumented) getLastError(variablesMustMatch?: boolean): ApolloError | undefined; // (undocumented) @@ -1177,9 +1242,9 @@ class ObservableQuery): Promise>; + refetch(variables?: Partial): Promise>>; // (undocumented) - reobserve(newOptions?: Partial>, newNetworkStatus?: NetworkStatus): Promise>; + reobserve(newOptions?: Partial>, newNetworkStatus?: NetworkStatus): Promise>>; // Warning: (ae-forgotten-export) The symbol "Concast" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1191,21 +1256,21 @@ class ObservableQuery) => void, onError?: (error: any) => void, onComplete?: () => void): Subscription; + resubscribeAfterError(onNext: (value: ApolloQueryResult>) => void, onError?: (error: any) => void, onComplete?: () => void): Subscription; // (undocumented) resubscribeAfterError(observer: Observer>): Subscription; // (undocumented) - result(): Promise>; + result(): Promise>>; // (undocumented) - setOptions(newOptions: Partial>): Promise>; - setVariables(variables: TVariables): Promise | void>; + setOptions(newOptions: Partial>): Promise>>; + setVariables(variables: TVariables): Promise> | void>; // (undocumented) silentSetOptions(newOptions: Partial>): void; startPolling(pollInterval: number): void; stopPolling(): void; // Warning: (ae-forgotten-export) The symbol "SubscribeToMoreOptions" needs to be exported by the entry point index.d.ts subscribeToMore(options: SubscribeToMoreOptions): () => void; - updateQuery(mapFn: (previousQueryResult: TData, options: Pick, "variables">) => TData): void; + updateQuery(mapFn: (previousQueryResult: Unmasked, options: Pick, "variables">) => Unmasked): void; get variables(): TVariables | undefined; } @@ -1237,6 +1302,11 @@ type OperationVariables = Record; // @public (undocumented) type Path = ReadonlyArray; +// @public (undocumented) +type Prettify = { + [K in keyof T]: T[K]; +} & {}; + // @public (undocumented) type Primitive = null | undefined | string | number | boolean | symbol | bigint; @@ -1315,6 +1385,8 @@ class QueryManager { // (undocumented) clearStore(options?: Cache_2.ResetOptions): Promise; // (undocumented) + readonly dataMasking: boolean; + // (undocumented) readonly defaultContext: Partial; // Warning: (ae-forgotten-export) The symbol "DefaultOptions" needs to be exported by the entry point index.d.ts // @@ -1381,14 +1453,22 @@ class QueryManager { onQueryUpdated?: OnQueryUpdated; keepRootFields?: boolean; }, cache?: ApolloCache): Promise>; + // Warning: (ae-forgotten-export) The symbol "MaskFragmentOptions" needs to be exported by the entry point index.d.ts + // // (undocumented) - mutate, TCache extends ApolloCache>({ mutation, variables, optimisticResponse, updateQueries, refetchQueries, awaitRefetchQueries, update: updateWithProxyFn, onQueryUpdated, fetchPolicy, errorPolicy, keepRootFields, context, }: MutationOptions): Promise>; + maskFragment(options: MaskFragmentOptions): TData; + // Warning: (ae-forgotten-export) The symbol "MaskOperationOptions" needs to be exported by the entry point index.d.ts + // + // (undocumented) + maskOperation(options: MaskOperationOptions): MaybeMasked; + // (undocumented) + mutate, TCache extends ApolloCache>({ mutation, variables, optimisticResponse, updateQueries, refetchQueries, awaitRefetchQueries, update: updateWithProxyFn, onQueryUpdated, fetchPolicy, errorPolicy, keepRootFields, context, }: MutationOptions): Promise>>; // (undocumented) mutationStore?: { [mutationId: string]: MutationStoreValue; }; // (undocumented) - query(options: QueryOptions, queryId?: string): Promise>; + query(options: QueryOptions, queryId?: string): Promise>>; // (undocumented) reFetchObservableQueries(includeStandby?: boolean): Promise[]>; // Warning: (ae-forgotten-export) The symbol "InternalRefetchQueriesOptions" needs to be exported by the entry point index.d.ts @@ -1405,7 +1485,7 @@ class QueryManager { // (undocumented) readonly ssrMode: boolean; // (undocumented) - startGraphQLSubscription({ query, fetchPolicy, errorPolicy, variables, context, extensions, }: SubscriptionOptions): Observable>; + startGraphQLSubscription(options: SubscriptionOptions): Observable>; stop(): void; // (undocumented) stopQuery(queryId: string): void; @@ -1426,6 +1506,8 @@ interface QueryManagerOptions { // (undocumented) clientAwareness: Record; // (undocumented) + dataMasking: boolean; + // (undocumented) defaultContext: Partial | undefined; // (undocumented) defaultOptions: DefaultOptions; @@ -1526,6 +1608,12 @@ type RefetchQueryDescriptor = string | DocumentNode; // @public (undocumented) type RefetchWritePolicy = "merge" | "overwrite"; +// @public (undocumented) +type RemoveFragmentName = T extends any ? Omit : T; + +// @public (undocumented) +type RemoveMaskedMarker = Omit; + // @public (undocumented) type RequestHandler = (operation: Operation, forward: NextLink) => Observable | null; @@ -1668,6 +1756,8 @@ interface TransformCacheEntry { // (undocumented) hasNonreactiveDirective: boolean; // (undocumented) + nonReactiveQuery: DocumentNode; + // (undocumented) serverQuery: DocumentNode | null; } @@ -1678,18 +1768,35 @@ type TransformFn = (document: DocumentNode) => DocumentNode; type UnionForAny = T extends never ? "a" : 1; // @public (undocumented) -type UnionToIntersection = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never; +type UnionToIntersection = (U extends any ? (x: U) => void : never) extends (x: infer I) => void ? I : never; + +// @public (undocumented) +type UnionToIntersection_2 = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never; + +// @public +type Unmasked = TData extends object ? UnwrapFragmentRefs>> : TData; + +// Warning: (ae-forgotten-export) The symbol "CombineFragmentRefs" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type UnwrapFragmentRefs = string extends keyof NonNullable ? TData : " $fragmentRefs" extends keyof NonNullable ? TData extends { + " $fragmentRefs"?: infer FragmentRefs extends object; +} ? Prettify<{ + [K in keyof TData as K extends " $fragmentRefs" ? never : K]: UnwrapFragmentRefs; +} & CombineFragmentRefs> : never : TData extends object ? { + [K in keyof TData]: UnwrapFragmentRefs; +} : TData; // @public (undocumented) type UpdateQueries = MutationOptions["updateQueries"]; // @public (undocumented) -type UpdateQueryFn = (previousQueryResult: TData, options: { +type UpdateQueryFn = (previousQueryResult: Unmasked, options: { subscriptionData: { - data: TSubscriptionData; + data: Unmasked; }; variables?: TSubscriptionVariables; -}) => TData; +}) => Unmasked; // @public (undocumented) interface UriFunction { @@ -1707,18 +1814,19 @@ export function wait(ms: number): Promise; interface WatchFragmentOptions { fragment: DocumentNode | TypedDocumentNode; fragmentName?: string; - from: StoreObject | Reference | string; + // Warning: (ae-forgotten-export) The symbol "FragmentType" needs to be exported by the entry point index.d.ts + from: StoreObject | Reference | FragmentType> | string; optimistic?: boolean; variables?: TVars; } // @public type WatchFragmentResult = { - data: TData; + data: MaybeMasked; complete: true; missing?: never; } | { - data: DeepPartial; + data: DeepPartial>; complete: false; missing: MissingTree; }; @@ -1744,20 +1852,20 @@ export function withWarningSpy(it: (...args: TArgs // Warnings were encountered during analysis: // -// src/cache/core/types/DataProxy.ts:146:7 - (ae-forgotten-export) The symbol "MissingFieldError" needs to be exported by the entry point index.d.ts +// src/cache/core/types/DataProxy.ts:147:7 - (ae-forgotten-export) The symbol "MissingFieldError" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:101:3 - (ae-forgotten-export) The symbol "ReadFieldFunction" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:102:3 - (ae-forgotten-export) The symbol "CanReadFunction" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:103:3 - (ae-forgotten-export) The symbol "isReference" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:104:3 - (ae-forgotten-export) The symbol "ToReferenceFunction" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:105:3 - (ae-forgotten-export) The symbol "StorageType" needs to be exported by the entry point index.d.ts // src/core/LocalState.ts:46:5 - (ae-forgotten-export) The symbol "FragmentMap" needs to be exported by the entry point index.d.ts -// src/core/ObservableQuery.ts:116:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts -// src/core/ObservableQuery.ts:117:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:138:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:382:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts -// src/core/types.ts:174:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts -// src/core/types.ts:203:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:275:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:120:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:121:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:155:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:408:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts +// src/core/types.ts:175:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts +// src/core/types.ts:204:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:277:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/.api-reports/api-report-testing_experimental.api.md b/.api-reports/api-report-testing_experimental.api.md index 330f543fc64..cfbe5ed404e 100644 --- a/.api-reports/api-report-testing_experimental.api.md +++ b/.api-reports/api-report-testing_experimental.api.md @@ -79,7 +79,7 @@ interface TestSchemaOptions { // Warnings were encountered during analysis: // // src/core/LocalState.ts:46:5 - (ae-forgotten-export) The symbol "FragmentMap" needs to be exported by the entry point index.d.ts -// src/core/types.ts:203:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts +// src/core/types.ts:204:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts // src/testing/experimental/createTestSchema.ts:10:23 - (ae-forgotten-export) The symbol "Resolvers" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/.api-reports/api-report-utilities.api.md b/.api-reports/api-report-utilities.api.md index 8a2ff540cb5..451fe8a5d66 100644 --- a/.api-reports/api-report-utilities.api.md +++ b/.api-reports/api-report-utilities.api.md @@ -31,6 +31,9 @@ import type { VariableDefinitionNode } from 'graphql'; import type { VariableNode } from 'graphql'; import { WeakCache } from '@wry/caches'; +// @public (undocumented) +export function addNonReactiveToNamedFragments(document: DocumentNode): DocumentNode; + // @public (undocumented) export const addTypenameToDocument: ((doc: TNode) => TNode) & { added(field: FieldNode): boolean; @@ -56,6 +59,8 @@ abstract class ApolloCache implements DataProxy { abstract evict(options: Cache_2.EvictOptions): boolean; abstract extract(optimistic?: boolean): TSerialized; // (undocumented) + fragmentMatches?(fragment: InlineFragmentNode, typename: string): boolean; + // (undocumented) gc(): string[]; // Warning: (ae-forgotten-export) The symbol "getApolloCacheMemoryInternals" needs to be exported by the entry point index.d.ts // @@ -64,19 +69,22 @@ abstract class ApolloCache implements DataProxy { // (undocumented) identify(object: StoreObject | Reference): string | undefined; // (undocumented) + lookupFragment(fragmentName: string): FragmentDefinitionNode | null; + // (undocumented) modify = Record>(options: Cache_2.ModifyOptions): boolean; // Warning: (ae-forgotten-export) The symbol "Transaction" needs to be exported by the entry point index.d.ts // // (undocumented) abstract performTransaction(transaction: Transaction, optimisticId?: string | null): void; // Warning: (ae-forgotten-export) The symbol "Cache_2" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "Unmasked" needs to be exported by the entry point index.d.ts // // (undocumented) - abstract read(query: Cache_2.ReadOptions): TData | null; + abstract read(query: Cache_2.ReadOptions): Unmasked | null; // (undocumented) - readFragment(options: Cache_2.ReadFragmentOptions, optimistic?: boolean): FragmentType | null; + readFragment(options: Cache_2.ReadFragmentOptions, optimistic?: boolean): Unmasked | null; // (undocumented) - readQuery(options: Cache_2.ReadQueryOptions, optimistic?: boolean): QueryType | null; + readQuery(options: Cache_2.ReadQueryOptions, optimistic?: boolean): Unmasked | null; // (undocumented) recordOptimisticTransaction(transaction: Transaction, optimisticId: string): void; // (undocumented) @@ -89,9 +97,9 @@ abstract class ApolloCache implements DataProxy { // (undocumented) transformForLink(document: DocumentNode): DocumentNode; // (undocumented) - updateFragment(options: Cache_2.UpdateFragmentOptions, update: (data: TData | null) => TData | null | void): TData | null; + updateFragment(options: Cache_2.UpdateFragmentOptions, update: (data: Unmasked | null) => Unmasked | null | void): Unmasked | null; // (undocumented) - updateQuery(options: Cache_2.UpdateQueryOptions, update: (data: TData | null) => TData | null | void): TData | null; + updateQuery(options: Cache_2.UpdateQueryOptions, update: (data: Unmasked | null) => Unmasked | null | void): Unmasked | null; // (undocumented) abstract watch(watch: Cache_2.WatchOptions): () => void; // Warning: (ae-forgotten-export) The symbol "OperationVariables" needs to be exported by the entry point index.d.ts @@ -146,16 +154,17 @@ class ApolloClient implements DataProxy { // Warning: (ae-forgotten-export) The symbol "DefaultContext" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "MutationOptions" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "FetchResult" needs to be exported by the entry point index.d.ts - mutate = DefaultContext, TCache extends ApolloCache = ApolloCache>(options: MutationOptions): Promise>; + mutate = DefaultContext, TCache extends ApolloCache = ApolloCache>(options: MutationOptions): Promise>>; onClearStore(cb: () => Promise): () => void; onResetStore(cb: () => Promise): () => void; // Warning: (ae-forgotten-export) The symbol "QueryOptions" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "ApolloQueryResult" needs to be exported by the entry point index.d.ts - query(options: QueryOptions): Promise>; + // Warning: (ae-forgotten-export) The symbol "MaybeMasked" needs to be exported by the entry point index.d.ts + query(options: QueryOptions): Promise>>; // (undocumented) queryDeduplication: boolean; - readFragment(options: DataProxy.Fragment, optimistic?: boolean): T | null; - readQuery(options: DataProxy.Query, optimistic?: boolean): T | null; + readFragment(options: DataProxy.Fragment, optimistic?: boolean): Unmasked | null; + readQuery(options: DataProxy.Query, optimistic?: boolean): Unmasked | null; reFetchObservableQueries(includeStandby?: boolean): Promise[]>; // Warning: (ae-forgotten-export) The symbol "RefetchQueriesOptions" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "RefetchQueriesResult" needs to be exported by the entry point index.d.ts @@ -168,7 +177,7 @@ class ApolloClient implements DataProxy { setResolvers(resolvers: Resolvers | Resolvers[]): void; stop(): void; // Warning: (ae-forgotten-export) The symbol "SubscriptionOptions" needs to be exported by the entry point index.d.ts - subscribe(options: SubscriptionOptions): Observable>; + subscribe(options: SubscriptionOptions): Observable>>; // Warning: (ae-forgotten-export) The symbol "ApolloClientOptions" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -191,6 +200,7 @@ interface ApolloClientOptions { connectToDevTools?: boolean; // (undocumented) credentials?: string; + dataMasking?: boolean; // (undocumented) defaultContext?: Partial; defaultOptions?: DefaultOptions; @@ -452,7 +462,7 @@ namespace Cache_2 { // (undocumented) dataId?: string; // (undocumented) - result: TResult; + result: Unmasked; } import DiffResult = DataProxy.DiffResult; import ReadQueryOptions = DataProxy.ReadQueryOptions; @@ -542,6 +552,14 @@ export function checkDocument(doc: DocumentNode): DocumentNode; // @public export function cloneDeep(value: T): T; +// Warning: (ae-forgotten-export) The symbol "UnwrapFragmentRefs" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "RemoveFragmentName" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type CombineFragmentRefs> = UnionToIntersection<{ + [K in keyof FragmentRefs]-?: UnwrapFragmentRefs>; +}[keyof FragmentRefs]>; + // @public export function compact(...objects: TArgs): TupleToIntersection; @@ -590,6 +608,10 @@ export function createFulfilledPromise(value: TValue): FulfilledPromise< // @public (undocumented) export function createRejectedPromise(reason: unknown): RejectedPromise; +// @public (undocumented) +interface DataMasking { +} + // @public (undocumented) namespace DataProxy { // (undocumented) @@ -648,7 +670,7 @@ namespace DataProxy { // (undocumented) interface WriteOptions { broadcast?: boolean; - data: TData; + data: Unmasked; overwrite?: boolean; } // Warning: (ae-forgotten-export) The symbol "DataProxy" needs to be exported by the entry point index.d.ts @@ -660,8 +682,8 @@ namespace DataProxy { // @public interface DataProxy { - readFragment(options: DataProxy.ReadFragmentOptions, optimistic?: boolean): FragmentType | null; - readQuery(options: DataProxy.ReadQueryOptions, optimistic?: boolean): QueryType | null; + readFragment(options: DataProxy.ReadFragmentOptions, optimistic?: boolean): Unmasked | null; + readQuery(options: DataProxy.ReadQueryOptions, optimistic?: boolean): Unmasked | null; writeFragment(options: DataProxy.WriteFragmentOptions): Reference | undefined; writeQuery(options: DataProxy.WriteQueryOptions): Reference | undefined; } @@ -1092,6 +1114,17 @@ interface FragmentRegistryAPI { transform(document: D): D; } +// @public (undocumented) +type FragmentType = [ +TData +] extends [{ + " $fragmentName"?: infer TKey; +}] ? TKey extends string ? { + " $fragmentRefs"?: { + [key in TKey]: TData; + }; +} : never : never; + // @public (undocumented) interface FulfilledPromise extends Promise { // (undocumented) @@ -1160,6 +1193,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; @@ -1315,6 +1351,8 @@ class InMemoryCache extends ApolloCache { // (undocumented) extract(optimistic?: boolean): NormalizedCacheObject; // (undocumented) + fragmentMatches(fragment: InlineFragmentNode, typename: string): boolean; + // (undocumented) gc(options?: { resetResultCache?: boolean; resetResultIdentities?: boolean; @@ -1325,6 +1363,8 @@ class InMemoryCache extends ApolloCache { getMemoryInternals?: typeof getInMemoryCacheMemoryInternals; // (undocumented) identify(object: StoreObject | Reference): string | undefined; + // (undocumented) + lookupFragment(fragmentName: string): FragmentDefinitionNode | null; // Warning: (ae-forgotten-export) The symbol "makeVar" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1464,11 +1504,11 @@ export function isReference(obj: any): obj is Reference; // @public (undocumented) export function isStatefulPromise(promise: Promise): promise is PromiseWithState; -// Warning: (ae-forgotten-export) The symbol "UnionToIntersection" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "UnionToIntersection_2" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "UnionForAny" needs to be exported by the entry point index.d.ts // // @public (undocumented) -export type IsStrictlyAny = UnionToIntersection> extends never ? true : false; +export type IsStrictlyAny = UnionToIntersection_2> extends never ? true : false; // @public (undocumented) export function isSubscriptionOperation(document: DocumentNode): boolean; @@ -1586,6 +1626,24 @@ export function makeUniqueId(prefix: string): string; // @public (undocumented) function makeVar(value: T): ReactiveVar; +// @public (undocumented) +interface MaskFragmentOptions { + // (undocumented) + data: TData; + // (undocumented) + fragment: DocumentNode; + // (undocumented) + fragmentName?: string; +} + +// @public (undocumented) +interface MaskOperationOptions { + // (undocumented) + data: TData; + // (undocumented) + document: DocumentNode; +} + // @public (undocumented) export function maybe(thunk: () => T): T | undefined; @@ -1595,6 +1653,16 @@ type MaybeAsync = T | PromiseLike; // @public (undocumented) export function maybeDeepFreeze(obj: T): T; +// Warning: (ae-forgotten-export) The symbol "RemoveMaskedMarker" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "DataMasking" needs to be exported by the entry point index.d.ts +// +// @public +type MaybeMasked = TData extends { + __masked?: true; +} ? Prettify> : DataMasking extends { + enabled: true; +} ? TData : Unmasked; + // @public (undocumented) export function mergeDeep(...sources: T): TupleToIntersection; @@ -1688,10 +1756,10 @@ interface MutationBaseOptions; // Warning: (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts - optimisticResponse?: TData | ((vars: TVariables, { IGNORE }: { + optimisticResponse?: Unmasked> | ((vars: TVariables, { IGNORE }: { IGNORE: IgnoreModifier; - }) => TData | IgnoreModifier); - refetchQueries?: ((result: FetchResult) => InternalRefetchQueriesInclude) | InternalRefetchQueriesInclude; + }) => Unmasked> | IgnoreModifier); + refetchQueries?: ((result: FetchResult>) => InternalRefetchQueriesInclude) | InternalRefetchQueriesInclude; // Warning: (ae-forgotten-export) The symbol "MutationUpdaterFunction" needs to be exported by the entry point index.d.ts update?: MutationUpdaterFunction; // Warning: (ae-forgotten-export) The symbol "MutationQueryReducersMap" needs to be exported by the entry point index.d.ts @@ -1713,7 +1781,7 @@ interface MutationOptions = (previousResult: Record, options: { - mutationResult: FetchResult; + mutationResult: FetchResult>; queryName: string | undefined; queryVariables: Record; }) => Record; @@ -1747,7 +1815,7 @@ interface MutationStoreValue { } // @public (undocumented) -type MutationUpdaterFunction> = (cache: TCache, result: Omit, "context">, options: { +type MutationUpdaterFunction> = (cache: TCache, result: Omit>, "context">, options: { context?: TContext; variables?: TVariables; }) => void; @@ -1783,6 +1851,10 @@ type NextLink = (operation: Operation) => Observable; // @public (undocumented) type NextResultListener = (method: "next" | "error" | "complete", arg?: any) => any; +// @public +type NoInfer_2 = [T][T extends any ? 0 : never]; +export { NoInfer_2 as NoInfer } + // @public interface NormalizedCache { // (undocumented) @@ -1829,7 +1901,7 @@ interface NormalizedCacheObject { export { Observable } // @public (undocumented) -class ObservableQuery extends Observable> { +class ObservableQuery extends Observable>> { constructor({ queryManager, queryInfo, options, }: { queryManager: QueryManager; queryInfo: QueryInfo; @@ -1837,13 +1909,13 @@ class ObservableQuery(fetchMoreOptions: FetchMoreQueryOptions & { - updateQuery?: (previousQueryResult: TData, options: { - fetchMoreResult: TFetchData; + updateQuery?: (previousQueryResult: Unmasked, options: { + fetchMoreResult: Unmasked; variables: TFetchVars; - }) => TData; - }): Promise>; + }) => Unmasked; + }): Promise>>; // (undocumented) - getCurrentResult(saveAsLastResult?: boolean): ApolloQueryResult; + getCurrentResult(saveAsLastResult?: boolean): ApolloQueryResult>; // (undocumented) getLastError(variablesMustMatch?: boolean): ApolloError | undefined; // (undocumented) @@ -1860,9 +1932,9 @@ class ObservableQuery): Promise>; + refetch(variables?: Partial): Promise>>; // (undocumented) - reobserve(newOptions?: Partial>, newNetworkStatus?: NetworkStatus): Promise>; + reobserve(newOptions?: Partial>, newNetworkStatus?: NetworkStatus): Promise>>; // (undocumented) reobserveAsConcast(newOptions?: Partial>, newNetworkStatus?: NetworkStatus): Concast>; // @internal (undocumented) @@ -1872,21 +1944,21 @@ class ObservableQuery) => void, onError?: (error: any) => void, onComplete?: () => void): ObservableSubscription; + resubscribeAfterError(onNext: (value: ApolloQueryResult>) => void, onError?: (error: any) => void, onComplete?: () => void): ObservableSubscription; // (undocumented) resubscribeAfterError(observer: Observer>): ObservableSubscription; // (undocumented) - result(): Promise>; + result(): Promise>>; // (undocumented) - setOptions(newOptions: Partial>): Promise>; - setVariables(variables: TVariables): Promise | void>; + setOptions(newOptions: Partial>): Promise>>; + setVariables(variables: TVariables): Promise> | void>; // (undocumented) silentSetOptions(newOptions: Partial>): void; startPolling(pollInterval: number): void; stopPolling(): void; // Warning: (ae-forgotten-export) The symbol "SubscribeToMoreOptions" needs to be exported by the entry point index.d.ts subscribeToMore(options: SubscribeToMoreOptions): () => void; - updateQuery(mapFn: (previousQueryResult: TData, options: Pick, "variables">) => TData): void; + updateQuery(mapFn: (previousQueryResult: Unmasked, options: Pick, "variables">) => Unmasked): void; get variables(): TVariables | undefined; } @@ -1990,6 +2062,14 @@ type PossibleTypesMap = { [supertype: string]: string[]; }; +// @public (undocumented) +export type Prettify = { + [K in keyof T]: T[K]; +} & {}; + +// @public (undocumented) +export function preventUnhandledRejection(promise: Promise): Promise; + // @public (undocumented) type Primitive = null | undefined | string | number | boolean | symbol | bigint; @@ -2079,6 +2159,8 @@ class QueryManager { // (undocumented) clearStore(options?: Cache_2.ResetOptions): Promise; // (undocumented) + readonly dataMasking: boolean; + // (undocumented) readonly defaultContext: Partial; // Warning: (ae-forgotten-export) The symbol "DefaultOptions" needs to be exported by the entry point index.d.ts // @@ -2145,14 +2227,22 @@ class QueryManager { onQueryUpdated?: OnQueryUpdated; keepRootFields?: boolean; }, cache?: ApolloCache): Promise>; + // Warning: (ae-forgotten-export) The symbol "MaskFragmentOptions" needs to be exported by the entry point index.d.ts + // + // (undocumented) + maskFragment(options: MaskFragmentOptions): TData; + // Warning: (ae-forgotten-export) The symbol "MaskOperationOptions" needs to be exported by the entry point index.d.ts + // // (undocumented) - mutate, TCache extends ApolloCache>({ mutation, variables, optimisticResponse, updateQueries, refetchQueries, awaitRefetchQueries, update: updateWithProxyFn, onQueryUpdated, fetchPolicy, errorPolicy, keepRootFields, context, }: MutationOptions): Promise>; + maskOperation(options: MaskOperationOptions): MaybeMasked; + // (undocumented) + mutate, TCache extends ApolloCache>({ mutation, variables, optimisticResponse, updateQueries, refetchQueries, awaitRefetchQueries, update: updateWithProxyFn, onQueryUpdated, fetchPolicy, errorPolicy, keepRootFields, context, }: MutationOptions): Promise>>; // (undocumented) mutationStore?: { [mutationId: string]: MutationStoreValue; }; // (undocumented) - query(options: QueryOptions, queryId?: string): Promise>; + query(options: QueryOptions, queryId?: string): Promise>>; // (undocumented) reFetchObservableQueries(includeStandby?: boolean): Promise[]>; // Warning: (ae-forgotten-export) The symbol "InternalRefetchQueriesOptions" needs to be exported by the entry point index.d.ts @@ -2169,7 +2259,7 @@ class QueryManager { // (undocumented) readonly ssrMode: boolean; // (undocumented) - startGraphQLSubscription({ query, fetchPolicy, errorPolicy, variables, context, extensions, }: SubscriptionOptions): Observable>; + startGraphQLSubscription(options: SubscriptionOptions): Observable>; stop(): void; // (undocumented) stopQuery(queryId: string): void; @@ -2190,6 +2280,8 @@ interface QueryManagerOptions { // (undocumented) clientAwareness: Record; // (undocumented) + dataMasking: boolean; + // (undocumented) defaultContext: Partial | undefined; // (undocumented) defaultOptions: DefaultOptions; @@ -2353,12 +2445,18 @@ export function removeDirectivesFromDocument(directives: RemoveDirectiveConfig[] // @public (undocumented) export type RemoveFragmentDefinitionConfig = RemoveNodeConfig; +// @public (undocumented) +type RemoveFragmentName = T extends any ? Omit : T; + // @public (undocumented) export type RemoveFragmentSpreadConfig = RemoveNodeConfig; // @public (undocumented) export function removeFragmentSpreadFromDocument(config: RemoveFragmentSpreadConfig[], doc: DocumentNode): DocumentNode | null; +// @public (undocumented) +type RemoveMaskedMarker = Omit; + // @public (undocumented) export type RemoveNodeConfig = { name?: string; @@ -2537,6 +2635,8 @@ interface TransformCacheEntry { // (undocumented) hasNonreactiveDirective: boolean; // (undocumented) + nonReactiveQuery: DocumentNode; + // (undocumented) serverQuery: DocumentNode | null; } @@ -2583,18 +2683,35 @@ type TypePolicy = { type UnionForAny = T extends never ? "a" : 1; // @public (undocumented) -type UnionToIntersection = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never; +export type UnionToIntersection = (U extends any ? (x: U) => void : never) extends (x: infer I) => void ? I : never; + +// @public (undocumented) +type UnionToIntersection_2 = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never; + +// @public +type Unmasked = TData extends object ? UnwrapFragmentRefs>> : TData; + +// Warning: (ae-forgotten-export) The symbol "CombineFragmentRefs" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type UnwrapFragmentRefs = string extends keyof NonNullable ? TData : " $fragmentRefs" extends keyof NonNullable ? TData extends { + " $fragmentRefs"?: infer FragmentRefs extends object; +} ? Prettify<{ + [K in keyof TData as K extends " $fragmentRefs" ? never : K]: UnwrapFragmentRefs; +} & CombineFragmentRefs> : never : TData extends object ? { + [K in keyof TData]: UnwrapFragmentRefs; +} : TData; // @public (undocumented) type UpdateQueries = MutationOptions["updateQueries"]; // @public (undocumented) -type UpdateQueryFn = (previousQueryResult: TData, options: { +type UpdateQueryFn = (previousQueryResult: Unmasked, options: { subscriptionData: { - data: TSubscriptionData; + data: Unmasked; }; variables?: TSubscriptionVariables; -}) => TData; +}) => Unmasked; // @public (undocumented) interface UriFunction { @@ -2612,18 +2729,19 @@ export type VariableValue = (node: VariableNode) => any; interface WatchFragmentOptions { fragment: DocumentNode | TypedDocumentNode; fragmentName?: string; - from: StoreObject | Reference | string; + // Warning: (ae-forgotten-export) The symbol "FragmentType" needs to be exported by the entry point index.d.ts + from: StoreObject | Reference | FragmentType> | string; optimistic?: boolean; variables?: TVars; } // @public type WatchFragmentResult = { - data: TData; + data: MaybeMasked; complete: true; missing?: never; } | { - data: DeepPartial; + data: DeepPartial>; complete: false; missing: MissingTree; }; @@ -2673,7 +2791,7 @@ interface WriteContext extends ReadMergeModifyContext { // Warnings were encountered during analysis: // -// src/cache/core/types/DataProxy.ts:146:7 - (ae-forgotten-export) The symbol "MissingFieldError" needs to be exported by the entry point index.d.ts +// src/cache/core/types/DataProxy.ts:147:7 - (ae-forgotten-export) The symbol "MissingFieldError" needs to be exported by the entry point index.d.ts // src/cache/inmemory/policies.ts:57:3 - (ae-forgotten-export) The symbol "TypePolicy" needs to be exported by the entry point index.d.ts // src/cache/inmemory/policies.ts:161:3 - (ae-forgotten-export) The symbol "KeySpecifier" needs to be exported by the entry point index.d.ts // src/cache/inmemory/policies.ts:161:3 - (ae-forgotten-export) The symbol "KeyArgsFunction" needs to be exported by the entry point index.d.ts @@ -2682,13 +2800,13 @@ interface WriteContext extends ReadMergeModifyContext { // src/cache/inmemory/types.ts:139:3 - (ae-forgotten-export) The symbol "KeyFieldsFunction" needs to be exported by the entry point index.d.ts // src/cache/inmemory/writeToStore.ts:65:7 - (ae-forgotten-export) The symbol "MergeTree" needs to be exported by the entry point index.d.ts // src/core/LocalState.ts:71:3 - (ae-forgotten-export) The symbol "ApolloClient" needs to be exported by the entry point index.d.ts -// src/core/ObservableQuery.ts:116:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts -// src/core/ObservableQuery.ts:117:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:138:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:382:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts -// src/core/types.ts:174:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts -// src/core/types.ts:203:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:275:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:120:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:121:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:155:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:408:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts +// src/core/types.ts:175:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts +// src/core/types.ts:204:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:277:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts // src/utilities/graphql/storeUtils.ts:226:12 - (ae-forgotten-export) The symbol "storeKeyNameStringify" needs to be exported by the entry point index.d.ts // src/utilities/policies/pagination.ts:76:3 - (ae-forgotten-export) The symbol "TRelayEdge" needs to be exported by the entry point index.d.ts // src/utilities/policies/pagination.ts:77:3 - (ae-forgotten-export) The symbol "TRelayPageInfo" needs to be exported by the entry point index.d.ts diff --git a/.api-reports/api-report.api.md b/.api-reports/api-report.api.md index 20848b2c922..e4cd4f85888 100644 --- a/.api-reports/api-report.api.md +++ b/.api-reports/api-report.api.md @@ -47,6 +47,8 @@ export abstract class ApolloCache implements DataProxy { abstract evict(options: Cache_2.EvictOptions): boolean; abstract extract(optimistic?: boolean): TSerialized; // (undocumented) + fragmentMatches?(fragment: InlineFragmentNode, typename: string): boolean; + // (undocumented) gc(): string[]; // Warning: (ae-forgotten-export) The symbol "getApolloCacheMemoryInternals" needs to be exported by the entry point index.d.ts // @@ -55,15 +57,19 @@ export abstract class ApolloCache implements DataProxy { // (undocumented) identify(object: StoreObject | Reference): string | undefined; // (undocumented) + lookupFragment(fragmentName: string): FragmentDefinitionNode | null; + // (undocumented) modify = Record>(options: Cache_2.ModifyOptions): boolean; // (undocumented) abstract performTransaction(transaction: Transaction, optimisticId?: string | null): void; + // Warning: (ae-forgotten-export) The symbol "Unmasked" needs to be exported by the entry point index.d.ts + // // (undocumented) - abstract read(query: Cache_2.ReadOptions): TData | null; + abstract read(query: Cache_2.ReadOptions): Unmasked | null; // (undocumented) - readFragment(options: Cache_2.ReadFragmentOptions, optimistic?: boolean): FragmentType | null; + readFragment(options: Cache_2.ReadFragmentOptions, optimistic?: boolean): Unmasked | null; // (undocumented) - readQuery(options: Cache_2.ReadQueryOptions, optimistic?: boolean): QueryType | null; + readQuery(options: Cache_2.ReadQueryOptions, optimistic?: boolean): Unmasked | null; // (undocumented) recordOptimisticTransaction(transaction: Transaction, optimisticId: string): void; // (undocumented) @@ -76,9 +82,9 @@ export abstract class ApolloCache implements DataProxy { // (undocumented) transformForLink(document: DocumentNode): DocumentNode; // (undocumented) - updateFragment(options: Cache_2.UpdateFragmentOptions, update: (data: TData | null) => TData | null | void): TData | null; + updateFragment(options: Cache_2.UpdateFragmentOptions, update: (data: Unmasked | null) => Unmasked | null | void): Unmasked | null; // (undocumented) - updateQuery(options: Cache_2.UpdateQueryOptions, update: (data: TData | null) => TData | null | void): TData | null; + updateQuery(options: Cache_2.UpdateQueryOptions, update: (data: Unmasked | null) => Unmasked | null | void): Unmasked | null; // (undocumented) abstract watch(watch: Cache_2.WatchOptions): () => void; watchFragment(options: WatchFragmentOptions): Observable>; @@ -119,14 +125,15 @@ export class ApolloClient implements DataProxy { getResolvers(): Resolvers; // (undocumented) link: ApolloLink; - mutate = DefaultContext, TCache extends ApolloCache = ApolloCache>(options: MutationOptions): Promise>; + mutate = DefaultContext, TCache extends ApolloCache = ApolloCache>(options: MutationOptions): Promise>>; onClearStore(cb: () => Promise): () => void; onResetStore(cb: () => Promise): () => void; - query(options: QueryOptions): Promise>; + // Warning: (ae-forgotten-export) The symbol "MaybeMasked" needs to be exported by the entry point index.d.ts + query(options: QueryOptions): Promise>>; // (undocumented) queryDeduplication: boolean; - readFragment(options: DataProxy.Fragment, optimistic?: boolean): T | null; - readQuery(options: DataProxy.Query, optimistic?: boolean): T | null; + readFragment(options: DataProxy.Fragment, optimistic?: boolean): Unmasked | null; + readQuery(options: DataProxy.Query, optimistic?: boolean): Unmasked | null; reFetchObservableQueries(includeStandby?: boolean): Promise[]>; refetchQueries = ApolloCache, TResult = Promise>>(options: RefetchQueriesOptions): RefetchQueriesResult; resetStore(): Promise[] | null>; @@ -135,7 +142,7 @@ export class ApolloClient implements DataProxy { setLocalStateFragmentMatcher(fragmentMatcher: FragmentMatcher): void; setResolvers(resolvers: Resolvers | Resolvers[]): void; stop(): void; - subscribe(options: SubscriptionOptions): Observable>; + subscribe(options: SubscriptionOptions): Observable>>; // (undocumented) readonly typeDefs: ApolloClientOptions["typeDefs"]; // (undocumented) @@ -154,6 +161,7 @@ export interface ApolloClientOptions { connectToDevTools?: boolean; // (undocumented) credentials?: string; + dataMasking?: boolean; // (undocumented) defaultContext?: Partial; defaultOptions?: DefaultOptions; @@ -345,7 +353,7 @@ export interface BaseMutationOptions; ignoreResults?: boolean; notifyOnNetworkStatusChange?: boolean; - onCompleted?: (data: TData, clientOptions?: BaseMutationOptions) => void; + onCompleted?: (data: MaybeMasked, clientOptions?: BaseMutationOptions) => void; onError?: (error: ApolloError, clientOptions?: BaseMutationOptions) => void; } @@ -470,7 +478,7 @@ namespace Cache_2 { // (undocumented) dataId?: string; // (undocumented) - result: TResult; + result: Unmasked; } import DiffResult = DataProxy.DiffResult; import ReadQueryOptions = DataProxy.ReadQueryOptions; @@ -519,6 +527,15 @@ export type ClientParseError = InvariantError & { parseError: Error; }; +// Warning: (ae-forgotten-export) The symbol "UnionToIntersection" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "UnwrapFragmentRefs" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "RemoveFragmentName" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type CombineFragmentRefs> = UnionToIntersection<{ + [K in keyof FragmentRefs]-?: UnwrapFragmentRefs>; +}[keyof FragmentRefs]>; + // @public (undocumented) export type CommonOptions = TOptions & { client?: ApolloClient; @@ -566,6 +583,10 @@ export const createSignalIfSupported: () => { signal: AbortSignal; }; +// @public (undocumented) +interface DataMasking { +} + // @public (undocumented) export namespace DataProxy { // (undocumented) @@ -614,7 +635,7 @@ export namespace DataProxy { // (undocumented) export interface WriteOptions { broadcast?: boolean; - data: TData; + data: Unmasked; overwrite?: boolean; } // (undocumented) @@ -624,8 +645,8 @@ export namespace DataProxy { // @public export interface DataProxy { - readFragment(options: DataProxy.ReadFragmentOptions, optimistic?: boolean): FragmentType | null; - readQuery(options: DataProxy.ReadQueryOptions, optimistic?: boolean): QueryType | null; + readFragment(options: DataProxy.ReadFragmentOptions, optimistic?: boolean): Unmasked | null; + readQuery(options: DataProxy.ReadQueryOptions, optimistic?: boolean): Unmasked | null; writeFragment(options: DataProxy.WriteFragmentOptions): Reference | undefined; writeQuery(options: DataProxy.WriteQueryOptions): Reference | undefined; } @@ -917,11 +938,11 @@ export const fallbackHttpConfig: { // @public (undocumented) type FetchMoreFunction = (fetchMoreOptions: FetchMoreQueryOptions & { - updateQuery?: (previousQueryResult: TData, options: { - fetchMoreResult: TData; + updateQuery?: (previousQueryResult: Unmasked, options: { + fetchMoreResult: Unmasked; variables: TVariables; - }) => TData; -}) => Promise>; + }) => Unmasked; +}) => Promise>>; // @public (undocumented) export interface FetchMoreOptions { @@ -1039,6 +1060,17 @@ interface FragmentRegistryAPI { transform(document: D): D; } +// @public (undocumented) +type FragmentType = [ +TData +] extends [{ + " $fragmentName"?: infer TKey; +}] ? TKey extends string ? { + " $fragmentRefs"?: { + [key in TKey]: TData; + }; +} : never : never; + // @public (undocumented) export const from: typeof ApolloLink.from; @@ -1238,6 +1270,8 @@ export class InMemoryCache extends ApolloCache { // (undocumented) extract(optimistic?: boolean): NormalizedCacheObject; // (undocumented) + fragmentMatches(fragment: InlineFragmentNode, typename: string): boolean; + // (undocumented) gc(options?: { resetResultCache?: boolean; resetResultIdentities?: boolean; @@ -1249,6 +1283,8 @@ export class InMemoryCache extends ApolloCache { // (undocumented) identify(object: StoreObject | Reference): string | undefined; // (undocumented) + lookupFragment(fragmentName: string): FragmentDefinitionNode | null; + // (undocumented) readonly makeVar: typeof makeVar; // (undocumented) modify = Record>(options: Cache_2.ModifyOptions): boolean; @@ -1334,11 +1370,11 @@ export function isNetworkRequestSettled(networkStatus?: NetworkStatus): boolean; // @public (undocumented) export function isReference(obj: any): obj is Reference; -// Warning: (ae-forgotten-export) The symbol "UnionToIntersection" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "UnionToIntersection_2" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "UnionForAny" needs to be exported by the entry point index.d.ts // // @public (undocumented) -type IsStrictlyAny = UnionToIntersection> extends never ? true : false; +type IsStrictlyAny = UnionToIntersection_2> extends never ? true : false; // @public (undocumented) type KeyArgsFunction = (args: Record | null, context: { @@ -1400,7 +1436,7 @@ export interface LazyQueryHookExecOptions extends BaseQueryOptions { // @internal (undocumented) defaultOptions?: Partial>; - onCompleted?: (data: TData) => void; + onCompleted?: (data: MaybeMasked) => void; onError?: (error: ApolloError) => void; } @@ -1485,9 +1521,38 @@ export function makeReference(id: string): Reference; // @public (undocumented) export function makeVar(value: T): ReactiveVar; +// @public (undocumented) +interface MaskFragmentOptions { + // (undocumented) + data: TData; + // (undocumented) + fragment: DocumentNode; + // (undocumented) + fragmentName?: string; +} + +// @public (undocumented) +interface MaskOperationOptions { + // (undocumented) + data: TData; + // (undocumented) + document: DocumentNode; +} + // @public (undocumented) type MaybeAsync = T | PromiseLike; +// Warning: (ae-forgotten-export) The symbol "Prettify" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "RemoveMaskedMarker" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "DataMasking" needs to be exported by the entry point index.d.ts +// +// @public +type MaybeMasked = TData extends { + __masked?: true; +} ? Prettify> : DataMasking extends { + enabled: true; +} ? TData : Unmasked; + // @public (undocumented) export interface MergeInfo { // (undocumented) @@ -1573,10 +1638,10 @@ interface MutationBaseOptions; // Warning: (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts - optimisticResponse?: TData | ((vars: TVariables, { IGNORE }: { + optimisticResponse?: Unmasked> | ((vars: TVariables, { IGNORE }: { IGNORE: IgnoreModifier; - }) => TData | IgnoreModifier); - refetchQueries?: ((result: FetchResult) => InternalRefetchQueriesInclude) | InternalRefetchQueriesInclude; + }) => Unmasked> | IgnoreModifier); + refetchQueries?: ((result: FetchResult>) => InternalRefetchQueriesInclude) | InternalRefetchQueriesInclude; update?: MutationUpdaterFunction; updateQueries?: MutationQueryReducersMap; variables?: TVariables; @@ -1592,7 +1657,7 @@ export interface MutationDataOptions; // @public (undocumented) -export type MutationFunction = ApolloCache> = (options?: MutationFunctionOptions) => Promise>; +export type MutationFunction = ApolloCache> = (options?: MutationFunctionOptions) => Promise>>; // @public (undocumented) export interface MutationFunctionOptions = ApolloCache> extends BaseMutationOptions { @@ -1610,7 +1675,7 @@ export interface MutationOptions = (previousResult: Record, options: { - mutationResult: FetchResult; + mutationResult: FetchResult>; queryName: string | undefined; queryVariables: Record; }) => Record; @@ -1626,7 +1691,7 @@ export type MutationQueryReducersMap { called: boolean; client: ApolloClient; - data?: TData | null; + data?: MaybeMasked | null; error?: ApolloError; loading: boolean; reset: () => void; @@ -1654,7 +1719,7 @@ interface MutationStoreValue { // @public (undocumented) export type MutationTuple = ApolloCache> = [ -mutate: (options?: MutationFunctionOptions) => Promise>, +mutate: (options?: MutationFunctionOptions) => Promise>>, result: MutationResult ]; @@ -1664,7 +1729,7 @@ export type MutationUpdaterFn = (cache: ApolloCache, mutationResult: FetchResult) => void; // @public (undocumented) -export type MutationUpdaterFunction> = (cache: TCache, result: Omit, "context">, options: { +export type MutationUpdaterFunction> = (cache: TCache, result: Omit>, "context">, options: { context?: TContext; variables?: TVariables; }) => void; @@ -1748,20 +1813,20 @@ export interface NormalizedCacheObject { export { Observable } // @public (undocumented) -export class ObservableQuery extends Observable> { +export class ObservableQuery extends Observable>> { constructor({ queryManager, queryInfo, options, }: { queryManager: QueryManager; queryInfo: QueryInfo; options: WatchQueryOptions; }); fetchMore(fetchMoreOptions: FetchMoreQueryOptions & { - updateQuery?: (previousQueryResult: TData, options: { - fetchMoreResult: TFetchData; + updateQuery?: (previousQueryResult: Unmasked, options: { + fetchMoreResult: Unmasked; variables: TFetchVars; - }) => TData; - }): Promise>; + }) => Unmasked; + }): Promise>>; // (undocumented) - getCurrentResult(saveAsLastResult?: boolean): ApolloQueryResult; + getCurrentResult(saveAsLastResult?: boolean): ApolloQueryResult>; // (undocumented) getLastError(variablesMustMatch?: boolean): ApolloError | undefined; // (undocumented) @@ -1778,9 +1843,9 @@ export class ObservableQuery): Promise>; + refetch(variables?: Partial): Promise>>; // (undocumented) - reobserve(newOptions?: Partial>, newNetworkStatus?: NetworkStatus): Promise>; + reobserve(newOptions?: Partial>, newNetworkStatus?: NetworkStatus): Promise>>; // Warning: (ae-forgotten-export) The symbol "Concast" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1792,38 +1857,38 @@ export class ObservableQuery) => void, onError?: (error: any) => void, onComplete?: () => void): ObservableSubscription; + resubscribeAfterError(onNext: (value: ApolloQueryResult>) => void, onError?: (error: any) => void, onComplete?: () => void): ObservableSubscription; // (undocumented) resubscribeAfterError(observer: Observer>): ObservableSubscription; // (undocumented) - result(): Promise>; + result(): Promise>>; // (undocumented) - setOptions(newOptions: Partial>): Promise>; - setVariables(variables: TVariables): Promise | void>; + setOptions(newOptions: Partial>): Promise>>; + setVariables(variables: TVariables): Promise> | void>; // (undocumented) silentSetOptions(newOptions: Partial>): void; startPolling(pollInterval: number): void; stopPolling(): void; subscribeToMore(options: SubscribeToMoreOptions): () => void; - updateQuery(mapFn: (previousQueryResult: TData, options: Pick, "variables">) => TData): void; + updateQuery(mapFn: (previousQueryResult: Unmasked, options: Pick, "variables">) => Unmasked): void; get variables(): TVariables | undefined; } // @public (undocumented) export interface ObservableQueryFields { fetchMore: (fetchMoreOptions: FetchMoreQueryOptions & { - updateQuery?: (previousQueryResult: TData, options: { - fetchMoreResult: TFetchData; + updateQuery?: (previousQueryResult: Unmasked, options: { + fetchMoreResult: Unmasked; variables: TFetchVars; - }) => TData; - }) => Promise>; - refetch: (variables?: Partial) => Promise>; + }) => Unmasked; + }) => Promise>>; + refetch: (variables?: Partial) => Promise>>; // @internal (undocumented) - reobserve: (newOptions?: Partial>, newNetworkStatus?: NetworkStatus) => Promise>; + reobserve: (newOptions?: Partial>, newNetworkStatus?: NetworkStatus) => Promise>>; startPolling: (pollInterval: number) => void; stopPolling: () => void; subscribeToMore: (options: SubscribeToMoreOptions) => () => void; - updateQuery: (mapFn: (previousQueryResult: TData, options: Pick, "variables">) => TData) => void; + updateQuery: (mapFn: (previousQueryResult: Unmasked, options: Pick, "variables">) => Unmasked) => void; variables: TVariables | undefined; } @@ -1998,6 +2063,11 @@ options?: PreloadQueryOptions> & Omit> & Omit ]; +// @public (undocumented) +type Prettify = { + [K in keyof T]: T[K]; +} & {}; + // @public (undocumented) type Primitive = null | undefined | string | number | boolean | symbol | bigint; @@ -2036,7 +2106,7 @@ export interface QueryDataOptions extends BaseQueryOptions { // @internal (undocumented) defaultOptions?: Partial>; - onCompleted?: (data: TData) => void; + onCompleted?: (data: MaybeMasked) => void; onError?: (error: ApolloError) => void; skip?: boolean; } @@ -2124,6 +2194,8 @@ class QueryManager { // (undocumented) clearStore(options?: Cache_2.ResetOptions): Promise; // (undocumented) + readonly dataMasking: boolean; + // (undocumented) readonly defaultContext: Partial; // (undocumented) defaultOptions: DefaultOptions; @@ -2188,14 +2260,22 @@ class QueryManager { onQueryUpdated?: OnQueryUpdated; keepRootFields?: boolean; }, cache?: ApolloCache): Promise>; + // Warning: (ae-forgotten-export) The symbol "MaskFragmentOptions" needs to be exported by the entry point index.d.ts + // + // (undocumented) + maskFragment(options: MaskFragmentOptions): TData; + // Warning: (ae-forgotten-export) The symbol "MaskOperationOptions" needs to be exported by the entry point index.d.ts + // // (undocumented) - mutate, TCache extends ApolloCache>({ mutation, variables, optimisticResponse, updateQueries, refetchQueries, awaitRefetchQueries, update: updateWithProxyFn, onQueryUpdated, fetchPolicy, errorPolicy, keepRootFields, context, }: MutationOptions): Promise>; + maskOperation(options: MaskOperationOptions): MaybeMasked; + // (undocumented) + mutate, TCache extends ApolloCache>({ mutation, variables, optimisticResponse, updateQueries, refetchQueries, awaitRefetchQueries, update: updateWithProxyFn, onQueryUpdated, fetchPolicy, errorPolicy, keepRootFields, context, }: MutationOptions): Promise>>; // (undocumented) mutationStore?: { [mutationId: string]: MutationStoreValue; }; // (undocumented) - query(options: QueryOptions, queryId?: string): Promise>; + query(options: QueryOptions, queryId?: string): Promise>>; // (undocumented) reFetchObservableQueries(includeStandby?: boolean): Promise[]>; // (undocumented) @@ -2209,7 +2289,7 @@ class QueryManager { // (undocumented) readonly ssrMode: boolean; // (undocumented) - startGraphQLSubscription({ query, fetchPolicy, errorPolicy, variables, context, extensions, }: SubscriptionOptions): Observable>; + startGraphQLSubscription(options: SubscriptionOptions): Observable>; stop(): void; // (undocumented) stopQuery(queryId: string): void; @@ -2230,6 +2310,8 @@ interface QueryManagerOptions { // (undocumented) clientAwareness: Record; // (undocumented) + dataMasking: boolean; + // (undocumented) defaultContext: Partial | undefined; // (undocumented) defaultOptions: DefaultOptions; @@ -2281,14 +2363,14 @@ export interface QueryReference extends Q export interface QueryResult extends ObservableQueryFields { called: boolean; client: ApolloClient; - data: TData | undefined; + data: MaybeMasked | undefined; error?: ApolloError; // @deprecated (undocumented) errors?: ReadonlyArray; loading: boolean; networkStatus: NetworkStatus; observable: ObservableQuery; - previousData?: TData; + previousData?: MaybeMasked; } // @public (undocumented) @@ -2398,6 +2480,12 @@ export type RefetchQueryDescriptor = string | DocumentNode; // @public (undocumented) export type RefetchWritePolicy = "merge" | "overwrite"; +// @public (undocumented) +type RemoveFragmentName = T extends any ? Omit : T; + +// @public (undocumented) +type RemoveMaskedMarker = Omit; + // @public (undocumented) class RenderPromises { // (undocumented) @@ -2611,7 +2699,7 @@ export interface SubscriptionOptions { - data?: TData; + data?: MaybeMasked; error?: ApolloError; loading: boolean; // @internal (undocumented) @@ -2664,6 +2752,8 @@ interface TransformCacheEntry { // (undocumented) hasNonreactiveDirective: boolean; // (undocumented) + nonReactiveQuery: DocumentNode; + // (undocumented) serverQuery: DocumentNode | null; } @@ -2693,18 +2783,35 @@ export type TypePolicy = { type UnionForAny = T extends never ? "a" : 1; // @public (undocumented) -type UnionToIntersection = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never; +type UnionToIntersection = (U extends any ? (x: U) => void : never) extends (x: infer I) => void ? I : never; + +// @public (undocumented) +type UnionToIntersection_2 = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never; + +// @public +type Unmasked = TData extends object ? UnwrapFragmentRefs>> : TData; + +// Warning: (ae-forgotten-export) The symbol "CombineFragmentRefs" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type UnwrapFragmentRefs = string extends keyof NonNullable ? TData : " $fragmentRefs" extends keyof NonNullable ? TData extends { + " $fragmentRefs"?: infer FragmentRefs extends object; +} ? Prettify<{ + [K in keyof TData as K extends " $fragmentRefs" ? never : K]: UnwrapFragmentRefs; +} & CombineFragmentRefs> : never : TData extends object ? { + [K in keyof TData]: UnwrapFragmentRefs; +} : TData; // @public (undocumented) type UpdateQueries = MutationOptions["updateQueries"]; // @public (undocumented) -type UpdateQueryFn = (previousQueryResult: TData, options: { +type UpdateQueryFn = (previousQueryResult: Unmasked, options: { subscriptionData: { - data: TSubscriptionData; + data: Unmasked; }; variables?: TSubscriptionVariables; -}) => TData; +}) => Unmasked; // @public (undocumented) export interface UpdateQueryOptions { @@ -2804,19 +2911,21 @@ export function useFragment(options: Us // @public (undocumented) export interface UseFragmentOptions extends Omit, NoInfer_2>, "id" | "query" | "optimistic" | "previousResult" | "returnPartialData">, Omit, "id" | "variables" | "returnPartialData"> { client?: ApolloClient; + // Warning: (ae-forgotten-export) The symbol "FragmentType" needs to be exported by the entry point index.d.ts + // // (undocumented) - from: StoreObject | Reference | string; + from: StoreObject | Reference | FragmentType> | string; // (undocumented) optimistic?: boolean; } // @public (undocumented) export type UseFragmentResult = { - data: TData; + data: MaybeMasked; complete: true; missing?: never; } | { - data: DeepPartial; + data: DeepPartial>; complete: false; missing?: MissingTree; }; @@ -2882,7 +2991,7 @@ export function useReadQuery(queryRef: QueryRef): UseReadQueryResu // @public (undocumented) export interface UseReadQueryResult { - data: TData; + data: MaybeMasked; error: ApolloError | undefined; networkStatus: NetworkStatus; } @@ -2891,7 +3000,7 @@ export interface UseReadQueryResult { export function useSubscription(subscription: DocumentNode | TypedDocumentNode, options?: SubscriptionHookOptions, NoInfer_2>): { restart: () => void; loading: boolean; - data?: TData | undefined; + data?: MaybeMasked | undefined; error?: ApolloError; variables?: TVariables | undefined; }; @@ -2942,7 +3051,7 @@ export interface UseSuspenseQueryResult; // (undocumented) - data: TData; + data: MaybeMasked; // (undocumented) error: ApolloError | undefined; // (undocumented) @@ -2970,18 +3079,18 @@ TVariables export interface WatchFragmentOptions { fragment: DocumentNode | TypedDocumentNode; fragmentName?: string; - from: StoreObject | Reference | string; + from: StoreObject | Reference | FragmentType> | string; optimistic?: boolean; variables?: TVars; } // @public export type WatchFragmentResult = { - data: TData; + data: MaybeMasked; complete: true; missing?: never; } | { - data: DeepPartial; + data: DeepPartial>; complete: false; missing: MissingTree; }; @@ -3032,11 +3141,11 @@ interface WriteContext extends ReadMergeModifyContext { // src/cache/inmemory/policies.ts:161:3 - (ae-forgotten-export) The symbol "KeySpecifier" needs to be exported by the entry point index.d.ts // src/cache/inmemory/policies.ts:161:3 - (ae-forgotten-export) The symbol "KeyArgsFunction" needs to be exported by the entry point index.d.ts // src/cache/inmemory/types.ts:139:3 - (ae-forgotten-export) The symbol "KeyFieldsFunction" needs to be exported by the entry point index.d.ts -// src/core/ObservableQuery.ts:116:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts -// src/core/ObservableQuery.ts:117:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:138:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:382:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:275:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:120:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:121:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:155:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:408:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:277:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts // src/link/http/selectHttpOptionsAndBody.ts:128:32 - (ae-forgotten-export) The symbol "HttpQueryOptions" needs to be exported by the entry point index.d.ts // src/react/hooks/useBackgroundQuery.ts:38:3 - (ae-forgotten-export) The symbol "SubscribeToMoreFunction" needs to be exported by the entry point index.d.ts // src/react/hooks/useBackgroundQuery.ts:54:3 - (ae-forgotten-export) The symbol "FetchMoreFunction" needs to be exported by the entry point index.d.ts diff --git a/.changeset/nasty-camels-pay.md b/.changeset/nasty-camels-pay.md new file mode 100644 index 00000000000..be446b1a1f0 --- /dev/null +++ b/.changeset/nasty-camels-pay.md @@ -0,0 +1,36 @@ +--- +"@apollo/client": minor +--- + +Introduces data masking into Apollo Client. Data masking allows components to access only the data they asked for through GraphQL fragments. This prevents coupling between components that might otherwise implicitly rely on fields not requested by the component. Data masking also provides the benefit that masked fields only rerender components that ask for the field. + +To enable data masking in Apollo Client, set the `dataMasking` option to `true`. + +```ts +new ApolloClient({ + dataMasking: true, + // ... other options +}) +``` + +You can selectively disable data masking using the `@unmask` directive. Apply this to any named fragment to receive all fields requested by the fragment. + +```graphql +query { + user { + id + ...UserFields @unmask + } +} +``` + +To help with migration, use the `@unmask` migrate mode which will add warnings when accessing fields that would otherwise be masked. + +```graphql +query { + user { + id + ...UserFields @unmask(mode: "migrate") + } +} +``` diff --git a/.github/workflows/compare-build-output.yml b/.github/workflows/compare-build-output.yml index 7b97c556665..b367e4672e6 100644 --- a/.github/workflows/compare-build-output.yml +++ b/.github/workflows/compare-build-output.yml @@ -1,9 +1,6 @@ name: Compare Build Output on: pull_request: - branches: - - main - - release-* concurrency: ${{ github.workflow }}-${{ github.ref }} diff --git a/.size-limits.json b/.size-limits.json index 4777b873e7d..c023e57b099 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 40251, - "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 33061 + "dist/apollo-client.min.cjs": 41438, + "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 34206 } diff --git a/config/entryPoints.js b/config/entryPoints.js index 674e1cc9ba2..e1778f4cb27 100644 --- a/config/entryPoints.js +++ b/config/entryPoints.js @@ -17,6 +17,7 @@ const entryPoints = [ { dirs: ["link", "subscriptions"] }, { dirs: ["link", "utils"] }, { dirs: ["link", "ws"] }, + { dirs: ["masking"] }, { dirs: ["react"] }, { dirs: ["react", "components"] }, { dirs: ["react", "context"] }, diff --git a/src/__tests__/ApolloClient.ts b/src/__tests__/ApolloClient.ts index 2eb82151380..298689e9329 100644 --- a/src/__tests__/ApolloClient.ts +++ b/src/__tests__/ApolloClient.ts @@ -3,20 +3,24 @@ import gql from "graphql-tag"; import { ApolloClient, ApolloError, + ApolloQueryResult, DefaultOptions, + ObservableQuery, QueryOptions, makeReference, } from "../core"; import { Kind } from "graphql"; import { Observable } from "../utilities"; -import { ApolloLink } from "../link/core"; +import { ApolloLink, FetchResult } from "../link/core"; import { HttpLink } from "../link/http"; import { createFragmentRegistry, InMemoryCache } from "../cache"; import { itAsync } from "../testing"; import { ObservableStream, spyOnConsole } from "../testing/internal"; import { TypedDocumentNode } from "@graphql-typed-document-node/core"; import { invariant } from "../utilities/globals"; +import { expectTypeOf } from "expect-type"; +import { Masked } from "../masking"; describe("ApolloClient", () => { describe("constructor", () => { @@ -2864,4 +2868,367 @@ describe("ApolloClient", () => { } ); }); + + describe.skip("type tests", () => { + test("client.mutate uses any as masked and unmasked type when using plain DocumentNode", () => { + const mutation = gql` + mutation ($id: ID!) { + updateUser(id: $id) { + id + ...UserFields + } + } + + fragment UserFields on User { + age + } + `; + + const client = new ApolloClient({ cache: new InMemoryCache() }); + + const promise = client.mutate({ + mutation, + optimisticResponse: { foo: "foo" }, + updateQueries: { + TestQuery: (_, { mutationResult }) => { + expectTypeOf(mutationResult.data).toMatchTypeOf(); + + return {}; + }, + }, + refetchQueries(result) { + expectTypeOf(result.data).toMatchTypeOf(); + + return "active"; + }, + update(_, result) { + expectTypeOf(result.data).toMatchTypeOf(); + }, + }); + + expectTypeOf(promise).toMatchTypeOf>>(); + }); + + test("client.mutate uses TData type when using plain TypedDocumentNode", () => { + interface Mutation { + updateUser: { + __typename: "User"; + id: string; + age: number; + }; + } + + interface Variables { + id: string; + } + + const mutation: TypedDocumentNode = gql` + mutation ($id: ID!) { + updateUser(id: $id) { + id + ...UserFields + } + } + + fragment UserFields on User { + age + } + `; + + const client = new ApolloClient({ cache: new InMemoryCache() }); + + const promise = client.mutate({ + variables: { id: "1" }, + mutation, + optimisticResponse: { + updateUser: { __typename: "User", id: "1", age: 30 }, + }, + updateQueries: { + TestQuery: (_, { mutationResult }) => { + expectTypeOf(mutationResult.data).toMatchTypeOf< + Mutation | null | undefined + >(); + + return {}; + }, + }, + refetchQueries(result) { + expectTypeOf(result.data).toMatchTypeOf< + Mutation | null | undefined + >(); + + return "active"; + }, + update(_, result) { + expectTypeOf(result.data).toMatchTypeOf< + Mutation | null | undefined + >(); + }, + }); + + expectTypeOf(promise).toMatchTypeOf>>(); + }); + + test("client.mutate uses masked/unmasked type when using Masked", async () => { + type UserFieldsFragment = { + age: number; + } & { " $fragmentName": "UserFieldsFragment" }; + + type Mutation = { + updateUser: { + __typename: "User"; + id: string; + } & { " $fragmentRefs": { UserFieldsFragment: UserFieldsFragment } }; + }; + + type UnmaskedMutation = { + updateUser: { + __typename: "User"; + id: string; + age: number; + }; + }; + + interface Variables { + id: string; + } + + const mutation: TypedDocumentNode, Variables> = gql` + mutation ($id: ID!) { + updateUser(id: $id) { + id + ...UserFields + } + } + + fragment UserFields on User { + age + } + `; + + const client = new ApolloClient({ cache: new InMemoryCache() }); + + const result = await client.mutate({ + variables: { id: "1" }, + mutation, + optimisticResponse: { + updateUser: { __typename: "User", id: "1", age: 30 }, + }, + updateQueries: { + TestQuery: (_, { mutationResult }) => { + expectTypeOf(mutationResult.data).toMatchTypeOf< + UnmaskedMutation | null | undefined + >(); + + return {}; + }, + }, + refetchQueries(result) { + expectTypeOf(result.data).toMatchTypeOf< + UnmaskedMutation | null | undefined + >(); + + return "active"; + }, + update(_, result) { + expectTypeOf(result.data).toMatchTypeOf< + UnmaskedMutation | null | undefined + >(); + }, + }); + + expectTypeOf(result.data).toMatchTypeOf(); + }); + + test("client.query uses correct masked/unmasked types", async () => { + type UserFieldsFragment = { + age: number; + } & { " $fragmentName": "UserFieldsFragment" }; + + type Query = { + user: { + __typename: "User"; + id: string; + } & { " $fragmentRefs": { UserFieldsFragment: UserFieldsFragment } }; + }; + + interface Variables { + id: string; + } + + const query: TypedDocumentNode, Variables> = gql` + query ($id: ID!) { + user(id: $id) { + id + ...UserFields + } + } + + fragment UserFields on User { + age + } + `; + + const client = new ApolloClient({ cache: new InMemoryCache() }); + const result = await client.query({ variables: { id: "1" }, query }); + + expectTypeOf(result.data).toMatchTypeOf(); + }); + + test("client.watchQuery uses correct masked/unmasked types", async () => { + type UserFieldsFragment = { + age: number; + } & { " $fragmentName": "UserFieldsFragment" }; + + type Query = { + user: { + __typename: "User"; + id: string; + } & { " $fragmentRefs": { UserFieldsFragment: UserFieldsFragment } }; + }; + + type UnmaskedQuery = { + user: { + __typename: "User"; + id: string; + age: number; + }; + }; + + type Subscription = { + updatedUser: { + __typename: "User"; + id: string; + } & { " $fragmentRefs": { UserFieldsFragment: UserFieldsFragment } }; + }; + + type UnmaskedSubscription = { + updatedUser: { + __typename: "User"; + id: string; + age: number; + }; + }; + + interface Variables { + id: string; + } + + const query: TypedDocumentNode, Variables> = gql` + query ($id: ID!) { + user(id: $id) { + id + ...UserFields + } + } + + fragment UserFields on User { + age + } + `; + + const subscription: TypedDocumentNode< + Masked, + Variables + > = gql` + subscription ($id: ID!) { + updatedUser(id: $id) { + id + ...UserFields + } + } + + fragment UserFields on User { + age + } + `; + + const client = new ApolloClient({ cache: new InMemoryCache() }); + const observableQuery = client.watchQuery({ + query, + variables: { id: "1" }, + }); + + expectTypeOf(observableQuery).toMatchTypeOf< + ObservableQuery + >(); + expectTypeOf(observableQuery).not.toMatchTypeOf< + ObservableQuery + >(); + + observableQuery.subscribe({ + next: (result) => { + expectTypeOf(result.data).toMatchTypeOf(); + expectTypeOf(result.data).not.toMatchTypeOf(); + }, + }); + + expectTypeOf(observableQuery.getCurrentResult()).toMatchTypeOf< + ApolloQueryResult + >(); + expectTypeOf(observableQuery.getCurrentResult()).not.toMatchTypeOf< + ApolloQueryResult + >(); + + const fetchMoreResult = await observableQuery.fetchMore({ + updateQuery: (previousData, { fetchMoreResult }) => { + expectTypeOf(previousData).toMatchTypeOf(); + expectTypeOf(previousData).not.toMatchTypeOf(); + + expectTypeOf(fetchMoreResult).toMatchTypeOf(); + expectTypeOf(fetchMoreResult).not.toMatchTypeOf(); + + return {} as UnmaskedQuery; + }, + }); + + expectTypeOf(fetchMoreResult.data).toMatchTypeOf(); + expectTypeOf(fetchMoreResult.data).not.toMatchTypeOf(); + + const refetchResult = await observableQuery.refetch(); + + expectTypeOf(refetchResult.data).toMatchTypeOf(); + expectTypeOf(refetchResult.data).not.toMatchTypeOf(); + + const setVariablesResult = await observableQuery.setVariables({ + id: "2", + }); + + expectTypeOf(setVariablesResult?.data).toMatchTypeOf(); + expectTypeOf(setVariablesResult?.data).not.toMatchTypeOf< + UnmaskedQuery | undefined + >(); + + const setOptionsResult = await observableQuery.setOptions({ + variables: { id: "2" }, + }); + + expectTypeOf(setOptionsResult.data).toMatchTypeOf(); + expectTypeOf(setOptionsResult.data).not.toMatchTypeOf< + UnmaskedQuery | undefined + >(); + + observableQuery.updateQuery((previousData) => { + expectTypeOf(previousData).toMatchTypeOf(); + expectTypeOf(previousData).not.toMatchTypeOf(); + + return {} as UnmaskedQuery; + }); + + observableQuery.subscribeToMore({ + document: subscription, + updateQuery(queryData, { subscriptionData }) { + expectTypeOf(queryData).toMatchTypeOf(); + expectTypeOf(queryData).not.toMatchTypeOf(); + + expectTypeOf( + subscriptionData.data + ).toMatchTypeOf(); + expectTypeOf(subscriptionData.data).not.toMatchTypeOf(); + + return {} as UnmaskedQuery; + }, + }); + }); + }); }); diff --git a/src/__tests__/__snapshots__/exports.ts.snap b/src/__tests__/__snapshots__/exports.ts.snap index d3ce1568654..92acb14a1da 100644 --- a/src/__tests__/__snapshots__/exports.ts.snap +++ b/src/__tests__/__snapshots__/exports.ts.snap @@ -263,6 +263,8 @@ Array [ ] `; +exports[`exports of public entry points @apollo/client/masking 1`] = `Array []`; + exports[`exports of public entry points @apollo/client/react 1`] = ` Array [ "ApolloConsumer", @@ -415,6 +417,7 @@ Array [ "DeepMerger", "DocumentTransform", "Observable", + "addNonReactiveToNamedFragments", "addTypenameToDocument", "argumentsObjectFromField", "asyncMap", @@ -440,6 +443,7 @@ Array [ "getFragmentDefinition", "getFragmentDefinitions", "getFragmentFromSelection", + "getFragmentMaskMode", "getFragmentQueryDocument", "getGraphQLErrorsFromResult", "getInclusionDirectives", @@ -481,6 +485,7 @@ Array [ "mergeOptions", "offsetLimitPagination", "omitDeep", + "preventUnhandledRejection", "print", "relayStylePagination", "removeArgumentsFromDocument", diff --git a/src/__tests__/dataMasking.ts b/src/__tests__/dataMasking.ts new file mode 100644 index 00000000000..44470aa14f8 --- /dev/null +++ b/src/__tests__/dataMasking.ts @@ -0,0 +1,4304 @@ +import { FragmentSpreadNode, Kind, visit } from "graphql"; +import { + ApolloCache, + ApolloClient, + ApolloError, + ApolloLink, + Cache, + DataProxy, + DocumentTransform, + FetchPolicy, + gql, + InMemoryCache, + Observable, + Reference, + TypedDocumentNode, +} from "../core"; +import { + MockedResponse, + MockLink, + MockSubscriptionLink, + wait, +} from "../testing"; +import { ObservableStream, spyOnConsole } from "../testing/internal"; +import { invariant } from "../utilities/globals"; +import { createFragmentRegistry } from "../cache/inmemory/fragmentRegistry"; +import { isSubscriptionOperation } from "../utilities"; +import { MaskedDocumentNode } from "../masking"; + +describe("client.watchQuery", () => { + test("masks queries when dataMasking is `true`", async () => { + type UserFieldsFragment = { + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" }; + + interface Query { + currentUser: { + __typename: "User"; + id: number; + name: string; + } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; + } + + const query: MaskedDocumentNode = gql` + query MaskedQuery { + 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), + }); + + 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", + }, + }); + } + }); + + test("does not mask query when dataMasking is `false`", async () => { + type UserFieldsFragment = { + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" }; + + interface Query { + currentUser: { + __typename: "User"; + id: number; + name: string; + } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; + } + + const query: TypedDocumentNode = gql` + query MaskedQuery { + 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: false, + 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, + }, + }); + } + }); + + test("does not mask query by default", async () => { + type UserFieldsFragment = { + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" }; + + interface Query { + currentUser: { + __typename: "User"; + id: number; + name: string; + } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; + } + + const query: TypedDocumentNode = gql` + query MaskedQuery { + 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({ + 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, + }, + }); + } + }); + + test("does not mask fragments marked with @unmask", async () => { + type UserFieldsFragment = { + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" }; + + interface Query { + currentUser: { + __typename: "User"; + id: number; + name: string; + age: number; + } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; + } + + const query: MaskedDocumentNode = 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, + }, + }); + } + }); + + test("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; + }, + }); + }); + + type UserFieldsFragment = { + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" }; + + interface Query { + currentUser: { + __typename: "User"; + id: number; + name: string; + age: number; + } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; + } + + const query: MaskedDocumentNode = 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, + }, + }); + } + }); + + test("does not mask query when using a cache that does not support it", async () => { + using _ = spyOnConsole("warn"); + + type UserFieldsFragment = { + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" }; + + interface Query { + currentUser: { + __typename: "User"; + id: number; + name: string; + } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; + } + + const query: TypedDocumentNode = gql` + query MaskedQuery { + 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 TestCache(), + 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, + }, + }); + } + + expect(console.warn).toHaveBeenCalledTimes(1); + expect(console.warn).toHaveBeenCalledWith( + expect.stringContaining( + "The configured cache does not support data masking" + ) + ); + }); + + test("masks queries updated by the cache", async () => { + type UserFieldsFragment = { + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" }; + + interface Query { + currentUser: { + __typename: "User"; + id: number; + name: string; + } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; + } + + const query: MaskedDocumentNode = gql` + query MaskedQuery { + 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), + }); + + 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", + }, + }); + } + + client.writeQuery({ + query, + data: { + currentUser: { + __typename: "User", + id: 1, + name: "Test User (updated)", + age: 35, + }, + }, + }); + + { + const { data } = await stream.takeNext(); + + expect(data).toEqual({ + currentUser: { + __typename: "User", + id: 1, + name: "Test User (updated)", + }, + }); + } + }); + + test("does not trigger update when updating field in named fragment", async () => { + type UserFieldsFragment = { + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" }; + + interface Query { + currentUser: { + __typename: "User"; + id: number; + name: string; + } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; + } + + const query: MaskedDocumentNode = gql` + query MaskedQuery { + 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), + }); + + 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", + }, + }); + } + + client.writeQuery({ + query, + data: { + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + age: 35, + }, + }, + }); + + await expect(stream.takeNext()).rejects.toThrow( + new Error("Timeout waiting for next event") + ); + + expect(client.readQuery({ query })).toEqual({ + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + age: 35, + }, + }); + }); + + it.each(["cache-first", "cache-only"] as FetchPolicy[])( + "masks result from cache when using with %s fetch policy", + async (fetchPolicy) => { + type UserFieldsFragment = { + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" }; + + interface Query { + currentUser: { + __typename: "User"; + id: number; + name: string; + } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; + } + + const query: MaskedDocumentNode = gql` + query MaskedQuery { + 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), + }); + + client.writeQuery({ + query, + data: { + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + age: 30, + }, + }, + }); + + const observable = client.watchQuery({ query, fetchPolicy }); + + const stream = new ObservableStream(observable); + + const { data } = await stream.takeNext(); + + expect(data).toEqual({ + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + }, + }); + } + ); + + test("masks cache and network result when using cache-and-network fetch policy", async () => { + type UserFieldsFragment = { + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" }; + + interface Query { + currentUser: { + __typename: "User"; + id: number; + name: string; + } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; + } + + const query: MaskedDocumentNode = gql` + query MaskedQuery { + currentUser { + id + name + ...UserFields + } + } + + fragment UserFields on User { + age + } + `; + + const mocks = [ + { + request: { query }, + result: { + data: { + currentUser: { + __typename: "User", + id: 1, + name: "Test User (server)", + age: 35, + }, + }, + }, + delay: 20, + }, + ]; + + const client = new ApolloClient({ + dataMasking: true, + cache: new InMemoryCache(), + link: new MockLink(mocks), + }); + + client.writeQuery({ + query, + data: { + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + age: 34, + }, + }, + }); + + const observable = client.watchQuery({ + query, + fetchPolicy: "cache-and-network", + }); + + const stream = new ObservableStream(observable); + + { + const { data } = await stream.takeNext(); + + expect(data).toEqual({ + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + }, + }); + } + + { + const { data } = await stream.takeNext(); + + expect(data).toEqual({ + currentUser: { + __typename: "User", + id: 1, + name: "Test User (server)", + }, + }); + } + }); + + test("masks partial cache data when returnPartialData is `true`", async () => { + type UserFieldsFragment = { + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" }; + + interface Query { + currentUser: { + __typename: "User"; + id: number; + name: string; + } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; + } + + const query: MaskedDocumentNode = gql` + query MaskedQuery { + currentUser { + id + name + ...UserFields + } + } + + fragment UserFields on User { + age + } + `; + + const mocks = [ + { + request: { query }, + result: { + data: { + currentUser: { + __typename: "User", + id: 1, + name: "Test User (server)", + age: 35, + }, + }, + }, + delay: 20, + }, + ]; + + const client = new ApolloClient({ + dataMasking: true, + cache: new InMemoryCache(), + link: new MockLink(mocks), + }); + + { + // Silence warning about writing partial data + using _ = spyOnConsole("error"); + + client.writeQuery({ + query, + data: { + // @ts-expect-error writing partial data + currentUser: { + __typename: "User", + id: 1, + age: 34, + }, + }, + }); + } + + const observable = client.watchQuery({ + query, + returnPartialData: true, + }); + + const stream = new ObservableStream(observable); + + { + const { data } = await stream.takeNext(); + + expect(data).toEqual({ + currentUser: { + __typename: "User", + id: 1, + }, + }); + } + + { + const { data } = await stream.takeNext(); + + expect(data).toEqual({ + currentUser: { + __typename: "User", + id: 1, + name: "Test User (server)", + }, + }); + } + }); + + test("masks partial data returned from data on errors with errorPolicy `all`", async () => { + type UserFieldsFragment = { + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" }; + + interface Query { + currentUser: { + __typename: "User"; + id: number; + name: string; + } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; + } + + const query: MaskedDocumentNode = gql` + query MaskedQuery { + currentUser { + id + name + ...UserFields + } + } + + fragment UserFields on User { + age + } + `; + + const mocks = [ + { + request: { query }, + result: { + data: { + currentUser: { + __typename: "User", + id: 1, + name: null, + age: 34, + }, + }, + errors: [{ message: "Couldn't get name" }], + }, + delay: 20, + }, + ]; + + const client = new ApolloClient({ + dataMasking: true, + cache: new InMemoryCache(), + link: new MockLink(mocks), + }); + + const observable = client.watchQuery({ query, errorPolicy: "all" }); + + const stream = new ObservableStream(observable); + + { + const { data, errors } = await stream.takeNext(); + + expect(data).toEqual({ + currentUser: { + __typename: "User", + id: 1, + name: null, + }, + }); + + expect(errors).toEqual([{ message: "Couldn't get name" }]); + } + }); + + it.each([ + "cache-first", + "network-only", + "cache-and-network", + ] as FetchPolicy[])( + "masks result returned from getCurrentResult when using %s fetchPolicy", + async (fetchPolicy) => { + type UserFieldsFragment = { + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" }; + + interface Query { + currentUser: { + __typename: "User"; + id: number; + name: string; + } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; + } + + const query: MaskedDocumentNode = gql` + query MaskedQuery { + currentUser { + id + name + ...UserFields + } + } + + fragment UserFields on User { + age + } + `; + + 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, fetchPolicy }); + const stream = new ObservableStream(observable); + + { + const { data } = await stream.takeNext(); + + expect(data).toEqual({ + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + }, + }); + } + + { + const { data } = observable.getCurrentResult(false); + + expect(data).toEqual({ + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + }, + }); + } + } + ); + + test("warns when accessing a unmasked field while using @unmask with mode: 'migrate'", async () => { + using consoleSpy = spyOnConsole("warn"); + + type UserFieldsFragment = { + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" }; + + interface Query { + currentUser: { + __typename: "User"; + id: number; + name: string; + /** @deprecated */ + age: number; + } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; + } + + const query: MaskedDocumentNode = 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); + } + }); + + test("reads fragment by passing parent object to `from`", async () => { + type UserFieldsFragment = { + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" }; + + interface Query { + currentUser: { + __typename: "User"; + id: number; + name: string; + } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; + } + + const fragment: MaskedDocumentNode = gql` + fragment UserFields on User { + age + } + `; + + const query: MaskedDocumentNode = gql` + query MaskedQuery { + currentUser { + id + name + ...UserFields + } + } + + ${fragment} + `; + + 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 queryStream = new ObservableStream(client.watchQuery({ query })); + + const { data } = await queryStream.takeNext(); + const fragmentObservable = client.watchFragment({ + fragment, + from: data.currentUser, + }); + + const fragmentStream = new ObservableStream(fragmentObservable); + + { + const { data, complete } = await fragmentStream.takeNext(); + + expect(complete).toBe(true); + expect(data).toEqual({ __typename: "User", age: 30 }); + } + }); + + test("warns when passing parent object to `from` when id is masked", async () => { + using _ = spyOnConsole("warn"); + + type UserFieldsFragment = { + id: number; + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" }; + + interface Query { + currentUser: { + __typename: "User"; + name: string; + } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; + } + + const fragment: MaskedDocumentNode = gql` + fragment UserFields on User { + id + age + } + `; + + const query: MaskedDocumentNode = gql` + query MaskedQuery { + currentUser { + name + ...UserFields + } + } + + ${fragment} + `; + + 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 queryStream = new ObservableStream(client.watchQuery({ query })); + + const { data } = await queryStream.takeNext(); + const fragmentObservable = client.watchFragment({ + fragment, + from: data.currentUser, + }); + + expect(console.warn).toHaveBeenCalledTimes(1); + expect(console.warn).toHaveBeenCalledWith( + "Could not identify object passed to `from` for '%s' fragment, either because the object is non-normalized or the key fields are missing. If you are masking this object, please ensure the key fields are requested by the parent object.", + "UserFields" + ); + + const fragmentStream = new ObservableStream(fragmentObservable); + + { + const { data, complete } = await fragmentStream.takeNext(); + + expect(data).toEqual({}); + // TODO: Update when https://github.com/apollographql/apollo-client/issues/12003 is fixed + expect(complete).toBe(true); + } + }); + + test("warns when passing parent object to `from` that is non-normalized", async () => { + using _ = spyOnConsole("warn"); + + type UserFieldsFragment = { + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" }; + + interface Query { + currentUser: { + __typename: "User"; + name: string; + } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; + } + + const fragment: TypedDocumentNode = gql` + fragment UserFields on User { + age + } + `; + + const query: TypedDocumentNode = gql` + query MaskedQuery { + currentUser { + name + ...UserFields + } + } + + ${fragment} + `; + + const mocks = [ + { + request: { query }, + result: { + data: { + currentUser: { + __typename: "User", + name: "Test User", + age: 30, + }, + }, + }, + }, + ]; + + const client = new ApolloClient({ + dataMasking: true, + cache: new InMemoryCache(), + link: new MockLink(mocks), + }); + + const queryStream = new ObservableStream(client.watchQuery({ query })); + + const { data } = await queryStream.takeNext(); + const fragmentObservable = client.watchFragment({ + fragment, + from: data.currentUser, + }); + + expect(console.warn).toHaveBeenCalledTimes(1); + expect(console.warn).toHaveBeenCalledWith( + "Could not identify object passed to `from` for '%s' fragment, either because the object is non-normalized or the key fields are missing. If you are masking this object, please ensure the key fields are requested by the parent object.", + "UserFields" + ); + + const fragmentStream = new ObservableStream(fragmentObservable); + + { + const { data, complete } = await fragmentStream.takeNext(); + + expect(data).toEqual({}); + // TODO: Update when https://github.com/apollographql/apollo-client/issues/12003 is fixed + expect(complete).toBe(true); + } + }); + + test("can lookup unmasked fragments from the fragment registry in queries", async () => { + const fragments = createFragmentRegistry(); + + type UserFieldsFragment = { + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" }; + + interface Query { + currentUser: { + __typename: "User"; + id: number; + name: string; + age: number; + } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; + } + + const query: MaskedDocumentNode = gql` + query MaskedQuery { + currentUser { + id + name + ...UserFields @unmask + } + } + `; + + fragments.register(gql` + fragment UserFields on User { + age + } + `); + + const client = new ApolloClient({ + dataMasking: true, + cache: new InMemoryCache({ fragments }), + link: new ApolloLink(() => { + return Observable.of({ + data: { + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + age: 30, + }, + }, + }); + }), + }); + + const stream = new ObservableStream(client.watchQuery({ query })); + + { + const { data } = await stream.takeNext(); + + expect(data).toEqual({ + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + age: 30, + }, + }); + } + }); + + test("masks result of refetch", async () => { + type UserFieldsFragment = { + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" }; + + interface Query { + currentUser: { + __typename: "User"; + id: number; + name: string; + } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; + } + + const query: MaskedDocumentNode = 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, + }, + }, + }, + }, + { + request: { query }, + result: { + data: { + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + age: 31, + }, + }, + }, + }, + ]; + + 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", + }, + }); + } + + const result = await observable.refetch(); + + expect(result.data).toEqual({ + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + }, + }); + + expect(client.readQuery({ query })).toEqual({ + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + age: 31, + }, + }); + + // Since we don't set notifyOnNetworkStatus to `true`, we don't expect to + // see another result since the masked data did not change + await expect(stream.takeNext()).rejects.toThrow( + new Error("Timeout waiting for next event") + ); + }); + + test("masks result of setVariables", async () => { + type UserFieldsFragment = { + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" }; + + interface Query { + user: { + __typename: "User"; + id: number; + name: string; + } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; + } + + interface Variables { + id: number; + } + + const query: MaskedDocumentNode = gql` + query UnmaskedQuery($id: ID!) { + user(id: $id) { + id + name + ...UserFields + } + } + + fragment UserFields on User { + age + } + `; + + const mocks = [ + { + request: { query, variables: { id: 1 } }, + result: { + data: { + user: { + __typename: "User", + id: 1, + name: "User 1", + age: 30, + }, + }, + }, + }, + { + request: { query, variables: { id: 2 } }, + result: { + data: { + user: { + __typename: "User", + id: 2, + name: "User 2", + age: 31, + }, + }, + }, + }, + ]; + + const client = new ApolloClient({ + dataMasking: true, + cache: new InMemoryCache(), + link: new MockLink(mocks), + }); + + const observable = client.watchQuery({ query, variables: { id: 1 } }); + const stream = new ObservableStream(observable); + + { + const { data } = await stream.takeNext(); + + expect(data).toEqual({ + user: { + __typename: "User", + id: 1, + name: "User 1", + }, + }); + } + + const result = await observable.setVariables({ id: 2 }); + + expect(result?.data).toEqual({ + user: { + __typename: "User", + id: 2, + name: "User 2", + }, + }); + + { + const { data } = await stream.takeNext(); + + expect(data).toEqual({ + user: { + __typename: "User", + id: 2, + name: "User 2", + }, + }); + } + + await expect(stream.takeNext()).rejects.toThrow( + new Error("Timeout waiting for next event") + ); + }); + + test("masks result of setOptions", async () => { + type UserFieldsFragment = { + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" }; + + interface Query { + user: { + __typename: "User"; + id: number; + name: string; + } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; + } + + interface Variables { + id: number; + } + + const query: MaskedDocumentNode = gql` + query UnmaskedQuery($id: ID!) { + user(id: $id) { + id + name + ...UserFields + } + } + + fragment UserFields on User { + age + } + `; + + const mocks = [ + { + request: { query, variables: { id: 1 } }, + result: { + data: { + user: { + __typename: "User", + id: 1, + name: "User 1", + age: 30, + }, + }, + }, + }, + { + request: { query, variables: { id: 2 } }, + result: { + data: { + user: { + __typename: "User", + id: 2, + name: "User 2", + age: 31, + }, + }, + }, + }, + ]; + + const client = new ApolloClient({ + dataMasking: true, + cache: new InMemoryCache(), + link: new MockLink(mocks), + }); + + const observable = client.watchQuery({ query, variables: { id: 1 } }); + const stream = new ObservableStream(observable); + + { + const { data } = await stream.takeNext(); + + expect(data).toEqual({ + user: { + __typename: "User", + id: 1, + name: "User 1", + }, + }); + } + + const result = await observable.setOptions({ variables: { id: 2 } }); + + expect(result?.data).toEqual({ + user: { + __typename: "User", + id: 2, + name: "User 2", + }, + }); + + { + const { data } = await stream.takeNext(); + + expect(data).toEqual({ + user: { + __typename: "User", + id: 2, + name: "User 2", + }, + }); + } + + await expect(stream.takeNext()).rejects.toThrow( + new Error("Timeout waiting for next event") + ); + }); + + test("does not mask data passed to updateQuery", async () => { + type UserFieldsFragment = { + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" }; + + interface Query { + user: { + __typename: "User"; + id: number; + name: string; + } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; + } + + interface Variables { + id: number; + } + + const query: MaskedDocumentNode = gql` + query UnmaskedQuery($id: ID!) { + user(id: $id) { + id + name + ...UserFields + } + } + + fragment UserFields on User { + age + } + `; + + const client = new ApolloClient({ + dataMasking: true, + cache: new InMemoryCache(), + }); + + client.writeQuery({ + query, + data: { + user: { + __typename: "User", + id: 1, + name: "User 1", + age: 30, + }, + }, + variables: { id: 1 }, + }); + + const observable = client.watchQuery({ query, variables: { id: 1 } }); + const stream = new ObservableStream(observable); + + { + const { data } = await stream.takeNext(); + + expect(data).toEqual({ + user: { + __typename: "User", + id: 1, + name: "User 1", + }, + }); + } + + const updateQuery: Parameters[0] = jest.fn( + (previousResult) => { + return { user: { ...previousResult.user, name: "User (updated)" } }; + } + ); + + observable.updateQuery(updateQuery); + + expect(updateQuery).toHaveBeenCalledWith( + { user: { __typename: "User", id: 1, name: "User 1", age: 30 } }, + { variables: { id: 1 } } + ); + + { + const { data } = await stream.takeNext(); + + expect(data).toEqual({ + user: { + __typename: "User", + id: 1, + name: "User (updated)", + }, + }); + } + + await expect(stream.takeNext()).rejects.toThrow( + new Error("Timeout waiting for next event") + ); + }); +}); + +describe("client.watchFragment", () => { + test("masks watched fragments when dataMasking is `true`", async () => { + type UserFieldsFragment = { + __typename: "User"; + id: number; + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" } & { + " $fragmentRefs"?: { NameFieldsFragment: NameFieldsFragment }; + }; + + type NameFieldsFragment = { + __typename: "User"; + firstName: string; + lastName: string; + } & { " $fragmentName"?: "NameFieldsFragment" }; + + const nameFieldsFragment: MaskedDocumentNode = gql` + fragment NameFields on User { + firstName + lastName + } + `; + + const userFieldsFragment: MaskedDocumentNode = gql` + fragment UserFields on User { + id + age + ...NameFields + } + + ${nameFieldsFragment} + `; + + const client = new ApolloClient({ + dataMasking: true, + cache: new InMemoryCache(), + }); + + client.writeFragment({ + fragment: userFieldsFragment, + fragmentName: "UserFields", + data: { + __typename: "User", + id: 1, + age: 30, + firstName: "Test", + lastName: "User", + }, + }); + + const fragmentStream = new ObservableStream( + client.watchFragment({ + fragment: userFieldsFragment, + fragmentName: "UserFields", + from: { __typename: "User", id: 1 }, + }) + ); + + const { data, complete } = await fragmentStream.takeNext(); + + expect(data).toEqual({ __typename: "User", id: 1, age: 30 }); + expect(complete).toBe(true); + invariant(complete, "Should never be incomplete"); + + const nestedFragmentStream = new ObservableStream( + client.watchFragment({ fragment: nameFieldsFragment, from: data }) + ); + + { + const { data, complete } = await nestedFragmentStream.takeNext(); + + expect(complete).toBe(true); + expect(data).toEqual({ + __typename: "User", + firstName: "Test", + lastName: "User", + }); + } + }); + + test("does not mask watched fragments when dataMasking is disabled", async () => { + type UserFieldsFragment = { + __typename: "User"; + id: number; + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" } & { + " $fragmentRefs"?: { NameFieldsFragment: NameFieldsFragment }; + }; + + type NameFieldsFragment = { + __typename: "User"; + firstName: string; + lastName: string; + } & { " $fragmentName"?: "NameFieldsFragment" }; + + const nameFieldsFragment: TypedDocumentNode = gql` + fragment NameFields on User { + __typename + firstName + lastName + } + `; + + const userFieldsFragment: TypedDocumentNode = gql` + fragment UserFields on User { + __typename + id + age + ...NameFields + } + + ${nameFieldsFragment} + `; + + const client = new ApolloClient({ + dataMasking: false, + cache: new InMemoryCache(), + }); + + client.writeFragment({ + fragment: userFieldsFragment, + fragmentName: "UserFields", + data: { + __typename: "User", + id: 1, + age: 30, + firstName: "Test", + lastName: "User", + }, + }); + + const fragmentStream = new ObservableStream( + client.watchFragment({ + fragment: userFieldsFragment, + fragmentName: "UserFields", + from: { __typename: "User", id: 1 }, + }) + ); + + const { data, complete } = await fragmentStream.takeNext(); + + expect(data).toEqual({ + __typename: "User", + id: 1, + age: 30, + firstName: "Test", + lastName: "User", + }); + expect(complete).toBe(true); + invariant(complete, "Should never be incomplete"); + + const nestedFragmentStream = new ObservableStream( + client.watchFragment({ fragment: nameFieldsFragment, from: data }) + ); + + { + const { data, complete } = await nestedFragmentStream.takeNext(); + + expect(complete).toBe(true); + expect(data).toEqual({ + __typename: "User", + firstName: "Test", + lastName: "User", + }); + } + }); + + test("does not mask watched fragments by default", async () => { + type UserFieldsFragment = { + __typename: "User"; + id: number; + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" } & { + " $fragmentRefs"?: { NameFieldsFragment: NameFieldsFragment }; + }; + + type NameFieldsFragment = { + __typename: "User"; + firstName: string; + lastName: string; + } & { " $fragmentName"?: "NameFieldsFragment" }; + + const nameFieldsFragment: TypedDocumentNode = gql` + fragment NameFields on User { + __typename + firstName + lastName + } + `; + + const userFieldsFragment: TypedDocumentNode = gql` + fragment UserFields on User { + __typename + id + age + ...NameFields + } + + ${nameFieldsFragment} + `; + + const client = new ApolloClient({ + cache: new InMemoryCache(), + }); + + client.writeFragment({ + fragment: userFieldsFragment, + fragmentName: "UserFields", + data: { + __typename: "User", + id: 1, + age: 30, + firstName: "Test", + lastName: "User", + }, + }); + + const fragmentStream = new ObservableStream( + client.watchFragment({ + fragment: userFieldsFragment, + fragmentName: "UserFields", + from: { __typename: "User", id: 1 }, + }) + ); + + const { data, complete } = await fragmentStream.takeNext(); + + expect(data).toEqual({ + __typename: "User", + id: 1, + age: 30, + firstName: "Test", + lastName: "User", + }); + expect(complete).toBe(true); + invariant(complete, "Should never be incomplete"); + + const nestedFragmentStream = new ObservableStream( + client.watchFragment({ fragment: nameFieldsFragment, from: data }) + ); + + { + const { data, complete } = await nestedFragmentStream.takeNext(); + + expect(complete).toBe(true); + expect(data).toEqual({ + __typename: "User", + firstName: "Test", + lastName: "User", + }); + } + }); + + test("does not mask watched fragments marked with @unmask", async () => { + type ProfileFieldsFragment = { + __typename: "User"; + age: number; + } & { " $fragmentName"?: "ProfileFieldsFragment" }; + + type UserFieldsFragment = { + __typename: "User"; + id: number; + name: string; + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" } & { + " $fragmentRefs"?: { ProfileFieldsFragment: ProfileFieldsFragment }; + }; + + const fragment: MaskedDocumentNode = gql` + fragment UserFields on User { + id + name + ...ProfileFields @unmask + } + + fragment ProfileFields on User { + age + } + `; + + const client = new ApolloClient({ + dataMasking: true, + cache: new InMemoryCache(), + }); + + client.writeFragment({ + fragment, + fragmentName: "UserFields", + data: { + __typename: "User", + id: 1, + name: "Test User", + age: 30, + }, + }); + + const observable = client.watchFragment({ + fragment, + fragmentName: "UserFields", + from: { __typename: "User", id: 1 }, + }); + + const stream = new ObservableStream(observable); + + { + const { data } = await stream.takeNext(); + + expect(data).toEqual({ + __typename: "User", + id: 1, + name: "Test User", + age: 30, + }); + } + }); + + test("masks watched fragments updated by the cache", async () => { + type ProfileFieldsFragment = { + __typename: "User"; + age: number; + } & { " $fragmentName": "UserFieldsFragment" }; + + type UserFieldsFragment = { + __typename: "User"; + id: number; + name: string; + } & { " $fragmentName": "UserFieldsFragment" } & { + " $fragmentRefs": { ProfileFieldsFragment: ProfileFieldsFragment }; + }; + + const fragment: MaskedDocumentNode = gql` + fragment UserFields on User { + id + name + ...ProfileFields + } + + fragment ProfileFields on User { + age + } + `; + + const client = new ApolloClient({ + dataMasking: true, + cache: new InMemoryCache(), + }); + + client.writeFragment({ + fragment, + fragmentName: "UserFields", + data: { + __typename: "User", + id: 1, + name: "Test User", + age: 30, + }, + }); + + const observable = client.watchFragment({ + fragment, + fragmentName: "UserFields", + from: { __typename: "User", id: 1 }, + }); + + const stream = new ObservableStream(observable); + + { + const { data } = await stream.takeNext(); + + expect(data).toEqual({ + __typename: "User", + id: 1, + name: "Test User", + }); + } + + client.writeFragment({ + fragment, + fragmentName: "UserFields", + data: { + __typename: "User", + id: 1, + name: "Test User (updated)", + // @ts-ignore TODO: Determine how to handle cache writes with masked + // query type + age: 35, + }, + }); + + { + const { data } = await stream.takeNext(); + + expect(data).toEqual({ + __typename: "User", + id: 1, + name: "Test User (updated)", + }); + } + }); + + test("does not trigger update on watched fragment when updating field in named fragment", async () => { + type ProfileFieldsFragment = { + __typename: "User"; + age: number; + } & { " $fragmentName": "UserFieldsFragment" }; + + type UserFieldsFragment = { + __typename: "User"; + id: number; + name: string; + } & { " $fragmentName": "UserFieldsFragment" } & { + " $fragmentRefs": { ProfileFieldsFragment: ProfileFieldsFragment }; + }; + + const fragment: MaskedDocumentNode = gql` + fragment UserFields on User { + id + name + ...ProfileFields + } + + fragment ProfileFields on User { + age + } + `; + + const client = new ApolloClient({ + dataMasking: true, + cache: new InMemoryCache(), + }); + + client.writeFragment({ + fragment, + fragmentName: "UserFields", + data: { + __typename: "User", + id: 1, + name: "Test User", + // @ts-ignore TODO: Determine how to handle cache writes with masking + age: 30, + }, + }); + + const observable = client.watchFragment({ + fragment, + fragmentName: "UserFields", + from: { __typename: "User", id: 1 }, + }); + const stream = new ObservableStream(observable); + + { + const { data } = await stream.takeNext(); + + expect(data).toEqual({ + __typename: "User", + id: 1, + name: "Test User", + }); + } + + client.writeFragment({ + fragment, + fragmentName: "UserFields", + data: { + __typename: "User", + id: 1, + name: "Test User", + // @ts-ignore TODO: Determine how to handle cache writes with masking + age: 35, + }, + }); + + await expect(stream.takeNext()).rejects.toThrow( + new Error("Timeout waiting for next event") + ); + + expect( + client.readFragment({ + fragment, + fragmentName: "UserFields", + id: "User:1", + }) + ).toEqual({ + __typename: "User", + id: 1, + name: "Test User", + age: 35, + }); + }); + + test("triggers update to child watched fragment when updating field in named fragment", async () => { + type ProfileFieldsFragment = { + __typename: "User"; + age: number; + } & { " $fragmentName": "UserFieldsFragment" }; + + type UserFieldsFragment = { + __typename: "User"; + id: number; + name: string; + } & { " $fragmentName": "UserFieldsFragment" } & { + " $fragmentRefs": { ProfileFieldsFragment: ProfileFieldsFragment }; + }; + + const profileFieldsFragment: MaskedDocumentNode< + ProfileFieldsFragment, + never + > = gql` + fragment ProfileFields on User { + age + } + `; + + const userFieldsFragment: MaskedDocumentNode = + gql` + fragment UserFields on User { + id + name + ...ProfileFields + } + + ${profileFieldsFragment} + `; + + const client = new ApolloClient({ + dataMasking: true, + cache: new InMemoryCache(), + }); + + client.writeFragment({ + fragment: userFieldsFragment, + fragmentName: "UserFields", + data: { + __typename: "User", + id: 1, + name: "Test User", + // @ts-ignore TODO: Determine how to handle cache writes with masking + age: 30, + }, + }); + + const userFieldsObservable = client.watchFragment({ + fragment: userFieldsFragment, + fragmentName: "UserFields", + from: { __typename: "User", id: 1 }, + }); + + const nameFieldsObservable = client.watchFragment({ + fragment: profileFieldsFragment, + from: { __typename: "User", id: 1 }, + }); + + const userFieldsStream = new ObservableStream(userFieldsObservable); + const nameFieldsStream = new ObservableStream(nameFieldsObservable); + + { + const { data } = await userFieldsStream.takeNext(); + + expect(data).toEqual({ + __typename: "User", + id: 1, + name: "Test User", + }); + } + + { + const { data } = await nameFieldsStream.takeNext(); + + expect(data).toEqual({ + __typename: "User", + age: 30, + }); + } + + client.writeFragment({ + fragment: userFieldsFragment, + fragmentName: "UserFields", + data: { + __typename: "User", + id: 1, + name: "Test User", + // @ts-ignore TODO: Determine how to handle cache writes with masking + age: 35, + }, + }); + + { + const { data } = await nameFieldsStream.takeNext(); + + expect(data).toEqual({ + __typename: "User", + age: 35, + }); + } + + await expect(userFieldsStream.takeNext()).rejects.toThrow( + new Error("Timeout waiting for next event") + ); + + expect( + client.readFragment({ + fragment: userFieldsFragment, + fragmentName: "UserFields", + id: "User:1", + }) + ).toEqual({ + __typename: "User", + id: 1, + name: "Test User", + age: 35, + }); + }); + + test("does not trigger update to watched fragments when updating field in named fragment with @nonreactive", async () => { + type ProfileFieldsFragment = { + __typename: "User"; + age: number; + lastUpdatedAt: string; + } & { " $fragmentName": "UserFieldsFragment" }; + + type UserFieldsFragment = { + __typename: "User"; + id: number; + lastUpdatedAt: string; + } & { " $fragmentName": "UserFieldsFragment" } & { + " $fragmentRefs": { ProfileFieldsFragment: ProfileFieldsFragment }; + }; + + const profileFieldsFragment: MaskedDocumentNode< + ProfileFieldsFragment, + never + > = gql` + fragment ProfileFields on User { + age + lastUpdatedAt @nonreactive + } + `; + + const userFieldsFragment: MaskedDocumentNode = + gql` + fragment UserFields on User { + id + lastUpdatedAt @nonreactive + ...ProfileFields + } + + ${profileFieldsFragment} + `; + + const client = new ApolloClient({ + dataMasking: true, + cache: new InMemoryCache(), + }); + + client.writeFragment({ + fragment: userFieldsFragment, + fragmentName: "UserFields", + data: { + __typename: "User", + id: 1, + lastUpdatedAt: "2024-01-01", + // @ts-ignore TODO: Determine how to handle cache writes with masking + age: 30, + }, + }); + + const userFieldsObservable = client.watchFragment({ + fragment: userFieldsFragment, + fragmentName: "UserFields", + from: { __typename: "User", id: 1 }, + }); + + const profileFieldsObservable = client.watchFragment({ + fragment: profileFieldsFragment, + from: { __typename: "User", id: 1 }, + }); + + const userFieldsStream = new ObservableStream(userFieldsObservable); + const profileFieldsStream = new ObservableStream(profileFieldsObservable); + + { + const { data } = await userFieldsStream.takeNext(); + + expect(data).toEqual({ + __typename: "User", + id: 1, + lastUpdatedAt: "2024-01-01", + }); + } + + { + const { data } = await profileFieldsStream.takeNext(); + + expect(data).toEqual({ + __typename: "User", + age: 30, + lastUpdatedAt: "2024-01-01", + }); + } + + client.writeFragment({ + fragment: userFieldsFragment, + fragmentName: "UserFields", + data: { + __typename: "User", + id: 1, + lastUpdatedAt: "2024-01-02", + // @ts-ignore TODO: Determine how to handle cache writes with masking + age: 30, + }, + }); + + await expect(userFieldsStream.takeNext()).rejects.toThrow( + new Error("Timeout waiting for next event") + ); + await expect(profileFieldsStream.takeNext()).rejects.toThrow( + new Error("Timeout waiting for next event") + ); + + expect( + client.readFragment({ + fragment: userFieldsFragment, + fragmentName: "UserFields", + id: "User:1", + }) + ).toEqual({ + __typename: "User", + id: 1, + lastUpdatedAt: "2024-01-02", + age: 30, + }); + }); + + test("does not trigger update to watched fragments when updating parent field with @nonreactive and child field", async () => { + type ProfileFieldsFragment = { + __typename: "User"; + age: number; + lastUpdatedAt: string; + } & { " $fragmentName": "UserFieldsFragment" }; + + type UserFieldsFragment = { + __typename: "User"; + id: number; + lastUpdatedAt: string; + } & { " $fragmentName": "UserFieldsFragment" } & { + " $fragmentRefs": { ProfileFieldsFragment: ProfileFieldsFragment }; + }; + + const profileFieldsFragment: MaskedDocumentNode< + ProfileFieldsFragment, + never + > = gql` + fragment ProfileFields on User { + age + lastUpdatedAt @nonreactive + } + `; + + const userFieldsFragment: MaskedDocumentNode = + gql` + fragment UserFields on User { + id + lastUpdatedAt @nonreactive + ...ProfileFields + } + + ${profileFieldsFragment} + `; + + const client = new ApolloClient({ + dataMasking: true, + cache: new InMemoryCache(), + }); + + client.writeFragment({ + fragment: userFieldsFragment, + fragmentName: "UserFields", + data: { + __typename: "User", + id: 1, + lastUpdatedAt: "2024-01-01", + // @ts-ignore TODO: Determine how to handle cache writes with masking + age: 30, + }, + }); + + const userFieldsObservable = client.watchFragment({ + fragment: userFieldsFragment, + fragmentName: "UserFields", + from: { __typename: "User", id: 1 }, + }); + + const profileFieldsObservable = client.watchFragment({ + fragment: profileFieldsFragment, + from: { __typename: "User", id: 1 }, + }); + + const userFieldsStream = new ObservableStream(userFieldsObservable); + const profileFieldsStream = new ObservableStream(profileFieldsObservable); + + { + const { data } = await userFieldsStream.takeNext(); + + expect(data).toEqual({ + __typename: "User", + id: 1, + lastUpdatedAt: "2024-01-01", + }); + } + + { + const { data } = await profileFieldsStream.takeNext(); + + expect(data).toEqual({ + __typename: "User", + age: 30, + lastUpdatedAt: "2024-01-01", + }); + } + + client.writeFragment({ + fragment: userFieldsFragment, + fragmentName: "UserFields", + data: { + __typename: "User", + id: 1, + lastUpdatedAt: "2024-01-02", + // @ts-ignore TODO: Determine how to handle cache writes with masking + age: 31, + }, + }); + + { + const { data } = await profileFieldsStream.takeNext(); + + expect(data).toEqual({ + __typename: "User", + age: 31, + lastUpdatedAt: "2024-01-02", + }); + } + + await expect(userFieldsStream.takeNext()).rejects.toThrow( + new Error("Timeout waiting for next event") + ); + + expect( + client.readFragment({ + fragment: userFieldsFragment, + fragmentName: "UserFields", + id: "User:1", + }) + ).toEqual({ + __typename: "User", + id: 1, + lastUpdatedAt: "2024-01-02", + age: 31, + }); + }); + + test("warns when accessing an unmasked field on a watched fragment while using @unmask with mode: 'migrate'", async () => { + using consoleSpy = spyOnConsole("warn"); + + type ProfileFieldsFragment = { + __typename: "User"; + age: number; + name: string; + } & { " $fragmentName": "UserFieldsFragment" }; + + type UserFieldsFragment = { + __typename: "User"; + id: number; + name: string; + /** @deprecated */ + age: number; + } & { " $fragmentName": "UserFieldsFragment" } & { + " $fragmentRefs": { ProfileFieldsFragment: ProfileFieldsFragment }; + }; + + const fragment: MaskedDocumentNode = gql` + fragment UserFields on User { + id + name + ...ProfileFields @unmask(mode: "migrate") + } + + fragment ProfileFields on User { + age + name + } + `; + + const client = new ApolloClient({ + dataMasking: true, + cache: new InMemoryCache(), + }); + + const observable = client.watchFragment({ + fragment, + fragmentName: "UserFields", + from: { __typename: "User", id: 1 }, + }); + const stream = new ObservableStream(observable); + + { + const { data } = await stream.takeNext(); + data.__typename; + data.id; + data.name; + + expect(consoleSpy.warn).not.toHaveBeenCalled(); + + data.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.", + "fragment 'UserFields'", + "age" + ); + + // Ensure we only warn once + data.age; + expect(consoleSpy.warn).toHaveBeenCalledTimes(1); + } + }); + + test("can lookup unmasked fragments from the fragment registry in watched fragments", async () => { + const fragments = createFragmentRegistry(); + + const profileFieldsFragment = gql` + fragment ProfileFields on User { + age + } + `; + + const userFieldsFragment = gql` + fragment UserFields on User { + id + ...ProfileFields @unmask + } + `; + + fragments.register(profileFieldsFragment); + + const client = new ApolloClient({ + dataMasking: true, + cache: new InMemoryCache({ fragments }), + }); + + client.writeFragment({ + fragment: userFieldsFragment, + fragmentName: "UserFields", + data: { + __typename: "User", + id: 1, + age: 30, + }, + }); + + const observable = client.watchFragment({ + fragment: userFieldsFragment, + fragmentName: "UserFields", + from: { __typename: "User", id: 1 }, + }); + + const stream = new ObservableStream(observable); + + { + const result = await stream.takeNext(); + + expect(result).toEqual({ + data: { + __typename: "User", + id: 1, + age: 30, + }, + complete: true, + }); + } + }); +}); + +describe("client.query", () => { + test("masks data returned from client.query when dataMasking is `true`", async () => { + type UserFieldsFragment = { + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" }; + + interface Query { + currentUser: { + __typename: "User"; + id: number; + name: string; + } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; + } + + const query: MaskedDocumentNode = gql` + query MaskedQuery { + 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), + }); + + const { data } = await client.query({ query }); + + expect(data).toEqual({ + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + }, + }); + }); + + test("does not mask data returned from client.query when dataMasking is `false`", async () => { + type UserFieldsFragment = { + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" }; + + interface Query { + currentUser: { + __typename: "User"; + id: number; + name: string; + } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; + } + + const query: TypedDocumentNode = gql` + query MaskedQuery { + 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: false, + cache: new InMemoryCache(), + link: new MockLink(mocks), + }); + + const { data } = await client.query({ query }); + + expect(data).toEqual({ + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + age: 30, + }, + }); + }); + + test("does not mask data returned from client.query by default", async () => { + type UserFieldsFragment = { + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" }; + + interface Query { + currentUser: { + __typename: "User"; + id: number; + name: string; + } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; + } + + const query: TypedDocumentNode = gql` + query MaskedQuery { + 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({ + cache: new InMemoryCache(), + link: new MockLink(mocks), + }); + + const { data } = await client.query({ query }); + + expect(data).toEqual({ + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + age: 30, + }, + }); + }); + + test("handles errors returned when using errorPolicy `none`", async () => { + const query = gql` + query MaskedQuery { + currentUser { + id + name + ...UserFields + } + } + + fragment UserFields on User { + age + } + `; + + const mocks = [ + { + request: { query }, + result: { + errors: [{ message: "User not logged in" }], + }, + }, + ]; + + const client = new ApolloClient({ + dataMasking: true, + cache: new InMemoryCache(), + link: new MockLink(mocks), + }); + + await expect(client.query({ query, errorPolicy: "none" })).rejects.toEqual( + new ApolloError({ + graphQLErrors: [{ message: "User not logged in" }], + }) + ); + }); + + test("handles errors returned when using errorPolicy `all`", async () => { + const query = gql` + query MaskedQuery { + currentUser { + id + name + ...UserFields + } + } + + fragment UserFields on User { + age + } + `; + + const mocks = [ + { + request: { query }, + result: { + data: { currentUser: null }, + errors: [{ message: "User not logged in" }], + }, + }, + ]; + + const client = new ApolloClient({ + dataMasking: true, + cache: new InMemoryCache(), + link: new MockLink(mocks), + }); + + const { data, errors } = await client.query({ query, errorPolicy: "all" }); + + expect(data).toEqual({ + currentUser: null, + }); + + expect(errors).toEqual([{ message: "User not logged in" }]); + }); + + test("masks fragment data in fields nulled by errors when using errorPolicy `all`", async () => { + const query = gql` + query MaskedQuery { + currentUser { + id + name + ...UserFields + } + } + + fragment UserFields on User { + age + } + `; + + const mocks = [ + { + request: { query }, + result: { + data: { + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + age: null, + }, + }, + errors: [{ message: "Could not determine age" }], + }, + }, + ]; + + const client = new ApolloClient({ + dataMasking: true, + cache: new InMemoryCache(), + link: new MockLink(mocks), + }); + + const { data, errors } = await client.query({ query, errorPolicy: "all" }); + + expect(data).toEqual({ + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + }, + }); + + expect(errors).toEqual([{ message: "Could not determine age" }]); + }); +}); + +describe("client.subscribe", () => { + test("masks data returned from subscriptions when dataMasking is `true`", async () => { + const subscription = gql` + subscription NewCommentSubscription { + addedComment { + id + ...CommentFields + } + } + + fragment CommentFields on Comment { + comment + author + } + `; + + const link = new MockSubscriptionLink(); + + const client = new ApolloClient({ + dataMasking: true, + cache: new InMemoryCache(), + link, + }); + + const observable = client.subscribe({ query: subscription }); + const stream = new ObservableStream(observable); + + link.simulateResult({ + result: { + data: { + addedComment: { + __typename: "Comment", + id: 1, + comment: "Test comment", + author: "Test User", + }, + }, + }, + }); + + const { data } = await stream.takeNext(); + + expect(data).toEqual({ + addedComment: { + __typename: "Comment", + id: 1, + }, + }); + }); + + test("does not mask data returned from subscriptions when dataMasking is `false`", async () => { + const subscription = gql` + subscription NewCommentSubscription { + addedComment { + id + ...CommentFields + } + } + + fragment CommentFields on Comment { + comment + author + } + `; + + const link = new MockSubscriptionLink(); + + const client = new ApolloClient({ + dataMasking: false, + cache: new InMemoryCache(), + link, + }); + + const observable = client.subscribe({ query: subscription }); + const stream = new ObservableStream(observable); + + link.simulateResult({ + result: { + data: { + addedComment: { + __typename: "Comment", + id: 1, + comment: "Test comment", + author: "Test User", + }, + }, + }, + }); + + const { data } = await stream.takeNext(); + + expect(data).toEqual({ + addedComment: { + __typename: "Comment", + id: 1, + comment: "Test comment", + author: "Test User", + }, + }); + }); + + test("does not mask data returned from subscriptions by default", async () => { + const subscription = gql` + subscription NewCommentSubscription { + addedComment { + id + ...CommentFields + } + } + + fragment CommentFields on Comment { + comment + author + } + `; + + const link = new MockSubscriptionLink(); + + const client = new ApolloClient({ + cache: new InMemoryCache(), + link, + }); + + const observable = client.subscribe({ query: subscription }); + const stream = new ObservableStream(observable); + + link.simulateResult({ + result: { + data: { + addedComment: { + __typename: "Comment", + id: 1, + comment: "Test comment", + author: "Test User", + }, + }, + }, + }); + + const { data } = await stream.takeNext(); + + expect(data).toEqual({ + addedComment: { + __typename: "Comment", + id: 1, + comment: "Test comment", + author: "Test User", + }, + }); + }); + + test("handles errors returned from the subscription when errorPolicy is `none`", async () => { + const subscription = gql` + subscription NewCommentSubscription { + addedComment { + id + ...CommentFields + } + } + + fragment CommentFields on Comment { + comment + author + } + `; + + const link = new MockSubscriptionLink(); + + const client = new ApolloClient({ + dataMasking: true, + cache: new InMemoryCache(), + link, + }); + + const observable = client.subscribe({ + query: subscription, + errorPolicy: "none", + }); + const stream = new ObservableStream(observable); + + link.simulateResult({ + result: { + data: { + addedComment: null, + }, + errors: [{ message: "Something went wrong" }], + }, + }); + + const error = await stream.takeError(); + + expect(error).toEqual( + new ApolloError({ graphQLErrors: [{ message: "Something went wrong" }] }) + ); + }); + + test("handles errors returned from the subscription when errorPolicy is `all`", async () => { + const subscription = gql` + subscription NewCommentSubscription { + addedComment { + id + ...CommentFields + } + } + + fragment CommentFields on Comment { + comment + author + } + `; + + const link = new MockSubscriptionLink(); + + const client = new ApolloClient({ + dataMasking: true, + cache: new InMemoryCache(), + link, + }); + + const observable = client.subscribe({ + query: subscription, + errorPolicy: "all", + }); + const stream = new ObservableStream(observable); + + link.simulateResult({ + result: { + data: { + addedComment: null, + }, + errors: [{ message: "Something went wrong" }], + }, + }); + + const { data, errors } = await stream.takeNext(); + + expect(data).toEqual({ addedComment: null }); + expect(errors).toEqual([{ message: "Something went wrong" }]); + }); + + test("masks partial data for errors returned from the subscription when errorPolicy is `all`", async () => { + const subscription = gql` + subscription NewCommentSubscription { + addedComment { + id + ...CommentFields + } + } + + fragment CommentFields on Comment { + comment + author + } + `; + + const link = new MockSubscriptionLink(); + + const client = new ApolloClient({ + dataMasking: true, + cache: new InMemoryCache(), + link, + }); + + const observable = client.subscribe({ + query: subscription, + errorPolicy: "all", + }); + const stream = new ObservableStream(observable); + + link.simulateResult({ + result: { + data: { + addedComment: { + __typename: "Comment", + id: 1, + comment: "Test comment", + author: null, + }, + }, + errors: [{ message: "Could not get author" }], + }, + }); + + const { data, errors } = await stream.takeNext(); + + expect(data).toEqual({ addedComment: { __typename: "Comment", id: 1 } }); + expect(errors).toEqual([{ message: "Could not get author" }]); + }); +}); + +describe("observableQuery.subscribeToMore", () => { + test("masks query data, does not mask updateQuery callback when dataMasking is `true`", async () => { + const fragment = gql` + fragment CommentFields on Comment { + comment + author + } + `; + + const query = gql` + query RecentCommentQuery { + recentComment { + id + ...CommentFields + } + } + + ${fragment} + `; + + const subscription = gql` + subscription NewCommentSubscription { + addedComment { + id + ...CommentFields + } + } + + ${fragment} + `; + + const mocks: MockedResponse[] = [ + { + request: { query }, + result: { + data: { + recentComment: { + __typename: "Comment", + id: 1, + comment: "Recent comment", + author: "Test User", + }, + }, + }, + }, + ]; + + const subscriptionLink = new MockSubscriptionLink(); + const link = ApolloLink.split( + (operation) => isSubscriptionOperation(operation.query), + subscriptionLink, + new MockLink(mocks) + ); + + const client = new ApolloClient({ + dataMasking: true, + cache: new InMemoryCache(), + link, + }); + + const observable = client.watchQuery({ query }); + const queryStream = new ObservableStream(observable); + + { + const { data } = await queryStream.takeNext(); + + expect(data).toEqual({ recentComment: { __typename: "Comment", id: 1 } }); + } + + const updateQuery = jest.fn((_, { subscriptionData }) => { + return { recentComment: subscriptionData.data.addedComment }; + }); + + observable.subscribeToMore({ document: subscription, updateQuery }); + + subscriptionLink.simulateResult({ + result: { + data: { + addedComment: { + __typename: "Comment", + id: 2, + comment: "Most recent comment", + author: "Test User Jr.", + }, + }, + }, + }); + + await wait(0); + + expect(updateQuery).toHaveBeenLastCalledWith( + { + recentComment: { + __typename: "Comment", + id: 1, + comment: "Recent comment", + author: "Test User", + }, + }, + { + variables: {}, + subscriptionData: { + data: { + addedComment: { + __typename: "Comment", + id: 2, + comment: "Most recent comment", + author: "Test User Jr.", + }, + }, + }, + } + ); + + { + const { data } = await queryStream.takeNext(); + + expect(data).toEqual({ recentComment: { __typename: "Comment", id: 2 } }); + } + }); + + test("does not mask data returned from subscriptions when dataMasking is `false`", async () => { + const fragment = gql` + fragment CommentFields on Comment { + comment + author + } + `; + + const query = gql` + query RecentCommentQuery { + recentComment { + id + ...CommentFields + } + } + + ${fragment} + `; + + const subscription = gql` + subscription NewCommentSubscription { + addedComment { + id + ...CommentFields + } + } + + ${fragment} + `; + + const mocks: MockedResponse[] = [ + { + request: { query }, + result: { + data: { + recentComment: { + __typename: "Comment", + id: 1, + comment: "Recent comment", + author: "Test User", + }, + }, + }, + }, + ]; + + const subscriptionLink = new MockSubscriptionLink(); + const link = ApolloLink.split( + (operation) => isSubscriptionOperation(operation.query), + subscriptionLink, + new MockLink(mocks) + ); + + const client = new ApolloClient({ + dataMasking: false, + cache: new InMemoryCache(), + link, + }); + + const observable = client.watchQuery({ query }); + const queryStream = new ObservableStream(observable); + + { + const { data } = await queryStream.takeNext(); + + expect(data).toEqual({ + recentComment: { + __typename: "Comment", + id: 1, + comment: "Recent comment", + author: "Test User", + }, + }); + } + + const updateQuery = jest.fn((_, { subscriptionData }) => { + return { recentComment: subscriptionData.data.addedComment }; + }); + + observable.subscribeToMore({ document: subscription, updateQuery }); + + subscriptionLink.simulateResult({ + result: { + data: { + addedComment: { + __typename: "Comment", + id: 2, + comment: "Most recent comment", + author: "Test User Jr.", + }, + }, + }, + }); + + await wait(0); + + expect(updateQuery).toHaveBeenLastCalledWith( + { + recentComment: { + __typename: "Comment", + id: 1, + comment: "Recent comment", + author: "Test User", + }, + }, + { + variables: {}, + subscriptionData: { + data: { + addedComment: { + __typename: "Comment", + id: 2, + comment: "Most recent comment", + author: "Test User Jr.", + }, + }, + }, + } + ); + + { + const { data } = await queryStream.takeNext(); + + expect(data).toEqual({ + recentComment: { + __typename: "Comment", + id: 2, + comment: "Most recent comment", + author: "Test User Jr.", + }, + }); + } + }); + + test("does not mask data by default", async () => { + const fragment = gql` + fragment CommentFields on Comment { + comment + author + } + `; + + const query = gql` + query RecentCommentQuery { + recentComment { + id + ...CommentFields + } + } + + ${fragment} + `; + + const subscription = gql` + subscription NewCommentSubscription { + addedComment { + id + ...CommentFields + } + } + + ${fragment} + `; + + const mocks: MockedResponse[] = [ + { + request: { query }, + result: { + data: { + recentComment: { + __typename: "Comment", + id: 1, + comment: "Recent comment", + author: "Test User", + }, + }, + }, + }, + ]; + + const subscriptionLink = new MockSubscriptionLink(); + const link = ApolloLink.split( + (operation) => isSubscriptionOperation(operation.query), + subscriptionLink, + new MockLink(mocks) + ); + + const client = new ApolloClient({ cache: new InMemoryCache(), link }); + const observable = client.watchQuery({ query }); + const queryStream = new ObservableStream(observable); + + { + const { data } = await queryStream.takeNext(); + + expect(data).toEqual({ + recentComment: { + __typename: "Comment", + id: 1, + comment: "Recent comment", + author: "Test User", + }, + }); + } + + const updateQuery = jest.fn((_, { subscriptionData }) => { + return { recentComment: subscriptionData.data.addedComment }; + }); + + observable.subscribeToMore({ document: subscription, updateQuery }); + + subscriptionLink.simulateResult({ + result: { + data: { + addedComment: { + __typename: "Comment", + id: 2, + comment: "Most recent comment", + author: "Test User Jr.", + }, + }, + }, + }); + + await wait(0); + + expect(updateQuery).toHaveBeenLastCalledWith( + { + recentComment: { + __typename: "Comment", + id: 1, + comment: "Recent comment", + author: "Test User", + }, + }, + { + variables: {}, + subscriptionData: { + data: { + addedComment: { + __typename: "Comment", + id: 2, + comment: "Most recent comment", + author: "Test User Jr.", + }, + }, + }, + } + ); + + { + const { data } = await queryStream.takeNext(); + + expect(data).toEqual({ + recentComment: { + __typename: "Comment", + id: 2, + comment: "Most recent comment", + author: "Test User Jr.", + }, + }); + } + }); +}); + +describe("client.mutate", () => { + test("masks data returned from client.mutate when dataMasking is `true`", async () => { + type UserFieldsFragment = { + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" }; + + interface Mutation { + updateUser: { + __typename: "User"; + id: number; + name: string; + } & { + " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment }; + }; + } + + const mutation: MaskedDocumentNode = gql` + mutation MaskedMutation { + updateUser { + id + name + ...UserFields + } + } + + fragment UserFields on User { + age + } + `; + + const mocks = [ + { + request: { query: mutation }, + result: { + data: { + updateUser: { + __typename: "User", + id: 1, + name: "Test User", + age: 30, + }, + }, + }, + }, + ]; + + const client = new ApolloClient({ + dataMasking: true, + cache: new InMemoryCache(), + link: new MockLink(mocks), + }); + + const { data } = await client.mutate({ mutation }); + + expect(data).toEqual({ + updateUser: { + __typename: "User", + id: 1, + name: "Test User", + }, + }); + }); + + test("does not mask data returned from client.mutate when dataMasking is `false`", async () => { + type UserFieldsFragment = { + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" }; + + interface Mutation { + updateUser: { + __typename: "User"; + id: number; + name: string; + } & { + " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment }; + }; + } + + const mutation: TypedDocumentNode = gql` + mutation MaskedMutation { + updateUser { + id + name + ...UserFields + } + } + + fragment UserFields on User { + age + } + `; + + const mocks = [ + { + request: { query: mutation }, + result: { + data: { + updateUser: { + __typename: "User", + id: 1, + name: "Test User", + age: 30, + }, + }, + }, + }, + ]; + + const client = new ApolloClient({ + dataMasking: false, + cache: new InMemoryCache(), + link: new MockLink(mocks), + }); + + const { data } = await client.mutate({ mutation }); + + expect(data).toEqual({ + updateUser: { + __typename: "User", + id: 1, + name: "Test User", + age: 30, + }, + }); + }); + + test("does not mask data returned from client.mutate by default", async () => { + type UserFieldsFragment = { + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" }; + + interface Mutation { + updateUser: { + __typename: "User"; + id: number; + name: string; + } & { + " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment }; + }; + } + + const mutation: TypedDocumentNode = gql` + mutation MaskedMutation { + updateUser { + id + name + ...UserFields + } + } + + fragment UserFields on User { + age + } + `; + + const mocks = [ + { + request: { query: mutation }, + result: { + data: { + updateUser: { + __typename: "User", + id: 1, + name: "Test User", + age: 30, + }, + }, + }, + }, + ]; + + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: new MockLink(mocks), + }); + + const { data } = await client.mutate({ mutation }); + + expect(data).toEqual({ + updateUser: { + __typename: "User", + id: 1, + name: "Test User", + age: 30, + }, + }); + }); + + test("does not mask data passed to update function", async () => { + type UserFieldsFragment = { + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" }; + + interface Mutation { + updateUser: { + __typename: "User"; + id: number; + name: string; + } & { + " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment }; + }; + } + + const mutation: MaskedDocumentNode = gql` + mutation MaskedMutation { + updateUser { + id + name + ...UserFields + } + } + + fragment UserFields on User { + age + } + `; + + const mocks = [ + { + request: { query: mutation }, + result: { + data: { + updateUser: { + __typename: "User", + id: 1, + name: "Test User", + age: 30, + }, + }, + }, + }, + ]; + + const cache = new InMemoryCache(); + const client = new ApolloClient({ + dataMasking: true, + cache, + link: new MockLink(mocks), + }); + + const update = jest.fn(); + await client.mutate({ mutation, update }); + + expect(update).toHaveBeenCalledTimes(1); + expect(update).toHaveBeenCalledWith( + cache, + { + data: { + updateUser: { + __typename: "User", + id: 1, + name: "Test User", + age: 30, + }, + }, + }, + { context: undefined, variables: {} } + ); + }); + + test("handles errors returned when using errorPolicy `none`", async () => { + type UserFieldsFragment = { + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" }; + + interface Mutation { + updateUser: { + __typename: "User"; + id: number; + name: string; + } & { + " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment }; + }; + } + + const mutation: MaskedDocumentNode = gql` + mutation MaskedMutation { + updateUser { + id + name + ...UserFields + } + } + + fragment UserFields on User { + age + } + `; + + const mocks = [ + { + request: { query: mutation }, + result: { + errors: [{ message: "User not logged in" }], + }, + }, + ]; + + const client = new ApolloClient({ + dataMasking: true, + cache: new InMemoryCache(), + link: new MockLink(mocks), + }); + + await expect( + client.mutate({ mutation, errorPolicy: "none" }) + ).rejects.toEqual( + new ApolloError({ + graphQLErrors: [{ message: "User not logged in" }], + }) + ); + }); + + test("handles errors returned when using errorPolicy `all`", async () => { + type UserFieldsFragment = { + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" }; + + interface Mutation { + updateUser: + | ({ + __typename: "User"; + id: number; + name: string; + } & { + " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment }; + }) + | null; + } + + const mutation: MaskedDocumentNode = gql` + mutation MaskedMutation { + updateUser { + id + name + ...UserFields + } + } + + fragment UserFields on User { + age + } + `; + + const mocks = [ + { + request: { query: mutation }, + result: { + data: { updateUser: null }, + errors: [{ message: "User not logged in" }], + }, + }, + ]; + + const client = new ApolloClient({ + dataMasking: true, + cache: new InMemoryCache(), + link: new MockLink(mocks), + }); + + const { data, errors } = await client.mutate({ + mutation, + errorPolicy: "all", + }); + + expect(data).toEqual({ updateUser: null }); + expect(errors).toEqual([{ message: "User not logged in" }]); + }); + + test("masks fragment data in fields nulled by errors when using errorPolicy `all`", async () => { + type UserFieldsFragment = { + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" }; + + interface Mutation { + updateUser: { + __typename: "User"; + id: number; + name: string; + } & { + " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment }; + }; + } + + const mutation: MaskedDocumentNode = gql` + mutation MaskedMutation { + updateUser { + id + name + ...UserFields + } + } + + fragment UserFields on User { + age + } + `; + + const mocks = [ + { + request: { query: mutation }, + result: { + data: { + updateUser: { + __typename: "User", + id: 1, + name: "Test User", + age: null, + }, + }, + errors: [{ message: "Could not determine age" }], + }, + }, + ]; + + const client = new ApolloClient({ + dataMasking: true, + cache: new InMemoryCache(), + link: new MockLink(mocks), + }); + + const { data, errors } = await client.mutate({ + mutation, + errorPolicy: "all", + }); + + expect(data).toEqual({ + updateUser: { + __typename: "User", + id: 1, + name: "Test User", + }, + }); + + expect(errors).toEqual([{ message: "Could not determine age" }]); + }); +}); + +class TestCache extends ApolloCache { + public diff(query: Cache.DiffOptions): DataProxy.DiffResult { + return {}; + } + + public evict(): boolean { + return false; + } + + public extract(optimistic?: boolean): unknown { + return undefined; + } + + public performTransaction( + transaction: (c: ApolloCache) => void + ): void { + transaction(this); + } + + public read( + query: Cache.ReadOptions + ): T | null { + return null; + } + + public removeOptimistic(id: string): void {} + + public reset(): Promise { + return new Promise(() => null); + } + + public restore(serializedState: unknown): ApolloCache { + return this; + } + + public watch(watch: Cache.WatchOptions): () => void { + return function () {}; + } + + public write( + _: Cache.WriteOptions + ): Reference | undefined { + return; + } +} diff --git a/src/__tests__/exports.ts b/src/__tests__/exports.ts index d50b933810c..6005727e782 100644 --- a/src/__tests__/exports.ts +++ b/src/__tests__/exports.ts @@ -21,6 +21,7 @@ import * as linkSchema from "../link/schema"; import * as linkSubscriptions from "../link/subscriptions"; import * as linkUtils from "../link/utils"; import * as linkWS from "../link/ws"; +import * as masking from "../masking"; import * as react from "../react"; import * as reactComponents from "../react/components"; import * as reactContext from "../react/context"; @@ -68,6 +69,7 @@ describe("exports of public entry points", () => { check("@apollo/client/link/subscriptions", linkSubscriptions); check("@apollo/client/link/utils", linkUtils); check("@apollo/client/link/ws", linkWS); + check("@apollo/client/masking", masking); check("@apollo/client/react", react); check("@apollo/client/react/components", reactComponents); check("@apollo/client/react/context", reactContext); diff --git a/src/cache/core/__tests__/cache.ts b/src/cache/core/__tests__/cache.ts index accee868452..59744928535 100644 --- a/src/cache/core/__tests__/cache.ts +++ b/src/cache/core/__tests__/cache.ts @@ -3,6 +3,7 @@ import { ApolloCache } from "../cache"; import { Cache, DataProxy } from "../.."; import { Reference } from "../../../utilities/graphql/storeUtils"; import { expectTypeOf } from "expect-type"; + class TestCache extends ApolloCache { constructor() { super(); diff --git a/src/cache/core/cache.ts b/src/cache/core/cache.ts index 1186605c0b5..409b4c5c985 100644 --- a/src/cache/core/cache.ts +++ b/src/cache/core/cache.ts @@ -1,15 +1,21 @@ -import type { DocumentNode } from "graphql"; +import type { + DocumentNode, + FragmentDefinitionNode, + InlineFragmentNode, +} from "graphql"; import { wrap } from "optimism"; import type { StoreObject, Reference, DeepPartial, + NoInfer, } from "../../utilities/index.js"; import { Observable, cacheSizes, defaultCacheSizes, + getFragmentDefinition, getFragmentQueryDocument, mergeDeepArray, } from "../../utilities/index.js"; @@ -23,6 +29,13 @@ import type { } from "../../core/types.js"; import type { MissingTree } from "./types/common.js"; import { equalByQuery } from "../../core/equalByQuery.js"; +import { invariant } from "../../utilities/globals/index.js"; +import { maskFragment } from "../../core/masking.js"; +import type { + FragmentType, + MaybeMasked, + Unmasked, +} from "../../masking/index.js"; export type Transaction = (c: ApolloCache) => void; @@ -45,7 +58,7 @@ export interface WatchFragmentOptions { * * @docGroup 1. Required options */ - from: StoreObject | Reference | string; + from: StoreObject | Reference | FragmentType> | string; /** * Any variables that the GraphQL fragment may depend on. * @@ -76,12 +89,12 @@ export interface WatchFragmentOptions { */ export type WatchFragmentResult = | { - data: TData; + data: MaybeMasked; complete: true; missing?: never; } | { - data: DeepPartial; + data: DeepPartial>; complete: false; missing: MissingTree; }; @@ -93,7 +106,7 @@ export abstract class ApolloCache implements DataProxy { // core API public abstract read( query: Cache.ReadOptions - ): TData | null; + ): Unmasked | null; public abstract write( write: Cache.WriteOptions ): Reference | undefined; @@ -134,6 +147,26 @@ export abstract class ApolloCache implements DataProxy { public abstract removeOptimistic(id: string): void; + // Data masking API + + // Used by data masking to determine if an inline fragment with a type + // condition matches a given typename. + // + // If not implemented by a cache subclass, data masking will effectively be + // disabled since we will not be able to accurately determine if a given type + // condition for a union or interface matches a particular type. + public fragmentMatches?( + fragment: InlineFragmentNode, + typename: string + ): boolean; + + // Function used to lookup a fragment when a fragment definition is not part + // of the GraphQL document. This is useful for caches, such as InMemoryCache, + // that register fragments ahead of time so they can be referenced by name. + public lookupFragment(fragmentName: string): FragmentDefinitionNode | null { + return null; + } + // Transactional API // The batch method is intended to replace/subsume both performTransaction @@ -205,7 +238,7 @@ export abstract class ApolloCache implements DataProxy { public readQuery( options: Cache.ReadQueryOptions, optimistic = !!options.optimistic - ): QueryType | null { + ): Unmasked | null { return this.read({ ...options, rootId: options.id || "ROOT_QUERY", @@ -225,20 +258,34 @@ export abstract class ApolloCache implements DataProxy { ...otherOptions } = options; const query = this.getFragmentDoc(fragment, fragmentName); + // While our TypeScript types do not allow for `undefined` as a valid + // `from`, its possible `useFragment` gives us an `undefined` since it + // calls` cache.identify` and provides that value to `from`. We are + // adding this fix here however to ensure those using plain JavaScript + // and using `cache.identify` themselves will avoid seeing the obscure + // warning. + const id = + typeof from === "undefined" || typeof from === "string" ? + from + : this.identify(from); + const dataMasking = !!(options as any)[Symbol.for("apollo.dataMasking")]; + + if (__DEV__) { + const actualFragmentName = + fragmentName || getFragmentDefinition(fragment).name.value; + + if (!id) { + invariant.warn( + "Could not identify object passed to `from` for '%s' fragment, either because the object is non-normalized or the key fields are missing. If you are masking this object, please ensure the key fields are requested by the parent object.", + actualFragmentName + ); + } + } const diffOptions: Cache.DiffOptions = { ...otherOptions, returnPartialData: true, - id: - // While our TypeScript types do not allow for `undefined` as a valid - // `from`, its possible `useFragment` gives us an `undefined` since it - // calls` cache.identify` and provides that value to `from`. We are - // adding this fix here however to ensure those using plain JavaScript - // and using `cache.identify` themselves will avoid seeing the obscure - // warning. - typeof from === "undefined" || typeof from === "string" ? - from - : this.identify(from), + id, query, optimistic, }; @@ -249,21 +296,22 @@ export abstract class ApolloCache implements DataProxy { return this.watch({ ...diffOptions, immediate: true, - callback(diff) { + callback: (diff) => { + const data = + dataMasking ? + maskFragment(diff.result, fragment, this, fragmentName) + : diff.result; + if ( // Always ensure we deliver the first result latestDiff && - equalByQuery( - query, - { data: latestDiff?.result }, - { data: diff.result } - ) + equalByQuery(query, { data: latestDiff?.result }, { data }) ) { return; } const result = { - data: diff.result as DeepPartial, + data, complete: !!diff.complete, } as WatchFragmentResult; @@ -273,7 +321,7 @@ export abstract class ApolloCache implements DataProxy { ); } - latestDiff = diff; + latestDiff = { ...diff, result: data }; observer.next(result); }, }); @@ -292,7 +340,7 @@ export abstract class ApolloCache implements DataProxy { public readFragment( options: Cache.ReadFragmentOptions, optimistic = !!options.optimistic - ): FragmentType | null { + ): Unmasked | null { return this.read({ ...options, query: this.getFragmentDoc(options.fragment, options.fragmentName), @@ -332,8 +380,8 @@ export abstract class ApolloCache implements DataProxy { public updateQuery( options: Cache.UpdateQueryOptions, - update: (data: TData | null) => TData | null | void - ): TData | null { + update: (data: Unmasked | null) => Unmasked | null | void + ): Unmasked | null { return this.batch({ update(cache) { const value = cache.readQuery(options); @@ -347,8 +395,8 @@ export abstract class ApolloCache implements DataProxy { public updateFragment( options: Cache.UpdateFragmentOptions, - update: (data: TData | null) => TData | null | void - ): TData | null { + update: (data: Unmasked | null) => Unmasked | null | void + ): Unmasked | null { return this.batch({ update(cache) { const value = cache.readFragment(options); diff --git a/src/cache/core/types/Cache.ts b/src/cache/core/types/Cache.ts index 0fa70742e15..58916943212 100644 --- a/src/cache/core/types/Cache.ts +++ b/src/cache/core/types/Cache.ts @@ -1,6 +1,7 @@ import { DataProxy } from "./DataProxy.js"; import type { AllFieldsModifier, Modifiers } from "./common.js"; import type { ApolloCache } from "../cache.js"; +import type { Unmasked } from "../../../masking/index.js"; export namespace Cache { export type WatchCallback = ( @@ -28,7 +29,7 @@ export namespace Cache { extends Omit, "id">, Omit, "data"> { dataId?: string; - result: TResult; + result: Unmasked; } export interface DiffOptions diff --git a/src/cache/core/types/DataProxy.ts b/src/cache/core/types/DataProxy.ts index 4e3e0f9cc73..746cd174a21 100644 --- a/src/cache/core/types/DataProxy.ts +++ b/src/cache/core/types/DataProxy.ts @@ -3,6 +3,7 @@ import type { TypedDocumentNode } from "@graphql-typed-document-node/core"; import type { MissingFieldError } from "./common.js"; import type { Reference } from "../../../utilities/index.js"; +import type { Unmasked } from "../../../masking/index.js"; export namespace DataProxy { export interface Query { @@ -93,7 +94,7 @@ export namespace DataProxy { /** * The data you will be writing to the store. */ - data: TData; + data: Unmasked; /** * Whether to notify query watchers (default: true). */ @@ -148,7 +149,7 @@ export interface DataProxy { readQuery( options: DataProxy.ReadQueryOptions, optimistic?: boolean - ): QueryType | null; + ): Unmasked | null; /** * Reads a GraphQL fragment from any arbitrary id. If there is more than @@ -158,7 +159,7 @@ export interface DataProxy { readFragment( options: DataProxy.ReadFragmentOptions, optimistic?: boolean - ): FragmentType | null; + ): Unmasked | null; /** * Writes a GraphQL query to the root query id. diff --git a/src/cache/inmemory/__tests__/policies.ts b/src/cache/inmemory/__tests__/policies.ts index 01beba28898..736f2a0874f 100644 --- a/src/cache/inmemory/__tests__/policies.ts +++ b/src/cache/inmemory/__tests__/policies.ts @@ -1191,7 +1191,7 @@ describe("type policies", function () { query: DocumentNode | TypedDocumentNode, variables?: TVars ) { - cache.writeQuery({ query, variables, data }); + cache.writeQuery({ query, variables, data }); expect(cache.readQuery({ query, variables })).toEqual(data); } diff --git a/src/cache/inmemory/inMemoryCache.ts b/src/cache/inmemory/inMemoryCache.ts index fe62023f165..7be5b921ab5 100644 --- a/src/cache/inmemory/inMemoryCache.ts +++ b/src/cache/inmemory/inMemoryCache.ts @@ -3,7 +3,11 @@ import { invariant } from "../../utilities/globals/index.js"; // Make builtins like Map and Set safe to use with non-extensible objects. import "./fixPolyfills.js"; -import type { DocumentNode } from "graphql"; +import type { + DocumentNode, + FragmentDefinitionNode, + InlineFragmentNode, +} from "graphql"; import type { OptimisticWrapperFunction } from "optimism"; import { wrap } from "optimism"; import { equal } from "@wry/equality"; @@ -528,6 +532,17 @@ export class InMemoryCache extends ApolloCache { return this.addTypenameToDocument(this.addFragmentsToDocument(document)); } + public fragmentMatches( + fragment: InlineFragmentNode, + typename: string + ): boolean { + return this.policies.fragmentMatches(fragment, typename); + } + + public lookupFragment(fragmentName: string): FragmentDefinitionNode | null { + return this.config.fragments?.lookup(fragmentName) || null; + } + protected broadcastWatches(options?: BroadcastOptions) { if (!this.txCount) { this.watches.forEach((c) => this.maybeBroadcastWatch(c, options)); diff --git a/src/core/ApolloClient.ts b/src/core/ApolloClient.ts index 7dce981b88d..fe8b47491d6 100644 --- a/src/core/ApolloClient.ts +++ b/src/core/ApolloClient.ts @@ -5,7 +5,8 @@ import type { DocumentNode, FormattedExecutionResult } from "graphql"; import type { FetchResult, GraphQLRequest } from "../link/core/index.js"; import { ApolloLink, execute } from "../link/core/index.js"; import type { ApolloCache, DataProxy, Reference } from "../cache/index.js"; -import type { DocumentTransform, Observable } from "../utilities/index.js"; +import type { DocumentTransform } from "../utilities/index.js"; +import type { Observable } from "../utilities/index.js"; import { version } from "../version.js"; import type { UriFunction } from "../link/http/index.js"; import { HttpLink } from "../link/http/index.js"; @@ -144,6 +145,13 @@ export interface ApolloClientOptions { * @since 3.11.0 */ devtools?: DevtoolsOptions; + + /** + * Determines if data masking is enabled for the client. + * + * @defaultValue false + */ + dataMasking?: boolean; } // Though mergeOptions now resides in @apollo/client/utilities, it was @@ -156,6 +164,7 @@ import type { WatchFragmentOptions, WatchFragmentResult, } from "../cache/core/cache.js"; +import type { MaybeMasked, Unmasked } from "../masking/index.js"; export { mergeOptions }; /** @@ -237,6 +246,7 @@ export class ApolloClient implements DataProxy { name: clientAwarenessName, version: clientAwarenessVersion, devtools, + dataMasking, } = options; let { link } = options; @@ -292,6 +302,7 @@ export class ApolloClient implements DataProxy { documentTransform, queryDeduplication, ssrMode, + dataMasking: !!dataMasking, clientAwareness: { name: clientAwarenessName!, version: clientAwarenessVersion!, @@ -443,7 +454,9 @@ export class ApolloClient implements DataProxy { public query< T = any, TVariables extends OperationVariables = OperationVariables, - >(options: QueryOptions): Promise> { + >( + options: QueryOptions + ): Promise>> { if (this.defaultOptions.query) { options = mergeOptions(this.defaultOptions.query, options); } @@ -478,7 +491,7 @@ export class ApolloClient implements DataProxy { TCache extends ApolloCache = ApolloCache, >( options: MutationOptions - ): Promise> { + ): Promise>> { if (this.defaultOptions.mutate) { options = mergeOptions(this.defaultOptions.mutate, options); } @@ -494,8 +507,18 @@ export class ApolloClient implements DataProxy { public subscribe< T = any, TVariables extends OperationVariables = OperationVariables, - >(options: SubscriptionOptions): Observable> { - return this.queryManager.startGraphQLSubscription(options); + >( + options: SubscriptionOptions + ): Observable>> { + return this.queryManager + .startGraphQLSubscription(options) + .map((result) => ({ + ...result, + data: this.queryManager.maskOperation({ + document: options.query, + data: result.data, + }), + })); } /** @@ -510,7 +533,7 @@ export class ApolloClient implements DataProxy { public readQuery( options: DataProxy.Query, optimistic: boolean = false - ): T | null { + ): Unmasked | null { return this.cache.readQuery(options, optimistic); } @@ -537,7 +560,10 @@ export class ApolloClient implements DataProxy { >( options: WatchFragmentOptions ): Observable> { - return this.cache.watchFragment(options); + return this.cache.watchFragment({ + ...options, + [Symbol.for("apollo.dataMasking")]: this.queryManager.dataMasking, + }); } /** @@ -557,7 +583,7 @@ export class ApolloClient implements DataProxy { public readFragment( options: DataProxy.Fragment, optimistic: boolean = false - ): T | null { + ): Unmasked | null { return this.cache.readFragment(options, optimistic); } diff --git a/src/core/ObservableQuery.ts b/src/core/ObservableQuery.ts index c7bd4dec582..d134b1989eb 100644 --- a/src/core/ObservableQuery.ts +++ b/src/core/ObservableQuery.ts @@ -16,6 +16,7 @@ import { iterateObserversSafely, fixObservableSubclass, getQueryDefinition, + preventUnhandledRejection, } from "../utilities/index.js"; import { ApolloError, isApolloError } from "../errors/index.js"; import type { QueryManager } from "./QueryManager.js"; @@ -36,6 +37,7 @@ import type { MissingFieldError } from "../cache/index.js"; import type { MissingTree } from "../cache/core/types/common.js"; import { equalByQuery } from "./equalByQuery.js"; import type { TODO } from "../utilities/types/TODO.js"; +import type { MaybeMasked, Unmasked } from "../masking/index.js"; const { assign, hasOwnProperty } = Object; @@ -65,7 +67,7 @@ interface Last { export class ObservableQuery< TData = any, TVariables extends OperationVariables = OperationVariables, -> extends Observable> { +> extends Observable>> { public readonly options: WatchQueryOptions; public readonly queryId: string; public readonly queryName?: string; @@ -89,7 +91,9 @@ export class ObservableQuery< private isTornDown: boolean; private queryManager: QueryManager; - private observers = new Set>>(); + private observers = new Set< + Observer>> + >(); private subscriptions = new Set(); private waitForOwnResult: boolean; @@ -117,7 +121,7 @@ export class ObservableQuery< queryInfo: QueryInfo; options: WatchQueryOptions; }) { - super((observer: Observer>) => { + super((observer: Observer>>) => { // Zen Observable has its own error function, so in order to log correctly // we need to provide a custom error callback. try { @@ -135,7 +139,7 @@ export class ObservableQuery< if (last && last.error) { observer.error && observer.error(last.error); } else if (last && last.result) { - observer.next && observer.next(last.result); + observer.next && observer.next(this.maskResult(last.result)); } // Initiate observation of this query if it hasn't been reported to @@ -164,6 +168,7 @@ export class ObservableQuery< this.isTornDown = false; this.subscribeToMore = this.subscribeToMore.bind(this); + this.maskResult = this.maskResult.bind(this); const { watchQuery: { fetchPolicy: defaultFetchPolicy = "cache-first" } = {}, @@ -196,13 +201,13 @@ export class ObservableQuery< this.queryName = opDef && opDef.name && opDef.name.value; } - public result(): Promise> { + public result(): Promise>> { return new Promise((resolve, reject) => { // TODO: this code doesn’t actually make sense insofar as the observer // will never exist in this.observers due how zen-observable wraps observables. // https://github.com/zenparsing/zen-observable/blob/master/src/Observable.js#L169 - const observer: Observer> = { - next: (result: ApolloQueryResult) => { + const observer: Observer>> = { + next: (result) => { resolve(result); // Stop the query within the QueryManager if we can before @@ -235,7 +240,9 @@ export class ObservableQuery< this.queryInfo.resetDiff(); } - public getCurrentResult(saveAsLastResult = true): ApolloQueryResult { + private getCurrentFullResult( + saveAsLastResult = true + ): ApolloQueryResult { // Use the last result as long as the variables match this.variables. const lastResult = this.getLastResult(true); @@ -317,6 +324,12 @@ export class ObservableQuery< return result; } + public getCurrentResult( + saveAsLastResult = true + ): ApolloQueryResult> { + return this.maskResult(this.getCurrentFullResult(saveAsLastResult)); + } + // Compares newResult to the snapshot we took of this.lastResult when it was // first received. public isDifferentFromLastResult( @@ -327,9 +340,13 @@ export class ObservableQuery< return true; } + const documentInfo = this.queryManager.getDocumentInfo(this.query); + const dataMasking = this.queryManager.dataMasking; + const query = dataMasking ? documentInfo.nonReactiveQuery : this.query; + const resultIsDifferent = - this.queryManager.getDocumentInfo(this.query).hasNonreactiveDirective ? - !equalByQuery(this.query, this.last.result, newResult, this.variables) + dataMasking || documentInfo.hasNonreactiveDirective ? + !equalByQuery(query, this.last.result, newResult, this.variables) : !equal(this.last.result, newResult); return ( @@ -379,7 +396,7 @@ export class ObservableQuery< */ public refetch( variables?: Partial - ): Promise> { + ): Promise>> { const reobserveOptions: Partial> = { // Always disable polling for refetches. pollInterval: 0, @@ -431,14 +448,14 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`, >( fetchMoreOptions: FetchMoreQueryOptions & { updateQuery?: ( - previousQueryResult: TData, + previousQueryResult: Unmasked, options: { - fetchMoreResult: TFetchData; + fetchMoreResult: Unmasked; variables: TFetchVars; } - ) => TData; + ) => Unmasked; } - ): Promise> { + ): Promise>> { const combinedOptions = { ...(fetchMoreOptions.query ? fetchMoreOptions : ( { @@ -521,8 +538,8 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`, optimistic: false, }, (previous) => - updateQuery(previous!, { - fetchMoreResult: fetchMoreResult.data, + updateQuery(previous! as any, { + fetchMoreResult: fetchMoreResult.data as any, variables: combinedOptions.variables as TFetchVars, }) ); @@ -535,7 +552,7 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`, cache.writeQuery({ query: combinedOptions.query, variables: combinedOptions.variables, - data: fetchMoreResult.data, + data: fetchMoreResult.data as Unmasked, }); } }, @@ -562,15 +579,18 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`, // expects that the first argument always contains previous result // data, but not `undefined`. const lastResult = this.getLast("result")!; - const data = updateQuery!(lastResult.data, { - fetchMoreResult: fetchMoreResult.data, + const data = updateQuery!(lastResult.data as Unmasked, { + fetchMoreResult: fetchMoreResult.data as Unmasked, variables: combinedOptions.variables as TFetchVars, }); - this.reportResult({ ...lastResult, data }, this.variables); + this.reportResult( + { ...lastResult, data: data as TData }, + this.variables + ); } - return fetchMoreResult; + return this.maskResult(fetchMoreResult); }) .finally(() => { // In case the cache writes above did not generate a broadcast @@ -609,7 +629,7 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`, context: options.context, }) .subscribe({ - next: (subscriptionData: { data: TSubscriptionData }) => { + next: (subscriptionData: { data: Unmasked }) => { const { updateQuery } = options; if (updateQuery) { this.updateQuery( @@ -641,7 +661,7 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`, public setOptions( newOptions: Partial> - ): Promise> { + ): Promise>> { return this.reobserve(newOptions); } @@ -672,7 +692,7 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`, */ public setVariables( variables: TVariables - ): Promise | void> { + ): Promise> | void> { if (equal(this.variables, variables)) { // If we have no observers, then we don't actually want to make a network // request. As soon as someone observes the query, the request will kick @@ -704,9 +724,9 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`, */ public updateQuery( mapFn: ( - previousQueryResult: TData, + previousQueryResult: Unmasked, options: Pick, "variables"> - ) => TData + ) => Unmasked ): void { const { queryManager } = this; const { result } = queryManager.cache.diff({ @@ -716,7 +736,7 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`, optimistic: false, }); - const newResult = mapFn(result!, { + const newResult = mapFn(result! as Unmasked, { variables: (this as any).variables, }); @@ -1005,13 +1025,16 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`, public reobserve( newOptions?: Partial>, newNetworkStatus?: NetworkStatus - ): Promise> { - return this.reobserveAsConcast(newOptions, newNetworkStatus) - .promise as TODO; + ): Promise>> { + return preventUnhandledRejection( + this.reobserveAsConcast(newOptions, newNetworkStatus).promise.then( + this.maskResult as TODO + ) + ); } public resubscribeAfterError( - onNext: (value: ApolloQueryResult) => void, + onNext: (value: ApolloQueryResult>) => void, onError?: (error: any) => void, onComplete?: () => void ): ObservableSubscription; @@ -1044,7 +1067,7 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`, // save the fetchMore result as this.lastResult, causing it to be // ignored due to the this.isDifferentFromLastResult check in // this.reportResult. - this.getCurrentResult(false), + this.getCurrentFullResult(false), this.variables ); } @@ -1063,7 +1086,7 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`, this.updateLastResult(result, variables); } if (lastError || isDifferent) { - iterateObserversSafely(this.observers, "next", result); + iterateObserversSafely(this.observers, "next", this.maskResult(result)); } } @@ -1107,6 +1130,20 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`, private transformDocument(document: DocumentNode) { return this.queryManager.transform(document); } + + private maskResult( + result: ApolloQueryResult + ): ApolloQueryResult> { + return result && "data" in result ? + { + ...result, + data: this.queryManager.maskOperation({ + document: this.query, + data: result.data, + }), + } + : result; + } } // Necessary because the ObservableQuery constructor has a different diff --git a/src/core/QueryInfo.ts b/src/core/QueryInfo.ts index d4058bae2af..2c065972b78 100644 --- a/src/core/QueryInfo.ts +++ b/src/core/QueryInfo.ts @@ -17,6 +17,7 @@ import { import { NetworkStatus, isNetworkRequestInFlight } from "./networkStatus.js"; import type { ApolloError } from "../errors/index.js"; import type { QueryManager } from "./QueryManager.js"; +import type { Unmasked } from "../masking/index.js"; export type QueryStoreValue = Pick< QueryInfo, @@ -419,7 +420,7 @@ export class QueryInfo { if (this.shouldWrite(result, options.variables)) { cache.writeQuery({ query: document, - data: result.data as T, + data: result.data as Unmasked, variables: options.variables, overwrite: cacheWriteBehavior === CacheWriteBehavior.OVERWRITE, }); diff --git a/src/core/QueryManager.ts b/src/core/QueryManager.ts index ea91c0abdea..e499a3d78b7 100644 --- a/src/core/QueryManager.ts +++ b/src/core/QueryManager.ts @@ -8,6 +8,7 @@ import { equal } from "@wry/equality"; import type { ApolloLink, FetchResult } from "../link/core/index.js"; import { execute } from "../link/core/index.js"; import { + addNonReactiveToNamedFragments, defaultCacheSizes, hasDirectives, isExecutionPatchIncrementalResult, @@ -95,6 +96,7 @@ interface TransformCacheEntry { hasClientExports: boolean; hasForcedResolvers: boolean; hasNonreactiveDirective: boolean; + nonReactiveQuery: DocumentNode; clientQuery: DocumentNode | null; serverQuery: DocumentNode | null; defaultVars: OperationVariables; @@ -104,6 +106,19 @@ interface TransformCacheEntry { import type { DefaultOptions } from "./ApolloClient.js"; import { Trie } from "@wry/trie"; import { AutoCleanedWeakCache, cacheSizes } from "../utilities/index.js"; +import { maskFragment, maskOperation } from "./masking.js"; +import type { MaybeMasked, Unmasked } from "../masking/index.js"; + +interface MaskFragmentOptions { + fragment: DocumentNode; + data: TData; + fragmentName?: string; +} + +interface MaskOperationOptions { + document: DocumentNode; + data: TData; +} export interface QueryManagerOptions { cache: ApolloCache; @@ -117,6 +132,7 @@ export interface QueryManagerOptions { localState: LocalState; assumeImmutableResults: boolean; defaultContext: Partial | undefined; + dataMasking: boolean; } export class QueryManager { @@ -128,6 +144,7 @@ export class QueryManager { public readonly documentTransform: DocumentTransform; public readonly ssrMode: boolean; public readonly defaultContext: Partial; + public readonly dataMasking: boolean; private queryDeduplication: boolean; private clientAwareness: Record = {}; @@ -163,6 +180,7 @@ export class QueryManager { this.localState = options.localState; this.ssrMode = options.ssrMode; this.assumeImmutableResults = options.assumeImmutableResults; + this.dataMasking = options.dataMasking; const documentTransform = options.documentTransform; this.documentTransform = documentTransform ? @@ -219,7 +237,7 @@ export class QueryManager { keepRootFields, context, }: MutationOptions): Promise< - FetchResult + FetchResult> > { invariant( mutation, @@ -303,7 +321,9 @@ export class QueryManager { const storeResult: typeof result = { ...result }; if (typeof refetchQueries === "function") { - refetchQueries = refetchQueries(storeResult); + refetchQueries = refetchQueries( + storeResult as FetchResult> + ); } if (errorPolicy === "ignore" && graphQLResultHasError(storeResult)) { @@ -337,7 +357,13 @@ export class QueryManager { // ExecutionPatchResult has arrived and we have assembled the // multipart response into a single result. if (!("hasNext" in storeResult) || storeResult.hasNext === false) { - resolve(storeResult); + resolve({ + ...storeResult, + data: self.maskOperation({ + document: mutation, + data: storeResult.data, + }) as any, + }); } }, @@ -454,7 +480,7 @@ export class QueryManager { if (complete && currentQueryResult) { // Run our reducer using the current query result and the mutation result. const nextQueryResult = updater(currentQueryResult, { - mutationResult: result, + mutationResult: result as FetchResult>, queryName: (document && getOperationName(document)) || void 0, queryVariables: variables!, }); @@ -530,7 +556,7 @@ export class QueryManager { // either a SingleExecutionResult or the final ExecutionPatchResult, // call the update function. if (isFinalResult) { - update(cache as TCache, result, { + update(cache as TCache, result as FetchResult>, { context: mutation.context, variables: mutation.variables, }); @@ -676,12 +702,14 @@ export class QueryManager { hasClientExports: hasClientExports(document), hasForcedResolvers: this.localState.shouldForceResolvers(document), hasNonreactiveDirective: hasDirectives(["nonreactive"], document), + nonReactiveQuery: addNonReactiveToNamedFragments(document), clientQuery: this.localState.clientQuery(document), serverQuery: removeDirectivesFromDocument( [ { name: "client", remove: true }, { name: "connection" }, { name: "nonreactive" }, + { name: "unmask" }, ], document ), @@ -762,7 +790,7 @@ export class QueryManager { public query( options: QueryOptions, queryId = this.generateQueryId() - ): Promise> { + ): Promise>> { invariant( options.query, "query option is required. You must specify your GraphQL document " + @@ -784,10 +812,17 @@ export class QueryManager { "pollInterval option only supported on watchQuery." ); - return this.fetchQuery(queryId, { - ...options, - query: this.transform(options.query), - }).finally(() => this.stopQuery(queryId)); + const query = this.transform(options.query); + + return this.fetchQuery(queryId, { ...options, query }) + .then( + (result) => + result && { + ...result, + data: this.maskOperation({ document: query, data: result.data }), + } + ) + .finally(() => this.stopQuery(queryId)); } private queryIdCounter = 1; @@ -967,14 +1002,17 @@ export class QueryManager { this.getQuery(observableQuery.queryId).setObservableQuery(observableQuery); } - public startGraphQLSubscription({ - query, - fetchPolicy, - errorPolicy = "none", - variables, - context = {}, - extensions = {}, - }: SubscriptionOptions): Observable> { + public startGraphQLSubscription( + options: SubscriptionOptions + ): Observable> { + let { query, variables } = options; + const { + fetchPolicy, + errorPolicy = "none", + context = {}, + extensions = {}, + } = options; + query = this.transform(query); variables = this.getVariables(query, variables); @@ -1516,6 +1554,25 @@ export class QueryManager { return results; } + public maskOperation( + options: MaskOperationOptions + ): MaybeMasked { + const { document, data } = options; + + return ( + this.dataMasking ? + maskOperation(data, document, this.cache) + : data) as MaybeMasked; + } + + public maskFragment(options: MaskFragmentOptions) { + const { data, fragment, fragmentName } = options; + + return this.dataMasking ? + maskFragment(data, fragment, this.cache, fragmentName) + : data; + } + private fetchQueryByPolicy( queryInfo: QueryInfo, { diff --git a/src/core/__tests__/QueryManager/index.ts b/src/core/__tests__/QueryManager/index.ts index e0bda61a9a5..def25285543 100644 --- a/src/core/__tests__/QueryManager/index.ts +++ b/src/core/__tests__/QueryManager/index.ts @@ -224,7 +224,7 @@ describe("QueryManager", () => { } { const result = observableQuery.getCurrentResult(); return { - data: result.data, + data: result.data as TData, partial: !!result.partial, }; } diff --git a/src/core/__tests__/masking.test.ts b/src/core/__tests__/masking.test.ts new file mode 100644 index 00000000000..27063e7be1d --- /dev/null +++ b/src/core/__tests__/masking.test.ts @@ -0,0 +1,2462 @@ +import { maskFragment, maskOperation } from "../masking.js"; +import { InMemoryCache, gql } from "../index.js"; +import { deepFreeze } from "../../utilities/common/maybeDeepFreeze.js"; +import { InvariantError } from "../../utilities/globals/index.js"; +import { spyOnConsole, withProdMode } from "../../testing/internal/index.js"; + +describe("maskOperation", () => { + test("throws when passing document with no operation to maskOperation", () => { + const document = gql` + fragment Foo on Bar { + foo + } + `; + + expect(() => maskOperation({}, document, new InMemoryCache())).toThrow( + new InvariantError( + "Expected a parsed GraphQL document with a query, mutation, or subscription." + ) + ); + }); + + test("throws when passing string query to maskOperation", () => { + const document = ` + query Foo { + foo + } + `; + + expect(() => + maskOperation( + {}, + // @ts-expect-error + document, + new InMemoryCache() + ) + ).toThrow( + new InvariantError( + 'Expecting a parsed GraphQL document. Perhaps you need to wrap the query string in a "gql" tag? http://docs.apollostack.com/apollo-client/core.html#gql' + ) + ); + }); + + test("throws when passing multiple operations to maskOperation", () => { + const document = gql` + query Foo { + foo + } + + query Bar { + bar + } + `; + + expect(() => maskOperation({}, document, new InMemoryCache())).toThrow( + new InvariantError("Ambiguous GraphQL document: contains 2 operations") + ); + }); + + test("returns null when data is null", () => { + const query = gql` + query { + foo + ...QueryFields + } + + fragment QueryFields on Query { + bar + } + `; + + const data = maskOperation(null, query, new InMemoryCache()); + + expect(data).toBe(null); + }); + + test("returns undefined when data is undefined", () => { + const query = gql` + query { + foo + ...QueryFields + } + + fragment QueryFields on Query { + bar + } + `; + + const data = maskOperation(undefined, query, new InMemoryCache()); + + expect(data).toBe(undefined); + }); + + test("strips top-level fragment data from query", () => { + const query = gql` + query { + foo + ...QueryFields + } + + fragment QueryFields on Query { + bar + } + `; + + const data = maskOperation( + { foo: true, bar: true }, + query, + new InMemoryCache() + ); + + expect(data).toEqual({ foo: true }); + }); + + test("strips fragment data from nested object", () => { + const query = gql` + query { + user { + id + ...UserFields + } + } + + fragment UserFields on User { + name + } + `; + + const data = maskOperation( + deepFreeze({ user: { __typename: "User", id: 1, name: "Test User" } }), + query, + new InMemoryCache() + ); + + expect(data).toEqual({ user: { __typename: "User", id: 1 } }); + }); + + test("retains __typename in the result", () => { + const query = gql` + query { + user { + id + profile { + id + } + ...UserFields + } + } + + fragment UserFields on Query { + age + } + `; + + const data = maskOperation( + deepFreeze({ + user: { + __typename: "User", + id: 1, + age: 30, + profile: { __typename: "Profile", id: 2 }, + }, + }), + query, + new InMemoryCache() + ); + + expect(data).toEqual({ + user: { + __typename: "User", + id: 1, + profile: { __typename: "Profile", id: 2 }, + }, + }); + }); + + test("masks fragments from nested objects when query gets fields from same object", () => { + const query = gql` + query { + user { + profile { + __typename + id + } + ...UserFields + } + } + fragment UserFields on User { + profile { + id + fullName + } + } + `; + + const data = maskOperation( + deepFreeze({ + user: { + __typename: "User", + profile: { __typename: "Profile", id: "1", fullName: "Test User" }, + }, + }), + query, + new InMemoryCache() + ); + + expect(data).toEqual({ + user: { + __typename: "User", + profile: { __typename: "Profile", id: "1" }, + }, + }); + }); + + test("handles nulls in child selection sets", () => { + const query = gql` + query { + user { + profile { + id + } + ...UserFields + } + } + fragment UserFields on User { + profile { + id + fullName + } + } + `; + + const nullUser = maskOperation( + deepFreeze({ user: null }), + query, + new InMemoryCache() + ); + const nullProfile = maskOperation( + deepFreeze({ user: { __typename: "User", profile: null } }), + query, + new InMemoryCache() + ); + + expect(nullUser).toEqual({ user: null }); + expect(nullProfile).toEqual({ + user: { __typename: "User", profile: null }, + }); + }); + + test("handles nulls in arrays", () => { + const query = gql` + query { + users { + profile { + id + } + ...UserFields + } + } + fragment UserFields on User { + profile { + id + fullName + } + } + `; + + const data = maskOperation( + deepFreeze({ + users: [ + null, + { __typename: "User", profile: null }, + { + __typename: "User", + profile: { __typename: "Profile", id: "1", fullName: "Test User" }, + }, + ], + }), + query, + new InMemoryCache() + ); + + expect(data).toEqual({ + users: [ + null, + { __typename: "User", profile: null }, + { __typename: "User", profile: { __typename: "Profile", id: "1" } }, + ], + }); + }); + + test("deep freezes the masked result if the original data is frozen", () => { + const query = gql` + query { + user { + id + ...UserFields + } + } + + fragment UserFields on User { + name + } + `; + + const frozenData = maskOperation( + deepFreeze({ user: { __typename: "User", id: 1, name: "Test User" } }), + query, + new InMemoryCache() + ); + + const nonFrozenData = maskOperation( + { user: { __typename: "User", id: 1, name: "Test User" } }, + query, + new InMemoryCache() + ); + + expect(Object.isFrozen(frozenData)).toBe(true); + expect(Object.isFrozen(nonFrozenData)).toBe(false); + }); + + test("strips fragment data from arrays", () => { + const query = gql` + query { + users { + id + ...UserFields + } + } + + fragment UserFields on User { + name + } + `; + + const data = maskOperation( + deepFreeze({ + users: [ + { __typename: "User", id: 1, name: "Test User 1" }, + { __typename: "User", id: 2, name: "Test User 2" }, + ], + }), + query, + new InMemoryCache() + ); + + expect(data).toEqual({ + users: [ + { __typename: "User", id: 1 }, + { __typename: "User", id: 2 }, + ], + }); + }); + + test("strips multiple fragments in the same selection set", () => { + const query = gql` + query { + user { + id + ...UserProfileFields + ...UserAvatarFields + } + } + + fragment UserProfileFields on User { + age + } + + fragment UserAvatarFields on User { + avatarUrl + } + `; + + const data = maskOperation( + { + user: { + __typename: "User", + id: 1, + age: 30, + avatarUrl: "https://example.com/avatar.jpg", + }, + }, + query, + new InMemoryCache() + ); + + expect(data).toEqual({ + user: { __typename: "User", id: 1 }, + }); + }); + + test("strips multiple fragments across different selection sets", () => { + const query = gql` + query { + user { + id + ...UserFields + } + post { + id + ...PostFields + } + } + + fragment UserFields on User { + name + } + + fragment PostFields on Post { + title + } + `; + + const data = maskOperation( + { + user: { + __typename: "User", + id: 1, + name: "test user", + }, + post: { + __typename: "Post", + id: 1, + title: "Test Post", + }, + }, + query, + new InMemoryCache() + ); + + expect(data).toEqual({ + user: { __typename: "User", id: 1 }, + post: { __typename: "Post", id: 1 }, + }); + }); + + test("leaves overlapping fields in query", () => { + const query = gql` + query { + user { + id + birthdate + ...UserProfileFields + } + } + + fragment UserProfileFields on User { + birthdate + name + } + `; + + const data = maskOperation( + deepFreeze({ + user: { + __typename: "User", + id: 1, + birthdate: "1990-01-01", + name: "Test User", + }, + }), + query, + new InMemoryCache() + ); + + expect(data).toEqual({ + user: { __typename: "User", id: 1, birthdate: "1990-01-01" }, + }); + }); + + test("does not strip inline fragments", () => { + const cache = new InMemoryCache({ + possibleTypes: { Profile: ["UserProfile"] }, + }); + + const query = gql` + query { + user { + id + ... @defer { + name + } + } + profile { + ... on UserProfile { + avatarUrl + } + } + } + `; + + const data = maskOperation( + deepFreeze({ + user: { + __typename: "User", + id: 1, + name: "Test User", + }, + profile: { + __typename: "UserProfile", + avatarUrl: "https://example.com/avatar.jpg", + }, + }), + query, + cache + ); + + expect(data).toEqual({ + user: { + __typename: "User", + id: 1, + name: "Test User", + }, + profile: { + __typename: "UserProfile", + avatarUrl: "https://example.com/avatar.jpg", + }, + }); + }); + + test("strips named fragments inside inline fragments", () => { + const cache = new InMemoryCache({ + possibleTypes: { Industry: ["TechIndustry"], Profile: ["UserProfile"] }, + }); + const query = gql` + query { + user { + id + ... @defer { + name + ...UserFields + } + } + profile { + ... on UserProfile { + avatarUrl + ...UserProfileFields + } + industry { + ... on TechIndustry { + ...TechIndustryFields + } + } + } + } + + fragment UserFields on User { + age + } + + fragment UserProfileFields on UserProfile { + hometown + } + + fragment TechIndustryFields on TechIndustry { + favoriteLanguage + } + `; + + const data = maskOperation( + deepFreeze({ + user: { + __typename: "User", + id: 1, + name: "Test User", + age: 30, + }, + profile: { + __typename: "UserProfile", + avatarUrl: "https://example.com/avatar.jpg", + industry: { + __typename: "TechIndustry", + primaryLanguage: "TypeScript", + }, + }, + }), + query, + cache + ); + + expect(data).toEqual({ + user: { + __typename: "User", + id: 1, + name: "Test User", + }, + profile: { + __typename: "UserProfile", + avatarUrl: "https://example.com/avatar.jpg", + industry: { __typename: "TechIndustry" }, + }, + }); + }); + + test("handles objects with no matching inline fragment condition", () => { + const cache = new InMemoryCache({ + possibleTypes: { + Drink: ["HotChocolate", "Juice"], + }, + }); + + const query = gql` + query { + drinks { + id + ... on Juice { + fruitBase + } + } + } + `; + + const data = maskOperation( + deepFreeze({ + drinks: [ + { __typename: "HotChocolate", id: 1 }, + { __typename: "Juice", id: 2, fruitBase: "Strawberry" }, + ], + }), + query, + cache + ); + + expect(data).toEqual({ + drinks: [ + { __typename: "HotChocolate", id: 1 }, + { __typename: "Juice", id: 2, fruitBase: "Strawberry" }, + ], + }); + }); + + test("handles field aliases", () => { + const query = gql` + query { + user { + id + fullName: name + ... @defer { + userAddress: address + } + } + } + `; + + const data = maskOperation( + deepFreeze({ + user: { + __typename: "User", + id: 1, + fullName: "Test User", + userAddress: "1234 Main St", + }, + }), + query, + new InMemoryCache() + ); + + expect(data).toEqual({ + user: { + __typename: "User", + id: 1, + fullName: "Test User", + userAddress: "1234 Main St", + }, + }); + }); + + test("handles overlapping fields inside multiple inline fragments", () => { + const cache = new InMemoryCache({ + possibleTypes: { + Drink: [ + "Espresso", + "Latte", + "Cappuccino", + "Cortado", + "Juice", + "HotChocolate", + ], + Espresso: ["Latte", "Cappuccino", "Cortado"], + }, + }); + const query = gql` + query { + drinks { + id + ... @defer { + amount + } + ... on Espresso { + milkType + ... on Latte { + flavor { + name + ...FlavorFields + } + } + ... on Cappuccino { + roast + } + ... on Cortado { + ...CortadoFields + } + } + ... on Latte { + ... @defer { + shots + } + } + ... on Juice { + ...JuiceFields + } + ... on HotChocolate { + milkType + ...HotChocolateFields + } + } + } + + fragment JuiceFields on Juice { + fruitBase + } + + fragment HotChocolateFields on HotChocolate { + chocolateType + } + + fragment FlavorFields on Flavor { + sweetness + } + + fragment CortadoFields on Cortado { + temperature + } + `; + + const data = maskOperation( + deepFreeze({ + drinks: [ + { + __typename: "Latte", + id: 1, + amount: 12, + shots: 2, + milkType: "Cow", + flavor: { + __typename: "Flavor", + name: "Cookie Butter", + sweetness: "high", + }, + }, + { + __typename: "Cappuccino", + id: 2, + amount: 12, + milkType: "Cow", + roast: "medium", + }, + { + __typename: "Cortado", + id: 3, + amount: 12, + milkType: "Cow", + temperature: 150, + }, + { __typename: "Juice", id: 4, amount: 10, fruitBase: "Apple" }, + { + __typename: "HotChocolate", + id: 5, + amount: 8, + milkType: "Cow", + chocolateType: "dark", + }, + ], + }), + query, + cache + ); + + expect(data).toEqual({ + drinks: [ + { + __typename: "Latte", + id: 1, + amount: 12, + shots: 2, + milkType: "Cow", + flavor: { + __typename: "Flavor", + name: "Cookie Butter", + }, + }, + { + __typename: "Cappuccino", + id: 2, + amount: 12, + milkType: "Cow", + roast: "medium", + }, + { + __typename: "Cortado", + id: 3, + amount: 12, + milkType: "Cow", + }, + { __typename: "Juice", id: 4, amount: 10 }, + { + __typename: "HotChocolate", + id: 5, + amount: 8, + milkType: "Cow", + }, + ], + }); + }); + + test("does nothing if there are no fragments to mask", () => { + const query = gql` + query { + user { + id + name + } + } + `; + + const data = maskOperation( + deepFreeze({ user: { __typename: "User", id: 1, name: "Test User" } }), + query, + new InMemoryCache() + ); + + expect(data).toEqual({ + user: { __typename: "User", id: 1, name: "Test User" }, + }); + }); + + test("maintains referential equality on subtrees that did not change", () => { + const query = gql` + query { + user { + id + profile { + avatarUrl + } + ...UserFields + } + post { + id + title + } + authors { + id + name + } + industries { + ... on TechIndustry { + languageRequirements + } + ... on FinanceIndustry { + ...FinanceIndustryFields + } + ... on TradeIndustry { + id + yearsInBusiness + ...TradeIndustryFields + } + } + drink { + ... on SportsDrink { + saltContent + } + ... on Espresso { + __typename + } + } + } + + fragment UserFields on User { + name + } + + fragment FinanceIndustryFields on FinanceIndustry { + yearsInBusiness + } + + fragment TradeIndustryFields on TradeIndustry { + languageRequirements + } + `; + + const profile = { + __typename: "Profile", + avatarUrl: "https://example.com/avatar.jpg", + }; + const user = { __typename: "User", id: 1, name: "Test User", profile }; + const post = { __typename: "Post", id: 1, title: "Test Post" }; + const authors = [{ __typename: "Author", id: 1, name: "A Author" }]; + const industries = [ + { __typename: "TechIndustry", languageRequirements: ["TypeScript"] }, + { __typename: "FinanceIndustry", yearsInBusiness: 10 }, + { + __typename: "TradeIndustry", + id: 10, + yearsInBusiness: 15, + languageRequirements: ["English", "German"], + }, + ]; + const drink = { __typename: "Espresso" }; + const originalData = deepFreeze({ user, post, authors, industries, drink }); + + const data = maskOperation(originalData, query, new InMemoryCache()); + + expect(data).toEqual({ + user: { + __typename: "User", + id: 1, + profile: { + __typename: "Profile", + avatarUrl: "https://example.com/avatar.jpg", + }, + }, + post: { __typename: "Post", id: 1, title: "Test Post" }, + authors: [{ __typename: "Author", id: 1, name: "A Author" }], + industries: [ + { __typename: "TechIndustry", languageRequirements: ["TypeScript"] }, + { __typename: "FinanceIndustry" }, + { __typename: "TradeIndustry", id: 10, yearsInBusiness: 15 }, + ], + drink: { __typename: "Espresso" }, + }); + + expect(data).not.toBe(originalData); + expect(data.user).not.toBe(user); + expect(data.user.profile).toBe(profile); + expect(data.post).toBe(post); + expect(data.authors).toBe(authors); + expect(data.industries).not.toBe(industries); + expect(data.industries[0]).toBe(industries[0]); + expect(data.industries[1]).not.toBe(industries[1]); + expect(data.industries[2]).not.toBe(industries[2]); + expect(data.drink).toBe(drink); + }); + + test("maintains referential equality the entire result if there are no fragments", () => { + const query = gql` + query { + user { + id + name + } + } + `; + + const originalData = deepFreeze({ + user: { + __typename: "User", + id: 1, + name: "Test User", + }, + }); + + const data = maskOperation(originalData, query, new InMemoryCache()); + + expect(data).toBe(originalData); + }); + + test("does not mask named fragment fields and returns original object when using `@unmask` directive", () => { + const query = gql` + query UnmaskedQuery { + currentUser { + id + name + ...UserFields @unmask + } + } + + fragment UserFields on User { + age + } + `; + + const queryData = deepFreeze({ + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + age: 30, + }, + }); + + const data = maskOperation(queryData, query, new InMemoryCache()); + + expect(data).toBe(queryData); + }); + + test("maintains referential equality on subtrees that contain @unmask", () => { + const query = gql` + query { + user { + id + profile { + avatarUrl + } + ...UserFields @unmask + } + post { + id + title + } + authors { + id + name + } + industries { + ... on TechIndustry { + ...TechIndustryFields @unmask + } + ... on FinanceIndustry { + ...FinanceIndustryFields + } + ... on TradeIndustry { + id + yearsInBusiness + ...TradeIndustryFields @unmask + } + } + } + + fragment UserFields on User { + name + ...UserSubfields @unmask + } + + fragment UserSubfields on User { + age + } + + fragment FinanceIndustryFields on FinanceIndustry { + yearsInBusiness + } + + fragment TradeIndustryFields on TradeIndustry { + languageRequirements + } + + fragment TechIndustryFields on TechIndustry { + languageRequirements + ...TechIndustrySubFields + } + + fragment TechIndustrySubFields on TechIndustry { + focus + } + `; + + const profile = { + __typename: "Profile", + avatarUrl: "https://example.com/avatar.jpg", + }; + const user = { + __typename: "User", + id: 1, + name: "Test User", + profile, + age: 30, + }; + const post = { __typename: "Post", id: 1, title: "Test Post" }; + const authors = [{ __typename: "Author", id: 1, name: "A Author" }]; + const industries = [ + { + __typename: "TechIndustry", + languageRequirements: ["TypeScript"], + focus: "innovation", + }, + { __typename: "FinanceIndustry", yearsInBusiness: 10 }, + { + __typename: "TradeIndustry", + id: 10, + yearsInBusiness: 15, + languageRequirements: ["English", "German"], + }, + ]; + const originalData = deepFreeze({ user, post, authors, industries }); + + const data = maskOperation(originalData, query, new InMemoryCache()); + + expect(data).toEqual({ + user: { + __typename: "User", + name: "Test User", + id: 1, + profile: { + __typename: "Profile", + avatarUrl: "https://example.com/avatar.jpg", + }, + age: 30, + }, + post: { __typename: "Post", id: 1, title: "Test Post" }, + authors: [{ __typename: "Author", id: 1, name: "A Author" }], + industries: [ + { __typename: "TechIndustry", languageRequirements: ["TypeScript"] }, + { __typename: "FinanceIndustry" }, + { + __typename: "TradeIndustry", + id: 10, + yearsInBusiness: 15, + languageRequirements: ["English", "German"], + }, + ], + }); + + expect(data).not.toBe(originalData); + expect(data.user).toBe(user); + expect(data.user.profile).toBe(profile); + expect(data.post).toBe(post); + expect(data.authors).toBe(authors); + expect(data.industries).not.toBe(industries); + expect(data.industries[0]).not.toBe(industries[0]); + expect(data.industries[1]).not.toBe(industries[1]); + expect(data.industries[2]).toBe(industries[2]); + }); + + test("warns when accessing unmasked fields when using `@unmask` directive with mode 'migrate'", () => { + using _ = spyOnConsole("warn"); + const query = gql` + query UnmaskedQuery { + currentUser { + id + name + ...UserFields @unmask(mode: "migrate") + } + } + + fragment UserFields on User { + age + } + `; + + const anonymousQuery = gql` + query { + currentUser { + id + name + ...UserFields @unmask(mode: "migrate") + } + } + + fragment UserFields on User { + age + } + `; + + const currentUser = { + __typename: "User", + id: 1, + name: "Test User", + age: 30, + }; + + const cache = new InMemoryCache(); + + const data = maskOperation(deepFreeze({ currentUser }), query, cache); + + const dataFromAnonymous = maskOperation( + { currentUser }, + anonymousQuery, + cache + ); + + data.currentUser.age; + + expect(console.warn).toHaveBeenCalledTimes(1); + expect(console.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" + ); + + dataFromAnonymous.currentUser.age; + + expect(console.warn).toHaveBeenCalledTimes(2); + expect(console.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.", + "anonymous query", + "currentUser.age" + ); + + data.currentUser.age; + dataFromAnonymous.currentUser.age; + + // Ensure we only warn once for each masked field + expect(console.warn).toHaveBeenCalledTimes(2); + }); + + test("does not warn when accessing unmasked fields when using `@unmask` directive with mode 'migrate' in non-DEV mode", () => { + using _ = withProdMode(); + using __ = spyOnConsole("warn"); + + const query = gql` + query UnmaskedQuery { + currentUser { + id + name + ...UserFields @unmask(mode: "migrate") + } + } + + fragment UserFields on User { + age + } + `; + + const data = maskOperation( + deepFreeze({ + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + age: 30, + }, + }), + query, + new InMemoryCache() + ); + + const age = data.currentUser.age; + + expect(age).toBe(30); + expect(console.warn).not.toHaveBeenCalled(); + }); + + test("warns when accessing unmasked fields in arrays with mode: 'migrate'", () => { + using _ = spyOnConsole("warn"); + const query = gql` + query UnmaskedQuery { + users { + id + name + ...UserFields @unmask(mode: "migrate") + } + } + + fragment UserFields on User { + age + } + `; + + const data = maskOperation( + deepFreeze({ + users: [ + { __typename: "User", id: 1, name: "John Doe", age: 30 }, + { __typename: "User", id: 2, name: "Jane Doe", age: 30 }, + ], + }), + query, + new InMemoryCache() + ); + + data.users[0].age; + data.users[1].age; + + expect(console.warn).toHaveBeenCalledTimes(2); + expect(console.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'", + "users[0].age" + ); + + expect(console.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'", + "users[1].age" + ); + }); + + test("can mix and match masked vs unmasked fragment fields with proper warnings", () => { + using _ = spyOnConsole("warn"); + + const query = gql` + query UnmaskedQuery { + currentUser { + id + name + ...UserFields @unmask + } + } + + fragment UserFields on User { + age + profile { + email + ... @defer { + username + } + ...ProfileFields + } + skills { + name + ...SkillFields @unmask(mode: "migrate") + } + } + + fragment ProfileFields on Profile { + settings { + darkMode + } + } + + fragment SkillFields on Skill { + description + } + `; + + const data = maskOperation( + deepFreeze({ + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + age: 30, + profile: { + __typename: "Profile", + email: "testuser@example.com", + username: "testuser", + settings: { + __typename: "Settings", + darkMode: true, + }, + }, + skills: [ + { + __typename: "Skill", + name: "Skill 1", + description: "Skill 1 description", + }, + { + __typename: "Skill", + name: "Skill 2", + description: "Skill 2 description", + }, + ], + }, + }), + query, + new InMemoryCache() + ); + + expect(data).toEqual({ + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + age: 30, + profile: { + __typename: "Profile", + email: "testuser@example.com", + username: "testuser", + }, + skills: [ + { + __typename: "Skill", + name: "Skill 1", + description: "Skill 1 description", + }, + { + __typename: "Skill", + name: "Skill 2", + description: "Skill 2 description", + }, + ], + }, + }); + + expect(console.warn).toHaveBeenCalledTimes(2); + expect(console.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.skills[0].description" + ); + expect(console.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.skills[1].description" + ); + }); + + test("masks child fragments of @unmask(mode: 'migrate')", () => { + using _ = spyOnConsole("warn"); + + const query = gql` + query UnmaskedQuery { + currentUser { + id + name + ...UserFields @unmask(mode: "migrate") + } + } + + fragment UserFields on User { + age + ...UserSubfields + ...UserSubfields2 @unmask + } + + fragment UserSubfields on User { + username + } + + fragment UserSubfields2 on User { + email + } + `; + + const data = maskOperation( + deepFreeze({ + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + age: 30, + username: "testuser", + email: "test@example.com", + }, + }), + query, + new InMemoryCache() + ); + + expect(data).toEqual({ + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + age: 30, + email: "test@example.com", + }, + }); + + expect(console.warn).toHaveBeenCalledTimes(1); + expect(console.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" + ); + }); + + test("warns when accessing unmasked fields with complex selections with mode: 'migrate'", () => { + using _ = spyOnConsole("warn"); + const query = gql` + query UnmaskedQuery { + currentUser { + id + name + ...UserFields @unmask(mode: "migrate") + } + } + + fragment UserFields on User { + age + profile { + email + ... @defer { + username + } + ...ProfileFields @unmask(mode: "migrate") + } + skills { + name + ...SkillFields @unmask(mode: "migrate") + } + } + + fragment ProfileFields on Profile { + settings { + dark: darkMode + } + } + + fragment SkillFields on Skill { + description + } + `; + + const currentUser = { + __typename: "User", + id: 1, + name: "Test User", + age: 30, + profile: { + __typename: "Profile", + email: "testuser@example.com", + username: "testuser", + settings: { + __typename: "Settings", + dark: true, + }, + }, + skills: [ + { + __typename: "Skill", + name: "Skill 1", + description: "Skill 1 description", + }, + { + __typename: "Skill", + name: "Skill 2", + description: "Skill 2 description", + }, + ], + }; + + const data = maskOperation( + deepFreeze({ currentUser }), + query, + new InMemoryCache() + ); + + data.currentUser.age; + data.currentUser.profile.email; + data.currentUser.profile.username; + data.currentUser.profile.settings; + data.currentUser.profile.settings.dark; + data.currentUser.skills[0].description; + data.currentUser.skills[1].description; + + expect(console.warn).toHaveBeenCalledTimes(9); + expect(console.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" + ); + + expect(console.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.profile" + ); + + expect(console.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.profile.email" + ); + + expect(console.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.profile.username" + ); + + expect(console.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.profile.settings" + ); + + expect(console.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.profile.settings.dark" + ); + + expect(console.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.skills" + ); + + expect(console.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.skills[0].description" + ); + + expect(console.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.skills[1].description" + ); + }); + + test("does not warn when accessing fields shared between the query and fragment with mode: 'migrate'", () => { + using _ = spyOnConsole("warn"); + const query = gql` + query UnmaskedQuery { + currentUser { + id + name + age + ...UserFields @unmask(mode: "migrate") + email + } + } + + fragment UserFields on User { + age + email + } + `; + + const data = maskOperation( + deepFreeze({ + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + age: 30, + email: "testuser@example.com", + }, + }), + query, + new InMemoryCache() + ); + + data.currentUser.age; + data.currentUser.email; + + expect(console.warn).not.toHaveBeenCalled(); + }); + + test("does not warn accessing fields with `@unmask` without mode argument", () => { + using _ = spyOnConsole("warn"); + const query = gql` + query UnmaskedQuery { + currentUser { + id + name + ...UserFields @unmask + } + } + + fragment UserFields on User { + age + } + `; + + const data = maskOperation( + deepFreeze({ + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + age: 30, + }, + }), + query, + new InMemoryCache() + ); + + data.currentUser.age; + + expect(console.warn).not.toHaveBeenCalled(); + }); + + test("masks fragments in subscription documents", () => { + const subscription = gql` + subscription { + onUserUpdated { + id + ...UserFields + } + } + + fragment UserFields on User { + name + } + `; + + const data = maskOperation( + deepFreeze({ + onUserUpdated: { __typename: "User", id: 1, name: "Test User" }, + }), + subscription, + new InMemoryCache() + ); + + expect(data).toEqual({ onUserUpdated: { __typename: "User", id: 1 } }); + }); + + test("honors @unmask used in subscription documents", () => { + const subscription = gql` + subscription { + onUserUpdated { + id + ...UserFields @unmask + } + } + + fragment UserFields on User { + name + } + `; + + const subscriptionData = deepFreeze({ + onUserUpdated: { __typename: "User", id: 1, name: "Test User" }, + }); + + const data = maskOperation( + subscriptionData, + subscription, + new InMemoryCache() + ); + + expect(data).toBe(subscriptionData); + }); + + test("warns when accessing unmasked fields used in subscription documents with @unmask(mode: 'migrate')", () => { + using _ = spyOnConsole("warn"); + + const subscription = gql` + subscription UserUpdatedSubscription { + onUserUpdated { + id + ...UserFields @unmask(mode: "migrate") + } + } + + fragment UserFields on User { + name + } + `; + + const subscriptionData = deepFreeze({ + onUserUpdated: { __typename: "User", id: 1, name: "Test User" }, + }); + + const data = maskOperation( + subscriptionData, + subscription, + new InMemoryCache() + ); + + data.onUserUpdated.name; + + expect(console.warn).toHaveBeenCalledTimes(1); + expect(console.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.", + "subscription 'UserUpdatedSubscription'", + "onUserUpdated.name" + ); + }); + + test("masks fragments in mutation documents", () => { + const mutation = gql` + mutation { + updateUser { + id + ...UserFields + } + } + + fragment UserFields on User { + name + } + `; + + const data = maskOperation( + deepFreeze({ + updateUser: { __typename: "User", id: 1, name: "Test User" }, + }), + mutation, + new InMemoryCache() + ); + + expect(data).toEqual({ updateUser: { __typename: "User", id: 1 } }); + }); + + test("honors @unmask used in mutation documents", () => { + const mutation = gql` + mutation { + updateUser { + id + ...UserFields @unmask + } + } + + fragment UserFields on User { + name + } + `; + + const mutationData = deepFreeze({ + updateUser: { __typename: "User", id: 1, name: "Test User" }, + }); + + const data = maskOperation(mutationData, mutation, new InMemoryCache()); + + expect(data).toBe(mutationData); + }); + + test("warns when accessing unmasked fields used in mutation documents with @unmask(mode: 'migrate')", () => { + using _ = spyOnConsole("warn"); + + const mutation = gql` + mutation UpdateUserMutation { + updateUser { + id + ...UserFields @unmask(mode: "migrate") + } + } + + fragment UserFields on User { + name + } + `; + + const mutationData = deepFreeze({ + updateUser: { __typename: "User", id: 1, name: "Test User" }, + }); + + const data = maskOperation(mutationData, mutation, new InMemoryCache()); + + data.updateUser.name; + + expect(console.warn).toHaveBeenCalledTimes(1); + expect(console.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.", + "mutation 'UpdateUserMutation'", + "updateUser.name" + ); + }); +}); + +describe("maskFragment", () => { + test("returns null when data is null", () => { + const fragment = gql` + fragment Foo on Query { + foo + ...QueryFields + } + + fragment QueryFields on Query { + bar + } + `; + + const data = maskFragment(null, fragment, new InMemoryCache(), "Foo"); + + expect(data).toBe(null); + }); + + test("returns undefined when data is undefined", () => { + const fragment = gql` + fragment Foo on Query { + foo + ...QueryFields + } + + fragment QueryFields on Query { + bar + } + `; + + const data = maskFragment(undefined, fragment, new InMemoryCache(), "Foo"); + + expect(data).toBe(undefined); + }); + test("masks named fragments in fragment documents", () => { + const fragment = gql` + fragment UserFields on User { + id + ...UserProfile + } + + fragment UserProfile on User { + age + } + `; + + const data = maskFragment( + deepFreeze({ __typename: "User", id: 1, age: 30 }), + fragment, + new InMemoryCache(), + "UserFields" + ); + + expect(data).toEqual({ __typename: "User", id: 1 }); + }); + + test("masks named fragments in nested fragment objects", () => { + const fragment = gql` + fragment UserFields on User { + id + profile { + ...UserProfile + } + } + + fragment UserProfile on User { + age + } + `; + + const data = maskFragment( + deepFreeze({ + __typename: "User", + id: 1, + profile: { __typename: "Profile", age: 30 }, + }), + fragment, + new InMemoryCache(), + "UserFields" + ); + + expect(data).toEqual({ + __typename: "User", + id: 1, + profile: { __typename: "Profile" }, + }); + }); + + test("handles nulls in child selection sets", () => { + const fragment = gql` + fragment UserFields on User { + profile { + id + } + ...ProfileFields + } + fragment ProfileFields on User { + profile { + id + fullName + } + } + `; + + const data = maskFragment( + deepFreeze({ __typename: "User", profile: null }), + fragment, + new InMemoryCache(), + "UserFields" + ); + + expect(data).toEqual({ __typename: "User", profile: null }); + }); + + test("handles nulls in arrays", () => { + const fragment = gql` + fragment UserFields on Query { + users { + profile { + id + } + ...ProfileFields + } + } + fragment ProfileFields on User { + profile { + id + fullName + } + } + `; + + const data = maskFragment( + deepFreeze({ + users: [ + null, + { __typename: "User", profile: null }, + { + __typename: "User", + profile: { __typename: "Profile", id: "1", fullName: "Test User" }, + }, + ], + }), + fragment, + new InMemoryCache(), + "UserFields" + ); + + expect(data).toEqual({ + users: [ + null, + { __typename: "User", profile: null }, + { __typename: "User", profile: { __typename: "Profile", id: "1" } }, + ], + }); + }); + + test("deep freezes the masked result if the original data is frozen", () => { + const fragment = gql` + fragment UserFields on User { + id + profile { + ...UserProfile + } + } + + fragment UserProfile on User { + age + } + `; + + const frozenData = maskFragment( + deepFreeze({ + __typename: "User", + id: 1, + profile: { __typename: "Profile", age: 30 }, + }), + fragment, + new InMemoryCache(), + "UserFields" + ); + + const nonFrozenData = maskFragment( + { + __typename: "User", + id: 1, + profile: { __typename: "Profile", age: 30 }, + }, + fragment, + new InMemoryCache(), + "UserFields" + ); + + expect(Object.isFrozen(frozenData)).toBe(true); + expect(Object.isFrozen(nonFrozenData)).toBe(false); + }); + + test("does not mask inline fragment in fragment documents", () => { + const fragment = gql` + fragment UserFields on User { + id + ... @defer { + age + } + } + `; + + const data = maskFragment( + deepFreeze({ __typename: "User", id: 1, age: 30 }), + fragment, + new InMemoryCache(), + "UserFields" + ); + + expect(data).toEqual({ __typename: "User", id: 1, age: 30 }); + }); + + test("throws when document contains more than 1 fragment without a fragmentName", () => { + const fragment = gql` + fragment UserFields on User { + id + ...UserProfile + } + + fragment UserProfile on User { + age + } + `; + + expect(() => + maskFragment( + deepFreeze({ __typename: "User", id: 1, age: 30 }), + fragment, + new InMemoryCache() + ) + ).toThrow( + new InvariantError( + "Found 2 fragments. `fragmentName` must be provided when there is not exactly 1 fragment." + ) + ); + }); + + test("throws when fragment cannot be found within document", () => { + const fragment = gql` + fragment UserFields on User { + id + ...UserProfile + } + + fragment UserProfile on User { + age + } + `; + + expect(() => + maskFragment( + deepFreeze({ __typename: "User", id: 1, age: 30 }), + fragment, + new InMemoryCache(), + "ProfileFields" + ) + ).toThrow( + new InvariantError('Could not find fragment with name "ProfileFields".') + ); + }); + + test("maintains referential equality on fragment subtrees that did not change", () => { + const fragment = gql` + fragment UserFields on User { + id + profile { + ...ProfileFields + } + post { + id + title + } + industries { + ... on TechIndustry { + languageRequirements + } + ... on FinanceIndustry { + ...FinanceIndustryFields + } + ... on TradeIndustry { + id + yearsInBusiness + ...TradeIndustryFields + } + } + drinks { + ... on SportsDrink { + ...SportsDrinkFields + } + ... on Espresso { + __typename + } + } + } + + fragment ProfileFields on Profile { + age + } + + fragment FinanceIndustryFields on FinanceIndustry { + yearsInBusiness + } + + fragment TradeIndustryFields on TradeIndustry { + languageRequirements + } + + fragment SportsDrinkFields on SportsDrink { + saltContent + } + `; + + const profile = { + __typename: "Profile", + age: 30, + }; + const post = { __typename: "Post", id: 1, title: "Test Post" }; + const industries = [ + { __typename: "TechIndustry", languageRequirements: ["TypeScript"] }, + { __typename: "FinanceIndustry", yearsInBusiness: 10 }, + { + __typename: "TradeIndustry", + id: 10, + yearsInBusiness: 15, + languageRequirements: ["English", "German"], + }, + ]; + const drinks = [ + { __typename: "Espresso" }, + { __typename: "SportsDrink", saltContent: "1000mg" }, + ]; + const user = deepFreeze({ + __typename: "User", + id: 1, + profile, + post, + industries, + drinks, + }); + + const data = maskFragment( + user, + fragment, + new InMemoryCache(), + "UserFields" + ); + + expect(data).toEqual({ + __typename: "User", + id: 1, + profile: { __typename: "Profile" }, + post: { __typename: "Post", id: 1, title: "Test Post" }, + industries: [ + { __typename: "TechIndustry", languageRequirements: ["TypeScript"] }, + { __typename: "FinanceIndustry" }, + { __typename: "TradeIndustry", id: 10, yearsInBusiness: 15 }, + ], + drinks: [{ __typename: "Espresso" }, { __typename: "SportsDrink" }], + }); + + expect(data).not.toBe(user); + expect(data.profile).not.toBe(profile); + expect(data.post).toBe(post); + expect(data.industries).not.toBe(industries); + expect(data.industries[0]).toBe(industries[0]); + expect(data.industries[1]).not.toBe(industries[1]); + expect(data.industries[2]).not.toBe(industries[2]); + expect(data.drinks).not.toBe(drinks); + expect(data.drinks[0]).toBe(drinks[0]); + expect(data.drinks[1]).not.toBe(drinks[1]); + }); + + test("maintains referential equality on fragment when no data is masked", () => { + const fragment = gql` + fragment UserFields on User { + id + age + } + `; + + const user = { __typename: "User", id: 1, age: 30 }; + + const data = maskFragment(deepFreeze(user), fragment, new InMemoryCache()); + + expect(data).toBe(user); + }); + + test("does not mask named fragments and returns original object when using `@unmask` directive", () => { + const fragment = gql` + fragment UnmaskedFragment on User { + id + name + ...UserFields @unmask + } + + fragment UserFields on User { + age + } + `; + + const fragmentData = deepFreeze({ + __typename: "User", + id: 1, + name: "Test User", + age: 30, + }); + + const data = maskFragment( + fragmentData, + fragment, + new InMemoryCache(), + "UnmaskedFragment" + ); + + expect(data).toBe(fragmentData); + }); + + test("warns when accessing unmasked fields when using `@unmask` directive with mode 'migrate'", () => { + using _ = spyOnConsole("warn"); + const query = gql` + fragment UnmaskedFragment on User { + id + name + ...UserFields @unmask(mode: "migrate") + } + + fragment UserFields on User { + age + } + `; + + const data = maskFragment( + deepFreeze({ + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + age: 30, + }, + }), + query, + new InMemoryCache(), + "UnmaskedFragment" + ); + + data.age; + + expect(console.warn).toHaveBeenCalledTimes(1); + expect(console.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.", + "fragment 'UnmaskedFragment'", + "age" + ); + + data.age; + + // Ensure we only warn once for each masked field + expect(console.warn).toHaveBeenCalledTimes(1); + }); + + test("maintains referential equality on `@unmask` fragment subtrees", () => { + const fragment = gql` + fragment UserFields on User { + id + profile { + ...ProfileFields @unmask + } + post { + id + title + } + industries { + ... on TechIndustry { + languageRequirements + } + ... on FinanceIndustry { + ...FinanceIndustryFields + } + ... on TradeIndustry { + id + yearsInBusiness + ...TradeIndustryFields + } + } + drinks { + ... on SportsDrink { + ...SportsDrinkFields @unmask + } + ... on Espresso { + __typename + } + } + } + + fragment ProfileFields on Profile { + age + ...ProfileSubfields @unmask + } + + fragment ProfileSubfields on Profile { + name + } + + fragment FinanceIndustryFields on FinanceIndustry { + yearsInBusiness + } + + fragment TradeIndustryFields on TradeIndustry { + languageRequirements + } + + fragment SportsDrinkFields on SportsDrink { + saltContent + } + `; + + const profile = { + __typename: "Profile", + age: 30, + name: "Test User", + }; + const post = { __typename: "Post", id: 1, title: "Test Post" }; + const industries = [ + { __typename: "TechIndustry", languageRequirements: ["TypeScript"] }, + { __typename: "FinanceIndustry", yearsInBusiness: 10 }, + { + __typename: "TradeIndustry", + id: 10, + yearsInBusiness: 15, + languageRequirements: ["English", "German"], + }, + ]; + const drinks = [ + { __typename: "Espresso" }, + { __typename: "SportsDrink", saltContent: "1000mg" }, + ]; + const user = deepFreeze({ + __typename: "User", + id: 1, + profile, + post, + industries, + drinks, + }); + + const data = maskFragment( + user, + fragment, + new InMemoryCache(), + "UserFields" + ); + + expect(data).toEqual({ + __typename: "User", + id: 1, + profile: { __typename: "Profile", age: 30, name: "Test User" }, + post: { __typename: "Post", id: 1, title: "Test Post" }, + industries: [ + { __typename: "TechIndustry", languageRequirements: ["TypeScript"] }, + { __typename: "FinanceIndustry" }, + { __typename: "TradeIndustry", id: 10, yearsInBusiness: 15 }, + ], + drinks: [ + { __typename: "Espresso" }, + { __typename: "SportsDrink", saltContent: "1000mg" }, + ], + }); + + expect(data).not.toBe(user); + expect(data.profile).toBe(profile); + expect(data.post).toBe(post); + expect(data.industries).not.toBe(industries); + expect(data.industries[0]).toBe(industries[0]); + expect(data.industries[1]).not.toBe(industries[1]); + expect(data.industries[2]).not.toBe(industries[2]); + expect(data.drinks).toBe(drinks); + expect(data.drinks[0]).toBe(drinks[0]); + expect(data.drinks[1]).toBe(drinks[1]); + }); + + test("masks child fragments of @unmask(mode: 'migrate')", () => { + using _ = spyOnConsole("warn"); + + const fragment = gql` + fragment UnmaskedUser on User { + id + name + ...UserFields @unmask(mode: "migrate") + } + + fragment UserFields on User { + age + ...UserSubfields + ...UserSubfields2 @unmask + } + + fragment UserSubfields on User { + username + } + + fragment UserSubfields2 on User { + email + } + `; + + const data = maskFragment( + deepFreeze({ + __typename: "User", + id: 1, + name: "Test User", + age: 30, + username: "testuser", + email: "test@example.com", + }), + fragment, + new InMemoryCache(), + "UnmaskedUser" + ); + + expect(data).toEqual({ + __typename: "User", + id: 1, + name: "Test User", + age: 30, + email: "test@example.com", + }); + + expect(console.warn).toHaveBeenCalledTimes(1); + expect(console.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.", + "fragment 'UnmaskedUser'", + "age" + ); + }); +}); diff --git a/src/core/masking.ts b/src/core/masking.ts new file mode 100644 index 00000000000..8e136eacbc2 --- /dev/null +++ b/src/core/masking.ts @@ -0,0 +1,407 @@ +import { Kind } from "graphql"; +import type { FragmentDefinitionNode, SelectionSetNode } from "graphql"; +import { + createFragmentMap, + resultKeyNameFromField, + getFragmentDefinitions, + getFragmentMaskMode, + getOperationDefinition, + maybeDeepFreeze, +} from "../utilities/index.js"; +import type { FragmentMap } from "../utilities/index.js"; +import type { ApolloCache, DocumentNode, TypedDocumentNode } from "./index.js"; +import { invariant } from "../utilities/globals/index.js"; + +interface MaskingContext { + operationType: "query" | "mutation" | "subscription" | "fragment"; + operationName: string | undefined; + fragmentMap: FragmentMap; + cache: ApolloCache; + disableWarnings?: boolean; +} + +export function maskOperation( + data: TData, + document: DocumentNode | TypedDocumentNode, + cache: ApolloCache +): TData { + if (!cache.fragmentMatches) { + if (__DEV__) { + warnOnImproperCacheImplementation(); + } + + return data; + } + + const definition = getOperationDefinition(document); + + invariant( + definition, + "Expected a parsed GraphQL document with a query, mutation, or subscription." + ); + + if (data == null) { + // Maintain the original `null` or `undefined` value + return data; + } + + const context: MaskingContext = { + operationType: definition.operation, + operationName: definition.name?.value, + fragmentMap: createFragmentMap(getFragmentDefinitions(document)), + cache, + }; + + const [masked, changed] = maskSelectionSet( + data, + definition.selectionSet, + context + ); + + if (Object.isFrozen(data)) { + context.disableWarnings = true; + maybeDeepFreeze(masked); + context.disableWarnings = false; + } + + return changed ? masked : data; +} + +export function maskFragment( + data: TData, + document: TypedDocumentNode | DocumentNode, + cache: ApolloCache, + fragmentName?: string +): TData { + if (!cache.fragmentMatches) { + if (__DEV__) { + warnOnImproperCacheImplementation(); + } + + return data; + } + + const fragments = document.definitions.filter( + (node): node is FragmentDefinitionNode => + node.kind === Kind.FRAGMENT_DEFINITION + ); + + if (typeof fragmentName === "undefined") { + invariant( + fragments.length === 1, + `Found %s fragments. \`fragmentName\` must be provided when there is not exactly 1 fragment.`, + fragments.length + ); + fragmentName = fragments[0].name.value; + } + + const fragment = fragments.find( + (fragment) => fragment.name.value === fragmentName + ); + + invariant( + !!fragment, + `Could not find fragment with name "%s".`, + fragmentName + ); + + if (data == null) { + // Maintain the original `null` or `undefined` value + return data; + } + + const context: MaskingContext = { + operationType: "fragment", + operationName: fragment.name.value, + fragmentMap: createFragmentMap(getFragmentDefinitions(document)), + cache, + }; + + const [masked, changed] = maskSelectionSet( + data, + fragment.selectionSet, + context + ); + + if (Object.isFrozen(data)) { + context.disableWarnings = true; + maybeDeepFreeze(masked); + context.disableWarnings = false; + } + + return changed ? masked : data; +} + +function maskSelectionSet( + data: any, + selectionSet: SelectionSetNode, + context: MaskingContext, + path?: string | undefined +): [data: any, changed: boolean] { + if (Array.isArray(data)) { + let changed = false; + + const masked = data.map((item, index) => { + if (item === null) { + return null; + } + + const [masked, itemChanged] = maskSelectionSet( + item, + selectionSet, + context, + __DEV__ ? `${path || ""}[${index}]` : void 0 + ); + changed ||= itemChanged; + + return itemChanged ? masked : item; + }); + + return [changed ? masked : data, changed]; + } + + const result = selectionSet.selections.reduce<[any, boolean]>( + ([memo, changed], selection) => { + switch (selection.kind) { + case Kind.FIELD: { + const keyName = resultKeyNameFromField(selection); + const childSelectionSet = selection.selectionSet; + + memo[keyName] = data[keyName]; + + if (childSelectionSet && data[keyName] !== null) { + const [masked, childChanged] = maskSelectionSet( + data[keyName], + childSelectionSet, + context, + __DEV__ ? `${path || ""}.${keyName}` : void 0 + ); + + if ( + childChanged || + // This check prevents cases where masked fields may accidentally be + // returned as part of this object when the fragment also selects + // additional fields from the same child selection. + Object.keys(masked).length !== Object.keys(data[keyName]).length + ) { + memo[keyName] = masked; + changed = true; + } + } + + return [memo, changed]; + } + case Kind.INLINE_FRAGMENT: { + if ( + selection.typeCondition && + !context.cache.fragmentMatches!(selection, data.__typename) + ) { + return [memo, changed]; + } + + const [fragmentData, childChanged] = maskSelectionSet( + data, + selection.selectionSet, + context, + path + ); + + return [ + { + ...memo, + ...fragmentData, + }, + changed || childChanged, + ]; + } + case Kind.FRAGMENT_SPREAD: { + const fragmentName = selection.name.value; + let fragment: FragmentDefinitionNode | null = + context.fragmentMap[fragmentName] || + (context.fragmentMap[fragmentName] = + context.cache.lookupFragment(fragmentName)!); + invariant( + fragment, + "Could not find fragment with name '%s'.", + fragmentName + ); + + const mode = getFragmentMaskMode(selection); + + if (mode === "mask") { + return [memo, true]; + } + + if (__DEV__) { + if (mode === "migrate") { + return [ + addFieldAccessorWarnings( + memo, + data, + fragment.selectionSet, + path || "", + context + ), + true, + ]; + } + } + + const [fragmentData, changed] = maskSelectionSet( + data, + fragment.selectionSet, + context, + path + ); + + return [{ ...memo, ...fragmentData }, changed]; + } + } + }, + [Object.create(null), false] + ); + + if ("__typename" in data && !("__typename" in result[0])) { + result[0].__typename = data.__typename; + } + + return result; +} + +function addFieldAccessorWarnings( + memo: Record, + data: Record, + selectionSetNode: SelectionSetNode, + path: string, + context: MaskingContext +) { + if (Array.isArray(data)) { + return data.map((item, index): unknown => { + return addFieldAccessorWarnings( + memo[index] || Object.create(null), + item, + selectionSetNode, + `${path}[${index}]`, + context + ); + }); + } + + return selectionSetNode.selections.reduce((memo, selection) => { + switch (selection.kind) { + case Kind.FIELD: { + const keyName = resultKeyNameFromField(selection); + const childSelectionSet = selection.selectionSet; + + if (keyName in memo) { + return memo; + } + + let value = data[keyName]; + + if (childSelectionSet) { + value = addFieldAccessorWarnings( + memo[keyName] || Object.create(null), + data[keyName] as Record, + childSelectionSet, + `${path}.${keyName}`, + context + ); + } + + if (__DEV__) { + addAccessorWarning(memo, value, keyName, path, context); + } + + if (!__DEV__) { + memo[keyName] = data[keyName]; + } + + return memo; + } + case Kind.INLINE_FRAGMENT: { + return addFieldAccessorWarnings( + memo, + data, + selection.selectionSet, + path, + context + ); + } + case Kind.FRAGMENT_SPREAD: { + const fragment = context.fragmentMap[selection.name.value]; + const mode = getFragmentMaskMode(selection); + + if (mode === "mask") { + return memo; + } + + if (mode === "unmask") { + const [fragmentData] = maskSelectionSet( + data, + fragment.selectionSet, + context, + path + ); + + return Object.assign(memo, fragmentData); + } + + return addFieldAccessorWarnings( + memo, + data, + fragment.selectionSet, + path, + context + ); + } + } + }, memo); +} + +function addAccessorWarning( + data: Record, + value: any, + fieldName: string, + path: string, + context: MaskingContext +) { + let getValue = () => { + if (context.disableWarnings) { + return value; + } + + invariant.warn( + "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.", + context.operationName ? + `${context.operationType} '${context.operationName}'` + : `anonymous ${context.operationType}`, + `${path}.${fieldName}`.replace(/^\./, "") + ); + + getValue = () => value; + + return value; + }; + + Object.defineProperty(data, fieldName, { + get() { + return getValue(); + }, + set(value) { + getValue = () => value; + }, + enumerable: true, + configurable: true, + }); +} + +let issuedWarning = false; +function warnOnImproperCacheImplementation() { + if (!issuedWarning) { + issuedWarning = true; + invariant.warn( + "The configured cache does not support data masking which effectively disables it. Please use a cache that supports data masking or disable data masking to silence this warning." + ); + } +} diff --git a/src/core/types.ts b/src/core/types.ts index fefe245b04c..434b9d1a098 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -10,6 +10,7 @@ import type { ObservableQuery } from "./ObservableQuery.js"; import type { QueryOptions } from "./watchQueryOptions.js"; import type { Cache } from "../cache/index.js"; import type { IsStrictlyAny } from "../utilities/index.js"; +import type { Unmasked } from "../masking/index.js"; export type { TypedDocumentNode } from "@graphql-typed-document-node/core"; @@ -164,7 +165,7 @@ export interface ApolloQueryResult { export type MutationQueryReducer = ( previousResult: Record, options: { - mutationResult: FetchResult; + mutationResult: FetchResult>; queryName: string | undefined; queryVariables: Record; } @@ -192,7 +193,7 @@ export type MutationUpdaterFunction< TCache extends ApolloCache, > = ( cache: TCache, - result: Omit, "context">, + result: Omit>, "context">, options: { context?: TContext; variables?: TVariables; diff --git a/src/core/watchQueryOptions.ts b/src/core/watchQueryOptions.ts index 0627f04ebc5..1528b1d0330 100644 --- a/src/core/watchQueryOptions.ts +++ b/src/core/watchQueryOptions.ts @@ -13,6 +13,8 @@ import type { import type { ApolloCache } from "../cache/index.js"; import type { ObservableQuery } from "./ObservableQuery.js"; import type { IgnoreModifier } from "../cache/core/types/common.js"; +import type { Unmasked } from "../masking/index.js"; +import type { NoInfer } from "../utilities/index.js"; /** * fetchPolicy determines where the client may return a result from. The options are: @@ -167,12 +169,12 @@ export type UpdateQueryFn< TSubscriptionVariables = OperationVariables, TSubscriptionData = TData, > = ( - previousQueryResult: TData, + previousQueryResult: Unmasked, options: { - subscriptionData: { data: TSubscriptionData }; + subscriptionData: { data: Unmasked }; variables?: TSubscriptionVariables; } -) => TData; +) => Unmasked; export type SubscribeToMoreOptions< TData = any, @@ -219,18 +221,18 @@ export interface MutationBaseOptions< > { /** {@inheritDoc @apollo/client!MutationOptionsDocumentation#optimisticResponse:member} */ optimisticResponse?: - | TData + | Unmasked> | (( vars: TVariables, { IGNORE }: { IGNORE: IgnoreModifier } - ) => TData | IgnoreModifier); + ) => Unmasked> | IgnoreModifier); /** {@inheritDoc @apollo/client!MutationOptionsDocumentation#updateQueries:member} */ updateQueries?: MutationQueryReducersMap; /** {@inheritDoc @apollo/client!MutationOptionsDocumentation#refetchQueries:member} */ refetchQueries?: - | ((result: FetchResult) => InternalRefetchQueriesInclude) + | ((result: FetchResult>) => InternalRefetchQueriesInclude) | InternalRefetchQueriesInclude; /** {@inheritDoc @apollo/client!MutationOptionsDocumentation#awaitRefetchQueries:member} */ diff --git a/src/masking/__tests__/types.test.ts b/src/masking/__tests__/types.test.ts new file mode 100644 index 00000000000..7eb07b96794 --- /dev/null +++ b/src/masking/__tests__/types.test.ts @@ -0,0 +1,133 @@ +import { expectTypeOf } from "expect-type"; +import type { Unmasked } from "../index"; +import { DeepPartial } from "../../utilities"; + +describe.skip("Unmasked", () => { + test("unmasks deeply nested fragments", () => { + type UserFieldsFragment = { + __typename: "User"; + id: number; + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" } & { + " $fragmentRefs"?: { + NameFieldsFragment: NameFieldsFragment; + JobFieldsFragment: JobFieldsFragment; + }; + }; + + type NameFieldsFragment = { + __typename: "User"; + firstName: string; + lastName: string; + } & { " $fragmentName"?: "NameFieldsFragment" }; + + type JobFieldsFragment = { + __typename: "User"; + job: string; + } & { " $fragmentName"?: "JobFieldsFragment" } & { + " $fragmentRefs"?: { CareerFieldsFragment: CareerFieldsFragment }; + }; + + type CareerFieldsFragment = { + __typename: "User"; + position: string; + } & { " $fragmentName"?: "CareerFieldsFragment" }; + + expectTypeOf>().toEqualTypeOf<{ + __typename: "User"; + id: number; + age: number; + firstName: string; + lastName: string; + job: string; + position: string; + }>(); + }); + + test("unmasks deeply nested fragments", () => { + type UserFieldsFragment = { + __typename: "User"; + id: number; + age: number; + jobs: Array< + { + __typename: "Job"; + id: string; + title: string; + } & { " $fragmentRefs"?: { JobFieldsFragment: JobFieldsFragment } } + >; + } & { " $fragmentName"?: "UserFieldsFragment" } & { + " $fragmentRefs"?: { + NameFieldsFragment: NameFieldsFragment; + }; + }; + + type NameFieldsFragment = { + __typename: "User"; + firstName: string; + lastName: string; + } & { " $fragmentName"?: "NameFieldsFragment" }; + + type JobFieldsFragment = { + __typename: "Job"; + job: string; + } & { " $fragmentName"?: "JobFieldsFragment" } & { + " $fragmentRefs"?: { CareerFieldsFragment: CareerFieldsFragment }; + }; + + type CareerFieldsFragment = { + __typename: "Job"; + position: string; + } & { " $fragmentName"?: "CareerFieldsFragment" }; + + expectTypeOf>().toEqualTypeOf<{ + __typename: "User"; + id: number; + age: number; + firstName: string; + lastName: string; + jobs: Array<{ + __typename: "Job"; + id: string; + title: string; + job: string; + position: string; + }>; + }>(); + }); + + test("unmasks DeepPartial types", () => { + type UserFieldsFragment = { + __typename: "User"; + id: number; + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" } & { + " $fragmentRefs"?: { + NameFieldsFragment: NameFieldsFragment; + }; + }; + + type NameFieldsFragment = { + __typename: "User"; + firstName: string; + lastName: string; + } & { " $fragmentName"?: "NameFieldsFragment" }; + + expectTypeOf>>().toEqualTypeOf<{ + __typename?: "User"; + id?: number; + age?: number; + firstName?: string; + lastName?: string; + }>(); + }); + + test("handles odd types", () => { + expectTypeOf>().toEqualTypeOf<{}>(); + expectTypeOf>>().toEqualTypeOf< + Record + >(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + }); +}); diff --git a/src/masking/index.ts b/src/masking/index.ts new file mode 100644 index 00000000000..1e9b4aad3d8 --- /dev/null +++ b/src/masking/index.ts @@ -0,0 +1,7 @@ +export type { + FragmentType, + Masked, + MaskedDocumentNode, + MaybeMasked, + Unmasked, +} from "./types.js"; diff --git a/src/masking/internal/types.ts b/src/masking/internal/types.ts new file mode 100644 index 00000000000..b92d1b03cca --- /dev/null +++ b/src/masking/internal/types.ts @@ -0,0 +1,30 @@ +import type { Prettify, UnionToIntersection } from "../../utilities/index.js"; + +export type UnwrapFragmentRefs = + // Leave TData alone if it is Record and not a specific shape + string extends keyof NonNullable ? TData + : " $fragmentRefs" extends keyof NonNullable ? + TData extends { " $fragmentRefs"?: infer FragmentRefs extends object } ? + Prettify< + { + [K in keyof TData as K extends " $fragmentRefs" ? never + : K]: UnwrapFragmentRefs; + } & CombineFragmentRefs + > + : never + : TData extends object ? { [K in keyof TData]: UnwrapFragmentRefs } + : TData; + +type CombineFragmentRefs> = + UnionToIntersection< + { + [K in keyof FragmentRefs]-?: UnwrapFragmentRefs< + RemoveFragmentName + >; + }[keyof FragmentRefs] + >; + +export type RemoveMaskedMarker = Omit; +// force distrubution when T is a union with | undefined +export type RemoveFragmentName = + T extends any ? Omit : T; diff --git a/src/masking/types.ts b/src/masking/types.ts new file mode 100644 index 00000000000..ae2216a5c96 --- /dev/null +++ b/src/masking/types.ts @@ -0,0 +1,50 @@ +import type { TypedDocumentNode } from "@graphql-typed-document-node/core"; +import type { + RemoveFragmentName, + RemoveMaskedMarker, + UnwrapFragmentRefs, +} from "./internal/types.ts"; +import type { Prettify } from "../utilities/index.js"; + +export interface DataMasking {} + +/** + * Marks a type as masked. This is used by MaybeMasked when determining + * whether to use the masked or unmasked type. + */ +export type Masked = TData & { + __masked?: true; +}; + +/** + * Marks a type as masked. This is a shortcut for + * TypedDocumentNode, TVariables> + */ +export type MaskedDocumentNode< + TData = { [key: string]: any }, + TVariables = { [key: string]: any }, +> = TypedDocumentNode, TVariables>; + +export type FragmentType = + [TData] extends [{ " $fragmentName"?: infer TKey }] ? + TKey extends string ? + { " $fragmentRefs"?: { [key in TKey]: TData } } + : never + : never; + +/** + * Returns TData as either masked or unmasked depending on whether masking is + * enabled. + */ +export type MaybeMasked = + TData extends { __masked?: true } ? Prettify> + : DataMasking extends { enabled: true } ? TData + : Unmasked; + +/** + * Unmasks a type to provide its full result. + */ +export type Unmasked = + TData extends object ? + UnwrapFragmentRefs>> + : TData; diff --git a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx index ac0ef98b87a..ad3e26d7b96 100644 --- a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx +++ b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx @@ -60,6 +60,12 @@ import { useTrackRenders, } from "../../../testing/internal"; import { SubscribeToMoreFunction } from "../useSuspenseQuery"; +import { + MaskedVariablesCaseData, + setupMaskedVariablesCase, + UnmaskedVariablesCaseData, +} from "../../../testing/internal/scenarios"; +import { Masked, MaskedDocumentNode } from "../../../masking"; afterEach(() => { jest.useRealTimers(); @@ -4108,202 +4114,1097 @@ it.each([ } ); -describe("refetch", () => { - it("re-suspends when calling `refetch`", async () => { - const { query, mocks: defaultMocks } = setupVariablesCase(); - const user = userEvent.setup(); - const Profiler = createDefaultProfiler(); - const { SuspenseFallback, ReadQueryHook } = - createDefaultTrackedComponents(Profiler); +it("masks queries when dataMasking is `true`", async () => { + type UserFieldsFragment = { + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" }; - const mocks: MockedResponse[] = [ - ...defaultMocks, - { - request: { query, variables: { id: "1" } }, - result: { - data: { - character: { - __typename: "Character", - id: "1", - name: "Spider-Man (refetched)", - }, + interface Query { + currentUser: { + __typename: "User"; + id: number; + name: string; + } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; + } + + const query: MaskedDocumentNode = gql` + query MaskedQuery { + 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, }, }, - delay: 10, }, - ]; + }, + ]; - function App() { - useTrackRenders(); - const [queryRef, { refetch }] = useBackgroundQuery(query, { - variables: { id: "1" }, - }); + const client = new ApolloClient({ + dataMasking: true, + cache: new InMemoryCache(), + link: new MockLink(mocks), + }); - return ( - <> - - }> - - - - ); - } + const Profiler = createDefaultProfiler>(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - renderWithMocks(, { mocks, wrapper: Profiler }); + function App() { + useTrackRenders(); + const [queryRef] = useBackgroundQuery(query); - { - const { renderedComponents } = await Profiler.takeRender(); + return ( + }> + + + ); + } - expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); - } + renderWithClient(, { client, wrapper: Profiler }); - { - const { snapshot } = await Profiler.takeRender(); + // loading + await Profiler.takeRender(); - expect(snapshot.result).toEqual({ - data: { - character: { - __typename: "Character", - id: "1", - name: "Spider-Man", - }, + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { + currentUser: { + __typename: "User", + id: 1, + name: "Test User", }, - error: undefined, - networkStatus: NetworkStatus.ready, - }); - } + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } +}); - await act(() => user.click(screen.getByText("Refetch"))); +it("does not mask query when dataMasking is `false`", async () => { + type UserFieldsFragment = { + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" }; - { - // parent component re-suspends - const { renderedComponents } = await Profiler.takeRender(); + interface Query { + currentUser: { + __typename: "User"; + id: number; + name: string; + } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; + } - expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + const query: TypedDocumentNode = gql` + query MaskedQuery { + currentUser { + id + name + ...UserFields + } } - { - const { snapshot } = await Profiler.takeRender(); + fragment UserFields on User { + age + } + `; - expect(snapshot.result).toEqual({ + const mocks = [ + { + request: { query }, + result: { data: { - character: { - __typename: "Character", - id: "1", - name: "Spider-Man (refetched)", + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + age: 30, }, }, - error: undefined, - networkStatus: NetworkStatus.ready, - }); - } + }, + }, + ]; - await expect(Profiler).not.toRerender({ timeout: 50 }); + const client = new ApolloClient({ + dataMasking: false, + cache: new InMemoryCache(), + link: new MockLink(mocks), }); - it("re-suspends when calling `refetch` with new variables", async () => { - const { query, mocks } = setupVariablesCase(); - const user = userEvent.setup(); - const Profiler = createDefaultProfiler(); - const { SuspenseFallback, ReadQueryHook } = - createDefaultTrackedComponents(Profiler); + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - function App() { - useTrackRenders(); - const [queryRef, { refetch }] = useBackgroundQuery(query, { - variables: { id: "1" }, - }); + function App() { + useTrackRenders(); + const [queryRef] = useBackgroundQuery(query); - return ( - <> - - }> - - - - ); - } + return ( + }> + + + ); + } - renderWithMocks(, { mocks, wrapper: Profiler }); + renderWithClient(, { client, wrapper: Profiler }); - { - const { renderedComponents } = await Profiler.takeRender(); + // loading + await Profiler.takeRender(); - expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); - } + const { snapshot } = await Profiler.takeRender(); - { - const { snapshot } = await Profiler.takeRender(); + expect(snapshot.result).toEqual({ + data: { + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + age: 30, + }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); +}); - expect(snapshot.result).toEqual({ - data: { - character: { - __typename: "Character", - id: "1", - name: "Spider-Man", - }, - }, - error: undefined, - networkStatus: NetworkStatus.ready, - }); - } +it("does not mask query by default", async () => { + type UserFieldsFragment = { + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" }; - await act(() => user.click(screen.getByText("Refetch"))); + interface Query { + currentUser: { + __typename: "User"; + id: number; + name: string; + } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; + } - { - const { renderedComponents } = await Profiler.takeRender(); + const query: TypedDocumentNode = gql` + query MaskedQuery { + currentUser { + id + name + ...UserFields + } + } - expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + fragment UserFields on User { + age } + `; + const mocks = [ { - const { snapshot } = await Profiler.takeRender(); - - expect(snapshot.result).toEqual({ + request: { query }, + result: { data: { - character: { - __typename: "Character", - id: "2", - name: "Black Widow", + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + age: 30, }, }, - error: undefined, - networkStatus: NetworkStatus.ready, - }); - } + }, + }, + ]; - await expect(Profiler).not.toRerender({ timeout: 50 }); + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: new MockLink(mocks), }); - it("re-suspends multiple times when calling `refetch` multiple times", async () => { - const { query, mocks: defaultMocks } = setupVariablesCase(); - const user = userEvent.setup(); - const Profiler = createDefaultProfiler(); - const { SuspenseFallback, ReadQueryHook } = - createDefaultTrackedComponents(Profiler); + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - const mocks: MockedResponse[] = [ - ...defaultMocks, - { - request: { query, variables: { id: "1" } }, - result: { - data: { - character: { - __typename: "Character", - id: "1", - name: "Spider-Man (refetched)", - }, - }, - }, - delay: 10, - }, - { - request: { query, variables: { id: "1" } }, - result: { - data: { - character: { - __typename: "Character", - id: "1", + function App() { + useTrackRenders(); + const [queryRef] = useBackgroundQuery(query); + + return ( + }> + + + ); + } + + renderWithClient(, { client, wrapper: Profiler }); + + // loading + await Profiler.takeRender(); + + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + age: 30, + }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); +}); + +it("masks queries updated by the cache", async () => { + type UserFieldsFragment = { + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" }; + + interface Query { + currentUser: { + __typename: "User"; + id: number; + name: string; + } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; + } + + const query: MaskedDocumentNode = gql` + query MaskedQuery { + 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), + }); + + const Profiler = createDefaultProfiler>(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); + + function App() { + useTrackRenders(); + const [queryRef] = useBackgroundQuery(query); + + return ( + }> + + + ); + } + + renderWithClient(, { client, wrapper: Profiler }); + + // loading + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + client.writeQuery({ + query, + data: { + currentUser: { + __typename: "User", + id: 1, + name: "Test User (updated)", + // @ts-ignore TODO: Determine how to handle cache writes with masked + // query type + age: 35, + }, + }, + }); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { + currentUser: { + __typename: "User", + id: 1, + name: "Test User (updated)", + }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } +}); + +it("does not rerender when updating field in named fragment", async () => { + type UserFieldsFragment = { + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" }; + + interface Query { + currentUser: { + __typename: "User"; + id: number; + name: string; + } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; + } + + const query: MaskedDocumentNode = gql` + query MaskedQuery { + 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), + }); + + const Profiler = createDefaultProfiler>(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); + + function App() { + useTrackRenders(); + const [queryRef] = useBackgroundQuery(query); + + return ( + }> + + + ); + } + + renderWithClient(, { client, wrapper: Profiler }); + + // loading + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + client.writeQuery({ + query, + data: { + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + // @ts-ignore TODO: Determine how to handle cache writes with masked + // query type + age: 35, + }, + }, + }); + + await expect(Profiler).not.toRerender(); + + expect(client.readQuery({ query })).toEqual({ + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + age: 35, + }, + }); +}); + +it("masks result from cache when using with cache-first fetch policy", async () => { + type UserFieldsFragment = { + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" }; + + interface Query { + currentUser: { + __typename: "User"; + id: number; + name: string; + } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; + } + + const query: MaskedDocumentNode = gql` + query MaskedQuery { + 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), + }); + + client.writeQuery({ + query, + data: { + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + age: 30, + }, + }, + }); + + const Profiler = createDefaultProfiler>(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); + + function App() { + useTrackRenders(); + const [queryRef] = useBackgroundQuery(query, { + fetchPolicy: "cache-first", + }); + + return ( + }> + + + ); + } + + renderWithClient(, { client, wrapper: Profiler }); + + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); +}); + +it("masks cache and network result when using cache-and-network fetch policy", async () => { + type UserFieldsFragment = { + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" }; + + interface Query { + currentUser: { + __typename: "User"; + id: number; + name: string; + } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; + } + + const query: MaskedDocumentNode = gql` + query MaskedQuery { + currentUser { + id + name + ...UserFields + } + } + + fragment UserFields on User { + age + } + `; + + const mocks = [ + { + request: { query }, + result: { + data: { + currentUser: { + __typename: "User", + id: 1, + name: "Test User (server)", + age: 35, + }, + }, + }, + delay: 20, + }, + ]; + + const client = new ApolloClient({ + dataMasking: true, + cache: new InMemoryCache(), + link: new MockLink(mocks), + }); + + client.writeQuery({ + query, + data: { + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + age: 34, + }, + }, + }); + + const Profiler = createDefaultProfiler>(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); + + function App() { + useTrackRenders(); + const [queryRef] = useBackgroundQuery(query, { + fetchPolicy: "cache-and-network", + }); + + return ( + }> + + + ); + } + + renderWithClient(, { client, wrapper: Profiler }); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + }, + }, + error: undefined, + networkStatus: NetworkStatus.loading, + }); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { + currentUser: { + __typename: "User", + id: 1, + name: "Test User (server)", + }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } +}); + +it("masks partial cache data when returnPartialData is `true`", async () => { + type UserFieldsFragment = { + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" }; + + interface Query { + currentUser: { + __typename: "User"; + id: number; + name: string; + } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; + } + + const query: MaskedDocumentNode = gql` + query MaskedQuery { + currentUser { + id + name + ...UserFields + } + } + + fragment UserFields on User { + age + } + `; + + const mocks = [ + { + request: { query }, + result: { + data: { + currentUser: { + __typename: "User", + id: 1, + name: "Test User (server)", + age: 35, + }, + }, + }, + delay: 20, + }, + ]; + + const client = new ApolloClient({ + dataMasking: true, + cache: new InMemoryCache(), + link: new MockLink(mocks), + }); + + { + using _ = spyOnConsole("error"); + client.writeQuery({ + query, + data: { + // @ts-expect-error writing partial cache data + currentUser: { + __typename: "User", + id: 1, + age: 34, + }, + }, + }); + } + + const Profiler = createDefaultProfiler>>(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); + + function App() { + useTrackRenders(); + const [queryRef] = useBackgroundQuery(query, { returnPartialData: true }); + + return ( + }> + + + ); + } + + renderWithClient(, { client, wrapper: Profiler }); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { + currentUser: { + __typename: "User", + id: 1, + }, + }, + error: undefined, + networkStatus: NetworkStatus.loading, + }); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { + currentUser: { + __typename: "User", + id: 1, + name: "Test User (server)", + }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } +}); + +it("masks partial data returned from data on errors with errorPolicy `all`", async () => { + type UserFieldsFragment = { + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" }; + + interface Query { + currentUser: { + __typename: "User"; + id: number; + name: string; + } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; + } + + const query: MaskedDocumentNode = gql` + query MaskedQuery { + currentUser { + id + name + ...UserFields + } + } + + fragment UserFields on User { + age + } + `; + + const mocks = [ + { + request: { query }, + result: { + data: { + currentUser: { + __typename: "User", + id: 1, + name: null, + age: 34, + }, + }, + errors: [new GraphQLError("Couldn't get name")], + }, + delay: 20, + }, + ]; + + const client = new ApolloClient({ + dataMasking: true, + cache: new InMemoryCache(), + link: new MockLink(mocks), + }); + + const Profiler = createDefaultProfiler | undefined>(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); + + function App() { + useTrackRenders(); + const [queryRef] = useBackgroundQuery(query, { errorPolicy: "all" }); + + return ( + }> + + + ); + } + + renderWithClient(, { client, wrapper: Profiler }); + + // loading + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { + currentUser: { + __typename: "User", + id: 1, + name: null, + }, + }, + error: new ApolloError({ + graphQLErrors: [new GraphQLError("Couldn't get name")], + }), + networkStatus: NetworkStatus.error, + }); + } +}); + +describe("refetch", () => { + it("re-suspends when calling `refetch`", async () => { + const { query, mocks: defaultMocks } = setupVariablesCase(); + const user = userEvent.setup(); + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); + + const mocks: MockedResponse[] = [ + ...defaultMocks, + { + request: { query, variables: { id: "1" } }, + result: { + data: { + character: { + __typename: "Character", + id: "1", + name: "Spider-Man (refetched)", + }, + }, + }, + delay: 10, + }, + ]; + + function App() { + useTrackRenders(); + const [queryRef, { refetch }] = useBackgroundQuery(query, { + variables: { id: "1" }, + }); + + return ( + <> + + }> + + + + ); + } + + renderWithMocks(, { mocks, wrapper: Profiler }); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { + character: { + __typename: "Character", + id: "1", + name: "Spider-Man", + }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await act(() => user.click(screen.getByText("Refetch"))); + + { + // parent component re-suspends + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { + character: { + __typename: "Character", + id: "1", + name: "Spider-Man (refetched)", + }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await expect(Profiler).not.toRerender({ timeout: 50 }); + }); + + it("re-suspends when calling `refetch` with new variables", async () => { + const { query, mocks } = setupVariablesCase(); + const user = userEvent.setup(); + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); + + function App() { + useTrackRenders(); + const [queryRef, { refetch }] = useBackgroundQuery(query, { + variables: { id: "1" }, + }); + + return ( + <> + + }> + + + + ); + } + + renderWithMocks(, { mocks, wrapper: Profiler }); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { + character: { + __typename: "Character", + id: "1", + name: "Spider-Man", + }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await act(() => user.click(screen.getByText("Refetch"))); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { + character: { + __typename: "Character", + id: "2", + name: "Black Widow", + }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await expect(Profiler).not.toRerender({ timeout: 50 }); + }); + + it("re-suspends multiple times when calling `refetch` multiple times", async () => { + const { query, mocks: defaultMocks } = setupVariablesCase(); + const user = userEvent.setup(); + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); + + const mocks: MockedResponse[] = [ + ...defaultMocks, + { + request: { query, variables: { id: "1" } }, + result: { + data: { + character: { + __typename: "Character", + id: "1", + name: "Spider-Man (refetched)", + }, + }, + }, + delay: 10, + }, + { + request: { query, variables: { id: "1" } }, + result: { + data: { + character: { + __typename: "Character", + id: "1", name: "Spider-Man (refetched again)", }, }, @@ -6231,8 +7132,40 @@ describe.skip("type tests", () => { const { data: explicit } = useReadQuery(explicitQueryRef); - expectTypeOf(explicit).toEqualTypeOf(); - expectTypeOf(explicit).not.toEqualTypeOf(); + expectTypeOf(explicit).toEqualTypeOf(); + expectTypeOf(explicit).not.toEqualTypeOf(); + + const { query: maskedQuery } = setupMaskedVariablesCase(); + + { + const [queryRef] = useBackgroundQuery(maskedQuery); + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).not.toEqualTypeOf(); + } + + { + const [queryRef] = useBackgroundQuery< + MaskedVariablesCaseData, + VariablesCaseVariables + >(maskedQuery); + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).not.toEqualTypeOf(); + } + + { + const [queryRef] = useBackgroundQuery< + Masked, + VariablesCaseVariables + >(maskedQuery); + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).not.toEqualTypeOf(); + } }); it('returns TData | undefined with errorPolicy: "ignore"', () => { @@ -6257,6 +7190,38 @@ describe.skip("type tests", () => { expectTypeOf(explicit).toEqualTypeOf(); expectTypeOf(explicit).not.toEqualTypeOf(); + + const { query: maskedQuery } = setupMaskedVariablesCase(); + + { + const [queryRef] = useBackgroundQuery(maskedQuery); + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).not.toEqualTypeOf(); + } + + { + const [queryRef] = useBackgroundQuery< + MaskedVariablesCaseData, + VariablesCaseVariables + >(maskedQuery); + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).not.toEqualTypeOf(); + } + + { + const [queryRef] = useBackgroundQuery< + Masked, + VariablesCaseVariables + >(maskedQuery); + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).not.toEqualTypeOf(); + } }); it('returns TData | undefined with errorPolicy: "all"', () => { @@ -6277,6 +7242,46 @@ describe.skip("type tests", () => { expectTypeOf(explicit).toEqualTypeOf(); expectTypeOf(explicit).not.toEqualTypeOf(); + + const { query: maskedQuery } = setupMaskedVariablesCase(); + + { + const [queryRef] = useBackgroundQuery(maskedQuery, { + errorPolicy: "all", + }); + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).not.toEqualTypeOf< + UnmaskedVariablesCaseData | undefined + >(); + } + + { + const [queryRef] = useBackgroundQuery< + MaskedVariablesCaseData, + VariablesCaseVariables + >(maskedQuery, { errorPolicy: "all" }); + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).not.toEqualTypeOf< + MaskedVariablesCaseData | undefined + >(); + } + + { + const [queryRef] = useBackgroundQuery< + Masked, + VariablesCaseVariables + >(maskedQuery, { errorPolicy: "all" }); + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).not.toEqualTypeOf< + UnmaskedVariablesCaseData | undefined + >(); + } }); it('returns TData with errorPolicy: "none"', () => { @@ -6297,6 +7302,40 @@ describe.skip("type tests", () => { expectTypeOf(explicit).toEqualTypeOf(); expectTypeOf(explicit).not.toEqualTypeOf(); + + const { query: maskedQuery } = setupMaskedVariablesCase(); + + { + const [queryRef] = useBackgroundQuery(maskedQuery, { + errorPolicy: "none", + }); + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).not.toEqualTypeOf(); + } + + { + const [queryRef] = useBackgroundQuery< + MaskedVariablesCaseData, + VariablesCaseVariables + >(maskedQuery, { errorPolicy: "none" }); + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).not.toEqualTypeOf(); + } + + { + const [queryRef] = useBackgroundQuery< + Masked, + VariablesCaseVariables + >(maskedQuery, { errorPolicy: "none" }); + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).not.toEqualTypeOf(); + } }); it("returns DeepPartial with returnPartialData: true", () => { @@ -6321,6 +7360,48 @@ describe.skip("type tests", () => { expectTypeOf(explicit).toEqualTypeOf>(); expectTypeOf(explicit).not.toEqualTypeOf(); + + const { query: maskedQuery } = setupMaskedVariablesCase(); + + { + const [queryRef] = useBackgroundQuery(maskedQuery, { + returnPartialData: true, + }); + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf>(); + expectTypeOf(data).not.toEqualTypeOf< + DeepPartial + >(); + } + + { + const [queryRef] = useBackgroundQuery< + MaskedVariablesCaseData, + VariablesCaseVariables + >(maskedQuery, { returnPartialData: true }); + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf< + DeepPartial + >(); + expectTypeOf(data).not.toEqualTypeOf< + DeepPartial + >(); + } + + { + const [queryRef] = useBackgroundQuery< + Masked, + VariablesCaseVariables + >(maskedQuery, { returnPartialData: true }); + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf>(); + expectTypeOf(data).not.toEqualTypeOf< + DeepPartial + >(); + } }); it("returns TData with returnPartialData: false", () => { @@ -6345,6 +7426,40 @@ describe.skip("type tests", () => { expectTypeOf(explicit).toEqualTypeOf(); expectTypeOf(explicit).not.toEqualTypeOf>(); + + const { query: maskedQuery } = setupMaskedVariablesCase(); + + { + const [queryRef] = useBackgroundQuery(maskedQuery, { + returnPartialData: false, + }); + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).not.toEqualTypeOf(); + } + + { + const [queryRef] = useBackgroundQuery< + MaskedVariablesCaseData, + VariablesCaseVariables + >(maskedQuery, { returnPartialData: false }); + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).not.toEqualTypeOf(); + } + + { + const [queryRef] = useBackgroundQuery< + Masked, + VariablesCaseVariables + >(maskedQuery, { returnPartialData: false }); + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).not.toEqualTypeOf(); + } }); it("returns TData when passing an option that does not affect TData", () => { @@ -6369,10 +7484,45 @@ describe.skip("type tests", () => { expectTypeOf(explicit).toEqualTypeOf(); expectTypeOf(explicit).not.toEqualTypeOf>(); + + const { query: maskedQuery } = setupMaskedVariablesCase(); + + { + const [queryRef] = useBackgroundQuery(maskedQuery, { + fetchPolicy: "no-cache", + }); + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).not.toEqualTypeOf(); + } + + { + const [queryRef] = useBackgroundQuery< + MaskedVariablesCaseData, + VariablesCaseVariables + >(maskedQuery, { fetchPolicy: "no-cache" }); + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).not.toEqualTypeOf(); + } + + { + const [queryRef] = useBackgroundQuery< + Masked, + VariablesCaseVariables + >(maskedQuery, { fetchPolicy: "no-cache" }); + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).not.toEqualTypeOf(); + } }); it("handles combinations of options", () => { const { query } = setupVariablesCase(); + const { query: maskedQuery } = setupMaskedVariablesCase(); const [inferredPartialDataIgnoreQueryRef] = useBackgroundQuery(query, { returnPartialData: true, @@ -6408,6 +7558,51 @@ describe.skip("type tests", () => { explicitPartialDataIgnore ).not.toEqualTypeOf(); + { + const [queryRef] = useBackgroundQuery(maskedQuery, { + returnPartialData: true, + errorPolicy: "ignore", + }); + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf< + DeepPartial | undefined + >(); + expectTypeOf(data).not.toEqualTypeOf< + DeepPartial | undefined + >(); + } + + { + const [queryRef] = useBackgroundQuery< + MaskedVariablesCaseData, + VariablesCaseVariables + >(maskedQuery, { returnPartialData: true, errorPolicy: "ignore" }); + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf< + DeepPartial | undefined + >(); + expectTypeOf(data).not.toEqualTypeOf< + DeepPartial | undefined + >(); + } + + { + const [queryRef] = useBackgroundQuery< + Masked, + VariablesCaseVariables + >(maskedQuery, { returnPartialData: true, errorPolicy: "ignore" }); + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf< + DeepPartial | undefined + >(); + expectTypeOf(data).not.toEqualTypeOf< + DeepPartial | undefined + >(); + } + const [inferredPartialDataNoneQueryRef] = useBackgroundQuery(query, { returnPartialData: true, errorPolicy: "none", @@ -6442,6 +7637,47 @@ describe.skip("type tests", () => { expectTypeOf( explicitPartialDataNone ).not.toEqualTypeOf(); + + { + const [queryRef] = useBackgroundQuery(maskedQuery, { + returnPartialData: true, + errorPolicy: "none", + }); + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf>(); + expectTypeOf(data).not.toEqualTypeOf< + DeepPartial + >(); + } + + { + const [queryRef] = useBackgroundQuery< + MaskedVariablesCaseData, + VariablesCaseVariables + >(maskedQuery, { returnPartialData: true, errorPolicy: "none" }); + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf< + DeepPartial + >(); + expectTypeOf(data).not.toEqualTypeOf< + DeepPartial + >(); + } + + { + const [queryRef] = useBackgroundQuery< + Masked, + VariablesCaseVariables + >(maskedQuery, { returnPartialData: true, errorPolicy: "none" }); + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf>(); + expectTypeOf(data).not.toEqualTypeOf< + DeepPartial + >(); + } }); it("returns correct TData type when combined options that do not affect TData", () => { @@ -6470,10 +7706,63 @@ describe.skip("type tests", () => { expectTypeOf(explicit).toEqualTypeOf>(); expectTypeOf(explicit).not.toEqualTypeOf(); + + const { query: maskedQuery } = setupMaskedVariablesCase(); + + { + const [queryRef] = useBackgroundQuery(maskedQuery, { + fetchPolicy: "no-cache", + returnPartialData: true, + errorPolicy: "none", + }); + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf>(); + expectTypeOf(data).not.toEqualTypeOf< + DeepPartial + >(); + } + + { + const [queryRef] = useBackgroundQuery< + MaskedVariablesCaseData, + VariablesCaseVariables + >(maskedQuery, { + fetchPolicy: "no-cache", + returnPartialData: true, + errorPolicy: "none", + }); + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf< + DeepPartial + >(); + expectTypeOf(data).not.toEqualTypeOf< + DeepPartial + >(); + } + + { + const [queryRef] = useBackgroundQuery< + Masked, + VariablesCaseVariables + >(maskedQuery, { + fetchPolicy: "no-cache", + returnPartialData: true, + errorPolicy: "none", + }); + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf>(); + expectTypeOf(data).not.toEqualTypeOf< + DeepPartial + >(); + } }); it("returns QueryRef | undefined when `skip` is present", () => { const { query } = setupVariablesCase(); + const { query: maskedQuery } = setupMaskedVariablesCase(); const [inferredQueryRef] = useBackgroundQuery(query, { skip: true, @@ -6504,6 +7793,48 @@ describe.skip("type tests", () => { QueryRef >(); + { + const [queryRef] = useBackgroundQuery(maskedQuery, { skip: true }); + + expectTypeOf(queryRef).toEqualTypeOf< + | QueryRef, VariablesCaseVariables> + | undefined + >(); + expectTypeOf(queryRef).not.toEqualTypeOf< + QueryRef | undefined + >(); + } + + { + const [queryRef] = useBackgroundQuery< + MaskedVariablesCaseData, + VariablesCaseVariables + >(maskedQuery, { skip: true }); + + expectTypeOf(queryRef).toEqualTypeOf< + QueryRef | undefined + >(); + expectTypeOf(queryRef).not.toEqualTypeOf< + | QueryRef, VariablesCaseVariables> + | undefined + >(); + } + + { + const [queryRef] = useBackgroundQuery< + Masked, + VariablesCaseVariables + >(maskedQuery, { skip: true }); + + expectTypeOf(queryRef).toEqualTypeOf< + | QueryRef, VariablesCaseVariables> + | undefined + >(); + expectTypeOf(queryRef).not.toEqualTypeOf< + QueryRef | undefined + >(); + } + // TypeScript is too smart and using a `const` or `let` boolean variable // for the `skip` option results in a false positive. Using an options // object allows us to properly check for a dynamic case. @@ -6524,6 +7855,50 @@ describe.skip("type tests", () => { expectTypeOf(dynamicQueryRef).not.toEqualTypeOf< QueryRef >(); + + { + const [queryRef] = useBackgroundQuery(maskedQuery, { + skip: options.skip, + }); + + expectTypeOf(queryRef).toEqualTypeOf< + | QueryRef, VariablesCaseVariables> + | undefined + >(); + expectTypeOf(queryRef).not.toEqualTypeOf< + QueryRef | undefined + >(); + } + + { + const [queryRef] = useBackgroundQuery< + MaskedVariablesCaseData, + VariablesCaseVariables + >(maskedQuery, { skip: options.skip }); + + expectTypeOf(queryRef).toEqualTypeOf< + QueryRef | undefined + >(); + expectTypeOf(queryRef).not.toEqualTypeOf< + | QueryRef, VariablesCaseVariables> + | undefined + >(); + } + + { + const [queryRef] = useBackgroundQuery< + Masked, + VariablesCaseVariables + >(maskedQuery, { skip: options.skip }); + + expectTypeOf(queryRef).toEqualTypeOf< + | QueryRef, VariablesCaseVariables> + | undefined + >(); + expectTypeOf(queryRef).not.toEqualTypeOf< + QueryRef | undefined + >(); + } }); it("returns `undefined` when using `skipToken` unconditionally", () => { @@ -6545,6 +7920,43 @@ describe.skip("type tests", () => { expectTypeOf(explicitQueryRef).not.toEqualTypeOf< QueryRef | undefined >(); + + const { query: maskedQuery } = setupMaskedVariablesCase(); + + { + const [queryRef] = useBackgroundQuery(maskedQuery, skipToken); + + expectTypeOf(queryRef).toEqualTypeOf(); + expectTypeOf(queryRef).not.toEqualTypeOf< + | QueryRef, VariablesCaseVariables> + | undefined + >(); + } + + { + const [queryRef] = useBackgroundQuery< + MaskedVariablesCaseData, + VariablesCaseVariables + >(maskedQuery, skipToken); + + expectTypeOf(queryRef).toEqualTypeOf(); + expectTypeOf(queryRef).not.toEqualTypeOf< + QueryRef | undefined + >(); + } + + { + const [queryRef] = useBackgroundQuery< + Masked, + VariablesCaseVariables + >(maskedQuery, skipToken); + + expectTypeOf(queryRef).toEqualTypeOf(); + expectTypeOf(queryRef).not.toEqualTypeOf< + | QueryRef, VariablesCaseVariables> + | undefined + >(); + } }); it("returns QueryRef | undefined when using conditional `skipToken`", () => { @@ -6582,6 +7994,53 @@ describe.skip("type tests", () => { expectTypeOf(explicitQueryRef).not.toEqualTypeOf< QueryRef >(); + + const { query: maskedQuery } = setupMaskedVariablesCase(); + + { + const [queryRef] = useBackgroundQuery( + maskedQuery, + options.skip ? skipToken : undefined + ); + + expectTypeOf(queryRef).toEqualTypeOf< + | QueryRef, VariablesCaseVariables> + | undefined + >(); + expectTypeOf(queryRef).not.toEqualTypeOf< + QueryRef | undefined + >(); + } + + { + const [queryRef] = useBackgroundQuery< + MaskedVariablesCaseData, + VariablesCaseVariables + >(maskedQuery, options.skip ? skipToken : undefined); + + expectTypeOf(queryRef).toEqualTypeOf< + QueryRef | undefined + >(); + expectTypeOf(queryRef).not.toEqualTypeOf< + | QueryRef, VariablesCaseVariables> + | undefined + >(); + } + + { + const [queryRef] = useBackgroundQuery< + Masked, + VariablesCaseVariables + >(maskedQuery, options.skip ? skipToken : undefined); + + expectTypeOf(queryRef).toEqualTypeOf< + | QueryRef, VariablesCaseVariables> + | undefined + >(); + expectTypeOf(queryRef).not.toEqualTypeOf< + QueryRef | undefined + >(); + } }); it("returns QueryRef> | undefined when using `skipToken` with `returnPartialData`", () => { @@ -6623,5 +8082,224 @@ describe.skip("type tests", () => { expectTypeOf(explicitQueryRef).not.toEqualTypeOf< QueryRef >(); + + const { query: maskedQuery } = setupMaskedVariablesCase(); + + { + const [queryRef] = useBackgroundQuery( + maskedQuery, + options.skip ? skipToken : { returnPartialData: true } + ); + + expectTypeOf(queryRef).toEqualTypeOf< + | QueryRef< + DeepPartial>, + VariablesCaseVariables + > + | undefined + >(); + expectTypeOf(queryRef).not.toEqualTypeOf< + | QueryRef, VariablesCaseVariables> + | undefined + >(); + } + + { + const [queryRef] = useBackgroundQuery< + MaskedVariablesCaseData, + VariablesCaseVariables + >(maskedQuery, options.skip ? skipToken : { returnPartialData: true }); + + expectTypeOf(queryRef).toEqualTypeOf< + | QueryRef, VariablesCaseVariables> + | undefined + >(); + expectTypeOf(queryRef).not.toEqualTypeOf< + | QueryRef< + DeepPartial>, + VariablesCaseVariables + > + | undefined + >(); + } + + { + const [queryRef] = useBackgroundQuery< + Masked, + VariablesCaseVariables + >(maskedQuery, options.skip ? skipToken : { returnPartialData: true }); + + expectTypeOf(queryRef).toEqualTypeOf< + | QueryRef< + DeepPartial>, + VariablesCaseVariables + > + | undefined + >(); + expectTypeOf(queryRef).not.toEqualTypeOf< + | QueryRef, VariablesCaseVariables> + | undefined + >(); + } + }); + + it("uses proper masked types for refetch", async () => { + const { query, unmaskedQuery } = setupMaskedVariablesCase(); + + { + const [, { refetch }] = useBackgroundQuery(query); + + const result = await refetch(); + + expectTypeOf(result.data).toEqualTypeOf(); + expectTypeOf(result.data).not.toEqualTypeOf(); + } + + { + const [, { refetch }] = useBackgroundQuery(unmaskedQuery); + + const result = await refetch(); + + expectTypeOf(result.data).toEqualTypeOf(); + expectTypeOf(result.data).not.toEqualTypeOf(); + } + }); + + it("uses proper masked types for fetchMore", async () => { + const { query, unmaskedQuery } = setupMaskedVariablesCase(); + + { + const [, { fetchMore }] = useBackgroundQuery(query); + + const result = await fetchMore({ + updateQuery: (queryData, { fetchMoreResult }) => { + expectTypeOf(queryData).toEqualTypeOf(); + expectTypeOf(queryData).not.toEqualTypeOf(); + + expectTypeOf( + fetchMoreResult + ).toEqualTypeOf(); + expectTypeOf( + fetchMoreResult + ).not.toEqualTypeOf(); + + return {} as UnmaskedVariablesCaseData; + }, + }); + + expectTypeOf(result.data).toEqualTypeOf(); + expectTypeOf(result.data).not.toEqualTypeOf(); + } + + { + const [, { fetchMore }] = useBackgroundQuery(unmaskedQuery); + + const result = await fetchMore({ + updateQuery: (queryData, { fetchMoreResult }) => { + expectTypeOf(queryData).toEqualTypeOf(); + expectTypeOf(queryData).not.toEqualTypeOf(); + + expectTypeOf( + fetchMoreResult + ).toEqualTypeOf(); + expectTypeOf( + fetchMoreResult + ).not.toEqualTypeOf(); + + return {} as UnmaskedVariablesCaseData; + }, + }); + + expectTypeOf(result.data).toEqualTypeOf(); + expectTypeOf(result.data).not.toEqualTypeOf(); + } + }); + + it("uses proper masked types for subscribeToMore", async () => { + type CharacterFragment = { + __typename: "Character"; + name: string; + } & { " $fragmentName": "CharacterFragment" }; + + type Subscription = { + pushLetter: { + __typename: "Character"; + id: number; + } & { " $fragmentRefs": { CharacterFragment: CharacterFragment } }; + }; + + type UnmaskedSubscription = { + pushLetter: { + __typename: "Character"; + id: number; + name: string; + }; + }; + + const { query, unmaskedQuery } = setupMaskedVariablesCase(); + + { + const [, { subscribeToMore }] = useBackgroundQuery(query); + + const subscription: MaskedDocumentNode = gql` + subscription { + pushLetter { + id + ...CharacterFragment + } + } + + fragment CharacterFragment on Character { + name + } + `; + + subscribeToMore({ + document: subscription, + updateQuery: (queryData, { subscriptionData }) => { + expectTypeOf(queryData).toEqualTypeOf(); + expectTypeOf(queryData).not.toEqualTypeOf(); + + expectTypeOf( + subscriptionData.data + ).toEqualTypeOf(); + expectTypeOf(subscriptionData.data).not.toEqualTypeOf(); + + return {} as UnmaskedVariablesCaseData; + }, + }); + } + + { + const [, { subscribeToMore }] = useBackgroundQuery(unmaskedQuery); + + const subscription: TypedDocumentNode = gql` + subscription { + pushLetter { + id + ...CharacterFragment + } + } + + fragment CharacterFragment on Character { + name + } + `; + + subscribeToMore({ + document: subscription, + updateQuery: (queryData, { subscriptionData }) => { + expectTypeOf(queryData).toEqualTypeOf(); + expectTypeOf(queryData).not.toEqualTypeOf(); + + expectTypeOf( + subscriptionData.data + ).toEqualTypeOf(); + expectTypeOf(subscriptionData.data).not.toEqualTypeOf(); + + return {} as UnmaskedVariablesCaseData; + }, + }); + } }); }); diff --git a/src/react/hooks/__tests__/useFragment.test.tsx b/src/react/hooks/__tests__/useFragment.test.tsx index 378f48f9a4d..c221fc957d7 100644 --- a/src/react/hooks/__tests__/useFragment.test.tsx +++ b/src/react/hooks/__tests__/useFragment.test.tsx @@ -9,7 +9,11 @@ import { import userEvent from "@testing-library/user-event"; import { act } from "@testing-library/react"; -import { UseFragmentOptions, useFragment } from "../useFragment"; +import { + UseFragmentOptions, + UseFragmentResult, + useFragment, +} from "../useFragment"; import { MockedProvider } from "../../../testing"; import { ApolloProvider } from "../../context"; import { @@ -29,7 +33,14 @@ import { concatPagination } from "../../../utilities"; import assert from "assert"; import { expectTypeOf } from "expect-type"; import { SubscriptionObserver } from "zen-observable-ts"; -import { profile, profileHook, spyOnConsole } from "../../../testing/internal"; +import { + createProfiler, + profile, + profileHook, + spyOnConsole, + useTrackRenders, +} from "../../../testing/internal"; +import { FragmentType } from "../../../masking"; describe("useFragment", () => { it("is importable and callable", () => { @@ -1455,7 +1466,7 @@ describe("useFragment", () => { it("does not rerender when fields with @nonreactive change", async () => { type Post = { - __typename: "User"; + __typename: "Post"; id: number; title: string; updatedAt: string; @@ -1522,7 +1533,7 @@ describe("useFragment", () => { it("does not rerender when fields with @nonreactive on nested fragment change", async () => { type Post = { - __typename: "User"; + __typename: "Post"; id: number; title: string; updatedAt: string; @@ -1597,6 +1608,46 @@ describe("useFragment", () => { await expect(ProfiledHook).not.toRerender(); }); + it("warns when passing parent object to `from` when key fields are missing", async () => { + using _ = spyOnConsole("warn"); + + interface Fragment { + age: number; + } + + const fragment: TypedDocumentNode = gql` + fragment UserFields on User { + age + } + `; + + const client = new ApolloClient({ cache: new InMemoryCache() }); + + const ProfiledHook = profileHook(() => + useFragment({ fragment, from: { __typename: "User" } }) + ); + + render(, { + wrapper: ({ children }) => ( + {children} + ), + }); + + expect(console.warn).toHaveBeenCalledTimes(1); + expect(console.warn).toHaveBeenCalledWith( + "Could not identify object passed to `from` for '%s' fragment, either because the object is non-normalized or the key fields are missing. If you are masking this object, please ensure the key fields are requested by the parent object.", + "UserFields" + ); + + { + const { data, complete } = await ProfiledHook.takeSnapshot(); + + expect(data).toEqual({}); + // TODO: Update when https://github.com/apollographql/apollo-client/issues/12003 is fixed + expect(complete).toBe(true); + } + }); + describe("tests with incomplete data", () => { let cache: InMemoryCache, wrapper: React.FunctionComponent; const ItemFragment = gql` @@ -1725,33 +1776,295 @@ describe("useFragment", () => { }); }); }); +}); - // https://github.com/apollographql/apollo-client/issues/12051 - it("does not warn when the cache identifier is invalid", async () => { - using _ = spyOnConsole("warn"); - const cache = new InMemoryCache(); +describe("data masking", () => { + it("returns masked fragment when data masking is enabled", async () => { + type Post = { + __typename: "Post"; + id: number; + title: string; + }; + + const client = new ApolloClient({ + dataMasking: true, + cache: new InMemoryCache(), + }); + + const fragment: TypedDocumentNode = gql` + fragment PostFragment on Post { + id + title + ...PostFields + } + + fragment PostFields on Post { + updatedAt + } + `; + + client.writeFragment({ + fragment, + fragmentName: "PostFragment", + data: { + __typename: "Post", + id: 1, + title: "Blog post", + // @ts-expect-error Need to determine how to work with masked types + updatedAt: "2024-01-01", + }, + }); const ProfiledHook = profileHook(() => useFragment({ - fragment: ItemFragment, - // Force a value that results in cache.identify === undefined - from: { __typename: "Item" }, + fragment, + fragmentName: "PostFragment", + from: { __typename: "Post", id: 1 }, }) ); render(, { wrapper: ({ children }) => ( - {children} + {children} + ), + }); + + { + const snapshot = await ProfiledHook.takeSnapshot(); + + expect(snapshot).toEqual({ + complete: true, + data: { + __typename: "Post", + id: 1, + title: "Blog post", + }, + }); + } + + await expect(ProfiledHook).not.toRerender(); + }); + + it("does not rerender for cache writes to masked fields", async () => { + type Post = { + __typename: "Post"; + id: number; + title: string; + }; + + const client = new ApolloClient({ + dataMasking: true, + cache: new InMemoryCache(), + }); + + const fragment: TypedDocumentNode = gql` + fragment PostFragment on Post { + id + title + ...PostFields + } + + fragment PostFields on Post { + updatedAt + } + `; + + client.writeFragment({ + fragment, + fragmentName: "PostFragment", + data: { + __typename: "Post", + id: 1, + title: "Blog post", + // @ts-expect-error Need to determine how to work with masked types + updatedAt: "2024-01-01", + }, + }); + + const ProfiledHook = profileHook(() => + useFragment({ + fragment, + fragmentName: "PostFragment", + from: { __typename: "Post", id: 1 }, + }) + ); + + render(, { + wrapper: ({ children }) => ( + {children} ), }); - expect(console.warn).not.toHaveBeenCalled(); + { + const snapshot = await ProfiledHook.takeSnapshot(); - const { data, complete } = await ProfiledHook.takeSnapshot(); + expect(snapshot).toEqual({ + complete: true, + data: { + __typename: "Post", + id: 1, + title: "Blog post", + }, + }); + } + + client.writeFragment({ + fragment, + fragmentName: "PostFragment", + data: { + __typename: "Post", + id: 1, + title: "Blog post", + // @ts-expect-error Need to determine how to work with masked types + updatedAt: "2024-02-01", + }, + }); + + await expect(ProfiledHook).not.toRerender(); + }); + + it("updates child fragments for cache updates to masked fields", async () => { + type ParentFragment = { + __typename: "Post"; + id: number; + title: string; + }; + + type ChildFragment = { + __typename: "Post"; + id: number; + title: string; + }; + + const client = new ApolloClient({ + dataMasking: true, + cache: new InMemoryCache(), + }); + + const childFragment: TypedDocumentNode = gql` + fragment PostFields on Post { + updatedAt + } + `; + + const parentFragment: TypedDocumentNode = gql` + fragment PostFragment on Post { + id + title + ...PostFields + } + + ${childFragment} + `; + + client.writeFragment({ + fragment: parentFragment, + fragmentName: "PostFragment", + data: { + __typename: "Post", + id: 1, + title: "Blog post", + // @ts-expect-error Need to determine how to work with masked types + updatedAt: "2024-01-01", + }, + }); + + const Profiler = createProfiler({ + initialSnapshot: { + parent: null as UseFragmentResult | null, + child: null as UseFragmentResult | null, + }, + }); + + function Parent() { + useTrackRenders(); + const parent = useFragment({ + fragment: parentFragment, + fragmentName: "PostFragment", + from: { __typename: "Post", id: 1 }, + }); + + Profiler.mergeSnapshot({ parent }); + + return parent.complete ? : null; + } + + function Child({ parent }: { parent: ParentFragment }) { + useTrackRenders(); + const child = useFragment({ fragment: childFragment, from: parent }); + + Profiler.mergeSnapshot({ child }); + + return null; + } + + render(, { + wrapper: ({ children }) => ( + + {children} + + ), + }); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([Parent, Child]); + expect(snapshot).toEqual({ + parent: { + complete: true, + data: { + __typename: "Post", + id: 1, + title: "Blog post", + }, + }, + child: { + complete: true, + data: { + __typename: "Post", + updatedAt: "2024-01-01", + }, + }, + }); + } + + client.writeFragment({ + fragment: parentFragment, + fragmentName: "PostFragment", + data: { + __typename: "Post", + id: 1, + title: "Blog post", + // @ts-expect-error Need to determine how to work with masked types + updatedAt: "2024-02-01", + }, + }); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([Child]); + expect(snapshot).toEqual({ + parent: { + complete: true, + data: { + __typename: "Post", + id: 1, + title: "Blog post", + }, + }, + child: { + complete: true, + data: { + __typename: "Post", + updatedAt: "2024-02-01", + }, + }, + }); + } - // TODO: Update when https://github.com/apollographql/apollo-client/issues/12003 is fixed - expect(complete).toBe(true); - expect(data).toEqual({}); + await expect(Profiler).not.toRerender(); }); }); @@ -2030,7 +2343,7 @@ describe.skip("Type Tests", () => { test("UseFragmentOptions interface shape", () => { expectTypeOf>().branded.toEqualTypeOf<{ - from: string | StoreObject | Reference; + from: string | StoreObject | Reference | FragmentType; fragment: DocumentNode | TypedDocumentNode; fragmentName?: string; optimistic?: boolean; diff --git a/src/react/hooks/__tests__/useLazyQuery.test.tsx b/src/react/hooks/__tests__/useLazyQuery.test.tsx index e96dc5d09b0..d02771df2a2 100644 --- a/src/react/hooks/__tests__/useLazyQuery.test.tsx +++ b/src/react/hooks/__tests__/useLazyQuery.test.tsx @@ -26,6 +26,8 @@ import { useLazyQuery } from "../useLazyQuery"; import { QueryResult } from "../../types/types"; import { profileHook } from "../../../testing/internal"; import { InvariantError } from "../../../utilities/globals"; +import { MaskedDocumentNode } from "../../../masking"; +import { expectTypeOf } from "expect-type"; describe("useLazyQuery Hook", () => { const helloQuery: TypedDocumentNode<{ @@ -1986,6 +1988,485 @@ describe("useLazyQuery Hook", () => { await expect(ProfiledHook).not.toRerender({ timeout: 50 }); expect(requests).toBe(1); }); + + describe("data masking", () => { + it("masks queries when dataMasking is `true`", async () => { + type UserFieldsFragment = { + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" }; + + interface Query { + currentUser: { + __typename: "User"; + id: number; + name: string; + } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; + } + + const query: MaskedDocumentNode = gql` + query MaskedQuery { + 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, + }, + }, + }, + delay: 10, + }, + ]; + + const client = new ApolloClient({ + dataMasking: true, + cache: new InMemoryCache(), + link: new MockLink(mocks), + }); + + const ProfiledHook = profileHook(() => useLazyQuery(query)); + + render(, { + wrapper: ({ children }) => ( + {children} + ), + }); + + // initial render + await ProfiledHook.takeSnapshot(); + + const [execute] = ProfiledHook.getCurrentSnapshot(); + const result = await execute(); + + expect(result.data).toEqual({ + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + }, + }); + + // Loading + await ProfiledHook.takeSnapshot(); + + { + const [, { data }] = await ProfiledHook.takeSnapshot(); + + expect(data).toEqual({ + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + }, + }); + } + }); + + it("does not mask queries when dataMasking is `false`", async () => { + type UserFieldsFragment = { + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" }; + + interface Query { + currentUser: { + __typename: "User"; + id: number; + name: string; + } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; + } + + const query: TypedDocumentNode = gql` + query MaskedQuery { + 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, + }, + }, + }, + delay: 10, + }, + ]; + + const client = new ApolloClient({ + dataMasking: false, + cache: new InMemoryCache(), + link: new MockLink(mocks), + }); + + const ProfiledHook = profileHook(() => useLazyQuery(query)); + + render(, { + wrapper: ({ children }) => ( + {children} + ), + }); + + // initial render + await ProfiledHook.takeSnapshot(); + + const [execute] = ProfiledHook.getCurrentSnapshot(); + const result = await execute(); + + expect(result.data).toEqual({ + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + age: 30, + }, + }); + + // Loading + await ProfiledHook.takeSnapshot(); + + { + const [, { data }] = await ProfiledHook.takeSnapshot(); + + expect(data).toEqual({ + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + age: 30, + }, + }); + } + }); + + it("does not mask queries by default", async () => { + type UserFieldsFragment = { + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" }; + + interface Query { + currentUser: { + __typename: "User"; + id: number; + name: string; + } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; + } + + const query: TypedDocumentNode = gql` + query MaskedQuery { + 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, + }, + }, + }, + delay: 10, + }, + ]; + + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: new MockLink(mocks), + }); + + const ProfiledHook = profileHook(() => useLazyQuery(query)); + + render(, { + wrapper: ({ children }) => ( + {children} + ), + }); + + // initial render + await ProfiledHook.takeSnapshot(); + + const [execute] = ProfiledHook.getCurrentSnapshot(); + const result = await execute(); + + expect(result.data).toEqual({ + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + age: 30, + }, + }); + + // Loading + await ProfiledHook.takeSnapshot(); + + { + const [, { data }] = await ProfiledHook.takeSnapshot(); + + expect(data).toEqual({ + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + age: 30, + }, + }); + } + }); + + it("masks queries updated by the cache", async () => { + type UserFieldsFragment = { + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" }; + + interface Query { + currentUser: { + __typename: "User"; + id: number; + name: string; + } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; + } + + const query: MaskedDocumentNode = gql` + query MaskedQuery { + 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, + }, + }, + }, + delay: 10, + }, + ]; + + const client = new ApolloClient({ + dataMasking: true, + cache: new InMemoryCache(), + link: new MockLink(mocks), + }); + + const ProfiledHook = profileHook(() => useLazyQuery(query)); + + render(, { + wrapper: ({ children }) => ( + {children} + ), + }); + + // initial render + await ProfiledHook.takeSnapshot(); + + const [execute] = ProfiledHook.getCurrentSnapshot(); + execute(); + + // Loading + await ProfiledHook.takeSnapshot(); + + { + const [, { data }] = await ProfiledHook.takeSnapshot(); + + expect(data).toEqual({ + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + }, + }); + } + + client.writeQuery({ + query, + data: { + currentUser: { + __typename: "User", + id: 1, + name: "Test User (updated)", + age: 35, + }, + }, + }); + + { + const [, { data, previousData }] = await ProfiledHook.takeSnapshot(); + + expect(data).toEqual({ + currentUser: { + __typename: "User", + id: 1, + name: "Test User (updated)", + }, + }); + + expect(previousData).toEqual({ + currentUser: { __typename: "User", id: 1, name: "Test User" }, + }); + } + }); + + it("does not rerender when updating field in named fragment", async () => { + type UserFieldsFragment = { + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" }; + + interface Query { + currentUser: { + __typename: "User"; + id: number; + name: string; + } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; + } + + const query: MaskedDocumentNode = gql` + query MaskedQuery { + 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), + }); + + const ProfiledHook = profileHook(() => useLazyQuery(query)); + + render(, { + wrapper: ({ children }) => ( + {children} + ), + }); + + // initial render + await ProfiledHook.takeSnapshot(); + + const [execute] = ProfiledHook.getCurrentSnapshot(); + execute(); + + // Loading + await ProfiledHook.takeSnapshot(); + + { + const [, { data }] = await ProfiledHook.takeSnapshot(); + + expect(data).toEqual({ + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + }, + }); + } + + client.writeQuery({ + query, + data: { + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + age: 35, + }, + }, + }); + + await expect(ProfiledHook).not.toRerender(); + + expect(client.readQuery({ query })).toEqual({ + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + age: 35, + }, + }); + }); + }); }); describe.skip("Type Tests", () => { @@ -2002,4 +2483,200 @@ describe.skip("Type Tests", () => { // @ts-expect-error variables?.nonExistingVariable; }); + + test("uses masked types when using masked document", async () => { + type UserFieldsFragment = { + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" }; + + interface Query { + currentUser: { + __typename: "User"; + id: number; + name: string; + } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; + } + + interface UnmaskedQuery { + currentUser: { + __typename: "User"; + id: number; + name: string; + age: number; + }; + } + + interface Subscription { + updatedUser: { + __typename: "User"; + id: number; + name: string; + } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; + } + + interface UnmaskedSubscription { + updatedUser: { + __typename: "User"; + id: number; + name: string; + age: number; + }; + } + + const query: MaskedDocumentNode = gql``; + + const [ + execute, + { data, previousData, subscribeToMore, fetchMore, refetch, updateQuery }, + ] = useLazyQuery(query, { + onCompleted(data) { + expectTypeOf(data).toEqualTypeOf(); + }, + }); + + expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(previousData).toEqualTypeOf(); + + subscribeToMore({ + document: gql`` as TypedDocumentNode, + updateQuery(queryData, { subscriptionData }) { + expectTypeOf(queryData).toEqualTypeOf(); + expectTypeOf( + subscriptionData.data + ).toEqualTypeOf(); + + return {} as UnmaskedQuery; + }, + }); + + updateQuery((previousData) => { + expectTypeOf(previousData).toEqualTypeOf(); + + return {} as UnmaskedQuery; + }); + + { + const { data } = await execute(); + + expectTypeOf(data).toEqualTypeOf(); + } + + { + const { data } = await fetchMore({ + variables: {}, + updateQuery: (queryData, { fetchMoreResult }) => { + expectTypeOf(queryData).toEqualTypeOf(); + expectTypeOf(fetchMoreResult).toEqualTypeOf(); + + return {} as UnmaskedQuery; + }, + }); + + expectTypeOf(data).toEqualTypeOf(); + } + + { + const { data } = await refetch(); + + expectTypeOf(data).toEqualTypeOf(); + } + }); + + test("uses unmasked types when using TypedDocumentNode", async () => { + type UserFieldsFragment = { + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" }; + + interface Query { + currentUser: { + __typename: "User"; + id: number; + name: string; + } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; + } + + interface UnmaskedQuery { + currentUser: { + __typename: "User"; + id: number; + name: string; + age: number; + }; + } + + interface Subscription { + updatedUser: { + __typename: "User"; + id: number; + name: string; + } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; + } + + interface UnmaskedSubscription { + updatedUser: { + __typename: "User"; + id: number; + name: string; + age: number; + }; + } + + const query: TypedDocumentNode = gql``; + + const [ + execute, + { data, previousData, fetchMore, refetch, subscribeToMore, updateQuery }, + ] = useLazyQuery(query, { + onCompleted(data) { + expectTypeOf(data).toEqualTypeOf(); + }, + }); + + expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(previousData).toEqualTypeOf(); + + subscribeToMore({ + document: gql`` as TypedDocumentNode, + updateQuery(queryData, { subscriptionData }) { + expectTypeOf(queryData).toEqualTypeOf(); + expectTypeOf( + subscriptionData.data + ).toEqualTypeOf(); + + return {} as UnmaskedQuery; + }, + }); + + updateQuery((previousData) => { + expectTypeOf(previousData).toEqualTypeOf(); + + return {} as UnmaskedQuery; + }); + + { + const { data } = await execute(); + + expectTypeOf(data).toEqualTypeOf(); + } + + { + const { data } = await fetchMore({ + variables: {}, + updateQuery: (queryData, { fetchMoreResult }) => { + expectTypeOf(queryData).toEqualTypeOf(); + expectTypeOf(fetchMoreResult).toEqualTypeOf(); + + return {} as UnmaskedQuery; + }, + }); + + expectTypeOf(data).toEqualTypeOf(); + } + + { + const { data } = await refetch(); + + expectTypeOf(data).toEqualTypeOf(); + } + }); }); diff --git a/src/react/hooks/__tests__/useMutation.test.tsx b/src/react/hooks/__tests__/useMutation.test.tsx index da26fd2c87d..d957955aaff 100644 --- a/src/react/hooks/__tests__/useMutation.test.tsx +++ b/src/react/hooks/__tests__/useMutation.test.tsx @@ -25,6 +25,7 @@ import { mockSingleLink, subscribeAndCount, MockedResponse, + MockLink, } from "../../../testing"; import { ApolloProvider } from "../../context"; import { useQuery } from "../useQuery"; @@ -32,6 +33,8 @@ import { useMutation } from "../useMutation"; import { BatchHttpLink } from "../../../link/batch-http"; import { FetchResult } from "../../../link/core"; import { profileHook, spyOnConsole } from "../../../testing/internal"; +import { expectTypeOf } from "expect-type"; +import { Masked } from "../../../masking"; describe("useMutation Hook", () => { interface Todo { @@ -2826,6 +2829,308 @@ describe("useMutation Hook", () => { }); }); +describe("data masking", () => { + test("masks data returned from useMutation when dataMasking is `true`", async () => { + interface Mutation { + updateUser: { + __typename: "User"; + id: number; + name: string; + }; + } + + const mutation: TypedDocumentNode = gql` + mutation MaskedMutation { + updateUser { + id + name + ...UserFields + } + } + + fragment UserFields on User { + age + } + `; + + const mocks = [ + { + request: { query: mutation }, + result: { + data: { + updateUser: { + __typename: "User", + id: 1, + name: "Test User", + age: 30, + }, + }, + }, + delay: 10, + }, + ]; + + const client = new ApolloClient({ + dataMasking: true, + cache: new InMemoryCache(), + link: new MockLink(mocks), + }); + + const ProfiledHook = profileHook(() => useMutation(mutation)); + + render(, { + wrapper: ({ children }) => ( + {children} + ), + }); + + const [mutate, result] = await ProfiledHook.takeSnapshot(); + + expect(result.loading).toBe(false); + expect(result.data).toBeUndefined(); + expect(result.error).toBeUndefined(); + + let promise!: Promise>; + act(() => { + promise = mutate(); + }); + + { + const [, result] = await ProfiledHook.takeSnapshot(); + + expect(result.loading).toBe(true); + expect(result.data).toBeUndefined(); + expect(result.error).toBeUndefined(); + } + + { + const [, result] = await ProfiledHook.takeSnapshot(); + + expect(result.loading).toBe(false); + expect(result.data).toEqual({ + updateUser: { + __typename: "User", + id: 1, + name: "Test User", + }, + }); + expect(result.error).toBeUndefined(); + } + + { + const { data, errors } = await promise; + + expect(data).toEqual({ + updateUser: { + __typename: "User", + id: 1, + name: "Test User", + }, + }); + expect(errors).toBeUndefined(); + } + + await expect(ProfiledHook).not.toRerender(); + }); + + test("does not mask data returned from useMutation when dataMasking is `false`", async () => { + interface Mutation { + updateUser: { + __typename: "User"; + id: number; + name: string; + }; + } + + const mutation: TypedDocumentNode = gql` + mutation MaskedMutation { + updateUser { + id + name + ...UserFields + } + } + + fragment UserFields on User { + age + } + `; + + const mocks = [ + { + request: { query: mutation }, + result: { + data: { + updateUser: { + __typename: "User", + id: 1, + name: "Test User", + age: 30, + }, + }, + }, + delay: 10, + }, + ]; + + const client = new ApolloClient({ + dataMasking: false, + cache: new InMemoryCache(), + link: new MockLink(mocks), + }); + + const ProfiledHook = profileHook(() => useMutation(mutation)); + + render(, { + wrapper: ({ children }) => ( + {children} + ), + }); + + const [mutate, result] = await ProfiledHook.takeSnapshot(); + + expect(result.loading).toBe(false); + expect(result.data).toBeUndefined(); + expect(result.error).toBeUndefined(); + + let promise!: Promise>; + act(() => { + promise = mutate(); + }); + + { + const [, result] = await ProfiledHook.takeSnapshot(); + + expect(result.loading).toBe(true); + expect(result.data).toBeUndefined(); + expect(result.error).toBeUndefined(); + } + + { + const [, result] = await ProfiledHook.takeSnapshot(); + + expect(result.loading).toBe(false); + expect(result.data).toEqual({ + updateUser: { + __typename: "User", + id: 1, + name: "Test User", + age: 30, + }, + }); + expect(result.error).toBeUndefined(); + } + + { + const { data, errors } = await promise; + + expect(data).toEqual({ + updateUser: { + __typename: "User", + id: 1, + name: "Test User", + age: 30, + }, + }); + expect(errors).toBeUndefined(); + } + + await expect(ProfiledHook).not.toRerender(); + }); + + test("passes masked data to onCompleted, does not pass masked data to update", async () => { + interface Mutation { + updateUser: { + __typename: "User"; + id: number; + name: string; + }; + } + + const mutation: TypedDocumentNode = gql` + mutation MaskedMutation { + updateUser { + id + name + ...UserFields + } + } + + fragment UserFields on User { + age + } + `; + + const mocks = [ + { + request: { query: mutation }, + result: { + data: { + updateUser: { + __typename: "User", + id: 1, + name: "Test User", + age: 30, + }, + }, + }, + delay: 10, + }, + ]; + + const cache = new InMemoryCache(); + const client = new ApolloClient({ + dataMasking: true, + cache, + link: new MockLink(mocks), + }); + + const update = jest.fn(); + const onCompleted = jest.fn(); + const ProfiledHook = profileHook(() => + useMutation(mutation, { onCompleted, update }) + ); + + render(, { + wrapper: ({ children }) => ( + {children} + ), + }); + + const [mutate] = await ProfiledHook.takeSnapshot(); + + await act(() => mutate()); + + expect(onCompleted).toHaveBeenCalledTimes(1); + expect(onCompleted).toHaveBeenCalledWith( + { + updateUser: { + __typename: "User", + id: 1, + name: "Test User", + }, + }, + expect.anything() + ); + + expect(update).toHaveBeenCalledTimes(1); + expect(update).toHaveBeenCalledWith( + cache, + { + data: { + updateUser: { + __typename: "User", + id: 1, + name: "Test User", + age: 30, + }, + }, + }, + { context: undefined, variables: {} } + ); + }); +}); + describe.skip("Type Tests", () => { test("NoInfer prevents adding arbitrary additional variables", () => { const typedNode = {} as TypedDocumentNode<{ foo: string }, { bar: number }>; @@ -2837,4 +3142,172 @@ describe.skip("Type Tests", () => { }, }); }); + + test("uses any as masked and unmasked type when using plain DocumentNode", () => { + const mutation = gql` + mutation ($id: ID!) { + updateUser(id: $id) { + id + ...UserFields + } + } + + fragment UserFields on User { + age + } + `; + + const [mutate, { data }] = useMutation(mutation, { + optimisticResponse: { foo: "foo" }, + updateQueries: { + TestQuery: (_, { mutationResult }) => { + expectTypeOf(mutationResult.data).toMatchTypeOf(); + + return {}; + }, + }, + refetchQueries(result) { + expectTypeOf(result.data).toMatchTypeOf(); + + return "active"; + }, + onCompleted(data) { + expectTypeOf(data).toMatchTypeOf(); + }, + update(_, result) { + expectTypeOf(result.data).toMatchTypeOf(); + }, + }); + + expectTypeOf(data).toMatchTypeOf(); + expectTypeOf(mutate()).toMatchTypeOf>>(); + }); + + test("uses TData type when using plain TypedDocumentNode", () => { + interface Mutation { + updateUser: { + __typename: "User"; + id: string; + age: number; + }; + } + + interface Variables { + id: string; + } + + const mutation: TypedDocumentNode = gql` + mutation ($id: ID!) { + updateUser(id: $id) { + id + ...UserFields + } + } + + fragment UserFields on User { + age + } + `; + + const [mutate, { data }] = useMutation(mutation, { + variables: { id: "1" }, + optimisticResponse: { + updateUser: { __typename: "User", id: "1", age: 30 }, + }, + updateQueries: { + TestQuery: (_, { mutationResult }) => { + expectTypeOf(mutationResult.data).toMatchTypeOf< + Mutation | null | undefined + >(); + + return {}; + }, + }, + refetchQueries(result) { + expectTypeOf(result.data).toMatchTypeOf(); + + return "active"; + }, + onCompleted(data) { + expectTypeOf(data).toMatchTypeOf(); + }, + update(_, result) { + expectTypeOf(result.data).toMatchTypeOf(); + }, + }); + + expectTypeOf(data).toMatchTypeOf(); + expectTypeOf(mutate()).toMatchTypeOf>>(); + }); + + test("uses masked/unmasked type when using Masked", async () => { + type UserFieldsFragment = { + age: number; + } & { " $fragmentName": "UserFieldsFragment" }; + + type Mutation = { + updateUser: { + __typename: "User"; + id: string; + } & { " $fragmentRefs": { UserFieldsFragment: UserFieldsFragment } }; + }; + + type UnmaskedMutation = { + updateUser: { + __typename: "User"; + id: string; + age: number; + }; + }; + + interface Variables { + id: string; + } + + const mutation: TypedDocumentNode, Variables> = gql` + mutation ($id: ID!) { + updateUser(id: $id) { + id + ...UserFields + } + } + + fragment UserFields on User { + age + } + `; + + const [mutate, { data }] = useMutation(mutation, { + optimisticResponse: { + updateUser: { __typename: "User", id: "1", age: 30 }, + }, + updateQueries: { + TestQuery: (_, { mutationResult }) => { + expectTypeOf(mutationResult.data).toMatchTypeOf< + UnmaskedMutation | null | undefined + >(); + + return {}; + }, + }, + refetchQueries(result) { + expectTypeOf(result.data).toMatchTypeOf< + UnmaskedMutation | null | undefined + >(); + + return "active"; + }, + onCompleted(data) { + expectTypeOf(data).toMatchTypeOf(); + }, + update(_, result) { + expectTypeOf(result.data).toMatchTypeOf< + UnmaskedMutation | null | undefined + >(); + }, + }); + + expectTypeOf(data).toMatchTypeOf(); + expectTypeOf(mutate()).toMatchTypeOf>>(); + }); }); diff --git a/src/react/hooks/__tests__/useQuery.test.tsx b/src/react/hooks/__tests__/useQuery.test.tsx index d521a96a0c3..64715007532 100644 --- a/src/react/hooks/__tests__/useQuery.test.tsx +++ b/src/react/hooks/__tests__/useQuery.test.tsx @@ -8,6 +8,7 @@ import { ApolloClient, ApolloError, ApolloQueryResult, + FetchPolicy, NetworkStatus, OperationVariables, TypedDocumentNode, @@ -42,6 +43,7 @@ import { useApolloClient } from "../useApolloClient"; import { useLazyQuery } from "../useLazyQuery"; import { mockFetchQuery } from "../../../core/__tests__/ObservableQuery"; import { InvariantError } from "../../../utilities/globals"; +import { Masked, MaskedDocumentNode } from "../../../masking"; const IS_REACT_17 = React.version.startsWith("17"); @@ -10208,6 +10210,923 @@ describe("useQuery Hook", () => { await expect(ProfiledHook).not.toRerender({ timeout: 200 }); }); + + describe("data masking", () => { + it("masks queries when dataMasking is `true`", async () => { + type UserFieldsFragment = { + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" }; + + interface Query { + currentUser: { + __typename: "User"; + id: number; + name: string; + } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; + } + + const query: MaskedDocumentNode = gql` + query MaskedQuery { + 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), + }); + + const Profiler = createProfiler({ + initialSnapshot: { + result: null as QueryResult, never> | null, + }, + }); + + function App() { + const result = useQuery(query); + + Profiler.replaceSnapshot({ result }); + + return null; + } + + render(, { + wrapper: ({ children }) => ( + + {children} + + ), + }); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result?.loading).toBe(true); + } + + { + const { snapshot } = await Profiler.takeRender(); + const { result } = snapshot; + + expect(result?.loading).toBe(false); + expect(result?.data).toEqual({ + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + }, + }); + expect(result?.previousData).toBeUndefined(); + } + }); + + it("does not mask query when dataMasking is `false`", async () => { + type UserFieldsFragment = { + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" }; + + interface Query { + currentUser: { + __typename: "User"; + id: number; + name: string; + } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; + } + + const query: TypedDocumentNode = gql` + query MaskedQuery { + 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: false, + cache: new InMemoryCache(), + link: new MockLink(mocks), + }); + + const Profiler = createProfiler({ + initialSnapshot: { + result: null as QueryResult | null, + }, + }); + + function App() { + const result = useQuery(query); + + Profiler.replaceSnapshot({ result }); + + return null; + } + + render(, { + wrapper: ({ children }) => ( + + {children} + + ), + }); + + // loading + await Profiler.takeRender(); + + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result?.data).toEqual({ + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + age: 30, + }, + }); + expect(snapshot.result?.previousData).toBeUndefined(); + }); + + it("does not mask query by default", async () => { + type UserFieldsFragment = { + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" }; + + interface Query { + currentUser: { + __typename: "User"; + id: number; + name: string; + } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; + } + + const query: TypedDocumentNode = gql` + query MaskedQuery { + 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({ + cache: new InMemoryCache(), + link: new MockLink(mocks), + }); + + const Profiler = createProfiler({ + initialSnapshot: { + result: null as QueryResult | null, + }, + }); + + function App() { + const result = useQuery(query); + + Profiler.replaceSnapshot({ result }); + + return null; + } + + render(, { + wrapper: ({ children }) => ( + + {children} + + ), + }); + + // loading + await Profiler.takeRender(); + + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result?.data).toEqual({ + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + age: 30, + }, + }); + expect(snapshot.result?.previousData).toBeUndefined(); + }); + + it("masks queries updated by the cache", async () => { + type UserFieldsFragment = { + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" }; + + interface Query { + currentUser: { + __typename: "User"; + id: number; + name: string; + } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; + } + + const query: MaskedDocumentNode = gql` + query MaskedQuery { + 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), + }); + + const Profiler = createProfiler({ + initialSnapshot: { + result: null as QueryResult, never> | null, + }, + }); + + function App() { + const result = useQuery(query); + + Profiler.replaceSnapshot({ result }); + + return null; + } + + render(, { + wrapper: ({ children }) => ( + + {children} + + ), + }); + + // loading + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result?.data).toEqual({ + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + }, + }); + expect(snapshot.result?.previousData).toBeUndefined(); + } + + client.writeQuery({ + query, + data: { + currentUser: { + __typename: "User", + id: 1, + name: "Test User (updated)", + age: 35, + }, + }, + }); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result?.data).toEqual({ + currentUser: { + __typename: "User", + id: 1, + name: "Test User (updated)", + }, + }); + expect(snapshot.result?.previousData).toEqual({ + currentUser: { __typename: "User", id: 1, name: "Test User" }, + }); + } + }); + + it("does not rerender when updating field in named fragment", async () => { + type UserFieldsFragment = { + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" }; + + interface Query { + currentUser: { + __typename: "User"; + id: number; + name: string; + } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; + } + + const query: MaskedDocumentNode = gql` + query MaskedQuery { + 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), + }); + + const Profiler = createProfiler({ + initialSnapshot: { + result: null as QueryResult, never> | null, + }, + }); + + function App() { + const result = useQuery(query); + + Profiler.replaceSnapshot({ result }); + + return null; + } + + render(, { + wrapper: ({ children }) => ( + + {children} + + ), + }); + + // loading + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result?.data).toEqual({ + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + }, + }); + expect(snapshot.result?.previousData).toBeUndefined(); + } + + client.writeQuery({ + query, + data: { + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + age: 35, + }, + }, + }); + + await expect(Profiler).not.toRerender(); + + expect(client.readQuery({ query })).toEqual({ + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + age: 35, + }, + }); + }); + + it.each(["cache-first", "cache-only"] as FetchPolicy[])( + "masks result from cache when using with %s fetch policy", + async (fetchPolicy) => { + type UserFieldsFragment = { + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" }; + + interface Query { + currentUser: { + __typename: "User"; + id: number; + name: string; + } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; + } + + const query: MaskedDocumentNode = gql` + query MaskedQuery { + 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), + }); + + client.writeQuery({ + query, + data: { + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + age: 30, + }, + }, + }); + + const Profiler = createProfiler({ + initialSnapshot: { + result: null as QueryResult, never> | null, + }, + }); + + function App() { + const result = useQuery(query, { fetchPolicy }); + + Profiler.replaceSnapshot({ result }); + + return null; + } + + render(, { + wrapper: ({ children }) => ( + + {children} + + ), + }); + + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result?.data).toEqual({ + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + }, + }); + expect(snapshot.result?.previousData).toBeUndefined(); + } + ); + + it("masks cache and network result when using cache-and-network fetch policy", async () => { + type UserFieldsFragment = { + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" }; + + interface Query { + currentUser: { + __typename: "User"; + id: number; + name: string; + } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; + } + + const query: MaskedDocumentNode = gql` + query MaskedQuery { + currentUser { + id + name + ...UserFields + } + } + + fragment UserFields on User { + age + } + `; + + const mocks = [ + { + request: { query }, + result: { + data: { + currentUser: { + __typename: "User", + id: 1, + name: "Test User (server)", + age: 35, + }, + }, + }, + delay: 20, + }, + ]; + + const client = new ApolloClient({ + dataMasking: true, + cache: new InMemoryCache(), + link: new MockLink(mocks), + }); + + client.writeQuery({ + query, + data: { + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + age: 34, + }, + }, + }); + + const Profiler = createProfiler({ + initialSnapshot: { + result: null as QueryResult, never> | null, + }, + }); + + function App() { + const result = useQuery(query, { fetchPolicy: "cache-and-network" }); + + Profiler.replaceSnapshot({ result }); + + return null; + } + + render(, { + wrapper: ({ children }) => ( + + {children} + + ), + }); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result?.data).toEqual({ + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + }, + }); + expect(snapshot.result?.previousData).toBeUndefined(); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result?.data).toEqual({ + currentUser: { + __typename: "User", + id: 1, + name: "Test User (server)", + }, + }); + expect(snapshot.result?.previousData).toEqual({ + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + }, + }); + } + }); + + it("masks partial cache data when returnPartialData is `true`", async () => { + type UserFieldsFragment = { + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" }; + + interface Query { + currentUser: { + __typename: "User"; + id: number; + name: string; + } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; + } + + const query: MaskedDocumentNode = gql` + query MaskedQuery { + currentUser { + id + name + ...UserFields + } + } + + fragment UserFields on User { + age + } + `; + + const mocks = [ + { + request: { query }, + result: { + data: { + currentUser: { + __typename: "User", + id: 1, + name: "Test User (server)", + age: 35, + }, + }, + }, + delay: 20, + }, + ]; + + const client = new ApolloClient({ + dataMasking: true, + cache: new InMemoryCache(), + link: new MockLink(mocks), + }); + + { + using _ = spyOnConsole("error"); + + client.writeQuery({ + query, + data: { + // @ts-expect-error writing partial result + currentUser: { + __typename: "User", + id: 1, + age: 34, + }, + }, + }); + } + + const Profiler = createProfiler({ + initialSnapshot: { + result: null as QueryResult, never> | null, + }, + }); + + function App() { + const result = useQuery(query, { returnPartialData: true }); + + Profiler.replaceSnapshot({ result }); + + return null; + } + + render(, { + wrapper: ({ children }) => ( + + {children} + + ), + }); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result?.data).toEqual({ + currentUser: { + __typename: "User", + id: 1, + }, + }); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result?.data).toEqual({ + currentUser: { + __typename: "User", + id: 1, + name: "Test User (server)", + }, + }); + } + }); + + it("masks partial data returned from data on errors with errorPolicy `all`", async () => { + type UserFieldsFragment = { + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" }; + + interface Query { + currentUser: { + __typename: "User"; + id: number; + name: string; + } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; + } + + const query: MaskedDocumentNode = gql` + query MaskedQuery { + currentUser { + id + name + ...UserFields + } + } + + fragment UserFields on User { + age + } + `; + + const mocks = [ + { + request: { query }, + result: { + data: { + currentUser: { + __typename: "User", + id: 1, + name: null, + age: 34, + }, + }, + errors: [new GraphQLError("Couldn't get name")], + }, + delay: 20, + }, + ]; + + const client = new ApolloClient({ + dataMasking: true, + cache: new InMemoryCache(), + link: new MockLink(mocks), + }); + + const Profiler = createProfiler({ + initialSnapshot: { + result: null as QueryResult, never> | null, + }, + }); + + function App() { + const result = useQuery(query, { errorPolicy: "all" }); + + Profiler.replaceSnapshot({ result }); + + return null; + } + + render(, { + wrapper: ({ children }) => ( + + {children} + + ), + }); + + // loading + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + const { result } = snapshot; + + expect(result?.data).toEqual({ + currentUser: { + __typename: "User", + id: 1, + name: null, + }, + }); + + expect(result?.error).toEqual( + new ApolloError({ + graphQLErrors: [new GraphQLError("Couldn't get name")], + }) + ); + } + }); + }); }); describe.skip("Type Tests", () => { diff --git a/src/react/hooks/__tests__/useSubscription.test.tsx b/src/react/hooks/__tests__/useSubscription.test.tsx index 0c9002638d1..fd6aaf18bab 100644 --- a/src/react/hooks/__tests__/useSubscription.test.tsx +++ b/src/react/hooks/__tests__/useSubscription.test.tsx @@ -20,6 +20,8 @@ import { ErrorBoundary } from "react-error-boundary"; import { MockedSubscriptionResult } from "../../../testing/core/mocking/mockSubscriptionLink"; import { GraphQLError } from "graphql"; import { InvariantError } from "ts-invariant"; +import { MaskedDocumentNode } from "../../../masking"; +import { expectTypeOf } from "expect-type"; describe("useSubscription Hook", () => { it("should handle a simple subscription properly", async () => { @@ -2060,6 +2062,315 @@ describe("ignoreResults", () => { }); }); +describe("data masking", () => { + test("masks data returned when dataMasking is `true`", async () => { + const subscription = gql` + subscription NewCommentSubscription { + addedComment { + id + ...CommentFields + } + } + + fragment CommentFields on Comment { + comment + author + } + `; + + const link = new MockSubscriptionLink(); + const client = new ApolloClient({ + dataMasking: true, + cache: new Cache(), + link, + }); + + const ProfiledHook = profileHook(() => useSubscription(subscription)); + + render(, { + wrapper: ({ children }) => ( + {children} + ), + }); + + { + const { data, loading, error } = await ProfiledHook.takeSnapshot(); + + expect(loading).toBe(true); + expect(data).toBeUndefined(); + expect(error).toBeUndefined(); + } + + link.simulateResult({ + result: { + data: { + addedComment: { + __typename: "Comment", + id: 1, + comment: "Test comment", + author: "Test User", + }, + }, + }, + }); + + { + const { data, loading, error } = await ProfiledHook.takeSnapshot(); + + expect(loading).toBe(false); + expect(data).toEqual({ + addedComment: { + __typename: "Comment", + id: 1, + }, + }); + expect(error).toBeUndefined(); + } + + await expect(ProfiledHook).not.toRerender(); + }); + + test("does not mask data returned from subscriptions when dataMasking is `false`", async () => { + const subscription = gql` + subscription NewCommentSubscription { + addedComment { + id + ...CommentFields + } + } + + fragment CommentFields on Comment { + comment + author + } + `; + + const link = new MockSubscriptionLink(); + const client = new ApolloClient({ + dataMasking: false, + cache: new Cache(), + link, + }); + + const ProfiledHook = profileHook(() => useSubscription(subscription)); + + render(, { + wrapper: ({ children }) => ( + {children} + ), + }); + + { + const { data, loading, error } = await ProfiledHook.takeSnapshot(); + + expect(loading).toBe(true); + expect(data).toBeUndefined(); + expect(error).toBeUndefined(); + } + + link.simulateResult({ + result: { + data: { + addedComment: { + __typename: "Comment", + id: 1, + comment: "Test comment", + author: "Test User", + }, + }, + }, + }); + + { + const { data, loading, error } = await ProfiledHook.takeSnapshot(); + + expect(loading).toBe(false); + expect(data).toEqual({ + addedComment: { + __typename: "Comment", + id: 1, + comment: "Test comment", + author: "Test User", + }, + }); + expect(error).toBeUndefined(); + } + + await expect(ProfiledHook).not.toRerender(); + }); + + test("masks data passed to onData callback when dataMasking is `true`", async () => { + const subscription = gql` + subscription NewCommentSubscription { + addedComment { + id + ...CommentFields + } + } + + fragment CommentFields on Comment { + comment + author + } + `; + + const link = new MockSubscriptionLink(); + const client = new ApolloClient({ + dataMasking: true, + cache: new Cache(), + link, + }); + + const onData = jest.fn(); + const ProfiledHook = profileHook(() => + useSubscription(subscription, { onData }) + ); + + render(, { + wrapper: ({ children }) => ( + {children} + ), + }); + + { + const { data, loading, error } = await ProfiledHook.takeSnapshot(); + + expect(loading).toBe(true); + expect(data).toBeUndefined(); + expect(error).toBeUndefined(); + } + + link.simulateResult({ + result: { + data: { + addedComment: { + __typename: "Comment", + id: 1, + comment: "Test comment", + author: "Test User", + }, + }, + }, + }); + + { + const { data, loading, error } = await ProfiledHook.takeSnapshot(); + + expect(loading).toBe(false); + expect(data).toEqual({ + addedComment: { + __typename: "Comment", + id: 1, + }, + }); + expect(error).toBeUndefined(); + + expect(onData).toHaveBeenCalledTimes(1); + expect(onData).toHaveBeenCalledWith({ + client: expect.anything(), + data: { + data: { addedComment: { __typename: "Comment", id: 1 } }, + loading: false, + error: undefined, + variables: undefined, + }, + }); + } + + await expect(ProfiledHook).not.toRerender(); + }); + + test("uses unmasked data when using the @unmask directive", async () => { + const subscription = gql` + subscription NewCommentSubscription { + addedComment { + id + ...CommentFields @unmask + } + } + + fragment CommentFields on Comment { + comment + author + } + `; + + const link = new MockSubscriptionLink(); + const client = new ApolloClient({ + dataMasking: true, + cache: new Cache(), + link, + }); + + const onData = jest.fn(); + const ProfiledHook = profileHook(() => + useSubscription(subscription, { onData }) + ); + + render(, { + wrapper: ({ children }) => ( + {children} + ), + }); + + { + const { data, loading, error } = await ProfiledHook.takeSnapshot(); + + expect(loading).toBe(true); + expect(data).toBeUndefined(); + expect(error).toBeUndefined(); + } + + link.simulateResult({ + result: { + data: { + addedComment: { + __typename: "Comment", + id: 1, + comment: "Test comment", + author: "Test User", + }, + }, + }, + }); + + { + const { data, loading, error } = await ProfiledHook.takeSnapshot(); + + expect(loading).toBe(false); + expect(data).toEqual({ + addedComment: { + __typename: "Comment", + id: 1, + comment: "Test comment", + author: "Test User", + }, + }); + expect(error).toBeUndefined(); + + expect(onData).toHaveBeenCalledTimes(1); + expect(onData).toHaveBeenCalledWith({ + client: expect.anything(), + data: { + data: { + addedComment: { + __typename: "Comment", + id: 1, + comment: "Test comment", + author: "Test User", + }, + }, + loading: false, + error: undefined, + variables: undefined, + }, + }); + } + + await expect(ProfiledHook).not.toRerender(); + }); +}); + describe.skip("Type Tests", () => { test("NoInfer prevents adding arbitrary additional variables", () => { const typedNode = {} as TypedDocumentNode<{ foo: string }, { bar: number }>; @@ -2074,4 +2385,73 @@ describe.skip("Type Tests", () => { // @ts-expect-error variables?.nonExistingVariable; }); + + test("uses masked types when using masked document", async () => { + type UserFieldsFragment = { + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" }; + + interface Subscription { + userUpdated: { + __typename: "User"; + id: string; + name: string; + } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; + } + + const subscription: MaskedDocumentNode = gql``; + + const { data } = useSubscription(subscription, { + onData: ({ data }) => { + expectTypeOf(data.data).toEqualTypeOf(); + }, + onSubscriptionData: ({ subscriptionData }) => { + expectTypeOf(subscriptionData.data).toEqualTypeOf< + Subscription | undefined + >(); + }, + }); + + expectTypeOf(data).toEqualTypeOf(); + }); + + test("uses unmasked types when using TypedDocumentNode", async () => { + type UserFieldsFragment = { + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" }; + + interface Subscription { + userUpdated: { + __typename: "User"; + id: string; + name: string; + } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; + } + + interface UnmaskedSubscription { + userUpdated: { + __typename: "User"; + id: string; + name: string; + age: number; + }; + } + + const subscription: TypedDocumentNode = gql``; + + const { data } = useSubscription(subscription, { + onData: ({ data }) => { + expectTypeOf(data.data).toEqualTypeOf< + UnmaskedSubscription | undefined + >(); + }, + onSubscriptionData: ({ subscriptionData }) => { + expectTypeOf(subscriptionData.data).toEqualTypeOf< + UnmaskedSubscription | undefined + >(); + }, + }); + + expectTypeOf(data).toEqualTypeOf(); + }); }); diff --git a/src/react/hooks/__tests__/useSuspenseQuery.test.tsx b/src/react/hooks/__tests__/useSuspenseQuery.test.tsx index 31855bf83a4..4c7960bae4c 100644 --- a/src/react/hooks/__tests__/useSuspenseQuery.test.tsx +++ b/src/react/hooks/__tests__/useSuspenseQuery.test.tsx @@ -60,6 +60,7 @@ import { spyOnConsole, useTrackRenders, } from "../../../testing/internal"; +import { Masked, MaskedDocumentNode, Unmasked } from "../../../masking"; type RenderSuspenseHookOptions = Omit< RenderHookOptions, @@ -221,7 +222,7 @@ function useErrorCase( networkError, graphQLErrors, }: { - data?: TData; + data?: Unmasked; networkError?: Error; graphQLErrors?: GraphQLError[]; } = Object.create(null) @@ -280,6 +281,61 @@ function useVariablesQueryCase() { return { query, mocks }; } +type CharacterFragment = { + name: string; +} & { " $fragmentName"?: "CharacterFragment" }; + +interface MaskedVariablesCaseData { + character: { + id: string; + } & { " $fragmentRefs"?: { CharacterFragment: CharacterFragment } }; +} + +interface UnmaskedVariablesCaseData { + character: { + id: string; + name: string; + }; +} + +function useMaskedVariablesQueryCase() { + const CHARACTERS = ["Spider-Man", "Black Widow", "Iron Man", "Hulk"]; + + const document = gql` + query CharacterQuery($id: ID!) { + character(id: $id) { + id + ...CharacterFragment + } + } + + fragment CharacterFragment on Character { + name + } + `; + + const query: MaskedDocumentNode< + MaskedVariablesCaseData, + VariablesCaseVariables + > = document; + + const unmaskedQuery: TypedDocumentNode< + MaskedVariablesCaseData, + VariablesCaseVariables + > = document; + + const mocks = CHARACTERS.map((name, index) => ({ + request: { query, variables: { id: String(index + 1) } }, + result: { + data: { + character: { __typename: "Character", id: String(index + 1), name }, + }, + }, + })); + + return { query, unmaskedQuery, mocks }; +} + function wait(delay: number) { return new Promise((resolve) => setTimeout(resolve, delay)); } @@ -10571,157 +10627,1287 @@ describe("useSuspenseQuery", () => { await expect(Profiler).not.toRerender(); }); - describe.skip("type tests", () => { - it("returns unknown when TData cannot be inferred", () => { - const query = gql` - query { - hello - } - `; + it("masks queries when dataMasking is `true`", async () => { + type UserFieldsFragment = { + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" }; - const { data } = useSuspenseQuery(query); + interface Query { + currentUser: { + __typename: "User"; + id: number; + name: string; + } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; + } - expectTypeOf(data).toEqualTypeOf(); - }); + const query: MaskedDocumentNode = gql` + query MaskedQuery { + currentUser { + id + name + ...UserFields + } + } - it("disallows wider variables type than specified", () => { - const { query } = useVariablesQueryCase(); + fragment UserFields on User { + age + } + `; - // @ts-expect-error should not allow wider TVariables type - useSuspenseQuery(query, { variables: { id: "1", foo: "bar" } }); + 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), }); - it("returns TData in default case", () => { - const { query } = useVariablesQueryCase(); + const Profiler = createProfiler({ + initialSnapshot: { + result: null as UseSuspenseQueryResult, never> | null, + }, + }); - const { data: inferred } = useSuspenseQuery(query); + function App() { + const result = useSuspenseQuery(query); - expectTypeOf(inferred).toEqualTypeOf(); - expectTypeOf(inferred).not.toEqualTypeOf(); + Profiler.replaceSnapshot({ result }); - const { data: explicit } = useSuspenseQuery< - VariablesCaseData, - VariablesCaseVariables - >(query); + return null; + } - expectTypeOf(explicit).toEqualTypeOf(); - expectTypeOf(explicit).not.toEqualTypeOf(); + render(, { + wrapper: ({ children }) => ( + + + Loading...}>{children} + + + ), }); - it('returns TData | undefined with errorPolicy: "ignore"', () => { - const { query } = useVariablesQueryCase(); - - const { data: inferred } = useSuspenseQuery(query, { - errorPolicy: "ignore", - }); + // loading + await Profiler.takeRender(); - expectTypeOf(inferred).toEqualTypeOf(); - expectTypeOf(inferred).not.toEqualTypeOf(); + { + const { snapshot } = await Profiler.takeRender(); + const { result } = snapshot; - const { data: explicit } = useSuspenseQuery< - VariablesCaseData, - VariablesCaseVariables - >(query, { - errorPolicy: "ignore", + expect(result?.data).toEqual({ + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + }, }); + } + }); - expectTypeOf(explicit).toEqualTypeOf(); - expectTypeOf(explicit).not.toEqualTypeOf(); - }); + it("does not mask query when dataMasking is `false`", async () => { + type UserFieldsFragment = { + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" }; - it('returns TData | undefined with errorPolicy: "all"', () => { - const { query } = useVariablesQueryCase(); + interface Query { + currentUser: { + __typename: "User"; + id: number; + name: string; + } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; + } - const { data: inferred } = useSuspenseQuery(query, { - errorPolicy: "all", - }); + const query: TypedDocumentNode = gql` + query MaskedQuery { + currentUser { + id + name + ...UserFields + } + } - expectTypeOf(inferred).toEqualTypeOf(); - expectTypeOf(inferred).not.toEqualTypeOf(); + fragment UserFields on User { + age + } + `; - const { data: explicit } = useSuspenseQuery< - VariablesCaseData, - VariablesCaseVariables - >(query, { - errorPolicy: "all", - }); + const mocks = [ + { + request: { query }, + result: { + data: { + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + age: 30, + }, + }, + }, + }, + ]; - expectTypeOf(explicit).toEqualTypeOf(); - expectTypeOf(explicit).not.toEqualTypeOf(); + const client = new ApolloClient({ + dataMasking: false, + cache: new InMemoryCache(), + link: new MockLink(mocks), }); - it('returns TData with errorPolicy: "none"', () => { - const { query } = useVariablesQueryCase(); + const Profiler = createProfiler({ + initialSnapshot: { + result: null as UseSuspenseQueryResult | null, + }, + }); - const { data: inferred } = useSuspenseQuery(query, { - errorPolicy: "none", - }); + function App() { + const result = useSuspenseQuery(query); - expectTypeOf(inferred).toEqualTypeOf(); - expectTypeOf(inferred).not.toEqualTypeOf(); + Profiler.replaceSnapshot({ result }); - const { data: explicit } = useSuspenseQuery< - VariablesCaseData, - VariablesCaseVariables - >(query, { - errorPolicy: "none", - }); + return null; + } - expectTypeOf(explicit).toEqualTypeOf(); - expectTypeOf(explicit).not.toEqualTypeOf(); + render(, { + wrapper: ({ children }) => ( + + + {children} + + + ), }); - it("returns DeepPartial with returnPartialData: true", () => { - const { query } = useVariablesQueryCase(); + // loading + await Profiler.takeRender(); - const { data: inferred } = useSuspenseQuery(query, { - returnPartialData: true, - }); + const { snapshot } = await Profiler.takeRender(); - expectTypeOf(inferred).toEqualTypeOf>(); - expectTypeOf(inferred).not.toEqualTypeOf(); + expect(snapshot.result?.data).toEqual({ + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + age: 30, + }, + }); + }); - const { data: explicit } = useSuspenseQuery< - VariablesCaseData, - VariablesCaseVariables - >(query, { - returnPartialData: true, - }); + it("does not mask query by default", async () => { + type UserFieldsFragment = { + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" }; - expectTypeOf(explicit).toEqualTypeOf>(); - expectTypeOf(explicit).not.toEqualTypeOf(); - }); + interface Query { + currentUser: { + __typename: "User"; + id: number; + name: string; + } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; + } - it("returns TData with returnPartialData: false", () => { - const { query } = useVariablesQueryCase(); + const query: TypedDocumentNode = gql` + query MaskedQuery { + currentUser { + id + name + ...UserFields + } + } - const { data: inferred } = useSuspenseQuery(query, { - returnPartialData: false, - }); + fragment UserFields on User { + age + } + `; - expectTypeOf(inferred).toEqualTypeOf(); - expectTypeOf(inferred).not.toEqualTypeOf< - DeepPartial - >(); + const mocks = [ + { + request: { query }, + result: { + data: { + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + age: 30, + }, + }, + }, + }, + ]; - const { data: explicit } = useSuspenseQuery< - VariablesCaseData, - VariablesCaseVariables - >(query, { - returnPartialData: false, - }); + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: new MockLink(mocks), + }); - expectTypeOf(explicit).toEqualTypeOf(); - expectTypeOf(explicit).not.toEqualTypeOf< - DeepPartial - >(); + const Profiler = createProfiler({ + initialSnapshot: { + result: null as UseSuspenseQueryResult | null, + }, }); - it("returns TData | undefined when skip is present", () => { - const { query } = useVariablesQueryCase(); + function App() { + const result = useSuspenseQuery(query); - const { data: inferred } = useSuspenseQuery(query, { - skip: true, + Profiler.replaceSnapshot({ result }); + + return null; + } + + render(, { + wrapper: ({ children }) => ( + + + {children} + + + ), + }); + + // loading + await Profiler.takeRender(); + + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result?.data).toEqual({ + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + age: 30, + }, + }); + }); + + it("masks queries updated by the cache", async () => { + type UserFieldsFragment = { + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" }; + + interface Query { + currentUser: { + __typename: "User"; + id: number; + name: string; + } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; + } + + const query: MaskedDocumentNode = gql` + query MaskedQuery { + 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), + }); + + const Profiler = createProfiler({ + initialSnapshot: { + result: null as UseSuspenseQueryResult, never> | null, + }, + }); + + function App() { + const result = useSuspenseQuery(query); + + Profiler.replaceSnapshot({ result }); + + return null; + } + + render(, { + wrapper: ({ children }) => ( + + + {children} + + + ), + }); + + // loading + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result?.data).toEqual({ + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + }, + }); + } + + setTimeout(() => { + client.writeQuery({ + query, + data: { + currentUser: { + __typename: "User", + id: 1, + name: "Test User (updated)", + // @ts-ignore TODO: Determine how to handle cache writes with masked + // query type + age: 35, + }, + }, + }); + }); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result?.data).toEqual({ + currentUser: { + __typename: "User", + id: 1, + name: "Test User (updated)", + }, + }); + } + }); + + it("does not rerender when updating field in named fragment", async () => { + type UserFieldsFragment = { + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" }; + + interface Query { + currentUser: { + __typename: "User"; + id: number; + name: string; + } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; + } + + const query: MaskedDocumentNode = gql` + query MaskedQuery { + 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), + }); + + const Profiler = createProfiler({ + initialSnapshot: { + result: null as UseSuspenseQueryResult, never> | null, + }, + }); + + function App() { + const result = useSuspenseQuery(query); + + Profiler.replaceSnapshot({ result }); + + return null; + } + + render(, { + wrapper: ({ children }) => ( + + + {children} + + + ), + }); + + // loading + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result?.data).toEqual({ + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + }, + }); + } + + client.writeQuery({ + query, + data: { + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + // @ts-ignore TODO: Determine how to handle cache writes with masked + // query type + age: 35, + }, + }, + }); + + await expect(Profiler).not.toRerender(); + + expect(client.readQuery({ query })).toEqual({ + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + age: 35, + }, + }); + }); + + it("masks result from cache when using with cache-first fetch policy", async () => { + type UserFieldsFragment = { + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" }; + + interface Query { + currentUser: { + __typename: "User"; + id: number; + name: string; + } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; + } + + const query: MaskedDocumentNode = gql` + query MaskedQuery { + 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), + }); + + client.writeQuery({ + query, + data: { + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + age: 30, + }, + }, + }); + + const Profiler = createProfiler({ + initialSnapshot: { + result: null as UseSuspenseQueryResult, never> | null, + }, + }); + + function App() { + const result = useSuspenseQuery(query, { fetchPolicy: "cache-first" }); + + Profiler.replaceSnapshot({ result }); + + return null; + } + + render(, { + wrapper: ({ children }) => ( + + + {children} + + + ), + }); + + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result?.data).toEqual({ + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + }, + }); + }); + + it("masks cache and network result when using cache-and-network fetch policy", async () => { + type UserFieldsFragment = { + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" }; + + interface Query { + currentUser: { + __typename: "User"; + id: number; + name: string; + } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; + } + + const query: MaskedDocumentNode = gql` + query MaskedQuery { + currentUser { + id + name + ...UserFields + } + } + + fragment UserFields on User { + age + } + `; + + const mocks = [ + { + request: { query }, + result: { + data: { + currentUser: { + __typename: "User", + id: 1, + name: "Test User (server)", + age: 35, + }, + }, + }, + delay: 20, + }, + ]; + + const client = new ApolloClient({ + dataMasking: true, + cache: new InMemoryCache(), + link: new MockLink(mocks), + }); + + client.writeQuery({ + query, + data: { + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + age: 34, + }, + }, + }); + + const Profiler = createProfiler({ + initialSnapshot: { + result: null as UseSuspenseQueryResult, never> | null, + }, + }); + + function App() { + const result = useSuspenseQuery(query, { + fetchPolicy: "cache-and-network", + }); + + Profiler.replaceSnapshot({ result }); + + return null; + } + + render(, { + wrapper: ({ children }) => ( + + + {children} + + + ), + }); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result?.data).toEqual({ + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + }, + }); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result?.data).toEqual({ + currentUser: { + __typename: "User", + id: 1, + name: "Test User (server)", + }, + }); + } + }); + + it("masks partial cache data when returnPartialData is `true`", async () => { + type UserFieldsFragment = { + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" }; + + interface Query { + currentUser: { + __typename: "User"; + id: number; + name: string; + } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; + } + + const query: MaskedDocumentNode = gql` + query MaskedQuery { + currentUser { + id + name + ...UserFields + } + } + + fragment UserFields on User { + age + } + `; + + const mocks = [ + { + request: { query }, + result: { + data: { + currentUser: { + __typename: "User", + id: 1, + name: "Test User (server)", + age: 35, + }, + }, + }, + delay: 20, + }, + ]; + + const client = new ApolloClient({ + dataMasking: true, + cache: new InMemoryCache(), + link: new MockLink(mocks), + }); + + { + using _ = spyOnConsole("error"); + client.writeQuery({ + query, + data: { + // @ts-expect-error writing partial cache data + currentUser: { + __typename: "User", + id: 1, + age: 34, + }, + }, + }); + } + + const Profiler = createProfiler({ + initialSnapshot: { + result: null as UseSuspenseQueryResult< + DeepPartial>, + never + > | null, + }, + }); + + function App() { + const result = useSuspenseQuery(query, { returnPartialData: true }); + + Profiler.replaceSnapshot({ result }); + + return null; + } + + render(, { + wrapper: ({ children }) => ( + + + {children} + + + ), + }); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result?.data).toEqual({ + currentUser: { + __typename: "User", + id: 1, + }, + }); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result?.data).toEqual({ + currentUser: { + __typename: "User", + id: 1, + name: "Test User (server)", + }, + }); + } + }); + + it("masks partial data returned from data on errors with errorPolicy `all`", async () => { + type UserFieldsFragment = { + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" }; + + interface Query { + currentUser: { + __typename: "User"; + id: number; + name: string; + } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; + } + + const query: MaskedDocumentNode = gql` + query MaskedQuery { + currentUser { + id + name + ...UserFields + } + } + + fragment UserFields on User { + age + } + `; + + const mocks = [ + { + request: { query }, + result: { + data: { + currentUser: { + __typename: "User", + id: 1, + name: null, + age: 34, + }, + }, + errors: [new GraphQLError("Couldn't get name")], + }, + delay: 20, + }, + ]; + + const client = new ApolloClient({ + dataMasking: true, + cache: new InMemoryCache(), + link: new MockLink(mocks), + }); + + const Profiler = createProfiler({ + initialSnapshot: { + result: null as UseSuspenseQueryResult< + Masked | undefined, + never + > | null, + }, + }); + + function App() { + const result = useSuspenseQuery(query, { errorPolicy: "all" }); + + Profiler.replaceSnapshot({ result }); + + return null; + } + + render(, { + wrapper: ({ children }) => ( + + + {children} + + + ), + }); + + // loading + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + const { result } = snapshot; + + expect(result?.data).toEqual({ + currentUser: { + __typename: "User", + id: 1, + name: null, + }, + }); + + expect(result?.error).toEqual( + new ApolloError({ + graphQLErrors: [new GraphQLError("Couldn't get name")], + }) + ); + } + }); + + describe.skip("type tests", () => { + it("returns unknown when TData cannot be inferred", () => { + const query = gql` + query { + hello + } + `; + + const { data } = useSuspenseQuery(query); + + expectTypeOf(data).toEqualTypeOf(); + }); + + it("disallows wider variables type than specified", () => { + const { query } = useVariablesQueryCase(); + + // @ts-expect-error should not allow wider TVariables type + useSuspenseQuery(query, { variables: { id: "1", foo: "bar" } }); + }); + + it("returns TData in default case", () => { + const { query } = useVariablesQueryCase(); + + const { data: inferred } = useSuspenseQuery(query); + + expectTypeOf(inferred).toEqualTypeOf(); + expectTypeOf(inferred).not.toEqualTypeOf(); + + const { data: explicit } = useSuspenseQuery< + VariablesCaseData, + VariablesCaseVariables + >(query); + + expectTypeOf(explicit).toEqualTypeOf(); + expectTypeOf(explicit).not.toEqualTypeOf(); + + const { query: maskedQuery } = useMaskedVariablesQueryCase(); + + { + const { data } = useSuspenseQuery(maskedQuery); + + expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).not.toEqualTypeOf(); + } + + { + const { data } = useSuspenseQuery< + MaskedVariablesCaseData, + VariablesCaseVariables + >(maskedQuery); + + expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).not.toEqualTypeOf(); + } + + { + const { data } = useSuspenseQuery< + Masked, + VariablesCaseVariables + >(maskedQuery); + + expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).not.toEqualTypeOf(); + } + }); + + it('returns TData | undefined with errorPolicy: "ignore"', () => { + const { query } = useVariablesQueryCase(); + + const { data: inferred } = useSuspenseQuery(query, { + errorPolicy: "ignore", + }); + + expectTypeOf(inferred).toEqualTypeOf(); + expectTypeOf(inferred).not.toEqualTypeOf(); + + const { data: explicit } = useSuspenseQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { + errorPolicy: "ignore", + }); + + expectTypeOf(explicit).toEqualTypeOf(); + expectTypeOf(explicit).not.toEqualTypeOf(); + + const { query: maskedQuery } = useMaskedVariablesQueryCase(); + + { + const { data } = useSuspenseQuery(maskedQuery, { + errorPolicy: "ignore", + }); + + expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).not.toEqualTypeOf< + UnmaskedVariablesCaseData | undefined + >(); + } + + { + const { data } = useSuspenseQuery< + MaskedVariablesCaseData, + VariablesCaseVariables + >(maskedQuery, { errorPolicy: "ignore" }); + + expectTypeOf(data).toEqualTypeOf< + UnmaskedVariablesCaseData | undefined + >(); + expectTypeOf(data).not.toEqualTypeOf< + MaskedVariablesCaseData | undefined + >(); + } + + { + const { data } = useSuspenseQuery< + Masked, + VariablesCaseVariables + >(maskedQuery, { errorPolicy: "ignore" }); + + expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).not.toEqualTypeOf< + UnmaskedVariablesCaseData | undefined + >(); + } + }); + + it('returns TData | undefined with errorPolicy: "all"', () => { + const { query } = useVariablesQueryCase(); + + const { data: inferred } = useSuspenseQuery(query, { + errorPolicy: "all", + }); + + expectTypeOf(inferred).toEqualTypeOf(); + expectTypeOf(inferred).not.toEqualTypeOf(); + + const { data: explicit } = useSuspenseQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { + errorPolicy: "all", + }); + + expectTypeOf(explicit).toEqualTypeOf(); + expectTypeOf(explicit).not.toEqualTypeOf(); + + const { query: maskedQuery } = useMaskedVariablesQueryCase(); + + { + const { data } = useSuspenseQuery(maskedQuery, { errorPolicy: "all" }); + + expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).not.toEqualTypeOf< + UnmaskedVariablesCaseData | undefined + >(); + } + + { + const { data } = useSuspenseQuery< + MaskedVariablesCaseData, + VariablesCaseVariables + >(maskedQuery, { errorPolicy: "all" }); + + expectTypeOf(data).toEqualTypeOf< + UnmaskedVariablesCaseData | undefined + >(); + expectTypeOf(data).not.toEqualTypeOf< + MaskedVariablesCaseData | undefined + >(); + } + + { + const { data } = useSuspenseQuery< + Masked, + VariablesCaseVariables + >(maskedQuery, { errorPolicy: "all" }); + + expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).not.toEqualTypeOf< + UnmaskedVariablesCaseData | undefined + >(); + } + }); + + it('returns TData with errorPolicy: "none"', () => { + const { query } = useVariablesQueryCase(); + + const { data: inferred } = useSuspenseQuery(query, { + errorPolicy: "none", + }); + + expectTypeOf(inferred).toEqualTypeOf(); + expectTypeOf(inferred).not.toEqualTypeOf(); + + const { data: explicit } = useSuspenseQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { + errorPolicy: "none", + }); + + expectTypeOf(explicit).toEqualTypeOf(); + expectTypeOf(explicit).not.toEqualTypeOf(); + + const { query: maskedQuery } = useMaskedVariablesQueryCase(); + + { + const { data } = useSuspenseQuery(maskedQuery, { errorPolicy: "none" }); + + expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).not.toEqualTypeOf(); + } + + { + const { data } = useSuspenseQuery< + MaskedVariablesCaseData, + VariablesCaseVariables + >(maskedQuery, { errorPolicy: "none" }); + + expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).not.toEqualTypeOf(); + } + + { + const { data } = useSuspenseQuery< + Masked, + VariablesCaseVariables + >(maskedQuery, { errorPolicy: "none" }); + + expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).not.toEqualTypeOf(); + } + }); + + it("returns DeepPartial with returnPartialData: true", () => { + const { query } = useVariablesQueryCase(); + + const { data: inferred } = useSuspenseQuery(query, { + returnPartialData: true, + }); + + expectTypeOf(inferred).toEqualTypeOf>(); + expectTypeOf(inferred).not.toEqualTypeOf(); + + const { data: explicit } = useSuspenseQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { + returnPartialData: true, + }); + + expectTypeOf(explicit).toEqualTypeOf>(); + expectTypeOf(explicit).not.toEqualTypeOf(); + + const { query: maskedQuery } = useMaskedVariablesQueryCase(); + + { + const { data } = useSuspenseQuery(maskedQuery, { + returnPartialData: true, + }); + + expectTypeOf(data).toEqualTypeOf< + DeepPartial + >(); + expectTypeOf(data).not.toEqualTypeOf< + DeepPartial + >(); + } + + { + const { data } = useSuspenseQuery< + MaskedVariablesCaseData, + VariablesCaseVariables + >(maskedQuery, { returnPartialData: true }); + + expectTypeOf(data).toEqualTypeOf< + DeepPartial + >(); + expectTypeOf(data).not.toEqualTypeOf< + DeepPartial + >(); + } + + { + const { data } = useSuspenseQuery< + Masked, + VariablesCaseVariables + >(maskedQuery, { returnPartialData: true }); + + expectTypeOf(data).toEqualTypeOf< + DeepPartial + >(); + expectTypeOf(data).not.toEqualTypeOf< + DeepPartial + >(); + } + }); + + it("returns TData with returnPartialData: false", () => { + const { query } = useVariablesQueryCase(); + + const { data: inferred } = useSuspenseQuery(query, { + returnPartialData: false, + }); + + expectTypeOf(inferred).toEqualTypeOf(); + expectTypeOf(inferred).not.toEqualTypeOf< + DeepPartial + >(); + + const { data: explicit } = useSuspenseQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { + returnPartialData: false, + }); + + expectTypeOf(explicit).toEqualTypeOf(); + expectTypeOf(explicit).not.toEqualTypeOf< + DeepPartial + >(); + + const { query: maskedQuery } = useMaskedVariablesQueryCase(); + + { + const { data } = useSuspenseQuery(maskedQuery, { + returnPartialData: false, + }); + + expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).not.toEqualTypeOf(); + } + + { + const { data } = useSuspenseQuery< + MaskedVariablesCaseData, + VariablesCaseVariables + >(maskedQuery, { returnPartialData: false }); + + expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).not.toEqualTypeOf(); + } + + { + const { data } = useSuspenseQuery< + Masked, + VariablesCaseVariables + >(maskedQuery, { returnPartialData: false }); + + expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).not.toEqualTypeOf(); + } + }); + + it("returns TData | undefined when skip is present", () => { + const { query } = useVariablesQueryCase(); + + const { data: inferred } = useSuspenseQuery(query, { + skip: true, }); expectTypeOf(inferred).toEqualTypeOf(); @@ -10750,6 +11936,59 @@ describe("useSuspenseQuery", () => { expectTypeOf(dynamic).toEqualTypeOf(); expectTypeOf(dynamic).not.toEqualTypeOf(); + + const { query: maskedQuery } = useMaskedVariablesQueryCase(); + + { + const { data } = useSuspenseQuery(maskedQuery, { skip: true }); + + expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).not.toEqualTypeOf< + UnmaskedVariablesCaseData | undefined + >(); + } + + { + const { data } = useSuspenseQuery< + MaskedVariablesCaseData, + VariablesCaseVariables + >(maskedQuery, { skip: true }); + + expectTypeOf(data).toEqualTypeOf< + UnmaskedVariablesCaseData | undefined + >(); + expectTypeOf(data).not.toEqualTypeOf< + MaskedVariablesCaseData | undefined + >(); + } + + { + const { data } = useSuspenseQuery< + Masked, + VariablesCaseVariables + >(maskedQuery, { skip: true }); + + expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).not.toEqualTypeOf< + UnmaskedVariablesCaseData | undefined + >(); + } + + { + const options = { + skip: true, + }; + + const { data } = useSuspenseQuery< + Masked, + VariablesCaseVariables + >(maskedQuery, { skip: options.skip }); + + expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).not.toEqualTypeOf< + UnmaskedVariablesCaseData | undefined + >(); + } }); it("returns TData | undefined when using `skipToken` as options", () => { @@ -10773,6 +12012,46 @@ describe("useSuspenseQuery", () => { expectTypeOf(explicit).toEqualTypeOf(); expectTypeOf(explicit).not.toEqualTypeOf(); + + const { query: maskedQuery } = useMaskedVariablesQueryCase(); + + { + const { data } = useSuspenseQuery( + maskedQuery, + options.skip ? skipToken : { variables: { id: "1" } } + ); + + expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).not.toEqualTypeOf< + UnmaskedVariablesCaseData | undefined + >(); + } + + { + const { data } = useSuspenseQuery< + MaskedVariablesCaseData, + VariablesCaseVariables + >(maskedQuery, options.skip ? skipToken : { variables: { id: "1" } }); + + expectTypeOf(data).toEqualTypeOf< + UnmaskedVariablesCaseData | undefined + >(); + expectTypeOf(data).not.toEqualTypeOf< + MaskedVariablesCaseData | undefined + >(); + } + + { + const { data } = useSuspenseQuery< + Masked, + VariablesCaseVariables + >(maskedQuery, options.skip ? skipToken : { variables: { id: "1" } }); + + expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).not.toEqualTypeOf< + UnmaskedVariablesCaseData | undefined + >(); + } }); it("returns TData | undefined when using `skipToken` with undefined options", () => { @@ -10796,6 +12075,46 @@ describe("useSuspenseQuery", () => { expectTypeOf(explicit).toEqualTypeOf(); expectTypeOf(explicit).not.toEqualTypeOf(); + + const { query: maskedQuery } = useMaskedVariablesQueryCase(); + + { + const { data } = useSuspenseQuery( + maskedQuery, + options.skip ? skipToken : undefined + ); + + expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).not.toEqualTypeOf< + UnmaskedVariablesCaseData | undefined + >(); + } + + { + const { data } = useSuspenseQuery< + MaskedVariablesCaseData, + VariablesCaseVariables + >(maskedQuery, options.skip ? skipToken : undefined); + + expectTypeOf(data).toEqualTypeOf< + UnmaskedVariablesCaseData | undefined + >(); + expectTypeOf(data).not.toEqualTypeOf< + MaskedVariablesCaseData | undefined + >(); + } + + { + const { data } = useSuspenseQuery< + Masked, + VariablesCaseVariables + >(maskedQuery, options.skip ? skipToken : undefined); + + expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).not.toEqualTypeOf< + UnmaskedVariablesCaseData | undefined + >(); + } }); it("returns DeepPartial | undefined when using `skipToken` as options with `returnPartialData`", () => { @@ -10823,6 +12142,50 @@ describe("useSuspenseQuery", () => { DeepPartial | undefined >(); expectTypeOf(explicit).not.toEqualTypeOf(); + + const { query: maskedQuery } = useMaskedVariablesQueryCase(); + + { + const { data } = useSuspenseQuery( + maskedQuery, + options.skip ? skipToken : { returnPartialData: true } + ); + + expectTypeOf(data).toEqualTypeOf< + DeepPartial | undefined + >(); + expectTypeOf(data).not.toEqualTypeOf< + DeepPartial | undefined + >(); + } + + { + const { data } = useSuspenseQuery< + MaskedVariablesCaseData, + VariablesCaseVariables + >(maskedQuery, options.skip ? skipToken : { returnPartialData: true }); + + expectTypeOf(data).toEqualTypeOf< + DeepPartial | undefined + >(); + expectTypeOf(data).not.toEqualTypeOf< + DeepPartial | undefined + >(); + } + + { + const { data } = useSuspenseQuery< + Masked, + VariablesCaseVariables + >(maskedQuery, options.skip ? skipToken : { returnPartialData: true }); + + expectTypeOf(data).toEqualTypeOf< + DeepPartial | undefined + >(); + expectTypeOf(data).not.toEqualTypeOf< + DeepPartial | undefined + >(); + } }); it("returns TData when passing an option that does not affect TData", () => { @@ -10848,6 +12211,37 @@ describe("useSuspenseQuery", () => { expectTypeOf(explicit).not.toEqualTypeOf< DeepPartial >(); + + const { query: maskedQuery } = useMaskedVariablesQueryCase(); + + { + const { data } = useSuspenseQuery(maskedQuery, { + fetchPolicy: "no-cache", + }); + + expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).not.toEqualTypeOf(); + } + + { + const { data } = useSuspenseQuery< + MaskedVariablesCaseData, + VariablesCaseVariables + >(maskedQuery, { fetchPolicy: "no-cache" }); + + expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).not.toEqualTypeOf(); + } + + { + const { data } = useSuspenseQuery< + Masked, + VariablesCaseVariables + >(maskedQuery, { fetchPolicy: "no-cache" }); + + expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).not.toEqualTypeOf(); + } }); it("handles combinations of options", () => { @@ -10860,6 +12254,7 @@ describe("useSuspenseQuery", () => { }; const { query } = useVariablesQueryCase(); + const { query: maskedQuery } = useMaskedVariablesQueryCase(); const { data: inferredPartialDataIgnore } = useSuspenseQuery(query, { returnPartialData: true, @@ -10888,6 +12283,20 @@ describe("useSuspenseQuery", () => { explicitPartialDataIgnore ).not.toEqualTypeOf(); + { + const { data } = useSuspenseQuery(maskedQuery, { + returnPartialData: true, + errorPolicy: "ignore", + }); + + expectTypeOf(data).toEqualTypeOf< + DeepPartial | undefined + >(); + expectTypeOf(data).not.toEqualTypeOf< + DeepPartial | undefined + >(); + } + const { data: inferredPartialDataNone } = useSuspenseQuery(query, { returnPartialData: true, errorPolicy: "none", @@ -10915,6 +12324,20 @@ describe("useSuspenseQuery", () => { explicitPartialDataNone ).not.toEqualTypeOf(); + { + const { data } = useSuspenseQuery(maskedQuery, { + returnPartialData: true, + errorPolicy: "ignore", + }); + + expectTypeOf(data).toEqualTypeOf< + DeepPartial | undefined + >(); + expectTypeOf(data).not.toEqualTypeOf< + DeepPartial | undefined + >(); + } + const { data: inferredSkipIgnore } = useSuspenseQuery(query, { skip: options.skip, errorPolicy: "ignore", @@ -10940,6 +12363,18 @@ describe("useSuspenseQuery", () => { >(); expectTypeOf(explicitSkipIgnore).not.toEqualTypeOf(); + { + const { data } = useSuspenseQuery(maskedQuery, { + skip: options.skip, + errorPolicy: "ignore", + }); + + expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).not.toEqualTypeOf< + UnmaskedVariablesCaseData | undefined + >(); + } + const { data: inferredSkipNone } = useSuspenseQuery(query, { skip: options.skip, errorPolicy: "none", @@ -10963,6 +12398,18 @@ describe("useSuspenseQuery", () => { >(); expectTypeOf(explicitSkipNone).not.toEqualTypeOf(); + { + const { data } = useSuspenseQuery(maskedQuery, { + skip: options.skip, + errorPolicy: "none", + }); + + expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).not.toEqualTypeOf< + UnmaskedVariablesCaseData | undefined + >(); + } + const { data: inferredPartialDataNoneSkip } = useSuspenseQuery(query, { skip: options.skip, returnPartialData: true, @@ -10991,6 +12438,21 @@ describe("useSuspenseQuery", () => { expectTypeOf( explicitPartialDataNoneSkip ).not.toEqualTypeOf(); + + { + const { data } = useSuspenseQuery(maskedQuery, { + skip: options.skip, + returnPartialData: true, + errorPolicy: "none", + }); + + expectTypeOf(data).toEqualTypeOf< + DeepPartial | undefined + >(); + expectTypeOf(data).not.toEqualTypeOf< + DeepPartial | undefined + >(); + } }); it("returns correct TData type when combined options that do not affect TData", () => { @@ -11016,6 +12478,235 @@ describe("useSuspenseQuery", () => { expectTypeOf(explicit).toEqualTypeOf>(); expectTypeOf(explicit).not.toEqualTypeOf(); + + const { query: maskedQuery } = useMaskedVariablesQueryCase(); + + { + const { data } = useSuspenseQuery(maskedQuery, { + fetchPolicy: "no-cache", + returnPartialData: true, + errorPolicy: "none", + }); + + expectTypeOf(data).toEqualTypeOf< + DeepPartial + >(); + expectTypeOf(data).not.toEqualTypeOf< + DeepPartial + >(); + } + + { + const { data } = useSuspenseQuery< + MaskedVariablesCaseData, + VariablesCaseVariables + >(maskedQuery, { + fetchPolicy: "no-cache", + returnPartialData: true, + errorPolicy: "none", + }); + + expectTypeOf(data).toEqualTypeOf< + DeepPartial + >(); + expectTypeOf(data).not.toEqualTypeOf< + DeepPartial + >(); + } + + { + const { data } = useSuspenseQuery< + Masked, + VariablesCaseVariables + >(maskedQuery, { + fetchPolicy: "no-cache", + returnPartialData: true, + errorPolicy: "none", + }); + + expectTypeOf(data).toEqualTypeOf< + DeepPartial + >(); + expectTypeOf(data).not.toEqualTypeOf< + DeepPartial + >(); + } + }); + + it("uses proper masked types for refetch", async () => { + const { query, unmaskedQuery } = useMaskedVariablesQueryCase(); + + { + const { refetch } = useSuspenseQuery(query); + + const result = await refetch(); + + expectTypeOf(result.data).toEqualTypeOf(); + expectTypeOf( + result.data + ).not.toEqualTypeOf(); + } + + { + const { refetch } = useSuspenseQuery(unmaskedQuery); + + const result = await refetch(); + + expectTypeOf(result.data).toEqualTypeOf(); + expectTypeOf(result.data).not.toEqualTypeOf(); + } + }); + + it("uses proper masked types for fetchMore", async () => { + const { query, unmaskedQuery } = useMaskedVariablesQueryCase(); + + { + const { fetchMore } = useSuspenseQuery(query); + + const result = await fetchMore({ + updateQuery: (queryData, { fetchMoreResult }) => { + expectTypeOf(queryData).toEqualTypeOf(); + expectTypeOf( + queryData + ).not.toEqualTypeOf(); + + expectTypeOf( + fetchMoreResult + ).toEqualTypeOf(); + expectTypeOf( + fetchMoreResult + ).not.toEqualTypeOf(); + + return {} as UnmaskedVariablesCaseData; + }, + }); + + expectTypeOf(result.data).toEqualTypeOf(); + expectTypeOf( + result.data + ).not.toEqualTypeOf(); + } + + { + const { fetchMore } = useSuspenseQuery(unmaskedQuery); + + const result = await fetchMore({ + updateQuery: (queryData, { fetchMoreResult }) => { + expectTypeOf(queryData).toEqualTypeOf(); + expectTypeOf( + queryData + ).not.toEqualTypeOf(); + + expectTypeOf( + fetchMoreResult + ).toEqualTypeOf(); + expectTypeOf( + fetchMoreResult + ).not.toEqualTypeOf(); + + return {} as UnmaskedVariablesCaseData; + }, + }); + + expectTypeOf(result.data).toEqualTypeOf(); + expectTypeOf(result.data).not.toEqualTypeOf(); + } + }); + + it("uses proper masked types for subscribeToMore", async () => { + type CharacterFragment = { + __typename: "Character"; + name: string; + } & { " $fragmentName": "CharacterFragment" }; + + type Subscription = { + pushLetter: { + __typename: "Character"; + id: number; + } & { " $fragmentRefs": { CharacterFragment: CharacterFragment } }; + }; + + type UnmaskedSubscription = { + pushLetter: { + __typename: "Character"; + id: number; + name: string; + }; + }; + + const { query, unmaskedQuery } = useMaskedVariablesQueryCase(); + + { + const { subscribeToMore } = useSuspenseQuery(query); + + const subscription: MaskedDocumentNode = gql` + subscription { + pushLetter { + id + ...CharacterFragment + } + } + + fragment CharacterFragment on Character { + name + } + `; + + subscribeToMore({ + document: subscription, + updateQuery: (queryData, { subscriptionData }) => { + expectTypeOf(queryData).toEqualTypeOf(); + expectTypeOf( + queryData + ).not.toEqualTypeOf(); + + expectTypeOf( + subscriptionData.data + ).toEqualTypeOf(); + expectTypeOf( + subscriptionData.data + ).not.toEqualTypeOf(); + + return {} as UnmaskedVariablesCaseData; + }, + }); + } + + { + const { subscribeToMore } = useSuspenseQuery(unmaskedQuery); + + const subscription: TypedDocumentNode = gql` + subscription { + pushLetter { + id + ...CharacterFragment + } + } + + fragment CharacterFragment on Character { + name + } + `; + + subscribeToMore({ + document: subscription, + updateQuery: (queryData, { subscriptionData }) => { + expectTypeOf(queryData).toEqualTypeOf(); + expectTypeOf( + queryData + ).not.toEqualTypeOf(); + + expectTypeOf( + subscriptionData.data + ).toEqualTypeOf(); + expectTypeOf( + subscriptionData.data + ).not.toEqualTypeOf(); + + return {} as UnmaskedVariablesCaseData; + }, + }); + } }); }); }); diff --git a/src/react/hooks/useFragment.ts b/src/react/hooks/useFragment.ts index 89ce1603a00..61736c5eee3 100644 --- a/src/react/hooks/useFragment.ts +++ b/src/react/hooks/useFragment.ts @@ -14,6 +14,7 @@ import type { ApolloClient, OperationVariables } from "../../core/index.js"; import type { NoInfer } from "../types/types.js"; import { useDeepMemo, wrapHook } from "./internal/index.js"; import equal from "@wry/equality"; +import type { FragmentType, MaybeMasked } from "../../masking/index.js"; export interface UseFragmentOptions extends Omit< @@ -24,7 +25,7 @@ export interface UseFragmentOptions Cache.ReadFragmentOptions, "id" | "variables" | "returnPartialData" > { - from: StoreObject | Reference | string; + from: StoreObject | Reference | FragmentType> | string; // Override this field to make it optional (default: true). optimistic?: boolean; /** @@ -40,12 +41,12 @@ export interface UseFragmentOptions export type UseFragmentResult = | { - data: TData; + data: MaybeMasked; complete: true; missing?: never; } | { - data: DeepPartial; + data: DeepPartial>; complete: false; missing?: MissingTree; }; @@ -63,7 +64,8 @@ export function useFragment( function _useFragment( options: UseFragmentOptions ): UseFragmentResult { - const { cache } = useApolloClient(options.client); + const client = useApolloClient(options.client); + const { cache } = client; const { from, ...rest } = options; // We calculate the cache id seperately from `stableOptions` because we don't @@ -81,19 +83,26 @@ function _useFragment( // get the correct diff on the next render given new diffOptions const diff = React.useMemo(() => { const { fragment, fragmentName, from, optimistic = true } = stableOptions; + const { cache } = client; + const diff = cache.diff({ + ...stableOptions, + returnPartialData: true, + id: from, + query: cache["getFragmentDoc"](fragment, fragmentName), + optimistic, + }); return { - result: diffToResult( - cache.diff({ - ...stableOptions, - returnPartialData: true, - id: from, - query: cache["getFragmentDoc"](fragment, fragmentName), - optimistic, - }) - ), + result: diffToResult({ + ...diff, + result: client["queryManager"].maskFragment({ + fragment, + fragmentName, + data: diff.result, + }), + }), }; - }, [stableOptions, cache]); + }, [client, stableOptions]); // Used for both getSnapshot and getServerSnapshot const getSnapshot = React.useCallback(() => diff.result, [diff]); @@ -102,7 +111,7 @@ function _useFragment( React.useCallback( (forceUpdate) => { let lastTimeout = 0; - const subscription = cache.watchFragment(stableOptions).subscribe({ + const subscription = client.watchFragment(stableOptions).subscribe({ next: (result) => { // Since `next` is called async by zen-observable, we want to avoid // unnecessarily rerendering this hook for the initial result @@ -123,7 +132,7 @@ function _useFragment( clearTimeout(lastTimeout); }; }, - [cache, stableOptions, diff] + [client, stableOptions, diff] ), getSnapshot, getSnapshot diff --git a/src/react/hooks/useLazyQuery.ts b/src/react/hooks/useLazyQuery.ts index 911d2b9e69c..671c23b9f86 100644 --- a/src/react/hooks/useLazyQuery.ts +++ b/src/react/hooks/useLazyQuery.ts @@ -248,7 +248,12 @@ function executeQuery( }, complete: () => { resolve( - toQueryResult(result, resultData.previousData, observable, client) + toQueryResult( + observable["maskResult"](result), + resultData.previousData, + observable, + client + ) ); }, }); diff --git a/src/react/hooks/useQuery.ts b/src/react/hooks/useQuery.ts index 5a483f9b4e8..a1b83c3c68c 100644 --- a/src/react/hooks/useQuery.ts +++ b/src/react/hooks/useQuery.ts @@ -56,6 +56,7 @@ import { } from "../../utilities/index.js"; import { wrapHook } from "./internal/index.js"; import type { RenderPromises } from "../ssr/RenderPromises.js"; +import type { MaybeMasked } from "../../masking/index.js"; const { prototype: { hasOwnProperty }, @@ -78,7 +79,7 @@ export interface InternalResult { // These members are populated by getCurrentResult and setResult, and it's // okay/normal for them to be initially undefined. current?: undefined | InternalQueryResult; - previousData?: undefined | TData; + previousData?: undefined | MaybeMasked; } interface InternalState { @@ -97,7 +98,7 @@ interface Callbacks { // Defining these methods as no-ops on the prototype allows us to call // state.onCompleted and/or state.onError without worrying about whether a // callback was provided. - onCompleted(data: TData): void; + onCompleted(data: MaybeMasked): void; onError(error: ApolloError): void; } @@ -331,7 +332,7 @@ function useObservableSubscriptionResult< partialRefetch: boolean | undefined, isSyncSSR: boolean, callbacks: { - onCompleted: (data: TData) => void; + onCompleted: (data: MaybeMasked) => void; onError: (error: ApolloError) => void; } ) { @@ -435,7 +436,8 @@ function useObservableSubscriptionResult< ) { setResult( { - data: (previousResult && previousResult.data) as TData, + data: (previousResult && + previousResult.data) as MaybeMasked, error: error as ApolloError, loading: false, networkStatus: NetworkStatus.error, @@ -650,7 +652,7 @@ export function getObsQueryOptions< } function setResult( - nextResult: ApolloQueryResult, + nextResult: ApolloQueryResult>, resultData: InternalResult, observable: ObservableQuery, client: ApolloClient, @@ -684,7 +686,7 @@ function setResult( } function handleErrorOrCompleted( - result: ApolloQueryResult, + result: ApolloQueryResult>, previousNetworkStatus: NetworkStatus | undefined, callbacks: Callbacks ) { @@ -759,8 +761,8 @@ export function toApolloError( } export function toQueryResult( - result: ApolloQueryResult, - previousData: TData | undefined, + result: ApolloQueryResult>, + previousData: MaybeMasked | undefined, observable: ObservableQuery, client: ApolloClient ): InternalQueryResult { @@ -781,10 +783,10 @@ function unsafeHandlePartialRefetch< TData, TVariables extends OperationVariables, >( - result: ApolloQueryResult, + result: ApolloQueryResult>, observable: ObservableQuery, partialRefetch: boolean | undefined -): ApolloQueryResult { +): ApolloQueryResult> { // TODO: This code should be removed when the partialRefetch option is // removed. I was unable to get this hook to behave reasonably in certain // edge cases when this block was put in an effect. diff --git a/src/react/hooks/useReadQuery.ts b/src/react/hooks/useReadQuery.ts index 3d6ae811df6..c43f0ac0cd0 100644 --- a/src/react/hooks/useReadQuery.ts +++ b/src/react/hooks/useReadQuery.ts @@ -12,6 +12,7 @@ import { useSyncExternalStore } from "./useSyncExternalStore.js"; import type { ApolloError } from "../../errors/index.js"; import type { NetworkStatus } from "../../core/index.js"; import { useApolloClient } from "./useApolloClient.js"; +import type { MaybeMasked } from "../../masking/index.js"; export interface UseReadQueryResult { /** @@ -20,7 +21,7 @@ export interface UseReadQueryResult { * This value might be `undefined` if a query results in one or more errors * (depending on the query's `errorPolicy`). */ - data: TData; + data: MaybeMasked; /** * If the query produces one or more errors, this object contains either an * array of `graphQLErrors` or a single `networkError`. Otherwise, this value diff --git a/src/react/hooks/useSubscription.ts b/src/react/hooks/useSubscription.ts index cf6c31ebabe..b86a8f78b9e 100644 --- a/src/react/hooks/useSubscription.ts +++ b/src/react/hooks/useSubscription.ts @@ -24,6 +24,7 @@ import { useDeepMemo } from "./internal/useDeepMemo.js"; import { useSyncExternalStore } from "./useSyncExternalStore.js"; import { toApolloError } from "./useQuery.js"; import { useIsomorphicLayoutEffect } from "./internal/useIsomorphicLayoutEffect.js"; +import type { MaybeMasked } from "../../masking/index.js"; /** * > Refer to the [Subscriptions](https://www.apollographql.com/docs/react/data/subscriptions/) section for a more in-depth overview of `useSubscription`. @@ -350,9 +351,9 @@ function createSubscription< }, }; - let observable: Observable> | null = null; + let observable: Observable>> | null = null; return Object.assign( - new Observable>((observer) => { + new Observable>>((observer) => { // lazily start the subscription when the first observer subscribes // to get around strict mode if (!observable) { diff --git a/src/react/hooks/useSuspenseQuery.ts b/src/react/hooks/useSuspenseQuery.ts index e3395390a6b..b38b199dc07 100644 --- a/src/react/hooks/useSuspenseQuery.ts +++ b/src/react/hooks/useSuspenseQuery.ts @@ -26,13 +26,14 @@ import { canonicalStringify } from "../../cache/index.js"; import { skipToken } from "./constants.js"; import type { SkipToken } from "./constants.js"; import type { CacheKey, QueryKey } from "../internal/index.js"; +import type { MaybeMasked, Unmasked } from "../../masking/index.js"; export interface UseSuspenseQueryResult< TData = unknown, TVariables extends OperationVariables = OperationVariables, > { client: ApolloClient; - data: TData; + data: MaybeMasked; error: ApolloError | undefined; fetchMore: FetchMoreFunction; networkStatus: NetworkStatus; @@ -43,14 +44,14 @@ export interface UseSuspenseQueryResult< export type FetchMoreFunction = ( fetchMoreOptions: FetchMoreQueryOptions & { updateQuery?: ( - previousQueryResult: TData, + previousQueryResult: Unmasked, options: { - fetchMoreResult: TData; + fetchMoreResult: Unmasked; variables: TVariables; } - ) => TData; + ) => Unmasked; } -) => Promise>; +) => Promise>>; export type RefetchFunction< TData, diff --git a/src/react/internal/cache/QueryReference.ts b/src/react/internal/cache/QueryReference.ts index b6279efd24c..30d88cb5a79 100644 --- a/src/react/internal/cache/QueryReference.ts +++ b/src/react/internal/cache/QueryReference.ts @@ -17,8 +17,11 @@ import { import type { QueryKey } from "./types.js"; import { wrapPromiseWithState } from "../../../utilities/index.js"; import { invariant } from "../../../utilities/globals/invariantWrappers.js"; +import type { MaybeMasked } from "../../../masking/index.js"; -type QueryRefPromise = PromiseWithState>; +type QueryRefPromise = PromiseWithState< + ApolloQueryResult> +>; type Listener = (promise: QueryRefPromise) => void; @@ -202,7 +205,7 @@ type ObservedOptions = Pick< >; export class InternalQueryReference { - public result!: ApolloQueryResult; + public result!: ApolloQueryResult>; public readonly key: QueryKey = {}; public readonly observable: ObservableQuery; @@ -212,7 +215,9 @@ export class InternalQueryReference { private listeners = new Set>(); private autoDisposeTimeoutId?: NodeJS.Timeout; - private resolve: ((result: ApolloQueryResult) => void) | undefined; + private resolve: + | ((result: ApolloQueryResult>) => void) + | undefined; private reject: ((error: unknown) => void) | undefined; private references = 0; @@ -390,7 +395,7 @@ export class InternalQueryReference { // noop. overridable by options } - private handleNext(result: ApolloQueryResult) { + private handleNext(result: ApolloQueryResult>) { switch (this.promise.status) { case "pending": { // Maintain the last successful `data` value if the next result does not @@ -440,7 +445,7 @@ export class InternalQueryReference { break; } default: { - this.promise = createRejectedPromise>(error); + this.promise = createRejectedPromise(error); this.deliver(this.promise); } } @@ -450,7 +455,9 @@ export class InternalQueryReference { this.listeners.forEach((listener) => listener(promise)); } - private initiateFetch(returnedPromise: Promise>) { + private initiateFetch( + returnedPromise: Promise>> + ) { this.promise = this.createPendingPromise(); this.promise.catch(() => {}); @@ -520,7 +527,7 @@ export class InternalQueryReference { private createPendingPromise() { return wrapPromiseWithState( - new Promise>((resolve, reject) => { + new Promise>>((resolve, reject) => { this.resolve = resolve; this.reject = reject; }) diff --git a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx index 8ab59054dd6..a4d8ce2814d 100644 --- a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx +++ b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx @@ -35,6 +35,11 @@ import { UseReadQueryResult, useReadQuery } from "../../hooks"; import { GraphQLError } from "graphql"; import { ErrorBoundary } from "react-error-boundary"; import userEvent from "@testing-library/user-event"; +import { + MaskedVariablesCaseData, + setupMaskedVariablesCase, +} from "../../../testing/internal/scenarios"; +import { Masked } from "../../../masking"; function createDefaultClient(mocks: MockedResponse[]) { return new ApolloClient({ @@ -1860,6 +1865,110 @@ test("suspends deferred queries until initial chunk loads then rerenders with de } }); +test("masks result when dataMasking is `true`", async () => { + const { query, mocks } = setupMaskedVariablesCase(); + const client = new ApolloClient({ + dataMasking: true, + cache: new InMemoryCache(), + link: new MockLink(mocks), + }); + const preloadQuery = createQueryPreloader(client); + + const queryRef = preloadQuery(query, { variables: { id: "1" } }); + + const { Profiler } = renderDefaultTestApp>({ + client, + queryRef, + }); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual(["App", "SuspenseFallback"]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { + character: { __typename: "Character", id: "1" }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } +}); + +test("does not mask result when dataMasking is `false`", async () => { + const { query, mocks } = setupMaskedVariablesCase(); + const client = new ApolloClient({ + dataMasking: false, + cache: new InMemoryCache(), + link: new MockLink(mocks), + }); + const preloadQuery = createQueryPreloader(client); + + const queryRef = preloadQuery(query, { variables: { id: "1" } }); + + const { Profiler } = renderDefaultTestApp({ + client, + queryRef, + }); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual(["App", "SuspenseFallback"]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { + character: { __typename: "Character", id: "1", name: "Spider-Man" }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } +}); + +test("does not mask results by default", async () => { + const { query, mocks } = setupMaskedVariablesCase(); + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: new MockLink(mocks), + }); + const preloadQuery = createQueryPreloader(client); + + const queryRef = preloadQuery(query, { variables: { id: "1" } }); + + const { Profiler } = renderDefaultTestApp({ + client, + queryRef, + }); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual(["App", "SuspenseFallback"]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { + character: { __typename: "Character", id: "1", name: "Spider-Man" }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } +}); + describe.skip("type tests", () => { const client = new ApolloClient({ cache: new InMemoryCache(), diff --git a/src/react/types/types.ts b/src/react/types/types.ts index 239f96fc133..7812fb34bc2 100644 --- a/src/react/types/types.ts +++ b/src/react/types/types.ts @@ -29,6 +29,7 @@ import type { MutationSharedOptions, SharedWatchQueryOptions, } from "../../core/watchQueryOptions.js"; +import type { MaybeMasked, Unmasked } from "../../masking/index.js"; /* QueryReference type */ @@ -67,7 +68,7 @@ export interface QueryFunctionOptions< /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#skip:member} */ skip?: boolean; /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#onCompleted:member} */ - onCompleted?: (data: TData) => void; + onCompleted?: (data: MaybeMasked) => void; /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#onError:member} */ onError?: (error: ApolloError) => void; @@ -102,19 +103,19 @@ export interface ObservableQueryFields< /** {@inheritDoc @apollo/client!QueryResultDocumentation#updateQuery:member} */ updateQuery: ( mapFn: ( - previousQueryResult: TData, + previousQueryResult: Unmasked, options: Pick, "variables"> - ) => TData + ) => Unmasked ) => void; /** {@inheritDoc @apollo/client!QueryResultDocumentation#refetch:member} */ refetch: ( variables?: Partial - ) => Promise>; + ) => Promise>>; /** @internal */ reobserve: ( newOptions?: Partial>, newNetworkStatus?: NetworkStatus - ) => Promise>; + ) => Promise>>; /** {@inheritDoc @apollo/client!QueryResultDocumentation#variables:member} */ variables: TVariables | undefined; /** {@inheritDoc @apollo/client!QueryResultDocumentation#fetchMore:member} */ @@ -124,14 +125,14 @@ export interface ObservableQueryFields< >( fetchMoreOptions: FetchMoreQueryOptions & { updateQuery?: ( - previousQueryResult: TData, + previousQueryResult: Unmasked, options: { - fetchMoreResult: TFetchData; + fetchMoreResult: Unmasked; variables: TFetchVars; } - ) => TData; + ) => Unmasked; } - ) => Promise>; + ) => Promise>>; } export interface QueryResult< @@ -143,9 +144,9 @@ export interface QueryResult< /** {@inheritDoc @apollo/client!QueryResultDocumentation#observable:member} */ observable: ObservableQuery; /** {@inheritDoc @apollo/client!QueryResultDocumentation#data:member} */ - data: TData | undefined; + data: MaybeMasked | undefined; /** {@inheritDoc @apollo/client!QueryResultDocumentation#previousData:member} */ - previousData?: TData; + previousData?: MaybeMasked; /** {@inheritDoc @apollo/client!QueryResultDocumentation#error:member} */ error?: ApolloError; /** @@ -180,7 +181,7 @@ export interface LazyQueryHookOptions< TVariables extends OperationVariables = OperationVariables, > extends BaseQueryOptions { /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#onCompleted:member} */ - onCompleted?: (data: TData) => void; + onCompleted?: (data: MaybeMasked) => void; /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#onError:member} */ onError?: (error: ApolloError) => void; @@ -351,7 +352,10 @@ export interface BaseMutationOptions< /** {@inheritDoc @apollo/client!MutationOptionsDocumentation#notifyOnNetworkStatusChange:member} */ notifyOnNetworkStatusChange?: boolean; /** {@inheritDoc @apollo/client!MutationOptionsDocumentation#onCompleted:member} */ - onCompleted?: (data: TData, clientOptions?: BaseMutationOptions) => void; + onCompleted?: ( + data: MaybeMasked, + clientOptions?: BaseMutationOptions + ) => void; /** {@inheritDoc @apollo/client!MutationOptionsDocumentation#onError:member} */ onError?: (error: ApolloError, clientOptions?: BaseMutationOptions) => void; /** {@inheritDoc @apollo/client!MutationOptionsDocumentation#ignoreResults:member} */ @@ -370,7 +374,7 @@ export interface MutationFunctionOptions< export interface MutationResult { /** {@inheritDoc @apollo/client!MutationResultDocumentation#data:member} */ - data?: TData | null; + data?: MaybeMasked | null; /** {@inheritDoc @apollo/client!MutationResultDocumentation#error:member} */ error?: ApolloError; /** {@inheritDoc @apollo/client!MutationResultDocumentation#loading:member} */ @@ -390,7 +394,7 @@ export declare type MutationFunction< TCache extends ApolloCache = ApolloCache, > = ( options?: MutationFunctionOptions -) => Promise>; +) => Promise>>; export interface MutationHookOptions< TData = any, @@ -418,7 +422,7 @@ export type MutationTuple< options?: MutationFunctionOptions // TODO This FetchResult seems strange here, as opposed to an // ApolloQueryResult - ) => Promise>, + ) => Promise>>, result: MutationResult, ]; @@ -477,7 +481,7 @@ export interface SubscriptionResult { /** {@inheritDoc @apollo/client!SubscriptionResultDocumentation#loading:member} */ loading: boolean; /** {@inheritDoc @apollo/client!SubscriptionResultDocumentation#data:member} */ - data?: TData; + data?: MaybeMasked; /** {@inheritDoc @apollo/client!SubscriptionResultDocumentation#error:member} */ error?: ApolloError; // This was added by the legacy useSubscription type, and is tested in unit @@ -511,29 +515,4 @@ export interface SubscriptionCurrentObservable { subscription?: ObservableSubscription; } -/** -Helper type that allows using a type in a way that cannot be "widened" by inference on the value it is used on. - -This type was first suggested [in this Github discussion](https://github.com/microsoft/TypeScript/issues/14829#issuecomment-504042546). - -Example usage: -```ts -export function useQuery< - TData = any, - TVariables extends OperationVariables = OperationVariables, ->( - query: DocumentNode | TypedDocumentNode, - options: QueryHookOptions, NoInfer> = Object.create(null), -) -``` -In this case, `TData` and `TVariables` should be inferred from `query`, but never widened from something in `options`. - -So, in this code example: -```ts -declare const typedNode: TypedDocumentNode<{ foo: string}, { bar: number }> -const { variables } = useQuery(typedNode, { variables: { bar: 4, nonExistingVariable: "string" } }); -``` -Without the use of `NoInfer`, `variables` would now be of the type `{ bar: number, nonExistingVariable: "string" }`. -With `NoInfer`, it will instead give an error on `nonExistingVariable`. - */ -export type NoInfer = [T][T extends any ? 0 : never]; +export type { NoInfer } from "../../utilities/index.js"; diff --git a/src/testing/core/mocking/mockLink.ts b/src/testing/core/mocking/mockLink.ts index 646567b2da9..898efe6bfd3 100644 --- a/src/testing/core/mocking/mockLink.ts +++ b/src/testing/core/mocking/mockLink.ts @@ -21,6 +21,7 @@ import { removeDirectivesFromDocument, checkDocument, } from "../../../utilities/index.js"; +import type { Unmasked } from "../../../masking/index.js"; /** @internal */ type CovariantUnaryFunction = { fn(arg: Arg): Ret }["fn"]; @@ -36,16 +37,19 @@ export type VariableMatcher> = CovariantUnaryFunction< >; export interface MockedResponse< + // @ts-ignore out TData = Record, out TVariables = Record, > { request: GraphQLRequest; maxUsageCount?: number; - result?: FetchResult | ResultFunction, TVariables>; + result?: + | FetchResult> + | ResultFunction>, TVariables>; error?: Error; delay?: number; variableMatcher?: VariableMatcher; - newData?: ResultFunction, TVariables>; + newData?: ResultFunction>, TVariables>; } export interface MockLinkOptions { @@ -213,7 +217,7 @@ ${unmatchedVars.map((d) => ` ${stringifyForDisplay(d)}`).join("\n")} ): MockedResponse { const newMockedResponse = cloneDeep(mockedResponse); const queryWithoutClientOnlyDirectives = removeDirectivesFromDocument( - [{ name: "connection" }, { name: "nonreactive" }], + [{ name: "connection" }, { name: "nonreactive" }, { name: "unmask" }], checkDocument(newMockedResponse.request.query) ); invariant(queryWithoutClientOnlyDirectives, "query is required"); diff --git a/src/testing/core/mocking/mockQueryManager.ts b/src/testing/core/mocking/mockQueryManager.ts index 6be4ab764ed..5395dcc9c11 100644 --- a/src/testing/core/mocking/mockQueryManager.ts +++ b/src/testing/core/mocking/mockQueryManager.ts @@ -18,6 +18,7 @@ export const getDefaultOptionsForQueryManagerTests = ( localState: new LocalState({ cache: options.cache }), assumeImmutableResults: !!options.cache.assumeImmutableResults, defaultContext: undefined, + dataMasking: false, ...options, }); diff --git a/src/testing/internal/disposables/index.ts b/src/testing/internal/disposables/index.ts index 9d61c88fd90..f86a3e9457a 100644 --- a/src/testing/internal/disposables/index.ts +++ b/src/testing/internal/disposables/index.ts @@ -2,3 +2,4 @@ export { disableActWarnings } from "./disableActWarnings.js"; export { spyOnConsole } from "./spyOnConsole.js"; export { withCleanup } from "./withCleanup.js"; export { enableFakeTimers } from "./enableFakeTimers.js"; +export { withProdMode } from "./withProdMode.js"; diff --git a/src/testing/internal/disposables/withProdMode.ts b/src/testing/internal/disposables/withProdMode.ts new file mode 100644 index 00000000000..ecdf3b408c2 --- /dev/null +++ b/src/testing/internal/disposables/withProdMode.ts @@ -0,0 +1,10 @@ +import { withCleanup } from "./withCleanup.js"; + +export function withProdMode() { + const prev = { prevDEV: __DEV__ }; + Object.defineProperty(globalThis, "__DEV__", { value: false }); + + return withCleanup(prev, ({ prevDEV }) => { + Object.defineProperty(globalThis, "__DEV__", { value: prevDEV }); + }); +} diff --git a/src/testing/internal/scenarios/index.ts b/src/testing/internal/scenarios/index.ts index c6943f51236..fc266ff3685 100644 --- a/src/testing/internal/scenarios/index.ts +++ b/src/testing/internal/scenarios/index.ts @@ -1,5 +1,6 @@ import { ApolloLink, Observable, gql } from "../../../core/index.js"; import type { TypedDocumentNode } from "../../../core/index.js"; +import type { MaskedDocumentNode } from "../../../masking/index.js"; import type { MockedResponse } from "../../core/index.js"; export interface SimpleCaseData { @@ -63,6 +64,69 @@ export function setupVariablesCase() { return { mocks, query }; } +export type MaskedVariablesCaseFragment = { + name: string; +} & { " $fragmentName"?: "MaskedVariablesCaseFragment" }; + +export interface MaskedVariablesCaseData { + character: { + __typename: "Character"; + id: string; + } & { + " $fragmentRefs"?: { + MaskedVariablesCaseFragment: MaskedVariablesCaseFragment; + }; + }; +} + +export interface UnmaskedVariablesCaseData { + character: { + __typename: "Character"; + id: string; + name: string; + }; +} + +export function setupMaskedVariablesCase() { + const document = gql` + query CharacterQuery($id: ID!) { + character(id: $id) { + id + ...CharacterFragment + } + } + + fragment CharacterFragment on Character { + name + } + `; + const query: MaskedDocumentNode< + MaskedVariablesCaseData, + VariablesCaseVariables + > = document; + + const unmaskedQuery: TypedDocumentNode< + MaskedVariablesCaseData, + VariablesCaseVariables + > = document; + + const CHARACTERS = ["Spider-Man", "Black Widow", "Iron Man", "Hulk"]; + + const mocks: MockedResponse[] = [...CHARACTERS].map( + (name, index) => ({ + request: { query, variables: { id: String(index + 1) } }, + result: { + data: { + character: { __typename: "Character", id: String(index + 1), name }, + }, + }, + delay: 20, + }) + ); + + return { mocks, query, unmaskedQuery }; +} + interface Letter { letter: string; position: number; diff --git a/src/utilities/common/maybeDeepFreeze.ts b/src/utilities/common/maybeDeepFreeze.ts index 1d44ac29f51..07f7ac5070f 100644 --- a/src/utilities/common/maybeDeepFreeze.ts +++ b/src/utilities/common/maybeDeepFreeze.ts @@ -1,6 +1,6 @@ import { isNonNullObject } from "./objects.js"; -function deepFreeze(value: any) { +export function deepFreeze(value: any) { const workSet = new Set([value]); workSet.forEach((obj) => { if (isNonNullObject(obj) && shallowFreeze(obj) === obj) { diff --git a/src/utilities/graphql/__tests__/directives.ts b/src/utilities/graphql/__tests__/directives.ts index 2e1d891754c..a46581c3b00 100644 --- a/src/utilities/graphql/__tests__/directives.ts +++ b/src/utilities/graphql/__tests__/directives.ts @@ -7,7 +7,11 @@ import { hasDirectives, hasAnyDirectives, hasAllDirectives, + getFragmentMaskMode, } from "../directives"; +import { spyOnConsole } from "../../../testing/internal"; +import { BREAK, visit } from "graphql"; +import type { DocumentNode, FragmentSpreadNode } from "graphql"; describe("hasDirectives", () => { it("should allow searching the ast for a directive", () => { @@ -512,3 +516,134 @@ describe("shouldInclude", () => { }).toThrow(); }); }); + +describe("getFragmentMaskMode", () => { + it("returns 'unmask' when @unmask used on fragment node", () => { + const fragmentNode = getFragmentSpreadNode(gql` + query { + ...MyFragment @unmask + } + `); + + const mode = getFragmentMaskMode(fragmentNode); + + expect(mode).toBe("unmask"); + }); + + it("returns 'mask' when no directives are present", () => { + const fragmentNode = getFragmentSpreadNode(gql` + query { + ...MyFragment + } + `); + + const mode = getFragmentMaskMode(fragmentNode); + + expect(mode).toBe("mask"); + }); + + it("returns 'mask' when a different directive is used", () => { + const fragmentNode = getFragmentSpreadNode(gql` + query { + ...MyFragment @myDirective + } + `); + + const mode = getFragmentMaskMode(fragmentNode); + + expect(mode).toBe("mask"); + }); + + it("returns 'unmask' when used with other directives", () => { + const fragmentNode = getFragmentSpreadNode(gql` + query { + ...MyFragment @myDirective @unmask + } + `); + + const mode = getFragmentMaskMode(fragmentNode); + + expect(mode).toBe("unmask"); + }); + + it("returns 'migrate' when passing mode: 'migrate' as argument", () => { + const fragmentNode = getFragmentSpreadNode(gql` + query { + ...MyFragment @unmask(mode: "migrate") + } + `); + + const mode = getFragmentMaskMode(fragmentNode); + + expect(mode).toBe("migrate"); + }); + + it("warns and returns 'unmask' when using variable for mode argument", () => { + using _ = spyOnConsole("warn"); + const fragmentNode = getFragmentSpreadNode(gql` + query ($mode: String!) { + ...MyFragment @unmask(mode: $mode) + } + `); + + const mode = getFragmentMaskMode(fragmentNode); + + expect(console.warn).toHaveBeenCalledTimes(1); + expect(console.warn).toHaveBeenCalledWith( + "@unmask 'mode' argument does not support variables." + ); + expect(mode).toBe("unmask"); + }); + + it("warns and returns 'unmask' when passing a non-string argument to mode", () => { + using _ = spyOnConsole("warn"); + const fragmentNode = getFragmentSpreadNode(gql` + query { + ...MyFragment @unmask(mode: true) + } + `); + + const mode = getFragmentMaskMode(fragmentNode); + + expect(console.warn).toHaveBeenCalledTimes(1); + expect(console.warn).toHaveBeenCalledWith( + "@unmask 'mode' argument must be of type string." + ); + expect(mode).toBe("unmask"); + }); + + it("warns and returns 'unmask' when passing a value other than 'migrate' to mode", () => { + using _ = spyOnConsole("warn"); + const fragmentNode = getFragmentSpreadNode(gql` + query { + ...MyFragment @unmask(mode: "invalid") + } + `); + + const mode = getFragmentMaskMode(fragmentNode); + + expect(console.warn).toHaveBeenCalledTimes(1); + expect(console.warn).toHaveBeenCalledWith( + "@unmask 'mode' argument does not recognize value '%s'.", + "invalid" + ); + expect(mode).toBe("unmask"); + }); +}); + +function getFragmentSpreadNode(document: DocumentNode): FragmentSpreadNode { + let fragmentSpreadNode: FragmentSpreadNode | undefined = undefined; + + visit(document, { + FragmentSpread: (node) => { + fragmentSpreadNode = node; + return BREAK; + }, + }); + + if (!fragmentSpreadNode) { + throw new Error("Must give a document with a fragment spread"); + } + + return fragmentSpreadNode; +} diff --git a/src/utilities/graphql/directives.ts b/src/utilities/graphql/directives.ts index 797823f00f1..40a55ade632 100644 --- a/src/utilities/graphql/directives.ts +++ b/src/utilities/graphql/directives.ts @@ -11,8 +11,9 @@ import type { ArgumentNode, ValueNode, ASTNode, + FragmentSpreadNode, } from "graphql"; -import { visit, BREAK } from "graphql"; +import { visit, BREAK, Kind } from "graphql"; export type DirectiveInfo = { [fieldName: string]: { [argName: string]: any }; @@ -133,3 +134,45 @@ export function getInclusionDirectives( return result; } + +/** @internal */ +export function getFragmentMaskMode( + fragment: FragmentSpreadNode +): "mask" | "migrate" | "unmask" { + const directive = fragment.directives?.find( + ({ name }) => name.value === "unmask" + ); + + if (!directive) { + return "mask"; + } + + const modeArg = directive.arguments?.find( + ({ name }) => name.value === "mode" + ); + + if (__DEV__) { + if (modeArg) { + if (modeArg.value.kind === Kind.VARIABLE) { + invariant.warn("@unmask 'mode' argument does not support variables."); + } else if (modeArg.value.kind !== Kind.STRING) { + invariant.warn("@unmask 'mode' argument must be of type string."); + } else if (modeArg.value.value !== "migrate") { + invariant.warn( + "@unmask 'mode' argument does not recognize value '%s'.", + modeArg.value.value + ); + } + } + } + + if ( + modeArg && + "value" in modeArg.value && + modeArg.value.value === "migrate" + ) { + return "migrate"; + } + + return "unmask"; +} diff --git a/src/utilities/graphql/transform.ts b/src/utilities/graphql/transform.ts index 7a424f4df20..f6e8b7e1c55 100644 --- a/src/utilities/graphql/transform.ts +++ b/src/utilities/graphql/transform.ts @@ -716,3 +716,22 @@ export function removeClientSetsFromDocument( return modifiedDoc; } + +export function addNonReactiveToNamedFragments(document: DocumentNode) { + checkDocument(document); + + return visit(document, { + FragmentSpread: (node) => { + return { + ...node, + directives: [ + ...(node.directives || []), + { + kind: Kind.DIRECTIVE, + name: { kind: Kind.NAME, value: "nonreactive" }, + } satisfies DirectiveNode, + ], + }; + }, + }); +} diff --git a/src/utilities/index.ts b/src/utilities/index.ts index 637ae100af7..59f2edb054f 100644 --- a/src/utilities/index.ts +++ b/src/utilities/index.ts @@ -12,6 +12,7 @@ export { hasClientExports, getDirectiveNames, getInclusionDirectives, + getFragmentMaskMode, } from "./graphql/directives.js"; export type { DocumentTransformCacheKey } from "./graphql/DocumentTransform.js"; @@ -72,6 +73,7 @@ export type { } from "./graphql/transform.js"; export { addTypenameToDocument, + addNonReactiveToNamedFragments, buildQueryFromSelectionSet, removeDirectivesFromDocument, removeConnectionDirectiveFromDocument, @@ -106,9 +108,11 @@ export { wrapPromiseWithState, } from "./promises/decoration.js"; +export { preventUnhandledRejection } from "./promises/preventUnhandledRejection.js"; + export * from "./common/mergeDeep.js"; export * from "./common/cloneDeep.js"; -export * from "./common/maybeDeepFreeze.js"; +export { maybeDeepFreeze } from "./common/maybeDeepFreeze.js"; export * from "./observables/iteration.js"; export * from "./observables/asyncMap.js"; export * from "./observables/Concast.js"; @@ -131,6 +135,9 @@ export * from "./types/IsStrictlyAny.js"; export type { DeepOmit } from "./types/DeepOmit.js"; export type { DeepPartial } from "./types/DeepPartial.js"; export type { OnlyRequiredProperties } from "./types/OnlyRequiredProperties.js"; +export type { Prettify } from "./types/Prettify.js"; +export type { UnionToIntersection } from "./types/UnionToIntersection.js"; +export type { NoInfer } from "./types/NoInfer.js"; export { AutoCleanedStrongCache, diff --git a/src/utilities/promises/preventUnhandledRejection.ts b/src/utilities/promises/preventUnhandledRejection.ts new file mode 100644 index 00000000000..8e62eea5509 --- /dev/null +++ b/src/utilities/promises/preventUnhandledRejection.ts @@ -0,0 +1,5 @@ +export function preventUnhandledRejection(promise: Promise): Promise { + promise.catch(() => {}); + + return promise; +} diff --git a/src/utilities/types/NoInfer.ts b/src/utilities/types/NoInfer.ts new file mode 100644 index 00000000000..1d0a2a70606 --- /dev/null +++ b/src/utilities/types/NoInfer.ts @@ -0,0 +1,26 @@ +/** +Helper type that allows using a type in a way that cannot be "widened" by inference on the value it is used on. + +This type was first suggested [in this Github discussion](https://github.com/microsoft/TypeScript/issues/14829#issuecomment-504042546). + +Example usage: +```ts +export function useQuery< + TData = any, + TVariables extends OperationVariables = OperationVariables, +>( + query: DocumentNode | TypedDocumentNode, + options: QueryHookOptions, NoInfer> = Object.create(null), +) +``` +In this case, `TData` and `TVariables` should be inferred from `query`, but never widened from something in `options`. + +So, in this code example: +```ts +declare const typedNode: TypedDocumentNode<{ foo: string}, { bar: number }> +const { variables } = useQuery(typedNode, { variables: { bar: 4, nonExistingVariable: "string" } }); +``` +Without the use of `NoInfer`, `variables` would now be of the type `{ bar: number, nonExistingVariable: "string" }`. +With `NoInfer`, it will instead give an error on `nonExistingVariable`. + */ +export type NoInfer = [T][T extends any ? 0 : never]; diff --git a/src/utilities/types/Prettify.ts b/src/utilities/types/Prettify.ts new file mode 100644 index 00000000000..b0b3f5f5169 --- /dev/null +++ b/src/utilities/types/Prettify.ts @@ -0,0 +1 @@ +export type Prettify = { [K in keyof T]: T[K] } & {}; diff --git a/src/utilities/types/UnionToIntersection.ts b/src/utilities/types/UnionToIntersection.ts new file mode 100644 index 00000000000..ba2b6951ed0 --- /dev/null +++ b/src/utilities/types/UnionToIntersection.ts @@ -0,0 +1,4 @@ +// https://stackoverflow.com/a/50375286/2012454 +export type UnionToIntersection = + (U extends any ? (x: U) => void : never) extends (x: infer I) => void ? I + : never;