spindb 0.46.5 → 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.
- package/dist/cli/commands/info.js +12 -2
- package/dist/cli/commands/info.js.map +1 -1
- package/dist/cli/commands/link.js +6 -0
- package/dist/cli/commands/link.js.map +1 -1
- package/dist/cli/commands/list.js +4 -1
- package/dist/cli/commands/list.js.map +1 -1
- package/dist/cli/commands/menu/container-handlers.js +25 -7
- package/dist/cli/commands/menu/container-handlers.js.map +1 -1
- package/dist/config/backup-formats.js +11 -11
- package/dist/config/backup-formats.js.map +1 -1
- package/dist/config/version.js +1 -1
- package/dist/core/credential-manager.js +34 -11
- package/dist/core/credential-manager.js.map +1 -1
- package/dist/core/query-parser.js +55 -1
- package/dist/core/query-parser.js.map +1 -1
- package/dist/core/remote-container.js +11 -0
- package/dist/core/remote-container.js.map +1 -1
- package/dist/engines/clickhouse/backup.js +42 -10
- package/dist/engines/clickhouse/backup.js.map +1 -1
- package/dist/engines/clickhouse/restore.js +41 -10
- package/dist/engines/clickhouse/restore.js.map +1 -1
- package/dist/engines/cockroachdb/backup.js +18 -22
- package/dist/engines/cockroachdb/backup.js.map +1 -1
- package/dist/engines/cockroachdb/cli-utils.js +66 -0
- package/dist/engines/cockroachdb/cli-utils.js.map +1 -1
- package/dist/engines/cockroachdb/index.js +199 -116
- package/dist/engines/cockroachdb/index.js.map +1 -1
- package/dist/engines/cockroachdb/restore.js +19 -26
- package/dist/engines/cockroachdb/restore.js.map +1 -1
- package/dist/engines/couchdb/backup.js +13 -4
- package/dist/engines/couchdb/backup.js.map +1 -1
- package/dist/engines/couchdb/index.js +93 -25
- package/dist/engines/couchdb/index.js.map +1 -1
- package/dist/engines/couchdb/restore.js +15 -4
- package/dist/engines/couchdb/restore.js.map +1 -1
- package/dist/engines/ferretdb/backup.js +88 -91
- package/dist/engines/ferretdb/backup.js.map +1 -1
- package/dist/engines/ferretdb/index.js +179 -226
- package/dist/engines/ferretdb/index.js.map +1 -1
- package/dist/engines/ferretdb/restore.js +223 -20
- package/dist/engines/ferretdb/restore.js.map +1 -1
- package/dist/engines/influxdb/api-client.js +1 -1
- package/dist/engines/influxdb/api-client.js.map +1 -1
- package/dist/engines/influxdb/backup.js +25 -5
- package/dist/engines/influxdb/backup.js.map +1 -1
- package/dist/engines/influxdb/index.js +165 -43
- package/dist/engines/influxdb/index.js.map +1 -1
- package/dist/engines/influxdb/restore.js +22 -2
- package/dist/engines/influxdb/restore.js.map +1 -1
- package/dist/engines/mariadb/backup.js +24 -15
- package/dist/engines/mariadb/backup.js.map +1 -1
- package/dist/engines/mariadb/env.js +11 -0
- package/dist/engines/mariadb/env.js.map +1 -0
- package/dist/engines/mariadb/index.js +192 -113
- package/dist/engines/mariadb/index.js.map +1 -1
- package/dist/engines/mariadb/restore.js +21 -5
- package/dist/engines/mariadb/restore.js.map +1 -1
- package/dist/engines/meilisearch/backup.js +8 -4
- package/dist/engines/meilisearch/backup.js.map +1 -1
- package/dist/engines/meilisearch/index.js +55 -58
- package/dist/engines/meilisearch/index.js.map +1 -1
- package/dist/engines/mongo-uri.js +8 -0
- package/dist/engines/mongo-uri.js.map +1 -0
- package/dist/engines/mongodb/backup.js +62 -13
- package/dist/engines/mongodb/backup.js.map +1 -1
- package/dist/engines/mongodb/index.js +170 -108
- package/dist/engines/mongodb/index.js.map +1 -1
- package/dist/engines/mongodb/restore.js +21 -1
- package/dist/engines/mongodb/restore.js.map +1 -1
- package/dist/engines/mysql/backup.js +24 -7
- package/dist/engines/mysql/backup.js.map +1 -1
- package/dist/engines/mysql/index.js +154 -89
- package/dist/engines/mysql/index.js.map +1 -1
- package/dist/engines/mysql/restore.js +14 -4
- package/dist/engines/mysql/restore.js.map +1 -1
- package/dist/engines/postgresql/backup.js +9 -2
- package/dist/engines/postgresql/backup.js.map +1 -1
- package/dist/engines/postgresql/index.js +10 -4
- package/dist/engines/postgresql/index.js.map +1 -1
- package/dist/engines/postgresql/restore.js +7 -3
- package/dist/engines/postgresql/restore.js.map +1 -1
- package/dist/engines/qdrant/backup.js +5 -1
- package/dist/engines/qdrant/backup.js.map +1 -1
- package/dist/engines/qdrant/index.js +31 -2
- package/dist/engines/qdrant/index.js.map +1 -1
- package/dist/engines/qdrant/restore.js +5 -3
- package/dist/engines/qdrant/restore.js.map +1 -1
- package/dist/engines/questdb/auth.js +26 -0
- package/dist/engines/questdb/auth.js.map +1 -0
- package/dist/engines/questdb/backup.js +10 -8
- package/dist/engines/questdb/backup.js.map +1 -1
- package/dist/engines/questdb/index.js +16 -8
- package/dist/engines/questdb/index.js.map +1 -1
- package/dist/engines/questdb/restore.js +7 -5
- package/dist/engines/questdb/restore.js.map +1 -1
- package/dist/engines/redis/backup.js +48 -15
- package/dist/engines/redis/backup.js.map +1 -1
- package/dist/engines/redis/index.js +31 -8
- package/dist/engines/redis/index.js.map +1 -1
- package/dist/engines/redis/restore.js +21 -2
- package/dist/engines/redis/restore.js.map +1 -1
- package/dist/engines/surrealdb/auth.js +98 -0
- package/dist/engines/surrealdb/auth.js.map +1 -0
- package/dist/engines/surrealdb/backup.js +72 -9
- package/dist/engines/surrealdb/backup.js.map +1 -1
- package/dist/engines/surrealdb/index.js +84 -144
- package/dist/engines/surrealdb/index.js.map +1 -1
- package/dist/engines/surrealdb/restore.js +32 -31
- package/dist/engines/surrealdb/restore.js.map +1 -1
- package/dist/engines/typedb/backup.js +9 -1
- package/dist/engines/typedb/backup.js.map +1 -1
- package/dist/engines/typedb/cli-utils.js +3 -3
- package/dist/engines/typedb/cli-utils.js.map +1 -1
- package/dist/engines/typedb/index.js +9 -2
- package/dist/engines/typedb/index.js.map +1 -1
- package/dist/engines/typedb/restore.js +19 -7
- package/dist/engines/typedb/restore.js.map +1 -1
- package/dist/engines/valkey/backup.js +37 -13
- package/dist/engines/valkey/backup.js.map +1 -1
- package/dist/engines/valkey/index.js +207 -58
- package/dist/engines/valkey/index.js.map +1 -1
- package/dist/engines/valkey/restore.js +21 -2
- package/dist/engines/valkey/restore.js.map +1 -1
- package/dist/engines/weaviate/backup.js +7 -2
- package/dist/engines/weaviate/backup.js.map +1 -1
- package/dist/types/index.js.map +1 -1
- 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
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
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
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
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 {
|
|
1192
|
+
const { database } = container;
|
|
1159
1193
|
const db = database || 'test';
|
|
1160
1194
|
assertValidDatabaseName(db);
|
|
1161
1195
|
try {
|
|
1162
|
-
const
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
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
|
-
|
|
1236
|
-
|
|
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
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
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
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
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: ['
|
|
1348
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1373
1349
|
});
|
|
1374
|
-
let
|
|
1375
|
-
let
|
|
1350
|
+
let stdoutBuf = '';
|
|
1351
|
+
let stderrBuf = '';
|
|
1376
1352
|
proc.stdout?.on('data', (data) => {
|
|
1377
|
-
|
|
1353
|
+
stdoutBuf += data.toString();
|
|
1378
1354
|
});
|
|
1379
1355
|
proc.stderr?.on('data', (data) => {
|
|
1380
|
-
|
|
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(
|
|
1369
|
+
reject(new Error(`${stderrBuf || `mongosh exited with code ${code}`}${stdoutBuf ? `\nOutput: ${stdoutBuf}` : ''}`));
|
|
1394
1370
|
return;
|
|
1395
1371
|
}
|
|
1396
|
-
|
|
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
|
|
1439
|
-
'
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
'--
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
}
|
|
1470
|
-
|
|
1471
|
-
|
|
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 =
|
|
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'],
|