@trafficgroup/knex-rel 0.1.9 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +5 -2
- package/.env.prod +5 -0
- package/CLAUDE.md +2 -11
- package/dist/constants/video.constants.d.ts +12 -0
- package/dist/constants/video.constants.js +18 -0
- package/dist/constants/video.constants.js.map +1 -0
- package/dist/dao/VideoMinuteResultDAO.d.ts +1 -1
- package/dist/dao/VideoMinuteResultDAO.js +23 -29
- package/dist/dao/VideoMinuteResultDAO.js.map +1 -1
- package/dist/dao/auth/auth.dao.js +1 -4
- package/dist/dao/auth/auth.dao.js.map +1 -1
- package/dist/dao/batch/batch.dao.js +14 -13
- package/dist/dao/batch/batch.dao.js.map +1 -1
- package/dist/dao/camera/camera.dao.js +7 -10
- package/dist/dao/camera/camera.dao.js.map +1 -1
- package/dist/dao/chat/chat.dao.d.ts +1 -1
- package/dist/dao/chat/chat.dao.js +35 -25
- package/dist/dao/chat/chat.dao.js.map +1 -1
- package/dist/dao/folder/folder.dao.js +2 -7
- package/dist/dao/folder/folder.dao.js.map +1 -1
- package/dist/dao/location/location.dao.js +9 -16
- package/dist/dao/location/location.dao.js.map +1 -1
- package/dist/dao/message/message.dao.d.ts +1 -1
- package/dist/dao/message/message.dao.js +26 -18
- package/dist/dao/message/message.dao.js.map +1 -1
- package/dist/dao/report-configuration/report-configuration.dao.js +31 -32
- package/dist/dao/report-configuration/report-configuration.dao.js.map +1 -1
- package/dist/dao/study/study.dao.js +2 -7
- package/dist/dao/study/study.dao.js.map +1 -1
- package/dist/dao/user/user.dao.js +1 -4
- package/dist/dao/user/user.dao.js.map +1 -1
- package/dist/dao/user-push-notification-token/user-push-notification-token.dao.js +8 -26
- package/dist/dao/user-push-notification-token/user-push-notification-token.dao.js.map +1 -1
- package/dist/dao/video/video.dao.d.ts +2 -1
- package/dist/dao/video/video.dao.js +42 -28
- package/dist/dao/video/video.dao.js.map +1 -1
- package/dist/index.d.ts +6 -4
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/interfaces/batch/batch.interfaces.d.ts +1 -1
- package/dist/interfaces/camera/camera.interfaces.d.ts +1 -1
- package/dist/interfaces/chat/chat.interfaces.d.ts +3 -3
- package/dist/interfaces/folder/folder.interfaces.d.ts +1 -1
- package/dist/interfaces/message/message.interfaces.d.ts +2 -2
- package/dist/interfaces/study/study.interfaces.d.ts +2 -2
- package/dist/interfaces/user/user.interfaces.d.ts +1 -1
- package/dist/interfaces/user-push-notification-token/user-push-notification-token.interfaces.d.ts +1 -1
- package/dist/interfaces/video/video.interfaces.d.ts +2 -2
- package/migrations/20250717160737_migration.ts +1 -1
- package/migrations/20250717160908_migration.ts +2 -5
- package/migrations/20250717161310_migration.ts +1 -1
- package/migrations/20250717161406_migration.ts +3 -3
- package/migrations/20250717162431_migration.ts +1 -1
- package/migrations/20250717173228_migration.ts +2 -2
- package/migrations/20250717204731_migration.ts +1 -1
- package/migrations/20250722210109_migration.ts +4 -8
- package/migrations/20250722211019_migration.ts +1 -1
- package/migrations/20250723153852_migration.ts +10 -13
- package/migrations/20250723162257_migration.ts +7 -4
- package/migrations/20250723171109_migration.ts +7 -4
- package/migrations/20250723205331_migration.ts +9 -6
- package/migrations/20250724191345_migration.ts +11 -8
- package/migrations/20250730180932_migration.ts +13 -14
- package/migrations/20250730213625_migration.ts +11 -8
- package/migrations/20250804124509_migration.ts +21 -26
- package/migrations/20250804132053_migration.ts +8 -5
- package/migrations/20250804164518_migration.ts +7 -7
- package/migrations/20250823223016_migration.ts +21 -32
- package/migrations/20250910015452_migration.ts +6 -18
- package/migrations/20250911000000_migration.ts +4 -18
- package/migrations/20250917144153_migration.ts +7 -14
- package/migrations/20250930200521_migration.ts +4 -8
- package/migrations/20251010143500_migration.ts +6 -27
- package/migrations/20251020225758_migration.ts +15 -51
- package/migrations/20251112120000_migration.ts +2 -10
- package/migrations/20251112120200_migration.ts +7 -19
- package/migrations/20251112120300_migration.ts +2 -7
- package/package.json +1 -1
- package/src/constants/video.constants.ts +19 -0
- package/src/d.types.ts +14 -18
- package/src/dao/VideoMinuteResultDAO.ts +49 -72
- package/src/dao/auth/auth.dao.ts +55 -58
- package/src/dao/batch/batch.dao.ts +98 -101
- package/src/dao/camera/camera.dao.ts +121 -124
- package/src/dao/chat/chat.dao.ts +43 -45
- package/src/dao/folder/folder.dao.ts +56 -65
- package/src/dao/location/location.dao.ts +87 -109
- package/src/dao/message/message.dao.ts +32 -32
- package/src/dao/report-configuration/report-configuration.dao.ts +342 -370
- package/src/dao/study/study.dao.ts +63 -88
- package/src/dao/user/user.dao.ts +50 -52
- package/src/dao/user-push-notification-token/user-push-notification-token.dao.ts +49 -83
- package/src/dao/video/video.dao.ts +339 -357
- package/src/entities/BaseEntity.ts +1 -1
- package/src/index.ts +22 -26
- package/src/interfaces/auth/auth.interfaces.ts +10 -10
- package/src/interfaces/batch/batch.interfaces.ts +1 -1
- package/src/interfaces/camera/camera.interfaces.ts +9 -9
- package/src/interfaces/chat/chat.interfaces.ts +4 -4
- package/src/interfaces/folder/folder.interfaces.ts +2 -2
- package/src/interfaces/location/location.interfaces.ts +7 -7
- package/src/interfaces/message/message.interfaces.ts +3 -3
- package/src/interfaces/report-configuration/report-configuration.interfaces.ts +16 -16
- package/src/interfaces/study/study.interfaces.ts +3 -3
- package/src/interfaces/user/user.interfaces.ts +9 -9
- package/src/interfaces/user-push-notification-token/user-push-notification-token.interfaces.ts +9 -9
- package/src/interfaces/video/video.interfaces.ts +34 -34
|
@@ -5,12 +5,7 @@ export async function up(knex: Knex): Promise<void> {
|
|
|
5
5
|
const studyHasCamera = await knex.schema.hasColumn("study", "cameraId");
|
|
6
6
|
if (!studyHasCamera) {
|
|
7
7
|
await knex.schema.alterTable("study", (table) => {
|
|
8
|
-
table
|
|
9
|
-
.integer("cameraId")
|
|
10
|
-
.nullable()
|
|
11
|
-
.references("id")
|
|
12
|
-
.inTable("cameras")
|
|
13
|
-
.onDelete("SET NULL");
|
|
8
|
+
table.integer("cameraId").nullable().references("id").inTable("cameras").onDelete("SET NULL");
|
|
14
9
|
table.index(["cameraId"]);
|
|
15
10
|
});
|
|
16
11
|
}
|
|
@@ -18,9 +13,7 @@ export async function up(knex: Knex): Promise<void> {
|
|
|
18
13
|
// 2. Remove cameraId from video table
|
|
19
14
|
const videoHasCamera = await knex.schema.hasColumn("video", "cameraId");
|
|
20
15
|
if (videoHasCamera) {
|
|
21
|
-
await knex.raw(
|
|
22
|
-
`ALTER TABLE video DROP CONSTRAINT IF EXISTS video_cameraid_foreign`,
|
|
23
|
-
);
|
|
16
|
+
await knex.raw(`ALTER TABLE video DROP CONSTRAINT IF EXISTS video_cameraid_foreign`);
|
|
24
17
|
await knex.raw(`DROP INDEX IF EXISTS video_cameraid_index`);
|
|
25
18
|
await knex.schema.alterTable("video", (table) => {
|
|
26
19
|
table.dropColumn("cameraId");
|
|
@@ -30,9 +23,7 @@ export async function up(knex: Knex): Promise<void> {
|
|
|
30
23
|
// 3. Remove cameraId from folders table
|
|
31
24
|
const foldersHasCamera = await knex.schema.hasColumn("folders", "cameraId");
|
|
32
25
|
if (foldersHasCamera) {
|
|
33
|
-
await knex.raw(
|
|
34
|
-
`ALTER TABLE folders DROP CONSTRAINT IF EXISTS folders_cameraid_foreign`,
|
|
35
|
-
);
|
|
26
|
+
await knex.raw(`ALTER TABLE folders DROP CONSTRAINT IF EXISTS folders_cameraid_foreign`);
|
|
36
27
|
await knex.raw(`DROP INDEX IF EXISTS folders_cameraid_index`);
|
|
37
28
|
await knex.schema.alterTable("folders", (table) => {
|
|
38
29
|
table.dropColumn("cameraId");
|
|
@@ -45,12 +36,7 @@ export async function down(knex: Knex): Promise<void> {
|
|
|
45
36
|
const foldersHasCamera = await knex.schema.hasColumn("folders", "cameraId");
|
|
46
37
|
if (!foldersHasCamera) {
|
|
47
38
|
await knex.schema.alterTable("folders", (table) => {
|
|
48
|
-
table
|
|
49
|
-
.integer("cameraId")
|
|
50
|
-
.nullable()
|
|
51
|
-
.references("id")
|
|
52
|
-
.inTable("cameras")
|
|
53
|
-
.onDelete("SET NULL");
|
|
39
|
+
table.integer("cameraId").nullable().references("id").inTable("cameras").onDelete("SET NULL");
|
|
54
40
|
table.index(["cameraId"]);
|
|
55
41
|
});
|
|
56
42
|
}
|
|
@@ -59,12 +45,7 @@ export async function down(knex: Knex): Promise<void> {
|
|
|
59
45
|
const videoHasCamera = await knex.schema.hasColumn("video", "cameraId");
|
|
60
46
|
if (!videoHasCamera) {
|
|
61
47
|
await knex.schema.alterTable("video", (table) => {
|
|
62
|
-
table
|
|
63
|
-
.integer("cameraId")
|
|
64
|
-
.nullable()
|
|
65
|
-
.references("id")
|
|
66
|
-
.inTable("cameras")
|
|
67
|
-
.onDelete("SET NULL");
|
|
48
|
+
table.integer("cameraId").nullable().references("id").inTable("cameras").onDelete("SET NULL");
|
|
68
49
|
table.index(["cameraId"]);
|
|
69
50
|
});
|
|
70
51
|
}
|
|
@@ -72,9 +53,7 @@ export async function down(knex: Knex): Promise<void> {
|
|
|
72
53
|
// 3. Remove cameraId from study table
|
|
73
54
|
const studyHasCamera = await knex.schema.hasColumn("study", "cameraId");
|
|
74
55
|
if (studyHasCamera) {
|
|
75
|
-
await knex.raw(
|
|
76
|
-
`ALTER TABLE study DROP CONSTRAINT IF EXISTS study_cameraid_foreign`,
|
|
77
|
-
);
|
|
56
|
+
await knex.raw(`ALTER TABLE study DROP CONSTRAINT IF EXISTS study_cameraid_foreign`);
|
|
78
57
|
await knex.raw(`DROP INDEX IF EXISTS study_cameraid_index`);
|
|
79
58
|
await knex.schema.alterTable("study", (table) => {
|
|
80
59
|
table.dropColumn("cameraId");
|
|
@@ -13,49 +13,28 @@ import type { Knex } from "knex";
|
|
|
13
13
|
export async function up(knex: Knex): Promise<void> {
|
|
14
14
|
// Step 1: Create video_batch table with proper id/uuid pattern
|
|
15
15
|
await knex.schema.createTable("video_batch", (table) => {
|
|
16
|
-
table
|
|
17
|
-
.increments("id")
|
|
18
|
-
.primary()
|
|
16
|
+
table.increments("id").primary()
|
|
19
17
|
.comment("Primary key for internal foreign key relationships");
|
|
20
18
|
|
|
21
|
-
table
|
|
22
|
-
.uuid("uuid")
|
|
23
|
-
.notNullable()
|
|
24
|
-
.unique()
|
|
25
|
-
.defaultTo(knex.raw("uuid_generate_v4()"))
|
|
19
|
+
table.uuid("uuid").notNullable().unique().defaultTo(knex.raw('uuid_generate_v4()'))
|
|
26
20
|
.comment("UUID for external API communication");
|
|
27
21
|
|
|
28
|
-
table
|
|
29
|
-
.
|
|
30
|
-
.unsigned()
|
|
31
|
-
.notNullable()
|
|
32
|
-
.references("id")
|
|
33
|
-
.inTable("folders")
|
|
34
|
-
.onDelete("CASCADE")
|
|
22
|
+
table.integer("folderId").unsigned().notNullable()
|
|
23
|
+
.references("id").inTable("folders").onDelete("CASCADE")
|
|
35
24
|
.comment("Foreign key to folders table");
|
|
36
25
|
|
|
37
|
-
table
|
|
38
|
-
.enu("status", ["PENDING", "IN_PROGRESS", "COMPLETED", "FAILED"])
|
|
26
|
+
table.enu("status", ["PENDING", "IN_PROGRESS", "COMPLETED", "FAILED"])
|
|
39
27
|
.notNullable()
|
|
40
28
|
.defaultTo("PENDING")
|
|
41
29
|
.comment("Status of the batch upload");
|
|
42
30
|
|
|
43
|
-
table
|
|
44
|
-
.integer("totalVideos")
|
|
45
|
-
.notNullable()
|
|
46
|
-
.defaultTo(0)
|
|
31
|
+
table.integer("totalVideos").notNullable().defaultTo(0)
|
|
47
32
|
.comment("Total number of videos in batch");
|
|
48
33
|
|
|
49
|
-
table
|
|
50
|
-
.integer("completedVideos")
|
|
51
|
-
.notNullable()
|
|
52
|
-
.defaultTo(0)
|
|
34
|
+
table.integer("completedVideos").notNullable().defaultTo(0)
|
|
53
35
|
.comment("Number of successfully created videos");
|
|
54
36
|
|
|
55
|
-
table
|
|
56
|
-
.integer("failedVideos")
|
|
57
|
-
.notNullable()
|
|
58
|
-
.defaultTo(0)
|
|
37
|
+
table.integer("failedVideos").notNullable().defaultTo(0)
|
|
59
38
|
.comment("Number of failed videos");
|
|
60
39
|
|
|
61
40
|
table.timestamps(true, true);
|
|
@@ -63,32 +42,17 @@ export async function up(knex: Knex): Promise<void> {
|
|
|
63
42
|
|
|
64
43
|
// Step 2: Add recording time, trimming, and batch columns to video table
|
|
65
44
|
await knex.schema.alterTable("video", (table) => {
|
|
66
|
-
table
|
|
67
|
-
.timestamp("recordingStartedAt", { useTz: true })
|
|
68
|
-
.nullable()
|
|
45
|
+
table.timestamp("recordingStartedAt", { useTz: true }).nullable()
|
|
69
46
|
.comment("Recording start time in UTC - null for backward compatibility");
|
|
70
47
|
|
|
71
|
-
table
|
|
72
|
-
.boolean("trimEnabled")
|
|
73
|
-
.notNullable()
|
|
74
|
-
.defaultTo(false)
|
|
48
|
+
table.boolean("trimEnabled").notNullable().defaultTo(false)
|
|
75
49
|
.comment("Whether video trimming is enabled");
|
|
76
50
|
|
|
77
|
-
table
|
|
78
|
-
.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
);
|
|
83
|
-
|
|
84
|
-
table
|
|
85
|
-
.integer("batchId")
|
|
86
|
-
.unsigned()
|
|
87
|
-
.nullable()
|
|
88
|
-
.references("id")
|
|
89
|
-
.inTable("video_batch")
|
|
90
|
-
.withKeyName("fk_video_batch")
|
|
91
|
-
.onDelete("SET NULL")
|
|
51
|
+
table.jsonb("trimPeriods").nullable()
|
|
52
|
+
.comment("Array of trim periods with startTime and endTime in ISO 8601 format");
|
|
53
|
+
|
|
54
|
+
table.integer("batchId").unsigned().nullable()
|
|
55
|
+
.references("id").inTable("video_batch").withKeyName("fk_video_batch").onDelete("SET NULL")
|
|
92
56
|
.comment("Foreign key to video_batch table (numerical ID, NOT UUID)");
|
|
93
57
|
});
|
|
94
58
|
|
|
@@ -29,11 +29,7 @@ export async function up(knex: Knex): Promise<void> {
|
|
|
29
29
|
|
|
30
30
|
// 5. Add new foreign key constraint with updated name
|
|
31
31
|
await knex.schema.alterTable("study", (table) => {
|
|
32
|
-
table
|
|
33
|
-
.foreign("locationId")
|
|
34
|
-
.references("id")
|
|
35
|
-
.inTable("locations")
|
|
36
|
-
.onDelete("SET NULL");
|
|
32
|
+
table.foreign("locationId").references("id").inTable("locations").onDelete("SET NULL");
|
|
37
33
|
});
|
|
38
34
|
|
|
39
35
|
// 6. Drop old index if it exists
|
|
@@ -66,11 +62,7 @@ export async function down(knex: Knex): Promise<void> {
|
|
|
66
62
|
|
|
67
63
|
// 4. Restore original foreign key constraint
|
|
68
64
|
await knex.schema.alterTable("study", (table) => {
|
|
69
|
-
table
|
|
70
|
-
.foreign("cameraId")
|
|
71
|
-
.references("id")
|
|
72
|
-
.inTable("locations")
|
|
73
|
-
.onDelete("SET NULL");
|
|
65
|
+
table.foreign("cameraId").references("id").inTable("locations").onDelete("SET NULL");
|
|
74
66
|
});
|
|
75
67
|
|
|
76
68
|
// 5. Restore original index
|
|
@@ -9,27 +9,15 @@ import { Knex } from "knex";
|
|
|
9
9
|
export async function up(knex: Knex): Promise<void> {
|
|
10
10
|
await knex.schema.createTable("cameras", (table) => {
|
|
11
11
|
table.increments("id").primary();
|
|
12
|
-
table
|
|
13
|
-
|
|
14
|
-
.
|
|
15
|
-
.unique()
|
|
16
|
-
.notNullable();
|
|
17
|
-
table
|
|
18
|
-
.integer("locationId")
|
|
19
|
-
.unsigned()
|
|
20
|
-
.notNullable()
|
|
21
|
-
.references("id")
|
|
22
|
-
.inTable("locations")
|
|
23
|
-
.onDelete("CASCADE");
|
|
12
|
+
table.uuid("uuid").defaultTo(knex.raw("uuid_generate_v4()")).unique().notNullable();
|
|
13
|
+
table.integer("locationId").unsigned().notNullable()
|
|
14
|
+
.references("id").inTable("locations").onDelete("CASCADE");
|
|
24
15
|
table.string("name", 100).notNullable();
|
|
25
16
|
table.text("description").nullable();
|
|
26
|
-
table
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
})
|
|
31
|
-
.defaultTo("ACTIVE")
|
|
32
|
-
.notNullable();
|
|
17
|
+
table.enu("status", ["ACTIVE", "INACTIVE", "MAINTENANCE"], {
|
|
18
|
+
useNative: true,
|
|
19
|
+
enumName: "camera_status_enum"
|
|
20
|
+
}).defaultTo("ACTIVE").notNullable();
|
|
33
21
|
table.jsonb("metadata").defaultTo("{}").notNullable();
|
|
34
22
|
table.timestamps(true, true);
|
|
35
23
|
|
|
@@ -8,13 +8,8 @@ import { Knex } from "knex";
|
|
|
8
8
|
*/
|
|
9
9
|
export async function up(knex: Knex): Promise<void> {
|
|
10
10
|
await knex.schema.alterTable("video", (table) => {
|
|
11
|
-
table
|
|
12
|
-
.
|
|
13
|
-
.unsigned()
|
|
14
|
-
.nullable()
|
|
15
|
-
.references("id")
|
|
16
|
-
.inTable("cameras")
|
|
17
|
-
.onDelete("SET NULL");
|
|
11
|
+
table.integer("cameraId").unsigned().nullable()
|
|
12
|
+
.references("id").inTable("cameras").onDelete("SET NULL");
|
|
18
13
|
table.index(["cameraId"], "video_cameraid_index");
|
|
19
14
|
});
|
|
20
15
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Video sorting constants
|
|
3
|
+
* Shared between DAO and API controller to ensure validation consistency
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const VALID_VIDEO_SORT_FIELDS = ['created_at', 'name', 'status'] as const;
|
|
7
|
+
export type VideoSortField = typeof VALID_VIDEO_SORT_FIELDS[number];
|
|
8
|
+
|
|
9
|
+
export const VALID_SORT_ORDERS = ['ASC', 'DESC'] as const;
|
|
10
|
+
export type SortOrder = typeof VALID_SORT_ORDERS[number];
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Maps API sort field names to database column names with table aliases
|
|
14
|
+
*/
|
|
15
|
+
export const VIDEO_SORT_COLUMN_MAP: Record<VideoSortField, string> = {
|
|
16
|
+
'created_at': 'v.created_at',
|
|
17
|
+
'name': 'v.name',
|
|
18
|
+
'status': 'v.status'
|
|
19
|
+
};
|
package/src/d.types.ts
CHANGED
|
@@ -1,22 +1,18 @@
|
|
|
1
1
|
export interface IDataPaginator<T> {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
success: boolean;
|
|
3
|
+
data: T[];
|
|
4
|
+
page: number;
|
|
5
|
+
limit: number;
|
|
6
|
+
count: number;
|
|
7
|
+
totalCount: number;
|
|
8
|
+
totalPages: number;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export interface IBaseDAO<T> {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
): Promise<IDataPaginator<T>>;
|
|
20
|
-
update(id: number, item: T): Promise<T | null>;
|
|
21
|
-
delete(id: number): Promise<boolean>;
|
|
22
|
-
}
|
|
12
|
+
create(item: T): Promise<T>;
|
|
13
|
+
getById(id: number): Promise<T | null>;
|
|
14
|
+
getByUuid(uuid: string): Promise<T | null>;
|
|
15
|
+
getAll(page: number, limit: number, entityId?: any | null): Promise<IDataPaginator<T>>;
|
|
16
|
+
update(id: number, item: T): Promise<T | null>;
|
|
17
|
+
delete(id: number): Promise<boolean>;
|
|
18
|
+
}
|
|
@@ -69,15 +69,15 @@ interface IGroupedResponse {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
interface IStudyTimeGroupResult {
|
|
72
|
-
absoluteTime: string;
|
|
72
|
+
absoluteTime: string; // ISO 8601 start of bucket
|
|
73
73
|
groupIndex: number;
|
|
74
|
-
startMinute: number;
|
|
75
|
-
endMinute: number;
|
|
76
|
-
label: string;
|
|
74
|
+
startMinute: number; // Start minute number (0-based from video start)
|
|
75
|
+
endMinute: number; // End minute number (0-based from video start)
|
|
76
|
+
label: string; // Formatted label
|
|
77
77
|
results: ITMCResult | IATRResult;
|
|
78
78
|
minuteCount: number;
|
|
79
79
|
videoCount: number;
|
|
80
|
-
contributingVideos: string[];
|
|
80
|
+
contributingVideos: string[]; // Video UUIDs
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
interface IGroupedStudyResponse {
|
|
@@ -87,7 +87,7 @@ interface IGroupedStudyResponse {
|
|
|
87
87
|
study: {
|
|
88
88
|
uuid: string;
|
|
89
89
|
name: string;
|
|
90
|
-
type:
|
|
90
|
+
type: 'TMC' | 'ATR';
|
|
91
91
|
status: string;
|
|
92
92
|
};
|
|
93
93
|
videoCount: number;
|
|
@@ -423,41 +423,37 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
|
|
|
423
423
|
// Use Knex query builder for safe parameter binding
|
|
424
424
|
const groupingQuery = this.knex(this.tableName)
|
|
425
425
|
.select(
|
|
426
|
-
this.knex.raw(
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
this.knex.raw(
|
|
430
|
-
this.knex.raw(
|
|
431
|
-
this.knex.raw("COUNT(*) as minute_count"),
|
|
432
|
-
this.knex.raw(
|
|
433
|
-
"array_agg(results ORDER BY minute_number) as all_results",
|
|
434
|
-
),
|
|
426
|
+
this.knex.raw('FLOOR(minute_number / ?) as group_index', [groupingMinutes]),
|
|
427
|
+
this.knex.raw('MIN(minute_number) as start_minute'),
|
|
428
|
+
this.knex.raw('MAX(minute_number) as end_minute'),
|
|
429
|
+
this.knex.raw('COUNT(*) as minute_count'),
|
|
430
|
+
this.knex.raw('array_agg(results ORDER BY minute_number) as all_results')
|
|
435
431
|
)
|
|
436
|
-
.where(
|
|
432
|
+
.where('video_id', video.id);
|
|
437
433
|
|
|
438
434
|
if (startMinute !== undefined) {
|
|
439
|
-
groupingQuery.where(
|
|
435
|
+
groupingQuery.where('minute_number', '>=', startMinute);
|
|
440
436
|
}
|
|
441
437
|
|
|
442
438
|
if (endMinute !== undefined) {
|
|
443
|
-
groupingQuery.where(
|
|
439
|
+
groupingQuery.where('minute_number', '<=', endMinute);
|
|
444
440
|
}
|
|
445
441
|
|
|
446
442
|
const rows = await groupingQuery
|
|
447
|
-
.groupBy(
|
|
448
|
-
.orderBy(
|
|
443
|
+
.groupBy('group_index')
|
|
444
|
+
.orderBy('group_index');
|
|
449
445
|
|
|
450
446
|
// Aggregate the results in TypeScript based on video type
|
|
451
447
|
const aggregatedGroups: IGroupedResult[] = rows.map((row: any) => {
|
|
452
|
-
if (!row || typeof row !==
|
|
453
|
-
throw new Error(
|
|
448
|
+
if (!row || typeof row !== 'object') {
|
|
449
|
+
throw new Error('Invalid row data received from database query');
|
|
454
450
|
}
|
|
455
451
|
|
|
456
452
|
const allResults = Array.isArray(row.all_results) ? row.all_results : [];
|
|
457
|
-
|
|
453
|
+
|
|
458
454
|
// Determine video type based on multiple factors
|
|
459
|
-
let studyType = video.videoType ||
|
|
460
|
-
|
|
455
|
+
let studyType = video.videoType || 'ATR'; // Default fallback to ATR
|
|
456
|
+
|
|
461
457
|
// Check if minute data has study_type field (ATR usually does)
|
|
462
458
|
if (allResults.length > 0 && allResults[0].study_type) {
|
|
463
459
|
studyType = allResults[0].study_type;
|
|
@@ -469,24 +465,20 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
|
|
|
469
465
|
const vehicleKeys = Object.keys(sampleResult.vehicles);
|
|
470
466
|
if (vehicleKeys.length > 0) {
|
|
471
467
|
const firstVehicleType = sampleResult.vehicles[vehicleKeys[0]];
|
|
472
|
-
if (firstVehicleType && typeof firstVehicleType ===
|
|
468
|
+
if (firstVehicleType && typeof firstVehicleType === 'object') {
|
|
473
469
|
const directions = Object.keys(firstVehicleType);
|
|
474
|
-
if (
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
directions.includes("EAST") ||
|
|
478
|
-
directions.includes("WEST")
|
|
479
|
-
) {
|
|
480
|
-
studyType = "TMC";
|
|
470
|
+
if (directions.includes('NORTH') || directions.includes('SOUTH') ||
|
|
471
|
+
directions.includes('EAST') || directions.includes('WEST')) {
|
|
472
|
+
studyType = 'TMC';
|
|
481
473
|
}
|
|
482
474
|
}
|
|
483
475
|
}
|
|
484
476
|
}
|
|
485
477
|
}
|
|
486
|
-
|
|
478
|
+
|
|
487
479
|
// Aggregate based on determined video type
|
|
488
480
|
let aggregatedResult;
|
|
489
|
-
if (studyType ===
|
|
481
|
+
if (studyType === 'TMC') {
|
|
490
482
|
aggregatedResult = this.aggregateTMCResults(allResults);
|
|
491
483
|
} else {
|
|
492
484
|
aggregatedResult = this.aggregateATRResults(allResults);
|
|
@@ -577,8 +569,8 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
|
|
|
577
569
|
"v.uuid as videoUuid",
|
|
578
570
|
"v.recordingStartedAt",
|
|
579
571
|
this.knex.raw(
|
|
580
|
-
'"v"."recordingStartedAt" + (vmr.minute_number || \' minutes\')::INTERVAL as "absoluteTime"'
|
|
581
|
-
)
|
|
572
|
+
'"v"."recordingStartedAt" + (vmr.minute_number || \' minutes\')::INTERVAL as "absoluteTime"'
|
|
573
|
+
)
|
|
582
574
|
)
|
|
583
575
|
.whereIn("v.id", videoIds)
|
|
584
576
|
.orderBy("absoluteTime", "asc");
|
|
@@ -616,7 +608,7 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
|
|
|
616
608
|
for (const minute of minuteResults) {
|
|
617
609
|
const absoluteTime = new Date(minute.absoluteTime);
|
|
618
610
|
const minutesSinceEarliest = Math.floor(
|
|
619
|
-
(absoluteTime.getTime() - earliestTime.getTime()) / (1000 * 60)
|
|
611
|
+
(absoluteTime.getTime() - earliestTime.getTime()) / (1000 * 60)
|
|
620
612
|
);
|
|
621
613
|
const bucketIndex = Math.floor(minutesSinceEarliest / groupingMinutes);
|
|
622
614
|
|
|
@@ -624,7 +616,7 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
|
|
|
624
616
|
// Calculate bucket start time
|
|
625
617
|
const bucketStartMinutes = bucketIndex * groupingMinutes;
|
|
626
618
|
const bucketStartTime = new Date(
|
|
627
|
-
earliestTime.getTime() + bucketStartMinutes * 60 * 1000
|
|
619
|
+
earliestTime.getTime() + bucketStartMinutes * 60 * 1000
|
|
628
620
|
);
|
|
629
621
|
|
|
630
622
|
buckets.set(bucketIndex, {
|
|
@@ -641,14 +633,12 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
|
|
|
641
633
|
}
|
|
642
634
|
|
|
643
635
|
// Step 5: Aggregate using existing methods based on study type
|
|
644
|
-
const aggregatedGroups: IStudyTimeGroupResult[] = Array.from(
|
|
645
|
-
buckets.values(),
|
|
646
|
-
)
|
|
636
|
+
const aggregatedGroups: IStudyTimeGroupResult[] = Array.from(buckets.values())
|
|
647
637
|
.sort((a, b) => a.groupIndex - b.groupIndex)
|
|
648
638
|
.map((bucket) => {
|
|
649
639
|
let aggregatedResult: ITMCResult | IATRResult;
|
|
650
640
|
|
|
651
|
-
if (study.type ===
|
|
641
|
+
if (study.type === 'TMC') {
|
|
652
642
|
aggregatedResult = this.aggregateTMCResults(bucket.results);
|
|
653
643
|
} else {
|
|
654
644
|
aggregatedResult = this.aggregateATRResults(bucket.results);
|
|
@@ -659,10 +649,7 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
|
|
|
659
649
|
groupIndex: bucket.groupIndex,
|
|
660
650
|
startMinute: bucket.groupIndex * groupingMinutes,
|
|
661
651
|
endMinute: (bucket.groupIndex + 1) * groupingMinutes - 1,
|
|
662
|
-
label: this.formatStudyTimeLabel(
|
|
663
|
-
bucket.absoluteTime,
|
|
664
|
-
groupingMinutes,
|
|
665
|
-
),
|
|
652
|
+
label: this.formatStudyTimeLabel(bucket.absoluteTime, groupingMinutes),
|
|
666
653
|
results: aggregatedResult,
|
|
667
654
|
minuteCount: bucket.results.length,
|
|
668
655
|
videoCount: bucket.videoUuids.size,
|
|
@@ -725,27 +712,25 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
|
|
|
725
712
|
vehicles: {},
|
|
726
713
|
counts: {
|
|
727
714
|
total_vehicles: 0,
|
|
728
|
-
entry_vehicles: 0
|
|
715
|
+
entry_vehicles: 0
|
|
729
716
|
},
|
|
730
717
|
total: 0,
|
|
731
718
|
totalcount: 0,
|
|
732
719
|
detected_classes: {},
|
|
733
|
-
study_type: "TMC"
|
|
720
|
+
study_type: "TMC"
|
|
734
721
|
};
|
|
735
722
|
|
|
736
723
|
for (const minute of minutes) {
|
|
737
724
|
const results = minute; // minute is already the results object from array_agg
|
|
738
725
|
|
|
739
726
|
// Aggregate vehicle movements by class and direction
|
|
740
|
-
if (results.vehicles && typeof results.vehicles ===
|
|
741
|
-
for (const [vehicleClass, directions] of Object.entries(
|
|
742
|
-
results.vehicles,
|
|
743
|
-
)) {
|
|
727
|
+
if (results.vehicles && typeof results.vehicles === 'object') {
|
|
728
|
+
for (const [vehicleClass, directions] of Object.entries(results.vehicles)) {
|
|
744
729
|
// Skip the 'total' pseudo vehicle class - validate it's actually aggregate data
|
|
745
|
-
if (vehicleClass ===
|
|
730
|
+
if (vehicleClass === 'total' && typeof directions === 'number') {
|
|
746
731
|
continue; // This is aggregate total data, not a vehicle class
|
|
747
732
|
}
|
|
748
|
-
|
|
733
|
+
|
|
749
734
|
if (!aggregated.vehicles[vehicleClass]) {
|
|
750
735
|
aggregated.vehicles[vehicleClass] = {};
|
|
751
736
|
}
|
|
@@ -768,12 +753,11 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
|
|
|
768
753
|
for (const [turnType, count] of Object.entries(turns)) {
|
|
769
754
|
const turnCount = (count as number) || 0;
|
|
770
755
|
aggregated.vehicles[vehicleClass][direction][turnType] =
|
|
771
|
-
(aggregated.vehicles[vehicleClass][direction][turnType] ||
|
|
772
|
-
|
|
773
|
-
|
|
756
|
+
(aggregated.vehicles[vehicleClass][direction][turnType] || 0) + turnCount;
|
|
757
|
+
|
|
774
758
|
// Add to detected_classes count for this vehicle type
|
|
775
759
|
aggregated.detected_classes[vehicleClass] += turnCount;
|
|
776
|
-
|
|
760
|
+
|
|
777
761
|
// Add to total counts
|
|
778
762
|
aggregated.total += turnCount;
|
|
779
763
|
aggregated.totalcount += turnCount;
|
|
@@ -781,12 +765,10 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
|
|
|
781
765
|
}
|
|
782
766
|
}
|
|
783
767
|
}
|
|
784
|
-
|
|
768
|
+
|
|
785
769
|
// Also process the 'total' entry for validation but don't count it as a vehicle class
|
|
786
770
|
if (results.vehicles.total) {
|
|
787
|
-
for (const [direction, turns] of Object.entries(
|
|
788
|
-
results.vehicles.total as any,
|
|
789
|
-
)) {
|
|
771
|
+
for (const [direction, turns] of Object.entries(results.vehicles.total as any)) {
|
|
790
772
|
if (typeof turns === "object" && turns !== null) {
|
|
791
773
|
for (const [turnType, count] of Object.entries(turns)) {
|
|
792
774
|
const turnCount = (count as number) || 0;
|
|
@@ -799,8 +781,7 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
|
|
|
799
781
|
}
|
|
800
782
|
|
|
801
783
|
// Set entry_vehicles same as total_vehicles for TMC
|
|
802
|
-
aggregated.counts.entry_vehicles =
|
|
803
|
-
aggregated.counts.total_vehicles || aggregated.total;
|
|
784
|
+
aggregated.counts.entry_vehicles = aggregated.counts.total_vehicles || aggregated.total;
|
|
804
785
|
|
|
805
786
|
return aggregated;
|
|
806
787
|
}
|
|
@@ -824,7 +805,7 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
|
|
|
824
805
|
if (results.vehicles) {
|
|
825
806
|
for (const [vehicleClass, lanes] of Object.entries(results.vehicles)) {
|
|
826
807
|
// Skip 'total' pseudo-class if present
|
|
827
|
-
if (vehicleClass ===
|
|
808
|
+
if (vehicleClass === 'total') {
|
|
828
809
|
continue;
|
|
829
810
|
}
|
|
830
811
|
|
|
@@ -833,8 +814,7 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
|
|
|
833
814
|
}
|
|
834
815
|
|
|
835
816
|
for (const [laneId, count] of Object.entries(lanes as any)) {
|
|
836
|
-
const numericCount =
|
|
837
|
-
typeof count === "number" ? count : parseInt(String(count)) || 0;
|
|
817
|
+
const numericCount = typeof count === 'number' ? count : (parseInt(String(count)) || 0);
|
|
838
818
|
aggregated.vehicles[vehicleClass][laneId] =
|
|
839
819
|
(aggregated.vehicles[vehicleClass][laneId] || 0) + numericCount;
|
|
840
820
|
}
|
|
@@ -941,10 +921,7 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
|
|
|
941
921
|
* Used when results are already grouped by date in the UI
|
|
942
922
|
* Uses UTC time for consistency with absoluteTime field
|
|
943
923
|
*/
|
|
944
|
-
private formatStudyTimeLabel(
|
|
945
|
-
startTime: Date,
|
|
946
|
-
durationMinutes: number,
|
|
947
|
-
): string {
|
|
924
|
+
private formatStudyTimeLabel(startTime: Date, durationMinutes: number): string {
|
|
948
925
|
const endTime = new Date(startTime.getTime() + durationMinutes * 60 * 1000);
|
|
949
926
|
|
|
950
927
|
const formatTime = (date: Date): string => {
|