[groupBy] groupBy relation fields (#15951)
Example query Here person has - a N - 1 relationship with company - a N - 1 morph relationship with pet or company <img width="862" height="374" alt="image" src="https://github.com/user-attachments/assets/59bc9b82-c943-43de-ad82-d3393b76904b" /> <img width="415" height="629" alt="image" src="https://github.com/user-attachments/assets/9a3176bc-99cd-4983-8611-68ca3a2cf527" /> truncated response <img width="299" height="447" alt="image" src="https://github.com/user-attachments/assets/45af0322-9e66-4eae-8353-6c0dda487bbe" /> We don't allow grouping by relations of relations. Left to do - rest api - tests on permissions
This commit is contained in:
+219
-7
@@ -4,6 +4,7 @@ import {
|
||||
FieldMetadataType,
|
||||
ObjectRecordGroupByDateGranularity,
|
||||
type ObjectRecordOrderByForCompositeField,
|
||||
type ObjectRecordOrderByForRelationField,
|
||||
type ObjectRecordOrderByForScalarField,
|
||||
type ObjectRecordOrderByWithGroupByDateField,
|
||||
OrderByDirection,
|
||||
@@ -19,9 +20,11 @@ import {
|
||||
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
|
||||
import { convertOrderByToFindOptionsOrder } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/utils/convert-order-by-to-find-options-order';
|
||||
import { parseCompositeFieldForOrder } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/utils/parse-composite-field-for-order.util';
|
||||
import { prepareForOrderByRelationFieldParsing } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/utils/prepare-for-order-by-relation-field-parsing.util';
|
||||
import {
|
||||
type GroupByDateField,
|
||||
type GroupByField,
|
||||
type GroupByRegularField,
|
||||
} from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/types/group-by-field.types';
|
||||
import { getGroupByExpression } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/get-group-by-expression.util';
|
||||
import { ProcessAggregateHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-aggregate.helper';
|
||||
@@ -34,6 +37,7 @@ import { UserInputError } from 'src/engine/core-modules/graphql/utils/graphql-er
|
||||
import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||
import { formatColumnNameForRelationField } from 'src/engine/twenty-orm/utils/format-column-name-for-relation-field.util';
|
||||
import { formatColumnNamesFromCompositeFieldAndSubfields } from 'src/engine/twenty-orm/utils/format-column-names-from-composite-field-and-subfield.util';
|
||||
|
||||
@@ -44,9 +48,14 @@ export type OrderByCondition = {
|
||||
|
||||
export class GraphqlQueryOrderFieldParser {
|
||||
private objectMetadataMapItem: ObjectMetadataItemWithFieldMaps;
|
||||
private objectMetadataMaps?: ObjectMetadataMaps;
|
||||
|
||||
constructor(objectMetadataMapItem: ObjectMetadataItemWithFieldMaps) {
|
||||
constructor(
|
||||
objectMetadataMapItem: ObjectMetadataItemWithFieldMaps,
|
||||
objectMetadataMaps?: ObjectMetadataMaps,
|
||||
) {
|
||||
this.objectMetadataMapItem = objectMetadataMapItem;
|
||||
this.objectMetadataMaps = objectMetadataMaps;
|
||||
}
|
||||
|
||||
parse(
|
||||
@@ -132,6 +141,12 @@ export class GraphqlQueryOrderFieldParser {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Object.keys(orderByArg).length > 1) {
|
||||
throw new UserInputError(
|
||||
'Please provide orderBy field criteria one by one in orderBy array',
|
||||
);
|
||||
}
|
||||
|
||||
const fieldName = Object.keys(orderByArg)[0];
|
||||
const fieldMetadataId =
|
||||
this.objectMetadataMapItem.fieldIdByName[fieldName];
|
||||
@@ -180,6 +195,24 @@ export class GraphqlQueryOrderFieldParser {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
this.isObjectRecordOrderByForRelationField(orderByArg, fieldMetadata)
|
||||
) {
|
||||
const parsedOrderByForRelationField =
|
||||
this.parseObjectRecordOrderByForRelationField({
|
||||
groupByFields,
|
||||
orderByArg,
|
||||
fieldMetadata,
|
||||
});
|
||||
|
||||
if (!isDefined(parsedOrderByForRelationField)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
parsedOrderBy.push(parsedOrderByForRelationField);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.isObjectRecordOrderByForCompositeField(orderByArg)) {
|
||||
const parsedOrderByForCompositeField =
|
||||
this.parseObjectRecordOrderByForCompositeField({
|
||||
@@ -221,7 +254,8 @@ export class GraphqlQueryOrderFieldParser {
|
||||
| ObjectRecordOrderByForScalarField
|
||||
| ObjectRecordOrderByForCompositeField
|
||||
| AggregateOrderByWithGroupByField
|
||||
| ObjectRecordOrderByWithGroupByDateField,
|
||||
| ObjectRecordOrderByWithGroupByDateField
|
||||
| ObjectRecordOrderByForRelationField,
|
||||
): orderByArg is AggregateOrderByWithGroupByField => {
|
||||
return isDefined(orderByArg.aggregate);
|
||||
};
|
||||
@@ -230,7 +264,8 @@ export class GraphqlQueryOrderFieldParser {
|
||||
orderByArg:
|
||||
| ObjectRecordOrderByForScalarField
|
||||
| ObjectRecordOrderByForCompositeField
|
||||
| ObjectRecordOrderByWithGroupByDateField,
|
||||
| ObjectRecordOrderByWithGroupByDateField
|
||||
| ObjectRecordOrderByForRelationField,
|
||||
): orderByArg is ObjectRecordOrderByForScalarField => {
|
||||
if (Object.keys(orderByArg).length > 1) {
|
||||
throw new UserInputError(
|
||||
@@ -256,7 +291,8 @@ export class GraphqlQueryOrderFieldParser {
|
||||
orderByArg:
|
||||
| ObjectRecordOrderByForScalarField
|
||||
| ObjectRecordOrderByForCompositeField
|
||||
| ObjectRecordOrderByWithGroupByDateField,
|
||||
| ObjectRecordOrderByWithGroupByDateField
|
||||
| ObjectRecordOrderByForRelationField,
|
||||
): orderByArg is ObjectRecordOrderByForCompositeField => {
|
||||
const compositeFieldOrderByValue = Object.values(orderByArg)[0];
|
||||
|
||||
@@ -292,7 +328,8 @@ export class GraphqlQueryOrderFieldParser {
|
||||
| ObjectRecordOrderByForScalarField
|
||||
| ObjectRecordOrderByForCompositeField
|
||||
| AggregateOrderByWithGroupByField
|
||||
| ObjectRecordOrderByWithGroupByDateField,
|
||||
| ObjectRecordOrderByWithGroupByDateField
|
||||
| ObjectRecordOrderByForRelationField,
|
||||
fieldMetadataType: FieldMetadataType,
|
||||
): orderByArg is ObjectRecordOrderByWithGroupByDateField => {
|
||||
if (
|
||||
@@ -443,7 +480,7 @@ export class GraphqlQueryOrderFieldParser {
|
||||
!groupByFields.some(
|
||||
(groupByField) =>
|
||||
groupByField.fieldMetadata.id === fieldMetadata.id &&
|
||||
groupByField.subFieldName === subFieldName,
|
||||
(groupByField as GroupByRegularField).subFieldName === subFieldName,
|
||||
)
|
||||
) {
|
||||
throw new UserInputError(
|
||||
@@ -485,7 +522,7 @@ export class GraphqlQueryOrderFieldParser {
|
||||
(groupByField) =>
|
||||
groupByField.fieldMetadata.id === fieldMetadataId &&
|
||||
(groupByField as GroupByDateField).dateGranularity === granularity,
|
||||
);
|
||||
) as GroupByDateField | undefined;
|
||||
|
||||
if (!isDefined(associatedGroupByField)) {
|
||||
throw new UserInputError(
|
||||
@@ -511,4 +548,179 @@ export class GraphqlQueryOrderFieldParser {
|
||||
[expression]: convertOrderByToFindOptionsOrder(orderByDirection),
|
||||
};
|
||||
};
|
||||
|
||||
parseObjectRecordOrderByForRelationField = ({
|
||||
groupByFields,
|
||||
orderByArg,
|
||||
fieldMetadata,
|
||||
}: {
|
||||
groupByFields: GroupByField[];
|
||||
orderByArg: ObjectRecordOrderByForRelationField;
|
||||
fieldMetadata: FieldMetadataEntity;
|
||||
}): Record<string, OrderByCondition> | null => {
|
||||
const {
|
||||
associatedGroupByField,
|
||||
nestedFieldMetadata,
|
||||
nestedFieldOrderByValue,
|
||||
} = prepareForOrderByRelationFieldParsing({
|
||||
orderByArg,
|
||||
fieldMetadata,
|
||||
objectMetadataMaps: this.objectMetadataMaps,
|
||||
groupByFields,
|
||||
});
|
||||
|
||||
if (
|
||||
!isDefined(associatedGroupByField) ||
|
||||
!isDefined(nestedFieldMetadata) ||
|
||||
!isDefined(nestedFieldOrderByValue)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Handle composite fields
|
||||
if (isCompositeFieldMetadataType(nestedFieldMetadata.type)) {
|
||||
if (!isObject(nestedFieldOrderByValue)) {
|
||||
throw new UserInputError(
|
||||
`Composite field "${nestedFieldMetadata.name}" requires a subfield to be specified`,
|
||||
);
|
||||
}
|
||||
|
||||
const compositeSubFields = Object.keys(nestedFieldOrderByValue);
|
||||
|
||||
if (compositeSubFields.length > 1) {
|
||||
throw new UserInputError(
|
||||
'Please provide composite subfield criteria one by one in orderBy array',
|
||||
);
|
||||
}
|
||||
|
||||
const nestedSubFieldName = compositeSubFields[0];
|
||||
const orderByDirection = (
|
||||
nestedFieldOrderByValue as Record<string, OrderByDirection>
|
||||
)[nestedSubFieldName];
|
||||
|
||||
if (!isDefined(orderByDirection)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
!isDefined(associatedGroupByField.nestedSubFieldName) ||
|
||||
associatedGroupByField.nestedSubFieldName !== nestedSubFieldName
|
||||
) {
|
||||
throw new UserInputError(
|
||||
`Cannot order by a composite subfield that is not in groupBy criteria: ${nestedSubFieldName}`,
|
||||
);
|
||||
}
|
||||
|
||||
const joinAlias = fieldMetadata.name;
|
||||
const nestedColumnName = formatColumnNamesFromCompositeFieldAndSubfields(
|
||||
nestedFieldMetadata.name,
|
||||
[nestedSubFieldName],
|
||||
)[0];
|
||||
|
||||
const columnNameWithQuotes = `"${joinAlias}"."${nestedColumnName}"`;
|
||||
|
||||
return {
|
||||
[columnNameWithQuotes]:
|
||||
convertOrderByToFindOptionsOrder(orderByDirection),
|
||||
};
|
||||
}
|
||||
|
||||
const isGroupByDateField =
|
||||
(nestedFieldMetadata.type === FieldMetadataType.DATE ||
|
||||
nestedFieldMetadata.type === FieldMetadataType.DATE_TIME) &&
|
||||
isObject(nestedFieldOrderByValue) &&
|
||||
'orderBy' in nestedFieldOrderByValue &&
|
||||
'granularity' in nestedFieldOrderByValue;
|
||||
|
||||
if (isGroupByDateField) {
|
||||
const orderByDirection = (
|
||||
nestedFieldOrderByValue as {
|
||||
orderBy: OrderByDirection;
|
||||
granularity: ObjectRecordGroupByDateGranularity;
|
||||
}
|
||||
).orderBy;
|
||||
const granularity = (
|
||||
nestedFieldOrderByValue as {
|
||||
orderBy: OrderByDirection;
|
||||
granularity: ObjectRecordGroupByDateGranularity;
|
||||
}
|
||||
).granularity;
|
||||
|
||||
if (
|
||||
!isDefined(associatedGroupByField.dateGranularity) ||
|
||||
associatedGroupByField.dateGranularity !== granularity
|
||||
) {
|
||||
throw new UserInputError(
|
||||
`Cannot order by a date granularity that is not in groupBy criteria: ${granularity}`,
|
||||
);
|
||||
}
|
||||
|
||||
const joinAlias = fieldMetadata.name;
|
||||
const nestedColumnName = formatColumnNamesFromCompositeFieldAndSubfields(
|
||||
nestedFieldMetadata.name,
|
||||
associatedGroupByField.nestedSubFieldName
|
||||
? [associatedGroupByField.nestedSubFieldName]
|
||||
: undefined,
|
||||
)[0];
|
||||
|
||||
const columnNameWithQuotes = `"${joinAlias}"."${nestedColumnName}"`;
|
||||
|
||||
const expression = getGroupByExpression({
|
||||
groupByField: associatedGroupByField,
|
||||
columnNameWithQuotes,
|
||||
});
|
||||
|
||||
return {
|
||||
[expression]: convertOrderByToFindOptionsOrder(orderByDirection),
|
||||
};
|
||||
}
|
||||
|
||||
// Handle regular nested fields
|
||||
if (
|
||||
typeof nestedFieldOrderByValue === 'string' &&
|
||||
Object.values(OrderByDirection).includes(
|
||||
nestedFieldOrderByValue as OrderByDirection,
|
||||
)
|
||||
) {
|
||||
const orderByDirection = nestedFieldOrderByValue as OrderByDirection;
|
||||
|
||||
const joinAlias = fieldMetadata.name;
|
||||
const nestedColumnName = formatColumnNamesFromCompositeFieldAndSubfields(
|
||||
nestedFieldMetadata.name,
|
||||
associatedGroupByField.nestedSubFieldName
|
||||
? [associatedGroupByField.nestedSubFieldName]
|
||||
: undefined,
|
||||
)[0];
|
||||
|
||||
const columnNameWithQuotes = `"${joinAlias}"."${nestedColumnName}"`;
|
||||
|
||||
return {
|
||||
[columnNameWithQuotes]:
|
||||
convertOrderByToFindOptionsOrder(orderByDirection),
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
isObjectRecordOrderByForRelationField = (
|
||||
orderByArg:
|
||||
| ObjectRecordOrderByForScalarField
|
||||
| ObjectRecordOrderByForCompositeField
|
||||
| ObjectRecordOrderByWithGroupByDateField
|
||||
| ObjectRecordOrderByForRelationField,
|
||||
fieldMetadata: FieldMetadataEntity,
|
||||
): orderByArg is ObjectRecordOrderByForRelationField => {
|
||||
if (!isFieldMetadataRelationOrMorphRelation(fieldMetadata)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const relationFieldOrderByValue = Object.values(orderByArg)[0];
|
||||
|
||||
if (!isObject(relationFieldOrderByValue)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Object.keys(relationFieldOrderByValue).length > 0;
|
||||
};
|
||||
}
|
||||
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
import { UserInputError } from 'apollo-server-core';
|
||||
import { isObject } from 'class-validator';
|
||||
import { type ObjectRecordOrderByForRelationField } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import {
|
||||
type GroupByField,
|
||||
type GroupByRelationField,
|
||||
} from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/types/group-by-field.types';
|
||||
import { isGroupByRelationField } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/is-group-by-relation-field.util';
|
||||
import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||
|
||||
export const prepareForOrderByRelationFieldParsing = ({
|
||||
orderByArg,
|
||||
fieldMetadata,
|
||||
objectMetadataMaps,
|
||||
groupByFields,
|
||||
}: {
|
||||
orderByArg: ObjectRecordOrderByForRelationField;
|
||||
fieldMetadata: FieldMetadataEntity;
|
||||
objectMetadataMaps?: ObjectMetadataMaps;
|
||||
groupByFields: GroupByField[];
|
||||
}) => {
|
||||
if (!objectMetadataMaps) {
|
||||
throw new UserInputError(
|
||||
`Object metadata maps not found for field ${fieldMetadata.name}`,
|
||||
);
|
||||
}
|
||||
|
||||
const relationFieldName = Object.keys(orderByArg)[0];
|
||||
const nestedFieldOrderByObject = orderByArg[relationFieldName];
|
||||
|
||||
if (
|
||||
!isDefined(nestedFieldOrderByObject) ||
|
||||
!isObject(nestedFieldOrderByObject)
|
||||
) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (Object.keys(nestedFieldOrderByObject).length > 1) {
|
||||
throw new UserInputError(
|
||||
'Please provide nested field criteria one by one in orderBy array',
|
||||
);
|
||||
}
|
||||
|
||||
const nestedFieldName = Object.keys(nestedFieldOrderByObject)[0];
|
||||
const nestedFieldOrderByValue = nestedFieldOrderByObject[nestedFieldName];
|
||||
|
||||
if (!isDefined(nestedFieldOrderByValue)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (!isDefined(fieldMetadata.relationTargetObjectMetadataId)) {
|
||||
throw new UserInputError(
|
||||
`Relation target object metadata id not found for field ${fieldMetadata.name}`,
|
||||
);
|
||||
}
|
||||
|
||||
const targetObjectMetadataItem =
|
||||
objectMetadataMaps?.byId[fieldMetadata.relationTargetObjectMetadataId];
|
||||
|
||||
if (!isDefined(targetObjectMetadataItem)) {
|
||||
throw new UserInputError(
|
||||
`Target object metadata item not found for field ${fieldMetadata.name}`,
|
||||
);
|
||||
}
|
||||
|
||||
const nestedFieldMetadataId =
|
||||
targetObjectMetadataItem?.fieldIdByName[nestedFieldName];
|
||||
|
||||
if (!isDefined(nestedFieldMetadataId)) {
|
||||
throw new UserInputError(
|
||||
`Nested field metadata id not found for field ${nestedFieldName}`,
|
||||
);
|
||||
}
|
||||
|
||||
const nestedFieldMetadata =
|
||||
targetObjectMetadataItem?.fieldsById[nestedFieldMetadataId];
|
||||
|
||||
if (!isDefined(nestedFieldMetadata) || !isDefined(nestedFieldMetadataId)) {
|
||||
throw new UserInputError(
|
||||
`Nested field "${nestedFieldName}" not found in target object "${targetObjectMetadataItem.nameSingular}"`,
|
||||
);
|
||||
}
|
||||
|
||||
const associatedGroupByField = groupByFields.find(
|
||||
(groupByField) =>
|
||||
isGroupByRelationField(groupByField) &&
|
||||
groupByField.fieldMetadata.id === fieldMetadata.id &&
|
||||
groupByField.nestedFieldMetadata.id === nestedFieldMetadataId,
|
||||
) as GroupByRelationField | undefined;
|
||||
|
||||
if (!isDefined(associatedGroupByField)) {
|
||||
throw new UserInputError(
|
||||
`Cannot order by a relation field that is not in groupBy criteria: ${relationFieldName}.${nestedFieldName}`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
associatedGroupByField,
|
||||
nestedFieldMetadata,
|
||||
nestedFieldOrderByValue,
|
||||
};
|
||||
};
|
||||
+1
@@ -40,6 +40,7 @@ export class GraphqlQueryParser {
|
||||
);
|
||||
this.orderFieldParser = new GraphqlQueryOrderFieldParser(
|
||||
this.objectMetadataMapItem,
|
||||
this.objectMetadataMaps,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
export type CompositeFieldGroupByDefinition = Record<string, boolean>;
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
import { type ObjectRecordGroupByDateGranularity } from 'twenty-shared/types';
|
||||
|
||||
export type DateFieldGroupByDefinition = {
|
||||
granularity: ObjectRecordGroupByDateGranularity;
|
||||
};
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
import { type CompositeFieldGroupByDefinition } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/types/composite-field-group-by-definition.type';
|
||||
import { type DateFieldGroupByDefinition } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/types/date-field-group-by-definition.type';
|
||||
|
||||
export type FieldGroupByDefinition =
|
||||
| boolean
|
||||
| CompositeFieldGroupByDefinition
|
||||
| DateFieldGroupByDefinition
|
||||
| undefined;
|
||||
+10
-1
@@ -11,4 +11,13 @@ export type GroupByDateField = {
|
||||
subFieldName?: string;
|
||||
dateGranularity: ObjectRecordGroupByDateGranularity;
|
||||
};
|
||||
export type GroupByField = GroupByRegularField | GroupByDateField;
|
||||
export type GroupByRelationField = {
|
||||
fieldMetadata: FieldMetadataEntity;
|
||||
nestedFieldMetadata: FieldMetadataEntity;
|
||||
nestedSubFieldName?: string;
|
||||
dateGranularity?: ObjectRecordGroupByDateGranularity;
|
||||
};
|
||||
export type GroupByField =
|
||||
| GroupByRegularField
|
||||
| GroupByDateField
|
||||
| GroupByRelationField;
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ import {
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type CommonGroupByOutputItem } from 'src/engine/api/common/types/common-group-by-output-item.type';
|
||||
import { type GroupByDefinition } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/types/group-by-definition.types';
|
||||
import { type GroupByDefinition } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/types/group-by-definition.type';
|
||||
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
|
||||
|
||||
+15
-5
@@ -1,8 +1,9 @@
|
||||
import { ObjectRecordGroupByDateGranularity } from 'twenty-shared/types';
|
||||
import { assertUnreachable } from 'twenty-shared/utils';
|
||||
import { assertUnreachable, isDefined } from 'twenty-shared/utils';
|
||||
|
||||
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';
|
||||
import { isGroupByRelationField } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/is-group-by-relation-field.util';
|
||||
|
||||
export const getGroupByExpression = ({
|
||||
groupByField,
|
||||
@@ -11,10 +12,19 @@ export const getGroupByExpression = ({
|
||||
groupByField: GroupByField;
|
||||
columnNameWithQuotes: string;
|
||||
}) => {
|
||||
if (!isGroupByDateField(groupByField)) {
|
||||
if (
|
||||
!(isGroupByDateField(groupByField) || isGroupByRelationField(groupByField))
|
||||
) {
|
||||
return columnNameWithQuotes;
|
||||
}
|
||||
switch (groupByField.dateGranularity) {
|
||||
|
||||
const dateGranularity = groupByField.dateGranularity;
|
||||
|
||||
if (!isDefined(dateGranularity)) {
|
||||
return columnNameWithQuotes;
|
||||
}
|
||||
|
||||
switch (dateGranularity) {
|
||||
case ObjectRecordGroupByDateGranularity.NONE:
|
||||
return columnNameWithQuotes;
|
||||
case ObjectRecordGroupByDateGranularity.DAY_OF_THE_WEEK:
|
||||
@@ -27,8 +37,8 @@ export const getGroupByExpression = ({
|
||||
case ObjectRecordGroupByDateGranularity.MONTH:
|
||||
case ObjectRecordGroupByDateGranularity.QUARTER:
|
||||
case ObjectRecordGroupByDateGranularity.YEAR:
|
||||
return `DATE_TRUNC('${groupByField.dateGranularity}', ${columnNameWithQuotes})`;
|
||||
return `DATE_TRUNC('${dateGranularity}', ${columnNameWithQuotes})`;
|
||||
default:
|
||||
assertUnreachable(groupByField.dateGranularity);
|
||||
assertUnreachable(dateGranularity);
|
||||
}
|
||||
};
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import { isDefined } from 'class-validator';
|
||||
import { ObjectRecordGroupByDateGranularity } from 'twenty-shared/types';
|
||||
|
||||
import { type FieldGroupByDefinition } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/types/field-group-by-definition.type';
|
||||
|
||||
export const isGroupByDateFieldDefinition = (
|
||||
fieldGroupByDefinition: FieldGroupByDefinition,
|
||||
): 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,
|
||||
)
|
||||
);
|
||||
};
|
||||
+4
-1
@@ -6,5 +6,8 @@ import {
|
||||
export const isGroupByDateField = (
|
||||
groupByField: GroupByField,
|
||||
): groupByField is GroupByDateField => {
|
||||
return 'dateGranularity' in groupByField;
|
||||
return (
|
||||
'dateGranularity' in groupByField &&
|
||||
!('nestedFieldMetadata' in groupByField)
|
||||
);
|
||||
};
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import {
|
||||
type GroupByField,
|
||||
type GroupByRelationField,
|
||||
} from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/types/group-by-field.types';
|
||||
|
||||
export const isGroupByRelationField = (
|
||||
groupByField: GroupByField,
|
||||
): groupByField is GroupByRelationField => {
|
||||
return 'nestedFieldMetadata' in groupByField;
|
||||
};
|
||||
+38
-48
@@ -1,63 +1,32 @@
|
||||
import { isDefined } from 'class-validator';
|
||||
import {
|
||||
FieldMetadataType,
|
||||
ObjectRecordGroupByDateGranularity,
|
||||
} from 'twenty-shared/types';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
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 { isGroupByDateFieldDefinition } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/is-group-by-date-field-definition.util';
|
||||
import { parseGroupByRelationField } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/parse-group-by-relation-field.util';
|
||||
import { validateSingleKeyForGroupByOrThrow } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/validate-single-key-for-group-by-or-throw.util';
|
||||
import { isFieldMetadataRelationOrMorphRelation } from 'src/engine/api/graphql/workspace-schema-builder/utils/is-field-metadata-relation-or-morph-relation.utils';
|
||||
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,
|
||||
)
|
||||
);
|
||||
};
|
||||
import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||
|
||||
export const parseGroupByArgs = (
|
||||
args: GroupByResolverArgs,
|
||||
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
|
||||
objectMetadataMaps: ObjectMetadataMaps,
|
||||
): GroupByField[] => {
|
||||
const groupByFieldNames = args.groupBy;
|
||||
|
||||
const groupByFields: GroupByField[] = [];
|
||||
|
||||
for (const fieldNames of groupByFieldNames) {
|
||||
if (Object.keys(fieldNames).length > 1) {
|
||||
throw new GraphqlQueryRunnerException(
|
||||
validateSingleKeyForGroupByOrThrow({
|
||||
groupByKeys: Object.keys(fieldNames),
|
||||
errorMessage:
|
||||
'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] ||
|
||||
@@ -69,6 +38,26 @@ export const parseGroupByArgs = (
|
||||
throw new Error(`Unidentified field in groupBy: ${fieldName}`);
|
||||
}
|
||||
|
||||
const isGroupByRelationField =
|
||||
isFieldMetadataRelationOrMorphRelation(fieldMetadata) &&
|
||||
typeof fieldNames[fieldName] === 'object' &&
|
||||
fieldNames[fieldName] !== null &&
|
||||
!isGroupByDateFieldDefinition(fieldNames[fieldName]);
|
||||
|
||||
// Handle relation fields
|
||||
if (isGroupByRelationField) {
|
||||
parseGroupByRelationField({
|
||||
fieldNames,
|
||||
fieldName,
|
||||
fieldMetadata,
|
||||
objectMetadataMaps,
|
||||
groupByFields,
|
||||
});
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle date fields
|
||||
if (
|
||||
fieldMetadata.type === FieldMetadataType.DATE ||
|
||||
fieldMetadata.type === FieldMetadataType.DATE_TIME
|
||||
@@ -88,6 +77,7 @@ export const parseGroupByArgs = (
|
||||
}
|
||||
}
|
||||
|
||||
// Handle regular fields and composite fields
|
||||
if (fieldNames[fieldName] === true) {
|
||||
groupByFields.push({
|
||||
fieldMetadata,
|
||||
@@ -95,12 +85,12 @@ export const parseGroupByArgs = (
|
||||
});
|
||||
continue;
|
||||
} else if (typeof fieldNames[fieldName] === 'object') {
|
||||
if (Object.keys(fieldNames[fieldName]).length > 1) {
|
||||
throw new GraphqlQueryRunnerException(
|
||||
validateSingleKeyForGroupByOrThrow({
|
||||
groupByKeys: Object.keys(fieldNames[fieldName]),
|
||||
errorMessage:
|
||||
'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] ===
|
||||
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
import { UserInputError } from 'apollo-server-core';
|
||||
import { isDefined } from 'class-validator';
|
||||
import {
|
||||
FieldMetadataType,
|
||||
type ObjectRecordGroupByDateGranularity,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
import {
|
||||
GraphqlQueryRunnerException,
|
||||
GraphqlQueryRunnerExceptionCode,
|
||||
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
|
||||
import { type CompositeFieldGroupByDefinition } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/types/composite-field-group-by-definition.type';
|
||||
import { type DateFieldGroupByDefinition } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/types/date-field-group-by-definition.type';
|
||||
import { type GroupByField } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/types/group-by-field.types';
|
||||
import { isGroupByDateFieldDefinition } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/is-group-by-date-field-definition.util';
|
||||
import { validateSingleKeyForGroupByOrThrow } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/validate-single-key-for-group-by-or-throw.util';
|
||||
import { getTargetObjectMetadataOrThrow } from 'src/engine/api/graphql/graphql-query-runner/utils/get-target-object-metadata.util';
|
||||
import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||
|
||||
const getNestedFieldMetadataDetails = ({
|
||||
fieldNames,
|
||||
fieldName,
|
||||
fieldMetadata,
|
||||
objectMetadataMaps,
|
||||
}: {
|
||||
fieldNames: Record<string, unknown>;
|
||||
fieldName: string;
|
||||
fieldMetadata: FieldMetadataEntity;
|
||||
objectMetadataMaps: ObjectMetadataMaps;
|
||||
}) => {
|
||||
const nestedFieldGroupByDefinitions = fieldNames[fieldName] as
|
||||
| Record<string, boolean>
|
||||
| Record<string, CompositeFieldGroupByDefinition>
|
||||
| Record<string, DateFieldGroupByDefinition>;
|
||||
|
||||
const targetObjectMetadata = getTargetObjectMetadataOrThrow(
|
||||
fieldMetadata,
|
||||
objectMetadataMaps,
|
||||
);
|
||||
|
||||
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 nestedFieldMetadataId =
|
||||
targetObjectMetadata.fieldIdByName[nestedFieldName];
|
||||
const nestedFieldMetadata =
|
||||
targetObjectMetadata.fieldsById[nestedFieldMetadataId];
|
||||
|
||||
if (!isDefined(nestedFieldMetadata) || !isDefined(nestedFieldMetadataId)) {
|
||||
throw new GraphqlQueryRunnerException(
|
||||
`Nested field "${nestedFieldName}" not found in target object "${targetObjectMetadata.nameSingular}"`,
|
||||
GraphqlQueryRunnerExceptionCode.FIELD_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
if (nestedFieldMetadata.type === FieldMetadataType.RELATION) {
|
||||
throw new UserInputError(
|
||||
`Cannot group by a relation field of the relation field: "${nestedFieldName}" is a relation field of "${targetObjectMetadata.nameSingular}"`,
|
||||
);
|
||||
}
|
||||
|
||||
const nestedFieldGroupByDefinition =
|
||||
nestedFieldGroupByDefinitions[nestedFieldName];
|
||||
|
||||
return {
|
||||
nestedFieldGroupByDefinition,
|
||||
nestedFieldMetadata,
|
||||
nestedFieldName,
|
||||
};
|
||||
};
|
||||
|
||||
const handleNestedCompositeField = ({
|
||||
nestedFieldGroupByDefinition,
|
||||
nestedFieldName,
|
||||
fieldMetadata,
|
||||
nestedFieldMetadata,
|
||||
groupByFields,
|
||||
}: {
|
||||
nestedFieldGroupByDefinition: CompositeFieldGroupByDefinition;
|
||||
nestedFieldName: string;
|
||||
fieldMetadata: FieldMetadataEntity;
|
||||
nestedFieldMetadata: FieldMetadataEntity;
|
||||
groupByFields: GroupByField[];
|
||||
}) => {
|
||||
if (
|
||||
typeof nestedFieldGroupByDefinition === 'object' &&
|
||||
nestedFieldGroupByDefinition !== null
|
||||
) {
|
||||
const compositeSubFields = Object.keys(
|
||||
nestedFieldGroupByDefinition as Record<string, unknown>,
|
||||
);
|
||||
|
||||
validateSingleKeyForGroupByOrThrow({
|
||||
groupByKeys: compositeSubFields,
|
||||
errorMessage:
|
||||
'You cannot provide multiple composite subfields in one GroupByInput, split them into multiple GroupByInput',
|
||||
});
|
||||
|
||||
const nestedSubFieldName = compositeSubFields[0];
|
||||
|
||||
if (
|
||||
(nestedFieldGroupByDefinition as Record<string, boolean>)[
|
||||
nestedSubFieldName
|
||||
] === true
|
||||
) {
|
||||
groupByFields.push({
|
||||
fieldMetadata,
|
||||
nestedFieldMetadata,
|
||||
nestedSubFieldName,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw new GraphqlQueryRunnerException(
|
||||
`Composite field "${nestedFieldName}" requires a subfield to be specified`,
|
||||
GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
|
||||
);
|
||||
};
|
||||
|
||||
export const parseGroupByRelationField = ({
|
||||
fieldNames,
|
||||
fieldName,
|
||||
fieldMetadata,
|
||||
objectMetadataMaps,
|
||||
groupByFields,
|
||||
}: {
|
||||
fieldNames: Record<string, unknown>;
|
||||
fieldName: string;
|
||||
fieldMetadata: FieldMetadataEntity;
|
||||
objectMetadataMaps: ObjectMetadataMaps;
|
||||
groupByFields: GroupByField[];
|
||||
}) => {
|
||||
const { nestedFieldGroupByDefinition, nestedFieldMetadata, nestedFieldName } =
|
||||
getNestedFieldMetadataDetails({
|
||||
fieldNames,
|
||||
fieldName,
|
||||
fieldMetadata,
|
||||
objectMetadataMaps,
|
||||
});
|
||||
|
||||
// Handle date fields in nested relations
|
||||
if (
|
||||
(nestedFieldMetadata.type === FieldMetadataType.DATE ||
|
||||
nestedFieldMetadata.type === FieldMetadataType.DATE_TIME) &&
|
||||
isGroupByDateFieldDefinition(nestedFieldGroupByDefinition)
|
||||
) {
|
||||
groupByFields.push({
|
||||
fieldMetadata,
|
||||
nestedFieldMetadata,
|
||||
dateGranularity: (
|
||||
nestedFieldGroupByDefinition as {
|
||||
granularity: ObjectRecordGroupByDateGranularity;
|
||||
}
|
||||
).granularity,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle composite fields in nested relations
|
||||
if (isCompositeFieldMetadataType(nestedFieldMetadata.type)) {
|
||||
handleNestedCompositeField({
|
||||
nestedFieldGroupByDefinition:
|
||||
nestedFieldGroupByDefinition as CompositeFieldGroupByDefinition,
|
||||
nestedFieldName,
|
||||
fieldMetadata,
|
||||
nestedFieldMetadata,
|
||||
groupByFields,
|
||||
});
|
||||
}
|
||||
|
||||
// Handle regular nested fields
|
||||
if (nestedFieldGroupByDefinition === true) {
|
||||
groupByFields.push({
|
||||
fieldMetadata,
|
||||
nestedFieldMetadata,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
};
|
||||
+7
-1
@@ -1,3 +1,9 @@
|
||||
export const removeQuotes = (string: string): string => {
|
||||
const removeQuotes = (string: string): string => {
|
||||
return string.replace(/["']/g, '');
|
||||
};
|
||||
|
||||
export const formatColumnNameAsAlias = (
|
||||
columnNameWithQuotes: string,
|
||||
): string => {
|
||||
return removeQuotes(columnNameWithQuotes).replace(/\./g, '_');
|
||||
};
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import {
|
||||
GraphqlQueryRunnerException,
|
||||
GraphqlQueryRunnerExceptionCode,
|
||||
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
|
||||
|
||||
export const validateSingleKeyForGroupByOrThrow = ({
|
||||
groupByKeys,
|
||||
errorMessage,
|
||||
}: {
|
||||
groupByKeys: string[];
|
||||
errorMessage: string;
|
||||
}): void => {
|
||||
if (groupByKeys.length > 1) {
|
||||
throw new GraphqlQueryRunnerException(
|
||||
errorMessage,
|
||||
GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
|
||||
);
|
||||
}
|
||||
};
|
||||
+9
-3
@@ -7,12 +7,13 @@ import { type ObjectLiteral } from 'typeorm';
|
||||
|
||||
import { ObjectRecordOrderBy } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||
|
||||
import { getObjectAlias } from 'src/engine/api/common/common-query-runners/utils/get-object-alias-for-group-by.util';
|
||||
import { CommonResultGettersService } from 'src/engine/api/common/common-result-getters/common-result-getters.service';
|
||||
import { CommonExtendedQueryRunnerContext } from 'src/engine/api/common/types/common-extended-query-runner-context.type';
|
||||
import { type CommonGroupByOutputItem } from 'src/engine/api/common/types/common-group-by-output-item.type';
|
||||
import { type GraphqlQuerySelectedFieldsResult } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser';
|
||||
import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
|
||||
import { type GroupByDefinition } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/types/group-by-definition.types';
|
||||
import { type GroupByDefinition } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/types/group-by-definition.type';
|
||||
import { formatResultWithGroupByDimensionValues } from 'src/engine/api/graphql/graphql-query-runner/group-by/resolvers/utils/format-result-with-group-by-dimension-values.util';
|
||||
import { getGroupLimit } from 'src/engine/api/graphql/graphql-query-runner/group-by/utils/get-group-limit.util';
|
||||
import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper';
|
||||
@@ -90,7 +91,7 @@ export class GroupByWithRecordsService {
|
||||
|
||||
const recordsResult = await queryBuilderWithPartitionBy.getRawMany();
|
||||
|
||||
if (isDefined(selectedFieldsResult.relations)) {
|
||||
if (!isEmpty(selectedFieldsResult.relations)) {
|
||||
await this.processNestedRelationsHelper.processNestedRelations({
|
||||
objectMetadataMaps,
|
||||
parentObjectMetadataItem: objectMetadataItemWithFieldMaps,
|
||||
@@ -156,8 +157,10 @@ export class GroupByWithRecordsService {
|
||||
queryBuilderForSubQuery,
|
||||
);
|
||||
|
||||
const objectAlias = getObjectAlias(objectMetadataItemWithFieldMaps);
|
||||
|
||||
const recordSelectWithAlias = Object.keys(columnsToSelect)
|
||||
.map((col) => `"${col}" as "${SUB_QUERY_PREFIX}${col}"`)
|
||||
.map((col) => `"${objectAlias}"."${col}" as "${SUB_QUERY_PREFIX}${col}"`)
|
||||
.join(', ');
|
||||
|
||||
const groupBySelectWithAlias = groupByDefinitions
|
||||
@@ -188,6 +191,9 @@ export class GroupByWithRecordsService {
|
||||
const mainQuery = mainQueryQueryBuilder
|
||||
.from(`(${subQuery.getQuery()})`, 'ranked_records')
|
||||
.setParameters(queryBuilderForSubQuery.expressionMap.parameters)
|
||||
.where('rn <= :recordsPerGroupLimit', {
|
||||
recordsPerGroupLimit: RECORDS_PER_GROUP_LIMIT,
|
||||
})
|
||||
.select(groupByAliases)
|
||||
.addSelect(
|
||||
`JSON_AGG(
|
||||
|
||||
+4
-1
@@ -92,7 +92,10 @@ export class GqlTypeGenerator {
|
||||
objectMetadata,
|
||||
);
|
||||
this.relationConnectGqlInputTypeGenerator.buildAndStore(objectMetadata);
|
||||
this.objectMetadataGqlInputTypeGenerator.buildAndStore(objectMetadata);
|
||||
this.objectMetadataGqlInputTypeGenerator.buildAndStore(
|
||||
objectMetadata,
|
||||
dynamicObjectMetadataCollection,
|
||||
);
|
||||
|
||||
if (
|
||||
objectContainsRelationField(objectMetadata) ||
|
||||
|
||||
+17
-1
@@ -22,6 +22,8 @@ import { isFieldMetadataRelationOrMorphRelation } from 'src/engine/api/graphql/w
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||
import { generateObjectMetadataMaps } from 'src/engine/metadata-modules/utils/generate-object-metadata-maps.util';
|
||||
import { pascalCase } from 'src/utils/pascal-case';
|
||||
|
||||
@Injectable()
|
||||
@@ -29,13 +31,26 @@ export class ObjectMetadataGroupByGqlInputTypeGenerator {
|
||||
private readonly logger = new Logger(
|
||||
ObjectMetadataGroupByGqlInputTypeGenerator.name,
|
||||
);
|
||||
private objectMetadataCollection: ObjectMetadataEntity[] = [];
|
||||
private objectMetadataMaps: ObjectMetadataMaps | undefined;
|
||||
|
||||
constructor(
|
||||
private readonly gqlTypesStorage: GqlTypesStorage,
|
||||
private readonly relationFieldMetadataGqlInputTypeGenerator: RelationFieldMetadataGqlInputTypeGenerator,
|
||||
private readonly typeMapperService: TypeMapperService,
|
||||
) {}
|
||||
|
||||
public buildAndStore(objectMetadata: ObjectMetadataEntity) {
|
||||
public buildAndStore(
|
||||
objectMetadata: ObjectMetadataEntity,
|
||||
objectMetadataCollection?: ObjectMetadataEntity[],
|
||||
) {
|
||||
if (isDefined(objectMetadataCollection)) {
|
||||
this.objectMetadataCollection = objectMetadataCollection;
|
||||
this.objectMetadataMaps = generateObjectMetadataMaps(
|
||||
objectMetadataCollection,
|
||||
);
|
||||
}
|
||||
|
||||
const inputType = new GraphQLInputObjectType({
|
||||
name: `${pascalCase(objectMetadata.nameSingular)}${GqlInputTypeDefinitionKind.GroupBy.toString()}Input`,
|
||||
description: objectMetadata.description,
|
||||
@@ -61,6 +76,7 @@ export class ObjectMetadataGroupByGqlInputTypeGenerator {
|
||||
)
|
||||
? this.relationFieldMetadataGqlInputTypeGenerator.generateSimpleRelationFieldGroupByInputType(
|
||||
fieldMetadata,
|
||||
this.objectMetadataMaps,
|
||||
)
|
||||
: this.generateField(fieldMetadata);
|
||||
|
||||
|
||||
+6
-1
@@ -19,7 +19,10 @@ export class ObjectMetadataGqlInputTypeGenerator {
|
||||
private readonly objectMetadataGroupByGqlInputTypeGenerator: ObjectMetadataGroupByGqlInputTypeGenerator,
|
||||
) {}
|
||||
|
||||
public buildAndStore(objectMetadata: ObjectMetadataEntity) {
|
||||
public buildAndStore(
|
||||
objectMetadata: ObjectMetadataEntity,
|
||||
objectMetadataCollection?: ObjectMetadataEntity[],
|
||||
) {
|
||||
this.objectMetadataCreateGqlInputTypeGenerator.buildAndStore(
|
||||
objectMetadata,
|
||||
);
|
||||
@@ -34,9 +37,11 @@ export class ObjectMetadataGqlInputTypeGenerator {
|
||||
});
|
||||
this.objectMetadataOrderByWithGroupByGqlInputTypeGenerator.buildAndStore({
|
||||
objectMetadata,
|
||||
objectMetadataCollection,
|
||||
});
|
||||
this.objectMetadataGroupByGqlInputTypeGenerator.buildAndStore(
|
||||
objectMetadata,
|
||||
objectMetadataCollection,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+4
@@ -18,6 +18,7 @@ import { isFieldMetadataRelationOrMorphRelation } from 'src/engine/api/graphql/w
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||
|
||||
@Injectable()
|
||||
export class ObjectMetadataOrderByBaseGenerator {
|
||||
@@ -31,10 +32,12 @@ export class ObjectMetadataOrderByBaseGenerator {
|
||||
objectMetadata,
|
||||
logger,
|
||||
orderByDateGranularity,
|
||||
objectMetadataMaps,
|
||||
}: {
|
||||
objectMetadata: ObjectMetadataEntity;
|
||||
logger: Logger;
|
||||
orderByDateGranularity?: boolean;
|
||||
objectMetadataMaps?: ObjectMetadataMaps;
|
||||
}): GraphQLInputFieldConfigMap {
|
||||
const allGeneratedFields: GraphQLInputFieldConfigMap = {};
|
||||
|
||||
@@ -54,6 +57,7 @@ export class ObjectMetadataOrderByBaseGenerator {
|
||||
{
|
||||
fieldMetadata,
|
||||
typeOptions,
|
||||
objectMetadataMaps,
|
||||
},
|
||||
);
|
||||
} else if (isCompositeFieldMetadataType(fieldMetadata.type)) {
|
||||
|
||||
+14
@@ -23,6 +23,8 @@ import { isFieldMetadataRelationOrMorphRelation } from 'src/engine/api/graphql/w
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||
import { generateObjectMetadataMaps } from 'src/engine/metadata-modules/utils/generate-object-metadata-maps.util';
|
||||
import { pascalCase } from 'src/utils/pascal-case';
|
||||
|
||||
@Injectable()
|
||||
@@ -37,11 +39,21 @@ export class ObjectMetadataOrderByWithGroupByGqlInputTypeGenerator {
|
||||
private readonly typeMapperService: TypeMapperService,
|
||||
) {}
|
||||
|
||||
private objectMetadataMaps?: ObjectMetadataMaps;
|
||||
|
||||
public buildAndStore({
|
||||
objectMetadata,
|
||||
objectMetadataCollection,
|
||||
}: {
|
||||
objectMetadata: ObjectMetadataEntity;
|
||||
objectMetadataCollection?: ObjectMetadataEntity[];
|
||||
}) {
|
||||
if (isDefined(objectMetadataCollection)) {
|
||||
this.objectMetadataMaps = generateObjectMetadataMaps(
|
||||
objectMetadataCollection,
|
||||
);
|
||||
}
|
||||
|
||||
const inputType = new GraphQLInputObjectType({
|
||||
name: `${pascalCase(objectMetadata.nameSingular)}${GqlInputTypeDefinitionKind.OrderByWithGroupBy.toString()}Input`,
|
||||
description: objectMetadata.description,
|
||||
@@ -86,6 +98,7 @@ export class ObjectMetadataOrderByWithGroupByGqlInputTypeGenerator {
|
||||
{
|
||||
fieldMetadata,
|
||||
typeOptions,
|
||||
objectMetadataMaps: this.objectMetadataMaps,
|
||||
},
|
||||
);
|
||||
} else if (isCompositeFieldMetadataType(fieldMetadata.type)) {
|
||||
@@ -124,6 +137,7 @@ export class ObjectMetadataOrderByWithGroupByGqlInputTypeGenerator {
|
||||
objectMetadata,
|
||||
logger: this.logger,
|
||||
orderByDateGranularity: true,
|
||||
objectMetadataMaps: this.objectMetadataMaps,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+74
-11
@@ -1,21 +1,24 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
GraphQLBoolean,
|
||||
GraphQLInputFieldConfigMap,
|
||||
isInputObjectType,
|
||||
isObjectType,
|
||||
} from 'graphql';
|
||||
import { FieldMetadataType, RelationType } 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 {
|
||||
TypeMapperService,
|
||||
TypeOptions,
|
||||
} 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';
|
||||
import { computeObjectMetadataInputTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-object-metadata-input-type.util';
|
||||
import { computeRelationConnectInputTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-relation-connect-input-type-key.util';
|
||||
import { extractGraphQLRelationFieldNames } from 'src/engine/api/graphql/workspace-schema-builder/utils/extract-graphql-relation-field-names.util';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||
|
||||
@Injectable()
|
||||
export class RelationFieldMetadataGqlInputTypeGenerator {
|
||||
@@ -115,16 +118,19 @@ export class RelationFieldMetadataGqlInputTypeGenerator {
|
||||
public generateSimpleRelationFieldOrderByInputType({
|
||||
fieldMetadata,
|
||||
typeOptions,
|
||||
objectMetadataMaps,
|
||||
}: {
|
||||
fieldMetadata: FieldMetadataEntity<
|
||||
FieldMetadataType.RELATION | FieldMetadataType.MORPH_RELATION
|
||||
>;
|
||||
typeOptions: TypeOptions;
|
||||
objectMetadataMaps?: ObjectMetadataMaps;
|
||||
}) {
|
||||
if (fieldMetadata.settings?.relationType === RelationType.ONE_TO_MANY)
|
||||
return {};
|
||||
|
||||
const { joinColumnName } = extractGraphQLRelationFieldNames(fieldMetadata);
|
||||
const { joinColumnName, fieldMetadataName } =
|
||||
extractGraphQLRelationFieldNames(fieldMetadata);
|
||||
|
||||
const type = this.typeMapperService.mapToOrderByType(fieldMetadata.type);
|
||||
|
||||
@@ -143,32 +149,89 @@ export class RelationFieldMetadataGqlInputTypeGenerator {
|
||||
typeOptions,
|
||||
);
|
||||
|
||||
return {
|
||||
const fields: GraphQLInputFieldConfigMap = {
|
||||
[joinColumnName]: {
|
||||
type: modifiedType,
|
||||
description: fieldMetadata.description,
|
||||
},
|
||||
};
|
||||
|
||||
if (
|
||||
isDefined(fieldMetadata.relationTargetObjectMetadataId) &&
|
||||
isDefined(objectMetadataMaps)
|
||||
) {
|
||||
const targetObjectMetadata =
|
||||
objectMetadataMaps.byId[fieldMetadata.relationTargetObjectMetadataId];
|
||||
|
||||
if (isDefined(targetObjectMetadata)) {
|
||||
const targetOrderByInputTypeKey = computeObjectMetadataInputTypeKey(
|
||||
targetObjectMetadata.nameSingular,
|
||||
GqlInputTypeDefinitionKind.OrderBy,
|
||||
);
|
||||
|
||||
const targetOrderByInputType = this.gqlTypesStorage.getGqlTypeByKey(
|
||||
targetOrderByInputTypeKey,
|
||||
);
|
||||
|
||||
if (
|
||||
isDefined(targetOrderByInputType) &&
|
||||
isInputObjectType(targetOrderByInputType)
|
||||
) {
|
||||
fields[fieldMetadataName] = {
|
||||
type: targetOrderByInputType,
|
||||
description: `Order by fields of the related ${targetObjectMetadata.nameSingular}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
public generateSimpleRelationFieldGroupByInputType(
|
||||
fieldMetadata: FieldMetadataEntity<
|
||||
FieldMetadataType.RELATION | FieldMetadataType.MORPH_RELATION
|
||||
>,
|
||||
objectMetadataMaps?: ObjectMetadataMaps,
|
||||
): GraphQLInputFieldConfigMap {
|
||||
if (fieldMetadata.settings?.relationType === RelationType.ONE_TO_MANY)
|
||||
return {};
|
||||
|
||||
const { joinColumnName } = extractGraphQLRelationFieldNames(fieldMetadata);
|
||||
const { fieldMetadataName } =
|
||||
extractGraphQLRelationFieldNames(fieldMetadata);
|
||||
|
||||
const type = this.typeMapperService.applyTypeOptions(GraphQLBoolean, {});
|
||||
const fields: GraphQLInputFieldConfigMap = {};
|
||||
|
||||
return {
|
||||
[joinColumnName]: {
|
||||
type,
|
||||
description: fieldMetadata.description,
|
||||
},
|
||||
};
|
||||
if (
|
||||
isDefined(fieldMetadata.relationTargetObjectMetadataId) &&
|
||||
isDefined(objectMetadataMaps)
|
||||
) {
|
||||
const targetObjectMetadata =
|
||||
objectMetadataMaps.byId[fieldMetadata.relationTargetObjectMetadataId];
|
||||
|
||||
if (isDefined(targetObjectMetadata)) {
|
||||
const targetGroupByInputTypeKey = computeObjectMetadataInputTypeKey(
|
||||
targetObjectMetadata.nameSingular,
|
||||
GqlInputTypeDefinitionKind.GroupBy,
|
||||
);
|
||||
|
||||
const targetGroupByInputType = this.gqlTypesStorage.getGqlTypeByKey(
|
||||
targetGroupByInputTypeKey,
|
||||
);
|
||||
|
||||
if (
|
||||
isDefined(targetGroupByInputType) &&
|
||||
isInputObjectType(targetGroupByInputType)
|
||||
) {
|
||||
fields[fieldMetadataName] = {
|
||||
type: targetGroupByInputType,
|
||||
description: `Group by fields of the related ${targetObjectMetadata.nameSingular}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
public generateConnectRelationFieldInputType({
|
||||
|
||||
Reference in New Issue
Block a user