Skip to content

Update backend controllers with timescale#277

Merged
alexwhelan12 merged 23 commits intomainfrom
update-backend-controllers-with-timescale
Jan 31, 2026
Merged

Update backend controllers with timescale#277
alexwhelan12 merged 23 commits intomainfrom
update-backend-controllers-with-timescale

Conversation

@alexwhelan12
Copy link
Contributor

@alexwhelan12 alexwhelan12 commented Jan 31, 2026

Summary by CodeRabbit

  • New Features

    • Enhanced favorites system with improved path-based labeling for better readability.
  • Improvements

    • Streamlined fault and warning displays with cleaner data representation.
    • Updated database configuration and credentials management for improved data handling.
    • Refined driver and lap data retrieval and storage.

✏️ Tip: You can customize this high-level summary in your review settings.

@alexwhelan12 alexwhelan12 requested a review from a team as a code owner January 31, 2026 21:31
@vercel
Copy link

vercel bot commented Jan 31, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
helios-telemetry Ready Ready Preview, Comment Jan 31, 2026 9:55pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 31, 2026

Warning

Rate limit exceeded

@alexwhelan12 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 0 minutes and 45 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📝 Walkthrough

Walkthrough

This pull request consolidates the telemetry data model by replacing individual sensor entity tables with a single TelemetryPacket hypertable, migrating the backend from DynamoDB to TimescaleDB-backed storage. It updates client-side PIS components to track field paths and removes isFault flags, while refactoring server layers to use a new DatabaseService.

Changes

Cohort / File(s) Summary
Backend Database Configuration
packages/amplify/amplify/backend.ts, packages/db/.env.example, packages/db/src/data-source.ts
Updated database credentials sourcing to use TimescaleConnectionString via fromSecretNameV2 for ECS tasks; removed legacy POSTGRES_PASSWORD and POSTGRES_USERNAME configuration; simplified AppDataSource to use fixed "tsdb" database with environment-driven credentials and production-specific SSL/logging behavior.
Consolidated Telemetry Entity
packages/db/src/entities/TelemetryPacket.entity.ts
Introduced new TelemetryPacket hypertable entity consolidating metadata, battery, faults, motors, MPPT, proximity sensors, and GPS/IMU telemetry fields (~1000 lines) with composite key on Timestamp and Rfid; replaces individual per-sensor entities with wide-table schema.
Removed Individual Sensor Entities
packages/db/src/entities/B3.entity.ts, Battery.entity.ts, BatteryFaults.entity.ts, Contactor.entity.ts, KeyMotor.entity.ts, MBMS.entity.ts, MotorDetails.entity.ts, MPPT.entity.ts, ProximitySensors.entity.ts, Telemetry.entity.ts, TelemetryMetadata.entity.ts
Deleted eleven sensor-specific hypertable entities (~1200 lines removed); data consolidation into TelemetryPacket.
Updated Driver & Lap Entities
packages/db/src/entities/Driver.entity.ts, Lap.entity.ts
Renamed Driver properties to PascalCase (rfid→Rfid, name→Name, created_at→CreatedAt, updated_at→UpdatedAt); refactored Lap to use PascalCase field names (id→Id, lap_time→LapTime, etc.) and removed Hypertable decorator; updated indexes to match new field names.
Repository Layer Refactoring
packages/db/src/repositories/*.ts
Removed repositories for B3, Battery, BatteryFaults, Contactor, KeyMotor, MBMS, MotorDetails, MPPT, ProximitySensors, Telemetry, and TelemetryMetadata; added TelemetryPacketRepository extending BaseRepository; updated BaseRepository interface to declare PascalCase keys (Timestamp, Rfid); expanded IBaseRepository with full CRUD methods (findById, findOne, findMany, update, delete, count).
Repository Interface Updates
packages/db/src/interfaces/repositories.interface.ts, packages/db/src/index.ts
Consolidated repository interfaces by removing eleven entity-specific repositories and adding ITelemetryPacketRepository; expanded IBaseRepository with query/mutation methods; updated imports and exports to reflect only TelemetryPacket, Driver, and Lap repositories.
Database Service Implementation
packages/db/src/services/DatabaseService.ts, DatabaseService.types.ts
Introduced singleton DatabaseService with lifecycle management (getInstance, initialize, close) and data accessors (getDrivers, getDriverNameUsingRfid, getDriverLaps, updateDriverInfo, getPacketData, scanPacketDataBetweenDates, insertPacketData, getFirstAndLastPacketDates, insertLapData, getLapData, insertIntoGpsLapCountTable); added flattenTelemetryData helper for packet normalization; added GenericResponse interface for standardized responses.
Server Backend Migration
packages/server/src/controllers/BackendController/BackendController.ts, BackendController.types.ts, packages/server/src/database/DatabaseManager.ts
Replaced DynamoDB and DatabaseManager with DatabaseService singleton; updated BackendController to initialize via DatabaseService.getInstance() and route packet/driver/lap operations through timescaleDB; removed DatabaseManager wrapper class.
Server Route Controller Updates
packages/server/src/controllers/routeControllers/driver.controller.ts, lap.controller.ts, playback.controller.ts
Updated data source calls from DynamoDB/DatabaseManager to DatabaseService for getDrivers, getDriverNameUsingRfid, getDriverLaps, updateDriverInfo, getPacketData, scanPacketDataBetweenDates, getLapData, and getFirstAndLastPacketDates.
Removed DynamoDB Datasource
packages/server/src/datasources/DynamoDB/DynamoDB.ts, DynamoDB.types.ts
Deleted entire DynamoDB implementation class and interfaces (~370 lines); removed DynamoDBClient instantiation, table name configuration, and all query/mutation methods; eliminated GenericResponse type duplication.
MQTT Client Enhancement
packages/server/src/datasources/SolarMQTTClient/SolarMQTTClient.ts, SolarMQTTClient.types.ts
Replaced dynamoDB with timescaleDB for driver name lookups; added publishLapData method and lapdataTopic to publish lap telemetry; added ILapData import and ensured initializeListeners is called after MQTT connection.
Client PIS Path Tracking
packages/client/src/components/transformers/PISTransformer.tsx
Introduced path state tracking through component hierarchy with path?: string[] prop; updated FieldPrinter and FieldsPrinter to accept and propagate basePath/fieldPath; refactored "Add to Favourites" logic to use fieldPath instead of field.name; removed field.isFault conditional rendering and always render add-to-favourites button.
Client PIS Data Updates
packages/client/src/hooks/PIS/PIS.battery.tsx, PIS.faults.tsx, packages/client/src/hooks/favouriteLookupTable.ts
Removed isFault: true flag from all fault/warning entries across BatteryFaults, ContactorFaults, MBMSFaults, and MotorFaults; updated favouriteLookupTable extraction logic to target arrays of I_PISField instead of generic object flattening; added I_PISField import for type checking.
Client UI Improvements
packages/client/src/components/containers/BottomInformationContainer.tsx
Added mbms to PIS data extraction; introduced formatPathLabel helper to prettify dotted path strings with spaces at camelCase boundaries and before digits; applied formatted labels to favourites rendering while preserving lookup/removal logic.
Environment Configuration
packages/server/.env.example
Added trailing newline to .env.example file (formatting only).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • burtonjong
  • justin-phxm

Poem

🐰 From sensors spread across the land,
Now gathered in one packet grand,
DynamoDB fades to night,
TimescaleDB shines so bright!
Paths tracked through PIS trees with care,
Data consolidated everywhere! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'Update backend controllers with timescale' directly describes the primary change: migrating backend controllers from DynamoDB to TimescaleDB. It is clear, concise, and accurately reflects the main objective of this comprehensive refactor.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch update-backend-controllers-with-timescale

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
packages/amplify/amplify/backend.ts (2)

479-479: ⚠️ Potential issue | 🟡 Minor

Fix typo in comment.

"hte" → "the"

📝 Proposed fix
-// Give DynamoDB Permissions to hte packet data and lap data
+// Give DynamoDB Permissions to the packet data and lap data

122-163: ⚠️ Potential issue | 🟠 Major

Remove unused DynamoDB tables to eliminate unnecessary AWS costs.

These DynamoDB tables (packetDataTable, lapDataTable, driverDataTable, gpsCalculatedLapDataTable) are provisioned at lines 122-163 and environment variables are exported at lines 323-327, but the server package does not reference them anywhere. The application now uses TimescaleDB (connection string active at lines 355, 359, 363, 367, 384, 397). These tables should be removed to avoid unnecessary infrastructure costs, unless they're being retained for archived data migration—in which case, add a comment documenting the retention strategy and timeline for deletion.

packages/db/src/repositories/BaseRepository.ts (1)

10-19: ⚠️ Potential issue | 🟠 Major

Primary key default value has casing mismatch with interface.

The BaseEntity interface now uses PascalCase (Timestamp, Rfid), but the constructor's default primaryKey value on line 18 is still lowercase "timestamp". This mismatch could cause issues if a subclass relies on the default value.

🐛 Proposed fix
   constructor(
     protected repository: Repository<T>,
-    protected primaryKey: keyof T = "timestamp" as keyof T,
+    protected primaryKey: keyof T = "Timestamp" as keyof T,
   ) {}
packages/server/src/controllers/BackendController/BackendController.ts (1)

59-68: ⚠️ Potential issue | 🟠 Major

Missing await on async database insert operation.

insertPacketData returns a Promise<GenericResponse> but is not awaited. This fire-and-forget pattern means:

  1. Database errors will be silently swallowed
  2. Under high packet rates, unresolved promises could accumulate
  3. No guarantee data is persisted before broadcasting to frontend

Consider awaiting the insert and handling errors appropriately, or at minimum adding a .catch() handler.

🐛 Proposed fix - Option 1: Await with error handling
   public async handlePacketReceive(message: ITelemetryData) {
     // Insert the packet into the database
-    this.timescaleDB.insertPacketData(message);
+    try {
+      await this.timescaleDB.insertPacketData(message);
+    } catch (error) {
+      logger.error("Failed to insert packet data:", error);
+    }
 
     // Broadcast the packet to the frontend
     this.socketIO.broadcastPacket(message);
🐛 Proposed fix - Option 2: Fire-and-forget with error logging
   public async handlePacketReceive(message: ITelemetryData) {
     // Insert the packet into the database
-    this.timescaleDB.insertPacketData(message);
+    this.timescaleDB.insertPacketData(message).catch((error) => {
+      logger.error("Failed to insert packet data:", error);
+    });
 
     // Broadcast the packet to the frontend
     this.socketIO.broadcastPacket(message);
🤖 Fix all issues with AI agents
In `@packages/amplify/amplify/backend.ts`:
- Around line 55-59: The stack references a pre-existing Secrets Manager secret
via secretsmanager.Secret.fromSecretNameV2(TelemetryBackendStack,
"TimescaleConnectionString", "TimescaleConnectionString"), so create a Secrets
Manager secret named exactly "TimescaleConnectionString" before deployment and
populate it with JSON keys DATABASE_HOST, DATABASE_PASSWORD, DATABASE_PORT, and
DATABASE_USERNAME (with appropriate values); verify the secret name matches the
string passed to fromSecretNameV2 and that the JSON structure is correct so the
ECS task can read each key at runtime.

In `@packages/db/src/data-source.ts`:
- Around line 38-42: The production DB config currently spreads ...(isProd && {
ssl: { rejectUnauthorized: false } }) which disables certificate validation;
update the isProd SSL branch to set rejectUnauthorized: true and load/attach CA
certificates (e.g., from an environment-provided PEM) into the ssl.ca option
instead of turning validation off; ensure the code that builds the data-source
config (the isProd/ssl block) reads a CA env var or file and assigns ssl.ca when
present and falls back to proper system CA validation.
- Around line 12-19: The current env-validation block incorrectly requires
DATABASE_PORT even though later code falls back to 5432; remove DATABASE_PORT
from the required-check so the throw condition only tests DATABASE_HOST,
DATABASE_USERNAME, and DATABASE_PASSWORD, keeping the fallback to 5432 intact
(update the if conditional that throws the Error and retain the existing
fallback logic that reads process.env.DATABASE_PORT || "5432").

In `@packages/db/src/services/DatabaseService.ts`:
- Around line 140-150: getDrivers currently swallows errors and can return
undefined; add the same isConnected check used in other DatabaseService methods
at the start of getDrivers (call the isConnected guard and throw or return an
empty array if not connected), and update the catch block to either rethrow the
caught error or return an empty array so callers never receive undefined;
specifically, ensure the method around driverRepo.find uses try/catch to log the
error and then return [] (or throw) and include the isConnected check before
calling driverRepo.find to match the service's behavior.
- Around line 152-183: Add the same database connection guard used in
getPacketData/insertPacketData to both getDriverNameUsingRfid and getDriverLaps:
at the start of each method check this.isConnected (or the class's connection
boolean) and throw or return a clear error if not connected, so these methods
fail consistently when DB isn’t initialized; update getDriverNameUsingRfid and
getDriverLaps to perform that check before calling this.driverRepo/find or
this.lapRepo.find and keep existing try/catch behavior afterward.
- Around line 16-34: The constructor of DatabaseService currently calls
AppDataSource.getRepository(...) (telemetryPacketRepo, driverRepo, lapRepo)
before the DataSource is initialized; change to lazy initialize
repositories—remove repository calls from the constructor and instead obtain
them after AppDataSource.initialize() (e.g., inside initialize() or via private
getter helpers that call AppDataSource.getRepository(...) on first access).
Ensure DatabaseService.initialize() sets isConnected and then assigns
telemetryPacketRepo, driverRepo, and lapRepo by calling
AppDataSource.getRepository for TelemetryPacket, Driver, and Lap so repository
access never happens before AppDataSource.initialize().
- Around line 349-354: The Timestamp being saved via lapRepo.save (Timestamp) is
using timestamp ?? new Date().getTime() (a number) — convert the incoming
timestamp into a Date object (e.g., new Date(timestamp) or new Date()) so
Timestamp is a Date; also stop defaulting Rfid to the string "unknown driver"
which masks bad input — either require/validate rfid and throw or persist
null/undefined instead (e.g., Rfid: rfid ?? null) and let upstream validation
handle missing RFIDs; update the object passed to lapRepo.save (fields: Rfid,
Timestamp, Type: "gps-lap") accordingly.

In `@packages/server/.env.example`:
- Line 23: Remove the unnecessary surrounding quotes from the environment value
for LAP_POSITION_PASSWORD in the .env example (change
LAP_POSITION_PASSWORD="changeme" to an unquoted LAP_POSITION_PASSWORD=changeme)
so dotenv-linter's QuoteCharacter warning is resolved; alternatively, if quoting
is required for some reason, update the dotenv-linter config to allow quoted
values for that variable name or pattern instead of leaving the quoted example
value.

In `@packages/server/src/datasources/SolarMQTTClient/SolarMQTTClient.ts`:
- Around line 72-78: The try/catch in publishLapData won't catch MQTT publish
failures because MQTT.js reports errors via the publish callback; update
publishLapData to call this.client.publish(lapdataTopic,
JSON.stringify(lapdata), callback) and handle the callback error (log via
logger.error with context and error details) or wrap the callback in a Promise
and await it so callers can observe failures; reference the publishLapData
method, this.client.publish, lapdataTopic, and logger.error when making the
change.
🧹 Nitpick comments (7)
packages/db/src/repositories/BaseRepository.ts (1)

40-47: Minor typo in comment.

"entitiy" should be "entity".

✏️ Proposed fix
-    // no nested updates possible with current any entitiy so Partial<T> is okay
+    // no nested updates possible with current entity so Partial<T> is okay
packages/db/src/entities/Lap.entity.ts (1)

4-9: Outdated comment references hypertable but decorator was removed.

The docstring still mentions "Lap data hypertable" but the @Hypertable decorator has been removed (unlike TelemetryPacket which retains it). Either update the comment to reflect it's now a regular table, or verify if the hypertable configuration should be restored.

✏️ Proposed fix if intentionally a regular table
 /**
- * Lap data hypertable
+ * Lap data table
  * Stores computed lap metrics for each lap
  */
packages/db/src/data-source.ts (1)

23-37: Consider parameterizing the database name.

The database name "tsdb" is hardcoded. For flexibility across environments (dev, staging, prod), consider making it configurable via environment variable.

♻️ Proposed enhancement
 export const AppDataSource = new DataSource({
-  database: "tsdb",
+  database: process.env.DATABASE_NAME || "tsdb",
   entities: [TelemetryPacket, Driver, Lap],
packages/db/src/interfaces/repositories.interface.ts (1)

8-16: IBaseRepository assumes timestamp-based identification, but not all entities use timestamp as primary key.

The findById, update, and delete methods all use timestamp: Date as the identifier. However, IDriverRepository extends this interface, and Driver entities are identified by Rfid (as seen in DatabaseService.getDriverNameUsingRfid). This creates a contract mismatch.

Consider either:

  1. Making IBaseRepository generic over the identifier type
  2. Defining entity-specific methods in each repository interface
Option 1: Generic identifier type
-export interface IBaseRepository<T> {
+export interface IBaseRepository<T, ID = Date> {
   create(entity: DeepPartial<T>): Promise<T>;
-  findById(timestamp: Date): Promise<T | null>;
+  findById(id: ID): Promise<T | null>;
   findOne(options: FindOneOptions<T>): Promise<T | null>;
   findMany(options?: FindManyOptions<T>): Promise<T[]>;
-  update(timestamp: Date, updates: DeepPartial<T>): Promise<T | null>;
-  delete(timestamp: Date): Promise<boolean>;
+  update(id: ID, updates: DeepPartial<T>): Promise<T | null>;
+  delete(id: ID): Promise<boolean>;
   count(options?: FindManyOptions<T>): Promise<number>;
 }

-export interface IDriverRepository extends IBaseRepository<Driver> {}
+export interface IDriverRepository extends IBaseRepository<Driver, string> {}
packages/db/src/services/DatabaseService.ts (3)

187-189: Redundant runtime type check for TypeScript-typed parameter.

The typeof Rfid !== "string" check is unnecessary since Rfid is already typed as string in the function signature. TypeScript enforces this at compile time.

Remove redundant check
   public async updateDriverInfo(Rfid: string, name: string) {
+    if (!this.isConnected) {
+      throw new Error("Database not connected");
+    }
     try {
-      if (typeof Rfid !== "string") {
-        throw new Error("Rfid must be a string");
-      }
-
       const existingDriver = await this.driverRepo.findOne({

148-148: Replace console statements with a logging framework.

Multiple console.error and console.log statements are flagged by ESLint. For production code, consider using a structured logging library (e.g., winston, pino) that supports log levels, formatting, and output configuration.

If console logging is intentional for this service, you may want to disable the ESLint rule selectively or configure an allowed logging utility.

Also applies to: 159-159, 165-165, 178-178, 207-207, 215-215


287-294: Consider using aggregate queries for better performance.

Two separate queries with ORDER BY and LIMIT 1 are less efficient than using MIN/MAX aggregates, especially on large TimescaleDB hypertables.

Optimized approach using query builder
const result = await this.telemetryPacketRepo
  .createQueryBuilder("packet")
  .select("MIN(packet.Timestamp)", "firstDate")
  .addSelect("MAX(packet.Timestamp)", "lastDate")
  .getRawOne();

return {
  firstDateUTC: result?.firstDate ? new Date(result.firstDate).getTime() : null,
  lastDateUTC: result?.lastDate ? new Date(result.lastDate).getTime() : null,
};

Comment on lines 55 to 59
const TimescaleConnectionString = secretsmanager.Secret.fromSecretNameV2(
TelemetryBackendStack,
"HeliosTelemetryDBCredentials",
{
secretName: "HeliosTelemetryDBCredentials" + backend.stack.stackName,
secretObjectValue: {
POSTGRES_PASSWORD: cdk.SecretValue.unsafePlainText(""),
POSTGRES_USERNAME: cdk.SecretValue.unsafePlainText(""),
},
},
"TimescaleConnectionString",
"TimescaleConnectionString",
);
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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the file structure to understand the repository layout
git ls-files | head -20

Repository: UCSolarCarTeam/Helios-Telemetry

Length of output: 443


🏁 Script executed:

# Search for references to the TimescaleConnectionString secret
rg "TimescaleConnectionString" -A 5 -B 2

Repository: UCSolarCarTeam/Helios-Telemetry

Length of output: 2747


🏁 Script executed:

# Search for the specific database environment variable keys mentioned
rg "DATABASE_HOST|DATABASE_PASSWORD|DATABASE_PORT|DATABASE_USERNAME" -A 2 -B 2

Repository: UCSolarCarTeam/Helios-Telemetry

Length of output: 2760


🏁 Script executed:

# Look for any setup or deployment documentation
fd -e md -e txt | xargs grep -l -i "secret\|timescale\|database" 2>/dev/null | head -10

Repository: UCSolarCarTeam/Helios-Telemetry

Length of output: 168


🏁 Script executed:

# Check the ECS task definition around lines 354-369
sed -n '350,375p' packages/amplify/amplify/backend.ts

Repository: UCSolarCarTeam/Helios-Telemetry

Length of output: 956


🏁 Script executed:

# Search for any AWS secrets configuration or deployment scripts
fd -e sh -e py -e ts -e js | xargs grep -l "Secrets Manager\|secrets\|DATABASE" 2>/dev/null | grep -E "(setup|deploy|init|config)" | head -5

Repository: UCSolarCarTeam/Helios-Telemetry

Length of output: 57


🏁 Script executed:

# Check the AMPLIFY documentation for secret setup instructions
cat docs/AMPLIFY.md

Repository: UCSolarCarTeam/Helios-Telemetry

Length of output: 7620


🏁 Script executed:

# Check the database package documentation
cat packages/db/README.md

Repository: UCSolarCarTeam/Helios-Telemetry

Length of output: 243


🏁 Script executed:

# Check for any other deployment or setup documentation
cat docs/SERVER.md | head -100

Repository: UCSolarCarTeam/Helios-Telemetry

Length of output: 5131


🏁 Script executed:

# Search for any CloudFormation templates or IaC that might provision the secret
find . -name "*.yml" -o -name "*.yaml" -o -name "*.json" | grep -E "(cloudformation|template|infrastructure|secrets)" | head -10

Repository: UCSolarCarTeam/Helios-Telemetry

Length of output: 57


🏁 Script executed:

# Check if there's any reference to secret provisioning in the codebase
rg "TimescaleConnectionString" -B 5 -A 5 --type-add 'config:*.yml' --type config --type-add 'template:*.json' --type template

Repository: UCSolarCarTeam/Helios-Telemetry

Length of output: 57


🏁 Script executed:

# Look for any environment setup or deployment instructions in README files
find . -maxdepth 2 -name "README*" -type f | xargs cat 2>/dev/null

Repository: UCSolarCarTeam/Helios-Telemetry

Length of output: 6068


Ensure the TimescaleConnectionString secret is pre-provisioned in AWS Secrets Manager before deployment.

The code references a TimescaleConnectionString secret and expects it to contain JSON keys: DATABASE_HOST, DATABASE_PASSWORD, DATABASE_PORT, and DATABASE_USERNAME. The ECS task will fail to start if this secret does not exist or if these keys are missing. Create this secret in Secrets Manager with the correct structure before deploying.

🤖 Prompt for AI Agents
In `@packages/amplify/amplify/backend.ts` around lines 55 - 59, The stack
references a pre-existing Secrets Manager secret via
secretsmanager.Secret.fromSecretNameV2(TelemetryBackendStack,
"TimescaleConnectionString", "TimescaleConnectionString"), so create a Secrets
Manager secret named exactly "TimescaleConnectionString" before deployment and
populate it with JSON keys DATABASE_HOST, DATABASE_PASSWORD, DATABASE_PORT, and
DATABASE_USERNAME (with appropriate values); verify the secret name matches the
string passed to fromSecretNameV2 and that the JSON structure is correct so the
ECS task can read each key at runtime.

Comment on lines 12 to 19
if (
!process.env.DATABASE_HOST ||
!process.env.DATABASE_PORT ||
!process.env.DATABASE_USERNAME ||
!process.env.DATABASE_PASSWORD
) {
throw new Error("Database configuration environment variables are not set.");
}
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 | 🟡 Minor

DATABASE_PORT validation is inconsistent with fallback logic.

The validation throws if DATABASE_PORT is not set, but lines 31-33 provide a fallback to 5432. Either remove DATABASE_PORT from the required validation (since there's a default), or remove the fallback to enforce the requirement.

🐛 Option 1: Remove from required validation (allow default)
 if (
   !process.env.DATABASE_HOST ||
-  !process.env.DATABASE_PORT ||
   !process.env.DATABASE_USERNAME ||
   !process.env.DATABASE_PASSWORD
 ) {
   throw new Error("Database configuration environment variables are not set.");
 }
🐛 Option 2: Remove fallback (enforce requirement)
-  port: process.env.DATABASE_PORT
-    ? parseInt(process.env.DATABASE_PORT, 10)
-    : 5432,
+  port: parseInt(process.env.DATABASE_PORT, 10),
📝 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
if (
!process.env.DATABASE_HOST ||
!process.env.DATABASE_PORT ||
!process.env.DATABASE_USERNAME ||
!process.env.DATABASE_PASSWORD
) {
throw new Error("Database configuration environment variables are not set.");
}
if (
!process.env.DATABASE_HOST ||
!process.env.DATABASE_USERNAME ||
!process.env.DATABASE_PASSWORD
) {
throw new Error("Database configuration environment variables are not set.");
}
🤖 Prompt for AI Agents
In `@packages/db/src/data-source.ts` around lines 12 - 19, The current
env-validation block incorrectly requires DATABASE_PORT even though later code
falls back to 5432; remove DATABASE_PORT from the required-check so the throw
condition only tests DATABASE_HOST, DATABASE_USERNAME, and DATABASE_PASSWORD,
keeping the fallback to 5432 intact (update the if conditional that throws the
Error and retain the existing fallback logic that reads
process.env.DATABASE_PORT || "5432").

Comment on lines 38 to 42
...(isProd && {
ssl: {
rejectUnauthorized: false,
},
}),
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 | 🟠 Major

Production SSL with rejectUnauthorized: false is insecure.

Setting rejectUnauthorized: false disables certificate validation, making the connection vulnerable to man-in-the-middle attacks. For production, consider:

  • Setting to true with proper CA certificates
  • Using ca option if using self-signed certs
🛡️ Proposed fix
   ...(isProd && {
     ssl: {
-      rejectUnauthorized: false,
+      rejectUnauthorized: true,
+      // If using self-signed certs, add: ca: process.env.DATABASE_CA_CERT
     },
   }),
📝 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
...(isProd && {
ssl: {
rejectUnauthorized: false,
},
}),
...(isProd && {
ssl: {
rejectUnauthorized: true,
// If using self-signed certs, add: ca: process.env.DATABASE_CA_CERT
},
}),
🤖 Prompt for AI Agents
In `@packages/db/src/data-source.ts` around lines 38 - 42, The production DB
config currently spreads ...(isProd && { ssl: { rejectUnauthorized: false } })
which disables certificate validation; update the isProd SSL branch to set
rejectUnauthorized: true and load/attach CA certificates (e.g., from an
environment-provided PEM) into the ssl.ca option instead of turning validation
off; ensure the code that builds the data-source config (the isProd/ssl block)
reads a CA env var or file and assigns ssl.ca when present and falls back to
proper system CA validation.

Comment on lines 16 to 34
constructor() {
this.dataSource = AppDataSource;
this.telemetryPacketRepo = AppDataSource.getRepository(TelemetryPacket);
this.driverRepo = AppDataSource.getRepository(Driver);
this.lapRepo = AppDataSource.getRepository(Lap);
}

async initialize(): Promise<void> {
async initialize() {
if (!this.isConnected) {
await AppDataSource.initialize();
this.isConnected = true;
}
}

static getInstance(): DatabaseService {
if (!DatabaseService.instance) {
DatabaseService.instance = new DatabaseService();
}
return DatabaseService.instance;
}
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 | 🔴 Critical

Repository access in constructor will fail if AppDataSource is not initialized.

The constructor calls AppDataSource.getRepository() immediately, but AppDataSource.initialize() is only called later in the initialize() method. TypeORM requires the DataSource to be initialized before accessing repositories.

This will throw an error like: "Cannot access repository before data source is initialized".

Proposed fix: Lazy repository initialization
 export class DatabaseService {
   private isConnected = false;
   private static instance: DatabaseService;
-  private telemetryPacketRepo: Repository<TelemetryPacket>;
-  private driverRepo: Repository<Driver>;
-  private lapRepo: Repository<Lap>;
+  private telemetryPacketRepo!: Repository<TelemetryPacket>;
+  private driverRepo!: Repository<Driver>;
+  private lapRepo!: Repository<Lap>;

-  constructor() {
-    this.telemetryPacketRepo = AppDataSource.getRepository(TelemetryPacket);
-    this.driverRepo = AppDataSource.getRepository(Driver);
-    this.lapRepo = AppDataSource.getRepository(Lap);
-  }
+  constructor() {}

   async initialize() {
     if (!this.isConnected) {
       await AppDataSource.initialize();
+      this.telemetryPacketRepo = AppDataSource.getRepository(TelemetryPacket);
+      this.driverRepo = AppDataSource.getRepository(Driver);
+      this.lapRepo = AppDataSource.getRepository(Lap);
       this.isConnected = true;
     }
   }
🤖 Prompt for AI Agents
In `@packages/db/src/services/DatabaseService.ts` around lines 16 - 34, The
constructor of DatabaseService currently calls AppDataSource.getRepository(...)
(telemetryPacketRepo, driverRepo, lapRepo) before the DataSource is initialized;
change to lazy initialize repositories—remove repository calls from the
constructor and instead obtain them after AppDataSource.initialize() (e.g.,
inside initialize() or via private getter helpers that call
AppDataSource.getRepository(...) on first access). Ensure
DatabaseService.initialize() sets isConnected and then assigns
telemetryPacketRepo, driverRepo, and lapRepo by calling
AppDataSource.getRepository for TelemetryPacket, Driver, and Lap so repository
access never happens before AppDataSource.initialize().

Comment on lines 140 to 150
public async getDrivers() {
try {
if (!this.dataSource.isInitialized) {
await this.dataSource.initialize();
console.log("Database connection initialized successfully");
const drivers = await this.driverRepo.find();
return drivers.map((driver) => ({
Rfid: driver.Rfid,
driver: driver.Name,
}));
} catch (error: unknown) {
console.error("Error getting drivers");
}
}
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 | 🟠 Major

Method returns undefined on error instead of throwing or returning an empty array.

The catch block logs an error but doesn't return a value, causing the method to implicitly return undefined. Callers iterating over the result (e.g., getDrivers().map(...)) will fail.

Also missing isConnected check unlike other methods.

Proposed fix
   public async getDrivers() {
+    if (!this.isConnected) {
+      throw new Error("Database not connected");
+    }
+
     try {
       const drivers = await this.driverRepo.find();
       return drivers.map((driver) => ({
         Rfid: driver.Rfid,
         driver: driver.Name,
       }));
     } catch (error: unknown) {
-      console.error("Error getting drivers");
+      throw new Error("Failed to get drivers: " + (error as Error).message);
     }
   }
🧰 Tools
🪛 ESLint

[error] 148-148: Unexpected console statement.

(no-console)

🤖 Prompt for AI Agents
In `@packages/db/src/services/DatabaseService.ts` around lines 140 - 150,
getDrivers currently swallows errors and can return undefined; add the same
isConnected check used in other DatabaseService methods at the start of
getDrivers (call the isConnected guard and throw or return an empty array if not
connected), and update the catch block to either rethrow the caught error or
return an empty array so callers never receive undefined; specifically, ensure
the method around driverRepo.find uses try/catch to log the error and then
return [] (or throw) and include the isConnected check before calling
driverRepo.find to match the service's behavior.

Comment on lines +152 to +183
public async getDriverNameUsingRfid(Rfid: string) {
try {
const driver = await this.driverRepo.findOne({
where: { Rfid },
});

if (!driver) {
console.error(`No driver found for Rfid: ${Rfid}`);
return "Driver not found";
}

return driver.Name;
} catch (error: unknown) {
console.error("Error getting driver name using the given Rfid");
throw new Error((error as Error).message);
}
}

public async getDriverLaps(Rfid: string) {
try {
const laps = await this.lapRepo.find({
order: { Timestamp: "DESC" },
where: { Rfid },
});
return laps;
} catch (error: unknown) {
console.error("Error getting lap data for driver", error);
throw new Error(
(error as Error).message || "Failed to fetch driver laps",
);
}
}
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 | 🟡 Minor

Missing isConnected checks in driver-related methods.

getDriverNameUsingRfid and getDriverLaps lack the isConnected guard that other methods (e.g., getPacketData, insertPacketData) have. This inconsistency could lead to confusing errors when the database isn't initialized.

Add consistent guards
   public async getDriverNameUsingRfid(Rfid: string) {
+    if (!this.isConnected) {
+      throw new Error("Database not connected");
+    }
     try {
       // ...

   public async getDriverLaps(Rfid: string) {
+    if (!this.isConnected) {
+      throw new Error("Database not connected");
+    }
     try {
       // ...
📝 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
public async getDriverNameUsingRfid(Rfid: string) {
try {
const driver = await this.driverRepo.findOne({
where: { Rfid },
});
if (!driver) {
console.error(`No driver found for Rfid: ${Rfid}`);
return "Driver not found";
}
return driver.Name;
} catch (error: unknown) {
console.error("Error getting driver name using the given Rfid");
throw new Error((error as Error).message);
}
}
public async getDriverLaps(Rfid: string) {
try {
const laps = await this.lapRepo.find({
order: { Timestamp: "DESC" },
where: { Rfid },
});
return laps;
} catch (error: unknown) {
console.error("Error getting lap data for driver", error);
throw new Error(
(error as Error).message || "Failed to fetch driver laps",
);
}
}
public async getDriverNameUsingRfid(Rfid: string) {
if (!this.isConnected) {
throw new Error("Database not connected");
}
try {
const driver = await this.driverRepo.findOne({
where: { Rfid },
});
if (!driver) {
console.error(`No driver found for Rfid: ${Rfid}`);
return "Driver not found";
}
return driver.Name;
} catch (error: unknown) {
console.error("Error getting driver name using the given Rfid");
throw new Error((error as Error).message);
}
}
public async getDriverLaps(Rfid: string) {
if (!this.isConnected) {
throw new Error("Database not connected");
}
try {
const laps = await this.lapRepo.find({
order: { Timestamp: "DESC" },
where: { Rfid },
});
return laps;
} catch (error: unknown) {
console.error("Error getting lap data for driver", error);
throw new Error(
(error as Error).message || "Failed to fetch driver laps",
);
}
}
🧰 Tools
🪛 ESLint

[error] 159-159: Unexpected console statement.

(no-console)


[error] 165-165: Unexpected console statement.

(no-console)


[error] 178-178: Unexpected console statement.

(no-console)

🤖 Prompt for AI Agents
In `@packages/db/src/services/DatabaseService.ts` around lines 152 - 183, Add the
same database connection guard used in getPacketData/insertPacketData to both
getDriverNameUsingRfid and getDriverLaps: at the start of each method check
this.isConnected (or the class's connection boolean) and throw or return a clear
error if not connected, so these methods fail consistently when DB isn’t
initialized; update getDriverNameUsingRfid and getDriverLaps to perform that
check before calling this.driverRepo/find or this.lapRepo.find and keep existing
try/catch behavior afterward.

Comment on lines +349 to +354
try {
await this.lapRepo.save({
Rfid: rfid ?? "unknown driver",
Timestamp: timestamp ?? new Date().getTime(),
Type: "gps-lap",
});
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 | 🔴 Critical

Type mismatch: Timestamp should be a Date, not a number.

Line 352 assigns timestamp ?? new Date().getTime() (a number) to Timestamp, but the Lap entity likely expects a Date type based on patterns elsewhere in this file (e.g., new Date(timestamp) on line 227).

Additionally, falling back to "unknown driver" silently masks invalid input data.

Proposed fix
   public async insertIntoGpsLapCountTable(
     rfid: string,
     timestamp: number,
   ): Promise<GenericResponse> {
     if (!this.isConnected) {
       throw new Error("Database not connected");
     }

+    if (!rfid) {
+      throw new Error("rfid is required");
+    }
+
     try {
       await this.lapRepo.save({
-        Rfid: rfid ?? "unknown driver",
-        Timestamp: timestamp ?? new Date().getTime(),
+        Rfid: rfid,
+        Timestamp: new Date(timestamp),
         Type: "gps-lap",
       });
🤖 Prompt for AI Agents
In `@packages/db/src/services/DatabaseService.ts` around lines 349 - 354, The
Timestamp being saved via lapRepo.save (Timestamp) is using timestamp ?? new
Date().getTime() (a number) — convert the incoming timestamp into a Date object
(e.g., new Date(timestamp) or new Date()) so Timestamp is a Date; also stop
defaulting Rfid to the string "unknown driver" which masks bad input — either
require/validate rfid and throw or persist null/undefined instead (e.g., Rfid:
rfid ?? null) and let upstream validation handle missing RFIDs; update the
object passed to lapRepo.save (fields: Rfid, Timestamp, Type: "gps-lap")
accordingly.

# App secrets / other #
########################
LAP_POSITION_PASSWORD="changeme"
LAP_POSITION_PASSWORD="changeme"
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 | 🟡 Minor

Dotenv-linter QuoteCharacter warning on quoted value.
If dotenv-linter runs in CI, quoted values can fail lint; consider removing quotes or adjusting linter config.

💡 Suggested change (if you want to satisfy dotenv-linter)
-LAP_POSITION_PASSWORD="changeme"
+LAP_POSITION_PASSWORD=changeme
📝 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
LAP_POSITION_PASSWORD="changeme"
LAP_POSITION_PASSWORD=changeme
🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 23-23: [QuoteCharacter] The value has quote characters (', ")

(QuoteCharacter)

🤖 Prompt for AI Agents
In `@packages/server/.env.example` at line 23, Remove the unnecessary surrounding
quotes from the environment value for LAP_POSITION_PASSWORD in the .env example
(change LAP_POSITION_PASSWORD="changeme" to an unquoted
LAP_POSITION_PASSWORD=changeme) so dotenv-linter's QuoteCharacter warning is
resolved; alternatively, if quoting is required for some reason, update the
dotenv-linter config to allow quoted values for that variable name or pattern
instead of leaving the quoted example value.

Comment on lines 72 to 78
public async publishLapData(lapdata: ILapData) {
try {
this.client.publish(lapdataTopic, JSON.stringify(lapdata));
} catch (error) {
logger.error("Error publishing lap data: ", 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 | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

MQTT v5 client.publish signature error handling callback

💡 Result:

It depends on the client library you’re using—MQTT v5 adds reason codes / properties, but each library exposes them differently.

Node.js (MQTT.js)

Signature

client.publish(topic, message, options, callback)

Callback

function (err, packet) { ... }

The callback fires when QoS handling completes (or next tick for QoS 0). Handle errors via err; the packet lets you inspect the ack flow (useful for MQTT v5). You can also use publishAsync() which returns a Promise. [1]

Python (Eclipse Paho paho-mqtt)

Publish completion is handled via on_publish, not an inline publish(..., callback=...).

on_publish signatures

  • Callback API v2 (includes MQTT v5 info):
on_publish(client, userdata, mid, reason_code, properties)
  • Callback API v1:
on_publish(client, userdata, mid)

Use reason_code / properties (API v2) for MQTT v5-level error reporting; otherwise you only get mid. [2]

Java (Eclipse Paho)

Use the async publish overload that takes an IMqttActionListener:

publish(topic, payload, qos, retained, userContext, callback)

Error handling is via:

  • callback.onSuccess(IMqttToken token)
  • callback.onFailure(IMqttToken token, Throwable exception) [3]

You can also track completion via MqttCallback.deliveryComplete(IMqttDeliveryToken) instead of per-call listeners. [3]

Sources: [1] MQTT.js README (publish callback signature) • [2] Eclipse Paho Python docs (on_publish signatures incl. v5 reason_code/properties) • [3] Eclipse Paho Java docs (publish(..., IMqttActionListener) and failure callback)


🏁 Script executed:

# Find MQTT library configuration and imports
find . -name "package.json" -type f | head -5 | xargs -I {} grep -l mqtt "{}" 2>/dev/null

Repository: UCSolarCarTeam/Helios-Telemetry

Length of output: 59


🏁 Script executed:

# Look at the imports in the SolarMQTTClient file to identify the MQTT library
head -30 packages/server/src/datasources/SolarMQTTClient/SolarMQTTClient.ts

Repository: UCSolarCarTeam/Helios-Telemetry

Length of output: 869


🏁 Script executed:

cd packages/server && cat package.json | grep -A 2 -B 2 '"mqtt"'

Repository: UCSolarCarTeam/Helios-Telemetry

Length of output: 201


🏁 Script executed:

# Also check if publishAsync is mentioned anywhere in the codebase
rg "publishAsync" packages/server/

Repository: UCSolarCarTeam/Helios-Telemetry

Length of output: 57


Publish error handling won't trigger as written.

client.publish() (MQTT.js v5.8.0) reports errors via callback, not synchronous exceptions. The current try/catch will never catch publish failures, making message drops difficult to diagnose.

💡 Suggested fix using publish callback
-  public async publishLapData(lapdata: ILapData) {
-    try {
-      this.client.publish(lapdataTopic, JSON.stringify(lapdata));
-    } catch (error) {
-      logger.error("Error publishing lap data: ", error);
-    }
-  }
+  public publishLapData(lapdata: ILapData): Promise<void> {
+    return new Promise((resolve, reject) => {
+      this.client.publish(lapdataTopic, JSON.stringify(lapdata), (err) => {
+        if (err) {
+          logger.error("Error publishing lap data: ", err);
+          reject(err);
+        } else {
+          resolve();
+        }
+      });
+    });
+  }
📝 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
public async publishLapData(lapdata: ILapData) {
try {
this.client.publish(lapdataTopic, JSON.stringify(lapdata));
} catch (error) {
logger.error("Error publishing lap data: ", error);
}
}
public publishLapData(lapdata: ILapData): Promise<void> {
return new Promise((resolve, reject) => {
this.client.publish(lapdataTopic, JSON.stringify(lapdata), (err) => {
if (err) {
logger.error("Error publishing lap data: ", err);
reject(err);
} else {
resolve();
}
});
});
}
🤖 Prompt for AI Agents
In `@packages/server/src/datasources/SolarMQTTClient/SolarMQTTClient.ts` around
lines 72 - 78, The try/catch in publishLapData won't catch MQTT publish failures
because MQTT.js reports errors via the publish callback; update publishLapData
to call this.client.publish(lapdataTopic, JSON.stringify(lapdata), callback) and
handle the callback error (log via logger.error with context and error details)
or wrap the callback in a Promise and await it so callers can observe failures;
reference the publishLapData method, this.client.publish, lapdataTopic, and
logger.error when making the change.

@alexwhelan12 alexwhelan12 requested a review from promatty January 31, 2026 22:22
Copy link
Member

@promatty promatty left a comment

Choose a reason for hiding this comment

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

probably good

@alexwhelan12 alexwhelan12 merged commit 52ac2fb into main Jan 31, 2026
8 checks passed
@alexwhelan12 alexwhelan12 deleted the update-backend-controllers-with-timescale branch January 31, 2026 22:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants