[BREAKING_CHANGE/GRAPHQL/OBJECT_METADATA_CREATE_ONE] Remove object/fields/view-fields v1 implementation (#15823)

# Introduction
Remove the v2 feature flag for view-field field-metadata and
object-metadata metadata entities

## Some details
- Disabled nestjs-query for object metadata creation and explicitly
calling it
- removed all v1 integration tests files

## Remarks
Not remove v2 referencing in both filenaming right now will handle that
globally later

## Breaking change
Due to object metadata resolver createOne standardization had to rename
the input from `CreateObjectInput` to `CreateOneObjectInput`
This commit is contained in:
Paul Rastoin
2025-11-17 10:55:19 +01:00
committed by GitHub
parent c1dcda6475
commit a39efeb1ab
75 changed files with 425 additions and 15441 deletions
@@ -13,8 +13,6 @@ import {
import { isDefined } from 'twenty-shared/utils';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
@@ -32,7 +30,6 @@ import {
} from 'src/engine/metadata-modules/view-field/exceptions/view-field.exception';
import { ViewFieldRestApiExceptionFilter } from 'src/engine/metadata-modules/view-field/filters/view-field-rest-api-exception.filter';
import { ViewFieldV2Service } from 'src/engine/metadata-modules/view-field/services/view-field-v2.service';
import { ViewFieldService } from 'src/engine/metadata-modules/view-field/services/view-field.service';
import { CreateViewFieldPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/create-view-field-permission.guard';
import { DeleteViewFieldPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/delete-view-field-permission.guard';
import { UpdateViewFieldPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/update-view-field-permission.guard';
@@ -41,11 +38,7 @@ import { UpdateViewFieldPermissionGuard } from 'src/engine/metadata-modules/view
@UseGuards(WorkspaceAuthGuard)
@UseFilters(ViewFieldRestApiExceptionFilter)
export class ViewFieldController {
constructor(
private readonly viewFieldService: ViewFieldService,
private readonly viewFieldV2Service: ViewFieldV2Service,
private readonly featureFlagService: FeatureFlagService,
) {}
constructor(private readonly viewFieldV2Service: ViewFieldV2Service) {}
@Get()
@UseGuards(NoPermissionGuard)
@@ -54,10 +47,10 @@ export class ViewFieldController {
@Query('viewId') viewId?: string,
): Promise<ViewFieldEntity[]> {
if (viewId) {
return this.viewFieldService.findByViewId(workspace.id, viewId);
return this.viewFieldV2Service.findByViewId(workspace.id, viewId);
}
return this.viewFieldService.findByWorkspaceId(workspace.id);
return this.viewFieldV2Service.findByWorkspaceId(workspace.id);
}
@Get(':id')
@@ -66,7 +59,7 @@ export class ViewFieldController {
@Param('id') id: string,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<ViewFieldEntity> {
const viewField = await this.viewFieldService.findById(id, workspace.id);
const viewField = await this.viewFieldV2Service.findById(id, workspace.id);
if (!isDefined(viewField)) {
throw new ViewFieldException(
@@ -93,39 +86,10 @@ export class ViewFieldController {
@Body() input: UpdateViewFieldInput['update'],
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<ViewFieldDTO> {
const isWorkspaceMigrationV2Enabled =
await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
workspace.id,
);
if (isWorkspaceMigrationV2Enabled) {
return await this.viewFieldV2Service.updateOne({
updateViewFieldInput: { id, update: input },
workspaceId: workspace.id,
});
}
const updatedViewField = await this.viewFieldService.update(
id,
workspace.id,
input,
);
// Convert ViewFieldEntity to ViewFieldDTO for consistency
return {
id: updatedViewField.id,
fieldMetadataId: updatedViewField.fieldMetadataId,
isVisible: updatedViewField.isVisible,
size: updatedViewField.size,
position: updatedViewField.position,
aggregateOperation: updatedViewField.aggregateOperation,
viewId: updatedViewField.viewId,
workspaceId: updatedViewField.workspaceId,
createdAt: updatedViewField.createdAt,
updatedAt: updatedViewField.updatedAt,
deletedAt: updatedViewField.deletedAt,
};
return await this.viewFieldV2Service.updateOne({
updateViewFieldInput: { id, update: input },
workspaceId: workspace.id,
});
}
@Post()
@@ -134,38 +98,10 @@ export class ViewFieldController {
@Body() input: CreateViewFieldInput,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<ViewFieldDTO> {
const isWorkspaceMigrationV2Enabled =
await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
workspace.id,
);
if (isWorkspaceMigrationV2Enabled) {
return await this.viewFieldV2Service.createOne({
createViewFieldInput: input,
workspaceId: workspace.id,
});
}
const createdViewField = await this.viewFieldService.create({
...input,
return await this.viewFieldV2Service.createOne({
createViewFieldInput: input,
workspaceId: workspace.id,
});
// Convert ViewFieldEntity to ViewFieldDTO for consistency
return {
id: createdViewField.id,
fieldMetadataId: createdViewField.fieldMetadataId,
isVisible: createdViewField.isVisible,
size: createdViewField.size,
position: createdViewField.position,
aggregateOperation: createdViewField.aggregateOperation,
viewId: createdViewField.viewId,
workspaceId: createdViewField.workspaceId,
createdAt: createdViewField.createdAt,
updatedAt: createdViewField.updatedAt,
deletedAt: createdViewField.deletedAt,
};
}
@Delete(':id')
@@ -174,25 +110,10 @@ export class ViewFieldController {
@Param('id') id: string,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<{ success: boolean }> {
const isWorkspaceMigrationV2Enabled =
await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
workspace.id,
);
if (isWorkspaceMigrationV2Enabled) {
const deletedViewField = await this.viewFieldV2Service.deleteOne({
deleteViewFieldInput: { id },
workspaceId: workspace.id,
});
return { success: isDefined(deletedViewField) };
}
const deletedViewField = await this.viewFieldService.delete(
id,
workspace.id,
);
const deletedViewField = await this.viewFieldV2Service.deleteOne({
deleteViewFieldInput: { id },
workspaceId: workspace.id,
});
return { success: isDefined(deletedViewField) };
}
@@ -1,8 +1,6 @@
import { UseFilters, UseGuards } from '@nestjs/common';
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
@@ -13,12 +11,7 @@ import { DestroyViewFieldInput } from 'src/engine/metadata-modules/view-field/dt
import { UpdateViewFieldInput } from 'src/engine/metadata-modules/view-field/dtos/inputs/update-view-field.input';
import { ViewFieldDTO } from 'src/engine/metadata-modules/view-field/dtos/view-field.dto';
import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity';
import {
ViewFieldException,
ViewFieldExceptionCode,
} from 'src/engine/metadata-modules/view-field/exceptions/view-field.exception';
import { ViewFieldV2Service } from 'src/engine/metadata-modules/view-field/services/view-field-v2.service';
import { ViewFieldService } from 'src/engine/metadata-modules/view-field/services/view-field.service';
import { CreateViewFieldPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/create-view-field-permission.guard';
import { DeleteViewFieldPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/delete-view-field-permission.guard';
import { DestroyViewFieldPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/destroy-view-field-permission.guard';
@@ -29,11 +22,7 @@ import { ViewGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/view/
@UseFilters(ViewGraphqlApiExceptionFilter)
@UseGuards(WorkspaceAuthGuard)
export class ViewFieldResolver {
constructor(
private readonly viewFieldService: ViewFieldService,
private readonly featureFlagService: FeatureFlagService,
private readonly viewFieldV2Service: ViewFieldV2Service,
) {}
constructor(private readonly viewFieldV2Service: ViewFieldV2Service) {}
@Query(() => [ViewFieldDTO])
@UseGuards(NoPermissionGuard)
@@ -41,7 +30,7 @@ export class ViewFieldResolver {
@Args('viewId', { type: () => String }) viewId: string,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<ViewFieldEntity[]> {
return this.viewFieldService.findByViewId(workspace.id, viewId);
return this.viewFieldV2Service.findByViewId(workspace.id, viewId);
}
@Query(() => ViewFieldDTO, { nullable: true })
@@ -50,7 +39,7 @@ export class ViewFieldResolver {
@Args('id', { type: () => String }) id: string,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<ViewFieldEntity | null> {
return this.viewFieldService.findById(id, workspace.id);
return this.viewFieldV2Service.findById(id, workspace.id);
}
@Mutation(() => ViewFieldDTO)
@@ -59,24 +48,10 @@ export class ViewFieldResolver {
@Args('input') updateViewFieldInput: UpdateViewFieldInput,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
): Promise<ViewFieldDTO> {
const isWorkspaceMigrationV2Enabled =
await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
workspaceId,
);
if (isWorkspaceMigrationV2Enabled) {
return await this.viewFieldV2Service.updateOne({
updateViewFieldInput,
workspaceId,
});
}
return this.viewFieldService.update(
updateViewFieldInput.id,
return await this.viewFieldV2Service.updateOne({
updateViewFieldInput,
workspaceId,
updateViewFieldInput.update,
);
});
}
@Mutation(() => ViewFieldDTO)
@@ -85,21 +60,8 @@ export class ViewFieldResolver {
@Args('input') createViewFieldInput: CreateViewFieldInput,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
): Promise<ViewFieldDTO> {
const isWorkspaceMigrationV2Enabled =
await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
workspaceId,
);
if (isWorkspaceMigrationV2Enabled) {
return await this.viewFieldV2Service.createOne({
createViewFieldInput,
workspaceId,
});
}
return this.viewFieldService.create({
...createViewFieldInput,
return await this.viewFieldV2Service.createOne({
createViewFieldInput,
workspaceId,
});
}
@@ -111,19 +73,6 @@ export class ViewFieldResolver {
createViewFieldInputs: CreateViewFieldInput[],
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
): Promise<ViewFieldDTO[]> {
const isWorkspaceMigrationV2Enabled =
await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
workspaceId,
);
if (!isWorkspaceMigrationV2Enabled) {
throw new ViewFieldException(
'Not implemented in v1, please active IS_WORKSPACE_MIGRATION_V2_ENABLED',
ViewFieldExceptionCode.INVALID_VIEW_FIELD_DATA,
);
}
return await this.viewFieldV2Service.createMany({
createViewFieldInputs,
workspaceId,
@@ -136,20 +85,10 @@ export class ViewFieldResolver {
@Args('input') deleteViewFieldInput: DeleteViewFieldInput,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
): Promise<ViewFieldDTO> {
const isWorkspaceMigrationV2Enabled =
await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
workspaceId,
);
if (isWorkspaceMigrationV2Enabled) {
return await this.viewFieldV2Service.deleteOne({
deleteViewFieldInput,
workspaceId,
});
}
return this.viewFieldService.delete(deleteViewFieldInput.id, workspaceId);
return await this.viewFieldV2Service.deleteOne({
deleteViewFieldInput,
workspaceId,
});
}
@Mutation(() => ViewFieldDTO)
@@ -158,19 +97,9 @@ export class ViewFieldResolver {
@Args('input') destroyViewFieldInput: DestroyViewFieldInput,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
): Promise<ViewFieldDTO> {
const isWorkspaceMigrationV2Enabled =
await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
workspaceId,
);
if (isWorkspaceMigrationV2Enabled) {
return await this.viewFieldV2Service.destroyOne({
destroyViewFieldInput,
workspaceId,
});
}
return this.viewFieldService.destroy(destroyViewFieldInput.id, workspaceId);
return await this.viewFieldV2Service.destroyOne({
destroyViewFieldInput,
workspaceId,
});
}
}
@@ -1,532 +0,0 @@
import { Test, type TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { UserInputError } from 'apollo-server-core';
import { type Repository } from 'typeorm';
import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity';
import {
ViewFieldException,
ViewFieldExceptionCode,
ViewFieldExceptionMessageKey,
generateViewFieldExceptionMessage,
generateViewFieldUserFriendlyExceptionMessage,
} from 'src/engine/metadata-modules/view-field/exceptions/view-field.exception';
import { ViewFieldService } from 'src/engine/metadata-modules/view-field/services/view-field.service';
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
describe('ViewFieldService', () => {
let viewFieldService: ViewFieldService;
let viewFieldRepository: Repository<ViewFieldEntity>;
const mockViewField = {
id: 'view-field-id',
fieldMetadataId: 'field-id',
viewId: 'view-id',
workspaceId: 'workspace-id',
position: 0,
isVisible: true,
size: 100,
createdAt: new Date(),
updatedAt: new Date(),
deletedAt: null,
} as ViewFieldEntity;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
ViewFieldService,
{
provide: getRepositoryToken(ViewFieldEntity),
useValue: {
find: jest.fn(),
findOne: jest.fn(),
create: jest.fn(),
save: jest.fn(),
softDelete: jest.fn(),
delete: jest.fn(),
},
},
{
provide: getRepositoryToken(ViewEntity),
useValue: {
findOne: jest.fn(),
},
},
{
provide: WorkspaceCacheStorageService,
useValue: {
flushGraphQLOperation: jest.fn(),
},
},
],
}).compile();
viewFieldService = module.get<ViewFieldService>(ViewFieldService);
viewFieldRepository = module.get<Repository<ViewFieldEntity>>(
getRepositoryToken(ViewFieldEntity),
);
});
it('should be defined', () => {
expect(viewFieldService).toBeDefined();
});
describe('findByWorkspaceId', () => {
it('should return view fields for a workspace', async () => {
const workspaceId = 'workspace-id';
const expectedViewFields = [mockViewField];
jest
.spyOn(viewFieldRepository, 'find')
.mockResolvedValue(expectedViewFields);
const result = await viewFieldService.findByWorkspaceId(workspaceId);
expect(viewFieldRepository.find).toHaveBeenCalledWith({
where: {
workspaceId,
deletedAt: expect.anything(),
},
order: { position: 'ASC' },
relations: ['workspace', 'view'],
});
expect(result).toEqual(expectedViewFields);
});
});
describe('findByViewId', () => {
it('should return view fields for a view', async () => {
const workspaceId = 'workspace-id';
const viewId = 'view-id';
const expectedViewFields = [mockViewField];
jest
.spyOn(viewFieldRepository, 'find')
.mockResolvedValue(expectedViewFields);
const result = await viewFieldService.findByViewId(workspaceId, viewId);
expect(viewFieldRepository.find).toHaveBeenCalledWith({
where: {
workspaceId,
viewId,
deletedAt: expect.anything(),
},
order: { position: 'ASC' },
relations: ['workspace', 'view'],
});
expect(result).toEqual(expectedViewFields);
});
});
describe('findById', () => {
it('should return a view field by id', async () => {
const id = 'view-field-id';
const workspaceId = 'workspace-id';
jest
.spyOn(viewFieldRepository, 'findOne')
.mockResolvedValue(mockViewField);
const result = await viewFieldService.findById(id, workspaceId);
expect(viewFieldRepository.findOne).toHaveBeenCalledWith({
where: {
id,
workspaceId,
deletedAt: expect.anything(),
},
relations: ['workspace', 'view'],
});
expect(result).toEqual(mockViewField);
});
it('should return null when view field is not found', async () => {
const id = 'non-existent-id';
const workspaceId = 'workspace-id';
jest.spyOn(viewFieldRepository, 'findOne').mockResolvedValue(null);
const result = await viewFieldService.findById(id, workspaceId);
expect(result).toBeNull();
});
});
describe('create', () => {
const validViewFieldData = {
fieldMetadataId: 'field-id',
viewId: 'view-id',
workspaceId: 'workspace-id',
position: 0,
isVisible: true,
size: 100,
};
it('should throw exception when workspaceId is missing', async () => {
const invalidData = { ...validViewFieldData, workspaceId: undefined };
await expect(viewFieldService.create(invalidData)).rejects.toThrow(
new ViewFieldException(
generateViewFieldExceptionMessage(
ViewFieldExceptionMessageKey.WORKSPACE_ID_REQUIRED,
),
ViewFieldExceptionCode.INVALID_VIEW_FIELD_DATA,
{
userFriendlyMessage: generateViewFieldUserFriendlyExceptionMessage(
ViewFieldExceptionMessageKey.WORKSPACE_ID_REQUIRED,
),
},
),
);
});
it('should throw exception when viewId is missing', async () => {
const invalidData = { ...validViewFieldData, viewId: undefined };
await expect(viewFieldService.create(invalidData)).rejects.toThrow(
new ViewFieldException(
generateViewFieldExceptionMessage(
ViewFieldExceptionMessageKey.VIEW_ID_REQUIRED,
),
ViewFieldExceptionCode.INVALID_VIEW_FIELD_DATA,
{
userFriendlyMessage: generateViewFieldUserFriendlyExceptionMessage(
ViewFieldExceptionMessageKey.VIEW_ID_REQUIRED,
),
},
),
);
});
it('should throw exception when fieldMetadataId is missing', async () => {
const invalidData = { ...validViewFieldData, fieldMetadataId: undefined };
await expect(viewFieldService.create(invalidData)).rejects.toThrow(
new ViewFieldException(
generateViewFieldExceptionMessage(
ViewFieldExceptionMessageKey.FIELD_METADATA_ID_REQUIRED,
),
ViewFieldExceptionCode.INVALID_VIEW_FIELD_DATA,
{
userFriendlyMessage: generateViewFieldUserFriendlyExceptionMessage(
ViewFieldExceptionMessageKey.FIELD_METADATA_ID_REQUIRED,
),
},
),
);
});
it('should throw exception if position is lower than label metadata identifier', async () => {
const labelIdentifierFieldMetadataId =
'label-identifier-field-matadata-id';
const labelIdentifierViewFieldId =
'view-field-for-label-metadata-identifier-id';
const labelIdentifierViewField = {
...mockViewField,
id: labelIdentifierViewFieldId,
fieldMetadataId: labelIdentifierFieldMetadataId,
position: 0,
};
const mockView = {
id: 'view-id',
objectMetadata: {
labelIdentifierFieldMetadataId,
},
viewFields: [
labelIdentifierViewField,
{ ...mockViewField, position: 1 },
],
} as ViewEntity;
jest.spyOn(viewFieldService, 'findById').mockImplementation((id) => {
if (id === mockViewField.id) {
return Promise.resolve(mockViewField);
}
return Promise.resolve(null);
});
jest
.spyOn(viewFieldService, 'findViewByIdWithRelations')
.mockResolvedValue(mockView);
const invalidData = { ...validViewFieldData, position: -1 };
await expect(viewFieldService.create(invalidData)).rejects.toThrow(
new UserInputError(
'Label metadata identifier must keep the minimal position in the view.',
{
userFriendlyMessage:
'Record text must be in first position of the view.',
},
),
);
});
});
describe('update', () => {
it('should update a view field successfully', async () => {
const id = 'view-field-id';
const workspaceId = 'workspace-id';
const updateData = { position: 1 };
const updatedViewField = { ...mockViewField, ...updateData };
const mockView = {
id: 'view-id',
objectMetadata: {
labelIdentifierFieldMetadataId: mockViewField.fieldMetadataId,
},
viewFields: [mockViewField],
} as ViewEntity;
jest.spyOn(viewFieldService, 'findById').mockResolvedValue(mockViewField);
jest
.spyOn(viewFieldRepository, 'save')
.mockResolvedValue(updatedViewField);
jest
.spyOn(viewFieldService, 'findViewByIdWithRelations')
.mockResolvedValue(mockView);
const result = await viewFieldService.update(id, workspaceId, updateData);
expect(viewFieldService.findById).toHaveBeenCalledWith(id, workspaceId);
expect(viewFieldRepository.save).toHaveBeenCalledWith({
id,
...updateData,
});
expect(result).toEqual({ ...mockViewField, ...updatedViewField });
});
it('should throw exception when view field is not found', async () => {
const id = 'non-existent-id';
const workspaceId = 'workspace-id';
const updateData = { position: 1 };
jest.spyOn(viewFieldService, 'findById').mockResolvedValue(null);
await expect(
viewFieldService.update(id, workspaceId, updateData),
).rejects.toThrow(
new ViewFieldException(
generateViewFieldExceptionMessage(
ViewFieldExceptionMessageKey.VIEW_FIELD_NOT_FOUND,
id,
),
ViewFieldExceptionCode.VIEW_FIELD_NOT_FOUND,
),
);
});
it('should throw exception when label metadata identifier is not in first position (label metadata identifier field update case)', async () => {
const workspaceId = 'workspace-id';
const updateData = { position: 2 };
const labelIdentifierFieldMetadataId =
'label-identifier-field-matadata-id';
const labelIdentifierViewFieldId =
'view-field-for-label-metadata-identifier-id';
const labelIdentifierViewField = {
...mockViewField,
id: labelIdentifierViewFieldId,
fieldMetadataId: labelIdentifierFieldMetadataId,
position: 0,
};
const mockView = {
id: 'view-id',
objectMetadata: {
labelIdentifierFieldMetadataId,
},
viewFields: [
labelIdentifierViewField,
{ ...mockViewField, position: 1 },
],
} as ViewEntity;
jest.spyOn(viewFieldService, 'findById').mockImplementation((id) => {
if (id === labelIdentifierViewFieldId) {
return Promise.resolve(labelIdentifierViewField);
}
return Promise.resolve(null);
});
jest
.spyOn(viewFieldService, 'findViewByIdWithRelations')
.mockResolvedValue(mockView);
await expect(
viewFieldService.update(
labelIdentifierViewFieldId,
workspaceId,
updateData,
),
).rejects.toThrow(
new UserInputError(
'Label metadata identifier must keep the minimal position in the view.',
{
userFriendlyMessage:
'Record text must be in first position of the view.',
},
),
);
});
it('should throw exception when label metadata identifier is not in first position (regular field update case)', async () => {
const workspaceId = 'workspace-id';
const updateData = { position: -1 };
const labelIdentifierFieldMetadataId =
'label-identifier-field-matadata-id';
const labelIdentifierViewFieldId =
'view-field-for-label-metadata-identifier-id';
const labelIdentifierViewField = {
...mockViewField,
id: labelIdentifierViewFieldId,
fieldMetadataId: labelIdentifierFieldMetadataId,
position: 0,
};
const mockView = {
id: 'view-id',
objectMetadata: {
labelIdentifierFieldMetadataId,
},
viewFields: [
labelIdentifierViewField,
{ ...mockViewField, position: 1 },
],
} as ViewEntity;
jest.spyOn(viewFieldService, 'findById').mockImplementation((id) => {
if (id === mockViewField.id) {
return Promise.resolve(mockViewField);
}
return Promise.resolve(null);
});
jest
.spyOn(viewFieldService, 'findViewByIdWithRelations')
.mockResolvedValue(mockView);
await expect(
viewFieldService.update(mockViewField.id, workspaceId, updateData),
).rejects.toThrow(
new UserInputError(
'Label metadata identifier must keep the minimal position in the view.',
{
userFriendlyMessage:
'Record text must be in first position of the view.',
},
),
);
});
it('should throw exception when attempting to make label metadata identifier invisible', async () => {
const workspaceId = 'workspace-id';
const updateData = { isVisible: false };
const labelIdentifierFieldMetadataId =
'label-identifier-field-matadata-id';
const labelIdentifierViewFieldId =
'view-field-for-label-metadata-identifier-id';
const labelIdentifierViewField = {
...mockViewField,
id: labelIdentifierViewFieldId,
fieldMetadataId: labelIdentifierFieldMetadataId,
position: 0,
};
const mockView = {
id: 'view-id',
objectMetadata: {
labelIdentifierFieldMetadataId,
},
viewFields: [labelIdentifierViewField, mockViewField],
} as ViewEntity;
jest.spyOn(viewFieldService, 'findById').mockImplementation((id) => {
if (id === labelIdentifierViewFieldId) {
return Promise.resolve(labelIdentifierViewField);
}
return Promise.resolve(null);
});
jest
.spyOn(viewFieldService, 'findViewByIdWithRelations')
.mockResolvedValue(mockView);
await expect(
viewFieldService.update(
labelIdentifierViewField.id,
workspaceId,
updateData,
),
).rejects.toThrow(
new UserInputError('Label metadata identifier must stay visible.', {
userFriendlyMessage: 'Record text must stay visible.',
}),
);
});
});
describe('delete', () => {
it('should delete a view field successfully', async () => {
const id = 'view-field-id';
const workspaceId = 'workspace-id';
jest.spyOn(viewFieldService, 'findById').mockResolvedValue(mockViewField);
jest
.spyOn(viewFieldRepository, 'softDelete')
.mockResolvedValue({} as any);
const result = await viewFieldService.delete(id, workspaceId);
expect(viewFieldService.findById).toHaveBeenCalledWith(id, workspaceId);
expect(viewFieldRepository.softDelete).toHaveBeenCalledWith(id);
expect(result).toEqual(mockViewField);
});
it('should throw exception when view field is not found', async () => {
const id = 'non-existent-id';
const workspaceId = 'workspace-id';
jest.spyOn(viewFieldService, 'findById').mockResolvedValue(null);
await expect(viewFieldService.delete(id, workspaceId)).rejects.toThrow(
new ViewFieldException(
generateViewFieldExceptionMessage(
ViewFieldExceptionMessageKey.VIEW_FIELD_NOT_FOUND,
id,
),
ViewFieldExceptionCode.VIEW_FIELD_NOT_FOUND,
),
);
});
});
describe('destroy', () => {
it('should destroy a view field successfully', async () => {
const id = 'view-field-id';
const workspaceId = 'workspace-id';
jest
.spyOn(viewFieldRepository, 'findOne')
.mockResolvedValue(mockViewField);
jest.spyOn(viewFieldRepository, 'delete').mockResolvedValue({} as any);
const result = await viewFieldService.destroy(id, workspaceId);
expect(viewFieldRepository.findOne).toHaveBeenCalledWith({
where: { id, workspaceId },
relations: ['workspace', 'view'],
withDeleted: true,
});
expect(viewFieldRepository.delete).toHaveBeenCalledWith(id);
expect(result).toBeDefined();
});
});
});
@@ -1,6 +1,8 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { isDefined } from 'twenty-shared/utils';
import { IsNull, Repository } from 'typeorm';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { computeFlatEntityMapsFromTo } from 'src/engine/metadata-modules/flat-entity/utils/compute-flat-entity-maps-from-to.util';
@@ -15,6 +17,7 @@ import { DeleteViewFieldInput } from 'src/engine/metadata-modules/view-field/dto
import { DestroyViewFieldInput } from 'src/engine/metadata-modules/view-field/dtos/inputs/destroy-view-field.input';
import { UpdateViewFieldInput } from 'src/engine/metadata-modules/view-field/dtos/inputs/update-view-field.input';
import { ViewFieldDTO } from 'src/engine/metadata-modules/view-field/dtos/view-field.dto';
import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity';
import {
ViewFieldException,
ViewFieldExceptionCode,
@@ -26,7 +29,9 @@ import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspa
export class ViewFieldV2Service {
constructor(
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
@InjectRepository(ViewFieldEntity)
private readonly viewFieldRepository: Repository<ViewFieldEntity>,
) {}
async createOne({
@@ -67,7 +72,7 @@ export class ViewFieldV2Service {
flatViewMaps,
flatFieldMetadataMaps,
flatObjectMetadataMaps,
} = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
} = await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: [
@@ -118,7 +123,7 @@ export class ViewFieldV2Service {
}
const { flatViewFieldMaps: recomputedExistingFlatViewFieldMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatViewFieldMaps'],
@@ -142,7 +147,7 @@ export class ViewFieldV2Service {
flatViewFieldMaps: existingFlatViewFieldMaps,
flatObjectMetadataMaps,
flatViewMaps,
} = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
} = await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: [
@@ -189,7 +194,7 @@ export class ViewFieldV2Service {
}
const { flatViewFieldMaps: recomputedExistingFlatViewFieldMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatViewFieldMaps'],
@@ -213,7 +218,7 @@ export class ViewFieldV2Service {
flatViewFieldMaps: existingFlatViewFieldMaps,
flatObjectMetadataMaps,
flatViewMaps,
} = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
} = await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: [
@@ -260,7 +265,7 @@ export class ViewFieldV2Service {
}
const { flatViewFieldMaps: recomputedExistingFlatViewFieldMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatViewFieldMaps'],
@@ -285,7 +290,7 @@ export class ViewFieldV2Service {
flatViewMaps: existingFlatViewMaps,
flatFieldMetadataMaps,
flatObjectMetadataMaps,
} = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
} = await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: [
@@ -338,4 +343,46 @@ export class ViewFieldV2Service {
return existingViewFieldToDelete;
}
async findByWorkspaceId(workspaceId: string): Promise<ViewFieldEntity[]> {
return this.viewFieldRepository.find({
where: {
workspaceId,
deletedAt: IsNull(),
},
order: { position: 'ASC' },
relations: ['workspace', 'view'],
});
}
async findByViewId(
workspaceId: string,
viewId: string,
): Promise<ViewFieldEntity[]> {
return this.viewFieldRepository.find({
where: {
workspaceId,
viewId,
deletedAt: IsNull(),
},
order: { position: 'ASC' },
relations: ['workspace', 'view'],
});
}
async findById(
id: string,
workspaceId: string,
): Promise<ViewFieldEntity | null> {
const viewField = await this.viewFieldRepository.findOne({
where: {
id,
workspaceId,
deletedAt: IsNull(),
},
relations: ['workspace', 'view'],
});
return viewField || null;
}
}
@@ -1,469 +0,0 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { msg } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import { IsNull, Repository } from 'typeorm';
import { UserInputError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity';
import {
ViewFieldException,
ViewFieldExceptionCode,
ViewFieldExceptionMessageKey,
generateViewFieldExceptionMessage,
generateViewFieldUserFriendlyExceptionMessage,
} from 'src/engine/metadata-modules/view-field/exceptions/view-field.exception';
import { FIND_ALL_CORE_VIEWS_GRAPHQL_OPERATION } from 'src/engine/metadata-modules/view/constants/find-all-core-views-graphql-operation.constant';
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
@Injectable()
export class ViewFieldService {
constructor(
@InjectRepository(ViewFieldEntity)
private readonly viewFieldRepository: Repository<ViewFieldEntity>,
@InjectRepository(ViewEntity)
private readonly viewRepository: Repository<ViewEntity>,
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
) {}
async findByWorkspaceId(workspaceId: string): Promise<ViewFieldEntity[]> {
return this.viewFieldRepository.find({
where: {
workspaceId,
deletedAt: IsNull(),
},
order: { position: 'ASC' },
relations: ['workspace', 'view'],
});
}
async findByViewId(
workspaceId: string,
viewId: string,
): Promise<ViewFieldEntity[]> {
return this.viewFieldRepository.find({
where: {
workspaceId,
viewId,
deletedAt: IsNull(),
},
order: { position: 'ASC' },
relations: ['workspace', 'view'],
});
}
async findById(
id: string,
workspaceId: string,
): Promise<ViewFieldEntity | null> {
const viewField = await this.viewFieldRepository.findOne({
where: {
id,
workspaceId,
deletedAt: IsNull(),
},
relations: ['workspace', 'view'],
});
return viewField || null;
}
async create(
viewFieldData: Partial<ViewFieldEntity>,
): Promise<ViewFieldEntity> {
if (this.hasRequiredFields(viewFieldData)) {
try {
if (isDefined(viewFieldData.position)) {
const viewFieldDataWithPosition =
viewFieldData as typeof viewFieldData & { position: number };
await this.verifyLabelMetadataIdentifierIsInFirstPositionOrThrow(
viewFieldDataWithPosition,
viewFieldData.workspaceId,
);
}
await this.verifyLabelMetadataIdentifierIsVisibleOrThrow(
viewFieldData,
viewFieldData.workspaceId,
);
const viewFieldDataWithPosition = await this.formatViewFieldData(
viewFieldData,
viewFieldData.workspaceId,
);
const viewField = this.viewFieldRepository.create(
viewFieldDataWithPosition,
);
const savedViewField = await this.viewFieldRepository.save(viewField);
await this.flushGraphQLCache(viewFieldData.workspaceId);
const createdViewField = await this.findById(
savedViewField.id,
viewFieldData.workspaceId,
);
if (!isDefined(createdViewField)) {
throw new ViewFieldException(
generateViewFieldExceptionMessage(
ViewFieldExceptionMessageKey.VIEW_FIELD_NOT_FOUND,
),
ViewFieldExceptionCode.VIEW_FIELD_NOT_FOUND,
{
userFriendlyMessage:
generateViewFieldUserFriendlyExceptionMessage(
ViewFieldExceptionMessageKey.VIEW_FIELD_NOT_FOUND,
),
},
);
}
return createdViewField;
} catch (error) {
if (
error.message.includes(
'duplicate key value violates unique constraint',
)
) {
throw new ViewFieldException(
generateViewFieldExceptionMessage(
ViewFieldExceptionMessageKey.VIEW_FIELD_ALREADY_EXISTS,
),
ViewFieldExceptionCode.INVALID_VIEW_FIELD_DATA,
{
userFriendlyMessage:
generateViewFieldUserFriendlyExceptionMessage(
ViewFieldExceptionMessageKey.VIEW_FIELD_ALREADY_EXISTS,
),
},
);
}
throw error;
}
} else {
if (!isDefined(viewFieldData.workspaceId)) {
throw new ViewFieldException(
generateViewFieldExceptionMessage(
ViewFieldExceptionMessageKey.WORKSPACE_ID_REQUIRED,
),
ViewFieldExceptionCode.INVALID_VIEW_FIELD_DATA,
{
userFriendlyMessage: generateViewFieldUserFriendlyExceptionMessage(
ViewFieldExceptionMessageKey.WORKSPACE_ID_REQUIRED,
),
},
);
}
if (!isDefined(viewFieldData.viewId)) {
throw new ViewFieldException(
generateViewFieldExceptionMessage(
ViewFieldExceptionMessageKey.VIEW_ID_REQUIRED,
),
ViewFieldExceptionCode.INVALID_VIEW_FIELD_DATA,
{
userFriendlyMessage: generateViewFieldUserFriendlyExceptionMessage(
ViewFieldExceptionMessageKey.VIEW_ID_REQUIRED,
),
},
);
}
throw new ViewFieldException(
generateViewFieldExceptionMessage(
ViewFieldExceptionMessageKey.FIELD_METADATA_ID_REQUIRED,
),
ViewFieldExceptionCode.INVALID_VIEW_FIELD_DATA,
{
userFriendlyMessage: generateViewFieldUserFriendlyExceptionMessage(
ViewFieldExceptionMessageKey.FIELD_METADATA_ID_REQUIRED,
),
},
);
}
}
async update(
id: string,
workspaceId: string,
updateData: Partial<ViewFieldEntity>,
): Promise<ViewFieldEntity> {
const existingViewField = await this.findById(id, workspaceId);
if (!isDefined(existingViewField)) {
throw new ViewFieldException(
generateViewFieldExceptionMessage(
ViewFieldExceptionMessageKey.VIEW_FIELD_NOT_FOUND,
id,
),
ViewFieldExceptionCode.VIEW_FIELD_NOT_FOUND,
);
}
const viewId = existingViewField.viewId;
if (this.updatesPosition(updateData)) {
await this.verifyLabelMetadataIdentifierIsInFirstPositionOrThrow(
{
...updateData,
viewId,
id,
fieldMetadataId: existingViewField.fieldMetadataId,
},
workspaceId,
);
}
if (this.disablesVisibility(updateData)) {
await this.verifyLabelMetadataIdentifierIsVisibleOrThrow(
{ ...updateData, viewId, id },
workspaceId,
);
}
const updatedViewField = await this.viewFieldRepository.save({
id,
...updateData,
});
await this.flushGraphQLCache(workspaceId);
return { ...existingViewField, ...updatedViewField };
}
async delete(id: string, workspaceId: string): Promise<ViewFieldEntity> {
const viewField = await this.findById(id, workspaceId);
if (!isDefined(viewField)) {
throw new ViewFieldException(
generateViewFieldExceptionMessage(
ViewFieldExceptionMessageKey.VIEW_FIELD_NOT_FOUND,
id,
),
ViewFieldExceptionCode.VIEW_FIELD_NOT_FOUND,
);
}
await this.viewFieldRepository.softDelete(id);
await this.flushGraphQLCache(workspaceId);
return viewField;
}
async destroy(id: string, workspaceId: string): Promise<ViewFieldEntity> {
const viewField = await this.viewFieldRepository.findOne({
where: {
id,
workspaceId,
},
relations: ['workspace', 'view'],
withDeleted: true,
});
if (!isDefined(viewField)) {
throw new ViewFieldException(
generateViewFieldExceptionMessage(
ViewFieldExceptionMessageKey.VIEW_FIELD_NOT_FOUND,
id,
),
ViewFieldExceptionCode.VIEW_FIELD_NOT_FOUND,
);
}
await this.viewFieldRepository.delete(id);
await this.flushGraphQLCache(workspaceId);
return viewField;
}
private updatesPosition(
data: Partial<ViewFieldEntity>,
): data is Partial<ViewFieldEntity> & { position: number } {
return isDefined(data.position);
}
private disablesVisibility(
data: Partial<ViewFieldEntity>,
): data is Partial<ViewFieldEntity> & { isVisible: boolean } {
return data.isVisible === false;
}
private async verifyLabelMetadataIdentifierIsVisibleOrThrow(
newOrUpdatedViewField: Partial<ViewFieldEntity> & {
viewId: string;
},
workspaceId: string,
) {
const view = await this.findViewByIdWithRelations(
newOrUpdatedViewField.viewId,
workspaceId,
);
if (!isDefined(view)) {
throw new Error(`View not found: ${newOrUpdatedViewField.viewId}`);
}
const labelMetadataIdentifierFieldMetadataId =
view.objectMetadata.labelIdentifierFieldMetadataId;
const labelMetadataIdentifierViewField = view.viewFields.find(
(viewField) =>
viewField.fieldMetadataId === labelMetadataIdentifierFieldMetadataId,
);
if (
!isDefined(labelMetadataIdentifierViewField) ||
labelMetadataIdentifierViewField.id !== newOrUpdatedViewField.id
) {
return;
}
if (newOrUpdatedViewField.isVisible === false) {
throw new UserInputError('Label metadata identifier must stay visible.', {
userFriendlyMessage: msg`Record text must stay visible.`,
});
}
}
private async verifyLabelMetadataIdentifierIsInFirstPositionOrThrow(
newOrUpdatedViewField: Partial<ViewFieldEntity> & { viewId: string } & {
position: number;
},
workspaceId: string,
) {
const view = await this.findViewByIdWithRelations(
newOrUpdatedViewField.viewId,
workspaceId,
);
if (!isDefined(view)) {
throw new Error(`View not found: ${newOrUpdatedViewField.viewId}`);
}
const viewFieldsWithoutUpdatedViewField = view.viewFields.filter(
(viewField) => viewField.id !== newOrUpdatedViewField?.id,
);
if (viewFieldsWithoutUpdatedViewField.length === 0) {
return;
}
const labelMetadataIdentifierFieldMetadataId =
view.objectMetadata.labelIdentifierFieldMetadataId;
if (
labelMetadataIdentifierFieldMetadataId ===
newOrUpdatedViewField.fieldMetadataId
) {
const minPositionInViewWithoutUpdatedViewField =
viewFieldsWithoutUpdatedViewField.reduce(
(minViewField, viewField) =>
viewField.position < minViewField.position
? viewField
: minViewField,
viewFieldsWithoutUpdatedViewField[0],
).position;
if (
newOrUpdatedViewField.position >=
minPositionInViewWithoutUpdatedViewField
) {
throw new UserInputError(
'Label metadata identifier must keep the minimal position in the view.',
{
userFriendlyMessage: msg`Record text must be in first position of the view.`,
},
);
}
} else {
const labelMetadataIdentifierViewFieldPosition = view.viewFields.find(
(viewField) =>
viewField.fieldMetadataId === labelMetadataIdentifierFieldMetadataId,
)?.position;
if (
isDefined(labelMetadataIdentifierViewFieldPosition) &&
newOrUpdatedViewField.position <=
labelMetadataIdentifierViewFieldPosition
) {
throw new UserInputError(
'Label metadata identifier must keep the minimal position in the view.',
{
userFriendlyMessage: msg`Record text must be in first position of the view.`,
},
);
}
}
}
private async formatViewFieldData(
viewFieldData: Partial<ViewFieldEntity> & { viewId: string },
workspaceId: string,
): Promise<Partial<ViewFieldEntity>> {
if (!isDefined(viewFieldData.position)) {
const view = await this.findViewByIdWithRelations(
viewFieldData.viewId,
workspaceId,
);
if (!isDefined(view)) {
throw new Error(`View not found: ${viewFieldData.viewId}`);
}
const highestPositionInView = view.viewFields.reduce(
(maxViewField, viewField) =>
viewField.position > maxViewField.position ? viewField : maxViewField,
view.viewFields[0],
);
return { ...viewFieldData, position: highestPositionInView.position + 1 };
} else {
return viewFieldData;
}
}
private hasRequiredFields(
data: Partial<ViewFieldEntity>,
): data is Partial<ViewFieldEntity> & {
viewId: string;
fieldMetadataId: string;
workspaceId: string;
} {
return (
isDefined(data.viewId) &&
isDefined(data.fieldMetadataId) &&
isDefined(data.workspaceId)
);
}
public async findViewByIdWithRelations(
id: string,
workspaceId: string,
): Promise<ViewEntity | null> {
const view = await this.viewRepository.findOne({
where: {
id,
workspaceId,
deletedAt: IsNull(),
},
relations: ['workspace', 'objectMetadata', 'viewFields'],
});
return view || null;
}
private async flushGraphQLCache(workspaceId: string): Promise<void> {
await this.workspaceCacheStorageService.flushGraphQLOperation({
operationName: FIND_ALL_CORE_VIEWS_GRAPHQL_OPERATION,
workspaceId,
});
}
}
@@ -1,14 +1,12 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { ViewFieldController } from 'src/engine/metadata-modules/view-field/controllers/view-field.controller';
import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity';
import { ViewFieldResolver } from 'src/engine/metadata-modules/view-field/resolvers/view-field.resolver';
import { ViewFieldV2Service } from 'src/engine/metadata-modules/view-field/services/view-field-v2.service';
import { ViewFieldService } from 'src/engine/metadata-modules/view-field/services/view-field.service';
import { ViewPermissionsModule } from 'src/engine/metadata-modules/view-permissions/view-permissions.module';
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
@@ -18,14 +16,13 @@ import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspa
imports: [
TypeOrmModule.forFeature([ViewFieldEntity, ViewEntity]),
WorkspaceCacheStorageModule,
FeatureFlagModule,
PermissionsModule,
WorkspaceMigrationV2Module,
WorkspaceManyOrAllFlatEntityMapsCacheModule,
ViewPermissionsModule,
],
controllers: [ViewFieldController],
providers: [ViewFieldService, ViewFieldResolver, ViewFieldV2Service],
exports: [ViewFieldService, ViewFieldV2Service],
providers: [ViewFieldResolver, ViewFieldV2Service],
exports: [ViewFieldV2Service],
})
export class ViewFieldModule {}