186 lines
7.8 KiB
JavaScript
186 lines
7.8 KiB
JavaScript
/**
|
||
* 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 SOURCE_SITE_ID = 480111;
|
||
const TARGET_SITE_ID = 496626;
|
||
const BATCH_SIZE = 20;
|
||
|
||
const baseDir = path.join(__dirname, "..", "..");
|
||
const configsDir = path.join(baseDir, "configs");
|
||
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 = JSON.parse(fs.readFileSync(path.join(baseDir, "config.json"), "utf-8"));
|
||
|
||
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();
|