import { BadRequestException, Injectable, NotFoundException, } from '@nestjs/common'; import { InjectKysely } from 'nestjs-kysely'; import { KyselyDB } from '@docmost/db/types/kysely.types'; import { executeTx } from '@docmost/db/utils'; import { IntegrationRepo } from './repos/integration.repo'; import { IntegrationConnectionRepo } from './repos/integration-connection.repo'; import { IntegrationRegistry } from './registry/integration-registry'; import { Integration } from '@docmost/db/types/entity.types'; @Injectable() export class IntegrationService { constructor( @InjectKysely() private readonly db: KyselyDB, private readonly integrationRepo: IntegrationRepo, private readonly connectionRepo: IntegrationConnectionRepo, private readonly registry: IntegrationRegistry, ) {} async getAvailableIntegrations() { return this.registry.getAvailableIntegrations(); } async getInstalledIntegrations(workspaceId: string): Promise { return this.integrationRepo.findAllByWorkspace(workspaceId); } async findById(integrationId: string): Promise { return this.integrationRepo.findById(integrationId); } async install( type: string, workspaceId: string, userId: string, ): Promise { const provider = this.registry.getProvider(type); if (!provider || provider.definition.hidden) { throw new BadRequestException(`Unknown integration type: ${type}`); } // OAuth providers install via install-and-authorize (see OAuthService). if (provider.definition.oauth) { throw new BadRequestException( 'This integration is installed by completing its OAuth flow', ); } const existing = await this.integrationRepo.findByWorkspaceAndType( workspaceId, type, ); if (existing) { throw new BadRequestException( `Integration "${type}" is already installed`, ); } return this.integrationRepo.insertOrRestore({ type, workspaceId, installedById: userId, }); } async uninstall(integrationId: string, workspaceId: string): Promise { const integration = await this.integrationRepo.findById(integrationId); if (!integration || integration.workspaceId !== workspaceId) { throw new NotFoundException('Integration not found'); } // Delete child rows first so no orphan connections keep feeding the token refresh scheduler. await executeTx(this.db, async (trx) => { await this.connectionRepo.deleteByIntegration(integrationId, trx); await this.integrationRepo.softDelete(integrationId, trx); }); } }