@trafficgroup/knex-rel 0.1.4 → 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/camera/camera.dao.d.ts +9 -0
- package/dist/dao/camera/camera.dao.js +61 -0
- package/dist/dao/camera/camera.dao.js.map +1 -1
- package/dist/dao/video/video.dao.d.ts +13 -0
- package/dist/dao/video/video.dao.js +63 -0
- package/dist/dao/video/video.dao.js.map +1 -1
- package/package.json +1 -1
- package/plan.md +596 -799
- package/src/dao/camera/camera.dao.ts +75 -0
- package/src/dao/video/video.dao.ts +75 -0
package/plan.md
CHANGED
|
@@ -1,1034 +1,831 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Missing DAO Methods Implementation Plan
|
|
2
2
|
|
|
3
3
|
## Executive Summary
|
|
4
4
|
|
|
5
|
-
This plan
|
|
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
|
-
- Configurations selected when viewing video results or study summaries
|
|
11
|
-
- Always maintain at least 1 configuration (cannot delete last one)
|
|
12
|
-
- Many-to-One ONLY mapping (no duplicates allowed)
|
|
13
|
-
- Unmapped FHWA classes excluded from reports
|
|
14
|
-
- API-level transformation (frontend receives only transformed data)
|
|
15
|
-
- Applies to both TMC and ATR study types
|
|
16
|
-
- Minute-level granularity transformation
|
|
9
|
+
## Schema Analysis
|
|
17
10
|
|
|
18
|
-
|
|
11
|
+
### Tables Involved
|
|
19
12
|
|
|
20
|
-
|
|
13
|
+
**cameras**
|
|
21
14
|
|
|
22
|
-
|
|
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
|
+
```
|
|
23
26
|
|
|
24
|
-
|
|
27
|
+
**video**
|
|
25
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)
|
|
26
39
|
```
|
|
27
|
-
|
|
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)
|
|
28
49
|
```
|
|
29
50
|
|
|
30
|
-
|
|
51
|
+
---
|
|
31
52
|
|
|
32
|
-
|
|
53
|
+
## Method 1: CameraDAO.getAllWithSearch()
|
|
33
54
|
|
|
34
|
-
|
|
35
|
-
- `car`, `vehicle`, `automobiles` → normalized to `cars`
|
|
36
|
-
- `medium`, `pickup`, `truck` → normalized to `mediums`
|
|
37
|
-
- `heavy`, `largetruck`, `bigtruck` → normalized to `heavy_trucks`
|
|
38
|
-
- `pedestrian`, `person`, `people` → normalized to `pedestrians`
|
|
39
|
-
- `bicycle`, `bike`, `cyclist` → normalized to `bicycles`
|
|
55
|
+
### Call Analysis
|
|
40
56
|
|
|
41
|
-
**
|
|
57
|
+
**Location**: `api-rel/src/service/camera/camera.service.ts:274`
|
|
42
58
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
Class 4: bus
|
|
48
|
-
Class 5: work_van
|
|
49
|
-
Class 6-8: single_unit_truck
|
|
50
|
-
Class 9-13: articulated_truck
|
|
59
|
+
**Method Signature**:
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
async getAllWithSearch(page: number, limit: number, name?: string): Promise<IDataPaginator<ICamera>>
|
|
51
63
|
```
|
|
52
64
|
|
|
53
|
-
|
|
65
|
+
**Called From**: CameraService.getAllWithSearch()
|
|
54
66
|
|
|
55
|
-
|
|
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**:
|
|
56
72
|
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
work_van: [5],
|
|
64
|
-
single_unit_truck: [6, 7, 8],
|
|
65
|
-
articulated_truck: [9, 10, 11, 12, 13],
|
|
66
|
-
// Non-motorized (excluded from FHWA system)
|
|
67
|
-
pedestrian: [],
|
|
68
|
-
bicycle: [],
|
|
69
|
-
motorized_vehicle: [2], // Map to car
|
|
70
|
-
non_motorized_vehicle: [],
|
|
71
|
-
};
|
|
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
|
|
72
79
|
```
|
|
73
80
|
|
|
74
|
-
|
|
81
|
+
### Implementation Requirements
|
|
75
82
|
|
|
76
|
-
|
|
77
|
-
2. DAO converts detection labels → FHWA classes using DETECTION_LABEL_TO_FHWA
|
|
78
|
-
3. DAO applies user configuration: FHWA classes → Custom classes
|
|
79
|
-
4. API returns only custom class counts to frontend
|
|
83
|
+
**SQL Query**:
|
|
80
84
|
|
|
81
|
-
|
|
85
|
+
```sql
|
|
86
|
+
-- Count query
|
|
87
|
+
SELECT COUNT(*) as count FROM cameras WHERE name ILIKE '%search%'
|
|
82
88
|
|
|
83
|
-
|
|
89
|
+
-- Data query
|
|
90
|
+
SELECT * FROM cameras
|
|
91
|
+
WHERE name ILIKE '%search%'
|
|
92
|
+
LIMIT ? OFFSET ?
|
|
93
|
+
```
|
|
84
94
|
|
|
85
|
-
|
|
86
|
-
2. Apply FHWA mapping + user configuration at aggregation time
|
|
87
|
-
3. Return only custom class names to API
|
|
95
|
+
**Key Details**:
|
|
88
96
|
|
|
89
|
-
|
|
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
|
|
90
101
|
|
|
91
|
-
|
|
102
|
+
**Performance Considerations**:
|
|
92
103
|
|
|
93
|
-
|
|
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)
|
|
94
107
|
|
|
95
|
-
|
|
96
|
-
CREATE TABLE report_configurations (
|
|
97
|
-
id SERIAL PRIMARY KEY,
|
|
98
|
-
uuid UUID NOT NULL UNIQUE DEFAULT gen_random_uuid(),
|
|
99
|
-
name VARCHAR(30) NOT NULL UNIQUE,
|
|
100
|
-
description TEXT,
|
|
101
|
-
configuration JSONB NOT NULL,
|
|
102
|
-
is_default BOOLEAN NOT NULL DEFAULT false,
|
|
103
|
-
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
104
|
-
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
|
105
|
-
);
|
|
106
|
-
|
|
107
|
-
CREATE INDEX idx_report_configurations_uuid ON report_configurations(uuid);
|
|
108
|
-
CREATE INDEX idx_report_configurations_is_default ON report_configurations(is_default);
|
|
109
|
-
CREATE INDEX idx_report_configurations_name_lower ON report_configurations(LOWER(name));
|
|
110
|
-
```
|
|
108
|
+
---
|
|
111
109
|
|
|
112
|
-
|
|
110
|
+
## Method 2: CameraDAO.getVideosByCamera()
|
|
113
111
|
|
|
114
|
-
|
|
115
|
-
- `uuid`: UUID, UNIQUE, NOT NULL, auto-generated
|
|
116
|
-
- `configuration`: JSONB, NOT NULL
|
|
117
|
-
- At least 1 configuration must exist (enforced at application level)
|
|
112
|
+
### Call Analysis
|
|
118
113
|
|
|
119
|
-
|
|
114
|
+
**Location**: `api-rel/src/service/camera/camera.service.ts:308`
|
|
115
|
+
|
|
116
|
+
**Method Signature**:
|
|
120
117
|
|
|
121
118
|
```typescript
|
|
122
|
-
|
|
123
|
-
"version": "1.0",
|
|
124
|
-
"customClasses": [
|
|
125
|
-
{
|
|
126
|
-
"name": "Light Vehicles",
|
|
127
|
-
"fhwaClasses": [1, 2, 3]
|
|
128
|
-
},
|
|
129
|
-
{
|
|
130
|
-
"name": "Medium Trucks",
|
|
131
|
-
"fhwaClasses": [5]
|
|
132
|
-
},
|
|
133
|
-
{
|
|
134
|
-
"name": "Heavy Trucks",
|
|
135
|
-
"fhwaClasses": [4, 6, 7, 8, 9, 10, 11, 12, 13]
|
|
136
|
-
}
|
|
137
|
-
]
|
|
138
|
-
}
|
|
119
|
+
async getVideosByCamera(cameraId: number, page: number, limit: number): Promise<IDataPaginator<IVideo>>
|
|
139
120
|
```
|
|
140
121
|
|
|
141
|
-
**
|
|
122
|
+
**Called From**: CameraService.getVideosByCamera()
|
|
142
123
|
|
|
143
|
-
-
|
|
144
|
-
-
|
|
145
|
-
-
|
|
146
|
-
- No FHWA class can appear in multiple custom classes (Many-to-One)
|
|
147
|
-
- Unmapped FHWA classes are allowed (will be excluded from reports)
|
|
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)
|
|
148
127
|
|
|
149
|
-
|
|
128
|
+
**Data Flow**:
|
|
150
129
|
|
|
151
|
-
|
|
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
|
+
```
|
|
152
138
|
|
|
153
|
-
|
|
139
|
+
**Critical Detail**: Service expects folder data in response (line 312: mapToVideoWithFolderDTO)
|
|
154
140
|
|
|
155
|
-
|
|
156
|
-
import type { Knex } from "knex";
|
|
157
|
-
|
|
158
|
-
export async function up(knex: Knex): Promise<void> {
|
|
159
|
-
await knex.schema.createTable("report_configurations", (table) => {
|
|
160
|
-
table.increments("id").primary();
|
|
161
|
-
table
|
|
162
|
-
.uuid("uuid")
|
|
163
|
-
.notNullable()
|
|
164
|
-
.unique()
|
|
165
|
-
.defaultTo(knex.raw("gen_random_uuid()"));
|
|
166
|
-
table.string("name", 30).notNullable().unique();
|
|
167
|
-
table.text("description").nullable();
|
|
168
|
-
table.jsonb("configuration").notNullable();
|
|
169
|
-
table.boolean("is_default").notNullable().defaultTo(false);
|
|
170
|
-
table.timestamps(true, true);
|
|
171
|
-
|
|
172
|
-
// Indexes
|
|
173
|
-
table.index("uuid", "idx_report_configurations_uuid");
|
|
174
|
-
table.index("is_default", "idx_report_configurations_is_default");
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
// Add case-insensitive name index
|
|
178
|
-
await knex.raw(`
|
|
179
|
-
CREATE INDEX idx_report_configurations_name_lower
|
|
180
|
-
ON report_configurations(LOWER(name))
|
|
181
|
-
`);
|
|
182
|
-
|
|
183
|
-
// Seed default configuration (matches current system behavior)
|
|
184
|
-
await knex("report_configurations").insert({
|
|
185
|
-
name: "Default",
|
|
186
|
-
description: "Standard configuration matching current system behavior",
|
|
187
|
-
configuration: JSON.stringify({
|
|
188
|
-
version: "1.0",
|
|
189
|
-
customClasses: [
|
|
190
|
-
{
|
|
191
|
-
name: "Cars",
|
|
192
|
-
fhwaClasses: [1, 2], // motorcycle, car
|
|
193
|
-
},
|
|
194
|
-
{
|
|
195
|
-
name: "Medium Trucks",
|
|
196
|
-
fhwaClasses: [3, 5], // pickup_truck, work_van
|
|
197
|
-
},
|
|
198
|
-
{
|
|
199
|
-
name: "Heavy Trucks",
|
|
200
|
-
fhwaClasses: [4, 6, 7, 8, 9, 10, 11, 12, 13], // bus, single_unit_truck, articulated_truck
|
|
201
|
-
},
|
|
202
|
-
],
|
|
203
|
-
}),
|
|
204
|
-
is_default: true,
|
|
205
|
-
});
|
|
206
|
-
}
|
|
141
|
+
### Implementation Requirements
|
|
207
142
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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 ?
|
|
212
157
|
```
|
|
213
158
|
|
|
214
|
-
**
|
|
159
|
+
**Key Details**:
|
|
215
160
|
|
|
216
|
-
-
|
|
217
|
-
-
|
|
218
|
-
-
|
|
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
|
|
219
165
|
|
|
220
|
-
|
|
166
|
+
**Performance Considerations**:
|
|
221
167
|
|
|
222
|
-
|
|
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
|
|
223
171
|
|
|
224
|
-
|
|
172
|
+
---
|
|
225
173
|
|
|
226
|
-
|
|
227
|
-
export interface IReportConfiguration {
|
|
228
|
-
id: number;
|
|
229
|
-
uuid: string;
|
|
230
|
-
name: string;
|
|
231
|
-
description?: string;
|
|
232
|
-
configuration: IReportConfigurationData;
|
|
233
|
-
isDefault: boolean;
|
|
234
|
-
createdAt: string;
|
|
235
|
-
updatedAt: string;
|
|
236
|
-
}
|
|
174
|
+
## Method 3: VideoDAO.bulkUpdateCamera()
|
|
237
175
|
|
|
238
|
-
|
|
239
|
-
version: string;
|
|
240
|
-
customClasses: ICustomClass[];
|
|
241
|
-
}
|
|
176
|
+
### Call Analysis
|
|
242
177
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
}
|
|
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`
|
|
247
181
|
|
|
248
|
-
|
|
249
|
-
name: string;
|
|
250
|
-
description?: string;
|
|
251
|
-
configuration: IReportConfigurationData;
|
|
252
|
-
isDefault?: boolean;
|
|
253
|
-
}
|
|
182
|
+
**Method Signature**:
|
|
254
183
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
errors: string[];
|
|
258
|
-
}
|
|
184
|
+
```typescript
|
|
185
|
+
async bulkUpdateCamera(videoIds: number[], cameraId: number | null, trx?: Knex.Transaction): Promise<number>
|
|
259
186
|
```
|
|
260
187
|
|
|
261
|
-
|
|
188
|
+
**Call Patterns**:
|
|
262
189
|
|
|
263
|
-
|
|
190
|
+
**Pattern 1 - CameraService.bulkAssignToVideos() (line 364)**:
|
|
264
191
|
|
|
265
|
-
|
|
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)
|
|
266
196
|
|
|
267
|
-
|
|
268
|
-
import { Knex } from "knex";
|
|
269
|
-
import { IBaseDAO, IDataPaginator } from "../../d.types";
|
|
270
|
-
import {
|
|
271
|
-
IReportConfiguration,
|
|
272
|
-
IReportConfigurationInput,
|
|
273
|
-
IReportConfigurationData,
|
|
274
|
-
IConfigurationValidationResult,
|
|
275
|
-
} from "../../interfaces/report-configuration/report-configuration.interfaces";
|
|
276
|
-
import KnexManager from "../../KnexConnection";
|
|
277
|
-
|
|
278
|
-
// Static mapping: Detection Label → FHWA Classes
|
|
279
|
-
const DETECTION_LABEL_TO_FHWA: Record<string, number[]> = {
|
|
280
|
-
motorcycle: [1],
|
|
281
|
-
car: [2],
|
|
282
|
-
pickup_truck: [3],
|
|
283
|
-
bus: [4],
|
|
284
|
-
work_van: [5],
|
|
285
|
-
single_unit_truck: [6, 7, 8],
|
|
286
|
-
articulated_truck: [9, 10, 11, 12, 13],
|
|
287
|
-
motorized_vehicle: [2], // Map to car
|
|
288
|
-
// Non-motorized excluded from FHWA
|
|
289
|
-
pedestrian: [],
|
|
290
|
-
bicycle: [],
|
|
291
|
-
non_motorized_vehicle: [],
|
|
292
|
-
};
|
|
293
|
-
|
|
294
|
-
export class ReportConfigurationDAO implements IBaseDAO<IReportConfiguration> {
|
|
295
|
-
private knex: Knex = KnexManager.getConnection();
|
|
296
|
-
private tableName = "report_configurations";
|
|
297
|
-
|
|
298
|
-
// ==================== STANDARD CRUD ====================
|
|
299
|
-
|
|
300
|
-
async create(item: IReportConfigurationInput): Promise<IReportConfiguration> {
|
|
301
|
-
// Validate configuration before creation
|
|
302
|
-
const validation = this.validateConfiguration(item.configuration);
|
|
303
|
-
if (!validation.valid) {
|
|
304
|
-
throw new Error(`Invalid configuration: ${validation.errors.join(", ")}`);
|
|
305
|
-
}
|
|
197
|
+
**Pattern 2 - VideoService.bulkUpdateCamera() (line 34)**:
|
|
306
198
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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)
|
|
311
204
|
|
|
312
|
-
|
|
313
|
-
}
|
|
205
|
+
**Pattern 3 - FolderService.cascadeUpdateVideosCamera() (line 96)**:
|
|
314
206
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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)
|
|
319
211
|
|
|
320
|
-
|
|
321
|
-
const result = await this.knex(this.tableName).where("uuid", uuid).first();
|
|
322
|
-
return result ? this.mapDbRowToEntity(result) : null;
|
|
323
|
-
}
|
|
212
|
+
### Implementation Requirements
|
|
324
213
|
|
|
325
|
-
|
|
326
|
-
page: number = 1,
|
|
327
|
-
limit: number = 10,
|
|
328
|
-
): Promise<IDataPaginator<IReportConfiguration>> {
|
|
329
|
-
const offset = (page - 1) * limit;
|
|
214
|
+
**SQL Query**:
|
|
330
215
|
|
|
331
|
-
|
|
332
|
-
|
|
216
|
+
```sql
|
|
217
|
+
UPDATE video
|
|
218
|
+
SET cameraId = ?, updated_at = NOW()
|
|
219
|
+
WHERE id = ANY(?)
|
|
220
|
+
RETURNING id
|
|
221
|
+
```
|
|
333
222
|
|
|
334
|
-
|
|
335
|
-
.select("*")
|
|
336
|
-
.orderBy("is_default", "desc") // Default first
|
|
337
|
-
.orderBy("name", "asc")
|
|
338
|
-
.limit(limit)
|
|
339
|
-
.offset(offset);
|
|
223
|
+
**Key Details**:
|
|
340
224
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
totalCount,
|
|
348
|
-
totalPages: Math.ceil(totalCount / limit),
|
|
349
|
-
};
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
async update(
|
|
353
|
-
id: number,
|
|
354
|
-
item: Partial<IReportConfigurationInput>,
|
|
355
|
-
): Promise<IReportConfiguration | null> {
|
|
356
|
-
// Validate configuration if provided
|
|
357
|
-
if (item.configuration) {
|
|
358
|
-
const validation = this.validateConfiguration(item.configuration);
|
|
359
|
-
if (!validation.valid) {
|
|
360
|
-
throw new Error(
|
|
361
|
-
`Invalid configuration: ${validation.errors.join(", ")}`,
|
|
362
|
-
);
|
|
363
|
-
}
|
|
364
|
-
}
|
|
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
|
|
365
231
|
|
|
366
|
-
|
|
367
|
-
const [result] = await this.knex(this.tableName)
|
|
368
|
-
.where("id", id)
|
|
369
|
-
.update({ ...dbData, updated_at: this.knex.fn.now() })
|
|
370
|
-
.returning("*");
|
|
232
|
+
**Transaction Handling**:
|
|
371
233
|
|
|
372
|
-
|
|
373
|
-
|
|
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
|
+
```
|
|
374
242
|
|
|
375
|
-
|
|
376
|
-
// Use deleteWithValidation to prevent deletion of last config
|
|
377
|
-
return this.deleteWithValidation(id);
|
|
378
|
-
}
|
|
243
|
+
**Performance Considerations**:
|
|
379
244
|
|
|
380
|
-
|
|
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)
|
|
381
249
|
|
|
382
|
-
|
|
383
|
-
* Delete configuration with validation (prevents deletion of last config)
|
|
384
|
-
*/
|
|
385
|
-
async deleteWithValidation(id: number): Promise<boolean> {
|
|
386
|
-
const [countResult] = await this.knex(this.tableName).count("* as count");
|
|
387
|
-
const totalCount = +countResult.count;
|
|
250
|
+
**Error Handling**:
|
|
388
251
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
);
|
|
393
|
-
}
|
|
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
|
|
394
255
|
|
|
395
|
-
|
|
396
|
-
return result > 0;
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
/**
|
|
400
|
-
* Get default configuration (for UI convenience)
|
|
401
|
-
*/
|
|
402
|
-
async getDefault(): Promise<IReportConfiguration | null> {
|
|
403
|
-
const result = await this.knex(this.tableName)
|
|
404
|
-
.where("is_default", true)
|
|
405
|
-
.first();
|
|
406
|
-
|
|
407
|
-
// If no default, return first configuration
|
|
408
|
-
if (!result) {
|
|
409
|
-
const fallback = await this.knex(this.tableName)
|
|
410
|
-
.orderBy("created_at", "asc")
|
|
411
|
-
.first();
|
|
412
|
-
return fallback ? this.mapDbRowToEntity(fallback) : null;
|
|
413
|
-
}
|
|
256
|
+
---
|
|
414
257
|
|
|
415
|
-
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
/**
|
|
419
|
-
* Set a configuration as default (removes default flag from others)
|
|
420
|
-
*/
|
|
421
|
-
async setDefault(id: number): Promise<boolean> {
|
|
422
|
-
await this.knex.transaction(async (trx) => {
|
|
423
|
-
// Remove default flag from all
|
|
424
|
-
await trx(this.tableName).update({ is_default: false });
|
|
425
|
-
|
|
426
|
-
// Set new default
|
|
427
|
-
await trx(this.tableName).where("id", id).update({ is_default: true });
|
|
428
|
-
});
|
|
429
|
-
|
|
430
|
-
return true;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
/**
|
|
434
|
-
* Validate configuration structure and business rules
|
|
435
|
-
*/
|
|
436
|
-
validateConfiguration(
|
|
437
|
-
config: IReportConfigurationData,
|
|
438
|
-
): IConfigurationValidationResult {
|
|
439
|
-
const errors: string[] = [];
|
|
440
|
-
|
|
441
|
-
// Check structure
|
|
442
|
-
if (!config || typeof config !== "object") {
|
|
443
|
-
errors.push("Configuration must be an object");
|
|
444
|
-
return { valid: false, errors };
|
|
445
|
-
}
|
|
258
|
+
## Method 4: VideoDAO.getVideoIdsByFolderId()
|
|
446
259
|
|
|
447
|
-
|
|
448
|
-
errors.push("customClasses must be an array");
|
|
449
|
-
return { valid: false, errors };
|
|
450
|
-
}
|
|
260
|
+
### Call Analysis
|
|
451
261
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
if (config.customClasses.length > 7) {
|
|
457
|
-
errors.push("Maximum 7 custom classes allowed");
|
|
458
|
-
}
|
|
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`
|
|
459
266
|
|
|
460
|
-
|
|
461
|
-
const usedFhwaClasses = new Set<number>();
|
|
462
|
-
const classNames = new Set<string>();
|
|
463
|
-
|
|
464
|
-
for (const customClass of config.customClasses) {
|
|
465
|
-
// Check name length
|
|
466
|
-
if (!customClass.name || customClass.name.length > 30) {
|
|
467
|
-
errors.push(
|
|
468
|
-
`Custom class name must be 1-30 characters: "${customClass.name}"`,
|
|
469
|
-
);
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
// Check for duplicate names
|
|
473
|
-
if (classNames.has(customClass.name)) {
|
|
474
|
-
errors.push(`Duplicate custom class name: "${customClass.name}"`);
|
|
475
|
-
}
|
|
476
|
-
classNames.add(customClass.name);
|
|
477
|
-
|
|
478
|
-
// Check fhwaClasses array
|
|
479
|
-
if (
|
|
480
|
-
!Array.isArray(customClass.fhwaClasses) ||
|
|
481
|
-
customClass.fhwaClasses.length === 0
|
|
482
|
-
) {
|
|
483
|
-
errors.push(
|
|
484
|
-
`Custom class "${customClass.name}" must have at least one FHWA class`,
|
|
485
|
-
);
|
|
486
|
-
continue;
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
// Check for duplicates (Many-to-One enforcement)
|
|
490
|
-
for (const fhwaClass of customClass.fhwaClasses) {
|
|
491
|
-
if (usedFhwaClasses.has(fhwaClass)) {
|
|
492
|
-
errors.push(
|
|
493
|
-
`FHWA class ${fhwaClass} appears in multiple custom classes (not allowed)`,
|
|
494
|
-
);
|
|
495
|
-
}
|
|
496
|
-
usedFhwaClasses.add(fhwaClass);
|
|
497
|
-
|
|
498
|
-
// Validate FHWA class range
|
|
499
|
-
if (fhwaClass < 1 || fhwaClass > 13) {
|
|
500
|
-
errors.push(`Invalid FHWA class ${fhwaClass} (must be 1-13)`);
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
}
|
|
267
|
+
**Method Signature**:
|
|
504
268
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
};
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
/**
|
|
512
|
-
* Apply configuration to raw detection counts
|
|
513
|
-
* @param detectionCounts - Raw counts from video processing (e.g., {car: 50, pickup_truck: 20})
|
|
514
|
-
* @param configuration - Report configuration to apply
|
|
515
|
-
* @returns Transformed counts with custom class names
|
|
516
|
-
*/
|
|
517
|
-
applyConfiguration(
|
|
518
|
-
detectionCounts: Record<string, number>,
|
|
519
|
-
configuration: IReportConfigurationData,
|
|
520
|
-
): Record<string, number> {
|
|
521
|
-
// Step 1: Convert detection labels to FHWA classes
|
|
522
|
-
const fhwaCounts: Record<number, number> = {};
|
|
523
|
-
|
|
524
|
-
for (const [detectionLabel, count] of Object.entries(detectionCounts)) {
|
|
525
|
-
const fhwaClasses = DETECTION_LABEL_TO_FHWA[detectionLabel] || [];
|
|
526
|
-
|
|
527
|
-
// Distribute count equally among FHWA classes (for multi-mapped labels like single_unit_truck)
|
|
528
|
-
const countPerClass = count / fhwaClasses.length;
|
|
529
|
-
|
|
530
|
-
for (const fhwaClass of fhwaClasses) {
|
|
531
|
-
fhwaCounts[fhwaClass] = (fhwaCounts[fhwaClass] || 0) + countPerClass;
|
|
532
|
-
}
|
|
533
|
-
}
|
|
269
|
+
```typescript
|
|
270
|
+
async getVideoIdsByFolderId(folderId: number): Promise<number[]>
|
|
271
|
+
```
|
|
534
272
|
|
|
535
|
-
|
|
536
|
-
const customCounts: Record<string, number> = {};
|
|
273
|
+
**Call Patterns**:
|
|
537
274
|
|
|
538
|
-
|
|
539
|
-
let total = 0;
|
|
275
|
+
**Pattern 1 - FolderService.updateWithCameraCascade() (line 49)**:
|
|
540
276
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
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
|
|
544
281
|
|
|
545
|
-
|
|
546
|
-
}
|
|
282
|
+
**Pattern 2 - FolderService.cascadeUpdateVideosCamera() (line 89)**:
|
|
547
283
|
|
|
548
|
-
|
|
549
|
-
|
|
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)
|
|
550
288
|
|
|
551
|
-
|
|
289
|
+
**Pattern 3 - FolderService.getVideoCascadeCount() (line 163)**:
|
|
552
290
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
name: row.name,
|
|
558
|
-
description: row.description,
|
|
559
|
-
configuration: row.configuration,
|
|
560
|
-
isDefault: row.is_default,
|
|
561
|
-
createdAt: row.created_at,
|
|
562
|
-
updatedAt: row.updated_at,
|
|
563
|
-
};
|
|
564
|
-
}
|
|
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)
|
|
565
295
|
|
|
566
|
-
|
|
567
|
-
return rows.map((row) => this.mapDbRowToEntity(row));
|
|
568
|
-
}
|
|
296
|
+
**Pattern 4 - VideoService.getVideoIdsByFolderId() (line 119)**:
|
|
569
297
|
|
|
570
|
-
|
|
571
|
-
|
|
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
|
|
572
302
|
|
|
573
|
-
|
|
574
|
-
if (input.description !== undefined) dbRow.description = input.description;
|
|
575
|
-
if (input.configuration !== undefined)
|
|
576
|
-
dbRow.configuration = input.configuration;
|
|
577
|
-
if (input.isDefault !== undefined) dbRow.is_default = input.isDefault;
|
|
303
|
+
### Implementation Requirements
|
|
578
304
|
|
|
579
|
-
|
|
580
|
-
}
|
|
581
|
-
}
|
|
305
|
+
**SQL Query**:
|
|
582
306
|
|
|
583
|
-
|
|
307
|
+
```sql
|
|
308
|
+
SELECT id FROM video WHERE folderId = ?
|
|
584
309
|
```
|
|
585
310
|
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
## Integration with VideoMinuteResultDAO
|
|
311
|
+
**Key Details**:
|
|
589
312
|
|
|
590
|
-
|
|
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)
|
|
591
318
|
|
|
592
|
-
|
|
319
|
+
**Performance Considerations**:
|
|
593
320
|
|
|
594
|
-
|
|
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
|
|
595
325
|
|
|
596
|
-
|
|
326
|
+
**Return Value**:
|
|
597
327
|
|
|
598
328
|
```typescript
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
startMinute?: number,
|
|
605
|
-
endMinute?: number,
|
|
606
|
-
): Promise<IGroupedResponse>
|
|
329
|
+
const ids = await this._knex("video")
|
|
330
|
+
.where({ folderId })
|
|
331
|
+
.select("id")
|
|
332
|
+
.orderBy("id", "asc");
|
|
333
|
+
return ids.map((row) => row.id);
|
|
607
334
|
```
|
|
608
335
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
1. **Import ReportConfigurationDAO:**
|
|
336
|
+
---
|
|
612
337
|
|
|
613
|
-
|
|
614
|
-
import { ReportConfigurationDAO } from "./report-configuration/report-configuration.dao";
|
|
615
|
-
```
|
|
338
|
+
## Method 5: VideoDAO.getVideosByCameraIdWithFolder()
|
|
616
339
|
|
|
617
|
-
|
|
618
|
-
- Accept `configuration` parameter
|
|
619
|
-
- Apply configuration transformation AFTER aggregating detection labels
|
|
620
|
-
- Replace vehicle class keys with custom class names
|
|
340
|
+
### Call Analysis
|
|
621
341
|
|
|
622
|
-
|
|
623
|
-
- Accept `configuration` parameter
|
|
624
|
-
- Remove `normalizeATRVehicleClass()` calls (lines 606, 634)
|
|
625
|
-
- Apply configuration transformation instead
|
|
342
|
+
**Location**: `api-rel/src/service/video/video.service.ts:77`
|
|
626
343
|
|
|
627
|
-
|
|
344
|
+
**Method Signature**:
|
|
628
345
|
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
346
|
+
```typescript
|
|
347
|
+
async getVideosByCameraIdWithFolder(cameraId: number, page: number, limit: number): Promise<IDataPaginator<IVideo>>
|
|
348
|
+
```
|
|
632
349
|
|
|
633
|
-
|
|
350
|
+
**Called From**: VideoService.getVideosByCameraId()
|
|
634
351
|
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
config = await configDAO.getDefault();
|
|
639
|
-
}
|
|
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
|
|
640
355
|
|
|
641
|
-
|
|
642
|
-
throw new Error("No report configuration found");
|
|
643
|
-
}
|
|
356
|
+
**Data Flow**:
|
|
644
357
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
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
|
+
```
|
|
648
364
|
|
|
649
|
-
|
|
365
|
+
**Critical Detail**: Method name explicitly includes "WithFolder" - MUST JOIN folders
|
|
650
366
|
|
|
651
|
-
|
|
652
|
-
// In getGroupedMinuteResultsByVideoUuid()
|
|
653
|
-
const configuration = await this.fetchConfiguration(configurationId);
|
|
367
|
+
### Implementation Requirements
|
|
654
368
|
|
|
655
|
-
|
|
656
|
-
// ... existing aggregation logic ...
|
|
369
|
+
**SQL Query**:
|
|
657
370
|
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
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
|
+
```
|
|
664
385
|
|
|
665
|
-
|
|
666
|
-
});
|
|
667
|
-
```
|
|
386
|
+
**Key Details**:
|
|
668
387
|
|
|
669
|
-
|
|
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>
|
|
670
393
|
|
|
671
|
-
**
|
|
394
|
+
**Performance Considerations**:
|
|
672
395
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
car: { lane_1: 50, lane_2: 30 },
|
|
677
|
-
pickup_truck: { lane_1: 10, lane_2: 15 },
|
|
678
|
-
bus: { lane_1: 5, lane_2: 3 }
|
|
679
|
-
},
|
|
680
|
-
detected_classes: {
|
|
681
|
-
car: 80,
|
|
682
|
-
pickup_truck: 25,
|
|
683
|
-
bus: 8
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
```
|
|
396
|
+
- Same as Method 2 (getVideosByCamera)
|
|
397
|
+
- cameraId indexed
|
|
398
|
+
- Consider composite index on (cameraId, created_at) for sorting
|
|
687
399
|
|
|
688
|
-
**
|
|
400
|
+
**Pattern Consistency**:
|
|
689
401
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
vehicles: {
|
|
694
|
-
"Cars": { lane_1: 50, lane_2: 30 }, // car → FHWA 2 → Cars
|
|
695
|
-
"Medium Trucks": { lane_1: 10, lane_2: 15 }, // pickup_truck → FHWA 3 → Medium Trucks
|
|
696
|
-
"Heavy Trucks": { lane_1: 5, lane_2: 3 } // bus → FHWA 4 → Heavy Trucks
|
|
697
|
-
},
|
|
698
|
-
detected_classes: {
|
|
699
|
-
"Cars": 80,
|
|
700
|
-
"Medium Trucks": 25,
|
|
701
|
-
"Heavy Trucks": 8
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
```
|
|
402
|
+
- This method duplicates CameraDAO.getVideosByCamera() logic
|
|
403
|
+
- Both should use IDENTICAL query structure
|
|
404
|
+
- Consider refactoring to shared private method in future
|
|
705
405
|
|
|
706
406
|
---
|
|
707
407
|
|
|
708
|
-
##
|
|
408
|
+
## Implementation Order
|
|
709
409
|
|
|
710
|
-
###
|
|
410
|
+
### Phase 1: Simple Methods (No Dependencies)
|
|
711
411
|
|
|
712
|
-
|
|
412
|
+
1. **CameraDAO.getAllWithSearch()** - Standalone search method
|
|
413
|
+
2. **VideoDAO.getVideoIdsByFolderId()** - Simple ID retrieval
|
|
713
414
|
|
|
714
|
-
|
|
715
|
-
// Add to DAOs section
|
|
716
|
-
export { ReportConfigurationDAO } from "./dao/report-configuration/report-configuration.dao";
|
|
717
|
-
|
|
718
|
-
// Add to Interfaces section
|
|
719
|
-
export {
|
|
720
|
-
IReportConfiguration,
|
|
721
|
-
IReportConfigurationInput,
|
|
722
|
-
IReportConfigurationData,
|
|
723
|
-
ICustomClass,
|
|
724
|
-
IConfigurationValidationResult,
|
|
725
|
-
} from "./interfaces/report-configuration/report-configuration.interfaces";
|
|
726
|
-
```
|
|
415
|
+
### Phase 2: Complex Query Methods (JOINs)
|
|
727
416
|
|
|
728
|
-
|
|
417
|
+
3. **CameraDAO.getVideosByCamera()** - JOIN with folders
|
|
418
|
+
4. **VideoDAO.getVideosByCameraIdWithFolder()** - Same as #3, different DAO
|
|
729
419
|
|
|
730
|
-
|
|
420
|
+
### Phase 3: Transaction-Aware Methods
|
|
731
421
|
|
|
732
|
-
|
|
422
|
+
5. **VideoDAO.bulkUpdateCamera()** - Transaction support required
|
|
733
423
|
|
|
734
|
-
|
|
735
|
-
2. Create migration: `YYYYMMDDHHMMSS_create_report_configurations.ts`
|
|
736
|
-
3. Run migration: `npm run migrate:deploy`
|
|
737
|
-
4. Verify seed data exists (1 default configuration)
|
|
424
|
+
---
|
|
738
425
|
|
|
739
|
-
|
|
426
|
+
## Database Indexing Recommendations
|
|
740
427
|
|
|
741
|
-
|
|
742
|
-
6. Implement standard CRUD methods
|
|
743
|
-
7. Implement validation logic
|
|
744
|
-
8. Implement `applyConfiguration()` transformation logic
|
|
745
|
-
9. Test DAO methods in isolation
|
|
428
|
+
### Existing Indexes (Already Present)
|
|
746
429
|
|
|
747
|
-
|
|
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)
|
|
748
434
|
|
|
749
|
-
|
|
750
|
-
11. Modify `VideoMinuteResultDAO.aggregateATRResults()`
|
|
751
|
-
12. Remove `normalizeATRVehicleClass()` calls
|
|
752
|
-
13. Add `fetchConfiguration()` helper
|
|
753
|
-
14. Update method signatures to accept `configurationId`
|
|
435
|
+
### Recommended New Indexes
|
|
754
436
|
|
|
755
|
-
|
|
437
|
+
**Optional - cameras.name (for search optimization)**
|
|
756
438
|
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
18. Test against sample video data
|
|
439
|
+
```sql
|
|
440
|
+
CREATE INDEX idx_cameras_name ON cameras(name);
|
|
441
|
+
```
|
|
761
442
|
|
|
762
|
-
|
|
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)
|
|
763
446
|
|
|
764
|
-
|
|
447
|
+
**High Priority - video.folderId (for cascade operations)**
|
|
765
448
|
|
|
766
|
-
|
|
449
|
+
```sql
|
|
450
|
+
CREATE INDEX idx_video_folder_id ON video(folderId);
|
|
451
|
+
```
|
|
767
452
|
|
|
768
|
-
|
|
453
|
+
- Benefits: Critical for getVideoIdsByFolderId() performance
|
|
454
|
+
- Usage: Heavy usage in cascade operations
|
|
455
|
+
- Decision: **CHECK IF EXISTS** - likely already present
|
|
769
456
|
|
|
770
|
-
**
|
|
457
|
+
**Optional - video.(cameraId, created_at) composite**
|
|
771
458
|
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
459
|
+
```sql
|
|
460
|
+
CREATE INDEX idx_video_camera_created ON video(cameraId, created_at DESC);
|
|
461
|
+
```
|
|
775
462
|
|
|
776
|
-
|
|
463
|
+
- Benefits: Optimizes sorted queries in getVideosByCameraIdWithFolder
|
|
464
|
+
- Decision: SKIP for now (single column index sufficient)
|
|
777
465
|
|
|
778
|
-
|
|
466
|
+
---
|
|
779
467
|
|
|
780
|
-
|
|
468
|
+
## Type Safety & Interfaces
|
|
781
469
|
|
|
782
|
-
|
|
470
|
+
All methods already have proper TypeScript interfaces defined:
|
|
783
471
|
|
|
784
|
-
-
|
|
785
|
-
- Pre-compute transformed results and store in separate JSONB column
|
|
786
|
-
- Add database-level JSONB transformation functions
|
|
472
|
+
**ICamera** - knex-rel/src/interfaces/camera/camera.interfaces.ts
|
|
787
473
|
|
|
788
|
-
|
|
474
|
+
- All fields present and correct
|
|
475
|
+
- No modifications needed
|
|
789
476
|
|
|
790
|
-
**
|
|
477
|
+
**IVideo** - knex-rel/src/interfaces/video/video.interfaces.ts
|
|
791
478
|
|
|
792
|
-
|
|
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
|
|
793
483
|
|
|
794
|
-
-
|
|
795
|
-
- API will return same class names initially (backwards compatible)
|
|
796
|
-
- Users can create custom configurations as needed
|
|
484
|
+
**IDataPaginator<T>** - Used consistently across all methods
|
|
797
485
|
|
|
798
|
-
|
|
486
|
+
- success: boolean
|
|
487
|
+
- data: T[]
|
|
488
|
+
- page: number
|
|
489
|
+
- limit: number
|
|
490
|
+
- count: number
|
|
491
|
+
- totalCount: number
|
|
492
|
+
- totalPages: number
|
|
799
493
|
|
|
800
|
-
|
|
494
|
+
---
|
|
801
495
|
|
|
802
|
-
|
|
496
|
+
## Error Handling Patterns
|
|
803
497
|
|
|
804
|
-
|
|
498
|
+
### Pattern 1: Search Methods (getAllWithSearch)
|
|
805
499
|
|
|
806
|
-
|
|
500
|
+
```typescript
|
|
501
|
+
// No try/catch in DAO
|
|
502
|
+
// Let Knex errors propagate to service
|
|
503
|
+
const result = await query...
|
|
504
|
+
return result;
|
|
505
|
+
```
|
|
807
506
|
|
|
808
|
-
###
|
|
507
|
+
### Pattern 2: Transaction Methods (bulkUpdateCamera)
|
|
809
508
|
|
|
810
509
|
```typescript
|
|
811
|
-
//
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
it("should reject custom class names > 30 chars");
|
|
817
|
-
it("should accept valid configurations");
|
|
818
|
-
});
|
|
819
|
-
|
|
820
|
-
// Test transformation
|
|
821
|
-
describe("ReportConfigurationDAO.applyConfiguration", () => {
|
|
822
|
-
it("should map detection labels to FHWA classes");
|
|
823
|
-
it("should map FHWA classes to custom classes");
|
|
824
|
-
it("should exclude unmapped FHWA classes");
|
|
825
|
-
it("should handle multi-mapped detection labels (single_unit_truck)");
|
|
826
|
-
});
|
|
827
|
-
|
|
828
|
-
// Test deletion validation
|
|
829
|
-
describe("ReportConfigurationDAO.deleteWithValidation", () => {
|
|
830
|
-
it("should prevent deletion of last configuration");
|
|
831
|
-
it("should allow deletion when > 1 configuration exists");
|
|
832
|
-
});
|
|
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;
|
|
833
515
|
```
|
|
834
516
|
|
|
835
|
-
###
|
|
517
|
+
### Pattern 3: Simple Queries (getVideoIdsByFolderId)
|
|
836
518
|
|
|
837
519
|
```typescript
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
it("should handle unmapped vehicle classes correctly");
|
|
843
|
-
});
|
|
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);
|
|
844
524
|
```
|
|
845
525
|
|
|
846
526
|
---
|
|
847
527
|
|
|
848
|
-
##
|
|
528
|
+
## Code Pattern Examples
|
|
849
529
|
|
|
850
|
-
###
|
|
530
|
+
### Example 1: getAllWithSearch() Implementation
|
|
851
531
|
|
|
852
|
-
|
|
532
|
+
```typescript
|
|
533
|
+
async getAllWithSearch(page: number, limit: number, name?: string): Promise<IDataPaginator<ICamera>> {
|
|
534
|
+
const offset = (page - 1) * limit;
|
|
853
535
|
|
|
854
|
-
|
|
855
|
-
- Index on `is_default` for quick default retrieval
|
|
856
|
-
- Consider in-memory caching (singleton) if configurations queried frequently
|
|
536
|
+
let query = this._knex("cameras");
|
|
857
537
|
|
|
858
|
-
|
|
538
|
+
// Apply search filter if name provided
|
|
539
|
+
if (name && name.trim().length > 0) {
|
|
540
|
+
query = query.where('name', 'ilike', `%${name.trim()}%`);
|
|
541
|
+
}
|
|
859
542
|
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
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');
|
|
863
546
|
|
|
864
|
-
|
|
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
|
|
865
560
|
|
|
866
561
|
```typescript
|
|
867
|
-
|
|
868
|
-
|
|
562
|
+
async bulkUpdateCamera(videoIds: number[], cameraId: number | null, trx?: Knex.Transaction): Promise<number> {
|
|
563
|
+
if (!videoIds || videoIds.length === 0) {
|
|
564
|
+
return 0;
|
|
565
|
+
}
|
|
869
566
|
|
|
870
|
-
|
|
871
|
-
return this.instance.get(id);
|
|
872
|
-
}
|
|
567
|
+
const query = trx || this._knex;
|
|
873
568
|
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
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');
|
|
877
576
|
|
|
878
|
-
|
|
879
|
-
this.instance.clear();
|
|
880
|
-
}
|
|
577
|
+
return result.length;
|
|
881
578
|
}
|
|
882
579
|
```
|
|
883
580
|
|
|
884
|
-
|
|
581
|
+
### Example 3: getVideosByCamera() Implementation
|
|
885
582
|
|
|
886
|
-
|
|
583
|
+
```typescript
|
|
584
|
+
async getVideosByCamera(cameraId: number, page: number, limit: number): Promise<IDataPaginator<IVideo>> {
|
|
585
|
+
const offset = (page - 1) * limit;
|
|
887
586
|
|
|
888
|
-
|
|
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);
|
|
889
591
|
|
|
890
|
-
|
|
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');
|
|
891
595
|
|
|
892
|
-
|
|
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
|
+
```
|
|
893
607
|
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
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);
|
|
904
618
|
}
|
|
905
619
|
```
|
|
906
620
|
|
|
907
|
-
|
|
621
|
+
### Example 5: getVideosByCameraIdWithFolder() Implementation
|
|
908
622
|
|
|
909
|
-
|
|
623
|
+
```typescript
|
|
624
|
+
async getVideosByCameraIdWithFolder(cameraId: number, page: number, limit: number): Promise<IDataPaginator<IVideo>> {
|
|
625
|
+
const offset = (page - 1) * limit;
|
|
910
626
|
|
|
911
|
-
|
|
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);
|
|
912
631
|
|
|
913
|
-
|
|
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');
|
|
914
635
|
|
|
915
|
-
|
|
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
|
+
```
|
|
916
647
|
|
|
917
|
-
|
|
648
|
+
---
|
|
918
649
|
|
|
919
|
-
|
|
650
|
+
## Migration Requirements
|
|
920
651
|
|
|
921
|
-
|
|
652
|
+
**No new migrations needed** - All schema changes already exist:
|
|
922
653
|
|
|
923
|
-
|
|
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
|
|
924
658
|
|
|
925
|
-
**
|
|
659
|
+
**Optional migration for performance**:
|
|
660
|
+
|
|
661
|
+
- Check if video.folderId index exists (likely already present)
|
|
926
662
|
|
|
927
663
|
---
|
|
928
664
|
|
|
929
|
-
##
|
|
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()**
|
|
930
694
|
|
|
931
|
-
|
|
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)
|
|
932
699
|
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
-
|
|
936
|
-
-
|
|
937
|
-
-
|
|
938
|
-
-
|
|
939
|
-
-
|
|
940
|
-
- [x] Integration strategy with VideoMinuteResultDAO is clear
|
|
941
|
-
- [x] Export updates are documented
|
|
942
|
-
- [x] Implementation order is logical
|
|
943
|
-
- [x] Risks are identified and mitigated
|
|
944
|
-
- [x] Testing strategy is comprehensive
|
|
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
|
|
945
707
|
|
|
946
708
|
---
|
|
947
709
|
|
|
948
|
-
##
|
|
710
|
+
## Implementation Checklist
|
|
949
711
|
|
|
950
|
-
###
|
|
712
|
+
### Pre-Implementation
|
|
951
713
|
|
|
952
|
-
-
|
|
953
|
-
-
|
|
954
|
-
-
|
|
955
|
-
- Foreign keys: N/A (system-wide table)
|
|
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
|
|
956
717
|
|
|
957
|
-
###
|
|
718
|
+
### Implementation Phase
|
|
958
719
|
|
|
959
|
-
-
|
|
960
|
-
-
|
|
961
|
-
-
|
|
962
|
-
-
|
|
720
|
+
- [ ] Implement CameraDAO.getAllWithSearch()
|
|
721
|
+
- [ ] Implement VideoDAO.getVideoIdsByFolderId()
|
|
722
|
+
- [ ] Implement CameraDAO.getVideosByCamera()
|
|
723
|
+
- [ ] Implement VideoDAO.getVideosByCameraIdWithFolder()
|
|
724
|
+
- [ ] Implement VideoDAO.bulkUpdateCamera()
|
|
963
725
|
|
|
964
|
-
###
|
|
726
|
+
### Testing Phase
|
|
965
727
|
|
|
966
|
-
-
|
|
967
|
-
-
|
|
968
|
-
-
|
|
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)
|
|
969
732
|
|
|
970
|
-
###
|
|
733
|
+
### Integration Phase
|
|
971
734
|
|
|
972
|
-
-
|
|
973
|
-
-
|
|
974
|
-
-
|
|
975
|
-
-
|
|
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
|
|
976
741
|
|
|
977
742
|
---
|
|
978
743
|
|
|
979
|
-
##
|
|
744
|
+
## Risk Assessment
|
|
980
745
|
|
|
981
|
-
|
|
746
|
+
### Low Risk
|
|
982
747
|
|
|
983
|
-
|
|
748
|
+
- getAllWithSearch() - Simple extension of getAll()
|
|
749
|
+
- getVideoIdsByFolderId() - Straightforward SELECT query
|
|
984
750
|
|
|
985
|
-
|
|
986
|
-
2. Approve DETECTION_LABEL_TO_FHWA mapping accuracy
|
|
987
|
-
3. Confirm migration timestamp format
|
|
988
|
-
4. Hand off to knex-code-agent for execution
|
|
751
|
+
### Medium Risk
|
|
989
752
|
|
|
990
|
-
|
|
753
|
+
- getVideosByCamera() - Requires correct JOIN syntax
|
|
754
|
+
- getVideosByCameraIdWithFolder() - Same as above
|
|
991
755
|
|
|
992
|
-
|
|
993
|
-
2. Test each phase before proceeding
|
|
994
|
-
3. Report any conflicts or issues discovered during implementation
|
|
995
|
-
4. Request clarification if detection labels don't match expectations
|
|
756
|
+
### High Risk
|
|
996
757
|
|
|
997
|
-
|
|
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
|
|
998
762
|
|
|
999
|
-
|
|
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
|
|
1000
771
|
|
|
1001
772
|
---
|
|
1002
773
|
|
|
1003
|
-
##
|
|
774
|
+
## Summary
|
|
1004
775
|
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
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
|
+
---
|
|
1020
791
|
|
|
1021
|
-
|
|
792
|
+
## Files to Modify
|
|
1022
793
|
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
-
|
|
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)`
|
|
1026
804
|
|
|
1027
805
|
---
|
|
1028
806
|
|
|
1029
|
-
##
|
|
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 ✅
|
|
1030
826
|
|
|
1031
|
-
-
|
|
1032
|
-
-
|
|
1033
|
-
-
|
|
1034
|
-
-
|
|
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
|