Implement query variables in useCombinedFindManyRecords (#10015)
Implements filtering, ordering and cursor filtering for the hook useCombinedFindManyRecords, because it was not implemented, which was misleading because variables could be passed to it. The difficult part was to make sure that the cursor filtering was working, both before and after a cursor, because it was only hard coded for last cursor (equivalent to after). The duplicate limit parameter in the type RecordGqlOperationVariables was merged into one limit parameter, because it was making the developer guess how both could be handled. This single limit parameter can be used for either : general limit without cursor, first records from after cursor, last records until before cursor. Since those cases are exclusive it's better to have only one limit parameter and have an internal logic handling those cases. Tests were added on the relevant parts, especially useCombinedFindManyRecordsQueryVariables which requires its own unit test to handle this cursor + limit logic. Record show page pagination was tested to make sure removing the duplicate limit parameter had no impact.
This commit is contained in:
@ -9,6 +9,5 @@ export type RecordGqlOperationVariables = {
|
||||
cursorFilter?: {
|
||||
cursor: string;
|
||||
cursorDirection: QueryCursorDirection;
|
||||
limit: number;
|
||||
};
|
||||
};
|
||||
|
||||
@ -82,7 +82,7 @@ export const useFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
|
||||
: {}),
|
||||
orderBy,
|
||||
lastCursor: cursorFilter?.cursor ?? undefined,
|
||||
limit: cursorFilter?.limit ?? limit,
|
||||
limit,
|
||||
},
|
||||
fetchPolicy: fetchPolicy,
|
||||
onCompleted: handleFindManyRecordsCompleted,
|
||||
|
||||
@ -0,0 +1,570 @@
|
||||
import { gql } from '@apollo/client';
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { RecordGqlFields } from '@/object-record/graphql/types/RecordGqlFields';
|
||||
import { RecordGqlOperationSignature } from '@/object-record/graphql/types/RecordGqlOperationSignature';
|
||||
import { useCombinedFindManyRecords } from '@/object-record/multiple-objects/hooks/useCombinedFindManyRecords';
|
||||
import { useGenerateCombinedFindManyRecordsQuery } from '@/object-record/multiple-objects/hooks/useGenerateCombinedFindManyRecordsQuery';
|
||||
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
|
||||
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
|
||||
|
||||
jest.mock(
|
||||
'@/object-record/multiple-objects/hooks/useGenerateCombinedFindManyRecordsQuery',
|
||||
() => ({
|
||||
useGenerateCombinedFindManyRecordsQuery: jest.fn(),
|
||||
}),
|
||||
);
|
||||
|
||||
const mockQuery = gql`
|
||||
query CombinedFindManyRecords(
|
||||
$filterPerson: PersonFilterInput
|
||||
$filterCompany: CompanyFilterInput
|
||||
$orderByPerson: [PersonOrderByInput]
|
||||
$orderByCompany: [CompanyOrderByInput]
|
||||
$firstPerson: Int
|
||||
$lastPerson: Int
|
||||
$afterPerson: String
|
||||
$beforePerson: String
|
||||
$firstCompany: Int
|
||||
$lastCompany: Int
|
||||
$afterCompany: String
|
||||
$beforeCompany: String
|
||||
$limitPerson: Int
|
||||
$limitCompany: Int
|
||||
) {
|
||||
people(
|
||||
filter: $filterPerson
|
||||
orderBy: $orderByPerson
|
||||
first: $firstPerson
|
||||
after: $afterPerson
|
||||
last: $lastPerson
|
||||
before: $beforePerson
|
||||
limit: $limitPerson
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
__typename
|
||||
id
|
||||
name {
|
||||
firstName
|
||||
lastName
|
||||
}
|
||||
}
|
||||
cursor
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
hasPreviousPage
|
||||
startCursor
|
||||
endCursor
|
||||
}
|
||||
totalCount
|
||||
}
|
||||
companies(
|
||||
filter: $filterCompany
|
||||
orderBy: $orderByCompany
|
||||
first: $firstCompany
|
||||
after: $afterCompany
|
||||
last: $lastCompany
|
||||
before: $beforeCompany
|
||||
limit: $limitCompany
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
__typename
|
||||
id
|
||||
name
|
||||
}
|
||||
cursor
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
hasPreviousPage
|
||||
startCursor
|
||||
endCursor
|
||||
}
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
type RenderUseCombinedFindManyRecordsHookParams = {
|
||||
operationSignatures: RecordGqlOperationSignature[];
|
||||
mockVariables?: Record<string, any>;
|
||||
mockResponseData?: Record<string, any>;
|
||||
skip?: boolean;
|
||||
expectedResult?: Record<string, any>;
|
||||
mockQueryResult?: any;
|
||||
};
|
||||
|
||||
const renderUseCombinedFindManyRecordsHook = async ({
|
||||
operationSignatures,
|
||||
mockVariables = {},
|
||||
mockResponseData,
|
||||
skip = false,
|
||||
expectedResult = {},
|
||||
mockQueryResult = mockQuery,
|
||||
}: RenderUseCombinedFindManyRecordsHookParams) => {
|
||||
(useGenerateCombinedFindManyRecordsQuery as jest.Mock).mockReturnValue(
|
||||
mockQueryResult,
|
||||
);
|
||||
|
||||
const mocks = [
|
||||
{
|
||||
request: {
|
||||
query: mockQuery,
|
||||
variables: mockVariables,
|
||||
},
|
||||
result: {
|
||||
data: mockResponseData,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const setObjectMetadataItems = useSetRecoilState(
|
||||
objectMetadataItemsState,
|
||||
);
|
||||
setObjectMetadataItems(generatedMockObjectMetadataItems);
|
||||
|
||||
return useCombinedFindManyRecords({
|
||||
operationSignatures,
|
||||
skip,
|
||||
});
|
||||
},
|
||||
{
|
||||
wrapper: getJestMetadataAndApolloMocksWrapper({ apolloMocks: mocks }),
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.current.loading).toBe(!skip);
|
||||
expect(result.current.result).toEqual({});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
|
||||
expect(result.current.result).toEqual(expectedResult);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
describe('useCombinedFindManyRecords', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return records for multiple objects', async () => {
|
||||
const mockResponseData = {
|
||||
people: {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
__typename: 'Person',
|
||||
id: '1',
|
||||
name: {
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
},
|
||||
},
|
||||
cursor: 'cursor1',
|
||||
},
|
||||
],
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startCursor: 'cursor1',
|
||||
endCursor: 'cursor1',
|
||||
},
|
||||
totalCount: 1,
|
||||
},
|
||||
companies: {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
__typename: 'Company',
|
||||
id: '1',
|
||||
name: 'Twenty',
|
||||
},
|
||||
cursor: 'cursor1',
|
||||
},
|
||||
],
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startCursor: 'cursor1',
|
||||
endCursor: 'cursor1',
|
||||
},
|
||||
totalCount: 1,
|
||||
},
|
||||
};
|
||||
|
||||
await renderUseCombinedFindManyRecordsHook({
|
||||
operationSignatures: [
|
||||
{
|
||||
objectNameSingular: 'person',
|
||||
fields: {
|
||||
id: true,
|
||||
name: {
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
},
|
||||
} as RecordGqlFields,
|
||||
variables: {},
|
||||
},
|
||||
{
|
||||
objectNameSingular: 'company',
|
||||
fields: {
|
||||
id: true,
|
||||
name: true,
|
||||
} as RecordGqlFields,
|
||||
variables: {},
|
||||
},
|
||||
],
|
||||
mockResponseData,
|
||||
expectedResult: {
|
||||
people: [
|
||||
{
|
||||
__typename: 'Person',
|
||||
id: '1',
|
||||
name: {
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
},
|
||||
},
|
||||
],
|
||||
companies: [
|
||||
{
|
||||
__typename: 'Company',
|
||||
id: '1',
|
||||
name: 'Twenty',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle forward pagination with after cursor and first limit', async () => {
|
||||
const mockResponseData = {
|
||||
people: {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
__typename: 'Person',
|
||||
id: '1',
|
||||
name: {
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
},
|
||||
},
|
||||
cursor: 'cursor1',
|
||||
},
|
||||
],
|
||||
pageInfo: {
|
||||
hasNextPage: true,
|
||||
hasPreviousPage: true,
|
||||
startCursor: 'cursor1',
|
||||
endCursor: 'cursor1',
|
||||
},
|
||||
totalCount: 10,
|
||||
},
|
||||
};
|
||||
|
||||
await renderUseCombinedFindManyRecordsHook({
|
||||
operationSignatures: [
|
||||
{
|
||||
objectNameSingular: 'person',
|
||||
fields: {
|
||||
id: true,
|
||||
name: {
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
},
|
||||
} as RecordGqlFields,
|
||||
variables: {
|
||||
limit: 1,
|
||||
cursorFilter: {
|
||||
cursor: 'previousCursor',
|
||||
cursorDirection: 'after',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
mockVariables: {
|
||||
firstPerson: 1,
|
||||
afterPerson: 'previousCursor',
|
||||
},
|
||||
mockResponseData,
|
||||
expectedResult: {
|
||||
people: [
|
||||
{
|
||||
__typename: 'Person',
|
||||
id: '1',
|
||||
name: {
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle backward pagination with before cursor and last limit', async () => {
|
||||
const mockResponseData = {
|
||||
people: {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
__typename: 'Person',
|
||||
id: '2',
|
||||
name: {
|
||||
firstName: 'Jane',
|
||||
lastName: 'Smith',
|
||||
},
|
||||
},
|
||||
cursor: 'cursor2',
|
||||
},
|
||||
],
|
||||
pageInfo: {
|
||||
hasNextPage: true,
|
||||
hasPreviousPage: true,
|
||||
startCursor: 'cursor2',
|
||||
endCursor: 'cursor2',
|
||||
},
|
||||
totalCount: 10,
|
||||
},
|
||||
};
|
||||
|
||||
await renderUseCombinedFindManyRecordsHook({
|
||||
operationSignatures: [
|
||||
{
|
||||
objectNameSingular: 'person',
|
||||
fields: {
|
||||
id: true,
|
||||
name: {
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
},
|
||||
} as RecordGqlFields,
|
||||
variables: {
|
||||
limit: 1,
|
||||
cursorFilter: {
|
||||
cursor: 'nextCursor',
|
||||
cursorDirection: 'before',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
mockVariables: {
|
||||
lastPerson: 1,
|
||||
beforePerson: 'nextCursor',
|
||||
},
|
||||
mockResponseData,
|
||||
expectedResult: {
|
||||
people: [
|
||||
{
|
||||
__typename: 'Person',
|
||||
id: '2',
|
||||
name: {
|
||||
firstName: 'Jane',
|
||||
lastName: 'Smith',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle limit-based pagination without cursor', async () => {
|
||||
const mockResponseData = {
|
||||
people: {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
__typename: 'Person',
|
||||
id: '3',
|
||||
name: {
|
||||
firstName: 'Alice',
|
||||
lastName: 'Johnson',
|
||||
},
|
||||
},
|
||||
cursor: 'cursor3',
|
||||
},
|
||||
],
|
||||
pageInfo: {
|
||||
hasNextPage: true,
|
||||
hasPreviousPage: false,
|
||||
startCursor: 'cursor3',
|
||||
endCursor: 'cursor3',
|
||||
},
|
||||
totalCount: 10,
|
||||
},
|
||||
};
|
||||
|
||||
await renderUseCombinedFindManyRecordsHook({
|
||||
operationSignatures: [
|
||||
{
|
||||
objectNameSingular: 'person',
|
||||
fields: {
|
||||
id: true,
|
||||
name: {
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
},
|
||||
} as RecordGqlFields,
|
||||
variables: {
|
||||
limit: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
mockVariables: {
|
||||
limitPerson: 1,
|
||||
},
|
||||
mockResponseData,
|
||||
expectedResult: {
|
||||
people: [
|
||||
{
|
||||
__typename: 'Person',
|
||||
id: '3',
|
||||
name: {
|
||||
firstName: 'Alice',
|
||||
lastName: 'Johnson',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle multiple objects with different pagination strategies', async () => {
|
||||
const mockResponseData = {
|
||||
people: {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
__typename: 'Person',
|
||||
id: '1',
|
||||
name: {
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
},
|
||||
},
|
||||
cursor: 'cursor1',
|
||||
},
|
||||
],
|
||||
pageInfo: {
|
||||
hasNextPage: true,
|
||||
hasPreviousPage: false,
|
||||
startCursor: 'cursor1',
|
||||
endCursor: 'cursor1',
|
||||
},
|
||||
totalCount: 10,
|
||||
},
|
||||
companies: {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
__typename: 'Company',
|
||||
id: '1',
|
||||
name: 'Twenty',
|
||||
},
|
||||
cursor: 'cursor1',
|
||||
},
|
||||
],
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startCursor: 'cursor1',
|
||||
endCursor: 'cursor1',
|
||||
},
|
||||
totalCount: 1,
|
||||
},
|
||||
};
|
||||
|
||||
await renderUseCombinedFindManyRecordsHook({
|
||||
operationSignatures: [
|
||||
{
|
||||
objectNameSingular: 'person',
|
||||
fields: {
|
||||
id: true,
|
||||
name: {
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
},
|
||||
} as RecordGqlFields,
|
||||
variables: {
|
||||
limit: 1,
|
||||
cursorFilter: {
|
||||
cursor: 'previousCursor',
|
||||
cursorDirection: 'after',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
objectNameSingular: 'company',
|
||||
fields: {
|
||||
id: true,
|
||||
name: true,
|
||||
} as RecordGqlFields,
|
||||
variables: {
|
||||
limit: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
mockVariables: {
|
||||
firstPerson: 1,
|
||||
afterPerson: 'previousCursor',
|
||||
limitCompany: 1,
|
||||
},
|
||||
mockResponseData,
|
||||
expectedResult: {
|
||||
people: [
|
||||
{
|
||||
__typename: 'Person',
|
||||
id: '1',
|
||||
name: {
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
},
|
||||
},
|
||||
],
|
||||
companies: [
|
||||
{
|
||||
__typename: 'Company',
|
||||
id: '1',
|
||||
name: 'Twenty',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle empty operation signatures', async () => {
|
||||
await renderUseCombinedFindManyRecordsHook({
|
||||
operationSignatures: [],
|
||||
mockResponseData: {},
|
||||
expectedResult: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle skip flag', async () => {
|
||||
await renderUseCombinedFindManyRecordsHook({
|
||||
operationSignatures: [
|
||||
{
|
||||
objectNameSingular: 'person',
|
||||
fields: {
|
||||
id: true,
|
||||
} as RecordGqlFields,
|
||||
variables: {},
|
||||
},
|
||||
],
|
||||
skip: true,
|
||||
mockResponseData: {},
|
||||
expectedResult: {},
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,191 @@
|
||||
import { RecordGqlFields } from '@/object-record/graphql/types/RecordGqlFields';
|
||||
import { RecordGqlOperationSignature } from '@/object-record/graphql/types/RecordGqlOperationSignature';
|
||||
import { useCombinedFindManyRecordsQueryVariables } from '@/object-record/multiple-objects/hooks/useCombinedFindManyRecordsQueryVariables';
|
||||
|
||||
describe('useCombinedFindManyRecordsQueryVariables', () => {
|
||||
it('should generate variables with after cursor and first limit', () => {
|
||||
const operationSignatures: RecordGqlOperationSignature[] = [
|
||||
{
|
||||
objectNameSingular: 'person',
|
||||
fields: {
|
||||
id: true,
|
||||
name: {
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
},
|
||||
} as RecordGqlFields,
|
||||
variables: {
|
||||
filter: { id: { eq: '123' } },
|
||||
orderBy: [{ createdAt: 'AscNullsLast' }],
|
||||
limit: 10,
|
||||
cursorFilter: {
|
||||
cursor: 'cursor123',
|
||||
cursorDirection: 'after',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const result = useCombinedFindManyRecordsQueryVariables({
|
||||
operationSignatures,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
filterPerson: { id: { eq: '123' } },
|
||||
orderByPerson: [{ createdAt: 'AscNullsLast' }],
|
||||
afterPerson: 'cursor123',
|
||||
firstPerson: 10,
|
||||
});
|
||||
});
|
||||
|
||||
it('should generate variables with before cursor and last limit', () => {
|
||||
const operationSignatures: RecordGqlOperationSignature[] = [
|
||||
{
|
||||
objectNameSingular: 'person',
|
||||
fields: {
|
||||
id: true,
|
||||
name: true,
|
||||
} as RecordGqlFields,
|
||||
variables: {
|
||||
filter: { id: { eq: '123' } },
|
||||
orderBy: [{ createdAt: 'AscNullsLast' }],
|
||||
limit: 10,
|
||||
cursorFilter: {
|
||||
cursor: 'cursor123',
|
||||
cursorDirection: 'before',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const result = useCombinedFindManyRecordsQueryVariables({
|
||||
operationSignatures,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
filterPerson: { id: { eq: '123' } },
|
||||
orderByPerson: [{ createdAt: 'AscNullsLast' }],
|
||||
beforePerson: 'cursor123',
|
||||
lastPerson: 10,
|
||||
});
|
||||
});
|
||||
|
||||
it('should generate variables with limit only (no cursor)', () => {
|
||||
const operationSignatures: RecordGqlOperationSignature[] = [
|
||||
{
|
||||
objectNameSingular: 'person',
|
||||
fields: {
|
||||
id: true,
|
||||
name: true,
|
||||
} as RecordGqlFields,
|
||||
variables: {
|
||||
filter: { id: { eq: '123' } },
|
||||
orderBy: [{ createdAt: 'AscNullsLast' }],
|
||||
limit: 10,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const result = useCombinedFindManyRecordsQueryVariables({
|
||||
operationSignatures,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
filterPerson: { id: { eq: '123' } },
|
||||
orderByPerson: [{ createdAt: 'AscNullsLast' }],
|
||||
limitPerson: 10,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle multiple objects with different pagination strategies', () => {
|
||||
const operationSignatures: RecordGqlOperationSignature[] = [
|
||||
{
|
||||
objectNameSingular: 'person',
|
||||
fields: {
|
||||
id: true,
|
||||
} as RecordGqlFields,
|
||||
variables: {
|
||||
filter: { id: { eq: '123' } },
|
||||
limit: 10,
|
||||
cursorFilter: {
|
||||
cursor: 'cursor123',
|
||||
cursorDirection: 'after',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
objectNameSingular: 'company',
|
||||
fields: {
|
||||
id: true,
|
||||
} as RecordGqlFields,
|
||||
variables: {
|
||||
filter: { name: { eq: 'Twenty' } },
|
||||
limit: 20,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const result = useCombinedFindManyRecordsQueryVariables({
|
||||
operationSignatures,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
filterPerson: { id: { eq: '123' } },
|
||||
afterPerson: 'cursor123',
|
||||
firstPerson: 10,
|
||||
filterCompany: { name: { eq: 'Twenty' } },
|
||||
limitCompany: 20,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle empty operation signatures', () => {
|
||||
const result = useCombinedFindManyRecordsQueryVariables({
|
||||
operationSignatures: [],
|
||||
});
|
||||
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it('should handle empty variables', () => {
|
||||
const operationSignatures: RecordGqlOperationSignature[] = [
|
||||
{
|
||||
objectNameSingular: 'person',
|
||||
fields: {
|
||||
id: true,
|
||||
} as RecordGqlFields,
|
||||
variables: {},
|
||||
},
|
||||
];
|
||||
|
||||
const result = useCombinedFindManyRecordsQueryVariables({
|
||||
operationSignatures,
|
||||
});
|
||||
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it('should handle cursor without limit', () => {
|
||||
const operationSignatures: RecordGqlOperationSignature[] = [
|
||||
{
|
||||
objectNameSingular: 'person',
|
||||
fields: {
|
||||
id: true,
|
||||
} as RecordGqlFields,
|
||||
variables: {
|
||||
cursorFilter: {
|
||||
cursor: 'cursor123',
|
||||
cursorDirection: 'after',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const result = useCombinedFindManyRecordsQueryVariables({
|
||||
operationSignatures,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
afterPerson: 'cursor123',
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -3,6 +3,7 @@ import { useQuery } from '@apollo/client';
|
||||
import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection';
|
||||
import { EMPTY_QUERY } from '@/object-record/constants/EmptyQuery';
|
||||
import { RecordGqlOperationSignature } from '@/object-record/graphql/types/RecordGqlOperationSignature';
|
||||
import { useCombinedFindManyRecordsQueryVariables } from '@/object-record/multiple-objects/hooks/useCombinedFindManyRecordsQueryVariables';
|
||||
import { useGenerateCombinedFindManyRecordsQuery } from '@/object-record/multiple-objects/hooks/useGenerateCombinedFindManyRecordsQuery';
|
||||
import { MultiObjectRecordQueryResult } from '@/object-record/relation-picker/hooks/useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray';
|
||||
|
||||
@ -17,10 +18,15 @@ export const useCombinedFindManyRecords = ({
|
||||
operationSignatures,
|
||||
});
|
||||
|
||||
const queryVariables = useCombinedFindManyRecordsQueryVariables({
|
||||
operationSignatures,
|
||||
});
|
||||
|
||||
const { data, loading } = useQuery<MultiObjectRecordQueryResult>(
|
||||
findManyQuery ?? EMPTY_QUERY,
|
||||
{
|
||||
skip,
|
||||
variables: queryVariables,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@ -0,0 +1,75 @@
|
||||
import { RecordGqlOperationSignature } from '@/object-record/graphql/types/RecordGqlOperationSignature';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { capitalize, isDefined } from 'twenty-shared';
|
||||
import { isNonEmptyArray } from '~/utils/isNonEmptyArray';
|
||||
|
||||
export const useCombinedFindManyRecordsQueryVariables = ({
|
||||
operationSignatures,
|
||||
}: {
|
||||
operationSignatures: RecordGqlOperationSignature[];
|
||||
}) => {
|
||||
if (!isNonEmptyArray(operationSignatures)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return operationSignatures.reduce(
|
||||
(acc, { objectNameSingular, variables }) => {
|
||||
const capitalizedName = capitalize(objectNameSingular);
|
||||
|
||||
const filter = isDefined(variables?.filter)
|
||||
? { [`filter${capitalizedName}`]: variables.filter }
|
||||
: {};
|
||||
|
||||
const orderBy = isDefined(variables?.orderBy)
|
||||
? { [`orderBy${capitalizedName}`]: variables.orderBy }
|
||||
: {};
|
||||
|
||||
let limit = {};
|
||||
|
||||
const hasLimit = isDefined(variables.limit) && variables.limit > 0;
|
||||
|
||||
const cursorDirection = variables.cursorFilter?.cursorDirection;
|
||||
|
||||
let cursorFilter = {};
|
||||
|
||||
if (isNonEmptyString(variables.cursorFilter?.cursor)) {
|
||||
if (cursorDirection === 'after') {
|
||||
cursorFilter = {
|
||||
[`after${capitalizedName}`]: variables.cursorFilter?.cursor,
|
||||
};
|
||||
|
||||
if (hasLimit) {
|
||||
cursorFilter = {
|
||||
...cursorFilter,
|
||||
[`first${capitalizedName}`]: variables.limit,
|
||||
};
|
||||
}
|
||||
} else if (cursorDirection === 'before') {
|
||||
cursorFilter = {
|
||||
[`before${capitalizedName}`]: variables.cursorFilter?.cursor,
|
||||
};
|
||||
|
||||
if (hasLimit) {
|
||||
cursorFilter = {
|
||||
...cursorFilter,
|
||||
[`last${capitalizedName}`]: variables.limit,
|
||||
};
|
||||
}
|
||||
}
|
||||
} else if (hasLimit) {
|
||||
limit = {
|
||||
[`limit${capitalizedName}`]: variables.limit,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...acc,
|
||||
...filter,
|
||||
...orderBy,
|
||||
...limit,
|
||||
...cursorFilter,
|
||||
};
|
||||
},
|
||||
{},
|
||||
);
|
||||
};
|
||||
@ -6,6 +6,7 @@ import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadat
|
||||
import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery';
|
||||
import { RecordGqlOperationSignature } from '@/object-record/graphql/types/RecordGqlOperationSignature';
|
||||
import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields';
|
||||
import { getCombinedFindManyRecordsQueryFilteringPart } from '@/object-record/multiple-objects/utils/getCombinedFindManyRecordsQueryFilteringPart';
|
||||
import { capitalize } from 'twenty-shared';
|
||||
import { isNonEmptyArray } from '~/utils/isNonEmptyArray';
|
||||
|
||||
@ -38,10 +39,10 @@ export const useGenerateCombinedFindManyRecordsQuery = ({
|
||||
)
|
||||
.join(', ');
|
||||
|
||||
const lastCursorPerMetadataItemArray = operationSignatures
|
||||
const cursorFilteringPerMetadataItemArray = operationSignatures
|
||||
.map(
|
||||
({ objectNameSingular }) =>
|
||||
`$lastCursor${capitalize(objectNameSingular)}: String`,
|
||||
`$after${capitalize(objectNameSingular)}: String, $before${capitalize(objectNameSingular)}: String, $first${capitalize(objectNameSingular)}: Int, $last${capitalize(objectNameSingular)}: Int`,
|
||||
)
|
||||
.join(', ');
|
||||
|
||||
@ -52,48 +53,42 @@ export const useGenerateCombinedFindManyRecordsQuery = ({
|
||||
)
|
||||
.join(', ');
|
||||
|
||||
const queryKeyWithObjectMetadataItemArray = operationSignatures.map(
|
||||
(queryKey) => {
|
||||
const queryOperationSignatureWithObjectMetadataItemArray =
|
||||
operationSignatures.map((operationSignature) => {
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(objectMetadataItem) =>
|
||||
objectMetadataItem.nameSingular === queryKey.objectNameSingular,
|
||||
objectMetadataItem.nameSingular ===
|
||||
operationSignature.objectNameSingular,
|
||||
);
|
||||
|
||||
if (isUndefined(objectMetadataItem)) {
|
||||
throw new Error(
|
||||
`Object metadata item not found for object name singular: ${queryKey.objectNameSingular}`,
|
||||
`Object metadata item not found for object name singular: ${operationSignature.objectNameSingular}`,
|
||||
);
|
||||
}
|
||||
|
||||
return { ...queryKey, objectMetadataItem };
|
||||
},
|
||||
);
|
||||
return { operationSignature, objectMetadataItem };
|
||||
});
|
||||
|
||||
return gql`
|
||||
query CombinedFindManyRecords(
|
||||
${filterPerMetadataItemArray},
|
||||
${orderByPerMetadataItemArray},
|
||||
${lastCursorPerMetadataItemArray},
|
||||
${cursorFilteringPerMetadataItemArray},
|
||||
${limitPerMetadataItemArray}
|
||||
) {
|
||||
${queryKeyWithObjectMetadataItemArray
|
||||
${queryOperationSignatureWithObjectMetadataItemArray
|
||||
.map(
|
||||
({ objectMetadataItem, fields }) =>
|
||||
`${objectMetadataItem.namePlural}(filter: $filter${capitalize(
|
||||
objectMetadataItem.nameSingular,
|
||||
)}, orderBy: $orderBy${capitalize(
|
||||
objectMetadataItem.nameSingular,
|
||||
)}, first: $limit${capitalize(
|
||||
objectMetadataItem.nameSingular,
|
||||
)}, after: $lastCursor${capitalize(
|
||||
objectMetadataItem.nameSingular,
|
||||
)}){
|
||||
({ objectMetadataItem, operationSignature }) =>
|
||||
`${getCombinedFindManyRecordsQueryFilteringPart(
|
||||
objectMetadataItem,
|
||||
)} {
|
||||
edges {
|
||||
node ${mapObjectMetadataToGraphQLQuery({
|
||||
objectMetadataItems: objectMetadataItems,
|
||||
objectMetadataItem,
|
||||
recordGqlFields:
|
||||
fields ??
|
||||
operationSignature.fields ??
|
||||
generateDepthOneRecordGqlFields({
|
||||
objectMetadataItem,
|
||||
}),
|
||||
|
||||
@ -0,0 +1,15 @@
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { capitalize } from 'twenty-shared';
|
||||
|
||||
export const getCombinedFindManyRecordsQueryFilteringPart = (
|
||||
objectMetadataItem: ObjectMetadataItem,
|
||||
) => {
|
||||
return `${objectMetadataItem.namePlural}(
|
||||
filter: $filter${capitalize(objectMetadataItem.nameSingular)},
|
||||
orderBy: $orderBy${capitalize(objectMetadataItem.nameSingular)},
|
||||
after: $after${capitalize(objectMetadataItem.nameSingular)},
|
||||
before: $before${capitalize(objectMetadataItem.nameSingular)},
|
||||
first: $first${capitalize(objectMetadataItem.nameSingular)},
|
||||
last: $last${capitalize(objectMetadataItem.nameSingular)},
|
||||
limit: $limit${capitalize(objectMetadataItem.nameSingular)})`;
|
||||
};
|
||||
@ -67,11 +67,11 @@ export const useRecordShowPagePagination = (
|
||||
id: { neq: objectRecordId },
|
||||
},
|
||||
orderBy,
|
||||
limit: isNonEmptyString(currentRecordCursorFromRequest) ? 1 : undefined,
|
||||
cursorFilter: isNonEmptyString(currentRecordCursorFromRequest)
|
||||
? {
|
||||
cursorDirection: 'before',
|
||||
cursor: currentRecordCursorFromRequest,
|
||||
limit: 1,
|
||||
}
|
||||
: undefined,
|
||||
objectNameSingular,
|
||||
@ -90,11 +90,11 @@ export const useRecordShowPagePagination = (
|
||||
},
|
||||
fetchPolicy: 'network-only',
|
||||
orderBy,
|
||||
limit: isNonEmptyString(currentRecordCursorFromRequest) ? 1 : undefined,
|
||||
cursorFilter: currentRecordCursorFromRequest
|
||||
? {
|
||||
cursorDirection: 'after',
|
||||
cursor: currentRecordCursorFromRequest,
|
||||
limit: 1,
|
||||
}
|
||||
: undefined,
|
||||
objectNameSingular,
|
||||
|
||||
Reference in New Issue
Block a user