Compare commits

...
Author SHA1 Message Date
Paul Rastoinandprastoin ff6cc138dd 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
2026-04-21 16:03:55 +02:00
2 changed files with 102 additions and 0 deletions
@@ -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 {}
@@ -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!'));
}
}