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:
+112
@@ -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');
|
||||
});
|
||||
});
|
||||
+13
@@ -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;
|
||||
};
|
||||
+32
@@ -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]);
|
||||
};
|
||||
+106
@@ -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);
|
||||
};
|
||||
+21
@@ -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;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user