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
This commit is contained in:
Paul Rastoin 2026-04-21 15:32:59 +02:00 committed by GitHub
parent 7947b1b843
commit 61fdb613e6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 102 additions and 0 deletions

View file

@ -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 {}

View file

@ -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<string>;
};
@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<string>): Set<string> {
const accumulator = previous ?? new Set<string>();
accumulator.add(val);
return accumulator;
}
override async run(
_passedParams: string[],
options: RebuildDefaultPackageFilesCommandOptions,
): Promise<void> {
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!'));
}
}