[groupBy][Requires cache flush] Add WEEK date granularity (#16099)
Closes https://github.com/twentyhq/core-team-issues/issues/1921 https://github.com/user-attachments/assets/bf400ec1-ce25-4d9e-b875-774168452514
This commit is contained in:
@@ -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'
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -86,6 +86,7 @@ export const ChartDateGranularitySelectionDropdownContent = ({
|
||||
|
||||
const dateGranularityOptions: ObjectRecordGroupByDateGranularity[] = [
|
||||
ObjectRecordGroupByDateGranularity.DAY,
|
||||
ObjectRecordGroupByDateGranularity.WEEK,
|
||||
ObjectRecordGroupByDateGranularity.MONTH,
|
||||
ObjectRecordGroupByDateGranularity.QUARTER,
|
||||
ObjectRecordGroupByDateGranularity.YEAR,
|
||||
|
||||
+2
@@ -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:
|
||||
|
||||
+3
-6
@@ -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({
|
||||
|
||||
+3
-6
@@ -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({
|
||||
|
||||
+5
@@ -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;
|
||||
|
||||
+2
-1
@@ -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
@@ -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',
|
||||
|
||||
+5
-4
@@ -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(
|
||||
|
||||
+2
@@ -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;
|
||||
};
|
||||
|
||||
+3
@@ -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
|
||||
|
||||
+15
-1
@@ -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);
|
||||
}
|
||||
|
||||
+1
@@ -72,6 +72,7 @@ export const parseGroupByArgs = (
|
||||
groupByFields.push({
|
||||
fieldMetadata,
|
||||
dateGranularity: fieldGroupByDefinition.granularity,
|
||||
weekStartDay: fieldGroupByDefinition.weekStartDay,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
+6
-9
@@ -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;
|
||||
|
||||
+2
@@ -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;
|
||||
};
|
||||
}>;
|
||||
|
||||
|
||||
+26
-3
@@ -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.',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
+1
-1
@@ -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 };
|
||||
|
||||
+2
-1
@@ -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(
|
||||
|
||||
+330
-4
@@ -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>;
|
||||
|
||||
+4
-3
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user