Skip to content

Commit b6b20a0

Browse files
authored
Merge pull request #2385 from trycompai/main
[comp] Production Deploy
2 parents 93c6e46 + 9f50fde commit b6b20a0

19 files changed

Lines changed: 1627 additions & 87 deletions

File tree

‎apps/api/src/integration-platform/controllers/checks.controller.ts‎

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
UseGuards,
1111
} from '@nestjs/common';
1212
import { ApiTags, ApiSecurity } from '@nestjs/swagger';
13+
import type { Prisma } from '@prisma/client';
1314
import { HybridAuthGuard } from '../../auth/hybrid-auth.guard';
1415
import { PermissionGuard } from '../../auth/permission.guard';
1516
import { RequirePermission } from '../../auth/require-permission.decorator';
@@ -271,14 +272,28 @@ export class ChecksController {
271272
await this.checkRunRepository.addResults(resultsToStore);
272273
}
273274

274-
// Update the check run status
275+
// Collect execution logs from all check results
276+
const allLogs = result.results.flatMap((checkResult) =>
277+
checkResult.result.logs.map((log) => ({
278+
check: checkResult.checkName,
279+
level: log.level,
280+
message: log.message,
281+
...(log.data ? { data: log.data } : {}),
282+
timestamp: log.timestamp.toISOString(),
283+
})),
284+
);
285+
286+
// Update the check run status with logs
275287
const startTime = checkRun.startedAt?.getTime() || Date.now();
276288
await this.checkRunRepository.complete(checkRun.id, {
277289
status: result.totalFindings > 0 ? 'failed' : 'success',
278290
durationMs: Date.now() - startTime,
279291
totalChecked: result.results.length,
280292
passedCount: result.totalPassing,
281293
failedCount: result.totalFindings,
294+
logs: allLogs.length > 0
295+
? (allLogs as unknown as Prisma.InputJsonValue)
296+
: undefined,
282297
});
283298

284299
return {
@@ -288,20 +303,29 @@ export class ChecksController {
288303
...result,
289304
};
290305
} catch (error) {
291-
// Mark the check run as failed
306+
// Mark the check run as failed with error details
292307
const startTime = checkRun.startedAt?.getTime() || Date.now();
308+
const errorMessage = error instanceof Error ? error.message : String(error);
309+
const errorStack = error instanceof Error ? error.stack : undefined;
293310
await this.checkRunRepository.complete(checkRun.id, {
294311
status: 'failed',
295312
durationMs: Date.now() - startTime,
296313
totalChecked: 0,
297314
passedCount: 0,
298315
failedCount: 0,
299-
errorMessage: error instanceof Error ? error.message : String(error),
316+
errorMessage,
317+
logs: [{
318+
check: body.checkId || 'all',
319+
level: 'error',
320+
message: errorMessage,
321+
...(errorStack ? { data: { stack: errorStack } } : {}),
322+
timestamp: new Date().toISOString(),
323+
}] as unknown as Prisma.InputJsonValue,
300324
});
301325

302326
this.logger.error(`Check execution failed: ${error}`);
303327
throw new HttpException(
304-
`Check execution failed: ${error instanceof Error ? error.message : String(error)}`,
328+
`Check execution failed: ${errorMessage}`,
305329
HttpStatus.INTERNAL_SERVER_ERROR,
306330
);
307331
}

‎apps/api/src/integration-platform/controllers/dynamic-integrations.controller.ts‎

Lines changed: 158 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,17 @@ import {
1313
UseGuards,
1414
} from '@nestjs/common';
1515
import type { Prisma } from '@prisma/client';
16+
import { db } from '@db';
1617
import { InternalTokenGuard } from '../../auth/internal-token.guard';
1718
import { DynamicIntegrationRepository } from '../repositories/dynamic-integration.repository';
1819
import { DynamicCheckRepository } from '../repositories/dynamic-check.repository';
1920
import { ProviderRepository } from '../repositories/provider.repository';
21+
import { CheckRunRepository } from '../repositories/check-run.repository';
2022
import { DynamicManifestLoaderService } from '../services/dynamic-manifest-loader.service';
21-
import { validateIntegrationDefinition } from '@trycompai/integration-platform';
23+
import {
24+
validateIntegrationDefinition,
25+
SyncDefinitionSchema,
26+
} from '@trycompai/integration-platform';
2227

2328
@Controller({ path: 'internal/dynamic-integrations', version: '1' })
2429
@UseGuards(InternalTokenGuard)
@@ -29,6 +34,7 @@ export class DynamicIntegrationsController {
2934
private readonly dynamicIntegrationRepo: DynamicIntegrationRepository,
3035
private readonly dynamicCheckRepo: DynamicCheckRepository,
3136
private readonly providerRepo: ProviderRepository,
37+
private readonly checkRunRepo: CheckRunRepository,
3238
private readonly loaderService: DynamicManifestLoaderService,
3339
) {}
3440

@@ -48,6 +54,12 @@ export class DynamicIntegrationsController {
4854

4955
const def = validation.data!;
5056

57+
// Validate and store syncDefinition through Zod to apply defaults (e.g., employeesPath)
58+
const rawSyncDef = (body as Record<string, unknown>).syncDefinition;
59+
const validatedSyncDef = rawSyncDef
60+
? SyncDefinitionSchema.parse(rawSyncDef)
61+
: undefined;
62+
5163
// Upsert the integration
5264
const integration = await this.dynamicIntegrationRepo.upsertBySlug({
5365
slug: def.slug,
@@ -61,6 +73,9 @@ export class DynamicIntegrationsController {
6173
authConfig: def.authConfig as unknown as Prisma.InputJsonValue,
6274
capabilities: def.capabilities as unknown as Prisma.InputJsonValue,
6375
supportsMultipleConnections: def.supportsMultipleConnections,
76+
syncDefinition: validatedSyncDef
77+
? (JSON.parse(JSON.stringify(validatedSyncDef)) as Prisma.InputJsonValue)
78+
: null,
6479
});
6580

6681
// Delete checks not in the new definition, then upsert the rest
@@ -132,6 +147,10 @@ export class DynamicIntegrationsController {
132147
);
133148
}
134149

150+
const rawSyncDefCreate = (body as Record<string, unknown>).syncDefinition;
151+
const validatedSyncDefCreate = rawSyncDefCreate
152+
? SyncDefinitionSchema.parse(rawSyncDefCreate)
153+
: undefined;
135154
const integration = await this.dynamicIntegrationRepo.create({
136155
slug: def.slug,
137156
name: def.name,
@@ -144,6 +163,9 @@ export class DynamicIntegrationsController {
144163
authConfig: def.authConfig as unknown as Prisma.InputJsonValue,
145164
capabilities: def.capabilities as unknown as Prisma.InputJsonValue,
146165
supportsMultipleConnections: def.supportsMultipleConnections,
166+
syncDefinition: validatedSyncDefCreate
167+
? (JSON.parse(JSON.stringify(validatedSyncDefCreate)) as Prisma.InputJsonValue)
168+
: undefined,
147169
});
148170

149171
for (const [index, check] of def.checks.entries()) {
@@ -365,4 +387,139 @@ export class DynamicIntegrationsController {
365387
this.logger.log(`Deactivated dynamic integration: ${integration.slug}`);
366388
return { success: true };
367389
}
390+
391+
// ==================== Agent Debugging Endpoints ====================
392+
393+
/**
394+
* Validate a definition without saving.
395+
* Agents use this to check syntax/structure before committing.
396+
*/
397+
@Post('validate')
398+
async validate(@Body() body: Record<string, unknown>) {
399+
const result = validateIntegrationDefinition(body);
400+
401+
if (!result.success) {
402+
return {
403+
valid: false,
404+
errors: result.errors,
405+
};
406+
}
407+
408+
// validateIntegrationDefinition validates everything via Zod:
409+
// the manifest fields, all check definitions, and syncDefinition.
410+
// If we got here, the entire definition is valid.
411+
const definition = result.data!;
412+
413+
return {
414+
valid: true,
415+
summary: {
416+
slug: definition.slug,
417+
name: definition.name,
418+
category: definition.category,
419+
capabilities: definition.capabilities,
420+
checksCount: definition.checks.length,
421+
checkSlugs: definition.checks.map((c) => c.checkSlug),
422+
hasSyncDefinition: !!(body as Record<string, unknown>).syncDefinition,
423+
},
424+
};
425+
}
426+
427+
/**
428+
* Get recent check run history for a dynamic integration.
429+
* Agents use this to debug failing checks — includes full logs and results.
430+
*/
431+
@Get(':id/check-runs')
432+
async getCheckRuns(@Param('id') id: string) {
433+
const integration = await this.dynamicIntegrationRepo.findById(id);
434+
if (!integration) {
435+
throw new HttpException('Dynamic integration not found', HttpStatus.NOT_FOUND);
436+
}
437+
438+
// Find all connections for this provider
439+
const connections = await db.integrationConnection.findMany({
440+
where: {
441+
provider: { slug: integration.slug },
442+
status: 'active',
443+
},
444+
select: { id: true, organizationId: true },
445+
});
446+
447+
if (connections.length === 0) {
448+
return { runs: [], total: 0 };
449+
}
450+
451+
// Get recent runs across all connections
452+
const runs = await db.integrationCheckRun.findMany({
453+
where: {
454+
connectionId: { in: connections.map((c) => c.id) },
455+
},
456+
include: {
457+
results: {
458+
select: {
459+
id: true,
460+
passed: true,
461+
title: true,
462+
resourceType: true,
463+
resourceId: true,
464+
severity: true,
465+
remediation: true,
466+
},
467+
},
468+
},
469+
orderBy: { createdAt: 'desc' },
470+
take: 20,
471+
});
472+
473+
return {
474+
runs: runs.map((run) => ({
475+
id: run.id,
476+
checkId: run.checkId,
477+
checkName: run.checkName,
478+
connectionId: run.connectionId,
479+
status: run.status,
480+
startedAt: run.startedAt,
481+
completedAt: run.completedAt,
482+
durationMs: run.durationMs,
483+
totalChecked: run.totalChecked,
484+
passedCount: run.passedCount,
485+
failedCount: run.failedCount,
486+
errorMessage: run.errorMessage,
487+
logs: run.logs,
488+
results: run.results,
489+
})),
490+
total: runs.length,
491+
};
492+
}
493+
494+
/**
495+
* Get a single check run with full details (logs, results, error info).
496+
* Agents use this to debug a specific failed run.
497+
*/
498+
@Get('check-runs/:runId')
499+
async getCheckRunById(@Param('runId') runId: string) {
500+
const run = await this.checkRunRepo.findById(runId);
501+
if (!run) {
502+
throw new HttpException('Check run not found', HttpStatus.NOT_FOUND);
503+
}
504+
505+
return {
506+
id: run.id,
507+
checkId: run.checkId,
508+
checkName: run.checkName,
509+
connectionId: run.connectionId,
510+
status: run.status,
511+
startedAt: run.startedAt,
512+
completedAt: run.completedAt,
513+
durationMs: run.durationMs,
514+
totalChecked: run.totalChecked,
515+
passedCount: run.passedCount,
516+
failedCount: run.failedCount,
517+
errorMessage: run.errorMessage,
518+
logs: run.logs,
519+
results: run.results,
520+
provider: run.connection?.provider
521+
? { slug: run.connection.provider.slug, name: run.connection.provider.name }
522+
: null,
523+
};
524+
}
368525
}

0 commit comments

Comments
 (0)