adding data-services

This commit is contained in:
Bojan Kucera 2025-06-03 07:42:48 -04:00
parent e3bfd05b90
commit 405b818c86
139 changed files with 55943 additions and 416 deletions

View file

@ -0,0 +1,360 @@
import { Context } from 'hono';
import { Logger } from '@stock-bot/utils';
import { DataCatalogService } from '../services/DataCatalogService';
import {
CreateDataAssetRequest,
UpdateDataAssetRequest,
DataAssetType,
DataClassification
} from '../types/DataCatalog';
export class DataCatalogController {
constructor(
private dataCatalogService: DataCatalogService,
private logger: Logger
) {}
async createAsset(c: Context) {
try {
const request: CreateDataAssetRequest = await c.req.json();
// Validate required fields
if (!request.name || !request.type || !request.description || !request.owner) {
return c.json({ error: 'Missing required fields: name, type, description, owner' }, 400);
}
const asset = await this.dataCatalogService.createAsset(request);
this.logger.info('Asset created via API', {
assetId: asset.id,
name: asset.name,
type: asset.type
});
return c.json(asset, 201);
} catch (error) {
this.logger.error('Failed to create asset', { error });
return c.json({ error: 'Internal server error' }, 500);
}
}
async getAsset(c: Context) {
try {
const assetId = c.req.param('id');
if (!assetId) {
return c.json({ error: 'Asset ID is required' }, 400);
}
const asset = await this.dataCatalogService.getAsset(assetId);
if (!asset) {
return c.json({ error: 'Asset not found' }, 404);
}
return c.json(asset);
} catch (error) {
this.logger.error('Failed to get asset', { error });
return c.json({ error: 'Internal server error' }, 500);
}
}
async updateAsset(c: Context) {
try {
const assetId = c.req.param('id');
const updates: UpdateDataAssetRequest = await c.req.json();
if (!assetId) {
return c.json({ error: 'Asset ID is required' }, 400);
}
const asset = await this.dataCatalogService.updateAsset(assetId, updates);
if (!asset) {
return c.json({ error: 'Asset not found' }, 404);
}
this.logger.info('Asset updated via API', {
assetId,
changes: Object.keys(updates)
});
return c.json(asset);
} catch (error) {
this.logger.error('Failed to update asset', { error });
return c.json({ error: 'Internal server error' }, 500);
}
}
async deleteAsset(c: Context) {
try {
const assetId = c.req.param('id');
if (!assetId) {
return c.json({ error: 'Asset ID is required' }, 400);
}
await this.dataCatalogService.deleteAsset(assetId);
this.logger.info('Asset deleted via API', { assetId });
return c.json({ message: 'Asset deleted successfully' });
} catch (error) {
this.logger.error('Failed to delete asset', { error });
if (error instanceof Error && error.message.includes('not found')) {
return c.json({ error: 'Asset not found' }, 404);
}
return c.json({ error: 'Internal server error' }, 500);
}
}
async listAssets(c: Context) {
try {
const query = c.req.query();
const filters: Record<string, any> = {};
// Parse query parameters
if (query.type) filters.type = query.type;
if (query.owner) filters.owner = query.owner;
if (query.classification) filters.classification = query.classification;
if (query.tags) {
filters.tags = Array.isArray(query.tags) ? query.tags : [query.tags];
}
const assets = await this.dataCatalogService.listAssets(filters);
return c.json({
assets,
total: assets.length,
filters: filters
});
} catch (error) {
this.logger.error('Failed to list assets', { error });
return c.json({ error: 'Internal server error' }, 500);
}
}
async searchAssets(c: Context) {
try {
const query = c.req.query('q');
const queryParams = c.req.query();
if (!query) {
return c.json({ error: 'Search query is required' }, 400);
}
const filters: Record<string, any> = {};
if (queryParams.type) filters.type = queryParams.type;
if (queryParams.owner) filters.owner = queryParams.owner;
if (queryParams.classification) filters.classification = queryParams.classification;
const assets = await this.dataCatalogService.searchAssets(query, filters);
this.logger.info('Asset search performed', {
query,
filters,
resultCount: assets.length
});
return c.json({
assets,
total: assets.length,
query,
filters
});
} catch (error) {
this.logger.error('Failed to search assets', { error });
return c.json({ error: 'Internal server error' }, 500);
}
}
async getAssetsByOwner(c: Context) {
try {
const owner = c.req.param('owner');
if (!owner) {
return c.json({ error: 'Owner is required' }, 400);
}
const assets = await this.dataCatalogService.getAssetsByOwner(owner);
return c.json({
assets,
total: assets.length,
owner
});
} catch (error) {
this.logger.error('Failed to get assets by owner', { error });
return c.json({ error: 'Internal server error' }, 500);
}
}
async getAssetsByType(c: Context) {
try {
const type = c.req.param('type') as DataAssetType;
if (!type) {
return c.json({ error: 'Asset type is required' }, 400);
}
if (!Object.values(DataAssetType).includes(type)) {
return c.json({ error: 'Invalid asset type' }, 400);
}
const assets = await this.dataCatalogService.getAssetsByType(type);
return c.json({
assets,
total: assets.length,
type
});
} catch (error) {
this.logger.error('Failed to get assets by type', { error });
return c.json({ error: 'Internal server error' }, 500);
}
}
async getAssetsByClassification(c: Context) {
try {
const classification = c.req.param('classification') as DataClassification;
if (!classification) {
return c.json({ error: 'Classification is required' }, 400);
}
if (!Object.values(DataClassification).includes(classification)) {
return c.json({ error: 'Invalid classification' }, 400);
}
const assets = await this.dataCatalogService.getAssetsByClassification(classification);
return c.json({
assets,
total: assets.length,
classification
});
} catch (error) {
this.logger.error('Failed to get assets by classification', { error });
return c.json({ error: 'Internal server error' }, 500);
}
}
async getAssetsByTags(c: Context) {
try {
const tagsParam = c.req.query('tags');
if (!tagsParam) {
return c.json({ error: 'Tags parameter is required' }, 400);
}
const tags = Array.isArray(tagsParam) ? tagsParam : [tagsParam];
const assets = await this.dataCatalogService.getAssetsByTags(tags);
return c.json({
assets,
total: assets.length,
tags
});
} catch (error) {
this.logger.error('Failed to get assets by tags', { error });
return c.json({ error: 'Internal server error' }, 500);
}
}
async getAssetMetrics(c: Context) {
try {
const assetId = c.req.param('id');
if (!assetId) {
return c.json({ error: 'Asset ID is required' }, 400);
}
const asset = await this.dataCatalogService.getAsset(assetId);
if (!asset) {
return c.json({ error: 'Asset not found' }, 404);
}
const metrics = {
id: asset.id,
name: asset.name,
type: asset.type,
classification: asset.classification,
usage: {
accessCount: asset.usage.accessCount,
uniqueUsers: asset.usage.uniqueUsers,
lastAccessed: asset.usage.lastAccessed,
usageTrend: asset.usage.usageTrend
},
quality: {
overallScore: asset.quality.overallScore,
lastAssessment: asset.quality.lastAssessment,
issueCount: asset.quality.issues.filter(issue => !issue.resolved).length
},
governance: {
policiesApplied: asset.governance.policies.length,
complianceStatus: asset.governance.compliance.every(c => c.status === 'passed') ? 'compliant' : 'non-compliant',
auditEntries: asset.governance.audit.length
},
lineage: {
upstreamCount: asset.lineage.upstreamAssets.length,
downstreamCount: asset.lineage.downstreamAssets.length
}
};
return c.json(metrics);
} catch (error) {
this.logger.error('Failed to get asset metrics', { error });
return c.json({ error: 'Internal server error' }, 500);
}
}
async getCatalogStatistics(c: Context) {
try {
const allAssets = await this.dataCatalogService.listAssets();
const statistics = {
totalAssets: allAssets.length,
assetsByType: this.groupByProperty(allAssets, 'type'),
assetsByClassification: this.groupByProperty(allAssets, 'classification'),
assetsByOwner: this.groupByProperty(allAssets, 'owner'),
recentAssets: allAssets
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())
.slice(0, 10)
.map(asset => ({
id: asset.id,
name: asset.name,
type: asset.type,
owner: asset.owner,
createdAt: asset.createdAt
})),
mostAccessed: allAssets
.sort((a, b) => b.usage.accessCount - a.usage.accessCount)
.slice(0, 10)
.map(asset => ({
id: asset.id,
name: asset.name,
type: asset.type,
accessCount: asset.usage.accessCount,
lastAccessed: asset.usage.lastAccessed
}))
};
return c.json(statistics);
} catch (error) {
this.logger.error('Failed to get catalog statistics', { error });
return c.json({ error: 'Internal server error' }, 500);
}
}
// Helper method to group assets by property
private groupByProperty(assets: any[], property: string): Record<string, number> {
return assets.reduce((acc, asset) => {
const value = asset[property];
acc[value] = (acc[value] || 0) + 1;
return acc;
}, {});
}
}

View file

@ -0,0 +1,414 @@
import { Hono } from 'hono';
import { DataGovernanceService } from '../services/DataGovernanceService';
import {
GovernancePolicy,
ComplianceCheck,
AccessRequest,
DataSubjectRequest,
AuditLog
} from '../types/DataCatalog';
export class GovernanceController {
private app: Hono;
private governanceService: DataGovernanceService;
constructor() {
this.app = new Hono();
this.governanceService = new DataGovernanceService();
this.setupRoutes();
}
private setupRoutes() {
// Create governance policy
this.app.post('/policies', async (c) => {
try {
const policy: Omit<GovernancePolicy, 'id' | 'createdAt' | 'updatedAt'> = await c.req.json();
const createdPolicy = await this.governanceService.createPolicy(policy);
return c.json({
success: true,
data: createdPolicy
});
} catch (error) {
console.error('Error creating governance policy:', error);
return c.json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
}, 500);
}
});
// Get governance policies
this.app.get('/policies', async (c) => {
try {
const type = c.req.query('type');
const category = c.req.query('category');
const active = c.req.query('active') === 'true';
const filters: any = {};
if (type) filters.type = type;
if (category) filters.category = category;
if (active !== undefined) filters.active = active;
const policies = await this.governanceService.getPolicies(filters);
return c.json({
success: true,
data: policies
});
} catch (error) {
console.error('Error getting governance policies:', error);
return c.json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
}, 500);
}
});
// Update governance policy
this.app.put('/policies/:policyId', async (c) => {
try {
const policyId = c.req.param('policyId');
const updates: Partial<GovernancePolicy> = await c.req.json();
const updatedPolicy = await this.governanceService.updatePolicy(policyId, updates);
return c.json({
success: true,
data: updatedPolicy
});
} catch (error) {
console.error('Error updating governance policy:', error);
return c.json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
}, 500);
}
});
// Delete governance policy
this.app.delete('/policies/:policyId', async (c) => {
try {
const policyId = c.req.param('policyId');
await this.governanceService.deletePolicy(policyId);
return c.json({
success: true,
message: 'Governance policy deleted successfully'
});
} catch (error) {
console.error('Error deleting governance policy:', error);
return c.json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
}, 500);
}
});
// Apply policy to asset
this.app.post('/policies/:policyId/apply/:assetId', async (c) => {
try {
const policyId = c.req.param('policyId');
const assetId = c.req.param('assetId');
await this.governanceService.applyPolicy(policyId, assetId);
return c.json({
success: true,
message: 'Policy applied successfully'
});
} catch (error) {
console.error('Error applying policy:', error);
return c.json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
}, 500);
}
});
// Check compliance for asset
this.app.post('/compliance/check', async (c) => {
try {
const request: { assetId: string; policyIds?: string[] } = await c.req.json();
const complianceResult = await this.governanceService.checkCompliance(
request.assetId,
request.policyIds
);
return c.json({
success: true,
data: complianceResult
});
} catch (error) {
console.error('Error checking compliance:', error);
return c.json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
}, 500);
}
});
// Get compliance violations
this.app.get('/compliance/violations', async (c) => {
try {
const assetId = c.req.query('assetId');
const severity = c.req.query('severity');
const status = c.req.query('status');
const limit = c.req.query('limit') ? parseInt(c.req.query('limit')!) : 100;
const offset = c.req.query('offset') ? parseInt(c.req.query('offset')!) : 0;
const filters: any = {};
if (assetId) filters.assetId = assetId;
if (severity) filters.severity = severity;
if (status) filters.status = status;
const violations = await this.governanceService.getComplianceViolations(
filters,
{ limit, offset }
);
return c.json({
success: true,
data: violations
});
} catch (error) {
console.error('Error getting compliance violations:', error);
return c.json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
}, 500);
}
});
// Request access to asset
this.app.post('/access/request', async (c) => {
try {
const request: Omit<AccessRequest, 'id' | 'requestedAt' | 'status'> = await c.req.json();
const accessRequest = await this.governanceService.requestAccess(request);
return c.json({
success: true,
data: accessRequest
});
} catch (error) {
console.error('Error requesting access:', error);
return c.json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
}, 500);
}
});
// Approve/deny access request
this.app.patch('/access/:requestId', async (c) => {
try {
const requestId = c.req.param('requestId');
const { action, reviewedBy, reviewComments } = await c.req.json();
const updatedRequest = await this.governanceService.reviewAccessRequest(
requestId,
action,
reviewedBy,
reviewComments
);
return c.json({
success: true,
data: updatedRequest
});
} catch (error) {
console.error('Error reviewing access request:', error);
return c.json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
}, 500);
}
});
// Check access authorization
this.app.post('/access/check', async (c) => {
try {
const { userId, assetId, action } = await c.req.json();
const authorized = await this.governanceService.checkAccess(userId, assetId, action);
return c.json({
success: true,
data: {
userId,
assetId,
action,
authorized
}
});
} catch (error) {
console.error('Error checking access:', error);
return c.json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
}, 500);
}
});
// Handle data subject request (GDPR)
this.app.post('/privacy/subject-request', async (c) => {
try {
const request: Omit<DataSubjectRequest, 'id' | 'submittedAt' | 'status'> = await c.req.json();
const subjectRequest = await this.governanceService.handleDataSubjectRequest(request);
return c.json({
success: true,
data: subjectRequest
});
} catch (error) {
console.error('Error handling data subject request:', error);
return c.json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
}, 500);
}
});
// Anonymize asset data
this.app.post('/privacy/anonymize/:assetId', async (c) => {
try {
const assetId = c.req.param('assetId');
const { fields, method, requestedBy } = await c.req.json();
const result = await this.governanceService.anonymizeData(
assetId,
fields,
method,
requestedBy
);
return c.json({
success: true,
data: result
});
} catch (error) {
console.error('Error anonymizing data:', error);
return c.json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
}, 500);
}
});
// Get audit logs
this.app.get('/audit/logs', async (c) => {
try {
const assetId = c.req.query('assetId');
const userId = c.req.query('userId');
const action = c.req.query('action');
const startDate = c.req.query('startDate');
const endDate = c.req.query('endDate');
const limit = c.req.query('limit') ? parseInt(c.req.query('limit')!) : 100;
const offset = c.req.query('offset') ? parseInt(c.req.query('offset')!) : 0;
const filters: any = {};
if (assetId) filters.assetId = assetId;
if (userId) filters.userId = userId;
if (action) filters.action = action;
if (startDate) filters.startDate = new Date(startDate);
if (endDate) filters.endDate = new Date(endDate);
const logs = await this.governanceService.getAuditLogs(filters, { limit, offset });
return c.json({
success: true,
data: logs
});
} catch (error) {
console.error('Error getting audit logs:', error);
return c.json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
}, 500);
}
});
// Log access event
this.app.post('/audit/log', async (c) => {
try {
const logEntry: Omit<AuditLog, 'id' | 'timestamp'> = await c.req.json();
const logged = await this.governanceService.logAccess(logEntry);
return c.json({
success: true,
data: logged
});
} catch (error) {
console.error('Error logging access event:', error);
return c.json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
}, 500);
}
});
// Get retention policies
this.app.get('/retention/policies', async (c) => {
try {
const assetType = c.req.query('assetType');
const policies = await this.governanceService.getRetentionPolicies(assetType);
return c.json({
success: true,
data: policies
});
} catch (error) {
console.error('Error getting retention policies:', error);
return c.json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
}, 500);
}
});
// Apply retention policy
this.app.post('/retention/apply', async (c) => {
try {
const { assetId, policyId, requestedBy } = await c.req.json();
const result = await this.governanceService.applyRetentionPolicy(
assetId,
policyId,
requestedBy
);
return c.json({
success: true,
data: result
});
} catch (error) {
console.error('Error applying retention policy:', error);
return c.json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
}, 500);
}
});
// Get governance metrics
this.app.get('/metrics', async (c) => {
try {
const timeRange = c.req.query('timeRange') || '30d';
const metrics = await this.governanceService.getGovernanceMetrics(timeRange);
return c.json({
success: true,
data: metrics
});
} catch (error) {
console.error('Error getting governance metrics:', error);
return c.json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
}, 500);
}
});
}
public getApp(): Hono {
return this.app;
}
}

View file

@ -0,0 +1,172 @@
import { Hono } from 'hono';
export class HealthController {
private app: Hono;
constructor() {
this.app = new Hono();
this.setupRoutes();
}
private setupRoutes() {
// Basic health check
this.app.get('/', async (c) => {
return c.json({
service: 'data-catalog',
status: 'healthy',
timestamp: new Date().toISOString(),
version: process.env.SERVICE_VERSION || '1.0.0'
});
});
// Detailed health check
this.app.get('/detailed', async (c) => {
try {
const healthStatus = {
service: 'data-catalog',
status: 'healthy',
timestamp: new Date().toISOString(),
version: process.env.SERVICE_VERSION || '1.0.0',
uptime: process.uptime(),
memory: process.memoryUsage(),
dependencies: {
database: await this.checkDatabase(),
search: await this.checkSearchService(),
eventBus: await this.checkEventBus()
}
};
// Determine overall status based on dependencies
const hasUnhealthyDependencies = Object.values(healthStatus.dependencies)
.some(dep => dep.status !== 'healthy');
if (hasUnhealthyDependencies) {
healthStatus.status = 'degraded';
}
const statusCode = healthStatus.status === 'healthy' ? 200 : 503;
return c.json(healthStatus, statusCode);
} catch (error) {
console.error('Health check error:', error);
return c.json({
service: 'data-catalog',
status: 'unhealthy',
timestamp: new Date().toISOString(),
error: error instanceof Error ? error.message : 'Unknown error'
}, 503);
}
});
// Readiness check
this.app.get('/ready', async (c) => {
try {
// Check if service is ready to accept requests
const readyChecks = await Promise.all([
this.checkDatabase(),
this.checkSearchService()
]);
const isReady = readyChecks.every(check => check.status === 'healthy');
if (isReady) {
return c.json({
service: 'data-catalog',
ready: true,
timestamp: new Date().toISOString()
});
} else {
return c.json({
service: 'data-catalog',
ready: false,
timestamp: new Date().toISOString(),
checks: readyChecks
}, 503);
}
} catch (error) {
console.error('Readiness check error:', error);
return c.json({
service: 'data-catalog',
ready: false,
timestamp: new Date().toISOString(),
error: error instanceof Error ? error.message : 'Unknown error'
}, 503);
}
});
// Liveness check
this.app.get('/live', async (c) => {
return c.json({
service: 'data-catalog',
alive: true,
timestamp: new Date().toISOString()
});
});
}
private async checkDatabase(): Promise<{ name: string; status: string; responseTime?: number }> {
const start = Date.now();
try {
// Simulate database check
// In real implementation, this would ping the actual database
await new Promise(resolve => setTimeout(resolve, 10));
return {
name: 'database',
status: 'healthy',
responseTime: Date.now() - start
};
} catch (error) {
return {
name: 'database',
status: 'unhealthy',
responseTime: Date.now() - start
};
}
}
private async checkSearchService(): Promise<{ name: string; status: string; responseTime?: number }> {
const start = Date.now();
try {
// Simulate search service check
// In real implementation, this would check search index health
await new Promise(resolve => setTimeout(resolve, 5));
return {
name: 'search',
status: 'healthy',
responseTime: Date.now() - start
};
} catch (error) {
return {
name: 'search',
status: 'unhealthy',
responseTime: Date.now() - start
};
}
}
private async checkEventBus(): Promise<{ name: string; status: string; responseTime?: number }> {
const start = Date.now();
try {
// Simulate event bus check
// In real implementation, this would check message broker connectivity
await new Promise(resolve => setTimeout(resolve, 3));
return {
name: 'eventBus',
status: 'healthy',
responseTime: Date.now() - start
};
} catch (error) {
return {
name: 'eventBus',
status: 'unhealthy',
responseTime: Date.now() - start
};
}
}
public getApp(): Hono {
return this.app;
}
}

View file

@ -0,0 +1,211 @@
import { Hono } from 'hono';
import { DataLineageService } from '../services/DataLineageService';
import { CreateLineageRequest, LineageQuery, ImpactAnalysisQuery } from '../types/DataCatalog';
export class LineageController {
private app: Hono;
private lineageService: DataLineageService;
constructor() {
this.app = new Hono();
this.lineageService = new DataLineageService();
this.setupRoutes();
}
private setupRoutes() {
// Create lineage relationship
this.app.post('/', async (c) => {
try {
const request: CreateLineageRequest = await c.req.json();
const lineage = await this.lineageService.createLineage(request);
return c.json({
success: true,
data: lineage
});
} catch (error) {
console.error('Error creating lineage:', error);
return c.json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
}, 500);
}
});
// Get lineage for asset
this.app.get('/assets/:assetId', async (c) => {
try {
const assetId = c.req.param('assetId');
const direction = c.req.query('direction') as 'upstream' | 'downstream' | 'both';
const depth = c.req.query('depth') ? parseInt(c.req.query('depth')!) : undefined;
const lineage = await this.lineageService.getAssetLineage(assetId, {
direction: direction || 'both',
depth: depth || 10
});
return c.json({
success: true,
data: lineage
});
} catch (error) {
console.error('Error getting asset lineage:', error);
return c.json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
}, 500);
}
});
// Get upstream dependencies
this.app.get('/assets/:assetId/upstream', async (c) => {
try {
const assetId = c.req.param('assetId');
const depth = c.req.query('depth') ? parseInt(c.req.query('depth')!) : 5;
const upstream = await this.lineageService.getUpstreamDependencies(assetId, depth);
return c.json({
success: true,
data: upstream
});
} catch (error) {
console.error('Error getting upstream dependencies:', error);
return c.json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
}, 500);
}
});
// Get downstream dependencies
this.app.get('/assets/:assetId/downstream', async (c) => {
try {
const assetId = c.req.param('assetId');
const depth = c.req.query('depth') ? parseInt(c.req.query('depth')!) : 5;
const downstream = await this.lineageService.getDownstreamDependencies(assetId, depth);
return c.json({
success: true,
data: downstream
});
} catch (error) {
console.error('Error getting downstream dependencies:', error);
return c.json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
}, 500);
}
});
// Perform impact analysis
this.app.post('/impact-analysis', async (c) => {
try {
const query: ImpactAnalysisQuery = await c.req.json();
const analysis = await this.lineageService.performImpactAnalysis(query);
return c.json({
success: true,
data: analysis
});
} catch (error) {
console.error('Error performing impact analysis:', error);
return c.json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
}, 500);
}
});
// Get lineage graph
this.app.get('/graph', async (c) => {
try {
const assetIds = c.req.query('assetIds')?.split(',') || [];
const depth = c.req.query('depth') ? parseInt(c.req.query('depth')!) : 3;
if (assetIds.length === 0) {
return c.json({
success: false,
error: 'Asset IDs are required'
}, 400);
}
const graph = await this.lineageService.getLineageGraph(assetIds, depth);
return c.json({
success: true,
data: graph
});
} catch (error) {
console.error('Error getting lineage graph:', error);
return c.json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
}, 500);
}
});
// Check for circular dependencies
this.app.get('/assets/:assetId/circular-check', async (c) => {
try {
const assetId = c.req.param('assetId');
const hasCycles = await this.lineageService.hasCircularDependencies(assetId);
return c.json({
success: true,
data: {
assetId,
hasCircularDependencies: hasCycles
}
});
} catch (error) {
console.error('Error checking circular dependencies:', error);
return c.json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
}, 500);
}
});
// Delete lineage relationship
this.app.delete('/:lineageId', async (c) => {
try {
const lineageId = c.req.param('lineageId');
await this.lineageService.deleteLineage(lineageId);
return c.json({
success: true,
message: 'Lineage relationship deleted successfully'
});
} catch (error) {
console.error('Error deleting lineage:', error);
return c.json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
}, 500);
}
});
// Get lineage statistics
this.app.get('/stats', async (c) => {
try {
const stats = await this.lineageService.getLineageStatistics();
return c.json({
success: true,
data: stats
});
} catch (error) {
console.error('Error getting lineage statistics:', error);
return c.json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
}, 500);
}
});
}
public getApp(): Hono {
return this.app;
}
}

View file

@ -0,0 +1,321 @@
import { Hono } from 'hono';
import { DataQualityService } from '../services/DataQualityService';
import {
QualityAssessmentRequest,
QualityRule,
QualityIssue,
QualityReportRequest
} from '../types/DataCatalog';
export class QualityController {
private app: Hono;
private qualityService: DataQualityService;
constructor() {
this.app = new Hono();
this.qualityService = new DataQualityService();
this.setupRoutes();
}
private setupRoutes() {
// Assess asset quality
this.app.post('/assess', async (c) => {
try {
const request: QualityAssessmentRequest = await c.req.json();
const assessment = await this.qualityService.assessQuality(request);
return c.json({
success: true,
data: assessment
});
} catch (error) {
console.error('Error assessing quality:', error);
return c.json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
}, 500);
}
});
// Get quality assessment for asset
this.app.get('/assets/:assetId', async (c) => {
try {
const assetId = c.req.param('assetId');
const assessment = await this.qualityService.getQualityAssessment(assetId);
if (!assessment) {
return c.json({
success: false,
error: 'Quality assessment not found'
}, 404);
}
return c.json({
success: true,
data: assessment
});
} catch (error) {
console.error('Error getting quality assessment:', error);
return c.json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
}, 500);
}
});
// Create quality rule
this.app.post('/rules', async (c) => {
try {
const rule: Omit<QualityRule, 'id' | 'createdAt' | 'updatedAt'> = await c.req.json();
const createdRule = await this.qualityService.createQualityRule(rule);
return c.json({
success: true,
data: createdRule
});
} catch (error) {
console.error('Error creating quality rule:', error);
return c.json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
}, 500);
}
});
// Get quality rules
this.app.get('/rules', async (c) => {
try {
const assetType = c.req.query('assetType');
const dimension = c.req.query('dimension');
const active = c.req.query('active') === 'true';
const filters: any = {};
if (assetType) filters.assetType = assetType;
if (dimension) filters.dimension = dimension;
if (active !== undefined) filters.active = active;
const rules = await this.qualityService.getQualityRules(filters);
return c.json({
success: true,
data: rules
});
} catch (error) {
console.error('Error getting quality rules:', error);
return c.json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
}, 500);
}
});
// Update quality rule
this.app.put('/rules/:ruleId', async (c) => {
try {
const ruleId = c.req.param('ruleId');
const updates: Partial<QualityRule> = await c.req.json();
const updatedRule = await this.qualityService.updateQualityRule(ruleId, updates);
return c.json({
success: true,
data: updatedRule
});
} catch (error) {
console.error('Error updating quality rule:', error);
return c.json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
}, 500);
}
});
// Delete quality rule
this.app.delete('/rules/:ruleId', async (c) => {
try {
const ruleId = c.req.param('ruleId');
await this.qualityService.deleteQualityRule(ruleId);
return c.json({
success: true,
message: 'Quality rule deleted successfully'
});
} catch (error) {
console.error('Error deleting quality rule:', error);
return c.json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
}, 500);
}
});
// Validate quality rules for asset
this.app.post('/validate/:assetId', async (c) => {
try {
const assetId = c.req.param('assetId');
const data = await c.req.json();
const validationResults = await this.qualityService.validateQualityRules(assetId, data);
return c.json({
success: true,
data: validationResults
});
} catch (error) {
console.error('Error validating quality rules:', error);
return c.json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
}, 500);
}
});
// Report quality issue
this.app.post('/issues', async (c) => {
try {
const issue: Omit<QualityIssue, 'id' | 'reportedAt' | 'updatedAt'> = await c.req.json();
const reportedIssue = await this.qualityService.reportQualityIssue(issue);
return c.json({
success: true,
data: reportedIssue
});
} catch (error) {
console.error('Error reporting quality issue:', error);
return c.json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
}, 500);
}
});
// Get quality issues
this.app.get('/issues', async (c) => {
try {
const assetId = c.req.query('assetId');
const severity = c.req.query('severity');
const status = c.req.query('status');
const dimension = c.req.query('dimension');
const limit = c.req.query('limit') ? parseInt(c.req.query('limit')!) : 100;
const offset = c.req.query('offset') ? parseInt(c.req.query('offset')!) : 0;
const filters: any = {};
if (assetId) filters.assetId = assetId;
if (severity) filters.severity = severity;
if (status) filters.status = status;
if (dimension) filters.dimension = dimension;
const issues = await this.qualityService.getQualityIssues(filters, { limit, offset });
return c.json({
success: true,
data: issues
});
} catch (error) {
console.error('Error getting quality issues:', error);
return c.json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
}, 500);
}
});
// Resolve quality issue
this.app.patch('/issues/:issueId/resolve', async (c) => {
try {
const issueId = c.req.param('issueId');
const { resolution, resolvedBy } = await c.req.json();
const resolvedIssue = await this.qualityService.resolveQualityIssue(
issueId,
resolution,
resolvedBy
);
return c.json({
success: true,
data: resolvedIssue
});
} catch (error) {
console.error('Error resolving quality issue:', error);
return c.json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
}, 500);
}
});
// Get quality trends
this.app.get('/trends', async (c) => {
try {
const assetId = c.req.query('assetId');
const dimension = c.req.query('dimension');
const timeRange = c.req.query('timeRange') || '30d';
const trends = await this.qualityService.getQualityTrends(
assetId,
dimension,
timeRange
);
return c.json({
success: true,
data: trends
});
} catch (error) {
console.error('Error getting quality trends:', error);
return c.json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
}, 500);
}
});
// Generate quality report
this.app.post('/reports', async (c) => {
try {
const request: QualityReportRequest = await c.req.json();
const report = await this.qualityService.generateQualityReport(request);
return c.json({
success: true,
data: report
});
} catch (error) {
console.error('Error generating quality report:', error);
return c.json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
}, 500);
}
});
// Get quality metrics summary
this.app.get('/metrics/summary', async (c) => {
try {
const assetIds = c.req.query('assetIds')?.split(',');
const timeRange = c.req.query('timeRange') || '7d';
const summary = await this.qualityService.getQualityMetricsSummary(
assetIds,
timeRange
);
return c.json({
success: true,
data: summary
});
} catch (error) {
console.error('Error getting quality metrics summary:', error);
return c.json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
}, 500);
}
});
}
public getApp(): Hono {
return this.app;
}
}

View file

@ -0,0 +1,334 @@
import { Context } from 'hono';
import { Logger } from '@stock-bot/utils';
import { SearchService } from '../services/SearchService';
import { SearchQuery, SearchFilters } from '../types/DataCatalog';
export class SearchController {
constructor(
private searchService: SearchService,
private logger: Logger
) {}
async search(c: Context) {
try {
const queryParams = c.req.query();
const searchQuery: SearchQuery = {
text: queryParams.q || '',
offset: parseInt(queryParams.offset || '0'),
limit: parseInt(queryParams.limit || '20'),
sortBy: queryParams.sortBy,
sortOrder: queryParams.sortOrder as 'asc' | 'desc',
userId: queryParams.userId
};
// Parse filters
const filters: SearchFilters = {};
if (queryParams.types) {
filters.types = Array.isArray(queryParams.types) ? queryParams.types : [queryParams.types];
}
if (queryParams.classifications) {
filters.classifications = Array.isArray(queryParams.classifications) ? queryParams.classifications : [queryParams.classifications];
}
if (queryParams.owners) {
filters.owners = Array.isArray(queryParams.owners) ? queryParams.owners : [queryParams.owners];
}
if (queryParams.tags) {
filters.tags = Array.isArray(queryParams.tags) ? queryParams.tags : [queryParams.tags];
}
if (queryParams.createdAfter) {
filters.createdAfter = new Date(queryParams.createdAfter);
}
if (queryParams.createdBefore) {
filters.createdBefore = new Date(queryParams.createdBefore);
}
if (Object.keys(filters).length > 0) {
searchQuery.filters = filters;
}
const result = await this.searchService.search(searchQuery);
this.logger.info('Search API call completed', {
query: searchQuery.text,
resultCount: result.total,
searchTime: result.searchTime
});
return c.json(result);
} catch (error) {
this.logger.error('Search API call failed', { error });
return c.json({ error: 'Internal server error' }, 500);
}
}
async suggest(c: Context) {
try {
const partial = c.req.query('q');
if (!partial || partial.length < 2) {
return c.json({ suggestions: [] });
}
const suggestions = await this.searchService.suggest(partial);
return c.json({ suggestions });
} catch (error) {
this.logger.error('Suggestion API call failed', { error });
return c.json({ error: 'Internal server error' }, 500);
}
}
async searchByFacets(c: Context) {
try {
const facets = await c.req.json();
if (!facets || typeof facets !== 'object') {
return c.json({ error: 'Facets object is required' }, 400);
}
const assets = await this.searchService.searchByFacets(facets);
return c.json({
assets,
total: assets.length,
facets
});
} catch (error) {
this.logger.error('Facet search API call failed', { error });
return c.json({ error: 'Internal server error' }, 500);
}
}
async searchSimilar(c: Context) {
try {
const assetId = c.req.param('id');
const limit = parseInt(c.req.query('limit') || '10');
if (!assetId) {
return c.json({ error: 'Asset ID is required' }, 400);
}
const similarAssets = await this.searchService.searchSimilar(assetId, limit);
return c.json({
assetId,
similarAssets,
total: similarAssets.length
});
} catch (error) {
this.logger.error('Similar search API call failed', { error });
return c.json({ error: 'Internal server error' }, 500);
}
}
async getPopularSearches(c: Context) {
try {
const limit = parseInt(c.req.query('limit') || '10');
const popularSearches = await this.searchService.getPopularSearches(limit);
return c.json({
searches: popularSearches,
total: popularSearches.length
});
} catch (error) {
this.logger.error('Popular searches API call failed', { error });
return c.json({ error: 'Internal server error' }, 500);
}
}
async getRecentSearches(c: Context) {
try {
const userId = c.req.param('userId');
const limit = parseInt(c.req.query('limit') || '10');
if (!userId) {
return c.json({ error: 'User ID is required' }, 400);
}
const recentSearches = await this.searchService.getRecentSearches(userId, limit);
return c.json({
userId,
searches: recentSearches,
total: recentSearches.length
});
} catch (error) {
this.logger.error('Recent searches API call failed', { error });
return c.json({ error: 'Internal server error' }, 500);
}
}
async reindexAssets(c: Context) {
try {
await this.searchService.reindexAll();
this.logger.info('Search index rebuilt via API');
return c.json({ message: 'Search index rebuilt successfully' });
} catch (error) {
this.logger.error('Reindex API call failed', { error });
return c.json({ error: 'Internal server error' }, 500);
}
}
async getSearchAnalytics(c: Context) {
try {
const timeframe = c.req.query('timeframe') || 'week';
const analytics = await this.searchService.getSearchAnalytics(timeframe);
return c.json({
timeframe,
analytics
});
} catch (error) {
this.logger.error('Search analytics API call failed', { error });
return c.json({ error: 'Internal server error' }, 500);
}
}
async advancedSearch(c: Context) {
try {
const searchRequest = await c.req.json();
if (!searchRequest) {
return c.json({ error: 'Search request is required' }, 400);
}
// Build advanced search query
const searchQuery: SearchQuery = {
text: searchRequest.query || '',
offset: searchRequest.offset || 0,
limit: searchRequest.limit || 20,
sortBy: searchRequest.sortBy,
sortOrder: searchRequest.sortOrder,
userId: searchRequest.userId,
filters: searchRequest.filters
};
const result = await this.searchService.search(searchQuery);
// If no results and query is complex, try to suggest simpler alternatives
if (result.total === 0 && searchQuery.text && searchQuery.text.split(' ').length > 2) {
const simpleQuery = searchQuery.text.split(' ')[0];
const simpleResult = await this.searchService.search({
...searchQuery,
text: simpleQuery
});
if (simpleResult.total > 0) {
result.suggestions = [`Try searching for "${simpleQuery}"`];
}
}
this.logger.info('Advanced search API call completed', {
query: searchQuery.text,
resultCount: result.total,
searchTime: result.searchTime
});
return c.json(result);
} catch (error) {
this.logger.error('Advanced search API call failed', { error });
return c.json({ error: 'Internal server error' }, 500);
}
}
async exportSearchResults(c: Context) {
try {
const queryParams = c.req.query();
const format = queryParams.format || 'json';
if (format !== 'json' && format !== 'csv') {
return c.json({ error: 'Unsupported export format. Use json or csv' }, 400);
}
// Perform search with maximum results
const searchQuery: SearchQuery = {
text: queryParams.q || '',
offset: 0,
limit: 10000, // Large limit for export
sortBy: queryParams.sortBy,
sortOrder: queryParams.sortOrder as 'asc' | 'desc'
};
const result = await this.searchService.search(searchQuery);
if (format === 'csv') {
const csv = this.convertToCSV(result.assets);
c.header('Content-Type', 'text/csv');
c.header('Content-Disposition', 'attachment; filename="search-results.csv"');
return c.text(csv);
} else {
c.header('Content-Type', 'application/json');
c.header('Content-Disposition', 'attachment; filename="search-results.json"');
return c.json(result);
}
} catch (error) {
this.logger.error('Export search results API call failed', { error });
return c.json({ error: 'Internal server error' }, 500);
}
}
async getSearchStatistics(c: Context) {
try {
const timeframe = c.req.query('timeframe') || 'week';
const analytics = await this.searchService.getSearchAnalytics(timeframe);
const statistics = {
searchVolume: analytics.totalSearches,
uniqueQueries: analytics.uniqueQueries,
averageResultsPerSearch: Math.round(analytics.averageResults),
noResultQueriesPercent: analytics.totalSearches > 0
? Math.round((analytics.noResultQueries / analytics.totalSearches) * 100)
: 0,
topSearchTerms: analytics.topQueries.slice(0, 5),
searchTrend: analytics.searchTrend.trend,
facetUsage: analytics.facetUsage
};
return c.json({
timeframe,
statistics
});
} catch (error) {
this.logger.error('Search statistics API call failed', { error });
return c.json({ error: 'Internal server error' }, 500);
}
}
// Helper method to convert assets to CSV format
private convertToCSV(assets: any[]): string {
if (assets.length === 0) {
return 'No results found';
}
const headers = [
'ID', 'Name', 'Type', 'Description', 'Owner', 'Classification',
'Tags', 'Created At', 'Updated At', 'Last Accessed'
];
const csvRows = [headers.join(',')];
for (const asset of assets) {
const row = [
asset.id,
`"${asset.name.replace(/"/g, '""')}"`,
asset.type,
`"${asset.description.replace(/"/g, '""')}"`,
asset.owner,
asset.classification,
`"${asset.tags.join('; ')}"`,
asset.createdAt.toISOString(),
asset.updatedAt.toISOString(),
asset.lastAccessed ? asset.lastAccessed.toISOString() : ''
];
csvRows.push(row.join(','));
}
return csvRows.join('\n');
}
}