From 637a749e8d858d440f109962d6110e0138a3ac8d Mon Sep 17 00:00:00 2001 From: vjrj Date: Tue, 14 Jul 2026 06:21:35 +0200 Subject: [PATCH] =?UTF-8?q?meteor3:=20REST=20API=20async=20migration=20COM?= =?UTF-8?q?PLETE=20=E2=80=94=20smoke=20green=20on=20Meteor=203.1=20+=20Mon?= =?UTF-8?q?go=207?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All 12 Flutter REST endpoints byte-identical to the 1.6.1.1 baseline, running on Meteor 3.1 against dockerized MongoDB 7. The 3.x jump works end-to-end. - Rest.js: every endpoint action -> async; all Mongo calls -> *Async (findOneAsync/countAsync/fetchAsync/upsertAsync/removeAsync). - Helpers to async: countRealFires (accepts array|cursor, forEachAsync->for-of), firesUnion (fetchAsync), fireFromHash/findOrCreateFire (await, dropped Fibers Promise.await), subscriptionsInsert/subscriptionsRemove, upsertFalsePositive, falsePositives.insert, subscriptions.update method, countFiresInRegions. - getFires: countAsync() on a $near cursor throws on the mongodb 6 driver ($near not allowed in the aggregation countDocuments uses) -> fetch once and use array length for total. - Route order: json-routes 3.0 matches in registration order, so mobile/subscriptions/all/:token/:mobileToken is now registered BEFORE :token/:mobileToken/:subsId (else GET .../all/x/y hit the delete-only route). --- imports/api/ActiveFires/server/countFires.js | 23 +-- imports/api/FalsePositives/methods.js | 10 +- .../api/FalsePositives/server/publications.js | 4 +- imports/api/Fires/server/publications.js | 17 +- imports/api/Rest/Rest.js | 173 +++++++++--------- imports/api/Subscriptions/methods.js | 14 +- 6 files changed, 124 insertions(+), 117 deletions(-) diff --git a/imports/api/ActiveFires/server/countFires.js b/imports/api/ActiveFires/server/countFires.js index b3b8511..116ae5b 100644 --- a/imports/api/ActiveFires/server/countFires.js +++ b/imports/api/ActiveFires/server/countFires.js @@ -29,40 +29,41 @@ const findFiresInRegion = (zone) => { return result; }; -export const countRealFires = (firesCursor) => { +export const countRealFires = async (firesCursor) => { const realFires = []; - firesCursor.forEach((fire) => { + const fires = Array.isArray(firesCursor) ? firesCursor : await firesCursor.fetchAsync(); + for (const fire of fires) { if (debug) console.log(`${JSON.stringify(fire)} -----`); - const union = firesUnion([fire]); + const union = await firesUnion([fire]); if (debug) console.log(`${JSON.stringify(union)} -----`); const falsePos = whichAreFalsePositives(FalsePositives, union); const industries = whichAreFalsePositives(Industries, union); - if (falsePos.count() === 0 && industries.count() === 0) { + if (await falsePos.countAsync() === 0 && await industries.countAsync() === 0) { realFires.push(fire); } - }); + } // group fires - const realUnion = firesUnion(realFires); + const realUnion = await firesUnion(realFires); const unionCount = realUnion[0] && realUnion[0].geometry && realUnion[0].geometry.coordinates ? realUnion[0].geometry.coordinates.length : 0; return unionCount; }; -const countFiresInRegions = (regions, stringsToRemove) => { +const countFiresInRegions = async (regions, stringsToRemove) => { let total = 0; const fireStats = {}; - regions.features.forEach((region) => { + for (const region of regions.features) { const regionName = cleanProv(region.properties.name, stringsToRemove); const regionCode = region.properties.iso_a2; if (debug) console.log(`${regionName} -----`); try { const fires = findFiresInRegion(region); // TODO Also check neighbour fires (when better implementation of that part) - const initialCount = fires.count(); + const initialCount = await fires.countAsync(); if (initialCount > 0) { - const unionCount = countRealFires(fires); + const unionCount = await countRealFires(fires); if (debug) console.log(`${regionName} initial: ${initialCount}, union calc: ${unionCount}`); if (unionCount > 0) { total += unionCount; @@ -72,7 +73,7 @@ const countFiresInRegions = (regions, stringsToRemove) => { } catch (e) { ravenLogger.log(e); } - }); + } return { total, fires: fireStats }; }; diff --git a/imports/api/FalsePositives/methods.js b/imports/api/FalsePositives/methods.js index 85005ba..2caf2b9 100644 --- a/imports/api/FalsePositives/methods.js +++ b/imports/api/FalsePositives/methods.js @@ -6,7 +6,7 @@ import FalsePositiveTypes from './FalsePositiveTypes'; import Fires from '../Fires/Fires'; import rateLimit from '../../modules/rate-limit'; -export function upsertFalsePositive(keyType, owner, fire) { +export async function upsertFalsePositive(keyType, owner, fire) { const fireType = fire.type; if (fireType === 'vecinal') { throw new Meteor.Error('500', 'No se puede marcar este tipo de fuego'); @@ -22,7 +22,7 @@ export function upsertFalsePositive(keyType, owner, fire) { fireId: fire._id, geo: fire.ourid }; - return FalsePositives.upsert({ geo: fire.ourid, owner }, { $set: set }, { multi: false, upsert: true }); + return await FalsePositives.upsertAsync({ geo: fire.ourid, owner }, { $set: set }, { multi: false, upsert: true }); } catch (exception) { console.log(exception); throw new Meteor.Error('500', exception); @@ -30,7 +30,7 @@ export function upsertFalsePositive(keyType, owner, fire) { } Meteor.methods({ - 'falsePositives.insert': function falsePositivesInsert(fireId, keyType) { + 'falsePositives.insert': async function falsePositivesInsert(fireId, keyType) { check(fireId, Meteor.Collection.ObjectID); check(keyType, String); const type = FalsePositiveTypes[keyType]; @@ -39,10 +39,10 @@ Meteor.methods({ throw new Meteor.Error('500', 'Regístrate o inicia sesión para aportar información sobre este fuego'); } check(this.userId, String); - const fire = Fires.findOne(fireId); + const fire = await Fires.findOneAsync(fireId); const owner = this.userId; if (fire) { - upsertFalsePositive(keyType, owner, fire); + await upsertFalsePositive(keyType, owner, fire); } else { throw new Meteor.Error('500', 'Fuego no encontrado'); } diff --git a/imports/api/FalsePositives/server/publications.js b/imports/api/FalsePositives/server/publications.js index b6ca42b..df6377d 100644 --- a/imports/api/FalsePositives/server/publications.js +++ b/imports/api/FalsePositives/server/publications.js @@ -17,8 +17,8 @@ Meteor.publish('falsePositivesTotal', function total() { return counter; }); -export const firesUnion = (fires) => { - const firesArray = Array.isArray(fires) ? fires : fires.fetch(); // if not is a cursor +export const firesUnion = async (fires) => { + const firesArray = Array.isArray(fires) ? fires : await fires.fetchAsync(); // if not is a cursor const remap = firesArray.map(function remap(doc) { const isNASA = doc.type === 'modis' || doc.type === 'viirs'; const pixelSize = doc.type === 'viirs' ? 0.375 : 1; // viirs has 375m pixel size, modis 1000m diff --git a/imports/api/Fires/server/publications.js b/imports/api/Fires/server/publications.js index 3862beb..9641f6e 100644 --- a/imports/api/Fires/server/publications.js +++ b/imports/api/Fires/server/publications.js @@ -53,12 +53,12 @@ const fixConfidence = (obj) => { return obj; }; -const findOrCreateFire = (obj) => { +const findOrCreateFire = async (obj) => { const fire = findFire(obj); - // console.log(`Found: ${fire.count()}`); + // console.log(`Found: ${await fire.countAsync()}`); if (!obj.address) { try { - const rev = Promise.await(geocoder.reverse({ lat: obj.lat, lon: obj.lon })); + const rev = await geocoder.reverse({ lat: obj.lat, lon: obj.lon }); if (rev[0]) { obj.address = rev[0].formattedAddress; } @@ -67,10 +67,10 @@ const findOrCreateFire = (obj) => { console.warn(reve); } } - if (fire.count() === 0) { + if (await fire.countAsync() === 0) { // const result = // console.log('Creating new fire'); - FiresCollection.upsert({ ourid: obj.ourid, when: obj.when, type: obj.type }, { $set: obj }, { multi: false, upsert: true }); + await FiresCollection.upsertAsync({ ourid: obj.ourid, when: obj.when, type: obj.type }, { $set: obj }, { multi: false, upsert: true }); // console.log(JSON.stringify(result)); } return findFire(obj); @@ -139,13 +139,12 @@ function logUrl(fireEnc, params) { ravenLogger.log(message); } -export function fireFromHash(fireEnc, params) { +export async function fireFromHash(fireEnc, params) { check(fireEnc, String); check(params, Object); // console.log(fireEnc); - // const unsealed = Promise.await(urlEnc.decrypt(fireEnc)); - const unsealed = Promise.await(unsealW(fireEnc)); + const unsealed = await unsealW(fireEnc); if (unsealed === undefined) { throw Error(`Fail to unseal '${fireEnc}'`); } @@ -159,7 +158,7 @@ export function fireFromHash(fireEnc, params) { // FIXME: const unsealedFix = fixConfidence(unsealed); FiresCollection.schema.validate(unsealedFix); - return findOrCreateFire(unsealedFix); + return await findOrCreateFire(unsealedFix); /* console.log(`fires: ${fire.count()}`); * return fire; */ } diff --git a/imports/api/Rest/Rest.js b/imports/api/Rest/Rest.js index ae5e383..d7eb83f 100644 --- a/imports/api/Rest/Rest.js +++ b/imports/api/Rest/Rest.js @@ -89,8 +89,8 @@ if (!Meteor.settings.private.internalApiToken) { }, endpoints: { get: { - action: function getFire() { - return Fires.findOne(new Meteor.Collection.ObjectID(this.urlParams.id)); + action: async function getFire() { + return Fires.findOneAsync(new Meteor.Collection.ObjectID(this.urlParams.id)); } } } @@ -98,22 +98,22 @@ if (!Meteor.settings.private.internalApiToken) { // Maps to: /api/v1/status/last-fire-check apiV1.addRoute('status/last-fire-check', { authRequired: false }, { - get: function get() { - return SiteSettings.findOne({ name: 'last-fire-check' }); + get: async function get() { + return SiteSettings.findOneAsync({ name: 'last-fire-check' }); } }); // Maps to: /api/v1/status/last-fire-detected apiV1.addRoute('status/last-fire-detected', { authRequired: false }, { - get: function get() { - return ActiveFiresCollection.findOne({}, { sort: { when: -1 } }); + get: async function get() { + return ActiveFiresCollection.findOneAsync({}, { sort: { when: -1 } }); } }); // Maps to: /api/v1/status/active-fires-count apiV1.addRoute('status/active-fires-count', { authRequired: false }, { - get: function get() { - return { total: ActiveFiresCollection.find({}).count() }; + get: async function get() { + return { total: await ActiveFiresCollection.find({}).countAsync() }; } }); @@ -124,7 +124,7 @@ if (!Meteor.settings.private.internalApiToken) { } }); - function getFires(route, full) { + async function getFires(route, full) { const lat = Number(route.urlParams.lat); const lng = Number(route.urlParams.lng); const km = Number(route.urlParams.km); @@ -163,7 +163,11 @@ if (!Meteor.settings.private.internalApiToken) { } }); - const result = { total: fires.count(), real: countRealFires(fires) }; + // Meteor 3 / mongodb driver 6: countAsync() on a $near cursor fails + // ($near/$geoNear not allowed inside the aggregation countDocuments uses). + // Fetch once and derive the total from the array length instead. + const firesA = await fires.fetchAsync(); + const result = { total: firesA.length, real: await countRealFires(firesA) }; if (debug) console.log(`Query for fires in ${lat}, ${lng} in ${km} km radius ${result}`); if (!full) { @@ -172,19 +176,16 @@ if (!Meteor.settings.private.internalApiToken) { let union; - // TODO only get real - const firesA = fires.fetch(); - if (firesA.length > 0) { - union = firesUnion(fires); + union = await firesUnion(firesA); } else { union = zoneToUnion(lat, lng, km); } const falsePos = whichAreFalsePositives(FalsePositives, union); const industries = whichAreFalsePositives(Industries, union); result.fires = firesA; - result.industries = industries.fetch(); - result.falsePos = falsePos.fetch(); + result.industries = await industries.fetchAsync(); + result.falsePos = await falsePos.fetchAsync(); return result; } @@ -192,13 +193,13 @@ if (!Meteor.settings.private.internalApiToken) { // 100 km max // Ex: http://127.0.0.1:3000/api/v1/fires-in/token/38.736946/-9.142685/100 apiV1.addRoute('fires-in/:token/:lat/:lng/:km', { authRequired: false }, { - get: function get() { + get: async function get() { return getFires(this, false); } }); apiV1.addRoute('fires-in-full/:token/:lat/:lng/:km', { authRequired: false }, { - get: function get() { + get: async function get() { return getFires(this, true); } }); @@ -209,7 +210,7 @@ if (!Meteor.settings.private.internalApiToken) { // // https://docs.meteor.com/api/passwords.html#Accounts-createUser apiV1.addRoute('mobile/users', { authRequired: false }, { - post: function post() { + post: async function post() { const { token, mobileToken, lang } = this.bodyParams; try { check(token, String); @@ -225,22 +226,23 @@ if (!Meteor.settings.private.internalApiToken) { let username; const already = Meteor.users.find({ fireBaseToken: mobileToken }); + const alreadyCount = await already.countAsync(); - if (already.count() > 1) { + if (alreadyCount > 1) { return restivusError(500, 'Unexpected error in REST call: several users with that mobile token?'); - } else if (already.count() === 1) { - username = already.fetch()[0].username; + } else if (alreadyCount === 1) { + username = (await already.fetchAsync())[0].username; } else { do { username = Random.id(15); - } while (Meteor.users.find({ username }).count() !== 0); + } while (await Meteor.users.find({ username }).countAsync() !== 0); } // FIXME check valid lang const now = new Date(); - const result = Meteor.users.upsert({ + const result = await Meteor.users.upsertAsync({ fireBaseToken: mobileToken }, { $set: { @@ -260,7 +262,7 @@ if (!Meteor.settings.private.internalApiToken) { console.log(this.queryParams); } - const upsertUser = Meteor.users.findOne({ username }); + const upsertUser = await Meteor.users.findOneAsync({ username }); if (debug) console.log(upsertUser); @@ -277,7 +279,7 @@ if (!Meteor.settings.private.internalApiToken) { // max sitance: 100km apiV1.addRoute('mobile/subscriptions', { authRequired: false }, { - post: function post() { + post: async function post() { const { token, mobileToken, @@ -305,7 +307,7 @@ if (!Meteor.settings.private.internalApiToken) { const failed = checkAuthToken(token); if (failed) return failed; - const user = Meteor.users.findOne({ fireBaseToken: mobileToken }); + const user = await Meteor.users.findOneAsync({ fireBaseToken: mobileToken }); if (!user) return failMsg('User not found'); const newSubs = {}; @@ -317,7 +319,7 @@ if (!Meteor.settings.private.internalApiToken) { let result; try { - result = subscriptionsInsert(newSubs, user._id, 'mobile'); + result = await subscriptionsInsert(newSubs, user._id, 'mobile'); } catch (e) { return fail(e); } @@ -325,8 +327,57 @@ if (!Meteor.settings.private.internalApiToken) { } }); + // NOTE: json-routes 3.0 (Meteor 3) matches routes in registration order, so + // the literal `all/:token/:mobileToken` route MUST be registered before the + // `:token/:mobileToken/:subsId` route — otherwise a GET to + // `.../all//` matches the 3-param route (delete-only) and restivus + // replies "API endpoint does not exist". + apiV1.addRoute('mobile/subscriptions/all/:token/:mobileToken', { authRequired: false }, { + get: async function get() { + const { token, mobileToken } = this.urlParams; + try { + check(token, String); + check(mobileToken, String); + } catch (e) { + return defaultFailParams(e); + } + + const failed = checkAuthToken(token); + if (failed) return failed; + + const user = await Meteor.users.findOneAsync({ fireBaseToken: mobileToken }); + if (!user) return failMsg('User not found'); + + const result = Subscriptions.find({ owner: user._id }); + + return jsend.success({ subscriptions: await result.fetchAsync(), count: await result.countAsync() }); + }, + delete: async function delAll() { + const { token, mobileToken } = this.urlParams; + try { + check(token, String); + check(mobileToken, String); + } catch (e) { + return defaultFailParams(e); + } + + const failed = checkAuthToken(token); + if (failed) return failed('Auth api check failed'); + + if (await Meteor.users.find({ fireBaseToken: mobileToken }).countAsync() !== 1) return fail; + + const user = await Meteor.users.findOneAsync({ fireBaseToken: mobileToken }); + if (!user) return failMsg('User not found'); + + const toRemove = await Subscriptions.find({ owner: user._id }).countAsync(); + await Subscriptions.removeAsync({ owner: user._id }); + + return jsend.success({ count: toRemove }); + } + }); + apiV1.addRoute('mobile/subscriptions/:token/:mobileToken/:subsId', { authRequired: false }, { - delete: function del() { + delete: async function del() { const { token, mobileToken, @@ -343,11 +394,11 @@ if (!Meteor.settings.private.internalApiToken) { const failed = checkAuthToken(token); if (failed) return failed; - const user = Meteor.users.findOne({ fireBaseToken: mobileToken }); + const user = await Meteor.users.findOneAsync({ fireBaseToken: mobileToken }); if (!user) return failMsg('User not found'); try { - Subscriptions.remove({ owner: user._id, _id: new Meteor.Collection.ObjectID(subsId) }); + await Subscriptions.removeAsync({ owner: user._id, _id: new Meteor.Collection.ObjectID(subsId) }); } catch (e) { return fail(e); } @@ -356,52 +407,8 @@ if (!Meteor.settings.private.internalApiToken) { } }); - apiV1.addRoute('mobile/subscriptions/all/:token/:mobileToken', { authRequired: false }, { - get: function get() { - const { token, mobileToken } = this.urlParams; - try { - check(token, String); - check(mobileToken, String); - } catch (e) { - return defaultFailParams(e); - } - - const failed = checkAuthToken(token); - if (failed) return failed; - - const user = Meteor.users.findOne({ fireBaseToken: mobileToken }); - if (!user) return failMsg('User not found'); - - const result = Subscriptions.find({ owner: user._id }); - - return jsend.success({ subscriptions: result.fetch(), count: result.count() }); - }, - delete: function delAll() { - const { token, mobileToken } = this.urlParams; - try { - check(token, String); - check(mobileToken, String); - } catch (e) { - return defaultFailParams(e); - } - - const failed = checkAuthToken(token); - if (failed) return failed('Auth api check failed'); - - if (Meteor.users.find({ fireBaseToken: mobileToken }).count() !== 1) return fail; - - const user = Meteor.users.findOne({ fireBaseToken: mobileToken }); - if (!user) return failMsg('User not found'); - - const toRemove = Subscriptions.find({ owner: user._id }).count(); - Subscriptions.remove({ owner: user._id }); - - return jsend.success({ count: toRemove }); - } - }); - apiV1.addRoute('status/subs-public-union/:token', { authRequired: false }, { - get: function get() { + get: async function get() { const { token } = this.urlParams; try { check(token, String); @@ -412,15 +419,15 @@ if (!Meteor.settings.private.internalApiToken) { const failed = checkAuthToken(token); if (failed) return failed; - const currentUnion = SiteSettings.findOne({ name: 'subs-public-union' }); - const userSubsBounds = SiteSettings.findOne({ name: 'subs-public-union-bounds' }); + const currentUnion = await SiteSettings.findOneAsync({ name: 'subs-public-union' }); + const userSubsBounds = await SiteSettings.findOneAsync({ name: 'subs-public-union-bounds' }); return jsend.success({ union: currentUnion, bounds: userSubsBounds }); } }); apiV1.addRoute('mobile/falsepositive', { authRequired: false }, { - post: function post() { + post: async function post() { const { token, mobileToken, @@ -440,15 +447,15 @@ if (!Meteor.settings.private.internalApiToken) { const failed = checkAuthToken(token); if (failed) return failed; - const user = Meteor.users.findOne({ fireBaseToken: mobileToken }); + const user = await Meteor.users.findOneAsync({ fireBaseToken: mobileToken }); if (!user) return failMsg('User not found'); console.log(`Marking hash fire as '${type}' false positive: ${sealed}`); - const fireC = fireFromHash(sealed, { id: sealed }); + const fireC = await fireFromHash(sealed, { id: sealed }); if (fireC && typeof fireC === 'object') { - const fire = fireC.fetch()[0]; + const fire = (await fireC.fetchAsync())[0]; console.log(`Marking fire as false positive: ${fire._id}`); - const result = upsertFalsePositive(type, user._id, fire); + const result = await upsertFalsePositive(type, user._id, fire); return jsend.success({ upsert: result }); } return failMsg('Cannot mark fire as false positive'); diff --git a/imports/api/Subscriptions/methods.js b/imports/api/Subscriptions/methods.js index cf927e0..63eaa17 100644 --- a/imports/api/Subscriptions/methods.js +++ b/imports/api/Subscriptions/methods.js @@ -10,7 +10,7 @@ function geo(doc) { }; } -export function subscriptionsInsert(doc, userId, type) { +export async function subscriptionsInsert(doc, userId, type) { check(doc, { _id: Match.Maybe(Meteor.Collection.ObjectID), location: Match.ObjectIncluding({ lat: Number, lon: Number }), @@ -23,23 +23,23 @@ export function subscriptionsInsert(doc, userId, type) { ...doc }; // console.log(newDoc); - const already = Subscriptions.findOne(newDoc); + const already = await Subscriptions.findOneAsync(newDoc); if (already) { throw new Meteor.Error('on-already-subscribed', 'The user is already subscribed to this area'); } try { - return Subscriptions.insert(newDoc); + return await Subscriptions.insertAsync(newDoc); } catch (exception) { // console.error(exception); throw new Meteor.Error('500', exception); } } -export function subscriptionsRemove(subscriptionId) { +export async function subscriptionsRemove(subscriptionId) { check(subscriptionId, Meteor.Collection.ObjectID); try { - return Subscriptions.remove(subscriptionId); + return await Subscriptions.removeAsync(subscriptionId); } catch (exception) { console.error(exception); throw new Meteor.Error('500', exception); @@ -54,7 +54,7 @@ Meteor.methods({ }); return subscriptionsInsert(doc, this.userId, 'web'); }, - 'subscriptions.update': function subscriptionsUpdate(doc) { + 'subscriptions.update': async function subscriptionsUpdate(doc) { check(doc, { _id: Meteor.Collection.ObjectID, location: Match.ObjectIncluding({ lat: Number, lon: Number }), @@ -65,7 +65,7 @@ Meteor.methods({ const dup = doc; const subscriptionId = doc._id; dup.geo = geo(doc); - Subscriptions.update(subscriptionId, { $set: dup }); + await Subscriptions.updateAsync(subscriptionId, { $set: dup }); return subscriptionId; // Return _id so we can redirect to subscription after update. } catch (exception) { throw new Meteor.Error('500', exception);