remove ai-model-preferences var env and config (#20859)

Split the single AI_MODEL_PREFERENCES JSON config into 4 array configs
and migrates existing workspace data.
This commit is contained in:
Etienne
2026-05-27 08:47:47 +02:00
committed by GitHub
parent 9d6c5b7d58
commit 4a82cddad6
13 changed files with 449 additions and 74 deletions
@@ -0,0 +1,118 @@
import { Logger } from '@nestjs/common';
import { isDefined } from 'twenty-shared/utils';
import { DataSource, IsNull, QueryRunner } from 'typeorm';
import {
KeyValuePairEntity,
KeyValuePairType,
} from 'src/engine/core-modules/key-value-pair/key-value-pair.entity';
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
import { SlowInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/slow-instance-command.interface';
import { aiModelPreferencesSchema } from 'src/engine/metadata-modules/ai/ai-models/types/ai-model-preferences.schema';
const NEW_KEYS = [
'AI_MODELS_DEFAULT_FAST',
'AI_MODELS_DEFAULT_SMART',
'AI_MODELS_DEFAULT_RECOMMENDED',
'AI_MODELS_DEFAULT_DISABLED',
] as const;
const PREFERENCE_KEY_MAP = {
AI_MODELS_DEFAULT_FAST: 'defaultFastModels',
AI_MODELS_DEFAULT_SMART: 'defaultSmartModels',
AI_MODELS_DEFAULT_RECOMMENDED: 'recommendedModels',
AI_MODELS_DEFAULT_DISABLED: 'disabledModels',
} as const;
@RegisteredInstanceCommand('2.9.0', 1799000010000, { type: 'slow' })
export class MigrateAiModelPreferencesSlowInstanceCommand
implements SlowInstanceCommand
{
private readonly logger = new Logger(
MigrateAiModelPreferencesSlowInstanceCommand.name,
);
async runDataMigration(dataSource: DataSource): Promise<void> {
const keyValuePairRepository = dataSource.getRepository(KeyValuePairEntity);
const existingRow = await keyValuePairRepository.findOne({
where: {
type: KeyValuePairType.CONFIG_VARIABLE,
key: 'AI_MODEL_PREFERENCES',
userId: IsNull(),
workspaceId: IsNull(),
},
});
if (!isDefined(existingRow)) {
this.logger.log(
'No server-level AI_MODEL_PREFERENCES row found, skipping',
);
return;
}
const parseResult = aiModelPreferencesSchema.safeParse(existingRow.value);
if (!parseResult.success) {
this.logger.error(
`Failed to parse server-level AI_MODEL_PREFERENCES: ${parseResult.error.message}`,
);
return;
}
const prefs = parseResult.data;
this.logger.log('Migrating server-level AI_MODEL_PREFERENCES');
await dataSource.transaction(async (manager) => {
const transactionalRepository = manager.getRepository(KeyValuePairEntity);
for (const newKey of NEW_KEYS) {
const prefField = PREFERENCE_KEY_MAP[newKey];
const value = prefs[prefField];
if (!isDefined(value) || value.length === 0) {
continue;
}
const existingNewKeyCount = await transactionalRepository.count({
where: {
type: KeyValuePairType.CONFIG_VARIABLE,
key: newKey,
userId: IsNull(),
workspaceId: IsNull(),
},
});
if (existingNewKeyCount > 0) {
continue;
}
await transactionalRepository.insert({
key: newKey,
value: value as unknown as JSON,
type: KeyValuePairType.CONFIG_VARIABLE,
userId: null,
workspaceId: null,
});
}
await transactionalRepository.delete({ id: existingRow.id });
});
this.logger.log(
'Migrated server-level AI_MODEL_PREFERENCES to 4 individual vars',
);
}
public async up(_queryRunner: QueryRunner): Promise<void> {
return;
}
public async down(_queryRunner: QueryRunner): Promise<void> {
return;
}
}
@@ -0,0 +1,15 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { WorkspaceIteratorModule } from 'src/database/commands/command-runners/workspace-iterator.module';
import { MigrateAiModelPreferencesCommand } from 'src/database/commands/upgrade-version-command/2-9/2-9-workspace-command-1799000000000-migrate-ai-model-preferences.command';
import { KeyValuePairEntity } from 'src/engine/core-modules/key-value-pair/key-value-pair.entity';
@Module({
imports: [
TypeOrmModule.forFeature([KeyValuePairEntity]),
WorkspaceIteratorModule,
],
providers: [MigrateAiModelPreferencesCommand],
})
export class V2_9_UpgradeVersionCommandModule {}
@@ -0,0 +1,136 @@
import { InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { IsNull, Repository } from 'typeorm';
import { isArray, isDefined } from 'class-validator';
import { ActiveOrSuspendedWorkspaceCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspace.command-runner';
import { WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service';
import { type RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspace.command-runner';
import {
KeyValuePairEntity,
KeyValuePairType,
} from 'src/engine/core-modules/key-value-pair/key-value-pair.entity';
import { RegisteredWorkspaceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-workspace-command.decorator';
import { aiModelPreferencesSchema } from 'src/engine/metadata-modules/ai/ai-models/types/ai-model-preferences.schema';
const NEW_KEYS = [
'AI_MODELS_DEFAULT_FAST',
'AI_MODELS_DEFAULT_SMART',
'AI_MODELS_DEFAULT_RECOMMENDED',
'AI_MODELS_DEFAULT_DISABLED',
] as const;
const PREFERENCE_KEY_MAP = {
AI_MODELS_DEFAULT_FAST: 'defaultFastModels',
AI_MODELS_DEFAULT_SMART: 'defaultSmartModels',
AI_MODELS_DEFAULT_RECOMMENDED: 'recommendedModels',
AI_MODELS_DEFAULT_DISABLED: 'disabledModels',
} as const;
@RegisteredWorkspaceCommand('2.9.0', 1799000000000)
@Command({
name: 'upgrade:2-9:migrate-ai-model-preferences',
description:
'Migrate AI_MODEL_PREFERENCES config var to the four individual AI_MODELS_DEFAULT_* vars, per workspace',
})
export class MigrateAiModelPreferencesCommand extends ActiveOrSuspendedWorkspaceCommandRunner {
constructor(
protected readonly workspaceIteratorService: WorkspaceIteratorService,
@InjectRepository(KeyValuePairEntity)
private readonly keyValuePairRepository: Repository<KeyValuePairEntity>,
) {
super(workspaceIteratorService);
}
override async runOnWorkspace({
workspaceId,
options,
}: RunOnWorkspaceArgs): Promise<void> {
const isDryRun = options.dryRun ?? false;
const existingPreferencesRow = await this.keyValuePairRepository.findOne({
where: {
key: 'AI_MODEL_PREFERENCES',
type: KeyValuePairType.CONFIG_VARIABLE,
workspaceId,
userId: IsNull(),
},
});
if (existingPreferencesRow === null) {
this.logger.log(
`No AI_MODEL_PREFERENCES row found for workspace ${workspaceId}, skipping`,
);
return;
}
const parseResult = aiModelPreferencesSchema.safeParse(
existingPreferencesRow.value,
);
if (!parseResult.success) {
this.logger.error(
`Failed to parse AI_MODEL_PREFERENCES for workspace ${workspaceId}: ${parseResult.error.message}`,
);
return;
}
const prefs = parseResult.data;
this.logger.log(
`${isDryRun ? '[DRY RUN] ' : ''}Migrating AI_MODEL_PREFERENCES for workspace ${workspaceId}`,
);
for (const newKey of NEW_KEYS) {
const prefField = PREFERENCE_KEY_MAP[newKey];
if (
!isDefined(prefField) ||
(isArray(prefs[prefField]) && prefs[prefField].length === 0)
) {
continue;
}
const value = prefs[prefField];
if (isDryRun) {
this.logger.log(
`[DRY RUN] Would insert ${newKey} = ${JSON.stringify(value)} for workspace ${workspaceId}`,
);
continue;
}
const existingNewKeyRow = await this.keyValuePairRepository.findOne({
where: {
key: newKey,
type: KeyValuePairType.CONFIG_VARIABLE,
workspaceId,
userId: IsNull(),
},
});
if (existingNewKeyRow !== null) {
continue;
}
await this.keyValuePairRepository.insert({
key: newKey,
value: value as unknown as JSON,
type: KeyValuePairType.CONFIG_VARIABLE,
workspaceId,
userId: null,
});
}
if (!isDryRun) {
await this.keyValuePairRepository.delete({
id: existingPreferencesRow.id,
});
this.logger.log(
`Migrated AI_MODEL_PREFERENCES to 4 individual vars for workspace ${workspaceId}`,
);
}
}
}
@@ -55,6 +55,7 @@ import { AddChannelSyncStageIndexesFastInstanceCommand } from 'src/database/comm
import { FinalizeRolePermissionFlagCutoverFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-7/2-7-instance-command-fast-1779600000000-finalize-role-permission-flag-cutover';
import { AddSubFieldNameToIndexFieldMetadataFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-8/2-8-instance-command-fast-1798200000000-add-sub-field-name-to-index-field-metadata';
import { DropFieldMetadataIsUniqueColumnFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-8/2-8-instance-command-fast-1798300000000-drop-field-metadata-is-unique-column';
import { MigrateAiModelPreferencesSlowInstanceCommand } from 'src/database/commands/upgrade-version-command/2-9/2-9-instance-command-slow-1799000010000-migrate-ai-model-preferences';
export const INSTANCE_COMMANDS = [
AddViewFieldGroupIdIndexOnViewFieldFastInstanceCommand,
@@ -112,4 +113,5 @@ export const INSTANCE_COMMANDS = [
FinalizeRolePermissionFlagCutoverFastInstanceCommand,
AddSubFieldNameToIndexFieldMetadataFastInstanceCommand,
DropFieldMetadataIsUniqueColumnFastInstanceCommand,
MigrateAiModelPreferencesSlowInstanceCommand,
];
@@ -11,6 +11,7 @@ import { V2_4_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-
import { V2_5_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/2-5/2-5-upgrade-version-command.module';
import { V2_7_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/2-7/2-7-upgrade-version-command.module';
import { V2_8_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/2-8/2-8-upgrade-version-command.module';
import { V2_9_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/2-9/2-9-upgrade-version-command.module';
@Module({
imports: [
@@ -25,6 +26,7 @@ import { V2_8_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-
V2_5_UpgradeVersionCommandModule,
V2_7_UpgradeVersionCommandModule,
V2_8_UpgradeVersionCommandModule,
V2_9_UpgradeVersionCommandModule,
],
})
export class WorkspaceCommandProviderModule {}