diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/2-0/2-0-instance-command-slow-1776894434000-seed-exa-pre-install-from-env.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/2-0/2-0-instance-command-slow-1776894434000-seed-exa-pre-install-from-env.ts new file mode 100644 index 00000000000..503b1acf516 --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/2-0/2-0-instance-command-slow-1776894434000-seed-exa-pre-install-from-env.ts @@ -0,0 +1,131 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { type DataSource, type QueryRunner, Repository } from 'typeorm'; + +import { MarketplaceService } from 'src/engine/core-modules/application/application-marketplace/marketplace.service'; +import { ApplicationRegistrationVariableEntity } from 'src/engine/core-modules/application/application-registration-variable/application-registration-variable.entity'; +import { ApplicationRegistrationEntity } from 'src/engine/core-modules/application/application-registration/application-registration.entity'; +import { ApplicationRegistrationSourceType } from 'src/engine/core-modules/application/application-registration/enums/application-registration-source-type.enum'; +import { ApplicationRegistrationService } from 'src/engine/core-modules/application/application-registration/application-registration.service'; +import { SecretEncryptionService } from 'src/engine/core-modules/secret-encryption/secret-encryption.service'; +import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator'; +import { type SlowInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/slow-instance-command.interface'; + +const EXA_PACKAGE_NAME = 'twenty-exa'; + +// One-shot bridge for instances that set `EXA_API_KEY` as an env var under +// the old web-search driver. Registers `twenty-exa` as an application, +// seeds the key onto its `ApplicationRegistrationVariable`, and flips +// `isPreInstalled=true` so the next workspace install backfills it. The +// env var can be removed from infra after deploy. Idempotent — each step +// no-ops when its target already exists. +@RegisteredInstanceCommand('2.0.0', 1776894434000, { type: 'slow' }) +@Injectable() +export class SeedExaPreInstallFromEnvSlowInstanceCommand + implements SlowInstanceCommand +{ + private readonly logger = new Logger( + SeedExaPreInstallFromEnvSlowInstanceCommand.name, + ); + + constructor( + private readonly marketplaceService: MarketplaceService, + private readonly applicationRegistrationService: ApplicationRegistrationService, + private readonly secretEncryptionService: SecretEncryptionService, + @InjectRepository(ApplicationRegistrationEntity) + private readonly applicationRegistrationRepository: Repository, + @InjectRepository(ApplicationRegistrationVariableEntity) + private readonly applicationRegistrationVariableRepository: Repository, + ) {} + + public async up(_queryRunner: QueryRunner): Promise {} + + public async down(_queryRunner: QueryRunner): Promise {} + + async runDataMigration(_dataSource: DataSource): Promise { + const apiKey = process.env.EXA_API_KEY; + + if (!apiKey || apiKey.length === 0) { + this.logger.log('EXA_API_KEY not set — skipping Exa pre-install seed.'); + + return; + } + + const packages = await this.marketplaceService.fetchAppsFromRegistry(); + const exaPackage = packages.find((pkg) => pkg.name === EXA_PACKAGE_NAME); + + if (!exaPackage) { + this.logger.warn( + `"${EXA_PACKAGE_NAME}" not found in the app registry — skipping seed. ` + + `Publish the package first, then run \`install-pre-installed-apps\` to backfill.`, + ); + + return; + } + + const manifest = await this.marketplaceService.fetchManifestFromRegistryCdn( + exaPackage.name, + exaPackage.version, + ); + + if (!manifest) { + this.logger.warn( + `Manifest not found for "${EXA_PACKAGE_NAME}@${exaPackage.version}" — skipping seed.`, + ); + + return; + } + + await this.applicationRegistrationService.upsertFromCatalog({ + universalIdentifier: manifest.application.universalIdentifier, + name: manifest.application.displayName ?? exaPackage.name, + sourceType: ApplicationRegistrationSourceType.NPM, + sourcePackage: exaPackage.name, + latestAvailableVersion: exaPackage.version, + isListed: true, + isFeatured: false, + manifest, + ownerWorkspaceId: null, + }); + + const registration = await this.applicationRegistrationRepository.findOne({ + where: { universalIdentifier: manifest.application.universalIdentifier }, + }); + + if (!registration) { + this.logger.error( + `upsertFromCatalog did not produce a registration for "${EXA_PACKAGE_NAME}".`, + ); + + return; + } + + // Fill EXA_API_KEY only when unset — never overwrite a value already + // edited via the admin UI. + const variable = + await this.applicationRegistrationVariableRepository.findOne({ + where: { + applicationRegistrationId: registration.id, + key: 'EXA_API_KEY', + }, + }); + + if (variable && variable.encryptedValue === '') { + await this.applicationRegistrationVariableRepository.update(variable.id, { + encryptedValue: this.secretEncryptionService.encrypt(apiKey), + }); + } + + if (!registration.isPreInstalled) { + await this.applicationRegistrationRepository.update(registration.id, { + isPreInstalled: true, + }); + } + + this.logger.log( + `Seeded "${EXA_PACKAGE_NAME}" registration + EXA_API_KEY + isPreInstalled=true. ` + + `Run \`install-pre-installed-apps\` to backfill existing workspaces.`, + ); + } +} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/instance-command-provider.module.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/instance-command-provider.module.ts index b35cc44420f..18dc81d65ae 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/instance-command-provider.module.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/instance-command-provider.module.ts @@ -1,8 +1,23 @@ import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; import { INSTANCE_COMMANDS } from 'src/database/commands/upgrade-version-command/instance-commands.constant'; +import { MarketplaceModule } from 'src/engine/core-modules/application/application-marketplace/marketplace.module'; +import { ApplicationRegistrationVariableEntity } from 'src/engine/core-modules/application/application-registration-variable/application-registration-variable.entity'; +import { ApplicationRegistrationEntity } from 'src/engine/core-modules/application/application-registration/application-registration.entity'; +import { ApplicationRegistrationModule } from 'src/engine/core-modules/application/application-registration/application-registration.module'; +import { SecretEncryptionModule } from 'src/engine/core-modules/secret-encryption/secret-encryption.module'; @Module({ + imports: [ + TypeOrmModule.forFeature([ + ApplicationRegistrationEntity, + ApplicationRegistrationVariableEntity, + ]), + ApplicationRegistrationModule, + MarketplaceModule, + SecretEncryptionModule, + ], providers: [...INSTANCE_COMMANDS], }) export class InstanceCommandProviderModule {} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/instance-commands.constant.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/instance-commands.constant.ts index 1ff661851d4..346db180fc0 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/instance-commands.constant.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/instance-commands.constant.ts @@ -16,6 +16,7 @@ import { AddGlobalObjectContextToCommandMenuItemAvailabilityTypeFastInstanceComm import { AddPageLayoutIdToCommandMenuItemFastInstanceCommand } from 'src/database/commands/upgrade-version-command/1-23/1-23-instance-command-fast-1776168404836-add-page-layout-id-to-command-menu-item'; import { AddConditionalAvailabilityExpressionToPageLayoutWidgetFastInstanceCommand } from 'src/database/commands/upgrade-version-command/1-23/1-23-instance-command-fast-1775654781000-add-conditional-availability-expression-to-page-layout-widget'; import { AddIsPreInstalledToApplicationRegistrationFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-0/2-0-instance-command-fast-1776886452831-add-is-pre-installed-to-application-registration'; +import { SeedExaPreInstallFromEnvSlowInstanceCommand } from 'src/database/commands/upgrade-version-command/2-0/2-0-instance-command-slow-1776894434000-seed-exa-pre-install-from-env'; export const INSTANCE_COMMANDS = [ AddViewFieldGroupIdIndexOnViewFieldFastInstanceCommand, @@ -34,4 +35,5 @@ export const INSTANCE_COMMANDS = [ AddPageLayoutIdToCommandMenuItemFastInstanceCommand, AddConditionalAvailabilityExpressionToPageLayoutWidgetFastInstanceCommand, AddIsPreInstalledToApplicationRegistrationFastInstanceCommand, + SeedExaPreInstallFromEnvSlowInstanceCommand, ];