Refactor seed to use twenty-standard application (#16598)

# Introduction
In this pull-request we introduce a service dedicated to the
twenty-standard app installation, we will later be able to re-use
existing logic to be more generic and allow any app installation.
For the moment sticking to this usage
https://github.com/twentyhq/core-team-issues/issues/1995

## Encountered issues
- We decided not to migrate deprecated fields ( also they will become
custom field for any existing workspace having them in the future )
- duplicate criteria
- wrong search index declaration
- forgotten isSearchable
- Attachement seed
- Restored standardId

## Note
For the moment we're still searching through standardId for code that
run on both existing and new workspaces.
For code running on new workspace exclusively we're searching using
universalIdentifier
We will standardize universalIdentifier usage later when we've migratred
all the existing workspaces

## Workspace creation
Will handle workspace creation the same way in another PR

Related https://github.com/twentyhq/twenty/pull/15065

## TODO
- [ ] Double all frontend hardcoded queries to not refer to deprecated
fields especially attachments
This commit is contained in:
Paul Rastoin
2025-12-18 17:08:55 +01:00
committed by GitHub
parent d7fc9387a0
commit 38785cd4e9
74 changed files with 897 additions and 726 deletions
@@ -4,7 +4,7 @@ import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
import { DataSource, Repository } from 'typeorm';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { FieldPermissionService } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.service';
@@ -13,7 +13,6 @@ import { RoleTargetService } from 'src/engine/metadata-modules/role-target/servi
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { RoleService } from 'src/engine/metadata-modules/role/role.service';
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import {
SEED_APPLE_WORKSPACE_ID,
SEED_YCOMBINATOR_WORKSPACE_ID,
@@ -23,7 +22,7 @@ import {
USER_WORKSPACE_DATA_SEED_IDS,
} from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-user-workspaces.util';
import { API_KEY_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/api-key-data-seeds.constant';
import { ADMIN_ROLE } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-roles/roles/admin-role';
import { STANDARD_ROLE } from 'src/engine/workspace-manager/twenty-standard-application/constants/standard-role.constant';
@Injectable()
export class DevSeederPermissionsService {
@@ -37,7 +36,6 @@ export class DevSeederPermissionsService {
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
@InjectRepository(RoleEntity)
private readonly roleRepository: Repository<RoleEntity>,
private readonly workspaceCacheService: WorkspaceCacheService,
private readonly fieldPermissionService: FieldPermissionService,
private readonly roleTargetService: RoleTargetService,
@InjectDataSource()
@@ -45,15 +43,15 @@ export class DevSeederPermissionsService {
) {}
public async initPermissions({
twentyStandardApplication,
twentyStandardFlatApplication,
workspaceId,
}: {
workspaceId: string;
twentyStandardApplication: ApplicationEntity;
twentyStandardFlatApplication: FlatApplication;
}) {
const adminRole = await this.roleRepository.findOne({
where: {
standardId: ADMIN_ROLE.standardId,
universalIdentifier: STANDARD_ROLE.admin.universalIdentifier,
workspaceId,
},
});
@@ -64,21 +62,15 @@ export class DevSeederPermissionsService {
);
}
try {
await this.roleTargetService.create({
createRoleTargetInput: {
roleId: adminRole.id,
targetId: API_KEY_DATA_SEED_IDS.ID_1,
targetMetadataForeignKey: 'apiKeyId',
applicationId: twentyStandardApplication.id,
},
workspaceId,
});
} catch (error) {
this.logger.error(
`Could not assign role to test API key: ${error.message}`,
);
}
await this.roleTargetService.create({
createRoleTargetInput: {
roleId: adminRole.id,
targetId: API_KEY_DATA_SEED_IDS.ID_1,
targetMetadataForeignKey: 'apiKeyId',
applicationId: twentyStandardFlatApplication.id,
},
workspaceId,
});
let adminUserWorkspaceId: string | undefined;
let memberUserWorkspaceIds: string[] = [];
@@ -96,7 +88,7 @@ export class DevSeederPermissionsService {
const guestRole = await this.roleService.createGuestRole({
workspaceId,
applicationId: twentyStandardApplication.id,
applicationId: twentyStandardFlatApplication.id,
});
await this.userRoleService.assignRoleToManyUserWorkspace({
@@ -107,7 +99,7 @@ export class DevSeederPermissionsService {
const limitedRole = await this.createLimitedRoleForSeedWorkspace({
workspaceId,
applicationId: twentyStandardApplication.id,
applicationId: twentyStandardFlatApplication.id,
});
await this.userRoleService.assignRoleToManyUserWorkspace({
@@ -124,17 +116,21 @@ export class DevSeederPermissionsService {
];
}
if (adminUserWorkspaceId) {
await this.userRoleService.assignRoleToManyUserWorkspace({
workspaceId,
userWorkspaceIds: [adminUserWorkspaceId],
roleId: adminRole.id,
});
if (!adminUserWorkspaceId) {
throw new Error(
'Should never occur, no eligible user workspace for admin has been found',
);
}
await this.userRoleService.assignRoleToManyUserWorkspace({
workspaceId,
userWorkspaceIds: [adminUserWorkspaceId],
roleId: adminRole.id,
});
const memberRole = await this.roleService.createMemberRole({
workspaceId,
applicationId: twentyStandardApplication.id,
applicationId: twentyStandardFlatApplication.id,
});
await this.coreDataSource
@@ -11,10 +11,7 @@ type AttachmentDataSeed = {
id: string;
name: string;
fullPath: string;
type: string;
fileCategory?: string;
// Deprecated: Use createdBy instead
authorId: string | null;
fileCategory: string;
createdBySource: string;
createdByWorkspaceMemberId: string;
createdByName: string;
@@ -29,9 +26,7 @@ export const ATTACHMENT_DATA_SEED_COLUMNS: (keyof AttachmentDataSeed)[] = [
'id',
'name',
'fullPath',
'type',
'fileCategory',
'authorId', // Deprecated: kept for backward compatibility during migration
'createdBySource',
'createdByWorkspaceMemberId',
'createdByName',
@@ -71,152 +66,128 @@ const FILE_NAME_VARIATIONS = [
// Documents
{
name: 'Service Agreement.pdf',
type: 'TextDocument',
fileCategory: 'TEXT_DOCUMENT',
pathIndex: 0,
},
{
name: 'NDA Document.pdf',
type: 'TextDocument',
fileCategory: 'TEXT_DOCUMENT',
pathIndex: 0,
},
{
name: 'Project Proposal.pdf',
type: 'TextDocument',
fileCategory: 'TEXT_DOCUMENT',
pathIndex: 0,
},
{
name: 'Invoice Q1 2024.pdf',
type: 'TextDocument',
fileCategory: 'TEXT_DOCUMENT',
pathIndex: 0,
},
{
name: 'Meeting Notes.pdf',
type: 'TextDocument',
fileCategory: 'TEXT_DOCUMENT',
pathIndex: 0,
},
{
name: 'Report Final.pdf',
type: 'TextDocument',
fileCategory: 'TEXT_DOCUMENT',
pathIndex: 0,
},
{
name: 'Contract Signed.pdf',
type: 'TextDocument',
fileCategory: 'TEXT_DOCUMENT',
pathIndex: 0,
},
// Spreadsheets
{
name: 'Financial Forecast.xlsx',
type: 'Spreadsheet',
fileCategory: 'SPREADSHEET',
pathIndex: 1,
},
{
name: 'Sales Report Q4.xlsx',
type: 'Spreadsheet',
fileCategory: 'SPREADSHEET',
pathIndex: 1,
},
{
name: 'Team Roster.xlsx',
type: 'Spreadsheet',
fileCategory: 'SPREADSHEET',
pathIndex: 1,
},
{
name: 'Expense Report.xlsx',
type: 'Spreadsheet',
fileCategory: 'SPREADSHEET',
pathIndex: 1,
},
{
name: 'Inventory List.xlsx',
type: 'Spreadsheet',
fileCategory: 'SPREADSHEET',
pathIndex: 1,
},
{
name: 'Data Export.csv',
type: 'Spreadsheet',
fileCategory: 'SPREADSHEET',
pathIndex: 1,
},
// Presentations
{
name: 'Pitch Deck.pptx',
type: 'Presentation',
fileCategory: 'PRESENTATION',
pathIndex: 2,
},
{
name: 'Q4 Results.pptx',
type: 'Presentation',
fileCategory: 'PRESENTATION',
pathIndex: 2,
},
{
name: 'Roadmap 2024.pptx',
type: 'Presentation',
fileCategory: 'PRESENTATION',
pathIndex: 2,
},
{
name: 'Company Overview.pptx',
type: 'Presentation',
fileCategory: 'PRESENTATION',
pathIndex: 2,
},
{
name: 'Training Materials.pptx',
type: 'Presentation',
fileCategory: 'PRESENTATION',
pathIndex: 2,
},
// Images
{
name: 'Company Logo.png',
type: 'Image',
fileCategory: 'IMAGE',
pathIndex: 3,
},
{
name: 'Product Photo.jpg',
type: 'Image',
fileCategory: 'IMAGE',
pathIndex: 3,
},
{ name: 'Diagram.png', type: 'Image', fileCategory: 'IMAGE', pathIndex: 3 },
{ name: 'Wireframe.png', type: 'Image', fileCategory: 'IMAGE', pathIndex: 3 },
{ name: 'Diagram.png', fileCategory: 'IMAGE', pathIndex: 3 },
{ name: 'Wireframe.png', fileCategory: 'IMAGE', pathIndex: 3 },
{
name: 'Mockup Design.png',
type: 'Image',
fileCategory: 'IMAGE',
pathIndex: 3,
},
{ name: 'Headshot.jpg', type: 'Image', fileCategory: 'IMAGE', pathIndex: 3 },
{ name: 'Headshot.jpg', fileCategory: 'IMAGE', pathIndex: 3 },
// Archives
{
name: 'Project Files.zip',
type: 'Archive',
fileCategory: 'ARCHIVE',
pathIndex: 4,
},
{
name: 'Backup Data.zip',
type: 'Archive',
fileCategory: 'ARCHIVE',
pathIndex: 4,
},
{
name: 'Source Code.zip',
type: 'Archive',
fileCategory: 'ARCHIVE',
pathIndex: 4,
},
@@ -276,10 +247,7 @@ const GENERATE_ATTACHMENT_SEEDS = (): AttachmentDataSeed[] => {
id: ATTACHMENT_DATA_SEED_IDS[`ID_${INDEX}`],
name: NAME_VARIATION.name,
fullPath: FILE_PATH,
type: NAME_VARIATION.type,
fileCategory: NAME_VARIATION.fileCategory,
// Deprecated: Use createdBy fields instead
authorId: WORKSPACE_MEMBER_DATA_SEED_IDS.TIM,
createdBySource: FieldActorSource.MANUAL,
createdByWorkspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.TIM,
createdByName: 'Tim A',
@@ -24,7 +24,8 @@ import { DevSeederDataService } from 'src/engine/workspace-manager/dev-seeder/da
import { TimelineActivitySeederService } from 'src/engine/workspace-manager/dev-seeder/data/services/timeline-activity-seeder.service';
import { DevSeederMetadataService } from 'src/engine/workspace-manager/dev-seeder/metadata/services/dev-seeder-metadata.service';
import { DevSeederService } from 'src/engine/workspace-manager/dev-seeder/services/dev-seeder.service';
import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.module';
import { TwentyStandardApplicationModule } from 'src/engine/workspace-manager/twenty-standard-application/twenty-standard-application.module';
import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-v2.module';
@Module({
imports: [
@@ -41,11 +42,12 @@ import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/worksp
ApplicationModule,
FeatureFlagModule,
FileStorageModule,
WorkspaceSyncMetadataModule,
TypeOrmModule.forFeature([WorkspaceEntity, ObjectMetadataEntity]),
ObjectPermissionModule,
WorkspaceManyOrAllFlatEntityMapsCacheModule,
WorkspaceCacheModule,
WorkspaceMigrationV2Module,
TwentyStandardApplicationModule,
],
exports: [DevSeederService],
providers: [
@@ -1,10 +1,7 @@
import { Injectable } from '@nestjs/common';
import { InjectDataSource } from '@nestjs/typeorm';
import { isDefined } from 'twenty-shared/utils';
import { DataSource } from 'typeorm';
import { FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { type DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
import { CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata.service';
@@ -25,7 +22,6 @@ import { ROCKET_CUSTOM_OBJECT_SEED } from 'src/engine/workspace-manager/dev-seed
import { SURVEY_RESULT_CUSTOM_OBJECT_SEED } from 'src/engine/workspace-manager/dev-seeder/metadata/custom-objects/constants/survey-results-object-seed.constant';
import { type FieldMetadataSeed } from 'src/engine/workspace-manager/dev-seeder/metadata/types/field-metadata-seed.type';
import { type ObjectMetadataSeed } from 'src/engine/workspace-manager/dev-seeder/metadata/types/object-metadata-seed.type';
import { prefillCoreViews } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-core-views';
@Injectable()
export class DevSeederMetadataService {
@@ -33,8 +29,6 @@ export class DevSeederMetadataService {
private readonly objectMetadataService: ObjectMetadataService,
private readonly fieldMetadataService: FieldMetadataService,
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
@InjectDataSource()
private readonly coreDataSource: DataSource,
) {}
private readonly workspaceConfigs: Record<
@@ -85,13 +79,9 @@ export class DevSeederMetadataService {
public async seed({
dataSourceMetadata,
workspaceId,
featureFlags,
twentyStandardFlatApplication,
}: {
dataSourceMetadata: DataSourceEntity;
workspaceId: string;
twentyStandardFlatApplication: FlatApplication;
featureFlags?: Record<string, boolean>;
}) {
const config = this.workspaceConfigs[workspaceId];
@@ -125,13 +115,6 @@ export class DevSeederMetadataService {
fieldMetadataSeeds: fieldConfig.seeds,
});
}
await this.seedCoreViews({
workspaceId,
dataSourceMetadata,
featureFlags,
twentyStandardFlatApplication,
});
}
private async seedCustomObject({
@@ -182,36 +165,6 @@ export class DevSeederMetadataService {
});
}
private async seedCoreViews({
workspaceId,
dataSourceMetadata,
featureFlags,
twentyStandardFlatApplication,
}: {
workspaceId: string;
dataSourceMetadata: DataSourceEntity;
featureFlags?: Record<string, boolean>;
twentyStandardFlatApplication: FlatApplication;
}): Promise<void> {
const { flatObjectMetadataMaps, flatFieldMetadataMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatObjectMetadataMaps', 'flatFieldMetadataMaps'],
},
);
await prefillCoreViews({
coreDataSource: this.coreDataSource,
workspaceId,
flatObjectMetadataMaps,
flatFieldMetadataMaps,
workspaceSchemaName: dataSourceMetadata.schema,
featureFlags,
twentyStandardFlatApplication,
});
}
public async seedRelations({ workspaceId }: { workspaceId: string }) {
const config = this.workspaceConfigs[workspaceId];
@@ -1,12 +1,10 @@
import { Injectable } from '@nestjs/common';
import { InjectDataSource } from '@nestjs/typeorm';
import { isDefined } from 'twenty-shared/utils';
import { DataSource } from 'typeorm';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
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 { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
@@ -21,8 +19,7 @@ import { seedPageLayoutWidgets } from 'src/engine/workspace-manager/dev-seeder/c
import { seedPageLayouts } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-page-layouts.util';
import { DevSeederDataService } from 'src/engine/workspace-manager/dev-seeder/data/services/dev-seeder-data.service';
import { DevSeederMetadataService } from 'src/engine/workspace-manager/dev-seeder/metadata/services/dev-seeder-metadata.service';
import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications';
import { WorkspaceSyncMetadataService } from 'src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.service';
import { TwentyStandardApplicationService } from 'src/engine/workspace-manager/twenty-standard-application/services/twenty-standard-application.service';
@Injectable()
export class DevSeederService {
@@ -31,8 +28,7 @@ export class DevSeederService {
private readonly twentyConfigService: TwentyConfigService,
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
private readonly dataSourceService: DataSourceService,
private readonly featureFlagService: FeatureFlagService,
private readonly workspaceSyncMetadataService: WorkspaceSyncMetadataService,
private readonly twentyStandardApplicationService: TwentyStandardApplicationService,
private readonly devSeederMetadataService: DevSeederMetadataService,
private readonly devSeederPermissionsService: DevSeederPermissionsService,
private readonly devSeederDataService: DevSeederDataService,
@@ -70,36 +66,22 @@ export class DevSeederService {
schemaName,
);
const twentyStandardApplication =
await this.applicationService.findByUniversalIdentifier({
workspaceId,
universalIdentifier: TWENTY_STANDARD_APPLICATION.universalIdentifier,
});
if (!isDefined(twentyStandardApplication)) {
throw new Error(
'Seeder failed to find twenty standard application, should never occur',
);
}
const { twentyStandardFlatApplication, workspaceCustomFlatApplication } =
const { workspaceCustomFlatApplication, twentyStandardFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{
workspaceId,
},
);
await this.workspaceSyncMetadataService.synchronize({
workspaceId: workspaceId,
dataSourceId: dataSourceMetadata.id,
featureFlags: featureFlagsMap,
});
await this.twentyStandardApplicationService.synchronizeTwentyStandardApplicationOrThrow(
{
workspaceId,
},
);
await this.devSeederMetadataService.seed({
dataSourceMetadata,
workspaceId,
featureFlags: featureFlagsMap,
twentyStandardFlatApplication,
});
await this.devSeederMetadataService.seedRelations({
@@ -108,7 +90,7 @@ export class DevSeederService {
await this.devSeederPermissionsService.initPermissions({
workspaceId,
twentyStandardApplication,
twentyStandardFlatApplication,
});
await seedPageLayouts(