Marie
2025-11-27 11:22:57 +01:00
committed by GitHub
parent 7e90dd888c
commit 9c9a01d55a
25 changed files with 437 additions and 42 deletions
@@ -2994,7 +2994,7 @@ export type ObjectRecordFilterInput = {
updatedAt?: InputMaybe<DateTimeFilter>;
};
/** Date granularity options (e.g. DAY, MONTH, QUARTER, YEAR, DAY_OF_THE_WEEK, MONTH_OF_THE_YEAR, QUARTER_OF_THE_YEAR) */
/** Date granularity options (e.g. DAY, MONTH, QUARTER, YEAR, WEEK, DAY_OF_THE_WEEK, MONTH_OF_THE_YEAR, QUARTER_OF_THE_YEAR) */
export enum ObjectRecordGroupByDateGranularity {
DAY = 'DAY',
DAY_OF_THE_WEEK = 'DAY_OF_THE_WEEK',
@@ -3003,6 +3003,7 @@ export enum ObjectRecordGroupByDateGranularity {
NONE = 'NONE',
QUARTER = 'QUARTER',
QUARTER_OF_THE_YEAR = 'QUARTER_OF_THE_YEAR',
WEEK = 'WEEK',
YEAR = 'YEAR'
}
@@ -2915,7 +2915,7 @@ export type ObjectRecordFilterInput = {
updatedAt?: InputMaybe<DateTimeFilter>;
};
/** Date granularity options (e.g. DAY, MONTH, QUARTER, YEAR, DAY_OF_THE_WEEK, MONTH_OF_THE_YEAR, QUARTER_OF_THE_YEAR) */
/** Date granularity options (e.g. DAY, MONTH, QUARTER, YEAR, WEEK, DAY_OF_THE_WEEK, MONTH_OF_THE_YEAR, QUARTER_OF_THE_YEAR) */
export enum ObjectRecordGroupByDateGranularity {
DAY = 'DAY',
DAY_OF_THE_WEEK = 'DAY_OF_THE_WEEK',
@@ -2924,6 +2924,7 @@ export enum ObjectRecordGroupByDateGranularity {
NONE = 'NONE',
QUARTER = 'QUARTER',
QUARTER_OF_THE_YEAR = 'QUARTER_OF_THE_YEAR',
WEEK = 'WEEK',
YEAR = 'YEAR'
}
@@ -86,6 +86,7 @@ export const ChartDateGranularitySelectionDropdownContent = ({
const dateGranularityOptions: ObjectRecordGroupByDateGranularity[] = [
ObjectRecordGroupByDateGranularity.DAY,
ObjectRecordGroupByDateGranularity.WEEK,
ObjectRecordGroupByDateGranularity.MONTH,
ObjectRecordGroupByDateGranularity.QUARTER,
ObjectRecordGroupByDateGranularity.YEAR,
@@ -7,6 +7,8 @@ export const getDateGranularityLabel = (
switch (granularity) {
case ObjectRecordGroupByDateGranularity.DAY:
return t`Day`;
case ObjectRecordGroupByDateGranularity.WEEK:
return t`Week`;
case ObjectRecordGroupByDateGranularity.MONTH:
return t`Month`;
case ObjectRecordGroupByDateGranularity.QUARTER:
@@ -1,10 +1,7 @@
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { CalendarStartDay } from 'twenty-shared';
import {
detectCalendarStartDay,
type NonSystemCalendarStartDay,
} from '@/localization/utils/detection/detectCalendarStartDay';
import { detectCalendarStartDay } from '@/localization/utils/detection/detectCalendarStartDay';
import { useApplyObjectFilterDropdownFilterValue } from '@/object-record/object-filter-dropdown/hooks/useApplyObjectFilterDropdownFilterValue';
import { objectFilterDropdownCurrentRecordFilterComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownCurrentRecordFilterComponentState';
import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState';
@@ -15,7 +12,7 @@ import { UserContext } from '@/users/contexts/UserContext';
import { stringifyRelativeDateFilter } from '@/views/view-filter-value/utils/stringifyRelativeDateFilter';
import { useContext } from 'react';
import { useRecoilValue } from 'recoil';
import { ViewFilterOperand } from 'twenty-shared/types';
import { type FirstDayOfTheWeek, ViewFilterOperand } from 'twenty-shared/types';
import {
isDefined,
type RelativeDateFilter,
@@ -74,7 +71,7 @@ export const ObjectFilterDropdownDateInput = ({
userDefinedCalendarStartDay === CalendarStartDay[CalendarStartDay.SYSTEM]
? defaultSystemCalendarStartDay
: userDefinedCalendarStartDay
) as NonSystemCalendarStartDay;
) as FirstDayOfTheWeek;
const newFilterValue = relativeDate
? stringifyRelativeDateFilter({
@@ -1,10 +1,7 @@
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { CalendarStartDay } from 'twenty-shared';
import {
detectCalendarStartDay,
type NonSystemCalendarStartDay,
} from '@/localization/utils/detection/detectCalendarStartDay';
import { detectCalendarStartDay } from '@/localization/utils/detection/detectCalendarStartDay';
import { useApplyObjectFilterDropdownFilterValue } from '@/object-record/object-filter-dropdown/hooks/useApplyObjectFilterDropdownFilterValue';
import { objectFilterDropdownCurrentRecordFilterComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownCurrentRecordFilterComponentState';
import { getRelativeDateDisplayValue } from '@/object-record/object-filter-dropdown/utils/getRelativeDateDisplayValue';
@@ -14,7 +11,7 @@ import { UserContext } from '@/users/contexts/UserContext';
import { stringifyRelativeDateFilter } from '@/views/view-filter-value/utils/stringifyRelativeDateFilter';
import { useContext, useState } from 'react';
import { useRecoilValue } from 'recoil';
import { ViewFilterOperand } from 'twenty-shared/types';
import { ViewFilterOperand, type FirstDayOfTheWeek } from 'twenty-shared/types';
import {
isDefined,
resolveDateTimeFilter,
@@ -81,7 +78,7 @@ export const ObjectFilterDropdownDateTimeInput = ({
userDefinedCalendarStartDay === CalendarStartDay[CalendarStartDay.SYSTEM]
? defaultSystemCalendarStartDay
: userDefinedCalendarStartDay
) as NonSystemCalendarStartDay;
) as FirstDayOfTheWeek;
const newFilterValue = relativeDate
? stringifyRelativeDateFilter({
@@ -7,6 +7,7 @@ type GenerateDateRangeParams = {
endDate: Date;
granularity:
| ObjectRecordGroupByDateGranularity.DAY
| ObjectRecordGroupByDateGranularity.WEEK
| ObjectRecordGroupByDateGranularity.MONTH
| ObjectRecordGroupByDateGranularity.QUARTER
| ObjectRecordGroupByDateGranularity.YEAR;
@@ -43,6 +44,10 @@ export const generateDateGroupsInRange = ({
currentDateCursor.setDate(currentDateCursor.getDate() + 1);
break;
case ObjectRecordGroupByDateGranularity.WEEK:
currentDateCursor.setDate(currentDateCursor.getDate() + 1);
break;
case ObjectRecordGroupByDateGranularity.MONTH:
currentDateCursor.setMonth(currentDateCursor.getMonth() + 1);
break;
@@ -5,7 +5,8 @@ export type SupportedDateGranularity =
| ObjectRecordGroupByDateGranularity.DAY
| ObjectRecordGroupByDateGranularity.MONTH
| ObjectRecordGroupByDateGranularity.QUARTER
| ObjectRecordGroupByDateGranularity.YEAR;
| ObjectRecordGroupByDateGranularity.YEAR
| ObjectRecordGroupByDateGranularity.WEEK;
export const getDateGroupsFromData = (
parsedDates: Date[],
@@ -7,6 +7,7 @@ export const formatDateByGranularity = (
| ObjectRecordGroupByDateGranularity.MONTH
| ObjectRecordGroupByDateGranularity.QUARTER
| ObjectRecordGroupByDateGranularity.YEAR
| ObjectRecordGroupByDateGranularity.WEEK
| ObjectRecordGroupByDateGranularity.NONE,
): string => {
switch (granularity) {
@@ -16,6 +17,12 @@ export const formatDateByGranularity = (
month: 'short',
day: 'numeric',
});
case ObjectRecordGroupByDateGranularity.WEEK:
return date.toLocaleDateString(undefined, {
year: 'numeric',
month: 'short',
day: 'numeric',
});
case ObjectRecordGroupByDateGranularity.MONTH:
return date.toLocaleDateString(undefined, {
year: 'numeric',
@@ -1,4 +1,5 @@
import { detectCalendarStartDay } from '@/localization/utils/detection/detectCalendarStartDay';
import { FirstDayOfTheWeek } from 'twenty-shared/types';
import { type RelativeDateFilter } from 'twenty-shared/utils';
import { stringifyRelativeDateFilter } from '../stringifyRelativeDateFilter';
@@ -213,7 +214,7 @@ describe('stringifyRelativeDateFilter', () => {
amount: 5,
unit: 'DAY',
timezone: 'America/New_York',
firstDayOfTheWeek: 'SUNDAY',
firstDayOfTheWeek: FirstDayOfTheWeek.SUNDAY,
};
expect(stringifyRelativeDateFilter(filter)).toBe(
@@ -259,7 +260,7 @@ describe('stringifyRelativeDateFilter', () => {
direction: 'PAST',
amount: 5,
unit: 'DAY',
firstDayOfTheWeek: 'SUNDAY',
firstDayOfTheWeek: FirstDayOfTheWeek.SUNDAY,
};
expect(stringifyRelativeDateFilter(filter)).toBe('PAST_5_DAY');
@@ -289,7 +290,7 @@ describe('stringifyRelativeDateFilter', () => {
amount: 10,
unit: 'MONTH',
timezone: 'Europe/London',
firstDayOfTheWeek: 'MONDAY',
firstDayOfTheWeek: FirstDayOfTheWeek.MONDAY,
};
expect(stringifyRelativeDateFilter(filter)).toBe(
@@ -303,7 +304,7 @@ describe('stringifyRelativeDateFilter', () => {
amount: 7,
unit: 'WEEK',
timezone: 'Asia/Tokyo',
firstDayOfTheWeek: 'SUNDAY',
firstDayOfTheWeek: FirstDayOfTheWeek.SUNDAY,
};
expect(stringifyRelativeDateFilter(filter)).toBe(
@@ -1,5 +1,7 @@
import { type ObjectRecordGroupByDateGranularity } from 'twenty-shared/types';
import { type FirstDayOfTheWeek } from 'twenty-shared/utils';
export type DateFieldGroupByDefinition = {
granularity: ObjectRecordGroupByDateGranularity;
weekStartDay?: FirstDayOfTheWeek;
};
@@ -1,4 +1,5 @@
import { type ObjectRecordGroupByDateGranularity } from 'twenty-shared/types';
import { type FirstDayOfTheWeek } from 'twenty-shared/utils';
import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
@@ -10,12 +11,14 @@ export type GroupByDateField = {
fieldMetadata: FieldMetadataEntity;
subFieldName?: string;
dateGranularity: ObjectRecordGroupByDateGranularity;
weekStartDay?: FirstDayOfTheWeek;
};
export type GroupByRelationField = {
fieldMetadata: FieldMetadataEntity;
nestedFieldMetadata: FieldMetadataEntity;
nestedSubFieldName?: string;
dateGranularity?: ObjectRecordGroupByDateGranularity;
weekStartDay?: FirstDayOfTheWeek;
};
export type GroupByField =
| GroupByRegularField
@@ -33,11 +33,25 @@ export const getGroupByExpression = ({
return `TRIM(TO_CHAR(${columnNameWithQuotes}, 'TMMonth'))`;
case ObjectRecordGroupByDateGranularity.QUARTER_OF_THE_YEAR:
return `TRIM(TO_CHAR(${columnNameWithQuotes}, '"Q"Q'))`;
case ObjectRecordGroupByDateGranularity.WEEK: {
const weekStartDay = groupByField.weekStartDay;
let shiftedExpression = `DATE_TRUNC('week', ${columnNameWithQuotes})`;
if (isDefined(weekStartDay)) {
if (weekStartDay === 'SUNDAY') {
shiftedExpression = `DATE_TRUNC('week', ${columnNameWithQuotes} + INTERVAL '1 day') - INTERVAL '1 day'`;
} else if (weekStartDay === 'SATURDAY') {
shiftedExpression = `DATE_TRUNC('week', ${columnNameWithQuotes} + INTERVAL '2 days') - INTERVAL '2 days'`;
}
}
return `TO_CHAR(${shiftedExpression}, 'YYYY-MM-DD"T"HH24:MI:SSTZH:TZM')`;
}
case ObjectRecordGroupByDateGranularity.DAY:
case ObjectRecordGroupByDateGranularity.MONTH:
case ObjectRecordGroupByDateGranularity.QUARTER:
case ObjectRecordGroupByDateGranularity.YEAR:
return `DATE_TRUNC('${dateGranularity}', ${columnNameWithQuotes})`;
return `TO_CHAR(DATE_TRUNC('${dateGranularity}', ${columnNameWithQuotes}), 'YYYY-MM-DD"T"HH24:MI:SSTZH:TZM')`;
default:
assertUnreachable(dateGranularity);
}
@@ -72,6 +72,7 @@ export const parseGroupByArgs = (
groupByFields.push({
fieldMetadata,
dateGranularity: fieldGroupByDefinition.granularity,
weekStartDay: fieldGroupByDefinition.weekStartDay,
});
continue;
}
@@ -1,9 +1,6 @@
import { UserInputError } from 'apollo-server-core';
import { isDefined } from 'class-validator';
import {
FieldMetadataType,
type ObjectRecordGroupByDateGranularity,
} from 'twenty-shared/types';
import { FieldMetadataType } from 'twenty-shared/types';
import {
GraphqlQueryRunnerException,
@@ -153,14 +150,14 @@ export const parseGroupByRelationField = ({
nestedFieldMetadata.type === FieldMetadataType.DATE_TIME) &&
isGroupByDateFieldDefinition(nestedFieldGroupByDefinition)
) {
const dateFieldDefinition =
nestedFieldGroupByDefinition as DateFieldGroupByDefinition;
groupByFields.push({
fieldMetadata,
nestedFieldMetadata,
dateGranularity: (
nestedFieldGroupByDefinition as {
granularity: ObjectRecordGroupByDateGranularity;
}
).granularity,
dateGranularity: dateFieldDefinition.granularity,
weekStartDay: dateFieldDefinition.weekStartDay,
});
return;
@@ -4,6 +4,7 @@ import {
type ObjectRecordOrderByForCompositeField,
type ObjectRecordOrderByForScalarField,
} from 'twenty-shared/types';
import { type FirstDayOfTheWeek } from 'twenty-shared/utils';
export type ObjectRecordFilter = Partial<{
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -27,6 +28,7 @@ export type ObjectRecordGroupByForCompositeField = Partial<{
export type ObjectRecordGroupByForDateField = Partial<{
[Property in keyof ObjectRecord]: {
granularity: ObjectRecordGroupByDateGranularity;
weekStartDay?: FirstDayOfTheWeek;
};
}>;
@@ -1,7 +1,10 @@
import { Injectable } from '@nestjs/common';
import { GraphQLEnumType, GraphQLInputObjectType } from 'graphql';
import { ObjectRecordGroupByDateGranularity } from 'twenty-shared/types';
import {
FirstDayOfTheWeek,
ObjectRecordGroupByDateGranularity,
} from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { OrderByDirectionType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/enum';
@@ -30,7 +33,7 @@ export class GroupByDateGranularityInputTypeGenerator {
{} as Record<string, { value: string }>,
),
description:
'Date granularity (e.g. day, month, quarter, year, day of the week, quarter of the year, month of the year)',
'Date granularity (e.g. day, month, quarter, year, week, day of the week, quarter of the year, month of the year)',
}),
);
@@ -42,13 +45,33 @@ export class GroupByDateGranularityInputTypeGenerator {
throw new Error('DateGranularityEnum not found');
}
const firstDayOfWeekEnum = new GraphQLEnumType({
name: 'FirstDayOfTheWeek',
values: Object.values(FirstDayOfTheWeek).reduce(
(acc, option) => {
acc[option] = { value: option };
return acc;
},
{} as Record<string, { value: string }>,
),
description: 'First day of the week (MONDAY, SUNDAY, SATURDAY)',
});
this.gqlTypesStorage.addGqlType('FirstDayOfTheWeek', firstDayOfWeekEnum);
const groupByDateField = new GraphQLInputObjectType({
name: GROUP_BY_DATE_GRANULARITY_INPUT_KEY,
fields: {
granularity: {
type: dateGranularityEnum,
description:
'Date granularity (e.g. day, month, quarter, year, day of the week, quarter of the year, month of the year)',
'Date granularity (e.g. day, month, quarter, year, week, day of the week, quarter of the year, month of the year)',
},
weekStartDay: {
type: firstDayOfWeekEnum,
description:
'First day of the week (only applicable when granularity is WEEK). Defaults to MONDAY if not specified.',
},
},
});
@@ -5,7 +5,7 @@ import { ObjectRecordGroupByDateGranularity } from 'twenty-shared/types';
registerEnumType(ObjectRecordGroupByDateGranularity, {
name: 'ObjectRecordGroupByDateGranularity',
description:
'Date granularity options (e.g. DAY, MONTH, QUARTER, YEAR, DAY_OF_THE_WEEK, MONTH_OF_THE_YEAR, QUARTER_OF_THE_YEAR)',
'Date granularity options (e.g. DAY, MONTH, QUARTER, YEAR, WEEK, DAY_OF_THE_WEEK, MONTH_OF_THE_YEAR, QUARTER_OF_THE_YEAR)',
});
export { ObjectRecordGroupByDateGranularity };
@@ -8,6 +8,7 @@ import {
subWeeks,
subYears,
} from 'date-fns';
import { FirstDayOfTheWeek } from 'twenty-shared/types';
import { type RelativeDateFilter } from 'twenty-shared/utils';
import {
@@ -468,7 +469,7 @@ describe('Relative Date Filter Utils', () => {
const relativeDateFilterValue: RelativeDateFilter = {
direction: 'THIS',
unit: 'WEEK',
firstDayOfTheWeek: 'MONDAY',
firstDayOfTheWeek: FirstDayOfTheWeek.MONDAY,
};
expect(
@@ -247,11 +247,11 @@ describe('group-by resolver (integration)', () => {
const testPerson2Id = randomUUID();
const testPerson3Id = randomUUID();
beforeAll(async () => {
const idJan2 = testPersonId;
const idJan8 = testPerson2Id;
const idMar3 = testPerson3Id;
const idJan2 = testPersonId;
const idJan8 = testPerson2Id;
const idMar3 = testPerson3Id;
beforeAll(async () => {
await makeGraphqlAPIRequest(
createOneOperationFactory({
objectMetadataSingularName: 'person',
@@ -336,6 +336,299 @@ describe('group-by resolver (integration)', () => {
expect(marGroup.totalCount).toBe(1);
});
describe('group by week', () => {
const idMarch1st = randomUUID();
const idMarch2nd = randomUUID();
const idMarch3rd = randomUUID();
beforeAll(async () => {
await makeGraphqlAPIRequest(
createOneOperationFactory({
objectMetadataSingularName: 'person',
gqlFields: PERSON_GQL_FIELDS,
data: { id: idMarch3rd, createdAt: '2025-03-03T09:30:00.000Z' }, // monday, march, Q1, 2025
}),
);
await makeGraphqlAPIRequest(
createOneOperationFactory({
objectMetadataSingularName: 'person',
gqlFields: PERSON_GQL_FIELDS,
data: { id: idMarch2nd, createdAt: '2025-03-02T09:30:00.000Z' }, // sunday, march, Q1, 2025
}),
);
await makeGraphqlAPIRequest(
createOneOperationFactory({
objectMetadataSingularName: 'person',
gqlFields: PERSON_GQL_FIELDS,
data: { id: idMarch1st, createdAt: '2025-03-01T09:30:00.000Z' }, // saturday, march, Q1, 2025
}),
);
});
afterAll(async () => {
await makeGraphqlAPIRequest(
destroyOneOperationFactory({
objectMetadataSingularName: 'person',
gqlFields: 'id',
recordId: idMarch1st,
}),
);
await makeGraphqlAPIRequest(
destroyOneOperationFactory({
objectMetadataSingularName: 'person',
gqlFields: 'id',
recordId: idMarch2nd,
}),
);
await makeGraphqlAPIRequest(
destroyOneOperationFactory({
objectMetadataSingularName: 'person',
gqlFields: 'id',
recordId: idMarch3rd,
}),
);
});
it('datetime field - groups by createdAt WEEK with default (MONDAY)', async () => {
const response = await makeGraphqlAPIRequest(
groupByOperationFactory({
objectMetadataSingularName: 'person',
objectMetadataPluralName: 'people',
groupBy: [{ createdAt: { granularity: 'WEEK' } }],
gqlFields: `
edges {
node {
id
}
}
`,
filter: filter2025,
}),
);
const groups = response.body.data.peopleGroupBy;
expect(groups).toBeDefined();
expect(groups.length).toBe(4);
// Group starting week of monday dec 30th, 2024
const mondayDec30thGroup = groups.find((group: any) =>
group.groupByDimensionValues[0].startsWith('2024-12-30T00:00:00'),
);
expect(mondayDec30thGroup).toBeDefined();
expect(mondayDec30thGroup.edges[0].node.id).toBe(idJan2);
expect(mondayDec30thGroup.totalCount).toBe(1);
// Group starting week of monday jan 6th, 2025
const mondayJan6thGroup = groups.find((group: any) =>
group.groupByDimensionValues[0].startsWith('2025-01-06T00:00:00'),
);
expect(mondayJan6thGroup).toBeDefined();
expect(mondayJan6thGroup.edges[0].node.id).toBe(idJan8);
expect(mondayJan6thGroup.totalCount).toBe(1);
// Group starting week of monday feb 24th, 2025
const mondayFeb24thGroup = groups.find((group: any) =>
group.groupByDimensionValues[0].startsWith('2025-02-24T00:00:00'),
);
expect(mondayFeb24thGroup).toBeDefined();
expect(mondayFeb24thGroup.edges.length).toBe(2);
expect(
mondayFeb24thGroup.edges.find(
(edge: any) => edge.node.id === idMarch2nd,
),
).toBeDefined;
expect(
mondayFeb24thGroup.edges.find(
(edge: any) => edge.node.id === idMarch1st,
),
).toBeDefined;
// Group starting week of monday march 3rd, 2025
const mondayMarch3rdGroup = groups.find((group: any) =>
group.groupByDimensionValues[0].startsWith('2025-03-03T00:00:00'),
);
expect(mondayMarch3rdGroup).toBeDefined();
expect(mondayMarch3rdGroup.edges.length).toBe(2);
expect(
mondayMarch3rdGroup.edges.find(
(edge: any) => edge.node.id === idMarch3rd,
),
).toBeDefined;
expect(
mondayMarch3rdGroup.edges.find(
(edge: any) => edge.node.id === idMar3,
),
).toBeDefined;
});
it('datetime field - groups by createdAt WEEK with weekStartDay SUNDAY', async () => {
const response = await makeGraphqlAPIRequest(
groupByOperationFactory({
objectMetadataSingularName: 'person',
objectMetadataPluralName: 'people',
groupBy: [
{ createdAt: { granularity: 'WEEK', weekStartDay: 'SUNDAY' } },
],
gqlFields: `
edges {
node {
id
}
}
`,
filter: filter2025,
}),
);
const groups = response.body.data.peopleGroupBy;
expect(groups).toBeDefined();
expect(groups.length).toBe(4);
// Group starting week of sunday dec 29th, 2024
const sundayDec29thGroup = groups.find((group: any) =>
group.groupByDimensionValues[0].startsWith('2024-12-29T00:00:00'),
);
expect(sundayDec29thGroup).toBeDefined();
expect(sundayDec29thGroup.totalCount).toBe(1);
expect(
sundayDec29thGroup.edges.find((edge: any) => edge.node.id === idJan2),
).toBeDefined();
// Group starting week of sunday jan 5th, 2025
const sundayJan5thGroup = groups.find((group: any) =>
group.groupByDimensionValues[0].startsWith('2025-01-05T00:00:00'),
);
expect(sundayJan5thGroup).toBeDefined();
expect(sundayJan5thGroup.totalCount).toBe(1);
expect(
sundayJan5thGroup.edges.find((edge: any) => edge.node.id === idJan8),
).toBeDefined();
// Group starting week of sunday feb 23rd, 2025
const sundayFeb23rdGroup = groups.find((group: any) =>
group.groupByDimensionValues[0].startsWith('2025-02-23T00:00:00'),
);
expect(sundayFeb23rdGroup).toBeDefined();
expect(sundayFeb23rdGroup.totalCount).toBe(1);
expect(
sundayFeb23rdGroup.edges.find(
(edge: any) => edge.node.id === idMarch1st,
),
).toBeDefined();
// Group starting week of sunday march 2nd, 2025
const sundayMarch2ndGroup = groups.find((group: any) =>
group.groupByDimensionValues[0].startsWith('2025-03-02T00:00:00'),
);
expect(sundayMarch2ndGroup).toBeDefined();
expect(sundayMarch2ndGroup.totalCount).toBe(3);
expect(
sundayMarch2ndGroup.edges.find(
(edge: any) => edge.node.id === idMarch2nd,
),
).toBeDefined();
expect(
sundayMarch2ndGroup.edges.find(
(edge: any) => edge.node.id === idMarch3rd,
),
).toBeDefined();
expect(
sundayMarch2ndGroup.edges.find(
(edge: any) => edge.node.id === idMar3,
),
).toBeDefined();
});
it('datetime field - groups by createdAt WEEK with weekStartDay SATURDAY', async () => {
const response = await makeGraphqlAPIRequest(
groupByOperationFactory({
objectMetadataSingularName: 'person',
objectMetadataPluralName: 'people',
groupBy: [
{ createdAt: { granularity: 'WEEK', weekStartDay: 'SATURDAY' } },
],
gqlFields: `
edges {
node {
id
}
}
`,
filter: filter2025,
}),
);
const groups = response.body.data.peopleGroupBy;
expect(groups).toBeDefined();
expect(groups.length).toBe(3);
// Group starting week of saturday dec 28th, 2024
const saturdayDec28thGroup = groups.find((group: any) =>
group.groupByDimensionValues[0].startsWith('2024-12-28T00:00:00'),
);
expect(saturdayDec28thGroup).toBeDefined();
expect(saturdayDec28thGroup.totalCount).toBe(1);
expect(
saturdayDec28thGroup.edges.find(
(edge: any) => edge.node.id === idJan2,
),
).toBeDefined();
// Group starting week of saturday jan 4th, 2025
const saturdayJan4thGroup = groups.find((group: any) =>
group.groupByDimensionValues[0].startsWith('2025-01-04T00:00:00'),
);
expect(saturdayJan4thGroup).toBeDefined();
expect(saturdayJan4thGroup.totalCount).toBe(1);
expect(
saturdayJan4thGroup.edges.find(
(edge: any) => edge.node.id === idJan8,
),
).toBeDefined();
// Group starting week of saturday march 1st, 2025
const saturdayMarch1stGroup = groups.find((group: any) =>
group.groupByDimensionValues[0].startsWith('2025-03-01T00:00:00'),
);
expect(saturdayMarch1stGroup).toBeDefined();
expect(saturdayMarch1stGroup.totalCount).toBe(4);
expect(
saturdayMarch1stGroup.edges.find(
(edge: any) => edge.node.id === idMarch1st,
),
).toBeDefined();
expect(
saturdayMarch1stGroup.edges.find(
(edge: any) => edge.node.id === idMarch2nd,
),
).toBeDefined();
expect(
saturdayMarch1stGroup.edges.find(
(edge: any) => edge.node.id === idMarch3rd,
),
).toBeDefined();
expect(
saturdayMarch1stGroup.edges.find(
(edge: any) => edge.node.id === idMar3,
),
).toBeDefined();
});
});
describe('cyclic date', () => {
const filter2024And2025 = {
and: [
@@ -838,6 +1131,39 @@ describe('group-by resolver (integration)', () => {
expect(wednesdayGroup.totalCount).toBe(1);
});
it('groups by one relation field - company createdAt with WEEK granularity and weekStartDay SUNDAY', async () => {
const response = await makeGraphqlAPIRequest(
groupByOperationFactory({
objectMetadataSingularName: 'person',
objectMetadataPluralName: 'people',
groupBy: [
{
company: {
createdAt: {
granularity: 'WEEK',
weekStartDay: 'SUNDAY',
},
},
},
],
filter: filter2025,
}),
);
const groups = response.body.data.peopleGroupBy;
expect(groups).toBeDefined();
expect(Array.isArray(groups)).toBe(true);
// Verify that all records are grouped
const totalCount = groups.reduce(
(sum: number, group: any) => sum + group.totalCount,
0,
);
expect(totalCount).toBe(3);
});
it('groups by two relation fields from the same joined table', async () => {
const response = await makeGraphqlAPIRequest(
groupByOperationFactory({
@@ -0,0 +1,5 @@
export enum FirstDayOfTheWeek {
MONDAY = 'MONDAY',
SUNDAY = 'SUNDAY',
SATURDAY = 'SATURDAY',
}
@@ -3,6 +3,7 @@ export enum ObjectRecordGroupByDateGranularity {
MONTH = 'MONTH',
QUARTER = 'QUARTER',
YEAR = 'YEAR',
WEEK = 'WEEK',
DAY_OF_THE_WEEK = 'DAY_OF_THE_WEEK',
MONTH_OF_THE_YEAR = 'MONTH_OF_THE_YEAR',
QUARTER_OF_THE_YEAR = 'QUARTER_OF_THE_YEAR',
@@ -109,6 +109,7 @@ export type {
FilterableAndTSVectorFieldType,
} from './FilterableFieldType';
export { FILTERABLE_FIELD_TYPES } from './FilterableFieldType';
export { FirstDayOfTheWeek } from './FirstDayOfTheWeek';
export type { FromTo } from './FromToType';
export type { IsEmptyRecord } from './IsEmptyRecord.type';
export type { IsExactly } from './IsExactly';
@@ -1,5 +1,10 @@
import { FirstDayOfTheWeek as FirstDayOfTheWeekEnum } from '@/types/FirstDayOfTheWeek';
import z from 'zod';
export const firstDayOfWeekSchema = z.enum(['MONDAY', 'SATURDAY', 'SUNDAY']);
export const firstDayOfWeekSchema = z.enum([
FirstDayOfTheWeekEnum.MONDAY,
FirstDayOfTheWeekEnum.SATURDAY,
FirstDayOfTheWeekEnum.SUNDAY,
]);
export type FirstDayOfTheWeek = z.infer<typeof firstDayOfWeekSchema>;
@@ -1,3 +1,4 @@
import { FirstDayOfTheWeek as FirstDayOfTheWeekEnum } from '@/types/FirstDayOfTheWeek';
import { assertUnreachable } from '@/utils/assertUnreachable';
import { type FirstDayOfTheWeek } from '@/utils/filter/dates/utils/firstDayOfWeekSchema';
@@ -5,11 +6,11 @@ export const getFirstDayOfTheWeekAsANumberForDateFNS = (
firstDayOfTheWeek: FirstDayOfTheWeek,
): 0 | 1 | 6 => {
switch (firstDayOfTheWeek) {
case 'MONDAY':
case FirstDayOfTheWeekEnum.MONDAY:
return 1;
case 'SATURDAY':
case FirstDayOfTheWeekEnum.SATURDAY:
return 6;
case 'SUNDAY':
case FirstDayOfTheWeekEnum.SUNDAY:
return 0;
default:
return assertUnreachable(firstDayOfTheWeek);