spindb 0.46.5 → 0.47.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.
Files changed (129) hide show
  1. package/dist/cli/commands/info.js +12 -2
  2. package/dist/cli/commands/info.js.map +1 -1
  3. package/dist/cli/commands/link.js +6 -0
  4. package/dist/cli/commands/link.js.map +1 -1
  5. package/dist/cli/commands/list.js +4 -1
  6. package/dist/cli/commands/list.js.map +1 -1
  7. package/dist/cli/commands/menu/container-handlers.js +25 -7
  8. package/dist/cli/commands/menu/container-handlers.js.map +1 -1
  9. package/dist/config/backup-formats.js +11 -11
  10. package/dist/config/backup-formats.js.map +1 -1
  11. package/dist/config/version.js +1 -1
  12. package/dist/core/credential-manager.js +34 -11
  13. package/dist/core/credential-manager.js.map +1 -1
  14. package/dist/core/query-parser.js +55 -1
  15. package/dist/core/query-parser.js.map +1 -1
  16. package/dist/core/remote-container.js +11 -0
  17. package/dist/core/remote-container.js.map +1 -1
  18. package/dist/engines/clickhouse/backup.js +42 -10
  19. package/dist/engines/clickhouse/backup.js.map +1 -1
  20. package/dist/engines/clickhouse/restore.js +41 -10
  21. package/dist/engines/clickhouse/restore.js.map +1 -1
  22. package/dist/engines/cockroachdb/backup.js +18 -22
  23. package/dist/engines/cockroachdb/backup.js.map +1 -1
  24. package/dist/engines/cockroachdb/cli-utils.js +66 -0
  25. package/dist/engines/cockroachdb/cli-utils.js.map +1 -1
  26. package/dist/engines/cockroachdb/index.js +199 -116
  27. package/dist/engines/cockroachdb/index.js.map +1 -1
  28. package/dist/engines/cockroachdb/restore.js +19 -26
  29. package/dist/engines/cockroachdb/restore.js.map +1 -1
  30. package/dist/engines/couchdb/backup.js +13 -4
  31. package/dist/engines/couchdb/backup.js.map +1 -1
  32. package/dist/engines/couchdb/index.js +93 -25
  33. package/dist/engines/couchdb/index.js.map +1 -1
  34. package/dist/engines/couchdb/restore.js +15 -4
  35. package/dist/engines/couchdb/restore.js.map +1 -1
  36. package/dist/engines/ferretdb/backup.js +88 -91
  37. package/dist/engines/ferretdb/backup.js.map +1 -1
  38. package/dist/engines/ferretdb/index.js +182 -227
  39. package/dist/engines/ferretdb/index.js.map +1 -1
  40. package/dist/engines/ferretdb/restore.js +223 -20
  41. package/dist/engines/ferretdb/restore.js.map +1 -1
  42. package/dist/engines/influxdb/api-client.js +1 -1
  43. package/dist/engines/influxdb/api-client.js.map +1 -1
  44. package/dist/engines/influxdb/backup.js +25 -5
  45. package/dist/engines/influxdb/backup.js.map +1 -1
  46. package/dist/engines/influxdb/index.js +165 -43
  47. package/dist/engines/influxdb/index.js.map +1 -1
  48. package/dist/engines/influxdb/restore.js +22 -2
  49. package/dist/engines/influxdb/restore.js.map +1 -1
  50. package/dist/engines/mariadb/backup.js +24 -15
  51. package/dist/engines/mariadb/backup.js.map +1 -1
  52. package/dist/engines/mariadb/env.js +11 -0
  53. package/dist/engines/mariadb/env.js.map +1 -0
  54. package/dist/engines/mariadb/index.js +192 -113
  55. package/dist/engines/mariadb/index.js.map +1 -1
  56. package/dist/engines/mariadb/restore.js +28 -5
  57. package/dist/engines/mariadb/restore.js.map +1 -1
  58. package/dist/engines/meilisearch/backup.js +8 -4
  59. package/dist/engines/meilisearch/backup.js.map +1 -1
  60. package/dist/engines/meilisearch/index.js +55 -58
  61. package/dist/engines/meilisearch/index.js.map +1 -1
  62. package/dist/engines/mongo-uri.js +11 -0
  63. package/dist/engines/mongo-uri.js.map +1 -0
  64. package/dist/engines/mongodb/backup.js +62 -13
  65. package/dist/engines/mongodb/backup.js.map +1 -1
  66. package/dist/engines/mongodb/index.js +170 -108
  67. package/dist/engines/mongodb/index.js.map +1 -1
  68. package/dist/engines/mongodb/restore.js +21 -1
  69. package/dist/engines/mongodb/restore.js.map +1 -1
  70. package/dist/engines/mysql/backup.js +24 -7
  71. package/dist/engines/mysql/backup.js.map +1 -1
  72. package/dist/engines/mysql/index.js +154 -89
  73. package/dist/engines/mysql/index.js.map +1 -1
  74. package/dist/engines/mysql/restore.js +14 -4
  75. package/dist/engines/mysql/restore.js.map +1 -1
  76. package/dist/engines/postgresql/backup.js +9 -2
  77. package/dist/engines/postgresql/backup.js.map +1 -1
  78. package/dist/engines/postgresql/index.js +45 -17
  79. package/dist/engines/postgresql/index.js.map +1 -1
  80. package/dist/engines/postgresql/restore.js +7 -3
  81. package/dist/engines/postgresql/restore.js.map +1 -1
  82. package/dist/engines/qdrant/backup.js +5 -1
  83. package/dist/engines/qdrant/backup.js.map +1 -1
  84. package/dist/engines/qdrant/index.js +31 -2
  85. package/dist/engines/qdrant/index.js.map +1 -1
  86. package/dist/engines/qdrant/restore.js +5 -3
  87. package/dist/engines/qdrant/restore.js.map +1 -1
  88. package/dist/engines/questdb/auth.js +26 -0
  89. package/dist/engines/questdb/auth.js.map +1 -0
  90. package/dist/engines/questdb/backup.js +10 -8
  91. package/dist/engines/questdb/backup.js.map +1 -1
  92. package/dist/engines/questdb/index.js +16 -8
  93. package/dist/engines/questdb/index.js.map +1 -1
  94. package/dist/engines/questdb/restore.js +7 -5
  95. package/dist/engines/questdb/restore.js.map +1 -1
  96. package/dist/engines/redis/backup.js +45 -16
  97. package/dist/engines/redis/backup.js.map +1 -1
  98. package/dist/engines/redis/cli-common.js +62 -0
  99. package/dist/engines/redis/cli-common.js.map +1 -0
  100. package/dist/engines/redis/index.js +31 -8
  101. package/dist/engines/redis/index.js.map +1 -1
  102. package/dist/engines/redis/restore.js +59 -9
  103. package/dist/engines/redis/restore.js.map +1 -1
  104. package/dist/engines/surrealdb/auth.js +98 -0
  105. package/dist/engines/surrealdb/auth.js.map +1 -0
  106. package/dist/engines/surrealdb/backup.js +72 -9
  107. package/dist/engines/surrealdb/backup.js.map +1 -1
  108. package/dist/engines/surrealdb/index.js +84 -144
  109. package/dist/engines/surrealdb/index.js.map +1 -1
  110. package/dist/engines/surrealdb/restore.js +32 -31
  111. package/dist/engines/surrealdb/restore.js.map +1 -1
  112. package/dist/engines/typedb/backup.js +9 -1
  113. package/dist/engines/typedb/backup.js.map +1 -1
  114. package/dist/engines/typedb/cli-utils.js +3 -3
  115. package/dist/engines/typedb/cli-utils.js.map +1 -1
  116. package/dist/engines/typedb/index.js +9 -2
  117. package/dist/engines/typedb/index.js.map +1 -1
  118. package/dist/engines/typedb/restore.js +19 -7
  119. package/dist/engines/typedb/restore.js.map +1 -1
  120. package/dist/engines/valkey/backup.js +37 -13
  121. package/dist/engines/valkey/backup.js.map +1 -1
  122. package/dist/engines/valkey/index.js +207 -58
  123. package/dist/engines/valkey/index.js.map +1 -1
  124. package/dist/engines/valkey/restore.js +21 -2
  125. package/dist/engines/valkey/restore.js.map +1 -1
  126. package/dist/engines/weaviate/backup.js +7 -2
  127. package/dist/engines/weaviate/backup.js.map +1 -1
  128. package/dist/types/index.js.map +1 -1
  129. package/package.json +1 -1
@@ -21,15 +21,18 @@ import { paths } from '../../config/paths.js';
21
21
  import { getEngineDefaults } from '../../config/defaults.js';
22
22
  import { platformService, isWindows } from '../../core/platform-service.js';
23
23
  import { configManager } from '../../core/config-manager.js';
24
+ import { getDefaultUsername, loadCredentials } from '../../core/credential-manager.js';
24
25
  import { containerManager } from '../../core/container-manager.js';
25
26
  import { logDebug, logWarning, assertValidDatabaseName, assertValidUsername, } from '../../core/error-handler.js';
26
27
  import { processManager } from '../../core/process-manager.js';
27
28
  import { spawnAsync } from '../../core/spawn-utils.js';
28
29
  import { ferretdbBinaryManager } from './binary-manager.js';
30
+ import { buildMongoUri, normalizeMongoHost, } from '../mongo-uri.js';
29
31
  import { SUPPORTED_MAJOR_VERSIONS, FALLBACK_VERSION_MAP, DEFAULT_DOCUMENTDB_VERSION, DEFAULT_V1_POSTGRESQL_VERSION, normalizeVersion, normalizeDocumentDBVersion, isV1, } from './version-maps.js';
30
32
  import { getBinaryUrls, isPlatformSupported } from './binary-urls.js';
31
33
  import { detectBackupFormat as detectBackupFormatImpl, restoreBackup, } from './restore.js';
32
34
  import { createBackup } from './backup.js';
35
+ import { Engine, } from '../../types/index.js';
33
36
  import { parseMongoDBResult } from '../../core/query-parser.js';
34
37
  const execAsync = promisify(exec);
35
38
  const ENGINE = 'ferretdb';
@@ -974,6 +977,71 @@ export class FerretDBEngine extends BaseEngine {
974
977
  }
975
978
  }
976
979
  }
980
+ async getLocalAuth(containerName) {
981
+ const savedCreds = await loadCredentials(containerName, Engine.FerretDB, getDefaultUsername(Engine.FerretDB));
982
+ if (!savedCreds) {
983
+ return null;
984
+ }
985
+ return {
986
+ username: savedCreds.username,
987
+ password: savedCreds.password,
988
+ authDatabase: savedCreds.database || 'admin',
989
+ };
990
+ }
991
+ async buildLocalMongoshArgs(container, database, options) {
992
+ const savedCreds = await this.getLocalAuth(container.name);
993
+ const connectHost = normalizeMongoHost(container.bindAddress);
994
+ const args = savedCreds
995
+ ? [
996
+ buildMongoUri(container.port, database, savedCreds, connectHost),
997
+ ]
998
+ : ['--host', connectHost, '--port', String(container.port), database];
999
+ if (options?.quiet) {
1000
+ args.push('--quiet');
1001
+ }
1002
+ return args;
1003
+ }
1004
+ async runLocalMongosh(container, database, options) {
1005
+ const mongosh = await this.getMongoshPath();
1006
+ const args = await this.buildLocalMongoshArgs(container, database, {
1007
+ quiet: options.quiet,
1008
+ });
1009
+ if (options.eval) {
1010
+ args.push('--eval', options.eval);
1011
+ }
1012
+ if (options.file) {
1013
+ args.push('--file', options.file);
1014
+ }
1015
+ return new Promise((resolve, reject) => {
1016
+ const proc = spawn(mongosh, args, {
1017
+ stdio: ['ignore', 'pipe', 'pipe'],
1018
+ });
1019
+ let stdout = '';
1020
+ let stderr = '';
1021
+ proc.stdout?.on('data', (data) => {
1022
+ stdout += data.toString();
1023
+ });
1024
+ proc.stderr?.on('data', (data) => {
1025
+ stderr += data.toString();
1026
+ });
1027
+ const timeout = setTimeout(() => {
1028
+ proc.kill('SIGTERM');
1029
+ reject(new Error(`${options.file ? 'mongosh file execution' : 'mongosh command'} timed out after ${(options.timeoutMs ?? 10000) / 1000} seconds`));
1030
+ }, options.timeoutMs ?? 10000);
1031
+ proc.on('error', (error) => {
1032
+ clearTimeout(timeout);
1033
+ reject(error);
1034
+ });
1035
+ proc.on('close', (code) => {
1036
+ clearTimeout(timeout);
1037
+ if (code !== 0) {
1038
+ reject(new Error(stderr || `mongosh exited with code ${code}`));
1039
+ return;
1040
+ }
1041
+ resolve({ stdout, stderr });
1042
+ });
1043
+ });
1044
+ }
977
1045
  // Get FerretDB server status
978
1046
  async status(container) {
979
1047
  const { name, port } = container;
@@ -1024,50 +1092,22 @@ export class FerretDBEngine extends BaseEngine {
1024
1092
  }
1025
1093
  // Restore a backup
1026
1094
  async restore(container, backupPath, options = {}) {
1027
- const { backendPort } = container;
1028
- const database = options.database || 'ferretdb';
1029
- if (!backendPort) {
1030
- throw new Error('Backend port not set - start the container first');
1031
- }
1095
+ const database = options.database || container.database || 'test';
1032
1096
  // Validate database name before restore (defense-in-depth)
1033
1097
  assertValidDatabaseName(database);
1034
- const result = await restoreBackup(container, backupPath, {
1098
+ return restoreBackup(container, backupPath, {
1099
+ containerName: container.name,
1100
+ port: container.port,
1035
1101
  database,
1036
1102
  drop: options.drop !== false,
1103
+ sourceDatabase: options.sourceDatabase,
1104
+ containerVersion: container.version,
1037
1105
  });
1038
- // Restart FerretDB proxy so it picks up the restored data.
1039
- // pg_restore writes directly to PostgreSQL, but FerretDB's proxy
1040
- // caches schema/collection metadata in memory and won't see
1041
- // the restored collections until restarted.
1042
- const containerDir = paths.getContainerPath(container.name, {
1043
- engine: ENGINE,
1044
- });
1045
- try {
1046
- await this.stopFerretDBProcess(containerDir);
1047
- // start() detects PG is already running and only launches the proxy
1048
- await this.start(container);
1049
- }
1050
- catch (error) {
1051
- const err = error;
1052
- logWarning(`Failed to restart FerretDB proxy after restore: ${err.message}`);
1053
- // Retry once — transient issues (port race, slow PG) can resolve on second attempt
1054
- try {
1055
- await this.stopFerretDBProcess(containerDir).catch(() => { });
1056
- await this.start(container);
1057
- }
1058
- catch {
1059
- throw new Error(`Restore succeeded but FerretDB proxy failed to restart. ` +
1060
- `Data is safely in PostgreSQL. Run 'spindb start ${container.name}' to restart manually. ` +
1061
- `Original error: ${err.message}`);
1062
- }
1063
- }
1064
- return result;
1065
1106
  }
1066
1107
  // Get connection string (MongoDB-compatible)
1067
1108
  getConnectionString(container, database) {
1068
1109
  const { port } = container;
1069
1110
  const db = database || container.database || 'test';
1070
- // No authentication required - FerretDB runs with --no-auth for local dev
1071
1111
  return `mongodb://127.0.0.1:${port}/${db}`;
1072
1112
  }
1073
1113
  // Get PostgreSQL backend connection string (for debugging)
@@ -1098,14 +1138,14 @@ export class FerretDBEngine extends BaseEngine {
1098
1138
  }
1099
1139
  // Open mongosh interactive shell
1100
1140
  async connect(container, database) {
1101
- const { port } = container;
1102
- const db = database || 'test';
1141
+ const db = database || container.database || 'test';
1103
1142
  const mongosh = await this.getMongoshPath();
1143
+ const args = await this.buildLocalMongoshArgs(container, db);
1104
1144
  const spawnOptions = {
1105
1145
  stdio: 'inherit',
1106
1146
  };
1107
1147
  return new Promise((resolve, reject) => {
1108
- const proc = spawn(mongosh, ['--host', '127.0.0.1', '--port', String(port), db], spawnOptions);
1148
+ const proc = spawn(mongosh, args, spawnOptions);
1109
1149
  proc.on('error', reject);
1110
1150
  proc.on('close', () => resolve());
1111
1151
  });
@@ -1118,19 +1158,16 @@ export class FerretDBEngine extends BaseEngine {
1118
1158
  */
1119
1159
  async createDatabase(container, database) {
1120
1160
  assertValidDatabaseName(database);
1121
- const { port } = container;
1122
1161
  try {
1123
- const mongosh = await this.getMongoshPath();
1124
1162
  // Create a temp collection then immediately drop it to force database creation
1125
1163
  // without leaving any visible marker collections.
1126
1164
  // Pre-drop in case a previous run was interrupted and left a stale collection.
1127
1165
  // NOTE: Use db.getCollection() instead of db._spindb_init shorthand because
1128
1166
  // mongosh doesn't support shorthand notation for collection names starting with underscore.
1129
- const script = 'try { db.getCollection("_spindb_init").drop(); } catch(e) {} db.createCollection("_spindb_init"); db.getCollection("_spindb_init").drop();';
1130
- const cmd = isWindows()
1131
- ? `"${mongosh}" --host 127.0.0.1 --port ${port} ${database} --eval "${script.replace(/"/g, '\\"')}"`
1132
- : `"${mongosh}" --host 127.0.0.1 --port ${port} ${database} --eval '${script}'`;
1133
- await execAsync(cmd, { timeout: 10000 });
1167
+ await this.runLocalMongosh(container, database, {
1168
+ eval: 'try { db.getCollection("_spindb_init").drop(); } catch(e) {} db.createCollection("_spindb_init"); db.getCollection("_spindb_init").drop();',
1169
+ timeoutMs: 10000,
1170
+ });
1134
1171
  logDebug(`Database "${database}" created via temp collection`);
1135
1172
  }
1136
1173
  catch (error) {
@@ -1141,13 +1178,11 @@ export class FerretDBEngine extends BaseEngine {
1141
1178
  // Drop a database
1142
1179
  async dropDatabase(container, database) {
1143
1180
  assertValidDatabaseName(database);
1144
- const { port } = container;
1145
1181
  try {
1146
- const mongosh = await this.getMongoshPath();
1147
- const cmd = isWindows()
1148
- ? `"${mongosh}" --host 127.0.0.1 --port ${port} ${database} --eval "db.dropDatabase()"`
1149
- : `"${mongosh}" --host 127.0.0.1 --port ${port} ${database} --eval 'db.dropDatabase()'`;
1150
- await execAsync(cmd, { timeout: 10000 });
1182
+ await this.runLocalMongosh(container, database, {
1183
+ eval: 'db.dropDatabase()',
1184
+ timeoutMs: 10000,
1185
+ });
1151
1186
  }
1152
1187
  catch (error) {
1153
1188
  logDebug(`dropDatabase result: ${error}`);
@@ -1155,16 +1190,15 @@ export class FerretDBEngine extends BaseEngine {
1155
1190
  }
1156
1191
  // Get the size of the database in bytes
1157
1192
  async getDatabaseSize(container) {
1158
- const { port, database } = container;
1193
+ const { database } = container;
1159
1194
  const db = database || 'test';
1160
1195
  assertValidDatabaseName(db);
1161
1196
  try {
1162
- const mongosh = await this.getMongoshPath();
1163
- const script = 'JSON.stringify(db.stats())';
1164
- const cmd = isWindows()
1165
- ? `"${mongosh}" --host 127.0.0.1 --port ${port} ${db} --quiet --eval "${script}"`
1166
- : `"${mongosh}" --host 127.0.0.1 --port ${port} ${db} --quiet --eval '${script}'`;
1167
- const { stdout } = await execAsync(cmd, { timeout: 10000 });
1197
+ const { stdout } = await this.runLocalMongosh(container, db, {
1198
+ eval: 'JSON.stringify(db.stats())',
1199
+ quiet: true,
1200
+ timeoutMs: 10000,
1201
+ });
1168
1202
  // Extract JSON from output
1169
1203
  const firstBrace = stdout.indexOf('{');
1170
1204
  const lastBrace = stdout.lastIndexOf('}');
@@ -1228,70 +1262,22 @@ export class FerretDBEngine extends BaseEngine {
1228
1262
  }
1229
1263
  // Run a JavaScript file or inline script against the database
1230
1264
  async runScript(container, options) {
1231
- const { port } = container;
1232
1265
  const db = options.database || container.database || 'test';
1233
- const mongosh = await this.getMongoshPath();
1234
1266
  if (options.file) {
1235
- const spawnOptions = {
1236
- stdio: 'inherit',
1237
- };
1238
- return new Promise((resolve, reject) => {
1239
- const proc = spawn(mongosh, [
1240
- '--host',
1241
- '127.0.0.1',
1242
- '--port',
1243
- String(port),
1244
- db,
1245
- '--file',
1246
- options.file,
1247
- ], spawnOptions);
1248
- proc.on('error', reject);
1249
- proc.on('close', (code) => {
1250
- if (code === 0) {
1251
- resolve();
1252
- }
1253
- else {
1254
- reject(new Error(`mongosh exited with code ${code}`));
1255
- }
1256
- });
1267
+ await this.runLocalMongosh(container, db, {
1268
+ file: options.file,
1269
+ timeoutMs: 60000,
1257
1270
  });
1258
1271
  }
1259
1272
  else if (options.sql) {
1260
- // sql field is actually JS for MongoDB-compatible databases
1261
- const script = options.sql;
1262
- return new Promise((resolve, reject) => {
1263
- const proc = spawn(mongosh, ['--host', '127.0.0.1', '--port', String(port), db, '--eval', script], { stdio: ['pipe', 'pipe', 'pipe'] });
1264
- let stdout = '';
1265
- let stderr = '';
1266
- proc.stdout?.on('data', (data) => {
1267
- stdout += data.toString();
1268
- });
1269
- proc.stderr?.on('data', (data) => {
1270
- stderr += data.toString();
1271
- });
1272
- // 60 second timeout
1273
- const timeout = setTimeout(() => {
1274
- proc.kill('SIGTERM');
1275
- reject(new Error('mongosh timed out after 60 seconds'));
1276
- }, 60000);
1277
- proc.on('error', (err) => {
1278
- clearTimeout(timeout);
1279
- reject(err);
1280
- });
1281
- proc.on('close', (code) => {
1282
- clearTimeout(timeout);
1283
- if (stdout)
1284
- process.stdout.write(stdout);
1285
- if (stderr)
1286
- process.stderr.write(stderr);
1287
- if (code === 0) {
1288
- resolve();
1289
- }
1290
- else {
1291
- reject(new Error(`mongosh exited with code ${code}`));
1292
- }
1293
- });
1273
+ const { stdout, stderr } = await this.runLocalMongosh(container, db, {
1274
+ eval: options.sql,
1275
+ timeoutMs: 60000,
1294
1276
  });
1277
+ if (stdout)
1278
+ process.stdout.write(stdout);
1279
+ if (stderr)
1280
+ process.stderr.write(stderr);
1295
1281
  }
1296
1282
  else {
1297
1283
  throw new Error('Either file or sql option must be provided');
@@ -1324,42 +1310,33 @@ export class FerretDBEngine extends BaseEngine {
1324
1310
  'Shell commands like "show dbs" and "use dbname" are not supported in executeQuery.');
1325
1311
  }
1326
1312
  }
1327
- // Wrap query in async IIFE to properly await cursor.toArray()
1328
- // This prevents JSON.stringify from serializing a Promise
1329
1313
  const script = `(async () => { const res = ${normalizedQuery}; return JSON.stringify(res.toArray ? await res.toArray() : await Promise.resolve(res)); })()`;
1330
- return new Promise((resolve, reject) => {
1331
- let args;
1332
- if (options?.host) {
1333
- // Remote: build a connection URI for mongosh
1334
- const user = options.username
1335
- ? encodeURIComponent(options.username)
1336
- : '';
1337
- const pass = options.password
1338
- ? encodeURIComponent(options.password)
1339
- : '';
1340
- const auth = user ? `${user}:${pass}@` : '';
1341
- const host = options.host;
1342
- const isSrv = options.scheme === 'mongodb+srv';
1343
- const scheme = isSrv ? 'mongodb+srv' : 'mongodb';
1344
- const portSuffix = isSrv ? '' : `:${port}`;
1345
- const sslParam = options.ssl && !isSrv ? 'tls=true' : '';
1346
- const uri = `${scheme}://${auth}${host}${portSuffix}/${db}${sslParam ? `?${sslParam}` : ''}`;
1347
- args = [uri, '--quiet', '--eval', script];
1348
- }
1349
- else if (options?.password) {
1350
- // Local with auth: build URI with credentials
1351
- const user = options.username
1352
- ? encodeURIComponent(options.username)
1353
- : '';
1354
- const pass = encodeURIComponent(options.password);
1355
- const auth = user ? `${user}:${pass}@` : '';
1356
- const uri = `mongodb://${auth}127.0.0.1:${port}/${db}`;
1357
- args = [uri, '--quiet', '--eval', script];
1358
- }
1359
- else {
1360
- args = [
1314
+ let args;
1315
+ if (options?.host) {
1316
+ const user = options.username ? encodeURIComponent(options.username) : '';
1317
+ const pass = options.password ? encodeURIComponent(options.password) : '';
1318
+ const auth = user ? `${user}:${pass}@` : '';
1319
+ const host = options.host;
1320
+ const isSrv = options.scheme === 'mongodb+srv';
1321
+ const scheme = isSrv ? 'mongodb+srv' : 'mongodb';
1322
+ const portSuffix = isSrv ? '' : `:${port}`;
1323
+ const sslParam = options.ssl && !isSrv ? 'tls=true' : '';
1324
+ const uri = `${scheme}://${auth}${host}${portSuffix}/${db}${sslParam ? `?${sslParam}` : ''}`;
1325
+ args = [uri, '--quiet', '--eval', script];
1326
+ }
1327
+ else {
1328
+ const savedCreds = await this.getLocalAuth(container.name);
1329
+ const connectHost = normalizeMongoHost(container.bindAddress);
1330
+ args = savedCreds
1331
+ ? [
1332
+ buildMongoUri(port, db, savedCreds, connectHost),
1333
+ '--quiet',
1334
+ '--eval',
1335
+ script,
1336
+ ]
1337
+ : [
1361
1338
  '--host',
1362
- '127.0.0.1',
1339
+ connectHost,
1363
1340
  '--port',
1364
1341
  String(port),
1365
1342
  db,
@@ -1367,17 +1344,18 @@ export class FerretDBEngine extends BaseEngine {
1367
1344
  '--eval',
1368
1345
  script,
1369
1346
  ];
1370
- }
1347
+ }
1348
+ const { stdout, stderr } = await new Promise((resolve, reject) => {
1371
1349
  const proc = spawn(mongosh, args, {
1372
- stdio: ['pipe', 'pipe', 'pipe'],
1350
+ stdio: ['ignore', 'pipe', 'pipe'],
1373
1351
  });
1374
- let stdout = '';
1375
- let stderr = '';
1352
+ let stdoutBuf = '';
1353
+ let stderrBuf = '';
1376
1354
  proc.stdout?.on('data', (data) => {
1377
- stdout += data.toString();
1355
+ stdoutBuf += data.toString();
1378
1356
  });
1379
1357
  proc.stderr?.on('data', (data) => {
1380
- stderr += data.toString();
1358
+ stderrBuf += data.toString();
1381
1359
  });
1382
1360
  const timeout = setTimeout(() => {
1383
1361
  proc.kill('SIGTERM');
@@ -1390,87 +1368,67 @@ export class FerretDBEngine extends BaseEngine {
1390
1368
  proc.on('close', (code) => {
1391
1369
  clearTimeout(timeout);
1392
1370
  if (code !== 0) {
1393
- reject(new Error(stderr || `mongosh exited with code ${code}`));
1371
+ reject(new Error(`${stderrBuf || `mongosh exited with code ${code}`}${stdoutBuf ? `\nOutput: ${stdoutBuf}` : ''}`));
1394
1372
  return;
1395
1373
  }
1396
- try {
1397
- // Extract JSON from output (mongosh may output extra info)
1398
- const jsonStart = stdout.indexOf('[');
1399
- const jsonEnd = stdout.lastIndexOf(']');
1400
- if (jsonStart !== -1 && jsonEnd !== -1 && jsonEnd > jsonStart) {
1401
- const jsonStr = stdout.substring(jsonStart, jsonEnd + 1);
1402
- resolve(parseMongoDBResult(jsonStr));
1403
- }
1404
- else {
1405
- // Try parsing as single object or scalar
1406
- const objStart = stdout.indexOf('{');
1407
- const objEnd = stdout.lastIndexOf('}');
1408
- if (objStart !== -1 && objEnd !== -1 && objEnd > objStart) {
1409
- const jsonStr = stdout.substring(objStart, objEnd + 1);
1410
- resolve(parseMongoDBResult(jsonStr));
1411
- }
1412
- else {
1413
- // Return as scalar result
1414
- resolve({
1415
- columns: ['result'],
1416
- rows: [{ result: stdout.trim() }],
1417
- rowCount: 1,
1418
- });
1419
- }
1420
- }
1421
- }
1422
- catch (error) {
1423
- reject(new Error(`Failed to parse query result: ${error instanceof Error ? error.message : error}`));
1424
- }
1374
+ resolve({ stdout: stdoutBuf, stderr: stderrBuf });
1425
1375
  });
1426
1376
  });
1377
+ if (stderr && !stdout.trim()) {
1378
+ throw new Error(`${stderr}${stdout ? `\nOutput: ${stdout}` : ''}`);
1379
+ }
1380
+ const jsonMatch = stdout.match(/\[[\s\S]*\]|\{[\s\S]*\}/);
1381
+ if (!jsonMatch) {
1382
+ return {
1383
+ columns: ['result'],
1384
+ rows: [{ result: stdout.trim() }],
1385
+ rowCount: 1,
1386
+ };
1387
+ }
1388
+ return parseMongoDBResult(jsonMatch[0]);
1427
1389
  }
1428
1390
  /**
1429
1391
  * List all user databases, excluding system databases (admin, config, local).
1430
1392
  * FerretDB uses MongoDB protocol, so same approach as MongoDB.
1431
1393
  */
1432
1394
  async listDatabases(container) {
1433
- const { port } = container;
1434
1395
  const mongosh = await this.getMongoshPath();
1435
1396
  return new Promise((resolve, reject) => {
1436
- // Use JSON output for reliable parsing
1437
1397
  const script = `JSON.stringify(db.adminCommand({listDatabases: 1}).databases.map(d => d.name))`;
1438
- const args = [
1439
- '--quiet',
1440
- '--host',
1441
- '127.0.0.1',
1442
- '--port',
1443
- String(port),
1444
- '--eval',
1445
- script,
1446
- ];
1447
- const proc = spawn(mongosh, args, {
1448
- stdio: ['ignore', 'pipe', 'pipe'],
1449
- });
1450
- let stdout = '';
1451
- let stderr = '';
1452
- proc.stdout?.on('data', (data) => {
1453
- stdout += data.toString();
1454
- });
1455
- proc.stderr?.on('data', (data) => {
1456
- stderr += data.toString();
1457
- });
1458
- proc.on('error', reject);
1459
- proc.on('close', (code) => {
1460
- if (code !== 0) {
1461
- reject(new Error(stderr || `mongosh exited with code ${code}`));
1462
- return;
1463
- }
1464
- try {
1465
- const allDatabases = JSON.parse(stdout.trim());
1466
- const systemDatabases = ['admin', 'config', 'local'];
1467
- const databases = allDatabases.filter((db) => !systemDatabases.includes(db));
1468
- resolve(databases);
1469
- }
1470
- catch (error) {
1471
- reject(new Error(`Failed to parse database list: ${error}`));
1472
- }
1473
- });
1398
+ const launch = async () => {
1399
+ const args = await this.buildLocalMongoshArgs(container, 'admin', {
1400
+ quiet: true,
1401
+ });
1402
+ args.push('--eval', script);
1403
+ const proc = spawn(mongosh, args, {
1404
+ stdio: ['ignore', 'pipe', 'pipe'],
1405
+ });
1406
+ let stdout = '';
1407
+ let stderr = '';
1408
+ proc.stdout?.on('data', (data) => {
1409
+ stdout += data.toString();
1410
+ });
1411
+ proc.stderr?.on('data', (data) => {
1412
+ stderr += data.toString();
1413
+ });
1414
+ proc.on('error', reject);
1415
+ proc.on('close', (code) => {
1416
+ if (code !== 0) {
1417
+ reject(new Error(stderr || `mongosh exited with code ${code}`));
1418
+ return;
1419
+ }
1420
+ try {
1421
+ const allDatabases = JSON.parse(stdout.trim());
1422
+ const systemDatabases = ['admin', 'config', 'local'];
1423
+ const databases = allDatabases.filter((db) => !systemDatabases.includes(db));
1424
+ resolve(databases);
1425
+ }
1426
+ catch (error) {
1427
+ reject(new Error(`Failed to parse database list: ${error}`));
1428
+ }
1429
+ });
1430
+ };
1431
+ void launch().catch(reject);
1474
1432
  });
1475
1433
  }
1476
1434
  async createUser(container, options) {
@@ -1480,12 +1438,9 @@ export class FerretDBEngine extends BaseEngine {
1480
1438
  const db = database ?? container.database ?? 'admin';
1481
1439
  assertValidDatabaseName(db);
1482
1440
  const mongosh = await this.getMongoshPath();
1483
- // Same as MongoDB - auth disabled with --no-auth but user is still created
1484
- // Use JSON.stringify for password to safely escape all special characters in JS context
1485
- // Pass script via stdin to avoid exposing passwords in process listings
1486
1441
  const jsonPwd = JSON.stringify(password);
1487
1442
  const script = `db.getSiblingDB('${db}').createUser({user:'${username}',pwd:${jsonPwd},roles:[{role:'readWrite',db:'${db}'}]})`;
1488
- const mongoshArgs = ['--host', '127.0.0.1', '--port', String(port), 'admin'];
1443
+ const mongoshArgs = await this.buildLocalMongoshArgs(container, 'admin');
1489
1444
  const runMongoshViaStdin = (js) => new Promise((resolve, reject) => {
1490
1445
  const proc = spawn(mongosh, mongoshArgs, {
1491
1446
  stdio: ['pipe', 'pipe', 'pipe'],