commit 58a3251b8a56853e013de2f879af4465daccbd5f Author: pronchev Date: Wed Apr 1 11:50:03 2026 +0300 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..723ef36 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..1199e1b --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,38 @@ +# mongo-index-helper + +Два bash-скрипта для экспорта и импорта схемы индексов MongoDB. Предназначены для запуска внутри контейнера mongo. + +## Скрипты + +- `export-indexes.sh` — экспортирует индексы всех коллекций указанной БД в JSON-файл +- `import-indexes.sh` — импортирует индексы из JSON-файла в указанную БД + +## Технические детали + +- Требуют `mongosh` (доступен в официальном образе mongo 5+) +- Встроенный JavaScript передаётся через временный `.js`-файл в `/tmp`, который удаляется после выполнения +- Переменные среды `MONGO_DB`, `DRY_RUN`, `INDEXES_JSON` используются для передачи параметров в JS-код внутри mongosh +- Индекс `_id_` пропускается при экспорте и импорте +- Поля `v` и `ns` удаляются из экспортируемых индексов (служебные, не нужны при создании) + +## Формат JSON + +```json +{ + "collectionName": [ + { + "key": { "field": 1 }, + "name": "index_name", + "unique": true + } + ] +} +``` + +Имя БД в файл не сохраняется — передаётся параметром `--db` при каждом запуске. + +## Поведение импорта + +- Если коллекция не существует — создаётся автоматически +- Если в коллекции уже есть индексы (кроме `_id_`) — они дропаются перед созданием новых +- `--dry-run` показывает все планируемые действия без их выполнения diff --git a/README.md b/README.md new file mode 100644 index 0000000..c3b9572 --- /dev/null +++ b/README.md @@ -0,0 +1,91 @@ +# mongo-index-helper + +Bash-скрипты для экспорта и импорта схемы индексов MongoDB. Работают внутри контейнера mongo через `mongosh`. + +## Требования + +- `mongosh` (входит в официальный образ `mongo:5+`) + +## Использование + +### Экспорт + +```bash +./export-indexes.sh --uri --db --output +``` + +| Параметр | Описание | Обязательный | +|------------|---------------------------------|:------------:| +| `--uri` | MongoDB connection URI | да | +| `--db` | Имя базы данных | да | +| `--output` | Путь к выходному JSON-файлу | да | + +**Пример:** +```bash +./export-indexes.sh \ + --uri "mongodb://admin:secret@localhost:27017/admin" \ + --db mydb \ + --output indexes.json +``` + +### Импорт + +```bash +./import-indexes.sh --uri --db --input [--dry-run] +``` + +| Параметр | Описание | Обязательный | +|-------------|-------------------------------------------------------|:------------:| +| `--uri` | MongoDB connection URI | да | +| `--db` | Имя базы данных | да | +| `--input` | Путь к входному JSON-файлу | да | +| `--dry-run` | Показать планируемые действия без реального выполнения | нет | + +**Примеры:** +```bash +# Предварительный просмотр +./import-indexes.sh \ + --uri "mongodb://admin:secret@localhost:27017/admin" \ + --db mydb \ + --input indexes.json \ + --dry-run + +# Реальный импорт +./import-indexes.sh \ + --uri "mongodb://admin:secret@localhost:27017/admin" \ + --db mydb \ + --input indexes.json +``` + +## Формат файла + +Индексы сохраняются в JSON без указания имени БД: + +```json +{ + "users": [ + { + "key": { "email": 1 }, + "name": "email_1", + "unique": true + }, + { + "key": { "createdAt": 1 }, + "name": "createdAt_1", + "expireAfterSeconds": 2592000 + } + ], + "orders": [ + { + "key": { "userId": 1, "status": 1 }, + "name": "userId_1_status_1" + } + ] +} +``` + +## Поведение импорта + +- Коллекция не существует → создаётся автоматически +- В коллекции есть индексы → дропаются перед созданием новых +- Индекс `_id_` игнорируется при экспорте и импорте diff --git a/export-indexes.sh b/export-indexes.sh new file mode 100755 index 0000000..4b9457b --- /dev/null +++ b/export-indexes.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + echo "Usage: $0 --uri --db --output " + echo "" + echo " --uri MongoDB connection URI (e.g. mongodb://user:pass@host:27017/admin)" + echo " --db Database name" + echo " --output Path to output JSON file" + exit 1 +} + +URI="" +DB="" +OUTPUT="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --uri) URI="$2"; shift 2 ;; + --db) DB="$2"; shift 2 ;; + --output) OUTPUT="$2"; shift 2 ;; + *) echo "Unknown argument: $1"; usage ;; + esac +done + +[[ -z "$URI" ]] && echo "Error: --uri is required" && usage +[[ -z "$DB" ]] && echo "Error: --db is required" && usage +[[ -z "$OUTPUT" ]] && echo "Error: --output is required" && usage + +TMP_JS=$(mktemp /tmp/mongo-export-XXXXXX.js) +trap 'rm -f "$TMP_JS"' EXIT + +cat > "$TMP_JS" <<'EOF' +const dbName = process.env.MONGO_DB; +const targetDb = db.getSiblingDB(dbName); + +const result = {}; +const collections = targetDb.getCollectionNames(); + +for (const collName of collections) { + const indexes = targetDb.getCollection(collName).getIndexes(); + const filtered = indexes.filter(idx => idx.name !== "_id_"); + + if (filtered.length > 0) { + result[collName] = filtered.map(idx => { + const { v, ns, ...rest } = idx; + return rest; + }); + } +} + +print(JSON.stringify(result, null, 2)); +EOF + +MONGO_DB="$DB" mongosh --quiet "$URI" --file "$TMP_JS" > "$OUTPUT" + +echo "Exported indexes to: $OUTPUT" diff --git a/import-indexes.sh b/import-indexes.sh new file mode 100755 index 0000000..c4b8f04 --- /dev/null +++ b/import-indexes.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + echo "Usage: $0 --uri --db --input [--dry-run]" + echo "" + echo " --uri MongoDB connection URI (e.g. mongodb://user:pass@host:27017/admin)" + echo " --db Database name" + echo " --input Path to input JSON file" + echo " --dry-run Show planned actions without executing them" + exit 1 +} + +URI="" +DB="" +INPUT="" +DRY_RUN="false" + +while [[ $# -gt 0 ]]; do + case "$1" in + --uri) URI="$2"; shift 2 ;; + --db) DB="$2"; shift 2 ;; + --input) INPUT="$2"; shift 2 ;; + --dry-run) DRY_RUN="true"; shift ;; + *) echo "Unknown argument: $1"; usage ;; + esac +done + +[[ -z "$URI" ]] && echo "Error: --uri is required" && usage +[[ -z "$DB" ]] && echo "Error: --db is required" && usage +[[ -z "$INPUT" ]] && echo "Error: --input is required" && usage + +[[ ! -f "$INPUT" ]] && echo "Error: input file not found: $INPUT" && exit 1 + +TMP_JS=$(mktemp /tmp/mongo-import-XXXXXX.js) +trap 'rm -f "$TMP_JS"' EXIT + +cat > "$TMP_JS" <<'EOF' +const dbName = process.env.MONGO_DB; +const dryRun = process.env.DRY_RUN === "true"; +const data = JSON.parse(process.env.INDEXES_JSON); + +const targetDb = db.getSiblingDB(dbName); +const existingCollections = new Set(targetDb.getCollectionNames()); + +if (dryRun) { + print("[dry-run] No changes will be made.\n"); +} + +for (const [collName, indexes] of Object.entries(data)) { + if (!existingCollections.has(collName)) { + if (dryRun) { + print(`[dry-run] Would create collection: ${collName}`); + } else { + targetDb.createCollection(collName); + print(`Created collection: ${collName}`); + } + } + + const existingIndexes = targetDb.getCollection(collName).getIndexes() + .filter(idx => idx.name !== "_id_"); + + if (existingIndexes.length > 0) { + if (dryRun) { + const names = existingIndexes.map(i => i.name).join(", "); + print(`[dry-run] Would drop indexes on ${collName}: ${names}`); + } else { + targetDb.getCollection(collName).dropIndexes(); + print(`Dropped existing indexes on: ${collName}`); + } + } + + for (const idx of indexes) { + if (idx.name === "_id_") continue; + + const { key, name, ...rest } = idx; + const options = { name, ...rest }; + + if (dryRun) { + print(`[dry-run] Would create index on ${collName}: ${JSON.stringify({ key, options })}`); + } else { + targetDb.getCollection(collName).createIndex(key, options); + print(`Created index "${name}" on: ${collName}`); + } + } +} + +if (!dryRun) { + print("\nImport complete."); +} +EOF + +MONGO_DB="$DB" \ +DRY_RUN="$DRY_RUN" \ +INDEXES_JSON="$(cat "$INPUT")" \ + mongosh --quiet "$URI" --file "$TMP_JS"