Fix user friendly message for non available object name (#14210)
Fixes https://github.com/twentyhq/twenty/issues/13577. When naming or renaming an object, we are already checking that the name was available compared to other existing objects. But we also need to check that the relation fields that will be created or updated are available as well: we create relation fields on Attachment, Note, Favorite, Task and TimelineActivites, that bear the name of the object. Thus, it is not possible to create an object that has the same name as one of the fields on Attachment, Note etc: name, createdAt, ... are not valid names.
This commit is contained in:
+3
-3
@@ -160,7 +160,7 @@ export class FieldMetadataRelationService {
|
||||
) {
|
||||
validateFieldNameAvailabilityOrThrow({
|
||||
name: `${fieldMetadataInput.name}Id`,
|
||||
objectMetadata,
|
||||
fieldMetadataMapById: objectMetadata.fieldsById,
|
||||
});
|
||||
|
||||
const relationCreationPayload = (
|
||||
@@ -189,12 +189,12 @@ export class FieldMetadataRelationService {
|
||||
|
||||
validateFieldNameAvailabilityOrThrow({
|
||||
name: computedMetadataNameFromLabel,
|
||||
objectMetadata: objectMetadataTarget,
|
||||
fieldMetadataMapById: objectMetadataTarget.fieldsById,
|
||||
});
|
||||
|
||||
validateFieldNameAvailabilityOrThrow({
|
||||
name: `${computedMetadataNameFromLabel}Id`,
|
||||
objectMetadata: objectMetadataTarget,
|
||||
fieldMetadataMapById: objectMetadataTarget.fieldsById,
|
||||
});
|
||||
|
||||
if (
|
||||
|
||||
+1
-1
@@ -170,7 +170,7 @@ export class FieldMetadataValidationService {
|
||||
try {
|
||||
validateFieldNameAvailabilityOrThrow({
|
||||
name: fieldMetadataInput.name,
|
||||
objectMetadata,
|
||||
fieldMetadataMapById: objectMetadata.fieldsById,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof InvalidMetadataException) {
|
||||
|
||||
+1
@@ -10,4 +10,5 @@ export enum ObjectMetadataExceptionCode {
|
||||
MISSING_CUSTOM_OBJECT_DEFAULT_LABEL_IDENTIFIER_FIELD = 'MISSING_CUSTOM_OBJECT_DEFAULT_LABEL_IDENTIFIER_FIELD',
|
||||
INVALID_ORM_OUTPUT = 'INVALID_ORM_OUTPUT',
|
||||
INTERNAL_SERVER_ERROR = 'INTERNAL_SERVER_ERROR',
|
||||
NAME_CONFLICT = 'NAME_CONFLICT',
|
||||
}
|
||||
|
||||
+34
-13
@@ -40,6 +40,7 @@ import {
|
||||
} 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';
|
||||
@@ -221,10 +222,18 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
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(
|
||||
createObjectInput.workspaceId,
|
||||
createdObjectMetadata,
|
||||
{ ...createdObjectMetadata, fieldsById },
|
||||
objectMetadataMaps,
|
||||
queryRunner,
|
||||
);
|
||||
@@ -374,12 +383,14 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
});
|
||||
|
||||
const { didUpdateLabelOrIcon } =
|
||||
await this.handleObjectNameAndLabelUpdates(
|
||||
await this.handleObjectNameAndLabelUpdates({
|
||||
existingObjectMetadata,
|
||||
existingObjectMetadataCombinedWithUpdateInput,
|
||||
objectMetadataForUpdate:
|
||||
existingObjectMetadataCombinedWithUpdateInput,
|
||||
inputPayload,
|
||||
queryRunner,
|
||||
);
|
||||
objectMetadataMaps,
|
||||
});
|
||||
|
||||
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrationsWithinTransaction(
|
||||
workspaceId,
|
||||
@@ -602,11 +613,17 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
await this.objectMetadataRepository.delete({ workspaceId });
|
||||
}
|
||||
|
||||
private async handleObjectNameAndLabelUpdates(
|
||||
private async handleObjectNameAndLabelUpdates({
|
||||
existingObjectMetadata,
|
||||
objectMetadataForUpdate,
|
||||
inputPayload,
|
||||
queryRunner,
|
||||
objectMetadataMaps,
|
||||
}: {
|
||||
existingObjectMetadata: Pick<
|
||||
ObjectMetadataItemWithFieldMaps,
|
||||
'nameSingular' | 'isCustom' | 'id' | 'labelPlural' | 'icon' | 'fieldsById'
|
||||
>,
|
||||
>;
|
||||
objectMetadataForUpdate: Pick<
|
||||
ObjectMetadataItemWithFieldMaps,
|
||||
| 'nameSingular'
|
||||
@@ -617,10 +634,11 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
| 'labelPlural'
|
||||
| 'icon'
|
||||
| 'fieldsById'
|
||||
>,
|
||||
inputPayload: UpdateObjectPayload,
|
||||
queryRunner: QueryRunner,
|
||||
): Promise<{ didUpdateLabelOrIcon: boolean }> {
|
||||
>;
|
||||
inputPayload: UpdateObjectPayload;
|
||||
queryRunner: QueryRunner;
|
||||
objectMetadataMaps: ObjectMetadataMaps;
|
||||
}): Promise<{ didUpdateLabelOrIcon: boolean }> {
|
||||
const newTargetTableName = computeObjectTargetTable(
|
||||
objectMetadataForUpdate,
|
||||
);
|
||||
@@ -638,9 +656,12 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
|
||||
const relationMetadataCollection =
|
||||
await this.objectMetadataFieldRelationService.updateRelationsAndForeignKeysMetadata(
|
||||
objectMetadataForUpdate.workspaceId,
|
||||
objectMetadataForUpdate,
|
||||
queryRunner,
|
||||
{
|
||||
workspaceId: objectMetadataForUpdate.workspaceId,
|
||||
updatedObjectMetadata: objectMetadataForUpdate,
|
||||
queryRunner,
|
||||
objectMetadataMaps,
|
||||
},
|
||||
);
|
||||
|
||||
await this.objectMetadataMigrationService.updateRelationMigrations(
|
||||
|
||||
+123
-31
@@ -21,8 +21,11 @@ import {
|
||||
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 { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-metadata/relation-on-delete-action.type';
|
||||
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,
|
||||
@@ -39,6 +42,11 @@ export const DEFAULT_RELATIONS_OBJECTS_STANDARD_IDS = [
|
||||
STANDARD_OBJECT_IDS.taskTarget,
|
||||
];
|
||||
|
||||
type PartialRelationFieldMetadata = Partial<
|
||||
FieldMetadataEntity<FieldMetadataType.RELATION>
|
||||
> &
|
||||
Required<Pick<FieldMetadataEntity<FieldMetadataType.RELATION>, 'name'>>;
|
||||
|
||||
@Injectable()
|
||||
export class ObjectMetadataFieldRelationService {
|
||||
constructor(
|
||||
@@ -52,7 +60,7 @@ export class ObjectMetadataFieldRelationService {
|
||||
workspaceId: string,
|
||||
sourceObjectMetadata: Pick<
|
||||
ObjectMetadataItemWithFieldMaps,
|
||||
'id' | 'nameSingular' | 'labelSingular'
|
||||
'id' | 'nameSingular' | 'labelSingular' | 'fieldsById'
|
||||
>,
|
||||
objectMetadataMaps: ObjectMetadataMaps,
|
||||
queryRunner?: QueryRunner,
|
||||
@@ -83,8 +91,9 @@ export class ObjectMetadataFieldRelationService {
|
||||
workspaceId: string;
|
||||
sourceObjectMetadata: Pick<
|
||||
ObjectMetadataItemWithFieldMaps,
|
||||
'id' | 'nameSingular' | 'labelSingular'
|
||||
'id' | 'nameSingular' | 'labelSingular' | 'fieldsById'
|
||||
>;
|
||||
|
||||
objectMetadataMaps: ObjectMetadataMaps;
|
||||
relationObjectMetadataStandardId: string;
|
||||
queryRunner?: QueryRunner;
|
||||
@@ -116,7 +125,7 @@ export class ObjectMetadataFieldRelationService {
|
||||
workspaceId: string,
|
||||
sourceObjectMetadata: Pick<
|
||||
ObjectMetadataItemWithFieldMaps,
|
||||
'id' | 'nameSingular' | 'labelSingular'
|
||||
'id' | 'nameSingular' | 'labelSingular' | 'fieldsById'
|
||||
>,
|
||||
targetObjectMetadata: ObjectMetadataItemWithFieldMaps,
|
||||
queryRunner?: QueryRunner,
|
||||
@@ -127,12 +136,22 @@ export class ObjectMetadataFieldRelationService {
|
||||
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;
|
||||
@@ -159,16 +178,22 @@ export class ObjectMetadataFieldRelationService {
|
||||
]);
|
||||
}
|
||||
|
||||
public async updateRelationsAndForeignKeysMetadata(
|
||||
workspaceId: string,
|
||||
public async updateRelationsAndForeignKeysMetadata({
|
||||
workspaceId,
|
||||
updatedObjectMetadata,
|
||||
objectMetadataMaps,
|
||||
queryRunner,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
updatedObjectMetadata: Pick<
|
||||
ObjectMetadataEntity,
|
||||
'nameSingular' | 'isCustom' | 'id' | 'labelSingular'
|
||||
>,
|
||||
queryRunner?: QueryRunner,
|
||||
): Promise<
|
||||
>;
|
||||
objectMetadataMaps: ObjectMetadataMaps;
|
||||
queryRunner?: QueryRunner;
|
||||
}): Promise<
|
||||
{
|
||||
targetObjectMetadata: ObjectMetadataEntity;
|
||||
targetObjectMetadata: ObjectMetadataItemWithFieldMaps;
|
||||
targetFieldMetadata: FieldMetadataEntity;
|
||||
sourceFieldMetadata: FieldMetadataEntity;
|
||||
}[]
|
||||
@@ -176,39 +201,61 @@ export class ObjectMetadataFieldRelationService {
|
||||
return await Promise.all(
|
||||
DEFAULT_RELATIONS_OBJECTS_STANDARD_IDS.map(
|
||||
async (relationObjectMetadataStandardId) =>
|
||||
this.updateRelationAndForeignKeyMetadata(
|
||||
this.updateRelationAndForeignKeyMetadata({
|
||||
workspaceId,
|
||||
updatedObjectMetadata,
|
||||
relationObjectMetadataStandardId,
|
||||
sourceObjectMetadata: updatedObjectMetadata,
|
||||
targetObjectMetadataStandardId: relationObjectMetadataStandardId,
|
||||
objectMetadataMaps,
|
||||
queryRunner,
|
||||
),
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private async updateRelationAndForeignKeyMetadata(
|
||||
workspaceId: string,
|
||||
private async updateRelationAndForeignKeyMetadata({
|
||||
workspaceId,
|
||||
sourceObjectMetadata,
|
||||
targetObjectMetadataStandardId,
|
||||
objectMetadataMaps,
|
||||
queryRunner,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
sourceObjectMetadata: Pick<
|
||||
ObjectMetadataEntity,
|
||||
'nameSingular' | 'id' | 'isCustom' | 'labelSingular'
|
||||
>,
|
||||
targetObjectMetadataStandardId: string,
|
||||
queryRunner?: QueryRunner,
|
||||
) {
|
||||
const objectMetadataRepository = queryRunner
|
||||
? queryRunner.manager.getRepository(ObjectMetadataEntity)
|
||||
: this.objectMetadataRepository;
|
||||
>;
|
||||
targetObjectMetadataStandardId: string;
|
||||
objectMetadataMaps: ObjectMetadataMaps;
|
||||
queryRunner?: QueryRunner;
|
||||
}): Promise<{
|
||||
targetObjectMetadata: ObjectMetadataItemWithFieldMaps;
|
||||
targetFieldMetadata: FieldMetadataEntity;
|
||||
sourceFieldMetadata: FieldMetadataEntity;
|
||||
}> {
|
||||
const fieldMetadataRepository = queryRunner
|
||||
? queryRunner.manager.getRepository(FieldMetadataEntity)
|
||||
: this.fieldMetadataRepository;
|
||||
|
||||
const targetObjectMetadata = await objectMetadataRepository.findOneByOrFail(
|
||||
{
|
||||
standardId: targetObjectMetadataStandardId,
|
||||
workspaceId: workspaceId,
|
||||
isCustom: false,
|
||||
},
|
||||
);
|
||||
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,
|
||||
@@ -226,6 +273,25 @@ export class ObjectMetadataFieldRelationService {
|
||||
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<FieldMetadataType.RELATION>
|
||||
@@ -293,7 +359,7 @@ export class ObjectMetadataFieldRelationService {
|
||||
ObjectMetadataItemWithFieldMaps,
|
||||
'namePlural' | 'labelSingular'
|
||||
>,
|
||||
): Partial<FieldMetadataEntity<FieldMetadataType.RELATION>> {
|
||||
): PartialRelationFieldMetadata {
|
||||
const relationObjectMetadataNamePlural = targetObjectMetadata.namePlural;
|
||||
|
||||
const { description } = buildDescriptionForRelationFieldMetadataOnFromField(
|
||||
@@ -353,7 +419,7 @@ export class ObjectMetadataFieldRelationService {
|
||||
ObjectMetadataItemWithFieldMaps,
|
||||
'namePlural' | 'labelSingular' | 'id' | 'nameSingular'
|
||||
>,
|
||||
): Partial<FieldMetadataEntity<FieldMetadataType.RELATION>> {
|
||||
): PartialRelationFieldMetadata {
|
||||
const customStandardFieldId =
|
||||
// @ts-expect-error legacy noImplicitAny
|
||||
STANDARD_OBJECT_FIELD_IDS[targetObjectMetadata.nameSingular].custom;
|
||||
@@ -524,4 +590,30 @@ export class ObjectMetadataFieldRelationService {
|
||||
|
||||
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: `Name "${name}" is not available.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -142,7 +142,7 @@ export class ObjectMetadataMigrationService {
|
||||
currentObjectMetadata: Pick<ObjectMetadataEntity, 'nameSingular'>,
|
||||
alteredObjectMetadata: Pick<ObjectMetadataEntity, 'nameSingular'>,
|
||||
relationMetadataCollection: {
|
||||
targetObjectMetadata: ObjectMetadataEntity;
|
||||
targetObjectMetadata: ObjectMetadataItemWithFieldMaps;
|
||||
targetFieldMetadata: FieldMetadataEntity;
|
||||
sourceFieldMetadata: FieldMetadataEntity;
|
||||
}[],
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ import {
|
||||
|
||||
export const buildDefaultFieldsForCustomObject = (
|
||||
workspaceId: string,
|
||||
): Partial<FieldMetadataEntity>[] => [
|
||||
): Array<Partial<FieldMetadataEntity> & { id: string }> => [
|
||||
{
|
||||
id: v4(),
|
||||
standardId: BASE_OBJECT_STANDARD_FIELD_IDS.id,
|
||||
|
||||
+1
@@ -31,6 +31,7 @@ export const objectMetadataGraphqlApiExceptionHandler = (error: Error) => {
|
||||
case ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT:
|
||||
throw new UserInputError(error);
|
||||
case ObjectMetadataExceptionCode.OBJECT_MUTATION_NOT_ALLOWED:
|
||||
case ObjectMetadataExceptionCode.NAME_CONFLICT:
|
||||
throw new ForbiddenError(error);
|
||||
case ObjectMetadataExceptionCode.OBJECT_ALREADY_EXISTS:
|
||||
throw new ConflictError(error);
|
||||
|
||||
+2
-2
@@ -64,14 +64,14 @@ describe('validateFieldNameAvailabilityOrThrow', () => {
|
||||
expect(() =>
|
||||
validateFieldNameAvailabilityOrThrow({
|
||||
name: input,
|
||||
objectMetadata: objectMetadataMapItemMock,
|
||||
fieldMetadataMapById: objectMetadataMapItemMock.fieldsById,
|
||||
}),
|
||||
).not.toThrow();
|
||||
} else {
|
||||
expect(() =>
|
||||
validateFieldNameAvailabilityOrThrow({
|
||||
name: input,
|
||||
objectMetadata: objectMetadataMapItemMock,
|
||||
fieldMetadataMapById: objectMetadataMapItemMock.fieldsById,
|
||||
}),
|
||||
).toThrowErrorMatchingSnapshot();
|
||||
}
|
||||
|
||||
+8
-8
@@ -4,18 +4,18 @@ 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';
|
||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import { type FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map';
|
||||
import {
|
||||
InvalidMetadataException,
|
||||
InvalidMetadataExceptionCode,
|
||||
} from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception';
|
||||
|
||||
const getReservedCompositeFieldNames = (
|
||||
objectMetadata: ObjectMetadataItemWithFieldMaps,
|
||||
fieldMetadataMapById: FieldMetadataMap,
|
||||
) => {
|
||||
const reservedCompositeFieldsNames: string[] = [];
|
||||
|
||||
for (const field of Object.values(objectMetadata.fieldsById)) {
|
||||
for (const field of Object.values(fieldMetadataMapById)) {
|
||||
if (isCompositeFieldMetadataType(field.type)) {
|
||||
const base = field.name;
|
||||
const compositeType = compositeTypeDefinitions.get(field.type);
|
||||
@@ -33,17 +33,17 @@ const getReservedCompositeFieldNames = (
|
||||
|
||||
type ValidateFieldNameAvailabilityOrThrowArgs = {
|
||||
name: string;
|
||||
objectMetadata: ObjectMetadataItemWithFieldMaps;
|
||||
fieldMetadataMapById: FieldMetadataMap;
|
||||
};
|
||||
export const validateFieldNameAvailabilityOrThrow = ({
|
||||
name,
|
||||
objectMetadata,
|
||||
fieldMetadataMapById,
|
||||
}: ValidateFieldNameAvailabilityOrThrowArgs) => {
|
||||
const reservedCompositeFieldsNames =
|
||||
getReservedCompositeFieldNames(objectMetadata);
|
||||
getReservedCompositeFieldNames(fieldMetadataMapById);
|
||||
|
||||
if (
|
||||
Object.values(objectMetadata.fieldsById).some(
|
||||
Object.values(fieldMetadataMapById).some(
|
||||
(field) =>
|
||||
field.name === name ||
|
||||
(field.type === FieldMetadataType.RELATION &&
|
||||
@@ -54,7 +54,7 @@ export const validateFieldNameAvailabilityOrThrow = ({
|
||||
`Name "${name}" is not available as it is already used by another field`,
|
||||
InvalidMetadataExceptionCode.NOT_AVAILABLE,
|
||||
{
|
||||
userFriendlyMessage: t`This name is not available as it is already used by another field`,
|
||||
userFriendlyMessage: t`This name is not available as it is already used by another field.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
+2
-2
@@ -6,7 +6,7 @@ exports[`Field metadata morph relation creation should fail relation MANY_TO_ONE
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "Name not available",
|
||||
"userFriendlyMessage": "This name is not available as it is already used by another field",
|
||||
"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",
|
||||
@@ -103,7 +103,7 @@ exports[`Field metadata morph relation creation should fail relation ONE_TO_MANY
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "Name not available",
|
||||
"userFriendlyMessage": "This name is not available as it is already used by another field",
|
||||
"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",
|
||||
|
||||
+4
-4
@@ -6,7 +6,7 @@ exports[`Field metadata relation creation should fail relation MANY_TO_ONE (rela
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "Name not available",
|
||||
"userFriendlyMessage": "This name is not available as it is already used by another field",
|
||||
"userFriendlyMessage": "This name is not available as it is already used by another field.",
|
||||
},
|
||||
"message": "Name "fieldNameBisId" is not available as it is already used by another field",
|
||||
"name": "UserInputError",
|
||||
@@ -20,7 +20,7 @@ exports[`Field metadata relation creation should fail relation MANY_TO_ONE (rela
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "Name not available",
|
||||
"userFriendlyMessage": "This name is not available as it is already used by another field",
|
||||
"userFriendlyMessage": "This name is not available as it is already used by another field.",
|
||||
},
|
||||
"message": "Name "fieldName" is not available as it is already used by another field",
|
||||
"name": "UserInputError",
|
||||
@@ -145,7 +145,7 @@ exports[`Field metadata relation creation should fail relation ONE_TO_MANY (rela
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "Name not available",
|
||||
"userFriendlyMessage": "This name is not available as it is already used by another field",
|
||||
"userFriendlyMessage": "This name is not available as it is already used by another field.",
|
||||
},
|
||||
"message": "Name "fieldNameBisId" is not available as it is already used by another field",
|
||||
"name": "UserInputError",
|
||||
@@ -159,7 +159,7 @@ exports[`Field metadata relation creation should fail relation ONE_TO_MANY (rela
|
||||
"extensions": {
|
||||
"code": "BAD_USER_INPUT",
|
||||
"subCode": "Name not available",
|
||||
"userFriendlyMessage": "This name is not available as it is already used by another field",
|
||||
"userFriendlyMessage": "This name is not available as it is already used by another field.",
|
||||
},
|
||||
"message": "Name "fieldName" is not available as it is already used by another field",
|
||||
"name": "UserInputError",
|
||||
|
||||
+27
-1
@@ -188,7 +188,33 @@ describe('Custom object renaming', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('3. should delete custom object', async () => {
|
||||
it('3. should fail when trying to rename object to "name"', async () => {
|
||||
// Act & Assert
|
||||
const { errors } = await updateOneObjectMetadata({
|
||||
gqlFields: `
|
||||
nameSingular
|
||||
labelSingular
|
||||
namePlural
|
||||
labelPlural
|
||||
`,
|
||||
input: {
|
||||
idToUpdate: listingObjectId,
|
||||
updatePayload: {
|
||||
nameSingular: 'name',
|
||||
},
|
||||
},
|
||||
expectToFail: true,
|
||||
});
|
||||
|
||||
// Assert that an error was thrown
|
||||
expect(errors).toBeDefined();
|
||||
expect(errors).toHaveLength(1);
|
||||
expect(errors[0].extensions.userFriendlyMessage).toContain(
|
||||
'Name "name" is not available',
|
||||
);
|
||||
});
|
||||
|
||||
it('4. should delete custom object', async () => {
|
||||
const { data } = await deleteOneObjectMetadata({
|
||||
input: {
|
||||
idToDelete: listingObjectId,
|
||||
|
||||
Reference in New Issue
Block a user