-
Notifications
You must be signed in to change notification settings - Fork 2.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Refetch aggregate queries on record creation/update/deletion of record (
#8885) Closes #8755. Refetching the aggregate queries on an object following creation, update, deletion of a record.
- Loading branch information
Showing
32 changed files
with
593 additions
and
74 deletions.
There are no files selected for viewing
2 changes: 1 addition & 1 deletion
2
packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlFieldsAggregate.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,3 @@ | ||
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations'; | ||
|
||
export type RecordGqlFieldsAggregate = Record<string, AGGREGATE_OPERATIONS>; | ||
export type RecordGqlFieldsAggregate = Record<string, AGGREGATE_OPERATIONS[]>; |
19 changes: 19 additions & 0 deletions
19
packages/twenty-front/src/modules/object-record/hooks/__mocks__/useAggregateRecords.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import { gql } from '@apollo/client'; | ||
|
||
export const AGGREGATE_QUERY = gql` | ||
query AggregateOpportunities($filter: OpportunityFilterInput) { | ||
opportunities(filter: $filter) { | ||
totalCount | ||
sumAmount | ||
avgAmount | ||
} | ||
} | ||
`; | ||
|
||
export const mockResponse = { | ||
opportunities: { | ||
totalCount: 42, | ||
sumAmount: 1000000, | ||
avgAmount: 23800 | ||
} | ||
}; |
129 changes: 129 additions & 0 deletions
129
packages/twenty-front/src/modules/object-record/hooks/__tests__/useAggregateRecords.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; | ||
import { | ||
AGGREGATE_QUERY, | ||
mockResponse, | ||
} from '@/object-record/hooks/__mocks__/useAggregateRecords'; | ||
import { useAggregateRecords } from '@/object-record/hooks/useAggregateRecords'; | ||
import { useAggregateRecordsQuery } from '@/object-record/hooks/useAggregateRecordsQuery'; | ||
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations'; | ||
import { useQuery } from '@apollo/client'; | ||
import { renderHook } from '@testing-library/react'; | ||
|
||
// Mocks | ||
jest.mock('@apollo/client'); | ||
jest.mock('@/object-metadata/hooks/useObjectMetadataItem'); | ||
jest.mock('@/object-record/hooks/useAggregateRecordsQuery'); | ||
|
||
const mockObjectMetadataItem = { | ||
nameSingular: 'opportunity', | ||
namePlural: 'opportunities', | ||
}; | ||
|
||
const mockGqlFieldToFieldMap = { | ||
sumAmount: ['amount', AGGREGATE_OPERATIONS.sum], | ||
avgAmount: ['amount', AGGREGATE_OPERATIONS.avg], | ||
totalCount: ['name', AGGREGATE_OPERATIONS.count], | ||
}; | ||
|
||
describe('useAggregateRecords', () => { | ||
beforeEach(() => { | ||
(useObjectMetadataItem as jest.Mock).mockReturnValue({ | ||
objectMetadataItem: mockObjectMetadataItem, | ||
}); | ||
|
||
(useAggregateRecordsQuery as jest.Mock).mockReturnValue({ | ||
aggregateQuery: AGGREGATE_QUERY, | ||
gqlFieldToFieldMap: mockGqlFieldToFieldMap, | ||
}); | ||
|
||
(useQuery as jest.Mock).mockReturnValue({ | ||
data: mockResponse, | ||
loading: false, | ||
error: undefined, | ||
}); | ||
}); | ||
|
||
it('should format data correctly', () => { | ||
const { result } = renderHook(() => | ||
useAggregateRecords({ | ||
objectNameSingular: 'opportunity', | ||
recordGqlFieldsAggregate: { | ||
amount: [AGGREGATE_OPERATIONS.sum, AGGREGATE_OPERATIONS.avg], | ||
name: [AGGREGATE_OPERATIONS.count], | ||
}, | ||
}), | ||
); | ||
|
||
expect(result.current.data).toEqual({ | ||
amount: { | ||
[AGGREGATE_OPERATIONS.sum]: 1000000, | ||
[AGGREGATE_OPERATIONS.avg]: 23800, | ||
}, | ||
name: { | ||
[AGGREGATE_OPERATIONS.count]: 42, | ||
}, | ||
}); | ||
expect(result.current.loading).toBe(false); | ||
expect(result.current.error).toBeUndefined(); | ||
}); | ||
|
||
it('should handle loading state', () => { | ||
(useQuery as jest.Mock).mockReturnValue({ | ||
data: undefined, | ||
loading: true, | ||
error: undefined, | ||
}); | ||
|
||
const { result } = renderHook(() => | ||
useAggregateRecords({ | ||
objectNameSingular: 'opportunity', | ||
recordGqlFieldsAggregate: { | ||
amount: [AGGREGATE_OPERATIONS.sum], | ||
}, | ||
}), | ||
); | ||
|
||
expect(result.current.data).toEqual({}); | ||
expect(result.current.loading).toBe(true); | ||
}); | ||
|
||
it('should handle error state', () => { | ||
const mockError = new Error('Query failed'); | ||
(useQuery as jest.Mock).mockReturnValue({ | ||
data: undefined, | ||
loading: false, | ||
error: mockError, | ||
}); | ||
|
||
const { result } = renderHook(() => | ||
useAggregateRecords({ | ||
objectNameSingular: 'opportunity', | ||
recordGqlFieldsAggregate: { | ||
amount: [AGGREGATE_OPERATIONS.sum], | ||
}, | ||
}), | ||
); | ||
|
||
expect(result.current.data).toEqual({}); | ||
expect(result.current.error).toBe(mockError); | ||
}); | ||
|
||
it('should skip query when specified', () => { | ||
renderHook(() => | ||
useAggregateRecords({ | ||
objectNameSingular: 'opportunity', | ||
recordGqlFieldsAggregate: { | ||
amount: [AGGREGATE_OPERATIONS.sum], | ||
}, | ||
skip: true, | ||
}), | ||
); | ||
|
||
expect(useQuery).toHaveBeenCalledWith( | ||
AGGREGATE_QUERY, | ||
expect.objectContaining({ | ||
skip: true, | ||
}), | ||
); | ||
}); | ||
}); |
144 changes: 144 additions & 0 deletions
144
.../twenty-front/src/modules/object-record/hooks/__tests__/useAggregateRecordsQuery.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,144 @@ | ||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; | ||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; | ||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; | ||
import { useAggregateRecordsQuery } from '@/object-record/hooks/useAggregateRecordsQuery'; | ||
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations'; | ||
import { generateAggregateQuery } from '@/object-record/utils/generateAggregateQuery'; | ||
import { renderHook } from '@testing-library/react'; | ||
import { FieldMetadataType } from '~/generated/graphql'; | ||
|
||
jest.mock('@/object-metadata/hooks/useObjectMetadataItem'); | ||
jest.mock('@/object-record/utils/generateAggregateQuery'); | ||
|
||
const mockObjectMetadataItem: ObjectMetadataItem = { | ||
nameSingular: 'company', | ||
namePlural: 'companies', | ||
id: 'test-id', | ||
labelSingular: 'Company', | ||
labelPlural: 'Companies', | ||
isCustom: false, | ||
isActive: true, | ||
createdAt: new Date().toISOString(), | ||
updatedAt: new Date().toISOString(), | ||
fields: [ | ||
{ | ||
id: 'field-1', | ||
name: 'amount', | ||
label: 'Amount', | ||
type: FieldMetadataType.Number, | ||
isCustom: false, | ||
isActive: true, | ||
createdAt: new Date().toISOString(), | ||
updatedAt: new Date().toISOString(), | ||
} as FieldMetadataItem, | ||
{ | ||
id: 'field-2', | ||
name: 'name', | ||
label: 'Name', | ||
type: FieldMetadataType.Text, | ||
isCustom: false, | ||
isActive: true, | ||
createdAt: new Date().toISOString(), | ||
updatedAt: new Date().toISOString(), | ||
} as FieldMetadataItem, | ||
], | ||
indexMetadatas: [], | ||
isLabelSyncedWithName: true, | ||
isRemote: false, | ||
isSystem: false, | ||
}; | ||
|
||
describe('useAggregateRecordsQuery', () => { | ||
beforeEach(() => { | ||
jest.clearAllMocks(); | ||
(useObjectMetadataItem as jest.Mock).mockReturnValue({ | ||
objectMetadataItem: mockObjectMetadataItem, | ||
}); | ||
|
||
(generateAggregateQuery as jest.Mock).mockReturnValue({ | ||
loc: { | ||
source: { | ||
body: 'query AggregateCompanies($filter: CompanyFilterInput) { companies(filter: $filter) { totalCount } }', | ||
}, | ||
}, | ||
}); | ||
}); | ||
|
||
it('should handle simple count operation', () => { | ||
const { result } = renderHook(() => | ||
useAggregateRecordsQuery({ | ||
objectNameSingular: 'company', | ||
recordGqlFieldsAggregate: { | ||
name: [AGGREGATE_OPERATIONS.count], | ||
}, | ||
}), | ||
); | ||
|
||
expect(result.current.gqlFieldToFieldMap).toEqual({ | ||
totalCount: ['name', 'COUNT'], | ||
}); | ||
expect(generateAggregateQuery).toHaveBeenCalledWith({ | ||
objectMetadataItem: mockObjectMetadataItem, | ||
recordGqlFields: { | ||
totalCount: true, | ||
}, | ||
}); | ||
}); | ||
|
||
it('should handle field aggregation', () => { | ||
const { result } = renderHook(() => | ||
useAggregateRecordsQuery({ | ||
objectNameSingular: 'company', | ||
recordGqlFieldsAggregate: { | ||
amount: [AGGREGATE_OPERATIONS.sum], | ||
}, | ||
}), | ||
); | ||
|
||
expect(result.current.gqlFieldToFieldMap).toEqual({ | ||
sumAmount: ['amount', 'SUM'], | ||
}); | ||
expect(generateAggregateQuery).toHaveBeenCalledWith( | ||
expect.objectContaining({ | ||
recordGqlFields: expect.objectContaining({ | ||
sumAmount: true, | ||
}), | ||
}), | ||
); | ||
}); | ||
|
||
it('should throw error for invalid aggregation operation', () => { | ||
expect(() => | ||
renderHook(() => | ||
useAggregateRecordsQuery({ | ||
objectNameSingular: 'company', | ||
recordGqlFieldsAggregate: { | ||
name: [AGGREGATE_OPERATIONS.sum], | ||
}, | ||
}), | ||
), | ||
).toThrow(); | ||
}); | ||
|
||
it('should handle multiple aggregations', () => { | ||
const { result } = renderHook(() => | ||
useAggregateRecordsQuery({ | ||
objectNameSingular: 'company', | ||
recordGqlFieldsAggregate: { | ||
amount: [AGGREGATE_OPERATIONS.sum], | ||
name: [AGGREGATE_OPERATIONS.count], | ||
}, | ||
}), | ||
); | ||
|
||
expect(result.current.gqlFieldToFieldMap).toHaveProperty('sumAmount'); | ||
expect(generateAggregateQuery).toHaveBeenCalledWith( | ||
expect.objectContaining({ | ||
recordGqlFields: expect.objectContaining({ | ||
totalCount: true, | ||
sumAmount: true, | ||
}), | ||
}), | ||
); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.