tide-commander 0.52.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/LICENSE +21 -0
- package/README.md +364 -0
- package/dist/assets/characters/Textures/colormap.png +0 -0
- package/dist/assets/characters/character-female-a.glb +0 -0
- package/dist/assets/characters/character-female-b.glb +0 -0
- package/dist/assets/characters/character-female-c.glb +0 -0
- package/dist/assets/characters/character-female-d.glb +0 -0
- package/dist/assets/characters/character-female-e.glb +0 -0
- package/dist/assets/characters/character-female-f.glb +0 -0
- package/dist/assets/characters/character-male-a-processed.gltf +11862 -0
- package/dist/assets/characters/character-male-a.glb +0 -0
- package/dist/assets/characters/character-male-b.glb +0 -0
- package/dist/assets/characters/character-male-c.glb +0 -0
- package/dist/assets/characters/character-male-d.glb +0 -0
- package/dist/assets/characters/character-male-e.glb +0 -0
- package/dist/assets/characters/character-male-f.glb +0 -0
- package/dist/assets/icons/icon-192.png +0 -0
- package/dist/assets/icons/icon-512.png +0 -0
- package/dist/assets/landing-Cc0MDBAK.css +1 -0
- package/dist/assets/main-BIpLsrUu.css +1 -0
- package/dist/assets/main-DMTRw3br.js +276 -0
- package/dist/assets/textures/concrete_floor_worn_001_diff_1k.jpg +0 -0
- package/dist/assets/textures/logo-blanco.png +0 -0
- package/dist/assets/vendor-react-uS-d4TUT.js +17 -0
- package/dist/assets/vendor-three-4iQNXcoo.js +3828 -0
- package/dist/assets/web-BZdi2lG9.js +1 -0
- package/dist/assets/web-yHsOO1Qb.js +1 -0
- package/dist/index.html +38 -0
- package/dist/manifest.json +39 -0
- package/dist/src/packages/landing/index.html +463 -0
- package/dist/src/packages/server/app.js +87 -0
- package/dist/src/packages/server/auth/index.js +121 -0
- package/dist/src/packages/server/claude/backend.js +578 -0
- package/dist/src/packages/server/claude/index.js +8 -0
- package/dist/src/packages/server/claude/runner/internal-events.js +22 -0
- package/dist/src/packages/server/claude/runner/process-lifecycle.js +208 -0
- package/dist/src/packages/server/claude/runner/recovery-store.js +72 -0
- package/dist/src/packages/server/claude/runner/resource-monitor.js +51 -0
- package/dist/src/packages/server/claude/runner/restart-policy.js +69 -0
- package/dist/src/packages/server/claude/runner/stdout-pipeline.js +153 -0
- package/dist/src/packages/server/claude/runner/watchdog.js +114 -0
- package/dist/src/packages/server/claude/runner.js +310 -0
- package/dist/src/packages/server/claude/session-loader.js +898 -0
- package/dist/src/packages/server/claude/types.js +5 -0
- package/dist/src/packages/server/cli.js +113 -0
- package/dist/src/packages/server/codex/backend.js +119 -0
- package/dist/src/packages/server/codex/index.js +2 -0
- package/dist/src/packages/server/codex/json-event-parser.js +612 -0
- package/dist/src/packages/server/data/builtin-skills/bitbucket-pr.js +298 -0
- package/dist/src/packages/server/data/builtin-skills/full-notifications.js +49 -0
- package/dist/src/packages/server/data/builtin-skills/git-captain.js +304 -0
- package/dist/src/packages/server/data/builtin-skills/index.js +61 -0
- package/dist/src/packages/server/data/builtin-skills/pm2-logs.js +354 -0
- package/dist/src/packages/server/data/builtin-skills/send-message-to-agent.js +51 -0
- package/dist/src/packages/server/data/builtin-skills/server-logs.js +124 -0
- package/dist/src/packages/server/data/builtin-skills/streaming-exec.js +94 -0
- package/dist/src/packages/server/data/builtin-skills/types.js +4 -0
- package/dist/src/packages/server/data/builtin-skills.js +6 -0
- package/dist/src/packages/server/data/index.js +890 -0
- package/dist/src/packages/server/data/snapshots.js +371 -0
- package/dist/src/packages/server/index.js +96 -0
- package/dist/src/packages/server/prompts/tide-commander.js +13 -0
- package/dist/src/packages/server/routes/agents.js +406 -0
- package/dist/src/packages/server/routes/config.js +347 -0
- package/dist/src/packages/server/routes/custom-models.js +170 -0
- package/dist/src/packages/server/routes/exec.js +269 -0
- package/dist/src/packages/server/routes/files.js +995 -0
- package/dist/src/packages/server/routes/index.js +38 -0
- package/dist/src/packages/server/routes/notifications.js +81 -0
- package/dist/src/packages/server/routes/permissions.js +115 -0
- package/dist/src/packages/server/routes/snapshots.js +224 -0
- package/dist/src/packages/server/routes/stt.js +99 -0
- package/dist/src/packages/server/routes/tts.js +166 -0
- package/dist/src/packages/server/routes/voice-assistant.js +310 -0
- package/dist/src/packages/server/runtime/claude-runtime-provider.js +10 -0
- package/dist/src/packages/server/runtime/codex-runtime-provider.js +11 -0
- package/dist/src/packages/server/runtime/index.js +2 -0
- package/dist/src/packages/server/runtime/types.js +6 -0
- package/dist/src/packages/server/services/agent-lifecycle-service.js +82 -0
- package/dist/src/packages/server/services/agent-service.js +410 -0
- package/dist/src/packages/server/services/boss-message-service.js +430 -0
- package/dist/src/packages/server/services/boss-service.js +553 -0
- package/dist/src/packages/server/services/building-service.js +867 -0
- package/dist/src/packages/server/services/claude-service.js +5 -0
- package/dist/src/packages/server/services/custom-class-service.js +323 -0
- package/dist/src/packages/server/services/database-service.js +914 -0
- package/dist/src/packages/server/services/docker-service.js +865 -0
- package/dist/src/packages/server/services/fileTracker.js +242 -0
- package/dist/src/packages/server/services/index.js +21 -0
- package/dist/src/packages/server/services/permission-service.js +258 -0
- package/dist/src/packages/server/services/pm2-service.js +435 -0
- package/dist/src/packages/server/services/runtime-command-execution.js +168 -0
- package/dist/src/packages/server/services/runtime-events.js +357 -0
- package/dist/src/packages/server/services/runtime-service.js +308 -0
- package/dist/src/packages/server/services/runtime-status-sync.js +104 -0
- package/dist/src/packages/server/services/runtime-subagents.js +50 -0
- package/dist/src/packages/server/services/runtime-watchdog.js +74 -0
- package/dist/src/packages/server/services/secrets-service.js +206 -0
- package/dist/src/packages/server/services/skill-service.js +508 -0
- package/dist/src/packages/server/services/subordinate-context-service.js +223 -0
- package/dist/src/packages/server/services/supervisor-claude.js +132 -0
- package/dist/src/packages/server/services/supervisor-prompts.js +80 -0
- package/dist/src/packages/server/services/supervisor-service.js +659 -0
- package/dist/src/packages/server/services/work-plan-service.js +476 -0
- package/dist/src/packages/server/setup.js +86 -0
- package/dist/src/packages/server/utils/index.js +4 -0
- package/dist/src/packages/server/utils/logger.js +302 -0
- package/dist/src/packages/server/utils/string.js +39 -0
- package/dist/src/packages/server/utils/tool-formatting.js +139 -0
- package/dist/src/packages/server/utils/unicode.js +46 -0
- package/dist/src/packages/server/websocket/handler.js +290 -0
- package/dist/src/packages/server/websocket/handlers/agent-handler.js +515 -0
- package/dist/src/packages/server/websocket/handlers/boss-handler.js +116 -0
- package/dist/src/packages/server/websocket/handlers/boss-response-handler.js +250 -0
- package/dist/src/packages/server/websocket/handlers/building-handler.js +298 -0
- package/dist/src/packages/server/websocket/handlers/command-handler.js +217 -0
- package/dist/src/packages/server/websocket/handlers/custom-class-handler.js +68 -0
- package/dist/src/packages/server/websocket/handlers/database-handler.js +223 -0
- package/dist/src/packages/server/websocket/handlers/notification-handler.js +25 -0
- package/dist/src/packages/server/websocket/handlers/permission-handler.js +21 -0
- package/dist/src/packages/server/websocket/handlers/secrets-handler.js +61 -0
- package/dist/src/packages/server/websocket/handlers/skill-handler.js +148 -0
- package/dist/src/packages/server/websocket/handlers/supervisor-handler.js +44 -0
- package/dist/src/packages/server/websocket/handlers/sync-handler.js +19 -0
- package/dist/src/packages/server/websocket/handlers/types.js +4 -0
- package/dist/src/packages/server/websocket/listeners/boss-listeners.js +21 -0
- package/dist/src/packages/server/websocket/listeners/index.js +32 -0
- package/dist/src/packages/server/websocket/listeners/permission-listeners.js +19 -0
- package/dist/src/packages/server/websocket/listeners/runtime-listeners.js +196 -0
- package/dist/src/packages/server/websocket/listeners/skill-listeners.js +51 -0
- package/dist/src/packages/server/websocket/listeners/supervisor-listeners.js +37 -0
- package/dist/src/packages/shared/agent-types.js +54 -0
- package/dist/src/packages/shared/building-types.js +43 -0
- package/dist/src/packages/shared/common-types.js +1 -0
- package/dist/src/packages/shared/database-types.js +8 -0
- package/dist/src/packages/shared/types/snapshot.js +7 -0
- package/dist/src/packages/shared/types.js +12 -0
- package/dist/src/packages/shared/websocket-messages.js +1 -0
- package/dist/sw.js +37 -0
- package/package.json +90 -0
|
@@ -0,0 +1,914 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database Service
|
|
3
|
+
* Handles database connections and query execution for MySQL and PostgreSQL
|
|
4
|
+
*/
|
|
5
|
+
import mysql from 'mysql2/promise';
|
|
6
|
+
import pg from 'pg';
|
|
7
|
+
import oracledb from 'oracledb';
|
|
8
|
+
import { loadQueryHistory, saveQueryHistory } from '../data/index.js';
|
|
9
|
+
// Connection pool storage
|
|
10
|
+
const mysqlPools = new Map();
|
|
11
|
+
const pgPools = new Map();
|
|
12
|
+
const oraclePools = new Map();
|
|
13
|
+
// Note: oracledb 6.0+ uses thin mode by default, which doesn't require Oracle Instant Client
|
|
14
|
+
// In-memory query history cache
|
|
15
|
+
const queryHistoryCache = new Map();
|
|
16
|
+
/**
|
|
17
|
+
* Generate a unique key for connection pooling
|
|
18
|
+
*/
|
|
19
|
+
function getConnectionKey(connection, database) {
|
|
20
|
+
return `${connection.id}:${database || connection.database || 'default'}`;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Get or create a MySQL connection pool
|
|
24
|
+
*/
|
|
25
|
+
async function getMySQLPool(connection, database) {
|
|
26
|
+
const key = getConnectionKey(connection, database);
|
|
27
|
+
if (mysqlPools.has(key)) {
|
|
28
|
+
return mysqlPools.get(key);
|
|
29
|
+
}
|
|
30
|
+
const pool = mysql.createPool({
|
|
31
|
+
host: connection.host,
|
|
32
|
+
port: connection.port,
|
|
33
|
+
user: connection.username,
|
|
34
|
+
password: connection.password,
|
|
35
|
+
database: database || connection.database,
|
|
36
|
+
ssl: connection.ssl ? {
|
|
37
|
+
rejectUnauthorized: connection.sslConfig?.rejectUnauthorized ?? true,
|
|
38
|
+
ca: connection.sslConfig?.ca,
|
|
39
|
+
cert: connection.sslConfig?.cert,
|
|
40
|
+
key: connection.sslConfig?.key,
|
|
41
|
+
} : undefined,
|
|
42
|
+
waitForConnections: true,
|
|
43
|
+
connectionLimit: 5,
|
|
44
|
+
queueLimit: 0,
|
|
45
|
+
enableKeepAlive: true,
|
|
46
|
+
keepAliveInitialDelay: 10000,
|
|
47
|
+
});
|
|
48
|
+
mysqlPools.set(key, pool);
|
|
49
|
+
return pool;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Get or create a PostgreSQL connection pool
|
|
53
|
+
*/
|
|
54
|
+
async function getPgPool(connection, database) {
|
|
55
|
+
const key = getConnectionKey(connection, database);
|
|
56
|
+
if (pgPools.has(key)) {
|
|
57
|
+
return pgPools.get(key);
|
|
58
|
+
}
|
|
59
|
+
const pool = new pg.Pool({
|
|
60
|
+
host: connection.host,
|
|
61
|
+
port: connection.port,
|
|
62
|
+
user: connection.username,
|
|
63
|
+
password: connection.password,
|
|
64
|
+
database: database || connection.database || 'postgres',
|
|
65
|
+
ssl: connection.ssl ? {
|
|
66
|
+
rejectUnauthorized: connection.sslConfig?.rejectUnauthorized ?? true,
|
|
67
|
+
ca: connection.sslConfig?.ca,
|
|
68
|
+
cert: connection.sslConfig?.cert,
|
|
69
|
+
key: connection.sslConfig?.key,
|
|
70
|
+
} : undefined,
|
|
71
|
+
max: 5,
|
|
72
|
+
idleTimeoutMillis: 30000,
|
|
73
|
+
connectionTimeoutMillis: 10000,
|
|
74
|
+
});
|
|
75
|
+
pgPools.set(key, pool);
|
|
76
|
+
return pool;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Get or create an Oracle connection pool
|
|
80
|
+
* Note: For Oracle, we always connect to the same service (from connection.database).
|
|
81
|
+
* The "database" parameter in other functions represents the schema/owner to query,
|
|
82
|
+
* not a different database to connect to.
|
|
83
|
+
*/
|
|
84
|
+
async function getOraclePool(connection) {
|
|
85
|
+
// For Oracle, we use only the connection ID as the key since we always connect
|
|
86
|
+
// to the same service - the schema is just used in queries, not connection
|
|
87
|
+
const key = connection.id;
|
|
88
|
+
if (oraclePools.has(key)) {
|
|
89
|
+
return oraclePools.get(key);
|
|
90
|
+
}
|
|
91
|
+
// Build connection string - Oracle uses service name or SID
|
|
92
|
+
// Format: host:port/serviceName
|
|
93
|
+
// The service name comes from connection.database (e.g., ORCLPDB1)
|
|
94
|
+
const serviceName = connection.database || 'ORCL';
|
|
95
|
+
const connectString = `${connection.host}:${connection.port}/${serviceName}`;
|
|
96
|
+
const pool = await oracledb.createPool({
|
|
97
|
+
user: connection.username,
|
|
98
|
+
password: connection.password,
|
|
99
|
+
connectString,
|
|
100
|
+
poolMin: 1,
|
|
101
|
+
poolMax: 5,
|
|
102
|
+
poolIncrement: 1,
|
|
103
|
+
poolTimeout: 60,
|
|
104
|
+
});
|
|
105
|
+
oraclePools.set(key, pool);
|
|
106
|
+
return pool;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Test a database connection
|
|
110
|
+
*/
|
|
111
|
+
export async function testConnection(connection) {
|
|
112
|
+
try {
|
|
113
|
+
if (connection.engine === 'mysql') {
|
|
114
|
+
const pool = await getMySQLPool(connection);
|
|
115
|
+
const [rows] = await pool.query('SELECT VERSION() as version');
|
|
116
|
+
const version = rows[0]?.version;
|
|
117
|
+
return { success: true, serverVersion: version };
|
|
118
|
+
}
|
|
119
|
+
else if (connection.engine === 'postgresql') {
|
|
120
|
+
const pool = await getPgPool(connection);
|
|
121
|
+
const result = await pool.query('SELECT version()');
|
|
122
|
+
const version = result.rows[0]?.version?.split(' ').slice(0, 2).join(' ');
|
|
123
|
+
return { success: true, serverVersion: version };
|
|
124
|
+
}
|
|
125
|
+
else if (connection.engine === 'oracle') {
|
|
126
|
+
const pool = await getOraclePool(connection);
|
|
127
|
+
const conn = await pool.getConnection();
|
|
128
|
+
try {
|
|
129
|
+
const result = await conn.execute("SELECT BANNER FROM V$VERSION WHERE ROWNUM = 1", [], { outFormat: oracledb.OUT_FORMAT_OBJECT });
|
|
130
|
+
const version = result.rows?.[0]?.BANNER || 'Oracle';
|
|
131
|
+
return { success: true, serverVersion: version };
|
|
132
|
+
}
|
|
133
|
+
finally {
|
|
134
|
+
await conn.close();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return { success: false, error: 'Unsupported database engine' };
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
141
|
+
return { success: false, error: message };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* List all databases available on the connection
|
|
146
|
+
*/
|
|
147
|
+
export async function listDatabases(connection) {
|
|
148
|
+
try {
|
|
149
|
+
if (connection.engine === 'mysql') {
|
|
150
|
+
const pool = await getMySQLPool(connection);
|
|
151
|
+
const [rows] = await pool.query('SHOW DATABASES');
|
|
152
|
+
return rows.map(r => r.Database);
|
|
153
|
+
}
|
|
154
|
+
else if (connection.engine === 'postgresql') {
|
|
155
|
+
const pool = await getPgPool(connection);
|
|
156
|
+
const result = await pool.query("SELECT datname FROM pg_database WHERE datistemplate = false ORDER BY datname");
|
|
157
|
+
return result.rows.map(r => r.datname);
|
|
158
|
+
}
|
|
159
|
+
else if (connection.engine === 'oracle') {
|
|
160
|
+
// Oracle: list accessible schemas as "databases"
|
|
161
|
+
// Try ALL_USERS first, fall back to just the current user's schema
|
|
162
|
+
const pool = await getOraclePool(connection);
|
|
163
|
+
const conn = await pool.getConnection();
|
|
164
|
+
try {
|
|
165
|
+
const execOptions = { outFormat: oracledb.OUT_FORMAT_OBJECT };
|
|
166
|
+
// First try to get all accessible schemas
|
|
167
|
+
try {
|
|
168
|
+
const result = await conn.execute(`SELECT USERNAME FROM ALL_USERS
|
|
169
|
+
WHERE USERNAME NOT IN ('SYS', 'SYSTEM', 'DBSNMP', 'OUTLN', 'XDB', 'WMSYS', 'CTXSYS', 'ANONYMOUS', 'MDSYS', 'OLAPSYS', 'ORDDATA', 'ORDSYS', 'EXFSYS', 'DMSYS', 'APEX_PUBLIC_USER', 'APPQOSSYS', 'AUDSYS', 'DBSFWUSER', 'DIP', 'GGSYS', 'GSMADMIN_INTERNAL', 'GSMCATUSER', 'GSMUSER', 'LBACSYS', 'OJVMSYS', 'REMOTE_SCHEDULER_AGENT', 'SYS$UMF', 'SYSBACKUP', 'SYSDG', 'SYSKM', 'SYSRAC', 'XS$NULL')
|
|
170
|
+
AND ORACLE_MAINTAINED = 'N'
|
|
171
|
+
ORDER BY USERNAME`, [], execOptions);
|
|
172
|
+
if (result.rows && result.rows.length > 0) {
|
|
173
|
+
return result.rows.map(r => r.USERNAME);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
// ORACLE_MAINTAINED column might not exist in older versions, try simpler query
|
|
178
|
+
try {
|
|
179
|
+
const result = await conn.execute(`SELECT USERNAME FROM ALL_USERS
|
|
180
|
+
WHERE USERNAME NOT IN ('SYS', 'SYSTEM', 'DBSNMP', 'OUTLN', 'XDB', 'WMSYS', 'CTXSYS', 'ANONYMOUS', 'MDSYS', 'OLAPSYS', 'ORDDATA', 'ORDSYS', 'EXFSYS', 'DMSYS', 'APEX_PUBLIC_USER')
|
|
181
|
+
ORDER BY USERNAME`, [], execOptions);
|
|
182
|
+
if (result.rows && result.rows.length > 0) {
|
|
183
|
+
return result.rows.map(r => r.USERNAME);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
// Fall through to current user only
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// Fallback: just return the current user's schema
|
|
191
|
+
const userResult = await conn.execute(`SELECT USER as USERNAME FROM DUAL`, [], execOptions);
|
|
192
|
+
return (userResult.rows || []).map(r => r.USERNAME);
|
|
193
|
+
}
|
|
194
|
+
finally {
|
|
195
|
+
await conn.close();
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return [];
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
console.error('Error listing databases:', error);
|
|
202
|
+
throw error;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* List all tables in a database
|
|
207
|
+
*/
|
|
208
|
+
export async function listTables(connection, database) {
|
|
209
|
+
try {
|
|
210
|
+
if (connection.engine === 'mysql') {
|
|
211
|
+
const pool = await getMySQLPool(connection, database);
|
|
212
|
+
const [rows] = await pool.query(`
|
|
213
|
+
SELECT
|
|
214
|
+
TABLE_NAME as name,
|
|
215
|
+
TABLE_TYPE as type,
|
|
216
|
+
ENGINE as engine,
|
|
217
|
+
TABLE_ROWS as \`rows\`,
|
|
218
|
+
DATA_LENGTH + INDEX_LENGTH as size,
|
|
219
|
+
TABLE_COMMENT as comment
|
|
220
|
+
FROM information_schema.TABLES
|
|
221
|
+
WHERE TABLE_SCHEMA = ?
|
|
222
|
+
ORDER BY TABLE_NAME
|
|
223
|
+
`, [database]);
|
|
224
|
+
return rows.map(r => ({
|
|
225
|
+
name: r.name,
|
|
226
|
+
type: r.type === 'VIEW' ? 'view' : 'table',
|
|
227
|
+
engine: r.engine,
|
|
228
|
+
rows: r.rows,
|
|
229
|
+
size: r.size,
|
|
230
|
+
comment: r.comment || undefined,
|
|
231
|
+
}));
|
|
232
|
+
}
|
|
233
|
+
else if (connection.engine === 'postgresql') {
|
|
234
|
+
const pool = await getPgPool(connection, database);
|
|
235
|
+
const result = await pool.query(`
|
|
236
|
+
SELECT
|
|
237
|
+
t.tablename as name,
|
|
238
|
+
'table' as type,
|
|
239
|
+
pg_total_relation_size(quote_ident(t.schemaname) || '.' || quote_ident(t.tablename)) as size,
|
|
240
|
+
obj_description((quote_ident(t.schemaname) || '.' || quote_ident(t.tablename))::regclass) as comment
|
|
241
|
+
FROM pg_tables t
|
|
242
|
+
WHERE t.schemaname = 'public'
|
|
243
|
+
UNION ALL
|
|
244
|
+
SELECT
|
|
245
|
+
v.viewname as name,
|
|
246
|
+
'view' as type,
|
|
247
|
+
0 as size,
|
|
248
|
+
obj_description((quote_ident(v.schemaname) || '.' || quote_ident(v.viewname))::regclass) as comment
|
|
249
|
+
FROM pg_views v
|
|
250
|
+
WHERE v.schemaname = 'public'
|
|
251
|
+
ORDER BY name
|
|
252
|
+
`);
|
|
253
|
+
return result.rows.map(r => ({
|
|
254
|
+
name: r.name,
|
|
255
|
+
type: r.type,
|
|
256
|
+
size: parseInt(r.size) || undefined,
|
|
257
|
+
comment: r.comment || undefined,
|
|
258
|
+
}));
|
|
259
|
+
}
|
|
260
|
+
else if (connection.engine === 'oracle') {
|
|
261
|
+
// In Oracle, database parameter is treated as schema/owner
|
|
262
|
+
const pool = await getOraclePool(connection);
|
|
263
|
+
const conn = await pool.getConnection();
|
|
264
|
+
try {
|
|
265
|
+
// Simple query without joins - more compatible with restricted permissions
|
|
266
|
+
const result = await conn.execute(`
|
|
267
|
+
SELECT TABLE_NAME as NAME, 'table' as TYPE, NUM_ROWS
|
|
268
|
+
FROM ALL_TABLES
|
|
269
|
+
WHERE OWNER = :owner
|
|
270
|
+
UNION ALL
|
|
271
|
+
SELECT VIEW_NAME as NAME, 'view' as TYPE, NULL as NUM_ROWS
|
|
272
|
+
FROM ALL_VIEWS
|
|
273
|
+
WHERE OWNER = :owner
|
|
274
|
+
ORDER BY NAME
|
|
275
|
+
`, { owner: database.toUpperCase() }, { outFormat: oracledb.OUT_FORMAT_OBJECT });
|
|
276
|
+
return (result.rows || []).map(r => ({
|
|
277
|
+
name: r.NAME,
|
|
278
|
+
type: r.TYPE,
|
|
279
|
+
rows: r.NUM_ROWS || undefined,
|
|
280
|
+
}));
|
|
281
|
+
}
|
|
282
|
+
finally {
|
|
283
|
+
await conn.close();
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return [];
|
|
287
|
+
}
|
|
288
|
+
catch (error) {
|
|
289
|
+
console.error('Error listing tables:', error);
|
|
290
|
+
throw error;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Get table schema (columns, indexes, foreign keys)
|
|
295
|
+
*/
|
|
296
|
+
export async function getTableSchema(connection, database, table) {
|
|
297
|
+
const columns = [];
|
|
298
|
+
const indexes = [];
|
|
299
|
+
const foreignKeys = [];
|
|
300
|
+
try {
|
|
301
|
+
if (connection.engine === 'mysql') {
|
|
302
|
+
const pool = await getMySQLPool(connection, database);
|
|
303
|
+
// Get columns
|
|
304
|
+
const [columnRows] = await pool.query(`
|
|
305
|
+
SELECT
|
|
306
|
+
COLUMN_NAME as name,
|
|
307
|
+
COLUMN_TYPE as type,
|
|
308
|
+
IS_NULLABLE as nullable,
|
|
309
|
+
COLUMN_DEFAULT as defaultValue,
|
|
310
|
+
COLUMN_KEY as columnKey,
|
|
311
|
+
EXTRA as extra,
|
|
312
|
+
COLUMN_COMMENT as comment
|
|
313
|
+
FROM information_schema.COLUMNS
|
|
314
|
+
WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?
|
|
315
|
+
ORDER BY ORDINAL_POSITION
|
|
316
|
+
`, [database, table]);
|
|
317
|
+
for (const col of columnRows) {
|
|
318
|
+
columns.push({
|
|
319
|
+
name: col.name,
|
|
320
|
+
type: col.type,
|
|
321
|
+
nullable: col.nullable === 'YES',
|
|
322
|
+
defaultValue: col.defaultValue ?? undefined,
|
|
323
|
+
primaryKey: col.columnKey === 'PRI',
|
|
324
|
+
autoIncrement: col.extra.includes('auto_increment'),
|
|
325
|
+
comment: col.comment || undefined,
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
// Get indexes
|
|
329
|
+
const [indexRows] = await pool.query(`
|
|
330
|
+
SELECT
|
|
331
|
+
INDEX_NAME as name,
|
|
332
|
+
GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX) as columns,
|
|
333
|
+
NOT NON_UNIQUE as isUnique,
|
|
334
|
+
INDEX_TYPE as type
|
|
335
|
+
FROM information_schema.STATISTICS
|
|
336
|
+
WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?
|
|
337
|
+
GROUP BY INDEX_NAME, NON_UNIQUE, INDEX_TYPE
|
|
338
|
+
`, [database, table]);
|
|
339
|
+
for (const idx of indexRows) {
|
|
340
|
+
indexes.push({
|
|
341
|
+
name: idx.name,
|
|
342
|
+
columns: idx.columns.split(','),
|
|
343
|
+
unique: Boolean(idx.isUnique),
|
|
344
|
+
type: idx.type,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
// Get foreign keys
|
|
348
|
+
const [fkRows] = await pool.query(`
|
|
349
|
+
SELECT
|
|
350
|
+
kcu.CONSTRAINT_NAME as name,
|
|
351
|
+
GROUP_CONCAT(kcu.COLUMN_NAME ORDER BY kcu.ORDINAL_POSITION) as columns,
|
|
352
|
+
kcu.REFERENCED_TABLE_NAME as referencedTable,
|
|
353
|
+
GROUP_CONCAT(kcu.REFERENCED_COLUMN_NAME ORDER BY kcu.ORDINAL_POSITION) as referencedColumns,
|
|
354
|
+
rc.DELETE_RULE as onDelete,
|
|
355
|
+
rc.UPDATE_RULE as onUpdate
|
|
356
|
+
FROM information_schema.KEY_COLUMN_USAGE kcu
|
|
357
|
+
JOIN information_schema.REFERENTIAL_CONSTRAINTS rc
|
|
358
|
+
ON kcu.CONSTRAINT_NAME = rc.CONSTRAINT_NAME
|
|
359
|
+
AND kcu.TABLE_SCHEMA = rc.CONSTRAINT_SCHEMA
|
|
360
|
+
WHERE kcu.TABLE_SCHEMA = ? AND kcu.TABLE_NAME = ? AND kcu.REFERENCED_TABLE_NAME IS NOT NULL
|
|
361
|
+
GROUP BY kcu.CONSTRAINT_NAME, kcu.REFERENCED_TABLE_NAME, rc.DELETE_RULE, rc.UPDATE_RULE
|
|
362
|
+
`, [database, table]);
|
|
363
|
+
for (const fk of fkRows) {
|
|
364
|
+
foreignKeys.push({
|
|
365
|
+
name: fk.name,
|
|
366
|
+
columns: fk.columns.split(','),
|
|
367
|
+
referencedTable: fk.referencedTable,
|
|
368
|
+
referencedColumns: fk.referencedColumns.split(','),
|
|
369
|
+
onDelete: fk.onDelete,
|
|
370
|
+
onUpdate: fk.onUpdate,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
else if (connection.engine === 'postgresql') {
|
|
375
|
+
const pool = await getPgPool(connection, database);
|
|
376
|
+
// Get columns
|
|
377
|
+
const columnResult = await pool.query(`
|
|
378
|
+
SELECT
|
|
379
|
+
a.attname as name,
|
|
380
|
+
pg_catalog.format_type(a.atttypid, a.atttypmod) as type,
|
|
381
|
+
NOT a.attnotnull as nullable,
|
|
382
|
+
pg_get_expr(d.adbin, d.adrelid) as "defaultValue",
|
|
383
|
+
COALESCE(pk.is_pk, false) as "primaryKey",
|
|
384
|
+
a.attidentity != '' OR COALESCE(s.is_serial, false) as "autoIncrement",
|
|
385
|
+
col_description(c.oid, a.attnum) as comment
|
|
386
|
+
FROM pg_class c
|
|
387
|
+
JOIN pg_attribute a ON a.attrelid = c.oid
|
|
388
|
+
LEFT JOIN pg_attrdef d ON d.adrelid = c.oid AND d.adnum = a.attnum
|
|
389
|
+
LEFT JOIN (
|
|
390
|
+
SELECT kcu.column_name, true as is_pk
|
|
391
|
+
FROM information_schema.table_constraints tc
|
|
392
|
+
JOIN information_schema.key_column_usage kcu ON tc.constraint_name = kcu.constraint_name
|
|
393
|
+
WHERE tc.table_name = $1 AND tc.constraint_type = 'PRIMARY KEY'
|
|
394
|
+
) pk ON pk.column_name = a.attname
|
|
395
|
+
LEFT JOIN (
|
|
396
|
+
SELECT column_name, true as is_serial
|
|
397
|
+
FROM information_schema.columns
|
|
398
|
+
WHERE table_name = $1 AND column_default LIKE 'nextval%'
|
|
399
|
+
) s ON s.column_name = a.attname
|
|
400
|
+
WHERE c.relname = $1 AND a.attnum > 0 AND NOT a.attisdropped
|
|
401
|
+
ORDER BY a.attnum
|
|
402
|
+
`, [table]);
|
|
403
|
+
for (const col of columnResult.rows) {
|
|
404
|
+
columns.push({
|
|
405
|
+
name: col.name,
|
|
406
|
+
type: col.type,
|
|
407
|
+
nullable: col.nullable,
|
|
408
|
+
defaultValue: col.defaultValue ?? undefined,
|
|
409
|
+
primaryKey: col.primaryKey,
|
|
410
|
+
autoIncrement: col.autoIncrement,
|
|
411
|
+
comment: col.comment || undefined,
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
// Get indexes
|
|
415
|
+
const indexResult = await pool.query(`
|
|
416
|
+
SELECT
|
|
417
|
+
i.relname as name,
|
|
418
|
+
array_agg(a.attname ORDER BY x.n) as columns,
|
|
419
|
+
ix.indisunique as "unique",
|
|
420
|
+
am.amname as type
|
|
421
|
+
FROM pg_index ix
|
|
422
|
+
JOIN pg_class t ON t.oid = ix.indrelid
|
|
423
|
+
JOIN pg_class i ON i.oid = ix.indexrelid
|
|
424
|
+
JOIN pg_am am ON am.oid = i.relam
|
|
425
|
+
CROSS JOIN LATERAL unnest(ix.indkey) WITH ORDINALITY AS x(attnum, n)
|
|
426
|
+
JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = x.attnum
|
|
427
|
+
WHERE t.relname = $1 AND t.relkind = 'r'
|
|
428
|
+
GROUP BY i.relname, ix.indisunique, am.amname
|
|
429
|
+
`, [table]);
|
|
430
|
+
for (const idx of indexResult.rows) {
|
|
431
|
+
indexes.push({
|
|
432
|
+
name: idx.name,
|
|
433
|
+
columns: idx.columns,
|
|
434
|
+
unique: idx.unique,
|
|
435
|
+
type: idx.type,
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
// Get foreign keys
|
|
439
|
+
const fkResult = await pool.query(`
|
|
440
|
+
SELECT
|
|
441
|
+
conname as name,
|
|
442
|
+
array_agg(a.attname ORDER BY x.n) as columns,
|
|
443
|
+
confrelid::regclass::text as "referencedTable",
|
|
444
|
+
array_agg(af.attname ORDER BY x.n) as "referencedColumns",
|
|
445
|
+
CASE confdeltype
|
|
446
|
+
WHEN 'a' THEN 'NO ACTION'
|
|
447
|
+
WHEN 'r' THEN 'RESTRICT'
|
|
448
|
+
WHEN 'c' THEN 'CASCADE'
|
|
449
|
+
WHEN 'n' THEN 'SET NULL'
|
|
450
|
+
WHEN 'd' THEN 'SET DEFAULT'
|
|
451
|
+
END as "onDelete",
|
|
452
|
+
CASE confupdtype
|
|
453
|
+
WHEN 'a' THEN 'NO ACTION'
|
|
454
|
+
WHEN 'r' THEN 'RESTRICT'
|
|
455
|
+
WHEN 'c' THEN 'CASCADE'
|
|
456
|
+
WHEN 'n' THEN 'SET NULL'
|
|
457
|
+
WHEN 'd' THEN 'SET DEFAULT'
|
|
458
|
+
END as "onUpdate"
|
|
459
|
+
FROM pg_constraint c
|
|
460
|
+
JOIN pg_class t ON t.oid = c.conrelid
|
|
461
|
+
CROSS JOIN LATERAL unnest(c.conkey, c.confkey) WITH ORDINALITY AS x(attnum, fkattnum, n)
|
|
462
|
+
JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = x.attnum
|
|
463
|
+
JOIN pg_attribute af ON af.attrelid = c.confrelid AND af.attnum = x.fkattnum
|
|
464
|
+
WHERE t.relname = $1 AND c.contype = 'f'
|
|
465
|
+
GROUP BY conname, confrelid, confdeltype, confupdtype
|
|
466
|
+
`, [table]);
|
|
467
|
+
for (const fk of fkResult.rows) {
|
|
468
|
+
foreignKeys.push({
|
|
469
|
+
name: fk.name,
|
|
470
|
+
columns: fk.columns,
|
|
471
|
+
referencedTable: fk.referencedTable,
|
|
472
|
+
referencedColumns: fk.referencedColumns,
|
|
473
|
+
onDelete: fk.onDelete,
|
|
474
|
+
onUpdate: fk.onUpdate,
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
else if (connection.engine === 'oracle') {
|
|
479
|
+
// In Oracle, database parameter is treated as schema/owner
|
|
480
|
+
const pool = await getOraclePool(connection);
|
|
481
|
+
const conn = await pool.getConnection();
|
|
482
|
+
const execOptions = { outFormat: oracledb.OUT_FORMAT_OBJECT };
|
|
483
|
+
try {
|
|
484
|
+
// Get columns (compatible with Oracle 11g and later)
|
|
485
|
+
const columnResult = await conn.execute(`
|
|
486
|
+
SELECT
|
|
487
|
+
c.COLUMN_NAME as NAME,
|
|
488
|
+
c.DATA_TYPE || CASE
|
|
489
|
+
WHEN c.DATA_TYPE IN ('VARCHAR2', 'CHAR', 'NVARCHAR2', 'NCHAR') THEN '(' || c.DATA_LENGTH || ')'
|
|
490
|
+
WHEN c.DATA_TYPE = 'NUMBER' AND c.DATA_PRECISION IS NOT NULL THEN '(' || c.DATA_PRECISION || ',' || NVL(c.DATA_SCALE, 0) || ')'
|
|
491
|
+
ELSE ''
|
|
492
|
+
END as TYPE,
|
|
493
|
+
c.NULLABLE,
|
|
494
|
+
c.DATA_DEFAULT as DEFAULT_VALUE,
|
|
495
|
+
CASE WHEN pk.COLUMN_NAME IS NOT NULL THEN 1 ELSE 0 END as PRIMARY_KEY,
|
|
496
|
+
cc.COMMENTS
|
|
497
|
+
FROM ALL_TAB_COLUMNS c
|
|
498
|
+
LEFT JOIN (
|
|
499
|
+
SELECT cols.COLUMN_NAME, cols.TABLE_NAME, cols.OWNER
|
|
500
|
+
FROM ALL_CONSTRAINTS cons
|
|
501
|
+
JOIN ALL_CONS_COLUMNS cols ON cons.CONSTRAINT_NAME = cols.CONSTRAINT_NAME AND cons.OWNER = cols.OWNER
|
|
502
|
+
WHERE cons.CONSTRAINT_TYPE = 'P' AND cons.TABLE_NAME = :table AND cons.OWNER = :owner
|
|
503
|
+
) pk ON c.COLUMN_NAME = pk.COLUMN_NAME AND c.TABLE_NAME = pk.TABLE_NAME AND c.OWNER = pk.OWNER
|
|
504
|
+
LEFT JOIN ALL_COL_COMMENTS cc ON c.COLUMN_NAME = cc.COLUMN_NAME AND c.TABLE_NAME = cc.TABLE_NAME AND c.OWNER = cc.OWNER
|
|
505
|
+
WHERE c.TABLE_NAME = :table AND c.OWNER = :owner
|
|
506
|
+
ORDER BY c.COLUMN_ID
|
|
507
|
+
`, { table: table.toUpperCase(), owner: database.toUpperCase() }, execOptions);
|
|
508
|
+
for (const col of columnResult.rows || []) {
|
|
509
|
+
columns.push({
|
|
510
|
+
name: col.NAME,
|
|
511
|
+
type: col.TYPE,
|
|
512
|
+
nullable: col.NULLABLE === 'Y',
|
|
513
|
+
defaultValue: col.DEFAULT_VALUE?.trim() || undefined,
|
|
514
|
+
primaryKey: col.PRIMARY_KEY === 1,
|
|
515
|
+
autoIncrement: false, // Oracle doesn't have auto_increment in the same way
|
|
516
|
+
comment: col.COMMENTS || undefined,
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
// Get indexes
|
|
520
|
+
const indexResult = await conn.execute(`
|
|
521
|
+
SELECT
|
|
522
|
+
i.INDEX_NAME as NAME,
|
|
523
|
+
LISTAGG(ic.COLUMN_NAME, ',') WITHIN GROUP (ORDER BY ic.COLUMN_POSITION) as COLUMNS,
|
|
524
|
+
i.UNIQUENESS as IS_UNIQUE,
|
|
525
|
+
i.INDEX_TYPE as TYPE
|
|
526
|
+
FROM ALL_INDEXES i
|
|
527
|
+
JOIN ALL_IND_COLUMNS ic ON i.INDEX_NAME = ic.INDEX_NAME AND i.OWNER = ic.INDEX_OWNER
|
|
528
|
+
WHERE i.TABLE_NAME = :table AND i.OWNER = :owner
|
|
529
|
+
GROUP BY i.INDEX_NAME, i.UNIQUENESS, i.INDEX_TYPE
|
|
530
|
+
`, { table: table.toUpperCase(), owner: database.toUpperCase() }, execOptions);
|
|
531
|
+
for (const idx of indexResult.rows || []) {
|
|
532
|
+
indexes.push({
|
|
533
|
+
name: idx.NAME,
|
|
534
|
+
columns: idx.COLUMNS.split(','),
|
|
535
|
+
unique: idx.IS_UNIQUE === 'UNIQUE',
|
|
536
|
+
type: idx.TYPE,
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
// Get foreign keys
|
|
540
|
+
const fkResult = await conn.execute(`
|
|
541
|
+
SELECT
|
|
542
|
+
c.CONSTRAINT_NAME as NAME,
|
|
543
|
+
LISTAGG(cols.COLUMN_NAME, ',') WITHIN GROUP (ORDER BY cols.POSITION) as COLUMNS,
|
|
544
|
+
r_cons.TABLE_NAME as REFERENCED_TABLE,
|
|
545
|
+
LISTAGG(r_cols.COLUMN_NAME, ',') WITHIN GROUP (ORDER BY r_cols.POSITION) as REFERENCED_COLUMNS,
|
|
546
|
+
c.DELETE_RULE
|
|
547
|
+
FROM ALL_CONSTRAINTS c
|
|
548
|
+
JOIN ALL_CONS_COLUMNS cols ON c.CONSTRAINT_NAME = cols.CONSTRAINT_NAME AND c.OWNER = cols.OWNER
|
|
549
|
+
JOIN ALL_CONSTRAINTS r_cons ON c.R_CONSTRAINT_NAME = r_cons.CONSTRAINT_NAME AND c.R_OWNER = r_cons.OWNER
|
|
550
|
+
JOIN ALL_CONS_COLUMNS r_cols ON r_cons.CONSTRAINT_NAME = r_cols.CONSTRAINT_NAME AND r_cons.OWNER = r_cols.OWNER
|
|
551
|
+
WHERE c.CONSTRAINT_TYPE = 'R' AND c.TABLE_NAME = :table AND c.OWNER = :owner
|
|
552
|
+
GROUP BY c.CONSTRAINT_NAME, r_cons.TABLE_NAME, c.DELETE_RULE
|
|
553
|
+
`, { table: table.toUpperCase(), owner: database.toUpperCase() }, execOptions);
|
|
554
|
+
for (const fk of fkResult.rows || []) {
|
|
555
|
+
foreignKeys.push({
|
|
556
|
+
name: fk.NAME,
|
|
557
|
+
columns: fk.COLUMNS.split(','),
|
|
558
|
+
referencedTable: fk.REFERENCED_TABLE,
|
|
559
|
+
referencedColumns: fk.REFERENCED_COLUMNS.split(','),
|
|
560
|
+
onDelete: fk.DELETE_RULE || undefined,
|
|
561
|
+
onUpdate: undefined, // Oracle doesn't support ON UPDATE in the same way
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
finally {
|
|
566
|
+
await conn.close();
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
catch (error) {
|
|
571
|
+
console.error('Error getting table schema:', error);
|
|
572
|
+
throw error;
|
|
573
|
+
}
|
|
574
|
+
return { columns, indexes, foreignKeys };
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Execute a query and return results
|
|
578
|
+
*/
|
|
579
|
+
export async function executeQuery(connection, database, query, limit = 1000) {
|
|
580
|
+
const queryId = `q_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
581
|
+
const startTime = Date.now();
|
|
582
|
+
try {
|
|
583
|
+
let rows = [];
|
|
584
|
+
let fields = [];
|
|
585
|
+
let affectedRows;
|
|
586
|
+
// Determine if this is a SELECT-like query
|
|
587
|
+
const trimmedQuery = query.trim().toLowerCase();
|
|
588
|
+
const isSelect = trimmedQuery.startsWith('select') ||
|
|
589
|
+
trimmedQuery.startsWith('show') ||
|
|
590
|
+
trimmedQuery.startsWith('describe') ||
|
|
591
|
+
trimmedQuery.startsWith('explain');
|
|
592
|
+
if (connection.engine === 'mysql') {
|
|
593
|
+
const pool = await getMySQLPool(connection, database);
|
|
594
|
+
if (isSelect) {
|
|
595
|
+
// Add LIMIT if not present
|
|
596
|
+
let limitedQuery = query;
|
|
597
|
+
if (!trimmedQuery.includes(' limit ')) {
|
|
598
|
+
limitedQuery = `${query.trim().replace(/;$/, '')} LIMIT ${limit}`;
|
|
599
|
+
}
|
|
600
|
+
const [result, fieldInfo] = await pool.query(limitedQuery);
|
|
601
|
+
rows = result;
|
|
602
|
+
if (fieldInfo && Array.isArray(fieldInfo)) {
|
|
603
|
+
fields = fieldInfo.map((f) => ({
|
|
604
|
+
name: f.name,
|
|
605
|
+
type: getFieldTypeName(f.type, 'mysql'),
|
|
606
|
+
table: f.table || undefined,
|
|
607
|
+
}));
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
else {
|
|
611
|
+
const [result] = await pool.query(query);
|
|
612
|
+
affectedRows = result.affectedRows;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
else if (connection.engine === 'postgresql') {
|
|
616
|
+
const pool = await getPgPool(connection, database);
|
|
617
|
+
if (isSelect) {
|
|
618
|
+
// Add LIMIT if not present
|
|
619
|
+
let limitedQuery = query;
|
|
620
|
+
if (!trimmedQuery.includes(' limit ')) {
|
|
621
|
+
limitedQuery = `${query.trim().replace(/;$/, '')} LIMIT ${limit}`;
|
|
622
|
+
}
|
|
623
|
+
const result = await pool.query(limitedQuery);
|
|
624
|
+
rows = result.rows;
|
|
625
|
+
if (result.fields) {
|
|
626
|
+
fields = result.fields.map(f => ({
|
|
627
|
+
name: f.name,
|
|
628
|
+
type: getFieldTypeName(f.dataTypeID, 'postgresql'),
|
|
629
|
+
table: undefined,
|
|
630
|
+
}));
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
else {
|
|
634
|
+
const result = await pool.query(query);
|
|
635
|
+
affectedRows = result.rowCount ?? undefined;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
else if (connection.engine === 'oracle') {
|
|
639
|
+
const pool = await getOraclePool(connection);
|
|
640
|
+
const conn = await pool.getConnection();
|
|
641
|
+
try {
|
|
642
|
+
if (isSelect) {
|
|
643
|
+
// Add FETCH FIRST for Oracle 12c+ if no ROWNUM/FETCH present
|
|
644
|
+
let limitedQuery = query;
|
|
645
|
+
if (!trimmedQuery.includes('rownum') && !trimmedQuery.includes('fetch ')) {
|
|
646
|
+
limitedQuery = `${query.trim().replace(/;$/, '')} FETCH FIRST ${limit} ROWS ONLY`;
|
|
647
|
+
}
|
|
648
|
+
const result = await conn.execute(limitedQuery, [], {
|
|
649
|
+
outFormat: oracledb.OUT_FORMAT_OBJECT,
|
|
650
|
+
fetchArraySize: limit,
|
|
651
|
+
});
|
|
652
|
+
rows = result.rows || [];
|
|
653
|
+
if (result.metaData) {
|
|
654
|
+
fields = result.metaData.map(m => ({
|
|
655
|
+
name: m.name,
|
|
656
|
+
type: getFieldTypeName(m.dbType, 'oracle'),
|
|
657
|
+
table: undefined,
|
|
658
|
+
}));
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
else {
|
|
662
|
+
const result = await conn.execute(query, [], { autoCommit: true });
|
|
663
|
+
affectedRows = result.rowsAffected;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
finally {
|
|
667
|
+
await conn.close();
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
const duration = Date.now() - startTime;
|
|
671
|
+
return {
|
|
672
|
+
id: queryId,
|
|
673
|
+
connectionId: connection.id,
|
|
674
|
+
database,
|
|
675
|
+
query,
|
|
676
|
+
status: 'success',
|
|
677
|
+
executedAt: startTime,
|
|
678
|
+
duration,
|
|
679
|
+
rows,
|
|
680
|
+
fields,
|
|
681
|
+
rowCount: rows.length,
|
|
682
|
+
affectedRows,
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
catch (error) {
|
|
686
|
+
const duration = Date.now() - startTime;
|
|
687
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
688
|
+
const errorCode = error?.code;
|
|
689
|
+
return {
|
|
690
|
+
id: queryId,
|
|
691
|
+
connectionId: connection.id,
|
|
692
|
+
database,
|
|
693
|
+
query,
|
|
694
|
+
status: 'error',
|
|
695
|
+
executedAt: startTime,
|
|
696
|
+
duration,
|
|
697
|
+
error: errorMessage,
|
|
698
|
+
errorCode,
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* Add a query to history
|
|
704
|
+
*/
|
|
705
|
+
export function addToHistory(buildingId, result) {
|
|
706
|
+
const entry = {
|
|
707
|
+
id: result.id,
|
|
708
|
+
buildingId,
|
|
709
|
+
connectionId: result.connectionId,
|
|
710
|
+
database: result.database,
|
|
711
|
+
query: result.query,
|
|
712
|
+
executedAt: result.executedAt,
|
|
713
|
+
duration: result.duration,
|
|
714
|
+
status: result.status,
|
|
715
|
+
rowCount: result.rowCount,
|
|
716
|
+
error: result.error,
|
|
717
|
+
favorite: false,
|
|
718
|
+
};
|
|
719
|
+
let history = queryHistoryCache.get(buildingId) || loadQueryHistory(buildingId);
|
|
720
|
+
// Add new entry at the beginning
|
|
721
|
+
history = [entry, ...history];
|
|
722
|
+
// Keep only last 50 entries
|
|
723
|
+
if (history.length > 50) {
|
|
724
|
+
history = history.slice(0, 50);
|
|
725
|
+
}
|
|
726
|
+
queryHistoryCache.set(buildingId, history);
|
|
727
|
+
saveQueryHistory(buildingId, history);
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
730
|
+
* Get query history for a building
|
|
731
|
+
*/
|
|
732
|
+
export function getHistory(buildingId, limit = 100) {
|
|
733
|
+
let history = queryHistoryCache.get(buildingId);
|
|
734
|
+
if (!history) {
|
|
735
|
+
history = loadQueryHistory(buildingId);
|
|
736
|
+
queryHistoryCache.set(buildingId, history);
|
|
737
|
+
}
|
|
738
|
+
return history.slice(0, limit);
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Toggle favorite status for a query
|
|
742
|
+
*/
|
|
743
|
+
export function toggleFavorite(buildingId, queryId) {
|
|
744
|
+
const history = queryHistoryCache.get(buildingId) || loadQueryHistory(buildingId);
|
|
745
|
+
const entry = history.find(h => h.id === queryId);
|
|
746
|
+
if (entry) {
|
|
747
|
+
entry.favorite = !entry.favorite;
|
|
748
|
+
queryHistoryCache.set(buildingId, history);
|
|
749
|
+
saveQueryHistory(buildingId, history);
|
|
750
|
+
return entry.favorite;
|
|
751
|
+
}
|
|
752
|
+
return false;
|
|
753
|
+
}
|
|
754
|
+
/**
|
|
755
|
+
* Delete a query from history
|
|
756
|
+
*/
|
|
757
|
+
export function deleteFromHistory(buildingId, queryId) {
|
|
758
|
+
let history = queryHistoryCache.get(buildingId) || loadQueryHistory(buildingId);
|
|
759
|
+
history = history.filter(h => h.id !== queryId);
|
|
760
|
+
queryHistoryCache.set(buildingId, history);
|
|
761
|
+
saveQueryHistory(buildingId, history);
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* Clear all query history for a building
|
|
765
|
+
*/
|
|
766
|
+
export function clearHistory(buildingId) {
|
|
767
|
+
queryHistoryCache.set(buildingId, []);
|
|
768
|
+
saveQueryHistory(buildingId, []);
|
|
769
|
+
}
|
|
770
|
+
/**
|
|
771
|
+
* Close all connection pools for a building/connection
|
|
772
|
+
*/
|
|
773
|
+
export async function closeConnection(connectionId) {
|
|
774
|
+
// Close all pools that match this connection ID
|
|
775
|
+
for (const [key, pool] of mysqlPools.entries()) {
|
|
776
|
+
if (key.startsWith(connectionId + ':')) {
|
|
777
|
+
pool.end();
|
|
778
|
+
mysqlPools.delete(key);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
for (const [key, pool] of pgPools.entries()) {
|
|
782
|
+
if (key.startsWith(connectionId + ':')) {
|
|
783
|
+
pool.end();
|
|
784
|
+
pgPools.delete(key);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
for (const [key, pool] of oraclePools.entries()) {
|
|
788
|
+
if (key.startsWith(connectionId + ':')) {
|
|
789
|
+
await pool.close(0);
|
|
790
|
+
oraclePools.delete(key);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* Close all connection pools
|
|
796
|
+
*/
|
|
797
|
+
export async function closeAllConnections() {
|
|
798
|
+
for (const pool of mysqlPools.values()) {
|
|
799
|
+
await pool.end();
|
|
800
|
+
}
|
|
801
|
+
mysqlPools.clear();
|
|
802
|
+
for (const pool of pgPools.values()) {
|
|
803
|
+
await pool.end();
|
|
804
|
+
}
|
|
805
|
+
pgPools.clear();
|
|
806
|
+
for (const pool of oraclePools.values()) {
|
|
807
|
+
await pool.close(0);
|
|
808
|
+
}
|
|
809
|
+
oraclePools.clear();
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* Get human-readable field type name
|
|
813
|
+
*/
|
|
814
|
+
function getFieldTypeName(typeId, engine) {
|
|
815
|
+
if (typeId === undefined)
|
|
816
|
+
return 'unknown';
|
|
817
|
+
if (engine === 'mysql') {
|
|
818
|
+
// MySQL field types
|
|
819
|
+
const mysqlTypes = {
|
|
820
|
+
0: 'DECIMAL',
|
|
821
|
+
1: 'TINYINT',
|
|
822
|
+
2: 'SMALLINT',
|
|
823
|
+
3: 'INT',
|
|
824
|
+
4: 'FLOAT',
|
|
825
|
+
5: 'DOUBLE',
|
|
826
|
+
6: 'NULL',
|
|
827
|
+
7: 'TIMESTAMP',
|
|
828
|
+
8: 'BIGINT',
|
|
829
|
+
9: 'MEDIUMINT',
|
|
830
|
+
10: 'DATE',
|
|
831
|
+
11: 'TIME',
|
|
832
|
+
12: 'DATETIME',
|
|
833
|
+
13: 'YEAR',
|
|
834
|
+
14: 'NEWDATE',
|
|
835
|
+
15: 'VARCHAR',
|
|
836
|
+
16: 'BIT',
|
|
837
|
+
245: 'JSON',
|
|
838
|
+
246: 'NEWDECIMAL',
|
|
839
|
+
247: 'ENUM',
|
|
840
|
+
248: 'SET',
|
|
841
|
+
249: 'TINY_BLOB',
|
|
842
|
+
250: 'MEDIUM_BLOB',
|
|
843
|
+
251: 'LONG_BLOB',
|
|
844
|
+
252: 'BLOB',
|
|
845
|
+
253: 'VAR_STRING',
|
|
846
|
+
254: 'STRING',
|
|
847
|
+
255: 'GEOMETRY',
|
|
848
|
+
};
|
|
849
|
+
return mysqlTypes[typeId] || `TYPE_${typeId}`;
|
|
850
|
+
}
|
|
851
|
+
else if (engine === 'postgresql') {
|
|
852
|
+
// PostgreSQL OID types
|
|
853
|
+
const pgTypes = {
|
|
854
|
+
16: 'boolean',
|
|
855
|
+
17: 'bytea',
|
|
856
|
+
18: 'char',
|
|
857
|
+
19: 'name',
|
|
858
|
+
20: 'bigint',
|
|
859
|
+
21: 'smallint',
|
|
860
|
+
23: 'integer',
|
|
861
|
+
25: 'text',
|
|
862
|
+
26: 'oid',
|
|
863
|
+
114: 'json',
|
|
864
|
+
142: 'xml',
|
|
865
|
+
700: 'real',
|
|
866
|
+
701: 'double',
|
|
867
|
+
790: 'money',
|
|
868
|
+
1042: 'char',
|
|
869
|
+
1043: 'varchar',
|
|
870
|
+
1082: 'date',
|
|
871
|
+
1083: 'time',
|
|
872
|
+
1114: 'timestamp',
|
|
873
|
+
1184: 'timestamptz',
|
|
874
|
+
1186: 'interval',
|
|
875
|
+
1266: 'timetz',
|
|
876
|
+
1700: 'numeric',
|
|
877
|
+
2950: 'uuid',
|
|
878
|
+
3802: 'jsonb',
|
|
879
|
+
};
|
|
880
|
+
return pgTypes[typeId] || `OID_${typeId}`;
|
|
881
|
+
}
|
|
882
|
+
else if (engine === 'oracle') {
|
|
883
|
+
// Oracle DB_TYPE_* constants from oracledb
|
|
884
|
+
const oracleTypes = {
|
|
885
|
+
2001: 'VARCHAR2',
|
|
886
|
+
2002: 'NUMBER',
|
|
887
|
+
2003: 'LONG',
|
|
888
|
+
2004: 'DATE',
|
|
889
|
+
2005: 'RAW',
|
|
890
|
+
2006: 'LONG RAW',
|
|
891
|
+
2007: 'ROWID',
|
|
892
|
+
2010: 'BINARY_FLOAT',
|
|
893
|
+
2011: 'BINARY_DOUBLE',
|
|
894
|
+
2012: 'CHAR',
|
|
895
|
+
2013: 'NCHAR',
|
|
896
|
+
2014: 'NVARCHAR2',
|
|
897
|
+
2015: 'CLOB',
|
|
898
|
+
2016: 'NCLOB',
|
|
899
|
+
2017: 'BLOB',
|
|
900
|
+
2018: 'BFILE',
|
|
901
|
+
2019: 'TIMESTAMP',
|
|
902
|
+
2020: 'TIMESTAMP WITH TIME ZONE',
|
|
903
|
+
2021: 'INTERVAL YEAR TO MONTH',
|
|
904
|
+
2022: 'INTERVAL DAY TO SECOND',
|
|
905
|
+
2023: 'TIMESTAMP WITH LOCAL TIME ZONE',
|
|
906
|
+
2024: 'OBJECT',
|
|
907
|
+
2025: 'CURSOR',
|
|
908
|
+
2100: 'BOOLEAN',
|
|
909
|
+
2101: 'JSON',
|
|
910
|
+
};
|
|
911
|
+
return oracleTypes[typeId] || `DBTYPE_${typeId}`;
|
|
912
|
+
}
|
|
913
|
+
return 'unknown';
|
|
914
|
+
}
|