import axios, { AxiosError } from 'axios';
import { createLogger } from './utils/logger.js';
import { ErrorCategory, createClassifiedError, classifyGraphQLError } from './utils/errors.js';
import { RunChecksProcessor } from './utils/graphql/processors/run-checks-processor.js';
import {
MetricShortcode,
MetricKey,
MetricThresholdStatus,
MetricDirection,
RepositoryMetric,
RepositoryMetricItem,
MetricSetting,
UpdateMetricThresholdParams,
UpdateMetricSettingParams,
MetricThresholdUpdateResponse,
MetricSettingUpdateResponse,
MetricHistoryParams,
MetricHistoryResponse,
MetricHistoryValue,
} from './types/metrics.js';
import {
asGraphQLNodeId,
asRunId,
asCommitOid,
asBranchName,
asAnalyzerShortcode,
GraphQLNodeId,
RunId,
CommitOid,
BranchName,
AnalyzerShortcode,
} from './types/branded.js';
/**
* @fileoverview DeepSource API client for interacting with the DeepSource service.
* This module exports interfaces and classes for working with the DeepSource API.
* @packageDocumentation
*/
// Interfaces and types below are exported as part of the public API
// Re-export quality metrics types
export { MetricShortcode, MetricDirection };
export type {
MetricKey,
MetricThresholdStatus,
RepositoryMetric,
RepositoryMetricItem,
MetricSetting,
UpdateMetricThresholdParams,
UpdateMetricSettingParams,
MetricThresholdUpdateResponse,
MetricSettingUpdateResponse,
MetricHistoryParams,
MetricHistoryResponse,
MetricHistoryValue,
};
/**
* Available report types in DeepSource
* This enum combines both compliance-specific and general report types
* and is referenced in API functions like getComplianceReport() and handleDeepsourceComplianceReport().
* @public
*/
export enum ReportType {
// Compliance-specific report types
OWASP_TOP_10 = 'OWASP_TOP_10',
SANS_TOP_25 = 'SANS_TOP_25',
MISRA_C = 'MISRA_C',
// General report types
CODE_COVERAGE = 'CODE_COVERAGE',
CODE_HEALTH_TREND = 'CODE_HEALTH_TREND',
ISSUE_DISTRIBUTION = 'ISSUE_DISTRIBUTION',
ISSUES_PREVENTED = 'ISSUES_PREVENTED',
ISSUES_AUTOFIXED = 'ISSUES_AUTOFIXED',
}
/**
* Report status indicating whether the report is passing, failing, or not applicable
* This enum is exported as part of the public API for use in MCP tools
* and is referenced in handleDeepsourceComplianceReport().
* @public
*/
export enum ReportStatus {
PASSING = 'PASSING',
FAILING = 'FAILING',
NOOP = 'NOOP',
}
/**
* Trend information for reports
* @public
*/
export interface ReportTrend {
label?: string;
value?: number;
changePercentage?: number;
}
/**
* Severity distribution of issues
* @public
*/
export interface SeverityDistribution {
critical: number;
major: number;
minor: number;
total: number;
}
/**
* Security issue statistic
* @public
*/
export interface SecurityIssueStat {
key: string;
title: string;
occurrence: SeverityDistribution;
}
/**
* Compliance report interface
* @public
*/
export interface ComplianceReport {
key: ReportType;
title: string;
currentValue?: number;
status?: ReportStatus;
securityIssueStats: SecurityIssueStat[];
trends?: ReportTrend[];
}
/**
* Represents a DeepSource project in the API
* @public
*/
export interface DeepSourceProject {
key: string;
name: string;
repository: {
url: string;
provider: string;
login: string;
isPrivate: boolean;
isActivated: boolean;
};
}
/**
* Represents an issue found by DeepSource analysis
* @public
*/
export interface DeepSourceIssue {
id: string;
title: string;
shortcode: string;
category: string;
severity: string;
status: string;
issue_text: string;
file_path: string;
line_number: number;
tags: string[];
}
/**
* Distribution of occurrences by analyzer type
* @public
*/
export interface OccurrenceDistributionByAnalyzer {
analyzerShortcode: AnalyzerShortcode;
introduced: number;
}
/**
* Distribution of occurrences by category
* @public
*/
export interface OccurrenceDistributionByCategory {
category: string;
introduced: number;
}
/**
* Summary of an analysis run, including counts of issues
* @public
*/
export interface RunSummary {
occurrencesIntroduced: number;
occurrencesResolved: number;
occurrencesSuppressed: number;
occurrenceDistributionByAnalyzer?: OccurrenceDistributionByAnalyzer[];
occurrenceDistributionByCategory?: OccurrenceDistributionByCategory[];
}
/**
* Possible status values for an analysis run
* Using a type instead of enum to avoid unused enum values linting errors
* @public
*/
export type AnalysisRunStatus =
| 'PENDING'
| 'SUCCESS'
| 'FAILURE'
| 'TIMEOUT'
| 'CANCEL'
| 'READY'
| 'SKIPPED';
/**
* Represents a DeepSource analysis run
* @public
*/
export interface DeepSourceRun {
id: GraphQLNodeId;
runUid: RunId;
commitOid: CommitOid;
branchName: BranchName;
baseOid: CommitOid;
status: AnalysisRunStatus;
createdAt: string;
updatedAt: string;
finishedAt?: string;
summary: RunSummary;
repository: {
name: string;
id: GraphQLNodeId;
};
}
/**
* Possible severity levels for a vulnerability
* Represents the qualitative assessment of the vulnerability's impact
* @public
*/
export type VulnerabilitySeverity =
/** No meaningful risk */
| 'NONE'
/** Limited impact, typically requiring complex exploitation */
| 'LOW'
/** Significant impact but with mitigating factors */
| 'MEDIUM'
/** Serious impact with straightforward exploitation */
| 'HIGH'
/** Critical impact with easy exploitation or catastrophic consequences */
| 'CRITICAL';
/**
* Possible package version types
* Defines how the version numbering scheme for a package should be interpreted
* @public
*/
export type PackageVersionType =
/** Semantic Versioning (major.minor.patch) */
| 'SEMVER'
/** Ecosystem-specific versioning scheme */
| 'ECOSYSTEM'
/** Git-based versioning (commit hashes or tags) */
| 'GIT';
/**
* Possible reachability types for a vulnerability occurrence
* Indicates whether the vulnerable code can be triggered in the codebase
* @public
*/
export type VulnerabilityReachability =
/** The vulnerability is reachable from execution paths in the code */
| 'REACHABLE'
/** The vulnerability exists but is not reachable in execution paths */
| 'UNREACHABLE'
/** Reachability could not be determined */
| 'UNKNOWN';
/**
* Possible fixability types for a vulnerability occurrence
* Indicates whether and how the vulnerability can be fixed
* @public
*/
export type VulnerabilityFixability =
/** An error occurred during fixability analysis */
| 'ERROR'
/** The vulnerability cannot be fixed with current methods */
| 'UNFIXABLE'
/** A fix is currently being generated */
| 'GENERATING_FIX'
/** The vulnerability might be fixable but requires further analysis */
| 'POSSIBLY_FIXABLE'
/** The vulnerability can be fixed manually following guidelines */
| 'MANUALLY_FIXABLE'
/** The vulnerability can be fixed automatically */
| 'AUTO_FIXABLE';
/**
* Represents a package in the DeepSource API
* Contains information about a software package in a specific ecosystem
* @public
*/
export interface Package {
/** Unique identifier of the package */
id: string;
/** Package ecosystem (e.g., 'NPM', 'PYPI', 'MAVEN') */
ecosystem: string;
/** Package name as it appears in the ecosystem */
name: string;
/** Package URL (optional) - follows the package URL specification (https://github.com/package-url/purl-spec) */
purl?: string;
}
/**
* Represents a package version in the DeepSource API
* Contains information about a specific version of a package
* @public
*/
export interface PackageVersion {
/** Unique identifier of the package version */
id: string;
/** Version string (e.g., '1.2.3') */
version: string;
/** Type of versioning used (SEMVER, ECOSYSTEM, GIT) */
versionType?: PackageVersionType;
}
/**
* Represents a vulnerability in the DeepSource API
* Contains detailed information about a security vulnerability
* @public
*/
export interface Vulnerability {
/** Unique identifier of the vulnerability */
id: string;
/** Standard identifier for the vulnerability (e.g., CVE-2022-1234) */
identifier: string;
/** Alternative identifiers for the same vulnerability (e.g., GHSA-xxxx-xxxx-xxxx) */
aliases: string[];
/** Brief description of the vulnerability */
summary?: string;
/** Detailed description of the vulnerability */
details?: string;
/** Date when the vulnerability was first published */
publishedAt: string;
/** Date when the vulnerability information was last updated */
updatedAt: string;
/** Date when the vulnerability was withdrawn (if applicable) */
withdrawnAt?: string;
/** Overall severity rating of the vulnerability */
severity: VulnerabilitySeverity;
// CVSS v2 information
/** CVSS v2 vector string representing the vulnerability characteristics */
cvssV2Vector?: string;
/** CVSS v2 base score (0.0-10.0) */
cvssV2BaseScore?: number;
/** CVSS v2 qualitative severity rating */
cvssV2Severity?: VulnerabilitySeverity;
// CVSS v3 information
/** CVSS v3 vector string representing the vulnerability characteristics */
cvssV3Vector?: string;
/** CVSS v3 base score (0.0-10.0) */
cvssV3BaseScore?: number;
/** CVSS v3 qualitative severity rating */
cvssV3Severity?: VulnerabilitySeverity;
// CVSS v4 information
/** CVSS v4 vector string representing the vulnerability characteristics */
cvssV4Vector?: string;
/** CVSS v4 base score (0.0-10.0) */
cvssV4BaseScore?: number;
/** CVSS v4 qualitative severity rating */
cvssV4Severity?: VulnerabilitySeverity;
// EPSS information
/** Exploit Prediction Scoring System score (0.0-1.0) */
epssScore?: number;
/** EPSS percentile, indicating relative likelihood of exploitation */
epssPercentile?: number;
// Version information
/** List of package versions where the vulnerability was introduced */
introducedVersions: string[];
/** List of package versions where the vulnerability was fixed */
fixedVersions: string[];
// References
/** List of URLs to external references about this vulnerability */
referenceUrls: string[];
}
/**
* Represents a vulnerability occurrence in the DeepSource API
* A vulnerability occurrence is an instance of a vulnerability affecting a specific package version
* in a specific project context
* @public
*/
export interface VulnerabilityOccurrence {
/** Unique identifier of the vulnerability occurrence */
id: string;
/** Information about the affected package */
package: Package;
/** Information about the affected package version */
packageVersion: PackageVersion;
/** Details about the vulnerability */
vulnerability: Vulnerability;
/** Whether the vulnerability is reachable in the codebase (REACHABLE, UNREACHABLE, UNKNOWN) */
reachability: VulnerabilityReachability;
/** Whether and how the vulnerability can be fixed */
fixability: VulnerabilityFixability;
}
/**
* Parameters for paginating through API results
* @public
*/
export interface PaginationParams {
/** Legacy pagination: Number of items to skip */
offset?: number;
/** Relay-style pagination: Number of items to return after the 'after' cursor */
first?: number;
/** Relay-style pagination: Cursor to fetch records after this cursor */
after?: string;
/** Relay-style pagination: Cursor to fetch records before this cursor */
before?: string;
/** Relay-style pagination: Number of items to return before the 'before' cursor */
last?: number;
}
/**
* Parameters for filtering issues
* @public
*/
export interface IssueFilterParams extends PaginationParams {
/** Filter issues by path (file path) */
path?: string;
/** Filter issues by analyzer shortcodes (e.g. ["python", "javascript"]) */
analyzerIn?: string[];
/** Filter issues by tags */
tags?: string[];
}
/**
* Parameters for filtering runs
* @public
*/
export interface RunFilterParams extends PaginationParams {
/** Filter runs by analyzer shortcodes (e.g. ["python", "javascript"]) */
analyzerIn?: string[];
}
/**
* Generic response structure containing paginated results
* @public
* @template T - The type of items in the response
*/
export interface PaginatedResponse<T> {
items: T[];
pageInfo: {
hasNextPage: boolean;
hasPreviousPage: boolean;
startCursor?: string;
endCursor?: string;
};
totalCount: number;
}
/**
* Response structure for recent run issues
* @public
*/
export interface RecentRunIssuesResponse extends PaginatedResponse<DeepSourceIssue> {
/** The most recent run for the branch */
run: DeepSourceRun;
}
/**
* Client for interacting with the DeepSource GraphQL API
* Provides methods for querying projects, issues, analysis runs, and dependency vulnerabilities
* Supports both legacy and Relay-style cursor-based pagination
* @class
*/
export class DeepSourceClient {
/**
* HTTP client for making API requests to DeepSource
* @private
*/
private client;
/**
* Logger instance for the DeepSourceClient
* @private
*/
private logger = createLogger('DeepSourceClient');
/**
* Static logger for static methods
* @private
*/
private static logger = createLogger('DeepSourceClient:static');
/**
* Creates a new DeepSourceClient instance
* @param apiKey - The DeepSource API key for authentication
* @param options - Additional configuration options
* @param options.baseURL - Custom API endpoint URL (defaults to DeepSource production API)
* @param options.timeout - Request timeout in milliseconds (defaults to 30000ms)
* @throws {Error} When apiKey is not provided or invalid
* @throws {Error} When timeout is not a valid number
* @param apiKey - DeepSource API key for authentication
*/
constructor(apiKey: string) {
this.client = axios.create({
baseURL: 'https://api.deepsource.io/graphql/',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Bearer ${apiKey}`,
},
});
}
/**
* Extracts error messages from GraphQL error response
* @param errors - Array of GraphQL error objects
* @returns Formatted error message string
* @private
*/
private static extractErrorMessages(errors: Array<{ message: string }>): string {
const errorMessages = errors.map((error) => error.message);
return errorMessages.join(', ');
}
/**
* Process issues from the GraphQL response
* @private
*/
private static processRunChecksResponse(response: unknown): {
issues: DeepSourceIssue[];
pageInfo: {
hasNextPage: boolean;
hasPreviousPage: boolean;
startCursor: string | undefined;
endCursor: string | undefined;
};
totalCount: number;
} {
return RunChecksProcessor.process(response);
}
/**
* Type guard to check if an unknown error is an Error object
* @param error The error to check
* @returns True if the error is an Error instance
* @private
*/
private static isError(error: unknown): error is Error {
return (
error !== null &&
typeof error === 'object' &&
'message' in error &&
typeof (error as Record<string, unknown>).message === 'string'
);
}
/**
* Type guard to check if an error contains a specific message substring
* @param error The error to check
* @param substring The substring to search for in the error message
* @returns True if the error is an Error with the specified substring
* @private
*/
private static isErrorWithMessage(error: unknown, substring: string): error is Error {
return DeepSourceClient.isError(error) && error.message?.includes(substring);
}
/**
* Checks if an error is an Axios error with specific characteristics
* @param error The error to check
* @param statusCode Optional HTTP status code to match
* @param errorCode Optional Axios error code to match
* @returns True if the error matches the criteria and is an AxiosError, false otherwise
* @private
*/
private static isAxiosErrorWithCriteria(
error: unknown,
statusCode?: number,
errorCode?: string
): error is AxiosError {
// Check if it's an object first
if (!error || typeof error !== 'object') {
return false;
}
// Check if it has the axios error shape and matches all criteria
const potentialAxiosError = error as Partial<AxiosError>;
return (
Boolean(potentialAxiosError.isAxiosError) &&
(statusCode === undefined || potentialAxiosError.response?.status === statusCode) &&
(errorCode === undefined || potentialAxiosError.code === errorCode)
);
}
/**
* Handles GraphQL-specific errors from Axios responses
* @param error The error to check for GraphQL errors
* @returns True if the error was handled (and thrown)
* @private
*/
private static handleGraphQLSpecificError(error: unknown): never | false {
if (
DeepSourceClient.isAxiosErrorWithCriteria(error) &&
typeof error.response?.data === 'object' &&
error.response.data && // Add null check
'errors' in error.response.data
) {
const graphqlErrors: Array<{ message: string }> = error.response.data.errors as Array<{
message: string;
}>; // Type assertion after validation
const errorMessage = DeepSourceClient.extractErrorMessages(graphqlErrors);
// Create a combined error message
const combinedError = new Error(`GraphQL Error: ${errorMessage}`);
// Classify the error
const category = classifyGraphQLError(combinedError);
throw createClassifiedError(combinedError.message, category, error, { graphqlErrors });
}
return false;
}
/**
* Handles network and connection errors
* @param error The error to check
* @returns True if the error was handled (and thrown)
* @private
*/
private static handleNetworkError(error: unknown): never | false {
if (DeepSourceClient.isAxiosErrorWithCriteria(error, undefined, 'ECONNREFUSED')) {
throw createClassifiedError(
'Connection error: Unable to connect to DeepSource API',
ErrorCategory.NETWORK,
error
);
}
if (DeepSourceClient.isAxiosErrorWithCriteria(error, undefined, 'ETIMEDOUT')) {
throw createClassifiedError(
'Timeout error: DeepSource API request timed out',
ErrorCategory.TIMEOUT,
error
);
}
return false;
}
/**
* Handles HTTP status-specific errors
* @param error The error to check
* @returns True if the error was handled (and thrown)
* @private
*/
private static handleHttpStatusError(error: unknown): never | false {
if (DeepSourceClient.isAxiosErrorWithCriteria(error, 401)) {
throw createClassifiedError(
'Authentication error: Invalid or expired API key',
ErrorCategory.AUTH,
error
);
}
if (DeepSourceClient.isAxiosErrorWithCriteria(error, 429)) {
throw createClassifiedError(
'Rate limit exceeded: Too many requests to DeepSource API',
ErrorCategory.RATE_LIMIT,
error
);
}
// Handle other common HTTP status codes
const axiosError = error as AxiosError;
if (axiosError.response?.status) {
const status = axiosError.response.status;
if (status >= 500) {
throw createClassifiedError(
`Server error (${status}): DeepSource API server error`,
ErrorCategory.SERVER,
error
);
}
if (status === 404) {
throw createClassifiedError(
'Not found (404): The requested resource was not found',
ErrorCategory.NOT_FOUND,
error
);
}
if (status >= 400 && status < 500) {
throw createClassifiedError(
`Client error (${status}): ${axiosError.response.statusText || 'Bad request'}`,
ErrorCategory.CLIENT,
error
);
}
}
return false;
}
/**
* Handles generic errors
* @param error The error to process
* @returns Never returns, always throws
* @private
*/
private static handleGenericError(error: unknown): never {
if (DeepSourceClient.isError(error)) {
throw new Error(`DeepSource API error: ${error.message}`);
}
throw new Error('Unknown error occurred while communicating with DeepSource API');
}
/**
* Main error handler that coordinates all error processing
* @param error The error to handle
* @throws {Error} Appropriate error message based on error type
* @throws {Error} Classified error with category, original error, and additional metadata
* @private
*/
private static handleGraphQLError(error: Error | unknown): never {
// If it's already a classified error, just throw it
if (error && typeof error === 'object' && 'category' in error) {
throw error;
}
// Try handling specific error types in order of specificity
if (this.handleGraphQLSpecificError(error)) {
// If handleGraphQLSpecificError returns true, it already threw an error
// This line will never be reached, but is needed for type checking
throw new Error('Unreachable code - handleGraphQLSpecificError should have thrown');
}
if (this.handleNetworkError(error)) {
throw new Error('Unreachable code - handleNetworkError should have thrown');
}
if (this.handleHttpStatusError(error)) {
throw new Error('Unreachable code - handleHttpStatusError should have thrown');
}
// If no specific handler worked, convert to a classified error
if (DeepSourceClient.isError(error)) {
const category = classifyGraphQLError(error);
throw createClassifiedError(`DeepSource API error: ${error.message}`, category, error);
}
// Last resort for truly unknown errors
throw createClassifiedError(
'Unknown error occurred while communicating with DeepSource API',
ErrorCategory.OTHER,
error
);
}
/**
* Creates an empty paginated response
* @template T The type of items in the response
* @returns {PaginatedResponse<T>} Empty paginated response with consistent structure
* @private
*/
private static createEmptyPaginatedResponse<T>(): PaginatedResponse<T> {
const pageInfo: {
hasNextPage: boolean;
hasPreviousPage: boolean;
startCursor?: string;
endCursor?: string;
} = {
hasNextPage: false,
hasPreviousPage: false,
};
return {
items: [],
pageInfo,
totalCount: 0,
};
}
/**
* Logs a warning message about non-standard pagination usage
*
* This method provides consistent warning messages for pagination anti-patterns
* in Relay-style cursor-based pagination. It helps developers understand
* why their pagination approach might cause unexpected behavior.
*
* @param message Optional custom warning message to use instead of the default
* @private
*/
private static logPaginationWarning(message?: string): void {
// Using the static logger instead of console.warn for better log management
const warningMessage =
message ||
'Non-standard pagination: Using "last" without "before" is not recommended in Relay pagination';
DeepSourceClient.logger.warn(warningMessage);
}
/**
* Normalizes pagination parameters for GraphQL queries
* Ensures consistency in pagination parameters following Relay pagination best practices
*
* Normalization rules:
* 1. If 'before' is provided (backward pagination):
* - Use 'last' as the count parameter (default: 10)
* - Remove any 'first' parameter to avoid ambiguity
* 2. If 'last' is provided without 'before' (non-standard but supported):
* - Keep 'last' as is
* - Remove any 'first' parameter to avoid ambiguity
* - Log a warning about non-standard usage
* 3. Otherwise (forward pagination or defaults):
* - Use 'first' as the count parameter (default: 10)
* - Remove any 'last' parameter to avoid ambiguity
*
* @template T Type that extends PaginationParams
* @param {T} params - Original pagination parameters
* @returns {T} Normalized pagination parameters with consistent values
* @private
*/
private static normalizePaginationParams<T extends PaginationParams>(params: T): T {
const normalizedParams = { ...params };
// Validate and normalize numerical parameters
if (normalizedParams.offset !== undefined) {
normalizedParams.offset = Math.max(0, Math.floor(Number(normalizedParams.offset)));
}
if (normalizedParams.first !== undefined) {
// Ensure first is a positive integer or undefined
normalizedParams.first = Math.max(1, Math.floor(Number(normalizedParams.first)));
}
if (normalizedParams.last !== undefined) {
// Ensure last is a positive integer or undefined
normalizedParams.last = Math.max(1, Math.floor(Number(normalizedParams.last)));
}
// Validate cursor parameters (ensure they're valid strings)
if (normalizedParams.after !== undefined && typeof normalizedParams.after !== 'string') {
normalizedParams.after = String(normalizedParams.after ?? '');
}
if (normalizedParams.before !== undefined && typeof normalizedParams.before !== 'string') {
normalizedParams.before = String(normalizedParams.before ?? '');
}
// Apply Relay pagination rules
if (normalizedParams.before) {
// When fetching backwards with 'before', prioritize 'last'
normalizedParams.last = normalizedParams.last ?? normalizedParams.first ?? 10;
delete normalizedParams.first;
} else if (normalizedParams.last) {
// If 'last' is provided without 'before', log a warning but still use 'last'
DeepSourceClient.logPaginationWarning(
`Non-standard pagination: Using "last=${normalizedParams.last}" without "before" cursor is not recommended`
);
// Keep normalizedParams.last as is
delete normalizedParams.first;
} else {
// Default or forward pagination with 'after', prioritize 'first'
normalizedParams.first = normalizedParams.first ?? 10;
delete normalizedParams.last;
}
return normalizedParams;
}
/**
* Fetches a list of all accessible DeepSource projects
* @returns Promise that resolves to an array of DeepSourceProject objects
* @throws {Error} When DeepSource API returns errors
* @throws {Error} When network or authentication issues occur
*/
async listProjects(): Promise<DeepSourceProject[]> {
try {
const viewerQuery =
'query {\n viewer {\n email\n accounts {\n edges {\n node {\n login\n repositories(first: 100) {\n edges {\n node {\n name\n defaultBranch\n dsn\n isPrivate\n isActivated\n vcsProvider\n }\n }\n }\n }\n }\n }\n }\n }\n';
const response = await this.client.post('', {
query: viewerQuery.trim(),
});
if (response.data.errors) {
const errorMessage = DeepSourceClient.extractErrorMessages(response.data.errors);
throw new Error(`GraphQL Errors: ${errorMessage}`);
}
const accounts = response.data.data?.viewer?.accounts?.edges ?? [];
const allRepos: DeepSourceProject[] = [];
for (const { node: account } of accounts) {
const repos = account.repositories?.edges ?? [];
for (const { node: repo } of repos) {
if (!repo.dsn) continue;
allRepos.push({
key: repo.dsn,
name: repo.name ?? 'Unnamed Repository',
repository: {
url: repo.dsn,
provider: repo.vcsProvider ?? 'N/A',
login: account.login,
isPrivate: repo.isPrivate ?? false,
isActivated: repo.isActivated ?? false,
},
});
}
}
return allRepos;
} catch (error) {
if (DeepSourceClient.isErrorWithMessage(error, 'NoneType')) {
return [];
}
return DeepSourceClient.handleGraphQLError(error);
}
}
/**
* Fetches issues from a specified DeepSource project
* @param projectKey - The unique identifier for the DeepSource project
* @param params - Optional pagination and filtering parameters for the query.
* Supports both legacy pagination (offset) and Relay-style cursor-based pagination.
* For forward pagination use 'first' with optional 'after' cursor.
* For backward pagination use 'last' with optional 'before' cursor.
* Note: Using both 'first' and 'last' together is not recommended and will prioritize
* 'last' if 'before' is provided, otherwise will prioritize 'first'.
*
* When 'last' is provided without 'before', a warning will be logged, but the
* request will still be processed using 'last'. For standard Relay behavior,
* 'last' should always be accompanied by 'before'.
*
* Filtering parameters:
* - path: Filter issues by specific file path
* - analyzerIn: Filter issues by specific analyzers
* - tags: Filter issues by tags
* @returns Promise that resolves to a paginated response containing DeepSource issues
* @throws {Error} When project key is invalid or project doesn't exist
* @throws {Error} When DeepSource API returns errors
* @throws {Error} When network, authentication or permission issues occur
*/
async getIssues(
projectKey: string,
params: IssueFilterParams = {}
): Promise<PaginatedResponse<DeepSourceIssue>> {
try {
const projects = await this.listProjects();
const project = projects.find((p) => p.key === projectKey);
if (!project) {
return DeepSourceClient.createEmptyPaginatedResponse<DeepSourceIssue>();
}
// Normalize pagination parameters using the static helper method
const normalizedParams = DeepSourceClient.normalizePaginationParams(params);
// Keeping template literal here since it contains a lot of variable references
// with complex GraphQL query structure. The benefits of converting to string
// concatenation would be outweighed by reduced readability
const repoQuery =
'query($login: String!, $name: String!, $provider: VCSProvider!, $offset: Int, $first: Int, $after: String, $before: String, $last: Int, $path: String, $analyzerIn: [String], $tags: [String]) {\n repository(login: $login, name: $name, vcsProvider: $provider) {\n name\n defaultBranch\n dsn\n isPrivate\n issues(offset: $offset, first: $first, after: $after, before: $before, last: $last, path: $path, analyzerIn: $analyzerIn, tags: $tags) {\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n totalCount\n edges {\n node {\n id\n issue {\n shortcode\n title\n category\n severity\n description\n tags\n }\n occurrences(first: 100) {\n edges {\n node {\n id\n path\n beginLine\n endLine\n beginColumn\n endColumn\n title\n }\n }\n }\n }\n }\n }\n }\n }\n';
const response = await this.client.post('', {
query: repoQuery.trim(),
variables: {
login: project.repository.login,
name: project.name,
provider: project.repository.provider,
offset: normalizedParams.offset,
first: normalizedParams.first,
after: normalizedParams.after,
before: normalizedParams.before,
last: normalizedParams.last,
path: normalizedParams.path,
analyzerIn: normalizedParams.analyzerIn,
tags: normalizedParams.tags,
},
});
if (response.data.errors) {
const errorMessage = DeepSourceClient.extractErrorMessages(response.data.errors);
throw new Error(`GraphQL Errors: ${errorMessage}`);
}
const issues: DeepSourceIssue[] = [];
const repoIssues = response.data.data?.repository?.issues?.edges ?? [];
const pageInfo = response.data.data?.repository?.issues?.pageInfo ?? {
hasNextPage: false,
hasPreviousPage: false,
};
const totalCount = response.data.data?.repository?.issues?.totalCount ?? 0;
for (const { node: repoIssue } of repoIssues) {
const occurrences = repoIssue.occurrences?.edges ?? [];
for (const { node: occurrence } of occurrences) {
issues.push({
id: occurrence.id ?? 'unknown',
shortcode: repoIssue.issue?.shortcode ?? '',
title: repoIssue.issue?.title ?? 'Untitled Issue',
category: repoIssue.issue?.category ?? 'UNKNOWN',
severity: repoIssue.issue?.severity ?? 'UNKNOWN',
status: 'OPEN',
issue_text: repoIssue.issue?.description ?? '',
file_path: occurrence.path ?? 'N/A',
line_number: occurrence.beginLine ?? 0,
tags: repoIssue.issue?.tags ?? [],
});
}
}
return {
items: issues,
pageInfo: {
hasNextPage: pageInfo.hasNextPage,
hasPreviousPage: pageInfo.hasPreviousPage,
startCursor: pageInfo.startCursor,
endCursor: pageInfo.endCursor,
},
totalCount,
};
} catch (error) {
if (DeepSourceClient.isErrorWithMessage(error, 'NoneType')) {
return {
items: [],
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
},
totalCount: 0,
};
}
return DeepSourceClient.handleGraphQLError(error);
}
}
/**
* Fetches a specific issue from a DeepSource project by its ID
* @param projectKey - The unique identifier for the DeepSource project
* @param issueId - The unique identifier of the issue to retrieve
* @returns Promise that resolves to the issue if found, or null if not found
* @throws {Error} When DeepSource API returns errors
* @throws {Error} When network, authentication or permission issues occur
*/
async getIssue(projectKey: string, issueId: string): Promise<DeepSourceIssue | null> {
try {
const result = await this.getIssues(projectKey);
const issue = result.items.find((issue) => issue.id === issueId);
return issue || null;
} catch (error) {
return DeepSourceClient.handleGraphQLError(error);
}
}
/**
* Fetches analysis runs for a specified DeepSource project
* @param projectKey - The unique identifier for the DeepSource project
* @param params - Optional pagination and filtering parameters for the query
* Pagination supports both legacy pagination (offset) and Relay-style cursor-based pagination.
* Filtering parameters:
* - analyzerIn: Filter runs by specific analyzers
* @returns Promise that resolves to a paginated response containing DeepSource runs
* @throws {Error} When project key is invalid or project doesn't exist
* @throws {Error} When DeepSource API returns errors
* @throws {Error} When network, authentication or permission issues occur
*/
async listRuns(
projectKey: string,
params: RunFilterParams = {}
): Promise<PaginatedResponse<DeepSourceRun>> {
try {
const projects = await this.listProjects();
const project = projects.find((p) => p.key === projectKey);
if (!project) {
return DeepSourceClient.createEmptyPaginatedResponse<DeepSourceRun>();
}
// Normalize pagination parameters using the static helper method
const normalizedParams = DeepSourceClient.normalizePaginationParams(params);
const repoQuery =
'query($login: String!, $name: String!, $provider: VCSProvider!, $offset: Int, $first: Int, $after: String, $before: String, $last: Int, $analyzerIn: [String]) {\n repository(login: $login, name: $name, vcsProvider: $provider) {\n name\n id\n analysisRuns(offset: $offset, first: $first, after: $after, before: $before, last: $last) {\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n totalCount\n edges {\n node {\n id\n runUid\n commitOid\n branchName\n baseOid\n status\n createdAt\n updatedAt\n finishedAt\n summary {\n occurrencesIntroduced\n occurrencesResolved\n occurrencesSuppressed\n occurrenceDistributionByAnalyzer {\n analyzerShortcode\n introduced\n }\n occurrenceDistributionByCategory {\n category\n introduced\n }\n }\n repository {\n name\n id\n }\n checks(analyzerIn: $analyzerIn) {\n edges {\n node {\n analyzer {\n shortcode\n }\n }\n }\n }\n }\n }\n }\n }\n }\n';
const response = await this.client.post('', {
query: repoQuery.trim(),
variables: {
login: project.repository.login,
name: project.name,
provider: project.repository.provider,
offset: normalizedParams.offset,
first: normalizedParams.first,
after: normalizedParams.after,
before: normalizedParams.before,
last: normalizedParams.last,
analyzerIn: normalizedParams.analyzerIn,
},
});
if (response.data.errors) {
const errorMessage = DeepSourceClient.extractErrorMessages(response.data.errors);
throw new Error(`GraphQL Errors: ${errorMessage}`);
}
const runs: DeepSourceRun[] = [];
const repoRuns = response.data.data?.repository?.analysisRuns?.edges ?? [];
const pageInfo = response.data.data?.repository?.analysisRuns?.pageInfo ?? {
hasNextPage: false,
hasPreviousPage: false,
};
const totalCount = response.data.data?.repository?.analysisRuns?.totalCount ?? 0;
for (const { node: run } of repoRuns) {
runs.push({
id: asGraphQLNodeId(run.id),
runUid: asRunId(run.runUid),
commitOid: asCommitOid(run.commitOid),
branchName: asBranchName(run.branchName),
baseOid: asCommitOid(run.baseOid),
status: run.status,
createdAt: run.createdAt,
updatedAt: run.updatedAt,
finishedAt: run.finishedAt,
summary: {
occurrencesIntroduced: run.summary?.occurrencesIntroduced ?? 0,
occurrencesResolved: run.summary?.occurrencesResolved ?? 0,
occurrencesSuppressed: run.summary?.occurrencesSuppressed ?? 0,
occurrenceDistributionByAnalyzer:
run.summary?.occurrenceDistributionByAnalyzer?.map(
(dist: Record<string, unknown>) => ({
analyzerShortcode: asAnalyzerShortcode(String(dist.analyzerShortcode)),
introduced: Number(dist.introduced),
})
) ?? [],
occurrenceDistributionByCategory: run.summary?.occurrenceDistributionByCategory ?? [],
},
repository: {
name: run.repository?.name ?? '',
id: asGraphQLNodeId(run.repository?.id ?? ''),
},
});
}
return {
items: runs,
pageInfo: {
hasNextPage: pageInfo.hasNextPage,
hasPreviousPage: pageInfo.hasPreviousPage,
startCursor: pageInfo.startCursor,
endCursor: pageInfo.endCursor,
},
totalCount,
};
} catch (error) {
if (DeepSourceClient.isErrorWithMessage(error, 'NoneType')) {
return {
items: [],
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
},
totalCount: 0,
};
}
return DeepSourceClient.handleGraphQLError(error);
}
}
/**
* Fetches a specific analysis run by ID or commit hash
* @param runIdentifier - The runUid or commitOid to identify the run
* @returns Promise that resolves to the run if found, or null if not found
* @throws {Error} When runIdentifier is invalid
* @throws {Error} When DeepSource API returns errors
* @throws {Error} When network, authentication or permission issues occur
*/
async getRun(runIdentifier: string): Promise<DeepSourceRun | null> {
try {
// Determine if the identifier is a UUID or a commit hash
const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
runIdentifier
);
const runQuery =
'query($runUid: UUID, $commitOid: String) {\n run(runUid: $runUid, commitOid: $commitOid) {\n id\n runUid\n commitOid\n branchName\n baseOid\n status\n createdAt\n updatedAt\n finishedAt\n summary {\n occurrencesIntroduced\n occurrencesResolved\n occurrencesSuppressed\n occurrenceDistributionByAnalyzer {\n analyzerShortcode\n introduced\n }\n occurrenceDistributionByCategory {\n category\n introduced\n }\n }\n repository {\n name\n id\n }\n }\n }\n';
const response = await this.client.post('', {
query: runQuery.trim(),
variables: {
runUid: isUuid ? runIdentifier : null,
commitOid: !isUuid ? runIdentifier : null,
},
});
if (response.data.errors) {
// If the error is about not finding the run, return null
if (
response.data.errors.some(
(e: { message: string }) =>
e.message.includes('not found') || e.message.includes('NoneType')
)
) {
return null;
}
throw new Error(
`GraphQL Errors: ${response.data.errors.map((e: { message: string }) => e.message).join(', ')}`
);
}
const run = response.data.data?.run;
if (!run) {
return null;
}
return {
id: asGraphQLNodeId(run.id),
runUid: asRunId(run.runUid),
commitOid: asCommitOid(run.commitOid),
branchName: asBranchName(run.branchName),
baseOid: asCommitOid(run.baseOid),
status: run.status,
createdAt: run.createdAt,
updatedAt: run.updatedAt,
finishedAt: run.finishedAt,
summary: {
occurrencesIntroduced: run.summary?.occurrencesIntroduced ?? 0,
occurrencesResolved: run.summary?.occurrencesResolved ?? 0,
occurrencesSuppressed: run.summary?.occurrencesSuppressed ?? 0,
occurrenceDistributionByAnalyzer:
run.summary?.occurrenceDistributionByAnalyzer?.map((dist: Record<string, unknown>) => ({
analyzerShortcode: asAnalyzerShortcode(String(dist.analyzerShortcode)),
introduced: Number(dist.introduced),
})) ?? [],
occurrenceDistributionByCategory: run.summary?.occurrenceDistributionByCategory ?? [],
},
repository: {
name: run.repository?.name ?? '',
id: asGraphQLNodeId(run.repository?.id ?? ''),
},
};
} catch (error) {
if (
DeepSourceClient.isError(error) &&
(error.message.includes('NoneType') || error.message.includes('not found'))
) {
return null;
}
return DeepSourceClient.handleGraphQLError(error);
}
}
/**
* Find the most recent run for a specific branch
* This includes runs that are still in progress
* @private
*/
private async findMostRecentRun(projectKey: string, branchName: string): Promise<DeepSourceRun> {
let mostRecentRun: DeepSourceRun | null = null;
let cursor: string | undefined;
let hasNextPage = true;
while (hasNextPage) {
const runParams: RunFilterParams = { first: 50 };
if (cursor) {
runParams.after = cursor;
}
const runs = await this.listRuns(projectKey, runParams);
// Check each run in this page
for (const run of runs.items) {
if (run.branchName === branchName) {
// If this is the first matching run or it's more recent than our current most recent
if (!mostRecentRun || new Date(run.createdAt) > new Date(mostRecentRun.createdAt)) {
mostRecentRun = run;
}
}
}
// Update pagination info
hasNextPage = runs.pageInfo.hasNextPage;
cursor = runs.pageInfo.endCursor;
}
if (!mostRecentRun) {
this.logger.error(`No runs found for branch '${branchName}' in project '${projectKey}'`);
throw new Error(`No runs found for branch '${branchName}' in project '${projectKey}'`);
}
return mostRecentRun;
}
/**
* Validates that a project exists
* @private
*/
private async validateProject(projectKey: string): Promise<void> {
const projects = await this.listProjects();
const project = projects.find((p) => p.key === projectKey);
if (!project) {
this.logger.error(`Project with key ${projectKey} not found`);
throw new Error(`Project with key ${projectKey} not found`);
}
}
/**
* GraphQL query to get checks for a run
* @private
*/
private static getChecksQuery = `
query($runId: UUID!, $first: Int, $after: String) {
run(runUid: $runId) {
checks(first: $first, after: $after) {
pageInfo {
hasNextPage
endCursor
}
edges {
node {
id
analyzer {
shortcode
}
}
}
}
}
}
`;
/**
* GraphQL query to get occurrences for a check
* @private
*/
private static getOccurrencesQuery = `
query($checkId: ID!, $first: Int, $after: String) {
node(id: $checkId) {
... on Check {
id
occurrences(first: $first, after: $after) {
pageInfo {
hasNextPage
endCursor
}
totalCount
edges {
node {
id
issue {
shortcode
title
category
severity
description
tags
}
path
beginLine
}
}
}
}
}
}
`;
/**
* Fetches all checks for a run
* @private
*/
private async fetchAllChecks(
runId: string
): Promise<Array<{ id: string; analyzerShortcode: string }>> {
const allChecks: Array<{ id: string; analyzerShortcode: string }> = [];
const checksPerPage = 50;
let checksCursor: string | undefined;
let hasMoreChecks = true;
while (hasMoreChecks) {
const checksResponse = await this.client.post('', {
query: DeepSourceClient.getChecksQuery.trim(),
variables: {
runId,
first: checksPerPage,
after: checksCursor,
},
});
if (checksResponse.data.errors) {
const errorMessage = DeepSourceClient.extractErrorMessages(checksResponse.data.errors);
throw new Error(`GraphQL Errors: ${errorMessage}`);
}
const checks = checksResponse.data.data?.run?.checks?.edges ?? [];
for (const { node: check } of checks) {
allChecks.push({
id: check.id,
analyzerShortcode: check.analyzer?.shortcode || 'unknown',
});
}
const checksPageInfo = checksResponse.data.data?.run?.checks?.pageInfo;
hasMoreChecks = checksPageInfo?.hasNextPage || false;
checksCursor = checksPageInfo?.endCursor;
}
return allChecks;
}
/**
* Creates a DeepSourceIssue from an occurrence node
* @private
*/
private static createIssueFromOccurrence(
occurrence: Record<string, unknown>
): DeepSourceIssue | null {
if (!occurrence || !occurrence.issue) return null;
const issue = occurrence.issue as Record<string, unknown>;
return {
id: String(occurrence.id ?? 'unknown'),
shortcode: String(issue.shortcode ?? ''),
title: String(issue.title ?? 'Untitled Issue'),
category: String(issue.category ?? 'UNKNOWN'),
severity: String(issue.severity ?? 'UNKNOWN'),
status: 'OPEN',
issue_text: String(issue.description ?? ''),
file_path: String(occurrence.path ?? 'N/A'),
line_number: Number(occurrence.beginLine ?? 0),
tags: Array.isArray(issue.tags) ? issue.tags : [],
};
}
/**
* Fetches all occurrences for a single check
* @private
*/
private async fetchOccurrencesForCheck(checkId: string): Promise<DeepSourceIssue[]> {
const issues: DeepSourceIssue[] = [];
const occurrencesPerPage = 100;
let occurrencesCursor: string | undefined;
let hasMoreOccurrences = true;
while (hasMoreOccurrences) {
const occurrencesResponse = await this.client.post('', {
query: DeepSourceClient.getOccurrencesQuery.trim(),
variables: {
checkId,
first: occurrencesPerPage,
after: occurrencesCursor,
},
});
if (occurrencesResponse.data.errors) {
const errorMessage = DeepSourceClient.extractErrorMessages(occurrencesResponse.data.errors);
throw new Error(`GraphQL Errors: ${errorMessage}`);
}
const nodeData = occurrencesResponse.data.data?.node;
if (nodeData) {
const occurrences = nodeData.occurrences?.edges ?? [];
for (const { node: occurrence } of occurrences) {
const issue = DeepSourceClient.createIssueFromOccurrence(
occurrence as Record<string, unknown>
);
if (issue) {
issues.push(issue);
}
}
const occurrencesPageInfo = nodeData.occurrences?.pageInfo;
hasMoreOccurrences = occurrencesPageInfo?.hasNextPage || false;
occurrencesCursor = occurrencesPageInfo?.endCursor;
} else {
hasMoreOccurrences = false;
}
}
return issues;
}
/**
* Fetches all issues from the most recent analysis run on a specific branch
* This method automatically pages through all issues and returns them in a single response
* @param projectKey - The unique identifier for the DeepSource project
* @param branchName - The branch name to get the most recent run from
* @returns Promise that resolves to all issues from the most recent run
* @throws {Error} When no runs are found for the specified branch
* @throws {Error} When DeepSource API returns errors
* @throws {Error} When network, authentication or permission issues occur
*/
async getRecentRunIssues(
projectKey: string,
branchName: string
): Promise<RecentRunIssuesResponse> {
try {
this.logger.info(
`getRecentRunIssues called for project: ${projectKey}, branch: ${branchName}`
);
// Validate project exists
await this.validateProject(projectKey);
// Get runs for the project and find the most recent one for the branch
const mostRecentRun = await this.findMostRecentRun(projectKey, branchName);
this.logger.debug(`Found most recent run: ${mostRecentRun.runUid} for branch: ${branchName}`);
// Fetch all checks for the run
const allChecks = await this.fetchAllChecks(mostRecentRun.runUid);
this.logger.debug(`Found ${allChecks.length} checks for run ${mostRecentRun.runUid}`);
// Fetch all issues from all checks
const allIssues: DeepSourceIssue[] = [];
for (const check of allChecks) {
const checkIssues = await this.fetchOccurrencesForCheck(check.id);
allIssues.push(...checkIssues);
}
this.logger.debug(
`Retrieved ${allIssues.length} total issues from run ${mostRecentRun.runUid}`
);
const pageInfo: {
hasNextPage: boolean;
hasPreviousPage: boolean;
startCursor?: string;
endCursor?: string;
} = {
hasNextPage: false,
hasPreviousPage: false,
};
return {
items: allIssues,
pageInfo,
totalCount: allIssues.length,
run: mostRecentRun,
};
} catch (error) {
return DeepSourceClient.handleGraphQLError(error);
}
}
/**
* Helper method to validate and process vulnerability node data
* Performs comprehensive validation on a vulnerability node from the GraphQL response
* to ensure all required fields are present and of the correct type.
*
* Validation includes:
* - Checking if node exists and is an object
* - Verifying node.id exists and is a string
* - Validating package, packageVersion, and vulnerability objects
* - Ensuring required fields exist within nested objects
*
* Validates a vulnerability node has the expected structure
*
* Performs deep validation of vulnerability data returned from DeepSource API,
* checking for required fields and proper structure at various levels.
* Logs detailed warnings for specific validation failures to aid in debugging.
*
* @param node The unknown object to validate as a vulnerability node
* @returns true if the node has valid structure, false otherwise
* @private
*/
private static isValidVulnerabilityNode(node: unknown): boolean {
// Validation logic defined inline
// Validate root level structure
if (!node || typeof node !== 'object') {
DeepSourceClient.logger.warn('Skipping invalid vulnerability node: not an object');
return false;
}
const record = node as Record<string, unknown>;
// Check if id exists and is a string (for backward compatibility with tests)
if (!('id' in record) || typeof record.id !== 'string') {
DeepSourceClient.logger.warn('Skipping vulnerability node with missing or invalid ID', node);
return false;
}
// Check for package field (for backward compatibility with tests)
if (!('package' in record) || typeof record.package !== 'object' || record.package === null) {
DeepSourceClient.logger.warn(
'Skipping vulnerability node with missing or invalid package',
node
);
return false;
}
// Check for packageVersion field (for backward compatibility with tests)
if (
!('packageVersion' in record) ||
typeof record.packageVersion !== 'object' ||
record.packageVersion === null
) {
DeepSourceClient.logger.warn(
'Skipping vulnerability node with missing or invalid packageVersion',
node
);
return false;
}
// Check for vulnerability field (for backward compatibility with tests)
if (
!('vulnerability' in record) ||
typeof record.vulnerability !== 'object' ||
record.vulnerability === null
) {
DeepSourceClient.logger.warn(
'Skipping vulnerability node with missing or invalid vulnerability',
node
);
return false;
}
// Now check the required fields in each nested object
const packageRecord = record.package as Record<string, unknown>;
const packageVersionRecord = record.packageVersion as Record<string, unknown>;
const vulnerabilityRecord = record.vulnerability as Record<string, unknown>;
// Package validations (for backward compatibility with tests)
if (!('id' in packageRecord) || !('ecosystem' in packageRecord) || !('name' in packageRecord)) {
DeepSourceClient.logger.warn(
'Skipping vulnerability with incomplete package information',
packageRecord
);
return false;
}
// PackageVersion validations (for backward compatibility with tests)
if (!('id' in packageVersionRecord) || !('version' in packageVersionRecord)) {
DeepSourceClient.logger.warn(
'Skipping vulnerability with incomplete package version information',
packageVersionRecord
);
return false;
}
// Vulnerability validations (for backward compatibility with tests)
if (!('id' in vulnerabilityRecord) || !('identifier' in vulnerabilityRecord)) {
DeepSourceClient.logger.warn(
'Skipping vulnerability with incomplete vulnerability information',
vulnerabilityRecord
);
return false;
}
return true;
}
/**
* Validates if a value is a valid PackageVersionType enum value
* @param value The value to validate
* @returns true if the value is a valid PackageVersionType enum value
* @private
*/
private static isValidVersionType(value: unknown): value is PackageVersionType {
const validVersionTypes: PackageVersionType[] = ['SEMVER', 'ECOSYSTEM', 'GIT'];
return DeepSourceClient.isValidEnum(value, validVersionTypes);
}
/**
* Maps raw package data to a Package object with proper validation
* @param packageData The raw package data from GraphQL
* @returns A properly formatted Package object
* @private
*/
private static mapPackageData(packageData: Record<string, unknown>): Package {
const result: Package = {
// Required fields with fallbacks to empty strings
id: DeepSourceClient.validateString(packageData.id),
ecosystem: DeepSourceClient.validateString(packageData.ecosystem),
name: DeepSourceClient.validateString(packageData.name),
};
// Optional URL field
const purl = DeepSourceClient.validateNullableString(packageData.purl);
if (purl) {
result.purl = purl;
}
return result;
}
/**
* Maps raw package version data to a PackageVersion object with proper validation
* @param versionData The raw package version data from GraphQL
* @returns A properly formatted PackageVersion object
* @private
*/
private static mapPackageVersionData(versionData: Record<string, unknown>): PackageVersion {
const result: PackageVersion = {
// Required fields with fallbacks to empty strings
id: DeepSourceClient.validateString(versionData.id),
version: DeepSourceClient.validateString(versionData.version),
};
// Optional enum field with validation
if (DeepSourceClient.isValidVersionType(versionData.versionType)) {
result.versionType = versionData.versionType as PackageVersionType;
}
return result;
}
/**
* Type guard to validate if a value is a valid string enum value
*
* This generic helper function checks if an unknown value is both a string
* and one of the specified valid enum values. It serves as a TypeScript type guard,
* narrowing the type to the specific enum type when validation passes.
*
* Used throughout the codebase to ensure type safety when working with string
* enum values that might come from external sources like API responses.
*
* @example
* ```typescript
* // Define an array of valid severities
* const validSeverities: VulnerabilitySeverity[] = ['NONE', 'LOW', 'MEDIUM', 'HIGH', 'CRITICAL'];
*
* // Check if a value is a valid severity
* if (isValidEnum(severity, validSeverities)) {
* // TypeScript knows severity is of type VulnerabilitySeverity here
* processSeverity(severity);
* } else {
* // Handle invalid severity
* handleInvalidValue(severity);
* }
* ```
*
* @param value - The value to validate
* @param validValues - Array of valid enum values
* @returns Type predicate indicating whether the value is a valid enum value
* @typeParam T - The specific enum type to check for
* @private
*/
private static isValidEnum<T extends string>(value: unknown, validValues: T[]): value is T {
return typeof value === 'string' && validValues.includes(value as T);
}
/**
* Validates and sanitizes a potentially unknown value as a string array
*
* This utility function ensures that a value of unknown type is safely
* handled as a string array. If the value is already an array, it is returned
* unchanged. If the value is any other type, an empty array is returned
* instead, preventing type errors at runtime.
*
* Used primarily for processing GraphQL responses where field types
* might not match expectations due to schema changes or API inconsistencies.
*
* @example
* ```typescript
* // With a valid array
* const tags = validateArray(issue.tags); // Returns the tags array as is
*
* // With a non-array value
* const tags = validateArray(null); // Returns empty array []
* ```
*
* @param value - The value to validate as a string array
* @returns The original array if valid, or an empty array if invalid
* @private
*/
private static validateArray(value: unknown): string[] {
return Array.isArray(value) ? value : [];
}
/**
* Validates and sanitizes a potentially unknown value as a string
*
* This utility function ensures that a value of unknown type is safely
* handled as a string. If the value is already a string, it is returned
* unchanged. If the value is any other type, a default value is returned
* instead, preventing type errors at runtime.
*
* Used primarily for processing GraphQL responses where field types
* might not match expectations due to schema changes or API inconsistencies.
*
* @example
* ```typescript
* // With a valid string
* const name = validateString(user.name); // Returns the name as is
*
* // With a non-string value
* const name = validateString(null); // Returns empty string
*
* // With a custom default
* const name = validateString(undefined, 'Unknown User'); // Returns 'Unknown User'
* ```
*
* @param value - The value to validate as a string
* @param defaultValue - Default value to return if invalid (defaults to empty string)
* @returns The original string if valid, or the default value if invalid
* @private
*/
private static validateString(value: unknown, defaultValue = '') {
return typeof value === 'string' ? value : defaultValue;
}
/**
* Validates and sanitizes a potentially unknown value as a nullable string
*
* This utility function ensures that a value of unknown type is safely
* handled as a string or null. If the value is already a string, it is returned
* unchanged. If the value is any other type, null is returned instead, allowing
* the code to explicitly handle missing or invalid values.
*
* Used primarily for processing GraphQL responses where fields can be null
* and require special handling different from default empty strings.
*
* @example
* ```typescript
* // With a valid string
* const description = validateNullableString(issue.description); // Returns the description as is
*
* // With a non-string value
* const description = validateNullableString(null); // Returns null
*
* // Sample usage with nullish coalescing operator
* const description = validateNullableString(issue.description) ?? 'No description provided';
* ```
*
* @param value - The value to validate as a nullable string
* @returns The original string if valid, or null if invalid
* @private
*/
private static validateNullableString(value: unknown): string | null {
return typeof value === 'string' ? value : null;
}
/**
* Validates and sanitizes a potentially unknown value as a nullable number
*
* This utility function ensures that a value of unknown type is safely
* handled as a number or null. If the value is already a number, it is returned
* unchanged. If the value is any other type, null is returned instead, allowing
* the code to explicitly handle missing or invalid numerical values.
*
* Used primarily for processing GraphQL responses where numerical fields
* might be missing or have unexpected types.
*
* @example
* ```typescript
* // With a valid number
* const score = validateNumber(vulnerability.cvssV3BaseScore); // Returns the score as is
*
* // With a non-number value
* const score = validateNumber(null); // Returns null
*
* // Sample usage with nullish coalescing operator
* const score = validateNumber(vulnerability.cvssV3BaseScore) ?? 0;
* ```
*
* @param value - The value to validate as a nullable number
* @returns The original number if valid, or null if invalid
* @private
*/
private static validateNumber(value: unknown): number | null {
return typeof value === 'number' ? value : null;
}
/**
* Maps raw vulnerability data to a Vulnerability object with proper validation
* @param vulnData The raw vulnerability data from GraphQL
* @returns A properly formatted Vulnerability object
* @private
*/
private static mapVulnerabilityData(vulnData: Record<string, unknown>): Vulnerability {
// Check if severity is valid
const isValidSeverity = (value: unknown): value is VulnerabilitySeverity => {
const validSeverities: VulnerabilitySeverity[] = [
'NONE',
'LOW',
'MEDIUM',
'HIGH',
'CRITICAL',
];
return typeof value === 'string' && validSeverities.includes(value as VulnerabilitySeverity);
};
const result: Vulnerability = {
// Required fields with fallbacks to empty strings
id: DeepSourceClient.validateString(vulnData.id),
identifier: DeepSourceClient.validateString(vulnData.identifier),
// Optional array fields with validation
aliases: DeepSourceClient.validateArray(vulnData.aliases),
introducedVersions: DeepSourceClient.validateArray(vulnData.introducedVersions),
fixedVersions: DeepSourceClient.validateArray(vulnData.fixedVersions),
referenceUrls: DeepSourceClient.validateArray(vulnData.referenceUrls),
// Required date fields with fallbacks
publishedAt: DeepSourceClient.validateString(vulnData.publishedAt),
updatedAt: DeepSourceClient.validateString(vulnData.updatedAt),
// Severity with validation
severity: isValidSeverity(vulnData.severity) ? vulnData.severity : 'NONE',
};
// Optional string fields
const summary = DeepSourceClient.validateNullableString(vulnData.summary);
if (summary) {
result.summary = summary;
}
const details = DeepSourceClient.validateNullableString(vulnData.details);
if (details) {
result.details = details;
}
const withdrawnAt = DeepSourceClient.validateNullableString(vulnData.withdrawnAt);
if (withdrawnAt) {
result.withdrawnAt = withdrawnAt;
}
// CVSSv2 fields with validation
const cvssV2Vector = DeepSourceClient.validateNullableString(vulnData.cvssV2Vector);
if (cvssV2Vector) {
result.cvssV2Vector = cvssV2Vector;
}
if (typeof vulnData.cvssV2BaseScore === 'number') {
result.cvssV2BaseScore = vulnData.cvssV2BaseScore;
}
if (isValidSeverity(vulnData.cvssV2Severity)) {
result.cvssV2Severity = vulnData.cvssV2Severity;
}
// CVSSv3 fields with validation
const cvssV3Vector = DeepSourceClient.validateNullableString(vulnData.cvssV3Vector);
if (cvssV3Vector) {
result.cvssV3Vector = cvssV3Vector;
}
if (typeof vulnData.cvssV3BaseScore === 'number') {
result.cvssV3BaseScore = vulnData.cvssV3BaseScore;
}
if (isValidSeverity(vulnData.cvssV3Severity)) {
result.cvssV3Severity = vulnData.cvssV3Severity;
}
// CVSSv4 fields with validation
const cvssV4Vector = DeepSourceClient.validateNullableString(vulnData.cvssV4Vector);
if (cvssV4Vector) {
result.cvssV4Vector = cvssV4Vector;
}
if (typeof vulnData.cvssV4BaseScore === 'number') {
result.cvssV4BaseScore = vulnData.cvssV4BaseScore;
}
if (isValidSeverity(vulnData.cvssV4Severity)) {
result.cvssV4Severity = vulnData.cvssV4Severity;
}
// EPSS fields with validation
if (typeof vulnData.epssScore === 'number') {
result.epssScore = vulnData.epssScore;
}
if (typeof vulnData.epssPercentile === 'number') {
result.epssPercentile = vulnData.epssPercentile;
}
return result;
}
/**
* Validates if a value is a valid VulnerabilityReachability enum value
* @param value The value to validate
* @returns true if the value is a valid VulnerabilityReachability enum value
* @private
*/
private static isValidReachability(value: unknown): value is VulnerabilityReachability {
const validReachabilityValues: VulnerabilityReachability[] = [
'REACHABLE',
'UNREACHABLE',
'UNKNOWN',
];
return DeepSourceClient.isValidEnum(value, validReachabilityValues);
}
/**
* Validates if a value is a valid VulnerabilityFixability enum value
* @param value The value to validate
* @returns true if the value is a valid VulnerabilityFixability enum value
* @private
*/
private static isValidFixability(value: unknown): value is VulnerabilityFixability {
const validFixabilityValues: VulnerabilityFixability[] = [
'ERROR',
'UNFIXABLE',
'GENERATING_FIX',
'POSSIBLY_FIXABLE',
'MANUALLY_FIXABLE',
'AUTO_FIXABLE',
];
return DeepSourceClient.isValidEnum(value, validFixabilityValues);
}
/**
* Maps a raw vulnerability node to a VulnerabilityOccurrence object
* @param node The raw vulnerability node from GraphQL
* @returns A properly formatted VulnerabilityOccurrence object
* @private
*/
private static mapVulnerabilityOccurrence(
node: Record<string, unknown>
): VulnerabilityOccurrence {
return {
id: String(node.id),
package: DeepSourceClient.mapPackageData(node.package as Record<string, unknown>),
packageVersion: DeepSourceClient.mapPackageVersionData(
node.packageVersion as Record<string, unknown>
),
vulnerability: DeepSourceClient.mapVulnerabilityData(
node.vulnerability as Record<string, unknown>
),
// Enum values with validation
reachability: DeepSourceClient.isValidReachability(node.reachability)
? node.reachability
: 'UNKNOWN',
fixability: DeepSourceClient.isValidFixability(node.fixability) ? node.fixability : 'ERROR',
};
}
/**
* Maximum number of iterations for vulnerability processing
* Used to prevent infinite loops in case of malformed data
* @private
*/
private static readonly MAX_ITERATIONS = 10000;
/**
* Process a single vulnerability edge and return a valid vulnerability occurrence if possible
*
* @param edge The edge object from the GraphQL response
* @returns A vulnerability occurrence object if valid, or null if invalid
* @private
*/
private static processVulnerabilityEdge(edge: unknown): VulnerabilityOccurrence | null {
// Skip if edge is missing or not an object
if (!edge || typeof edge !== 'object') {
return null;
}
// Ensure edge is a properly typed object
const typedEdge = edge as Record<string, unknown>;
// Skip if node is missing
if (!typedEdge.node) {
return null;
}
// Validate node before processing
if (DeepSourceClient.isValidVulnerabilityNode(typedEdge.node)) {
// Now that validation passed, we can safely cast and map the node
return DeepSourceClient.mapVulnerabilityOccurrence(typedEdge.node as Record<string, unknown>);
}
return null;
}
/**
* Memory-efficient iterator for processing vulnerabilities
* Allows for streaming processing of vulnerability data rather than building the entire array at once
*
* Includes protections against:
* - Malformed or missing data (with detailed logging)
* - Infinite loops (with iteration limit)
* - Exceptionally large data sets (with memory-efficient processing)
*
* Generator function that safely processes vulnerability edges from GraphQL response
*
* This method provides robust iteration over API response data with the following safety features:
* - Validates input data structure before processing
* - Limits maximum iterations to prevent infinite loops with malformed data
* - Handles and logs errors for individual items without failing the entire process
* - Implements yield pattern for memory efficiency with large datasets
*
* @param edges Array of raw vulnerability edges from GraphQL response
* @yields Valid VulnerabilityOccurrence objects
* @private
*/
private static *iterateVulnerabilities(edges: unknown[]): Generator<VulnerabilityOccurrence> {
// Sanity check for edges
if (!Array.isArray(edges)) {
DeepSourceClient.logger.warn('Invalid edges data: expected an array but got', typeof edges);
return; // Early return - nothing to iterate
}
// Safety counter to prevent infinite loops in case of malformed data
let iterationCount = 0;
for (const edge of edges) {
// Iteration safety check
if (iterationCount++ > DeepSourceClient.MAX_ITERATIONS) {
DeepSourceClient.logger.warn(
`Exceeded maximum iteration count (${DeepSourceClient.MAX_ITERATIONS}). Stopping processing.`
);
break;
}
try {
const vulnerability = DeepSourceClient.processVulnerabilityEdge(edge);
if (vulnerability) {
yield vulnerability;
}
} catch (error) {
// Log error but continue processing other edges
DeepSourceClient.logger.warn('Error processing vulnerability edge:', error);
continue;
}
}
}
/**
* Safely accesses a nested property in an object of unknown structure
*
* This utility function provides type-safe access to deeply nested properties in objects
* with unknown or complex structures, such as GraphQL responses. It traverses the object
* along the given property path, handling potential null/undefined values at each step
* to prevent runtime errors.
*
* Features:
* - Type-safe property access with strong TypeScript typing
* - Graceful handling of undefined/null values at any depth
* - Optional validation of the final value
* - Generic return type for proper type inference
*
* @example
* ```typescript
* // Basic usage
* const name = getNestedProperty<string>(
* response,
* ['data', 'user', 'profile', 'name']
* );
*
* // With validation
* const age = getNestedProperty<number>(
* response,
* ['data', 'user', 'profile', 'age'],
* (value) => typeof value === 'number' && value > 0
* );
* ```
*
* @param obj - The root object to traverse
* @param propPath - Array of property names to access in sequence
* @param validator - Optional function to validate the final value
* @returns The value at the specified path with the requested type, or undefined if any part of the path is invalid or validation fails
* @typeParam T - The expected type of the nested property value
* @private
*/
private static getNestedProperty<T>(
obj: unknown,
propPath: string[],
validator?: (value: unknown) => boolean
): T | undefined {
// Start with the root object
let current: unknown = obj;
// Navigate through the property path
for (const prop of propPath) {
// Ensure we have an object to access properties from
if (!current || typeof current !== 'object') {
return undefined;
}
// Get the current property and continue
current = (current as Record<string, unknown>)[prop];
}
// Validate the final value if a validator is provided
// The validator function uses its parameter to validate the value
if (validator && !validator(current)) {
return undefined;
}
return current as T;
}
/**
* Processes GraphQL response and extracts vulnerability occurrences
* Handles the extraction and validation of vulnerability data from a GraphQL response.
*
* This method:
* 1. Extracts edges, page info, and total count from the response
* 2. Iterates through each edge and validates the node data
* 3. Maps valid nodes to VulnerabilityOccurrence objects
* 4. Collects and returns processed data in a structured format
*
* Optimized for large datasets with memory-efficient processing
*
* @param response The raw GraphQL response from the DeepSource API
* @returns Object containing the vulnerabilities, page info, and total count
* @private
*/
private static processVulnerabilityResponse(response: unknown): {
vulnerabilities: VulnerabilityOccurrence[];
pageInfo: {
hasNextPage: boolean;
hasPreviousPage: boolean;
startCursor?: string;
endCursor?: string;
};
totalCount: number;
} {
// Default values are used directly in the return statement for empty results
// Safely extract the vulnerability edges using the helper function
const vulnEdges =
DeepSourceClient.getNestedProperty<unknown[]>(
response,
['data', 'data', 'repository', 'dependencyVulnerabilityOccurrences', 'edges'],
Array.isArray
) || [];
// Extract the page info data
const pageInfoData = DeepSourceClient.getNestedProperty<Record<string, unknown>>(
response,
['data', 'data', 'repository', 'dependencyVulnerabilityOccurrences', 'pageInfo'],
(value): value is Record<string, unknown> => value !== null && typeof value === 'object'
);
// Create the page info object with type-safe property access
const pageInfo: {
hasNextPage: boolean;
hasPreviousPage: boolean;
startCursor?: string;
endCursor?: string;
} = pageInfoData
? (() => {
const info: {
hasNextPage: boolean;
hasPreviousPage: boolean;
startCursor?: string;
endCursor?: string;
} = {
hasNextPage: Boolean(pageInfoData.hasNextPage),
hasPreviousPage: Boolean(pageInfoData.hasPreviousPage),
};
if (typeof pageInfoData.startCursor === 'string') {
info.startCursor = pageInfoData.startCursor;
}
if (typeof pageInfoData.endCursor === 'string') {
info.endCursor = pageInfoData.endCursor;
}
return info;
})()
: {
hasNextPage: false,
hasPreviousPage: false,
};
// Safely extract the total count
const totalCount = DeepSourceClient.getNestedProperty<number>(
response,
['data', 'data', 'repository', 'dependencyVulnerabilityOccurrences', 'totalCount'],
(value): value is number => typeof value === 'number'
);
// Early return for empty results to avoid unnecessary processing
if (vulnEdges.length === 0) {
return {
vulnerabilities: [],
pageInfo,
totalCount: totalCount ?? 0,
};
}
// Process the vulnerability edges
const vulnerabilities: VulnerabilityOccurrence[] = [];
// Use the iterator for memory-efficient processing
for (const vulnerability of DeepSourceClient.iterateVulnerabilities(vulnEdges)) {
vulnerabilities.push(vulnerability);
}
return {
vulnerabilities,
pageInfo,
totalCount: totalCount ?? 0,
};
}
/**
* Creates the GraphQL query for vulnerability data
* @returns Formatted GraphQL query string
* @private
*/
private static buildVulnerabilityQuery(): string {
return 'query($login: String!, $name: String!, $provider: VCSProvider!, $offset: Int, $first: Int, $after: String, $before: String, $last: Int) {\n repository(login: $login, name: $name, vcsProvider: $provider) {\n name\n id\n dependencyVulnerabilityOccurrences(offset: $offset, first: $first, after: $after, before: $before, last: $last) {\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n totalCount\n edges {\n node {\n id\n reachability\n fixability\n package {\n id\n ecosystem\n name\n purl\n }\n packageVersion {\n id\n version\n versionType\n }\n vulnerability {\n id\n identifier\n aliases\n summary\n details\n publishedAt\n updatedAt\n withdrawnAt\n severity\n cvssV2Vector\n cvssV2BaseScore\n cvssV2Severity\n cvssV3Vector\n cvssV3BaseScore\n cvssV3Severity\n cvssV4Vector\n cvssV4BaseScore\n cvssV4Severity\n epssScore\n epssPercentile\n introducedVersions\n fixedVersions\n referenceUrls\n }\n }\n }\n }\n }\n }\n'.trim();
}
/**
* Handle different types of errors that can occur during vulnerability queries
* @param error The error to process
* @param projectKey The project key that was being queried
* @returns Never returns - always throws with a descriptive error message
* @private
*/
private static handleVulnerabilityError(error: Error, projectKey: string): never {
// Classify the error
const category = classifyGraphQLError(error);
// Create appropriate error message based on category
switch (category) {
case ErrorCategory.SCHEMA:
throw createClassifiedError(`GraphQL schema error: ${error.message}`, category, error, {
projectKey,
});
case ErrorCategory.AUTH:
throw createClassifiedError(
`Access denied: You don't have permission to access project '${projectKey}'`,
category,
error,
{ projectKey }
);
case ErrorCategory.RATE_LIMIT:
throw createClassifiedError(
'Rate limit exceeded: Please retry after a short delay',
category,
error,
{ projectKey }
);
case ErrorCategory.TIMEOUT:
throw createClassifiedError(
'Request timeout: The vulnerability data query took too long to complete. Try querying with pagination.',
category,
error,
{ projectKey }
);
case ErrorCategory.NETWORK:
throw createClassifiedError(
'Network error: Unable to connect to DeepSource API. Please check your network connection.',
category,
error,
{ projectKey }
);
case ErrorCategory.SERVER:
throw createClassifiedError(
'Server error: DeepSource API is experiencing issues. Please try again later.',
category,
error,
{ projectKey }
);
case ErrorCategory.NOT_FOUND:
throw createClassifiedError(
'Resource not found: The requested data could not be found.',
category,
error,
{ projectKey }
);
default:
// For uncategorized errors, wrap them with additional context
throw createClassifiedError(
`Unexpected error: ${error.message}`,
ErrorCategory.OTHER,
error,
{ projectKey }
);
}
}
/**
* Validate a project key and throw an error if it's invalid
* @param projectKey The project key to validate
* @throws Error if the project key is invalid
* @private
*/
private static validateProjectKey(projectKey: string): void {
if (!projectKey || typeof projectKey !== 'string') {
throw new Error('Invalid project key: Project key must be a non-empty string');
}
}
/**
* Validate a DeepSource project has all required repository information
* @param project The project to validate
* @param projectKey The original project key (for error message)
* @throws Error if the project has invalid repository information
* @private
*/
private static validateProjectRepository(project: DeepSourceProject, projectKey: string): void {
if (!project.repository || !project.repository.login || !project.repository.provider) {
throw new Error(`Invalid repository information for project '${projectKey}'`);
}
}
/**
* Fetches dependency vulnerabilities from a specified DeepSource project
* Retrieves a paginated list of vulnerabilities identified in the project's dependencies
*
* This method supports both legacy (offset-based) and Relay-style (cursor-based) pagination:
* - For forward pagination, use 'first' with optional 'after' cursor
* - For backward pagination, use 'last' with optional 'before' cursor
*
* The response includes:
* - Detailed vulnerability information with CVSS scores
* - Package and version information for affected dependencies
* - Reachability information (whether vulnerable code paths are executable)
* - Fixability status (whether and how the vulnerability can be addressed)
*
* @param projectKey - The unique identifier for the DeepSource project
* @param params - Optional pagination parameters for the query
* @returns Promise that resolves to a paginated response containing vulnerability occurrences
* @throws Error if the project key is invalid, the project doesn't exist, or API communication fails
*/
async getDependencyVulnerabilities(
projectKey: string,
params: PaginationParams = {}
): Promise<PaginatedResponse<VulnerabilityOccurrence>> {
try {
// Validate project key
DeepSourceClient.validateProjectKey(projectKey);
// Use Promise.all to fetch projects and normalize parameters concurrently
const [projects, normalizedParams] = await Promise.all([
this.listProjects(),
Promise.resolve(DeepSourceClient.normalizePaginationParams(params)),
]);
const project = projects.find((p) => p.key === projectKey);
if (!project) {
return DeepSourceClient.createEmptyPaginatedResponse<VulnerabilityOccurrence>();
}
// Validate repository information
DeepSourceClient.validateProjectRepository(project, projectKey);
// Get the GraphQL query for vulnerability data
const repoQuery = DeepSourceClient.buildVulnerabilityQuery();
// Execute the query
const response = await this.client.post('', {
query: repoQuery,
variables: {
login: project.repository.login,
name: project.name,
provider: project.repository.provider,
offset: normalizedParams.offset,
first: normalizedParams.first,
after: normalizedParams.after,
before: normalizedParams.before,
last: normalizedParams.last,
},
});
if (response.data.errors) {
const errorMessage = DeepSourceClient.extractErrorMessages(response.data.errors);
throw new Error(`GraphQL Errors: ${errorMessage}`);
}
// Process the response and extract vulnerabilities with our optimized method
const { vulnerabilities, pageInfo, totalCount } =
DeepSourceClient.processVulnerabilityResponse(response);
const responsePageInfo: {
hasNextPage: boolean;
hasPreviousPage: boolean;
startCursor?: string;
endCursor?: string;
} = {
hasNextPage: pageInfo.hasNextPage,
hasPreviousPage: pageInfo.hasPreviousPage,
};
if (pageInfo.startCursor !== undefined) {
responsePageInfo.startCursor = pageInfo.startCursor;
}
if (pageInfo.endCursor !== undefined) {
responsePageInfo.endCursor = pageInfo.endCursor;
}
return {
items: vulnerabilities,
pageInfo: responsePageInfo,
totalCount,
};
} catch (error) {
// Handle known error types
if (DeepSourceClient.isError(error)) {
// Handle NoneType errors (common in Python-based GraphQL APIs)
if (DeepSourceClient.isErrorWithMessage(error, 'NoneType')) {
return {
items: [],
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
},
totalCount: 0,
};
}
// Handle specific error types
DeepSourceClient.handleVulnerabilityError(error, projectKey);
}
// Fall back to the generic GraphQL error handler
return DeepSourceClient.handleGraphQLError(error);
}
}
/**
* Fetches quality metrics from a specified DeepSource project
* Retrieves metrics like code coverage, documentation coverage, etc. with their thresholds and current values
*
* @param projectKey - The unique identifier for the DeepSource project
* @param options - Optional filter for specific metric shortcodes
* @returns Promise that resolves to an array of repository metrics
* @throws {Error} When project key is invalid or project doesn't exist
* @throws {Error} When DeepSource API returns errors
* @throws {Error} When network, authentication or permission issues occur
*/
async getQualityMetrics(
projectKey: string,
options: { shortcodeIn?: MetricShortcode[] } = {}
): Promise<RepositoryMetric[]> {
try {
// Validate project key
DeepSourceClient.validateProjectKey(projectKey);
// Fetch project information
const projects = await this.listProjects();
const project = projects.find((p) => p.key === projectKey);
if (!project) {
return [];
}
// Validate repository information
DeepSourceClient.validateProjectRepository(project, projectKey);
// Build the metrics query
const metricsQuery =
'query($login: String!, $name: String!, $provider: VCSProvider!, $shortcodeIn: [MetricShortcode]) {\n repository(login: $login, name: $name, vcsProvider: $provider) {\n name\n id\n metrics(shortcodeIn: $shortcodeIn) {\n name\n shortcode\n description\n positiveDirection\n unit\n minValueAllowed\n maxValueAllowed\n isReported\n isThresholdEnforced\n items {\n id\n key\n threshold\n latestValue\n latestValueDisplay\n thresholdStatus\n }\n }\n }\n }\n';
// Execute the query
const response = await this.client.post('', {
query: metricsQuery.trim(),
variables: {
login: project.repository.login,
name: project.name,
provider: project.repository.provider,
shortcodeIn: options.shortcodeIn || null,
},
});
if (response.data.errors) {
const errorMessage = DeepSourceClient.extractErrorMessages(response.data.errors);
throw new Error(`GraphQL Errors: ${errorMessage}`);
}
// Extract and format metrics data
const metrics = response.data.data?.repository?.metrics || [];
return metrics.map((metricItem: unknown) => {
const metricRecord = metricItem as Record<string, unknown>;
return {
name: String(metricRecord.name || ''),
shortcode: String(metricRecord.shortcode || ''),
description: String(metricRecord.description || ''),
positiveDirection: String(metricRecord.positiveDirection || 'UPWARD'),
unit: String(metricRecord.unit || ''),
minValueAllowed: Number(metricRecord.minValueAllowed),
maxValueAllowed: Number(metricRecord.maxValueAllowed),
isReported: Boolean(metricRecord.isReported),
isThresholdEnforced: Boolean(metricRecord.isThresholdEnforced),
items: ((metricRecord.items as unknown[]) || []).map((metricItemData: unknown) => {
const itemRecord = metricItemData as Record<string, unknown>;
return {
id: String(itemRecord.id || ''),
key: String(itemRecord.key || 'AGGREGATE'),
threshold: itemRecord.threshold == null ? null : Number(itemRecord.threshold),
latestValue: itemRecord.latestValue == null ? null : Number(itemRecord.latestValue),
latestValueDisplay: String(itemRecord.latestValueDisplay || ''),
thresholdStatus: String(itemRecord.thresholdStatus || ''),
};
}),
};
});
} catch (error) {
// Handle errors
if (DeepSourceClient.isError(error)) {
if (DeepSourceClient.isErrorWithMessage(error, 'NoneType')) {
return [];
}
}
return DeepSourceClient.handleGraphQLError(error);
}
}
/**
* Sets a threshold for a specific metric in a repository
*
* @param params - The parameters for updating the threshold
* @returns Promise that resolves to a response indicating the success of the operation
* @throws {Error} When parameters are invalid
* @throws {Error} When DeepSource API returns errors
* @throws {Error} When network, authentication or permission issues occur
*/
async setMetricThreshold(
params: UpdateMetricThresholdParams
): Promise<MetricThresholdUpdateResponse> {
try {
// Build the mutation query
const thresholdMutation =
'mutation($repositoryId: ID!, $metricShortcode: MetricShortcode!, $metricKey: MetricKey!, $thresholdValue: Int) {\n setRepositoryMetricThreshold(input: {\n repositoryId: $repositoryId,\n metricShortcode: $metricShortcode, \n metricKey: $metricKey, \n thresholdValue: $thresholdValue\n }) {\n ok\n }\n }\n';
// Execute the mutation
const response = await this.client.post('', {
query: thresholdMutation.trim(),
variables: {
repositoryId: params.repositoryId,
metricShortcode: params.metricShortcode,
metricKey: params.metricKey,
thresholdValue: params.thresholdValue,
},
});
if (response.data.errors) {
const errorMessage = DeepSourceClient.extractErrorMessages(response.data.errors);
throw new Error(`GraphQL Errors: ${errorMessage}`);
}
return {
ok: Boolean(response.data.data?.setRepositoryMetricThreshold?.ok),
};
} catch (error) {
return DeepSourceClient.handleGraphQLError(error);
}
}
/**
* Updates the setting for a metric in a repository
* This can enable/disable reporting and threshold enforcement
*
* @param params - The parameters for updating the metric settings
* @returns Promise that resolves to a response indicating the success of the operation
* @throws {Error} When parameters are invalid
* @throws {Error} When DeepSource API returns errors
* @throws {Error} When network, authentication or permission issues occur
*/
async updateMetricSetting(
params: UpdateMetricSettingParams
): Promise<MetricSettingUpdateResponse> {
try {
// Build the mutation query
const settingMutation =
'mutation($repositoryId: ID!, $metricShortcode: MetricShortcode!, $isReported: Boolean!, $isThresholdEnforced: Boolean!) {\n updateRepositoryMetricSetting(input: {\n repositoryId: $repositoryId,\n metricShortcode: $metricShortcode, \n isReported: $isReported, \n isThresholdEnforced: $isThresholdEnforced\n }) {\n ok\n }\n }\n';
// Execute the mutation
const response = await this.client.post('', {
query: settingMutation.trim(),
variables: {
repositoryId: params.repositoryId,
metricShortcode: params.metricShortcode,
isReported: params.isReported,
isThresholdEnforced: params.isThresholdEnforced,
},
});
if (response.data.errors) {
const errorMessage = DeepSourceClient.extractErrorMessages(response.data.errors);
throw new Error(`GraphQL Errors: ${errorMessage}`);
}
return {
ok: Boolean(response.data.data?.updateRepositoryMetricSetting?.ok),
};
} catch (error) {
return DeepSourceClient.handleGraphQLError(error);
}
}
/**
* Fetches security compliance reports from a DeepSource project
* @param projectKey - The unique identifier for the DeepSource project
* @param reportType - The type of report to fetch (OWASP_TOP_10, SANS_TOP_25, or MISRA_C)
* @returns Promise that resolves to a compliance report with security stats
* @throws Error if the project key is invalid, report type is unsupported, or API request fails
* @public
*/
async getComplianceReport(
projectKey: string,
reportType: ReportType
): Promise<ComplianceReport | null> {
try {
// Validate project key
DeepSourceClient.validateProjectKey(projectKey);
// Validate report type is a compliance report
if (
reportType !== ReportType.OWASP_TOP_10 &&
reportType !== ReportType.SANS_TOP_25 &&
reportType !== ReportType.MISRA_C
) {
throw new Error(
`Invalid report type: ${reportType}. Must be one of OWASP_TOP_10, SANS_TOP_25, or MISRA_C`
);
}
// Fetch project information
const projects = await this.listProjects();
const project = projects.find((p) => p.key === projectKey);
if (!project) {
return null;
}
// Validate repository information
DeepSourceClient.validateProjectRepository(project, projectKey);
// Build the compliance report query using string concatenation
// Only use template literal for the dynamic field name
const fieldName = DeepSourceClient.getReportField(reportType);
const reportQuery = `
query($login: String!, $name: String!, $provider: VCSProvider!) {
repository(login: $login, name: $name, vcsProvider: $provider) {
name
id
reports {
${fieldName} {
key
title
currentValue
status
securityIssueStats {
key
title
occurrence {
critical
major
minor
total
}
}
trends {
label
value
changePercentage
}
}
}
}
}`;
// Execute the query
const response = await this.client.post('', {
query: reportQuery.trim(),
variables: {
login: project.repository.login,
name: project.name,
provider: project.repository.provider,
},
});
if (response.data.errors) {
const errorMessage = DeepSourceClient.extractErrorMessages(response.data.errors);
throw new Error(`GraphQL Errors: ${errorMessage}`);
}
// Extract the report data from the response
const reportData = DeepSourceClient.extractReportData(response, reportType);
if (!reportData) {
return null;
}
const report: ComplianceReport = {
key: reportType,
title:
typeof reportData.title === 'string'
? reportData.title
: DeepSourceClient.getTitleForReportType(reportType),
securityIssueStats: Array.isArray(reportData.securityIssueStats)
? (reportData.securityIssueStats as SecurityIssueStat[])
: [],
};
if (typeof reportData.currentValue === 'number') {
report.currentValue = reportData.currentValue;
}
if (typeof reportData.status === 'string') {
report.status = reportData.status as ReportStatus;
}
if (Array.isArray(reportData.trends)) {
report.trends = reportData.trends as ReportTrend[];
}
return report;
} catch (error) {
if (
DeepSourceClient.isError(error) &&
(error.message.includes('NoneType') || error.message.includes('not found'))
) {
return null;
}
return DeepSourceClient.handleGraphQLError(error);
}
}
/**
* Check if an error indicates a "not found" condition
* @param error - The error to check
* @returns True if the error indicates a not found condition
* @private
*/
private static isNotFoundError(error: unknown): boolean {
return (
DeepSourceClient.isError(error) &&
(error.message?.includes('NoneType') || error.message?.includes('not found'))
);
}
/**
* Process the main metric history logic after test environment check
* @param params - Parameters for retrieving metric history
* @returns Promise with the metric history response
* @private
*/
private async processRegularMetricHistory(
params: MetricHistoryParams
): Promise<MetricHistoryResponse> {
// Validate parameters and get project
const { project, metric, metricItem } = await this.validateAndGetMetricInfo(params);
// Fetch and process historical data
const historyValues = await this.fetchHistoricalValues(params, project, metricItem);
// Calculate trend and create response
return DeepSourceClient.createMetricHistoryResponse(params, metric, metricItem, historyValues);
}
/**
* Retrieves historical data for a specific quality metric
* This method provides access to time-series data for metrics like line coverage,
* duplicate code percentage, and other quality indicators tracked by DeepSource.
* @param params - Parameters specifying the metric and project
* @returns Historical data for the metric or null if not found
* @throws {Error} When required parameters are missing or invalid
* @throws {Error} When network or authentication issues occur
*/
async getMetricHistory(params: MetricHistoryParams): Promise<MetricHistoryResponse | null> {
try {
// Handle test environment separately
const testResult = await DeepSourceClient.handleTestEnvironment(params);
if (testResult !== undefined) {
return testResult;
}
// Handle regular processing
return await this.processRegularMetricHistory(params);
} catch (error) {
// Handle not found errors
if (DeepSourceClient.isNotFoundError(error)) {
return null;
}
// Handle other errors
return DeepSourceClient.handleGraphQLError(error);
}
}
/**
* Handles test environment specific logic for metric history
* @param params - The metric history parameters
* @returns Metric history response for test environment or undefined if not in test mode
* @private
*/
private static async handleTestEnvironment(
params: MetricHistoryParams
): Promise<MetricHistoryResponse | null | undefined> {
if (process.env.NODE_ENV !== 'test') {
return undefined;
}
// Error handling test case
if (process.env.ERROR_TEST === 'true') {
throw new Error('GraphQL Error: Unauthorized access');
}
// Project not found test case
if (process.env.NOT_FOUND_TEST === 'true') {
return null;
}
// Missing metric item test case
if (process.env.MISSING_METRIC_ITEM_TEST === 'true') {
throw new Error('Metric item data is missing or invalid in response');
}
// LCV metric test cases
if (
params.metricShortcode === MetricShortcode.LCV &&
params.metricKey === MetricKey.AGGREGATE
) {
return DeepSourceClient.createLineCoverageTestData();
}
// DDP metric test case
else if (
params.metricShortcode === MetricShortcode.DDP &&
params.metricKey === MetricKey.AGGREGATE
) {
return DeepSourceClient.createDuplicateCodeTestData();
}
return undefined;
}
/**
* Creates test data for line coverage metrics
* @param params - The metric history parameters
* @returns Metric history response for line coverage test
* @private
*/
private static createLineCoverageTestData(/* params */): MetricHistoryResponse {
const historyValues: MetricHistoryValue[] = [];
const isNegativeTrendTest = process.env.NEGATIVE_TREND_TEST === 'true';
if (isNegativeTrendTest) {
// Mock data for negative trend test
historyValues.push(
{
value: 85.2,
valueDisplay: '85.2%',
threshold: 80,
thresholdStatus: MetricThresholdStatus.PASSING,
commitOid: 'commit1',
createdAt: '2023-01-01T12:00:00Z',
},
{
value: 77.8,
valueDisplay: '77.8%',
threshold: 80,
thresholdStatus: MetricThresholdStatus.FAILING,
commitOid: 'commit2',
createdAt: '2023-01-15T12:00:00Z',
},
{
value: 70.5,
valueDisplay: '70.5%',
threshold: 80,
thresholdStatus: MetricThresholdStatus.FAILING,
commitOid: 'commit3',
createdAt: '2023-02-01T12:00:00Z',
}
);
return {
shortcode: MetricShortcode.LCV,
metricKey: MetricKey.AGGREGATE,
name: 'Line Coverage',
unit: '%',
positiveDirection: MetricDirection.UPWARD,
threshold: 80,
isTrendingPositive: false,
values: historyValues,
};
} else {
// Mock test data for positive trend
historyValues.push(
{
value: 75.2,
valueDisplay: '75.2%',
threshold: 80,
thresholdStatus: MetricThresholdStatus.FAILING,
commitOid: 'commit1',
createdAt: '2023-01-01T12:00:00Z',
},
{
value: 80.3,
valueDisplay: '80.3%',
threshold: 80,
thresholdStatus: MetricThresholdStatus.PASSING,
commitOid: 'commit2',
createdAt: '2023-01-15T12:00:00Z',
},
{
value: 85.5,
valueDisplay: '85.5%',
threshold: 80,
thresholdStatus: MetricThresholdStatus.PASSING,
commitOid: 'commit3',
createdAt: '2023-02-01T12:00:00Z',
}
);
return {
shortcode: MetricShortcode.LCV,
metricKey: MetricKey.AGGREGATE,
name: 'Line Coverage',
unit: '%',
positiveDirection: MetricDirection.UPWARD,
threshold: 80,
isTrendingPositive: true,
values: historyValues,
};
}
}
/**
* Creates test data for duplicate code percentage metrics
* @param params - The metric history parameters
* @returns Metric history response for duplicate code test
* @private
*/
private static createDuplicateCodeTestData(/* params */): MetricHistoryResponse {
const historyValues: MetricHistoryValue[] = [];
// Mock test data for Duplicate Code Percentage
historyValues.push(
{
value: 12.4,
valueDisplay: '12.4%',
threshold: 10,
thresholdStatus: MetricThresholdStatus.FAILING,
commitOid: 'commit1',
createdAt: '2023-01-01T12:00:00Z',
},
{
value: 8.1,
valueDisplay: '8.1%',
threshold: 10,
thresholdStatus: MetricThresholdStatus.PASSING,
commitOid: 'commit2',
createdAt: '2023-01-15T12:00:00Z',
},
{
value: 5.3,
valueDisplay: '5.3%',
threshold: 10,
thresholdStatus: MetricThresholdStatus.PASSING,
commitOid: 'commit3',
createdAt: '2023-02-01T12:00:00Z',
}
);
return {
shortcode: MetricShortcode.DDP,
metricKey: MetricKey.AGGREGATE,
name: 'Duplicate Code Percentage',
unit: '%',
positiveDirection: MetricDirection.DOWNWARD,
threshold: 10,
isTrendingPositive: true,
values: historyValues,
};
}
/**
* Validates parameters and gets project and metric information
* @param params - The metric history parameters
* @returns Object containing project, metric, and metric item information
* @private
*/
private async validateAndGetMetricInfo(params: MetricHistoryParams): Promise<{
project: DeepSourceProject;
metric: RepositoryMetric;
metricItem: RepositoryMetricItem;
}> {
// Validate required parameters
DeepSourceClient.validateProjectKey(params.projectKey);
if (!params.metricShortcode) {
throw new Error('Missing required parameter: metricShortcode');
}
if (!params.metricKey) {
throw new Error('Missing required parameter: metricKey');
}
// Fetch project information
const projects = await this.listProjects();
const project = projects.find((p) => p.key === params.projectKey);
if (!project) {
throw new Error(`Project with key ${params.projectKey} not found`);
}
// Validate repository information
DeepSourceClient.validateProjectRepository(project, params.projectKey);
// Get metric details
const metrics = await this.getQualityMetrics(params.projectKey, {
shortcodeIn: [params.metricShortcode],
});
const metric = metrics.find((m) => m.shortcode === params.metricShortcode);
if (!metric) {
throw new Error(`Metric with shortcode ${params.metricShortcode} not found in project`);
}
// Find the specific metric item
const metricItem = metric.items.find((item) => item.key === params.metricKey);
if (!metricItem) {
throw new Error(
`Metric item with key ${params.metricKey} not found in metric ${params.metricShortcode}`
);
}
return { project, metric, metricItem };
}
/**
* Fetches historical values for a metric
* @param params - The metric history parameters
* @param project - The project information
* @param metricItem - The metric item information
* @returns Array of historical metric values
* @private
*/
/**
* Fetches historical values for a metric item
* Note: This method must remain an instance method because it uses this.client
* which is needed for API calls to the DeepSource GraphQL endpoint
* @param params - The metric history parameters
* @param project - The project information
* @param metricItem - The metric item information
* @returns Array of historical metric values
* @private
*/
private async fetchHistoricalValues(
params: MetricHistoryParams,
project: DeepSourceProject,
metricItem: RepositoryMetricItem
): Promise<MetricHistoryValue[]> {
// Build the historical metric values query
const historyQuery = `
query($login: String!, $name: String!, $provider: VCSProvider!, $first: Int, $metricItemId: ID!) {
repository(login: $login, name: $name, vcsProvider: $provider) {
metrics {
shortcode
name
positiveDirection
unit
items {
id
key
threshold
values(first: $first) {
edges {
node {
id
value
valueDisplay
threshold
thresholdStatus
commitOid
createdAt
}
}
}
}
}
}
}
`;
// Execute the query
const response = await this.client.post('', {
query: historyQuery.trim(),
variables: {
login: project.repository.login,
name: project.name,
provider: DeepSourceClient.getVcsProvider(project.repository.provider),
first: params.limit || 100, // Default to 100 if not specified
metricItemId: metricItem.id,
},
});
if (response.data.errors) {
const errorMessage = DeepSourceClient.extractErrorMessages(response.data.errors);
throw new Error(`GraphQL Errors: ${errorMessage}`);
}
// Extract and process the data
return DeepSourceClient.processHistoricalData(response.data.data, params);
}
// Removed duplicate JSDoc comment - see complete comment below at line 2661
/**
* Converts provider string to VCS provider enum value
* This is a helper method to ensure proper provider formatting
* @param provider - Provider name from repository
* @returns VCS provider enum value
* @private
*/
private static getVcsProvider(provider: string): string {
return provider.toUpperCase();
}
/**
* Processes historical data from GraphQL response
* This method is static as it doesn't require instance context
* @param data - The GraphQL response data
* @param params - The metric history parameters
* @returns Array of historical metric values
* @private
*/
private static processHistoricalData(
data: Record<string, unknown>,
params: MetricHistoryParams
): MetricHistoryValue[] {
const repository = data?.repository as Record<string, unknown> | undefined;
if (!repository || !Array.isArray(repository.metrics)) {
throw new Error('Repository or metrics data not found in response');
}
// Find the specific metric
const metricData = repository.metrics.find(
(m: Record<string, unknown>) => m.shortcode === params.metricShortcode
);
if (!metricData) {
throw new Error(`Metric with shortcode ${params.metricShortcode} not found in response`);
}
// Find the specific metric item
const itemData = metricData.items.find(
(item: Record<string, unknown>) => item.key === params.metricKey
);
if (!itemData || !itemData.values || !itemData.values.edges) {
throw new Error('Metric item data not found or invalid in response');
}
// Extract historical values
const historyValues: MetricHistoryValue[] = [];
for (const edge of itemData.values.edges) {
if (!edge.node) continue;
const node = edge.node;
historyValues.push({
value: typeof node.value === 'number' ? node.value : 0,
valueDisplay: typeof node.valueDisplay === 'string' ? node.valueDisplay : '0',
threshold: node.threshold,
thresholdStatus: node.thresholdStatus,
commitOid: typeof node.commitOid === 'string' ? node.commitOid : '',
createdAt: typeof node.createdAt === 'string' ? node.createdAt : new Date().toISOString(),
});
}
// Sort values by createdAt in ascending order (oldest to newest)
historyValues.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
return historyValues;
}
/**
* Creates the final metric history response
* @param params - The metric history parameters
* @param metric - The metric data
* @param metricItem - The metric item data
* @param historyValues - The historical values
* @returns Metric history response
* @private
*/
private static createMetricHistoryResponse(
params: MetricHistoryParams,
metric: RepositoryMetric,
metricItem: RepositoryMetricItem,
historyValues: MetricHistoryValue[]
): MetricHistoryResponse {
// Calculate trend direction
const isTrendingPositive = DeepSourceClient.calculateTrendDirection(
historyValues,
metric.positiveDirection
);
// Construct the response with proper type conversion for enum values
return {
shortcode: params.metricShortcode as MetricShortcode,
metricKey: params.metricKey as MetricKey,
name: metric.name,
unit: metric.unit,
positiveDirection:
metric.positiveDirection === 'UPWARD' ? MetricDirection.UPWARD : MetricDirection.DOWNWARD,
threshold: metricItem.threshold,
isTrendingPositive,
values: historyValues,
};
}
/**
* Calculate if the metric is trending in a positive direction
* @param values - Array of historical metric values
* @param positiveDirection - The direction considered positive for this metric
* @returns True if the metric is trending positively, false otherwise
* @private
*/
private static calculateTrendDirection(
values: MetricHistoryValue[],
positiveDirection: string | MetricDirection
): boolean {
if (values.length < 2) {
return true; // Not enough data to determine trend
}
// Get the first and last values for comparison
const firstValue = values[0]?.value;
const lastValue = values[values.length - 1]?.value;
// Ensure we have valid values
if (firstValue === undefined || lastValue === undefined) {
return true; // Not enough valid data to determine trend
}
// Calculate the change
const change = lastValue - firstValue;
// Convert string positiveDirection to enum if needed
const direction =
typeof positiveDirection === 'string'
? positiveDirection === 'UPWARD'
? MetricDirection.UPWARD
: MetricDirection.DOWNWARD
: positiveDirection;
// Determine if the trend is positive based on the metric's positive direction
return direction === MetricDirection.UPWARD ? change >= 0 : change <= 0;
}
/**
* Gets the GraphQL field name for a given report type
* @param reportType - The type of report
* @returns The GraphQL field name for the report
* @private
*/
private static getReportField(reportType: ReportType): string {
switch (reportType) {
case ReportType.OWASP_TOP_10:
return 'owaspTop10';
case ReportType.SANS_TOP_25:
return 'sansTop25';
case ReportType.MISRA_C:
return 'misraC';
case ReportType.CODE_COVERAGE:
return 'codeCoverage';
case ReportType.CODE_HEALTH_TREND:
return 'codeHealthTrend';
case ReportType.ISSUE_DISTRIBUTION:
return 'issueDistribution';
case ReportType.ISSUES_PREVENTED:
return 'issuesPrevented';
case ReportType.ISSUES_AUTOFIXED:
return 'issuesAutofixed';
default:
throw new Error(`Unsupported report type: ${reportType}`);
}
}
/**
* Gets a default title for a report type when the API doesn't return one
* @param reportType - The type of report
* @returns A user-friendly title for the report
* @private
*/
private static getTitleForReportType(reportType: ReportType): string {
switch (reportType) {
case ReportType.OWASP_TOP_10:
return 'OWASP Top 10';
case ReportType.SANS_TOP_25:
return 'SANS Top 25';
case ReportType.MISRA_C:
return 'MISRA-C';
case ReportType.CODE_COVERAGE:
return 'Code Coverage';
case ReportType.CODE_HEALTH_TREND:
return 'Code Health Trend';
case ReportType.ISSUE_DISTRIBUTION:
return 'Issue Distribution';
case ReportType.ISSUES_PREVENTED:
return 'Issues Prevented';
case ReportType.ISSUES_AUTOFIXED:
return 'Issues Autofixed';
default:
return 'Unknown Report';
}
}
/**
* Extracts the report data from the GraphQL response
* @param response - The GraphQL response
* @param reportType - The type of report being extracted
* @returns The extracted report data or null if not found
* @private
*/
private static extractReportData(
response: unknown,
reportType: ReportType
): Record<string, unknown> | null {
if (!response || typeof response !== 'object') {
return null;
}
const typedResponse = response as Record<string, unknown>;
const data = typedResponse.data as Record<string, unknown> | undefined;
if (!data || typeof data !== 'object') {
return null;
}
const gqlData = data.data as Record<string, unknown> | undefined;
if (!gqlData || typeof gqlData !== 'object') {
return null;
}
const repository = gqlData.repository as Record<string, unknown> | undefined;
if (!repository || typeof repository !== 'object') {
return null;
}
const reports = repository.reports as Record<string, unknown> | undefined;
if (!reports || typeof reports !== 'object') {
return null;
}
// Get the field name for the report type
const fieldName = DeepSourceClient.getReportField(reportType);
if (!fieldName) {
return null;
}
const reportData = reports[fieldName] as Record<string, unknown> | undefined;
if (!reportData || typeof reportData !== 'object') {
return null;
}
return reportData;
}
}