Workspace schema migration runner v2 - Fix Enums and Create TsVector (#13955)

## Context
- Adding ts-vector generatedType/asExpression as TS_VECTOR settings
- Using those settings to setup properly tsVector searchVector column
through the new migration runner
- Fix enum creation/suppression

Note: regarding the new tsVector, we should implement a command to
update existing fields

TODO: 
- TS_VECTOR search vector column update (note: should be properly
updated whenever the object labelIdentifier is updated or a new TEXT
field is added to the object to follow the current logic)
- relation type fields and columns are not implemented yet 
- index migrations
This commit is contained in:
Weiko
2025-08-18 16:33:51 +02:00
committed by GitHub
parent 3d3191425e
commit 43bb8d6043
16 changed files with 280 additions and 297 deletions
@@ -6,6 +6,7 @@ import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfa
import { getFlatFieldMetadataMock } from 'src/engine/metadata-modules/flat-field-metadata/__mocks__/get-flat-field-metadata.mock';
import { getFlatObjectMetadataMock } from 'src/engine/metadata-modules/flat-object-metadata/__mocks__/get-flat-object-metadata.mock';
import { type WorkspaceSchemaManagerService } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.service';
import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
import { WorkspaceSchemaFieldActionRunnerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-schema-migration-runner/workspace-schema-field-action-runner.service';
describe('WorkspaceSchemaFieldActionRunner', () => {
@@ -14,6 +15,7 @@ describe('WorkspaceSchemaFieldActionRunner', () => {
let mockQueryRunner: jest.Mocked<QueryRunner>;
const mockWorkspaceId = '20202020-1c25-4d02-bf25-6aeccf7ea419';
const mockSchemaName = getWorkspaceSchemaName(mockWorkspaceId);
const mockObjectMetadataId = '20202020-1c25-4d02-bf25-6aeccf7ea418';
const mockFieldMetadataId = '20202020-1c25-4d02-bf25-6aeccf7ea417';
@@ -99,7 +101,7 @@ describe('WorkspaceSchemaFieldActionRunner', () => {
mockSchemaManagerService.columnManager.dropColumns,
).toHaveBeenCalledWith({
queryRunner: mockQueryRunner,
schemaName: 'workspace_1wgvd1injqtife6y4rvfbu3h5',
schemaName: mockSchemaName,
tableName: '_person',
columnNames: [
'homeAddressAddressStreet1',
@@ -171,7 +173,7 @@ describe('WorkspaceSchemaFieldActionRunner', () => {
mockSchemaManagerService.columnManager.dropColumns,
).toHaveBeenCalledWith({
queryRunner: mockQueryRunner,
schemaName: 'workspace_1wgvd1injqtife6y4rvfbu3h5',
schemaName: mockSchemaName,
tableName: '_person',
columnNames: ['status'],
});
@@ -181,7 +183,7 @@ describe('WorkspaceSchemaFieldActionRunner', () => {
mockSchemaManagerService.enumManager.dropEnum,
).toHaveBeenCalledWith({
queryRunner: mockQueryRunner,
schemaName: 'workspace_1wgvd1injqtife6y4rvfbu3h5',
schemaName: mockSchemaName,
enumName: '_person_status_enum',
});
});
@@ -226,7 +228,7 @@ describe('WorkspaceSchemaFieldActionRunner', () => {
mockSchemaManagerService.columnManager.dropColumns,
).toHaveBeenCalledWith({
queryRunner: mockQueryRunner,
schemaName: 'workspace_1wgvd1injqtife6y4rvfbu3h5',
schemaName: mockSchemaName,
tableName: '_person',
columnNames: ['companyId'],
});
@@ -298,7 +300,7 @@ describe('WorkspaceSchemaFieldActionRunner', () => {
mockSchemaManagerService.enumManager.createEnum,
).toHaveBeenCalledWith({
queryRunner: mockQueryRunner,
schemaName: 'workspace_1wgvd1injqtife6y4rvfbu3h5',
schemaName: mockSchemaName,
enumName: '_task_priority_enum',
values: ['HIGH', 'LOW'],
});
@@ -308,17 +310,16 @@ describe('WorkspaceSchemaFieldActionRunner', () => {
mockSchemaManagerService.columnManager.addColumns,
).toHaveBeenCalledWith({
queryRunner: mockQueryRunner,
schemaName: 'workspace_1wgvd1injqtife6y4rvfbu3h5',
schemaName: mockSchemaName,
tableName: '_task',
columnDefinitions: [
{
name: 'priority',
type: '_task_priority_enum',
type: `"${mockSchemaName}"."_task_priority_enum"`,
isNullable: true,
isArray: false,
isUnique: false,
default: null,
enumValues: ['HIGH', 'LOW'],
},
],
});
@@ -367,7 +368,7 @@ describe('WorkspaceSchemaFieldActionRunner', () => {
mockSchemaManagerService.columnManager.addColumns,
).toHaveBeenCalledWith({
queryRunner: mockQueryRunner,
schemaName: 'workspace_1wgvd1injqtife6y4rvfbu3h5',
schemaName: mockSchemaName,
tableName: '_employee',
columnDefinitions: [
{
@@ -452,7 +453,7 @@ describe('WorkspaceSchemaFieldActionRunner', () => {
mockSchemaManagerService.columnManager.renameColumn,
).toHaveBeenCalledWith({
queryRunner: mockQueryRunner,
schemaName: 'workspace_1wgvd1injqtife6y4rvfbu3h5',
schemaName: mockSchemaName,
tableName: '_company',
oldColumnName: fromName,
newColumnName: toName,
@@ -550,16 +551,16 @@ describe('WorkspaceSchemaFieldActionRunner', () => {
mockSchemaManagerService.enumManager.alterEnumValues,
).toHaveBeenCalledWith({
queryRunner: mockQueryRunner,
schemaName: 'workspace_1wgvd1injqtife6y4rvfbu3h5',
schemaName: mockSchemaName,
tableName: '_person',
enumValues: ['UPDATED_ACTIVE', 'UPDATED_INACTIVE'],
columnDefinition: {
name: 'status',
type: '_person_status_enum',
type: `"${mockSchemaName}"."_person_status_enum"`,
isNullable: true,
isArray: false,
isUnique: false,
default: null,
enumValues: ['UPDATED_ACTIVE', 'UPDATED_INACTIVE'],
},
oldToNewEnumOptionMap: {
ACTIVE: 'UPDATED_ACTIVE', // Keep original values
@@ -622,7 +623,7 @@ describe('WorkspaceSchemaFieldActionRunner', () => {
mockSchemaManagerService.columnManager.alterColumnDefault,
).toHaveBeenCalledWith({
queryRunner: mockQueryRunner,
schemaName: 'workspace_1wgvd1injqtife6y4rvfbu3h5',
schemaName: mockSchemaName,
tableName: '_product',
columnName: 'priceAmountMicros',
defaultValue: '100000000',
@@ -632,7 +633,7 @@ describe('WorkspaceSchemaFieldActionRunner', () => {
mockSchemaManagerService.columnManager.alterColumnDefault,
).toHaveBeenCalledWith({
queryRunner: mockQueryRunner,
schemaName: 'workspace_1wgvd1injqtife6y4rvfbu3h5',
schemaName: mockSchemaName,
tableName: '_product',
columnName: 'priceCurrencyCode',
defaultValue: 'EUR',
@@ -678,7 +679,7 @@ describe('WorkspaceSchemaFieldActionRunner', () => {
mockSchemaManagerService.columnManager.dropColumns,
).toHaveBeenCalledWith({
queryRunner: mockQueryRunner,
schemaName: 'workspace_1wgvd1injqtife6y4rvfbu3h5',
schemaName: mockSchemaName,
tableName: '_person',
columnNames: [], // Empty array - no columns to drop
});
@@ -720,7 +721,7 @@ describe('WorkspaceSchemaFieldActionRunner', () => {
mockSchemaManagerService.enumManager.createEnum,
).toHaveBeenCalledWith({
queryRunner: mockQueryRunner,
schemaName: 'workspace_1wgvd1injqtife6y4rvfbu3h5',
schemaName: mockSchemaName,
enumName: '_test_emptyStatus_enum',
values: [], // Empty enum values array
});
@@ -4,6 +4,7 @@ import { type QueryRunner } from 'typeorm';
import { getFlatFieldMetadataMock } from 'src/engine/metadata-modules/flat-field-metadata/__mocks__/get-flat-field-metadata.mock';
import { getFlatObjectMetadataMock } from 'src/engine/metadata-modules/flat-object-metadata/__mocks__/get-flat-object-metadata.mock';
import { type WorkspaceSchemaManagerService } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.service';
import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
import { WorkspaceSchemaObjectActionRunnerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-schema-migration-runner/workspace-schema-object-action-runner.service';
describe('WorkspaceSchemaObjectActionRunner', () => {
@@ -13,6 +14,7 @@ describe('WorkspaceSchemaObjectActionRunner', () => {
const mockWorkspaceId = '20202020-1c25-4d02-bf25-6aeccf7ea419';
const mockObjectMetadataId = '20202020-1c25-4d02-bf25-6aeccf7ea418';
const mockSchemaName = getWorkspaceSchemaName(mockWorkspaceId);
const createMockFlatObjectMetadataMaps = (
objectMetadata: any,
@@ -121,7 +123,7 @@ describe('WorkspaceSchemaObjectActionRunner', () => {
mockSchemaManagerService.tableManager.dropTable,
).toHaveBeenCalledWith({
queryRunner: mockQueryRunner,
schemaName: 'workspace_1wgvd1injqtife6y4rvfbu3h5',
schemaName: mockSchemaName,
tableName: '_task',
});
@@ -133,14 +135,14 @@ describe('WorkspaceSchemaObjectActionRunner', () => {
mockSchemaManagerService.enumManager.dropEnum,
).toHaveBeenCalledWith({
queryRunner: mockQueryRunner,
schemaName: 'workspace_1wgvd1injqtife6y4rvfbu3h5',
schemaName: mockSchemaName,
enumName: '_task_status_enum',
});
expect(
mockSchemaManagerService.enumManager.dropEnum,
).toHaveBeenCalledWith({
queryRunner: mockQueryRunner,
schemaName: 'workspace_1wgvd1injqtife6y4rvfbu3h5',
schemaName: mockSchemaName,
enumName: '_task_priority_enum',
});
});
@@ -202,7 +204,7 @@ describe('WorkspaceSchemaObjectActionRunner', () => {
mockSchemaManagerService.tableManager.dropTable,
).toHaveBeenCalledWith({
queryRunner: mockQueryRunner,
schemaName: 'workspace_1wgvd1injqtife6y4rvfbu3h5',
schemaName: mockSchemaName,
tableName: '_project',
});
@@ -214,7 +216,7 @@ describe('WorkspaceSchemaObjectActionRunner', () => {
mockSchemaManagerService.enumManager.dropEnum,
).toHaveBeenCalledWith({
queryRunner: mockQueryRunner,
schemaName: 'workspace_1wgvd1injqtife6y4rvfbu3h5',
schemaName: mockSchemaName,
enumName: '_project_tags_enum',
});
});
@@ -253,7 +255,7 @@ describe('WorkspaceSchemaObjectActionRunner', () => {
mockSchemaManagerService.tableManager.dropTable,
).toHaveBeenCalledWith({
queryRunner: mockQueryRunner,
schemaName: 'workspace_1wgvd1injqtife6y4rvfbu3h5',
schemaName: mockSchemaName,
tableName: '_simpleObject',
});
@@ -350,7 +352,7 @@ describe('WorkspaceSchemaObjectActionRunner', () => {
mockSchemaManagerService.tableManager.createTable,
).toHaveBeenCalledWith({
queryRunner: mockQueryRunner,
schemaName: 'workspace_1wgvd1injqtife6y4rvfbu3h5',
schemaName: mockSchemaName,
tableName: '_article',
columnDefinitions: [
// TEXT field column
@@ -361,17 +363,15 @@ describe('WorkspaceSchemaObjectActionRunner', () => {
isArray: false,
isUnique: false,
default: null,
enumValues: undefined,
},
// SELECT field column
{
name: 'status',
type: '_article_status_enum',
type: `"${mockSchemaName}"."_article_status_enum"`,
isNullable: true,
isArray: false,
isUnique: false,
default: null,
enumValues: ['DRAFT', 'PUBLISHED'],
},
// CURRENCY field columns (2 composite columns)
{
@@ -397,7 +397,7 @@ describe('WorkspaceSchemaObjectActionRunner', () => {
mockSchemaManagerService.enumManager.createEnum,
).toHaveBeenCalledWith({
queryRunner: mockQueryRunner,
schemaName: 'workspace_1wgvd1injqtife6y4rvfbu3h5',
schemaName: mockSchemaName,
enumName: '_article_status_enum',
values: ['DRAFT', 'PUBLISHED'],
});
@@ -449,13 +449,13 @@ describe('WorkspaceSchemaObjectActionRunner', () => {
mockSchemaManagerService.tableManager.createTable,
).toHaveBeenCalledWith({
queryRunner: mockQueryRunner,
schemaName: 'workspace_1wgvd1injqtife6y4rvfbu3h5',
schemaName: mockSchemaName,
tableName: '_company',
columnDefinitions: [
{
name: 'headquartersAddressStreet1',
type: 'text',
isNullable: false,
isNullable: true,
isUnique: false,
default: null,
isArray: false,
@@ -463,7 +463,7 @@ describe('WorkspaceSchemaObjectActionRunner', () => {
{
name: 'headquartersAddressStreet2',
type: 'text',
isNullable: false,
isNullable: true,
isUnique: false,
default: null,
isArray: false,
@@ -471,7 +471,7 @@ describe('WorkspaceSchemaObjectActionRunner', () => {
{
name: 'headquartersAddressCity',
type: 'text',
isNullable: false,
isNullable: true,
isUnique: false,
default: null,
isArray: false,
@@ -479,7 +479,7 @@ describe('WorkspaceSchemaObjectActionRunner', () => {
{
name: 'headquartersAddressPostcode',
type: 'text',
isNullable: false,
isNullable: true,
isUnique: false,
default: null,
isArray: false,
@@ -487,7 +487,7 @@ describe('WorkspaceSchemaObjectActionRunner', () => {
{
name: 'headquartersAddressState',
type: 'text',
isNullable: false,
isNullable: true,
isUnique: false,
default: null,
isArray: false,
@@ -495,7 +495,7 @@ describe('WorkspaceSchemaObjectActionRunner', () => {
{
name: 'headquartersAddressCountry',
type: 'text',
isNullable: false,
isNullable: true,
isUnique: false,
default: null,
isArray: false,
@@ -503,7 +503,7 @@ describe('WorkspaceSchemaObjectActionRunner', () => {
{
name: 'headquartersAddressLat',
type: 'numeric',
isNullable: false,
isNullable: true,
isUnique: false,
default: null,
isArray: false,
@@ -511,7 +511,7 @@ describe('WorkspaceSchemaObjectActionRunner', () => {
{
name: 'headquartersAddressLng',
type: 'numeric',
isNullable: false,
isNullable: true,
isUnique: false,
default: null,
isArray: false,
@@ -555,7 +555,7 @@ describe('WorkspaceSchemaObjectActionRunner', () => {
mockSchemaManagerService.tableManager.createTable,
).toHaveBeenCalledWith({
queryRunner: mockQueryRunner,
schemaName: 'workspace_1wgvd1injqtife6y4rvfbu3h5',
schemaName: mockSchemaName,
tableName: '_emptyObject',
columnDefinitions: [], // Empty columns array
});
@@ -629,7 +629,7 @@ describe('WorkspaceSchemaObjectActionRunner', () => {
mockSchemaManagerService.tableManager.renameTable,
).toHaveBeenCalledWith({
queryRunner: mockQueryRunner,
schemaName: 'workspace_1wgvd1injqtife6y4rvfbu3h5',
schemaName: mockSchemaName,
oldTableName: '_blogPost',
newTableName: '_article',
});
@@ -642,7 +642,7 @@ describe('WorkspaceSchemaObjectActionRunner', () => {
mockSchemaManagerService.enumManager.renameEnum,
).toHaveBeenCalledWith({
queryRunner: mockQueryRunner,
schemaName: 'workspace_1wgvd1injqtife6y4rvfbu3h5',
schemaName: mockSchemaName,
oldEnumName: '_blogPost_category_enum',
newEnumName: '_article_category_enum',
});
@@ -650,7 +650,7 @@ describe('WorkspaceSchemaObjectActionRunner', () => {
mockSchemaManagerService.enumManager.renameEnum,
).toHaveBeenCalledWith({
queryRunner: mockQueryRunner,
schemaName: 'workspace_1wgvd1injqtife6y4rvfbu3h5',
schemaName: mockSchemaName,
oldEnumName: '_blogPost_tags_enum',
newEnumName: '_article_tags_enum',
});
@@ -751,7 +751,7 @@ describe('WorkspaceSchemaObjectActionRunner', () => {
mockSchemaManagerService.tableManager.renameTable,
).toHaveBeenCalledWith({
queryRunner: mockQueryRunner,
schemaName: 'workspace_1wgvd1injqtife6y4rvfbu3h5',
schemaName: mockSchemaName,
oldTableName: '_oldEntity',
newTableName: '_newEntity',
});
@@ -764,7 +764,7 @@ describe('WorkspaceSchemaObjectActionRunner', () => {
mockSchemaManagerService.enumManager.renameEnum,
).toHaveBeenCalledWith({
queryRunner: mockQueryRunner,
schemaName: 'workspace_1wgvd1injqtife6y4rvfbu3h5',
schemaName: mockSchemaName,
oldEnumName: '_oldEntity_status_enum',
newEnumName: '_newEntity_status_enum',
});
@@ -810,7 +810,7 @@ describe('WorkspaceSchemaObjectActionRunner', () => {
mockSchemaManagerService.tableManager.createTable,
).toHaveBeenCalledWith({
queryRunner: mockQueryRunner,
schemaName: 'workspace_1wgvd1injqtife6y4rvfbu3h5',
schemaName: mockSchemaName,
tableName: '_testObject',
columnDefinitions: [],
});
@@ -859,7 +859,7 @@ describe('WorkspaceSchemaObjectActionRunner', () => {
mockSchemaManagerService.tableManager.renameTable,
).toHaveBeenCalledWith({
queryRunner: mockQueryRunner,
schemaName: 'workspace_1wgvd1injqtife6y4rvfbu3h5',
schemaName: mockSchemaName,
oldTableName: '_emptyFieldsObject',
newTableName: '_renamedEmptyFieldsObject',
});
@@ -2,6 +2,7 @@ import { FieldMetadataType } from 'twenty-shared/types';
import { getFlatFieldMetadataMock } from 'src/engine/metadata-modules/flat-field-metadata/__mocks__/get-flat-field-metadata.mock';
import { getFlatObjectMetadataMock } from 'src/engine/metadata-modules/flat-object-metadata/__mocks__/get-flat-object-metadata.mock';
import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
import { generateColumnDefinitions } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-schema-migration-runner/utils/generate-column-definitions.util';
describe('Generate Column Definitions', () => {
@@ -12,6 +13,8 @@ describe('Generate Column Definitions', () => {
nameSingular: 'person',
});
const mockSchemaName = getWorkspaceSchemaName(mockObjectMetadata.workspaceId);
describe('Enum Field Schema Generation', () => {
it('should generate deterministic enum names to prevent schema conflicts', () => {
const enumField = getFlatFieldMetadataMock({
@@ -38,7 +41,7 @@ describe('Generate Column Definitions', () => {
});
const columns = generateColumnDefinitions({
fieldMetadata: enumField,
flatFieldMetadata: enumField,
flatObjectMetadataWithoutFields: mockObjectMetadata,
});
@@ -48,8 +51,7 @@ describe('Generate Column Definitions', () => {
expect(column).toEqual({
name: 'status',
type: '_person_status_enum',
enumValues: ['ACTIVE', 'INACTIVE'],
type: `"${mockSchemaName}"."_person_status_enum"`,
isArray: false,
isNullable: true,
isUnique: false,
@@ -82,7 +84,7 @@ describe('Generate Column Definitions', () => {
});
const columns = generateColumnDefinitions({
fieldMetadata: multiSelectField,
flatFieldMetadata: multiSelectField,
flatObjectMetadataWithoutFields: mockObjectMetadata,
});
@@ -92,8 +94,7 @@ describe('Generate Column Definitions', () => {
expect(column).toEqual({
name: 'tags',
type: '_person_tags_enum',
enumValues: ['URGENT', 'LOW_PRIORITY'],
type: `"${mockSchemaName}"."_person_tags_enum"`,
isArray: true,
isNullable: true,
isUnique: false,
@@ -113,7 +114,7 @@ describe('Generate Column Definitions', () => {
});
const columns = generateColumnDefinitions({
fieldMetadata: relationField,
flatFieldMetadata: relationField,
flatObjectMetadataWithoutFields: mockObjectMetadata,
});
@@ -134,7 +135,7 @@ describe('Generate Column Definitions', () => {
});
const columns = generateColumnDefinitions({
fieldMetadata: relationField,
flatFieldMetadata: relationField,
flatObjectMetadataWithoutFields: mockObjectMetadata,
});
@@ -164,7 +165,7 @@ describe('Generate Column Definitions', () => {
});
const columns = generateColumnDefinitions({
fieldMetadata: addressField,
flatFieldMetadata: addressField,
flatObjectMetadataWithoutFields: mockObjectMetadata,
});
@@ -204,7 +205,7 @@ describe('Generate Column Definitions', () => {
});
const columns = generateColumnDefinitions({
fieldMetadata: currencyField,
flatFieldMetadata: currencyField,
flatObjectMetadataWithoutFields: mockObjectMetadata,
});
@@ -243,7 +244,7 @@ describe('Generate Column Definitions', () => {
});
const columns = generateColumnDefinitions({
fieldMetadata: textField,
flatFieldMetadata: textField,
flatObjectMetadataWithoutFields: mockObjectMetadata,
});
@@ -254,7 +255,6 @@ describe('Generate Column Definitions', () => {
isNullable: true,
isUnique: false,
default: null,
enumValues: undefined,
isArray: false,
},
]);
@@ -270,7 +270,7 @@ describe('Generate Column Definitions', () => {
});
const columns = generateColumnDefinitions({
fieldMetadata: booleanField,
flatFieldMetadata: booleanField,
flatObjectMetadataWithoutFields: mockObjectMetadata,
});
@@ -281,7 +281,6 @@ describe('Generate Column Definitions', () => {
isNullable: true,
isUnique: false,
default: true,
enumValues: undefined,
isArray: false,
},
]);
@@ -298,7 +297,7 @@ describe('Generate Column Definitions', () => {
});
const columns = generateColumnDefinitions({
fieldMetadata: textField,
flatFieldMetadata: textField,
flatObjectMetadataWithoutFields: mockObjectMetadata,
});
@@ -309,7 +308,6 @@ describe('Generate Column Definitions', () => {
isNullable: true,
isUnique: false,
default: null,
enumValues: undefined,
isArray: false,
},
]);
@@ -324,7 +322,7 @@ describe('Generate Column Definitions', () => {
});
const columns = generateColumnDefinitions({
fieldMetadata: uuidField,
flatFieldMetadata: uuidField,
flatObjectMetadataWithoutFields: mockObjectMetadata,
});
@@ -335,7 +333,6 @@ describe('Generate Column Definitions', () => {
isNullable: true,
isUnique: false,
default: null,
enumValues: undefined,
isArray: false,
},
]);
@@ -21,7 +21,6 @@ import {
WorkspaceSchemaMigrationException,
WorkspaceSchemaMigrationExceptionCode,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-schema-migration-runner/exceptions/workspace-schema-migration.exception';
import { getTsVectorColumnExpressionFromFields } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util';
import { getWorkspaceSchemaContextForMigration } from './get-workspace-schema-context-for-migration.util';
@@ -34,7 +33,7 @@ export const generateCompositeColumnDefinition = ({
parentFieldMetadata: FlatFieldMetadata<CompositeFieldMetadataType>;
flatObjectMetadataWithoutFields: FlatObjectMetadataWithoutFields;
}): WorkspaceSchemaColumnDefinition => {
const { tableName } = getWorkspaceSchemaContextForMigration({
const { tableName, schemaName } = getWorkspaceSchemaContextForMigration({
workspaceId: flatObjectMetadataWithoutFields.workspaceId,
flatObjectMetadataWithoutFields,
});
@@ -70,51 +69,47 @@ export const generateCompositeColumnDefinition = ({
name: columnName,
type:
columnType === 'enum'
? computePostgresEnumName({ tableName, columnName })
? `"${schemaName}"."${computePostgresEnumName({ tableName, columnName })}"`
: columnType,
// Align composite column nullability with parent field nullability by default
isNullable: parentFieldMetadata.isNullable ?? true,
isNullable: parentFieldMetadata.isNullable || !compositeProperty.isRequired,
isUnique: parentFieldMetadata.isUnique ?? false,
default: unserializedDefaultValue,
isArray: isArrayFlag,
};
if (columnType === 'enum') {
definition.enumValues = compositeProperty.options?.map(
(option) => option.value,
);
}
return definition;
};
const generateTsVectorColumnDefinition = (
fieldMetadata: FlatFieldMetadata<FieldMetadataType.TS_VECTOR>,
flatFieldMetadata: FlatFieldMetadata<FieldMetadataType.TS_VECTOR>,
): WorkspaceSchemaColumnDefinition => {
const columnName = computeColumnName(fieldMetadata.name);
const columnName = computeColumnName(flatFieldMetadata.name);
return {
name: columnName,
type: fieldMetadataTypeToColumnType(fieldMetadata.type),
type: fieldMetadataTypeToColumnType(flatFieldMetadata.type),
isNullable: true,
isArray: false,
isUnique: false,
default: null,
generatedType: 'STORED',
asExpression: getTsVectorColumnExpressionFromFields([]), // TODO: setup asExpression in flatFieldMetadata transpilation
asExpression: flatFieldMetadata.settings?.asExpression ?? undefined,
generatedType: flatFieldMetadata.settings?.generatedType ?? undefined,
};
};
const generateRelationColumnDefinition = (
fieldMetadata: FlatFieldMetadata<
flatFieldMetadata: FlatFieldMetadata<
FieldMetadataType.RELATION | FieldMetadataType.MORPH_RELATION
>,
): WorkspaceSchemaColumnDefinition | null => {
if (!fieldMetadata.settings || !fieldMetadata.settings.joinColumnName) {
if (
!flatFieldMetadata.settings ||
!flatFieldMetadata.settings.joinColumnName
) {
return null;
}
const joinColumnName = fieldMetadata.settings.joinColumnName;
const joinColumnName = flatFieldMetadata.settings.joinColumnName;
return {
name: joinColumnName,
@@ -127,78 +122,80 @@ const generateRelationColumnDefinition = (
};
const generateStandardColumnDefinition = (
fieldMetadata: FlatFieldMetadata,
flatFieldMetadata: FlatFieldMetadata,
tableName: string,
schemaName: string,
): WorkspaceSchemaColumnDefinition => {
const columnName = computeColumnName(fieldMetadata.name);
const columnName = computeColumnName(flatFieldMetadata.name);
const serializedDefaultValue = serializeDefaultValue(
fieldMetadata.defaultValue,
flatFieldMetadata.defaultValue,
);
const columnType = fieldMetadataTypeToColumnType(fieldMetadata.type);
const columnType = fieldMetadataTypeToColumnType(flatFieldMetadata.type);
return {
name: columnName,
type:
columnType === 'enum'
? computePostgresEnumName({ tableName, columnName })
? `"${schemaName}"."${computePostgresEnumName({ tableName, columnName })}"`
: columnType,
isNullable: fieldMetadata.isNullable ?? true,
isNullable: flatFieldMetadata.isNullable ?? true,
isArray:
fieldMetadata.type === FieldMetadataType.ARRAY ||
fieldMetadata.type === FieldMetadataType.MULTI_SELECT,
isUnique: fieldMetadata.isUnique ?? false,
flatFieldMetadata.type === FieldMetadataType.ARRAY ||
flatFieldMetadata.type === FieldMetadataType.MULTI_SELECT,
isUnique: flatFieldMetadata.isUnique ?? false,
default: serializedDefaultValue,
enumValues:
columnType === 'enum'
? fieldMetadata.options?.map((option) => option.value)
: undefined,
};
};
export const generateColumnDefinitions = ({
fieldMetadata,
flatFieldMetadata,
flatObjectMetadataWithoutFields,
}: {
fieldMetadata: FlatFieldMetadata;
flatFieldMetadata: FlatFieldMetadata;
flatObjectMetadataWithoutFields: FlatObjectMetadataWithoutFields;
}): WorkspaceSchemaColumnDefinition[] => {
const { tableName } = getWorkspaceSchemaContextForMigration({
const { tableName, schemaName } = getWorkspaceSchemaContextForMigration({
workspaceId: flatObjectMetadataWithoutFields.workspaceId,
flatObjectMetadataWithoutFields: flatObjectMetadataWithoutFields,
});
if (isCompositeFlatFieldMetadata(fieldMetadata)) {
const compositeType = getCompositeTypeOrThrow(fieldMetadata.type);
if (isCompositeFlatFieldMetadata(flatFieldMetadata)) {
const compositeType = getCompositeTypeOrThrow(flatFieldMetadata.type);
return compositeType.properties.map((property) =>
generateCompositeColumnDefinition({
compositeProperty: property,
parentFieldMetadata: fieldMetadata,
parentFieldMetadata: flatFieldMetadata,
flatObjectMetadataWithoutFields: flatObjectMetadataWithoutFields,
}),
);
}
if (
isFlatFieldMetadataEntityOfType(fieldMetadata, FieldMetadataType.TS_VECTOR)
isFlatFieldMetadataEntityOfType(
flatFieldMetadata,
FieldMetadataType.TS_VECTOR,
)
) {
return [generateTsVectorColumnDefinition(fieldMetadata)];
return [generateTsVectorColumnDefinition(flatFieldMetadata)];
}
if (
isFlatFieldMetadataEntityOfType(
fieldMetadata,
flatFieldMetadata,
FieldMetadataType.RELATION,
) ||
isFlatFieldMetadataEntityOfType(
fieldMetadata,
flatFieldMetadata,
FieldMetadataType.MORPH_RELATION,
)
) {
const relationColumn = generateRelationColumnDefinition(fieldMetadata);
const relationColumn = generateRelationColumnDefinition(flatFieldMetadata);
return relationColumn ? [relationColumn] : [];
}
return [generateStandardColumnDefinition(fieldMetadata, tableName)];
return [
generateStandardColumnDefinition(flatFieldMetadata, tableName, schemaName),
];
};
@@ -1,7 +1,4 @@
import {
type EnumFieldMetadataType,
type FieldMetadataType,
} from 'twenty-shared/types';
import { type FieldMetadataType } from 'twenty-shared/types';
import { assertUnreachable } from 'twenty-shared/utils';
import { type QueryRunner } from 'typeorm';
@@ -186,17 +183,17 @@ export const collectEnumOperationsForField = ({
export const collectEnumOperationsForObject = ({
tableName,
operation,
enumFlatFieldMetadatas,
flatFieldMetadatas,
options,
}: {
tableName: string;
operation: EnumOperation;
enumFlatFieldMetadatas: FlatFieldMetadata<EnumFieldMetadataType>[];
flatFieldMetadatas: FlatFieldMetadata[];
options?: { newTableName?: string; newFieldName?: string };
}): EnumOperationSpec[] => {
return enumFlatFieldMetadatas.flatMap((enumFlatFieldMetadata) =>
return flatFieldMetadatas.flatMap((flatFieldMetadata) =>
collectEnumOperationsForField({
flatFieldMetadata: enumFlatFieldMetadata,
flatFieldMetadata,
tableName,
operation,
options,
@@ -1,6 +1,7 @@
import { Injectable } from '@nestjs/common';
import { FieldMetadataType, type FromTo } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type QueryRunner } from 'typeorm';
import { type FieldMetadataDefaultValueForAnyType } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface';
@@ -69,7 +70,7 @@ export class WorkspaceSchemaFieldActionRunnerService
});
const columnDefinitions = generateColumnDefinitions({
fieldMetadata,
flatFieldMetadata: fieldMetadata,
flatObjectMetadataWithoutFields: flatObjectMetadata,
});
const columnNamesToDrop = columnDefinitions.map((def) => def.name);
@@ -126,7 +127,7 @@ export class WorkspaceSchemaFieldActionRunnerService
});
const columnDefinitions = generateColumnDefinitions({
fieldMetadata: flatFieldMetadata,
flatFieldMetadata: flatFieldMetadata,
flatObjectMetadataWithoutFields: flatObjectMetadata,
});
@@ -333,32 +334,28 @@ export class WorkspaceSchemaFieldActionRunnerService
) {
const fromOptionsById = new Map(
(update.from ?? [])
.filter(
(opt: FieldMetadataDefaultOption | FieldMetadataComplexOption) =>
Boolean(opt.id),
)
.map((opt: FieldMetadataDefaultOption | FieldMetadataComplexOption) => [
opt.id,
opt,
]),
.filter((opt) => isDefined(opt.id))
.map((opt) => [opt.id, opt]),
);
const toOptionsById = new Map(
(update.to ?? [])
.filter((opt) => isDefined(opt.id))
.map((opt) => [opt.id, opt]),
);
const valueMapping: Record<string, string> = {};
for (const toOption of flatFieldMetadata.options ?? []) {
if (!toOption.id) {
continue;
}
for (const toOption of toOptionsById.values()) {
const fromOption = fromOptionsById.get(toOption.id);
if (fromOption && fromOption.value !== toOption.value) {
if (fromOption) {
valueMapping[fromOption.value] = toOption.value;
}
}
const enumColumnDefinitions = generateColumnDefinitions({
fieldMetadata: flatFieldMetadata,
flatFieldMetadata: flatFieldMetadata,
flatObjectMetadataWithoutFields: flatObjectMetadata,
});
@@ -368,6 +365,7 @@ export class WorkspaceSchemaFieldActionRunnerService
schemaName,
tableName,
columnDefinition: enumColumnDefinition,
enumValues: update.to?.map((opt) => opt.value) ?? [],
oldToNewEnumOptionMap: valueMapping,
});
}
@@ -1,6 +1,7 @@
import { Injectable } from '@nestjs/common';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { isCompositeFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-composite-flat-field-metadata.util';
import { isEnumFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-enum-flat-field-metadata.util';
import { WorkspaceSchemaManagerService } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.service';
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
@@ -55,7 +56,7 @@ export class WorkspaceSchemaObjectActionRunnerService
.filter((field) => isEnumFlatFieldMetadata(field));
const enumOperations = collectEnumOperationsForObject({
enumFlatFieldMetadatas,
flatFieldMetadatas: enumFlatFieldMetadatas,
tableName,
operation: EnumOperation.DROP,
});
@@ -77,18 +78,21 @@ export class WorkspaceSchemaObjectActionRunnerService
const columnDefinitions = createFieldActions.flatMap((createFieldAction) =>
generateColumnDefinitions({
fieldMetadata: createFieldAction.flatFieldMetadata,
flatFieldMetadata: createFieldAction.flatFieldMetadata,
flatObjectMetadataWithoutFields: flatObjectMetadataWithoutFields,
}),
);
const enumFlatFieldMetadatas = createFieldActions
const enumOrCompositeFlatFieldMetadatas = createFieldActions
.map((createFieldAction) => createFieldAction.flatFieldMetadata)
.filter((field): field is FlatFieldMetadata => field != null)
.filter((field) => isEnumFlatFieldMetadata(field));
.filter(
(field) =>
isEnumFlatFieldMetadata(field) || isCompositeFlatFieldMetadata(field),
);
const enumOperations = collectEnumOperationsForObject({
enumFlatFieldMetadatas,
flatFieldMetadatas: enumOrCompositeFlatFieldMetadatas,
tableName,
operation: EnumOperation.CREATE,
});
@@ -143,14 +147,18 @@ export class WorkspaceSchemaObjectActionRunnerService
newTableName,
});
const enumFlatFieldMetadatas = Object.values(
const enumOrCompositeFlatFieldMetadatas = Object.values(
flatObjectMetadataWithFlatFieldMaps.fieldsById,
)
.filter((field): field is FlatFieldMetadata => field != null)
.filter((field) => isEnumFlatFieldMetadata(field));
.filter(
(field) =>
isEnumFlatFieldMetadata(field) ||
isCompositeFlatFieldMetadata(field),
);
const enumOperations = collectEnumOperationsForObject({
enumFlatFieldMetadatas,
flatFieldMetadatas: enumOrCompositeFlatFieldMetadatas,
tableName: currentTableName,
operation: EnumOperation.RENAME,
options: {