From 22a200171ef90e4acd5400db979de64a1df7d465 Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Mon, 1 Sep 2025 15:24:30 +0200 Subject: [PATCH] Fix view performances (#14209) --- .../database/typeorm/core/core.datasource.ts | 16 ++++++- .../common/1756732238898-addViewIndexes.ts | 33 +++++++++++++ .../engine/core-modules/view/dtos/view.dto.ts | 20 ++++++++ .../view/entities/view-field.entity.ts | 3 ++ .../view/entities/view-filter-group.entity.ts | 3 ++ .../view/entities/view-filter.entity.ts | 3 ++ .../view/entities/view-group.entity.ts | 3 ++ .../view/entities/view-sort.entity.ts | 3 ++ .../view/resolvers/view.resolver.ts | 47 +------------------ .../engine/dataloaders/dataloader.service.ts | 2 +- 10 files changed, 84 insertions(+), 49 deletions(-) create mode 100644 packages/twenty-server/src/database/typeorm/core/migrations/common/1756732238898-addViewIndexes.ts diff --git a/packages/twenty-server/src/database/typeorm/core/core.datasource.ts b/packages/twenty-server/src/database/typeorm/core/core.datasource.ts index d90200b96f..d2cc311a8f 100644 --- a/packages/twenty-server/src/database/typeorm/core/core.datasource.ts +++ b/packages/twenty-server/src/database/typeorm/core/core.datasource.ts @@ -1,18 +1,30 @@ import { type TypeOrmModuleOptions } from '@nestjs/typeorm'; import { config } from 'dotenv'; -import { DataSource, type DataSourceOptions } from 'typeorm'; +import { DataSource, type DataSourceOptions, type LogLevel } from 'typeorm'; config({ path: process.env.NODE_ENV === 'test' ? '.env.test' : '.env', override: true, }); +const getLoggingConfig = (): LogLevel[] => { + if (process.env.NODE_ENV === 'development') { + return ['query', 'error']; + } + + if (process.env.NODE_ENV === 'test') { + return []; + } + + return ['error']; +}; + const isJest = process.argv.some((arg) => arg.includes('jest')); export const typeORMCoreModuleOptions: TypeOrmModuleOptions = { url: process.env.PG_DATABASE_URL, type: 'postgres', - logging: ['error'], + logging: getLoggingConfig(), schema: 'core', entities: process.env.IS_BILLING_ENABLED === 'true' diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/common/1756732238898-addViewIndexes.ts b/packages/twenty-server/src/database/typeorm/core/migrations/common/1756732238898-addViewIndexes.ts new file mode 100644 index 0000000000..846e7983e4 --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/core/migrations/common/1756732238898-addViewIndexes.ts @@ -0,0 +1,33 @@ +import { type MigrationInterface, type QueryRunner } from 'typeorm'; + +export class AddViewIndexes1756732238898 implements MigrationInterface { + name = 'AddViewIndexes1756732238898'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE INDEX "IDX_VIEW_FILTER_VIEW_ID" ON "core"."viewFilter" ("viewId") WHERE "deletedAt" IS NULL`, + ); + await queryRunner.query( + `CREATE INDEX "IDX_VIEW_FILTER_GROUP_VIEW_ID" ON "core"."viewFilterGroup" ("viewId") WHERE "deletedAt" IS NULL`, + ); + await queryRunner.query( + `CREATE INDEX "IDX_VIEW_GROUP_VIEW_ID" ON "core"."viewGroup" ("viewId") WHERE "deletedAt" IS NULL`, + ); + await queryRunner.query( + `CREATE INDEX "IDX_VIEW_SORT_VIEW_ID" ON "core"."viewSort" ("viewId") WHERE "deletedAt" IS NULL`, + ); + await queryRunner.query( + `CREATE INDEX "IDX_VIEW_FIELD_VIEW_ID" ON "core"."viewField" ("viewId") WHERE "deletedAt" IS NULL`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "core"."IDX_VIEW_FIELD_VIEW_ID"`); + await queryRunner.query(`DROP INDEX "core"."IDX_VIEW_SORT_VIEW_ID"`); + await queryRunner.query(`DROP INDEX "core"."IDX_VIEW_GROUP_VIEW_ID"`); + await queryRunner.query( + `DROP INDEX "core"."IDX_VIEW_FILTER_GROUP_VIEW_ID"`, + ); + await queryRunner.query(`DROP INDEX "core"."IDX_VIEW_FILTER_VIEW_ID"`); + } +} diff --git a/packages/twenty-server/src/engine/core-modules/view/dtos/view.dto.ts b/packages/twenty-server/src/engine/core-modules/view/dtos/view.dto.ts index cb5154022c..d40a47930b 100644 --- a/packages/twenty-server/src/engine/core-modules/view/dtos/view.dto.ts +++ b/packages/twenty-server/src/engine/core-modules/view/dtos/view.dto.ts @@ -4,6 +4,11 @@ import { IDField } from '@ptc-org/nestjs-query-graphql'; import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant'; import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; +import { ViewFieldDTO } from 'src/engine/core-modules/view/dtos/view-field.dto'; +import { ViewFilterGroupDTO } from 'src/engine/core-modules/view/dtos/view-filter-group.dto'; +import { ViewFilterDTO } from 'src/engine/core-modules/view/dtos/view-filter.dto'; +import { ViewGroupDTO } from 'src/engine/core-modules/view/dtos/view-group.dto'; +import { ViewSortDTO } from 'src/engine/core-modules/view/dtos/view-sort.dto'; import { ViewKey } from 'src/engine/core-modules/view/enums/view-key.enum'; import { ViewOpenRecordIn } from 'src/engine/core-modules/view/enums/view-open-record-in'; import { ViewType } from 'src/engine/core-modules/view/enums/view-type.enum'; @@ -67,4 +72,19 @@ export class ViewDTO { @Field(() => Date, { nullable: true }) deletedAt?: Date | null; + + @Field(() => [ViewFieldDTO]) + viewFields: ViewFieldDTO[]; + + @Field(() => [ViewFilterDTO]) + viewFilters: ViewFilterDTO[]; + + @Field(() => [ViewFilterGroupDTO]) + viewFilterGroups: ViewFilterGroupDTO[]; + + @Field(() => [ViewSortDTO]) + viewSorts: ViewSortDTO[]; + + @Field(() => [ViewGroupDTO]) + viewGroups: ViewGroupDTO[]; } diff --git a/packages/twenty-server/src/engine/core-modules/view/entities/view-field.entity.ts b/packages/twenty-server/src/engine/core-modules/view/entities/view-field.entity.ts index b34b3db01e..91a6f26dc0 100644 --- a/packages/twenty-server/src/engine/core-modules/view/entities/view-field.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/view/entities/view-field.entity.ts @@ -20,6 +20,9 @@ import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/ @Entity({ name: 'viewField', schema: 'core' }) @Index('IDX_VIEW_FIELD_WORKSPACE_ID_VIEW_ID', ['workspaceId', 'viewId']) +@Index('IDX_VIEW_FIELD_VIEW_ID', ['viewId'], { + where: '"deletedAt" IS NULL', +}) @Index( 'IDX_VIEW_FIELD_FIELD_METADATA_ID_VIEW_ID_UNIQUE', ['fieldMetadataId', 'viewId'], diff --git a/packages/twenty-server/src/engine/core-modules/view/entities/view-filter-group.entity.ts b/packages/twenty-server/src/engine/core-modules/view/entities/view-filter-group.entity.ts index c3f1101ae5..ced23f5e75 100644 --- a/packages/twenty-server/src/engine/core-modules/view/entities/view-filter-group.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/view/entities/view-filter-group.entity.ts @@ -21,6 +21,9 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; @Entity({ name: 'viewFilterGroup', schema: 'core' }) @Index('IDX_VIEW_FILTER_GROUP_WORKSPACE_ID_VIEW_ID', ['workspaceId', 'viewId']) +@Index('IDX_VIEW_FILTER_GROUP_VIEW_ID', ['viewId'], { + where: '"deletedAt" IS NULL', +}) export class ViewFilterGroupEntity extends SyncableEntity { @PrimaryGeneratedColumn('uuid') id: string; diff --git a/packages/twenty-server/src/engine/core-modules/view/entities/view-filter.entity.ts b/packages/twenty-server/src/engine/core-modules/view/entities/view-filter.entity.ts index f312d7ffbe..ff2bd329fe 100644 --- a/packages/twenty-server/src/engine/core-modules/view/entities/view-filter.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/view/entities/view-filter.entity.ts @@ -22,6 +22,9 @@ import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/ @Entity({ name: 'viewFilter', schema: 'core' }) @Index('IDX_VIEW_FILTER_WORKSPACE_ID_VIEW_ID', ['workspaceId', 'viewId']) +@Index('IDX_VIEW_FILTER_VIEW_ID', ['viewId'], { + where: '"deletedAt" IS NULL', +}) @Index('IDX_VIEW_FILTER_FIELD_METADATA_ID', ['fieldMetadataId']) export class ViewFilterEntity extends SyncableEntity { @PrimaryGeneratedColumn('uuid') diff --git a/packages/twenty-server/src/engine/core-modules/view/entities/view-group.entity.ts b/packages/twenty-server/src/engine/core-modules/view/entities/view-group.entity.ts index 6432519866..502b8b031f 100644 --- a/packages/twenty-server/src/engine/core-modules/view/entities/view-group.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/view/entities/view-group.entity.ts @@ -19,6 +19,9 @@ import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/ @Entity({ name: 'viewGroup', schema: 'core' }) @Index('IDX_VIEW_GROUP_WORKSPACE_ID_VIEW_ID', ['workspaceId', 'viewId']) +@Index('IDX_VIEW_GROUP_VIEW_ID', ['viewId'], { + where: '"deletedAt" IS NULL', +}) export class ViewGroupEntity extends SyncableEntity { @PrimaryGeneratedColumn('uuid') id: string; diff --git a/packages/twenty-server/src/engine/core-modules/view/entities/view-sort.entity.ts b/packages/twenty-server/src/engine/core-modules/view/entities/view-sort.entity.ts index 71a4e788af..2f3bfb2462 100644 --- a/packages/twenty-server/src/engine/core-modules/view/entities/view-sort.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/view/entities/view-sort.entity.ts @@ -20,6 +20,9 @@ import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/ @Entity({ name: 'viewSort', schema: 'core' }) @Index('IDX_VIEW_SORT_WORKSPACE_ID_VIEW_ID', ['workspaceId', 'viewId']) +@Index('IDX_VIEW_SORT_VIEW_ID', ['viewId'], { + where: '"deletedAt" IS NULL', +}) @Index( 'IDX_VIEW_SORT_FIELD_METADATA_ID_VIEW_ID_UNIQUE', ['fieldMetadataId', 'viewId'], diff --git a/packages/twenty-server/src/engine/core-modules/view/resolvers/view.resolver.ts b/packages/twenty-server/src/engine/core-modules/view/resolvers/view.resolver.ts index 9abfea2105..e6a3ee63ff 100644 --- a/packages/twenty-server/src/engine/core-modules/view/resolvers/view.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/view/resolvers/view.resolver.ts @@ -13,15 +13,9 @@ import { isDefined } from 'twenty-shared/utils'; import { I18nService } from 'src/engine/core-modules/i18n/i18n.service'; import { type I18nContext } from 'src/engine/core-modules/i18n/types/i18n-context.type'; -import { type IDataloaders } from 'src/engine/dataloaders/dataloader.interface'; import { generateMessageId } from 'src/engine/core-modules/i18n/utils/generateMessageId'; import { CreateViewInput } from 'src/engine/core-modules/view/dtos/inputs/create-view.input'; import { UpdateViewInput } from 'src/engine/core-modules/view/dtos/inputs/update-view.input'; -import { ViewFieldDTO } from 'src/engine/core-modules/view/dtos/view-field.dto'; -import { ViewFilterGroupDTO } from 'src/engine/core-modules/view/dtos/view-filter-group.dto'; -import { ViewFilterDTO } from 'src/engine/core-modules/view/dtos/view-filter.dto'; -import { ViewGroupDTO } from 'src/engine/core-modules/view/dtos/view-group.dto'; -import { ViewSortDTO } from 'src/engine/core-modules/view/dtos/view-sort.dto'; import { ViewDTO } from 'src/engine/core-modules/view/dtos/view.dto'; import { ViewFieldService } from 'src/engine/core-modules/view/services/view-field.service'; import { ViewFilterGroupService } from 'src/engine/core-modules/view/services/view-filter-group.service'; @@ -31,6 +25,7 @@ import { ViewSortService } from 'src/engine/core-modules/view/services/view-sort import { ViewService } from 'src/engine/core-modules/view/services/view.service'; import { ViewGraphqlApiExceptionFilter } from 'src/engine/core-modules/view/utils/view-graphql-api-exception.filter'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { type IDataloaders } from 'src/engine/dataloaders/dataloader.interface'; import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; import { resolveObjectMetadataStandardOverride } from 'src/engine/metadata-modules/object-metadata/utils/resolve-object-metadata-standard-override.util'; @@ -168,44 +163,4 @@ export class ViewResolver { return isDefined(deletedView); } - - @ResolveField(() => [ViewFieldDTO]) - async viewFields( - @Parent() view: ViewDTO, - @AuthWorkspace() workspace: Workspace, - ) { - return this.viewFieldService.findByViewId(workspace.id, view.id); - } - - @ResolveField(() => [ViewFilterDTO]) - async viewFilters( - @Parent() view: ViewDTO, - @AuthWorkspace() workspace: Workspace, - ) { - return this.viewFilterService.findByViewId(workspace.id, view.id); - } - - @ResolveField(() => [ViewFilterGroupDTO]) - async viewFilterGroups( - @Parent() view: ViewDTO, - @AuthWorkspace() workspace: Workspace, - ) { - return this.viewFilterGroupService.findByViewId(workspace.id, view.id); - } - - @ResolveField(() => [ViewSortDTO]) - async viewSorts( - @Parent() view: ViewDTO, - @AuthWorkspace() workspace: Workspace, - ) { - return this.viewSortService.findByViewId(workspace.id, view.id); - } - - @ResolveField(() => [ViewGroupDTO]) - async viewGroups( - @Parent() view: ViewDTO, - @AuthWorkspace() workspace: Workspace, - ) { - return this.viewGroupService.findByViewId(workspace.id, view.id); - } } diff --git a/packages/twenty-server/src/engine/dataloaders/dataloader.service.ts b/packages/twenty-server/src/engine/dataloaders/dataloader.service.ts index def540b820..e5a79b62a5 100644 --- a/packages/twenty-server/src/engine/dataloaders/dataloader.service.ts +++ b/packages/twenty-server/src/engine/dataloaders/dataloader.service.ts @@ -8,7 +8,6 @@ import { type IndexMetadataInterface } from 'src/engine/metadata-modules/index-m import { I18nService } from 'src/engine/core-modules/i18n/i18n.service'; import { type IDataloaders } from 'src/engine/dataloaders/dataloader.interface'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import { filterMorphRelationDuplicateFieldsDTO } from 'src/engine/dataloaders/utils/filter-morph-relation-duplicate-fields.util'; import { type FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto'; import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; @@ -19,6 +18,7 @@ import { resolveFieldMetadataStandardOverride } from 'src/engine/metadata-module import { type IndexFieldMetadataDTO } from 'src/engine/metadata-modules/index-metadata/dtos/index-field-metadata.dto'; import { type IndexMetadataDTO } from 'src/engine/metadata-modules/index-metadata/dtos/index-metadata.dto'; import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service'; export type RelationMetadataLoaderPayload = {