#!/usr/bin/env node 'use strict'; /** * Migration: backfill App.avgSpraySpeed for all existing applications. * * Uses a cursor over App documents to avoid loading everything into memory. * Safe to run on production — read-only on AppDetail, bulkWrite on App. * Re-entrant: apps that already have avgSpraySpeed are skipped. * * Usage: * node scripts/migrate_avg_spray_speed.js * node scripts/migrate_avg_spray_speed.js --dry-run */ 'use strict'; const debug = require('debug')('agm:migrate-avg-spray-speed'); const { DBConnection } = require('../helpers/db/connect.js'); const { App, AppFile, AppDetail } = require('../model/index.js'); const utils = require('../helpers/utils.js'); const DRY_RUN = process.argv.includes('--dry-run'); const BATCH_SIZE = 50; // Apps per bulkWrite batch const PROGRESS_EVERY = 100; // Log every N apps async function computeAvgSpraySpeed(appId) { const files = await AppFile.find({ appId, markedDelete: { $ne: true } }, '_id').lean(); if (!files.length) return null; const fileIds = files.map(f => f._id); let speedAcc = 0, count = 0; const cursor = AppDetail.find( { fileId: { $in: fileIds }, sprayStat: { $in: [1, 2] } }, { grSpeed: 1 }, { lean: true } ).cursor(); for await (const record of cursor) { if (utils.isNumber(record.grSpeed)) { speedAcc += record.grSpeed; count++; } } return count > 0 ? speedAcc / count : null; } async function migrate() { debug(`Starting${DRY_RUN ? ' (DRY RUN)' : ''}...`); const total = await App.countDocuments({ avgSpraySpeed: { $exists: false }, status: 3 }); debug(`Apps to process: ${total}`); if (!total) { debug('Nothing to do.'); return; } let processed = 0, updated = 0, skipped = 0, errors = 0; let bulk = []; const appCursor = App.find( { avgSpraySpeed: { $exists: false }, status: 3 }, '_id' ).sort({ _id: 1 }).lean().cursor(); for await (const app of appCursor) { try { const avgSpeed = await computeAvgSpraySpeed(app._id); if (avgSpeed !== null) { bulk.push({ updateOne: { filter: { _id: app._id }, update: { $set: { avgSpraySpeed: avgSpeed } } } }); updated++; } else { skipped++; } } catch (err) { errors++; debug(`Error on App ${app._id}: ${err.message}`); } processed++; if (processed % PROGRESS_EVERY === 0) { debug(`Progress: ${processed}/${total} (updated=${updated}, skipped=${skipped}, errors=${errors})`); } if (bulk.length >= BATCH_SIZE) { if (!DRY_RUN) await App.bulkWrite(bulk, { ordered: false }); bulk = []; } } if (bulk.length && !DRY_RUN) { await App.bulkWrite(bulk, { ordered: false }); } debug(`Done. processed=${processed}, updated=${updated}, skipped=${skipped}, errors=${errors}${DRY_RUN ? ' (DRY RUN — no writes)' : ''}`); } const workerDB = new DBConnection('Migrate avgSpraySpeed'); workerDB.initialize({ setupExitHandlers: false, onReady: async () => { try { await migrate(); process.exit(0); } catch (err) { debug('Migration failed:', err); process.exit(1); } } });