Workspace migration v2 testing (#13136)

# Introduction
Introduced `EachTesting` pattern for the builder unit tests.
As always any suggestions are more than welcomed !


Still need to:
- [x] implem basic tests for field
- [x] create `get-flat-index-field-metadata.mock.ts`
- [x] Implement basic tests for index and index-fields
- [ ] Implem standard edges cases tests TDD style

## Misc
- was https://github.com/twentyhq/twenty/pull/13132 closed due to mess
to rebase on main
This commit is contained in:
Paul Rastoin
2025-07-15 16:08:50 +02:00
committed by GitHub
parent 2c7a459634
commit c5a74b8e92
25 changed files with 3013 additions and 1575 deletions

View File

@ -0,0 +1,54 @@
import { faker } from '@faker-js/faker';
import { FieldMetadataType } from 'twenty-shared/types';
import { FlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration-v2/types/flat-field-metadata';
type FlatFieldMetadataOverrides<
T extends FieldMetadataType = FieldMetadataType,
> = Required<
Pick<FlatFieldMetadata<T>, 'uniqueIdentifier' | 'objectMetadataId'>
> &
Partial<FlatFieldMetadata<T>>;
export const getFlatFieldMetadataMock = <
T extends FieldMetadataType = FieldMetadataType,
>(
overrides: FlatFieldMetadataOverrides<T>,
): FlatFieldMetadata<T> => {
const createdAt = faker.date.anytime();
return {
createdAt,
description: 'default flat field metadata description',
icon: 'icon',
id: faker.string.uuid(),
isActive: true,
isCustom: true,
name: 'flatFieldMetadataName',
label: 'flat field metadata label',
isNullable: true,
isUnique: false,
relationTargetFieldMetadataId: undefined,
relationTargetObjectMetadataId: undefined,
type: FieldMetadataType.TEXT as T,
isLabelSyncedWithName: false,
isSystem: false,
standardId: undefined,
standardOverrides: undefined,
updatedAt: createdAt,
workspaceId: faker.string.uuid(),
...overrides,
};
};
export const getStandardFlatFieldMetadataMock = (
overrides: Omit<FlatFieldMetadataOverrides, 'isCustom' | 'isSystem'>,
) => {
return getFlatFieldMetadataMock({
standardId: faker.string.uuid(),
standardOverrides: {},
isCustom: false,
isSystem: true,
...overrides,
});
};

View File

@ -0,0 +1,24 @@
import { faker } from '@faker-js/faker';
import { FlatIndexFieldMetadata } from 'src/engine/workspace-manager/workspace-migration-v2/types/flat-index-field-metadata';
type FlatIndexFieldMetadataOverrides = Required<
Pick<
FlatIndexFieldMetadata,
'fieldMetadataId' | 'indexMetadataId' | 'uniqueIdentifier'
>
> &
Partial<FlatIndexFieldMetadata>;
export const getFlatIndexFieldMetadataMock = (
overrides: FlatIndexFieldMetadataOverrides,
): FlatIndexFieldMetadata => {
const createdAt = faker.date.anytime();
return {
createdAt,
id: faker.string.uuid(),
order: faker.number.int(),
updatedAt: createdAt,
...overrides,
};
};

View File

@ -0,0 +1,37 @@
import { faker } from '@faker-js/faker';
import { IndexType } from 'src/engine/metadata-modules/index-metadata/types/indexType.types';
import { FlatIndexMetadata } from 'src/engine/workspace-manager/workspace-migration-v2/types/flat-index-metadata';
type FlatIndexMetadataOverrides = Required<
Pick<FlatIndexMetadata, 'uniqueIdentifier' | 'objectMetadataId'>
> &
Partial<FlatIndexMetadata>;
export const getFlatIndexMetadataMock = (
overrides: FlatIndexMetadataOverrides,
): FlatIndexMetadata => {
const createdAt = faker.date.anytime();
return {
flatIndexFieldMetadatas: [],
createdAt,
id: faker.string.uuid(),
indexType: IndexType.BTREE,
indexWhereClause: undefined,
isCustom: false,
isUnique: false,
name: 'defaultFlatIndexMetadataName',
updatedAt: createdAt,
workspaceId: faker.string.uuid(),
...overrides,
};
};
export const getStandardFlatIndexMetadataMock = (
overrides: Omit<FlatIndexMetadataOverrides, 'isCustom'>,
) => {
return getFlatIndexMetadataMock({
isCustom: false,
...overrides,
});
};

View File

@ -0,0 +1,56 @@
import { faker } from '@faker-js/faker';
import { FlatObjectMetadata } from 'src/engine/workspace-manager/workspace-migration-v2/types/flat-object-metadata';
type FlatObjectMetadataOverrides = Required<
Pick<FlatObjectMetadata, 'uniqueIdentifier'>
> &
Partial<FlatObjectMetadata>;
export const getFlatObjectMetadataMock = (
overrides: FlatObjectMetadataOverrides,
): FlatObjectMetadata => {
const createdAt = faker.date.anytime();
return {
flatFieldMetadatas: [],
flatIndexMetadatas: [],
createdAt,
dataSourceId: faker.string.uuid(),
description: 'default flat object metadata description',
duplicateCriteria: [],
icon: 'icon',
id: faker.string.uuid(),
imageIdentifierFieldMetadataId: faker.string.uuid(),
isActive: true,
isAuditLogged: true,
isCustom: true,
isLabelSyncedWithName: false,
isRemote: false,
isSearchable: true,
isSystem: false,
labelIdentifierFieldMetadataId: faker.string.uuid(),
labelPlural: 'default flat object metadata label plural',
labelSingular: 'default flat object metadata label singular',
namePlural: 'defaultflatObjectMetadataNamePlural',
nameSingular: 'defaultflatObjectMetadataNameSingular',
shortcut: 'shortcut',
standardId: undefined,
standardOverrides: undefined,
targetTableName: '',
updatedAt: createdAt,
workspaceId: faker.string.uuid(),
...overrides,
};
};
export const getStandardFlatObjectMetadataMock = (
overrides: Omit<FlatObjectMetadataOverrides, 'isCustom' | 'isSystem'>,
) => {
return getFlatObjectMetadataMock({
standardId: faker.string.uuid(),
standardOverrides: {},
isCustom: false,
isSystem: true,
...overrides,
});
};

View File

@ -0,0 +1,12 @@
import { Relation } from 'typeorm';
export type ExtractRecordTypeOrmRelationProperties<T, TRelationTargets> =
NonNullable<
{
[P in keyof T]: T[P] extends Relation<
TRelationTargets | TRelationTargets[]
>
? P
: never;
}[keyof T]
>;

View File

@ -1,7 +1,18 @@
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { FieldMetadataType } from 'twenty-shared/types';
export type FlatFieldMetadata = Partial<
Omit<FieldMetadataEntity, 'object' | 'indexFieldMetadatas'>
> & {
uniqueIdentifier: string;
};
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ExtractRecordTypeOrmRelationProperties } from 'src/engine/workspace-manager/workspace-migration-v2/types/extract-record-typeorm-relation-properties.type';
import { MetadataEntitiesRelationTarget } from 'src/engine/workspace-manager/workspace-migration-v2/types/metadata-entities-relation-targets.type';
type FieldMetadataEntityRelationProperties =
ExtractRecordTypeOrmRelationProperties<
FieldMetadataEntity,
MetadataEntitiesRelationTarget
>;
export type FlatFieldMetadata<T extends FieldMetadataType = FieldMetadataType> =
Partial<
Omit<FieldMetadataEntity<T>, FieldMetadataEntityRelationProperties>
> & {
uniqueIdentifier: string;
};

View File

@ -1,15 +1,15 @@
import { IndexFieldMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-field-metadata.entity';
import { ExtractRecordTypeOrmRelationProperties } from 'src/engine/workspace-manager/workspace-migration-v2/types/extract-record-typeorm-relation-properties.type';
import { MetadataEntitiesRelationTarget } from 'src/engine/workspace-manager/workspace-migration-v2/types/metadata-entities-relation-targets.type';
const indexFieldMetadataEntityRelationProperties = [
'indexMetadata',
'fieldMetadata',
] as const satisfies (keyof IndexFieldMetadataEntity)[];
type IndexFieldMetadataRelationProperties =
(typeof indexFieldMetadataEntityRelationProperties)[number];
type IndexFieldMetadataEntityRelationProperties =
ExtractRecordTypeOrmRelationProperties<
IndexFieldMetadataEntity,
MetadataEntitiesRelationTarget
>;
export type FlatIndexFieldMetadata = Partial<
Omit<IndexFieldMetadataEntity, IndexFieldMetadataRelationProperties>
Omit<IndexFieldMetadataEntity, IndexFieldMetadataEntityRelationProperties>
> & {
uniqueIdentifier: string;
};

View File

@ -1,8 +1,15 @@
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
import { ExtractRecordTypeOrmRelationProperties } from 'src/engine/workspace-manager/workspace-migration-v2/types/extract-record-typeorm-relation-properties.type';
import { FlatIndexFieldMetadata } from 'src/engine/workspace-manager/workspace-migration-v2/types/flat-index-field-metadata';
import { MetadataEntitiesRelationTarget } from 'src/engine/workspace-manager/workspace-migration-v2/types/metadata-entities-relation-targets.type';
type IndexMetadataRelationProperties = ExtractRecordTypeOrmRelationProperties<
IndexMetadataEntity,
MetadataEntitiesRelationTarget
>;
export type FlatIndexMetadata = Partial<
Omit<IndexMetadataEntity, 'indexFieldMetadatas' | 'objectMetadata'> // Might have an issue as ObjectMetadataId != uniqueIdentifier
Omit<IndexMetadataEntity, IndexMetadataRelationProperties>
> & {
flatIndexFieldMetadatas: FlatIndexFieldMetadata[];
uniqueIdentifier: string;

View File

@ -1,9 +1,16 @@
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { ExtractRecordTypeOrmRelationProperties } from 'src/engine/workspace-manager/workspace-migration-v2/types/extract-record-typeorm-relation-properties.type';
import { FlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration-v2/types/flat-field-metadata';
import { FlatIndexMetadata } from 'src/engine/workspace-manager/workspace-migration-v2/types/flat-index-metadata';
import { MetadataEntitiesRelationTarget } from 'src/engine/workspace-manager/workspace-migration-v2/types/metadata-entities-relation-targets.type';
type ObjectMetadataRelationProperties = ExtractRecordTypeOrmRelationProperties<
ObjectMetadataEntity,
MetadataEntitiesRelationTarget
>;
export type FlatObjectMetadata = Partial<
Omit<ObjectMetadataEntity, 'fields' | 'indexMetadatas'>
Omit<ObjectMetadataEntity, ObjectMetadataRelationProperties>
> & {
uniqueIdentifier: string;
flatIndexMetadatas: FlatIndexMetadata[];

View File

@ -0,0 +1,10 @@
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { IndexFieldMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-field-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { FieldPermissionEntity } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.entity';
export type MetadataEntitiesRelationTarget =
| ObjectMetadataEntity
| FieldMetadataEntity
| IndexFieldMetadataEntity
| FieldPermissionEntity;

View File

@ -2,6 +2,7 @@ import diff from 'microdiff';
import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util';
import { FlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration-v2/types/flat-field-metadata';
import { FromTo } from 'src/engine/workspace-manager/workspace-migration-v2/types/from-to.type';
import { UpdateFieldAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-field-action-v2';
@ -18,6 +19,11 @@ const flatFieldMetadataPropertiesToCompare = [
'name',
'options',
'standardOverrides',
'settings',
// To reactivate once we authorize relation edition, see https://github.com/twentyhq/twenty/commit/39f6f3c4bb101272a9014e142a842d0801a3c33b
// 'relationTargetFieldMetadataId',
// 'relationTargetObjectMetadataId',
///
] as const satisfies (keyof FlatFieldMetadata)[];
export type FlatFieldMetadataPropertiesToCompare =
@ -26,6 +32,7 @@ export type FlatFieldMetadataPropertiesToCompare =
const fieldMetadataPropertiesToStringify = [
'defaultValue',
'standardOverrides',
'settings',
] as const satisfies FlatFieldMetadataPropertiesToCompare[];
const shouldNotOverrideDefaultValue = (type: FieldMetadataType) => {
@ -65,6 +72,15 @@ export const compareTwoFlatFieldMetadata = ({
return true;
}
// Remove below assertion when we authorize relation edition, see https://github.com/twentyhq/twenty/commit/39f6f3c4bb101272a9014e142a842d0801a3c33b
if (
isDefined(fieldMetadata.type) &&
isRelationFieldMetadataType(fieldMetadata.type) &&
!['label', 'description', 'isActive'].includes(property)
) {
return true;
}
return false;
},
propertiesToStringify: fieldMetadataPropertiesToStringify,

View File

@ -0,0 +1,284 @@
import { faker } from '@faker-js/faker';
import { FieldMetadataType } from 'twenty-shared/types';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { getFlatFieldMetadataMock } from 'src/engine/workspace-manager/workspace-migration-v2/__tests__/get-flat-field-metadata.mock';
import { getFlatObjectMetadataMock } from 'src/engine/workspace-manager/workspace-migration-v2/__tests__/get-flat-object-metadata.mock';
import { WorkspaceMigrationBuilderTestCase } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/__tests__/types/workspace-migration-builder-test-case.type';
const basicObjectMetadataId = faker.string.uuid();
const basicFlatFieldMetadatas = Array.from({ length: 5 }, (_value, index) =>
getFlatFieldMetadataMock({
objectMetadataId: basicObjectMetadataId,
uniqueIdentifier: `field_${index}`,
}),
);
// TODO prastoin test defaultValue and settings updates
// TODO prastoin test standard abstraction in TDD style
const relationTestCases: WorkspaceMigrationBuilderTestCase[] = [
{
title: 'It should build an create_field action for a RELATION field',
context: {
input: () => {
const objectMetadataId = faker.string.uuid();
const createdFlatRelationFieldMetadata = getFlatFieldMetadataMock({
uniqueIdentifier: 'field-metadata-unique-identifier-1',
objectMetadataId,
type: FieldMetadataType.RELATION,
relationTargetFieldMetadataId: faker.string.uuid(),
relationTargetObjectMetadataId: faker.string.uuid(),
});
const flatObjectMetadata = getFlatObjectMetadataMock({
uniqueIdentifier: 'object-metadata-unique-identifier-1',
isLabelSyncedWithName: true,
flatFieldMetadatas: [],
});
return {
from: [flatObjectMetadata],
to: [
{
...flatObjectMetadata,
flatFieldMetadatas: [createdFlatRelationFieldMetadata],
},
],
};
},
expectedActionsTypeCounter: {
createField: 1,
},
},
},
{
title: 'It should build an update_field action for a RELATION field',
context: {
input: () => {
const objectMetadataId = faker.string.uuid();
const updatedFieldMetadata = getFlatFieldMetadataMock({
uniqueIdentifier: 'field-metadata-unique-identifier-1',
objectMetadataId,
type: FieldMetadataType.RELATION,
relationTargetFieldMetadataId: faker.string.uuid(),
relationTargetObjectMetadataId: faker.string.uuid(),
});
const flatObjectMetadata = getFlatObjectMetadataMock({
uniqueIdentifier: 'object-metadata-unique-identifier-1',
isLabelSyncedWithName: true,
flatFieldMetadatas: [
...basicFlatFieldMetadatas,
updatedFieldMetadata,
],
});
return {
from: [flatObjectMetadata],
to: [
{
...flatObjectMetadata,
flatFieldMetadatas: [
...basicFlatFieldMetadatas,
{
...updatedFieldMetadata,
isActive: false,
description: 'new description',
label: 'new label',
},
],
},
],
};
},
expectedActionsTypeCounter: {
updateField: 1,
},
},
},
{
title:
'It should NOT build an update_field action for a field RELATION uncovered fields mutation',
context: {
input: () => {
const objectMetadataId = faker.string.uuid();
const updatedFieldMetadata =
getFlatFieldMetadataMock<FieldMetadataType.RELATION>({
uniqueIdentifier: 'field-metadata-unique-identifier-1',
objectMetadataId,
type: FieldMetadataType.RELATION,
settings: {
relationType: RelationType.MANY_TO_ONE,
isForeignKey: true,
joinColumnName: 'column-name',
onDelete: undefined,
},
relationTargetFieldMetadataId: faker.string.uuid(),
relationTargetObjectMetadataId: faker.string.uuid(),
});
const flatObjectMetadata = getFlatObjectMetadataMock({
uniqueIdentifier: 'object-metadata-unique-identifier-1',
isLabelSyncedWithName: true,
flatFieldMetadatas: [updatedFieldMetadata],
});
return {
from: [flatObjectMetadata],
to: [
{
...flatObjectMetadata,
flatFieldMetadatas: [
{
...updatedFieldMetadata,
settings: {
relationType: RelationType.ONE_TO_MANY,
isForeignKey: false,
joinColumnName: 'new-column-name',
onDelete: undefined,
},
relationTargetFieldMetadataId: faker.string.uuid(),
relationTargetObjectMetadataId: faker.string.uuid(),
},
],
},
],
};
},
},
},
];
const basicCrudTestCases: WorkspaceMigrationBuilderTestCase[] = [
{
title: 'It should build an create_field action',
context: {
input: () => {
const objectMetadataId = faker.string.uuid();
const flatFieldMetadata = getFlatFieldMetadataMock({
uniqueIdentifier: 'field-metadata-unique-identifier-1',
objectMetadataId,
});
const flatObjectMetadata = getFlatObjectMetadataMock({
uniqueIdentifier: 'object-metadata-unique-identifier-1',
isLabelSyncedWithName: true,
flatFieldMetadatas: [],
});
return {
from: [flatObjectMetadata],
to: [
{
...flatObjectMetadata,
flatFieldMetadatas: [flatFieldMetadata],
},
],
};
},
expectedActionsTypeCounter: {
createField: 1,
},
},
},
{
title: 'It should build an update_field action',
context: {
input: () => {
const objectMetadataId = faker.string.uuid();
const flatFieldMetadata = getFlatFieldMetadataMock({
uniqueIdentifier: 'field-metadata-unique-identifier-1',
objectMetadataId,
});
const flatObjectMetadata = getFlatObjectMetadataMock({
uniqueIdentifier: 'object-metadata-unique-identifier-1',
flatFieldMetadatas: [...basicFlatFieldMetadatas, flatFieldMetadata],
});
return {
from: [flatObjectMetadata],
to: [
{
...flatObjectMetadata,
flatFieldMetadatas: [
...basicFlatFieldMetadatas,
{
...flatFieldMetadata,
description: 'new description',
name: 'new name',
isActive: false,
icon: 'new icon',
},
],
},
],
};
},
expectedActionsTypeCounter: {
updateField: 1,
},
},
},
{
title: 'It should build a delete_field action',
context: {
input: () => {
const objectMetadataId = faker.string.uuid();
const flatFieldMetadata = getFlatFieldMetadataMock({
uniqueIdentifier: 'field-metadata-unique-identifier-1',
objectMetadataId,
});
const flatObjectMetadata = getFlatObjectMetadataMock({
uniqueIdentifier: 'object-metadata-unique-identifier-1',
flatFieldMetadatas: [...basicFlatFieldMetadatas, flatFieldMetadata],
});
return {
from: [flatObjectMetadata],
to: [
{
...flatObjectMetadata,
flatFieldMetadatas: basicFlatFieldMetadatas,
},
],
};
},
expectedActionsTypeCounter: {
deleteField: 1,
},
},
},
];
export const WORKSPACE_MIGRATION_FIELD_BUILDER_TEST_CASES: WorkspaceMigrationBuilderTestCase[] =
[
...relationTestCases,
...basicCrudTestCases,
{
title:
'It should not infer any actions as from and to fields are identical',
context: {
input: () => {
const objectMetadataId = faker.string.uuid();
const flatFieldMetadata = getFlatFieldMetadataMock({
uniqueIdentifier: 'field-metadata-unique-identifier-1',
objectMetadataId,
});
const from = [
getFlatObjectMetadataMock({
uniqueIdentifier: 'object-metadata-unique-identifier-1',
flatFieldMetadatas: [flatFieldMetadata],
}),
];
return {
from,
to: from,
};
},
},
},
];

View File

@ -0,0 +1,130 @@
import { faker } from '@faker-js/faker';
import { getFlatIndexMetadataMock } from 'src/engine/workspace-manager/workspace-migration-v2/__tests__/get-flat-index-metadata.mock';
import { getFlatObjectMetadataMock } from 'src/engine/workspace-manager/workspace-migration-v2/__tests__/get-flat-object-metadata.mock';
import { WorkspaceMigrationBuilderTestCase } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/__tests__/types/workspace-migration-builder-test-case.type';
// Should test more things such as flatFieldIndex diffing
const objectMetadataId = faker.string.uuid();
export const WORKSPACE_MIGRATION_INDEX_BUILDER_TEST_CASES: WorkspaceMigrationBuilderTestCase[] =
[
{
title: 'It should build an create_index action',
context: {
input: () => {
const flatIndexMetadata = getFlatIndexMetadataMock({
uniqueIdentifier: 'field-metadata-unique-identifier-1',
objectMetadataId,
});
const flatObjectMetadata = getFlatObjectMetadataMock({
uniqueIdentifier: 'object-metadata-unique-identifier-1',
isLabelSyncedWithName: true,
flatIndexMetadatas: [],
});
return {
from: [flatObjectMetadata],
to: [
{
...flatObjectMetadata,
flatIndexMetadatas: [flatIndexMetadata],
},
],
};
},
expectedActionsTypeCounter: {
createIndex: 1,
},
},
},
{
title:
'It should build an delete_index and a create_index action ( the way we handle update )',
context: {
input: () => {
const flatIndexMetadata = getFlatIndexMetadataMock({
uniqueIdentifier: 'field-metadata-unique-identifier-1',
objectMetadataId,
});
const flatObjectMetadata = getFlatObjectMetadataMock({
uniqueIdentifier: 'object-metadata-unique-identifier-1',
isLabelSyncedWithName: true,
flatIndexMetadatas: [flatIndexMetadata],
});
return {
from: [flatObjectMetadata],
to: [
{
...flatObjectMetadata,
flatIndexMetadatas: [
{
...flatIndexMetadata,
name: 'new index name',
isUnique: false,
indexWhereClause: 'new index where clause',
},
],
},
],
};
},
expectedActionsTypeCounter: {
createIndex: 1,
deleteIndex: 1,
},
},
},
{
title: 'It should build a delete_index action',
context: {
input: () => {
const flatIndexMetadata = getFlatIndexMetadataMock({
uniqueIdentifier: 'field-metadata-unique-identifier-1',
objectMetadataId,
});
const flatObjectMetadata = getFlatObjectMetadataMock({
uniqueIdentifier: 'object-metadata-unique-identifier-1',
isLabelSyncedWithName: true,
flatIndexMetadatas: [flatIndexMetadata],
});
return {
from: [flatObjectMetadata],
to: [
{
...flatObjectMetadata,
flatIndexMetadatas: [],
},
],
};
},
expectedActionsTypeCounter: {
deleteIndex: 1,
},
},
},
{
title:
'It should not infer any actions as from and to indexes are identical',
context: {
input: () => {
const flatIndexMetadata = getFlatIndexMetadataMock({
uniqueIdentifier: 'field-metadata-unique-identifier-1',
objectMetadataId,
});
const flatObjectMetadata = getFlatObjectMetadataMock({
uniqueIdentifier: 'object-metadata-unique-identifier-1',
isLabelSyncedWithName: true,
flatIndexMetadatas: [flatIndexMetadata],
});
return {
from: [flatObjectMetadata],
to: [flatObjectMetadata],
};
},
},
},
];

View File

@ -0,0 +1,130 @@
import { faker } from '@faker-js/faker';
import { getFlatFieldMetadataMock } from 'src/engine/workspace-manager/workspace-migration-v2/__tests__/get-flat-field-metadata.mock';
import { getFlatObjectMetadataMock } from 'src/engine/workspace-manager/workspace-migration-v2/__tests__/get-flat-object-metadata.mock';
import { WorkspaceMigrationBuilderTestCase } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/__tests__/types/workspace-migration-builder-test-case.type';
export const WORKSPACE_MIGRATION_OBJECT_BUILDER_TEST_CASES: WorkspaceMigrationBuilderTestCase[] =
[
{
title:
'It should build an update_object action with all object updated fields',
context: {
input: () => {
const flatObjectMetadata = getFlatObjectMetadataMock({
uniqueIdentifier: 'pomme',
nameSingular: 'toto',
namePlural: 'totos',
isLabelSyncedWithName: true,
});
return {
from: [flatObjectMetadata],
to: [
{
...flatObjectMetadata,
nameSingular: 'prastouin',
namePlural: 'prastoins',
isLabelSyncedWithName: false,
},
],
};
},
expectedActionsTypeCounter: {
updateObject: 1,
},
},
},
{
title: 'It should build a create_object action',
context: {
input: () => {
const flatObjectMetadata = getFlatObjectMetadataMock({
uniqueIdentifier: 'pomme',
nameSingular: 'toto',
namePlural: 'totos',
isLabelSyncedWithName: true,
});
return {
from: [],
to: [flatObjectMetadata],
};
},
expectedActionsTypeCounter: {
createObject: 1,
},
},
},
{
title:
'It should build a create_object and create_field actions for each of this fieldMetadata',
context: {
input: () => {
const objectMetadataId = faker.string.uuid();
const flatFieldMetadatas = Array.from(
{ length: 5 },
(_value, index) =>
getFlatFieldMetadataMock({
objectMetadataId,
uniqueIdentifier: `field_${index}`,
}),
);
const flatObjectMetadata = getFlatObjectMetadataMock({
uniqueIdentifier: 'pomme',
nameSingular: 'toto',
namePlural: 'totos',
isLabelSyncedWithName: true,
id: objectMetadataId,
flatFieldMetadatas,
});
return {
from: [],
to: [flatObjectMetadata],
};
},
expectedActionsTypeCounter: {
createObject: 1,
createField: 5,
},
},
},
{
title: 'It should build a delete_object action',
context: {
input: () => {
const flatObjectMetadata = getFlatObjectMetadataMock({
uniqueIdentifier: 'pomme',
nameSingular: 'toto',
namePlural: 'totos',
isLabelSyncedWithName: true,
});
return {
from: [flatObjectMetadata],
to: [],
};
},
expectedActionsTypeCounter: {
deleteObject: 1,
},
},
},
{
title: 'It should not infer any actions as from and to are identical',
context: {
input: () => {
const from = [
getFlatObjectMetadataMock({ uniqueIdentifier: 'pomme' }),
];
return {
from,
to: from,
};
},
},
},
];

View File

@ -0,0 +1,25 @@
import { EachTestingContext } from 'twenty-shared/testing';
import { WorkspaceMigrationActionTypeV2 } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-action-common-v2';
import { WorkspaceMigrationBuilderV2Service } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/workspace-migration-builder-v2.service';
type WorkspaceBuilderArgs = Parameters<
typeof WorkspaceMigrationBuilderV2Service.prototype.build
>[0];
type ConvertActionTypeToCamelCase<T extends string> =
T extends `${infer Before}_${infer After}`
? `${Before}${Capitalize<After>}`
: T;
export type CamelCasedWorkspaceMigrationActionsType =
ConvertActionTypeToCamelCase<WorkspaceMigrationActionTypeV2>;
export type ExpectedActionCounters = Partial<
Record<CamelCasedWorkspaceMigrationActionsType, number>
>;
export type WorkspaceMigrationBuilderTestCase = EachTestingContext<{
input: WorkspaceBuilderArgs | (() => WorkspaceBuilderArgs);
expectedActionsTypeCounter?: ExpectedActionCounters;
}>;

View File

@ -0,0 +1,102 @@
import { extractRecordIdsAndDatesAsExpectAny } from 'test/utils/extract-record-ids-and-dates-as-expect-any';
import { eachTestingContextFilter } from 'twenty-shared/testing';
import { capitalize } from 'twenty-shared/utils';
import { WORKSPACE_MIGRATION_FIELD_BUILDER_TEST_CASES } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/__tests__/common/workspace-migration-builder-field-test-case';
import { WORKSPACE_MIGRATION_INDEX_BUILDER_TEST_CASES } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/__tests__/common/workspace-migration-builder-index-test-case';
import { WORKSPACE_MIGRATION_OBJECT_BUILDER_TEST_CASES } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/__tests__/common/workspace-migration-builder-object-test-case';
import {
CamelCasedWorkspaceMigrationActionsType,
ExpectedActionCounters,
WorkspaceMigrationBuilderTestCase,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/__tests__/types/workspace-migration-builder-test-case.type';
import { WorkspaceMigrationV2 } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-v2';
import { WorkspaceMigrationBuilderV2Service } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/workspace-migration-builder-v2.service';
const allWorkspaceBuilderTestCases: {
label: string;
testCases: WorkspaceMigrationBuilderTestCase[];
}[] = [
{
label: 'object',
testCases: WORKSPACE_MIGRATION_OBJECT_BUILDER_TEST_CASES,
},
{
label: 'field',
testCases: WORKSPACE_MIGRATION_FIELD_BUILDER_TEST_CASES,
},
{
label: 'index',
testCases: WORKSPACE_MIGRATION_INDEX_BUILDER_TEST_CASES,
},
];
const expectedActionsTypeCounterChecker = ({
expectedActionsTypeCounter,
workspaceMigration,
}: {
workspaceMigration: WorkspaceMigrationV2;
expectedActionsTypeCounter?: ExpectedActionCounters;
}) => {
const initialAcc: ExpectedActionCounters = {
createField: 0,
createIndex: 0,
createObject: 0,
deleteField: 0,
deleteIndex: 0,
deleteObject: 0,
updateField: 0,
updateObject: 0,
};
const actualActionsTypeCounter = workspaceMigration.actions.reduce(
(acc, action) => {
const { type } = action;
const [operation, target] = type.split('_');
const formattedActionKey =
`${operation}${capitalize(target)}` as CamelCasedWorkspaceMigrationActionsType;
return {
...acc,
[formattedActionKey]: (acc[formattedActionKey] ?? 0) + 1,
};
},
initialAcc,
);
expect(actualActionsTypeCounter).toEqual({
...initialAcc,
...expectedActionsTypeCounter,
});
};
describe.each(allWorkspaceBuilderTestCases)(
'Workspace migration builder $label actions test suite',
({ testCases }) => {
let service: WorkspaceMigrationBuilderV2Service;
beforeEach(() => {
service = new WorkspaceMigrationBuilderV2Service();
});
it.each(eachTestingContextFilter(testCases))(
'$title',
({ context: { input, expectedActionsTypeCounter } }) => {
const { from, to } = typeof input === 'function' ? input() : input;
const workspaceMigration = service.build({
from,
to,
});
expectedActionsTypeCounterChecker({
expectedActionsTypeCounter,
workspaceMigration,
});
const { actions } = workspaceMigration;
expect(actions).toMatchSnapshot(
actions.map(extractRecordIdsAndDatesAsExpectAny),
);
},
);
},
);

View File

@ -1,605 +0,0 @@
import { FieldMetadataType } from 'twenty-shared/types';
import { FlatObjectMetadata } from 'src/engine/workspace-manager/workspace-migration-v2/types/flat-object-metadata';
import { WorkspaceMigrationBuilderV2Service } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/workspace-migration-builder-v2.service';
describe('WorkspaceMigrationBuilderV2Service', () => {
let service: WorkspaceMigrationBuilderV2Service;
const baseObject: FlatObjectMetadata = {
uniqueIdentifier: '20202020-e89b-12d3-a456-426614175000',
nameSingular: 'Contact',
namePlural: 'Contacts',
labelSingular: 'Contact',
flatIndexMetadatas: [],
labelPlural: 'Contacts',
description: 'A contact',
flatFieldMetadatas: [
{
uniqueIdentifier: '20202020-e89b-12d3-a456-426614174000',
name: 'firstName',
label: 'First Name',
type: FieldMetadataType.FULL_NAME,
defaultValue: '',
description: '',
},
],
};
beforeEach(() => {
service = new WorkspaceMigrationBuilderV2Service();
});
it('should return a migration when nameSingular changes', () => {
const from: FlatObjectMetadata = baseObject;
const to: FlatObjectMetadata = {
...from,
nameSingular: 'Person',
};
const result = service.build({ from: [from], to: [to] });
expect(result).toMatchInlineSnapshot(`
{
"actions": [
{
"flatObjectMetadata": {
"description": "A contact",
"flatFieldMetadatas": [
{
"defaultValue": "",
"description": "",
"label": "First Name",
"name": "firstName",
"type": "FULL_NAME",
"uniqueIdentifier": "20202020-e89b-12d3-a456-426614174000",
},
],
"flatIndexMetadatas": [],
"labelPlural": "Contacts",
"labelSingular": "Contact",
"namePlural": "Contacts",
"nameSingular": "Person",
"uniqueIdentifier": "20202020-e89b-12d3-a456-426614175000",
},
"type": "update_object",
"updates": [
{
"from": "Contact",
"property": "nameSingular",
"to": "Person",
},
],
},
],
}
`);
});
it('should return a migration when creating a new object', () => {
const newObject: FlatObjectMetadata = {
uniqueIdentifier: '20202020-e89b-12d3-a456-426614175001',
nameSingular: 'Company',
namePlural: 'Companies',
flatIndexMetadatas: [],
labelSingular: 'Company',
labelPlural: 'Companies',
description: 'A company',
flatFieldMetadatas: [
{
uniqueIdentifier: '20202020-e89b-12d3-a456-426614174001',
name: 'name',
label: 'Name',
type: FieldMetadataType.ADDRESS,
defaultValue: '',
description: '',
},
],
};
const result = service.build({ from: [], to: [newObject] });
expect(result).toMatchInlineSnapshot(`
{
"actions": [
{
"flatObjectMetadata": {
"description": "A company",
"flatFieldMetadatas": [
{
"defaultValue": "",
"description": "",
"label": "Name",
"name": "name",
"type": "ADDRESS",
"uniqueIdentifier": "20202020-e89b-12d3-a456-426614174001",
},
],
"flatIndexMetadatas": [],
"labelPlural": "Companies",
"labelSingular": "Company",
"namePlural": "Companies",
"nameSingular": "Company",
"uniqueIdentifier": "20202020-e89b-12d3-a456-426614175001",
},
"type": "create_object",
},
{
"flatFieldMetadata": {
"defaultValue": "",
"description": "",
"label": "Name",
"name": "name",
"type": "ADDRESS",
"uniqueIdentifier": "20202020-e89b-12d3-a456-426614174001",
},
"flatObjectMetadata": {
"description": "A company",
"flatFieldMetadatas": [
{
"defaultValue": "",
"description": "",
"label": "Name",
"name": "name",
"type": "ADDRESS",
"uniqueIdentifier": "20202020-e89b-12d3-a456-426614174001",
},
],
"flatIndexMetadatas": [],
"labelPlural": "Companies",
"labelSingular": "Company",
"namePlural": "Companies",
"nameSingular": "Company",
"uniqueIdentifier": "20202020-e89b-12d3-a456-426614175001",
},
"type": "create_field",
},
],
}
`);
});
it('should return a migration when deleting an object', () => {
const result = service.build({ from: [baseObject], to: [] });
expect(result).toMatchInlineSnapshot(`
{
"actions": [
{
"flatObjectMetadata": {
"description": "A contact",
"flatFieldMetadatas": [
{
"defaultValue": "",
"description": "",
"label": "First Name",
"name": "firstName",
"type": "FULL_NAME",
"uniqueIdentifier": "20202020-e89b-12d3-a456-426614174000",
},
],
"flatIndexMetadatas": [],
"labelPlural": "Contacts",
"labelSingular": "Contact",
"namePlural": "Contacts",
"nameSingular": "Contact",
"uniqueIdentifier": "20202020-e89b-12d3-a456-426614175000",
},
"type": "delete_object",
},
],
}
`);
});
it('should handle multiple operations in a single migration', () => {
const objectToUpdate: FlatObjectMetadata = {
...baseObject,
nameSingular: 'Person',
flatFieldMetadatas: [
...baseObject.flatFieldMetadatas,
{
defaultValue: '',
label: 'New field',
type: FieldMetadataType.NUMBER,
name: 'newField',
uniqueIdentifier: '20202020-3ad3-4fec-9c46-8dc9158980e3',
description: 'new field description',
},
],
};
const objectToDelete = {
...baseObject,
uniqueIdentifier: '20202020-59ef-4a14-a509-0a02acb248d5',
};
const objectToCreate: FlatObjectMetadata = {
uniqueIdentifier: '20202020-1218-4fc0-b32d-fc4f005c4bab',
nameSingular: 'Company',
namePlural: 'Companies',
flatIndexMetadatas: [],
labelSingular: 'Company',
labelPlural: 'Companies',
description: 'A company',
flatFieldMetadatas: [
{
uniqueIdentifier: '20202020-1016-4f09-bad6-e75681f385f4',
name: 'name',
label: 'Name',
type: FieldMetadataType.ADDRESS,
defaultValue: '',
description: '',
},
],
};
const result = service.build({
from: [baseObject, objectToDelete],
to: [objectToUpdate, objectToCreate],
});
expect(result).toMatchInlineSnapshot(`
{
"actions": [
{
"flatObjectMetadata": {
"description": "A company",
"flatFieldMetadatas": [
{
"defaultValue": "",
"description": "",
"label": "Name",
"name": "name",
"type": "ADDRESS",
"uniqueIdentifier": "20202020-1016-4f09-bad6-e75681f385f4",
},
],
"flatIndexMetadatas": [],
"labelPlural": "Companies",
"labelSingular": "Company",
"namePlural": "Companies",
"nameSingular": "Company",
"uniqueIdentifier": "20202020-1218-4fc0-b32d-fc4f005c4bab",
},
"type": "create_object",
},
{
"flatObjectMetadata": {
"description": "A contact",
"flatFieldMetadatas": [
{
"defaultValue": "",
"description": "",
"label": "First Name",
"name": "firstName",
"type": "FULL_NAME",
"uniqueIdentifier": "20202020-e89b-12d3-a456-426614174000",
},
],
"flatIndexMetadatas": [],
"labelPlural": "Contacts",
"labelSingular": "Contact",
"namePlural": "Contacts",
"nameSingular": "Contact",
"uniqueIdentifier": "20202020-59ef-4a14-a509-0a02acb248d5",
},
"type": "delete_object",
},
{
"flatObjectMetadata": {
"description": "A contact",
"flatFieldMetadatas": [
{
"defaultValue": "",
"description": "",
"label": "First Name",
"name": "firstName",
"type": "FULL_NAME",
"uniqueIdentifier": "20202020-e89b-12d3-a456-426614174000",
},
{
"defaultValue": "",
"description": "new field description",
"label": "New field",
"name": "newField",
"type": "NUMBER",
"uniqueIdentifier": "20202020-3ad3-4fec-9c46-8dc9158980e3",
},
],
"flatIndexMetadatas": [],
"labelPlural": "Contacts",
"labelSingular": "Contact",
"namePlural": "Contacts",
"nameSingular": "Person",
"uniqueIdentifier": "20202020-e89b-12d3-a456-426614175000",
},
"type": "update_object",
"updates": [
{
"from": "Contact",
"property": "nameSingular",
"to": "Person",
},
],
},
{
"flatFieldMetadata": {
"defaultValue": "",
"description": "",
"label": "Name",
"name": "name",
"type": "ADDRESS",
"uniqueIdentifier": "20202020-1016-4f09-bad6-e75681f385f4",
},
"flatObjectMetadata": {
"description": "A company",
"flatFieldMetadatas": [
{
"defaultValue": "",
"description": "",
"label": "Name",
"name": "name",
"type": "ADDRESS",
"uniqueIdentifier": "20202020-1016-4f09-bad6-e75681f385f4",
},
],
"flatIndexMetadatas": [],
"labelPlural": "Companies",
"labelSingular": "Company",
"namePlural": "Companies",
"nameSingular": "Company",
"uniqueIdentifier": "20202020-1218-4fc0-b32d-fc4f005c4bab",
},
"type": "create_field",
},
{
"flatFieldMetadata": {
"defaultValue": "",
"description": "new field description",
"label": "New field",
"name": "newField",
"type": "NUMBER",
"uniqueIdentifier": "20202020-3ad3-4fec-9c46-8dc9158980e3",
},
"flatObjectMetadata": {
"description": "A contact",
"flatFieldMetadatas": [
{
"defaultValue": "",
"description": "",
"label": "First Name",
"name": "firstName",
"type": "FULL_NAME",
"uniqueIdentifier": "20202020-e89b-12d3-a456-426614174000",
},
{
"defaultValue": "",
"description": "new field description",
"label": "New field",
"name": "newField",
"type": "NUMBER",
"uniqueIdentifier": "20202020-3ad3-4fec-9c46-8dc9158980e3",
},
],
"flatIndexMetadatas": [],
"labelPlural": "Contacts",
"labelSingular": "Contact",
"namePlural": "Contacts",
"nameSingular": "Person",
"uniqueIdentifier": "20202020-e89b-12d3-a456-426614175000",
},
"type": "create_field",
},
],
}
`);
});
it('should treat objects with the same name but different IDs as distinct', () => {
const objectA: FlatObjectMetadata = {
uniqueIdentifier: 'id-1',
flatIndexMetadatas: [],
nameSingular: 'Duplicate',
namePlural: 'Duplicates',
labelSingular: 'Duplicate',
labelPlural: 'Duplicates',
description: 'First object',
flatFieldMetadatas: [
{
uniqueIdentifier: 'field-1',
name: 'fieldA',
label: 'Field A',
type: FieldMetadataType.FULL_NAME,
defaultValue: '',
description: '',
},
],
};
const objectB: FlatObjectMetadata = {
uniqueIdentifier: 'id-2',
nameSingular: 'Duplicate',
namePlural: 'Duplicates',
labelSingular: 'Duplicate',
labelPlural: 'Duplicates',
flatIndexMetadatas: [],
description: 'Second object',
flatFieldMetadatas: [
{
uniqueIdentifier: 'field-2',
name: 'fieldB',
label: 'Field B',
type: FieldMetadataType.ADDRESS,
defaultValue: '',
description: '',
},
],
};
const result = service.build({ from: [], to: [objectA, objectB] });
expect(result.actions).toMatchInlineSnapshot(`
[
{
"flatObjectMetadata": {
"description": "First object",
"flatFieldMetadatas": [
{
"defaultValue": "",
"description": "",
"label": "Field A",
"name": "fieldA",
"type": "FULL_NAME",
"uniqueIdentifier": "field-1",
},
],
"flatIndexMetadatas": [],
"labelPlural": "Duplicates",
"labelSingular": "Duplicate",
"namePlural": "Duplicates",
"nameSingular": "Duplicate",
"uniqueIdentifier": "id-1",
},
"type": "create_object",
},
{
"flatObjectMetadata": {
"description": "Second object",
"flatFieldMetadatas": [
{
"defaultValue": "",
"description": "",
"label": "Field B",
"name": "fieldB",
"type": "ADDRESS",
"uniqueIdentifier": "field-2",
},
],
"flatIndexMetadatas": [],
"labelPlural": "Duplicates",
"labelSingular": "Duplicate",
"namePlural": "Duplicates",
"nameSingular": "Duplicate",
"uniqueIdentifier": "id-2",
},
"type": "create_object",
},
{
"flatFieldMetadata": {
"defaultValue": "",
"description": "",
"label": "Field A",
"name": "fieldA",
"type": "FULL_NAME",
"uniqueIdentifier": "field-1",
},
"flatObjectMetadata": {
"description": "First object",
"flatFieldMetadatas": [
{
"defaultValue": "",
"description": "",
"label": "Field A",
"name": "fieldA",
"type": "FULL_NAME",
"uniqueIdentifier": "field-1",
},
],
"flatIndexMetadatas": [],
"labelPlural": "Duplicates",
"labelSingular": "Duplicate",
"namePlural": "Duplicates",
"nameSingular": "Duplicate",
"uniqueIdentifier": "id-1",
},
"type": "create_field",
},
{
"flatFieldMetadata": {
"defaultValue": "",
"description": "",
"label": "Field B",
"name": "fieldB",
"type": "ADDRESS",
"uniqueIdentifier": "field-2",
},
"flatObjectMetadata": {
"description": "Second object",
"flatFieldMetadatas": [
{
"defaultValue": "",
"description": "",
"label": "Field B",
"name": "fieldB",
"type": "ADDRESS",
"uniqueIdentifier": "field-2",
},
],
"flatIndexMetadatas": [],
"labelPlural": "Duplicates",
"labelSingular": "Duplicate",
"namePlural": "Duplicates",
"nameSingular": "Duplicate",
"uniqueIdentifier": "id-2",
},
"type": "create_field",
},
]
`);
const deleteResult = service.build({ from: [objectA, objectB], to: [] });
expect(deleteResult.actions).toMatchInlineSnapshot(`
[
{
"flatObjectMetadata": {
"description": "First object",
"flatFieldMetadatas": [
{
"defaultValue": "",
"description": "",
"label": "Field A",
"name": "fieldA",
"type": "FULL_NAME",
"uniqueIdentifier": "field-1",
},
],
"flatIndexMetadatas": [],
"labelPlural": "Duplicates",
"labelSingular": "Duplicate",
"namePlural": "Duplicates",
"nameSingular": "Duplicate",
"uniqueIdentifier": "id-1",
},
"type": "delete_object",
},
{
"flatObjectMetadata": {
"description": "Second object",
"flatFieldMetadatas": [
{
"defaultValue": "",
"description": "",
"label": "Field B",
"name": "fieldB",
"type": "ADDRESS",
"uniqueIdentifier": "field-2",
},
],
"flatIndexMetadatas": [],
"labelPlural": "Duplicates",
"labelSingular": "Duplicate",
"namePlural": "Duplicates",
"nameSingular": "Duplicate",
"uniqueIdentifier": "id-2",
},
"type": "delete_object",
},
]
`);
});
it('should emit no actions when from and to are deeply equal', () => {
const obj: FlatObjectMetadata = { ...baseObject };
const result = service.build({ from: [obj], to: [obj] });
expect(result.actions).toEqual([]);
});
});

View File

@ -1,522 +0,0 @@
import { FieldMetadataType } from 'twenty-shared/types';
import { IndexType } from 'src/engine/metadata-modules/index-metadata/types/indexType.types';
import { FlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration-v2/types/flat-field-metadata';
import { FlatIndexMetadata } from 'src/engine/workspace-manager/workspace-migration-v2/types/flat-index-metadata';
import { FlatObjectMetadata } from 'src/engine/workspace-manager/workspace-migration-v2/types/flat-object-metadata';
import { WorkspaceMigrationBuilderV2Service } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/workspace-migration-builder-v2.service';
describe('Workspace migration builder indexes tests suite', () => {
let service: WorkspaceMigrationBuilderV2Service;
beforeEach(() => {
service = new WorkspaceMigrationBuilderV2Service();
});
const createMockObject = (
identifier: string,
fields: Partial<FlatFieldMetadata>[] = [],
indexes: FlatIndexMetadata[] = [],
): FlatObjectMetadata => ({
uniqueIdentifier: identifier,
flatIndexMetadatas: indexes,
flatFieldMetadatas: fields.map((field) => ({
type: FieldMetadataType.TEXT,
name: 'defaultName',
label: 'Default Label',
isCustom: true,
isActive: true,
isNullable: true,
uniqueIdentifier: 'default-id',
...field,
})),
});
const createMockIndex = (
name: string,
fields: string[],
isUnique = false,
): FlatIndexMetadata => ({
name,
isUnique,
indexType: IndexType.BTREE,
indexWhereClause: null,
uniqueIdentifier: name,
flatIndexFieldMetadatas: fields.map((field, index) => ({
uniqueIdentifier: `${name}-field-${index}`,
order: index,
})),
});
describe('buildWorkspaceMigrationV2IndexActions', () => {
it('should create index actions for created indexes', () => {
const fromObjects: FlatObjectMetadata[] = [];
const toObjects: FlatObjectMetadata[] = [
createMockObject(
'company',
[
{
type: FieldMetadataType.TEXT,
name: 'name',
label: 'Name',
uniqueIdentifier: 'name',
},
],
[createMockIndex('idx_company_name', ['name'], true)],
),
];
const result = service.build({ from: fromObjects, to: toObjects });
expect(result).toMatchInlineSnapshot(`
{
"actions": [
{
"flatObjectMetadata": {
"flatFieldMetadatas": [
{
"isActive": true,
"isCustom": true,
"isNullable": true,
"label": "Name",
"name": "name",
"type": "TEXT",
"uniqueIdentifier": "name",
},
],
"flatIndexMetadatas": [
{
"flatIndexFieldMetadatas": [
{
"order": 0,
"uniqueIdentifier": "idx_company_name-field-0",
},
],
"indexType": "BTREE",
"indexWhereClause": null,
"isUnique": true,
"name": "idx_company_name",
"uniqueIdentifier": "idx_company_name",
},
],
"uniqueIdentifier": "company",
},
"type": "create_object",
},
{
"flatFieldMetadata": {
"isActive": true,
"isCustom": true,
"isNullable": true,
"label": "Name",
"name": "name",
"type": "TEXT",
"uniqueIdentifier": "name",
},
"flatObjectMetadata": {
"flatFieldMetadatas": [
{
"isActive": true,
"isCustom": true,
"isNullable": true,
"label": "Name",
"name": "name",
"type": "TEXT",
"uniqueIdentifier": "name",
},
],
"flatIndexMetadatas": [
{
"flatIndexFieldMetadatas": [
{
"order": 0,
"uniqueIdentifier": "idx_company_name-field-0",
},
],
"indexType": "BTREE",
"indexWhereClause": null,
"isUnique": true,
"name": "idx_company_name",
"uniqueIdentifier": "idx_company_name",
},
],
"uniqueIdentifier": "company",
},
"type": "create_field",
},
{
"flatIndexMetadata": {
"flatIndexFieldMetadatas": [
{
"order": 0,
"uniqueIdentifier": "idx_company_name-field-0",
},
],
"indexType": "BTREE",
"indexWhereClause": null,
"isUnique": true,
"name": "idx_company_name",
"uniqueIdentifier": "idx_company_name",
},
"type": "create_index",
},
],
}
`);
});
it('should create delete actions for deleted indexes', () => {
const fromObjects: FlatObjectMetadata[] = [
createMockObject(
'company',
[
{
type: FieldMetadataType.TEXT,
name: 'name',
label: 'Name',
uniqueIdentifier: 'name',
},
],
[createMockIndex('idx_company_name', ['name'], true)],
),
];
const toObjects: FlatObjectMetadata[] = [
createMockObject('company', [
{
type: FieldMetadataType.TEXT,
name: 'name',
label: 'Name',
uniqueIdentifier: 'name',
},
]),
];
const result = service.build({ from: fromObjects, to: toObjects });
expect(result).toMatchInlineSnapshot(`
{
"actions": [
{
"flatIndexMetadata": {
"flatIndexFieldMetadatas": [
{
"order": 0,
"uniqueIdentifier": "idx_company_name-field-0",
},
],
"indexType": "BTREE",
"indexWhereClause": null,
"isUnique": true,
"name": "idx_company_name",
"uniqueIdentifier": "idx_company_name",
},
"type": "delete_index",
},
],
}
`);
});
it('should handle multiple index changes across different objects', () => {
const fromObjects: FlatObjectMetadata[] = [
createMockObject(
'company',
[
{
type: FieldMetadataType.TEXT,
name: 'name',
label: 'Name',
uniqueIdentifier: 'name',
},
],
[createMockIndex('idx_company_name_old', ['name'], true)],
),
];
const toObjects: FlatObjectMetadata[] = [
createMockObject(
'company',
[
{
type: FieldMetadataType.TEXT,
name: 'name',
label: 'Name',
uniqueIdentifier: 'name',
},
],
[createMockIndex('idx_company_name_new', ['name'], true)],
),
createMockObject(
'person',
[
{
type: FieldMetadataType.TEXT,
name: 'email',
label: 'Email',
uniqueIdentifier: 'email',
},
],
[createMockIndex('idx_person_email', ['email'], true)],
),
];
const result = service.build({ from: fromObjects, to: toObjects });
expect(result).toMatchInlineSnapshot(`
{
"actions": [
{
"flatObjectMetadata": {
"flatFieldMetadatas": [
{
"isActive": true,
"isCustom": true,
"isNullable": true,
"label": "Email",
"name": "email",
"type": "TEXT",
"uniqueIdentifier": "email",
},
],
"flatIndexMetadatas": [
{
"flatIndexFieldMetadatas": [
{
"order": 0,
"uniqueIdentifier": "idx_person_email-field-0",
},
],
"indexType": "BTREE",
"indexWhereClause": null,
"isUnique": true,
"name": "idx_person_email",
"uniqueIdentifier": "idx_person_email",
},
],
"uniqueIdentifier": "person",
},
"type": "create_object",
},
{
"flatFieldMetadata": {
"isActive": true,
"isCustom": true,
"isNullable": true,
"label": "Email",
"name": "email",
"type": "TEXT",
"uniqueIdentifier": "email",
},
"flatObjectMetadata": {
"flatFieldMetadatas": [
{
"isActive": true,
"isCustom": true,
"isNullable": true,
"label": "Email",
"name": "email",
"type": "TEXT",
"uniqueIdentifier": "email",
},
],
"flatIndexMetadatas": [
{
"flatIndexFieldMetadatas": [
{
"order": 0,
"uniqueIdentifier": "idx_person_email-field-0",
},
],
"indexType": "BTREE",
"indexWhereClause": null,
"isUnique": true,
"name": "idx_person_email",
"uniqueIdentifier": "idx_person_email",
},
],
"uniqueIdentifier": "person",
},
"type": "create_field",
},
{
"flatIndexMetadata": {
"flatIndexFieldMetadatas": [
{
"order": 0,
"uniqueIdentifier": "idx_person_email-field-0",
},
],
"indexType": "BTREE",
"indexWhereClause": null,
"isUnique": true,
"name": "idx_person_email",
"uniqueIdentifier": "idx_person_email",
},
"type": "create_index",
},
{
"flatIndexMetadata": {
"flatIndexFieldMetadatas": [
{
"order": 0,
"uniqueIdentifier": "idx_company_name_new-field-0",
},
],
"indexType": "BTREE",
"indexWhereClause": null,
"isUnique": true,
"name": "idx_company_name_new",
"uniqueIdentifier": "idx_company_name_new",
},
"type": "create_index",
},
{
"flatIndexMetadata": {
"flatIndexFieldMetadatas": [
{
"order": 0,
"uniqueIdentifier": "idx_company_name_old-field-0",
},
],
"indexType": "BTREE",
"indexWhereClause": null,
"isUnique": true,
"name": "idx_company_name_old",
"uniqueIdentifier": "idx_company_name_old",
},
"type": "delete_index",
},
],
}
`);
});
it('should handle empty objects', () => {
const result = service.build({ from: [], to: [] });
expect(result).toMatchInlineSnapshot(`
{
"actions": [],
}
`);
});
it('should handle objects with no index changes', () => {
const objects = [
createMockObject('company', [
{
type: FieldMetadataType.TEXT,
name: 'name',
label: 'Name',
uniqueIdentifier: 'name',
},
]),
];
const result = service.build({ from: objects, to: objects });
expect(result).toMatchInlineSnapshot(`
{
"actions": [],
}
`);
});
it('should handle index updates', () => {
const fromObjects: FlatObjectMetadata[] = [
createMockObject(
'company',
[
{
type: FieldMetadataType.TEXT,
name: 'name',
label: 'Name',
uniqueIdentifier: 'name',
},
],
[createMockIndex('idx_company_name', ['name'], true)],
),
];
const toObjects: FlatObjectMetadata[] = [
createMockObject(
'company',
[
{
type: FieldMetadataType.TEXT,
name: 'name',
label: 'Name',
uniqueIdentifier: 'name',
},
],
[createMockIndex('idx_company_name', ['name'], false)],
),
];
const result = service.build({ from: fromObjects, to: toObjects });
expect(result).toMatchInlineSnapshot(`
{
"actions": [
{
"flatIndexMetadata": {
"flatIndexFieldMetadatas": [
{
"order": 0,
"uniqueIdentifier": "idx_company_name-field-0",
},
],
"indexType": "BTREE",
"indexWhereClause": null,
"isUnique": true,
"name": "idx_company_name",
"uniqueIdentifier": "idx_company_name",
},
"type": "delete_index",
},
{
"flatIndexMetadata": {
"flatIndexFieldMetadatas": [
{
"order": 0,
"uniqueIdentifier": "idx_company_name-field-0",
},
],
"indexType": "BTREE",
"indexWhereClause": null,
"isUnique": false,
"name": "idx_company_name",
"uniqueIdentifier": "idx_company_name",
},
"type": "create_index",
},
],
}
`);
});
it('should not generate any actions when indexes are identical', () => {
const objects = [
createMockObject(
'company',
[
{
type: FieldMetadataType.TEXT,
name: 'name',
label: 'Name',
uniqueIdentifier: 'name',
},
],
[createMockIndex('idx_company_name', ['name'], true)],
),
];
const result = service.build({ from: objects, to: objects });
expect(result).toMatchInlineSnapshot(`
{
"actions": [],
}
`);
});
});
});

View File

@ -1,431 +0,0 @@
import { FieldMetadataType } from 'twenty-shared/types';
import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-on-delete-action.interface';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { FlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration-v2/types/flat-field-metadata';
import { FlatObjectMetadata } from 'src/engine/workspace-manager/workspace-migration-v2/types/flat-object-metadata';
import { WorkspaceMigrationBuilderV2Service } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/workspace-migration-builder-v2.service';
describe('Workspace migration builder relations tests suite', () => {
let service: WorkspaceMigrationBuilderV2Service;
beforeEach(() => {
service = new WorkspaceMigrationBuilderV2Service();
});
const createMockObject = (
identifier: string,
fields: Partial<FlatFieldMetadata>[] = [],
): FlatObjectMetadata => ({
uniqueIdentifier: identifier,
flatIndexMetadatas: [],
flatFieldMetadatas: fields.map((field) => ({
type: FieldMetadataType.TEXT,
name: 'defaultName',
label: 'Default Label',
isCustom: true,
isActive: true,
isNullable: true,
uniqueIdentifier: 'default-id',
...field,
})),
});
describe('buildWorkspaceMigrationV2RelationActions', () => {
it('should create relation actions for created fields', () => {
const fromObjects: FlatObjectMetadata[] = [];
const toObjects: FlatObjectMetadata[] = [
createMockObject('company', [
{
type: FieldMetadataType.RELATION,
name: 'employees',
label: 'Employees',
uniqueIdentifier: 'employees',
relationTargetFieldMetadataId: 'field-2',
relationTargetObjectMetadataId: 'obj-2',
},
]),
];
const result = service.build({ from: fromObjects, to: toObjects });
expect(result).toMatchInlineSnapshot(`
{
"actions": [
{
"flatObjectMetadata": {
"flatFieldMetadatas": [
{
"isActive": true,
"isCustom": true,
"isNullable": true,
"label": "Employees",
"name": "employees",
"relationTargetFieldMetadataId": "field-2",
"relationTargetObjectMetadataId": "obj-2",
"type": "RELATION",
"uniqueIdentifier": "employees",
},
],
"flatIndexMetadatas": [],
"uniqueIdentifier": "company",
},
"type": "create_object",
},
{
"flatFieldMetadata": {
"isActive": true,
"isCustom": true,
"isNullable": true,
"label": "Employees",
"name": "employees",
"relationTargetFieldMetadataId": "field-2",
"relationTargetObjectMetadataId": "obj-2",
"type": "RELATION",
"uniqueIdentifier": "employees",
},
"flatObjectMetadata": {
"flatFieldMetadatas": [
{
"isActive": true,
"isCustom": true,
"isNullable": true,
"label": "Employees",
"name": "employees",
"relationTargetFieldMetadataId": "field-2",
"relationTargetObjectMetadataId": "obj-2",
"type": "RELATION",
"uniqueIdentifier": "employees",
},
],
"flatIndexMetadatas": [],
"uniqueIdentifier": "company",
},
"type": "create_field",
},
],
}
`);
});
it('should create delete actions for deleted fields', () => {
const fromObjects: FlatObjectMetadata[] = [
createMockObject('company', [
{
type: FieldMetadataType.RELATION,
name: 'employees',
label: 'Employees',
uniqueIdentifier: 'employees',
relationTargetFieldMetadataId: 'field-2',
relationTargetObjectMetadataId: 'obj-2',
},
]),
];
const toObjects: FlatObjectMetadata[] = [createMockObject('company')];
const result = service.build({ from: fromObjects, to: toObjects });
expect(result).toMatchInlineSnapshot(`
{
"actions": [
{
"flatFieldMetadata": {
"isActive": true,
"isCustom": true,
"isNullable": true,
"label": "Employees",
"name": "employees",
"relationTargetFieldMetadataId": "field-2",
"relationTargetObjectMetadataId": "obj-2",
"type": "RELATION",
"uniqueIdentifier": "employees",
},
"flatObjectMetadata": {
"flatFieldMetadatas": [],
"flatIndexMetadatas": [],
"uniqueIdentifier": "company",
},
"type": "delete_field",
},
],
}
`);
});
it('should handle multiple relation changes across different objects', () => {
const fromObjects: FlatObjectMetadata[] = [
createMockObject('company', [
{
type: FieldMetadataType.RELATION,
name: 'oldRelation',
label: 'Old Relation',
uniqueIdentifier: 'old-relation',
relationTargetFieldMetadataId: 'field-1',
relationTargetObjectMetadataId: 'obj-1',
},
]),
];
const toObjects: FlatObjectMetadata[] = [
createMockObject('company', [
{
type: FieldMetadataType.RELATION,
name: 'newRelation',
label: 'New Relation',
uniqueIdentifier: 'new-relation',
relationTargetFieldMetadataId: 'field-2',
relationTargetObjectMetadataId: 'obj-2',
},
]),
createMockObject('person', [
{
type: FieldMetadataType.RELATION,
name: 'manager',
label: 'Manager',
uniqueIdentifier: 'manager',
relationTargetFieldMetadataId: 'field-3',
relationTargetObjectMetadataId: 'obj-3',
},
]),
];
const result = service.build({ from: fromObjects, to: toObjects });
expect(result).toMatchInlineSnapshot(`
{
"actions": [
{
"flatObjectMetadata": {
"flatFieldMetadatas": [
{
"isActive": true,
"isCustom": true,
"isNullable": true,
"label": "Manager",
"name": "manager",
"relationTargetFieldMetadataId": "field-3",
"relationTargetObjectMetadataId": "obj-3",
"type": "RELATION",
"uniqueIdentifier": "manager",
},
],
"flatIndexMetadatas": [],
"uniqueIdentifier": "person",
},
"type": "create_object",
},
{
"flatFieldMetadata": {
"isActive": true,
"isCustom": true,
"isNullable": true,
"label": "Manager",
"name": "manager",
"relationTargetFieldMetadataId": "field-3",
"relationTargetObjectMetadataId": "obj-3",
"type": "RELATION",
"uniqueIdentifier": "manager",
},
"flatObjectMetadata": {
"flatFieldMetadatas": [
{
"isActive": true,
"isCustom": true,
"isNullable": true,
"label": "Manager",
"name": "manager",
"relationTargetFieldMetadataId": "field-3",
"relationTargetObjectMetadataId": "obj-3",
"type": "RELATION",
"uniqueIdentifier": "manager",
},
],
"flatIndexMetadatas": [],
"uniqueIdentifier": "person",
},
"type": "create_field",
},
{
"flatFieldMetadata": {
"isActive": true,
"isCustom": true,
"isNullable": true,
"label": "New Relation",
"name": "newRelation",
"relationTargetFieldMetadataId": "field-2",
"relationTargetObjectMetadataId": "obj-2",
"type": "RELATION",
"uniqueIdentifier": "new-relation",
},
"flatObjectMetadata": {
"flatFieldMetadatas": [
{
"isActive": true,
"isCustom": true,
"isNullable": true,
"label": "New Relation",
"name": "newRelation",
"relationTargetFieldMetadataId": "field-2",
"relationTargetObjectMetadataId": "obj-2",
"type": "RELATION",
"uniqueIdentifier": "new-relation",
},
],
"flatIndexMetadatas": [],
"uniqueIdentifier": "company",
},
"type": "create_field",
},
{
"flatFieldMetadata": {
"isActive": true,
"isCustom": true,
"isNullable": true,
"label": "Old Relation",
"name": "oldRelation",
"relationTargetFieldMetadataId": "field-1",
"relationTargetObjectMetadataId": "obj-1",
"type": "RELATION",
"uniqueIdentifier": "old-relation",
},
"flatObjectMetadata": {
"flatFieldMetadatas": [
{
"isActive": true,
"isCustom": true,
"isNullable": true,
"label": "New Relation",
"name": "newRelation",
"relationTargetFieldMetadataId": "field-2",
"relationTargetObjectMetadataId": "obj-2",
"type": "RELATION",
"uniqueIdentifier": "new-relation",
},
],
"flatIndexMetadatas": [],
"uniqueIdentifier": "company",
},
"type": "delete_field",
},
],
}
`);
});
it('should handle empty objects', () => {
const result = service.build({ from: [], to: [] });
expect(result).toMatchInlineSnapshot(`
{
"actions": [],
}
`);
});
it('should handle objects with no relation changes', () => {
const objects = [
createMockObject('company', [
{
type: FieldMetadataType.TEXT,
name: 'name',
label: 'Name',
uniqueIdentifier: 'name',
},
]),
];
const result = service.build({ from: objects, to: objects });
expect(result).toMatchInlineSnapshot(`
{
"actions": [],
}
`);
});
it('should handle relation field updates', () => {
const baseField = {
type: FieldMetadataType.RELATION,
name: 'employees',
label: 'Employees',
uniqueIdentifier: 'employees',
isCustom: true,
isActive: true,
isNullable: true,
description: 'Company employees',
};
const fromObjects: FlatObjectMetadata[] = [
createMockObject('company', [
{
...baseField,
relationTargetFieldMetadataId: 'field-1',
relationTargetObjectMetadataId: 'obj-1',
settings: {
relationType: RelationType.ONE_TO_MANY,
onDelete: RelationOnDeleteAction.CASCADE,
} as FieldMetadataSettings<FieldMetadataType.RELATION>,
},
]),
];
const toObjects: FlatObjectMetadata[] = [
{
...fromObjects[0],
flatFieldMetadatas: [
{
...baseField,
name: 'updatedName',
},
],
},
];
const result = service.build({ from: fromObjects, to: toObjects });
expect(result).toMatchInlineSnapshot(`
{
"actions": [
{
"flatFieldMetadata": {
"description": "Company employees",
"isActive": true,
"isCustom": true,
"isNullable": true,
"label": "Employees",
"name": "updatedName",
"type": "RELATION",
"uniqueIdentifier": "employees",
},
"flatObjectMetadata": {
"flatFieldMetadatas": [
{
"description": "Company employees",
"isActive": true,
"isCustom": true,
"isNullable": true,
"label": "Employees",
"name": "updatedName",
"type": "RELATION",
"uniqueIdentifier": "employees",
},
],
"flatIndexMetadatas": [],
"uniqueIdentifier": "company",
},
"type": "update_field",
"updates": [
{
"from": "employees",
"property": "name",
"to": "updatedName",
},
],
},
],
}
`);
});
});
});

View File

@ -1,4 +1,4 @@
import { faker } from '@faker-js/faker/.';
import { faker } from '@faker-js/faker';
import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util';
import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';

View File

@ -0,0 +1,46 @@
import { isDefined } from 'twenty-shared/utils';
export const extractRecordIdsAndDatesAsExpectAny = (
record: Record<string, unknown> | Array<Record<string, unknown>>,
): any => {
if (Array.isArray(record)) {
return record.map(extractRecordIdsAndDatesAsExpectAny);
}
if (typeof record !== 'object') {
throw new Error(
'extractRecordIdsAndDatesAsExpectAny should be called with an array or a record only',
);
}
return Object.entries(record).reduce((acc, [key, value]) => {
if (!isDefined(value)) {
return acc;
}
if (key.endsWith('Id') || key === 'id') {
return {
...acc,
[key]: expect.any(String),
};
}
if (value instanceof Date) {
return {
...acc,
[key]: expect.any(Date),
};
}
if (typeof value === 'object' || Array.isArray(value)) {
return {
...acc,
[key]: extractRecordIdsAndDatesAsExpectAny(
value as Record<string, unknown>,
),
};
}
return acc;
}, {});
};

View File

@ -0,0 +1,20 @@
import { EachTestingContext } from '@/testing/types/EachTestingContext.type';
export const eachTestingContextFilter = <T>(
testCases: EachTestingContext<T>[],
) => {
const onlyTestsCases = testCases.filter((testCase) => testCase.only === true);
if (process.env.CI && onlyTestsCases.length > 0) {
console.warn(
'Should never push tests cases with an only to true, only to use in dev env\n returning the whole test suite anyway',
);
return testCases;
}
if (onlyTestsCases.length > 0) {
return onlyTestsCases;
}
return testCases;
};

View File

@ -7,5 +7,6 @@
* |___/
*/
export { eachTestingContextFilter } from './EachTestingContextFilter';
export type { EachTestingContext } from './types/EachTestingContext.type';
export type { SuccessfulAndFailingTestCases } from './types/SuccessfulAndFailingTestCases';

View File

@ -1,4 +1,5 @@
export type EachTestingContext<T> = {
title: string;
context: T;
only?: true;
};