@trafficgroup/knex-rel 0.1.9 → 0.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/.claude/settings.local.json +5 -2
  2. package/.env.prod +5 -0
  3. package/CLAUDE.md +2 -11
  4. package/dist/constants/video.constants.d.ts +12 -0
  5. package/dist/constants/video.constants.js +18 -0
  6. package/dist/constants/video.constants.js.map +1 -0
  7. package/dist/dao/VideoMinuteResultDAO.d.ts +1 -1
  8. package/dist/dao/VideoMinuteResultDAO.js +23 -29
  9. package/dist/dao/VideoMinuteResultDAO.js.map +1 -1
  10. package/dist/dao/auth/auth.dao.js +1 -4
  11. package/dist/dao/auth/auth.dao.js.map +1 -1
  12. package/dist/dao/batch/batch.dao.js +14 -13
  13. package/dist/dao/batch/batch.dao.js.map +1 -1
  14. package/dist/dao/camera/camera.dao.js +7 -10
  15. package/dist/dao/camera/camera.dao.js.map +1 -1
  16. package/dist/dao/chat/chat.dao.d.ts +1 -1
  17. package/dist/dao/chat/chat.dao.js +35 -25
  18. package/dist/dao/chat/chat.dao.js.map +1 -1
  19. package/dist/dao/folder/folder.dao.js +2 -7
  20. package/dist/dao/folder/folder.dao.js.map +1 -1
  21. package/dist/dao/location/location.dao.js +9 -16
  22. package/dist/dao/location/location.dao.js.map +1 -1
  23. package/dist/dao/message/message.dao.d.ts +1 -1
  24. package/dist/dao/message/message.dao.js +26 -18
  25. package/dist/dao/message/message.dao.js.map +1 -1
  26. package/dist/dao/report-configuration/report-configuration.dao.js +31 -32
  27. package/dist/dao/report-configuration/report-configuration.dao.js.map +1 -1
  28. package/dist/dao/study/study.dao.js +2 -7
  29. package/dist/dao/study/study.dao.js.map +1 -1
  30. package/dist/dao/user/user.dao.js +1 -4
  31. package/dist/dao/user/user.dao.js.map +1 -1
  32. package/dist/dao/user-push-notification-token/user-push-notification-token.dao.js +8 -26
  33. package/dist/dao/user-push-notification-token/user-push-notification-token.dao.js.map +1 -1
  34. package/dist/dao/video/video.dao.d.ts +2 -1
  35. package/dist/dao/video/video.dao.js +42 -28
  36. package/dist/dao/video/video.dao.js.map +1 -1
  37. package/dist/index.d.ts +6 -4
  38. package/dist/index.js +6 -1
  39. package/dist/index.js.map +1 -1
  40. package/dist/interfaces/batch/batch.interfaces.d.ts +1 -1
  41. package/dist/interfaces/camera/camera.interfaces.d.ts +1 -1
  42. package/dist/interfaces/chat/chat.interfaces.d.ts +3 -3
  43. package/dist/interfaces/folder/folder.interfaces.d.ts +1 -1
  44. package/dist/interfaces/message/message.interfaces.d.ts +2 -2
  45. package/dist/interfaces/study/study.interfaces.d.ts +2 -2
  46. package/dist/interfaces/user/user.interfaces.d.ts +1 -1
  47. package/dist/interfaces/user-push-notification-token/user-push-notification-token.interfaces.d.ts +1 -1
  48. package/dist/interfaces/video/video.interfaces.d.ts +2 -2
  49. package/migrations/20250717160737_migration.ts +1 -1
  50. package/migrations/20250717160908_migration.ts +2 -5
  51. package/migrations/20250717161310_migration.ts +1 -1
  52. package/migrations/20250717161406_migration.ts +3 -3
  53. package/migrations/20250717162431_migration.ts +1 -1
  54. package/migrations/20250717173228_migration.ts +2 -2
  55. package/migrations/20250717204731_migration.ts +1 -1
  56. package/migrations/20250722210109_migration.ts +4 -8
  57. package/migrations/20250722211019_migration.ts +1 -1
  58. package/migrations/20250723153852_migration.ts +10 -13
  59. package/migrations/20250723162257_migration.ts +7 -4
  60. package/migrations/20250723171109_migration.ts +7 -4
  61. package/migrations/20250723205331_migration.ts +9 -6
  62. package/migrations/20250724191345_migration.ts +11 -8
  63. package/migrations/20250730180932_migration.ts +13 -14
  64. package/migrations/20250730213625_migration.ts +11 -8
  65. package/migrations/20250804124509_migration.ts +21 -26
  66. package/migrations/20250804132053_migration.ts +8 -5
  67. package/migrations/20250804164518_migration.ts +7 -7
  68. package/migrations/20250823223016_migration.ts +21 -32
  69. package/migrations/20250910015452_migration.ts +6 -18
  70. package/migrations/20250911000000_migration.ts +4 -18
  71. package/migrations/20250917144153_migration.ts +7 -14
  72. package/migrations/20250930200521_migration.ts +4 -8
  73. package/migrations/20251010143500_migration.ts +6 -27
  74. package/migrations/20251020225758_migration.ts +15 -51
  75. package/migrations/20251112120000_migration.ts +2 -10
  76. package/migrations/20251112120200_migration.ts +7 -19
  77. package/migrations/20251112120300_migration.ts +2 -7
  78. package/package.json +1 -1
  79. package/src/constants/video.constants.ts +19 -0
  80. package/src/d.types.ts +14 -18
  81. package/src/dao/VideoMinuteResultDAO.ts +49 -72
  82. package/src/dao/auth/auth.dao.ts +55 -58
  83. package/src/dao/batch/batch.dao.ts +98 -101
  84. package/src/dao/camera/camera.dao.ts +121 -124
  85. package/src/dao/chat/chat.dao.ts +43 -45
  86. package/src/dao/folder/folder.dao.ts +56 -65
  87. package/src/dao/location/location.dao.ts +87 -109
  88. package/src/dao/message/message.dao.ts +32 -32
  89. package/src/dao/report-configuration/report-configuration.dao.ts +342 -370
  90. package/src/dao/study/study.dao.ts +63 -88
  91. package/src/dao/user/user.dao.ts +50 -52
  92. package/src/dao/user-push-notification-token/user-push-notification-token.dao.ts +49 -83
  93. package/src/dao/video/video.dao.ts +339 -357
  94. package/src/entities/BaseEntity.ts +1 -1
  95. package/src/index.ts +22 -26
  96. package/src/interfaces/auth/auth.interfaces.ts +10 -10
  97. package/src/interfaces/batch/batch.interfaces.ts +1 -1
  98. package/src/interfaces/camera/camera.interfaces.ts +9 -9
  99. package/src/interfaces/chat/chat.interfaces.ts +4 -4
  100. package/src/interfaces/folder/folder.interfaces.ts +2 -2
  101. package/src/interfaces/location/location.interfaces.ts +7 -7
  102. package/src/interfaces/message/message.interfaces.ts +3 -3
  103. package/src/interfaces/report-configuration/report-configuration.interfaces.ts +16 -16
  104. package/src/interfaces/study/study.interfaces.ts +3 -3
  105. package/src/interfaces/user/user.interfaces.ts +9 -9
  106. package/src/interfaces/user-push-notification-token/user-push-notification-token.interfaces.ts +9 -9
  107. package/src/interfaces/video/video.interfaces.ts +34 -34
@@ -2,231 +2,210 @@ import { Knex } from "knex";
2
2
  import { IBaseDAO, IDataPaginator } from "../../d.types";
3
3
  import { IVideo } from "../../interfaces/video/video.interfaces";
4
4
  import KnexManager from "../../KnexConnection";
5
+ import { VALID_VIDEO_SORT_FIELDS, VALID_SORT_ORDERS, VIDEO_SORT_COLUMN_MAP, VideoSortField, SortOrder } from "../../constants/video.constants";
5
6
 
6
7
  export class VideoDAO implements IBaseDAO<IVideo> {
7
- private _knex: Knex<any, unknown[]> = KnexManager.getConnection();
8
-
9
- static getInstance(): Knex<any, unknown[]> {
10
- return KnexManager.getConnection();
11
- }
12
-
13
- async create(item: IVideo): Promise<IVideo> {
14
- const [createdVideo] = await this._knex("video")
15
- .insert(item)
16
- .returning("*");
17
- return createdVideo;
18
- }
19
-
20
- async getById(id: number): Promise<IVideo | null> {
21
- const video = await this._knex("video as v")
22
- .innerJoin("folders as f", "v.folderId", "f.id")
23
- .select("v.*", this._knex.raw("to_jsonb(f.*) as folder"))
24
- .where("v.id", id)
25
- .first();
26
- return video || null;
27
- }
28
-
29
- async getByUuid(uuid: string): Promise<IVideo | null> {
30
- const video = await this._knex("video as v")
31
- .innerJoin("folders as f", "v.folderId", "f.id")
32
- .select("v.*", this._knex.raw("to_jsonb(f.*) as folder"))
33
- .where("v.uuid", uuid)
34
- .first();
35
- return video || null;
36
- }
37
-
38
- async update(id: number, item: Partial<IVideo>): Promise<IVideo | null> {
39
- const [updatedVideo] = await this._knex("video")
40
- .where({ id })
41
- .update(item)
42
- .returning("*");
43
- return updatedVideo || null;
44
- }
45
-
46
- async delete(id: number): Promise<boolean> {
47
- const result = await this._knex("video").where({ id }).del();
48
- return result > 0;
49
- }
50
-
51
- // entityId corresponde al id del folder al que pertenece el video
52
- async getAll(
53
- page: number,
54
- limit: number,
55
- folderId?: number | null,
56
- ): Promise<IDataPaginator<IVideo>> {
57
- const offset = (page - 1) * limit;
58
-
59
- const query = this._knex("video as v")
60
- .innerJoin("folders as f", "v.folderId", "f.id")
61
- .leftJoin("cameras as c", "v.cameraId", "c.id")
62
- .select(
63
- "v.*",
64
- this._knex.raw("to_jsonb(f.*) as folder"),
65
- "c.name as cameraName",
66
- "c.uuid as cameraUuid",
67
- );
68
- if (folderId !== undefined && folderId !== null) {
69
- query.where("v.folderId", folderId);
8
+ private _knex: Knex<any, unknown[]> = KnexManager.getConnection();
9
+
10
+ static getInstance(): Knex<any, unknown[]> {
11
+ return KnexManager.getConnection();
12
+ }
13
+
14
+ async create(item: IVideo): Promise<IVideo> {
15
+ const [createdVideo] = await this._knex("video").insert(item).returning("*");
16
+ return createdVideo;
17
+ }
18
+
19
+ async getById(id: number): Promise<IVideo | null> {
20
+ const video = await this._knex("video as v")
21
+ .innerJoin("folders as f", "v.folderId", "f.id")
22
+ .select("v.*", this._knex.raw("to_jsonb(f.*) as folder"))
23
+ .where("v.id", id)
24
+ .first();
25
+ return video || null;
26
+ }
27
+
28
+ async getByUuid(uuid: string): Promise<IVideo | null> {
29
+ const video = await this._knex("video as v")
30
+ .innerJoin("folders as f", "v.folderId", "f.id")
31
+ .select("v.*", this._knex.raw("to_jsonb(f.*) as folder"))
32
+ .where("v.uuid", uuid)
33
+ .first();
34
+ return video || null;
70
35
  }
71
36
 
72
- const [countResult] = await query.clone().clearSelect().count("* as count");
73
- const totalCount = +countResult.count;
74
- const videos = await query.clone().limit(limit).offset(offset);
75
-
76
- return {
77
- success: true,
78
- data: videos,
79
- page,
80
- limit,
81
- count: videos.length,
82
- totalCount,
83
- totalPages: Math.ceil(totalCount / limit),
84
- };
85
- }
86
-
87
- async getVideoStatsByFolderIds(folderIds: number[]): Promise<{
88
- total_videos: number;
89
- completed_videos: number;
90
- failed_videos: number;
91
- processing_videos: number;
92
- }> {
93
- if (!folderIds || folderIds.length === 0) {
94
- return {
95
- total_videos: 0,
96
- completed_videos: 0,
97
- failed_videos: 0,
98
- processing_videos: 0,
99
- };
37
+ async update(id: number, item: Partial<IVideo>): Promise<IVideo | null> {
38
+ const [updatedVideo] = await this._knex("video").where({ id }).update(item).returning("*");
39
+ return updatedVideo || null;
100
40
  }
101
41
 
102
- const result = (await this._knex("video")
103
- .whereIn("folderId", folderIds)
104
- .select(
105
- this._knex.raw("COUNT(*) as total_videos"),
106
- this._knex.raw(
107
- "COUNT(CASE WHEN status = ? THEN 1 END) as completed_videos",
108
- ["COMPLETED"],
109
- ),
110
- this._knex.raw(
111
- "COUNT(CASE WHEN status = ? THEN 1 END) as failed_videos",
112
- ["FAILED"],
113
- ),
114
- this._knex.raw(
115
- "COUNT(CASE WHEN status = ? THEN 1 END) as processing_videos",
116
- ["PROCESSING"],
117
- ),
118
- )
119
- .first()) as any;
120
-
121
- return {
122
- total_videos: parseInt(result?.total_videos) || 0,
123
- completed_videos: parseInt(result?.completed_videos) || 0,
124
- failed_videos: parseInt(result?.failed_videos) || 0,
125
- processing_videos: parseInt(result?.processing_videos) || 0,
126
- };
127
- }
128
-
129
- async getCompletedVideosByFolderIds(
130
- folderIds: number[],
131
- videoType?: string,
132
- ): Promise<IVideo[]> {
133
- if (!folderIds || folderIds.length === 0) {
134
- return [];
42
+ async delete(id: number): Promise<boolean> {
43
+ const result = await this._knex("video").where({ id }).del();
44
+ return result > 0;
45
+ }
46
+
47
+ // entityId corresponde al id del folder al que pertenece el video
48
+ async getAll(page: number, limit: number, folderId?: number | null, sortBy?: VideoSortField, sortOrder?: SortOrder): Promise<IDataPaginator<IVideo>> {
49
+ const offset = (page - 1) * limit;
50
+
51
+ // Validate and set defaults for sorting (defensive programming)
52
+ const sortField = (sortBy || 'name') as VideoSortField;
53
+
54
+ if (!VALID_VIDEO_SORT_FIELDS.includes(sortField as any)) {
55
+ throw new Error(`Invalid sortBy value: ${sortField}. Must be one of: ${VALID_VIDEO_SORT_FIELDS.join(', ')}`);
56
+ }
57
+
58
+ const sortDirection = ((sortOrder || 'DESC').toUpperCase()) as SortOrder;
59
+
60
+ if (!VALID_SORT_ORDERS.includes(sortDirection as any)) {
61
+ throw new Error(`Invalid sortOrder value: ${sortOrder}. Must be one of: ${VALID_SORT_ORDERS.join(', ')}`);
62
+ }
63
+
64
+ // Map sortBy to database column with table alias
65
+ const sortColumn = VIDEO_SORT_COLUMN_MAP[sortField];
66
+
67
+ const query = this._knex("video as v")
68
+ .innerJoin("folders as f", "v.folderId", "f.id")
69
+ .leftJoin("cameras as c", "v.cameraId", "c.id")
70
+ .select(
71
+ "v.*",
72
+ this._knex.raw("to_jsonb(f.*) as folder"),
73
+ "c.name as cameraName",
74
+ "c.uuid as cameraUuid"
75
+ );
76
+ if (folderId !== undefined && folderId !== null) {
77
+ query.where("v.folderId", folderId);
78
+ }
79
+
80
+ const [countResult] = await query.clone().clearSelect().count("* as count");
81
+ const totalCount = +countResult.count;
82
+ const videos = await query
83
+ .clone()
84
+ .orderBy(sortColumn, sortDirection.toLowerCase() as 'asc' | 'desc')
85
+ .limit(limit)
86
+ .offset(offset);
87
+
88
+ return {
89
+ success: true,
90
+ data: videos,
91
+ page,
92
+ limit,
93
+ count: videos.length,
94
+ totalCount,
95
+ totalPages: Math.ceil(totalCount / limit),
96
+ };
97
+ }
98
+
99
+ async getVideoStatsByFolderIds(folderIds: number[]): Promise<{
100
+ total_videos: number;
101
+ completed_videos: number;
102
+ failed_videos: number;
103
+ processing_videos: number;
104
+ }> {
105
+ if (!folderIds || folderIds.length === 0) {
106
+ return {
107
+ total_videos: 0,
108
+ completed_videos: 0,
109
+ failed_videos: 0,
110
+ processing_videos: 0
111
+ };
112
+ }
113
+
114
+ const result = await this._knex("video")
115
+ .whereIn("folderId", folderIds)
116
+ .select(
117
+ this._knex.raw('COUNT(*) as total_videos'),
118
+ this._knex.raw("COUNT(CASE WHEN status = ? THEN 1 END) as completed_videos", ['COMPLETED']),
119
+ this._knex.raw("COUNT(CASE WHEN status = ? THEN 1 END) as failed_videos", ['FAILED']),
120
+ this._knex.raw("COUNT(CASE WHEN status = ? THEN 1 END) as processing_videos", ['PROCESSING'])
121
+ )
122
+ .first() as any;
123
+
124
+ return {
125
+ total_videos: parseInt(result?.total_videos) || 0,
126
+ completed_videos: parseInt(result?.completed_videos) || 0,
127
+ failed_videos: parseInt(result?.failed_videos) || 0,
128
+ processing_videos: parseInt(result?.processing_videos) || 0
129
+ };
130
+ }
131
+
132
+ async getCompletedVideosByFolderIds(folderIds: number[], videoType?: string): Promise<IVideo[]> {
133
+ if (!folderIds || folderIds.length === 0) {
134
+ return [];
135
+ }
136
+
137
+ const query = this._knex("video")
138
+ .whereIn("folderId", folderIds)
139
+ .where("status", "COMPLETED");
140
+
141
+ if (videoType) {
142
+ query.where("videoType", videoType);
143
+ }
144
+
145
+ return await query.select("id", "name", "metadata", "results", "created_at", this._knex.raw("EXTRACT(EPOCH FROM (updated_at - created_at)) as duration"));
135
146
  }
136
147
 
137
- const query = this._knex("video")
138
- .whereIn("folderId", folderIds)
139
- .where("status", "COMPLETED");
148
+ /**
149
+ * Update the duration in seconds for a video
150
+ */
151
+ async updateDuration(id: number, durationSeconds: number): Promise<IVideo | null> {
152
+ const [updatedVideo] = await this._knex("video")
153
+ .where({ id })
154
+ .update({ duration_seconds: durationSeconds, updated_at: this._knex.fn.now() })
155
+ .returning("*");
156
+ return updatedVideo || null;
157
+ }
140
158
 
141
- if (videoType) {
142
- query.where("videoType", videoType);
159
+ /**
160
+ * Get videos that don't have minute-by-minute data yet
161
+ */
162
+ async getVideosWithoutMinuteData(page: number, limit: number): Promise<IDataPaginator<IVideo>> {
163
+ const offset = (page - 1) * limit;
164
+
165
+ const query = this._knex("video as v")
166
+ .leftJoin("video_minute_results as vmr", "v.id", "vmr.video_id")
167
+ .whereNull("vmr.video_id")
168
+ .where("v.status", "COMPLETED")
169
+ .innerJoin("folders as f", "v.folderId", "f.id")
170
+ .select("v.*", this._knex.raw("to_jsonb(f.*) as folder"))
171
+ .groupBy("v.id", "f.id");
172
+
173
+ const [countResult] = await query.clone().clearSelect().count("* as count");
174
+ const totalCount = +countResult.count;
175
+ const videos = await query.clone().limit(limit).offset(offset);
176
+
177
+ return {
178
+ success: true,
179
+ data: videos,
180
+ page,
181
+ limit,
182
+ count: videos.length,
183
+ totalCount,
184
+ totalPages: Math.ceil(totalCount / limit),
185
+ };
143
186
  }
144
187
 
145
- return await query.select(
146
- "id",
147
- "name",
148
- "metadata",
149
- "results",
150
- "created_at",
151
- this._knex.raw(
152
- "EXTRACT(EPOCH FROM (updated_at - created_at)) as duration",
153
- ),
154
- );
155
- }
156
-
157
- /**
158
- * Update the duration in seconds for a video
159
- */
160
- async updateDuration(
161
- id: number,
162
- durationSeconds: number,
163
- ): Promise<IVideo | null> {
164
- const [updatedVideo] = await this._knex("video")
165
- .where({ id })
166
- .update({
167
- duration_seconds: durationSeconds,
168
- updated_at: this._knex.fn.now(),
169
- })
170
- .returning("*");
171
- return updatedVideo || null;
172
- }
173
-
174
- /**
175
- * Get videos that don't have minute-by-minute data yet
176
- */
177
- async getVideosWithoutMinuteData(
178
- page: number,
179
- limit: number,
180
- ): Promise<IDataPaginator<IVideo>> {
181
- const offset = (page - 1) * limit;
182
-
183
- const query = this._knex("video as v")
184
- .leftJoin("video_minute_results as vmr", "v.id", "vmr.video_id")
185
- .whereNull("vmr.video_id")
186
- .where("v.status", "COMPLETED")
187
- .innerJoin("folders as f", "v.folderId", "f.id")
188
- .select("v.*", this._knex.raw("to_jsonb(f.*) as folder"))
189
- .groupBy("v.id", "f.id");
190
-
191
- const [countResult] = await query.clone().clearSelect().count("* as count");
192
- const totalCount = +countResult.count;
193
- const videos = await query.clone().limit(limit).offset(offset);
194
-
195
- return {
196
- success: true,
197
- data: videos,
198
- page,
199
- limit,
200
- count: videos.length,
201
- totalCount,
202
- totalPages: Math.ceil(totalCount / limit),
203
- };
204
- }
205
-
206
- /**
207
- * Get videos from same folder with same type that have metadata and are COMPLETED
208
- * Suitable for use as lane annotation templates
209
- */
210
- async getTemplateVideos(
211
- folderId: number,
212
- videoType: string,
213
- ): Promise<IVideo[]> {
214
- try {
215
- let query = this._knex("video")
216
- .where("folderId", folderId)
217
- .where("videoType", videoType)
218
- .where("status", "COMPLETED")
219
- .whereNotNull("metadata")
220
- .whereRaw("metadata != '{}'");
221
-
222
- // Apply video type specific metadata validation
223
- if (videoType === "ATR") {
224
- // ATR videos use lanes array structure
225
- query = query.whereRaw("jsonb_array_length(metadata->'lanes') > 0");
226
- } else {
227
- // TMC/JUNCTION/ROUNDABOUT/PATHWAY videos use objects with pt1/pt2 structure
228
- // Check if metadata has at least one key with pt1 and pt2 properties
229
- query = query.whereRaw(`
188
+ /**
189
+ * Get videos from same folder with same type that have metadata and are COMPLETED
190
+ * Suitable for use as lane annotation templates
191
+ */
192
+ async getTemplateVideos(folderId: number, videoType: string): Promise<IVideo[]> {
193
+ try {
194
+ let query = this._knex("video")
195
+ .where("folderId", folderId)
196
+ .where("videoType", videoType)
197
+ .where("status", "COMPLETED")
198
+ .whereNotNull("metadata")
199
+ .whereRaw("metadata != '{}'");
200
+
201
+ // Apply video type specific metadata validation
202
+ if (videoType === "ATR") {
203
+ // ATR videos use lanes array structure
204
+ query = query.whereRaw("jsonb_array_length(metadata->'lanes') > 0");
205
+ } else {
206
+ // TMC/JUNCTION/ROUNDABOUT/PATHWAY videos use objects with pt1/pt2 structure
207
+ // Check if metadata has at least one key with pt1 and pt2 properties
208
+ query = query.whereRaw(`
230
209
  EXISTS (
231
210
  SELECT 1
232
211
  FROM jsonb_each(metadata) as entry(key, value)
@@ -241,154 +220,157 @@ export class VideoDAO implements IBaseDAO<IVideo> {
241
220
  AND jsonb_array_length(value->'pt2') = 2
242
221
  )
243
222
  `);
244
- }
223
+ }
224
+
225
+ const videos = await query
226
+ .orderBy("updated_at", "desc")
227
+ .select("*");
228
+
229
+ return videos;
230
+ } catch (error) {
231
+ console.error("Error fetching template videos:", error);
232
+ throw error;
233
+ }
234
+ }
235
+
236
+ /**
237
+ * Get all video IDs for a specific folder (used for cascade operations)
238
+ */
239
+ async getVideoIdsByFolderId(folderId: number): Promise<number[]> {
240
+ const rows = await this._knex('video')
241
+ .where({ folderId })
242
+ .select('id')
243
+ .orderBy('id', 'asc');
245
244
 
246
- const videos = await query.orderBy("updated_at", "desc").select("*");
245
+ return rows.map(row => row.id);
246
+ }
247
+
248
+ /**
249
+ * Get videos by batch ID
250
+ */
251
+ async getByBatchId(batchId: number): Promise<IVideo[]> {
252
+ return this._knex("video as v")
253
+ .innerJoin("folders as f", "v.folderId", "f.id")
254
+ .select("v.*", this._knex.raw("to_jsonb(f.*) as folder"))
255
+ .where("v.batchId", batchId)
256
+ .orderBy("v.created_at", "asc");
257
+ }
247
258
 
248
- return videos;
249
- } catch (error) {
250
- console.error("Error fetching template videos:", error);
251
- throw error;
259
+ /**
260
+ * Get videos sorted chronologically by recording start time for a study
261
+ */
262
+ async getChronologicalVideos(
263
+ studyId: number,
264
+ startDate?: Date,
265
+ endDate?: Date
266
+ ): Promise<IDataPaginator<IVideo>> {
267
+ let query = this._knex("video as v")
268
+ .innerJoin("folders as f", "v.folderId", "f.id")
269
+ .select("v.*", this._knex.raw("to_jsonb(f.*) as folder"))
270
+ .where("f.studyId", studyId)
271
+ .whereNotNull("v.recordingStartedAt");
272
+
273
+ if (startDate) {
274
+ query = query.where("v.recordingStartedAt", ">=", startDate);
275
+ }
276
+
277
+ if (endDate) {
278
+ query = query.where("v.recordingStartedAt", "<=", endDate);
279
+ }
280
+
281
+ const [countResult] = await query.clone().clearSelect().count("* as count");
282
+ const totalCount = +countResult.count;
283
+ const videos = await query.clone().orderBy("v.recordingStartedAt", "asc");
284
+
285
+ return {
286
+ success: true,
287
+ data: videos,
288
+ page: 1,
289
+ limit: totalCount,
290
+ count: videos.length,
291
+ totalCount,
292
+ totalPages: 1,
293
+ };
252
294
  }
253
- }
254
-
255
- /**
256
- * Get all video IDs for a specific folder (used for cascade operations)
257
- */
258
- async getVideoIdsByFolderId(folderId: number): Promise<number[]> {
259
- const rows = await this._knex("video")
260
- .where({ folderId })
261
- .select("id")
262
- .orderBy("id", "asc");
263
-
264
- return rows.map((row) => row.id);
265
- }
266
-
267
- /**
268
- * Get videos by batch ID
269
- */
270
- async getByBatchId(batchId: number): Promise<IVideo[]> {
271
- return this._knex("video as v")
272
- .innerJoin("folders as f", "v.folderId", "f.id")
273
- .select("v.*", this._knex.raw("to_jsonb(f.*) as folder"))
274
- .where("v.batchId", batchId)
275
- .orderBy("v.created_at", "asc");
276
- }
277
-
278
- /**
279
- * Get videos sorted chronologically by recording start time for a study
280
- */
281
- async getChronologicalVideos(
282
- studyId: number,
283
- startDate?: Date,
284
- endDate?: Date,
285
- ): Promise<IDataPaginator<IVideo>> {
286
- let query = this._knex("video as v")
287
- .innerJoin("folders as f", "v.folderId", "f.id")
288
- .select("v.*", this._knex.raw("to_jsonb(f.*) as folder"))
289
- .where("f.studyId", studyId)
290
- .whereNotNull("v.recordingStartedAt");
291
-
292
- if (startDate) {
293
- query = query.where("v.recordingStartedAt", ">=", startDate);
295
+
296
+ /**
297
+ * Update recording start time for a video
298
+ */
299
+ async updateRecordingStartTime(id: number, recordingStartedAt: Date): Promise<void> {
300
+ await this._knex("video")
301
+ .where({ id })
302
+ .update({
303
+ recordingStartedAt,
304
+ updated_at: this._knex.fn.now()
305
+ });
294
306
  }
295
307
 
296
- if (endDate) {
297
- query = query.where("v.recordingStartedAt", "<=", endDate);
308
+ /**
309
+ * Update trim settings for a video
310
+ */
311
+ async updateTrimSettings(
312
+ id: number,
313
+ trimEnabled: boolean,
314
+ trimPeriods: { startTime: string; endTime: string }[] | null
315
+ ): Promise<void> {
316
+ await this._knex("video")
317
+ .where({ id })
318
+ .update({
319
+ trimEnabled,
320
+ trimPeriods: trimPeriods ? JSON.stringify(trimPeriods) : null,
321
+ updated_at: this._knex.fn.now()
322
+ });
298
323
  }
299
324
 
300
- const [countResult] = await query.clone().clearSelect().count("* as count");
301
- const totalCount = +countResult.count;
302
- const videos = await query.clone().orderBy("v.recordingStartedAt", "asc");
303
-
304
- return {
305
- success: true,
306
- data: videos,
307
- page: 1,
308
- limit: totalCount,
309
- count: videos.length,
310
- totalCount,
311
- totalPages: 1,
312
- };
313
- }
314
-
315
- /**
316
- * Update recording start time for a video
317
- */
318
- async updateRecordingStartTime(
319
- id: number,
320
- recordingStartedAt: Date,
321
- ): Promise<void> {
322
- await this._knex("video").where({ id }).update({
323
- recordingStartedAt,
324
- updated_at: this._knex.fn.now(),
325
- });
326
- }
327
-
328
- /**
329
- * Update trim settings for a video
330
- */
331
- async updateTrimSettings(
332
- id: number,
333
- trimEnabled: boolean,
334
- trimPeriods: { startTime: string; endTime: string }[] | null,
335
- ): Promise<void> {
336
- await this._knex("video")
337
- .where({ id })
338
- .update({
339
- trimEnabled,
340
- trimPeriods: trimPeriods ? JSON.stringify(trimPeriods) : null,
341
- updated_at: this._knex.fn.now(),
342
- });
343
- }
344
-
345
- /**
346
- * Get videos with trimming enabled for a folder
347
- */
348
- async getVideosWithTrimming(folderId: number): Promise<IVideo[]> {
349
- return this._knex("video as v")
350
- .innerJoin("folders as f", "v.folderId", "f.id")
351
- .select("v.*", this._knex.raw("to_jsonb(f.*) as folder"))
352
- .where("v.folderId", folderId)
353
- .where("v.trimEnabled", true)
354
- .orderBy("v.recordingStartedAt", "asc");
355
- }
356
-
357
- /**
358
- * Get paginated videos for a specific camera
359
- * @param cameraId - The camera ID
360
- * @param page - Page number
361
- * @param limit - Items per page
362
- */
363
- async getVideosByCameraId(
364
- cameraId: number,
365
- page: number,
366
- limit: number,
367
- ): Promise<IDataPaginator<IVideo>> {
368
- const offset = (page - 1) * limit;
369
-
370
- const query = this._knex("video as v")
371
- .innerJoin("folders as f", "v.folderId", "f.id")
372
- .select("v.*", this._knex.raw("to_jsonb(f.*) as folder"))
373
- .where("v.cameraId", cameraId);
374
-
375
- const [countResult] = await query.clone().clearSelect().count("* as count");
376
- const totalCount = +countResult.count;
377
-
378
- const videos = await query
379
- .clone()
380
- .limit(limit)
381
- .offset(offset)
382
- .orderBy("v.created_at", "desc");
383
-
384
- return {
385
- success: true,
386
- data: videos,
387
- page,
388
- limit,
389
- count: videos.length,
390
- totalCount,
391
- totalPages: Math.ceil(totalCount / limit),
392
- };
393
- }
325
+ /**
326
+ * Get videos with trimming enabled for a folder
327
+ */
328
+ async getVideosWithTrimming(folderId: number): Promise<IVideo[]> {
329
+ return this._knex("video as v")
330
+ .innerJoin("folders as f", "v.folderId", "f.id")
331
+ .select("v.*", this._knex.raw("to_jsonb(f.*) as folder"))
332
+ .where("v.folderId", folderId)
333
+ .where("v.trimEnabled", true)
334
+ .orderBy("v.recordingStartedAt", "asc");
335
+ }
336
+
337
+ /**
338
+ * Get paginated videos for a specific camera
339
+ * @param cameraId - The camera ID
340
+ * @param page - Page number
341
+ * @param limit - Items per page
342
+ */
343
+ async getVideosByCameraId(
344
+ cameraId: number,
345
+ page: number,
346
+ limit: number
347
+ ): Promise<IDataPaginator<IVideo>> {
348
+ const offset = (page - 1) * limit;
349
+
350
+ const query = this._knex("video as v")
351
+ .innerJoin("folders as f", "v.folderId", "f.id")
352
+ .select("v.*", this._knex.raw("to_jsonb(f.*) as folder"))
353
+ .where("v.cameraId", cameraId);
354
+
355
+ const [countResult] = await query.clone()
356
+ .clearSelect()
357
+ .count("* as count");
358
+ const totalCount = +countResult.count;
359
+
360
+ const videos = await query
361
+ .clone()
362
+ .limit(limit)
363
+ .offset(offset)
364
+ .orderBy("v.created_at", "desc");
365
+
366
+ return {
367
+ success: true,
368
+ data: videos,
369
+ page,
370
+ limit,
371
+ count: videos.length,
372
+ totalCount,
373
+ totalPages: Math.ceil(totalCount / limit)
374
+ };
375
+ }
394
376
  }