@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.
- package/dist/dao/VideoMinuteResultDAO.d.ts +37 -0
- package/dist/dao/VideoMinuteResultDAO.js +146 -0
- package/dist/dao/VideoMinuteResultDAO.js.map +1 -1
- package/dist/dao/batch/batch.dao.d.ts +27 -0
- package/dist/dao/batch/batch.dao.js +135 -0
- package/dist/dao/batch/batch.dao.js.map +1 -0
- package/dist/dao/folder/folder.dao.js +3 -6
- package/dist/dao/folder/folder.dao.js.map +1 -1
- package/dist/dao/study/study.dao.d.ts +1 -0
- package/dist/dao/study/study.dao.js +18 -3
- package/dist/dao/study/study.dao.js.map +1 -1
- package/dist/dao/video/video.dao.d.ts +19 -5
- package/dist/dao/video/video.dao.js +61 -32
- package/dist/dao/video/video.dao.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/interfaces/batch/batch.interfaces.d.ts +13 -0
- package/dist/interfaces/batch/batch.interfaces.js +3 -0
- package/dist/interfaces/batch/batch.interfaces.js.map +1 -0
- package/dist/interfaces/folder/folder.interfaces.d.ts +0 -3
- package/dist/interfaces/study/study.interfaces.d.ts +5 -0
- package/dist/interfaces/video/video.interfaces.d.ts +8 -3
- package/migrations/20251010143500_migration.ts +83 -0
- package/migrations/20251020225758_migration.ts +135 -0
- package/package.json +1 -1
- package/plan.md +524 -711
- package/src/dao/VideoMinuteResultDAO.ts +232 -0
- package/src/dao/batch/batch.dao.ts +121 -0
- package/src/dao/folder/folder.dao.ts +3 -18
- package/src/dao/study/study.dao.ts +34 -3
- package/src/dao/video/video.dao.ts +70 -43
- package/src/index.ts +11 -1
- package/src/interfaces/batch/batch.interfaces.ts +14 -0
- package/src/interfaces/folder/folder.interfaces.ts +0 -3
- package/src/interfaces/study/study.interfaces.ts +5 -0
- 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
|
-
.
|
|
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
|
-
.
|
|
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
|
-
.
|
|
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
|
-
.
|
|
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
|
-
.
|
|
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
|
-
.
|
|
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
|
-
*
|
|
263
|
-
* Supports optional transaction for service-level transaction management
|
|
262
|
+
* Get videos by batch ID
|
|
264
263
|
*/
|
|
265
|
-
async
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
|
273
|
+
* Get videos sorted chronologically by recording start time for a study
|
|
289
274
|
*/
|
|
290
|
-
async
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
275
|
+
async getChronologicalVideos(
|
|
276
|
+
studyId: number,
|
|
277
|
+
startDate?: Date,
|
|
278
|
+
endDate?: Date,
|
|
294
279
|
): Promise<IDataPaginator<IVideo>> {
|
|
295
|
-
|
|
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("
|
|
283
|
+
.where("f.studyId", studyId)
|
|
284
|
+
.whereNotNull("v.recordingStartedAt");
|
|
285
|
+
|
|
286
|
+
if (startDate) {
|
|
287
|
+
query = query.where("v.recordingStartedAt", ">=", startDate);
|
|
288
|
+
}
|
|
301
289
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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:
|
|
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
|
}
|