Skip to content

Commit 56346be

Browse files
authored
[O2B-952] Fixed a bug where the tally of replies was not accurately reflected on the log overview pages (#1279)
* [O2B-952] add custom query that counts a log's total reply count * [O2B-952] make reply counter available to all log overviews * [O2B-952] fix: addressing SQL injection vulnerability in the existing getTagDistributionQuery * [O2B-952] reverted code simplification because of breaking tests * [O2B-952] missed addChildrenCountByRootLog that has been replaced * [O2B-952] Implemented the query as a combined query to reduce amount of queries sent * [O2B-952] add a safety check to prevent the method's execution when there are now logs in the rows
1 parent 9749e03 commit 56346be

6 files changed

Lines changed: 50 additions & 22 deletions

File tree

lib/database/repositories/LogRepository.js

Lines changed: 42 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,39 @@ const { timestampToMysql } = require('../../server/utilities/timestampToMysql.js
2121
* @param {Period} the period in which logs should have been created
2222
* @return {string} the query
2323
*/
24-
const getTagDistributionQuery = ({ from, to }) => `
24+
const getTagDistributionQuery = ({ from, to }) => sequelize.query(`
2525
SELECT t.text tag, COUNT(*) count
2626
FROM logs l
2727
INNER JOIN log_tags lt on l.id = lt.log_id
2828
INNER JOIN tags t on lt.tag_id = t.id
29-
WHERE l.created_at >= '${timestampToMysql(from)}'
30-
AND l.created_at < '${timestampToMysql(to)}'
29+
WHERE l.created_at >= :fromTimeStamp
30+
AND l.created_at < :toTimeStamp
3131
AND t.archived_at IS NULL
3232
GROUP BY t.text
3333
ORDER BY COUNT(*) DESC, t.text
34-
`;
34+
`, {
35+
replacements: { fromTimeStamp: timestampToMysql(from), toTimeStamp: timestampToMysql(to) },
36+
});
37+
38+
/**
39+
* Returns the query to fetch the reply count for a given array of logIds.
40+
*
41+
* @param {Array<number>} logIds The log ids for which the reply count will be fetched.
42+
* @returns {Promise<Array<{ originLogId: number, replyCounter: number }>>} A promise resolving to an array of objects,
43+
* each containing for each originLogId the reply count.
44+
*/
45+
const getLogsWithReplyCountQuery = (logIds) => sequelize.query(`
46+
WITH RECURSIVE log_tree (originLogId, id) AS (
47+
SELECT id originId, id FROM logs
48+
WHERE id IN (:logIds)
49+
UNION ALL
50+
SELECT originLogId, l.id FROM logs as l
51+
JOIN log_tree AS lt ON lt.id = l.parent_log_id
52+
)
53+
SELECT originLogId, COUNT(*) - 1 as replyCount FROM log_tree GROUP BY originLogId;
54+
`, {
55+
replacements: { logIds },
56+
});
3557

3658
/**
3759
* Sequelize implementation of the LogRepository.
@@ -45,21 +67,24 @@ class LogRepository extends Repository {
4567
}
4668

4769
/**
48-
* Adds a count of root log by their amount of children onto every applicable log.
70+
* Adds a 'replies' property to the given rows array which denotes the amount of children for each applicable log.
4971
*
50-
* @param {Array} rows The collection of rows already fetched
51-
* @param {QueryBuilder} queryBuilder The QueryBuilder to use.
52-
* @returns {Promise} Promise object representing the full mock data
72+
* @param {Array} rows The collection of log rows already fetched.
73+
* @returns {Promise} Promise returning logs with a 'replies' property, denoting the reply count for each log.
5374
*/
54-
async addChildrenCountByRootLog(rows, queryBuilder = new QueryBuilder()) {
55-
const rootLogIds = [...new Set(rows.map((row) => row.rootLogId))];
56-
queryBuilder
57-
.where('rootLogId').oneOf(...rootLogIds)
58-
.set('group', ['rootLogId']);
75+
async addChildrenCountByLog(rows) {
76+
const logIds = [...new Set(rows.map((row) => row.id))];
77+
78+
if (logIds.length === 0) {
79+
return rows;
80+
}
81+
82+
const [results] = await getLogsWithReplyCountQuery(logIds);
83+
84+
results.forEach((result) => {
85+
const count = result.replyCount || 0;
86+
const rowToAttachTo = rows.find(({ id }) => id === result.originLogId);
5987

60-
const replies = await this.count(queryBuilder);
61-
replies.forEach(({ rootLogId, count }) => {
62-
const rowToAttachTo = rows.find(({ id }) => id === rootLogId);
6388
if (rowToAttachTo) {
6489
rowToAttachTo.replies = count;
6590
}
@@ -109,7 +134,7 @@ class LogRepository extends Repository {
109134
* @return {Promise<{tag: string, count: number}[]>} the tag and their amount
110135
*/
111136
async getTagDistribution(period) {
112-
const [rows] = await sequelize.query(getTagDistributionQuery(period));
137+
const [rows] = await getTagDistributionQuery(period);
113138
return rows;
114139
}
115140
}

lib/server/services/log/LogService.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ class LogService {
8787

8888
/**
8989
* Return all the logs that correspond to a given list of ids with a default list of shallow relations
90+
* and also adds the number of replies to each log.
9091
*
9192
* @param {number[]} logIds the list of ids of logs to fetch
9293
* @return {Promise<Log[]>} the resulting logs
@@ -98,7 +99,9 @@ class LogService {
9899
.include({ association: 'lhcFills', attributes: ['fillNumber'] })
99100
.include({ association: 'environments', attributes: ['id'] });
100101

101-
return (await LogRepository.findAll(logQueryBuilder)).map(logAdapter.toEntity);
102+
const logs = await LogRepository.findAll(logQueryBuilder);
103+
const logEntities = logs.map(logAdapter.toEntity);
104+
return LogRepository.addChildrenCountByLog(logEntities);
102105
}
103106

104107
/**

lib/usecases/log/GetAllLogsUseCase.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ class GetAllLogsUseCase {
245245
return LogRepository.findAndCountAll(queryBuilder);
246246
});
247247

248-
const rowsWithReplies = await LogRepository.addChildrenCountByRootLog(rows);
248+
const rowsWithReplies = await LogRepository.addChildrenCountByLog(rows);
249249

250250
return {
251251
count,

lib/usecases/run/GetLogsByRunUseCase.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ class GetLogsByRunUseCase {
4545
});
4646

4747
if (logs) {
48-
const logsWithReplies = await LogRepository.addChildrenCountByRootLog(logs);
48+
const logsWithReplies = await LogRepository.addChildrenCountByLog(logs);
4949
return logsWithReplies.map(logAdapter.toEntity);
5050
} else {
5151
return null;

lib/usecases/subsystem/GetLogsBySubsystemUseCase.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ class GetLogsBySubsystemUseCase {
4545
});
4646

4747
if (logs) {
48-
const logsWithReplies = await LogRepository.addChildrenCountByRootLog(logs);
48+
const logsWithReplies = await LogRepository.addChildrenCountByLog(logs);
4949
return logsWithReplies.map(logAdapter.toEntity);
5050
} else {
5151
return null;

lib/usecases/tag/GetLogsByTagUseCase.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ class GetLogsByTagUseCase {
6363
},
6464
});
6565

66-
const logsWithCount = await LogRepository.addChildrenCountByRootLog(logs);
66+
const logsWithCount = await LogRepository.addChildrenCountByLog(logs);
6767

6868
return { logs: logsWithCount, count };
6969
});

0 commit comments

Comments
 (0)