Remove sync-metadata and IS_WORKSPACE_CREATION_V2_ENABLED feature flag (#16997)

# Introduction
Followup of
https://github.com/twentyhq/twenty/pull/17001#pullrequestreview-3638508738
close https://github.com/twentyhq/core-team-issues/issues/1910

We've completely decom the `sync-metadata` in production. We're now then
removing its implementation in favor of the v2.

## TODO:
- [x] Remove sync-metadata implem and commands
- [x] Remove workspace decorators 
- [x] Type each deprecated field to deprecated on their workspaceEntity
- [x] Remove the `workspace-sync-metadata` folder entirely
- [x] remove workspace migration
- [x] workspace migration removal migration
- [x] remove the `v2` references from workspace manager file names
- [x] remove the `v2` references from workspace manager modules
- [ ] Double check impact on translation file path updates


## Note
- Removed the gate logic
- Remains some service v2 naming, serverless needs to be migrated on v2
fully
- Removed workspaceMigration service app health consumption, making it
always returning up ( no more down ) cc @FelixMalfait ( quite obsolete
health check now, will require complete refactor once we introduce inter
app dependency etc )
This commit is contained in:
Paul Rastoin
2026-01-08 16:45:12 +01:00
committed by GitHub
parent f1aee7fd18
commit 942d2fef83
629 changed files with 1985 additions and 21492 deletions
@@ -0,0 +1,112 @@
import { FieldMetadataType } from 'twenty-shared/types';
import {
type FieldTypeAndNameMetadata,
getTsVectorColumnExpressionFromFields,
} from 'src/engine/workspace-manager/utils/get-ts-vector-column-expression.util';
const nameTextField = { name: 'name', type: FieldMetadataType.TEXT };
const nameFullNameField = {
name: 'name',
type: FieldMetadataType.FULL_NAME,
};
const jobTitleTextField = { name: 'jobTitle', type: FieldMetadataType.TEXT };
const emailsEmailsField = { name: 'emails', type: FieldMetadataType.EMAILS };
const phonesPhonesField = { name: 'phones', type: FieldMetadataType.PHONES };
describe('getTsVectorColumnExpressionFromFields', () => {
it('should generate correct expression for simple text field', () => {
const fields = [nameTextField] as FieldTypeAndNameMetadata[];
const result = getTsVectorColumnExpressionFromFields(fields);
expect(result).toContain(
"to_tsvector('simple', COALESCE(public.unaccent_immutable(\"name\"), ''))",
);
});
it('should handle multiple fields', () => {
const fields = [
nameFullNameField,
jobTitleTextField,
emailsEmailsField,
] as FieldTypeAndNameMetadata[];
const result = getTsVectorColumnExpressionFromFields(fields);
expect(result).toContain(
'COALESCE(public.unaccent_immutable("nameFirstName"), \'\')',
);
expect(result).toContain(
'COALESCE(public.unaccent_immutable("nameLastName"), \'\')',
);
expect(result).toContain(
'COALESCE(public.unaccent_immutable("jobTitle"), \'\')',
);
expect(result).toContain(
'COALESCE(public.unaccent_immutable("emailsPrimaryEmail"), \'\')',
);
expect(result).toContain(
"COALESCE(public.unaccent_immutable(SPLIT_PART(\"emailsPrimaryEmail\", '@', 2)), '')",
);
});
it('should handle rich text fields', () => {
const fields = [
{ name: 'body', type: FieldMetadataType.RICH_TEXT },
] as FieldTypeAndNameMetadata[];
const result = getTsVectorColumnExpressionFromFields(fields);
expect(result).toBe(
"to_tsvector('simple', COALESCE(public.unaccent_immutable(\"body\"), ''))",
);
});
it('should handle rich text v2 fields', () => {
const fields = [
{ name: 'bodyV2', type: FieldMetadataType.RICH_TEXT_V2 },
] as FieldTypeAndNameMetadata[];
const result = getTsVectorColumnExpressionFromFields(fields);
expect(result).toBe(
"to_tsvector('simple', COALESCE(public.unaccent_immutable(\"bodyV2Markdown\"), ''))",
);
});
it('should handle phone fields without unaccenting', () => {
const fields = [phonesPhonesField] as FieldTypeAndNameMetadata[];
const result = getTsVectorColumnExpressionFromFields(fields);
expect(result).toContain('COALESCE("phonesPrimaryPhoneNumber", \'\')');
expect(result).toContain('COALESCE("phonesPrimaryPhoneCallingCode", \'\')');
expect(result).not.toContain('unaccent_immutable');
});
it('should generate international format expressions for phone fields', () => {
const fields = [phonesPhonesField] as FieldTypeAndNameMetadata[];
const result = getTsVectorColumnExpressionFromFields(fields);
expect(result).toContain(
'COALESCE("phonesPrimaryPhoneCallingCode" || "phonesPrimaryPhoneNumber", \'\')',
);
expect(result).toContain(
"COALESCE(REPLACE(\"phonesPrimaryPhoneCallingCode\", '+', '') || \"phonesPrimaryPhoneNumber\", '')",
);
});
it('should generate trunk prefix format expression for phone fields', () => {
const fields = [phonesPhonesField] as FieldTypeAndNameMetadata[];
const result = getTsVectorColumnExpressionFromFields(fields);
expect(result).toContain(
"COALESCE('0' || \"phonesPrimaryPhoneNumber\", '')",
);
});
it('should properly index phone subfields', () => {
const fields = [phonesPhonesField] as FieldTypeAndNameMetadata[];
const result = getTsVectorColumnExpressionFromFields(fields);
expect(result).toContain('phonesPrimaryPhoneNumber');
expect(result).toContain('phonesPrimaryPhoneCallingCode');
expect(result).not.toContain('phonesAdditionalPhones');
});
});
@@ -0,0 +1,13 @@
import { camelCase } from 'src/utils/camel-case';
const classSuffix = 'WorkspaceEntity';
export const convertClassNameToObjectMetadataName = (name: string): string => {
let objectName = camelCase(name);
if (objectName.endsWith(classSuffix)) {
objectName = objectName.slice(0, -classSuffix.length);
}
return objectName;
};
@@ -0,0 +1,32 @@
import { createHash } from 'crypto';
export function createDeterministicUuid(
uuidOrUuids: string[] | string,
): string {
const inputForHash = Array.isArray(uuidOrUuids)
? uuidOrUuids.join('-')
: uuidOrUuids;
const hash = createHash('sha256').update(inputForHash).digest('hex');
return `20202020-${hash.substring(0, 4)}-4${hash.substring(
4,
7,
)}-8${hash.substring(7, 10)}-${hash.substring(10, 22)}`;
}
type UuidPair = {
objectId: string;
standardId: string;
};
export const createRelationDeterministicUuid = (uuidPair: UuidPair): string => {
// Chaging the order in the array will result in different UUIDs
return createDeterministicUuid([uuidPair.objectId, uuidPair.standardId]);
};
export const createForeignKeyDeterministicUuid = (
uuidPair: UuidPair,
): string => {
// Chaging the order in the array will result in different UUIDs
return createDeterministicUuid([uuidPair.standardId, uuidPair.objectId]);
};
@@ -0,0 +1,106 @@
import {
FieldMetadataType,
compositeTypeDefinitions,
} from 'twenty-shared/types';
import {
computeColumnName,
computeCompositeColumnName,
} from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
import {
isSearchableFieldType,
type SearchableFieldType,
} from 'src/engine/workspace-manager/utils/is-searchable-field.util';
import { isSearchableSubfield } from 'src/engine/workspace-manager/utils/is-searchable-subfield.util';
export type FieldTypeAndNameMetadata = {
name: string;
type: SearchableFieldType;
};
export const getTsVectorColumnExpressionFromFields = (
fieldsUsedForSearch: FieldTypeAndNameMetadata[],
): string => {
const filteredFieldsUsedForSearch = fieldsUsedForSearch.filter((field) =>
isSearchableFieldType(field.type),
);
if (filteredFieldsUsedForSearch.length < 1) {
throw new Error('No searchable fields found');
}
const columnExpressions = fieldsUsedForSearch.flatMap(
getColumnExpressionsFromField,
);
const concatenatedExpression = columnExpressions.join(" || ' ' || ");
return `to_tsvector('simple', ${concatenatedExpression})`;
};
const getColumnExpressionsFromField = (
fieldMetadataTypeAndName: FieldTypeAndNameMetadata,
): string[] => {
if (isCompositeFieldMetadataType(fieldMetadataTypeAndName.type)) {
const compositeType = compositeTypeDefinitions.get(
fieldMetadataTypeAndName.type,
);
if (!compositeType) {
throw new Error(
`Composite type not found for field metadata type: ${fieldMetadataTypeAndName.type}`,
);
}
const baseExpressions = compositeType.properties
.filter((property) =>
isSearchableSubfield(compositeType.type, property.type, property.name),
)
.map((property) => {
const columnName = computeCompositeColumnName(
fieldMetadataTypeAndName,
property,
);
return getColumnExpression(columnName, fieldMetadataTypeAndName.type);
});
if (fieldMetadataTypeAndName.type === FieldMetadataType.PHONES) {
const phoneNumberColumn = `"${fieldMetadataTypeAndName.name}PrimaryPhoneNumber"`;
const callingCodeColumn = `"${fieldMetadataTypeAndName.name}PrimaryPhoneCallingCode"`;
const internationalFormats = [
`COALESCE(${callingCodeColumn} || ${phoneNumberColumn}, '')`,
`COALESCE(REPLACE(${callingCodeColumn}, '+', '') || ${phoneNumberColumn}, '')`,
`COALESCE('0' || ${phoneNumberColumn}, '')`,
];
return [...baseExpressions, ...internationalFormats];
}
return baseExpressions;
}
const columnName = computeColumnName(fieldMetadataTypeAndName.name);
return [getColumnExpression(columnName, fieldMetadataTypeAndName.type)];
};
const getColumnExpression = (
columnName: string,
fieldType: FieldMetadataType,
): string => {
const quotedColumnName = `"${columnName}"`;
switch (fieldType) {
case FieldMetadataType.EMAILS:
return `
COALESCE(public.unaccent_immutable(${quotedColumnName}), '') || ' ' ||
COALESCE(public.unaccent_immutable(SPLIT_PART(${quotedColumnName}, '@', 2)), '')`;
case FieldMetadataType.PHONES:
return `COALESCE(${quotedColumnName}, '')`;
default:
return `COALESCE(public.unaccent_immutable(${quotedColumnName}), '')`;
}
};
@@ -0,0 +1,19 @@
import { FieldMetadataType } from 'twenty-shared/types';
const SEARCHABLE_FIELD_TYPES = [
FieldMetadataType.TEXT,
FieldMetadataType.FULL_NAME,
FieldMetadataType.EMAILS,
FieldMetadataType.ADDRESS,
FieldMetadataType.LINKS,
FieldMetadataType.PHONES,
FieldMetadataType.RICH_TEXT,
FieldMetadataType.RICH_TEXT_V2,
] as const;
export type SearchableFieldType = (typeof SEARCHABLE_FIELD_TYPES)[number];
export const isSearchableFieldType = (
type: FieldMetadataType,
): type is SearchableFieldType => {
return SEARCHABLE_FIELD_TYPES.includes(type as SearchableFieldType);
};
@@ -0,0 +1,21 @@
import { FieldMetadataType } from 'twenty-shared/types';
export const isSearchableSubfield = (
compositeFieldMetadataType: FieldMetadataType,
subFieldMetadataType: FieldMetadataType,
subFieldName: string,
) => {
if (subFieldMetadataType !== FieldMetadataType.TEXT) {
return false;
}
switch (compositeFieldMetadataType) {
case FieldMetadataType.RICH_TEXT_V2:
return ['markdown'].includes(subFieldName);
case FieldMetadataType.PHONES:
return ['primaryPhoneNumber', 'primaryPhoneCallingCode'].includes(
subFieldName,
);
default:
return true;
}
};