ken_nogi/ClaudePleasanter/営業積算システム/.claude/js/migrate-data-480111-to-496626.js
Kenichiro NOGI ed33892f08 chore: 作業中の変更を整理しコミット(複数プロジェクト分)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-02 11:09:50 +09:00

186 lines
7.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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();