adding data-services
This commit is contained in:
parent
e3bfd05b90
commit
405b818c86
139 changed files with 55943 additions and 416 deletions
|
|
@ -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;
|
||||
}, {});
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue