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

GrottoCenter / grottocenter-api / 10996013011

23 Sep 2024 02:06PM UTC coverage: 46.158% (-2.8%) from 48.952%
10996013011

Pull #1306

github

vmarseguerra
feat(entities): adds delete / restore for document
Pull Request #1306: Adds delete / restore for entrance sub entities

740 of 2203 branches covered (33.59%)

Branch coverage included in aggregate %.

528 of 1295 new or added lines in 114 files covered. (40.77%)

23 existing lines in 18 files now uncovered.

2462 of 4734 relevant lines covered (52.01%)

4.5 hits per line

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

64.99
/api/services/NotificationService.js
1
const NameService = require('./NameService');
6✔
2

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

16
const NOTIFICATION_TYPES = {
6✔
17
  CREATE: 'CREATE',
18
  DELETE: 'DELETE',
19
  PERMANENT_DELETE: 'PERMANENT_DELETE',
20
  UPDATE: 'UPDATE',
21
  VALIDATE: 'VALIDATE',
22
  RESTORE: 'RESTORE',
23
};
24

25
const safeGetPropId = (prop, data) => {
6✔
26
  if (data && data[prop]) {
210✔
27
    if (data[prop] instanceof Object) {
51✔
28
      return data[prop].id;
30✔
29
    }
30
    return data[prop];
21✔
31
  }
32
  return undefined;
159✔
33
};
34

35
const sendNotificationEmail = async (
6✔
36
  entity,
37
  notificationType,
38
  notificationEntity,
39
  req,
40
  user
41
) => {
42
  // Get entity name (handle all cases)
43
  let entityName = '';
26✔
44
  if (entity.name) entityName = entity.name;
26✔
45
  else if (entity.names) entityName = entity.names[0]?.name;
18!
46
  else if (entity.title) entityName = entity.title;
18✔
47
  else if (entity.titles) entityName = entity.titles[0]?.text;
6!
48
  else if (entity.body) entityName = `${entity.body.slice(0, 50)}...`;
6!
49
  else if (entity.descriptions) entityName = entity.descriptions[0].title;
6!
50

51
  // Format action verb
52
  let actionVerb = '';
26✔
53
  switch (notificationType) {
26✔
54
    case NOTIFICATION_TYPES.CREATE:
55
      actionVerb = 'created';
4✔
56
      break;
4✔
57

58
    case NOTIFICATION_TYPES.DELETE:
59
      actionVerb = 'deleted';
5✔
60
      break;
5✔
61
    case NOTIFICATION_TYPES.PERMANENT_DELETE:
62
      actionVerb = 'permanently deleted';
4✔
63
      break;
4✔
64

65
    case NOTIFICATION_TYPES.UPDATE:
66
      actionVerb = 'updated';
4✔
67
      break;
4✔
68

69
    case NOTIFICATION_TYPES.VALIDATE:
70
      actionVerb = 'validated';
4✔
71
      break;
4✔
72

73
    case NOTIFICATION_TYPES.RESTORE:
74
      actionVerb = 'restored';
4✔
75
      break;
4✔
76

77
    default:
78
      throw Error(`Unknown notification type: ${notificationType}`);
1✔
79
  }
80

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

87
  switch (notificationEntity) {
25!
88
    case NOTIFICATION_ENTITIES.CAVE:
89
      entityLink += `caves/${entity.id}`;
×
90
      break;
×
91

92
    case NOTIFICATION_ENTITIES.COMMENT:
93
    case NOTIFICATION_ENTITIES.HISTORY:
94
    case NOTIFICATION_ENTITIES.RIGGING:
95
      if (relatedCaveId) entityLink += `caves/${relatedCaveId}`;
6!
96
      else if (relatedEntranceId)
6!
97
        entityLink += `entrances/${relatedEntranceId}`;
6✔
98
      else
99
        throw Error(
×
100
          `Can't find related entity (cave or entrance) of the
101
          ${notificationType === NOTIFICATION_ENTITIES.COMMENT && 'comment'}
×
102
          ${notificationType === NOTIFICATION_ENTITIES.HISTORY && 'history'}
×
103
          ${
104
            notificationType === NOTIFICATION_ENTITIES.RIGGING && 'rigging'
×
105
          } with id ${entity.id}`
106
        );
107
      break;
6✔
108

109
    case NOTIFICATION_ENTITIES.DESCRIPTION:
110
      if (relatedCaveId) entityLink += `caves/${relatedCaveId}`;
6!
111
      else if (relatedEntranceId)
6!
112
        entityLink += `entrances/${relatedEntranceId}`;
6✔
113
      else if (relatedMassifId) entityLink += `massifs/${relatedMassifId}`;
×
114
      else
115
        throw Error(
×
116
          `Cant find related entity (cave, entrance or massif) of the description with id ${entity.id}`
117
        );
118
      break;
6✔
119

120
    case NOTIFICATION_ENTITIES.DOCUMENT:
121
      entityLink += `documents/${entity.id}`;
6✔
122
      break;
6✔
123

124
    case NOTIFICATION_ENTITIES.ENTRANCE:
125
      entityLink += `entrances/${entity.id}`;
×
126
      break;
×
127

128
    case NOTIFICATION_ENTITIES.LOCATION:
129
      if (relatedEntranceId) entityLink += `entrances/${relatedEntranceId}`;
×
130
      else
131
        throw Error(
×
132
          `Cant find related entity (entrance) of the location with id ${entity.id}`
133
        );
134
      break;
×
135

136
    case NOTIFICATION_ENTITIES.MASSIF:
137
      entityLink += `massifs/${entity.id}`;
6✔
138
      break;
6✔
139

140
    case NOTIFICATION_ENTITIES.ORGANIZATION:
141
      entityLink += `organizations/${entity.id}`;
×
142
      break;
×
143

144
    default:
145
      throw Error(`Unknown notification entity: ${notificationEntity}`);
1✔
146
  }
147

148
  await sails.helpers.sendEmail
24✔
149
    .with({
150
      allowResponse: false,
151
      emailSubject: 'Notification',
152
      i18n: req.i18n,
153
      recipientEmail: user.mail,
154
      viewName: 'notification',
155
      viewValues: {
156
        actionVerb,
157
        entityLink,
158
        entityName,
159
        entityType: notificationEntity,
160
        recipientName: user.nickname,
161
        subscriptionName: user.subscriptionName,
162
        subscriptionType: user.subscriptionType,
163
      },
164
    })
165
    .intercept('sendSESEmailError', () => {
166
      sails.log.error(
×
167
        `The email service has encountered an error while trying to notify user ${user.nickname} (id=${user.id}).`
168
      );
169
      return false;
×
170
    });
171
};
172

173
const getCountryAndMassifSubscribers = async (
6✔
174
  entityCountryId,
175
  entityMassifIds
176
) => {
177
  const countrySubscribers = [];
29✔
178
  const massifsSubscribers = [];
29✔
179
  if (entityCountryId) {
29✔
180
    const country =
181
      await TCountry.findOne(entityCountryId).populate('subscribedCavers');
1✔
182
    countrySubscribers.push(
1✔
183
      ...country.subscribedCavers.map((caver) => ({
1✔
184
        ...caver,
185
        subscriptionName: country.nativeName,
186
        subscriptionType: 'country',
187
      }))
188
    );
189
  }
190
  if (entityMassifIds) {
29!
191
    await Promise.all(
29✔
192
      entityMassifIds.map(async (massifId) => {
193
        const massif =
194
          await TMassif.findOne(massifId).populate('subscribedCavers');
10✔
195
        await NameService.setNames([massif], 'massif');
10✔
196
        massifsSubscribers.push(
10✔
197
          ...massif.subscribedCavers.map((caver) => ({
9✔
198
            ...caver,
199
            subscriptionType: 'massif',
200
            subscriptionName: massif.names[0]?.name,
201
          }))
202
        );
203
      })
204
    );
205
  }
206
  return { countrySubscribers, massifsSubscribers };
29✔
207
};
208

209
module.exports = {
6✔
210
  NOTIFICATION_ENTITIES,
211
  NOTIFICATION_TYPES,
212
  ...(process.env.NODE_ENV === 'test' ? { sendNotificationEmail } : undefined),
6!
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');
29✔
233

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

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

252
      // Format notification and populate entity
253
      const notification = await module.exports.populateEntities({
29✔
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];
29✔
265

266
      // Find massifs nor country concerned about the notification
267
      let entityMassifIds = [];
29✔
268
      let entityCountryId;
269

270
      const caveId = safeGetPropId('cave', populatedEntity);
29✔
271
      const entranceId = safeGetPropId('entrance', populatedEntity);
29✔
272
      const massifId = safeGetPropId('massif', populatedEntity);
29✔
273

274
      const getMassifIdsFromCave = async (id) =>
29✔
275
        (await CaveService.getMassifs(id)).map((m) => m.id);
21✔
276
      const getCountryId = (id) => safeGetPropId('country', id);
29✔
277

278
      switch (notificationEntity) {
29!
279
        case NOTIFICATION_ENTITIES.CAVE: {
280
          if (
7✔
281
            populatedEntity.entrances &&
14✔
282
            populatedEntity.entrances.length > 0
283
          ) {
284
            entityCountryId = getCountryId(populatedEntity?.entrances[0]);
3✔
285
          }
286
          entityMassifIds = await getMassifIdsFromCave(populatedEntity.id);
7✔
287
          break;
7✔
288
        }
289

290
        case NOTIFICATION_ENTITIES.COMMENT: {
291
          if (caveId) {
3!
292
            if (
×
293
              populatedEntity.cave.entrances &&
×
294
              populatedEntity.cave.entrances.length > 0
295
            ) {
296
              entityCountryId = getCountryId(
×
297
                populatedEntity?.cave.entrances[0]
298
              ); // Get country from first entrance (not perfect but good enough)
299
            }
300
            entityMassifIds = await getMassifIdsFromCave(caveId);
×
301
          } else if (entranceId) {
3!
302
            entityCountryId = getCountryId(populatedEntity?.entrance);
3✔
303
            entityMassifIds = await getMassifIdsFromCave(
3✔
304
              safeGetPropId('cave', populatedEntity.entrance)
305
            );
306
          } else {
307
            throw new Error(`Can't retrieve related cave or entrance id.`);
×
308
          }
309
          break;
3✔
310
        }
311

312
        case NOTIFICATION_ENTITIES.DESCRIPTION: {
313
          if (caveId) {
×
314
            if (
×
315
              populatedEntity.cave.entrances &&
×
316
              populatedEntity.cave.entrances.length > 0
317
            ) {
318
              entityCountryId = getCountryId(
×
319
                populatedEntity?.cave?.entrances[0]
320
              ); // Get country from first entrance (not perfect but good enough)
321
            }
322
            entityMassifIds = await getMassifIdsFromCave(caveId);
×
323
          } else if (entranceId) {
×
324
            entityCountryId = getCountryId(populatedEntity?.entrance);
×
325
            entityMassifIds = await getMassifIdsFromCave(
×
326
              safeGetPropId('cave', populatedEntity.entrance)
327
            );
328
          } else if (massifId) {
×
329
            entityMassifIds = [populatedEntity.massif];
×
330
          } else {
331
            throw new Error(
×
332
              `Can't retrieve related cave, entrance or massif id.`
333
            );
334
          }
335
          break;
×
336
        }
337
        case NOTIFICATION_ENTITIES.DOCUMENT: {
338
          if (caveId) {
4!
339
            if (
×
340
              populatedEntity.cave.entrances &&
×
341
              populatedEntity.cave.entrances.length > 0
342
            ) {
343
              entityCountryId = getCountryId(
×
344
                populatedEntity?.cave.entrances[0]
345
              ); // Get country from first entrance (not perfect but good enough)
346
            }
347
            entityMassifIds = await getMassifIdsFromCave(caveId);
×
348
          } else if (entranceId) {
4!
349
            entityCountryId = getCountryId(populatedEntity.entrance);
×
350
            entityMassifIds = await getMassifIdsFromCave(
×
351
              safeGetPropId('cave', populatedEntity.entrance)
352
            );
353
          } else if (massifId) {
4!
354
            entityMassifIds = [safeGetPropId('massif', populatedEntity)];
×
355
          }
356
          // A document can relate to no massif / cave / entrance
357
          break;
4✔
358
        }
359
        case NOTIFICATION_ENTITIES.ENTRANCE:
360
          entityCountryId = getCountryId(populatedEntity);
6✔
361
          if (populatedEntity?.cave) {
6!
362
            entityMassifIds = await getMassifIdsFromCave(
6✔
363
              safeGetPropId('cave', populatedEntity.entrance)
364
            );
365
          }
366
          break;
6✔
367
        case NOTIFICATION_ENTITIES.HISTORY: {
368
          if (caveId) {
3✔
369
            if (
1!
370
              populatedEntity.cave.entrances &&
1!
371
              populatedEntity.cave.entrances.length > 0
372
            ) {
373
              entityCountryId = getCountryId(
×
374
                populatedEntity?.cave?.entrances[0]
375
              ); // Get country from first entrance (not perfect but good enough)
376
            }
377
            entityMassifIds = await getMassifIdsFromCave(caveId);
1✔
378
          } else if (entranceId) {
2!
379
            entityCountryId = getCountryId(populatedEntity.entrance);
2✔
380
            entityMassifIds = await getMassifIdsFromCave(
2✔
381
              safeGetPropId('cave', populatedEntity.entrance)
382
            );
383
          } else {
384
            throw new Error(`Can't retrieve related cave or entrance id.`);
×
385
          }
386
          break;
3✔
387
        }
388
        case NOTIFICATION_ENTITIES.LOCATION: {
389
          if (entranceId) {
×
390
            entityCountryId = getCountryId(populatedEntity.entrance);
×
391
            entityMassifIds = await getMassifIdsFromCave(
×
392
              safeGetPropId('cave', populatedEntity.entrance)
393
            );
394
          } else {
395
            throw new Error(`Can't retrieve related entrance id.`);
×
396
          }
397
          break;
×
398
        }
399
        case NOTIFICATION_ENTITIES.MASSIF:
400
          entityMassifIds = [populatedEntity.id];
2✔
401
          break;
2✔
402
        case NOTIFICATION_ENTITIES.ORGANIZATION:
403
          entityCountryId = getCountryId(populatedEntity);
2✔
404
          break;
2✔
405

406
        case NOTIFICATION_ENTITIES.RIGGING: {
407
          if (caveId) {
2!
408
            if (
×
409
              populatedEntity.cave.entrances &&
×
410
              populatedEntity.cave.entrances.length > 0
411
            ) {
412
              entityCountryId = getCountryId(
×
413
                populatedEntity?.cave.entrances[0]
414
              ); // Get country from first entrance (not perfect but good enough)
415
            }
416
            entityMassifIds = await getMassifIdsFromCave(caveId);
×
417
          } else if (entranceId) {
2!
418
            entityCountryId = getCountryId(populatedEntity.entrance);
2✔
419
            entityMassifIds = await getMassifIdsFromCave(
2✔
420
              safeGetPropId('cave', populatedEntity.entrance)
421
            );
422
          } else {
423
            throw new Error(`Can't retrieve related cave or entrance id.`);
×
424
          }
425
          break;
2✔
426
        }
427
        default:
428
          throw new Error(
×
429
            `Can't find what to do with the following notification entity value: ${notificationEntity}`
430
          );
431
      }
432

433
      // Find subscribers to the entity.
434
      const { countrySubscribers, massifsSubscribers } =
435
        await getCountryAndMassifSubscribers(entityCountryId, entityMassifIds);
29✔
436
      // List users who will receive the notification
437
      const uniqueUsers = Array.from(
29✔
438
        new Set([
439
          // Don't notify the notifierId
440
          ...countrySubscribers.filter((u) => u.id !== notifierId),
1✔
441
          ...massifsSubscribers.filter((u) => u.id !== notifierId),
9✔
442
        ])
443
      );
444

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

461
          if (user.sendNotificationByEmail) {
10!
462
            await sendNotificationEmail(
×
463
              populatedEntity,
464
              notificationType,
465
              notificationEntity,
466
              req,
467
              user
468
            );
469
          }
470
          return true;
10✔
471
        })
472
      );
473
      return res;
29✔
474
    } catch (error) {
475
      // Fail silently to avoid sending an error to the user
UNCOV
476
      sails.log.error(
×
477
        `An error occurred when trying to notify subscribers: ${error.message} ${error.stack}`
478
      );
UNCOV
479
      return false;
×
480
    }
481
  },
482

483
  populateEntities: async (notification) => {
484
    const populatedNotification = notification;
40✔
485
    if (populatedNotification.cave) {
40✔
486
      await NameService.setNames([populatedNotification.cave], 'cave');
10✔
487
    }
488
    if (populatedNotification.comment) {
40✔
489
      populatedNotification.comment = await TComment.findOne(
5✔
490
        safeGetPropId('comment', notification)
491
      )
492
        .populate('cave')
493
        .populate('entrance');
494
    }
495
    if (populatedNotification.description) {
40!
496
      populatedNotification.description = await TDescription.findOne(
×
497
        safeGetPropId('description', notification)
498
      )
499
        .populate('cave')
500
        .populate('document')
501
        .populate('entrance')
502
        .populate('massif');
503
    }
504
    if (populatedNotification.document) {
40✔
505
      // Had to require in the function to avoid a circular dependency with notifySubscribers() in DocumentService.createDocument()
506
      // eslint-disable-next-line global-require
507
      const DocumentService = require('./DocumentService');
5✔
508
      const populatedDocuments = await DocumentService.getDocuments([
5✔
509
        safeGetPropId('document', notification),
510
      ]);
511
      populatedNotification.document = populatedDocuments[0];
5✔
512
    }
513
    if (populatedNotification.entrance) {
40✔
514
      await NameService.setNames([populatedNotification.entrance], 'entrance');
7✔
515
    }
516
    if (populatedNotification.grotto) {
40✔
517
      await NameService.setNames([populatedNotification.grotto], 'grotto');
2✔
518
    }
519
    if (populatedNotification.history) {
40✔
520
      populatedNotification.history = await THistory.findOne(
5✔
521
        safeGetPropId('history', notification)
522
      )
523
        .populate('cave')
524
        .populate('entrance');
525
    }
526
    if (populatedNotification.location) {
40!
527
      populatedNotification.location = await TLocation.findOne(
×
528
        safeGetPropId('location', notification)
529
      ).populate('entrance');
530
    }
531
    if (populatedNotification.massif) {
40✔
532
      await NameService.setNames([populatedNotification.massif], 'massif');
4✔
533
    }
534
    if (populatedNotification.rigging) {
40✔
535
      populatedNotification.rigging = await TRigging.findOne(
2✔
536
        safeGetPropId('rigging', notification)
537
      )
538
        .populate('entrance')
539
        .populate('cave');
540
    }
541
    return populatedNotification;
40✔
542
  },
543
};
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