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
@@ -48,6 +48,11 @@ export type FieldMetadataAddressSettings = {
subFields?: AllowedAddressSubField[];
};
export type FieldMetadataTsVectorSettings = {
asExpression?: string;
generatedType?: 'STORED' | 'VIRTUAL';
};
type FieldMetadataSettingsMapping = {
[FieldMetadataType.NUMBER]: FieldMetadataNumberSettings | null;
[FieldMetadataType.DATE]: FieldMetadataDateSettings | null;
@@ -56,6 +61,7 @@ type FieldMetadataSettingsMapping = {
[FieldMetadataType.RELATION]: FieldMetadataRelationSettings;
[FieldMetadataType.ADDRESS]: FieldMetadataAddressSettings | null;
[FieldMetadataType.MORPH_RELATION]: FieldMetadataRelationSettings | null; // TODO Should not be null
[FieldMetadataType.TS_VECTOR]: FieldMetadataTsVectorSettings | null;
};
export type AllFieldMetadataSettings =
@@ -125,6 +125,15 @@ export const fromCreateFieldInputToFlatFieldMetadatasToCreate = async ({
],
};
}
case FieldMetadataType.TS_VECTOR: {
return {
status: 'fail',
error: new FieldMetadataException(
'TS Vector is not supported for field creation',
FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
),
};
}
case FieldMetadataType.UUID:
case FieldMetadataType.TEXT:
case FieldMetadataType.PHONES:
@@ -143,8 +152,7 @@ export const fromCreateFieldInputToFlatFieldMetadatasToCreate = async ({
case FieldMetadataType.RICH_TEXT:
case FieldMetadataType.RICH_TEXT_V2:
case FieldMetadataType.ACTOR:
case FieldMetadataType.ARRAY:
case FieldMetadataType.TS_VECTOR: {
case FieldMetadataType.ARRAY: {
return {
status: 'success',
result: [
@@ -10,4 +10,5 @@ export const FLAT_OBJECT_METADATA_PROPERTIES_TO_COMPARE = [
'namePlural',
'nameSingular',
'standardOverrides', // Only if standard
'labelIdentifierFieldMetadataId',
] as const satisfies (keyof FlatObjectMetadata)[];
@@ -7,6 +7,7 @@ import {
BASE_OBJECT_STANDARD_FIELD_IDS,
CUSTOM_OBJECT_STANDARD_FIELD_IDS,
} from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
import { getTsVectorColumnExpressionFromFields } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util';
export const buildDefaultFieldsForCustomObject = (
workspaceId: string,
@@ -336,6 +337,39 @@ export const buildDefaultFlatFieldMetadataForCustomObject = ({
settings: null,
};
const searchVectorField: FlatFieldMetadata<FieldMetadataType.TS_VECTOR> = {
type: FieldMetadataType.TS_VECTOR,
id: v4(),
isLabelSyncedWithName: false,
isUnique: false,
objectMetadataId,
uniqueIdentifier: CUSTOM_OBJECT_STANDARD_FIELD_IDS.searchVector,
workspaceId,
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.searchVector,
name: 'searchVector',
label: 'Search vector',
icon: 'IconSearch',
description: 'Search vector',
isNullable: true,
isActive: true,
isCustom: false,
isSystem: true,
defaultValue: null,
createdAt,
updatedAt: createdAt,
flatRelationTargetFieldMetadata: null,
flatRelationTargetObjectMetadata: null,
options: null,
standardOverrides: null,
relationTargetFieldMetadataId: null,
relationTargetObjectMetadataId: null,
settings: {
asExpression: getTsVectorColumnExpressionFromFields([nameField]),
generatedType: 'STORED',
},
};
return {
idField,
nameField,
@@ -344,5 +378,6 @@ export const buildDefaultFlatFieldMetadataForCustomObject = ({
deletedAtField,
createdByField,
positionField,
searchVectorField,
} as const;
};
@@ -137,13 +137,15 @@ export class WorkspaceSchemaEnumManagerService {
schemaName,
tableName,
columnDefinition,
enumValues,
oldToNewEnumOptionMap,
}: {
queryRunner: QueryRunner;
schemaName: string;
tableName: string;
columnDefinition: WorkspaceSchemaColumnDefinition;
oldToNewEnumOptionMap?: Record<string, string>;
enumValues: string[];
oldToNewEnumOptionMap: Record<string, string>;
}): Promise<void> {
const isTransactionAlreadyActive = queryRunner.isTransactionActive;
@@ -151,10 +153,7 @@ export class WorkspaceSchemaEnumManagerService {
await queryRunner.startTransaction();
}
if (
!columnDefinition.enumValues ||
columnDefinition.enumValues.length === 0
) {
if (!enumValues || enumValues.length === 0) {
throw new WorkspaceSchemaManagerException(
`Cannot alter enum values for column ${columnDefinition.name} because it has no enum values`,
WorkspaceSchemaManagerExceptionCode.ENUM_OPERATION_FAILED,
@@ -182,7 +181,7 @@ export class WorkspaceSchemaEnumManagerService {
queryRunner,
schemaName,
enumName,
values: columnDefinition.enumValues,
values: enumValues,
});
const oldColumnName = `${columnName}_old`;
@@ -217,13 +216,13 @@ export class WorkspaceSchemaEnumManagerService {
});
}
await this.dropEnum({ queryRunner, schemaName, enumName: oldEnumName });
await this.dropColumn({
queryRunner,
schemaName,
tableName,
columnName: oldColumnName,
});
await this.dropEnum({ queryRunner, schemaName, enumName: oldEnumName });
if (!isTransactionAlreadyActive) {
await queryRunner.commitTransaction();
@@ -271,12 +270,14 @@ export class WorkspaceSchemaEnumManagerService {
columnDefinition: WorkspaceSchemaColumnDefinition;
enumTypeName: string;
}): Promise<void> {
const columnDef = buildSqlColumnDefinition({
...columnDefinition,
type: enumTypeName,
});
const safeSchemaName = removeSqlDDLInjection(schemaName);
const safeTableName = removeSqlDDLInjection(tableName);
const columnDef = buildSqlColumnDefinition({
...columnDefinition,
type: `"${safeSchemaName}"."${enumTypeName}"`,
});
const sql = `ALTER TABLE "${safeSchemaName}"."${safeTableName}" ADD COLUMN ${columnDef}`;
await queryRunner.query(sql);
@@ -322,22 +323,36 @@ export class WorkspaceSchemaEnumManagerService {
const safeOldColumnName = removeSqlDDLInjection(oldColumnName);
const safeNewColumnName = removeSqlDDLInjection(newColumnName);
const newEnumTypeName = computePostgresEnumName({
tableName,
columnName: newColumnName,
});
if (Object.keys(oldToNewEnumOptionMap).length === 0) {
return;
}
const caseStatements = Object.entries(oldToNewEnumOptionMap)
.map(
([oldEnumOption, newEnumOption]) =>
`WHEN '${removeSqlDDLInjection(oldEnumOption)}' THEN '${removeSqlDDLInjection(newEnumOption)}'`,
`WHEN '${removeSqlDDLInjection(oldEnumOption)}' THEN '${removeSqlDDLInjection(newEnumOption)}'::"${safeSchemaName}"."${newEnumTypeName}"`,
)
.join(' ');
const updateSql = `
UPDATE "${safeSchemaName}"."${safeTableName}"
SET "${safeNewColumnName}" =
CASE "${safeOldColumnName}"
${caseStatements}
ELSE "${safeOldColumnName}"
END
WHERE "${safeOldColumnName}" IS NOT NULL`;
const mappedValuesCondition = Object.keys(oldToNewEnumOptionMap)
.map((oldValue) => `'${removeSqlDDLInjection(oldValue)}'`)
.join(', ');
await queryRunner.query(updateSql);
// Update rows with mapped enum values
const updateMappedSql = `
UPDATE "${safeSchemaName}"."${safeTableName}"
SET "${safeNewColumnName}" =
CASE "${safeOldColumnName}"::text
${caseStatements}
END
WHERE "${safeOldColumnName}" IS NOT NULL
AND "${safeOldColumnName}"::text IN (${mappedValuesCondition})`;
await queryRunner.query(updateMappedSql);
}
}
@@ -8,5 +8,4 @@ export type WorkspaceSchemaColumnDefinition = {
isArray?: boolean;
asExpression?: string;
generatedType?: 'STORED' | 'VIRTUAL';
enumValues?: string[];
};
@@ -2,26 +2,15 @@ import { sanitizeDefaultValue } from 'src/engine/twenty-orm/workspace-schema-man
describe('sanitizeDefaultValue', () => {
describe('allowed functions', () => {
it('should allow gen_random_uuid() function', () => {
// Prepare
const input = 'gen_random_uuid()';
// Act
const result = sanitizeDefaultValue(input);
// Assert
expect(result).toBe('gen_random_uuid()');
});
it('should allow uuid_generate_v4() function', () => {
// Prepare
const input = 'uuid_generate_v4()';
const input = 'public.uuid_generate_v4()';
// Act
const result = sanitizeDefaultValue(input);
// Assert
expect(result).toBe('uuid_generate_v4()');
expect(result).toBe('public.uuid_generate_v4()');
});
it('should allow now() function', () => {
@@ -35,71 +24,10 @@ describe('sanitizeDefaultValue', () => {
expect(result).toBe('now()');
});
it('should allow current_timestamp function', () => {
// Prepare
const input = 'current_timestamp';
// Act
const result = sanitizeDefaultValue(input);
// Assert
expect(result).toBe('current_timestamp');
});
it('should allow current_date function', () => {
// Prepare
const input = 'current_date';
// Act
const result = sanitizeDefaultValue(input);
// Assert
expect(result).toBe('current_date');
});
it('should allow current_time function', () => {
// Prepare
const input = 'current_time';
// Act
const result = sanitizeDefaultValue(input);
// Assert
expect(result).toBe('current_time');
});
it('should allow localtime function', () => {
// Prepare
const input = 'localtime';
// Act
const result = sanitizeDefaultValue(input);
// Assert
expect(result).toBe('localtime');
});
it('should allow localtimestamp function', () => {
// Prepare
const input = 'localtimestamp';
// Act
const result = sanitizeDefaultValue(input);
// Assert
expect(result).toBe('localtimestamp');
});
it('should be case insensitive for allowed functions', () => {
// Act & Assert
expect(sanitizeDefaultValue('GEN_RANDOM_UUID()')).toBe(
'GEN_RANDOM_UUID()',
);
expect(sanitizeDefaultValue('NOW()')).toBe('NOW()');
expect(sanitizeDefaultValue('CURRENT_TIMESTAMP')).toBe(
'CURRENT_TIMESTAMP',
);
expect(sanitizeDefaultValue('Current_Date')).toBe('Current_Date');
});
});
@@ -115,7 +43,7 @@ describe('sanitizeDefaultValue', () => {
expect(result).not.toContain('DROP TABLE');
expect(result).not.toContain(';');
expect(result).not.toContain('--');
expect(result).toBe('DROPTABLEusers');
expect(result).toBe("'DROPTABLEusers'");
});
it('should sanitize quotes in string values', () => {
@@ -127,7 +55,7 @@ describe('sanitizeDefaultValue', () => {
// Assert
expect(result).not.toContain('"');
expect(result).toBe('testvalue');
expect(result).toBe("'testvalue'");
});
it('should sanitize parentheses in non-function values', () => {
@@ -140,7 +68,7 @@ describe('sanitizeDefaultValue', () => {
// Assert
expect(result).not.toContain('(');
expect(result).not.toContain(')');
expect(result).toBe('testvalue');
expect(result).toBe("'testvalue'");
});
it('should sanitize backslashes', () => {
@@ -152,7 +80,7 @@ describe('sanitizeDefaultValue', () => {
// Assert
expect(result).not.toContain('\\');
expect(result).toBe('testvalue');
expect(result).toBe("'testvalue'");
});
it('should sanitize SQL comment patterns', () => {
@@ -165,7 +93,7 @@ describe('sanitizeDefaultValue', () => {
// Assert
expect(result).not.toContain('/*');
expect(result).not.toContain('*/');
expect(result).toBe('valuecommenttest');
expect(result).toBe("'valuecommenttest'");
});
it('should remove non-alphanumeric characters but preserve SQL keywords in alphanumeric form', () => {
@@ -176,7 +104,7 @@ describe('sanitizeDefaultValue', () => {
const result = sanitizeDefaultValue(inputWithKeywords);
// Assert
expect(result).toBe('SELECTFROMusers');
expect(result).toBe("'SELECTFROMusers'");
expect(result).not.toContain('*');
expect(result).not.toContain(' ');
});
@@ -191,24 +119,24 @@ describe('sanitizeDefaultValue', () => {
const result = sanitizeDefaultValue(input);
// Assert
expect(result).toBe('simple_value');
expect(result).toBe("'simple_value'");
});
it('should preserve numeric string values', () => {
it('should preserve numeric values', () => {
// Prepare
const input = '12345';
const input = 12345;
// Act
const result = sanitizeDefaultValue(input);
// Assert
expect(result).toBe('12345');
expect(result).toBe(12345);
});
it('should preserve boolean string values', () => {
it('should preserve boolean values', () => {
// Act & Assert
expect(sanitizeDefaultValue('true')).toBe('true');
expect(sanitizeDefaultValue('false')).toBe('false');
expect(sanitizeDefaultValue(true)).toBe(true);
expect(sanitizeDefaultValue(false)).toBe(false);
});
it('should handle empty string', () => {
@@ -219,7 +147,7 @@ describe('sanitizeDefaultValue', () => {
const result = sanitizeDefaultValue(input);
// Assert
expect(result).toBe('');
expect(result).toBe("''");
});
it('should remove whitespace but preserve alphanumeric and underscores', () => {
@@ -230,7 +158,7 @@ describe('sanitizeDefaultValue', () => {
const result = sanitizeDefaultValue(input);
// Assert
expect(result).toBe('test');
expect(result).toBe("'test'");
});
it('should preserve alphanumeric values with underscores', () => {
@@ -241,7 +169,7 @@ describe('sanitizeDefaultValue', () => {
const result = sanitizeDefaultValue(input);
// Assert
expect(result).toBe('test_value_123');
expect(result).toBe("'test_value_123'");
});
});
@@ -249,14 +177,14 @@ describe('sanitizeDefaultValue', () => {
it('should distinguish between allowed functions and similar strings', () => {
// Act & Assert
expect(sanitizeDefaultValue('now()')).toBe('now()');
expect(sanitizeDefaultValue('now_test')).toBe('now_test');
expect(sanitizeDefaultValue('not_now()')).toBe('not_now');
expect(sanitizeDefaultValue('now_test')).toBe("'now_test'");
expect(sanitizeDefaultValue('not_now()')).toBe("'not_now'");
});
it('should handle functions with different casing but sanitize non-functions normally', () => {
// Act & Assert
expect(sanitizeDefaultValue('NOW()')).toBe('NOW()');
expect(sanitizeDefaultValue('now_function')).toBe('now_function');
expect(sanitizeDefaultValue('now_function')).toBe("'now_function'");
});
it('should handle complex mixed input', () => {
@@ -267,7 +195,7 @@ describe('sanitizeDefaultValue', () => {
const result = sanitizeDefaultValue(complexInput);
// Assert
expect(result).toBe('testvalueDROPTABLEuserscommentnow');
expect(result).toBe("'testvalueDROPTABLEuserscommentnow'");
expect(result).not.toContain(';');
expect(result).not.toContain('"');
expect(result).not.toContain('/*');
@@ -277,18 +205,16 @@ describe('sanitizeDefaultValue', () => {
});
describe('edge cases', () => {
it('should handle null-like strings', () => {
it('should handle null', () => {
// Act & Assert
expect(sanitizeDefaultValue('null')).toBe('null');
expect(sanitizeDefaultValue('NULL')).toBe('NULL');
expect(sanitizeDefaultValue('undefined')).toBe('undefined');
expect(sanitizeDefaultValue(null)).toBe('NULL');
});
it('should handle strings that start with allowed function names', () => {
// Act & Assert
expect(sanitizeDefaultValue('now_extended')).toBe('now_extended');
expect(sanitizeDefaultValue('now_extended')).toBe("'now_extended'");
expect(sanitizeDefaultValue('gen_random_uuid_custom')).toBe(
'gen_random_uuid_custom',
"'gen_random_uuid_custom'",
);
});
@@ -300,7 +226,7 @@ describe('sanitizeDefaultValue', () => {
const result = sanitizeDefaultValue(specialChars);
// Assert
expect(result).toBe('');
expect(result).toBe("''");
});
it('should handle very long strings', () => {
@@ -311,7 +237,7 @@ describe('sanitizeDefaultValue', () => {
const result = sanitizeDefaultValue(longString);
// Assert
expect(result).toBe('a'.repeat(1000) + 'DROPTABLEusers');
expect(result).toBe(`'${'a'.repeat(1000)}DROPTABLEusers'`);
expect(result).not.toContain(';');
expect(result).not.toContain(' ');
});
@@ -1,3 +1,5 @@
import { isDefined } from 'twenty-shared/utils';
import { type WorkspaceSchemaColumnDefinition } from 'src/engine/twenty-orm/workspace-schema-manager/types/workspace-schema-column-definition.type';
import { sanitizeDefaultValue } from 'src/engine/twenty-orm/workspace-schema-manager/utils/sanitize-default-value.util';
import { removeSqlDDLInjection } from 'src/engine/workspace-manager/workspace-migration-runner/utils/remove-sql-injection.util';
@@ -8,33 +10,35 @@ export const buildSqlColumnDefinition = (
const safeName = removeSqlDDLInjection(column.name);
const parts = [`"${safeName}"`];
if (column.asExpression) {
parts.push(`AS (${column.asExpression})`); // TODO: to sanitize
parts.push(column.isArray ? `${column.type}[]` : column.type);
if (column.asExpression && column.type === 'tsvector') {
parts.push(`GENERATED ALWAYS AS (${column.asExpression})`); // TODO: to sanitize
if (column.generatedType) {
parts.push(column.generatedType);
}
} else {
const safeType = removeSqlDDLInjection(column.type);
}
parts.push(column.isArray ? `${safeType}[]` : safeType);
if (column.isPrimary) {
parts.push('PRIMARY KEY');
}
if (column.isPrimary) {
parts.push('PRIMARY KEY');
}
if (column.isNullable === false) {
parts.push('NOT NULL');
}
if (column.isNullable === false) {
parts.push('NOT NULL');
}
if (column.isUnique) {
parts.push('UNIQUE');
}
if (column.isUnique) {
parts.push('UNIQUE');
}
if (
isDefined(column.default) &&
column.default !== '' &&
column.type !== 'tsvector'
) {
const safeDefault = sanitizeDefaultValue(column.default);
if (column.default !== undefined) {
const safeDefault = sanitizeDefaultValue(column.default);
parts.push(`DEFAULT ${safeDefault}`);
}
parts.push(`DEFAULT ${safeDefault}`);
}
return parts.join(' ');
@@ -7,23 +7,14 @@ export const sanitizeDefaultValue = (
return 'NULL';
}
const allowedFunctions = [
'gen_random_uuid()',
'uuid_generate_v4()',
'now()',
'current_timestamp',
'current_date',
'current_time',
'localtime',
'localtimestamp',
];
const allowedFunctions = ['public.uuid_generate_v4()', 'now()'];
if (typeof defaultValue === 'string') {
if (allowedFunctions.includes(defaultValue.toLowerCase())) {
return defaultValue;
}
return removeSqlDDLInjection(defaultValue);
return `'${removeSqlDDLInjection(defaultValue)}'`;
}
return defaultValue;
@@ -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: {