spindb 0.46.4 → 0.47.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.
Files changed (127) 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 +179 -226
  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 +21 -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 +8 -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 +10 -4
  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 +48 -15
  97. package/dist/engines/redis/backup.js.map +1 -1
  98. package/dist/engines/redis/index.js +45 -12
  99. package/dist/engines/redis/index.js.map +1 -1
  100. package/dist/engines/redis/restore.js +21 -2
  101. package/dist/engines/redis/restore.js.map +1 -1
  102. package/dist/engines/surrealdb/auth.js +98 -0
  103. package/dist/engines/surrealdb/auth.js.map +1 -0
  104. package/dist/engines/surrealdb/backup.js +72 -9
  105. package/dist/engines/surrealdb/backup.js.map +1 -1
  106. package/dist/engines/surrealdb/index.js +84 -144
  107. package/dist/engines/surrealdb/index.js.map +1 -1
  108. package/dist/engines/surrealdb/restore.js +32 -31
  109. package/dist/engines/surrealdb/restore.js.map +1 -1
  110. package/dist/engines/typedb/backup.js +9 -1
  111. package/dist/engines/typedb/backup.js.map +1 -1
  112. package/dist/engines/typedb/cli-utils.js +3 -3
  113. package/dist/engines/typedb/cli-utils.js.map +1 -1
  114. package/dist/engines/typedb/index.js +9 -2
  115. package/dist/engines/typedb/index.js.map +1 -1
  116. package/dist/engines/typedb/restore.js +19 -7
  117. package/dist/engines/typedb/restore.js.map +1 -1
  118. package/dist/engines/valkey/backup.js +37 -13
  119. package/dist/engines/valkey/backup.js.map +1 -1
  120. package/dist/engines/valkey/index.js +207 -58
  121. package/dist/engines/valkey/index.js.map +1 -1
  122. package/dist/engines/valkey/restore.js +21 -2
  123. package/dist/engines/valkey/restore.js.map +1 -1
  124. package/dist/engines/weaviate/backup.js +7 -2
  125. package/dist/engines/weaviate/backup.js.map +1 -1
  126. package/dist/types/index.js.map +1 -1
  127. 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 } 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,70 @@ 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 args = savedCreds
994
+ ? [
995
+ buildMongoUri(container.port, database, savedCreds, container.bindAddress ?? '127.0.0.1'),
996
+ ]
997
+ : ['--host', '127.0.0.1', '--port', String(container.port), database];
998
+ if (options?.quiet) {
999
+ args.push('--quiet');
1000
+ }
1001
+ return args;
1002
+ }
1003
+ async runLocalMongosh(container, database, options) {
1004
+ const mongosh = await this.getMongoshPath();
1005
+ const args = await this.buildLocalMongoshArgs(container, database, {
1006
+ quiet: options.quiet,
1007
+ });
1008
+ if (options.eval) {
1009
+ args.push('--eval', options.eval);
1010
+ }
1011
+ if (options.file) {
1012
+ args.push('--file', options.file);
1013
+ }
1014
+ return new Promise((resolve, reject) => {
1015
+ const proc = spawn(mongosh, args, {
1016
+ stdio: ['ignore', 'pipe', 'pipe'],
1017
+ });
1018
+ let stdout = '';
1019
+ let stderr = '';
1020
+ proc.stdout?.on('data', (data) => {
1021
+ stdout += data.toString();
1022
+ });
1023
+ proc.stderr?.on('data', (data) => {
1024
+ stderr += data.toString();
1025
+ });
1026
+ const timeout = setTimeout(() => {
1027
+ proc.kill('SIGTERM');
1028
+ reject(new Error(`${options.file ? 'mongosh file execution' : 'mongosh command'} timed out after ${(options.timeoutMs ?? 10000) / 1000} seconds`));
1029
+ }, options.timeoutMs ?? 10000);
1030
+ proc.on('error', (error) => {
1031
+ clearTimeout(timeout);
1032
+ reject(error);
1033
+ });
1034
+ proc.on('close', (code) => {
1035
+ clearTimeout(timeout);
1036
+ if (code !== 0) {
1037
+ reject(new Error(stderr || `mongosh exited with code ${code}`));
1038
+ return;
1039
+ }
1040
+ resolve({ stdout, stderr });
1041
+ });
1042
+ });
1043
+ }
977
1044
  // Get FerretDB server status
978
1045
  async status(container) {
979
1046
  const { name, port } = container;
@@ -1024,50 +1091,22 @@ export class FerretDBEngine extends BaseEngine {
1024
1091
  }
1025
1092
  // Restore a backup
1026
1093
  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
- }
1094
+ const database = options.database || container.database || 'test';
1032
1095
  // Validate database name before restore (defense-in-depth)
1033
1096
  assertValidDatabaseName(database);
1034
- const result = await restoreBackup(container, backupPath, {
1097
+ return restoreBackup(container, backupPath, {
1098
+ containerName: container.name,
1099
+ port: container.port,
1035
1100
  database,
1036
1101
  drop: options.drop !== false,
1102
+ sourceDatabase: options.sourceDatabase,
1103
+ containerVersion: container.version,
1037
1104
  });
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
1105
  }
1066
1106
  // Get connection string (MongoDB-compatible)
1067
1107
  getConnectionString(container, database) {
1068
1108
  const { port } = container;
1069
1109
  const db = database || container.database || 'test';
1070
- // No authentication required - FerretDB runs with --no-auth for local dev
1071
1110
  return `mongodb://127.0.0.1:${port}/${db}`;
1072
1111
  }
1073
1112
  // Get PostgreSQL backend connection string (for debugging)
@@ -1098,14 +1137,14 @@ export class FerretDBEngine extends BaseEngine {
1098
1137
  }
1099
1138
  // Open mongosh interactive shell
1100
1139
  async connect(container, database) {
1101
- const { port } = container;
1102
- const db = database || 'test';
1140
+ const db = database || container.database || 'test';
1103
1141
  const mongosh = await this.getMongoshPath();
1142
+ const args = await this.buildLocalMongoshArgs(container, db);
1104
1143
  const spawnOptions = {
1105
1144
  stdio: 'inherit',
1106
1145
  };
1107
1146
  return new Promise((resolve, reject) => {
1108
- const proc = spawn(mongosh, ['--host', '127.0.0.1', '--port', String(port), db], spawnOptions);
1147
+ const proc = spawn(mongosh, args, spawnOptions);
1109
1148
  proc.on('error', reject);
1110
1149
  proc.on('close', () => resolve());
1111
1150
  });
@@ -1118,19 +1157,16 @@ export class FerretDBEngine extends BaseEngine {
1118
1157
  */
1119
1158
  async createDatabase(container, database) {
1120
1159
  assertValidDatabaseName(database);
1121
- const { port } = container;
1122
1160
  try {
1123
- const mongosh = await this.getMongoshPath();
1124
1161
  // Create a temp collection then immediately drop it to force database creation
1125
1162
  // without leaving any visible marker collections.
1126
1163
  // Pre-drop in case a previous run was interrupted and left a stale collection.
1127
1164
  // NOTE: Use db.getCollection() instead of db._spindb_init shorthand because
1128
1165
  // 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 });
1166
+ await this.runLocalMongosh(container, database, {
1167
+ eval: 'try { db.getCollection("_spindb_init").drop(); } catch(e) {} db.createCollection("_spindb_init"); db.getCollection("_spindb_init").drop();',
1168
+ timeoutMs: 10000,
1169
+ });
1134
1170
  logDebug(`Database "${database}" created via temp collection`);
1135
1171
  }
1136
1172
  catch (error) {
@@ -1141,13 +1177,11 @@ export class FerretDBEngine extends BaseEngine {
1141
1177
  // Drop a database
1142
1178
  async dropDatabase(container, database) {
1143
1179
  assertValidDatabaseName(database);
1144
- const { port } = container;
1145
1180
  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 });
1181
+ await this.runLocalMongosh(container, database, {
1182
+ eval: 'db.dropDatabase()',
1183
+ timeoutMs: 10000,
1184
+ });
1151
1185
  }
1152
1186
  catch (error) {
1153
1187
  logDebug(`dropDatabase result: ${error}`);
@@ -1155,16 +1189,15 @@ export class FerretDBEngine extends BaseEngine {
1155
1189
  }
1156
1190
  // Get the size of the database in bytes
1157
1191
  async getDatabaseSize(container) {
1158
- const { port, database } = container;
1192
+ const { database } = container;
1159
1193
  const db = database || 'test';
1160
1194
  assertValidDatabaseName(db);
1161
1195
  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 });
1196
+ const { stdout } = await this.runLocalMongosh(container, db, {
1197
+ eval: 'JSON.stringify(db.stats())',
1198
+ quiet: true,
1199
+ timeoutMs: 10000,
1200
+ });
1168
1201
  // Extract JSON from output
1169
1202
  const firstBrace = stdout.indexOf('{');
1170
1203
  const lastBrace = stdout.lastIndexOf('}');
@@ -1228,70 +1261,22 @@ export class FerretDBEngine extends BaseEngine {
1228
1261
  }
1229
1262
  // Run a JavaScript file or inline script against the database
1230
1263
  async runScript(container, options) {
1231
- const { port } = container;
1232
1264
  const db = options.database || container.database || 'test';
1233
- const mongosh = await this.getMongoshPath();
1234
1265
  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
- });
1266
+ await this.runLocalMongosh(container, db, {
1267
+ file: options.file,
1268
+ timeoutMs: 60000,
1257
1269
  });
1258
1270
  }
1259
1271
  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
- });
1272
+ const { stdout, stderr } = await this.runLocalMongosh(container, db, {
1273
+ eval: options.sql,
1274
+ timeoutMs: 60000,
1294
1275
  });
1276
+ if (stdout)
1277
+ process.stdout.write(stdout);
1278
+ if (stderr)
1279
+ process.stderr.write(stderr);
1295
1280
  }
1296
1281
  else {
1297
1282
  throw new Error('Either file or sql option must be provided');
@@ -1324,40 +1309,30 @@ export class FerretDBEngine extends BaseEngine {
1324
1309
  'Shell commands like "show dbs" and "use dbname" are not supported in executeQuery.');
1325
1310
  }
1326
1311
  }
1327
- // Wrap query in async IIFE to properly await cursor.toArray()
1328
- // This prevents JSON.stringify from serializing a Promise
1329
1312
  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 = [
1313
+ let args;
1314
+ if (options?.host) {
1315
+ const user = options.username ? encodeURIComponent(options.username) : '';
1316
+ const pass = options.password ? encodeURIComponent(options.password) : '';
1317
+ const auth = user ? `${user}:${pass}@` : '';
1318
+ const host = options.host;
1319
+ const isSrv = options.scheme === 'mongodb+srv';
1320
+ const scheme = isSrv ? 'mongodb+srv' : 'mongodb';
1321
+ const portSuffix = isSrv ? '' : `:${port}`;
1322
+ const sslParam = options.ssl && !isSrv ? 'tls=true' : '';
1323
+ const uri = `${scheme}://${auth}${host}${portSuffix}/${db}${sslParam ? `?${sslParam}` : ''}`;
1324
+ args = [uri, '--quiet', '--eval', script];
1325
+ }
1326
+ else {
1327
+ const savedCreds = await this.getLocalAuth(container.name);
1328
+ args = savedCreds
1329
+ ? [
1330
+ buildMongoUri(port, db, savedCreds, container.bindAddress ?? '127.0.0.1'),
1331
+ '--quiet',
1332
+ '--eval',
1333
+ script,
1334
+ ]
1335
+ : [
1361
1336
  '--host',
1362
1337
  '127.0.0.1',
1363
1338
  '--port',
@@ -1367,17 +1342,18 @@ export class FerretDBEngine extends BaseEngine {
1367
1342
  '--eval',
1368
1343
  script,
1369
1344
  ];
1370
- }
1345
+ }
1346
+ const { stdout, stderr } = await new Promise((resolve, reject) => {
1371
1347
  const proc = spawn(mongosh, args, {
1372
- stdio: ['pipe', 'pipe', 'pipe'],
1348
+ stdio: ['ignore', 'pipe', 'pipe'],
1373
1349
  });
1374
- let stdout = '';
1375
- let stderr = '';
1350
+ let stdoutBuf = '';
1351
+ let stderrBuf = '';
1376
1352
  proc.stdout?.on('data', (data) => {
1377
- stdout += data.toString();
1353
+ stdoutBuf += data.toString();
1378
1354
  });
1379
1355
  proc.stderr?.on('data', (data) => {
1380
- stderr += data.toString();
1356
+ stderrBuf += data.toString();
1381
1357
  });
1382
1358
  const timeout = setTimeout(() => {
1383
1359
  proc.kill('SIGTERM');
@@ -1390,87 +1366,67 @@ export class FerretDBEngine extends BaseEngine {
1390
1366
  proc.on('close', (code) => {
1391
1367
  clearTimeout(timeout);
1392
1368
  if (code !== 0) {
1393
- reject(new Error(stderr || `mongosh exited with code ${code}`));
1369
+ reject(new Error(`${stderrBuf || `mongosh exited with code ${code}`}${stdoutBuf ? `\nOutput: ${stdoutBuf}` : ''}`));
1394
1370
  return;
1395
1371
  }
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
- }
1372
+ resolve({ stdout: stdoutBuf, stderr: stderrBuf });
1425
1373
  });
1426
1374
  });
1375
+ if (stderr && !stdout.trim()) {
1376
+ throw new Error(`${stderr}${stdout ? `\nOutput: ${stdout}` : ''}`);
1377
+ }
1378
+ const jsonMatch = stdout.match(/\[[\s\S]*\]|\{[\s\S]*\}/);
1379
+ if (!jsonMatch) {
1380
+ return {
1381
+ columns: ['result'],
1382
+ rows: [{ result: stdout.trim() }],
1383
+ rowCount: 1,
1384
+ };
1385
+ }
1386
+ return parseMongoDBResult(jsonMatch[0]);
1427
1387
  }
1428
1388
  /**
1429
1389
  * List all user databases, excluding system databases (admin, config, local).
1430
1390
  * FerretDB uses MongoDB protocol, so same approach as MongoDB.
1431
1391
  */
1432
1392
  async listDatabases(container) {
1433
- const { port } = container;
1434
1393
  const mongosh = await this.getMongoshPath();
1435
1394
  return new Promise((resolve, reject) => {
1436
- // Use JSON output for reliable parsing
1437
1395
  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
- });
1396
+ const launch = async () => {
1397
+ const args = await this.buildLocalMongoshArgs(container, 'admin', {
1398
+ quiet: true,
1399
+ });
1400
+ args.push('--eval', script);
1401
+ const proc = spawn(mongosh, args, {
1402
+ stdio: ['ignore', 'pipe', 'pipe'],
1403
+ });
1404
+ let stdout = '';
1405
+ let stderr = '';
1406
+ proc.stdout?.on('data', (data) => {
1407
+ stdout += data.toString();
1408
+ });
1409
+ proc.stderr?.on('data', (data) => {
1410
+ stderr += data.toString();
1411
+ });
1412
+ proc.on('error', reject);
1413
+ proc.on('close', (code) => {
1414
+ if (code !== 0) {
1415
+ reject(new Error(stderr || `mongosh exited with code ${code}`));
1416
+ return;
1417
+ }
1418
+ try {
1419
+ const allDatabases = JSON.parse(stdout.trim());
1420
+ const systemDatabases = ['admin', 'config', 'local'];
1421
+ const databases = allDatabases.filter((db) => !systemDatabases.includes(db));
1422
+ resolve(databases);
1423
+ }
1424
+ catch (error) {
1425
+ reject(new Error(`Failed to parse database list: ${error}`));
1426
+ }
1427
+ });
1428
+ };
1429
+ void launch().catch(reject);
1474
1430
  });
1475
1431
  }
1476
1432
  async createUser(container, options) {
@@ -1480,12 +1436,9 @@ export class FerretDBEngine extends BaseEngine {
1480
1436
  const db = database ?? container.database ?? 'admin';
1481
1437
  assertValidDatabaseName(db);
1482
1438
  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
1439
  const jsonPwd = JSON.stringify(password);
1487
1440
  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'];
1441
+ const mongoshArgs = await this.buildLocalMongoshArgs(container, 'admin');
1489
1442
  const runMongoshViaStdin = (js) => new Promise((resolve, reject) => {
1490
1443
  const proc = spawn(mongosh, mongoshArgs, {
1491
1444
  stdio: ['pipe', 'pipe', 'pipe'],