From 61fdb613e68115be08abe4e973b256580ce13ebf Mon Sep 17 00:00:00 2001 From: Paul Rastoin <45004772+prastoin@users.noreply.github.com> Date: Tue, 21 Apr 2026 15:32:59 +0200 Subject: [PATCH] Reset default app packages command (#19931) # Introduction If a self host creates its twenty instance using storage type local, and then edit through the admin panel the storage type, the apps default deps file won't be swapped to the new storage location This command allow to manually rebuild them ## What could be done in addition - We could display a modal in the admin panel when the user is editing env variable that might have a side effect - We might wanna rebuild the deps by default if we detect such a change through the UI though we can't really if it's through the `.env` so I'm not sure we wanna prio such logic --- .../commands/database-command.module.ts | 8 ++ ...ebuild-application-default-deps.command.ts | 94 +++++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 packages/twenty-server/src/database/commands/rebuild-application-default-deps.command.ts diff --git a/packages/twenty-server/src/database/commands/database-command.module.ts b/packages/twenty-server/src/database/commands/database-command.module.ts index 94452fc091..bcfcb56d19 100644 --- a/packages/twenty-server/src/database/commands/database-command.module.ts +++ b/packages/twenty-server/src/database/commands/database-command.module.ts @@ -16,6 +16,10 @@ import { GenerateApiKeyCommand } from 'src/engine/core-modules/api-key/commands/ import { MarketplaceModule } from 'src/engine/core-modules/application/application-marketplace/marketplace.module'; import { StaleRegistrationCleanupModule } from 'src/engine/core-modules/application/application-oauth/stale-registration-cleanup/stale-registration-cleanup.module'; import { ApplicationUpgradeModule } from 'src/engine/core-modules/application/application-upgrade/application-upgrade.module'; +import { RebuildApplicationDefaultDepsCommand } from 'src/database/commands/rebuild-application-default-deps.command'; +import { WorkspaceIteratorModule } from 'src/database/commands/command-runners/workspace-iterator.module'; +import { ApplicationModule } from 'src/engine/core-modules/application/application.module'; +import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module'; import { EnforceUsageCapCronCommand } from 'src/engine/core-modules/billing/crons/commands/enforce-usage-cap.cron.command'; import { EnterpriseKeyValidationCronCommand } from 'src/engine/core-modules/enterprise/cron/command/enterprise-key-validation.cron.command'; import { EnterpriseModule } from 'src/engine/core-modules/enterprise/enterprise.module'; @@ -73,6 +77,9 @@ import { AutomatedTriggerModule } from 'src/modules/workflow/workflow-trigger/au MarketplaceModule, ApplicationUpgradeModule, StaleRegistrationCleanupModule, + WorkspaceIteratorModule, + ApplicationModule, + WorkspaceCacheModule, WorkspaceVersionModule, UpgradeModule, ], @@ -88,6 +95,7 @@ import { AutomatedTriggerModule } from 'src/modules/workflow/workflow-trigger/au GenerateApiKeyCommand, EnforceUsageCapCronCommand, UpgradeStatusCommand, + RebuildApplicationDefaultDepsCommand, ], }) export class DatabaseCommandModule {} diff --git a/packages/twenty-server/src/database/commands/rebuild-application-default-deps.command.ts b/packages/twenty-server/src/database/commands/rebuild-application-default-deps.command.ts new file mode 100644 index 0000000000..10aee054d8 --- /dev/null +++ b/packages/twenty-server/src/database/commands/rebuild-application-default-deps.command.ts @@ -0,0 +1,94 @@ +import chalk from 'chalk'; +import { Command, CommandRunner, Option } from 'nest-commander'; +import { isDefined } from 'twenty-shared/utils'; + +import { WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service'; +import { CommandLogger } from 'src/database/commands/logger'; +import { ApplicationService } from 'src/engine/core-modules/application/application.service'; +import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type'; +import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; + +type RebuildDefaultPackageFilesCommandOptions = { + workspaceId?: Set; +}; + +@Command({ + name: 'application:rebuild-default-deps', + description: + 'Re-upload default package.json and yarn.lock to file storage for all applications in the workspace', +}) +export class RebuildApplicationDefaultDepsCommand extends CommandRunner { + protected logger: CommandLogger; + + constructor( + private readonly workspaceIteratorService: WorkspaceIteratorService, + private readonly applicationService: ApplicationService, + private readonly workspaceCacheService: WorkspaceCacheService, + ) { + super(); + this.logger = new CommandLogger({ + verbose: false, + constructorName: this.constructor.name, + }); + } + + @Option({ + flags: '-w, --workspace-id [workspace_id]', + description: + 'workspace id. Command runs on all active/suspended workspaces if not provided.', + required: false, + }) + parseWorkspaceId(val: string, previous?: Set): Set { + const accumulator = previous ?? new Set(); + + accumulator.add(val); + + return accumulator; + } + + override async run( + _passedParams: string[], + options: RebuildDefaultPackageFilesCommandOptions, + ): Promise { + const workspaceIds = isDefined(options.workspaceId) + ? Array.from(options.workspaceId) + : undefined; + const report = await this.workspaceIteratorService.iterate({ + workspaceIds, + callback: async ({ workspaceId }) => { + const { flatApplicationMaps } = + await this.workspaceCacheService.getOrRecompute(workspaceId, [ + 'flatApplicationMaps', + ]); + + const applications = Object.values(flatApplicationMaps.byId).filter( + (application): application is FlatApplication => + isDefined(application) && !isDefined(application.deletedAt), + ); + + this.logger.log( + `Found ${applications.length} application(s) in workspace ${workspaceId}`, + ); + + for (const application of applications) { + await this.applicationService.uploadDefaultPackageFilesAndSetFileIds({ + id: application.id, + universalIdentifier: application.universalIdentifier, + workspaceId, + }); + this.logger.log( + `Rebuilt default package files for application "${application.name}" (${application.id})`, + ); + } + }, + }); + + if (report.fail.length > 0) { + throw new Error( + `Command completed with ${report.fail.length} failure(s)`, + ); + } + + this.logger.log(chalk.blue('Command completed!')); + } +}