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