@trafficgroup/knex-rel 0.1.5 → 0.1.7

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 DELETED
@@ -1,831 +0,0 @@
1
- # Missing DAO Methods Implementation Plan
2
-
3
- ## Executive Summary
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.
6
-
7
- ---
8
-
9
- ## Schema Analysis
10
-
11
- ### Tables Involved
12
-
13
- **cameras**
14
-
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`
58
-
59
- **Method Signature**:
60
-
61
- ```typescript
62
- async getAllWithSearch(page: number, limit: number, name?: string): Promise<IDataPaginator<ICamera>>
63
- ```
64
-
65
- **Called From**: CameraService.getAllWithSearch()
66
-
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
70
-
71
- **Data Flow**:
72
-
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
- ```
80
-
81
- ### Implementation Requirements
82
-
83
- **SQL Query**:
84
-
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
- ```
94
-
95
- **Key Details**:
96
-
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
101
-
102
- **Performance Considerations**:
103
-
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)
107
-
108
- ---
109
-
110
- ## Method 2: CameraDAO.getVideosByCamera()
111
-
112
- ### Call Analysis
113
-
114
- **Location**: `api-rel/src/service/camera/camera.service.ts:308`
115
-
116
- **Method Signature**:
117
-
118
- ```typescript
119
- async getVideosByCamera(cameraId: number, page: number, limit: number): Promise<IDataPaginator<IVideo>>
120
- ```
121
-
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
137
- ```
138
-
139
- **Critical Detail**: Service expects folder data in response (line 312: mapToVideoWithFolderDTO)
140
-
141
- ### Implementation Requirements
142
-
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**:
183
-
184
- ```typescript
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
368
-
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
- };
556
- }
557
- ```
558
-
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
- ```
580
-
581
- ### Example 3: getVideosByCamera() Implementation
582
-
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
- ```
607
-
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);
618
- }
619
- ```
620
-
621
- ### Example 5: getVideosByCameraIdWithFolder() Implementation
622
-
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
- ```
647
-
648
- ---
649
-
650
- ## Migration Requirements
651
-
652
- **No new migrations needed** - All schema changes already exist:
653
-
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
658
-
659
- **Optional migration for performance**:
660
-
661
- - Check if video.folderId index exists (likely already present)
662
-
663
- ---
664
-
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()**
685
-
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
- ---
743
-
744
- ## Risk Assessment
745
-
746
- ### Low Risk
747
-
748
- - getAllWithSearch() - Simple extension of getAll()
749
- - getVideoIdsByFolderId() - Straightforward SELECT query
750
-
751
- ### Medium Risk
752
-
753
- - getVideosByCamera() - Requires correct JOIN syntax
754
- - getVideosByCameraIdWithFolder() - Same as above
755
-
756
- ### High Risk
757
-
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
762
-
763
- ### Performance Risks
764
-
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
771
-
772
- ---
773
-
774
- ## Summary
775
-
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
808
-
809
- ### Database Patterns ✅
810
-
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
816
-
817
- ### DAO Patterns ✅
818
-
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
824
-
825
- ### Query Patterns ✅
826
-
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