[groupBy] Date ranges (#14923)

Closes https://github.com/twentyhq/core-team-issues/issues/1559

2 kinds of date ranges: absolute (day, month, quarter, year) and
"cyclic" (day of the year, month of the year, quarter of the year. ex
dates in january 2024 and january 2025 will be grouped together in
"Q1").

Absolute dates are expressed in datetime format to ease their handling,
while cyclic dates are expressed with more human readable labels ("Q1")
because datetime does not make sense anyway

<img width="851" height="722" alt="Capture d’écran 2025-10-06 à 18 13
29"
src="https://github.com/user-attachments/assets/44cce55d-a363-4bfa-a13d-e385ce5d227e"
/>

<img width="854" height="716" alt="Capture d’écran 2025-10-06 à 18 14
02"
src="https://github.com/user-attachments/assets/c1c1f176-c5b6-441c-b419-7e1692600b65"
/>

<img width="878" height="721" alt="Capture d’écran 2025-10-06 à 18 14
14"
src="https://github.com/user-attachments/assets/76318c53-e1cd-4e73-aa56-9e705ea40596"
/>
This commit is contained in:
Marie
2025-10-07 17:52:24 +02:00
committed by GitHub
parent 063db1efe2
commit 0474f00b5f
15 changed files with 670 additions and 117 deletions
@@ -1,6 +1,7 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { GraphqlQueryGroupByResolverService } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/graphql-query-group-by-resolver.service';
import { ProcessAggregateHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-aggregate.helper';
import { ProcessNestedRelationsV2Helper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations-v2.helper';
import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper';
@@ -13,7 +14,6 @@ import { GraphqlQueryDestroyOneResolverService } from 'src/engine/api/graphql/gr
import { GraphqlQueryFindDuplicatesResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-duplicates-resolver.service';
import { GraphqlQueryFindManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service';
import { GraphqlQueryFindOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service';
import { GraphqlQueryGroupByResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-group-by-resolver.service';
import { GraphqlQueryMergeManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-merge-many-resolver.service';
import { GraphqlQueryRestoreManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-many-resolver.service';
import { GraphqlQueryRestoreOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-one-resolver.service';
@@ -27,10 +27,11 @@ import { IGroupByConnection } from 'src/engine/api/graphql/workspace-query-runne
import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
import { GroupByResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import {
GraphqlQueryRunnerException,
GraphqlQueryRunnerExceptionCode,
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
import { formatResultWithGroupByDimensionValues } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/format-result-with-group-by-dimension-values.util';
import { getGroupByExpression } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/get-group-by-expression.util';
import { isGroupByDateField } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/is-group-by-date-field.util';
import { parseGroupByArgs } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/parse-group-by-args.util';
import { removeQuotes } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/remove-quote.util';
import { ProcessAggregateHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-aggregate.helper';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { ViewEntity } from 'src/engine/core-modules/view/entities/view.entity';
@@ -38,10 +39,8 @@ import { ViewFilterGroupService } from 'src/engine/core-modules/view/services/vi
import { ViewFilterService } from 'src/engine/core-modules/view/services/view-filter.service';
import { ViewService } from 'src/engine/core-modules/view/services/view.service';
import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
import { formatColumnNamesFromCompositeFieldAndSubfields } from 'src/engine/twenty-orm/utils/format-column-names-from-composite-field-and-subfield.util';
@Injectable()
export class GraphqlQueryGroupByResolverService extends GraphqlQueryBaseResolverService<
GroupByResolverArgs,
@@ -97,27 +96,44 @@ export class GraphqlQueryGroupByResolverService extends GraphqlQueryBaseResolver
objectMetadataNameSingular,
});
const groupByFields = this.parseGroupByArgs(
const groupByFields = parseGroupByArgs(
executionArgs.args,
objectMetadataItemWithFieldMaps,
);
const groupByColumnsWithQuotes = groupByFields.map((groupByField) => {
return `"${
const groupByDefinitions = groupByFields.map((groupByField) => {
const columnNameWithQuotes = `"${
formatColumnNamesFromCompositeFieldAndSubfields(
groupByField.fieldMetadata.name,
groupByField.subFieldName ? [groupByField.subFieldName] : undefined,
)[0]
}"`;
const alias =
removeQuotes(columnNameWithQuotes) +
(isGroupByDateField(groupByField)
? `_${groupByField.dateGranularity}`
: '');
return {
columnNameWithQuotes,
expression: getGroupByExpression({
groupByField,
columnNameWithQuotes,
}),
alias,
dateGranularity: isGroupByDateField(groupByField)
? groupByField.dateGranularity
: undefined,
};
});
groupByColumnsWithQuotes.forEach((groupByColumn, index) => {
queryBuilder.addSelect(groupByColumn);
groupByDefinitions.forEach((groupByColumn, index) => {
queryBuilder.addSelect(groupByColumn.expression, groupByColumn.alias);
if (index === 0) {
queryBuilder.groupBy(groupByColumn);
queryBuilder.groupBy(groupByColumn.expression);
} else {
queryBuilder.addGroupBy(groupByColumn);
queryBuilder.addGroupBy(groupByColumn.expression);
}
});
@@ -134,87 +150,7 @@ export class GraphqlQueryGroupByResolverService extends GraphqlQueryBaseResolver
const result = await queryBuilder.getRawMany();
return this.formatResultWithGroupByDimensionValues(
result,
groupByColumnsWithQuotes,
);
}
private formatResultWithGroupByDimensionValues(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
result: any[],
groupByColumnsWithQuotes: string[],
): IGroupByConnection<ObjectRecord, IEdge<ObjectRecord>>[] {
let formattedResult: IGroupByConnection<
ObjectRecord,
IEdge<ObjectRecord>
>[] = [];
result.forEach((group) => {
let dimensionValues = [];
for (const groupByColumn of groupByColumnsWithQuotes) {
const groupByColumnWithoutQuotes = groupByColumn.replace(/["']/g, '');
dimensionValues.push(group[groupByColumnWithoutQuotes]);
}
formattedResult.push({
groupByDimensionValues: dimensionValues,
...group,
});
});
return formattedResult;
}
private parseGroupByArgs(
args: GroupByResolverArgs,
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
): { fieldMetadata: FieldMetadataEntity; subFieldName?: string }[] {
const groupByFieldNames = args.groupBy;
const groupByFields = [];
for (const fieldNames of groupByFieldNames) {
if (Object.keys(fieldNames).length > 1) {
throw new GraphqlQueryRunnerException(
'You cannot provide multiple fields in one GroupByInput, split them into multiple GroupByInput',
GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
);
}
for (const fieldName of Object.keys(fieldNames)) {
const fieldMetadataId =
objectMetadataItemWithFieldMaps.fieldIdByName[fieldName];
const fieldMetadata =
objectMetadataItemWithFieldMaps.fieldsById[fieldMetadataId];
if (fieldNames[fieldName] === true) {
groupByFields.push({
fieldMetadata,
subFieldName: undefined,
});
continue;
} else if (typeof fieldNames[fieldName] === 'object') {
if (Object.keys(fieldNames[fieldName]).length > 1) {
throw new GraphqlQueryRunnerException(
'You cannot provide multiple subfields in one GroupByInput, split them into multiple GroupByInput',
GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
);
}
for (const subFieldName of Object.keys(fieldNames[fieldName])) {
if (fieldNames[fieldName][subFieldName] === true) {
groupByFields.push({
fieldMetadata,
subFieldName,
});
continue;
}
}
}
}
}
return groupByFields;
return formatResultWithGroupByDimensionValues(result, groupByDefinitions);
}
private async addFiltersFromView({
@@ -0,0 +1,14 @@
import { type ObjectRecordGroupByDateGranularity } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
export type GroupByRegularField = {
fieldMetadata: FieldMetadataEntity;
subFieldName?: string;
};
export type GroupByDateField = {
fieldMetadata: FieldMetadataEntity;
subFieldName?: string;
dateGranularity: ObjectRecordGroupByDateGranularity;
};
export type GroupByField = GroupByRegularField | GroupByDateField;
@@ -0,0 +1,113 @@
import { t } from '@lingui/core/macro';
import {
ObjectRecordGroupByDateGranularity,
type ObjectRecord,
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { type IEdge } from 'src/engine/api/graphql/workspace-query-runner/interfaces/edge.interface';
import { type IGroupByConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/group-by-connection.interface';
import { removeQuotes } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/remove-quote.util';
export const formatResultWithGroupByDimensionValues = (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
result: any[],
groupByColumnsWithQuotes: {
columnNameWithQuotes: string;
alias: string;
dateGranularity?: ObjectRecordGroupByDateGranularity;
}[],
): IGroupByConnection<ObjectRecord, IEdge<ObjectRecord>>[] => {
let formattedResult: IGroupByConnection<ObjectRecord, IEdge<ObjectRecord>>[] =
[];
result.forEach((group) => {
let dimensionValues = [];
for (const groupByColumn of groupByColumnsWithQuotes) {
dimensionValues.push(group[groupByColumn.alias]);
}
const groupWithValueMappedToUnaliasedColumn = {
...group,
...groupByColumnsWithQuotes.reduce<Record<string, unknown>>(
(acc, groupByColumn) => {
const value = group[groupByColumn.alias];
acc[removeQuotes(groupByColumn.columnNameWithQuotes)] =
getTranslatedValueIfApplicable(
value,
groupByColumn.dateGranularity,
);
return acc;
},
{},
),
};
formattedResult.push({
groupByDimensionValues: dimensionValues,
...groupWithValueMappedToUnaliasedColumn,
});
});
return formattedResult;
};
const getTranslatedValueIfApplicable = (
value: unknown,
dateGranularity?: ObjectRecordGroupByDateGranularity,
) => {
switch (dateGranularity) {
case ObjectRecordGroupByDateGranularity.DAY_OF_THE_WEEK:
switch (value) {
case 'Monday':
return t`Monday`;
case 'Tuesday':
return t`Tuesday`;
case 'Wednesday':
return t`Wednesday`;
case 'Thursday':
return t`Thursday`;
case 'Friday':
return t`Friday`;
case 'Saturday':
return t`Saturday`;
case 'Sunday':
return t`Sunday`;
default:
return value;
}
case ObjectRecordGroupByDateGranularity.MONTH_OF_THE_YEAR:
switch (value) {
case 'January':
return t`January`;
case 'February':
return t`February`;
case 'March':
return t`March`;
case 'April':
return t`April`;
case 'May':
return t`May`;
case 'June':
return t`June`;
case 'July':
return t`July`;
case 'August':
return t`August`;
case 'September':
return t`September`;
case 'October':
return t`October`;
case 'November':
return t`November`;
case 'December':
return t`December`;
default:
return value;
}
default:
return value;
}
};
@@ -0,0 +1,35 @@
import { assertUnreachable } from 'twenty-shared/utils';
import { ObjectRecordGroupByDateGranularity } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { type GroupByField } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/types/group-by-field.types';
import { isGroupByDateField } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/is-group-by-date-field.util';
export const getGroupByExpression = ({
groupByField,
columnNameWithQuotes,
}: {
groupByField: GroupByField;
columnNameWithQuotes: string;
}) => {
if (!isGroupByDateField(groupByField)) {
return columnNameWithQuotes;
}
switch (groupByField.dateGranularity) {
case ObjectRecordGroupByDateGranularity.NONE:
return columnNameWithQuotes;
case ObjectRecordGroupByDateGranularity.DAY_OF_THE_WEEK:
return `TRIM(TO_CHAR(${columnNameWithQuotes}, 'TMDay'))`;
case ObjectRecordGroupByDateGranularity.MONTH_OF_THE_YEAR:
return `TRIM(TO_CHAR(${columnNameWithQuotes}, 'TMMonth'))`;
case ObjectRecordGroupByDateGranularity.QUARTER_OF_THE_YEAR:
return `TRIM(TO_CHAR(${columnNameWithQuotes}, '"Q"Q'))`;
case ObjectRecordGroupByDateGranularity.DAY:
case ObjectRecordGroupByDateGranularity.MONTH:
case ObjectRecordGroupByDateGranularity.QUARTER:
case ObjectRecordGroupByDateGranularity.YEAR:
return `DATE_TRUNC('${groupByField.dateGranularity}', ${columnNameWithQuotes})`;
default:
assertUnreachable(groupByField.dateGranularity);
}
};
@@ -0,0 +1,10 @@
import {
type GroupByDateField,
type GroupByField,
} from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/types/group-by-field.types';
export const isGroupByDateField = (
groupByField: GroupByField,
): groupByField is GroupByDateField => {
return 'dateGranularity' in groupByField;
};
@@ -0,0 +1,114 @@
import { isDefined } from 'class-validator';
import { FieldMetadataType } from 'twenty-shared/types';
import { ObjectRecordGroupByDateGranularity } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { type GroupByResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import {
GraphqlQueryRunnerException,
GraphqlQueryRunnerExceptionCode,
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
import { type GroupByField } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/types/group-by-field.types';
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
const isGroupByDateFieldDefinition = (
fieldGroupByDefinition:
| boolean
| Record<string, boolean>
| { granularity: ObjectRecordGroupByDateGranularity }
| undefined,
): fieldGroupByDefinition is {
granularity: ObjectRecordGroupByDateGranularity;
} => {
if (
typeof fieldGroupByDefinition !== 'object' ||
!isDefined(fieldGroupByDefinition)
) {
return false;
}
if (!('granularity' in fieldGroupByDefinition)) {
return false;
}
const granularity = fieldGroupByDefinition.granularity;
return (
isDefined(granularity) &&
typeof granularity === 'string' &&
Object.values(ObjectRecordGroupByDateGranularity).includes(
granularity as ObjectRecordGroupByDateGranularity,
)
);
};
export const parseGroupByArgs = (
args: GroupByResolverArgs,
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
): GroupByField[] => {
const groupByFieldNames = args.groupBy;
const groupByFields: GroupByField[] = [];
for (const fieldNames of groupByFieldNames) {
if (Object.keys(fieldNames).length > 1) {
throw new GraphqlQueryRunnerException(
'You cannot provide multiple fields in one GroupByInput, split them into multiple GroupByInput',
GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
);
}
for (const fieldName of Object.keys(fieldNames)) {
const fieldMetadataId =
objectMetadataItemWithFieldMaps.fieldIdByName[fieldName];
const fieldMetadata =
objectMetadataItemWithFieldMaps.fieldsById[fieldMetadataId];
if (
fieldMetadata.type === FieldMetadataType.DATE ||
fieldMetadata.type === FieldMetadataType.DATE_TIME
) {
const fieldGroupByDefinition = fieldNames[fieldName];
const shouldGroupByDateGranularity = isGroupByDateFieldDefinition(
fieldGroupByDefinition,
);
if (shouldGroupByDateGranularity) {
groupByFields.push({
fieldMetadata,
dateGranularity: fieldGroupByDefinition.granularity,
});
continue;
}
}
if (fieldNames[fieldName] === true) {
groupByFields.push({
fieldMetadata,
subFieldName: undefined,
});
continue;
} else if (typeof fieldNames[fieldName] === 'object') {
if (Object.keys(fieldNames[fieldName]).length > 1) {
throw new GraphqlQueryRunnerException(
'You cannot provide multiple subfields in one GroupByInput, split them into multiple GroupByInput',
GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
);
}
for (const subFieldName of Object.keys(fieldNames[fieldName])) {
if (
(fieldNames[fieldName] as Record<string, boolean>)[subFieldName] ===
true
) {
groupByFields.push({
fieldMetadata,
subFieldName,
});
continue;
}
}
}
}
}
return groupByFields;
};
@@ -0,0 +1,3 @@
export const removeQuotes = (string: string): string => {
return string.replace(/["']/g, '');
};
@@ -13,7 +13,9 @@ export type ObjectRecordFilter = Partial<{
}>;
export type ObjectRecordGroupBy = Array<
ObjectRecordGroupByForAtomicField | ObjectRecordGroupByForCompositeField
| ObjectRecordGroupByForAtomicField
| ObjectRecordGroupByForCompositeField
| ObjectRecordGroupByForDateField
>;
export type ObjectRecordGroupByForAtomicField = Partial<{
@@ -24,6 +26,12 @@ export type ObjectRecordGroupByForCompositeField = Partial<{
[Property in keyof ObjectRecord]: Record<string, boolean>;
}>;
export type ObjectRecordGroupByForDateField = Partial<{
[Property in keyof ObjectRecord]: {
granularity: ObjectRecordGroupByDateGranularity;
};
}>;
export enum OrderByDirection {
AscNullsFirst = 'AscNullsFirst',
AscNullsLast = 'AscNullsLast',
@@ -31,6 +39,17 @@ export enum OrderByDirection {
DescNullsLast = 'DescNullsLast',
}
export enum ObjectRecordGroupByDateGranularity {
DAY = 'DAY',
MONTH = 'MONTH',
QUARTER = 'QUARTER',
YEAR = 'YEAR',
DAY_OF_THE_WEEK = 'DAY_OF_THE_WEEK',
QUARTER_OF_THE_YEAR = 'QUARTER_OF_THE_YEAR',
MONTH_OF_THE_YEAR = 'MONTH_OF_THE_YEAR',
NONE = 'NONE',
}
export type ObjectRecordOrderBy = Array<
ObjectRecordOrderByForScalarField | ObjectRecordOrderByForCompositeField
>;
@@ -8,7 +8,7 @@ import {
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
import { GraphqlQueryGroupByResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-group-by-resolver.service';
import { GraphqlQueryGroupByResolverService } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/graphql-query-group-by-resolver.service';
import { RESOLVER_METHOD_NAMES } from 'src/engine/api/graphql/workspace-resolver-builder/constants/resolver-method-names';
@Injectable()
@@ -5,6 +5,7 @@ import { type CompositeType } from 'src/engine/metadata-modules/field-metadata/i
import { CompositeFieldMetadataGqlEnumTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/enum-types/composite-field-metadata-gql-enum-type.generator';
import { EnumFieldMetadataGqlEnumTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/enum-types/enum-field-metadata-gql-enum-type.generator';
import { CompositeFieldMetadataGqlInputTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/composite-field-metadata-gql-input-type.generator';
import { GroupByDateGranularityInputTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/group-by-input/group-by-date-granularity-gql-input-type.generator';
import { ObjectMetadataGqlInputTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/object-metadata-gql-input-type.generator';
import { RelationConnectGqlInputTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/relation-connect-gql-input-type.generator';
import { CompositeFieldMetadataGqlObjectTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/composite-field-metadata-gql-object-type.generator';
@@ -36,6 +37,7 @@ export class GqlTypeGenerator {
private readonly groupByConnectionGqlObjectTypeGenerator: GroupByConnectionGqlObjectTypeGenerator,
private readonly objectMetadataWithRelationsGqlObjectTypeGenerator: ObjectMetadataWithRelationsGqlObjectTypeGenerator,
private readonly relationConnectGqlInputTypeGenerator: RelationConnectGqlInputTypeGenerator,
private readonly groupByDateGranularityInputTypeGenerator: GroupByDateGranularityInputTypeGenerator,
private readonly queryTypeGenerator: QueryTypeGenerator,
private readonly mutationTypeGenerator: MutationTypeGenerator,
) {}
@@ -47,6 +49,7 @@ export class GqlTypeGenerator {
const compositeTypeCollection = [...compositeTypeDefinitions.values()];
this.buildAndStoreCompositeFieldMetadataGqlTypes(compositeTypeCollection);
this.buildAndStoreDateFieldMetadataGroupByGqlTypes();
this.buildAndStoreObjectMetadataGqlTypes(objectMetadataCollection);
await this.queryTypeGenerator.buildAndStore(
objectMetadataCollection,
@@ -71,6 +74,10 @@ export class GqlTypeGenerator {
}
}
private buildAndStoreDateFieldMetadataGroupByGqlTypes() {
this.groupByDateGranularityInputTypeGenerator.buildAndStore();
}
private buildAndStoreObjectMetadataGqlTypes(
dynamicObjectMetadataCollection: ObjectMetadataEntity[],
) {
@@ -0,0 +1,42 @@
import { Injectable } from '@nestjs/common';
import { GraphQLEnumType, GraphQLInputObjectType } from 'graphql';
import { ObjectRecordGroupByDateGranularity } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { GqlTypesStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/gql-types.storage';
export const GROUP_BY_DATE_GRANULARITY_INPUT_KEY = 'GroupByDateInput';
@Injectable()
export class GroupByDateGranularityInputTypeGenerator {
constructor(private readonly gqlTypesStorage: GqlTypesStorage) {}
public buildAndStore() {
const key = GROUP_BY_DATE_GRANULARITY_INPUT_KEY;
const type = new GraphQLInputObjectType({
name: GROUP_BY_DATE_GRANULARITY_INPUT_KEY,
fields: {
granularity: {
type: new GraphQLEnumType({
name: 'DateGranularityEnum',
values: Object.entries(ObjectRecordGroupByDateGranularity).reduce(
(acc, [key, value]) => {
acc[key] = { value };
return acc;
},
{} 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)',
}),
description:
'Date granularity (e.g. day, month, quarter, year, day of the week, quarter of the year, month of the year)',
},
},
});
this.gqlTypesStorage.addGqlType(key, type);
}
}
@@ -4,11 +4,15 @@ import {
GraphQLBoolean,
GraphQLInputFieldConfigMap,
GraphQLInputObjectType,
GraphQLInputType,
GraphQLUnionType,
isInputObjectType,
} from 'graphql';
import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { GqlInputTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schema-builder/enums/gql-input-type-definition-kind.enum';
import { GROUP_BY_DATE_GRANULARITY_INPUT_KEY } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/group-by-input/group-by-date-granularity-gql-input-type.generator';
import { RelationFieldMetadataGqlInputTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/relation-field-metadata-gql-type.generator';
import { TypeMapperService } from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service';
import { GqlTypesStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/gql-types.storage';
@@ -70,7 +74,30 @@ export class ObjectMetadataGroupByGqlInputTypeGenerator {
if (isCompositeFieldMetadataType(fieldMetadata.type))
return this.generateCompositeFieldGroupByInputType(fieldMetadata);
const type = this.typeMapperService.applyTypeOptions(GraphQLBoolean, {});
let type: GraphQLInputType | GraphQLUnionType;
if (
fieldMetadata.type === FieldMetadataType.DATE ||
fieldMetadata.type === FieldMetadataType.DATE_TIME
) {
const groupByDateGranularityInputType =
this.gqlTypesStorage.getGqlTypeByKey(
GROUP_BY_DATE_GRANULARITY_INPUT_KEY,
);
if (
!isDefined(groupByDateGranularityInputType) ||
!isInputObjectType(groupByDateGranularityInputType)
) {
throw new Error(
'Could not find a GraphQL input type for GroupByDateGranularityInput',
);
}
type = groupByDateGranularityInputType;
} else {
type = this.typeMapperService.applyTypeOptions(GraphQLBoolean, {});
}
return {
[fieldMetadata.name]: {
@@ -7,6 +7,7 @@ import { ObjectMetadataCreateGqlInputTypeGenerator } from 'src/engine/api/graphq
import { CompositeFieldMetadataFilterGqlInputTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/filter-input/composite-field-metadata-filter-gql-input-types.generator';
import { ObjectMetadataFilterGqlInputTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/filter-input/object-metadata-filter-gql-input-type.generator';
import { CompositeFieldMetadataGroupByGqlInputTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/group-by-input/composite-field-metadata-group-by-gql-input-type.generator';
import { GroupByDateGranularityInputTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/group-by-input/group-by-date-granularity-gql-input-type.generator';
import { ObjectMetadataGroupByGqlInputTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/group-by-input/object-metadata-group-by-gql-input-type.generator';
import { ObjectMetadataGqlInputTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/object-metadata-gql-input-type.generator';
import { CompositeFieldMetadataOrderByGqlInputTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/order-by-input/composite-field-metadata-order-by-gql-input-type.generator';
@@ -47,6 +48,7 @@ export const workspaceSchemaBuilderTypeGenerators = [
ObjectMetadataOrderByGqlInputTypeGenerator,
ObjectMetadataGroupByGqlInputTypeGenerator,
ObjectMetadataOrderByWithGroupByGqlInputTypeGenerator,
GroupByDateGranularityInputTypeGenerator,
CompositeFieldMetadataGqlObjectTypeGenerator,
ObjectMetadataGqlObjectTypeGenerator,
RelationConnectGqlInputTypeGenerator,