| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229 |
- import fs from 'node:fs';
- import path from 'node:path';
- const PARSE_HOST = normalizeParseHost(process.env.SMOKE_PARSE_HOST || 'https://server.fmode.cn');
- const APP_ID = process.env.SMOKE_PARSE_APP_ID || 'ncloudmaster';
- const SESSION_TOKEN = process.env.SMOKE_SESSION_TOKEN || '';
- const CONFIRM_TEXT = 'DELETE_VIDEO_WORKFLOW_TEST_DATA';
- const CONFIRM = readArg('--confirm') || process.env.VIDEO_WORKFLOW_CLEANUP_CONFIRM || '';
- const EXECUTE = CONFIRM === CONFIRM_TEXT;
- const CONCURRENCY = clamp(Number(readArg('--concurrency') || process.env.VIDEO_WORKFLOW_CLEANUP_CONCURRENCY || 2), 1, 4);
- const OUTPUT_DIR = process.env.VIDEO_WORKFLOW_CLEANUP_OUTPUT_DIR
- ? path.resolve(process.env.VIDEO_WORKFLOW_CLEANUP_OUTPUT_DIR)
- : path.join(process.cwd(), 'tmp');
- const PROJECT_KEY = 'video-workflow';
- const ALLOWED_CLASSES = [
- 'VideoWorkflowEntity',
- 'VideoWorkflowAudit',
- 'VideoWorkflowMigration',
- 'VideoWorkflowFileAsset',
- ];
- const requestedClasses = parseRequestedClasses(readArg('--classes') || process.env.VIDEO_WORKFLOW_CLEANUP_CLASSES || '');
- const targetClasses = requestedClasses.length ? requestedClasses : ALLOWED_CLASSES;
- if (!SESSION_TOKEN) {
- console.error('Missing SMOKE_SESSION_TOKEN. Refusing to inspect or delete Parse data.');
- process.exit(1);
- }
- const invalidClasses = targetClasses.filter((className) => !ALLOWED_CLASSES.includes(className));
- if (invalidClasses.length) {
- console.error(`Refusing to continue. Classes outside allowlist: ${invalidClasses.join(', ')}`);
- console.error(`Allowed classes: ${ALLOWED_CLASSES.join(', ')}`);
- process.exit(1);
- }
- const startedAt = new Date().toISOString();
- const me = await parseRequest('GET', '/users/me');
- const ownerId = me?.objectId || '';
- if (!ownerId) {
- console.error('Could not resolve current Parse user from SMOKE_SESSION_TOKEN.');
- process.exit(1);
- }
- const rowsByClass = {};
- for (const className of targetClasses) {
- rowsByClass[className] = await listRows(className, ownerId);
- }
- const allTargets = Object.entries(rowsByClass).flatMap(([className, rows]) =>
- rows.map((row) => ({
- className,
- objectId: row.objectId,
- ownerId: row.ownerId || '',
- projectKey: row.projectKey || '',
- entityType: row.entityType || '',
- entityId: row.entityId || '',
- status: row.status || '',
- createdAt: row.createdAt || '',
- updatedAt: row.updatedAt || '',
- })),
- );
- console.log(`VideoWorkflow Parse cleanup generatedAt=${startedAt}`);
- console.log(`mode=${EXECUTE ? 'EXECUTE_DELETE' : 'DRY_RUN_ONLY'}`);
- console.log(`ownerId=${ownerId}`);
- console.log(`classes=${targetClasses.join(', ')}`);
- console.log('');
- console.log('Rows by class:');
- for (const className of targetClasses) {
- console.log(`- ${className}: ${rowsByClass[className].length}`);
- }
- console.log('');
- console.log(`Total target rows: ${allTargets.length}`);
- let deleteResults = [];
- if (!EXECUTE) {
- console.log('');
- console.log('No data was deleted.');
- console.log(`To execute physical delete, rerun with: --confirm ${CONFIRM_TEXT}`);
- } else {
- deleteResults = await deleteTargets(allTargets);
- }
- const completedAt = new Date().toISOString();
- const failures = deleteResults.filter((item) => !item.ok);
- const report = {
- startedAt,
- completedAt,
- mode: EXECUTE ? 'execute' : 'dry-run',
- ownerId,
- allowedClasses: ALLOWED_CLASSES,
- targetClasses,
- totalTargets: allTargets.length,
- countsByClass: Object.fromEntries(targetClasses.map((className) => [className, rowsByClass[className].length])),
- deleteAttempted: deleteResults.length,
- deleteSuccess: deleteResults.filter((item) => item.ok).length,
- deleteFailures: failures.length,
- targets: allTargets,
- deleteResults,
- };
- fs.mkdirSync(OUTPUT_DIR, { recursive: true });
- const reportPath = path.join(
- OUTPUT_DIR,
- `videoworkflow-parse-cleanup-${EXECUTE ? 'delete' : 'dry-run'}-${safeTimestamp(completedAt)}.json`,
- );
- fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
- console.log('');
- console.log(`Cleanup ${EXECUTE ? 'execute' : 'dry-run'} complete.`);
- console.log(`Report: ${reportPath}`);
- if (failures.length) process.exit(1);
- async function listRows(className, ownerId) {
- const output = [];
- const pageSize = 1000;
- for (let skip = 0; ; skip += pageSize) {
- const query = new URLSearchParams();
- query.set('where', JSON.stringify({ projectKey: PROJECT_KEY, ownerId }));
- query.set('limit', String(pageSize));
- query.set('skip', String(skip));
- query.set('order', 'createdAt');
- query.set('keys', 'objectId,projectKey,ownerId,entityType,entityId,status,createdAt,updatedAt');
- const data = await parseRequest('GET', `/classes/${encodeURIComponent(className)}?${query.toString()}`);
- const rows = Array.isArray(data.results) ? data.results : [];
- output.push(...rows);
- if (rows.length < pageSize) break;
- }
- return output;
- }
- async function deleteTargets(targets) {
- const results = [];
- let index = 0;
- await Promise.all(Array.from({ length: CONCURRENCY }, async () => {
- while (index < targets.length) {
- const currentIndex = index;
- index += 1;
- const target = targets[currentIndex];
- results[currentIndex] = await deleteTarget(target);
- if ((currentIndex + 1) % 100 === 0 || currentIndex + 1 === targets.length) {
- console.log(`delete progress ${currentIndex + 1}/${targets.length}`);
- }
- }
- }));
- const failures = results.filter((item) => !item.ok);
- console.log('');
- console.log(`Delete complete. success=${results.length - failures.length} failures=${failures.length}`);
- return results;
- }
- async function deleteTarget(target) {
- try {
- await parseRequest('DELETE', `/classes/${encodeURIComponent(target.className)}/${encodeURIComponent(target.objectId)}`);
- return { ...target, ok: true, error: '' };
- } catch (error) {
- return { ...target, ok: false, error: error?.message || 'delete failed' };
- }
- }
- async function parseRequest(method, parsePath) {
- const headers = {
- Accept: 'application/json',
- 'X-Parse-Application-Id': APP_ID,
- 'X-Parse-Session-Token': SESSION_TOKEN,
- };
- let lastError = null;
- for (let attempt = 1; attempt <= 4; attempt += 1) {
- try {
- const response = await fetch(`${PARSE_HOST}/parse${parsePath}`, { method, headers });
- const data = await response.json().catch(() => ({}));
- if (!response.ok || data.error) {
- const error = new Error(data.error || data.message || `Parse ${method} ${parsePath} failed`);
- error.status = response.status || 500;
- error.detail = data;
- if (attempt < 4 && isRetryableError(error)) {
- await sleep(500 * attempt);
- continue;
- }
- throw error;
- }
- return data;
- } catch (error) {
- lastError = error;
- if (attempt < 4 && isRetryableError(error)) {
- await sleep(500 * attempt);
- continue;
- }
- throw error;
- }
- }
- throw lastError || new Error(`Parse ${method} ${parsePath} failed`);
- }
- function parseRequestedClasses(value) {
- return String(value || '')
- .split(',')
- .map((item) => item.trim())
- .filter(Boolean);
- }
- function readArg(name) {
- const index = process.argv.indexOf(name);
- return index >= 0 ? process.argv[index + 1] || '' : '';
- }
- function normalizeParseHost(value) {
- return String(value || 'https://server.fmode.cn').replace(/\/+$/, '').replace(/\/parse$/i, '');
- }
- function isRetryableError(error) {
- const status = Number(error?.status || 0);
- const message = `${error?.message || ''} ${error?.cause?.code || ''} ${error?.detail?.error || ''}`;
- return status >= 500 || /fetch failed|Failed to fetch|NetworkError|Load failed|ECONNRESET|ETIMEDOUT|EAI_AGAIN/i.test(message);
- }
- function clamp(value, min, max) {
- if (!Number.isFinite(value)) return min;
- return Math.max(min, Math.min(max, Math.floor(value)));
- }
- function sleep(ms) {
- return new Promise((resolve) => setTimeout(resolve, ms));
- }
- function safeTimestamp(value) {
- return String(value).replace(/[:.]/g, '-');
- }
|