Files
2026-07-13 12:30:28 +08:00

211 lines
6.8 KiB
TypeScript

import chalk from 'chalk';
import fs from 'fs-extra';
import path from 'path';
import axios from 'axios';
import { getAuthHeaders } from './utils/webLogin';
import {
createApiError,
createConfigError,
printCliError,
} from './utils/cliError';
import { getPinmeApiUrl } from './utils/config';
import tracker, { getTrackErrorReason } from './utils/tracker';
import {
TRACK_EVENTS,
TRACK_PAGES,
resolveTrackAction,
} from './utils/trackerEvents';
const PROJECT_DIR = process.cwd();
interface UpdateDbOptions {
projectName?: string;
name?: string;
}
// ============ 工具函数 ============
function loadConfig() {
const configPath = path.join(PROJECT_DIR, 'pinme.toml');
if (!fs.existsSync(configPath)) {
throw createConfigError('`pinme.toml` not found in the current directory.', [
'Run this command from the Pinme project root.',
]);
}
const configContent = fs.readFileSync(configPath, 'utf-8');
const projectNameMatch = configContent.match(/project_name\s*=\s*"([^"]+)"/);
return {
project_name: projectNameMatch?.[1] || '',
};
}
function getSqlFiles(): string[] {
const sqlDir = path.join(PROJECT_DIR, 'db');
if (!fs.existsSync(sqlDir)) {
throw createConfigError('SQL directory not found: `db/`.', [
'Create a `db/` directory and add at least one `.sql` migration file.',
]);
}
const files = fs.readdirSync(sqlDir)
.filter(f => f.endsWith('.sql'))
.sort();
if (files.length === 0) {
throw createConfigError('No `.sql` files were found in `db/`.', [
'Add one or more migration files before running `pinme update-db`.',
]);
}
return files.map(f => path.join(sqlDir, f));
}
// ============ 数据库更新 ============
async function updateDb(sqlFiles: string[], projectName: string) {
console.log(chalk.blue('Importing SQL files to database...'));
console.log(chalk.gray(`Project: ${projectName}`));
console.log(chalk.gray(`SQL files: ${sqlFiles.length}`));
const apiUrl = `${getPinmeApiUrl('/update_db')}?project_name=${encodeURIComponent(projectName)}`;
const headers = getAuthHeaders();
console.log(chalk.gray(`API URL: ${apiUrl}`));
try {
const FormData = (await import('formdata-node')).FormData;
const Blob = (await import('formdata-node')).Blob;
const formData = new FormData() as any;
// Add SQL files (can have multiple files with same field name)
let totalSize = 0;
for (const sqlFile of sqlFiles) {
const filename = path.basename(sqlFile);
const content = fs.readFileSync(sqlFile);
totalSize += content.length;
if (totalSize > 10 * 1024 * 1024) {
throw createConfigError('Total SQL payload exceeds the 10MB platform limit.', [
'Split migrations into smaller batches, then rerun `pinme update-db`.',
]);
}
formData.append('file', new Blob([content], {
type: 'application/sql',
}), filename);
console.log(chalk.gray(` Including: ${filename} (${content.length} bytes)`));
}
const response = await axios.post(apiUrl, formData, {
headers: { ...headers },
timeout: 120000,
});
console.log(chalk.gray(` Response: ${JSON.stringify(response.data)}`));
if (response.data.code === 200) {
console.log(chalk.green('SQL files imported successfully!'));
// Display results
const results = response.data.data.results;
for (const result of results) {
if (result.status === 'complete') {
console.log(chalk.green(` COMPLETE ${result.filename}: ${result.num_queries} queries, ${result.duration}ms`));
if (result.changes !== undefined) {
console.log(chalk.gray(` Changes: ${result.changes}, Read: ${result.rows_read}, Written: ${result.rows_written}`));
}
} else if (result.status === 'error') {
console.log(chalk.red(` ERROR ${result.filename}: ${result.error}`));
}
}
} else {
// Handle partial failure or other errors
throw createApiError('database update', { response: { data: response.data } }, [
`Project: ${projectName}`,
`Endpoint: ${apiUrl}`,
], [
'Inspect the SQL result list above to identify the failing migration file.',
]);
}
} catch (error: any) {
throw createApiError('database update', error, [
`Project: ${projectName}`,
`Endpoint: ${apiUrl}`,
], [
'Validate the SQL syntax and check whether any migration is re-applying an existing schema change.',
]);
}
}
// ============ 主函数 ============
/**
* Update database: import SQL files to project's D1 database
* API: POST /api/v4/update_db?project_name={name}
*/
export default async function updateDbCmd(options?: UpdateDbOptions): Promise<void> {
try {
// Check if user is logged in
const headers = getAuthHeaders();
if (!headers['authentication-tokens'] || !headers['token-address']) {
throw createConfigError('No valid local login session was found.', [
'Run `pinme login` and retry.',
]);
}
// Copy token to project directory for sub-commands
const projectDir = options?.projectName || options?.name ? path.join(PROJECT_DIR, options.projectName || options.name!) : PROJECT_DIR;
const tokenFileSrc = path.join(PROJECT_DIR, '.token.json');
const tokenFileDst = path.join(projectDir, '.token.json');
if (fs.existsSync(tokenFileSrc) && !fs.existsSync(tokenFileDst)) {
fs.copySync(tokenFileSrc, tokenFileDst);
}
console.log(chalk.blue('Importing SQL to database...\n'));
console.log(chalk.gray(`Project dir: ${PROJECT_DIR}`));
const config = loadConfig();
const projectName = config.project_name;
if (!projectName) {
throw createConfigError('`project_name` is missing in `pinme.toml`.', [
'Set `project_name = "your-project-name"` in `pinme.toml`.',
]);
}
console.log(chalk.gray(`Project: ${projectName}`));
// Get SQL files from db directory
const sqlFiles = getSqlFiles();
console.log(chalk.gray(`Found ${sqlFiles.length} SQL file(s) in db`));
await updateDb(sqlFiles, projectName);
console.log(chalk.green('\nDatabase update complete.'));
void tracker.trackEvent(
TRACK_EVENTS.projectUpdateDbSuccess,
TRACK_PAGES.deploy,
{
a: resolveTrackAction(TRACK_EVENTS.projectUpdateDbSuccess),
project_name: projectName,
sql_file_count: sqlFiles.length,
},
);
process.exit(0);
} catch (error: any) {
void tracker.trackEvent(
TRACK_EVENTS.projectUpdateDbFailed,
TRACK_PAGES.deploy,
{
a: resolveTrackAction(TRACK_EVENTS.projectUpdateDbFailed),
project_name: options?.projectName || options?.name,
reason: getTrackErrorReason(error),
},
);
printCliError(error, 'Database update failed.');
process.exit(1);
}
}