import { readFile } from "node:fs/promises"; import mysql from "mysql2/promise"; const allowedTables = [ "partners", "tasks", "contents", "accounts", "claims", "delegation_bundles", "distributions", "collection_runs", "users", "auth_sessions", "mcp_export_tokens", ]; function databaseUrl() { if (process.env.DATABASE_URL) return process.env.DATABASE_URL; const host = process.env.MYSQL_HOST ?? "127.0.0.1"; const port = process.env.MYSQL_PORT ?? "3306"; const user = encodeURIComponent(process.env.MYSQL_USER ?? "koc"); const password = encodeURIComponent(process.env.MYSQL_PASSWORD ?? ""); const database = process.env.MYSQL_DATABASE ?? "koc_loop"; return `mysql://${user}:${password}@${host}:${port}/${database}`; } function normalizeImportedValue(value) { if ( typeof value === "string" && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/.test(value) ) { return value.replace("T", " ").replace(/Z$/, ""); } return value; } const inputPath = process.argv[2]; if (!inputPath) { throw new Error("用法:npm run db:import-json -- /path/to/d1-export.json"); } const parsed = JSON.parse(await readFile(inputPath, "utf8")); const tables = parsed.tables && typeof parsed.tables === "object" ? parsed.tables : parsed; const pool = mysql.createPool({ uri: databaseUrl(), connectionLimit: 2, charset: "utf8mb4", timezone: "Z", dateStrings: true, }); try { for (const table of allowedTables) { const rows = Array.isArray(tables[table]) ? tables[table] : []; if (rows.length === 0) continue; const [columnRows] = await pool.query( `SELECT column_name, extra FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = ? ORDER BY ordinal_position`, [table], ); const allowedColumns = new Set( columnRows .filter((column) => !String(column.extra ?? "").includes("GENERATED")) .map((column) => String(column.column_name)), ); const columns = [...new Set(rows.flatMap((row) => Object.keys(row)))] .filter((column) => allowedColumns.has(column)); if (columns.length === 0) continue; const quotedColumns = columns.map((column) => `\`${column}\``).join(", "); const updates = columns .filter((column) => column !== "id" && column !== "token_hash") .map((column) => `\`${column}\` = VALUES(\`${column}\`)`) .join(", "); const connection = await pool.getConnection(); try { await connection.beginTransaction(); for (let offset = 0; offset < rows.length; offset += 200) { const chunk = rows.slice(offset, offset + 200); const placeholders = chunk .map(() => `(${columns.map(() => "?").join(", ")})`) .join(", "); const sql = `INSERT INTO \`${table}\` (${quotedColumns}) VALUES ${placeholders}${ updates ? ` ON DUPLICATE KEY UPDATE ${updates}` : "" }`; const values = chunk.flatMap((row) => columns.map((column) => normalizeImportedValue(row[column] ?? null)), ); await connection.query(sql, values); } await connection.commit(); console.info(`imported ${table}: ${rows.length}`); } catch (error) { await connection.rollback(); throw error; } finally { connection.release(); } } } finally { await pool.end(); }