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

GrottoCenter / grottocenter-api / 25457721116

06 May 2026 07:47PM UTC coverage: 86.494% (-0.2%) from 86.702%
25457721116

Pull #1565

github

ClemRz
feat(account): consolidate account endpoints and add locale-aware emails

- Add GET /api/v1/account for authenticated user's private data
- Add PATCH /api/v1/account for self-service updates (name, surname,
  nickname, email, password, language)
- Restrict PUT /api/v1/cavers/:caverId to admin-only
- Add optional language param to sign-up endpoint
- Add locale support to send-email helper for recipient-language emails
- Remove orphaned change-email, change-alert-for-news controllers
- Update Swagger spec with consolidated /account path
- Update tests to match new endpoint behavior
- Silence pre-existing func-names lint warnings in property tests
Pull Request #1565: feat(account): consolidate endpoints and add locale-aware emails

3047 of 3661 branches covered (83.23%)

Branch coverage included in aggregate %.

71 of 91 new or added lines in 8 files covered. (78.02%)

27 existing lines in 6 files now uncovered.

6297 of 7142 relevant lines covered (88.17%)

53.2 hits per line

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

91.07
/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,869✔
37
    if (data[prop] instanceof Object) {
556✔
38
      return data[prop].id;
187✔
39
    }
40
    return data[prop];
369✔
41
  }
42
  return undefined;
1,313✔
43
};
44

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

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

66
  const entityName = getEntityName(entity);
287✔
67

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

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

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

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

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

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

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

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

209
module.exports = {
8✔
210
  NOTIFICATION_ENTITIES,
211
  NOTIFICATION_TYPES,
212
  ...(process.env.NODE_ENV === 'test' ? { sendNotificationEmail } : undefined),
8!
213

214
  /**
215
   *
216
   * @param {*} req
217
   * @param {*} entity
218
   * @param {Number} notifierId
219
   * @param {NOTIFICATION_TYPES} notificationType
220
   * @param {NOTIFICATION_ENTITIES} notificationEntity
221
   * @return {Boolean} true if everything went well, else false
222
   */
223
  notifySubscribers: async (
224
    req,
225
    entity,
226
    notifierId,
227
    notificationType,
228
    notificationEntity
229
  ) => {
230
    // Had to require in the function to avoid a circular dependency with notifySubscribers() in CaveService.createCave()
231
    // eslint-disable-next-line global-require
232
    const CaveService = require('./CaveService');
155✔
233

234
    // Check params and silently fail to avoid sending an error to the client
235
    if (!Object.values(NOTIFICATION_ENTITIES).includes(notificationEntity)) {
155✔
236
      throw new Error(`Invalid notification entity: ${notificationEntity}`);
1✔
237
    }
238
    if (!Object.values(NOTIFICATION_TYPES).includes(notificationType)) {
154✔
239
      throw new Error(`Invalid notification type: ${notificationType}`);
1✔
240
    }
241
    if (!notifierId) {
153✔
242
      throw new Error(`Missing notifier id`);
1✔
243
    }
244

245
    try {
152✔
246
      // For the populateEntities() method, must use "grotto" instead of "organization"
247
      const entityKey =
248
        notificationEntity === NOTIFICATION_ENTITIES.ORGANIZATION
152✔
249
          ? 'grotto'
250
          : notificationEntity;
251

252
      // Format notification and populate entity
253
      const notification = await module.exports.populateEntities({
152✔
254
        dateInscription: new Date(),
255
        notificationType: (
256
          await TNotificationType.findOne({
257
            name: notificationType,
258
          })
259
        ).id,
260
        notifier: notifierId,
261
        [entityKey]: entity,
262
      });
263

264
      const populatedEntity = notification[entityKey];
152✔
265

266
      const caveId = safeGetPropId('cave', populatedEntity);
152✔
267
      const entranceId = safeGetPropId('entrance', populatedEntity);
152✔
268
      const massifId = safeGetPropId('massif', populatedEntity);
152✔
269

270
      const getMassifIdsFromCave = async (id) =>
152✔
271
        (await CaveService.getMassifs(id)).map((m) => m.id);
75✔
272
      const getCountryId = (id) => safeGetPropId('country', id);
152✔
273
      const getRegionId = (entityData) => entityData?.iso_3166_2;
152✔
274

275
      const getCountryFromCaveEntrances = (cave) => {
152✔
276
        if (cave?.entrances?.length > 0) {
16✔
277
          return getCountryId(cave.entrances[0]);
5✔
278
        }
279
        return null;
11✔
280
      };
281

282
      const resolveLocationFromCaveOrEntrance = async (
152✔
283
        relatedCaveId,
284
        relatedEntranceId,
285
        entityData
286
      ) => {
287
        if (relatedCaveId) {
62✔
288
          return {
3✔
289
            countryId: getCountryFromCaveEntrances(entityData.cave),
290
            massifIds: await getMassifIdsFromCave(relatedCaveId),
291
            regionId: entityData.cave?.entrances?.[0]
3!
292
              ? getRegionId(entityData.cave.entrances[0])
293
              : null,
294
          };
295
        }
296
        if (relatedEntranceId) {
59✔
297
          return {
19✔
298
            countryId: getCountryId(entityData.entrance),
299
            massifIds: await getMassifIdsFromCave(
300
              safeGetPropId('cave', entityData.entrance)
301
            ),
302
            regionId: getRegionId(entityData.entrance),
303
          };
304
        }
305
        return { countryId: null, massifIds: [], regionId: null };
40✔
306
      };
307

308
      // Entity-specific location resolution
309
      const entityResolvers = {
152✔
310
        [NOTIFICATION_ENTITIES.CAVE]: async () => ({
13✔
311
          countryId: getCountryFromCaveEntrances(populatedEntity),
312
          massifIds: await getMassifIdsFromCave(populatedEntity.id),
313
          regionId: populatedEntity?.entrances?.[0]
13✔
314
            ? getRegionId(populatedEntity.entrances[0])
315
            : null,
316
        }),
317

318
        [NOTIFICATION_ENTITIES.ENTRANCE]: async () => ({
39✔
319
          countryId: getCountryId(populatedEntity),
320
          massifIds: populatedEntity?.cave
39✔
321
            ? await getMassifIdsFromCave(safeGetPropId('cave', populatedEntity))
322
            : [],
323
          regionId: getRegionId(populatedEntity),
324
        }),
325

326
        [NOTIFICATION_ENTITIES.MASSIF]: async () => ({
11✔
327
          countryId: null,
328
          massifIds: [populatedEntity.id],
329
          regionId: null,
330
        }),
331

332
        [NOTIFICATION_ENTITIES.ORGANIZATION]: async () => ({
16✔
333
          countryId: getCountryId(populatedEntity),
334
          massifIds: [],
335
          regionId: getRegionId(populatedEntity),
336
        }),
337

338
        [NOTIFICATION_ENTITIES.LOCATION]: async () => {
339
          if (!entranceId)
11✔
340
            throw new Error(`Can't retrieve related entrance id.`);
2✔
341
          return {
9✔
342
            countryId: getCountryId(populatedEntity.entrance),
343
            massifIds: await getMassifIdsFromCave(
344
              safeGetPropId('cave', populatedEntity.entrance)
345
            ),
346
            regionId: getRegionId(populatedEntity.entrance),
347
          };
348
        },
349
      };
350

351
      // Entities that can relate to cave, entrance, or massif
352
      const multiRelationEntities = [
152✔
353
        NOTIFICATION_ENTITIES.COMMENT,
354
        NOTIFICATION_ENTITIES.DESCRIPTION,
355
        NOTIFICATION_ENTITIES.HISTORY,
356
        NOTIFICATION_ENTITIES.RIGGING,
357
      ];
358

359
      // Find massifs and country concerned about the notification
360
      let result;
361
      if (entityResolvers[notificationEntity]) {
152✔
362
        result = await entityResolvers[notificationEntity]();
90✔
363
      } else if (multiRelationEntities.includes(notificationEntity)) {
62✔
364
        result = await resolveLocationFromCaveOrEntrance(
30✔
365
          caveId,
366
          entranceId,
367
          populatedEntity
368
        );
369

370
        // Handle massif-only case for description and document
371
        if (!result.countryId && !result.massifIds.length && massifId) {
30!
UNCOV
372
          result.massifIds = [safeGetPropId('massif', populatedEntity)];
×
373
        }
374

375
        // Require cave or entrance for most entities
376
        if (
30✔
377
          !caveId &&
65✔
378
          !entranceId &&
379
          ![
380
            NOTIFICATION_ENTITIES.DESCRIPTION,
381
            NOTIFICATION_ENTITIES.DOCUMENT,
382
          ].includes(notificationEntity)
383
        ) {
384
          throw new Error(`Can't retrieve related cave or entrance id.`);
6✔
385
        }
386
      } else if (notificationEntity === NOTIFICATION_ENTITIES.DOCUMENT) {
32!
387
        result = await resolveLocationFromCaveOrEntrance(
32✔
388
          caveId,
389
          entranceId,
390
          populatedEntity
391
        );
392
        if (!result.countryId && !result.massifIds.length && massifId) {
32!
393
          result.massifIds = [safeGetPropId('massif', populatedEntity)];
×
394
        }
395
      } else {
UNCOV
396
        throw new Error(
×
397
          `Can't find what to do with the following notification entity value: ${notificationEntity}`
398
        );
399
      }
400

401
      const entityCountryId = result.countryId;
144✔
402
      const entityMassifIds = result.massifIds;
144✔
403
      const entityRegionId = result.regionId;
144✔
404

405
      // Find subscribers to the entity.
406
      const { countrySubscribers, massifsSubscribers, regionSubscribers } =
407
        await getCountryMassifAndRegionSubscribers(
144✔
408
          entityCountryId,
409
          entityMassifIds,
410
          entityRegionId
411
        );
412
      // Consolidate subscribers by user ID and combine subscription types
413
      const subscriberMap = new Map();
144✔
414

415
      const addSubscribers = (subscribers) => {
144✔
416
        subscribers
432✔
417
          .filter((u) => u.id !== notifierId)
73✔
418
          .forEach((user) => {
419
            if (subscriberMap.has(user.id)) {
73✔
420
              const existing = subscriberMap.get(user.id);
9✔
421
              existing.subscriptionNames.push(user.subscriptionName);
9✔
422
              existing.subscriptionTypes.push(user.subscriptionType);
9✔
423
            } else {
424
              subscriberMap.set(user.id, {
64✔
425
                ...user,
426
                subscriptionNames: [user.subscriptionName],
427
                subscriptionTypes: [user.subscriptionType],
428
              });
429
            }
430
          });
431
      };
432

433
      addSubscribers(countrySubscribers);
144✔
434
      addSubscribers(massifsSubscribers);
144✔
435
      addSubscribers(regionSubscribers);
144✔
436

437
      const uniqueUsers = Array.from(subscriberMap.values()).map((user) => ({
144✔
438
        ...user,
439
        subscriptionName: user.subscriptionNames.join(' and '),
440
        subscriptionType: user.subscriptionTypes.join(', '),
441
      }));
442

443
      // Create notifications & optionally send email
444
      const res = await Promise.all(
144✔
445
        uniqueUsers.map(async (user) => {
446
          try {
64✔
447
            await TNotification.create({
64✔
448
              ...notification,
449
              notified: user.id,
450
              [entityKey]: notification[entityKey].id, // id only for the DB storage
451
            });
452
          } catch (e) {
453
            sails.log.error(
×
454
              `An error occured when trying to create a notification: ${e.message}`
455
            );
UNCOV
456
            return false;
×
457
          }
458

459
          if (user.sendNotificationByEmail) {
64!
UNCOV
460
            await sendNotificationEmail(
×
461
              populatedEntity,
462
              notificationType,
463
              notificationEntity,
464
              req,
465
              user
466
            );
467
          }
468
          return true;
64✔
469
        })
470
      );
471

472
      // 5% chance to also remove older notifications
473
      if (process.env.NODE_ENV !== 'test' && Math.random() < 0.05) {
144!
UNCOV
474
        try {
×
UNCOV
475
          await removeOlderNotifications();
×
476
        } catch (cleanupError) {
UNCOV
477
          sails.log.error(
×
478
            `Error during notification cleanup: ${cleanupError.message}`
479
          );
480
        }
481
      }
482

483
      return res;
144✔
484
    } catch (error) {
485
      // Fail silently to avoid sending an error to the user
486
      sails.log.error(
8✔
487
        `An error occurred when trying to notify subscribers: ${error.message} ${error.stack}`
488
      );
489
      return false;
8✔
490
    }
491
  },
492

493
  /**
494
   * Create an in-app notification for the document author and optionally send an email.
495
   *
496
   * @param {Object}  req              - Express request (carries i18n)
497
   * @param {Object}  document         - Populated TDocument (must have .author)
498
   * @param {Number}  moderatorId      - ID of the moderator who made the decision
499
   * @param {String}  notificationType - NOTIFICATION_TYPES.VALIDATE or NOTIFICATION_TYPES.REJECT
500
   * @param {String|null} validationComment - Moderator's comment (required for REJECT)
501
   * @returns {Boolean} true on success, false on silent failure
502
   */
503
  notifyAuthor: async (
504
    req,
505
    document,
506
    moderatorId,
507
    notificationType,
508
    validationComment
509
  ) => {
510
    const authorId = safeGetPropId('author', document);
323✔
511
    if (!authorId) {
323✔
512
      sails.log.debug(
1✔
513
        `notifyAuthor: document ${document.id} has no author, skipping notification`
514
      );
515
      return true;
1✔
516
    }
517
    if (authorId === moderatorId) {
322✔
518
      return true;
103✔
519
    }
520

521
    if (!Object.values(NOTIFICATION_TYPES).includes(notificationType)) {
219!
UNCOV
522
      throw new Error(`Invalid notification type: ${notificationType}`);
×
523
    }
524

525
    try {
219✔
526
      const notificationTypeRecord = await TNotificationType.findOne({
219✔
527
        name: notificationType,
528
      });
529

530
      if (!notificationTypeRecord) {
219!
UNCOV
531
        throw new Error(
×
532
          `Notification type '${notificationType}' not found in DB — migration may not have run`
533
        );
534
      }
535

536
      await TNotification.create({
219✔
537
        dateInscription: new Date(),
538
        notificationType: notificationTypeRecord.id,
539
        notifier: moderatorId,
540
        notified: authorId,
541
        document: document.id,
542
      });
543

544
      const author = await TCaver.findOne(authorId);
219✔
545

546
      if (author && author.sendNotificationByEmail) {
219✔
547
        await sendNotificationEmail(
43✔
548
          document,
549
          notificationType,
550
          NOTIFICATION_ENTITIES.DOCUMENT,
551
          req,
552
          {
553
            ...author,
554
            isAuthorNotification: true,
555
            validationComment,
556
          }
557
        );
558
      }
559

560
      return true;
219✔
561
    } catch (error) {
UNCOV
562
      sails.log.error(
×
563
        `An error occurred when trying to notify the document author: ${error.message} ${error.stack}`
564
      );
UNCOV
565
      return false;
×
566
    }
567
  },
568

569
  populateEntities: async (notification) => {
570
    const populatedNotification = notification;
202✔
571
    if (populatedNotification.cave) {
202✔
572
      await NameService.setNames([populatedNotification.cave], 'cave');
15✔
573
    }
574
    if (populatedNotification.comment) {
202✔
575
      populatedNotification.comment = await TComment.findOne(
11✔
576
        safeGetPropId('comment', notification)
577
      )
578
        .populate('cave')
579
        .populate('entrance');
580
    }
581
    if (populatedNotification.description) {
202✔
582
      populatedNotification.description = await TDescription.findOne(
10✔
583
        safeGetPropId('description', notification)
584
      )
585
        .populate('cave')
586
        .populate('document')
587
        .populate('entrance')
588
        .populate('massif');
589
    }
590
    if (populatedNotification.document) {
202✔
591
      // Had to require in the function to avoid a circular dependency with notifySubscribers() in DocumentService.createDocument()
592
      // eslint-disable-next-line global-require
593
      const DocumentService = require('./DocumentService');
39✔
594
      const populatedDocuments = await DocumentService.getDocuments([
39✔
595
        safeGetPropId('document', notification),
596
      ]);
597
      populatedNotification.document = populatedDocuments[0];
39✔
598
    }
599
    if (populatedNotification.entrance) {
202✔
600
      await NameService.setNames([populatedNotification.entrance], 'entrance');
55✔
601
    }
602
    if (populatedNotification.grotto) {
202✔
603
      await NameService.setNames([populatedNotification.grotto], 'grotto');
16✔
604
    }
605
    if (populatedNotification.history) {
202✔
606
      populatedNotification.history = await THistory.findOne(
7✔
607
        safeGetPropId('history', notification)
608
      )
609
        .populate('cave')
610
        .populate('entrance');
611
    }
612
    if (populatedNotification.location) {
202✔
613
      populatedNotification.location = await TLocation.findOne(
12✔
614
        safeGetPropId('location', notification)
615
      ).populate('entrance');
616
    }
617
    if (populatedNotification.massif) {
202✔
618
      await NameService.setNames([populatedNotification.massif], 'massif');
11✔
619
    }
620
    if (populatedNotification.rigging) {
202✔
621
      populatedNotification.rigging = await TRigging.findOne(
6✔
622
        safeGetPropId('rigging', notification)
623
      )
624
        .populate('entrance')
625
        .populate('cave');
626
    }
627
    return populatedNotification;
202✔
628
  },
629
};
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