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

GrottoCenter / grottocenter-api / 23021407638

12 Mar 2026 08:03PM UTC coverage: 85.34% (-0.07%) from 85.405%
23021407638

push

github

ClemRz
chore(ci): upgrade GitHub Actions to Node.js 24

Upgrade GitHub Actions to Node.js 24 compatible versions:
- actions/checkout v4 -> v6
- actions/setup-node v4 -> v6
- actions/upload-artifact v4 -> v6
- actions/download-artifact v4 -> v8

2669 of 3289 branches covered (81.15%)

Branch coverage included in aggregate %.

5615 of 6418 relevant lines covered (87.49%)

33.35 hits per line

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

95.5
/api/services/CaveService.js
1
const CommonService = require('./CommonService');
5✔
2
const DocumentService = require('./DocumentService');
5✔
3
const DescriptionService = require('./DescriptionService');
5✔
4
const NameService = require('./NameService');
5✔
5
const SearchService = require('./SearchService');
5✔
6
const NotificationService = require('./NotificationService');
5✔
7
const RecentChangeService = require('./RecentChangeService');
5✔
8

9
const GET_CUMULATED_LENGTH = `
5✔
10
  SELECT SUM(c.length) as sum_length, COUNT(c.length) as nb_data
11
  FROM t_entrance e
12
  JOIN t_cave c ON e.id_cave = c.id
13
  WHERE c.length IS NOT NULL
14
  AND c.is_deleted = false
15
  AND e.is_deleted = false
16
`;
17

18
module.exports = {
5✔
19
  /**
20
   * @param {Array<Object>} caves caves to set
21
   *
22
   * @returns {Promise} the caves with their attribute "entrances" completed
23
   */
24
  setEntrances: async (caves) => {
25
    const entrances = await TEntrance.find()
52✔
26
      .where({ cave: { in: caves.map((c) => c.id) } })
17✔
27
      .populate('names');
28
    for (const cave of caves) {
52✔
29
      cave.entrances = entrances.filter((e) => e.cave === cave.id);
38✔
30
    }
31
  },
32

33
  /**
34
   *
35
   * @param {Object} req
36
   * @param {Object} cleanedData cave-only related data
37
   * @param {Object} nameData name data (should contain an author, text and language attributes)
38
   * @param {Array[Object]} [descriptionsData] descriptions data (for each description,
39
   *  should contain an author, title, text and language attributes)
40
   * @throws Sails ORM errors (see https://sailsjs.com/documentation/concepts/models-and-orm/errors)
41
   *
42
   * @returns {Promise} the created cave
43
   */
44
  createCave: async (req, caveData, nameData, descriptionsData) => {
45
    const res = await sails.getDatastore().transaction(async (db) => {
4✔
46
      // Create cave
47
      const createdCave = await TCave.create({ ...caveData })
4✔
48
        .fetch()
49
        .usingConnection(db);
50

51
      // Format & create name
52
      await TName.create({
3✔
53
        ...nameData,
54
        cave: createdCave.id,
55
        dateInscription: new Date(),
56
        isMain: true,
57
      }).usingConnection(db);
58

59
      // Format & create descriptions
60
      if (descriptionsData) {
3✔
61
        descriptionsData.map(async (d) => {
2✔
62
          const desc = await TDescription.create({
5✔
63
            ...d,
64
            cave: createdCave.id,
65
            dateInscription: new Date(),
66
          }).usingConnection(db);
67
          return desc;
5✔
68
        });
69
      }
70

71
      return createdCave;
3✔
72
    });
73

74
    const populatedCave = await module.exports.getPopulatedCave(res.id);
3✔
75
    module.exports.updateInSearch(populatedCave);
3✔
76

77
    await RecentChangeService.setNameCreate(
3✔
78
      'cave',
79
      res.id,
80
      req.token.id,
81
      nameData.name
82
    );
83

84
    await NotificationService.notifySubscribers(
3✔
85
      req,
86
      populatedCave,
87
      req.token.id,
88
      NotificationService.NOTIFICATION_TYPES.CREATE,
89
      NotificationService.NOTIFICATION_ENTITIES.CAVE
90
    );
91

92
    return populatedCave;
3✔
93
  },
94

95
  // Extract everything from the request body except id and dateInscription
96
  getConvertedDataFromClient: (req) => ({
2✔
97
    // The TCave.create() function doesn't work with TCave field alias. See TCave.js Model
98
    depth: req.param('depth'),
99
    documents: req.param('documents'),
100
    isDiving: req.param('isDiving'),
101
    latitude: req.param('latitude'),
102
    longitude: req.param('longitude'),
103
    caveLength: req.param('length'),
104
    massif: req.param('massif'),
105
    temperature: req.param('temperature'),
106
  }),
107

108
  /**
109
   * Get the massifs in which the cave is contained.
110
   * If there is none, return an empty array.
111
   *
112
   * Note: this query routes through t_entrance to leverage the GiST index on
113
   * point_geom. A cave with no entrance will therefore return no massifs, which
114
   * is acceptable because every cave in the domain model must have at least one
115
   * entrance.
116
   *
117
   * @param {*} caveId
118
   * @returns [Massif]
119
   */
120
  getMassifs: async (caveId) => {
121
    try {
106✔
122
      const query = `
106✔
123
      SELECT DISTINCT m.*
124
      FROM t_massif AS m
125
      JOIN t_entrance AS e ON ST_Contains(m.geog_polygon::geometry, e.point_geom)
126
      WHERE e.id_cave = $1
127
      AND e.is_deleted = false
128
      AND m.is_deleted = false
129
    `;
130
      const queryResult = await CommonService.query(query, [caveId]);
106✔
131
      return queryResult.rows;
106✔
132
    } catch (e) {
133
      // Fail silently (happens when the point_geom is null for example)
134
      return [];
×
135
    }
136
  },
137

138
  /**
139
   *
140
   * @param
141
   * @returns {Object} the cumulated length of the caves present in the database whose value length is not null
142
   *                and the number of data on which this value is calculated
143
   *                or null if no result or something went wrong
144
   */
145
  getCumulatedLength: async () => {
146
    try {
2✔
147
      const queryResult = await CommonService.query(GET_CUMULATED_LENGTH, []);
2✔
148
      const result = queryResult.rows;
2✔
149
      if (result.length > 0) {
2!
150
        return result[0];
2✔
151
      }
152
      return null;
×
153
    } catch (e) {
154
      return null;
×
155
    }
156
  },
157

158
  /**
159
   * Get organizations that explored the cave
160
   * @param {number} caveId
161
   * @returns {Promise<Array>} Array of organizations with id and name
162
   */
163
  getExploringOrganizations: async (caveId) => {
164
    const query = `
47✔
165
      SELECT g.*
166
      FROM t_grotto g
167
             JOIN j_grotto_cave_explorer j ON g.id = j.id_grotto
168
      WHERE j.id_cave = $1
169
        AND g.is_deleted = false
170
    `;
171
    const result = await CommonService.query(query, [caveId]);
47✔
172
    const grottos = result.rows;
47✔
173

174
    if (!grottos || grottos.length === 0) {
47✔
175
      return [];
22✔
176
    }
177

178
    await NameService.setNames(grottos, 'grotto');
25✔
179
    return grottos;
25✔
180
  },
181

182
  async getPopulatedCave(caveId, subEntitiesWhere = {}) {
27✔
183
    const cave = await TCave.findOne(caveId)
29✔
184
      .populate('author')
185
      .populate('reviewer')
186
      .populate('names')
187
      .populate('descriptions')
188
      .populate('entrances')
189
      .populate('documents');
190

191
    if (!cave) return null;
29✔
192

193
    [cave.massifs, cave.descriptions, cave.documents] = await Promise.all([
25✔
194
      module.exports.getMassifs(cave.id),
195
      DescriptionService.getCaveDescriptions(cave.id, subEntitiesWhere),
196
      DocumentService.getDocuments(cave.documents?.map((d) => d.id) ?? []),
12!
197
    ]);
198

199
    cave.exploringOrganizations =
25✔
200
      await module.exports.getExploringOrganizations(cave.id);
201

202
    const nameAsyncArr = [
25✔
203
      NameService.setNames(cave?.entrances, 'entrance'),
204
      NameService.setNames(cave?.massifs, 'massif'),
205
    ];
206
    if (cave.names.length === 0) {
25✔
207
      // As the name service will also get the entrance name if needed
208
      nameAsyncArr.push(NameService.setNames([cave], 'cave'));
5✔
209
    }
210
    await Promise.all(nameAsyncArr);
25✔
211

212
    // TODO What about other linked entities ?
213
    // - histories
214
    // - riggings
215
    // - comments
216
    // - partneringGrottos
217

218
    return cave;
25✔
219
  },
220

221
  async deleteInSearch(caveId) {
222
    await SearchService.deleteDocument('caves', caveId);
6✔
223
  },
224

225
  async updateInSearch(populatedCave) {
226
    const { names, ...c } = populatedCave;
9✔
227
    const cave = {
9✔
228
      id: c.id,
229
      dateInscription: c.dateInscription,
230
      dateReviewed: c.dateReviewed,
231
      authorId: c.author.id,
232
      author: c.author.nickname,
233
      reviewerId: c.reviewer?.id,
234
      reviewer: c.reviewer?.nickname,
235
      name: names[0].name,
236
      language: names[0].language,
237
      depth: c.depth,
238
      length: c.caveLength,
239
      temperature: c.temperature,
240
      isDiving: c.isDiving,
241
    };
242
    await SearchService.updateDocument('caves', cave);
9✔
243
  },
244

245
  async permanentlyDeleteCave(cave, shouldMergeInto, mergeIntoId) {
246
    await TCave.update({ redirectTo: cave.id }).set({
8✔
247
      redirectTo: shouldMergeInto ? mergeIntoId : null,
8✔
248
    });
249
    await TNotification.destroy({ cave: cave.id });
8✔
250

251
    if (cave.documents.length > 0) {
8✔
252
      if (shouldMergeInto) {
2✔
253
        const newDocuments = cave.documents.map((e) => e.id);
1✔
254
        await TCave.addToCollection(mergeIntoId, 'documents', newDocuments);
1✔
255
      }
256
      await HDocument.update({ cave: cave.id }).set({ cave: null });
2✔
257
      await TCave.updateOne(cave.id).set({ documents: [] });
2✔
258
    }
259

260
    if (cave.entrances.length > 0 && shouldMergeInto) {
8✔
261
      const newEntrances = cave.entrances.map((e) => e.id);
1✔
262
      await TCave.addToCollection(mergeIntoId, 'entrances', newEntrances);
1✔
263
    }
264
    await HEntrance.update({ cave: cave.id }).set({ cave: null });
8✔
265

266
    if (cave.descriptions.length > 0) {
8✔
267
      if (shouldMergeInto) {
2✔
268
        await TDescription.update({ cave: cave.id }).set({
1✔
269
          cave: mergeIntoId,
270
        });
271
        await HDescription.update({ cave: cave.id }).set({
1✔
272
          cave: mergeIntoId,
273
        });
274
      } else {
275
        await TDescription.destroy({ cave: cave.id }); // TDescription first soft delete
1✔
276
        await HDescription.destroy({ cave: cave.id });
1✔
277
        await TDescription.destroy({ cave: cave.id });
1✔
278
      }
279
    }
280

281
    await NameService.permanentDelete({ cave: cave.id });
8✔
282

283
    await HCave.destroy({ id: cave.id });
8✔
284
    await TCave.destroyOne({ id: cave.id }); // Hard delete
8✔
285
  },
286
};
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