/**
* Data Merger
*
* Handles intelligent merging of data from multiple providers for read operations.
* Removes duplicates, combines unique fields, and maintains data integrity.
*/
import { ProviderResult } from '../providers/types.js';
export class DataMerger {
/**
* Merges list operation results from multiple providers
* Removes duplicates based on name/id fields and combines arrays
*/
static mergeListResults(results: ProviderResult[]): any[] {
const allItems: any[] = [];
const seen = new Set<string>();
// Collect all items from successful results
for (const result of results) {
if (result.success && result.data) {
const items = Array.isArray(result.data) ? result.data : [result.data];
for (const item of items) {
const key = this.getItemKey(item);
if (!seen.has(key)) {
seen.add(key);
// Add provider information to each item
allItems.push({
...item,
_providers: [result.provider]
});
} else {
// Item already exists, add provider to existing item
const existingItem = allItems.find(i => this.getItemKey(i) === key);
if (existingItem && !existingItem._providers.includes(result.provider)) {
existingItem._providers.push(result.provider);
}
}
}
}
}
// Sort by updated_at or created_at if available
return this.sortItemsByDate(allItems);
}
/**
* Merges get operation results from multiple providers
* Combines unique fields from different providers into a single object
*/
static mergeGetResults(results: ProviderResult[]): any {
if (results.length === 0) return null;
if (results.length === 1) return results[0].success ? results[0].data : null;
// Start with the first successful result
let mergedData: any = null;
const providers: string[] = [];
for (const result of results) {
if (result.success && result.data) {
providers.push(result.provider);
if (!mergedData) {
mergedData = { ...result.data };
} else {
// Merge unique fields from this provider
mergedData = this.mergeObjects(mergedData, result.data, result.provider);
}
}
}
if (mergedData) {
mergedData._providers = providers;
mergedData._mergeInfo = {
totalProviders: results.length,
successfulProviders: providers.length,
merged: true
};
}
return mergedData;
}
/**
* Merges search operation results from multiple providers
* Combines search results, removes duplicates, and ranks by relevance
*/
static mergeSearchResults(results: ProviderResult[]): any[] {
const allItems: any[] = [];
const seen = new Set<string>();
// Collect all items from successful results
for (const result of results) {
if (result.success && result.data) {
const items = Array.isArray(result.data) ? result.data : [result.data];
for (const item of items) {
const key = this.getItemKey(item);
if (!seen.has(key)) {
seen.add(key);
// Add provider information and search metadata
allItems.push({
...item,
_providers: [result.provider],
_search: {
provider: result.provider,
relevance: this.calculateRelevance(item, result.provider)
}
});
} else {
// Item already exists, add provider to existing item
const existingItem = allItems.find(i => this.getItemKey(i) === key);
if (existingItem) {
if (!existingItem._providers.includes(result.provider)) {
existingItem._providers.push(result.provider);
}
// Update search info if this provider has better relevance
const newRelevance = this.calculateRelevance(item, result.provider);
if (newRelevance > existingItem._search.relevance) {
existingItem._search = {
provider: result.provider,
relevance: newRelevance
};
}
}
}
}
}
}
// Sort by relevance score, then by date
return allItems.sort((a, b) => {
// Higher relevance first
if (b._search.relevance !== a._search.relevance) {
return b._search.relevance - a._search.relevance;
}
// Then by date if available
return this.compareByDate(a, b);
});
}
/**
* Generates a unique key for an item based on common identifier fields
*/
private static getItemKey(item: any): string {
// Try different identifier fields in order of preference
const possibleKeys = [
item.id,
item.name,
item.full_name,
item.title,
`${item.owner?.login || item.owner}/${item.name || item.repo}`,
item.sha, // for commits
item.number, // for issues/PRs
JSON.stringify(item) // fallback
];
for (const key of possibleKeys) {
if (key && typeof key === 'string') {
return key;
}
}
return Math.random().toString(36); // ultimate fallback
}
/**
* Merges two objects, preserving unique fields from each provider
*/
private static mergeObjects(baseObj: any, newObj: any, provider: string): any {
const merged = { ...baseObj };
for (const [key, value] of Object.entries(newObj)) {
// If field doesn't exist in base, add it
if (!(key in merged)) {
merged[key] = value;
merged[`_${key}_${provider}`] = value; // Also keep provider-specific version
}
// If field exists but is different, keep both versions
else if (merged[key] !== value) {
merged[`_${key}_${provider}`] = value;
merged[`_${key}_conflict`] = true;
}
}
return merged;
}
/**
* Calculates relevance score for search results
*/
private static calculateRelevance(item: any, provider: string): number {
let score = 0;
// Base score by provider (GitHub slightly preferred)
if (provider === 'github') {
score += 1;
}
// Score by item type
if (item.stargazers_count !== undefined) {
score += Math.min(item.stargazers_count / 100, 10); // Max 10 points
}
if (item.forks_count !== undefined) {
score += Math.min(item.forks_count / 50, 5); // Max 5 points
}
if (item.updated_at) {
const daysSinceUpdate = (Date.now() - new Date(item.updated_at).getTime()) / (1000 * 60 * 60 * 24);
score += Math.max(0, 5 - daysSinceUpdate / 30); // Max 5 points, decays over time
}
return score;
}
/**
* Sorts items by date field (updated_at, created_at, etc.)
*/
private static sortItemsByDate(items: any[]): any[] {
return items.sort((a, b) => this.compareByDate(a, b));
}
/**
* Compares two items by date fields
*/
private static compareByDate(a: any, b: any): number {
const dateFields = ['updated_at', 'created_at', 'pushed_at', 'committed_date'];
for (const field of dateFields) {
const dateA = a[field] ? new Date(a[field]).getTime() : 0;
const dateB = b[field] ? new Date(b[field]).getTime() : 0;
if (dateA !== dateB) {
return dateB - dateA; // Newest first
}
}
return 0;
}
/**
* Removes duplicate items from an array based on a field
*/
static removeDuplicates(items: any[], field: string): any[] {
const seen = new Set();
return items.filter(item => {
const value = item[field];
if (seen.has(value)) {
return false;
}
seen.add(value);
return true;
});
}
}