Workspace commands writes inupgradeMigration (#19379)

# Introduction
As for the instance commands we want to keep a track of what has been
run for the workspace commands
Note that the history will be updated only when the workspace command
has been run through the upgrade directly and not when run atomically

## What's next
Later we will use this history in order to determine the current
workspace's version and instance's version getting rid of the version in
database, that will be the last stone
This commit is contained in:
Paul Rastoin
2026-04-07 14:15:06 +02:00
committed by GitHub
parent d10ef156c1
commit 1c9fc94c1f
6 changed files with 242 additions and 72 deletions
@@ -0,0 +1,43 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddWorkspaceIdToUpgradeMigration1775553825848
implements MigrationInterface
{
name = 'AddWorkspaceIdToUpgradeMigration1775553825848';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'ALTER TABLE "core"."upgradeMigration" DROP CONSTRAINT "UQ_upgrade_migration_name_attempt"',
);
await queryRunner.query(
'ALTER TABLE "core"."upgradeMigration" ADD "workspaceId" uuid',
);
await queryRunner.query(
'CREATE UNIQUE INDEX "UQ_upgrade_migration_workspace" ON "core"."upgradeMigration" ("name", "attempt", "workspaceId") WHERE "workspaceId" IS NOT NULL',
);
await queryRunner.query(
'CREATE UNIQUE INDEX "UQ_upgrade_migration_instance" ON "core"."upgradeMigration" ("name", "attempt") WHERE "workspaceId" IS NULL',
);
await queryRunner.query(
'ALTER TABLE "core"."upgradeMigration" ADD CONSTRAINT "FK_77f64a697c55f8802592bd7eeba" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION',
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'ALTER TABLE "core"."upgradeMigration" DROP CONSTRAINT "FK_77f64a697c55f8802592bd7eeba"',
);
await queryRunner.query(
'DROP INDEX "core"."UQ_upgrade_migration_instance"',
);
await queryRunner.query(
'DROP INDEX "core"."UQ_upgrade_migration_workspace"',
);
await queryRunner.query(
'ALTER TABLE "core"."upgradeMigration" DROP COLUMN "workspaceId"',
);
await queryRunner.query(
'ALTER TABLE "core"."upgradeMigration" ADD CONSTRAINT "UQ_upgrade_migration_name_attempt" UNIQUE ("name", "attempt")',
);
}
}
@@ -1,18 +1,10 @@
import { Injectable } from '@nestjs/common';
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { InjectDataSource } from '@nestjs/typeorm';
import {
DataSource,
MigrationInterface,
type QueryRunner,
Repository,
} from 'typeorm';
import { DataSource, MigrationInterface } from 'typeorm';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import {
UpgradeMigrationEntity,
type UpgradeMigrationStatus,
} from 'src/engine/core-modules/upgrade/upgrade-migration.entity';
import { UpgradeMigrationService } from 'src/engine/core-modules/upgrade/services/upgrade-migration.service';
export type RunSingleMigrationResult =
| { status: 'success' }
@@ -22,11 +14,10 @@ export type RunSingleMigrationResult =
@Injectable()
export class InstanceUpgradeService {
constructor(
@InjectRepository(UpgradeMigrationEntity)
private readonly upgradeMigrationRepository: Repository<UpgradeMigrationEntity>,
@InjectDataSource()
private readonly dataSource: DataSource,
private readonly twentyConfigService: TwentyConfigService,
private readonly upgradeMigrationService: UpgradeMigrationService,
) {}
async runSingleMigration(
@@ -36,11 +27,13 @@ export class InstanceUpgradeService {
const executedByVersion =
this.twentyConfigService.get('APP_VERSION') ?? 'unknown';
const isAlreadyExecuted = await this.upgradeMigrationRepository.exists({
where: { name: migrationName, status: 'completed' },
});
const isAlreadyCompleted =
await this.upgradeMigrationService.isLastAttemptCompleted({
name: migrationName,
workspaceId: null,
});
if (isAlreadyExecuted) {
if (isAlreadyCompleted) {
return { status: 'already-executed' };
}
@@ -52,10 +45,11 @@ export class InstanceUpgradeService {
await migration.up(queryRunner);
await this.markAsCompleted({
queryRunner,
await this.upgradeMigrationService.markAsCompleted({
name: migrationName,
workspaceId: null,
executedByVersion,
queryRunner,
});
await queryRunner.commitTransaction();
@@ -64,8 +58,9 @@ export class InstanceUpgradeService {
await queryRunner.rollbackTransaction();
}
await this.markFailed({
await this.upgradeMigrationService.markAsFailed({
name: migrationName,
workspaceId: null,
executedByVersion,
});
@@ -76,46 +71,4 @@ export class InstanceUpgradeService {
return { status: 'success' };
}
private async markAsCompleted({
queryRunner,
name,
executedByVersion,
}: {
queryRunner: QueryRunner;
name: string;
executedByVersion: string;
}): Promise<void> {
const repository = queryRunner.manager.getRepository(
UpgradeMigrationEntity,
);
const previousAttempts = await repository.count({ where: { name } });
await repository.save({
name,
status: 'completed' as UpgradeMigrationStatus,
attempt: previousAttempts + 1,
executedByVersion,
});
}
private async markFailed({
name,
executedByVersion,
}: {
name: string;
executedByVersion: string;
}): Promise<void> {
const previousAttempts = await this.upgradeMigrationRepository.count({
where: { name },
});
await this.upgradeMigrationRepository.save({
name,
status: 'failed' as UpgradeMigrationStatus,
attempt: previousAttempts + 1,
executedByVersion,
});
}
}
@@ -0,0 +1,88 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { isDefined } from 'twenty-shared/utils';
import { IsNull, type QueryRunner, Repository } from 'typeorm';
import { UpgradeMigrationEntity } from 'src/engine/core-modules/upgrade/upgrade-migration.entity';
@Injectable()
export class UpgradeMigrationService {
constructor(
@InjectRepository(UpgradeMigrationEntity)
private readonly upgradeMigrationRepository: Repository<UpgradeMigrationEntity>,
) {}
async isLastAttemptCompleted({
name,
workspaceId,
}: {
name: string;
workspaceId: string | null;
}): Promise<boolean> {
const latestAttempt = await this.upgradeMigrationRepository.findOne({
where: {
name,
workspaceId: workspaceId === null ? IsNull() : workspaceId,
},
order: { attempt: 'DESC' },
});
return isDefined(latestAttempt) && latestAttempt.status === 'completed';
}
async markAsCompleted({
name,
workspaceId,
executedByVersion,
queryRunner,
}: {
name: string;
workspaceId: string | null;
executedByVersion: string;
queryRunner?: QueryRunner;
}): Promise<void> {
const repository = queryRunner
? queryRunner.manager.getRepository(UpgradeMigrationEntity)
: this.upgradeMigrationRepository;
const previousAttempts = await repository.count({
where: {
name,
workspaceId: workspaceId === null ? IsNull() : workspaceId,
},
});
await repository.save({
name,
status: 'completed',
attempt: previousAttempts + 1,
executedByVersion,
workspaceId,
});
}
async markAsFailed({
name,
workspaceId,
executedByVersion,
}: {
name: string;
workspaceId: string | null;
executedByVersion: string;
}): Promise<void> {
const previousAttempts = await this.upgradeMigrationRepository.count({
where: {
name,
workspaceId: workspaceId === null ? IsNull() : workspaceId,
},
});
await this.upgradeMigrationRepository.save({
name,
status: 'failed',
attempt: previousAttempts + 1,
executedByVersion,
workspaceId,
});
}
}
@@ -5,12 +5,13 @@ import { SemVer } from 'semver';
import { assertUnreachable, isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { type WorkspaceIteratorContext } from 'src/database/commands/command-runners/workspace-iterator.service';
import {
type UpgradeCommandOptions,
type VersionCommands,
} from 'src/database/commands/command-runners/upgrade.command-runner';
import { type RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspace.command-runner';
import { type WorkspaceIteratorContext } from 'src/database/commands/command-runners/workspace-iterator.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { UpgradeMigrationService } from 'src/engine/core-modules/upgrade/services/upgrade-migration.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import {
type CompareVersionMajorAndMinorReturnType,
@@ -32,6 +33,8 @@ export class WorkspaceUpgradeService {
constructor(
@InjectRepository(WorkspaceEntity)
private readonly workspaceRepository: Repository<WorkspaceEntity>,
private readonly twentyConfigService: TwentyConfigService,
private readonly upgradeMigrationService: UpgradeMigrationService,
) {}
async upgradeWorkspace({
@@ -60,13 +63,16 @@ export class WorkspaceUpgradeService {
);
}
case 'equal': {
const executedByVersion =
this.twentyConfigService.get('APP_VERSION') ?? 'unknown';
for (const workspaceCommand of workspaceCommands) {
await workspaceCommand.runOnWorkspace({
options: options as RunOnWorkspaceArgs['options'],
await this.runSingleWorkspaceCommandOrThrow({
workspaceCommand,
workspaceId,
dataSource: iteratorContext.dataSource,
index,
total,
executedByVersion,
options,
iteratorContext,
});
}
@@ -112,4 +118,62 @@ export class WorkspaceUpgradeService {
fromWorkspaceVersion.version,
);
}
private async runSingleWorkspaceCommandOrThrow({
workspaceCommand,
workspaceId,
executedByVersion,
options,
iteratorContext,
}: {
workspaceCommand: VersionCommands[number];
workspaceId: string;
executedByVersion: string;
options: UpgradeCommandOptions;
iteratorContext: WorkspaceIteratorContext;
}): Promise<void> {
const commandName = workspaceCommand.constructor.name;
const isAlreadyCompleted =
await this.upgradeMigrationService.isLastAttemptCompleted({
name: commandName,
workspaceId,
});
if (isAlreadyCompleted) {
this.logger.log(
`Workspace command ${commandName} already completed for workspace ${workspaceId}, skipping`,
);
return;
}
try {
await workspaceCommand.runOnWorkspace({
options,
workspaceId,
dataSource: iteratorContext.dataSource,
index: iteratorContext.index,
total: iteratorContext.total,
});
if (!options.dryRun) {
await this.upgradeMigrationService.markAsCompleted({
name: commandName,
workspaceId,
executedByVersion,
});
}
} catch (error) {
if (!options.dryRun) {
await this.upgradeMigrationService.markAsFailed({
name: commandName,
workspaceId,
executedByVersion,
});
}
throw error;
}
}
}
@@ -2,14 +2,26 @@ import {
Column,
CreateDateColumn,
Entity,
Index,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
Unique,
type Relation,
} from 'typeorm';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
export type UpgradeMigrationStatus = 'completed' | 'failed';
@Entity({ name: 'upgradeMigration', schema: 'core' })
@Unique('UQ_upgrade_migration_name_attempt', ['name', 'attempt'])
@Index('UQ_upgrade_migration_instance', ['name', 'attempt'], {
unique: true,
where: '"workspaceId" IS NULL',
})
@Index('UQ_upgrade_migration_workspace', ['name', 'attempt', 'workspaceId'], {
unique: true,
where: '"workspaceId" IS NOT NULL',
})
export class UpgradeMigrationEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@@ -26,6 +38,13 @@ export class UpgradeMigrationEntity {
@Column({ type: 'varchar', nullable: false })
executedByVersion: string;
@ManyToOne(() => WorkspaceEntity, { onDelete: 'CASCADE', nullable: true })
@JoinColumn({ name: 'workspaceId' })
workspace: Relation<WorkspaceEntity> | null;
@Column({ type: 'uuid', nullable: true })
workspaceId: string | null;
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date;
}
@@ -2,10 +2,11 @@ import { Module } from '@nestjs/common';
import { DiscoveryModule } from '@nestjs/core';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UpgradeMigrationEntity } from 'src/engine/core-modules/upgrade/upgrade-migration.entity';
import { InstanceUpgradeService } from 'src/engine/core-modules/upgrade/services/instance-upgrade.service';
import { RegisteredInstanceMigrationService } from 'src/engine/core-modules/upgrade/services/registered-instance-migration-registry.service';
import { UpgradeMigrationService } from 'src/engine/core-modules/upgrade/services/upgrade-migration.service';
import { WorkspaceUpgradeService } from 'src/engine/core-modules/upgrade/services/workspace-upgrade.service';
import { UpgradeMigrationEntity } from 'src/engine/core-modules/upgrade/upgrade-migration.entity';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
@Module({
@@ -14,11 +15,13 @@ import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.ent
TypeOrmModule.forFeature([UpgradeMigrationEntity, WorkspaceEntity]),
],
providers: [
UpgradeMigrationService,
InstanceUpgradeService,
WorkspaceUpgradeService,
RegisteredInstanceMigrationService,
],
exports: [
UpgradeMigrationService,
InstanceUpgradeService,
WorkspaceUpgradeService,
RegisteredInstanceMigrationService,