@trafficgroup/knex-rel 0.1.0 → 0.1.1
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/camera/camera.dao.d.ts +13 -0
- package/dist/dao/camera/camera.dao.js +93 -0
- package/dist/dao/camera/camera.dao.js.map +1 -0
- package/dist/dao/video/video.dao.d.ts +5 -0
- package/dist/dao/video/video.dao.js +46 -0
- package/dist/dao/video/video.dao.js.map +1 -1
- package/dist/index.d.ts +12 -10
- package/dist/index.js +13 -11
- package/dist/index.js.map +1 -1
- package/dist/interfaces/camera/camera.interfaces.d.ts +9 -0
- package/dist/interfaces/camera/camera.interfaces.js +3 -0
- package/dist/interfaces/camera/camera.interfaces.js.map +1 -0
- package/dist/interfaces/folder/folder.interfaces.d.ts +3 -0
- package/dist/interfaces/video/video.interfaces.d.ts +4 -0
- package/migrations/20250911000000_migration.ts +61 -0
- package/migrations/20250917144153_migration.ts +37 -0
- package/package.json +1 -1
- package/plan.md +129 -0
- package/src/dao/camera/camera.dao.ts +79 -0
- package/src/dao/video/video.dao.ts +49 -0
- package/src/index.ts +12 -10
- package/src/interfaces/camera/camera.interfaces.ts +9 -0
- package/src/interfaces/folder/folder.interfaces.ts +3 -0
- package/src/interfaces/video/video.interfaces.ts +4 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { IBaseDAO, IDataPaginator } from "../../d.types";
|
|
2
|
+
import { ICamera } from "../../interfaces/camera/camera.interfaces";
|
|
3
|
+
export declare class CameraDAO implements IBaseDAO<ICamera> {
|
|
4
|
+
private _knex;
|
|
5
|
+
create(item: ICamera): Promise<ICamera>;
|
|
6
|
+
getById(id: number): Promise<ICamera | null>;
|
|
7
|
+
getByUuid(uuid: string): Promise<ICamera | null>;
|
|
8
|
+
update(id: number, item: Partial<ICamera>): Promise<ICamera | null>;
|
|
9
|
+
delete(id: number): Promise<boolean>;
|
|
10
|
+
getAll(page: number, limit: number): Promise<IDataPaginator<ICamera>>;
|
|
11
|
+
getByName(name: string): Promise<ICamera | null>;
|
|
12
|
+
getCamerasNearCoordinates(longitude: number, latitude: number, radiusKm?: number): Promise<ICamera[]>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.CameraDAO = void 0;
|
|
16
|
+
const KnexConnection_1 = __importDefault(require("../../KnexConnection"));
|
|
17
|
+
class CameraDAO {
|
|
18
|
+
constructor() {
|
|
19
|
+
this._knex = KnexConnection_1.default.getConnection();
|
|
20
|
+
}
|
|
21
|
+
create(item) {
|
|
22
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
23
|
+
const [createdCamera] = yield this._knex("cameras")
|
|
24
|
+
.insert(item)
|
|
25
|
+
.returning("*");
|
|
26
|
+
return createdCamera;
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
getById(id) {
|
|
30
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
31
|
+
const camera = yield this._knex("cameras").where({ id }).first();
|
|
32
|
+
return camera || null;
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
getByUuid(uuid) {
|
|
36
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
37
|
+
const camera = yield this._knex("cameras").where({ uuid }).first();
|
|
38
|
+
return camera || null;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
update(id, item) {
|
|
42
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
43
|
+
const [updatedCamera] = yield this._knex("cameras")
|
|
44
|
+
.where({ id })
|
|
45
|
+
.update(item)
|
|
46
|
+
.returning("*");
|
|
47
|
+
return updatedCamera || null;
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
delete(id) {
|
|
51
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
52
|
+
const result = yield this._knex("cameras").where({ id }).del();
|
|
53
|
+
return result > 0;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
getAll(page, limit) {
|
|
57
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
58
|
+
const offset = (page - 1) * limit;
|
|
59
|
+
const [countResult] = yield this._knex("cameras").count("* as count");
|
|
60
|
+
const totalCount = +countResult.count;
|
|
61
|
+
const cameras = yield this._knex("cameras").limit(limit).offset(offset);
|
|
62
|
+
return {
|
|
63
|
+
success: true,
|
|
64
|
+
data: cameras,
|
|
65
|
+
page,
|
|
66
|
+
limit,
|
|
67
|
+
count: cameras.length,
|
|
68
|
+
totalCount,
|
|
69
|
+
totalPages: Math.ceil(totalCount / limit),
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
getByName(name) {
|
|
74
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
75
|
+
const camera = yield this._knex("cameras").where({ name }).first();
|
|
76
|
+
return camera || null;
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
getCamerasNearCoordinates(longitude_1, latitude_1) {
|
|
80
|
+
return __awaiter(this, arguments, void 0, function* (longitude, latitude, radiusKm = 1) {
|
|
81
|
+
// Using ST_DWithin for geographic distance calculation
|
|
82
|
+
// This is a PostgreSQL-specific query for geospatial operations
|
|
83
|
+
const cameras = yield this._knex("cameras").whereRaw(`ST_DWithin(
|
|
84
|
+
ST_MakePoint(longitude, latitude)::geography,
|
|
85
|
+
ST_MakePoint(?, ?)::geography,
|
|
86
|
+
?
|
|
87
|
+
)`, [longitude, latitude, radiusKm * 1000]);
|
|
88
|
+
return cameras;
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
exports.CameraDAO = CameraDAO;
|
|
93
|
+
//# sourceMappingURL=camera.dao.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"camera.dao.js","sourceRoot":"","sources":["../../../src/dao/camera/camera.dao.ts"],"names":[],"mappings":"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"}
|
|
@@ -25,4 +25,9 @@ export declare class VideoDAO implements IBaseDAO<IVideo> {
|
|
|
25
25
|
* Get videos that don't have minute-by-minute data yet
|
|
26
26
|
*/
|
|
27
27
|
getVideosWithoutMinuteData(page: number, limit: number): Promise<IDataPaginator<IVideo>>;
|
|
28
|
+
/**
|
|
29
|
+
* Get videos from same folder with same type that have metadata and are COMPLETED
|
|
30
|
+
* Suitable for use as lane annotation templates
|
|
31
|
+
*/
|
|
32
|
+
getTemplateVideos(folderId: number, videoType: string): Promise<IVideo[]>;
|
|
28
33
|
}
|
|
@@ -166,6 +166,52 @@ class VideoDAO {
|
|
|
166
166
|
};
|
|
167
167
|
});
|
|
168
168
|
}
|
|
169
|
+
/**
|
|
170
|
+
* Get videos from same folder with same type that have metadata and are COMPLETED
|
|
171
|
+
* Suitable for use as lane annotation templates
|
|
172
|
+
*/
|
|
173
|
+
getTemplateVideos(folderId, videoType) {
|
|
174
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
175
|
+
try {
|
|
176
|
+
let query = this._knex("video")
|
|
177
|
+
.where("folderId", folderId)
|
|
178
|
+
.where("videoType", videoType)
|
|
179
|
+
.where("status", "COMPLETED")
|
|
180
|
+
.whereNotNull("metadata")
|
|
181
|
+
.whereRaw("metadata != '{}'");
|
|
182
|
+
// Apply video type specific metadata validation
|
|
183
|
+
if (videoType === "ATR") {
|
|
184
|
+
// ATR videos use lanes array structure
|
|
185
|
+
query = query.whereRaw("jsonb_array_length(metadata->'lanes') > 0");
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
// TMC/JUNCTION/ROUNDABOUT/PATHWAY videos use objects with pt1/pt2 structure
|
|
189
|
+
// Check if metadata has at least one key with pt1 and pt2 properties
|
|
190
|
+
query = query.whereRaw(`
|
|
191
|
+
EXISTS (
|
|
192
|
+
SELECT 1
|
|
193
|
+
FROM jsonb_each(metadata) as entry(key, value)
|
|
194
|
+
WHERE key != 'lanes'
|
|
195
|
+
AND key != 'finish_line'
|
|
196
|
+
AND jsonb_typeof(value) = 'object'
|
|
197
|
+
AND value ? 'pt1'
|
|
198
|
+
AND value ? 'pt2'
|
|
199
|
+
AND jsonb_typeof(value->'pt1') = 'array'
|
|
200
|
+
AND jsonb_typeof(value->'pt2') = 'array'
|
|
201
|
+
AND jsonb_array_length(value->'pt1') = 2
|
|
202
|
+
AND jsonb_array_length(value->'pt2') = 2
|
|
203
|
+
)
|
|
204
|
+
`);
|
|
205
|
+
}
|
|
206
|
+
const videos = yield query.orderBy("updated_at", "desc").select("*");
|
|
207
|
+
return videos;
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
console.error("Error fetching template videos:", error);
|
|
211
|
+
throw error;
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
}
|
|
169
215
|
}
|
|
170
216
|
exports.VideoDAO = VideoDAO;
|
|
171
217
|
//# sourceMappingURL=video.dao.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"video.dao.js","sourceRoot":"","sources":["../../../src/dao/video/video.dao.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAGA,0EAA+C;AAE/C,MAAa,QAAQ;IAArB;QACU,UAAK,GAAyB,wBAAW,CAAC,aAAa,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"video.dao.js","sourceRoot":"","sources":["../../../src/dao/video/video.dao.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,21 +1,23 @@
|
|
|
1
|
-
export { UserDAO } from "./dao/user/user.dao";
|
|
2
|
-
export { StudyDAO } from "./dao/study/study.dao";
|
|
3
|
-
export { FolderDAO } from "./dao/folder/folder.dao";
|
|
4
|
-
export { VideoDAO } from "./dao/video/video.dao";
|
|
5
1
|
export { AuthDAO } from "./dao/auth/auth.dao";
|
|
6
|
-
export {
|
|
2
|
+
export { CameraDAO } from "./dao/camera/camera.dao";
|
|
7
3
|
export { ChatDAO } from "./dao/chat/chat.dao";
|
|
4
|
+
export { FolderDAO } from "./dao/folder/folder.dao";
|
|
8
5
|
export { MessageDAO } from "./dao/message/message.dao";
|
|
6
|
+
export { StudyDAO } from "./dao/study/study.dao";
|
|
7
|
+
export { UserDAO } from "./dao/user/user.dao";
|
|
8
|
+
export { UserPushNotificationTokenDAO } from "./dao/user-push-notification-token/user-push-notification-token.dao";
|
|
9
|
+
export { VideoDAO } from "./dao/video/video.dao";
|
|
9
10
|
export { VideoMinuteResultDAO } from "./dao/VideoMinuteResultDAO";
|
|
10
11
|
export { IDataPaginator } from "./d.types";
|
|
11
|
-
export { IUser } from "./interfaces/user/user.interfaces";
|
|
12
|
-
export { IStudy } from "./interfaces/study/study.interfaces";
|
|
13
|
-
export { IFolder } from "./interfaces/folder/folder.interfaces";
|
|
14
|
-
export { IVideo } from "./interfaces/video/video.interfaces";
|
|
15
12
|
export { IAuth } from "./interfaces/auth/auth.interfaces";
|
|
16
|
-
export {
|
|
13
|
+
export { ICamera } from "./interfaces/camera/camera.interfaces";
|
|
17
14
|
export { IChat, IChatCreate, IChatUpdate, } from "./interfaces/chat/chat.interfaces";
|
|
15
|
+
export { IFolder } from "./interfaces/folder/folder.interfaces";
|
|
18
16
|
export { IMessage, IMessageCreate, IMessageUpdate, } from "./interfaces/message/message.interfaces";
|
|
17
|
+
export { IStudy } from "./interfaces/study/study.interfaces";
|
|
18
|
+
export { IUser } from "./interfaces/user/user.interfaces";
|
|
19
|
+
export { IUserPushNotificationToken } from "./interfaces/user-push-notification-token/user-push-notification-token.interfaces";
|
|
20
|
+
export { IVideo } from "./interfaces/video/video.interfaces";
|
|
19
21
|
export { IVideoMinuteResult, IVideoMinuteResultInput, IVideoMinuteBatch, } from "./entities/VideoMinuteResult";
|
|
20
22
|
import KnexManager from "./KnexConnection";
|
|
21
23
|
export { KnexManager };
|
package/dist/index.js
CHANGED
|
@@ -3,24 +3,26 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.KnexManager = exports.VideoMinuteResultDAO = exports.
|
|
6
|
+
exports.KnexManager = exports.VideoMinuteResultDAO = exports.VideoDAO = exports.UserPushNotificationTokenDAO = exports.UserDAO = exports.StudyDAO = exports.MessageDAO = exports.FolderDAO = exports.ChatDAO = exports.CameraDAO = exports.AuthDAO = void 0;
|
|
7
7
|
// DAOs
|
|
8
|
-
var user_dao_1 = require("./dao/user/user.dao");
|
|
9
|
-
Object.defineProperty(exports, "UserDAO", { enumerable: true, get: function () { return user_dao_1.UserDAO; } });
|
|
10
|
-
var study_dao_1 = require("./dao/study/study.dao");
|
|
11
|
-
Object.defineProperty(exports, "StudyDAO", { enumerable: true, get: function () { return study_dao_1.StudyDAO; } });
|
|
12
|
-
var folder_dao_1 = require("./dao/folder/folder.dao");
|
|
13
|
-
Object.defineProperty(exports, "FolderDAO", { enumerable: true, get: function () { return folder_dao_1.FolderDAO; } });
|
|
14
|
-
var video_dao_1 = require("./dao/video/video.dao");
|
|
15
|
-
Object.defineProperty(exports, "VideoDAO", { enumerable: true, get: function () { return video_dao_1.VideoDAO; } });
|
|
16
8
|
var auth_dao_1 = require("./dao/auth/auth.dao");
|
|
17
9
|
Object.defineProperty(exports, "AuthDAO", { enumerable: true, get: function () { return auth_dao_1.AuthDAO; } });
|
|
18
|
-
var
|
|
19
|
-
Object.defineProperty(exports, "
|
|
10
|
+
var camera_dao_1 = require("./dao/camera/camera.dao");
|
|
11
|
+
Object.defineProperty(exports, "CameraDAO", { enumerable: true, get: function () { return camera_dao_1.CameraDAO; } });
|
|
20
12
|
var chat_dao_1 = require("./dao/chat/chat.dao");
|
|
21
13
|
Object.defineProperty(exports, "ChatDAO", { enumerable: true, get: function () { return chat_dao_1.ChatDAO; } });
|
|
14
|
+
var folder_dao_1 = require("./dao/folder/folder.dao");
|
|
15
|
+
Object.defineProperty(exports, "FolderDAO", { enumerable: true, get: function () { return folder_dao_1.FolderDAO; } });
|
|
22
16
|
var message_dao_1 = require("./dao/message/message.dao");
|
|
23
17
|
Object.defineProperty(exports, "MessageDAO", { enumerable: true, get: function () { return message_dao_1.MessageDAO; } });
|
|
18
|
+
var study_dao_1 = require("./dao/study/study.dao");
|
|
19
|
+
Object.defineProperty(exports, "StudyDAO", { enumerable: true, get: function () { return study_dao_1.StudyDAO; } });
|
|
20
|
+
var user_dao_1 = require("./dao/user/user.dao");
|
|
21
|
+
Object.defineProperty(exports, "UserDAO", { enumerable: true, get: function () { return user_dao_1.UserDAO; } });
|
|
22
|
+
var user_push_notification_token_dao_1 = require("./dao/user-push-notification-token/user-push-notification-token.dao");
|
|
23
|
+
Object.defineProperty(exports, "UserPushNotificationTokenDAO", { enumerable: true, get: function () { return user_push_notification_token_dao_1.UserPushNotificationTokenDAO; } });
|
|
24
|
+
var video_dao_1 = require("./dao/video/video.dao");
|
|
25
|
+
Object.defineProperty(exports, "VideoDAO", { enumerable: true, get: function () { return video_dao_1.VideoDAO; } });
|
|
24
26
|
var VideoMinuteResultDAO_1 = require("./dao/VideoMinuteResultDAO");
|
|
25
27
|
Object.defineProperty(exports, "VideoMinuteResultDAO", { enumerable: true, get: function () { return VideoMinuteResultDAO_1.VideoMinuteResultDAO; } });
|
|
26
28
|
const KnexConnection_1 = __importDefault(require("./KnexConnection"));
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO;AACP,gDAA8C;AAArC,mGAAA,OAAO,OAAA;AAChB,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO;AACP,gDAA8C;AAArC,mGAAA,OAAO,OAAA;AAChB,sDAAoD;AAA3C,uGAAA,SAAS,OAAA;AAClB,gDAA8C;AAArC,mGAAA,OAAO,OAAA;AAChB,sDAAoD;AAA3C,uGAAA,SAAS,OAAA;AAClB,yDAAuD;AAA9C,yGAAA,UAAU,OAAA;AACnB,mDAAiD;AAAxC,qGAAA,QAAQ,OAAA;AACjB,gDAA8C;AAArC,mGAAA,OAAO,OAAA;AAChB,wHAAmH;AAA1G,gJAAA,4BAA4B,OAAA;AACrC,mDAAiD;AAAxC,qGAAA,QAAQ,OAAA;AACjB,mEAAkE;AAAzD,4HAAA,oBAAoB,OAAA;AA2B7B,sEAA2C;AAClC,sBADF,wBAAW,CACE"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"camera.interfaces.js","sourceRoot":"","sources":["../../../src/interfaces/camera/camera.interfaces.ts"],"names":[],"mappings":""}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { IStudy } from "../study/study.interfaces";
|
|
2
|
+
import type { ICamera } from "../camera/camera.interfaces";
|
|
2
3
|
export interface IFolder {
|
|
3
4
|
id: number;
|
|
4
5
|
uuid: string;
|
|
@@ -6,7 +7,9 @@ export interface IFolder {
|
|
|
6
7
|
createdBy: number;
|
|
7
8
|
status: "UPLOADING" | "COMPLETE";
|
|
8
9
|
studyId: number;
|
|
10
|
+
cameraId?: number;
|
|
9
11
|
created_at: string;
|
|
10
12
|
updated_at: string;
|
|
11
13
|
study?: IStudy;
|
|
14
|
+
camera?: ICamera;
|
|
12
15
|
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import type { IFolder } from "../folder/folder.interfaces";
|
|
2
|
+
import type { ICamera } from "../camera/camera.interfaces";
|
|
2
3
|
export interface IVideo {
|
|
3
4
|
id: number;
|
|
4
5
|
uuid: string;
|
|
5
6
|
folderId: number;
|
|
7
|
+
cameraId?: number;
|
|
8
|
+
annotationSourceId?: number;
|
|
6
9
|
name: string;
|
|
7
10
|
videoLocation: string;
|
|
8
11
|
videoOutputLocation: string | null;
|
|
@@ -22,4 +25,5 @@ export interface IVideo {
|
|
|
22
25
|
created_at: string;
|
|
23
26
|
updated_at: string;
|
|
24
27
|
folder?: IFolder;
|
|
28
|
+
camera?: ICamera;
|
|
25
29
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { Knex } from "knex";
|
|
2
|
+
|
|
3
|
+
export async function up(knex: Knex): Promise<void> {
|
|
4
|
+
// Create cameras table
|
|
5
|
+
await knex.schema.createTable("cameras", (table) => {
|
|
6
|
+
table.increments("id").primary();
|
|
7
|
+
table
|
|
8
|
+
.uuid("uuid")
|
|
9
|
+
.defaultTo(knex.raw("uuid_generate_v4()"))
|
|
10
|
+
.notNullable()
|
|
11
|
+
.unique();
|
|
12
|
+
table.string("name", 100).notNullable();
|
|
13
|
+
table.decimal("longitude", 10, 7).notNullable();
|
|
14
|
+
table.decimal("latitude", 10, 7).notNullable();
|
|
15
|
+
table.timestamps(true, true);
|
|
16
|
+
|
|
17
|
+
table.index(["uuid"]);
|
|
18
|
+
table.index(["longitude", "latitude"]);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Add cameraId to videos table
|
|
22
|
+
await knex.schema.alterTable("video", (table) => {
|
|
23
|
+
table
|
|
24
|
+
.integer("cameraId")
|
|
25
|
+
.nullable()
|
|
26
|
+
.references("id")
|
|
27
|
+
.inTable("cameras")
|
|
28
|
+
.onDelete("SET NULL");
|
|
29
|
+
table.index(["cameraId"]);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Add cameraId to folders table
|
|
33
|
+
await knex.schema.alterTable("folders", (table) => {
|
|
34
|
+
table
|
|
35
|
+
.integer("cameraId")
|
|
36
|
+
.nullable()
|
|
37
|
+
.references("id")
|
|
38
|
+
.inTable("cameras")
|
|
39
|
+
.onDelete("SET NULL");
|
|
40
|
+
table.index(["cameraId"]);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function down(knex: Knex): Promise<void> {
|
|
45
|
+
// Remove cameraId from folders table
|
|
46
|
+
await knex.schema.alterTable("folders", (table) => {
|
|
47
|
+
table.dropIndex(["cameraId"]);
|
|
48
|
+
table.dropForeign(["cameraId"]);
|
|
49
|
+
table.dropColumn("cameraId");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Remove cameraId from videos table
|
|
53
|
+
await knex.schema.alterTable("video", (table) => {
|
|
54
|
+
table.dropIndex(["cameraId"]);
|
|
55
|
+
table.dropForeign(["cameraId"]);
|
|
56
|
+
table.dropColumn("cameraId");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Drop cameras table
|
|
60
|
+
await knex.schema.dropTable("cameras");
|
|
61
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Knex } from "knex";
|
|
2
|
+
|
|
3
|
+
export async function up(knex: Knex): Promise<void> {
|
|
4
|
+
await knex.schema.alterTable("video", (table) => {
|
|
5
|
+
// Add annotationSourceId column with foreign key to video table
|
|
6
|
+
table
|
|
7
|
+
.integer("annotationSourceId")
|
|
8
|
+
.nullable()
|
|
9
|
+
.references("id")
|
|
10
|
+
.inTable("video")
|
|
11
|
+
.onDelete("SET NULL");
|
|
12
|
+
|
|
13
|
+
// Add index for performance when querying by annotation source
|
|
14
|
+
table.index(["annotationSourceId"], "idx_video_annotation_source_id");
|
|
15
|
+
|
|
16
|
+
// Add composite index for efficient template lookups
|
|
17
|
+
table.index(
|
|
18
|
+
["folderId", "videoType", "status"],
|
|
19
|
+
"idx_video_template_lookup",
|
|
20
|
+
);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function down(knex: Knex): Promise<void> {
|
|
25
|
+
await knex.schema.alterTable("video", (table) => {
|
|
26
|
+
// Drop indexes first
|
|
27
|
+
table.dropIndex(
|
|
28
|
+
["folderId", "videoType", "status"],
|
|
29
|
+
"idx_video_template_lookup",
|
|
30
|
+
);
|
|
31
|
+
table.dropIndex(["annotationSourceId"], "idx_video_annotation_source_id");
|
|
32
|
+
|
|
33
|
+
// Drop foreign key and column
|
|
34
|
+
table.dropForeign(["annotationSourceId"]);
|
|
35
|
+
table.dropColumn("annotationSourceId");
|
|
36
|
+
});
|
|
37
|
+
}
|
package/package.json
CHANGED
package/plan.md
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# Lane/Annotation Reuse Implementation Plan
|
|
2
|
+
|
|
3
|
+
## Schema Changes
|
|
4
|
+
|
|
5
|
+
### Tables
|
|
6
|
+
|
|
7
|
+
- **video**: Add `annotation_source_id` column (nullable INTEGER, FK to video.id)
|
|
8
|
+
|
|
9
|
+
### Modifications
|
|
10
|
+
|
|
11
|
+
- **video table**:
|
|
12
|
+
- Add `annotation_source_id INTEGER NULL REFERENCES video(id) ON DELETE SET NULL`
|
|
13
|
+
- Add index on `annotation_source_id` for performance
|
|
14
|
+
- Add composite index on `(folderId, videoType)` for template lookup optimization
|
|
15
|
+
|
|
16
|
+
## Migrations
|
|
17
|
+
|
|
18
|
+
### 20250917143052_add_annotation_source_to_video.ts
|
|
19
|
+
|
|
20
|
+
- **Up operations**:
|
|
21
|
+
- Add `annotation_source_id` column to video table
|
|
22
|
+
- Add foreign key constraint to video(id) with SET NULL on delete
|
|
23
|
+
- Add index on `annotation_source_id`
|
|
24
|
+
- Add composite index on `(folderId, videoType)` for efficient template queries
|
|
25
|
+
- **Down operations**:
|
|
26
|
+
- Drop indexes
|
|
27
|
+
- Drop foreign key constraint
|
|
28
|
+
- Drop column
|
|
29
|
+
- **Safety level**: LOW RISK - Adding nullable column with proper constraints
|
|
30
|
+
|
|
31
|
+
## DAOs
|
|
32
|
+
|
|
33
|
+
### Modify: VideoDAO
|
|
34
|
+
|
|
35
|
+
- **File**: src/dao/video/video.dao.ts
|
|
36
|
+
- **New methods**:
|
|
37
|
+
- `getTemplateVideos(folderId: number, videoType: string): Promise<IVideo[]>` - Get videos from same folder/type that can serve as templates (have metadata and completed status)
|
|
38
|
+
- `getVideosUsingTemplate(templateVideoId: number): Promise<IVideo[]>` - Get videos that used a specific video as template
|
|
39
|
+
- `setAnnotationSource(videoId: number, sourceVideoId: number | null): Promise<IVideo | null>` - Set/update annotation source
|
|
40
|
+
- **Performance optimizations**:
|
|
41
|
+
- Use JOINs to fetch related annotation source data in main queries
|
|
42
|
+
- Eliminate N+1 queries when fetching videos with their annotation sources
|
|
43
|
+
- Optimized template lookup using composite index
|
|
44
|
+
|
|
45
|
+
### Query Patterns
|
|
46
|
+
|
|
47
|
+
```sql
|
|
48
|
+
-- Get template videos (same folder + type, completed, with metadata)
|
|
49
|
+
SELECT v.* FROM video v
|
|
50
|
+
WHERE v.folderId = ?
|
|
51
|
+
AND v.videoType = ?
|
|
52
|
+
AND v.status = 'COMPLETED'
|
|
53
|
+
AND v.metadata IS NOT NULL
|
|
54
|
+
AND v.metadata != '{}'
|
|
55
|
+
ORDER BY v.created_at DESC;
|
|
56
|
+
|
|
57
|
+
-- Get videos using specific template
|
|
58
|
+
SELECT v.* FROM video v
|
|
59
|
+
WHERE v.annotation_source_id = ?;
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Interfaces
|
|
63
|
+
|
|
64
|
+
### Modify: IVideo.ts
|
|
65
|
+
|
|
66
|
+
- **File**: src/interfaces/video/video.interfaces.ts
|
|
67
|
+
- **New properties**:
|
|
68
|
+
- `annotationSourceId?: number;` - ID of video whose annotations were copied
|
|
69
|
+
- `annotationSource?: IVideo;` - Optional populated annotation source video object
|
|
70
|
+
- **Export**: Update index.ts exports
|
|
71
|
+
|
|
72
|
+
## Implementation Order
|
|
73
|
+
|
|
74
|
+
1. **Interfaces** → Update IVideo interface with new fields
|
|
75
|
+
2. **Migrations** → Create and run migration to add database column
|
|
76
|
+
3. **DAOs** → Add new methods to VideoDAO for template management
|
|
77
|
+
4. **Exports** → Update index.ts to export new interface changes
|
|
78
|
+
5. **Build** → Compile and test changes
|
|
79
|
+
|
|
80
|
+
## Business Logic Constraints
|
|
81
|
+
|
|
82
|
+
### Template Selection Rules
|
|
83
|
+
|
|
84
|
+
- Only videos from **same folder** (`folderId` match)
|
|
85
|
+
- Only videos of **same type** (`videoType` match: TMC → TMC, ATR → ATR)
|
|
86
|
+
- Only **COMPLETED** videos with non-empty metadata can be templates
|
|
87
|
+
- Template videos must have lane configuration in metadata field
|
|
88
|
+
|
|
89
|
+
### Data Integrity
|
|
90
|
+
|
|
91
|
+
- `annotation_source_id` references `video.id` with CASCADE DELETE behavior set to SET NULL
|
|
92
|
+
- If template video is deleted, dependent videos keep their annotations but lose the source reference
|
|
93
|
+
- Self-referencing constraint: video cannot reference itself as annotation source
|
|
94
|
+
|
|
95
|
+
## Performance Considerations
|
|
96
|
+
|
|
97
|
+
### Indexes
|
|
98
|
+
|
|
99
|
+
- `annotation_source_id` - for JOIN operations and template lookups
|
|
100
|
+
- `(folderId, videoType)` - composite index for efficient template discovery
|
|
101
|
+
- Existing `uuid` index maintained for external API calls
|
|
102
|
+
|
|
103
|
+
### Query Optimization
|
|
104
|
+
|
|
105
|
+
- Template lookup query uses composite index for O(log n) performance
|
|
106
|
+
- JOIN operations for fetching annotation source data in single query
|
|
107
|
+
- Pagination maintained for all list operations
|
|
108
|
+
|
|
109
|
+
## Risks & Validation
|
|
110
|
+
|
|
111
|
+
### Migration Safety
|
|
112
|
+
|
|
113
|
+
- **LOW RISK**: Adding nullable column with proper constraints
|
|
114
|
+
- **Rollback plan**: Down migration removes column and constraints cleanly
|
|
115
|
+
- **Data preservation**: No existing data affected
|
|
116
|
+
|
|
117
|
+
### Pattern Compliance
|
|
118
|
+
|
|
119
|
+
- **100% compliant** with existing DAO patterns (IBaseDAO implementation)
|
|
120
|
+
- **100% compliant** with naming conventions (snake_case DB, camelCase TS)
|
|
121
|
+
- **100% compliant** with interface patterns (I prefix, proper exports)
|
|
122
|
+
- **100% compliant** with migration patterns (timestamp prefix, up/down functions)
|
|
123
|
+
|
|
124
|
+
### Testing Strategy
|
|
125
|
+
|
|
126
|
+
- Verify foreign key constraints work correctly
|
|
127
|
+
- Test template discovery with same folder/type filtering
|
|
128
|
+
- Validate NULL handling for videos without annotation sources
|
|
129
|
+
- Performance test template lookup queries with large datasets
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Knex } from "knex";
|
|
2
|
+
import { IBaseDAO, IDataPaginator } from "../../d.types";
|
|
3
|
+
import { ICamera } from "../../interfaces/camera/camera.interfaces";
|
|
4
|
+
import KnexManager from "../../KnexConnection";
|
|
5
|
+
|
|
6
|
+
export class CameraDAO implements IBaseDAO<ICamera> {
|
|
7
|
+
private _knex: Knex<any, unknown[]> = KnexManager.getConnection();
|
|
8
|
+
|
|
9
|
+
async create(item: ICamera): Promise<ICamera> {
|
|
10
|
+
const [createdCamera] = await this._knex("cameras")
|
|
11
|
+
.insert(item)
|
|
12
|
+
.returning("*");
|
|
13
|
+
return createdCamera;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async getById(id: number): Promise<ICamera | null> {
|
|
17
|
+
const camera = await this._knex("cameras").where({ id }).first();
|
|
18
|
+
return camera || null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async getByUuid(uuid: string): Promise<ICamera | null> {
|
|
22
|
+
const camera = await this._knex("cameras").where({ uuid }).first();
|
|
23
|
+
return camera || null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async update(id: number, item: Partial<ICamera>): Promise<ICamera | null> {
|
|
27
|
+
const [updatedCamera] = await this._knex("cameras")
|
|
28
|
+
.where({ id })
|
|
29
|
+
.update(item)
|
|
30
|
+
.returning("*");
|
|
31
|
+
return updatedCamera || null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async delete(id: number): Promise<boolean> {
|
|
35
|
+
const result = await this._knex("cameras").where({ id }).del();
|
|
36
|
+
return result > 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async getAll(page: number, limit: number): Promise<IDataPaginator<ICamera>> {
|
|
40
|
+
const offset = (page - 1) * limit;
|
|
41
|
+
|
|
42
|
+
const [countResult] = await this._knex("cameras").count("* as count");
|
|
43
|
+
const totalCount = +countResult.count;
|
|
44
|
+
const cameras = await this._knex("cameras").limit(limit).offset(offset);
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
success: true,
|
|
48
|
+
data: cameras,
|
|
49
|
+
page,
|
|
50
|
+
limit,
|
|
51
|
+
count: cameras.length,
|
|
52
|
+
totalCount,
|
|
53
|
+
totalPages: Math.ceil(totalCount / limit),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async getByName(name: string): Promise<ICamera | null> {
|
|
58
|
+
const camera = await this._knex("cameras").where({ name }).first();
|
|
59
|
+
return camera || null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async getCamerasNearCoordinates(
|
|
63
|
+
longitude: number,
|
|
64
|
+
latitude: number,
|
|
65
|
+
radiusKm: number = 1,
|
|
66
|
+
): Promise<ICamera[]> {
|
|
67
|
+
// Using ST_DWithin for geographic distance calculation
|
|
68
|
+
// This is a PostgreSQL-specific query for geospatial operations
|
|
69
|
+
const cameras = await this._knex("cameras").whereRaw(
|
|
70
|
+
`ST_DWithin(
|
|
71
|
+
ST_MakePoint(longitude, latitude)::geography,
|
|
72
|
+
ST_MakePoint(?, ?)::geography,
|
|
73
|
+
?
|
|
74
|
+
)`,
|
|
75
|
+
[longitude, latitude, radiusKm * 1000], // Convert km to meters
|
|
76
|
+
);
|
|
77
|
+
return cameras;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -196,4 +196,53 @@ export class VideoDAO implements IBaseDAO<IVideo> {
|
|
|
196
196
|
totalPages: Math.ceil(totalCount / limit),
|
|
197
197
|
};
|
|
198
198
|
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Get videos from same folder with same type that have metadata and are COMPLETED
|
|
202
|
+
* Suitable for use as lane annotation templates
|
|
203
|
+
*/
|
|
204
|
+
async getTemplateVideos(
|
|
205
|
+
folderId: number,
|
|
206
|
+
videoType: string,
|
|
207
|
+
): Promise<IVideo[]> {
|
|
208
|
+
try {
|
|
209
|
+
let query = this._knex("video")
|
|
210
|
+
.where("folderId", folderId)
|
|
211
|
+
.where("videoType", videoType)
|
|
212
|
+
.where("status", "COMPLETED")
|
|
213
|
+
.whereNotNull("metadata")
|
|
214
|
+
.whereRaw("metadata != '{}'");
|
|
215
|
+
|
|
216
|
+
// Apply video type specific metadata validation
|
|
217
|
+
if (videoType === "ATR") {
|
|
218
|
+
// ATR videos use lanes array structure
|
|
219
|
+
query = query.whereRaw("jsonb_array_length(metadata->'lanes') > 0");
|
|
220
|
+
} else {
|
|
221
|
+
// TMC/JUNCTION/ROUNDABOUT/PATHWAY videos use objects with pt1/pt2 structure
|
|
222
|
+
// Check if metadata has at least one key with pt1 and pt2 properties
|
|
223
|
+
query = query.whereRaw(`
|
|
224
|
+
EXISTS (
|
|
225
|
+
SELECT 1
|
|
226
|
+
FROM jsonb_each(metadata) as entry(key, value)
|
|
227
|
+
WHERE key != 'lanes'
|
|
228
|
+
AND key != 'finish_line'
|
|
229
|
+
AND jsonb_typeof(value) = 'object'
|
|
230
|
+
AND value ? 'pt1'
|
|
231
|
+
AND value ? 'pt2'
|
|
232
|
+
AND jsonb_typeof(value->'pt1') = 'array'
|
|
233
|
+
AND jsonb_typeof(value->'pt2') = 'array'
|
|
234
|
+
AND jsonb_array_length(value->'pt1') = 2
|
|
235
|
+
AND jsonb_array_length(value->'pt2') = 2
|
|
236
|
+
)
|
|
237
|
+
`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const videos = await query.orderBy("updated_at", "desc").select("*");
|
|
241
|
+
|
|
242
|
+
return videos;
|
|
243
|
+
} catch (error) {
|
|
244
|
+
console.error("Error fetching template videos:", error);
|
|
245
|
+
throw error;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
199
248
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,32 +1,34 @@
|
|
|
1
1
|
// DAOs
|
|
2
|
-
export { UserDAO } from "./dao/user/user.dao";
|
|
3
|
-
export { StudyDAO } from "./dao/study/study.dao";
|
|
4
|
-
export { FolderDAO } from "./dao/folder/folder.dao";
|
|
5
|
-
export { VideoDAO } from "./dao/video/video.dao";
|
|
6
2
|
export { AuthDAO } from "./dao/auth/auth.dao";
|
|
7
|
-
export {
|
|
3
|
+
export { CameraDAO } from "./dao/camera/camera.dao";
|
|
8
4
|
export { ChatDAO } from "./dao/chat/chat.dao";
|
|
5
|
+
export { FolderDAO } from "./dao/folder/folder.dao";
|
|
9
6
|
export { MessageDAO } from "./dao/message/message.dao";
|
|
7
|
+
export { StudyDAO } from "./dao/study/study.dao";
|
|
8
|
+
export { UserDAO } from "./dao/user/user.dao";
|
|
9
|
+
export { UserPushNotificationTokenDAO } from "./dao/user-push-notification-token/user-push-notification-token.dao";
|
|
10
|
+
export { VideoDAO } from "./dao/video/video.dao";
|
|
10
11
|
export { VideoMinuteResultDAO } from "./dao/VideoMinuteResultDAO";
|
|
11
12
|
|
|
12
13
|
// Interfaces
|
|
13
14
|
export { IDataPaginator } from "./d.types";
|
|
14
|
-
export { IUser } from "./interfaces/user/user.interfaces";
|
|
15
|
-
export { IStudy } from "./interfaces/study/study.interfaces";
|
|
16
|
-
export { IFolder } from "./interfaces/folder/folder.interfaces";
|
|
17
|
-
export { IVideo } from "./interfaces/video/video.interfaces";
|
|
18
15
|
export { IAuth } from "./interfaces/auth/auth.interfaces";
|
|
19
|
-
export {
|
|
16
|
+
export { ICamera } from "./interfaces/camera/camera.interfaces";
|
|
20
17
|
export {
|
|
21
18
|
IChat,
|
|
22
19
|
IChatCreate,
|
|
23
20
|
IChatUpdate,
|
|
24
21
|
} from "./interfaces/chat/chat.interfaces";
|
|
22
|
+
export { IFolder } from "./interfaces/folder/folder.interfaces";
|
|
25
23
|
export {
|
|
26
24
|
IMessage,
|
|
27
25
|
IMessageCreate,
|
|
28
26
|
IMessageUpdate,
|
|
29
27
|
} from "./interfaces/message/message.interfaces";
|
|
28
|
+
export { IStudy } from "./interfaces/study/study.interfaces";
|
|
29
|
+
export { IUser } from "./interfaces/user/user.interfaces";
|
|
30
|
+
export { IUserPushNotificationToken } from "./interfaces/user-push-notification-token/user-push-notification-token.interfaces";
|
|
31
|
+
export { IVideo } from "./interfaces/video/video.interfaces";
|
|
30
32
|
export {
|
|
31
33
|
IVideoMinuteResult,
|
|
32
34
|
IVideoMinuteResultInput,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { IStudy } from "../study/study.interfaces";
|
|
2
|
+
import type { ICamera } from "../camera/camera.interfaces";
|
|
2
3
|
|
|
3
4
|
export interface IFolder {
|
|
4
5
|
id: number;
|
|
@@ -7,7 +8,9 @@ export interface IFolder {
|
|
|
7
8
|
createdBy: number; // user.id
|
|
8
9
|
status: "UPLOADING" | "COMPLETE";
|
|
9
10
|
studyId: number; // study.id
|
|
11
|
+
cameraId?: number; // camera.id
|
|
10
12
|
created_at: string;
|
|
11
13
|
updated_at: string;
|
|
12
14
|
study?: IStudy;
|
|
15
|
+
camera?: ICamera;
|
|
13
16
|
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import type { IFolder } from "../folder/folder.interfaces";
|
|
2
|
+
import type { ICamera } from "../camera/camera.interfaces";
|
|
2
3
|
|
|
3
4
|
export interface IVideo {
|
|
4
5
|
id: number;
|
|
5
6
|
uuid: string;
|
|
6
7
|
folderId: number;
|
|
8
|
+
cameraId?: number;
|
|
9
|
+
annotationSourceId?: number;
|
|
7
10
|
name: string;
|
|
8
11
|
videoLocation: string;
|
|
9
12
|
videoOutputLocation: string | null;
|
|
@@ -28,4 +31,5 @@ export interface IVideo {
|
|
|
28
31
|
created_at: string;
|
|
29
32
|
updated_at: string;
|
|
30
33
|
folder?: IFolder;
|
|
34
|
+
camera?: ICamera;
|
|
31
35
|
}
|