@trafficgroup/knex-rel 0.1.1 → 0.1.3
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/cameras_analysis.md +199 -0
- package/dist/dao/camera/camera.dao.d.ts +2 -0
- package/dist/dao/camera/camera.dao.js +42 -0
- package/dist/dao/camera/camera.dao.js.map +1 -1
- package/dist/dao/folder/folder.dao.js +6 -3
- package/dist/dao/folder/folder.dao.js.map +1 -1
- package/dist/dao/video/video.dao.d.ts +3 -0
- package/dist/dao/video/video.dao.js +39 -0
- package/dist/dao/video/video.dao.js.map +1 -1
- package/folder_cameraid_analysis.md +167 -0
- package/migrations/20250924000000_camera_name_search_index.ts +22 -0
- package/package.json +1 -1
- package/plan.md +252 -93
- package/src/dao/camera/camera.dao.ts +55 -0
- package/src/dao/folder/folder.dao.ts +18 -3
- package/src/dao/video/video.dao.ts +47 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# Database Schema Analysis - Cameras Table Implementation
|
|
2
|
+
|
|
3
|
+
## Current Schema Analysis
|
|
4
|
+
|
|
5
|
+
### 1. Videos Table (Current Structure)
|
|
6
|
+
|
|
7
|
+
- **Table Name**: `video`
|
|
8
|
+
- **Primary Key**: `id` (auto-increment number)
|
|
9
|
+
- **UUID**: `uuid` (unique, not null) - for external references
|
|
10
|
+
- **Foreign Keys**:
|
|
11
|
+
- `folderId`: references `folders.id` (CASCADE delete)
|
|
12
|
+
- `cameraId`: references `cameras.id` (SET NULL on delete) ✅ **ALREADY EXISTS**
|
|
13
|
+
- `annotationSourceId`: self-reference to `video.id` (SET NULL)
|
|
14
|
+
- **Key Fields**:
|
|
15
|
+
- `videoLocation`: S3 path/URL for video file
|
|
16
|
+
- `videoRate`: frame rate (FPS)
|
|
17
|
+
- `videoType`: enum ['TMC', 'ATR', 'JUNCTION', 'ROUNDABOUT', 'PATHWAY']
|
|
18
|
+
- `status`: enum ['QUEUED', 'PROCESSING', 'COMPLETED', 'FAILED', 'PENDING']
|
|
19
|
+
- `metadata`: JSONB for lane annotations
|
|
20
|
+
- `results`: JSONB for processing results
|
|
21
|
+
- **Timestamps**: `created_at`, `updated_at`
|
|
22
|
+
- **Indexes**:
|
|
23
|
+
- `uuid` (unique)
|
|
24
|
+
- `cameraId`
|
|
25
|
+
- `annotationSourceId`
|
|
26
|
+
- Composite: `(folderId, videoType, status)`
|
|
27
|
+
|
|
28
|
+
### 2. Folders Table (Current Structure)
|
|
29
|
+
|
|
30
|
+
- **Table Name**: `folders`
|
|
31
|
+
- **Primary Key**: `id` (auto-increment number)
|
|
32
|
+
- **UUID**: `uuid` (unique, not null) - for external references
|
|
33
|
+
- **Foreign Keys**:
|
|
34
|
+
- `createdBy`: references `user.id` (CASCADE delete)
|
|
35
|
+
- `studyId`: references `study.id` (CASCADE delete)
|
|
36
|
+
- `cameraId`: references `cameras.id` (SET NULL on delete) ✅ **ALREADY EXISTS**
|
|
37
|
+
- **Key Fields**:
|
|
38
|
+
- `name`: folder name
|
|
39
|
+
- `status`: enum ['UPLOADING', 'COMPLETE']
|
|
40
|
+
- **Timestamps**: `created_at`, `updated_at`
|
|
41
|
+
- **Indexes**:
|
|
42
|
+
- `uuid` (unique)
|
|
43
|
+
- `cameraId`
|
|
44
|
+
|
|
45
|
+
### 3. Cameras Table (Current Structure) ✅ **ALREADY IMPLEMENTED**
|
|
46
|
+
|
|
47
|
+
- **Table Name**: `cameras`
|
|
48
|
+
- **Primary Key**: `id` (auto-increment number)
|
|
49
|
+
- **UUID**: `uuid` (unique, not null) - for external references
|
|
50
|
+
- **Fields**:
|
|
51
|
+
- `name`: string(100), not null
|
|
52
|
+
- `longitude`: decimal(10,7), not null
|
|
53
|
+
- `latitude`: decimal(10,7), not null
|
|
54
|
+
- **Timestamps**: `created_at`, `updated_at`
|
|
55
|
+
- **Indexes**:
|
|
56
|
+
- `uuid` (unique)
|
|
57
|
+
- Composite: `(longitude, latitude)` for geospatial queries
|
|
58
|
+
|
|
59
|
+
## Relationship Analysis
|
|
60
|
+
|
|
61
|
+
### Current Relationships ✅ **FULLY IMPLEMENTED**
|
|
62
|
+
|
|
63
|
+
1. **Study → Folders** (One-to-Many)
|
|
64
|
+
- Foreign key: `folders.studyId → study.id`
|
|
65
|
+
- Cascade delete: deleting study removes all folders
|
|
66
|
+
|
|
67
|
+
2. **Folders → Videos** (One-to-Many)
|
|
68
|
+
- Foreign key: `video.folderId → folders.id`
|
|
69
|
+
- Cascade delete: deleting folder removes all videos
|
|
70
|
+
|
|
71
|
+
3. **Cameras → Folders** (One-to-Many) ✅
|
|
72
|
+
- Foreign key: `folders.cameraId → cameras.id`
|
|
73
|
+
- SET NULL on delete: deleting camera keeps folders but removes reference
|
|
74
|
+
|
|
75
|
+
4. **Cameras → Videos** (One-to-Many) ✅
|
|
76
|
+
- Foreign key: `video.cameraId → cameras.id`
|
|
77
|
+
- SET NULL on delete: deleting camera keeps videos but removes reference
|
|
78
|
+
|
|
79
|
+
5. **Videos → Videos** (Self-Reference for Templates)
|
|
80
|
+
- Foreign key: `video.annotationSourceId → video.id`
|
|
81
|
+
- Used for lane annotation templates
|
|
82
|
+
|
|
83
|
+
## DAO Implementation Status
|
|
84
|
+
|
|
85
|
+
### CameraDAO ✅ **FULLY IMPLEMENTED**
|
|
86
|
+
|
|
87
|
+
- **File**: `src/dao/camera/camera.dao.ts`
|
|
88
|
+
- **Standard Methods**: create, getById, getByUuid, update, delete, getAll ✅
|
|
89
|
+
- **Custom Methods**:
|
|
90
|
+
- `getByName(name: string)`: Find camera by name ✅
|
|
91
|
+
- `getCamerasNearCoordinates(lng, lat, radius)`: Geospatial search using PostGIS ✅
|
|
92
|
+
- **Performance Features**: Uses PostGIS ST_DWithin for geographic queries ✅
|
|
93
|
+
|
|
94
|
+
### VideoDAO - Camera Integration ✅ **ALREADY INTEGRATED**
|
|
95
|
+
|
|
96
|
+
- **JOIN Queries**: Already includes folder data in getById/getByUuid ✅
|
|
97
|
+
- **Camera Support**: Interface includes `cameraId?: number` and `camera?: ICamera` ✅
|
|
98
|
+
- **Note**: DAO doesn't currently JOIN camera data, but interface supports it
|
|
99
|
+
|
|
100
|
+
### FolderDAO - Camera Integration ✅ **ALREADY INTEGRATED**
|
|
101
|
+
|
|
102
|
+
- **JOIN Queries**: Already includes study data in getById/getByUuid ✅
|
|
103
|
+
- **Camera Support**: Interface includes `cameraId?: number` and `camera?: ICamera` ✅
|
|
104
|
+
- **Note**: DAO doesn't currently JOIN camera data, but interface supports it
|
|
105
|
+
|
|
106
|
+
## Migration Status ✅ **COMPLETE**
|
|
107
|
+
|
|
108
|
+
**Migration**: `20250911000000_migration.ts` ✅ **ALREADY DEPLOYED**
|
|
109
|
+
|
|
110
|
+
- Creates `cameras` table with proper schema ✅
|
|
111
|
+
- Adds `cameraId` to `video` table with foreign key ✅
|
|
112
|
+
- Adds `cameraId` to `folders` table with foreign key ✅
|
|
113
|
+
- Proper indexes for performance ✅
|
|
114
|
+
- Safe rollback implementation ✅
|
|
115
|
+
|
|
116
|
+
## Interface Implementation Status ✅ **COMPLETE**
|
|
117
|
+
|
|
118
|
+
### ICamera ✅ **FULLY IMPLEMENTED**
|
|
119
|
+
|
|
120
|
+
- **File**: `src/interfaces/camera/camera.interfaces.ts`
|
|
121
|
+
- **Fields**: id, uuid, name, longitude, latitude, created_at, updated_at ✅
|
|
122
|
+
- **Export**: Properly exported in `src/index.ts` ✅
|
|
123
|
+
|
|
124
|
+
### IVideo ✅ **CAMERA INTEGRATION COMPLETE**
|
|
125
|
+
|
|
126
|
+
- **Camera Fields**: `cameraId?: number`, `camera?: ICamera` ✅
|
|
127
|
+
- **Relationships**: Imports ICamera interface ✅
|
|
128
|
+
|
|
129
|
+
### IFolder ✅ **CAMERA INTEGRATION COMPLETE**
|
|
130
|
+
|
|
131
|
+
- **Camera Fields**: `cameraId?: number`, `camera?: ICamera` ✅
|
|
132
|
+
- **Relationships**: Imports ICamera interface ✅
|
|
133
|
+
|
|
134
|
+
## Performance Considerations ✅ **OPTIMIZED**
|
|
135
|
+
|
|
136
|
+
### Database Indexes ✅
|
|
137
|
+
|
|
138
|
+
- `cameras.uuid` (unique) - for UUID lookups ✅
|
|
139
|
+
- `cameras(longitude, latitude)` - for geospatial queries ✅
|
|
140
|
+
- `video.cameraId` - for camera-to-videos lookups ✅
|
|
141
|
+
- `folders.cameraId` - for camera-to-folders lookups ✅
|
|
142
|
+
|
|
143
|
+
### Query Optimization ✅
|
|
144
|
+
|
|
145
|
+
- **VideoDAO**: Uses JOINs to eliminate N+1 queries (folder data) ✅
|
|
146
|
+
- **FolderDAO**: Uses JOINs to eliminate N+1 queries (study data) ✅
|
|
147
|
+
- **CameraDAO**: Geospatial queries use PostGIS for performance ✅
|
|
148
|
+
|
|
149
|
+
### Missing JOIN Optimizations (Minor Enhancement Opportunity)
|
|
150
|
+
|
|
151
|
+
- VideoDAO could JOIN camera data in getById/getByUuid queries
|
|
152
|
+
- FolderDAO could JOIN camera data in getById/getByUuid queries
|
|
153
|
+
- These would eliminate additional queries when camera data is needed
|
|
154
|
+
|
|
155
|
+
## System Architecture Compliance ✅
|
|
156
|
+
|
|
157
|
+
### Pattern Compliance ✅
|
|
158
|
+
|
|
159
|
+
- **Naming**: Follows `entityUuid` pattern for external references ✅
|
|
160
|
+
- **Timestamps**: Uses `created_at`/`updated_at` consistently ✅
|
|
161
|
+
- **Primary Keys**: Auto-increment `id` for internal use ✅
|
|
162
|
+
- **Foreign Keys**: Proper referential integrity with appropriate cascade rules ✅
|
|
163
|
+
|
|
164
|
+
### TypeScript Integration ✅
|
|
165
|
+
|
|
166
|
+
- **Type Safety**: Full TypeScript interfaces with proper typing ✅
|
|
167
|
+
- **Optional Fields**: Camera relationships marked as optional ✅
|
|
168
|
+
- **Export Structure**: All entities properly exported from main index ✅
|
|
169
|
+
|
|
170
|
+
## Current Implementation Status: ✅ **COMPLETE**
|
|
171
|
+
|
|
172
|
+
### What's Already Working:
|
|
173
|
+
|
|
174
|
+
1. **Database Schema**: Cameras table exists with proper relationships ✅
|
|
175
|
+
2. **Migrations**: All database changes deployed ✅
|
|
176
|
+
3. **DAOs**: CameraDAO fully implemented with geospatial features ✅
|
|
177
|
+
4. **Interfaces**: All TypeScript interfaces support camera relationships ✅
|
|
178
|
+
5. **Exports**: All components properly exported ✅
|
|
179
|
+
6. **Relationships**: Many-to-one relationships correctly implemented ✅
|
|
180
|
+
7. **Performance**: Proper indexing for UUID and geospatial queries ✅
|
|
181
|
+
|
|
182
|
+
### Minor Optimization Opportunities:
|
|
183
|
+
|
|
184
|
+
1. **JOIN Queries**: VideoDAO and FolderDAO could include camera data in their JOIN queries
|
|
185
|
+
2. **Query Performance**: Consider adding camera data to reduce roundtrips
|
|
186
|
+
|
|
187
|
+
## Conclusion
|
|
188
|
+
|
|
189
|
+
The cameras table implementation is **COMPLETE and FULLY FUNCTIONAL**. The database schema, migrations, DAOs, and interfaces are all properly implemented following the project's architectural patterns. The system supports:
|
|
190
|
+
|
|
191
|
+
- ✅ Camera management with geographic coordinates
|
|
192
|
+
- ✅ Many-to-one relationships between cameras and folders/videos
|
|
193
|
+
- ✅ UUID-based external references
|
|
194
|
+
- ✅ Geospatial queries for location-based camera searches
|
|
195
|
+
- ✅ Proper foreign key constraints with appropriate cascade behavior
|
|
196
|
+
- ✅ Full TypeScript type safety
|
|
197
|
+
- ✅ Performance optimization through proper indexing
|
|
198
|
+
|
|
199
|
+
**No database changes are required** - the system is ready for camera-based video processing workflows.
|
|
@@ -10,4 +10,6 @@ export declare class CameraDAO implements IBaseDAO<ICamera> {
|
|
|
10
10
|
getAll(page: number, limit: number): Promise<IDataPaginator<ICamera>>;
|
|
11
11
|
getByName(name: string): Promise<ICamera | null>;
|
|
12
12
|
getCamerasNearCoordinates(longitude: number, latitude: number, radiusKm?: number): Promise<ICamera[]>;
|
|
13
|
+
getAllWithSearch(page: number, limit: number, name?: string): Promise<IDataPaginator<ICamera>>;
|
|
14
|
+
getVideosByCamera(cameraId: number, page: number, limit: number): Promise<IDataPaginator<any>>;
|
|
13
15
|
}
|
|
@@ -88,6 +88,48 @@ class CameraDAO {
|
|
|
88
88
|
return cameras;
|
|
89
89
|
});
|
|
90
90
|
}
|
|
91
|
+
getAllWithSearch(page, limit, name) {
|
|
92
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
93
|
+
const offset = (page - 1) * limit;
|
|
94
|
+
const query = this._knex("cameras");
|
|
95
|
+
if (name && name.trim() !== "") {
|
|
96
|
+
query.where("name", "ilike", `%${name.trim()}%`);
|
|
97
|
+
}
|
|
98
|
+
const [countResult] = yield query.clone().count("* as count");
|
|
99
|
+
const totalCount = +countResult.count;
|
|
100
|
+
const cameras = yield query.clone().limit(limit).offset(offset);
|
|
101
|
+
return {
|
|
102
|
+
success: true,
|
|
103
|
+
data: cameras,
|
|
104
|
+
page,
|
|
105
|
+
limit,
|
|
106
|
+
count: cameras.length,
|
|
107
|
+
totalCount,
|
|
108
|
+
totalPages: Math.ceil(totalCount / limit),
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
getVideosByCamera(cameraId, page, limit) {
|
|
113
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
114
|
+
const offset = (page - 1) * limit;
|
|
115
|
+
const query = this._knex("videos as v")
|
|
116
|
+
.innerJoin("folders as f", "v.folderId", "f.id")
|
|
117
|
+
.where("v.cameraId", cameraId)
|
|
118
|
+
.select("v.*", this._knex.raw("to_jsonb(f.*) as folder"));
|
|
119
|
+
const [countResult] = yield query.clone().clearSelect().count("* as count");
|
|
120
|
+
const totalCount = +countResult.count;
|
|
121
|
+
const videos = yield query.clone().limit(limit).offset(offset);
|
|
122
|
+
return {
|
|
123
|
+
success: true,
|
|
124
|
+
data: videos,
|
|
125
|
+
page,
|
|
126
|
+
limit,
|
|
127
|
+
count: videos.length,
|
|
128
|
+
totalCount,
|
|
129
|
+
totalPages: Math.ceil(totalCount / limit),
|
|
130
|
+
};
|
|
131
|
+
});
|
|
132
|
+
}
|
|
91
133
|
}
|
|
92
134
|
exports.CameraDAO = CameraDAO;
|
|
93
135
|
//# sourceMappingURL=camera.dao.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"camera.dao.js","sourceRoot":"","sources":["../../../src/dao/camera/camera.dao.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAGA,0EAA+C;AAE/C,MAAa,SAAS;IAAtB;QACU,UAAK,GAAyB,wBAAW,CAAC,aAAa,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"camera.dao.js","sourceRoot":"","sources":["../../../src/dao/camera/camera.dao.ts"],"names":[],"mappings":"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"}
|
|
@@ -30,7 +30,8 @@ class FolderDAO {
|
|
|
30
30
|
return __awaiter(this, void 0, void 0, function* () {
|
|
31
31
|
const folder = yield this._knex("folders as f")
|
|
32
32
|
.innerJoin("study as s", "f.studyId", "s.id")
|
|
33
|
-
.
|
|
33
|
+
.leftJoin("cameras as c", "f.cameraId", "c.id")
|
|
34
|
+
.select("f.*", this._knex.raw("to_jsonb(s.*) as study"), this._knex.raw("to_jsonb(c.*) as camera"))
|
|
34
35
|
.where("f.id", id)
|
|
35
36
|
.first();
|
|
36
37
|
return folder || null;
|
|
@@ -40,7 +41,8 @@ class FolderDAO {
|
|
|
40
41
|
return __awaiter(this, void 0, void 0, function* () {
|
|
41
42
|
const folder = yield this._knex("folders as f")
|
|
42
43
|
.innerJoin("study as s", "f.studyId", "s.id")
|
|
43
|
-
.
|
|
44
|
+
.leftJoin("cameras as c", "f.cameraId", "c.id")
|
|
45
|
+
.select("f.*", this._knex.raw("to_jsonb(s.*) as study"), this._knex.raw("to_jsonb(c.*) as camera"))
|
|
44
46
|
.where("f.uuid", uuid)
|
|
45
47
|
.first();
|
|
46
48
|
return folder || null;
|
|
@@ -66,7 +68,8 @@ class FolderDAO {
|
|
|
66
68
|
const offset = (page - 1) * limit;
|
|
67
69
|
const query = this._knex("folders as f")
|
|
68
70
|
.innerJoin("study as s", "f.studyId", "s.id")
|
|
69
|
-
.
|
|
71
|
+
.leftJoin("cameras as c", "f.cameraId", "c.id")
|
|
72
|
+
.select("f.*", this._knex.raw("to_jsonb(s.*) as study"), this._knex.raw("to_jsonb(c.*) as camera"));
|
|
70
73
|
if (studyId !== undefined && studyId !== null) {
|
|
71
74
|
query.where("f.studyId", studyId);
|
|
72
75
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"folder.dao.js","sourceRoot":"","sources":["../../../src/dao/folder/folder.dao.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAGA,0EAA+C;AAE/C,MAAa,SAAS;IAAtB;QACU,UAAK,GAAyB,wBAAW,CAAC,aAAa,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"folder.dao.js","sourceRoot":"","sources":["../../../src/dao/folder/folder.dao.ts"],"names":[],"mappings":"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"}
|
|
@@ -30,4 +30,7 @@ export declare class VideoDAO implements IBaseDAO<IVideo> {
|
|
|
30
30
|
* Suitable for use as lane annotation templates
|
|
31
31
|
*/
|
|
32
32
|
getTemplateVideos(folderId: number, videoType: string): Promise<IVideo[]>;
|
|
33
|
+
getVideoIdsByFolderId(folderId: number): Promise<number[]>;
|
|
34
|
+
bulkUpdateCamera(videoIds: number[], cameraId: number | null, trx?: Knex.Transaction): Promise<number>;
|
|
35
|
+
getVideosByCameraIdWithFolder(cameraId: number, page: number, limit: number): Promise<IDataPaginator<IVideo>>;
|
|
33
36
|
}
|
|
@@ -212,6 +212,45 @@ class VideoDAO {
|
|
|
212
212
|
}
|
|
213
213
|
});
|
|
214
214
|
}
|
|
215
|
+
getVideoIdsByFolderId(folderId) {
|
|
216
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
217
|
+
const videos = yield this._knex("video")
|
|
218
|
+
.where("folderId", folderId)
|
|
219
|
+
.select("id");
|
|
220
|
+
return videos.map((video) => video.id);
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
bulkUpdateCamera(videoIds, cameraId, trx) {
|
|
224
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
225
|
+
const knexInstance = trx || this._knex;
|
|
226
|
+
const result = yield knexInstance("video").whereIn("id", videoIds).update({
|
|
227
|
+
cameraId: cameraId,
|
|
228
|
+
updated_at: knexInstance.fn.now(),
|
|
229
|
+
});
|
|
230
|
+
return result;
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
getVideosByCameraIdWithFolder(cameraId, page, limit) {
|
|
234
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
235
|
+
const offset = (page - 1) * limit;
|
|
236
|
+
const query = this._knex("video as v")
|
|
237
|
+
.innerJoin("folders as f", "v.folderId", "f.id")
|
|
238
|
+
.where("v.cameraId", cameraId)
|
|
239
|
+
.select("v.*", this._knex.raw("to_jsonb(f.*) as folder"));
|
|
240
|
+
const [countResult] = yield query.clone().clearSelect().count("* as count");
|
|
241
|
+
const totalCount = +countResult.count;
|
|
242
|
+
const videos = yield query.clone().limit(limit).offset(offset);
|
|
243
|
+
return {
|
|
244
|
+
success: true,
|
|
245
|
+
data: videos,
|
|
246
|
+
page,
|
|
247
|
+
limit,
|
|
248
|
+
count: videos.length,
|
|
249
|
+
totalCount,
|
|
250
|
+
totalPages: Math.ceil(totalCount / limit),
|
|
251
|
+
};
|
|
252
|
+
});
|
|
253
|
+
}
|
|
215
254
|
}
|
|
216
255
|
exports.VideoDAO = VideoDAO;
|
|
217
256
|
//# sourceMappingURL=video.dao.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"video.dao.js","sourceRoot":"","sources":["../../../src/dao/video/video.dao.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAGA,0EAA+C;AAE/C,MAAa,QAAQ;IAArB;QACU,UAAK,GAAyB,wBAAW,CAAC,aAAa,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"video.dao.js","sourceRoot":"","sources":["../../../src/dao/video/video.dao.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# Folder Table CameraId Analysis
|
|
2
|
+
|
|
3
|
+
## Issue Summary
|
|
4
|
+
|
|
5
|
+
The `cameraId` column in the `folders` table is being saved as `null` despite being defined as optional in the schema and interface.
|
|
6
|
+
|
|
7
|
+
## Database Schema Analysis
|
|
8
|
+
|
|
9
|
+
### Current Schema (from migrations)
|
|
10
|
+
|
|
11
|
+
#### Initial Folder Table Creation (20250717161310_migration.ts)
|
|
12
|
+
|
|
13
|
+
```sql
|
|
14
|
+
CREATE TABLE folders (
|
|
15
|
+
id SERIAL PRIMARY KEY,
|
|
16
|
+
name VARCHAR NOT NULL,
|
|
17
|
+
createdBy INTEGER NOT NULL REFERENCES user(id) ON DELETE CASCADE,
|
|
18
|
+
status ENUM('UPLOADING', 'COMPLETE') NOT NULL DEFAULT 'UPLOADING',
|
|
19
|
+
studyId INTEGER NOT NULL REFERENCES study(id) ON DELETE CASCADE,
|
|
20
|
+
created_at TIMESTAMP,
|
|
21
|
+
updated_at TIMESTAMP
|
|
22
|
+
);
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
#### Camera Integration (20250911000000_migration.ts)
|
|
26
|
+
|
|
27
|
+
```sql
|
|
28
|
+
-- Added cameraId column to folders table
|
|
29
|
+
ALTER TABLE folders ADD COLUMN cameraId INTEGER NULLABLE
|
|
30
|
+
REFERENCES cameras(id) ON DELETE SET NULL;
|
|
31
|
+
CREATE INDEX ON folders (cameraId);
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Current Interface Definition
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
export interface IFolder {
|
|
38
|
+
id: number;
|
|
39
|
+
uuid: string;
|
|
40
|
+
name: string;
|
|
41
|
+
createdBy: number; // user.id
|
|
42
|
+
status: "UPLOADING" | "COMPLETE";
|
|
43
|
+
studyId: number; // study.id
|
|
44
|
+
cameraId?: number; // camera.id - OPTIONAL field
|
|
45
|
+
created_at: string;
|
|
46
|
+
updated_at: string;
|
|
47
|
+
study?: IStudy;
|
|
48
|
+
camera?: ICamera;
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## DAO Implementation Analysis
|
|
53
|
+
|
|
54
|
+
### Current FolderDAO Issues
|
|
55
|
+
|
|
56
|
+
1. **No Camera Relationship Joins**: The DAO queries join with `study` table but don't include `camera` joins
|
|
57
|
+
2. **Missing Camera Data Population**: Queries don't populate the optional `camera` field
|
|
58
|
+
3. **Basic CRUD Operations**: The `create()` method simply inserts the provided data without validation
|
|
59
|
+
|
|
60
|
+
### Key Methods Analysis
|
|
61
|
+
|
|
62
|
+
#### Create Method
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
async create(item: IFolder): Promise<IFolder> {
|
|
66
|
+
const [createdFolder] = await this._knex("folders")
|
|
67
|
+
.insert(item)
|
|
68
|
+
.returning("*");
|
|
69
|
+
return createdFolder;
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
- **Issue**: This will insert whatever `cameraId` is provided in the `item` parameter
|
|
74
|
+
- **Behavior**: If `cameraId` is undefined or not provided, it will be stored as `NULL` (which is valid)
|
|
75
|
+
|
|
76
|
+
#### Query Methods (getById, getByUuid, getAll)
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
// Current pattern - only joins with study
|
|
80
|
+
const folder = await this._knex("folders as f")
|
|
81
|
+
.innerJoin("study as s", "f.studyId", "s.id")
|
|
82
|
+
.select("f.*", this._knex.raw("to_jsonb(s.*) as study"))
|
|
83
|
+
.where("f.id", id)
|
|
84
|
+
.first();
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
- **Missing**: No LEFT JOIN with cameras table to populate camera data
|
|
88
|
+
- **Impact**: Even if `cameraId` has a valid value, the `camera` object won't be populated
|
|
89
|
+
|
|
90
|
+
## Root Cause Analysis
|
|
91
|
+
|
|
92
|
+
### Why CameraId is Null
|
|
93
|
+
|
|
94
|
+
1. **Optional Field Behavior**: Since `cameraId` is optional (`cameraId?: number`), if not explicitly provided in create requests, it defaults to `null`
|
|
95
|
+
|
|
96
|
+
2. **No Default Value**: Unlike other fields, there's no database-level default value for `cameraId`
|
|
97
|
+
|
|
98
|
+
3. **Application Logic Gap**: No validation or business logic to ensure `cameraId` is populated when appropriate
|
|
99
|
+
|
|
100
|
+
### Database Schema is Correct
|
|
101
|
+
|
|
102
|
+
The database schema properly supports `cameraId`:
|
|
103
|
+
|
|
104
|
+
- ✅ Column exists and is nullable
|
|
105
|
+
- ✅ Foreign key constraint to `cameras(id)`
|
|
106
|
+
- ✅ Proper ON DELETE SET NULL behavior
|
|
107
|
+
- ✅ Index created for performance
|
|
108
|
+
|
|
109
|
+
## Recommended Solutions
|
|
110
|
+
|
|
111
|
+
### 1. Immediate Fix: Verify Input Data
|
|
112
|
+
|
|
113
|
+
Check what data is being passed to the `create()` method:
|
|
114
|
+
|
|
115
|
+
- Log the `item` parameter in FolderDAO.create()
|
|
116
|
+
- Verify if `cameraId` is being provided in the request
|
|
117
|
+
|
|
118
|
+
### 2. Enhanced DAO Queries
|
|
119
|
+
|
|
120
|
+
Update FolderDAO to include camera relationships:
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
// Enhanced getById with camera join
|
|
124
|
+
async getById(id: number): Promise<IFolder | null> {
|
|
125
|
+
const folder = await this._knex("folders as f")
|
|
126
|
+
.innerJoin("study as s", "f.studyId", "s.id")
|
|
127
|
+
.leftJoin("cameras as c", "f.cameraId", "c.id")
|
|
128
|
+
.select(
|
|
129
|
+
"f.*",
|
|
130
|
+
this._knex.raw("to_jsonb(s.*) as study"),
|
|
131
|
+
this._knex.raw("to_jsonb(c.*) as camera")
|
|
132
|
+
)
|
|
133
|
+
.where("f.id", id)
|
|
134
|
+
.first();
|
|
135
|
+
return folder || null;
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### 3. API Layer Investigation
|
|
140
|
+
|
|
141
|
+
Check the API endpoints that create folders:
|
|
142
|
+
|
|
143
|
+
- Verify if `cameraId` is being extracted from request body
|
|
144
|
+
- Ensure DTO/validation layer passes `cameraId` through
|
|
145
|
+
- Check if frontend is sending `cameraId` in requests
|
|
146
|
+
|
|
147
|
+
### 4. Business Logic Validation
|
|
148
|
+
|
|
149
|
+
Consider if folders should always have a camera:
|
|
150
|
+
|
|
151
|
+
- If required: Add validation in create method
|
|
152
|
+
- If optional: Current behavior is correct (null is valid)
|
|
153
|
+
|
|
154
|
+
## Implementation Priority
|
|
155
|
+
|
|
156
|
+
1. **High**: Investigate API request data flow
|
|
157
|
+
2. **High**: Add camera relationship to DAO queries
|
|
158
|
+
3. **Medium**: Add logging to track cameraId values
|
|
159
|
+
4. **Low**: Consider business logic changes
|
|
160
|
+
|
|
161
|
+
## Files to Examine Next
|
|
162
|
+
|
|
163
|
+
1. `api-rel/src/controllers/folder/folder.controller.ts` - Controller logic
|
|
164
|
+
2. `api-rel/src/routes/folder/` - Route definitions and DTOs
|
|
165
|
+
3. Frontend folder creation forms - Check if cameraId is being sent
|
|
166
|
+
|
|
167
|
+
The database schema and DAO patterns are correctly implemented. The issue likely lies in the data flow from the API/frontend layers not providing `cameraId` values during folder creation.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Knex } from "knex";
|
|
2
|
+
|
|
3
|
+
export async function up(knex: Knex): Promise<void> {
|
|
4
|
+
await knex.schema.alterTable("cameras", (table) => {
|
|
5
|
+
// Add index for case-insensitive name searches
|
|
6
|
+
table.index(knex.raw("LOWER(name)"), "idx_cameras_name_lower");
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
// Add GIN index for full-text search if needed for fuzzy matching
|
|
10
|
+
await knex.raw(`
|
|
11
|
+
CREATE INDEX idx_cameras_name_gin
|
|
12
|
+
ON cameras
|
|
13
|
+
USING GIN (to_tsvector('english', name))
|
|
14
|
+
`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function down(knex: Knex): Promise<void> {
|
|
18
|
+
await knex.raw("DROP INDEX IF EXISTS idx_cameras_name_gin");
|
|
19
|
+
await knex.schema.alterTable("cameras", (table) => {
|
|
20
|
+
table.dropIndex(knex.raw("LOWER(name)"), "idx_cameras_name_lower");
|
|
21
|
+
});
|
|
22
|
+
}
|
package/package.json
CHANGED
package/plan.md
CHANGED
|
@@ -1,129 +1,288 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Database Optimization Plan for Camera Bulk Operations
|
|
2
2
|
|
|
3
|
-
## Schema
|
|
3
|
+
## Current Schema Analysis
|
|
4
4
|
|
|
5
|
-
### Tables
|
|
5
|
+
### Existing Tables & Indexes
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
**cameras table:**
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
- Primary key: `id` (auto-increment)
|
|
10
|
+
- Unique index: `uuid`
|
|
11
|
+
- Composite index: `[longitude, latitude]` for geospatial queries
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
- Add `annotation_source_id INTEGER NULL REFERENCES video(id) ON DELETE SET NULL`
|
|
13
|
-
- Add index on `annotation_source_id` for performance
|
|
14
|
-
- Add composite index on `(folderId, videoType)` for template lookup optimization
|
|
13
|
+
**video table:**
|
|
15
14
|
|
|
16
|
-
|
|
15
|
+
- Primary key: `id`
|
|
16
|
+
- Foreign key: `cameraId` → `cameras.id` (nullable, with index)
|
|
17
|
+
- Foreign key: `folderId` → `folders.id` (with implicit index)
|
|
18
|
+
- Unique index: `uuid`
|
|
19
|
+
- Composite indexes:
|
|
20
|
+
- `[annotationSourceId]`
|
|
21
|
+
- `[folderId, videoType, status]` (template lookup)
|
|
17
22
|
|
|
18
|
-
|
|
23
|
+
**folders table:**
|
|
19
24
|
|
|
20
|
-
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
- Add composite index on `(folderId, videoType)` for efficient template queries
|
|
25
|
-
- **Down operations**:
|
|
26
|
-
- Drop indexes
|
|
27
|
-
- Drop foreign key constraint
|
|
28
|
-
- Drop column
|
|
29
|
-
- **Safety level**: LOW RISK - Adding nullable column with proper constraints
|
|
25
|
+
- Primary key: `id`
|
|
26
|
+
- Foreign key: `cameraId` → `cameras.id` (nullable, with index)
|
|
27
|
+
- Foreign key: `studyId` → `study.id` (with implicit index)
|
|
28
|
+
- Unique index: `uuid`
|
|
30
29
|
|
|
31
|
-
|
|
30
|
+
### Performance Analysis for Required Operations
|
|
32
31
|
|
|
33
|
-
|
|
32
|
+
#### 1. ✅ Bulk update videos when folder cameraId changes
|
|
34
33
|
|
|
35
|
-
|
|
36
|
-
- **New methods**:
|
|
37
|
-
- `getTemplateVideos(folderId: number, videoType: string): Promise<IVideo[]>` - Get videos from same folder/type that can serve as templates (have metadata and completed status)
|
|
38
|
-
- `getVideosUsingTemplate(templateVideoId: number): Promise<IVideo[]>` - Get videos that used a specific video as template
|
|
39
|
-
- `setAnnotationSource(videoId: number, sourceVideoId: number | null): Promise<IVideo | null>` - Set/update annotation source
|
|
40
|
-
- **Performance optimizations**:
|
|
41
|
-
- Use JOINs to fetch related annotation source data in main queries
|
|
42
|
-
- Eliminate N+1 queries when fetching videos with their annotation sources
|
|
43
|
-
- Optimized template lookup using composite index
|
|
34
|
+
**Query:** `UPDATE video SET cameraId = ? WHERE folderId = ?`
|
|
44
35
|
|
|
45
|
-
|
|
36
|
+
- **Current Performance:** GOOD
|
|
37
|
+
- **Existing Index:** `[folderId]` (implicit from FK constraint)
|
|
38
|
+
- **Status:** No optimization needed
|
|
46
39
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
SELECT
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
40
|
+
#### 2. ❌ Search cameras by name with pagination
|
|
41
|
+
|
|
42
|
+
**Query:** `SELECT * FROM cameras WHERE name ILIKE '%search%' ORDER BY name LIMIT ? OFFSET ?`
|
|
43
|
+
|
|
44
|
+
- **Current Performance:** POOR - Full table scan
|
|
45
|
+
- **Missing Index:** Text search index on `name` column
|
|
46
|
+
- **Impact:** Critical performance issue for camera search
|
|
47
|
+
|
|
48
|
+
#### 3. ✅ Find videos by specific camera
|
|
49
|
+
|
|
50
|
+
**Query:** `SELECT * FROM video WHERE cameraId = ?`
|
|
51
|
+
|
|
52
|
+
- **Current Performance:** GOOD
|
|
53
|
+
- **Existing Index:** `[cameraId]` from migration `20250911000000_migration.ts`
|
|
54
|
+
- **Status:** No optimization needed
|
|
55
|
+
|
|
56
|
+
#### 4. ⚠️ Bulk assign camera to multiple videos
|
|
57
|
+
|
|
58
|
+
**Query:** `UPDATE video SET cameraId = ? WHERE id IN (...)`
|
|
59
|
+
|
|
60
|
+
- **Current Performance:** ACCEPTABLE but can be optimized
|
|
61
|
+
- **Existing Index:** Primary key on `id`
|
|
62
|
+
- **Optimization Opportunity:** Batch processing with transaction optimization
|
|
63
|
+
|
|
64
|
+
## Required Database Optimizations
|
|
65
|
+
|
|
66
|
+
### New Migrations Required
|
|
67
|
+
|
|
68
|
+
#### Migration: Add camera name search index
|
|
69
|
+
|
|
70
|
+
**File:** `20250924000000_camera_name_search_index.ts`
|
|
71
|
+
**Purpose:** Optimize camera search by name functionality
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
export async function up(knex: Knex): Promise<void> {
|
|
75
|
+
await knex.schema.alterTable("cameras", (table) => {
|
|
76
|
+
// Add index for case-insensitive name searches
|
|
77
|
+
table.index(knex.raw("LOWER(name)"), "idx_cameras_name_lower");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Add GIN index for full-text search if needed for fuzzy matching
|
|
81
|
+
await knex.raw(`
|
|
82
|
+
CREATE INDEX idx_cameras_name_gin
|
|
83
|
+
ON cameras
|
|
84
|
+
USING GIN (to_tsvector('english', name))
|
|
85
|
+
`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function down(knex: Knex): Promise<void> {
|
|
89
|
+
await knex.raw("DROP INDEX IF EXISTS idx_cameras_name_gin");
|
|
90
|
+
await knex.schema.alterTable("cameras", (table) => {
|
|
91
|
+
table.dropIndex(knex.raw("LOWER(name)"), "idx_cameras_name_lower");
|
|
92
|
+
});
|
|
93
|
+
}
|
|
60
94
|
```
|
|
61
95
|
|
|
62
|
-
|
|
96
|
+
### DAO Method Optimizations
|
|
97
|
+
|
|
98
|
+
#### CameraDAO Enhancements Required
|
|
99
|
+
|
|
100
|
+
**New Methods Needed:**
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
// Optimized paginated search by name
|
|
104
|
+
async searchByName(
|
|
105
|
+
searchTerm: string,
|
|
106
|
+
page: number,
|
|
107
|
+
limit: number
|
|
108
|
+
): Promise<IDataPaginator<ICamera>> {
|
|
109
|
+
const offset = (page - 1) * limit;
|
|
110
|
+
const searchPattern = `%${searchTerm.toLowerCase()}%`;
|
|
111
|
+
|
|
112
|
+
const query = this._knex("cameras")
|
|
113
|
+
.whereRaw("LOWER(name) LIKE ?", [searchPattern])
|
|
114
|
+
.orderBy("name");
|
|
115
|
+
|
|
116
|
+
const [countResult] = await query.clone().clearSelect().count("* as count");
|
|
117
|
+
const totalCount = +countResult.count;
|
|
118
|
+
const cameras = await query.clone().limit(limit).offset(offset);
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
success: true,
|
|
122
|
+
data: cameras,
|
|
123
|
+
page,
|
|
124
|
+
limit,
|
|
125
|
+
count: cameras.length,
|
|
126
|
+
totalCount,
|
|
127
|
+
totalPages: Math.ceil(totalCount / limit),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Get all videos associated with a camera (with pagination)
|
|
132
|
+
async getVideosByCamera(
|
|
133
|
+
cameraId: number,
|
|
134
|
+
page: number,
|
|
135
|
+
limit: number
|
|
136
|
+
): Promise<IDataPaginator<IVideo>> {
|
|
137
|
+
const offset = (page - 1) * limit;
|
|
138
|
+
|
|
139
|
+
const query = this._knex("video as v")
|
|
140
|
+
.innerJoin("folders as f", "v.folderId", "f.id")
|
|
141
|
+
.select("v.*", this._knex.raw("to_jsonb(f.*) as folder"))
|
|
142
|
+
.where("v.cameraId", cameraId)
|
|
143
|
+
.orderBy("v.created_at", "desc");
|
|
144
|
+
|
|
145
|
+
const [countResult] = await query.clone().clearSelect().count("* as count");
|
|
146
|
+
const totalCount = +countResult.count;
|
|
147
|
+
const videos = await query.clone().limit(limit).offset(offset);
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
success: true,
|
|
151
|
+
data: videos,
|
|
152
|
+
page,
|
|
153
|
+
limit,
|
|
154
|
+
count: videos.length,
|
|
155
|
+
totalCount,
|
|
156
|
+
totalPages: Math.ceil(totalCount / limit),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
```
|
|
63
160
|
|
|
64
|
-
|
|
161
|
+
#### VideoDAO Enhancements Required
|
|
162
|
+
|
|
163
|
+
**New Methods Needed:**
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
// Bulk update videos by folder (folder camera cascade)
|
|
167
|
+
async bulkUpdateCameraByFolder(
|
|
168
|
+
folderId: number,
|
|
169
|
+
cameraId: number | null
|
|
170
|
+
): Promise<number> {
|
|
171
|
+
const result = await this._knex("video")
|
|
172
|
+
.where("folderId", folderId)
|
|
173
|
+
.update({
|
|
174
|
+
cameraId: cameraId,
|
|
175
|
+
updated_at: this._knex.fn.now()
|
|
176
|
+
});
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Bulk assign camera to multiple videos
|
|
181
|
+
async bulkUpdateCamera(
|
|
182
|
+
videoIds: number[],
|
|
183
|
+
cameraId: number | null
|
|
184
|
+
): Promise<number> {
|
|
185
|
+
if (videoIds.length === 0) return 0;
|
|
186
|
+
|
|
187
|
+
const result = await this._knex("video")
|
|
188
|
+
.whereIn("id", videoIds)
|
|
189
|
+
.update({
|
|
190
|
+
cameraId: cameraId,
|
|
191
|
+
updated_at: this._knex.fn.now()
|
|
192
|
+
});
|
|
193
|
+
return result;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Get videos by multiple folder IDs (for folder camera propagation)
|
|
197
|
+
async getVideosByFolderIds(folderIds: number[]): Promise<IVideo[]> {
|
|
198
|
+
if (folderIds.length === 0) return [];
|
|
199
|
+
|
|
200
|
+
return await this._knex("video")
|
|
201
|
+
.whereIn("folderId", folderIds)
|
|
202
|
+
.select("id", "uuid", "name", "folderId", "cameraId");
|
|
203
|
+
}
|
|
204
|
+
```
|
|
65
205
|
|
|
66
|
-
|
|
67
|
-
- **New properties**:
|
|
68
|
-
- `annotationSourceId?: number;` - ID of video whose annotations were copied
|
|
69
|
-
- `annotationSource?: IVideo;` - Optional populated annotation source video object
|
|
70
|
-
- **Export**: Update index.ts exports
|
|
206
|
+
## Query Performance Strategies
|
|
71
207
|
|
|
72
|
-
|
|
208
|
+
### 1. Camera Search Optimization
|
|
209
|
+
|
|
210
|
+
- **Index Strategy:** Case-insensitive BTREE index on `LOWER(name)`
|
|
211
|
+
- **Query Pattern:** `WHERE LOWER(name) LIKE LOWER(?)`
|
|
212
|
+
- **Full-text Option:** GIN index with `to_tsvector()` for fuzzy search
|
|
213
|
+
- **Expected Performance:** O(log n) lookup instead of O(n) scan
|
|
73
214
|
|
|
74
|
-
|
|
75
|
-
2. **Migrations** → Create and run migration to add database column
|
|
76
|
-
3. **DAOs** → Add new methods to VideoDAO for template management
|
|
77
|
-
4. **Exports** → Update index.ts to export new interface changes
|
|
78
|
-
5. **Build** → Compile and test changes
|
|
215
|
+
### 2. Bulk Operation Optimization
|
|
79
216
|
|
|
80
|
-
|
|
217
|
+
- **Transaction Wrapping:** All bulk operations in single transactions
|
|
218
|
+
- **Batch Size Limits:** Process in chunks of 1000 records max
|
|
219
|
+
- **Index Usage:** Leverage existing primary key and foreign key indexes
|
|
81
220
|
|
|
82
|
-
###
|
|
221
|
+
### 3. Folder Camera Cascade Optimization
|
|
83
222
|
|
|
84
|
-
-
|
|
85
|
-
-
|
|
86
|
-
-
|
|
87
|
-
- Template videos must have lane configuration in metadata field
|
|
223
|
+
- **Single Query Strategy:** Use `WHERE folderId = ?` leveraging existing index
|
|
224
|
+
- **Atomic Updates:** Single UPDATE statement rather than individual updates
|
|
225
|
+
- **Expected Performance:** O(log n) folder lookup + O(k) video updates
|
|
88
226
|
|
|
89
|
-
|
|
227
|
+
## Implementation Order
|
|
90
228
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
229
|
+
1. **Migration First:** Deploy camera name search index
|
|
230
|
+
2. **DAO Methods:** Add optimized search and bulk operation methods
|
|
231
|
+
3. **Transaction Optimization:** Implement proper transaction handling
|
|
232
|
+
4. **Testing:** Validate performance improvements
|
|
94
233
|
|
|
95
|
-
##
|
|
234
|
+
## Risk Assessment
|
|
96
235
|
|
|
97
|
-
###
|
|
236
|
+
### Migration Safety: LOW RISK
|
|
98
237
|
|
|
99
|
-
-
|
|
100
|
-
-
|
|
101
|
-
-
|
|
238
|
+
- Adding indexes is non-blocking operation
|
|
239
|
+
- No data changes, only performance improvements
|
|
240
|
+
- Easy rollback with down migration
|
|
102
241
|
|
|
103
|
-
###
|
|
242
|
+
### Performance Impact: HIGH BENEFIT
|
|
104
243
|
|
|
105
|
-
-
|
|
106
|
-
-
|
|
107
|
-
-
|
|
244
|
+
- Camera search: 100x-1000x improvement (O(n) → O(log n))
|
|
245
|
+
- Bulk operations: Proper indexing already exists
|
|
246
|
+
- Folder cascade: Leverages existing folderId index
|
|
108
247
|
|
|
109
|
-
|
|
248
|
+
### Pattern Compliance: 100% COMPLIANT
|
|
249
|
+
|
|
250
|
+
- Follows existing DAO patterns exactly
|
|
251
|
+
- Maintains IBaseDAO interface requirements
|
|
252
|
+
- Uses established naming conventions
|
|
253
|
+
- Preserves UUID external API pattern
|
|
254
|
+
|
|
255
|
+
## Validation Strategy
|
|
256
|
+
|
|
257
|
+
### Performance Testing Queries
|
|
258
|
+
|
|
259
|
+
```sql
|
|
260
|
+
-- Test camera search performance
|
|
261
|
+
EXPLAIN ANALYZE SELECT * FROM cameras
|
|
262
|
+
WHERE LOWER(name) LIKE '%traffic%'
|
|
263
|
+
ORDER BY name LIMIT 20;
|
|
264
|
+
|
|
265
|
+
-- Test video-by-camera lookup
|
|
266
|
+
EXPLAIN ANALYZE SELECT * FROM video
|
|
267
|
+
WHERE cameraId = 1
|
|
268
|
+
ORDER BY created_at DESC LIMIT 20;
|
|
269
|
+
|
|
270
|
+
-- Test folder cascade update
|
|
271
|
+
EXPLAIN ANALYZE UPDATE video
|
|
272
|
+
SET cameraId = 1 WHERE folderId = 5;
|
|
273
|
+
```
|
|
110
274
|
|
|
111
|
-
###
|
|
275
|
+
### Success Metrics
|
|
112
276
|
|
|
113
|
-
-
|
|
114
|
-
-
|
|
115
|
-
-
|
|
277
|
+
- Camera search < 50ms for 10k+ cameras
|
|
278
|
+
- Video-by-camera < 100ms for 1k+ videos
|
|
279
|
+
- Folder cascade < 200ms for 100+ videos per folder
|
|
280
|
+
- Bulk video update < 500ms for 1000+ videos
|
|
116
281
|
|
|
117
|
-
|
|
282
|
+
## Conclusion
|
|
118
283
|
|
|
119
|
-
|
|
120
|
-
- **100% compliant** with naming conventions (snake_case DB, camelCase TS)
|
|
121
|
-
- **100% compliant** with interface patterns (I prefix, proper exports)
|
|
122
|
-
- **100% compliant** with migration patterns (timestamp prefix, up/down functions)
|
|
284
|
+
**Current Status:** Schema is well-designed with most needed indexes already present. Only critical gap is camera name search optimization.
|
|
123
285
|
|
|
124
|
-
|
|
286
|
+
**Priority:** HIGH for camera name search index, MEDIUM for DAO method enhancements.
|
|
125
287
|
|
|
126
|
-
-
|
|
127
|
-
- Test template discovery with same folder/type filtering
|
|
128
|
-
- Validate NULL handling for videos without annotation sources
|
|
129
|
-
- Performance test template lookup queries with large datasets
|
|
288
|
+
**Complexity:** LOW - Single migration required, standard DAO patterns.
|
|
@@ -76,4 +76,59 @@ export class CameraDAO implements IBaseDAO<ICamera> {
|
|
|
76
76
|
);
|
|
77
77
|
return cameras;
|
|
78
78
|
}
|
|
79
|
+
|
|
80
|
+
async getAllWithSearch(
|
|
81
|
+
page: number,
|
|
82
|
+
limit: number,
|
|
83
|
+
name?: string,
|
|
84
|
+
): Promise<IDataPaginator<ICamera>> {
|
|
85
|
+
const offset = (page - 1) * limit;
|
|
86
|
+
|
|
87
|
+
const query = this._knex("cameras");
|
|
88
|
+
|
|
89
|
+
if (name && name.trim() !== "") {
|
|
90
|
+
query.where("name", "ilike", `%${name.trim()}%`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const [countResult] = await query.clone().count("* as count");
|
|
94
|
+
const totalCount = +countResult.count;
|
|
95
|
+
const cameras = await query.clone().limit(limit).offset(offset);
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
success: true,
|
|
99
|
+
data: cameras,
|
|
100
|
+
page,
|
|
101
|
+
limit,
|
|
102
|
+
count: cameras.length,
|
|
103
|
+
totalCount,
|
|
104
|
+
totalPages: Math.ceil(totalCount / limit),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async getVideosByCamera(
|
|
109
|
+
cameraId: number,
|
|
110
|
+
page: number,
|
|
111
|
+
limit: number,
|
|
112
|
+
): Promise<IDataPaginator<any>> {
|
|
113
|
+
const offset = (page - 1) * limit;
|
|
114
|
+
|
|
115
|
+
const query = this._knex("videos as v")
|
|
116
|
+
.innerJoin("folders as f", "v.folderId", "f.id")
|
|
117
|
+
.where("v.cameraId", cameraId)
|
|
118
|
+
.select("v.*", this._knex.raw("to_jsonb(f.*) as folder"));
|
|
119
|
+
|
|
120
|
+
const [countResult] = await query.clone().clearSelect().count("* as count");
|
|
121
|
+
const totalCount = +countResult.count;
|
|
122
|
+
const videos = await query.clone().limit(limit).offset(offset);
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
success: true,
|
|
126
|
+
data: videos,
|
|
127
|
+
page,
|
|
128
|
+
limit,
|
|
129
|
+
count: videos.length,
|
|
130
|
+
totalCount,
|
|
131
|
+
totalPages: Math.ceil(totalCount / limit),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
79
134
|
}
|
|
@@ -16,7 +16,12 @@ export class FolderDAO implements IBaseDAO<IFolder> {
|
|
|
16
16
|
async getById(id: number): Promise<IFolder | null> {
|
|
17
17
|
const folder = await this._knex("folders as f")
|
|
18
18
|
.innerJoin("study as s", "f.studyId", "s.id")
|
|
19
|
-
.
|
|
19
|
+
.leftJoin("cameras as c", "f.cameraId", "c.id")
|
|
20
|
+
.select(
|
|
21
|
+
"f.*",
|
|
22
|
+
this._knex.raw("to_jsonb(s.*) as study"),
|
|
23
|
+
this._knex.raw("to_jsonb(c.*) as camera"),
|
|
24
|
+
)
|
|
20
25
|
.where("f.id", id)
|
|
21
26
|
.first();
|
|
22
27
|
return folder || null;
|
|
@@ -25,7 +30,12 @@ export class FolderDAO implements IBaseDAO<IFolder> {
|
|
|
25
30
|
async getByUuid(uuid: string): Promise<IFolder | null> {
|
|
26
31
|
const folder = await this._knex("folders as f")
|
|
27
32
|
.innerJoin("study as s", "f.studyId", "s.id")
|
|
28
|
-
.
|
|
33
|
+
.leftJoin("cameras as c", "f.cameraId", "c.id")
|
|
34
|
+
.select(
|
|
35
|
+
"f.*",
|
|
36
|
+
this._knex.raw("to_jsonb(s.*) as study"),
|
|
37
|
+
this._knex.raw("to_jsonb(c.*) as camera"),
|
|
38
|
+
)
|
|
29
39
|
.where("f.uuid", uuid)
|
|
30
40
|
.first();
|
|
31
41
|
return folder || null;
|
|
@@ -53,7 +63,12 @@ export class FolderDAO implements IBaseDAO<IFolder> {
|
|
|
53
63
|
|
|
54
64
|
const query = this._knex("folders as f")
|
|
55
65
|
.innerJoin("study as s", "f.studyId", "s.id")
|
|
56
|
-
.
|
|
66
|
+
.leftJoin("cameras as c", "f.cameraId", "c.id")
|
|
67
|
+
.select(
|
|
68
|
+
"f.*",
|
|
69
|
+
this._knex.raw("to_jsonb(s.*) as study"),
|
|
70
|
+
this._knex.raw("to_jsonb(c.*) as camera"),
|
|
71
|
+
);
|
|
57
72
|
if (studyId !== undefined && studyId !== null) {
|
|
58
73
|
query.where("f.studyId", studyId);
|
|
59
74
|
}
|
|
@@ -245,4 +245,51 @@ export class VideoDAO implements IBaseDAO<IVideo> {
|
|
|
245
245
|
throw error;
|
|
246
246
|
}
|
|
247
247
|
}
|
|
248
|
+
|
|
249
|
+
async getVideoIdsByFolderId(folderId: number): Promise<number[]> {
|
|
250
|
+
const videos = await this._knex("video")
|
|
251
|
+
.where("folderId", folderId)
|
|
252
|
+
.select("id");
|
|
253
|
+
return videos.map((video) => video.id);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async bulkUpdateCamera(
|
|
257
|
+
videoIds: number[],
|
|
258
|
+
cameraId: number | null,
|
|
259
|
+
trx?: Knex.Transaction,
|
|
260
|
+
): Promise<number> {
|
|
261
|
+
const knexInstance = trx || this._knex;
|
|
262
|
+
const result = await knexInstance("video").whereIn("id", videoIds).update({
|
|
263
|
+
cameraId: cameraId,
|
|
264
|
+
updated_at: knexInstance.fn.now(),
|
|
265
|
+
});
|
|
266
|
+
return result;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async getVideosByCameraIdWithFolder(
|
|
270
|
+
cameraId: number,
|
|
271
|
+
page: number,
|
|
272
|
+
limit: number,
|
|
273
|
+
): Promise<IDataPaginator<IVideo>> {
|
|
274
|
+
const offset = (page - 1) * limit;
|
|
275
|
+
|
|
276
|
+
const query = this._knex("video as v")
|
|
277
|
+
.innerJoin("folders as f", "v.folderId", "f.id")
|
|
278
|
+
.where("v.cameraId", cameraId)
|
|
279
|
+
.select("v.*", this._knex.raw("to_jsonb(f.*) as folder"));
|
|
280
|
+
|
|
281
|
+
const [countResult] = await query.clone().clearSelect().count("* as count");
|
|
282
|
+
const totalCount = +countResult.count;
|
|
283
|
+
const videos = await query.clone().limit(limit).offset(offset);
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
success: true,
|
|
287
|
+
data: videos,
|
|
288
|
+
page,
|
|
289
|
+
limit,
|
|
290
|
+
count: videos.length,
|
|
291
|
+
totalCount,
|
|
292
|
+
totalPages: Math.ceil(totalCount / limit),
|
|
293
|
+
};
|
|
294
|
+
}
|
|
248
295
|
}
|