@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.
- package/dist/dao/VideoMinuteResultDAO.d.ts +0 -4
- package/dist/dao/VideoMinuteResultDAO.js +7 -48
- package/dist/dao/VideoMinuteResultDAO.js.map +1 -1
- package/dist/dao/camera/camera.dao.d.ts +8 -1
- package/dist/dao/camera/camera.dao.js +27 -8
- package/dist/dao/camera/camera.dao.js.map +1 -1
- package/dist/dao/report-configuration/report-configuration.dao.d.ts +94 -0
- package/dist/dao/report-configuration/report-configuration.dao.js +352 -0
- package/dist/dao/report-configuration/report-configuration.dao.js.map +1 -0
- package/dist/dao/video/video.dao.d.ts +10 -0
- package/dist/dao/video/video.dao.js +40 -16
- package/dist/dao/video/video.dao.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/interfaces/report-configuration/report-configuration.interfaces.d.ts +26 -0
- package/dist/interfaces/report-configuration/report-configuration.interfaces.js +3 -0
- package/dist/interfaces/report-configuration/report-configuration.interfaces.js.map +1 -0
- package/migrations/20250930200521_migration.ts +52 -0
- package/package.json +1 -1
- package/plan.md +755 -212
- package/src/dao/VideoMinuteResultDAO.ts +7 -64
- package/src/dao/camera/camera.dao.ts +30 -10
- package/src/dao/report-configuration/report-configuration.dao.ts +402 -0
- package/src/dao/video/video.dao.ts +46 -18
- package/src/index.ts +8 -0
- package/src/interfaces/report-configuration/report-configuration.interfaces.ts +30 -0
- package/cameras_analysis.md +0 -199
- package/folder_cameraid_analysis.md +0 -167
- package/migrations/20250924000000_camera_name_search_index.ts +0 -22
package/plan.md
CHANGED
|
@@ -1,288 +1,831 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Missing DAO Methods Implementation Plan
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## Executive Summary
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
|
|
7
|
+
---
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
- Unique index: `uuid`
|
|
11
|
-
- Composite index: `[longitude, latitude]` for geospatial queries
|
|
9
|
+
## Schema Analysis
|
|
12
10
|
|
|
13
|
-
|
|
11
|
+
### Tables Involved
|
|
14
12
|
|
|
15
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
61
|
+
```typescript
|
|
62
|
+
async getAllWithSearch(page: number, limit: number, name?: string): Promise<IDataPaginator<ICamera>>
|
|
63
|
+
```
|
|
31
64
|
|
|
32
|
-
|
|
65
|
+
**Called From**: CameraService.getAllWithSearch()
|
|
33
66
|
|
|
34
|
-
**
|
|
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
|
-
|
|
37
|
-
- **Existing Index:** `[folderId]` (implicit from FK constraint)
|
|
38
|
-
- **Status:** No optimization needed
|
|
71
|
+
**Data Flow**:
|
|
39
72
|
|
|
40
|
-
|
|
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
|
-
|
|
81
|
+
### Implementation Requirements
|
|
43
82
|
|
|
44
|
-
|
|
45
|
-
- **Missing Index:** Text search index on `name` column
|
|
46
|
-
- **Impact:** Critical performance issue for camera search
|
|
83
|
+
**SQL Query**:
|
|
47
84
|
|
|
48
|
-
|
|
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
|
-
**
|
|
95
|
+
**Key Details**:
|
|
51
96
|
|
|
52
|
-
-
|
|
53
|
-
-
|
|
54
|
-
-
|
|
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
|
-
|
|
102
|
+
**Performance Considerations**:
|
|
57
103
|
|
|
58
|
-
|
|
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
|
-
|
|
61
|
-
- **Existing Index:** Primary key on `id`
|
|
62
|
-
- **Optimization Opportunity:** Batch processing with transaction optimization
|
|
108
|
+
---
|
|
63
109
|
|
|
64
|
-
##
|
|
110
|
+
## Method 2: CameraDAO.getVideosByCamera()
|
|
65
111
|
|
|
66
|
-
###
|
|
112
|
+
### Call Analysis
|
|
67
113
|
|
|
68
|
-
|
|
114
|
+
**Location**: `api-rel/src/service/camera/camera.service.ts:308`
|
|
69
115
|
|
|
70
|
-
**
|
|
71
|
-
**Purpose:** Optimize camera search by name functionality
|
|
116
|
+
**Method Signature**:
|
|
72
117
|
|
|
73
118
|
```typescript
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
139
|
+
**Critical Detail**: Service expects folder data in response (line 312: mapToVideoWithFolderDTO)
|
|
97
140
|
|
|
98
|
-
|
|
141
|
+
### Implementation Requirements
|
|
99
142
|
|
|
100
|
-
**
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
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
|
-
|
|
581
|
+
### Example 3: getVideosByCamera() Implementation
|
|
164
582
|
|
|
165
583
|
```typescript
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
.
|
|
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
|
-
|
|
197
|
-
async getVideosByFolderIds(folderIds: number[]): Promise<IVideo[]> {
|
|
198
|
-
if (folderIds.length === 0) return [];
|
|
621
|
+
### Example 5: getVideosByCameraIdWithFolder() Implementation
|
|
199
622
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
648
|
+
---
|
|
207
649
|
|
|
208
|
-
|
|
650
|
+
## Migration Requirements
|
|
209
651
|
|
|
210
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
661
|
+
- Check if video.folderId index exists (likely already present)
|
|
222
662
|
|
|
223
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
###
|
|
746
|
+
### Low Risk
|
|
237
747
|
|
|
238
|
-
-
|
|
239
|
-
-
|
|
240
|
-
- Easy rollback with down migration
|
|
748
|
+
- getAllWithSearch() - Simple extension of getAll()
|
|
749
|
+
- getVideoIdsByFolderId() - Straightforward SELECT query
|
|
241
750
|
|
|
242
|
-
###
|
|
751
|
+
### Medium Risk
|
|
243
752
|
|
|
244
|
-
-
|
|
245
|
-
-
|
|
246
|
-
- Folder cascade: Leverages existing folderId index
|
|
753
|
+
- getVideosByCamera() - Requires correct JOIN syntax
|
|
754
|
+
- getVideosByCameraIdWithFolder() - Same as above
|
|
247
755
|
|
|
248
|
-
###
|
|
756
|
+
### High Risk
|
|
249
757
|
|
|
250
|
-
-
|
|
251
|
-
-
|
|
252
|
-
-
|
|
253
|
-
-
|
|
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
|
-
|
|
763
|
+
### Performance Risks
|
|
256
764
|
|
|
257
|
-
|
|
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
|
-
|
|
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
|
-
|
|
266
|
-
EXPLAIN ANALYZE SELECT * FROM video
|
|
267
|
-
WHERE cameraId = 1
|
|
268
|
-
ORDER BY created_at DESC LIMIT 20;
|
|
774
|
+
## Summary
|
|
269
775
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
###
|
|
809
|
+
### Database Patterns ✅
|
|
276
810
|
|
|
277
|
-
-
|
|
278
|
-
-
|
|
279
|
-
-
|
|
280
|
-
-
|
|
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
|
-
|
|
817
|
+
### DAO Patterns ✅
|
|
283
818
|
|
|
284
|
-
|
|
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
|
-
|
|
825
|
+
### Query Patterns ✅
|
|
287
826
|
|
|
288
|
-
|
|
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
|