TimelineActivity migration to morph (#15652)
# TimelineActivity migration to morph - Creates `timelineActivities2` relations on Company, Dashboard, Note, Opportunity, Person, Task, Workflow, WorkflowRun, and WorkflowVersion entities with proper metadata and cascade delete behavior. It was required to create standard fields as well since the mapObjectMetadataByUniqueIdentifier needs it. otherwise the fields won't be considered - Feature Flag `IS_TIMELINE_ACTIVITY_MIGRATED` necessary to have the two states in parallel. It is used as a stamp once the migration has been run - Migration is done using the coreDataSource. Why ? even though is unsafe to use, the first implementation of the migration took forever on each workspace. See [this commit](https://github.com/twentyhq/twenty/pull/15652/commits/477011e8d7d4c580f79ba7ec4a8fb002a3ec86b2) The plan for this complex migration is as follows :  Note: we will need to rename fields in the release 1.12 (there is no easy way to do all this in one release)
This commit is contained in:
@@ -1307,6 +1307,7 @@ export enum FeatureFlagKey {
|
||||
IS_PUBLIC_DOMAIN_ENABLED = 'IS_PUBLIC_DOMAIN_ENABLED',
|
||||
IS_RECORD_PAGE_LAYOUT_ENABLED = 'IS_RECORD_PAGE_LAYOUT_ENABLED',
|
||||
IS_STRIPE_INTEGRATION_ENABLED = 'IS_STRIPE_INTEGRATION_ENABLED',
|
||||
IS_TIMELINE_ACTIVITY_MIGRATED = 'IS_TIMELINE_ACTIVITY_MIGRATED',
|
||||
IS_UNIQUE_INDEXES_ENABLED = 'IS_UNIQUE_INDEXES_ENABLED',
|
||||
IS_WORKFLOW_RUN_STOPPAGE_ENABLED = 'IS_WORKFLOW_RUN_STOPPAGE_ENABLED'
|
||||
}
|
||||
|
||||
@@ -1290,6 +1290,7 @@ export enum FeatureFlagKey {
|
||||
IS_PUBLIC_DOMAIN_ENABLED = 'IS_PUBLIC_DOMAIN_ENABLED',
|
||||
IS_RECORD_PAGE_LAYOUT_ENABLED = 'IS_RECORD_PAGE_LAYOUT_ENABLED',
|
||||
IS_STRIPE_INTEGRATION_ENABLED = 'IS_STRIPE_INTEGRATION_ENABLED',
|
||||
IS_TIMELINE_ACTIVITY_MIGRATED = 'IS_TIMELINE_ACTIVITY_MIGRATED',
|
||||
IS_UNIQUE_INDEXES_ENABLED = 'IS_UNIQUE_INDEXES_ENABLED',
|
||||
IS_WORKFLOW_RUN_STOPPAGE_ENABLED = 'IS_WORKFLOW_RUN_STOPPAGE_ENABLED'
|
||||
}
|
||||
|
||||
+12
-4
@@ -5,15 +5,23 @@ import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getActivi
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useGenerateDepthRecordGqlFieldsFromObject } from '@/object-record/graphql/record-gql-fields/hooks/useGenerateDepthRecordGqlFieldsFromObject';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { capitalize, isDefined } from 'twenty-shared/utils';
|
||||
import { FeatureFlagKey } from '~/generated/graphql';
|
||||
|
||||
// do we need to test this?
|
||||
export const useTimelineActivities = (
|
||||
targetableObject: ActivityTargetableObject,
|
||||
) => {
|
||||
const targetableObjectFieldIdName = getActivityTargetObjectFieldIdName({
|
||||
nameSingular: targetableObject.targetObjectNameSingular,
|
||||
});
|
||||
const isTimelineActivityMigrated = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_TIMELINE_ACTIVITY_MIGRATED,
|
||||
);
|
||||
|
||||
const targetableObjectFieldIdName = isTimelineActivityMigrated
|
||||
? `target${capitalize(targetableObject.targetObjectNameSingular)}Id`
|
||||
: getActivityTargetObjectFieldIdName({
|
||||
nameSingular: targetableObject.targetObjectNameSingular,
|
||||
});
|
||||
|
||||
const { recordGqlFields: depthOneRecordGqlFields } =
|
||||
useGenerateDepthRecordGqlFieldsFromObject({
|
||||
|
||||
+267
@@ -0,0 +1,267 @@
|
||||
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { capitalize } from 'twenty-shared/utils';
|
||||
import { DataSource, Repository } from 'typeorm';
|
||||
|
||||
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
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 { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util';
|
||||
import { TIMELINE_ACTIVITY_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
||||
import { buildTimelineActivityRelatedMorphFieldMetadataName } from 'src/modules/timeline/utils/timeline-activity-related-morph-field-metadata-name-builder.util';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-13:migrate-timeline-activity-to-morph-relations',
|
||||
description:
|
||||
'Migrate timeline activity relations to morph relation fields and set feature flag',
|
||||
})
|
||||
export class MigrateTimelineActivityToMorphRelationsCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
@InjectRepository(ObjectMetadataEntity)
|
||||
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
||||
@InjectDataSource()
|
||||
private readonly coreDataSource: DataSource,
|
||||
private readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
|
||||
protected readonly dataSourceService: DataSourceService,
|
||||
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
const isMigrated = await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_TIMELINE_ACTIVITY_MIGRATED,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
this.logger.log(`Migrating timelineActivity for workspace ${workspaceId}`);
|
||||
|
||||
if (isMigrated) {
|
||||
this.logger.log(
|
||||
`Timeline activity migration already completed. Skipping...`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.dryRun) {
|
||||
this.logger.log(
|
||||
`Would have migrated timeline activities for workspace ${workspaceId}. Skipping...`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const queryRunner = this.coreDataSource.createQueryRunner();
|
||||
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
try {
|
||||
const schemaName = getWorkspaceSchemaName(workspaceId);
|
||||
const tableName = 'timelineActivity';
|
||||
|
||||
const customObjectMetadata = await this.objectMetadataRepository.find({
|
||||
where: {
|
||||
workspaceId,
|
||||
isCustom: true,
|
||||
},
|
||||
});
|
||||
const customObjectMetadataNames = customObjectMetadata.map(
|
||||
(objectMetadata) => objectMetadata.nameSingular,
|
||||
);
|
||||
|
||||
const fieldNameMigrations = [
|
||||
{ old: 'company', new: 'targetCompany' },
|
||||
{ old: 'person', new: 'targetPerson' },
|
||||
{ old: 'opportunity', new: 'targetOpportunity' },
|
||||
{ old: 'note', new: 'targetNote' },
|
||||
{ old: 'task', new: 'targetTask' },
|
||||
{ old: 'workflow', new: 'targetWorkflow' },
|
||||
{ old: 'workflowVersion', new: 'targetWorkflowVersion' },
|
||||
{ old: 'workflowRun', new: 'targetWorkflowRun' },
|
||||
{ old: 'dashboard', new: 'targetDashboard' },
|
||||
...customObjectMetadataNames.map((customObjectName) => ({
|
||||
old: `${customObjectName}`,
|
||||
new: `target${capitalize(customObjectName)}`,
|
||||
})),
|
||||
];
|
||||
|
||||
const fieldMigrations = fieldNameMigrations.map(
|
||||
({ old: oldFieldName, new: newFieldName }) => ({
|
||||
old: `${oldFieldName}Id`,
|
||||
new: `${newFieldName}Id`,
|
||||
}),
|
||||
);
|
||||
|
||||
const timelineActivityObjectMetadata =
|
||||
await this.objectMetadataRepository.findOne({
|
||||
where: {
|
||||
workspaceId,
|
||||
nameSingular: 'timelineActivity',
|
||||
},
|
||||
relations: ['fields'],
|
||||
});
|
||||
|
||||
if (!timelineActivityObjectMetadata) {
|
||||
this.logger.error(
|
||||
`🟥 Timeline activity object metadata not found for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
for (const { new: newField, old: oldField } of fieldMigrations) {
|
||||
try {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "${schemaName}"."${tableName}"
|
||||
RENAME COLUMN "${oldField}" TO "${newField}"`,
|
||||
);
|
||||
this.logger.log(
|
||||
`Renamed column "${oldField}" to "${newField}" for "${tableName}"`,
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Error renaming column "${oldField}" to "${newField}" for "${tableName}" in workspace ${workspaceId}`,
|
||||
error,
|
||||
);
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(`✅ Successfully migrated timeline activity records`);
|
||||
|
||||
const objectNamesToMigrate = fieldNameMigrations.map(
|
||||
({ old: oldFieldName }) => oldFieldName,
|
||||
);
|
||||
|
||||
const relatedObjectMetadata = await this.objectMetadataRepository.find({
|
||||
where: {
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
const relatedObjectMetadataMap = new Map(
|
||||
relatedObjectMetadata.map((obj) => [obj.nameSingular, obj]),
|
||||
);
|
||||
|
||||
const morphId = TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.targetMorphId;
|
||||
|
||||
for (const objectName of objectNamesToMigrate) {
|
||||
const relatedObject = relatedObjectMetadataMap.get(objectName);
|
||||
|
||||
if (!relatedObject) {
|
||||
this.logger.warn(
|
||||
`Related object metadata not found for ${objectName} in workspace ${workspaceId}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const fieldToMigrate = timelineActivityObjectMetadata.fields.find(
|
||||
(field) =>
|
||||
field.type === 'RELATION' &&
|
||||
field.relationTargetObjectMetadataId === relatedObject.id,
|
||||
);
|
||||
|
||||
if (!fieldToMigrate) {
|
||||
this.logger.log(
|
||||
`No RELATION field found for ${objectName} in timelineActivity`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const newFieldName = buildTimelineActivityRelatedMorphFieldMetadataName(
|
||||
fieldToMigrate.name,
|
||||
);
|
||||
|
||||
const settings = {
|
||||
...fieldToMigrate.settings,
|
||||
joinColumnName: computeMorphOrRelationFieldJoinColumnName({
|
||||
name: newFieldName,
|
||||
}),
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await queryRunner.query(
|
||||
`UPDATE core."fieldMetadata"
|
||||
SET name = $1, type = 'MORPH_RELATION', "morphId" = $3, settings = $4
|
||||
WHERE id = $2`,
|
||||
[newFieldName, fieldToMigrate.id, morphId, settings],
|
||||
);
|
||||
|
||||
const rowsUpdated = result[1] || 0;
|
||||
|
||||
if (rowsUpdated > 0) {
|
||||
this.logger.log(
|
||||
`Updated fieldMetadata: ${fieldToMigrate.name} → ${newFieldName} (type: MORPH_RELATION)`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Error updating fieldMetadata for ${objectName} in workspace ${workspaceId}`,
|
||||
error,
|
||||
);
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`✅ Successfully migrated timeline activity fieldmetadata`,
|
||||
);
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
|
||||
await this.featureFlagService.enableFeatureFlags(
|
||||
[FeatureFlagKey.IS_TIMELINE_ACTIVITY_MIGRATED],
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [
|
||||
'flatFieldMetadataMaps',
|
||||
'flatObjectMetadataMaps',
|
||||
'featureFlagsMap',
|
||||
]);
|
||||
|
||||
await this.workspaceMetadataVersionService.incrementMetadataVersion(
|
||||
workspaceId,
|
||||
);
|
||||
this.logger.log(`Cache flushed`);
|
||||
|
||||
this.logger.log(
|
||||
`Set IS_TIMELINE_ACTIVITY_MIGRATED feature flag for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
this.logger.log(`Flush cache for workspace ${workspaceId}`);
|
||||
await this.workspaceCacheStorageService.flush(workspaceId);
|
||||
} catch (error) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
this.logger.error(
|
||||
`Error migrating timeline activity to morph relations (rolled transaction back on ${workspaceId})`,
|
||||
error,
|
||||
);
|
||||
throw error;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
+16
-1
@@ -6,23 +6,30 @@ import { BackfillViewMainGroupByFieldMetadataIdCommand } from 'src/database/comm
|
||||
import { CleanEmptyStringNullInTextFieldsCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-clean-empty-string-null-in-text-fields.command';
|
||||
import { DeduplicateRoleTargetsCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-deduplicate-role-targets.command';
|
||||
import { MigrateStandardInvalidEntitiesCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-migrate-standard-invalid-entities.command';
|
||||
import { MigrateTimelineActivityToMorphRelationsCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-migrate-timeline-activity-to-morph-relations.command';
|
||||
import { RenameIndexNameCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-rename-unique-index.command';
|
||||
import { UpdateRoleTargetsUniqueConstraintMigrationCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-update-role-targets-unique-constraint-migration.command';
|
||||
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
|
||||
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/field-metadata.module';
|
||||
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
|
||||
import { PageLayoutTabEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout-tab.entity';
|
||||
import { PageLayoutWidgetEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout-widget.entity';
|
||||
import { PageLayoutEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout.entity';
|
||||
import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity';
|
||||
import { ViewGroupEntity } from 'src/engine/metadata-modules/view-group/entities/view-group.entity';
|
||||
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
|
||||
import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module';
|
||||
import { WorkspaceSchemaManagerModule } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.module';
|
||||
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
|
||||
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
|
||||
import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -38,11 +45,17 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache
|
||||
PageLayoutEntity,
|
||||
PageLayoutWidgetEntity,
|
||||
PageLayoutTabEntity,
|
||||
TimelineActivityWorkspaceEntity,
|
||||
]),
|
||||
DataSourceModule,
|
||||
ApplicationModule,
|
||||
WorkspaceSchemaManagerModule,
|
||||
WorkspaceCacheStorageModule,
|
||||
WorkspaceMetadataVersionModule,
|
||||
FeatureFlagModule,
|
||||
WorkspaceCacheModule,
|
||||
FieldMetadataModule,
|
||||
ObjectMetadataModule,
|
||||
ApplicationModule,
|
||||
],
|
||||
providers: [
|
||||
CleanEmptyStringNullInTextFieldsCommand,
|
||||
@@ -52,6 +65,7 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache
|
||||
RenameIndexNameCommand,
|
||||
MigrateStandardInvalidEntitiesCommand,
|
||||
UpdateRoleTargetsUniqueConstraintMigrationCommand,
|
||||
MigrateTimelineActivityToMorphRelationsCommand,
|
||||
],
|
||||
exports: [
|
||||
CleanEmptyStringNullInTextFieldsCommand,
|
||||
@@ -61,6 +75,7 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache
|
||||
RenameIndexNameCommand,
|
||||
MigrateStandardInvalidEntitiesCommand,
|
||||
UpdateRoleTargetsUniqueConstraintMigrationCommand,
|
||||
MigrateTimelineActivityToMorphRelationsCommand,
|
||||
],
|
||||
})
|
||||
export class V1_13_UpgradeVersionCommandModule {}
|
||||
|
||||
+3
@@ -30,6 +30,7 @@ import { WorkspaceCustomApplicationIdNonNullableCommand } from 'src/database/com
|
||||
import { BackfillPageLayoutUniversalIdentifiersCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-backfill-page-layout-universal-identifiers.command';
|
||||
import { DeduplicateRoleTargetsCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-deduplicate-role-targets.command';
|
||||
import { MigrateStandardInvalidEntitiesCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-migrate-standard-invalid-entities.command';
|
||||
import { MigrateTimelineActivityToMorphRelationsCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-migrate-timeline-activity-to-morph-relations.command';
|
||||
import { UpdateRoleTargetsUniqueConstraintMigrationCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-update-role-targets-unique-constraint-migration.command';
|
||||
import { FixLabelIdentifierPositionAndVisibilityCommand } from 'src/database/commands/upgrade-version-command/1-6/1-6-fix-label-identifier-position-and-visibility.command';
|
||||
import { BackfillWorkflowManualTriggerAvailabilityCommand } from 'src/database/commands/upgrade-version-command/1-7/1-7-backfill-workflow-manual-trigger-availability.command';
|
||||
@@ -98,6 +99,7 @@ export class UpgradeCommand extends UpgradeCommandRunner {
|
||||
protected readonly cleanNullEquivalentValuesCommand: CleanNullEquivalentValuesCommand,
|
||||
|
||||
// 1.13 Commands
|
||||
protected readonly migrateTimelineActivityToMorphRelationsCommand: MigrateTimelineActivityToMorphRelationsCommand,
|
||||
protected readonly deduplicateRoleTargetsCommand: DeduplicateRoleTargetsCommand,
|
||||
protected readonly updateRoleTargetsUniqueConstraintMigrationCommand: UpdateRoleTargetsUniqueConstraintMigrationCommand,
|
||||
protected readonly backfillPageLayoutUniversalIdentifiersCommand: BackfillPageLayoutUniversalIdentifiersCommand,
|
||||
@@ -173,6 +175,7 @@ export class UpgradeCommand extends UpgradeCommandRunner {
|
||||
beforeSyncMetadata: [
|
||||
this.deduplicateRoleTargetsCommand,
|
||||
this.updateRoleTargetsUniqueConstraintMigrationCommand,
|
||||
this.migrateTimelineActivityToMorphRelationsCommand,
|
||||
this.backfillPageLayoutUniversalIdentifiersCommand,
|
||||
this.migrateStandardInvalidEntitiesCommand,
|
||||
],
|
||||
|
||||
+1
@@ -13,5 +13,6 @@ export enum FeatureFlagKey {
|
||||
IS_EMAILING_DOMAIN_ENABLED = 'IS_EMAILING_DOMAIN_ENABLED',
|
||||
IS_WORKFLOW_RUN_STOPPAGE_ENABLED = 'IS_WORKFLOW_RUN_STOPPAGE_ENABLED',
|
||||
IS_DASHBOARD_V2_ENABLED = 'IS_DASHBOARD_V2_ENABLED',
|
||||
IS_TIMELINE_ACTIVITY_MIGRATED = 'IS_TIMELINE_ACTIVITY_MIGRATED',
|
||||
IS_GLOBAL_WORKSPACE_DATASOURCE_ENABLED = 'IS_GLOBAL_WORKSPACE_DATASOURCE_ENABLED',
|
||||
}
|
||||
|
||||
+6
-6
@@ -129,7 +129,7 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su
|
||||
"isNullable": true,
|
||||
"isSystem": false,
|
||||
"isUIReadOnly": false,
|
||||
"isUnique": null,
|
||||
"isUnique": false,
|
||||
"kanbanAggregateOperationViewIds": [],
|
||||
"label": "newFieldLabel",
|
||||
"mainGroupByFieldMetadataViewIds": [],
|
||||
@@ -156,7 +156,7 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su
|
||||
"calendarViewIds": [],
|
||||
"createdAt": Any<String>,
|
||||
"defaultValue": null,
|
||||
"description": null,
|
||||
"description": "new field description",
|
||||
"icon": "IconPet",
|
||||
"id": Any<String>,
|
||||
"isActive": true,
|
||||
@@ -165,7 +165,7 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su
|
||||
"isNullable": true,
|
||||
"isSystem": false,
|
||||
"isUIReadOnly": false,
|
||||
"isUnique": null,
|
||||
"isUnique": false,
|
||||
"kanbanAggregateOperationViewIds": [],
|
||||
"label": "Pet",
|
||||
"mainGroupByFieldMetadataViewIds": [],
|
||||
@@ -203,7 +203,7 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su
|
||||
"isNullable": true,
|
||||
"isSystem": false,
|
||||
"isUIReadOnly": false,
|
||||
"isUnique": null,
|
||||
"isUnique": false,
|
||||
"kanbanAggregateOperationViewIds": [],
|
||||
"label": "newFieldLabel",
|
||||
"mainGroupByFieldMetadataViewIds": [],
|
||||
@@ -230,7 +230,7 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su
|
||||
"calendarViewIds": [],
|
||||
"createdAt": Any<String>,
|
||||
"defaultValue": null,
|
||||
"description": null,
|
||||
"description": "new field description",
|
||||
"icon": "IconBuilding",
|
||||
"id": Any<String>,
|
||||
"isActive": true,
|
||||
@@ -239,7 +239,7 @@ exports[`fromCreateFieldInputToFlatFieldMetadatasToCreate MORPH_RELATION test su
|
||||
"isNullable": true,
|
||||
"isSystem": false,
|
||||
"isUIReadOnly": false,
|
||||
"isUnique": null,
|
||||
"isUnique": false,
|
||||
"kanbanAggregateOperationViewIds": [],
|
||||
"label": "Company",
|
||||
"mainGroupByFieldMetadataViewIds": [],
|
||||
|
||||
+330
@@ -0,0 +1,330 @@
|
||||
import { type EachTestingContext } from 'twenty-shared/testing';
|
||||
import {
|
||||
type FieldMetadataRelationSettings,
|
||||
FieldMetadataType,
|
||||
RelationOnDeleteAction,
|
||||
RelationType,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
import {
|
||||
generateMorphOrRelationFlatFieldMetadataPair,
|
||||
type SourceTargetMorphOrRelationFlatFieldAndFlatIndex,
|
||||
} from 'src/engine/metadata-modules/flat-field-metadata/utils/generate-morph-or-relation-flat-field-metadata-pair.util';
|
||||
import { COMPANY_FLAT_OBJECT_MOCK } from 'src/engine/metadata-modules/flat-object-metadata/__mocks__/company-flat-object.mock';
|
||||
import { PET_FLAT_OBJECT_MOCK } from 'src/engine/metadata-modules/flat-object-metadata/__mocks__/pet-flat-object.mock';
|
||||
|
||||
type GenerateMorphOrRelationFlatFieldMetadataPairTestInput = Parameters<
|
||||
typeof generateMorphOrRelationFlatFieldMetadataPair
|
||||
>[0];
|
||||
|
||||
type TestCase = EachTestingContext<{
|
||||
input: GenerateMorphOrRelationFlatFieldMetadataPairTestInput;
|
||||
expectedSourceFieldType: FieldMetadataType;
|
||||
expectedTargetFieldType: FieldMetadataType;
|
||||
expectedSourceRelationType: RelationType;
|
||||
expectedTargetRelationType: RelationType;
|
||||
shouldSourceHaveMorphId: boolean;
|
||||
}>;
|
||||
|
||||
describe('generate Morph Or Relation Flat Field Metadata Pair test suite', () => {
|
||||
const mockWorkspaceId = '20202020-1c25-4d02-bf25-6aeccf7ea419';
|
||||
const mockWorkspaceCustomApplicationId =
|
||||
'20202020-81ee-42da-a281-668632f32fe7';
|
||||
|
||||
describe('Success cases', () => {
|
||||
const testCases: TestCase[] = [
|
||||
{
|
||||
title:
|
||||
'should generate a regular RELATION field pair with ONE_TO_MANY relation type',
|
||||
context: {
|
||||
input: {
|
||||
sourceFlatObjectMetadata: COMPANY_FLAT_OBJECT_MOCK,
|
||||
targetFlatObjectMetadata: PET_FLAT_OBJECT_MOCK,
|
||||
targetFlatFieldMetadataType: FieldMetadataType.RELATION,
|
||||
sourceFlatObjectMetadataJoinColumnName: 'petId',
|
||||
workspaceId: mockWorkspaceId,
|
||||
workspaceCustomApplicationId: mockWorkspaceCustomApplicationId,
|
||||
createFieldInput: {
|
||||
name: 'pets',
|
||||
label: 'Pets',
|
||||
description: 'Company pets',
|
||||
icon: 'IconCat',
|
||||
type: FieldMetadataType.RELATION,
|
||||
objectMetadataId: COMPANY_FLAT_OBJECT_MOCK.id,
|
||||
isCustom: true,
|
||||
isSystem: false,
|
||||
isUnique: false,
|
||||
relationCreationPayload: {
|
||||
type: RelationType.ONE_TO_MANY,
|
||||
targetObjectMetadataId: PET_FLAT_OBJECT_MOCK.id,
|
||||
targetFieldLabel: 'Company',
|
||||
targetFieldIcon: 'IconBuildingSkyscraper',
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedSourceFieldType: FieldMetadataType.RELATION,
|
||||
expectedTargetFieldType: FieldMetadataType.RELATION,
|
||||
expectedSourceRelationType: RelationType.ONE_TO_MANY,
|
||||
expectedTargetRelationType: RelationType.MANY_TO_ONE,
|
||||
shouldSourceHaveMorphId: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'should generate a regular RELATION field pair with MANY_TO_ONE relation type',
|
||||
context: {
|
||||
input: {
|
||||
sourceFlatObjectMetadata: COMPANY_FLAT_OBJECT_MOCK,
|
||||
targetFlatObjectMetadata: PET_FLAT_OBJECT_MOCK,
|
||||
targetFlatFieldMetadataType: FieldMetadataType.RELATION,
|
||||
sourceFlatObjectMetadataJoinColumnName: 'petId',
|
||||
workspaceId: mockWorkspaceId,
|
||||
workspaceCustomApplicationId: mockWorkspaceCustomApplicationId,
|
||||
createFieldInput: {
|
||||
name: 'pets',
|
||||
label: 'Pets',
|
||||
description: 'Company pets',
|
||||
icon: 'IconCat',
|
||||
type: FieldMetadataType.RELATION,
|
||||
objectMetadataId: COMPANY_FLAT_OBJECT_MOCK.id,
|
||||
isCustom: true,
|
||||
isSystem: false,
|
||||
isUnique: false,
|
||||
relationCreationPayload: {
|
||||
type: RelationType.MANY_TO_ONE,
|
||||
targetObjectMetadataId: PET_FLAT_OBJECT_MOCK.id,
|
||||
targetFieldLabel: 'Company',
|
||||
targetFieldIcon: 'IconBuildingSkyscraper',
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedSourceFieldType: FieldMetadataType.RELATION,
|
||||
expectedTargetFieldType: FieldMetadataType.RELATION,
|
||||
expectedSourceRelationType: RelationType.MANY_TO_ONE,
|
||||
expectedTargetRelationType: RelationType.ONE_TO_MANY,
|
||||
shouldSourceHaveMorphId: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'should generate a regular RELATION with isCustom, isSytem, isUnique not default values',
|
||||
context: {
|
||||
input: {
|
||||
sourceFlatObjectMetadata: COMPANY_FLAT_OBJECT_MOCK,
|
||||
targetFlatObjectMetadata: PET_FLAT_OBJECT_MOCK,
|
||||
targetFlatFieldMetadataType: FieldMetadataType.RELATION,
|
||||
sourceFlatObjectMetadataJoinColumnName: 'petId',
|
||||
workspaceId: mockWorkspaceId,
|
||||
workspaceCustomApplicationId: mockWorkspaceCustomApplicationId,
|
||||
createFieldInput: {
|
||||
name: 'pets',
|
||||
label: 'Pets',
|
||||
description: 'Company pets',
|
||||
icon: 'IconCat',
|
||||
type: FieldMetadataType.RELATION,
|
||||
objectMetadataId: COMPANY_FLAT_OBJECT_MOCK.id,
|
||||
isCustom: false,
|
||||
isSystem: true,
|
||||
isUnique: true,
|
||||
relationCreationPayload: {
|
||||
type: RelationType.MANY_TO_ONE,
|
||||
targetObjectMetadataId: PET_FLAT_OBJECT_MOCK.id,
|
||||
targetFieldLabel: 'Company',
|
||||
targetFieldIcon: 'IconBuildingSkyscraper',
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedSourceFieldType: FieldMetadataType.RELATION,
|
||||
expectedTargetFieldType: FieldMetadataType.RELATION,
|
||||
expectedSourceRelationType: RelationType.MANY_TO_ONE,
|
||||
expectedTargetRelationType: RelationType.ONE_TO_MANY,
|
||||
shouldSourceHaveMorphId: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'should generate a MORPH_RELATION (MANY_TO_ONE) field pair with morphId when provided',
|
||||
context: {
|
||||
input: {
|
||||
sourceFlatObjectMetadata: COMPANY_FLAT_OBJECT_MOCK,
|
||||
targetFlatObjectMetadata: PET_FLAT_OBJECT_MOCK,
|
||||
targetFlatFieldMetadataType: FieldMetadataType.RELATION,
|
||||
sourceFlatObjectMetadataJoinColumnName: 'targetPetId',
|
||||
workspaceId: mockWorkspaceId,
|
||||
workspaceCustomApplicationId: mockWorkspaceCustomApplicationId,
|
||||
morphId: '20202020-9a2b-4c3d-a4e5-f6a7b8c9d0e1',
|
||||
createFieldInput: {
|
||||
name: 'targetPet',
|
||||
label: 'Target Pet',
|
||||
description: 'Morph relation to pet',
|
||||
icon: 'IconCat',
|
||||
type: FieldMetadataType.MORPH_RELATION,
|
||||
objectMetadataId: COMPANY_FLAT_OBJECT_MOCK.id,
|
||||
isCustom: false,
|
||||
isSystem: true,
|
||||
isUnique: false,
|
||||
relationCreationPayload: {
|
||||
type: RelationType.MANY_TO_ONE,
|
||||
targetObjectMetadataId: PET_FLAT_OBJECT_MOCK.id,
|
||||
targetFieldLabel: 'Companies',
|
||||
targetFieldIcon: 'IconBuildingSkyscraper',
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedSourceFieldType: FieldMetadataType.MORPH_RELATION,
|
||||
expectedTargetFieldType: FieldMetadataType.RELATION,
|
||||
expectedSourceRelationType: RelationType.MANY_TO_ONE,
|
||||
expectedTargetRelationType: RelationType.ONE_TO_MANY,
|
||||
shouldSourceHaveMorphId: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'should generate a MORPH_RELATION (ONE_TO_MANY) field pair with morphId when provided',
|
||||
context: {
|
||||
input: {
|
||||
sourceFlatObjectMetadata: COMPANY_FLAT_OBJECT_MOCK,
|
||||
targetFlatObjectMetadata: PET_FLAT_OBJECT_MOCK,
|
||||
targetFlatFieldMetadataType: FieldMetadataType.RELATION,
|
||||
sourceFlatObjectMetadataJoinColumnName: 'targetPetId',
|
||||
workspaceId: mockWorkspaceId,
|
||||
workspaceCustomApplicationId: mockWorkspaceCustomApplicationId,
|
||||
morphId: '20202020-9a2b-4c3d-a4e5-f6a7b8c9d0e1',
|
||||
createFieldInput: {
|
||||
name: 'targetPet',
|
||||
label: 'Target Pet',
|
||||
description: 'Morph relation to pet',
|
||||
icon: 'IconCat',
|
||||
type: FieldMetadataType.MORPH_RELATION,
|
||||
objectMetadataId: COMPANY_FLAT_OBJECT_MOCK.id,
|
||||
isCustom: false,
|
||||
isSystem: true,
|
||||
isUnique: false,
|
||||
relationCreationPayload: {
|
||||
type: RelationType.ONE_TO_MANY,
|
||||
targetObjectMetadataId: PET_FLAT_OBJECT_MOCK.id,
|
||||
targetFieldLabel: 'Companies',
|
||||
targetFieldIcon: 'IconBuildingSkyscraper',
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedSourceFieldType: FieldMetadataType.MORPH_RELATION,
|
||||
expectedTargetFieldType: FieldMetadataType.RELATION,
|
||||
expectedSourceRelationType: RelationType.ONE_TO_MANY,
|
||||
expectedTargetRelationType: RelationType.MANY_TO_ONE,
|
||||
shouldSourceHaveMorphId: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'should generate a MORPH_RELATION (ONE_TO_MANY) field pair with MORPH as target',
|
||||
context: {
|
||||
input: {
|
||||
sourceFlatObjectMetadata: COMPANY_FLAT_OBJECT_MOCK,
|
||||
targetFlatObjectMetadata: PET_FLAT_OBJECT_MOCK,
|
||||
targetFlatFieldMetadataType: FieldMetadataType.MORPH_RELATION,
|
||||
sourceFlatObjectMetadataJoinColumnName: 'targetPetId',
|
||||
workspaceId: mockWorkspaceId,
|
||||
workspaceCustomApplicationId: mockWorkspaceCustomApplicationId,
|
||||
morphId: '20202020-9a2b-4c3d-a4e5-f6a7b8c9d0e1',
|
||||
createFieldInput: {
|
||||
name: 'targetPet',
|
||||
label: 'Target Pet',
|
||||
description: 'Morph relation to pet',
|
||||
icon: 'IconCat',
|
||||
type: FieldMetadataType.RELATION,
|
||||
objectMetadataId: COMPANY_FLAT_OBJECT_MOCK.id,
|
||||
isCustom: false,
|
||||
isSystem: true,
|
||||
isUnique: false,
|
||||
relationCreationPayload: {
|
||||
type: RelationType.ONE_TO_MANY,
|
||||
targetObjectMetadataId: PET_FLAT_OBJECT_MOCK.id,
|
||||
targetFieldLabel: 'Companies',
|
||||
targetFieldIcon: 'IconBuildingSkyscraper',
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedSourceFieldType: FieldMetadataType.RELATION,
|
||||
expectedTargetFieldType: FieldMetadataType.MORPH_RELATION,
|
||||
expectedSourceRelationType: RelationType.ONE_TO_MANY,
|
||||
expectedTargetRelationType: RelationType.MANY_TO_ONE,
|
||||
shouldSourceHaveMorphId: false,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
test.each(testCases)(
|
||||
'$title',
|
||||
({
|
||||
context: {
|
||||
input,
|
||||
expectedSourceFieldType,
|
||||
expectedTargetFieldType,
|
||||
expectedSourceRelationType,
|
||||
expectedTargetRelationType,
|
||||
shouldSourceHaveMorphId,
|
||||
},
|
||||
}) => {
|
||||
const result: SourceTargetMorphOrRelationFlatFieldAndFlatIndex =
|
||||
generateMorphOrRelationFlatFieldMetadataPair(input);
|
||||
|
||||
expect(result.flatFieldMetadatas).toHaveLength(2);
|
||||
expect(result.indexMetadatas).toHaveLength(1);
|
||||
|
||||
const [sourceFieldMetadata, targetFieldMetadata] =
|
||||
result.flatFieldMetadatas;
|
||||
|
||||
expect(sourceFieldMetadata.type).toBe(expectedSourceFieldType);
|
||||
expect(sourceFieldMetadata.name).toBe(input.createFieldInput.name);
|
||||
expect(sourceFieldMetadata.label).toBe(input.createFieldInput.label);
|
||||
expect(sourceFieldMetadata.objectMetadataId).toBe(
|
||||
input.sourceFlatObjectMetadata.id,
|
||||
);
|
||||
expect(sourceFieldMetadata.relationTargetObjectMetadataId).toBe(
|
||||
input.targetFlatObjectMetadata.id,
|
||||
);
|
||||
const sourceSettings =
|
||||
sourceFieldMetadata.settings as FieldMetadataRelationSettings;
|
||||
const targetSettings =
|
||||
targetFieldMetadata.settings as FieldMetadataRelationSettings;
|
||||
|
||||
expect(sourceSettings.relationType).toBe(expectedSourceRelationType);
|
||||
|
||||
if (shouldSourceHaveMorphId) {
|
||||
expect(sourceFieldMetadata.morphId).toBe(input.morphId);
|
||||
} else {
|
||||
expect(sourceFieldMetadata.morphId).toBeNull();
|
||||
}
|
||||
|
||||
expect(targetFieldMetadata.type).toBe(expectedTargetFieldType);
|
||||
expect(targetFieldMetadata.objectMetadataId).toBe(
|
||||
input.targetFlatObjectMetadata.id,
|
||||
);
|
||||
expect(targetFieldMetadata.relationTargetObjectMetadataId).toBe(
|
||||
input.sourceFlatObjectMetadata.id,
|
||||
);
|
||||
expect(targetSettings.relationType).toBe(expectedTargetRelationType);
|
||||
|
||||
expect(sourceFieldMetadata.relationTargetFieldMetadataId).toBe(
|
||||
targetFieldMetadata.id,
|
||||
);
|
||||
expect(targetFieldMetadata.relationTargetFieldMetadataId).toBe(
|
||||
sourceFieldMetadata.id,
|
||||
);
|
||||
|
||||
if (expectedSourceRelationType === RelationType.MANY_TO_ONE) {
|
||||
expect(sourceSettings.joinColumnName).toBe(
|
||||
input.sourceFlatObjectMetadataJoinColumnName,
|
||||
);
|
||||
expect(sourceSettings.onDelete).toBe(RelationOnDeleteAction.SET_NULL);
|
||||
}
|
||||
|
||||
if (expectedTargetRelationType === RelationType.MANY_TO_ONE) {
|
||||
expect(targetSettings.joinColumnName).toBeDefined();
|
||||
expect(targetSettings.onDelete).toBe(RelationOnDeleteAction.SET_NULL);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
+1
@@ -99,6 +99,7 @@ export const computeFlatFieldToUpdateFromMorphRelationUpdatePayload = ({
|
||||
},
|
||||
sourceFlatObjectMetadata: sourceObjectMetadata,
|
||||
targetFlatObjectMetadata: newTargetObjectMetadata,
|
||||
targetFlatFieldMetadataType: FieldMetadataType.RELATION,
|
||||
workspaceId: fieldMetadataToUpdate.workspaceId,
|
||||
workspaceCustomApplicationId,
|
||||
sourceFlatObjectMetadataJoinColumnName:
|
||||
|
||||
+2
-1
@@ -1,5 +1,5 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { type FieldMetadataType } from 'twenty-shared/types';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { computeMorphRelationFieldName, isDefined } from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
@@ -92,6 +92,7 @@ export const fromMorphRelationCreateFieldInputToFlatFieldMetadatas = async ({
|
||||
sourceFlatObjectMetadataJoinColumnName,
|
||||
sourceFlatObjectMetadata,
|
||||
targetFlatObjectMetadata,
|
||||
targetFlatFieldMetadataType: FieldMetadataType.RELATION,
|
||||
workspaceId,
|
||||
morphId,
|
||||
workspaceCustomApplicationId,
|
||||
|
||||
+2
-1
@@ -1,5 +1,5 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { type FieldMetadataType } from 'twenty-shared/types';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
|
||||
@@ -70,6 +70,7 @@ export const fromRelationCreateFieldInputToFlatFieldMetadatas = async ({
|
||||
}),
|
||||
sourceFlatObjectMetadata,
|
||||
targetFlatObjectMetadata,
|
||||
targetFlatFieldMetadataType: FieldMetadataType.RELATION,
|
||||
workspaceId,
|
||||
workspaceCustomApplicationId,
|
||||
});
|
||||
|
||||
+47
-17
@@ -14,6 +14,8 @@ import { generateIndexForFlatFieldMetadata } from 'src/engine/metadata-modules/f
|
||||
import { getDefaultFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/get-default-flat-field-metadata-from-create-field-input.util';
|
||||
import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import { buildDescriptionForRelationFieldMetadataOnFromField } from 'src/engine/metadata-modules/object-metadata/utils/build-description-for-relation-field-on-from-field.util';
|
||||
import { buildDescriptionForRelationFieldMetadataOnToField } from 'src/engine/metadata-modules/object-metadata/utils/build-description-for-relation-field-on-to-field.util';
|
||||
|
||||
type ComputeFieldMetadataRelationSettingsForRelationTypeArgs = {
|
||||
relationType: RelationType;
|
||||
@@ -39,6 +41,7 @@ const computeFieldMetadataRelationSettingsForRelationType = ({
|
||||
type GenerateMorphOrRelationFlatFieldMetadataPairArgs = {
|
||||
targetFlatObjectMetadata: FlatObjectMetadata;
|
||||
sourceFlatObjectMetadata: FlatObjectMetadata;
|
||||
targetFlatFieldMetadataType: FieldMetadataType;
|
||||
sourceFlatObjectMetadataJoinColumnName: string;
|
||||
createFieldInput: Omit<CreateFieldInput, 'workspaceId'> &
|
||||
Required<
|
||||
@@ -59,12 +62,15 @@ export const generateMorphOrRelationFlatFieldMetadataPair = ({
|
||||
createFieldInput,
|
||||
sourceFlatObjectMetadata,
|
||||
targetFlatObjectMetadata,
|
||||
targetFlatFieldMetadataType,
|
||||
workspaceId,
|
||||
workspaceCustomApplicationId,
|
||||
sourceFlatObjectMetadataJoinColumnName,
|
||||
morphId = null,
|
||||
targetFieldName,
|
||||
}: GenerateMorphOrRelationFlatFieldMetadataPairArgs): SourceTargetMorphOrRelationFlatFieldAndFlatIndex => {
|
||||
const sourceFlatFieldMetadataType = createFieldInput.type;
|
||||
|
||||
const { relationCreationPayload } = createFieldInput;
|
||||
|
||||
const sourceFlatFieldMetadataSettings =
|
||||
@@ -74,6 +80,17 @@ export const generateMorphOrRelationFlatFieldMetadataPair = ({
|
||||
});
|
||||
const targetRelationTargetFieldMetadataId = v4();
|
||||
const sourceRelationTargetFieldMetadataId = v4();
|
||||
|
||||
const defaultDescriptionFromField =
|
||||
buildDescriptionForRelationFieldMetadataOnFromField({
|
||||
relationObjectMetadataNamePlural: sourceFlatObjectMetadata.namePlural,
|
||||
targetObjectLabelSingular: targetFlatObjectMetadata.labelSingular,
|
||||
});
|
||||
const defaultDescriptionToField =
|
||||
buildDescriptionForRelationFieldMetadataOnToField({
|
||||
relationObjectMetadataNamePlural: targetFlatObjectMetadata.namePlural,
|
||||
targetObjectLabelSingular: sourceFlatObjectMetadata.labelSingular,
|
||||
});
|
||||
const sourceFlatFieldMetadata: Omit<
|
||||
FlatFieldMetadata<MorphOrRelationFieldMetadataType>,
|
||||
'flatRelationTargetFieldMetadata'
|
||||
@@ -84,10 +101,15 @@ export const generateMorphOrRelationFlatFieldMetadataPair = ({
|
||||
fieldMetadataId: sourceRelationTargetFieldMetadataId,
|
||||
workspaceCustomApplicationId,
|
||||
}),
|
||||
morphId,
|
||||
morphId:
|
||||
sourceFlatFieldMetadataType === FieldMetadataType.MORPH_RELATION
|
||||
? morphId
|
||||
: null,
|
||||
objectMetadataId: sourceFlatObjectMetadata.id,
|
||||
icon: createFieldInput.icon ?? 'IconRelationOneToMany',
|
||||
type: createFieldInput.type,
|
||||
type: sourceFlatFieldMetadataType,
|
||||
description:
|
||||
createFieldInput.description ?? defaultDescriptionFromField.description,
|
||||
defaultValue: null,
|
||||
settings: sourceFlatFieldMetadataSettings,
|
||||
options: null,
|
||||
@@ -97,6 +119,8 @@ export const generateMorphOrRelationFlatFieldMetadataPair = ({
|
||||
|
||||
const targetCreateFieldInput: CreateFieldInput = {
|
||||
icon: relationCreationPayload.targetFieldIcon ?? 'Icon123',
|
||||
description:
|
||||
createFieldInput.description ?? defaultDescriptionToField.description,
|
||||
label: relationCreationPayload.targetFieldLabel,
|
||||
name:
|
||||
targetFieldName ??
|
||||
@@ -117,21 +141,27 @@ export const generateMorphOrRelationFlatFieldMetadataPair = ({
|
||||
? RelationType.MANY_TO_ONE
|
||||
: RelationType.ONE_TO_MANY,
|
||||
});
|
||||
const targetFlatFieldMetadata: FlatFieldMetadata<FieldMetadataType.RELATION> =
|
||||
{
|
||||
...getDefaultFlatFieldMetadata({
|
||||
createFieldInput: targetCreateFieldInput,
|
||||
workspaceId,
|
||||
fieldMetadataId: targetRelationTargetFieldMetadataId,
|
||||
workspaceCustomApplicationId,
|
||||
}),
|
||||
type: FieldMetadataType.RELATION,
|
||||
defaultValue: null,
|
||||
settings: targetFlatFieldMetadataSettings,
|
||||
options: null,
|
||||
relationTargetFieldMetadataId: sourceRelationTargetFieldMetadataId,
|
||||
relationTargetObjectMetadataId: sourceFlatObjectMetadata.id,
|
||||
};
|
||||
|
||||
const targetFlatFieldMetadata: FlatFieldMetadata<
|
||||
typeof targetFlatFieldMetadataType
|
||||
> = {
|
||||
...getDefaultFlatFieldMetadata({
|
||||
createFieldInput: targetCreateFieldInput,
|
||||
workspaceId,
|
||||
fieldMetadataId: targetRelationTargetFieldMetadataId,
|
||||
workspaceCustomApplicationId,
|
||||
}),
|
||||
morphId:
|
||||
targetFlatFieldMetadataType === FieldMetadataType.MORPH_RELATION
|
||||
? morphId
|
||||
: null,
|
||||
type: targetFlatFieldMetadataType,
|
||||
defaultValue: null,
|
||||
settings: targetFlatFieldMetadataSettings,
|
||||
options: null,
|
||||
relationTargetFieldMetadataId: sourceRelationTargetFieldMetadataId,
|
||||
relationTargetObjectMetadataId: sourceFlatObjectMetadata.id,
|
||||
};
|
||||
|
||||
const indexMetadata: FlatIndexMetadata = generateIndexForFlatFieldMetadata({
|
||||
flatFieldMetadata:
|
||||
|
||||
+1
-1
@@ -39,7 +39,7 @@ export const getDefaultFlatFieldMetadata = ({
|
||||
createFieldInput.isRemoteCreation,
|
||||
),
|
||||
isSystem: false,
|
||||
isUnique: createFieldInput.isUnique ?? null,
|
||||
isUnique: createFieldInput.isUnique ?? false,
|
||||
label: createFieldInput.label,
|
||||
name: createFieldInput.name,
|
||||
objectMetadataId: createFieldInput.objectMetadataId,
|
||||
|
||||
+8
@@ -56,5 +56,13 @@ export const validateMorphRelationFlatFieldMetadata = (
|
||||
});
|
||||
}
|
||||
|
||||
if (!isDefined(flatFieldMetadataToValidate.morphId)) {
|
||||
errors.push({
|
||||
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
message: 'Morph relation field must have a morph id',
|
||||
userFriendlyMessage: msg`Morph relation field must have a morph id`,
|
||||
});
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
+6
@@ -4,6 +4,8 @@ import {
|
||||
} from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { type FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface';
|
||||
|
||||
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
|
||||
@@ -18,6 +20,7 @@ type FromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCreateArgs
|
||||
createObjectInput: CreateObjectInput;
|
||||
workspaceId: string;
|
||||
workspaceCustomApplicationId: string;
|
||||
existingFeatureFlagsMap: FeatureFlagMap;
|
||||
} & Pick<AllFlatEntityMaps, 'flatObjectMetadataMaps'>;
|
||||
export const fromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCreate =
|
||||
({
|
||||
@@ -25,6 +28,7 @@ export const fromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCre
|
||||
workspaceId,
|
||||
workspaceCustomApplicationId,
|
||||
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
|
||||
existingFeatureFlagsMap,
|
||||
}: FromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCreateArgs): {
|
||||
flatObjectMetadataToCreate: FlatObjectMetadata;
|
||||
relationTargetFlatFieldMetadataToCreate: FlatFieldMetadata[];
|
||||
@@ -96,6 +100,8 @@ export const fromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCre
|
||||
existingFlatObjectMetadataMaps,
|
||||
sourceFlatObjectMetadata: flatObjectMetadataToCreate,
|
||||
workspaceId,
|
||||
workspaceCustomApplicationId,
|
||||
existingFeatureFlagsMap,
|
||||
});
|
||||
|
||||
const objectFlatFieldMetadatas: FlatFieldMetadata[] = [
|
||||
|
||||
+8
-7
@@ -324,13 +324,13 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
workspaceId,
|
||||
},
|
||||
);
|
||||
const { flatObjectMetadataMaps: existingFlatObjectMetadataMaps } =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatObjectMetadataMaps'],
|
||||
},
|
||||
);
|
||||
const {
|
||||
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
|
||||
featureFlagsMap: existingFeatureFlagsMap,
|
||||
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'flatObjectMetadataMaps',
|
||||
'featureFlagsMap',
|
||||
]);
|
||||
|
||||
const {
|
||||
flatObjectMetadataToCreate,
|
||||
@@ -343,6 +343,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
workspaceCustomApplicationId:
|
||||
applicationId ?? workspaceCustomFlatApplication.id,
|
||||
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
|
||||
existingFeatureFlagsMap,
|
||||
});
|
||||
|
||||
const optimisticFlatFieldMetadataMaps = [
|
||||
|
||||
+127
-194
@@ -1,169 +1,26 @@
|
||||
import { type STANDARD_OBJECT_IDS } from 'twenty-shared/metadata';
|
||||
import { FieldMetadataType, RelationOnDeleteAction } from 'twenty-shared/types';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { capitalize, isDefined } from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { type 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 { type MorphOrRelationFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/types/morph-or-relation-field-metadata-type.type';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { generateMorphOrRelationFlatFieldMetadataPair } from 'src/engine/metadata-modules/flat-field-metadata/utils/generate-morph-or-relation-flat-field-metadata-pair.util';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import {
|
||||
ObjectMetadataException,
|
||||
ObjectMetadataExceptionCode,
|
||||
} from 'src/engine/metadata-modules/object-metadata/object-metadata.exception';
|
||||
import { buildDescriptionForRelationFieldMetadataOnFromField } from 'src/engine/metadata-modules/object-metadata/utils/build-description-for-relation-field-on-from-field.util';
|
||||
import { buildDescriptionForRelationFieldMetadataOnToField } from 'src/engine/metadata-modules/object-metadata/utils/build-description-for-relation-field-on-to-field.util';
|
||||
import {
|
||||
CUSTOM_OBJECT_STANDARD_FIELD_IDS,
|
||||
STANDARD_OBJECT_FIELD_IDS,
|
||||
TIMELINE_ACTIVITY_STANDARD_FIELD_IDS,
|
||||
} from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
||||
import { STANDARD_OBJECT_ICONS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-icons';
|
||||
import { createRelationDeterministicUuid } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/create-deterministic-uuid.util';
|
||||
|
||||
const generateSourceFlatFieldMetadata = ({
|
||||
workspaceId,
|
||||
targetFlatObjectMetadata,
|
||||
sourceFlatObjectMetadata,
|
||||
}: Omit<
|
||||
BuildDefaultRelationFieldsForCustomObjectArgs,
|
||||
'existingFlatObjectMetadataMaps'
|
||||
> & {
|
||||
sourceFlatObjectMetadata: FlatObjectMetadata;
|
||||
targetFlatObjectMetadata: FlatObjectMetadata;
|
||||
}): FlatFieldMetadata<FieldMetadataType.RELATION> => {
|
||||
const { description } = buildDescriptionForRelationFieldMetadataOnFromField({
|
||||
relationObjectMetadataNamePlural: targetFlatObjectMetadata.namePlural,
|
||||
targetObjectLabelSingular: sourceFlatObjectMetadata.labelSingular,
|
||||
});
|
||||
|
||||
const standardId =
|
||||
CUSTOM_OBJECT_STANDARD_FIELD_IDS[
|
||||
targetFlatObjectMetadata.namePlural as keyof typeof CUSTOM_OBJECT_STANDARD_FIELD_IDS
|
||||
];
|
||||
|
||||
if (!isDefined(standardId)) {
|
||||
throw new ObjectMetadataException(
|
||||
`Standard field ID not found for target object ${targetFlatObjectMetadata.namePlural}`,
|
||||
ObjectMetadataExceptionCode.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
const createdAt = new Date().toISOString();
|
||||
const sourceFieldMetadataId = v4();
|
||||
const targetFieldMetadataId = v4();
|
||||
const icon =
|
||||
STANDARD_OBJECT_ICONS[
|
||||
targetFlatObjectMetadata.nameSingular as keyof typeof STANDARD_OBJECT_ICONS
|
||||
] || 'IconBuildingSkyscraper';
|
||||
|
||||
return {
|
||||
calendarViewIds: [],
|
||||
mainGroupByFieldMetadataViewIds: [],
|
||||
kanbanAggregateOperationViewIds: [],
|
||||
viewFilterIds: [],
|
||||
viewFieldIds: [],
|
||||
createdAt,
|
||||
updatedAt: createdAt,
|
||||
defaultValue: null,
|
||||
description: description,
|
||||
icon,
|
||||
id: sourceFieldMetadataId,
|
||||
isActive: true,
|
||||
isCustom: false,
|
||||
isLabelSyncedWithName: false,
|
||||
isNullable: true,
|
||||
isUIReadOnly: false,
|
||||
isSystem: true,
|
||||
isUnique: false,
|
||||
label: capitalize(targetFlatObjectMetadata.namePlural),
|
||||
name: targetFlatObjectMetadata.namePlural,
|
||||
objectMetadataId: sourceFlatObjectMetadata.id,
|
||||
options: null,
|
||||
relationTargetFieldMetadataId: targetFieldMetadataId,
|
||||
relationTargetObjectMetadataId: targetFlatObjectMetadata.id,
|
||||
settings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
standardId,
|
||||
standardOverrides: null,
|
||||
type: FieldMetadataType.RELATION,
|
||||
universalIdentifier: sourceFieldMetadataId,
|
||||
workspaceId,
|
||||
morphId: null,
|
||||
applicationId: sourceFlatObjectMetadata.applicationId,
|
||||
};
|
||||
};
|
||||
|
||||
const generateTargetFlatFieldMetadata = ({
|
||||
sourceFlatObjectMetadata,
|
||||
targetFlatObjectMetadata,
|
||||
sourceFlatFieldMetadata,
|
||||
workspaceId,
|
||||
}: Omit<
|
||||
BuildDefaultRelationFieldsForCustomObjectArgs,
|
||||
'existingFlatObjectMetadataMaps'
|
||||
> & {
|
||||
sourceFlatObjectMetadata: FlatObjectMetadata;
|
||||
targetFlatObjectMetadata: FlatObjectMetadata;
|
||||
sourceFlatFieldMetadata: FlatFieldMetadata<MorphOrRelationFieldMetadataType>;
|
||||
}): FlatFieldMetadata<FieldMetadataType.RELATION> => {
|
||||
const customStandardFieldId =
|
||||
STANDARD_OBJECT_FIELD_IDS[
|
||||
targetFlatObjectMetadata.nameSingular as (typeof DEFAULT_RELATIONS_OBJECTS_STANDARD_IDS)[number]
|
||||
].custom;
|
||||
|
||||
const { description } = buildDescriptionForRelationFieldMetadataOnToField({
|
||||
relationObjectMetadataNamePlural: targetFlatObjectMetadata.namePlural,
|
||||
targetObjectLabelSingular: sourceFlatObjectMetadata.labelSingular,
|
||||
});
|
||||
const createdAt = new Date().toISOString();
|
||||
const standardId = createRelationDeterministicUuid({
|
||||
objectId: sourceFlatObjectMetadata.id,
|
||||
standardId: customStandardFieldId,
|
||||
});
|
||||
|
||||
return {
|
||||
morphId: null,
|
||||
calendarViewIds: [],
|
||||
mainGroupByFieldMetadataViewIds: [],
|
||||
viewFieldIds: [],
|
||||
kanbanAggregateOperationViewIds: [],
|
||||
viewFilterIds: [],
|
||||
id: sourceFlatFieldMetadata.relationTargetFieldMetadataId,
|
||||
name: sourceFlatObjectMetadata.nameSingular,
|
||||
label: sourceFlatObjectMetadata.labelSingular,
|
||||
description,
|
||||
standardId,
|
||||
objectMetadataId: targetFlatObjectMetadata.id,
|
||||
workspaceId: workspaceId,
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: true,
|
||||
isUIReadOnly: false,
|
||||
type: FieldMetadataType.RELATION,
|
||||
icon: 'IconBuildingSkyscraper',
|
||||
isNullable: true,
|
||||
settings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
onDelete: RelationOnDeleteAction.CASCADE,
|
||||
joinColumnName: `${sourceFlatObjectMetadata.nameSingular}Id`,
|
||||
},
|
||||
createdAt,
|
||||
updatedAt: createdAt,
|
||||
defaultValue: null,
|
||||
isLabelSyncedWithName: false,
|
||||
isUnique: false,
|
||||
options: null,
|
||||
relationTargetFieldMetadataId: sourceFlatFieldMetadata.id,
|
||||
relationTargetObjectMetadataId: sourceFlatObjectMetadata.id,
|
||||
standardOverrides: null,
|
||||
universalIdentifier: sourceFlatFieldMetadata.relationTargetFieldMetadataId,
|
||||
applicationId: sourceFlatObjectMetadata.applicationId,
|
||||
};
|
||||
};
|
||||
|
||||
const DEFAULT_RELATIONS_OBJECTS_STANDARD_IDS = [
|
||||
'timelineActivity',
|
||||
@@ -173,10 +30,29 @@ const DEFAULT_RELATIONS_OBJECTS_STANDARD_IDS = [
|
||||
'taskTarget',
|
||||
] as const satisfies (keyof typeof STANDARD_OBJECT_IDS)[];
|
||||
|
||||
const morphIdByRelationObjectNameSingular = {
|
||||
timelineActivity: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.targetMorphId,
|
||||
favorite: null,
|
||||
attachment: null,
|
||||
noteTarget: null,
|
||||
taskTarget: null,
|
||||
} satisfies Record<
|
||||
(typeof DEFAULT_RELATIONS_OBJECTS_STANDARD_IDS)[number],
|
||||
string | null
|
||||
>;
|
||||
|
||||
// TODO: once we have finished migrating, we can delete custom code
|
||||
// once we migrate timeline activity to morph relations, we can add it.
|
||||
// another way to check if an object is migrated to morph relations is to check if the feature flag is enabled
|
||||
const DEFAULT_MORPH_RELATIONS_OBJECTS_STANDARD_IDS =
|
||||
[] as const satisfies (keyof typeof STANDARD_OBJECT_IDS)[];
|
||||
|
||||
export type BuildDefaultRelationFieldsForCustomObjectArgs = {
|
||||
existingFeatureFlagsMap: FeatureFlagMap;
|
||||
existingFlatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
|
||||
workspaceId: string;
|
||||
sourceFlatObjectMetadata: FlatObjectMetadata;
|
||||
workspaceCustomApplicationId: string;
|
||||
};
|
||||
|
||||
type SourceAndTargetFlatFieldMetadatasRecord = {
|
||||
@@ -190,9 +66,11 @@ const EMPTY_SOURCE_AND_TARGET_FLAT_FIELD_METADATAS_RECORD: SourceAndTargetFlatFi
|
||||
};
|
||||
|
||||
export const buildDefaultRelationFlatFieldMetadatasForCustomObject = ({
|
||||
existingFeatureFlagsMap,
|
||||
existingFlatObjectMetadataMaps,
|
||||
sourceFlatObjectMetadata,
|
||||
workspaceId,
|
||||
workspaceCustomApplicationId,
|
||||
}: BuildDefaultRelationFieldsForCustomObjectArgs): SourceAndTargetFlatFieldMetadatasRecord => {
|
||||
const objectIdByNameSingular = Object.values(
|
||||
existingFlatObjectMetadataMaps.byId,
|
||||
@@ -207,54 +85,109 @@ export const buildDefaultRelationFlatFieldMetadatasForCustomObject = ({
|
||||
};
|
||||
}, {});
|
||||
|
||||
return DEFAULT_RELATIONS_OBJECTS_STANDARD_IDS.reduce<SourceAndTargetFlatFieldMetadatasRecord>(
|
||||
(sourceAndTargetFlatFieldMetadatasRecord, objectMetadataNameSingular) => {
|
||||
const targetFlatObjectMetadataId =
|
||||
objectIdByNameSingular[objectMetadataNameSingular];
|
||||
const result =
|
||||
DEFAULT_RELATIONS_OBJECTS_STANDARD_IDS.reduce<SourceAndTargetFlatFieldMetadatasRecord>(
|
||||
(sourceAndTargetFlatFieldMetadatasRecord, objectMetadataNameSingular) => {
|
||||
const isObjectMigratedFromOlderReleases =
|
||||
DEFAULT_MORPH_RELATIONS_OBJECTS_STANDARD_IDS.map(toString).includes(
|
||||
objectMetadataNameSingular,
|
||||
);
|
||||
const isFeatureFlagEnabled =
|
||||
(objectMetadataNameSingular === 'timelineActivity' &&
|
||||
existingFeatureFlagsMap[
|
||||
FeatureFlagKey.IS_TIMELINE_ACTIVITY_MIGRATED
|
||||
]) ??
|
||||
false;
|
||||
const isObjectMigratedToMorphRelations =
|
||||
isObjectMigratedFromOlderReleases || isFeatureFlagEnabled;
|
||||
|
||||
if (!isDefined(targetFlatObjectMetadataId)) {
|
||||
throw new ObjectMetadataException(
|
||||
`Standard target object metadata id ${targetFlatObjectMetadataId} not found in cache`,
|
||||
ObjectMetadataExceptionCode.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
const targetFlatObjectMetadataId =
|
||||
objectIdByNameSingular[objectMetadataNameSingular];
|
||||
|
||||
const targetFlatObjectMetadata = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityMaps: existingFlatObjectMetadataMaps,
|
||||
flatEntityId: targetFlatObjectMetadataId,
|
||||
});
|
||||
if (!isDefined(targetFlatObjectMetadataId)) {
|
||||
throw new ObjectMetadataException(
|
||||
`Standard target object metadata id ${targetFlatObjectMetadataId} not found in cache`,
|
||||
ObjectMetadataExceptionCode.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
if (!isDefined(targetFlatObjectMetadata)) {
|
||||
throw new ObjectMetadataException(
|
||||
`Standard target object metadata of id ${targetFlatObjectMetadataId} not found in cache`,
|
||||
ObjectMetadataExceptionCode.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
const targetFlatObjectMetadata =
|
||||
findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityMaps: existingFlatObjectMetadataMaps,
|
||||
flatEntityId: targetFlatObjectMetadataId,
|
||||
});
|
||||
|
||||
const sourceFlatFieldMetadata = generateSourceFlatFieldMetadata({
|
||||
sourceFlatObjectMetadata,
|
||||
targetFlatObjectMetadata,
|
||||
workspaceId,
|
||||
});
|
||||
const standardId =
|
||||
CUSTOM_OBJECT_STANDARD_FIELD_IDS[
|
||||
targetFlatObjectMetadata.namePlural as keyof typeof CUSTOM_OBJECT_STANDARD_FIELD_IDS
|
||||
];
|
||||
|
||||
const targetFlatFieldMetadata = generateTargetFlatFieldMetadata({
|
||||
sourceFlatFieldMetadata,
|
||||
sourceFlatObjectMetadata,
|
||||
targetFlatObjectMetadata,
|
||||
workspaceId,
|
||||
});
|
||||
if (!isDefined(standardId)) {
|
||||
throw new ObjectMetadataException(
|
||||
`Standard field ID not found for target object ${targetFlatObjectMetadata.namePlural}`,
|
||||
ObjectMetadataExceptionCode.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
standardSourceFlatFieldMetadatas: [
|
||||
...sourceAndTargetFlatFieldMetadatasRecord.standardSourceFlatFieldMetadatas,
|
||||
sourceFlatFieldMetadata,
|
||||
],
|
||||
standardTargetFlatFieldMetadatas: [
|
||||
...sourceAndTargetFlatFieldMetadatasRecord.standardTargetFlatFieldMetadatas,
|
||||
targetFlatFieldMetadata,
|
||||
],
|
||||
};
|
||||
},
|
||||
EMPTY_SOURCE_AND_TARGET_FLAT_FIELD_METADATAS_RECORD,
|
||||
);
|
||||
const icon =
|
||||
STANDARD_OBJECT_ICONS[
|
||||
targetFlatObjectMetadata.nameSingular as keyof typeof STANDARD_OBJECT_ICONS
|
||||
] || 'IconBuildingSkyscraper';
|
||||
|
||||
const morphFieldName = `target${capitalize(sourceFlatObjectMetadata.nameSingular)}`;
|
||||
const fieldName = isObjectMigratedToMorphRelations
|
||||
? morphFieldName
|
||||
: sourceFlatObjectMetadata.nameSingular;
|
||||
const joinColumnName = computeMorphOrRelationFieldJoinColumnName({
|
||||
name: fieldName,
|
||||
});
|
||||
|
||||
const morphId =
|
||||
morphIdByRelationObjectNameSingular[objectMetadataNameSingular];
|
||||
|
||||
const { flatFieldMetadatas } =
|
||||
generateMorphOrRelationFlatFieldMetadataPair({
|
||||
sourceFlatObjectMetadata,
|
||||
targetFlatObjectMetadata,
|
||||
targetFlatFieldMetadataType: isObjectMigratedToMorphRelations
|
||||
? FieldMetadataType.MORPH_RELATION
|
||||
: FieldMetadataType.RELATION,
|
||||
workspaceId,
|
||||
workspaceCustomApplicationId,
|
||||
sourceFlatObjectMetadataJoinColumnName: joinColumnName,
|
||||
morphId,
|
||||
targetFieldName: fieldName,
|
||||
createFieldInput: {
|
||||
icon: 'IconBuildingSkyscraper',
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: targetFlatObjectMetadata.namePlural,
|
||||
label: capitalize(targetFlatObjectMetadata.labelPlural),
|
||||
objectMetadataId: sourceFlatObjectMetadata.id,
|
||||
standardId,
|
||||
relationCreationPayload: {
|
||||
type: RelationType.ONE_TO_MANY,
|
||||
targetObjectMetadataId: targetFlatObjectMetadata.id,
|
||||
targetFieldLabel: capitalize(
|
||||
sourceFlatObjectMetadata.nameSingular,
|
||||
),
|
||||
targetFieldIcon: icon,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
standardSourceFlatFieldMetadatas: [
|
||||
...sourceAndTargetFlatFieldMetadatasRecord.standardSourceFlatFieldMetadatas,
|
||||
flatFieldMetadatas[0],
|
||||
],
|
||||
standardTargetFlatFieldMetadatas: [
|
||||
...sourceAndTargetFlatFieldMetadatasRecord.standardTargetFlatFieldMetadatas,
|
||||
flatFieldMetadatas[1],
|
||||
],
|
||||
};
|
||||
},
|
||||
EMPTY_SOURCE_AND_TARGET_FLAT_FIELD_METADATAS_RECORD,
|
||||
);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
+3
-3
@@ -2,11 +2,11 @@ import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import {
|
||||
type FieldMetadataType,
|
||||
type FieldMetadataSettings,
|
||||
type FieldMetadataType,
|
||||
} from 'twenty-shared/types';
|
||||
import { In, Repository } from 'typeorm';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { In, Repository } from 'typeorm';
|
||||
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
@@ -210,7 +210,7 @@ export class RemoteTableRelationsService {
|
||||
{
|
||||
standardId: createForeignKeyDeterministicUuid({
|
||||
objectId: createdObjectMetadata.id,
|
||||
standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.custom,
|
||||
standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.targetCustom,
|
||||
}),
|
||||
objectMetadataId: timelineActivityObjectMetadata.id,
|
||||
workspaceId: workspaceId,
|
||||
|
||||
@@ -146,6 +146,7 @@ export class CustomWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
},
|
||||
icon: 'IconIconTimelineEvent',
|
||||
inverseSideTarget: () => TimelineActivityWorkspaceEntity,
|
||||
inverseSideFieldKey: 'targetCustom',
|
||||
onDelete: RelationOnDeleteAction.CASCADE,
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
|
||||
+1
@@ -205,6 +205,7 @@ describe('WorkspaceEntityManager', () => {
|
||||
IS_EMAILING_DOMAIN_ENABLED: false,
|
||||
IS_WORKFLOW_RUN_STOPPAGE_ENABLED: false,
|
||||
IS_DASHBOARD_V2_ENABLED: false,
|
||||
IS_TIMELINE_ACTIVITY_MIGRATED: false,
|
||||
IS_GLOBAL_WORKSPACE_DATASOURCE_ENABLED: false,
|
||||
},
|
||||
eventEmitterService: {
|
||||
|
||||
+5
@@ -81,6 +81,11 @@ export const seedFeatureFlags = async ({
|
||||
workspaceId: workspaceId,
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
key: FeatureFlagKey.IS_TIMELINE_ACTIVITY_MIGRATED,
|
||||
workspaceId: workspaceId,
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
key: FeatureFlagKey.IS_GLOBAL_WORKSPACE_DATASOURCE_ENABLED,
|
||||
workspaceId: workspaceId,
|
||||
|
||||
+42
-30
@@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
|
||||
|
||||
import chunk from 'lodash.chunk';
|
||||
import { ObjectRecord } from 'twenty-shared/types';
|
||||
import { capitalize } from 'twenty-shared/utils';
|
||||
|
||||
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
|
||||
import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
|
||||
@@ -27,6 +28,7 @@ import { TASK_DATA_SEEDS } from 'src/engine/workspace-manager/dev-seeder/data/co
|
||||
import { TASK_TARGET_DATA_SEEDS_MAP } from 'src/engine/workspace-manager/dev-seeder/data/constants/task-target-data-seeds.constant';
|
||||
import { WORKSPACE_MEMBER_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/workspace-member-data-seeds.constant';
|
||||
import { type TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity';
|
||||
import { buildTimelineActivityRelatedMorphFieldMetadataName } from 'src/modules/timeline/utils/timeline-activity-related-morph-field-metadata-name-builder.util';
|
||||
|
||||
type RecordSeedWithId = Pick<ObjectRecord, 'id'> & Record<string, unknown>;
|
||||
|
||||
@@ -38,11 +40,11 @@ type TimelineActivitySeedData = Pick<
|
||||
| 'linkedRecordId'
|
||||
| 'linkedObjectMetadataId'
|
||||
| 'workspaceMemberId'
|
||||
| 'companyId'
|
||||
| 'personId'
|
||||
| 'noteId'
|
||||
| 'taskId'
|
||||
| 'opportunityId'
|
||||
| 'targetNoteId'
|
||||
| 'targetTaskId'
|
||||
| 'targetPersonId'
|
||||
| 'targetCompanyId'
|
||||
| 'targetOpportunityId'
|
||||
> & {
|
||||
properties: string; // JSON stringified for raw insertion
|
||||
createdAt: string; // ISO string for raw insertion
|
||||
@@ -253,11 +255,11 @@ export class TimelineActivitySeederService {
|
||||
'linkedRecordId',
|
||||
'linkedObjectMetadataId',
|
||||
'workspaceMemberId',
|
||||
'companyId',
|
||||
'personId',
|
||||
'noteId',
|
||||
'taskId',
|
||||
'opportunityId',
|
||||
'targetNoteId',
|
||||
'targetTaskId',
|
||||
'targetPersonId',
|
||||
'targetCompanyId',
|
||||
'targetOpportunityId',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'happensAt',
|
||||
@@ -290,21 +292,31 @@ export class TimelineActivitySeederService {
|
||||
linkedRecordId: recordId,
|
||||
linkedObjectMetadataId: null,
|
||||
workspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.TIM,
|
||||
companyId: null,
|
||||
personId: null,
|
||||
noteId: null,
|
||||
taskId: null,
|
||||
opportunityId: null,
|
||||
targetNoteId: null,
|
||||
targetTaskId: null,
|
||||
targetPersonId: null,
|
||||
targetCompanyId: null,
|
||||
targetOpportunityId: null,
|
||||
createdAt: creationDate,
|
||||
updatedAt: creationDate,
|
||||
happensAt: creationDate,
|
||||
};
|
||||
|
||||
// Set the appropriate entity ID
|
||||
const entityIdKey = `${entityType}Id`;
|
||||
// Set the appropriate target entity ID for entities that have target columns
|
||||
const entitiesWithTargetColumns = new Set([
|
||||
'note',
|
||||
'task',
|
||||
'person',
|
||||
'company',
|
||||
'opportunity',
|
||||
]);
|
||||
|
||||
// @ts-expect-error - This is okay for morph
|
||||
timelineActivity[entityIdKey] = recordId;
|
||||
if (entitiesWithTargetColumns.has(entityType)) {
|
||||
const targetIdKey = `${buildTimelineActivityRelatedMorphFieldMetadataName(entityType)}Id`;
|
||||
|
||||
// @ts-expect-error - This is okay for morph
|
||||
timelineActivity[targetIdKey] = recordId;
|
||||
}
|
||||
|
||||
return timelineActivity;
|
||||
}
|
||||
@@ -576,30 +588,30 @@ export class TimelineActivitySeederService {
|
||||
linkedRecordId: recordSeed.id,
|
||||
linkedObjectMetadataId,
|
||||
workspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.TIM,
|
||||
companyId: null,
|
||||
personId: null,
|
||||
noteId: null,
|
||||
taskId: null,
|
||||
opportunityId: null,
|
||||
targetNoteId: null,
|
||||
targetTaskId: null,
|
||||
targetPersonId: null,
|
||||
targetCompanyId: null,
|
||||
targetOpportunityId: null,
|
||||
createdAt: creationDate,
|
||||
updatedAt: creationDate,
|
||||
happensAt: creationDate,
|
||||
};
|
||||
|
||||
// Set target ID (person, company, or opportunity)
|
||||
const targetIdKey = `${targetInfo.targetType}Id`;
|
||||
const targetIdKey = `target${capitalize(targetInfo.targetType)}Id`;
|
||||
|
||||
// @ts-expect-error - This is okay for morph
|
||||
linkedActivity[targetIdKey] = targetInfo.targetId;
|
||||
|
||||
// Only set activity ID for entities that have corresponding columns
|
||||
const entitiesWithColumns = new Set(['note', 'task']);
|
||||
// Only set target activity ID for entities that have corresponding columns
|
||||
const entitiesWithTargetColumns = new Set(['note', 'task']);
|
||||
|
||||
if (entitiesWithColumns.has(activityType)) {
|
||||
const activityIdKey = `${activityType}Id`;
|
||||
if (entitiesWithTargetColumns.has(activityType)) {
|
||||
const targetActivityIdKey = `target${capitalize(activityType)}Id`;
|
||||
|
||||
// @ts-expect-error - This is okay for morph
|
||||
linkedActivity[activityIdKey] = recordSeed.id;
|
||||
linkedActivity[targetActivityIdKey] = recordSeed.id;
|
||||
}
|
||||
|
||||
return linkedActivity;
|
||||
|
||||
+28
-20
@@ -1555,32 +1555,40 @@ export const STANDARD_OBJECTS = {
|
||||
universalIdentifier:
|
||||
TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.workspaceMember,
|
||||
},
|
||||
person: {
|
||||
universalIdentifier: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.person,
|
||||
targetPerson: {
|
||||
universalIdentifier: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.targetPerson,
|
||||
},
|
||||
company: {
|
||||
universalIdentifier: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.company,
|
||||
targetCompany: {
|
||||
universalIdentifier: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.targetCompany,
|
||||
},
|
||||
opportunity: {
|
||||
universalIdentifier: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.opportunity,
|
||||
},
|
||||
task: { universalIdentifier: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.task },
|
||||
note: { universalIdentifier: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.note },
|
||||
workflow: {
|
||||
universalIdentifier: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.workflow,
|
||||
},
|
||||
workflowVersion: {
|
||||
targetOpportunity: {
|
||||
universalIdentifier:
|
||||
TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.workflowVersion,
|
||||
TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.targetOpportunity,
|
||||
},
|
||||
workflowRun: {
|
||||
universalIdentifier: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.workflowRun,
|
||||
targetTask: {
|
||||
universalIdentifier: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.targetTask,
|
||||
},
|
||||
dashboard: {
|
||||
universalIdentifier: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.dashboard,
|
||||
targetNote: {
|
||||
universalIdentifier: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.targetNote,
|
||||
},
|
||||
custom: {
|
||||
universalIdentifier: '20202020-a9b2-4f85-b3c7-6d8e9f1a4c2b',
|
||||
targetWorkflow: {
|
||||
universalIdentifier:
|
||||
TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.targetWorkflow,
|
||||
},
|
||||
targetWorkflowVersion: {
|
||||
universalIdentifier:
|
||||
TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.targetWorkflowVersion,
|
||||
},
|
||||
targetWorkflowRun: {
|
||||
universalIdentifier:
|
||||
TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.targetWorkflowRun,
|
||||
},
|
||||
targetDashboard: {
|
||||
universalIdentifier:
|
||||
TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.targetDashboard,
|
||||
},
|
||||
targetCustom: {
|
||||
universalIdentifier: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.targetCustom,
|
||||
},
|
||||
linkedRecordCachedName: {
|
||||
universalIdentifier:
|
||||
|
||||
+1
-1
@@ -472,7 +472,7 @@ export const buildCompanyStandardFlatFieldMetadatas = ({
|
||||
isSystem: true,
|
||||
isNullable: true,
|
||||
targetObjectName: 'timelineActivity',
|
||||
targetFieldName: 'company',
|
||||
targetFieldName: 'targetCompany',
|
||||
settings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
|
||||
+1
-1
@@ -195,7 +195,7 @@ export const buildDashboardStandardFlatFieldMetadatas = ({
|
||||
isSystem: true,
|
||||
isNullable: true,
|
||||
targetObjectName: 'timelineActivity',
|
||||
targetFieldName: 'dashboard',
|
||||
targetFieldName: 'targetDashboard',
|
||||
settings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
|
||||
+1
-1
@@ -258,7 +258,7 @@ export const buildNoteStandardFlatFieldMetadatas = ({
|
||||
isSystem: true,
|
||||
isNullable: true,
|
||||
targetObjectName: 'timelineActivity',
|
||||
targetFieldName: 'note',
|
||||
targetFieldName: 'targetNote',
|
||||
settings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
|
||||
+1
-1
@@ -382,7 +382,7 @@ export const buildOpportunityStandardFlatFieldMetadatas = ({
|
||||
isSystem: true,
|
||||
isNullable: true,
|
||||
targetObjectName: 'timelineActivity',
|
||||
targetFieldName: 'opportunity',
|
||||
targetFieldName: 'targetOpportunity',
|
||||
settings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
|
||||
+1
-1
@@ -492,7 +492,7 @@ export const buildPersonStandardFlatFieldMetadatas = ({
|
||||
isSystem: true,
|
||||
isNullable: true,
|
||||
targetObjectName: 'timelineActivity',
|
||||
targetFieldName: 'person',
|
||||
targetFieldName: 'targetPerson',
|
||||
settings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
|
||||
+1
-1
@@ -327,7 +327,7 @@ export const buildTaskStandardFlatFieldMetadatas = ({
|
||||
isSystem: true,
|
||||
isNullable: true,
|
||||
targetObjectName: 'timelineActivity',
|
||||
targetFieldName: 'task',
|
||||
targetFieldName: 'targetTask',
|
||||
settings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
|
||||
+32
-31
@@ -7,10 +7,11 @@ import {
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { type AllStandardObjectFieldName } from 'src/engine/workspace-manager/twenty-standard-application/types/all-standard-object-field-name.type';
|
||||
import {
|
||||
type CreateStandardFieldArgs,
|
||||
createStandardFieldFlatMetadata,
|
||||
type CreateStandardFieldArgs,
|
||||
} from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-field-flat-metadata.util';
|
||||
import { createStandardRelationFieldFlatMetadata } from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-relation-field-flat-metadata.util';
|
||||
import { createStandardMorphRelationFieldFlatMetadata } from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-morph-relation-field-flat-metadata.util';
|
||||
|
||||
export const buildTimelineActivityStandardFlatFieldMetadatas = ({
|
||||
now,
|
||||
@@ -222,11 +223,11 @@ export const buildTimelineActivityStandardFlatFieldMetadatas = ({
|
||||
twentyStandardApplicationId,
|
||||
now,
|
||||
}),
|
||||
person: createStandardRelationFieldFlatMetadata({
|
||||
targetPerson: createStandardMorphRelationFieldFlatMetadata({
|
||||
objectName,
|
||||
workspaceId,
|
||||
context: {
|
||||
fieldName: 'person',
|
||||
fieldName: 'targetPerson',
|
||||
label: 'Person',
|
||||
description: 'Event person',
|
||||
icon: 'IconUser',
|
||||
@@ -236,7 +237,7 @@ export const buildTimelineActivityStandardFlatFieldMetadatas = ({
|
||||
settings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
onDelete: RelationOnDeleteAction.CASCADE,
|
||||
joinColumnName: 'personId',
|
||||
joinColumnName: 'targetPersonId',
|
||||
},
|
||||
},
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
@@ -244,11 +245,11 @@ export const buildTimelineActivityStandardFlatFieldMetadatas = ({
|
||||
twentyStandardApplicationId,
|
||||
now,
|
||||
}),
|
||||
company: createStandardRelationFieldFlatMetadata({
|
||||
targetCompany: createStandardMorphRelationFieldFlatMetadata({
|
||||
objectName,
|
||||
workspaceId,
|
||||
context: {
|
||||
fieldName: 'company',
|
||||
fieldName: 'targetCompany',
|
||||
label: 'Company',
|
||||
description: 'Event company',
|
||||
icon: 'IconBuildingSkyscraper',
|
||||
@@ -258,7 +259,7 @@ export const buildTimelineActivityStandardFlatFieldMetadatas = ({
|
||||
settings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
onDelete: RelationOnDeleteAction.CASCADE,
|
||||
joinColumnName: 'companyId',
|
||||
joinColumnName: 'targetCompanyId',
|
||||
},
|
||||
},
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
@@ -266,11 +267,11 @@ export const buildTimelineActivityStandardFlatFieldMetadatas = ({
|
||||
twentyStandardApplicationId,
|
||||
now,
|
||||
}),
|
||||
opportunity: createStandardRelationFieldFlatMetadata({
|
||||
targetOpportunity: createStandardMorphRelationFieldFlatMetadata({
|
||||
objectName,
|
||||
workspaceId,
|
||||
context: {
|
||||
fieldName: 'opportunity',
|
||||
fieldName: 'targetOpportunity',
|
||||
label: 'Opportunity',
|
||||
description: 'Event opportunity',
|
||||
icon: 'IconTargetArrow',
|
||||
@@ -280,7 +281,7 @@ export const buildTimelineActivityStandardFlatFieldMetadatas = ({
|
||||
settings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
onDelete: RelationOnDeleteAction.SET_NULL,
|
||||
joinColumnName: 'opportunityId',
|
||||
joinColumnName: 'targetOpportunityId',
|
||||
},
|
||||
},
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
@@ -288,11 +289,11 @@ export const buildTimelineActivityStandardFlatFieldMetadatas = ({
|
||||
twentyStandardApplicationId,
|
||||
now,
|
||||
}),
|
||||
note: createStandardRelationFieldFlatMetadata({
|
||||
targetNote: createStandardMorphRelationFieldFlatMetadata({
|
||||
objectName,
|
||||
workspaceId,
|
||||
context: {
|
||||
fieldName: 'note',
|
||||
fieldName: 'targetNote',
|
||||
label: 'Note',
|
||||
description: 'Event note',
|
||||
icon: 'IconTargetArrow',
|
||||
@@ -302,7 +303,7 @@ export const buildTimelineActivityStandardFlatFieldMetadatas = ({
|
||||
settings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
onDelete: RelationOnDeleteAction.SET_NULL,
|
||||
joinColumnName: 'noteId',
|
||||
joinColumnName: 'targetNoteId',
|
||||
},
|
||||
},
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
@@ -310,11 +311,11 @@ export const buildTimelineActivityStandardFlatFieldMetadatas = ({
|
||||
twentyStandardApplicationId,
|
||||
now,
|
||||
}),
|
||||
task: createStandardRelationFieldFlatMetadata({
|
||||
targetTask: createStandardMorphRelationFieldFlatMetadata({
|
||||
objectName,
|
||||
workspaceId,
|
||||
context: {
|
||||
fieldName: 'task',
|
||||
fieldName: 'targetTask',
|
||||
label: 'Task',
|
||||
description: 'Event task',
|
||||
icon: 'IconTargetArrow',
|
||||
@@ -324,7 +325,7 @@ export const buildTimelineActivityStandardFlatFieldMetadatas = ({
|
||||
settings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
onDelete: RelationOnDeleteAction.SET_NULL,
|
||||
joinColumnName: 'taskId',
|
||||
joinColumnName: 'targetTaskId',
|
||||
},
|
||||
},
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
@@ -332,11 +333,11 @@ export const buildTimelineActivityStandardFlatFieldMetadatas = ({
|
||||
twentyStandardApplicationId,
|
||||
now,
|
||||
}),
|
||||
workflow: createStandardRelationFieldFlatMetadata({
|
||||
targetWorkflow: createStandardMorphRelationFieldFlatMetadata({
|
||||
objectName,
|
||||
workspaceId,
|
||||
context: {
|
||||
fieldName: 'workflow',
|
||||
fieldName: 'targetWorkflow',
|
||||
label: 'Workflow',
|
||||
description: 'Event workflow',
|
||||
icon: 'IconTargetArrow',
|
||||
@@ -346,7 +347,7 @@ export const buildTimelineActivityStandardFlatFieldMetadatas = ({
|
||||
settings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
onDelete: RelationOnDeleteAction.CASCADE,
|
||||
joinColumnName: 'workflowId',
|
||||
joinColumnName: 'targetWorkflowId',
|
||||
},
|
||||
},
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
@@ -354,11 +355,11 @@ export const buildTimelineActivityStandardFlatFieldMetadatas = ({
|
||||
twentyStandardApplicationId,
|
||||
now,
|
||||
}),
|
||||
workflowVersion: createStandardRelationFieldFlatMetadata({
|
||||
targetWorkflowVersion: createStandardMorphRelationFieldFlatMetadata({
|
||||
objectName,
|
||||
workspaceId,
|
||||
context: {
|
||||
fieldName: 'workflowVersion',
|
||||
fieldName: 'targetWorkflowVersion',
|
||||
label: 'WorkflowVersion',
|
||||
description: 'Event workflow version',
|
||||
icon: 'IconTargetArrow',
|
||||
@@ -368,7 +369,7 @@ export const buildTimelineActivityStandardFlatFieldMetadatas = ({
|
||||
settings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
onDelete: RelationOnDeleteAction.CASCADE,
|
||||
joinColumnName: 'workflowVersionId',
|
||||
joinColumnName: 'targetWorkflowVersionId',
|
||||
},
|
||||
},
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
@@ -376,11 +377,11 @@ export const buildTimelineActivityStandardFlatFieldMetadatas = ({
|
||||
twentyStandardApplicationId,
|
||||
now,
|
||||
}),
|
||||
workflowRun: createStandardRelationFieldFlatMetadata({
|
||||
targetWorkflowRun: createStandardMorphRelationFieldFlatMetadata({
|
||||
objectName,
|
||||
workspaceId,
|
||||
context: {
|
||||
fieldName: 'workflowRun',
|
||||
fieldName: 'targetWorkflowRun',
|
||||
label: 'Workflow Run',
|
||||
description: 'Event workflow run',
|
||||
icon: 'IconTargetArrow',
|
||||
@@ -390,7 +391,7 @@ export const buildTimelineActivityStandardFlatFieldMetadatas = ({
|
||||
settings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
onDelete: RelationOnDeleteAction.CASCADE,
|
||||
joinColumnName: 'workflowRunId',
|
||||
joinColumnName: 'targetWorkflowRunId',
|
||||
},
|
||||
},
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
@@ -398,11 +399,11 @@ export const buildTimelineActivityStandardFlatFieldMetadatas = ({
|
||||
twentyStandardApplicationId,
|
||||
now,
|
||||
}),
|
||||
dashboard: createStandardRelationFieldFlatMetadata({
|
||||
targetDashboard: createStandardMorphRelationFieldFlatMetadata({
|
||||
objectName,
|
||||
workspaceId,
|
||||
context: {
|
||||
fieldName: 'dashboard',
|
||||
fieldName: 'targetDashboard',
|
||||
label: 'Dashboard',
|
||||
description: 'Event dashboard',
|
||||
icon: 'IconTargetArrow',
|
||||
@@ -412,7 +413,7 @@ export const buildTimelineActivityStandardFlatFieldMetadatas = ({
|
||||
settings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
onDelete: RelationOnDeleteAction.SET_NULL,
|
||||
joinColumnName: 'dashboardId',
|
||||
joinColumnName: 'targetDashboardId',
|
||||
},
|
||||
},
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
@@ -420,11 +421,11 @@ export const buildTimelineActivityStandardFlatFieldMetadatas = ({
|
||||
twentyStandardApplicationId,
|
||||
now,
|
||||
}),
|
||||
custom: createStandardRelationFieldFlatMetadata({
|
||||
targetCustom: createStandardMorphRelationFieldFlatMetadata({
|
||||
objectName,
|
||||
workspaceId,
|
||||
context: {
|
||||
fieldName: 'custom',
|
||||
fieldName: 'targetCustom',
|
||||
label: 'Custom',
|
||||
description: 'Timeline Activity custom object',
|
||||
icon: 'IconTimeline',
|
||||
@@ -434,7 +435,7 @@ export const buildTimelineActivityStandardFlatFieldMetadatas = ({
|
||||
settings: {
|
||||
relationType: RelationType.MANY_TO_ONE,
|
||||
onDelete: RelationOnDeleteAction.CASCADE,
|
||||
joinColumnName: 'customId',
|
||||
joinColumnName: 'targetCustomId',
|
||||
},
|
||||
},
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
|
||||
+1
-1
@@ -368,7 +368,7 @@ export const buildWorkflowRunStandardFlatFieldMetadatas = ({
|
||||
isSystem: true,
|
||||
isNullable: false,
|
||||
targetObjectName: 'timelineActivity',
|
||||
targetFieldName: 'workflowRun',
|
||||
targetFieldName: 'targetWorkflowRun',
|
||||
settings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
|
||||
+1
-1
@@ -324,7 +324,7 @@ export const buildWorkflowStandardFlatFieldMetadatas = ({
|
||||
isSystem: true,
|
||||
isNullable: false,
|
||||
targetObjectName: 'timelineActivity',
|
||||
targetFieldName: 'workflow',
|
||||
targetFieldName: 'targetWorkflow',
|
||||
settings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
|
||||
+1
-1
@@ -283,7 +283,7 @@ export const buildWorkflowVersionStandardFlatFieldMetadatas = ({
|
||||
isSystem: true,
|
||||
isNullable: false,
|
||||
targetObjectName: 'timelineActivity',
|
||||
targetFieldName: 'workflowVersion',
|
||||
targetFieldName: 'targetWorkflowVersion',
|
||||
settings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { type AllStandardObjectName } from 'src/engine/workspace-manager/twenty-standard-application/types/all-standard-object-name.type';
|
||||
import { type StandardBuilderArgs } from 'src/engine/workspace-manager/twenty-standard-application/types/metadata-standard-buillder-args.type';
|
||||
import {
|
||||
type CreateStandardMorphOrRelationFieldContext,
|
||||
createStandardRelationFieldFlatMetadata,
|
||||
} from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/create-standard-relation-field-flat-metadata.util';
|
||||
|
||||
export type CreateStandardMorphRelationFieldContext<
|
||||
O extends AllStandardObjectName,
|
||||
T extends AllStandardObjectName,
|
||||
> = CreateStandardMorphOrRelationFieldContext<
|
||||
O,
|
||||
T,
|
||||
FieldMetadataType.MORPH_RELATION
|
||||
>;
|
||||
|
||||
export type CreateStandardMorphRelationFieldArgs<
|
||||
O extends AllStandardObjectName,
|
||||
T extends AllStandardObjectName,
|
||||
> = StandardBuilderArgs<'fieldMetadata'> & {
|
||||
objectName: O;
|
||||
context: CreateStandardMorphRelationFieldContext<O, T>;
|
||||
};
|
||||
|
||||
export const createStandardMorphRelationFieldFlatMetadata = <
|
||||
O extends AllStandardObjectName,
|
||||
T extends AllStandardObjectName,
|
||||
>(
|
||||
args: CreateStandardMorphRelationFieldArgs<O, T>,
|
||||
): FlatFieldMetadata => {
|
||||
const simpleRelationFlatFieldMetadata =
|
||||
createStandardRelationFieldFlatMetadata(args);
|
||||
|
||||
return {
|
||||
...simpleRelationFlatFieldMetadata,
|
||||
type: FieldMetadataType.MORPH_RELATION,
|
||||
};
|
||||
};
|
||||
+7
-1
@@ -15,6 +15,12 @@ import { type StandardBuilderArgs } from 'src/engine/workspace-manager/twenty-st
|
||||
export type CreateStandardRelationFieldContext<
|
||||
O extends AllStandardObjectName,
|
||||
T extends AllStandardObjectName,
|
||||
> = CreateStandardMorphOrRelationFieldContext<O, T, FieldMetadataType.RELATION>;
|
||||
|
||||
export type CreateStandardMorphOrRelationFieldContext<
|
||||
O extends AllStandardObjectName,
|
||||
T extends AllStandardObjectName,
|
||||
F extends FieldMetadataType.RELATION | FieldMetadataType.MORPH_RELATION,
|
||||
> = {
|
||||
fieldName: AllStandardObjectFieldName<O>;
|
||||
label: string;
|
||||
@@ -26,7 +32,7 @@ export type CreateStandardRelationFieldContext<
|
||||
isNullable?: boolean;
|
||||
isUIReadOnly?: boolean;
|
||||
defaultValue?: FieldMetadataDefaultValueForAnyType;
|
||||
settings: FieldMetadataSettings<FieldMetadataType.RELATION>;
|
||||
settings: FieldMetadataSettings<F>;
|
||||
options?: FieldMetadataDefaultOption[] | FieldMetadataComplexOption[] | null;
|
||||
};
|
||||
|
||||
|
||||
+9
-9
@@ -33,7 +33,7 @@ export const buildTimelineActivityStandardFlatIndexMetadatas = ({
|
||||
workspaceId,
|
||||
context: {
|
||||
indexName: 'personIdIndex',
|
||||
relatedFieldNames: ['person'],
|
||||
relatedFieldNames: ['targetPerson'],
|
||||
},
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
dependencyFlatEntityMaps,
|
||||
@@ -45,7 +45,7 @@ export const buildTimelineActivityStandardFlatIndexMetadatas = ({
|
||||
workspaceId,
|
||||
context: {
|
||||
indexName: 'companyIdIndex',
|
||||
relatedFieldNames: ['company'],
|
||||
relatedFieldNames: ['targetCompany'],
|
||||
},
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
dependencyFlatEntityMaps,
|
||||
@@ -57,7 +57,7 @@ export const buildTimelineActivityStandardFlatIndexMetadatas = ({
|
||||
workspaceId,
|
||||
context: {
|
||||
indexName: 'opportunityIdIndex',
|
||||
relatedFieldNames: ['opportunity'],
|
||||
relatedFieldNames: ['targetOpportunity'],
|
||||
},
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
dependencyFlatEntityMaps,
|
||||
@@ -69,7 +69,7 @@ export const buildTimelineActivityStandardFlatIndexMetadatas = ({
|
||||
workspaceId,
|
||||
context: {
|
||||
indexName: 'noteIdIndex',
|
||||
relatedFieldNames: ['note'],
|
||||
relatedFieldNames: ['targetNote'],
|
||||
},
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
dependencyFlatEntityMaps,
|
||||
@@ -81,7 +81,7 @@ export const buildTimelineActivityStandardFlatIndexMetadatas = ({
|
||||
workspaceId,
|
||||
context: {
|
||||
indexName: 'taskIdIndex',
|
||||
relatedFieldNames: ['task'],
|
||||
relatedFieldNames: ['targetTask'],
|
||||
},
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
dependencyFlatEntityMaps,
|
||||
@@ -93,7 +93,7 @@ export const buildTimelineActivityStandardFlatIndexMetadatas = ({
|
||||
workspaceId,
|
||||
context: {
|
||||
indexName: 'workflowIdIndex',
|
||||
relatedFieldNames: ['workflow'],
|
||||
relatedFieldNames: ['targetWorkflow'],
|
||||
},
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
dependencyFlatEntityMaps,
|
||||
@@ -105,7 +105,7 @@ export const buildTimelineActivityStandardFlatIndexMetadatas = ({
|
||||
workspaceId,
|
||||
context: {
|
||||
indexName: 'workflowVersionIdIndex',
|
||||
relatedFieldNames: ['workflowVersion'],
|
||||
relatedFieldNames: ['targetWorkflowVersion'],
|
||||
},
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
dependencyFlatEntityMaps,
|
||||
@@ -117,7 +117,7 @@ export const buildTimelineActivityStandardFlatIndexMetadatas = ({
|
||||
workspaceId,
|
||||
context: {
|
||||
indexName: 'workflowRunIdIndex',
|
||||
relatedFieldNames: ['workflowRun'],
|
||||
relatedFieldNames: ['targetWorkflowRun'],
|
||||
},
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
dependencyFlatEntityMaps,
|
||||
@@ -129,7 +129,7 @@ export const buildTimelineActivityStandardFlatIndexMetadatas = ({
|
||||
workspaceId,
|
||||
context: {
|
||||
indexName: 'dashboardIdIndex',
|
||||
relatedFieldNames: ['dashboard'],
|
||||
relatedFieldNames: ['targetDashboard'],
|
||||
},
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
dependencyFlatEntityMaps,
|
||||
|
||||
+4
-2
@@ -1,3 +1,5 @@
|
||||
import { type FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
|
||||
export const DEFAULT_FEATURE_FLAGS = [] as const satisfies FeatureFlagKey[];
|
||||
export const DEFAULT_FEATURE_FLAGS = [
|
||||
FeatureFlagKey.IS_TIMELINE_ACTIVITY_MIGRATED,
|
||||
] as const satisfies FeatureFlagKey[];
|
||||
|
||||
+11
-10
@@ -163,19 +163,20 @@ export const TIMELINE_ACTIVITY_STANDARD_FIELD_IDS = {
|
||||
name: '20202020-7207-46e8-9dab-849505ae8497',
|
||||
properties: '20202020-f142-4b04-b91b-6a2b4af3bf11',
|
||||
workspaceMember: '20202020-af23-4479-9a30-868edc474b36',
|
||||
person: '20202020-c414-45b9-a60a-ac27aa96229f',
|
||||
company: '20202020-04ad-4221-a744-7a8278a5ce21',
|
||||
opportunity: '20202020-7664-4a35-a3df-580d389fd527',
|
||||
task: '20202020-b2f5-415c-9135-a31dfe49501b',
|
||||
note: '20202020-ec55-4135-8da5-3a20badc0156',
|
||||
workflow: '20202020-616c-4ad3-a2e9-c477c341e295',
|
||||
workflowVersion: '20202020-74f1-4711-a129-e14ca0ecd744',
|
||||
workflowRun: '20202020-96f0-401b-9186-a3a0759225ac',
|
||||
dashboard: '20202020-7864-48f5-af7c-9e4b60140948',
|
||||
custom: '20202020-4a71-41b0-9f83-9cdcca3f8b14',
|
||||
targetPerson: '20202020-c414-45b9-a60a-ac27aa96229f',
|
||||
targetCompany: '20202020-04ad-4221-a744-7a8278a5ce21',
|
||||
targetOpportunity: '20202020-7664-4a35-a3df-580d389fd527',
|
||||
targetTask: '20202020-b2f5-415c-9135-a31dfe49501b',
|
||||
targetNote: '20202020-ec55-4135-8da5-3a20badc0156',
|
||||
targetWorkflow: '20202020-616c-4ad3-a2e9-c477c341e295',
|
||||
targetWorkflowVersion: '20202020-74f1-4711-a129-e14ca0ecd744',
|
||||
targetWorkflowRun: '20202020-96f0-401b-9186-a3a0759225ac',
|
||||
targetDashboard: '20202020-7864-48f5-af7c-9e4b60140948',
|
||||
targetCustom: '20202020-4a71-41b0-9f83-9cdcca3f8b14',
|
||||
linkedRecordCachedName: '20202020-cfdb-4bef-bbce-a29f41230934',
|
||||
linkedRecordId: '20202020-2e0e-48c0-b445-ee6c1e61687d',
|
||||
linkedObjectMetadataId: '20202020-c595-449d-9f89-562758c9ee69',
|
||||
targetMorphId: '20202020-9a2b-4c3d-a4e5-f6a7b8c9d0e1',
|
||||
} as const;
|
||||
|
||||
export const FAVORITE_STANDARD_FIELD_IDS = {
|
||||
|
||||
+1
@@ -272,6 +272,7 @@ export class AttachmentWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
@WorkspaceJoinColumn('workflow')
|
||||
workflowId: string | null;
|
||||
|
||||
// todo: remove this decorator and the custom field
|
||||
@WorkspaceDynamicRelation({
|
||||
type: RelationType.MANY_TO_ONE,
|
||||
argsFactory: (oppositeObjectMetadata) => ({
|
||||
|
||||
+16
-1
@@ -3,10 +3,16 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
|
||||
|
||||
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module';
|
||||
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 { WorkspaceFeatureFlagsMapCacheService } from 'src/engine/metadata-modules/workspace-feature-flags-map-cache/workspace-feature-flags-map-cache.service';
|
||||
import { GetDataFromCacheWithRecomputeService } from 'src/engine/workspace-cache-storage/services/get-data-from-cache-with-recompute.service';
|
||||
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
|
||||
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
|
||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||
import { CalendarEventParticipantMatchParticipantJob } from 'src/modules/calendar/calendar-event-participant-manager/jobs/calendar-event-participant-match-participant.job';
|
||||
import { CalendarEventParticipantPersonListener } from 'src/modules/calendar/calendar-event-participant-manager/listeners/calendar-event-participant-person.listener';
|
||||
@@ -20,10 +26,15 @@ import { MatchParticipantModule } from 'src/modules/match-participant/match-part
|
||||
imports: [
|
||||
WorkspaceDataSourceModule,
|
||||
WorkspaceModule,
|
||||
TypeOrmModule.forFeature([ObjectMetadataEntity, FieldMetadataEntity]),
|
||||
TypeOrmModule.forFeature([
|
||||
ObjectMetadataEntity,
|
||||
FieldMetadataEntity,
|
||||
FeatureFlagEntity,
|
||||
]),
|
||||
NestjsQueryTypeOrmModule.forFeature([WorkspaceEntity]),
|
||||
ContactCreationManagerModule,
|
||||
MatchParticipantModule,
|
||||
WorkspaceCacheModule,
|
||||
],
|
||||
providers: [
|
||||
CalendarEventParticipantService,
|
||||
@@ -31,6 +42,10 @@ import { MatchParticipantModule } from 'src/modules/match-participant/match-part
|
||||
CalendarEventParticipantListener,
|
||||
CalendarEventParticipantPersonListener,
|
||||
CalendarEventParticipantWorkspaceMemberListener,
|
||||
FeatureFlagService,
|
||||
WorkspaceFeatureFlagsMapCacheService,
|
||||
WorkspaceCacheStorageService,
|
||||
GetDataFromCacheWithRecomputeService,
|
||||
],
|
||||
exports: [CalendarEventParticipantService],
|
||||
})
|
||||
|
||||
+11
-1
@@ -5,12 +5,14 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { OnCustomBatchEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-custom-batch-event.decorator';
|
||||
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 { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
|
||||
import { CustomWorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/custom-workspace-batch-event.type';
|
||||
import { type CalendarEventParticipantWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event-participant.workspace-entity';
|
||||
import { TimelineActivityRepository } from 'src/modules/timeline/repositories/timeline-activity.repository';
|
||||
import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity';
|
||||
import { CustomWorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/custom-workspace-batch-event.type';
|
||||
|
||||
@Injectable()
|
||||
export class CalendarEventParticipantListener {
|
||||
@@ -19,6 +21,7 @@ export class CalendarEventParticipantListener {
|
||||
private readonly timelineActivityRepository: TimelineActivityRepository,
|
||||
@InjectRepository(ObjectMetadataEntity)
|
||||
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
) {}
|
||||
|
||||
@OnCustomBatchEvent('calendarEventParticipant_matched')
|
||||
@@ -72,10 +75,17 @@ export class CalendarEventParticipantListener {
|
||||
.filter(isDefined);
|
||||
});
|
||||
|
||||
const isFeatureFlagTimelineActivityMigrated =
|
||||
await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_TIMELINE_ACTIVITY_MIGRATED,
|
||||
batchEvent.workspaceId,
|
||||
);
|
||||
|
||||
await this.timelineActivityRepository.upsertTimelineActivities({
|
||||
objectSingularName: 'person',
|
||||
workspaceId: batchEvent.workspaceId,
|
||||
payloads: timelineActivityPayloads.filter(isDefined),
|
||||
isFeatureFlagTimelineActivityMigrated,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -266,6 +266,7 @@ export class CompanyWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
description: msg`Timeline Activities linked to the company`,
|
||||
icon: 'IconIconTimelineEvent',
|
||||
inverseSideTarget: () => TimelineActivityWorkspaceEntity,
|
||||
inverseSideFieldKey: 'targetCompany',
|
||||
onDelete: RelationOnDeleteAction.CASCADE,
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
|
||||
+1
@@ -97,6 +97,7 @@ export class DashboardWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
label: msg`Timeline Activities`,
|
||||
description: msg`Timeline activities linked to the dashboard`,
|
||||
inverseSideTarget: () => TimelineActivityWorkspaceEntity,
|
||||
inverseSideFieldKey: 'targetDashboard',
|
||||
onDelete: RelationOnDeleteAction.SET_NULL,
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { FieldMetadataType, RelationOnDeleteAction } from 'twenty-shared/types';
|
||||
import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata';
|
||||
import { FieldMetadataType, RelationOnDeleteAction } from 'twenty-shared/types';
|
||||
|
||||
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
||||
import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface';
|
||||
@@ -237,6 +237,7 @@ export class FavoriteWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
@WorkspaceIsNullable()
|
||||
viewId: string;
|
||||
|
||||
// todo: remove this decorator and the custom field
|
||||
@WorkspaceDynamicRelation({
|
||||
type: RelationType.MANY_TO_ONE,
|
||||
argsFactory: (oppositeObjectMetadata) => ({
|
||||
|
||||
+11
-1
@@ -5,12 +5,14 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { OnCustomBatchEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-custom-batch-event.decorator';
|
||||
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 { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
|
||||
import { CustomWorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/custom-workspace-batch-event.type';
|
||||
import { type MessageParticipantWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-participant.workspace-entity';
|
||||
import { TimelineActivityRepository } from 'src/modules/timeline/repositories/timeline-activity.repository';
|
||||
import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity';
|
||||
import { CustomWorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/custom-workspace-batch-event.type';
|
||||
|
||||
@Injectable()
|
||||
export class MessageParticipantListener {
|
||||
@@ -19,6 +21,7 @@ export class MessageParticipantListener {
|
||||
private readonly timelineActivityRepository: TimelineActivityRepository,
|
||||
@InjectRepository(ObjectMetadataEntity)
|
||||
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
) {}
|
||||
|
||||
@OnCustomBatchEvent('messageParticipant_matched')
|
||||
@@ -71,10 +74,17 @@ export class MessageParticipantListener {
|
||||
.filter(isDefined);
|
||||
});
|
||||
|
||||
const isFeatureFlagTimelineActivityMigrated =
|
||||
await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_TIMELINE_ACTIVITY_MIGRATED,
|
||||
batchEvent.workspaceId,
|
||||
);
|
||||
|
||||
await this.timelineActivityRepository.upsertTimelineActivities({
|
||||
objectSingularName: 'person',
|
||||
workspaceId: batchEvent.workspaceId,
|
||||
payloads: timelineActivityPayloads.filter(isDefined),
|
||||
isFeatureFlagTimelineActivityMigrated,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+14
@@ -3,9 +3,16 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { AuditModule } from 'src/engine/core-modules/audit/audit.module';
|
||||
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { WorkspaceFeatureFlagsMapCacheService } from 'src/engine/metadata-modules/workspace-feature-flags-map-cache/workspace-feature-flags-map-cache.service';
|
||||
import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
|
||||
import { GetDataFromCacheWithRecomputeService } from 'src/engine/workspace-cache-storage/services/get-data-from-cache-with-recompute.service';
|
||||
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
|
||||
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
|
||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||
import { ContactCreationManagerModule } from 'src/modules/contact-creation-manager/contact-creation-manager.module';
|
||||
import { MatchParticipantModule } from 'src/modules/match-participant/match-participant.module';
|
||||
@@ -23,6 +30,8 @@ import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-o
|
||||
FeatureFlagEntity,
|
||||
WorkspaceEntity,
|
||||
ObjectMetadataEntity,
|
||||
RoleEntity,
|
||||
RoleTargetEntity,
|
||||
]),
|
||||
AuditModule,
|
||||
ContactCreationManagerModule,
|
||||
@@ -32,6 +41,7 @@ import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-o
|
||||
]),
|
||||
MessagingCommonModule,
|
||||
MatchParticipantModule,
|
||||
WorkspaceCacheModule,
|
||||
],
|
||||
providers: [
|
||||
MessagingMessageParticipantService,
|
||||
@@ -39,6 +49,10 @@ import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-o
|
||||
MessageParticipantListener,
|
||||
MessageParticipantPersonListener,
|
||||
MessageParticipantWorkspaceMemberListener,
|
||||
FeatureFlagService,
|
||||
WorkspaceFeatureFlagsMapCacheService,
|
||||
WorkspaceCacheStorageService,
|
||||
GetDataFromCacheWithRecomputeService,
|
||||
],
|
||||
exports: [MessagingMessageParticipantService],
|
||||
})
|
||||
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { RelationOnDeleteAction } from 'twenty-shared/types';
|
||||
import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata';
|
||||
import { RelationOnDeleteAction } from 'twenty-shared/types';
|
||||
|
||||
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
||||
import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface';
|
||||
@@ -95,6 +95,7 @@ export class NoteTargetWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
@WorkspaceJoinColumn('opportunity')
|
||||
opportunityId: string | null;
|
||||
|
||||
// todo: remove this decorator and the custom field
|
||||
@WorkspaceDynamicRelation({
|
||||
type: RelationType.MANY_TO_ONE,
|
||||
argsFactory: (oppositeObjectMetadata) => ({
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata';
|
||||
import {
|
||||
ActorMetadata,
|
||||
FieldMetadataType,
|
||||
RelationOnDeleteAction,
|
||||
type RichTextV2Metadata,
|
||||
} from 'twenty-shared/types';
|
||||
import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata';
|
||||
|
||||
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
||||
import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface';
|
||||
|
||||
import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/search-field-metadata/constants/search-vector-field.constants';
|
||||
import { IndexType } from 'src/engine/metadata-modules/index-metadata/types/indexType.types';
|
||||
import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/search-field-metadata/constants/search-vector-field.constants';
|
||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
||||
import { WorkspaceFieldIndex } from 'src/engine/twenty-orm/decorators/workspace-field-index.decorator';
|
||||
@@ -127,6 +127,7 @@ export class NoteWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
description: msg`Timeline Activities linked to the note.`,
|
||||
icon: 'IconTimelineEvent',
|
||||
inverseSideTarget: () => TimelineActivityWorkspaceEntity,
|
||||
inverseSideFieldKey: 'targetNote',
|
||||
onDelete: RelationOnDeleteAction.SET_NULL,
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
|
||||
+3
-2
@@ -1,17 +1,17 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata';
|
||||
import {
|
||||
ActorMetadata,
|
||||
type CurrencyMetadata,
|
||||
FieldMetadataType,
|
||||
RelationOnDeleteAction,
|
||||
} from 'twenty-shared/types';
|
||||
import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata';
|
||||
|
||||
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
||||
import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface';
|
||||
|
||||
import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/search-field-metadata/constants/search-vector-field.constants';
|
||||
import { IndexType } from 'src/engine/metadata-modules/index-metadata/types/indexType.types';
|
||||
import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/search-field-metadata/constants/search-vector-field.constants';
|
||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
||||
import { WorkspaceFieldIndex } from 'src/engine/twenty-orm/decorators/workspace-field-index.decorator';
|
||||
@@ -219,6 +219,7 @@ export class OpportunityWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
description: msg`Timeline Activities linked to the opportunity.`,
|
||||
icon: 'IconTimelineEvent',
|
||||
inverseSideTarget: () => TimelineActivityWorkspaceEntity,
|
||||
inverseSideFieldKey: 'targetOpportunity',
|
||||
onDelete: RelationOnDeleteAction.SET_NULL,
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
|
||||
@@ -306,6 +306,7 @@ export class PersonWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
description: msg`Events linked to the person`,
|
||||
icon: 'IconTimelineEvent',
|
||||
inverseSideTarget: () => TimelineActivityWorkspaceEntity,
|
||||
inverseSideFieldKey: 'targetPerson',
|
||||
onDelete: RelationOnDeleteAction.CASCADE,
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { RelationOnDeleteAction } from 'twenty-shared/types';
|
||||
import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata';
|
||||
import { RelationOnDeleteAction } from 'twenty-shared/types';
|
||||
|
||||
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
||||
import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface';
|
||||
@@ -95,6 +95,7 @@ export class TaskTargetWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
@WorkspaceJoinColumn('opportunity')
|
||||
opportunityId: string | null;
|
||||
|
||||
// todo: remove this decorator and the custom field
|
||||
@WorkspaceDynamicRelation({
|
||||
type: RelationType.MANY_TO_ONE,
|
||||
argsFactory: (oppositeObjectMetadata) => ({
|
||||
|
||||
@@ -182,6 +182,7 @@ export class TaskWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
description: msg`Timeline Activities linked to the task.`,
|
||||
icon: 'IconTimelineEvent',
|
||||
inverseSideTarget: () => TimelineActivityWorkspaceEntity,
|
||||
inverseSideFieldKey: 'targetTask',
|
||||
onDelete: RelationOnDeleteAction.SET_NULL,
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
|
||||
+37
-3
@@ -8,6 +8,7 @@ import { objectRecordDiffMerge } from 'src/engine/core-modules/event-emitter/uti
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
|
||||
import { type TimelineActivityPayload } from 'src/modules/timeline/types/timeline-activity-payload';
|
||||
import { buildTimelineActivityRelatedMorphFieldMetadataName } from 'src/modules/timeline/utils/timeline-activity-related-morph-field-metadata-name-builder.util';
|
||||
|
||||
type TimelineActivityPayloadWorkspaceIdAndObjectSingularName = {
|
||||
payloads: (Omit<TimelineActivityPayload, 'properties'> & {
|
||||
@@ -15,6 +16,7 @@ type TimelineActivityPayloadWorkspaceIdAndObjectSingularName = {
|
||||
})[];
|
||||
workspaceId: string;
|
||||
objectSingularName: string;
|
||||
isFeatureFlagTimelineActivityMigrated: boolean;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
@@ -27,6 +29,7 @@ export class TimelineActivityRepository {
|
||||
objectSingularName,
|
||||
workspaceId,
|
||||
payloads,
|
||||
isFeatureFlagTimelineActivityMigrated,
|
||||
}: TimelineActivityPayloadWorkspaceIdAndObjectSingularName) {
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
@@ -38,6 +41,7 @@ export class TimelineActivityRepository {
|
||||
objectSingularName,
|
||||
workspaceId,
|
||||
payloads,
|
||||
isFeatureFlagTimelineActivityMigrated,
|
||||
});
|
||||
|
||||
const payloadsWithDiff = payloads
|
||||
@@ -59,10 +63,16 @@ export class TimelineActivityRepository {
|
||||
const payloadsToInsert: TimelineActivityPayloadWorkspaceIdAndObjectSingularName['payloads'] =
|
||||
[];
|
||||
|
||||
const timelineActivityPropertyName =
|
||||
await this.getTimelineActivityPropertyName(
|
||||
objectSingularName,
|
||||
isFeatureFlagTimelineActivityMigrated,
|
||||
);
|
||||
|
||||
for (const payload of payloadsWithDiff) {
|
||||
const recentTimelineActivity = recentTimelineActivities.find(
|
||||
(timelineActivity) =>
|
||||
timelineActivity[`${objectSingularName}Id`] ===
|
||||
timelineActivity[timelineActivityPropertyName] ===
|
||||
payload.recordId &&
|
||||
timelineActivity.workspaceMemberId ===
|
||||
payload.workspaceMemberId &&
|
||||
@@ -92,6 +102,7 @@ export class TimelineActivityRepository {
|
||||
objectSingularName,
|
||||
payloads: payloadsToInsert,
|
||||
workspaceId,
|
||||
isFeatureFlagTimelineActivityMigrated,
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -101,6 +112,7 @@ export class TimelineActivityRepository {
|
||||
objectSingularName,
|
||||
workspaceId,
|
||||
payloads,
|
||||
isFeatureFlagTimelineActivityMigrated,
|
||||
}: TimelineActivityPayloadWorkspaceIdAndObjectSingularName) {
|
||||
const timelineActivityTypeORMRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository(
|
||||
@@ -113,8 +125,14 @@ export class TimelineActivityRepository {
|
||||
|
||||
const tenMinutesAgo = new Date(Date.now() - 10 * 60 * 1000);
|
||||
|
||||
const timelineActivityPropertyName =
|
||||
await this.getTimelineActivityPropertyName(
|
||||
objectSingularName,
|
||||
isFeatureFlagTimelineActivityMigrated,
|
||||
);
|
||||
|
||||
const whereConditions: Record<string, unknown> = {
|
||||
[`${objectSingularName}Id`]: In(
|
||||
[timelineActivityPropertyName]: In(
|
||||
payloads.map((payload) => payload.recordId),
|
||||
),
|
||||
name: In(payloads.map((payload) => payload.name)),
|
||||
@@ -135,6 +153,7 @@ export class TimelineActivityRepository {
|
||||
objectSingularName,
|
||||
workspaceId,
|
||||
payloads,
|
||||
isFeatureFlagTimelineActivityMigrated,
|
||||
}: TimelineActivityPayloadWorkspaceIdAndObjectSingularName) {
|
||||
if (payloads.length === 0) {
|
||||
return;
|
||||
@@ -149,12 +168,18 @@ export class TimelineActivityRepository {
|
||||
},
|
||||
);
|
||||
|
||||
const timelineActivityPropertyName =
|
||||
await this.getTimelineActivityPropertyName(
|
||||
objectSingularName,
|
||||
isFeatureFlagTimelineActivityMigrated,
|
||||
);
|
||||
|
||||
return timelineActivityTypeORMRepository.insert(
|
||||
payloads.map((payload) => ({
|
||||
name: payload.name,
|
||||
properties: payload.properties,
|
||||
workspaceMemberId: payload.workspaceMemberId,
|
||||
[`${objectSingularName}Id`]: payload.recordId,
|
||||
[timelineActivityPropertyName]: payload.recordId,
|
||||
linkedRecordCachedName: payload.linkedRecordCachedName ?? '',
|
||||
linkedRecordId: payload.linkedRecordId,
|
||||
linkedObjectMetadataId: payload.linkedObjectMetadataId,
|
||||
@@ -187,4 +212,13 @@ export class TimelineActivityRepository {
|
||||
workspaceMemberId: workspaceMemberId,
|
||||
});
|
||||
}
|
||||
|
||||
private async getTimelineActivityPropertyName(
|
||||
objectSingularName: string,
|
||||
isFeatureFlagTimelineActivityMigrated: boolean,
|
||||
) {
|
||||
return isFeatureFlagTimelineActivityMigrated
|
||||
? `${buildTimelineActivityRelatedMorphFieldMetadataName(objectSingularName)}Id`
|
||||
: `${objectSingularName}Id`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import { In } from 'typeorm';
|
||||
|
||||
import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util';
|
||||
import { type ObjectRecordBaseEvent } from 'src/engine/core-modules/event-emitter/types/object-record.base.event';
|
||||
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 { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
@@ -25,6 +27,7 @@ export class TimelineActivityService {
|
||||
constructor(
|
||||
@InjectObjectMetadataRepository(TimelineActivityWorkspaceEntity)
|
||||
private readonly timelineActivityRepository: TimelineActivityRepository,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
) {}
|
||||
@@ -76,11 +79,18 @@ export class TimelineActivityService {
|
||||
{} as Record<string, TimelineActivityPayload[]>,
|
||||
);
|
||||
|
||||
const isFeatureFlagTimelineActivityMigrated =
|
||||
await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_TIMELINE_ACTIVITY_MIGRATED,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
for (const objectSingularName in payloadsByObjectSingularName) {
|
||||
this.timelineActivityRepository.upsertTimelineActivities({
|
||||
await this.timelineActivityRepository.upsertTimelineActivities({
|
||||
objectSingularName,
|
||||
workspaceId,
|
||||
payloads: payloadsByObjectSingularName[objectSingularName],
|
||||
isFeatureFlagTimelineActivityMigrated,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+73
-37
@@ -1,6 +1,7 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata';
|
||||
import { FieldMetadataType, RelationOnDeleteAction } from 'twenty-shared/types';
|
||||
import { capitalize } from 'twenty-shared/utils';
|
||||
|
||||
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
||||
import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface';
|
||||
@@ -120,7 +121,7 @@ export class TimelineActivityWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
workspaceMemberId: string | null;
|
||||
|
||||
@WorkspaceRelation({
|
||||
standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.person,
|
||||
standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.targetPerson,
|
||||
type: RelationType.MANY_TO_ONE,
|
||||
label: msg`Person`,
|
||||
description: msg`Event person`,
|
||||
@@ -128,15 +129,17 @@ export class TimelineActivityWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
inverseSideTarget: () => PersonWorkspaceEntity,
|
||||
inverseSideFieldKey: 'timelineActivities',
|
||||
onDelete: RelationOnDeleteAction.CASCADE,
|
||||
isMorphRelation: true,
|
||||
morphId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.targetMorphId,
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
person: Relation<PersonWorkspaceEntity> | null;
|
||||
targetPerson: Relation<PersonWorkspaceEntity> | null;
|
||||
|
||||
@WorkspaceJoinColumn('person')
|
||||
personId: string | null;
|
||||
@WorkspaceJoinColumn('targetPerson')
|
||||
targetPersonId: string | null;
|
||||
|
||||
@WorkspaceRelation({
|
||||
standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.company,
|
||||
standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.targetCompany,
|
||||
type: RelationType.MANY_TO_ONE,
|
||||
label: msg`Company`,
|
||||
description: msg`Event company`,
|
||||
@@ -144,15 +147,17 @@ export class TimelineActivityWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
inverseSideTarget: () => CompanyWorkspaceEntity,
|
||||
inverseSideFieldKey: 'timelineActivities',
|
||||
onDelete: RelationOnDeleteAction.CASCADE,
|
||||
isMorphRelation: true,
|
||||
morphId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.targetMorphId,
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
company: Relation<CompanyWorkspaceEntity> | null;
|
||||
targetCompany: Relation<CompanyWorkspaceEntity> | null;
|
||||
|
||||
@WorkspaceJoinColumn('company')
|
||||
companyId: string | null;
|
||||
@WorkspaceJoinColumn('targetCompany')
|
||||
targetCompanyId: string | null;
|
||||
|
||||
@WorkspaceRelation({
|
||||
standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.opportunity,
|
||||
standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.targetOpportunity,
|
||||
type: RelationType.MANY_TO_ONE,
|
||||
label: msg`Opportunity`,
|
||||
description: msg`Event opportunity`,
|
||||
@@ -160,15 +165,17 @@ export class TimelineActivityWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
inverseSideTarget: () => OpportunityWorkspaceEntity,
|
||||
inverseSideFieldKey: 'timelineActivities',
|
||||
onDelete: RelationOnDeleteAction.SET_NULL,
|
||||
isMorphRelation: true,
|
||||
morphId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.targetMorphId,
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
opportunity: Relation<OpportunityWorkspaceEntity> | null;
|
||||
targetOpportunity: Relation<OpportunityWorkspaceEntity> | null;
|
||||
|
||||
@WorkspaceJoinColumn('opportunity')
|
||||
opportunityId: string | null;
|
||||
@WorkspaceJoinColumn('targetOpportunity')
|
||||
targetOpportunityId: string | null;
|
||||
|
||||
@WorkspaceRelation({
|
||||
standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.note,
|
||||
standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.targetNote,
|
||||
type: RelationType.MANY_TO_ONE,
|
||||
label: msg`Note`,
|
||||
description: msg`Event note`,
|
||||
@@ -176,15 +183,17 @@ export class TimelineActivityWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
inverseSideTarget: () => NoteWorkspaceEntity,
|
||||
inverseSideFieldKey: 'timelineActivities',
|
||||
onDelete: RelationOnDeleteAction.SET_NULL,
|
||||
isMorphRelation: true,
|
||||
morphId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.targetMorphId,
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
note: Relation<NoteWorkspaceEntity> | null;
|
||||
targetNote: Relation<NoteWorkspaceEntity> | null;
|
||||
|
||||
@WorkspaceJoinColumn('note')
|
||||
noteId: string | null;
|
||||
@WorkspaceJoinColumn('targetNote')
|
||||
targetNoteId: string | null;
|
||||
|
||||
@WorkspaceRelation({
|
||||
standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.task,
|
||||
standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.targetTask,
|
||||
type: RelationType.MANY_TO_ONE,
|
||||
label: msg`Task`,
|
||||
description: msg`Event task`,
|
||||
@@ -192,15 +201,17 @@ export class TimelineActivityWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
inverseSideTarget: () => TaskWorkspaceEntity,
|
||||
inverseSideFieldKey: 'timelineActivities',
|
||||
onDelete: RelationOnDeleteAction.SET_NULL,
|
||||
isMorphRelation: true,
|
||||
morphId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.targetMorphId,
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
task: Relation<TaskWorkspaceEntity> | null;
|
||||
targetTask: Relation<TaskWorkspaceEntity> | null;
|
||||
|
||||
@WorkspaceJoinColumn('task')
|
||||
taskId: string | null;
|
||||
@WorkspaceJoinColumn('targetTask')
|
||||
targetTaskId: string | null;
|
||||
|
||||
@WorkspaceRelation({
|
||||
standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.workflow,
|
||||
standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.targetWorkflow,
|
||||
type: RelationType.MANY_TO_ONE,
|
||||
label: msg`Workflow`,
|
||||
description: msg`Event workflow`,
|
||||
@@ -208,15 +219,17 @@ export class TimelineActivityWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
inverseSideTarget: () => WorkflowWorkspaceEntity,
|
||||
inverseSideFieldKey: 'timelineActivities',
|
||||
onDelete: RelationOnDeleteAction.CASCADE,
|
||||
isMorphRelation: true,
|
||||
morphId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.targetMorphId,
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
workflow: Relation<WorkflowWorkspaceEntity> | null;
|
||||
targetWorkflow: Relation<WorkflowWorkspaceEntity> | null;
|
||||
|
||||
@WorkspaceJoinColumn('workflow')
|
||||
workflowId: string | null;
|
||||
@WorkspaceJoinColumn('targetWorkflow')
|
||||
targetWorkflowId: string | null;
|
||||
|
||||
@WorkspaceRelation({
|
||||
standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.workflowVersion,
|
||||
standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.targetWorkflowVersion,
|
||||
type: RelationType.MANY_TO_ONE,
|
||||
label: msg`WorkflowVersion`,
|
||||
description: msg`Event workflow version`,
|
||||
@@ -224,15 +237,17 @@ export class TimelineActivityWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
inverseSideTarget: () => WorkflowVersionWorkspaceEntity,
|
||||
inverseSideFieldKey: 'timelineActivities',
|
||||
onDelete: RelationOnDeleteAction.CASCADE,
|
||||
isMorphRelation: true,
|
||||
morphId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.targetMorphId,
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
workflowVersion: Relation<WorkflowVersionWorkspaceEntity> | null;
|
||||
targetWorkflowVersion: Relation<WorkflowVersionWorkspaceEntity> | null;
|
||||
|
||||
@WorkspaceJoinColumn('workflowVersion')
|
||||
workflowVersionId: string | null;
|
||||
@WorkspaceJoinColumn('targetWorkflowVersion')
|
||||
targetWorkflowVersionId: string | null;
|
||||
|
||||
@WorkspaceRelation({
|
||||
standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.workflowRun,
|
||||
standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.targetWorkflowRun,
|
||||
type: RelationType.MANY_TO_ONE,
|
||||
label: msg`Workflow Run`,
|
||||
description: msg`Event workflow run`,
|
||||
@@ -240,15 +255,17 @@ export class TimelineActivityWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
inverseSideTarget: () => WorkflowRunWorkspaceEntity,
|
||||
inverseSideFieldKey: 'timelineActivities',
|
||||
onDelete: RelationOnDeleteAction.CASCADE,
|
||||
isMorphRelation: true,
|
||||
morphId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.targetMorphId,
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
workflowRun: Relation<WorkflowRunWorkspaceEntity> | null;
|
||||
targetWorkflowRun: Relation<WorkflowRunWorkspaceEntity> | null;
|
||||
|
||||
@WorkspaceJoinColumn('workflowRun')
|
||||
workflowRunId: string | null;
|
||||
@WorkspaceJoinColumn('targetWorkflowRun')
|
||||
targetWorkflowRunId: string | null;
|
||||
|
||||
@WorkspaceRelation({
|
||||
standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.dashboard,
|
||||
standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.targetDashboard,
|
||||
type: RelationType.MANY_TO_ONE,
|
||||
label: msg`Dashboard`,
|
||||
description: msg`Event dashboard`,
|
||||
@@ -256,17 +273,20 @@ export class TimelineActivityWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
inverseSideTarget: () => DashboardWorkspaceEntity,
|
||||
inverseSideFieldKey: 'timelineActivities',
|
||||
onDelete: RelationOnDeleteAction.SET_NULL,
|
||||
isMorphRelation: true,
|
||||
morphId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.targetMorphId,
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
dashboard: Relation<DashboardWorkspaceEntity> | null;
|
||||
targetDashboard: Relation<DashboardWorkspaceEntity> | null;
|
||||
|
||||
@WorkspaceJoinColumn('dashboard')
|
||||
dashboardId: string | null;
|
||||
@WorkspaceJoinColumn('targetDashboard')
|
||||
targetDashboardId: string | null;
|
||||
|
||||
// todo: remove this decorator and the custom field
|
||||
@WorkspaceDynamicRelation({
|
||||
type: RelationType.MANY_TO_ONE,
|
||||
argsFactory: (oppositeObjectMetadata) => ({
|
||||
standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.custom,
|
||||
standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.targetCustom,
|
||||
name: oppositeObjectMetadata.nameSingular,
|
||||
label: oppositeObjectMetadata.labelSingular,
|
||||
description: `Timeline Activity ${oppositeObjectMetadata.labelSingular}`,
|
||||
@@ -278,4 +298,20 @@ export class TimelineActivityWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
onDelete: RelationOnDeleteAction.CASCADE,
|
||||
})
|
||||
custom: Relation<CustomWorkspaceEntity>;
|
||||
|
||||
@WorkspaceDynamicRelation({
|
||||
type: RelationType.MANY_TO_ONE,
|
||||
argsFactory: (oppositeObjectMetadata) => ({
|
||||
standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.targetCustom,
|
||||
name: oppositeObjectMetadata.nameSingular,
|
||||
label: oppositeObjectMetadata.labelSingular,
|
||||
description: `Timeline Activity ${oppositeObjectMetadata.labelSingular}`,
|
||||
joinColumn: `target${capitalize(oppositeObjectMetadata.nameSingular)}Id`,
|
||||
icon: 'IconTimeline',
|
||||
}),
|
||||
inverseSideTarget: () => CustomWorkspaceEntity,
|
||||
inverseSideFieldKey: 'timelineActivities',
|
||||
onDelete: RelationOnDeleteAction.CASCADE,
|
||||
})
|
||||
targetCustom: Relation<CustomWorkspaceEntity>;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
|
||||
import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
|
||||
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
|
||||
@@ -12,6 +13,7 @@ import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-o
|
||||
TimelineActivityWorkspaceEntity,
|
||||
]),
|
||||
TwentyORMModule,
|
||||
FeatureFlagModule,
|
||||
WorkspaceManyOrAllFlatEntityMapsCacheModule,
|
||||
],
|
||||
providers: [TimelineActivityService],
|
||||
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
import { capitalize } from 'twenty-shared/utils';
|
||||
|
||||
export const buildTimelineActivityRelatedMorphFieldMetadataName = (
|
||||
name: string,
|
||||
) => {
|
||||
return `target${capitalize(name)}`;
|
||||
};
|
||||
+1
@@ -284,6 +284,7 @@ export class WorkflowRunWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
label: msg`Timeline Activities`,
|
||||
description: msg`Timeline activities linked to the run`,
|
||||
inverseSideTarget: () => TimelineActivityWorkspaceEntity,
|
||||
inverseSideFieldKey: 'targetWorkflowRun',
|
||||
onDelete: RelationOnDeleteAction.CASCADE,
|
||||
})
|
||||
@WorkspaceIsSystem()
|
||||
|
||||
+1
@@ -198,6 +198,7 @@ export class WorkflowVersionWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
label: msg`Timeline Activities`,
|
||||
description: msg`Timeline activities linked to the version`,
|
||||
inverseSideTarget: () => TimelineActivityWorkspaceEntity,
|
||||
inverseSideFieldKey: 'targetWorkflowVersion',
|
||||
onDelete: RelationOnDeleteAction.CASCADE,
|
||||
})
|
||||
@WorkspaceIsSystem()
|
||||
|
||||
+2
-1
@@ -9,9 +9,9 @@ import {
|
||||
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
||||
import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface';
|
||||
|
||||
import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/search-field-metadata/constants/search-vector-field.constants';
|
||||
import { type FieldMetadataComplexOption } from 'src/engine/metadata-modules/field-metadata/dtos/options.input';
|
||||
import { IndexType } from 'src/engine/metadata-modules/index-metadata/types/indexType.types';
|
||||
import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/search-field-metadata/constants/search-vector-field.constants';
|
||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
||||
import { WorkspaceFieldIndex } from 'src/engine/twenty-orm/decorators/workspace-field-index.decorator';
|
||||
@@ -193,6 +193,7 @@ export class WorkflowWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
label: msg`Timeline Activities`,
|
||||
description: msg`Timeline activities linked to the workflow`,
|
||||
inverseSideTarget: () => TimelineActivityWorkspaceEntity,
|
||||
inverseSideFieldKey: 'targetWorkflow',
|
||||
onDelete: RelationOnDeleteAction.CASCADE,
|
||||
})
|
||||
@WorkspaceIsSystem()
|
||||
|
||||
+20
-20
@@ -21,16 +21,16 @@ describe('timelineActivitiesResolver (e2e)', () => {
|
||||
updatedAt
|
||||
deletedAt
|
||||
workspaceMemberId
|
||||
personId
|
||||
companyId
|
||||
opportunityId
|
||||
noteId
|
||||
taskId
|
||||
workflowId
|
||||
workflowVersionId
|
||||
workflowRunId
|
||||
petId
|
||||
surveyResultId
|
||||
targetPersonId
|
||||
targetCompanyId
|
||||
targetOpportunityId
|
||||
targetNoteId
|
||||
targetTaskId
|
||||
targetWorkflowId
|
||||
targetWorkflowVersionId
|
||||
targetWorkflowRunId
|
||||
targetPetId
|
||||
targetSurveyResultId
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -69,16 +69,16 @@ describe('timelineActivitiesResolver (e2e)', () => {
|
||||
expect(timelineActivities).toHaveProperty('updatedAt');
|
||||
expect(timelineActivities).toHaveProperty('deletedAt');
|
||||
expect(timelineActivities).toHaveProperty('workspaceMemberId');
|
||||
expect(timelineActivities).toHaveProperty('personId');
|
||||
expect(timelineActivities).toHaveProperty('companyId');
|
||||
expect(timelineActivities).toHaveProperty('opportunityId');
|
||||
expect(timelineActivities).toHaveProperty('noteId');
|
||||
expect(timelineActivities).toHaveProperty('taskId');
|
||||
expect(timelineActivities).toHaveProperty('workflowId');
|
||||
expect(timelineActivities).toHaveProperty('workflowVersionId');
|
||||
expect(timelineActivities).toHaveProperty('workflowRunId');
|
||||
expect(timelineActivities).toHaveProperty('petId');
|
||||
expect(timelineActivities).toHaveProperty('surveyResultId');
|
||||
expect(timelineActivities).toHaveProperty('targetPersonId');
|
||||
expect(timelineActivities).toHaveProperty('targetCompanyId');
|
||||
expect(timelineActivities).toHaveProperty('targetOpportunityId');
|
||||
expect(timelineActivities).toHaveProperty('targetNoteId');
|
||||
expect(timelineActivities).toHaveProperty('targetTaskId');
|
||||
expect(timelineActivities).toHaveProperty('targetWorkflowId');
|
||||
expect(timelineActivities).toHaveProperty('targetWorkflowVersionId');
|
||||
expect(timelineActivities).toHaveProperty('targetWorkflowRunId');
|
||||
expect(timelineActivities).toHaveProperty('targetPetId');
|
||||
expect(timelineActivities).toHaveProperty('targetSurveyResultId');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
+2
-2
@@ -21,7 +21,7 @@ exports[`updateOne FieldMetadataService morph relation fields v2 It should updat
|
||||
exports[`updateOne FieldMetadataService morph relation fields v2 It should update all morph related flat field metadata and their related field allowing its deletion 2`] = `
|
||||
{
|
||||
"description": [
|
||||
null,
|
||||
"Description for all",
|
||||
],
|
||||
"isActive": [
|
||||
true,
|
||||
@@ -58,7 +58,7 @@ exports[`updateOne FieldMetadataService morph relation fields v2 It should updat
|
||||
exports[`updateOne FieldMetadataService morph relation fields v2 It should update all morph related flat field metadata and their related field allowing its deletion 4`] = `
|
||||
{
|
||||
"description": [
|
||||
null,
|
||||
"Description for all",
|
||||
],
|
||||
"isActive": [
|
||||
false,
|
||||
|
||||
+29
-41
@@ -227,7 +227,7 @@ exports[`Object metadata creation should fail v2 when labelPlural contains only
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"id": Any<String>,
|
||||
"name": "listinga",
|
||||
"name": "targetListinga",
|
||||
"objectMetadataId": Any<String>,
|
||||
},
|
||||
"status": "fail",
|
||||
@@ -853,7 +853,7 @@ exports[`Object metadata creation should fail v2 when labelPlural exceeds maximu
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"id": Any<String>,
|
||||
"name": "listinga",
|
||||
"name": "targetListinga",
|
||||
"objectMetadataId": Any<String>,
|
||||
},
|
||||
"status": "fail",
|
||||
@@ -1490,7 +1490,7 @@ exports[`Object metadata creation should fail v2 when labelSingular contains onl
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"id": Any<String>,
|
||||
"name": "listinga",
|
||||
"name": "targetListinga",
|
||||
"objectMetadataId": Any<String>,
|
||||
},
|
||||
"status": "fail",
|
||||
@@ -2116,7 +2116,7 @@ exports[`Object metadata creation should fail v2 when labelSingular exceeds maxi
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"id": Any<String>,
|
||||
"name": "listinga",
|
||||
"name": "targetListinga",
|
||||
"objectMetadataId": Any<String>,
|
||||
},
|
||||
"status": "fail",
|
||||
@@ -2753,7 +2753,7 @@ exports[`Object metadata creation should fail v2 when labels are identical 1`] =
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"id": Any<String>,
|
||||
"name": "listinga",
|
||||
"name": "targetListinga",
|
||||
"objectMetadataId": Any<String>,
|
||||
},
|
||||
"status": "fail",
|
||||
@@ -3379,7 +3379,7 @@ exports[`Object metadata creation should fail v2 when labels with whitespaces re
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"id": Any<String>,
|
||||
"name": "listinga",
|
||||
"name": "targetListinga",
|
||||
"objectMetadataId": Any<String>,
|
||||
},
|
||||
"status": "fail",
|
||||
@@ -4001,7 +4001,7 @@ exports[`Object metadata creation should fail v2 when name exceeds maximum lengt
|
||||
"code": "INVALID_FIELD_INPUT",
|
||||
"message": "Name is too long",
|
||||
"userFriendlyMessage": "Name is too long",
|
||||
"value": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"value": "targetAaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
},
|
||||
{
|
||||
"code": "OBJECT_METADATA_NOT_FOUND",
|
||||
@@ -4011,7 +4011,7 @@ exports[`Object metadata creation should fail v2 when name exceeds maximum lengt
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"id": Any<String>,
|
||||
"name": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"name": "targetAaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"objectMetadataId": Any<String>,
|
||||
},
|
||||
"status": "fail",
|
||||
@@ -4661,7 +4661,7 @@ exports[`Object metadata creation should fail v2 when namePlural has invalid cha
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"id": Any<String>,
|
||||
"name": "listinga",
|
||||
"name": "targetListinga",
|
||||
"objectMetadataId": Any<String>,
|
||||
},
|
||||
"status": "fail",
|
||||
@@ -5287,7 +5287,7 @@ exports[`Object metadata creation should fail v2 when namePlural is a reserved k
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"id": Any<String>,
|
||||
"name": "listinga",
|
||||
"name": "targetListinga",
|
||||
"objectMetadataId": Any<String>,
|
||||
},
|
||||
"status": "fail",
|
||||
@@ -5924,7 +5924,7 @@ exports[`Object metadata creation should fail v2 when namePlural is not camelCas
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"id": Any<String>,
|
||||
"name": "listinga",
|
||||
"name": "targetListinga",
|
||||
"objectMetadataId": Any<String>,
|
||||
},
|
||||
"status": "fail",
|
||||
@@ -6552,13 +6552,13 @@ exports[`Object metadata creation should fail v2 when nameSingular contains only
|
||||
"code": "INVALID_FIELD_INPUT",
|
||||
"message": "Name should be in camelCase",
|
||||
"userFriendlyMessage": "Name should be in camelCase",
|
||||
"value": "a a",
|
||||
"value": "targetA a",
|
||||
},
|
||||
{
|
||||
"code": "INVALID_FIELD_INPUT",
|
||||
"message": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters",
|
||||
"userFriendlyMessage": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters",
|
||||
"value": "a a",
|
||||
"value": "targetA a",
|
||||
},
|
||||
{
|
||||
"code": "OBJECT_METADATA_NOT_FOUND",
|
||||
@@ -6568,7 +6568,7 @@ exports[`Object metadata creation should fail v2 when nameSingular contains only
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"id": Any<String>,
|
||||
"name": "a a",
|
||||
"name": "targetA a",
|
||||
"objectMetadataId": Any<String>,
|
||||
},
|
||||
"status": "fail",
|
||||
@@ -7240,18 +7240,6 @@ exports[`Object metadata creation should fail v2 when nameSingular contains only
|
||||
},
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "INVALID_FIELD_INPUT",
|
||||
"message": "Name is too short",
|
||||
"userFriendlyMessage": "Name is too short",
|
||||
"value": "",
|
||||
},
|
||||
{
|
||||
"code": "INVALID_FIELD_INPUT",
|
||||
"message": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters",
|
||||
"userFriendlyMessage": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters",
|
||||
"value": "",
|
||||
},
|
||||
{
|
||||
"code": "OBJECT_METADATA_NOT_FOUND",
|
||||
"message": "Relation target object metadata not found",
|
||||
@@ -7260,7 +7248,7 @@ exports[`Object metadata creation should fail v2 when nameSingular contains only
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"id": Any<String>,
|
||||
"name": "",
|
||||
"name": "target",
|
||||
"objectMetadataId": Any<String>,
|
||||
},
|
||||
"status": "fail",
|
||||
@@ -7932,11 +7920,17 @@ exports[`Object metadata creation should fail v2 when nameSingular has invalid c
|
||||
},
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "INVALID_FIELD_INPUT",
|
||||
"message": "Name should be in camelCase",
|
||||
"userFriendlyMessage": "Name should be in camelCase",
|
||||
"value": "targetΜ",
|
||||
},
|
||||
{
|
||||
"code": "INVALID_FIELD_INPUT",
|
||||
"message": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters",
|
||||
"userFriendlyMessage": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters",
|
||||
"value": "μ",
|
||||
"value": "targetΜ",
|
||||
},
|
||||
{
|
||||
"code": "OBJECT_METADATA_NOT_FOUND",
|
||||
@@ -7946,7 +7940,7 @@ exports[`Object metadata creation should fail v2 when nameSingular has invalid c
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"id": Any<String>,
|
||||
"name": "μ",
|
||||
"name": "targetΜ",
|
||||
"objectMetadataId": Any<String>,
|
||||
},
|
||||
"status": "fail",
|
||||
@@ -8588,12 +8582,6 @@ exports[`Object metadata creation should fail v2 when nameSingular is a reserved
|
||||
},
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "INVALID_FIELD_INPUT",
|
||||
"message": "This name is reserved. Use a different name or the system will add "Custom" suffix.",
|
||||
"userFriendlyMessage": "This name is reserved. Use a different name or the system will add "Custom" suffix.",
|
||||
"value": "user",
|
||||
},
|
||||
{
|
||||
"code": "OBJECT_METADATA_NOT_FOUND",
|
||||
"message": "Relation target object metadata not found",
|
||||
@@ -8602,7 +8590,7 @@ exports[`Object metadata creation should fail v2 when nameSingular is a reserved
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"id": Any<String>,
|
||||
"name": "user",
|
||||
"name": "targetUser",
|
||||
"objectMetadataId": Any<String>,
|
||||
},
|
||||
"status": "fail",
|
||||
@@ -9259,13 +9247,13 @@ exports[`Object metadata creation should fail v2 when nameSingular is not camelC
|
||||
"code": "INVALID_FIELD_INPUT",
|
||||
"message": "Name should be in camelCase",
|
||||
"userFriendlyMessage": "Name should be in camelCase",
|
||||
"value": "Not_Camel_Case",
|
||||
"value": "targetNot_Camel_Case",
|
||||
},
|
||||
{
|
||||
"code": "INVALID_FIELD_INPUT",
|
||||
"message": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters",
|
||||
"userFriendlyMessage": "Name is not valid: it must start with lowercase letter and contain only alphanumeric letters",
|
||||
"value": "Not_Camel_Case",
|
||||
"value": "targetNot_Camel_Case",
|
||||
},
|
||||
{
|
||||
"code": "OBJECT_METADATA_NOT_FOUND",
|
||||
@@ -9275,7 +9263,7 @@ exports[`Object metadata creation should fail v2 when nameSingular is not camelC
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"id": Any<String>,
|
||||
"name": "Not_Camel_Case",
|
||||
"name": "targetNot_Camel_Case",
|
||||
"objectMetadataId": Any<String>,
|
||||
},
|
||||
"status": "fail",
|
||||
@@ -9955,7 +9943,7 @@ exports[`Object metadata creation should fail v2 when names are identical 1`] =
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"id": Any<String>,
|
||||
"name": "fooBar",
|
||||
"name": "targetFooBar",
|
||||
"objectMetadataId": Any<String>,
|
||||
},
|
||||
"status": "fail",
|
||||
@@ -10581,7 +10569,7 @@ exports[`Object metadata creation should fail v2 when names with whitespaces res
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"id": Any<String>,
|
||||
"name": "fooBar",
|
||||
"name": "targetFooBar",
|
||||
"objectMetadataId": Any<String>,
|
||||
},
|
||||
"status": "fail",
|
||||
|
||||
+13
-5
@@ -5,6 +5,7 @@ import { findManyObjectMetadataQueryFactory } from 'test/integration/metadata/su
|
||||
import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util';
|
||||
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { capitalize } from 'twenty-shared/utils';
|
||||
|
||||
describe('Custom object renaming', () => {
|
||||
let listingObjectId = '';
|
||||
@@ -66,7 +67,9 @@ describe('Custom object renaming', () => {
|
||||
standardObjectRelationsMap[relation].objectMetadataId =
|
||||
standardObjects.body.data.objects.edges.find(
|
||||
// @ts-expect-error legacy noImplicitAny
|
||||
(object) => object.node.nameSingular === relation,
|
||||
(object) =>
|
||||
object.node.nameSingular === relation ||
|
||||
object.node.nameSingular === `target${relation}`,
|
||||
).node.id;
|
||||
});
|
||||
};
|
||||
@@ -110,21 +113,26 @@ describe('Custom object renaming', () => {
|
||||
.filter(
|
||||
// @ts-expect-error legacy noImplicitAny
|
||||
(field) =>
|
||||
field.node.name === `${CUSTOM_OBJECT.nameSingular}` &&
|
||||
field.node.type === FieldMetadataType.RELATION,
|
||||
(field.node.name === `${CUSTOM_OBJECT.nameSingular}` &&
|
||||
FieldMetadataType.RELATION) ||
|
||||
(field.node.name ===
|
||||
`target${capitalize(CUSTOM_OBJECT.nameSingular)}` &&
|
||||
FieldMetadataType.MORPH_RELATION),
|
||||
)
|
||||
// @ts-expect-error legacy noImplicitAny
|
||||
.map((field) => field.node);
|
||||
|
||||
STANDARD_OBJECT_RELATIONS.forEach((relation) => {
|
||||
// relation field
|
||||
const relationFieldMetadataId = relationFieldsMetadataForListing.find(
|
||||
const relationFieldMetadata = relationFieldsMetadataForListing.find(
|
||||
// @ts-expect-error legacy noImplicitAny
|
||||
(field) =>
|
||||
field.object.id ===
|
||||
// @ts-expect-error legacy noImplicitAny
|
||||
standardObjectRelationsMap[relation].objectMetadataId,
|
||||
).id;
|
||||
);
|
||||
|
||||
const relationFieldMetadataId = relationFieldMetadata?.id;
|
||||
|
||||
expect(relationFieldMetadataId).not.toBeUndefined();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user