@trafficgroup/knex-rel 0.1.18 → 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.
- package/.claude/settings.local.json +5 -2
- package/CLAUDE.md +2 -11
- package/dist/constants/folder.constants.d.ts +2 -2
- package/dist/constants/folder.constants.js +6 -15
- package/dist/constants/folder.constants.js.map +1 -1
- package/dist/constants/study.constants.d.ts +10 -0
- package/dist/constants/study.constants.js +18 -0
- package/dist/constants/study.constants.js.map +1 -0
- package/dist/constants/video.constants.d.ts +2 -2
- package/dist/constants/video.constants.js +5 -9
- package/dist/constants/video.constants.js.map +1 -1
- package/dist/dao/VideoMinuteResultDAO.d.ts +1 -1
- package/dist/dao/VideoMinuteResultDAO.js +23 -29
- package/dist/dao/VideoMinuteResultDAO.js.map +1 -1
- package/dist/dao/auth/auth.dao.js +1 -4
- package/dist/dao/auth/auth.dao.js.map +1 -1
- package/dist/dao/batch/batch.dao.js +14 -13
- package/dist/dao/batch/batch.dao.js.map +1 -1
- package/dist/dao/camera/camera.dao.js +7 -10
- package/dist/dao/camera/camera.dao.js.map +1 -1
- package/dist/dao/chat/chat.dao.d.ts +1 -1
- package/dist/dao/chat/chat.dao.js +40 -27
- package/dist/dao/chat/chat.dao.js.map +1 -1
- package/dist/dao/folder/folder.dao.js +7 -18
- package/dist/dao/folder/folder.dao.js.map +1 -1
- package/dist/dao/location/location.dao.js +9 -16
- package/dist/dao/location/location.dao.js.map +1 -1
- package/dist/dao/message/message.dao.d.ts +1 -1
- package/dist/dao/message/message.dao.js +26 -18
- package/dist/dao/message/message.dao.js.map +1 -1
- package/dist/dao/report-configuration/report-configuration.dao.js +31 -32
- package/dist/dao/report-configuration/report-configuration.dao.js.map +1 -1
- package/dist/dao/study/study.dao.d.ts +8 -1
- package/dist/dao/study/study.dao.js +20 -11
- package/dist/dao/study/study.dao.js.map +1 -1
- package/dist/dao/systemConfiguration/SystemConfigurationDAO.d.ts +2 -2
- package/dist/dao/systemConfiguration/SystemConfigurationDAO.js +26 -26
- package/dist/dao/systemConfiguration/SystemConfigurationDAO.js.map +1 -1
- package/dist/dao/user/user.dao.js +1 -4
- package/dist/dao/user/user.dao.js.map +1 -1
- package/dist/dao/user-push-notification-token/user-push-notification-token.dao.js +8 -26
- package/dist/dao/user-push-notification-token/user-push-notification-token.dao.js.map +1 -1
- package/dist/dao/video/video.dao.js +28 -30
- package/dist/dao/video/video.dao.js.map +1 -1
- package/dist/index.d.ts +10 -7
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/interfaces/batch/batch.interfaces.d.ts +1 -1
- package/dist/interfaces/camera/camera.interfaces.d.ts +1 -1
- package/dist/interfaces/chat/chat.interfaces.d.ts +3 -3
- package/dist/interfaces/folder/folder.interfaces.d.ts +1 -1
- package/dist/interfaces/message/message.interfaces.d.ts +2 -2
- package/dist/interfaces/study/study.interfaces.d.ts +2 -2
- package/dist/interfaces/user/user.interfaces.d.ts +1 -1
- package/dist/interfaces/user-push-notification-token/user-push-notification-token.interfaces.d.ts +1 -1
- package/dist/interfaces/video/video.interfaces.d.ts +2 -2
- package/migrations/20250717160737_migration.ts +1 -1
- package/migrations/20250717160908_migration.ts +2 -5
- package/migrations/20250717161310_migration.ts +1 -1
- package/migrations/20250717161406_migration.ts +3 -3
- package/migrations/20250717162431_migration.ts +1 -1
- package/migrations/20250717173228_migration.ts +2 -2
- package/migrations/20250717204731_migration.ts +1 -1
- package/migrations/20250722210109_migration.ts +4 -8
- package/migrations/20250722211019_migration.ts +1 -1
- package/migrations/20250723153852_migration.ts +10 -13
- package/migrations/20250723162257_migration.ts +7 -4
- package/migrations/20250723171109_migration.ts +7 -4
- package/migrations/20250723205331_migration.ts +9 -6
- package/migrations/20250724191345_migration.ts +11 -8
- package/migrations/20250730180932_migration.ts +13 -14
- package/migrations/20250730213625_migration.ts +11 -8
- package/migrations/20250804124509_migration.ts +21 -26
- package/migrations/20250804132053_migration.ts +8 -5
- package/migrations/20250804164518_migration.ts +7 -7
- package/migrations/20250823223016_migration.ts +21 -32
- package/migrations/20250910015452_migration.ts +6 -18
- package/migrations/20250911000000_migration.ts +4 -18
- package/migrations/20250917144153_migration.ts +7 -14
- package/migrations/20250930200521_migration.ts +4 -8
- package/migrations/20251010143500_migration.ts +6 -27
- package/migrations/20251020225758_migration.ts +15 -51
- package/migrations/20251112120000_migration.ts +2 -10
- package/migrations/20251112120200_migration.ts +7 -19
- package/migrations/20251112120300_migration.ts +2 -7
- package/migrations/20260109140000_migration.ts +2 -7
- package/package.json +1 -1
- package/src/constants/folder.constants.ts +8 -17
- package/src/constants/study.constants.ts +17 -0
- package/src/constants/video.constants.ts +7 -11
- package/src/d.types.ts +14 -18
- package/src/dao/VideoMinuteResultDAO.ts +52 -83
- package/src/dao/auth/auth.dao.ts +55 -58
- package/src/dao/batch/batch.dao.ts +98 -101
- package/src/dao/camera/camera.dao.ts +121 -124
- package/src/dao/chat/chat.dao.ts +45 -45
- package/src/dao/folder/folder.dao.ts +90 -105
- package/src/dao/location/location.dao.ts +87 -109
- package/src/dao/message/message.dao.ts +32 -32
- package/src/dao/reconciliation-log/reconciliation-log.dao.ts +1 -1
- package/src/dao/report-configuration/report-configuration.dao.ts +342 -370
- package/src/dao/study/study.dao.ts +84 -88
- package/src/dao/systemConfiguration/SystemConfigurationDAO.ts +35 -41
- package/src/dao/user/user.dao.ts +50 -52
- package/src/dao/user-push-notification-token/user-push-notification-token.dao.ts +48 -80
- package/src/dao/video/video.dao.ts +345 -396
- package/src/entities/BaseEntity.ts +1 -1
- package/src/index.ts +29 -37
- package/src/interfaces/auth/auth.interfaces.ts +10 -10
- package/src/interfaces/batch/batch.interfaces.ts +1 -1
- package/src/interfaces/camera/camera.interfaces.ts +9 -9
- package/src/interfaces/chat/chat.interfaces.ts +4 -4
- package/src/interfaces/folder/folder.interfaces.ts +2 -2
- package/src/interfaces/location/location.interfaces.ts +7 -7
- package/src/interfaces/message/message.interfaces.ts +3 -3
- package/src/interfaces/report-configuration/report-configuration.interfaces.ts +16 -16
- package/src/interfaces/study/study.interfaces.ts +3 -3
- package/src/interfaces/user/user.interfaces.ts +9 -9
- package/src/interfaces/user-push-notification-token/user-push-notification-token.interfaces.ts +9 -9
- package/src/interfaces/video/video.interfaces.ts +34 -34
|
@@ -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
|
|
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
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 = [
|
|
14
|
-
export type SortOrder =
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
success: boolean;
|
|
3
|
+
data: T[];
|
|
4
|
+
page: number;
|
|
5
|
+
limit: number;
|
|
6
|
+
count: number;
|
|
7
|
+
totalCount: number;
|
|
8
|
+
totalPages: number;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export interface IBaseDAO<T> {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
): Promise<IDataPaginator<T>>;
|
|
20
|
-
update(id: number, item: T): Promise<T | null>;
|
|
21
|
-
delete(id: number): Promise<boolean>;
|
|
22
|
-
}
|
|
12
|
+
create(item: T): Promise<T>;
|
|
13
|
+
getById(id: number): Promise<T | null>;
|
|
14
|
+
getByUuid(uuid: string): Promise<T | null>;
|
|
15
|
+
getAll(page: number, limit: number, entityId?: any | null): Promise<IDataPaginator<T>>;
|
|
16
|
+
update(id: number, item: T): Promise<T | null>;
|
|
17
|
+
delete(id: number): Promise<boolean>;
|
|
18
|
+
}
|
|
@@ -69,15 +69,15 @@ interface IGroupedResponse {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
interface IStudyTimeGroupResult {
|
|
72
|
-
absoluteTime: string;
|
|
72
|
+
absoluteTime: string; // ISO 8601 start of bucket
|
|
73
73
|
groupIndex: number;
|
|
74
|
-
startMinute: number;
|
|
75
|
-
endMinute: number;
|
|
76
|
-
label: string;
|
|
74
|
+
startMinute: number; // Start minute number (0-based from video start)
|
|
75
|
+
endMinute: number; // End minute number (0-based from video start)
|
|
76
|
+
label: string; // Formatted label
|
|
77
77
|
results: ITMCResult | IATRResult;
|
|
78
78
|
minuteCount: number;
|
|
79
79
|
videoCount: number;
|
|
80
|
-
contributingVideos: string[];
|
|
80
|
+
contributingVideos: string[]; // Video UUIDs
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
interface IGroupedStudyResponse {
|
|
@@ -87,7 +87,7 @@ interface IGroupedStudyResponse {
|
|
|
87
87
|
study: {
|
|
88
88
|
uuid: string;
|
|
89
89
|
name: string;
|
|
90
|
-
type:
|
|
90
|
+
type: 'TMC' | 'ATR';
|
|
91
91
|
status: string;
|
|
92
92
|
};
|
|
93
93
|
videoCount: number;
|
|
@@ -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(
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
this.knex.raw(
|
|
434
|
-
this.knex.raw(
|
|
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(
|
|
432
|
+
.where('video_id', video.id);
|
|
441
433
|
|
|
442
434
|
if (startMinute !== undefined) {
|
|
443
|
-
groupingQuery.where(
|
|
435
|
+
groupingQuery.where('minute_number', '>=', startMinute);
|
|
444
436
|
}
|
|
445
437
|
|
|
446
438
|
if (endMinute !== undefined) {
|
|
447
|
-
groupingQuery.where(
|
|
439
|
+
groupingQuery.where('minute_number', '<=', endMinute);
|
|
448
440
|
}
|
|
449
441
|
|
|
450
442
|
const rows = await groupingQuery
|
|
451
|
-
.groupBy(
|
|
452
|
-
.orderBy(
|
|
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 !==
|
|
457
|
-
throw new Error(
|
|
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 ||
|
|
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 ===
|
|
468
|
+
if (firstVehicleType && typeof firstVehicleType === 'object') {
|
|
477
469
|
const directions = Object.keys(firstVehicleType);
|
|
478
|
-
if (
|
|
479
|
-
|
|
480
|
-
|
|
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 ===
|
|
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 ===
|
|
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 ===
|
|
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 ===
|
|
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
|
-
|
|
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 ===
|
|
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 => {
|
package/src/dao/auth/auth.dao.ts
CHANGED
|
@@ -4,61 +4,58 @@ import { IAuth } from "../../interfaces/auth/auth.interfaces";
|
|
|
4
4
|
import KnexManager from "../../KnexConnection";
|
|
5
5
|
|
|
6
6
|
export class AuthDAO implements IBaseDAO<IAuth> {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
}
|
|
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
|
+
}
|