/** * migrate-process028-rename-496626.js * ------------------------------------------------------------ * site-496626「新・着工要因(東京)v2」item8対応。 * SiteSettings側で行った番号リネーム(Date029〜048 → Date028〜047、 * Date129〜148 → Date128〜147、Class029〜048 → Class028〜047、 * Check129〜148 → Check128〜147)と同じマッピングで、既存全レコードの * フィールド値を同一サイト内で付け替える(update)。 * * 旧Date028(引越し)の値は破棄する。リネームで空くスロット * (新Date048/Date148/Class048/Check148)は明示的にクリアする * (日付のNULL化はPleasanter仕様により "1899/12/31" を送る。 * 参照: https://pleasanter.org/ja/manual/faq-api-set-date-to-null)。 * * 読み取りは常に移行前の元レコードから行い、書き込みは新しいハッシュ * オブジェクトへ行うため、処理順序による上書き事故は発生しない。 * * 使い方: * node migrate-process028-rename-496626.js … 件数・サンプル表示のみ(送信なし) * node migrate-process028-rename-496626.js --execute … 上記に加えて実際にupdateで送信する * ------------------------------------------------------------ */ const fs = require("fs"); const path = require("path"); const { findSiteDir, latestModifyDir, newModifyDir } = require("../../site-paths"); const { REPO_ROOT, loadServerConfig } = require("../../resolve-project"); const PROJECT_NAME = "新・着工要因システム"; const ENV = "production"; const 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"); function pad3(n) { return String(n).padStart(3, "0"); } // Pleasanterの「未入力」は 1899-12-30 という番兵値で返るため、年が1899以下なら未入力扱いにする function isEmptyDate(v) { if (!v) return true; const d = new Date(v); return isNaN(d.getTime()) || d.getFullYear() <= 1899; } function timestamp() { return new Date().toISOString().replace(/[:.]/g, "-"); } // SiteSettings側のリネームと同じマッピングを構築する(29〜48 → 28〜47) const dateMap = {}; const classMap = {}; const checkMap = {}; for (let n = 29; n <= 48; n++) { const newN = n - 1; dateMap[`Date${pad3(n)}`] = `Date${pad3(newN)}`; dateMap[`Date${n + 100}`] = `Date${newN + 100}`; classMap[`Class${pad3(n)}`] = `Class${pad3(newN)}`; checkMap[`Check${n + 100}`] = `Check${newN + 100}`; } 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; } // 読み取りは常に元のrecordから、書き込みは新しいhashへ行う function buildMigratedRecord(record) { const oldDate = record.DateHash || {}; const oldClass = record.ClassHash || {}; const oldCheck = record.CheckHash || {}; const newDate = {}; const newClass = {}; const newCheck = {}; for (const [oldKey, newKey] of Object.entries(dateMap)) { newDate[newKey] = oldDate[oldKey] || "1899/12/31"; } for (const [oldKey, newKey] of Object.entries(classMap)) { newClass[newKey] = oldClass[oldKey] || ""; } for (const [oldKey, newKey] of Object.entries(checkMap)) { newCheck[newKey] = oldCheck[oldKey] || false; } // 空くスロット(新Date048/Date148/Class048/Check148)を明示的にクリア newDate["Date048"] = "1899/12/31"; newDate["Date148"] = "1899/12/31"; newClass["Class048"] = ""; newCheck["Check148"] = false; return { ResultId: record.ResultId, DateHash: newDate, ClassHash: newClass, CheckHash: newCheck, }; } async function main() { const config = loadServerConfig(ENV); console.log(`[INFO] SiteId ${SITE_ID} の全レコードを取得しています...`); const records = await fetchAllRecords(SITE_ID, config); console.log(`[OK] ${records.length}件取得しました。`); const migrated = records.map(buildMigratedRecord); const withProcess028Data = records.filter( (r) => r.DateHash && !isEmptyDate(r.DateHash.Date028) ); console.log("\n========================================"); console.log(` データ移行: 引越し(Date028)削除+後続20工程リネーム${execute ? "(実行)" : "(プレビューのみ)"}`); console.log("========================================"); console.log(`移行対象件数: ${migrated.length}件`); if (withProcess028Data.length > 0) { console.log( `\n[注意] 旧「引越し」(Date028)に実績日が入っていたレコードが${withProcess028Data.length}件あります(ResultId: ${withProcess028Data .map((r) => r.ResultId) .join(", ")})。この値は破棄されます。` ); } else { console.log("旧「引越し」(Date028)にデータが入っているレコードはありません。"); } console.log("\n--- サンプル(1件目、移行後の姿) ---"); console.log(JSON.stringify(migrated[0], null, 2)); const siteDir = findSiteDir(configsDir, SITE_ID); const modifyDir = latestModifyDir(siteDir) || newModifyDir(siteDir, "9item-revision"); const previewPath = path.join(modifyDir, "data_migration_process028_preview.json"); fs.writeFileSync(previewPath, JSON.stringify(migrated, null, 2), "utf-8"); console.log(`\n[OK] 移行予定データ全件をプレビュー保存しました: ${previewPath}`); // 突合用に移行前の生データも保存しておく const beforePath = path.join(modifyDir, "data_migration_process028_before.json"); fs.writeFileSync(beforePath, JSON.stringify(records, null, 2), "utf-8"); console.log(`[OK] 移行前の生データを保存しました: ${beforePath}`); if (!execute) { console.log("\n[注意] --execute が指定されていないため、送信は行っていません。"); console.log("内容を確認の上、問題なければ次を実行してください: node migrate-process028-rename-496626.js --execute"); return; } // bulkupsertはResultIdを指定しても新規追加として扱われる事象を確認したため、 // 確実に既存レコードを更新する単体update API(/api/items/{ResultId}/update)を // レコードごとに個別に呼び出す方式にする。 const results = []; let okCount = 0; let ngCount = 0; for (const rec of migrated) { const { ResultId, ...body } = rec; const url = `${config.BaseUrl.replace(/\/+$/, "")}/api/items/${ResultId}/update`; const res = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ApiVersion: config.ApiVersion || "1.1", ApiKey: config.ApiKey, ...body, }), }); const text = await res.text(); let json; let ok = false; try { json = JSON.parse(text); ok = json.StatusCode === 200; } catch { ok = false; } ok ? okCount++ : ngCount++; console.log(`ResultId ${ResultId}: HTTP ${res.status} ${ok ? "[OK]" : "[エラー] " + text.slice(0, 300)}`); results.push({ resultId: ResultId, status: res.status, body: text }); } console.log(`\n成功: ${okCount}件 / 失敗: ${ngCount}件`); const resultPath = path.join(modifyDir, `data_migration_process028_result_${timestamp()}.json`); fs.writeFileSync(resultPath, JSON.stringify(results, null, 2), "utf-8"); console.log(`\n[OK] 送信結果を保存しました: ${resultPath}`); } main();