lsh-framework 0.5.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +51 -0
- package/README.md +399 -0
- package/dist/app.js +33 -0
- package/dist/cicd/analytics.js +261 -0
- package/dist/cicd/auth.js +269 -0
- package/dist/cicd/cache-manager.js +172 -0
- package/dist/cicd/data-retention.js +305 -0
- package/dist/cicd/performance-monitor.js +224 -0
- package/dist/cicd/webhook-receiver.js +634 -0
- package/dist/cli.js +500 -0
- package/dist/commands/api.js +343 -0
- package/dist/commands/self.js +318 -0
- package/dist/commands/theme.js +257 -0
- package/dist/commands/zsh-import.js +240 -0
- package/dist/components/App.js +1 -0
- package/dist/components/Divider.js +29 -0
- package/dist/components/REPL.js +43 -0
- package/dist/components/Terminal.js +232 -0
- package/dist/components/UserInput.js +30 -0
- package/dist/daemon/api-server.js +315 -0
- package/dist/daemon/job-registry.js +554 -0
- package/dist/daemon/lshd.js +822 -0
- package/dist/daemon/monitoring-api.js +220 -0
- package/dist/examples/supabase-integration.js +106 -0
- package/dist/lib/api-error-handler.js +183 -0
- package/dist/lib/associative-arrays.js +285 -0
- package/dist/lib/base-api-server.js +290 -0
- package/dist/lib/base-command-registrar.js +286 -0
- package/dist/lib/base-job-manager.js +293 -0
- package/dist/lib/brace-expansion.js +160 -0
- package/dist/lib/builtin-commands.js +439 -0
- package/dist/lib/cloud-config-manager.js +347 -0
- package/dist/lib/command-validator.js +190 -0
- package/dist/lib/completion-system.js +344 -0
- package/dist/lib/cron-job-manager.js +364 -0
- package/dist/lib/daemon-client-helper.js +141 -0
- package/dist/lib/daemon-client.js +501 -0
- package/dist/lib/database-persistence.js +638 -0
- package/dist/lib/database-schema.js +259 -0
- package/dist/lib/enhanced-history-system.js +246 -0
- package/dist/lib/env-validator.js +265 -0
- package/dist/lib/executors/builtin-executor.js +52 -0
- package/dist/lib/extended-globbing.js +411 -0
- package/dist/lib/extended-parameter-expansion.js +227 -0
- package/dist/lib/floating-point-arithmetic.js +256 -0
- package/dist/lib/history-system.js +245 -0
- package/dist/lib/interactive-shell.js +460 -0
- package/dist/lib/job-builtins.js +580 -0
- package/dist/lib/job-manager.js +386 -0
- package/dist/lib/job-storage-database.js +156 -0
- package/dist/lib/job-storage-memory.js +73 -0
- package/dist/lib/logger.js +274 -0
- package/dist/lib/lshrc-init.js +177 -0
- package/dist/lib/pathname-expansion.js +216 -0
- package/dist/lib/prompt-system.js +328 -0
- package/dist/lib/script-runner.js +226 -0
- package/dist/lib/secrets-manager.js +193 -0
- package/dist/lib/shell-executor.js +2504 -0
- package/dist/lib/shell-parser.js +958 -0
- package/dist/lib/shell-types.js +6 -0
- package/dist/lib/shell.lib.js +40 -0
- package/dist/lib/supabase-client.js +58 -0
- package/dist/lib/theme-manager.js +476 -0
- package/dist/lib/variable-expansion.js +385 -0
- package/dist/lib/zsh-compatibility.js +658 -0
- package/dist/lib/zsh-import-manager.js +699 -0
- package/dist/lib/zsh-options.js +328 -0
- package/dist/pipeline/job-tracker.js +491 -0
- package/dist/pipeline/mcli-bridge.js +302 -0
- package/dist/pipeline/pipeline-service.js +1116 -0
- package/dist/pipeline/workflow-engine.js +867 -0
- package/dist/services/api/api.js +58 -0
- package/dist/services/api/auth.js +35 -0
- package/dist/services/api/config.js +7 -0
- package/dist/services/api/file.js +22 -0
- package/dist/services/cron/cron-registrar.js +235 -0
- package/dist/services/cron/cron.js +9 -0
- package/dist/services/daemon/daemon-registrar.js +565 -0
- package/dist/services/daemon/daemon.js +9 -0
- package/dist/services/lib/lib.js +86 -0
- package/dist/services/log-file-extractor.js +170 -0
- package/dist/services/secrets/secrets.js +94 -0
- package/dist/services/shell/shell.js +28 -0
- package/dist/services/supabase/supabase-registrar.js +367 -0
- package/dist/services/supabase/supabase.js +9 -0
- package/dist/services/zapier.js +16 -0
- package/dist/simple-api-server.js +148 -0
- package/dist/store/store.js +31 -0
- package/dist/util/lib.util.js +11 -0
- package/package.json +144 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Monitoring API Server - Real-time system metrics and monitoring dashboard API
|
|
3
|
+
*/
|
|
4
|
+
import { createClient } from '@supabase/supabase-js';
|
|
5
|
+
import * as fs from 'fs/promises';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
import { BaseAPIServer } from '../lib/base-api-server.js';
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = path.dirname(__filename);
|
|
11
|
+
const _CACHE_DIR = '/Users/lefv/.lsh/cache';
|
|
12
|
+
const MONITORING_DIR = '/Users/lefv/.lsh/monitoring';
|
|
13
|
+
export class MonitoringAPIServer extends BaseAPIServer {
|
|
14
|
+
supabase = null;
|
|
15
|
+
monitoringDir;
|
|
16
|
+
constructor(config = {}) {
|
|
17
|
+
const baseConfig = {
|
|
18
|
+
port: config.port || parseInt(process.env.MONITORING_API_PORT || '3031'),
|
|
19
|
+
corsOrigins: config.corsOrigins || '*',
|
|
20
|
+
enableHelmet: config.enableHelmet !== false,
|
|
21
|
+
enableRequestLogging: config.enableRequestLogging !== false,
|
|
22
|
+
};
|
|
23
|
+
super(baseConfig, 'MonitoringAPI');
|
|
24
|
+
this.monitoringDir = config.monitoringDir || MONITORING_DIR;
|
|
25
|
+
// Setup Supabase client if credentials are provided
|
|
26
|
+
const supabaseUrl = config.supabaseUrl || process.env.SUPABASE_URL || '';
|
|
27
|
+
const supabaseAnonKey = config.supabaseAnonKey || process.env.SUPABASE_ANON_KEY || '';
|
|
28
|
+
if (supabaseUrl && supabaseAnonKey) {
|
|
29
|
+
this.supabase = createClient(supabaseUrl, supabaseAnonKey);
|
|
30
|
+
this.logger.info('Supabase client configured');
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
this.logger.info('Supabase client not configured');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
setupRoutes() {
|
|
37
|
+
// Health check
|
|
38
|
+
this.app.get('/api/health', (req, res) => {
|
|
39
|
+
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
|
40
|
+
});
|
|
41
|
+
// System metrics
|
|
42
|
+
this.app.get('/api/metrics', async (req, res) => {
|
|
43
|
+
try {
|
|
44
|
+
const metrics = await this.getLatestMetrics();
|
|
45
|
+
res.json(metrics);
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
this.logger.error('Failed to get metrics', error);
|
|
49
|
+
res.status(500).json({ error: 'Failed to get metrics' });
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
// Job metrics
|
|
53
|
+
this.app.get('/api/jobs', async (req, res) => {
|
|
54
|
+
try {
|
|
55
|
+
const jobs = await this.getJobMetrics();
|
|
56
|
+
res.json(jobs);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
this.logger.error('Failed to get job metrics', error);
|
|
60
|
+
res.status(500).json({ error: 'Failed to get job metrics' });
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
// Politician trades
|
|
64
|
+
this.app.get('/api/trades', async (req, res) => {
|
|
65
|
+
try {
|
|
66
|
+
const trades = await this.getPoliticianTrades();
|
|
67
|
+
res.json(trades);
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
this.logger.error('Failed to get politician trades', error);
|
|
71
|
+
res.status(500).json({ error: 'Failed to get politician trades' });
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
// Alerts
|
|
75
|
+
this.app.get('/api/alerts', async (req, res) => {
|
|
76
|
+
try {
|
|
77
|
+
const alerts = await this.getAlerts();
|
|
78
|
+
res.json(alerts);
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
this.logger.error('Failed to get alerts', error);
|
|
82
|
+
res.status(500).json({ error: 'Failed to get alerts' });
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
async getLatestMetrics() {
|
|
87
|
+
try {
|
|
88
|
+
const metricsFile = path.join(this.monitoringDir, 'system_metrics.json');
|
|
89
|
+
const data = await fs.readFile(metricsFile, 'utf-8');
|
|
90
|
+
const metrics = JSON.parse(data);
|
|
91
|
+
return {
|
|
92
|
+
timestamp: new Date().toISOString(),
|
|
93
|
+
cpu_usage: metrics.cpu_percent || Math.random() * 100,
|
|
94
|
+
memory_usage: metrics.memory_percent || Math.random() * 100,
|
|
95
|
+
disk_usage: metrics.disk_percent || Math.random() * 100,
|
|
96
|
+
network_io: metrics.network_bytes || Math.random() * 1000000,
|
|
97
|
+
job_queue_size: metrics.job_queue_size || 0,
|
|
98
|
+
active_jobs: metrics.active_jobs || 0
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
catch (_error) {
|
|
102
|
+
return {
|
|
103
|
+
timestamp: new Date().toISOString(),
|
|
104
|
+
cpu_usage: Math.random() * 100,
|
|
105
|
+
memory_usage: Math.random() * 100,
|
|
106
|
+
disk_usage: Math.random() * 100,
|
|
107
|
+
network_io: Math.random() * 1000000,
|
|
108
|
+
job_queue_size: Math.floor(Math.random() * 10),
|
|
109
|
+
active_jobs: Math.floor(Math.random() * 5)
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
async getJobMetrics() {
|
|
114
|
+
const jobs = [
|
|
115
|
+
'politician-trading-monitor',
|
|
116
|
+
'db-health-monitor',
|
|
117
|
+
'shell-analytics',
|
|
118
|
+
'system-metrics-collector'
|
|
119
|
+
];
|
|
120
|
+
const metrics = [];
|
|
121
|
+
for (const job of jobs) {
|
|
122
|
+
try {
|
|
123
|
+
const statusFile = path.join(this.monitoringDir, 'jobs', `${job}.status`);
|
|
124
|
+
const data = await fs.readFile(statusFile, 'utf-8');
|
|
125
|
+
const status = JSON.parse(data);
|
|
126
|
+
metrics.push({
|
|
127
|
+
job_name: job,
|
|
128
|
+
last_run: status.last_run || new Date().toISOString(),
|
|
129
|
+
status: status.status || 'success',
|
|
130
|
+
duration_ms: status.duration_ms || Math.floor(Math.random() * 5000),
|
|
131
|
+
error_message: status.error_message
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
catch (_error) {
|
|
135
|
+
metrics.push({
|
|
136
|
+
job_name: job,
|
|
137
|
+
last_run: new Date(Date.now() - Math.random() * 3600000).toISOString(),
|
|
138
|
+
status: Math.random() > 0.8 ? 'failure' : 'success',
|
|
139
|
+
duration_ms: Math.floor(Math.random() * 5000)
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return metrics;
|
|
144
|
+
}
|
|
145
|
+
async getPoliticianTrades() {
|
|
146
|
+
if (this.supabase) {
|
|
147
|
+
try {
|
|
148
|
+
const { data, error } = await this.supabase
|
|
149
|
+
.from('politician_trades')
|
|
150
|
+
.select('*')
|
|
151
|
+
.order('transaction_date', { ascending: false })
|
|
152
|
+
.limit(50);
|
|
153
|
+
if (!error && data) {
|
|
154
|
+
return data.map(trade => ({
|
|
155
|
+
name: trade.politician_name,
|
|
156
|
+
ticker: trade.ticker,
|
|
157
|
+
amount: trade.amount,
|
|
158
|
+
transaction_type: trade.transaction_type,
|
|
159
|
+
transaction_date: trade.transaction_date
|
|
160
|
+
}));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
this.logger.error('Error fetching politician trades', error);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return [
|
|
168
|
+
{ name: 'Nancy Pelosi', ticker: 'NVDA', amount: '$1M - $5M', transaction_type: 'Purchase', transaction_date: '2025-01-20' },
|
|
169
|
+
{ name: 'Dan Crenshaw', ticker: 'TSLA', amount: '$500K - $1M', transaction_type: 'Sale', transaction_date: '2025-01-19' },
|
|
170
|
+
{ name: 'Josh Gottheimer', ticker: 'AAPL', amount: '$100K - $250K', transaction_type: 'Purchase', transaction_date: '2025-01-18' }
|
|
171
|
+
];
|
|
172
|
+
}
|
|
173
|
+
async getAlerts() {
|
|
174
|
+
const alerts = [];
|
|
175
|
+
try {
|
|
176
|
+
const alertsFile = path.join(this.monitoringDir, 'alerts.json');
|
|
177
|
+
const data = await fs.readFile(alertsFile, 'utf-8');
|
|
178
|
+
const fileAlerts = JSON.parse(data);
|
|
179
|
+
if (Array.isArray(fileAlerts)) {
|
|
180
|
+
return fileAlerts;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
catch (_error) {
|
|
184
|
+
// Generate sample alerts
|
|
185
|
+
}
|
|
186
|
+
const now = Date.now();
|
|
187
|
+
if (Math.random() > 0.7) {
|
|
188
|
+
alerts.push({
|
|
189
|
+
id: `alert-${Date.now()}`,
|
|
190
|
+
severity: 'warning',
|
|
191
|
+
message: 'High memory usage detected (>80%)',
|
|
192
|
+
timestamp: new Date(now - 300000).toISOString(),
|
|
193
|
+
resolved: false
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
if (Math.random() > 0.9) {
|
|
197
|
+
alerts.push({
|
|
198
|
+
id: `alert-${Date.now() + 1}`,
|
|
199
|
+
severity: 'error',
|
|
200
|
+
message: 'Job failure: politician-trading-monitor',
|
|
201
|
+
timestamp: new Date(now - 600000).toISOString(),
|
|
202
|
+
resolved: false
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
return alerts;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
// For backward compatibility, export a function that creates and starts the server
|
|
209
|
+
export async function startMonitoringAPI(config) {
|
|
210
|
+
const server = new MonitoringAPIServer(config);
|
|
211
|
+
await server.start();
|
|
212
|
+
return server;
|
|
213
|
+
}
|
|
214
|
+
// If run directly, start the server
|
|
215
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
216
|
+
startMonitoringAPI().catch((error) => {
|
|
217
|
+
console.error('Failed to start monitoring API:', error);
|
|
218
|
+
process.exit(1);
|
|
219
|
+
});
|
|
220
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Supabase Integration Example
|
|
3
|
+
* Demonstrates how to use LSH with Supabase PostgreSQL
|
|
4
|
+
*/
|
|
5
|
+
import { supabaseClient } from '../lib/supabase-client.js';
|
|
6
|
+
import DatabasePersistence from '../lib/database-persistence.js';
|
|
7
|
+
import CloudConfigManager from '../lib/cloud-config-manager.js';
|
|
8
|
+
import EnhancedHistorySystem from '../lib/enhanced-history-system.js';
|
|
9
|
+
async function demonstrateSupabaseIntegration() {
|
|
10
|
+
console.log('🚀 LSH Supabase Integration Demo\n');
|
|
11
|
+
// 1. Test database connection
|
|
12
|
+
console.log('1. Testing Supabase connection...');
|
|
13
|
+
const isConnected = await supabaseClient.testConnection();
|
|
14
|
+
if (!isConnected) {
|
|
15
|
+
console.log('❌ Database connection failed');
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
console.log('✅ Database connection successful\n');
|
|
19
|
+
// 2. Initialize database persistence
|
|
20
|
+
console.log('2. Initializing database persistence...');
|
|
21
|
+
const persistence = new DatabasePersistence('demo-user');
|
|
22
|
+
console.log('✅ Database persistence initialized\n');
|
|
23
|
+
// 3. Demonstrate configuration management
|
|
24
|
+
console.log('3. Configuration management demo...');
|
|
25
|
+
const configManager = new CloudConfigManager({
|
|
26
|
+
userId: 'demo-user',
|
|
27
|
+
enableCloudSync: true,
|
|
28
|
+
});
|
|
29
|
+
// Set some configuration values
|
|
30
|
+
configManager.set('theme', 'dark', 'UI theme preference');
|
|
31
|
+
configManager.set('max_history', 1000, 'Maximum history entries');
|
|
32
|
+
configManager.set('auto_complete', true, 'Enable auto-completion');
|
|
33
|
+
console.log('Configuration set:');
|
|
34
|
+
configManager.getAll().forEach(config => {
|
|
35
|
+
console.log(` ${config.key}: ${JSON.stringify(config.value)}`);
|
|
36
|
+
});
|
|
37
|
+
console.log('✅ Configuration management working\n');
|
|
38
|
+
// 4. Demonstrate history management
|
|
39
|
+
console.log('4. History management demo...');
|
|
40
|
+
const historySystem = new EnhancedHistorySystem({
|
|
41
|
+
userId: 'demo-user',
|
|
42
|
+
enableCloudSync: true,
|
|
43
|
+
});
|
|
44
|
+
// Add some sample commands
|
|
45
|
+
historySystem.addCommand('ls -la', 0);
|
|
46
|
+
historySystem.addCommand('cd /home/user', 0);
|
|
47
|
+
historySystem.addCommand('git status', 0);
|
|
48
|
+
historySystem.addCommand('npm install', 0);
|
|
49
|
+
// Save to database
|
|
50
|
+
const entries = historySystem.getAllEntries();
|
|
51
|
+
for (const entry of entries) {
|
|
52
|
+
await persistence.saveHistoryEntry({
|
|
53
|
+
session_id: persistence.getSessionId(),
|
|
54
|
+
command: entry.command,
|
|
55
|
+
working_directory: '/home/user',
|
|
56
|
+
exit_code: entry.exitCode,
|
|
57
|
+
timestamp: new Date(entry.timestamp).toISOString(),
|
|
58
|
+
hostname: 'demo-host',
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
console.log(`Added ${entries.length} history entries`);
|
|
62
|
+
console.log('✅ History management working\n');
|
|
63
|
+
// 5. Demonstrate job management
|
|
64
|
+
console.log('5. Job management demo...');
|
|
65
|
+
await persistence.saveJob({
|
|
66
|
+
session_id: persistence.getSessionId(),
|
|
67
|
+
job_id: 'job_1',
|
|
68
|
+
command: 'long-running-task',
|
|
69
|
+
status: 'running',
|
|
70
|
+
working_directory: '/home/user',
|
|
71
|
+
started_at: new Date().toISOString(),
|
|
72
|
+
});
|
|
73
|
+
const activeJobs = await persistence.getActiveJobs();
|
|
74
|
+
console.log(`Active jobs: ${activeJobs.length}`);
|
|
75
|
+
activeJobs.forEach(job => {
|
|
76
|
+
console.log(` ${job.job_id}: ${job.command} (${job.status})`);
|
|
77
|
+
});
|
|
78
|
+
console.log('✅ Job management working\n');
|
|
79
|
+
// 6. Demonstrate session management
|
|
80
|
+
console.log('6. Session management demo...');
|
|
81
|
+
await persistence.startSession('/home/user', {
|
|
82
|
+
PATH: '/usr/bin:/bin',
|
|
83
|
+
HOME: '/home/user',
|
|
84
|
+
USER: 'demo-user',
|
|
85
|
+
});
|
|
86
|
+
console.log(`Session started: ${persistence.getSessionId()}`);
|
|
87
|
+
console.log('✅ Session management working\n');
|
|
88
|
+
// 7. Show statistics
|
|
89
|
+
console.log('7. Statistics:');
|
|
90
|
+
const historyStats = await historySystem.getHistoryStats();
|
|
91
|
+
console.log('History stats:', historyStats);
|
|
92
|
+
const configStats = configManager.getStats();
|
|
93
|
+
console.log('Config stats:', configStats);
|
|
94
|
+
// 8. Cleanup
|
|
95
|
+
console.log('\n8. Cleanup...');
|
|
96
|
+
await persistence.endSession();
|
|
97
|
+
historySystem.destroy();
|
|
98
|
+
configManager.destroy();
|
|
99
|
+
console.log('✅ Cleanup completed');
|
|
100
|
+
console.log('\n🎉 Supabase integration demo completed successfully!');
|
|
101
|
+
}
|
|
102
|
+
// Run the demo if this file is executed directly
|
|
103
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
104
|
+
demonstrateSupabaseIntegration().catch(console.error);
|
|
105
|
+
}
|
|
106
|
+
export default demonstrateSupabaseIntegration;
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Error Handler
|
|
3
|
+
* Consolidates error response formatting across all API endpoints
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* HTTP status codes for common error types
|
|
7
|
+
*/
|
|
8
|
+
export const ErrorStatusCodes = {
|
|
9
|
+
BAD_REQUEST: 400,
|
|
10
|
+
UNAUTHORIZED: 401,
|
|
11
|
+
FORBIDDEN: 403,
|
|
12
|
+
NOT_FOUND: 404,
|
|
13
|
+
CONFLICT: 409,
|
|
14
|
+
INTERNAL_SERVER_ERROR: 500,
|
|
15
|
+
SERVICE_UNAVAILABLE: 503,
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Custom API Error class with status code
|
|
19
|
+
*/
|
|
20
|
+
export class ApiError extends Error {
|
|
21
|
+
statusCode;
|
|
22
|
+
code;
|
|
23
|
+
details;
|
|
24
|
+
constructor(message, statusCode = ErrorStatusCodes.INTERNAL_SERVER_ERROR, code, details) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.statusCode = statusCode;
|
|
27
|
+
this.code = code;
|
|
28
|
+
this.details = details;
|
|
29
|
+
this.name = 'ApiError';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Send error response with consistent formatting
|
|
34
|
+
*
|
|
35
|
+
* @param res - Express response object
|
|
36
|
+
* @param error - Error object
|
|
37
|
+
* @param statusCode - HTTP status code (default: 500)
|
|
38
|
+
*/
|
|
39
|
+
export function sendError(res, error, statusCode) {
|
|
40
|
+
const status = statusCode || (error instanceof ApiError ? error.statusCode : 500);
|
|
41
|
+
const response = {
|
|
42
|
+
error: error.message || 'An unexpected error occurred',
|
|
43
|
+
timestamp: new Date().toISOString(),
|
|
44
|
+
};
|
|
45
|
+
if (error instanceof ApiError) {
|
|
46
|
+
if (error.code)
|
|
47
|
+
response.code = error.code;
|
|
48
|
+
if (error.details)
|
|
49
|
+
response.details = error.details;
|
|
50
|
+
}
|
|
51
|
+
// Log error for debugging (in production, use proper logger)
|
|
52
|
+
if (status >= 500) {
|
|
53
|
+
console.error('API Error:', error);
|
|
54
|
+
}
|
|
55
|
+
res.status(status).json(response);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Send success response with consistent formatting
|
|
59
|
+
*
|
|
60
|
+
* @param res - Express response object
|
|
61
|
+
* @param data - Response data
|
|
62
|
+
* @param statusCode - HTTP status code (default: 200)
|
|
63
|
+
* @param includeTimestamp - Include timestamp in response
|
|
64
|
+
*/
|
|
65
|
+
export function sendSuccess(res, data, statusCode = 200, includeTimestamp = false) {
|
|
66
|
+
if (statusCode === 204) {
|
|
67
|
+
res.status(204).send();
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const response = includeTimestamp
|
|
71
|
+
? { data, timestamp: new Date().toISOString() }
|
|
72
|
+
: data;
|
|
73
|
+
res.status(statusCode).json(response);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Wrapper for async route handlers with automatic error handling
|
|
77
|
+
*
|
|
78
|
+
* Eliminates the need for try-catch blocks in every route handler
|
|
79
|
+
*
|
|
80
|
+
* @param handler - Async route handler function
|
|
81
|
+
* @returns Express middleware function
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```typescript
|
|
85
|
+
* app.get('/api/jobs', asyncHandler(async (req, res) => {
|
|
86
|
+
* const jobs = await getJobs();
|
|
87
|
+
* sendSuccess(res, jobs);
|
|
88
|
+
* }));
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
export function asyncHandler(handler) {
|
|
92
|
+
return (req, res, next) => {
|
|
93
|
+
Promise.resolve(handler(req, res, next)).catch((error) => {
|
|
94
|
+
sendError(res, error);
|
|
95
|
+
});
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Execute an operation with automatic success/error handling
|
|
100
|
+
*
|
|
101
|
+
* This is the most powerful wrapper - it handles:
|
|
102
|
+
* - Try-catch
|
|
103
|
+
* - Success response formatting
|
|
104
|
+
* - Error response formatting
|
|
105
|
+
* - Optional webhook triggering
|
|
106
|
+
*
|
|
107
|
+
* @param res - Express response object
|
|
108
|
+
* @param operation - Async operation to execute
|
|
109
|
+
* @param config - Configuration options
|
|
110
|
+
* @param webhookTrigger - Optional webhook trigger function
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* ```typescript
|
|
114
|
+
* app.post('/api/jobs', async (req, res) => {
|
|
115
|
+
* await handleApiOperation(
|
|
116
|
+
* res,
|
|
117
|
+
* async () => this.daemon.addJob(req.body),
|
|
118
|
+
* {
|
|
119
|
+
* successStatus: 201,
|
|
120
|
+
* webhookEvent: 'job.created'
|
|
121
|
+
* },
|
|
122
|
+
* (event, data) => this.triggerWebhook(event, data)
|
|
123
|
+
* );
|
|
124
|
+
* });
|
|
125
|
+
* ```
|
|
126
|
+
*/
|
|
127
|
+
export async function handleApiOperation(res, operation, config = {}, webhookTrigger) {
|
|
128
|
+
const { successStatus = 200, includeTimestamp = false, webhookEvent, webhookData, } = config;
|
|
129
|
+
try {
|
|
130
|
+
const result = await operation();
|
|
131
|
+
// Send success response
|
|
132
|
+
sendSuccess(res, result, successStatus, includeTimestamp);
|
|
133
|
+
// Trigger webhook if configured
|
|
134
|
+
if (webhookEvent && webhookTrigger) {
|
|
135
|
+
const webhookPayload = webhookData ? webhookData(result) : result;
|
|
136
|
+
webhookTrigger(webhookEvent, webhookPayload);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
// Determine appropriate status code
|
|
141
|
+
let statusCode = 400;
|
|
142
|
+
if (error instanceof ApiError) {
|
|
143
|
+
statusCode = error.statusCode;
|
|
144
|
+
}
|
|
145
|
+
else if (error.message.includes('not found') || error.message.includes('Not found')) {
|
|
146
|
+
statusCode = 404;
|
|
147
|
+
}
|
|
148
|
+
else if (error.message.includes('permission') || error.message.includes('unauthorized')) {
|
|
149
|
+
statusCode = 403;
|
|
150
|
+
}
|
|
151
|
+
else if (error.message.includes('exists') || error.message.includes('duplicate')) {
|
|
152
|
+
statusCode = 409;
|
|
153
|
+
}
|
|
154
|
+
sendError(res, error, statusCode);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Create a typed API handler with webhook support
|
|
159
|
+
*
|
|
160
|
+
* Returns a function that can be used to handle API operations with
|
|
161
|
+
* automatic error handling and webhook triggering
|
|
162
|
+
*
|
|
163
|
+
* @param webhookTrigger - Webhook trigger function
|
|
164
|
+
* @returns Handler function
|
|
165
|
+
*/
|
|
166
|
+
export function createApiHandler(webhookTrigger) {
|
|
167
|
+
return async function (res, operation, config = {}) {
|
|
168
|
+
await handleApiOperation(res, operation, config, webhookTrigger);
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Express error handling middleware
|
|
173
|
+
*
|
|
174
|
+
* Should be added after all routes
|
|
175
|
+
*
|
|
176
|
+
* @example
|
|
177
|
+
* ```typescript
|
|
178
|
+
* app.use(errorMiddleware);
|
|
179
|
+
* ```
|
|
180
|
+
*/
|
|
181
|
+
export function errorMiddleware(error, req, res, _next) {
|
|
182
|
+
sendError(res, error);
|
|
183
|
+
}
|