Guard yarn database:migrate:prod (#19008)

## Motivations
A lot of self hosters hands up using the `yarn database:migrated:prod`
either manually or through AI assisted debug while they try to upgrade
an instance while their workspace is still blocked in a previous one
Leading to their whole database permanent corruption

## What happened
Replaced the direct call the the typeorm cli to a command calling it
programmatically, adding a layer of security in case a workspace seems
to be blocked in a previous version than the one just before the one
being installed ( e.g 1.0 when you try to upgrade from 1.1 to 1.2 )

For our cloud we still need a way to bypass this security explaining the
-f flag

## Remark
Centralized this logic and refactored creating new services
`WorkspaceVersionService` and `CoreEngineVersionService` that will
become useful for the upcoming upgrade refactor

Related to https://github.com/twentyhq/twenty-infra/pull/529
This commit is contained in:
Paul Rastoin
2026-03-27 15:39:18 +01:00
committed by GitHub
parent c96c034908
commit 281bb6d783
25 changed files with 401 additions and 216 deletions
@@ -0,0 +1,83 @@
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { isDefined } from 'twenty-shared/utils';
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
import { In, Repository } from 'typeorm';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { compareVersionMajorAndMinor } from 'src/utils/version/compare-version-minor-and-major';
@Injectable()
export class WorkspaceVersionService {
private readonly logger = new Logger(WorkspaceVersionService.name);
constructor(
@InjectRepository(WorkspaceEntity)
private readonly workspaceRepository: Repository<WorkspaceEntity>,
) {}
async hasActiveOrSuspendedWorkspaces(): Promise<boolean> {
return this.workspaceRepository.exists({
where: {
activationStatus: In([
WorkspaceActivationStatus.ACTIVE,
WorkspaceActivationStatus.SUSPENDED,
]),
},
});
}
async getWorkspacesBelowVersion(
version: string,
): Promise<Pick<WorkspaceEntity, 'id' | 'displayName' | 'version'>[]> {
const allActiveOrSuspendedWorkspaces =
await this.loadActiveOrSuspendedWorkspaces();
if (allActiveOrSuspendedWorkspaces.length === 0) {
this.logger.log(
'No workspaces found. Running migrations for fresh installation.',
);
return [];
}
return allActiveOrSuspendedWorkspaces.filter((workspace) => {
if (!isDefined(workspace.version)) {
return true;
}
try {
const versionCompareResult = compareVersionMajorAndMinor(
workspace.version,
version,
);
return versionCompareResult === 'lower';
} catch (error) {
this.logger.error(
`Error checking workspace ${workspace.id} version: ${error.message}`,
);
return true;
}
});
}
private async loadActiveOrSuspendedWorkspaces(): Promise<
Pick<WorkspaceEntity, 'id' | 'version' | 'displayName'>[]
> {
return this.workspaceRepository.find({
select: ['id', 'version', 'displayName'],
where: {
activationStatus: In([
WorkspaceActivationStatus.ACTIVE,
WorkspaceActivationStatus.SUSPENDED,
]),
},
order: {
id: 'ASC',
},
});
}
}
@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { WorkspaceVersionService } from 'src/engine/workspace-manager/workspace-version/services/workspace-version.service';
@Module({
imports: [TypeOrmModule.forFeature([WorkspaceEntity])],
providers: [WorkspaceVersionService],
exports: [WorkspaceVersionService],
})
export class WorkspaceVersionModule {}