huge refactor on web-api and web-app
This commit is contained in:
parent
1d299e52d4
commit
265e10a658
23 changed files with 1545 additions and 1233 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -11,15 +11,22 @@ export const healthRoutes = new Hono();
|
|||
|
||||
// Basic health check
|
||||
healthRoutes.get('/', c => {
|
||||
return c.json({
|
||||
logger.debug('Basic health check requested');
|
||||
|
||||
const response = {
|
||||
status: 'healthy',
|
||||
service: 'web-api',
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
};
|
||||
|
||||
logger.info('Basic health check successful', { status: response.status });
|
||||
return c.json(response);
|
||||
});
|
||||
|
||||
// Detailed health check with database connectivity
|
||||
healthRoutes.get('/detailed', async c => {
|
||||
logger.debug('Detailed health check requested');
|
||||
|
||||
const health = {
|
||||
status: 'healthy',
|
||||
service: 'web-api',
|
||||
|
|
@ -31,6 +38,7 @@ healthRoutes.get('/detailed', async c => {
|
|||
};
|
||||
|
||||
// Check MongoDB
|
||||
logger.debug('Checking MongoDB connectivity');
|
||||
try {
|
||||
const mongoClient = getMongoDBClient();
|
||||
if (mongoClient.connected) {
|
||||
|
|
@ -38,26 +46,34 @@ healthRoutes.get('/detailed', async c => {
|
|||
const db = mongoClient.getDatabase();
|
||||
await db.admin().ping();
|
||||
health.checks.mongodb = { status: 'healthy', message: 'Connected and responsive' };
|
||||
logger.debug('MongoDB health check passed');
|
||||
} else {
|
||||
health.checks.mongodb = { status: 'unhealthy', message: 'Not connected' };
|
||||
logger.warn('MongoDB health check failed - not connected');
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
health.checks.mongodb = {
|
||||
status: 'unhealthy',
|
||||
message: error instanceof Error ? error.message : 'Unknown error',
|
||||
message: errorMessage,
|
||||
};
|
||||
logger.error('MongoDB health check failed', { error: errorMessage });
|
||||
}
|
||||
|
||||
// Check PostgreSQL
|
||||
logger.debug('Checking PostgreSQL connectivity');
|
||||
try {
|
||||
const postgresClient = getPostgreSQLClient();
|
||||
await postgresClient.query('SELECT 1');
|
||||
health.checks.postgresql = { status: 'healthy', message: 'Connected and responsive' };
|
||||
logger.debug('PostgreSQL health check passed');
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
health.checks.postgresql = {
|
||||
status: 'unhealthy',
|
||||
message: error instanceof Error ? error.message : 'Unknown error',
|
||||
message: errorMessage,
|
||||
};
|
||||
logger.error('PostgreSQL health check failed', { error: errorMessage });
|
||||
}
|
||||
|
||||
// Overall status
|
||||
|
|
@ -65,5 +81,19 @@ healthRoutes.get('/detailed', async c => {
|
|||
health.status = allHealthy ? 'healthy' : 'unhealthy';
|
||||
|
||||
const statusCode = allHealthy ? 200 : 503;
|
||||
|
||||
if (allHealthy) {
|
||||
logger.info('Detailed health check successful - all systems healthy', {
|
||||
mongodb: health.checks.mongodb.status,
|
||||
postgresql: health.checks.postgresql.status
|
||||
});
|
||||
} else {
|
||||
logger.warn('Detailed health check failed - some systems unhealthy', {
|
||||
mongodb: health.checks.mongodb.status,
|
||||
postgresql: health.checks.postgresql.status,
|
||||
overallStatus: health.status
|
||||
});
|
||||
}
|
||||
|
||||
return c.json(health, statusCode);
|
||||
});
|
||||
|
|
|
|||
381
apps/web-api/src/services/exchange.service.ts
Normal file
381
apps/web-api/src/services/exchange.service.ts
Normal file
|
|
@ -0,0 +1,381 @@
|
|||
import { getLogger } from '@stock-bot/logger';
|
||||
import { getPostgreSQLClient } from '@stock-bot/postgres-client';
|
||||
import { getMongoDBClient } from '@stock-bot/mongodb-client';
|
||||
import {
|
||||
Exchange,
|
||||
ExchangeWithMappings,
|
||||
ProviderMapping,
|
||||
CreateExchangeRequest,
|
||||
UpdateExchangeRequest,
|
||||
CreateProviderMappingRequest,
|
||||
UpdateProviderMappingRequest,
|
||||
ProviderExchange,
|
||||
ExchangeStats,
|
||||
} from '../types/exchange.types';
|
||||
|
||||
const logger = getLogger('exchange-service');
|
||||
|
||||
export class ExchangeService {
|
||||
private postgresClient = getPostgreSQLClient();
|
||||
private mongoClient = getMongoDBClient();
|
||||
|
||||
// Exchanges
|
||||
async getAllExchanges(): Promise<ExchangeWithMappings[]> {
|
||||
const exchangesQuery = `
|
||||
SELECT
|
||||
e.id,
|
||||
e.code,
|
||||
e.name,
|
||||
e.country,
|
||||
e.currency,
|
||||
e.active,
|
||||
e.visible,
|
||||
e.created_at,
|
||||
e.updated_at,
|
||||
COUNT(pem.id) as provider_mapping_count,
|
||||
COUNT(CASE WHEN pem.active = true THEN 1 END) as active_mapping_count,
|
||||
COUNT(CASE WHEN pem.verified = true THEN 1 END) as verified_mapping_count,
|
||||
STRING_AGG(DISTINCT pem.provider, ', ') as providers
|
||||
FROM exchanges e
|
||||
LEFT JOIN provider_exchange_mappings pem ON e.id = pem.master_exchange_id
|
||||
WHERE e.visible = true
|
||||
GROUP BY e.id, e.code, e.name, e.country, e.currency, e.active, e.visible, e.created_at, e.updated_at
|
||||
ORDER BY e.code
|
||||
`;
|
||||
|
||||
const exchangesResult = await this.postgresClient.query(exchangesQuery);
|
||||
|
||||
// Get all provider mappings
|
||||
const mappingsQuery = `
|
||||
SELECT
|
||||
pem.*,
|
||||
e.code as master_exchange_code,
|
||||
e.name as master_exchange_name
|
||||
FROM provider_exchange_mappings pem
|
||||
JOIN exchanges e ON pem.master_exchange_id = e.id
|
||||
WHERE e.visible = true
|
||||
ORDER BY pem.master_exchange_id, pem.provider, pem.provider_exchange_code
|
||||
`;
|
||||
const mappingsResult = await this.postgresClient.query(mappingsQuery);
|
||||
|
||||
// Group mappings by exchange ID
|
||||
const mappingsByExchange = mappingsResult.rows.reduce((acc, mapping) => {
|
||||
const exchangeId = mapping.master_exchange_id;
|
||||
if (!acc[exchangeId]) {
|
||||
acc[exchangeId] = [];
|
||||
}
|
||||
acc[exchangeId].push(mapping);
|
||||
return acc;
|
||||
}, {} as Record<string, ProviderMapping[]>);
|
||||
|
||||
// Attach mappings to exchanges
|
||||
return exchangesResult.rows.map(exchange => ({
|
||||
...exchange,
|
||||
provider_mappings: mappingsByExchange[exchange.id] || [],
|
||||
}));
|
||||
}
|
||||
|
||||
async getExchangeById(id: string): Promise<{ exchange: Exchange; provider_mappings: ProviderMapping[] } | null> {
|
||||
const exchangeQuery = 'SELECT * FROM exchanges WHERE id = $1 AND visible = true';
|
||||
const exchangeResult = await this.postgresClient.query(exchangeQuery, [id]);
|
||||
|
||||
if (exchangeResult.rows.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const mappingsQuery = `
|
||||
SELECT
|
||||
pem.*,
|
||||
e.code as master_exchange_code,
|
||||
e.name as master_exchange_name
|
||||
FROM provider_exchange_mappings pem
|
||||
JOIN exchanges e ON pem.master_exchange_id = e.id
|
||||
WHERE pem.master_exchange_id = $1
|
||||
ORDER BY pem.provider, pem.provider_exchange_code
|
||||
`;
|
||||
const mappingsResult = await this.postgresClient.query(mappingsQuery, [id]);
|
||||
|
||||
return {
|
||||
exchange: exchangeResult.rows[0],
|
||||
provider_mappings: mappingsResult.rows,
|
||||
};
|
||||
}
|
||||
|
||||
async createExchange(data: CreateExchangeRequest): Promise<Exchange> {
|
||||
const query = `
|
||||
INSERT INTO exchanges (code, name, country, currency, active, visible)
|
||||
VALUES ($1, $2, $3, $4, $5, true)
|
||||
RETURNING *
|
||||
`;
|
||||
|
||||
const result = await this.postgresClient.query(query, [
|
||||
data.code,
|
||||
data.name,
|
||||
data.country,
|
||||
data.currency,
|
||||
data.active,
|
||||
]);
|
||||
|
||||
logger.info('Exchange created', {
|
||||
exchangeId: result.rows[0].id,
|
||||
code: data.code,
|
||||
name: data.name,
|
||||
});
|
||||
|
||||
return result.rows[0];
|
||||
}
|
||||
|
||||
async updateExchange(id: string, updates: UpdateExchangeRequest): Promise<Exchange | null> {
|
||||
const updateFields = [];
|
||||
const values = [];
|
||||
let paramIndex = 1;
|
||||
|
||||
Object.entries(updates).forEach(([key, value]) => {
|
||||
updateFields.push(`${key} = $${paramIndex++}`);
|
||||
values.push(value);
|
||||
});
|
||||
|
||||
updateFields.push(`updated_at = NOW()`);
|
||||
values.push(id);
|
||||
|
||||
const query = `
|
||||
UPDATE exchanges
|
||||
SET ${updateFields.join(', ')}
|
||||
WHERE id = $${paramIndex}
|
||||
RETURNING *
|
||||
`;
|
||||
|
||||
const result = await this.postgresClient.query(query, values);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If hiding an exchange, delete its provider mappings
|
||||
if (updates.visible === false) {
|
||||
await this.deleteProviderMappingsByExchangeId(id);
|
||||
}
|
||||
|
||||
logger.info('Exchange updated', { exchangeId: id, updates });
|
||||
|
||||
return result.rows[0];
|
||||
}
|
||||
|
||||
// Provider Mappings
|
||||
async getAllProviderMappings(): Promise<ProviderMapping[]> {
|
||||
const query = `
|
||||
SELECT
|
||||
pem.*,
|
||||
e.code as master_exchange_code,
|
||||
e.name as master_exchange_name,
|
||||
e.active as master_exchange_active
|
||||
FROM provider_exchange_mappings pem
|
||||
JOIN exchanges e ON pem.master_exchange_id = e.id
|
||||
WHERE e.visible = true
|
||||
ORDER BY pem.provider, pem.provider_exchange_code
|
||||
`;
|
||||
|
||||
const result = await this.postgresClient.query(query);
|
||||
return result.rows;
|
||||
}
|
||||
|
||||
async getProviderMappingsByProvider(provider: string): Promise<ProviderMapping[]> {
|
||||
const query = `
|
||||
SELECT
|
||||
pem.*,
|
||||
e.code as master_exchange_code,
|
||||
e.name as master_exchange_name,
|
||||
e.active as master_exchange_active
|
||||
FROM provider_exchange_mappings pem
|
||||
JOIN exchanges e ON pem.master_exchange_id = e.id
|
||||
WHERE pem.provider = $1 AND e.visible = true
|
||||
ORDER BY pem.provider_exchange_code
|
||||
`;
|
||||
|
||||
const result = await this.postgresClient.query(query, [provider]);
|
||||
return result.rows;
|
||||
}
|
||||
|
||||
async createProviderMapping(data: CreateProviderMappingRequest): Promise<ProviderMapping> {
|
||||
const query = `
|
||||
INSERT INTO provider_exchange_mappings
|
||||
(provider, provider_exchange_code, provider_exchange_name, master_exchange_id,
|
||||
country_code, currency, confidence, active, verified, auto_mapped)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, false)
|
||||
RETURNING *
|
||||
`;
|
||||
|
||||
const result = await this.postgresClient.query(query, [
|
||||
data.provider,
|
||||
data.provider_exchange_code,
|
||||
data.provider_exchange_name,
|
||||
data.master_exchange_id,
|
||||
data.country_code,
|
||||
data.currency,
|
||||
data.confidence,
|
||||
data.active,
|
||||
data.verified,
|
||||
]);
|
||||
|
||||
logger.info('Provider mapping created', {
|
||||
provider: data.provider,
|
||||
provider_exchange_code: data.provider_exchange_code,
|
||||
master_exchange_id: data.master_exchange_id,
|
||||
});
|
||||
|
||||
return result.rows[0];
|
||||
}
|
||||
|
||||
async updateProviderMapping(id: string, updates: UpdateProviderMappingRequest): Promise<ProviderMapping | null> {
|
||||
const updateFields = [];
|
||||
const values = [];
|
||||
let paramIndex = 1;
|
||||
|
||||
Object.entries(updates).forEach(([key, value]) => {
|
||||
updateFields.push(`${key} = $${paramIndex++}`);
|
||||
values.push(value);
|
||||
});
|
||||
|
||||
updateFields.push(`updated_at = NOW()`);
|
||||
updateFields.push(`auto_mapped = false`); // Mark as manually managed
|
||||
values.push(id);
|
||||
|
||||
const query = `
|
||||
UPDATE provider_exchange_mappings
|
||||
SET ${updateFields.join(', ')}
|
||||
WHERE id = $${paramIndex}
|
||||
RETURNING *
|
||||
`;
|
||||
|
||||
const result = await this.postgresClient.query(query, values);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
logger.info('Provider mapping updated', { mappingId: id, updates });
|
||||
|
||||
return result.rows[0];
|
||||
}
|
||||
|
||||
async deleteProviderMappingsByExchangeId(exchangeId: string): Promise<number> {
|
||||
const query = 'DELETE FROM provider_exchange_mappings WHERE master_exchange_id = $1';
|
||||
const result = await this.postgresClient.query(query, [exchangeId]);
|
||||
|
||||
logger.info('Deleted provider mappings for hidden exchange', {
|
||||
exchangeId,
|
||||
deletedMappings: result.rowCount,
|
||||
});
|
||||
|
||||
return result.rowCount || 0;
|
||||
}
|
||||
|
||||
// Providers and Statistics
|
||||
async getProviders(): Promise<string[]> {
|
||||
const query = `
|
||||
SELECT DISTINCT provider
|
||||
FROM provider_exchange_mappings
|
||||
ORDER BY provider
|
||||
`;
|
||||
|
||||
const result = await this.postgresClient.query(query);
|
||||
return result.rows.map(row => row.provider);
|
||||
}
|
||||
|
||||
async getExchangeStats(): Promise<ExchangeStats> {
|
||||
const query = `
|
||||
SELECT
|
||||
(SELECT COUNT(*) FROM exchanges WHERE visible = true) as total_exchanges,
|
||||
(SELECT COUNT(*) FROM exchanges WHERE active = true AND visible = true) as active_exchanges,
|
||||
(SELECT COUNT(DISTINCT country) FROM exchanges WHERE visible = true) as countries,
|
||||
(SELECT COUNT(DISTINCT currency) FROM exchanges WHERE visible = true) as currencies,
|
||||
(SELECT COUNT(*) FROM provider_exchange_mappings pem JOIN exchanges e ON pem.master_exchange_id = e.id WHERE e.visible = true) as total_provider_mappings,
|
||||
(SELECT COUNT(*) FROM provider_exchange_mappings pem JOIN exchanges e ON pem.master_exchange_id = e.id WHERE pem.active = true AND e.visible = true) as active_provider_mappings,
|
||||
(SELECT COUNT(*) FROM provider_exchange_mappings pem JOIN exchanges e ON pem.master_exchange_id = e.id WHERE pem.verified = true AND e.visible = true) as verified_provider_mappings,
|
||||
(SELECT COUNT(DISTINCT provider) FROM provider_exchange_mappings pem JOIN exchanges e ON pem.master_exchange_id = e.id WHERE e.visible = true) as providers
|
||||
`;
|
||||
|
||||
const result = await this.postgresClient.query(query);
|
||||
return result.rows[0];
|
||||
}
|
||||
|
||||
async getUnmappedProviderExchanges(provider: string): Promise<ProviderExchange[]> {
|
||||
// Get existing mappings for this provider
|
||||
const existingMappingsQuery = `
|
||||
SELECT provider_exchange_code
|
||||
FROM provider_exchange_mappings
|
||||
WHERE provider = $1
|
||||
`;
|
||||
const existingMappings = await this.postgresClient.query(existingMappingsQuery, [provider]);
|
||||
const mappedCodes = new Set(existingMappings.rows.map(row => row.provider_exchange_code));
|
||||
|
||||
const db = this.mongoClient.getDatabase();
|
||||
let providerExchanges: ProviderExchange[] = [];
|
||||
|
||||
switch (provider) {
|
||||
case 'eod': {
|
||||
const eodExchanges = await db.collection('eodExchanges').find({ active: true }).toArray();
|
||||
providerExchanges = eodExchanges
|
||||
.filter(exchange => !mappedCodes.has(exchange.Code))
|
||||
.map(exchange => ({
|
||||
provider_exchange_code: exchange.Code,
|
||||
provider_exchange_name: exchange.Name,
|
||||
country_code: exchange.CountryISO2,
|
||||
currency: exchange.Currency,
|
||||
symbol_count: null,
|
||||
}));
|
||||
break;
|
||||
}
|
||||
|
||||
case 'ib': {
|
||||
const ibExchanges = await db.collection('ibExchanges').find({}).toArray();
|
||||
providerExchanges = ibExchanges
|
||||
.filter(exchange => !mappedCodes.has(exchange.exchange_id))
|
||||
.map(exchange => ({
|
||||
provider_exchange_code: exchange.exchange_id,
|
||||
provider_exchange_name: exchange.name,
|
||||
country_code: exchange.country_code,
|
||||
currency: null,
|
||||
symbol_count: null,
|
||||
}));
|
||||
break;
|
||||
}
|
||||
|
||||
case 'qm': {
|
||||
const qmExchanges = await db.collection('qmExchanges').find({}).toArray();
|
||||
providerExchanges = qmExchanges
|
||||
.filter(exchange => !mappedCodes.has(exchange.exchangeCode))
|
||||
.map(exchange => ({
|
||||
provider_exchange_code: exchange.exchangeCode,
|
||||
provider_exchange_name: exchange.name,
|
||||
country_code: exchange.countryCode,
|
||||
currency: exchange.countryCode === 'CA' ? 'CAD' : 'USD',
|
||||
symbol_count: null,
|
||||
}));
|
||||
break;
|
||||
}
|
||||
|
||||
case 'unified': {
|
||||
const unifiedExchanges = await db.collection('exchanges').find({}).toArray();
|
||||
providerExchanges = unifiedExchanges
|
||||
.filter(exchange => !mappedCodes.has(exchange.sourceCode || exchange.code))
|
||||
.map(exchange => ({
|
||||
provider_exchange_code: exchange.sourceCode || exchange.code,
|
||||
provider_exchange_name: exchange.sourceName || exchange.name,
|
||||
country_code: null,
|
||||
currency: null,
|
||||
symbol_count: null,
|
||||
}));
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown provider: ${provider}`);
|
||||
}
|
||||
|
||||
return providerExchanges;
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const exchangeService = new ExchangeService();
|
||||
103
apps/web-api/src/types/exchange.types.ts
Normal file
103
apps/web-api/src/types/exchange.types.ts
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
export interface Exchange {
|
||||
id: string;
|
||||
code: string;
|
||||
name: string;
|
||||
country: string;
|
||||
currency: string;
|
||||
active: boolean;
|
||||
visible: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface ExchangeWithMappings extends Exchange {
|
||||
provider_mapping_count: string;
|
||||
active_mapping_count: string;
|
||||
verified_mapping_count: string;
|
||||
providers: string | null;
|
||||
provider_mappings: ProviderMapping[];
|
||||
}
|
||||
|
||||
export interface ProviderMapping {
|
||||
id: string;
|
||||
provider: string;
|
||||
provider_exchange_code: string;
|
||||
provider_exchange_name: string;
|
||||
master_exchange_id: string;
|
||||
country_code: string | null;
|
||||
currency: string | null;
|
||||
confidence: number;
|
||||
active: boolean;
|
||||
verified: boolean;
|
||||
auto_mapped: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
master_exchange_code?: string;
|
||||
master_exchange_name?: string;
|
||||
master_exchange_active?: boolean;
|
||||
}
|
||||
|
||||
export interface CreateExchangeRequest {
|
||||
code: string;
|
||||
name: string;
|
||||
country: string;
|
||||
currency: string;
|
||||
active?: boolean;
|
||||
}
|
||||
|
||||
export interface UpdateExchangeRequest {
|
||||
name?: string;
|
||||
active?: boolean;
|
||||
visible?: boolean;
|
||||
country?: string;
|
||||
currency?: string;
|
||||
}
|
||||
|
||||
export interface CreateProviderMappingRequest {
|
||||
provider: string;
|
||||
provider_exchange_code: string;
|
||||
provider_exchange_name?: string;
|
||||
master_exchange_id: string;
|
||||
country_code?: string;
|
||||
currency?: string;
|
||||
confidence?: number;
|
||||
active?: boolean;
|
||||
verified?: boolean;
|
||||
}
|
||||
|
||||
export interface UpdateProviderMappingRequest {
|
||||
active?: boolean;
|
||||
verified?: boolean;
|
||||
confidence?: number;
|
||||
master_exchange_id?: string;
|
||||
}
|
||||
|
||||
export interface ProviderExchange {
|
||||
provider_exchange_code: string;
|
||||
provider_exchange_name: string;
|
||||
country_code: string | null;
|
||||
currency: string | null;
|
||||
symbol_count: number | null;
|
||||
is_mapped?: boolean;
|
||||
mapped_to_exchange_id?: string | null;
|
||||
mapped_to_exchange_code?: string | null;
|
||||
}
|
||||
|
||||
export interface ExchangeStats {
|
||||
total_exchanges: number;
|
||||
active_exchanges: number;
|
||||
countries: number;
|
||||
currencies: number;
|
||||
total_provider_mappings: number;
|
||||
active_provider_mappings: number;
|
||||
verified_provider_mappings: number;
|
||||
providers: number;
|
||||
}
|
||||
|
||||
export interface ApiResponse<T = any> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
error?: string;
|
||||
message?: string;
|
||||
total?: number;
|
||||
}
|
||||
64
apps/web-api/src/utils/error-handler.ts
Normal file
64
apps/web-api/src/utils/error-handler.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import { Context } from 'hono';
|
||||
import { getLogger } from '@stock-bot/logger';
|
||||
import { ValidationError } from './validation';
|
||||
import { ApiResponse } from '../types/exchange.types';
|
||||
|
||||
const logger = getLogger('error-handler');
|
||||
|
||||
export function handleError(c: Context, error: unknown, operation: string): Response {
|
||||
logger.error(`Failed ${operation}`, { error });
|
||||
|
||||
// Handle validation errors
|
||||
if (error instanceof ValidationError) {
|
||||
const response: ApiResponse = {
|
||||
success: false,
|
||||
error: error.message,
|
||||
};
|
||||
return c.json(response, 400);
|
||||
}
|
||||
|
||||
// Handle database constraint violations
|
||||
if (error instanceof Error && error.message.includes('duplicate key')) {
|
||||
const response: ApiResponse = {
|
||||
success: false,
|
||||
error: 'Resource already exists with this unique identifier',
|
||||
};
|
||||
return c.json(response, 409);
|
||||
}
|
||||
|
||||
// Handle not found errors
|
||||
if (error instanceof Error && error.message.includes('not found')) {
|
||||
const response: ApiResponse = {
|
||||
success: false,
|
||||
error: error.message,
|
||||
};
|
||||
return c.json(response, 404);
|
||||
}
|
||||
|
||||
// Generic error response
|
||||
const response: ApiResponse = {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
||||
};
|
||||
return c.json(response, 500);
|
||||
}
|
||||
|
||||
export function createSuccessResponse<T>(
|
||||
data: T,
|
||||
message?: string,
|
||||
total?: number
|
||||
): ApiResponse<T> {
|
||||
const response: ApiResponse<T> = {
|
||||
success: true,
|
||||
data,
|
||||
};
|
||||
|
||||
if (message) {
|
||||
response.message = message;
|
||||
}
|
||||
if (total !== undefined) {
|
||||
response.total = total;
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
161
apps/web-api/src/utils/validation.ts
Normal file
161
apps/web-api/src/utils/validation.ts
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
import { CreateExchangeRequest, CreateProviderMappingRequest } from '../types/exchange.types';
|
||||
|
||||
export class ValidationError extends Error {
|
||||
constructor(message: string, public field?: string) {
|
||||
super(message);
|
||||
this.name = 'ValidationError';
|
||||
}
|
||||
}
|
||||
|
||||
export function validateCreateExchange(data: any): CreateExchangeRequest {
|
||||
const { code, name, country, currency, active = true } = data;
|
||||
|
||||
if (!code || typeof code !== 'string') {
|
||||
throw new ValidationError('Exchange code is required', 'code');
|
||||
}
|
||||
|
||||
if (!name || typeof name !== 'string') {
|
||||
throw new ValidationError('Exchange name is required', 'name');
|
||||
}
|
||||
|
||||
if (!country || typeof country !== 'string') {
|
||||
throw new ValidationError('Country is required', 'country');
|
||||
}
|
||||
|
||||
if (!currency || typeof currency !== 'string') {
|
||||
throw new ValidationError('Currency is required', 'currency');
|
||||
}
|
||||
|
||||
if (code.length > 10) {
|
||||
throw new ValidationError('Exchange code must be 10 characters or less', 'code');
|
||||
}
|
||||
|
||||
if (country.length !== 2) {
|
||||
throw new ValidationError('Country must be exactly 2 characters (e.g., US, CA, GB)', 'country');
|
||||
}
|
||||
|
||||
if (currency.length !== 3) {
|
||||
throw new ValidationError('Currency must be exactly 3 characters (e.g., USD, EUR, CAD)', 'currency');
|
||||
}
|
||||
|
||||
return {
|
||||
code: code.toUpperCase().trim(),
|
||||
name: name.trim(),
|
||||
country: country.toUpperCase().trim(),
|
||||
currency: currency.toUpperCase().trim(),
|
||||
active: Boolean(active),
|
||||
};
|
||||
}
|
||||
|
||||
export function validateCreateProviderMapping(data: any): CreateProviderMappingRequest {
|
||||
const {
|
||||
provider,
|
||||
provider_exchange_code,
|
||||
provider_exchange_name,
|
||||
master_exchange_id,
|
||||
country_code,
|
||||
currency,
|
||||
confidence = 1.0,
|
||||
active = false,
|
||||
verified = false,
|
||||
} = data;
|
||||
|
||||
if (!provider || typeof provider !== 'string') {
|
||||
throw new ValidationError('Provider is required', 'provider');
|
||||
}
|
||||
|
||||
if (!provider_exchange_code || typeof provider_exchange_code !== 'string') {
|
||||
throw new ValidationError('Provider exchange code is required', 'provider_exchange_code');
|
||||
}
|
||||
|
||||
if (!master_exchange_id || typeof master_exchange_id !== 'string') {
|
||||
throw new ValidationError('Master exchange ID is required', 'master_exchange_id');
|
||||
}
|
||||
|
||||
// Validate currency is 3 characters or null
|
||||
const cleanCurrency = currency && currency.length <= 3 ? currency : null;
|
||||
|
||||
return {
|
||||
provider: provider.trim(),
|
||||
provider_exchange_code: provider_exchange_code.trim(),
|
||||
provider_exchange_name: provider_exchange_name?.trim(),
|
||||
master_exchange_id: master_exchange_id.trim(),
|
||||
country_code: country_code?.trim() || null,
|
||||
currency: cleanCurrency,
|
||||
confidence: Number(confidence),
|
||||
active: Boolean(active),
|
||||
verified: Boolean(verified),
|
||||
};
|
||||
}
|
||||
|
||||
export function validateUpdateExchange(data: any): any {
|
||||
const updates: any = {};
|
||||
|
||||
if (data.name !== undefined) {
|
||||
if (typeof data.name !== 'string') {
|
||||
throw new ValidationError('Name must be a string', 'name');
|
||||
}
|
||||
updates.name = data.name.trim();
|
||||
}
|
||||
|
||||
if (data.active !== undefined) {
|
||||
updates.active = Boolean(data.active);
|
||||
}
|
||||
|
||||
if (data.visible !== undefined) {
|
||||
updates.visible = Boolean(data.visible);
|
||||
}
|
||||
|
||||
if (data.country !== undefined) {
|
||||
if (typeof data.country !== 'string' || data.country.length !== 2) {
|
||||
throw new ValidationError('Country must be exactly 2 characters', 'country');
|
||||
}
|
||||
updates.country = data.country.toUpperCase().trim();
|
||||
}
|
||||
|
||||
if (data.currency !== undefined) {
|
||||
if (typeof data.currency !== 'string' || data.currency.length !== 3) {
|
||||
throw new ValidationError('Currency must be exactly 3 characters', 'currency');
|
||||
}
|
||||
updates.currency = data.currency.toUpperCase().trim();
|
||||
}
|
||||
|
||||
if (Object.keys(updates).length === 0) {
|
||||
throw new ValidationError('No valid fields to update');
|
||||
}
|
||||
|
||||
return updates;
|
||||
}
|
||||
|
||||
export function validateUpdateProviderMapping(data: any): any {
|
||||
const updates: any = {};
|
||||
|
||||
if (data.active !== undefined) {
|
||||
updates.active = Boolean(data.active);
|
||||
}
|
||||
|
||||
if (data.verified !== undefined) {
|
||||
updates.verified = Boolean(data.verified);
|
||||
}
|
||||
|
||||
if (data.confidence !== undefined) {
|
||||
const confidence = Number(data.confidence);
|
||||
if (isNaN(confidence) || confidence < 0 || confidence > 1) {
|
||||
throw new ValidationError('Confidence must be a number between 0 and 1', 'confidence');
|
||||
}
|
||||
updates.confidence = confidence;
|
||||
}
|
||||
|
||||
if (data.master_exchange_id !== undefined) {
|
||||
if (typeof data.master_exchange_id !== 'string') {
|
||||
throw new ValidationError('Master exchange ID must be a string', 'master_exchange_id');
|
||||
}
|
||||
updates.master_exchange_id = data.master_exchange_id.trim();
|
||||
}
|
||||
|
||||
if (Object.keys(updates).length === 0) {
|
||||
throw new ValidationError('No valid fields to update');
|
||||
}
|
||||
|
||||
return updates;
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue