@ -1,96 +0,0 @@
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { useEffect } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import {
|
||||
mockPageSize,
|
||||
peopleMockWithIdsOnly,
|
||||
query,
|
||||
responseFirstRequest,
|
||||
responseSecondRequest,
|
||||
responseThirdRequest,
|
||||
variablesFirstRequest,
|
||||
variablesSecondRequest,
|
||||
variablesThirdRequest,
|
||||
} from '@/object-record/hooks/__mocks__/useFetchAllRecordIds';
|
||||
import { useFetchAllRecordIds } from '@/object-record/hooks/useFetchAllRecordIds';
|
||||
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
|
||||
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
|
||||
|
||||
const mocks = [
|
||||
{
|
||||
delay: 100,
|
||||
request: {
|
||||
query,
|
||||
variables: variablesFirstRequest,
|
||||
},
|
||||
result: jest.fn(() => ({
|
||||
data: responseFirstRequest,
|
||||
})),
|
||||
},
|
||||
{
|
||||
delay: 100,
|
||||
request: {
|
||||
query,
|
||||
variables: variablesSecondRequest,
|
||||
},
|
||||
result: jest.fn(() => ({
|
||||
data: responseSecondRequest,
|
||||
})),
|
||||
},
|
||||
{
|
||||
delay: 100,
|
||||
request: {
|
||||
query,
|
||||
variables: variablesThirdRequest,
|
||||
},
|
||||
result: jest.fn(() => ({
|
||||
data: responseThirdRequest,
|
||||
})),
|
||||
},
|
||||
];
|
||||
|
||||
const Wrapper = getJestMetadataAndApolloMocksWrapper({
|
||||
apolloMocks: mocks,
|
||||
});
|
||||
|
||||
describe('useFetchAllRecordIds', () => {
|
||||
it('fetches all record ids with fetch more synchronous loop', async () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const [, setObjectMetadataItems] = useRecoilState(
|
||||
objectMetadataItemsState,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setObjectMetadataItems(generatedMockObjectMetadataItems);
|
||||
}, [setObjectMetadataItems]);
|
||||
|
||||
return useFetchAllRecordIds({
|
||||
objectNameSingular: 'person',
|
||||
pageSize: mockPageSize,
|
||||
});
|
||||
},
|
||||
{
|
||||
wrapper: Wrapper,
|
||||
},
|
||||
);
|
||||
|
||||
const { fetchAllRecordIds } = result.current;
|
||||
|
||||
let recordIds: string[] = [];
|
||||
|
||||
await act(async () => {
|
||||
recordIds = await fetchAllRecordIds();
|
||||
});
|
||||
|
||||
expect(mocks[0].result).toHaveBeenCalled();
|
||||
expect(mocks[1].result).toHaveBeenCalled();
|
||||
expect(mocks[2].result).toHaveBeenCalled();
|
||||
|
||||
expect(recordIds).toEqual(
|
||||
peopleMockWithIdsOnly.edges.map((edge) => edge.node.id).slice(0, 6),
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,173 @@
|
||||
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
|
||||
import { act, renderHook, waitFor } from '@testing-library/react';
|
||||
import { expect } from '@storybook/test';
|
||||
import { useLazyFetchAllRecords } from '@/object-record/hooks/useLazyFetchAllRecords';
|
||||
import { MockedResponse } from '@apollo/client/testing';
|
||||
import gql from 'graphql-tag';
|
||||
import { PERSON_FRAGMENT_WITH_DEPTH_ZERO_RELATIONS } from '@/object-record/hooks/__mocks__/personFragments';
|
||||
import { getJestMetadataAndApolloMocksAndActionMenuWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksAndContextStoreWrapper';
|
||||
|
||||
const defaultResponseData = {
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startCursor: '',
|
||||
endCursor: '',
|
||||
},
|
||||
totalCount: 2,
|
||||
};
|
||||
|
||||
const mockPerson = {
|
||||
__typename: 'Person',
|
||||
updatedAt: '2021-08-03T19:20:06.000Z',
|
||||
whatsapp: {
|
||||
primaryPhoneNumber: '+1',
|
||||
primaryPhoneCountryCode: '234-567-890',
|
||||
additionalPhones: [],
|
||||
},
|
||||
linkedinLink: {
|
||||
primaryLinkUrl: 'https://www.linkedin.com',
|
||||
primaryLinkLabel: 'linkedin',
|
||||
secondaryLinks: ['https://www.linkedin.com'],
|
||||
},
|
||||
name: {
|
||||
firstName: 'firstName',
|
||||
lastName: 'lastName',
|
||||
},
|
||||
emails: {
|
||||
primaryEmail: 'email',
|
||||
additionalEmails: [],
|
||||
},
|
||||
position: 'position',
|
||||
createdBy: {
|
||||
source: 'source',
|
||||
workspaceMemberId: '1',
|
||||
name: 'name',
|
||||
},
|
||||
avatarUrl: 'avatarUrl',
|
||||
jobTitle: 'jobTitle',
|
||||
xLink: {
|
||||
primaryLinkUrl: 'https://www.linkedin.com',
|
||||
primaryLinkLabel: 'linkedin',
|
||||
secondaryLinks: ['https://www.linkedin.com'],
|
||||
},
|
||||
performanceRating: 1,
|
||||
createdAt: '2021-08-03T19:20:06.000Z',
|
||||
phones: {
|
||||
primaryPhoneNumber: '+1',
|
||||
primaryPhoneCountryCode: '234-567-890',
|
||||
additionalPhones: [],
|
||||
},
|
||||
id: '123',
|
||||
city: 'city',
|
||||
companyId: '1',
|
||||
intro: 'intro',
|
||||
deletedAt: null,
|
||||
workPreference: 'workPreference',
|
||||
};
|
||||
|
||||
const mock: MockedResponse = {
|
||||
request: {
|
||||
query: gql`
|
||||
query FindManyPeople(
|
||||
$filter: PersonFilterInput
|
||||
$orderBy: [PersonOrderByInput]
|
||||
$lastCursor: String
|
||||
$limit: Int
|
||||
) {
|
||||
people(
|
||||
filter: $filter
|
||||
orderBy: $orderBy
|
||||
first: $limit
|
||||
after: $lastCursor
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
${PERSON_FRAGMENT_WITH_DEPTH_ZERO_RELATIONS}
|
||||
}
|
||||
cursor
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
hasPreviousPage
|
||||
startCursor
|
||||
endCursor
|
||||
}
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
limit: 30,
|
||||
},
|
||||
},
|
||||
result: jest.fn(() => ({
|
||||
data: {
|
||||
people: {
|
||||
...defaultResponseData,
|
||||
edges: [
|
||||
{
|
||||
node: mockPerson,
|
||||
cursor: '1',
|
||||
},
|
||||
{
|
||||
node: mockPerson,
|
||||
cursor: '2',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})),
|
||||
};
|
||||
|
||||
const Wrapper = getJestMetadataAndApolloMocksAndActionMenuWrapper({
|
||||
apolloMocks: [mock],
|
||||
componentInstanceId: 'recordIndexId',
|
||||
contextStoreTargetedRecordsRule: {
|
||||
mode: 'selection',
|
||||
selectedRecordIds: [],
|
||||
},
|
||||
contextStoreCurrentObjectMetadataNameSingular: 'person',
|
||||
});
|
||||
|
||||
describe('useLazyFetchAllRecords', () => {
|
||||
const objectNameSingular = 'person';
|
||||
const objectMetadataItem = generatedMockObjectMetadataItems.find(
|
||||
(item) => item.nameSingular === objectNameSingular,
|
||||
);
|
||||
if (!objectMetadataItem) {
|
||||
throw new Error('Object metadata item not found');
|
||||
}
|
||||
|
||||
it('should handle one single page', async () => {
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useLazyFetchAllRecords({
|
||||
objectNameSingular,
|
||||
limit: 30,
|
||||
}),
|
||||
{
|
||||
wrapper: Wrapper,
|
||||
},
|
||||
);
|
||||
|
||||
let res: any;
|
||||
|
||||
act(() => {
|
||||
res = result.current.fetchAllRecords();
|
||||
});
|
||||
|
||||
expect(result.current.isDownloading).toBe(true);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isDownloading).toBe(false);
|
||||
expect(result.current.progress).toEqual({ displayType: 'number' });
|
||||
});
|
||||
|
||||
expect(result.current.progress).toEqual({ displayType: 'number' });
|
||||
|
||||
const finalResult = await res;
|
||||
|
||||
expect(finalResult).toEqual([mockPerson, mockPerson]);
|
||||
});
|
||||
});
|
||||
@ -1,88 +0,0 @@
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { DEFAULT_QUERY_PAGE_SIZE } from '@/object-record/constants/DefaultQueryPageSize';
|
||||
import { UseFindManyRecordsParams } from '@/object-record/hooks/useFetchMoreRecordsWithPagination';
|
||||
import { useLazyFindManyRecords } from '@/object-record/hooks/useLazyFindManyRecords';
|
||||
import { useCallback } from 'react';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
type UseLazyFetchAllRecordIdsParams<T> = Omit<
|
||||
UseFindManyRecordsParams<T>,
|
||||
'skip'
|
||||
> & { pageSize?: number };
|
||||
|
||||
export const useFetchAllRecordIds = <T>({
|
||||
objectNameSingular,
|
||||
filter,
|
||||
orderBy,
|
||||
pageSize = DEFAULT_QUERY_PAGE_SIZE,
|
||||
}: UseLazyFetchAllRecordIdsParams<T>) => {
|
||||
const { fetchMore, findManyRecords } = useLazyFindManyRecords({
|
||||
objectNameSingular,
|
||||
filter,
|
||||
orderBy,
|
||||
limit: pageSize,
|
||||
recordGqlFields: { id: true },
|
||||
});
|
||||
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const fetchAllRecordIds = useCallback(async () => {
|
||||
if (!isDefined(findManyRecords)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const findManyRecordsDataResult = await findManyRecords();
|
||||
|
||||
const firstQueryResult =
|
||||
findManyRecordsDataResult?.data?.[objectMetadataItem.namePlural];
|
||||
|
||||
const totalCount = firstQueryResult?.totalCount ?? 0;
|
||||
|
||||
const recordsCount = firstQueryResult?.edges.length ?? 0;
|
||||
|
||||
const recordIdSet = new Set(
|
||||
firstQueryResult?.edges?.map((edge) => edge.node.id) ?? [],
|
||||
);
|
||||
|
||||
const remainingCount = totalCount - recordsCount;
|
||||
|
||||
const remainingPages = Math.ceil(remainingCount / pageSize);
|
||||
|
||||
let lastCursor = firstQueryResult?.pageInfo.endCursor ?? null;
|
||||
|
||||
for (let pageIndex = 0; pageIndex < remainingPages; pageIndex++) {
|
||||
if (lastCursor === null) {
|
||||
break;
|
||||
}
|
||||
|
||||
const rawResult = await fetchMore?.({
|
||||
variables: {
|
||||
lastCursor: lastCursor,
|
||||
limit: pageSize,
|
||||
},
|
||||
});
|
||||
|
||||
const fetchMoreResult = rawResult?.data?.[objectMetadataItem.namePlural];
|
||||
|
||||
for (const edge of fetchMoreResult.edges) {
|
||||
recordIdSet.add(edge.node.id);
|
||||
}
|
||||
|
||||
if (fetchMoreResult.pageInfo.hasNextPage === false) {
|
||||
break;
|
||||
}
|
||||
|
||||
lastCursor = fetchMoreResult.pageInfo.endCursor ?? null;
|
||||
}
|
||||
|
||||
const recordIds = Array.from(recordIdSet);
|
||||
|
||||
return recordIds;
|
||||
}, [fetchMore, findManyRecords, objectMetadataItem.namePlural, pageSize]);
|
||||
|
||||
return {
|
||||
fetchAllRecordIds,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,141 @@
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { UseFindManyRecordsParams } from '@/object-record/hooks/useFetchMoreRecordsWithPagination';
|
||||
import { useLazyFindManyRecords } from '@/object-record/hooks/useLazyFindManyRecords';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import { sleep } from '~/utils/sleep';
|
||||
import { DEFAULT_QUERY_PAGE_SIZE } from '@/object-record/constants/DefaultQueryPageSize';
|
||||
|
||||
type UseLazyFetchAllRecordIdsParams<T> = Omit<
|
||||
UseFindManyRecordsParams<T>,
|
||||
'skip'
|
||||
> & {
|
||||
pageSize?: number;
|
||||
delayMs?: number;
|
||||
maximumRequests?: number;
|
||||
};
|
||||
|
||||
type ExportProgress = {
|
||||
exportedRecordCount?: number;
|
||||
totalRecordCount?: number;
|
||||
displayType: 'percentage' | 'number';
|
||||
};
|
||||
|
||||
export const useLazyFetchAllRecords = <T>({
|
||||
objectNameSingular,
|
||||
filter,
|
||||
orderBy,
|
||||
limit = DEFAULT_QUERY_PAGE_SIZE,
|
||||
delayMs = 0,
|
||||
maximumRequests = 100,
|
||||
recordGqlFields,
|
||||
}: UseLazyFetchAllRecordIdsParams<T>) => {
|
||||
const [isDownloading, setIsDownloading] = useState(false);
|
||||
const [progress, setProgress] = useState<ExportProgress>({
|
||||
displayType: 'number',
|
||||
});
|
||||
const { fetchMore, findManyRecords } = useLazyFindManyRecords({
|
||||
objectNameSingular,
|
||||
filter,
|
||||
orderBy,
|
||||
limit,
|
||||
recordGqlFields,
|
||||
});
|
||||
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const fetchAllRecords = useCallback(async () => {
|
||||
if (!isDefined(findManyRecords)) {
|
||||
return [];
|
||||
}
|
||||
setIsDownloading(true);
|
||||
|
||||
const findManyRecordsDataResult = await findManyRecords();
|
||||
|
||||
const firstQueryResult =
|
||||
findManyRecordsDataResult?.data?.[objectMetadataItem.namePlural];
|
||||
|
||||
const totalCount = firstQueryResult?.totalCount ?? 0;
|
||||
|
||||
const recordsCount = firstQueryResult?.edges.length ?? 0;
|
||||
|
||||
const records = firstQueryResult?.edges?.map((edge) => edge.node) ?? [];
|
||||
|
||||
setProgress({
|
||||
exportedRecordCount: recordsCount,
|
||||
totalRecordCount: totalCount,
|
||||
displayType: totalCount ? 'percentage' : 'number',
|
||||
});
|
||||
|
||||
const remainingCount = totalCount - recordsCount;
|
||||
|
||||
const remainingPages = Math.ceil(remainingCount / limit);
|
||||
|
||||
let lastCursor = firstQueryResult?.pageInfo.endCursor ?? null;
|
||||
|
||||
for (
|
||||
let pageIndex = 0;
|
||||
pageIndex < Math.min(maximumRequests, remainingPages);
|
||||
pageIndex++
|
||||
) {
|
||||
if (lastCursor === null) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (!isDefined(fetchMore)) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (delayMs > 0) {
|
||||
await sleep(delayMs);
|
||||
}
|
||||
|
||||
const rawResult = await fetchMore({
|
||||
variables: {
|
||||
lastCursor: lastCursor,
|
||||
limit,
|
||||
},
|
||||
});
|
||||
|
||||
const fetchMoreResult = rawResult?.data?.[objectMetadataItem.namePlural];
|
||||
|
||||
for (const edge of fetchMoreResult.edges) {
|
||||
records.push(edge.node);
|
||||
}
|
||||
|
||||
setProgress({
|
||||
exportedRecordCount: records.length,
|
||||
totalRecordCount: totalCount,
|
||||
displayType: totalCount ? 'percentage' : 'number',
|
||||
});
|
||||
|
||||
if (fetchMoreResult.pageInfo.hasNextPage === false) {
|
||||
break;
|
||||
}
|
||||
|
||||
lastCursor = fetchMoreResult.pageInfo.endCursor ?? null;
|
||||
}
|
||||
|
||||
setIsDownloading(false);
|
||||
setProgress({
|
||||
displayType: 'number',
|
||||
});
|
||||
|
||||
return records;
|
||||
}, [
|
||||
delayMs,
|
||||
fetchMore,
|
||||
findManyRecords,
|
||||
objectMetadataItem.namePlural,
|
||||
limit,
|
||||
maximumRequests,
|
||||
]);
|
||||
|
||||
return {
|
||||
progress,
|
||||
isDownloading,
|
||||
fetchAllRecords,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user