Skip to content

Backend/readings#32

Merged
Stcwal merged 4 commits intomainfrom
backend/readings
Apr 8, 2026
Merged

Backend/readings#32
Stcwal merged 4 commits intomainfrom
backend/readings

Conversation

@SindreJB
Copy link
Copy Markdown
Collaborator

@SindreJB SindreJB commented Apr 8, 2026

No description provided.

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 8, 2026

Backend Test Results

132 tests   131 ✅  7s ⏱️
 34 suites    0 💤
 34 files      1 ❌

For more details on these failures, see this check.

Results for commit 1261532.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new backend API for temperature reading statistics to support charting/analytics, including grouped time-series points per unit and a companion list of deviation events.

Changes:

  • Introduces /api/readings/stats endpoint (ADMIN/MANAGER) returning series + deviations payload.
  • Implements stats aggregation logic in TemperatureReadingService and adds repository queries to fetch readings for stats.
  • Adds DTOs and tests covering service aggregation, controller JSON contract, and security annotations.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
backend/src/main/java/backend/fullstack/temperature/api/TemperatureReadingController.java Adds GET /readings/stats endpoint with query params and response wrapper.
backend/src/main/java/backend/fullstack/temperature/application/TemperatureReadingService.java Implements getReadingStats aggregation, deviation mapping, and time bucketing.
backend/src/main/java/backend/fullstack/temperature/infrastructure/TemperatureReadingRepository.java Adds JPQL queries to fetch readings for stats (with/without unit filters).
backend/src/main/java/backend/fullstack/temperature/api/dto/TemperatureReadingDeviationResponse.java New DTO representing deviation event entries.
backend/src/main/java/backend/fullstack/temperature/api/dto/TemperatureReadingStatsGroupBy.java New enum for time grouping resolution.
backend/src/main/java/backend/fullstack/temperature/api/dto/TemperatureReadingStatsPointResponse.java New DTO for averaged stats points.
backend/src/main/java/backend/fullstack/temperature/api/dto/TemperatureReadingStatsResponse.java New top-level stats response DTO (series + deviations).
backend/src/main/java/backend/fullstack/temperature/api/dto/TemperatureReadingStatsSeriesResponse.java New DTO representing per-unit time series.
backend/src/test/java/backend/fullstack/temperature/application/TemperatureReadingServiceTest.java Adds tests for stats aggregation behavior and unitId sanitization behavior.
backend/src/test/java/backend/fullstack/temperature/api/TemperatureReadingControllerTest.java Adds MockMvc contract tests for /api/readings/stats param parsing and JSON structure.
backend/src/test/java/backend/fullstack/temperature/api/TemperatureReadingControllerSecurityAnnotationsTest.java Adds @PreAuthorize assertion for the new controller method.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +58 to +70
@Query("""
SELECT r
FROM TemperatureReading r
WHERE r.organization.id = :organizationId
AND (:from IS NULL OR r.recordedAt >= :from)
AND (:to IS NULL OR r.recordedAt <= :to)
ORDER BY r.unit.name ASC, r.recordedAt ASC
""")
List<TemperatureReading> findForStatsByOrganizationAndRange(
@Param("organizationId") Long organizationId,
@Param("from") LocalDateTime from,
@Param("to") LocalDateTime to
);
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TemperatureReading.unit is FetchType.LAZY, but this stats query returns TemperatureReading entities without fetching unit. Since getReadingStats() immediately accesses r.unit.id/name and thresholds, this will trigger N+1 selects. Consider changing the query to JOIN FETCH r.unit (or using a DTO/projection query) to load unit data in the initial query.

Copilot uses AI. Check for mistakes.
Comment on lines +72 to +86
@Query("""
SELECT r
FROM TemperatureReading r
WHERE r.organization.id = :organizationId
AND r.unit.id IN :unitIds
AND (:from IS NULL OR r.recordedAt >= :from)
AND (:to IS NULL OR r.recordedAt <= :to)
ORDER BY r.unit.name ASC, r.recordedAt ASC
""")
List<TemperatureReading> findForStatsByOrganizationAndUnitIdsAndRange(
@Param("organizationId") Long organizationId,
@Param("unitIds") List<Long> unitIds,
@Param("from") LocalDateTime from,
@Param("to") LocalDateTime to
);
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same N+1 concern as the unfiltered stats query: this query returns readings with unit still lazy, but the service reads unit.id/name and thresholds for every row. Prefer JOIN FETCH r.unit (and possibly reference the join alias in ORDER BY) or switch to a projection to avoid extra queries per unit/reading.

Copilot uses AI. Check for mistakes.
Comment on lines +81 to +109
@Transactional(readOnly = true)
public TemperatureReadingStatsResponse getReadingStats(
Long organizationId,
List<Long> unitIds,
LocalDateTime from,
LocalDateTime to,
TemperatureReadingStatsGroupBy groupBy
) {
List<Long> distinctUnitIds = sanitizeUnitIds(unitIds);
List<TemperatureReading> readings = distinctUnitIds == null
? readingRepository.findForStatsByOrganizationAndRange(organizationId, from, to)
: readingRepository.findForStatsByOrganizationAndUnitIdsAndRange(organizationId, distinctUnitIds, from, to);

Map<Long, List<TemperatureReading>> readingsByUnit = readings.stream()
.collect(Collectors.groupingBy(reading -> reading.getUnit().getId()));

List<TemperatureReadingStatsSeriesResponse> series = readingsByUnit.values().stream()
.sorted(Comparator.comparing(unitReadings -> unitReadings.get(0).getUnit().getName(), String.CASE_INSENSITIVE_ORDER))
.map(unitReadings -> toSeries(unitReadings, groupBy))
.toList();

List<TemperatureReadingDeviationResponse> deviations = readings.stream()
.filter(TemperatureReading::isDeviation)
.sorted(Comparator.comparing(TemperatureReading::getRecordedAt).reversed())
.map(this::toDeviationResponse)
.toList();

return new TemperatureReadingStatsResponse(series, deviations);
}
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new getReadingStats method is indented differently than the rest of the class (annotation, method signature, and closing brace). This hurts readability and may trip formatting/lint rules; align it to the same indentation level as listReadings/createReading.

Suggested change
@Transactional(readOnly = true)
public TemperatureReadingStatsResponse getReadingStats(
Long organizationId,
List<Long> unitIds,
LocalDateTime from,
LocalDateTime to,
TemperatureReadingStatsGroupBy groupBy
) {
List<Long> distinctUnitIds = sanitizeUnitIds(unitIds);
List<TemperatureReading> readings = distinctUnitIds == null
? readingRepository.findForStatsByOrganizationAndRange(organizationId, from, to)
: readingRepository.findForStatsByOrganizationAndUnitIdsAndRange(organizationId, distinctUnitIds, from, to);
Map<Long, List<TemperatureReading>> readingsByUnit = readings.stream()
.collect(Collectors.groupingBy(reading -> reading.getUnit().getId()));
List<TemperatureReadingStatsSeriesResponse> series = readingsByUnit.values().stream()
.sorted(Comparator.comparing(unitReadings -> unitReadings.get(0).getUnit().getName(), String.CASE_INSENSITIVE_ORDER))
.map(unitReadings -> toSeries(unitReadings, groupBy))
.toList();
List<TemperatureReadingDeviationResponse> deviations = readings.stream()
.filter(TemperatureReading::isDeviation)
.sorted(Comparator.comparing(TemperatureReading::getRecordedAt).reversed())
.map(this::toDeviationResponse)
.toList();
return new TemperatureReadingStatsResponse(series, deviations);
}
@Transactional(readOnly = true)
public TemperatureReadingStatsResponse getReadingStats(
Long organizationId,
List<Long> unitIds,
LocalDateTime from,
LocalDateTime to,
TemperatureReadingStatsGroupBy groupBy
) {
List<Long> distinctUnitIds = sanitizeUnitIds(unitIds);
List<TemperatureReading> readings = distinctUnitIds == null
? readingRepository.findForStatsByOrganizationAndRange(organizationId, from, to)
: readingRepository.findForStatsByOrganizationAndUnitIdsAndRange(organizationId, distinctUnitIds, from, to);
Map<Long, List<TemperatureReading>> readingsByUnit = readings.stream()
.collect(Collectors.groupingBy(reading -> reading.getUnit().getId()));
List<TemperatureReadingStatsSeriesResponse> series = readingsByUnit.values().stream()
.sorted(Comparator.comparing(unitReadings -> unitReadings.get(0).getUnit().getName(), String.CASE_INSENSITIVE_ORDER))
.map(unitReadings -> toSeries(unitReadings, groupBy))
.toList();
List<TemperatureReadingDeviationResponse> deviations = readings.stream()
.filter(TemperatureReading::isDeviation)
.sorted(Comparator.comparing(TemperatureReading::getRecordedAt).reversed())
.map(this::toDeviationResponse)
.toList();
return new TemperatureReadingStatsResponse(series, deviations);
}

Copilot uses AI. Check for mistakes.
Comment on lines +89 to +93
List<Long> distinctUnitIds = sanitizeUnitIds(unitIds);
List<TemperatureReading> readings = distinctUnitIds == null
? readingRepository.findForStatsByOrganizationAndRange(organizationId, from, to)
: readingRepository.findForStatsByOrganizationAndUnitIdsAndRange(organizationId, distinctUnitIds, from, to);

Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getReadingStats fetches all readings in the requested range and aggregates them in-memory. For large ranges/orgs this can become slow and memory-heavy; consider moving bucketing/AVG aggregation to the DB (and fetching deviations via a separate bounded query) to keep response time predictable.

Copilot uses AI. Check for mistakes.
@Stcwal Stcwal merged commit 47f4e80 into main Apr 8, 2026
4 of 7 checks passed
@SindreJB SindreJB deleted the backend/readings branch April 10, 2026 11:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants