diff --git a/server/src/main.ts b/server/src/main.ts index f28842326..56effa393 100644 --- a/server/src/main.ts +++ b/server/src/main.ts @@ -26,7 +26,6 @@ const bootstrap = async () => { // Apply validation pipes globally app.useGlobalPipes( new ValidationPipe({ - // whitelist: true, transform: true, }), ); diff --git a/server/src/metadata/field-metadata/field-metadata.module.ts b/server/src/metadata/field-metadata/field-metadata.module.ts index acfa268e4..f332a8480 100644 --- a/server/src/metadata/field-metadata/field-metadata.module.ts +++ b/server/src/metadata/field-metadata/field-metadata.module.ts @@ -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 { FieldMetadataResolver } from 'src/metadata/field-metadata/field-metadata.resolver'; 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 { FieldMetadataEntity } from './field-metadata.entity'; @@ -65,6 +66,7 @@ import { UpdateFieldInput } from './dtos/update-field.input'; ], providers: [ IsFieldMetadataDefaultValue, + IsFieldMetadataOptions, FieldMetadataService, FieldMetadataResolver, ], diff --git a/server/src/metadata/field-metadata/utils/validate-default-value-for-type.util.ts b/server/src/metadata/field-metadata/utils/validate-default-value-for-type.util.ts index cb6125e18..1735544d9 100644 --- a/server/src/metadata/field-metadata/utils/validate-default-value-for-type.util.ts +++ b/server/src/metadata/field-metadata/utils/validate-default-value-for-type.util.ts @@ -57,7 +57,13 @@ export const validateDefaultValueForType = ( FieldMetadataDefaultValue >(validator, defaultValue); - return validateSync(defaultValueInstance).length === 0; + return ( + validateSync(defaultValueInstance, { + whitelist: true, + forbidNonWhitelisted: true, + forbidUnknownValues: true, + }).length === 0 + ); }); return isValid; diff --git a/server/src/metadata/field-metadata/utils/validate-options-for-type.util.ts b/server/src/metadata/field-metadata/utils/validate-options-for-type.util.ts index c2742afb4..0a3ec8740 100644 --- a/server/src/metadata/field-metadata/utils/validate-options-for-type.util.ts +++ b/server/src/metadata/field-metadata/utils/validate-options-for-type.util.ts @@ -11,7 +11,7 @@ import { export const optionsValidatorsMap = { [FieldMetadataType.RATING]: [FieldMetadataDefaultOptions], - [FieldMetadataType.SELECT]: [FieldMetadataDefaultOptions], + [FieldMetadataType.SELECT]: [FieldMetadataComplexOptions], [FieldMetadataType.MULTI_SELECT]: [FieldMetadataComplexOptions], }; @@ -31,12 +31,18 @@ export const validateOptionsForType = ( const isValid = options.every((option) => { return validators.some((validator) => { - const optionsInstance = plainToInstance( - validator, - option, - ); + const optionsInstance = plainToInstance< + any, + FieldMetadataDefaultOptions | FieldMetadataComplexOptions + >(validator, option); - return validateSync(optionsInstance).length === 0; + return ( + validateSync(optionsInstance, { + whitelist: true, + forbidNonWhitelisted: true, + forbidUnknownValues: true, + }).length === 0 + ); }); }); diff --git a/server/src/metadata/field-metadata/validators/is-field-metadata-options.validator.ts b/server/src/metadata/field-metadata/validators/is-field-metadata-options.validator.ts index 3aa785cec..74051cb1f 100644 --- a/server/src/metadata/field-metadata/validators/is-field-metadata-options.validator.ts +++ b/server/src/metadata/field-metadata/validators/is-field-metadata-options.validator.ts @@ -5,7 +5,10 @@ import { ValidationArguments, ValidatorConstraint } from 'class-validator'; import { FieldMetadataOptions } from 'src/metadata/field-metadata/interfaces/field-metadata-options.interface'; 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'; @Injectable() @@ -28,7 +31,13 @@ export class IsFieldMetadataOptions { 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; } diff --git a/server/src/metadata/workspace-migration/factories/basic-column-action.factory.ts b/server/src/metadata/workspace-migration/factories/basic-column-action.factory.ts index b81dd33be..22bd35e59 100644 --- a/server/src/metadata/workspace-migration/factories/basic-column-action.factory.ts +++ b/server/src/metadata/workspace-migration/factories/basic-column-action.factory.ts @@ -47,21 +47,31 @@ export class BasicColumnActionFactory extends ColumnActionAbstractFactory, - nextFieldMetadata: FieldMetadataInterface, + currentFieldMetadata: FieldMetadataInterface, + alteredFieldMetadata: FieldMetadataInterface, options?: WorkspaceColumnActionOptions, ): WorkspaceMigrationColumnAlter { const defaultValue = - this.getDefaultValue(nextFieldMetadata.defaultValue) ?? + this.getDefaultValue(alteredFieldMetadata.defaultValue) ?? options?.defaultValue; const serializedDefaultValue = serializeDefaultValue(defaultValue); return { action: WorkspaceMigrationColumnActionType.ALTER, - columnName: nextFieldMetadata.targetColumnMap.value, - columnType: fieldMetadataTypeToColumnType(nextFieldMetadata.type), - isNullable: nextFieldMetadata.isNullable, - defaultValue: serializedDefaultValue, + currentColumnDefinition: { + columnName: currentFieldMetadata.targetColumnMap.value, + 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, + }, }; } diff --git a/server/src/metadata/workspace-migration/factories/column-action-abstract.factory.ts b/server/src/metadata/workspace-migration/factories/column-action-abstract.factory.ts index 38abfb64c..b4e4bbac2 100644 --- a/server/src/metadata/workspace-migration/factories/column-action-abstract.factory.ts +++ b/server/src/metadata/workspace-migration/factories/column-action-abstract.factory.ts @@ -23,21 +23,21 @@ export class ColumnActionAbstractFactory< action: | WorkspaceMigrationColumnActionType.CREATE | WorkspaceMigrationColumnActionType.ALTER, - previousFieldMetadata: FieldMetadataInterface | undefined, - nextFieldMetadata: FieldMetadataInterface, + currentFieldMetadata: FieldMetadataInterface | undefined, + alteredFieldMetadata: FieldMetadataInterface, options?: WorkspaceColumnActionOptions, ): WorkspaceMigrationColumnAction { switch (action) { case WorkspaceMigrationColumnActionType.CREATE: - return this.handleCreateAction(nextFieldMetadata, options); + return this.handleCreateAction(alteredFieldMetadata, options); case WorkspaceMigrationColumnActionType.ALTER: { - if (!previousFieldMetadata) { - throw new Error('Previous field metadata is required for alter'); + if (!currentFieldMetadata) { + throw new Error('current field metadata is required for alter'); } return this.handleAlterAction( - previousFieldMetadata, - nextFieldMetadata, + currentFieldMetadata, + alteredFieldMetadata, options, ); } @@ -57,8 +57,8 @@ export class ColumnActionAbstractFactory< } protected handleAlterAction( - _previousFieldMetadata: FieldMetadataInterface, - _nextFieldMetadata: FieldMetadataInterface, + _currentFieldMetadata: FieldMetadataInterface, + _alteredFieldMetadata: FieldMetadataInterface, _options?: WorkspaceColumnActionOptions, ): WorkspaceMigrationColumnAlter { throw new Error('handleAlterAction method not implemented.'); diff --git a/server/src/metadata/workspace-migration/factories/enum-column-action.factory.ts b/server/src/metadata/workspace-migration/factories/enum-column-action.factory.ts index 4f5d04cd5..f24458304 100644 --- a/server/src/metadata/workspace-migration/factories/enum-column-action.factory.ts +++ b/server/src/metadata/workspace-migration/factories/enum-column-action.factory.ts @@ -45,24 +45,24 @@ export class EnumColumnActionFactory extends ColumnActionAbstractFactory, - nextFieldMetadata: FieldMetadataInterface, + currentFieldMetadata: FieldMetadataInterface, + alteredFieldMetadata: FieldMetadataInterface, options: WorkspaceColumnActionOptions, ): WorkspaceMigrationColumnAlter { const defaultValue = - nextFieldMetadata.defaultValue?.value ?? options?.defaultValue; + alteredFieldMetadata.defaultValue?.value ?? options?.defaultValue; const serializedDefaultValue = serializeDefaultValue(defaultValue); - const enumOptions = nextFieldMetadata.options + const enumOptions = alteredFieldMetadata.options ? [ - ...nextFieldMetadata.options.map((option) => { - const previousOption = previousFieldMetadata.options?.find( - (previousOption) => previousOption.id === option.id, + ...alteredFieldMetadata.options.map((option) => { + const currentOption = currentFieldMetadata.options?.find( + (currentOption) => currentOption.id === option.id, ); // 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 { - from: previousOption.value, + from: currentOption.value, to: option.value, }; } @@ -74,12 +74,26 @@ export class EnumColumnActionFactory extends ColumnActionAbstractFactory 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, + isArray: alteredFieldMetadata.type === FieldMetadataType.MULTI_SELECT, + isNullable: alteredFieldMetadata.isNullable, + defaultValue: serializedDefaultValue, + }, }; } } diff --git a/server/src/metadata/workspace-migration/interfaces/workspace-column-action-factory.interface.ts b/server/src/metadata/workspace-migration/interfaces/workspace-column-action-factory.interface.ts index 71318f161..44c05d029 100644 --- a/server/src/metadata/workspace-migration/interfaces/workspace-column-action-factory.interface.ts +++ b/server/src/metadata/workspace-migration/interfaces/workspace-column-action-factory.interface.ts @@ -14,8 +14,8 @@ export interface WorkspaceColumnActionFactory< action: | WorkspaceMigrationColumnActionType.CREATE | WorkspaceMigrationColumnActionType.ALTER, - previousFieldMetadata: FieldMetadataInterface | undefined, - nextFieldMetadata: FieldMetadataInterface, + currentFieldMetadata: FieldMetadataInterface | undefined, + alteredFieldMetadata: FieldMetadataInterface, options?: WorkspaceColumnActionOptions, ): WorkspaceMigrationColumnAction; } diff --git a/server/src/metadata/workspace-migration/workspace-migration.entity.ts b/server/src/metadata/workspace-migration/workspace-migration.entity.ts index 7ce8accee..8e560ef0a 100644 --- a/server/src/metadata/workspace-migration/workspace-migration.entity.ts +++ b/server/src/metadata/workspace-migration/workspace-migration.entity.ts @@ -13,24 +13,24 @@ export enum WorkspaceMigrationColumnActionType { export type WorkspaceMigrationEnum = string | { from: string; to: string }; -export type WorkspaceMigrationColumnCreate = { - action: WorkspaceMigrationColumnActionType.CREATE; +export interface WorkspaceMigrationColumnDefinition { columnName: string; columnType: string; enum?: WorkspaceMigrationEnum[]; isArray?: boolean; isNullable?: boolean; defaultValue?: any; -}; +} + +export interface WorkspaceMigrationColumnCreate + extends WorkspaceMigrationColumnDefinition { + action: WorkspaceMigrationColumnActionType.CREATE; +} export type WorkspaceMigrationColumnAlter = { action: WorkspaceMigrationColumnActionType.ALTER; - columnName: string; - columnType: string; - enum?: WorkspaceMigrationEnum[]; - isArray?: boolean; - isNullable?: boolean; - defaultValue?: any; + currentColumnDefinition: WorkspaceMigrationColumnDefinition; + alteredColumnDefinition: WorkspaceMigrationColumnDefinition; }; export type WorkspaceMigrationColumnRelation = { diff --git a/server/src/metadata/workspace-migration/workspace-migration.factory.ts b/server/src/metadata/workspace-migration/workspace-migration.factory.ts index fecc6f56a..9c575be06 100644 --- a/server/src/metadata/workspace-migration/workspace-migration.factory.ts +++ b/server/src/metadata/workspace-migration/workspace-migration.factory.ts @@ -97,51 +97,51 @@ export class WorkspaceMigrationFactory { createColumnActions( action: WorkspaceMigrationColumnActionType.ALTER, - previousFieldMetadata: FieldMetadataInterface, - nextFieldMetadata: FieldMetadataInterface, + currentFieldMetadata: FieldMetadataInterface, + alteredFieldMetadata: FieldMetadataInterface, ): WorkspaceMigrationColumnAction[]; createColumnActions( action: | WorkspaceMigrationColumnActionType.CREATE | WorkspaceMigrationColumnActionType.ALTER, - fieldMetadataOrPreviousFieldMetadata: FieldMetadataInterface, - undefinedOrnextFieldMetadata?: FieldMetadataInterface, + fieldMetadataOrCurrentFieldMetadata: FieldMetadataInterface, + undefinedOrAlteredFieldMetadata?: FieldMetadataInterface, ): WorkspaceMigrationColumnAction[] { - const previousFieldMetadata = + const currentFieldMetadata = action === WorkspaceMigrationColumnActionType.ALTER - ? fieldMetadataOrPreviousFieldMetadata + ? fieldMetadataOrCurrentFieldMetadata : undefined; - const nextFieldMetadata = + const alteredFieldMetadata = action === WorkspaceMigrationColumnActionType.CREATE - ? fieldMetadataOrPreviousFieldMetadata - : undefinedOrnextFieldMetadata; + ? fieldMetadataOrCurrentFieldMetadata + : undefinedOrAlteredFieldMetadata; - if (!nextFieldMetadata) { + if (!alteredFieldMetadata) { this.logger.error( `No field metadata provided for action ${action}`, - fieldMetadataOrPreviousFieldMetadata, + undefinedOrAlteredFieldMetadata, ); 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 (isCompositeFieldMetadataType(nextFieldMetadata.type)) { + if (isCompositeFieldMetadataType(alteredFieldMetadata.type)) { const fieldMetadataCollection = this.compositeDefinitions.get( - nextFieldMetadata.type, + alteredFieldMetadata.type, ); if (!fieldMetadataCollection) { this.logger.error( - `No composite definition found for type ${nextFieldMetadata.type}`, + `No composite definition found for type ${alteredFieldMetadata.type}`, { - nextFieldMetadata, + alteredFieldMetadata, }, ); 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 const columnAction = this.createColumnAction( action, - previousFieldMetadata, - nextFieldMetadata, + currentFieldMetadata, + alteredFieldMetadata, ); return [columnAction]; @@ -164,24 +164,27 @@ export class WorkspaceMigrationFactory { action: | WorkspaceMigrationColumnActionType.CREATE | WorkspaceMigrationColumnActionType.ALTER, - previousFieldMetadata: FieldMetadataInterface | undefined, - nextFieldMetadata: FieldMetadataInterface, + currentFieldMetadata: FieldMetadataInterface | undefined, + alteredFieldMetadata: FieldMetadataInterface, ): WorkspaceMigrationColumnAction { const { factory, options } = - this.factoriesMap.get(nextFieldMetadata.type) ?? {}; + this.factoriesMap.get(alteredFieldMetadata.type) ?? {}; if (!factory) { - this.logger.error(`No factory found for type ${nextFieldMetadata.type}`, { - nextFieldMetadata, - }); + this.logger.error( + `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( action, - previousFieldMetadata, - nextFieldMetadata, + currentFieldMetadata, + alteredFieldMetadata, options, ); } diff --git a/server/src/workspace/workspace-migration-runner/services/workspace-migration-enum.service.ts b/server/src/workspace/workspace-migration-runner/services/workspace-migration-enum.service.ts index 33aef3422..1dbbd4890 100644 --- a/server/src/workspace/workspace-migration-runner/services/workspace-migration-enum.service.ts +++ b/server/src/workspace/workspace-migration-runner/services/workspace-migration-enum.service.ts @@ -12,10 +12,11 @@ export class WorkspaceMigrationEnumService { tableName: string, migrationColumn: WorkspaceMigrationColumnAlter, ) { - const oldEnumTypeName = `${tableName}_${migrationColumn.columnName}_enum`; - const newEnumTypeName = `${tableName}_${migrationColumn.columnName}_enum_new`; + const columnDefinition = migrationColumn.alteredColumnDefinition; + const oldEnumTypeName = `${tableName}_${columnDefinition.columnName}_enum`; + const newEnumTypeName = `${tableName}_${columnDefinition.columnName}_enum_new`; const enumValues = - migrationColumn.enum?.map((enumValue) => { + columnDefinition.enum?.map((enumValue) => { if (typeof enumValue === 'string') { return enumValue; } @@ -23,8 +24,8 @@ export class WorkspaceMigrationEnumService { return enumValue.to; }) ?? []; - if (!migrationColumn.isNullable && !migrationColumn.defaultValue) { - migrationColumn.defaultValue = migrationColumn.enum?.[0]; + if (!columnDefinition.isNullable && !columnDefinition.defaultValue) { + columnDefinition.defaultValue = columnDefinition.enum?.[0]; } // Create new enum type with new values @@ -38,7 +39,7 @@ export class WorkspaceMigrationEnumService { // Temporarily change column type to text await queryRunner.query(` ALTER TABLE "${schemaName}"."${tableName}" - ALTER COLUMN "${migrationColumn.columnName}" TYPE TEXT + ALTER COLUMN "${columnDefinition.columnName}" TYPE TEXT `); // Migrate existing values to new values @@ -63,7 +64,7 @@ export class WorkspaceMigrationEnumService { queryRunner, schemaName, tableName, - migrationColumn.columnName, + columnDefinition.columnName, newEnumTypeName, ); @@ -100,20 +101,21 @@ export class WorkspaceMigrationEnumService { tableName: string, migrationColumn: WorkspaceMigrationColumnAlter, ) { - if (!migrationColumn.enum) { + const columnDefinition = migrationColumn.alteredColumnDefinition; + + if (!columnDefinition.enum) { return; } - for (const enumValue of migrationColumn.enum) { + for (const enumValue of columnDefinition.enum) { // Skip string values if (typeof enumValue === 'string') { continue; } - await queryRunner.query(` UPDATE "${schemaName}"."${tableName}" - SET "${migrationColumn.columnName}" = '${enumValue.to}' - WHERE "${migrationColumn.columnName}" = '${enumValue.from}' + SET "${columnDefinition.columnName}" = '${enumValue.to}' + WHERE "${columnDefinition.columnName}" = '${enumValue.from}' `); } } @@ -125,23 +127,25 @@ export class WorkspaceMigrationEnumService { migrationColumn: WorkspaceMigrationColumnAlter, enumValues: string[], ) { + const columnDefinition = migrationColumn.alteredColumnDefinition; + // Set missing values to null or default value let defaultValue = 'NULL'; - if (migrationColumn.defaultValue) { - if (Array.isArray(migrationColumn.defaultValue)) { - defaultValue = `ARRAY[${migrationColumn.defaultValue + if (columnDefinition.defaultValue) { + if (Array.isArray(columnDefinition.defaultValue)) { + defaultValue = `ARRAY[${columnDefinition.defaultValue .map((e) => `'${e}'`) .join(', ')}]`; } else { - defaultValue = `'${migrationColumn.defaultValue}'`; + defaultValue = `'${columnDefinition.defaultValue}'`; } } await queryRunner.query(` UPDATE "${schemaName}"."${tableName}" - SET "${migrationColumn.columnName}" = ${defaultValue} - WHERE "${migrationColumn.columnName}" NOT IN (${enumValues + SET "${columnDefinition.columnName}" = ${defaultValue} + WHERE "${columnDefinition.columnName}" NOT IN (${enumValues .map((e) => `'${e}'`) .join(', ')}) `); diff --git a/server/src/workspace/workspace-migration-runner/workspace-migration-runner.service.ts b/server/src/workspace/workspace-migration-runner/workspace-migration-runner.service.ts index b39f985be..30b079201 100644 --- a/server/src/workspace/workspace-migration-runner/workspace-migration-runner.service.ts +++ b/server/src/workspace/workspace-migration-runner/workspace-migration-runner.service.ts @@ -237,7 +237,7 @@ export class WorkspaceMigrationRunnerService { tableName: string, 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 if (enumValues) { @@ -248,18 +248,32 @@ export class WorkspaceMigrationRunnerService { tableName, migrationColumn, ); - } else { - await queryRunner.changeColumn( - `${schemaName}.${tableName}`, - migrationColumn.columnName, - new TableColumn({ - name: migrationColumn.columnName, - type: migrationColumn.columnType, - default: migrationColumn.defaultValue, - isNullable: migrationColumn.isNullable, - }), - ); } + + await queryRunner.changeColumn( + `${schemaName}.${tableName}`, + new TableColumn({ + name: migrationColumn.currentColumnDefinition.columnName, + type: migrationColumn.currentColumnDefinition.columnType, + default: migrationColumn.currentColumnDefinition.defaultValue, + 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(