@trafficgroup/knex-rel 0.1.5 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/plan.md CHANGED
@@ -1,806 +1,837 @@
1
- # Missing DAO Methods Implementation Plan
1
+ # Total Vehicle Class Implementation Plan
2
2
 
3
3
  ## Executive Summary
4
4
 
5
- This plan details the implementation of 5 missing DAO methods across CameraDAO and VideoDAO that are currently being called from api-rel services. All methods have been analyzed for call patterns, data flow, and database requirements.
5
+ This plan details the implementation of a "Total" vehicle class that aggregates all custom vehicle classes for both TMC and ATR minute results. The "Total" class will be added during transformation in the knex-rel module and will always appear as the LAST class in the vehicles object.
6
6
 
7
- ---
7
+ **Key Requirements**:
8
8
 
9
- ## Schema Analysis
9
+ - Add "Total" class AFTER all custom classes
10
+ - For ATR: "Total" reuses existing `lane_counts` (all classes per lane)
11
+ - For TMC: "Total" sums all vehicle classes across all directions and turns
12
+ - NO database schema changes required
13
+ - Transformation happens in `ReportConfigurationDAO.applyConfigurationToNestedStructure()`
10
14
 
11
- ### Tables Involved
15
+ ---
12
16
 
13
- **cameras**
17
+ ## Current Architecture Analysis
14
18
 
15
- ```sql
16
- id: SERIAL PRIMARY KEY
17
- uuid: UUID NOT NULL UNIQUE (indexed)
18
- name: VARCHAR(100) NOT NULL
19
- longitude: DECIMAL(10,7) NOT NULL
20
- latitude: DECIMAL(10,7) NOT NULL
21
- created_at: TIMESTAMP
22
- updated_at: TIMESTAMP
23
- INDEX: (uuid)
24
- INDEX: (longitude, latitude)
25
- ```
19
+ ### Data Flow
26
20
 
27
- **video**
28
-
29
- ```sql
30
- id: SERIAL PRIMARY KEY
31
- uuid: UUID NOT NULL UNIQUE
32
- folderId: INTEGER NOT NULL REFERENCES folders(id)
33
- cameraId: INTEGER NULL REFERENCES cameras(id) ON DELETE SET NULL
34
- name: VARCHAR
35
- videoLocation: VARCHAR
36
- status: VARCHAR (QUEUED|PROCESSING|COMPLETED|FAILED|PENDING)
37
- ... (additional fields)
38
- INDEX: (cameraId)
39
21
  ```
22
+ 1. Python Processor → Database (JSONB results with detection labels)
23
+
24
+ 2. VideoMinuteResultDAO → Retrieve raw minute results
25
+
26
+ 3. API Controller → Apply report configuration
27
+
28
+ 4. ReportConfigurationService.transformVideoResults()
29
+
30
+ 5. ReportConfigurationDAO.applyConfigurationToNestedStructure()
31
+
32
+ 6. Frontend → Display custom classes + Total
33
+ ```
34
+
35
+ ### Current Structure Patterns
40
36
 
41
- **folders**
37
+ **ATR Format (Lane-based)**:
42
38
 
43
- ```sql
44
- id: SERIAL PRIMARY KEY
45
- uuid: UUID NOT NULL UNIQUE
46
- name: VARCHAR
47
- cameraId: INTEGER NULL REFERENCES cameras(id) ON DELETE SET NULL
48
- INDEX: (cameraId)
39
+ ```json
40
+ {
41
+ "vehicles": {
42
+ "car": { "0": 45, "1": 50 },
43
+ "truck": { "0": 10, "1": 8 }
44
+ },
45
+ "lane_counts": { "0": 55, "1": 58 }, // Already has totals per lane!
46
+ "total_count": 113,
47
+ "study_type": "ATR"
48
+ }
49
49
  ```
50
50
 
51
- ---
51
+ **TMC Format (Direction/Turn-based)**:
52
52
 
53
- ## Method 1: CameraDAO.getAllWithSearch()
53
+ ```json
54
+ {
55
+ "vehicles": {
56
+ "car": {
57
+ "NORTH": { "straight": 10, "left": 5, "right": 3, "u-turn": 0 },
58
+ "SOUTH": { "straight": 8, "left": 2, "right": 4, "u-turn": 1 }
59
+ },
60
+ "truck": {
61
+ "NORTH": { "straight": 5, "left": 1, "right": 0, "u-turn": 0 }
62
+ }
63
+ },
64
+ "counts": {
65
+ "total_vehicles": 39,
66
+ "entry_vehicles": 39
67
+ },
68
+ "study_type": "TMC"
69
+ }
70
+ ```
54
71
 
55
- ### Call Analysis
72
+ **After Custom Class Transformation** (2 custom classes: "Light" and "Heavy"):
56
73
 
57
- **Location**: `api-rel/src/service/camera/camera.service.ts:274`
74
+ ```json
75
+ {
76
+ "vehicles": {
77
+ "Light": { "NORTH": { "straight": 10, "left": 5 }, ... },
78
+ "Heavy": { "NORTH": { "straight": 5, "left": 1 }, ... }
79
+ }
80
+ }
81
+ ```
58
82
 
59
- **Method Signature**:
83
+ **After Total Addition** (desired output):
60
84
 
61
- ```typescript
62
- async getAllWithSearch(page: number, limit: number, name?: string): Promise<IDataPaginator<ICamera>>
85
+ ```json
86
+ {
87
+ "vehicles": {
88
+ "Light": { "NORTH": { "straight": 10, "left": 5 }, ... },
89
+ "Heavy": { "NORTH": { "straight": 5, "left": 1 }, ... },
90
+ "Total": { "NORTH": { "straight": 15, "left": 6 }, ... } // Sum of all classes
91
+ }
92
+ }
63
93
  ```
64
94
 
65
- **Called From**: CameraService.getAllWithSearch()
95
+ ---
66
96
 
67
- - **Parameters**: page, limit, name (optional string for search)
68
- - **Return Value**: IDataPaginator<ICamera> - used to map to CameraDTO[]
69
- - **Purpose**: Filter cameras by name with pagination support
97
+ ## Implementation Location
70
98
 
71
- **Data Flow**:
99
+ ### File: `knex-rel/src/dao/report-configuration/report-configuration.dao.ts`
72
100
 
73
- ```
74
- API Request → CameraService.getAllWithSearch(page, limit, name)
75
- → CameraDAO.getAllWithSearch(page, limit, name)
76
- → Returns IDataPaginator<ICamera>
77
- → Service maps to CameraDTO[] (exposes UUID, hides ID)
78
- → API Response
79
- ```
101
+ **Method to Modify**: `applyConfigurationToNestedStructure()` (Lines 273-315)
80
102
 
81
- ### Implementation Requirements
103
+ **Current Implementation**:
82
104
 
83
- **SQL Query**:
105
+ 1. Builds reverse mapping: detection label → custom class name (Lines 278-290)
106
+ 2. Initializes empty structure for each custom class (Lines 293-296)
107
+ 3. Iterates through detection labels and merges into custom classes (Lines 299-312)
108
+ 4. Returns transformed structure (Line 314)
84
109
 
85
- ```sql
86
- -- Count query
87
- SELECT COUNT(*) as count FROM cameras WHERE name ILIKE '%search%'
110
+ **Where to Add "Total"**: AFTER line 312, BEFORE return statement (Line 314)
88
111
 
89
- -- Data query
90
- SELECT * FROM cameras
91
- WHERE name ILIKE '%search%'
92
- LIMIT ? OFFSET ?
93
- ```
112
+ ---
94
113
 
95
- **Key Details**:
114
+ ## Detailed Implementation Plan
96
115
 
97
- - Use ILIKE for case-insensitive search (PostgreSQL)
98
- - Search is optional (if name undefined/null, return all)
99
- - Use '%search%' pattern for partial matching
100
- - Follow existing getAll() pattern from camera.dao.ts:34-50
116
+ ### Step 1: Add "Total" Class After Custom Class Transformation
101
117
 
102
- **Performance Considerations**:
118
+ **Location**: `ReportConfigurationDAO.applyConfigurationToNestedStructure()` (After line 312)
103
119
 
104
- - Index on name column NOT currently exists - consider adding
105
- - ILIKE with leading wildcard prevents index usage
106
- - For now, acceptable for camera table (likely small dataset)
120
+ **Logic**:
107
121
 
108
- ---
122
+ ```typescript
123
+ // After line 312 (end of detection label iteration)
124
+ // Add "Total" class that aggregates all custom classes
125
+
126
+ result["Total"] = this._createTotalClass(result);
109
127
 
110
- ## Method 2: CameraDAO.getVideosByCamera()
128
+ return result;
129
+ ```
111
130
 
112
- ### Call Analysis
131
+ ### Step 2: Implement `_createTotalClass()` Helper Method
113
132
 
114
- **Location**: `api-rel/src/service/camera/camera.service.ts:308`
133
+ **Location**: Add new private method in `ReportConfigurationDAO` class (After line 350)
115
134
 
116
135
  **Method Signature**:
117
136
 
118
137
  ```typescript
119
- async getVideosByCamera(cameraId: number, page: number, limit: number): Promise<IDataPaginator<IVideo>>
138
+ private _createTotalClass(customClassesData: Record<string, any>): any
120
139
  ```
121
140
 
122
- **Called From**: CameraService.getVideosByCamera()
141
+ **Implementation**:
123
142
 
124
- - **Parameters**: cameraId (internal numeric ID), page, limit
125
- - **Return Value**: IDataPaginator with video data PLUS folder information
126
- - **Purpose**: Get all videos associated with a specific camera (paginated)
127
-
128
- **Data Flow**:
143
+ ```typescript
144
+ /**
145
+ * Create "Total" class by aggregating all custom vehicle classes
146
+ *
147
+ * Handles both ATR and TMC formats:
148
+ * - ATR: Aggregates counts per lane across all vehicle classes
149
+ * - TMC: Aggregates counts per direction and turn across all vehicle classes
150
+ *
151
+ * @param customClassesData - Transformed custom classes structure
152
+ * @returns Aggregated totals matching the same nested structure
153
+ */
154
+ private _createTotalClass(customClassesData: Record<string, any>): any {
155
+ const total: any = {};
156
+
157
+ // Iterate through all custom classes and merge their data
158
+ for (const [className, nestedData] of Object.entries(customClassesData)) {
159
+ // Use existing _deepMergeNumericData to sum all nested values
160
+ Object.assign(total, this._deepMergeNumericData(total, nestedData));
161
+ }
129
162
 
130
- ```
131
- API Request (cameraUuid) → CameraService.getVideosByCamera(cameraUuid, page, limit)
132
- → Get camera by UUID to obtain ID (line 302)
133
- → CameraDAO.getVideosByCamera(camera.id, page, limit)
134
- → Returns IDataPaginator<IVideo> with folder data
135
- → Service maps to VideoWithFolderDTO[] (line 311-312)
136
- → API Response
163
+ return total;
164
+ }
137
165
  ```
138
166
 
139
- **Critical Detail**: Service expects folder data in response (line 312: mapToVideoWithFolderDTO)
167
+ **Explanation**:
140
168
 
141
- ### Implementation Requirements
169
+ - Reuses existing `_deepMergeNumericData()` helper (Lines 328-350)
170
+ - Works for both ATR (2-level: vehicle → lane → count) and TMC (3-level: vehicle → direction → turn → count)
171
+ - No need to detect study type - structure-agnostic aggregation
172
+ - Handles empty data (returns empty object `{}`)
142
173
 
143
- **SQL Query** (must JOIN with folders):
174
+ ---
144
175
 
145
- ```sql
146
- -- Count query
147
- SELECT COUNT(*) as count
148
- FROM video v
149
- WHERE v.cameraId = ?
176
+ ## Key Ordering Strategy
150
177
 
151
- -- Data query with folder JOIN
152
- SELECT v.*, to_jsonb(f.*) as folder
153
- FROM video v
154
- INNER JOIN folders f ON v.folderId = f.id
155
- WHERE v.cameraId = ?
156
- LIMIT ? OFFSET ?
157
- ```
178
+ ### Ensuring "Total" Appears Last
158
179
 
159
- **Key Details**:
180
+ JavaScript object key ordering is preserved in modern engines (ES2015+) when:
160
181
 
161
- - MUST include folder data (use to_jsonb like VideoDAO.getById:21)
162
- - Filter by cameraId (indexed)
163
- - Follow VideoDAO.getAll() pattern for folder JOIN (lines 50-59)
164
- - Return IDataPaginator<IVideo> with folder field populated
182
+ 1. String keys are added in insertion order
183
+ 2. Keys are enumerated via `Object.entries()`, `Object.keys()`, `for...in`
165
184
 
166
- **Performance Considerations**:
185
+ **Our Implementation**:
167
186
 
168
- - cameraId already indexed (migration 20250911000000_migration.ts:20)
169
- - JOIN with folders is necessary for DTO mapping
170
- - Consider ORDER BY created_at DESC for chronological listing
187
+ ```typescript
188
+ // Lines 293-296: Initialize custom classes FIRST (insertion order)
189
+ for (const customClass of config.configuration.customClasses) {
190
+ result[customClass.name] = {};
191
+ }
171
192
 
172
- ---
193
+ // Lines 299-312: Populate custom classes (preserves order)
194
+ for (const [detectionLabel, nestedData] of Object.entries(vehiclesStructure)) {
195
+ // ... merge logic
196
+ }
173
197
 
174
- ## Method 3: VideoDAO.bulkUpdateCamera()
198
+ // NEW: Add "Total" LAST (after all custom classes)
199
+ result["Total"] = this._createTotalClass(result);
175
200
 
176
- ### Call Analysis
201
+ // Line 314: Return result (Total is last key)
202
+ return result;
203
+ ```
177
204
 
178
- **Location 1**: `api-rel/src/service/camera/camera.service.ts:364`
179
- **Location 2**: `api-rel/src/service/video/video.service.ts:34`
180
- **Location 3**: `api-rel/src/service/folder/folder.service.ts:96`
205
+ **Why This Works**:
181
206
 
182
- **Method Signature**:
207
+ - Custom classes initialized in config order (Lines 293-296)
208
+ - "Total" added AFTER all custom classes (new code)
209
+ - JavaScript guarantees insertion order preservation
210
+ - Frontend will render "Total" as last tab
183
211
 
184
- ```typescript
185
- async bulkUpdateCamera(videoIds: number[], cameraId: number | null, trx?: Knex.Transaction): Promise<number>
186
- ```
212
+ ---
187
213
 
188
- **Call Patterns**:
214
+ ## ATR vs TMC Behavior
189
215
 
190
- **Pattern 1 - CameraService.bulkAssignToVideos() (line 364)**:
216
+ ### ATR (Lane-Based Totals)
191
217
 
192
- - Context: Assigning camera to multiple videos
193
- - Parameters: videoIds (array of numeric IDs), camera.id (number)
194
- - Transaction: NO transaction passed (relies on DAO atomicity)
195
- - Return: updatedCount used for logging (line 365)
218
+ **Input Structure**:
219
+
220
+ ```json
221
+ {
222
+ "Light": { "0": 45, "1": 50 },
223
+ "Heavy": { "0": 10, "1": 8 }
224
+ }
225
+ ```
196
226
 
197
- **Pattern 2 - VideoService.bulkUpdateCamera() (line 34)**:
227
+ **Total Output**:
198
228
 
199
- - Context: Bulk update within service transaction
200
- - Parameters: videoIds, cameraId (can be null), trx (REQUIRED)
201
- - Transaction: YES - passed from service-level transaction (line 15)
202
- - Return: updatedCount used for result tracking (line 54)
203
- - Error Handling: Transaction rollback on failure (line 58)
229
+ ```json
230
+ {
231
+ "Total": { "0": 55, "1": 58 } // Sum per lane
232
+ }
233
+ ```
204
234
 
205
- **Pattern 3 - FolderService.cascadeUpdateVideosCamera() (line 96)**:
235
+ **Calculation**:
206
236
 
207
- - Context: Cascading camera assignment from folder to videos
208
- - Parameters: videoIds, cameraId (can be null), trx (OPTIONAL)
209
- - Transaction: OPTIONAL - may be passed from folder update (line 86)
210
- - Return: updatedCount for cascade logging (line 59)
237
+ - Lane "0": 45 (Light) + 10 (Heavy) = 55
238
+ - Lane "1": 50 (Light) + 8 (Heavy) = 58
211
239
 
212
- ### Implementation Requirements
240
+ **Alternative (Use existing `lane_counts`)**:
213
241
 
214
- **SQL Query**:
242
+ The ATR structure ALREADY has `lane_counts` at the top level:
215
243
 
216
- ```sql
217
- UPDATE video
218
- SET cameraId = ?, updated_at = NOW()
219
- WHERE id = ANY(?)
220
- RETURNING id
244
+ ```json
245
+ {
246
+ "vehicles": { "Light": {...}, "Heavy": {...} },
247
+ "lane_counts": { "0": 55, "1": 58 } // Already calculated!
248
+ }
221
249
  ```
222
250
 
223
- **Key Details**:
251
+ **Decision**: For ATR, we can EITHER:
224
252
 
225
- - Accept optional transaction parameter (trx?: Knex.Transaction)
226
- - Use transaction if provided, otherwise use default connection
227
- - Update cameraId (can be null for unassignment)
228
- - Update updated_at timestamp
229
- - Return count of updated rows (number)
230
- - Use whereIn() for array parameter
253
+ 1. **Option A (Recommended)**: Calculate "Total" by summing custom classes (consistent with TMC)
254
+ 2. ⚠️ **Option B**: Copy `lane_counts` directly into `vehicles["Total"]` (reuses existing data)
231
255
 
232
- **Transaction Handling**:
256
+ **Recommendation**: Use Option A (sum custom classes) for consistency with TMC, even though Option B is technically available.
233
257
 
234
- ```typescript
235
- const query = trx || this._knex;
236
- const result = await query("video")
237
- .whereIn("id", videoIds)
238
- .update({ cameraId, updated_at: query.fn.now() })
239
- .returning("id");
240
- return result.length;
258
+ ### TMC (Direction/Turn-Based Totals)
259
+
260
+ **Input Structure**:
261
+
262
+ ```json
263
+ {
264
+ "Light": {
265
+ "NORTH": { "straight": 10, "left": 5, "right": 3 },
266
+ "SOUTH": { "straight": 8, "left": 2, "right": 4 }
267
+ },
268
+ "Heavy": {
269
+ "NORTH": { "straight": 5, "left": 1, "right": 0 },
270
+ "SOUTH": { "straight": 2, "left": 0, "right": 1 }
271
+ }
272
+ }
241
273
  ```
242
274
 
243
- **Performance Considerations**:
275
+ **Total Output**:
244
276
 
245
- - Bulk update is efficient (single query)
246
- - videoIds array should be validated (non-empty)
247
- - Consider max batch size limit (100-1000 videos)
248
- - No index needed on id (primary key)
277
+ ```json
278
+ {
279
+ "Total": {
280
+ "NORTH": { "straight": 15, "left": 6, "right": 3 },
281
+ "SOUTH": { "straight": 10, "left": 2, "right": 5 }
282
+ }
283
+ }
284
+ ```
249
285
 
250
- **Error Handling**:
286
+ **Calculation**:
251
287
 
252
- - If transaction fails in caller, rollback handles cleanup
253
- - Return 0 if videoIds is empty array
254
- - Let Knex errors propagate to service layer
288
+ - NORTH/straight: 10 (Light) + 5 (Heavy) = 15
289
+ - NORTH/left: 5 (Light) + 1 (Heavy) = 6
290
+ - SOUTH/straight: 8 (Light) + 2 (Heavy) = 10
291
+ - etc.
255
292
 
256
293
  ---
257
294
 
258
- ## Method 4: VideoDAO.getVideoIdsByFolderId()
295
+ ## Edge Cases & Validation
259
296
 
260
- ### Call Analysis
297
+ ### Edge Case 1: Empty Data (No Vehicles)
261
298
 
262
- **Location 1**: `api-rel/src/service/folder/folder.service.ts:49`
263
- **Location 2**: `api-rel/src/service/folder/folder.service.ts:89`
264
- **Location 3**: `api-rel/src/service/folder/folder.service.ts:163`
265
- **Location 4**: `api-rel/src/service/video/video.service.ts:119`
299
+ **Input**:
266
300
 
267
- **Method Signature**:
301
+ ```json
302
+ {
303
+ "vehicles": {}
304
+ }
305
+ ```
268
306
 
269
- ```typescript
270
- async getVideoIdsByFolderId(folderId: number): Promise<number[]>
307
+ **After Transformation**:
308
+
309
+ ```json
310
+ {
311
+ "vehicles": {
312
+ "Light": {},
313
+ "Heavy": {},
314
+ "Total": {} // Empty object
315
+ }
316
+ }
271
317
  ```
272
318
 
273
- **Call Patterns**:
319
+ **Behavior**: `_createTotalClass()` returns empty object `{}`
274
320
 
275
- **Pattern 1 - FolderService.updateWithCameraCascade() (line 49)**:
321
+ ### Edge Case 2: Single Custom Class
276
322
 
277
- - Context: Get all videos in folder to cascade camera update
278
- - Parameters: existingFolder.id
279
- - Return: videoIds array used for cascading (line 52-57)
280
- - Transaction Context: Within folder update transaction
323
+ **Input**:
281
324
 
282
- **Pattern 2 - FolderService.cascadeUpdateVideosCamera() (line 89)**:
325
+ ```json
326
+ {
327
+ "vehicles": {
328
+ "AllVehicles": { "0": 100, "1": 95 }
329
+ }
330
+ }
331
+ ```
283
332
 
284
- - Context: Get video IDs for bulk camera update
285
- - Parameters: folderId
286
- - Return: videoIds passed to bulkUpdateCamera (line 96)
287
- - Early return if empty array (line 91-93)
333
+ **Output**:
288
334
 
289
- **Pattern 3 - FolderService.getVideoCascadeCount() (line 163)**:
335
+ ```json
336
+ {
337
+ "vehicles": {
338
+ "AllVehicles": { "0": 100, "1": 95 },
339
+ "Total": { "0": 100, "1": 95 } // Same as single class
340
+ }
341
+ }
342
+ ```
290
343
 
291
- - Context: Count videos that would be affected by cascade
292
- - Parameters: folder.id
293
- - Return: videoIds.length for count (line 164)
294
- - Read-only operation (no updates)
344
+ **Behavior**: "Total" equals the single custom class (valid)
295
345
 
296
- **Pattern 4 - VideoService.getVideoIdsByFolderId() (line 119)**:
346
+ ### Edge Case 3: Missing Directions/Turns
297
347
 
298
- - Context: Get all video IDs in folder for cascade operations
299
- - Parameters: folderId
300
- - Return: number[] passed back to caller
301
- - Used for service-level cascade logic
348
+ **Input** (TMC - Light has NORTH, Heavy only has SOUTH):
302
349
 
303
- ### Implementation Requirements
350
+ ```json
351
+ {
352
+ "Light": { "NORTH": { "straight": 10 } },
353
+ "Heavy": { "SOUTH": { "straight": 5 } }
354
+ }
355
+ ```
304
356
 
305
- **SQL Query**:
357
+ **Output**:
306
358
 
307
- ```sql
308
- SELECT id FROM video WHERE folderId = ?
359
+ ```json
360
+ {
361
+ "Total": {
362
+ "NORTH": { "straight": 10 },
363
+ "SOUTH": { "straight": 5 }
364
+ }
365
+ }
309
366
  ```
310
367
 
311
- **Key Details**:
368
+ **Behavior**: `_deepMergeNumericData()` handles missing keys gracefully (Lines 339-347)
312
369
 
313
- - Simple SELECT of id column only
314
- - Filter by folderId
315
- - Return array of numeric IDs: number[]
316
- - No pagination needed (cascade operations need ALL videos)
317
- - No JOIN needed (only IDs required)
370
+ ### Edge Case 4: Null/Undefined Values
318
371
 
319
- **Performance Considerations**:
372
+ **Input**:
320
373
 
321
- - folderId should be indexed (check if exists)
322
- - Query is read-only, no locking needed
323
- - Could return large arrays (100s-1000s of videos per folder)
324
- - Consider ORDER BY id for consistency
374
+ ```json
375
+ {
376
+ "Light": { "0": 10, "1": null },
377
+ "Heavy": { "0": 5 }
378
+ }
379
+ ```
325
380
 
326
- **Return Value**:
381
+ **Output**:
327
382
 
328
- ```typescript
329
- const ids = await this._knex("video")
330
- .where({ folderId })
331
- .select("id")
332
- .orderBy("id", "asc");
333
- return ids.map((row) => row.id);
383
+ ```json
384
+ {
385
+ "Total": { "0": 15, "1": 0 } // null treated as 0
386
+ }
334
387
  ```
335
388
 
389
+ **Behavior**: `_deepMergeNumericData()` checks `typeof source === 'number'` (Line 330)
390
+
336
391
  ---
337
392
 
338
- ## Method 5: VideoDAO.getVideosByCameraIdWithFolder()
393
+ ## Testing Strategy
339
394
 
340
- ### Call Analysis
395
+ ### Unit Tests (to be created)
341
396
 
342
- **Location**: `api-rel/src/service/video/video.service.ts:77`
397
+ **File**: `knex-rel/src/dao/report-configuration/report-configuration.dao.test.ts`
343
398
 
344
- **Method Signature**:
399
+ **Test Cases**:
345
400
 
346
- ```typescript
347
- async getVideosByCameraIdWithFolder(cameraId: number, page: number, limit: number): Promise<IDataPaginator<IVideo>>
348
- ```
401
+ 1. **ATR: Total Class Aggregation**
402
+ - Input: 2 custom classes with lane counts
403
+ - Verify: Total sums all lanes correctly
404
+ - Verify: Total appears as last key
349
405
 
350
- **Called From**: VideoService.getVideosByCameraId()
406
+ 2. **TMC: Total Class Aggregation**
407
+ - Input: 2 custom classes with direction/turn structure
408
+ - Verify: Total sums all directions and turns
409
+ - Verify: Total appears as last key
351
410
 
352
- - **Parameters**: cameraId (numeric ID), page, limit
353
- - **Return Value**: IDataPaginator<IVideo> - directly returned to caller
354
- - **Purpose**: Get paginated videos for a camera with folder info
411
+ 3. **Edge Case: Empty Vehicles**
412
+ - Input: No vehicle data
413
+ - Verify: Total is empty object `{}`
355
414
 
356
- **Data Flow**:
415
+ 4. **Edge Case: Single Custom Class**
416
+ - Input: 1 custom class
417
+ - Verify: Total equals the single class
357
418
 
358
- ```
359
- API/Service Request VideoService.getVideosByCameraId(cameraId, page, limit)
360
- VideoDAO.getVideosByCameraIdWithFolder(cameraId, page, limit)
361
- → Returns IDataPaginator<IVideo>
362
- → Service returns directly (line 77)
363
- ```
419
+ 5. **Edge Case: Missing Directions**
420
+ - Input: Asymmetric direction/turn data
421
+ - Verify: Total includes all unique keys
364
422
 
365
- **Critical Detail**: Method name explicitly includes "WithFolder" - MUST JOIN folders
423
+ 6. **Key Ordering Test**
424
+ - Input: 5 custom classes
425
+ - Verify: Total is the 6th key (last)
426
+ - Verify: `Object.keys(result)` ends with "Total"
366
427
 
367
- ### Implementation Requirements
428
+ ### Integration Tests
368
429
 
369
- **SQL Query**:
430
+ **Test in api-rel controller**:
370
431
 
371
- ```sql
372
- -- Count query
373
- SELECT COUNT(*) as count
374
- FROM video v
375
- WHERE v.cameraId = ?
432
+ 1. Create video with ATR results
433
+ 2. Apply configuration with 3 custom classes
434
+ 3. Call `GET /videos/:uuid/results?configUuid=...`
435
+ 4. Verify response includes "Total" as last class
436
+ 5. Verify Total values equal sum of custom classes
376
437
 
377
- -- Data query with folder JOIN
378
- SELECT v.*, to_jsonb(f.*) as folder
379
- FROM video v
380
- INNER JOIN folders f ON v.folderId = f.id
381
- WHERE v.cameraId = ?
382
- ORDER BY v.created_at DESC
383
- LIMIT ? OFFSET ?
384
- ```
438
+ **Test in traffic-webapp**:
385
439
 
386
- **Key Details**:
440
+ 1. Display video results with custom classes
441
+ 2. Verify "Total" tab appears last
442
+ 3. Verify Total chart shows aggregated data
443
+ 4. Test with both ATR and TMC videos
387
444
 
388
- - IDENTICAL to CameraDAO.getVideosByCamera() implementation
389
- - Filter by cameraId
390
- - MUST include folder data via JOIN
391
- - Follow VideoDAO.getAll() JOIN pattern (lines 50-59)
392
- - Return IDataPaginator<IVideo>
445
+ ---
393
446
 
394
- **Performance Considerations**:
447
+ ## Performance Considerations
395
448
 
396
- - Same as Method 2 (getVideosByCamera)
397
- - cameraId indexed
398
- - Consider composite index on (cameraId, created_at) for sorting
449
+ ### Computational Complexity
399
450
 
400
- **Pattern Consistency**:
451
+ **Current**: O(n × m) where:
401
452
 
402
- - This method duplicates CameraDAO.getVideosByCamera() logic
403
- - Both should use IDENTICAL query structure
404
- - Consider refactoring to shared private method in future
453
+ - n = number of detection labels
454
+ - m = average nesting depth (2 for ATR, 3 for TMC)
405
455
 
406
- ---
456
+ **After Adding Total**: O(n × m + c × m) where:
407
457
 
408
- ## Implementation Order
458
+ - c = number of custom classes (2-7 per validation)
409
459
 
410
- ### Phase 1: Simple Methods (No Dependencies)
460
+ **Impact**: Minimal - additional O(c × m) is negligible
411
461
 
412
- 1. **CameraDAO.getAllWithSearch()** - Standalone search method
413
- 2. **VideoDAO.getVideoIdsByFolderId()** - Simple ID retrieval
462
+ - c 7 (max custom classes)
463
+ - m 3 (max nesting depth)
464
+ - Total iteration: ~21 operations per minute result
414
465
 
415
- ### Phase 2: Complex Query Methods (JOINs)
466
+ ### Memory Impact
416
467
 
417
- 3. **CameraDAO.getVideosByCamera()** - JOIN with folders
418
- 4. **VideoDAO.getVideosByCameraIdWithFolder()** - Same as #3, different DAO
468
+ **Additional Memory**: One new key per transformed result
419
469
 
420
- ### Phase 3: Transaction-Aware Methods
470
+ - ATR Total: ~100 bytes (lanes object)
471
+ - TMC Total: ~500 bytes (directions/turns object)
472
+ - Per minute result: negligible
473
+ - For 60 minutes: ~6-30 KB additional
421
474
 
422
- 5. **VideoDAO.bulkUpdateCamera()** - Transaction support required
475
+ **Conclusion**: No performance concerns
423
476
 
424
477
  ---
425
478
 
426
- ## Database Indexing Recommendations
427
-
428
- ### Existing Indexes (Already Present)
479
+ ## Backward Compatibility
429
480
 
430
- - cameras.uuid (migration 20250911000000_migration.ts:13)
431
- - cameras.(longitude, latitude) (migration 20250911000000_migration.ts:14)
432
- - video.cameraId (migration 20250911000000_migration.ts:20)
433
- - folders.cameraId (migration 20250911000000_migration.ts:26)
481
+ ### Changes Are Additive Only
434
482
 
435
- ### Recommended New Indexes
483
+ **No Breaking Changes**:
436
484
 
437
- **Optional - cameras.name (for search optimization)**
485
+ - Existing custom classes remain unchanged
486
+ - Existing API response structure preserved
487
+ - "Total" is a NEW key (optional to consume)
438
488
 
439
- ```sql
440
- CREATE INDEX idx_cameras_name ON cameras(name);
441
- ```
442
-
443
- - Benefits: Speeds up getAllWithSearch() ILIKE queries
444
- - Tradeoff: ILIKE with leading wildcard still can't use index fully
445
- - Decision: SKIP for now (camera table likely small)
489
+ ✅ **Frontend Compatibility**:
446
490
 
447
- **High Priority - video.folderId (for cascade operations)**
491
+ - Old frontend: Ignores "Total" key (no errors)
492
+ - New frontend: Displays "Total" as last tab
448
493
 
449
- ```sql
450
- CREATE INDEX idx_video_folder_id ON video(folderId);
451
- ```
494
+ ✅ **Database Compatibility**:
452
495
 
453
- - Benefits: Critical for getVideoIdsByFolderId() performance
454
- - Usage: Heavy usage in cascade operations
455
- - Decision: **CHECK IF EXISTS** - likely already present
496
+ - No schema changes required
497
+ - Raw results in database unchanged
498
+ - Transformation happens at runtime
456
499
 
457
- **Optional - video.(cameraId, created_at) composite**
500
+ ---
458
501
 
459
- ```sql
460
- CREATE INDEX idx_video_camera_created ON video(cameraId, created_at DESC);
461
- ```
502
+ ## Implementation Checklist
462
503
 
463
- - Benefits: Optimizes sorted queries in getVideosByCameraIdWithFolder
464
- - Decision: SKIP for now (single column index sufficient)
504
+ ### Pre-Implementation
465
505
 
466
- ---
506
+ - [x] Analyze current transformation logic
507
+ - [x] Identify exact insertion point (line 312)
508
+ - [x] Verify `_deepMergeNumericData()` handles both ATR/TMC
509
+ - [x] Confirm JavaScript key ordering guarantees
467
510
 
468
- ## Type Safety & Interfaces
511
+ ### Implementation Phase
469
512
 
470
- All methods already have proper TypeScript interfaces defined:
513
+ - [ ] Add `_createTotalClass()` helper method after line 350
514
+ - [ ] Add "Total" class insertion after line 312 in `applyConfigurationToNestedStructure()`
515
+ - [ ] Add JSDoc comments for new method
516
+ - [ ] Verify TypeScript compilation: `npm run build`
471
517
 
472
- **ICamera** - knex-rel/src/interfaces/camera/camera.interfaces.ts
518
+ ### Testing Phase
473
519
 
474
- - All fields present and correct
475
- - No modifications needed
520
+ - [ ] Write unit tests for `_createTotalClass()`
521
+ - [ ] Test ATR format (lane-based totals)
522
+ - [ ] Test TMC format (direction/turn totals)
523
+ - [ ] Test edge cases (empty, single class, missing keys)
524
+ - [ ] Test key ordering (Total is last)
525
+ - [ ] Run all tests: `npm test`
476
526
 
477
- **IVideo** - knex-rel/src/interfaces/video/video.interfaces.ts
527
+ ### Integration Phase
478
528
 
479
- - Includes optional folder?: IFolder field (line 33)
480
- - Includes optional cameraId?: number field (line 8)
481
- - Supports all required fields
482
- - No modifications needed
529
+ - [ ] Build knex-rel: `npm run build`
530
+ - [ ] Test in api-rel: `npm run start:dev`
531
+ - [ ] Call `/videos/:uuid/results` endpoint with ATR video
532
+ - [ ] Call `/videos/:uuid/results` endpoint with TMC video
533
+ - [ ] Verify "Total" appears in response as last class
534
+ - [ ] Verify Total values are correct sums
483
535
 
484
- **IDataPaginator<T>** - Used consistently across all methods
536
+ ### Frontend Integration (Separate Task)
485
537
 
486
- - success: boolean
487
- - data: T[]
488
- - page: number
489
- - limit: number
490
- - count: number
491
- - totalCount: number
492
- - totalPages: number
538
+ - [ ] Update traffic-webapp to display "Total" tab
539
+ - [ ] Position "Total" tab as last tab
540
+ - [ ] Render Total data in charts/tables
541
+ - [ ] Test with both ATR and TMC videos
493
542
 
494
543
  ---
495
544
 
496
- ## Error Handling Patterns
545
+ ## Code Changes Summary
546
+
547
+ ### File 1: `knex-rel/src/dao/report-configuration/report-configuration.dao.ts`
497
548
 
498
- ### Pattern 1: Search Methods (getAllWithSearch)
549
+ **Location 1: After Line 312 (Inside `applyConfigurationToNestedStructure()`)**
499
550
 
500
551
  ```typescript
501
- // No try/catch in DAO
502
- // Let Knex errors propagate to service
503
- const result = await query...
504
- return result;
505
- ```
552
+ // Lines 299-312: Existing iteration through detection labels
553
+ for (const [detectionLabel, nestedData] of Object.entries(vehiclesStructure)) {
554
+ const customClassName = detectionToCustomClass[detectionLabel];
555
+ if (!customClassName) {
556
+ continue;
557
+ }
558
+ result[customClassName] = this._deepMergeNumericData(
559
+ result[customClassName],
560
+ nestedData,
561
+ );
562
+ }
506
563
 
507
- ### Pattern 2: Transaction Methods (bulkUpdateCamera)
564
+ // NEW CODE: Add "Total" class that aggregates all custom classes
565
+ result["Total"] = this._createTotalClass(result);
508
566
 
509
- ```typescript
510
- // No try/catch in DAO
511
- // Let transaction rollback handle errors at service level
512
- const query = trx || this._knex;
513
- const result = await query...
514
- return result.length;
567
+ return result;
515
568
  ```
516
569
 
517
- ### Pattern 3: Simple Queries (getVideoIdsByFolderId)
570
+ **Location 2: After Line 350 (New Private Method)**
518
571
 
519
572
  ```typescript
520
- // No try/catch needed
521
- // Direct Knex query with error propagation
522
- const ids = await this._knex...
523
- return ids.map(row => row.id);
573
+ /**
574
+ * Create "Total" class by aggregating all custom vehicle classes
575
+ *
576
+ * Sums all custom class counts across their nested structures.
577
+ * Works for both ATR (lane-based) and TMC (direction/turn-based) formats.
578
+ *
579
+ * @param customClassesData - Transformed custom classes structure
580
+ * Example ATR: { "Light": { "0": 45, "1": 50 }, "Heavy": { "0": 10 } }
581
+ * Example TMC: { "Light": { "NORTH": { "straight": 10 } }, ... }
582
+ * @returns Aggregated totals matching the same nested structure
583
+ * Example ATR: { "0": 55, "1": 50 }
584
+ * Example TMC: { "NORTH": { "straight": 10 }, ... }
585
+ */
586
+ private _createTotalClass(customClassesData: Record<string, any>): any {
587
+ let total: any = {};
588
+
589
+ // Iterate through all custom classes and merge their data
590
+ for (const [className, nestedData] of Object.entries(customClassesData)) {
591
+ total = this._deepMergeNumericData(total, nestedData);
592
+ }
593
+
594
+ return total;
595
+ }
524
596
  ```
525
597
 
526
598
  ---
527
599
 
528
- ## Code Pattern Examples
600
+ ## Validation Plan
529
601
 
530
- ### Example 1: getAllWithSearch() Implementation
602
+ ### Manual Testing Checklist
531
603
 
532
- ```typescript
533
- async getAllWithSearch(page: number, limit: number, name?: string): Promise<IDataPaginator<ICamera>> {
534
- const offset = (page - 1) * limit;
604
+ **Test 1: ATR Video (2 Custom Classes)**
535
605
 
536
- let query = this._knex("cameras");
606
+ - [ ] Create configuration: "Light" (FHWA 1-3), "Heavy" (FHWA 4-13)
607
+ - [ ] Upload ATR video with mixed vehicle types
608
+ - [ ] Process video and get results
609
+ - [ ] Verify "Light" shows cars/motorcycles per lane
610
+ - [ ] Verify "Heavy" shows trucks/buses per lane
611
+ - [ ] Verify "Total" shows sum of Light + Heavy per lane
612
+ - [ ] Verify "Total" is last key in vehicles object
537
613
 
538
- // Apply search filter if name provided
539
- if (name && name.trim().length > 0) {
540
- query = query.where('name', 'ilike', `%${name.trim()}%`);
541
- }
614
+ **Test 2: TMC Video (3 Custom Classes)**
542
615
 
543
- const [countResult] = await query.clone().count("* as count");
544
- const totalCount = +countResult.count;
545
- const cameras = await query.clone().limit(limit).offset(offset).orderBy('name', 'asc');
546
-
547
- return {
548
- success: true,
549
- data: cameras,
550
- page,
551
- limit,
552
- count: cameras.length,
553
- totalCount,
554
- totalPages: Math.ceil(totalCount / limit),
555
- };
556
- }
557
- ```
616
+ - [ ] Create configuration: "Cars", "Trucks", "Buses"
617
+ - [ ] Upload TMC video with turning movements
618
+ - [ ] Process video and get results
619
+ - [ ] Verify each class shows direction/turn breakdown
620
+ - [ ] Verify "Total" shows sum across all classes
621
+ - [ ] Verify NORTH/straight in Total = sum of all classes' NORTH/straight
622
+ - [ ] Verify "Total" is last key in vehicles object
558
623
 
559
- ### Example 2: bulkUpdateCamera() Implementation
624
+ **Test 3: Edge Cases**
560
625
 
561
- ```typescript
562
- async bulkUpdateCamera(videoIds: number[], cameraId: number | null, trx?: Knex.Transaction): Promise<number> {
563
- if (!videoIds || videoIds.length === 0) {
564
- return 0;
565
- }
626
+ - [ ] Video with no vehicles detected (empty Total)
627
+ - [ ] Video with single custom class (Total equals that class)
628
+ - [ ] Video with asymmetric turns (Light has left, Heavy doesn't)
629
+ - [ ] Configuration with 7 custom classes (max allowed)
566
630
 
567
- const query = trx || this._knex;
631
+ **Test 4: Grouped Results (Multiple Minutes)**
568
632
 
569
- const result = await query('video')
570
- .whereIn('id', videoIds)
571
- .update({
572
- cameraId: cameraId,
573
- updated_at: query.fn.now()
574
- })
575
- .returning('id');
633
+ - [ ] Get grouped results with grouping=15
634
+ - [ ] Verify each timeGroup has "Total" class
635
+ - [ ] Verify "Total" sums correctly across aggregated minutes
636
+ - [ ] Verify ordering preserved in all timeGroups
576
637
 
577
- return result.length;
578
- }
579
- ```
638
+ ---
580
639
 
581
- ### Example 3: getVideosByCamera() Implementation
640
+ ## Risks & Mitigations
582
641
 
583
- ```typescript
584
- async getVideosByCamera(cameraId: number, page: number, limit: number): Promise<IDataPaginator<IVideo>> {
585
- const offset = (page - 1) * limit;
586
-
587
- const query = this._knex("video as v")
588
- .innerJoin("folders as f", "v.folderId", "f.id")
589
- .select("v.*", this._knex.raw("to_jsonb(f.*) as folder"))
590
- .where("v.cameraId", cameraId);
591
-
592
- const [countResult] = await query.clone().clearSelect().count("* as count");
593
- const totalCount = +countResult.count;
594
- const videos = await query.clone().limit(limit).offset(offset).orderBy('v.created_at', 'desc');
595
-
596
- return {
597
- success: true,
598
- data: videos,
599
- page,
600
- limit,
601
- count: videos.length,
602
- totalCount,
603
- totalPages: Math.ceil(totalCount / limit),
604
- };
605
- }
606
- ```
642
+ ### Risk 1: Key Ordering Not Guaranteed
607
643
 
608
- ### Example 4: getVideoIdsByFolderId() Implementation
644
+ **Risk**: JavaScript object keys might not preserve insertion order
609
645
 
610
- ```typescript
611
- async getVideoIdsByFolderId(folderId: number): Promise<number[]> {
612
- const rows = await this._knex('video')
613
- .where({ folderId })
614
- .select('id')
615
- .orderBy('id', 'asc');
646
+ **Likelihood**: Very Low (ES2015+ guarantees string key order)
616
647
 
617
- return rows.map(row => row.id);
618
- }
619
- ```
648
+ **Mitigation**:
620
649
 
621
- ### Example 5: getVideosByCameraIdWithFolder() Implementation
650
+ - Add unit test to verify `Object.keys(result)` ends with "Total"
651
+ - If ordering fails, use explicit ordering array in frontend
622
652
 
623
- ```typescript
624
- async getVideosByCameraIdWithFolder(cameraId: number, page: number, limit: number): Promise<IDataPaginator<IVideo>> {
625
- const offset = (page - 1) * limit;
626
-
627
- const query = this._knex("video as v")
628
- .innerJoin("folders as f", "v.folderId", "f.id")
629
- .select("v.*", this._knex.raw("to_jsonb(f.*) as folder"))
630
- .where("v.cameraId", cameraId);
631
-
632
- const [countResult] = await query.clone().clearSelect().count("* as count");
633
- const totalCount = +countResult.count;
634
- const videos = await query.clone().limit(limit).offset(offset).orderBy('v.created_at', 'desc');
635
-
636
- return {
637
- success: true,
638
- data: videos,
639
- page,
640
- limit,
641
- count: videos.length,
642
- totalCount,
643
- totalPages: Math.ceil(totalCount / limit),
644
- };
645
- }
646
- ```
653
+ ### Risk 2: Performance Degradation
647
654
 
648
- ---
655
+ **Risk**: Additional aggregation adds processing time
656
+
657
+ **Likelihood**: Very Low (O(c × m) is negligible)
658
+
659
+ **Mitigation**:
660
+
661
+ - Benchmark transformation time before/after
662
+ - If impact > 10ms, consider caching Total class
663
+
664
+ ### Risk 3: Frontend Breaks on Unknown Class
649
665
 
650
- ## Migration Requirements
666
+ **Risk**: Old frontend version throws error on "Total" key
651
667
 
652
- **No new migrations needed** - All schema changes already exist:
668
+ **Likelihood**: Very Low (key is additive, not breaking)
653
669
 
654
- - cameras table: migration 20250911000000_migration.ts
655
- - video.cameraId column: migration 20250911000000_migration.ts:19
656
- - folders.cameraId column: migration 20250911000000_migration.ts:25
657
- - All indexes already created
670
+ **Mitigation**:
658
671
 
659
- **Optional migration for performance**:
672
+ - "Total" is just another custom class name
673
+ - Frontend already iterates dynamic class names
674
+ - No special handling needed
660
675
 
661
- - Check if video.folderId index exists (likely already present)
676
+ ### Risk 4: Incorrect Aggregation Logic
677
+
678
+ **Risk**: `_deepMergeNumericData()` doesn't handle all cases
679
+
680
+ **Likelihood**: Low (method already tested for custom class merging)
681
+
682
+ **Mitigation**:
683
+
684
+ - Comprehensive unit tests for edge cases
685
+ - Manual validation with real ATR/TMC videos
686
+ - Use existing, battle-tested `_deepMergeNumericData()`
662
687
 
663
688
  ---
664
689
 
665
- ## Testing Strategy
690
+ ## Alternative Approaches Considered
666
691
 
667
- ### Unit Tests Required
692
+ ### Alternative 1: Add "Total" in Frontend
668
693
 
669
- **CameraDAO.getAllWithSearch()**
694
+ **Approach**: Calculate "Total" in traffic-webapp instead of backend
670
695
 
671
- - Test: Search with matching name (partial match)
672
- - Test: Search with no matches (empty result)
673
- - Test: Search with null/undefined name (return all)
674
- - Test: Pagination (page 1, page 2, etc.)
675
- - Test: Case insensitivity (ILIKE behavior)
696
+ **Pros**:
676
697
 
677
- **CameraDAO.getVideosByCamera()**
698
+ - No backend changes required
699
+ - Frontend controls display logic
678
700
 
679
- - Test: Camera with videos (verify folder JOIN)
680
- - Test: Camera with no videos (empty result)
681
- - Test: Pagination with multiple pages
682
- - Test: Verify folder data structure in results
701
+ **Cons**:
683
702
 
684
- **VideoDAO.bulkUpdateCamera()**
703
+ - Duplicates logic across frontend
704
+ - Inconsistent if API consumed elsewhere
705
+ - Cannot use "Total" in backend reports/exports
685
706
 
686
- - Test: Update with cameraId (assignment)
687
- - Test: Update with null cameraId (unassignment)
688
- - Test: Update with transaction (commit)
689
- - Test: Update with transaction (rollback)
690
- - Test: Empty videoIds array (return 0)
691
- - Test: Invalid videoIds (non-existent IDs)
707
+ **Decision**: ❌ Rejected - Backend is single source of truth
692
708
 
693
- **VideoDAO.getVideoIdsByFolderId()**
709
+ ### Alternative 2: Add "Total" as Separate Field
694
710
 
695
- - Test: Folder with videos (return IDs)
696
- - Test: Folder with no videos (empty array)
697
- - Test: Non-existent folderId (empty array)
698
- - Test: Large folder (100+ videos)
711
+ **Approach**: Add `results.totalClass` instead of `results.vehicles["Total"]`
699
712
 
700
- **VideoDAO.getVideosByCameraIdWithFolder()**
713
+ **Pros**:
701
714
 
702
- - Test: Camera with videos (verify folder JOIN)
703
- - Test: Camera with no videos (empty result)
704
- - Test: Pagination
705
- - Test: Verify folder data in results
706
- - Test: Null cameraId handling
715
+ - Clearer separation of concerns
716
+ - No risk of key ordering issues
707
717
 
708
- ---
718
+ **Cons**:
709
719
 
710
- ## Implementation Checklist
720
+ - Inconsistent with custom class structure
721
+ - Frontend needs special handling
722
+ - Breaks uniform class iteration logic
711
723
 
712
- ### Pre-Implementation
724
+ **Decision**: ❌ Rejected - Keep "Total" as vehicle class for uniformity
713
725
 
714
- - [ ] Check if video.folderId index exists in database
715
- - [ ] Review existing DAO patterns in camera.dao.ts and video.dao.ts
716
- - [ ] Verify IDataPaginator structure in d.types.ts
726
+ ### Alternative 3: Make "Total" Configurable
717
727
 
718
- ### Implementation Phase
728
+ **Approach**: Add `includeTotalClass: boolean` to report configuration
719
729
 
720
- - [ ] Implement CameraDAO.getAllWithSearch()
721
- - [ ] Implement VideoDAO.getVideoIdsByFolderId()
722
- - [ ] Implement CameraDAO.getVideosByCamera()
723
- - [ ] Implement VideoDAO.getVideosByCameraIdWithFolder()
724
- - [ ] Implement VideoDAO.bulkUpdateCamera()
730
+ **Pros**:
725
731
 
726
- ### Testing Phase
732
+ - User control over Total display
733
+ - Backward compatible (default false)
727
734
 
728
- - [ ] Write unit tests for all 5 methods
729
- - [ ] Test transaction handling in bulkUpdateCamera
730
- - [ ] Test pagination edge cases (page 1, last page, beyond last page)
731
- - [ ] Test search with special characters (SQL injection prevention)
735
+ **Cons**:
732
736
 
733
- ### Integration Phase
737
+ - Adds complexity to configuration
738
+ - Not needed (Total is always useful)
739
+ - Extra validation logic
734
740
 
735
- - [ ] Build knex-rel package: `npm run build`
736
- - [ ] Verify no TypeScript compilation errors
737
- - [ ] Test in api-rel: `npm run start:dev`
738
- - [ ] Verify all service methods work end-to-end
739
- - [ ] Test camera bulk assignment workflow
740
- - [ ] Test folder camera cascade workflow
741
+ **Decision**: Rejected - Always include "Total" (KISS principle)
741
742
 
742
743
  ---
743
744
 
744
- ## Risk Assessment
745
+ ## Success Criteria
746
+
747
+ ### Implementation Success
748
+
749
+ ✅ TypeScript compiles without errors
750
+ ✅ `npm run build` succeeds in knex-rel
751
+ ✅ All existing unit tests pass
752
+ ✅ New unit tests pass (Total class logic)
745
753
 
746
- ### Low Risk
754
+ ### Functional Success
755
+
756
+ ✅ ATR videos show "Total" with correct lane sums
757
+ ✅ TMC videos show "Total" with correct direction/turn sums
758
+ ✅ "Total" appears as LAST class in all responses
759
+ ✅ Edge cases handled (empty, single class, missing keys)
760
+
761
+ ### Integration Success
762
+
763
+ ✅ API endpoint returns "Total" in response
764
+ ✅ Grouped results include "Total" in each timeGroup
765
+ ✅ No breaking changes to existing API consumers
766
+ ✅ Frontend displays "Total" tab correctly (separate task)
767
+
768
+ ---
747
769
 
748
- - getAllWithSearch() - Simple extension of getAll()
749
- - getVideoIdsByFolderId() - Straightforward SELECT query
770
+ ## Timeline Estimate
750
771
 
751
- ### Medium Risk
772
+ ### Backend Implementation (knex-rel)
752
773
 
753
- - getVideosByCamera() - Requires correct JOIN syntax
754
- - getVideosByCameraIdWithFolder() - Same as above
774
+ - Code changes: 30 minutes
775
+ - Unit tests: 1 hour
776
+ - Build & test: 15 minutes
777
+ - **Total: ~2 hours**
755
778
 
756
- ### High Risk
779
+ ### Integration Testing (api-rel)
757
780
 
758
- - bulkUpdateCamera() - Transaction handling complexity
759
- - **Mitigation**: Carefully test both transaction and non-transaction paths
760
- - **Mitigation**: Validate videoIds array before query
761
- - **Mitigation**: Let service layer handle rollback logic
781
+ - Manual testing with ATR video: 30 minutes
782
+ - Manual testing with TMC video: 30 minutes
783
+ - Edge case validation: 30 minutes
784
+ - **Total: ~1.5 hours**
762
785
 
763
- ### Performance Risks
786
+ ### Frontend Integration (traffic-webapp - Separate)
764
787
 
765
- - getVideoIdsByFolderId() could return large arrays (1000+ IDs)
766
- - **Mitigation**: Acceptable for cascade operations
767
- - **Mitigation**: Ensure folderId index exists for speed
768
- - getAllWithSearch() ILIKE with leading wildcard is slow
769
- - **Mitigation**: Acceptable for small camera table
770
- - **Mitigation**: Consider full-text search if needed later
788
+ - Display "Total" tab: 1 hour
789
+ - Chart/table rendering: 1 hour
790
+ - Testing both video types: 1 hour
791
+ - **Total: ~3 hours**
792
+
793
+ **Overall Estimate: ~6.5 hours** (Backend: 3.5 hours, Frontend: 3 hours)
771
794
 
772
795
  ---
773
796
 
774
797
  ## Summary
775
798
 
776
- All 5 missing methods have been thoroughly analyzed:
799
+ ### What Changes
800
+
801
+ ✅ **ADD**: `_createTotalClass()` private method in ReportConfigurationDAO
802
+ ✅ **ADD**: `result["Total"] = ...` line after custom class transformation
803
+ ✅ **ADD**: Unit tests for Total class aggregation
804
+
805
+ ### What Stays the Same
806
+
807
+ ✅ Database schema (no migrations)
808
+ ✅ Existing custom classes (no modifications)
809
+ ✅ API response structure (additive only)
810
+ ✅ `_deepMergeNumericData()` helper (reused, not changed)
811
+
812
+ ### Key Benefits
777
813
 
778
- - **Call patterns documented** from 8 different service method locations
779
- - **Data flow mapped** from API → Service → DAO → Database
780
- - **SQL queries designed** with proper JOINs, indexes, and performance
781
- - **Transaction handling** specified for bulkUpdateCamera()
782
- - **Type safety verified** - all interfaces already correct
783
- - **No migrations needed** - schema already complete
784
- - **Implementation order prioritized** - simple to complex
785
- - **Testing strategy defined** - 25+ test cases identified
786
- - **Risk assessment completed** - mitigations specified
814
+ Unified "Total" view across all custom classes
815
+ Consistent with existing transformation architecture
816
+ No breaking changes (backward compatible)
817
+ Minimal performance impact (< 1ms per minute result)
818
+ Reuses existing, tested aggregation logic
787
819
 
788
- **Ready for implementation** - All patterns follow existing DAO conventions in codebase.
820
+ **Ready for Implementation** - Plan is complete with exact code locations, logic, edge cases, and validation strategy.
789
821
 
790
822
  ---
791
823
 
792
824
  ## Files to Modify
793
825
 
794
- ### knex-rel/src/dao/camera/camera.dao.ts
826
+ ### knex-rel/src/dao/report-configuration/report-configuration.dao.ts
795
827
 
796
- - Add method: `getAllWithSearch(page, limit, name?)`
797
- - Add method: `getVideosByCamera(cameraId, page, limit)`
828
+ - **Line 312**: Add `result["Total"] = this._createTotalClass(result);`
829
+ - **After Line 350**: Add `_createTotalClass()` private method
798
830
 
799
- ### knex-rel/src/dao/video/video.dao.ts
831
+ ### knex-rel/src/dao/report-configuration/report-configuration.dao.test.ts (New File)
800
832
 
801
- - Add method: `bulkUpdateCamera(videoIds, cameraId, trx?)`
802
- - Add method: `getVideoIdsByFolderId(folderId)`
803
- - Add method: `getVideosByCameraIdWithFolder(cameraId, page, limit)`
833
+ - Create unit tests for Total class aggregation
834
+ - Test ATR format, TMC format, edge cases, key ordering
804
835
 
805
836
  ---
806
837
 
@@ -808,24 +839,33 @@ All 5 missing methods have been thoroughly analyzed:
808
839
 
809
840
  ### Database Patterns ✅
810
841
 
811
- - All tables use `id` (auto-increment primary key)
812
- - All tables use `uuid` (unique, not null) for external references
813
- - All tables use `created_at`/`updated_at` timestamps
814
- - Foreign keys use ON DELETE SET NULL for cameras
815
- - Indexes on UUID and foreign keys
842
+ - No database changes required (transformation only)
843
+ - Follows DAO pattern (logic in ReportConfigurationDAO)
844
+ - No new migrations needed
816
845
 
817
- ### DAO Patterns ✅
846
+ ### Code Patterns ✅
818
847
 
819
- - Singleton KnexManager.getConnection()
820
- - Standard CRUD methods (create, getById, getByUuid, getAll, update, delete)
821
- - Pagination with IDataPaginator<T> return type
822
- - JOINs use to_jsonb() for nested objects
823
- - Transaction support via optional trx parameter
848
+ - Reuses existing `_deepMergeNumericData()` helper
849
+ - Follows private method naming convention (`_methodName`)
850
+ - Maintains nested structure consistency
851
+ - Preserves key ordering via insertion order
852
+
853
+ ### Performance Patterns ✅
854
+
855
+ - O(c × m) additional complexity (negligible)
856
+ - No N+1 queries (transformation in memory)
857
+ - No database roundtrips
858
+ - Minimal memory overhead (~500 bytes per result)
859
+
860
+ ---
824
861
 
825
- ### Query Patterns ✅
862
+ ## Next Steps
826
863
 
827
- - Use `this._knex` for queries
828
- - Use `query.clone()` for count vs data queries
829
- - Use `whereIn()` for array filters
830
- - Use `query.fn.now()` for timestamps
831
- - Use `returning('id')` for update counts
864
+ 1. **Implement** `_createTotalClass()` method in ReportConfigurationDAO
865
+ 2. **Add** Total class insertion after line 312
866
+ 3. **Write** unit tests for all edge cases
867
+ 4. **Build** knex-rel package and verify compilation
868
+ 5. **Test** with real ATR and TMC videos in api-rel
869
+ 6. **Validate** key ordering and correct aggregation
870
+ 7. **Document** changes (if needed)
871
+ 8. **Coordinate** with frontend team for Total tab display