ken_nogi/Pleasanter/.claude/js/site-scripts/新・着工要因システム/migrate-process028-rename-496626.js
Kenichiro NOGI ce58cb4be4 初回コミット: dev配下(NodeSrv/Pleasanter等)をGitea管理下に統合
GitHub(nextgroup2706/ken_nogi)は今後使わず自社Gitea運用に切替え。
NodeSrvは旧リポジトリの履歴を破棄しファイルのみ統合(Dokploy用サービスアカウントは
別途mygit-admin/NodeSrv.gitに履歴あり)。notepmエクスポート(12GB)とPleasanter
インストーラzip(208MB)はサイズが大きいため.gitignoreで除外。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-04 15:37:06 +09:00

212 lines
8.2 KiB
JavaScript
Raw Permalink 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-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();