Skip to content

Commit

Permalink
Refetch aggregate queries on record creation/update/deletion of record (
Browse files Browse the repository at this point in the history
#8885)

Closes #8755.
Refetching the aggregate queries on an object following creation,
update, deletion of a record.
  • Loading branch information
ijreilly authored Dec 5, 2024
1 parent 9ed9b47 commit 26ff344
Show file tree
Hide file tree
Showing 32 changed files with 593 additions and 74 deletions.
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[]>;
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
}
};
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,
}),
);
});
});
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,
}),
}),
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,19 @@ import {
variables,
} from '@/object-record/hooks/__mocks__/useCreateManyRecords';
import { useCreateManyRecords } from '@/object-record/hooks/useCreateManyRecords';
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';

jest.mock('uuid', () => ({
v4: jest.fn(),
}));

jest.mock('@/object-record/hooks/useRefetchAggregateQueries');
const mockRefetchAggregateQueries = jest.fn();
(useRefetchAggregateQueries as jest.Mock).mockReturnValue({
refetchAggregateQueries: mockRefetchAggregateQueries,
});

mocked(v4)
.mockReturnValueOnce(variables.data[0].id)
.mockReturnValueOnce(variables.data[1].id);
Expand All @@ -40,6 +47,9 @@ const Wrapper = getJestMetadataAndApolloMocksWrapper({
});

describe('useCreateManyRecords', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('works as expected', async () => {
const { result } = renderHook(
() =>
Expand All @@ -57,5 +67,6 @@ describe('useCreateManyRecords', () => {
});

expect(mocks[0].result).toHaveBeenCalled();
expect(mockRefetchAggregateQueries).toHaveBeenCalledTimes(1);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
responseData,
} from '@/object-record/hooks/__mocks__/useCreateOneRecord';
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';

const personId = 'a7286b9a-c039-4a89-9567-2dfa7953cda9';
Expand All @@ -15,6 +16,12 @@ jest.mock('uuid', () => ({
v4: jest.fn(() => personId),
}));

jest.mock('@/object-record/hooks/useRefetchAggregateQueries');
const mockRefetchAggregateQueries = jest.fn();
(useRefetchAggregateQueries as jest.Mock).mockReturnValue({
refetchAggregateQueries: mockRefetchAggregateQueries,
});

const mocks = [
{
request: {
Expand All @@ -34,6 +41,9 @@ const Wrapper = getJestMetadataAndApolloMocksWrapper({
});

describe('useCreateOneRecord', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('works as expected', async () => {
const { result } = renderHook(
() =>
Expand All @@ -52,5 +62,6 @@ describe('useCreateOneRecord', () => {
});

expect(mocks[0].result).toHaveBeenCalled();
expect(mockRefetchAggregateQueries).toHaveBeenCalledTimes(1);
});
});
Loading

0 comments on commit 26ff344

Please sign in to comment.