@trafficgroup/knex-rel 0.1.7 → 0.1.9
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 +40 -0
- package/dist/dao/VideoMinuteResultDAO.js +149 -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/camera/camera.dao.d.ts +17 -7
- package/dist/dao/camera/camera.dao.js +33 -48
- package/dist/dao/camera/camera.dao.js.map +1 -1
- package/dist/dao/folder/folder.dao.js +2 -1
- package/dist/dao/folder/folder.dao.js.map +1 -1
- package/dist/dao/location/location.dao.d.ts +17 -0
- package/dist/dao/location/location.dao.js +123 -0
- package/dist/dao/location/location.dao.js.map +1 -0
- package/dist/dao/study/study.dao.d.ts +1 -1
- package/dist/dao/study/study.dao.js +10 -10
- package/dist/dao/study/study.dao.js.map +1 -1
- package/dist/dao/video/video.dao.d.ts +30 -0
- package/dist/dao/video/video.dao.js +113 -1
- package/dist/dao/video/video.dao.js.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +5 -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/camera/camera.interfaces.d.ts +4 -2
- package/dist/interfaces/location/location.interfaces.d.ts +9 -0
- package/dist/interfaces/location/location.interfaces.js +3 -0
- package/dist/interfaces/location/location.interfaces.js.map +1 -0
- package/dist/interfaces/study/study.interfaces.d.ts +4 -3
- package/dist/interfaces/video/video.interfaces.d.ts +9 -0
- package/migrations/20251020225758_migration.ts +135 -0
- package/migrations/20251112120000_migration.ts +89 -0
- package/migrations/20251112120100_migration.ts +21 -0
- package/migrations/20251112120200_migration.ts +50 -0
- package/migrations/20251112120300_migration.ts +27 -0
- package/package.json +1 -1
- package/src/dao/VideoMinuteResultDAO.ts +237 -0
- package/src/dao/batch/batch.dao.ts +121 -0
- package/src/dao/camera/camera.dao.ts +44 -61
- package/src/dao/folder/folder.dao.ts +7 -1
- package/src/dao/location/location.dao.ts +123 -0
- package/src/dao/study/study.dao.ts +10 -10
- package/src/dao/video/video.dao.ts +135 -1
- package/src/index.ts +13 -1
- package/src/interfaces/batch/batch.interfaces.ts +14 -0
- package/src/interfaces/camera/camera.interfaces.ts +4 -2
- package/src/interfaces/location/location.interfaces.ts +9 -0
- package/src/interfaces/study/study.interfaces.ts +4 -3
- package/src/interfaces/video/video.interfaces.ts +13 -1
|
@@ -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
|
+
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { Knex } from "knex";
|
|
2
2
|
import { IBaseDAO, IDataPaginator } from "../../d.types";
|
|
3
3
|
import { ICamera } from "../../interfaces/camera/camera.interfaces";
|
|
4
|
-
import { IVideo } from "../../interfaces/video/video.interfaces";
|
|
5
4
|
import KnexManager from "../../KnexConnection";
|
|
6
5
|
|
|
7
6
|
export class CameraDAO implements IBaseDAO<ICamera> {
|
|
@@ -55,45 +54,43 @@ export class CameraDAO implements IBaseDAO<ICamera> {
|
|
|
55
54
|
};
|
|
56
55
|
}
|
|
57
56
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
async
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
radiusKm: number = 1,
|
|
57
|
+
/**
|
|
58
|
+
* Get cameras for a specific location
|
|
59
|
+
* @param locationId - The location ID
|
|
60
|
+
* @param status - Optional filter by camera status
|
|
61
|
+
*/
|
|
62
|
+
async getCamerasByLocationId(
|
|
63
|
+
locationId: number,
|
|
64
|
+
status?: string,
|
|
67
65
|
): Promise<ICamera[]> {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
)`,
|
|
76
|
-
[longitude, latitude, radiusKm * 1000], // Convert km to meters
|
|
77
|
-
);
|
|
78
|
-
return cameras;
|
|
66
|
+
let query = this._knex("cameras").where("locationId", locationId);
|
|
67
|
+
|
|
68
|
+
if (status) {
|
|
69
|
+
query = query.where("status", status);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return query.orderBy("name", "asc");
|
|
79
73
|
}
|
|
80
74
|
|
|
81
75
|
/**
|
|
82
|
-
* Get
|
|
76
|
+
* Get cameras for a specific location with pagination
|
|
77
|
+
* @param locationId - The location ID
|
|
78
|
+
* @param page - Page number
|
|
79
|
+
* @param limit - Items per page
|
|
80
|
+
* @param status - Optional filter by camera status
|
|
83
81
|
*/
|
|
84
|
-
async
|
|
82
|
+
async getCamerasByLocationIdPaginated(
|
|
83
|
+
locationId: number,
|
|
85
84
|
page: number,
|
|
86
85
|
limit: number,
|
|
87
|
-
|
|
86
|
+
status?: string,
|
|
88
87
|
): Promise<IDataPaginator<ICamera>> {
|
|
89
88
|
const offset = (page - 1) * limit;
|
|
90
89
|
|
|
91
|
-
let query = this._knex("cameras");
|
|
90
|
+
let query = this._knex("cameras").where("locationId", locationId);
|
|
92
91
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const escapedName = name.trim().replace(/[%_\\]/g, "\\$&");
|
|
96
|
-
query = query.where("name", "ilike", `%${escapedName}%`);
|
|
92
|
+
if (status) {
|
|
93
|
+
query = query.where("status", status);
|
|
97
94
|
}
|
|
98
95
|
|
|
99
96
|
const [countResult] = await query.clone().count("* as count");
|
|
@@ -116,39 +113,25 @@ export class CameraDAO implements IBaseDAO<ICamera> {
|
|
|
116
113
|
}
|
|
117
114
|
|
|
118
115
|
/**
|
|
119
|
-
*
|
|
116
|
+
* Check if a camera name already exists for a location
|
|
117
|
+
* @param name - Camera name
|
|
118
|
+
* @param locationId - Location ID
|
|
119
|
+
* @param excludeId - Optional camera ID to exclude (for update operations)
|
|
120
120
|
*/
|
|
121
|
-
async
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
): Promise<
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
.
|
|
132
|
-
|
|
133
|
-
// Optimized count query without JOIN
|
|
134
|
-
const [countResult] = await this._knex("video as v")
|
|
135
|
-
.where("v.cameraId", cameraId)
|
|
136
|
-
.count("* as count");
|
|
137
|
-
const totalCount = +countResult.count;
|
|
138
|
-
const videos = await query
|
|
139
|
-
.clone()
|
|
140
|
-
.limit(limit)
|
|
141
|
-
.offset(offset)
|
|
142
|
-
.orderBy("v.created_at", "desc");
|
|
121
|
+
async existsByNameAndLocation(
|
|
122
|
+
name: string,
|
|
123
|
+
locationId: number,
|
|
124
|
+
excludeId?: number,
|
|
125
|
+
): Promise<boolean> {
|
|
126
|
+
let query = this._knex("cameras")
|
|
127
|
+
.where("locationId", locationId)
|
|
128
|
+
.where("name", name);
|
|
129
|
+
|
|
130
|
+
if (excludeId !== undefined) {
|
|
131
|
+
query = query.whereNot("id", excludeId);
|
|
132
|
+
}
|
|
143
133
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
data: videos,
|
|
147
|
-
page,
|
|
148
|
-
limit,
|
|
149
|
-
count: videos.length,
|
|
150
|
-
totalCount,
|
|
151
|
-
totalPages: Math.ceil(totalCount / limit),
|
|
152
|
-
};
|
|
134
|
+
const result = await query.first();
|
|
135
|
+
return result !== null && result !== undefined;
|
|
153
136
|
}
|
|
154
137
|
}
|
|
@@ -53,7 +53,13 @@ export class FolderDAO implements IBaseDAO<IFolder> {
|
|
|
53
53
|
|
|
54
54
|
const query = this._knex("folders as f")
|
|
55
55
|
.innerJoin("study as s", "f.studyId", "s.id")
|
|
56
|
-
.
|
|
56
|
+
.leftJoin("locations as l", "s.locationId", "l.id")
|
|
57
|
+
.select(
|
|
58
|
+
"f.*",
|
|
59
|
+
this._knex.raw("to_jsonb(s.*) as study"),
|
|
60
|
+
"l.name as locationName",
|
|
61
|
+
"l.uuid as locationUuid",
|
|
62
|
+
);
|
|
57
63
|
if (studyId !== undefined && studyId !== null) {
|
|
58
64
|
query.where("f.studyId", studyId);
|
|
59
65
|
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { Knex } from "knex";
|
|
2
|
+
import { IBaseDAO, IDataPaginator } from "../../d.types";
|
|
3
|
+
import { ILocation } from "../../interfaces/location/location.interfaces";
|
|
4
|
+
import { IVideo } from "../../interfaces/video/video.interfaces";
|
|
5
|
+
import KnexManager from "../../KnexConnection";
|
|
6
|
+
|
|
7
|
+
export class LocationDAO implements IBaseDAO<ILocation> {
|
|
8
|
+
private _knex: Knex<any, unknown[]> = KnexManager.getConnection();
|
|
9
|
+
|
|
10
|
+
async create(item: ILocation): Promise<ILocation> {
|
|
11
|
+
const [createdLocation] = await this._knex("locations")
|
|
12
|
+
.insert(item)
|
|
13
|
+
.returning("*");
|
|
14
|
+
return createdLocation;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async getById(id: number): Promise<ILocation | null> {
|
|
18
|
+
const location = await this._knex("locations").where({ id }).first();
|
|
19
|
+
return location || null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async getByUuid(uuid: string): Promise<ILocation | null> {
|
|
23
|
+
const location = await this._knex("locations").where({ uuid }).first();
|
|
24
|
+
return location || null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async update(
|
|
28
|
+
id: number,
|
|
29
|
+
item: Partial<ILocation>,
|
|
30
|
+
): Promise<ILocation | null> {
|
|
31
|
+
const [updatedLocation] = await this._knex("locations")
|
|
32
|
+
.where({ id })
|
|
33
|
+
.update(item)
|
|
34
|
+
.returning("*");
|
|
35
|
+
return updatedLocation || null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async delete(id: number): Promise<boolean> {
|
|
39
|
+
const result = await this._knex("locations").where({ id }).del();
|
|
40
|
+
return result > 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async getAll(
|
|
44
|
+
page: number,
|
|
45
|
+
limit: number,
|
|
46
|
+
): Promise<IDataPaginator<ILocation>> {
|
|
47
|
+
const offset = (page - 1) * limit;
|
|
48
|
+
|
|
49
|
+
const [countResult] = await this._knex("locations").count("* as count");
|
|
50
|
+
const totalCount = +countResult.count;
|
|
51
|
+
const locations = await this._knex("locations").limit(limit).offset(offset);
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
success: true,
|
|
55
|
+
data: locations,
|
|
56
|
+
page,
|
|
57
|
+
limit,
|
|
58
|
+
count: locations.length,
|
|
59
|
+
totalCount,
|
|
60
|
+
totalPages: Math.ceil(totalCount / limit),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async getByName(name: string): Promise<ILocation | null> {
|
|
65
|
+
const location = await this._knex("locations").where({ name }).first();
|
|
66
|
+
return location || null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async getLocationsNearCoordinates(
|
|
70
|
+
longitude: number,
|
|
71
|
+
latitude: number,
|
|
72
|
+
radiusKm: number = 1,
|
|
73
|
+
): Promise<ILocation[]> {
|
|
74
|
+
// Using ST_DWithin for geographic distance calculation
|
|
75
|
+
// This is a PostgreSQL-specific query for geospatial operations
|
|
76
|
+
const locations = await this._knex("locations").whereRaw(
|
|
77
|
+
`ST_DWithin(
|
|
78
|
+
ST_MakePoint(longitude, latitude)::geography,
|
|
79
|
+
ST_MakePoint(?, ?)::geography,
|
|
80
|
+
?
|
|
81
|
+
)`,
|
|
82
|
+
[longitude, latitude, radiusKm * 1000], // Convert km to meters
|
|
83
|
+
);
|
|
84
|
+
return locations;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Get all locations with optional search filter by name (case-insensitive partial match)
|
|
89
|
+
*/
|
|
90
|
+
async getAllWithSearch(
|
|
91
|
+
page: number,
|
|
92
|
+
limit: number,
|
|
93
|
+
name?: string,
|
|
94
|
+
): Promise<IDataPaginator<ILocation>> {
|
|
95
|
+
const offset = (page - 1) * limit;
|
|
96
|
+
|
|
97
|
+
let query = this._knex("locations");
|
|
98
|
+
|
|
99
|
+
// Apply search filter if name provided (escape special chars to prevent pattern injection)
|
|
100
|
+
if (name && name.trim().length > 0) {
|
|
101
|
+
const escapedName = name.trim().replace(/[%_\\]/g, "\\$&");
|
|
102
|
+
query = query.where("name", "ilike", `%${escapedName}%`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const [countResult] = await query.clone().count("* as count");
|
|
106
|
+
const totalCount = +countResult.count;
|
|
107
|
+
const locations = await query
|
|
108
|
+
.clone()
|
|
109
|
+
.limit(limit)
|
|
110
|
+
.offset(offset)
|
|
111
|
+
.orderBy("name", "asc");
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
success: true,
|
|
115
|
+
data: locations,
|
|
116
|
+
page,
|
|
117
|
+
limit,
|
|
118
|
+
count: locations.length,
|
|
119
|
+
totalCount,
|
|
120
|
+
totalPages: Math.ceil(totalCount / limit),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -16,11 +16,11 @@ 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
|
-
.leftJoin("
|
|
19
|
+
.leftJoin("locations as l", "s.locationId", "l.id")
|
|
20
20
|
.select(
|
|
21
21
|
"s.*",
|
|
22
22
|
this._knex.raw("to_jsonb(u.*) as user"),
|
|
23
|
-
this._knex.raw("to_jsonb(
|
|
23
|
+
this._knex.raw("to_jsonb(l.*) as location"),
|
|
24
24
|
)
|
|
25
25
|
.where("s.id", id)
|
|
26
26
|
.first();
|
|
@@ -30,11 +30,11 @@ export class StudyDAO implements IBaseDAO<IStudy> {
|
|
|
30
30
|
async getByUuid(uuid: string): Promise<IStudy | null> {
|
|
31
31
|
const study = await this._knex("study as s")
|
|
32
32
|
.innerJoin("users as u", "s.createdBy", "u.id")
|
|
33
|
-
.leftJoin("
|
|
33
|
+
.leftJoin("locations as l", "s.locationId", "l.id")
|
|
34
34
|
.select(
|
|
35
35
|
"s.*",
|
|
36
36
|
this._knex.raw("to_jsonb(u.*) as user"),
|
|
37
|
-
this._knex.raw("to_jsonb(
|
|
37
|
+
this._knex.raw("to_jsonb(l.*) as location"),
|
|
38
38
|
)
|
|
39
39
|
.where("s.uuid", uuid)
|
|
40
40
|
.first();
|
|
@@ -63,11 +63,11 @@ export class StudyDAO implements IBaseDAO<IStudy> {
|
|
|
63
63
|
|
|
64
64
|
const query = this._knex("study as s")
|
|
65
65
|
.innerJoin("users as u", "s.createdBy", "u.id")
|
|
66
|
-
.leftJoin("
|
|
66
|
+
.leftJoin("locations as l", "s.locationId", "l.id")
|
|
67
67
|
.select(
|
|
68
68
|
"s.*",
|
|
69
69
|
this._knex.raw("to_jsonb(u.*) as user"),
|
|
70
|
-
this._knex.raw("to_jsonb(
|
|
70
|
+
this._knex.raw("to_jsonb(l.*) as location"),
|
|
71
71
|
);
|
|
72
72
|
if (createdBy !== undefined && createdBy !== null) {
|
|
73
73
|
query.where("s.createdBy", createdBy);
|
|
@@ -88,16 +88,16 @@ export class StudyDAO implements IBaseDAO<IStudy> {
|
|
|
88
88
|
};
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
async
|
|
91
|
+
async getStudiesByLocation(locationId: number): Promise<IStudy[]> {
|
|
92
92
|
const studies = await this._knex("study as s")
|
|
93
93
|
.innerJoin("users as u", "s.createdBy", "u.id")
|
|
94
|
-
.leftJoin("
|
|
94
|
+
.leftJoin("locations as l", "s.locationId", "l.id")
|
|
95
95
|
.select(
|
|
96
96
|
"s.*",
|
|
97
97
|
this._knex.raw("to_jsonb(u.*) as user"),
|
|
98
|
-
this._knex.raw("to_jsonb(
|
|
98
|
+
this._knex.raw("to_jsonb(l.*) as location"),
|
|
99
99
|
)
|
|
100
|
-
.where("s.
|
|
100
|
+
.where("s.locationId", locationId)
|
|
101
101
|
.orderBy("s.created_at", "desc")
|
|
102
102
|
.limit(10);
|
|
103
103
|
|
|
@@ -58,7 +58,13 @@ export class VideoDAO implements IBaseDAO<IVideo> {
|
|
|
58
58
|
|
|
59
59
|
const query = this._knex("video as v")
|
|
60
60
|
.innerJoin("folders as f", "v.folderId", "f.id")
|
|
61
|
-
.
|
|
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
|
+
);
|
|
62
68
|
if (folderId !== undefined && folderId !== null) {
|
|
63
69
|
query.where("v.folderId", folderId);
|
|
64
70
|
}
|
|
@@ -257,4 +263,132 @@ export class VideoDAO implements IBaseDAO<IVideo> {
|
|
|
257
263
|
|
|
258
264
|
return rows.map((row) => row.id);
|
|
259
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);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (endDate) {
|
|
297
|
+
query = query.where("v.recordingStartedAt", "<=", endDate);
|
|
298
|
+
}
|
|
299
|
+
|
|
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
|
+
}
|
|
260
394
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
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";
|
|
7
|
+
export { LocationDAO } from "./dao/location/location.dao";
|
|
6
8
|
export { MessageDAO } from "./dao/message/message.dao";
|
|
7
9
|
export { ReportConfigurationDAO } from "./dao/report-configuration/report-configuration.dao";
|
|
8
10
|
export { StudyDAO } from "./dao/study/study.dao";
|
|
@@ -14,6 +16,7 @@ export { VideoMinuteResultDAO } from "./dao/VideoMinuteResultDAO";
|
|
|
14
16
|
// Interfaces
|
|
15
17
|
export { IDataPaginator } from "./d.types";
|
|
16
18
|
export { IAuth } from "./interfaces/auth/auth.interfaces";
|
|
19
|
+
export { IBatch } from "./interfaces/batch/batch.interfaces";
|
|
17
20
|
export { ICamera } from "./interfaces/camera/camera.interfaces";
|
|
18
21
|
export {
|
|
19
22
|
IChat,
|
|
@@ -21,6 +24,7 @@ export {
|
|
|
21
24
|
IChatUpdate,
|
|
22
25
|
} from "./interfaces/chat/chat.interfaces";
|
|
23
26
|
export { IFolder } from "./interfaces/folder/folder.interfaces";
|
|
27
|
+
export { ILocation } from "./interfaces/location/location.interfaces";
|
|
24
28
|
export {
|
|
25
29
|
IMessage,
|
|
26
30
|
IMessageCreate,
|
|
@@ -36,12 +40,20 @@ export {
|
|
|
36
40
|
export { IStudy } from "./interfaces/study/study.interfaces";
|
|
37
41
|
export { IUser } from "./interfaces/user/user.interfaces";
|
|
38
42
|
export { IUserPushNotificationToken } from "./interfaces/user-push-notification-token/user-push-notification-token.interfaces";
|
|
39
|
-
export { IVideo } from "./interfaces/video/video.interfaces";
|
|
43
|
+
export { IVideo, ITrimPeriod } from "./interfaces/video/video.interfaces";
|
|
40
44
|
export {
|
|
41
45
|
IVideoMinuteResult,
|
|
42
46
|
IVideoMinuteResultInput,
|
|
43
47
|
IVideoMinuteBatch,
|
|
44
48
|
} from "./entities/VideoMinuteResult";
|
|
49
|
+
export type {
|
|
50
|
+
IStudyTimeGroupResult,
|
|
51
|
+
IGroupedStudyResponse,
|
|
52
|
+
IGroupedResponse,
|
|
53
|
+
IGroupedResult,
|
|
54
|
+
ITMCResult,
|
|
55
|
+
IATRResult,
|
|
56
|
+
} from "./dao/VideoMinuteResultDAO";
|
|
45
57
|
|
|
46
58
|
import KnexManager from "./KnexConnection";
|
|
47
59
|
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,9 +1,11 @@
|
|
|
1
1
|
export interface ICamera {
|
|
2
2
|
id: number;
|
|
3
3
|
uuid: string;
|
|
4
|
+
locationId: number;
|
|
4
5
|
name: string;
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
description?: string;
|
|
7
|
+
status: "ACTIVE" | "INACTIVE" | "MAINTENANCE";
|
|
8
|
+
metadata: Record<string, any>;
|
|
7
9
|
created_at: string;
|
|
8
10
|
updated_at: string;
|
|
9
11
|
}
|