Prevent field name conflicts (#13280)
Fixes https://github.com/twentyhq/twenty/issues/13184
This commit is contained in:
@ -77,7 +77,7 @@ export class FieldMetadataMorphRelationService {
|
||||
}
|
||||
|
||||
const relationFieldMetadataForCreate =
|
||||
await this.fieldMetadataRelationService.addCustomRelationFieldMetadataForCreation(
|
||||
this.fieldMetadataRelationService.computeCustomRelationFieldMetadataForCreation(
|
||||
{
|
||||
fieldMetadataInput: fieldMetadataForCreate,
|
||||
relationCreationPayload: relation,
|
||||
@ -94,6 +94,7 @@ export class FieldMetadataMorphRelationService {
|
||||
fieldMetadataInput: relationFieldMetadataForCreate,
|
||||
fieldMetadataType: relationFieldMetadataForCreate.type,
|
||||
objectMetadataMaps,
|
||||
objectMetadata,
|
||||
},
|
||||
);
|
||||
|
||||
@ -118,7 +119,7 @@ export class FieldMetadataMorphRelationService {
|
||||
);
|
||||
|
||||
const targetFieldMetadataToCreateWithRelation =
|
||||
await this.fieldMetadataRelationService.addCustomRelationFieldMetadataForCreation(
|
||||
this.fieldMetadataRelationService.computeCustomRelationFieldMetadataForCreation(
|
||||
{
|
||||
fieldMetadataInput: targetFieldMetadataToCreate,
|
||||
relationCreationPayload: {
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { Injectable, ValidationError } from '@nestjs/common';
|
||||
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { plainToInstance } from 'class-transformer';
|
||||
import { IsEnum, IsString, IsUUID, validateOrReject } from 'class-validator';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
@ -17,6 +18,7 @@ import {
|
||||
FieldMetadataException,
|
||||
FieldMetadataExceptionCode,
|
||||
} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
|
||||
import { computeRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-relation-field-join-column-name.util';
|
||||
import { prepareCustomFieldMetadataForCreation } from 'src/engine/metadata-modules/field-metadata/utils/prepare-field-metadata-for-creation.util';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-metadata/relation-on-delete-action.type';
|
||||
@ -28,7 +30,6 @@ import { validateMetadataNameOrThrow } from 'src/engine/metadata-modules/utils/v
|
||||
import { computeMetadataNameFromLabel } from 'src/engine/metadata-modules/utils/validate-name-and-label-are-sync-or-throw.util';
|
||||
import { isFieldMetadataInterfaceOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
|
||||
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
|
||||
import { computeRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-relation-field-join-column-name.util';
|
||||
|
||||
export class RelationCreationPayloadValidation {
|
||||
@IsUUID()
|
||||
@ -93,7 +94,7 @@ export class FieldMetadataRelationService {
|
||||
});
|
||||
|
||||
const targetFieldMetadataToCreateWithRelation =
|
||||
await this.addCustomRelationFieldMetadataForCreation({
|
||||
this.computeCustomRelationFieldMetadataForCreation({
|
||||
fieldMetadataInput: targetFieldMetadataToCreate,
|
||||
relationCreationPayload: {
|
||||
targetObjectMetadataId: objectMetadata.id,
|
||||
@ -134,9 +135,13 @@ export class FieldMetadataRelationService {
|
||||
fieldMetadataInput,
|
||||
fieldMetadataType,
|
||||
objectMetadataMaps,
|
||||
objectMetadata,
|
||||
}: Pick<
|
||||
ValidateFieldMetadataArgs<T>,
|
||||
'fieldMetadataInput' | 'fieldMetadataType' | 'objectMetadataMaps'
|
||||
| 'fieldMetadataInput'
|
||||
| 'fieldMetadataType'
|
||||
| 'objectMetadataMaps'
|
||||
| 'objectMetadata'
|
||||
>): Promise<T> {
|
||||
// TODO: clean typings, we should try to validate both update and create inputs in the same function
|
||||
const isRelation =
|
||||
@ -150,6 +155,11 @@ export class FieldMetadataRelationService {
|
||||
.relationCreationPayload,
|
||||
)
|
||||
) {
|
||||
validateFieldNameAvailabilityOrThrow(
|
||||
`${fieldMetadataInput.name}Id`,
|
||||
objectMetadata,
|
||||
);
|
||||
|
||||
const relationCreationPayload = (
|
||||
fieldMetadataInput as unknown as CreateFieldInput
|
||||
).relationCreationPayload;
|
||||
@ -180,6 +190,24 @@ export class FieldMetadataRelationService {
|
||||
computedMetadataNameFromLabel,
|
||||
objectMetadataTarget,
|
||||
);
|
||||
|
||||
validateFieldNameAvailabilityOrThrow(
|
||||
`${computedMetadataNameFromLabel}Id`,
|
||||
objectMetadataTarget,
|
||||
);
|
||||
|
||||
if (
|
||||
computedMetadataNameFromLabel === fieldMetadataInput.name &&
|
||||
objectMetadata.id === objectMetadataTarget.id
|
||||
) {
|
||||
throw new FieldMetadataException(
|
||||
`Name "${computedMetadataNameFromLabel}" cannot be the same on both side of the relation`,
|
||||
FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
{
|
||||
userFriendlyMessage: t`Name "${computedMetadataNameFromLabel}" cannot be the same on both side of the relation`,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -285,7 +313,7 @@ export class FieldMetadataRelationService {
|
||||
});
|
||||
}
|
||||
|
||||
addCustomRelationFieldMetadataForCreation({
|
||||
computeCustomRelationFieldMetadataForCreation({
|
||||
fieldMetadataInput,
|
||||
relationCreationPayload,
|
||||
joinColumnName,
|
||||
|
||||
@ -32,6 +32,7 @@ import {
|
||||
computeColumnName,
|
||||
computeCompositeColumnName,
|
||||
} from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
|
||||
import { computeRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-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';
|
||||
@ -57,7 +58,6 @@ import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.
|
||||
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';
|
||||
import { ViewService } from 'src/modules/view/services/view.service';
|
||||
import { computeRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-relation-field-join-column-name.util';
|
||||
|
||||
@Injectable()
|
||||
export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntity> {
|
||||
@ -671,7 +671,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
|
||||
if (fieldMetadataInput.type === FieldMetadataType.RELATION) {
|
||||
const relationFieldMetadataForCreate =
|
||||
await this.fieldMetadataRelationService.addCustomRelationFieldMetadataForCreation(
|
||||
this.fieldMetadataRelationService.computeCustomRelationFieldMetadataForCreation(
|
||||
{
|
||||
fieldMetadataInput: fieldMetadataForCreate,
|
||||
relationCreationPayload: fieldMetadataInput.relationCreationPayload,
|
||||
@ -686,6 +686,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
fieldMetadataInput: relationFieldMetadataForCreate,
|
||||
fieldMetadataType: fieldMetadataForCreate.type,
|
||||
objectMetadataMaps,
|
||||
objectMetadata,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@ -12,7 +12,7 @@ import { InvalidMetadataException } from 'src/engine/metadata-modules/utils/exce
|
||||
|
||||
export const fieldMetadataGraphqlApiExceptionHandler = (error: Error) => {
|
||||
if (error instanceof InvalidMetadataException) {
|
||||
throw new UserInputError(error.message);
|
||||
throw new UserInputError(error);
|
||||
}
|
||||
|
||||
if (error instanceof FieldMetadataException) {
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
|
||||
import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
|
||||
@ -39,14 +40,17 @@ export const validateFieldNameAvailabilityOrThrow = (
|
||||
|
||||
if (
|
||||
Object.values(objectMetadata.fieldsById).some(
|
||||
(field) => field.name === name,
|
||||
(field) =>
|
||||
field.name === name ||
|
||||
(field.type === FieldMetadataType.RELATION &&
|
||||
`${field.name}Id` === name),
|
||||
)
|
||||
) {
|
||||
throw new InvalidMetadataException(
|
||||
`Name "${name}" is not available`,
|
||||
`Name "${name}" is not available as it is already used by another field`,
|
||||
InvalidMetadataExceptionCode.NOT_AVAILABLE,
|
||||
{
|
||||
userFriendlyMessage: t`This name is not available.`,
|
||||
userFriendlyMessage: t`This name is not available as it is already used by another field`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user