spindb 0.8.2 → 0.9.1
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/README.md +87 -7
- package/cli/commands/clone.ts +6 -0
- package/cli/commands/connect.ts +115 -14
- package/cli/commands/create.ts +170 -8
- package/cli/commands/doctor.ts +320 -0
- package/cli/commands/edit.ts +209 -9
- package/cli/commands/engines.ts +34 -3
- package/cli/commands/info.ts +81 -26
- package/cli/commands/list.ts +64 -9
- package/cli/commands/logs.ts +9 -3
- package/cli/commands/menu/backup-handlers.ts +52 -21
- package/cli/commands/menu/container-handlers.ts +433 -127
- package/cli/commands/menu/engine-handlers.ts +128 -4
- package/cli/commands/menu/index.ts +5 -1
- package/cli/commands/menu/shell-handlers.ts +105 -21
- package/cli/commands/menu/sql-handlers.ts +16 -4
- package/cli/commands/menu/update-handlers.ts +278 -0
- package/cli/commands/restore.ts +83 -23
- package/cli/commands/run.ts +27 -11
- package/cli/commands/url.ts +17 -9
- package/cli/constants.ts +1 -0
- package/cli/helpers.ts +41 -1
- package/cli/index.ts +2 -0
- package/cli/ui/prompts.ts +148 -7
- package/config/engine-defaults.ts +14 -0
- package/config/os-dependencies.ts +66 -0
- package/config/paths.ts +8 -0
- package/core/container-manager.ts +191 -32
- package/core/dependency-manager.ts +18 -0
- package/core/error-handler.ts +31 -0
- package/core/port-manager.ts +2 -0
- package/core/process-manager.ts +25 -3
- package/engines/index.ts +4 -0
- package/engines/mysql/backup.ts +53 -36
- package/engines/mysql/index.ts +48 -5
- package/engines/postgresql/index.ts +6 -0
- package/engines/sqlite/index.ts +606 -0
- package/engines/sqlite/registry.ts +185 -0
- package/package.json +1 -1
- package/types/index.ts +26 -0
package/core/error-handler.ts
CHANGED
|
@@ -56,6 +56,7 @@ export const ErrorCodes = {
|
|
|
56
56
|
CONTAINER_CREATE_FAILED: 'CONTAINER_CREATE_FAILED',
|
|
57
57
|
INIT_FAILED: 'INIT_FAILED',
|
|
58
58
|
DATABASE_CREATE_FAILED: 'DATABASE_CREATE_FAILED',
|
|
59
|
+
INVALID_DATABASE_NAME: 'INVALID_DATABASE_NAME',
|
|
59
60
|
|
|
60
61
|
// Dependency errors
|
|
61
62
|
DEPENDENCY_MISSING: 'DEPENDENCY_MISSING',
|
|
@@ -308,3 +309,33 @@ export function createDependencyMissingError(
|
|
|
308
309
|
{ toolName, engine },
|
|
309
310
|
)
|
|
310
311
|
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Validate a database name to prevent SQL injection.
|
|
315
|
+
* Database names must start with a letter and contain only
|
|
316
|
+
* alphanumeric characters and underscores.
|
|
317
|
+
*
|
|
318
|
+
* Note: Hyphens are excluded because they require quoted identifiers
|
|
319
|
+
* in SQL, which is error-prone for users.
|
|
320
|
+
*/
|
|
321
|
+
export function isValidDatabaseName(name: string): boolean {
|
|
322
|
+
// Must start with a letter to be valid in all database systems
|
|
323
|
+
// Hyphens excluded to avoid requiring quoted identifiers in SQL
|
|
324
|
+
return /^[a-zA-Z][a-zA-Z0-9_]*$/.test(name)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Assert that a database name is valid, throwing SpinDBError if not.
|
|
329
|
+
* Use this at the entry points where database names are accepted.
|
|
330
|
+
*/
|
|
331
|
+
export function assertValidDatabaseName(name: string): void {
|
|
332
|
+
if (!isValidDatabaseName(name)) {
|
|
333
|
+
throw new SpinDBError(
|
|
334
|
+
ErrorCodes.INVALID_DATABASE_NAME,
|
|
335
|
+
`Invalid database name: "${name}"`,
|
|
336
|
+
'error',
|
|
337
|
+
'Database names must start with a letter and contain only letters, numbers, and underscores',
|
|
338
|
+
{ databaseName: name },
|
|
339
|
+
)
|
|
340
|
+
}
|
|
341
|
+
}
|
package/core/port-manager.ts
CHANGED
|
@@ -27,6 +27,8 @@ export class PortManager {
|
|
|
27
27
|
const server = net.createServer()
|
|
28
28
|
|
|
29
29
|
server.once('error', (err: NodeJS.ErrnoException) => {
|
|
30
|
+
// Always close the server to prevent resource leaks
|
|
31
|
+
server.close()
|
|
30
32
|
if (err.code === 'EADDRINUSE') {
|
|
31
33
|
resolve(false)
|
|
32
34
|
} else {
|
package/core/process-manager.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { exec, spawn } from 'child_process'
|
|
2
2
|
import { promisify } from 'util'
|
|
3
3
|
import { existsSync } from 'fs'
|
|
4
|
-
import { readFile } from 'fs/promises'
|
|
4
|
+
import { readFile, rm } from 'fs/promises'
|
|
5
5
|
import { paths } from '../config/paths'
|
|
6
6
|
import { logDebug } from './error-handler'
|
|
7
7
|
import type { ProcessResult, StatusResult } from '../types'
|
|
@@ -42,6 +42,9 @@ export class ProcessManager {
|
|
|
42
42
|
): Promise<ProcessResult> {
|
|
43
43
|
const { superuser = 'postgres' } = options
|
|
44
44
|
|
|
45
|
+
// Track if directory existed before initdb (to know if we should clean up)
|
|
46
|
+
const dirExistedBefore = existsSync(dataDir)
|
|
47
|
+
|
|
45
48
|
const args = [
|
|
46
49
|
'-D',
|
|
47
50
|
dataDir,
|
|
@@ -52,6 +55,21 @@ export class ProcessManager {
|
|
|
52
55
|
'--no-locale',
|
|
53
56
|
]
|
|
54
57
|
|
|
58
|
+
// Helper to clean up data directory on failure
|
|
59
|
+
const cleanupOnFailure = async () => {
|
|
60
|
+
// Only clean up if initdb created the directory (it didn't exist before)
|
|
61
|
+
if (!dirExistedBefore && existsSync(dataDir)) {
|
|
62
|
+
try {
|
|
63
|
+
await rm(dataDir, { recursive: true, force: true })
|
|
64
|
+
logDebug(`Cleaned up data directory after initdb failure: ${dataDir}`)
|
|
65
|
+
} catch (cleanupErr) {
|
|
66
|
+
logDebug(
|
|
67
|
+
`Failed to clean up data directory: ${cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr)}`,
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
55
73
|
return new Promise((resolve, reject) => {
|
|
56
74
|
const proc = spawn(initdbPath, args, {
|
|
57
75
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
@@ -67,15 +85,19 @@ export class ProcessManager {
|
|
|
67
85
|
stderr += data.toString()
|
|
68
86
|
})
|
|
69
87
|
|
|
70
|
-
proc.on('close', (code) => {
|
|
88
|
+
proc.on('close', async (code) => {
|
|
71
89
|
if (code === 0) {
|
|
72
90
|
resolve({ stdout, stderr })
|
|
73
91
|
} else {
|
|
92
|
+
await cleanupOnFailure()
|
|
74
93
|
reject(new Error(`initdb failed with code ${code}: ${stderr}`))
|
|
75
94
|
}
|
|
76
95
|
})
|
|
77
96
|
|
|
78
|
-
proc.on('error',
|
|
97
|
+
proc.on('error', async (err) => {
|
|
98
|
+
await cleanupOnFailure()
|
|
99
|
+
reject(err)
|
|
100
|
+
})
|
|
79
101
|
})
|
|
80
102
|
}
|
|
81
103
|
|
package/engines/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { postgresqlEngine } from './postgresql'
|
|
2
2
|
import { mysqlEngine } from './mysql'
|
|
3
|
+
import { sqliteEngine } from './sqlite'
|
|
3
4
|
import type { BaseEngine } from './base-engine'
|
|
4
5
|
import type { EngineInfo } from '../types'
|
|
5
6
|
|
|
@@ -14,6 +15,9 @@ export const engines: Record<string, BaseEngine> = {
|
|
|
14
15
|
// MySQL and aliases
|
|
15
16
|
mysql: mysqlEngine,
|
|
16
17
|
mariadb: mysqlEngine, // MariaDB is MySQL-compatible
|
|
18
|
+
// SQLite and aliases
|
|
19
|
+
sqlite: sqliteEngine,
|
|
20
|
+
lite: sqliteEngine,
|
|
17
21
|
}
|
|
18
22
|
|
|
19
23
|
/**
|
package/engines/mysql/backup.ts
CHANGED
|
@@ -56,6 +56,20 @@ async function createSqlBackup(
|
|
|
56
56
|
outputPath: string,
|
|
57
57
|
): Promise<BackupResult> {
|
|
58
58
|
return new Promise((resolve, reject) => {
|
|
59
|
+
let settled = false
|
|
60
|
+
const safeResolve = (value: BackupResult) => {
|
|
61
|
+
if (!settled) {
|
|
62
|
+
settled = true
|
|
63
|
+
resolve(value)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const safeReject = (err: Error) => {
|
|
67
|
+
if (!settled) {
|
|
68
|
+
settled = true
|
|
69
|
+
reject(err)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
59
73
|
const args = [
|
|
60
74
|
'-h',
|
|
61
75
|
'127.0.0.1',
|
|
@@ -79,20 +93,20 @@ async function createSqlBackup(
|
|
|
79
93
|
})
|
|
80
94
|
|
|
81
95
|
proc.on('error', (err: NodeJS.ErrnoException) => {
|
|
82
|
-
|
|
96
|
+
safeReject(err)
|
|
83
97
|
})
|
|
84
98
|
|
|
85
99
|
proc.on('close', async (code) => {
|
|
86
100
|
if (code === 0) {
|
|
87
101
|
const stats = await stat(outputPath)
|
|
88
|
-
|
|
102
|
+
safeResolve({
|
|
89
103
|
path: outputPath,
|
|
90
104
|
format: 'sql',
|
|
91
105
|
size: stats.size,
|
|
92
106
|
})
|
|
93
107
|
} else {
|
|
94
108
|
const errorMessage = stderr || `mysqldump exited with code ${code}`
|
|
95
|
-
|
|
109
|
+
safeReject(new Error(errorMessage))
|
|
96
110
|
}
|
|
97
111
|
})
|
|
98
112
|
})
|
|
@@ -108,52 +122,55 @@ async function createCompressedBackup(
|
|
|
108
122
|
database: string,
|
|
109
123
|
outputPath: string,
|
|
110
124
|
): Promise<BackupResult> {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
})
|
|
125
|
+
const args = [
|
|
126
|
+
'-h',
|
|
127
|
+
'127.0.0.1',
|
|
128
|
+
'-P',
|
|
129
|
+
String(port),
|
|
130
|
+
'-u',
|
|
131
|
+
engineDef.superuser,
|
|
132
|
+
database,
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
const proc = spawn(mysqldump, args, {
|
|
136
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
137
|
+
})
|
|
125
138
|
|
|
126
|
-
|
|
127
|
-
|
|
139
|
+
const gzip = createGzip()
|
|
140
|
+
const output = createWriteStream(outputPath)
|
|
128
141
|
|
|
129
|
-
|
|
142
|
+
let stderr = ''
|
|
130
143
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
144
|
+
proc.stderr?.on('data', (data: Buffer) => {
|
|
145
|
+
stderr += data.toString()
|
|
146
|
+
})
|
|
134
147
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
.then(async () => {
|
|
138
|
-
const stats = await stat(outputPath)
|
|
139
|
-
resolve({
|
|
140
|
-
path: outputPath,
|
|
141
|
-
format: 'compressed',
|
|
142
|
-
size: stats.size,
|
|
143
|
-
})
|
|
144
|
-
})
|
|
145
|
-
.catch(reject)
|
|
148
|
+
// Create promise for pipeline completion
|
|
149
|
+
const pipelinePromise = pipeline(proc.stdout!, gzip, output)
|
|
146
150
|
|
|
151
|
+
// Create promise for process exit
|
|
152
|
+
const exitPromise = new Promise<void>((resolve, reject) => {
|
|
147
153
|
proc.on('error', (err: NodeJS.ErrnoException) => {
|
|
148
154
|
reject(err)
|
|
149
155
|
})
|
|
150
156
|
|
|
151
157
|
proc.on('close', (code) => {
|
|
152
|
-
if (code
|
|
158
|
+
if (code === 0) {
|
|
159
|
+
resolve()
|
|
160
|
+
} else {
|
|
153
161
|
const errorMessage = stderr || `mysqldump exited with code ${code}`
|
|
154
162
|
reject(new Error(errorMessage))
|
|
155
163
|
}
|
|
156
|
-
// If code is 0, the pipeline promise will resolve
|
|
157
164
|
})
|
|
158
165
|
})
|
|
166
|
+
|
|
167
|
+
// Wait for both pipeline AND process exit to succeed
|
|
168
|
+
await Promise.all([pipelinePromise, exitPromise])
|
|
169
|
+
|
|
170
|
+
const stats = await stat(outputPath)
|
|
171
|
+
return {
|
|
172
|
+
path: outputPath,
|
|
173
|
+
format: 'compressed',
|
|
174
|
+
size: stats.size,
|
|
175
|
+
}
|
|
159
176
|
}
|
package/engines/mysql/index.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { spawn, exec } from 'child_process'
|
|
7
7
|
import { promisify } from 'util'
|
|
8
8
|
import { existsSync, createReadStream } from 'fs'
|
|
9
|
-
import { mkdir, writeFile, readFile, unlink } from 'fs/promises'
|
|
9
|
+
import { mkdir, writeFile, readFile, unlink, rm } from 'fs/promises'
|
|
10
10
|
import { join } from 'path'
|
|
11
11
|
import { BaseEngine } from '../base-engine'
|
|
12
12
|
import { paths } from '../../config/paths'
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
logWarning,
|
|
17
17
|
ErrorCodes,
|
|
18
18
|
SpinDBError,
|
|
19
|
+
assertValidDatabaseName,
|
|
19
20
|
} from '../../core/error-handler'
|
|
20
21
|
import {
|
|
21
22
|
getMysqldPath,
|
|
@@ -135,9 +136,27 @@ export class MySQLEngine extends BaseEngine {
|
|
|
135
136
|
engine: ENGINE,
|
|
136
137
|
})
|
|
137
138
|
|
|
139
|
+
// Track if we created the directory (for cleanup on failure)
|
|
140
|
+
let createdDataDir = false
|
|
141
|
+
|
|
138
142
|
// Create data directory if it doesn't exist
|
|
139
143
|
if (!existsSync(dataDir)) {
|
|
140
144
|
await mkdir(dataDir, { recursive: true })
|
|
145
|
+
createdDataDir = true
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Helper to clean up on failure
|
|
149
|
+
const cleanupOnFailure = async () => {
|
|
150
|
+
if (createdDataDir) {
|
|
151
|
+
try {
|
|
152
|
+
await rm(dataDir, { recursive: true, force: true })
|
|
153
|
+
logDebug(`Cleaned up data directory after init failure: ${dataDir}`)
|
|
154
|
+
} catch (cleanupErr) {
|
|
155
|
+
logDebug(
|
|
156
|
+
`Failed to clean up data directory: ${cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr)}`,
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
141
160
|
}
|
|
142
161
|
|
|
143
162
|
// Check if we're using MariaDB or MySQL
|
|
@@ -148,6 +167,7 @@ export class MySQLEngine extends BaseEngine {
|
|
|
148
167
|
const installDb =
|
|
149
168
|
(await getMariadbInstallDbPath()) || (await getMysqlInstallDbPath())
|
|
150
169
|
if (!installDb) {
|
|
170
|
+
await cleanupOnFailure()
|
|
151
171
|
throw new Error(
|
|
152
172
|
'MariaDB detected but mysql_install_db not found.\n' +
|
|
153
173
|
'Install MariaDB server package which includes the initialization script.',
|
|
@@ -177,10 +197,11 @@ export class MySQLEngine extends BaseEngine {
|
|
|
177
197
|
stderr += data.toString()
|
|
178
198
|
})
|
|
179
199
|
|
|
180
|
-
proc.on('close', (code) => {
|
|
200
|
+
proc.on('close', async (code) => {
|
|
181
201
|
if (code === 0) {
|
|
182
202
|
resolve(dataDir)
|
|
183
203
|
} else {
|
|
204
|
+
await cleanupOnFailure()
|
|
184
205
|
reject(
|
|
185
206
|
new Error(
|
|
186
207
|
`MariaDB initialization failed with code ${code}: ${stderr || stdout}`,
|
|
@@ -189,12 +210,16 @@ export class MySQLEngine extends BaseEngine {
|
|
|
189
210
|
}
|
|
190
211
|
})
|
|
191
212
|
|
|
192
|
-
proc.on('error',
|
|
213
|
+
proc.on('error', async (err) => {
|
|
214
|
+
await cleanupOnFailure()
|
|
215
|
+
reject(err)
|
|
216
|
+
})
|
|
193
217
|
})
|
|
194
218
|
} else {
|
|
195
219
|
// MySQL uses mysqld --initialize-insecure
|
|
196
220
|
const mysqld = await getMysqldPath()
|
|
197
221
|
if (!mysqld) {
|
|
222
|
+
await cleanupOnFailure()
|
|
198
223
|
throw new Error(getInstallInstructions())
|
|
199
224
|
}
|
|
200
225
|
|
|
@@ -221,10 +246,11 @@ export class MySQLEngine extends BaseEngine {
|
|
|
221
246
|
stderr += data.toString()
|
|
222
247
|
})
|
|
223
248
|
|
|
224
|
-
proc.on('close', (code) => {
|
|
249
|
+
proc.on('close', async (code) => {
|
|
225
250
|
if (code === 0) {
|
|
226
251
|
resolve(dataDir)
|
|
227
252
|
} else {
|
|
253
|
+
await cleanupOnFailure()
|
|
228
254
|
reject(
|
|
229
255
|
new Error(
|
|
230
256
|
`MySQL initialization failed with code ${code}: ${stderr || stdout}`,
|
|
@@ -233,7 +259,10 @@ export class MySQLEngine extends BaseEngine {
|
|
|
233
259
|
}
|
|
234
260
|
})
|
|
235
261
|
|
|
236
|
-
proc.on('error',
|
|
262
|
+
proc.on('error', async (err) => {
|
|
263
|
+
await cleanupOnFailure()
|
|
264
|
+
reject(err)
|
|
265
|
+
})
|
|
237
266
|
})
|
|
238
267
|
}
|
|
239
268
|
}
|
|
@@ -313,6 +342,14 @@ export class MySQLEngine extends BaseEngine {
|
|
|
313
342
|
connectionString: this.getConnectionString(container),
|
|
314
343
|
})
|
|
315
344
|
return
|
|
345
|
+
} else {
|
|
346
|
+
// mysqladmin not found - cannot verify MySQL is ready
|
|
347
|
+
reject(
|
|
348
|
+
new Error(
|
|
349
|
+
'mysqladmin not found - cannot verify MySQL startup. Install MySQL client tools.',
|
|
350
|
+
),
|
|
351
|
+
)
|
|
352
|
+
return
|
|
316
353
|
}
|
|
317
354
|
} catch {
|
|
318
355
|
if (attempts < maxAttempts) {
|
|
@@ -687,6 +724,7 @@ export class MySQLEngine extends BaseEngine {
|
|
|
687
724
|
container: ContainerConfig,
|
|
688
725
|
database: string,
|
|
689
726
|
): Promise<void> {
|
|
727
|
+
assertValidDatabaseName(database)
|
|
690
728
|
const { port } = container
|
|
691
729
|
|
|
692
730
|
const mysql = await getMysqlClientPath()
|
|
@@ -720,6 +758,7 @@ export class MySQLEngine extends BaseEngine {
|
|
|
720
758
|
container: ContainerConfig,
|
|
721
759
|
database: string,
|
|
722
760
|
): Promise<void> {
|
|
761
|
+
assertValidDatabaseName(database)
|
|
723
762
|
const { port } = container
|
|
724
763
|
|
|
725
764
|
const mysql = await getMysqlClientPath()
|
|
@@ -748,6 +787,9 @@ export class MySQLEngine extends BaseEngine {
|
|
|
748
787
|
const { port, database } = container
|
|
749
788
|
const db = database || 'mysql'
|
|
750
789
|
|
|
790
|
+
// Validate database name to prevent SQL injection
|
|
791
|
+
assertValidDatabaseName(db)
|
|
792
|
+
|
|
751
793
|
try {
|
|
752
794
|
const mysql = await getMysqlClientPath()
|
|
753
795
|
if (!mysql) return null
|
|
@@ -856,6 +898,7 @@ export class MySQLEngine extends BaseEngine {
|
|
|
856
898
|
): Promise<void> {
|
|
857
899
|
const { port } = container
|
|
858
900
|
const db = options.database || container.database || 'mysql'
|
|
901
|
+
assertValidDatabaseName(db)
|
|
859
902
|
|
|
860
903
|
const mysql = await getMysqlClientPath()
|
|
861
904
|
if (!mysql) {
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
} from './binary-urls'
|
|
19
19
|
import { detectBackupFormat, restoreBackup } from './restore'
|
|
20
20
|
import { createBackup } from './backup'
|
|
21
|
+
import { assertValidDatabaseName } from '../../core/error-handler'
|
|
21
22
|
import type {
|
|
22
23
|
ContainerConfig,
|
|
23
24
|
ProgressCallback,
|
|
@@ -395,6 +396,7 @@ export class PostgreSQLEngine extends BaseEngine {
|
|
|
395
396
|
container: ContainerConfig,
|
|
396
397
|
database: string,
|
|
397
398
|
): Promise<void> {
|
|
399
|
+
assertValidDatabaseName(database)
|
|
398
400
|
const { port } = container
|
|
399
401
|
const psqlPath = await this.getPsqlPath()
|
|
400
402
|
|
|
@@ -418,6 +420,7 @@ export class PostgreSQLEngine extends BaseEngine {
|
|
|
418
420
|
container: ContainerConfig,
|
|
419
421
|
database: string,
|
|
420
422
|
): Promise<void> {
|
|
423
|
+
assertValidDatabaseName(database)
|
|
421
424
|
const { port } = container
|
|
422
425
|
const psqlPath = await this.getPsqlPath()
|
|
423
426
|
|
|
@@ -443,6 +446,9 @@ export class PostgreSQLEngine extends BaseEngine {
|
|
|
443
446
|
const { port, database } = container
|
|
444
447
|
const db = database || 'postgres'
|
|
445
448
|
|
|
449
|
+
// Validate database name to prevent SQL injection
|
|
450
|
+
assertValidDatabaseName(db)
|
|
451
|
+
|
|
446
452
|
try {
|
|
447
453
|
const psqlPath = await this.getPsqlPath()
|
|
448
454
|
// Query pg_database_size for the specific database
|