@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.
- package/.claude/settings.local.json +2 -5
- package/CLAUDE.md +11 -2
- package/dist/constants/folder.constants.d.ts +2 -2
- package/dist/constants/folder.constants.js +15 -6
- package/dist/constants/folder.constants.js.map +1 -1
- package/dist/constants/study.constants.d.ts +2 -2
- package/dist/constants/study.constants.js +11 -6
- package/dist/constants/study.constants.js.map +1 -1
- package/dist/constants/video.constants.d.ts +2 -2
- package/dist/constants/video.constants.js +9 -5
- package/dist/constants/video.constants.js.map +1 -1
- package/dist/dao/VideoMinuteResultDAO.d.ts +1 -1
- package/dist/dao/VideoMinuteResultDAO.js +29 -23
- package/dist/dao/VideoMinuteResultDAO.js.map +1 -1
- package/dist/dao/auth/auth.dao.js +4 -1
- package/dist/dao/auth/auth.dao.js.map +1 -1
- package/dist/dao/batch/batch.dao.d.ts +10 -0
- package/dist/dao/batch/batch.dao.js +51 -14
- package/dist/dao/batch/batch.dao.js.map +1 -1
- package/dist/dao/camera/camera.dao.js +10 -7
- 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 +27 -40
- package/dist/dao/chat/chat.dao.js.map +1 -1
- package/dist/dao/folder/folder.dao.js +18 -7
- package/dist/dao/folder/folder.dao.js.map +1 -1
- package/dist/dao/location/location.dao.js +16 -9
- 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 +18 -26
- package/dist/dao/message/message.dao.js.map +1 -1
- package/dist/dao/report-configuration/report-configuration.dao.js +32 -31
- package/dist/dao/report-configuration/report-configuration.dao.js.map +1 -1
- package/dist/dao/study/study.dao.d.ts +1 -8
- package/dist/dao/study/study.dao.js +15 -20
- 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 +4 -1
- package/dist/dao/user/user.dao.js.map +1 -1
- package/dist/dao/user-push-notification-token/user-push-notification-token.dao.js +26 -8
- package/dist/dao/user-push-notification-token/user-push-notification-token.dao.js.map +1 -1
- package/dist/dao/video/video.dao.js +30 -28
- package/dist/dao/video/video.dao.js.map +1 -1
- package/dist/index.d.ts +9 -10
- package/dist/index.js +4 -4
- 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 +8 -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 +5 -2
- 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 +8 -4
- package/migrations/20250722211019_migration.ts +1 -1
- package/migrations/20250723153852_migration.ts +13 -10
- package/migrations/20250723162257_migration.ts +4 -7
- package/migrations/20250723171109_migration.ts +4 -7
- package/migrations/20250723205331_migration.ts +6 -9
- package/migrations/20250724191345_migration.ts +8 -11
- package/migrations/20250730180932_migration.ts +14 -13
- package/migrations/20250730213625_migration.ts +8 -11
- package/migrations/20250804124509_migration.ts +26 -21
- package/migrations/20250804132053_migration.ts +5 -8
- package/migrations/20250804164518_migration.ts +7 -7
- package/migrations/20250823223016_migration.ts +32 -21
- package/migrations/20250910015452_migration.ts +18 -6
- package/migrations/20250911000000_migration.ts +18 -4
- package/migrations/20250917144153_migration.ts +14 -7
- package/migrations/20250930200521_migration.ts +8 -4
- package/migrations/20251010143500_migration.ts +27 -6
- package/migrations/20251020225758_migration.ts +51 -15
- package/migrations/20251112120000_migration.ts +10 -2
- package/migrations/20251112120200_migration.ts +19 -7
- package/migrations/20251112120300_migration.ts +7 -2
- package/migrations/20260109140000_migration.ts +7 -2
- package/package.json +1 -1
- package/src/constants/folder.constants.ts +17 -8
- package/src/constants/study.constants.ts +12 -7
- package/src/constants/video.constants.ts +11 -7
- package/src/d.types.ts +18 -14
- package/src/dao/VideoMinuteResultDAO.ts +83 -52
- package/src/dao/auth/auth.dao.ts +58 -55
- package/src/dao/batch/batch.dao.ts +145 -100
- package/src/dao/camera/camera.dao.ts +124 -121
- package/src/dao/chat/chat.dao.ts +45 -45
- package/src/dao/folder/folder.dao.ts +105 -90
- package/src/dao/location/location.dao.ts +109 -87
- 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 +370 -342
- package/src/dao/study/study.dao.ts +94 -83
- package/src/dao/systemConfiguration/SystemConfigurationDAO.ts +41 -35
- package/src/dao/user/user.dao.ts +52 -50
- package/src/dao/user-push-notification-token/user-push-notification-token.dao.ts +80 -48
- package/src/dao/video/video.dao.ts +396 -345
- package/src/entities/BaseEntity.ts +1 -1
- package/src/index.ts +43 -29
- 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 +10 -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
|
@@ -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
|
|
13
|
-
|
|
14
|
-
.
|
|
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
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
12
|
-
.
|
|
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
|
|
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:
|
|
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
|
@@ -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 = [
|
|
7
|
-
|
|
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 = [
|
|
10
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
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 = [
|
|
7
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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 = [
|
|
7
|
-
|
|
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 = [
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
|
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,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(
|
|
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(
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
this.knex.raw(
|
|
430
|
-
this.knex.raw(
|
|
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(
|
|
440
|
+
.where("video_id", video.id);
|
|
433
441
|
|
|
434
442
|
if (startMinute !== undefined) {
|
|
435
|
-
groupingQuery.where(
|
|
443
|
+
groupingQuery.where("minute_number", ">=", startMinute);
|
|
436
444
|
}
|
|
437
445
|
|
|
438
446
|
if (endMinute !== undefined) {
|
|
439
|
-
groupingQuery.where(
|
|
447
|
+
groupingQuery.where("minute_number", "<=", endMinute);
|
|
440
448
|
}
|
|
441
449
|
|
|
442
450
|
const rows = await groupingQuery
|
|
443
|
-
.groupBy(
|
|
444
|
-
.orderBy(
|
|
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 !==
|
|
449
|
-
throw new Error(
|
|
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 ||
|
|
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 ===
|
|
476
|
+
if (firstVehicleType && typeof firstVehicleType === "object") {
|
|
469
477
|
const directions = Object.keys(firstVehicleType);
|
|
470
|
-
if (
|
|
471
|
-
|
|
472
|
-
|
|
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 ===
|
|
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(
|
|
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(
|
|
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 ===
|
|
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(
|
|
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 ===
|
|
728
|
-
for (const [vehicleClass, directions] of Object.entries(
|
|
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 ===
|
|
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] ||
|
|
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(
|
|
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 =
|
|
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 ===
|
|
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 =
|
|
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(
|
|
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 => {
|
package/src/dao/auth/auth.dao.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
+
}
|