create groupBy resolver + mock groupBy service (#14530)

To test :

```
query : 
query companiesGroupBy($groupBy: CompanyGroupByInput){
  companiesGroupBy(groupBy: $groupBy){
    groupByDimensionValues
    countEmptyId
    sumAnnualRecurringRevenueAmountMicros
    countEmptyAddress
    percentageEmptySearchVector
  }
}

input : 
{"groupBy": {"name": true, "employees": true, "domainName": {"primaryLinkUrl": true}}}

response : 
{
  "data": {
    "companiesGroupBy": [
      {
        "groupByDimensionValues": [
          "Text 1",
          "twenty.com",
          "1"
        ],
        "countEmptyId": 39,
        "sumAnnualRecurringRevenueAmountMicros": 20,
        "countEmptyAddress": 56,
        "percentageEmptySearchVector": 88
      },
      {
        "groupByDimensionValues": [
          "Long long long long and long text 2",
          "github.com/twentyhq/twenty",
          "2"
        ],
        "countEmptyId": 20,
        "sumAnnualRecurringRevenueAmountMicros": 56,
        "countEmptyAddress": 88,
        "percentageEmptySearchVector": 2
      },
      {
        "groupByDimensionValues": [
          "Text 3",
          "react.dev",
          "3"
        ],
        "countEmptyId": 56,
        "sumAnnualRecurringRevenueAmountMicros": 88,
        "countEmptyAddress": 2,
        "percentageEmptySearchVector": 39
      }
    ]
  }
}
```
closes https://github.com/twentyhq/core-team-issues/issues/1439
This commit is contained in:
Etienne
2025-09-17 10:23:34 +02:00
committed by GitHub
parent 2430f21d83
commit 61b9d82950
31 changed files with 1091 additions and 232 deletions
@@ -13,6 +13,7 @@ import { GraphqlQueryDestroyOneResolverService } from 'src/engine/api/graphql/gr
import { GraphqlQueryFindDuplicatesResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-duplicates-resolver.service';
import { GraphqlQueryFindManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service';
import { GraphqlQueryFindOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service';
import { GraphqlQueryGroupByResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-group-by-resolver.service';
import { GraphqlQueryMergeManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-merge-many-resolver.service';
import { GraphqlQueryRestoreManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-many-resolver.service';
import { GraphqlQueryRestoreOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-one-resolver.service';
@@ -35,6 +36,7 @@ const graphqlQueryResolvers = [
GraphqlQueryFindDuplicatesResolverService,
GraphqlQueryFindManyResolverService,
GraphqlQueryFindOneResolverService,
GraphqlQueryGroupByResolverService,
GraphqlQueryMergeManyResolverService,
GraphqlQueryRestoreManyResolverService,
GraphqlQueryRestoreOneResolverService,
@@ -0,0 +1,332 @@
import { Injectable } from '@nestjs/common';
import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import {
GraphqlQueryBaseResolverService,
GraphqlQueryResolverExecutionArgs,
} from 'src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service';
import {
ObjectRecord,
ObjectRecordFilter,
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { IEdge } from 'src/engine/api/graphql/workspace-query-runner/interfaces/edge.interface';
import { IGroupByConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/group-by-connection.interface';
import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
import { GroupByResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import {
GraphqlQueryRunnerException,
GraphqlQueryRunnerExceptionCode,
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
@Injectable()
export class GraphqlQueryGroupByResolverService extends GraphqlQueryBaseResolverService<
GroupByResolverArgs,
IGroupByConnection<ObjectRecord, IEdge<ObjectRecord>>[]
> {
async resolve(
executionArgs: GraphqlQueryResolverExecutionArgs<GroupByResolverArgs>,
_featureFlagsMap: Record<FeatureFlagKey, boolean>,
): Promise<IGroupByConnection<ObjectRecord, IEdge<ObjectRecord>>[]> {
const { objectMetadataItemWithFieldMaps } = executionArgs.options;
const groupByFields = this.parseGroupByArgs(
executionArgs.args,
objectMetadataItemWithFieldMaps,
);
const aggregateMetrics = Object.keys(
executionArgs.graphqlQuerySelectedFieldsResult.aggregate,
);
const mockedGroupByValues = Array.from({ length: 3 }, (_, index) => ({
groupByDimensionValues: groupByFields.map((groupByField) =>
this.getFakeDimensionValues(groupByField, index),
),
...aggregateMetrics.reduce(
(acc, aggregateMetric, subIndex) => {
acc[aggregateMetric] = this.getFakeAggregateValues(subIndex + index);
return acc;
},
{} as Record<string, number>,
),
}));
return mockedGroupByValues as unknown as IGroupByConnection<
ObjectRecord,
IEdge<ObjectRecord>
>[];
}
private parseGroupByArgs(
args: GroupByResolverArgs,
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
): { fieldMetadata: FieldMetadataEntity; subFieldName?: string }[] {
const groupByFieldNames = args.groupBy;
const groupByFields = [];
for (const fieldNames of groupByFieldNames) {
if (Object.keys(fieldNames).length > 1) {
throw new GraphqlQueryRunnerException(
'You cannot provide multiple fields in one GroupByInput, split them into multiple GroupByInput',
GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
);
}
for (const fieldName of Object.keys(fieldNames)) {
const fieldMetadataId =
objectMetadataItemWithFieldMaps.fieldIdByName[fieldName];
const fieldMetadata =
objectMetadataItemWithFieldMaps.fieldsById[fieldMetadataId];
if (fieldNames[fieldName] === true) {
groupByFields.push({
fieldMetadata,
subFieldName: undefined,
});
continue;
} else if (typeof fieldNames[fieldName] === 'object') {
if (Object.keys(fieldNames[fieldName]).length > 1) {
throw new GraphqlQueryRunnerException(
'You cannot provide multiple subfields in one GroupByInput, split them into multiple GroupByInput',
GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
);
}
for (const subFieldName of Object.keys(fieldNames[fieldName])) {
if (fieldNames[fieldName][subFieldName] === true) {
groupByFields.push({
fieldMetadata,
subFieldName,
});
continue;
}
}
}
}
}
return groupByFields;
}
async validate(
_args: GroupByResolverArgs<ObjectRecordFilter>,
_options: WorkspaceQueryRunnerOptions,
): Promise<void> {}
//todo : remove these methods after the groupBy logic implementation
private getFakeDimensionValues(
groupByField: {
fieldMetadata: FieldMetadataEntity;
subFieldName?: string;
},
rank: number,
): string {
const fakeDimensionValuesByField = {
[FieldMetadataType.TEXT]: [
'Text 1',
'Long long long long and long text 2',
'Text 3',
],
[FieldMetadataType.NUMERIC]: [2000, 3000, 4000],
[FieldMetadataType.NUMBER]: [1, 2, 3],
[FieldMetadataType.BOOLEAN]: [true, false, true],
[FieldMetadataType.DATE_TIME]: [
new Date(2025, 5, 10, 12, 1, 0),
new Date(2025, 5, 10, 12, 1, 0),
new Date(2018, 6, 14, 12, 2, 0),
],
[FieldMetadataType.DATE]: [
new Date(2025, 1, 1),
new Date(2025, 1, 2),
new Date(2025, 1, 3),
],
[FieldMetadataType.UUID]: [
'00000000-0000-0000-0000-000000000000',
'00000000-0000-0000-0000-000000000001',
'00000000-0000-0000-0000-000000000002',
],
[FieldMetadataType.SELECT]: ['Option 1', 'Option 2', 'Option 3'],
[FieldMetadataType.MULTI_SELECT]: [
['Tag 1', 'Tag 2'],
['Tag 3'],
['Tag 4', 'Tag 5', 'Tag 6'],
],
[FieldMetadataType.RELATION]: [
{ id: 'relation-1', name: 'Relation 1' },
{ id: 'relation-2', name: 'Relation 2' },
{ id: 'relation-3', name: 'Relation 3' },
],
[FieldMetadataType.MORPH_RELATION]: [
{ id: 'morph-1', name: 'Morph Relation 1' },
{ id: 'morph-2', name: 'Morph Relation 2' },
{ id: 'morph-3', name: 'Morph Relation 3' },
],
[FieldMetadataType.RATING]: ['RATING_3', 'RATING_4', 'RATING_5'],
[FieldMetadataType.RAW_JSON]: [
{ key: 'value1' },
{ key: 'value2', key2: 'value2' },
{},
],
[FieldMetadataType.ARRAY]: [['value1', 'value2'], ['value3'], []],
[FieldMetadataType.POSITION]: [1, 2, 3],
[FieldMetadataType.TS_VECTOR]: ['vector1', 'vector2', 'vector3'],
[FieldMetadataType.EMAILS]: [
{
primaryEmail: 'tim@twenty.com',
additionalEmails: [
'tim@twenty.com',
'timapple@twenty.com',
'johnappletim@twenty.com',
],
},
{
primaryEmail: 'jane@twenty.com',
additionalEmails: ['jane@twenty.com', 'jane.doe@twenty.com'],
},
{
primaryEmail: 'john@twenty.com',
additionalEmails: ['john.doe@twenty.com'],
},
],
[FieldMetadataType.PHONES]: [
{
primaryPhoneCallingCode: '+33',
primaryPhoneCountryCode: 'FR',
primaryPhoneNumber: '789012345',
additionalPhones: [
{ number: '617272323', callingCode: '+33', countryCode: 'FR' },
],
},
{
primaryPhoneCallingCode: '+1',
primaryPhoneCountryCode: 'US',
primaryPhoneNumber: '612345789',
additionalPhones: [
{ number: '123456789', callingCode: '+1', countryCode: 'US' },
{ number: '617272323', callingCode: '+1', countryCode: 'US' },
],
},
{
primaryPhoneCallingCode: '+33',
primaryPhoneCountryCode: 'FR',
primaryPhoneNumber: '123456789',
additionalPhones: [],
},
],
[FieldMetadataType.CURRENCY]: [
{ amountMicros: 1000000, currencyCode: 'USD' },
{ amountMicros: 2000000, currencyCode: 'EUR' },
{ amountMicros: 3000000, currencyCode: 'GBP' },
],
[FieldMetadataType.LINKS]: [
{
primaryLinkUrl: 'twenty.com',
primaryLinkLabel: '',
secondaryLinks: [{ url: 'twenty.com', label: 'Twenty' }],
},
{
primaryLinkUrl: 'github.com/twentyhq/twenty',
primaryLinkLabel: 'Twenty Repo',
secondaryLinks: [{ url: 'twenty.com', label: '' }],
},
{
primaryLinkUrl: 'react.dev',
primaryLinkLabel: '',
secondaryLinks: [],
},
],
[FieldMetadataType.FULL_NAME]: [
{ firstName: 'John', lastName: 'Doe' },
{ firstName: 'Jane', lastName: 'Doe' },
{ firstName: 'John', lastName: 'Smith' },
],
[FieldMetadataType.ADDRESS]: [
{
addressStreet1: '456 Oak Street',
addressStreet2: '',
addressCity: 'Springfield',
addressState: 'California',
addressCountry: 'United States',
addressPostcode: '90210',
addressLat: 34.0522,
addressLng: -118.2437,
},
{
addressStreet1: '123 Main Street',
addressStreet2: '',
addressCity: 'New York',
addressState: 'New York',
addressCountry: 'United States',
addressPostcode: '10001',
addressLat: 40.7128,
addressLng: -74.006,
},
{
addressStreet1: '8 rue Saint-Anne',
addressStreet2: '',
addressCity: 'Paris',
addressState: 'Ile-de-France',
addressCountry: 'France',
addressPostcode: '75001',
addressLat: 40.7128,
addressLng: -74.006,
},
],
[FieldMetadataType.ACTOR]: [
{
source: 'IMPORT',
name: 'name',
workspaceMemberId: 'id',
context: { provider: 'GOOGLE' },
},
{
source: 'MANUAL',
name: 'name',
workspaceMemberId: 'id',
context: { provider: 'MICROSOFT' },
},
{
source: 'WEBHOOK',
name: 'name',
workspaceMemberId: 'id',
context: {},
},
],
[FieldMetadataType.RICH_TEXT_V2]: [
{
blocknote: '[{"type":"heading","content":"Hello"}]',
markdown: '# Hello',
},
{
blocknote: '[{"type":"heading","content":"Hello World"}]',
markdown: '# Hello World',
},
{
blocknote: '[{"type":"heading","content":"Hello Again"}]',
markdown: '# Hello Again',
},
],
[FieldMetadataType.RICH_TEXT]: [],
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as Record<FieldMetadataType, any>;
const fakeDimensionValues =
fakeDimensionValuesByField[groupByField.fieldMetadata.type][rank % 3];
return isDefined(groupByField.subFieldName)
? fakeDimensionValues[groupByField.subFieldName].toString()
: fakeDimensionValues.toString();
}
private getFakeAggregateValues(rank: number) {
const fakeAggregateValues = [39, 20, 56, 88, 2];
return fakeAggregateValues[rank % fakeAggregateValues.length];
}
}
@@ -12,6 +12,18 @@ export type ObjectRecordFilter = Partial<{
[Property in keyof ObjectRecord]: any;
}>;
export type ObjectRecordGroupBy = Array<
ObjectRecordGroupByForAtomicField | ObjectRecordGroupByForCompositeField
>;
export type ObjectRecordGroupByForAtomicField = Partial<{
[Property in keyof ObjectRecord]: boolean;
}>;
export type ObjectRecordGroupByForCompositeField = Partial<{
[Property in keyof ObjectRecord]: Record<string, boolean>;
}>;
export enum OrderByDirection {
AscNullsFirst = 'AscNullsFirst',
AscNullsLast = 'AscNullsLast',
@@ -0,0 +1,9 @@
import { type IConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/connection.interface';
import { type IEdge } from 'src/engine/api/graphql/workspace-query-runner/interfaces/edge.interface';
export type IGroupByConnection<
T,
CustomEdge extends IEdge<T> = IEdge<T>,
> = IConnection<T, CustomEdge> & {
groupByDimensionValues: string[];
};
@@ -2,6 +2,7 @@ export const RESOLVER_METHOD_NAMES = {
FIND_MANY: 'findMany',
FIND_ONE: 'findOne',
FIND_DUPLICATES: 'findDuplicates',
GROUP_BY: 'groupBy',
CREATE_MANY: 'createMany',
CREATE_ONE: 'createOne',
UPDATE_MANY: 'updateMany',
@@ -1,5 +1,6 @@
import { DestroyManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/destroy-many-resolver.factory';
import { DestroyOneResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/destroy-one-resolver.factory';
import { GroupByResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/group-by-resolver.factory';
import { RestoreManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/restore-many-resolver.factory';
import { RestoreOneResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/restore-one-resolver.factory';
import { UpdateManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/update-many-resolver.factory';
@@ -29,6 +30,7 @@ export const workspaceResolverBuilderFactories = [
RestoreOneResolverFactory,
RestoreManyResolverFactory,
MergeManyResolverFactory,
GroupByResolverFactory,
];
export const workspaceResolverBuilderMethodNames = {
@@ -36,6 +38,7 @@ export const workspaceResolverBuilderMethodNames = {
FindManyResolverFactory.methodName,
FindOneResolverFactory.methodName,
FindDuplicatesResolverFactory.methodName,
GroupByResolverFactory.methodName,
],
mutations: [
CreateManyResolverFactory.methodName,
@@ -0,0 +1,45 @@
import { Injectable } from '@nestjs/common';
import { type WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
import { type WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
import {
GroupByResolverArgs,
type Resolver,
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
import { GraphqlQueryGroupByResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-group-by-resolver.service';
import { RESOLVER_METHOD_NAMES } from 'src/engine/api/graphql/workspace-resolver-builder/constants/resolver-method-names';
@Injectable()
export class GroupByResolverFactory
implements WorkspaceResolverBuilderFactoryInterface
{
public static methodName = RESOLVER_METHOD_NAMES.GROUP_BY;
constructor(
private readonly graphqlQueryRunnerService: GraphqlQueryGroupByResolverService,
) {}
create(
context: WorkspaceSchemaBuilderContext,
): Resolver<GroupByResolverArgs> {
const internalContext = context;
return async (_source, args, _context, info) => {
const options: WorkspaceQueryRunnerOptions = {
authContext: internalContext.authContext,
info,
objectMetadataMaps: internalContext.objectMetadataMaps,
objectMetadataItemWithFieldMaps:
internalContext.objectMetadataItemWithFieldMaps,
};
return await this.graphqlQueryRunnerService.execute(
args,
options,
GroupByResolverFactory.methodName,
);
};
}
}
@@ -3,6 +3,7 @@ import { type GraphQLFieldResolver } from 'graphql';
import {
type ObjectRecord,
type ObjectRecordFilter,
type ObjectRecordGroupBy,
type ObjectRecordOrderBy,
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
@@ -64,6 +65,13 @@ export interface CreateManyResolverArgs<
upsert?: boolean;
}
export interface GroupByResolverArgs<Filter = ObjectRecordFilter> {
filter?: Filter;
groupBy: ObjectRecordGroupBy;
viewId?: string;
orderBy?: ObjectRecordOrderBy;
}
export interface UpdateOneResolverArgs<
Data extends Partial<ObjectRecord> = Partial<ObjectRecord>,
> {
@@ -131,6 +139,7 @@ export interface WorkspaceResolverBuilderMethods {
export type ResolverArgs =
| CreateManyResolverArgs
| CreateOneResolverArgs
| GroupByResolverArgs
| DeleteManyResolverArgs
| DeleteOneResolverArgs
| DestroyManyResolverArgs
@@ -6,6 +6,7 @@ import { isDefined } from 'twenty-shared/utils';
import { DeleteManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/delete-many-resolver.factory';
import { DestroyManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/destroy-many-resolver.factory';
import { DestroyOneResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/destroy-one-resolver.factory';
import { GroupByResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/group-by-resolver.factory';
import { MergeManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/merge-many-resolver.factory';
import { RestoreManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/restore-many-resolver.factory';
import { RestoreOneResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/restore-one-resolver.factory';
@@ -54,6 +55,7 @@ export class WorkspaceResolverFactory {
private readonly restoreManyResolverFactory: RestoreManyResolverFactory,
private readonly destroyManyResolverFactory: DestroyManyResolverFactory,
private readonly mergeManyResolverFactory: MergeManyResolverFactory,
private readonly groupByResolverFactory: GroupByResolverFactory,
private readonly workspaceResolverBuilderService: WorkspaceResolverBuilderService,
private readonly featureFlagService: FeatureFlagService,
) {}
@@ -81,6 +83,7 @@ export class WorkspaceResolverFactory {
['restoreOne', this.restoreOneResolverFactory],
['updateMany', this.updateManyResolverFactory],
['updateOne', this.updateOneResolverFactory],
['groupBy', this.groupByResolverFactory],
]);
const resolvers: IResolvers = {
Query: {},
@@ -3,4 +3,5 @@ export enum GqlInputTypeDefinitionKind {
Update = 'Update',
Filter = 'Filter',
OrderBy = 'OrderBy',
GroupBy = 'GroupBy',
}
@@ -1,4 +1,5 @@
export enum ObjectTypeDefinitionKind {
GroupByConnection = 'GroupByConnection',
Connection = 'Connection',
Edge = 'Edge',
Plain = '',
@@ -10,6 +10,7 @@ import { RelationConnectGqlInputTypeGenerator } from 'src/engine/api/graphql/wor
import { CompositeFieldMetadataGqlObjectTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/composite-field-metadata-gql-object-type.generator';
import { ConnectionGqlObjectTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/connection-gql-object-type.generator';
import { EdgeGqlObjectTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/edge-gql-object-type.generator';
import { GroupByConnectionGqlObjectTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/group-by-connection-gql-object-type.generator';
import { ObjectMetadataGqlObjectTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/object-metadata-gql-object-type.generator';
import { ObjectMetadataWithRelationsGqlObjectTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/object-metadata-with-relations-gql-object-type.generator';
import { MutationTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/root-types/mutation-type.generator';
@@ -32,6 +33,7 @@ export class GqlTypeGenerator {
private readonly compositeFieldMetadataGqlInputTypeGenerator: CompositeFieldMetadataGqlInputTypeGenerator,
private readonly edgeGqlObjectTypeGenerator: EdgeGqlObjectTypeGenerator,
private readonly connectionGqlObjectTypeGenerator: ConnectionGqlObjectTypeGenerator,
private readonly groupByConnectionGqlObjectTypeGenerator: GroupByConnectionGqlObjectTypeGenerator,
private readonly objectMetadataWithRelationsGqlObjectTypeGenerator: ObjectMetadataWithRelationsGqlObjectTypeGenerator,
private readonly relationConnectGqlInputTypeGenerator: RelationConnectGqlInputTypeGenerator,
private readonly queryTypeGenerator: QueryTypeGenerator,
@@ -77,6 +79,9 @@ export class GqlTypeGenerator {
this.objectMetadataGqlObjectTypeGenerator.buildAndStore(objectMetadata);
this.edgeGqlObjectTypeGenerator.buildAndStore(objectMetadata);
this.connectionGqlObjectTypeGenerator.buildAndStore(objectMetadata);
this.groupByConnectionGqlObjectTypeGenerator.buildAndStore(
objectMetadata,
);
this.relationConnectGqlInputTypeGenerator.buildAndStore(objectMetadata);
this.objectMetadataGqlInputTypeGenerator.buildAndStore(objectMetadata);
@@ -4,6 +4,7 @@ import { type CompositeType } from 'src/engine/metadata-modules/field-metadata/i
import { CompositeFieldMetadataCreateGqlInputTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/create-input/composite-field-metadata-create-gql-input-type.generator';
import { CompositeFieldMetadataFilterGqlInputTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/filter-input/composite-field-metadata-filter-gql-input-types.generator';
import { CompositeFieldMetadataGroupByGqlInputTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/group-by-input/composite-field-metadata-group-by-gql-input-type.generator';
import { CompositeFieldMetadataOrderByGqlInputTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/order-by-input/composite-field-metadata-order-by-gql-input-type.generator';
import { CompositeFieldMetadataUpdateGqlInputTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/update-input/composite-field-metadata-update-gql-input-type.generator';
@@ -14,6 +15,7 @@ export class CompositeFieldMetadataGqlInputTypeGenerator {
private readonly compositeFieldMetadataUpdateGqlInputTypeGenerator: CompositeFieldMetadataUpdateGqlInputTypeGenerator,
private readonly compositeFieldMetadataFilterGqlInputTypeGenerator: CompositeFieldMetadataFilterGqlInputTypeGenerator,
private readonly compositeFieldMetadataOrderByGqlInputTypeGenerator: CompositeFieldMetadataOrderByGqlInputTypeGenerator,
private readonly compositeFieldMetadataGroupByGqlInputTypeGenerator: CompositeFieldMetadataGroupByGqlInputTypeGenerator,
) {}
public buildAndStore(compositeType: CompositeType) {
@@ -29,5 +31,8 @@ export class CompositeFieldMetadataGqlInputTypeGenerator {
this.compositeFieldMetadataOrderByGqlInputTypeGenerator.buildAndStore(
compositeType,
);
this.compositeFieldMetadataGroupByGqlInputTypeGenerator.buildAndStore(
compositeType,
);
}
}
@@ -1,25 +1,27 @@
import { Injectable, Logger } from '@nestjs/common';
import { isDefined } from 'class-validator';
import {
GraphQLEnumType,
GraphQLInputFieldConfigMap,
GraphQLInputObjectType,
GraphQLScalarType,
isEnumType,
isInputObjectType,
isObjectType,
} from 'graphql';
import { isDefined } from 'twenty-shared/utils';
import { GqlInputTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schema-builder/enums/gql-input-type-definition-kind.enum';
import { RelationFieldMetadataGqlInputTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/relation-field-metadata-gql-type.generator';
import { TypeMapperService } from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service';
import {
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 { computeFieldInputTypeOptions } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-field-input-type-options.util';
import { computeCompositeFieldInputTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-composite-field-input-type-key.util';
import { computeEnumFieldGqlTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-enum-field-gql-type-key.util';
import { computeObjectMetadataInputTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-object-metadata-input-type.util';
import { isFieldMetadataRelationOrMorphRelation } from 'src/engine/api/graphql/workspace-schema-builder/utils/is-field-metadata-relation-or-morph-relation.utils';
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 { isEnumFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
@@ -79,77 +81,113 @@ export class ObjectMetadataCreateGqlInputTypeGenerator {
},
),
};
} else if (isEnumFieldMetadataType(fieldMetadata.type)) {
generatedFields = this.generateEnumFieldCreateInputType(
objectMetadata,
fieldMetadata,
typeOptions,
);
} else if (isCompositeFieldMetadataType(fieldMetadata.type)) {
generatedFields = this.generateCompositeFieldCreateInputType(
fieldMetadata,
typeOptions,
);
} else {
let type:
| GraphQLInputObjectType
| GraphQLEnumType
| GraphQLScalarType
| undefined;
if (isEnumFieldMetadataType(fieldMetadata.type)) {
const key = computeEnumFieldGqlTypeKey(
objectMetadata.nameSingular,
fieldMetadata.name,
);
const enumType = this.gqlTypesStorage.getGqlTypeByKey(key);
if (!isDefined(enumType) || !isEnumType(enumType)) {
const message = `Could not find a GraphQL input type for ${fieldMetadata.type} field metadata`;
this.logger.error(message, {
type,
typeOptions,
});
throw new Error(message);
}
type = enumType;
} else if (isCompositeFieldMetadataType(fieldMetadata.type)) {
const key = computeCompositeFieldInputTypeKey(
fieldMetadata.type,
GqlInputTypeDefinitionKind.Create,
);
const compositeType = this.gqlTypesStorage.getGqlTypeByKey(key);
if (!isDefined(compositeType) || !isInputObjectType(compositeType)) {
const message = `Could not find a GraphQL input type for ${fieldMetadata.type} field metadata`;
this.logger.error(message, {
type,
typeOptions,
});
throw new Error(message);
}
type = compositeType;
} else {
type = this.typeMapperService.mapToScalarType(
fieldMetadata.type,
typeOptions,
);
if (!isDefined(type) || isObjectType(type)) {
const message = `Could not find a GraphQL input type for ${fieldMetadata.type} field metadata`;
this.logger.error(message, {
type,
typeOptions,
});
throw new Error(message);
}
}
generatedFields = {
[fieldMetadata.name]: {
type,
description: fieldMetadata.description,
},
};
generatedFields = this.generateAtomicFieldCreateInputType(
fieldMetadata,
typeOptions,
);
}
Object.assign(allGeneratedFields, generatedFields);
}
return allGeneratedFields;
}
private generateEnumFieldCreateInputType(
objectMetadata: ObjectMetadataEntity,
fieldMetadata: FieldMetadataEntity,
typeOptions: TypeOptions,
) {
const key = computeEnumFieldGqlTypeKey(
objectMetadata.nameSingular,
fieldMetadata.name,
);
const enumType = this.gqlTypesStorage.getGqlTypeByKey(key);
if (!isDefined(enumType) || !isEnumType(enumType)) {
const message = `Could not find a GraphQL input type for ${fieldMetadata.type} field metadata`;
this.logger.error(message, {
fieldMetadata,
typeOptions,
});
throw new Error(message);
}
return {
[fieldMetadata.name]: {
type: enumType,
description: fieldMetadata.description,
},
};
}
private generateCompositeFieldCreateInputType(
fieldMetadata: FieldMetadataEntity,
typeOptions: TypeOptions,
) {
const key = computeCompositeFieldInputTypeKey(
fieldMetadata.type,
GqlInputTypeDefinitionKind.Create,
);
const compositeType = this.gqlTypesStorage.getGqlTypeByKey(key);
if (!isDefined(compositeType) || !isInputObjectType(compositeType)) {
const message = `Could not find a GraphQL input type for ${fieldMetadata.type} field metadata`;
this.logger.error(message, {
fieldMetadata,
typeOptions,
});
throw new Error(message);
}
return {
[fieldMetadata.name]: {
type: compositeType,
description: fieldMetadata.description,
},
};
}
private generateAtomicFieldCreateInputType(
fieldMetadata: FieldMetadataEntity,
typeOptions: TypeOptions,
) {
const type = this.typeMapperService.mapToScalarType(
fieldMetadata.type,
typeOptions,
);
if (!isDefined(type) || isObjectType(type)) {
const message = `Could not find a GraphQL input type for ${fieldMetadata.type} field metadata`;
this.logger.error(message, {
type,
typeOptions,
});
throw new Error(message);
}
return {
[fieldMetadata.name]: {
type,
description: fieldMetadata.description,
},
};
}
}
@@ -3,7 +3,6 @@ import { Injectable, Logger } from '@nestjs/common';
import {
GraphQLInputFieldConfigMap,
GraphQLInputObjectType,
GraphQLInputType,
isEnumType,
isInputObjectType,
isObjectType,
@@ -12,7 +11,10 @@ import { isDefined } from 'twenty-shared/utils';
import { GqlInputTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schema-builder/enums/gql-input-type-definition-kind.enum';
import { RelationFieldMetadataGqlInputTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/relation-field-metadata-gql-type.generator';
import { TypeMapperService } from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service';
import {
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 { computeFieldInputTypeOptions } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-field-input-type-options.util';
import { computeCompositeFieldInputTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-composite-field-input-type-key.util';
@@ -20,6 +22,7 @@ import { computeEnumFieldGqlTypeKey } from 'src/engine/api/graphql/workspace-sch
import { computeObjectMetadataInputTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-object-metadata-input-type.util';
import { createGqlEnumFilterType } from 'src/engine/api/graphql/workspace-schema-builder/utils/create-gql-enum-filter-type.util';
import { isFieldMetadataRelationOrMorphRelation } from 'src/engine/api/graphql/workspace-schema-builder/utils/is-field-metadata-relation-or-morph-relation.utils';
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 { isEnumFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
@@ -67,7 +70,7 @@ export class ObjectMetadataFilterGqlInputTypeGenerator {
const typeOptions = computeFieldInputTypeOptions(
fieldMetadata,
GqlInputTypeDefinitionKind.Update,
GqlInputTypeDefinitionKind.Filter,
);
let generatedFields;
@@ -80,69 +83,22 @@ export class ObjectMetadataFilterGqlInputTypeGenerator {
typeOptions,
},
);
} else if (isEnumFieldMetadataType(fieldMetadata.type)) {
generatedFields = this.generateEnumFieldFilterInputType(
objectMetadata,
fieldMetadata,
typeOptions,
);
} else if (isCompositeFieldMetadataType(fieldMetadata.type)) {
generatedFields = this.generateCompositeFieldFilterInputType(
fieldMetadata,
typeOptions,
);
} else {
let type: GraphQLInputType | undefined;
if (isEnumFieldMetadataType(fieldMetadata.type)) {
const key = computeEnumFieldGqlTypeKey(
objectMetadata.nameSingular,
fieldMetadata.name,
);
const enumType = this.gqlTypesStorage.getGqlTypeByKey(key);
if (!isDefined(enumType) || !isEnumType(enumType)) {
const message = `Could not find a GraphQL input type for ${fieldMetadata.type} field metadata`;
this.logger.error(message, {
type,
typeOptions,
});
throw new Error(message);
}
type = createGqlEnumFilterType(enumType);
} else if (isCompositeFieldMetadataType(fieldMetadata.type)) {
const key = computeCompositeFieldInputTypeKey(
fieldMetadata.type,
GqlInputTypeDefinitionKind.Filter,
);
const compositeType = this.gqlTypesStorage.getGqlTypeByKey(key);
if (!isDefined(compositeType) || !isInputObjectType(compositeType)) {
const message = `Could not find a GraphQL input type for ${fieldMetadata.type} field metadata`;
this.logger.error(message, {
type,
typeOptions,
});
throw new Error(message);
}
type = compositeType;
} else {
type = this.typeMapperService.mapToFilterType(
fieldMetadata.type,
typeOptions,
);
if (!isDefined(type) || isObjectType(type)) {
const message = `Could not find a GraphQL input type for ${fieldMetadata.type} field metadata`;
this.logger.error(message, {
type,
typeOptions,
});
throw new Error(message);
}
}
generatedFields = {
[fieldMetadata.name]: {
type,
description: fieldMetadata.description,
},
};
generatedFields = this.generateAtomicFieldFilterInputType(
fieldMetadata,
typeOptions,
);
}
Object.assign(allGeneratedFields, generatedFields);
}
@@ -162,4 +118,92 @@ export class ObjectMetadataFilterGqlInputTypeGenerator {
},
};
}
private generateEnumFieldFilterInputType(
objectMetadata: ObjectMetadataEntity,
fieldMetadata: FieldMetadataEntity,
typeOptions: TypeOptions,
) {
const key = computeEnumFieldGqlTypeKey(
objectMetadata.nameSingular,
fieldMetadata.name,
);
const enumType = this.gqlTypesStorage.getGqlTypeByKey(key);
if (!isDefined(enumType) || !isEnumType(enumType)) {
const message = `Could not find a GraphQL input type for ${fieldMetadata.type} field metadata`;
this.logger.error(message, {
fieldMetadata,
typeOptions,
});
throw new Error(message);
}
const type = createGqlEnumFilterType(enumType);
return {
[fieldMetadata.name]: {
type,
description: fieldMetadata.description,
},
};
}
private generateCompositeFieldFilterInputType(
fieldMetadata: FieldMetadataEntity,
typeOptions: TypeOptions,
) {
const key = computeCompositeFieldInputTypeKey(
fieldMetadata.type,
GqlInputTypeDefinitionKind.Filter,
);
const compositeType = this.gqlTypesStorage.getGqlTypeByKey(key);
if (!isDefined(compositeType) || !isInputObjectType(compositeType)) {
const message = `Could not find a GraphQL input type for ${fieldMetadata.type} field metadata`;
this.logger.error(message, {
fieldMetadata,
typeOptions,
});
throw new Error(message);
}
return {
[fieldMetadata.name]: {
type: compositeType,
description: fieldMetadata.description,
},
};
}
private generateAtomicFieldFilterInputType(
fieldMetadata: FieldMetadataEntity,
typeOptions: TypeOptions,
) {
const type = this.typeMapperService.mapToFilterType(
fieldMetadata.type,
typeOptions,
);
if (!isDefined(type) || isObjectType(type)) {
const message = `Could not find a GraphQL input type for ${fieldMetadata.type} field metadata`;
this.logger.error(message, {
fieldMetadata,
typeOptions,
});
throw new Error(message);
}
return {
[fieldMetadata.name]: {
type,
description: fieldMetadata.description,
},
};
}
}
@@ -0,0 +1,73 @@
import { Injectable, Logger } from '@nestjs/common';
import {
GraphQLBoolean,
GraphQLInputFieldConfigMap,
GraphQLInputObjectType,
} from 'graphql';
import { CompositeType } from 'src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface';
import { GqlInputTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schema-builder/enums/gql-input-type-definition-kind.enum';
import { TypeMapperService } from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service';
import { GqlTypesStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/gql-types.storage';
import { computeCompositeFieldInputTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-composite-field-input-type-key.util';
import { isMorphOrRelationFieldMetadataType } from 'src/engine/utils/is-morph-or-relation-field-metadata-type.util';
import { pascalCase } from 'src/utils/pascal-case';
@Injectable()
export class CompositeFieldMetadataGroupByGqlInputTypeGenerator {
private readonly logger = new Logger(
CompositeFieldMetadataGroupByGqlInputTypeGenerator.name,
);
constructor(
private readonly gqlTypesStorage: GqlTypesStorage,
private readonly typeMapperService: TypeMapperService,
) {}
public buildAndStore(compositeType: CompositeType) {
const key = computeCompositeFieldInputTypeKey(
compositeType.type,
GqlInputTypeDefinitionKind.GroupBy,
);
const type = new GraphQLInputObjectType({
name: `${pascalCase(compositeType.type)}${GqlInputTypeDefinitionKind.GroupBy.toString()}Input`,
fields: this.generateFields(compositeType),
});
this.gqlTypesStorage.addGqlType(key, type);
}
public generateFields(
compositeType: CompositeType,
): GraphQLInputFieldConfigMap {
const fields: GraphQLInputFieldConfigMap = {};
for (const property of compositeType.properties) {
// Relation fields are not supported in composite types
if (isMorphOrRelationFieldMetadataType(property.type)) {
this.logger.error(
'Relation fields are not supported in composite types',
{ compositeType, property },
);
throw new Error('Relation fields are not supported in composite types');
}
// Skip hidden fields
if (property.hidden === true || property.hidden === 'input') {
continue;
}
const type = this.typeMapperService.applyTypeOptions(GraphQLBoolean, {});
fields[property.name] = {
type,
description: property.description,
};
}
return fields;
}
}
@@ -0,0 +1,109 @@
import { Injectable, Logger } from '@nestjs/common';
import {
GraphQLBoolean,
GraphQLInputFieldConfigMap,
GraphQLInputObjectType,
isInputObjectType,
} from 'graphql';
import { isDefined } from 'twenty-shared/utils';
import { GqlInputTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schema-builder/enums/gql-input-type-definition-kind.enum';
import { RelationFieldMetadataGqlInputTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/relation-field-metadata-gql-type.generator';
import { TypeMapperService } from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service';
import { GqlTypesStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/gql-types.storage';
import { computeCompositeFieldInputTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-composite-field-input-type-key.util';
import { computeObjectMetadataInputTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-object-metadata-input-type.util';
import { isFieldMetadataRelationOrMorphRelation } from 'src/engine/api/graphql/workspace-schema-builder/utils/is-field-metadata-relation-or-morph-relation.utils';
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 { pascalCase } from 'src/utils/pascal-case';
@Injectable()
export class ObjectMetadataGroupByGqlInputTypeGenerator {
private readonly logger = new Logger(
ObjectMetadataGroupByGqlInputTypeGenerator.name,
);
constructor(
private readonly gqlTypesStorage: GqlTypesStorage,
private readonly relationFieldMetadataGqlInputTypeGenerator: RelationFieldMetadataGqlInputTypeGenerator,
private readonly typeMapperService: TypeMapperService,
) {}
public buildAndStore(objectMetadata: ObjectMetadataEntity) {
const inputType = new GraphQLInputObjectType({
name: `${pascalCase(objectMetadata.nameSingular)}${GqlInputTypeDefinitionKind.GroupBy.toString()}Input`,
description: objectMetadata.description,
fields: () => this.generateFields(objectMetadata),
}) as GraphQLInputObjectType;
const key = computeObjectMetadataInputTypeKey(
objectMetadata.nameSingular,
GqlInputTypeDefinitionKind.GroupBy,
);
this.gqlTypesStorage.addGqlType(key, inputType);
}
private generateFields(
objectMetadata: ObjectMetadataEntity,
): GraphQLInputFieldConfigMap {
const allGeneratedFields: GraphQLInputFieldConfigMap = {};
for (const fieldMetadata of objectMetadata.fields) {
const generatedField = isFieldMetadataRelationOrMorphRelation(
fieldMetadata,
)
? this.relationFieldMetadataGqlInputTypeGenerator.generateSimpleRelationFieldGroupByInputType(
fieldMetadata,
)
: this.generateField(fieldMetadata);
Object.assign(allGeneratedFields, generatedField);
}
return allGeneratedFields;
}
private generateField(fieldMetadata: FieldMetadataEntity) {
if (isCompositeFieldMetadataType(fieldMetadata.type))
return this.generateCompositeFieldGroupByInputType(fieldMetadata);
const type = this.typeMapperService.applyTypeOptions(GraphQLBoolean, {});
return {
[fieldMetadata.name]: {
type,
description: fieldMetadata.description,
},
};
}
private generateCompositeFieldGroupByInputType(
fieldMetadata: FieldMetadataEntity,
) {
const key = computeCompositeFieldInputTypeKey(
fieldMetadata.type,
GqlInputTypeDefinitionKind.GroupBy,
);
const compositeType = this.gqlTypesStorage.getGqlTypeByKey(key);
if (!isDefined(compositeType) || !isInputObjectType(compositeType)) {
const message = `Could not find a GraphQL input type for ${fieldMetadata.type} field metadata`;
this.logger.error(message, {
fieldMetadata,
});
throw new Error(message);
}
return {
[fieldMetadata.name]: {
type: compositeType,
description: fieldMetadata.description,
},
};
}
}
@@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
import { ObjectMetadataCreateGqlInputTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/create-input/object-metadata-create-gql-input-type.generator';
import { ObjectMetadataFilterGqlInputTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/filter-input/object-metadata-filter-gql-input-type.generator';
import { ObjectMetadataGroupByGqlInputTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/group-by-input/object-metadata-group-by-gql-input-type.generator';
import { ObjectMetadataOrderByGqlInputTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/order-by-input/object-metadata-order-by-gql-input-type.generator';
import { ObjectMetadataUpdateGqlInputTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/update-input/object-metadata-update-gql-input-type.generator';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
@@ -13,6 +14,7 @@ export class ObjectMetadataGqlInputTypeGenerator {
private readonly objectMetadataUpdateGqlInputTypeGenerator: ObjectMetadataUpdateGqlInputTypeGenerator,
private readonly objectMetadataFilterGqlInputTypeGenerator: ObjectMetadataFilterGqlInputTypeGenerator,
private readonly objectMetadataOrderByGqlInputTypeGenerator: ObjectMetadataOrderByGqlInputTypeGenerator,
private readonly objectMetadataGroupByGqlInputTypeGenerator: ObjectMetadataGroupByGqlInputTypeGenerator,
) {}
public buildAndStore(objectMetadata: ObjectMetadataEntity) {
@@ -28,5 +30,8 @@ export class ObjectMetadataGqlInputTypeGenerator {
this.objectMetadataOrderByGqlInputTypeGenerator.buildAndStore(
objectMetadata,
);
this.objectMetadataGroupByGqlInputTypeGenerator.buildAndStore(
objectMetadata,
);
}
}
@@ -13,9 +13,7 @@ import { GqlInputTypeDefinitionKind } from 'src/engine/api/graphql/workspace-sch
import { TypeMapperService } from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service';
import { GqlTypesStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/gql-types.storage';
import { computeCompositeFieldTypeOptions } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-composite-field-type-options.util';
import { computeCompositeFieldEnumTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-composite-field-enum-type-key.util';
import { computeCompositeFieldInputTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-composite-field-input-type-key.util';
import { isEnumFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util';
import { isMorphOrRelationFieldMetadataType } from 'src/engine/utils/is-morph-or-relation-field-metadata-type.util';
import { pascalCase } from 'src/utils/pascal-case';
@@ -61,20 +59,13 @@ export class CompositeFieldMetadataOrderByGqlInputTypeGenerator {
}
// Skip hidden fields
if (property.hidden === true || property.hidden === 'input') {
if (property.hidden === true) {
continue;
}
const key = computeCompositeFieldEnumTypeKey(
compositeType.type,
property.name,
);
const typeOptions = computeCompositeFieldTypeOptions(property);
const type = isEnumFieldMetadataType(property.type)
? this.gqlTypesStorage.getGqlTypeByKey(key)
: this.typeMapperService.mapToOrderByType(property.type);
const type = this.typeMapperService.mapToOrderByType(property.type);
if (!isDefined(type) || isObjectType(type)) {
const message = `Could not find a GraphQL input type for ${compositeType.type} ${property.name}`;
@@ -1,12 +1,8 @@
import { Injectable, Logger } from '@nestjs/common';
import {
GraphQLEnumType,
GraphQLInputFieldConfigMap,
GraphQLInputObjectType,
GraphQLInputType,
GraphQLList,
GraphQLScalarType,
isEnumType,
isInputObjectType,
} from 'graphql';
@@ -14,13 +10,17 @@ import { isDefined } from 'twenty-shared/utils';
import { GqlInputTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schema-builder/enums/gql-input-type-definition-kind.enum';
import { RelationFieldMetadataGqlInputTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/relation-field-metadata-gql-type.generator';
import { TypeMapperService } from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service';
import {
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 { computeFieldInputTypeOptions } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-field-input-type-options.util';
import { computeCompositeFieldInputTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-composite-field-input-type-key.util';
import { computeEnumFieldGqlTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-enum-field-gql-type-key.util';
import { computeObjectMetadataInputTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-object-metadata-input-type.util';
import { isFieldMetadataRelationOrMorphRelation } from 'src/engine/api/graphql/workspace-schema-builder/utils/is-field-metadata-relation-or-morph-relation.utils';
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 { isEnumFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
@@ -68,93 +68,119 @@ export class ObjectMetadataOrderByGqlInputTypeGenerator {
let generatedFields;
if (isFieldMetadataRelationOrMorphRelation(fieldMetadata)) {
generatedFields = {
...this.relationFieldMetadataGqlInputTypeGenerator.generateSimpleRelationFieldCreateOrUpdateInputType(
generatedFields =
this.relationFieldMetadataGqlInputTypeGenerator.generateSimpleRelationFieldOrderByInputType(
{
fieldMetadata,
typeOptions,
},
),
...this.relationFieldMetadataGqlInputTypeGenerator.generateConnectRelationFieldInputType(
{
fieldMetadata,
typeOptions,
},
),
};
);
} else if (isEnumFieldMetadataType(fieldMetadata.type)) {
generatedFields = this.generateEnumFieldOrderByInputType(
objectMetadata,
fieldMetadata,
typeOptions,
);
} else if (isCompositeFieldMetadataType(fieldMetadata.type)) {
generatedFields = this.generateCompositeFieldOrderByInputType(
fieldMetadata,
typeOptions,
);
} else {
let type:
| GraphQLInputObjectType
| GraphQLEnumType
| GraphQLScalarType
| GraphQLList<GraphQLInputType>
| undefined;
if (isEnumFieldMetadataType(fieldMetadata.type)) {
const key = computeEnumFieldGqlTypeKey(
objectMetadata.nameSingular,
fieldMetadata.name,
);
const enumType = this.gqlTypesStorage.getGqlTypeByKey(key);
if (!isDefined(enumType) || !isEnumType(enumType)) {
const message = `Could not find a GraphQL input type for ${fieldMetadata.type} field metadata`;
this.logger.error(message, {
type,
typeOptions,
});
throw new Error(message);
}
type = enumType;
} else if (isCompositeFieldMetadataType(fieldMetadata.type)) {
const key = computeCompositeFieldInputTypeKey(
fieldMetadata.type,
GqlInputTypeDefinitionKind.OrderBy,
);
const compositeType = this.gqlTypesStorage.getGqlTypeByKey(key);
if (!isDefined(compositeType) || !isInputObjectType(compositeType)) {
const message = `Could not find a GraphQL input type for ${fieldMetadata.type} field metadata`;
this.logger.error(message, {
type,
typeOptions,
});
throw new Error(message);
}
type = compositeType;
} else {
const orderByType = this.typeMapperService.mapToOrderByType(
fieldMetadata.type,
);
if (!isDefined(orderByType)) {
const message = `Could not find a GraphQL input type for ${fieldMetadata.type} field metadata`;
this.logger.error(message, {
type,
typeOptions,
});
throw new Error(message);
}
type = orderByType;
}
generatedFields = {
[fieldMetadata.name]: {
type,
description: fieldMetadata.description,
},
};
generatedFields = this.generateAtomicFieldOrderByInputType(
fieldMetadata,
typeOptions,
);
}
Object.assign(allGeneratedFields, generatedFields);
}
return allGeneratedFields;
}
private generateCompositeFieldOrderByInputType(
fieldMetadata: FieldMetadataEntity,
typeOptions: TypeOptions,
) {
const key = computeCompositeFieldInputTypeKey(
fieldMetadata.type,
GqlInputTypeDefinitionKind.OrderBy,
);
const compositeType = this.gqlTypesStorage.getGqlTypeByKey(key);
if (!isDefined(compositeType) || !isInputObjectType(compositeType)) {
const message = `Could not find a GraphQL input type for ${fieldMetadata.type} field metadata`;
this.logger.error(message, {
fieldMetadata,
typeOptions,
});
throw new Error(message);
}
return {
[fieldMetadata.name]: {
type: compositeType,
description: fieldMetadata.description,
},
};
}
private generateAtomicFieldOrderByInputType(
fieldMetadata: FieldMetadataEntity,
typeOptions: TypeOptions,
) {
const orderByType = this.typeMapperService.mapToOrderByType(
fieldMetadata.type,
);
if (!isDefined(orderByType)) {
const message = `Could not find a GraphQL input type for ${fieldMetadata.type} field metadata`;
this.logger.error(message, {
fieldMetadata,
typeOptions,
});
throw new Error(message);
}
return {
[fieldMetadata.name]: {
type: orderByType,
description: fieldMetadata.description,
},
};
}
private generateEnumFieldOrderByInputType(
objectMetadata: ObjectMetadataEntity,
fieldMetadata: FieldMetadataEntity,
typeOptions: TypeOptions,
) {
const key = computeEnumFieldGqlTypeKey(
objectMetadata.nameSingular,
fieldMetadata.name,
);
const enumType = this.gqlTypesStorage.getGqlTypeByKey(key);
if (!isDefined(enumType) || !isEnumType(enumType)) {
const message = `Could not find a GraphQL input type for ${fieldMetadata.type} field metadata`;
this.logger.error(message, {
fieldMetadata,
typeOptions,
});
throw new Error(message);
}
return {
[fieldMetadata.name]: {
type: enumType,
description: fieldMetadata.description,
},
};
}
}
@@ -1,6 +1,10 @@
import { Injectable, Logger } from '@nestjs/common';
import { isObjectType } from 'graphql';
import {
GraphQLBoolean,
GraphQLInputFieldConfigMap,
isObjectType,
} from 'graphql';
import { FieldMetadataType, RelationType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
@@ -147,6 +151,26 @@ export class RelationFieldMetadataGqlInputTypeGenerator {
};
}
public generateSimpleRelationFieldGroupByInputType(
fieldMetadata: FieldMetadataEntity<
FieldMetadataType.RELATION | FieldMetadataType.MORPH_RELATION
>,
): GraphQLInputFieldConfigMap {
if (fieldMetadata.settings?.relationType === RelationType.ONE_TO_MANY)
return {};
const { joinColumnName } = extractGraphQLRelationFieldNames(fieldMetadata);
const type = this.typeMapperService.applyTypeOptions(GraphQLBoolean, {});
return {
[joinColumnName]: {
type,
description: fieldMetadata.description,
},
};
}
public generateConnectRelationFieldInputType({
fieldMetadata,
typeOptions,
@@ -0,0 +1,79 @@
import { Injectable, Logger } from '@nestjs/common';
import { GraphQLObjectType, isObjectType } from 'graphql';
import GraphQLJSON from 'graphql-type-json';
import { isDefined } from 'twenty-shared/utils';
import { ObjectTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schema-builder/enums/object-type-definition-kind.enum';
import { TypeMapperService } from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service';
import { GqlTypesStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/gql-types.storage';
import { GraphQLOutputTypeFieldConfigMap } from 'src/engine/api/graphql/workspace-schema-builder/types/graphql-field-config-map.types';
import { computeObjectMetadataObjectTypeKey } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-stored-gql-type-key-utils/compute-object-metadata-object-type-key.util';
import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { pascalCase } from 'src/utils/pascal-case';
@Injectable()
export class GroupByConnectionGqlObjectTypeGenerator {
private readonly logger = new Logger(
GroupByConnectionGqlObjectTypeGenerator.name,
);
constructor(
private readonly typeMapperService: TypeMapperService,
private readonly gqlTypesStorage: GqlTypesStorage,
) {}
public buildAndStore(objectMetadata: ObjectMetadataEntity) {
const kind = ObjectTypeDefinitionKind.GroupByConnection;
const key = computeObjectMetadataObjectTypeKey(
objectMetadata.nameSingular,
kind,
);
this.gqlTypesStorage.addGqlType(
key,
new GraphQLObjectType({
name: `${pascalCase(objectMetadata.nameSingular)}${kind.toString()}`,
description: objectMetadata.description,
fields: () => this.generateFields(objectMetadata),
}),
);
}
private generateFields(
objectMetadata: ObjectMetadataEntity,
): GraphQLOutputTypeFieldConfigMap {
const fields: GraphQLOutputTypeFieldConfigMap = {};
const connection = this.gqlTypesStorage.getGqlTypeByKey(
computeObjectMetadataObjectTypeKey(
objectMetadata.nameSingular,
ObjectTypeDefinitionKind.Connection,
),
);
if (!isDefined(connection) || !isObjectType(connection)) {
this.logger.error(
`Connection type for ${objectMetadata.nameSingular} was not found.`,
{
objectMetadata,
},
);
throw new Error(
`Connection type for ${objectMetadata.nameSingular} was not found.`,
);
}
Object.assign(fields, connection.toConfig().fields);
fields.groupByDimensionValues = {
type: this.typeMapperService.applyTypeOptions(GraphQLJSON, {
nullable: false,
}),
};
return fields;
}
}
@@ -100,19 +100,20 @@ export class RootTypeGenerator {
);
}
const allowedMethodNames = [
const isMethodReturningArrayObjectType = [
'updateMany',
'deleteMany',
'createMany',
'findDuplicates',
'restoreMany',
'destroyMany',
'groupBy',
];
const outputType = this.typeMapperService.applyTypeOptions(
objectType,
{
isArray: allowedMethodNames.includes(methodName),
isArray: isMethodReturningArrayObjectType.includes(methodName),
},
);
@@ -135,6 +136,8 @@ export class RootTypeGenerator {
case 'findMany':
case 'findDuplicates':
return ObjectTypeDefinitionKind.Connection;
case 'groupBy':
return ObjectTypeDefinitionKind.GroupByConnection;
default:
return ObjectTypeDefinitionKind.Plain;
}
@@ -6,6 +6,8 @@ import { CompositeFieldMetadataCreateGqlInputTypeGenerator } from 'src/engine/ap
import { ObjectMetadataCreateGqlInputTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/create-input/object-metadata-create-gql-input-type.generator';
import { CompositeFieldMetadataFilterGqlInputTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/filter-input/composite-field-metadata-filter-gql-input-types.generator';
import { ObjectMetadataFilterGqlInputTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/filter-input/object-metadata-filter-gql-input-type.generator';
import { CompositeFieldMetadataGroupByGqlInputTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/group-by-input/composite-field-metadata-group-by-gql-input-type.generator';
import { ObjectMetadataGroupByGqlInputTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/group-by-input/object-metadata-group-by-gql-input-type.generator';
import { ObjectMetadataGqlInputTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/object-metadata-gql-input-type.generator';
import { CompositeFieldMetadataOrderByGqlInputTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/order-by-input/composite-field-metadata-order-by-gql-input-type.generator';
import { ObjectMetadataOrderByGqlInputTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/input-types/order-by-input/object-metadata-order-by-gql-input-type.generator';
@@ -17,6 +19,7 @@ import { AggregationObjectTypeGenerator } from 'src/engine/api/graphql/workspace
import { CompositeFieldMetadataGqlObjectTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/composite-field-metadata-gql-object-type.generator';
import { ConnectionGqlObjectTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/connection-gql-object-type.generator';
import { EdgeGqlObjectTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/edge-gql-object-type.generator';
import { GroupByConnectionGqlObjectTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/group-by-connection-gql-object-type.generator';
import { ObjectMetadataGqlObjectTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/object-metadata-gql-object-type.generator';
import { ObjectMetadataWithRelationsGqlObjectTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/object-metadata-with-relations-gql-object-type.generator';
import { RelationFieldMetadataGqlObjectTypeGenerator } from 'src/engine/api/graphql/workspace-schema-builder/graphql-type-generators/object-types/relation-field-metadata-gql-object-type.generator';
@@ -33,6 +36,7 @@ export const workspaceSchemaBuilderTypeGenerators = [
CompositeFieldMetadataUpdateGqlInputTypeGenerator,
CompositeFieldMetadataFilterGqlInputTypeGenerator,
CompositeFieldMetadataOrderByGqlInputTypeGenerator,
CompositeFieldMetadataGroupByGqlInputTypeGenerator,
RelationFieldMetadataGqlInputTypeGenerator,
RelationFieldMetadataGqlObjectTypeGenerator,
ObjectMetadataGqlInputTypeGenerator,
@@ -40,11 +44,13 @@ export const workspaceSchemaBuilderTypeGenerators = [
ObjectMetadataUpdateGqlInputTypeGenerator,
ObjectMetadataFilterGqlInputTypeGenerator,
ObjectMetadataOrderByGqlInputTypeGenerator,
ObjectMetadataGroupByGqlInputTypeGenerator,
CompositeFieldMetadataGqlObjectTypeGenerator,
ObjectMetadataGqlObjectTypeGenerator,
RelationConnectGqlInputTypeGenerator,
ConnectionGqlObjectTypeGenerator,
EdgeGqlObjectTypeGenerator,
GroupByConnectionGqlObjectTypeGenerator,
ObjectMetadataWithRelationsGqlObjectTypeGenerator,
AggregationObjectTypeGenerator,
ArgsTypeGenerator,
@@ -161,6 +161,27 @@ export const getResolverArgs = (
isNullable: true,
},
};
case 'groupBy':
return {
groupBy: {
kind: GqlInputTypeDefinitionKind.GroupBy,
isNullable: false,
isArray: true,
},
filter: {
kind: GqlInputTypeDefinitionKind.Filter,
isNullable: true,
},
orderBy: {
kind: GqlInputTypeDefinitionKind.OrderBy,
isNullable: true,
isArray: true,
},
viewId: {
type: UUIDScalarType,
isNullable: true,
},
};
default:
throw new Error(`Unknown resolver type: ${type}`);
}