@trafficgroup/knex-rel 0.1.17 → 0.1.19

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 (128) hide show
  1. package/.claude/settings.local.json +5 -2
  2. package/CLAUDE.md +2 -11
  3. package/dist/constants/folder.constants.d.ts +2 -2
  4. package/dist/constants/folder.constants.js +6 -15
  5. package/dist/constants/folder.constants.js.map +1 -1
  6. package/dist/constants/study.constants.d.ts +10 -0
  7. package/dist/constants/study.constants.js +18 -0
  8. package/dist/constants/study.constants.js.map +1 -0
  9. package/dist/constants/video.constants.d.ts +2 -2
  10. package/dist/constants/video.constants.js +5 -9
  11. package/dist/constants/video.constants.js.map +1 -1
  12. package/dist/dao/VideoMinuteResultDAO.d.ts +1 -1
  13. package/dist/dao/VideoMinuteResultDAO.js +23 -29
  14. package/dist/dao/VideoMinuteResultDAO.js.map +1 -1
  15. package/dist/dao/auth/auth.dao.js +1 -4
  16. package/dist/dao/auth/auth.dao.js.map +1 -1
  17. package/dist/dao/batch/batch.dao.js +14 -13
  18. package/dist/dao/batch/batch.dao.js.map +1 -1
  19. package/dist/dao/camera/camera.dao.js +7 -10
  20. package/dist/dao/camera/camera.dao.js.map +1 -1
  21. package/dist/dao/chat/chat.dao.d.ts +1 -1
  22. package/dist/dao/chat/chat.dao.js +40 -27
  23. package/dist/dao/chat/chat.dao.js.map +1 -1
  24. package/dist/dao/folder/folder.dao.js +7 -18
  25. package/dist/dao/folder/folder.dao.js.map +1 -1
  26. package/dist/dao/location/location.dao.js +9 -16
  27. package/dist/dao/location/location.dao.js.map +1 -1
  28. package/dist/dao/message/message.dao.d.ts +1 -1
  29. package/dist/dao/message/message.dao.js +26 -18
  30. package/dist/dao/message/message.dao.js.map +1 -1
  31. package/dist/dao/reconciliation-log/reconciliation-log.dao.d.ts +6 -0
  32. package/dist/dao/reconciliation-log/reconciliation-log.dao.js +39 -0
  33. package/dist/dao/reconciliation-log/reconciliation-log.dao.js.map +1 -0
  34. package/dist/dao/report-configuration/report-configuration.dao.js +31 -32
  35. package/dist/dao/report-configuration/report-configuration.dao.js.map +1 -1
  36. package/dist/dao/study/study.dao.d.ts +8 -1
  37. package/dist/dao/study/study.dao.js +20 -11
  38. package/dist/dao/study/study.dao.js.map +1 -1
  39. package/dist/dao/systemConfiguration/SystemConfigurationDAO.d.ts +2 -2
  40. package/dist/dao/systemConfiguration/SystemConfigurationDAO.js +26 -26
  41. package/dist/dao/systemConfiguration/SystemConfigurationDAO.js.map +1 -1
  42. package/dist/dao/user/user.dao.js +1 -4
  43. package/dist/dao/user/user.dao.js.map +1 -1
  44. package/dist/dao/user-push-notification-token/user-push-notification-token.dao.js +8 -26
  45. package/dist/dao/user-push-notification-token/user-push-notification-token.dao.js.map +1 -1
  46. package/dist/dao/video/video.dao.js +28 -30
  47. package/dist/dao/video/video.dao.js.map +1 -1
  48. package/dist/index.d.ts +12 -7
  49. package/dist/index.js +6 -1
  50. package/dist/index.js.map +1 -1
  51. package/dist/interfaces/batch/batch.interfaces.d.ts +1 -1
  52. package/dist/interfaces/camera/camera.interfaces.d.ts +1 -1
  53. package/dist/interfaces/chat/chat.interfaces.d.ts +3 -3
  54. package/dist/interfaces/folder/folder.interfaces.d.ts +1 -1
  55. package/dist/interfaces/message/message.interfaces.d.ts +2 -2
  56. package/dist/interfaces/reconciliation-log/reconciliation-log.interfaces.d.ts +15 -0
  57. package/dist/interfaces/reconciliation-log/reconciliation-log.interfaces.js +3 -0
  58. package/dist/interfaces/reconciliation-log/reconciliation-log.interfaces.js.map +1 -0
  59. package/dist/interfaces/study/study.interfaces.d.ts +2 -2
  60. package/dist/interfaces/user/user.interfaces.d.ts +1 -1
  61. package/dist/interfaces/user-push-notification-token/user-push-notification-token.interfaces.d.ts +1 -1
  62. package/dist/interfaces/video/video.interfaces.d.ts +2 -2
  63. package/migrations/20250717160737_migration.ts +1 -1
  64. package/migrations/20250717160908_migration.ts +2 -5
  65. package/migrations/20250717161310_migration.ts +1 -1
  66. package/migrations/20250717161406_migration.ts +3 -3
  67. package/migrations/20250717162431_migration.ts +1 -1
  68. package/migrations/20250717173228_migration.ts +2 -2
  69. package/migrations/20250717204731_migration.ts +1 -1
  70. package/migrations/20250722210109_migration.ts +4 -8
  71. package/migrations/20250722211019_migration.ts +1 -1
  72. package/migrations/20250723153852_migration.ts +10 -13
  73. package/migrations/20250723162257_migration.ts +7 -4
  74. package/migrations/20250723171109_migration.ts +7 -4
  75. package/migrations/20250723205331_migration.ts +9 -6
  76. package/migrations/20250724191345_migration.ts +11 -8
  77. package/migrations/20250730180932_migration.ts +13 -14
  78. package/migrations/20250730213625_migration.ts +11 -8
  79. package/migrations/20250804124509_migration.ts +21 -26
  80. package/migrations/20250804132053_migration.ts +8 -5
  81. package/migrations/20250804164518_migration.ts +7 -7
  82. package/migrations/20250823223016_migration.ts +21 -32
  83. package/migrations/20250910015452_migration.ts +6 -18
  84. package/migrations/20250911000000_migration.ts +4 -18
  85. package/migrations/20250917144153_migration.ts +7 -14
  86. package/migrations/20250930200521_migration.ts +4 -8
  87. package/migrations/20251010143500_migration.ts +6 -27
  88. package/migrations/20251020225758_migration.ts +15 -51
  89. package/migrations/20251112120000_migration.ts +2 -10
  90. package/migrations/20251112120200_migration.ts +7 -19
  91. package/migrations/20251112120300_migration.ts +2 -7
  92. package/migrations/20260109140000_migration.ts +2 -7
  93. package/migrations/20260209140000_migration.ts +27 -0
  94. package/package.json +1 -1
  95. package/src/constants/folder.constants.ts +8 -17
  96. package/src/constants/study.constants.ts +17 -0
  97. package/src/constants/video.constants.ts +7 -11
  98. package/src/d.types.ts +14 -18
  99. package/src/dao/VideoMinuteResultDAO.ts +52 -83
  100. package/src/dao/auth/auth.dao.ts +55 -58
  101. package/src/dao/batch/batch.dao.ts +98 -101
  102. package/src/dao/camera/camera.dao.ts +121 -124
  103. package/src/dao/chat/chat.dao.ts +45 -45
  104. package/src/dao/folder/folder.dao.ts +90 -105
  105. package/src/dao/location/location.dao.ts +87 -109
  106. package/src/dao/message/message.dao.ts +32 -32
  107. package/src/dao/reconciliation-log/reconciliation-log.dao.ts +26 -0
  108. package/src/dao/report-configuration/report-configuration.dao.ts +342 -370
  109. package/src/dao/study/study.dao.ts +84 -88
  110. package/src/dao/systemConfiguration/SystemConfigurationDAO.ts +35 -41
  111. package/src/dao/user/user.dao.ts +50 -52
  112. package/src/dao/user-push-notification-token/user-push-notification-token.dao.ts +48 -80
  113. package/src/dao/video/video.dao.ts +345 -396
  114. package/src/entities/BaseEntity.ts +1 -1
  115. package/src/index.ts +31 -37
  116. package/src/interfaces/auth/auth.interfaces.ts +10 -10
  117. package/src/interfaces/batch/batch.interfaces.ts +1 -1
  118. package/src/interfaces/camera/camera.interfaces.ts +9 -9
  119. package/src/interfaces/chat/chat.interfaces.ts +4 -4
  120. package/src/interfaces/folder/folder.interfaces.ts +2 -2
  121. package/src/interfaces/location/location.interfaces.ts +7 -7
  122. package/src/interfaces/message/message.interfaces.ts +3 -3
  123. package/src/interfaces/reconciliation-log/reconciliation-log.interfaces.ts +15 -0
  124. package/src/interfaces/report-configuration/report-configuration.interfaces.ts +16 -16
  125. package/src/interfaces/study/study.interfaces.ts +3 -3
  126. package/src/interfaces/user/user.interfaces.ts +9 -9
  127. package/src/interfaces/user-push-notification-token/user-push-notification-token.interfaces.ts +9 -9
  128. package/src/interfaces/video/video.interfaces.ts +34 -34
@@ -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
  }
@@ -4,11 +4,7 @@ export async function up(knex: Knex): Promise<void> {
4
4
  // Create system_configuration table
5
5
  await knex.schema.createTable("system_configuration", (table) => {
6
6
  table.increments("id").primary();
7
- table
8
- .uuid("configUuid")
9
- .notNullable()
10
- .unique()
11
- .defaultTo(knex.raw("uuid_generate_v4()"));
7
+ table.uuid("configUuid").notNullable().unique().defaultTo(knex.raw("uuid_generate_v4()"));
12
8
  table.string("key").notNullable().unique();
13
9
  table.text("value").notNullable();
14
10
  table.string("dataType").notNullable();
@@ -27,8 +23,7 @@ export async function up(knex: Knex): Promise<void> {
27
23
  key: "batch_email_enabled",
28
24
  value: "false",
29
25
  dataType: "boolean",
30
- description:
31
- "If it's true, send a single email upon completion of the batch. If it's false, send one email for each video.",
26
+ description: "If it's true, send a single email upon completion of the batch. If it's false, send one email for each video."
32
27
  });
33
28
  }
34
29
 
@@ -0,0 +1,27 @@
1
+ import type { Knex } from "knex";
2
+
3
+ export async function up(knex: Knex): Promise<void> {
4
+ await knex.schema.createTable("reconciliation_log", (table) => {
5
+ table.increments("id").primary();
6
+ table.timestamp("startedAt").notNullable();
7
+ table.timestamp("completedAt").notNullable();
8
+ table.integer("totalS3Objects").notNullable();
9
+ table.integer("totalDbReferences").notNullable();
10
+ table.integer("orphanedCount").notNullable();
11
+ table.integer("deletedCount").notNullable();
12
+ table.integer("errorCount").notNullable();
13
+ table.integer("durationMs").notNullable();
14
+ table.boolean("dryRun").notNullable();
15
+ table.string("scanType", 20).notNullable();
16
+ table.timestamp("scanFrom").nullable();
17
+ table.jsonb("errors").defaultTo("[]");
18
+ });
19
+
20
+ await knex.schema.alterTable("reconciliation_log", (table) => {
21
+ table.index("startedAt", "idx_reconciliation_log_started_at");
22
+ });
23
+ }
24
+
25
+ export async function down(knex: Knex): Promise<void> {
26
+ await knex.schema.dropTableIfExists("reconciliation_log");
27
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trafficgroup/knex-rel",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "description": "Knex Module",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
@@ -3,27 +3,18 @@
3
3
  * Shared between DAO and API controller to ensure validation consistency
4
4
  */
5
5
 
6
- export const VALID_FOLDER_SORT_FIELDS = [
7
- "created_at",
8
- "name",
9
- "status",
10
- "updated_at",
11
- ] as const;
12
- export type FolderSortField = (typeof VALID_FOLDER_SORT_FIELDS)[number];
6
+ export const VALID_FOLDER_SORT_FIELDS = ['created_at', 'name', 'status', 'updated_at'] as const;
7
+ export type FolderSortField = typeof VALID_FOLDER_SORT_FIELDS[number];
13
8
 
14
- export const VALID_FOLDER_STATUSES = [
15
- "UPLOADING",
16
- "COMPLETED",
17
- "FAILED",
18
- ] as const;
19
- export type FolderStatus = (typeof VALID_FOLDER_STATUSES)[number];
9
+ export const VALID_FOLDER_STATUSES = ['UPLOADING', 'COMPLETED', 'FAILED'] as const;
10
+ export type FolderStatus = typeof VALID_FOLDER_STATUSES[number];
20
11
 
21
12
  /**
22
13
  * Maps API sort field names to database column names with table aliases
23
14
  */
24
15
  export const FOLDER_SORT_COLUMN_MAP: Record<FolderSortField, string> = {
25
- created_at: "f.created_at",
26
- name: "f.name",
27
- status: "f.status",
28
- updated_at: "f.updated_at",
16
+ 'created_at': 'f.created_at',
17
+ 'name': 'f.name',
18
+ 'status': 'f.status',
19
+ 'updated_at': 'f.updated_at'
29
20
  };
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Study sorting and filtering constants
3
+ * Shared between DAO and API controller to ensure validation consistency
4
+ */
5
+
6
+ export const VALID_STUDY_SORT_FIELDS = ['created_at', 'name', 'type', 'status'] as const;
7
+ export type StudySortField = typeof VALID_STUDY_SORT_FIELDS[number];
8
+
9
+ /**
10
+ * Maps API sort field names to database column names with table aliases
11
+ */
12
+ export const STUDY_SORT_COLUMN_MAP: Record<StudySortField, string> = {
13
+ 'created_at': 's.created_at',
14
+ 'name': 's.name',
15
+ 'type': 's.type',
16
+ 'status': 's.status'
17
+ };
@@ -3,21 +3,17 @@
3
3
  * Shared between DAO and API controller to ensure validation consistency
4
4
  */
5
5
 
6
- export const VALID_VIDEO_SORT_FIELDS = [
7
- "created_at",
8
- "name",
9
- "status",
10
- ] as const;
11
- export type VideoSortField = (typeof VALID_VIDEO_SORT_FIELDS)[number];
6
+ export const VALID_VIDEO_SORT_FIELDS = ['created_at', 'name', 'status'] as const;
7
+ export type VideoSortField = typeof VALID_VIDEO_SORT_FIELDS[number];
12
8
 
13
- export const VALID_SORT_ORDERS = ["ASC", "DESC"] as const;
14
- export type SortOrder = (typeof VALID_SORT_ORDERS)[number];
9
+ export const VALID_SORT_ORDERS = ['ASC', 'DESC'] as const;
10
+ export type SortOrder = typeof VALID_SORT_ORDERS[number];
15
11
 
16
12
  /**
17
13
  * Maps API sort field names to database column names with table aliases
18
14
  */
19
15
  export const VIDEO_SORT_COLUMN_MAP: Record<VideoSortField, string> = {
20
- created_at: "v.created_at",
21
- name: "v.name",
22
- status: "v.status",
16
+ 'created_at': 'v.created_at',
17
+ 'name': 'v.name',
18
+ 'status': 'v.status'
23
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;
@@ -406,11 +406,7 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
406
406
  groupIndex: row.minute_number,
407
407
  startMinute: row.minute_number,
408
408
  endMinute: row.minute_number,
409
- label: this.formatTimeLabel(
410
- row.minute_number,
411
- row.minute_number,
412
- video.recordingStartedAt,
413
- ),
409
+ label: this.formatTimeLabel(row.minute_number, row.minute_number, video.recordingStartedAt),
414
410
  results: row.results,
415
411
  minuteCount: 1,
416
412
  })),
@@ -427,41 +423,37 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
427
423
  // Use Knex query builder for safe parameter binding
428
424
  const groupingQuery = this.knex(this.tableName)
429
425
  .select(
430
- this.knex.raw("FLOOR(minute_number / ?) as group_index", [
431
- groupingMinutes,
432
- ]),
433
- this.knex.raw("MIN(minute_number) as start_minute"),
434
- this.knex.raw("MAX(minute_number) as end_minute"),
435
- this.knex.raw("COUNT(*) as minute_count"),
436
- this.knex.raw(
437
- "array_agg(results ORDER BY minute_number) as all_results",
438
- ),
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')
439
431
  )
440
- .where("video_id", video.id);
432
+ .where('video_id', video.id);
441
433
 
442
434
  if (startMinute !== undefined) {
443
- groupingQuery.where("minute_number", ">=", startMinute);
435
+ groupingQuery.where('minute_number', '>=', startMinute);
444
436
  }
445
437
 
446
438
  if (endMinute !== undefined) {
447
- groupingQuery.where("minute_number", "<=", endMinute);
439
+ groupingQuery.where('minute_number', '<=', endMinute);
448
440
  }
449
441
 
450
442
  const rows = await groupingQuery
451
- .groupBy("group_index")
452
- .orderBy("group_index");
443
+ .groupBy('group_index')
444
+ .orderBy('group_index');
453
445
 
454
446
  // Aggregate the results in TypeScript based on video type
455
447
  const aggregatedGroups: IGroupedResult[] = rows.map((row: any) => {
456
- if (!row || typeof row !== "object") {
457
- 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');
458
450
  }
459
451
 
460
452
  const allResults = Array.isArray(row.all_results) ? row.all_results : [];
461
-
453
+
462
454
  // Determine video type based on multiple factors
463
- let studyType = video.videoType || "ATR"; // Default fallback to ATR
464
-
455
+ let studyType = video.videoType || 'ATR'; // Default fallback to ATR
456
+
465
457
  // Check if minute data has study_type field (ATR usually does)
466
458
  if (allResults.length > 0 && allResults[0].study_type) {
467
459
  studyType = allResults[0].study_type;
@@ -473,24 +465,20 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
473
465
  const vehicleKeys = Object.keys(sampleResult.vehicles);
474
466
  if (vehicleKeys.length > 0) {
475
467
  const firstVehicleType = sampleResult.vehicles[vehicleKeys[0]];
476
- if (firstVehicleType && typeof firstVehicleType === "object") {
468
+ if (firstVehicleType && typeof firstVehicleType === 'object') {
477
469
  const directions = Object.keys(firstVehicleType);
478
- if (
479
- directions.includes("NORTH") ||
480
- directions.includes("SOUTH") ||
481
- directions.includes("EAST") ||
482
- directions.includes("WEST")
483
- ) {
484
- studyType = "TMC";
470
+ if (directions.includes('NORTH') || directions.includes('SOUTH') ||
471
+ directions.includes('EAST') || directions.includes('WEST')) {
472
+ studyType = 'TMC';
485
473
  }
486
474
  }
487
475
  }
488
476
  }
489
477
  }
490
-
478
+
491
479
  // Aggregate based on determined video type
492
480
  let aggregatedResult;
493
- if (studyType === "TMC") {
481
+ if (studyType === 'TMC') {
494
482
  aggregatedResult = this.aggregateTMCResults(allResults);
495
483
  } else {
496
484
  aggregatedResult = this.aggregateATRResults(allResults);
@@ -500,11 +488,7 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
500
488
  groupIndex: row.group_index,
501
489
  startMinute: row.start_minute,
502
490
  endMinute: row.end_minute,
503
- label: this.formatTimeLabel(
504
- row.start_minute,
505
- row.end_minute,
506
- video.recordingStartedAt,
507
- ),
491
+ label: this.formatTimeLabel(row.start_minute, row.end_minute, video.recordingStartedAt),
508
492
  results: aggregatedResult,
509
493
  minuteCount: row.minute_count,
510
494
  };
@@ -585,8 +569,8 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
585
569
  "v.uuid as videoUuid",
586
570
  "v.recordingStartedAt",
587
571
  this.knex.raw(
588
- '"v"."recordingStartedAt" + (vmr.minute_number || \' minutes\')::INTERVAL as "absoluteTime"',
589
- ),
572
+ '"v"."recordingStartedAt" + (vmr.minute_number || \' minutes\')::INTERVAL as "absoluteTime"'
573
+ )
590
574
  )
591
575
  .whereIn("v.id", videoIds)
592
576
  .orderBy("absoluteTime", "asc");
@@ -624,7 +608,7 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
624
608
  for (const minute of minuteResults) {
625
609
  const absoluteTime = new Date(minute.absoluteTime);
626
610
  const minutesSinceEarliest = Math.floor(
627
- (absoluteTime.getTime() - earliestTime.getTime()) / (1000 * 60),
611
+ (absoluteTime.getTime() - earliestTime.getTime()) / (1000 * 60)
628
612
  );
629
613
  const bucketIndex = Math.floor(minutesSinceEarliest / groupingMinutes);
630
614
 
@@ -632,7 +616,7 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
632
616
  // Calculate bucket start time
633
617
  const bucketStartMinutes = bucketIndex * groupingMinutes;
634
618
  const bucketStartTime = new Date(
635
- earliestTime.getTime() + bucketStartMinutes * 60 * 1000,
619
+ earliestTime.getTime() + bucketStartMinutes * 60 * 1000
636
620
  );
637
621
 
638
622
  buckets.set(bucketIndex, {
@@ -649,14 +633,12 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
649
633
  }
650
634
 
651
635
  // Step 5: Aggregate using existing methods based on study type
652
- const aggregatedGroups: IStudyTimeGroupResult[] = Array.from(
653
- buckets.values(),
654
- )
636
+ const aggregatedGroups: IStudyTimeGroupResult[] = Array.from(buckets.values())
655
637
  .sort((a, b) => a.groupIndex - b.groupIndex)
656
638
  .map((bucket) => {
657
639
  let aggregatedResult: ITMCResult | IATRResult;
658
640
 
659
- if (study.type === "TMC") {
641
+ if (study.type === 'TMC') {
660
642
  aggregatedResult = this.aggregateTMCResults(bucket.results);
661
643
  } else {
662
644
  aggregatedResult = this.aggregateATRResults(bucket.results);
@@ -667,10 +649,7 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
667
649
  groupIndex: bucket.groupIndex,
668
650
  startMinute: bucket.groupIndex * groupingMinutes,
669
651
  endMinute: (bucket.groupIndex + 1) * groupingMinutes - 1,
670
- label: this.formatStudyTimeLabel(
671
- bucket.absoluteTime,
672
- groupingMinutes,
673
- ),
652
+ label: this.formatStudyTimeLabel(bucket.absoluteTime, groupingMinutes),
674
653
  results: aggregatedResult,
675
654
  minuteCount: bucket.results.length,
676
655
  videoCount: bucket.videoUuids.size,
@@ -733,27 +712,25 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
733
712
  vehicles: {},
734
713
  counts: {
735
714
  total_vehicles: 0,
736
- entry_vehicles: 0,
715
+ entry_vehicles: 0
737
716
  },
738
717
  total: 0,
739
718
  totalcount: 0,
740
719
  detected_classes: {},
741
- study_type: "TMC",
720
+ study_type: "TMC"
742
721
  };
743
722
 
744
723
  for (const minute of minutes) {
745
724
  const results = minute; // minute is already the results object from array_agg
746
725
 
747
726
  // Aggregate vehicle movements by class and direction
748
- if (results.vehicles && typeof results.vehicles === "object") {
749
- for (const [vehicleClass, directions] of Object.entries(
750
- results.vehicles,
751
- )) {
727
+ if (results.vehicles && typeof results.vehicles === 'object') {
728
+ for (const [vehicleClass, directions] of Object.entries(results.vehicles)) {
752
729
  // Skip the 'total' pseudo vehicle class - validate it's actually aggregate data
753
- if (vehicleClass === "total" && typeof directions === "number") {
730
+ if (vehicleClass === 'total' && typeof directions === 'number') {
754
731
  continue; // This is aggregate total data, not a vehicle class
755
732
  }
756
-
733
+
757
734
  if (!aggregated.vehicles[vehicleClass]) {
758
735
  aggregated.vehicles[vehicleClass] = {};
759
736
  }
@@ -776,12 +753,11 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
776
753
  for (const [turnType, count] of Object.entries(turns)) {
777
754
  const turnCount = (count as number) || 0;
778
755
  aggregated.vehicles[vehicleClass][direction][turnType] =
779
- (aggregated.vehicles[vehicleClass][direction][turnType] ||
780
- 0) + turnCount;
781
-
756
+ (aggregated.vehicles[vehicleClass][direction][turnType] || 0) + turnCount;
757
+
782
758
  // Add to detected_classes count for this vehicle type
783
759
  aggregated.detected_classes[vehicleClass] += turnCount;
784
-
760
+
785
761
  // Add to total counts
786
762
  aggregated.total += turnCount;
787
763
  aggregated.totalcount += turnCount;
@@ -789,12 +765,10 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
789
765
  }
790
766
  }
791
767
  }
792
-
768
+
793
769
  // Also process the 'total' entry for validation but don't count it as a vehicle class
794
770
  if (results.vehicles.total) {
795
- for (const [direction, turns] of Object.entries(
796
- results.vehicles.total as any,
797
- )) {
771
+ for (const [direction, turns] of Object.entries(results.vehicles.total as any)) {
798
772
  if (typeof turns === "object" && turns !== null) {
799
773
  for (const [turnType, count] of Object.entries(turns)) {
800
774
  const turnCount = (count as number) || 0;
@@ -807,8 +781,7 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
807
781
  }
808
782
 
809
783
  // Set entry_vehicles same as total_vehicles for TMC
810
- aggregated.counts.entry_vehicles =
811
- aggregated.counts.total_vehicles || aggregated.total;
784
+ aggregated.counts.entry_vehicles = aggregated.counts.total_vehicles || aggregated.total;
812
785
 
813
786
  return aggregated;
814
787
  }
@@ -832,7 +805,7 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
832
805
  if (results.vehicles) {
833
806
  for (const [vehicleClass, lanes] of Object.entries(results.vehicles)) {
834
807
  // Skip 'total' pseudo-class if present
835
- if (vehicleClass === "total") {
808
+ if (vehicleClass === 'total') {
836
809
  continue;
837
810
  }
838
811
 
@@ -841,8 +814,7 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
841
814
  }
842
815
 
843
816
  for (const [laneId, count] of Object.entries(lanes as any)) {
844
- const numericCount =
845
- typeof count === "number" ? count : parseInt(String(count)) || 0;
817
+ const numericCount = typeof count === 'number' ? count : (parseInt(String(count)) || 0);
846
818
  aggregated.vehicles[vehicleClass][laneId] =
847
819
  (aggregated.vehicles[vehicleClass][laneId] || 0) + numericCount;
848
820
  }
@@ -939,7 +911,7 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
939
911
  private formatTimeLabel(
940
912
  startMinute: number,
941
913
  endMinute: number,
942
- recordingStartedAt?: Date | string | null,
914
+ recordingStartedAt?: Date | string | null
943
915
  ): string {
944
916
  if (recordingStartedAt) {
945
917
  // Calculate actual wall-clock times using recordingStartedAt
@@ -978,10 +950,7 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
978
950
  * Used when results are already grouped by date in the UI
979
951
  * Uses UTC time for consistency with absoluteTime field
980
952
  */
981
- private formatStudyTimeLabel(
982
- startTime: Date,
983
- durationMinutes: number,
984
- ): string {
953
+ private formatStudyTimeLabel(startTime: Date, durationMinutes: number): string {
985
954
  const endTime = new Date(startTime.getTime() + durationMinutes * 60 * 1000);
986
955
 
987
956
  const formatTime = (date: Date): string => {