Fix and refactor relation field name collision validation (#15920)
# Introduction Fixes https://github.com/twentyhq/private-issues/issues/371 We weren't strictly validating relation field collision on join column name availability of the target field object ## Refactor Extracted morph or relation specific condition out of the common flat field metadata name validate availability to be located in the dedicated morph or relation flat field validator ## Tests Added two tests, ONE_TO_MANY and MANY_TO_ONE in order to cover the use case
This commit is contained in:
+7
-1
@@ -4,6 +4,7 @@ import { msg } from '@lingui/core/macro';
|
||||
import { isDefined } from 'class-validator';
|
||||
|
||||
import { FieldMetadataExceptionCode } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
|
||||
import { FlatEntityPropertiesUpdates } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-properties-updates.type';
|
||||
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';
|
||||
import { validateEnumSelectFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-enum-flat-field-metadata.util';
|
||||
@@ -14,6 +15,11 @@ const DEFAULT_NO_VALIDATION = async (): Promise<
|
||||
FlatFieldMetadataValidationError[]
|
||||
> => [];
|
||||
|
||||
export type GenericValidateFlatFieldMetadataTypeSpecificitiesArgs =
|
||||
FlatEntityValidationArgs<'fieldMetadata'> & {
|
||||
updates?: FlatEntityPropertiesUpdates<'fieldMetadata'>;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class FlatFieldMetadataTypeValidatorService {
|
||||
constructor() {}
|
||||
@@ -51,7 +57,7 @@ export class FlatFieldMetadataTypeValidatorService {
|
||||
};
|
||||
|
||||
public async validateFlatFieldMetadataTypeSpecificities(
|
||||
args: FlatEntityValidationArgs<'fieldMetadata'>,
|
||||
args: GenericValidateFlatFieldMetadataTypeSpecificitiesArgs,
|
||||
): Promise<FlatFieldMetadataValidationError[]> {
|
||||
const { flatEntityToValidate } = args;
|
||||
const fieldType = flatEntityToValidate.type;
|
||||
|
||||
+5
-2
@@ -1,11 +1,14 @@
|
||||
import { type FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { type GenericValidateFlatFieldMetadataTypeSpecificitiesArgs } from 'src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-type-validator.service';
|
||||
import { type FlatFieldMetadataValidationError } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-validation-error.type';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { type FlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-validation-args.type';
|
||||
|
||||
export type FlatFieldMetadataTypeValidationArgs<T extends FieldMetadataType> =
|
||||
Omit<FlatEntityValidationArgs<'fieldMetadata'>, 'flatEntityToValidate'> & {
|
||||
Omit<
|
||||
GenericValidateFlatFieldMetadataTypeSpecificitiesArgs,
|
||||
'flatEntityToValidate'
|
||||
> & {
|
||||
flatEntityToValidate: FlatFieldMetadata<T>;
|
||||
};
|
||||
|
||||
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
import { RelationType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
|
||||
type ObjectFieldNamesAndJoinColumnNames = {
|
||||
fieldNames: string[];
|
||||
joinColumnNames: string[];
|
||||
};
|
||||
export const getObjectFieldNamesAndJoinColumnNames = ({
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadata,
|
||||
}: {
|
||||
flatObjectMetadata: FlatObjectMetadata;
|
||||
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
|
||||
}): {
|
||||
objectFieldNamesAndJoinColumnNames: ObjectFieldNamesAndJoinColumnNames;
|
||||
} => {
|
||||
const objectFlatFieldMetadatas =
|
||||
findManyFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityMaps: flatFieldMetadataMaps,
|
||||
flatEntityIds: flatObjectMetadata.fieldMetadataIds,
|
||||
});
|
||||
const initialAccumulator: ObjectFieldNamesAndJoinColumnNames = {
|
||||
joinColumnNames: [],
|
||||
fieldNames: [],
|
||||
};
|
||||
|
||||
const objectFieldNamesAndJoinColumnNames = objectFlatFieldMetadatas.reduce(
|
||||
(acc, flatFieldMetadata) => {
|
||||
if (
|
||||
isMorphOrRelationFlatFieldMetadata(flatFieldMetadata) &&
|
||||
flatFieldMetadata.settings.relationType === RelationType.MANY_TO_ONE &&
|
||||
isDefined(flatFieldMetadata.settings.joinColumnName)
|
||||
) {
|
||||
return {
|
||||
...acc,
|
||||
fieldNames: [...acc.fieldNames, flatFieldMetadata.name],
|
||||
joinColumnNames: [
|
||||
...acc.joinColumnNames,
|
||||
flatFieldMetadata.settings.joinColumnName,
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...acc,
|
||||
fieldNames: [...acc.fieldNames, flatFieldMetadata.name],
|
||||
};
|
||||
},
|
||||
initialAccumulator,
|
||||
);
|
||||
|
||||
return { objectFieldNamesAndJoinColumnNames };
|
||||
};
|
||||
+29
-62
@@ -1,20 +1,15 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import {
|
||||
FieldMetadataType,
|
||||
compositeTypeDefinitions,
|
||||
} from 'twenty-shared/types';
|
||||
import { compositeTypeDefinitions } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { FieldMetadataExceptionCode } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
|
||||
import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
|
||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
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 { type FlatFieldMetadataValidationError } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-validation-error.type';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util';
|
||||
import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util';
|
||||
import { getObjectFieldNamesAndJoinColumnNames } from 'src/engine/metadata-modules/flat-field-metadata/utils/get-object-field-names-and-join-column-names.util';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
|
||||
const getReservedCompositeFieldNames = (
|
||||
@@ -41,15 +36,13 @@ const getReservedCompositeFieldNames = (
|
||||
};
|
||||
|
||||
export const validateFlatFieldMetadataNameAvailability = ({
|
||||
flatFieldMetadata,
|
||||
name,
|
||||
flatFieldMetadataMaps,
|
||||
remainingFlatEntityMapsToValidate,
|
||||
flatObjectMetadata,
|
||||
}: {
|
||||
flatFieldMetadata: FlatFieldMetadata;
|
||||
name: string;
|
||||
flatObjectMetadata: FlatObjectMetadata;
|
||||
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
|
||||
remainingFlatEntityMapsToValidate?: FlatEntityMaps<FlatFieldMetadata>;
|
||||
}): FlatFieldMetadataValidationError[] => {
|
||||
const errors: FlatFieldMetadataValidationError[] = [];
|
||||
const objectFlatFieldMetadatas =
|
||||
@@ -60,63 +53,37 @@ export const validateFlatFieldMetadataNameAvailability = ({
|
||||
const reservedCompositeFieldsNames = getReservedCompositeFieldNames(
|
||||
objectFlatFieldMetadatas,
|
||||
);
|
||||
const flatFieldMetadataName = flatFieldMetadata.name;
|
||||
|
||||
if (
|
||||
!isFlatFieldMetadataOfType(
|
||||
// CHALLENGE Could remove the is morph relation assertion
|
||||
flatFieldMetadata,
|
||||
FieldMetadataType.MORPH_RELATION,
|
||||
) &&
|
||||
objectFlatFieldMetadatas.some((existingFlatFieldMetadata) => {
|
||||
const firstDegreeCollision =
|
||||
existingFlatFieldMetadata.name === flatFieldMetadataName;
|
||||
|
||||
if (firstDegreeCollision) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!isMorphOrRelationFlatFieldMetadata(existingFlatFieldMetadata)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const targetFlatFieldMetadata =
|
||||
flatFieldMetadata.id ===
|
||||
existingFlatFieldMetadata.relationTargetFieldMetadataId
|
||||
? flatFieldMetadata
|
||||
: (remainingFlatEntityMapsToValidate?.byId[
|
||||
existingFlatFieldMetadata.relationTargetFieldMetadataId
|
||||
] ??
|
||||
findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityId:
|
||||
existingFlatFieldMetadata.relationTargetFieldMetadataId,
|
||||
flatEntityMaps: flatFieldMetadataMaps,
|
||||
}));
|
||||
|
||||
if (!isMorphOrRelationFlatFieldMetadata(targetFlatFieldMetadata)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
targetFlatFieldMetadata.settings.joinColumnName ===
|
||||
flatFieldMetadataName
|
||||
);
|
||||
})
|
||||
) {
|
||||
if (reservedCompositeFieldsNames.includes(name)) {
|
||||
errors.push({
|
||||
code: FieldMetadataExceptionCode.NOT_AVAILABLE,
|
||||
value: flatFieldMetadataName,
|
||||
message: `Name "${flatFieldMetadataName}" is not available as it is already used by another field`,
|
||||
userFriendlyMessage: msg`Name "${flatFieldMetadataName}" is not available as it is already used by another field`,
|
||||
code: FieldMetadataExceptionCode.RESERVED_KEYWORD,
|
||||
message: `Name "${name}" is reserved composite field name`,
|
||||
value: name,
|
||||
userFriendlyMessage: msg`Name "${name}" is not available`,
|
||||
});
|
||||
}
|
||||
|
||||
if (reservedCompositeFieldsNames.includes(flatFieldMetadataName)) {
|
||||
const { objectFieldNamesAndJoinColumnNames } =
|
||||
getObjectFieldNamesAndJoinColumnNames({
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadata,
|
||||
});
|
||||
|
||||
if (objectFieldNamesAndJoinColumnNames.fieldNames.includes(name)) {
|
||||
errors.push({
|
||||
code: FieldMetadataExceptionCode.RESERVED_KEYWORD,
|
||||
message: `Name "${flatFieldMetadataName}" is reserved composite field name`,
|
||||
value: flatFieldMetadataName,
|
||||
userFriendlyMessage: msg`Name "${flatFieldMetadataName}" is not available`,
|
||||
code: FieldMetadataExceptionCode.NOT_AVAILABLE,
|
||||
value: name,
|
||||
message: `Name "${name}" is not available as it is already used by another field`,
|
||||
userFriendlyMessage: msg`Name "${name}" is not available as it is already used by another field`,
|
||||
});
|
||||
}
|
||||
|
||||
if (objectFieldNamesAndJoinColumnNames.joinColumnNames.includes(name)) {
|
||||
errors.push({
|
||||
code: FieldMetadataExceptionCode.NOT_AVAILABLE,
|
||||
value: name,
|
||||
message: `Name "${name}" is not available as it is already used by another join column name`,
|
||||
userFriendlyMessage: msg`Name "${name}" is not available as it is already used by join column name`,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { RelationType } from 'twenty-shared/types';
|
||||
import { assertUnreachable, isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { FieldMetadataExceptionCode } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
|
||||
import { type MorphOrRelationFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/types/morph-or-relation-field-metadata-type.type';
|
||||
import { type MetadataFlatEntityAndRelatedFlatEntityMapsForValidation } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity-and-related-flat-entity-maps-for-validation.type';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { type FlatFieldMetadataValidationError } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-validation-error.type';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { validateFlatFieldMetadataNameAvailability } from 'src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-flat-field-metadata-name-availability.util';
|
||||
|
||||
export const validateMorphOrRelationFlatFieldJoinColumName = ({
|
||||
flatFieldMetadata,
|
||||
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
},
|
||||
}: {
|
||||
flatFieldMetadata: FlatFieldMetadata<MorphOrRelationFieldMetadataType>;
|
||||
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: MetadataFlatEntityAndRelatedFlatEntityMapsForValidation<'fieldMetadata'>;
|
||||
}): FlatFieldMetadataValidationError[] => {
|
||||
const errors: FlatFieldMetadataValidationError[] = [];
|
||||
|
||||
switch (flatFieldMetadata.settings.relationType) {
|
||||
case RelationType.MANY_TO_ONE: {
|
||||
if (!isDefined(flatFieldMetadata.settings.joinColumnName)) {
|
||||
errors.push({
|
||||
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
message:
|
||||
'Many to one field metadata should carry the join column name in its settings',
|
||||
userFriendlyMessage: msg`A many to one relation field should always declare a join column`,
|
||||
});
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
const flatObjectMetadata = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: flatFieldMetadata.objectMetadataId,
|
||||
flatEntityMaps: flatObjectMetadataMaps,
|
||||
});
|
||||
|
||||
if (!isDefined(flatObjectMetadata)) {
|
||||
errors.push({
|
||||
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
message: 'Could not find relation field parent flat object',
|
||||
userFriendlyMessage: msg`Could not find relation field parent flat object`,
|
||||
});
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
errors.push(
|
||||
...validateFlatFieldMetadataNameAvailability({
|
||||
name: flatFieldMetadata.settings.joinColumnName,
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadata,
|
||||
}),
|
||||
);
|
||||
break;
|
||||
}
|
||||
case RelationType.ONE_TO_MANY: {
|
||||
if (isDefined(flatFieldMetadata.settings.joinColumnName)) {
|
||||
errors.push({
|
||||
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
message:
|
||||
'One to many field metadata should not carry the join column name in its settings',
|
||||
userFriendlyMessage: msg`A one to many relation field should never declare a join column`,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
assertUnreachable(flatFieldMetadata.settings.relationType);
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
+46
-11
@@ -3,8 +3,12 @@ import { isDefined, isValidUuid } from 'twenty-shared/utils';
|
||||
|
||||
import { FieldMetadataExceptionCode } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
|
||||
import { type MorphOrRelationFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/types/morph-or-relation-field-metadata-type.type';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { type FlatFieldMetadataTypeValidationArgs } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-type-validator.type';
|
||||
import { type FlatFieldMetadataValidationError } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-validation-error.type';
|
||||
import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util';
|
||||
import { validateMorphOrRelationFlatFieldJoinColumName } from 'src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-morph-or-relation-flat-field-join-column-name.util';
|
||||
import { findFlatEntityPropertyUpdate } from 'src/engine/workspace-manager/workspace-migration-v2/utils/find-flat-entity-property-update.util';
|
||||
|
||||
export const validateMorphOrRelationFlatFieldMetadata = async ({
|
||||
flatEntityToValidate: flatFieldMetadataToValidate,
|
||||
@@ -12,6 +16,7 @@ export const validateMorphOrRelationFlatFieldMetadata = async ({
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
},
|
||||
updates,
|
||||
remainingFlatEntityMapsToValidate,
|
||||
}: FlatFieldMetadataTypeValidationArgs<MorphOrRelationFieldMetadataType>): Promise<
|
||||
FlatFieldMetadataValidationError[]
|
||||
@@ -39,25 +44,29 @@ export const validateMorphOrRelationFlatFieldMetadata = async ({
|
||||
|
||||
const errors: FlatFieldMetadataValidationError[] = [];
|
||||
|
||||
const targetRelationFlatObjectMetadata =
|
||||
flatObjectMetadataMaps?.byId[relationTargetObjectMetadataId];
|
||||
const targetFlatObjectMetadata = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: relationTargetObjectMetadataId,
|
||||
flatEntityMaps: flatObjectMetadataMaps,
|
||||
});
|
||||
|
||||
if (!isDefined(targetRelationFlatObjectMetadata)) {
|
||||
const targetFlatFieldMetadata =
|
||||
remainingFlatEntityMapsToValidate?.byId[relationTargetFieldMetadataId] ??
|
||||
findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: relationTargetFieldMetadataId,
|
||||
flatEntityMaps: flatFieldMetadataMaps,
|
||||
});
|
||||
|
||||
if (!isDefined(targetFlatObjectMetadata)) {
|
||||
errors.push({
|
||||
code: FieldMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND,
|
||||
message: 'Relation target object metadata not found',
|
||||
userFriendlyMessage: msg`Object targeted by the relation not found`,
|
||||
});
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
const targetRelationFlatFieldMetadata =
|
||||
remainingFlatEntityMapsToValidate?.byId[relationTargetFieldMetadataId] ??
|
||||
flatFieldMetadataMaps.byId[relationTargetFieldMetadataId];
|
||||
|
||||
if (
|
||||
isDefined(targetRelationFlatObjectMetadata) &&
|
||||
!isDefined(targetRelationFlatFieldMetadata)
|
||||
) {
|
||||
if (!isDefined(targetFlatFieldMetadata)) {
|
||||
errors.push({
|
||||
code: FieldMetadataExceptionCode.FIELD_METADATA_NOT_FOUND,
|
||||
message: isDefined(remainingFlatEntityMapsToValidate)
|
||||
@@ -65,7 +74,33 @@ export const validateMorphOrRelationFlatFieldMetadata = async ({
|
||||
: 'Relation field target metadata not found',
|
||||
userFriendlyMessage: msg`Relation field target metadata not found`,
|
||||
});
|
||||
} else if (!isMorphOrRelationFlatFieldMetadata(targetFlatFieldMetadata)) {
|
||||
errors.push({
|
||||
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
message:
|
||||
'A relation field can only target a MORPH_RELATION or another RELATION field',
|
||||
userFriendlyMessage: msg`Invalid relation field target`,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
!isDefined(updates) ||
|
||||
isDefined(
|
||||
findFlatEntityPropertyUpdate({
|
||||
flatEntityUpdates: updates,
|
||||
property: 'settings',
|
||||
}),
|
||||
)
|
||||
)
|
||||
errors.push(
|
||||
...validateMorphOrRelationFlatFieldJoinColumName({
|
||||
flatFieldMetadata: flatFieldMetadataToValidate,
|
||||
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
+3
-3
@@ -131,7 +131,7 @@ export class FlatFieldMetadataValidatorService {
|
||||
validationResult.errors.push(
|
||||
...validateFlatFieldMetadataName(flatFieldMetadataToValidate.name),
|
||||
...validateFlatFieldMetadataNameAvailability({
|
||||
flatFieldMetadata: flatFieldMetadataToValidate,
|
||||
name: flatFieldMetadataToValidate.name,
|
||||
flatFieldMetadataMaps: optimisticFlatFieldMetadataMaps,
|
||||
flatObjectMetadata,
|
||||
}),
|
||||
@@ -152,6 +152,7 @@ export class FlatFieldMetadataValidatorService {
|
||||
const fieldMetadataTypeValidationErrors =
|
||||
await this.flatFieldMetadataTypeValidatorService.validateFlatFieldMetadataTypeSpecificities(
|
||||
{
|
||||
updates,
|
||||
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
|
||||
flatFieldMetadataMaps: optimisticFlatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
@@ -313,9 +314,8 @@ export class FlatFieldMetadataValidatorService {
|
||||
|
||||
validationResult.errors.push(
|
||||
...validateFlatFieldMetadataNameAvailability({
|
||||
flatFieldMetadata: flatFieldMetadataToValidate,
|
||||
name: flatFieldMetadataToValidate.name,
|
||||
flatFieldMetadataMaps: optimisticFlatFieldMetadataMaps,
|
||||
remainingFlatEntityMapsToValidate,
|
||||
flatObjectMetadata: parentFlatObjectMetadata,
|
||||
}),
|
||||
);
|
||||
|
||||
+2
-2
@@ -117,8 +117,8 @@ exports[`failing createOne FieldMetadataService morph relation fields v2 Morh re
|
||||
"errors": [
|
||||
{
|
||||
"code": "NOT_AVAILABLE",
|
||||
"message": "Name "fieldNamePersonForMorphRelationSecondId" is not available as it is already used by another field",
|
||||
"userFriendlyMessage": "Name "fieldNamePersonForMorphRelationSecondId" is not available as it is already used by another field",
|
||||
"message": "Name "fieldNamePersonForMorphRelationSecondId" is not available as it is already used by another join column name",
|
||||
"userFriendlyMessage": "Name "fieldNamePersonForMorphRelationSecondId" is not available as it is already used by join column name",
|
||||
"value": "fieldNamePersonForMorphRelationSecondId",
|
||||
},
|
||||
],
|
||||
|
||||
+1
-1
@@ -487,7 +487,7 @@ describe('failing createOne FieldMetadataService morph relation fields v2', () =
|
||||
input: {
|
||||
label: 'colliding field label',
|
||||
name: `fieldNamePersonForMorphRelationSecondId`,
|
||||
objectMetadataId: createdObjectMetadataPersonId,
|
||||
objectMetadataId: createdObjectMetadataCompanyId,
|
||||
type: FieldMetadataType.TEXT,
|
||||
},
|
||||
expectToFail: true,
|
||||
|
||||
+178
@@ -1455,3 +1455,181 @@ exports[`Field metadata relation creation should fail should fail when creating
|
||||
"name": "GraphQLError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Field metadata relation creation should fail should fail when creating a relation field with name that conflicts with existing field name 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "METADATA_VALIDATION_FAILED",
|
||||
"errors": {
|
||||
"cronTrigger": [],
|
||||
"databaseEventTrigger": [],
|
||||
"fieldMetadata": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "NOT_AVAILABLE",
|
||||
"message": "Name "fieldName" is not available as it is already used by another field",
|
||||
"userFriendlyMessage": "Name "fieldName" is not available as it is already used by another field",
|
||||
"value": "fieldName",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"id": Any<String>,
|
||||
"name": "fieldName",
|
||||
"objectMetadataId": Any<String>,
|
||||
},
|
||||
"status": "fail",
|
||||
"type": "create_field",
|
||||
},
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "FIELD_METADATA_NOT_FOUND",
|
||||
"message": "Relation field target metadata not found in both existing and about to be created field metadatas",
|
||||
"userFriendlyMessage": "Relation field target metadata not found",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"id": Any<String>,
|
||||
"name": "relatedObject",
|
||||
"objectMetadataId": Any<String>,
|
||||
},
|
||||
"status": "fail",
|
||||
"type": "create_field",
|
||||
},
|
||||
],
|
||||
"index": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"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<String>,
|
||||
"name": "IDX_7ee6c7ff1e4648b0665f60cd5e4",
|
||||
},
|
||||
"status": "fail",
|
||||
"type": "create_index",
|
||||
},
|
||||
],
|
||||
"objectMetadata": [],
|
||||
"routeTrigger": [],
|
||||
"serverlessFunction": [],
|
||||
"view": [],
|
||||
"viewField": [],
|
||||
"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)",
|
||||
},
|
||||
"message": "Multiple validation errors occurred while creating fields",
|
||||
"name": "GraphQLError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Field metadata relation creation should fail should fail when creating a relation field with name that conflicts with existing join column name 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "METADATA_VALIDATION_FAILED",
|
||||
"errors": {
|
||||
"cronTrigger": [],
|
||||
"databaseEventTrigger": [],
|
||||
"fieldMetadata": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "NOT_AVAILABLE",
|
||||
"message": "Name "companyId" is not available as it is already used by another join column name",
|
||||
"userFriendlyMessage": "Name "companyId" is not available as it is already used by join column name",
|
||||
"value": "companyId",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"id": Any<String>,
|
||||
"name": "companyId",
|
||||
"objectMetadataId": Any<String>,
|
||||
},
|
||||
"status": "fail",
|
||||
"type": "create_field",
|
||||
},
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "FIELD_METADATA_NOT_FOUND",
|
||||
"message": "Relation field target metadata not found in both existing and about to be created field metadatas",
|
||||
"userFriendlyMessage": "Relation field target metadata not found",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"id": Any<String>,
|
||||
"name": "related",
|
||||
"objectMetadataId": Any<String>,
|
||||
},
|
||||
"status": "fail",
|
||||
"type": "create_field",
|
||||
},
|
||||
],
|
||||
"index": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"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<String>,
|
||||
"name": "IDX_ed8240d8bc1e69a39dab1c17fe2",
|
||||
},
|
||||
"status": "fail",
|
||||
"type": "create_index",
|
||||
},
|
||||
],
|
||||
"objectMetadata": [],
|
||||
"routeTrigger": [],
|
||||
"serverlessFunction": [],
|
||||
"view": [],
|
||||
"viewField": [],
|
||||
"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)",
|
||||
},
|
||||
"message": "Multiple validation errors occurred while creating fields",
|
||||
"name": "GraphQLError",
|
||||
}
|
||||
`;
|
||||
|
||||
+75
-4
@@ -1,13 +1,16 @@
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.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 { 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 { extractRecordIdsAndDatesAsExpectAny } from 'test/utils/extract-record-ids-and-dates-as-expect-any';
|
||||
import { type EachTestingContext } from 'twenty-shared/testing';
|
||||
import {
|
||||
eachTestingContextFilter,
|
||||
type EachTestingContext,
|
||||
} from 'twenty-shared/testing';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util';
|
||||
|
||||
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
||||
|
||||
@@ -252,7 +255,7 @@ describe('Field metadata relation creation should fail', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it.each(failingLabelsCreationTestsUseCase)(
|
||||
it.each(eachTestingContextFilter(failingLabelsCreationTestsUseCase))(
|
||||
'relation ONE_TO_MANY $title',
|
||||
async ({ context }) => {
|
||||
const computedRelationCreationPayload =
|
||||
@@ -291,7 +294,7 @@ describe('Field metadata relation creation should fail', () => {
|
||||
},
|
||||
);
|
||||
|
||||
it.each(failingLabelsCreationTestsUseCase)(
|
||||
it.each(eachTestingContextFilter(failingLabelsCreationTestsUseCase))(
|
||||
'relation MANY_TO_ONE $title',
|
||||
async ({ context }) => {
|
||||
const computedRelationCreationPayload =
|
||||
@@ -376,4 +379,72 @@ describe('Field metadata relation creation should fail', () => {
|
||||
errors,
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail when creating a relation field with name that conflicts with existing field name', async () => {
|
||||
const { errors } = await createOneFieldMetadata({
|
||||
expectToFail: true,
|
||||
input: {
|
||||
objectMetadataId: globalTestContext.objectMetadataIds.sourceObjectId,
|
||||
name: globalTestContext.collisionFieldName,
|
||||
label: 'Duplicate Field',
|
||||
isLabelSyncedWithName: false,
|
||||
type: FieldMetadataType.RELATION,
|
||||
relationCreationPayload: {
|
||||
targetFieldLabel: 'Related Object',
|
||||
type: RelationType.MANY_TO_ONE,
|
||||
targetObjectMetadataId:
|
||||
globalTestContext.objectMetadataIds.targetObjectId,
|
||||
targetFieldIcon: 'IconLink',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expectOneNotInternalServerErrorSnapshot({
|
||||
errors,
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail when creating a relation field with name that conflicts with existing join column name', async () => {
|
||||
const relationFieldName = 'company';
|
||||
|
||||
await createOneFieldMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
objectMetadataId: globalTestContext.objectMetadataIds.sourceObjectId,
|
||||
name: relationFieldName,
|
||||
label: 'Company',
|
||||
isLabelSyncedWithName: false,
|
||||
type: FieldMetadataType.RELATION,
|
||||
relationCreationPayload: {
|
||||
targetFieldLabel: 'company',
|
||||
type: RelationType.MANY_TO_ONE,
|
||||
targetObjectMetadataId:
|
||||
globalTestContext.objectMetadataIds.targetObjectId,
|
||||
targetFieldIcon: 'IconBuilding',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { errors } = await createOneFieldMetadata({
|
||||
expectToFail: true,
|
||||
input: {
|
||||
objectMetadataId: globalTestContext.objectMetadataIds.sourceObjectId,
|
||||
name: `${relationFieldName}Id`,
|
||||
label: 'Company Id Field',
|
||||
isLabelSyncedWithName: false,
|
||||
type: FieldMetadataType.RELATION,
|
||||
relationCreationPayload: {
|
||||
targetFieldLabel: 'Related',
|
||||
type: RelationType.MANY_TO_ONE,
|
||||
targetObjectMetadataId:
|
||||
globalTestContext.objectMetadataIds.targetObjectId,
|
||||
targetFieldIcon: 'IconLink',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expectOneNotInternalServerErrorSnapshot({
|
||||
errors,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user