Add dataloader and read from cache for view entities (#18594)

## Context
Improve view resolution using cache and dataloader

## Performance Comparison

|Run|Main (no DataLoaders/cache)|Feature Branch (DataLoaders +
cache)|Speedup|
|---|---|---|---|
|1 (cold)|418ms|95ms|~4.4x faster|
|2|42ms|19ms|~2.2x faster|
|3|37ms|19ms|~1.9x faster|
|4|39ms|12ms|~3.2x faster|
|5|33ms|13ms|~2.5x faster|

The biggest improvement is to use dataloaders for the multiple relations
associated with views. Cache is a bit less significant since there are
other cache mechanism such as PostgreSQL buffer cache but it will
probably be more meaningful with bigger workspaces
This commit is contained in:
Weiko
2026-03-12 19:05:30 +01:00
committed by GitHub
parent a3c392ce8b
commit 1cb4c98cb3
12 changed files with 1121 additions and 737 deletions
@@ -60,12 +60,15 @@ export class ViewController {
@Query('objectMetadataId') objectMetadataId?: string,
): Promise<ViewDTO[]> {
const views = objectMetadataId
? await this.viewService.findByObjectMetadataId(
? await this.viewService.findByObjectMetadataIdWithRelations(
workspace.id,
objectMetadataId,
userWorkspaceId,
)
: await this.viewService.findByWorkspaceId(workspace.id, userWorkspaceId);
: await this.viewService.findByWorkspaceIdWithRelations(
workspace.id,
userWorkspaceId,
);
return this.processViewsWithTemplates(views, workspace.id, locale);
}
@@ -77,7 +80,7 @@ export class ViewController {
@RequestLocale() locale: keyof typeof APP_LOCALES | undefined,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<ViewDTO> {
const view = await this.viewService.findById(id, workspace.id);
const view = await this.viewService.findByIdWithRelations(id, workspace.id);
if (!isDefined(view)) {
throw new ViewException(
@@ -8,7 +8,6 @@ import {
ResolveField,
} from '@nestjs/graphql';
import { isArray } from '@sniptt/guards';
import { ViewType, ViewVisibility } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
@@ -25,23 +24,17 @@ 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';
import { ViewFieldGroupDTO } from 'src/engine/metadata-modules/view-field-group/dtos/view-field-group.dto';
import { ViewFieldDTO } from 'src/engine/metadata-modules/view-field/dtos/view-field.dto';
import { ViewFieldService } from 'src/engine/metadata-modules/view-field/services/view-field.service';
import { ViewFilterGroupDTO } from 'src/engine/metadata-modules/view-filter-group/dtos/view-filter-group.dto';
import { ViewFilterGroupService } from 'src/engine/metadata-modules/view-filter-group/services/view-filter-group.service';
import { ViewFilterDTO } from 'src/engine/metadata-modules/view-filter/dtos/view-filter.dto';
import { ViewFilterService } from 'src/engine/metadata-modules/view-filter/services/view-filter.service';
import { ViewGroupDTO } from 'src/engine/metadata-modules/view-group/dtos/view-group.dto';
import { ViewGroupService } from 'src/engine/metadata-modules/view-group/services/view-group.service';
import { CreateViewPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/create-view-permission.guard';
import { DeleteViewPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/delete-view-permission.guard';
import { DestroyViewPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/destroy-view-permission.guard';
import { UpdateViewPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/update-view-permission.guard';
import { ViewSortDTO } from 'src/engine/metadata-modules/view-sort/dtos/view-sort.dto';
import { ViewSortService } from 'src/engine/metadata-modules/view-sort/services/view-sort.service';
import { CreateViewInput } from 'src/engine/metadata-modules/view/dtos/inputs/create-view.input';
import { UpdateViewInput } from 'src/engine/metadata-modules/view/dtos/inputs/update-view.input';
import { ViewDTO } from 'src/engine/metadata-modules/view/dtos/view.dto';
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
import { ViewService } from 'src/engine/metadata-modules/view/services/view.service';
import { ViewGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/view/utils/view-graphql-api-exception.filter';
@@ -51,13 +44,7 @@ import { ViewGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/view/
export class ViewResolver {
constructor(
private readonly viewService: ViewService,
private readonly viewFilterService: ViewFilterService,
private readonly viewFilterGroupService: ViewFilterGroupService,
private readonly viewSortService: ViewSortService,
private readonly viewGroupService: ViewGroupService,
private readonly i18nService: I18nService,
private readonly viewFieldService: ViewFieldService,
) {}
@ResolveField(() => String)
@@ -114,7 +101,7 @@ export class ViewResolver {
objectMetadataId?: string,
@Args('viewTypes', { type: () => [ViewType], nullable: true })
viewTypes?: ViewType[],
): Promise<ViewEntity[]> {
): Promise<ViewDTO[]> {
if (objectMetadataId) {
return this.viewService.findByObjectMetadataId(
workspace.id,
@@ -143,7 +130,6 @@ export class ViewResolver {
return null;
}
// Do not apply list visibility filtering here: unlisted views are accessible by link
return view;
}
@@ -211,61 +197,61 @@ export class ViewResolver {
@ResolveField(() => [ViewFieldDTO])
async viewFields(
@Parent() view: ViewDTO,
@Context() context: { loaders: IDataloaders },
@AuthWorkspace() workspace: WorkspaceEntity,
) {
if (isArray(view.viewFields)) {
return view.viewFields;
}
return this.viewFieldService.findByViewId(workspace.id, view.id);
return context.loaders.viewFieldsByViewIdLoader.load({
workspaceId: workspace.id,
viewId: view.id,
});
}
@ResolveField(() => [ViewFilterDTO])
async viewFilters(
@Parent() view: ViewDTO,
@Context() context: { loaders: IDataloaders },
@AuthWorkspace() workspace: WorkspaceEntity,
) {
if (isArray(view.viewFilters)) {
return view.viewFilters;
}
return this.viewFilterService.findByViewId(workspace.id, view.id);
return context.loaders.viewFiltersByViewIdLoader.load({
workspaceId: workspace.id,
viewId: view.id,
});
}
@ResolveField(() => [ViewFilterGroupDTO])
async viewFilterGroups(
@Parent() view: ViewDTO,
@Context() context: { loaders: IDataloaders },
@AuthWorkspace() workspace: WorkspaceEntity,
) {
if (isArray(view.viewFilterGroups)) {
return view.viewFilterGroups;
}
return this.viewFilterGroupService.findByViewId(workspace.id, view.id);
return context.loaders.viewFilterGroupsByViewIdLoader.load({
workspaceId: workspace.id,
viewId: view.id,
});
}
@ResolveField(() => [ViewSortDTO])
async viewSorts(
@Parent() view: ViewDTO,
@Context() context: { loaders: IDataloaders },
@AuthWorkspace() workspace: WorkspaceEntity,
) {
if (isArray(view.viewSorts)) {
return view.viewSorts;
}
return this.viewSortService.findByViewId(workspace.id, view.id);
return context.loaders.viewSortsByViewIdLoader.load({
workspaceId: workspace.id,
viewId: view.id,
});
}
@ResolveField(() => [ViewGroupDTO])
async viewGroups(
@Parent() view: ViewDTO,
@Context() context: { loaders: IDataloaders },
@AuthWorkspace() workspace: WorkspaceEntity,
) {
if (isArray(view.viewGroups)) {
return view.viewGroups;
}
return this.viewGroupService.findByViewId(workspace.id, view.id);
return context.loaders.viewGroupsByViewIdLoader.load({
workspaceId: workspace.id,
viewId: view.id,
});
}
@ResolveField(() => [ViewFieldGroupDTO])
@@ -274,10 +260,6 @@ export class ViewResolver {
@Context() context: { loaders: IDataloaders },
@AuthWorkspace() workspace: WorkspaceEntity,
) {
if (isArray(view.viewFieldGroups)) {
return view.viewFieldGroups;
}
return context.loaders.viewFieldGroupsByViewIdLoader.load({
workspaceId: workspace.id,
viewId: view.id,
@@ -65,7 +65,7 @@ describe('ViewQueryParamsService', () => {
{
provide: ViewService,
useValue: {
findById: jest.fn(),
findByIdWithRelations: jest.fn(),
},
},
{
@@ -98,7 +98,7 @@ describe('ViewQueryParamsService', () => {
describe('resolveViewToQueryParams', () => {
it('should throw error when view is not found', async () => {
viewService.findById.mockResolvedValue(null);
viewService.findByIdWithRelations.mockResolvedValue(null);
await expect(
viewQueryParamsService.resolveViewToQueryParams(
@@ -120,7 +120,7 @@ describe('ViewQueryParamsService', () => {
viewSorts: [],
};
viewService.findById.mockResolvedValue(mockView as any);
viewService.findByIdWithRelations.mockResolvedValue(mockView as any);
flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps.mockResolvedValue(
{
flatObjectMetadataMaps: mockFlatObjectMetadataMaps,
@@ -168,7 +168,7 @@ describe('ViewQueryParamsService', () => {
viewSorts: [],
};
viewService.findById.mockResolvedValue(mockView as any);
viewService.findByIdWithRelations.mockResolvedValue(mockView as any);
flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps.mockResolvedValue(
{
flatObjectMetadataMaps: mockFlatObjectMetadataMaps,
@@ -204,7 +204,7 @@ describe('ViewQueryParamsService', () => {
],
};
viewService.findById.mockResolvedValue(mockView as any);
viewService.findByIdWithRelations.mockResolvedValue(mockView as any);
flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps.mockResolvedValue(
{
flatObjectMetadataMaps: mockFlatObjectMetadataMaps,
@@ -251,7 +251,7 @@ describe('ViewQueryParamsService', () => {
viewSorts: [],
};
viewService.findById.mockResolvedValue(mockView as any);
viewService.findByIdWithRelations.mockResolvedValue(mockView as any);
flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps.mockResolvedValue(
{
flatObjectMetadataMaps: mockFlatObjectMetadataMaps,
@@ -46,7 +46,10 @@ export class ViewQueryParamsService {
workspaceId: string,
currentWorkspaceMemberId?: string,
): Promise<ViewQueryParams> {
const view = await this.viewService.findById(viewId, workspaceId);
const view = await this.viewService.findByIdWithRelations(
viewId,
workspaceId,
);
if (!view) {
throw new Error(`View with id ${viewId} not found`);
@@ -4,18 +4,27 @@ import { InjectRepository } from '@nestjs/typeorm';
import { APP_LOCALES, SOURCE_LOCALE } from 'twenty-shared/translations';
import { ViewType, ViewVisibility } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { In, IsNull, Repository } from 'typeorm';
import { Repository } from 'typeorm';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
import { generateMessageId } from 'src/engine/core-modules/i18n/utils/generateMessageId';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { findFlatEntityByUniversalIdentifierOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier-or-throw.util';
import { findManyFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps.util';
import { type FlatView } from 'src/engine/metadata-modules/flat-view/types/flat-view.type';
import { fromCreateViewInputToFlatViewToCreate } from 'src/engine/metadata-modules/flat-view/utils/from-create-view-input-to-flat-view-to-create.util';
import { fromDeleteViewInputToFlatViewOrThrow } from 'src/engine/metadata-modules/flat-view/utils/from-delete-view-input-to-flat-view-or-throw.util';
import { fromDestroyViewInputToFlatViewOrThrow } from 'src/engine/metadata-modules/flat-view/utils/from-destroy-view-input-to-flat-view-or-throw.util';
import { fromUpdateViewInputToFlatViewToUpdateOrThrow } from 'src/engine/metadata-modules/flat-view/utils/from-update-view-input-to-flat-view-to-update-or-throw.util';
import { fromFlatViewFieldGroupToViewFieldGroupDto } from 'src/engine/metadata-modules/view-field-group/utils/from-flat-view-field-group-to-view-field-group-dto.util';
import { fromFlatViewFieldToViewFieldDto } from 'src/engine/metadata-modules/view-field/utils/from-flat-view-field-to-view-field-dto.util';
import { fromFlatViewFilterGroupToViewFilterGroupDto } from 'src/engine/metadata-modules/view-filter-group/utils/from-flat-view-filter-group-to-view-filter-group-dto.util';
import { fromFlatViewFilterToViewFilterDto } from 'src/engine/metadata-modules/view-filter/utils/from-flat-view-filter-to-view-filter-dto.util';
import { fromFlatViewGroupToViewGroupDto } from 'src/engine/metadata-modules/view-group/utils/from-flat-view-group-to-view-group-dto.util';
import { fromFlatViewSortToViewSortDto } from 'src/engine/metadata-modules/view-sort/utils/from-flat-view-sort-to-view-sort-dto.util';
import { CreateViewInput } from 'src/engine/metadata-modules/view/dtos/inputs/create-view.input';
import { DeleteViewInput } from 'src/engine/metadata-modules/view/dtos/inputs/delete-view.input';
import { DestroyViewInput } from 'src/engine/metadata-modules/view/dtos/inputs/destroy-view.input';
@@ -372,42 +381,73 @@ export class ViewService {
return viewName;
}
private isViewVisibleToUser(
view: {
visibility: ViewVisibility;
createdByUserWorkspaceId: string | null;
},
userWorkspaceId?: string,
): boolean {
if (view.visibility === ViewVisibility.WORKSPACE) {
return true;
}
return (
view.visibility === ViewVisibility.UNLISTED &&
isDefined(userWorkspaceId) &&
view.createdByUserWorkspaceId === userWorkspaceId
);
}
private async getFilteredFlatViews({
workspaceId,
objectMetadataId,
userWorkspaceId,
viewTypes,
}: {
workspaceId: string;
objectMetadataId?: string;
userWorkspaceId?: string;
viewTypes?: ViewType[];
}): Promise<FlatView[]> {
const { flatViewMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatViewMaps'],
},
);
return Object.values(flatViewMaps.byUniversalIdentifier)
.filter(isDefined)
.filter((flatView) => flatView.workspaceId === workspaceId)
.filter(
(flatView) =>
!objectMetadataId || flatView.objectMetadataId === objectMetadataId,
)
.filter((flatView) => flatView.deletedAt === null)
.filter(
(flatView) =>
!viewTypes ||
viewTypes.length === 0 ||
viewTypes.includes(flatView.type),
)
.filter((flatView) => this.isViewVisibleToUser(flatView, userWorkspaceId))
.sort((a, b) => a.position - b.position);
}
async findByWorkspaceId(
workspaceId: string,
userWorkspaceId?: string,
viewTypes?: ViewType[],
): Promise<ViewEntity[]> {
const views = await this.viewRepository.find({
where: {
workspaceId,
deletedAt: IsNull(),
...(viewTypes && viewTypes.length > 0 && { type: In(viewTypes) }),
},
order: { position: 'ASC' },
relations: [
'workspace',
'viewFields',
'viewFilters',
'viewSorts',
'viewGroups',
'viewFilterGroups',
],
): Promise<ViewDTO[]> {
const flatViews = await this.getFilteredFlatViews({
workspaceId,
userWorkspaceId,
viewTypes,
});
return views.filter((view) => {
if (view.visibility === ViewVisibility.WORKSPACE) {
return true;
}
if (
view.visibility === ViewVisibility.UNLISTED &&
isDefined(userWorkspaceId) &&
view.createdByUserWorkspaceId === userWorkspaceId
) {
return true;
}
return false;
});
return flatViews.map(fromFlatViewToViewDto);
}
async findByObjectMetadataId(
@@ -415,60 +455,170 @@ export class ViewService {
objectMetadataId: string,
userWorkspaceId?: string,
viewTypes?: ViewType[],
): Promise<ViewEntity[]> {
const views = await this.viewRepository.find({
where: {
workspaceId,
objectMetadataId,
deletedAt: IsNull(),
...(viewTypes && viewTypes.length > 0 && { type: In(viewTypes) }),
},
order: { position: 'ASC' },
relations: [
'workspace',
'viewFields',
'viewFilters',
'viewSorts',
'viewGroups',
'viewFilterGroups',
],
): Promise<ViewDTO[]> {
const flatViews = await this.getFilteredFlatViews({
workspaceId,
objectMetadataId,
userWorkspaceId,
viewTypes,
});
// Apply visibility filtering
return views.filter((view) => {
if (view.visibility === ViewVisibility.WORKSPACE) {
return true;
}
if (
view.visibility === ViewVisibility.UNLISTED &&
isDefined(userWorkspaceId) &&
view.createdByUserWorkspaceId === userWorkspaceId
) {
return true;
}
return flatViews.map(fromFlatViewToViewDto);
}
return false;
async findById(id: string, workspaceId: string): Promise<ViewDTO | null> {
const { flatViewMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatViewMaps'],
},
);
const flatView = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: id,
flatEntityMaps: flatViewMaps,
});
if (!isDefined(flatView) || flatView.deletedAt !== null) {
return null;
}
return fromFlatViewToViewDto(flatView);
}
async findManyWithRelationsFromCache(
flatViews: FlatView[],
workspaceId: string,
): Promise<ViewDTO[]> {
const {
flatViewFieldMaps,
flatViewFieldGroupMaps,
flatViewFilterMaps,
flatViewFilterGroupMaps,
flatViewSortMaps,
flatViewGroupMaps,
} =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: [
'flatViewFieldMaps',
'flatViewFieldGroupMaps',
'flatViewFilterMaps',
'flatViewFilterGroupMaps',
'flatViewSortMaps',
'flatViewGroupMaps',
],
},
);
return flatViews.map((flatView) => {
const viewDto = fromFlatViewToViewDto(flatView);
viewDto.viewFields = findManyFlatEntityByIdInFlatEntityMaps({
flatEntityIds: flatView.viewFieldIds,
flatEntityMaps: flatViewFieldMaps,
})
.filter((flatEntity) => flatEntity.deletedAt === null)
.map(fromFlatViewFieldToViewFieldDto);
viewDto.viewFieldGroups = findManyFlatEntityByIdInFlatEntityMaps({
flatEntityIds: flatView.viewFieldGroupIds,
flatEntityMaps: flatViewFieldGroupMaps,
})
.filter((flatEntity) => flatEntity.deletedAt === null)
.map(fromFlatViewFieldGroupToViewFieldGroupDto);
viewDto.viewFilters = findManyFlatEntityByIdInFlatEntityMaps({
flatEntityIds: flatView.viewFilterIds,
flatEntityMaps: flatViewFilterMaps,
})
.filter((flatEntity) => flatEntity.deletedAt === null)
.map(fromFlatViewFilterToViewFilterDto);
viewDto.viewFilterGroups = findManyFlatEntityByIdInFlatEntityMaps({
flatEntityIds: flatView.viewFilterGroupIds,
flatEntityMaps: flatViewFilterGroupMaps,
})
.filter((flatEntity) => flatEntity.deletedAt === null)
.map(fromFlatViewFilterGroupToViewFilterGroupDto);
viewDto.viewSorts = findManyFlatEntityByIdInFlatEntityMaps({
flatEntityIds: flatView.viewSortIds,
flatEntityMaps: flatViewSortMaps,
})
.filter((flatEntity) => flatEntity.deletedAt === null)
.map(fromFlatViewSortToViewSortDto);
viewDto.viewGroups = findManyFlatEntityByIdInFlatEntityMaps({
flatEntityIds: flatView.viewGroupIds,
flatEntityMaps: flatViewGroupMaps,
})
.filter((flatEntity) => flatEntity.deletedAt === null)
.map(fromFlatViewGroupToViewGroupDto);
return viewDto;
});
}
async findById(id: string, workspaceId: string): Promise<ViewEntity | null> {
const view = await this.viewRepository.findOne({
where: {
id,
workspaceId,
deletedAt: IsNull(),
},
relations: [
'workspace',
'viewFields',
'viewFilters',
'viewSorts',
'viewGroups',
'viewFilterGroups',
],
async findByWorkspaceIdWithRelations(
workspaceId: string,
userWorkspaceId?: string,
viewTypes?: ViewType[],
): Promise<ViewDTO[]> {
const flatViews = await this.getFilteredFlatViews({
workspaceId,
userWorkspaceId,
viewTypes,
});
return view || null;
return this.findManyWithRelationsFromCache(flatViews, workspaceId);
}
async findByObjectMetadataIdWithRelations(
workspaceId: string,
objectMetadataId: string,
userWorkspaceId?: string,
viewTypes?: ViewType[],
): Promise<ViewDTO[]> {
const flatViews = await this.getFilteredFlatViews({
workspaceId,
objectMetadataId,
userWorkspaceId,
viewTypes,
});
return this.findManyWithRelationsFromCache(flatViews, workspaceId);
}
async findByIdWithRelations(
id: string,
workspaceId: string,
): Promise<ViewDTO | null> {
const { flatViewMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatViewMaps'],
},
);
const flatView = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: id,
flatEntityMaps: flatViewMaps,
});
if (!isDefined(flatView) || flatView.deletedAt !== null) {
return null;
}
const results = await this.findManyWithRelationsFromCache(
[flatView],
workspaceId,
);
return results[0] ?? null;
}
async findByIdIncludingDeleted(
@@ -8,10 +8,6 @@ import { FlatViewModule } from 'src/engine/metadata-modules/flat-view/flat-view.
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module';
import { ViewFieldGroupModule } from 'src/engine/metadata-modules/view-field-group/view-field-group.module';
import { ViewFieldModule } from 'src/engine/metadata-modules/view-field/view-field.module';
import { ViewFilterGroupModule } from 'src/engine/metadata-modules/view-filter-group/view-filter-group.module';
import { ViewFilterModule } from 'src/engine/metadata-modules/view-filter/view-filter.module';
import { ViewGroupModule } from 'src/engine/metadata-modules/view-group/view-group.module';
import { ViewPermissionsModule } from 'src/engine/metadata-modules/view-permissions/view-permissions.module';
import { ViewSortModule } from 'src/engine/metadata-modules/view-sort/view-sort.module';
import { ViewController } from 'src/engine/metadata-modules/view/controllers/view.controller';
@@ -27,11 +23,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
imports: [
TypeOrmModule.forFeature([ViewEntity]),
ViewPermissionsModule,
ViewFieldModule,
ViewFieldGroupModule,
ViewFilterModule,
ViewFilterGroupModule,
ViewGroupModule,
ViewSortModule,
I18nModule,
ApplicationModule,