- Add searchReference to ApplicationListItem type - Fix result variable in toApplicationDetails function - Add query helper functions for req.query parameter handling - Fix req.query.* type errors in routes (applications, cache, classifications, objects) - Fix CompletenessCategoryConfig missing id property - Fix number | null type errors in dataService - Add utils/queryHelpers.ts for reusable query parameter helpers
682 lines
26 KiB
TypeScript
682 lines
26 KiB
TypeScript
import { Router, Request, Response } from 'express';
|
|
import { dataService } from '../services/dataService.js';
|
|
import { databaseService } from '../services/database.js';
|
|
import { cmdbService } from '../services/cmdbService.js';
|
|
import { logger } from '../services/logger.js';
|
|
import { calculateRequiredEffortApplicationManagementWithBreakdown } from '../services/effortCalculation.js';
|
|
import { findBIAMatch, loadBIAData, clearBIACache, calculateSimilarity } from '../services/biaMatchingService.js';
|
|
import { calculateApplicationCompleteness } from '../services/dataCompletenessConfig.js';
|
|
import { getQueryString } from '../utils/queryHelpers.js';
|
|
import type { SearchFilters, ReferenceValue, ClassificationResult, ApplicationDetails, ApplicationStatus } from '../types/index.js';
|
|
import type { Server, Flows, Certificate, Domain, AzureSubscription, CMDBObjectTypeName } from '../generated/jira-types.js';
|
|
|
|
const router = Router();
|
|
|
|
// Search applications with filters
|
|
router.post('/search', async (req: Request, res: Response) => {
|
|
try {
|
|
const { filters, page = 1, pageSize = 25 } = req.body as {
|
|
filters: SearchFilters;
|
|
page?: number;
|
|
pageSize?: number;
|
|
};
|
|
|
|
const result = await dataService.searchApplications(filters, page, pageSize);
|
|
res.json(result);
|
|
} catch (error) {
|
|
logger.error('Failed to search applications', error);
|
|
res.status(500).json({ error: 'Failed to search applications' });
|
|
}
|
|
});
|
|
|
|
// Get team dashboard data
|
|
router.get('/team-dashboard', async (req: Request, res: Response) => {
|
|
try {
|
|
const excludedStatusesParam = getQueryString(req, 'excludedStatuses');
|
|
let excludedStatuses: ApplicationStatus[] = [];
|
|
|
|
if (excludedStatusesParam && excludedStatusesParam.trim().length > 0) {
|
|
// Parse comma-separated statuses
|
|
excludedStatuses = excludedStatusesParam
|
|
.split(',')
|
|
.map(s => s.trim())
|
|
.filter(s => s.length > 0) as ApplicationStatus[];
|
|
} else {
|
|
// Default to excluding 'Closed' and 'Deprecated' if not specified
|
|
excludedStatuses = ['Closed', 'Deprecated'];
|
|
}
|
|
|
|
const data = await dataService.getTeamDashboardData(excludedStatuses);
|
|
res.json(data);
|
|
} catch (error) {
|
|
logger.error('Failed to get team dashboard data', error);
|
|
res.status(500).json({ error: 'Failed to get team dashboard data' });
|
|
}
|
|
});
|
|
|
|
// Get team portfolio health metrics
|
|
router.get('/team-portfolio-health', async (req: Request, res: Response) => {
|
|
try {
|
|
const excludedStatusesParam = getQueryString(req, 'excludedStatuses');
|
|
let excludedStatuses: ApplicationStatus[] = [];
|
|
|
|
if (excludedStatusesParam && excludedStatusesParam.trim().length > 0) {
|
|
// Parse comma-separated statuses
|
|
excludedStatuses = excludedStatusesParam
|
|
.split(',')
|
|
.map(s => s.trim())
|
|
.filter(s => s.length > 0) as ApplicationStatus[];
|
|
} else {
|
|
// Default to excluding 'Closed' and 'Deprecated' if not specified
|
|
excludedStatuses = ['Closed', 'Deprecated'];
|
|
}
|
|
|
|
const data = await dataService.getTeamPortfolioHealth(excludedStatuses);
|
|
res.json(data);
|
|
} catch (error) {
|
|
logger.error('Failed to get team portfolio health', error);
|
|
res.status(500).json({ error: 'Failed to get team portfolio health' });
|
|
}
|
|
});
|
|
|
|
// Get Business Importance vs BIA comparison
|
|
// NOTE: This must come BEFORE the /:id route to avoid route conflicts
|
|
router.get('/business-importance-comparison', async (req: Request, res: Response) => {
|
|
try {
|
|
logger.info('Business Importance comparison endpoint called');
|
|
const data = await dataService.getBusinessImportanceComparison();
|
|
logger.info(`Business Importance comparison: returning ${data.applications.length} applications`);
|
|
res.json(data);
|
|
} catch (error) {
|
|
logger.error('Failed to get business importance comparison', error);
|
|
res.status(500).json({ error: 'Failed to get business importance comparison' });
|
|
}
|
|
});
|
|
|
|
// Test BIA data loading (for debugging)
|
|
router.get('/bia-test', async (req: Request, res: Response) => {
|
|
try {
|
|
if (getQueryString(req, 'clear') === 'true') {
|
|
clearBIACache();
|
|
}
|
|
const biaData = loadBIAData();
|
|
res.json({
|
|
recordCount: biaData.length,
|
|
records: biaData.slice(0, 20), // First 20 records
|
|
sample: biaData.slice(0, 5).map(r => `${r.applicationName} -> ${r.biaValue}`),
|
|
});
|
|
} catch (error) {
|
|
logger.error('Failed to test BIA data loading', error);
|
|
res.status(500).json({ error: 'Failed to test BIA data loading', details: error instanceof Error ? error.message : String(error) });
|
|
}
|
|
});
|
|
|
|
// Comprehensive debug endpoint for BIA matching
|
|
router.get('/bia-debug', async (req: Request, res: Response) => {
|
|
try {
|
|
clearBIACache();
|
|
const biaData = loadBIAData();
|
|
|
|
// Get a few sample applications
|
|
const searchResult = await dataService.searchApplications({}, 1, 50);
|
|
const sampleApps = searchResult.applications.slice(0, 10);
|
|
|
|
// Test specific examples mentioned by user
|
|
const testNames = ['Aanmeldzuilen', 'PregnaOne', 'BeagleBoxx'];
|
|
const testApps = searchResult.applications.filter(app =>
|
|
testNames.some(name =>
|
|
app.name.toLowerCase().includes(name.toLowerCase()) ||
|
|
(app.searchReference && app.searchReference.toLowerCase().includes(name.toLowerCase()))
|
|
)
|
|
);
|
|
|
|
const debugResults = [];
|
|
|
|
// Test each sample app
|
|
for (const app of [...sampleApps, ...testApps]) {
|
|
const matchResult = findBIAMatch(app.name, app.searchReference ?? null);
|
|
|
|
// Find all potential matches in Excel data for detailed analysis
|
|
const normalizedAppName = app.name.toLowerCase().trim();
|
|
const normalizedSearchRef = app.searchReference ? app.searchReference.toLowerCase().trim() : null;
|
|
|
|
const potentialMatches = biaData.map(record => {
|
|
const normalizedRecordName = record.applicationName.toLowerCase().trim();
|
|
const exactNameMatch = normalizedAppName === normalizedRecordName;
|
|
const exactSearchRefMatch = normalizedSearchRef === normalizedRecordName;
|
|
const startsWithApp = normalizedRecordName.startsWith(normalizedAppName) || normalizedAppName.startsWith(normalizedRecordName);
|
|
const containsApp = normalizedRecordName.includes(normalizedAppName) || normalizedAppName.includes(normalizedRecordName);
|
|
|
|
// Calculate similarity using the exported function
|
|
const similarity = calculateSimilarity(normalizedAppName, normalizedRecordName);
|
|
|
|
return {
|
|
excelName: record.applicationName,
|
|
excelBIA: record.biaValue,
|
|
exactNameMatch,
|
|
exactSearchRefMatch,
|
|
startsWithApp,
|
|
containsApp,
|
|
similarity: similarity,
|
|
};
|
|
}).filter(m => m.exactNameMatch || m.exactSearchRefMatch || m.startsWithApp || m.containsApp || m.similarity > 0.5);
|
|
|
|
debugResults.push({
|
|
appName: app.name,
|
|
searchReference: app.searchReference,
|
|
matchResult: matchResult,
|
|
potentialMatches: potentialMatches.slice(0, 5), // Top 5 potential matches
|
|
allExcelRecords: biaData.length,
|
|
});
|
|
}
|
|
|
|
res.json({
|
|
excelDataLoaded: biaData.length,
|
|
sampleExcelRecords: biaData.slice(0, 20),
|
|
debugResults,
|
|
testNames: testNames.map(name => {
|
|
const matchingExcel = biaData.filter(r =>
|
|
r.applicationName.toLowerCase().includes(name.toLowerCase()) ||
|
|
name.toLowerCase().includes(r.applicationName.toLowerCase())
|
|
);
|
|
return {
|
|
name,
|
|
matchingExcelRecords: matchingExcel,
|
|
};
|
|
}),
|
|
});
|
|
} catch (error) {
|
|
logger.error('Failed to debug BIA matching', error);
|
|
res.status(500).json({
|
|
error: 'Failed to debug BIA matching',
|
|
details: error instanceof Error ? error.message : String(error),
|
|
stack: error instanceof Error ? error.stack : undefined,
|
|
});
|
|
}
|
|
});
|
|
|
|
// Get BIA comparison data
|
|
router.get('/bia-comparison', async (req: Request, res: Response) => {
|
|
try {
|
|
// Clear cache and reload BIA data to ensure fresh data
|
|
const { loadBIAData, clearBIACache } = await import('../services/biaMatchingService.js');
|
|
clearBIACache();
|
|
|
|
// Load fresh data
|
|
const testBIAData = loadBIAData();
|
|
logger.info(`BIA comparison: Loaded ${testBIAData.length} records from Excel file`);
|
|
if (testBIAData.length === 0) {
|
|
logger.error('BIA comparison: No Excel data loaded - check if BIA.xlsx exists and is readable');
|
|
logger.error('This could indicate:');
|
|
logger.error(' 1. Excel file not found at backend/data/BIA.xlsx');
|
|
logger.error(' 2. Column C (index 2) or K (index 10) not found');
|
|
logger.error(' 3. No valid BIA values (A-F) in column K');
|
|
} else {
|
|
logger.info(`BIA comparison: Sample Excel records: ${testBIAData.slice(0, 5).map(r => `"${r.applicationName}"->${r.biaValue}`).join(', ')}`);
|
|
}
|
|
|
|
// Get all applications (no filters)
|
|
const searchResult = await dataService.searchApplications({}, 1, 10000);
|
|
logger.info(`BIA comparison: Found ${searchResult.applications.length} applications to compare`);
|
|
|
|
// Get all Business Impact Analyses for mapping
|
|
const biaReferences = await dataService.getBusinessImpactAnalyses();
|
|
const biaMap = new Map<string, ReferenceValue>();
|
|
biaReferences.forEach(bia => {
|
|
// Map by the letter (A-F) - typically the name starts with the letter
|
|
const letter = bia.name.charAt(0).toUpperCase();
|
|
if (/^[A-F]$/.test(letter)) {
|
|
biaMap.set(letter, bia);
|
|
}
|
|
// Also try to match by key or name containing the letter
|
|
if (bia.name.toUpperCase().includes(' - ')) {
|
|
const parts = bia.name.split(' - ');
|
|
if (parts.length > 0 && /^[A-F]$/.test(parts[0].trim().toUpperCase())) {
|
|
biaMap.set(parts[0].trim().toUpperCase(), bia);
|
|
}
|
|
}
|
|
});
|
|
|
|
const applications = searchResult.applications;
|
|
const comparisonItems = [];
|
|
|
|
let matched = 0;
|
|
let mismatched = 0;
|
|
let notFound = 0;
|
|
let noExcelBIA = 0;
|
|
|
|
for (const app of applications) {
|
|
// Find BIA match in Excel
|
|
const matchResult = findBIAMatch(app.name, app.searchReference ?? null);
|
|
|
|
// Log first few matches for debugging
|
|
if (comparisonItems.length < 5) {
|
|
logger.debug(`BIA match for "${app.name}": ${matchResult.biaValue || 'NOT FOUND'} (type: ${matchResult.matchType || 'none'})`);
|
|
}
|
|
|
|
// Extract BIA letter from current BIA name (handle formats like "A", "A - Test/Archief", etc.)
|
|
let currentBIALetter: string | null = null;
|
|
if (app.businessImpactAnalyse?.name) {
|
|
const match = app.businessImpactAnalyse.name.match(/^([A-F])/);
|
|
if (match) {
|
|
currentBIALetter = match[1].toUpperCase();
|
|
} else {
|
|
// Fallback to first character
|
|
currentBIALetter = app.businessImpactAnalyse.name.charAt(0).toUpperCase();
|
|
if (!/^[A-F]$/.test(currentBIALetter)) {
|
|
currentBIALetter = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
const excelBIALetter = matchResult.biaValue;
|
|
|
|
let matchStatus: 'match' | 'mismatch' | 'not_found' | 'no_excel_bia';
|
|
|
|
if (!excelBIALetter) {
|
|
matchStatus = 'no_excel_bia';
|
|
noExcelBIA++;
|
|
} else if (!currentBIALetter) {
|
|
matchStatus = 'not_found';
|
|
notFound++;
|
|
} else if (currentBIALetter === excelBIALetter) {
|
|
matchStatus = 'match';
|
|
matched++;
|
|
} else {
|
|
matchStatus = 'mismatch';
|
|
mismatched++;
|
|
}
|
|
|
|
comparisonItems.push({
|
|
id: app.id,
|
|
key: app.key,
|
|
name: app.name,
|
|
searchReference: app.searchReference,
|
|
currentBIA: app.businessImpactAnalyse,
|
|
excelBIA: excelBIALetter,
|
|
excelApplicationName: matchResult.excelApplicationName,
|
|
matchStatus,
|
|
matchType: matchResult.matchType,
|
|
matchConfidence: matchResult.matchConfidence,
|
|
allMatches: matchResult.allMatches,
|
|
});
|
|
}
|
|
|
|
res.json({
|
|
applications: comparisonItems,
|
|
summary: {
|
|
total: applications.length,
|
|
matched,
|
|
mismatched,
|
|
notFound,
|
|
noExcelBIA,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
logger.error('Failed to get BIA comparison', error);
|
|
res.status(500).json({ error: 'Failed to get BIA comparison' });
|
|
}
|
|
});
|
|
|
|
// Get application by ID
|
|
// Query params:
|
|
// - mode=edit: Force refresh from Jira for editing (includes _jiraUpdatedAt for conflict detection)
|
|
router.get('/:id', async (req: Request, res: Response) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const mode = getQueryString(req, 'mode');
|
|
|
|
// Don't treat special routes as application IDs
|
|
if (id === 'team-dashboard' || id === 'team-portfolio-health' || id === 'business-importance-comparison' || id === 'bia-comparison' || id === 'bia-test' || id === 'calculate-effort' || id === 'search') {
|
|
res.status(404).json({ error: 'Route not found' });
|
|
return;
|
|
}
|
|
|
|
// Edit mode: force refresh from Jira for fresh data + conflict detection
|
|
const application = mode === 'edit'
|
|
? await dataService.getApplicationForEdit(id)
|
|
: await dataService.getApplicationById(id);
|
|
|
|
if (!application) {
|
|
res.status(404).json({ error: 'Application not found' });
|
|
return;
|
|
}
|
|
|
|
// Calculate data completeness percentage
|
|
const completenessPercentage = calculateApplicationCompleteness(application);
|
|
const applicationWithCompleteness = {
|
|
...application,
|
|
dataCompletenessPercentage: Math.round(completenessPercentage * 10) / 10, // Round to 1 decimal
|
|
};
|
|
|
|
res.json(applicationWithCompleteness);
|
|
} catch (error) {
|
|
logger.error('Failed to get application', error);
|
|
res.status(500).json({ error: 'Failed to get application' });
|
|
}
|
|
});
|
|
|
|
// Update application with conflict detection
|
|
router.put('/:id', async (req: Request, res: Response) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const { updates, _jiraUpdatedAt } = req.body as {
|
|
updates?: {
|
|
applicationFunctions?: ReferenceValue[];
|
|
dynamicsFactor?: ReferenceValue;
|
|
complexityFactor?: ReferenceValue;
|
|
numberOfUsers?: ReferenceValue;
|
|
governanceModel?: ReferenceValue;
|
|
applicationSubteam?: ReferenceValue;
|
|
applicationTeam?: ReferenceValue;
|
|
applicationType?: ReferenceValue;
|
|
hostingType?: ReferenceValue;
|
|
businessImpactAnalyse?: ReferenceValue;
|
|
overrideFTE?: number | null;
|
|
applicationManagementHosting?: string;
|
|
applicationManagementTAM?: string;
|
|
source?: 'AI_ACCEPTED' | 'AI_MODIFIED' | 'MANUAL';
|
|
};
|
|
_jiraUpdatedAt?: string;
|
|
};
|
|
|
|
// Support both new format (updates object) and legacy format (direct body)
|
|
const actualUpdates = updates || req.body;
|
|
|
|
const application = await dataService.getApplicationById(id);
|
|
if (!application) {
|
|
res.status(404).json({ error: 'Application not found' });
|
|
return;
|
|
}
|
|
|
|
// Build changes object for history
|
|
const changes: ClassificationResult['changes'] = {};
|
|
if (actualUpdates.applicationFunctions) {
|
|
changes.applicationFunctions = {
|
|
from: application.applicationFunctions,
|
|
to: actualUpdates.applicationFunctions,
|
|
};
|
|
}
|
|
if (actualUpdates.dynamicsFactor) {
|
|
changes.dynamicsFactor = {
|
|
from: application.dynamicsFactor,
|
|
to: actualUpdates.dynamicsFactor,
|
|
};
|
|
}
|
|
if (actualUpdates.complexityFactor) {
|
|
changes.complexityFactor = {
|
|
from: application.complexityFactor,
|
|
to: actualUpdates.complexityFactor,
|
|
};
|
|
}
|
|
if (actualUpdates.numberOfUsers) {
|
|
changes.numberOfUsers = {
|
|
from: application.numberOfUsers,
|
|
to: actualUpdates.numberOfUsers,
|
|
};
|
|
}
|
|
if (actualUpdates.governanceModel) {
|
|
changes.governanceModel = {
|
|
from: application.governanceModel,
|
|
to: actualUpdates.governanceModel,
|
|
};
|
|
}
|
|
|
|
// Call updateApplication with conflict detection if _jiraUpdatedAt is provided
|
|
const result = await dataService.updateApplication(id, actualUpdates, _jiraUpdatedAt);
|
|
|
|
// Check for conflicts
|
|
if (!result.success && result.conflict) {
|
|
// Return 409 Conflict with details
|
|
res.status(409).json({
|
|
status: 'conflict',
|
|
message: 'Object is gewijzigd door iemand anders',
|
|
conflicts: result.conflict.conflicts,
|
|
jiraUpdatedAt: result.conflict.jiraUpdatedAt,
|
|
canMerge: result.conflict.canMerge,
|
|
warning: result.conflict.warning,
|
|
actions: {
|
|
forceOverwrite: true,
|
|
merge: result.conflict.canMerge || false,
|
|
discard: true,
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (!result.success) {
|
|
res.status(500).json({ error: result.error || 'Failed to update application' });
|
|
return;
|
|
}
|
|
|
|
// Save to classification history
|
|
const classificationResult: ClassificationResult = {
|
|
applicationId: id,
|
|
applicationName: application.name,
|
|
changes,
|
|
source: actualUpdates.source || 'MANUAL',
|
|
timestamp: new Date(),
|
|
};
|
|
await databaseService.saveClassificationResult(classificationResult);
|
|
|
|
// Return updated application
|
|
const updatedApp = result.data || await dataService.getApplicationById(id);
|
|
res.json(updatedApp);
|
|
} catch (error) {
|
|
logger.error('Failed to update application', error);
|
|
res.status(500).json({ error: 'Failed to update application' });
|
|
}
|
|
});
|
|
|
|
// Force update (ignore conflicts)
|
|
router.put('/:id/force', async (req: Request, res: Response) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const updates = req.body;
|
|
|
|
const application = await dataService.getApplicationById(id);
|
|
if (!application) {
|
|
res.status(404).json({ error: 'Application not found' });
|
|
return;
|
|
}
|
|
|
|
// Force update without conflict check
|
|
const result = await dataService.updateApplication(id, updates);
|
|
|
|
if (!result.success) {
|
|
res.status(500).json({ error: result.error || 'Failed to update application' });
|
|
return;
|
|
}
|
|
|
|
const updatedApp = result.data || await dataService.getApplicationById(id);
|
|
res.json(updatedApp);
|
|
} catch (error) {
|
|
logger.error('Failed to force update application', error);
|
|
res.status(500).json({ error: 'Failed to force update application' });
|
|
}
|
|
});
|
|
|
|
// Calculate FTE effort for an application (real-time calculation without saving)
|
|
router.post('/calculate-effort', async (req: Request, res: Response) => {
|
|
try {
|
|
const applicationData = req.body as Partial<ApplicationDetails>;
|
|
|
|
// Build a complete ApplicationDetails object with defaults
|
|
const application: ApplicationDetails = {
|
|
id: applicationData.id || '',
|
|
key: applicationData.key || '',
|
|
name: applicationData.name || '',
|
|
searchReference: applicationData.searchReference || null,
|
|
description: applicationData.description || null,
|
|
supplierProduct: applicationData.supplierProduct || null,
|
|
organisation: applicationData.organisation || null,
|
|
hostingType: applicationData.hostingType || null,
|
|
status: applicationData.status || null,
|
|
businessImportance: applicationData.businessImportance || null,
|
|
businessImpactAnalyse: applicationData.businessImpactAnalyse || null,
|
|
systemOwner: applicationData.systemOwner || null,
|
|
businessOwner: applicationData.businessOwner || null,
|
|
functionalApplicationManagement: applicationData.functionalApplicationManagement || null,
|
|
technicalApplicationManagement: applicationData.technicalApplicationManagement || null,
|
|
technicalApplicationManagementPrimary: applicationData.technicalApplicationManagementPrimary || null,
|
|
technicalApplicationManagementSecondary: applicationData.technicalApplicationManagementSecondary || null,
|
|
medischeTechniek: applicationData.medischeTechniek || false,
|
|
applicationFunctions: applicationData.applicationFunctions || [],
|
|
dynamicsFactor: applicationData.dynamicsFactor || null,
|
|
complexityFactor: applicationData.complexityFactor || null,
|
|
numberOfUsers: applicationData.numberOfUsers || null,
|
|
governanceModel: applicationData.governanceModel || null,
|
|
applicationSubteam: applicationData.applicationSubteam || null,
|
|
applicationTeam: applicationData.applicationTeam || null,
|
|
applicationType: applicationData.applicationType || null,
|
|
platform: applicationData.platform || null,
|
|
requiredEffortApplicationManagement: null,
|
|
overrideFTE: applicationData.overrideFTE || null,
|
|
applicationManagementHosting: applicationData.applicationManagementHosting || null,
|
|
applicationManagementTAM: applicationData.applicationManagementTAM || null,
|
|
technischeArchitectuur: applicationData.technischeArchitectuur || null,
|
|
};
|
|
|
|
const result = calculateRequiredEffortApplicationManagementWithBreakdown(application);
|
|
|
|
res.json({
|
|
requiredEffortApplicationManagement: result.finalEffort,
|
|
breakdown: result.breakdown,
|
|
});
|
|
} catch (error) {
|
|
logger.error('Failed to calculate effort', error);
|
|
res.status(500).json({ error: 'Failed to calculate effort' });
|
|
}
|
|
});
|
|
|
|
// Get application classification history
|
|
router.get('/:id/history', async (req: Request, res: Response) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const history = await databaseService.getClassificationsByApplicationId(id);
|
|
res.json(history);
|
|
} catch (error) {
|
|
logger.error('Failed to get classification history', error);
|
|
res.status(500).json({ error: 'Failed to get classification history' });
|
|
}
|
|
});
|
|
|
|
// Get related objects for an application (from cache)
|
|
router.get('/:id/related/:objectType', async (req: Request, res: Response) => {
|
|
try {
|
|
const { id, objectType } = req.params;
|
|
|
|
// Map object type string to CMDBObjectTypeName
|
|
const typeMap: Record<string, CMDBObjectTypeName> = {
|
|
'Server': 'Server',
|
|
'server': 'Server',
|
|
'Flows': 'Flows',
|
|
'flows': 'Flows',
|
|
'Flow': 'Flows',
|
|
'flow': 'Flows',
|
|
'Connection': 'Flows', // Frontend uses "Connection" for Flows
|
|
'connection': 'Flows',
|
|
'Certificate': 'Certificate',
|
|
'certificate': 'Certificate',
|
|
'Domain': 'Domain',
|
|
'domain': 'Domain',
|
|
'AzureSubscription': 'AzureSubscription',
|
|
'azuresubscription': 'AzureSubscription',
|
|
};
|
|
|
|
const typeName = typeMap[objectType];
|
|
if (!typeName) {
|
|
res.status(400).json({ error: `Unknown object type: ${objectType}` });
|
|
return;
|
|
}
|
|
|
|
// Use CMDBService to get related objects from cache
|
|
type RelatedObjectType = Server | Flows | Certificate | Domain | AzureSubscription;
|
|
let relatedObjects: RelatedObjectType[] = [];
|
|
|
|
switch (typeName) {
|
|
case 'Server':
|
|
relatedObjects = await cmdbService.getReferencingObjects<Server>(id, 'Server');
|
|
break;
|
|
case 'Flows': {
|
|
// Flows reference ApplicationComponents via Source and Target attributes
|
|
// We need to find Flows where this ApplicationComponent is the target of the reference
|
|
relatedObjects = await cmdbService.getReferencingObjects<Flows>(id, 'Flows');
|
|
break;
|
|
}
|
|
case 'Certificate':
|
|
relatedObjects = await cmdbService.getReferencingObjects<Certificate>(id, 'Certificate');
|
|
break;
|
|
case 'Domain':
|
|
relatedObjects = await cmdbService.getReferencingObjects<Domain>(id, 'Domain');
|
|
break;
|
|
case 'AzureSubscription':
|
|
relatedObjects = await cmdbService.getReferencingObjects<AzureSubscription>(id, 'AzureSubscription');
|
|
break;
|
|
default:
|
|
relatedObjects = [];
|
|
}
|
|
|
|
// Get requested attributes from query string
|
|
const attributesParam = getQueryString(req, 'attributes');
|
|
const requestedAttrs = attributesParam
|
|
? attributesParam.split(',').map(a => a.trim())
|
|
: [];
|
|
|
|
// Format response - must match RelatedObjectsResponse type expected by frontend
|
|
const objects = relatedObjects.map(obj => {
|
|
// Extract attributes from the object
|
|
const attributes: Record<string, string | null> = {};
|
|
const objData = obj as unknown as Record<string, unknown>;
|
|
|
|
// If specific attributes are requested, extract those
|
|
if (requestedAttrs.length > 0) {
|
|
for (const attrName of requestedAttrs) {
|
|
// Convert attribute name to camelCase field name
|
|
const fieldName = attrName.charAt(0).toLowerCase() + attrName.slice(1).replace(/\s+/g, '');
|
|
const value = objData[fieldName] ?? objData[attrName.toLowerCase()] ?? objData[attrName];
|
|
|
|
if (value === null || value === undefined) {
|
|
attributes[attrName] = null;
|
|
} else if (typeof value === 'object' && value !== null) {
|
|
// ObjectReference - extract label
|
|
const ref = value as { label?: string; name?: string; displayValue?: string };
|
|
attributes[attrName] = ref.label || ref.name || ref.displayValue || null;
|
|
} else {
|
|
attributes[attrName] = String(value);
|
|
}
|
|
}
|
|
} else {
|
|
// No specific attributes requested - include common ones
|
|
if ('status' in objData) {
|
|
const status = objData.status;
|
|
if (typeof status === 'object' && status !== null) {
|
|
attributes['Status'] = (status as { label?: string }).label || String(status);
|
|
} else if (status) {
|
|
attributes['Status'] = String(status);
|
|
}
|
|
}
|
|
if ('state' in objData) {
|
|
attributes['State'] = objData.state ? String(objData.state) : null;
|
|
}
|
|
}
|
|
|
|
return {
|
|
id: obj.id,
|
|
key: obj.objectKey,
|
|
label: obj.label,
|
|
name: obj.label,
|
|
objectType: obj._objectType,
|
|
attributes,
|
|
};
|
|
});
|
|
|
|
res.json({ objects, total: objects.length });
|
|
} catch (error) {
|
|
logger.error(`Failed to get related ${req.params.objectType} objects`, error);
|
|
res.status(500).json({ error: `Failed to get related objects` });
|
|
}
|
|
});
|
|
|
|
export default router;
|