Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

first version of the averages API #3762

Merged
merged 9 commits into from
Oct 25, 2024
Merged

first version of the averages API #3762

merged 9 commits into from
Oct 25, 2024

Conversation

Baalmart
Copy link
Contributor

@Baalmart Baalmart commented Oct 25, 2024

Description

first version of the averages API

Changes Made

  • first version of the averages API

Testing

  • Tested locally
  • Tested against staging environment
  • Relevant tests passed: [List test names]

Affected Services

  • Which services were modified:
    • Device Registry

Endpoints Ready for Testing

  • New endpoints ready for testing:
    • Measurements Averages by Site ID: {{baseUrl}}/api/v2/devices/measurements/sites/{SITE_ID}/averages
    • Reading Averages by Site ID: {{baseUrl}}/api/v2/devices/readings/sites/{SITE_ID}/averages

API Documentation Updated?

  • Yes, API documentation was updated
  • No, API documentation does not need updating

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features

    • Introduced new methods to retrieve average readings and events for specified sites.
    • Added a new endpoint to access average measurements at /sites/:site_id/averages.
    • Implemented a method to calculate air quality analytics, including daily averages and compliance with WHO guidelines.
    • Added a rate limiter to the new endpoint to manage request rates effectively.
  • Bug Fixes

    • Enhanced error handling and logging for average measurement requests.
  • Documentation

    • Updated validation rules for new query parameters in the API.

Copy link
Contributor

coderabbitai bot commented Oct 25, 2024

📝 Walkthrough
📝 Walkthrough

Walkthrough

The changes introduce two new methods, listReadingAverages and listAverages, to the createEvent controller, allowing for the retrieval of average readings and events. A static method getAirQualityAverages is added to the Event model for calculating air quality averages over the past two weeks. Additionally, a new GET endpoint /sites/:site_id/averages is established in the routes, complete with validation for incoming request parameters. The utility functions are updated to incorporate caching logic for the new averages functionality.

Changes

File Path Change Summary
src/device-registry/controllers/create-event.js Added methods listReadingAverages and listAverages for handling requests to list average readings and events.
src/device-registry/models/Event.js Added static method getAirQualityAverages(siteId, next) for calculating air quality averages over two weeks.
src/device-registry/routes/v2/measurements.js Introduced new GET endpoint /sites/:site_id/averages with validation for various parameters.
src/device-registry/utils/create-event.js Added methods listAverages and listReadingAverages for retrieving air quality averages; updated generateCacheID method.
src/device-registry/models/Reading.js Added static method getAirQualityAnalytics(siteId, next) for comprehensive air quality analytics.
src/device-registry/routes/v2/readings.js Added new route /sites/:site_id/averages with enhanced validation for incoming requests.

Possibly related issues

Possibly related PRs

Suggested reviewers

  • NicholasTurner23
  • BenjaminSsempala

🎉 In the realm of data, where averages play,
A new method emerges, brightening the day.
From sites to air quality, we gather with glee,
Listing the averages, as clear as can be!
With routes and validations, we pave the way,
To insights and metrics, come join the fray! 🌟


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

codecov bot commented Oct 25, 2024

Codecov Report

Attention: Patch coverage is 3.11111% with 218 lines in your changes missing coverage. Please review.

Project coverage is 11.71%. Comparing base (783e5ce) to head (a8180e7).
Report is 109 commits behind head on staging.

Files with missing lines Patch % Lines
src/device-registry/models/Reading.js 1.33% 74 Missing ⚠️
src/device-registry/utils/create-event.js 0.00% 69 Missing ⚠️
src/device-registry/controllers/create-event.js 0.00% 54 Missing ⚠️
src/device-registry/models/Event.js 5.00% 19 Missing ⚠️
src/device-registry/routes/v2/measurements.js 75.00% 1 Missing ⚠️
src/device-registry/routes/v2/readings.js 66.66% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff             @@
##           staging    #3762      +/-   ##
===========================================
- Coverage    11.84%   11.71%   -0.14%     
===========================================
  Files          113      113              
  Lines        14938    15162     +224     
  Branches       274      274              
===========================================
+ Hits          1770     1776       +6     
- Misses       13168    13386     +218     
Files with missing lines Coverage Δ
src/device-registry/routes/v2/measurements.js 36.43% <75.00%> (+1.23%) ⬆️
src/device-registry/routes/v2/readings.js 42.85% <66.66%> (+0.35%) ⬆️
src/device-registry/models/Event.js 6.73% <5.00%> (-0.08%) ⬇️
src/device-registry/controllers/create-event.js 2.01% <0.00%> (-0.11%) ⬇️
src/device-registry/utils/create-event.js 3.32% <0.00%> (-0.25%) ⬇️
src/device-registry/models/Reading.js 16.76% <1.33%> (-11.81%) ⬇️

Copy link
Contributor

Device registry changes in this PR available for preview here

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 11

🧹 Outside diff range and nitpick comments (1)
src/device-registry/controllers/create-event.js (1)

1234-1261: Add time range validation and response documentation.

The averaging functionality should include:

  1. Validation for time range parameters to ensure meaningful averages
  2. JSDoc documentation describing the response format and time window

Add validation and documentation:

+/**
+ * List average measurements for sites/devices
+ * @param {Object} req - Express request object
+ * @param {Object} req.query
+ * @param {string} [req.query.start_date] - Start date for averaging window (ISO format)
+ * @param {string} [req.query.end_date] - End date for averaging window (ISO format)
+ * @returns {Object} Response containing averages and metadata
+ */
 listAverages: async (req, res, next) => {
+  // Validate time range
+  const { start_date, end_date } = req.query;
+  if (start_date && end_date) {
+    const start = new Date(start_date);
+    const end = new Date(end_date);
+    if (isNaN(start.getTime()) || isNaN(end.getTime()) || start > end) {
+      return res.status(httpStatus.BAD_REQUEST).json({
+        success: false,
+        errors: { message: "Invalid time range provided" },
+        message: "Bad Request Error",
+      });
+    }
+  }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between f14bacb and f5a1cec.

📒 Files selected for processing (4)
  • src/device-registry/controllers/create-event.js (1 hunks)
  • src/device-registry/models/Event.js (1 hunks)
  • src/device-registry/routes/v2/measurements.js (1 hunks)
  • src/device-registry/utils/create-event.js (3 hunks)
🔇 Additional comments (3)
src/device-registry/routes/v2/measurements.js (1)

Line range hint 1-24: Enhance API security with additional headers and request validation.

While the current implementation includes basic security measures, consider adding:

  1. Additional security headers
  2. Request size limits
  3. More robust error handling

[security]
Add security middleware at the router level:

const helmet = require('helmet');
const express = require('express');
const router = express.Router();

// Add security headers
router.use(helmet());

// Limit request size
router.use(express.json({ limit: '10kb' }));

// Global error handler
router.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({
    success: false,
    message: 'Internal Server Error',
    errors: {
      message: process.env.NODE_ENV === 'production' 
        ? 'An unexpected error occurred' 
        : err.message
    }
  });
});
src/device-registry/utils/create-event.js (2)

Line range hint 2131-2157: LGTM: Cache ID generation update is consistent

The addition of the averages parameter to the cache ID generation follows the existing pattern and maintains consistency with other parameters.


818-840: Consider adding rate limiting and improving cache security

The caching mechanism could benefit from additional security measures:

  1. Consider implementing rate limiting to prevent abuse
  2. Add cache key versioning to handle breaking changes
  3. Consider using shorter cache expiration for sensitive data

Let's verify the cache configuration:

Comment on lines 1262 to 1280
res.status(httpStatus.INTERNAL_SERVER_ERROR).json({
success: false,
errors: {
message: `Unable to process measurements for the provided measurement IDs`,
},
message: "Internal Server Error",
});
}
} catch (error) {
logger.error(`🐛🐛 Internal Server Error ${error.message}`);
next(
new HttpError(
"Internal Server Error",
httpStatus.INTERNAL_SERVER_ERROR,
{ message: error.message }
)
);
return;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Enhance error handling with better categorization and logging.

The error handling could be improved with:

  1. More specific error categories for better client-side handling
  2. Structured error logging for better debugging
 } catch (error) {
+  const errorContext = {
+    method: 'listAverages',
+    params: req.params,
+    query: req.query,
+    error: error.message,
+    stack: error.stack,
+  };
-  logger.error(`🐛🐛 Internal Server Error ${error.message}`);
+  logger.error('Internal Server Error in listAverages', errorContext);
   next(
     new HttpError(
       "Internal Server Error",
       httpStatus.INTERNAL_SERVER_ERROR,
-      { message: error.message }
+      {
+        message: error.message,
+        code: 'AVERAGES_ERROR',
+        context: errorContext,
+      }
     )
   );
   return;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
res.status(httpStatus.INTERNAL_SERVER_ERROR).json({
success: false,
errors: {
message: `Unable to process measurements for the provided measurement IDs`,
},
message: "Internal Server Error",
});
}
} catch (error) {
logger.error(`🐛🐛 Internal Server Error ${error.message}`);
next(
new HttpError(
"Internal Server Error",
httpStatus.INTERNAL_SERVER_ERROR,
{ message: error.message }
)
);
return;
}
res.status(httpStatus.INTERNAL_SERVER_ERROR).json({
success: false,
errors: {
message: `Unable to process measurements for the provided measurement IDs`,
},
message: "Internal Server Error",
});
}
} catch (error) {
const errorContext = {
method: 'listAverages',
params: req.params,
query: req.query,
error: error.message,
stack: error.stack,
};
logger.error('Internal Server Error in listAverages', errorContext);
next(
new HttpError(
"Internal Server Error",
httpStatus.INTERNAL_SERVER_ERROR,
{
message: error.message,
code: 'AVERAGES_ERROR',
context: errorContext,
}
)
);
return;
}

Comment on lines +1198 to +1218
listAverages: async (req, res, next) => {
try {
const errors = extractErrorsFromRequest(req);
if (errors) {
next(
new HttpError("bad request errors", httpStatus.BAD_REQUEST, errors)
);
return;
}

const request = req;
const defaultTenant = constants.DEFAULT_TENANT || "airqo";
request.query.tenant = isEmpty(req.query.tenant)
? defaultTenant
: req.query.tenant;

request.query.recent = "no";
request.query.metadata = "site_id";
request.query.averages = "events";
request.query.brief = "yes";
const { cohort_id, grid_id } = { ...req.query, ...req.params };
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider centralizing the default tenant configuration.

The default tenant value "airqo" is duplicated across multiple controller methods. Consider moving this to a single configuration constant to improve maintainability.

-const defaultTenant = constants.DEFAULT_TENANT || "airqo";
+const { DEFAULT_TENANT: defaultTenant = "airqo" } = constants;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
listAverages: async (req, res, next) => {
try {
const errors = extractErrorsFromRequest(req);
if (errors) {
next(
new HttpError("bad request errors", httpStatus.BAD_REQUEST, errors)
);
return;
}
const request = req;
const defaultTenant = constants.DEFAULT_TENANT || "airqo";
request.query.tenant = isEmpty(req.query.tenant)
? defaultTenant
: req.query.tenant;
request.query.recent = "no";
request.query.metadata = "site_id";
request.query.averages = "events";
request.query.brief = "yes";
const { cohort_id, grid_id } = { ...req.query, ...req.params };
listAverages: async (req, res, next) => {
try {
const errors = extractErrorsFromRequest(req);
if (errors) {
next(
new HttpError("bad request errors", httpStatus.BAD_REQUEST, errors)
);
return;
}
const request = req;
const { DEFAULT_TENANT: defaultTenant = "airqo" } = constants;
request.query.tenant = isEmpty(req.query.tenant)
? defaultTenant
: req.query.tenant;
request.query.recent = "no";
request.query.metadata = "site_id";
request.query.averages = "events";
request.query.brief = "yes";
const { cohort_id, grid_id } = { ...req.query, ...req.params };

Comment on lines 1198 to 1314
.withMessage("the site_id should be provided")
.bail()
.notEmpty()
.withMessage("the provided site_id cannot be empty")
.bail()
.trim()
.isMongoId()
.withMessage("the site_id must be an object ID")
.bail()
.customSanitizer((value) => {
return ObjectId(value);
}),
],
]),
oneOf([
[
query("startTime")
.optional()
.notEmpty()
.withMessage("startTime cannot be empty IF provided")
.bail()
.trim()
.isISO8601({ strict: true, strictSeparator: true })
.withMessage("startTime must be a valid datetime."),
query("endTime")
.optional()
.notEmpty()
.withMessage("endTime cannot be empty IF provided")
.bail()
.trim()
.isISO8601({ strict: true, strictSeparator: true })
.withMessage("endTime must be a valid datetime."),
query("frequency")
.optional()
.notEmpty()
.withMessage("the frequency cannot be empty if provided")
.bail()
.trim()
.toLowerCase()
.isIn(["hourly", "daily", "raw", "minute"])
.withMessage(
"the frequency value is not among the expected ones which include: hourly, daily, minute and raw"
),
query("format")
.optional()
.notEmpty()
.withMessage("the format cannot be empty if provided")
.bail()
.trim()
.toLowerCase()
.isIn(["json", "csv"])
.withMessage(
"the format value is not among the expected ones which include: csv and json"
),
query("external")
.optional()
.notEmpty()
.withMessage("external cannot be empty IF provided")
.bail()
.trim()
.toLowerCase()
.isIn(["yes", "no"])
.withMessage(
"the external value is not among the expected ones which include: no and yes"
),
query("recent")
.optional()
.notEmpty()
.withMessage("recent cannot be empty IF provided")
.bail()
.trim()
.toLowerCase()
.isIn(["yes", "no"])
.withMessage(
"the recent value is not among the expected ones which include: no and yes"
),
query("metadata")
.optional()
.notEmpty()
.withMessage("metadata cannot be empty IF provided")
.bail()
.trim()
.toLowerCase()
.isIn(["site", "site_id", "device", "device_id"])
.withMessage(
"valid values include: site, site_id, device and device_id"
),
query("test")
.optional()
.notEmpty()
.withMessage("test cannot be empty IF provided")
.bail()
.trim()
.toLowerCase()
.isIn(["yes", "no"])
.withMessage("valid values include: YES and NO"),
],
]),
eventController.listAverages
);
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider adding documentation and rate limiting for the averages endpoint.

While the implementation follows good validation practices, consider:

  1. Adding JSDoc comments to document the route's purpose, expected response format, and example usage
  2. Implementing rate limiting for this endpoint as average calculations can be resource-intensive

Add documentation above the route:

/**
 * @route GET /sites/:site_id/averages
 * @description Get average measurements for a specific site
 * @param {string} site_id - MongoDB ObjectId of the site
 * @query {string} [tenant] - Optional tenant identifier
 * @query {string} [startTime] - ISO8601 start time
 * @query {string} [endTime] - ISO8601 end time
 * @query {string} [frequency] - Data frequency (hourly|daily|raw|minute)
 * @returns {object} Average measurements data
 */

Consider implementing rate limiting using Express middleware:

const rateLimit = require('express-rate-limit');

const averagesLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100 // limit each IP to 100 requests per windowMs
});

router.get('/sites/:site_id/averages', averagesLimiter, /* ... */);

Comment on lines +1231 to +1311
query("startTime")
.optional()
.notEmpty()
.withMessage("startTime cannot be empty IF provided")
.bail()
.trim()
.isISO8601({ strict: true, strictSeparator: true })
.withMessage("startTime must be a valid datetime."),
query("endTime")
.optional()
.notEmpty()
.withMessage("endTime cannot be empty IF provided")
.bail()
.trim()
.isISO8601({ strict: true, strictSeparator: true })
.withMessage("endTime must be a valid datetime."),
query("frequency")
.optional()
.notEmpty()
.withMessage("the frequency cannot be empty if provided")
.bail()
.trim()
.toLowerCase()
.isIn(["hourly", "daily", "raw", "minute"])
.withMessage(
"the frequency value is not among the expected ones which include: hourly, daily, minute and raw"
),
query("format")
.optional()
.notEmpty()
.withMessage("the format cannot be empty if provided")
.bail()
.trim()
.toLowerCase()
.isIn(["json", "csv"])
.withMessage(
"the format value is not among the expected ones which include: csv and json"
),
query("external")
.optional()
.notEmpty()
.withMessage("external cannot be empty IF provided")
.bail()
.trim()
.toLowerCase()
.isIn(["yes", "no"])
.withMessage(
"the external value is not among the expected ones which include: no and yes"
),
query("recent")
.optional()
.notEmpty()
.withMessage("recent cannot be empty IF provided")
.bail()
.trim()
.toLowerCase()
.isIn(["yes", "no"])
.withMessage(
"the recent value is not among the expected ones which include: no and yes"
),
query("metadata")
.optional()
.notEmpty()
.withMessage("metadata cannot be empty IF provided")
.bail()
.trim()
.toLowerCase()
.isIn(["site", "site_id", "device", "device_id"])
.withMessage(
"valid values include: site, site_id, device and device_id"
),
query("test")
.optional()
.notEmpty()
.withMessage("test cannot be empty IF provided")
.bail()
.trim()
.toLowerCase()
.isIn(["yes", "no"])
.withMessage("valid values include: YES and NO"),
],
Copy link
Contributor

@coderabbitai coderabbitai bot Oct 25, 2024

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Reduce code duplication by extracting common validation rules.

The validation rules for query parameters (startTime, endTime, frequency, etc.) are duplicated across multiple routes. Consider extracting these into reusable validation chains.

Create a validation utilities module:

// validation/measurement-params.js
const { query } = require('express-validator');

const commonValidators = {
  timeRange: [
    query('startTime')
      .optional()
      .notEmpty()
      .withMessage('startTime cannot be empty IF provided')
      .bail()
      .trim()
      .isISO8601({ strict: true, strictSeparator: true })
      .withMessage('startTime must be a valid datetime.'),
    query('endTime')
      /* ... similar validation ... */
  ],
  frequency: [
    query('frequency')
      /* ... existing validation ... */
  ],
  // ... other common validators
};

module.exports = commonValidators;

Then use in routes:

const { timeRange, frequency } = require('./validation/measurement-params');

router.get('/sites/:site_id/averages',
  oneOf([/* tenant validation */]),
  oneOf([/* site_id validation */]),
  timeRange,
  frequency,
  /* other validators */,
  eventController.listAverages
);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

generate a github issue for this.

Copy link
Contributor

Choose a reason for hiding this comment

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

@Baalmart, I've created the GitHub issue as you requested: Issue #3768.

🔗 Analysis chain

@Baalmart, I've created a GitHub issue for this as you requested.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
gh issue create \
  --title "Reduce code duplication in validation rules" \
  --body "Code duplication has been observed in the validation rules for query parameters across multiple routes.

This issue has been requested by @Baalmart.

Related PR: [#3762](https://github.com/airqo-platform/AirQo-api/pull/3762)

Comment URL: https://github.com/airqo-platform/AirQo-api/pull/3762#discussion_r1816357999" \
  --assignee Baalmart

Length of output: 472

@@ -2541,6 +2541,132 @@ eventSchema.statics.signal = async function(filter) {
}
};

eventSchema.statics.getAirQualityAverages = async function(siteId, next) {
Copy link
Contributor

@coderabbitai coderabbitai bot Oct 25, 2024

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Remove the next parameter from the static method

Including next in the model's static method getAirQualityAverages is unconventional for Mongoose models. It's better to handle errors by throwing them, allowing the caller to manage error propagation.

Apply this diff to refactor the method signature:

-eventSchema.statics.getAirQualityAverages = async function(siteId, next) {
+eventSchema.statics.getAirQualityAverages = async function(siteId) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
eventSchema.statics.getAirQualityAverages = async function(siteId, next) {
eventSchema.statics.getAirQualityAverages = async function(siteId) {

Copy link
Contributor Author

Choose a reason for hiding this comment

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

generate a Github issue for this

Copy link
Contributor

Choose a reason for hiding this comment

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

@Baalmart, thank you for creating the GitHub issue. This will help address the refactoring suggestion.

(^.^)/

🔗 Analysis chain

@Baalmart, I've prepared a GitHub issue for this.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
gh issue create \
  --title "Refactor getAirQualityAverages method to remove unnecessary 'next' parameter" \
  --body "There's an issue with the static method \`getAirQualityAverages\` in \`eventSchema\`. Including the \`next\` parameter in this method is unconventional for Mongoose models. It's recommended to handle errors by throwing them, allowing the caller to manage error propagation.

Refer to this PR: https://github.com/airqo-platform/AirQo-api/pull/3762

Refer to the comment: https://github.com/airqo-platform/AirQo-api/pull/3762#discussion_r1816358012

Requested by: @Baalmart" \
  --assignee Baalmart

Length of output: 667

Comment on lines 2630 to 2633
const percentageDifference =
((currentWeek.weeklyAverage - previousWeek.weeklyAverage) /
previousWeek.weeklyAverage) *
100;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Handle potential division by zero in percentage calculation

If previousWeek.weeklyAverage is zero, dividing by zero will result in Infinity or NaN. To maintain robustness, please add a check to prevent division by zero.

Apply this diff to safeguard the calculation:

 const percentageDifference =
-  ((currentWeek.weeklyAverage - previousWeek.weeklyAverage) /
-    previousWeek.weeklyAverage) *
-  100;
+  previousWeek.weeklyAverage !== 0
+    ? ((currentWeek.weeklyAverage - previousWeek.weeklyAverage) /
+        previousWeek.weeklyAverage) *
+      100
+    : 0;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const percentageDifference =
((currentWeek.weeklyAverage - previousWeek.weeklyAverage) /
previousWeek.weeklyAverage) *
100;
const percentageDifference =
previousWeek.weeklyAverage !== 0
? ((currentWeek.weeklyAverage - previousWeek.weeklyAverage) /
previousWeek.weeklyAverage) *
100
: 0;

pm2_5: "$values.pm2_5.value",
dayOfYear: { $dayOfYear: "$values.time" },
year: { $year: "$values.time" },
week: { $week: "$values.time" },
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Use $isoWeek instead of $week for ISO-compliant week numbers

The $week operator may yield inconsistent results depending on the locale and MongoDB version. Using $isoWeek ensures that week numbers conform to the ISO 8601 standard.

Apply this diff to update the operator:

-week: { $week: "$values.time" },
+week: { $isoWeek: "$values.time" },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
week: { $week: "$values.time" },
week: { $isoWeek: "$values.time" },

Comment on lines 2549 to 2550
const twoWeeksAgo = new Date();
twoWeeksAgo.setDate(today.getDate() - 14);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Correct the calculation of twoWeeksAgo

The current method of calculating twoWeeksAgo may produce incorrect dates, especially around month boundaries. It's advisable to base twoWeeksAgo on today to ensure accuracy.

Apply this diff to fix the date calculation:

-const twoWeeksAgo = new Date();
-twoWeeksAgo.setDate(today.getDate() - 14);
+const twoWeeksAgo = new Date(today);
+twoWeeksAgo.setDate(twoWeeksAgo.getDate() - 14);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const twoWeeksAgo = new Date();
twoWeeksAgo.setDate(today.getDate() - 14);
const twoWeeksAgo = new Date(today);
twoWeeksAgo.setDate(twoWeeksAgo.getDate() - 14);

Comment on lines +2659 to +2667
`Internal Server Error --- getAirQualityAverages --- ${error.message}`
);
logObject("error", error);
next(
new HttpError("Internal Server Error", httpStatus.INTERNAL_SERVER_ERROR, {
message: error.message,
})
);
}
Copy link
Contributor

@coderabbitai coderabbitai bot Oct 25, 2024

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Refactor error handling to throw errors instead of calling next

Within model methods, throwing errors is preferred over calling next. This change promotes better error management and aligns with standard practices.

Apply this diff to update the error handling:

     logger.error(
       `Internal Server Error --- getAirQualityAverages --- ${error.message}`
     );
     logObject("error", error);
-    next(
-      new HttpError("Internal Server Error", httpStatus.INTERNAL_SERVER_ERROR, {
+    throw new HttpError("Internal Server Error", httpStatus.INTERNAL_SERVER_ERROR, {
         message: error.message,
       })
-    );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
`Internal Server Error --- getAirQualityAverages --- ${error.message}`
);
logObject("error", error);
next(
new HttpError("Internal Server Error", httpStatus.INTERNAL_SERVER_ERROR, {
message: error.message,
})
);
}
`Internal Server Error --- getAirQualityAverages --- ${error.message}`
);
logObject("error", error);
throw new HttpError("Internal Server Error", httpStatus.INTERNAL_SERVER_ERROR, {
message: error.message,
});
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

please create a Github issue for this.

Copy link
Contributor

Choose a reason for hiding this comment

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

@Baalmart, the GitHub issue has been successfully created: #3769.

🔗 Analysis chain

@Baalmart, I'll create a GitHub issue for this as requested.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Creating a new GitHub issue for refactoring error handling in getAirQualityAverages method
gh issue create \
  --title "Refactor error handling in getAirQualityAverages method" \
  --body "Within the \`getAirQualityAverages\` method in \`src/device-registry/models/Event.js\`, refactor the error handling to throw errors instead of calling \`next\`. This issue was raised during code review at [this comment](https://github.com/airqo-platform/AirQo-api/pull/3762#discussion_r1816358022) in [PR #3762](https://github.com/airqo-platform/AirQo-api/pull/3762). Requested by @Baalmart." \
  --assignee Baalmart

Length of output: 554

Copy link
Contributor

Device registry changes in this PR available for preview here

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

🧹 Outside diff range and nitpick comments (6)
src/device-registry/routes/v2/readings.js (2)

286-296: Simplify Validation: Remove Unnecessary 'oneOf' Wrapper for 'tenant'

The oneOf function is used when there are alternative validation chains. Since there's only one validation chain for the tenant query parameter, consider removing the oneOf wrapper to simplify the code.

Suggested change:

-router.get(
  "/sites/:site_id/averages",
-  oneOf([
     query("tenant")
       .optional()
       .notEmpty()
       .withMessage("tenant should not be empty if provided")
       .bail()
       .trim()
       .toLowerCase()
       .isIn(constants.NETWORKS)
       .withMessage("the tenant value is not among the expected ones"),
-  ]),

297-314: Simplify Validation: Remove Unnecessary 'oneOf' and Array for 'site_id'

The validation for site_id is wrapped inside oneOf and an array, which is unnecessary since there's only a single validation chain. Simplify the code by removing oneOf and the array.

Suggested change:

   query("tenant")
     // Tenant validation logic
-  ]),
-  oneOf([
-    [
       param("site_id")
         .exists()
         .withMessage("the site_id should be provided")
         .bail()
         .notEmpty()
         .withMessage("the provided site_id cannot be empty")
         .bail()
         .trim()
         .isMongoId()
         .withMessage("the site_id must be an object ID")
         .bail()
         .customSanitizer((value) => {
           return ObjectId(value);
         }),
-    ],
-  ]),
src/device-registry/models/Reading.js (4)

538-552: Handle division by zero in compliance calculations

In the calculateCompliance function, if any of the WHO guideline values are zero, dividing by zero will occur. While the current guidelines have non-zero values, adding a check enhances robustness.

You might implement the check like this:

 pm2_5: {
   compliant: averages.pm2_5 <= WHO_GUIDELINES.pm2_5,
-  percentage: (averages.pm2_5 / WHO_GUIDELINES.pm2_5) * 100,
+  percentage: WHO_GUIDELINES.pm2_5 !== 0 ? (averages.pm2_5 / WHO_GUIDELINES.pm2_5) * 100 : null,
 },

579-589: Enhance trend calculation logic for accuracy

The current method uses a fixed 5% threshold to determine trends, which might not be suitable for all data sets. Consider making the threshold configurable or employing statistical methods to determine significant changes more accurately.

Exploring statistical approaches could provide more meaningful insights into the trends.


642-652: Simplify the recursive rounding function

The roundObject function can be simplified for better readability without affecting its functionality.

Here's an alternative implementation:

 const roundObject = (obj) => {
   return Object.fromEntries(
     Object.entries(obj).map(([key, value]) => [
       key,
-      typeof value === "object" && value !== null ? roundObject(value) :
-      typeof value === "number" ? Number(value.toFixed(2)) : value,
+      (typeof value === "number") ? Number(value.toFixed(2)) :
+      (value && typeof value === "object") ? roundObject(value) : value,
     ])
   );
 };

642-652: Avoid mutating objects within recursive functions

To prevent unintended side effects, ensure that the roundObject function does not mutate the original objects. This practice enhances the reliability of your code.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between f5a1cec and 2b66524.

📒 Files selected for processing (4)
  • src/device-registry/controllers/create-event.js (2 hunks)
  • src/device-registry/models/Reading.js (1 hunks)
  • src/device-registry/routes/v2/readings.js (2 hunks)
  • src/device-registry/utils/create-event.js (4 hunks)
🧰 Additional context used
🪛 GitHub Check: codecov/patch
src/device-registry/controllers/create-event.js

[warning] 942-945: src/device-registry/controllers/create-event.js#L942-L945
Added lines #L942 - L945 were not covered by tests


[warning] 948-948: src/device-registry/controllers/create-event.js#L948
Added line #L948 was not covered by tests


[warning] 951-951: src/device-registry/controllers/create-event.js#L951
Added line #L951 was not covered by tests


[warning] 960-960: src/device-registry/controllers/create-event.js#L960
Added line #L960 was not covered by tests


[warning] 962-963: src/device-registry/controllers/create-event.js#L962-L963
Added lines #L962 - L963 were not covered by tests


[warning] 966-968: src/device-registry/controllers/create-event.js#L966-L968
Added lines #L966 - L968 were not covered by tests


[warning] 974-975: src/device-registry/controllers/create-event.js#L974-L975
Added lines #L974 - L975 were not covered by tests


[warning] 982-983: src/device-registry/controllers/create-event.js#L982-L983
Added lines #L982 - L983 were not covered by tests


[warning] 990-990: src/device-registry/controllers/create-event.js#L990
Added line #L990 was not covered by tests


[warning] 1251-1254: src/device-registry/controllers/create-event.js#L1251-L1254
Added lines #L1251 - L1254 were not covered by tests


[warning] 1257-1257: src/device-registry/controllers/create-event.js#L1257
Added line #L1257 was not covered by tests


[warning] 1260-1262: src/device-registry/controllers/create-event.js#L1260-L1262
Added lines #L1260 - L1262 were not covered by tests


[warning] 1266-1270: src/device-registry/controllers/create-event.js#L1266-L1270
Added lines #L1266 - L1270 were not covered by tests


[warning] 1272-1272: src/device-registry/controllers/create-event.js#L1272
Added line #L1272 was not covered by tests


[warning] 1274-1277: src/device-registry/controllers/create-event.js#L1274-L1277
Added lines #L1274 - L1277 were not covered by tests


[warning] 1279-1282: src/device-registry/controllers/create-event.js#L1279-L1282
Added lines #L1279 - L1282 were not covered by tests


[warning] 1286-1287: src/device-registry/controllers/create-event.js#L1286-L1287
Added lines #L1286 - L1287 were not covered by tests


[warning] 1289-1290: src/device-registry/controllers/create-event.js#L1289-L1290
Added lines #L1289 - L1290 were not covered by tests


[warning] 1293-1294: src/device-registry/controllers/create-event.js#L1293-L1294
Added lines #L1293 - L1294 were not covered by tests


[warning] 1296-1296: src/device-registry/controllers/create-event.js#L1296
Added line #L1296 was not covered by tests


[warning] 1302-1303: src/device-registry/controllers/create-event.js#L1302-L1303
Added lines #L1302 - L1303 were not covered by tests


[warning] 1306-1307: src/device-registry/controllers/create-event.js#L1306-L1307
Added lines #L1306 - L1307 were not covered by tests


[warning] 1314-1314: src/device-registry/controllers/create-event.js#L1314
Added line #L1314 was not covered by tests


[warning] 1323-1324: src/device-registry/controllers/create-event.js#L1323-L1324
Added lines #L1323 - L1324 were not covered by tests


[warning] 1331-1331: src/device-registry/controllers/create-event.js#L1331
Added line #L1331 was not covered by tests

🔇 Additional comments (6)
src/device-registry/utils/create-event.js (2)

1386-1502: Apply the same refactoring patterns as listAverages

This method has similar patterns and issues as the listAverages method.

Please apply the same refactoring suggestions provided for the listAverages method to maintain consistency and reduce code duplication.


Line range hint 2248-2274: LGTM: Cache key generation updated correctly

The addition of the averages parameter to the cache key generation follows the existing pattern and maintains consistency.

src/device-registry/routes/v2/readings.js (2)

7-7: Import Addition: Included 'param' from 'express-validator'

Adding param to the import statement is appropriate for parameter validation in the new route.


284-285: New Route Defined for Average Readings

The new GET route '/sites/:site_id/averages' is correctly defined to fetch average readings by site ID.

src/device-registry/models/Reading.js (2)

375-386: Ensure correct handling of date boundaries and time zones

The current date calculations use local time, which might lead to inconsistencies if the server's time zone differs from the data's time zone. It's important to handle dates in a consistent time zone, preferably UTC, to avoid any discrepancies.

Please verify whether the date boundaries should be adjusted to UTC to align with the data's time zone.


362-693: Overall, the analytics function is well-implemented

The getAirQualityAnalytics function is well-structured and thoughtfully implements the required analytics. The use of aggregation pipelines and concurrent execution is efficient, and error handling is appropriately managed.

Comment on lines +810 to +931

if (cacheResult.success === true) {
logText(cacheResult.message);
return cacheResult.data;
}
} catch (error) {
logger.error(`🐛🐛 Internal Server Errors -- ${stringify(error)}`);
}

const responseFromListEvents = await EventModel(
tenant
).getAirQualityAverages(site_id, next);

if (
language !== undefined &&
!isEmpty(responseFromListEvents) &&
responseFromListEvents.success === true &&
!isEmpty(responseFromListEvents.data)
) {
const data = responseFromListEvents.data;
for (const event of data) {
const translatedHealthTips = await translateUtil.translateTips(
{ healthTips: event.health_tips, targetLanguage: language },
next
);
if (translatedHealthTips.success === true) {
event.health_tips = translatedHealthTips.data;
}
}
}

if (responseFromListEvents.success === true) {
const data = !isEmpty(missingDataMessage)
? []
: responseFromListEvents.data;

logText("Setting cache...");

try {
const resultOfCacheOperation = await Promise.race([
createEvent.setCache(data, request, next),
new Promise((resolve) =>
setTimeout(resolve, 60000, {
success: false,
message: "Internal Server Error",
status: httpStatus.INTERNAL_SERVER_ERROR,
errors: { message: "Cache timeout" },
})
),
]);
if (resultOfCacheOperation.success === false) {
const errors = resultOfCacheOperation.errors
? resultOfCacheOperation.errors
: { message: "Internal Server Error" };
logger.error(`🐛🐛 Internal Server Error -- ${stringify(errors)}`);
// return resultOfCacheOperation;
}
} catch (error) {
logger.error(`🐛🐛 Internal Server Errors -- ${stringify(error)}`);
}

logText("Cache set.");

return {
success: true,
message: !isEmpty(missingDataMessage)
? missingDataMessage
: isEmpty(data)
? "no measurements for this search"
: responseFromListEvents.message,
data,
status: responseFromListEvents.status || "",
isCache: false,
};
} else {
logger.error(
`Unable to retrieve events --- ${stringify(
responseFromListEvents.errors
)}`
);

return {
success: false,
message: responseFromListEvents.message,
errors: responseFromListEvents.errors || { message: "" },
status: responseFromListEvents.status || "",
isCache: false,
};
}
} catch (error) {
logger.error(`🐛🐛 Internal Server Error ${error.message}`);
next(
new HttpError(
"Internal Server Error",
httpStatus.INTERNAL_SERVER_ERROR,
{ message: error.message }
)
);
}
},
Copy link
Contributor

@coderabbitai coderabbitai bot Oct 25, 2024

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Refactor opportunity: Extract common patterns and improve error handling

The listAverages method has several areas that could be improved:

  1. The cache timeout value (60000ms) is hardcoded in multiple places
  2. The missingDataMessage variable is initialized but never populated
  3. The method shares significant code patterns with other methods like list and view
  4. Missing input validation for site_id

Consider these improvements:

+ // Define constants at the top of the file
+ const CACHE_TIMEOUT_MS = 60000;
+ 
+ // Extract common cache handling logic
+ async function handleCache(request, next) {
+   try {
+     const cacheResult = await Promise.race([
+       createEvent.getCache(request, next),
+       new Promise((resolve) =>
+         setTimeout(resolve, CACHE_TIMEOUT_MS, {
+           success: false,
+           message: "Internal Server Error",
+           status: httpStatus.INTERNAL_SERVER_ERROR,
+           errors: { message: "Cache timeout" },
+         })
+       ),
+     ]);
+     
+     if (cacheResult.success === true) {
+       return cacheResult.data;
+     }
+     return null;
+   } catch (error) {
+     logger.error(`🐛🐛 Cache Error: ${stringify(error)}`);
+     return null;
+   }
+ }

+ // Add input validation
+ function validateSiteId(site_id) {
+   if (!site_id) {
+     throw new HttpError(
+       "Bad Request",
+       httpStatus.BAD_REQUEST,
+       { message: "site_id is required" }
+     );
+   }
+ }

Then update the method:

   listAverages: async (request, next) => {
     try {
+      const { site_id, tenant, language } = {
+        ...request.query,
+        ...request.params,
+      };
+      
+      validateSiteId(site_id);
+      
-      let missingDataMessage = "";  // Remove unused variable
+      const cacheData = await handleCache(request, next);
+      if (cacheData) {
+        return cacheData;
+      }

Committable suggestion was skipped due to low confidence.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Create a Github issue for this

Copy link
Contributor

Choose a reason for hiding this comment

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

@Baalmart, the GitHub issue has been created successfully: #3773.

🔗 Analysis chain

@Baalmart, I've created a GitHub issue to track this refactor opportunity.


@Baalmart, thank you for bringing this to my attention. The backticks in the script were causing command substitution errors. I've corrected the script by escaping the backticks so they're treated as literal characters.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash

# Create a GitHub issue to track the refactor opportunity.
gh issue create \
  --title "Refactor `listAverages` method in `create-event.js`" \
  --body "The `listAverages` method in \`src/device-registry/utils/create-event.js\` can be refactored to improve error handling and code reuse.

Consider extracting common cache handling logic, defining constants for cache timeouts, and adding input validation for \`site_id\`.

Refer to PR [#3762](https://github.com/airqo-platform/AirQo-api/pull/3762) and [this comment](https://github.com/airqo-platform/AirQo-api/pull/3762#discussion_r1816402973).

Requested by @Baalmart."

Length of output: 857


Script:

#!/bin/bash

# Create a GitHub issue to track the refactor opportunity.
gh issue create \
  --title "Refactor \\\`listAverages\\\` method in \\\`create-event.js\\\`" \
  --body "The \\\`listAverages\\\` method in \\\`src/device-registry/utils/create-event.js\\\` can be refactored to improve error handling and code reuse.

Consider extracting common cache handling logic, defining constants for cache timeouts, and adding input validation for \\\`site_id\\\`.

Refer to PR [#3762](https://github.com/airqo-platform/AirQo-api/pull/3762) and [this comment](https://github.com/airqo-platform/AirQo-api/pull/3762#discussion_r1816402973).

Requested by @Baalmart."

Length of output: 618

Comment on lines +315 to +398
oneOf([
[
query("startTime")
.optional()
.notEmpty()
.withMessage("startTime cannot be empty IF provided")
.bail()
.trim()
.isISO8601({ strict: true, strictSeparator: true })
.withMessage("startTime must be a valid datetime."),
query("endTime")
.optional()
.notEmpty()
.withMessage("endTime cannot be empty IF provided")
.bail()
.trim()
.isISO8601({ strict: true, strictSeparator: true })
.withMessage("endTime must be a valid datetime."),
query("frequency")
.optional()
.notEmpty()
.withMessage("the frequency cannot be empty if provided")
.bail()
.trim()
.toLowerCase()
.isIn(["hourly", "daily", "raw", "minute"])
.withMessage(
"the frequency value is not among the expected ones which include: hourly, daily, minute and raw"
),
query("format")
.optional()
.notEmpty()
.withMessage("the format cannot be empty if provided")
.bail()
.trim()
.toLowerCase()
.isIn(["json", "csv"])
.withMessage(
"the format value is not among the expected ones which include: csv and json"
),
query("external")
.optional()
.notEmpty()
.withMessage("external cannot be empty IF provided")
.bail()
.trim()
.toLowerCase()
.isIn(["yes", "no"])
.withMessage(
"the external value is not among the expected ones which include: no and yes"
),
query("recent")
.optional()
.notEmpty()
.withMessage("recent cannot be empty IF provided")
.bail()
.trim()
.toLowerCase()
.isIn(["yes", "no"])
.withMessage(
"the recent value is not among the expected ones which include: no and yes"
),
query("metadata")
.optional()
.notEmpty()
.withMessage("metadata cannot be empty IF provided")
.bail()
.trim()
.toLowerCase()
.isIn(["site", "site_id", "device", "device_id"])
.withMessage(
"valid values include: site, site_id, device and device_id"
),
query("test")
.optional()
.notEmpty()
.withMessage("test cannot be empty IF provided")
.bail()
.trim()
.toLowerCase()
.isIn(["yes", "no"])
.withMessage("valid values include: YES and NO"),
],
]),
Copy link
Contributor

@coderabbitai coderabbitai bot Oct 25, 2024

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Refactor Suggestion: Extract Common Validation Logic

The validation rules for query parameters like startTime, endTime, frequency, etc., are similar across multiple routes. Consider refactoring them into reusable middleware functions or validation schemas to reduce code duplication and enhance maintainability.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Create a Github issue for this.

Copy link
Contributor

Choose a reason for hiding this comment

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

@Baalmart, the GitHub issue has been created successfully: airqo-platform/AirQo-api#3770. Please let me know if there's anything else I can assist with.

🔗 Analysis chain

@Baalmart, I've created a GitHub issue to track this refactoring suggestion as per your request.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Create a GitHub issue for refactoring common validation logic

gh issue create \
  --title "Refactor Common Validation Logic in Routes" \
  --body "The validation rules for query parameters like \`startTime\`, \`endTime\`, \`frequency\`, etc., are similar across multiple routes such as in \`src/device-registry/routes/v2/readings.js\`. Consider refactoring them into reusable middleware functions or validation schemas to reduce code duplication and enhance maintainability.

Referenced in PR [#3762](https://github.com/airqo-platform/AirQo-api/pull/3762) and [this comment](https://github.com/airqo-platform/AirQo-api/pull/3762#discussion_r1816402975).

Requested by @Baalmart." \
  --assignee Baalmart

Length of output: 679

Comment on lines +389 to +393
const WHO_GUIDELINES = {
pm2_5: 5, // μg/m³
pm10: 15, // μg/m³
no2: 10, // μg/m³
};
Copy link
Contributor

@coderabbitai coderabbitai bot Oct 25, 2024

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Externalize WHO guidelines for maintainability

Hardcoding the WHO air quality guidelines within the function makes updates more cumbersome. To enhance maintainability, consider externalizing these values into a configuration file or constants module.

This approach will make it easier to update the guidelines in the future without modifying the function logic.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Generate a Github issue for this

Copy link
Contributor

Choose a reason for hiding this comment

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

@Baalmart, the GitHub issue has been created: #3772. Thank you for addressing this.

🔗 Analysis chain

@Baalmart, I've created a GitHub issue to track this suggestion. Thank you for addressing this.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Create a GitHub issue to externalize WHO Air Quality Guidelines
gh issue create \
  --title "Externalize WHO Air Quality Guidelines in getAirQualityAnalytics()" \
  --body "In [PR #3762](https://github.com/airqo-platform/AirQo-api/pull/3762), within the \`getAirQualityAnalytics()\` method in \`src/device-registry/models/Reading.js\`, the WHO air quality guidelines are hardcoded. To enhance maintainability, these values should be externalized into a configuration file or constants module.

Original PR comment: [Link to the comment](https://github.com/airqo-platform/AirQo-api/pull/3762#discussion_r1816402978)

Requested by @Baalmart." \
  --assignee Baalmart

Length of output: 642

Comment on lines 582 to 584
const lastValue = data[data.length - 1];
const previousValue = data[data.length - 2];
const percentChange = ((lastValue - previousValue) / previousValue) * 100;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Prevent potential division by zero in trend calculation

In the calculateTrend function, if previousValue is zero, dividing by zero will cause an error. To safeguard against this, consider adding a check for zero before performing the division.

You can modify the calculation as follows:

 const percentChange = 
-  ((lastValue - previousValue) / previousValue) * 100;
+  previousValue === 0 ? 0 : ((lastValue - previousValue) / previousValue) * 100;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const lastValue = data[data.length - 1];
const previousValue = data[data.length - 2];
const percentChange = ((lastValue - previousValue) / previousValue) * 100;
const lastValue = data[data.length - 1];
const previousValue = data[data.length - 2];
const percentChange = previousValue === 0 ? 0 : ((lastValue - previousValue) / previousValue) * 100;

Comment on lines +405 to +410
site_id: siteId,
time: {
$gte: today,
$lt: tomorrow,
},
},
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Optimize query performance with appropriate indexing

The aggregation pipeline matches on site_id and time. Ensuring these fields are properly indexed can significantly improve query performance.

Consider verifying that indexes exist on these fields:

+ReadingsSchema.index({ site_id: 1, time: 1 });

Committable suggestion was skipped due to low confidence.

Comment on lines 592 to 603
const weeklyData = processWeeklyData(weeklyComparison);
const dailyAverages = {
pm2_5:
Object.values(hourlyData).reduce((sum, hour) => sum + hour.pm2_5, 0) /
24,
pm10:
Object.values(hourlyData).reduce((sum, hour) => sum + hour.pm10, 0) /
24,
no2:
Object.values(hourlyData).reduce((sum, hour) => sum + hour.no2, 0) / 24,
};

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Consider adjusting the calculation of daily averages

Currently, the daily averages are calculated by summing the hourly pollutant values and dividing by 24. If there are missing data points for some hours, this could result in inaccurate averages. It would be more accurate to divide by the actual number of data points available.

You might consider the following adjustment:

 const dailyAverages = {
   pm2_5:
-    Object.values(hourlyData).reduce((sum, hour) => sum + hour.pm2_5, 0) / 24,
+    Object.values(hourlyData).reduce((sum, hour) => sum + hour.pm2_5, 0) / Object.keys(hourlyData).length,
   pm10:
-    Object.values(hourlyData).reduce((sum, hour) => sum + hour.pm10, 0) / 24,
+    Object.values(hourlyData).reduce((sum, hour) => sum + hour.pm10, 0) / Object.keys(hourlyData).length,
   no2:
-    Object.values(hourlyData).reduce((sum, hour) => sum + hour.no2, 0) / 24,
+    Object.values(hourlyData).reduce((sum, hour) => sum + hour.no2, 0) / Object.keys(hourlyData).length,
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const weeklyData = processWeeklyData(weeklyComparison);
const dailyAverages = {
pm2_5:
Object.values(hourlyData).reduce((sum, hour) => sum + hour.pm2_5, 0) /
24,
pm10:
Object.values(hourlyData).reduce((sum, hour) => sum + hour.pm10, 0) /
24,
no2:
Object.values(hourlyData).reduce((sum, hour) => sum + hour.no2, 0) / 24,
};
const weeklyData = processWeeklyData(weeklyComparison);
const dailyAverages = {
pm2_5:
Object.values(hourlyData).reduce((sum, hour) => sum + hour.pm2_5, 0) /
Object.keys(hourlyData).length,
pm10:
Object.values(hourlyData).reduce((sum, hour) => sum + hour.pm10, 0) /
Object.keys(hourlyData).length,
no2:
Object.values(hourlyData).reduce((sum, hour) => sum + hour.no2, 0) /
Object.keys(hourlyData).length,
};

Comment on lines +941 to +991
listReadingAverages: async (req, res, next) => {
try {
const errors = extractErrorsFromRequest(req);
if (errors) {
next(
new HttpError("bad request errors", httpStatus.BAD_REQUEST, errors)
);
return;
}

const request = {
...req,
query: {
...req.query,
tenant: isEmpty(req.query.tenant) ? "airqo" : req.query.tenant,
averages: "readings",
},
};

const result = await createEventUtil.listReadingAverages(request, next);

if (isEmpty(result) || res.headersSent) {
return;
}

const status = result.status || httpStatus.OK;
if (result.success === true) {
res.status(status).json({
success: true,
message: result.message,
measurements: result.data,
});
} else {
const errorStatus = result.status || httpStatus.INTERNAL_SERVER_ERROR;
res.status(errorStatus).json({
success: false,
errors: result.errors || { message: "" },
message: result.message,
});
}
} catch (error) {
logger.error(`🐛🐛 Internal Server Error ${error.message}`);
next(
new HttpError(
"Internal Server Error",
httpStatus.INTERNAL_SERVER_ERROR,
{ message: error.message }
)
);
return;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add unit tests for listReadingAverages to improve code coverage.

The new method listReadingAverages is not currently covered by unit tests, as indicated by the static analysis tools. Implementing unit tests will help ensure the method functions correctly and maintain code reliability.

🧰 Tools
🪛 GitHub Check: codecov/patch

[warning] 942-945: src/device-registry/controllers/create-event.js#L942-L945
Added lines #L942 - L945 were not covered by tests


[warning] 948-948: src/device-registry/controllers/create-event.js#L948
Added line #L948 was not covered by tests


[warning] 951-951: src/device-registry/controllers/create-event.js#L951
Added line #L951 was not covered by tests


[warning] 960-960: src/device-registry/controllers/create-event.js#L960
Added line #L960 was not covered by tests


[warning] 962-963: src/device-registry/controllers/create-event.js#L962-L963
Added lines #L962 - L963 were not covered by tests


[warning] 966-968: src/device-registry/controllers/create-event.js#L966-L968
Added lines #L966 - L968 were not covered by tests


[warning] 974-975: src/device-registry/controllers/create-event.js#L974-L975
Added lines #L974 - L975 were not covered by tests


[warning] 982-983: src/device-registry/controllers/create-event.js#L982-L983
Added lines #L982 - L983 were not covered by tests


[warning] 990-990: src/device-registry/controllers/create-event.js#L990
Added line #L990 was not covered by tests

Comment on lines 1250 to 1333
listAverages: async (req, res, next) => {
try {
const errors = extractErrorsFromRequest(req);
if (errors) {
next(
new HttpError("bad request errors", httpStatus.BAD_REQUEST, errors)
);
return;
}

const request = req;
const defaultTenant = constants.DEFAULT_TENANT || "airqo";
request.query.tenant = isEmpty(req.query.tenant)
? defaultTenant
: req.query.tenant;

request.query.recent = "no";
request.query.metadata = "site_id";
request.query.averages = "events";
request.query.brief = "yes";
const { cohort_id, grid_id } = { ...req.query, ...req.params };

let locationErrors = 0;

if (cohort_id) {
await processCohortIds(cohort_id, request);
if (isEmpty(request.query.device_id)) {
locationErrors++;
}
} else if (grid_id) {
await processGridIds(grid_id, request);
if (isEmpty(request.query.site_id)) {
locationErrors++;
}
}

if (locationErrors === 0) {
const result = await createEventUtil.listAverages(request, next);

if (isEmpty(result) || res.headersSent) {
return;
}

if (result.success === true) {
const status = result.status ? result.status : httpStatus.OK;

res.status(status).json({
success: true,
isCache: result.isCache,
message: result.message,
measurements: result.data,
});
} else if (result.success === false) {
const status = result.status
? result.status
: httpStatus.INTERNAL_SERVER_ERROR;
const errors = result.errors ? result.errors : { message: "" };
res.status(status).json({
success: false,
errors,
message: result.message,
});
}
} else {
res.status(httpStatus.INTERNAL_SERVER_ERROR).json({
success: false,
errors: {
message: `Unable to process measurements for the provided measurement IDs`,
},
message: "Internal Server Error",
});
}
} catch (error) {
logger.error(`🐛🐛 Internal Server Error ${error.message}`);
next(
new HttpError(
"Internal Server Error",
httpStatus.INTERNAL_SERVER_ERROR,
{ message: error.message }
)
);
return;
}
},
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add unit tests for listAverages to enhance test coverage.

The new method listAverages lacks unit tests, as highlighted by the static analysis hints. Adding unit tests will help verify the functionality and ensure any future changes do not introduce regressions.

🧰 Tools
🪛 GitHub Check: codecov/patch

[warning] 1251-1254: src/device-registry/controllers/create-event.js#L1251-L1254
Added lines #L1251 - L1254 were not covered by tests


[warning] 1257-1257: src/device-registry/controllers/create-event.js#L1257
Added line #L1257 was not covered by tests


[warning] 1260-1262: src/device-registry/controllers/create-event.js#L1260-L1262
Added lines #L1260 - L1262 were not covered by tests


[warning] 1266-1270: src/device-registry/controllers/create-event.js#L1266-L1270
Added lines #L1266 - L1270 were not covered by tests


[warning] 1272-1272: src/device-registry/controllers/create-event.js#L1272
Added line #L1272 was not covered by tests


[warning] 1274-1277: src/device-registry/controllers/create-event.js#L1274-L1277
Added lines #L1274 - L1277 were not covered by tests


[warning] 1279-1282: src/device-registry/controllers/create-event.js#L1279-L1282
Added lines #L1279 - L1282 were not covered by tests


[warning] 1286-1287: src/device-registry/controllers/create-event.js#L1286-L1287
Added lines #L1286 - L1287 were not covered by tests


[warning] 1289-1290: src/device-registry/controllers/create-event.js#L1289-L1290
Added lines #L1289 - L1290 were not covered by tests


[warning] 1293-1294: src/device-registry/controllers/create-event.js#L1293-L1294
Added lines #L1293 - L1294 were not covered by tests


[warning] 1296-1296: src/device-registry/controllers/create-event.js#L1296
Added line #L1296 was not covered by tests


[warning] 1302-1303: src/device-registry/controllers/create-event.js#L1302-L1303
Added lines #L1302 - L1303 were not covered by tests


[warning] 1306-1307: src/device-registry/controllers/create-event.js#L1306-L1307
Added lines #L1306 - L1307 were not covered by tests


[warning] 1314-1314: src/device-registry/controllers/create-event.js#L1314
Added line #L1314 was not covered by tests


[warning] 1323-1324: src/device-registry/controllers/create-event.js#L1323-L1324
Added lines #L1323 - L1324 were not covered by tests


[warning] 1331-1331: src/device-registry/controllers/create-event.js#L1331
Added line #L1331 was not covered by tests

Comment on lines 1314 to 1320
res.status(httpStatus.INTERNAL_SERVER_ERROR).json({
success: false,
errors: {
message: `Unable to process measurements for the provided measurement IDs`,
},
message: "Internal Server Error",
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Use appropriate HTTP status code for client-side errors.

The error response uses httpStatus.INTERNAL_SERVER_ERROR, but the issue arises due to client input (e.g., missing or invalid measurement IDs). Consider using httpStatus.BAD_REQUEST to indicate a client-side error, and update the message accordingly.

Apply this diff to adjust the status code and message:

- res.status(httpStatus.INTERNAL_SERVER_ERROR).json({
+ res.status(httpStatus.BAD_REQUEST).json({
    success: false,
    errors: {
        message: `Unable to process measurements for the provided measurement IDs`,
    },
-   message: "Internal Server Error",
+   message: "Bad Request",
});
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
res.status(httpStatus.INTERNAL_SERVER_ERROR).json({
success: false,
errors: {
message: `Unable to process measurements for the provided measurement IDs`,
},
message: "Internal Server Error",
});
res.status(httpStatus.BAD_REQUEST).json({
success: false,
errors: {
message: `Unable to process measurements for the provided measurement IDs`,
},
message: "Bad Request",
});
🧰 Tools
🪛 GitHub Check: codecov/patch

[warning] 1314-1314: src/device-registry/controllers/create-event.js#L1314
Added line #L1314 was not covered by tests

Copy link
Contributor

Device registry changes in this PR available for preview here

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 Outside diff range and nitpick comments (5)
src/device-registry/models/Reading.js (1)

690-698: Consider adding specific error types.

The error handling could be improved by categorizing different types of errors (e.g., database errors, calculation errors) to provide more specific error messages.

   } catch (error) {
     logger.error(`🐛🐛 Internal Server Error -- ${error.message}`);
+    const errorType = error.name === 'MongoError' ? 'Database Error' : 'Calculation Error';
     next(
-      new HttpError("Internal Server Error", httpStatus.INTERNAL_SERVER_ERROR, {
+      new HttpError(errorType, httpStatus.INTERNAL_SERVER_ERROR, {
         message: error.message,
       })
     );
src/device-registry/models/Event.js (3)

2553-2559: Consider adding index hint for query optimization

The base query matches on values.site_id and values.time. Adding an index hint could improve query performance.

Consider creating a compound index:

eventSchema.index({
  "values.site_id": 1,
  "values.time": 1
});
🧰 Tools
🪛 GitHub Check: codecov/patch

[warning] 2553-2553: src/device-registry/models/Event.js#L2553
Added line #L2553 was not covered by tests


2616-2623: Enhance error response with more details

The insufficient data response could be more informative.

Consider enhancing the error response:

 return {
   success: false,
   message: "Insufficient data for comparison",
+  data: {
+    availableWeeks: result.length,
+    requiredWeeks: 2,
+    timeRange: {
+      start: twoWeeksAgo.toISOString(),
+      end: now.toISOString()
+    }
+  },
   status: httpStatus.NOT_FOUND,
 };
🧰 Tools
🪛 GitHub Check: codecov/patch

[warning] 2617-2618: src/device-registry/models/Event.js#L2617-L2618
Added lines #L2617 - L2618 were not covered by tests


2544-2671: Add test coverage for getAirQualityAverages method

The static analysis indicates that the new method lacks test coverage. This is critical functionality that should be thoroughly tested.

Would you like me to help create comprehensive test cases covering:

  • Success scenarios with various data patterns
  • Edge cases (empty data, single week data)
  • Error handling scenarios
  • Date calculation accuracy
  • Aggregation result validation

I can generate a complete test suite if you'd like.

🧰 Tools
🪛 GitHub Check: codecov/patch

[warning] 2545-2545: src/device-registry/models/Event.js#L2545
Added line #L2545 was not covered by tests


[warning] 2547-2550: src/device-registry/models/Event.js#L2547-L2550
Added lines #L2547 - L2550 were not covered by tests


[warning] 2553-2553: src/device-registry/models/Event.js#L2553
Added line #L2553 was not covered by tests


[warning] 2561-2561: src/device-registry/models/Event.js#L2561
Added line #L2561 was not covered by tests


[warning] 2617-2618: src/device-registry/models/Event.js#L2617-L2618
Added lines #L2617 - L2618 were not covered by tests

src/device-registry/controllers/create-event.js (1)

941-992: Enhance logging for better observability.

Consider adding debug logs to track the flow of data through this endpoint, similar to other methods in this controller.

Add logging statements:

 listReadingAverages: async (req, res, next) => {
   try {
+    logText("processing reading averages request...");
     const errors = extractErrorsFromRequest(req);
     if (errors) {
+      logger.error(`Bad request errors: ${JSON.stringify(errors)}`);
       next(
         new HttpError("bad request errors", httpStatus.BAD_REQUEST, errors)
       );
       return;
     }

     const request = {
       ...req,
       query: {
         ...req.query,
         tenant: isEmpty(req.query.tenant) ? "airqo" : req.query.tenant,
         averages: "readings",
       },
     };
+    logObject("request query parameters", request.query);

     const result = await createEventUtil.listReadingAverages(request, next);
+    logObject("reading averages result", result);
🧰 Tools
🪛 GitHub Check: codecov/patch

[warning] 942-945: src/device-registry/controllers/create-event.js#L942-L945
Added lines #L942 - L945 were not covered by tests


[warning] 948-948: src/device-registry/controllers/create-event.js#L948
Added line #L948 was not covered by tests


[warning] 951-951: src/device-registry/controllers/create-event.js#L951
Added line #L951 was not covered by tests


[warning] 960-960: src/device-registry/controllers/create-event.js#L960
Added line #L960 was not covered by tests


[warning] 962-963: src/device-registry/controllers/create-event.js#L962-L963
Added lines #L962 - L963 were not covered by tests


[warning] 966-968: src/device-registry/controllers/create-event.js#L966-L968
Added lines #L966 - L968 were not covered by tests


[warning] 974-975: src/device-registry/controllers/create-event.js#L974-L975
Added lines #L974 - L975 were not covered by tests


[warning] 982-983: src/device-registry/controllers/create-event.js#L982-L983
Added lines #L982 - L983 were not covered by tests


[warning] 990-990: src/device-registry/controllers/create-event.js#L990
Added line #L990 was not covered by tests

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 2b66524 and a8180e7.

⛔ Files ignored due to path filters (1)
  • src/device-registry/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (5)
  • src/device-registry/controllers/create-event.js (2 hunks)
  • src/device-registry/models/Event.js (1 hunks)
  • src/device-registry/models/Reading.js (1 hunks)
  • src/device-registry/package.json (1 hunks)
  • src/device-registry/routes/v2/measurements.js (2 hunks)
🧰 Additional context used
🪛 GitHub Check: codecov/patch
src/device-registry/controllers/create-event.js

[warning] 942-945: src/device-registry/controllers/create-event.js#L942-L945
Added lines #L942 - L945 were not covered by tests


[warning] 948-948: src/device-registry/controllers/create-event.js#L948
Added line #L948 was not covered by tests


[warning] 951-951: src/device-registry/controllers/create-event.js#L951
Added line #L951 was not covered by tests


[warning] 960-960: src/device-registry/controllers/create-event.js#L960
Added line #L960 was not covered by tests


[warning] 962-963: src/device-registry/controllers/create-event.js#L962-L963
Added lines #L962 - L963 were not covered by tests


[warning] 966-968: src/device-registry/controllers/create-event.js#L966-L968
Added lines #L966 - L968 were not covered by tests


[warning] 974-975: src/device-registry/controllers/create-event.js#L974-L975
Added lines #L974 - L975 were not covered by tests


[warning] 982-983: src/device-registry/controllers/create-event.js#L982-L983
Added lines #L982 - L983 were not covered by tests


[warning] 990-990: src/device-registry/controllers/create-event.js#L990
Added line #L990 was not covered by tests


[warning] 1251-1254: src/device-registry/controllers/create-event.js#L1251-L1254
Added lines #L1251 - L1254 were not covered by tests


[warning] 1257-1257: src/device-registry/controllers/create-event.js#L1257
Added line #L1257 was not covered by tests


[warning] 1260-1262: src/device-registry/controllers/create-event.js#L1260-L1262
Added lines #L1260 - L1262 were not covered by tests


[warning] 1266-1270: src/device-registry/controllers/create-event.js#L1266-L1270
Added lines #L1266 - L1270 were not covered by tests


[warning] 1272-1272: src/device-registry/controllers/create-event.js#L1272
Added line #L1272 was not covered by tests


[warning] 1274-1277: src/device-registry/controllers/create-event.js#L1274-L1277
Added lines #L1274 - L1277 were not covered by tests


[warning] 1279-1282: src/device-registry/controllers/create-event.js#L1279-L1282
Added lines #L1279 - L1282 were not covered by tests


[warning] 1286-1287: src/device-registry/controllers/create-event.js#L1286-L1287
Added lines #L1286 - L1287 were not covered by tests


[warning] 1289-1290: src/device-registry/controllers/create-event.js#L1289-L1290
Added lines #L1289 - L1290 were not covered by tests


[warning] 1293-1294: src/device-registry/controllers/create-event.js#L1293-L1294
Added lines #L1293 - L1294 were not covered by tests


[warning] 1296-1296: src/device-registry/controllers/create-event.js#L1296
Added line #L1296 was not covered by tests


[warning] 1302-1303: src/device-registry/controllers/create-event.js#L1302-L1303
Added lines #L1302 - L1303 were not covered by tests


[warning] 1306-1307: src/device-registry/controllers/create-event.js#L1306-L1307
Added lines #L1306 - L1307 were not covered by tests


[warning] 1314-1314: src/device-registry/controllers/create-event.js#L1314
Added line #L1314 was not covered by tests


[warning] 1323-1324: src/device-registry/controllers/create-event.js#L1323-L1324
Added lines #L1323 - L1324 were not covered by tests


[warning] 1331-1331: src/device-registry/controllers/create-event.js#L1331
Added line #L1331 was not covered by tests

src/device-registry/models/Event.js

[warning] 2545-2545: src/device-registry/models/Event.js#L2545
Added line #L2545 was not covered by tests


[warning] 2547-2550: src/device-registry/models/Event.js#L2547-L2550
Added lines #L2547 - L2550 were not covered by tests


[warning] 2553-2553: src/device-registry/models/Event.js#L2553
Added line #L2553 was not covered by tests


[warning] 2561-2561: src/device-registry/models/Event.js#L2561
Added line #L2561 was not covered by tests


[warning] 2617-2618: src/device-registry/models/Event.js#L2617-L2618
Added lines #L2617 - L2618 were not covered by tests

🔇 Additional comments (11)
src/device-registry/package.json (2)

57-57: Good addition of rate limiting middleware!

Adding express-rate-limit is a great security practice to protect your API endpoints from potential DoS attacks and excessive usage. The version 7.4.1 is recent and includes important security fixes.


57-57: Verify rate limit configuration for averages endpoints.

Since these endpoints perform averaging calculations which could be computationally intensive, let's verify if the rate limit of 100 requests per 15-minute window is appropriate for your use case.

Consider factors like:

  • Expected number of client applications
  • Typical request patterns
  • Server resource capacity
✅ Verification successful

Rate limit configuration appears appropriate for the use case

The rate limit of 100 requests per 15-minute window for the averages endpoint is reasonable based on the following findings:

  • The rate limit is specifically applied only to the /sites/:site_id/averages endpoint, not impacting other measurement endpoints
  • The implementation in measurements.js shows proper configuration with windowMs: 15 * 60 * 1000 and max: 100
  • The codebase has multiple other measurement endpoints (/historical, /recent, /latest etc.) that remain unrestricted, providing alternative access points if needed
  • The rate limit is per IP address, allowing reasonable usage while preventing abuse
  • The averaging endpoint appears to be one of many available endpoints, so restricting it does not severely impact overall API usability

The current limit of ~6.7 requests per minute per IP should be sufficient for most client applications while protecting server resources.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for rate limiter configuration to verify the limits
rg -A 5 "express-rate-limit" 
rg -A 5 "RateLimit" 

Length of output: 3079


Script:

#!/bin/bash
# Let's check the usage of averagesLimiter and any related endpoints
rg -A 10 "averagesLimiter" src/device-registry/routes/v2/measurements.js

# Also check for any averaging-related routes or calculations
rg -A 5 "average" src/device-registry/routes/v2/measurements.js

# Let's see the actual route handlers to understand the computation
rg -A 10 "router\.(get|post)" src/device-registry/routes/v2/measurements.js

Length of output: 6938


Script:

#!/bin/bash
# Let's examine the implementation of listAverages to understand the computational complexity
ast-grep --pattern 'listAverages = async $_' src/device-registry/controllers/event.controller.js

# Check if there are any existing performance-related configurations or comments
rg -A 5 "performance|intensive|heavy" src/device-registry/

# Look for any documentation about API usage patterns
fd README.md --exec cat {} \;

Length of output: 79146

src/device-registry/models/Reading.js (4)

362-372: LGTM! Input validation is properly implemented.

The function correctly validates the required siteId parameter and returns an appropriate error response if missing.


389-393: Previous review comment is still valid.

The WHO guidelines should be externalized into a configuration file.


659-689: LGTM! Response structure is well-organized.

The response object is properly structured with clear naming and includes all necessary analytics data.


401-424: Consider adding indexes to improve query performance.

The aggregation pipelines heavily rely on site_id and time fields. While there are existing indexes, verify they are being utilized effectively.

Also applies to: 426-447, 449-494

✅ Verification successful

Let me search for index definitions in the codebase since we can't directly query the database. This will help us verify if appropriate indexes are defined in the schema/model files.


Indexes are properly configured for the aggregation pipeline

The codebase shows that appropriate indexes are already defined in the Reading model:

  • Compound index on {device_id: 1, time: 1}
  • Compound index on {device: 1, time: 1}
  • Compound index on {site_id: 1, time: 1}

These indexes, particularly the {site_id: 1, time: 1} index, will efficiently support the aggregation pipeline's $match stage that filters on site_id and time fields. The index order matches the query pattern, making it optimal for the given query.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check if indexes are being used for the aggregation queries
echo "Checking index usage for site_id and time fields..."
mongo --eval 'db.readings.getIndexes()' | grep -E "site_id|time"

# Verify if the indexes are being used in the aggregation pipeline
mongo --eval 'db.readings.explain("executionStats").aggregate([
  { $match: { site_id: "sample_id", time: { $gte: new Date() } } }
])'

Length of output: 462


Script:

#!/bin/bash
# Let's check for index definitions in the schema or model files
echo "Searching for index definitions in schema/model files..."
rg -l "index|createIndex" --type js

# Look for specific index definitions for site_id and time
echo -e "\nChecking for site_id and time index definitions..."
rg "(?i)(?:index|createIndex).*(?:site_id|time)" --type js -A 2 -B 2

# Look for mongoose schema definitions that might contain index configurations
echo -e "\nChecking mongoose schema definitions..."
ast-grep --pattern 'new Schema({
  $$$
})'

Length of output: 36222

src/device-registry/models/Event.js (2)

2570-2576: Use ISO week for consistent week numbering

The current week calculation might give inconsistent results across different locales.

A past review comment already suggested using $isoWeek instead of $week. This change is still needed.


2660-2670: Refactor error handling to use async/await pattern

The error handling should be modernized and consistent.

A past review comment already suggested throwing errors instead of using the next callback. Additionally, consider using try-catch at the controller level.

src/device-registry/controllers/create-event.js (1)

1314-1321: LGTM: Improved error response.

The error response now correctly uses httpStatus.BAD_REQUEST instead of INTERNAL_SERVER_ERROR for client-side validation failures, which is more appropriate.

🧰 Tools
🪛 GitHub Check: codecov/patch

[warning] 1314-1314: src/device-registry/controllers/create-event.js#L1314
Added line #L1314 was not covered by tests

src/device-registry/routes/v2/measurements.js (2)

19-23: LGTM! Well-configured rate limiting.

The rate limiter configuration with 100 requests per 15-minute window provides good protection against potential abuse while allowing reasonable usage of the averages endpoint.


1203-1330: Well-structured implementation of the averages endpoint.

The implementation includes:

  • Clear documentation
  • Comprehensive parameter validation
  • Rate limiting protection
  • Consistent error handling

Let's verify the controller implementation:

✅ Verification successful

Controller implementation for averages endpoint is properly structured and complete

The implementation follows a clean architecture with proper separation of concerns:

  • Controller (create-event.js) handles request validation and response formatting
  • Utility layer (create-event.js utils) manages business logic and data retrieval
  • Model layer (Event.js) implements the core getAirQualityAverages functionality
  • Proper error handling and status codes are implemented at each layer
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check the implementation of listAverages in the controller
# Expect: Method implementation with proper error handling and average calculation logic

# Search for the listAverages method implementation
ast-grep --pattern 'listAverages = async (req, res, next) => { $$$ }'

# Look for average calculation logic
rg -l "getAirQualityAverages|calculateAverages" --type js

Length of output: 209


Script:

#!/bin/bash
# Let's check the implementation in the Event model and utils
rg -A 10 "getAirQualityAverages|calculateAverages" src/device-registry/models/Event.js src/device-registry/utils/create-event.js

# Also check for the controller file that might contain listAverages
fd -e js controller src/device-registry --exec rg -l "listAverages"

# If we find the controller, let's examine its implementation
fd -e js controller src/device-registry --exec rg -A 20 "listAverages"

Length of output: 2645


Script:

#!/bin/bash
# Let's find the event controller specifically
fd -t f "event.controller.js|events.controller.js" src/device-registry

# And check its content for listAverages
fd -t f "event.controller.js|events.controller.js" src/device-registry --exec cat {}

# Also check for any test files that might verify this functionality
fd -t f -e test.js src/device-registry --exec rg -l "listAverages"

Length of output: 610


Script:

#!/bin/bash
# Let's correct the fd command syntax and search for controller files
fd -t f "event.controller.js|events.controller.js" .

# Search for listAverages in all JS files under controllers directory
fd -t f -e js . controllers --exec rg -A 10 "listAverages" {}

# Look for the route handler implementation
rg -A 10 "listAverages" --type js

Length of output: 3421

Comment on lines +578 to +592
// Calculate trends
const calculateTrend = (data) => {
if (data.length < 2) return "insufficient_data";

const lastValue = data[data.length - 1];
const previousValue = data[data.length - 2];
const percentChange =
previousValue === 0
? 0
: ((lastValue - previousValue) / previousValue) * 100;

if (percentChange > 5) return "increasing";
if (percentChange < -5) return "decreasing";
return "stable";
};
Copy link
Contributor

@coderabbitai coderabbitai bot Oct 25, 2024

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Enhance trend calculation with configurable thresholds.

The trend calculation uses hardcoded thresholds (5% and -5%). Consider making these configurable to allow for different sensitivity levels.

-      if (percentChange > 5) return "increasing";
-      if (percentChange < -5) return "decreasing";
+      const TREND_THRESHOLD = config.analytics.trendThreshold || 5;
+      if (percentChange > TREND_THRESHOLD) return "increasing";
+      if (percentChange < -TREND_THRESHOLD) return "decreasing";

Committable suggestion was skipped due to low confidence.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Create a Github issue for this.

Copy link
Contributor

Choose a reason for hiding this comment

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

@Baalmart, the GitHub issue has been successfully created: #3774

🔗 Analysis chain

@Baalmart, I've proceeded to create a GitHub issue for this enhancement.

(^•ᴥ•^)

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Create a GitHub issue to enhance trend calculation thresholds
gh issue create \
  --title "Enhance trend calculation with configurable thresholds" \
  --body "The trend calculation in the \`ReadingsSchema\` uses hardcoded thresholds (5% and -5%). It would be beneficial to make these thresholds configurable to allow for different sensitivity levels.

References:
- Pull Request: https://github.com/airqo-platform/AirQo-api/pull/3762
- Comment: https://github.com/airqo-platform/AirQo-api/pull/3762#discussion_r1816465489

Requested by @Baalmart." \
  --assignee "Baalmart"

Length of output: 553

Comment on lines +374 to +386
// Get current date boundaries
const today = new Date();
today.setHours(0, 0, 0, 0);
const tomorrow = new Date(today);
tomorrow.setDate(tomorrow.getDate() + 1);

// Calculate time boundaries
const currentWeekStart = new Date(today);
currentWeekStart.setDate(currentWeekStart.getDate() - 7);
const lastWeekStart = new Date(currentWeekStart);
lastWeekStart.setDate(lastWeekStart.getDate() - 7);
const monthStart = new Date(today);
monthStart.setDate(1);
Copy link
Contributor

@coderabbitai coderabbitai bot Oct 25, 2024

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider using a date utility library for better maintainability.

The date calculations are correct but could benefit from using a library like date-fns or moment.js to improve readability and reduce potential bugs.

-    const today = new Date();
-    today.setHours(0, 0, 0, 0);
-    const tomorrow = new Date(today);
-    tomorrow.setDate(tomorrow.getDate() + 1);
+    const today = startOfDay(new Date());
+    const tomorrow = addDays(today, 1);
+    const currentWeekStart = subDays(today, 7);
+    const lastWeekStart = subDays(currentWeekStart, 7);
+    const monthStart = startOfMonth(today);

Committable suggestion was skipped due to low confidence.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Create a Github issue for this.

Copy link
Contributor

Choose a reason for hiding this comment

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

@Baalmart, I've created the GitHub issue as requested: #3775

🔗 Analysis chain

@Baalmart, I'll create a GitHub issue to track this suggestion.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
gh issue create \
  --title "Consider using a date utility library for better maintainability" \
  --body "As suggested in [PR #3762](https://github.com/airqo-platform/AirQo-api/pull/3762), in the [comment](https://github.com/airqo-platform/AirQo-api/pull/3762#discussion_r1816465493), @Baalmart requested to consider using a date utility library like \`date-fns\` or \`moment.js\` to improve readability and reduce potential bugs in date calculations.\n\nRequest from @Baalmart." \
  --assignee Baalmart

Length of output: 546

Comment on lines +647 to +657
const roundObject = (obj) => {
return Object.entries(obj).reduce((acc, [key, value]) => {
if (typeof value === "object" && value !== null) {
acc[key] = roundObject(value);
} else {
acc[key] =
typeof value === "number" ? Number(value.toFixed(2)) : value;
}
return acc;
}, {});
};
Copy link
Contributor

@coderabbitai coderabbitai bot Oct 25, 2024

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider memoizing the roundObject function.

The roundObject function is called multiple times with potentially similar objects. Memoization could improve performance for large datasets.

+    const memoizedRoundObject = memoize(roundObject);
     const response = {
       data: {
-        dailyAverage: roundObject(dailyAverages),
-        hourlyBreakdown: roundObject(hourlyData),
+        dailyAverage: memoizedRoundObject(dailyAverages),
+        hourlyBreakdown: memoizedRoundObject(hourlyData),

Committable suggestion was skipped due to low confidence.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Create a Github issue for this.

Copy link
Contributor

Choose a reason for hiding this comment

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

@Baalmart, I've created the GitHub issue as requested: #3776

🔗 Analysis chain

@Baalmart, I've created a GitHub issue for this as per your request.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Create a GitHub issue for the suggestion to memoize the roundObject function in getAirQualityAnalytics.
gh issue create \
  --title "Consider memoizing the roundObject function in getAirQualityAnalytics" \
  --body "The function \`roundObject\` in \`src/device-registry/models/Reading.js\` is called multiple times with potentially similar objects. Memoization could improve performance for large datasets.

Reference: [Pull Request #3762](https://github.com/airqo-platform/AirQo-api/pull/3762)
Comment: [CodeRabbit suggestion](https://github.com/airqo-platform/AirQo-api/pull/3762#discussion_r1816465495)
Requester: @Baalmart" \
  --assignee 'Baalmart'

Length of output: 589

Comment on lines +2547 to +2550
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const twoWeeksAgo = new Date(today);
twoWeeksAgo.setDate(today.getDate() - 14);
Copy link
Contributor

@coderabbitai coderabbitai bot Oct 25, 2024

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Improve date calculation logic for better accuracy

The current date calculation logic could be improved for better accuracy and clarity.

Apply this diff to improve the date calculations:

-const now = new Date();
-const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
-const twoWeeksAgo = new Date(today);
-twoWeeksAgo.setDate(today.getDate() - 14);
+const now = new Date();
+const today = new Date(now.setHours(0, 0, 0, 0));
+const twoWeeksAgo = new Date(today.getTime() - (14 * 24 * 60 * 60 * 1000));

This change:

  • Ensures consistent midnight timestamp for today
  • Uses millisecond arithmetic for more precise date calculation
  • Avoids potential issues with month boundaries
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const twoWeeksAgo = new Date(today);
twoWeeksAgo.setDate(today.getDate() - 14);
const now = new Date();
const today = new Date(now.setHours(0, 0, 0, 0));
const twoWeeksAgo = new Date(today.getTime() - (14 * 24 * 60 * 60 * 1000));
🧰 Tools
🪛 GitHub Check: codecov/patch

[warning] 2547-2550: src/device-registry/models/Event.js#L2547-L2550
Added lines #L2547 - L2550 were not covered by tests

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Create a Github issue for this.

Copy link
Contributor

Choose a reason for hiding this comment

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

@Baalmart, I've created the GitHub issue as requested: #3778.

🔗 Analysis chain

@Baalmart, I'll create a GitHub issue for this.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
gh issue create \
  --title "Improve date calculation logic for better accuracy" \
  --body "As discussed in [PR #3762](https://github.com/airqo-platform/AirQo-api/pull/3762), it's suggested to improve the date calculation logic for better accuracy.\n\nRequester: @Baalmart\n\nRelated comment: [Link to the comment](https://github.com/airqo-platform/AirQo-api/pull/3762#discussion_r1816465498)" \
  --assignee Baalmart

Length of output: 467

Comment on lines +941 to +992
listReadingAverages: async (req, res, next) => {
try {
const errors = extractErrorsFromRequest(req);
if (errors) {
next(
new HttpError("bad request errors", httpStatus.BAD_REQUEST, errors)
);
return;
}

const request = {
...req,
query: {
...req.query,
tenant: isEmpty(req.query.tenant) ? "airqo" : req.query.tenant,
averages: "readings",
},
};

const result = await createEventUtil.listReadingAverages(request, next);

if (isEmpty(result) || res.headersSent) {
return;
}

const status = result.status || httpStatus.OK;
if (result.success === true) {
res.status(status).json({
success: true,
message: result.message,
measurements: result.data,
});
} else {
const errorStatus = result.status || httpStatus.INTERNAL_SERVER_ERROR;
res.status(errorStatus).json({
success: false,
errors: result.errors || { message: "" },
message: result.message,
});
}
} catch (error) {
logger.error(`🐛🐛 Internal Server Error ${error.message}`);
next(
new HttpError(
"Internal Server Error",
httpStatus.INTERNAL_SERVER_ERROR,
{ message: error.message }
)
);
return;
}
},
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add unit tests for the new listReadingAverages endpoint.

The new endpoint lacks test coverage, which is crucial for ensuring reliability and preventing regressions. Consider adding tests that cover:

  • Success scenarios with different query parameters
  • Error handling for invalid inputs
  • Edge cases with empty/null responses

Would you like me to help create a test suite for this endpoint?

🧰 Tools
🪛 GitHub Check: codecov/patch

[warning] 942-945: src/device-registry/controllers/create-event.js#L942-L945
Added lines #L942 - L945 were not covered by tests


[warning] 948-948: src/device-registry/controllers/create-event.js#L948
Added line #L948 was not covered by tests


[warning] 951-951: src/device-registry/controllers/create-event.js#L951
Added line #L951 was not covered by tests


[warning] 960-960: src/device-registry/controllers/create-event.js#L960
Added line #L960 was not covered by tests


[warning] 962-963: src/device-registry/controllers/create-event.js#L962-L963
Added lines #L962 - L963 were not covered by tests


[warning] 966-968: src/device-registry/controllers/create-event.js#L966-L968
Added lines #L966 - L968 were not covered by tests


[warning] 974-975: src/device-registry/controllers/create-event.js#L974-L975
Added lines #L974 - L975 were not covered by tests


[warning] 982-983: src/device-registry/controllers/create-event.js#L982-L983
Added lines #L982 - L983 were not covered by tests


[warning] 990-990: src/device-registry/controllers/create-event.js#L990
Added line #L990 was not covered by tests

Comment on lines +1250 to +1333
listAverages: async (req, res, next) => {
try {
const errors = extractErrorsFromRequest(req);
if (errors) {
next(
new HttpError("bad request errors", httpStatus.BAD_REQUEST, errors)
);
return;
}

const request = req;
const defaultTenant = constants.DEFAULT_TENANT || "airqo";
request.query.tenant = isEmpty(req.query.tenant)
? defaultTenant
: req.query.tenant;

request.query.recent = "no";
request.query.metadata = "site_id";
request.query.averages = "events";
request.query.brief = "yes";
const { cohort_id, grid_id } = { ...req.query, ...req.params };

let locationErrors = 0;

if (cohort_id) {
await processCohortIds(cohort_id, request);
if (isEmpty(request.query.device_id)) {
locationErrors++;
}
} else if (grid_id) {
await processGridIds(grid_id, request);
if (isEmpty(request.query.site_id)) {
locationErrors++;
}
}

if (locationErrors === 0) {
const result = await createEventUtil.listAverages(request, next);

if (isEmpty(result) || res.headersSent) {
return;
}

if (result.success === true) {
const status = result.status ? result.status : httpStatus.OK;

res.status(status).json({
success: true,
isCache: result.isCache,
message: result.message,
measurements: result.data,
});
} else if (result.success === false) {
const status = result.status
? result.status
: httpStatus.INTERNAL_SERVER_ERROR;
const errors = result.errors ? result.errors : { message: "" };
res.status(status).json({
success: false,
errors,
message: result.message,
});
}
} else {
res.status(httpStatus.BAD_REQUEST).json({
success: false,
errors: {
message: `Unable to process measurements for the provided measurement IDs`,
},
message: "Bad Request",
});
}
} catch (error) {
logger.error(`🐛🐛 Internal Server Error ${error.message}`);
next(
new HttpError(
"Internal Server Error",
httpStatus.INTERNAL_SERVER_ERROR,
{ message: error.message }
)
);
return;
}
},
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add unit tests for the new listAverages endpoint.

The new endpoint requires test coverage to ensure proper functionality. Consider adding tests that cover:

  • Success scenarios for both cohort and grid processing
  • Error handling for invalid cohort/grid IDs
  • Edge cases with empty/null responses

Would you like me to help create a test suite for this endpoint?

🧰 Tools
🪛 GitHub Check: codecov/patch

[warning] 1251-1254: src/device-registry/controllers/create-event.js#L1251-L1254
Added lines #L1251 - L1254 were not covered by tests


[warning] 1257-1257: src/device-registry/controllers/create-event.js#L1257
Added line #L1257 was not covered by tests


[warning] 1260-1262: src/device-registry/controllers/create-event.js#L1260-L1262
Added lines #L1260 - L1262 were not covered by tests


[warning] 1266-1270: src/device-registry/controllers/create-event.js#L1266-L1270
Added lines #L1266 - L1270 were not covered by tests


[warning] 1272-1272: src/device-registry/controllers/create-event.js#L1272
Added line #L1272 was not covered by tests


[warning] 1274-1277: src/device-registry/controllers/create-event.js#L1274-L1277
Added lines #L1274 - L1277 were not covered by tests


[warning] 1279-1282: src/device-registry/controllers/create-event.js#L1279-L1282
Added lines #L1279 - L1282 were not covered by tests


[warning] 1286-1287: src/device-registry/controllers/create-event.js#L1286-L1287
Added lines #L1286 - L1287 were not covered by tests


[warning] 1289-1290: src/device-registry/controllers/create-event.js#L1289-L1290
Added lines #L1289 - L1290 were not covered by tests


[warning] 1293-1294: src/device-registry/controllers/create-event.js#L1293-L1294
Added lines #L1293 - L1294 were not covered by tests


[warning] 1296-1296: src/device-registry/controllers/create-event.js#L1296
Added line #L1296 was not covered by tests


[warning] 1302-1303: src/device-registry/controllers/create-event.js#L1302-L1303
Added lines #L1302 - L1303 were not covered by tests


[warning] 1306-1307: src/device-registry/controllers/create-event.js#L1306-L1307
Added lines #L1306 - L1307 were not covered by tests


[warning] 1314-1314: src/device-registry/controllers/create-event.js#L1314
Added line #L1314 was not covered by tests


[warning] 1323-1324: src/device-registry/controllers/create-event.js#L1323-L1324
Added lines #L1323 - L1324 were not covered by tests


[warning] 1331-1331: src/device-registry/controllers/create-event.js#L1331
Added line #L1331 was not covered by tests

Comment on lines +1271 to +1284

let locationErrors = 0;

if (cohort_id) {
await processCohortIds(cohort_id, request);
if (isEmpty(request.query.device_id)) {
locationErrors++;
}
} else if (grid_id) {
await processGridIds(grid_id, request);
if (isEmpty(request.query.site_id)) {
locationErrors++;
}
}
Copy link
Contributor

@coderabbitai coderabbitai bot Oct 25, 2024

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider refactoring location processing logic.

The location processing logic (handling cohort_id and grid_id) is duplicated across multiple methods in this controller. Consider extracting it into a shared utility function.

Create a shared utility function:

+const processLocationIds = async (request, { cohort_id, grid_id }) => {
+  let locationErrors = 0;
+
+  if (cohort_id) {
+    await processCohortIds(cohort_id, request);
+    if (isEmpty(request.query.device_id)) {
+      locationErrors++;
+    }
+  } else if (grid_id) {
+    await processGridIds(grid_id, request);
+    if (isEmpty(request.query.site_id)) {
+      locationErrors++;
+    }
+  }
+
+  return locationErrors;
+};

 listAverages: async (req, res, next) => {
   try {
     // ... existing setup code ...
     
-    let locationErrors = 0;
-    if (cohort_id) {
-      await processCohortIds(cohort_id, request);
-      if (isEmpty(request.query.device_id)) {
-        locationErrors++;
-      }
-    } else if (grid_id) {
-      await processGridIds(grid_id, request);
-      if (isEmpty(request.query.site_id)) {
-        locationErrors++;
-      }
-    }
+    const locationErrors = await processLocationIds(request, { cohort_id, grid_id });

Committable suggestion was skipped due to low confidence.

🧰 Tools
🪛 GitHub Check: codecov/patch

[warning] 1272-1272: src/device-registry/controllers/create-event.js#L1272
Added line #L1272 was not covered by tests


[warning] 1274-1277: src/device-registry/controllers/create-event.js#L1274-L1277
Added lines #L1274 - L1277 were not covered by tests


[warning] 1279-1282: src/device-registry/controllers/create-event.js#L1279-L1282
Added lines #L1279 - L1282 were not covered by tests

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Create a Github issue for this.

Copy link
Contributor

Choose a reason for hiding this comment

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

@Baalmart, I've created a GitHub issue to track this refactoring suggestion: Issue #3779.

🔗 Analysis chain
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Create a new GitHub issue for refactoring the location processing logic
gh issue create \
  --title "Refactor location processing logic in create-event.js" \
  --body "As discussed in [PR #3762](https://github.com/airqo-platform/AirQo-api/pull/3762), the location processing logic for handling \`cohort_id\` and \`grid_id\` is duplicated across multiple methods in \`src/device-registry/controllers/create-event.js\`. Extracting this logic into a shared utility function would improve code maintainability and reduce redundancy.

This issue was requested by @Baalmart in [this comment](https://github.com/airqo-platform/AirQo-api/pull/3762#discussion_r1816465509)." \
  --assignee "Baalmart"

Length of output: 657

@Baalmart Baalmart merged commit c46889b into staging Oct 25, 2024
48 of 50 checks passed
@Baalmart Baalmart deleted the ft-air-quality-averages branch October 25, 2024 10:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant