mirror of
				https://github.com/lingble/twenty.git
				synced 2025-11-03 22:27:57 +00:00 
			
		
		
		
	Fix/enum validation (#2863)
* fix: SELECT enum can have a color key * fix: "findOneOrFail" of undefined * feat: alter column migration store previous metadata informations * fix: enum validation extra keys
This commit is contained in:
		@@ -26,7 +26,6 @@ const bootstrap = async () => {
 | 
				
			|||||||
  // Apply validation pipes globally
 | 
					  // Apply validation pipes globally
 | 
				
			||||||
  app.useGlobalPipes(
 | 
					  app.useGlobalPipes(
 | 
				
			||||||
    new ValidationPipe({
 | 
					    new ValidationPipe({
 | 
				
			||||||
      // whitelist: true,
 | 
					 | 
				
			||||||
      transform: true,
 | 
					      transform: true,
 | 
				
			||||||
    }),
 | 
					    }),
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -16,6 +16,7 @@ import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
 | 
				
			|||||||
import { IsFieldMetadataDefaultValue } from 'src/metadata/field-metadata/validators/is-field-metadata-default-value.validator';
 | 
					import { IsFieldMetadataDefaultValue } from 'src/metadata/field-metadata/validators/is-field-metadata-default-value.validator';
 | 
				
			||||||
import { FieldMetadataResolver } from 'src/metadata/field-metadata/field-metadata.resolver';
 | 
					import { FieldMetadataResolver } from 'src/metadata/field-metadata/field-metadata.resolver';
 | 
				
			||||||
import { FieldMetadataDTO } from 'src/metadata/field-metadata/dtos/field-metadata.dto';
 | 
					import { FieldMetadataDTO } from 'src/metadata/field-metadata/dtos/field-metadata.dto';
 | 
				
			||||||
 | 
					import { IsFieldMetadataOptions } from 'src/metadata/field-metadata/validators/is-field-metadata-options.validator';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { FieldMetadataService } from './field-metadata.service';
 | 
					import { FieldMetadataService } from './field-metadata.service';
 | 
				
			||||||
import { FieldMetadataEntity } from './field-metadata.entity';
 | 
					import { FieldMetadataEntity } from './field-metadata.entity';
 | 
				
			||||||
@@ -65,6 +66,7 @@ import { UpdateFieldInput } from './dtos/update-field.input';
 | 
				
			|||||||
  ],
 | 
					  ],
 | 
				
			||||||
  providers: [
 | 
					  providers: [
 | 
				
			||||||
    IsFieldMetadataDefaultValue,
 | 
					    IsFieldMetadataDefaultValue,
 | 
				
			||||||
 | 
					    IsFieldMetadataOptions,
 | 
				
			||||||
    FieldMetadataService,
 | 
					    FieldMetadataService,
 | 
				
			||||||
    FieldMetadataResolver,
 | 
					    FieldMetadataResolver,
 | 
				
			||||||
  ],
 | 
					  ],
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -57,7 +57,13 @@ export const validateDefaultValueForType = (
 | 
				
			|||||||
      FieldMetadataDefaultValue
 | 
					      FieldMetadataDefaultValue
 | 
				
			||||||
    >(validator, defaultValue);
 | 
					    >(validator, defaultValue);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return validateSync(defaultValueInstance).length === 0;
 | 
					    return (
 | 
				
			||||||
 | 
					      validateSync(defaultValueInstance, {
 | 
				
			||||||
 | 
					        whitelist: true,
 | 
				
			||||||
 | 
					        forbidNonWhitelisted: true,
 | 
				
			||||||
 | 
					        forbidUnknownValues: true,
 | 
				
			||||||
 | 
					      }).length === 0
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return isValid;
 | 
					  return isValid;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,7 +11,7 @@ import {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export const optionsValidatorsMap = {
 | 
					export const optionsValidatorsMap = {
 | 
				
			||||||
  [FieldMetadataType.RATING]: [FieldMetadataDefaultOptions],
 | 
					  [FieldMetadataType.RATING]: [FieldMetadataDefaultOptions],
 | 
				
			||||||
  [FieldMetadataType.SELECT]: [FieldMetadataDefaultOptions],
 | 
					  [FieldMetadataType.SELECT]: [FieldMetadataComplexOptions],
 | 
				
			||||||
  [FieldMetadataType.MULTI_SELECT]: [FieldMetadataComplexOptions],
 | 
					  [FieldMetadataType.MULTI_SELECT]: [FieldMetadataComplexOptions],
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -31,12 +31,18 @@ export const validateOptionsForType = (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  const isValid = options.every((option) => {
 | 
					  const isValid = options.every((option) => {
 | 
				
			||||||
    return validators.some((validator) => {
 | 
					    return validators.some((validator) => {
 | 
				
			||||||
      const optionsInstance = plainToInstance<any, FieldMetadataDefaultOptions>(
 | 
					      const optionsInstance = plainToInstance<
 | 
				
			||||||
        validator,
 | 
					        any,
 | 
				
			||||||
        option,
 | 
					        FieldMetadataDefaultOptions | FieldMetadataComplexOptions
 | 
				
			||||||
      );
 | 
					      >(validator, option);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      return validateSync(optionsInstance).length === 0;
 | 
					      return (
 | 
				
			||||||
 | 
					        validateSync(optionsInstance, {
 | 
				
			||||||
 | 
					          whitelist: true,
 | 
				
			||||||
 | 
					          forbidNonWhitelisted: true,
 | 
				
			||||||
 | 
					          forbidUnknownValues: true,
 | 
				
			||||||
 | 
					        }).length === 0
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,7 +5,10 @@ import { ValidationArguments, ValidatorConstraint } from 'class-validator';
 | 
				
			|||||||
import { FieldMetadataOptions } from 'src/metadata/field-metadata/interfaces/field-metadata-options.interface';
 | 
					import { FieldMetadataOptions } from 'src/metadata/field-metadata/interfaces/field-metadata-options.interface';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { FieldMetadataService } from 'src/metadata/field-metadata/field-metadata.service';
 | 
					import { FieldMetadataService } from 'src/metadata/field-metadata/field-metadata.service';
 | 
				
			||||||
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
 | 
					import {
 | 
				
			||||||
 | 
					  FieldMetadataEntity,
 | 
				
			||||||
 | 
					  FieldMetadataType,
 | 
				
			||||||
 | 
					} from 'src/metadata/field-metadata/field-metadata.entity';
 | 
				
			||||||
import { validateOptionsForType } from 'src/metadata/field-metadata/utils/validate-options-for-type.util';
 | 
					import { validateOptionsForType } from 'src/metadata/field-metadata/utils/validate-options-for-type.util';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Injectable()
 | 
					@Injectable()
 | 
				
			||||||
@@ -28,7 +31,13 @@ export class IsFieldMetadataOptions {
 | 
				
			|||||||
        return false;
 | 
					        return false;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const fieldMetadata = await this.fieldMetadataService.findOneOrFail(id);
 | 
					      let fieldMetadata: FieldMetadataEntity;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        fieldMetadata = await this.fieldMetadataService.findOneOrFail(id);
 | 
				
			||||||
 | 
					      } catch {
 | 
				
			||||||
 | 
					        return false;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      type = fieldMetadata.type;
 | 
					      type = fieldMetadata.type;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -47,21 +47,31 @@ export class BasicColumnActionFactory extends ColumnActionAbstractFactory<BasicF
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  protected handleAlterAction(
 | 
					  protected handleAlterAction(
 | 
				
			||||||
    previousFieldMetadata: FieldMetadataInterface<BasicFieldMetadataType>,
 | 
					    currentFieldMetadata: FieldMetadataInterface<BasicFieldMetadataType>,
 | 
				
			||||||
    nextFieldMetadata: FieldMetadataInterface<BasicFieldMetadataType>,
 | 
					    alteredFieldMetadata: FieldMetadataInterface<BasicFieldMetadataType>,
 | 
				
			||||||
    options?: WorkspaceColumnActionOptions,
 | 
					    options?: WorkspaceColumnActionOptions,
 | 
				
			||||||
  ): WorkspaceMigrationColumnAlter {
 | 
					  ): WorkspaceMigrationColumnAlter {
 | 
				
			||||||
    const defaultValue =
 | 
					    const defaultValue =
 | 
				
			||||||
      this.getDefaultValue(nextFieldMetadata.defaultValue) ??
 | 
					      this.getDefaultValue(alteredFieldMetadata.defaultValue) ??
 | 
				
			||||||
      options?.defaultValue;
 | 
					      options?.defaultValue;
 | 
				
			||||||
    const serializedDefaultValue = serializeDefaultValue(defaultValue);
 | 
					    const serializedDefaultValue = serializeDefaultValue(defaultValue);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      action: WorkspaceMigrationColumnActionType.ALTER,
 | 
					      action: WorkspaceMigrationColumnActionType.ALTER,
 | 
				
			||||||
      columnName: nextFieldMetadata.targetColumnMap.value,
 | 
					      currentColumnDefinition: {
 | 
				
			||||||
      columnType: fieldMetadataTypeToColumnType(nextFieldMetadata.type),
 | 
					        columnName: currentFieldMetadata.targetColumnMap.value,
 | 
				
			||||||
      isNullable: nextFieldMetadata.isNullable,
 | 
					        columnType: fieldMetadataTypeToColumnType(currentFieldMetadata.type),
 | 
				
			||||||
 | 
					        isNullable: currentFieldMetadata.isNullable,
 | 
				
			||||||
 | 
					        defaultValue: serializeDefaultValue(
 | 
				
			||||||
 | 
					          this.getDefaultValue(currentFieldMetadata.defaultValue),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      alteredColumnDefinition: {
 | 
				
			||||||
 | 
					        columnName: alteredFieldMetadata.targetColumnMap.value,
 | 
				
			||||||
 | 
					        columnType: fieldMetadataTypeToColumnType(alteredFieldMetadata.type),
 | 
				
			||||||
 | 
					        isNullable: alteredFieldMetadata.isNullable,
 | 
				
			||||||
        defaultValue: serializedDefaultValue,
 | 
					        defaultValue: serializedDefaultValue,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -23,21 +23,21 @@ export class ColumnActionAbstractFactory<
 | 
				
			|||||||
    action:
 | 
					    action:
 | 
				
			||||||
      | WorkspaceMigrationColumnActionType.CREATE
 | 
					      | WorkspaceMigrationColumnActionType.CREATE
 | 
				
			||||||
      | WorkspaceMigrationColumnActionType.ALTER,
 | 
					      | WorkspaceMigrationColumnActionType.ALTER,
 | 
				
			||||||
    previousFieldMetadata: FieldMetadataInterface<T> | undefined,
 | 
					    currentFieldMetadata: FieldMetadataInterface<T> | undefined,
 | 
				
			||||||
    nextFieldMetadata: FieldMetadataInterface<T>,
 | 
					    alteredFieldMetadata: FieldMetadataInterface<T>,
 | 
				
			||||||
    options?: WorkspaceColumnActionOptions,
 | 
					    options?: WorkspaceColumnActionOptions,
 | 
				
			||||||
  ): WorkspaceMigrationColumnAction {
 | 
					  ): WorkspaceMigrationColumnAction {
 | 
				
			||||||
    switch (action) {
 | 
					    switch (action) {
 | 
				
			||||||
      case WorkspaceMigrationColumnActionType.CREATE:
 | 
					      case WorkspaceMigrationColumnActionType.CREATE:
 | 
				
			||||||
        return this.handleCreateAction(nextFieldMetadata, options);
 | 
					        return this.handleCreateAction(alteredFieldMetadata, options);
 | 
				
			||||||
      case WorkspaceMigrationColumnActionType.ALTER: {
 | 
					      case WorkspaceMigrationColumnActionType.ALTER: {
 | 
				
			||||||
        if (!previousFieldMetadata) {
 | 
					        if (!currentFieldMetadata) {
 | 
				
			||||||
          throw new Error('Previous field metadata is required for alter');
 | 
					          throw new Error('current field metadata is required for alter');
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return this.handleAlterAction(
 | 
					        return this.handleAlterAction(
 | 
				
			||||||
          previousFieldMetadata,
 | 
					          currentFieldMetadata,
 | 
				
			||||||
          nextFieldMetadata,
 | 
					          alteredFieldMetadata,
 | 
				
			||||||
          options,
 | 
					          options,
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
@@ -57,8 +57,8 @@ export class ColumnActionAbstractFactory<
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  protected handleAlterAction(
 | 
					  protected handleAlterAction(
 | 
				
			||||||
    _previousFieldMetadata: FieldMetadataInterface<T>,
 | 
					    _currentFieldMetadata: FieldMetadataInterface<T>,
 | 
				
			||||||
    _nextFieldMetadata: FieldMetadataInterface<T>,
 | 
					    _alteredFieldMetadata: FieldMetadataInterface<T>,
 | 
				
			||||||
    _options?: WorkspaceColumnActionOptions,
 | 
					    _options?: WorkspaceColumnActionOptions,
 | 
				
			||||||
  ): WorkspaceMigrationColumnAlter {
 | 
					  ): WorkspaceMigrationColumnAlter {
 | 
				
			||||||
    throw new Error('handleAlterAction method not implemented.');
 | 
					    throw new Error('handleAlterAction method not implemented.');
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -45,24 +45,24 @@ export class EnumColumnActionFactory extends ColumnActionAbstractFactory<EnumFie
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  protected handleAlterAction(
 | 
					  protected handleAlterAction(
 | 
				
			||||||
    previousFieldMetadata: FieldMetadataInterface<EnumFieldMetadataType>,
 | 
					    currentFieldMetadata: FieldMetadataInterface<EnumFieldMetadataType>,
 | 
				
			||||||
    nextFieldMetadata: FieldMetadataInterface<EnumFieldMetadataType>,
 | 
					    alteredFieldMetadata: FieldMetadataInterface<EnumFieldMetadataType>,
 | 
				
			||||||
    options: WorkspaceColumnActionOptions,
 | 
					    options: WorkspaceColumnActionOptions,
 | 
				
			||||||
  ): WorkspaceMigrationColumnAlter {
 | 
					  ): WorkspaceMigrationColumnAlter {
 | 
				
			||||||
    const defaultValue =
 | 
					    const defaultValue =
 | 
				
			||||||
      nextFieldMetadata.defaultValue?.value ?? options?.defaultValue;
 | 
					      alteredFieldMetadata.defaultValue?.value ?? options?.defaultValue;
 | 
				
			||||||
    const serializedDefaultValue = serializeDefaultValue(defaultValue);
 | 
					    const serializedDefaultValue = serializeDefaultValue(defaultValue);
 | 
				
			||||||
    const enumOptions = nextFieldMetadata.options
 | 
					    const enumOptions = alteredFieldMetadata.options
 | 
				
			||||||
      ? [
 | 
					      ? [
 | 
				
			||||||
          ...nextFieldMetadata.options.map((option) => {
 | 
					          ...alteredFieldMetadata.options.map((option) => {
 | 
				
			||||||
            const previousOption = previousFieldMetadata.options?.find(
 | 
					            const currentOption = currentFieldMetadata.options?.find(
 | 
				
			||||||
              (previousOption) => previousOption.id === option.id,
 | 
					              (currentOption) => currentOption.id === option.id,
 | 
				
			||||||
            );
 | 
					            );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // The id is the same, but the value is different, so we need to alter the enum
 | 
					            // The id is the same, but the value is different, so we need to alter the enum
 | 
				
			||||||
            if (previousOption && previousOption.value !== option.value) {
 | 
					            if (currentOption && currentOption.value !== option.value) {
 | 
				
			||||||
              return {
 | 
					              return {
 | 
				
			||||||
                from: previousOption.value,
 | 
					                from: currentOption.value,
 | 
				
			||||||
                to: option.value,
 | 
					                to: option.value,
 | 
				
			||||||
              };
 | 
					              };
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
@@ -74,12 +74,26 @@ export class EnumColumnActionFactory extends ColumnActionAbstractFactory<EnumFie
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      action: WorkspaceMigrationColumnActionType.ALTER,
 | 
					      action: WorkspaceMigrationColumnActionType.ALTER,
 | 
				
			||||||
      columnName: nextFieldMetadata.targetColumnMap.value,
 | 
					      currentColumnDefinition: {
 | 
				
			||||||
      columnType: fieldMetadataTypeToColumnType(nextFieldMetadata.type),
 | 
					        columnName: currentFieldMetadata.targetColumnMap.value,
 | 
				
			||||||
 | 
					        columnType: fieldMetadataTypeToColumnType(currentFieldMetadata.type),
 | 
				
			||||||
 | 
					        enum: currentFieldMetadata.options
 | 
				
			||||||
 | 
					          ? [...currentFieldMetadata.options.map((option) => option.value)]
 | 
				
			||||||
 | 
					          : undefined,
 | 
				
			||||||
 | 
					        isArray: currentFieldMetadata.type === FieldMetadataType.MULTI_SELECT,
 | 
				
			||||||
 | 
					        isNullable: currentFieldMetadata.isNullable,
 | 
				
			||||||
 | 
					        defaultValue: serializeDefaultValue(
 | 
				
			||||||
 | 
					          currentFieldMetadata.defaultValue?.value,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      alteredColumnDefinition: {
 | 
				
			||||||
 | 
					        columnName: alteredFieldMetadata.targetColumnMap.value,
 | 
				
			||||||
 | 
					        columnType: fieldMetadataTypeToColumnType(alteredFieldMetadata.type),
 | 
				
			||||||
        enum: enumOptions,
 | 
					        enum: enumOptions,
 | 
				
			||||||
      isArray: nextFieldMetadata.type === FieldMetadataType.MULTI_SELECT,
 | 
					        isArray: alteredFieldMetadata.type === FieldMetadataType.MULTI_SELECT,
 | 
				
			||||||
      isNullable: nextFieldMetadata.isNullable,
 | 
					        isNullable: alteredFieldMetadata.isNullable,
 | 
				
			||||||
        defaultValue: serializedDefaultValue,
 | 
					        defaultValue: serializedDefaultValue,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,8 +14,8 @@ export interface WorkspaceColumnActionFactory<
 | 
				
			|||||||
    action:
 | 
					    action:
 | 
				
			||||||
      | WorkspaceMigrationColumnActionType.CREATE
 | 
					      | WorkspaceMigrationColumnActionType.CREATE
 | 
				
			||||||
      | WorkspaceMigrationColumnActionType.ALTER,
 | 
					      | WorkspaceMigrationColumnActionType.ALTER,
 | 
				
			||||||
    previousFieldMetadata: FieldMetadataInterface<T> | undefined,
 | 
					    currentFieldMetadata: FieldMetadataInterface<T> | undefined,
 | 
				
			||||||
    nextFieldMetadata: FieldMetadataInterface<T>,
 | 
					    alteredFieldMetadata: FieldMetadataInterface<T>,
 | 
				
			||||||
    options?: WorkspaceColumnActionOptions,
 | 
					    options?: WorkspaceColumnActionOptions,
 | 
				
			||||||
  ): WorkspaceMigrationColumnAction;
 | 
					  ): WorkspaceMigrationColumnAction;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -13,24 +13,24 @@ export enum WorkspaceMigrationColumnActionType {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export type WorkspaceMigrationEnum = string | { from: string; to: string };
 | 
					export type WorkspaceMigrationEnum = string | { from: string; to: string };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type WorkspaceMigrationColumnCreate = {
 | 
					export interface WorkspaceMigrationColumnDefinition {
 | 
				
			||||||
  action: WorkspaceMigrationColumnActionType.CREATE;
 | 
					 | 
				
			||||||
  columnName: string;
 | 
					  columnName: string;
 | 
				
			||||||
  columnType: string;
 | 
					  columnType: string;
 | 
				
			||||||
  enum?: WorkspaceMigrationEnum[];
 | 
					  enum?: WorkspaceMigrationEnum[];
 | 
				
			||||||
  isArray?: boolean;
 | 
					  isArray?: boolean;
 | 
				
			||||||
  isNullable?: boolean;
 | 
					  isNullable?: boolean;
 | 
				
			||||||
  defaultValue?: any;
 | 
					  defaultValue?: any;
 | 
				
			||||||
};
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface WorkspaceMigrationColumnCreate
 | 
				
			||||||
 | 
					  extends WorkspaceMigrationColumnDefinition {
 | 
				
			||||||
 | 
					  action: WorkspaceMigrationColumnActionType.CREATE;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type WorkspaceMigrationColumnAlter = {
 | 
					export type WorkspaceMigrationColumnAlter = {
 | 
				
			||||||
  action: WorkspaceMigrationColumnActionType.ALTER;
 | 
					  action: WorkspaceMigrationColumnActionType.ALTER;
 | 
				
			||||||
  columnName: string;
 | 
					  currentColumnDefinition: WorkspaceMigrationColumnDefinition;
 | 
				
			||||||
  columnType: string;
 | 
					  alteredColumnDefinition: WorkspaceMigrationColumnDefinition;
 | 
				
			||||||
  enum?: WorkspaceMigrationEnum[];
 | 
					 | 
				
			||||||
  isArray?: boolean;
 | 
					 | 
				
			||||||
  isNullable?: boolean;
 | 
					 | 
				
			||||||
  defaultValue?: any;
 | 
					 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type WorkspaceMigrationColumnRelation = {
 | 
					export type WorkspaceMigrationColumnRelation = {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -97,51 +97,51 @@ export class WorkspaceMigrationFactory {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  createColumnActions(
 | 
					  createColumnActions(
 | 
				
			||||||
    action: WorkspaceMigrationColumnActionType.ALTER,
 | 
					    action: WorkspaceMigrationColumnActionType.ALTER,
 | 
				
			||||||
    previousFieldMetadata: FieldMetadataInterface,
 | 
					    currentFieldMetadata: FieldMetadataInterface,
 | 
				
			||||||
    nextFieldMetadata: FieldMetadataInterface,
 | 
					    alteredFieldMetadata: FieldMetadataInterface,
 | 
				
			||||||
  ): WorkspaceMigrationColumnAction[];
 | 
					  ): WorkspaceMigrationColumnAction[];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  createColumnActions(
 | 
					  createColumnActions(
 | 
				
			||||||
    action:
 | 
					    action:
 | 
				
			||||||
      | WorkspaceMigrationColumnActionType.CREATE
 | 
					      | WorkspaceMigrationColumnActionType.CREATE
 | 
				
			||||||
      | WorkspaceMigrationColumnActionType.ALTER,
 | 
					      | WorkspaceMigrationColumnActionType.ALTER,
 | 
				
			||||||
    fieldMetadataOrPreviousFieldMetadata: FieldMetadataInterface,
 | 
					    fieldMetadataOrCurrentFieldMetadata: FieldMetadataInterface,
 | 
				
			||||||
    undefinedOrnextFieldMetadata?: FieldMetadataInterface,
 | 
					    undefinedOrAlteredFieldMetadata?: FieldMetadataInterface,
 | 
				
			||||||
  ): WorkspaceMigrationColumnAction[] {
 | 
					  ): WorkspaceMigrationColumnAction[] {
 | 
				
			||||||
    const previousFieldMetadata =
 | 
					    const currentFieldMetadata =
 | 
				
			||||||
      action === WorkspaceMigrationColumnActionType.ALTER
 | 
					      action === WorkspaceMigrationColumnActionType.ALTER
 | 
				
			||||||
        ? fieldMetadataOrPreviousFieldMetadata
 | 
					        ? fieldMetadataOrCurrentFieldMetadata
 | 
				
			||||||
        : undefined;
 | 
					        : undefined;
 | 
				
			||||||
    const nextFieldMetadata =
 | 
					    const alteredFieldMetadata =
 | 
				
			||||||
      action === WorkspaceMigrationColumnActionType.CREATE
 | 
					      action === WorkspaceMigrationColumnActionType.CREATE
 | 
				
			||||||
        ? fieldMetadataOrPreviousFieldMetadata
 | 
					        ? fieldMetadataOrCurrentFieldMetadata
 | 
				
			||||||
        : undefinedOrnextFieldMetadata;
 | 
					        : undefinedOrAlteredFieldMetadata;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!nextFieldMetadata) {
 | 
					    if (!alteredFieldMetadata) {
 | 
				
			||||||
      this.logger.error(
 | 
					      this.logger.error(
 | 
				
			||||||
        `No field metadata provided for action ${action}`,
 | 
					        `No field metadata provided for action ${action}`,
 | 
				
			||||||
        fieldMetadataOrPreviousFieldMetadata,
 | 
					        undefinedOrAlteredFieldMetadata,
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      throw new Error(`No field metadata provided for action ${action}`);
 | 
					      throw new Error(`No field metadata provided for action ${action}`);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // If it's a composite field type, we need to create a column action for each of the fields
 | 
					    // If it's a composite field type, we need to create a column action for each of the fields
 | 
				
			||||||
    if (isCompositeFieldMetadataType(nextFieldMetadata.type)) {
 | 
					    if (isCompositeFieldMetadataType(alteredFieldMetadata.type)) {
 | 
				
			||||||
      const fieldMetadataCollection = this.compositeDefinitions.get(
 | 
					      const fieldMetadataCollection = this.compositeDefinitions.get(
 | 
				
			||||||
        nextFieldMetadata.type,
 | 
					        alteredFieldMetadata.type,
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (!fieldMetadataCollection) {
 | 
					      if (!fieldMetadataCollection) {
 | 
				
			||||||
        this.logger.error(
 | 
					        this.logger.error(
 | 
				
			||||||
          `No composite definition found for type ${nextFieldMetadata.type}`,
 | 
					          `No composite definition found for type ${alteredFieldMetadata.type}`,
 | 
				
			||||||
          {
 | 
					          {
 | 
				
			||||||
            nextFieldMetadata,
 | 
					            alteredFieldMetadata,
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        throw new Error(
 | 
					        throw new Error(
 | 
				
			||||||
          `No composite definition found for type ${nextFieldMetadata.type}`,
 | 
					          `No composite definition found for type ${alteredFieldMetadata.type}`,
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -153,8 +153,8 @@ export class WorkspaceMigrationFactory {
 | 
				
			|||||||
    // Otherwise, we create a single column action
 | 
					    // Otherwise, we create a single column action
 | 
				
			||||||
    const columnAction = this.createColumnAction(
 | 
					    const columnAction = this.createColumnAction(
 | 
				
			||||||
      action,
 | 
					      action,
 | 
				
			||||||
      previousFieldMetadata,
 | 
					      currentFieldMetadata,
 | 
				
			||||||
      nextFieldMetadata,
 | 
					      alteredFieldMetadata,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return [columnAction];
 | 
					    return [columnAction];
 | 
				
			||||||
@@ -164,24 +164,27 @@ export class WorkspaceMigrationFactory {
 | 
				
			|||||||
    action:
 | 
					    action:
 | 
				
			||||||
      | WorkspaceMigrationColumnActionType.CREATE
 | 
					      | WorkspaceMigrationColumnActionType.CREATE
 | 
				
			||||||
      | WorkspaceMigrationColumnActionType.ALTER,
 | 
					      | WorkspaceMigrationColumnActionType.ALTER,
 | 
				
			||||||
    previousFieldMetadata: FieldMetadataInterface | undefined,
 | 
					    currentFieldMetadata: FieldMetadataInterface | undefined,
 | 
				
			||||||
    nextFieldMetadata: FieldMetadataInterface,
 | 
					    alteredFieldMetadata: FieldMetadataInterface,
 | 
				
			||||||
  ): WorkspaceMigrationColumnAction {
 | 
					  ): WorkspaceMigrationColumnAction {
 | 
				
			||||||
    const { factory, options } =
 | 
					    const { factory, options } =
 | 
				
			||||||
      this.factoriesMap.get(nextFieldMetadata.type) ?? {};
 | 
					      this.factoriesMap.get(alteredFieldMetadata.type) ?? {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!factory) {
 | 
					    if (!factory) {
 | 
				
			||||||
      this.logger.error(`No factory found for type ${nextFieldMetadata.type}`, {
 | 
					      this.logger.error(
 | 
				
			||||||
        nextFieldMetadata,
 | 
					        `No factory found for type ${alteredFieldMetadata.type}`,
 | 
				
			||||||
      });
 | 
					        {
 | 
				
			||||||
 | 
					          alteredFieldMetadata,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      throw new Error(`No factory found for type ${nextFieldMetadata.type}`);
 | 
					      throw new Error(`No factory found for type ${alteredFieldMetadata.type}`);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return factory.create(
 | 
					    return factory.create(
 | 
				
			||||||
      action,
 | 
					      action,
 | 
				
			||||||
      previousFieldMetadata,
 | 
					      currentFieldMetadata,
 | 
				
			||||||
      nextFieldMetadata,
 | 
					      alteredFieldMetadata,
 | 
				
			||||||
      options,
 | 
					      options,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,10 +12,11 @@ export class WorkspaceMigrationEnumService {
 | 
				
			|||||||
    tableName: string,
 | 
					    tableName: string,
 | 
				
			||||||
    migrationColumn: WorkspaceMigrationColumnAlter,
 | 
					    migrationColumn: WorkspaceMigrationColumnAlter,
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
    const oldEnumTypeName = `${tableName}_${migrationColumn.columnName}_enum`;
 | 
					    const columnDefinition = migrationColumn.alteredColumnDefinition;
 | 
				
			||||||
    const newEnumTypeName = `${tableName}_${migrationColumn.columnName}_enum_new`;
 | 
					    const oldEnumTypeName = `${tableName}_${columnDefinition.columnName}_enum`;
 | 
				
			||||||
 | 
					    const newEnumTypeName = `${tableName}_${columnDefinition.columnName}_enum_new`;
 | 
				
			||||||
    const enumValues =
 | 
					    const enumValues =
 | 
				
			||||||
      migrationColumn.enum?.map((enumValue) => {
 | 
					      columnDefinition.enum?.map((enumValue) => {
 | 
				
			||||||
        if (typeof enumValue === 'string') {
 | 
					        if (typeof enumValue === 'string') {
 | 
				
			||||||
          return enumValue;
 | 
					          return enumValue;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -23,8 +24,8 @@ export class WorkspaceMigrationEnumService {
 | 
				
			|||||||
        return enumValue.to;
 | 
					        return enumValue.to;
 | 
				
			||||||
      }) ?? [];
 | 
					      }) ?? [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!migrationColumn.isNullable && !migrationColumn.defaultValue) {
 | 
					    if (!columnDefinition.isNullable && !columnDefinition.defaultValue) {
 | 
				
			||||||
      migrationColumn.defaultValue = migrationColumn.enum?.[0];
 | 
					      columnDefinition.defaultValue = columnDefinition.enum?.[0];
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Create new enum type with new values
 | 
					    // Create new enum type with new values
 | 
				
			||||||
@@ -38,7 +39,7 @@ export class WorkspaceMigrationEnumService {
 | 
				
			|||||||
    // Temporarily change column type to text
 | 
					    // Temporarily change column type to text
 | 
				
			||||||
    await queryRunner.query(`
 | 
					    await queryRunner.query(`
 | 
				
			||||||
      ALTER TABLE "${schemaName}"."${tableName}"
 | 
					      ALTER TABLE "${schemaName}"."${tableName}"
 | 
				
			||||||
      ALTER COLUMN "${migrationColumn.columnName}" TYPE TEXT
 | 
					      ALTER COLUMN "${columnDefinition.columnName}" TYPE TEXT
 | 
				
			||||||
    `);
 | 
					    `);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Migrate existing values to new values
 | 
					    // Migrate existing values to new values
 | 
				
			||||||
@@ -63,7 +64,7 @@ export class WorkspaceMigrationEnumService {
 | 
				
			|||||||
      queryRunner,
 | 
					      queryRunner,
 | 
				
			||||||
      schemaName,
 | 
					      schemaName,
 | 
				
			||||||
      tableName,
 | 
					      tableName,
 | 
				
			||||||
      migrationColumn.columnName,
 | 
					      columnDefinition.columnName,
 | 
				
			||||||
      newEnumTypeName,
 | 
					      newEnumTypeName,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -100,20 +101,21 @@ export class WorkspaceMigrationEnumService {
 | 
				
			|||||||
    tableName: string,
 | 
					    tableName: string,
 | 
				
			||||||
    migrationColumn: WorkspaceMigrationColumnAlter,
 | 
					    migrationColumn: WorkspaceMigrationColumnAlter,
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
    if (!migrationColumn.enum) {
 | 
					    const columnDefinition = migrationColumn.alteredColumnDefinition;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!columnDefinition.enum) {
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for (const enumValue of migrationColumn.enum) {
 | 
					    for (const enumValue of columnDefinition.enum) {
 | 
				
			||||||
      // Skip string values
 | 
					      // Skip string values
 | 
				
			||||||
      if (typeof enumValue === 'string') {
 | 
					      if (typeof enumValue === 'string') {
 | 
				
			||||||
        continue;
 | 
					        continue;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					 | 
				
			||||||
      await queryRunner.query(`
 | 
					      await queryRunner.query(`
 | 
				
			||||||
        UPDATE "${schemaName}"."${tableName}"
 | 
					        UPDATE "${schemaName}"."${tableName}"
 | 
				
			||||||
        SET "${migrationColumn.columnName}" = '${enumValue.to}'
 | 
					        SET "${columnDefinition.columnName}" = '${enumValue.to}'
 | 
				
			||||||
        WHERE "${migrationColumn.columnName}" = '${enumValue.from}'
 | 
					        WHERE "${columnDefinition.columnName}" = '${enumValue.from}'
 | 
				
			||||||
      `);
 | 
					      `);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@@ -125,23 +127,25 @@ export class WorkspaceMigrationEnumService {
 | 
				
			|||||||
    migrationColumn: WorkspaceMigrationColumnAlter,
 | 
					    migrationColumn: WorkspaceMigrationColumnAlter,
 | 
				
			||||||
    enumValues: string[],
 | 
					    enumValues: string[],
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
 | 
					    const columnDefinition = migrationColumn.alteredColumnDefinition;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Set missing values to null or default value
 | 
					    // Set missing values to null or default value
 | 
				
			||||||
    let defaultValue = 'NULL';
 | 
					    let defaultValue = 'NULL';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (migrationColumn.defaultValue) {
 | 
					    if (columnDefinition.defaultValue) {
 | 
				
			||||||
      if (Array.isArray(migrationColumn.defaultValue)) {
 | 
					      if (Array.isArray(columnDefinition.defaultValue)) {
 | 
				
			||||||
        defaultValue = `ARRAY[${migrationColumn.defaultValue
 | 
					        defaultValue = `ARRAY[${columnDefinition.defaultValue
 | 
				
			||||||
          .map((e) => `'${e}'`)
 | 
					          .map((e) => `'${e}'`)
 | 
				
			||||||
          .join(', ')}]`;
 | 
					          .join(', ')}]`;
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        defaultValue = `'${migrationColumn.defaultValue}'`;
 | 
					        defaultValue = `'${columnDefinition.defaultValue}'`;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await queryRunner.query(`
 | 
					    await queryRunner.query(`
 | 
				
			||||||
      UPDATE "${schemaName}"."${tableName}"
 | 
					      UPDATE "${schemaName}"."${tableName}"
 | 
				
			||||||
      SET "${migrationColumn.columnName}" = ${defaultValue}
 | 
					      SET "${columnDefinition.columnName}" = ${defaultValue}
 | 
				
			||||||
      WHERE "${migrationColumn.columnName}" NOT IN (${enumValues
 | 
					      WHERE "${columnDefinition.columnName}" NOT IN (${enumValues
 | 
				
			||||||
      .map((e) => `'${e}'`)
 | 
					      .map((e) => `'${e}'`)
 | 
				
			||||||
      .join(', ')})
 | 
					      .join(', ')})
 | 
				
			||||||
    `);
 | 
					    `);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -237,7 +237,7 @@ export class WorkspaceMigrationRunnerService {
 | 
				
			|||||||
    tableName: string,
 | 
					    tableName: string,
 | 
				
			||||||
    migrationColumn: WorkspaceMigrationColumnAlter,
 | 
					    migrationColumn: WorkspaceMigrationColumnAlter,
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
    const enumValues = migrationColumn.enum;
 | 
					    const enumValues = migrationColumn.alteredColumnDefinition.enum;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // TODO: Maybe we can do something better if we can recreate the old `TableColumn` object
 | 
					    // TODO: Maybe we can do something better if we can recreate the old `TableColumn` object
 | 
				
			||||||
    if (enumValues) {
 | 
					    if (enumValues) {
 | 
				
			||||||
@@ -248,18 +248,32 @@ export class WorkspaceMigrationRunnerService {
 | 
				
			|||||||
        tableName,
 | 
					        tableName,
 | 
				
			||||||
        migrationColumn,
 | 
					        migrationColumn,
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    } else {
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await queryRunner.changeColumn(
 | 
					    await queryRunner.changeColumn(
 | 
				
			||||||
      `${schemaName}.${tableName}`,
 | 
					      `${schemaName}.${tableName}`,
 | 
				
			||||||
        migrationColumn.columnName,
 | 
					 | 
				
			||||||
      new TableColumn({
 | 
					      new TableColumn({
 | 
				
			||||||
          name: migrationColumn.columnName,
 | 
					        name: migrationColumn.currentColumnDefinition.columnName,
 | 
				
			||||||
          type: migrationColumn.columnType,
 | 
					        type: migrationColumn.currentColumnDefinition.columnType,
 | 
				
			||||||
          default: migrationColumn.defaultValue,
 | 
					        default: migrationColumn.currentColumnDefinition.defaultValue,
 | 
				
			||||||
          isNullable: migrationColumn.isNullable,
 | 
					        enum: migrationColumn.currentColumnDefinition.enum?.filter(
 | 
				
			||||||
 | 
					          (value): value is string => typeof value === 'string',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        isArray: migrationColumn.currentColumnDefinition.isArray,
 | 
				
			||||||
 | 
					        isNullable: migrationColumn.currentColumnDefinition.isNullable,
 | 
				
			||||||
 | 
					      }),
 | 
				
			||||||
 | 
					      new TableColumn({
 | 
				
			||||||
 | 
					        name: migrationColumn.alteredColumnDefinition.columnName,
 | 
				
			||||||
 | 
					        type: migrationColumn.alteredColumnDefinition.columnType,
 | 
				
			||||||
 | 
					        default: migrationColumn.alteredColumnDefinition.defaultValue,
 | 
				
			||||||
 | 
					        enum: migrationColumn.currentColumnDefinition.enum?.filter(
 | 
				
			||||||
 | 
					          (value): value is string => typeof value === 'string',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        isArray: migrationColumn.alteredColumnDefinition.isArray,
 | 
				
			||||||
 | 
					        isNullable: migrationColumn.alteredColumnDefinition.isNullable,
 | 
				
			||||||
      }),
 | 
					      }),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    }
 | 
					    // }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private async createForeignKey(
 | 
					  private async createForeignKey(
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user