/**
* @fileoverview QualityMetrics repository implementation
*
* Concrete implementation of IQualityMetricsRepository using DeepSource API.
*/
import { IQualityMetricsRepository } from '../../domain/aggregates/quality-metrics/quality-metrics.repository.js';
import { QualityMetrics } from '../../domain/aggregates/quality-metrics/quality-metrics.aggregate.js';
import { ProjectKey } from '../../types/branded.js';
import { MetricShortcode, MetricKey } from '../../models/metrics.js';
import { QualityMetricsId } from '../../domain/aggregates/quality-metrics/quality-metrics.types.js';
import { DeepSourceClient } from '../../deepsource.js';
import { QualityMetricsMapper } from '../mappers/quality-metrics.mapper.js';
import { createLogger } from '../../utils/logging/logger.js';
import { BaseDeepSourceClient } from '../../client/base-client.js';
import type { AxiosInstance } from 'axios';
const logger = createLogger('QualityMetricsRepository');
/**
* Concrete implementation of IQualityMetricsRepository using DeepSource API
*
* This repository provides access to QualityMetrics aggregates by fetching data
* from the DeepSource API and mapping it to domain models.
*
* Note: The DeepSource API returns metrics at the repository level with items
* for each context (language). This repository maps each item to a separate
* QualityMetrics aggregate. This ensures fresh data retrieval on every request
* as per requirements.
*/
export class QualityMetricsRepository implements IQualityMetricsRepository {
constructor(private readonly client: DeepSourceClient) {
// client is stored for use in methods
}
/**
* Helper method to get repository ID for a project
*
* @param projectKey - The project key
* @returns The repository GraphQL ID
*/
private async getRepositoryId(projectKey: ProjectKey): Promise<string> {
const projects = await this.client.listProjects();
const project = projects.find((p) => p.key === projectKey);
if (!project) {
throw new Error(`Project not found: ${projectKey}`);
}
// The listProjects doesn't include repository ID, so we need to fetch it
// using a repository query. We can get it from any repository-specific query.
// Let's use the runs query with minimal parameters to get the repository ID.
try {
const runs = await this.client.listRuns(projectKey, { first: 1 });
// The runs query includes repository information with ID
if (runs.items.length > 0 && runs.items[0]?.repository?.id) {
return runs.items[0].repository.id;
}
// If no runs exist, we need to make a direct repository query
// This is a fallback that uses the GraphQL client directly
const repoQuery = `
query($login: String!, $name: String!, $provider: VCSProvider!) {
repository(login: $login, name: $name, vcsProvider: $provider) {
id
}
}
`;
// Access protected client member via type assertion
const clientWithAxios = this.client as unknown as BaseDeepSourceClient & {
client: AxiosInstance;
};
const response = await clientWithAxios.client.post('', {
query: repoQuery.trim(),
variables: {
login: project.repository.login,
name: project.name,
provider: project.repository.provider,
},
});
if (response.data.errors) {
throw new Error(
`GraphQL Errors: ${response.data.errors.map((e: { message: string }) => e.message).join(', ')}`
);
}
const repositoryId = response.data.data?.repository?.id;
if (!repositoryId) {
throw new Error(`Repository ID not found for project: ${projectKey}`);
}
return repositoryId;
} catch (error) {
logger.error('Error fetching repository ID', { projectKey, error });
throw error;
}
}
/**
* Finds metrics by composite ID
*
* @param id - The composite ID (projectKey:metricKey:shortcode)
* @returns The metrics if found, null otherwise
*/
async findById(id: string): Promise<QualityMetrics | null> {
try {
logger.debug('Finding metrics by composite ID', { id });
// Parse composite ID
const parts = id.split(':');
if (parts.length !== 3) {
logger.error('Invalid composite ID format', { id });
return null;
}
const [projectKey, metricKey, shortcode] = parts;
return this.findByProjectAndMetric(
projectKey as ProjectKey,
shortcode as MetricShortcode,
metricKey as MetricKey
);
} catch (error) {
logger.error('Error finding metrics by ID', { id, error });
throw error;
}
}
/**
* Finds all metrics for a project
*
* @param projectKey - The project key
* @returns All quality metrics for the project
*/
async findByProject(projectKey: ProjectKey): Promise<QualityMetrics[]> {
try {
logger.debug('Finding all metrics for project', { projectKey });
const repositoryId = await this.getRepositoryId(projectKey);
const apiMetrics = await this.client.getQualityMetrics(projectKey);
const domainMetrics = QualityMetricsMapper.toDomainFromList(
apiMetrics,
projectKey,
repositoryId
);
logger.debug('Metrics found for project', {
projectKey,
count: domainMetrics.length,
});
return domainMetrics;
} catch (error) {
logger.error('Error finding metrics by project', { projectKey, error });
throw error;
}
}
/**
* Finds metrics for a project with server-side filtering
*
* @param projectKey - The project key
* @param shortcodeIn - Array of metric shortcodes to filter by
* @returns Filtered quality metrics for the project
*/
async findByProjectWithFilter(
projectKey: ProjectKey,
shortcodeIn: MetricShortcode[]
): Promise<QualityMetrics[]> {
try {
logger.debug('Finding metrics for project with filter', { projectKey, shortcodeIn });
const repositoryId = await this.getRepositoryId(projectKey);
// This now uses server-side filtering via the optimized client
const apiMetrics = await this.client.getQualityMetrics(projectKey, { shortcodeIn });
const domainMetrics = QualityMetricsMapper.toDomainFromList(
apiMetrics,
projectKey,
repositoryId
);
logger.debug('Filtered metrics found for project', {
projectKey,
count: domainMetrics.length,
requestedShortcodes: shortcodeIn.length,
});
return domainMetrics;
} catch (error) {
logger.error('Error finding filtered metrics by project', { projectKey, shortcodeIn, error });
throw error;
}
}
/**
* Finds metrics by project and metric type
*
* @param projectKey - The project key
* @param shortcode - The metric shortcode
* @param metricKey - Optional metric key filter
* @returns The metrics if found, null otherwise
*/
async findByProjectAndMetric(
projectKey: ProjectKey,
shortcode: MetricShortcode,
metricKey?: MetricKey
): Promise<QualityMetrics | null> {
try {
logger.debug('Finding metrics by project and type', {
projectKey,
shortcode,
metricKey,
});
const repositoryId = await this.getRepositoryId(projectKey);
const apiMetrics = await this.client.getQualityMetrics(projectKey, {
shortcodeIn: [shortcode],
});
const metric = apiMetrics.find((m) => m.shortcode === shortcode);
if (!metric) {
logger.debug('Metric not found', { projectKey, shortcode });
return null;
}
// If metricKey is specified, find that specific item
if (metricKey) {
const item = metric.items.find((i) => i.key === metricKey);
if (!item) {
logger.debug('Metric item not found', { projectKey, shortcode, metricKey });
return null;
}
return QualityMetricsMapper.toDomain(metric, item, projectKey, repositoryId);
}
// If no metricKey specified, return the first item (usually AGGREGATE)
if (metric.items.length > 0 && metric.items[0]) {
return QualityMetricsMapper.toDomain(metric, metric.items[0], projectKey, repositoryId);
}
logger.debug('No metric items found', { projectKey, shortcode });
return null;
} catch (error) {
logger.error('Error finding metrics by project and type', {
projectKey,
shortcode,
metricKey,
error,
});
throw error;
}
}
/**
* Finds all metrics with failing thresholds for a project
*
* @param projectKey - The project key
* @returns Metrics that are currently failing their thresholds
*/
async findFailingMetrics(projectKey: ProjectKey): Promise<QualityMetrics[]> {
try {
logger.debug('Finding failing metrics for project', { projectKey });
const allMetrics = await this.findByProject(projectKey);
const failingMetrics = allMetrics.filter((m) => m.thresholdStatus === 'FAILING');
logger.debug('Failing metrics found', {
projectKey,
count: failingMetrics.length,
});
return failingMetrics;
} catch (error) {
logger.error('Error finding failing metrics', { projectKey, error });
throw error;
}
}
/**
* Finds all reported metrics for a project
*
* @param projectKey - The project key
* @returns Metrics that have isReported set to true
*/
async findReportedMetrics(projectKey: ProjectKey): Promise<QualityMetrics[]> {
try {
logger.debug('Finding reported metrics for project', { projectKey });
const allMetrics = await this.findByProject(projectKey);
const reportedMetrics = allMetrics.filter((m) => m.configuration.isReported);
logger.debug('Reported metrics found', {
projectKey,
count: reportedMetrics.length,
});
return reportedMetrics;
} catch (error) {
logger.error('Error finding reported metrics', { projectKey, error });
throw error;
}
}
/**
* Finds metrics by composite ID components
*
* @param id - The composite ID components
* @returns The metrics if found, null otherwise
*/
async findByCompositeId(id: QualityMetricsId): Promise<QualityMetrics | null> {
try {
logger.debug('Finding metrics by composite ID components', { id });
return this.findByProjectAndMetric(id.projectKey, id.shortcode, id.metricKey);
} catch (error) {
logger.error('Error finding metrics by composite ID', { id, error });
throw error;
}
}
/**
* Counts total metrics for a project
*
* @param projectKey - The project key
* @returns The total number of metrics
*/
async countByProject(projectKey: ProjectKey): Promise<number> {
try {
logger.debug('Counting metrics for project', { projectKey });
const metrics = await this.findByProject(projectKey);
const count = metrics.length;
logger.debug('Metric count for project', { projectKey, count });
return count;
} catch (error) {
logger.error('Error counting metrics', { projectKey, error });
throw error;
}
}
/**
* Counts failing metrics for a project
*
* @param projectKey - The project key
* @returns The number of failing metrics
*/
async countFailingByProject(projectKey: ProjectKey): Promise<number> {
try {
logger.debug('Counting failing metrics for project', { projectKey });
const failingMetrics = await this.findFailingMetrics(projectKey);
const count = failingMetrics.length;
logger.debug('Failing metric count', { projectKey, count });
return count;
} catch (error) {
logger.error('Error counting failing metrics', { projectKey, error });
throw error;
}
}
/**
* Checks if metrics exist for a project and shortcode
*
* @param projectKey - The project key
* @param shortcode - The metric shortcode
* @returns True if metrics exist, false otherwise
*/
async exists(projectKey: ProjectKey, shortcode: MetricShortcode): Promise<boolean> {
try {
logger.debug('Checking if metrics exist', { projectKey, shortcode });
const metric = await this.findByProjectAndMetric(projectKey, shortcode);
const exists = metric !== null;
logger.debug('Metrics existence check', { projectKey, shortcode, exists });
return exists;
} catch (error) {
logger.error('Error checking metrics existence', { projectKey, shortcode, error });
throw error;
}
}
/**
* Saves quality metrics
*
* Note: The DeepSource API only supports updating thresholds and settings,
* not creating new metrics. This method updates the threshold if needed.
*
* @param metrics - The metrics to save
* @throws Error if the metric doesn't exist or update fails
*/
async save(metrics: QualityMetrics): Promise<void> {
logger.debug('Attempting to save metrics', {
id: metrics.id,
projectKey: metrics.projectKey,
});
try {
// Update threshold if it has changed
const threshold = metrics.configuration.threshold;
const thresholdValue = threshold ? threshold.value : null;
await this.client.setMetricThreshold({
repositoryId: metrics.repositoryId,
metricShortcode: metrics.configuration.shortcode,
metricKey: metrics.configuration.metricKey,
thresholdValue,
});
// Update settings
await this.client.updateMetricSetting({
repositoryId: metrics.repositoryId,
metricShortcode: metrics.configuration.shortcode,
isReported: metrics.configuration.isReported,
isThresholdEnforced: metrics.configuration.isThresholdEnforced,
});
logger.debug('Metrics saved successfully', { id: metrics.id });
} catch (error) {
logger.error('Error saving metrics', { id: metrics.id, error });
throw error;
}
}
/**
* Deletes quality metrics
*
* Note: The DeepSource API doesn't support deleting metrics.
* This method is implemented to satisfy the interface but will
* throw an error indicating the operation is not supported.
*
* @param id - The metrics ID to delete
* @throws Error indicating the operation is not supported
*/
// skipcq: JS-0105 - Repository method required by interface contract
async delete(id: string): Promise<void> {
logger.warn('Attempted to delete metrics', { id });
throw new Error(
'Delete operation is not supported by DeepSource API. ' +
'Metrics are managed automatically based on repository configuration.'
);
}
}