[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:
+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