@trafficgroup/knex-rel 0.1.19 → 0.1.21

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