spindb 0.31.4 → 0.33.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/LICENSE +8 -0
- package/README.md +107 -826
- package/cli/commands/create.ts +5 -1
- package/cli/commands/engines.ts +256 -1
- package/cli/commands/menu/backup-handlers.ts +16 -0
- package/cli/commands/menu/container-handlers.ts +170 -17
- package/cli/commands/menu/engine-handlers.ts +6 -0
- package/cli/commands/menu/settings-handlers.ts +6 -0
- package/cli/commands/menu/shell-handlers.ts +74 -14
- package/cli/commands/menu/sql-handlers.ts +8 -50
- package/cli/commands/menu/validators.ts +8 -0
- package/cli/commands/users.ts +264 -0
- package/cli/constants.ts +8 -0
- package/cli/helpers.ts +140 -0
- package/cli/index.ts +2 -0
- package/cli/ui/prompts.ts +24 -20
- package/config/backup-formats.ts +28 -0
- package/config/engine-defaults.ts +26 -0
- package/config/engines-registry.ts +1 -0
- package/config/engines.json +50 -0
- package/config/engines.schema.json +6 -1
- package/core/base-binary-manager.ts +6 -1
- package/core/config-manager.ts +20 -0
- package/core/credential-manager.ts +257 -0
- package/core/dependency-manager.ts +5 -0
- package/core/docker-exporter.ts +30 -0
- package/core/error-handler.ts +19 -0
- package/engines/base-engine.ts +32 -1
- package/engines/clickhouse/index.ts +99 -3
- package/engines/cockroachdb/index.ts +69 -2
- package/engines/couchdb/index.ts +149 -1
- package/engines/ferretdb/README.md +4 -0
- package/engines/ferretdb/index.ts +342 -13
- package/engines/index.ts +8 -0
- package/engines/influxdb/README.md +180 -0
- package/engines/influxdb/api-client.ts +64 -0
- package/engines/influxdb/backup.ts +160 -0
- package/engines/influxdb/binary-manager.ts +110 -0
- package/engines/influxdb/binary-urls.ts +69 -0
- package/engines/influxdb/hostdb-releases.ts +23 -0
- package/engines/influxdb/index.ts +1227 -0
- package/engines/influxdb/restore.ts +417 -0
- package/engines/influxdb/version-maps.ts +75 -0
- package/engines/influxdb/version-validator.ts +128 -0
- package/engines/mariadb/index.ts +96 -1
- package/engines/meilisearch/index.ts +97 -1
- package/engines/mongodb/index.ts +82 -0
- package/engines/mysql/index.ts +105 -1
- package/engines/postgresql/index.ts +92 -0
- package/engines/qdrant/index.ts +107 -2
- package/engines/redis/index.ts +106 -12
- package/engines/surrealdb/index.ts +102 -2
- package/engines/typedb/backup.ts +167 -0
- package/engines/typedb/binary-manager.ts +200 -0
- package/engines/typedb/binary-urls.ts +38 -0
- package/engines/typedb/cli-utils.ts +210 -0
- package/engines/typedb/hostdb-releases.ts +118 -0
- package/engines/typedb/index.ts +1275 -0
- package/engines/typedb/restore.ts +377 -0
- package/engines/typedb/version-maps.ts +48 -0
- package/engines/typedb/version-validator.ts +127 -0
- package/engines/valkey/index.ts +70 -2
- package/package.json +4 -1
- package/types/index.ts +37 -0
package/engines/couchdb/index.ts
CHANGED
|
@@ -7,7 +7,11 @@ import { paths } from '../../config/paths'
|
|
|
7
7
|
import { getEngineDefaults } from '../../config/defaults'
|
|
8
8
|
import { platformService, isWindows } from '../../core/platform-service'
|
|
9
9
|
import { configManager } from '../../core/config-manager'
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
logDebug,
|
|
12
|
+
logWarning,
|
|
13
|
+
assertValidUsername,
|
|
14
|
+
} from '../../core/error-handler'
|
|
11
15
|
import { processManager } from '../../core/process-manager'
|
|
12
16
|
import { portManager } from '../../core/port-manager'
|
|
13
17
|
import { couchdbBinaryManager } from './binary-manager'
|
|
@@ -37,6 +41,8 @@ import {
|
|
|
37
41
|
type StatusResult,
|
|
38
42
|
type QueryResult,
|
|
39
43
|
type QueryOptions,
|
|
44
|
+
type CreateUserOptions,
|
|
45
|
+
type UserCredentials,
|
|
40
46
|
} from '../../types'
|
|
41
47
|
import { parseRESTAPIResult } from '../../core/query-parser'
|
|
42
48
|
|
|
@@ -1276,6 +1282,148 @@ export class CouchDBEngine extends BaseEngine {
|
|
|
1276
1282
|
|
|
1277
1283
|
return databases
|
|
1278
1284
|
}
|
|
1285
|
+
|
|
1286
|
+
async createUser(
|
|
1287
|
+
container: ContainerConfig,
|
|
1288
|
+
options: CreateUserOptions,
|
|
1289
|
+
): Promise<UserCredentials> {
|
|
1290
|
+
const { username, password, database } = options
|
|
1291
|
+
assertValidUsername(username)
|
|
1292
|
+
const { port } = container
|
|
1293
|
+
const db = database || container.database
|
|
1294
|
+
if (!db) {
|
|
1295
|
+
throw new Error(
|
|
1296
|
+
'No database specified. Use --database or create a database first.',
|
|
1297
|
+
)
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
// Ensure _users system database exists (CouchDB 3.x doesn't auto-create it)
|
|
1301
|
+
const usersDbResponse = await couchdbApiRequest(port, 'PUT', '/_users')
|
|
1302
|
+
if (usersDbResponse.status !== 201 && usersDbResponse.status !== 412) {
|
|
1303
|
+
throw new Error(
|
|
1304
|
+
`Failed to ensure _users database exists: ${JSON.stringify(usersDbResponse.data)}`,
|
|
1305
|
+
)
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
// Create user document in _users database
|
|
1309
|
+
const userDoc = {
|
|
1310
|
+
_id: `org.couchdb.user:${username}`,
|
|
1311
|
+
name: username,
|
|
1312
|
+
type: 'user',
|
|
1313
|
+
roles: [],
|
|
1314
|
+
password,
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
const createResponse = await couchdbApiRequest(
|
|
1318
|
+
port,
|
|
1319
|
+
'PUT',
|
|
1320
|
+
`/_users/org.couchdb.user:${encodeURIComponent(username)}`,
|
|
1321
|
+
userDoc as unknown as Record<string, unknown>,
|
|
1322
|
+
)
|
|
1323
|
+
|
|
1324
|
+
if (createResponse.status !== 201 && createResponse.status !== 409) {
|
|
1325
|
+
throw new Error(
|
|
1326
|
+
`Failed to create user: ${JSON.stringify(createResponse.data)}`,
|
|
1327
|
+
)
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
if (createResponse.status === 409) {
|
|
1331
|
+
// User exists — fetch current doc revision and update password
|
|
1332
|
+
const getResponse = await couchdbApiRequest(
|
|
1333
|
+
port,
|
|
1334
|
+
'GET',
|
|
1335
|
+
`/_users/org.couchdb.user:${encodeURIComponent(username)}`,
|
|
1336
|
+
)
|
|
1337
|
+
|
|
1338
|
+
if (getResponse.status !== 200 || !getResponse.data) {
|
|
1339
|
+
throw new Error(
|
|
1340
|
+
`Failed to fetch existing user "${username}" (status ${getResponse.status}): ${JSON.stringify(getResponse.data)}`,
|
|
1341
|
+
)
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
const existingDoc = getResponse.data as Record<string, unknown>
|
|
1345
|
+
const rev = existingDoc._rev as string
|
|
1346
|
+
if (!rev) {
|
|
1347
|
+
throw new Error(
|
|
1348
|
+
`User "${username}" already exists but document has no _rev field: ${JSON.stringify(getResponse.data)}`,
|
|
1349
|
+
)
|
|
1350
|
+
}
|
|
1351
|
+
const updateResponse = await couchdbApiRequest(
|
|
1352
|
+
port,
|
|
1353
|
+
'PUT',
|
|
1354
|
+
`/_users/org.couchdb.user:${encodeURIComponent(username)}`,
|
|
1355
|
+
{ ...existingDoc, password, _rev: rev },
|
|
1356
|
+
)
|
|
1357
|
+
if (updateResponse.status !== 201) {
|
|
1358
|
+
throw new Error(
|
|
1359
|
+
`Failed to update user: ${JSON.stringify(updateResponse.data)}`,
|
|
1360
|
+
)
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
// Ensure the target database exists before setting security
|
|
1365
|
+
const dbCreateResponse = await couchdbApiRequest(
|
|
1366
|
+
port,
|
|
1367
|
+
'PUT',
|
|
1368
|
+
`/${encodeURIComponent(db)}`,
|
|
1369
|
+
)
|
|
1370
|
+
// 201 = created, 412 = already exists — both are fine
|
|
1371
|
+
if (dbCreateResponse.status !== 201 && dbCreateResponse.status !== 412) {
|
|
1372
|
+
throw new Error(
|
|
1373
|
+
`Failed to ensure database "${db}" exists: ${JSON.stringify(dbCreateResponse.data)}`,
|
|
1374
|
+
)
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
// Grant access to the target database via _security document
|
|
1378
|
+
const secResponse = await couchdbApiRequest(
|
|
1379
|
+
port,
|
|
1380
|
+
'GET',
|
|
1381
|
+
`/${encodeURIComponent(db)}/_security`,
|
|
1382
|
+
)
|
|
1383
|
+
|
|
1384
|
+
if (secResponse.status !== 200) {
|
|
1385
|
+
throw new Error(
|
|
1386
|
+
`Failed to read database security for "${db}": ${JSON.stringify(secResponse.data)}`,
|
|
1387
|
+
)
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
const security = (secResponse.data || {}) as Record<string, unknown>
|
|
1391
|
+
const members = (security.members || {}) as Record<string, unknown>
|
|
1392
|
+
const names = ((members.names || []) as string[]).slice()
|
|
1393
|
+
|
|
1394
|
+
if (!names.includes(username)) {
|
|
1395
|
+
names.push(username)
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
const secPutResponse = await couchdbApiRequest(
|
|
1399
|
+
port,
|
|
1400
|
+
'PUT',
|
|
1401
|
+
`/${encodeURIComponent(db)}/_security`,
|
|
1402
|
+
{
|
|
1403
|
+
...security,
|
|
1404
|
+
members: { ...members, names },
|
|
1405
|
+
},
|
|
1406
|
+
)
|
|
1407
|
+
|
|
1408
|
+
if (secPutResponse.status !== 200 && secPutResponse.status !== 201) {
|
|
1409
|
+
throw new Error(
|
|
1410
|
+
`Failed to update database security for "${db}": ${JSON.stringify(secPutResponse.data)}`,
|
|
1411
|
+
)
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
logDebug(`Created CouchDB user: ${username}`)
|
|
1415
|
+
|
|
1416
|
+
const connectionString = `http://${encodeURIComponent(username)}:${encodeURIComponent(password)}@127.0.0.1:${port}/${encodeURIComponent(db)}`
|
|
1417
|
+
|
|
1418
|
+
return {
|
|
1419
|
+
username,
|
|
1420
|
+
password,
|
|
1421
|
+
connectionString,
|
|
1422
|
+
engine: container.engine,
|
|
1423
|
+
container: container.name,
|
|
1424
|
+
database: db,
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1279
1427
|
}
|
|
1280
1428
|
|
|
1281
1429
|
export const couchdbEngine = new CouchDBEngine()
|
|
@@ -23,6 +23,10 @@ This is a **composite engine** with unique binary management requirements.
|
|
|
23
23
|
|
|
24
24
|
FerretDB is **not available on Windows** due to postgresql-documentdb startup issues. The Windows binaries exist in hostdb, but the PostgreSQL backend fails to initialize properly. This has been extensively tested and currently requires WSL as a workaround.
|
|
25
25
|
|
|
26
|
+
### macOS SIP / Container Limitations
|
|
27
|
+
|
|
28
|
+
On macOS, System Integrity Protection (SIP) can block creating symlinks in system directories (e.g., `/usr/local`). In containerized or locked-down environments, even `sudo` may not permit writes to those paths. If you hit permission errors during setup, use a non-system install location or run with elevated privileges when available. See https://github.com/robertjbass/spindb#ferretdb for details.
|
|
29
|
+
|
|
26
30
|
## Binary Packaging
|
|
27
31
|
|
|
28
32
|
### Archive Format
|
|
@@ -15,8 +15,15 @@ import { spawn, exec, type SpawnOptions } from 'child_process'
|
|
|
15
15
|
import { promisify } from 'util'
|
|
16
16
|
import { existsSync } from 'fs'
|
|
17
17
|
import net from 'net'
|
|
18
|
-
import {
|
|
19
|
-
|
|
18
|
+
import {
|
|
19
|
+
mkdir,
|
|
20
|
+
writeFile,
|
|
21
|
+
readFile,
|
|
22
|
+
symlink,
|
|
23
|
+
unlink,
|
|
24
|
+
readdir,
|
|
25
|
+
} from 'fs/promises'
|
|
26
|
+
import { join, basename, dirname } from 'path'
|
|
20
27
|
import { BaseEngine } from '../base-engine'
|
|
21
28
|
import { paths } from '../../config/paths'
|
|
22
29
|
import { getEngineDefaults } from '../../config/defaults'
|
|
@@ -27,6 +34,7 @@ import {
|
|
|
27
34
|
logDebug,
|
|
28
35
|
logWarning,
|
|
29
36
|
assertValidDatabaseName,
|
|
37
|
+
assertValidUsername,
|
|
30
38
|
} from '../../core/error-handler'
|
|
31
39
|
import { processManager } from '../../core/process-manager'
|
|
32
40
|
import { spawnAsync } from '../../core/spawn-utils'
|
|
@@ -57,6 +65,8 @@ import {
|
|
|
57
65
|
type StatusResult,
|
|
58
66
|
type QueryResult,
|
|
59
67
|
type QueryOptions,
|
|
68
|
+
type CreateUserOptions,
|
|
69
|
+
type UserCredentials,
|
|
60
70
|
} from '../../types'
|
|
61
71
|
import { parseMongoDBResult } from '../../core/query-parser'
|
|
62
72
|
|
|
@@ -332,12 +342,130 @@ export class FerretDBEngine extends BaseEngine {
|
|
|
332
342
|
arch,
|
|
333
343
|
)
|
|
334
344
|
|
|
345
|
+
// Homebrew-derived x64 binaries have compiled-in absolute paths for
|
|
346
|
+
// sharedir, pkglibdir ($libdir), and libdir that don't exist when running
|
|
347
|
+
// from ~/.spindb/bin/. We fix this by:
|
|
348
|
+
// 1. Using initdb's -L flag to explicitly set the share directory
|
|
349
|
+
// 2. Creating symlinks at compiled-in paths for pkglibdir and libdir
|
|
350
|
+
// (these are needed by the bootstrap postgres subprocess during initdb)
|
|
351
|
+
const shareDirBase = join(documentdbPath, 'share')
|
|
352
|
+
const actualShareDir = existsSync(join(shareDirBase, 'postgres.bki'))
|
|
353
|
+
? shareDirBase
|
|
354
|
+
: existsSync(join(shareDirBase, 'postgresql', 'postgres.bki'))
|
|
355
|
+
? join(shareDirBase, 'postgresql')
|
|
356
|
+
: shareDirBase
|
|
357
|
+
|
|
358
|
+
// Homebrew-derived binaries have compiled-in absolute paths that only
|
|
359
|
+
// need fixup on macOS. On Linux the paths are relative or handled by
|
|
360
|
+
// LD_LIBRARY_PATH, so skip the pg_config symlink fixups entirely.
|
|
361
|
+
if (platform === 'darwin') {
|
|
362
|
+
const pgConfigBin = join(documentdbPath, 'bin', `pg_config${ext}`)
|
|
363
|
+
if (existsSync(pgConfigBin)) {
|
|
364
|
+
// Query all relevant compiled-in paths and create symlinks where needed
|
|
365
|
+
const pathFixups: Array<{
|
|
366
|
+
flag: string
|
|
367
|
+
actualDir: string
|
|
368
|
+
label: string
|
|
369
|
+
}> = [
|
|
370
|
+
{ flag: '--sharedir', actualDir: actualShareDir, label: 'share' },
|
|
371
|
+
{
|
|
372
|
+
flag: '--pkglibdir',
|
|
373
|
+
actualDir: existsSync(join(documentdbPath, 'lib', 'postgresql'))
|
|
374
|
+
? join(documentdbPath, 'lib', 'postgresql')
|
|
375
|
+
: join(documentdbPath, 'lib'),
|
|
376
|
+
label: 'pkglib',
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
flag: '--libdir',
|
|
380
|
+
actualDir: join(documentdbPath, 'lib'),
|
|
381
|
+
label: 'lib',
|
|
382
|
+
},
|
|
383
|
+
]
|
|
384
|
+
|
|
385
|
+
// Create symlinks at compiled-in paths so PostgreSQL can find its
|
|
386
|
+
// libraries. These paths may be in system directories (e.g. /usr/local/),
|
|
387
|
+
// which require elevated privileges to write to.
|
|
388
|
+
for (const { flag, actualDir, label } of pathFixups) {
|
|
389
|
+
try {
|
|
390
|
+
const { stdout: out } = await execAsync(
|
|
391
|
+
`"${pgConfigBin}" ${flag}`,
|
|
392
|
+
{ timeout: 5000 },
|
|
393
|
+
)
|
|
394
|
+
const compiledDir = out.trim()
|
|
395
|
+
logDebug(`pg_config ${flag}: ${compiledDir}`)
|
|
396
|
+
if (compiledDir && !existsSync(compiledDir)) {
|
|
397
|
+
await mkdir(dirname(compiledDir), { recursive: true })
|
|
398
|
+
await symlink(actualDir, compiledDir)
|
|
399
|
+
logDebug(
|
|
400
|
+
`Created ${label} symlink: ${compiledDir} -> ${actualDir}`,
|
|
401
|
+
)
|
|
402
|
+
}
|
|
403
|
+
} catch (error) {
|
|
404
|
+
const e = error as NodeJS.ErrnoException
|
|
405
|
+
const isPermission =
|
|
406
|
+
e.code === 'EACCES' ||
|
|
407
|
+
e.code === 'EPERM' ||
|
|
408
|
+
(e.message && /permission denied/i.test(e.message))
|
|
409
|
+
if (isPermission) {
|
|
410
|
+
logWarning(
|
|
411
|
+
`Cannot create ${label} symlink (permission denied). ` +
|
|
412
|
+
`This can be caused by macOS SIP or container/sudo limitations when compiled-in paths point to system directories. ` +
|
|
413
|
+
`Workaround: use a non-system install path, or run with elevated privileges if available (e.g., sudo spindb engines download ferretdb <version>). ` +
|
|
414
|
+
`See https://github.com/robertjbass/spindb#ferretdb for details. ` +
|
|
415
|
+
`Target: ${flag} -> ${actualDir}`,
|
|
416
|
+
)
|
|
417
|
+
} else {
|
|
418
|
+
logDebug(`Could not fix compiled ${label} path: ${e.message}`)
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
} else {
|
|
424
|
+
logDebug(
|
|
425
|
+
'Skipping pg_config symlink fixups (not required on this platform)',
|
|
426
|
+
)
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// On macOS, fix hardcoded Homebrew dylib paths in extension libraries.
|
|
430
|
+
// The x64 build may have extensions (e.g. pg_documentdb_core.dylib) that
|
|
431
|
+
// reference Homebrew libraries (e.g. libbson2.2.dylib from mongo-c-driver)
|
|
432
|
+
// via absolute paths that don't exist on the target machine.
|
|
433
|
+
if (platform === 'darwin') {
|
|
434
|
+
const dylibMarker = join(documentdbPath, '.dylib_fix_done')
|
|
435
|
+
if (!existsSync(dylibMarker)) {
|
|
436
|
+
await this.fixDylibDependencies(documentdbPath)
|
|
437
|
+
try {
|
|
438
|
+
await writeFile(dylibMarker, '', { flag: 'wx' })
|
|
439
|
+
} catch {
|
|
440
|
+
// Marker may already exist from a parallel init — safe to ignore
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// On macOS, set DYLD_FALLBACK_LIBRARY_PATH as additional library search path.
|
|
446
|
+
// Unlike DYLD_LIBRARY_PATH, this is NOT stripped by SIP.
|
|
447
|
+
const initdbEnv =
|
|
448
|
+
platform === 'darwin'
|
|
449
|
+
? {
|
|
450
|
+
...spawnEnv,
|
|
451
|
+
DYLD_FALLBACK_LIBRARY_PATH: join(documentdbPath, 'lib'),
|
|
452
|
+
}
|
|
453
|
+
: spawnEnv
|
|
454
|
+
|
|
335
455
|
try {
|
|
336
|
-
// Add timeout to prevent hanging on Windows
|
|
337
456
|
await spawnAsync(
|
|
338
457
|
initdb,
|
|
339
|
-
[
|
|
340
|
-
|
|
458
|
+
[
|
|
459
|
+
'-D',
|
|
460
|
+
pgDataDir,
|
|
461
|
+
'-U',
|
|
462
|
+
'postgres',
|
|
463
|
+
'--encoding=UTF8',
|
|
464
|
+
'--locale=C',
|
|
465
|
+
'-L',
|
|
466
|
+
actualShareDir,
|
|
467
|
+
],
|
|
468
|
+
{ env: initdbEnv, timeout: 60000 },
|
|
341
469
|
)
|
|
342
470
|
logDebug(`Initialized PostgreSQL data directory: ${pgDataDir}`)
|
|
343
471
|
} catch (error) {
|
|
@@ -347,11 +475,11 @@ export class FerretDBEngine extends BaseEngine {
|
|
|
347
475
|
|
|
348
476
|
// Copy the bundled postgresql.conf.sample to ensure shared_preload_libraries is set
|
|
349
477
|
// This is critical for DocumentDB extension to load properly
|
|
350
|
-
const bundledConf =
|
|
351
|
-
|
|
352
|
-
'share',
|
|
353
|
-
'postgresql.conf.sample',
|
|
478
|
+
const bundledConf = existsSync(
|
|
479
|
+
join(shareDirBase, 'postgresql.conf.sample'),
|
|
354
480
|
)
|
|
481
|
+
? join(shareDirBase, 'postgresql.conf.sample')
|
|
482
|
+
: join(shareDirBase, 'postgresql', 'postgresql.conf.sample')
|
|
355
483
|
const pgConf = join(pgDataDir, 'postgresql.conf')
|
|
356
484
|
|
|
357
485
|
if (existsSync(bundledConf)) {
|
|
@@ -433,12 +561,21 @@ export class FerretDBEngine extends BaseEngine {
|
|
|
433
561
|
arch,
|
|
434
562
|
)
|
|
435
563
|
|
|
436
|
-
// Get spawn env for
|
|
437
|
-
|
|
564
|
+
// Get spawn env for postgresql-documentdb binaries:
|
|
565
|
+
// - Linux: LD_LIBRARY_PATH for shared libraries
|
|
566
|
+
// - macOS: DYLD_FALLBACK_LIBRARY_PATH (not stripped by SIP)
|
|
567
|
+
const baseSpawnEnv = ferretdbBinaryManager.getDocumentDBSpawnEnv(
|
|
438
568
|
fullBackendVersion,
|
|
439
569
|
platform,
|
|
440
570
|
arch,
|
|
441
571
|
)
|
|
572
|
+
const pgSpawnEnv =
|
|
573
|
+
platform === 'darwin'
|
|
574
|
+
? {
|
|
575
|
+
...baseSpawnEnv,
|
|
576
|
+
DYLD_FALLBACK_LIBRARY_PATH: join(documentdbPath, 'lib'),
|
|
577
|
+
}
|
|
578
|
+
: baseSpawnEnv
|
|
442
579
|
|
|
443
580
|
const ext = platformService.getExecutableExtension()
|
|
444
581
|
const ferretdbBinary = join(ferretdbPath, 'bin', `ferretdb${ext}`)
|
|
@@ -467,6 +604,20 @@ export class FerretDBEngine extends BaseEngine {
|
|
|
467
604
|
// Allocate backend port
|
|
468
605
|
const backendPort = existingBackendPort || (await allocateBackendPort())
|
|
469
606
|
|
|
607
|
+
// Fix hardcoded Homebrew dylib paths (darwin-x64 binaries)
|
|
608
|
+
// Skip if already completed (marker written by initDataDir or a previous start)
|
|
609
|
+
if (platform === 'darwin') {
|
|
610
|
+
const dylibMarker = join(documentdbPath, '.dylib_fix_done')
|
|
611
|
+
if (!existsSync(dylibMarker)) {
|
|
612
|
+
await this.fixDylibDependencies(documentdbPath)
|
|
613
|
+
try {
|
|
614
|
+
await writeFile(dylibMarker, '', { flag: 'wx' })
|
|
615
|
+
} catch {
|
|
616
|
+
// Marker may already exist — safe to ignore
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
470
621
|
let pgStarted = false
|
|
471
622
|
let ferretStarted = false
|
|
472
623
|
|
|
@@ -699,12 +850,19 @@ export class FerretDBEngine extends BaseEngine {
|
|
|
699
850
|
arch,
|
|
700
851
|
)
|
|
701
852
|
|
|
702
|
-
// Get spawn env for
|
|
703
|
-
const
|
|
853
|
+
// Get spawn env for postgresql-documentdb binaries
|
|
854
|
+
const baseStopEnv = ferretdbBinaryManager.getDocumentDBSpawnEnv(
|
|
704
855
|
fullBackendVersion,
|
|
705
856
|
platform,
|
|
706
857
|
arch,
|
|
707
858
|
)
|
|
859
|
+
const pgSpawnEnv =
|
|
860
|
+
platform === 'darwin'
|
|
861
|
+
? {
|
|
862
|
+
...baseStopEnv,
|
|
863
|
+
DYLD_FALLBACK_LIBRARY_PATH: join(documentdbPath, 'lib'),
|
|
864
|
+
}
|
|
865
|
+
: baseStopEnv
|
|
708
866
|
|
|
709
867
|
const ext = platformService.getExecutableExtension()
|
|
710
868
|
const pgCtl = join(documentdbPath, 'bin', `pg_ctl${ext}`)
|
|
@@ -725,6 +883,99 @@ export class FerretDBEngine extends BaseEngine {
|
|
|
725
883
|
logDebug('FerretDB stopped')
|
|
726
884
|
}
|
|
727
885
|
|
|
886
|
+
/**
|
|
887
|
+
* Fix hardcoded Homebrew dylib paths in extension libraries.
|
|
888
|
+
*
|
|
889
|
+
* The x64 darwin build of postgresql-documentdb has extensions whose dylib
|
|
890
|
+
* load commands reference absolute Homebrew paths (e.g.
|
|
891
|
+
* /usr/local/opt/mongo-c-driver/lib/libbson2.2.dylib). When these paths
|
|
892
|
+
* don't exist on the target machine, the extension fails to load.
|
|
893
|
+
*
|
|
894
|
+
* This method scans extension dylibs with `otool -L`, finds missing
|
|
895
|
+
* dependencies, searches our bundle for matching libraries, and creates
|
|
896
|
+
* symlinks at the expected Homebrew paths.
|
|
897
|
+
*/
|
|
898
|
+
private async fixDylibDependencies(documentdbPath: string): Promise<void> {
|
|
899
|
+
const libDir = join(documentdbPath, 'lib')
|
|
900
|
+
const pkgLibDir = join(libDir, 'postgresql')
|
|
901
|
+
|
|
902
|
+
if (!existsSync(pkgLibDir)) return
|
|
903
|
+
|
|
904
|
+
// Collect all .dylib files in our bundle's lib/ directory
|
|
905
|
+
const bundledLibNames = new Set<string>()
|
|
906
|
+
const bundledLibPaths = new Map<string, string>()
|
|
907
|
+
try {
|
|
908
|
+
const libFiles = await readdir(libDir)
|
|
909
|
+
for (const f of libFiles) {
|
|
910
|
+
if (f.endsWith('.dylib')) {
|
|
911
|
+
bundledLibNames.add(f)
|
|
912
|
+
bundledLibPaths.set(f, join(libDir, f))
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
} catch {
|
|
916
|
+
return
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// Scan extension dylibs for missing dependencies
|
|
920
|
+
let extFiles: string[]
|
|
921
|
+
try {
|
|
922
|
+
extFiles = (await readdir(pkgLibDir)).filter((f) => f.endsWith('.dylib'))
|
|
923
|
+
} catch {
|
|
924
|
+
return
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
for (const extFile of extFiles) {
|
|
928
|
+
const extPath = join(pkgLibDir, extFile)
|
|
929
|
+
try {
|
|
930
|
+
const { stdout } = await execAsync(`otool -L "${extPath}"`, {
|
|
931
|
+
timeout: 5000,
|
|
932
|
+
})
|
|
933
|
+
|
|
934
|
+
for (const line of stdout.split('\n')) {
|
|
935
|
+
const match = line.trim().match(/^(\/[^\s]+\.dylib)\s/)
|
|
936
|
+
if (!match) continue
|
|
937
|
+
const depPath = match[1]
|
|
938
|
+
|
|
939
|
+
// Skip system libs, @-prefixed paths, and paths in our bundle
|
|
940
|
+
if (depPath.startsWith('/usr/lib/')) continue
|
|
941
|
+
if (depPath.startsWith('/System/')) continue
|
|
942
|
+
if (depPath.startsWith('@')) continue
|
|
943
|
+
if (depPath.includes(documentdbPath)) continue
|
|
944
|
+
|
|
945
|
+
if (!existsSync(depPath)) {
|
|
946
|
+
const depName = basename(depPath)
|
|
947
|
+
|
|
948
|
+
// Check if we have this exact library in our bundle
|
|
949
|
+
if (bundledLibPaths.has(depName)) {
|
|
950
|
+
try {
|
|
951
|
+
await mkdir(dirname(depPath), { recursive: true })
|
|
952
|
+
} catch {
|
|
953
|
+
logDebug(
|
|
954
|
+
`Cannot create directory for dylib dep: ${dirname(depPath)} (skipping)`,
|
|
955
|
+
)
|
|
956
|
+
continue
|
|
957
|
+
}
|
|
958
|
+
try {
|
|
959
|
+
await symlink(bundledLibPaths.get(depName)!, depPath)
|
|
960
|
+
logDebug(
|
|
961
|
+
`Fixed dylib dep: ${depPath} -> ${bundledLibPaths.get(depName)}`,
|
|
962
|
+
)
|
|
963
|
+
} catch {
|
|
964
|
+
// Symlink may already exist from a parallel fix
|
|
965
|
+
}
|
|
966
|
+
} else {
|
|
967
|
+
logDebug(
|
|
968
|
+
`Missing dylib dependency: ${depPath} (not found in bundle)`,
|
|
969
|
+
)
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
} catch {
|
|
974
|
+
logDebug(`Could not scan dylib deps for ${extFile}`)
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
728
979
|
/**
|
|
729
980
|
* Stop FerretDB proxy process
|
|
730
981
|
*/
|
|
@@ -1343,6 +1594,84 @@ export class FerretDBEngine extends BaseEngine {
|
|
|
1343
1594
|
})
|
|
1344
1595
|
})
|
|
1345
1596
|
}
|
|
1597
|
+
|
|
1598
|
+
async createUser(
|
|
1599
|
+
container: ContainerConfig,
|
|
1600
|
+
options: CreateUserOptions,
|
|
1601
|
+
): Promise<UserCredentials> {
|
|
1602
|
+
const { username, password, database } = options
|
|
1603
|
+
assertValidUsername(username)
|
|
1604
|
+
const { port } = container
|
|
1605
|
+
const db = database ?? container.database ?? 'admin'
|
|
1606
|
+
assertValidDatabaseName(db)
|
|
1607
|
+
const mongosh = await this.getMongoshPath()
|
|
1608
|
+
|
|
1609
|
+
// Same as MongoDB - auth disabled with --no-auth but user is still created
|
|
1610
|
+
// Use JSON.stringify for password to safely escape all special characters in JS context
|
|
1611
|
+
// Pass script via stdin to avoid exposing passwords in process listings
|
|
1612
|
+
const jsonPwd = JSON.stringify(password)
|
|
1613
|
+
const script = `db.getSiblingDB('${db}').createUser({user:'${username}',pwd:${jsonPwd},roles:[{role:'readWrite',db:'${db}'}]})`
|
|
1614
|
+
|
|
1615
|
+
const mongoshArgs = ['--host', '127.0.0.1', '--port', String(port), 'admin']
|
|
1616
|
+
|
|
1617
|
+
const runMongoshViaStdin = (js: string): Promise<void> =>
|
|
1618
|
+
new Promise((resolve, reject) => {
|
|
1619
|
+
const proc = spawn(mongosh, mongoshArgs, {
|
|
1620
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1621
|
+
})
|
|
1622
|
+
|
|
1623
|
+
let stderr = ''
|
|
1624
|
+
proc.stderr?.on('data', (data: Buffer) => {
|
|
1625
|
+
stderr += data.toString()
|
|
1626
|
+
})
|
|
1627
|
+
|
|
1628
|
+
const timeout = setTimeout(() => {
|
|
1629
|
+
proc.kill('SIGTERM')
|
|
1630
|
+
reject(new Error('mongosh timed out after 10 seconds'))
|
|
1631
|
+
}, 10000)
|
|
1632
|
+
|
|
1633
|
+
proc.on('error', (err) => {
|
|
1634
|
+
clearTimeout(timeout)
|
|
1635
|
+
reject(err)
|
|
1636
|
+
})
|
|
1637
|
+
|
|
1638
|
+
proc.on('close', (code) => {
|
|
1639
|
+
clearTimeout(timeout)
|
|
1640
|
+
if (code === 0) resolve()
|
|
1641
|
+
else reject(new Error(stderr || `mongosh exited with code ${code}`))
|
|
1642
|
+
})
|
|
1643
|
+
|
|
1644
|
+
proc.stdin?.write(js)
|
|
1645
|
+
proc.stdin?.end()
|
|
1646
|
+
})
|
|
1647
|
+
|
|
1648
|
+
try {
|
|
1649
|
+
await runMongoshViaStdin(script)
|
|
1650
|
+
} catch (error) {
|
|
1651
|
+
const err = error as Error
|
|
1652
|
+
if (
|
|
1653
|
+
err.message.includes('51003') ||
|
|
1654
|
+
err.message.includes('already exists')
|
|
1655
|
+
) {
|
|
1656
|
+
// User exists — update password instead
|
|
1657
|
+
const updateScript = `db.getSiblingDB('${db}').updateUser('${username}',{pwd:${jsonPwd}})`
|
|
1658
|
+
await runMongoshViaStdin(updateScript)
|
|
1659
|
+
} else {
|
|
1660
|
+
throw error
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
const connectionString = `mongodb://${encodeURIComponent(username)}:${encodeURIComponent(password)}@127.0.0.1:${port}/${db}`
|
|
1665
|
+
|
|
1666
|
+
return {
|
|
1667
|
+
username,
|
|
1668
|
+
password,
|
|
1669
|
+
connectionString,
|
|
1670
|
+
engine: container.engine,
|
|
1671
|
+
container: container.name,
|
|
1672
|
+
database: db,
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1346
1675
|
}
|
|
1347
1676
|
|
|
1348
1677
|
export const ferretdbEngine = new FerretDBEngine()
|
package/engines/index.ts
CHANGED
|
@@ -14,6 +14,8 @@ import { couchdbEngine } from './couchdb'
|
|
|
14
14
|
import { cockroachdbEngine } from './cockroachdb'
|
|
15
15
|
import { surrealdbEngine } from './surrealdb'
|
|
16
16
|
import { questdbEngine } from './questdb'
|
|
17
|
+
import { typedbEngine } from './typedb'
|
|
18
|
+
import { influxdbEngine } from './influxdb'
|
|
17
19
|
import { platformService } from '../core/platform-service'
|
|
18
20
|
import { Engine, Platform } from '../types'
|
|
19
21
|
import type { BaseEngine } from './base-engine'
|
|
@@ -76,6 +78,12 @@ export const engines: Record<string, BaseEngine> = {
|
|
|
76
78
|
// QuestDB and aliases
|
|
77
79
|
[Engine.QuestDB]: questdbEngine,
|
|
78
80
|
quest: questdbEngine,
|
|
81
|
+
// TypeDB and aliases
|
|
82
|
+
[Engine.TypeDB]: typedbEngine,
|
|
83
|
+
tdb: typedbEngine,
|
|
84
|
+
// InfluxDB and aliases
|
|
85
|
+
[Engine.InfluxDB]: influxdbEngine,
|
|
86
|
+
influx: influxdbEngine,
|
|
79
87
|
}
|
|
80
88
|
|
|
81
89
|
// Get an engine by name
|