Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,15 @@ import {
getManifest,
runAllChecks,
type CheckRunResult,
type OAuthConfig,
} from '@trycompai/integration-platform';
import { ConnectionRepository } from '../repositories/connection.repository';
import { ProviderRepository } from '../repositories/provider.repository';
import { CheckRunRepository } from '../repositories/check-run.repository';
import { CredentialVaultService } from '../services/credential-vault.service';
import { OAuthCredentialsService } from '../services/oauth-credentials.service';
import { TaskIntegrationChecksService } from '../services/task-integration-checks.service';
import { getStringValue, toStringCredentials } from '../utils/credential-utils';
import { isCheckDisabledForTask } from '../utils/disabled-task-checks';
import { db } from '@db';
import type { Prisma } from '@db';

Expand All @@ -39,6 +40,8 @@ interface TaskIntegrationCheck {
checkName: string;
checkDescription: string;
isConnected: boolean;
/** True when the check has been manually disconnected from this task. */
isDisabledForTask: boolean;
needsConfiguration: boolean;
connectionId?: string;
connectionStatus?: string;
Expand All @@ -56,6 +59,11 @@ interface RunCheckForTaskDto {
checkId: string;
}

interface ToggleCheckForTaskDto {
connectionId: string;
checkId: string;
}

@Controller({ path: 'integrations/tasks', version: '1' })
@ApiTags('Integrations')
@UseGuards(HybridAuthGuard, PermissionGuard)
Expand All @@ -69,18 +77,22 @@ export class TaskIntegrationsController {
private readonly checkRunRepository: CheckRunRepository,
private readonly credentialVaultService: CredentialVaultService,
private readonly oauthCredentialsService: OAuthCredentialsService,
private readonly taskIntegrationChecksService: TaskIntegrationChecksService,
) {}

/**
* Get all integration checks that can auto-complete a specific task template
* Get all integration checks that can auto-complete a specific task template.
* When a specific `taskId` is also provided, per-task disable state is
* resolved from the matching connection's metadata so the UI can show
* which checks have been manually disconnected from that task.
*/
@Get('template/:templateId/checks')
@RequirePermission('integration', 'read')
async getChecksForTaskTemplate(
@Param('templateId') templateId: string,
@OrganizationId() organizationId: string,
taskIdForDisableState?: string,
): Promise<{ checks: TaskIntegrationCheck[] }> {

const manifests = getActiveManifests();
const checks: TaskIntegrationCheck[] = [];

Expand Down Expand Up @@ -136,6 +148,15 @@ export class TaskIntegrationsController {
oauthConfigured = availability.available;
}

const isDisabledForTask =
!!taskIdForDisableState &&
!!connection &&
isCheckDisabledForTask(
connection.metadata,
taskIdForDisableState,
check.id,
);

checks.push({
integrationId: manifest.id,
integrationName: manifest.name,
Expand All @@ -144,6 +165,7 @@ export class TaskIntegrationsController {
checkName: check.name,
checkDescription: check.description,
isConnected: !!connection && connection.status === 'active',
isDisabledForTask,
needsConfiguration,
connectionId: connection?.id,
connectionStatus: connection?.status,
Expand All @@ -169,7 +191,6 @@ export class TaskIntegrationsController {
checks: TaskIntegrationCheck[];
task: { id: string; title: string; templateId: string | null };
}> {

// Get the task to find its template ID
const task = await db.task.findUnique({
where: { id: taskId, organizationId },
Expand All @@ -187,10 +208,11 @@ export class TaskIntegrationsController {
};
}

// Get checks for this template
// Get checks for this template, annotated with per-task disable state
const { checks } = await this.getChecksForTaskTemplate(
task.taskTemplateId,
organizationId,
task.id,
);

return {
Expand All @@ -215,7 +237,6 @@ export class TaskIntegrationsController {
checkRunId?: string;
taskStatus?: string | null;
}> {

const { connectionId, checkId } = body;

// Verify task exists
Expand All @@ -240,6 +261,14 @@ export class TaskIntegrationsController {
);
}

// Reject runs for checks that have been disconnected from this task.
if (isCheckDisabledForTask(connection.metadata, taskId, checkId)) {
throw new HttpException(
'This check is disconnected from the task. Reconnect it before running.',
HttpStatus.BAD_REQUEST,
);
}

// Get provider and manifest
const provider = await this.providerRepository.findById(
connection.providerId,
Expand Down Expand Up @@ -493,6 +522,48 @@ export class TaskIntegrationsController {
}
}

/**
* Disconnect a single integration check from a specific task.
* Does not affect the connection itself or any other task that uses the
* same check. Scheduled runs, manual runs, and the task detail UI will all
* skip this (task, check) pair until it is reconnected.
*/
@Post(':taskId/checks/disconnect')
@RequirePermission('integration', 'update')
async disconnectCheckFromTask(
@Param('taskId') taskId: string,
@OrganizationId() organizationId: string,
@Body() body: ToggleCheckForTaskDto,
): Promise<{ success: true; disabled: true }> {
await this.taskIntegrationChecksService.disconnectCheckFromTask({
taskId,
connectionId: body.connectionId,
checkId: body.checkId,
organizationId,
});
return { success: true, disabled: true };
}

/**
* Re-enable a previously disconnected integration check for a specific
* task. Inverse of the disconnect endpoint.
*/
@Post(':taskId/checks/reconnect')
@RequirePermission('integration', 'update')
async reconnectCheckToTask(
@Param('taskId') taskId: string,
@OrganizationId() organizationId: string,
@Body() body: ToggleCheckForTaskDto,
): Promise<{ success: true; disabled: false }> {
await this.taskIntegrationChecksService.reconnectCheckToTask({
taskId,
connectionId: body.connectionId,
checkId: body.checkId,
organizationId,
});
return { success: true, disabled: false };
}

/**
* Get check run history for a task
*/
Expand All @@ -502,7 +573,6 @@ export class TaskIntegrationsController {
@Param('taskId') taskId: string,
@Query('limit') limit?: string,
) {

const runs = await this.checkRunRepository.findByTask(
taskId,
limit ? parseInt(limit, 10) : 10,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { AutoCheckRunnerService } from './services/auto-check-runner.service';
import { ConnectionAuthTeardownService } from './services/connection-auth-teardown.service';
import { OAuthTokenRevocationService } from './services/oauth-token-revocation.service';
import { DynamicManifestLoaderService } from './services/dynamic-manifest-loader.service';
import { TaskIntegrationChecksService } from './services/task-integration-checks.service';
import { ProviderRepository } from './repositories/provider.repository';
import { ConnectionRepository } from './repositories/connection.repository';
import { CredentialRepository } from './repositories/credential.repository';
Expand Down Expand Up @@ -52,6 +53,7 @@ import { GenericEmployeeSyncService } from './services/generic-employee-sync.ser
OAuthTokenRevocationService,
ConnectionAuthTeardownService,
DynamicManifestLoaderService,
TaskIntegrationChecksService,
IntegrationSyncLoggerService,
GenericEmployeeSyncService,
// Repositories
Expand Down
Loading
Loading