diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 35c0712058..fdd9b15439 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -852,7 +852,7 @@ export type CreateOneFieldMetadataInput = { }; export type CreateOneObjectInput = { - /** The record to create */ + /** The object to create */ object: CreateObjectInput; }; diff --git a/packages/twenty-front/src/generated/graphql.ts b/packages/twenty-front/src/generated/graphql.ts index e5d51ea1fc..6e9a95d1b5 100644 --- a/packages/twenty-front/src/generated/graphql.ts +++ b/packages/twenty-front/src/generated/graphql.ts @@ -800,11 +800,30 @@ export type CreateFieldInput = { type: FieldMetadataType; }; +export type CreateObjectInput = { + description?: InputMaybe; + icon?: InputMaybe; + isLabelSyncedWithName?: InputMaybe; + isRemote?: InputMaybe; + labelPlural: Scalars['String']; + labelSingular: Scalars['String']; + namePlural: Scalars['String']; + nameSingular: Scalars['String']; + primaryKeyColumnType?: InputMaybe; + primaryKeyFieldMetadataSettings?: InputMaybe; + shortcut?: InputMaybe; +}; + export type CreateOneFieldMetadataInput = { /** The record to create */ field: CreateFieldInput; }; +export type CreateOneObjectInput = { + /** The object to create */ + object: CreateObjectInput; +}; + export type CreatePageLayoutInput = { name: Scalars['String']; objectMetadataId?: InputMaybe; @@ -1999,6 +2018,11 @@ export type MutationCreateOneFieldArgs = { }; +export type MutationCreateOneObjectArgs = { + input: CreateOneObjectInput; +}; + + export type MutationCreateOneRoleArgs = { createRoleInput: CreateRoleInput; }; diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-make-sure-dashboard-naming-available.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-make-sure-dashboard-naming-available.command.ts index 01c722f788..f2cf6d9f69 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-make-sure-dashboard-naming-available.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-10/1-10-make-sure-dashboard-naming-available.command.ts @@ -62,7 +62,7 @@ export class MakeSureDashboardNamingAvailableCommand extends ActiveOrSuspendedWo `Updating the dashboard object metadata for workspace ${workspaceId}...`, ); - await this.objectMetadataServiceV2.updateOne({ + await this.objectMetadataServiceV2.updateOneObject({ workspaceId, updateObjectInput: { id: potentialCustomDashboardObjectMetadata.id, diff --git a/packages/twenty-server/src/engine/api/graphql/__tests__/workspace.factory.spec.ts b/packages/twenty-server/src/engine/api/graphql/__tests__/workspace.factory.spec.ts index 4b62aff9fb..085f9cfe6b 100644 --- a/packages/twenty-server/src/engine/api/graphql/__tests__/workspace.factory.spec.ts +++ b/packages/twenty-server/src/engine/api/graphql/__tests__/workspace.factory.spec.ts @@ -7,7 +7,7 @@ import { WorkspaceSchemaFactory } from 'src/engine/api/graphql/workspace-schema. import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; -import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service'; +import { ObjectMetadataServiceV2 } from 'src/engine/metadata-modules/object-metadata/object-metadata-v2.service'; import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service'; import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; @@ -23,7 +23,7 @@ describe('WorkspaceSchemaFactory', () => { useValue: {}, }, { - provide: ObjectMetadataService, + provide: ObjectMetadataServiceV2, useValue: {}, }, { diff --git a/packages/twenty-server/src/engine/core-modules/ai/services/__tests__/tool.service.spec.ts b/packages/twenty-server/src/engine/core-modules/ai/services/__tests__/tool.service.spec.ts index fe66f81bca..73bb692a84 100644 --- a/packages/twenty-server/src/engine/core-modules/ai/services/__tests__/tool.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/ai/services/__tests__/tool.service.spec.ts @@ -6,7 +6,7 @@ import { DeleteRecordService } from 'src/engine/core-modules/record-crud/service import { FindRecordsService } from 'src/engine/core-modules/record-crud/services/find-records.service'; import { UpdateRecordService } from 'src/engine/core-modules/record-crud/services/update-record.service'; import { RecordInputTransformerService } from 'src/engine/core-modules/record-transformer/services/record-input-transformer.service'; -import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service'; +import { ObjectMetadataServiceV2 } from 'src/engine/metadata-modules/object-metadata/object-metadata-v2.service'; import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; @@ -57,7 +57,7 @@ describe('ToolService', () => { }, }, { - provide: ObjectMetadataService, + provide: ObjectMetadataServiceV2, useValue: { findManyWithinWorkspace: jest.fn().mockResolvedValue([testObject]), }, diff --git a/packages/twenty-server/src/engine/core-modules/ai/services/tool.service.ts b/packages/twenty-server/src/engine/core-modules/ai/services/tool.service.ts index 3fc2d060ae..fff714d638 100644 --- a/packages/twenty-server/src/engine/core-modules/ai/services/tool.service.ts +++ b/packages/twenty-server/src/engine/core-modules/ai/services/tool.service.ts @@ -17,7 +17,7 @@ import { type ToolHints, type ToolOperation, } from 'src/engine/metadata-modules/ai-router/types/tool-hints.interface'; -import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service'; +import { ObjectMetadataServiceV2 } from 'src/engine/metadata-modules/object-metadata/object-metadata-v2.service'; import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config'; @@ -29,7 +29,7 @@ export class ToolService { constructor( private readonly twentyORMGlobalManager: TwentyORMGlobalManager, - private readonly objectMetadataService: ObjectMetadataService, + private readonly objectMetadataService: ObjectMetadataServiceV2, protected readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService, private readonly createRecordService: CreateRecordService, private readonly updateRecordService: UpdateRecordService, diff --git a/packages/twenty-server/src/engine/core-modules/application/application-sync.service.ts b/packages/twenty-server/src/engine/core-modules/application/application-sync.service.ts index b3973f329f..394ffa469a 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-sync.service.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-sync.service.ts @@ -219,7 +219,7 @@ export class ApplicationSyncService { ); for (const fieldToDelete of fieldsToDelete) { - await this.fieldMetadataServiceV2.updateOne({ + await this.fieldMetadataServiceV2.updateOneField({ updateFieldInput: { id: fieldToDelete.id, isActive: false, @@ -256,7 +256,7 @@ export class ApplicationSyncService { isNullable: fieldToSync.isNullable ?? true, }; - await this.fieldMetadataServiceV2.updateOne({ + await this.fieldMetadataServiceV2.updateOneField({ updateFieldInput, workspaceId, }); @@ -281,7 +281,7 @@ export class ApplicationSyncService { workspaceId, }; - await this.fieldMetadataServiceV2.createOne({ + await this.fieldMetadataServiceV2.createOneField({ createFieldInput, workspaceId, }); @@ -341,7 +341,7 @@ export class ApplicationSyncService { ); for (const objectToDelete of objectsToDelete) { - await this.objectMetadataServiceV2.deleteOne({ + await this.objectMetadataServiceV2.deleteOneObject({ deleteObjectInput: { id: objectToDelete.id }, workspaceId, isSystemBuild: true, @@ -372,7 +372,7 @@ export class ApplicationSyncService { }, }; - await this.objectMetadataServiceV2.updateOne({ + await this.objectMetadataServiceV2.updateOneObject({ updateObjectInput, workspaceId, }); @@ -404,7 +404,7 @@ export class ApplicationSyncService { applicationId, }; - const createdObject = await this.objectMetadataServiceV2.createOne({ + const createdObject = await this.objectMetadataServiceV2.createOneObject({ createObjectInput, workspaceId, }); diff --git a/packages/twenty-server/src/engine/core-modules/open-api/open-api.service.spec.ts b/packages/twenty-server/src/engine/core-modules/open-api/open-api.service.spec.ts index 7e75f74f4a..6305e11436 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/open-api.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/open-api.service.spec.ts @@ -4,7 +4,7 @@ import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/ import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { OpenApiService } from 'src/engine/core-modules/open-api/open-api.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; -import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service'; +import { ObjectMetadataServiceV2 } from 'src/engine/metadata-modules/object-metadata/object-metadata-v2.service'; describe('OpenApiService', () => { let service: OpenApiService; @@ -18,7 +18,7 @@ describe('OpenApiService', () => { useValue: {}, }, { - provide: ObjectMetadataService, + provide: ObjectMetadataServiceV2, useValue: {}, }, { diff --git a/packages/twenty-server/src/engine/core-modules/open-api/open-api.service.ts b/packages/twenty-server/src/engine/core-modules/open-api/open-api.service.ts index 218c3489a0..b9c141c8ed 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/open-api.service.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/open-api.service.ts @@ -47,8 +47,8 @@ import { import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception'; +import { ObjectMetadataServiceV2 } from 'src/engine/metadata-modules/object-metadata/object-metadata-v2.service'; import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service'; import { standardObjectMetadataDefinitions } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-objects'; import { shouldExcludeFromWorkspaceApi } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/should-exclude-from-workspace-api.util'; import { getServerUrl } from 'src/utils/get-server-url'; @@ -58,7 +58,7 @@ export class OpenApiService { constructor( private readonly accessTokenService: AccessTokenService, private readonly twentyConfigService: TwentyConfigService, - private readonly objectMetadataService: ObjectMetadataService, + private readonly objectMetadataService: ObjectMetadataServiceV2, private readonly featureFlagService: FeatureFlagService, ) {} diff --git a/packages/twenty-server/src/engine/core-modules/twenty-config/utils/__tests__/apply-basic-validators.util.spec.ts b/packages/twenty-server/src/engine/core-modules/twenty-config/utils/__tests__/apply-basic-validators.util.spec.ts deleted file mode 100644 index 3adfc54387..0000000000 --- a/packages/twenty-server/src/engine/core-modules/twenty-config/utils/__tests__/apply-basic-validators.util.spec.ts +++ /dev/null @@ -1,215 +0,0 @@ -import { Transform } from 'class-transformer'; -import { - IsArray, - IsBoolean, - IsEnum, - IsNumber, - IsString, -} from 'class-validator'; - -import { ConfigVariableType } from 'src/engine/core-modules/twenty-config/enums/config-variable-type.enum'; -import { applyBasicValidators } from 'src/engine/core-modules/twenty-config/utils/apply-basic-validators.util'; -import { configTransformers } from 'src/engine/core-modules/twenty-config/utils/config-transformers.util'; - -jest.mock('class-transformer', () => ({ - Transform: jest.fn(), -})); - -jest.mock('class-validator', () => ({ - IsBoolean: jest.fn().mockReturnValue(jest.fn()), - IsNumber: jest.fn().mockReturnValue(jest.fn()), - IsString: jest.fn().mockReturnValue(jest.fn()), - IsEnum: jest.fn().mockReturnValue(jest.fn()), - IsArray: jest.fn().mockReturnValue(jest.fn()), -})); - -jest.mock( - 'src/engine/core-modules/twenty-config/utils/config-transformers.util', - () => ({ - configTransformers: { - boolean: jest.fn(), - number: jest.fn(), - }, - }), -); - -describe('applyBasicValidators', () => { - const mockTarget = {}; - const mockPropertyKey = 'testProperty'; - - beforeEach(() => { - jest.clearAllMocks(); - }); - - describe('boolean type', () => { - it('should apply boolean transformers and validators', () => { - let capturedTransformFn; - - (Transform as jest.Mock).mockImplementation((transformFn) => { - capturedTransformFn = transformFn; - - return jest.fn(); - }); - - applyBasicValidators( - ConfigVariableType.BOOLEAN, - mockTarget, - mockPropertyKey, - ); - - expect(Transform).toHaveBeenCalled(); - expect(IsBoolean).toHaveBeenCalled(); - - const transformFn = capturedTransformFn; - const mockTransformParams = { value: 'true' }; - - (configTransformers.boolean as jest.Mock).mockReturnValueOnce(true); - // @ts-expect-error legacy noImplicitAny - const result1 = transformFn(mockTransformParams); - - expect(configTransformers.boolean).toHaveBeenCalledWith('true'); - expect(result1).toBe(true); - - (configTransformers.boolean as jest.Mock).mockReturnValueOnce(undefined); - // @ts-expect-error legacy noImplicitAny - const result2 = transformFn(mockTransformParams); - - expect(result2).toBe('true'); - }); - }); - - describe('number type', () => { - it('should apply number transformers and validators', () => { - let capturedTransformFn; - - (Transform as jest.Mock).mockImplementation((transformFn) => { - capturedTransformFn = transformFn; - - return jest.fn(); - }); - - applyBasicValidators( - ConfigVariableType.NUMBER, - mockTarget, - mockPropertyKey, - ); - - expect(Transform).toHaveBeenCalled(); - expect(IsNumber).toHaveBeenCalled(); - - const transformFn = capturedTransformFn; - const mockTransformParams = { value: '42' }; - - (configTransformers.number as jest.Mock).mockReturnValueOnce(42); - // @ts-expect-error legacy noImplicitAny - const result1 = transformFn(mockTransformParams); - - expect(configTransformers.number).toHaveBeenCalledWith('42'); - expect(result1).toBe(42); - - (configTransformers.number as jest.Mock).mockReturnValueOnce(undefined); - // @ts-expect-error legacy noImplicitAny - const result2 = transformFn(mockTransformParams); - - expect(result2).toBe('42'); - }); - }); - - describe('string type', () => { - it('should apply string validator', () => { - applyBasicValidators( - ConfigVariableType.STRING, - mockTarget, - mockPropertyKey, - ); - - expect(IsString).toHaveBeenCalled(); - expect(Transform).not.toHaveBeenCalled(); - }); - }); - - describe('enum type', () => { - it('should apply enum validator with string array options', () => { - const enumOptions = ['option1', 'option2', 'option3']; - - applyBasicValidators( - ConfigVariableType.ENUM, - mockTarget, - mockPropertyKey, - enumOptions, - ); - - expect(IsEnum).toHaveBeenCalledWith(enumOptions); - expect(Transform).not.toHaveBeenCalled(); - }); - - it('should apply enum validator with enum object options', () => { - enum TestEnum { - Option1 = 'value1', - Option2 = 'value2', - Option3 = 'value3', - } - - jest.mock( - 'src/engine/core-modules/twenty-config/utils/type-transformers.registry', - () => ({ - typeTransformers: { - enum: { - getValidators: jest.fn().mockImplementation((options) => { - if (options && Object.keys(options).length > 0) { - return [IsEnum(options)]; - } - - return []; - }), - getTransformers: jest.fn().mockReturnValue([]), - }, - }, - }), - { virtual: true }, - ); - - applyBasicValidators( - ConfigVariableType.ENUM, - mockTarget, - mockPropertyKey, - TestEnum, - ); - - expect(IsEnum).toHaveBeenCalledWith(TestEnum); - expect(Transform).not.toHaveBeenCalled(); - }); - - it('should not apply enum validator without options', () => { - applyBasicValidators( - ConfigVariableType.ENUM, - mockTarget, - mockPropertyKey, - ); - - expect(IsEnum).not.toHaveBeenCalled(); - expect(Transform).not.toHaveBeenCalled(); - }); - }); - - describe('array type', () => { - it('should apply array validator', () => { - applyBasicValidators( - ConfigVariableType.ARRAY, - mockTarget, - mockPropertyKey, - ); - - expect(IsArray).toHaveBeenCalled(); - expect(Transform).not.toHaveBeenCalled(); - }); - }); - - describe('unsupported type', () => { - it('should throw error for unsupported types', () => { - expect(() => { - applyBasicValidators('unsupported' as any, mockTarget, mockPropertyKey); - }).toThrow('Unsupported config variable type: unsupported'); - }); - }); -}); diff --git a/packages/twenty-server/src/engine/metadata-modules/ai-router/ai-router.service.ts b/packages/twenty-server/src/engine/metadata-modules/ai-router/ai-router.service.ts index 3d10b94831..18576dcf31 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai-router/ai-router.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai-router/ai-router.service.ts @@ -15,9 +15,9 @@ import { AI_TELEMETRY_CONFIG } from 'src/engine/core-modules/ai/constants/ai-tel import { AiModelRegistryService } from 'src/engine/core-modules/ai/services/ai-model-registry.service'; import { AgentEntity } from 'src/engine/metadata-modules/agent/agent.entity'; import { isWorkflowRelatedObject } from 'src/engine/metadata-modules/agent/utils/is-workflow-related-object.util'; -import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service'; import { DATA_MANIPULATOR_AGENT } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-agents/agents/data-manipulator-agent'; import { HELPER_AGENT } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-agents/agents/helper-agent'; +import { ObjectMetadataServiceV2 } from 'src/engine/metadata-modules/object-metadata/object-metadata-v2.service'; import { type ToolHints } from './types/tool-hints.interface'; @@ -47,7 +47,7 @@ export class AiRouterService { @InjectRepository(AgentEntity) private readonly agentRepository: Repository, private readonly aiModelRegistryService: AiModelRegistryService, - private readonly objectMetadataService: ObjectMetadataService, + private readonly objectMetadataService: ObjectMetadataServiceV2, ) {} // Routes a user message to the most appropriate agent diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.module.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.module.ts index 555fbab9ce..e13cd8fc10 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.module.ts @@ -14,11 +14,9 @@ import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; import { FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto'; import { FieldMetadataResolver } from 'src/engine/metadata-modules/field-metadata/field-metadata.resolver'; -import { BeforeUpdateOneField } from 'src/engine/metadata-modules/field-metadata/hooks/before-update-one-field.hook'; import { FieldMetadataGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/field-metadata/interceptors/field-metadata-graphql-api-exception.interceptor'; import { FieldMetadataEnumValidationService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata-enum-validation.service'; import { FieldMetadataMorphRelationService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata-morph-relation.service'; -import { FieldMetadataRelatedRecordsService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata-related-records.service'; import { FieldMetadataRelationService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata-relation.service'; import { FieldMetadataValidationService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata-validation.service'; import { FieldMetadataServiceV2 } from 'src/engine/metadata-modules/field-metadata/services/field-metadata.service-v2'; @@ -45,7 +43,6 @@ import { FieldMetadataEntity } from './field-metadata.entity'; import { CreateFieldInput } from './dtos/create-field.input'; import { UpdateFieldInput } from './dtos/update-field.input'; -import { FieldMetadataService } from './services/field-metadata.service'; @Module({ imports: [ @@ -77,9 +74,7 @@ import { FieldMetadataService } from './services/field-metadata.service'; ], services: [ IsFieldMetadataDefaultValue, - FieldMetadataService, FieldMetadataServiceV2, - FieldMetadataRelatedRecordsService, FieldMetadataMorphRelationService, FieldMetadataRelationService, FieldMetadataValidationService, @@ -91,20 +86,16 @@ import { FieldMetadataService } from './services/field-metadata.service'; DTOClass: FieldMetadataDTO, CreateDTOClass: CreateFieldInput, UpdateDTOClass: UpdateFieldInput, - ServiceClass: FieldMetadataService, + ServiceClass: FieldMetadataServiceV2, pagingStrategy: PagingStrategies.CURSOR, read: { defaultSort: [{ field: 'id', direction: SortDirection.DESC }], }, create: { - // Manually created because of the async validation - one: { disabled: true }, - many: { disabled: true }, + disabled: true, }, update: { - // Manually created because of the async validation - one: { disabled: true }, - many: { disabled: true }, + disabled: true, }, delete: { disabled: true }, guards: [WorkspaceAuthGuard], @@ -116,22 +107,17 @@ import { FieldMetadataService } from './services/field-metadata.service'; providers: [ IsFieldMetadataDefaultValue, IsFieldMetadataOptions, - FieldMetadataService, FieldMetadataServiceV2, FieldMetadataRelationService, - FieldMetadataRelatedRecordsService, FieldMetadataMorphRelationService, FieldMetadataValidationService, FieldMetadataEnumValidationService, FieldMetadataResolver, - BeforeUpdateOneField, ], exports: [ - FieldMetadataService, FieldMetadataServiceV2, FieldMetadataRelationService, FieldMetadataMorphRelationService, - FieldMetadataRelatedRecordsService, FieldMetadataEnumValidationService, FieldMetadataValidationService, ], diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.resolver.ts index 47427fe67d..9e96408516 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.resolver.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.resolver.ts @@ -10,14 +10,9 @@ 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 { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter'; import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe'; -import { - ForbiddenError, - ValidationError, -} from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; +import { ForbiddenError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; import { I18nService } from 'src/engine/core-modules/i18n/i18n.service'; import { I18nContext } from 'src/engine/core-modules/i18n/types/i18n-context.type'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; @@ -29,14 +24,10 @@ import { CreateOneFieldMetadataInput } from 'src/engine/metadata-modules/field-m import { DeleteOneFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/delete-field.input'; import { FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto'; import { RelationDTO } from 'src/engine/metadata-modules/field-metadata/dtos/relation.dto'; -import { - UpdateOneFieldMetadataInput, - type UpdateFieldInput, -} from 'src/engine/metadata-modules/field-metadata/dtos/update-field.input'; -import { BeforeUpdateOneField } from 'src/engine/metadata-modules/field-metadata/hooks/before-update-one-field.hook'; -import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata.service'; +import { UpdateOneFieldMetadataInput } from 'src/engine/metadata-modules/field-metadata/dtos/update-field.input'; import { FieldMetadataServiceV2 } from 'src/engine/metadata-modules/field-metadata/services/field-metadata.service-v2'; import { fieldMetadataGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/field-metadata/utils/field-metadata-graphql-api-exception-handler.util'; +import { fromFlatFieldMetadataToFieldMetadataDto } from 'src/engine/metadata-modules/flat-field-metadata/utils/from-flat-field-metadata-to-field-metadata-dto.util'; import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants'; import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter'; @@ -49,9 +40,6 @@ import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-module ) export class FieldMetadataResolver { constructor( - private readonly fieldMetadataService: FieldMetadataService, - private readonly beforeUpdateOneField: BeforeUpdateOneField, - private readonly featureFlagService: FeatureFlagService, private readonly fieldMetadataServiceV2: FieldMetadataServiceV2, private readonly i18nService: I18nService, ) {} @@ -64,10 +52,13 @@ export class FieldMetadataResolver { @Context() context: I18nContext, ) { try { - return await this.fieldMetadataService.createOne({ - ...input.field, - workspaceId, - }); + const flatFieldMetadata = + await this.fieldMetadataServiceV2.createOneField({ + createFieldInput: input.field, + workspaceId, + }); + + return fromFlatFieldMetadataToFieldMetadataDto(flatFieldMetadata); } catch (error) { return fieldMetadataGraphqlApiExceptionHandler( error, @@ -83,29 +74,14 @@ export class FieldMetadataResolver { @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, @Context() context: I18nContext, ) { - const isWorkspaceMigrationV2Enabled = - await this.featureFlagService.isFeatureEnabled( - FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED, - workspaceId, - ); - try { - if (isWorkspaceMigrationV2Enabled) { - return await this.fieldMetadataServiceV2.updateOne({ + const flatFieldMetadata = + await this.fieldMetadataServiceV2.updateOneField({ updateFieldInput: { ...input.update, id: input.id }, workspaceId, }); - } - const updatedInput = (await this.beforeUpdateOneField.run(input, { - workspaceId, - locale: context.req.locale, - })) as UpdateOneFieldMetadataInput; - - return await this.fieldMetadataService.updateOne(updatedInput.id, { - ...updatedInput.update, - workspaceId, - }); + return fromFlatFieldMetadataToFieldMetadataDto(flatFieldMetadata); } catch (error) { fieldMetadataGraphqlApiExceptionHandler( error, @@ -117,7 +93,7 @@ export class FieldMetadataResolver { @UseGuards(SettingsPermissionGuard(PermissionFlagType.DATA_MODEL)) @Mutation(() => FieldMetadataDTO) async deleteOneField( - @Args('input') input: DeleteOneFieldInput, + @Args('input') deleteOneFieldInput: DeleteOneFieldInput, @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, @Context() context: I18nContext, ) { @@ -126,46 +102,13 @@ export class FieldMetadataResolver { } try { - const isWorkspaceMigrationV2Enabled = - await this.featureFlagService.isFeatureEnabled( - FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED, - workspaceId, - ); - - if (isWorkspaceMigrationV2Enabled) { - return await this.fieldMetadataServiceV2.deleteOneField({ - deleteOneFieldInput: input, + const flatFieldMetadata = + await this.fieldMetadataServiceV2.deleteOneField({ + deleteOneFieldInput, workspaceId, }); - } - } catch (error) { - fieldMetadataGraphqlApiExceptionHandler( - error, - this.i18nService.getI18nInstance(context.req.locale), - ); - } - const fieldMetadata = - await this.fieldMetadataService.findOneWithinWorkspace(workspaceId, { - where: { - id: input.id.toString(), - }, - }); - - if (!isDefined(fieldMetadata)) { - throw new ValidationError('Field does not exist'); - } - - if (!fieldMetadata.isCustom) { - throw new ValidationError("Standard Fields can't be deleted"); - } - - if (fieldMetadata.isActive) { - throw new ValidationError("Active fields can't be deleted"); - } - - try { - return await this.fieldMetadataService.deleteOneField(input, workspaceId); + return fromFlatFieldMetadataToFieldMetadataDto(flatFieldMetadata); } catch (error) { fieldMetadataGraphqlApiExceptionHandler( error, @@ -177,7 +120,6 @@ export class FieldMetadataResolver { @ResolveField(() => RelationDTO, { nullable: true }) async relation( @AuthWorkspace() workspace: WorkspaceEntity, - @Parent() { id: fieldMetadataId, diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/hooks/__tests__/before-update-one-field.hook.spec.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/hooks/__tests__/before-update-one-field.hook.spec.ts deleted file mode 100644 index 9022a65cb7..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/hooks/__tests__/before-update-one-field.hook.spec.ts +++ /dev/null @@ -1,684 +0,0 @@ -import { Test, type TestingModule } from '@nestjs/testing'; - -import { type UpdateOneInputType } from '@ptc-org/nestjs-query-graphql'; -import { FieldMetadataType } from 'twenty-shared/types'; - -import { - ForbiddenError, - ValidationError, -} from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; -import { type UpdateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/update-field.input'; -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { BeforeUpdateOneField } from 'src/engine/metadata-modules/field-metadata/hooks/before-update-one-field.hook'; -import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata.service'; -import { type IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service'; -import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock'; -import { I18nService } from 'src/engine/core-modules/i18n/i18n.service'; - -// Create a type that omits id and workspaceId from UpdateFieldInput -type UpdateFieldInputForTest = Omit; - -describe('BeforeUpdateOneField', () => { - let hook: BeforeUpdateOneField; - let fieldMetadataService: FieldMetadataService; - let objectMetadataService: ObjectMetadataService; - - const mockWorkspaceId = 'workspace-id'; - const mockFieldId = 'field-id'; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - BeforeUpdateOneField, - { - provide: FieldMetadataService, - useValue: { - findOneWithinWorkspace: jest.fn(), - }, - }, - { - provide: ObjectMetadataService, - useValue: { - findOneWithinWorkspace: jest.fn(), - }, - }, - { - provide: I18nService, - useValue: { - getI18nInstance: jest.fn().mockReturnValue({ - _: jest.fn().mockReturnValue('mocked-translation'), - }), - }, - }, - ], - }).compile(); - - hook = - module.get>(BeforeUpdateOneField); - fieldMetadataService = - module.get(FieldMetadataService); - objectMetadataService = module.get( - ObjectMetadataService, - ); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - it('should throw ForbiddenError if workspaceId is not provided', async () => { - const instance: UpdateOneInputType = { - id: mockFieldId, - update: { - isActive: true, - }, - }; - - await expect( - hook.run(instance as UpdateOneInputType, { - workspaceId: '', - locale: undefined, - }), - ).rejects.toThrow(ForbiddenError); - }); - - it('should throw ValidationError if field does not exist', async () => { - const instance: UpdateOneInputType = { - id: mockFieldId, - update: { - isActive: true, - }, - }; - - jest - .spyOn(fieldMetadataService, 'findOneWithinWorkspace') - .mockResolvedValue(null as unknown as FieldMetadataEntity); - - await expect( - hook.run(instance as UpdateOneInputType, { - workspaceId: mockWorkspaceId, - locale: undefined, - }), - ).rejects.toThrow(ValidationError); - }); - - it('should not affect custom fields', async () => { - const instance: UpdateOneInputType = { - id: mockFieldId, - update: { - isActive: true, - name: 'newName', // Custom fields can update all properties - }, - }; - - const mockField = getMockFieldMetadataEntity({ - workspaceId: mockWorkspaceId, - objectMetadataId: '20202020-0000-0000-0000-000000000002', - id: mockFieldId, - type: FieldMetadataType.TEXT, - name: 'oldName', - label: 'Old Name', - isNullable: true, - isCustom: true, - isLabelSyncedWithName: true, - createdAt: new Date(), - updatedAt: new Date(), - }); - - jest - .spyOn(fieldMetadataService, 'findOneWithinWorkspace') - .mockResolvedValue(mockField); - - const result = await hook.run( - instance as UpdateOneInputType, - { - workspaceId: mockWorkspaceId, - locale: undefined, - }, - ); - - expect(result).toEqual(instance); - }); - - it('should throw ValidationError when trying to update non-updatable fields on standard fields', async () => { - const instance: UpdateOneInputType = { - id: mockFieldId, - update: { - name: 'newName', // Not allowed for standard fields - }, - }; - - const mockField = getMockFieldMetadataEntity({ - workspaceId: mockWorkspaceId, - objectMetadataId: '20202020-0000-0000-0000-000000000002', - id: mockFieldId, - type: FieldMetadataType.TEXT, - name: 'oldName', - label: 'Old Name', - isNullable: true, - isCustom: false, - isLabelSyncedWithName: true, - createdAt: new Date(), - updatedAt: new Date(), - }); - - jest - .spyOn(fieldMetadataService, 'findOneWithinWorkspace') - .mockResolvedValue(mockField); - - await expect( - hook.run(instance as UpdateOneInputType, { - workspaceId: mockWorkspaceId, - locale: undefined, - }), - ).rejects.toThrow(ValidationError); - }); - - it('should handle isActive updates for standard fields', async () => { - const instance: UpdateOneInputType = { - id: mockFieldId, - update: { - isActive: false, - }, - }; - - const mockField = getMockFieldMetadataEntity({ - workspaceId: mockWorkspaceId, - objectMetadataId: '20202020-0000-0000-0000-000000000002', - id: mockFieldId, - type: FieldMetadataType.TEXT, - name: 'oldName', - label: 'Old Name', - isNullable: true, - isCustom: false, - isActive: true, - isLabelSyncedWithName: true, - createdAt: new Date(), - updatedAt: new Date(), - }); - - jest - .spyOn(fieldMetadataService, 'findOneWithinWorkspace') - .mockResolvedValue(mockField); - - const result = await hook.run( - instance as UpdateOneInputType, - { - workspaceId: mockWorkspaceId, - locale: undefined, - }, - ); - - const expectedResult = { - id: mockFieldId, - update: { - isActive: false, - standardOverrides: {}, - }, - }; - - expect(result).toEqual(expectedResult); - }); - - it('should handle isLabelSyncedWithName updates for standard fields', async () => { - const instance: UpdateOneInputType = { - id: mockFieldId, - update: { - isLabelSyncedWithName: true, - }, - }; - - const mockField = getMockFieldMetadataEntity({ - workspaceId: mockWorkspaceId, - objectMetadataId: '20202020-0000-0000-0000-000000000002', - id: mockFieldId, - type: FieldMetadataType.TEXT, - name: 'oldName', - label: 'Old Name', - isNullable: true, - isCustom: false, - isLabelSyncedWithName: false, - standardOverrides: { - label: 'Custom Label', - }, - createdAt: new Date(), - updatedAt: new Date(), - }); - - jest - .spyOn(fieldMetadataService, 'findOneWithinWorkspace') - .mockResolvedValue(mockField); - - const result = await hook.run( - instance as UpdateOneInputType, - { - workspaceId: mockWorkspaceId, - locale: undefined, - }, - ); - - const expectedResult = { - id: mockFieldId, - update: { - isLabelSyncedWithName: true, - standardOverrides: { - label: null, - }, - }, - }; - - expect(result).toEqual(expectedResult); - }); - - it('should handle label override when not synced with name', async () => { - const instance: UpdateOneInputType = { - id: mockFieldId, - update: { - label: 'New Label', - }, - }; - - const mockField = getMockFieldMetadataEntity({ - workspaceId: mockWorkspaceId, - objectMetadataId: '20202020-0000-0000-0000-000000000002', - id: mockFieldId, - type: FieldMetadataType.TEXT, - name: 'oldName', - label: 'Default Label', - isNullable: true, - isCustom: false, - isLabelSyncedWithName: false, - createdAt: new Date(), - updatedAt: new Date(), - }); - - jest - .spyOn(fieldMetadataService, 'findOneWithinWorkspace') - .mockResolvedValue(mockField); - - const result = await hook.run( - instance as UpdateOneInputType, - { - workspaceId: mockWorkspaceId, - locale: undefined, - }, - ); - - const expectedResult = { - id: mockFieldId, - update: { - standardOverrides: { - label: 'New Label', - }, - }, - }; - - expect(result).toEqual(expectedResult); - }); - - it('should reset label override when it matches the original value', async () => { - const instance: UpdateOneInputType = { - id: mockFieldId, - update: { - label: 'Default Label', - }, - }; - - const mockField: Partial = { - id: mockFieldId, - isCustom: false, - isLabelSyncedWithName: false, - label: 'Default Label', - standardOverrides: { - label: 'Custom Label', - }, - }; - - jest - .spyOn(fieldMetadataService, 'findOneWithinWorkspace') - .mockResolvedValue(mockField as FieldMetadataEntity); - - const result = await hook.run( - instance as UpdateOneInputType, - { - workspaceId: mockWorkspaceId, - locale: undefined, - }, - ); - - const expectedResult = { - id: mockFieldId, - update: { - standardOverrides: { - label: null, - }, - }, - }; - - expect(result).toEqual(expectedResult); - }); - - it('should handle description override', async () => { - const instance: UpdateOneInputType = { - id: mockFieldId, - update: { - description: 'New Description', - }, - }; - - const mockField: Partial = { - id: mockFieldId, - isCustom: false, - description: 'Default Description', - }; - - jest - .spyOn(fieldMetadataService, 'findOneWithinWorkspace') - .mockResolvedValue(mockField as FieldMetadataEntity); - - const result = await hook.run( - instance as UpdateOneInputType, - { - workspaceId: mockWorkspaceId, - locale: undefined, - }, - ); - - const expectedResult = { - id: mockFieldId, - update: { - standardOverrides: { - description: 'New Description', - }, - }, - }; - - expect(result).toEqual(expectedResult); - }); - - it('should handle icon override', async () => { - const instance: UpdateOneInputType = { - id: mockFieldId, - update: { - icon: 'IconStar', - }, - }; - - const mockField: Partial = { - id: mockFieldId, - isCustom: false, - icon: 'IconCircle', - }; - - jest - .spyOn(fieldMetadataService, 'findOneWithinWorkspace') - .mockResolvedValue(mockField as FieldMetadataEntity); - - const result = await hook.run( - instance as UpdateOneInputType, - { - workspaceId: mockWorkspaceId, - locale: undefined, - }, - ); - - const expectedResult = { - id: mockFieldId, - update: { - standardOverrides: { - icon: 'IconStar', - }, - }, - }; - - expect(result).toEqual(expectedResult); - }); - - it('should handle locale-specific translations', async () => { - const instance: UpdateOneInputType = { - id: mockFieldId, - update: { - label: 'Étiquette', - description: 'Description en français', - }, - }; - - const mockField: Partial = { - id: mockFieldId, - isCustom: false, - isLabelSyncedWithName: false, - label: 'Label', - description: 'Description', - }; - - jest - .spyOn(fieldMetadataService, 'findOneWithinWorkspace') - .mockResolvedValue(mockField as FieldMetadataEntity); - - const result = await hook.run( - instance as UpdateOneInputType, - { - workspaceId: mockWorkspaceId, - locale: 'fr-FR', - }, - ); - - // The expected result returned by the hook - const expectedResult = { - id: mockFieldId, - update: { - standardOverrides: { - translations: { - 'fr-FR': { - label: 'Étiquette', - description: 'Description en français', - }, - }, - }, - }, - }; - - expect(result).toEqual(expectedResult); - }); - - it('should reset locale-specific translations when they match translated defaults', async () => { - const translatedLabel = 'mocked-translation'; - - const instance: UpdateOneInputType = { - id: mockFieldId, - update: { - label: translatedLabel, - }, - }; - - const mockField: Partial = { - id: mockFieldId, - isCustom: false, - isLabelSyncedWithName: false, - label: 'Label', - standardOverrides: { - translations: { - 'fr-FR': { - label: 'Ancienne Étiquette', - }, - }, - }, - }; - - jest - .spyOn(fieldMetadataService, 'findOneWithinWorkspace') - .mockResolvedValue(mockField as FieldMetadataEntity); - - const result = await hook.run( - instance as UpdateOneInputType, - { - workspaceId: mockWorkspaceId, - locale: 'fr-FR', - }, - ); - - const expectedResult = { - id: mockFieldId, - update: { - standardOverrides: { - translations: { - 'fr-FR': { - label: null, - }, - }, - }, - }, - }; - - expect(result).toEqual(expectedResult); - }); - - it('should handle multiple updates together', async () => { - const instance: UpdateOneInputType = { - id: mockFieldId, - update: { - isActive: false, - isLabelSyncedWithName: false, - label: 'New Label', - icon: 'IconStar', - description: 'New Description', - }, - }; - - const mockField: Partial = { - id: mockFieldId, - isCustom: false, - isActive: true, - isLabelSyncedWithName: false, - label: 'Default Label', - icon: 'IconCircle', - description: 'Default Description', - }; - - jest - .spyOn(fieldMetadataService, 'findOneWithinWorkspace') - .mockResolvedValue(mockField as FieldMetadataEntity); - - const result = await hook.run( - instance as UpdateOneInputType, - { - workspaceId: mockWorkspaceId, - locale: undefined, - }, - ); - - const expectedResult = { - id: mockFieldId, - update: { - isActive: false, - isLabelSyncedWithName: false, - standardOverrides: { - label: 'New Label', - icon: 'IconStar', - description: 'New Description', - }, - }, - }; - - expect(result).toEqual(expectedResult); - }); - - it('should throw ValidationError if isUnique is updated for a standard field with a standard unique index', async () => { - const mockField: Partial = { - id: mockFieldId, - isCustom: false, - isUnique: true, - }; - - jest - .spyOn(fieldMetadataService, 'findOneWithinWorkspace') - .mockResolvedValue(mockField as FieldMetadataEntity); - - const mockObjectMetadata: Partial = { - id: mockWorkspaceId, - indexMetadatas: [ - { - isUnique: true, - isCustom: false, - indexFieldMetadatas: [{ fieldMetadataId: mockFieldId }], - } as IndexMetadataEntity, - ], - }; - - jest - .spyOn(objectMetadataService, 'findOneWithinWorkspace') - .mockResolvedValue(mockObjectMetadata as ObjectMetadataEntity); - - const instance: UpdateOneInputType = { - id: mockFieldId, - update: { - isUnique: false, - }, - }; - - await expect( - hook.run(instance as UpdateOneInputType, { - workspaceId: mockWorkspaceId, - locale: undefined, - }), - ).rejects.toThrow(ValidationError); - }); - - it('should not throw ValidationError if isUnique is updated for a standard field without a standard unique index', async () => { - const mockField: Partial = { - id: mockFieldId, - isCustom: false, - isUnique: true, - }; - - jest - .spyOn(fieldMetadataService, 'findOneWithinWorkspace') - .mockResolvedValue(mockField as FieldMetadataEntity); - - const mockObjectMetadata: Partial = { - id: mockWorkspaceId, - indexMetadatas: [ - { - isUnique: false, - isCustom: false, - indexFieldMetadatas: [{ fieldMetadataId: mockFieldId }], - } as IndexMetadataEntity, - ], - }; - - jest - .spyOn(objectMetadataService, 'findOneWithinWorkspace') - .mockResolvedValue(mockObjectMetadata as ObjectMetadataEntity); - - const instance: UpdateOneInputType = { - id: mockFieldId, - update: { - isUnique: false, - }, - }; - - jest - .spyOn(fieldMetadataService, 'findOneWithinWorkspace') - .mockResolvedValue(mockField as FieldMetadataEntity); - - const result = await hook.run( - instance as UpdateOneInputType, - { - workspaceId: mockWorkspaceId, - locale: undefined, - }, - ); - - const expectedResult = { - id: mockFieldId, - update: { - isUnique: false, - standardOverrides: {}, - }, - }; - - expect(result).toEqual(expectedResult); - }); -}); diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/hooks/before-update-one-field.hook.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/hooks/before-update-one-field.hook.ts deleted file mode 100644 index b0e944a4e1..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/hooks/before-update-one-field.hook.ts +++ /dev/null @@ -1,448 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { - type BeforeUpdateOneHook, - type UpdateOneInputType, -} from '@ptc-org/nestjs-query-graphql'; -import { SOURCE_LOCALE, type APP_LOCALES } from 'twenty-shared/translations'; -import { isDefined } from 'twenty-shared/utils'; - -import { - ForbiddenError, - ValidationError, -} from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; -import { generateMessageId } from 'src/engine/core-modules/i18n/utils/generateMessageId'; -import { I18nService } from 'src/engine/core-modules/i18n/i18n.service'; -import { type FieldStandardOverridesDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-standard-overrides.dto'; -import { type UpdateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/update-field.input'; -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata.service'; -import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service'; - -interface StandardFieldUpdate extends Partial { - standardOverrides?: FieldStandardOverridesDTO; -} - -@Injectable() -export class BeforeUpdateOneField - implements BeforeUpdateOneHook -{ - constructor( - readonly fieldMetadataService: FieldMetadataService, - readonly objectMetadataService: ObjectMetadataService, - private readonly i18nService: I18nService, - ) {} - - async run( - instance: UpdateOneInputType, - { - workspaceId, - locale, - }: { - workspaceId: string; - locale: keyof typeof APP_LOCALES | undefined; - }, - ): Promise> { - if (!workspaceId) { - throw new ForbiddenError('Could not retrieve workspace ID'); - } - - const fieldMetadata = await this.getFieldMetadata(instance, workspaceId); - - if (!fieldMetadata.isCustom) { - return await this.handleStandardFieldUpdate( - instance, - fieldMetadata, - locale, - ); - } - - return instance; - } - - private async getFieldMetadata( - instance: UpdateOneInputType, - workspaceId: string, - ) { - const fieldMetadata = - await this.fieldMetadataService.findOneWithinWorkspace(workspaceId, { - where: { - id: instance.id.toString(), - }, - }); - - if (!fieldMetadata) { - throw new ValidationError('Field does not exist'); - } - - return fieldMetadata; - } - - private async handleStandardFieldUpdate( - instance: UpdateOneInputType, - fieldMetadata: FieldMetadataEntity, - locale?: keyof typeof APP_LOCALES, - ): Promise> { - const update: StandardFieldUpdate = {}; - const updatableFields = [ - 'isActive', - 'isLabelSyncedWithName', - 'options', - 'settings', - 'defaultValue', - 'isUnique', - ]; - const overridableFields = ['label', 'icon', 'description']; - - const nonUpdatableFields = Object.keys(instance.update).filter( - (key) => - !updatableFields.includes(key) && !overridableFields.includes(key), - ); - - if (nonUpdatableFields.length > 0) { - throw new ValidationError( - `Only isActive, isLabelSyncedWithName, label, icon, description, isUnique and defaultValue fields can be updated for standard fields. Invalid fields: ${nonUpdatableFields.join(', ')}`, - ); - } - - // Preserve existing overrides - update.standardOverrides = fieldMetadata.standardOverrides - ? { ...fieldMetadata.standardOverrides } - : {}; - - this.handleActiveField(instance, update); - this.handleLabelSyncedWithNameField(instance, update); - this.handleStandardOverrides(instance, fieldMetadata, update, locale); - this.handleOptionsField(instance, update); - this.handleSettingsField(instance, update); - this.handleDefaultValueField(instance, update); - await this.handleIsUniqueField(instance, fieldMetadata, update); - - return { - id: instance.id, - update: update as T, - }; - } - - private async handleIsUniqueField( - instance: UpdateOneInputType, - fieldMetadata: FieldMetadataEntity, - update: StandardFieldUpdate, - ) { - if (!isDefined(instance.update.isUnique)) { - return; - } - - const objectMetadata = - await this.objectMetadataService.findOneWithinWorkspace( - fieldMetadata.workspaceId, - { - where: { - id: fieldMetadata.objectMetadataId, - }, - }, - ); - - const hasStandardUniqueIndex = objectMetadata?.indexMetadatas.some( - (index) => - index.isUnique && - !index.isCustom && - index.indexFieldMetadatas?.some( - (field) => field.fieldMetadataId === fieldMetadata.id, - ), - ); - - if (hasStandardUniqueIndex && instance.update.isUnique === false) { - throw new ValidationError( - 'Unique standard field cannot be updated to non-unique.', - ); - } - - update.isUnique = instance.update.isUnique; - } - - private handleDefaultValueField( - instance: UpdateOneInputType, - update: StandardFieldUpdate, - ): void { - if (!isDefined(instance.update.defaultValue)) { - return; - } - - update.defaultValue = instance.update.defaultValue; - } - - private handleOptionsField( - instance: UpdateOneInputType, - update: StandardFieldUpdate, - ): void { - if (!isDefined(instance.update.options)) { - return; - } - - update.options = instance.update.options; - } - - private handleSettingsField( - instance: UpdateOneInputType, - update: StandardFieldUpdate, - ): void { - if (!isDefined(instance.update.settings)) { - return; - } - - update.settings = instance.update.settings; - } - - private handleActiveField( - instance: UpdateOneInputType, - update: StandardFieldUpdate, - ): void { - if (!isDefined(instance.update.isActive)) { - return; - } - - update.isActive = instance.update.isActive; - } - - private handleLabelSyncedWithNameField( - instance: UpdateOneInputType, - update: StandardFieldUpdate, - ): void { - if (!isDefined(instance.update.isLabelSyncedWithName)) { - return; - } - - update.isLabelSyncedWithName = instance.update.isLabelSyncedWithName; - - if (instance.update.isLabelSyncedWithName === false) { - return; - } - - update.standardOverrides = update.standardOverrides || {}; - update.standardOverrides.label = null; - } - - private handleStandardOverrides( - instance: UpdateOneInputType, - fieldMetadata: FieldMetadataEntity, - update: StandardFieldUpdate, - locale?: keyof typeof APP_LOCALES, - ): void { - const hasStandardOverrides = - isDefined(instance.update.description) || - isDefined(instance.update.icon) || - isDefined(instance.update.label); - - if (!hasStandardOverrides) { - return; - } - - update.standardOverrides = update.standardOverrides || {}; - - this.handleDescriptionOverride(instance, fieldMetadata, update, locale); - this.handleIconOverride(instance, fieldMetadata, update); - this.handleLabelOverride(instance, fieldMetadata, update, locale); - } - - private resetOverrideIfMatchesOriginal({ - update, - overrideKey, - newValue, - originalValue, - locale, - }: { - update: StandardFieldUpdate; - overrideKey: 'label' | 'description' | 'icon'; - newValue: string; - originalValue: string | null; - locale?: keyof typeof APP_LOCALES | undefined; - }): boolean { - // Handle localized overrides - if (locale && locale !== SOURCE_LOCALE) { - const wasOverrideReset = this.resetLocalizedOverride( - update, - overrideKey, - newValue, - originalValue, - locale, - ); - - return wasOverrideReset; - } - - // Handle default language overrides - const wasOverrideReset = this.resetDefaultOverride( - update, - overrideKey, - newValue, - originalValue, - ); - - return wasOverrideReset; - } - - private resetLocalizedOverride( - update: StandardFieldUpdate, - overrideKey: 'label' | 'description' | 'icon', - newValue: string, - originalValue: string | null, - locale: keyof typeof APP_LOCALES, - ): boolean { - const messageId = generateMessageId(originalValue ?? ''); - const i18n = this.i18nService.getI18nInstance(locale); - const translatedMessage = i18n._(messageId); - - if (newValue !== translatedMessage) { - return false; - } - - // Initialize the translations structure if needed - update.standardOverrides = update.standardOverrides || {}; - update.standardOverrides.translations = - update.standardOverrides.translations || {}; - update.standardOverrides.translations[locale] = - update.standardOverrides.translations[locale] || {}; - - // Reset the override by setting it to null - const localeTranslations = update.standardOverrides.translations[locale]; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (localeTranslations as Record)[overrideKey] = null; - - return true; - } - - private resetDefaultOverride( - update: StandardFieldUpdate, - overrideKey: 'label' | 'description' | 'icon', - newValue: string, - originalValue: string | null, - ): boolean { - if (newValue !== originalValue) { - return false; - } - - update.standardOverrides = update.standardOverrides || {}; - update.standardOverrides[overrideKey] = null; - - return true; - } - - private setOverrideValue( - update: StandardFieldUpdate, - overrideKey: 'label' | 'description' | 'icon', - value: string, - locale?: keyof typeof APP_LOCALES, - ): void { - update.standardOverrides = update.standardOverrides || {}; - - const shouldSetLocalizedOverride = - locale && locale !== SOURCE_LOCALE && overrideKey !== 'icon'; - - if (!shouldSetLocalizedOverride) { - update.standardOverrides[overrideKey] = value; - - return; - } - - this.setLocalizedOverrideValue(update, overrideKey, value, locale); - } - - private setLocalizedOverrideValue( - update: StandardFieldUpdate, - overrideKey: 'label' | 'description' | 'icon', - value: string, - locale: keyof typeof APP_LOCALES, - ): void { - update.standardOverrides = update.standardOverrides || {}; - update.standardOverrides.translations = - update.standardOverrides.translations || {}; - update.standardOverrides.translations[locale] = - update.standardOverrides.translations[locale] || {}; - - const localeTranslations = update.standardOverrides.translations[locale]; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (localeTranslations as Record)[overrideKey] = value; - } - - private handleDescriptionOverride( - instance: UpdateOneInputType, - fieldMetadata: FieldMetadataEntity, - update: StandardFieldUpdate, - locale?: keyof typeof APP_LOCALES, - ): void { - if (!isDefined(instance.update.description)) { - return; - } - - if ( - this.resetOverrideIfMatchesOriginal({ - update, - overrideKey: 'description', - newValue: instance.update.description, - originalValue: fieldMetadata.description, - locale, - }) - ) { - return; - } - - this.setOverrideValue( - update, - 'description', - instance.update.description, - locale, - ); - } - - private handleIconOverride( - instance: UpdateOneInputType, - fieldMetadata: FieldMetadataEntity, - update: StandardFieldUpdate, - ): void { - if (!isDefined(instance.update.icon)) { - return; - } - - if ( - this.resetOverrideIfMatchesOriginal({ - update, - overrideKey: 'icon', - newValue: instance.update.icon, - originalValue: fieldMetadata.icon, - locale: undefined, - }) - ) { - return; - } - - this.setOverrideValue(update, 'icon', instance.update.icon); - } - - private handleLabelOverride( - instance: UpdateOneInputType, - fieldMetadata: FieldMetadataEntity, - update: StandardFieldUpdate, - locale?: keyof typeof APP_LOCALES, - ): void { - if (!isDefined(instance.update.label)) { - return; - } - - if ( - this.resetOverrideIfMatchesOriginal({ - update, - overrideKey: 'label', - newValue: instance.update.label, - originalValue: fieldMetadata.label, - locale, - }) - ) { - return; - } - - this.setOverrideValue(update, 'label', instance.update.label, locale); - } -} diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/__tests__/field-metadata-related-records.spec.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/__tests__/field-metadata-related-records.spec.ts deleted file mode 100644 index 343dea9130..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/__tests__/field-metadata-related-records.spec.ts +++ /dev/null @@ -1,357 +0,0 @@ -import { type EachTestingContext } from 'twenty-shared/testing'; - -import { type FieldMetadataDefaultOption } from 'src/engine/metadata-modules/field-metadata/dtos/options.input'; -import { FieldMetadataRelatedRecordsService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata-related-records.service'; -import { type ViewFieldService } from 'src/engine/metadata-modules/view-field/services/view-field.service'; -import { type ViewFilterService } from 'src/engine/metadata-modules/view-filter/services/view-filter.service'; -import { type ViewGroupService } from 'src/engine/metadata-modules/view-group/services/view-group.service'; -import { type ViewService } from 'src/engine/metadata-modules/view/services/view.service'; - -type GetOptionsDifferencesTestContext = EachTestingContext<{ - oldOptions: FieldMetadataDefaultOption[]; - newOptions: FieldMetadataDefaultOption[]; - compareLabel?: boolean; - expected: { - created: FieldMetadataDefaultOption[]; - updated: { - old: FieldMetadataDefaultOption; - new: FieldMetadataDefaultOption; - }[]; - deleted: FieldMetadataDefaultOption[]; - }; -}>; - -describe('FieldMetadataRelatedRecordsService', () => { - describe('getOptionsDifferences', () => { - let service: FieldMetadataRelatedRecordsService; - let viewService: ViewService = {} as ViewService; - let viewFieldService: ViewFieldService = {} as ViewFieldService; - let viewFilterService: ViewFilterService = {} as ViewFilterService; - let viewGroupService: ViewGroupService = {} as ViewGroupService; - - beforeEach(() => { - service = new FieldMetadataRelatedRecordsService( - viewService, - viewFieldService, - viewFilterService, - viewGroupService, - ); - }); - - const testCases: GetOptionsDifferencesTestContext[] = [ - { - title: 'should identify created options', - context: { - oldOptions: [ - { id: '1', label: 'Option 1', value: 'value1', position: 0 }, - ], - newOptions: [ - { id: '1', label: 'Option 1', value: 'value1', position: 0 }, - { id: '2', label: 'Option 2', value: 'value2', position: 1 }, - ], - expected: { - created: [ - { id: '2', label: 'Option 2', value: 'value2', position: 1 }, - ], - updated: [], - deleted: [], - }, - }, - }, - { - title: 'should identify updated options', - context: { - oldOptions: [ - { id: '1', label: 'Option 1', value: 'value1', position: 0 }, - ], - newOptions: [ - { - id: '1', - label: 'Option 1', - value: 'updated-value1', - position: 0, - }, - ], - expected: { - created: [], - updated: [ - { - old: { - id: '1', - label: 'Option 1', - value: 'value1', - position: 0, - }, - new: { - id: '1', - label: 'Option 1', - value: 'updated-value1', - position: 0, - }, - }, - ], - deleted: [], - }, - }, - }, - { - title: 'should identify deleted options', - context: { - oldOptions: [ - { id: '1', label: 'Option 1', value: 'value1', position: 0 }, - { id: '2', label: 'Option 2', value: 'value2', position: 1 }, - ], - newOptions: [ - { id: '1', label: 'Option 1', value: 'value1', position: 0 }, - ], - expected: { - created: [], - updated: [], - deleted: [ - { id: '2', label: 'Option 2', value: 'value2', position: 1 }, - ], - }, - }, - }, - { - title: 'should identify all types of changes', - context: { - oldOptions: [ - { id: '1', label: 'Option 1', value: 'value1', position: 0 }, - { id: '2', label: 'Option 2', value: 'value2', position: 1 }, - { id: '3', label: 'Option 3', value: 'value3', position: 2 }, - ], - newOptions: [ - { - id: '1', - label: 'Option 1', - value: 'updated-value1', - position: 0, - }, - { id: '3', label: 'Option 3', value: 'value3', position: 1 }, - { id: '4', label: 'Option 4', value: 'value4', position: 2 }, - ], - expected: { - created: [ - { id: '4', label: 'Option 4', value: 'value4', position: 2 }, - ], - updated: [ - { - old: { - id: '1', - label: 'Option 1', - value: 'value1', - position: 0, - }, - new: { - id: '1', - label: 'Option 1', - value: 'updated-value1', - position: 0, - }, - }, - ], - deleted: [ - { id: '2', label: 'Option 2', value: 'value2', position: 1 }, - ], - }, - }, - }, - { - title: 'should handle empty arrays', - context: { - oldOptions: [], - newOptions: [], - expected: { - created: [], - updated: [], - deleted: [], - }, - }, - }, - { - title: 'should handle all new options', - context: { - oldOptions: [], - newOptions: [ - { id: '1', label: 'Option 1', value: 'value1', position: 0 }, - { id: '2', label: 'Option 2', value: 'value2', position: 1 }, - ], - expected: { - created: [ - { id: '1', label: 'Option 1', value: 'value1', position: 0 }, - { id: '2', label: 'Option 2', value: 'value2', position: 1 }, - ], - updated: [], - deleted: [], - }, - }, - }, - { - title: 'should handle all deleted options', - context: { - oldOptions: [ - { id: '1', label: 'Option 1', value: 'value1', position: 0 }, - { id: '2', label: 'Option 2', value: 'value2', position: 1 }, - ], - newOptions: [], - expected: { - created: [], - updated: [], - deleted: [ - { id: '1', label: 'Option 1', value: 'value1', position: 0 }, - { id: '2', label: 'Option 2', value: 'value2', position: 1 }, - ], - }, - }, - }, - { - title: - 'should not consider changes to label as updates when value remains the same', - context: { - oldOptions: [ - { - id: 'f86eaffd-b773-4c9a-957b-86dca4a62731', - label: 'Option 0', - value: 'option0', - position: 1, - }, - { - id: '28d80b3c-79bd-4a1b-a868-9616534de0fa', - label: 'Option 1', - value: 'option1', - position: 2, - }, - { - id: '25a05cd8-256f-4652-9e4a-6d9ca0b96f4d', - label: 'Option 2', - value: 'option2', - position: 3, - }, - ], - newOptions: [ - { - id: 'f86eaffd-b773-4c9a-957b-86dca4a62731', - label: 'Option 0_UPDATED', // Label changed but value remains the same - value: 'option0', - position: 1, - }, - { - id: '28d80b3c-79bd-4a1b-a868-9616534de0fa', - label: 'Option 1', // No change - value: 'option1', - position: 2, - }, - { - id: '25a05cd8-256f-4652-9e4a-6d9ca0b96f4d', - label: 'Option 2_UPDATED', // Label changed but value remains the same - value: 'option2', - position: 3, - }, - ], - expected: { - created: [], - updated: [], // No updates because only labels changed, not values - deleted: [], - }, - }, - }, - { - title: - 'should consider changes to label as updates when value remains the same if compareLabel is true', - context: { - oldOptions: [ - { - id: 'f86eaffd-b773-4c9a-957b-86dca4a62731', - label: 'Option 0', - value: 'option0', - position: 1, - }, - { - id: '28d80b3c-79bd-4a1b-a868-9616534de0fa', - label: 'Option 1', - value: 'option1', - position: 2, - }, - { - id: '25a05cd8-256f-4652-9e4a-6d9ca0b96f4d', - label: 'Option 2', - value: 'option2', - position: 3, - }, - ], - newOptions: [ - { - id: 'f86eaffd-b773-4c9a-957b-86dca4a62731', - label: 'Option 0_UPDATED', // Label changed but value remains the same - value: 'option0', - position: 1, - }, - { - id: '28d80b3c-79bd-4a1b-a868-9616534de0fa', - label: 'Option 1', // No change - value: 'option1', - position: 2, - }, - { - id: '25a05cd8-256f-4652-9e4a-6d9ca0b96f4d', - label: 'Option 2_UPDATED', // Label changed but value remains the same - value: 'option2', - position: 3, - }, - ], - expected: { - created: [], - updated: [ - { - new: { - id: 'f86eaffd-b773-4c9a-957b-86dca4a62731', - label: 'Option 0_UPDATED', - position: 1, - value: 'option0', - }, - old: { - id: 'f86eaffd-b773-4c9a-957b-86dca4a62731', - label: 'Option 0', - position: 1, - value: 'option0', - }, - }, - { - new: { - id: '25a05cd8-256f-4652-9e4a-6d9ca0b96f4d', - label: 'Option 2_UPDATED', - position: 3, - value: 'option2', - }, - old: { - id: '25a05cd8-256f-4652-9e4a-6d9ca0b96f4d', - label: 'Option 2', - position: 3, - value: 'option2', - }, - }, - ], - deleted: [], - }, - compareLabel: true, - }, - }, - ]; - - test.each(testCases)( - '$title', - ({ context: { oldOptions, newOptions, expected, compareLabel } }) => { - const result = service.getOptionsDifferences( - oldOptions, - newOptions, - compareLabel, - ); - - expect(result.created).toEqual(expected.created); - expect(result.updated).toEqual(expected.updated); - expect(result.deleted).toEqual(expected.deleted); - }, - ); - }); -}); diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-related-records.service.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-related-records.service.ts deleted file mode 100644 index 3809488c2c..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-related-records.service.ts +++ /dev/null @@ -1,390 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { isNonEmptyString } from '@sniptt/guards'; -import { isDefined, parseJson } from 'twenty-shared/utils'; - -import { settings } from 'src/engine/constants/settings'; -import { - type FieldMetadataComplexOption, - type FieldMetadataDefaultOption, -} from 'src/engine/metadata-modules/field-metadata/dtos/options.input'; -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { - FieldMetadataException, - FieldMetadataExceptionCode, -} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception'; -import { isSelectFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-select-field-metadata-type.util'; -import { type SelectOrMultiSelectFieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/utils/is-select-or-multi-select-field-metadata.util'; -import { ViewFieldService } from 'src/engine/metadata-modules/view-field/services/view-field.service'; -import { ViewFilterService } from 'src/engine/metadata-modules/view-filter/services/view-filter.service'; -import { ViewGroupEntity } from 'src/engine/metadata-modules/view-group/entities/view-group.entity'; -import { ViewGroupService } from 'src/engine/metadata-modules/view-group/services/view-group.service'; -import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity'; -import { ViewKey } from 'src/engine/metadata-modules/view/enums/view-key.enum'; -import { ViewService } from 'src/engine/metadata-modules/view/services/view.service'; -import { DEFAULT_VIEW_FIELD_SIZE } from 'src/engine/workspace-manager/standard-objects-prefill-data/views/constants/DEFAULT_VIEW_FIELD_SIZE'; - -type Differences = { - created: T[]; - updated: { old: T; new: T }[]; - deleted: T[]; -}; - -type GetOptionsDifferences = Differences< - FieldMetadataDefaultOption | FieldMetadataComplexOption ->; - -@Injectable() -export class FieldMetadataRelatedRecordsService { - constructor( - private readonly viewService: ViewService, - private readonly viewFieldService: ViewFieldService, - private readonly viewFilterService: ViewFilterService, - private readonly viewGroupService: ViewGroupService, - ) {} - - public async updateRelatedViewGroups( - oldFieldMetadata: SelectOrMultiSelectFieldMetadataEntity, - newFieldMetadata: SelectOrMultiSelectFieldMetadataEntity, - ): Promise { - // TODO legacy should support multi-select and rating ? - if ( - !isSelectFieldMetadataType(newFieldMetadata.type) || - !isSelectFieldMetadataType(oldFieldMetadata.type) - ) { - return; - } - const objectMetadataViews = await this.viewService.findByObjectMetadataId( - newFieldMetadata.workspaceId, - newFieldMetadata.objectMetadataId, - ); - - const { created, updated, deleted } = this.getOptionsDifferences( - oldFieldMetadata.options ?? [], - newFieldMetadata.options ?? [], - ); - - for (const view of objectMetadataViews) { - if (view.viewGroups.length === 0) { - continue; - } - - const valuesToDelete = deleted.map((option) => option.value); - - if (valuesToDelete.length > 0) { - for (const valueToDelete of valuesToDelete) { - const viewGroupsToDelete = view.viewGroups.filter( - (group) => group.fieldValue === valueToDelete, - ); - - for (const viewGroup of viewGroupsToDelete) { - await this.viewGroupService.destroy( - viewGroup.id, - newFieldMetadata.workspaceId, - ); - } - } - } - const maxPosition = this.getMaxPosition(view.viewGroups); - - for (const [index, option] of created.entries()) { - this.viewGroupService.create({ - fieldMetadataId: newFieldMetadata.id, - fieldValue: option.value, - position: maxPosition + index, - isVisible: true, - viewId: view.id, - workspaceId: newFieldMetadata.workspaceId, - }); - } - - for (const { old: oldOption, new: newOption } of updated) { - const existingViewGroup = view.viewGroups.find( - (group) => group.fieldValue === oldOption.value, - ); - - if (!existingViewGroup) { - continue; - } - - this.viewGroupService.update( - existingViewGroup.id, - newFieldMetadata.workspaceId, - { fieldValue: newOption.value }, - ); - } - - await this.syncNoValueViewGroup(newFieldMetadata, view); - } - await this.viewService.flushGraphQLCache(newFieldMetadata.workspaceId); - } - - public async resetViewKanbanAggregateOperation( - fieldMetadata: Pick< - FieldMetadataEntity, - 'id' | 'workspaceId' | 'objectMetadataId' - >, - ): Promise { - const views = await this.viewService.findByObjectMetadataId( - fieldMetadata.workspaceId, - fieldMetadata.objectMetadataId, - ); - - const viewsHavingFieldAsAggregateOperation = views.filter( - (view) => - view.kanbanAggregateOperationFieldMetadataId === fieldMetadata.id, - ); - - for (const view of viewsHavingFieldAsAggregateOperation) { - await this.viewService.update(view.id, fieldMetadata.workspaceId, { - kanbanAggregateOperationFieldMetadataId: null, - kanbanAggregateOperation: null, - }); - } - await this.viewService.flushGraphQLCache(fieldMetadata.workspaceId); - } - - public async updateRelatedViewFilters( - oldFieldMetadata: SelectOrMultiSelectFieldMetadataEntity, - newFieldMetadata: SelectOrMultiSelectFieldMetadataEntity, - ): Promise { - const views = await this.viewService.findByObjectMetadataId( - newFieldMetadata.workspaceId, - newFieldMetadata.objectMetadataId, - ); - - const fieldMetadataViews = views.filter((view) => - view.viewFilters.some( - (filter) => filter.fieldMetadataId === newFieldMetadata.id, - ), - ); - - const alsoCompareLabel = true; - const { - updated: updatedFieldMetadataOptions, - deleted: deletedFieldMetadataOptions, - } = this.getOptionsDifferences( - oldFieldMetadata.options, - newFieldMetadata.options, - alsoCompareLabel, - ); - - if ( - updatedFieldMetadataOptions.length === 0 && - deletedFieldMetadataOptions.length === 0 - ) { - return; - } - - for (const view of fieldMetadataViews) { - if (view.viewFilters.length === 0) { - continue; - } - - for (const viewFilter of view.viewFilters) { - if (!isDefined(viewFilter.value)) { - continue; - } - - // TODO: all view filter value should be stored as JSON, this is ongoing work (we are missing a command to migrate the data) - const parsedValue = isNonEmptyString(viewFilter.value) - ? parseJson(viewFilter.value) - : viewFilter.value; - - if (!isDefined(parsedValue) || !Array.isArray(parsedValue)) { - throw new FieldMetadataException( - `Unexpected invalid view filter value for filter ${viewFilter.id}`, - FieldMetadataExceptionCode.INTERNAL_SERVER_ERROR, - ); - } - - const viewFilterOptions = parsedValue - .map((value) => { - if (!isDefined(oldFieldMetadata.options)) { - return undefined; - } - - return oldFieldMetadata.options.find( - (option) => option.value === value, - ); - }) - .filter(isDefined); - - const afterDeleteViewFilterOptions = viewFilterOptions.filter( - (viewFilterOption) => - !deletedFieldMetadataOptions.some( - (option) => option.value === viewFilterOption.value, - ), - ); - - if (afterDeleteViewFilterOptions.length === 0) { - await this.viewFilterService.destroy( - viewFilter.id, - newFieldMetadata.workspaceId, - ); - continue; - } - - const afterUpdateAndDeleteViewFilterOptions = - afterDeleteViewFilterOptions.map((viewFilterOption) => { - const updatedOption = updatedFieldMetadataOptions.find( - ({ old }) => viewFilterOption.value === old.value, - ); - - return isDefined(updatedOption) - ? updatedOption.new - : viewFilterOption; - }); - - const value = afterUpdateAndDeleteViewFilterOptions.map( - (option) => option.value, - ); - - await this.viewFilterService.update( - viewFilter.id, - newFieldMetadata.workspaceId, - { - value, - }, - ); - } - } - await this.viewService.flushGraphQLCache(newFieldMetadata.workspaceId); - } - - async syncNoValueViewGroup( - fieldMetadata: SelectOrMultiSelectFieldMetadataEntity, - view: ViewEntity, - ): Promise { - const noValueGroup = view.viewGroups.find( - (group) => group.fieldValue === '', - ); - - if (fieldMetadata.isNullable && !noValueGroup) { - const maxPosition = this.getMaxPosition(view.viewGroups); - - this.viewGroupService.create({ - fieldMetadataId: fieldMetadata.id, - fieldValue: '', - position: maxPosition + 1, - isVisible: true, - viewId: view.id, - workspaceId: fieldMetadata.workspaceId, - }); - } else if (!fieldMetadata.isNullable && noValueGroup) { - await this.viewGroupService.destroy( - noValueGroup.id, - fieldMetadata.workspaceId, - ); - } - await this.viewService.flushGraphQLCache(fieldMetadata.workspaceId); - } - - public getOptionsDifferences( - rawOldOptions: - | (FieldMetadataDefaultOption | FieldMetadataComplexOption)[] - | null, - rawNewOptions: - | (FieldMetadataDefaultOption | FieldMetadataComplexOption)[] - | null, - compareLabel = false, - ): GetOptionsDifferences { - const differences: Differences< - FieldMetadataDefaultOption | FieldMetadataComplexOption - > = { - created: [], - updated: [], - deleted: [], - }; - - const oldOptions = rawOldOptions ?? []; - const newOptions = rawNewOptions ?? []; - - const oldOptionsMap = new Map(oldOptions.map((opt) => [opt.id, opt])); - - for (const newOption of newOptions) { - const oldOption = oldOptionsMap.get(newOption.id); - - if (!isDefined(oldOption)) { - differences.created.push(newOption); - continue; - } - - if ( - oldOption.value !== newOption.value || - (compareLabel && oldOption.label !== newOption.label) - ) { - differences.updated.push({ old: oldOption, new: newOption }); - continue; - } - } - - const newOptionsMap = new Map(newOptions.map((opt) => [opt.id, opt])); - - for (const oldOption of oldOptions) { - if (!newOptionsMap.has(oldOption.id)) { - differences.deleted.push(oldOption); - } - } - - return differences; - } - - private getMaxPosition(viewGroups: ViewGroupEntity[]): number { - return viewGroups.reduce((max, group) => Math.max(max, group.position), 0); - } - - async createViewAndViewFields( - createdFieldMetadatas: FieldMetadataEntity[], - workspaceId: string, - ) { - const views = await this.viewService.findByWorkspaceId(workspaceId); - - for (const createdFieldMetadata of createdFieldMetadatas) { - const objectViews = views.filter( - (view) => - view.objectMetadataId === createdFieldMetadata.objectMetadataId, - ); - - if (objectViews.length === 0) { - return; - } - - const indexView = objectViews.find((view) => view.key === ViewKey.INDEX); - - if (!indexView) { - return; - } - const isVisible = - indexView.viewFields.length < settings.maxVisibleViewFields; - - const createdFieldIsAlreadyInView = indexView.viewFields.some( - (existingViewField) => - existingViewField.fieldMetadataId === createdFieldMetadata.id, - ); - - if (createdFieldIsAlreadyInView) { - continue; - } - - const lastPosition = indexView.viewFields - .map((viewField) => viewField.position) - .reduce((acc, position) => { - if (position > acc) { - return position; - } - - return acc; - }, -1); - - await this.viewFieldService.create({ - fieldMetadataId: createdFieldMetadata.id, - position: lastPosition + 1, - isVisible, - size: DEFAULT_VIEW_FIELD_SIZE, - viewId: indexView.id, - workspaceId: createdFieldMetadata.workspaceId, - }); - } - } -} diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service-v2.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service-v2.ts index 6b55e82951..4e0de4386d 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service-v2.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service-v2.ts @@ -1,12 +1,12 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; +import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm'; import { isDefined } from 'twenty-shared/utils'; -import { In, Repository } from 'typeorm'; +import { FindOneOptions, Repository } from 'typeorm'; import { type CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input'; import { type DeleteOneFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/delete-field.input'; -import { FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto'; import { type UpdateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/update-field.input'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { @@ -15,31 +15,35 @@ import { } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception'; 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'; +import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; +import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; +import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; import { fromCreateFieldInputToFlatFieldMetadatasToCreate } from 'src/engine/metadata-modules/flat-field-metadata/utils/from-create-field-input-to-flat-field-metadatas-to-create.util'; import { fromDeleteFieldInputToFlatFieldMetadatasToDelete } from 'src/engine/metadata-modules/flat-field-metadata/utils/from-delete-field-input-to-flat-field-metadatas-to-delete.util'; -import { fromFlatFieldMetadataToFieldMetadataDto } from 'src/engine/metadata-modules/flat-field-metadata/utils/from-flat-field-metadata-to-field-metadata-dto.util'; import { fromUpdateFieldInputToFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/from-update-field-input-to-flat-field-metadata.util'; import { throwOnFieldInputTranspilationsError } from 'src/engine/metadata-modules/flat-field-metadata/utils/throw-on-field-input-transpilations-error.util'; import { WorkspaceMigrationBuilderExceptionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-builder-exception-v2'; import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-validate-build-and-run-service'; @Injectable() -export class FieldMetadataServiceV2 { +export class FieldMetadataServiceV2 extends TypeOrmQueryService { constructor( @InjectRepository(FieldMetadataEntity) private readonly fieldMetadataRepository: Repository, private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService, - ) {} + ) { + super(fieldMetadataRepository); + } - async createOne({ + async createOneField({ createFieldInput, workspaceId, }: { createFieldInput: Omit; workspaceId: string; - }): Promise { - const [createdFieldMetadata] = await this.createMany({ + }): Promise { + const [createdFieldMetadata] = await this.createManyFields({ workspaceId, createFieldInputs: [createFieldInput], }); @@ -54,7 +58,7 @@ export class FieldMetadataServiceV2 { return createdFieldMetadata; } - public async deleteOneField({ + async deleteOneField({ deleteOneFieldInput, workspaceId, isSystemBuild = false, @@ -62,7 +66,7 @@ export class FieldMetadataServiceV2 { deleteOneFieldInput: DeleteOneFieldInput; workspaceId: string; isSystemBuild?: boolean; - }): Promise { + }): Promise { const { flatObjectMetadataMaps: existingFlatObjectMetadataMaps, flatIndexMaps: existingFlatIndexMaps, @@ -127,18 +131,16 @@ export class FieldMetadataServiceV2 { ); } - return fromFlatFieldMetadataToFieldMetadataDto( - flatFieldMetadatasToDelete[0], - ); + return flatFieldMetadatasToDelete[0]; } - async updateOne({ + async updateOneField({ updateFieldInput, workspaceId, }: { updateFieldInput: Omit; workspaceId: string; - }): Promise { + }): Promise { const { flatObjectMetadataMaps: existingFlatObjectMetadataMaps, flatIndexMaps: existingFlatIndexMaps, @@ -264,21 +266,27 @@ export class FieldMetadataServiceV2 { ); } - return this.fieldMetadataRepository.findOneOrFail({ - where: { - id: flatFieldMetadatasToUpdate[0].id, - workspaceId, - }, + const { flatFieldMetadataMaps: recomputedFlatFieldMetadataMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatFieldMetadataMaps'], + }, + ); + + return findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityId: flatFieldMetadatasToUpdate[0].id, + flatEntityMaps: recomputedFlatFieldMetadataMaps, }); } - async createMany({ + async createManyFields({ createFieldInputs, workspaceId, }: { createFieldInputs: Omit[]; workspaceId: string; - }): Promise { + }): Promise { if (createFieldInputs.length === 0) { return []; } @@ -365,15 +373,34 @@ export class FieldMetadataServiceV2 { ); } - return this.fieldMetadataRepository.find({ + const { flatFieldMetadataMaps: recomputedFlatFieldMetadataMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatFieldMetadataMaps'], + }, + ); + + return findManyFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityIds: allTranspiledTranspilationInputs.map( + ({ result: { flatFieldMetadatas } }) => flatFieldMetadatas[0].id, + ), + flatEntityMaps: recomputedFlatFieldMetadataMaps, + }); + } + + public async findOneWithinWorkspace( + workspaceId: string, + options: FindOneOptions, + ) { + const [fieldMetadata] = await this.fieldMetadataRepository.find({ + ...options, where: { - id: In( - allTranspiledTranspilationInputs.map( - ({ result: { flatFieldMetadatas } }) => flatFieldMetadatas[0].id, - ), - ), + ...options.where, workspaceId, }, }); + + return fieldMetadata; } } diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service.ts deleted file mode 100644 index cc3c8b73b1..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service.ts +++ /dev/null @@ -1,1148 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; - -import { msg } from '@lingui/core/macro'; -import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm'; -import { - FieldMetadataType, - compositeTypeDefinitions, -} from 'twenty-shared/types'; -import { isDefined } from 'twenty-shared/utils'; -import { - DataSource, - In, - Repository, - type FindOneOptions, - type QueryRunner, -} from 'typeorm'; - -import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; - -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 { type CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input'; -import { type DeleteOneFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/delete-field.input'; -import { type UpdateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/update-field.input'; -import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { - FieldMetadataException, - FieldMetadataExceptionCode, -} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception'; -import { FieldMetadataMorphRelationService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata-morph-relation.service'; -import { FieldMetadataRelatedRecordsService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata-related-records.service'; -import { FieldMetadataRelationService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata-relation.service'; -import { FieldMetadataValidationService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata-validation.service'; -import { FieldMetadataServiceV2 } from 'src/engine/metadata-modules/field-metadata/services/field-metadata.service-v2'; -import { areFieldMetadatasOfType } from 'src/engine/metadata-modules/field-metadata/utils/are-field-metadatas-of-type.util'; -import { assertDoesNotNullifyDefaultValueForNonNullableField } from 'src/engine/metadata-modules/field-metadata/utils/assert-does-not-nullify-default-value-for-non-nullable-field.util'; -import { buildUpdatableStandardFieldInput } from 'src/engine/metadata-modules/field-metadata/utils/build-updatable-standard-field-input.util'; -import { checkCanDeactivateFieldOrThrow } from 'src/engine/metadata-modules/field-metadata/utils/check-can-deactivate-field-or-throw'; -import { - computeColumnName, - computeCompositeColumnName, -} from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; -import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util'; -import { createMigrationActions } from 'src/engine/metadata-modules/field-metadata/utils/create-migration-actions.util'; -import { generateRatingOptions } from 'src/engine/metadata-modules/field-metadata/utils/generate-rating-optionts.util'; -import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; -import { isFieldMetadataTypeMorphRelation } from 'src/engine/metadata-modules/field-metadata/utils/is-field-metadata-type-morph-relation.util'; -import { isFieldMetadataTypeRelation } from 'src/engine/metadata-modules/field-metadata/utils/is-field-metadata-type-relation.util'; -import { isSelectOrMultiSelectFieldMetadata } from 'src/engine/metadata-modules/field-metadata/utils/is-select-or-multi-select-field-metadata.util'; -import { isValidUniqueFieldDefaultValueCombination } from 'src/engine/metadata-modules/field-metadata/utils/is-valid-unique-input.util'; -import { prepareCustomFieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/utils/prepare-custom-field-metadata-for-options.util'; -import { prepareCustomFieldMetadataForCreation } from 'src/engine/metadata-modules/field-metadata/utils/prepare-field-metadata-for-creation.util'; -import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; -import { IndexMetadataService } from 'src/engine/metadata-modules/index-metadata/index-metadata.service'; -import { computeUniqueIndexWhereClause } from 'src/engine/metadata-modules/index-metadata/utils/compute-unique-index-where-clause.util'; -import { validateCanCreateUniqueIndex } from 'src/engine/metadata-modules/index-metadata/utils/validate-can-create-unique-index.util'; -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; -import { getObjectMetadataEntityFromObjectMetadataItemWithFieldsMap } from 'src/engine/metadata-modules/utils/get-object-metadata-entity-from-object-metadata-item-with-fields-map.util'; -import { validateNameAndLabelAreSyncOrThrow } from 'src/engine/metadata-modules/utils/validate-name-and-label-are-sync-or-throw.util'; -import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service'; -import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service'; -import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util'; -import { - WorkspaceMigrationColumnActionType, - WorkspaceMigrationTableActionType, - type WorkspaceMigrationColumnDrop, - type WorkspaceMigrationTableAction, -} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; -import { WorkspaceMigrationFactory } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.factory'; -import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.service'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; -import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; -import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service'; - -type GenerateMigrationArgs = { - fieldMetadata: - | FieldMetadataEntity - | FieldMetadataEntity; - workspaceId: string; - queryRunner: QueryRunner; -}; - -@Injectable() -export class FieldMetadataService extends TypeOrmQueryService { - constructor( - @InjectDataSource() - private readonly coreDataSource: DataSource, - @InjectRepository(FieldMetadataEntity) - private readonly fieldMetadataRepository: Repository, - private readonly workspaceMigrationFactory: WorkspaceMigrationFactory, - private readonly workspaceMigrationService: WorkspaceMigrationService, - private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService, - private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService, - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, - private readonly fieldMetadataRelatedRecordsService: FieldMetadataRelatedRecordsService, - private readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService, - private readonly featureFlagService: FeatureFlagService, - private readonly fieldMetadataValidationService: FieldMetadataValidationService, - private readonly fieldMetadataMorphRelationService: FieldMetadataMorphRelationService, - private readonly fieldMetadataRelationService: FieldMetadataRelationService, - private readonly fieldMetadataServiceV2: FieldMetadataServiceV2, - private readonly indexMetadataService: IndexMetadataService, - private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, - ) { - super(fieldMetadataRepository); - } - - override async createOne( - fieldMetadataInput: CreateFieldInput, - ): Promise { - const isWorkspaceMigrationV2Enabled = - await this.featureFlagService.isFeatureEnabled( - FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED, - fieldMetadataInput.workspaceId, - ); - - if (isWorkspaceMigrationV2Enabled) { - return this.fieldMetadataServiceV2.createOne({ - createFieldInput: fieldMetadataInput, - workspaceId: fieldMetadataInput.workspaceId, - }); - } - - const [createdFieldMetadata] = await this.createMany([fieldMetadataInput]); - - if (!isDefined(createdFieldMetadata)) { - throw new FieldMetadataException( - 'Failed to create field metadata', - FieldMetadataExceptionCode.INTERNAL_SERVER_ERROR, - ); - } - - return createdFieldMetadata; - } - - override async updateOne( - id: string, - fieldMetadataInput: UpdateFieldInput, - ): Promise { - const { objectMetadataMaps } = - await this.workspaceMetadataCacheService.getExistingOrRecomputeMetadataMaps( - { workspaceId: fieldMetadataInput.workspaceId }, - ); - const { workspaceId } = fieldMetadataInput; - - let existingFieldMetadata: FieldMetadataEntity | undefined; - - for (const objectMetadataItem of Object.values( - objectMetadataMaps.byId, - ).filter(isDefined)) { - const fieldMetadata = objectMetadataItem.fieldsById[id]; - - if (fieldMetadata) { - existingFieldMetadata = fieldMetadata; - break; - } - } - - if (!isDefined(existingFieldMetadata)) { - throw new FieldMetadataException( - 'Field does not exist', - FieldMetadataExceptionCode.FIELD_METADATA_NOT_FOUND, - ); - } - - const objectMetadataItemWithFieldMaps = - objectMetadataMaps.byId[existingFieldMetadata.objectMetadataId]; - - if (!isDefined(objectMetadataItemWithFieldMaps)) { - throw new FieldMetadataException( - 'Object metadata does not exist', - FieldMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND, - ); - } - - if ( - !isValidUniqueFieldDefaultValueCombination({ - defaultValue: isDefined(fieldMetadataInput.defaultValue) - ? fieldMetadataInput.defaultValue - : existingFieldMetadata.defaultValue, - isUnique: isDefined(fieldMetadataInput.isUnique) - ? fieldMetadataInput.isUnique - : (existingFieldMetadata.isUnique ?? false), - type: existingFieldMetadata.type, - }) - ) { - throw new FieldMetadataException( - 'Unique field cannot have a default value', - FieldMetadataExceptionCode.INVALID_FIELD_INPUT, - { - userFriendlyMessage: msg`Unique field cannot have a default value`, - }, - ); - } - - const queryRunner = this.coreDataSource.createQueryRunner(); - - await queryRunner.connect(); - await queryRunner.startTransaction(); - - try { - const fieldMetadataRepository = - queryRunner.manager.getRepository( - FieldMetadataEntity, - ); - - if ( - !isDefined( - objectMetadataItemWithFieldMaps.labelIdentifierFieldMetadataId, - ) - ) { - throw new FieldMetadataException( - 'Label identifier field metadata id does not exist', - FieldMetadataExceptionCode.LABEL_IDENTIFIER_FIELD_METADATA_ID_NOT_FOUND, - ); - } - assertMutationNotOnRemoteObject(objectMetadataItemWithFieldMaps); - - assertDoesNotNullifyDefaultValueForNonNullableField({ - isNullable: existingFieldMetadata.isNullable, - defaultValueFromUpdate: fieldMetadataInput.defaultValue, - }); - - if (fieldMetadataInput.isActive === false) { - checkCanDeactivateFieldOrThrow({ - labelIdentifierFieldMetadataId: - objectMetadataItemWithFieldMaps.labelIdentifierFieldMetadataId, - existingFieldMetadata, - }); - } - - const updatableFieldInput = - existingFieldMetadata.isCustom === false - ? buildUpdatableStandardFieldInput( - fieldMetadataInput, - existingFieldMetadata, - ) - : fieldMetadataInput; - - const optionsForUpdate = isDefined(fieldMetadataInput.options) - ? prepareCustomFieldMetadataOptions(fieldMetadataInput.options) - : undefined; - const defaultValueForUpdate = - updatableFieldInput.defaultValue !== undefined - ? updatableFieldInput.defaultValue - : existingFieldMetadata.defaultValue; - - const fieldMetadataForUpdate = { - ...updatableFieldInput, - ...optionsForUpdate, - defaultValue: defaultValueForUpdate, - }; - - await this.fieldMetadataValidationService.validateFieldMetadata({ - fieldMetadataType: existingFieldMetadata.type, - existingFieldMetadata, - fieldMetadataInput: fieldMetadataForUpdate, - objectMetadata: objectMetadataItemWithFieldMaps, - }); - - const isLabelSyncedWithName = - fieldMetadataForUpdate.isLabelSyncedWithName ?? - existingFieldMetadata.isLabelSyncedWithName; - - if (isLabelSyncedWithName) { - validateNameAndLabelAreSyncOrThrow({ - label: fieldMetadataForUpdate.label ?? existingFieldMetadata.label, - name: fieldMetadataForUpdate.name ?? existingFieldMetadata.name, - }); - } - - await fieldMetadataRepository.update(id, fieldMetadataForUpdate); - - const [updatedFieldMetadata] = await fieldMetadataRepository.find({ - where: { id }, - }); - - if (!isDefined(updatedFieldMetadata)) { - throw new FieldMetadataException( - 'Field does not exist', - FieldMetadataExceptionCode.FIELD_METADATA_NOT_FOUND, - ); - } - - const workspaceMigrationsOnCustomUniqueIndex: WorkspaceMigrationTableAction[] = - []; - - const shouldUpdateUniqueIndex = - isDefined(fieldMetadataInput.name) && - fieldMetadataInput.name !== existingFieldMetadata.name && - existingFieldMetadata.isUnique === true; - - if (shouldUpdateUniqueIndex) { - workspaceMigrationsOnCustomUniqueIndex.push( - await this.updateUniqueIndexMetdataAndCreateMigrationActions({ - fieldMetadataInput, - objectMetadataItemWithFieldMaps, - updatedFieldMetadata, - queryRunner, - }), - ); - } - - const shouldCreateUniqueIndex = - fieldMetadataInput.isUnique === true && !existingFieldMetadata.isUnique; - - if (shouldCreateUniqueIndex) { - workspaceMigrationsOnCustomUniqueIndex.push( - await this.createUniqueIndexMetadataAndCreateMigrationActions({ - fieldMetadataItem: updatedFieldMetadata, - objectMetadataItemWithFieldMaps, - fieldMetadataInput, - queryRunner, - }), - ); - } - - const shouldDeleteUniqueIndex = - (fieldMetadataInput.isUnique === null || - fieldMetadataInput.isUnique === false) && - existingFieldMetadata.isUnique; - - if (shouldDeleteUniqueIndex) { - workspaceMigrationsOnCustomUniqueIndex.push( - await this.deleteUniqueIndexMetadataAndCreateMigrationActions({ - fieldMetadataInput, - objectMetadataItemWithFieldMaps, - updatedFieldMetadata, - queryRunner, - }), - ); - } - - if ( - isDefined(fieldMetadataInput.name) || - isDefined(updatableFieldInput.options) || - isDefined(updatableFieldInput.defaultValue) || - isDefined(updatableFieldInput.isUnique) - ) { - await this.workspaceMigrationService.createCustomMigration( - generateMigrationName(`update-${updatedFieldMetadata.name}`), - fieldMetadataInput.workspaceId, - [ - { - name: computeObjectTargetTable(objectMetadataItemWithFieldMaps), - action: WorkspaceMigrationTableActionType.ALTER, - columns: this.workspaceMigrationFactory.createColumnActions( - WorkspaceMigrationColumnActionType.ALTER, - existingFieldMetadata, - updatedFieldMetadata, - ), - } satisfies WorkspaceMigrationTableAction, - ...workspaceMigrationsOnCustomUniqueIndex, - ], - queryRunner, - ); - - await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrationsWithinTransaction( - updatedFieldMetadata.workspaceId, - queryRunner, - ); - } - - await queryRunner.commitTransaction(); - - if ( - updatedFieldMetadata.isActive && - isSelectOrMultiSelectFieldMetadata(updatedFieldMetadata) && - isSelectOrMultiSelectFieldMetadata(existingFieldMetadata) - ) { - await this.fieldMetadataRelatedRecordsService.updateRelatedViewGroups( - existingFieldMetadata, - updatedFieldMetadata, - ); - - await this.fieldMetadataRelatedRecordsService.updateRelatedViewFilters( - existingFieldMetadata, - updatedFieldMetadata, - ); - } - - await this.workspaceMetadataVersionService.incrementMetadataVersion( - fieldMetadataInput.workspaceId, - ); - - await this.flatEntityMapsCacheService.invalidateFlatEntityMaps({ - workspaceId, - flatMapsKeys: ['flatFieldMetadataMaps'], - }); - - return updatedFieldMetadata; - } catch (error) { - if (queryRunner.isTransactionActive) { - try { - await queryRunner.rollbackTransaction(); - } catch (error) { - // eslint-disable-next-line no-console - console.trace(`Failed to rollback transaction: ${error.message}`); - } - } - throw error; - } finally { - await queryRunner.release(); - } - } - - public async deleteOneField( - input: DeleteOneFieldInput, - workspaceId: string, - ): Promise { - const queryRunner = this.coreDataSource.createQueryRunner(); - - await queryRunner.connect(); - await queryRunner.startTransaction(); - - try { - const fieldMetadataRepository = - queryRunner.manager.getRepository( - FieldMetadataEntity, - ); - - const [fieldMetadata] = await fieldMetadataRepository.find({ - where: { - id: input.id, - workspaceId: workspaceId, - }, - relations: [ - 'object', - 'relationTargetFieldMetadata', - 'relationTargetObjectMetadata', - ], - }); - - if (!isDefined(fieldMetadata)) { - throw new FieldMetadataException( - 'Field does not exist', - FieldMetadataExceptionCode.FIELD_METADATA_NOT_FOUND, - ); - } - - if (!isDefined(fieldMetadata.object)) { - throw new FieldMetadataException( - 'Object metadata does not exist', - FieldMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND, - ); - } - - if ( - fieldMetadata.object.labelIdentifierFieldMetadataId === fieldMetadata.id - ) { - throw new FieldMetadataException( - 'Cannot delete, please update the label identifier field first', - FieldMetadataExceptionCode.FIELD_MUTATION_NOT_ALLOWED, - { - userFriendlyMessage: msg`Cannot delete, please update the label identifier field first`, - }, - ); - } - - await this.fieldMetadataRelatedRecordsService.resetViewKanbanAggregateOperation( - fieldMetadata, - ); - - if (isFieldMetadataTypeRelation(fieldMetadata)) { - const fieldMetadataIdsToDelete: string[] = []; - - fieldMetadataIdsToDelete.push( - fieldMetadata.id, - fieldMetadata.relationTargetFieldMetadataId, - ); - - await fieldMetadataRepository.delete({ - id: In(fieldMetadataIdsToDelete), - }); - - await this.generateDeleteRelationMigration({ - fieldMetadata, - workspaceId, - queryRunner, - }); - } else if (isFieldMetadataTypeMorphRelation(fieldMetadata)) { - const fieldMetadataIdsToDelete: string[] = []; - - const allMophFieldMetadatas = - await this.getAllMorphFieldMetadatasByMorphId({ - morphId: fieldMetadata.morphId, - objectMetadataId: fieldMetadata.objectMetadataId, - workspaceId, - fieldMetadataRepository, - }); - - allMophFieldMetadatas.forEach((morphRelation) => { - fieldMetadataIdsToDelete.push( - morphRelation.id, - morphRelation.relationTargetFieldMetadataId, - ); - }); - - await fieldMetadataRepository.delete({ - id: In(fieldMetadataIdsToDelete), - }); - - for (const morphRelation of allMophFieldMetadatas) { - await this.generateDeleteRelationMigration({ - fieldMetadata: morphRelation, - workspaceId, - queryRunner, - }); - } - } else if (isCompositeFieldMetadataType(fieldMetadata.type)) { - await fieldMetadataRepository.delete(fieldMetadata.id); - const compositeType = compositeTypeDefinitions.get(fieldMetadata.type); - - if (!compositeType) { - throw new Error( - `Composite type not found for field metadata type: ${fieldMetadata.type}`, - ); - } - - await this.workspaceMigrationService.createCustomMigration( - generateMigrationName( - `delete-${fieldMetadata.name}-composite-columns`, - ), - workspaceId, - [ - { - name: computeObjectTargetTable(fieldMetadata.object), - action: WorkspaceMigrationTableActionType.ALTER, - columns: compositeType.properties.map((property) => { - return { - action: WorkspaceMigrationColumnActionType.DROP, - columnName: computeCompositeColumnName( - fieldMetadata.name, - property, - ), - } satisfies WorkspaceMigrationColumnDrop; - }), - } satisfies WorkspaceMigrationTableAction, - ], - queryRunner, - ); - } else { - await fieldMetadataRepository.delete(fieldMetadata.id); - await this.workspaceMigrationService.createCustomMigration( - generateMigrationName(`delete-${fieldMetadata.name}`), - workspaceId, - [ - { - name: computeObjectTargetTable(fieldMetadata.object), - action: WorkspaceMigrationTableActionType.ALTER, - columns: [ - { - action: WorkspaceMigrationColumnActionType.DROP, - columnName: computeColumnName(fieldMetadata), - } satisfies WorkspaceMigrationColumnDrop, - ], - } satisfies WorkspaceMigrationTableAction, - ], - queryRunner, - ); - } - - await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrationsWithinTransaction( - workspaceId, - queryRunner, - ); - - await queryRunner.commitTransaction(); - - await this.workspaceMetadataVersionService.incrementMetadataVersion( - workspaceId, - ); - - await this.flatEntityMapsCacheService.invalidateFlatEntityMaps({ - workspaceId, - flatMapsKeys: ['flatObjectMetadataMaps', 'flatFieldMetadataMaps'], - }); - - return fieldMetadata; - } catch (error) { - if (queryRunner.isTransactionActive) { - try { - await queryRunner.rollbackTransaction(); - } catch (error) { - // eslint-disable-next-line no-console - console.trace(`Failed to rollback transaction: ${error.message}`); - } - } - throw error; - } finally { - await queryRunner.release(); - } - } - - public async findOneWithinWorkspace( - workspaceId: string, - options: FindOneOptions, - ) { - const [fieldMetadata] = await this.fieldMetadataRepository.find({ - ...options, - where: { - ...options.where, - workspaceId, - }, - }); - - return fieldMetadata; - } - - private groupFieldInputsByObjectId( - fieldMetadataInputs: CreateFieldInput[], - ): Record { - return fieldMetadataInputs.reduce( - (acc, input) => { - if (!acc[input.objectMetadataId]) { - acc[input.objectMetadataId] = []; - } - acc[input.objectMetadataId].push(input); - - return acc; - }, - {} as Record, - ); - } - - async createMany( - fieldMetadataInputs: CreateFieldInput[], - ): Promise { - if (!fieldMetadataInputs.length) { - return []; - } - const workspaceId = fieldMetadataInputs[0].workspaceId; - const isWorkspaceMigrationV2Enabled = - await this.featureFlagService.isFeatureEnabled( - FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED, - workspaceId, - ); - - if (isWorkspaceMigrationV2Enabled) { - return this.fieldMetadataServiceV2.createMany({ - createFieldInputs: fieldMetadataInputs, - workspaceId, - }); - } - - const { objectMetadataMaps } = - await this.workspaceMetadataCacheService.getExistingOrRecomputeMetadataMaps( - { workspaceId: fieldMetadataInputs[0].workspaceId }, - ); - - const queryRunner = this.coreDataSource.createQueryRunner(); - - await queryRunner.connect(); - await queryRunner.startTransaction(); - - try { - const fieldMetadataRepository = - queryRunner.manager.getRepository( - FieldMetadataEntity, - ); - - const inputsByObjectId = - this.groupFieldInputsByObjectId(fieldMetadataInputs); - const objectMetadataIds = Object.keys(inputsByObjectId); - - const createdFieldMetadatas: FieldMetadataEntity[] = []; - const migrationActions: WorkspaceMigrationTableAction[] = []; - - for (const objectMetadataId of objectMetadataIds) { - const objectMetadata = objectMetadataMaps.byId[objectMetadataId]; - - if (!isDefined(objectMetadata)) { - throw new FieldMetadataException( - 'Object metadata does not exist', - FieldMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND, - ); - } - - const inputs = inputsByObjectId[objectMetadataId]; - - for (const fieldMetadataInput of inputs) { - const createdFieldMetadataItems = - await this.validateAndCreateFieldMetadataItems( - fieldMetadataInput, - objectMetadata, - fieldMetadataRepository, - objectMetadataMaps, - ); - - createdFieldMetadatas.push(...createdFieldMetadataItems); - - const fieldMigrationActions = await createMigrationActions({ - createdFieldMetadataItems, - objectMetadataMap: objectMetadataMaps.byId, - isRemoteCreation: fieldMetadataInput.isRemoteCreation ?? false, - workspaceMigrationFactory: this.workspaceMigrationFactory, - }); - - migrationActions.push(...fieldMigrationActions); - - if (fieldMetadataInput.isUnique) { - if (createdFieldMetadataItems.length > 1) { - throw new FieldMetadataException( - 'Unique field cannot bet RELATION or MORPH_RELATION type', - FieldMetadataExceptionCode.INVALID_FIELD_INPUT, - ); - } - - const uniqueIndexMigration = - await this.createUniqueIndexForNewField({ - createdFieldMetadataItem: createdFieldMetadataItems[0], - objectMetadata, - fieldMetadataInput, - workspaceId, - queryRunner, - }); - - migrationActions.push( - ...(isDefined(uniqueIndexMigration) - ? [uniqueIndexMigration] - : []), - ); - } - } - } - - if (migrationActions.length > 0) { - await this.workspaceMigrationService.createCustomMigration( - generateMigrationName(`create-multiple-fields`), - workspaceId, - migrationActions, - queryRunner, - ); - - await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrationsWithinTransaction( - workspaceId, - queryRunner, - ); - } - - await queryRunner.commitTransaction(); - - await this.fieldMetadataRelatedRecordsService.createViewAndViewFields( - createdFieldMetadatas, - workspaceId, - ); - - await this.workspaceMetadataVersionService.incrementMetadataVersion( - workspaceId, - ); - - await this.flatEntityMapsCacheService.invalidateFlatEntityMaps({ - workspaceId, - flatMapsKeys: ['flatObjectMetadataMaps', 'flatFieldMetadataMaps'], - }); - - return createdFieldMetadatas; - } catch (error) { - if (queryRunner.isTransactionActive) { - try { - await queryRunner.rollbackTransaction(); - } catch (error) { - // eslint-disable-next-line no-console - console.trace(`Failed to rollback transaction: ${error.message}`); - } - } - throw error; - } finally { - await queryRunner.release(); - } - } - - private async createUniqueIndexForNewField({ - createdFieldMetadataItem, - objectMetadata, - fieldMetadataInput, - workspaceId, - queryRunner, - }: { - createdFieldMetadataItem: FieldMetadataEntity; - objectMetadata: ObjectMetadataItemWithFieldMaps; - fieldMetadataInput: CreateFieldInput; - workspaceId: string; - queryRunner: QueryRunner; - }) { - if ( - isDefined(fieldMetadataInput.defaultValue) && - !isValidUniqueFieldDefaultValueCombination({ - defaultValue: fieldMetadataInput.defaultValue, - isUnique: fieldMetadataInput.isUnique ?? false, - type: fieldMetadataInput.type, - }) - ) - throw new FieldMetadataException( - 'Unique field cannot have a default value', - FieldMetadataExceptionCode.INVALID_FIELD_INPUT, - { - userFriendlyMessage: msg`Unique field cannot have a default value`, - }, - ); - - if (fieldMetadataInput.isUnique !== true) return; - - validateCanCreateUniqueIndex(createdFieldMetadataItem); - - await this.indexMetadataService.createIndexMetadata({ - workspaceId, - objectMetadata: - getObjectMetadataEntityFromObjectMetadataItemWithFieldsMap( - objectMetadata, - ), - fieldMetadataToIndex: [createdFieldMetadataItem], - isUnique: true, - isCustom: true, - indexWhereClause: computeUniqueIndexWhereClause(createdFieldMetadataItem), - queryRunner, - }); - - return this.indexMetadataService.computeIndexCreationMigration({ - objectMetadata: - getObjectMetadataEntityFromObjectMetadataItemWithFieldsMap( - objectMetadata, - ), - fieldMetadataToIndex: [createdFieldMetadataItem], - isUnique: true, - indexWhereClause: computeUniqueIndexWhereClause(createdFieldMetadataItem), - }); - } - - private async createUniqueIndexMetadataAndCreateMigrationActions({ - fieldMetadataItem, - objectMetadataItemWithFieldMaps, - fieldMetadataInput, - queryRunner, - }: { - fieldMetadataItem: FieldMetadataEntity; - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; - fieldMetadataInput: UpdateFieldInput; - queryRunner: QueryRunner; - }) { - validateCanCreateUniqueIndex(fieldMetadataItem); - - await this.indexMetadataService.createIndexMetadata({ - workspaceId: fieldMetadataInput.workspaceId, - objectMetadata: - getObjectMetadataEntityFromObjectMetadataItemWithFieldsMap( - objectMetadataItemWithFieldMaps, - ), - fieldMetadataToIndex: [fieldMetadataItem], - isUnique: true, - isCustom: true, - indexWhereClause: computeUniqueIndexWhereClause(fieldMetadataItem), - queryRunner, - }); - - return this.indexMetadataService.computeIndexCreationMigration({ - objectMetadata: - getObjectMetadataEntityFromObjectMetadataItemWithFieldsMap( - objectMetadataItemWithFieldMaps, - ), - fieldMetadataToIndex: [fieldMetadataItem], - isUnique: true, - indexWhereClause: computeUniqueIndexWhereClause(fieldMetadataItem), - }); - } - - private async updateUniqueIndexMetdataAndCreateMigrationActions({ - fieldMetadataInput, - objectMetadataItemWithFieldMaps, - updatedFieldMetadata, - queryRunner, - }: { - fieldMetadataInput: UpdateFieldInput; - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; - updatedFieldMetadata: FieldMetadataEntity; - queryRunner: QueryRunner; - }) { - const recomputedIndexPayload = - await this.indexMetadataService.recomputeUniqueCustomIndexMetadataForField( - { - workspaceId: fieldMetadataInput.workspaceId, - objectMetadata: - getObjectMetadataEntityFromObjectMetadataItemWithFieldsMap( - objectMetadataItemWithFieldMaps, - ), - updatedFieldMetadata: updatedFieldMetadata, - queryRunner, - }, - ); - - if (!isDefined(recomputedIndexPayload)) { - throw new FieldMetadataException( - 'Unique index not found for unique field', - FieldMetadataExceptionCode.INTERNAL_SERVER_ERROR, - ); - } - - const { updatedIndex, previousName } = recomputedIndexPayload; - - return this.indexMetadataService.createIndexRecomputeMigrationActions( - objectMetadataItemWithFieldMaps, - { - indexMetadata: updatedIndex, - previousName, - newName: updatedIndex.name, - }, - ); - } - - private async deleteUniqueIndexMetadataAndCreateMigrationActions({ - fieldMetadataInput, - objectMetadataItemWithFieldMaps, - updatedFieldMetadata, - queryRunner, - }: { - fieldMetadataInput: UpdateFieldInput; - objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps; - updatedFieldMetadata: FieldMetadataEntity; - queryRunner: QueryRunner; - }) { - await this.indexMetadataService.deleteIndexMetadata({ - workspaceId: fieldMetadataInput.workspaceId, - objectMetadata: - getObjectMetadataEntityFromObjectMetadataItemWithFieldsMap( - objectMetadataItemWithFieldMaps, - ), - fieldMetadataToIndex: [updatedFieldMetadata], - queryRunner, - }); - - return this.indexMetadataService.computeIndexDeletionMigration({ - objectMetadata: - getObjectMetadataEntityFromObjectMetadataItemWithFieldsMap( - objectMetadataItemWithFieldMaps, - ), - fieldMetadataToIndex: [updatedFieldMetadata], - isUnique: fieldMetadataInput.isUnique ?? false, - }); - } - - private async validateAndCreateFieldMetadataItems( - fieldMetadataInput: CreateFieldInput, - objectMetadata: ObjectMetadataItemWithFieldMaps, - fieldMetadataRepository: Repository, - objectMetadataMaps: ObjectMetadataMaps, - ): Promise { - if (!fieldMetadataInput.isRemoteCreation) { - assertMutationNotOnRemoteObject(objectMetadata); - } - - if (fieldMetadataInput.type === FieldMetadataType.RATING) { - fieldMetadataInput.options = generateRatingOptions(); - } - - if (fieldMetadataInput.isLabelSyncedWithName === true) { - validateNameAndLabelAreSyncOrThrow({ - label: fieldMetadataInput.label, - name: fieldMetadataInput.name, - }); - } - - const fieldMetadataForCreate = - prepareCustomFieldMetadataForCreation(fieldMetadataInput); - - await this.fieldMetadataValidationService.validateFieldMetadata({ - fieldMetadataType: fieldMetadataForCreate.type, - fieldMetadataInput: fieldMetadataForCreate, - objectMetadata, - }); - - const isRelation = - fieldMetadataInput.type === FieldMetadataType.RELATION || - fieldMetadataInput.type === FieldMetadataType.MORPH_RELATION; - - if (!isRelation) { - const createdFieldMetadataItem = await fieldMetadataRepository.save( - fieldMetadataForCreate, - ); - - return [createdFieldMetadataItem]; - } - - if (fieldMetadataInput.type === FieldMetadataType.RELATION) { - const relationFieldMetadataForCreate = - this.fieldMetadataRelationService.computeCustomRelationFieldMetadataForCreation( - { - fieldMetadataInput: fieldMetadataForCreate, - relationCreationPayload: fieldMetadataInput.relationCreationPayload, - joinColumnName: computeMorphOrRelationFieldJoinColumnName({ - name: fieldMetadataForCreate.name, - }), - }, - ); - - await this.fieldMetadataRelationService.validateFieldMetadataRelationSpecifics( - { - fieldMetadataInput: relationFieldMetadataForCreate, - fieldMetadataType: fieldMetadataForCreate.type, - objectMetadataMaps, - objectMetadata, - }, - ); - - return await this.fieldMetadataRelationService.createRelationFieldMetadataItems( - { - fieldMetadataInput: relationFieldMetadataForCreate, - objectMetadata, - fieldMetadataRepository, - }, - ); - } - - return await this.fieldMetadataMorphRelationService.createMorphRelationFieldMetadataItems( - { - fieldMetadataForCreate, - morphRelationsCreationPayload: - fieldMetadataInput.morphRelationsCreationPayload, - objectMetadata, - fieldMetadataRepository, - objectMetadataMaps, - }, - ); - } - - private async getAllMorphFieldMetadatasByMorphId({ - morphId, - objectMetadataId, - workspaceId, - fieldMetadataRepository, - }: { - morphId: string; - objectMetadataId: string; - workspaceId: string; - fieldMetadataRepository: Repository; - }): Promise[]> { - const fieldMetadatas = await fieldMetadataRepository.find({ - where: { - morphId, - objectMetadataId, - workspaceId, - }, - relations: [ - 'object', - 'relationTargetFieldMetadata', - 'relationTargetObjectMetadata', - ], - }); - - if ( - !areFieldMetadatasOfType(fieldMetadatas, FieldMetadataType.MORPH_RELATION) - ) { - throw new FieldMetadataException( - `Encountered a non morph relation morphId related flat field metadata type`, - FieldMetadataExceptionCode.INTERNAL_SERVER_ERROR, - ); - } - - return fieldMetadatas; - } - - private async generateDeleteRelationMigration({ - fieldMetadata, - workspaceId, - queryRunner, - }: GenerateMigrationArgs) { - if (fieldMetadata.settings?.relationType === RelationType.ONE_TO_MANY) { - await this.generateDeleteOneToManyRelationMigration({ - fieldMetadata, - workspaceId, - queryRunner, - }); - } else { - await this.generateDeleteManyToOneRelationMigration({ - fieldMetadata, - workspaceId, - queryRunner, - }); - } - } - - private async generateDeleteManyToOneRelationMigration({ - fieldMetadata, - workspaceId, - queryRunner, - }: GenerateMigrationArgs) { - if (fieldMetadata.settings?.relationType !== RelationType.MANY_TO_ONE) { - throw new FieldMetadataException( - 'Field metadata is not a many to one relation', - FieldMetadataExceptionCode.INTERNAL_SERVER_ERROR, - ); - } - - await this.workspaceMigrationService.createCustomMigration( - generateMigrationName(`delete-${fieldMetadata.name}`), - workspaceId, - [ - { - name: computeObjectTargetTable(fieldMetadata.object), - action: WorkspaceMigrationTableActionType.ALTER, - columns: [ - { - action: WorkspaceMigrationColumnActionType.DROP, - columnName: `${fieldMetadata.settings?.joinColumnName}`, - } satisfies WorkspaceMigrationColumnDrop, - ], - } satisfies WorkspaceMigrationTableAction, - ], - queryRunner, - ); - } - - private async generateDeleteOneToManyRelationMigration({ - fieldMetadata, - workspaceId, - queryRunner, - }: GenerateMigrationArgs) { - if (fieldMetadata.settings?.relationType !== RelationType.ONE_TO_MANY) { - throw new FieldMetadataException( - 'Field metadata is not a one to many relation', - FieldMetadataExceptionCode.INTERNAL_SERVER_ERROR, - ); - } - - await this.workspaceMigrationService.createCustomMigration( - generateMigrationName(`delete-${fieldMetadata.name}`), - workspaceId, - [ - { - name: computeObjectTargetTable( - fieldMetadata.relationTargetObjectMetadata as ObjectMetadataEntity, - ), - action: WorkspaceMigrationTableActionType.ALTER, - columns: [ - { - action: WorkspaceMigrationColumnActionType.DROP, - columnName: `${(fieldMetadata.relationTargetFieldMetadata as FieldMetadataEntity).settings?.joinColumnName}`, - } satisfies WorkspaceMigrationColumnDrop, - ], - } satisfies WorkspaceMigrationTableAction, - ], - queryRunner, - ); - } -} diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-type-validator.service.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-type-validator.service.ts index 3caed00ec2..fc3fb8f3fc 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-type-validator.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-type-validator.service.ts @@ -3,7 +3,6 @@ import { Injectable } from '@nestjs/common'; import { msg } from '@lingui/core/macro'; import { isDefined } from 'class-validator'; -import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { FieldMetadataExceptionCode } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception'; import { type FlatFieldMetadataTypeValidator } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-type-validator.type'; import { FlatFieldMetadataValidationError } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-validation-error.type'; @@ -17,7 +16,7 @@ const DEFAULT_NO_VALIDATION = async (): Promise< @Injectable() export class FlatFieldMetadataTypeValidatorService { - constructor(private readonly featureFlagService: FeatureFlagService) {} + constructor() {} private readonly FIELD_METADATA_TYPE_VALIDATOR_HASHMAP: FlatFieldMetadataTypeValidator = { diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-create-object-input-to-flat-object-metadata-and-flat-field-metadatas-to-create.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-create-object-input-to-flat-object-metadata-and-flat-field-metadatas-to-create.util.ts index 10f2457a29..554fbfdef8 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-create-object-input-to-flat-object-metadata-and-flat-field-metadatas-to-create.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-create-object-input-to-flat-object-metadata-and-flat-field-metadatas-to-create.util.ts @@ -15,7 +15,7 @@ import { buildDefaultRelationFlatFieldMetadatasForCustomObject } from 'src/engin type FromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCreateArgs = { - createObjectInput: Omit; + createObjectInput: CreateObjectInput; workspaceId: string; } & Pick; export const fromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCreate = diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.constants.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/constants/object-metadata.constants.ts similarity index 100% rename from packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.constants.ts rename to packages/twenty-server/src/engine/metadata-modules/object-metadata/constants/object-metadata.constants.ts diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/dtos/create-object.input.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/dtos/create-object.input.ts index a6cde80f96..f00a7e7aa2 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/dtos/create-object.input.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/dtos/create-object.input.ts @@ -1,18 +1,22 @@ import { Field, HideField, InputType } from '@nestjs/graphql'; -import { BeforeCreateOne } from '@ptc-org/nestjs-query-graphql'; -import { IsBoolean, IsNotEmpty, IsOptional, IsString } from 'class-validator'; +import { + IsBoolean, + IsNotEmpty, + IsOptional, + IsString, + ValidateNested, +} from 'class-validator'; import GraphQLJSON from 'graphql-type-json'; import { - type FieldMetadataType, type FieldMetadataSettings, + type FieldMetadataType, } from 'twenty-shared/types'; +import { Type } from 'class-transformer'; import { IsValidMetadataName } from 'src/engine/decorators/metadata/is-valid-metadata-name.decorator'; -import { BeforeCreateOneObject } from 'src/engine/metadata-modules/object-metadata/hooks/before-create-one-object.hook'; @InputType() -@BeforeCreateOne(BeforeCreateOneObject) export class CreateObjectInput { @IsString() @IsNotEmpty() @@ -54,9 +58,6 @@ export class CreateObjectInput { @HideField() dataSourceId: string; - @HideField() - workspaceId: string; - @HideField() applicationId?: string; @@ -81,3 +82,13 @@ export class CreateObjectInput { @Field({ nullable: true }) // Not nullable to me isLabelSyncedWithName?: boolean; } + +@InputType() +export class CreateOneObjectInput { + @Type(() => CreateObjectInput) + @ValidateNested() + @Field(() => CreateObjectInput, { + description: 'The object to create', + }) + object!: CreateObjectInput; +} diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/dtos/delete-object.input.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/dtos/delete-object.input.ts index 974c5ec19c..d629a46089 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/dtos/delete-object.input.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/dtos/delete-object.input.ts @@ -1,13 +1,11 @@ import { InputType } from '@nestjs/graphql'; -import { BeforeDeleteOne, IDField } from '@ptc-org/nestjs-query-graphql'; +import { IDField } from '@ptc-org/nestjs-query-graphql'; import { IsUUID } from 'class-validator'; import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; -import { BeforeDeleteOneObject } from 'src/engine/metadata-modules/object-metadata/hooks/before-delete-one-object.hook'; @InputType() -@BeforeDeleteOne(BeforeDeleteOneObject) export class DeleteOneObjectInput { @IDField(() => UUIDScalarType, { description: 'The id of the record to delete.', diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/dtos/object-metadata.dto.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/dtos/object-metadata.dto.ts index 5cdfb454c1..43f4ac07aa 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/dtos/object-metadata.dto.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/dtos/object-metadata.dto.ts @@ -2,7 +2,6 @@ import { Field, HideField, ObjectType } from '@nestjs/graphql'; import { Authorize, - BeforeDeleteOne, CursorConnection, FilterableField, IDField, @@ -14,7 +13,6 @@ import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/ import { FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto'; import { IndexMetadataDTO } from 'src/engine/metadata-modules/index-metadata/dtos/index-metadata.dto'; import { ObjectStandardOverridesDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-standard-overrides.dto'; -import { BeforeDeleteOneObject } from 'src/engine/metadata-modules/object-metadata/hooks/before-delete-one-object.hook'; @ObjectType('Object') @Authorize({ @@ -28,7 +26,6 @@ import { BeforeDeleteOneObject } from 'src/engine/metadata-modules/object-metada disableSort: true, maxResultsSize: 1000, }) -@BeforeDeleteOne(BeforeDeleteOneObject) @CursorConnection('fields', () => FieldMetadataDTO) @CursorConnection('indexMetadatas', () => IndexMetadataDTO) export class ObjectMetadataDTO { diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/dtos/update-object.input.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/dtos/update-object.input.ts index 38a450f27c..71279b939b 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/dtos/update-object.input.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/dtos/update-object.input.ts @@ -1,6 +1,5 @@ import { Field, InputType } from '@nestjs/graphql'; -import { BeforeUpdateOne } from '@ptc-org/nestjs-query-graphql'; import { Type } from 'class-transformer'; import { IsBoolean, @@ -13,7 +12,6 @@ import { import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; import { IsValidMetadataName } from 'src/engine/decorators/metadata/is-valid-metadata-name.decorator'; -import { BeforeUpdateOneObject } from 'src/engine/metadata-modules/object-metadata/hooks/before-update-one-object.hook'; @InputType() export class UpdateObjectPayload { @@ -76,7 +74,6 @@ export class UpdateObjectPayload { } @InputType() -@BeforeUpdateOne(BeforeUpdateOneObject) export class UpdateOneObjectInput { @Type(() => UpdateObjectPayload) @ValidateNested() diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/hooks/__tests__/before-update-one-object.hook.spec.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/hooks/__tests__/before-update-one-object.hook.spec.ts deleted file mode 100644 index d3d5b039c6..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/hooks/__tests__/before-update-one-object.hook.spec.ts +++ /dev/null @@ -1,697 +0,0 @@ -import { BadRequestException, UnauthorizedException } from '@nestjs/common'; -import { Test, type TestingModule } from '@nestjs/testing'; -import { getRepositoryToken } from '@nestjs/typeorm'; - -import { type UpdateOneInputType } from '@ptc-org/nestjs-query-graphql'; -import { type Repository } from 'typeorm'; - -import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { type UpdateObjectPayload } from 'src/engine/metadata-modules/object-metadata/dtos/update-object.input'; -import { BeforeUpdateOneObject } from 'src/engine/metadata-modules/object-metadata/hooks/before-update-one-object.hook'; -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service'; -import { I18nService } from 'src/engine/core-modules/i18n/i18n.service'; - -type UpdateObjectPayloadForTest = Omit< - UpdateObjectPayload, - 'id' | 'workspaceId' ->; - -describe('BeforeUpdateOneObject', () => { - let hook: BeforeUpdateOneObject; - let objectMetadataService: ObjectMetadataService; - let fieldMetadataRepository: Repository; - - const mockWorkspaceId = 'workspace-id'; - const mockObjectId = 'object-id'; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - BeforeUpdateOneObject, - { - provide: ObjectMetadataService, - useValue: { - findOneWithinWorkspace: jest.fn(), - }, - }, - { - provide: getRepositoryToken(FieldMetadataEntity), - useValue: { - findBy: jest.fn(), - }, - }, - { - provide: I18nService, - useValue: { - getI18nInstance: jest.fn().mockReturnValue({ - _: jest.fn().mockReturnValue('mocked-translation'), - }), - }, - }, - ], - }).compile(); - - hook = module.get>( - BeforeUpdateOneObject, - ); - objectMetadataService = module.get( - ObjectMetadataService, - ); - fieldMetadataRepository = module.get>( - getRepositoryToken(FieldMetadataEntity), - ); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - it('should throw UnauthorizedException if workspaceId is not provided', async () => { - const instance: UpdateOneInputType = { - id: mockObjectId, - update: { - isActive: true, - }, - }; - - await expect( - hook.run(instance as UpdateOneInputType, { - workspaceId: '', - locale: undefined, - }), - ).rejects.toThrow(UnauthorizedException); - }); - - it('should throw BadRequestException if object does not exist', async () => { - const instance: UpdateOneInputType = { - id: mockObjectId, - update: { - isActive: true, - }, - }; - - jest - .spyOn(objectMetadataService, 'findOneWithinWorkspace') - .mockResolvedValue(null as unknown as ObjectMetadataEntity); - - await expect( - hook.run(instance as UpdateOneInputType, { - workspaceId: mockWorkspaceId, - locale: undefined, - }), - ).rejects.toThrow(BadRequestException); - }); - - it('should not affect custom objects', async () => { - const instance: UpdateOneInputType = { - id: mockObjectId, - update: { - nameSingular: 'newName', - isActive: true, - }, - }; - - const mockObject: Partial = { - id: mockObjectId, - isCustom: true, - }; - - jest - .spyOn(objectMetadataService, 'findOneWithinWorkspace') - .mockResolvedValue(mockObject as ObjectMetadataEntity); - - jest.spyOn(fieldMetadataRepository, 'findBy').mockResolvedValue([]); - - const result = await hook.run( - instance as UpdateOneInputType, - { - workspaceId: mockWorkspaceId, - locale: undefined, - }, - ); - - expect(result).toEqual(instance); - }); - - it('should throw BadRequestException when trying to update non-updatable fields on standard objects', async () => { - const instance: UpdateOneInputType = { - id: mockObjectId, - update: { - nameSingular: 'newName', - }, - }; - - const mockObject: Partial = { - id: mockObjectId, - isCustom: false, - }; - - jest - .spyOn(objectMetadataService, 'findOneWithinWorkspace') - .mockResolvedValue(mockObject as ObjectMetadataEntity); - - await expect( - hook.run(instance as UpdateOneInputType, { - workspaceId: mockWorkspaceId, - locale: undefined, - }), - ).rejects.toThrow(BadRequestException); - }); - - it('should throw BadRequestException when trying to update labels when they are synced with name', async () => { - const instance: UpdateOneInputType = { - id: mockObjectId, - update: { - labelSingular: 'New Label', - }, - }; - - const mockObject: Partial = { - id: mockObjectId, - isCustom: false, - isLabelSyncedWithName: true, - labelSingular: 'Old Label', - }; - - jest - .spyOn(objectMetadataService, 'findOneWithinWorkspace') - .mockResolvedValue(mockObject as ObjectMetadataEntity); - - await expect( - hook.run(instance as UpdateOneInputType, { - workspaceId: mockWorkspaceId, - locale: undefined, - }), - ).rejects.toThrow(BadRequestException); - }); - - it('should handle isActive updates for standard objects', async () => { - const instance: UpdateOneInputType = { - id: mockObjectId, - update: { - isActive: false, - }, - }; - - const mockObject: Partial = { - id: mockObjectId, - isCustom: false, - isActive: true, - }; - - jest - .spyOn(objectMetadataService, 'findOneWithinWorkspace') - .mockResolvedValue(mockObject as ObjectMetadataEntity); - - const result = await hook.run( - instance as UpdateOneInputType, - { - workspaceId: mockWorkspaceId, - locale: undefined, - }, - ); - - const expectedResult = { - id: mockObjectId, - update: { - isActive: false, - standardOverrides: {}, - }, - }; - - expect(result).toEqual(expectedResult); - }); - - it('should handle isLabelSyncedWithName updates for standard objects', async () => { - const instance: UpdateOneInputType = { - id: mockObjectId, - update: { - isLabelSyncedWithName: true, - }, - }; - - const mockObject: Partial = { - id: mockObjectId, - isCustom: false, - isLabelSyncedWithName: false, - standardOverrides: { - labelSingular: 'Custom Label', - labelPlural: 'Custom Labels', - }, - }; - - jest - .spyOn(objectMetadataService, 'findOneWithinWorkspace') - .mockResolvedValue(mockObject as ObjectMetadataEntity); - - const result = await hook.run( - instance as UpdateOneInputType, - { - workspaceId: mockWorkspaceId, - locale: undefined, - }, - ); - - const expectedResult = { - id: mockObjectId, - update: { - isLabelSyncedWithName: true, - standardOverrides: { - labelSingular: null, - labelPlural: null, - }, - }, - }; - - expect(result).toEqual(expectedResult); - }); - - it('should handle labelSingular override when not synced with name', async () => { - const instance: UpdateOneInputType = { - id: mockObjectId, - update: { - labelSingular: 'New Label', - }, - }; - - const mockObject: Partial = { - id: mockObjectId, - isCustom: false, - isLabelSyncedWithName: false, - labelSingular: 'Default Label', - }; - - jest - .spyOn(objectMetadataService, 'findOneWithinWorkspace') - .mockResolvedValue(mockObject as ObjectMetadataEntity); - - const result = await hook.run( - instance as UpdateOneInputType, - { - workspaceId: mockWorkspaceId, - locale: undefined, - }, - ); - - const expectedResult = { - id: mockObjectId, - update: { - standardOverrides: { - labelSingular: 'New Label', - }, - }, - }; - - expect(result).toEqual(expectedResult); - }); - - it('should handle labelPlural override when not synced with name', async () => { - const instance: UpdateOneInputType = { - id: mockObjectId, - update: { - labelPlural: 'New Labels', - }, - }; - - const mockObject: Partial = { - id: mockObjectId, - isCustom: false, - isLabelSyncedWithName: false, - labelPlural: 'Default Labels', - }; - - jest - .spyOn(objectMetadataService, 'findOneWithinWorkspace') - .mockResolvedValue(mockObject as ObjectMetadataEntity); - - const result = await hook.run( - instance as UpdateOneInputType, - { - workspaceId: mockWorkspaceId, - locale: undefined, - }, - ); - - const expectedResult = { - id: mockObjectId, - update: { - standardOverrides: { - labelPlural: 'New Labels', - }, - }, - }; - - expect(result).toEqual(expectedResult); - }); - - it('should reset labelSingular override when it matches the original value', async () => { - const instance: UpdateOneInputType = { - id: mockObjectId, - update: { - labelSingular: 'Default Label', - }, - }; - - const mockObject: Partial = { - id: mockObjectId, - isCustom: false, - isLabelSyncedWithName: false, - labelSingular: 'Default Label', - standardOverrides: { - labelSingular: 'Custom Label', - }, - }; - - jest - .spyOn(objectMetadataService, 'findOneWithinWorkspace') - .mockResolvedValue(mockObject as ObjectMetadataEntity); - - const result = await hook.run( - instance as UpdateOneInputType, - { - workspaceId: mockWorkspaceId, - locale: undefined, - }, - ); - - const expectedResult = { - id: mockObjectId, - update: { - standardOverrides: { - labelSingular: null, - }, - }, - }; - - expect(result).toEqual(expectedResult); - }); - - it('should handle description override', async () => { - const instance: UpdateOneInputType = { - id: mockObjectId, - update: { - description: 'New Description', - }, - }; - - const mockObject: Partial = { - id: mockObjectId, - isCustom: false, - description: 'Default Description', - }; - - jest - .spyOn(objectMetadataService, 'findOneWithinWorkspace') - .mockResolvedValue(mockObject as ObjectMetadataEntity); - - const result = await hook.run( - instance as UpdateOneInputType, - { - workspaceId: mockWorkspaceId, - locale: undefined, - }, - ); - - const expectedResult = { - id: mockObjectId, - update: { - standardOverrides: { - description: 'New Description', - }, - }, - }; - - expect(result).toEqual(expectedResult); - }); - - it('should handle icon override', async () => { - const instance: UpdateOneInputType = { - id: mockObjectId, - update: { - icon: 'IconStar', - }, - }; - - const mockObject: Partial = { - id: mockObjectId, - isCustom: false, - icon: 'IconCircle', - }; - - jest - .spyOn(objectMetadataService, 'findOneWithinWorkspace') - .mockResolvedValue(mockObject as ObjectMetadataEntity); - - const result = await hook.run( - instance as UpdateOneInputType, - { - workspaceId: mockWorkspaceId, - locale: undefined, - }, - ); - - const expectedResult = { - id: mockObjectId, - update: { - standardOverrides: { - icon: 'IconStar', - }, - }, - }; - - expect(result).toEqual(expectedResult); - }); - - it('should handle locale-specific translations', async () => { - const instance: UpdateOneInputType = { - id: mockObjectId, - update: { - labelSingular: 'Étiquette', - labelPlural: 'Étiquettes', - description: 'Description en français', - }, - }; - - const mockObject: Partial = { - id: mockObjectId, - isCustom: false, - isLabelSyncedWithName: false, - labelSingular: 'Label', - labelPlural: 'Labels', - description: 'Description', - }; - - jest - .spyOn(objectMetadataService, 'findOneWithinWorkspace') - .mockResolvedValue(mockObject as ObjectMetadataEntity); - - const result = await hook.run( - instance as UpdateOneInputType, - { - workspaceId: mockWorkspaceId, - locale: 'fr-FR', - }, - ); - - const expectedResult = { - id: mockObjectId, - update: { - standardOverrides: { - translations: { - 'fr-FR': { - labelSingular: 'Étiquette', - labelPlural: 'Étiquettes', - description: 'Description en français', - }, - }, - }, - }, - }; - - expect(result).toEqual(expectedResult); - }); - - it('should reset locale-specific translations when they match translated defaults', async () => { - const translatedLabel = 'mocked-translation'; - - const instance: UpdateOneInputType = { - id: mockObjectId, - update: { - labelSingular: translatedLabel, - }, - }; - - const mockObject: Partial = { - id: mockObjectId, - isCustom: false, - isLabelSyncedWithName: false, - labelSingular: 'Label', - standardOverrides: { - translations: { - 'fr-FR': { - labelSingular: 'Ancienne Étiquette', - }, - }, - }, - }; - - jest - .spyOn(objectMetadataService, 'findOneWithinWorkspace') - .mockResolvedValue(mockObject as ObjectMetadataEntity); - - const result = await hook.run( - instance as UpdateOneInputType, - { - workspaceId: mockWorkspaceId, - locale: 'fr-FR', - }, - ); - - const expectedResult = { - id: mockObjectId, - update: { - standardOverrides: { - translations: { - 'fr-FR': { - labelSingular: null, - }, - }, - }, - }, - }; - - expect(result).toEqual(expectedResult); - }); - - it('should handle multiple updates together', async () => { - const instance: UpdateOneInputType = { - id: mockObjectId, - update: { - isActive: false, - isLabelSyncedWithName: false, - labelSingular: 'New Label', - labelPlural: 'New Labels', - icon: 'IconStar', - description: 'New Description', - }, - }; - - const mockObject: Partial = { - id: mockObjectId, - isCustom: false, - isActive: true, - isLabelSyncedWithName: false, - labelSingular: 'Default Label', - labelPlural: 'Default Labels', - icon: 'IconCircle', - description: 'Default Description', - }; - - jest - .spyOn(objectMetadataService, 'findOneWithinWorkspace') - .mockResolvedValue(mockObject as ObjectMetadataEntity); - - const result = await hook.run( - instance as UpdateOneInputType, - { - workspaceId: mockWorkspaceId, - locale: undefined, - }, - ); - - const expectedResult = { - id: mockObjectId, - update: { - isActive: false, - isLabelSyncedWithName: false, - standardOverrides: { - labelSingular: 'New Label', - labelPlural: 'New Labels', - icon: 'IconStar', - description: 'New Description', - }, - }, - }; - - expect(result).toEqual(expectedResult); - }); - - it('should validate label identifier field correctly for custom objects', async () => { - const labelIdentifierFieldId = 'label-field-id'; - const instance: UpdateOneInputType = { - id: mockObjectId, - update: { - labelIdentifierFieldMetadataId: labelIdentifierFieldId, - }, - }; - - const mockObject: Partial = { - id: mockObjectId, - isCustom: true, - }; - - jest - .spyOn(objectMetadataService, 'findOneWithinWorkspace') - .mockResolvedValue(mockObject as ObjectMetadataEntity); - - jest - .spyOn(fieldMetadataRepository, 'findBy') - .mockImplementation(async (criteria) => { - expect(criteria).toHaveProperty('workspaceId'); - expect(criteria).toHaveProperty('objectMetadataId'); - expect(criteria).toHaveProperty('id'); - - return [{ id: labelIdentifierFieldId } as FieldMetadataEntity]; - }); - - const result = await hook.run( - instance as UpdateOneInputType, - { - workspaceId: mockWorkspaceId, - locale: undefined, - }, - ); - - expect(result).toEqual(instance); - }); - - it('should validate image identifier field correctly for custom objects', async () => { - const imageIdentifierFieldId = 'image-field-id'; - const instance: UpdateOneInputType = { - id: mockObjectId, - update: { - imageIdentifierFieldMetadataId: imageIdentifierFieldId, - }, - }; - - const mockObject: Partial = { - id: mockObjectId, - isCustom: true, - }; - - jest - .spyOn(objectMetadataService, 'findOneWithinWorkspace') - .mockResolvedValue(mockObject as ObjectMetadataEntity); - - jest - .spyOn(fieldMetadataRepository, 'findBy') - .mockImplementation(async (criteria) => { - expect(criteria).toHaveProperty('workspaceId'); - expect(criteria).toHaveProperty('objectMetadataId'); - expect(criteria).toHaveProperty('id'); - - return [{ id: imageIdentifierFieldId } as FieldMetadataEntity]; - }); - - const result = await hook.run( - instance as UpdateOneInputType, - { - workspaceId: mockWorkspaceId, - locale: undefined, - }, - ); - - expect(result).toEqual(instance); - }); -}); diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/hooks/before-create-one-object.hook.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/hooks/before-create-one-object.hook.ts deleted file mode 100644 index f189518625..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/hooks/before-create-one-object.hook.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Injectable, UnauthorizedException } from '@nestjs/common'; - -import { - type BeforeCreateOneHook, - type CreateOneInputType, -} from '@ptc-org/nestjs-query-graphql'; - -import { type CreateObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/create-object.input'; - -@Injectable() -export class BeforeCreateOneObject - implements BeforeCreateOneHook -{ - async run( - instance: CreateOneInputType, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - context: any, - ): Promise> { - const workspaceId = context?.req?.workspace?.id; - - if (!workspaceId) { - throw new UnauthorizedException(); - } - - instance.input.workspaceId = workspaceId; - - return instance; - } -} diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/hooks/before-delete-one-object.hook.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/hooks/before-delete-one-object.hook.ts deleted file mode 100644 index fd6894862d..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/hooks/before-delete-one-object.hook.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { - BadRequestException, - Injectable, - UnauthorizedException, -} from '@nestjs/common'; - -import { - type BeforeDeleteOneHook, - type DeleteOneInputType, -} from '@ptc-org/nestjs-query-graphql'; - -import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service'; - -@Injectable() -export class BeforeDeleteOneObject implements BeforeDeleteOneHook { - constructor(readonly objectMetadataService: ObjectMetadataService) {} - - async run( - instance: DeleteOneInputType, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - context: any, - ): Promise { - const workspaceId = context?.req?.workspace?.id; - - if (!workspaceId) { - throw new UnauthorizedException(); - } - - const objectMetadata = - await this.objectMetadataService.findOneWithinWorkspace(workspaceId, { - where: { - id: instance.id.toString(), - }, - }); - - if (!objectMetadata) { - throw new BadRequestException('Object does not exist'); - } - - if (!objectMetadata.isCustom) { - throw new BadRequestException("Standard Objects can't be deleted"); - } - - if (objectMetadata.isActive) { - throw new BadRequestException("Active objects can't be deleted"); - } - - return instance; - } -} diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/hooks/before-update-one-object.hook.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/hooks/before-update-one-object.hook.ts deleted file mode 100644 index e85693b391..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/hooks/before-update-one-object.hook.ts +++ /dev/null @@ -1,434 +0,0 @@ -import { - BadRequestException, - Injectable, - UnauthorizedException, -} from '@nestjs/common'; - -import { - type BeforeUpdateOneHook, - type UpdateOneInputType, -} from '@ptc-org/nestjs-query-graphql'; -import { type APP_LOCALES, SOURCE_LOCALE } from 'twenty-shared/translations'; -import { isDefined } from 'twenty-shared/utils'; - -import { generateMessageId } from 'src/engine/core-modules/i18n/utils/generateMessageId'; -import { I18nService } from 'src/engine/core-modules/i18n/i18n.service'; -import { type ObjectStandardOverridesDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-standard-overrides.dto'; -import { type UpdateObjectPayload } from 'src/engine/metadata-modules/object-metadata/dtos/update-object.input'; -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service'; - -interface StandardObjectUpdate extends Partial { - standardOverrides?: ObjectStandardOverridesDTO; -} - -@Injectable() -export class BeforeUpdateOneObject - implements BeforeUpdateOneHook -{ - constructor( - readonly objectMetadataService: ObjectMetadataService, - private readonly i18nService: I18nService, - ) {} - - // TODO: this logic could be moved to a policy guard - async run( - instance: UpdateOneInputType, - { - workspaceId, - locale, - }: { - workspaceId: string; - locale: keyof typeof APP_LOCALES | undefined; - }, - ): Promise> { - if (!workspaceId) { - throw new UnauthorizedException(); - } - - const objectMetadata = await this.getObjectMetadata(instance, workspaceId); - - if (!objectMetadata.isCustom) { - return this.handleStandardObjectUpdate(instance, objectMetadata, locale); - } - - return instance; - } - - private async getObjectMetadata( - instance: UpdateOneInputType, - workspaceId: string, - ) { - const objectMetadata = - await this.objectMetadataService.findOneWithinWorkspace(workspaceId, { - where: { - id: instance.id.toString(), - }, - }); - - if (!objectMetadata) { - throw new BadRequestException('Object does not exist'); - } - - return objectMetadata; - } - - private handleStandardObjectUpdate( - instance: UpdateOneInputType, - objectMetadata: ObjectMetadataEntity, - locale: keyof typeof APP_LOCALES | undefined, - ): UpdateOneInputType { - const update: StandardObjectUpdate = {}; - const updatableFields = ['isActive', 'isLabelSyncedWithName']; - const overridableFields = [ - 'labelSingular', - 'labelPlural', - 'icon', - 'description', - ]; - - const nonUpdatableFields = Object.keys(instance.update).filter( - (key) => - !updatableFields.includes(key) && !overridableFields.includes(key), - ); - - const isUpdatingLabelsWhenSynced = - (instance.update.labelSingular || instance.update.labelPlural) && - objectMetadata.isLabelSyncedWithName && - instance.update.isLabelSyncedWithName !== false && - (instance.update.labelSingular !== objectMetadata.labelSingular || - instance.update.labelPlural !== objectMetadata.labelPlural); - - if (isUpdatingLabelsWhenSynced) { - throw new BadRequestException( - 'Cannot update labels when they are synced with name', - ); - } - - if (nonUpdatableFields.length > 0) { - throw new BadRequestException( - `Only isActive, isLabelSyncedWithName, labelSingular, labelPlural, icon and description fields can be updated for standard objects. Disallowed fields: ${nonUpdatableFields.join(', ')}`, - ); - } - - // preserve existing overrides - update.standardOverrides = objectMetadata.standardOverrides - ? { ...objectMetadata.standardOverrides } - : {}; - - this.handleActiveField(instance, update); - this.handleLabelSyncedWithNameField(instance, update); - this.handleStandardOverrides(instance, objectMetadata, update, locale); - - return { - id: instance.id, - update: update as T, - }; - } - - private handleActiveField( - instance: UpdateOneInputType, - update: StandardObjectUpdate, - ): void { - if (!isDefined(instance.update.isActive)) { - return; - } - - update.isActive = instance.update.isActive; - } - - private handleLabelSyncedWithNameField( - instance: UpdateOneInputType, - update: StandardObjectUpdate, - ): void { - if (!isDefined(instance.update.isLabelSyncedWithName)) { - return; - } - - update.isLabelSyncedWithName = instance.update.isLabelSyncedWithName; - - if (instance.update.isLabelSyncedWithName === false) { - return; - } - - // If setting isLabelSyncedWithName to true, clear label overrides - update.standardOverrides = update.standardOverrides || {}; - update.standardOverrides.labelSingular = null; - update.standardOverrides.labelPlural = null; - } - - private handleStandardOverrides( - instance: UpdateOneInputType, - objectMetadata: ObjectMetadataEntity, - update: StandardObjectUpdate, - locale: keyof typeof APP_LOCALES | undefined, - ): void { - const hasStandardOverrides = - isDefined(instance.update.description) || - isDefined(instance.update.icon) || - isDefined(instance.update.labelSingular) || - isDefined(instance.update.labelPlural); - - if (!hasStandardOverrides) { - return; - } - - update.standardOverrides = update.standardOverrides || {}; - - this.handleIconOverride(instance, objectMetadata, update); - this.handleDescriptionOverride(instance, objectMetadata, update, locale); - this.handleLabelOverrides(instance, objectMetadata, update, locale); - } - - private resetOverrideIfMatchesOriginal({ - update, - overrideKey, - newValue, - originalValue, - locale, - }: { - update: StandardObjectUpdate; - overrideKey: 'labelSingular' | 'labelPlural' | 'description' | 'icon'; - newValue: string; - originalValue: string | null; - locale?: keyof typeof APP_LOCALES | undefined; - }): boolean { - if (locale && locale !== SOURCE_LOCALE) { - const wasOverrideReset = this.resetLocalizedOverride( - update, - overrideKey, - newValue, - originalValue, - locale, - ); - - return wasOverrideReset; - } - - const wasOverrideReset = this.resetDefaultOverride( - update, - overrideKey, - newValue, - originalValue, - ); - - return wasOverrideReset; - } - - private resetLocalizedOverride( - update: StandardObjectUpdate, - overrideKey: 'labelSingular' | 'labelPlural' | 'description' | 'icon', - newValue: string, - originalValue: string | null, - locale: keyof typeof APP_LOCALES, - ): boolean { - const messageId = generateMessageId(originalValue ?? ''); - const i18n = this.i18nService.getI18nInstance(locale); - const translatedMessage = i18n._(messageId); - - if (newValue !== translatedMessage) { - return false; - } - - // Initialize the translations structure if needed - update.standardOverrides = update.standardOverrides || {}; - update.standardOverrides.translations = - update.standardOverrides.translations || {}; - update.standardOverrides.translations[locale] = - update.standardOverrides.translations[locale] || {}; - - // Reset the override by setting it to null - const localeTranslations = update.standardOverrides.translations[locale]; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (localeTranslations as Record)[overrideKey] = null; - - return true; - } - - private resetDefaultOverride( - update: StandardObjectUpdate, - overrideKey: 'labelSingular' | 'labelPlural' | 'description' | 'icon', - newValue: string, - originalValue: string | null, - ): boolean { - if (newValue !== originalValue) { - return false; - } - - update.standardOverrides = update.standardOverrides || {}; - update.standardOverrides[overrideKey] = null; - - return true; - } - - private setOverrideValue( - update: StandardObjectUpdate, - overrideKey: 'labelSingular' | 'labelPlural' | 'description' | 'icon', - value: string, - locale?: keyof typeof APP_LOCALES, - ): void { - update.standardOverrides = update.standardOverrides || {}; - - const shouldSetLocalizedOverride = - locale && locale !== SOURCE_LOCALE && overrideKey !== 'icon'; - - if (!shouldSetLocalizedOverride) { - update.standardOverrides[overrideKey] = value; - - return; - } - - this.setLocalizedOverrideValue(update, overrideKey, value, locale); - } - - private setLocalizedOverrideValue( - update: StandardObjectUpdate, - overrideKey: 'labelSingular' | 'labelPlural' | 'description' | 'icon', - value: string, - locale: keyof typeof APP_LOCALES, - ): void { - update.standardOverrides = update.standardOverrides || {}; - update.standardOverrides.translations = - update.standardOverrides.translations || {}; - update.standardOverrides.translations[locale] = - update.standardOverrides.translations[locale] || {}; - - const localeTranslations = update.standardOverrides.translations[locale]; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (localeTranslations as Record)[overrideKey] = value; - } - - private handleDescriptionOverride( - instance: UpdateOneInputType, - objectMetadata: ObjectMetadataEntity, - update: StandardObjectUpdate, - locale?: keyof typeof APP_LOCALES, - ): void { - if (!isDefined(instance.update.description)) { - return; - } - - if ( - this.resetOverrideIfMatchesOriginal({ - update, - overrideKey: 'description', - newValue: instance.update.description, - originalValue: objectMetadata.description, - locale, - }) - ) { - return; - } - - this.setOverrideValue( - update, - 'description', - instance.update.description, - locale, - ); - } - - private handleIconOverride( - instance: UpdateOneInputType, - objectMetadata: ObjectMetadataEntity, - update: StandardObjectUpdate, - ): void { - if (!isDefined(instance.update.icon)) { - return; - } - - if ( - this.resetOverrideIfMatchesOriginal({ - update, - overrideKey: 'icon', - newValue: instance.update.icon, - originalValue: objectMetadata.icon, - locale: undefined, - }) - ) { - return; - } - - this.setOverrideValue(update, 'icon', instance.update.icon); - } - - private handleLabelOverrides( - instance: UpdateOneInputType, - objectMetadata: ObjectMetadataEntity, - update: StandardObjectUpdate, - locale?: keyof typeof APP_LOCALES, - ): void { - // Skip label updates if labels are synced with name or will be synced - if ( - objectMetadata.isLabelSyncedWithName || - update.isLabelSyncedWithName === true - ) { - return; - } - - this.handleLabelSingularOverride(instance, objectMetadata, update, locale); - this.handleLabelPluralOverride(instance, objectMetadata, update, locale); - } - - private handleLabelSingularOverride( - instance: UpdateOneInputType, - objectMetadata: ObjectMetadataEntity, - update: StandardObjectUpdate, - locale?: keyof typeof APP_LOCALES, - ): void { - if (!isDefined(instance.update.labelSingular)) { - return; - } - - if ( - this.resetOverrideIfMatchesOriginal({ - update, - overrideKey: 'labelSingular', - newValue: instance.update.labelSingular, - originalValue: objectMetadata.labelSingular, - locale, - }) - ) { - return; - } - - this.setOverrideValue( - update, - 'labelSingular', - instance.update.labelSingular, - locale, - ); - } - - private handleLabelPluralOverride( - instance: UpdateOneInputType, - objectMetadata: ObjectMetadataEntity, - update: StandardObjectUpdate, - locale?: keyof typeof APP_LOCALES, - ): void { - if (!isDefined(instance.update.labelPlural)) { - return; - } - - if ( - this.resetOverrideIfMatchesOriginal({ - update, - overrideKey: 'labelPlural', - newValue: instance.update.labelPlural, - originalValue: objectMetadata.labelPlural, - locale, - }) - ) { - return; - } - - this.setOverrideValue( - update, - 'labelPlural', - instance.update.labelPlural, - locale, - ); - } -} diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata-v2.service.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata-v2.service.ts index 4a801c2fe0..1b15ed9d76 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata-v2.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata-v2.service.ts @@ -1,6 +1,9 @@ import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm'; import { fromArrayToUniqueKeyRecord, isDefined } from 'twenty-shared/utils'; +import { FindManyOptions, FindOneOptions, 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'; @@ -10,15 +13,14 @@ import { FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metada import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { fromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCreate } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-create-object-input-to-flat-object-metadata-and-flat-field-metadatas-to-create.util'; import { fromDeleteObjectInputToFlatFieldMetadatasToDelete } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-delete-object-input-to-flat-field-metadatas-to-delete.util'; -import { fromFlatObjectMetadataToObjectMetadataDto } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-flat-object-metadata-to-object-metadata-dto.util'; import { fromUpdateObjectInputToFlatObjectMetadataAndRelatedFlatEntities } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-update-object-input-to-flat-object-metadata-and-related-flat-entities.util'; import { fromCreateViewFieldInputToFlatViewFieldToCreate } from 'src/engine/metadata-modules/flat-view-field/utils/from-create-view-field-input-to-flat-view-field-to-create.util'; import { 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 { CreateObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/create-object.input'; import { DeleteOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/delete-object.input'; -import { ObjectMetadataDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-metadata.dto'; import { UpdateOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/update-object.input'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { ObjectMetadataException, ObjectMetadataExceptionCode, @@ -33,21 +35,25 @@ import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspa import { FavoriteWorkspaceEntity } from 'src/modules/favorite/standard-objects/favorite.workspace-entity'; @Injectable() -export class ObjectMetadataServiceV2 { +export class ObjectMetadataServiceV2 extends TypeOrmQueryService { constructor( + @InjectRepository(ObjectMetadataEntity) + private readonly objectMetadataRepository: Repository, private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService, private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService, private readonly twentyORMGlobalManager: TwentyORMGlobalManager, - ) {} + ) { + super(objectMetadataRepository); + } - async updateOne({ + async updateOneObject({ updateObjectInput, workspaceId, }: { workspaceId: string; updateObjectInput: UpdateOneObjectInput; - }): Promise { + }): Promise { const { flatObjectMetadataMaps: existingFlatObjectMetadataMaps, flatIndexMaps: existingFlatIndexMaps, @@ -154,10 +160,10 @@ export class ObjectMetadataServiceV2 { ); } - return fromFlatObjectMetadataToObjectMetadataDto(updatedFlatObjectMetadata); + return updatedFlatObjectMetadata; } - async deleteOne({ + async deleteOneObject({ deleteObjectInput, workspaceId, isSystemBuild = false, @@ -165,7 +171,7 @@ export class ObjectMetadataServiceV2 { deleteObjectInput: DeleteOneObjectInput; workspaceId: string; isSystemBuild?: boolean; - }): Promise { + }): Promise { const deletedObjectMetadataDtos = await this.deleteManyObjectMetadatas({ deleteObjectInputs: [deleteObjectInput], workspaceId, @@ -192,7 +198,7 @@ export class ObjectMetadataServiceV2 { deleteObjectInputs: DeleteOneObjectInput[]; workspaceId: string; isSystemBuild?: boolean; - }): Promise { + }): Promise { if (deleteObjectInputs.length === 0) { return []; } @@ -311,16 +317,14 @@ export class ObjectMetadataServiceV2 { ); } - return flatObjectMetadatasToDelete.map( - fromFlatObjectMetadataToObjectMetadataDto, - ); + return flatObjectMetadatasToDelete; } - async createOne({ + async createOneObject({ createObjectInput, workspaceId, }: { - createObjectInput: Omit; + createObjectInput: CreateObjectInput; workspaceId: string; }): Promise { const { @@ -549,4 +553,43 @@ export class ObjectMetadataServiceV2 { isSystemBuild: true, }); } + + public async findOneWithinWorkspace( + workspaceId: string, + options: FindOneOptions, + ): Promise { + return this.objectMetadataRepository.findOne({ + relations: [ + 'fields', + 'indexMetadatas', + 'indexMetadatas.indexFieldMetadatas', + ], + ...options, + where: { + ...options.where, + workspaceId, + }, + }); + } + + public async findManyWithinWorkspace( + workspaceId: string, + options?: FindManyOptions, + ) { + return this.objectMetadataRepository.find({ + relations: [ + 'fields.object', + 'fields', + 'fields.relationTargetObjectMetadata', + ], + ...options, + where: { + ...options?.where, + workspaceId, + }, + order: { + ...options?.order, + }, + }); + } } diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.module.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.module.ts index 42432724e4..e9185fb82c 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.module.ts @@ -11,7 +11,6 @@ import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm'; import { TypeORMModule } from 'src/database/typeorm/typeorm.module'; import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; -import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; @@ -21,20 +20,13 @@ import { IndexMetadataModule } from 'src/engine/metadata-modules/index-metadata/ import { CreateObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/create-object.input'; import { ObjectMetadataDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-metadata.dto'; import { UpdateObjectPayload } from 'src/engine/metadata-modules/object-metadata/dtos/update-object.input'; -import { BeforeUpdateOneObject } from 'src/engine/metadata-modules/object-metadata/hooks/before-update-one-object.hook'; import { ObjectMetadataGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/object-metadata/interceptors/object-metadata-graphql-api-exception.interceptor'; import { ObjectMetadataServiceV2 } from 'src/engine/metadata-modules/object-metadata/object-metadata-v2.service'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { ObjectMetadataResolver } from 'src/engine/metadata-modules/object-metadata/object-metadata.resolver'; -import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service'; -import { ObjectMetadataFieldRelationService } from 'src/engine/metadata-modules/object-metadata/services/object-metadata-field-relation.service'; -import { ObjectMetadataMigrationService } from 'src/engine/metadata-modules/object-metadata/services/object-metadata-migration.service'; -import { ObjectMetadataRelatedRecordsService } from 'src/engine/metadata-modules/object-metadata/services/object-metadata-related-records.service'; -import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants'; import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter'; import { RemoteTableRelationsModule } from 'src/engine/metadata-modules/remote-server/remote-table/remote-table-relations/remote-table-relations.module'; -import { SearchVectorModule } from 'src/engine/metadata-modules/search-vector/search-vector.module'; import { ViewFieldModule } from 'src/engine/metadata-modules/view-field/view-field.module'; import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity'; import { ViewModule } from 'src/engine/metadata-modules/view/view.module'; @@ -63,7 +55,6 @@ import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspa WorkspaceMigrationRunnerModule, WorkspaceMetadataVersionModule, RemoteTableRelationsModule, - SearchVectorModule, IndexMetadataModule, PermissionsModule, WorkspacePermissionsCacheModule, @@ -77,13 +68,9 @@ import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspa WorkspaceManyOrAllFlatEntityMapsCacheModule, ], services: [ - ObjectMetadataService, ObjectMetadataServiceV2, FlatFieldMetadataValidatorService, FlatFieldMetadataTypeValidatorService, - ObjectMetadataMigrationService, - ObjectMetadataFieldRelationService, - ObjectMetadataRelatedRecordsService, ], resolvers: [ { @@ -91,14 +78,13 @@ import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspa DTOClass: ObjectMetadataDTO, CreateDTOClass: CreateObjectInput, UpdateDTOClass: UpdateObjectPayload, - ServiceClass: ObjectMetadataService, + ServiceClass: ObjectMetadataServiceV2, pagingStrategy: PagingStrategies.CURSOR, read: { defaultSort: [{ field: 'id', direction: SortDirection.DESC }], }, create: { - many: { disabled: true }, - guards: [SettingsPermissionGuard(PermissionFlagType.DATA_MODEL)], + disabled: true, }, update: { disabled: true }, delete: { disabled: true }, @@ -109,12 +95,7 @@ import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspa ], }), ], - providers: [ - ObjectMetadataService, - ObjectMetadataServiceV2, - ObjectMetadataResolver, - BeforeUpdateOneObject, - ], - exports: [ObjectMetadataService, ObjectMetadataServiceV2], + providers: [ObjectMetadataServiceV2, ObjectMetadataResolver], + exports: [ObjectMetadataServiceV2], }) export class ObjectMetadataModule {} diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.resolver.ts index 55774ebf25..6bd7010460 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.resolver.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.resolver.ts @@ -8,8 +8,6 @@ import { 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 { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter'; import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe'; import { I18nService } from 'src/engine/core-modules/i18n/i18n.service'; @@ -20,16 +18,13 @@ import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorat import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; import { FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto'; +import { fromFlatObjectMetadataToObjectMetadataDto } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-flat-object-metadata-to-object-metadata-dto.util'; import { IndexMetadataDTO } from 'src/engine/metadata-modules/index-metadata/dtos/index-metadata.dto'; +import { CreateOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/create-object.input'; import { DeleteOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/delete-object.input'; import { ObjectMetadataDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-metadata.dto'; -import { - UpdateOneObjectInput, - type UpdateObjectPayload, -} from 'src/engine/metadata-modules/object-metadata/dtos/update-object.input'; -import { BeforeUpdateOneObject } from 'src/engine/metadata-modules/object-metadata/hooks/before-update-one-object.hook'; +import { UpdateOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/update-object.input'; import { ObjectMetadataServiceV2 } from 'src/engine/metadata-modules/object-metadata/object-metadata-v2.service'; -import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service'; import { objectMetadataGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/object-metadata/utils/object-metadata-graphql-api-exception-handler.util'; import { resolveObjectMetadataStandardOverride } from 'src/engine/metadata-modules/object-metadata/utils/resolve-object-metadata-standard-override.util'; import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants'; @@ -44,9 +39,6 @@ import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-module ) export class ObjectMetadataResolver { constructor( - private readonly objectMetadataService: ObjectMetadataService, - private readonly beforeUpdateOneObject: BeforeUpdateOneObject, - private readonly featureFlagService: FeatureFlagService, private readonly objectMetadataServiceV2: ObjectMetadataServiceV2, private readonly i18nService: I18nService, ) {} @@ -114,16 +106,42 @@ export class ObjectMetadataResolver { @UseGuards(SettingsPermissionGuard(PermissionFlagType.DATA_MODEL)) @Mutation(() => ObjectMetadataDTO) - async deleteOneObject( - @Args('input') input: DeleteOneObjectInput, + async createOneObject( + @Args('input') input: CreateOneObjectInput, @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, @Context() context: I18nContext, ) { try { - return await this.objectMetadataService.deleteOneObject( - input, - workspaceId, + const flatobjectMetadata = + await this.objectMetadataServiceV2.createOneObject({ + createObjectInput: input.object, + workspaceId, + }); + + return fromFlatObjectMetadataToObjectMetadataDto(flatobjectMetadata); + } catch (error) { + objectMetadataGraphqlApiExceptionHandler( + error, + this.i18nService.getI18nInstance(context.req.locale), ); + } + } + + @UseGuards(SettingsPermissionGuard(PermissionFlagType.DATA_MODEL)) + @Mutation(() => ObjectMetadataDTO) + async deleteOneObject( + @Args('input') deleteObjectInput: DeleteOneObjectInput, + @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, + @Context() context: I18nContext, + ) { + try { + const flatobjectMetadata = + await this.objectMetadataServiceV2.deleteOneObject({ + deleteObjectInput, + workspaceId, + }); + + return fromFlatObjectMetadataToObjectMetadataDto(flatobjectMetadata); } catch (error) { objectMetadataGraphqlApiExceptionHandler( error, @@ -139,39 +157,14 @@ export class ObjectMetadataResolver { @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, @Context() context: I18nContext, ) { - const isWorkspaceMigrationV2Enabled = - await this.featureFlagService.isFeatureEnabled( - FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED, - workspaceId, - ); - - if (isWorkspaceMigrationV2Enabled) { - try { - return await this.objectMetadataServiceV2.updateOne({ + try { + const flatobjectMetadata = + await this.objectMetadataServiceV2.updateOneObject({ updateObjectInput, workspaceId, }); - } catch (error) { - objectMetadataGraphqlApiExceptionHandler( - error, - this.i18nService.getI18nInstance(context.req.locale), - ); - } - } - try { - const updatedInput = (await this.beforeUpdateOneObject.run( - updateObjectInput, - { - workspaceId, - locale: context.req.locale, - }, - )) as UpdateOneObjectInput; - - return await this.objectMetadataService.updateOneObject( - updatedInput, - workspaceId, - ); + return fromFlatObjectMetadataToObjectMetadataDto(flatobjectMetadata); } catch (error) { objectMetadataGraphqlApiExceptionHandler( error, diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts deleted file mode 100644 index 8741680db7..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts +++ /dev/null @@ -1,792 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; - -import { type Query, type QueryOptions } from '@ptc-org/nestjs-query-core'; -import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm'; -import { FieldMetadataType } from 'twenty-shared/types'; -import { capitalize, isDefined } from 'twenty-shared/utils'; -import { - DataSource, - In, - Repository, - type FindManyOptions, - type FindOneOptions, - type QueryRunner, -} from 'typeorm'; - -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 { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; -import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; -import { IndexMetadataService } from 'src/engine/metadata-modules/index-metadata/index-metadata.service'; -import { type DeleteOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/delete-object.input'; -import { - type UpdateObjectPayload, - type UpdateOneObjectInput, -} from 'src/engine/metadata-modules/object-metadata/dtos/update-object.input'; -import { ObjectMetadataServiceV2 } from 'src/engine/metadata-modules/object-metadata/object-metadata-v2.service'; -import { - ObjectMetadataException, - ObjectMetadataExceptionCode, -} from 'src/engine/metadata-modules/object-metadata/object-metadata.exception'; -import { ObjectMetadataFieldRelationService } from 'src/engine/metadata-modules/object-metadata/services/object-metadata-field-relation.service'; -import { ObjectMetadataMigrationService } from 'src/engine/metadata-modules/object-metadata/services/object-metadata-migration.service'; -import { ObjectMetadataRelatedRecordsService } from 'src/engine/metadata-modules/object-metadata/services/object-metadata-related-records.service'; -import { buildDefaultFieldsForCustomObject } from 'src/engine/metadata-modules/object-metadata/utils/build-default-fields-for-custom-object.util'; -import { - validateLowerCasedAndTrimmedStringsAreDifferentOrThrow, - validateObjectMetadataInputLabelsOrThrow, - validateObjectMetadataInputNamesOrThrow, -} from 'src/engine/metadata-modules/object-metadata/utils/validate-object-metadata-input.util'; -import { SearchVectorService } from 'src/engine/metadata-modules/search-vector/search-vector.service'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; -import { validateMetadataIdentifierFieldMetadataIds } from 'src/engine/metadata-modules/utils/validate-metadata-identifier-field-metadata-id.utils'; -import { validateNameAndLabelAreSyncOrThrow } from 'src/engine/metadata-modules/utils/validate-name-and-label-are-sync-or-throw.util'; -import { validatesNoOtherObjectWithSameNameExistsOrThrows } from 'src/engine/metadata-modules/utils/validate-no-other-object-with-same-name-exists-or-throw.util'; -import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service'; -import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service'; -import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service'; -import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; -import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; -import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service'; -import { CUSTOM_OBJECT_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids'; -import { isSearchableFieldType } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/is-searchable-field.util'; - -import { ObjectMetadataEntity } from './object-metadata.entity'; - -import { type CreateObjectInput } from './dtos/create-object.input'; - -@Injectable() -export class ObjectMetadataService extends TypeOrmQueryService { - constructor( - @InjectRepository(ObjectMetadataEntity) - private readonly objectMetadataRepository: Repository, - @InjectRepository(FieldMetadataEntity) - private readonly fieldMetadataRepository: Repository, - - private readonly dataSourceService: DataSourceService, - private readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService, - private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService, - private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService, - private readonly searchVectorService: SearchVectorService, - private readonly objectMetadataFieldRelationService: ObjectMetadataFieldRelationService, - private readonly objectMetadataMigrationService: ObjectMetadataMigrationService, - private readonly objectMetadataRelatedRecordsService: ObjectMetadataRelatedRecordsService, - private readonly indexMetadataService: IndexMetadataService, - private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService, - private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, - @InjectDataSource() - private readonly coreDataSource: DataSource, - private readonly featureFlagService: FeatureFlagService, - private readonly objectMetadataServiceV2: ObjectMetadataServiceV2, - ) { - super(objectMetadataRepository); - } - - override async query( - query: Query, - opts?: QueryOptions | undefined, - ): Promise { - const start = performance.now(); - - const result = super.query(query, opts); - - const end = performance.now(); - - // eslint-disable-next-line no-console - console.log(`metadata query time: ${end - start} ms`); - - return result; - } - - override async createOne( - createObjectInput: CreateObjectInput, - ): Promise { - const { workspaceId } = createObjectInput; - const isWorkspaceMigrationV2Enabled = - await this.featureFlagService.isFeatureEnabled( - FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED, - workspaceId, - ); - - if (isWorkspaceMigrationV2Enabled) { - const flatObjectMetadata = await this.objectMetadataServiceV2.createOne({ - createObjectInput, - workspaceId, - }); - - const createdObjectMetadata = await this.objectMetadataRepository.findOne( - { - where: { - id: flatObjectMetadata.id, - workspaceId, - }, - }, - ); - - if (!isDefined(createdObjectMetadata)) { - throw new ObjectMetadataException( - 'Created object metadata not found', - ObjectMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND, - ); - } - - return createdObjectMetadata; - } - - const queryRunner = this.coreDataSource.createQueryRunner(); - - await queryRunner.connect(); - await queryRunner.startTransaction(); - - try { - const objectMetadataRepository = - queryRunner.manager.getRepository(ObjectMetadataEntity); - - const { objectMetadataMaps } = - await this.workspaceMetadataCacheService.getExistingOrRecomputeMetadataMaps( - { - workspaceId, - }, - ); - - const lastDataSourceMetadata = - await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail( - workspaceId, - ); - - createObjectInput.labelSingular = capitalize( - createObjectInput.labelSingular, - ); - createObjectInput.labelPlural = capitalize(createObjectInput.labelPlural); - - validateObjectMetadataInputNamesOrThrow(createObjectInput); - validateObjectMetadataInputLabelsOrThrow(createObjectInput); - - validateLowerCasedAndTrimmedStringsAreDifferentOrThrow({ - inputs: [createObjectInput.nameSingular, createObjectInput.namePlural], - message: - 'The singular and plural names cannot be the same for an object', - }); - validateLowerCasedAndTrimmedStringsAreDifferentOrThrow({ - inputs: [ - createObjectInput.labelPlural, - createObjectInput.labelSingular, - ], - message: - 'The singular and plural labels cannot be the same for an object', - }); - - if (createObjectInput.isLabelSyncedWithName === true) { - validateNameAndLabelAreSyncOrThrow({ - label: createObjectInput.labelSingular, - name: createObjectInput.nameSingular, - }); - validateNameAndLabelAreSyncOrThrow({ - label: createObjectInput.labelPlural, - name: createObjectInput.namePlural, - }); - } - - validatesNoOtherObjectWithSameNameExistsOrThrows({ - objectMetadataNamePlural: createObjectInput.namePlural, - objectMetadataNameSingular: createObjectInput.nameSingular, - objectMetadataMaps, - }); - - const baseCustomFields = buildDefaultFieldsForCustomObject(workspaceId); - - const labelIdentifierFieldMetadataId = baseCustomFields.find( - (field) => field.standardId === CUSTOM_OBJECT_STANDARD_FIELD_IDS.name, - )?.id; - - if (!isDefined(labelIdentifierFieldMetadataId)) { - throw new ObjectMetadataException( - 'Label identifier field metadata not created properly', - ObjectMetadataExceptionCode.MISSING_CUSTOM_OBJECT_DEFAULT_LABEL_IDENTIFIER_FIELD, - ); - } - - const createdObjectMetadata = await objectMetadataRepository.save({ - ...createObjectInput, - dataSourceId: lastDataSourceMetadata.id, - targetTableName: 'DEPRECATED', - isActive: true, - isCustom: !createObjectInput.isRemote, - isSystem: false, - isRemote: createObjectInput.isRemote, - isSearchable: !createObjectInput.isRemote, - fields: createObjectInput.isRemote ? [] : baseCustomFields, - labelIdentifierFieldMetadataId, - }); - - if (createObjectInput.isRemote) { - throw new Error('Remote objects are not supported yet'); - } else { - const fieldsById = createdObjectMetadata.fields.reduce( - (acc, field) => ({ - ...acc, - [field.id]: field, - }), - {}, - ); - - const createdRelatedObjectMetadataCollection = - await this.objectMetadataFieldRelationService.createRelationsAndForeignKeysMetadata( - workspaceId, - { ...createdObjectMetadata, fieldsById }, - objectMetadataMaps, - queryRunner, - ); - - await this.objectMetadataMigrationService.createTableMigration( - createdObjectMetadata, - queryRunner, - ); - - await this.objectMetadataMigrationService.createColumnsMigrations( - createdObjectMetadata, - createdObjectMetadata.fields, - queryRunner, - ); - - await this.objectMetadataMigrationService.createRelationMigrations( - createdObjectMetadata, - createdRelatedObjectMetadataCollection, - queryRunner, - ); - - await this.searchVectorService.createSearchVectorFieldForObject( - createObjectInput, - createdObjectMetadata, - queryRunner, - ); - } - - await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrationsWithinTransaction( - createdObjectMetadata.workspaceId, - queryRunner, - ); - - await queryRunner.commitTransaction(); - - // After commit, do non-transactional work - await this.workspacePermissionsCacheService.recomputeRolesPermissionsCache( - { - workspaceId, - }, - ); - await this.objectMetadataRelatedRecordsService.createObjectRelatedRecords( - createdObjectMetadata, - ); - - await this.workspaceMetadataVersionService.incrementMetadataVersion( - workspaceId, - ); - - await this.flatEntityMapsCacheService.invalidateFlatEntityMaps({ - workspaceId, - flatMapsKeys: ['flatFieldMetadataMaps', 'flatObjectMetadataMaps'], - }); - - return createdObjectMetadata; - } catch (error) { - if (queryRunner.isTransactionActive) { - try { - await queryRunner.rollbackTransaction(); - } catch (error) { - // eslint-disable-next-line no-console - console.trace(`Failed to rollback transaction: ${error.message}`); - } - } - throw error; - } finally { - await queryRunner.release(); - } - } - - public async updateOneObject( - updateObjectInput: UpdateOneObjectInput, - workspaceId: string, - ): Promise { - const queryRunner = this.coreDataSource.createQueryRunner(); - - await queryRunner.connect(); - await queryRunner.startTransaction(); - - try { - const objectMetadataRepository = - queryRunner.manager.getRepository(ObjectMetadataEntity); - - const { objectMetadataMaps } = - await this.workspaceMetadataCacheService.getExistingOrRecomputeMetadataMaps( - { workspaceId }, - ); - const inputId = updateObjectInput.id; - const inputPayload = { - ...updateObjectInput.update, - ...(isDefined(updateObjectInput.update.labelSingular) - ? { - labelSingular: capitalize(updateObjectInput.update.labelSingular), - } - : {}), - ...(isDefined(updateObjectInput.update.labelPlural) - ? { labelPlural: capitalize(updateObjectInput.update.labelPlural) } - : {}), - }; - - validateObjectMetadataInputNamesOrThrow(inputPayload); - const existingObjectMetadata = objectMetadataMaps.byId[inputId]; - - if (!existingObjectMetadata) { - throw new ObjectMetadataException( - 'Object does not exist', - ObjectMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND, - ); - } - const existingObjectMetadataCombinedWithUpdateInput = { - ...existingObjectMetadata, - ...inputPayload, - }; - - validatesNoOtherObjectWithSameNameExistsOrThrows({ - objectMetadataNameSingular: - existingObjectMetadataCombinedWithUpdateInput.nameSingular, - objectMetadataNamePlural: - existingObjectMetadataCombinedWithUpdateInput.namePlural, - existingObjectMetadataId: - existingObjectMetadataCombinedWithUpdateInput.id, - objectMetadataMaps, - }); - if (existingObjectMetadataCombinedWithUpdateInput.isLabelSyncedWithName) { - validateNameAndLabelAreSyncOrThrow({ - label: existingObjectMetadataCombinedWithUpdateInput.labelSingular, - name: existingObjectMetadataCombinedWithUpdateInput.nameSingular, - }); - validateNameAndLabelAreSyncOrThrow({ - label: existingObjectMetadataCombinedWithUpdateInput.labelPlural, - name: existingObjectMetadataCombinedWithUpdateInput.namePlural, - }); - } - if ( - isDefined(inputPayload.nameSingular) || - isDefined(inputPayload.namePlural) - ) { - validateLowerCasedAndTrimmedStringsAreDifferentOrThrow({ - inputs: [ - existingObjectMetadataCombinedWithUpdateInput.nameSingular, - existingObjectMetadataCombinedWithUpdateInput.namePlural, - ], - message: - 'The singular and plural names cannot be the same for an object', - }); - } - validateMetadataIdentifierFieldMetadataIds({ - fieldMetadataItems: Object.values(existingObjectMetadata.fieldsById), - labelIdentifierFieldMetadataId: - inputPayload.labelIdentifierFieldMetadataId, - imageIdentifierFieldMetadataId: - inputPayload.imageIdentifierFieldMetadataId, - }); - const updatedObject = await objectMetadataRepository.save({ - ...existingObjectMetadata, - ...inputPayload, - }); - - const { didUpdateLabelOrIcon } = - await this.handleObjectNameAndLabelUpdates({ - existingObjectMetadata, - objectMetadataForUpdate: - existingObjectMetadataCombinedWithUpdateInput, - inputPayload, - queryRunner, - objectMetadataMaps, - }); - - await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrationsWithinTransaction( - workspaceId, - queryRunner, - ); - - if (inputPayload.labelIdentifierFieldMetadataId) { - const labelIdentifierFieldMetadata = - existingObjectMetadata.fieldsById[ - inputPayload.labelIdentifierFieldMetadataId - ]; - - if (isSearchableFieldType(labelIdentifierFieldMetadata.type)) { - await this.searchVectorService.updateSearchVector( - inputId, - [ - { - name: labelIdentifierFieldMetadata.name, - type: labelIdentifierFieldMetadata.type, - }, - ], - workspaceId, - queryRunner, - ); - } - await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrationsWithinTransaction( - workspaceId, - queryRunner, - ); - } - - await queryRunner.commitTransaction(); - - // After commit, do non-transactional work - await this.workspacePermissionsCacheService.recomputeRolesPermissionsCache( - { - workspaceId, - }, - ); - - if (didUpdateLabelOrIcon) { - await this.objectMetadataRelatedRecordsService.updateObjectViews( - updatedObject, - workspaceId, - ); - } - - if ( - isDefined(inputPayload.labelIdentifierFieldMetadataId) && - inputPayload.labelIdentifierFieldMetadataId !== - existingObjectMetadata.labelIdentifierFieldMetadataId - ) { - const labelIdentifierFieldMetadata = - existingObjectMetadata.fieldsById[ - inputPayload.labelIdentifierFieldMetadataId - ]; - - await this.objectMetadataRelatedRecordsService.updateLabelMetadataIdentifierInObjectViews( - { - newLabelMetadataIdentifierFieldMetadata: - labelIdentifierFieldMetadata, - }, - ); - } - - await this.workspaceMetadataVersionService.incrementMetadataVersion( - workspaceId, - ); - - await this.flatEntityMapsCacheService.invalidateFlatEntityMaps({ - workspaceId, - flatMapsKeys: ['flatObjectMetadataMaps', 'flatFieldMetadataMaps'], - }); - - const formattedUpdatedObject = { - ...updatedObject, - createdAt: new Date(updatedObject.createdAt), - updatedAt: new Date(updatedObject.updatedAt), - }; - - return formattedUpdatedObject; - } catch (error) { - if (queryRunner.isTransactionActive) { - try { - await queryRunner.rollbackTransaction(); - } catch (error) { - // eslint-disable-next-line no-console - console.trace(`Failed to rollback transaction: ${error.message}`); - } - } - throw error; - } finally { - await queryRunner.release(); - } - } - - public async deleteOneObject( - deleteObjectInput: DeleteOneObjectInput, - workspaceId: string, - ): Promise> { - const isWorkspaceMigrationV2Enabled = - await this.featureFlagService.isFeatureEnabled( - FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED, - workspaceId, - ); - - if (isWorkspaceMigrationV2Enabled) { - return await this.objectMetadataServiceV2.deleteOne({ - deleteObjectInput, - workspaceId, - }); - } - - const queryRunner = this.coreDataSource.createQueryRunner(); - - await queryRunner.connect(); - await queryRunner.startTransaction(); - - try { - const objectMetadataRepository = - queryRunner.manager.getRepository(ObjectMetadataEntity); - const fieldMetadataRepository = - queryRunner.manager.getRepository(FieldMetadataEntity); - - const objectMetadata = await objectMetadataRepository.findOne({ - relations: [ - 'fields', - 'fields.object', - 'fields.relationTargetFieldMetadata', - 'fields.relationTargetFieldMetadata.object', - ], - where: { - id: deleteObjectInput.id, - workspaceId, - }, - }); - - if (!objectMetadata) { - throw new ObjectMetadataException( - 'Object does not exist', - ObjectMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND, - ); - } - - if (objectMetadata.isRemote) { - throw new ObjectMetadataException( - 'Remote objects are not supported yet', - ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT, - ); - } else { - await this.objectMetadataMigrationService.deleteAllRelationsAndDropTable( - objectMetadata, - workspaceId, - queryRunner, - ); - } - - await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrationsWithinTransaction( - workspaceId, - queryRunner, - ); - - const fieldMetadataIds = objectMetadata.fields.map((field) => field.id); - const relationMetadataIds = objectMetadata.fields.flatMap((field) => { - if ( - isFieldMetadataEntityOfType(field, FieldMetadataType.MORPH_RELATION) - ) { - return field.relationTargetFieldMetadata.id; - } - - return []; - }); - - await fieldMetadataRepository.delete({ - id: In(fieldMetadataIds.concat(relationMetadataIds)), - }); - - await objectMetadataRepository.delete(objectMetadata.id); - - await queryRunner.commitTransaction(); - - // After commit, do non-transactional work - await this.workspaceMetadataVersionService.incrementMetadataVersion( - workspaceId, - ); - - await this.flatEntityMapsCacheService.invalidateFlatEntityMaps({ - workspaceId, - flatMapsKeys: ['flatObjectMetadataMaps', 'flatFieldMetadataMaps'], - }); - - await this.workspacePermissionsCacheService.recomputeRolesPermissionsCache( - { - workspaceId, - }, - ); - - await this.objectMetadataRelatedRecordsService.deleteObjectViews( - objectMetadata, - workspaceId, - ); - - return objectMetadata; - } catch (error) { - if (queryRunner.isTransactionActive) { - try { - await queryRunner.rollbackTransaction(); - } catch (error) { - // eslint-disable-next-line no-console - console.trace(`Failed to rollback transaction: ${error.message}`); - } - } - throw error; - } finally { - await queryRunner.release(); - } - } - - public async findOneWithinWorkspace( - workspaceId: string, - options: FindOneOptions, - ): Promise { - return this.objectMetadataRepository.findOne({ - relations: [ - 'fields', - 'indexMetadatas', - 'indexMetadatas.indexFieldMetadatas', - ], - ...options, - where: { - ...options.where, - workspaceId, - }, - }); - } - - public async findManyWithinWorkspace( - workspaceId: string, - options?: FindManyOptions, - ) { - return this.objectMetadataRepository.find({ - relations: [ - 'fields.object', - 'fields', - 'fields.relationTargetObjectMetadata', - ], - ...options, - where: { - ...options?.where, - workspaceId, - }, - order: { - ...options?.order, - }, - }); - } - - public async deleteObjectsMetadata(workspaceId: string) { - const objectsMetadata = await this.objectMetadataRepository.find({ - where: { - workspaceId, - }, - }); - - await this.fieldMetadataRepository.delete({ - workspaceId, - type: In([FieldMetadataType.MORPH_RELATION, FieldMetadataType.RELATION]), - }); - - for (const objectMetadata of objectsMetadata) { - await this.objectMetadataRepository.delete({ - id: objectMetadata.id, - }); - } - } - - private async handleObjectNameAndLabelUpdates({ - existingObjectMetadata, - objectMetadataForUpdate, - inputPayload, - queryRunner, - objectMetadataMaps, - }: { - existingObjectMetadata: Pick< - ObjectMetadataItemWithFieldMaps, - 'nameSingular' | 'isCustom' | 'id' | 'labelPlural' | 'icon' | 'fieldsById' - >; - objectMetadataForUpdate: Pick< - ObjectMetadataItemWithFieldMaps, - | 'nameSingular' - | 'isCustom' - | 'workspaceId' - | 'id' - | 'labelSingular' - | 'labelPlural' - | 'icon' - | 'fieldsById' - >; - inputPayload: UpdateObjectPayload; - queryRunner: QueryRunner; - objectMetadataMaps: ObjectMetadataMaps; - }): Promise<{ didUpdateLabelOrIcon: boolean }> { - const newTargetTableName = computeObjectTargetTable( - objectMetadataForUpdate, - ); - const existingTargetTableName = computeObjectTargetTable( - existingObjectMetadata, - ); - - if (newTargetTableName !== existingTargetTableName) { - await this.objectMetadataMigrationService.createRenameTableMigration( - existingObjectMetadata, - objectMetadataForUpdate, - objectMetadataForUpdate.workspaceId, - queryRunner, - ); - - const relationMetadataCollection = - await this.objectMetadataFieldRelationService.updateRelationsAndForeignKeysMetadata( - { - workspaceId: objectMetadataForUpdate.workspaceId, - updatedObjectMetadata: objectMetadataForUpdate, - queryRunner, - objectMetadataMaps, - }, - ); - - await this.objectMetadataMigrationService.updateRelationMigrations( - existingObjectMetadata, - objectMetadataForUpdate, - relationMetadataCollection, - objectMetadataForUpdate.workspaceId, - queryRunner, - ); - - const morphRelationFieldMetadataToUpdate = - await this.objectMetadataFieldRelationService.updateMorphRelationsJoinColumnName( - { - existingObjectMetadata, - objectMetadataForUpdate, - queryRunner, - }, - ); - - await this.objectMetadataMigrationService.updateMorphRelationMigrations({ - workspaceId: objectMetadataForUpdate.workspaceId, - morphRelationFieldMetadataToUpdate: morphRelationFieldMetadataToUpdate, - queryRunner, - }); - - await this.objectMetadataMigrationService.recomputeEnumNames( - objectMetadataForUpdate, - objectMetadataForUpdate.workspaceId, - queryRunner, - ); - - const recomputedIndexes = - await this.indexMetadataService.recomputeIndexMetadataForObject( - objectMetadataForUpdate.workspaceId, - objectMetadataForUpdate, - queryRunner, - ); - - await this.indexMetadataService.createIndexRecomputeMigrations( - objectMetadataForUpdate.workspaceId, - objectMetadataForUpdate, - recomputedIndexes, - queryRunner, - ); - - if ( - (inputPayload.labelPlural || inputPayload.icon) && - (inputPayload.labelPlural !== existingObjectMetadata.labelPlural || - inputPayload.icon !== existingObjectMetadata.icon) - ) { - return { - didUpdateLabelOrIcon: true, - }; - } - } - - return { - didUpdateLabelOrIcon: false, - }; - } -} diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/services/object-metadata-field-relation.service.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/services/object-metadata-field-relation.service.ts deleted file mode 100644 index 04cc8a2acc..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/services/object-metadata-field-relation.service.ts +++ /dev/null @@ -1,617 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; - -import { msg } from '@lingui/core/macro'; -import { FieldMetadataType, RelationOnDeleteAction } from 'twenty-shared/types'; -import { capitalize, isDefined } from 'twenty-shared/utils'; -import { type QueryRunner, Repository } from 'typeorm'; -import { v4 as uuidV4 } from 'uuid'; - -import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; - -import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util'; -import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { - ObjectMetadataException, - ObjectMetadataExceptionCode, -} from 'src/engine/metadata-modules/object-metadata/object-metadata.exception'; -import { buildDescriptionForRelationFieldMetadataOnFromField } from 'src/engine/metadata-modules/object-metadata/utils/build-description-for-relation-field-on-from-field.util'; -import { buildDescriptionForRelationFieldMetadataOnToField } from 'src/engine/metadata-modules/object-metadata/utils/build-description-for-relation-field-on-to-field.util'; -import { type FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; -import { InvalidMetadataException } from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception'; -import { validateFieldNameAvailabilityOrThrow } from 'src/engine/metadata-modules/utils/validate-field-name-availability.utils'; -import { - CUSTOM_OBJECT_STANDARD_FIELD_IDS, - STANDARD_OBJECT_FIELD_IDS, -} from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids'; -import { STANDARD_OBJECT_ICONS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-icons'; -import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids'; -import { createRelationDeterministicUuid } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/create-deterministic-uuid.util'; - -export const DEFAULT_RELATIONS_OBJECTS_STANDARD_IDS = [ - STANDARD_OBJECT_IDS.timelineActivity, - STANDARD_OBJECT_IDS.favorite, - STANDARD_OBJECT_IDS.attachment, - STANDARD_OBJECT_IDS.noteTarget, - STANDARD_OBJECT_IDS.taskTarget, -]; - -type PartialRelationFieldMetadata = Partial< - FieldMetadataEntity -> & - Required, 'name'>>; - -@Injectable() -export class ObjectMetadataFieldRelationService { - constructor( - @InjectRepository(ObjectMetadataEntity) - private readonly objectMetadataRepository: Repository, - @InjectRepository(FieldMetadataEntity) - private readonly fieldMetadataRepository: Repository, - ) {} - - public async createRelationsAndForeignKeysMetadata( - workspaceId: string, - sourceObjectMetadata: Pick< - ObjectMetadataItemWithFieldMaps, - 'id' | 'nameSingular' | 'labelSingular' | 'fieldsById' - >, - objectMetadataMaps: ObjectMetadataMaps, - queryRunner?: QueryRunner, - ) { - const relatedObjectMetadataCollection = await Promise.all( - DEFAULT_RELATIONS_OBJECTS_STANDARD_IDS.map( - async (relationObjectMetadataStandardId) => - this.createRelationAndForeignKeyMetadata({ - workspaceId, - sourceObjectMetadata, - relationObjectMetadataStandardId, - objectMetadataMaps, - queryRunner, - }), - ), - ); - - return relatedObjectMetadataCollection; - } - - private async createRelationAndForeignKeyMetadata({ - workspaceId, - sourceObjectMetadata, - relationObjectMetadataStandardId, - objectMetadataMaps, - queryRunner, - }: { - workspaceId: string; - sourceObjectMetadata: Pick< - ObjectMetadataItemWithFieldMaps, - 'id' | 'nameSingular' | 'labelSingular' | 'fieldsById' - >; - - objectMetadataMaps: ObjectMetadataMaps; - relationObjectMetadataStandardId: string; - queryRunner?: QueryRunner; - }) { - const targetObjectMetadata = Object.values(objectMetadataMaps.byId) - .filter(isDefined) - .find( - (objectMetadata) => - objectMetadata.standardId === relationObjectMetadataStandardId, - ); - - if (!targetObjectMetadata) { - throw new Error( - `Target object metadata not found for standard ID: ${relationObjectMetadataStandardId}`, - ); - } - - await this.createFieldMetadataRelation( - workspaceId, - sourceObjectMetadata, - targetObjectMetadata, - queryRunner, - ); - - return targetObjectMetadata; - } - - private async createFieldMetadataRelation( - workspaceId: string, - sourceObjectMetadata: Pick< - ObjectMetadataItemWithFieldMaps, - 'id' | 'nameSingular' | 'labelSingular' | 'fieldsById' - >, - targetObjectMetadata: ObjectMetadataItemWithFieldMaps, - queryRunner?: QueryRunner, - ): Promise[]> { - const sourceFieldMetadata = this.createSourceFieldMetadata( - workspaceId, - sourceObjectMetadata, - targetObjectMetadata, - ); - - this.validateFieldNameAvailabilityOrThrow({ - name: sourceFieldMetadata.name, - fieldMetadataMapById: sourceObjectMetadata.fieldsById, - }); - - const targetFieldMetadata = this.createTargetFieldMetadata( - workspaceId, - sourceObjectMetadata, - targetObjectMetadata, - ); - - this.validateFieldNameAvailabilityOrThrow({ - name: targetFieldMetadata.name, - fieldMetadataMapById: sourceObjectMetadata.fieldsById, - }); - - const fieldMetadataRepository = queryRunner - ? queryRunner.manager.getRepository(FieldMetadataEntity) - : this.fieldMetadataRepository; - - return fieldMetadataRepository.save([ - { - ...sourceFieldMetadata, - settings: { - relationType: RelationType.ONE_TO_MANY, - }, - relationTargetObjectMetadataId: targetObjectMetadata.id, - relationTargetFieldMetadataId: targetFieldMetadata.id, - } as Partial>, - { - ...targetFieldMetadata, - settings: { - relationType: RelationType.MANY_TO_ONE, - onDelete: RelationOnDeleteAction.CASCADE, - joinColumnName: `${sourceObjectMetadata.nameSingular}Id`, - }, - relationTargetObjectMetadataId: sourceObjectMetadata.id, - relationTargetFieldMetadataId: sourceFieldMetadata.id, - } as Partial>, - ]); - } - - public async updateRelationsAndForeignKeysMetadata({ - workspaceId, - updatedObjectMetadata, - objectMetadataMaps, - queryRunner, - }: { - workspaceId: string; - updatedObjectMetadata: Pick< - ObjectMetadataEntity, - 'nameSingular' | 'isCustom' | 'id' | 'labelSingular' - >; - objectMetadataMaps: ObjectMetadataMaps; - queryRunner?: QueryRunner; - }): Promise< - { - targetObjectMetadata: ObjectMetadataItemWithFieldMaps; - targetFieldMetadata: FieldMetadataEntity; - sourceFieldMetadata: FieldMetadataEntity; - }[] - > { - return await Promise.all( - DEFAULT_RELATIONS_OBJECTS_STANDARD_IDS.map( - async (relationObjectMetadataStandardId) => - this.updateRelationAndForeignKeyMetadata({ - workspaceId, - sourceObjectMetadata: updatedObjectMetadata, - targetObjectMetadataStandardId: relationObjectMetadataStandardId, - objectMetadataMaps, - queryRunner, - }), - ), - ); - } - - private async updateRelationAndForeignKeyMetadata({ - workspaceId, - sourceObjectMetadata, - targetObjectMetadataStandardId, - objectMetadataMaps, - queryRunner, - }: { - workspaceId: string; - sourceObjectMetadata: Pick< - ObjectMetadataEntity, - 'nameSingular' | 'id' | 'isCustom' | 'labelSingular' - >; - targetObjectMetadataStandardId: string; - objectMetadataMaps: ObjectMetadataMaps; - queryRunner?: QueryRunner; - }): Promise<{ - targetObjectMetadata: ObjectMetadataItemWithFieldMaps; - targetFieldMetadata: FieldMetadataEntity; - sourceFieldMetadata: FieldMetadataEntity; - }> { - const fieldMetadataRepository = queryRunner - ? queryRunner.manager.getRepository(FieldMetadataEntity) - : this.fieldMetadataRepository; - - const targetObjectMetadataId = Object.values(objectMetadataMaps.byId).find( - (objectMetadata) => - objectMetadata?.standardId === targetObjectMetadataStandardId && - objectMetadata.isCustom === false, - )?.id; - - if (!targetObjectMetadataId) { - throw new Error( - `Target object metadata id not found for standard ID: ${targetObjectMetadataStandardId}`, - ); - } - - const targetObjectMetadata = - objectMetadataMaps.byId[targetObjectMetadataId]; - - if (!targetObjectMetadata) { - throw new Error( - `Target object metadata not found for id: ${targetObjectMetadataId}`, - ); - } - - const targetFieldMetadataUpdateData = this.updateTargetFieldMetadata( - sourceObjectMetadata, - targetObjectMetadata, - ); - const targetFieldMetadataToUpdate = - await fieldMetadataRepository.findOneByOrFail({ - standardId: createRelationDeterministicUuid({ - objectId: sourceObjectMetadata.id, - standardId: - // @ts-expect-error legacy noImplicitAny - STANDARD_OBJECT_FIELD_IDS[targetObjectMetadata.nameSingular].custom, - }), - objectMetadataId: targetObjectMetadata.id, - workspaceId: workspaceId, - }); - - const nameIsUpdated = - targetFieldMetadataUpdateData.name !== targetFieldMetadataToUpdate.name; - - if (nameIsUpdated) { - const targetObjectMetadataFieldsById = - objectMetadataMaps.byId[targetObjectMetadata.id]?.fieldsById; - - if (!targetObjectMetadataFieldsById) { - throw new Error( - `Target object metadata fields not found for ${targetObjectMetadata.id}`, - ); - } - - this.validateFieldNameAvailabilityOrThrow({ - name: targetFieldMetadataUpdateData.name, - fieldMetadataMapById: targetObjectMetadataFieldsById, - }); - } - - const isTargetFieldMetadataManyToOneRelation = - ( - targetFieldMetadataToUpdate as FieldMetadataEntity - ).settings?.relationType === RelationType.MANY_TO_ONE; - - const targetFieldMetadata = await fieldMetadataRepository.save({ - id: targetFieldMetadataToUpdate.id, - ...targetFieldMetadataUpdateData, - settings: { - ...targetFieldMetadataToUpdate.settings, - ...(isTargetFieldMetadataManyToOneRelation - ? { - joinColumnName: `${sourceObjectMetadata.nameSingular}Id`, - } - : {}), - }, - }); - - const sourceFieldMetadataUpdateData = this.updateSourceFieldMetadata( - sourceObjectMetadata, - targetObjectMetadata, - ); - - const sourceFieldMetadataToUpdate = - await fieldMetadataRepository.findOneByOrFail({ - standardId: - // @ts-expect-error legacy noImplicitAny - CUSTOM_OBJECT_STANDARD_FIELD_IDS[targetObjectMetadata.namePlural], - objectMetadataId: sourceObjectMetadata.id, - workspaceId: workspaceId, - }); - - const isSourceFieldMetadataManyToOneRelation = - ( - sourceFieldMetadataToUpdate as FieldMetadataEntity - ).settings?.relationType === RelationType.MANY_TO_ONE; - - const sourceFieldMetadata = await fieldMetadataRepository.save({ - id: sourceFieldMetadataToUpdate.id, - ...sourceFieldMetadataUpdateData, - settings: { - ...sourceFieldMetadataToUpdate.settings, - ...(isSourceFieldMetadataManyToOneRelation - ? { - joinColumnName: `${targetObjectMetadata.nameSingular}Id`, - } - : {}), - }, - }); - - return { - targetObjectMetadata, - targetFieldMetadata, - sourceFieldMetadata, - }; - } - - private createSourceFieldMetadata( - workspaceId: string, - sourceObjectMetadata: Pick< - ObjectMetadataItemWithFieldMaps, - 'labelSingular' | 'id' - >, - targetObjectMetadata: Pick< - ObjectMetadataItemWithFieldMaps, - 'namePlural' | 'labelSingular' - >, - ): PartialRelationFieldMetadata { - const relationObjectMetadataNamePlural = targetObjectMetadata.namePlural; - - const { description } = buildDescriptionForRelationFieldMetadataOnFromField( - { - relationObjectMetadataNamePlural, - targetObjectLabelSingular: sourceObjectMetadata.labelSingular, - }, - ); - - return { - id: uuidV4(), - standardId: - // @ts-expect-error legacy noImplicitAny - CUSTOM_OBJECT_STANDARD_FIELD_IDS[relationObjectMetadataNamePlural], - objectMetadataId: sourceObjectMetadata.id, - workspaceId: workspaceId, - isCustom: false, - isActive: true, - isSystem: true, - type: FieldMetadataType.RELATION, - name: targetObjectMetadata.namePlural, - label: capitalize(relationObjectMetadataNamePlural), - description, - icon: - // @ts-expect-error legacy noImplicitAny - STANDARD_OBJECT_ICONS[targetObjectMetadata.nameSingular] || - 'IconBuildingSkyscraper', - isNullable: true, - }; - } - - private updateSourceFieldMetadata( - sourceObjectMetadata: Pick, - targetObjectMetadata: Pick, - ) { - const relationObjectMetadataNamePlural = targetObjectMetadata.namePlural; - - const { description } = buildDescriptionForRelationFieldMetadataOnFromField( - { - relationObjectMetadataNamePlural, - targetObjectLabelSingular: sourceObjectMetadata.labelSingular, - }, - ); - - return { - description, - }; - } - - private createTargetFieldMetadata( - workspaceId: string, - sourceObjectMetadata: Pick< - ObjectMetadataItemWithFieldMaps, - 'labelSingular' | 'id' | 'nameSingular' - >, - targetObjectMetadata: Pick< - ObjectMetadataItemWithFieldMaps, - 'namePlural' | 'labelSingular' | 'id' | 'nameSingular' - >, - ): PartialRelationFieldMetadata { - const customStandardFieldId = - // @ts-expect-error legacy noImplicitAny - STANDARD_OBJECT_FIELD_IDS[targetObjectMetadata.nameSingular].custom; - - if (!customStandardFieldId) { - throw new Error( - `Custom standard field ID not found for ${targetObjectMetadata.nameSingular}`, - ); - } - - const { description } = buildDescriptionForRelationFieldMetadataOnToField({ - relationObjectMetadataNamePlural: targetObjectMetadata.namePlural, - targetObjectLabelSingular: sourceObjectMetadata.labelSingular, - }); - - return { - id: uuidV4(), - name: sourceObjectMetadata.nameSingular, - label: sourceObjectMetadata.labelSingular, - description, - standardId: createRelationDeterministicUuid({ - objectId: sourceObjectMetadata.id, - standardId: customStandardFieldId, - }), - objectMetadataId: targetObjectMetadata.id, - workspaceId: workspaceId, - isCustom: false, - isActive: true, - isSystem: true, - type: FieldMetadataType.RELATION, - icon: 'IconBuildingSkyscraper', - isNullable: true, - }; - } - - private updateTargetFieldMetadata( - sourceObjectMetadata: Pick< - ObjectMetadataEntity, - 'nameSingular' | 'labelSingular' - >, - targetObjectMetadata: Pick< - ObjectMetadataEntity, - 'nameSingular' | 'namePlural' - >, - ) { - const customStandardFieldId = - // @ts-expect-error legacy noImplicitAny - STANDARD_OBJECT_FIELD_IDS[targetObjectMetadata.nameSingular].custom; - - if (!customStandardFieldId) { - throw new Error( - `Custom standard field ID not found for ${targetObjectMetadata.nameSingular}`, - ); - } - - const { description } = buildDescriptionForRelationFieldMetadataOnToField({ - relationObjectMetadataNamePlural: targetObjectMetadata.namePlural, - targetObjectLabelSingular: sourceObjectMetadata.labelSingular, - }); - - return { - name: sourceObjectMetadata.nameSingular, - label: sourceObjectMetadata.labelSingular, - description, - }; - } - - private validateFieldMetadataTypeIsMorphRelation = ( - fieldMetadatas: FieldMetadataEntity[], - ): fieldMetadatas is Array< - FieldMetadataEntity & FieldMetadataEntity - > => { - return fieldMetadatas.every( - (fieldMetadata) => - fieldMetadata.type === FieldMetadataType.MORPH_RELATION, - ); - }; - - public async findTargetMorphRelationFieldMetadatas( - objectMetadataId: string, - ): Promise[]> { - const fieldMetadatas = await this.fieldMetadataRepository.find({ - where: { - relationTargetObjectMetadataId: objectMetadataId, - type: FieldMetadataType.MORPH_RELATION, - }, - relations: { - relationTargetObjectMetadata: true, - object: true, - }, - }); - - if (!this.validateFieldMetadataTypeIsMorphRelation(fieldMetadatas)) { - throw new ObjectMetadataException( - 'Invalid field metadata type. Expected MORPH_RELATION only', - ObjectMetadataExceptionCode.INVALID_ORM_OUTPUT, - ); - } - - return fieldMetadatas; - } - - // Not maintained on v1, this side effect is broken and will duplicated field name references on - // object update - // It's functional on v2 - public async updateMorphRelationsJoinColumnName({ - existingObjectMetadata, - objectMetadataForUpdate: _, - queryRunner, - }: { - existingObjectMetadata: Pick< - ObjectMetadataItemWithFieldMaps, - 'nameSingular' | 'isCustom' | 'id' | 'labelPlural' | 'icon' | 'fieldsById' - >; - objectMetadataForUpdate: Pick< - ObjectMetadataItemWithFieldMaps, - | 'nameSingular' - | 'isCustom' - | 'workspaceId' - | 'id' - | 'labelSingular' - | 'labelPlural' - | 'icon' - | 'fieldsById' - >; - queryRunner: QueryRunner; - }): Promise< - { - fieldMetadata: FieldMetadataEntity; - newJoinColumnName: string; - }[] - > { - const fieldMetadataRepository = - queryRunner.manager.getRepository(FieldMetadataEntity); - - const morphRelationFieldMetadataTargets = - await this.findTargetMorphRelationFieldMetadatas( - existingObjectMetadata.id, - ); - const morphRelationFieldMetadataToUpdate = - morphRelationFieldMetadataTargets.filter( - (morphRelationFieldMetadata) => - morphRelationFieldMetadata.settings?.relationType === - RelationType.MANY_TO_ONE, - ); - - const morphRelationFieldMetadataToUpdateWithNewJoinColumnName = []; - - if (morphRelationFieldMetadataToUpdate.length > 0) { - for (const morphRelationFieldMetadata of morphRelationFieldMetadataToUpdate) { - const newJoinColumnName = computeMorphOrRelationFieldJoinColumnName({ - name: morphRelationFieldMetadata.name, - }); - - await fieldMetadataRepository.save({ - ...morphRelationFieldMetadata, - settings: { - ...morphRelationFieldMetadata.settings, - joinColumnName: newJoinColumnName, - }, - }); - - morphRelationFieldMetadataToUpdateWithNewJoinColumnName.push({ - fieldMetadata: morphRelationFieldMetadata, - newJoinColumnName, - }); - } - } - - return morphRelationFieldMetadataToUpdateWithNewJoinColumnName; - } - - private validateFieldNameAvailabilityOrThrow({ - name, - fieldMetadataMapById, - }: { - name: string; - fieldMetadataMapById: FieldMetadataMap; - }) { - try { - validateFieldNameAvailabilityOrThrow({ - name, - fieldMetadataMapById, - }); - } catch (error) { - if (error instanceof InvalidMetadataException) { - throw new ObjectMetadataException( - `Name "${name}" is not available.`, - ObjectMetadataExceptionCode.NAME_CONFLICT, - { - userFriendlyMessage: msg`Name "${name}" is not available.`, - }, - ); - } - throw error; - } - } -} diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/services/object-metadata-migration.service.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/services/object-metadata-migration.service.ts deleted file mode 100644 index 0f98b416d3..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/services/object-metadata-migration.service.ts +++ /dev/null @@ -1,424 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; - -import { FieldMetadataType } from 'twenty-shared/types'; -import { type QueryRunner, Repository } from 'typeorm'; - -import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; - -import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { - ObjectMetadataException, - ObjectMetadataExceptionCode, -} from 'src/engine/metadata-modules/object-metadata/object-metadata.exception'; -import { buildMigrationsForCustomObjectRelations } from 'src/engine/metadata-modules/object-metadata/utils/build-migrations-for-custom-object-relations.util'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { fieldMetadataTypeToColumnType } from 'src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util'; -import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util'; -import { - WorkspaceMigrationColumnActionType, - type WorkspaceMigrationColumnDrop, - type WorkspaceMigrationTableAction, - WorkspaceMigrationTableActionType, -} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; -import { WorkspaceMigrationFactory } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.factory'; -import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.service'; -import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; -import { computeTableName } from 'src/engine/utils/compute-table-name.util'; -import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; -import { RELATION_MIGRATION_PRIORITY_PREFIX } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service'; - -@Injectable() -export class ObjectMetadataMigrationService { - constructor( - @InjectRepository(FieldMetadataEntity) - private readonly fieldMetadataRepository: Repository, - private readonly workspaceMigrationService: WorkspaceMigrationService, - private readonly workspaceMigrationFactory: WorkspaceMigrationFactory, - ) {} - - public async createTableMigration( - createdObjectMetadata: ObjectMetadataEntity, - queryRunner?: QueryRunner, - ) { - await this.workspaceMigrationService.createCustomMigration( - generateMigrationName(`create-${createdObjectMetadata.nameSingular}`), - createdObjectMetadata.workspaceId, - [ - { - name: computeObjectTargetTable(createdObjectMetadata), - action: WorkspaceMigrationTableActionType.CREATE, - } satisfies WorkspaceMigrationTableAction, - ], - queryRunner, - ); - } - - public async createColumnsMigrations( - createdObjectMetadata: ObjectMetadataEntity, - fieldMetadataCollection: FieldMetadataEntity[], - queryRunner?: QueryRunner, - ) { - await this.workspaceMigrationService.createCustomMigration( - generateMigrationName( - `create-${createdObjectMetadata.nameSingular}-fields`, - ), - createdObjectMetadata.workspaceId, - [ - { - name: computeObjectTargetTable(createdObjectMetadata), - action: WorkspaceMigrationTableActionType.ALTER, - columns: fieldMetadataCollection.flatMap((fieldMetadata) => - this.workspaceMigrationFactory.createColumnActions( - WorkspaceMigrationColumnActionType.CREATE, - fieldMetadata, - ), - ), - }, - ], - queryRunner, - ); - } - - public async createRelationMigrations( - createdObjectMetadata: Pick< - ObjectMetadataItemWithFieldMaps, - 'nameSingular' | 'workspaceId' | 'isCustom' - >, - relatedObjectMetadataCollection: Pick< - ObjectMetadataItemWithFieldMaps, - 'nameSingular' | 'isCustom' - >[], - queryRunner?: QueryRunner, - ) { - await this.workspaceMigrationService.createCustomMigration( - generateMigrationName( - `create-${createdObjectMetadata.nameSingular}-relations`, - ), - createdObjectMetadata.workspaceId, - buildMigrationsForCustomObjectRelations( - createdObjectMetadata, - relatedObjectMetadataCollection, - ), - queryRunner, - ); - } - - public async createRenameTableMigration( - existingObjectMetadata: Pick< - ObjectMetadataEntity, - 'nameSingular' | 'isCustom' - >, - objectMetadataForUpdate: Pick< - ObjectMetadataEntity, - 'nameSingular' | 'isCustom' - >, - workspaceId: string, - queryRunner?: QueryRunner, - ) { - const newTargetTableName = computeObjectTargetTable( - objectMetadataForUpdate, - ); - const existingTargetTableName = computeObjectTargetTable( - existingObjectMetadata, - ); - - await this.workspaceMigrationService.createCustomMigration( - generateMigrationName(`rename-${existingObjectMetadata.nameSingular}`), - workspaceId, - [ - { - name: existingTargetTableName, - newName: newTargetTableName, - action: WorkspaceMigrationTableActionType.ALTER, - }, - ], - queryRunner, - ); - } - - public async updateRelationMigrations( - currentObjectMetadata: Pick, - alteredObjectMetadata: Pick, - relationMetadataCollection: { - targetObjectMetadata: ObjectMetadataItemWithFieldMaps; - targetFieldMetadata: FieldMetadataEntity; - sourceFieldMetadata: FieldMetadataEntity; - }[], - workspaceId: string, - queryRunner?: QueryRunner, - ) { - for (const { targetObjectMetadata } of relationMetadataCollection) { - const targetTableName = computeObjectTargetTable(targetObjectMetadata); - const columnName = `${currentObjectMetadata.nameSingular}Id`; - - await this.workspaceMigrationService.createCustomMigration( - generateMigrationName( - `rename-${currentObjectMetadata.nameSingular}-to-${alteredObjectMetadata.nameSingular}-in-${targetObjectMetadata.nameSingular}`, - ), - workspaceId, - [ - { - name: targetTableName, - action: WorkspaceMigrationTableActionType.ALTER, - columns: [ - { - action: WorkspaceMigrationColumnActionType.ALTER, - currentColumnDefinition: { - columnName, - columnType: 'uuid', - isNullable: true, - defaultValue: null, - }, - alteredColumnDefinition: { - columnName: `${alteredObjectMetadata.nameSingular}Id`, - columnType: 'uuid', - isNullable: true, - defaultValue: null, - }, - }, - ], - }, - ], - queryRunner, - ); - } - } - - public async createUpdateForeignKeysMigrations( - existingObjectMetadata: ObjectMetadataEntity, - updatedObjectMetadata: ObjectMetadataEntity, - relationsAndForeignKeysMetadata: { - relatedObjectMetadata: ObjectMetadataEntity; - foreignKeyFieldMetadata: FieldMetadataEntity; - }[], - workspaceId: string, - queryRunner?: QueryRunner, - ) { - for (const { - relatedObjectMetadata, - foreignKeyFieldMetadata, - } of relationsAndForeignKeysMetadata) { - const relatedObjectTableName = computeObjectTargetTable( - relatedObjectMetadata, - ); - const columnName = `${existingObjectMetadata.nameSingular}Id`; - const columnType = fieldMetadataTypeToColumnType( - foreignKeyFieldMetadata.type, - ); - - await this.workspaceMigrationService.createCustomMigration( - generateMigrationName( - `rename-${existingObjectMetadata.nameSingular}-to-${updatedObjectMetadata.nameSingular}-in-${relatedObjectMetadata.nameSingular}`, - ), - workspaceId, - [ - { - name: relatedObjectTableName, - action: WorkspaceMigrationTableActionType.ALTER, - columns: [ - { - action: WorkspaceMigrationColumnActionType.ALTER, - currentColumnDefinition: { - columnName, - columnType, - isNullable: true, - defaultValue: null, - }, - alteredColumnDefinition: { - columnName: `${updatedObjectMetadata.nameSingular}Id`, - columnType, - isNullable: true, - defaultValue: null, - }, - }, - ], - }, - ], - queryRunner, - ); - } - } - - public async deleteAllRelationsAndDropTable( - objectMetadata: ObjectMetadataEntity, - workspaceId: string, - queryRunner?: QueryRunner, - ) { - const relationFields = objectMetadata.fields.filter( - (field) => - isFieldMetadataEntityOfType(field, FieldMetadataType.RELATION) || - isFieldMetadataEntityOfType(field, FieldMetadataType.MORPH_RELATION), - ) as FieldMetadataEntity< - FieldMetadataType.RELATION | FieldMetadataType.MORPH_RELATION - >[]; - - const relationFieldsToDelete = [ - ...relationFields, - ...(relationFields.map( - (relation) => relation.relationTargetFieldMetadata, - ) as FieldMetadataEntity< - FieldMetadataType.RELATION | FieldMetadataType.MORPH_RELATION - >[]), - ]; - - if (relationFieldsToDelete.length !== 0) { - await this.fieldMetadataRepository.delete( - relationFieldsToDelete.map((relation) => relation.id), - ); - } - - for (const relationToDelete of relationFieldsToDelete) { - if ( - relationToDelete.settings?.relationType === RelationType.ONE_TO_MANY - ) { - continue; - } - - const joinColumnName = relationToDelete.settings?.joinColumnName; - - if (!joinColumnName) { - throw new Error( - `Join column name is not set for relation field ${relationToDelete.name}`, - ); - } - - if (relationToDelete.type !== FieldMetadataType.MORPH_RELATION) { - await this.workspaceMigrationService.createCustomMigration( - generateMigrationName( - `delete-${RELATION_MIGRATION_PRIORITY_PREFIX}-${relationToDelete.name}`, - ), - workspaceId, - [ - { - name: computeTableName( - relationToDelete.object.nameSingular, - relationToDelete.object.isCustom, - ), - action: WorkspaceMigrationTableActionType.ALTER, - columns: [ - { - action: WorkspaceMigrationColumnActionType.DROP, - columnName: joinColumnName, - } satisfies WorkspaceMigrationColumnDrop, - ], - }, - ], - queryRunner, - ); - } - } - - await this.workspaceMigrationService.createCustomMigration( - generateMigrationName(`delete-${objectMetadata.nameSingular}`), - workspaceId, - [ - { - name: computeObjectTargetTable(objectMetadata), - action: WorkspaceMigrationTableActionType.DROP, - }, - ], - queryRunner, - ); - } - - public async recomputeEnumNames( - updatedObjectMetadata: Pick< - ObjectMetadataItemWithFieldMaps, - 'nameSingular' | 'isCustom' | 'id' | 'fieldsById' - >, - workspaceId: string, - queryRunner?: QueryRunner, - ) { - const enumFieldMetadataTypes = [ - FieldMetadataType.SELECT, - FieldMetadataType.MULTI_SELECT, - FieldMetadataType.RATING, - FieldMetadataType.ACTOR, - ]; - - const fieldMetadataToUpdate = Object.values( - updatedObjectMetadata.fieldsById, - ).filter((field) => enumFieldMetadataTypes.includes(field.type)); - - for (const fieldMetadata of fieldMetadataToUpdate) { - await this.workspaceMigrationService.createCustomMigration( - generateMigrationName(`update-${fieldMetadata.name}-enum-name`), - workspaceId, - [ - { - name: computeTableName( - updatedObjectMetadata.nameSingular, - updatedObjectMetadata.isCustom, - ), - action: WorkspaceMigrationTableActionType.ALTER, - columns: this.workspaceMigrationFactory.createColumnActions( - WorkspaceMigrationColumnActionType.ALTER, - fieldMetadata, - fieldMetadata, - ), - }, - ], - queryRunner, - ); - } - } - - public async updateMorphRelationMigrations({ - workspaceId, - morphRelationFieldMetadataToUpdate, - queryRunner, - }: { - workspaceId: string; - morphRelationFieldMetadataToUpdate: { - fieldMetadata: FieldMetadataEntity; - newJoinColumnName: string; - }[]; - queryRunner?: QueryRunner; - }) { - for (const morphRelationFieldMetadata of morphRelationFieldMetadataToUpdate) { - if (!morphRelationFieldMetadata.fieldMetadata.settings?.joinColumnName) { - throw new ObjectMetadataException( - `Settings for morph relation field should be defined ${morphRelationFieldMetadata.fieldMetadata.name}`, - ObjectMetadataExceptionCode.INVALID_ORM_OUTPUT, - ); - } - - await this.workspaceMigrationService.createCustomMigration( - generateMigrationName( - `rename-join-column-name-${morphRelationFieldMetadata.fieldMetadata.name}-to-${morphRelationFieldMetadata.newJoinColumnName}-in-${morphRelationFieldMetadata.fieldMetadata.object.nameSingular}`, - ), - workspaceId, - [ - { - name: computeObjectTargetTable( - morphRelationFieldMetadata.fieldMetadata.object, - ), - action: WorkspaceMigrationTableActionType.ALTER, - columns: [ - { - action: WorkspaceMigrationColumnActionType.ALTER, - currentColumnDefinition: { - columnName: - morphRelationFieldMetadata.fieldMetadata.settings - ?.joinColumnName, - columnType: 'uuid', - isNullable: true, - defaultValue: null, - }, - alteredColumnDefinition: { - columnName: `${morphRelationFieldMetadata.newJoinColumnName}`, - columnType: 'uuid', - isNullable: true, - defaultValue: null, - }, - }, - ], - }, - ], - queryRunner, - ); - } - } -} diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/services/object-metadata-related-records.service.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/services/object-metadata-related-records.service.ts deleted file mode 100644 index 1e203ec5d9..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/services/object-metadata-related-records.service.ts +++ /dev/null @@ -1,176 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; - -import { isDefined } from 'twenty-shared/utils'; -import { Repository } from 'typeorm'; - -import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity'; -import { ViewFieldService } from 'src/engine/metadata-modules/view-field/services/view-field.service'; -import { ViewDTO } from 'src/engine/metadata-modules/view/dtos/view.dto'; -import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity'; -import { ViewKey } from 'src/engine/metadata-modules/view/enums/view-key.enum'; -import { ViewType } from 'src/engine/metadata-modules/view/enums/view-type.enum'; -import { ViewService } from 'src/engine/metadata-modules/view/services/view.service'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; -import { DEFAULT_VIEW_FIELD_SIZE } from 'src/engine/workspace-manager/standard-objects-prefill-data/views/constants/DEFAULT_VIEW_FIELD_SIZE'; -import { type FavoriteWorkspaceEntity } from 'src/modules/favorite/standard-objects/favorite.workspace-entity'; - -@Injectable() -export class ObjectMetadataRelatedRecordsService { - constructor( - private readonly viewService: ViewService, - private readonly viewFieldService: ViewFieldService, - @InjectRepository(ViewEntity) - private readonly viewRepository: Repository, - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, - ) {} - - public async createObjectRelatedRecords( - objectMetadata: ObjectMetadataEntity, - ) { - const view = await this.createView(objectMetadata); - - await this.createViewFields(objectMetadata, view.id); - await this.createViewWorkspaceFavorite(objectMetadata.workspaceId, view.id); - await this.viewService.flushGraphQLCache(objectMetadata.workspaceId); - } - - public async updateLabelMetadataIdentifierInObjectViews({ - newLabelMetadataIdentifierFieldMetadata, - }: { - newLabelMetadataIdentifierFieldMetadata: FieldMetadataEntity; - }) { - const objectMetadataViews = await this.viewService.findByObjectMetadataId( - newLabelMetadataIdentifierFieldMetadata.workspaceId, - newLabelMetadataIdentifierFieldMetadata.objectMetadataId, - ); - - for (const view of objectMetadataViews) { - let labelMetadataIdentifierViewField = view.viewFields.find( - (viewField) => - viewField.fieldMetadataId === - newLabelMetadataIdentifierFieldMetadata.id, - ); - - const currentMinPositionAmongViewFields = this.getViewFieldsMinPosition( - view.viewFields, - ); - - if (!labelMetadataIdentifierViewField) { - await this.viewFieldService.create({ - fieldMetadataId: newLabelMetadataIdentifierFieldMetadata.id, - position: currentMinPositionAmongViewFields - 1, - isVisible: true, - size: DEFAULT_VIEW_FIELD_SIZE, - viewId: view.id, - workspaceId: newLabelMetadataIdentifierFieldMetadata.workspaceId, - }); - continue; - } - - await this.viewFieldService.update( - labelMetadataIdentifierViewField.id, - newLabelMetadataIdentifierFieldMetadata.workspaceId, - { - position: currentMinPositionAmongViewFields - 1, - isVisible: true, - }, - ); - } - } - - private getViewFieldsMinPosition(viewFields: ViewFieldEntity[]): number { - if (viewFields.length === 0) { - return 0; - } - - return viewFields.reduce((min, field) => Math.min(min, field.position), 0); - } - - private async createView( - objectMetadata: ObjectMetadataEntity, - ): Promise { - return await this.viewService.create({ - objectMetadataId: objectMetadata.id, - type: ViewType.TABLE, - name: `All ${objectMetadata.labelPlural}`, - key: ViewKey.INDEX, - icon: 'IconList', - workspaceId: objectMetadata.workspaceId, - }); - } - - private async createViewFields( - objectMetadata: ObjectMetadataEntity, - viewId: string, - ): Promise { - const viewFields = objectMetadata.fields - .filter((field) => field.name !== 'id' && field.name !== 'deletedAt') - .map((field, index) => ({ - fieldMetadataId: field.id, - position: index, - isVisible: true, - size: DEFAULT_VIEW_FIELD_SIZE, - viewId: viewId, - workspaceId: objectMetadata.workspaceId, - })); - - for (const viewField of viewFields) { - await this.viewFieldService.create(viewField); - } - } - - public async createViewWorkspaceFavorite( - workspaceId: string, - viewId: string, - ): Promise { - const favoriteRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'favorite', - ); - - const favoriteCount = await favoriteRepository.count(); - - await favoriteRepository.insert( - favoriteRepository.create({ - viewId: viewId, - position: favoriteCount, - }), - ); - } - - public async updateObjectViews( - updatedObjectMetadata: Pick< - ObjectMetadataEntity, - 'id' | 'labelPlural' | 'icon' - >, - workspaceId: string, - ) { - await this.viewRepository.update( - { - objectMetadataId: updatedObjectMetadata.id, - key: ViewKey.INDEX, - workspaceId, - }, - { - name: `All ${updatedObjectMetadata.labelPlural}`, - ...(isDefined(updatedObjectMetadata.icon) - ? { icon: updatedObjectMetadata.icon } - : {}), - }, - ); - } - - public async deleteObjectViews( - objectMetadata: ObjectMetadataEntity, - workspaceId: string, - ) { - await this.viewRepository.delete({ - objectMetadataId: objectMetadata.id, - workspaceId, - }); - } -} diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-table.service.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-table.service.ts index c16384c555..b077b30718 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-table.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-table.service.ts @@ -6,11 +6,9 @@ import { plural } from 'pluralize'; import { DataSource, Repository } from 'typeorm'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; -import { type CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input'; -import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata.service'; -import { type CreateObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/create-object.input'; -import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service'; +import { FieldMetadataServiceV2 } from 'src/engine/metadata-modules/field-metadata/services/field-metadata.service-v2'; +import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { ObjectMetadataServiceV2 } from 'src/engine/metadata-modules/object-metadata/object-metadata-v2.service'; import { RemoteServerEntity, type RemoteServerType, @@ -57,8 +55,8 @@ export class RemoteTableService { >, private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService, private readonly dataSourceService: DataSourceService, - private readonly objectMetadataService: ObjectMetadataService, - private readonly fieldMetadataService: FieldMetadataService, + private readonly objectMetadataService: ObjectMetadataServiceV2, + private readonly fieldMetadataService: FieldMetadataServiceV2, private readonly distantTableService: DistantTableService, private readonly foreignTableService: ForeignTableService, private readonly workspaceDataSourceService: WorkspaceDataSourceService, @@ -421,10 +419,10 @@ export class RemoteTableService { }); if (objectMetadata) { - await this.objectMetadataService.deleteOneObject( - { id: objectMetadata.id }, + await this.objectMetadataService.deleteOneObject({ + deleteObjectInput: { id: objectMetadata.id }, workspaceId, - ); + }); } await this.foreignTableService.deleteForeignTable( @@ -455,21 +453,23 @@ export class RemoteTableService { ? `${plural(localTableBaseName)}${localTableSuffix}` : plural(localTableBaseName); - const objectMetadata = await this.objectMetadataService.createOne({ - nameSingular: camelCase(localTableNameSingular), - namePlural: camelCase(localTableNamePlural), - labelSingular: camelToTitleCase(camelCase(localTableBaseName)), - labelPlural: camelToTitleCase(plural(camelCase(localTableBaseName))), - description: 'Remote table', - dataSourceId: dataSourceMetadataId, + const objectMetadata = await this.objectMetadataService.createOneObject({ + createObjectInput: { + nameSingular: camelCase(localTableNameSingular), + namePlural: camelCase(localTableNamePlural), + labelSingular: camelToTitleCase(camelCase(localTableBaseName)), + labelPlural: camelToTitleCase(plural(camelCase(localTableBaseName))), + description: 'Remote table', + dataSourceId: dataSourceMetadataId, + icon: 'IconPlug', + isRemote: true, + primaryKeyColumnType: distantTableIdColumn.udtName, + primaryKeyFieldMetadataSettings: mapUdtNameToFieldSettings( + distantTableIdColumn.udtName, + ), + }, workspaceId: workspaceId, - icon: 'IconPlug', - isRemote: true, - primaryKeyColumnType: distantTableIdColumn.udtName, - primaryKeyFieldMetadataSettings: mapUdtNameToFieldSettings( - distantTableIdColumn.udtName, - ), - } satisfies CreateObjectInput); + }); for (const column of distantTableColumns) { const columnName = camelCase(column.columnName); @@ -484,8 +484,14 @@ export class RemoteTableService { ); if (columnName === 'id') { - await this.objectMetadataService.updateOne(objectMetadata.id, { - labelIdentifierFieldMetadataId: field.id, + await this.objectMetadataService.updateOneObject({ + workspaceId, + updateObjectInput: { + id: objectMetadata.id, + update: { + labelIdentifierFieldMetadataId: field.id, + }, + }, }); } } catch (error) { @@ -589,7 +595,10 @@ export class RemoteTableService { ); } - await this.fieldMetadataService.deleteOne(fieldMetadataToDelete.id); + await this.fieldMetadataService.deleteOneField({ + deleteOneFieldInput: { id: fieldMetadataToDelete.id }, + workspaceId, + }); } } @@ -598,18 +607,20 @@ export class RemoteTableService { columnName: string, columnType: string, objectMetadataId: string, - ): Promise { - return this.fieldMetadataService.createOne({ - name: columnName, - label: camelToTitleCase(columnName), - description: 'Field of remote', - type: mapUdtNameToFieldType(columnType), + ): Promise { + return this.fieldMetadataService.createOneField({ + createFieldInput: { + name: columnName, + label: camelToTitleCase(columnName), + description: 'Field of remote', + type: mapUdtNameToFieldType(columnType), + objectMetadataId: objectMetadataId, + isRemoteCreation: true, + isNullable: true, + icon: 'IconPlug', + settings: mapUdtNameToFieldSettings(columnType), + }, workspaceId: workspaceId, - objectMetadataId: objectMetadataId, - isRemoteCreation: true, - isNullable: true, - icon: 'IconPlug', - settings: mapUdtNameToFieldSettings(columnType), - } satisfies CreateFieldInput); + }); } } diff --git a/packages/twenty-server/src/engine/metadata-modules/search-vector/search-vector.module.ts b/packages/twenty-server/src/engine/metadata-modules/search-vector/search-vector.module.ts deleted file mode 100644 index d37384c55c..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/search-vector/search-vector.module.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Module } from '@nestjs/common'; - -import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm'; - -import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { IndexMetadataModule } from 'src/engine/metadata-modules/index-metadata/index-metadata.module'; -import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { SearchVectorService } from 'src/engine/metadata-modules/search-vector/search-vector.service'; -import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module'; - -@Module({ - imports: [ - NestjsQueryTypeOrmModule.forFeature([ - ObjectMetadataEntity, - FieldMetadataEntity, - ]), - IndexMetadataModule, - WorkspaceMigrationModule, - ], - providers: [SearchVectorService], - exports: [SearchVectorService], -}) -export class SearchVectorModule {} diff --git a/packages/twenty-server/src/engine/metadata-modules/search-vector/search-vector.service.ts b/packages/twenty-server/src/engine/metadata-modules/search-vector/search-vector.service.ts deleted file mode 100644 index 4b55fbae79..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/search-vector/search-vector.service.ts +++ /dev/null @@ -1,191 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; - -import { FieldMetadataType } from 'twenty-shared/types'; -import { isDefined } from 'twenty-shared/utils'; -import { type QueryRunner, Repository } from 'typeorm'; - -import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/constants/search-vector-field.constants'; -import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { IndexMetadataService } from 'src/engine/metadata-modules/index-metadata/index-metadata.service'; -import { IndexType } from 'src/engine/metadata-modules/index-metadata/types/indexType.types'; -import { type CreateObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/create-object.input'; -import { DEFAULT_LABEL_IDENTIFIER_FIELD_NAME } from 'src/engine/metadata-modules/object-metadata/object-metadata.constants'; -import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { - TsVectorColumnActionFactory, - type TsVectorFieldMetadata, -} from 'src/engine/metadata-modules/workspace-migration/factories/ts-vector-column-action.factory'; -import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util'; -import { - WorkspaceMigrationColumnActionType, - WorkspaceMigrationTableActionType, -} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; -import { WorkspaceMigrationFactory } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.factory'; -import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.service'; -import { computeTableName } from 'src/engine/utils/compute-table-name.util'; -import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; -import { CUSTOM_OBJECT_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids'; -import { - type FieldTypeAndNameMetadata, - getTsVectorColumnExpressionFromFields, -} from 'src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util'; -import { type SearchableFieldType } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/is-searchable-field.util'; - -@Injectable() -export class SearchVectorService { - constructor( - @InjectRepository(ObjectMetadataEntity) - private readonly objectMetadataRepository: Repository, - private readonly tsVectorColumnActionFactory: TsVectorColumnActionFactory, - private readonly indexMetadataService: IndexMetadataService, - @InjectRepository(FieldMetadataEntity) - private readonly fieldMetadataRepository: Repository, - private readonly workspaceMigrationService: WorkspaceMigrationService, - private readonly workspaceMigrationFactory: WorkspaceMigrationFactory, - ) {} - - public async createSearchVectorFieldForObject( - objectMetadataInput: CreateObjectInput, - createdObjectMetadata: ObjectMetadataEntity, - queryRunner?: QueryRunner, - ) { - const repository = queryRunner - ? queryRunner.manager.getRepository(FieldMetadataEntity) - : this.fieldMetadataRepository; - - const searchVectorFieldMetadata = await repository.save({ - standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.searchVector, - objectMetadataId: createdObjectMetadata.id, - workspaceId: objectMetadataInput.workspaceId, - isCustom: false, - isActive: false, - isSystem: true, - type: FieldMetadataType.TS_VECTOR, - name: SEARCH_VECTOR_FIELD.name, - label: SEARCH_VECTOR_FIELD.label.message ?? '', - description: SEARCH_VECTOR_FIELD.description.message ?? '', - isNullable: true, - }); - - if ( - !isFieldMetadataEntityOfType( - searchVectorFieldMetadata, - FieldMetadataType.TS_VECTOR, - ) - ) { - throw new Error( - 'Should never occur, created searchVectorFieldMetadata is not a TS_VECTOR field', - ); - } - - const searchableFieldForCustomObject = - createdObjectMetadata.labelIdentifierFieldMetadataId - ? createdObjectMetadata.fields.find( - (field) => - field.id === createdObjectMetadata.labelIdentifierFieldMetadataId, - ) - : createdObjectMetadata.fields.find( - (field) => field.name === DEFAULT_LABEL_IDENTIFIER_FIELD_NAME, - ); - - if (!isDefined(searchableFieldForCustomObject)) { - throw new Error( - `No searchable field found for custom object (object name: ${createdObjectMetadata.nameSingular})`, - ); - } - - await this.workspaceMigrationService.createCustomMigration( - generateMigrationName(`create-${createdObjectMetadata.nameSingular}`), - createdObjectMetadata.workspaceId, - [ - { - name: computeTableName( - createdObjectMetadata.nameSingular, - createdObjectMetadata.isCustom, - ), - action: WorkspaceMigrationTableActionType.ALTER, - columns: this.tsVectorColumnActionFactory.handleCreateAction({ - ...searchVectorFieldMetadata, - generatedType: 'STORED', - asExpression: getTsVectorColumnExpressionFromFields([ - { - type: searchableFieldForCustomObject.type as SearchableFieldType, - name: searchableFieldForCustomObject.name, - }, - ]), - }), - }, - ], - queryRunner, - ); - - await this.indexMetadataService.createIndex({ - workspaceId: objectMetadataInput.workspaceId, - objectMetadata: createdObjectMetadata, - fieldMetadataToIndex: [searchVectorFieldMetadata], - isUnique: false, - isCustom: false, - indexType: IndexType.GIN, - queryRunner, - }); - } - - public async updateSearchVector( - objectMetadataId: string, - fieldMetadataNameAndTypeForSearch: FieldTypeAndNameMetadata[], - workspaceId: string, - queryRunner?: QueryRunner, - ) { - const repository = queryRunner - ? queryRunner.manager.getRepository(ObjectMetadataEntity) - : this.objectMetadataRepository; - - const objectMetadata = await repository.findOneByOrFail({ - id: objectMetadataId, - }); - - const existingSearchVectorFieldMetadata = - await this.fieldMetadataRepository.findOneByOrFail({ - name: SEARCH_VECTOR_FIELD.name, - objectMetadataId, - }); - - await this.workspaceMigrationService.createCustomMigration( - generateMigrationName(`update-${objectMetadata.nameSingular}`), - workspaceId, - [ - { - name: computeTableName( - objectMetadata.nameSingular, - objectMetadata.isCustom, - ), - action: WorkspaceMigrationTableActionType.ALTER, - columns: this.workspaceMigrationFactory.createColumnActions( - WorkspaceMigrationColumnActionType.ALTER, - existingSearchVectorFieldMetadata, - { - ...existingSearchVectorFieldMetadata, - asExpression: getTsVectorColumnExpressionFromFields( - fieldMetadataNameAndTypeForSearch, - ), - generatedType: 'STORED', // Not stored on fieldMetadata - options: null, - } as TsVectorFieldMetadata, - ), - }, - ], - queryRunner, - ); - - // index needs to be recreated as typeorm deletes then recreates searchVector column at alter - await this.indexMetadataService.createIndexCreationMigration({ - workspaceId, - objectMetadata, - fieldMetadataToIndex: [existingSearchVectorFieldMetadata], - isUnique: false, - indexType: IndexType.GIN, - queryRunner, - }); - } -} diff --git a/packages/twenty-server/src/engine/metadata-modules/view-field/controllers/view-field.controller.ts b/packages/twenty-server/src/engine/metadata-modules/view-field/controllers/view-field.controller.ts index 554f088a2f..ada53554e9 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view-field/controllers/view-field.controller.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view-field/controllers/view-field.controller.ts @@ -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 { 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 { - 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 { - 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 { - 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) }; } diff --git a/packages/twenty-server/src/engine/metadata-modules/view-field/resolvers/view-field.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/view-field/resolvers/view-field.resolver.ts index cb80703957..2e14be5a6a 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view-field/resolvers/view-field.resolver.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view-field/resolvers/view-field.resolver.ts @@ -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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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, + }); } } diff --git a/packages/twenty-server/src/engine/metadata-modules/view-field/services/tests/view-field.service.spec.ts b/packages/twenty-server/src/engine/metadata-modules/view-field/services/tests/view-field.service.spec.ts deleted file mode 100644 index 09d60034a6..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/view-field/services/tests/view-field.service.spec.ts +++ /dev/null @@ -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; - - 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); - viewFieldRepository = module.get>( - 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(); - }); - }); -}); diff --git a/packages/twenty-server/src/engine/metadata-modules/view-field/services/view-field-v2.service.ts b/packages/twenty-server/src/engine/metadata-modules/view-field/services/view-field-v2.service.ts index 7f65a6cee6..08cbd33513 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view-field/services/view-field-v2.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view-field/services/view-field-v2.service.ts @@ -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, ) {} 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 { + return this.viewFieldRepository.find({ + where: { + workspaceId, + deletedAt: IsNull(), + }, + order: { position: 'ASC' }, + relations: ['workspace', 'view'], + }); + } + + async findByViewId( + workspaceId: string, + viewId: string, + ): Promise { + return this.viewFieldRepository.find({ + where: { + workspaceId, + viewId, + deletedAt: IsNull(), + }, + order: { position: 'ASC' }, + relations: ['workspace', 'view'], + }); + } + + async findById( + id: string, + workspaceId: string, + ): Promise { + const viewField = await this.viewFieldRepository.findOne({ + where: { + id, + workspaceId, + deletedAt: IsNull(), + }, + relations: ['workspace', 'view'], + }); + + return viewField || null; + } } diff --git a/packages/twenty-server/src/engine/metadata-modules/view-field/services/view-field.service.ts b/packages/twenty-server/src/engine/metadata-modules/view-field/services/view-field.service.ts deleted file mode 100644 index 92a789cb27..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/view-field/services/view-field.service.ts +++ /dev/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, - @InjectRepository(ViewEntity) - private readonly viewRepository: Repository, - private readonly workspaceCacheStorageService: WorkspaceCacheStorageService, - ) {} - - async findByWorkspaceId(workspaceId: string): Promise { - return this.viewFieldRepository.find({ - where: { - workspaceId, - deletedAt: IsNull(), - }, - order: { position: 'ASC' }, - relations: ['workspace', 'view'], - }); - } - - async findByViewId( - workspaceId: string, - viewId: string, - ): Promise { - return this.viewFieldRepository.find({ - where: { - workspaceId, - viewId, - deletedAt: IsNull(), - }, - order: { position: 'ASC' }, - relations: ['workspace', 'view'], - }); - } - - async findById( - id: string, - workspaceId: string, - ): Promise { - const viewField = await this.viewFieldRepository.findOne({ - where: { - id, - workspaceId, - deletedAt: IsNull(), - }, - relations: ['workspace', 'view'], - }); - - return viewField || null; - } - - async create( - viewFieldData: Partial, - ): Promise { - 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, - ): Promise { - 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 { - 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 { - 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, - ): data is Partial & { position: number } { - return isDefined(data.position); - } - - private disablesVisibility( - data: Partial, - ): data is Partial & { isVisible: boolean } { - return data.isVisible === false; - } - - private async verifyLabelMetadataIdentifierIsVisibleOrThrow( - newOrUpdatedViewField: Partial & { - 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 & { 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 & { viewId: string }, - workspaceId: string, - ): Promise> { - 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, - ): data is Partial & { - viewId: string; - fieldMetadataId: string; - workspaceId: string; - } { - return ( - isDefined(data.viewId) && - isDefined(data.fieldMetadataId) && - isDefined(data.workspaceId) - ); - } - - public async findViewByIdWithRelations( - id: string, - workspaceId: string, - ): Promise { - const view = await this.viewRepository.findOne({ - where: { - id, - workspaceId, - deletedAt: IsNull(), - }, - relations: ['workspace', 'objectMetadata', 'viewFields'], - }); - - return view || null; - } - - private async flushGraphQLCache(workspaceId: string): Promise { - await this.workspaceCacheStorageService.flushGraphQLOperation({ - operationName: FIND_ALL_CORE_VIEWS_GRAPHQL_OPERATION, - workspaceId, - }); - } -} diff --git a/packages/twenty-server/src/engine/metadata-modules/view-field/view-field.module.ts b/packages/twenty-server/src/engine/metadata-modules/view-field/view-field.module.ts index f1e9c01739..21d3e3983d 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view-field/view-field.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view-field/view-field.module.ts @@ -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 {} diff --git a/packages/twenty-server/src/engine/metadata-modules/view/resolvers/view.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/view/resolvers/view.resolver.ts index 553e121da0..d1b78cfc5a 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view/resolvers/view.resolver.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view/resolvers/view.resolver.ts @@ -24,9 +24,8 @@ import { CustomPermissionGuard } from 'src/engine/guards/custom-permission.guard import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard'; 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 { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service'; 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 { ViewFieldV2Service } from 'src/engine/metadata-modules/view-field/services/view-field-v2.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'; @@ -54,7 +53,6 @@ import { ViewGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/view/ export class ViewResolver { constructor( private readonly viewService: ViewService, - private readonly viewFieldService: ViewFieldService, private readonly viewFilterService: ViewFilterService, private readonly viewFilterGroupService: ViewFilterGroupService, private readonly viewSortService: ViewSortService, @@ -62,7 +60,8 @@ export class ViewResolver { private readonly i18nService: I18nService, private readonly featureFlagService: FeatureFlagService, private readonly viewV2Service: ViewV2Service, - private readonly userRoleService: UserRoleService, + + private readonly viewFieldV2Service: ViewFieldV2Service, ) {} @ResolveField(() => String) @@ -265,7 +264,7 @@ export class ViewResolver { return view.viewFields; } - return this.viewFieldService.findByViewId(workspace.id, view.id); + return this.viewFieldV2Service.findByViewId(workspace.id, view.id); } @ResolveField(() => [ViewFilterDTO]) diff --git a/packages/twenty-server/src/engine/metadata-modules/view/view.module.ts b/packages/twenty-server/src/engine/metadata-modules/view/view.module.ts index 338b188ea2..852da8b7c5 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view/view.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view/view.module.ts @@ -7,11 +7,11 @@ import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata import { FlatViewModule } from 'src/engine/metadata-modules/flat-view/flat-view.module'; import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module'; -import { ViewPermissionsModule } from 'src/engine/metadata-modules/view-permissions/view-permissions.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'; import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity'; diff --git a/packages/twenty-server/src/engine/twenty-orm/custom.workspace-entity.ts b/packages/twenty-server/src/engine/twenty-orm/custom.workspace-entity.ts index bce2fa09b8..8441e97f94 100644 --- a/packages/twenty-server/src/engine/twenty-orm/custom.workspace-entity.ts +++ b/packages/twenty-server/src/engine/twenty-orm/custom.workspace-entity.ts @@ -1,15 +1,15 @@ import { msg } from '@lingui/core/macro'; import { + ActorMetadata, FieldMetadataType, RelationOnDeleteAction, - ActorMetadata, } from 'twenty-shared/types'; import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/constants/search-vector-field.constants'; import { IndexType } from 'src/engine/metadata-modules/index-metadata/types/indexType.types'; -import { DEFAULT_LABEL_IDENTIFIER_FIELD_NAME } from 'src/engine/metadata-modules/object-metadata/object-metadata.constants'; +import { DEFAULT_LABEL_IDENTIFIER_FIELD_NAME } from 'src/engine/metadata-modules/object-metadata/constants/object-metadata.constants'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { WorkspaceCustomEntity } from 'src/engine/twenty-orm/decorators/workspace-custom-entity.decorator'; import { WorkspaceFieldIndex } from 'src/engine/twenty-orm/decorators/workspace-field-index.decorator'; diff --git a/packages/twenty-server/src/engine/workspace-manager/__tests__/workspace-manager.service.spec.ts b/packages/twenty-server/src/engine/workspace-manager/__tests__/workspace-manager.service.spec.ts index 240efddac8..6df70cc5ff 100644 --- a/packages/twenty-server/src/engine/workspace-manager/__tests__/workspace-manager.service.spec.ts +++ b/packages/twenty-server/src/engine/workspace-manager/__tests__/workspace-manager.service.spec.ts @@ -13,7 +13,6 @@ import { DataSourceService } from 'src/engine/metadata-modules/data-source/data- import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { ObjectMetadataServiceV2 } from 'src/engine/metadata-modules/object-metadata/object-metadata-v2.service'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service'; import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service'; import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; @@ -28,13 +27,13 @@ import { WorkspaceSyncMetadataService } from 'src/engine/workspace-manager/works describe('WorkspaceManagerService', () => { let service: WorkspaceManagerService; - let objectMetadataServiceV2: ObjectMetadataServiceV2; let workspaceMigrationRepository: Repository; let dataSourceRepository: Repository; let workspaceDataSourceService: WorkspaceDataSourceService; let roleTargetsRepository: Repository; let roleRepository: Repository; let mockDataSource: jest.Mocked; + let objectMetadataServiceV2: ObjectMetadataServiceV2; beforeEach(async () => { mockDataSource = { @@ -102,10 +101,6 @@ describe('WorkspaceManagerService', () => { provide: FeatureFlagService, useValue: {}, }, - { - provide: ObjectMetadataService, - useValue: {}, - }, { provide: RoleService, useValue: {}, @@ -154,9 +149,6 @@ describe('WorkspaceManagerService', () => { }).compile(); service = module.get(WorkspaceManagerService); - objectMetadataServiceV2 = module.get( - ObjectMetadataServiceV2, - ); workspaceMigrationRepository = module.get< Repository >(getRepositoryToken(WorkspaceMigrationEntity)); @@ -172,6 +164,9 @@ describe('WorkspaceManagerService', () => { roleRepository = module.get>( getRepositoryToken(RoleEntity), ); + objectMetadataServiceV2 = module.get( + ObjectMetadataServiceV2, + ); }); it('should be defined', () => { diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/services/dev-seeder-data.service.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/services/dev-seeder-data.service.ts index a2949997f3..bdceb99b25 100644 --- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/services/dev-seeder-data.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/services/dev-seeder-data.service.ts @@ -8,8 +8,8 @@ import { DataSource } from 'typeorm'; import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service'; +import { ObjectMetadataServiceV2 } from 'src/engine/metadata-modules/object-metadata/object-metadata-v2.service'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service'; import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager'; import { computeTableName } from 'src/engine/utils/compute-table-name.util'; import { @@ -265,7 +265,7 @@ export class DevSeederDataService { constructor( @InjectDataSource() private readonly coreDataSource: DataSource, - private readonly objectMetadataService: ObjectMetadataService, + private readonly objectMetadataService: ObjectMetadataServiceV2, private readonly timelineActivitySeederService: TimelineActivitySeederService, private readonly fileStorageService: FileStorageService, ) {} diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/services/timeline-activity-seeder.service.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/services/timeline-activity-seeder.service.ts index 4d3a297287..047e390745 100644 --- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/services/timeline-activity-seeder.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/services/timeline-activity-seeder.service.ts @@ -3,7 +3,7 @@ import { Injectable } from '@nestjs/common'; import chunk from 'lodash.chunk'; import { ObjectRecord } from 'twenty-shared/types'; -import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service'; +import { ObjectMetadataServiceV2 } from 'src/engine/metadata-modules/object-metadata/object-metadata-v2.service'; import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager'; import { CALENDAR_EVENT_DATA_SEEDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/calendar-event-data-seeds.constant'; import { @@ -107,7 +107,9 @@ export class TimelineActivitySeederService { 'message', ]); - constructor(private readonly objectMetadataService: ObjectMetadataService) {} + constructor( + private readonly objectMetadataService: ObjectMetadataServiceV2, + ) {} private getLinkedActivityName(activityType: string): string { // Notes and tasks use the legacy format: linked-{type}.created diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/metadata/services/dev-seeder-metadata.service.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/metadata/services/dev-seeder-metadata.service.ts index 2f5b64fd76..7c6681135f 100644 --- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/metadata/services/dev-seeder-metadata.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/metadata/services/dev-seeder-metadata.service.ts @@ -8,7 +8,6 @@ import { type DataSourceEntity } from 'src/engine/metadata-modules/data-source/d import { CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input'; import { FieldMetadataServiceV2 } from 'src/engine/metadata-modules/field-metadata/services/field-metadata.service-v2'; import { ObjectMetadataServiceV2 } from 'src/engine/metadata-modules/object-metadata/object-metadata-v2.service'; -import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service'; import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service'; import { @@ -30,7 +29,6 @@ import { prefillCoreViews } from 'src/engine/workspace-manager/standard-objects- @Injectable() export class DevSeederMetadataService { constructor( - private readonly objectMetadataService: ObjectMetadataService, private readonly objectMetadataServiceV2: ObjectMetadataServiceV2, private readonly fieldMetadataServiceV2: FieldMetadataServiceV2, private readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService, @@ -140,7 +138,7 @@ export class DevSeederMetadataService { workspaceId: string; objectMetadataSeed: ObjectMetadataSeed; }): Promise { - await this.objectMetadataServiceV2.createOne({ + await this.objectMetadataServiceV2.createOneObject({ createObjectInput: { ...objectMetadataSeed, dataSourceId, @@ -159,7 +157,7 @@ export class DevSeederMetadataService { fieldMetadataSeeds: FieldMetadataSeed[]; }): Promise { const objectMetadata = - await this.objectMetadataService.findOneWithinWorkspace(workspaceId, { + await this.objectMetadataServiceV2.findOneWithinWorkspace(workspaceId, { where: { nameSingular: objectMetadataNameSingular }, }); @@ -173,7 +171,7 @@ export class DevSeederMetadataService { objectMetadataId: objectMetadata.id, })); - await this.fieldMetadataServiceV2.createMany({ + await this.fieldMetadataServiceV2.createManyFields({ createFieldInputs, workspaceId, }); @@ -189,7 +187,7 @@ export class DevSeederMetadataService { featureFlags?: Record; }): Promise { const createdObjectMetadata = - await this.objectMetadataService.findManyWithinWorkspace(workspaceId); + await this.objectMetadataServiceV2.findManyWithinWorkspace(workspaceId); await prefillCoreViews({ coreDataSource: this.coreDataSource, @@ -243,7 +241,7 @@ export class DevSeederMetadataService { objectMetadataMaps, }); - await this.fieldMetadataServiceV2.createMany({ + await this.fieldMetadataServiceV2.createManyFields({ createFieldInputs, workspaceId, }); diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-health/workspace-health.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-health/workspace-health.service.ts index f886e8fd8f..b84eeecce1 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-health/workspace-health.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-health/workspace-health.service.ts @@ -11,7 +11,7 @@ import { } from 'src/engine/workspace-manager/workspace-health/interfaces/workspace-health-options.interface'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; -import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service'; +import { ObjectMetadataServiceV2 } from 'src/engine/metadata-modules/object-metadata/object-metadata-v2.service'; import { WorkspaceMigrationEntity } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util'; @@ -29,7 +29,7 @@ export class WorkspaceHealthService { @InjectDataSource() private readonly coreDataSource: DataSource, private readonly dataSourceService: DataSourceService, - private readonly objectMetadataService: ObjectMetadataService, + private readonly objectMetadataService: ObjectMetadataServiceV2, private readonly databaseStructureService: DatabaseStructureService, private readonly objectMetadataHealthService: ObjectMetadataHealthService, private readonly fieldMetadataHealthService: FieldMetadataHealthService, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-manager.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-manager.service.ts index f0ce3bc0e4..0a55ca6dfa 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-manager.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-manager.service.ts @@ -10,7 +10,6 @@ import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.ent import { type DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; import { ObjectMetadataServiceV2 } from 'src/engine/metadata-modules/object-metadata/object-metadata-v2.service'; -import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service'; import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { RoleService } from 'src/engine/metadata-modules/role/role.service'; @@ -32,7 +31,6 @@ export class WorkspaceManagerService { private readonly coreDataSource: DataSource, private readonly workspaceDataSourceService: WorkspaceDataSourceService, private readonly workspaceMigrationService: WorkspaceMigrationService, - private readonly objectMetadataService: ObjectMetadataService, private readonly objectMetadataServiceV2: ObjectMetadataServiceV2, private readonly dataSourceService: DataSourceService, private readonly workspaceSyncMetadataService: WorkspaceSyncMetadataService, @@ -121,7 +119,7 @@ export class WorkspaceManagerService { featureFlags: Record, ) { const createdObjectMetadata = - await this.objectMetadataService.findManyWithinWorkspace(workspaceId); + await this.objectMetadataServiceV2.findManyWithinWorkspace(workspaceId); await standardObjectsPrefillData( this.coreDataSource, diff --git a/packages/twenty-server/test/integration/graphql/suites/view/view-field/view-field-resolver.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/view/view-field/view-field-resolver.integration-spec.ts deleted file mode 100644 index 949fa7691c..0000000000 --- a/packages/twenty-server/test/integration/graphql/suites/view/view-field/view-field-resolver.integration-spec.ts +++ /dev/null @@ -1,359 +0,0 @@ -import { findViewFieldsOperationFactory } from 'test/integration/graphql/utils/find-view-fields-operation-factory.util'; -import { - assertGraphQLErrorResponse, - assertGraphQLSuccessfulResponse, -} from 'test/integration/graphql/utils/graphql-test-assertions.util'; -import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util'; -import { - createViewFieldData, - updateViewFieldData, -} from 'test/integration/graphql/utils/view-data-factory.util'; -import { createTestViewWithGraphQL } from 'test/integration/graphql/utils/view-graphql.util'; -import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util'; -import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util'; -import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util'; -import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util'; -import { updateFeatureFlag } from 'test/integration/metadata/suites/utils/update-feature-flag.util'; -import { createOneCoreViewField } from 'test/integration/metadata/suites/view-field/utils/create-one-core-view-field.util'; -import { deleteOneCoreViewField } from 'test/integration/metadata/suites/view-field/utils/delete-one-core-view-field.util'; -import { destroyOneCoreViewField } from 'test/integration/metadata/suites/view-field/utils/destroy-one-core-view-field.util'; -import { updateOneCoreViewField } from 'test/integration/metadata/suites/view-field/utils/update-one-core-view-field.util'; -import { destroyOneCoreView } from 'test/integration/metadata/suites/view/utils/destroy-one-core-view.util'; -import { assertViewFieldStructure } from 'test/integration/utils/view-test.util'; -import { FieldMetadataType } from 'twenty-shared/types'; - -import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; -import { ErrorCode } from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; -import { type CreateViewFieldInput } from 'src/engine/metadata-modules/view-field/dtos/inputs/create-view-field.input'; -import { - generateViewFieldExceptionMessage, - ViewFieldExceptionMessageKey, -} from 'src/engine/metadata-modules/view-field/exceptions/view-field.exception'; - -const TEST_NOT_EXISTING_VIEW_FIELD_ID = '20202020-0000-4000-8000-000000000001'; - -describe('View Field Resolver', () => { - let testViewId: string; - let testObjectMetadataId: string; - let testFieldMetadataId: string; - - beforeAll(async () => { - await updateFeatureFlag({ - expectToFail: false, - featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED, - value: false, - }); - }); - - beforeAll(async () => { - const { - data: { - createOneObject: { id: objectMetadataId }, - }, - } = await createOneObjectMetadata({ - expectToFail: false, - input: { - nameSingular: 'myFieldTestObject', - namePlural: 'myFieldTestObjects', - labelSingular: 'My Field Test Object', - labelPlural: 'My Field Test Objects', - icon: 'Icon123', - }, - }); - - testObjectMetadataId = objectMetadataId; - - const createFieldInput = { - name: 'testField', - label: 'Test Field', - type: FieldMetadataType.TEXT, - objectMetadataId: testObjectMetadataId, - isLabelSyncedWithName: true, - }; - - const { - data: { - createOneField: { id: fieldMetadataId }, - }, - } = await createOneFieldMetadata({ - expectToFail: false, - input: createFieldInput, - gqlFields: ` - id - name - label - isLabelSyncedWithName - `, - }); - - testFieldMetadataId = fieldMetadataId; - }); - - afterAll(async () => { - await updateOneObjectMetadata({ - expectToFail: false, - input: { - idToUpdate: testObjectMetadataId, - updatePayload: { - isActive: false, - }, - }, - }); - await deleteOneObjectMetadata({ - expectToFail: false, - input: { idToDelete: testObjectMetadataId }, - }); - }); - - afterAll(async () => { - await updateFeatureFlag({ - expectToFail: false, - featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED, - value: true, - }); - }); - - beforeEach(async () => { - const view = await createTestViewWithGraphQL({ - name: 'Test View for Fields', - objectMetadataId: testObjectMetadataId, - }); - - testViewId = view.id; - }); - - afterEach(async () => { - await destroyOneCoreView({ - viewId: testViewId, - expectToFail: false, - }); - }); - - describe('getCoreViewFields', () => { - it('should return empty array when no view fields exist', async () => { - const operation = findViewFieldsOperationFactory({ viewId: testViewId }); - const response = await makeGraphqlAPIRequest(operation); - - assertGraphQLSuccessfulResponse(response); - expect(response.body.data.getCoreViewFields).toEqual([]); - }); - - it('should return view fields for a specific view', async () => { - const fieldData = createViewFieldData(testViewId, { - position: 0, - isVisible: true, - size: 150, - fieldMetadataId: testFieldMetadataId, - }); - - await createOneCoreViewField({ - input: fieldData as CreateViewFieldInput, - expectToFail: false, - }); - - const getOperation = findViewFieldsOperationFactory({ - viewId: testViewId, - }); - const response = await makeGraphqlAPIRequest(getOperation); - - assertGraphQLSuccessfulResponse(response); - expect(response.body.data.getCoreViewFields).toHaveLength(1); - assertViewFieldStructure(response.body.data.getCoreViewFields[0], { - fieldMetadataId: testFieldMetadataId, - position: 0, - isVisible: true, - size: 150, - viewId: testViewId, - }); - }); - }); - - describe('createCoreViewField', () => { - it('should create a new view field', async () => { - const fieldData = createViewFieldData(testViewId, { - position: 1, - isVisible: true, - size: 200, - fieldMetadataId: testFieldMetadataId, - }); - - const response = await createOneCoreViewField({ - input: fieldData as CreateViewFieldInput, - expectToFail: false, - }); - - assertGraphQLSuccessfulResponse({ body: response, status: 200 }); - assertViewFieldStructure(response.data.createCoreViewField, { - fieldMetadataId: testFieldMetadataId, - position: 1, - isVisible: true, - size: 200, - viewId: testViewId, - }); - }); - - it('should create a hidden view field', async () => { - const fieldData = { - fieldMetadataId: testFieldMetadataId, - position: 2, - isVisible: false, - size: 100, - viewId: testViewId, - }; - - const response = await createOneCoreViewField({ - input: fieldData, - expectToFail: false, - }); - - assertGraphQLSuccessfulResponse({ body: response, status: 200 }); - assertViewFieldStructure(response.data.createCoreViewField, { - fieldMetadataId: testFieldMetadataId, - position: 2, - isVisible: false, - size: 100, - viewId: testViewId, - }); - }); - }); - - describe('updateCoreViewField', () => { - it('should update an existing view field', async () => { - const fieldData = createViewFieldData(testViewId, { - position: 0, - isVisible: true, - size: 150, - fieldMetadataId: testFieldMetadataId, - }); - const createResponse = await createOneCoreViewField({ - input: fieldData as CreateViewFieldInput, - expectToFail: false, - }); - const viewField = createResponse.data.createCoreViewField; - - const updateInput = updateViewFieldData({ - position: 5, - isVisible: false, - size: 300, - }); - const response = await updateOneCoreViewField({ - input: { - id: viewField.id, - update: updateInput, - }, - expectToFail: false, - }); - - assertGraphQLSuccessfulResponse({ body: response, status: 200 }); - expect(response.data.updateCoreViewField).toMatchObject({ - id: viewField.id, - position: 5, - isVisible: false, - size: 300, - }); - }); - - it('should throw an error when updating non-existent view field', async () => { - const response = await updateOneCoreViewField({ - input: { - id: TEST_NOT_EXISTING_VIEW_FIELD_ID, - update: { - position: 1, - }, - }, - expectToFail: true, - }); - - assertGraphQLErrorResponse( - { body: response, status: 200 }, - ErrorCode.NOT_FOUND, - generateViewFieldExceptionMessage( - ViewFieldExceptionMessageKey.VIEW_FIELD_NOT_FOUND, - TEST_NOT_EXISTING_VIEW_FIELD_ID, - ), - ); - }); - }); - - describe('deleteCoreViewField', () => { - it('should delete an existing view field', async () => { - const fieldData = createViewFieldData(testViewId, { - fieldMetadataId: testFieldMetadataId, - }); - const createResponse = await createOneCoreViewField({ - input: fieldData as CreateViewFieldInput, - expectToFail: false, - }); - const viewField = createResponse.data.createCoreViewField; - - const response = await deleteOneCoreViewField({ - input: { id: viewField.id }, - expectToFail: false, - }); - - assertGraphQLSuccessfulResponse({ body: response, status: 200 }); - expect(response.data.deleteCoreViewField).toMatchObject({ - id: viewField.id, - }); - }); - - it('should throw an error when deleting non-existent view field', async () => { - const response = await deleteOneCoreViewField({ - input: { id: TEST_NOT_EXISTING_VIEW_FIELD_ID }, - expectToFail: true, - }); - - assertGraphQLErrorResponse( - { body: response, status: 200 }, - ErrorCode.NOT_FOUND, - generateViewFieldExceptionMessage( - ViewFieldExceptionMessageKey.VIEW_FIELD_NOT_FOUND, - TEST_NOT_EXISTING_VIEW_FIELD_ID, - ), - ); - }); - }); - - describe('destroyCoreViewField', () => { - it('should destroy an existing view field', async () => { - const fieldData = createViewFieldData(testViewId, { - fieldMetadataId: testFieldMetadataId, - }); - const createResponse = await createOneCoreViewField({ - input: fieldData as CreateViewFieldInput, - expectToFail: false, - }); - const viewField = createResponse.data.createCoreViewField; - - const response = await destroyOneCoreViewField({ - input: { - id: viewField.id, - }, - expectToFail: false, - }); - - assertGraphQLSuccessfulResponse({ body: response, status: 200 }); - expect(response.data.destroyCoreViewField).toMatchObject({ - id: viewField.id, - }); - }); - - it('should throw an error when destroying non-existent view field', async () => { - const response = await destroyOneCoreViewField({ - input: { - id: TEST_NOT_EXISTING_VIEW_FIELD_ID, - }, - expectToFail: true, - }); - - assertGraphQLErrorResponse( - { body: response, status: 200 }, - ErrorCode.NOT_FOUND, - generateViewFieldExceptionMessage( - ViewFieldExceptionMessageKey.VIEW_FIELD_NOT_FOUND, - TEST_NOT_EXISTING_VIEW_FIELD_ID, - ), - ); - }); - }); -}); diff --git a/packages/twenty-server/test/integration/metadata/suites/agent/utils/agent-tool-test-utils.ts b/packages/twenty-server/test/integration/metadata/suites/agent/utils/agent-tool-test-utils.ts index c79bd7441c..4af26c3e76 100644 --- a/packages/twenty-server/test/integration/metadata/suites/agent/utils/agent-tool-test-utils.ts +++ b/packages/twenty-server/test/integration/metadata/suites/agent/utils/agent-tool-test-utils.ts @@ -20,8 +20,8 @@ import { AgentHandoffService } from 'src/engine/metadata-modules/agent/agent-han import { AgentToolGeneratorService } from 'src/engine/metadata-modules/agent/agent-tool-generator.service'; import { AgentEntity } from 'src/engine/metadata-modules/agent/agent.entity'; import { AgentService } from 'src/engine/metadata-modules/agent/agent.service'; +import { ObjectMetadataServiceV2 } from 'src/engine/metadata-modules/object-metadata/object-metadata-v2.service'; import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service'; import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service'; @@ -36,7 +36,7 @@ export interface AgentToolTestContext { module: TestingModule; agentToolService: AgentToolGeneratorService; agentService: AgentService; - objectMetadataService: ObjectMetadataService; + objectMetadataService: ObjectMetadataServiceV2; roleRepository: Repository; workspacePermissionsCacheService: WorkspacePermissionsCacheService; twentyORMGlobalManager: TwentyORMGlobalManager; @@ -78,7 +78,7 @@ export const createAgentToolTestModule = }, }, { - provide: ObjectMetadataService, + provide: ObjectMetadataServiceV2, useValue: { findManyWithinWorkspace: jest.fn(), findOneWithinWorkspace: jest.fn(), @@ -215,8 +215,8 @@ export const createAgentToolTestModule = AgentToolGeneratorService, ); const agentService = module.get(AgentService); - const objectMetadataService = module.get( - ObjectMetadataService, + const objectMetadataService = module.get( + ObjectMetadataServiceV2, ); const roleRepository = module.get>( getRepositoryToken(RoleEntity), diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/__snapshots__/update-one-field-metadata-related-record.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/field-metadata/__snapshots__/update-one-field-metadata-related-record.integration-spec.ts.snap deleted file mode 100644 index 68c702d4a9..0000000000 --- a/packages/twenty-server/test/integration/metadata/suites/field-metadata/__snapshots__/update-one-field-metadata-related-record.integration-spec.ts.snap +++ /dev/null @@ -1,221 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`update-one-field-metadata-related-record MULTI_SELECT should handle adding new options while maintaining existing view filter 1`] = ` -{ - "operand": "IS", - "positionInViewFilterGroup": null, - "subFieldName": null, - "value": [ - "OPTION_0", - "OPTION_1", - ], - "viewFilterGroupId": null, -} -`; - -exports[`update-one-field-metadata-related-record MULTI_SELECT should handle no changes update of options while maintaining existing view filter values 1`] = ` -{ - "operand": "IS", - "positionInViewFilterGroup": null, - "subFieldName": null, - "value": [ - "OPTION_0", - "OPTION_1", - "OPTION_2", - "OPTION_3", - "OPTION_4", - "OPTION_5", - "OPTION_6", - "OPTION_7", - "OPTION_8", - "OPTION_9", - ], - "viewFilterGroupId": null, -} -`; - -exports[`update-one-field-metadata-related-record MULTI_SELECT should handle partial deletion of selected options in view filter 1`] = ` -{ - "operand": "IS", - "positionInViewFilterGroup": null, - "subFieldName": null, - "value": [ - "OPTION_4", - "OPTION_5", - "OPTION_6", - "OPTION_7", - "OPTION_8", - "OPTION_9", - ], - "viewFilterGroupId": null, -} -`; - -exports[`update-one-field-metadata-related-record MULTI_SELECT should handle reordering of options while maintaining view filter values 1`] = ` -{ - "operand": "IS", - "positionInViewFilterGroup": null, - "subFieldName": null, - "value": [ - "OPTION_0", - "OPTION_1", - ], - "viewFilterGroupId": null, -} -`; - -exports[`update-one-field-metadata-related-record MULTI_SELECT should throw error if view filter value is not a stringified JSON array 1`] = ` -[ - { - "extensions": { - "code": "INTERNAL_SERVER_ERROR", - "exceptionEventId": "mocked-exception-id", - "userFriendlyMessage": "An error occurred.", - }, - "message": "Unexpected invalid view filter value for filter 20202020-e3b5-4fa7-85aa-9b1950fc7bf5", - }, -] -`; - -exports[`update-one-field-metadata-related-record MULTI_SELECT should update related multi selected options view filter 1`] = ` -{ - "operand": "IS", - "positionInViewFilterGroup": null, - "subFieldName": null, - "value": [ - "OPTION_0_UPDATED", - "OPTION_1", - "OPTION_2_UPDATED", - "OPTION_3", - "OPTION_4_UPDATED", - "OPTION_5", - "OPTION_6_UPDATED", - "OPTION_7", - "OPTION_8_UPDATED", - "OPTION_9", - ], - "viewFilterGroupId": null, -} -`; - -exports[`update-one-field-metadata-related-record MULTI_SELECT should update related solo selected option view filter 1`] = ` -{ - "operand": "IS", - "positionInViewFilterGroup": null, - "subFieldName": null, - "value": [ - "OPTION_5_UPDATED", - ], - "viewFilterGroupId": null, -} -`; - -exports[`update-one-field-metadata-related-record SELECT should handle adding new options while maintaining existing view filter 1`] = ` -{ - "operand": "IS", - "positionInViewFilterGroup": null, - "subFieldName": null, - "value": [ - "OPTION_0", - "OPTION_1", - ], - "viewFilterGroupId": null, -} -`; - -exports[`update-one-field-metadata-related-record SELECT should handle no changes update of options while maintaining existing view filter values 1`] = ` -{ - "operand": "IS", - "positionInViewFilterGroup": null, - "subFieldName": null, - "value": [ - "OPTION_0", - "OPTION_1", - "OPTION_2", - "OPTION_3", - "OPTION_4", - "OPTION_5", - "OPTION_6", - "OPTION_7", - "OPTION_8", - "OPTION_9", - ], - "viewFilterGroupId": null, -} -`; - -exports[`update-one-field-metadata-related-record SELECT should handle partial deletion of selected options in view filter 1`] = ` -{ - "operand": "IS", - "positionInViewFilterGroup": null, - "subFieldName": null, - "value": [ - "OPTION_4", - "OPTION_5", - "OPTION_6", - "OPTION_7", - "OPTION_8", - "OPTION_9", - ], - "viewFilterGroupId": null, -} -`; - -exports[`update-one-field-metadata-related-record SELECT should handle reordering of options while maintaining view filter values 1`] = ` -{ - "operand": "IS", - "positionInViewFilterGroup": null, - "subFieldName": null, - "value": [ - "OPTION_0", - "OPTION_1", - ], - "viewFilterGroupId": null, -} -`; - -exports[`update-one-field-metadata-related-record SELECT should throw error if view filter value is not a stringified JSON array 1`] = ` -[ - { - "extensions": { - "code": "INTERNAL_SERVER_ERROR", - "exceptionEventId": "mocked-exception-id", - "userFriendlyMessage": "An error occurred.", - }, - "message": "Unexpected invalid view filter value for filter 20202020-e3b5-4fa7-85aa-9b1950fc7bf5", - }, -] -`; - -exports[`update-one-field-metadata-related-record SELECT should update related multi selected options view filter 1`] = ` -{ - "operand": "IS", - "positionInViewFilterGroup": null, - "subFieldName": null, - "value": [ - "OPTION_0_UPDATED", - "OPTION_1", - "OPTION_2_UPDATED", - "OPTION_3", - "OPTION_4_UPDATED", - "OPTION_5", - "OPTION_6_UPDATED", - "OPTION_7", - "OPTION_8_UPDATED", - "OPTION_9", - ], - "viewFilterGroupId": null, -} -`; - -exports[`update-one-field-metadata-related-record SELECT should update related solo selected option view filter 1`] = ` -{ - "operand": "IS", - "positionInViewFilterGroup": null, - "subFieldName": null, - "value": [ - "OPTION_5_UPDATED", - ], - "viewFilterGroupId": null, -} -`; diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/delete-one-field-metadata.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/field-metadata/delete-one-field-metadata.integration-spec.ts deleted file mode 100644 index d6bde7e9e4..0000000000 --- a/packages/twenty-server/test/integration/metadata/suites/field-metadata/delete-one-field-metadata.integration-spec.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util'; -import { deleteOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/delete-one-field-metadata.util'; -import { updateOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/update-one-field-metadata.util'; -import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util'; -import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util'; -import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util'; -import { updateFeatureFlag } from 'test/integration/metadata/suites/utils/update-feature-flag.util'; -import { - createTestViewWithRestApi, - findViewByIdWithRestApi, -} from 'test/integration/rest/utils/view-rest-api.util'; -import { generateRecordName } from 'test/integration/utils/generate-record-name'; -import { FieldMetadataType } from 'twenty-shared/types'; -import { isDefined } from 'twenty-shared/utils'; - -import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant'; -import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; -import { ViewType } from 'src/engine/metadata-modules/view/enums/view-type.enum'; - -describe('deleteOne', () => { - beforeAll(async () => { - await updateFeatureFlag({ - expectToFail: false, - featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED, - value: false, - }); - }); - - afterAll(async () => { - await updateFeatureFlag({ - expectToFail: false, - featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED, - value: true, - }); - }); - describe('Kanban aggregate operation', () => { - let listingObjectId = ''; - let testFieldId = ''; - let viewId = ''; - - beforeEach(async () => { - const { data } = await createOneObjectMetadata({ - expectToFail: false, - input: { - nameSingular: 'listingv2', - namePlural: 'listingsv2', - labelSingular: 'Listing', - labelPlural: 'Listings', - icon: 'IconBuildingSkyscraper', - }, - }); - - listingObjectId = data.createOneObject.id; - const { data: createdFieldData } = await createOneFieldMetadata({ - expectToFail: false, - input: { - name: 'house', - type: FieldMetadataType.TEXT, - label: 'House', - objectMetadataId: listingObjectId, - }, - }); - - testFieldId = createdFieldData.createOneField.id; - - const createdView = await createTestViewWithRestApi({ - name: generateRecordName('By Type'), - objectMetadataId: listingObjectId, - type: ViewType.KANBAN, - kanbanAggregateOperationFieldMetadataId: testFieldId, - kanbanAggregateOperation: AggregateOperations.MAX, - icon: 'IconLayoutKanban', - }); - - viewId = createdView.id; - }); - afterEach(async () => { - await updateOneObjectMetadata({ - expectToFail: false, - input: { - idToUpdate: listingObjectId, - updatePayload: { - isActive: false, - }, - }, - }); - await deleteOneObjectMetadata({ - expectToFail: false, - input: { idToDelete: listingObjectId }, - }); - }); - - it('should delete kanban aggregate operation when deleting a field used as kanbanAggregateOperationFieldMetadataId', async () => { - const viewThatShouldBeUpdated = await findViewByIdWithRestApi(viewId); - - if (!isDefined(viewThatShouldBeUpdated)) { - throw new Error('View not found, this should not happen'); - } - - expect( - viewThatShouldBeUpdated.kanbanAggregateOperationFieldMetadataId, - ).toBe(testFieldId); - expect(viewThatShouldBeUpdated.kanbanAggregateOperation).toBe('MAX'); - - await updateOneFieldMetadata({ - expectToFail: false, - input: { - idToUpdate: testFieldId, - updatePayload: { isActive: false }, - }, - gqlFields: ` - id - isActive - `, - }); - - const { data } = await deleteOneFieldMetadata({ - expectToFail: false, - input: { idToDelete: testFieldId }, - }); - - expect(data.deleteOneField.id).toBe(testFieldId); - - const updatedViewResponse = await findViewByIdWithRestApi(viewId); - - if (!isDefined(updatedViewResponse)) { - throw new Error('View not found, this should not happen'); - } - - expect( - updatedViewResponse.kanbanAggregateOperationFieldMetadataId, - ).toBeNull(); - expect(updatedViewResponse.kanbanAggregateOperation).toBe(null); - }); - }); -}); diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/enum/__snapshots__/failing-create-one-multi-select-field-metadata-enum.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/field-metadata/enum/__snapshots__/failing-create-one-multi-select-field-metadata-enum.integration-spec.ts.snap deleted file mode 100644 index b92855602d..0000000000 --- a/packages/twenty-server/test/integration/metadata/suites/field-metadata/enum/__snapshots__/failing-create-one-multi-select-field-metadata-enum.integration-spec.ts.snap +++ /dev/null @@ -1,518 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with an invalid default value 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "An error occurred.", - }, - "message": "Default value for multi-select must be an array", - "name": "UserInputError", - }, -] -`; - -exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with an unknown default value 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "An error occurred.", - }, - "message": "Default value for multi-select must be an array", - "name": "UserInputError", - }, -] -`; - -exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with comma in option label 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Label must not contain a comma", - }, - "message": "Label must not contain a comma", - "name": "UserInputError", - }, -] -`; - -exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with duplicated option ids 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Duplicated option id", - }, - "message": "Duplicated option {field}", - "name": "UserInputError", - }, -] -`; - -exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with duplicated option positions 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Duplicated option position", - }, - "message": "Duplicated option {field}", - "name": "UserInputError", - }, -] -`; - -exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with duplicated option values 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Duplicated option value", - }, - "message": "Duplicated option {field}", - "name": "UserInputError", - }, -] -`; - -exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with duplicated trimmed option values 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Duplicated option value", - }, - "message": "Duplicated option {field}", - "name": "UserInputError", - }, -] -`; - -exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with empty multi-select default values array 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "If defined default value must contain at least one value", - }, - "message": "If defined default value must contain at least one value", - "name": "UserInputError", - }, -] -`; - -exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with empty options 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "An error occurred.", - }, - "message": "Options are required for enum fields", - "name": "UserInputError", - }, -] -`; - -exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with empty string defaultValue 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Default value should be as quoted string", - }, - "message": "Default value should be as quoted string", - "name": "UserInputError", - }, -] -`; - -exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with empty string id 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Option id is invalid", - }, - "message": "Option id is invalid", - "name": "UserInputError", - }, -] -`; - -exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with empty string label 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Option label "" is beneath 1 character", - }, - "message": "Option label "{sanitizedLabel}" is beneath 1 character", - "name": "UserInputError", - }, -] -`; - -exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with empty string value 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Option value "" is beneath 1 character", - }, - "message": "Option value "{sanitizedValue}" is beneath 1 character", - "name": "UserInputError", - }, -] -`; - -exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with invalid multi-select default value format 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Default value should be as quoted string", - }, - "message": "Default value should be as quoted string", - "name": "UserInputError", - }, -] -`; - -exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with invalid option id 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Option id is invalid", - }, - "message": "Option id is invalid", - "name": "UserInputError", - }, -] -`; - -exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with invalid option value format 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Value must be in UPPER_CASE and follow snake_case "Option 1 and some other things, /"", - }, - "message": "Value must be in UPPER_CASE and follow snake_case "{sanitizedValue}"", - "name": "UserInputError", - }, -] -`; - -exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with non stringified array default value 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "An error occurred.", - }, - "message": "Default value for multi-select must be an array", - "name": "UserInputError", - }, -] -`; - -exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with not a string defaultValue 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "An error occurred.", - }, - "message": "Default value for multi-select must be a stringified array", - "name": "UserInputError", - }, -] -`; - -exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with not a string id 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Option id is invalid", - }, - "message": "Option id is invalid", - "name": "UserInputError", - }, -] -`; - -exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with not a string label 1`] = ` -[ - { - "extensions": { - "code": "INTERNAL_SERVER_ERROR", - "exceptionEventId": "mocked-exception-id", - "userFriendlyMessage": "An error occurred.", - }, - "message": "label.includes is not a function", - }, -] -`; - -exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with not a string value 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Value must be in UPPER_CASE and follow snake_case "22222"", - }, - "message": "Value must be in UPPER_CASE and follow snake_case "{sanitizedValue}"", - "name": "UserInputError", - }, -] -`; - -exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with null id 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Option id is required", - }, - "message": "Option id is required", - "name": "UserInputError", - }, -] -`; - -exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with null label 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Option label is required", - }, - "message": "Option label is required", - "name": "UserInputError", - }, -] -`; - -exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with null options 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "An error occurred.", - }, - "message": "Options are required for enum fields", - "name": "UserInputError", - }, -] -`; - -exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with null value 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Option value is required", - }, - "message": "Option value is required", - "name": "UserInputError", - }, -] -`; - -exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with only white spaces defaultValue 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Default value should be as quoted string", - }, - "message": "Default value should be as quoted string", - "name": "UserInputError", - }, -] -`; - -exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with only white spaces id 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Option id is invalid", - }, - "message": "Option id is invalid", - "name": "UserInputError", - }, -] -`; - -exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with only white spaces label 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Option label "" is beneath 1 character", - }, - "message": "Option label "{sanitizedLabel}" is beneath 1 character", - "name": "UserInputError", - }, -] -`; - -exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with only white spaces value 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Option value "" is beneath 1 character", - }, - "message": "Option value "{sanitizedValue}" is beneath 1 character", - "name": "UserInputError", - }, -] -`; - -exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with too long defaultValue 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Default value should be as quoted string", - }, - "message": "Default value should be as quoted string", - "name": "UserInputError", - }, -] -`; - -exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with too long id 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Option id is invalid", - }, - "message": "Option id is invalid", - "name": "UserInputError", - }, -] -`; - -exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with too long label 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Option label exceeds 63 characters", - }, - "message": "Option label exceeds 63 characters", - "name": "UserInputError", - }, -] -`; - -exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with too long value 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Option value exceeds 63 characters", - }, - "message": "Option value exceeds 63 characters", - "name": "UserInputError", - }, -] -`; - -exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with undefined option label 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Option label is required", - }, - "message": "Option label is required", - "name": "UserInputError", - }, -] -`; - -exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with undefined option value 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Option value is required", - }, - "message": "Option value is required", - "name": "UserInputError", - }, -] -`; - -exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with undefined options 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "An error occurred.", - }, - "message": "Options are required for enum fields", - "name": "UserInputError", - }, -] -`; - -exports[`Failing create field metadata MULTI_SELECT tests suite Create should fail with unknown multi-select default values 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Default value "'UNKNOWN_OPTION'" must be one of the option values", - }, - "message": "Default value "{defaultValue}" must be one of the option values", - "name": "UserInputError", - }, -] -`; diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/enum/__snapshots__/failing-create-one-select-field-metadata-enum.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/field-metadata/enum/__snapshots__/failing-create-one-select-field-metadata-enum.integration-spec.ts.snap deleted file mode 100644 index 91adc73ff9..0000000000 --- a/packages/twenty-server/test/integration/metadata/suites/field-metadata/enum/__snapshots__/failing-create-one-select-field-metadata-enum.integration-spec.ts.snap +++ /dev/null @@ -1,462 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Failing create field metadata RATING tests suite Create should fail with an invalid default value 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Default value should be as quoted string", - }, - "message": "Default value should be as quoted string", - "name": "UserInputError", - }, -] -`; - -exports[`Failing create field metadata RATING tests suite Create should fail with an unknown default value 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Default value "'OPTION_424242'" must be one of the option values", - }, - "message": "Default value "{defaultValue}" must be one of the option values", - "name": "UserInputError", - }, -] -`; - -exports[`Failing create field metadata RATING tests suite Create should fail with comma in option label 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Label must not contain a comma", - }, - "message": "Label must not contain a comma", - "name": "UserInputError", - }, -] -`; - -exports[`Failing create field metadata RATING tests suite Create should fail with duplicated option ids 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Duplicated option id", - }, - "message": "Duplicated option {field}", - "name": "UserInputError", - }, -] -`; - -exports[`Failing create field metadata RATING tests suite Create should fail with duplicated option positions 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Duplicated option position", - }, - "message": "Duplicated option {field}", - "name": "UserInputError", - }, -] -`; - -exports[`Failing create field metadata RATING tests suite Create should fail with duplicated option values 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Duplicated option value", - }, - "message": "Duplicated option {field}", - "name": "UserInputError", - }, -] -`; - -exports[`Failing create field metadata RATING tests suite Create should fail with duplicated trimmed option values 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Duplicated option value", - }, - "message": "Duplicated option {field}", - "name": "UserInputError", - }, -] -`; - -exports[`Failing create field metadata RATING tests suite Create should fail with empty options 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "An error occurred.", - }, - "message": "Options are required for enum fields", - "name": "UserInputError", - }, -] -`; - -exports[`Failing create field metadata RATING tests suite Create should fail with empty string defaultValue 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Default value should be as quoted string", - }, - "message": "Default value should be as quoted string", - "name": "UserInputError", - }, -] -`; - -exports[`Failing create field metadata RATING tests suite Create should fail with empty string id 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Option id is invalid", - }, - "message": "Option id is invalid", - "name": "UserInputError", - }, -] -`; - -exports[`Failing create field metadata RATING tests suite Create should fail with empty string label 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Option label "" is beneath 1 character", - }, - "message": "Option label "{sanitizedLabel}" is beneath 1 character", - "name": "UserInputError", - }, -] -`; - -exports[`Failing create field metadata RATING tests suite Create should fail with empty string value 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Option value "" is beneath 1 character", - }, - "message": "Option value "{sanitizedValue}" is beneath 1 character", - "name": "UserInputError", - }, -] -`; - -exports[`Failing create field metadata RATING tests suite Create should fail with invalid option id 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Option id is invalid", - }, - "message": "Option id is invalid", - "name": "UserInputError", - }, -] -`; - -exports[`Failing create field metadata RATING tests suite Create should fail with invalid option value format 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Value must be in UPPER_CASE and follow snake_case "Option 1 and some other things, /"", - }, - "message": "Value must be in UPPER_CASE and follow snake_case "{sanitizedValue}"", - "name": "UserInputError", - }, -] -`; - -exports[`Failing create field metadata RATING tests suite Create should fail with not a string defaultValue 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "An error occurred.", - }, - "message": "Default value for multi-select must be a stringified array", - "name": "UserInputError", - }, -] -`; - -exports[`Failing create field metadata RATING tests suite Create should fail with not a string id 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Option id is invalid", - }, - "message": "Option id is invalid", - "name": "UserInputError", - }, -] -`; - -exports[`Failing create field metadata RATING tests suite Create should fail with not a string label 1`] = ` -[ - { - "extensions": { - "code": "INTERNAL_SERVER_ERROR", - "exceptionEventId": "mocked-exception-id", - "userFriendlyMessage": "An error occurred.", - }, - "message": "label.includes is not a function", - }, -] -`; - -exports[`Failing create field metadata RATING tests suite Create should fail with not a string value 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Value must be in UPPER_CASE and follow snake_case "22222"", - }, - "message": "Value must be in UPPER_CASE and follow snake_case "{sanitizedValue}"", - "name": "UserInputError", - }, -] -`; - -exports[`Failing create field metadata RATING tests suite Create should fail with null id 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Option id is required", - }, - "message": "Option id is required", - "name": "UserInputError", - }, -] -`; - -exports[`Failing create field metadata RATING tests suite Create should fail with null label 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Option label is required", - }, - "message": "Option label is required", - "name": "UserInputError", - }, -] -`; - -exports[`Failing create field metadata RATING tests suite Create should fail with null options 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "An error occurred.", - }, - "message": "Options are required for enum fields", - "name": "UserInputError", - }, -] -`; - -exports[`Failing create field metadata RATING tests suite Create should fail with null value 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Option value is required", - }, - "message": "Option value is required", - "name": "UserInputError", - }, -] -`; - -exports[`Failing create field metadata RATING tests suite Create should fail with only white spaces defaultValue 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Default value should be as quoted string", - }, - "message": "Default value should be as quoted string", - "name": "UserInputError", - }, -] -`; - -exports[`Failing create field metadata RATING tests suite Create should fail with only white spaces id 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Option id is invalid", - }, - "message": "Option id is invalid", - "name": "UserInputError", - }, -] -`; - -exports[`Failing create field metadata RATING tests suite Create should fail with only white spaces label 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Option label "" is beneath 1 character", - }, - "message": "Option label "{sanitizedLabel}" is beneath 1 character", - "name": "UserInputError", - }, -] -`; - -exports[`Failing create field metadata RATING tests suite Create should fail with only white spaces value 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Option value "" is beneath 1 character", - }, - "message": "Option value "{sanitizedValue}" is beneath 1 character", - "name": "UserInputError", - }, -] -`; - -exports[`Failing create field metadata RATING tests suite Create should fail with too long defaultValue 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Default value should be as quoted string", - }, - "message": "Default value should be as quoted string", - "name": "UserInputError", - }, -] -`; - -exports[`Failing create field metadata RATING tests suite Create should fail with too long id 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Option id is invalid", - }, - "message": "Option id is invalid", - "name": "UserInputError", - }, -] -`; - -exports[`Failing create field metadata RATING tests suite Create should fail with too long label 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Option label exceeds 63 characters", - }, - "message": "Option label exceeds 63 characters", - "name": "UserInputError", - }, -] -`; - -exports[`Failing create field metadata RATING tests suite Create should fail with too long value 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Option value exceeds 63 characters", - }, - "message": "Option value exceeds 63 characters", - "name": "UserInputError", - }, -] -`; - -exports[`Failing create field metadata RATING tests suite Create should fail with undefined option label 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Option label is required", - }, - "message": "Option label is required", - "name": "UserInputError", - }, -] -`; - -exports[`Failing create field metadata RATING tests suite Create should fail with undefined option value 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Option value is required", - }, - "message": "Option value is required", - "name": "UserInputError", - }, -] -`; - -exports[`Failing create field metadata RATING tests suite Create should fail with undefined options 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "An error occurred.", - }, - "message": "Options are required for enum fields", - "name": "UserInputError", - }, -] -`; diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/enum/__snapshots__/failing-update-one-enum-field-metadata.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/field-metadata/enum/__snapshots__/failing-update-one-enum-field-metadata.integration-spec.ts.snap deleted file mode 100644 index 15bfdbdce2..0000000000 --- a/packages/twenty-server/test/integration/metadata/suites/field-metadata/enum/__snapshots__/failing-update-one-enum-field-metadata.integration-spec.ts.snap +++ /dev/null @@ -1,951 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with an invalid default value 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "An error occurred.", - }, - "message": "Default value for multi-select must be an array", - "name": "UserInputError", - }, -] -`; - -exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with an unknown default value 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "An error occurred.", - }, - "message": "Default value for multi-select must be an array", - "name": "UserInputError", - }, -] -`; - -exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with comma in option label 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Label must not contain a comma", - }, - "message": "Label must not contain a comma", - "name": "UserInputError", - }, -] -`; - -exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with duplicated option ids 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Duplicated option id", - }, - "message": "Duplicated option {field}", - "name": "UserInputError", - }, -] -`; - -exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with duplicated option positions 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Duplicated option position", - }, - "message": "Duplicated option {field}", - "name": "UserInputError", - }, -] -`; - -exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with duplicated option values 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Duplicated option value", - }, - "message": "Duplicated option {field}", - "name": "UserInputError", - }, -] -`; - -exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with duplicated trimmed option values 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Duplicated option value", - }, - "message": "Duplicated option {field}", - "name": "UserInputError", - }, -] -`; - -exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with empty multi-select default values array 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "If defined default value must contain at least one value", - }, - "message": "If defined default value must contain at least one value", - "name": "UserInputError", - }, -] -`; - -exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with empty options 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "An error occurred.", - }, - "message": "Options are required for enum fields", - "name": "UserInputError", - }, -] -`; - -exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with empty string defaultValue 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Default value should be as quoted string", - }, - "message": "Default value should be as quoted string", - "name": "UserInputError", - }, -] -`; - -exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with empty string id 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Option id is invalid", - }, - "message": "Option id is invalid", - "name": "UserInputError", - }, -] -`; - -exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with empty string label 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Option label "" is beneath 1 character", - }, - "message": "Option label "{sanitizedLabel}" is beneath 1 character", - "name": "UserInputError", - }, -] -`; - -exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with empty string value 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Option value "" is beneath 1 character", - }, - "message": "Option value "{sanitizedValue}" is beneath 1 character", - "name": "UserInputError", - }, -] -`; - -exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with invalid multi-select default value format 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Default value should be as quoted string", - }, - "message": "Default value should be as quoted string", - "name": "UserInputError", - }, -] -`; - -exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with invalid option id 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Option id is invalid", - }, - "message": "Option id is invalid", - "name": "UserInputError", - }, -] -`; - -exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with invalid option value format 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Value must be in UPPER_CASE and follow snake_case "Option 1 and some other things, /"", - }, - "message": "Value must be in UPPER_CASE and follow snake_case "{sanitizedValue}"", - "name": "UserInputError", - }, -] -`; - -exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with non stringified array default value 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "An error occurred.", - }, - "message": "Default value for multi-select must be an array", - "name": "UserInputError", - }, -] -`; - -exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with not a string defaultValue 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "An error occurred.", - }, - "message": "Default value for multi-select must be a stringified array", - "name": "UserInputError", - }, -] -`; - -exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with not a string id 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Option id is invalid", - }, - "message": "Option id is invalid", - "name": "UserInputError", - }, -] -`; - -exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with not a string label 1`] = ` -[ - { - "extensions": { - "code": "INTERNAL_SERVER_ERROR", - "exceptionEventId": "mocked-exception-id", - "userFriendlyMessage": "An error occurred.", - }, - "message": "label.includes is not a function", - }, -] -`; - -exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with not a string value 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Value must be in UPPER_CASE and follow snake_case "22222"", - }, - "message": "Value must be in UPPER_CASE and follow snake_case "{sanitizedValue}"", - "name": "UserInputError", - }, -] -`; - -exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with null id 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Option id is required", - }, - "message": "Option id is required", - "name": "UserInputError", - }, -] -`; - -exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with null label 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Option label is required", - }, - "message": "Option label is required", - "name": "UserInputError", - }, -] -`; - -exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with null value 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Option value is required", - }, - "message": "Option value is required", - "name": "UserInputError", - }, -] -`; - -exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with only white spaces defaultValue 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Default value should be as quoted string", - }, - "message": "Default value should be as quoted string", - "name": "UserInputError", - }, -] -`; - -exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with only white spaces id 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Option id is invalid", - }, - "message": "Option id is invalid", - "name": "UserInputError", - }, -] -`; - -exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with only white spaces label 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Option label "" is beneath 1 character", - }, - "message": "Option label "{sanitizedLabel}" is beneath 1 character", - "name": "UserInputError", - }, -] -`; - -exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with only white spaces value 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Option value "" is beneath 1 character", - }, - "message": "Option value "{sanitizedValue}" is beneath 1 character", - "name": "UserInputError", - }, -] -`; - -exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with too long defaultValue 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Default value should be as quoted string", - }, - "message": "Default value should be as quoted string", - "name": "UserInputError", - }, -] -`; - -exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with too long id 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Option id is invalid", - }, - "message": "Option id is invalid", - "name": "UserInputError", - }, -] -`; - -exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with too long label 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Option label exceeds 63 characters", - }, - "message": "Option label exceeds 63 characters", - "name": "UserInputError", - }, -] -`; - -exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with too long value 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Option value exceeds 63 characters", - }, - "message": "Option value exceeds 63 characters", - "name": "UserInputError", - }, -] -`; - -exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with undefined option label 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Option label is required", - }, - "message": "Option label is required", - "name": "UserInputError", - }, -] -`; - -exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with undefined option value 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Option value is required", - }, - "message": "Option value is required", - "name": "UserInputError", - }, -] -`; - -exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with unknown default value and no options 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Default value "'OPTION_42'" must be one of the option values", - }, - "message": "Default value "{defaultValue}" must be one of the option values", - "name": "UserInputError", - }, -] -`; - -exports[`Failing update field metadata MULTI_SELECT tests suite Update should fail with unknown multi-select default values 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Default value "'UNKNOWN_OPTION'" must be one of the option values", - }, - "message": "Default value "{defaultValue}" must be one of the option values", - "name": "UserInputError", - }, -] -`; - -exports[`Failing update field metadata SELECT tests suite Update should fail with an invalid default value 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Default value should be as quoted string", - }, - "message": "Default value should be as quoted string", - "name": "UserInputError", - }, -] -`; - -exports[`Failing update field metadata SELECT tests suite Update should fail with an unknown default value 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Default value "'OPTION_424242'" must be one of the option values", - }, - "message": "Default value "{defaultValue}" must be one of the option values", - "name": "UserInputError", - }, -] -`; - -exports[`Failing update field metadata SELECT tests suite Update should fail with comma in option label 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Label must not contain a comma", - }, - "message": "Label must not contain a comma", - "name": "UserInputError", - }, -] -`; - -exports[`Failing update field metadata SELECT tests suite Update should fail with duplicated option ids 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Duplicated option id", - }, - "message": "Duplicated option {field}", - "name": "UserInputError", - }, -] -`; - -exports[`Failing update field metadata SELECT tests suite Update should fail with duplicated option positions 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Duplicated option position", - }, - "message": "Duplicated option {field}", - "name": "UserInputError", - }, -] -`; - -exports[`Failing update field metadata SELECT tests suite Update should fail with duplicated option values 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Duplicated option value", - }, - "message": "Duplicated option {field}", - "name": "UserInputError", - }, -] -`; - -exports[`Failing update field metadata SELECT tests suite Update should fail with duplicated trimmed option values 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Duplicated option value", - }, - "message": "Duplicated option {field}", - "name": "UserInputError", - }, -] -`; - -exports[`Failing update field metadata SELECT tests suite Update should fail with empty options 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "An error occurred.", - }, - "message": "Options are required for enum fields", - "name": "UserInputError", - }, -] -`; - -exports[`Failing update field metadata SELECT tests suite Update should fail with empty string defaultValue 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Default value should be as quoted string", - }, - "message": "Default value should be as quoted string", - "name": "UserInputError", - }, -] -`; - -exports[`Failing update field metadata SELECT tests suite Update should fail with empty string id 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Option id is invalid", - }, - "message": "Option id is invalid", - "name": "UserInputError", - }, -] -`; - -exports[`Failing update field metadata SELECT tests suite Update should fail with empty string label 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Option label "" is beneath 1 character", - }, - "message": "Option label "{sanitizedLabel}" is beneath 1 character", - "name": "UserInputError", - }, -] -`; - -exports[`Failing update field metadata SELECT tests suite Update should fail with empty string value 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Option value "" is beneath 1 character", - }, - "message": "Option value "{sanitizedValue}" is beneath 1 character", - "name": "UserInputError", - }, -] -`; - -exports[`Failing update field metadata SELECT tests suite Update should fail with invalid option id 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Option id is invalid", - }, - "message": "Option id is invalid", - "name": "UserInputError", - }, -] -`; - -exports[`Failing update field metadata SELECT tests suite Update should fail with invalid option value format 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Value must be in UPPER_CASE and follow snake_case "Option 1 and some other things, /"", - }, - "message": "Value must be in UPPER_CASE and follow snake_case "{sanitizedValue}"", - "name": "UserInputError", - }, -] -`; - -exports[`Failing update field metadata SELECT tests suite Update should fail with not a string defaultValue 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "An error occurred.", - }, - "message": "Default value for multi-select must be a stringified array", - "name": "UserInputError", - }, -] -`; - -exports[`Failing update field metadata SELECT tests suite Update should fail with not a string id 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Option id is invalid", - }, - "message": "Option id is invalid", - "name": "UserInputError", - }, -] -`; - -exports[`Failing update field metadata SELECT tests suite Update should fail with not a string label 1`] = ` -[ - { - "extensions": { - "code": "INTERNAL_SERVER_ERROR", - "exceptionEventId": "mocked-exception-id", - "userFriendlyMessage": "An error occurred.", - }, - "message": "label.includes is not a function", - }, -] -`; - -exports[`Failing update field metadata SELECT tests suite Update should fail with not a string value 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Value must be in UPPER_CASE and follow snake_case "22222"", - }, - "message": "Value must be in UPPER_CASE and follow snake_case "{sanitizedValue}"", - "name": "UserInputError", - }, -] -`; - -exports[`Failing update field metadata SELECT tests suite Update should fail with null id 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Option id is required", - }, - "message": "Option id is required", - "name": "UserInputError", - }, -] -`; - -exports[`Failing update field metadata SELECT tests suite Update should fail with null label 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Option label is required", - }, - "message": "Option label is required", - "name": "UserInputError", - }, -] -`; - -exports[`Failing update field metadata SELECT tests suite Update should fail with null value 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Option value is required", - }, - "message": "Option value is required", - "name": "UserInputError", - }, -] -`; - -exports[`Failing update field metadata SELECT tests suite Update should fail with only white spaces defaultValue 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Default value should be as quoted string", - }, - "message": "Default value should be as quoted string", - "name": "UserInputError", - }, -] -`; - -exports[`Failing update field metadata SELECT tests suite Update should fail with only white spaces id 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Option id is invalid", - }, - "message": "Option id is invalid", - "name": "UserInputError", - }, -] -`; - -exports[`Failing update field metadata SELECT tests suite Update should fail with only white spaces label 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Option label "" is beneath 1 character", - }, - "message": "Option label "{sanitizedLabel}" is beneath 1 character", - "name": "UserInputError", - }, -] -`; - -exports[`Failing update field metadata SELECT tests suite Update should fail with only white spaces value 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Option value "" is beneath 1 character", - }, - "message": "Option value "{sanitizedValue}" is beneath 1 character", - "name": "UserInputError", - }, -] -`; - -exports[`Failing update field metadata SELECT tests suite Update should fail with too long defaultValue 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Default value should be as quoted string", - }, - "message": "Default value should be as quoted string", - "name": "UserInputError", - }, -] -`; - -exports[`Failing update field metadata SELECT tests suite Update should fail with too long id 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Option id is invalid", - }, - "message": "Option id is invalid", - "name": "UserInputError", - }, -] -`; - -exports[`Failing update field metadata SELECT tests suite Update should fail with too long label 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Option label exceeds 63 characters", - }, - "message": "Option label exceeds 63 characters", - "name": "UserInputError", - }, -] -`; - -exports[`Failing update field metadata SELECT tests suite Update should fail with too long value 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Option value exceeds 63 characters", - }, - "message": "Option value exceeds 63 characters", - "name": "UserInputError", - }, -] -`; - -exports[`Failing update field metadata SELECT tests suite Update should fail with undefined option label 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Option label is required", - }, - "message": "Option label is required", - "name": "UserInputError", - }, -] -`; - -exports[`Failing update field metadata SELECT tests suite Update should fail with undefined option value 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Option value is required", - }, - "message": "Option value is required", - "name": "UserInputError", - }, -] -`; - -exports[`Failing update field metadata SELECT tests suite Update should fail with unknown default value and no options 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "INVALID_FIELD_INPUT", - "userFriendlyMessage": "Default value "'OPTION_42'" must be one of the option values", - }, - "message": "Default value "{defaultValue}" must be one of the option values", - "name": "UserInputError", - }, -] -`; diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/enum/failing-create-one-multi-select-field-metadata-enum.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/field-metadata/enum/failing-create-one-multi-select-field-metadata-enum.integration-spec.ts deleted file mode 100644 index f27f810c81..0000000000 --- a/packages/twenty-server/test/integration/metadata/suites/field-metadata/enum/failing-create-one-multi-select-field-metadata-enum.integration-spec.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { CREATE_ENUM_FIELD_METADATA_TEST_CASES } from 'test/integration/metadata/suites/field-metadata/enum/common/create-enum-field-metadata-test-cases'; -import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util'; -import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util'; -import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util'; -import { updateFeatureFlag } from 'test/integration/metadata/suites/utils/update-feature-flag.util'; -import { FieldMetadataType } from 'twenty-shared/types'; -import { isDefined } from 'twenty-shared/utils'; - -import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; - -describe('Failing create field metadata MULTI_SELECT tests suite', () => { - let createdObjectMetadataId: string; - const testCases = - CREATE_ENUM_FIELD_METADATA_TEST_CASES[FieldMetadataType.MULTI_SELECT]; - - if (!isDefined(testCases)) { - return; - } - const { failing: failingTestCases } = testCases; - - beforeAll(async () => { - await updateFeatureFlag({ - expectToFail: false, - featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED, - value: false, - }); - }); - - beforeAll(async () => { - const { data } = await createOneObjectMetadata({ - expectToFail: false, - input: { - labelSingular: 'Enum testing v1', - labelPlural: 'Enums testings v1', - nameSingular: 'enumTestingV1', - namePlural: 'enumsTestingsV1', - icon: 'IconBuildingSkyscraper', - isLabelSyncedWithName: false, - }, - }); - - createdObjectMetadataId = data.createOneObject.id; - }); - - afterAll(async () => { - await deleteOneObjectMetadata({ - expectToFail: false, - input: { idToDelete: createdObjectMetadataId }, - }); - }); - - afterAll(async () => { - await updateFeatureFlag({ - expectToFail: false, - featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED, - value: true, - }); - }); - - test.each(failingTestCases)( - 'Create $title', - async ({ context: { input } }) => { - const { data, errors } = await createOneFieldMetadata({ - expectToFail: true, - input: { - objectMetadataId: createdObjectMetadataId, - type: FieldMetadataType.MULTI_SELECT, - name: 'testField', - label: 'Test Field', - isLabelSyncedWithName: false, - ...input, - }, - gqlFields: ` - id - options - defaultValue - `, - }); - - expect(data).toBeNull(); - expect(errors).toBeDefined(); - expect(errors).toMatchSnapshot(); - }, - ); -}); diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/enum/failing-create-one-select-field-metadata-enum.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/field-metadata/enum/failing-create-one-select-field-metadata-enum.integration-spec.ts deleted file mode 100644 index 655904b245..0000000000 --- a/packages/twenty-server/test/integration/metadata/suites/field-metadata/enum/failing-create-one-select-field-metadata-enum.integration-spec.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { CREATE_ENUM_FIELD_METADATA_TEST_CASES } from 'test/integration/metadata/suites/field-metadata/enum/common/create-enum-field-metadata-test-cases'; -import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util'; -import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util'; -import { updateFeatureFlag } from 'test/integration/metadata/suites/utils/update-feature-flag.util'; -import { isDefined } from 'twenty-shared/utils'; -import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util'; -import { FieldMetadataType } from 'twenty-shared/types'; - -import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; - -describe('Failing create field metadata RATING tests suite', () => { - let createdObjectMetadataId: string; - const testCases = - CREATE_ENUM_FIELD_METADATA_TEST_CASES[FieldMetadataType.SELECT]; - - if (!isDefined(testCases)) { - return; - } - const { failing: failingTestCases } = testCases; - - beforeAll(async () => { - await updateFeatureFlag({ - expectToFail: false, - featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED, - value: false, - }); - }); - - beforeAll(async () => { - const { data } = await createOneObjectMetadata({ - expectToFail: false, - input: { - labelSingular: 'Enum testing v1', - labelPlural: 'Enums testings v1', - nameSingular: 'enumTestingV1', - namePlural: 'enumsTestingsV1', - icon: 'IconBuildingSkyscraper', - isLabelSyncedWithName: false, - }, - }); - - createdObjectMetadataId = data.createOneObject.id; - }); - - afterAll(async () => { - await deleteOneObjectMetadata({ - expectToFail: false, - input: { idToDelete: createdObjectMetadataId }, - }); - }); - - afterAll(async () => { - await updateFeatureFlag({ - expectToFail: false, - featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED, - value: true, - }); - }); - - test.each(failingTestCases)( - 'Create $title', - async ({ context: { input } }) => { - const { data, errors } = await createOneFieldMetadata({ - expectToFail: true, - input: { - objectMetadataId: createdObjectMetadataId, - type: FieldMetadataType.SELECT, - name: 'testField', - label: 'Test Field', - isLabelSyncedWithName: false, - ...input, - }, - gqlFields: ` - id - options - defaultValue - `, - }); - - expect(data).toBeNull(); - expect(errors).toBeDefined(); - expect(errors).toMatchSnapshot(); - }, - ); -}); diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/enum/failing-update-one-enum-field-metadata.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/field-metadata/enum/failing-update-one-enum-field-metadata.integration-spec.ts deleted file mode 100644 index f0498c349a..0000000000 --- a/packages/twenty-server/test/integration/metadata/suites/field-metadata/enum/failing-update-one-enum-field-metadata.integration-spec.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { UPDATE_ENUM_FIELD_METADATA_TEST_CASES } from 'test/integration/metadata/suites/field-metadata/enum/common/update-enum-field-metadata-test-cases'; -import { type CreateOneFieldFactoryInput } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata-query-factory.util'; -import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util'; -import { updateOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/update-one-field-metadata.util'; -import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util'; -import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util'; -import { updateFeatureFlag } from 'test/integration/metadata/suites/utils/update-feature-flag.util'; -import { isDefined } from 'twenty-shared/utils'; - -import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; -import { fieldMetadataEnumTypes } from 'src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util'; - -describe.each(fieldMetadataEnumTypes)( - 'Failing update field metadata %s tests suite', - (testedFieldMetadataType) => { - let createdObjectMetadataId: string; - let createdFieldMetadataId: string; - const testCases = - UPDATE_ENUM_FIELD_METADATA_TEST_CASES[testedFieldMetadataType]; - - if (!isDefined(testCases)) { - return; - } - const { failing: failingTestCases } = testCases; - - beforeAll(async () => { - await updateFeatureFlag({ - expectToFail: false, - featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED, - value: false, - }); - }); - - const initialOptions: CreateOneFieldFactoryInput['options'] = [ - { - label: 'Option 1', - value: 'OPTION_1', - color: 'green', - position: 1, - }, - { - label: 'Option 2', - value: 'OPTION_2', - color: 'green', - position: 2, - }, - ]; - - beforeAll(async () => { - const { data } = await createOneObjectMetadata({ - expectToFail: false, - input: { - labelSingular: 'failingUpdateEnumField', - labelPlural: 'failingUpdateEnumFields', - nameSingular: 'failingUpdateEnumField', - namePlural: 'failingUpdateEnumFields', - icon: 'IconBuildingSkyscraper', - isLabelSyncedWithName: false, - }, - }); - - createdObjectMetadataId = data.createOneObject.id; - - const { - data: { createOneField }, - } = await createOneFieldMetadata({ - input: { - objectMetadataId: createdObjectMetadataId, - type: testedFieldMetadataType, - name: 'testField', - label: 'Test Field', - isLabelSyncedWithName: false, - options: initialOptions, - }, - gqlFields: ` - id - type - `, - }); - - createdFieldMetadataId = createOneField.id; - }); - - afterAll(async () => { - await deleteOneObjectMetadata({ - input: { - idToDelete: createdObjectMetadataId, - }, - expectToFail: false, - }); - }); - - afterAll(async () => { - await updateFeatureFlag({ - expectToFail: false, - featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED, - value: true, - }); - }); - - test.each(failingTestCases)( - 'Update $title', - async ({ context: { input } }) => { - const { ...updatePayload } = input; - - const { data, errors } = await updateOneFieldMetadata({ - expectToFail: true, - input: { - idToUpdate: createdFieldMetadataId, - updatePayload, - }, - gqlFields: ` - id - options - `, - }); - - expect(data).toBeNull(); - expect(errors).toBeDefined(); - expect(errors).toMatchSnapshot(); - }, - ); - }, -); diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/enum/successful-create-one-field-metadata-enum.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/field-metadata/enum/successful-create-one-field-metadata-enum.integration-spec.ts deleted file mode 100644 index 665a5a268b..0000000000 --- a/packages/twenty-server/test/integration/metadata/suites/field-metadata/enum/successful-create-one-field-metadata-enum.integration-spec.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { CREATE_ENUM_FIELD_METADATA_TEST_CASES } from 'test/integration/metadata/suites/field-metadata/enum/common/create-enum-field-metadata-test-cases'; -import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util'; -import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util'; -import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util'; -import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util'; -import { updateFeatureFlag } from 'test/integration/metadata/suites/utils/update-feature-flag.util'; -import { isDefined } from 'twenty-shared/utils'; - -import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; -import { fieldMetadataEnumTypes } from 'src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util'; - -describe.each(fieldMetadataEnumTypes)( - 'Successful create field metadata %s tests suite', - (testedFieldMetadataType) => { - let createdObjectMetadataId: string; - const testCases = - CREATE_ENUM_FIELD_METADATA_TEST_CASES[testedFieldMetadataType]; - - if (!isDefined(testCases)) { - return; - } - const { successful: successfulTestCases } = testCases; - - beforeAll(async () => { - await updateFeatureFlag({ - expectToFail: false, - featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED, - value: false, - }); - }); - - afterAll(async () => { - await updateFeatureFlag({ - expectToFail: false, - featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED, - value: true, - }); - }); - - beforeEach(async () => { - const { data } = await createOneObjectMetadata({ - expectToFail: false, - input: { - labelSingular: 'successCreateEnumField', - labelPlural: 'successCreateEnumFields', - nameSingular: 'successCreateEnumField', - namePlural: 'successCreateEnumFields', - icon: 'IconBuildingSkyscraper', - isLabelSyncedWithName: false, - }, - }); - - createdObjectMetadataId = data.createOneObject.id; - }); - - afterEach(async () => { - await updateOneObjectMetadata({ - input: { - idToUpdate: createdObjectMetadataId, - updatePayload: { - isActive: false, - }, - }, - expectToFail: false, - }); - await deleteOneObjectMetadata({ - expectToFail: false, - input: { idToDelete: createdObjectMetadataId }, - }); - }); - - test.each(successfulTestCases)( - 'Create $title', - async ({ context: { input, expectedOptions } }) => { - const { data, errors } = await createOneFieldMetadata< - typeof testedFieldMetadataType - >({ - input: { - objectMetadataId: createdObjectMetadataId, - type: testedFieldMetadataType, - name: 'testField', - label: 'Test Field', - isLabelSyncedWithName: false, - ...input, - }, - gqlFields: ` - id - options - defaultValue - type - `, - }); - - expect(data).not.toBeNull(); - expect(data.createOneField).toBeDefined(); - expect(data.createOneField.type).toEqual(testedFieldMetadataType); - - const createdOptions = data.createOneField.options; - const optionsToCompare = expectedOptions ?? input.options ?? []; - - expect(errors).toBeUndefined(); - expect(createdOptions?.length).toBe(optionsToCompare.length); - createdOptions?.forEach((option) => expect(option.id).toBeDefined()); - expect(createdOptions).toMatchObject(optionsToCompare); - - if (isDefined(input.defaultValue)) { - expect(data.createOneField.defaultValue).toEqual(input.defaultValue); - } - }, - ); - }, -); diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/enum/successful-update-one-enum-field-metadata.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/field-metadata/enum/successful-update-one-enum-field-metadata.integration-spec.ts deleted file mode 100644 index 50d7b0a994..0000000000 --- a/packages/twenty-server/test/integration/metadata/suites/field-metadata/enum/successful-update-one-enum-field-metadata.integration-spec.ts +++ /dev/null @@ -1,204 +0,0 @@ -import { UPDATE_ENUM_FIELD_METADATA_TEST_CASES } from 'test/integration/metadata/suites/field-metadata/enum/common/update-enum-field-metadata-test-cases'; -import { type CreateOneFieldFactoryInput } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata-query-factory.util'; -import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util'; -import { updateOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/update-one-field-metadata.util'; -import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util'; -import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util'; -import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util'; -import { updateFeatureFlag } from 'test/integration/metadata/suites/utils/update-feature-flag.util'; -import { FieldMetadataType } from 'twenty-shared/types'; -import { isDefined } from 'twenty-shared/utils'; - -import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; -import { - type FieldMetadataComplexOption, - type FieldMetadataDefaultOption, -} from 'src/engine/metadata-modules/field-metadata/dtos/options.input'; -import { fieldMetadataEnumTypes } from 'src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util'; - -describe.each(fieldMetadataEnumTypes)( - 'Successful update field metadata %s tests suite', - (testedFieldMetadataType) => { - let createdObjectMetadataId: string; - let createdFieldMetadataId: string; - const testCases = - UPDATE_ENUM_FIELD_METADATA_TEST_CASES[testedFieldMetadataType]; - - if (!isDefined(testCases)) { - return; - } - const { successful: successfulTestCases } = testCases; - - beforeAll(async () => { - await updateFeatureFlag({ - expectToFail: false, - featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED, - value: false, - }); - }); - - afterAll(async () => { - await updateFeatureFlag({ - expectToFail: false, - featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED, - value: true, - }); - }); - - const initialOptions: CreateOneFieldFactoryInput['options'] = [ - { - label: 'Option 1', - value: 'OPTION_1', - color: 'green', - position: 1, - }, - { - label: 'Option 2', - value: 'OPTION_2', - color: 'green', - position: 2, - }, - ]; - - beforeEach(async () => { - const { data } = await createOneObjectMetadata({ - expectToFail: false, - input: { - labelSingular: 'successUpdateEnumField', - labelPlural: 'successUpdateEnumFields', - nameSingular: 'successUpdateEnumField', - namePlural: 'successUpdateEnumFields', - icon: 'IconBuildingSkyscraper', - isLabelSyncedWithName: false, - }, - }); - - createdObjectMetadataId = data.createOneObject.id; - - const { - data: { createOneField }, - } = await createOneFieldMetadata({ - expectToFail: false, - input: { - objectMetadataId: createdObjectMetadataId, - type: testedFieldMetadataType, - name: 'testField', - label: 'Test Field', - isLabelSyncedWithName: false, - options: initialOptions, - }, - gqlFields: ` - id - type - `, - }); - - createdFieldMetadataId = createOneField.id; - }); - - afterEach(async () => { - await updateOneObjectMetadata({ - expectToFail: false, - input: { - idToUpdate: createdObjectMetadataId, - updatePayload: { - isActive: false, - }, - }, - }); - await deleteOneObjectMetadata({ - expectToFail: false, - input: { idToDelete: createdObjectMetadataId }, - }); - }); - - it('Should update default value to null even if it was set before', async () => { - const isMultiSelect = - testedFieldMetadataType === FieldMetadataType.MULTI_SELECT; - const rawDefaultValue = `'${initialOptions[0].value}'`; - const expectedDefaultValue = isMultiSelect - ? [rawDefaultValue] - : rawDefaultValue; - - const { data: firstUpdate } = await updateOneFieldMetadata({ - input: { - idToUpdate: createdFieldMetadataId, - updatePayload: { - defaultValue: expectedDefaultValue, - }, - }, - gqlFields: ` - id - defaultValue - `, - }); - - expect(firstUpdate.updateOneField.defaultValue).toEqual( - expectedDefaultValue, - ); - - const updatedOptions = initialOptions.slice(1); - const { data: secondUpdate, errors } = await updateOneFieldMetadata({ - input: { - idToUpdate: createdFieldMetadataId, - updatePayload: { - defaultValue: null, - options: updatedOptions, - }, - }, - gqlFields: ` - id - options - defaultValue - `, - }); - - expect(errors).toBeUndefined(); - expect(secondUpdate.updateOneField.defaultValue).toBeNull(); - expect(secondUpdate.updateOneField.options).toMatchObject(updatedOptions); - }); - - test.each(successfulTestCases)( - 'Update $title', - async ({ context: { input, expectedOptions } }) => { - const { ...updatePayload } = input; - - const { data, errors } = await updateOneFieldMetadata({ - input: { - idToUpdate: createdFieldMetadataId, - updatePayload, - }, - gqlFields: ` - id - options - defaultValue - `, - }); - - expect(data.updateOneField).toBeDefined(); - const updatedOptions: - | FieldMetadataComplexOption[] - | FieldMetadataDefaultOption[] - | null = data.updateOneField.options; - - expect(updatedOptions).toBeDefined(); - if (!isDefined(updatedOptions)) - throw new Error( - 'Should never occur, type invariant post test assertion', - ); - - expect(errors).toBeUndefined(); - updatedOptions.forEach((option) => expect(option.id).toBeDefined()); - - const optionsToCompare = expectedOptions ?? input.options ?? []; - - expect(updatedOptions.length).toBe(optionsToCompare.length); - expect(updatedOptions).toMatchObject(optionsToCompare); - - if (isDefined(input.defaultValue)) { - expect(data.updateOneField.defaultValue).toEqual(input.defaultValue); - } - }, - ); - }, -); diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/__snapshots__/failing-field-metadata-morph-relation-creation.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/__snapshots__/failing-field-metadata-morph-relation-creation.integration-spec.ts.snap deleted file mode 100644 index 64d2b58dff..0000000000 --- a/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/__snapshots__/failing-field-metadata-morph-relation-creation.integration-spec.ts.snap +++ /dev/null @@ -1,191 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Field metadata morph relation creation should fail relation MANY_TO_ONE when targetFieldLabel conflicts with an existing field on target object metadata id 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "Name not available", - "userFriendlyMessage": "This name is not available as it is already used by another field.", - }, - "message": "Name "collisionfieldlabel" is not available as it is already used by another field", - "name": "UserInputError", - }, -] -`; - -exports[`Field metadata morph relation creation should fail relation MANY_TO_ONE when targetFieldLabel contains only whitespace 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "Invalid label", - "userFriendlyMessage": "An error occurred.", - }, - "message": "Invalid label: " "", - "name": "UserInputError", - }, -] -`; - -exports[`Field metadata morph relation creation should fail relation MANY_TO_ONE when targetFieldLabel exceeds maximum length 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "Exceeds max length", - "userFriendlyMessage": "An error occurred.", - }, - "message": "Name is too long: it exceeds the 63 characters limit.", - "name": "UserInputError", - }, -] -`; - -exports[`Field metadata morph relation creation should fail relation MANY_TO_ONE when targetFieldLabel is empty 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "Input too short", - "userFriendlyMessage": "An error occurred.", - }, - "message": "Input is too short: """, - "name": "UserInputError", - }, -] -`; - -exports[`Field metadata morph relation creation should fail relation MANY_TO_ONE when targetObjectMetadataId is unknown 1`] = ` -[ - { - "extensions": { - "code": "INTERNAL_SERVER_ERROR", - "exceptionEventId": "mocked-exception-id", - "userFriendlyMessage": "An error occurred.", - }, - "message": "Target object metadata does not exist in the object metadata maps", - }, -] -`; - -exports[`Field metadata morph relation creation should fail relation MANY_TO_ONE when type is a wrong value 1`] = ` -[ - { - "extensions": { - "code": "INTERNAL_SERVER_ERROR", - "exceptionEventId": "mocked-exception-id", - "userFriendlyMessage": "An error occurred.", - }, - "message": "Invalid relation type (wrong) for field owner on firstTargetRelation", - }, -] -`; - -exports[`Field metadata morph relation creation should fail relation MANY_TO_ONE when type is not provided 1`] = ` -[ - { - "extensions": { - "code": "INTERNAL_SERVER_ERROR", - "exceptionEventId": "mocked-exception-id", - "userFriendlyMessage": "An error occurred.", - }, - "message": "Invalid relation type (undefined) for field owner on firstTargetRelation", - }, -] -`; - -exports[`Field metadata morph relation creation should fail relation ONE_TO_MANY when targetFieldLabel conflicts with an existing field on target object metadata id 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "Name not available", - "userFriendlyMessage": "This name is not available as it is already used by another field.", - }, - "message": "Name "collisionfieldlabel" is not available as it is already used by another field", - "name": "UserInputError", - }, -] -`; - -exports[`Field metadata morph relation creation should fail relation ONE_TO_MANY when targetFieldLabel contains only whitespace 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "Invalid label", - "userFriendlyMessage": "An error occurred.", - }, - "message": "Invalid label: " "", - "name": "UserInputError", - }, -] -`; - -exports[`Field metadata morph relation creation should fail relation ONE_TO_MANY when targetFieldLabel exceeds maximum length 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "Exceeds max length", - "userFriendlyMessage": "An error occurred.", - }, - "message": "Name is too long: it exceeds the 63 characters limit.", - "name": "UserInputError", - }, -] -`; - -exports[`Field metadata morph relation creation should fail relation ONE_TO_MANY when targetFieldLabel is empty 1`] = ` -[ - { - "extensions": { - "code": "BAD_USER_INPUT", - "subCode": "Input too short", - "userFriendlyMessage": "An error occurred.", - }, - "message": "Input is too short: """, - "name": "UserInputError", - }, -] -`; - -exports[`Field metadata morph relation creation should fail relation ONE_TO_MANY when targetObjectMetadataId is unknown 1`] = ` -[ - { - "extensions": { - "code": "INTERNAL_SERVER_ERROR", - "exceptionEventId": "mocked-exception-id", - "userFriendlyMessage": "An error occurred.", - }, - "message": "Target object metadata does not exist in the object metadata maps", - }, -] -`; - -exports[`Field metadata morph relation creation should fail relation ONE_TO_MANY when type is a wrong value 1`] = ` -[ - { - "extensions": { - "code": "INTERNAL_SERVER_ERROR", - "exceptionEventId": "mocked-exception-id", - "userFriendlyMessage": "An error occurred.", - }, - "message": "Invalid relation type (wrong) for field owner on firstTargetRelation", - }, -] -`; - -exports[`Field metadata morph relation creation should fail relation ONE_TO_MANY when type is not provided 1`] = ` -[ - { - "extensions": { - "code": "INTERNAL_SERVER_ERROR", - "exceptionEventId": "mocked-exception-id", - "userFriendlyMessage": "An error occurred.", - }, - "message": "Invalid relation type (undefined) for field owner on firstTargetRelation", - }, -] -`; diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/failing-field-metadata-morph-relation-creation.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/failing-field-metadata-morph-relation-creation.integration-spec.ts deleted file mode 100644 index 0aa4bb7f7a..0000000000 --- a/packages/twenty-server/test/integration/metadata/suites/field-metadata/morph-relation/failing-field-metadata-morph-relation-creation.integration-spec.ts +++ /dev/null @@ -1,247 +0,0 @@ -import { faker } from '@faker-js/faker/.'; -import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util'; -import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util'; -import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util'; -import { getMockCreateObjectInput } from 'test/integration/metadata/suites/object-metadata/utils/generate-mock-create-object-metadata-input'; -import { type EachTestingContext } from 'twenty-shared/testing'; -import { FieldMetadataType } from 'twenty-shared/types'; -import { updateFeatureFlag } from 'test/integration/metadata/suites/utils/update-feature-flag.util'; - -import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; - -import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; -import { type CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input'; - -type GlobalTestContext = { - objectMetadataIds: { - firstTargetObjectId: string; - secondTargetObjectId: string; - sourceObjectId: string; - }; - targetFieldLabel: string; - type: FieldMetadataType; - targetFieldIcon: string; - collisionFieldLabel: string; -}; -const globalTestContext: GlobalTestContext = { - objectMetadataIds: { - firstTargetObjectId: '', - secondTargetObjectId: '', - sourceObjectId: '', - }, - targetFieldLabel: 'defaultTargetFieldLabel', - type: FieldMetadataType.MORPH_RELATION, - targetFieldIcon: 'IconBuildingSkyscraper', - collisionFieldLabel: 'collisionfieldlabel', -}; - -type TestedRelationCreationPayload = Partial< - NonNullable ->; - -type CreateOneObjectMetadataItemTestingContext = EachTestingContext< - | TestedRelationCreationPayload - | ((context: GlobalTestContext) => TestedRelationCreationPayload) ->[]; -describe('Field metadata morph relation creation should fail', () => { - beforeAll(async () => { - await updateFeatureFlag({ - expectToFail: false, - featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED, - value: false, - }); - }); - - const failingLabelsCreationTestsUseCase: CreateOneObjectMetadataItemTestingContext = - [ - { - title: 'when targetFieldLabel is empty', - context: { targetFieldLabel: '' }, - }, - { - title: 'when targetFieldLabel exceeds maximum length', - context: { - targetFieldLabel: 'A'.repeat(64), - }, - }, - { - // Not handled gracefully should be refactored - title: 'when targetObjectMetadataId is unknown', - context: { - targetObjectMetadataId: faker.string.uuid(), - }, - }, - { - title: 'when targetFieldLabel contains only whitespace', - context: { targetFieldLabel: ' ' }, - }, - { - title: - 'when targetFieldLabel conflicts with an existing field on target object metadata id', - context: ({ collisionFieldLabel }) => ({ - targetFieldLabel: collisionFieldLabel, - }), - }, - { - title: 'when type is not provided', - context: { type: undefined }, - }, - { - title: 'when type is a wrong value', - context: { type: 'wrong' as RelationType }, - }, - ]; - - beforeAll(async () => { - const { - data: { - createOneObject: { id: sourceObjectId }, - }, - } = await createOneObjectMetadata({ - input: getMockCreateObjectInput({ - namePlural: 'sourceRelations', - nameSingular: 'sourceRelation', - }), - }); - - const { - data: { - createOneObject: { id: firstTargetObjectId }, - }, - } = await createOneObjectMetadata({ - input: getMockCreateObjectInput({ - namePlural: 'firstTargetRelations', - nameSingular: 'firstTargetRelation', - }), - }); - - const { - data: { - createOneObject: { id: secondTargetObjectId }, - }, - } = await createOneObjectMetadata({ - input: getMockCreateObjectInput({ - namePlural: 'secondTargetRelations', - nameSingular: 'secondTargetRelation', - }), - }); - - globalTestContext.objectMetadataIds = { - sourceObjectId, - firstTargetObjectId, - secondTargetObjectId, - }; - - const { data } = await createOneFieldMetadata({ - input: { - objectMetadataId: firstTargetObjectId, - name: globalTestContext.collisionFieldLabel, - label: 'LabelThatCouldBeAnything', - isLabelSyncedWithName: false, - type: FieldMetadataType.TEXT, - }, - }); - - expect(data).toBeDefined(); - }); - - afterAll(async () => { - for (const objectMetadataId of Object.values( - globalTestContext.objectMetadataIds, - )) { - await deleteOneObjectMetadata({ - input: { - idToDelete: objectMetadataId, - }, - }); - } - }); - - afterAll(async () => { - await updateFeatureFlag({ - expectToFail: false, - featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED, - value: true, - }); - }); - - it.each(failingLabelsCreationTestsUseCase)( - 'relation ONE_TO_MANY $title', - async ({ context }) => { - const computedContext = - typeof context === 'function' ? context(globalTestContext) : context; - - const { errors } = await createOneFieldMetadata({ - expectToFail: true, - input: { - objectMetadataId: globalTestContext.objectMetadataIds.sourceObjectId, - name: 'owner', - label: 'owner field', - isLabelSyncedWithName: false, - type: FieldMetadataType.MORPH_RELATION, - morphRelationsCreationPayload: [ - { - targetFieldLabel: 'defaultFirstTargetFieldLabel', - type: RelationType.ONE_TO_MANY, - targetObjectMetadataId: - globalTestContext.objectMetadataIds.firstTargetObjectId, - targetFieldIcon: 'IconBuildingSkyscraper', - ...computedContext, - }, - { - targetFieldLabel: 'defaultSecondTargetFieldLabel', - type: RelationType.ONE_TO_MANY, - targetObjectMetadataId: - globalTestContext.objectMetadataIds.secondTargetObjectId, - targetFieldIcon: 'IconBuildingSkyscraper', - ...computedContext, - }, - ], - }, - }); - - expect(errors).toBeDefined(); - expect(errors).toMatchSnapshot(); - }, - ); - - it.each(failingLabelsCreationTestsUseCase)( - 'relation MANY_TO_ONE $title', - async ({ context }) => { - const computedContext = - typeof context === 'function' ? context(globalTestContext) : context; - - const { errors } = await createOneFieldMetadata({ - expectToFail: true, - input: { - objectMetadataId: globalTestContext.objectMetadataIds.sourceObjectId, - name: 'owner', - label: 'owner field', - isLabelSyncedWithName: false, - type: FieldMetadataType.MORPH_RELATION, - morphRelationsCreationPayload: [ - { - targetFieldLabel: 'defaultFirstTargetFieldLabel', - type: RelationType.MANY_TO_ONE, - targetObjectMetadataId: - globalTestContext.objectMetadataIds.firstTargetObjectId, - targetFieldIcon: 'IconBuildingSkyscraper', - ...computedContext, - }, - { - targetFieldLabel: 'defaultSecondTargetFieldLabel', - type: RelationType.MANY_TO_ONE, - targetObjectMetadataId: - globalTestContext.objectMetadataIds.secondTargetObjectId, - targetFieldIcon: 'IconBuildingSkyscraper', - ...computedContext, - }, - ], - }, - }); - - expect(errors).toBeDefined(); - expect(errors).toMatchSnapshot(); - }, - ); -}); diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/unique-field/record-creation-with-custom-unique-field.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/field-metadata/unique-field/record-creation-with-custom-unique-field.integration-spec.ts deleted file mode 100644 index 3a701a51ce..0000000000 --- a/packages/twenty-server/test/integration/metadata/suites/field-metadata/unique-field/record-creation-with-custom-unique-field.integration-spec.ts +++ /dev/null @@ -1,226 +0,0 @@ -import gql from 'graphql-tag'; -import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util'; -import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util'; -import { updateOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/update-one-field-metadata.util'; -import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util'; -import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util'; -import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util'; -import { updateFeatureFlag } from 'test/integration/metadata/suites/utils/update-feature-flag.util'; -import { FieldMetadataType } from 'twenty-shared/types'; - -import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; - -const createRecordQuery = gql` - mutation CreateFirstRecord($data: TestRecordObjectCreateInput!) { - createTestRecordObject(data: $data) { - id - uniqueTestField - } - } -`; - -describe('create records with custom unique fields', () => { - let createdObjectMetadataId = ''; - let uniqueFieldId = ''; - - beforeAll(async () => { - await updateFeatureFlag({ - expectToFail: false, - featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED, - value: false, - }); - }); - - afterAll(async () => { - await updateFeatureFlag({ - expectToFail: false, - featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED, - value: true, - }); - }); - - beforeEach(async () => { - const { - data: { - createOneObject: { id: objectMetadataId }, - }, - } = await createOneObjectMetadata({ - input: { - nameSingular: 'testRecordObject', - namePlural: 'testRecordObjects', - labelSingular: 'Test Record Object', - labelPlural: 'Test Record Objects', - icon: 'IconTestRecord', - }, - }); - - createdObjectMetadataId = objectMetadataId; - - const { data: createdField } = await createOneFieldMetadata({ - input: { - name: 'uniqueTestField', - label: 'Unique Test Field', - type: FieldMetadataType.TEXT, - objectMetadataId: createdObjectMetadataId, - isUnique: true, - }, - gqlFields: ` - id - name - label - type - isUnique - `, - }); - - uniqueFieldId = createdField.createOneField.id; - }); - - afterEach(async () => { - await updateOneObjectMetadata({ - expectToFail: false, - input: { - idToUpdate: createdObjectMetadataId, - updatePayload: { - isActive: false, - }, - }, - }); - await deleteOneObjectMetadata({ - input: { idToDelete: createdObjectMetadataId }, - }); - }); - - it('should not create the second record with same value on unique field', async () => { - const firstRecordResponse = await makeGraphqlAPIRequest({ - query: createRecordQuery, - variables: { - data: { - uniqueTestField: 'uniqueValue123', - }, - }, - }); - - expect(firstRecordResponse.body.errors).toBeUndefined(); - expect(firstRecordResponse.body.data.createTestRecordObject).toBeDefined(); - expect( - firstRecordResponse.body.data.createTestRecordObject.uniqueTestField, - ).toBe('uniqueValue123'); - - const secondRecordResponse = await makeGraphqlAPIRequest({ - query: createRecordQuery, - variables: { - data: { - uniqueTestField: 'uniqueValue123', - }, - }, - }); - - expect(secondRecordResponse.body.errors).toBeDefined(); - expect(secondRecordResponse.body.errors[0].message).toContain( - 'Duplicate Unique Test Field with value uniqueValue123. Please set a unique one.', - ); - expect(secondRecordResponse.body.data.createTestRecordObject).toBeNull(); - }); - - it('should create the second record with same value on updated field (previously unique)', async () => { - const firstRecordResponse = await makeGraphqlAPIRequest({ - query: createRecordQuery, - variables: { - data: { - uniqueTestField: 'duplicateValue', - }, - }, - }); - - expect(firstRecordResponse.body.errors).toBeUndefined(); - expect( - firstRecordResponse.body.data.createTestRecordObject.uniqueTestField, - ).toBe('duplicateValue'); - - const { data, errors } = await updateOneFieldMetadata({ - input: { - idToUpdate: uniqueFieldId, - updatePayload: { isUnique: false }, - }, - gqlFields: ` - id - name - isUnique - `, - }); - - expect(errors).toBeUndefined(); - expect(data.updateOneField.isUnique).toBe(false); - - const secondRecordResponse = await makeGraphqlAPIRequest({ - query: createRecordQuery, - variables: { - data: { - uniqueTestField: 'duplicateValue', - }, - }, - }); - - expect(secondRecordResponse.body.errors).toBeUndefined(); - expect(secondRecordResponse.body.data.createTestRecordObject).toBeDefined(); - expect( - secondRecordResponse.body.data.createTestRecordObject.uniqueTestField, - ).toBe('duplicateValue'); - }); - - it('should not create a unique index on field if records with same value already exist', async () => { - await updateOneFieldMetadata({ - input: { - idToUpdate: uniqueFieldId, - updatePayload: { isUnique: false }, - }, - gqlFields: ` - id - name - isUnique - `, - }); - - const firstRecordResponse = await makeGraphqlAPIRequest({ - query: createRecordQuery, - variables: { - data: { - uniqueTestField: 'duplicatedValue', - }, - }, - }); - - expect(firstRecordResponse.body.errors).toBeUndefined(); - - const secondRecordResponse = await makeGraphqlAPIRequest({ - query: createRecordQuery, - variables: { - data: { - uniqueTestField: 'duplicatedValue', - }, - }, - }); - - expect(secondRecordResponse.body.errors).toBeUndefined(); - - const { data, errors } = await updateOneFieldMetadata({ - input: { - idToUpdate: uniqueFieldId, - updatePayload: { isUnique: true }, - }, - gqlFields: ` - id - name - isUnique - `, - expectToFail: true, - }); - - expect(data).toBeNull(); - expect(errors).toBeDefined(); - expect(errors[0].message).toContain( - 'Unique index creation failed because of unique constraint violation', - ); - }); -}); diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/unique-field/update-one-unique-field-metadata.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/field-metadata/unique-field/update-one-unique-field-metadata.integration-spec.ts deleted file mode 100644 index 3b508cef41..0000000000 --- a/packages/twenty-server/test/integration/metadata/suites/field-metadata/unique-field/update-one-unique-field-metadata.integration-spec.ts +++ /dev/null @@ -1,380 +0,0 @@ -import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util'; -import { deleteOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/delete-one-field-metadata.util'; -import { updateOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/update-one-field-metadata.util'; -import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util'; -import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util'; -import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util'; -import { updateFeatureFlag } from 'test/integration/metadata/suites/utils/update-feature-flag.util'; -import { FieldMetadataType } from 'twenty-shared/types'; - -import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; - -describe('update one unique field metadata', () => { - let createdObjectId = ''; - - beforeAll(async () => { - await updateFeatureFlag({ - expectToFail: false, - featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED, - value: false, - }); - }); - - afterAll(async () => { - await updateFeatureFlag({ - expectToFail: false, - featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED, - value: true, - }); - }); - - beforeEach(async () => { - const { - data: { - createOneObject: { id: objectMetadataId }, - }, - } = await createOneObjectMetadata({ - input: { - nameSingular: 'testObjectToTestUniqueField', - namePlural: 'testObjectsToTestUniqueFields', - labelSingular: 'Test Object To Test Unique Field', - labelPlural: 'Test Objects To Test Unique Fields', - icon: 'IconTest', - }, - }); - - createdObjectId = objectMetadataId; - }); - - afterEach(async () => { - await updateOneObjectMetadata({ - expectToFail: false, - input: { - idToUpdate: createdObjectId, - updatePayload: { - isActive: false, - }, - }, - }); - await deleteOneObjectMetadata({ - input: { idToDelete: createdObjectId }, - }); - }); - - it('should update non unique field metadata to unique', async () => { - const { data: createdField } = await createOneFieldMetadata({ - input: { - name: 'testField', - label: 'Test Field', - type: FieldMetadataType.TEXT, - objectMetadataId: createdObjectId, - isUnique: false, - }, - gqlFields: ` - id - name - label - type - isUnique - `, - }); - - const { data, errors } = await updateOneFieldMetadata({ - input: { - idToUpdate: createdField.createOneField.id, - updatePayload: { isUnique: true }, - }, - gqlFields: ` - id - name - label - type - isUnique - `, - }); - - expect(errors).toBeUndefined(); - expect(data).not.toBeNull(); - expect(data.updateOneField).toBeDefined(); - expect(data.updateOneField.isUnique).toBe(true); - }); - - it('should update unique field metadata to non unique', async () => { - const { data: createdField } = await createOneFieldMetadata({ - input: { - name: 'uniqueTestField', - label: 'Unique Test Field', - type: FieldMetadataType.TEXT, - objectMetadataId: createdObjectId, - isUnique: true, - }, - gqlFields: ` - id - name - label - type - isUnique - `, - }); - - const { data, errors } = await updateOneFieldMetadata({ - input: { - idToUpdate: createdField.createOneField.id, - updatePayload: { isUnique: false }, - }, - gqlFields: ` - id - name - label - type - isUnique - `, - }); - - expect(errors).toBeUndefined(); - expect(data).not.toBeNull(); - expect(data.updateOneField).toBeDefined(); - expect(data.updateOneField.isUnique).toBe(false); - }); - - it('should update unique index name if field metadata name is updated', async () => { - const { data: createdField } = await createOneFieldMetadata({ - input: { - name: 'uniqueTestField', - label: 'Unique Test Field', - type: FieldMetadataType.TEXT, - objectMetadataId: createdObjectId, - isUnique: true, - }, - gqlFields: ` - id - name - label - type - isUnique - `, - }); - - const { data, errors } = await updateOneFieldMetadata({ - input: { - idToUpdate: createdField.createOneField.id, - updatePayload: { - name: 'renamedUniqueField', - label: 'Renamed Unique Field', - }, - }, - gqlFields: ` - id - name - label - type - isUnique - `, - }); - - expect(errors).toBeUndefined(); - expect(data).not.toBeNull(); - expect(data.updateOneField).toBeDefined(); - expect(data.updateOneField.name).toBe('renamedUniqueField'); - expect(data.updateOneField.isUnique).toBe(true); - - await updateOneFieldMetadata({ - input: { - idToUpdate: createdField.createOneField.id, - updatePayload: { - isActive: false, - }, - }, - gqlFields: ` - id - `, - }); - - const { data: deletedField, errors: deletedFieldErrors } = - await deleteOneFieldMetadata({ - input: { - idToDelete: createdField.createOneField.id, - }, - }); - - expect(deletedField).not.toBeNull(); - expect(deletedFieldErrors).toBeUndefined(); - }); - - it('should not update to unique field metadata if it has custom default value', async () => { - const { data: createdField } = await createOneFieldMetadata({ - input: { - name: 'fieldWithDefault', - label: 'Field With Default', - type: FieldMetadataType.TEXT, - objectMetadataId: createdObjectId, - defaultValue: "'defaultValue'", - isUnique: false, - }, - gqlFields: ` - id - name - label - type - isUnique - `, - }); - - const { data, errors } = await updateOneFieldMetadata({ - input: { - idToUpdate: createdField.createOneField.id, - updatePayload: { isUnique: true }, - }, - gqlFields: ` - id - name - label - type - isUnique - `, - expectToFail: true, - }); - - expect(data).toBeNull(); - expect(errors).toBeDefined(); - expect(errors[0].message).toBe('Unique field cannot have a default value'); - }); - - it('should not update with custom default value if unique field metadata', async () => { - const { data: createdField } = await createOneFieldMetadata({ - input: { - name: 'uniqueTestField', - label: 'Unique Test Field', - type: FieldMetadataType.TEXT, - objectMetadataId: createdObjectId, - isUnique: true, - }, - gqlFields: ` - id - name - label - type - isUnique - `, - }); - - const { data, errors } = await updateOneFieldMetadata({ - input: { - idToUpdate: createdField.createOneField.id, - updatePayload: { defaultValue: "'defaultValue'" }, - }, - gqlFields: ` - id - name - label - type - isUnique - `, - expectToFail: true, - }); - - expect(data).toBeNull(); - expect(errors).toBeDefined(); - expect(errors[0].message).toBe('Unique field cannot have a default value'); - }); - - it('should not update to unique field metadata if it has composite type without unique subfields', async () => { - const { data: createdField } = await createOneFieldMetadata({ - input: { - name: 'fullNameField', - label: 'Full Name Field', - type: FieldMetadataType.FULL_NAME, - objectMetadataId: createdObjectId, - isUnique: false, - }, - gqlFields: ` - id - name - label - type - isUnique - `, - }); - - const { data, errors } = await updateOneFieldMetadata({ - input: { - idToUpdate: createdField.createOneField.id, - updatePayload: { isUnique: true }, - }, - gqlFields: ` - id - name - label - type - isUnique - `, - expectToFail: true, - }); - - expect(data).toBeNull(); - expect(errors).toBeDefined(); - expect(errors[0].message).toContain( - 'Unique index cannot be created for field fullNameField of type FULL_NAME', - ); - }); - - it('should update a standard field to unique field metadata if it has not standard unique index', async () => { - const { data: createdField } = await createOneFieldMetadata({ - input: { - name: 'testField', - label: 'Test Field', - type: FieldMetadataType.TEXT, - objectMetadataId: createdObjectId, - isUnique: false, - isCustom: false, - }, - gqlFields: ` - id - name - label - type - isUnique - `, - }); - - const { data: addUniqueData, errors: addUniqueErrors } = - await updateOneFieldMetadata({ - input: { - idToUpdate: createdField.createOneField.id, - updatePayload: { isUnique: true }, - }, - gqlFields: ` - id - name - label - type - isUnique - `, - }); - - expect(addUniqueErrors).toBeUndefined(); - expect(addUniqueData).not.toBeNull(); - expect(addUniqueData.updateOneField).toBeDefined(); - expect(addUniqueData.updateOneField.isUnique).toBe(true); - - const { data: removeUniqueData, errors: removeUniqueErrors } = - await updateOneFieldMetadata({ - input: { - idToUpdate: createdField.createOneField.id, - updatePayload: { isUnique: false }, - }, - gqlFields: ` - id - name - label - type - isUnique - `, - }); - - expect(removeUniqueErrors).toBeUndefined(); - expect(removeUniqueData).not.toBeNull(); - expect(removeUniqueData.updateOneField).toBeDefined(); - expect(removeUniqueData.updateOneField.isUnique).toBe(false); - }); -}); diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/update-one-field-metadata-related-record.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/field-metadata/update-one-field-metadata-related-record.integration-spec.ts deleted file mode 100644 index 527168dc6c..0000000000 --- a/packages/twenty-server/test/integration/metadata/suites/field-metadata/update-one-field-metadata-related-record.integration-spec.ts +++ /dev/null @@ -1,366 +0,0 @@ -import { faker } from '@faker-js/faker'; -import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util'; -import { updateOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/update-one-field-metadata.util'; -import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util'; -import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util'; -import { getMockCreateObjectInput } from 'test/integration/metadata/suites/object-metadata/utils/generate-mock-create-object-metadata-input'; -import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util'; -import { updateFeatureFlag } from 'test/integration/metadata/suites/utils/update-feature-flag.util'; -import { - createTestViewFilterWithRestApi, - createTestViewWithRestApi, - findViewFilterWithRestApi, -} from 'test/integration/rest/utils/view-rest-api.util'; -import { type EachTestingContext } from 'twenty-shared/testing'; -import { - type EnumFieldMetadataType, - FieldMetadataType, - ViewFilterOperand, -} from 'twenty-shared/types'; -import { isDefined } from 'twenty-shared/utils'; - -import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; -import { - type FieldMetadataComplexOption, - type FieldMetadataDefaultOption, -} from 'src/engine/metadata-modules/field-metadata/dtos/options.input'; -import { type ViewFilterValue } from 'src/engine/metadata-modules/view-filter/types/view-filter-value.type'; -import { ViewType } from 'src/engine/metadata-modules/view/enums/view-type.enum'; - -type Option = FieldMetadataDefaultOption | FieldMetadataComplexOption; - -const generateOption = (index: number): Option => ({ - label: `Option ${index}`, - value: `OPTION_${index}`, - color: 'green', - position: index, -}); -const generateOptions = (length: number) => - Array.from({ length }, (_value, index) => generateOption(index)); -const fakeOptionUpdate = ({ value, label, ...option }: Option) => ({ - ...option, - value: `${value}_UPDATED`, - label: `${label} updated`, -}); - -const ALL_OPTIONS = generateOptions(10); - -const isEven = (_value: unknown, index: number) => index % 2 === 0; - -type ViewFilterUpdate = { - value: string[]; -}; - -type FieldMetadataOptionsAndType = { - options: Option[]; - type: EnumFieldMetadataType; -}; - -type TestCase = EachTestingContext<{ - fieldMetadata?: FieldMetadataOptionsAndType; - createViewFilter?: ViewFilterUpdate; - updateOptions: ( - options: FieldMetadataDefaultOption[] | FieldMetadataComplexOption[], - ) => FieldMetadataDefaultOption[] | FieldMetadataComplexOption[]; - expected?: null; -}>; -const testFieldMetadataType: EnumFieldMetadataType[] = [ - FieldMetadataType.SELECT, - FieldMetadataType.MULTI_SELECT, -]; - -describe('update-one-field-metadata-related-record', () => { - let idToDelete: string; - - beforeAll(async () => { - await updateFeatureFlag({ - expectToFail: false, - featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED, - value: false, - }); - }); - - afterAll(async () => { - await updateFeatureFlag({ - expectToFail: false, - featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED, - value: true, - }); - }); - - const createObjectSelectFieldAndView = async ({ - options, - type: fieldMetadataType, - }: FieldMetadataOptionsAndType) => { - const singular = faker.lorem.words(); - const plural = singular + faker.lorem.word(); - const { - data: { createOneObject }, - } = await createOneObjectMetadata({ - expectToFail: false, - input: getMockCreateObjectInput({ - labelSingular: singular, - labelPlural: plural, - nameSingular: singular.split(' ').join(''), - namePlural: plural.split(' ').join(''), - isLabelSyncedWithName: false, - }), - }); - - idToDelete = createOneObject.id; - - const { - data: { createOneField }, - } = await createOneFieldMetadata({ - expectToFail: false, - input: { - objectMetadataId: createOneObject.id, - type: fieldMetadataType, - name: 'testName', - label: 'Test name', - isLabelSyncedWithName: true, - options, - }, - gqlFields: ` - id - options - `, - }); - - const createdView = await createTestViewWithRestApi({ - id: faker.string.uuid(), - name: 'Test View', - objectMetadataId: createOneObject.id, - type: ViewType.TABLE, - }); - - return { createOneObject, createOneField, createdView }; - }; - - afterEach(async () => { - if (isDefined(idToDelete)) { - await updateOneObjectMetadata({ - expectToFail: false, - input: { - idToUpdate: idToDelete, - updatePayload: { - isActive: false, - }, - }, - }); - await deleteOneObjectMetadata({ - expectToFail: false, - input: { idToDelete: idToDelete }, - }); - } - }); - - describe.each(testFieldMetadataType)('%s', (fieldType) => { - const testCases: TestCase[] = [ - { - title: - 'should delete related view filter if all select field options got deleted', - context: { - updateOptions: () => generateOptions(3), - expected: null, - }, - }, - { - title: 'should update related multi selected options view filter', - context: { - updateOptions: (options) => - options.map((option, index) => - isEven(option, index) ? fakeOptionUpdate(option) : option, - ), - }, - }, - { - title: 'should update related solo selected option view filter', - context: { - createViewFilter: { - value: [ALL_OPTIONS[5].value], - }, - updateOptions: (options) => [fakeOptionUpdate(options[5])], - }, - }, - { - title: - 'should handle partial deletion of selected options in view filter', - context: { - updateOptions: (options) => options.slice(4), - }, - }, - { - title: - 'should handle reordering of options while maintaining view filter values', - context: { - createViewFilter: { - value: ALL_OPTIONS.slice(0, 2).map((option) => option.value), - }, - updateOptions: (options) => [...options].reverse(), - }, - }, - { - title: - 'should handle no changes update of options while maintaining existing view filter values', - context: { - updateOptions: (options) => options, - }, - }, - { - title: - 'should handle adding new options while maintaining existing view filter', - context: { - fieldMetadata: { - options: ALL_OPTIONS.slice(0, 5), - type: fieldType, - }, - createViewFilter: { - value: ALL_OPTIONS.slice(0, 2).map((option) => option.value), - }, - updateOptions: (options) => [ - ...options, - ...generateOptions(6).slice(5), - ], - }, - }, - ]; - - test.each(testCases)( - '$title', - async ({ - context: { - expected, - createViewFilter = { - value: ALL_OPTIONS.map((option) => option.value), - }, - fieldMetadata = { options: ALL_OPTIONS, type: fieldType }, - updateOptions, - }, - }) => { - const { createOneField, createdView } = - await createObjectSelectFieldAndView(fieldMetadata); - - const createdViewFilter = await createTestViewFilterWithRestApi({ - viewId: createdView.id, - fieldMetadataId: createOneField.id, - operand: ViewFilterOperand.IS, - value: createViewFilter.value, - }); - - const optionsWithIds = createOneField.options; - - if (!isDefined(optionsWithIds)) { - throw new Error('optionsWithIds is not defined'); - } - const updatedOptions = updateOptions(optionsWithIds); - - await updateOneFieldMetadata({ - input: { - idToUpdate: createOneField.id, - updatePayload: { - options: updatedOptions, - }, - }, - gqlFields: ` - id - options - `, - }); - - const updatedViewFilter = await findViewFilterWithRestApi( - createdViewFilter.id, - ); - - if (expected !== undefined) { - expect(updatedViewFilter).toBe(expected); - - return; - } - - if (!isDefined(updatedViewFilter)) { - throw new Error( - 'updatedViewFilter is not defined but should be at this point', - ); - } - - expect(updatedViewFilter.value).not.toBeNull(); - if (updatedViewFilter.value === null) { - throw new Error('Invariant parsedValue should not be null'); - } - expect(updatedOptions.map((option) => option.value)).toEqual( - expect.arrayContaining(updatedViewFilter.value as string[]), - ); - - expect({ - value: updatedViewFilter.value, - operand: updatedViewFilter.operand, - viewFilterGroupId: updatedViewFilter.viewFilterGroupId, - positionInViewFilterGroup: - updatedViewFilter.positionInViewFilterGroup, - subFieldName: updatedViewFilter.subFieldName, - }).toMatchSnapshot(); - }, - ); - - // Note these test exists only because we do not validate the view filter value on creation/update - // Should be removed after https://github.com/twentyhq/core-team-issues/issues/1009 completion - const failingTestCases: EachTestingContext<{ - createViewFilterValue: unknown; - }>[] = [ - { - title: - 'should throw error if view filter value is not a stringified JSON array', - context: { - createViewFilterValue: JSON.stringify( - 'not an array stringified json', - ), - }, - }, - ]; - - test.each(failingTestCases)( - '$title', - async ({ context: { createViewFilterValue } }) => { - const { createOneField, createdView } = - await createObjectSelectFieldAndView({ - options: ALL_OPTIONS, - type: fieldType, - }); - - const viewFilterId = '20202020-e3b5-4fa7-85aa-9b1950fc7bf5'; - - await createTestViewFilterWithRestApi({ - id: viewFilterId, - viewId: createdView.id, - fieldMetadataId: createOneField.id, - operand: ViewFilterOperand.IS, - value: createViewFilterValue as unknown as ViewFilterValue, - }); - - const optionsWithIds = createOneField.options; - - if (!isDefined(optionsWithIds)) { - throw new Error('optionsWithIds is not defined'); - } - const updatePayload = { - options: optionsWithIds.map((option) => fakeOptionUpdate(option)), - }; - const { errors, data } = await updateOneFieldMetadata({ - input: { - idToUpdate: createOneField.id, - updatePayload, - }, - gqlFields: ` - id - options - `, - }); - - expect(data).toBeNull(); - expect(errors).toBeDefined(); - expect(errors).toMatchSnapshot(); - }, - ); - }); -}); diff --git a/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/failing-create-one-object-metadata-v2.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/failing-create-one-object-metadata-v2.integration-spec.ts.snap index f0e0fd5d72..d28e537acb 100644 --- a/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/failing-create-one-object-metadata-v2.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/failing-create-one-object-metadata-v2.integration-spec.ts.snap @@ -1229,613 +1229,11 @@ exports[`Object metadata creation should fail v2 when labelPlural exceeds maximu exports[`Object metadata creation should fail v2 when labelPlural is empty 1`] = ` { "extensions": { - "code": "METADATA_VALIDATION_FAILED", - "errors": { - "cronTrigger": [], - "databaseEventTrigger": [], - "fieldMetadata": [ - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "id", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "name", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "createdAt", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "updatedAt", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "deletedAt", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "createdBy", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "position", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "searchVector", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "timelineActivities", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "favorites", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "attachments", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "noteTargets", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "taskTargets", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Relation target object metadata not found", - "userFriendlyMessage": "Object targeted by the relation not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "listinga", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Relation target object metadata not found", - "userFriendlyMessage": "Object targeted by the relation not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "listinga", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Relation target object metadata not found", - "userFriendlyMessage": "Object targeted by the relation not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "listinga", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Relation target object metadata not found", - "userFriendlyMessage": "Object targeted by the relation not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "listinga", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Relation target object metadata not found", - "userFriendlyMessage": "Object targeted by the relation not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "listinga", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - ], - "index": [ - { - "errors": [ - { - "code": "INDEX_OBJECT_NOT_FOUND", - "message": "Could not find index related object metadata", - "userFriendlyMessage": "Index related object not found", - }, - { - "code": "INDEX_FIELD_NOT_FOUND", - "message": "Could not find index field related field metadata", - "userFriendlyMessage": "Field referenced in index does not exist", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "IDX_ffebb98ec981552dbecc71223c8", - }, - "status": "fail", - "type": "create_index", - }, - ], - "objectMetadata": [ - { - "errors": [ - { - "code": "INVALID_OBJECT_INPUT", - "message": "Object label is too short", - "value": "", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "namePlural": "listingas", - "nameSingular": "listinga", - }, - "status": "fail", - "type": "create_object", - }, - ], - "routeTrigger": [], - "serverlessFunction": [], - "view": [ - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Object metadata not found", - "userFriendlyMessage": "Object metadata not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - }, - "status": "fail", - "type": "create_view", - }, - ], - "viewField": [ - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataId": Any, - "id": Any, - "viewId": Any, - }, - "status": "fail", - "type": "create_view_field", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataId": Any, - "id": Any, - "viewId": Any, - }, - "status": "fail", - "type": "create_view_field", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataId": Any, - "id": Any, - "viewId": Any, - }, - "status": "fail", - "type": "create_view_field", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataId": Any, - "id": Any, - "viewId": Any, - }, - "status": "fail", - "type": "create_view_field", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataId": Any, - "id": Any, - "viewId": Any, - }, - "status": "fail", - "type": "create_view_field", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataId": Any, - "id": Any, - "viewId": Any, - }, - "status": "fail", - "type": "create_view_field", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataId": Any, - "id": Any, - "viewId": Any, - }, - "status": "fail", - "type": "create_view_field", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataId": Any, - "id": Any, - "viewId": Any, - }, - "status": "fail", - "type": "create_view_field", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataId": Any, - "id": Any, - "viewId": Any, - }, - "status": "fail", - "type": "create_view_field", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataId": Any, - "id": Any, - "viewId": Any, - }, - "status": "fail", - "type": "create_view_field", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataId": Any, - "id": Any, - "viewId": Any, - }, - "status": "fail", - "type": "create_view_field", - }, - ], - "viewFilter": [], - "viewGroup": [], - }, - "message": "Validation failed for 0 object(s) and 0 field(s)", - "summary": { - "invalidCronTrigger": 0, - "invalidDatabaseEventTrigger": 0, - "invalidFieldMetadata": 0, - "invalidIndex": 0, - "invalidObjectMetadata": 0, - "invalidRouteTrigger": 0, - "invalidServerlessFunction": 0, - "invalidView": 0, - "invalidViewField": 0, - "invalidViewFilter": 0, - "invalidViewGroup": 0, - "totalErrors": 0, - }, - "userFriendlyMessage": "Validation failed for 0 object(s) and 0 field(s)", + "code": "BAD_USER_INPUT", + "userFriendlyMessage": "An error occurred.", }, - "message": "Multiple validation errors occurred while creating object", - "name": "GraphQLError", + "message": "labelPlural should not be empty", + "name": "UserInputError", } `; @@ -3068,613 +2466,11 @@ exports[`Object metadata creation should fail v2 when labelSingular exceeds maxi exports[`Object metadata creation should fail v2 when labelSingular is empty 1`] = ` { "extensions": { - "code": "METADATA_VALIDATION_FAILED", - "errors": { - "cronTrigger": [], - "databaseEventTrigger": [], - "fieldMetadata": [ - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "id", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "name", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "createdAt", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "updatedAt", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "deletedAt", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "createdBy", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "position", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "searchVector", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "timelineActivities", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "favorites", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "attachments", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "noteTargets", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "taskTargets", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Relation target object metadata not found", - "userFriendlyMessage": "Object targeted by the relation not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "listinga", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Relation target object metadata not found", - "userFriendlyMessage": "Object targeted by the relation not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "listinga", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Relation target object metadata not found", - "userFriendlyMessage": "Object targeted by the relation not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "listinga", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Relation target object metadata not found", - "userFriendlyMessage": "Object targeted by the relation not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "listinga", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Relation target object metadata not found", - "userFriendlyMessage": "Object targeted by the relation not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "listinga", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - ], - "index": [ - { - "errors": [ - { - "code": "INDEX_OBJECT_NOT_FOUND", - "message": "Could not find index related object metadata", - "userFriendlyMessage": "Index related object not found", - }, - { - "code": "INDEX_FIELD_NOT_FOUND", - "message": "Could not find index field related field metadata", - "userFriendlyMessage": "Field referenced in index does not exist", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "IDX_ffebb98ec981552dbecc71223c8", - }, - "status": "fail", - "type": "create_index", - }, - ], - "objectMetadata": [ - { - "errors": [ - { - "code": "INVALID_OBJECT_INPUT", - "message": "Object label is too short", - "value": "", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "namePlural": "listingas", - "nameSingular": "listinga", - }, - "status": "fail", - "type": "create_object", - }, - ], - "routeTrigger": [], - "serverlessFunction": [], - "view": [ - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Object metadata not found", - "userFriendlyMessage": "Object metadata not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - }, - "status": "fail", - "type": "create_view", - }, - ], - "viewField": [ - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataId": Any, - "id": Any, - "viewId": Any, - }, - "status": "fail", - "type": "create_view_field", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataId": Any, - "id": Any, - "viewId": Any, - }, - "status": "fail", - "type": "create_view_field", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataId": Any, - "id": Any, - "viewId": Any, - }, - "status": "fail", - "type": "create_view_field", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataId": Any, - "id": Any, - "viewId": Any, - }, - "status": "fail", - "type": "create_view_field", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataId": Any, - "id": Any, - "viewId": Any, - }, - "status": "fail", - "type": "create_view_field", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataId": Any, - "id": Any, - "viewId": Any, - }, - "status": "fail", - "type": "create_view_field", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataId": Any, - "id": Any, - "viewId": Any, - }, - "status": "fail", - "type": "create_view_field", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataId": Any, - "id": Any, - "viewId": Any, - }, - "status": "fail", - "type": "create_view_field", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataId": Any, - "id": Any, - "viewId": Any, - }, - "status": "fail", - "type": "create_view_field", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataId": Any, - "id": Any, - "viewId": Any, - }, - "status": "fail", - "type": "create_view_field", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataId": Any, - "id": Any, - "viewId": Any, - }, - "status": "fail", - "type": "create_view_field", - }, - ], - "viewFilter": [], - "viewGroup": [], - }, - "message": "Validation failed for 0 object(s) and 0 field(s)", - "summary": { - "invalidCronTrigger": 0, - "invalidDatabaseEventTrigger": 0, - "invalidFieldMetadata": 0, - "invalidIndex": 0, - "invalidObjectMetadata": 0, - "invalidRouteTrigger": 0, - "invalidServerlessFunction": 0, - "invalidView": 0, - "invalidViewField": 0, - "invalidViewFilter": 0, - "invalidViewGroup": 0, - "totalErrors": 0, - }, - "userFriendlyMessage": "Validation failed for 0 object(s) and 0 field(s)", + "code": "BAD_USER_INPUT", + "userFriendlyMessage": "An error occurred.", }, - "message": "Multiple validation errors occurred while creating object", - "name": "GraphQLError", + "message": "labelSingular should not be empty", + "name": "UserInputError", } `; @@ -6778,618 +5574,11 @@ exports[`Object metadata creation should fail v2 when namePlural is a reserved k exports[`Object metadata creation should fail v2 when namePlural is an empty string 1`] = ` { "extensions": { - "code": "METADATA_VALIDATION_FAILED", - "errors": { - "cronTrigger": [], - "databaseEventTrigger": [], - "fieldMetadata": [ - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "id", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "name", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "createdAt", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "updatedAt", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "deletedAt", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "createdBy", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "position", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "searchVector", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "timelineActivities", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "favorites", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "attachments", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "noteTargets", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "taskTargets", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Relation target object metadata not found", - "userFriendlyMessage": "Object targeted by the relation not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "listinga", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Relation target object metadata not found", - "userFriendlyMessage": "Object targeted by the relation not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "listinga", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Relation target object metadata not found", - "userFriendlyMessage": "Object targeted by the relation not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "listinga", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Relation target object metadata not found", - "userFriendlyMessage": "Object targeted by the relation not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "listinga", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Relation target object metadata not found", - "userFriendlyMessage": "Object targeted by the relation not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "listinga", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - ], - "index": [ - { - "errors": [ - { - "code": "INDEX_OBJECT_NOT_FOUND", - "message": "Could not find index related object metadata", - "userFriendlyMessage": "Index related object not found", - }, - { - "code": "INDEX_FIELD_NOT_FOUND", - "message": "Could not find index field related field metadata", - "userFriendlyMessage": "Field referenced in index does not exist", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "IDX_ffebb98ec981552dbecc71223c8", - }, - "status": "fail", - "type": "create_index", - }, - ], - "objectMetadata": [ - { - "errors": [ - { - "code": "INVALID_OBJECT_INPUT", - "message": "Name is too short", - "value": "", - }, - { - "code": "INVALID_OBJECT_INPUT", - "message": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", - "value": "", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "namePlural": "", - "nameSingular": "listinga", - }, - "status": "fail", - "type": "create_object", - }, - ], - "routeTrigger": [], - "serverlessFunction": [], - "view": [ - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Object metadata not found", - "userFriendlyMessage": "Object metadata not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - }, - "status": "fail", - "type": "create_view", - }, - ], - "viewField": [ - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataId": Any, - "id": Any, - "viewId": Any, - }, - "status": "fail", - "type": "create_view_field", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataId": Any, - "id": Any, - "viewId": Any, - }, - "status": "fail", - "type": "create_view_field", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataId": Any, - "id": Any, - "viewId": Any, - }, - "status": "fail", - "type": "create_view_field", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataId": Any, - "id": Any, - "viewId": Any, - }, - "status": "fail", - "type": "create_view_field", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataId": Any, - "id": Any, - "viewId": Any, - }, - "status": "fail", - "type": "create_view_field", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataId": Any, - "id": Any, - "viewId": Any, - }, - "status": "fail", - "type": "create_view_field", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataId": Any, - "id": Any, - "viewId": Any, - }, - "status": "fail", - "type": "create_view_field", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataId": Any, - "id": Any, - "viewId": Any, - }, - "status": "fail", - "type": "create_view_field", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataId": Any, - "id": Any, - "viewId": Any, - }, - "status": "fail", - "type": "create_view_field", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataId": Any, - "id": Any, - "viewId": Any, - }, - "status": "fail", - "type": "create_view_field", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataId": Any, - "id": Any, - "viewId": Any, - }, - "status": "fail", - "type": "create_view_field", - }, - ], - "viewFilter": [], - "viewGroup": [], - }, - "message": "Validation failed for 0 object(s) and 0 field(s)", - "summary": { - "invalidCronTrigger": 0, - "invalidDatabaseEventTrigger": 0, - "invalidFieldMetadata": 0, - "invalidIndex": 0, - "invalidObjectMetadata": 0, - "invalidRouteTrigger": 0, - "invalidServerlessFunction": 0, - "invalidView": 0, - "invalidViewField": 0, - "invalidViewFilter": 0, - "invalidViewGroup": 0, - "totalErrors": 0, - }, - "userFriendlyMessage": "Validation failed for 0 object(s) and 0 field(s)", + "code": "BAD_USER_INPUT", + "userFriendlyMessage": "An error occurred.", }, - "message": "Multiple validation errors occurred while creating object", - "name": "GraphQLError", + "message": "namePlural has failed the name validation check, namePlural should not be empty", + "name": "UserInputError", } `; @@ -10656,678 +8845,11 @@ exports[`Object metadata creation should fail v2 when nameSingular is a reserved exports[`Object metadata creation should fail v2 when nameSingular is an empty string 1`] = ` { "extensions": { - "code": "METADATA_VALIDATION_FAILED", - "errors": { - "cronTrigger": [], - "databaseEventTrigger": [], - "fieldMetadata": [ - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "id", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "name", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "createdAt", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "updatedAt", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "deletedAt", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "createdBy", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "position", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "searchVector", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "timelineActivities", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "favorites", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "attachments", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "noteTargets", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "taskTargets", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - { - "errors": [ - { - "code": "INVALID_FIELD_INPUT", - "message": "Name is too short", - "userFriendlyMessage": "Name is too short", - "value": "", - }, - { - "code": "INVALID_FIELD_INPUT", - "message": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", - "userFriendlyMessage": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", - "value": "", - }, - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Relation target object metadata not found", - "userFriendlyMessage": "Object targeted by the relation not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - { - "errors": [ - { - "code": "INVALID_FIELD_INPUT", - "message": "Name is too short", - "userFriendlyMessage": "Name is too short", - "value": "", - }, - { - "code": "INVALID_FIELD_INPUT", - "message": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", - "userFriendlyMessage": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", - "value": "", - }, - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Relation target object metadata not found", - "userFriendlyMessage": "Object targeted by the relation not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - { - "errors": [ - { - "code": "INVALID_FIELD_INPUT", - "message": "Name is too short", - "userFriendlyMessage": "Name is too short", - "value": "", - }, - { - "code": "INVALID_FIELD_INPUT", - "message": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", - "userFriendlyMessage": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", - "value": "", - }, - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Relation target object metadata not found", - "userFriendlyMessage": "Object targeted by the relation not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - { - "errors": [ - { - "code": "INVALID_FIELD_INPUT", - "message": "Name is too short", - "userFriendlyMessage": "Name is too short", - "value": "", - }, - { - "code": "INVALID_FIELD_INPUT", - "message": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", - "userFriendlyMessage": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", - "value": "", - }, - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Relation target object metadata not found", - "userFriendlyMessage": "Object targeted by the relation not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - { - "errors": [ - { - "code": "INVALID_FIELD_INPUT", - "message": "Name is too short", - "userFriendlyMessage": "Name is too short", - "value": "", - }, - { - "code": "INVALID_FIELD_INPUT", - "message": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", - "userFriendlyMessage": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", - "value": "", - }, - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Relation target object metadata not found", - "userFriendlyMessage": "Object targeted by the relation not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "", - "objectMetadataId": Any, - }, - "status": "fail", - "type": "create_field", - }, - ], - "index": [ - { - "errors": [ - { - "code": "INDEX_OBJECT_NOT_FOUND", - "message": "Could not find index related object metadata", - "userFriendlyMessage": "Index related object not found", - }, - { - "code": "INDEX_FIELD_NOT_FOUND", - "message": "Could not find index field related field metadata", - "userFriendlyMessage": "Field referenced in index does not exist", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "name": "IDX_e0b8367f7f89bf39915991232b6", - }, - "status": "fail", - "type": "create_index", - }, - ], - "objectMetadata": [ - { - "errors": [ - { - "code": "INVALID_OBJECT_INPUT", - "message": "Name is too short", - "value": "", - }, - { - "code": "INVALID_OBJECT_INPUT", - "message": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters", - "value": "", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "namePlural": "listingas", - "nameSingular": "", - }, - "status": "fail", - "type": "create_object", - }, - ], - "routeTrigger": [], - "serverlessFunction": [], - "view": [ - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Object metadata not found", - "userFriendlyMessage": "Object metadata not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - }, - "status": "fail", - "type": "create_view", - }, - ], - "viewField": [ - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataId": Any, - "id": Any, - "viewId": Any, - }, - "status": "fail", - "type": "create_view_field", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataId": Any, - "id": Any, - "viewId": Any, - }, - "status": "fail", - "type": "create_view_field", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataId": Any, - "id": Any, - "viewId": Any, - }, - "status": "fail", - "type": "create_view_field", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataId": Any, - "id": Any, - "viewId": Any, - }, - "status": "fail", - "type": "create_view_field", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataId": Any, - "id": Any, - "viewId": Any, - }, - "status": "fail", - "type": "create_view_field", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataId": Any, - "id": Any, - "viewId": Any, - }, - "status": "fail", - "type": "create_view_field", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataId": Any, - "id": Any, - "viewId": Any, - }, - "status": "fail", - "type": "create_view_field", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataId": Any, - "id": Any, - "viewId": Any, - }, - "status": "fail", - "type": "create_view_field", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataId": Any, - "id": Any, - "viewId": Any, - }, - "status": "fail", - "type": "create_view_field", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataId": Any, - "id": Any, - "viewId": Any, - }, - "status": "fail", - "type": "create_view_field", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataId": Any, - "id": Any, - "viewId": Any, - }, - "status": "fail", - "type": "create_view_field", - }, - ], - "viewFilter": [], - "viewGroup": [], - }, - "message": "Validation failed for 0 object(s) and 0 field(s)", - "summary": { - "invalidCronTrigger": 0, - "invalidDatabaseEventTrigger": 0, - "invalidFieldMetadata": 0, - "invalidIndex": 0, - "invalidObjectMetadata": 0, - "invalidRouteTrigger": 0, - "invalidServerlessFunction": 0, - "invalidView": 0, - "invalidViewField": 0, - "invalidViewFilter": 0, - "invalidViewGroup": 0, - "totalErrors": 0, - }, - "userFriendlyMessage": "Validation failed for 0 object(s) and 0 field(s)", + "code": "BAD_USER_INPUT", + "userFriendlyMessage": "An error occurred.", }, - "message": "Multiple validation errors occurred while creating object", - "name": "GraphQLError", + "message": "nameSingular has failed the name validation check, nameSingular should not be empty", + "name": "UserInputError", } `; diff --git a/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/failing-create-one-object-metadata.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/failing-create-one-object-metadata.integration-spec.ts.snap deleted file mode 100644 index ae731cee6d..0000000000 --- a/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/failing-create-one-object-metadata.integration-spec.ts.snap +++ /dev/null @@ -1,43 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Object metadata creation should fail when labelPlural contains only whitespace 1`] = `"Input is too short: """`; - -exports[`Object metadata creation should fail when labelPlural exceeds maximum length 1`] = `"Name is too long: it exceeds the 63 characters limit."`; - -exports[`Object metadata creation should fail when labelPlural is empty 1`] = `"Input is too short: """`; - -exports[`Object metadata creation should fail when labelSingular contains only whitespace 1`] = `"Input is too short: """`; - -exports[`Object metadata creation should fail when labelSingular exceeds maximum length 1`] = `"Name is too long: it exceeds the 63 characters limit."`; - -exports[`Object metadata creation should fail when labelSingular is empty 1`] = `"Input is too short: """`; - -exports[`Object metadata creation should fail when labels are identical 1`] = `"The singular and plural labels cannot be the same for an object"`; - -exports[`Object metadata creation should fail when labels with whitespaces result to be identical 1`] = `"The singular and plural labels cannot be the same for an object"`; - -exports[`Object metadata creation should fail when name exceeds maximum length 1`] = `"Name is too long: it exceeds the 63 characters limit."`; - -exports[`Object metadata creation should fail when namePlural has invalid characters 1`] = `"String "μ" is not valid: must start with lowercase letter and contain only alphanumeric letters"`; - -exports[`Object metadata creation should fail when namePlural is a reserved keyword 1`] = `"The name "users" is not available"`; - -exports[`Object metadata creation should fail when namePlural is an empty string 1`] = `"Input is too short: """`; - -exports[`Object metadata creation should fail when namePlural is not camelCased 1`] = `"Not_Camel_Case should be in camelCase"`; - -exports[`Object metadata creation should fail when nameSingular contains only one char and whitespaces 1`] = `"a a should be in camelCase"`; - -exports[`Object metadata creation should fail when nameSingular contains only whitespaces 1`] = `"should be in camelCase"`; - -exports[`Object metadata creation should fail when nameSingular has invalid characters 1`] = `"String "μ" is not valid: must start with lowercase letter and contain only alphanumeric letters"`; - -exports[`Object metadata creation should fail when nameSingular is a reserved keyword 1`] = `"The name "user" is not available"`; - -exports[`Object metadata creation should fail when nameSingular is an empty string 1`] = `"Input is too short: """`; - -exports[`Object metadata creation should fail when nameSingular is not camelCased 1`] = `"Not_Camel_Case should be in camelCase"`; - -exports[`Object metadata creation should fail when names are identical 1`] = `"The singular and plural names cannot be the same for an object"`; - -exports[`Object metadata creation should fail when names with whitespaces result to be identical 1`] = `"fooBar should be in camelCase"`; diff --git a/packages/twenty-server/test/integration/metadata/suites/object-metadata/failing-create-one-object-metadata.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/object-metadata/failing-create-one-object-metadata.integration-spec.ts deleted file mode 100644 index 698a9d07da..0000000000 --- a/packages/twenty-server/test/integration/metadata/suites/object-metadata/failing-create-one-object-metadata.integration-spec.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { OBJECT_METADATA_LABEL_FAILING_TEST_CASES } from 'test/integration/metadata/suites/object-metadata/common/object-metadata-label-failing-tests-cases'; -import { OBJECT_METADATA_NAMES_FAILING_TEST_CASES } from 'test/integration/metadata/suites/object-metadata/common/object-metadata-names-failing-tests-cases'; -import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util'; -import { getMockCreateObjectInput } from 'test/integration/metadata/suites/object-metadata/utils/generate-mock-create-object-metadata-input'; -import { updateFeatureFlag } from 'test/integration/metadata/suites/utils/update-feature-flag.util'; - -import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; -import { ErrorCode } from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; -import { SEED_APPLE_WORKSPACE_ID } from 'src/engine/workspace-manager/dev-seeder/core/constants/seeder-workspaces.constant'; - -const allTestsUseCases = [ - ...OBJECT_METADATA_NAMES_FAILING_TEST_CASES, - ...OBJECT_METADATA_LABEL_FAILING_TEST_CASES, -]; - -describe('Object metadata creation should fail', () => { - beforeAll(async () => { - await updateFeatureFlag({ - expectToFail: false, - featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED, - value: false, - workspaceId: SEED_APPLE_WORKSPACE_ID, - }); - }); - - afterAll(async () => { - await updateFeatureFlag({ - expectToFail: false, - featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED, - value: true, - workspaceId: SEED_APPLE_WORKSPACE_ID, - }); - }); - - it.each(allTestsUseCases)('$title', async ({ context }) => { - const { errors } = await createOneObjectMetadata({ - input: getMockCreateObjectInput(context), - expectToFail: true, - }); - - expect(errors.length).toBe(1); - const firstError = errors[0]; - - expect(firstError.extensions.code).toBe(ErrorCode.BAD_USER_INPUT); - expect(firstError.message).toMatchSnapshot(); - }); -});