mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-20 03:26:47 +08:00
refactor(core): adjust graphql hook (#5339)
This commit is contained in:
@@ -1,145 +0,0 @@
|
||||
/**
|
||||
* @vitest-environment happy-dom
|
||||
*/
|
||||
import { uploadAvatarMutation } from '@affine/graphql';
|
||||
import { render } from '@testing-library/react';
|
||||
import type { Mock } from 'vitest';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { useMutation, useQuery } from '../gql';
|
||||
|
||||
let fetch: Mock;
|
||||
describe('GraphQL wrapper for SWR', () => {
|
||||
beforeEach(() => {
|
||||
fetch = vi.fn(() =>
|
||||
Promise.resolve(
|
||||
new Response(JSON.stringify({ data: { hello: 1 } }), {
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
vi.stubGlobal('fetch', fetch);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fetch.mockReset();
|
||||
});
|
||||
|
||||
describe('useQuery', () => {
|
||||
const Component = ({ id }: { id: number }) => {
|
||||
const { data, isLoading, error } = useQuery({
|
||||
query: {
|
||||
id: 'query',
|
||||
query: `
|
||||
query {
|
||||
hello
|
||||
}
|
||||
`,
|
||||
operationName: 'query',
|
||||
definitionName: 'query',
|
||||
},
|
||||
// @ts-expect-error forgive the fake variables
|
||||
variables: { id },
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return <div>loading</div>;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <div>error</div>;
|
||||
}
|
||||
|
||||
// @ts-expect-error
|
||||
return <div>number: {data!.hello}</div>;
|
||||
};
|
||||
|
||||
it('should send query correctly', async () => {
|
||||
const component = <Component id={1} />;
|
||||
const renderer = render(component);
|
||||
const el = await renderer.findByText('number: 1');
|
||||
expect(el).toMatchInlineSnapshot(`
|
||||
<div>
|
||||
number:${' '}
|
||||
1
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
|
||||
it('should not send request if cache hit', async () => {
|
||||
const component = <Component id={2} />;
|
||||
const renderer = render(component);
|
||||
expect(fetch).toBeCalledTimes(1);
|
||||
|
||||
renderer.rerender(component);
|
||||
expect(fetch).toBeCalledTimes(1);
|
||||
|
||||
render(<Component id={3} />);
|
||||
|
||||
expect(fetch).toBeCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useMutation', () => {
|
||||
const Component = () => {
|
||||
const { trigger, error, isMutating } = useMutation({
|
||||
mutation: {
|
||||
id: 'mutation',
|
||||
query: `
|
||||
mutation {
|
||||
hello
|
||||
}
|
||||
`,
|
||||
operationName: 'mutation',
|
||||
definitionName: 'mutation',
|
||||
},
|
||||
});
|
||||
|
||||
if (isMutating) {
|
||||
return <div>mutating</div>;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <div>error</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => trigger()}>click</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
it('should trigger mutation', async () => {
|
||||
const component = <Component />;
|
||||
const renderer = render(component);
|
||||
const button = await renderer.findByText('click');
|
||||
|
||||
button.click();
|
||||
expect(fetch).toBeCalledTimes(1);
|
||||
|
||||
renderer.rerender(component);
|
||||
expect(renderer.asFragment()).toMatchInlineSnapshot(`
|
||||
<DocumentFragment>
|
||||
<div>
|
||||
mutating
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`);
|
||||
});
|
||||
|
||||
it('should get rid of generated types', async () => {
|
||||
function _NotActuallyRunDefinedForTypeTesting() {
|
||||
const { trigger } = useMutation({
|
||||
mutation: uploadAvatarMutation,
|
||||
});
|
||||
trigger({
|
||||
avatar: new File([''], 'avatar.png'),
|
||||
});
|
||||
}
|
||||
expect(_NotActuallyRunDefinedForTypeTesting).toBeTypeOf('function');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,216 +0,0 @@
|
||||
import { setupGlobal } from '@affine/env/global';
|
||||
import type {
|
||||
GraphQLQuery,
|
||||
MutationOptions,
|
||||
QueryOptions,
|
||||
QueryResponse,
|
||||
QueryVariables,
|
||||
RecursiveMaybeFields,
|
||||
} from '@affine/graphql';
|
||||
import { gqlFetcherFactory } from '@affine/graphql';
|
||||
import type { GraphQLError } from 'graphql';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import type { Key, SWRConfiguration, SWRResponse } from 'swr';
|
||||
import useSWR, { useSWRConfig } from 'swr';
|
||||
import useSWRImutable from 'swr/immutable';
|
||||
import useSWRInfinite from 'swr/infinite';
|
||||
import type {
|
||||
SWRMutationConfiguration,
|
||||
SWRMutationResponse,
|
||||
} from 'swr/mutation';
|
||||
import useSWRMutation from 'swr/mutation';
|
||||
|
||||
setupGlobal();
|
||||
|
||||
export const fetcher = gqlFetcherFactory('/graphql');
|
||||
|
||||
/**
|
||||
* A `useSWR` wrapper for sending graphql queries
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* ```ts
|
||||
* import { someQuery, someQueryWithNoVars } from '@affine/graphql'
|
||||
*
|
||||
* const swrResponse1 = useQuery({
|
||||
* query: workspaceByIdQuery,
|
||||
* variables: { id: '1' }
|
||||
* })
|
||||
*
|
||||
* const swrResponse2 = useQuery({
|
||||
* query: someQueryWithNoVars
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
type useQueryFn = <Query extends GraphQLQuery>(
|
||||
options?: QueryOptions<Query>,
|
||||
config?: Omit<
|
||||
SWRConfiguration<
|
||||
QueryResponse<Query>,
|
||||
GraphQLError | GraphQLError[],
|
||||
typeof fetcher<Query>
|
||||
>,
|
||||
'fetcher'
|
||||
>
|
||||
) => SWRResponse<
|
||||
QueryResponse<Query>,
|
||||
GraphQLError | GraphQLError[],
|
||||
{
|
||||
suspense: true;
|
||||
}
|
||||
>;
|
||||
|
||||
const createUseQuery =
|
||||
(immutable: boolean): useQueryFn =>
|
||||
(options, config) => {
|
||||
const configWithSuspense: SWRConfiguration = useMemo(
|
||||
() => ({
|
||||
suspense: true,
|
||||
...config,
|
||||
}),
|
||||
[config]
|
||||
);
|
||||
|
||||
const useSWRFn = immutable ? useSWRImutable : useSWR;
|
||||
return useSWRFn(
|
||||
options ? () => ['cloud', options.query.id, options.variables] : null,
|
||||
options ? () => fetcher(options) : null,
|
||||
configWithSuspense
|
||||
);
|
||||
};
|
||||
|
||||
export const useQuery = createUseQuery(false);
|
||||
export const useQueryImmutable = createUseQuery(true);
|
||||
|
||||
export function useQueryInfinite<Query extends GraphQLQuery>(
|
||||
options: Omit<QueryOptions<Query>, 'variables'> & {
|
||||
getVariables: (
|
||||
pageIndex: number,
|
||||
previousPageData: QueryResponse<Query>
|
||||
) => QueryOptions<Query>['variables'];
|
||||
},
|
||||
config?: Omit<
|
||||
SWRConfiguration<
|
||||
QueryResponse<Query>,
|
||||
GraphQLError | GraphQLError[],
|
||||
typeof fetcher<Query>
|
||||
>,
|
||||
'fetcher'
|
||||
>
|
||||
) {
|
||||
const configWithSuspense: SWRConfiguration = useMemo(
|
||||
() => ({
|
||||
suspense: true,
|
||||
...config,
|
||||
}),
|
||||
[config]
|
||||
);
|
||||
|
||||
const { data, setSize, size, error } = useSWRInfinite<
|
||||
QueryResponse<Query>,
|
||||
GraphQLError | GraphQLError[]
|
||||
>(
|
||||
(pageIndex: number, previousPageData: QueryResponse<Query>) => [
|
||||
'cloud',
|
||||
options.query.id,
|
||||
options.getVariables(pageIndex, previousPageData),
|
||||
],
|
||||
async ([_, __, variables]) => {
|
||||
const params = { ...options, variables } as QueryOptions<Query>;
|
||||
return fetcher(params);
|
||||
},
|
||||
configWithSuspense
|
||||
);
|
||||
|
||||
const loadingMore = size > 0 && data && !data[size - 1];
|
||||
|
||||
// todo: find a generic way to know whether or not there are more items to load
|
||||
const loadMore = useCallback(() => {
|
||||
if (loadingMore) {
|
||||
return;
|
||||
}
|
||||
setSize(size => size + 1).catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}, [loadingMore, setSize]);
|
||||
return {
|
||||
data,
|
||||
error,
|
||||
loadingMore,
|
||||
loadMore,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* A useSWRMutation wrapper for sending graphql mutations
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* ```ts
|
||||
* import { someMutation } from '@affine/graphql'
|
||||
*
|
||||
* const { trigger } = useMutation({
|
||||
* mutation: someMutation,
|
||||
* })
|
||||
*
|
||||
* trigger({ name: 'John Doe' })
|
||||
*/
|
||||
export function useMutation<Mutation extends GraphQLQuery, K extends Key = Key>(
|
||||
options: Omit<MutationOptions<Mutation>, 'variables'>,
|
||||
config?: Omit<
|
||||
SWRMutationConfiguration<
|
||||
QueryResponse<Mutation>,
|
||||
GraphQLError | GraphQLError[],
|
||||
K,
|
||||
QueryVariables<Mutation>
|
||||
>,
|
||||
'fetcher'
|
||||
>
|
||||
): SWRMutationResponse<
|
||||
QueryResponse<Mutation>,
|
||||
GraphQLError | GraphQLError[],
|
||||
K,
|
||||
QueryVariables<Mutation>
|
||||
>;
|
||||
export function useMutation(
|
||||
options: Omit<MutationOptions<GraphQLQuery>, 'variables'>,
|
||||
config?: any
|
||||
) {
|
||||
return useSWRMutation(
|
||||
() => ['cloud', options.mutation.id],
|
||||
(_: unknown[], { arg }: { arg: any }) =>
|
||||
fetcher({ ...options, query: options.mutation, variables: arg }),
|
||||
config
|
||||
);
|
||||
}
|
||||
|
||||
export const gql = fetcher;
|
||||
|
||||
// use this to revalidate all queries that match the filter
|
||||
export const useMutateQueryResource = () => {
|
||||
const { mutate } = useSWRConfig();
|
||||
const revalidateResource = useMemo(
|
||||
() =>
|
||||
<Q extends GraphQLQuery>(
|
||||
query: Q,
|
||||
varsFilter: (
|
||||
vars: RecursiveMaybeFields<QueryVariables<Q>>
|
||||
) => boolean = _vars => true
|
||||
) => {
|
||||
return mutate(key => {
|
||||
const res =
|
||||
Array.isArray(key) &&
|
||||
key[0] === 'cloud' &&
|
||||
key[1] === query.id &&
|
||||
varsFilter(key[2]);
|
||||
if (res) {
|
||||
console.debug('revalidate resource', key);
|
||||
}
|
||||
return res;
|
||||
});
|
||||
},
|
||||
[mutate]
|
||||
);
|
||||
|
||||
return revalidateResource;
|
||||
};
|
||||
@@ -5,8 +5,8 @@ import {
|
||||
listBlobsQuery,
|
||||
setBlobMutation,
|
||||
} from '@affine/graphql';
|
||||
import { fetcher } from '@affine/graphql';
|
||||
|
||||
import { fetcher } from '../../affine/gql';
|
||||
import type { BlobStorage } from '../../engine/blob';
|
||||
import { bufferToBlob } from '../../utils/buffer-to-blob';
|
||||
|
||||
|
||||
@@ -4,12 +4,12 @@ import {
|
||||
deleteWorkspaceMutation,
|
||||
getWorkspacesQuery,
|
||||
} from '@affine/graphql';
|
||||
import { fetcher } from '@affine/graphql';
|
||||
import { Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
|
||||
import { difference } from 'lodash-es';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { applyUpdate, encodeStateAsUpdate } from 'yjs';
|
||||
|
||||
import { fetcher } from '../../affine/gql';
|
||||
import { globalBlockSuiteSchema } from '../../global-schema';
|
||||
import type { WorkspaceListProvider } from '../../list';
|
||||
import { createLocalBlobStorage } from '../local/blob';
|
||||
|
||||
Reference in New Issue
Block a user