feat: new relation sync-metadata, twenty-orm, create/update (#10217)

Fix
https://github.com/twentyhq/core-team-issues/issues/330#issue-2827026606
and
https://github.com/twentyhq/core-team-issues/issues/327#issue-2827001814

What this PR does when `isNewRelationEnabled` is set to `true`:
- [x] Drop the creation of the  foreign key as a `FieldMetadata`
- [x] Stop creating `RelationMetadata`
- [x] Properly fill `FieldMetadata` of type `RELATION` during the sync
command
- [x] Use new relation settings in TwentyORM
- [x] Properly create `FieldMetadata` relations when we create a new
object
- [x] Handle `database:reset` with new relations

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
This commit is contained in:
Jérémy M
2025-04-22 19:01:39 +02:00
committed by GitHub
parent de1489aabb
commit cc29c25176
160 changed files with 3247 additions and 711 deletions

View File

@ -93,9 +93,13 @@ export class WorkspaceManagerService {
schemaName,
);
const featureFlags =
await this.featureFlagService.getWorkspaceFeatureFlagsMap(workspaceId);
await this.workspaceSyncMetadataService.synchronize({
workspaceId,
dataSourceId: dataSourceMetadata.id,
featureFlags,
});
const dataSourceMetadataCreationEnd = performance.now();
@ -145,9 +149,13 @@ export class WorkspaceManagerService {
schemaName,
);
const featureFlags =
await this.featureFlagService.getWorkspaceFeatureFlagsMap(workspaceId);
await this.workspaceSyncMetadataService.synchronize({
workspaceId,
dataSourceId: dataSourceMetadata.id,
featureFlags,
});
await this.prefillWorkspaceWithDemoObjects(dataSourceMetadata, workspaceId);
@ -165,9 +173,13 @@ export class WorkspaceManagerService {
schemaName,
);
const featureFlags =
await this.featureFlagService.getWorkspaceFeatureFlagsMap(workspaceId);
await this.workspaceSyncMetadataService.synchronize({
workspaceId: workspaceId,
dataSourceId: dataSourceMetadata.id,
featureFlags,
});
await this.initPermissionsDev(workspaceId);

View File

@ -1,12 +1,14 @@
import { WorkspaceMigrationIndexFactory } from 'src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-index.factory';
import { WorkspaceMigrationObjectFactory } from './workspace-migration-object.factory';
import { WorkspaceMigrationFieldRelationFactory } from './workspace-migration-field-relation.factory';
import { WorkspaceMigrationFieldFactory } from './workspace-migration-field.factory';
import { WorkspaceMigrationObjectFactory } from './workspace-migration-object.factory';
import { WorkspaceMigrationRelationFactory } from './workspace-migration-relation.factory';
export const workspaceMigrationBuilderFactories = [
WorkspaceMigrationObjectFactory,
WorkspaceMigrationFieldFactory,
WorkspaceMigrationFieldRelationFactory,
WorkspaceMigrationRelationFactory,
WorkspaceMigrationIndexFactory,
];

View File

@ -0,0 +1,388 @@
import { Injectable } from '@nestjs/common';
import { FieldMetadataType } from 'twenty-shared/types';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { WorkspaceMigrationBuilderAction } from 'src/engine/workspace-manager/workspace-migration-builder/interfaces/workspace-migration-builder-action.interface';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util';
import {
WorkspaceMigrationColumnActionType,
WorkspaceMigrationEntity,
WorkspaceMigrationTableAction,
WorkspaceMigrationTableActionType,
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
import { WorkspaceMigrationFactory } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.factory';
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
import { FieldMetadataUpdate } from 'src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-field.factory';
import { camelCase } from 'src/utils/camel-case';
@Injectable()
export class WorkspaceMigrationFieldRelationFactory {
constructor(
private readonly workspaceMigrationFactory: WorkspaceMigrationFactory,
) {}
async create(
originalObjectMetadataCollection: ObjectMetadataEntity[],
fieldMetadataCollection: FieldMetadataEntity<FieldMetadataType.RELATION>[],
action:
| WorkspaceMigrationBuilderAction.CREATE
| WorkspaceMigrationBuilderAction.DELETE,
): Promise<Partial<WorkspaceMigrationEntity>[]>;
async create(
originalObjectMetadataCollection: ObjectMetadataEntity[],
fieldMetadataUpdateCollection: FieldMetadataUpdate<FieldMetadataType.RELATION>[],
action: WorkspaceMigrationBuilderAction.UPDATE,
): Promise<Partial<WorkspaceMigrationEntity>[]>;
/**
* Deletion of the relation is handled by field deletion
*/
async create(
originalObjectMetadataCollection: ObjectMetadataEntity[],
fieldMetadataCollectionOrFieldMetadataUpdateCollection:
| FieldMetadataEntity<FieldMetadataType.RELATION>[]
| FieldMetadataUpdate<FieldMetadataType.RELATION>[],
action: WorkspaceMigrationBuilderAction,
): Promise<Partial<WorkspaceMigrationEntity>[]> {
const originalObjectMetadataMap = originalObjectMetadataCollection.reduce(
(result, currentObject) => {
result[currentObject.id] = currentObject;
return result;
},
{} as Record<string, ObjectMetadataEntity>,
);
switch (action) {
case WorkspaceMigrationBuilderAction.CREATE:
return this.createFieldRelationMigration(
originalObjectMetadataMap,
fieldMetadataCollectionOrFieldMetadataUpdateCollection as FieldMetadataEntity<FieldMetadataType.RELATION>[],
);
case WorkspaceMigrationBuilderAction.UPDATE:
return this.updateFieldRelationMigration(
originalObjectMetadataMap,
fieldMetadataCollectionOrFieldMetadataUpdateCollection as FieldMetadataUpdate<FieldMetadataType.RELATION>[],
);
case WorkspaceMigrationBuilderAction.DELETE:
return this.deleteFieldRelationMigration(
originalObjectMetadataMap,
fieldMetadataCollectionOrFieldMetadataUpdateCollection as FieldMetadataEntity<FieldMetadataType.RELATION>[],
);
default:
return [];
}
}
private async updateFieldRelationMigration(
originalObjectMetadataMap: Record<string, ObjectMetadataEntity>,
fieldMetadataUpdateCollection: FieldMetadataUpdate<FieldMetadataType.RELATION>[],
): Promise<Partial<WorkspaceMigrationEntity>[]> {
const workspaceMigrations: Partial<WorkspaceMigrationEntity>[] = [];
for (const {
altered: sourceFieldMetadata,
} of fieldMetadataUpdateCollection) {
const sourceObjectMetadata =
originalObjectMetadataMap[sourceFieldMetadata.objectMetadataId];
const targetObjectMetadata =
originalObjectMetadataMap[
sourceFieldMetadata.relationTargetObjectMetadataId
];
if (!sourceObjectMetadata) {
throw new Error(
`ObjectMetadata with id ${sourceFieldMetadata.objectMetadataId} not found`,
);
}
if (!targetObjectMetadata) {
throw new Error(
`ObjectMetadata with id ${sourceFieldMetadata.relationTargetObjectMetadataId} not found`,
);
}
const targetFieldMetadata = targetObjectMetadata.fields.find(
(field) =>
field.id === sourceFieldMetadata.relationTargetFieldMetadataId,
);
if (!targetFieldMetadata) {
throw new Error(
`FieldMetadata with id ${sourceFieldMetadata.relationTargetFieldMetadataId} not found`,
);
}
if (
!isFieldMetadataEntityOfType(
targetFieldMetadata,
FieldMetadataType.RELATION,
)
) {
throw new Error(
`FieldMetadata with id ${sourceFieldMetadata.relationTargetFieldMetadataId} is not a relation`,
);
}
if (!targetFieldMetadata.settings) {
throw new Error(
`FieldMetadata for relation with id ${sourceFieldMetadata.id} has no settings`,
);
}
const migrations: WorkspaceMigrationTableAction[] = [
{
name: computeObjectTargetTable(targetObjectMetadata),
action: WorkspaceMigrationTableActionType.ALTER,
columns: [
{
action: WorkspaceMigrationColumnActionType.DROP_FOREIGN_KEY,
columnName: `${camelCase(targetFieldMetadata.name)}Id`,
},
],
},
{
name: computeObjectTargetTable(targetObjectMetadata),
action: WorkspaceMigrationTableActionType.ALTER,
columns: [
{
action: WorkspaceMigrationColumnActionType.CREATE_FOREIGN_KEY,
columnName: `${camelCase(targetFieldMetadata.name)}Id`,
referencedTableName:
computeObjectTargetTable(sourceObjectMetadata),
referencedTableColumnName: 'id',
isUnique:
targetFieldMetadata.settings.relationType ===
RelationType.ONE_TO_ONE,
onDelete: targetFieldMetadata.settings.onDelete,
},
],
},
];
workspaceMigrations.push({
workspaceId: sourceFieldMetadata.workspaceId,
name: generateMigrationName(
`update-relation-from-${sourceObjectMetadata.nameSingular}-to-${targetObjectMetadata.nameSingular}`,
),
isCustom: false,
migrations,
});
}
return workspaceMigrations;
}
private async createFieldRelationMigration(
originalObjectMetadataMap: Record<string, ObjectMetadataEntity>,
fieldRelationMetadataCollection: FieldMetadataEntity<FieldMetadataType.RELATION>[],
): Promise<Partial<WorkspaceMigrationEntity>[]> {
const workspaceMigrations: Partial<WorkspaceMigrationEntity>[] = [];
for (const sourceFieldMetadata of fieldRelationMetadataCollection) {
const sourceObjectMetadata =
originalObjectMetadataMap[sourceFieldMetadata.objectMetadataId];
const targetObjectMetadata =
originalObjectMetadataMap[
sourceFieldMetadata.relationTargetObjectMetadataId
];
if (!sourceFieldMetadata.settings) {
throw new Error(
`FieldMetadata for relation with id ${sourceFieldMetadata.id} has no settings`,
);
}
// We're creating it from `ONE_TO_MANY` with the join column so we don't need to create a migration for `MANY_TO_ONE`
if (
sourceFieldMetadata.settings.relationType === RelationType.MANY_TO_ONE
) {
continue;
}
if (!sourceObjectMetadata) {
throw new Error(
`ObjectMetadata with id ${sourceFieldMetadata.objectMetadataId} not found`,
);
}
if (!targetObjectMetadata) {
throw new Error(
`ObjectMetadata with id ${sourceFieldMetadata.relationTargetObjectMetadataId} not found`,
);
}
const targetFieldMetadata = targetObjectMetadata.fields.find(
(field) =>
field.id === sourceFieldMetadata.relationTargetFieldMetadataId,
);
if (!targetFieldMetadata) {
throw new Error(
`FieldMetadata with id ${sourceFieldMetadata.relationTargetFieldMetadataId} not found`,
);
}
if (
!isFieldMetadataEntityOfType(
targetFieldMetadata,
FieldMetadataType.RELATION,
)
) {
throw new Error(
`FieldMetadata with id ${sourceFieldMetadata.relationTargetFieldMetadataId} is not a relation`,
);
}
if (!targetFieldMetadata.settings) {
throw new Error(
`FieldMetadata for relation with id ${sourceFieldMetadata.id} has no settings`,
);
}
if (!targetFieldMetadata.settings.joinColumnName) {
continue;
}
const migrations: WorkspaceMigrationTableAction[] = [
{
name: computeObjectTargetTable(targetObjectMetadata),
action: WorkspaceMigrationTableActionType.ALTER,
columns: [
...this.workspaceMigrationFactory.createColumnActions(
WorkspaceMigrationColumnActionType.CREATE,
targetFieldMetadata,
),
],
},
{
name: computeObjectTargetTable(targetObjectMetadata),
action: WorkspaceMigrationTableActionType.ALTER,
columns: [
{
action: WorkspaceMigrationColumnActionType.CREATE_FOREIGN_KEY,
columnName:
targetFieldMetadata.settings.joinColumnName ??
`${camelCase(targetFieldMetadata.name)}Id`,
referencedTableName:
computeObjectTargetTable(sourceObjectMetadata),
referencedTableColumnName: 'id',
isUnique:
targetFieldMetadata.settings.relationType ===
RelationType.ONE_TO_ONE,
onDelete: targetFieldMetadata.settings.onDelete,
},
],
},
];
workspaceMigrations.push({
workspaceId: sourceFieldMetadata.workspaceId,
name: generateMigrationName(
`create-relation-from-${sourceObjectMetadata.nameSingular}-to-${targetObjectMetadata.nameSingular}`,
),
isCustom: false,
migrations,
});
}
return workspaceMigrations;
}
private async deleteFieldRelationMigration(
originalObjectMetadataMap: Record<string, ObjectMetadataEntity>,
fieldRelationMetadataCollection: FieldMetadataEntity<FieldMetadataType.RELATION>[],
): Promise<Partial<WorkspaceMigrationEntity>[]> {
const workspaceMigrations: Partial<WorkspaceMigrationEntity>[] = [];
for (const sourceFieldMetadata of fieldRelationMetadataCollection) {
const sourceObjectMetadata =
originalObjectMetadataMap[sourceFieldMetadata.objectMetadataId];
const targetObjectMetadata =
originalObjectMetadataMap[
sourceFieldMetadata.relationTargetObjectMetadataId
];
if (!sourceObjectMetadata) {
throw new Error(
`ObjectMetadata with id ${sourceFieldMetadata.objectMetadataId} not found`,
);
}
if (!targetObjectMetadata) {
throw new Error(
`ObjectMetadata with id ${sourceFieldMetadata.relationTargetObjectMetadataId} not found`,
);
}
const targetFieldMetadata = targetObjectMetadata.fields.find(
(field) =>
field.id === sourceFieldMetadata.relationTargetFieldMetadataId,
);
if (!targetFieldMetadata) {
throw new Error(
`FieldMetadata with id ${sourceFieldMetadata.relationTargetFieldMetadataId} not found`,
);
}
if (
!isFieldMetadataEntityOfType(
targetFieldMetadata,
FieldMetadataType.RELATION,
)
) {
throw new Error(
`FieldMetadata with id ${sourceFieldMetadata.relationTargetFieldMetadataId} is not a relation`,
);
}
if (!targetFieldMetadata.settings) {
throw new Error(
`FieldMetadata for relation with id ${sourceFieldMetadata.id} has no settings`,
);
}
const migrations: WorkspaceMigrationTableAction[] = [
{
name: computeObjectTargetTable(targetObjectMetadata),
action: WorkspaceMigrationTableActionType.ALTER,
columns: [
{
action: WorkspaceMigrationColumnActionType.DROP_FOREIGN_KEY,
columnName: `${camelCase(targetFieldMetadata.name)}Id`,
},
],
},
{
name: computeObjectTargetTable(targetObjectMetadata),
action: WorkspaceMigrationTableActionType.ALTER,
columns: [
{
action: WorkspaceMigrationColumnActionType.DROP,
columnName:
targetFieldMetadata.settings.joinColumnName ??
`${camelCase(targetFieldMetadata.name)}Id`,
},
],
},
];
workspaceMigrations.push({
workspaceId: sourceFieldMetadata.workspaceId,
name: generateMigrationName(
`update-relation-from-${sourceObjectMetadata.nameSingular}-to-${targetObjectMetadata.nameSingular}`,
),
isCustom: false,
migrations,
});
}
return workspaceMigrations;
}
}

View File

@ -5,6 +5,8 @@ import { FieldMetadataType } from 'twenty-shared/types';
import { WorkspaceMigrationBuilderAction } from 'src/engine/workspace-manager/workspace-migration-builder/interfaces/workspace-migration-builder-action.interface';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util';
@ -18,15 +20,18 @@ import {
import { WorkspaceMigrationFactory } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.factory';
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
export interface FieldMetadataUpdate {
current: FieldMetadataEntity;
altered: FieldMetadataEntity;
export interface FieldMetadataUpdate<
Type extends FieldMetadataType = FieldMetadataType,
> {
current: FieldMetadataEntity<Type>;
altered: FieldMetadataEntity<Type>;
}
@Injectable()
export class WorkspaceMigrationFieldFactory {
constructor(
private readonly workspaceMigrationFactory: WorkspaceMigrationFactory,
private readonly featureFlagService: FeatureFlagService,
) {}
async create(
@ -86,6 +91,17 @@ export class WorkspaceMigrationFieldFactory {
): Promise<Partial<WorkspaceMigrationEntity>[]> {
const workspaceMigrations: Partial<WorkspaceMigrationEntity>[] = [];
if (fieldMetadataCollection.length === 0) {
return [];
}
const workspaceId = fieldMetadataCollection[0]?.workspaceId;
const isNewRelationEnabled = await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IsNewRelationEnabled,
workspaceId,
);
const fieldMetadataCollectionGroupByObjectMetadataId =
fieldMetadataCollection.reduce(
(result, currentFieldMetadata) => {
@ -110,7 +126,10 @@ export class WorkspaceMigrationFieldFactory {
for (const fieldMetadata of fieldMetadataCollection) {
// Relations are handled in workspace-migration-relation.factory.ts
if (fieldMetadata.type === FieldMetadataType.RELATION) {
if (
!isNewRelationEnabled &&
fieldMetadata.type === FieldMetadataType.RELATION
) {
continue;
}
@ -127,7 +146,7 @@ export class WorkspaceMigrationFieldFactory {
name: generateMigrationName(
`create-${objectMetadata.nameSingular}-fields`,
),
isCustom: false,
isCustom: objectMetadata.isCustom,
migrations: [
{
name: computeObjectTargetTable(
@ -149,9 +168,23 @@ export class WorkspaceMigrationFieldFactory {
): Promise<Partial<WorkspaceMigrationEntity>[]> {
const workspaceMigrations: Partial<WorkspaceMigrationEntity>[] = [];
if (fieldMetadataUpdateCollection.length === 0) {
return [];
}
const workspaceId = fieldMetadataUpdateCollection[0]?.current.workspaceId;
const isNewRelationEnabled = await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IsNewRelationEnabled,
workspaceId,
);
for (const fieldMetadataUpdate of fieldMetadataUpdateCollection) {
// Skip relations, because they're just representation and not real columns
if (fieldMetadataUpdate.altered.type === FieldMetadataType.RELATION) {
if (
!isNewRelationEnabled &&
fieldMetadataUpdate.altered.type === FieldMetadataType.RELATION
) {
continue;
}
@ -211,7 +244,7 @@ export class WorkspaceMigrationFieldFactory {
name: generateMigrationName(
`update-${fieldMetadataUpdate.altered.name}`,
),
isCustom: false,
isCustom: fieldMetadataUpdate.altered.isCustom,
migrations,
});
}
@ -250,7 +283,7 @@ export class WorkspaceMigrationFieldFactory {
workspaceMigrations.push({
workspaceId: fieldMetadata.workspaceId,
name: generateMigrationName(`delete-${fieldMetadata.name}`),
isCustom: false,
isCustom: fieldMetadata.isCustom,
migrations,
});
}

View File

@ -109,7 +109,7 @@ export class WorkspaceMigrationObjectFactory {
workspaceMigrations.push({
workspaceId: objectMetadata.workspaceId,
name: generateMigrationName(`create-${objectMetadata.nameSingular}`),
isCustom: false,
isCustom: objectMetadata.isCustom,
migrations,
});
}
@ -136,7 +136,7 @@ export class WorkspaceMigrationObjectFactory {
name: generateMigrationName(
`rename-${objectMetadataUpdate.current.nameSingular}`,
),
isCustom: false,
isCustom: objectMetadataUpdate.altered.isCustom,
migrations: [
{
name: oldTableName,
@ -167,7 +167,7 @@ export class WorkspaceMigrationObjectFactory {
workspaceMigrations.push({
workspaceId: objectMetadata.workspaceId,
name: generateMigrationName(`delete-${objectMetadata.nameSingular}`),
isCustom: false,
isCustom: objectMetadata.isCustom,
migrations: [
...(relationMetadataCollection ?? []).map(
(relationMetadata) =>

View File

@ -1,11 +1,12 @@
import { Module } from '@nestjs/common';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module';
import { workspaceMigrationBuilderFactories } from './factories';
@Module({
imports: [WorkspaceMigrationModule],
imports: [WorkspaceMigrationModule, FeatureFlagModule],
providers: [...workspaceMigrationBuilderFactories],
exports: [...workspaceMigrationBuilderFactories],
})

View File

@ -3,6 +3,10 @@ import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-met
export const convertOnDeleteActionToOnDelete = (
onDeleteAction: RelationOnDeleteAction | undefined,
): 'CASCADE' | 'SET NULL' | 'RESTRICT' | 'NO ACTION' | undefined => {
if (!onDeleteAction) {
return undefined;
}
switch (onDeleteAction) {
case 'CASCADE':
return 'CASCADE';

View File

@ -7,6 +7,7 @@ import {
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
RunOnWorkspaceArgs,
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
@ -25,6 +26,7 @@ export class SyncWorkspaceMetadataCommand extends ActiveOrSuspendedWorkspacesMig
private readonly workspaceSyncMetadataService: WorkspaceSyncMetadataService,
private readonly dataSourceService: DataSourceService,
private readonly syncWorkspaceLoggerService: SyncWorkspaceLoggerService,
private readonly featureFlagService: FeatureFlagService,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
) {
super(workspaceRepository, twentyORMGlobalManager);
@ -45,11 +47,15 @@ export class SyncWorkspaceMetadataCommand extends ActiveOrSuspendedWorkspacesMig
workspaceId,
);
const featureFlags =
await this.featureFlagService.getWorkspaceFeatureFlagsMap(workspaceId);
const { storage, workspaceMigrations } =
await this.workspaceSyncMetadataService.synchronize(
{
workspaceId,
dataSourceId: dataSourceMetadata.id,
featureFlags,
},
{ applyChanges: !options.dryRun },
);

View File

@ -1,6 +1,7 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
@ -19,6 +20,7 @@ import { SyncWorkspaceMetadataCommand } from './sync-workspace-metadata.command'
WorkspaceModule,
DataSourceModule,
WorkspaceDataSourceModule,
FeatureFlagModule,
TypeOrmModule.forFeature([Workspace], 'core'),
SyncWorkspaceLoggerModule,
],

View File

@ -1,3 +1,4 @@
import { WorkspaceFieldRelationComparator } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-field-relation.comparator';
import { WorkspaceIndexComparator } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-index.comparator';
import { WorkspaceFieldComparator } from './workspace-field.comparator';
@ -6,6 +7,7 @@ import { WorkspaceRelationComparator } from './workspace-relation.comparator';
export const workspaceSyncMetadataComparators = [
WorkspaceFieldComparator,
WorkspaceFieldRelationComparator,
WorkspaceObjectComparator,
WorkspaceRelationComparator,
WorkspaceIndexComparator,

View File

@ -0,0 +1,250 @@
import { Injectable } from '@nestjs/common';
import diff, { DifferenceChange, DifferenceRemove } from 'microdiff';
import { FieldMetadataType } from 'twenty-shared/types';
import { FieldMetadataRelationSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
import {
ComparatorAction,
FieldRelationComparatorResult,
} from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/comparator.interface';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { transformMetadataForComparison } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/utils/transform-metadata-for-comparison.util';
const fieldPropertiesToCompare = [
'settings',
'relationTargetObjectMetadataId',
'relationTargetFieldMetadataId',
];
const fieldPropertiesToStringify = ['settings'] as const;
@Injectable()
export class WorkspaceFieldRelationComparator {
constructor() {}
public compare(
originalFieldMetadataCollection: FieldMetadataEntity<FieldMetadataType.RELATION>[],
standardFieldMetadataCollection: FieldMetadataEntity<FieldMetadataType.RELATION>[],
): FieldRelationComparatorResult[] {
const result: FieldRelationComparatorResult[] = [];
const propertiesMap: Record<
string,
Partial<FieldMetadataEntity<FieldMetadataType.RELATION>>
> = {};
// Double security to only compare non-custom fields
const filteredOriginalFieldCollection =
originalFieldMetadataCollection.filter((field) => !field.isCustom);
const originalFieldMetadataMap = transformMetadataForComparison(
filteredOriginalFieldCollection,
{
shouldIgnoreProperty: (property) => {
if (fieldPropertiesToCompare.includes(property)) {
return false;
}
return true;
},
propertiesToStringify: fieldPropertiesToStringify,
keyFactory(datum) {
// Happen when the field is custom
return datum.standardId || datum.name;
},
},
);
const standardFieldMetadataMap = transformMetadataForComparison(
standardFieldMetadataCollection,
{
shouldIgnoreProperty: (property) => {
if (fieldPropertiesToCompare.includes(property)) {
return false;
}
return true;
},
propertiesToStringify: fieldPropertiesToStringify,
keyFactory(datum) {
// Happen when the field is custom
return datum.standardId || datum.name;
},
},
);
// Compare fields
const fieldMetadataDifference = diff(
originalFieldMetadataMap,
standardFieldMetadataMap,
);
const fieldMetadataDifferenceMap = fieldMetadataDifference.reduce(
(acc, difference) => {
const fieldId = difference.path[0];
if (difference.type === 'CREATE') {
throw new Error(
`CREATE should never happen when comparing relations as they are created previously`,
);
}
if (!acc[fieldId]) {
acc[fieldId] = [];
}
acc[fieldId].push(difference);
return acc;
},
{} as Record<string, (DifferenceChange | DifferenceRemove)[]>,
);
differenceLoop: for (const [fieldId, differences] of Object.entries(
fieldMetadataDifferenceMap,
)) {
const findField = (
field: FieldMetadataEntity<FieldMetadataType.RELATION>,
) => {
return field.standardId === fieldId;
};
// Object shouldn't have thousands of fields, so we can use find here
const standardFieldMetadata =
standardFieldMetadataCollection.find(findField);
const originalFieldMetadata =
originalFieldMetadataCollection.find(findField);
const allNewPropertiesAreNull = Object.values(differences).every(
(difference) => {
if (difference.type === 'REMOVE') {
return true;
}
return difference.value === null;
},
);
const allOldPropertiesAreNull = Object.values(differences).every(
(difference) => difference.oldValue === null,
);
let relationTypeChange = false;
if (!originalFieldMetadata) {
throw new Error(`Field ${fieldId} not found in originalObjectMetadata`);
}
if (!standardFieldMetadata) {
throw new Error(`Field ${fieldId} not found in standardObjectMetadata`);
}
for (const difference of differences) {
const property = difference.path[difference.path.length - 1];
if (difference.type === 'REMOVE') {
// Whole relation is removed
if (property === fieldId) {
result.push({
action: ComparatorAction.DELETE,
object: originalFieldMetadata,
});
continue differenceLoop;
}
throw new Error(
`REMOVE partial part of relation should never happen, it should ben an update`,
);
}
// If the old value and the new value are both null, skip
// Database is storing null, and we can get undefined here
if (
difference.oldValue === null &&
(difference.value === null || difference.value === undefined)
) {
break;
}
if (typeof property !== 'string') {
break;
}
if (!propertiesMap[fieldId]) {
propertiesMap[fieldId] = {};
}
// If the property is a stringified JSON, parse it
if (
(fieldPropertiesToStringify as readonly string[]).includes(property)
) {
const newValue = this.parseJSONOrString(difference.value);
if (property === 'settings' && difference.oldValue) {
const newSettings = newValue as FieldMetadataRelationSettings;
const oldSettings =
difference.oldValue as FieldMetadataRelationSettings;
// Check if the relation type has changed
if (oldSettings.relationType !== newSettings.relationType) {
relationTypeChange = true;
}
}
propertiesMap[fieldId][property] = newValue;
} else {
propertiesMap[fieldId][property] = difference.value;
}
}
if (relationTypeChange) {
result.push({
action: ComparatorAction.DELETE,
object: originalFieldMetadata,
});
result.push({
action: ComparatorAction.CREATE,
object: {
...propertiesMap[fieldId],
id: originalFieldMetadata.id,
standardId: standardFieldMetadata.standardId ?? undefined,
},
});
} else if (allOldPropertiesAreNull) {
result.push({
action: ComparatorAction.CREATE,
object: {
...propertiesMap[fieldId],
id: originalFieldMetadata.id,
standardId: standardFieldMetadata.standardId ?? undefined,
},
});
} else if (allNewPropertiesAreNull) {
result.push({
action: ComparatorAction.DELETE,
object: originalFieldMetadata,
});
} else {
result.push({
action: ComparatorAction.UPDATE,
object: {
...propertiesMap[fieldId],
id: originalFieldMetadata.id,
standardId: standardFieldMetadata.standardId ?? undefined,
},
});
}
}
return result;
}
private parseJSONOrString(value: string | null): string | object | null {
if (value === null) {
return null;
}
try {
return JSON.parse(value);
} catch {
return value;
}
}
}

View File

@ -1,5 +1,6 @@
import { StandardIndexFactory } from 'src/engine/workspace-manager/workspace-sync-metadata/factories/standard-index.factory';
import { StandardFieldRelationFactory } from './standard-field-relation.factory';
import { StandardFieldFactory } from './standard-field.factory';
import { StandardObjectFactory } from './standard-object.factory';
import { StandardRelationFactory } from './standard-relation.factory';
@ -8,5 +9,6 @@ export const workspaceSyncMetadataFactories = [
StandardFieldFactory,
StandardObjectFactory,
StandardRelationFactory,
StandardFieldRelationFactory,
StandardIndexFactory,
];

View File

@ -0,0 +1,186 @@
import { Injectable } from '@nestjs/common';
import { FieldMetadataType } from 'twenty-shared/types';
import { WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage';
import { getJoinColumn } from 'src/engine/twenty-orm/utils/get-join-column.util';
import { convertClassNameToObjectMetadataName } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/convert-class-to-object-metadata-name.util';
import { isGatedAndNotEnabled } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/is-gate-and-not-enabled.util';
import { assert } from 'src/utils/assert';
interface CustomRelationFactory {
object: ObjectMetadataEntity;
metadata: typeof BaseWorkspaceEntity;
}
@Injectable()
export class StandardFieldRelationFactory {
createFieldRelationForCustomObject(
customObjectFactories: CustomRelationFactory[],
context: WorkspaceSyncContext,
originalObjectMetadataMap: Record<string, ObjectMetadataEntity>,
): FieldMetadataEntity<FieldMetadataType.RELATION>[] {
return customObjectFactories.flatMap((customObjectFactory) =>
this.updateFieldRelationMetadata(
customObjectFactory,
context,
originalObjectMetadataMap,
),
);
}
createFieldRelationForStandardObject(
standardObjectMetadataDefinitions: (typeof BaseWorkspaceEntity)[],
context: WorkspaceSyncContext,
originalObjectMetadataMap: Record<string, ObjectMetadataEntity>,
): Map<string, FieldMetadataEntity<FieldMetadataType.RELATION>[]> {
return standardObjectMetadataDefinitions.reduce(
(acc, standardObjectMetadata) => {
const workspaceEntityMetadataArgs = metadataArgsStorage.filterEntities(
standardObjectMetadata,
);
if (!workspaceEntityMetadataArgs) {
return acc;
}
if (
isGatedAndNotEnabled(
workspaceEntityMetadataArgs.gate,
context.featureFlags,
)
) {
return acc;
}
acc.set(
workspaceEntityMetadataArgs.standardId,
this.updateFieldRelationMetadata(
standardObjectMetadata,
context,
originalObjectMetadataMap,
),
);
return acc;
},
new Map<string, FieldMetadataEntity<FieldMetadataType.RELATION>[]>(),
);
}
private updateFieldRelationMetadata(
workspaceEntityOrCustomRelationFactory:
| typeof BaseWorkspaceEntity
| CustomRelationFactory,
context: WorkspaceSyncContext,
originalObjectMetadataMap: Record<string, ObjectMetadataEntity>,
): FieldMetadataEntity<FieldMetadataType.RELATION>[] {
const target =
'metadata' in workspaceEntityOrCustomRelationFactory
? workspaceEntityOrCustomRelationFactory.metadata
: workspaceEntityOrCustomRelationFactory;
const workspaceEntity =
'metadata' in workspaceEntityOrCustomRelationFactory
? metadataArgsStorage.filterExtendedEntities(target)
: metadataArgsStorage.filterEntities(target);
const workspaceRelationMetadataArgsCollection =
metadataArgsStorage.filterRelations(target);
if (!workspaceEntity) {
throw new Error(
`Object metadata decorator not found, can't parse ${target.name}`,
);
}
if (
!workspaceRelationMetadataArgsCollection ||
isGatedAndNotEnabled(workspaceEntity?.gate, context.featureFlags)
) {
return [];
}
return workspaceRelationMetadataArgsCollection
.filter(
(workspaceRelationMetadataArgs) =>
!isGatedAndNotEnabled(
workspaceRelationMetadataArgs.gate,
context.featureFlags,
),
)
.map((workspaceRelationMetadataArgs) => {
// Compute reflect relation metadata
const sourceObjectNameSingular =
'object' in workspaceEntityOrCustomRelationFactory
? workspaceEntityOrCustomRelationFactory.object.nameSingular
: convertClassNameToObjectMetadataName(
workspaceRelationMetadataArgs.target.name,
);
const inverseSideTarget =
workspaceRelationMetadataArgs.inverseSideTarget();
const targetObjectNameSingular = convertClassNameToObjectMetadataName(
inverseSideTarget.name,
);
const sourceFieldMetadataName = workspaceRelationMetadataArgs.name;
const targetFieldMetadataName =
(workspaceRelationMetadataArgs.inverseSideFieldKey as
| string
| undefined) ?? sourceObjectNameSingular;
const sourceObjectMetadata =
originalObjectMetadataMap[sourceObjectNameSingular];
const joinColumnsMetadataArgsCollection =
metadataArgsStorage.filterJoinColumns(target);
const joinColumnName = getJoinColumn(
joinColumnsMetadataArgsCollection,
workspaceRelationMetadataArgs,
);
assert(
sourceObjectMetadata,
`Source object ${sourceObjectNameSingular} not found in databse for relation ${workspaceRelationMetadataArgs.name} of type ${workspaceRelationMetadataArgs.type}`,
);
const targetObjectMetadata =
originalObjectMetadataMap[targetObjectNameSingular];
assert(
targetObjectMetadata,
`Target object ${targetObjectNameSingular} not found in databse for relation ${workspaceRelationMetadataArgs.name} of type ${workspaceRelationMetadataArgs.type}`,
);
const sourceFieldMetadata = sourceObjectMetadata?.fields.find(
(field) => field.name === sourceFieldMetadataName,
) as FieldMetadataEntity<FieldMetadataType.RELATION>;
assert(
sourceFieldMetadata,
`Source field ${sourceFieldMetadataName} not found in object ${sourceObjectNameSingular} for relation ${workspaceRelationMetadataArgs.name} of type ${workspaceRelationMetadataArgs.type}`,
);
const targetFieldMetadata = targetObjectMetadata?.fields.find(
(field) => field.name === targetFieldMetadataName,
) as FieldMetadataEntity<FieldMetadataType.RELATION>;
assert(
targetFieldMetadata,
`Target field ${targetFieldMetadataName} not found in object ${targetObjectNameSingular} for relation ${workspaceRelationMetadataArgs.name} of type ${workspaceRelationMetadataArgs.type}`,
);
return {
...sourceFieldMetadata,
type: FieldMetadataType.RELATION,
settings: {
relationType: workspaceRelationMetadataArgs.type,
onDelete: workspaceRelationMetadataArgs.onDelete,
joinColumnName,
},
relationTargetObjectMetadataId: targetObjectMetadata.id,
relationTargetFieldMetadataId: targetFieldMetadata.id,
} satisfies FieldMetadataEntity<FieldMetadataType.RELATION>;
});
}
}

View File

@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
import { FieldMetadataType } from 'twenty-shared/types';
import { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { WorkspaceDynamicRelationMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-dynamic-relation-metadata-args.interface';
import { WorkspaceEntityMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-entity-metadata-args.interface';
import { WorkspaceFieldMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-field-metadata-args.interface';
@ -13,7 +13,7 @@ import {
} from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/partial-field-metadata.interface';
import { WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface';
import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage';
import { getJoinColumn } from 'src/engine/twenty-orm/utils/get-join-column.util';
@ -25,13 +25,11 @@ export class StandardFieldFactory {
create(
target: typeof BaseWorkspaceEntity,
context: WorkspaceSyncContext,
workspaceFeatureFlagsMap: FeatureFlagMap,
): (PartialFieldMetadata | PartialComputedFieldMetadata)[];
create(
targets: (typeof BaseWorkspaceEntity)[],
context: WorkspaceSyncContext,
workspaceFeatureFlagsMap: FeatureFlagMap, // Map of standardId to field metadata
): Map<string, (PartialFieldMetadata | PartialComputedFieldMetadata)[]>;
create(
@ -39,7 +37,6 @@ export class StandardFieldFactory {
| typeof BaseWorkspaceEntity
| (typeof BaseWorkspaceEntity)[],
context: WorkspaceSyncContext,
workspaceFeatureFlagsMap: FeatureFlagMap,
):
| (PartialFieldMetadata | PartialComputedFieldMetadata)[]
| Map<string, (PartialFieldMetadata | PartialComputedFieldMetadata)[]> {
@ -55,7 +52,7 @@ export class StandardFieldFactory {
if (
isGatedAndNotEnabled(
workspaceEntityMetadataArgs.gate,
workspaceFeatureFlagsMap,
context.featureFlags,
)
) {
return acc;
@ -63,7 +60,7 @@ export class StandardFieldFactory {
acc.set(
workspaceEntityMetadataArgs.standardId,
this.create(target, context, workspaceFeatureFlagsMap),
this.create(target, context),
);
return acc;
@ -79,21 +76,18 @@ export class StandardFieldFactory {
workspaceEntityMetadataArgs,
metadataCollections.fields,
context,
workspaceFeatureFlagsMap,
this.createFieldMetadata,
),
...this.processMetadata(
workspaceEntityMetadataArgs,
metadataCollections.relations,
context,
workspaceFeatureFlagsMap,
this.createFieldRelationMetadata,
),
...this.processMetadata(
workspaceEntityMetadataArgs,
metadataCollections.dynamicRelations,
context,
workspaceFeatureFlagsMap,
this.createComputedFieldRelationMetadata,
),
];
@ -114,22 +108,15 @@ export class StandardFieldFactory {
workspaceEntityMetadataArgs: WorkspaceEntityMetadataArgs | undefined,
metadataArgs: T[],
context: WorkspaceSyncContext,
featureFlagsMap: FeatureFlagMap,
createMetadata: (
workspaceEntityMetadataArgs: WorkspaceEntityMetadataArgs | undefined,
args: T,
context: WorkspaceSyncContext,
featureFlagsMap: FeatureFlagMap,
) => U[],
): U[] {
return metadataArgs
.flatMap((args) =>
createMetadata(
workspaceEntityMetadataArgs,
args,
context,
featureFlagsMap,
),
createMetadata(workspaceEntityMetadataArgs, args, context),
)
.filter(Boolean) as U[];
}
@ -141,12 +128,11 @@ export class StandardFieldFactory {
workspaceEntityMetadataArgs: WorkspaceEntityMetadataArgs | undefined,
workspaceFieldMetadataArgs: WorkspaceFieldMetadataArgs,
context: WorkspaceSyncContext,
workspaceFeatureFlagsMap: FeatureFlagMap,
): PartialFieldMetadata[] {
if (
isGatedAndNotEnabled(
workspaceFieldMetadataArgs.gate,
workspaceFeatureFlagsMap,
context.featureFlags,
)
) {
return [];
@ -182,8 +168,10 @@ export class StandardFieldFactory {
workspaceEntityMetadataArgs: WorkspaceEntityMetadataArgs | undefined,
workspaceRelationMetadataArgs: WorkspaceRelationMetadataArgs,
context: WorkspaceSyncContext,
workspaceFeatureFlagsMap: FeatureFlagMap,
): PartialFieldMetadata[] {
const isNewRelationEnabled =
context.featureFlags[FeatureFlagKey.IsNewRelationEnabled];
const fieldMetadataCollection: PartialFieldMetadata[] = [];
const foreignKeyStandardId = createDeterministicUuid(
workspaceRelationMetadataArgs.standardId,
@ -200,13 +188,14 @@ export class StandardFieldFactory {
if (
isGatedAndNotEnabled(
workspaceRelationMetadataArgs.gate,
workspaceFeatureFlagsMap,
context.featureFlags,
)
) {
return [];
}
if (joinColumn) {
// We don't want to create the join column field metadata for new relation
if (!isNewRelationEnabled && joinColumn) {
fieldMetadataCollection.push({
type: FieldMetadataType.UUID,
standardId: foreignKeyStandardId,
@ -222,8 +211,7 @@ export class StandardFieldFactory {
isSystem: true,
isNullable: workspaceRelationMetadataArgs.isNullable,
isUnique:
workspaceRelationMetadataArgs.type ===
RelationMetadataType.ONE_TO_ONE,
workspaceRelationMetadataArgs.type === RelationType.ONE_TO_ONE,
isActive: workspaceRelationMetadataArgs.isActive ?? true,
});
}
@ -235,15 +223,13 @@ export class StandardFieldFactory {
label: workspaceRelationMetadataArgs.label,
description: workspaceRelationMetadataArgs.description,
icon: workspaceRelationMetadataArgs.icon,
defaultValue: null,
workspaceId: context.workspaceId,
isCustom: false,
isSystem:
workspaceEntityMetadataArgs?.isSystem ||
workspaceRelationMetadataArgs.isSystem,
isNullable: true,
isUnique:
workspaceRelationMetadataArgs.type === RelationMetadataType.ONE_TO_ONE,
isUnique: workspaceRelationMetadataArgs.type === RelationType.ONE_TO_ONE,
isActive: workspaceRelationMetadataArgs.isActive ?? true,
});
@ -259,13 +245,12 @@ export class StandardFieldFactory {
| WorkspaceDynamicRelationMetadataArgs
| undefined,
context: WorkspaceSyncContext,
workspaceFeatureFlagsMap: FeatureFlagMap,
): PartialComputedFieldMetadata[] {
if (
!workspaceDynamicRelationMetadataArgs ||
isGatedAndNotEnabled(
workspaceDynamicRelationMetadataArgs.gate,
workspaceFeatureFlagsMap,
context.featureFlags,
)
) {
return [];

View File

@ -1,6 +1,5 @@
import { Injectable } from '@nestjs/common';
import { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface';
import { PartialIndexMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/partial-index-metadata.interface';
import { WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface';
@ -20,7 +19,6 @@ export class StandardIndexFactory {
context: WorkspaceSyncContext,
originalStandardObjectMetadataMap: Record<string, ObjectMetadataEntity>,
originalCustomObjectMetadataMap: Record<string, ObjectMetadataEntity>,
workspaceFeatureFlagsMap: FeatureFlagMap,
): Partial<IndexMetadataEntity>[] {
const standardIndexOnStandardObjects =
standardObjectMetadataDefinitions.flatMap((standardObjectMetadata) =>
@ -28,7 +26,6 @@ export class StandardIndexFactory {
standardObjectMetadata,
context,
originalStandardObjectMetadataMap,
workspaceFeatureFlagsMap,
),
);
@ -36,7 +33,6 @@ export class StandardIndexFactory {
this.createStandardIndexMetadataForCustomObject(
context,
originalCustomObjectMetadataMap,
workspaceFeatureFlagsMap,
);
return [
@ -49,7 +45,6 @@ export class StandardIndexFactory {
target: typeof BaseWorkspaceEntity,
context: WorkspaceSyncContext,
originalStandardObjectMetadataMap: Record<string, ObjectMetadataEntity>,
workspaceFeatureFlagsMap: FeatureFlagMap,
): Partial<IndexMetadataEntity>[] {
const workspaceEntity = metadataArgsStorage.filterEntities(target);
@ -59,7 +54,7 @@ export class StandardIndexFactory {
);
}
if (isGatedAndNotEnabled(workspaceEntity?.gate, workspaceFeatureFlagsMap)) {
if (isGatedAndNotEnabled(workspaceEntity?.gate, context.featureFlags)) {
return [];
}
@ -68,7 +63,7 @@ export class StandardIndexFactory {
.filter((workspaceIndexMetadataArgs) => {
return !isGatedAndNotEnabled(
workspaceIndexMetadataArgs.gate,
workspaceFeatureFlagsMap,
context.featureFlags,
);
});
@ -102,7 +97,6 @@ export class StandardIndexFactory {
private createStandardIndexMetadataForCustomObject(
context: WorkspaceSyncContext,
originalCustomObjectMetadataMap: Record<string, ObjectMetadataEntity>,
workspaceFeatureFlagsMap: FeatureFlagMap,
): Partial<IndexMetadataEntity>[] {
const target = CustomWorkspaceEntity;
const workspaceEntity = metadataArgsStorage.filterExtendedEntities(target);
@ -118,7 +112,7 @@ export class StandardIndexFactory {
.filter((workspaceIndexMetadataArgs) => {
return !isGatedAndNotEnabled(
workspaceIndexMetadataArgs.gate,
workspaceFeatureFlagsMap,
context.featureFlags,
);
});

View File

@ -1,6 +1,5 @@
import { Injectable } from '@nestjs/common';
import { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface';
import { PartialWorkspaceEntity } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/partial-object-metadata.interface';
import { WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface';
@ -13,19 +12,15 @@ export class StandardObjectFactory {
create(
standardObjectMetadataDefinitions: (typeof BaseWorkspaceEntity)[],
context: WorkspaceSyncContext,
workspaceFeatureFlagsMap: FeatureFlagMap,
): Omit<PartialWorkspaceEntity, 'fields' | 'indexMetadatas'>[] {
return standardObjectMetadataDefinitions
.map((metadata) =>
this.createObjectMetadata(metadata, context, workspaceFeatureFlagsMap),
)
.map((metadata) => this.createObjectMetadata(metadata, context))
.filter((metadata): metadata is PartialWorkspaceEntity => !!metadata);
}
private createObjectMetadata(
target: typeof BaseWorkspaceEntity,
context: WorkspaceSyncContext,
workspaceFeatureFlagsMap: FeatureFlagMap,
): Omit<PartialWorkspaceEntity, 'fields' | 'indexMetadatas'> | undefined {
const workspaceEntityMetadataArgs =
metadataArgsStorage.filterEntities(target);
@ -39,7 +34,7 @@ export class StandardObjectFactory {
if (
isGatedAndNotEnabled(
workspaceEntityMetadataArgs.gate,
workspaceFeatureFlagsMap,
context.featureFlags,
)
) {
return undefined;

View File

@ -1,18 +1,18 @@
import { Injectable } from '@nestjs/common';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface';
import { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface';
import { isGatedAndNotEnabled } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/is-gate-and-not-enabled.util';
import { assert } from 'src/utils/assert';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import {
RelationMetadataEntity,
RelationMetadataType,
} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { convertClassNameToObjectMetadataName } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/convert-class-to-object-metadata-name.util';
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage';
import { convertClassNameToObjectMetadataName } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/convert-class-to-object-metadata-name.util';
import { isGatedAndNotEnabled } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/is-gate-and-not-enabled.util';
import { assert } from 'src/utils/assert';
interface CustomRelationFactory {
object: ObjectMetadataEntity;
@ -25,14 +25,12 @@ export class StandardRelationFactory {
customObjectFactories: CustomRelationFactory[],
context: WorkspaceSyncContext,
originalObjectMetadataMap: Record<string, ObjectMetadataEntity>,
workspaceFeatureFlagsMap: FeatureFlagMap,
): Partial<RelationMetadataEntity>[];
create(
standardObjectMetadataDefinitions: (typeof BaseWorkspaceEntity)[],
context: WorkspaceSyncContext,
originalObjectMetadataMap: Record<string, ObjectMetadataEntity>,
workspaceFeatureFlagsMap: FeatureFlagMap,
): Partial<RelationMetadataEntity>[];
create(
@ -44,7 +42,6 @@ export class StandardRelationFactory {
}[],
context: WorkspaceSyncContext,
originalObjectMetadataMap: Record<string, ObjectMetadataEntity>,
workspaceFeatureFlagsMap: FeatureFlagMap,
): Partial<RelationMetadataEntity>[] {
return standardObjectMetadataDefinitionsOrCustomObjectFactories.flatMap(
(
@ -56,7 +53,6 @@ export class StandardRelationFactory {
standardObjectMetadata,
context,
originalObjectMetadataMap,
workspaceFeatureFlagsMap,
),
);
}
@ -67,7 +63,6 @@ export class StandardRelationFactory {
| CustomRelationFactory,
context: WorkspaceSyncContext,
originalObjectMetadataMap: Record<string, ObjectMetadataEntity>,
workspaceFeatureFlagsMap: FeatureFlagMap,
): Partial<RelationMetadataEntity>[] {
const target =
'metadata' in workspaceEntityOrCustomRelationFactory
@ -88,7 +83,7 @@ export class StandardRelationFactory {
if (
!workspaceRelationMetadataArgsCollection ||
isGatedAndNotEnabled(workspaceEntity?.gate, workspaceFeatureFlagsMap)
isGatedAndNotEnabled(workspaceEntity?.gate, context.featureFlags)
) {
return [];
}
@ -96,16 +91,13 @@ export class StandardRelationFactory {
return workspaceRelationMetadataArgsCollection
.filter((workspaceRelationMetadataArgs) => {
// We're not storing many-to-one relations in the DB for the moment
if (
workspaceRelationMetadataArgs.type ===
RelationMetadataType.MANY_TO_ONE
) {
if (workspaceRelationMetadataArgs.type === RelationType.MANY_TO_ONE) {
return false;
}
return !isGatedAndNotEnabled(
workspaceRelationMetadataArgs.gate,
workspaceFeatureFlagsMap,
context.featureFlags,
);
})
.map((workspaceRelationMetadataArgs) => {
@ -163,7 +155,9 @@ export class StandardRelationFactory {
);
return {
relationType: workspaceRelationMetadataArgs.type,
// TODO: Will be removed when we drop RelationMetadata
relationType:
workspaceRelationMetadataArgs.type as unknown as RelationMetadataType,
fromObjectMetadataId: fromObjectMetadata?.id,
toObjectMetadataId: toObjectMetadata?.id,
fromFieldMetadataId: fromFieldMetadata?.id,

View File

@ -1,3 +1,5 @@
import { FieldMetadataType } from 'twenty-shared/types';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
@ -52,6 +54,20 @@ export type FieldComparatorResult =
>
| ComparatorDeleteResult<FieldMetadataEntity>;
export type FieldRelationComparatorResult =
| ComparatorSkipResult
| ComparatorCreateResult<
Partial<ComputedPartialFieldMetadata<FieldMetadataType.RELATION>> & {
id: string;
}
>
| ComparatorUpdateResult<
Partial<ComputedPartialFieldMetadata<FieldMetadataType.RELATION>> & {
id: string;
}
>
| ComparatorDeleteResult<FieldMetadataEntity<FieldMetadataType.RELATION>>;
export type RelationComparatorResult =
| ComparatorCreateResult<Partial<RelationMetadataEntity>>
| ComparatorDeleteResult<RelationMetadataEntity>

View File

@ -5,8 +5,10 @@ import { WorkspaceDynamicRelationMetadataArgsFactory } from 'src/engine/twenty-o
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
export type PartialFieldMetadata = Omit<
FieldMetadataInterface,
export type PartialFieldMetadata<
T extends FieldMetadataType = FieldMetadataType,
> = Omit<
FieldMetadataInterface<T>,
'id' | 'label' | 'description' | 'objectMetadataId'
> & {
standardId: string;
@ -31,6 +33,10 @@ export type PartialComputedFieldMetadata = {
objectMetadataId?: string;
};
export type ComputedPartialFieldMetadata = {
[K in keyof PartialFieldMetadata]: ExcludeFunctions<PartialFieldMetadata[K]>;
export type ComputedPartialFieldMetadata<
T extends FieldMetadataType = FieldMetadataType,
> = {
[K in keyof PartialFieldMetadata<T>]: ExcludeFunctions<
PartialFieldMetadata<T>[K]
>;
};

View File

@ -0,0 +1 @@
export type UpdaterAction = 'create' | 'update' | 'delete';

View File

@ -0,0 +1,5 @@
import { UpdaterAction } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/updater-action.type';
export interface UpdaterOptions {
actions: UpdaterAction[];
}

View File

@ -1,4 +1,7 @@
import { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface';
export interface WorkspaceSyncContext {
workspaceId: string;
dataSourceId: string;
featureFlags: FeatureFlagMap;
}

View File

@ -1,5 +1,7 @@
import { Injectable } from '@nestjs/common';
import { FieldMetadataType } from 'twenty-shared/types';
import { capitalize } from 'twenty-shared/utils';
import {
EntityManager,
EntityTarget,
@ -10,12 +12,13 @@ import {
} from 'typeorm';
import { DeepPartial } from 'typeorm/common/DeepPartial';
import { v4 as uuidV4 } from 'uuid';
import { capitalize } from 'twenty-shared/utils';
import { FieldMetadataType } from 'twenty-shared/types';
import { PartialFieldMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/partial-field-metadata.interface';
import { PartialIndexMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/partial-index-metadata.interface';
import { UpdaterOptions } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/updater-options.interface';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
import { FieldMetadataComplexOption } from 'src/engine/metadata-modules/field-metadata/dtos/options.input';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
@ -25,63 +28,77 @@ import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
import { CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory';
import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
import { FieldMetadataUpdate } from 'src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-field.factory';
import { ObjectMetadataUpdate } from 'src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-object.factory';
import { WorkspaceSyncStorage } from 'src/engine/workspace-manager/workspace-sync-metadata/storage/workspace-sync.storage';
@Injectable()
export class WorkspaceMetadataUpdaterService {
constructor(private readonly featureFlagService: FeatureFlagService) {}
async updateObjectMetadata(
manager: EntityManager,
storage: WorkspaceSyncStorage,
options?: UpdaterOptions,
): Promise<{
createdObjectMetadataCollection: ObjectMetadataEntity[];
updatedObjectMetadataCollection: ObjectMetadataUpdate[];
}> {
const objectMetadataRepository =
manager.getRepository(ObjectMetadataEntity);
let createdObjectMetadataCollection: ObjectMetadataEntity[] = [];
let updatedObjectMetadataCollection: ObjectMetadataUpdate[] = [];
/**
* Create object metadata
*/
const createdPartialObjectMetadataCollection =
await objectMetadataRepository.save(
storage.objectMetadataCreateCollection.map((objectMetadata) => ({
...objectMetadata,
isActive: true,
})) as DeepPartial<ObjectMetadataEntity>[],
if (!options || options.actions.includes('create')) {
const createdPartialObjectMetadataCollection =
await objectMetadataRepository.save(
storage.objectMetadataCreateCollection.map((objectMetadata) => ({
...objectMetadata,
isActive: true,
})) as DeepPartial<ObjectMetadataEntity>[],
);
const identifiers = createdPartialObjectMetadataCollection.map(
(object) => object.id,
);
const identifiers = createdPartialObjectMetadataCollection.map(
(object) => object.id,
);
const createdObjectMetadataCollection = await manager.find(
ObjectMetadataEntity,
{
where: { id: In(identifiers) },
relations: ['dataSource', 'fields'],
},
);
createdObjectMetadataCollection = await manager.find(
ObjectMetadataEntity,
{
where: { id: In(identifiers) },
relations: ['dataSource', 'fields'],
},
);
}
/**
* Update object metadata
*/
const updatedObjectMetadataCollection = await this.updateEntities(
manager,
ObjectMetadataEntity,
storage.objectMetadataUpdateCollection,
[
'fields',
'dataSourceId',
'workspaceId',
'labelIdentifierFieldMetadataId',
'imageIdentifierFieldMetadataId',
],
);
if (!options || options.actions.includes('update')) {
updatedObjectMetadataCollection = await this.updateEntities(
manager,
ObjectMetadataEntity,
storage.objectMetadataUpdateCollection,
[
'fields',
'dataSourceId',
'workspaceId',
'labelIdentifierFieldMetadataId',
'imageIdentifierFieldMetadataId',
],
);
}
/**
* Delete object metadata
*/
if (storage.objectMetadataDeleteCollection.length > 0) {
if (
storage.objectMetadataDeleteCollection.length > 0 &&
(!options || options.actions.includes('delete'))
) {
await objectMetadataRepository.delete(
storage.objectMetadataDeleteCollection.map((object) => object.id),
);
@ -121,6 +138,7 @@ export class WorkspaceMetadataUpdaterService {
async updateFieldMetadata(
manager: EntityManager,
storage: WorkspaceSyncStorage,
options?: UpdaterOptions,
): Promise<{
createdFieldMetadataCollection: FieldMetadataEntity[];
updatedFieldMetadataCollection: FieldMetadataUpdate[];
@ -130,26 +148,32 @@ export class WorkspaceMetadataUpdaterService {
IndexFieldMetadataEntity,
);
const indexMetadataRepository = manager.getRepository(IndexMetadataEntity);
let createdFieldMetadataCollection: FieldMetadataEntity[] = [];
let updatedFieldMetadataCollection: FieldMetadataUpdate[] = [];
/**
* Update field metadata
*/
const updatedFieldMetadataCollection =
await this.updateEntities<FieldMetadataEntity>(
manager,
FieldMetadataEntity,
storage.fieldMetadataUpdateCollection,
['objectMetadataId', 'workspaceId'],
);
if (!options || options.actions.includes('update')) {
updatedFieldMetadataCollection =
await this.updateEntities<FieldMetadataEntity>(
manager,
FieldMetadataEntity,
storage.fieldMetadataUpdateCollection,
['objectMetadataId', 'workspaceId'],
);
}
/**
* Create field metadata
*/
const createdFieldMetadataCollection = await fieldMetadataRepository.save(
storage.fieldMetadataCreateCollection.map((field) =>
this.prepareFieldMetadataForCreation(field),
) as DeepPartial<FieldMetadataEntity>[],
);
if (!options || options.actions.includes('create')) {
createdFieldMetadataCollection = await fieldMetadataRepository.save(
storage.fieldMetadataCreateCollection.map((field) =>
this.prepareFieldMetadataForCreation(field),
) as DeepPartial<FieldMetadataEntity>[],
);
}
/**
* Delete field metadata
@ -160,7 +184,10 @@ export class WorkspaceMetadataUpdaterService {
(field) => field.type !== FieldMetadataType.RELATION,
);
if (fieldMetadataDeleteCollectionWithoutRelationType.length > 0) {
if (
fieldMetadataDeleteCollectionWithoutRelationType.length > 0 &&
(!options || options.actions.includes('delete'))
) {
await this.deleteIndexFieldMetadata(
fieldMetadataDeleteCollectionWithoutRelationType,
indexFieldMetadataRepository,
@ -181,6 +208,68 @@ export class WorkspaceMetadataUpdaterService {
};
}
async updateFieldRelationMetadata(
manager: EntityManager,
storage: WorkspaceSyncStorage,
options?: UpdaterOptions,
): Promise<{
createdFieldRelationMetadataCollection: FieldMetadataUpdate<FieldMetadataType.RELATION>[];
updatedFieldRelationMetadataCollection: FieldMetadataUpdate<FieldMetadataType.RELATION>[];
deletedFieldRelationMetadataCollection: FieldMetadataUpdate<FieldMetadataType.RELATION>[];
}> {
let createdFieldRelationMetadataCollection: FieldMetadataUpdate<FieldMetadataType.RELATION>[] =
[];
let updatedFieldRelationMetadataCollection: FieldMetadataUpdate<FieldMetadataType.RELATION>[] =
[];
let deletedFieldRelationMetadataCollection: FieldMetadataUpdate<FieldMetadataType.RELATION>[] =
[];
/**
* Create field relation metadata
*/
if (!options || options.actions.includes('create')) {
createdFieldRelationMetadataCollection = await this.updateEntities<
FieldMetadataEntity<FieldMetadataType.RELATION>
>(
manager,
FieldMetadataEntity,
storage.fieldRelationMetadataCreateCollection,
['objectMetadataId', 'workspaceId'],
);
}
/**
* Update field relation metadata
*/
if (!options || options.actions.includes('update')) {
updatedFieldRelationMetadataCollection = await this.updateEntities<
FieldMetadataEntity<FieldMetadataType.RELATION>
>(
manager,
FieldMetadataEntity,
storage.fieldRelationMetadataUpdateCollection,
['objectMetadataId', 'workspaceId'],
);
}
if (!options || options.actions.includes('delete')) {
deletedFieldRelationMetadataCollection = await this.updateEntities<
FieldMetadataEntity<FieldMetadataType.RELATION>
>(
manager,
FieldMetadataEntity,
storage.fieldRelationMetadataDeleteCollection,
['objectMetadataId', 'workspaceId'],
);
}
return {
createdFieldRelationMetadataCollection,
updatedFieldRelationMetadataCollection,
deletedFieldRelationMetadataCollection,
};
}
async deleteIndexFieldMetadata(
fieldMetadataDeleteCollectionWithoutRelationType: Partial<FieldMetadataEntity>[],
indexFieldMetadataRepository: Repository<IndexFieldMetadataEntity>,
@ -276,6 +365,15 @@ export class WorkspaceMetadataUpdaterService {
createdIndexMetadataCollection: IndexMetadataEntity[];
}> {
const indexMetadataRepository = manager.getRepository(IndexMetadataEntity);
const workspaceId = originalObjectMetadataCollection?.[0]?.workspaceId;
let isNewRelationEnabled = false;
if (workspaceId) {
isNewRelationEnabled = await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IsNewRelationEnabled,
workspaceId,
);
}
const convertIndexMetadataForSaving = (
indexMetadata: PartialIndexMetadata,
@ -288,6 +386,15 @@ export class WorkspaceMetadataUpdaterService {
const fieldMetadata = originalObjectMetadataCollection
.find((object) => object.id === indexMetadata.objectMetadataId)
?.fields.find((field) => {
if (
isNewRelationEnabled &&
isFieldMetadataEntityOfType(field, FieldMetadataType.RELATION)
) {
if (field.settings?.joinColumnName === column) {
return true;
}
}
if (field.name === column) {
return true;
}

View File

@ -0,0 +1,267 @@
import { Injectable, Logger } from '@nestjs/common';
import { FieldMetadataType } from 'twenty-shared/types';
import { EntityManager } from 'typeorm';
import { WorkspaceMigrationBuilderAction } from 'src/engine/workspace-manager/workspace-migration-builder/interfaces/workspace-migration-builder-action.interface';
import {
ComparatorAction,
FieldRelationComparatorResult,
} from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/comparator.interface';
import { WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { WorkspaceMigrationEntity } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
import { CustomWorkspaceEntity } from 'src/engine/twenty-orm/custom.workspace-entity';
import { WorkspaceMigrationFieldRelationFactory } from 'src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-field-relation.factory';
import {
FieldMetadataUpdate,
WorkspaceMigrationFieldFactory,
} from 'src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-field.factory';
import { WorkspaceFieldRelationComparator } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-field-relation.comparator';
import { StandardFieldRelationFactory } from 'src/engine/workspace-manager/workspace-sync-metadata/factories/standard-field-relation.factory';
import { WorkspaceMetadataUpdaterService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-metadata-updater.service';
import { standardObjectMetadataDefinitions } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-objects';
import { WorkspaceSyncStorage } from 'src/engine/workspace-manager/workspace-sync-metadata/storage/workspace-sync.storage';
import { mapObjectMetadataByUniqueIdentifier } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/sync-metadata.util';
@Injectable()
export class WorkspaceSyncFieldMetadataRelationService {
private readonly logger = new Logger(
WorkspaceSyncFieldMetadataRelationService.name,
);
constructor(
private readonly standardFieldRelationFactory: StandardFieldRelationFactory,
private readonly workspaceFieldRelationComparator: WorkspaceFieldRelationComparator,
private readonly workspaceMetadataUpdaterService: WorkspaceMetadataUpdaterService,
private readonly workspaceMigrationFieldFactory: WorkspaceMigrationFieldFactory,
private readonly workspaceMigrationFieldRelationFactory: WorkspaceMigrationFieldRelationFactory,
) {}
async synchronize(
context: WorkspaceSyncContext,
manager: EntityManager,
storage: WorkspaceSyncStorage,
): Promise<Partial<WorkspaceMigrationEntity>[]> {
let originalObjectMetadataCollection =
await this.getOriginalObjectMetadataCollection(context, manager);
const customObjectMetadataCollection =
originalObjectMetadataCollection.filter(
(objectMetadata) => objectMetadata.isCustom,
);
// Create map of object metadata & field metadata by unique identifier
const originalObjectMetadataMapByName = mapObjectMetadataByUniqueIdentifier(
originalObjectMetadataCollection,
// Relation are based on the singular name
(objectMetadata) => objectMetadata.nameSingular,
);
await this.synchronizeStandardObjectRelationFields(
context,
originalObjectMetadataCollection,
originalObjectMetadataMapByName,
storage,
);
await this.synchronizeCustomObjectRelationFields(
context,
customObjectMetadataCollection,
originalObjectMetadataMapByName,
storage,
);
this.logger.log('Updating workspace metadata');
// Save field metadata to DB
const metadataFieldUpdaterResult =
await this.workspaceMetadataUpdaterService.updateFieldRelationMetadata(
manager,
storage,
);
this.logger.log('Generating migrations');
const updateFieldWorkspaceMigrations =
await this.workspaceMigrationFieldFactory.create(
originalObjectMetadataCollection,
[
// Both of them are update as field where created in WorkspaceSyncFieldMetadataService
...metadataFieldUpdaterResult.createdFieldRelationMetadataCollection,
...metadataFieldUpdaterResult.updatedFieldRelationMetadataCollection,
...metadataFieldUpdaterResult.deletedFieldRelationMetadataCollection,
] as FieldMetadataUpdate[],
WorkspaceMigrationBuilderAction.UPDATE,
);
// Resync updated object metadata
originalObjectMetadataCollection =
await this.getOriginalObjectMetadataCollection(context, manager);
const deletedFieldRelationMetadataCollection =
metadataFieldUpdaterResult.deletedFieldRelationMetadataCollection.map(
(field) => field.altered,
);
const deleteFieldRelationWorkspaceMigrations =
await this.workspaceMigrationFieldRelationFactory.create(
originalObjectMetadataCollection,
deletedFieldRelationMetadataCollection,
WorkspaceMigrationBuilderAction.DELETE,
);
const createdFieldRelationMetadataCollection =
metadataFieldUpdaterResult.createdFieldRelationMetadataCollection.map(
(field) => field.altered,
);
const createFieldRelationWorkspaceMigrations =
await this.workspaceMigrationFieldRelationFactory.create(
originalObjectMetadataCollection,
createdFieldRelationMetadataCollection,
WorkspaceMigrationBuilderAction.CREATE,
);
const updateFieldRelationWorkspaceMigrations =
await this.workspaceMigrationFieldRelationFactory.create(
originalObjectMetadataCollection,
metadataFieldUpdaterResult.updatedFieldRelationMetadataCollection,
WorkspaceMigrationBuilderAction.UPDATE,
);
// TODO: Should we handle deletion of relation here?
this.logger.log('Saving migrations');
return [
...updateFieldWorkspaceMigrations,
...deleteFieldRelationWorkspaceMigrations,
...createFieldRelationWorkspaceMigrations,
...updateFieldRelationWorkspaceMigrations,
];
}
private async synchronizeStandardObjectRelationFields(
context: WorkspaceSyncContext,
originalObjectMetadataCollection: ObjectMetadataEntity[],
originalObjectMetadataMapByName: Record<string, ObjectMetadataEntity>,
storage: WorkspaceSyncStorage,
): Promise<void> {
// Create standard field metadata map
const standardFieldMetadataRelationCollection =
this.standardFieldRelationFactory.createFieldRelationForStandardObject(
standardObjectMetadataDefinitions,
context,
originalObjectMetadataMapByName,
);
// Create map of original and standard object metadata by standard ids
const originalObjectMetadataMap = mapObjectMetadataByUniqueIdentifier(
originalObjectMetadataCollection,
);
// Loop over all standard objects and compare them with the objects in DB
for (const [
standardObjectId,
standardFieldMetadataCollection,
] of standardFieldMetadataRelationCollection) {
const originalObjectMetadata =
originalObjectMetadataMap[standardObjectId];
const originalFieldRelationMetadataCollection =
(originalObjectMetadata?.fields.filter(
(field) => field.type === FieldMetadataType.RELATION,
) ?? []) as FieldMetadataEntity<FieldMetadataType.RELATION>[];
if (originalFieldRelationMetadataCollection.length === 0) {
continue;
}
const fieldComparatorResults =
this.workspaceFieldRelationComparator.compare(
originalFieldRelationMetadataCollection,
standardFieldMetadataCollection,
);
this.storeComparatorResults(fieldComparatorResults, storage);
}
}
private async synchronizeCustomObjectRelationFields(
context: WorkspaceSyncContext,
customObjectMetadataCollection: ObjectMetadataEntity[],
originalObjectMetadataMapByName: Record<string, ObjectMetadataEntity>,
storage: WorkspaceSyncStorage,
): Promise<void> {
// Create standard field metadata collection
const customFieldMetadataRelationCollection =
this.standardFieldRelationFactory.createFieldRelationForCustomObject(
customObjectMetadataCollection.map((objectMetadata) => ({
object: objectMetadata,
metadata: CustomWorkspaceEntity,
})),
context,
originalObjectMetadataMapByName,
);
// Loop over all custom objects from the DB and compare their fields with standard fields
for (const customObjectMetadata of customObjectMetadataCollection) {
/**
* COMPARE FIELD METADATA
*/
const fieldComparatorResults =
this.workspaceFieldRelationComparator.compare(
customObjectMetadata.fields as FieldMetadataEntity<FieldMetadataType.RELATION>[],
customFieldMetadataRelationCollection,
);
this.storeComparatorResults(fieldComparatorResults, storage);
}
}
private async getOriginalObjectMetadataCollection(
context: WorkspaceSyncContext,
manager: EntityManager,
): Promise<ObjectMetadataEntity[]> {
const objectMetadataRepository =
manager.getRepository(ObjectMetadataEntity);
const originalObjectMetadataCollection =
await objectMetadataRepository.find({
where: {
workspaceId: context.workspaceId,
fields: {
type: FieldMetadataType.RELATION,
},
},
relations: ['dataSource', 'fields'],
});
return originalObjectMetadataCollection;
}
private storeComparatorResults(
fieldComparatorResults: FieldRelationComparatorResult[],
storage: WorkspaceSyncStorage,
): void {
for (const fieldComparatorResult of fieldComparatorResults) {
switch (fieldComparatorResult.action) {
case ComparatorAction.CREATE: {
storage.addCreateFieldRelationMetadata(fieldComparatorResult.object);
break;
}
case ComparatorAction.UPDATE: {
storage.addUpdateFieldRelationMetadata(fieldComparatorResult.object);
break;
}
case ComparatorAction.DELETE: {
storage.addDeleteFieldRelationMetadata(fieldComparatorResult.object);
break;
}
}
}
}
}

View File

@ -2,7 +2,6 @@ import { Injectable, Logger } from '@nestjs/common';
import { EntityManager } from 'typeorm';
import { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface';
import { WorkspaceMigrationBuilderAction } from 'src/engine/workspace-manager/workspace-migration-builder/interfaces/workspace-migration-builder-action.interface';
import {
ComparatorAction,
@ -37,7 +36,6 @@ export class WorkspaceSyncFieldMetadataService {
context: WorkspaceSyncContext,
manager: EntityManager,
storage: WorkspaceSyncStorage,
workspaceFeatureFlagsMap: FeatureFlagMap,
): Promise<Partial<WorkspaceMigrationEntity>[]> {
const objectMetadataRepository =
manager.getRepository(ObjectMetadataEntity);
@ -61,14 +59,12 @@ export class WorkspaceSyncFieldMetadataService {
originalObjectMetadataCollection,
customObjectMetadataCollection,
storage,
workspaceFeatureFlagsMap,
);
await this.synchronizeCustomObjectFields(
context,
customObjectMetadataCollection,
storage,
workspaceFeatureFlagsMap,
);
this.logger.log('Updating workspace metadata');
@ -121,14 +117,12 @@ export class WorkspaceSyncFieldMetadataService {
originalObjectMetadataCollection: ObjectMetadataEntity[],
customObjectMetadataCollection: ObjectMetadataEntity[],
storage: WorkspaceSyncStorage,
workspaceFeatureFlagsMap: FeatureFlagMap,
): Promise<void> {
// Create standard field metadata map
const standardObjectStandardFieldMetadataMap =
this.standardFieldFactory.create(
standardObjectMetadataDefinitions,
context,
workspaceFeatureFlagsMap,
);
// Create map of original and standard object metadata by standard ids
@ -145,6 +139,7 @@ export class WorkspaceSyncFieldMetadataService {
originalObjectMetadataMap[standardObjectId];
const computedStandardFieldMetadataCollection = computeStandardFields(
context,
standardFieldMetadataCollection,
originalObjectMetadata,
// We need to provide this for generated relations with custom objects
@ -161,24 +156,46 @@ export class WorkspaceSyncFieldMetadataService {
}
}
synchronizeCustomObject(
context: WorkspaceSyncContext,
customObjectMetadata: ObjectMetadataEntity,
): FieldComparatorResult[] {
// Create standard field metadata collection
const customObjectStandardFieldMetadataCollection =
this.standardFieldFactory.create(CustomWorkspaceEntity, context);
const standardFieldMetadataCollection = computeStandardFields(
context,
customObjectStandardFieldMetadataCollection,
customObjectMetadata,
);
/**
* COMPARE FIELD METADATA
*/
const fieldComparatorResults = this.workspaceFieldComparator.compare(
customObjectMetadata.id,
customObjectMetadata.fields,
standardFieldMetadataCollection,
);
return fieldComparatorResults;
}
private async synchronizeCustomObjectFields(
context: WorkspaceSyncContext,
customObjectMetadataCollection: ObjectMetadataEntity[],
storage: WorkspaceSyncStorage,
workspaceFeatureFlagsMap: FeatureFlagMap,
): Promise<void> {
// Create standard field metadata collection
const customObjectStandardFieldMetadataCollection =
this.standardFieldFactory.create(
CustomWorkspaceEntity,
context,
workspaceFeatureFlagsMap,
);
this.standardFieldFactory.create(CustomWorkspaceEntity, context);
// Loop over all custom objects from the DB and compare their fields with standard fields
for (const customObjectMetadata of customObjectMetadataCollection) {
// Also, maybe it's better to refactor a bit and move generation part into a separate module ?
const standardFieldMetadataCollection = computeStandardFields(
context,
customObjectStandardFieldMetadataCollection,
customObjectMetadata,
);

View File

@ -2,7 +2,6 @@ import { Injectable, Logger } from '@nestjs/common';
import { Any, EntityManager } from 'typeorm';
import { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface';
import { WorkspaceMigrationBuilderAction } from 'src/engine/workspace-manager/workspace-migration-builder/interfaces/workspace-migration-builder-action.interface';
import { ComparatorAction } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/comparator.interface';
import { WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface';
@ -33,7 +32,6 @@ export class WorkspaceSyncIndexMetadataService {
context: WorkspaceSyncContext,
manager: EntityManager,
storage: WorkspaceSyncStorage,
workspaceFeatureFlagsMap: FeatureFlagMap,
): Promise<Partial<WorkspaceMigrationEntity>[]> {
this.logger.log('Syncing index metadata');
@ -89,7 +87,6 @@ export class WorkspaceSyncIndexMetadataService {
context,
originalStandardObjectMetadataMap,
originalCustomObjectMetadataMap,
workspaceFeatureFlagsMap,
);
const indexComparatorResults = this.workspaceIndexComparator.compare(

View File

@ -3,7 +3,6 @@ import { Injectable } from '@nestjs/common';
import { EntityManager, Repository } from 'typeorm';
import { FieldMetadataType } from 'twenty-shared/types';
import { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface';
import { WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
@ -21,7 +20,6 @@ export class WorkspaceSyncObjectMetadataIdentifiersService {
context: WorkspaceSyncContext,
manager: EntityManager,
_storage: WorkspaceSyncStorage,
workspaceFeatureFlagsMap: FeatureFlagMap,
): Promise<void> {
const objectMetadataRepository =
manager.getRepository(ObjectMetadataEntity);
@ -32,10 +30,8 @@ export class WorkspaceSyncObjectMetadataIdentifiersService {
objectMetadataRepository,
);
const standardObjectMetadataMap = this.createStandardObjectMetadataMap(
context,
workspaceFeatureFlagsMap,
);
const standardObjectMetadataMap =
this.createStandardObjectMetadataMap(context);
await this.processObjectMetadataCollection(
originalObjectMetadataCollection,
@ -56,12 +52,10 @@ export class WorkspaceSyncObjectMetadataIdentifiersService {
private createStandardObjectMetadataMap(
context: WorkspaceSyncContext,
workspaceFeatureFlagsMap: FeatureFlagMap,
): Record<string, any> {
const standardObjectMetadataCollection = this.standardObjectFactory.create(
standardObjectMetadataDefinitions,
context,
workspaceFeatureFlagsMap,
);
return mapObjectMetadataByUniqueIdentifier(

View File

@ -2,7 +2,6 @@ import { Injectable, Logger } from '@nestjs/common';
import { EntityManager } from 'typeorm';
import { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface';
import { WorkspaceMigrationBuilderAction } from 'src/engine/workspace-manager/workspace-migration-builder/interfaces/workspace-migration-builder-action.interface';
import { ComparatorAction } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/comparator.interface';
import { WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface';
@ -33,7 +32,6 @@ export class WorkspaceSyncObjectMetadataService {
context: WorkspaceSyncContext,
manager: EntityManager,
storage: WorkspaceSyncStorage,
workspaceFeatureFlagsMap: FeatureFlagMap,
): Promise<Partial<WorkspaceMigrationEntity>[]> {
const objectMetadataRepository =
manager.getRepository(ObjectMetadataEntity);
@ -83,7 +81,6 @@ export class WorkspaceSyncObjectMetadataService {
const standardObjectMetadataCollection = this.standardObjectFactory.create(
standardObjectMetadataDefinitions,
context,
workspaceFeatureFlagsMap,
);
// Create map of original and standard object metadata by standard ids

View File

@ -2,7 +2,6 @@ import { Injectable } from '@nestjs/common';
import { EntityManager } from 'typeorm';
import { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface';
import { WorkspaceMigrationBuilderAction } from 'src/engine/workspace-manager/workspace-migration-builder/interfaces/workspace-migration-builder-action.interface';
import { ComparatorAction } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/comparator.interface';
import { WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface';
@ -32,7 +31,6 @@ export class WorkspaceSyncRelationMetadataService {
context: WorkspaceSyncContext,
manager: EntityManager,
storage: WorkspaceSyncStorage,
workspaceFeatureFlagsMap: FeatureFlagMap,
): Promise<Partial<WorkspaceMigrationEntity>[]> {
const objectMetadataRepository =
manager.getRepository(ObjectMetadataEntity);
@ -76,7 +74,6 @@ export class WorkspaceSyncRelationMetadataService {
standardObjectMetadataDefinitions,
context,
originalObjectMetadataMap,
workspaceFeatureFlagsMap,
);
const customRelationMetadataCollection =
@ -87,7 +84,6 @@ export class WorkspaceSyncRelationMetadataService {
})),
context,
originalObjectMetadataMap,
workspaceFeatureFlagsMap,
);
const relationComparatorResults = this.workspaceRelationComparator.compare(

View File

@ -1,3 +1,5 @@
import { FieldMetadataType } from 'twenty-shared/types';
import { ComputedPartialFieldMetadata } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/partial-field-metadata.interface';
import { ComputedPartialWorkspaceEntity } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/partial-object-metadata.interface';
@ -27,6 +29,20 @@ export class WorkspaceSyncStorage {
})[] = [];
private readonly _fieldMetadataDeleteCollection: FieldMetadataEntity[] = [];
// Field relation metadata
private readonly _fieldRelationMetadataCreateCollection: (Partial<
ComputedPartialFieldMetadata<FieldMetadataType.RELATION>
> & {
id: string;
})[] = [];
private readonly _fieldRelationMetadataUpdateCollection: (Partial<
ComputedPartialFieldMetadata<FieldMetadataType.RELATION>
> & {
id: string;
})[] = [];
private readonly _fieldRelationMetadataDeleteCollection: FieldMetadataEntity<FieldMetadataType.RELATION>[] =
[];
// Relation metadata
private readonly _relationMetadataCreateCollection: Partial<RelationMetadataEntity>[] =
[];
@ -68,6 +84,18 @@ export class WorkspaceSyncStorage {
return this._fieldMetadataDeleteCollection;
}
get fieldRelationMetadataCreateCollection() {
return this._fieldRelationMetadataCreateCollection;
}
get fieldRelationMetadataUpdateCollection() {
return this._fieldRelationMetadataUpdateCollection;
}
get fieldRelationMetadataDeleteCollection() {
return this._fieldRelationMetadataDeleteCollection;
}
get relationMetadataCreateCollection() {
return this._relationMetadataCreateCollection;
}
@ -118,6 +146,28 @@ export class WorkspaceSyncStorage {
this._fieldMetadataDeleteCollection.push(field);
}
addCreateFieldRelationMetadata(
field: Partial<ComputedPartialFieldMetadata<FieldMetadataType.RELATION>> & {
id: string;
},
) {
this._fieldRelationMetadataCreateCollection.push(field);
}
addUpdateFieldRelationMetadata(
field: Partial<ComputedPartialFieldMetadata<FieldMetadataType.RELATION>> & {
id: string;
},
) {
this._fieldRelationMetadataUpdateCollection.push(field);
}
addDeleteFieldRelationMetadata(
field: FieldMetadataEntity<FieldMetadataType.RELATION>,
) {
this._fieldRelationMetadataDeleteCollection.push(field);
}
addCreateRelationMetadata(relation: Partial<RelationMetadataEntity>) {
this._relationMetadataCreateCollection.push(relation);
}

View File

@ -5,7 +5,9 @@ import {
PartialComputedFieldMetadata,
PartialFieldMetadata,
} from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/partial-field-metadata.interface';
import { WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import {
createForeignKeyDeterministicUuid,
@ -13,6 +15,7 @@ import {
} from 'src/engine/workspace-manager/workspace-sync-metadata/utils/create-deterministic-uuid.util';
export const computeStandardFields = (
context: WorkspaceSyncContext,
standardFieldMetadataCollection: (
| PartialFieldMetadata
| PartialComputedFieldMetadata
@ -22,6 +25,9 @@ export const computeStandardFields = (
): ComputedPartialFieldMetadata[] => {
const fields: ComputedPartialFieldMetadata[] = [];
const isNewRelationEnabled =
context.featureFlags[FeatureFlagKey.IsNewRelationEnabled];
for (const partialFieldMetadata of standardFieldMetadataCollection) {
// Relation from standard object to custom object
if ('argsFactory' in partialFieldMetadata) {
@ -52,18 +58,22 @@ export const computeStandardFields = (
defaultValue: null,
});
// Foreign key
fields.push({
...rest,
standardId: foreignKeyStandardId,
name: joinColumn,
type: FieldMetadataType.UUID,
label: `${data.label} ID (foreign key)`,
description: `${data.description} id foreign key`,
defaultValue: null,
icon: undefined,
isSystem: true,
});
// Only add foreign key if new relation is disabled
// As new relation will no longer create the field metadata related to foreign key
if (!isNewRelationEnabled) {
// Foreign key
fields.push({
...rest,
standardId: foreignKeyStandardId,
name: joinColumn,
type: FieldMetadataType.UUID,
label: `${data.label} ID (foreign key)`,
description: `${data.description} id foreign key`,
defaultValue: null,
icon: undefined,
isSystem: true,
});
}
}
} else {
// Relation from standard object to standard object

View File

@ -17,6 +17,7 @@ import { SyncWorkspaceMetadataCommand } from 'src/engine/workspace-manager/works
import { workspaceSyncMetadataComparators } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators';
import { workspaceSyncMetadataFactories } from 'src/engine/workspace-manager/workspace-sync-metadata/factories';
import { WorkspaceMetadataUpdaterService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-metadata-updater.service';
import { WorkspaceSyncFieldMetadataRelationService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-field-metadata-relation.service';
import { WorkspaceSyncFieldMetadataService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-field-metadata.service';
import { WorkspaceSyncIndexMetadataService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-index-metadata.service';
import { WorkspaceSyncObjectMetadataIdentifiersService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-object-metadata-identifiers.service';
@ -50,6 +51,7 @@ import { WorkspaceSyncMetadataService } from 'src/engine/workspace-manager/works
WorkspaceSyncObjectMetadataIdentifiersService,
WorkspaceSyncRelationMetadataService,
WorkspaceSyncFieldMetadataService,
WorkspaceSyncFieldMetadataRelationService,
WorkspaceSyncMetadataService,
WorkspaceSyncIndexMetadataService,
SyncWorkspaceLoggerService,

View File

@ -5,6 +5,7 @@ import { DataSource, QueryFailedError } from 'typeorm';
import { WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service';
import {
@ -12,6 +13,7 @@ import {
WorkspaceMigrationTableActionType,
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service';
import { WorkspaceSyncFieldMetadataRelationService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-field-metadata-relation.service';
import { WorkspaceSyncFieldMetadataService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-field-metadata.service';
import { WorkspaceSyncIndexMetadataService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-index-metadata.service';
import { WorkspaceSyncObjectMetadataIdentifiersService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-object-metadata-identifiers.service';
@ -30,14 +32,15 @@ export class WorkspaceSyncMetadataService {
constructor(
@InjectDataSource('metadata')
private readonly metadataDataSource: DataSource,
private readonly featureFlagService: FeatureFlagService,
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
private readonly workspaceSyncObjectMetadataService: WorkspaceSyncObjectMetadataService,
private readonly workspaceSyncRelationMetadataService: WorkspaceSyncRelationMetadataService,
private readonly workspaceSyncFieldMetadataService: WorkspaceSyncFieldMetadataService,
private readonly workspaceSyncFieldMetadataRelationService: WorkspaceSyncFieldMetadataRelationService,
private readonly workspaceSyncIndexMetadataService: WorkspaceSyncIndexMetadataService,
private readonly workspaceSyncObjectMetadataIdentifiersService: WorkspaceSyncObjectMetadataIdentifiersService,
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
private readonly featureFlagService: FeatureFlagService,
) {}
/**
@ -67,16 +70,16 @@ export class WorkspaceSyncMetadataService {
const manager = queryRunner.manager;
try {
const isNewRelationEnabled =
await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IsNewRelationEnabled,
context.workspaceId,
);
const workspaceMigrationRepository = manager.getRepository(
WorkspaceMigrationEntity,
);
// Retrieve feature flags
const workspaceFeatureFlagsMap =
await this.featureFlagService.getWorkspaceFeatureFlagsMap(
context.workspaceId,
);
this.logger.log('Syncing standard objects and fields metadata');
// 1 - Sync standard objects
@ -87,7 +90,6 @@ export class WorkspaceSyncMetadataService {
context,
manager,
storage,
workspaceFeatureFlagsMap,
);
const workspaceObjectMigrationsEnd = performance.now();
@ -103,7 +105,6 @@ export class WorkspaceSyncMetadataService {
context,
manager,
storage,
workspaceFeatureFlagsMap,
);
const workspaceFieldMigrationsEnd = performance.now();
@ -123,13 +124,24 @@ export class WorkspaceSyncMetadataService {
// 3 - Sync standard relations on standard and custom objects
const workspaceRelationMigrationsStart = performance.now();
const workspaceRelationMigrations =
await this.workspaceSyncRelationMetadataService.synchronize(
context,
manager,
storage,
workspaceFeatureFlagsMap,
);
let workspaceRelationMigrations: Partial<WorkspaceMigrationEntity>[] = [];
if (isNewRelationEnabled) {
workspaceRelationMigrations =
await this.workspaceSyncFieldMetadataRelationService.synchronize(
context,
manager,
storage,
);
} else {
workspaceRelationMigrations =
await this.workspaceSyncRelationMetadataService.synchronize(
context,
manager,
storage,
);
}
const workspaceRelationMigrationsEnd = performance.now();
@ -144,7 +156,6 @@ export class WorkspaceSyncMetadataService {
context,
manager,
storage,
workspaceFeatureFlagsMap,
);
const workspaceIndexMigrationsEnd = performance.now();
@ -160,7 +171,6 @@ export class WorkspaceSyncMetadataService {
context,
manager,
storage,
workspaceFeatureFlagsMap,
);
const workspaceObjectMetadataIdentifiersEnd = performance.now();