@trafficgroup/knex-rel 0.1.3 → 0.1.5

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.
Files changed (30) hide show
  1. package/dist/dao/VideoMinuteResultDAO.d.ts +0 -4
  2. package/dist/dao/VideoMinuteResultDAO.js +7 -48
  3. package/dist/dao/VideoMinuteResultDAO.js.map +1 -1
  4. package/dist/dao/camera/camera.dao.d.ts +8 -1
  5. package/dist/dao/camera/camera.dao.js +27 -8
  6. package/dist/dao/camera/camera.dao.js.map +1 -1
  7. package/dist/dao/report-configuration/report-configuration.dao.d.ts +94 -0
  8. package/dist/dao/report-configuration/report-configuration.dao.js +352 -0
  9. package/dist/dao/report-configuration/report-configuration.dao.js.map +1 -0
  10. package/dist/dao/video/video.dao.d.ts +10 -0
  11. package/dist/dao/video/video.dao.js +40 -16
  12. package/dist/dao/video/video.dao.js.map +1 -1
  13. package/dist/index.d.ts +2 -0
  14. package/dist/index.js +3 -1
  15. package/dist/index.js.map +1 -1
  16. package/dist/interfaces/report-configuration/report-configuration.interfaces.d.ts +26 -0
  17. package/dist/interfaces/report-configuration/report-configuration.interfaces.js +3 -0
  18. package/dist/interfaces/report-configuration/report-configuration.interfaces.js.map +1 -0
  19. package/migrations/20250930200521_migration.ts +52 -0
  20. package/package.json +1 -1
  21. package/plan.md +755 -212
  22. package/src/dao/VideoMinuteResultDAO.ts +7 -64
  23. package/src/dao/camera/camera.dao.ts +30 -10
  24. package/src/dao/report-configuration/report-configuration.dao.ts +402 -0
  25. package/src/dao/video/video.dao.ts +46 -18
  26. package/src/index.ts +8 -0
  27. package/src/interfaces/report-configuration/report-configuration.interfaces.ts +30 -0
  28. package/cameras_analysis.md +0 -199
  29. package/folder_cameraid_analysis.md +0 -167
  30. package/migrations/20250924000000_camera_name_search_index.ts +0 -22
package/plan.md CHANGED
@@ -1,288 +1,831 @@
1
- # Database Optimization Plan for Camera Bulk Operations
1
+ # Missing DAO Methods Implementation Plan
2
2
 
3
- ## Current Schema Analysis
3
+ ## Executive Summary
4
4
 
5
- ### Existing Tables & Indexes
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.
6
6
 
7
- **cameras table:**
7
+ ---
8
8
 
9
- - Primary key: `id` (auto-increment)
10
- - Unique index: `uuid`
11
- - Composite index: `[longitude, latitude]` for geospatial queries
9
+ ## Schema Analysis
12
10
 
13
- **video table:**
11
+ ### Tables Involved
14
12
 
15
- - Primary key: `id`
16
- - Foreign key: `cameraId` → `cameras.id` (nullable, with index)
17
- - Foreign key: `folderId` → `folders.id` (with implicit index)
18
- - Unique index: `uuid`
19
- - Composite indexes:
20
- - `[annotationSourceId]`
21
- - `[folderId, videoType, status]` (template lookup)
13
+ **cameras**
22
14
 
23
- **folders table:**
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
+ ```
26
+
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
+ ```
40
+
41
+ **folders**
42
+
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)
49
+ ```
50
+
51
+ ---
52
+
53
+ ## Method 1: CameraDAO.getAllWithSearch()
54
+
55
+ ### Call Analysis
56
+
57
+ **Location**: `api-rel/src/service/camera/camera.service.ts:274`
24
58
 
25
- - Primary key: `id`
26
- - Foreign key: `cameraId` → `cameras.id` (nullable, with index)
27
- - Foreign key: `studyId` → `study.id` (with implicit index)
28
- - Unique index: `uuid`
59
+ **Method Signature**:
29
60
 
30
- ### Performance Analysis for Required Operations
61
+ ```typescript
62
+ async getAllWithSearch(page: number, limit: number, name?: string): Promise<IDataPaginator<ICamera>>
63
+ ```
31
64
 
32
- #### 1. ✅ Bulk update videos when folder cameraId changes
65
+ **Called From**: CameraService.getAllWithSearch()
33
66
 
34
- **Query:** `UPDATE video SET cameraId = ? WHERE folderId = ?`
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
35
70
 
36
- - **Current Performance:** GOOD
37
- - **Existing Index:** `[folderId]` (implicit from FK constraint)
38
- - **Status:** No optimization needed
71
+ **Data Flow**:
39
72
 
40
- #### 2. ❌ Search cameras by name with pagination
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
+ ```
41
80
 
42
- **Query:** `SELECT * FROM cameras WHERE name ILIKE '%search%' ORDER BY name LIMIT ? OFFSET ?`
81
+ ### Implementation Requirements
43
82
 
44
- - **Current Performance:** POOR - Full table scan
45
- - **Missing Index:** Text search index on `name` column
46
- - **Impact:** Critical performance issue for camera search
83
+ **SQL Query**:
47
84
 
48
- #### 3. ✅ Find videos by specific camera
85
+ ```sql
86
+ -- Count query
87
+ SELECT COUNT(*) as count FROM cameras WHERE name ILIKE '%search%'
88
+
89
+ -- Data query
90
+ SELECT * FROM cameras
91
+ WHERE name ILIKE '%search%'
92
+ LIMIT ? OFFSET ?
93
+ ```
49
94
 
50
- **Query:** `SELECT * FROM video WHERE cameraId = ?`
95
+ **Key Details**:
51
96
 
52
- - **Current Performance:** GOOD
53
- - **Existing Index:** `[cameraId]` from migration `20250911000000_migration.ts`
54
- - **Status:** No optimization needed
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
55
101
 
56
- #### 4. ⚠️ Bulk assign camera to multiple videos
102
+ **Performance Considerations**:
57
103
 
58
- **Query:** `UPDATE video SET cameraId = ? WHERE id IN (...)`
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)
59
107
 
60
- - **Current Performance:** ACCEPTABLE but can be optimized
61
- - **Existing Index:** Primary key on `id`
62
- - **Optimization Opportunity:** Batch processing with transaction optimization
108
+ ---
63
109
 
64
- ## Required Database Optimizations
110
+ ## Method 2: CameraDAO.getVideosByCamera()
65
111
 
66
- ### New Migrations Required
112
+ ### Call Analysis
67
113
 
68
- #### Migration: Add camera name search index
114
+ **Location**: `api-rel/src/service/camera/camera.service.ts:308`
69
115
 
70
- **File:** `20250924000000_camera_name_search_index.ts`
71
- **Purpose:** Optimize camera search by name functionality
116
+ **Method Signature**:
72
117
 
73
118
  ```typescript
74
- export async function up(knex: Knex): Promise<void> {
75
- await knex.schema.alterTable("cameras", (table) => {
76
- // Add index for case-insensitive name searches
77
- table.index(knex.raw("LOWER(name)"), "idx_cameras_name_lower");
78
- });
79
-
80
- // Add GIN index for full-text search if needed for fuzzy matching
81
- await knex.raw(`
82
- CREATE INDEX idx_cameras_name_gin
83
- ON cameras
84
- USING GIN (to_tsvector('english', name))
85
- `);
86
- }
119
+ async getVideosByCamera(cameraId: number, page: number, limit: number): Promise<IDataPaginator<IVideo>>
120
+ ```
87
121
 
88
- export async function down(knex: Knex): Promise<void> {
89
- await knex.raw("DROP INDEX IF EXISTS idx_cameras_name_gin");
90
- await knex.schema.alterTable("cameras", (table) => {
91
- table.dropIndex(knex.raw("LOWER(name)"), "idx_cameras_name_lower");
92
- });
93
- }
122
+ **Called From**: CameraService.getVideosByCamera()
123
+
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**:
129
+
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
94
137
  ```
95
138
 
96
- ### DAO Method Optimizations
139
+ **Critical Detail**: Service expects folder data in response (line 312: mapToVideoWithFolderDTO)
97
140
 
98
- #### CameraDAO Enhancements Required
141
+ ### Implementation Requirements
99
142
 
100
- **New Methods Needed:**
143
+ **SQL Query** (must JOIN with folders):
144
+
145
+ ```sql
146
+ -- Count query
147
+ SELECT COUNT(*) as count
148
+ FROM video v
149
+ WHERE v.cameraId = ?
150
+
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
+ ```
158
+
159
+ **Key Details**:
160
+
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
165
+
166
+ **Performance Considerations**:
167
+
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
171
+
172
+ ---
173
+
174
+ ## Method 3: VideoDAO.bulkUpdateCamera()
175
+
176
+ ### Call Analysis
177
+
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`
181
+
182
+ **Method Signature**:
101
183
 
102
184
  ```typescript
103
- // Optimized paginated search by name
104
- async searchByName(
105
- searchTerm: string,
106
- page: number,
107
- limit: number
108
- ): Promise<IDataPaginator<ICamera>> {
109
- const offset = (page - 1) * limit;
110
- const searchPattern = `%${searchTerm.toLowerCase()}%`;
111
-
112
- const query = this._knex("cameras")
113
- .whereRaw("LOWER(name) LIKE ?", [searchPattern])
114
- .orderBy("name");
115
-
116
- const [countResult] = await query.clone().clearSelect().count("* as count");
117
- const totalCount = +countResult.count;
118
- const cameras = await query.clone().limit(limit).offset(offset);
119
-
120
- return {
121
- success: true,
122
- data: cameras,
123
- page,
124
- limit,
125
- count: cameras.length,
126
- totalCount,
127
- totalPages: Math.ceil(totalCount / limit),
128
- };
129
- }
185
+ async bulkUpdateCamera(videoIds: number[], cameraId: number | null, trx?: Knex.Transaction): Promise<number>
186
+ ```
187
+
188
+ **Call Patterns**:
189
+
190
+ **Pattern 1 - CameraService.bulkAssignToVideos() (line 364)**:
191
+
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)
196
+
197
+ **Pattern 2 - VideoService.bulkUpdateCamera() (line 34)**:
198
+
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)
204
+
205
+ **Pattern 3 - FolderService.cascadeUpdateVideosCamera() (line 96)**:
206
+
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)
211
+
212
+ ### Implementation Requirements
213
+
214
+ **SQL Query**:
215
+
216
+ ```sql
217
+ UPDATE video
218
+ SET cameraId = ?, updated_at = NOW()
219
+ WHERE id = ANY(?)
220
+ RETURNING id
221
+ ```
222
+
223
+ **Key Details**:
224
+
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
231
+
232
+ **Transaction Handling**:
233
+
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;
241
+ ```
242
+
243
+ **Performance Considerations**:
244
+
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)
249
+
250
+ **Error Handling**:
251
+
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
255
+
256
+ ---
257
+
258
+ ## Method 4: VideoDAO.getVideoIdsByFolderId()
259
+
260
+ ### Call Analysis
261
+
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`
266
+
267
+ **Method Signature**:
268
+
269
+ ```typescript
270
+ async getVideoIdsByFolderId(folderId: number): Promise<number[]>
271
+ ```
272
+
273
+ **Call Patterns**:
274
+
275
+ **Pattern 1 - FolderService.updateWithCameraCascade() (line 49)**:
276
+
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
281
+
282
+ **Pattern 2 - FolderService.cascadeUpdateVideosCamera() (line 89)**:
283
+
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)
288
+
289
+ **Pattern 3 - FolderService.getVideoCascadeCount() (line 163)**:
290
+
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)
295
+
296
+ **Pattern 4 - VideoService.getVideoIdsByFolderId() (line 119)**:
297
+
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
302
+
303
+ ### Implementation Requirements
304
+
305
+ **SQL Query**:
306
+
307
+ ```sql
308
+ SELECT id FROM video WHERE folderId = ?
309
+ ```
310
+
311
+ **Key Details**:
312
+
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)
318
+
319
+ **Performance Considerations**:
320
+
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
325
+
326
+ **Return Value**:
327
+
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);
334
+ ```
335
+
336
+ ---
337
+
338
+ ## Method 5: VideoDAO.getVideosByCameraIdWithFolder()
339
+
340
+ ### Call Analysis
341
+
342
+ **Location**: `api-rel/src/service/video/video.service.ts:77`
343
+
344
+ **Method Signature**:
345
+
346
+ ```typescript
347
+ async getVideosByCameraIdWithFolder(cameraId: number, page: number, limit: number): Promise<IDataPaginator<IVideo>>
348
+ ```
349
+
350
+ **Called From**: VideoService.getVideosByCameraId()
351
+
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
355
+
356
+ **Data Flow**:
357
+
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
+ ```
364
+
365
+ **Critical Detail**: Method name explicitly includes "WithFolder" - MUST JOIN folders
366
+
367
+ ### Implementation Requirements
130
368
 
131
- // Get all videos associated with a camera (with pagination)
132
- async getVideosByCamera(
133
- cameraId: number,
134
- page: number,
135
- limit: number
136
- ): Promise<IDataPaginator<IVideo>> {
137
- const offset = (page - 1) * limit;
138
-
139
- const query = this._knex("video as v")
140
- .innerJoin("folders as f", "v.folderId", "f.id")
141
- .select("v.*", this._knex.raw("to_jsonb(f.*) as folder"))
142
- .where("v.cameraId", cameraId)
143
- .orderBy("v.created_at", "desc");
144
-
145
- const [countResult] = await query.clone().clearSelect().count("* as count");
146
- const totalCount = +countResult.count;
147
- const videos = await query.clone().limit(limit).offset(offset);
148
-
149
- return {
150
- success: true,
151
- data: videos,
152
- page,
153
- limit,
154
- count: videos.length,
155
- totalCount,
156
- totalPages: Math.ceil(totalCount / limit),
157
- };
369
+ **SQL Query**:
370
+
371
+ ```sql
372
+ -- Count query
373
+ SELECT COUNT(*) as count
374
+ FROM video v
375
+ WHERE v.cameraId = ?
376
+
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
+ ```
385
+
386
+ **Key Details**:
387
+
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>
393
+
394
+ **Performance Considerations**:
395
+
396
+ - Same as Method 2 (getVideosByCamera)
397
+ - cameraId indexed
398
+ - Consider composite index on (cameraId, created_at) for sorting
399
+
400
+ **Pattern Consistency**:
401
+
402
+ - This method duplicates CameraDAO.getVideosByCamera() logic
403
+ - Both should use IDENTICAL query structure
404
+ - Consider refactoring to shared private method in future
405
+
406
+ ---
407
+
408
+ ## Implementation Order
409
+
410
+ ### Phase 1: Simple Methods (No Dependencies)
411
+
412
+ 1. **CameraDAO.getAllWithSearch()** - Standalone search method
413
+ 2. **VideoDAO.getVideoIdsByFolderId()** - Simple ID retrieval
414
+
415
+ ### Phase 2: Complex Query Methods (JOINs)
416
+
417
+ 3. **CameraDAO.getVideosByCamera()** - JOIN with folders
418
+ 4. **VideoDAO.getVideosByCameraIdWithFolder()** - Same as #3, different DAO
419
+
420
+ ### Phase 3: Transaction-Aware Methods
421
+
422
+ 5. **VideoDAO.bulkUpdateCamera()** - Transaction support required
423
+
424
+ ---
425
+
426
+ ## Database Indexing Recommendations
427
+
428
+ ### Existing Indexes (Already Present)
429
+
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)
434
+
435
+ ### Recommended New Indexes
436
+
437
+ **Optional - cameras.name (for search optimization)**
438
+
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)
446
+
447
+ **High Priority - video.folderId (for cascade operations)**
448
+
449
+ ```sql
450
+ CREATE INDEX idx_video_folder_id ON video(folderId);
451
+ ```
452
+
453
+ - Benefits: Critical for getVideoIdsByFolderId() performance
454
+ - Usage: Heavy usage in cascade operations
455
+ - Decision: **CHECK IF EXISTS** - likely already present
456
+
457
+ **Optional - video.(cameraId, created_at) composite**
458
+
459
+ ```sql
460
+ CREATE INDEX idx_video_camera_created ON video(cameraId, created_at DESC);
461
+ ```
462
+
463
+ - Benefits: Optimizes sorted queries in getVideosByCameraIdWithFolder
464
+ - Decision: SKIP for now (single column index sufficient)
465
+
466
+ ---
467
+
468
+ ## Type Safety & Interfaces
469
+
470
+ All methods already have proper TypeScript interfaces defined:
471
+
472
+ **ICamera** - knex-rel/src/interfaces/camera/camera.interfaces.ts
473
+
474
+ - All fields present and correct
475
+ - No modifications needed
476
+
477
+ **IVideo** - knex-rel/src/interfaces/video/video.interfaces.ts
478
+
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
483
+
484
+ **IDataPaginator<T>** - Used consistently across all methods
485
+
486
+ - success: boolean
487
+ - data: T[]
488
+ - page: number
489
+ - limit: number
490
+ - count: number
491
+ - totalCount: number
492
+ - totalPages: number
493
+
494
+ ---
495
+
496
+ ## Error Handling Patterns
497
+
498
+ ### Pattern 1: Search Methods (getAllWithSearch)
499
+
500
+ ```typescript
501
+ // No try/catch in DAO
502
+ // Let Knex errors propagate to service
503
+ const result = await query...
504
+ return result;
505
+ ```
506
+
507
+ ### Pattern 2: Transaction Methods (bulkUpdateCamera)
508
+
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;
515
+ ```
516
+
517
+ ### Pattern 3: Simple Queries (getVideoIdsByFolderId)
518
+
519
+ ```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);
524
+ ```
525
+
526
+ ---
527
+
528
+ ## Code Pattern Examples
529
+
530
+ ### Example 1: getAllWithSearch() Implementation
531
+
532
+ ```typescript
533
+ async getAllWithSearch(page: number, limit: number, name?: string): Promise<IDataPaginator<ICamera>> {
534
+ const offset = (page - 1) * limit;
535
+
536
+ let query = this._knex("cameras");
537
+
538
+ // Apply search filter if name provided
539
+ if (name && name.trim().length > 0) {
540
+ query = query.where('name', 'ilike', `%${name.trim()}%`);
541
+ }
542
+
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
+ };
158
556
  }
159
557
  ```
160
558
 
161
- #### VideoDAO Enhancements Required
559
+ ### Example 2: bulkUpdateCamera() Implementation
560
+
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
+ }
566
+
567
+ const query = trx || this._knex;
568
+
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');
576
+
577
+ return result.length;
578
+ }
579
+ ```
162
580
 
163
- **New Methods Needed:**
581
+ ### Example 3: getVideosByCamera() Implementation
164
582
 
165
583
  ```typescript
166
- // Bulk update videos by folder (folder camera cascade)
167
- async bulkUpdateCameraByFolder(
168
- folderId: number,
169
- cameraId: number | null
170
- ): Promise<number> {
171
- const result = await this._knex("video")
172
- .where("folderId", folderId)
173
- .update({
174
- cameraId: cameraId,
175
- updated_at: this._knex.fn.now()
176
- });
177
- return result;
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
+ };
178
605
  }
606
+ ```
179
607
 
180
- // Bulk assign camera to multiple videos
181
- async bulkUpdateCamera(
182
- videoIds: number[],
183
- cameraId: number | null
184
- ): Promise<number> {
185
- if (videoIds.length === 0) return 0;
186
-
187
- const result = await this._knex("video")
188
- .whereIn("id", videoIds)
189
- .update({
190
- cameraId: cameraId,
191
- updated_at: this._knex.fn.now()
192
- });
193
- return result;
608
+ ### Example 4: getVideoIdsByFolderId() Implementation
609
+
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');
616
+
617
+ return rows.map(row => row.id);
194
618
  }
619
+ ```
195
620
 
196
- // Get videos by multiple folder IDs (for folder camera propagation)
197
- async getVideosByFolderIds(folderIds: number[]): Promise<IVideo[]> {
198
- if (folderIds.length === 0) return [];
621
+ ### Example 5: getVideosByCameraIdWithFolder() Implementation
199
622
 
200
- return await this._knex("video")
201
- .whereIn("folderId", folderIds)
202
- .select("id", "uuid", "name", "folderId", "cameraId");
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
+ };
203
645
  }
204
646
  ```
205
647
 
206
- ## Query Performance Strategies
648
+ ---
207
649
 
208
- ### 1. Camera Search Optimization
650
+ ## Migration Requirements
209
651
 
210
- - **Index Strategy:** Case-insensitive BTREE index on `LOWER(name)`
211
- - **Query Pattern:** `WHERE LOWER(name) LIKE LOWER(?)`
212
- - **Full-text Option:** GIN index with `to_tsvector()` for fuzzy search
213
- - **Expected Performance:** O(log n) lookup instead of O(n) scan
652
+ **No new migrations needed** - All schema changes already exist:
214
653
 
215
- ### 2. Bulk Operation Optimization
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
216
658
 
217
- - **Transaction Wrapping:** All bulk operations in single transactions
218
- - **Batch Size Limits:** Process in chunks of 1000 records max
219
- - **Index Usage:** Leverage existing primary key and foreign key indexes
659
+ **Optional migration for performance**:
220
660
 
221
- ### 3. Folder Camera Cascade Optimization
661
+ - Check if video.folderId index exists (likely already present)
222
662
 
223
- - **Single Query Strategy:** Use `WHERE folderId = ?` leveraging existing index
224
- - **Atomic Updates:** Single UPDATE statement rather than individual updates
225
- - **Expected Performance:** O(log n) folder lookup + O(k) video updates
663
+ ---
226
664
 
227
- ## Implementation Order
665
+ ## Testing Strategy
666
+
667
+ ### Unit Tests Required
668
+
669
+ **CameraDAO.getAllWithSearch()**
670
+
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)
676
+
677
+ **CameraDAO.getVideosByCamera()**
678
+
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
683
+
684
+ **VideoDAO.bulkUpdateCamera()**
228
685
 
229
- 1. **Migration First:** Deploy camera name search index
230
- 2. **DAO Methods:** Add optimized search and bulk operation methods
231
- 3. **Transaction Optimization:** Implement proper transaction handling
232
- 4. **Testing:** Validate performance improvements
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)
692
+
693
+ **VideoDAO.getVideoIdsByFolderId()**
694
+
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)
699
+
700
+ **VideoDAO.getVideosByCameraIdWithFolder()**
701
+
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
707
+
708
+ ---
709
+
710
+ ## Implementation Checklist
711
+
712
+ ### Pre-Implementation
713
+
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
717
+
718
+ ### Implementation Phase
719
+
720
+ - [ ] Implement CameraDAO.getAllWithSearch()
721
+ - [ ] Implement VideoDAO.getVideoIdsByFolderId()
722
+ - [ ] Implement CameraDAO.getVideosByCamera()
723
+ - [ ] Implement VideoDAO.getVideosByCameraIdWithFolder()
724
+ - [ ] Implement VideoDAO.bulkUpdateCamera()
725
+
726
+ ### Testing Phase
727
+
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)
732
+
733
+ ### Integration Phase
734
+
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
+
742
+ ---
233
743
 
234
744
  ## Risk Assessment
235
745
 
236
- ### Migration Safety: LOW RISK
746
+ ### Low Risk
237
747
 
238
- - Adding indexes is non-blocking operation
239
- - No data changes, only performance improvements
240
- - Easy rollback with down migration
748
+ - getAllWithSearch() - Simple extension of getAll()
749
+ - getVideoIdsByFolderId() - Straightforward SELECT query
241
750
 
242
- ### Performance Impact: HIGH BENEFIT
751
+ ### Medium Risk
243
752
 
244
- - Camera search: 100x-1000x improvement (O(n) O(log n))
245
- - Bulk operations: Proper indexing already exists
246
- - Folder cascade: Leverages existing folderId index
753
+ - getVideosByCamera() - Requires correct JOIN syntax
754
+ - getVideosByCameraIdWithFolder() - Same as above
247
755
 
248
- ### Pattern Compliance: 100% COMPLIANT
756
+ ### High Risk
249
757
 
250
- - Follows existing DAO patterns exactly
251
- - Maintains IBaseDAO interface requirements
252
- - Uses established naming conventions
253
- - Preserves UUID external API pattern
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
254
762
 
255
- ## Validation Strategy
763
+ ### Performance Risks
256
764
 
257
- ### Performance Testing Queries
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
258
771
 
259
- ```sql
260
- -- Test camera search performance
261
- EXPLAIN ANALYZE SELECT * FROM cameras
262
- WHERE LOWER(name) LIKE '%traffic%'
263
- ORDER BY name LIMIT 20;
772
+ ---
264
773
 
265
- -- Test video-by-camera lookup
266
- EXPLAIN ANALYZE SELECT * FROM video
267
- WHERE cameraId = 1
268
- ORDER BY created_at DESC LIMIT 20;
774
+ ## Summary
269
775
 
270
- -- Test folder cascade update
271
- EXPLAIN ANALYZE UPDATE video
272
- SET cameraId = 1 WHERE folderId = 5;
273
- ```
776
+ All 5 missing methods have been thoroughly analyzed:
777
+
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
787
+
788
+ **Ready for implementation** - All patterns follow existing DAO conventions in codebase.
789
+
790
+ ---
791
+
792
+ ## Files to Modify
793
+
794
+ ### knex-rel/src/dao/camera/camera.dao.ts
795
+
796
+ - Add method: `getAllWithSearch(page, limit, name?)`
797
+ - Add method: `getVideosByCamera(cameraId, page, limit)`
798
+
799
+ ### knex-rel/src/dao/video/video.dao.ts
800
+
801
+ - Add method: `bulkUpdateCamera(videoIds, cameraId, trx?)`
802
+ - Add method: `getVideoIdsByFolderId(folderId)`
803
+ - Add method: `getVideosByCameraIdWithFolder(cameraId, page, limit)`
804
+
805
+ ---
806
+
807
+ ## Pattern Compliance Summary
274
808
 
275
- ### Success Metrics
809
+ ### Database Patterns ✅
276
810
 
277
- - Camera search < 50ms for 10k+ cameras
278
- - Video-by-camera < 100ms for 1k+ videos
279
- - Folder cascade < 200ms for 100+ videos per folder
280
- - Bulk video update < 500ms for 1000+ videos
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
281
816
 
282
- ## Conclusion
817
+ ### DAO Patterns ✅
283
818
 
284
- **Current Status:** Schema is well-designed with most needed indexes already present. Only critical gap is camera name search optimization.
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
285
824
 
286
- **Priority:** HIGH for camera name search index, MEDIUM for DAO method enhancements.
825
+ ### Query Patterns
287
826
 
288
- **Complexity:** LOW - Single migration required, standard DAO patterns.
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