• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

GrottoCenter / grottocenter-api / 26106637338

19 May 2026 03:16PM UTC coverage: 86.833% (-0.2%) from 87.052%
26106637338

Pull #1566

github

dawoldo
fix(1451): fixed email notifications for messages and update associated templates and localizations
Pull Request #1566: 1451 private messaging

3338 of 3994 branches covered (83.58%)

Branch coverage included in aggregate %.

206 of 246 new or added lines in 16 files covered. (83.74%)

3 existing lines in 1 file now uncovered.

6772 of 7649 relevant lines covered (88.53%)

55.24 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.25
/api/services/NotificationService.js
1
const NameService = require('./NameService');
8✔
2
const CommonService = require('./CommonService');
8✔
3
const LanguageService = require('./LanguageService');
8✔
4

5
const NOTIFICATION_ENTITIES = {
8✔
6
  CAVE: 'cave',
7
  COMMENT: 'comment',
8
  DESCRIPTION: 'description',
9
  DOCUMENT: 'document',
10
  ENTRANCE: 'entrance',
11
  HISTORY: 'history',
12
  LOCATION: 'location',
13
  MASSIF: 'massif',
14
  ORGANIZATION: 'organization',
15
  RIGGING: 'rigging',
16
};
17

18
const NOTIFICATION_TYPES = {
8✔
19
  CREATE: 'CREATE',
20
  DELETE: 'DELETE',
21
  PERMANENT_DELETE: 'PERMANENT_DELETE',
22
  UPDATE: 'UPDATE',
23
  VALIDATE: 'VALIDATE',
24
  RESTORE: 'RESTORE',
25
  REJECT: 'REJECT',
26
};
27

28
async function removeOlderNotifications() {
29
  const query = `DELETE
×
30
                 FROM t_notification
31
                 WHERE date_inscription < current_timestamp - interval '2 month';`;
32
  await CommonService.query(query);
×
33
}
34

35
const safeGetPropId = (prop, data) => {
8✔
36
  if (data && data[prop]) {
1,923✔
37
    if (data[prop] instanceof Object) {
554✔
38
      return data[prop].id;
185✔
39
    }
40
    return data[prop];
369✔
41
  }
42
  return undefined;
1,369✔
43
};
44

45
const sendNotificationEmail = async (
8✔
46
  entity,
47
  notificationType,
48
  notificationEntity,
49
  user
50
) => {
51
  // Resolve the recipient's preferred locale
52
  const locale = await LanguageService.getLocale(user.language);
302✔
53

54
  // Get entity name (handle all cases)
55
  const getEntityName = (entityData) => {
302✔
56
    if (entityData.name) return entityData.name;
302✔
57
    if (entityData.names) return entityData.names[0]?.name;
31✔
58
    if (entityData.title) return entityData.title;
30✔
59
    if (entityData.titles) return entityData.titles[0]?.text;
16✔
60
    if (entityData.body) return `${entityData.body.slice(0, 50)}...`;
15✔
61
    if (entityData.descriptions) return entityData.descriptions[0].title;
14✔
62
    return '';
13✔
63
  };
64

65
  const entityName = getEntityName(entity);
302✔
66

67
  // Format action verb
68
  const actionVerbMap = {
302✔
69
    [NOTIFICATION_TYPES.CREATE]: 'created',
70
    [NOTIFICATION_TYPES.DELETE]: 'deleted',
71
    [NOTIFICATION_TYPES.PERMANENT_DELETE]: 'permanently deleted',
72
    [NOTIFICATION_TYPES.UPDATE]: 'updated',
73
    [NOTIFICATION_TYPES.VALIDATE]: 'validated',
74
    [NOTIFICATION_TYPES.RESTORE]: 'restored',
75
    [NOTIFICATION_TYPES.REJECT]: 'rejected',
76
  };
77

78
  const actionVerb = actionVerbMap[notificationType];
302✔
79
  if (!actionVerb) {
302✔
80
    throw Error(`Unknown notification type: ${notificationType}`);
1✔
81
  }
82

83
  // Format entity Link
84
  const baseUrl = `${sails.config.custom.baseUrl}/ui/`;
301✔
85
  const relatedCaveId = safeGetPropId('cave', entity);
301✔
86
  const relatedEntranceId = safeGetPropId('entrance', entity);
301✔
87
  const relatedMassifId = safeGetPropId('massif', entity);
301✔
88

89
  const getRelatedEntityLink = () => {
301✔
90
    if (relatedCaveId) return `caves/${relatedCaveId}`;
22✔
91
    if (relatedEntranceId) return `entrances/${relatedEntranceId}`;
20✔
92
    if (relatedMassifId) return `massifs/${relatedMassifId}`;
5✔
93
    return null;
4✔
94
  };
95

96
  const directLinkEntities = {
301✔
97
    [NOTIFICATION_ENTITIES.CAVE]: `caves/${entity.id}`,
98
    [NOTIFICATION_ENTITIES.DOCUMENT]: `documents/${entity.id}`,
99
    [NOTIFICATION_ENTITIES.ENTRANCE]: `entrances/${entity.id}`,
100
    [NOTIFICATION_ENTITIES.MASSIF]: `massifs/${entity.id}`,
101
    [NOTIFICATION_ENTITIES.ORGANIZATION]: `organizations/${entity.id}`,
102
  };
103

104
  let entityLink;
105
  if (directLinkEntities[notificationEntity]) {
301✔
106
    entityLink = baseUrl + directLinkEntities[notificationEntity];
279✔
107
  } else {
108
    const relatedLink = getRelatedEntityLink();
22✔
109
    if (!relatedLink) {
22✔
110
      const entityTypeName = notificationEntity.toLowerCase();
4✔
111
      let requiredEntities;
112
      if (notificationEntity === NOTIFICATION_ENTITIES.DESCRIPTION) {
4✔
113
        requiredEntities = 'cave, entrance or massif';
1✔
114
      } else if (notificationEntity === NOTIFICATION_ENTITIES.LOCATION) {
3✔
115
        requiredEntities = 'entrance';
1✔
116
      } else {
117
        requiredEntities = 'cave or entrance';
2✔
118
      }
119
      throw Error(
4✔
120
        `Can't find related entity (${requiredEntities}) of the ${entityTypeName} with id ${entity.id}`
121
      );
122
    }
123
    entityLink = baseUrl + relatedLink;
18✔
124
  }
125

126
  await sails.helpers.sendEmail
297✔
127
    .with({
128
      allowResponse: false,
129
      emailSubject: 'Notification',
130
      locale,
131
      recipientEmail: user.mail,
132
      viewName: 'notification',
133
      viewValues: {
134
        actionVerb,
135
        entityLink,
136
        entityName,
137
        entityType: notificationEntity,
138
        recipientName: user.nickname,
139
        subscriptionName: user.subscriptionName,
140
        subscriptionType: user.subscriptionType,
141
        isAuthorNotification: user.isAuthorNotification || false,
380✔
142
        validationComment: user.validationComment || null,
409✔
143
      },
144
    })
145
    .intercept('sendSESEmailError', () => {
146
      sails.log.error(
×
147
        `The email service has encountered an error while trying to notify user ${user.nickname} (id=${user.id}).`
148
      );
149
      return false;
×
150
    });
151
};
152

153
const getCountryMassifAndRegionSubscribers = async (
8✔
154
  entityCountryId,
155
  entityMassifIds,
156
  entityRegionId
157
) => {
158
  const countrySubscribers = [];
146✔
159
  const massifsSubscribers = [];
146✔
160
  const regionSubscribers = [];
146✔
161
  if (entityCountryId) {
146✔
162
    const country =
163
      await TCountry.findOne(entityCountryId).populate('subscribedCavers');
3✔
164
    if (country) {
3!
165
      countrySubscribers.push(
3✔
166
        ...country.subscribedCavers.map((caver) => ({
3✔
167
          ...caver,
168
          subscriptionName: country.nativeName,
169
          subscriptionType: 'country',
170
        }))
171
      );
172
    }
173
  }
174
  if (entityMassifIds) {
146!
175
    await Promise.all(
146✔
176
      entityMassifIds.map(async (massifId) => {
177
        const massif =
178
          await TMassif.findOne(massifId).populate('subscribedCavers');
77✔
179
        if (massif) {
77✔
180
          await NameService.setNames([massif], 'massif');
73✔
181
          massifsSubscribers.push(
73✔
182
            ...massif.subscribedCavers.map((caver) => ({
63✔
183
              ...caver,
184
              subscriptionType: 'massif',
185
              subscriptionName: massif.names[0]?.name,
186
            }))
187
          );
188
        }
189
      })
190
    );
191
  }
192
  if (entityRegionId) {
146✔
193
    const region =
194
      await TISO31662.findOne(entityRegionId).populate('subscribedCavers');
3✔
195
    if (region) {
3!
196
      regionSubscribers.push(
3✔
197
        ...region.subscribedCavers.map((caver) => ({
3✔
198
          ...caver,
199
          subscriptionName: region.name,
200
          subscriptionType: 'region',
201
        }))
202
      );
203
    }
204
  }
205
  return { countrySubscribers, massifsSubscribers, regionSubscribers };
146✔
206
};
207

208
module.exports = {
8✔
209
  NOTIFICATION_ENTITIES,
210
  notifyMessageRecipient: async (req, senderId, conversationId) => {
211
    try {
9✔
212
      const sender = await TCaver.findOne({ id: senderId });
9✔
213
      if (!sender) return;
9!
214
      const query = `SELECT id_caver FROM j_participant WHERE id_conversation = $1 AND id_caver != $2`;
9✔
215
      const result = await CommonService.query(query, [
9✔
216
        conversationId,
217
        senderId,
218
      ]);
219
      if (result.rows.length === 0) return;
9!
220
      await Promise.all(
9✔
221
        result.rows.map(async (row) => {
222
          const recipient = await TCaver.findOne({ id: row.id_caver });
9✔
223
          if (!recipient || !recipient.sendMessageNotificationByEmail) return;
9✔
224
          const locale = await LanguageService.getLocale(recipient.language);
7✔
225
          const conversationLink = `${sails.config.custom.frontendUrl}/ui/messages/${conversationId}`;
7✔
226
          await sails.helpers.sendEmail
7✔
227
            .with({
228
              allowResponse: false,
229
              emailSubject: 'New Message',
230
              locale,
231
              recipientEmail: recipient.mail,
232
              viewName: 'new-message',
233
              viewValues: {
234
                senderNickname: sender.nickname,
235
                conversationLink,
236
                recipientName: recipient.nickname,
237
              },
238
            })
239
            .intercept('sendSESEmailError', () => {
NEW
240
              sails.log.error(
×
241
                `The email service error notifying user ${recipient.nickname}.`
242
              );
NEW
243
              return false;
×
244
            });
245
        })
246
      );
247
    } catch (error) {
NEW
248
      sails.log.error(
×
249
        `An error occurred in notifyMessageRecipient: ${error.message}`
250
      );
251
    }
252
  },
253
  NOTIFICATION_TYPES,
254
  ...(process.env.NODE_ENV === 'test' ? { sendNotificationEmail } : undefined),
8!
255

256
  /**
257
   *
258
   * @param {*} entity
259
   * @param {Number} notifierId
260
   * @param {NOTIFICATION_TYPES} notificationType
261
   * @param {NOTIFICATION_ENTITIES} notificationEntity
262
   * @return {Boolean} true if everything went well, else false
263
   */
264
  notifySubscribers: async (
265
    entity,
266
    notifierId,
267
    notificationType,
268
    notificationEntity
269
  ) => {
270
    // Had to require in the function to avoid a circular dependency with notifySubscribers() in CaveService.createCave()
271
    // eslint-disable-next-line global-require
272
    const CaveService = require('./CaveService');
157✔
273

274
    // Check params and silently fail to avoid sending an error to the client
275
    if (!Object.values(NOTIFICATION_ENTITIES).includes(notificationEntity)) {
157✔
276
      throw new Error(`Invalid notification entity: ${notificationEntity}`);
1✔
277
    }
278
    if (!Object.values(NOTIFICATION_TYPES).includes(notificationType)) {
156✔
279
      throw new Error(`Invalid notification type: ${notificationType}`);
1✔
280
    }
281
    if (!notifierId) {
155✔
282
      throw new Error(`Missing notifier id`);
1✔
283
    }
284

285
    try {
154✔
286
      // For the populateEntities() method, must use "grotto" instead of "organization"
287
      const entityKey =
288
        notificationEntity === NOTIFICATION_ENTITIES.ORGANIZATION
154✔
289
          ? 'grotto'
290
          : notificationEntity;
291

292
      // Format notification and populate entity
293
      const notification = await module.exports.populateEntities({
154✔
294
        dateInscription: new Date(),
295
        notificationType: (
296
          await TNotificationType.findOne({
297
            name: notificationType,
298
          })
299
        ).id,
300
        notifier: notifierId,
301
        [entityKey]: entity,
302
      });
303

304
      const populatedEntity = notification[entityKey];
154✔
305

306
      const caveId = safeGetPropId('cave', populatedEntity);
154✔
307
      const entranceId = safeGetPropId('entrance', populatedEntity);
154✔
308
      const massifId = safeGetPropId('massif', populatedEntity);
154✔
309

310
      const getMassifIdsFromCave = async (id) =>
154✔
311
        (await CaveService.getMassifs(id)).map((m) => m.id);
76✔
312
      const getCountryId = (id) => safeGetPropId('country', id);
154✔
313
      const getRegionId = (entityData) => entityData?.iso_3166_2;
154✔
314

315
      const getCountryFromCaveEntrances = (cave) => {
154✔
316
        if (cave?.entrances?.length > 0) {
16✔
317
          return getCountryId(cave.entrances[0]);
5✔
318
        }
319
        return null;
11✔
320
      };
321

322
      const resolveLocationFromCaveOrEntrance = async (
154✔
323
        relatedCaveId,
324
        relatedEntranceId,
325
        entityData
326
      ) => {
327
        if (relatedCaveId) {
65✔
328
          return {
3✔
329
            countryId: getCountryFromCaveEntrances(entityData.cave),
330
            massifIds: await getMassifIdsFromCave(relatedCaveId),
331
            regionId: entityData.cave?.entrances?.[0]
3!
332
              ? getRegionId(entityData.cave.entrances[0])
333
              : null,
334
          };
335
        }
336
        if (relatedEntranceId) {
62✔
337
          return {
19✔
338
            countryId: getCountryId(entityData.entrance),
339
            massifIds: await getMassifIdsFromCave(
340
              safeGetPropId('cave', entityData.entrance)
341
            ),
342
            regionId: getRegionId(entityData.entrance),
343
          };
344
        }
345
        return { countryId: null, massifIds: [], regionId: null };
43✔
346
      };
347

348
      // Entity-specific location resolution
349
      const entityResolvers = {
154✔
350
        [NOTIFICATION_ENTITIES.CAVE]: async () => ({
13✔
351
          countryId: getCountryFromCaveEntrances(populatedEntity),
352
          massifIds: await getMassifIdsFromCave(populatedEntity.id),
353
          regionId: populatedEntity?.entrances?.[0]
13✔
354
            ? getRegionId(populatedEntity.entrances[0])
355
            : null,
356
        }),
357

358
        [NOTIFICATION_ENTITIES.ENTRANCE]: async () => ({
38✔
359
          countryId: getCountryId(populatedEntity),
360
          massifIds: populatedEntity?.cave
38✔
361
            ? await getMassifIdsFromCave(safeGetPropId('cave', populatedEntity))
362
            : [],
363
          regionId: getRegionId(populatedEntity),
364
        }),
365

366
        [NOTIFICATION_ENTITIES.MASSIF]: async () => ({
11✔
367
          countryId: null,
368
          massifIds: [populatedEntity.id],
369
          regionId: null,
370
        }),
371

372
        [NOTIFICATION_ENTITIES.ORGANIZATION]: async () => ({
16✔
373
          countryId: getCountryId(populatedEntity),
374
          massifIds: [],
375
          regionId: getRegionId(populatedEntity),
376
        }),
377

378
        [NOTIFICATION_ENTITIES.LOCATION]: async () => {
379
          if (!entranceId)
11✔
380
            throw new Error(`Can't retrieve related entrance id.`);
2✔
381
          return {
9✔
382
            countryId: getCountryId(populatedEntity.entrance),
383
            massifIds: await getMassifIdsFromCave(
384
              safeGetPropId('cave', populatedEntity.entrance)
385
            ),
386
            regionId: getRegionId(populatedEntity.entrance),
387
          };
388
        },
389
      };
390

391
      // Entities that can relate to cave, entrance, or massif
392
      const multiRelationEntities = [
154✔
393
        NOTIFICATION_ENTITIES.COMMENT,
394
        NOTIFICATION_ENTITIES.DESCRIPTION,
395
        NOTIFICATION_ENTITIES.HISTORY,
396
        NOTIFICATION_ENTITIES.RIGGING,
397
      ];
398

399
      // Find massifs and country concerned about the notification
400
      let result;
401
      if (entityResolvers[notificationEntity]) {
154✔
402
        result = await entityResolvers[notificationEntity]();
89✔
403
      } else if (multiRelationEntities.includes(notificationEntity)) {
65✔
404
        result = await resolveLocationFromCaveOrEntrance(
30✔
405
          caveId,
406
          entranceId,
407
          populatedEntity
408
        );
409

410
        // Handle massif-only case for description and document
411
        if (!result.countryId && !result.massifIds.length && massifId) {
30!
412
          result.massifIds = [safeGetPropId('massif', populatedEntity)];
×
413
        }
414

415
        // Require cave or entrance for most entities
416
        if (
30✔
417
          !caveId &&
65✔
418
          !entranceId &&
419
          ![
420
            NOTIFICATION_ENTITIES.DESCRIPTION,
421
            NOTIFICATION_ENTITIES.DOCUMENT,
422
          ].includes(notificationEntity)
423
        ) {
424
          throw new Error(`Can't retrieve related cave or entrance id.`);
6✔
425
        }
426
      } else if (notificationEntity === NOTIFICATION_ENTITIES.DOCUMENT) {
35!
427
        result = await resolveLocationFromCaveOrEntrance(
35✔
428
          caveId,
429
          entranceId,
430
          populatedEntity
431
        );
432
        if (!result.countryId && !result.massifIds.length && massifId) {
35!
433
          result.massifIds = [safeGetPropId('massif', populatedEntity)];
×
434
        }
435
      } else {
436
        throw new Error(
×
437
          `Can't find what to do with the following notification entity value: ${notificationEntity}`
438
        );
439
      }
440

441
      const entityCountryId = result.countryId;
146✔
442
      const entityMassifIds = result.massifIds;
146✔
443
      const entityRegionId = result.regionId;
146✔
444

445
      // Find subscribers to the entity.
446
      const { countrySubscribers, massifsSubscribers, regionSubscribers } =
447
        await getCountryMassifAndRegionSubscribers(
146✔
448
          entityCountryId,
449
          entityMassifIds,
450
          entityRegionId
451
        );
452
      // Consolidate subscribers by user ID and combine subscription types
453
      const subscriberMap = new Map();
146✔
454

455
      const addSubscribers = (subscribers) => {
146✔
456
        subscribers
438✔
457
          .filter((u) => u.id !== notifierId)
69✔
458
          .forEach((user) => {
459
            if (subscriberMap.has(user.id)) {
69✔
460
              const existing = subscriberMap.get(user.id);
2✔
461
              existing.subscriptionNames.push(user.subscriptionName);
2✔
462
              existing.subscriptionTypes.push(user.subscriptionType);
2✔
463
            } else {
464
              subscriberMap.set(user.id, {
67✔
465
                ...user,
466
                subscriptionNames: [user.subscriptionName],
467
                subscriptionTypes: [user.subscriptionType],
468
              });
469
            }
470
          });
471
      };
472

473
      addSubscribers(countrySubscribers);
146✔
474
      addSubscribers(massifsSubscribers);
146✔
475
      addSubscribers(regionSubscribers);
146✔
476

477
      const uniqueUsers = Array.from(subscriberMap.values()).map((user) => ({
146✔
478
        ...user,
479
        subscriptionName: user.subscriptionNames.join(' and '),
480
        subscriptionType: user.subscriptionTypes.join(', '),
481
      }));
482

483
      // Create notifications & optionally send email
484
      const res = await Promise.all(
146✔
485
        uniqueUsers.map(async (user) => {
486
          try {
67✔
487
            await TNotification.create({
67✔
488
              ...notification,
489
              notified: user.id,
490
              [entityKey]: notification[entityKey].id, // id only for the DB storage
491
            });
492
          } catch (e) {
493
            sails.log.error(
×
494
              `An error occured when trying to create a notification: ${e.message}`
495
            );
496
            return false;
×
497
          }
498

499
          if (user.sendNotificationByEmail) {
67!
500
            await sendNotificationEmail(
×
501
              populatedEntity,
502
              notificationType,
503
              notificationEntity,
504
              user
505
            );
506
          }
507
          return true;
67✔
508
        })
509
      );
510

511
      // 5% chance to also remove older notifications
512
      if (process.env.NODE_ENV !== 'test' && Math.random() < 0.05) {
146!
513
        try {
×
514
          await removeOlderNotifications();
×
515
        } catch (cleanupError) {
516
          sails.log.error(
×
517
            `Error during notification cleanup: ${cleanupError.message}`
518
          );
519
        }
520
      }
521

522
      return res;
146✔
523
    } catch (error) {
524
      // Fail silently to avoid sending an error to the user
525
      sails.log.error(
8✔
526
        `An error occurred when trying to notify subscribers: ${error.message} ${error.stack}`
527
      );
528
      return false;
8✔
529
    }
530
  },
531

532
  /**
533
   * Create an in-app notification for the document author and optionally send an email.
534
   *
535
   * @param {Object}  document         - Populated TDocument (must have .author)
536
   * @param {Number}  moderatorId      - ID of the moderator who made the decision
537
   * @param {String}  notificationType - NOTIFICATION_TYPES.VALIDATE or NOTIFICATION_TYPES.REJECT
538
   * @param {String|null} validationComment - Moderator's comment (required for REJECT)
539
   * @returns {Boolean} true on success, false on silent failure
540
   */
541
  notifyAuthor: async (
542
    document,
543
    moderatorId,
544
    notificationType,
545
    validationComment
546
  ) => {
547
    const authorId = safeGetPropId('author', document);
323✔
548
    if (!authorId) {
323✔
549
      sails.log.debug(
1✔
550
        `notifyAuthor: document ${document.id} has no author, skipping notification`
551
      );
552
      return true;
1✔
553
    }
554
    if (authorId === moderatorId) {
322✔
555
      return true;
103✔
556
    }
557

558
    if (!Object.values(NOTIFICATION_TYPES).includes(notificationType)) {
219!
559
      throw new Error(`Invalid notification type: ${notificationType}`);
×
560
    }
561

562
    try {
219✔
563
      const notificationTypeRecord = await TNotificationType.findOne({
219✔
564
        name: notificationType,
565
      });
566

567
      if (!notificationTypeRecord) {
219!
568
        throw new Error(
×
569
          `Notification type '${notificationType}' not found in DB — migration may not have run`
570
        );
571
      }
572

573
      await TNotification.create({
219✔
574
        dateInscription: new Date(),
575
        notificationType: notificationTypeRecord.id,
576
        notifier: moderatorId,
577
        notified: authorId,
578
        document: document.id,
579
      });
580

581
      const author = await TCaver.findOne(authorId);
219✔
582

583
      if (author && author.sendNotificationByEmail) {
219✔
584
        await sendNotificationEmail(
58✔
585
          document,
586
          notificationType,
587
          NOTIFICATION_ENTITIES.DOCUMENT,
588
          {
589
            ...author,
590
            isAuthorNotification: true,
591
            validationComment,
592
          }
593
        );
594
      }
595

596
      return true;
219✔
597
    } catch (error) {
598
      sails.log.error(
×
599
        `An error occurred when trying to notify the document author: ${error.message} ${error.stack}`
600
      );
601
      return false;
×
602
    }
603
  },
604

605
  populateEntities: async (notification) => {
606
    const populatedNotification = notification;
204✔
607
    if (populatedNotification.cave) {
204✔
608
      await NameService.setNames([populatedNotification.cave], 'cave');
15✔
609
    }
610
    if (populatedNotification.comment) {
204✔
611
      populatedNotification.comment = await TComment.findOne(
11✔
612
        safeGetPropId('comment', notification)
613
      )
614
        .populate('cave')
615
        .populate('entrance');
616
    }
617
    if (populatedNotification.description) {
204✔
618
      populatedNotification.description = await TDescription.findOne(
10✔
619
        safeGetPropId('description', notification)
620
      )
621
        .populate('cave')
622
        .populate('document')
623
        .populate('entrance')
624
        .populate('massif');
625
    }
626
    if (populatedNotification.document) {
204✔
627
      // Had to require in the function to avoid a circular dependency with notifySubscribers() in DocumentService.createDocument()
628
      // eslint-disable-next-line global-require
629
      const DocumentService = require('./DocumentService');
42✔
630
      const populatedDocuments = await DocumentService.getDocuments([
42✔
631
        safeGetPropId('document', notification),
632
      ]);
633
      populatedNotification.document = populatedDocuments[0];
42✔
634
    }
635
    if (populatedNotification.entrance) {
204✔
636
      await NameService.setNames([populatedNotification.entrance], 'entrance');
53✔
637
    }
638
    if (populatedNotification.grotto) {
204✔
639
      await NameService.setNames([populatedNotification.grotto], 'grotto');
16✔
640
    }
641
    if (populatedNotification.history) {
204✔
642
      populatedNotification.history = await THistory.findOne(
7✔
643
        safeGetPropId('history', notification)
644
      )
645
        .populate('cave')
646
        .populate('entrance');
647
    }
648
    if (populatedNotification.location) {
204✔
649
      populatedNotification.location = await TLocation.findOne(
12✔
650
        safeGetPropId('location', notification)
651
      ).populate('entrance');
652
    }
653
    if (populatedNotification.massif) {
204✔
654
      await NameService.setNames([populatedNotification.massif], 'massif');
11✔
655
    }
656
    if (populatedNotification.rigging) {
204✔
657
      populatedNotification.rigging = await TRigging.findOne(
6✔
658
        safeGetPropId('rigging', notification)
659
      )
660
        .populate('entrance')
661
        .populate('cave');
662
    }
663
    return populatedNotification;
204✔
664
  },
665
};
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc