From 1f1f5c697d05748a3345f0818852424cedafb5b1 Mon Sep 17 00:00:00 2001 From: vjrj Date: Tue, 14 Jul 2026 11:36:30 +0200 Subject: [PATCH] meteor3: port all 18 migration up() bodies to async percolate:migrations 2.0.1 awaits async up(), so historical migrations now run on Meteor 3: fetchAsync + for..of instead of cursor forEach, insert/update/remove -> *Async, _ensureIndex -> createIndexAsync, Accounts.createUser -> createUserAsync, awaited rawCollection indexes. Verified: empty Mongo 7 DB migrates 0->18 cleanly (indexes + industry registries created, control unlocked at v18); pre-marking version=18 is no longer needed (UPGRADE.md updated). REST smoke stays byte-identical. --- UPGRADE.md | 11 +- imports/startup/server/migrations.js | 163 ++++++++++++++------------- 2 files changed, 90 insertions(+), 84 deletions(-) diff --git a/UPGRADE.md b/UPGRADE.md index 5b9095e..baf1bd7 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -199,8 +199,8 @@ work lives on `meteor3-wip` until (a) production Mongo is migrated to 7 and ```bash docker run -d --name tcef-mongo7 -p 27019:27019 mongo:7 --replSet rs0 --port 27019 --bind_ip_all docker exec tcef-mongo7 mongosh --port 27019 --quiet --eval 'rs.initiate({_id:"rs0",members:[{_id:0,host:"localhost:27019"}]})' -# mark migrations current so historical (still-sync) up() bodies don't run: -docker exec tcef-mongo7 mongosh --port 27019 fuegos --quiet --eval 'db.migrations.replaceOne({_id:"control"},{_id:"control",version:18,locked:false},{upsert:true})' +# (migrations run from scratch on an empty DB — all up() bodies are async now, +# no need to pre-mark db.migrations at version 18) export MONGO_URL="mongodb://localhost:27019/fuegos?replicaSet=rs0" export NODE_OPTIONS="--dns-result-order=ipv4first --no-network-family-autoselection" # reach warehouse.meteor.com on Node 22 meteor --settings settings-development.json --port 3100 @@ -272,8 +272,11 @@ MONGO_CONTAINER=tcef-mongo7 MONGO_SHELL=mongosh MONGO_PORT=27019 ./smoke/smoke.s - Map/server publications not exercised by the REST smoke converted to async: `activefiresmyloc`, `activefiresunionmyloc`, `fireAlerts` (countAsync/fetchAsync/awaited firesUnion), and `oauth.verifyConfiguration` - (findOneAsync). `migrations.js` bodies stay sync **by design** (control is - pinned at v18; prod data arrives pre-migrated) — tracked as debt. + (findOneAsync). `migrations.js`: all 18 historical `up()` bodies ported to + async (`fetchAsync`+`for..of`, `*Async` writes, `createIndexAsync`, + `Accounts.createUserAsync`; percolate:migrations 2.0.1 awaits async `up()`). + Verified: empty Mongo 7 DB migrates 0→18 with no errors (indexes + + industry registries created); pre-marking `version=18` is no longer needed. Verified in a real browser: `/` and `/fires` render (map, search, layers, cookie banner), zero uncaught exceptions; REST smoke byte-identical. - ✅ **React 16 → 18.3** — `react`/`react-dom` bumped to `^18.3.1` diff --git a/imports/startup/server/migrations.js b/imports/startup/server/migrations.js index 92301ad..e67bad2 100644 --- a/imports/startup/server/migrations.js +++ b/imports/startup/server/migrations.js @@ -1,5 +1,6 @@ /* global Migrations */ /* eslint-disable import/no-absolute-path */ +/* eslint-disable no-await-in-loop, no-restricted-syntax */ import { Meteor } from 'meteor/meteor'; import Comments from '/imports/api/Comments/Comments'; import { Accounts } from 'meteor/accounts-base'; @@ -24,38 +25,40 @@ Meteor.startup(async () => { Migrations.add({ version: 1, - up: function migrateIds() { + up: async function migrateIds() { // https://docs.mongodb.com/manual/reference/operator/query/type/ - Meteor.users.find({ _id: { $type: 7 } }).forEach((user) => { + const users = await Meteor.users.find({ _id: { $type: 7 } }).fetchAsync(); + for (const user of users) { const migratedUser = user; const id = user._id.valueOf(); console.log(`Migrating id of user: ${JSON.stringify(user)}`); - Meteor.users.remove({ _id: user._id }); + await Meteor.users.removeAsync({ _id: user._id }); migratedUser._id = id; - Meteor.users.insert(migratedUser); - }); + await Meteor.users.insertAsync(migratedUser); + } } }); Migrations.add({ version: 2, - up: function migrateSubsForeignKey() { - UserSubsToFiresCollection.find({ owner: null }).forEach((sub) => { + up: async function migrateSubsForeignKey() { + const subs = await UserSubsToFiresCollection.find({ owner: null }).fetchAsync(); + for (const sub of subs) { console.log(`Migrating subs of chatId: ${sub.chatId}`); - const subsUser = Meteor.users.findOne({ telegramChatId: sub.chatId }); + const subsUser = await Meteor.users.findOneAsync({ telegramChatId: sub.chatId }); if (subsUser) { console.log(`Migrating linking to user: ${JSON.stringify(subsUser)}`); - UserSubsToFiresCollection.update({ _id: sub._id }, { $set: { owner: subsUser._id } }); + await UserSubsToFiresCollection.updateAsync({ _id: sub._id }, { $set: { owner: subsUser._id } }); } else { // create user with chatId and def language const username = `tel${sub.chatId.toString().replace(/^-/, '')}`; console.log(`Linking to new user: ${username}`); - const newUserId = Accounts.createUser({ + const newUserId = await Accounts.createUserAsync({ username, password: randomHex(50), profile: { name: {} } }); - Meteor.users.update({ _id: newUserId }, { + await Meteor.users.updateAsync({ _id: newUserId }, { $set: { emails: [], roles: ['user'], @@ -63,87 +66,90 @@ Meteor.startup(async () => { lang: 'es' } }); - UserSubsToFiresCollection.update({ _id: sub._id }, { $set: { owner: newUserId } }); + await UserSubsToFiresCollection.updateAsync({ _id: sub._id }, { $set: { owner: newUserId } }); } - }); + } } }); Migrations.add({ version: 3, - up: function emptySubsTypes() { - UserSubsToFiresCollection.find({ type: null }).forEach((sub) => { - UserSubsToFiresCollection.update({ _id: sub._id }, { $set: { type: 'telegram' } }); - }); + up: async function emptySubsTypes() { + const subs = await UserSubsToFiresCollection.find({ type: null }).fetchAsync(); + for (const sub of subs) { + await UserSubsToFiresCollection.updateAsync({ _id: sub._id }, { $set: { type: 'telegram' } }); + } } }); Migrations.add({ version: 4, - up: function deleteOldAlertFiresAndIndexes() { - FireAlertsCollection.remove({ createdAd: null }); + up: async function deleteOldAlertFiresAndIndexes() { + await FireAlertsCollection.removeAsync({ createdAd: null }); const raw = FireAlertsCollection.rawCollection(); - raw.createIndex({ ourid: '2dsphere' }); - raw.createIndex({ when: 1 }); - raw.createIndex({ updatedAt: 1 }); - raw.createIndex({ createdAt: 1 }); - raw.createIndex({ ourid: 1, type: 1 }); + await raw.createIndex({ ourid: '2dsphere' }); + await raw.createIndex({ when: 1 }); + await raw.createIndex({ updatedAt: 1 }); + await raw.createIndex({ createdAt: 1 }); + await raw.createIndex({ ourid: 1, type: 1 }); } }); Migrations.add({ version: 5, - up: function siteSettingsIndex() { + up: async function siteSettingsIndex() { // other way: - SiteSettings._ensureIndex({ name: 1 }, { unique: 1 }); + await SiteSettings.createIndexAsync({ name: 1 }, { unique: true }); } }); Migrations.add({ version: 6, - up: function falsePositiveIndexes() { - FalsePositives._ensureIndex({ chatId: 1 }); - FalsePositives._ensureIndex({ owner: 1 }); - FalsePositives._ensureIndex({ fireId: 1 }); - FalsePositives._ensureIndex({ type: 1 }); - FalsePositives._ensureIndex({ geo: '2dsphere' }); + up: async function falsePositiveIndexes() { + await FalsePositives.createIndexAsync({ chatId: 1 }); + await FalsePositives.createIndexAsync({ owner: 1 }); + await FalsePositives.createIndexAsync({ fireId: 1 }); + await FalsePositives.createIndexAsync({ type: 1 }); + await FalsePositives.createIndexAsync({ geo: '2dsphere' }); } }); Migrations.add({ version: 7, - up: function defLangIfNull() { - Meteor.users.find({ lang: null }).forEach((user) => { - Meteor.users.update({ _id: user._id }, { + up: async function defLangIfNull() { + const users = await Meteor.users.find({ lang: null }).fetchAsync(); + for (const user of users) { + await Meteor.users.updateAsync({ _id: user._id }, { $set: { lang: 'es' } }); - }); + } } }); Migrations.add({ version: 8, - up: function siteSettingsAddIndex() { - SiteSettings._ensureIndex({ isPublic: 1 }); - SiteSettings.find({ isPublic: null }).forEach((setting) => { - SiteSettings.update({ _id: setting._id }, { $set: { isPublic: true } }); - }); + up: async function siteSettingsAddIndex() { + await SiteSettings.createIndexAsync({ isPublic: 1 }); + const settings = await SiteSettings.find({ isPublic: null }).fetchAsync(); + for (const setting of settings) { + await SiteSettings.updateAsync({ _id: setting._id }, { $set: { isPublic: true } }); + } } }); Migrations.add({ version: 9, - up: function siteSettingsAddIndex() { - Industries._ensureIndex({ registry: 1 }); - Industries._ensureIndex({ geo: '2dsphere' }); + up: async function industriesIndexesAndRegistries() { + await Industries.createIndexAsync({ registry: 1 }); + await Industries.createIndexAsync({ geo: '2dsphere' }); // https://www.eea.europa.eu/data-and-maps/data/member-states-reporting-art-7-under-the-european-pollutant-release-and-transfer-register-e-prtr-regulation-16 - IndustryRegistries.insert({ + await IndustryRegistries.insertAsync({ _id: '1', name: 'E-PRTR', agency: 'EEA', region: 'EU' }); // https://www.epa.gov/enviro/epa-frs-facilities-state-single-file-csv-download - IndustryRegistries.insert({ + await IndustryRegistries.insertAsync({ _id: '2', name: 'FRS', agency: 'EPA', region: 'US' }); } @@ -151,11 +157,11 @@ Meteor.startup(async () => { Migrations.add({ version: 10, - up: function siteSettingsAddIndex() { - IndustryRegistries.insert({ + up: async function moreIndustryRegistries() { + await IndustryRegistries.insertAsync({ _id: '3', name: 'NPRI', agency: 'GCODP', region: 'Canada' }); - IndustryRegistries.insert({ + await IndustryRegistries.insertAsync({ _id: '4', name: 'NPI', agency: 'DEE', region: 'Australia' }); } @@ -163,15 +169,15 @@ Meteor.startup(async () => { Migrations.add({ version: 11, - up: function noAnonComments() { - Comments.remove({ isAnonymous: true }); + up: async function noAnonComments() { + await Comments.removeAsync({ isAnonymous: true }); } }); Migrations.add({ version: 12, - up: function setTelegramUsersBotId() { - Meteor.users.update({ telegramChatId: { $ne: null }, telegramBot: null }, { + up: async function setTelegramUsersBotId() { + await Meteor.users.updateAsync({ telegramChatId: { $ne: null }, telegramBot: null }, { $set: { telegramBot: 'es' } @@ -181,8 +187,8 @@ Meteor.startup(async () => { Migrations.add({ version: 13, - up: function removeTelegramBotFromUsersId() { - Meteor.users.update({}, { + up: async function removeTelegramBotFromUsersId() { + await Meteor.users.updateAsync({}, { $unset: { telegramBot: '' } @@ -192,8 +198,8 @@ Meteor.startup(async () => { Migrations.add({ version: 14, - up: function setTelegramUsersBotId() { - UserSubsToFiresCollection.update({ chatId: { $ne: null }, telegramBot: null }, { + up: async function setTelegramSubsBotId() { + await UserSubsToFiresCollection.updateAsync({ chatId: { $ne: null }, telegramBot: null }, { $set: { telegramBot: 'es' } @@ -203,40 +209,41 @@ Meteor.startup(async () => { Migrations.add({ version: 15, - up: function moveToFalsePositivesUppercase() { + up: async function moveToFalsePositivesUppercase() { /* const falsepositiveslower = new Mongo.Collection('falsepositives', { idGeneration: 'MONGO' }); * falsepositiveslower.find({}).forEach((falseDoc) => { * FalsePositives.insert(falseDoc); - * });*/ + * }); */ // TODO remove falsepositives lowercase collection } }); Migrations.add({ version: 16, - up: function moveToFalsePositivesUppercaseWithUser() { + up: async function moveToFalsePositivesUppercaseWithUser() { const falsepositiveslower = new Mongo.Collection('falsepositives', { idGeneration: 'MONGO' }); const now = new Date(); - falsepositiveslower.find({}).forEach((falseDoc) => { - const user = Meteor.users.findOne({ telegramChatId: falseDoc.chatId }); + const falseDocs = await falsepositiveslower.find({}).fetchAsync(); + for (const falseDoc of falseDocs) { + const user = await Meteor.users.findOneAsync({ telegramChatId: falseDoc.chatId }); if (user) { - falseDoc.owner= user._id; + falseDoc.owner = user._id; } falseDoc.type = 'industry'; falseDoc.createdAt = now; falseDoc.updatedAt = now; - FalsePositives.insert(falseDoc); - }); + await FalsePositives.insertAsync(falseDoc); + } } }); Migrations.add({ version: 17, - up: function renameWebNotifiedField() { - Notifications.update({ webNotified: { $exists: true } }, { + up: async function renameWebNotifiedField() { + await Notifications.updateAsync({ webNotified: { $exists: true } }, { $rename: { webNotifiedAt: 'notifiedAt' } }, { upsert: false, multi: true }); - Notifications.update({ webNotified: { $exists: true } }, { + await Notifications.updateAsync({ webNotified: { $exists: true } }, { $rename: { webNotified: 'notified' } }, { upsert: false, multi: true }); } @@ -244,22 +251,18 @@ Meteor.startup(async () => { Migrations.add({ version: 18, - up: () => { + up: async () => { const raw = ActiveFiresUnion.rawCollection(); - raw.createIndex({ centerid: '2dsphere' }); - raw.createIndex({ shape: '2dsphere' }); - raw.createIndex({ when: 1 }); - raw.createIndex({ createdAt: 1 }); - raw.createIndex({ updatedAt: 1 }); + await raw.createIndex({ centerid: '2dsphere' }); + await raw.createIndex({ shape: '2dsphere' }); + await raw.createIndex({ when: 1 }); + await raw.createIndex({ createdAt: 1 }); + await raw.createIndex({ updatedAt: 1 }); } }); - // Set createdAt in users & subs - // Meteor 3: migrateTo is async now. - // NOTE: the individual up() bodies below still use SYNC Mongo — they only run - // on a DB whose migration version is behind. Production data is restored from - // rsmain already at the latest version, so they never execute. If migrations - // must ever run on a truly-fresh DB, their up() bodies need async conversion. + // Meteor 3: migrateTo is async and awaits each async up(), so a fresh DB can + // migrate from 0 to latest without pre-marking the control version. await Migrations.migrateTo('latest'); // Migrations.migrateTo('14,rerun');