meteor3: REST API async migration COMPLETE — smoke green on Meteor 3.1 + Mongo 7

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).
This commit is contained in:
vjrj 2026-07-14 06:21:35 +02:00
parent b37069c3e5
commit 637a749e8d
6 changed files with 124 additions and 117 deletions

View file

@ -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/<tok>/<mob>` 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');