/** * migrate-data-480111-to-496626.js * ------------------------------------------------------------ * SiteId 480111 の全レコードを取得し、SiteSettings側で行った連番リネーム * (Date031〜047 → Date032〜048、Date131〜147 → Date132〜148、 * Check131〜147 → Check132〜148、Class031〜047 → Class032〜048)と * 同じマッピングでフィールド値を付け替えた上で、SiteId 496626 へ * bulkupsert(新規作成)でインポートする。 * * 除外するもの: * - ResultId / SiteId / Ver / CreatedTime / UpdatedTime / Locked / ItemTitle * (新サイト側で自動採番・自動計算されるため) * - Comments(変更履歴コメント。テキスト量が多く、Creatorが480111時点のユーザーIDのため * そのまま再現すると誤解を招く可能性がある。除外し、必要なら別途手動で移行する) * - AttachmentsHash(添付ファイルはサイトをまたいだAPI経由の単純コピーができないため対象外。 * 添付がある場合はプレビュー時に警告する) * * 使い方: * node migrate-data-480111-to-496626.js … 件数・サンプル・警告の表示のみ(送信なし) * node migrate-data-480111-to-496626.js --execute … 上記に加えて実際にbulkupsertで送信する * ------------------------------------------------------------ */ const fs = require("fs"); const path = require("path"); const { findSiteDir, newModifyDir } = require("../../site-paths"); const { REPO_ROOT, loadServerConfig } = require("../../resolve-project"); const PROJECT_NAME = "新・着工要因システム"; const ENV = "production"; const SOURCE_SITE_ID = 480111; const TARGET_SITE_ID = 496626; const BATCH_SIZE = 20; const baseDir = path.join(REPO_ROOT, PROJECT_NAME); const configsDir = path.join(baseDir, "configs", ENV); const execute = process.argv.includes("--execute"); const requestArg = process.argv.find((a) => a.startsWith("--request=")); const requestLabel = requestArg ? requestArg.split("=")[1] : null; function pad3(n) { return String(n).padStart(3, "0"); } function timestamp() { return new Date().toISOString().replace(/[:.]/g, "-"); } // SiteSettings側の連番リネームと同じマッピングを構築する const classMap = {}; const dateMap = {}; const checkMap = {}; for (let i = 31; i <= 47; i++) { classMap[`Class${pad3(i)}`] = `Class${pad3(i + 1)}`; dateMap[`Date${pad3(i)}`] = `Date${pad3(i + 1)}`; dateMap[`Date${i + 100}`] = `Date${i + 101}`; checkMap[`Check${i + 100}`] = `Check${i + 101}`; } function remapHash(hash, map) { const result = {}; for (const [key, value] of Object.entries(hash || {})) { const newKey = map[key] || key; result[newKey] = value; } return result; } async function fetchAllRecords(siteId, { BaseUrl, ApiKey, ApiVersion }) { const url = `${BaseUrl.replace(/\/+$/, "")}/api/items/${siteId}/get`; let all = []; let offset = 0; let totalCount = null; while (true) { const res = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ApiVersion: ApiVersion || "1.1", ApiKey, Offset: offset }), }); const json = await res.json(); if (json.StatusCode !== 200) { console.error(`[エラー] レコード取得に失敗しました(Offset:${offset}):`, JSON.stringify(json).slice(0, 500)); process.exit(1); } const page = json.Response.Data || []; all = all.concat(page); totalCount = json.Response.TotalCount ?? all.length; if (page.length === 0 || all.length >= totalCount) break; offset = all.length; } return all; } function buildMigratedRecord(record) { return { Body: record.Body, Status: record.Status, Manager: record.Manager, Owner: record.Owner, ClassHash: remapHash(record.ClassHash, classMap), NumHash: record.NumHash || {}, DateHash: remapHash(record.DateHash, dateMap), DescriptionHash: record.DescriptionHash || {}, CheckHash: remapHash(record.CheckHash, checkMap), }; } async function main() { const config = loadServerConfig(ENV); console.log(`[INFO] SiteId ${SOURCE_SITE_ID} の全レコードを取得しています...`); const sourceRecords = await fetchAllRecords(SOURCE_SITE_ID, config); console.log(`[OK] ${sourceRecords.length}件取得しました。`); const attachmentRecords = sourceRecords.filter( (r) => r.AttachmentsHash && Object.values(r.AttachmentsHash).some((a) => Array.isArray(a) && a.length > 0) ); const migrated = sourceRecords.map(buildMigratedRecord); console.log("\n========================================"); console.log(` データ移行 (480111 → 496626)${execute ? "(実行)" : "(プレビューのみ)"}`); console.log("========================================"); console.log(`移行対象件数: ${migrated.length}件`); if (attachmentRecords.length > 0) { console.log(`\n⚠️ 添付ファイルを含むレコードが${attachmentRecords.length}件あります(ResultId: ${attachmentRecords.map((r) => r.ResultId).join(", ")})。`); console.log(" 添付ファイルは本スクリプトの移行対象外です(API経由でのサイト間コピーは非対応のため)。"); console.log(" 必要な場合は管理画面から手動での再アップロードを検討してください。"); } else { console.log("添付ファイルを含むレコードはありません。"); } console.log("\nComments(変更履歴コメント)は移行対象外です。"); console.log("\n--- サンプル(1件目、移行後の姿) ---"); console.log(JSON.stringify(migrated[0], null, 2)); const siteDir = findSiteDir(configsDir, TARGET_SITE_ID); if (!siteDir) { console.error(`[エラー] SiteId ${TARGET_SITE_ID} のフォルダが見つかりません。`); process.exit(1); } // requestLabel未指定時は固定ラベルを使い、プレビュー→実行を同じフォルダにまとめる const modifyDir = newModifyDir(siteDir, requestLabel || "data-migration"); const previewPath = path.join(modifyDir, "data_migration_preview.json"); fs.writeFileSync(previewPath, JSON.stringify(migrated, null, 2), "utf-8"); console.log(`\n[OK] 移行予定データ全件をプレビュー保存しました: ${previewPath}`); if (!execute) { console.log("\n[注意] --execute が指定されていないため、送信は行っていません。"); console.log("内容を確認の上、問題なければ次を実行してください: node migrate-data-480111-to-496626.js --execute"); return; } const url = `${config.BaseUrl.replace(/\/+$/, "")}/api/items/${TARGET_SITE_ID}/bulkupsert`; const results = []; for (let i = 0; i < migrated.length; i += BATCH_SIZE) { const batch = migrated.slice(i, i + BATCH_SIZE); console.log(`\n実際に送信します(${i + 1}〜${i + batch.length}件目 / 全${migrated.length}件)...`); const res = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ApiVersion: config.ApiVersion || "1.1", ApiKey: config.ApiKey, SsCache: true, Data: batch, }), }); const text = await res.text(); console.log(`HTTP ${res.status} ${res.statusText}`); let json; try { json = JSON.parse(text); console.log(json.StatusCode === 200 ? "[OK] このバッチは成功しました。" : "[エラー] このバッチは失敗した可能性があります。"); } catch { console.log(text.slice(0, 1000)); } results.push({ batchStart: i, status: res.status, body: text }); } const resultPath = path.join(modifyDir, `data_migration_result_${timestamp()}.json`); fs.writeFileSync(resultPath, JSON.stringify(results, null, 2), "utf-8"); console.log(`\n[OK] 送信結果を保存しました: ${resultPath}`); } main();