[AI] Add group_by_* database tools and centralize groupBy validation (#19406)

closes
https://discord.com/channels/1130383047699738754/1488990242873806868



https://github.com/user-attachments/assets/2b2bbfba-3fa6-4114-9a26-96a61599d748

<img width="729" height="1283" alt="CleanShot 2026-04-07 at 20 43 06"
src="https://github.com/user-attachments/assets/815efb97-81a0-44ea-8d79-b3ce7d5b00b6"
/>


<img width="708" height="1266" alt="CleanShot 2026-04-07 at 20 40 13"
src="https://github.com/user-attachments/assets/692366bc-b629-4d9f-b6b8-ab670d5ad046"
/>

<img width="665" height="3524" alt="CleanShot 2026-04-07 at 20 42 00"
src="https://github.com/user-attachments/assets/5e844e0f-7835-47a8-9d20-a5baddc0992d"
/>
This commit is contained in:
nitin
2026-04-10 14:15:18 +05:30
committed by GitHub
parent 43ce396152
commit f13e7e01fe
32 changed files with 1504 additions and 258 deletions
@@ -1,9 +1,31 @@
import { Injectable } from '@nestjs/common';
import {
AggregateOperations,
type RestrictedFieldsPermissions,
} from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type GroupByField } from 'src/engine/api/common/common-query-runners/types/group-by-field.types';
import {
CommonQueryRunnerException,
CommonQueryRunnerExceptionCode,
} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
import { STANDARD_ERROR_MESSAGE } from 'src/engine/api/common/common-query-runners/errors/standard-error-message.constant';
import { validateAndTransformGroupByFieldsOrThrow } from 'src/engine/api/common/common-args-processors/group-by-arg-processor/utils/validate-and-transform-group-by-fields-or-throw.util';
import {
type AggregationField,
getAvailableAggregationsFromObjectFields,
} from 'src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util';
import {
ObjectRecordGroupByForAtomicField,
ObjectRecordGroupByForCompositeField,
ObjectRecordGroupByForDateField,
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { findManyFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps.util';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { resolveAggregateFieldKey } from 'src/engine/core-modules/record-crud/utils/resolve-aggregate-field-key.util';
@Injectable()
export class GroupByArgProcessorService {
@@ -30,4 +52,113 @@ export class GroupByArgProcessorService {
return [groupBy];
}
validateAndTransformGroupByFieldsOrThrow({
groupBy,
flatObjectMetadata,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
}: {
groupBy: Array<
| ObjectRecordGroupByForAtomicField
| ObjectRecordGroupByForCompositeField
| ObjectRecordGroupByForDateField
>;
flatObjectMetadata: FlatObjectMetadata;
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
}): GroupByField[] {
return validateAndTransformGroupByFieldsOrThrow({
groupBy,
flatObjectMetadata,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
});
}
getAvailableAggregations({
flatObjectMetadata,
flatFieldMetadataMaps,
restrictedFields,
}: {
flatObjectMetadata: FlatObjectMetadata;
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
restrictedFields?: RestrictedFieldsPermissions;
}): Record<string, AggregationField> {
const objectFields = findManyFlatEntityByIdInFlatEntityMaps({
flatEntityIds: flatObjectMetadata.fieldIds,
flatEntityMaps: flatFieldMetadataMaps,
}).filter((field) => restrictedFields?.[field.id]?.canRead !== false);
return getAvailableAggregationsFromObjectFields(objectFields);
}
validateAggregateFieldKeysOrThrow({
aggregateFieldKeys,
availableAggregations,
}: {
aggregateFieldKeys: string[];
availableAggregations: Record<string, AggregationField>;
}): void {
const invalidAggregateFieldKeys = aggregateFieldKeys.filter(
(aggregateFieldKey) =>
!isDefined(availableAggregations[aggregateFieldKey]),
);
if (invalidAggregateFieldKeys.length === 0) {
return;
}
throw new CommonQueryRunnerException(
`Unknown aggregate field(s): ${invalidAggregateFieldKeys.join(', ')}`,
CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
{ userFriendlyMessage: STANDARD_ERROR_MESSAGE },
);
}
resolveToolAggregateFieldKeyOrThrow({
aggregateOperation,
aggregateFieldName,
availableAggregations,
}: {
aggregateOperation: keyof typeof AggregateOperations;
aggregateFieldName?: string;
availableAggregations: Record<string, AggregationField>;
}): string {
if (aggregateOperation === AggregateOperations.COUNT) {
if (aggregateFieldName) {
throw new CommonQueryRunnerException(
'aggregateFieldName is not supported for COUNT operation',
CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
{ userFriendlyMessage: STANDARD_ERROR_MESSAGE },
);
}
return 'totalCount';
}
if (!aggregateFieldName) {
throw new CommonQueryRunnerException(
`aggregateFieldName is required for ${aggregateOperation} operation`,
CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
{ userFriendlyMessage: STANDARD_ERROR_MESSAGE },
);
}
const aggregateFieldKey = resolveAggregateFieldKey(
aggregateOperation,
aggregateFieldName,
availableAggregations,
);
if (!aggregateFieldKey) {
throw new CommonQueryRunnerException(
`No aggregation available for ${aggregateOperation} on field "${aggregateFieldName}"`,
CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
{ userFriendlyMessage: STANDARD_ERROR_MESSAGE },
);
}
return aggregateFieldKey;
}
}
@@ -0,0 +1 @@
export type CompositeFieldGroupByDefinition = Record<string, boolean>;
@@ -0,0 +1,10 @@
import {
type FirstDayOfTheWeek,
type ObjectRecordGroupByDateGranularity,
} from 'twenty-shared/types';
export type DateFieldGroupByDefinition = {
granularity: ObjectRecordGroupByDateGranularity;
weekStartDay?: FirstDayOfTheWeek;
timeZone?: string;
};
@@ -0,0 +1,8 @@
import { type CompositeFieldGroupByDefinition } from 'src/engine/api/common/common-args-processors/group-by-arg-processor/types/composite-field-group-by-definition.type';
import { type DateFieldGroupByDefinition } from 'src/engine/api/common/common-args-processors/group-by-arg-processor/types/date-field-group-by-definition.type';
export type FieldGroupByDefinition =
| boolean
| CompositeFieldGroupByDefinition
| DateFieldGroupByDefinition
| undefined;
@@ -0,0 +1,27 @@
import { ObjectRecordGroupByDateGranularity } from 'twenty-shared/types';
import { isPlainObject } from 'twenty-shared/utils';
import { type DateFieldGroupByDefinition } from 'src/engine/api/common/common-args-processors/group-by-arg-processor/types/date-field-group-by-definition.type';
const GROUP_BY_DATE_GRANULARITIES = new Set<string>(
Object.values(ObjectRecordGroupByDateGranularity),
);
export const isGroupByDateFieldDefinition = (
fieldGroupByDefinition: unknown,
): fieldGroupByDefinition is DateFieldGroupByDefinition => {
if (!isPlainObject(fieldGroupByDefinition)) {
return false;
}
if (!('granularity' in fieldGroupByDefinition)) {
return false;
}
const granularity = fieldGroupByDefinition.granularity;
return (
typeof granularity === 'string' &&
GROUP_BY_DATE_GRANULARITIES.has(granularity)
);
};
@@ -0,0 +1,16 @@
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { isFlatFieldMetadataSupportedInGroupBy } from 'src/engine/metadata-modules/field-metadata/utils/is-supported-in-group-by.util';
export const isRelationNestedFieldSupportedInGroupBy = ({
nestedFieldName,
nestedFieldMetadata,
}: {
nestedFieldName: string;
nestedFieldMetadata: FlatFieldMetadata;
}): boolean => {
if (nestedFieldName === 'id') {
return true;
}
return isFlatFieldMetadataSupportedInGroupBy(nestedFieldMetadata);
};
@@ -0,0 +1,271 @@
import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined, isPlainObject } from 'twenty-shared/utils';
import { type GroupByField } from 'src/engine/api/common/common-query-runners/types/group-by-field.types';
import {
CommonQueryRunnerException,
CommonQueryRunnerExceptionCode,
} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
import { STANDARD_ERROR_MESSAGE } from 'src/engine/api/common/common-query-runners/errors/standard-error-message.constant';
import { isGroupByDateFieldDefinition } from 'src/engine/api/common/common-args-processors/group-by-arg-processor/utils/is-group-by-date-field-definition.util';
import { validateAndTransformRelationGroupByFieldOrThrow } from 'src/engine/api/common/common-args-processors/group-by-arg-processor/utils/validate-and-transform-relation-group-by-field-or-throw.util';
import { validateSingleKeyForGroupByOrThrow } from 'src/engine/api/common/common-args-processors/group-by-arg-processor/utils/validate-single-key-for-group-by-or-throw.util';
import {
ObjectRecordGroupByForAtomicField,
ObjectRecordGroupByForCompositeField,
ObjectRecordGroupByForDateField,
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
import { getGroupableSubFieldsForCompositeType } from 'src/engine/metadata-modules/field-metadata/utils/get-groupable-sub-fields-for-composite-type.util';
import { isFlatFieldMetadataSupportedInGroupBy } from 'src/engine/metadata-modules/field-metadata/utils/is-supported-in-group-by.util';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util';
import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
const getFieldMetadataForGroupByOrThrow = ({
fieldName,
fieldIdByName,
fieldIdByJoinColumnName,
flatFieldMetadataMaps,
}: {
fieldName: string;
fieldIdByName: Record<string, string>;
fieldIdByJoinColumnName: Record<string, string>;
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
}): FlatFieldMetadata => {
const fieldMetadataId =
fieldIdByName[fieldName] || fieldIdByJoinColumnName[fieldName];
const fieldMetadata = fieldMetadataId
? findFlatEntityByIdInFlatEntityMaps({
flatEntityId: fieldMetadataId,
flatEntityMaps: flatFieldMetadataMaps,
})
: undefined;
if (!isDefined(fieldMetadata) || !isDefined(fieldMetadataId)) {
throw new CommonQueryRunnerException(
`Unidentified field in groupBy: ${fieldName}`,
CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
{ userFriendlyMessage: STANDARD_ERROR_MESSAGE },
);
}
return fieldMetadata;
};
const validateAndTransformCompositeGroupByDefinitionOrThrow = ({
fieldName,
fieldMetadata,
fieldGroupByDefinition,
groupByFields,
}: {
fieldName: string;
fieldMetadata: FlatFieldMetadata;
fieldGroupByDefinition: Record<string, unknown>;
groupByFields: GroupByField[];
}) => {
if (!isCompositeFieldMetadataType(fieldMetadata.type)) {
throw new CommonQueryRunnerException(
`Field "${fieldName}" does not support nested subfields in groupBy`,
CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
{ userFriendlyMessage: STANDARD_ERROR_MESSAGE },
);
}
const supportedCompositeSubFields = getGroupableSubFieldsForCompositeType(
fieldMetadata.type,
);
validateSingleKeyForGroupByOrThrow({
groupByKeys: Object.keys(fieldGroupByDefinition),
errorMessage:
'You cannot provide multiple subfields in one GroupByInput, split them into multiple GroupByInput',
});
for (const subFieldName of Object.keys(fieldGroupByDefinition)) {
if (
isCompositeFieldMetadataType(fieldMetadata.type) &&
!supportedCompositeSubFields?.includes(subFieldName)
) {
throw new CommonQueryRunnerException(
`Composite subfield "${subFieldName}" is not supported in groupBy for "${fieldName}"`,
CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
{ userFriendlyMessage: STANDARD_ERROR_MESSAGE },
);
}
if (fieldGroupByDefinition[subFieldName] !== true) {
throw new CommonQueryRunnerException(
`Composite subfield "${subFieldName}" must be set to true in groupBy for "${fieldName}"`,
CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
{ userFriendlyMessage: STANDARD_ERROR_MESSAGE },
);
}
groupByFields.push({
fieldMetadata,
subFieldName,
});
}
};
const validateAndTransformSingleGroupByFieldOrThrow = ({
fieldNames,
fieldName,
fieldIdByName,
fieldIdByJoinColumnName,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
groupByFields,
}: {
fieldNames: Record<string, unknown>;
fieldName: string;
fieldIdByName: Record<string, string>;
fieldIdByJoinColumnName: Record<string, string>;
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
groupByFields: GroupByField[];
}) => {
const fieldMetadata = getFieldMetadataForGroupByOrThrow({
fieldName,
fieldIdByName,
fieldIdByJoinColumnName,
flatFieldMetadataMaps,
});
if (!isFlatFieldMetadataSupportedInGroupBy(fieldMetadata)) {
throw new CommonQueryRunnerException(
`Field "${fieldName}" is not supported in groupBy`,
CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
{ userFriendlyMessage: STANDARD_ERROR_MESSAGE },
);
}
const fieldGroupByDefinition = fieldNames[fieldName];
const isObjectFieldGroupByDefinition = isPlainObject(fieldGroupByDefinition);
const isGroupByRelationField =
isMorphOrRelationFlatFieldMetadata(fieldMetadata) &&
isObjectFieldGroupByDefinition &&
!isGroupByDateFieldDefinition(fieldGroupByDefinition);
const isGroupByRelationJoinColumnField =
isMorphOrRelationFlatFieldMetadata(fieldMetadata) &&
fieldGroupByDefinition === true &&
isDefined(fieldIdByJoinColumnName[fieldName]);
if (isGroupByRelationField || isGroupByRelationJoinColumnField) {
const normalizedFieldNames = isGroupByRelationJoinColumnField
? { ...fieldNames, [fieldName]: { id: true } }
: fieldNames;
validateAndTransformRelationGroupByFieldOrThrow({
fieldNames: normalizedFieldNames,
fieldName,
fieldMetadata,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
groupByFields,
});
return;
}
if (
(fieldMetadata.type === FieldMetadataType.DATE ||
fieldMetadata.type === FieldMetadataType.DATE_TIME) &&
isGroupByDateFieldDefinition(fieldGroupByDefinition)
) {
groupByFields.push({
fieldMetadata,
dateGranularity: fieldGroupByDefinition.granularity,
weekStartDay: fieldGroupByDefinition.weekStartDay,
timeZone: fieldGroupByDefinition.timeZone,
});
return;
}
if (isObjectFieldGroupByDefinition && 'unnest' in fieldGroupByDefinition) {
groupByFields.push({
fieldMetadata,
subFieldName: undefined,
shouldUnnest: true,
});
return;
}
if (fieldGroupByDefinition === true) {
groupByFields.push({
fieldMetadata,
subFieldName: undefined,
});
return;
}
if (isObjectFieldGroupByDefinition) {
validateAndTransformCompositeGroupByDefinitionOrThrow({
fieldName,
fieldMetadata,
fieldGroupByDefinition,
groupByFields,
});
return;
}
throw new CommonQueryRunnerException(
`Invalid groupBy definition for field "${fieldName}"`,
CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
{ userFriendlyMessage: STANDARD_ERROR_MESSAGE },
);
};
export const validateAndTransformGroupByFieldsOrThrow = ({
groupBy,
flatObjectMetadata,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
}: {
groupBy: Array<
| ObjectRecordGroupByForAtomicField
| ObjectRecordGroupByForCompositeField
| ObjectRecordGroupByForDateField
>;
flatObjectMetadata: FlatObjectMetadata;
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
}): GroupByField[] => {
const groupByFields: GroupByField[] = [];
const { fieldIdByName, fieldIdByJoinColumnName } =
buildFieldMapsFromFlatObjectMetadata(
flatFieldMetadataMaps,
flatObjectMetadata,
);
for (const fieldNames of groupBy) {
validateSingleKeyForGroupByOrThrow({
groupByKeys: Object.keys(fieldNames),
errorMessage:
'You cannot provide multiple fields in one GroupByInput, split them into multiple GroupByInput',
});
for (const fieldName of Object.keys(fieldNames)) {
validateAndTransformSingleGroupByFieldOrThrow({
fieldNames,
fieldName,
fieldIdByName,
fieldIdByJoinColumnName,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
groupByFields,
});
}
}
return groupByFields;
};
@@ -0,0 +1,245 @@
import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined, isPlainObject } from 'twenty-shared/utils';
import { type CompositeFieldGroupByDefinition } from 'src/engine/api/common/common-args-processors/group-by-arg-processor/types/composite-field-group-by-definition.type';
import { isGroupByDateFieldDefinition } from 'src/engine/api/common/common-args-processors/group-by-arg-processor/utils/is-group-by-date-field-definition.util';
import { isRelationNestedFieldSupportedInGroupBy } from 'src/engine/api/common/common-args-processors/group-by-arg-processor/utils/is-relation-nested-field-supported-in-group-by.util';
import { validateSingleKeyForGroupByOrThrow } from 'src/engine/api/common/common-args-processors/group-by-arg-processor/utils/validate-single-key-for-group-by-or-throw.util';
import {
CommonQueryRunnerException,
CommonQueryRunnerExceptionCode,
} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
import { STANDARD_ERROR_MESSAGE } from 'src/engine/api/common/common-query-runners/errors/standard-error-message.constant';
import { type GroupByField } from 'src/engine/api/common/common-query-runners/types/group-by-field.types';
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
import { getGroupableSubFieldsForCompositeType } from 'src/engine/metadata-modules/field-metadata/utils/get-groupable-sub-fields-for-composite-type.util';
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
const getNestedFieldMetadataDetails = ({
fieldNames,
fieldName,
fieldMetadata,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
}: {
fieldNames: Record<string, unknown>;
fieldName: string;
fieldMetadata: FlatFieldMetadata;
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
}) => {
const nestedFieldGroupByDefinitions = fieldNames[fieldName];
if (!isPlainObject(nestedFieldGroupByDefinitions)) {
throw new CommonQueryRunnerException(
`Invalid groupBy definition for relation field "${fieldName}"`,
CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
{ userFriendlyMessage: STANDARD_ERROR_MESSAGE },
);
}
if (!isDefined(fieldMetadata.relationTargetObjectMetadataId)) {
throw new CommonQueryRunnerException(
`Relation target object metadata id not found for field ${fieldMetadata.name}`,
CommonQueryRunnerExceptionCode.INTERNAL_SERVER_ERROR,
{ userFriendlyMessage: STANDARD_ERROR_MESSAGE },
);
}
const targetObjectMetadata = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: fieldMetadata.relationTargetObjectMetadataId,
flatEntityMaps: flatObjectMetadataMaps,
});
if (!isDefined(targetObjectMetadata)) {
throw new CommonQueryRunnerException(
`Target object metadata not found for relation field ${fieldMetadata.name}`,
CommonQueryRunnerExceptionCode.INTERNAL_SERVER_ERROR,
{ userFriendlyMessage: STANDARD_ERROR_MESSAGE },
);
}
const nestedFieldNames = Object.keys(nestedFieldGroupByDefinitions);
validateSingleKeyForGroupByOrThrow({
groupByKeys: nestedFieldNames,
errorMessage:
'You cannot provide multiple nested fields in one relation GroupByInput, split them into multiple GroupByInput',
});
const nestedFieldName = nestedFieldNames[0];
const { fieldIdByName } = buildFieldMapsFromFlatObjectMetadata(
flatFieldMetadataMaps,
targetObjectMetadata,
);
const nestedFieldMetadataId = fieldIdByName[nestedFieldName];
const nestedFieldMetadata = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: nestedFieldMetadataId,
flatEntityMaps: flatFieldMetadataMaps,
});
if (!isDefined(nestedFieldMetadata) || !isDefined(nestedFieldMetadataId)) {
throw new CommonQueryRunnerException(
`Nested field "${nestedFieldName}" not found in target object "${targetObjectMetadata.nameSingular}"`,
CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
{ userFriendlyMessage: STANDARD_ERROR_MESSAGE },
);
}
if (
!isRelationNestedFieldSupportedInGroupBy({
nestedFieldName,
nestedFieldMetadata,
})
) {
throw new CommonQueryRunnerException(
`Nested field "${nestedFieldName}" is not supported in groupBy`,
CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
{ userFriendlyMessage: STANDARD_ERROR_MESSAGE },
);
}
if (nestedFieldMetadata.type === FieldMetadataType.RELATION) {
throw new CommonQueryRunnerException(
`Cannot group by a relation field of the relation field: "${nestedFieldName}" is a relation field of "${targetObjectMetadata.nameSingular}"`,
CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
{ userFriendlyMessage: STANDARD_ERROR_MESSAGE },
);
}
const nestedFieldGroupByDefinition =
nestedFieldGroupByDefinitions[nestedFieldName];
return {
nestedFieldGroupByDefinition,
nestedFieldMetadata,
nestedFieldName,
};
};
const validateAndTransformNestedCompositeFieldOrThrow = ({
nestedFieldGroupByDefinition,
nestedFieldName,
fieldMetadata,
nestedFieldMetadata,
groupByFields,
}: {
nestedFieldGroupByDefinition: unknown;
nestedFieldName: string;
fieldMetadata: FlatFieldMetadata;
nestedFieldMetadata: FlatFieldMetadata;
groupByFields: GroupByField[];
}) => {
if (!isPlainObject(nestedFieldGroupByDefinition)) {
throw new CommonQueryRunnerException(
`Composite field "${nestedFieldName}" requires a subfield to be specified`,
CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
{ userFriendlyMessage: STANDARD_ERROR_MESSAGE },
);
}
const compositeSubFields = Object.keys(nestedFieldGroupByDefinition);
validateSingleKeyForGroupByOrThrow({
groupByKeys: compositeSubFields,
errorMessage:
'You cannot provide multiple composite subfields in one GroupByInput, split them into multiple GroupByInput',
});
const nestedSubFieldName = compositeSubFields[0];
const supportedCompositeSubFields = getGroupableSubFieldsForCompositeType(
nestedFieldMetadata.type,
);
if (
!supportedCompositeSubFields?.includes(nestedSubFieldName) ||
nestedFieldGroupByDefinition[nestedSubFieldName] !== true
) {
throw new CommonQueryRunnerException(
`Composite subfield "${nestedSubFieldName}" is not supported in groupBy for "${nestedFieldName}"`,
CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
{ userFriendlyMessage: STANDARD_ERROR_MESSAGE },
);
}
groupByFields.push({
fieldMetadata,
nestedFieldMetadata,
nestedSubFieldName,
});
};
export const validateAndTransformRelationGroupByFieldOrThrow = ({
fieldNames,
fieldName,
fieldMetadata,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
groupByFields,
}: {
fieldNames: Record<string, unknown>;
fieldName: string;
fieldMetadata: FlatFieldMetadata;
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
groupByFields: GroupByField[];
}) => {
const { nestedFieldGroupByDefinition, nestedFieldMetadata, nestedFieldName } =
getNestedFieldMetadataDetails({
fieldNames,
fieldName,
fieldMetadata,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
});
if (
(nestedFieldMetadata.type === FieldMetadataType.DATE ||
nestedFieldMetadata.type === FieldMetadataType.DATE_TIME) &&
isGroupByDateFieldDefinition(nestedFieldGroupByDefinition)
) {
const dateFieldDefinition = nestedFieldGroupByDefinition;
groupByFields.push({
fieldMetadata,
nestedFieldMetadata,
dateGranularity: dateFieldDefinition.granularity,
weekStartDay: dateFieldDefinition.weekStartDay,
timeZone: dateFieldDefinition.timeZone,
});
return;
}
if (isCompositeFieldMetadataType(nestedFieldMetadata.type)) {
validateAndTransformNestedCompositeFieldOrThrow({
nestedFieldGroupByDefinition,
nestedFieldName,
fieldMetadata,
nestedFieldMetadata,
groupByFields,
});
return;
}
if (nestedFieldGroupByDefinition === true) {
groupByFields.push({
fieldMetadata,
nestedFieldMetadata,
});
return;
}
throw new CommonQueryRunnerException(
`Invalid groupBy definition for nested field "${fieldName}.${nestedFieldName}"`,
CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
{ userFriendlyMessage: STANDARD_ERROR_MESSAGE },
);
};
@@ -0,0 +1,21 @@
import {
CommonQueryRunnerException,
CommonQueryRunnerExceptionCode,
} from 'src/engine/api/common/common-query-runners/errors/common-query-runner.exception';
import { STANDARD_ERROR_MESSAGE } from 'src/engine/api/common/common-query-runners/errors/standard-error-message.constant';
export const validateSingleKeyForGroupByOrThrow = ({
groupByKeys,
errorMessage,
}: {
groupByKeys: string[];
errorMessage: string;
}): void => {
if (groupByKeys.length > 1) {
throw new CommonQueryRunnerException(
errorMessage,
CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
{ userFriendlyMessage: STANDARD_ERROR_MESSAGE },
);
}
};
@@ -41,7 +41,6 @@ import {
import { CommonSelectedFieldsResult } from 'src/engine/api/common/types/common-selected-fields-result.type';
import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
import { formatResultWithGroupByDimensionValues } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/format-result-with-group-by-dimension-values.util';
import { parseGroupByArgs } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/parse-group-by-args.util';
import { GroupByWithRecordsService } from 'src/engine/api/graphql/graphql-query-runner/group-by/services/group-by-with-records.service';
import { getGroupLimit } from 'src/engine/api/graphql/graphql-query-runner/group-by/utils/get-group-limit.util';
import { ProcessAggregateHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-aggregate.helper';
@@ -94,12 +93,13 @@ export class CommonGroupByQueryRunnerService extends CommonBaseQueryRunnerServic
objectMetadataNameSingular,
);
const groupByFields = parseGroupByArgs(
args,
flatObjectMetadata,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
);
const groupByFields =
this.groupByArgProcessor.validateAndTransformGroupByFieldsOrThrow({
groupBy: args.groupBy,
flatObjectMetadata,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
});
const objectAlias = getObjectAlias(flatObjectMetadata);
@@ -393,9 +393,20 @@ export class CommonGroupByQueryRunnerService extends CommonBaseQueryRunnerServic
}
async validate(
_args: CommonInput<GroupByQueryArgs>,
_queryRunnerContext: CommonBaseQueryRunnerContext,
): Promise<void> {}
args: CommonInput<GroupByQueryArgs>,
queryRunnerContext: CommonBaseQueryRunnerContext,
): Promise<void> {
const normalizedGroupBy = this.groupByArgProcessor.process({
groupBy: args.groupBy,
});
this.groupByArgProcessor.validateAndTransformGroupByFieldsOrThrow({
groupBy: normalizedGroupBy,
flatObjectMetadata: queryRunnerContext.flatObjectMetadata,
flatObjectMetadataMaps: queryRunnerContext.flatObjectMetadataMaps,
flatFieldMetadataMaps: queryRunnerContext.flatFieldMetadataMaps,
});
}
async computeArgs(
args: CommonInput<GroupByQueryArgs>,
@@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { CommonArgsProcessors } from 'src/engine/api/common/common-args-processors/common-args-processors';
import { GroupByArgProcessorService } from 'src/engine/api/common/common-args-processors/group-by-arg-processor/group-by-arg-processor.service';
import { ProcessNestedRelationsV2Helper } from 'src/engine/api/common/common-nested-relations-processor/process-nested-relations-v2.helper';
import { ProcessNestedRelationsHelper } from 'src/engine/api/common/common-nested-relations-processor/process-nested-relations.helper';
import { CommonQueryRunners } from 'src/engine/api/common/common-query-runners/common-query-runners';
@@ -53,6 +54,6 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache
CommonResultGettersService,
GroupByWithRecordsService,
],
exports: [...CommonQueryRunners],
exports: [...CommonQueryRunners, GroupByArgProcessorService],
})
export class CoreCommonApiModule {}