tide-commander 1.87.0 → 1.89.0
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/dist/assets/{BossLogsModal-S3Rke-8g.js → BossLogsModal-BK6N5fG2.js} +1 -1
- package/dist/assets/{BossSpawnModal-BjWGNCnz.js → BossSpawnModal-BTy-lus4.js} +1 -1
- package/dist/assets/{ControlsModal-6yfU0XjZ.js → ControlsModal-B4MhaF1V.js} +1 -1
- package/dist/assets/{DockerLogsModal-CYq0hNz6.js → DockerLogsModal-C33dAwy1.js} +1 -1
- package/dist/assets/{EmbeddedEditor-ZBdqRDqm.js → EmbeddedEditor-BfjjT-GF.js} +1 -1
- package/dist/assets/{GmailOAuthSetup-BcV5jAse.js → GmailOAuthSetup-TQyjHs3_.js} +1 -1
- package/dist/assets/{GoogleOAuthSetup-DyUW_STE.js → GoogleOAuthSetup-DAIzYKy8.js} +1 -1
- package/dist/assets/{IframeModal-D9A3dUUc.js → IframeModal-g8tC4aah.js} +1 -1
- package/dist/assets/IntegrationsPanel-CuKr7702.js +2 -0
- package/dist/assets/{LogViewerModal-BWkbY7wa.js → LogViewerModal-DO45Kea0.js} +1 -1
- package/dist/assets/{MonitoringModal-AZzokZAZ.js → MonitoringModal-OIwmagj2.js} +1 -1
- package/dist/assets/{PM2LogsModal-q98eiBfq.js → PM2LogsModal-BRQzSiFN.js} +1 -1
- package/dist/assets/{RestoreArchivedAreaModal-CTxRP2qE.js → RestoreArchivedAreaModal-CBRN9Xpb.js} +1 -1
- package/dist/assets/{Scene2DCanvas-C11dztp1.js → Scene2DCanvas-4J4ZefT6.js} +1 -1
- package/dist/assets/{SceneManager-CsW9MYrD.js → SceneManager-DZsJcYvW.js} +1 -1
- package/dist/assets/{SkillsPanel-BeZr9w6E.js → SkillsPanel-DHk7h3Ja.js} +1 -1
- package/dist/assets/SlackMultiInstanceSetup-Dp1q2zM1.js +2 -0
- package/dist/assets/{SpawnModal-DY_KM6lX.js → SpawnModal-CfozYMNI.js} +1 -1
- package/dist/assets/{SubordinateAssignmentModal-D6RvjGX9.js → SubordinateAssignmentModal-BBfbpVUr.js} +1 -1
- package/dist/assets/{TriggerManagerPanel-BmqjXv9T.js → TriggerManagerPanel-DQw9nt1r.js} +2 -2
- package/dist/assets/{WorkflowEditorPanel-Rd5ZjJmt.js → WorkflowEditorPanel-BM2ec8CS.js} +1 -1
- package/dist/assets/{index-DSvJOrb7.js → index-BiAZinYH.js} +2 -2
- package/dist/assets/{index-BYVHgVEo.js → index-BqbR55dr.js} +1 -1
- package/dist/assets/{index-DRGyDtmm.js → index-CcSJA57k.js} +1 -1
- package/dist/assets/{index-BtJyOo4p.js → index-DNEUJDeO.js} +1 -1
- package/dist/assets/{index-BoORE9Q1.js → index-DY9w7IcH.js} +1 -1
- package/dist/assets/{index-DHHRkTG1.js → index-bcwTXJ6F.js} +1 -1
- package/dist/assets/index-fZfyvIUZ.js +2 -0
- package/dist/assets/{index-BxaEkSIx.js → index-jXkaBxIq.js} +3 -3
- package/dist/assets/index-xEvpFBA8.js +8 -0
- package/dist/assets/main-Bw5ZddEN.css +1 -0
- package/dist/assets/main-D-YFCprA.js +213 -0
- package/dist/assets/{web-D3zCwsS9.js → web-BrBkKQlr.js} +1 -1
- package/dist/assets/{web-DS0FHmg8.js → web-DCu3NTho.js} +1 -1
- package/dist/assets/{web-DEq3Te_H.js → web-DX588C-g.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/src/packages/server/data/builtin-skills/explore-database.js +175 -0
- package/dist/src/packages/server/data/builtin-skills/index.js +2 -0
- package/dist/src/packages/server/data/event-queries.js +2 -0
- package/dist/src/packages/server/data/index.js +56 -2
- package/dist/src/packages/server/data/migrations/006_slack_messages_integration_instance.sql +9 -0
- package/dist/src/packages/server/index.js +2 -1
- package/dist/src/packages/server/integrations/gmail/gmail-trigger-handler.js +9 -1
- package/dist/src/packages/server/integrations/slack/index.js +65 -19
- package/dist/src/packages/server/integrations/slack/slack-client.js +44 -602
- package/dist/src/packages/server/integrations/slack/slack-config.js +229 -29
- package/dist/src/packages/server/integrations/slack/slack-instance-manifest.js +150 -0
- package/dist/src/packages/server/integrations/slack/slack-instance.js +801 -0
- package/dist/src/packages/server/integrations/slack/slack-polling-client.js +522 -0
- package/dist/src/packages/server/integrations/slack/slack-routes.js +243 -24
- package/dist/src/packages/server/integrations/slack/slack-trigger-handler.js +53 -20
- package/dist/src/packages/server/integrations/slack/slack-watermark-store.js +124 -0
- package/dist/src/packages/server/integrations/whatsapp/index.js +5 -4
- package/dist/src/packages/server/integrations/whatsapp/whatsapp-client.js +10 -0
- package/dist/src/packages/server/integrations/whatsapp/whatsapp-routes.js +68 -0
- package/dist/src/packages/server/integrations/whatsapp/whatsapp-trigger-handler.js +127 -0
- package/dist/src/packages/server/routes/database.js +221 -0
- package/dist/src/packages/server/routes/files.js +219 -18
- package/dist/src/packages/server/routes/index.js +2 -0
- package/dist/src/packages/server/services/building-service.js +41 -0
- package/dist/src/packages/server/services/database-service.js +61 -9
- package/dist/src/packages/server/services/index.js +1 -0
- package/dist/src/packages/server/services/ssh-tunnel-service.js +255 -0
- package/dist/src/packages/server/websocket/handler.js +2 -1
- package/dist/src/packages/server/websocket/handlers/database-handler.js +35 -0
- package/package.json +3 -1
- package/dist/assets/IntegrationsPanel-CHaNJBJW.js +0 -2
- package/dist/assets/index-BOr_tbLK.js +0 -2
- package/dist/assets/index-Co7njQ0Q.js +0 -8
- package/dist/assets/main-BrZe9Zbd.js +0 -201
- package/dist/assets/main-kpU9m5LW.css +0 -1
|
@@ -198,6 +198,74 @@ export function createWhatsAppRoutes(ctx) {
|
|
|
198
198
|
res.status(502).json({ error: err instanceof Error ? err.message : String(err) });
|
|
199
199
|
}
|
|
200
200
|
});
|
|
201
|
+
// ─── POST /sessions/:sessionId/chats/:chatId/sync-messages — Force on-demand history sync for a chat ───
|
|
202
|
+
router.post('/sessions/:sessionId/chats/:chatId/sync-messages', async (req, res) => {
|
|
203
|
+
const built = getClient();
|
|
204
|
+
if ('error' in built) {
|
|
205
|
+
res.status(built.status).json({ error: built.error });
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const rawCount = typeof req.query.count === 'string' ? Number(req.query.count) : NaN;
|
|
209
|
+
const count = Number.isFinite(rawCount) && rawCount > 0 ? Math.floor(rawCount) : 50;
|
|
210
|
+
try {
|
|
211
|
+
const result = await built.client.syncChatMessages(req.params.sessionId, req.params.chatId, count);
|
|
212
|
+
res.json({ success: true, data: result });
|
|
213
|
+
}
|
|
214
|
+
catch (err) {
|
|
215
|
+
log.error(`WhatsApp syncChatMessages error: ${err}`);
|
|
216
|
+
res.status(502).json({ error: err instanceof Error ? err.message : String(err) });
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
// ─── POST /sessions/:sessionId/sync-contacts — Force address-book resync via Baileys app-state ───
|
|
220
|
+
router.post('/sessions/:sessionId/sync-contacts', async (req, res) => {
|
|
221
|
+
const built = getClient();
|
|
222
|
+
if ('error' in built) {
|
|
223
|
+
res.status(built.status).json({ error: built.error });
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
try {
|
|
227
|
+
const result = await built.client.syncContacts(req.params.sessionId);
|
|
228
|
+
res.json({ success: true, data: result });
|
|
229
|
+
}
|
|
230
|
+
catch (err) {
|
|
231
|
+
log.error(`WhatsApp syncContacts error: ${err}`);
|
|
232
|
+
res.status(502).json({ error: err instanceof Error ? err.message : String(err) });
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
// ─── GET /sessions/:sessionId/contacts — List contacts for a session ───
|
|
236
|
+
router.get('/sessions/:sessionId/contacts', async (req, res) => {
|
|
237
|
+
const built = getClient();
|
|
238
|
+
if ('error' in built) {
|
|
239
|
+
res.status(built.status).json({ error: built.error });
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
try {
|
|
243
|
+
const result = await built.client.getContacts(req.params.sessionId);
|
|
244
|
+
res.json({ success: true, data: result });
|
|
245
|
+
}
|
|
246
|
+
catch (err) {
|
|
247
|
+
log.error(`WhatsApp getContacts error: ${err}`);
|
|
248
|
+
res.status(502).json({ error: err instanceof Error ? err.message : String(err) });
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
// ─── GET /sessions/:sessionId/chats/:chatId/messages — Fetch recent messages for a chat ───
|
|
252
|
+
router.get('/sessions/:sessionId/chats/:chatId/messages', async (req, res) => {
|
|
253
|
+
const built = getClient();
|
|
254
|
+
if ('error' in built) {
|
|
255
|
+
res.status(built.status).json({ error: built.error });
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const rawLimit = typeof req.query.limit === 'string' ? Number(req.query.limit) : NaN;
|
|
259
|
+
const limit = Number.isFinite(rawLimit) && rawLimit > 0 ? Math.floor(rawLimit) : 50;
|
|
260
|
+
try {
|
|
261
|
+
const result = await built.client.getChatMessages(req.params.sessionId, req.params.chatId, limit);
|
|
262
|
+
res.json({ success: true, data: result });
|
|
263
|
+
}
|
|
264
|
+
catch (err) {
|
|
265
|
+
log.error(`WhatsApp getChatMessages error: ${err}`);
|
|
266
|
+
res.status(502).json({ error: err instanceof Error ? err.message : String(err) });
|
|
267
|
+
}
|
|
268
|
+
});
|
|
201
269
|
// ─── POST /send-message — Send a text message via Baileys ───
|
|
202
270
|
router.post('/send-message', async (req, res) => {
|
|
203
271
|
const body = (req.body ?? {});
|
|
@@ -13,6 +13,23 @@ import { WhatsAppWsClient } from './whatsapp-ws-client.js';
|
|
|
13
13
|
import { MessageDedupeCache } from './message-dedupe.js';
|
|
14
14
|
import { ContactNameCache } from './contact-name-cache.js';
|
|
15
15
|
import { WhatsAppClient } from './whatsapp-client.js';
|
|
16
|
+
import { createLogger } from '../../utils/logger.js';
|
|
17
|
+
const log = createLogger('WhatsAppTrigger');
|
|
18
|
+
// Module-level subscriber set fed by the in-process bridge whenever a normalized
|
|
19
|
+
// WhatsApp message is broadcast. The trigger-service-facing handler below
|
|
20
|
+
// registers its callback here, so subscribers stay attached even when the bridge
|
|
21
|
+
// restarts (e.g. on config change).
|
|
22
|
+
const triggerSubscribers = new Set();
|
|
23
|
+
function notifyTriggerSubscribers(payload) {
|
|
24
|
+
for (const cb of triggerSubscribers) {
|
|
25
|
+
try {
|
|
26
|
+
cb(payload);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// Subscriber errors must not break the bridge — swallow.
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
16
33
|
// Dedupe cache config. The upstream whatsapp-api fires BOTH `message` and
|
|
17
34
|
// `message_create` for the same Baileys event on inbound DMs (see
|
|
18
35
|
// providers/baileysSessionManager.js around the `messages.upsert` handler — it
|
|
@@ -105,6 +122,7 @@ export function createWhatsAppTriggerHandler(ctx) {
|
|
|
105
122
|
!!payload.from;
|
|
106
123
|
if (!needsEnrichment) {
|
|
107
124
|
ctx.broadcast({ type: 'whatsapp_message', payload });
|
|
125
|
+
notifyTriggerSubscribers(payload);
|
|
108
126
|
return;
|
|
109
127
|
}
|
|
110
128
|
void contactNames.lookup(msg.sessionId, payload.from).then((name) => {
|
|
@@ -112,9 +130,11 @@ export function createWhatsAppTriggerHandler(ctx) {
|
|
|
112
130
|
if (sanitized)
|
|
113
131
|
payload.fromName = sanitized;
|
|
114
132
|
ctx.broadcast({ type: 'whatsapp_message', payload });
|
|
133
|
+
notifyTriggerSubscribers(payload);
|
|
115
134
|
}, (err) => {
|
|
116
135
|
ctx.log.warn(`WhatsApp contact lookup failed: ${err}`);
|
|
117
136
|
ctx.broadcast({ type: 'whatsapp_message', payload });
|
|
137
|
+
notifyTriggerSubscribers(payload);
|
|
118
138
|
});
|
|
119
139
|
}
|
|
120
140
|
// Other event types (message_ack, group_join, group_leave, hello, error) are
|
|
@@ -216,6 +236,7 @@ function normalizeBaileysMessage(sessionId, eventName, data, ts, baseUrl) {
|
|
|
216
236
|
mediaType,
|
|
217
237
|
mediaUrl,
|
|
218
238
|
direction,
|
|
239
|
+
chatId: remoteJid ?? from,
|
|
219
240
|
};
|
|
220
241
|
}
|
|
221
242
|
function extractContent(envelope, message) {
|
|
@@ -355,3 +376,109 @@ export function sanitizeFromName(name, fromJid) {
|
|
|
355
376
|
}
|
|
356
377
|
return trimmed;
|
|
357
378
|
}
|
|
379
|
+
// Chat IDs that represent non-message channels (status updates, broadcast
|
|
380
|
+
// lists). The bridge surfaces them as messages because Baileys delivers
|
|
381
|
+
// status posts via `messages.upsert`, but they're not 1:1 / group chats and
|
|
382
|
+
// most triggers shouldn't fire on them.
|
|
383
|
+
function isStatusOrBroadcastChat(chatId) {
|
|
384
|
+
if (!chatId)
|
|
385
|
+
return false;
|
|
386
|
+
if (chatId === 'status@broadcast')
|
|
387
|
+
return true;
|
|
388
|
+
if (chatId.endsWith('@broadcast'))
|
|
389
|
+
return true;
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
// Drop presence/typing/group-system events that arrive through the bridge
|
|
393
|
+
// with no body and no media — they fire the agent for nothing. Emoji-only
|
|
394
|
+
// bodies and media-with-empty-caption are real content and pass through.
|
|
395
|
+
export function isEmptyContentMessage(msg) {
|
|
396
|
+
const bodyEmpty = !msg.body || msg.body.trim().length === 0;
|
|
397
|
+
const noMedia = !msg.mediaType && !msg.mediaUrl;
|
|
398
|
+
return bodyEmpty && noMedia;
|
|
399
|
+
}
|
|
400
|
+
let triggerUnsubscribe = null;
|
|
401
|
+
export const whatsappTriggerHandler = {
|
|
402
|
+
triggerType: 'whatsapp',
|
|
403
|
+
async startListening(onEvent) {
|
|
404
|
+
const cb = (msg) => {
|
|
405
|
+
onEvent({
|
|
406
|
+
source: 'whatsapp',
|
|
407
|
+
type: 'message',
|
|
408
|
+
data: msg,
|
|
409
|
+
timestamp: msg.timestamp,
|
|
410
|
+
});
|
|
411
|
+
};
|
|
412
|
+
triggerSubscribers.add(cb);
|
|
413
|
+
triggerUnsubscribe = () => triggerSubscribers.delete(cb);
|
|
414
|
+
},
|
|
415
|
+
async stopListening() {
|
|
416
|
+
if (triggerUnsubscribe) {
|
|
417
|
+
triggerUnsubscribe();
|
|
418
|
+
triggerUnsubscribe = null;
|
|
419
|
+
}
|
|
420
|
+
},
|
|
421
|
+
structuralMatch(trigger, event) {
|
|
422
|
+
const msg = event.data;
|
|
423
|
+
const cfg = trigger.config;
|
|
424
|
+
// Drop status updates and broadcast-list messages by default — they're
|
|
425
|
+
// surfaced through the same bridge as real messages but represent stories /
|
|
426
|
+
// broadcasts, not conversations. Opt-in via includeStatuses.
|
|
427
|
+
if (!cfg.includeStatuses && isStatusOrBroadcastChat(msg.chatId))
|
|
428
|
+
return false;
|
|
429
|
+
if (isEmptyContentMessage(msg)) {
|
|
430
|
+
log.debug(`whatsapp.trigger.skipped reason=empty_content session=${msg.sessionId}`);
|
|
431
|
+
return false;
|
|
432
|
+
}
|
|
433
|
+
if (cfg.direction && cfg.direction !== 'any' && msg.direction !== cfg.direction)
|
|
434
|
+
return false;
|
|
435
|
+
if (cfg.groupOnly && !msg.isGroup)
|
|
436
|
+
return false;
|
|
437
|
+
if (cfg.dmOnly && msg.isGroup)
|
|
438
|
+
return false;
|
|
439
|
+
if (cfg.sessionId && msg.sessionId !== cfg.sessionId)
|
|
440
|
+
return false;
|
|
441
|
+
if (cfg.fromFilter?.length) {
|
|
442
|
+
const fromLower = msg.from.toLowerCase();
|
|
443
|
+
if (!cfg.fromFilter.some(f => fromLower.includes(f.toLowerCase())))
|
|
444
|
+
return false;
|
|
445
|
+
}
|
|
446
|
+
if (cfg.bodyPattern) {
|
|
447
|
+
try {
|
|
448
|
+
if (!new RegExp(cfg.bodyPattern, 'i').test(msg.body))
|
|
449
|
+
return false;
|
|
450
|
+
}
|
|
451
|
+
catch {
|
|
452
|
+
return false;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
return true;
|
|
456
|
+
},
|
|
457
|
+
extractVariables(trigger, event) {
|
|
458
|
+
const msg = event.data;
|
|
459
|
+
void trigger;
|
|
460
|
+
return {
|
|
461
|
+
'whatsapp.from': msg.from,
|
|
462
|
+
'whatsapp.fromName': msg.fromName ?? '',
|
|
463
|
+
'whatsapp.body': msg.body,
|
|
464
|
+
'whatsapp.sessionId': msg.sessionId,
|
|
465
|
+
'whatsapp.chatId': msg.chatId,
|
|
466
|
+
'whatsapp.isGroup': String(msg.isGroup),
|
|
467
|
+
'whatsapp.groupName': msg.groupName ?? '',
|
|
468
|
+
'whatsapp.direction': msg.direction,
|
|
469
|
+
'whatsapp.mediaType': msg.mediaType ?? '',
|
|
470
|
+
'whatsapp.mediaUrl': msg.mediaUrl ?? '',
|
|
471
|
+
'whatsapp.timestamp': new Date(msg.timestamp).toISOString(),
|
|
472
|
+
};
|
|
473
|
+
},
|
|
474
|
+
formatEventForLLM(event) {
|
|
475
|
+
const msg = event.data;
|
|
476
|
+
const sender = msg.fromName ? `${msg.fromName} (${msg.from})` : msg.from;
|
|
477
|
+
const channel = msg.isGroup ? `group "${msg.groupName ?? msg.from}"` : 'DM';
|
|
478
|
+
const verb = msg.direction === 'outbound' ? 'sent' : 'received';
|
|
479
|
+
const media = msg.mediaType
|
|
480
|
+
? `\n[${msg.mediaType} attachment${msg.mediaUrl ? ` ${msg.mediaUrl}` : ''}]`
|
|
481
|
+
: '';
|
|
482
|
+
return `WhatsApp message ${verb} in ${channel}\nFrom: ${sender}\nSession: ${msg.sessionId}\nTime: ${new Date(msg.timestamp).toISOString()}${media}\n\n${msg.body}`;
|
|
483
|
+
},
|
|
484
|
+
};
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database Routes
|
|
3
|
+
* REST API endpoints for exploring database buildings.
|
|
4
|
+
*
|
|
5
|
+
* Mirrors the WebSocket database handler operations (test/list/schema/query)
|
|
6
|
+
* over plain HTTP so curl-based agent skills can use them.
|
|
7
|
+
*/
|
|
8
|
+
import { Router } from 'express';
|
|
9
|
+
import { databaseService, buildingService } from '../services/index.js';
|
|
10
|
+
import { createLogger } from '../utils/logger.js';
|
|
11
|
+
const log = createLogger('DatabaseRoutes');
|
|
12
|
+
const router = Router();
|
|
13
|
+
const MAX_QUERY_LIMIT = 10000;
|
|
14
|
+
const DEFAULT_QUERY_LIMIT = 1000;
|
|
15
|
+
function getDatabaseBuilding(buildingId) {
|
|
16
|
+
const building = buildingService.getBuilding(buildingId);
|
|
17
|
+
if (!building || building.type !== 'database' || !building.database) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
return building;
|
|
21
|
+
}
|
|
22
|
+
function getConnection(buildingId, connectionId) {
|
|
23
|
+
const building = getDatabaseBuilding(buildingId);
|
|
24
|
+
if (!building)
|
|
25
|
+
return null;
|
|
26
|
+
return building.database.connections.find(c => c.id === connectionId) || null;
|
|
27
|
+
}
|
|
28
|
+
function redactConnection(c) {
|
|
29
|
+
return {
|
|
30
|
+
id: c.id,
|
|
31
|
+
name: c.name,
|
|
32
|
+
engine: c.engine,
|
|
33
|
+
host: c.host,
|
|
34
|
+
port: c.port,
|
|
35
|
+
username: c.username,
|
|
36
|
+
database: c.database,
|
|
37
|
+
filepath: c.filepath,
|
|
38
|
+
ssl: c.ssl,
|
|
39
|
+
hasPassword: Boolean(c.password),
|
|
40
|
+
ssh: c.ssh
|
|
41
|
+
? { enabled: c.ssh.enabled, host: c.ssh.host, port: c.ssh.port, username: c.ssh.username }
|
|
42
|
+
: undefined,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
// GET /api/database/buildings - list all database buildings (redacted)
|
|
46
|
+
router.get('/buildings', (_req, res) => {
|
|
47
|
+
const buildings = buildingService.getBuildings()
|
|
48
|
+
.filter(b => b.type === 'database' && b.database)
|
|
49
|
+
.map(b => ({
|
|
50
|
+
id: b.id,
|
|
51
|
+
name: b.name,
|
|
52
|
+
activeConnectionId: b.database.activeConnectionId,
|
|
53
|
+
activeDatabase: b.database.activeDatabase,
|
|
54
|
+
connections: b.database.connections.map(redactConnection),
|
|
55
|
+
}));
|
|
56
|
+
res.json({ buildings });
|
|
57
|
+
});
|
|
58
|
+
// GET /api/database/buildings/:buildingId - one database building (redacted)
|
|
59
|
+
router.get('/buildings/:buildingId', (req, res) => {
|
|
60
|
+
const building = getDatabaseBuilding(String(req.params.buildingId));
|
|
61
|
+
if (!building) {
|
|
62
|
+
res.status(404).json({ error: 'Database building not found' });
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
res.json({
|
|
66
|
+
id: building.id,
|
|
67
|
+
name: building.name,
|
|
68
|
+
activeConnectionId: building.database.activeConnectionId,
|
|
69
|
+
activeDatabase: building.database.activeDatabase,
|
|
70
|
+
connections: building.database.connections.map(redactConnection),
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
// POST /api/database/buildings/:buildingId/connections/:connectionId/test
|
|
74
|
+
router.post('/buildings/:buildingId/connections/:connectionId/test', async (req, res) => {
|
|
75
|
+
const buildingId = String(req.params.buildingId);
|
|
76
|
+
const connectionId = String(req.params.connectionId);
|
|
77
|
+
const connection = getConnection(buildingId, connectionId);
|
|
78
|
+
if (!connection) {
|
|
79
|
+
res.status(404).json({ error: 'Connection not found' });
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
const result = await databaseService.testConnection(connection);
|
|
84
|
+
res.json(result);
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
88
|
+
log.error(`testConnection failed for ${buildingId}/${connectionId}:`, message);
|
|
89
|
+
res.status(500).json({ success: false, error: message });
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
// GET /api/database/buildings/:buildingId/connections/:connectionId/databases
|
|
93
|
+
router.get('/buildings/:buildingId/connections/:connectionId/databases', async (req, res) => {
|
|
94
|
+
const buildingId = String(req.params.buildingId);
|
|
95
|
+
const connectionId = String(req.params.connectionId);
|
|
96
|
+
const connection = getConnection(buildingId, connectionId);
|
|
97
|
+
if (!connection) {
|
|
98
|
+
res.status(404).json({ error: 'Connection not found' });
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
const databases = await databaseService.listDatabases(connection);
|
|
103
|
+
res.json({ databases });
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
107
|
+
log.error(`listDatabases failed for ${buildingId}/${connectionId}:`, message);
|
|
108
|
+
res.status(500).json({ error: message });
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
// GET /api/database/buildings/:buildingId/connections/:connectionId/databases/:database/tables
|
|
112
|
+
router.get('/buildings/:buildingId/connections/:connectionId/databases/:database/tables', async (req, res) => {
|
|
113
|
+
const buildingId = String(req.params.buildingId);
|
|
114
|
+
const connectionId = String(req.params.connectionId);
|
|
115
|
+
const database = String(req.params.database);
|
|
116
|
+
const connection = getConnection(buildingId, connectionId);
|
|
117
|
+
if (!connection) {
|
|
118
|
+
res.status(404).json({ error: 'Connection not found' });
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
const tables = await databaseService.listTables(connection, database);
|
|
123
|
+
res.json({ database, tables });
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
127
|
+
log.error(`listTables failed for ${buildingId}/${connectionId}/${database}:`, message);
|
|
128
|
+
res.status(500).json({ error: message });
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
// GET /api/database/buildings/:buildingId/connections/:connectionId/databases/:database/tables/:table/columns
|
|
132
|
+
// Lean variant of /schema — returns only the column list (no indexes / foreign keys).
|
|
133
|
+
router.get('/buildings/:buildingId/connections/:connectionId/databases/:database/tables/:table/columns', async (req, res) => {
|
|
134
|
+
const buildingId = String(req.params.buildingId);
|
|
135
|
+
const connectionId = String(req.params.connectionId);
|
|
136
|
+
const database = String(req.params.database);
|
|
137
|
+
const table = String(req.params.table);
|
|
138
|
+
const connection = getConnection(buildingId, connectionId);
|
|
139
|
+
if (!connection) {
|
|
140
|
+
res.status(404).json({ error: 'Connection not found' });
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
const { columns } = await databaseService.getTableSchema(connection, database, table);
|
|
145
|
+
res.json({ database, table, columns });
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
149
|
+
log.error(`describe (columns) failed for ${buildingId}/${connectionId}/${database}/${table}:`, message);
|
|
150
|
+
res.status(500).json({ error: message });
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
// GET /api/database/buildings/:buildingId/connections/:connectionId/databases/:database/tables/:table/schema
|
|
154
|
+
router.get('/buildings/:buildingId/connections/:connectionId/databases/:database/tables/:table/schema', async (req, res) => {
|
|
155
|
+
const buildingId = String(req.params.buildingId);
|
|
156
|
+
const connectionId = String(req.params.connectionId);
|
|
157
|
+
const database = String(req.params.database);
|
|
158
|
+
const table = String(req.params.table);
|
|
159
|
+
const connection = getConnection(buildingId, connectionId);
|
|
160
|
+
if (!connection) {
|
|
161
|
+
res.status(404).json({ error: 'Connection not found' });
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
const schema = await databaseService.getTableSchema(connection, database, table);
|
|
166
|
+
res.json({ database, table, ...schema });
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
170
|
+
log.error(`getTableSchema failed for ${buildingId}/${connectionId}/${database}/${table}:`, message);
|
|
171
|
+
res.status(500).json({ error: message });
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
// POST /api/database/buildings/:buildingId/connections/:connectionId/query
|
|
175
|
+
// Body: { database: string, query: string, limit?: number, recordHistory?: boolean }
|
|
176
|
+
router.post('/buildings/:buildingId/connections/:connectionId/query', async (req, res) => {
|
|
177
|
+
const buildingId = String(req.params.buildingId);
|
|
178
|
+
const connectionId = String(req.params.connectionId);
|
|
179
|
+
const { database, query, limit, recordHistory } = req.body ?? {};
|
|
180
|
+
if (typeof database !== 'string' || !database) {
|
|
181
|
+
res.status(400).json({ error: 'Body field "database" is required' });
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (typeof query !== 'string' || !query.trim()) {
|
|
185
|
+
res.status(400).json({ error: 'Body field "query" is required' });
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const connection = getConnection(buildingId, connectionId);
|
|
189
|
+
if (!connection) {
|
|
190
|
+
res.status(404).json({ error: 'Connection not found' });
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const effectiveLimit = Math.min(Math.max(1, Number.isFinite(limit) ? Number(limit) : DEFAULT_QUERY_LIMIT), MAX_QUERY_LIMIT);
|
|
194
|
+
try {
|
|
195
|
+
const result = await databaseService.executeQuery(connection, database, query, effectiveLimit);
|
|
196
|
+
if (recordHistory && result.status === 'success') {
|
|
197
|
+
databaseService.addToHistory(buildingId, result);
|
|
198
|
+
}
|
|
199
|
+
const status = result.status === 'success' ? 200 : 400;
|
|
200
|
+
res.status(status).json(result);
|
|
201
|
+
}
|
|
202
|
+
catch (err) {
|
|
203
|
+
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
204
|
+
log.error(`executeQuery failed for ${buildingId}/${connectionId}:`, message);
|
|
205
|
+
res.status(500).json({ status: 'error', error: message });
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
// GET /api/database/buildings/:buildingId/history?limit=100
|
|
209
|
+
router.get('/buildings/:buildingId/history', (req, res) => {
|
|
210
|
+
const buildingId = String(req.params.buildingId);
|
|
211
|
+
const building = getDatabaseBuilding(buildingId);
|
|
212
|
+
if (!building) {
|
|
213
|
+
res.status(404).json({ error: 'Database building not found' });
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const rawLimit = Number(req.query.limit);
|
|
217
|
+
const limit = Number.isFinite(rawLimit) && rawLimit > 0 ? Math.min(rawLimit, 500) : 100;
|
|
218
|
+
const history = databaseService.getHistory(buildingId, limit);
|
|
219
|
+
res.json({ history });
|
|
220
|
+
});
|
|
221
|
+
export default router;
|