@trafficgroup/knex-rel 0.1.6 → 0.1.8

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 (37) hide show
  1. package/dist/dao/VideoMinuteResultDAO.d.ts +37 -0
  2. package/dist/dao/VideoMinuteResultDAO.js +146 -0
  3. package/dist/dao/VideoMinuteResultDAO.js.map +1 -1
  4. package/dist/dao/batch/batch.dao.d.ts +27 -0
  5. package/dist/dao/batch/batch.dao.js +135 -0
  6. package/dist/dao/batch/batch.dao.js.map +1 -0
  7. package/dist/dao/folder/folder.dao.js +3 -6
  8. package/dist/dao/folder/folder.dao.js.map +1 -1
  9. package/dist/dao/study/study.dao.d.ts +1 -0
  10. package/dist/dao/study/study.dao.js +18 -3
  11. package/dist/dao/study/study.dao.js.map +1 -1
  12. package/dist/dao/video/video.dao.d.ts +19 -5
  13. package/dist/dao/video/video.dao.js +61 -32
  14. package/dist/dao/video/video.dao.js.map +1 -1
  15. package/dist/index.d.ts +4 -1
  16. package/dist/index.js +3 -1
  17. package/dist/index.js.map +1 -1
  18. package/dist/interfaces/batch/batch.interfaces.d.ts +13 -0
  19. package/dist/interfaces/batch/batch.interfaces.js +3 -0
  20. package/dist/interfaces/batch/batch.interfaces.js.map +1 -0
  21. package/dist/interfaces/folder/folder.interfaces.d.ts +0 -3
  22. package/dist/interfaces/study/study.interfaces.d.ts +5 -0
  23. package/dist/interfaces/video/video.interfaces.d.ts +8 -3
  24. package/migrations/20251010143500_migration.ts +83 -0
  25. package/migrations/20251020225758_migration.ts +135 -0
  26. package/package.json +1 -1
  27. package/plan.md +524 -711
  28. package/src/dao/VideoMinuteResultDAO.ts +232 -0
  29. package/src/dao/batch/batch.dao.ts +121 -0
  30. package/src/dao/folder/folder.dao.ts +3 -18
  31. package/src/dao/study/study.dao.ts +34 -3
  32. package/src/dao/video/video.dao.ts +70 -43
  33. package/src/index.ts +11 -1
  34. package/src/interfaces/batch/batch.interfaces.ts +14 -0
  35. package/src/interfaces/folder/folder.interfaces.ts +0 -3
  36. package/src/interfaces/study/study.interfaces.ts +5 -0
  37. package/src/interfaces/video/video.interfaces.ts +12 -4
@@ -68,6 +68,33 @@ interface IGroupedResponse {
68
68
  };
69
69
  }
70
70
 
71
+ interface IStudyTimeGroupResult {
72
+ absoluteTime: string; // ISO 8601 start of bucket
73
+ groupIndex: number;
74
+ label: string; // Formatted label
75
+ results: ITMCResult | IATRResult;
76
+ minuteCount: number;
77
+ videoCount: number;
78
+ contributingVideos: string[]; // Video UUIDs
79
+ }
80
+
81
+ interface IGroupedStudyResponse {
82
+ success: boolean;
83
+ data: IStudyTimeGroupResult[];
84
+ groupingMinutes: number;
85
+ study: {
86
+ uuid: string;
87
+ name: string;
88
+ type: "TMC" | "ATR";
89
+ status: string;
90
+ };
91
+ videoCount: number;
92
+ dateRange: {
93
+ earliest: string;
94
+ latest: string;
95
+ };
96
+ }
97
+
71
98
  export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
72
99
  private knex = KnexManager.getConnection();
73
100
  private tableName = "video_minute_results";
@@ -486,6 +513,182 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
486
513
  };
487
514
  }
488
515
 
516
+ /**
517
+ * Get grouped minute results by study UUID with time block aggregation across all videos
518
+ * @param studyUuid - The UUID of the study
519
+ * @param groupingMinutes - Number of minutes to group together (default: 15)
520
+ */
521
+ async getGroupedMinuteResultsByStudyUuid(
522
+ studyUuid: string,
523
+ groupingMinutes: number = 15,
524
+ ): Promise<IGroupedStudyResponse> {
525
+ // Step 1: Fetch study and validate
526
+ const study = await this.knex("study").where("uuid", studyUuid).first();
527
+
528
+ if (!study) {
529
+ throw new Error(`Study with UUID ${studyUuid} not found`);
530
+ }
531
+
532
+ // Step 2: Fetch all COMPLETED videos in the study with recordingStartedAt
533
+ const videos = await this.knex("video as v")
534
+ .innerJoin("folders as f", "v.folderId", "f.id")
535
+ .select("v.id", "v.uuid", "v.name", "v.recordingStartedAt", "v.videoType")
536
+ .where("f.studyId", study.id)
537
+ .where("v.status", "COMPLETED")
538
+ .whereNotNull("v.recordingStartedAt")
539
+ .orderBy("v.recordingStartedAt", "asc");
540
+
541
+ // If no completed videos with recordingStartedAt, return empty result
542
+ if (videos.length === 0) {
543
+ return {
544
+ success: true,
545
+ data: [],
546
+ groupingMinutes,
547
+ study: {
548
+ uuid: study.uuid,
549
+ name: study.name,
550
+ type: study.type,
551
+ status: study.status,
552
+ },
553
+ videoCount: 0,
554
+ dateRange: {
555
+ earliest: "",
556
+ latest: "",
557
+ },
558
+ };
559
+ }
560
+
561
+ // Get video IDs for minute results query
562
+ const videoIds = videos.map((v) => v.id);
563
+
564
+ // Calculate date range
565
+ const earliestVideo = videos[0];
566
+ const latestVideo = videos[videos.length - 1];
567
+ const earliestTime = new Date(earliestVideo.recordingStartedAt);
568
+
569
+ // Step 3: Fetch and normalize all minute results with absolute times
570
+ const minuteResults = await this.knex("video_minute_results as vmr")
571
+ .innerJoin("video as v", "vmr.video_id", "v.id")
572
+ .select(
573
+ "vmr.minute_number",
574
+ "vmr.results",
575
+ "v.uuid as videoUuid",
576
+ "v.recordingStartedAt",
577
+ this.knex.raw(
578
+ '"v"."recordingStartedAt" + (vmr.minute_number || \' minutes\')::INTERVAL as "absoluteTime"',
579
+ ),
580
+ )
581
+ .whereIn("v.id", videoIds)
582
+ .orderBy("absoluteTime", "asc");
583
+
584
+ // If no minute results found, return empty result
585
+ if (minuteResults.length === 0) {
586
+ return {
587
+ success: true,
588
+ data: [],
589
+ groupingMinutes,
590
+ study: {
591
+ uuid: study.uuid,
592
+ name: study.name,
593
+ type: study.type,
594
+ status: study.status,
595
+ },
596
+ videoCount: videos.length,
597
+ dateRange: {
598
+ earliest: earliestVideo.recordingStartedAt.toISOString(),
599
+ latest: latestVideo.recordingStartedAt.toISOString(),
600
+ },
601
+ };
602
+ }
603
+
604
+ // Step 4: Group by time buckets in TypeScript
605
+ interface IBucket {
606
+ groupIndex: number;
607
+ absoluteTime: Date;
608
+ results: any[];
609
+ videoUuids: Set<string>;
610
+ }
611
+
612
+ const buckets = new Map<number, IBucket>();
613
+
614
+ for (const minute of minuteResults) {
615
+ const absoluteTime = new Date(minute.absoluteTime);
616
+ const minutesSinceEarliest = Math.floor(
617
+ (absoluteTime.getTime() - earliestTime.getTime()) / (1000 * 60),
618
+ );
619
+ const bucketIndex = Math.floor(minutesSinceEarliest / groupingMinutes);
620
+
621
+ if (!buckets.has(bucketIndex)) {
622
+ // Calculate bucket start time
623
+ const bucketStartMinutes = bucketIndex * groupingMinutes;
624
+ const bucketStartTime = new Date(
625
+ earliestTime.getTime() + bucketStartMinutes * 60 * 1000,
626
+ );
627
+
628
+ buckets.set(bucketIndex, {
629
+ groupIndex: bucketIndex,
630
+ absoluteTime: bucketStartTime,
631
+ results: [],
632
+ videoUuids: new Set<string>(),
633
+ });
634
+ }
635
+
636
+ const bucket = buckets.get(bucketIndex)!;
637
+ bucket.results.push(minute.results);
638
+ bucket.videoUuids.add(minute.videoUuid);
639
+ }
640
+
641
+ // Step 5: Aggregate using existing methods based on study type
642
+ const aggregatedGroups: IStudyTimeGroupResult[] = Array.from(
643
+ buckets.values(),
644
+ )
645
+ .sort((a, b) => a.groupIndex - b.groupIndex)
646
+ .map((bucket) => {
647
+ let aggregatedResult: ITMCResult | IATRResult;
648
+
649
+ if (study.type === "TMC") {
650
+ aggregatedResult = this.aggregateTMCResults(bucket.results);
651
+ } else {
652
+ aggregatedResult = this.aggregateATRResults(bucket.results);
653
+ }
654
+
655
+ return {
656
+ absoluteTime: bucket.absoluteTime.toISOString(),
657
+ groupIndex: bucket.groupIndex,
658
+ label: this.formatStudyTimeLabel(
659
+ bucket.absoluteTime,
660
+ groupingMinutes,
661
+ ),
662
+ results: aggregatedResult,
663
+ minuteCount: bucket.results.length,
664
+ videoCount: bucket.videoUuids.size,
665
+ contributingVideos: Array.from(bucket.videoUuids),
666
+ };
667
+ });
668
+
669
+ // Calculate latest time from last minute result
670
+ const lastMinute = minuteResults[minuteResults.length - 1];
671
+ const latestTime = new Date(lastMinute.absoluteTime);
672
+
673
+ // Step 6: Format response
674
+ return {
675
+ success: true,
676
+ data: aggregatedGroups,
677
+ groupingMinutes,
678
+ study: {
679
+ uuid: study.uuid,
680
+ name: study.name,
681
+ type: study.type,
682
+ status: study.status,
683
+ },
684
+ videoCount: videos.length,
685
+ dateRange: {
686
+ earliest: earliestTime.toISOString(),
687
+ latest: latestTime.toISOString(),
688
+ },
689
+ };
690
+ }
691
+
489
692
  /**
490
693
  * Aggregate minute results based on video type (TMC or ATR)
491
694
  */
@@ -728,6 +931,35 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
728
931
 
729
932
  return `${formatMinute(startMinute)}:00 - ${formatMinute(endMinute)}:59`;
730
933
  }
934
+
935
+ /**
936
+ * Format time label for study-level results (time only, no dates)
937
+ * Used when results are already grouped by date in the UI
938
+ */
939
+ private formatStudyTimeLabel(
940
+ startTime: Date,
941
+ durationMinutes: number,
942
+ ): string {
943
+ const endTime = new Date(startTime.getTime() + durationMinutes * 60 * 1000);
944
+
945
+ const formatTime = (date: Date): string => {
946
+ const hours = date.getHours().toString().padStart(2, "0");
947
+ const minutes = date.getMinutes().toString().padStart(2, "0");
948
+ return `${hours}:${minutes}`;
949
+ };
950
+
951
+ return `${formatTime(startTime)} - ${formatTime(endTime)}`;
952
+ }
731
953
  }
732
954
 
955
+ // Export interfaces for external use
956
+ export type {
957
+ IStudyTimeGroupResult,
958
+ IGroupedStudyResponse,
959
+ IGroupedResponse,
960
+ IGroupedResult,
961
+ ITMCResult,
962
+ IATRResult,
963
+ };
964
+
733
965
  export default VideoMinuteResultDAO;
@@ -0,0 +1,121 @@
1
+ import { Knex } from "knex";
2
+ import { IBaseDAO, IDataPaginator } from "../../d.types";
3
+ import { IBatch } from "../../interfaces/batch/batch.interfaces";
4
+ import KnexManager from "../../KnexConnection";
5
+
6
+ export class BatchDAO implements IBaseDAO<IBatch> {
7
+ private _knex: Knex<any, unknown[]> = KnexManager.getConnection();
8
+
9
+ async create(item: IBatch): Promise<IBatch> {
10
+ const [createdBatch] = await this._knex("video_batch")
11
+ .insert(item)
12
+ .returning("*");
13
+ return createdBatch;
14
+ }
15
+
16
+ async getById(id: number): Promise<IBatch | null> {
17
+ const batch = await this._knex("video_batch as b")
18
+ .innerJoin("folders as f", "b.folderId", "f.id")
19
+ .select("b.*", this._knex.raw("to_jsonb(f.*) as folder"))
20
+ .where("b.id", id)
21
+ .first();
22
+ return batch || null;
23
+ }
24
+
25
+ async getByUuid(uuid: string): Promise<IBatch | null> {
26
+ const batch = await this._knex("video_batch as b")
27
+ .innerJoin("folders as f", "b.folderId", "f.id")
28
+ .select("b.*", this._knex.raw("to_jsonb(f.*) as folder"))
29
+ .where("b.uuid", uuid)
30
+ .first();
31
+ return batch || null;
32
+ }
33
+
34
+ async update(id: number, item: Partial<IBatch>): Promise<IBatch | null> {
35
+ const [updatedBatch] = await this._knex("video_batch")
36
+ .where({ id })
37
+ .update(item)
38
+ .returning("*");
39
+ return updatedBatch || null;
40
+ }
41
+
42
+ async delete(id: number): Promise<boolean> {
43
+ const result = await this._knex("video_batch").where({ id }).del();
44
+ return result > 0;
45
+ }
46
+
47
+ async getAll(
48
+ page: number,
49
+ limit: number,
50
+ folderId?: number | null,
51
+ ): Promise<IDataPaginator<IBatch>> {
52
+ const offset = (page - 1) * limit;
53
+
54
+ const query = this._knex("video_batch as b")
55
+ .innerJoin("folders as f", "b.folderId", "f.id")
56
+ .select("b.*", this._knex.raw("to_jsonb(f.*) as folder"));
57
+ if (folderId !== undefined && folderId !== null) {
58
+ query.where("b.folderId", folderId);
59
+ }
60
+
61
+ const [countResult] = await query.clone().clearSelect().count("* as count");
62
+ const totalCount = +countResult.count;
63
+ const batches = await query.clone().limit(limit).offset(offset);
64
+
65
+ return {
66
+ success: true,
67
+ data: batches,
68
+ page,
69
+ limit,
70
+ count: batches.length,
71
+ totalCount,
72
+ totalPages: Math.ceil(totalCount / limit),
73
+ };
74
+ }
75
+
76
+ /**
77
+ * Get batches by folder ID
78
+ */
79
+ async getByFolderId(folderId: number): Promise<IBatch[]> {
80
+ return this._knex("video_batch as b")
81
+ .innerJoin("folders as f", "b.folderId", "f.id")
82
+ .select("b.*", this._knex.raw("to_jsonb(f.*) as folder"))
83
+ .where("b.folderId", folderId)
84
+ .orderBy("b.created_at", "desc");
85
+ }
86
+
87
+ /**
88
+ * Update batch progress
89
+ */
90
+ async updateProgress(
91
+ id: number,
92
+ completedVideos: number,
93
+ failedVideos: number,
94
+ ): Promise<void> {
95
+ await this._knex("video_batch").where({ id }).update({
96
+ completedVideos,
97
+ failedVideos,
98
+ updated_at: this._knex.fn.now(),
99
+ });
100
+ }
101
+
102
+ /**
103
+ * Mark batch as completed
104
+ */
105
+ async markCompleted(id: number): Promise<void> {
106
+ await this._knex("video_batch").where({ id }).update({
107
+ status: "COMPLETED",
108
+ updated_at: this._knex.fn.now(),
109
+ });
110
+ }
111
+
112
+ /**
113
+ * Mark batch as failed
114
+ */
115
+ async markFailed(id: number): Promise<void> {
116
+ await this._knex("video_batch").where({ id }).update({
117
+ status: "FAILED",
118
+ updated_at: this._knex.fn.now(),
119
+ });
120
+ }
121
+ }
@@ -16,12 +16,7 @@ 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
- .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
- )
19
+ .select("f.*", this._knex.raw("to_jsonb(s.*) as study"))
25
20
  .where("f.id", id)
26
21
  .first();
27
22
  return folder || null;
@@ -30,12 +25,7 @@ export class FolderDAO implements IBaseDAO<IFolder> {
30
25
  async getByUuid(uuid: string): Promise<IFolder | null> {
31
26
  const folder = await this._knex("folders as f")
32
27
  .innerJoin("study as s", "f.studyId", "s.id")
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
- )
28
+ .select("f.*", this._knex.raw("to_jsonb(s.*) as study"))
39
29
  .where("f.uuid", uuid)
40
30
  .first();
41
31
  return folder || null;
@@ -63,12 +53,7 @@ export class FolderDAO implements IBaseDAO<IFolder> {
63
53
 
64
54
  const query = this._knex("folders as f")
65
55
  .innerJoin("study as s", "f.studyId", "s.id")
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
- );
56
+ .select("f.*", this._knex.raw("to_jsonb(s.*) as study"));
72
57
  if (studyId !== undefined && studyId !== null) {
73
58
  query.where("f.studyId", studyId);
74
59
  }
@@ -16,7 +16,12 @@ export class StudyDAO implements IBaseDAO<IStudy> {
16
16
  async getById(id: number): Promise<IStudy | null> {
17
17
  const study = await this._knex("study as s")
18
18
  .innerJoin("users as u", "s.createdBy", "u.id")
19
- .select("s.*", this._knex.raw("to_jsonb(u.*) as user"))
19
+ .leftJoin("cameras as c", "s.cameraId", "c.id")
20
+ .select(
21
+ "s.*",
22
+ this._knex.raw("to_jsonb(u.*) as user"),
23
+ this._knex.raw("to_jsonb(c.*) as camera"),
24
+ )
20
25
  .where("s.id", id)
21
26
  .first();
22
27
  return study || null;
@@ -25,7 +30,12 @@ export class StudyDAO implements IBaseDAO<IStudy> {
25
30
  async getByUuid(uuid: string): Promise<IStudy | null> {
26
31
  const study = await this._knex("study as s")
27
32
  .innerJoin("users as u", "s.createdBy", "u.id")
28
- .select("s.*", this._knex.raw("to_jsonb(u.*) as user"))
33
+ .leftJoin("cameras as c", "s.cameraId", "c.id")
34
+ .select(
35
+ "s.*",
36
+ this._knex.raw("to_jsonb(u.*) as user"),
37
+ this._knex.raw("to_jsonb(c.*) as camera"),
38
+ )
29
39
  .where("s.uuid", uuid)
30
40
  .first();
31
41
  return study || null;
@@ -53,7 +63,12 @@ export class StudyDAO implements IBaseDAO<IStudy> {
53
63
 
54
64
  const query = this._knex("study as s")
55
65
  .innerJoin("users as u", "s.createdBy", "u.id")
56
- .select("s.*", this._knex.raw("to_jsonb(u.*) as user"));
66
+ .leftJoin("cameras as c", "s.cameraId", "c.id")
67
+ .select(
68
+ "s.*",
69
+ this._knex.raw("to_jsonb(u.*) as user"),
70
+ this._knex.raw("to_jsonb(c.*) as camera"),
71
+ );
57
72
  if (createdBy !== undefined && createdBy !== null) {
58
73
  query.where("s.createdBy", createdBy);
59
74
  }
@@ -72,4 +87,20 @@ export class StudyDAO implements IBaseDAO<IStudy> {
72
87
  totalPages: Math.ceil(totalCount / limit),
73
88
  };
74
89
  }
90
+
91
+ async getStudiesByCamera(cameraId: number): Promise<IStudy[]> {
92
+ const studies = await this._knex("study as s")
93
+ .innerJoin("users as u", "s.createdBy", "u.id")
94
+ .leftJoin("cameras as c", "s.cameraId", "c.id")
95
+ .select(
96
+ "s.*",
97
+ this._knex.raw("to_jsonb(u.*) as user"),
98
+ this._knex.raw("to_jsonb(c.*) as camera"),
99
+ )
100
+ .where("s.cameraId", cameraId)
101
+ .orderBy("s.created_at", "desc")
102
+ .limit(10);
103
+
104
+ return studies;
105
+ }
75
106
  }
@@ -259,65 +259,92 @@ export class VideoDAO implements IBaseDAO<IVideo> {
259
259
  }
260
260
 
261
261
  /**
262
- * Bulk update camera assignment for multiple videos
263
- * Supports optional transaction for service-level transaction management
262
+ * Get videos by batch ID
264
263
  */
265
- async bulkUpdateCamera(
266
- videoIds: number[],
267
- cameraId: number | null,
268
- trx?: Knex.Transaction,
269
- ): Promise<number> {
270
- if (!videoIds || videoIds.length === 0) {
271
- return 0;
272
- }
273
-
274
- const query = trx || this._knex;
275
-
276
- const result = await query("video")
277
- .whereIn("id", videoIds)
278
- .update({
279
- cameraId: cameraId,
280
- updated_at: query.fn.now(),
281
- })
282
- .returning("id");
283
-
284
- return result.length;
264
+ async getByBatchId(batchId: number): Promise<IVideo[]> {
265
+ return this._knex("video as v")
266
+ .innerJoin("folders as f", "v.folderId", "f.id")
267
+ .select("v.*", this._knex.raw("to_jsonb(f.*) as folder"))
268
+ .where("v.batchId", batchId)
269
+ .orderBy("v.created_at", "asc");
285
270
  }
286
271
 
287
272
  /**
288
- * Get videos by camera ID with folder information (paginated)
273
+ * Get videos sorted chronologically by recording start time for a study
289
274
  */
290
- async getVideosByCameraIdWithFolder(
291
- cameraId: number,
292
- page: number,
293
- limit: number,
275
+ async getChronologicalVideos(
276
+ studyId: number,
277
+ startDate?: Date,
278
+ endDate?: Date,
294
279
  ): Promise<IDataPaginator<IVideo>> {
295
- const offset = (page - 1) * limit;
296
-
297
- const query = this._knex("video as v")
280
+ let query = this._knex("video as v")
298
281
  .innerJoin("folders as f", "v.folderId", "f.id")
299
282
  .select("v.*", this._knex.raw("to_jsonb(f.*) as folder"))
300
- .where("v.cameraId", cameraId);
283
+ .where("f.studyId", studyId)
284
+ .whereNotNull("v.recordingStartedAt");
285
+
286
+ if (startDate) {
287
+ query = query.where("v.recordingStartedAt", ">=", startDate);
288
+ }
301
289
 
302
- // Optimized count query without JOIN
303
- const [countResult] = await this._knex("video as v")
304
- .where("v.cameraId", cameraId)
305
- .count("* as count");
290
+ if (endDate) {
291
+ query = query.where("v.recordingStartedAt", "<=", endDate);
292
+ }
293
+
294
+ const [countResult] = await query.clone().clearSelect().count("* as count");
306
295
  const totalCount = +countResult.count;
307
- const videos = await query
308
- .clone()
309
- .limit(limit)
310
- .offset(offset)
311
- .orderBy("v.created_at", "desc");
296
+ const videos = await query.clone().orderBy("v.recordingStartedAt", "asc");
312
297
 
313
298
  return {
314
299
  success: true,
315
300
  data: videos,
316
- page,
317
- limit,
301
+ page: 1,
302
+ limit: totalCount,
318
303
  count: videos.length,
319
304
  totalCount,
320
- totalPages: Math.ceil(totalCount / limit),
305
+ totalPages: 1,
321
306
  };
322
307
  }
308
+
309
+ /**
310
+ * Update recording start time for a video
311
+ */
312
+ async updateRecordingStartTime(
313
+ id: number,
314
+ recordingStartedAt: Date,
315
+ ): Promise<void> {
316
+ await this._knex("video").where({ id }).update({
317
+ recordingStartedAt,
318
+ updated_at: this._knex.fn.now(),
319
+ });
320
+ }
321
+
322
+ /**
323
+ * Update trim settings for a video
324
+ */
325
+ async updateTrimSettings(
326
+ id: number,
327
+ trimEnabled: boolean,
328
+ trimPeriods: { startTime: string; endTime: string }[] | null,
329
+ ): Promise<void> {
330
+ await this._knex("video")
331
+ .where({ id })
332
+ .update({
333
+ trimEnabled,
334
+ trimPeriods: trimPeriods ? JSON.stringify(trimPeriods) : null,
335
+ updated_at: this._knex.fn.now(),
336
+ });
337
+ }
338
+
339
+ /**
340
+ * Get videos with trimming enabled for a folder
341
+ */
342
+ async getVideosWithTrimming(folderId: number): Promise<IVideo[]> {
343
+ return this._knex("video as v")
344
+ .innerJoin("folders as f", "v.folderId", "f.id")
345
+ .select("v.*", this._knex.raw("to_jsonb(f.*) as folder"))
346
+ .where("v.folderId", folderId)
347
+ .where("v.trimEnabled", true)
348
+ .orderBy("v.recordingStartedAt", "asc");
349
+ }
323
350
  }
package/src/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  // DAOs
2
2
  export { AuthDAO } from "./dao/auth/auth.dao";
3
+ export { BatchDAO } from "./dao/batch/batch.dao";
3
4
  export { CameraDAO } from "./dao/camera/camera.dao";
4
5
  export { ChatDAO } from "./dao/chat/chat.dao";
5
6
  export { FolderDAO } from "./dao/folder/folder.dao";
@@ -14,6 +15,7 @@ export { VideoMinuteResultDAO } from "./dao/VideoMinuteResultDAO";
14
15
  // Interfaces
15
16
  export { IDataPaginator } from "./d.types";
16
17
  export { IAuth } from "./interfaces/auth/auth.interfaces";
18
+ export { IBatch } from "./interfaces/batch/batch.interfaces";
17
19
  export { ICamera } from "./interfaces/camera/camera.interfaces";
18
20
  export {
19
21
  IChat,
@@ -36,12 +38,20 @@ export {
36
38
  export { IStudy } from "./interfaces/study/study.interfaces";
37
39
  export { IUser } from "./interfaces/user/user.interfaces";
38
40
  export { IUserPushNotificationToken } from "./interfaces/user-push-notification-token/user-push-notification-token.interfaces";
39
- export { IVideo } from "./interfaces/video/video.interfaces";
41
+ export { IVideo, ITrimPeriod } from "./interfaces/video/video.interfaces";
40
42
  export {
41
43
  IVideoMinuteResult,
42
44
  IVideoMinuteResultInput,
43
45
  IVideoMinuteBatch,
44
46
  } from "./entities/VideoMinuteResult";
47
+ export type {
48
+ IStudyTimeGroupResult,
49
+ IGroupedStudyResponse,
50
+ IGroupedResponse,
51
+ IGroupedResult,
52
+ ITMCResult,
53
+ IATRResult,
54
+ } from "./dao/VideoMinuteResultDAO";
45
55
 
46
56
  import KnexManager from "./KnexConnection";
47
57
  export { KnexManager };
@@ -0,0 +1,14 @@
1
+ import type { IFolder } from "../folder/folder.interfaces";
2
+
3
+ export interface IBatch {
4
+ id: number;
5
+ uuid: string;
6
+ folderId: number;
7
+ status: "PENDING" | "IN_PROGRESS" | "COMPLETED" | "FAILED";
8
+ totalVideos: number;
9
+ completedVideos: number;
10
+ failedVideos: number;
11
+ created_at: string;
12
+ updated_at: string;
13
+ folder?: IFolder;
14
+ }
@@ -1,5 +1,4 @@
1
1
  import type { IStudy } from "../study/study.interfaces";
2
- import type { ICamera } from "../camera/camera.interfaces";
3
2
 
4
3
  export interface IFolder {
5
4
  id: number;
@@ -8,9 +7,7 @@ export interface IFolder {
8
7
  createdBy: number; // user.id
9
8
  status: "UPLOADING" | "COMPLETE";
10
9
  studyId: number; // study.id
11
- cameraId?: number; // camera.id
12
10
  created_at: string;
13
11
  updated_at: string;
14
12
  study?: IStudy;
15
- camera?: ICamera;
16
13
  }