211 lines
6.8 KiB
TypeScript
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);
|
|
}
|
|
}
|