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

GrottoCenter / grottocenter-api / 5123216243

pending completion
5123216243

push

github

vmarseguerra
feat(entities): list latest contributions

774 of 2004 branches covered (38.62%)

Branch coverage included in aggregate %.

123 of 123 new or added lines in 11 files covered. (100.0%)

2775 of 5089 relevant lines covered (54.53%)

14.54 hits per line

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

5.38
/api/services/RecentChangeService.js
1
const CommonService = require('./CommonService');
5✔
2

3
const GROUP_MAX_TIME_DIFF_S = 60 * 60 * 6; // 6 Hours
5✔
4

5
async function removeOlderChanges() {
6
  const query = `DELETE FROM t_last_change WHERE date_change < current_timestamp - interval '1 month';`;
×
7
  await CommonService.query(query);
×
8
}
9

10
// When creating a cave, entrance, massif, document or grotto the name is created after the entity
11
// So the name is updated afterward
12
async function setNameCreate(entityType, entityId, authorId, name) {
13
  const query = `
8✔
14
  UPDATE t_last_change
15
  SET name = $1
16
  WHERE type_entity = $2 AND type_change = 'create' AND id_entity = $3 AND id_author = $4 AND date_change > current_timestamp - interval '1 minute';
17
  `;
18
  await CommonService.query(query, [name, entityType, entityId, authorId]);
8✔
19
}
20

21
// To make the change list more relevant we groups change event when they are from the same author and about the same entity
22
function groupChanges(changes) {
23
  const authorChanges = {};
×
24
  // Grouping order:
25
  // - author
26
  // - related entity
27
  // - time
28
  // Change type 'create' or 'restore' have priority over 'update'
29
  // Change type 'delete' is never grouped
30

31
  const createNewGroupFromChange = (c) => ({
×
32
    date: c.date_change,
33
    authorId: c.id_author,
34
    author: c.nickname,
35
    mainEntityType: c.type_related_entity ?? c.type_entity,
×
36
    mainEntityId: c.id_related_entity ?? c.id_entity,
×
37
    mainAction: c.id_related_entity ? null : c.type_change, // Null in case it is a change on a sub entity
×
38
    subEntityTypes: c.id_related_entity ? [c.type_entity] : [],
×
39
    subAction: c.id_related_entity ? c.type_change : null, // Null when no sub entities
×
40
    name: c.name, // Can be the real entity name or the related entity name
41
  });
42

43
  const addChangeToExistingGroup = (g, c) => {
×
44
    if (
×
45
      !c.id_related_entity &&
×
46
      g.mainAction === 'update' &&
47
      ['create', 'restore'].includes(c.type_change)
48
    )
49
      g.mainAction = c.type_change; // eslint-disable-line no-param-reassign
×
50
    if (c.id_related_entity && !g.subEntityTypes.includes(c.type_entity))
×
51
      g.subEntityTypes.push(c.type_entity);
×
52
    if (c.id_related_entity && g.subAction !== c.type_change)
×
53
      g.subAction = 'change'; // eslint-disable-line no-param-reassign
×
54
  };
55

56
  for (const change of changes) {
×
57
    if (!authorChanges[change.id_author]) {
×
58
      authorChanges[change.id_author] = [createNewGroupFromChange(change)];
×
59
      continue; // eslint-disable-line no-continue
×
60
    }
61

62
    const authorsChanges = authorChanges[change.id_author];
×
63
    const previousChangeForThisEntity = authorsChanges.find(
×
64
      (e) =>
65
        e.mainEntityId === (change.id_related_entity ?? change.id_entity) &&
×
66
        e.mainEntityType === (change.type_related_entity ?? change.type_entity)
×
67
    );
68

69
    if (
×
70
      !previousChangeForThisEntity ||
×
71
      Math.abs(previousChangeForThisEntity.date - change.date_change) >
72
        GROUP_MAX_TIME_DIFF_S * 1000 ||
73
      change.type_change === 'delete' ||
74
      previousChangeForThisEntity.mainAction === 'delete'
75
    ) {
76
      authorsChanges.unshift(createNewGroupFromChange(change));
×
77
      continue; // eslint-disable-line no-continue
×
78
    }
79
    addChangeToExistingGroup(previousChangeForThisEntity, change);
×
80
  }
81

82
  const allGroups = Object.values(authorChanges).flat();
×
83
  return allGroups.sort((a, b) => b.date - a.date);
×
84
}
85

86
async function getRecent() {
87
  // The t_last_change table is populated by trigger on the other main tables
88
  const query = `
×
89
  SELECT tbl.*, author.nickname FROM t_last_change tbl
90
  LEFT JOIN t_caver author ON tbl.id_author = author.id
91
  ORDER BY date_change DESC
92
  `;
93
  const rep = await CommonService.query(query);
×
94

95
  // When creating/updating a entrance the associated cave is also created/updated (when not part of a network)
96
  // As cave changes are duplicate we filter out them
97
  const changes = rep.rows.filter((e, i, a) => {
×
98
    if (e.type_entity !== 'cave') return true;
×
99

100
    // Is the cave change is after the entrance change ?
101
    if (
×
102
      i < a.length - 2 &&
×
103
      a[i + 1].date_change - e.date_change < 5000 &&
104
      a[i + 1].id_author === e.id_author &&
105
      a[i + 1].type_entity === 'entrance'
106
    )
107
      return false;
×
108

109
    // Is the cave change is before the entrance change ?
110
    if (
×
111
      i > 0 &&
×
112
      e.date_change - a[i - 1].date_change < 5000 &&
113
      a[i - 1].id_author === e.id_author &&
114
      a[i - 1].type_entity === 'entrance'
115
    )
116
      return false;
×
117

118
    return true;
×
119
  });
120

121
  // 5% chance to also remove older changes
122
  if (Math.random() < 0.05) removeOlderChanges();
×
123

124
  return groupChanges(changes);
×
125
}
126

127
module.exports = {
5✔
128
  getRecent,
129
  setNameCreate,
130
};
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