@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.
Files changed (107) hide show
  1. package/.claude/settings.local.json +5 -2
  2. package/.env.prod +5 -0
  3. package/CLAUDE.md +2 -11
  4. package/dist/constants/video.constants.d.ts +12 -0
  5. package/dist/constants/video.constants.js +18 -0
  6. package/dist/constants/video.constants.js.map +1 -0
  7. package/dist/dao/VideoMinuteResultDAO.d.ts +1 -1
  8. package/dist/dao/VideoMinuteResultDAO.js +23 -29
  9. package/dist/dao/VideoMinuteResultDAO.js.map +1 -1
  10. package/dist/dao/auth/auth.dao.js +1 -4
  11. package/dist/dao/auth/auth.dao.js.map +1 -1
  12. package/dist/dao/batch/batch.dao.js +14 -13
  13. package/dist/dao/batch/batch.dao.js.map +1 -1
  14. package/dist/dao/camera/camera.dao.js +7 -10
  15. package/dist/dao/camera/camera.dao.js.map +1 -1
  16. package/dist/dao/chat/chat.dao.d.ts +1 -1
  17. package/dist/dao/chat/chat.dao.js +35 -25
  18. package/dist/dao/chat/chat.dao.js.map +1 -1
  19. package/dist/dao/folder/folder.dao.js +2 -7
  20. package/dist/dao/folder/folder.dao.js.map +1 -1
  21. package/dist/dao/location/location.dao.js +9 -16
  22. package/dist/dao/location/location.dao.js.map +1 -1
  23. package/dist/dao/message/message.dao.d.ts +1 -1
  24. package/dist/dao/message/message.dao.js +26 -18
  25. package/dist/dao/message/message.dao.js.map +1 -1
  26. package/dist/dao/report-configuration/report-configuration.dao.js +31 -32
  27. package/dist/dao/report-configuration/report-configuration.dao.js.map +1 -1
  28. package/dist/dao/study/study.dao.js +2 -7
  29. package/dist/dao/study/study.dao.js.map +1 -1
  30. package/dist/dao/user/user.dao.js +1 -4
  31. package/dist/dao/user/user.dao.js.map +1 -1
  32. package/dist/dao/user-push-notification-token/user-push-notification-token.dao.js +8 -26
  33. package/dist/dao/user-push-notification-token/user-push-notification-token.dao.js.map +1 -1
  34. package/dist/dao/video/video.dao.d.ts +2 -1
  35. package/dist/dao/video/video.dao.js +42 -28
  36. package/dist/dao/video/video.dao.js.map +1 -1
  37. package/dist/index.d.ts +6 -4
  38. package/dist/index.js +6 -1
  39. package/dist/index.js.map +1 -1
  40. package/dist/interfaces/batch/batch.interfaces.d.ts +1 -1
  41. package/dist/interfaces/camera/camera.interfaces.d.ts +1 -1
  42. package/dist/interfaces/chat/chat.interfaces.d.ts +3 -3
  43. package/dist/interfaces/folder/folder.interfaces.d.ts +1 -1
  44. package/dist/interfaces/message/message.interfaces.d.ts +2 -2
  45. package/dist/interfaces/study/study.interfaces.d.ts +2 -2
  46. package/dist/interfaces/user/user.interfaces.d.ts +1 -1
  47. package/dist/interfaces/user-push-notification-token/user-push-notification-token.interfaces.d.ts +1 -1
  48. package/dist/interfaces/video/video.interfaces.d.ts +2 -2
  49. package/migrations/20250717160737_migration.ts +1 -1
  50. package/migrations/20250717160908_migration.ts +2 -5
  51. package/migrations/20250717161310_migration.ts +1 -1
  52. package/migrations/20250717161406_migration.ts +3 -3
  53. package/migrations/20250717162431_migration.ts +1 -1
  54. package/migrations/20250717173228_migration.ts +2 -2
  55. package/migrations/20250717204731_migration.ts +1 -1
  56. package/migrations/20250722210109_migration.ts +4 -8
  57. package/migrations/20250722211019_migration.ts +1 -1
  58. package/migrations/20250723153852_migration.ts +10 -13
  59. package/migrations/20250723162257_migration.ts +7 -4
  60. package/migrations/20250723171109_migration.ts +7 -4
  61. package/migrations/20250723205331_migration.ts +9 -6
  62. package/migrations/20250724191345_migration.ts +11 -8
  63. package/migrations/20250730180932_migration.ts +13 -14
  64. package/migrations/20250730213625_migration.ts +11 -8
  65. package/migrations/20250804124509_migration.ts +21 -26
  66. package/migrations/20250804132053_migration.ts +8 -5
  67. package/migrations/20250804164518_migration.ts +7 -7
  68. package/migrations/20250823223016_migration.ts +21 -32
  69. package/migrations/20250910015452_migration.ts +6 -18
  70. package/migrations/20250911000000_migration.ts +4 -18
  71. package/migrations/20250917144153_migration.ts +7 -14
  72. package/migrations/20250930200521_migration.ts +4 -8
  73. package/migrations/20251010143500_migration.ts +6 -27
  74. package/migrations/20251020225758_migration.ts +15 -51
  75. package/migrations/20251112120000_migration.ts +2 -10
  76. package/migrations/20251112120200_migration.ts +7 -19
  77. package/migrations/20251112120300_migration.ts +2 -7
  78. package/package.json +1 -1
  79. package/src/constants/video.constants.ts +19 -0
  80. package/src/d.types.ts +14 -18
  81. package/src/dao/VideoMinuteResultDAO.ts +49 -72
  82. package/src/dao/auth/auth.dao.ts +55 -58
  83. package/src/dao/batch/batch.dao.ts +98 -101
  84. package/src/dao/camera/camera.dao.ts +121 -124
  85. package/src/dao/chat/chat.dao.ts +43 -45
  86. package/src/dao/folder/folder.dao.ts +56 -65
  87. package/src/dao/location/location.dao.ts +87 -109
  88. package/src/dao/message/message.dao.ts +32 -32
  89. package/src/dao/report-configuration/report-configuration.dao.ts +342 -370
  90. package/src/dao/study/study.dao.ts +63 -88
  91. package/src/dao/user/user.dao.ts +50 -52
  92. package/src/dao/user-push-notification-token/user-push-notification-token.dao.ts +49 -83
  93. package/src/dao/video/video.dao.ts +339 -357
  94. package/src/entities/BaseEntity.ts +1 -1
  95. package/src/index.ts +22 -26
  96. package/src/interfaces/auth/auth.interfaces.ts +10 -10
  97. package/src/interfaces/batch/batch.interfaces.ts +1 -1
  98. package/src/interfaces/camera/camera.interfaces.ts +9 -9
  99. package/src/interfaces/chat/chat.interfaces.ts +4 -4
  100. package/src/interfaces/folder/folder.interfaces.ts +2 -2
  101. package/src/interfaces/location/location.interfaces.ts +7 -7
  102. package/src/interfaces/message/message.interfaces.ts +3 -3
  103. package/src/interfaces/report-configuration/report-configuration.interfaces.ts +16 -16
  104. package/src/interfaces/study/study.interfaces.ts +3 -3
  105. package/src/interfaces/user/user.interfaces.ts +9 -9
  106. package/src/interfaces/user-push-notification-token/user-push-notification-token.interfaces.ts +9 -9
  107. 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
- .integer("folderId")
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
- .jsonb("trimPeriods")
79
- .nullable()
80
- .comment(
81
- "Array of trim periods with startTime and endTime in ISO 8601 format",
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
- .uuid("uuid")
14
- .defaultTo(knex.raw("uuid_generate_v4()"))
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
- .enu("status", ["ACTIVE", "INACTIVE", "MAINTENANCE"], {
28
- useNative: true,
29
- enumName: "camera_status_enum",
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
- .integer("cameraId")
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trafficgroup/knex-rel",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
4
4
  "description": "Knex Module",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
@@ -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
- success: boolean;
3
- data: T[];
4
- page: number;
5
- limit: number;
6
- count: number;
7
- totalCount: number;
8
- totalPages: number;
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
- create(item: T): Promise<T>;
13
- getById(id: number): Promise<T | null>;
14
- getByUuid(uuid: string): Promise<T | null>;
15
- getAll(
16
- page: number,
17
- limit: number,
18
- entityId?: any | null,
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; // ISO 8601 start of bucket
72
+ absoluteTime: string; // ISO 8601 start of bucket
73
73
  groupIndex: number;
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
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[]; // Video UUIDs
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: "TMC" | "ATR";
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("FLOOR(minute_number / ?) as group_index", [
427
- groupingMinutes,
428
- ]),
429
- this.knex.raw("MIN(minute_number) as start_minute"),
430
- this.knex.raw("MAX(minute_number) as end_minute"),
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("video_id", video.id);
432
+ .where('video_id', video.id);
437
433
 
438
434
  if (startMinute !== undefined) {
439
- groupingQuery.where("minute_number", ">=", startMinute);
435
+ groupingQuery.where('minute_number', '>=', startMinute);
440
436
  }
441
437
 
442
438
  if (endMinute !== undefined) {
443
- groupingQuery.where("minute_number", "<=", endMinute);
439
+ groupingQuery.where('minute_number', '<=', endMinute);
444
440
  }
445
441
 
446
442
  const rows = await groupingQuery
447
- .groupBy("group_index")
448
- .orderBy("group_index");
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 !== "object") {
453
- throw new Error("Invalid row data received from database query");
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 || "ATR"; // Default fallback to ATR
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 === "object") {
468
+ if (firstVehicleType && typeof firstVehicleType === 'object') {
473
469
  const directions = Object.keys(firstVehicleType);
474
- if (
475
- directions.includes("NORTH") ||
476
- directions.includes("SOUTH") ||
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 === "TMC") {
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 === "TMC") {
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 === "object") {
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 === "total" && typeof directions === "number") {
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
- 0) + turnCount;
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 === "total") {
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 => {