Metadata api create entity in workspace custom app (#15911)

# Introduction
Cleaner and fewer scope version of
https://github.com/twentyhq/twenty/pull/15745 ( removed sync-metadata
hack through, too ambitious migration and upgrade )

Please note that this PR won't have any interaction with the existing
sync-metadata
Which mean that the sync metadata does not update the standard entities
applicationId and universalIdentifier, and it won't we will deprecate it
on favor of a workspace migration aka twenty-standard app installation

## API Metadata
Any operation going through the api metadata nows automatically scope
the related entity to the workspace custom application instance. (
optionally passing an applicationId to allow current hacky implem of app
sync service )

We need to either ignore the tests or remove the cli status check from
the blocking status badges for a PR to be merged

## New workspace
Already handled in previous
https://github.com/twentyhq/twenty/pull/15625, when a workspace is
created it gets created a twenty standard and custom workspace instance

All his views and permissions will be prefilled to the its twenty
standard app instance with a specific universalIdentifier

## New universalIdentifier
At the contrary as before with standardIds, universalIdentifier are
unique for a given workspace
This means that createdAt field of both object company and opportunity
will have a unique universalIdentifier whereas they share the same
standardId

## FlatApplication
Introduced the flatApplication and cache. Will migrate existing
`MetadataName` to be `SyncableMetadataName` in a following PR

## What's next
Next we will describe a twenty standard app configuration as json that
will be used to generate a workspace migration that will be run instead
of the sync metadata, in a nutshell we aim to deprecated the sync
metadata
So we can standardize any entity to have a non nullable applicationId
and universalIdentifier

## Upgrade command
Introduced an upgrade command that will create a custom workspace
instance for any workspace that do not have one in order to align with
the new behavior when creating a new workspace
This commit is contained in:
Paul Rastoin
2025-11-23 22:35:17 +01:00
committed by GitHub
parent 4848bc03f3
commit f9ab09c404
99 changed files with 3458 additions and 730 deletions
@@ -9,6 +9,7 @@ import {
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
type RunOnWorkspaceArgs,
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
@@ -37,6 +38,7 @@ export class SeedDashboardViewCommand extends ActiveOrSuspendedWorkspacesMigrati
private readonly dataSourceRepository: Repository<DataSourceEntity>,
@InjectRepository(ViewEntity)
private readonly viewRepository: Repository<ViewEntity>,
private readonly applicationService: ApplicationService,
) {
super(workspaceRepository, twentyORMGlobalManager);
}
@@ -59,7 +61,17 @@ export class SeedDashboardViewCommand extends ActiveOrSuspendedWorkspacesMigrati
);
}
const views = [dashboardsAllView([dashboardObjectMetadata], true)];
const { twentyStandardFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
);
const views = [
dashboardsAllView({
objectMetadataItems: [dashboardObjectMetadata],
useCoreNaming: true,
twentyStandardFlatApplication,
}),
];
const schema = await this.dataSourceRepository.findOne({
where: {
@@ -99,7 +111,12 @@ export class SeedDashboardViewCommand extends ActiveOrSuspendedWorkspacesMigrati
await queryRunner.connect();
const createdViews = await createCoreViews(queryRunner, workspaceId, views);
const createdViews = await createCoreViews(
queryRunner,
workspaceId,
views,
twentyStandardFlatApplication,
);
await prefillWorkspaceFavorites(
createdViews.map((view) => view.id),
@@ -11,9 +11,12 @@ import { MigrateAttachmentTypeToFileCategoryCommand } from 'src/database/command
import { MigrateChannelPartialFullSyncStagesCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-migrate-channel-partial-full-sync-stages.command';
import { RegenerateSearchVectorsCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-regenerate-search-vectors.command';
import { SeedDashboardViewCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-seed-dashboard-view.command';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.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';
@@ -30,10 +33,13 @@ import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/
IndexMetadataEntity,
ViewEntity,
DataSourceEntity,
ApplicationEntity,
]),
WorkspaceSchemaManagerModule,
WorkspaceCacheStorageModule,
ObjectMetadataModule,
WorkspaceManyOrAllFlatEntityMapsCacheModule,
ApplicationModule,
],
providers: [
MigrateChannelPartialFullSyncStagesCommand,
@@ -9,9 +9,9 @@ import {
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { TWENTY_STANDARD_APPLICATION } from 'src/engine/core-modules/application/constants/twenty-standard-applications';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/twenty-standard-applications';
@Command({
name: 'upgrade:1-11:create-twenty-standard-application',
@@ -0,0 +1,65 @@
import { InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import {
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
type RunOnWorkspaceArgs,
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
@Command({
name: 'upgrade:1-12:create-workspace-custom-application',
description:
'Create workspace-custom application for workspaces that do not have them',
})
export class CreateWorkspaceCustomApplicationCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly applicationService: ApplicationService,
) {
super(workspaceRepository, twentyORMGlobalManager);
}
override async runOnWorkspace({
workspaceId,
}: RunOnWorkspaceArgs): Promise<void> {
this.logger.log(
`Checking standard applications for workspace ${workspaceId}`,
);
const workspace = await this.workspaceRepository.findOne({
where: { id: workspaceId },
});
if (!isDefined(workspace)) {
throw new Error(`${workspaceId} workspace not found`);
}
if (isDefined(workspace.workspaceCustomApplicationId)) {
this.logger.log(
`${workspaceId} skipping custom workspace application creation as already exists`,
);
return;
}
const customWorkspaceApplication =
await this.applicationService.createWorkspaceCustomApplication({
workspaceId,
workspaceDisplayName: workspace.displayName,
});
await this.workspaceRepository.update(workspace.id, {
workspaceCustomApplicationId: customWorkspaceApplication.id,
});
this.logger.log(`Successfully create workspace custom application`);
}
}
@@ -1,18 +1,17 @@
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Command } from 'nest-commander';
import { Repository } from 'typeorm';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import {
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
RunOnWorkspaceArgs,
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/twenty-standard-applications';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
// Note: this command needs to be run after CreateWorkspaceCustomApplicationCommand
@Command({
name: 'upgrade:1-12:set-standard-application-not-uninstallable',
description: 'Set canBeUninstalled flag to false for standard applications',
@@ -22,8 +21,6 @@ export class SetStandardApplicationNotUninstallableCommand extends ActiveOrSuspe
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
@InjectRepository(ApplicationEntity)
private readonly applicationRepository: Repository<ApplicationEntity>,
private readonly applicationService: ApplicationService,
) {
super(workspaceRepository, twentyORMGlobalManager);
@@ -35,36 +32,31 @@ export class SetStandardApplicationNotUninstallableCommand extends ActiveOrSuspe
}: RunOnWorkspaceArgs): Promise<void> {
try {
this.logger.log(
`Checking standard applications for workspace ${workspaceId}`,
`Checking workspace applications for workspace ${workspaceId}`,
);
const existingApplications = await this.applicationRepository.find({
where: [
const { twentyStandardFlatApplication, workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{
workspaceId,
universalIdentifier:
TWENTY_STANDARD_APPLICATION.universalIdentifier,
},
{
workspaceId,
description: 'Workspace custom application',
sourcePath: 'workspace-custom',
},
],
});
);
if (options.dryRun) {
return;
}
for (const existingApplication of existingApplications) {
for (const existingApplication of [
twentyStandardFlatApplication,
workspaceCustomFlatApplication,
]) {
await this.applicationService.update(existingApplication.id, {
canBeUninstalled: false,
});
}
this.logger.log(`Successfully updated standard applications`);
this.logger.log(`Successfully updated workspace application`);
} catch (e) {
this.logger.error(`Failed to update standard applications`, e);
this.logger.error(`Failed to update workspace application`, e);
}
}
}
@@ -1,19 +1,25 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { WorkspaceSchemaManagerModule } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.module';
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
import { CreateWorkspaceCustomApplicationCommand } from 'src/database/commands/upgrade-version-command/1-12/1-12-create-workspace-custom-application.command';
import { SetStandardApplicationNotUninstallableCommand } from 'src/database/commands/upgrade-version-command/1-12/1-12-set-standard-application-not-uninstallable.command';
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { WorkspaceSchemaManagerModule } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.module';
@Module({
imports: [
TypeOrmModule.forFeature([WorkspaceEntity, ApplicationEntity]),
TypeOrmModule.forFeature([WorkspaceEntity]),
WorkspaceSchemaManagerModule,
ApplicationModule,
],
providers: [SetStandardApplicationNotUninstallableCommand],
exports: [SetStandardApplicationNotUninstallableCommand],
providers: [
CreateWorkspaceCustomApplicationCommand,
SetStandardApplicationNotUninstallableCommand,
],
exports: [
CreateWorkspaceCustomApplicationCommand,
SetStandardApplicationNotUninstallableCommand,
],
})
export class V1_12_UpgradeVersionCommandModule {}
@@ -3,10 +3,10 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { V1_10_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-10/1-10-upgrade-version-command.module';
import { V1_11_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-11/1-11-upgrade-version-command.module';
import { V1_12_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-12/1-12-upgrade-version-command.module';
import { V1_6_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-6/1-6-upgrade-version-command.module';
import { V1_7_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-7/1-7-upgrade-version-command.module';
import { V1_8_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-8/1-8-upgrade-version-command.module';
import { V1_12_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-12/1-12-upgrade-version-command.module';
import { UpgradeCommand } from 'src/database/commands/upgrade-version-command/upgrade.command';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.module';
@@ -18,10 +18,11 @@ import { MigrateAttachmentAuthorToCreatedByCommand } from 'src/database/commands
import { MigrateAttachmentTypeToFileCategoryCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-migrate-attachment-type-to-file-category.command';
import { MigrateChannelPartialFullSyncStagesCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-migrate-channel-partial-full-sync-stages.command';
import { RegenerateSearchVectorsCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-regenerate-search-vectors.command';
import { SeedDashboardViewCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-seed-dashboard-view.command';
import { CleanOrphanedRoleTargetsCommand } from 'src/database/commands/upgrade-version-command/1-11/1-11-clean-orphaned-role-targets.command';
import { CleanOrphanedUserWorkspacesCommand } from 'src/database/commands/upgrade-version-command/1-11/1-11-clean-orphaned-user-workspaces.command';
import { CreateTwentyStandardApplicationCommand } from 'src/database/commands/upgrade-version-command/1-11/1-11-create-twenty-standard-application.command';
import { CreateWorkspaceCustomApplicationCommand } from 'src/database/commands/upgrade-version-command/1-12/1-12-create-workspace-custom-application.command';
import { SetStandardApplicationNotUninstallableCommand } from 'src/database/commands/upgrade-version-command/1-12/1-12-set-standard-application-not-uninstallable.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';
import { DeduplicateUniqueFieldsCommand } from 'src/database/commands/upgrade-version-command/1-8/1-8-deduplicate-unique-fields.command';
@@ -33,7 +34,6 @@ import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twent
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { SyncWorkspaceMetadataCommand } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command';
import { SetStandardApplicationNotUninstallableCommand } from 'src/database/commands/upgrade-version-command/1-12/1-12-set-standard-application-not-uninstallable.command';
@Command({
name: 'upgrade',
@@ -70,7 +70,6 @@ export class UpgradeCommand extends UpgradeCommandRunner {
protected readonly cleanOrphanedKanbanAggregateOperationFieldMetadataIdCommand: CleanOrphanedKanbanAggregateOperationFieldMetadataIdCommand,
protected readonly migrateChannelPartialFullSyncStagesCommand: MigrateChannelPartialFullSyncStagesCommand,
protected readonly makeSureDashboardNamingAvailableCommand: MakeSureDashboardNamingAvailableCommand,
protected readonly seedDashboardViewCommand: SeedDashboardViewCommand,
protected readonly createViewKanbanFieldMetadataIdForeignKeyMigrationCommand: CreateViewKanbanFieldMetadataIdForeignKeyMigrationCommand,
protected readonly flushWorkspaceCacheCommand: FlushCacheCommand,
@@ -81,6 +80,8 @@ export class UpgradeCommand extends UpgradeCommandRunner {
// 1.12 Commands
protected readonlysetStandardApplicationNotUninstallableCommand: SetStandardApplicationNotUninstallableCommand,
protected readonly createTwentyStandardApplicationCommand: CreateTwentyStandardApplicationCommand,
protected readonly createWorkspaceCustomApplicationCommand: CreateWorkspaceCustomApplicationCommand,
) {
super(
workspaceRepository,
@@ -124,13 +125,12 @@ export class UpgradeCommand extends UpgradeCommandRunner {
afterSyncMetadata: [
this.migrateAttachmentAuthorToCreatedByCommand,
this.migrateAttachmentTypeToFileCategoryCommand,
this.seedDashboardViewCommand,
this.flushWorkspaceCacheCommand,
],
};
const commands_1110: VersionCommands = {
beforeSyncMetadata: [this.seedStandardApplicationsCommand],
beforeSyncMetadata: [this.createTwentyStandardApplicationCommand],
afterSyncMetadata: [
this.cleanOrphanedUserWorkspacesCommand,
this.cleanOrphanedRoleTargetsCommand,
@@ -138,7 +138,7 @@ export class UpgradeCommand extends UpgradeCommandRunner {
};
const commands_1120: VersionCommands = {
beforeSyncMetadata: [],
beforeSyncMetadata: [this.createWorkspaceCustomApplicationCommand],
afterSyncMetadata: [
this.readonlysetStandardApplicationNotUninstallableCommand,
],