Files
twenty/packages/twenty-server/src/database/commands/data-seed-dev-workspace.command.ts
Baptiste Devessier 729c990546 Activate/Deactivate workflow and Discard Draft (#7022)
## Setup

This PR can be tested only if some feature flags have specific values:

- `IsWorkflowEnabled` equals `true`
- `IsQueryRunnerTwentyORMEnabled` equals `false`

These feature flags weren't committed to don't break other branches.

## What this PR brings

- Display buttons to activate and deactivate a workflow version and a
button to discard the current draft version. I also scaffolded a "Test"
button, which doesn't do anything for now.
- Wired the activate, deactivate and discard draft buttons to the
backend.
- Made it possible to "edit" active and deactivated versions by
automatically creating a new draft version when the user tries to edit
the version.
- Hide the "Discard Draft", button if the current version is not a draft
or is the first version ever created.
- On the backend, don't consider discarded drafts when checking if a new
draft version can be created.
- On the backend, disallow deleting the first created workflow version.
Otherwise, we will end up with a blank canvas in the front end, and it
will be impossible to recover from it.
- On the backend, disallow running deactivation steps if the workflow
version is not currently active. Previously, we were throwing, which is
unnecessary as it's a valid case.

## Spotted bugs that we must dive into

### Duplicate workflow versions in Apollo cache


https://github.com/user-attachments/assets/7cfffd06-11e0-417a-8da0-f9a5f43b84e2

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2024-09-25 18:09:31 +02:00

299 lines
11 KiB
TypeScript

import { Logger } from '@nestjs/common';
import { Command, CommandRunner } from 'nest-commander';
import { EntityManager } from 'typeorm';
import { seedCoreSchema } from 'src/database/typeorm-seeds/core';
import {
SEED_APPLE_WORKSPACE_ID,
SEED_TWENTY_WORKSPACE_ID,
} from 'src/database/typeorm-seeds/core/workspaces';
import {
getDevSeedCompanyCustomFields,
getDevSeedPeopleCustomFields,
} from 'src/database/typeorm-seeds/metadata/fieldsMetadata';
import { getDevSeedCustomObjects } from 'src/database/typeorm-seeds/metadata/objectsMetadata';
import { seedCalendarChannels } from 'src/database/typeorm-seeds/workspace/calendar-channel';
import { seedCalendarChannelEventAssociations } from 'src/database/typeorm-seeds/workspace/calendar-channel-event-association';
import { seedCalendarEventParticipants } from 'src/database/typeorm-seeds/workspace/calendar-event-participants';
import { seedCalendarEvents } from 'src/database/typeorm-seeds/workspace/calendar-events';
import { seedCompanies } from 'src/database/typeorm-seeds/workspace/companies';
import { seedConnectedAccount } from 'src/database/typeorm-seeds/workspace/connected-account';
import { seedWorkspaceFavorites } from 'src/database/typeorm-seeds/workspace/favorites';
import { seedMessageChannelMessageAssociation } from 'src/database/typeorm-seeds/workspace/message-channel-message-associations';
import { seedMessageChannel } from 'src/database/typeorm-seeds/workspace/message-channels';
import { seedMessageParticipant } from 'src/database/typeorm-seeds/workspace/message-participants';
import { seedMessageThreadSubscribers } from 'src/database/typeorm-seeds/workspace/message-thread-subscribers';
import { seedMessageThread } from 'src/database/typeorm-seeds/workspace/message-threads';
import { seedMessage } from 'src/database/typeorm-seeds/workspace/messages';
import { seedOpportunity } from 'src/database/typeorm-seeds/workspace/opportunities';
import { seedPeople } from 'src/database/typeorm-seeds/workspace/people';
import { seedWorkspaceMember } from 'src/database/typeorm-seeds/workspace/workspace-members';
import { rawDataSource } from 'src/database/typeorm/raw/raw.datasource';
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator';
import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service';
import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum';
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 { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/field-metadata.service';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
import { viewPrefillData } from 'src/engine/workspace-manager/standard-objects-prefill-data/view';
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
import { WorkspaceSyncMetadataService } from 'src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.service';
// TODO: implement dry-run
@Command({
name: 'workspace:seed:dev',
description:
'Seed workspace with initial data. This command is intended for development only.',
})
export class DataSeedWorkspaceCommand extends CommandRunner {
workspaceIds = [SEED_APPLE_WORKSPACE_ID, SEED_TWENTY_WORKSPACE_ID];
private readonly logger = new Logger(DataSeedWorkspaceCommand.name);
constructor(
private readonly dataSourceService: DataSourceService,
private readonly typeORMService: TypeORMService,
private readonly workspaceSyncMetadataService: WorkspaceSyncMetadataService,
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
private readonly fieldMetadataService: FieldMetadataService,
private readonly objectMetadataService: ObjectMetadataService,
@InjectCacheStorage(CacheStorageNamespace.EngineWorkspace)
private readonly workspaceSchemaCache: CacheStorageService,
private readonly featureFlagService: FeatureFlagService,
) {
super();
}
async run(): Promise<void> {
try {
for (const workspaceId of this.workspaceIds) {
await this.workspaceSchemaCache.flush();
await rawDataSource.initialize();
await seedCoreSchema(rawDataSource, workspaceId);
await rawDataSource.destroy();
const schemaName =
await this.workspaceDataSourceService.createWorkspaceDBSchema(
workspaceId,
);
const dataSourceMetadata =
await this.dataSourceService.createDataSourceMetadata(
workspaceId,
schemaName,
);
await this.workspaceSyncMetadataService.synchronize({
workspaceId: workspaceId,
dataSourceId: dataSourceMetadata.id,
});
}
} catch (error) {
this.logger.error(error);
return;
}
for (const workspaceId of this.workspaceIds) {
const dataSourceMetadata =
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
workspaceId,
);
const workspaceDataSource =
await this.typeORMService.connectToDataSource(dataSourceMetadata);
if (!workspaceDataSource) {
throw new Error('Could not connect to workspace data source');
}
try {
const objectMetadata =
await this.objectMetadataService.findManyWithinWorkspace(workspaceId);
const objectMetadataMap = objectMetadata.reduce((acc, object) => {
acc[object.standardId ?? ''] = {
id: object.id,
fields: object.fields.reduce((acc, field) => {
acc[field.standardId ?? ''] = field.id;
return acc;
}, {}),
};
return acc;
}, {});
const isMessageThreadSubscriberEnabled =
await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IsMessageThreadSubscriberEnabled,
workspaceId,
);
const isWorkflowEnabled =
await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IsWorkflowEnabled,
workspaceId,
);
await this.seedCompanyCustomFields(
objectMetadataMap[STANDARD_OBJECT_IDS.company],
workspaceId,
);
await this.seedPeopleCustomFields(
objectMetadataMap[STANDARD_OBJECT_IDS.person],
workspaceId,
);
await this.seedCustomObjects(workspaceId, dataSourceMetadata.id);
await workspaceDataSource.transaction(
async (entityManager: EntityManager) => {
await seedCompanies(entityManager, dataSourceMetadata.schema);
await seedPeople(entityManager, dataSourceMetadata.schema);
await seedOpportunity(entityManager, dataSourceMetadata.schema);
await seedWorkspaceMember(
entityManager,
dataSourceMetadata.schema,
workspaceId,
);
if (workspaceId === SEED_APPLE_WORKSPACE_ID) {
await seedMessageThread(entityManager, dataSourceMetadata.schema);
await seedConnectedAccount(
entityManager,
dataSourceMetadata.schema,
);
if (isMessageThreadSubscriberEnabled) {
await seedMessageThreadSubscribers(
entityManager,
dataSourceMetadata.schema,
);
}
await seedMessage(entityManager, dataSourceMetadata.schema);
await seedMessageChannel(
entityManager,
dataSourceMetadata.schema,
);
await seedMessageChannelMessageAssociation(
entityManager,
dataSourceMetadata.schema,
);
await seedMessageParticipant(
entityManager,
dataSourceMetadata.schema,
);
await seedCalendarEvents(
entityManager,
dataSourceMetadata.schema,
);
await seedCalendarChannels(
entityManager,
dataSourceMetadata.schema,
);
await seedCalendarChannelEventAssociations(
entityManager,
dataSourceMetadata.schema,
);
await seedCalendarEventParticipants(
entityManager,
dataSourceMetadata.schema,
);
}
const viewDefinitionsWithId = await viewPrefillData(
entityManager,
dataSourceMetadata.schema,
objectMetadataMap,
isWorkflowEnabled,
);
await seedWorkspaceFavorites(
viewDefinitionsWithId
.filter((view) => view.key === 'INDEX')
.map((view) => view.id),
entityManager,
dataSourceMetadata.schema,
);
},
);
} catch (error) {
this.logger.error(error);
}
await this.typeORMService.disconnectFromDataSource(dataSourceMetadata.id);
}
}
async seedCompanyCustomFields(
companyObjectMetadata: ObjectMetadataEntity,
workspaceId: string,
) {
const companyObjectMetadataId = companyObjectMetadata?.id;
if (!companyObjectMetadataId) {
throw new Error(
`Company object metadata not found for workspace ${workspaceId}, can't seed custom fields`,
);
}
const DEV_SEED_COMPANY_CUSTOM_FIELDS = getDevSeedCompanyCustomFields(
companyObjectMetadataId,
workspaceId,
);
for (const customField of DEV_SEED_COMPANY_CUSTOM_FIELDS) {
// TODO: Use createMany once implemented for better performances
await this.fieldMetadataService.createOne({
...customField,
isCustom: true,
});
}
}
async seedPeopleCustomFields(
personObjectMetadata: ObjectMetadataEntity,
workspaceId: string,
) {
const personObjectMetadataId = personObjectMetadata?.id;
if (!personObjectMetadataId) {
throw new Error(
`Person object metadata not found for workspace ${workspaceId}, can't seed custom fields`,
);
}
const DEV_SEED_PERSON_CUSTOM_FIELDS = getDevSeedPeopleCustomFields(
personObjectMetadataId,
workspaceId,
);
for (const customField of DEV_SEED_PERSON_CUSTOM_FIELDS) {
await this.fieldMetadataService.createOne({
...customField,
isCustom: true,
});
}
}
async seedCustomObjects(workspaceId: string, dataSourceId: string) {
const devSeedCustomObjects = getDevSeedCustomObjects(
workspaceId,
dataSourceId,
);
for (const customObject of devSeedCustomObjects) {
await this.objectMetadataService.createOne(customObject);
}
}
}