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

GrottoCenter / grottocenter-api / 26441057068

26 May 2026 08:23AM UTC coverage: 86.883% (-0.2%) from 87.055%
26441057068

Pull #1566

github

web-flow
Merge branch 'develop' into 1451-private-messaging
Pull Request #1566: 1451 private messaging

3371 of 4026 branches covered (83.73%)

Branch coverage included in aggregate %.

207 of 248 new or added lines in 16 files covered. (83.47%)

3 existing lines in 1 file now uncovered.

6790 of 7669 relevant lines covered (88.54%)

54.96 hits per line

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

90.36
/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,900✔
37
    if (data[prop] instanceof Object) {
558✔
38
      return data[prop].id;
189✔
39
    }
40
    return data[prop];
369✔
41
  }
42
  return undefined;
1,342✔
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 =
53
    (await LanguageService.getLocale(user.language)) ||
291✔
54
    sails.config.i18n.defaultLocale;
55

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

67
  const entityName = getEntityName(entity);
291✔
68

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

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

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

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

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

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

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

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

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

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

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

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

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

305
      const populatedEntity = notification[entityKey];
156✔
306

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

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

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

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

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

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

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

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

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

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

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

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

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

442
      const entityCountryId = result.countryId;
148✔
443
      const entityMassifIds = result.massifIds;
148✔
444
      const entityRegionId = result.regionId;
148✔
445

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

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

474
      addSubscribers(countrySubscribers);
148✔
475
      addSubscribers(massifsSubscribers);
148✔
476
      addSubscribers(regionSubscribers);
148✔
477

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

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

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

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

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

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

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

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

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

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

582
      const author = await TCaver.findOne(authorId);
220✔
583

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

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

606
  populateEntities: async (notification) => {
607
    const populatedNotification = notification;
206✔
608
    if (populatedNotification.cave) {
206✔
609
      await NameService.setNames([populatedNotification.cave], 'cave');
14✔
610
    }
611
    if (populatedNotification.comment) {
206✔
612
      populatedNotification.comment = await TComment.findOne(
11✔
613
        safeGetPropId('comment', notification)
614
      )
615
        .populate('cave')
616
        .populate('entrance');
617
    }
618
    if (populatedNotification.description) {
206✔
619
      populatedNotification.description = await TDescription.findOne(
10✔
620
        safeGetPropId('description', notification)
621
      )
622
        .populate('cave')
623
        .populate('document')
624
        .populate('entrance')
625
        .populate('massif');
626
    }
627
    if (populatedNotification.document) {
206✔
628
      // Had to require in the function to avoid a circular dependency with notifySubscribers() in DocumentService.createDocument()
629
      // eslint-disable-next-line global-require
630
      const DocumentService = require('./DocumentService');
45✔
631
      const populatedDocuments = await DocumentService.getDocuments([
45✔
632
        safeGetPropId('document', notification),
633
      ]);
634
      populatedNotification.document = populatedDocuments[0];
45✔
635
    }
636
    if (populatedNotification.entrance) {
206✔
637
      await NameService.setNames([populatedNotification.entrance], 'entrance');
53✔
638
    }
639
    if (populatedNotification.grotto) {
206✔
640
      await NameService.setNames([populatedNotification.grotto], 'grotto');
16✔
641
    }
642
    if (populatedNotification.history) {
206✔
643
      populatedNotification.history = await THistory.findOne(
7✔
644
        safeGetPropId('history', notification)
645
      )
646
        .populate('cave')
647
        .populate('entrance');
648
    }
649
    if (populatedNotification.location) {
206✔
650
      populatedNotification.location = await TLocation.findOne(
12✔
651
        safeGetPropId('location', notification)
652
      ).populate('entrance');
653
    }
654
    if (populatedNotification.massif) {
206✔
655
      await NameService.setNames([populatedNotification.massif], 'massif');
11✔
656
    }
657
    if (populatedNotification.rigging) {
206✔
658
      populatedNotification.rigging = await TRigging.findOne(
6✔
659
        safeGetPropId('rigging', notification)
660
      )
661
        .populate('entrance')
662
        .populate('cave');
663
    }
664
    return populatedNotification;
206✔
665
  },
666
};
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