Files
twenty/packages/twenty-server/src/engine/metadata-modules/field-metadata/hooks/before-update-one-field.hook.ts
2025-07-08 12:23:28 +02:00

419 lines
11 KiB
TypeScript

import { Injectable } from '@nestjs/common';
import { i18n } from '@lingui/core';
import {
BeforeUpdateOneHook,
UpdateOneInputType,
} from '@ptc-org/nestjs-query-graphql';
import { APP_LOCALES, SOURCE_LOCALE } 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 { FieldStandardOverridesDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-standard-overrides.dto';
import { 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 { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata.service';
interface StandardFieldUpdate extends Partial<UpdateFieldInput> {
standardOverrides?: FieldStandardOverridesDTO;
}
@Injectable()
export class BeforeUpdateOneField<T extends UpdateFieldInput>
implements BeforeUpdateOneHook<T>
{
constructor(readonly fieldMetadataService: FieldMetadataService) {}
async run(
instance: UpdateOneInputType<T>,
{
workspaceId,
locale,
}: {
workspaceId: string;
locale: keyof typeof APP_LOCALES | undefined;
},
): Promise<UpdateOneInputType<T>> {
if (!workspaceId) {
throw new ForbiddenError('Could not retrieve workspace ID');
}
const fieldMetadata = await this.getFieldMetadata(instance, workspaceId);
if (!fieldMetadata.isCustom) {
return this.handleStandardFieldUpdate(instance, fieldMetadata, locale);
}
return instance;
}
private async getFieldMetadata(
instance: UpdateOneInputType<T>,
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 handleStandardFieldUpdate(
instance: UpdateOneInputType<T>,
fieldMetadata: FieldMetadataEntity,
locale?: keyof typeof APP_LOCALES,
): UpdateOneInputType<T> {
const update: StandardFieldUpdate = {};
const updatableFields = [
'isActive',
'isLabelSyncedWithName',
'options',
'settings',
'defaultValue',
];
const overridableFields = ['label', 'icon', 'description'];
const nonUpdatableFields = Object.keys(instance.update).filter(
(key) =>
!updatableFields.includes(key) && !overridableFields.includes(key),
);
const isUpdatingLabelWhenSynced =
instance.update.label &&
fieldMetadata.isLabelSyncedWithName &&
instance.update.isLabelSyncedWithName !== false &&
instance.update.label !== fieldMetadata.label;
if (isUpdatingLabelWhenSynced) {
throw new ValidationError(
'Cannot update label when it is synced with name',
);
}
if (nonUpdatableFields.length > 0) {
throw new ValidationError(
`Only isActive, isLabelSyncedWithName, label, icon, description 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);
return {
id: instance.id,
update: update as T,
};
}
private handleDefaultValueField(
instance: UpdateOneInputType<T>,
update: StandardFieldUpdate,
): void {
if (!isDefined(instance.update.defaultValue)) {
return;
}
update.defaultValue = instance.update.defaultValue;
}
private handleOptionsField(
instance: UpdateOneInputType<T>,
update: StandardFieldUpdate,
): void {
if (!isDefined(instance.update.options)) {
return;
}
update.options = instance.update.options;
}
private handleSettingsField(
instance: UpdateOneInputType<T>,
update: StandardFieldUpdate,
): void {
if (!isDefined(instance.update.settings)) {
return;
}
update.settings = instance.update.settings;
}
private handleActiveField(
instance: UpdateOneInputType<T>,
update: StandardFieldUpdate,
): void {
if (!isDefined(instance.update.isActive)) {
return;
}
update.isActive = instance.update.isActive;
}
private handleLabelSyncedWithNameField(
instance: UpdateOneInputType<T>,
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<T>,
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;
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,
locale: keyof typeof APP_LOCALES,
): boolean {
const messageId = generateMessageId(originalValue ?? '');
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<string, any>)[overrideKey] = null;
return true;
}
private resetDefaultOverride(
update: StandardFieldUpdate,
overrideKey: 'label' | 'description' | 'icon',
newValue: string,
originalValue: string,
): 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<string, any>)[overrideKey] = value;
}
private handleDescriptionOverride(
instance: UpdateOneInputType<T>,
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<T>,
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<T>,
fieldMetadata: FieldMetadataEntity,
update: StandardFieldUpdate,
locale?: keyof typeof APP_LOCALES,
): void {
if (
fieldMetadata.isLabelSyncedWithName ||
update.isLabelSyncedWithName === true
) {
return;
}
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);
}
}