From d35d3d4458f29aa9d504781958ac81244ca82744 Mon Sep 17 00:00:00 2001 From: Valery Bugakov Date: Thu, 23 Sep 2021 16:28:53 +0800 Subject: [PATCH] web: enable `apollo-cache-persist` (#23351) --- .../src/shared/backend/requestGraphQl.ts | 4 +- .../src/api/client/mainthread-api.test.ts | 2 +- client/shared/src/extensions/helpers.ts | 5 +- .../shared/src/graphql/{ => apollo}/cache.ts | 10 +- client/shared/src/graphql/apollo/client.ts | 77 +++++++++++ .../{ => apollo}/fromObservableQuery.test.ts | 0 .../{ => apollo}/fromObservableQuery.ts | 0 client/shared/src/graphql/apollo/hooks.ts | 80 +++++++++++ client/shared/src/graphql/apollo/index.ts | 3 + .../graphql/apollo/persistenceMapper.test.ts | 85 ++++++++++++ .../src/graphql/apollo/persistenceMapper.ts | 80 +++++++++++ client/shared/src/graphql/constants.ts | 1 + client/shared/src/graphql/graphql.ts | 127 +----------------- .../src/testing/apollo/mockedTestProvider.tsx | 2 +- .../shared/src/testing/integration/context.ts | 1 + .../src/testing/integration/mockExtension.ts | 1 + .../src/apollo/MockedStoryProvider.tsx | 4 +- client/web/src/backend/graphql.ts | 1 + .../web/src/integration/blob-viewer.test.ts | 6 + .../integration/extension-registry.test.ts | 2 + client/web/src/integration/repository.test.ts | 1 + client/web/src/platform/context.ts | 3 +- .../temporary/TemporarySettingsStorage.ts | 2 +- package.json | 1 + yarn.lock | 5 + 25 files changed, 367 insertions(+), 136 deletions(-) rename client/shared/src/graphql/{ => apollo}/cache.ts (64%) create mode 100644 client/shared/src/graphql/apollo/client.ts rename client/shared/src/graphql/{ => apollo}/fromObservableQuery.test.ts (100%) rename client/shared/src/graphql/{ => apollo}/fromObservableQuery.ts (100%) create mode 100644 client/shared/src/graphql/apollo/hooks.ts create mode 100644 client/shared/src/graphql/apollo/index.ts create mode 100644 client/shared/src/graphql/apollo/persistenceMapper.test.ts create mode 100644 client/shared/src/graphql/apollo/persistenceMapper.ts create mode 100644 client/shared/src/graphql/constants.ts diff --git a/client/browser/src/shared/backend/requestGraphQl.ts b/client/browser/src/shared/backend/requestGraphQl.ts index 7eade890a03..f1b473ba0fc 100644 --- a/client/browser/src/shared/backend/requestGraphQl.ts +++ b/client/browser/src/shared/backend/requestGraphQl.ts @@ -107,7 +107,9 @@ export function createGraphQLHelpers(sourcegraphURL: string, isExtension: boolea * Memoized Apollo Client getter. It should be executed once to restore the cache from the local storage. * After that, the same instance should be used by all consumers. */ - const getBrowserGraphQLClient = once(() => getGraphQLClient({ headers: getHeaders(), baseUrl: sourcegraphURL })) + const getBrowserGraphQLClient = once(() => + getGraphQLClient({ headers: getHeaders(), baseUrl: sourcegraphURL, isAuthenticated: false }) + ) return { getBrowserGraphQLClient, requestGraphQL } } diff --git a/client/shared/src/api/client/mainthread-api.test.ts b/client/shared/src/api/client/mainthread-api.test.ts index bdcb2d672b4..c3ade68a95c 100644 --- a/client/shared/src/api/client/mainthread-api.test.ts +++ b/client/shared/src/api/client/mainthread-api.test.ts @@ -12,7 +12,7 @@ import { SettingsEdit } from './services/settings' describe('MainThreadAPI', () => { // TODO(tj): commands, notifications - const getGraphQLClient = () => getGraphQLClientBase({ headers: {} }) + const getGraphQLClient = () => getGraphQLClientBase({ headers: {}, isAuthenticated: false }) describe('graphQL', () => { test('PlatformContext#requestGraphQL is called with the correct arguments', async () => { diff --git a/client/shared/src/extensions/helpers.ts b/client/shared/src/extensions/helpers.ts index 1dbc0bc3fb6..56c5a3dfa2d 100644 --- a/client/shared/src/extensions/helpers.ts +++ b/client/shared/src/extensions/helpers.ts @@ -7,8 +7,7 @@ import { ExtensionsWithPrioritizeExtensionIDsParamAndNoJSONFieldsResult, ExtensionsWithPrioritizeExtensionIDsParamAndNoJSONFieldsVariables, } from '../graphql-operations' -import { fromObservableQueryPromise } from '../graphql/fromObservableQuery' -import { getDocumentNode, gql } from '../graphql/graphql' +import { fromObservableQueryPromise, getDocumentNode, gql } from '../graphql/graphql' import { PlatformContext } from '../platform/context' import { createAggregateError } from '../util/errors' @@ -24,6 +23,7 @@ const ExtensionsQuery = gql` extensionRegistry { extensions(first: $first, extensionIDs: $extensionIDs) { nodes { + id extensionID manifest { jsonFields(fields: $extensionManifestFields) @@ -39,6 +39,7 @@ const ExtensionsWithPrioritizeExtensionIDsParameterAndNoJSONFieldsQuery = gql` extensionRegistry { extensions(first: $first, prioritizeExtensionIDs: $extensionIDs) { nodes { + id extensionID manifest { raw diff --git a/client/shared/src/graphql/cache.ts b/client/shared/src/graphql/apollo/cache.ts similarity index 64% rename from client/shared/src/graphql/cache.ts rename to client/shared/src/graphql/apollo/cache.ts index e31b36b2893..153a04ddded 100644 --- a/client/shared/src/graphql/cache.ts +++ b/client/shared/src/graphql/apollo/cache.ts @@ -1,10 +1,18 @@ import { InMemoryCache } from '@apollo/client' -import { TypedTypePolicies } from '../graphql-operations' +import { TypedTypePolicies } from '../../graphql-operations' +import { IExtensionRegistry } from '../schema' // Defines how the Apollo cache interacts with our GraphQL schema. // See https://www.apollographql.com/docs/react/caching/cache-configuration/#typepolicy-fields const typePolicies: TypedTypePolicies = { + ExtensionRegistry: { + // Replace existing `ExtensionRegistry` with the incoming value. + // Required because of the missing `id` on the `ExtensionRegistry` field. + merge(existing: IExtensionRegistry, incoming: IExtensionRegistry): IExtensionRegistry { + return incoming + }, + }, Query: { fields: { node: { diff --git a/client/shared/src/graphql/apollo/client.ts b/client/shared/src/graphql/apollo/client.ts new file mode 100644 index 00000000000..0ef0aa338de --- /dev/null +++ b/client/shared/src/graphql/apollo/client.ts @@ -0,0 +1,77 @@ +import { ApolloClient, createHttpLink, NormalizedCacheObject } from '@apollo/client' +import { LocalStorageWrapper, CachePersistor } from 'apollo3-cache-persist' +import { once } from 'lodash' + +import { GRAPHQL_URI } from '../constants' + +import { cache } from './cache' +import { persistenceMapper } from './persistenceMapper' + +interface GetGraphqlClientOptions { + headers: RequestInit['headers'] + isAuthenticated: boolean + baseUrl?: string +} + +export type GraphQLClient = ApolloClient + +/** + * 🚨 SECURITY: Use two unique keys for authenticated and anonymous users + * to avoid keeping private information in localStorage after logout. + */ +const getApolloPersistCacheKey = (isAuthenticated: boolean): string => + `apollo-cache-persist-${isAuthenticated ? 'authenticated' : 'anonymous'}` + +export const getGraphQLClient = once( + async (options: GetGraphqlClientOptions): Promise => { + const { headers, baseUrl, isAuthenticated } = options + const uri = baseUrl ? new URL(GRAPHQL_URI, baseUrl).href : GRAPHQL_URI + + const persistor = new CachePersistor({ + cache, + persistenceMapper, + // Use max 4 MB for persistent cache. Leave 1 MB for other means out of 5 MB available. + // If exceeded, persistence will pause and app will start up cold on next launch. + maxSize: 1024 * 1024 * 4, + key: getApolloPersistCacheKey(isAuthenticated), + storage: new LocalStorageWrapper(window.localStorage), + }) + + // 🚨 SECURITY: Drop persisted cache item in case `isAuthenticated` value changed. + localStorage.removeItem(getApolloPersistCacheKey(!isAuthenticated)) + await persistor.restore() + + const apolloClient = new ApolloClient({ + uri, + cache, + defaultOptions: { + /** + * The default `fetchPolicy` is `cache-first`, which returns a cached response + * and doesn't trigger cache update. This is undesirable default behavior because + * we want to keep our cache updated to avoid confusing the user with stale data. + * `cache-and-network` allows us to return a cached result right away and then update + * all consumers with the fresh data from the network request. + */ + watchQuery: { + fetchPolicy: 'cache-and-network', + }, + /** + * `client.query()` returns promise, so it can only resolve one response. + * Meaning we cannot return the cached result first and then update it with + * the response from the network as it's done in `client.watchQuery()`. + * So we always need to make a network request to get data unless another + * `fetchPolicy` is specified in the `client.query()` call. + */ + query: { + fetchPolicy: 'network-only', + }, + }, + link: createHttpLink({ + uri: ({ operationName }) => `${uri}?${operationName}`, + headers, + }), + }) + + return Promise.resolve(apolloClient) + } +) diff --git a/client/shared/src/graphql/fromObservableQuery.test.ts b/client/shared/src/graphql/apollo/fromObservableQuery.test.ts similarity index 100% rename from client/shared/src/graphql/fromObservableQuery.test.ts rename to client/shared/src/graphql/apollo/fromObservableQuery.test.ts diff --git a/client/shared/src/graphql/fromObservableQuery.ts b/client/shared/src/graphql/apollo/fromObservableQuery.ts similarity index 100% rename from client/shared/src/graphql/fromObservableQuery.ts rename to client/shared/src/graphql/apollo/fromObservableQuery.ts diff --git a/client/shared/src/graphql/apollo/hooks.ts b/client/shared/src/graphql/apollo/hooks.ts new file mode 100644 index 00000000000..2edb309fca1 --- /dev/null +++ b/client/shared/src/graphql/apollo/hooks.ts @@ -0,0 +1,80 @@ +import { + gql as apolloGql, + useQuery as useApolloQuery, + useMutation as useApolloMutation, + useLazyQuery as useApolloLazyQuery, + DocumentNode, + OperationVariables, + QueryHookOptions, + QueryResult, + MutationHookOptions, + MutationTuple, + QueryTuple, +} from '@apollo/client' +import { useMemo } from 'react' + +type RequestDocument = string | DocumentNode + +/** + * Returns a `DocumentNode` value to support integrations with GraphQL clients that require this. + * + * @param document The GraphQL operation payload + * @returns The created `DocumentNode` + */ +export const getDocumentNode = (document: RequestDocument): DocumentNode => { + if (typeof document === 'string') { + return apolloGql(document) + } + return document +} + +const useDocumentNode = (document: RequestDocument): DocumentNode => + useMemo(() => getDocumentNode(document), [document]) + +/** + * Send a query to GraphQL and respond to updates. + * Wrapper around Apollo `useQuery` that supports `DocumentNode` and `string` types. + * + * @param query GraphQL operation payload. + * @param options Operation variables and request configuration + * @returns GraphQL response + */ +export function useQuery( + query: RequestDocument, + options: QueryHookOptions +): QueryResult { + const documentNode = useDocumentNode(query) + return useApolloQuery(documentNode, options) +} + +/** + * Unlike with `useQuery`, when you call `useLazyQuery`, it does not immediately execute its associated query. + * Wrapper around Apollo `useLazyQuery` that supports `DocumentNode` and `string` types. + * + * @param query GraphQL operation payload. + * @param options Operation variables and request configuration. + * @returns returns a query function in its result tuple that you call whenever you're ready to execute the query. + */ +export function useLazyQuery( + query: RequestDocument, + options: QueryHookOptions +): QueryTuple { + const documentNode = useDocumentNode(query) + return useApolloLazyQuery(documentNode, options) +} + +/** + * Send a mutation to GraphQL and respond to updates. + * Wrapper around Apollo `useMutation` that supports `DocumentNode` and `string` types. + * + * @param mutation GraphQL operation payload. + * @param options Operation variables and request configuration + * @returns GraphQL response + */ +export function useMutation( + mutation: RequestDocument, + options?: MutationHookOptions +): MutationTuple { + const documentNode = useDocumentNode(mutation) + return useApolloMutation(documentNode, options) +} diff --git a/client/shared/src/graphql/apollo/index.ts b/client/shared/src/graphql/apollo/index.ts new file mode 100644 index 00000000000..277bc153f13 --- /dev/null +++ b/client/shared/src/graphql/apollo/index.ts @@ -0,0 +1,3 @@ +export * from './fromObservableQuery' +export * from './client' +export * from './hooks' diff --git a/client/shared/src/graphql/apollo/persistenceMapper.test.ts b/client/shared/src/graphql/apollo/persistenceMapper.test.ts new file mode 100644 index 00000000000..22ee9b145bc --- /dev/null +++ b/client/shared/src/graphql/apollo/persistenceMapper.test.ts @@ -0,0 +1,85 @@ +import { persistenceMapper, ROOT_QUERY_KEY, CacheObject } from './persistenceMapper' + +describe('persistenceMapper', () => { + const userKey = 'User:01' + const settingsKey = 'Settings:01' + + const createStringifiedCache = (rootQuery: Record, references?: Record) => + JSON.stringify({ + [ROOT_QUERY_KEY]: { + __typename: 'query', + ...rootQuery, + }, + ...references, + }) + + const parseCacheString = (cacheString: string) => JSON.parse(cacheString) as CacheObject + + it('does not persist anything if the cache is empty', async () => { + const persistedString = await persistenceMapper(JSON.stringify({})) + + expect(Object.keys(parseCacheString(persistedString))).toEqual([]) + }) + + it('persists only hardcoded queries', async () => { + const persistedString = await persistenceMapper( + createStringifiedCache({ + viewerSettings: { empty: null, data: true }, + extensionRegistry: { data: true }, + shouldNotBePersisted: {}, + }) + ) + + expect(Object.keys(parseCacheString(persistedString).ROOT_QUERY)).not.toContain('shouldNotBePersisted') + }) + + it('persists cache references', async () => { + const persistedString = await persistenceMapper( + createStringifiedCache( + { + viewerSettings: { data: { __ref: userKey } }, + shouldNotBePersisted: {}, + }, + { + [userKey]: { settings: { __ref: settingsKey } }, + [settingsKey]: { data: true }, + } + ) + ) + + expect(Object.keys(parseCacheString(persistedString))).toEqual([ROOT_QUERY_KEY, userKey, settingsKey]) + }) + + it('persists array of cache references', async () => { + const persistedString = await persistenceMapper( + createStringifiedCache( + { + viewerSettings: { data: [{ __ref: userKey }, { __ref: settingsKey }] }, + shouldNotBePersisted: {}, + }, + { + [userKey]: { settings: { __ref: settingsKey } }, + [settingsKey]: { data: true }, + } + ) + ) + + expect(Object.keys(parseCacheString(persistedString))).toEqual([ROOT_QUERY_KEY, userKey, settingsKey]) + }) + + it('persists deeply nested cache references', async () => { + const persistedString = await persistenceMapper( + createStringifiedCache( + { + viewerSettings: { data: { settings: { sourcegraph: { user: { __ref: userKey } } } } }, + shouldNotBePersisted: {}, + }, + { + [userKey]: { data: true }, + } + ) + ) + + expect(Object.keys(parseCacheString(persistedString))).toEqual([ROOT_QUERY_KEY, userKey]) + }) +}) diff --git a/client/shared/src/graphql/apollo/persistenceMapper.ts b/client/shared/src/graphql/apollo/persistenceMapper.ts new file mode 100644 index 00000000000..dbc3486fb2b --- /dev/null +++ b/client/shared/src/graphql/apollo/persistenceMapper.ts @@ -0,0 +1,80 @@ +import { IQuery } from '../schema' + +/** + * Hardcoded names of the queries which will be persisted to the local storage. + * After the implementation of the `persistLink` which will support `@persist` directive + * hardcoded query names will be deprecated. + */ +export const QUERIES_TO_PERSIST: (keyof IQuery)[] = ['viewerSettings', 'extensionRegistry', 'temporarySettings'] +export const ROOT_QUERY_KEY = 'ROOT_QUERY' + +export interface CacheReference { + __ref: string +} + +export interface CacheObject { + ROOT_QUERY: Record + [cacheKey: string]: unknown +} + +// Ensures that we persist data required only for `QUERIES_TO_PERSIST`. Everything else is ignored. +export const persistenceMapper = (data: string): Promise => { + const initialData = JSON.parse(data) as CacheObject + + // If `ROOT_QUERY` cache is empty, return initial data right away. + if (!initialData[ROOT_QUERY_KEY] || Object.keys(initialData[ROOT_QUERY_KEY]).length === 0) { + return Promise.resolve(data) + } + + const dataToPersist: Record = { + [ROOT_QUERY_KEY]: { + __typename: initialData[ROOT_QUERY_KEY].__typename, + }, + } + + function findNestedCacheReferences(entry: unknown): void { + if (!entry) { + return + } + + if (Array.isArray(entry)) { + for (const item of entry) { + findNestedCacheReferences(item) + } + } else if (isCacheReference(entry)) { + const referenceKey = entry.__ref + + dataToPersist[referenceKey] = initialData[referenceKey] + findNestedCacheReferences(initialData[referenceKey]) + } else if (entry && typeof entry === 'object') { + for (const item of Object.values(entry)) { + findNestedCacheReferences(item) + } + } + } + + /** + * Add responses of the specified queries to the result object and + * go through nested fields of the persisted responses and add references used there to the result object. + * + * Example ROOT_QUERY: { viewerSettings: { user: { __ref: 'User:01' } }, 'User:01': { ... } } + * 'User:01' should be persisted, to have a complete cached response to the `viewerSettings` query. + */ + for (const queryName of QUERIES_TO_PERSIST) { + const entryToPersist = initialData[ROOT_QUERY_KEY][queryName] + + if (entryToPersist) { + Object.assign(dataToPersist[ROOT_QUERY_KEY], { + [queryName]: entryToPersist, + }) + + findNestedCacheReferences(entryToPersist) + } + } + + return Promise.resolve(JSON.stringify(dataToPersist)) +} + +function isCacheReference(entry: any): entry is CacheReference { + return Boolean(entry.__ref) +} diff --git a/client/shared/src/graphql/constants.ts b/client/shared/src/graphql/constants.ts new file mode 100644 index 00000000000..4ab045b6509 --- /dev/null +++ b/client/shared/src/graphql/constants.ts @@ -0,0 +1 @@ +export const GRAPHQL_URI = '/.api/graphql' diff --git a/client/shared/src/graphql/graphql.ts b/client/shared/src/graphql/graphql.ts index 474ecd9c7c8..28c9ee022e1 100644 --- a/client/shared/src/graphql/graphql.ts +++ b/client/shared/src/graphql/graphql.ts @@ -1,22 +1,4 @@ -import { - gql as apolloGql, - useQuery as useApolloQuery, - useMutation as useApolloMutation, - useLazyQuery as useApolloLazyQuery, - DocumentNode, - ApolloClient, - createHttpLink, - NormalizedCacheObject, - OperationVariables, - QueryHookOptions, - QueryResult, - MutationHookOptions, - MutationTuple, - QueryTuple, -} from '@apollo/client' import { GraphQLError } from 'graphql' -import { once } from 'lodash' -import { useMemo } from 'react' import { Observable } from 'rxjs' import { fromFetch } from 'rxjs/fetch' import { Omit } from 'utility-types' @@ -24,7 +6,7 @@ import { Omit } from 'utility-types' import { checkOk } from '../backend/fetch' import { createAggregateError } from '../util/errors' -import { cache } from './cache' +import { GRAPHQL_URI } from './constants' /** * Use this template string tag for all GraphQL queries. @@ -67,8 +49,6 @@ export interface GraphQLRequestOptions extends Omit({ }) } -interface GetGraphqlClientOptions { - headers: RequestInit['headers'] - baseUrl?: string -} - -export type GraphQLClient = ApolloClient - -export const getGraphQLClient = once( - async (options: GetGraphqlClientOptions): Promise => { - const { headers, baseUrl } = options - const uri = baseUrl ? new URL(GRAPHQL_URI, baseUrl).href : GRAPHQL_URI - - const apolloClient = new ApolloClient({ - uri, - cache, - defaultOptions: { - /** - * The default `fetchPolicy` is `cache-first`, which returns a cached response - * and doesn't trigger cache update. This is undesirable default behavior because - * we want to keep our cache updated to avoid confusing the user with stale data. - * `cache-and-network` allows us to return a cached result right away and then update - * all consumers with the fresh data from the network request. - */ - watchQuery: { - fetchPolicy: 'cache-and-network', - }, - /** - * `client.query()` returns promise, so it can only resolve one response. - * Meaning we cannot return the cached result first and then update it with - * the response from the network as it's done in `client.watchQuery()`. - * So we always need to make a network request to get data unless another - * `fetchPolicy` is specified in the `client.query()` call. - */ - query: { - fetchPolicy: 'network-only', - }, - }, - link: createHttpLink({ - uri: ({ operationName }) => `${uri}?${operationName}`, - headers, - }), - }) - - return Promise.resolve(apolloClient) - } -) - -type RequestDocument = string | DocumentNode - -/** - * Returns a `DocumentNode` value to support integrations with GraphQL clients that require this. - * - * @param document The GraphQL operation payload - * @returns The created `DocumentNode` - */ -export const getDocumentNode = (document: RequestDocument): DocumentNode => { - if (typeof document === 'string') { - return apolloGql(document) - } - return document -} - -const useDocumentNode = (document: RequestDocument): DocumentNode => - useMemo(() => getDocumentNode(document), [document]) - -/** - * Send a query to GraphQL and respond to updates. - * Wrapper around Apollo `useQuery` that supports `DocumentNode` and `string` types. - * - * @param query GraphQL operation payload. - * @param options Operation variables and request configuration - * @returns GraphQL response - */ -export function useQuery( - query: RequestDocument, - options: QueryHookOptions -): QueryResult { - const documentNode = useDocumentNode(query) - return useApolloQuery(documentNode, options) -} - -export function useLazyQuery( - query: RequestDocument, - options: QueryHookOptions -): QueryTuple { - const documentNode = useDocumentNode(query) - return useApolloLazyQuery(documentNode, options) -} - -/** - * Send a mutation to GraphQL and respond to updates. - * Wrapper around Apollo `useMutation` that supports `DocumentNode` and `string` types. - * - * @param mutation GraphQL operation payload. - * @param options Operation variables and request configuration - * @returns GraphQL response - */ -export function useMutation( - mutation: RequestDocument, - options?: MutationHookOptions -): MutationTuple { - const documentNode = useDocumentNode(mutation) - return useApolloMutation(documentNode, options) -} +export * from './apollo' diff --git a/client/shared/src/testing/apollo/mockedTestProvider.tsx b/client/shared/src/testing/apollo/mockedTestProvider.tsx index 0cc5f8d6036..857eddc2b07 100644 --- a/client/shared/src/testing/apollo/mockedTestProvider.tsx +++ b/client/shared/src/testing/apollo/mockedTestProvider.tsx @@ -1,7 +1,7 @@ import { MockedProvider, MockedProviderProps } from '@apollo/client/testing' import React, { useMemo } from 'react' -import { generateCache } from '../../graphql/cache' +import { generateCache } from '../../graphql/apollo/cache' export const MockedTestProvider: React.FunctionComponent = ({ children, ...props }) => { /** diff --git a/client/shared/src/testing/integration/context.ts b/client/shared/src/testing/integration/context.ts index e6abb70d38b..41940e95e06 100644 --- a/client/shared/src/testing/integration/context.ts +++ b/client/shared/src/testing/integration/context.ts @@ -266,6 +266,7 @@ export const createSharedIntegrationTestContext = async < DISPOSE_ACTION_TIMEOUT, new Error('Recording coverage timed out') ) + await driver.page.evaluate(() => localStorage.clear()) await pTimeout(driver.page.close(), DISPOSE_ACTION_TIMEOUT, new Error('Closing Puppeteer page timed out')) await pTimeout(polly.stop(), DISPOSE_ACTION_TIMEOUT, new Error('Stopping Polly timed out')) }, diff --git a/client/shared/src/testing/integration/mockExtension.ts b/client/shared/src/testing/integration/mockExtension.ts index 99fd37701ac..9e9e2928015 100644 --- a/client/shared/src/testing/integration/mockExtension.ts +++ b/client/shared/src/testing/integration/mockExtension.ts @@ -78,6 +78,7 @@ export function setupExtensionMocking({ // Mutate mock data objects extensionSettings[id] = true extensionsResult.extensionRegistry.extensions.nodes.push({ + id, extensionID: id, manifest: { jsonFields: extensionManifest, diff --git a/client/storybook/src/apollo/MockedStoryProvider.tsx b/client/storybook/src/apollo/MockedStoryProvider.tsx index 31e1150b6ad..286bed3953f 100644 --- a/client/storybook/src/apollo/MockedStoryProvider.tsx +++ b/client/storybook/src/apollo/MockedStoryProvider.tsx @@ -3,11 +3,11 @@ import { MockedProvider, MockedProviderProps, MockedResponse, MockLink } from '@ import { getOperationName } from '@apollo/client/utilities' import React from 'react' -import { cache } from '@sourcegraph/shared/src/graphql/cache' +import { cache } from '@sourcegraph/shared/src/graphql/apollo/cache' /** * Intercept each mocked Apollo request and ensure that any request variables match the specified mock. - * This effectively means we are mocking agains the operationName of the query being fired. + * This effectively means we are mocking against the operationName of the query being fired. */ const forceMockVariablesLink = (mocks: readonly MockedResponse[]): ApolloLink => new ApolloLink((operation, forward) => { diff --git a/client/web/src/backend/graphql.ts b/client/web/src/backend/graphql.ts index 6dbd47c6e2b..dd5f3986bf7 100644 --- a/client/web/src/backend/graphql.ts +++ b/client/web/src/backend/graphql.ts @@ -68,6 +68,7 @@ export const mutateGraphQL = (request: string, variables?: {}): Observable getGraphQLClient({ + isAuthenticated: window.context.isAuthenticatedUser, headers: { ...window?.context?.xhrHeaders, 'X-Sourcegraph-Should-Trace': new URLSearchParams(window.location.search).get('trace') || 'false', diff --git a/client/web/src/integration/blob-viewer.test.ts b/client/web/src/integration/blob-viewer.test.ts index f479b057a40..c60704fd7bc 100644 --- a/client/web/src/integration/blob-viewer.test.ts +++ b/client/web/src/integration/blob-viewer.test.ts @@ -223,6 +223,7 @@ describe('Blob viewer', () => { extensions: { nodes: [ { + id: 'test', extensionID: 'test/test', manifest: { jsonFields: extensionManifest, @@ -292,6 +293,7 @@ describe('Blob viewer', () => { }) interface MockExtension { + id: string extensionID: string extensionManifest: ExtensionManifest /** @@ -308,6 +310,7 @@ describe('Blob viewer', () => { it('adds and clears line decoration attachments properly', async () => { const mockExtensions: MockExtension[] = [ { + id: 'test', extensionID: 'test/fixed-line', extensionManifest: { url: new URL( @@ -369,6 +372,7 @@ describe('Blob viewer', () => { }, }, { + id: 'selected-line', extensionID: 'test/selected-line', extensionManifest: { url: new URL( @@ -616,6 +620,7 @@ describe('Blob viewer', () => { */ const wordFinder: MockExtension = { + id: 'word-finder', extensionID: 'test/word-finder', extensionManifest: { url: new URL( @@ -904,6 +909,7 @@ describe('Blob viewer', () => { extensions: { nodes: [ { + id: 'test', extensionID: 'test/references', manifest: { jsonFields: extensionManifest, diff --git a/client/web/src/integration/extension-registry.test.ts b/client/web/src/integration/extension-registry.test.ts index 6272d58455d..8920b0f5625 100644 --- a/client/web/src/integration/extension-registry.test.ts +++ b/client/web/src/integration/extension-registry.test.ts @@ -93,12 +93,14 @@ const registryExtensionNodes: RegistryExtensionFieldsForList[] = [ const extensionNodes: ExtensionsResult['extensionRegistry']['extensions']['nodes'] = [ { + id: 'typescript', extensionID: 'sourcegraph/typescript', manifest: { jsonFields: typescriptRawManifest, }, }, { + id: 'count', extensionID: 'sqs/word-count', manifest: { jsonFields: wordCountRawManifest, diff --git a/client/web/src/integration/repository.test.ts b/client/web/src/integration/repository.test.ts index df425aa7312..84bc2da3757 100644 --- a/client/web/src/integration/repository.test.ts +++ b/client/web/src/integration/repository.test.ts @@ -752,6 +752,7 @@ describe('Repository', () => { extensions: { nodes: [ { + id: 'test', extensionID: 'test/test', manifest: { jsonFields: extensionManifest, diff --git a/client/web/src/platform/context.ts b/client/web/src/platform/context.ts index 834a48c9799..84c823d8ce9 100644 --- a/client/web/src/platform/context.ts +++ b/client/web/src/platform/context.ts @@ -3,8 +3,7 @@ import { map, publishReplay, refCount, shareReplay } from 'rxjs/operators' import { Tooltip } from '@sourcegraph/branded/src/components/tooltip/Tooltip' import { createExtensionHost } from '@sourcegraph/shared/src/api/extension/worker' -import { fromObservableQueryPromise } from '@sourcegraph/shared/src/graphql/fromObservableQuery' -import { getDocumentNode, gql } from '@sourcegraph/shared/src/graphql/graphql' +import { fromObservableQueryPromise, getDocumentNode, gql } from '@sourcegraph/shared/src/graphql/graphql' import * as GQL from '@sourcegraph/shared/src/graphql/schema' import { PlatformContext } from '@sourcegraph/shared/src/platform/context' import { mutateSettings, updateSettings } from '@sourcegraph/shared/src/settings/edit' diff --git a/client/web/src/settings/temporary/TemporarySettingsStorage.ts b/client/web/src/settings/temporary/TemporarySettingsStorage.ts index 7f5243bae78..12912c95b8f 100644 --- a/client/web/src/settings/temporary/TemporarySettingsStorage.ts +++ b/client/web/src/settings/temporary/TemporarySettingsStorage.ts @@ -3,7 +3,7 @@ import { isEqual } from 'lodash' import { Observable, of, Subscription, from, ReplaySubject, Subscriber } from 'rxjs' import { distinctUntilChanged, map } from 'rxjs/operators' -import { fromObservableQuery } from '@sourcegraph/shared/src/graphql/fromObservableQuery' +import { fromObservableQuery } from '@sourcegraph/shared/src/graphql/graphql' import { GetTemporarySettingsResult } from '../../graphql-operations' diff --git a/package.json b/package.json index cd2e1638476..002b16268c8 100644 --- a/package.json +++ b/package.json @@ -336,6 +336,7 @@ "@visx/pattern": "^1.7.0", "@visx/scale": "^1.7.0", "@visx/xychart": "^1.7.3", + "apollo3-cache-persist": "^0.12.1", "bloomfilter": "^0.0.18", "bootstrap": "^4.5.2", "classnames": "^2.2.6", diff --git a/yarn.lock b/yarn.lock index 171a7804d77..23bf9d15199 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6637,6 +6637,11 @@ anymatch@^2.0.0: micromatch "^3.1.4" normalize-path "^2.1.1" +apollo3-cache-persist@^0.12.1: + version "0.12.1" + resolved "https://registry.npmjs.org/apollo3-cache-persist/-/apollo3-cache-persist-0.12.1.tgz#6641e398cb3285dbc8c6cbdd9ecea3e25bff79c7" + integrity sha512-bYh/OzhzR70URRRf0OkzkpTLB7+fwx1Ftiw8/MWW8NhEgF+K9u3FO786DSUcE6X2NqSObYKF0+CpM9K8vrJNxw== + app-root-dir@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/app-root-dir/-/app-root-dir-1.0.2.tgz#38187ec2dea7577fff033ffcb12172692ff6e118"