mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 15:51:43 +00:00
web: enable apollo-cache-persist (#23351)
This commit is contained in:
parent
b9d8621a1b
commit
d35d3d4458
@ -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 }
|
||||
}
|
||||
|
||||
@ -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 () => {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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: {
|
||||
77
client/shared/src/graphql/apollo/client.ts
Normal file
77
client/shared/src/graphql/apollo/client.ts
Normal file
@ -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<NormalizedCacheObject>
|
||||
|
||||
/**
|
||||
* 🚨 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<GraphQLClient> => {
|
||||
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)
|
||||
}
|
||||
)
|
||||
80
client/shared/src/graphql/apollo/hooks.ts
Normal file
80
client/shared/src/graphql/apollo/hooks.ts
Normal file
@ -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<TData = any, TVariables = OperationVariables>(
|
||||
query: RequestDocument,
|
||||
options: QueryHookOptions<TData, TVariables>
|
||||
): QueryResult<TData, TVariables> {
|
||||
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<TData = any, TVariables = OperationVariables>(
|
||||
query: RequestDocument,
|
||||
options: QueryHookOptions<TData, TVariables>
|
||||
): QueryTuple<TData, TVariables> {
|
||||
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<TData = any, TVariables = OperationVariables>(
|
||||
mutation: RequestDocument,
|
||||
options?: MutationHookOptions<TData, TVariables>
|
||||
): MutationTuple<TData, TVariables> {
|
||||
const documentNode = useDocumentNode(mutation)
|
||||
return useApolloMutation(documentNode, options)
|
||||
}
|
||||
3
client/shared/src/graphql/apollo/index.ts
Normal file
3
client/shared/src/graphql/apollo/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './fromObservableQuery'
|
||||
export * from './client'
|
||||
export * from './hooks'
|
||||
85
client/shared/src/graphql/apollo/persistenceMapper.test.ts
Normal file
85
client/shared/src/graphql/apollo/persistenceMapper.test.ts
Normal file
@ -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<string, unknown>, references?: Record<string, unknown>) =>
|
||||
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])
|
||||
})
|
||||
})
|
||||
80
client/shared/src/graphql/apollo/persistenceMapper.ts
Normal file
80
client/shared/src/graphql/apollo/persistenceMapper.ts
Normal file
@ -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<string, unknown>
|
||||
[cacheKey: string]: unknown
|
||||
}
|
||||
|
||||
// Ensures that we persist data required only for `QUERIES_TO_PERSIST`. Everything else is ignored.
|
||||
export const persistenceMapper = (data: string): Promise<string> => {
|
||||
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<string, unknown> = {
|
||||
[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)
|
||||
}
|
||||
1
client/shared/src/graphql/constants.ts
Normal file
1
client/shared/src/graphql/constants.ts
Normal file
@ -0,0 +1 @@
|
||||
export const GRAPHQL_URI = '/.api/graphql'
|
||||
@ -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<RequestInit, 'method' | 'bod
|
||||
baseUrl?: string
|
||||
}
|
||||
|
||||
const GRAPHQL_URI = '/.api/graphql'
|
||||
|
||||
/**
|
||||
* This function should not be called directly as it does not
|
||||
* add the necessary headers to authorize the GraphQL API call.
|
||||
@ -94,107 +74,4 @@ export function requestGraphQLCommon<T, V = object>({
|
||||
})
|
||||
}
|
||||
|
||||
interface GetGraphqlClientOptions {
|
||||
headers: RequestInit['headers']
|
||||
baseUrl?: string
|
||||
}
|
||||
|
||||
export type GraphQLClient = ApolloClient<NormalizedCacheObject>
|
||||
|
||||
export const getGraphQLClient = once(
|
||||
async (options: GetGraphqlClientOptions): Promise<GraphQLClient> => {
|
||||
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<TData = any, TVariables = OperationVariables>(
|
||||
query: RequestDocument,
|
||||
options: QueryHookOptions<TData, TVariables>
|
||||
): QueryResult<TData, TVariables> {
|
||||
const documentNode = useDocumentNode(query)
|
||||
return useApolloQuery(documentNode, options)
|
||||
}
|
||||
|
||||
export function useLazyQuery<TData = any, TVariables = OperationVariables>(
|
||||
query: RequestDocument,
|
||||
options: QueryHookOptions<TData, TVariables>
|
||||
): QueryTuple<TData, TVariables> {
|
||||
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<TData = any, TVariables = OperationVariables>(
|
||||
mutation: RequestDocument,
|
||||
options?: MutationHookOptions<TData, TVariables>
|
||||
): MutationTuple<TData, TVariables> {
|
||||
const documentNode = useDocumentNode(mutation)
|
||||
return useApolloMutation(documentNode, options)
|
||||
}
|
||||
export * from './apollo'
|
||||
|
||||
@ -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<MockedProviderProps> = ({ children, ...props }) => {
|
||||
/**
|
||||
|
||||
@ -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'))
|
||||
},
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -68,6 +68,7 @@ export const mutateGraphQL = (request: string, variables?: {}): Observable<Graph
|
||||
*/
|
||||
export const getWebGraphQLClient = memoize(() =>
|
||||
getGraphQLClient({
|
||||
isAuthenticated: window.context.isAuthenticatedUser,
|
||||
headers: {
|
||||
...window?.context?.xhrHeaders,
|
||||
'X-Sourcegraph-Should-Trace': new URLSearchParams(window.location.search).get('trace') || 'false',
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -752,6 +752,7 @@ describe('Repository', () => {
|
||||
extensions: {
|
||||
nodes: [
|
||||
{
|
||||
id: 'test',
|
||||
extensionID: 'test/test',
|
||||
manifest: {
|
||||
jsonFields: extensionManifest,
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user