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