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>
170 lines
6.0 KiB
JavaScript
170 lines
6.0 KiB
JavaScript
"use strict";
|
||
/*
|
||
* 本番プリザンター(data/*.csv)からエクスポート済みのDepts/Users/Groupsを
|
||
* テスト環境(PLEASANTER_TEST_BASE_URL)へ投入する一回限りの移行スクリプト。
|
||
*
|
||
* 実行:
|
||
* PLEASANTER_TEST_BASE_URL=... PLEASANTER_TEST_API_KEY=... TEST_USER_PASSWORD=... node scripts/import-prod-to-test.js
|
||
*/
|
||
const fs = require("fs");
|
||
const path = require("path");
|
||
|
||
const BASE_URL = process.env.PLEASANTER_TEST_BASE_URL;
|
||
const API_KEY = process.env.PLEASANTER_TEST_API_KEY;
|
||
const PASSWORD = process.env.TEST_USER_PASSWORD;
|
||
const DATA_DIR = path.join(__dirname, "..", "data");
|
||
|
||
if (!BASE_URL || !API_KEY || !PASSWORD) {
|
||
console.error("PLEASANTER_TEST_BASE_URL / PLEASANTER_TEST_API_KEY / TEST_USER_PASSWORD が未設定");
|
||
process.exit(1);
|
||
}
|
||
|
||
function parseCsv(text) {
|
||
const rows = [];
|
||
let row = [], field = "", inQuotes = false;
|
||
const s = text.replace(/^/, "");
|
||
for (let i = 0; i < s.length; i++) {
|
||
const c = s[i];
|
||
if (inQuotes) {
|
||
if (c === '"') { if (s[i + 1] === '"') { field += '"'; i++; } else inQuotes = false; }
|
||
else field += c;
|
||
} else {
|
||
if (c === '"') inQuotes = true;
|
||
else if (c === ",") { row.push(field); field = ""; }
|
||
else if (c === "\n") { row.push(field); rows.push(row); row = []; field = ""; }
|
||
else if (c === "\r") { /* skip */ }
|
||
else field += c;
|
||
}
|
||
}
|
||
if (field.length || row.length) { row.push(field); rows.push(row); }
|
||
const header = rows[0];
|
||
return rows.slice(1).filter(r => r.length === header.length).map(r => Object.fromEntries(header.map((h, i) => [h, r[i]])));
|
||
}
|
||
|
||
async function api(pathname, body) {
|
||
const res = await fetch(BASE_URL.replace(/\/$/, "") + pathname, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json; charset=utf-8" },
|
||
body: JSON.stringify({ ApiVersion: "1.1", ApiKey: API_KEY, ...body }),
|
||
});
|
||
const data = await res.json();
|
||
if (!res.ok || data.StatusCode !== 200) {
|
||
throw new Error(`${pathname} failed: ${res.status} ${JSON.stringify(data)}`);
|
||
}
|
||
return data;
|
||
}
|
||
|
||
async function importDepts() {
|
||
const depts = parseCsv(fs.readFileSync(path.join(DATA_DIR, "pleasanter_depts.csv"), "utf8"));
|
||
const deptIdMap = new Map(); // 本番DeptId -> テスト環境DeptId
|
||
let ok = 0, ng = 0;
|
||
for (const d of depts) {
|
||
try {
|
||
const res = await api("/api/depts/create", { DeptCode: d.DeptCode, DeptName: d.DeptName });
|
||
deptIdMap.set(d.DeptId, res.Id);
|
||
ok++;
|
||
} catch (e) {
|
||
console.error("[dept]", d.DeptId, d.DeptName, e.message);
|
||
ng++;
|
||
}
|
||
}
|
||
console.log(`Depts: ok=${ok} ng=${ng}`);
|
||
return deptIdMap;
|
||
}
|
||
|
||
async function importUsers(deptIdMap) {
|
||
const users = parseCsv(fs.readFileSync(path.join(DATA_DIR, "pleasanter_users.csv"), "utf8"))
|
||
.filter(u => u.UserId !== "1");
|
||
const userIdMap = new Map(); // 本番UserId -> テスト環境UserId
|
||
let ok = 0, ng = 0;
|
||
for (const u of users) {
|
||
try {
|
||
const mail = (u.MailAddresses || "").split(";").filter(Boolean);
|
||
const newDeptId = deptIdMap.get(u.DeptId);
|
||
const res = await api("/api/users/create", {
|
||
LoginId: u.LoginId,
|
||
Name: u.Name,
|
||
Password: PASSWORD,
|
||
MailAddresses: mail,
|
||
DeptId: newDeptId || 0,
|
||
GlobalId: `pleasanter-prod-${u.UserId}`,
|
||
});
|
||
userIdMap.set(u.UserId, res.Id);
|
||
ok++;
|
||
} catch (e) {
|
||
console.error("[user]", u.UserId, u.LoginId, e.message);
|
||
ng++;
|
||
}
|
||
}
|
||
console.log(`Users: ok=${ok} ng=${ng}`);
|
||
return userIdMap;
|
||
}
|
||
|
||
async function importGroups(userIdMap, deptIdMap) {
|
||
const groups = parseCsv(fs.readFileSync(path.join(DATA_DIR, "pleasanter_groups.csv"), "utf8"));
|
||
const groupIdMap = new Map(); // 本番GroupId -> テスト環境GroupId
|
||
let ok = 0, ng = 0;
|
||
|
||
// 1st pass: 空メンバーで作成しID採番
|
||
for (const g of groups) {
|
||
try {
|
||
const res = await api("/api/groups/create", { GroupName: g.GroupName });
|
||
groupIdMap.set(g.GroupId, res.Id);
|
||
ok++;
|
||
} catch (e) {
|
||
console.error("[group create]", g.GroupId, g.GroupName, e.message);
|
||
ng++;
|
||
}
|
||
}
|
||
console.log(`Groups(create): ok=${ok} ng=${ng}`);
|
||
|
||
// 2nd pass: GroupMembers/GroupChildrenを本番ID→テスト環境IDへ変換してUpdate
|
||
let uOk = 0, uNg = 0;
|
||
for (const g of groups) {
|
||
const newGroupId = groupIdMap.get(g.GroupId);
|
||
if (!newGroupId) continue;
|
||
const members = (g.GroupMembers || "").split(";").filter(Boolean).map(m => {
|
||
const [kind, id, flag] = m.split(",");
|
||
if (kind === "User") {
|
||
const nid = userIdMap.get(id);
|
||
return nid ? `User,${nid},${flag}` : null;
|
||
}
|
||
if (kind === "Dept") {
|
||
const nid = deptIdMap.get(id);
|
||
return nid ? `Dept,${nid},${flag}` : null;
|
||
}
|
||
return null;
|
||
}).filter(Boolean);
|
||
const children = (g.GroupChildren || "").split(";").filter(Boolean).map(c => {
|
||
const [, id] = c.split(",");
|
||
const nid = groupIdMap.get(id);
|
||
return nid ? `Group,${nid},` : null;
|
||
}).filter(Boolean);
|
||
|
||
try {
|
||
await api(`/api/groups/${newGroupId}/update`, { GroupMembers: members, GroupChildren: children });
|
||
uOk++;
|
||
} catch (e) {
|
||
console.error("[group update]", g.GroupId, "->", newGroupId, e.message);
|
||
uNg++;
|
||
}
|
||
}
|
||
console.log(`Groups(update members): ok=${uOk} ng=${uNg}`);
|
||
return groupIdMap;
|
||
}
|
||
|
||
async function main() {
|
||
const deptIdMap = await importDepts();
|
||
const userIdMap = await importUsers(deptIdMap);
|
||
const groupIdMap = await importGroups(userIdMap, deptIdMap);
|
||
|
||
const mapDir = path.join(DATA_DIR, "test-import-maps");
|
||
fs.mkdirSync(mapDir, { recursive: true });
|
||
fs.writeFileSync(path.join(mapDir, "dept-id-map.json"), JSON.stringify(Object.fromEntries(deptIdMap), null, 2));
|
||
fs.writeFileSync(path.join(mapDir, "user-id-map.json"), JSON.stringify(Object.fromEntries(userIdMap), null, 2));
|
||
fs.writeFileSync(path.join(mapDir, "group-id-map.json"), JSON.stringify(Object.fromEntries(groupIdMap), null, 2));
|
||
console.log("完了。IDマッピングを data/test-import-maps/ に保存");
|
||
}
|
||
|
||
main().catch(e => { console.error(e); process.exit(1); });
|