From 7f3c8a281d279adb0f8749a92a402f5b150022ad Mon Sep 17 00:00:00 2001 From: Jimmy Domagala-Tang Date: Wed, 14 Jan 2026 11:55:35 -0500 Subject: [PATCH] Async await entity providers (#28841) * fix: async await entity providers * fix: async await userMembershipGetters * fix: move to export class style for UserMembershipsHandler * fix: update how Publisher model returns its v1 values GitOrigin-RevId: f94bcb81c9703deaf938cdf7dff31c6e24dc4bd8 --- .../Institutions/InstitutionsGetter.mjs | 57 ++++++------ .../UserMembership/UserMembershipsHandler.mjs | 88 ++++++------------- services/web/app/src/models/Institution.mjs | 26 +++--- services/web/app/src/models/Publisher.mjs | 56 ++++++------ services/web/app/src/models/Subscription.mjs | 10 ++- 5 files changed, 100 insertions(+), 137 deletions(-) diff --git a/services/web/app/src/Features/Institutions/InstitutionsGetter.mjs b/services/web/app/src/Features/Institutions/InstitutionsGetter.mjs index 3467d1f2fa..ddc88f093d 100644 --- a/services/web/app/src/Features/Institutions/InstitutionsGetter.mjs +++ b/services/web/app/src/Features/Institutions/InstitutionsGetter.mjs @@ -1,4 +1,4 @@ -import { promisify, callbackify } from 'node:util' +import { callbackify } from 'node:util' import UserGetter from '../User/UserGetter.mjs' import UserMembershipsHandler from '../UserMembership/UserMembershipsHandler.mjs' import UserMembershipEntityConfigs from '../UserMembership/UserMembershipEntityConfigs.mjs' @@ -55,39 +55,36 @@ async function getCurrentInstitutionsWithLicence(userId) { return Object.values(institutions) } +async function getConfirmedAffiliations(userId) { + const emailsData = await UserGetter.promises.getUserFullEmails(userId) + + const confirmedAffiliations = emailsData + .filter( + emailData => + emailData.confirmedAt && + emailData.affiliation && + emailData.affiliation.institution && + emailData.affiliation.institution.confirmed + ) + .map(emailData => emailData.affiliation) + + return confirmedAffiliations +} + +async function getManagedInstitutions(userId) { + return await UserMembershipsHandler.promises.getEntitiesByUser( + UserMembershipEntityConfigs.institution, + userId + ) +} + const InstitutionsGetter = { - getConfirmedAffiliations(userId, callback) { - UserGetter.getUserFullEmails(userId, function (error, emailsData) { - if (error) { - return callback(error) - } - - const confirmedAffiliations = emailsData - .filter( - emailData => - emailData.confirmedAt && - emailData.affiliation && - emailData.affiliation.institution && - emailData.affiliation.institution.confirmed - ) - .map(emailData => emailData.affiliation) - - callback(null, confirmedAffiliations) - }) - }, - + getConfirmedAffiliations: callbackify(getConfirmedAffiliations), getCurrentInstitutionIds: callbackify(getCurrentInstitutionIds), getCurrentInstitutionsWithLicence: callbackify( getCurrentInstitutionsWithLicence ), - - getManagedInstitutions(userId, callback) { - UserMembershipsHandler.getEntitiesByUser( - UserMembershipEntityConfigs.institution, - userId, - callback - ) - }, + getManagedInstitutions: callbackify(getManagedInstitutions), } InstitutionsGetter.promises = { @@ -95,7 +92,7 @@ InstitutionsGetter.promises = { getCurrentInstitutionIds, getCurrentInstitutionsWithLicence, getCurrentAndPastAffiliationIds, - getManagedInstitutions: promisify(InstitutionsGetter.getManagedInstitutions), + getManagedInstitutions, } export default InstitutionsGetter diff --git a/services/web/app/src/Features/UserMembership/UserMembershipsHandler.mjs b/services/web/app/src/Features/UserMembership/UserMembershipsHandler.mjs index 8d9643bd7e..b9b3e552ea 100644 --- a/services/web/app/src/Features/UserMembership/UserMembershipsHandler.mjs +++ b/services/web/app/src/Features/UserMembership/UserMembershipsHandler.mjs @@ -1,17 +1,4 @@ -/* eslint-disable - n/handle-callback-err, - max-len, -*/ -// TODO: This file was created by bulk-decaffeinate. -// Fix any style issues and re-enable lint. -/* - * decaffeinate suggestions: - * DS207: Consider shorter variations of null checks - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ -import async from 'async' - -import { promisifyAll } from '@overleaf/promise-utils' +import { callbackifyAll } from '@overleaf/promise-utils' import UserMembershipEntityConfigs from './UserMembershipEntityConfigs.mjs' import * as InstitutionModel from '../../models/Institution.mjs' import * as SubscriptionModel from '../../models/Subscription.mjs' @@ -25,11 +12,25 @@ const EntityModels = { } const UserMembershipsHandler = { - removeUserFromAllEntities(userId, callback) { - // get all writable entity types - if (callback == null) { - callback = function () {} + async getEntitiesByUser(entityConfig, userId) { + if (!Features.hasFeature('saas')) { + return [] } + const query = entityConfig.baseQuery || {} + query[entityConfig.fields.access] = userId + + const entities = + (await EntityModels[entityConfig.modelName].find(query)) || [] + + const filledEntities = [] + for (const entity of entities) { + const filled = await entity.fetchV1DataPromise() + filledEntities.push(filled) + } + return filledEntities + }, + + async removeUserFromAllEntities(userId) { const entityConfigs = [] for (const key in UserMembershipEntityConfigs) { const entityConfig = UserMembershipEntityConfigs[key] @@ -39,52 +40,19 @@ const UserMembershipsHandler = { } // remove the user from all entities types - async.map( - entityConfigs, - (entityConfig, innerCallback) => - UserMembershipsHandler.removeUserFromEntities( - entityConfig, - userId, - innerCallback - ), - callback - ) + for (const entityConfig of entityConfigs) { + await UserMembershipsHandler.removeUserFromEntities(entityConfig, userId) + } }, - removeUserFromEntities(entityConfig, userId, callback) { - if (callback == null) { - callback = function () {} - } + async removeUserFromEntities(entityConfig, userId) { const removeOperation = { $pull: {} } removeOperation.$pull[entityConfig.fields.write] = userId - EntityModels[entityConfig.modelName] - .updateMany({}, removeOperation) - .then(result => callback(null, result)) - .catch(callback) - }, - - getEntitiesByUser(entityConfig, userId, callback) { - if (callback == null) { - callback = function () {} - } - if (!Features.hasFeature('saas')) return callback(null, []) - const query = Object.assign({}, entityConfig.baseQuery) - query[entityConfig.fields.access] = userId - EntityModels[entityConfig.modelName] - .find(query) - .then(entities => { - if (entities == null) { - entities = [] - } - async.mapSeries( - entities, - (entity, cb) => entity.fetchV1Data(cb), - callback - ) - }) - .catch(callback) + await EntityModels[entityConfig.modelName].updateMany({}, removeOperation) }, } -UserMembershipsHandler.promises = promisifyAll(UserMembershipsHandler) -export default UserMembershipsHandler +export default { + ...callbackifyAll(UserMembershipsHandler), + promises: UserMembershipsHandler, +} diff --git a/services/web/app/src/models/Institution.mjs b/services/web/app/src/models/Institution.mjs index bccf80ddd0..12a5a7a182 100644 --- a/services/web/app/src/models/Institution.mjs +++ b/services/web/app/src/models/Institution.mjs @@ -1,7 +1,7 @@ import mongoose from '../infrastructure/Mongoose.mjs' import settings from '@overleaf/settings' import logger from '@overleaf/logger' -import { promisify } from '@overleaf/promise-utils' +import { callbackify } from '@overleaf/promise-utils' import { fetchJson } from '@overleaf/fetch-utils' const { Schema } = mongoose const { ObjectId } = Schema @@ -20,16 +20,15 @@ export const InstitutionSchema = new Schema( ) // fetch institution's data from v1 API. Errors are ignored -InstitutionSchema.method('fetchV1Data', async function (callback) { +async function fetchV1DataPromise() { const url = `${settings.apis.v1.url}/universities/list/${this.v1Id}` try { const parsedBody = await fetchJson(url) - this.name = parsedBody != null ? parsedBody.name : undefined - this.countryCode = parsedBody != null ? parsedBody.country_code : undefined - this.departments = parsedBody != null ? parsedBody.departments : undefined - this.portalSlug = parsedBody != null ? parsedBody.portal_slug : undefined - this.enterpriseCommons = - parsedBody != null ? parsedBody.enterprise_commons : undefined + this.name = parsedBody?.name + this.countryCode = parsedBody?.country_code + this.departments = parsedBody?.departments + this.portalSlug = parsedBody?.portal_slug + this.enterpriseCommons = parsedBody?.enterprise_commons } catch (error) { // log error and carry on without v1 data logger.err( @@ -37,12 +36,11 @@ InstitutionSchema.method('fetchV1Data', async function (callback) { '[fetchV1DataError]' ) } - callback(null, this) -}) + return this +} -InstitutionSchema.method( - 'fetchV1DataPromise', - promisify(InstitutionSchema.methods.fetchV1Data) -) +InstitutionSchema.method('fetchV1DataPromise', fetchV1DataPromise) + +InstitutionSchema.method('fetchV1Data', callbackify(fetchV1DataPromise)) export const Institution = mongoose.model('Institution', InstitutionSchema) diff --git a/services/web/app/src/models/Publisher.mjs b/services/web/app/src/models/Publisher.mjs index 10d34b2173..a18a1f1765 100644 --- a/services/web/app/src/models/Publisher.mjs +++ b/services/web/app/src/models/Publisher.mjs @@ -1,7 +1,8 @@ import mongoose from '../infrastructure/Mongoose.mjs' import settings from '@overleaf/settings' import logger from '@overleaf/logger' -import request from 'request' +import { fetchJson } from '@overleaf/fetch-utils' +import { callbackify } from '@overleaf/promise-utils' const { Schema } = mongoose const { ObjectId } = Schema @@ -13,37 +14,32 @@ export const PublisherSchema = new Schema( { minimize: false } ) -// fetch publisher's (brand on v1) data from v1 API. Errors are ignored -PublisherSchema.method('fetchV1Data', function (callback) { - request( - { - baseUrl: settings.apis.v1.url, - url: `/api/v2/brands/${this.slug}`, - method: 'GET', - auth: { +async function fetchV1DataPromise() { + const url = `${settings.apis.v1.url}/api/v2/brands/${this.slug}` + try { + const parsedBody = await fetchJson(url, { + basicAuth: { user: settings.apis.v1.user, pass: settings.apis.v1.pass, - sendImmediately: true, }, - timeout: settings.apis.v1.timeout, - }, - (error, response, body) => { - let parsedBody - try { - parsedBody = JSON.parse(body) - } catch (error1) { - // log error and carry on without v1 data - error = error1 - logger.err( - { model: 'Publisher', slug: this.slug, error }, - '[fetchV1DataError]' - ) - } - this.name = parsedBody != null ? parsedBody.name : undefined - this.partner = parsedBody != null ? parsedBody.partner : undefined - callback(null, this) - } - ) -}) + signal: AbortSignal.timeout(settings.apis.v1.timeout), + }) + + this.name = parsedBody?.name + this.partner = parsedBody?.partner + return this + } catch (error) { + // log error and carry on without v1 data + logger.err( + { model: 'Publisher', slug: this.slug, error }, + '[fetchV1DataError]' + ) + } + return this +} + +PublisherSchema.method('fetchV1DataPromise', fetchV1DataPromise) + +PublisherSchema.method('fetchV1Data', callbackify(fetchV1DataPromise)) export const Publisher = mongoose.model('Publisher', PublisherSchema) diff --git a/services/web/app/src/models/Subscription.mjs b/services/web/app/src/models/Subscription.mjs index c7d1deaaa0..602edfb1bd 100644 --- a/services/web/app/src/models/Subscription.mjs +++ b/services/web/app/src/models/Subscription.mjs @@ -1,5 +1,6 @@ import mongoose from '../infrastructure/Mongoose.mjs' import { TeamInviteSchema } from './TeamInvite.mjs' +import { callbackify } from '@overleaf/promise-utils' const { Schema } = mongoose const { ObjectId } = Schema @@ -123,8 +124,11 @@ export const SubscriptionSchema = new Schema( ) // Subscriptions have no v1 data to fetch -SubscriptionSchema.method('fetchV1Data', function (callback) { - callback(null, this) -}) +async function fetchV1DataPromise() { + return this +} +SubscriptionSchema.method('fetchV1Data', callbackify(fetchV1DataPromise)) + +SubscriptionSchema.method('fetchV1DataPromise', fetchV1DataPromise) export const Subscription = mongoose.model('Subscription', SubscriptionSchema)