Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion api/controllers/v1/massif/create.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,10 +110,17 @@ module.exports = async (req, res) => {
});

if (newMassif.isSensitive) {
// Resolve the containing massifs in one batch so the fan-out does not run a
// spatial query per entrance.
const massifsByEntrance =
await MassifService.findMassifsByEntranceIds(updatedEntranceIds);
await Promise.all(
updatedEntranceIds.map(async (id) => {
const populated = await EntranceService.getPopulatedEntrance(id);
if (populated) await EntranceService.updateInSearch(populated);
if (populated)
await EntranceService.updateInSearch(populated, {
massifs: massifsByEntrance[id] ?? [],
});
})
);
}
Expand Down
9 changes: 7 additions & 2 deletions api/controllers/v1/massif/mark-sensitive.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,18 @@ module.exports = async (req, res) => {
req.token.id
);

// Update search index for each affected entrance
// Update search index for each affected entrance. The containing massifs are
// resolved in one batch so the fan-out does not run a spatial query per entrance.
const massifsByEntrance =
await MassifService.findMassifsByEntranceIds(updatedEntranceIds);
await Promise.all(
updatedEntranceIds.map(async (id) => {
const populatedEntrance =
await EntranceService.getPopulatedEntrance(id);
if (populatedEntrance) {
await EntranceService.updateInSearch(populatedEntrance);
await EntranceService.updateInSearch(populatedEntrance, {
massifs: massifsByEntrance[id] ?? [],
});
}
})
);
Expand Down
33 changes: 8 additions & 25 deletions api/dbSync/entities/entrance.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ const {
} = require('../../../config/constants/entrance');
const { getQualityData } = require('../../utils/computeEntranceDataQuality');
const { computeCommentsRating } = require('../../utils/commentsRating');
const CommonService = require('../../services/CommonService');

const query = `
SELECT
Expand Down Expand Up @@ -108,30 +107,14 @@ async function* processRows(source) {

await Promise.all(joins.map((e) => exportUtils.joinMany(e)));

// Spatial join: find massifs containing each entrance
const ids = rows.map((r) => r.id);
const massifQuery = `
SELECT e.id AS id_entrance, m.id AS id_massif, n.name AS massif_name, n.id_language AS language
FROM t_entrance e
JOIN t_massif m ON ST_Contains(m.geog_polygon::geometry, e.point_geom)
LEFT JOIN t_name n ON n.id_massif = m.id AND n.is_main = true AND n.is_deleted = false
WHERE e.id = ANY($1::int[])
AND e.is_deleted = false
AND m.is_deleted = false
`;
const { rows: massifRows } = await CommonService.query(massifQuery, [ids]);
const massifsByEntrance = {};
for (const mr of massifRows) {
if (!massifsByEntrance[mr.id_entrance]) {
massifsByEntrance[mr.id_entrance] = [];
}
massifsByEntrance[mr.id_entrance].push({
id: mr.id_massif,
name: mr.massif_name,
language: mr.language,
isDeleted: false,
});
}
// Spatial join: find massifs containing each entrance.
// Had to require in the function to avoid a circular dependency: SearchService
// requires this module at load time to read its search schema.
// eslint-disable-next-line global-require
const MassifService = require('../../services/MassifService');
const massifsByEntrance = await MassifService.findMassifsByEntranceIds(
rows.map((r) => r.id)
);

for (const row of rows) {
if (row.geology) row.geology = row.geology.trim();
Expand Down
2 changes: 1 addition & 1 deletion api/dbSync/entities/massif.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const query = `
LEFT JOIN t_name n ON n.id_massif = m.id AND n.is_main = true
LEFT JOIN t_caver a ON a.id = m.id_author
LEFT JOIN t_caver r ON r.id = m.id_reviewer
LEFT JOIN t_entrance e ON e.point_geom && m.geog_polygon AND ST_Contains(m.geog_polygon::geometry, e.point_geom) AND e.is_deleted = false
LEFT JOIN t_entrance e ON e.point_geom && m.geog_polygon::geometry AND ST_Contains(m.geog_polygon::geometry, e.point_geom) AND e.is_deleted = false
WHERE m.is_deleted = false
GROUP BY m.id, m.geog_polygon, n.name, n.id_language, r.nickname, a.nickname
ORDER BY m.id ASC
Expand Down
2 changes: 1 addition & 1 deletion api/services/CaveService.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ module.exports = {
const query = `
SELECT DISTINCT m.*
FROM t_massif AS m
JOIN t_entrance AS e ON ST_Contains(m.geog_polygon::geometry, e.point_geom)
JOIN t_entrance AS e ON e.point_geom && m.geog_polygon AND ST_Contains(m.geog_polygon::geometry, e.point_geom)
WHERE e.id_cave = $1
AND e.is_deleted = false
AND m.is_deleted = false
Expand Down
41 changes: 30 additions & 11 deletions api/services/EntranceService.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@ const INTEREST_ENTRANCES_QUERY =
// query to get a random entrance of interest
const RANDOM_ENTRANCE_QUERY = `${INTEREST_ENTRANCES_QUERY} ORDER BY RANDOM() LIMIT 1`;

// query to get the massifs containing a single entrance.
// The `&&` bounding-box pre-filter is what lets PostGIS use the GiST index on
// t_massif(geog_polygon); without it every massif polygon is tested exactly.
const MASSIFS_CONTAINING_ENTRANCE_QUERY = `
SELECT m.id, n.name, n.id_language AS language
FROM t_massif m
JOIN t_entrance e ON e.point_geom && m.geog_polygon AND ST_Contains(m.geog_polygon::geometry, e.point_geom)
LEFT JOIN t_name n ON n.id_massif = m.id AND n.is_main = true AND n.is_deleted = false
WHERE e.id = $1 AND e.is_deleted = false AND m.is_deleted = false
`;

const CommonService = require('./CommonService');
const SearchService = require('./SearchService');
const NotificationService = require('./NotificationService');
Expand Down Expand Up @@ -413,7 +424,15 @@ module.exports = {
await SearchService.deleteDocument('entrances', entranceId);
},

async updateInSearch(populatedEntrance) {
/**
* @param {*} populatedEntrance
* @param {object} [options]
* @param {Array} [options.massifs] Containing massifs, already resolved by the
* caller. Supply this from bulk paths that hold a list of entrance ids (see
* MassifService.findMassifsByEntranceIds) to skip the per-entrance spatial
* lookup; pass an empty array for an entrance in no massif.
*/
async updateInSearch(populatedEntrance, { massifs } = {}) {
// Warning: All linked entities may contain sensitive information (same as in document).
// For example, the complete caver object for the 'author' and 'reviewer' fields.
// Although we could leave them intact, since search results also pass through the converter,
Expand Down Expand Up @@ -468,7 +487,8 @@ module.exports = {
entrance.longitude = null;
}

// Compute data quality score and fetch massifs in parallel (independent queries)
// Compute data quality score and fetch massifs in parallel (independent queries).
// The spatial lookup is skipped when the caller already resolved the massifs.
const [qualityRows, massifRows] = await Promise.all([
CommonService.query(
`SELECT general_latest_date_of_update, general_nb_contributions,
Expand All @@ -481,25 +501,24 @@ module.exports = {
FROM v_data_quality_compute_entrance WHERE id_entrance = $1 ORDER BY id_massif ASC LIMIT 1`,
[rawEntrance.id]
),
CommonService.query(
`SELECT m.id, n.name, n.id_language AS language
FROM t_massif m
JOIN t_entrance e ON ST_Contains(m.geog_polygon::geometry, e.point_geom)
LEFT JOIN t_name n ON n.id_massif = m.id AND n.is_main = true AND n.is_deleted = false
WHERE e.id = $1 AND e.is_deleted = false AND m.is_deleted = false`,
[rawEntrance.id]
),
massifs
? null
: CommonService.query(MASSIFS_CONTAINING_ENTRANCE_QUERY, [
rawEntrance.id,
]),
]);
entrance.dataQuality = qualityRows?.rows?.[0]
? getQualityData(qualityRows.rows[0])
: 0;
entrance.massifs =
massifs ??
massifRows?.rows?.map((r) => ({
id: r.id,
name: r.name,
language: r.language,
isDeleted: false,
})) ?? [];
})) ??
[];

await SearchService.updateDocument('entrances', entrance);
},
Expand Down
2 changes: 2 additions & 0 deletions api/services/GeoLocService.js
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ const MASSIFS_IN_BOUNDS = `
SELECT COUNT(e.id)::integer
FROM t_entrance AS e
WHERE e.is_deleted = false
AND e.point_geom && m.geog_polygon::geometry
AND ST_Contains(m.geog_polygon::geometry, e.point_geom)
) AS "entranceCount",
(
Expand All @@ -173,6 +174,7 @@ const MASSIFS_IN_BOUNDS = `
JOIN t_cave AS c ON c.id = e.id_cave
WHERE e.is_deleted = false
AND c.is_deleted = false
AND e.point_geom && m.geog_polygon::geometry
AND ST_Contains(m.geog_polygon::geometry, e.point_geom)
GROUP BY c.id
HAVING COUNT(e.id) > 1
Expand Down
Loading
Loading