• 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

93.75
/api/services/GeoLocService.js
1
const PUBLIC_ENTRANCES_IN_BOUNDS = `
2✔
2
  SELECT e.id as id, ne.name as name, e.city as city,
3
  e.region as region, e.longitude as longitude, e.latitude as latitude,
4
  c.size_coef as size_coef, e.id_cave as idCave, nc.name as nameCave, c.depth as depthCave,
5
  c.length as lengthCave
6
  FROM t_entrance as e
7
  LEFT JOIN t_name as ne ON ne.id_entrance = e.id
8
  LEFT JOIN t_name as nc ON nc.id_cave = e.id_cave
9
  LEFT JOIN t_cave as c ON c.Id = e.id_cave
10
  WHERE ST_Within(e.point_geom, ST_MakeEnvelope($1, $2, $3, $4, 4326))
11
  AND e.is_sensitive = false
12
  AND e.is_deleted = false
13
  ORDER BY size_coef DESC
14
  LIMIT $5;
15
`;
16

17
const PUBLIC_ENTRANCES_IN_BOUNDS_AND_MASSIF = `
2✔
18
  SELECT e.id as id, ne.name as name, e.city as city,
19
  e.region as region, e.longitude as longitude, e.latitude as latitude,
20
  c.size_coef as size_coef, e.id_cave as idCave, nc.name as nameCave, c.depth as depthCave,
21
  c.length as lengthCave
22
  FROM t_entrance as e
23
  LEFT JOIN t_name as ne ON ne.id_entrance = e.id
24
  LEFT JOIN t_name as nc ON nc.id_cave = e.id_cave
25
  LEFT JOIN t_cave as c ON c.Id = e.id_cave
26
  JOIN t_massif AS m ON m.id = $6
27
  WHERE ST_Within(e.point_geom, ST_MakeEnvelope($1, $2, $3, $4, 4326))
28
  AND ST_Contains(m.geog_polygon::geometry, e.point_geom)
29
  AND e.is_sensitive = false
30
  AND e.is_deleted = false
31
  ORDER BY size_coef DESC
32
  LIMIT $5;
33
`;
34
const PUBLIC_ENTRANCES_COORDINATES_IN_BOUNDS = `
2✔
35
  SELECT e.longitude as longitude, e.latitude as latitude
36
  FROM t_entrance as e
37
  WHERE ST_Within(e.point_geom, ST_MakeEnvelope($1, $2, $3, $4, 4326))
38
  AND e.is_sensitive = false
39
  AND e.is_deleted = false
40
  LIMIT $5;
41
`;
42

43
const PUBLIC_ENTRANCES_COORDINATES_IN_BOUNDS_AND_MASSIF = `
2✔
44
  SELECT e.longitude AS longitude, e.latitude AS latitude
45
  FROM t_entrance AS e
46
  JOIN t_massif AS m ON m.id = $6
47
  WHERE ST_Within(e.point_geom, ST_MakeEnvelope($1, $2, $3, $4, 4326))
48
  AND ST_Contains(m.geog_polygon::geometry, e.point_geom)
49
  AND e.is_sensitive = false
50
  AND e.is_deleted = false
51
  LIMIT $5;
52
`;
53

54
const NETWORKS_IN_BOUNDS = `
2✔
55
  SELECT c.id as id, COALESCE(nc.name, ne.name) as name, avg(en.longitude) as longitude, avg(en.latitude) as latitude
56
  FROM t_entrance as en
57
  INNER JOIN t_cave c ON c.id = en.id_cave
58
  LEFT JOIN t_name AS nc ON nc.id_cave = c.id
59
  LEFT JOIN t_name as ne ON ne.id_entrance = en.id
60
  WHERE ST_Within(en.point_geom, ST_MakeEnvelope($1, $2, $3, $4, 4326))
61
  AND en.is_sensitive = false
62
  AND en.is_deleted = false
63
  AND c.is_deleted = false
64
  GROUP BY c.id, COALESCE(nc.name, ne.name)
65
  HAVING count(en.id_cave) > 1
66
`;
67

68
const PUBLIC_NETWORKS_COORDINATES_IN_BOUNDS = `
2✔
69
  SELECT avg(en.longitude) as longitude, avg(en.latitude) as latitude
70
  FROM t_cave AS c
71
  LEFT JOIN t_entrance en ON c.id = en.id_cave
72
  WHERE ST_Within(en.point_geom, ST_MakeEnvelope($1, $2, $3, $4, 4326))
73
  AND en.is_sensitive = false
74
  AND en.is_deleted = false
75
  AND c.is_deleted = false
76
  GROUP BY c.id
77
  HAVING count(en.id_cave) > 1
78
  LIMIT $5;
79
`;
80

81
const CommonService = require('./CommonService');
2✔
82
const NameService = require('./NameService');
2✔
83

84
/**
85
 * return a light version of the networks
86
 * @param networks
87
 */
88
const formatNetworks = (networks) =>
2✔
89
  networks.map((network) => ({
×
90
    id: network.id,
91
    name: network.name,
92
    longitude: Number(network.longitude),
93
    latitude: Number(network.latitude),
94
  }));
95

96
/**
97
 * Format the quality entrances in a lighter version
98
 * Quality entrance stand for an entrance that won't be clustered
99
 * @param entrances
100
 */
101
const formatEntrances = (entrances) =>
2✔
102
  entrances.map((entrance) => ({
7✔
103
    id: entrance.id,
104
    name: entrance.name,
105
    city: entrance.city,
106
    region: entrance.region,
107
    caveId: entrance.idcave,
108
    caveName: entrance.namecave,
109
    depth: entrance.depthcave,
110
    length: entrance.lengthcave,
111
    longitude: parseFloat(entrance.longitude),
112
    latitude: parseFloat(entrance.latitude),
113
    quality: entrance.size_coef,
114
  }));
115

116
/**
117
 * Return a lighter version of the grottos
118
 * @param grottos
119
 */
120
const formatGrottos = (grottos) =>
2✔
121
  grottos.map((grotto) => ({
2✔
122
    id: grotto.id,
123
    name: grotto.name,
124
    address: grotto.address,
125
    longitude: parseFloat(grotto.longitude),
126
    latitude: parseFloat(grotto.latitude),
127
  }));
128

129
/**
130
 * Parse and validate the optional `massif` query parameter.
131
 * Returns { massifId, errorResponse } where errorResponse is null if valid.
132
 * If errorResponse is not null, the caller should return it immediately.
133
 */
134
const checkAndGetMassifParam = async (req, res) => {
2✔
135
  const rawMassifId = req.param('massif', null);
112✔
136
  const massifId = rawMassifId ? parseInt(rawMassifId, 10) : null;
112✔
137

138
  if (rawMassifId && (!Number.isFinite(massifId) || massifId < 1)) {
112!
139
    return {
×
140
      massifId: null,
141
      errorResponse: res.badRequest(
142
        'massif parameter must be a positive integer.'
143
      ),
144
    };
145
  }
146

147
  if (massifId) {
112✔
148
    const massif = await TMassif.findOne(massifId);
105✔
149
    if (!massif) {
105✔
150
      return {
2✔
151
        massifId: null,
152
        errorResponse: res.notFound({
153
          message: `Massif of id ${massifId} not found.`,
154
        }),
155
      };
156
    }
157
  }
158

159
  return { massifId, errorResponse: null };
110✔
160
};
161

162
// ====================================
163

164
module.exports = {
2✔
165
  checkAndGetMassifParam,
166
  checkAndGetCoordinatesParams: (req) => {
167
    let errorMessage = '';
135✔
168
    const errors = [];
135✔
169
    const neededParams = [
135✔
170
      { key: 'sw_lat', name: 'South west latitude', value: null },
171
      { key: 'sw_lng', name: 'South west longitude', value: null },
172
      { key: 'ne_lat', name: 'North east latitude', value: null },
173
      { key: 'ne_lng', name: 'North east longitude', value: null },
174
    ];
175

176
    const result = neededParams.map((param) => ({
540✔
177
      ...param,
178
      value: req.param(param.key, null),
179
    }));
180

181
    // Check null values
182
    const missingParams = result.filter((p) => p.value === null);
540✔
183
    if (missingParams.length > 0) {
135✔
184
      errorMessage = 'You must provide the following parameter(s): ';
9✔
185
      for (const missingParam of missingParams) {
9✔
186
        errors.push(`${missingParam.name} value on key ${missingParam.key}`);
13✔
187
      }
188
    } else {
189
      // Check valid values
190
      for (const param of result) {
126✔
191
        if (
504✔
192
          param.key.endsWith('lat') &&
1,006✔
193
          (param.value < -90 || param.value > 90)
194
        ) {
195
          errors.push(
5✔
196
            `${param.name} value must be between -90 & 90 (value found: ${param.value})`
197
          );
198
        }
199
        if (
504✔
200
          param.key.endsWith('lng') &&
1,005✔
201
          (param.value < -180 || param.value > 180)
202
        ) {
203
          errors.push(
5✔
204
            `${param.name} value must be between -180 & 180 (value found: ${param.value})`
205
          );
206
        }
207
      }
208
    }
209

210
    if (errors.length > 0) errorMessage += `${errors.join(', ')}.`;
135✔
211

212
    return {
135✔
213
      errorMessage,
214
      southWestBound: {
215
        lat: result.find((p) => p.key === 'sw_lat').value,
135✔
216
        lng: result.find((p) => p.key === 'sw_lng').value,
270✔
217
      },
218
      northEastBound: {
219
        lat: result.find((p) => p.key === 'ne_lat').value,
405✔
220
        lng: result.find((p) => p.key === 'ne_lng').value,
540✔
221
      },
222
    };
223
  },
224

225
  countEntrances: async (southWestBound, northEastBound) => {
226
    const result = await CommonService.query(
3✔
227
      `SELECT count(*) as count FROM t_entrance AS e
228
       WHERE ST_Within(e.point_geom, ST_MakeEnvelope($1, $2, $3, $4, 4326))
229
       AND e.is_sensitive = false
230
       AND e.is_deleted = false`,
231
      [
232
        southWestBound.lng,
233
        southWestBound.lat,
234
        northEastBound.lng,
235
        northEastBound.lat,
236
      ]
237
    );
238
    return parseInt(result.rows[0].count, 10);
3✔
239
  },
240

241
  getEntrancesCoordinates: async (
242
    southWestBound,
243
    northEastBound,
244
    limitEntrances,
245
    massifId = null
5✔
246
  ) => {
247
    const query = massifId
107✔
248
      ? PUBLIC_ENTRANCES_COORDINATES_IN_BOUNDS_AND_MASSIF
249
      : PUBLIC_ENTRANCES_COORDINATES_IN_BOUNDS;
250
    const params = [
107✔
251
      southWestBound.lng,
252
      southWestBound.lat,
253
      northEastBound.lng,
254
      northEastBound.lat,
255
      limitEntrances,
256
    ];
257
    if (massifId) {
107✔
258
      params.push(massifId);
102✔
259
    }
260
    const results = await CommonService.query(query, params);
107✔
261
    if (!results || results.rows.length <= 0) {
107✔
262
      return [];
85✔
263
    }
264
    const coordinates = results.rows;
22✔
265

266
    return coordinates.map((coord) => [
84✔
267
      Number(coord.longitude),
268
      Number(coord.latitude),
269
    ]);
270
  },
271

272
  getNetworksCoordinates: async (
273
    southWestBound,
274
    northEastBound,
275
    limitNetworks
276
  ) => {
277
    const results = await CommonService.query(
3✔
278
      PUBLIC_NETWORKS_COORDINATES_IN_BOUNDS,
279
      [
280
        southWestBound.lng,
281
        southWestBound.lat,
282
        northEastBound.lng,
283
        northEastBound.lat,
284
        limitNetworks,
285
      ]
286
    );
287
    if (!results || results.rows.length <= 0) {
3!
288
      return [];
3✔
289
    }
290
    const coordinates = results.rows;
×
291
    return coordinates.map((coord) => [
×
292
      Number(coord.longitude),
293
      Number(coord.latitude),
294
    ]);
295
  },
296

297
  /**
298
   * @param southWestBound
299
   * @param northEastBound
300
   * @param limitEntrances Max number of entrances that will be showed at a certain level of zoom
301
   * @returns {Promise<any>}
302
   */
303
  getEntrancesMap: async (
304
    southWestBound,
305
    northEastBound,
306
    limitEntrances,
307
    massifId = null
2✔
308
  ) => {
309
    const query = massifId
4✔
310
      ? PUBLIC_ENTRANCES_IN_BOUNDS_AND_MASSIF
311
      : PUBLIC_ENTRANCES_IN_BOUNDS;
312
    const params = [
4✔
313
      southWestBound.lng,
314
      southWestBound.lat,
315
      northEastBound.lng,
316
      northEastBound.lat,
317
      limitEntrances,
318
    ];
319
    if (massifId) {
4✔
320
      params.push(massifId);
1✔
321
    }
322
    const results = await CommonService.query(query, params);
4✔
323
    if (!results || results.rows.length <= 0) {
4✔
324
      return [];
3✔
325
    }
326
    return formatEntrances(results.rows);
1✔
327
  },
328

329
  getGrottosMap: async (southWestBound, northEastBound) => {
330
    const parameters = {
2✔
331
      isDeleted: false,
332
      latitude: {
333
        '>': southWestBound.lat,
334
        '<': northEastBound.lat,
335
      },
336
      longitude: {
337
        '>': southWestBound.lng,
338
        '<': northEastBound.lng,
339
      },
340
    };
341
    const grottos = await TGrotto.find(parameters);
2✔
342
    await NameService.setNames(grottos, 'grotto');
2✔
343
    return formatGrottos(grottos);
2✔
344
  },
345

346
  getNetworksMap: async (southWestBound, northEastBound) => {
347
    const results = await CommonService.query(NETWORKS_IN_BOUNDS, [
3✔
348
      southWestBound.lng,
349
      southWestBound.lat,
350
      northEastBound.lng,
351
      northEastBound.lat,
352
    ]);
353
    if (!results || results.rows.length <= 0) {
3!
354
      return [];
3✔
355
    }
356
    return formatNetworks(results.rows);
×
357
  },
358
};
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