Fallback to default value when migrating value from enum (#6517)

When migrating the option values of a select type, if the field is non
nullable (for now, only available for opportunity's "stage" standard
field), we fallback to the (potentially updated) default value instead
of nullifying the value to avoid getting a database error.

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Marie
2024-08-03 17:53:01 +02:00
committed by GitHub
parent 5f88caf409
commit 6e0c1b4c73
16 changed files with 154 additions and 57 deletions

View File

@ -100,6 +100,11 @@ export type FieldRichTextMetadata = {
fieldName: string;
};
export type FieldPositionMetadata = {
objectMetadataNameSingular?: string;
fieldName: string;
};
export type FieldDefinitionRelationType =
| 'FROM_MANY_OBJECTS'
| 'FROM_ONE_OBJECT'

View File

@ -0,0 +1,9 @@
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { FieldDefinition } from '../FieldDefinition';
import { FieldMetadata, FieldPositionMetadata } from '../FieldMetadata';
export const isFieldPosition = (
field: Pick<FieldDefinition<FieldMetadata>, 'type'>,
): field is FieldDefinition<FieldPositionMetadata> =>
field.type === FieldMetadataType.Position;

View File

@ -0,0 +1,8 @@
import { isNumber } from '@sniptt/guards';
import { FieldPositionMetadata } from '../FieldMetadata';
// TODO: add zod
export const isFieldPhoneValue = (
fieldValue: unknown,
): fieldValue is FieldPositionMetadata =>
isNumber(fieldValue) || fieldValue === 'first' || fieldValue === 'last';

View File

@ -22,6 +22,7 @@ import { isFieldMultiSelect } from '@/object-record/record-field/types/guards/is
import { isFieldMultiSelectValue } from '@/object-record/record-field/types/guards/isFieldMultiSelectValue';
import { isFieldNumber } from '@/object-record/record-field/types/guards/isFieldNumber';
import { isFieldPhone } from '@/object-record/record-field/types/guards/isFieldPhone';
import { isFieldPosition } from '@/object-record/record-field/types/guards/isFieldPosition';
import { isFieldRating } from '@/object-record/record-field/types/guards/isFieldRating';
import { isFieldRawJson } from '@/object-record/record-field/types/guards/isFieldRawJson';
import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation';
@ -58,7 +59,8 @@ export const isFieldValueEmpty = ({
isFieldRelation(fieldDefinition) ||
isFieldRawJson(fieldDefinition) ||
isFieldRichText(fieldDefinition) ||
isFieldPhone(fieldDefinition)
isFieldPhone(fieldDefinition) ||
isFieldPosition(fieldDefinition)
) {
return isValueEmpty(fieldValue);
}

View File

@ -18,7 +18,7 @@ export const getCurrencyFieldPreviewValue = ({
const placeholderDefaultValue = getSettingsFieldTypeConfig(
FieldMetadataType.Currency,
).defaultValue;
).exampleValue;
return currencyFieldDefaultValueSchema
.transform((value) => ({

View File

@ -0,0 +1,42 @@
import { FieldMetadataDefaultSerializableValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface';
export const unserializeDefaultValue = (
serializedDefaultValue: FieldMetadataDefaultSerializableValue,
): any => {
if (serializedDefaultValue === null) {
return null;
}
if (typeof serializedDefaultValue === 'number') {
return serializedDefaultValue;
}
if (typeof serializedDefaultValue === 'boolean') {
return serializedDefaultValue;
}
if (typeof serializedDefaultValue === 'string') {
return serializedDefaultValue.replace(/'/g, '');
}
if (Array.isArray(serializedDefaultValue)) {
return serializedDefaultValue.map((value) =>
unserializeDefaultValue(value),
);
}
if (typeof serializedDefaultValue === 'object') {
return Object.entries(serializedDefaultValue).reduce(
(acc, [key, value]) => {
acc[key] = unserializeDefaultValue(value);
return acc;
},
{},
);
}
throw new Error(
`Invalid serialized default value "${serializedDefaultValue}"`,
);
};

View File

@ -34,6 +34,7 @@ export const buildMigrationsForCustomObjectRelations = (
}),
columnType: 'uuid',
isNullable: true,
defaultValue: null,
} satisfies WorkspaceMigrationColumnCreate,
],
},
@ -64,6 +65,7 @@ export const buildMigrationsForCustomObjectRelations = (
}),
columnType: 'uuid',
isNullable: true,
defaultValue: null,
} satisfies WorkspaceMigrationColumnCreate,
],
},
@ -94,6 +96,7 @@ export const buildMigrationsForCustomObjectRelations = (
}),
columnType: 'uuid',
isNullable: true,
defaultValue: null,
} satisfies WorkspaceMigrationColumnCreate,
],
},
@ -124,6 +127,7 @@ export const buildMigrationsForCustomObjectRelations = (
}),
columnType: 'uuid',
isNullable: true,
defaultValue: null,
} satisfies WorkspaceMigrationColumnCreate,
],
},
@ -154,6 +158,7 @@ export const buildMigrationsForCustomObjectRelations = (
}),
columnType: 'uuid',
isNullable: true,
defaultValue: null,
} satisfies WorkspaceMigrationColumnCreate,
],
},
@ -184,6 +189,7 @@ export const buildMigrationsForCustomObjectRelations = (
}),
columnType: 'uuid',
isNullable: true,
defaultValue: null,
} satisfies WorkspaceMigrationColumnCreate,
],
},
@ -211,6 +217,7 @@ export const buildMigrationsForCustomObjectRelations = (
columnName: 'position',
columnType: 'float',
isNullable: true,
defaultValue: null,
} satisfies WorkspaceMigrationColumnCreate,
],
} satisfies WorkspaceMigrationTableAction,
@ -224,6 +231,7 @@ export const buildMigrationsForCustomObjectRelations = (
columnName: 'name',
columnType: 'text',
defaultValue: "'Untitled'",
isNullable: false,
} satisfies WorkspaceMigrationColumnCreate,
],
} satisfies WorkspaceMigrationTableAction,

View File

@ -2,36 +2,36 @@ import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
import { FindOneOptions, In, Repository } from 'typeorm';
import camelCase from 'lodash.camelcase';
import { FindOneOptions, In, Repository } from 'typeorm';
import { v4 as uuidV4 } from 'uuid';
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/field-metadata.service';
import { CreateRelationInput } from 'src/engine/metadata-modules/relation-metadata/dtos/create-relation.input';
import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service';
import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.service';
import {
FieldMetadataEntity,
FieldMetadataType,
} from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/field-metadata.service';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
import { CreateRelationInput } from 'src/engine/metadata-modules/relation-metadata/dtos/create-relation.input';
import {
RelationMetadataException,
RelationMetadataExceptionCode,
} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.exception';
import {
InvalidStringException,
validateMetadataNameOrThrow,
} from 'src/engine/metadata-modules/utils/validate-metadata-name.utils';
import { WorkspaceCacheVersionService } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.service';
import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util';
import {
WorkspaceMigrationColumnActionType,
WorkspaceMigrationColumnDrop,
WorkspaceMigrationTableActionType,
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.service';
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util';
import {
validateMetadataNameOrThrow,
InvalidStringException,
} from 'src/engine/metadata-modules/utils/validate-metadata-name.utils';
import { WorkspaceCacheVersionService } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.service';
import {
RelationMetadataException,
RelationMetadataExceptionCode,
} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.exception';
import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service';
import {
RelationMetadataEntity,
@ -213,6 +213,7 @@ export class RelationMetadataService extends TypeOrmQueryService<RelationMetadat
columnName,
columnType: 'uuid',
isNullable: true,
defaultValue: null,
},
],
},

View File

@ -15,10 +15,10 @@ import { WorkspaceCacheVersionService } from 'src/engine/metadata-modules/worksp
import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util';
import {
ReferencedTable,
WorkspaceMigrationTableActionType,
WorkspaceMigrationColumnAction,
WorkspaceMigrationForeignColumnDefinition,
WorkspaceMigrationForeignTable,
WorkspaceMigrationColumnAction,
WorkspaceMigrationTableActionType,
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.service';
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
@ -77,6 +77,8 @@ export class ForeignTableService {
columnName: getForeignTableColumnName(column.columnName),
columnType: column.dataType,
distantColumnName: column.columnName,
isNullable: false,
defaultValue: null,
}) satisfies WorkspaceMigrationForeignColumnDefinition,
),
referencedTable,

View File

@ -1,9 +1,9 @@
import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import {
WorkspaceMigrationTableAction,
WorkspaceMigrationColumnActionType,
WorkspaceMigrationColumnCreate,
WorkspaceMigrationTableAction,
WorkspaceMigrationTableActionType,
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
@ -24,6 +24,7 @@ export const buildMigrationsToCreateRemoteTableRelations = (
}),
columnType: primaryKeyColumnType,
isNullable: true,
defaultValue: null,
} satisfies WorkspaceMigrationColumnCreate,
],
}));

View File

@ -1,15 +1,15 @@
import { Injectable } from '@nestjs/common';
import { getForeignTableColumnName as convertToForeignTableColumnName } from 'src/engine/metadata-modules/remote-server/remote-table/foreign-table/utils/get-foreign-table-column-name.util';
import { DistantTables } from 'src/engine/metadata-modules/remote-server/remote-table/distant-table/types/distant-table';
import { DistantTableUpdate } from 'src/engine/metadata-modules/remote-server/remote-table/dtos/remote-table.dto';
import { getForeignTableColumnName as convertToForeignTableColumnName } from 'src/engine/metadata-modules/remote-server/remote-table/foreign-table/utils/get-foreign-table-column-name.util';
import { RemoteTableEntity } from 'src/engine/metadata-modules/remote-server/remote-table/remote-table.entity';
import { fetchTableColumns } from 'src/engine/metadata-modules/remote-server/remote-table/utils/fetch-table-columns.util';
import { PostgresTableSchemaColumn } from 'src/engine/metadata-modules/remote-server/types/postgres-table-schema-column';
import {
WorkspaceMigrationColumnAction,
WorkspaceMigrationColumnCreate,
WorkspaceMigrationColumnActionType,
WorkspaceMigrationColumnCreate,
WorkspaceMigrationColumnDrop,
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
@ -33,6 +33,8 @@ export class RemoteTableSchemaUpdateService {
action: WorkspaceMigrationColumnActionType.CREATE,
columnName: columnAdded.name,
columnType: columnAdded.type,
isNullable: true,
defaultValue: null,
}));
const columnsDeletedUpdates: WorkspaceMigrationColumnDrop[] =

View File

@ -1,18 +1,18 @@
import { Injectable, Logger } from '@nestjs/common';
import { WorkspaceColumnActionOptions } from 'src/engine/metadata-modules/workspace-migration/interfaces/workspace-column-action-options.interface';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { WorkspaceColumnActionOptions } from 'src/engine/metadata-modules/workspace-migration/interfaces/workspace-column-action-options.interface';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
import { serializeDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/serialize-default-value';
import { ColumnActionAbstractFactory } from 'src/engine/metadata-modules/workspace-migration/factories/column-action-abstract.factory';
import { fieldMetadataTypeToColumnType } from 'src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util';
import {
WorkspaceMigrationColumnActionType,
WorkspaceMigrationColumnAlter,
WorkspaceMigrationColumnCreate,
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
import { serializeDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/serialize-default-value';
import { fieldMetadataTypeToColumnType } from 'src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util';
import { ColumnActionAbstractFactory } from 'src/engine/metadata-modules/workspace-migration/factories/column-action-abstract.factory';
import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
import {
WorkspaceMigrationException,
WorkspaceMigrationExceptionCode,
@ -48,7 +48,7 @@ export class BasicColumnActionFactory extends ColumnActionAbstractFactory<BasicF
action: WorkspaceMigrationColumnActionType.CREATE,
columnName,
columnType: fieldMetadataTypeToColumnType(fieldMetadata.type),
isNullable: fieldMetadata.isNullable,
isNullable: fieldMetadata.isNullable ?? true,
defaultValue: serializedDefaultValue,
},
];
@ -81,7 +81,7 @@ export class BasicColumnActionFactory extends ColumnActionAbstractFactory<BasicF
currentColumnDefinition: {
columnName: currentColumnName,
columnType: fieldMetadataTypeToColumnType(currentFieldMetadata.type),
isNullable: currentFieldMetadata.isNullable,
isNullable: currentFieldMetadata.isNullable ?? true,
defaultValue: serializeDefaultValue(
currentFieldMetadata.defaultValue,
),
@ -89,7 +89,7 @@ export class BasicColumnActionFactory extends ColumnActionAbstractFactory<BasicF
alteredColumnDefinition: {
columnName: alteredColumnName,
columnType: fieldMetadataTypeToColumnType(alteredFieldMetadata.type),
isNullable: alteredFieldMetadata.isNullable,
isNullable: alteredFieldMetadata.isNullable ?? true,
defaultValue: serializedDefaultValue,
},
},

View File

@ -1,18 +1,18 @@
import { Injectable, Logger } from '@nestjs/common';
import { WorkspaceColumnActionOptions } from 'src/engine/metadata-modules/workspace-migration/interfaces/workspace-column-action-options.interface';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { WorkspaceColumnActionOptions } from 'src/engine/metadata-modules/workspace-migration/interfaces/workspace-column-action-options.interface';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
import { serializeDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/serialize-default-value';
import { ColumnActionAbstractFactory } from 'src/engine/metadata-modules/workspace-migration/factories/column-action-abstract.factory';
import { fieldMetadataTypeToColumnType } from 'src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util';
import {
WorkspaceMigrationColumnActionType,
WorkspaceMigrationColumnAlter,
WorkspaceMigrationColumnCreate,
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
import { serializeDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/serialize-default-value';
import { fieldMetadataTypeToColumnType } from 'src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util';
import { ColumnActionAbstractFactory } from 'src/engine/metadata-modules/workspace-migration/factories/column-action-abstract.factory';
import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
import {
WorkspaceMigrationException,
WorkspaceMigrationExceptionCode,
@ -45,7 +45,7 @@ export class EnumColumnActionFactory extends ColumnActionAbstractFactory<EnumFie
columnType: fieldMetadataTypeToColumnType(fieldMetadata.type),
enum: enumOptions,
isArray: fieldMetadata.type === FieldMetadataType.MULTI_SELECT,
isNullable: fieldMetadata.isNullable,
isNullable: fieldMetadata.isNullable ?? true,
defaultValue: serializedDefaultValue,
},
];
@ -102,7 +102,7 @@ export class EnumColumnActionFactory extends ColumnActionAbstractFactory<EnumFie
? [...currentFieldMetadata.options.map((option) => option.value)]
: undefined,
isArray: currentFieldMetadata.type === FieldMetadataType.MULTI_SELECT,
isNullable: currentFieldMetadata.isNullable,
isNullable: currentFieldMetadata.isNullable ?? true,
defaultValue: serializeDefaultValue(
currentFieldMetadata.defaultValue,
),
@ -112,7 +112,7 @@ export class EnumColumnActionFactory extends ColumnActionAbstractFactory<EnumFie
columnType: fieldMetadataTypeToColumnType(alteredFieldMetadata.type),
enum: enumOptions,
isArray: alteredFieldMetadata.type === FieldMetadataType.MULTI_SELECT,
isNullable: alteredFieldMetadata.isNullable,
isNullable: alteredFieldMetadata.isNullable ?? true,
defaultValue: serializedDefaultValue,
},
},

View File

@ -28,8 +28,8 @@ export interface WorkspaceMigrationColumnDefinition {
columnType: string;
enum?: WorkspaceMigrationEnum[];
isArray?: boolean;
isNullable?: boolean;
defaultValue?: any;
isNullable: boolean;
defaultValue: any;
}
export interface WorkspaceMigrationIndexAction {

View File

@ -20,7 +20,6 @@ import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-s
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
import { CleanInactiveWorkspacesCommandOptions } from 'src/engine/workspace-manager/workspace-cleaner/commands/clean-inactive-workspaces.command';
import { getDryRunLogHeader } from 'src/utils/get-dry-run-log-header';
@ -39,7 +38,7 @@ export class CleanInactiveWorkspaceJob {
private readonly inactiveDaysBeforeEmail;
constructor(
@InjectRepository(WorkspaceEntity, 'core')
@InjectRepository(Workspace, 'core')
private readonly workspaceRepository: Repository<Workspace>,
private readonly dataSourceService: DataSourceService,
private readonly objectMetadataService: ObjectMetadataService,

View File

@ -1,14 +1,15 @@
import { Injectable } from '@nestjs/common';
import { isDefined } from 'class-validator';
import { QueryRunner, TableColumn } from 'typeorm';
import { v4 } from 'uuid';
import { isDefined } from 'class-validator';
import { serializeDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/serialize-default-value';
import { unserializeDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/unserialize-default-value';
import {
WorkspaceMigrationColumnAlter,
WorkspaceMigrationRenamedEnum,
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
import { serializeDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/serialize-default-value';
@Injectable()
export class WorkspaceMigrationEnumService {
@ -111,11 +112,17 @@ export class WorkspaceMigrationEnumService {
`);
}
private migrateEnumValue(
value: string,
renamedEnumValues?: WorkspaceMigrationRenamedEnum[],
allEnumValues?: string[],
) {
private migrateEnumValue({
value,
renamedEnumValues,
allEnumValues,
defaultValueFallback,
}: {
value: string;
renamedEnumValues?: WorkspaceMigrationRenamedEnum[];
allEnumValues?: string[];
defaultValueFallback?: string;
}) {
if (renamedEnumValues?.find((enumVal) => enumVal?.from === value)?.to) {
return renamedEnumValues?.find((enumVal) => enumVal?.from === value)?.to;
}
@ -124,6 +131,10 @@ export class WorkspaceMigrationEnumService {
return value;
}
if (isDefined(defaultValueFallback)) {
return defaultValueFallback;
}
return null;
}
@ -152,16 +163,23 @@ export class WorkspaceMigrationEnumService {
.split(',')
.map((v: string) => v.trim())
.map((v: string) =>
this.migrateEnumValue(v, renamedEnumValues, enumValues),
this.migrateEnumValue({
value: v,
renamedEnumValues: renamedEnumValues,
allEnumValues: enumValues,
}),
)
.filter((v: string | null) => isDefined(v)),
);
} else if (typeof val === 'string') {
const migratedValue = this.migrateEnumValue(
val,
renamedEnumValues,
enumValues,
);
const migratedValue = this.migrateEnumValue({
value: val,
renamedEnumValues: renamedEnumValues,
allEnumValues: enumValues,
defaultValueFallback: columnDefinition.isNullable
? null
: unserializeDefaultValue(columnDefinition.defaultValue),
});
val = isDefined(migratedValue) ? `'${migratedValue}'` : null;
}