/**
* Formats API responses with consistent structure and metadata
*/
export default class ResponseFormatter {
constructor(options = {}) {
this.config = {
includeMetadata: options.includeMetadata !== false,
includeTiming: options.includeTiming !== false,
includeDebugInfo: options.includeDebugInfo || false,
compressResponses: options.compressResponses || false,
standardizeErrors: options.standardizeErrors !== false,
apiVersion: options.apiVersion || '1.0.0',
...options
};
this.initializeFormatters();
this.initializeTemplates();
}
/**
* Initialize response formatters for different content types
*/
initializeFormatters() {
this.formatters = {
success: this.formatSuccessResponse.bind(this),
error: this.formatErrorResponse.bind(this),
navigation: this.formatNavigationResponse.bind(this),
preview: this.formatPreviewResponse.bind(this),
options: this.formatOptionsResponse.bind(this),
schema: this.formatSchemaResponse.bind(this),
health: this.formatHealthResponse.bind(this),
metrics: this.formatMetricsResponse.bind(this)
};
}
/**
* Initialize response templates
*/
initializeTemplates() {
this.templates = {
base: {
success: true,
data: null,
metadata: {},
links: {},
version: this.config.apiVersion
},
error: {
success: false,
error: {
code: '',
message: '',
details: null,
timestamp: null
},
requestId: null,
version: this.config.apiVersion
},
pagination: {
page: 1,
limit: 50,
total: 0,
hasNext: false,
hasPrev: false
}
};
}
/**
* Main formatting method - routes to appropriate formatter
* @param {Object} data - Response data to format
* @param {Object} context - Request context and metadata
* @returns {Object} Formatted response
*/
async format(data, context = {}) {
try {
// Determine response type
const responseType = this.determineResponseType(data, context);
// Get appropriate formatter
const formatter = this.formatters[responseType] || this.formatters.success;
// Format the response
const formattedResponse = await formatter(data, context);
// Add common metadata
this.addCommonMetadata(formattedResponse, context);
// Apply post-processing
return this.postProcessResponse(formattedResponse, context);
} catch (error) {
// Fallback error formatting
return this.formatErrorResponse({
message: `Response formatting failed: ${error.message}`,
code: 'FORMATTING_ERROR'
}, context);
}
}
/**
* Determine response type based on data structure
*/
determineResponseType(data, context) {
if (data.success === false || data.error) {
return 'error';
}
if (data.navigation && data.content) {
return 'navigation';
}
if (data.preview === true) {
return 'preview';
}
if (data.options) {
return 'options';
}
if (data.schema) {
return 'schema';
}
if (data.status && data.checks) {
return 'health';
}
if (data.metrics) {
return 'metrics';
}
return 'success';
}
/**
* Format success responses
*/
async formatSuccessResponse(data, context) {
const response = {
...this.templates.base,
data: this.sanitizeData(data),
requestId: context.requestId
};
// Add timing information
if (this.config.includeTiming && context.processingTime) {
response.metadata.timing = {
processingTime: context.processingTime,
timestamp: new Date().toISOString()
};
}
// Add pagination if applicable
if (this.isPaginatedResponse(data)) {
response.pagination = this.buildPaginationInfo(data, context);
}
return response;
}
/**
* Format error responses
*/
async formatErrorResponse(error, context) {
const response = {
...this.templates.error,
error: {
code: error.code || 'UNKNOWN_ERROR',
message: error.message || 'An unknown error occurred',
details: this.config.includeDebugInfo ? error.details : null,
timestamp: new Date().toISOString()
},
requestId: context.requestId
};
// Add error context if available
if (error.statusCode) {
response.error.statusCode = error.statusCode;
}
if (error.field) {
response.error.field = error.field;
}
// Add rate limit information for rate limit errors
if (error.rateLimitInfo) {
response.error.rateLimit = {
remaining: error.rateLimitInfo.remaining,
resetTime: error.rateLimitInfo.resetTime
};
}
return response;
}
/**
* Format navigation responses
*/
async formatNavigationResponse(data, context) {
const response = {
success: true,
requestId: data.requestId,
navigation: {
parameters: this.formatNavigationParameters(data.navigation),
results: {
format: this.detectContentFormat(data.content),
size: this.calculateContentSize(data.content),
chunks: this.countChunks(data.content)
}
},
content: this.formatNavigationContent(data.content),
metadata: this.formatNavigationMetadata(data.metadata),
version: this.config.apiVersion
};
// Add diagnostics in debug mode
if (this.config.includeDebugInfo && data.diagnostics) {
response.diagnostics = data.diagnostics;
}
// Add performance information
if (data.metadata?.pipeline) {
response.performance = {
totalTime: data.metadata.pipeline.totalTime,
stages: {
selection: data.metadata.pipeline.selectionTime,
projection: data.metadata.pipeline.projectionTime,
transformation: data.metadata.pipeline.transformTime
}
};
}
return response;
}
/**
* Format preview responses
*/
async formatPreviewResponse(data, context) {
const response = {
success: true,
requestId: data.requestId,
preview: {
navigation: this.formatNavigationParameters(data.navigation),
summary: data.summary,
sample: {
corpuscles: this.formatSampleCorpuscles(data.corpuscles),
count: data.corpuscles?.length || 0
}
},
recommendations: this.generateRecommendations(data),
version: this.config.apiVersion
};
return response;
}
/**
* Format options responses
*/
async formatOptionsResponse(data, context) {
const response = {
success: true,
requestId: data.requestId,
options: {
navigation: {
zoom: {
levels: data.options.zoomLevels,
descriptions: this.getZoomDescriptions()
},
tilt: {
representations: data.options.tiltRepresentations,
descriptions: this.getTiltDescriptions()
},
pan: {
filters: ['topic', 'entity', 'temporal', 'geographic'],
descriptions: this.getPanDescriptions()
}
},
output: {
formats: data.options.outputFormats,
encodings: data.options.encodingStrategies,
tokenLimits: data.options.maxTokenLimits
},
technical: {
tokenizers: data.options.supportedTokenizers,
models: Object.keys(data.options.maxTokenLimits)
}
},
examples: this.generateUsageExamples(),
version: this.config.apiVersion
};
return response;
}
/**
* Format schema responses
*/
async formatSchemaResponse(data, context) {
const response = {
success: true,
requestId: data.requestId,
schema: {
parameters: data.schema,
validation: {
required: this.extractRequiredFields(data.schema),
optional: this.extractOptionalFields(data.schema)
},
examples: data.examples,
documentation: data.documentation
},
version: this.config.apiVersion
};
return response;
}
/**
* Format health check responses
*/
async formatHealthResponse(data, context) {
const response = {
success: true,
requestId: data.requestId,
health: {
status: data.status,
checks: this.formatHealthChecks(data.checks),
system: {
activeRequests: data.activeRequests,
uptime: data.uptime,
timestamp: new Date().toISOString()
}
},
version: this.config.apiVersion
};
return response;
}
/**
* Format metrics responses
*/
async formatMetricsResponse(data, context) {
const response = {
success: true,
requestId: data.requestId,
metrics: {
api: this.formatApiMetrics(data.metrics.requests),
components: this.formatComponentMetrics(data.metrics.components),
system: this.formatSystemMetrics(data.metrics.system),
timestamp: new Date().toISOString()
},
version: this.config.apiVersion
};
return response;
}
/**
* Helper methods for data formatting
*/
formatNavigationParameters(navigation) {
return {
zoom: navigation.zoom?.level || navigation.zoom,
pan: this.formatPanParameters(navigation.pan),
tilt: navigation.tilt?.representation || navigation.tilt,
transform: this.formatTransformParameters(navigation.transform)
};
}
formatPanParameters(pan) {
if (!pan || Object.keys(pan).length === 0) {
return { applied: false };
}
const formatted = { applied: true, filters: {} };
if (pan.topic) {
formatted.filters.topic = {
value: pan.topic.value || pan.topic,
pattern: pan.topic.pattern || 'exact'
};
}
if (pan.entity) {
formatted.filters.entity = {
count: pan.entity.count || (Array.isArray(pan.entity) ? pan.entity.length : 1),
type: pan.entity.type || 'multiple'
};
}
if (pan.temporal) {
formatted.filters.temporal = {
hasRange: !!(pan.temporal.start && pan.temporal.end),
duration: pan.temporal.durationDays
};
}
if (pan.geographic) {
formatted.filters.geographic = {
type: pan.geographic.bbox ? 'bbox' : pan.geographic.center ? 'center' : 'unknown',
area: this.calculateGeographicArea(pan.geographic)
};
}
return formatted;
}
formatTransformParameters(transform) {
if (!transform) return { default: true };
return {
maxTokens: transform.maxTokens,
format: transform.format,
tokenizer: transform.tokenizer,
strategy: transform.chunkStrategy,
metadata: transform.includeMetadata
};
}
formatNavigationContent(content) {
if (!content) return null;
if (Array.isArray(content)) {
// Chunked content
return {
type: 'chunked',
chunks: content.map((chunk, index) => ({
id: chunk.chunkId || `chunk_${index}`,
size: this.calculateChunkSize(chunk),
preview: this.generateContentPreview(chunk)
})),
totalChunks: content.length
};
} else {
// Single content
return {
type: 'single',
size: this.calculateContentSize(content),
format: this.detectContentFormat(content),
preview: this.generateContentPreview(content)
};
}
}
formatNavigationMetadata(metadata) {
if (!metadata) return null;
const formatted = {
processing: {
totalTime: metadata.pipeline?.totalTime,
stages: metadata.pipeline ? Object.keys(metadata.pipeline).length - 1 : 0
}
};
if (metadata.selection) {
formatted.selection = {
corpuscleCount: metadata.selection.resultCount,
fromCache: metadata.selection.fromCache,
complexity: metadata.selection.complexity
};
}
if (metadata.projection) {
formatted.projection = {
type: metadata.projection.representation,
processingTime: metadata.projection.processingTime
};
}
if (metadata.transformation) {
formatted.transformation = {
format: metadata.transformation.output?.format,
chunked: metadata.transformation.output?.chunked,
hasMetadata: metadata.transformation.output?.hasMetadata
};
}
return formatted;
}
formatSampleCorpuscles(corpuscles) {
if (!corpuscles || corpuscles.length === 0) return [];
return corpuscles.map(corpuscle => ({
uri: corpuscle.uri,
type: corpuscle.type,
score: corpuscle.score,
preview: this.generateCorpusclePreview(corpuscle)
}));
}
formatHealthChecks(checks) {
const formatted = {};
for (const [component, check] of Object.entries(checks)) {
if (check === null) continue;
formatted[component] = {
status: check.status,
...(check.metrics && { metrics: check.metrics }),
...(check.error && { error: check.error }),
...(check.issues && { issues: check.issues })
};
}
return formatted;
}
formatApiMetrics(requestMetrics) {
return {
requests: {
total: requestMetrics.totalRequests,
successful: requestMetrics.successfulRequests,
failed: requestMetrics.failedRequests,
successRate: requestMetrics.totalRequests > 0 ?
(requestMetrics.successfulRequests / requestMetrics.totalRequests * 100).toFixed(2) + '%' : '0%'
},
performance: {
avgResponseTime: Math.round(requestMetrics.avgResponseTime),
unit: 'ms'
},
endpoints: this.formatEndpointMetrics(requestMetrics.requestsByEndpoint)
};
}
formatEndpointMetrics(endpointMap) {
const formatted = {};
for (const [endpoint, stats] of endpointMap.entries()) {
formatted[endpoint] = {
total: stats.total,
successful: stats.successful,
successRate: stats.total > 0 ?
(stats.successful / stats.total * 100).toFixed(2) + '%' : '0%'
};
}
return formatted;
}
formatComponentMetrics(componentMetrics) {
const formatted = {};
for (const [component, metrics] of Object.entries(componentMetrics || {})) {
if (metrics) {
formatted[component] = {
...metrics,
// Add computed fields if available
...(metrics.cacheHitRate !== undefined && {
cachePerformance: (metrics.cacheHitRate * 100).toFixed(1) + '%'
})
};
}
}
return formatted;
}
formatSystemMetrics(systemMetrics) {
return {
activeRequests: systemMetrics.activeRequests,
memory: {
rss: Math.round(systemMetrics.memoryUsage.rss / 1024 / 1024) + ' MB',
heapUsed: Math.round(systemMetrics.memoryUsage.heapUsed / 1024 / 1024) + ' MB',
heapTotal: Math.round(systemMetrics.memoryUsage.heapTotal / 1024 / 1024) + ' MB'
},
uptime: {
seconds: Math.round(systemMetrics.uptime),
formatted: this.formatUptime(systemMetrics.uptime)
}
};
}
/**
* Utility methods
*/
sanitizeData(data) {
// Remove sensitive information and circular references
return this.removeCircularReferences(data);
}
removeCircularReferences(obj, seen = new WeakSet()) {
if (obj === null || typeof obj !== 'object') return obj;
if (seen.has(obj)) return '[Circular Reference]';
seen.add(obj);
if (Array.isArray(obj)) {
return obj.map(item => this.removeCircularReferences(item, seen));
}
const cleaned = {};
for (const [key, value] of Object.entries(obj)) {
// Skip sensitive fields
if (this.isSensitiveField(key)) continue;
cleaned[key] = this.removeCircularReferences(value, seen);
}
return cleaned;
}
isSensitiveField(fieldName) {
const sensitiveFields = ['password', 'secret', 'token', 'key', 'auth'];
return sensitiveFields.some(field =>
fieldName.toLowerCase().includes(field)
);
}
detectContentFormat(content) {
if (!content) return 'unknown';
if (typeof content === 'string') {
if (content.trim().startsWith('{') || content.trim().startsWith('[')) {
return 'json';
}
if (content.includes('# ') || content.includes('## ')) {
return 'markdown';
}
return 'text';
}
if (typeof content === 'object') {
return 'structured';
}
return 'unknown';
}
calculateContentSize(content) {
if (!content) return 0;
if (typeof content === 'string') {
return content.length;
}
return JSON.stringify(content).length;
}
calculateChunkSize(chunk) {
if (!chunk) return 0;
const content = chunk.encoded?.content || chunk.formatted?.content || chunk.content || chunk;
return this.calculateContentSize(content);
}
countChunks(content) {
if (Array.isArray(content)) {
return content.length;
}
return 1;
}
generateContentPreview(content, maxLength = 200) {
let text = '';
if (typeof content === 'string') {
text = content;
} else if (content?.encoded?.content) {
text = content.encoded.content;
} else if (content?.formatted?.content) {
text = content.formatted.content;
} else if (content?.content) {
text = content.content;
} else {
text = JSON.stringify(content);
}
return text.length > maxLength ?
text.substring(0, maxLength) + '...' : text;
}
generateCorpusclePreview(corpuscle) {
const content = corpuscle.content || {};
const preview = content.label || content.prefLabel || content.text || content.content;
if (typeof preview === 'string') {
return preview.length > 100 ? preview.substring(0, 100) + '...' : preview;
}
return corpuscle.uri || 'Unknown';
}
calculateGeographicArea(geographic) {
if (geographic.bbox) {
const { minLon, minLat, maxLon, maxLat } = geographic.bbox;
return (maxLon - minLon) * (maxLat - minLat);
}
if (geographic.radius) {
return Math.PI * geographic.radius * geographic.radius;
}
return 0;
}
formatUptime(seconds) {
const days = Math.floor(seconds / 86400);
const hours = Math.floor((seconds % 86400) / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
if (days > 0) {
return `${days}d ${hours}h ${minutes}m`;
} else if (hours > 0) {
return `${hours}h ${minutes}m`;
} else {
return `${minutes}m`;
}
}
isPaginatedResponse(data) {
return data.page !== undefined || data.limit !== undefined || data.total !== undefined;
}
buildPaginationInfo(data, context) {
return {
page: data.page || 1,
limit: data.limit || 50,
total: data.total || 0,
hasNext: data.hasNext || false,
hasPrev: data.hasPrev || false
};
}
addCommonMetadata(response, context) {
if (!this.config.includeMetadata) return;
response.metadata = response.metadata || {};
if (context.endpoint) {
response.metadata.endpoint = context.endpoint;
}
if (this.config.includeTiming && context.processingTime) {
response.metadata.timing = {
processingTime: context.processingTime,
timestamp: new Date().toISOString()
};
}
}
postProcessResponse(response, context) {
// Add HATEOAS links if applicable
if (context.addLinks) {
response.links = this.generateHateoasLinks(response, context);
}
// Compress response if enabled
if (this.config.compressResponses) {
response = this.compressResponse(response);
}
return response;
}
generateHateoasLinks(response, context) {
const links = {};
if (response.navigation) {
links.self = `/api/navigate`;
links.preview = `/api/navigate/preview`;
links.options = `/api/navigate/options`;
links.schema = `/api/navigate/schema`;
}
return links;
}
compressResponse(response) {
// Simple compression - remove empty fields
return this.removeEmptyFields(response);
}
removeEmptyFields(obj) {
if (Array.isArray(obj)) {
return obj.map(item => this.removeEmptyFields(item)).filter(item => item !== null);
}
if (obj && typeof obj === 'object') {
const cleaned = {};
for (const [key, value] of Object.entries(obj)) {
const cleanedValue = this.removeEmptyFields(value);
if (cleanedValue !== null && cleanedValue !== undefined && cleanedValue !== '') {
cleaned[key] = cleanedValue;
}
}
return Object.keys(cleaned).length > 0 ? cleaned : null;
}
return obj;
}
/**
* Configuration and documentation helpers
*/
getZoomDescriptions() {
return {
entity: 'Individual entities and concepts',
unit: 'Semantic units and meaningful segments',
text: 'Full text elements and documents',
community: 'Thematic communities and clusters',
corpus: 'Entire corpus overview and metadata'
};
}
getTiltDescriptions() {
return {
embedding: 'Vector embeddings and similarity analysis',
keywords: 'Keyword extraction and term analysis',
graph: 'Graph structure and relationship analysis',
temporal: 'Temporal sequences and chronological analysis'
};
}
getPanDescriptions() {
return {
topic: 'Filter by subject area or theme',
entity: 'Filter by specific entities or concepts',
temporal: 'Filter by time period or date range',
geographic: 'Filter by location or geographic area'
};
}
generateUsageExamples() {
return {
basic: {
description: 'Basic navigation request',
request: {
zoom: 'unit',
tilt: 'keywords'
}
},
filtered: {
description: 'Navigation with topic filter',
request: {
zoom: 'entity',
pan: { topic: 'machine-learning' },
tilt: 'embedding'
}
},
advanced: {
description: 'Advanced navigation with multiple filters',
request: {
zoom: 'text',
pan: {
topic: 'artificial-intelligence',
temporal: {
start: '2024-01-01',
end: '2024-12-31'
}
},
tilt: 'graph',
transform: {
maxTokens: 8000,
format: 'markdown'
}
}
}
};
}
extractRequiredFields(schema) {
const required = [];
for (const [field, config] of Object.entries(schema)) {
if (config.required) {
required.push(field);
}
}
return required;
}
extractOptionalFields(schema) {
const optional = [];
for (const [field, config] of Object.entries(schema)) {
if (!config.required) {
optional.push(field);
}
}
return optional;
}
generateRecommendations(previewData) {
const recommendations = [];
if (previewData.summary?.estimatedTokens > 8000) {
recommendations.push({
type: 'token_optimization',
message: 'Consider using chunking for large content',
suggestion: 'Add "chunkStrategy": "semantic" to transform parameters'
});
}
if (previewData.summary?.corpuscleCount < 5) {
recommendations.push({
type: 'scope_expansion',
message: 'Few results found, consider broadening filters',
suggestion: 'Try using a higher zoom level or removing some pan filters'
});
}
if (previewData.summary?.complexity > 8) {
recommendations.push({
type: 'complexity_reduction',
message: 'High complexity detected',
suggestion: 'Consider simplifying pan filters for better performance'
});
}
return recommendations;
}
/**
* Configuration methods
*/
updateConfig(newConfig) {
Object.assign(this.config, newConfig);
}
getFormatterInfo() {
return {
config: this.config,
formatters: Object.keys(this.formatters),
templates: Object.keys(this.templates)
};
}
}