@trafficgroup/knex-rel 0.1.3 → 0.1.4

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 (30) hide show
  1. package/dist/dao/VideoMinuteResultDAO.d.ts +0 -4
  2. package/dist/dao/VideoMinuteResultDAO.js +7 -48
  3. package/dist/dao/VideoMinuteResultDAO.js.map +1 -1
  4. package/dist/dao/camera/camera.dao.d.ts +0 -2
  5. package/dist/dao/camera/camera.dao.js +0 -42
  6. package/dist/dao/camera/camera.dao.js.map +1 -1
  7. package/dist/dao/report-configuration/report-configuration.dao.d.ts +94 -0
  8. package/dist/dao/report-configuration/report-configuration.dao.js +352 -0
  9. package/dist/dao/report-configuration/report-configuration.dao.js.map +1 -0
  10. package/dist/dao/video/video.dao.d.ts +0 -3
  11. package/dist/dao/video/video.dao.js +4 -43
  12. package/dist/dao/video/video.dao.js.map +1 -1
  13. package/dist/index.d.ts +2 -0
  14. package/dist/index.js +3 -1
  15. package/dist/index.js.map +1 -1
  16. package/dist/interfaces/report-configuration/report-configuration.interfaces.d.ts +26 -0
  17. package/dist/interfaces/report-configuration/report-configuration.interfaces.js +3 -0
  18. package/dist/interfaces/report-configuration/report-configuration.interfaces.js.map +1 -0
  19. package/migrations/20250930200521_migration.ts +52 -0
  20. package/package.json +1 -1
  21. package/plan.md +950 -204
  22. package/src/dao/VideoMinuteResultDAO.ts +7 -64
  23. package/src/dao/camera/camera.dao.ts +0 -55
  24. package/src/dao/report-configuration/report-configuration.dao.ts +402 -0
  25. package/src/dao/video/video.dao.ts +4 -51
  26. package/src/index.ts +8 -0
  27. package/src/interfaces/report-configuration/report-configuration.interfaces.ts +30 -0
  28. package/cameras_analysis.md +0 -199
  29. package/folder_cameraid_analysis.md +0 -167
  30. package/migrations/20250924000000_camera_name_search_index.ts +0 -22
@@ -615,11 +615,11 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
615
615
 
616
616
  // Aggregate vehicle counts by class and lane
617
617
  if (results.vehicles) {
618
- for (const [rawVehicleClass, lanes] of Object.entries(
619
- results.vehicles,
620
- )) {
621
- // Normalize vehicle class names to standard format for frontend compatibility
622
- const vehicleClass = this.normalizeATRVehicleClass(rawVehicleClass);
618
+ for (const [vehicleClass, lanes] of Object.entries(results.vehicles)) {
619
+ // Skip 'total' pseudo-class if present
620
+ if (vehicleClass === "total") {
621
+ continue;
622
+ }
623
623
 
624
624
  if (!aggregated.vehicles[vehicleClass]) {
625
625
  aggregated.vehicles[vehicleClass] = {};
@@ -645,12 +645,9 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
645
645
  // Aggregate total count
646
646
  aggregated.total_count += results.total_count || 0;
647
647
 
648
- // Aggregate detected classes with normalized names
648
+ // Aggregate detected classes (use raw detection labels)
649
649
  if (results.detected_classes) {
650
- for (const [rawCls, count] of Object.entries(
651
- results.detected_classes,
652
- )) {
653
- const cls = this.normalizeATRVehicleClass(rawCls);
650
+ for (const [cls, count] of Object.entries(results.detected_classes)) {
654
651
  aggregated.detected_classes[cls] =
655
652
  (aggregated.detected_classes[cls] || 0) + ((count as number) || 0);
656
653
  }
@@ -715,60 +712,6 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
715
712
  return cloned;
716
713
  }
717
714
 
718
- /**
719
- * Normalize ATR vehicle class names to standard format for frontend compatibility
720
- */
721
- private normalizeATRVehicleClass(rawVehicleClass: string): string {
722
- const normalized = rawVehicleClass.toLowerCase().replace(/[_\s-]/g, "");
723
-
724
- // Map raw vehicle classes to standard classes
725
- if (
726
- normalized.includes("car") ||
727
- normalized === "vehicle" ||
728
- normalized === "automobiles"
729
- ) {
730
- return "cars";
731
- }
732
- if (
733
- normalized.includes("medium") ||
734
- normalized.includes("pickup") ||
735
- (normalized.includes("truck") && !normalized.includes("heavy"))
736
- ) {
737
- return "mediums";
738
- }
739
- if (
740
- normalized.includes("heavy") ||
741
- normalized.includes("largetruck") ||
742
- normalized.includes("bigtruck")
743
- ) {
744
- return "heavy_trucks";
745
- }
746
- if (
747
- normalized.includes("pedestrian") ||
748
- normalized.includes("person") ||
749
- normalized.includes("people")
750
- ) {
751
- return "pedestrians";
752
- }
753
- if (
754
- normalized.includes("bicycle") ||
755
- normalized.includes("bike") ||
756
- normalized.includes("cyclist")
757
- ) {
758
- return "bicycles";
759
- }
760
- if (normalized.includes("total") || normalized.includes("all")) {
761
- return "total";
762
- }
763
-
764
- // Handle specific known ATR classes
765
- if (rawVehicleClass === "mediums") return "mediums";
766
- if (rawVehicleClass === "heavy_trucks") return "heavy_trucks";
767
-
768
- // Default fallback for unknown classes
769
- return "cars";
770
- }
771
-
772
715
  /**
773
716
  * Format time label for display (HH:MM:SS format)
774
717
  */
@@ -76,59 +76,4 @@ export class CameraDAO implements IBaseDAO<ICamera> {
76
76
  );
77
77
  return cameras;
78
78
  }
79
-
80
- async getAllWithSearch(
81
- page: number,
82
- limit: number,
83
- name?: string,
84
- ): Promise<IDataPaginator<ICamera>> {
85
- const offset = (page - 1) * limit;
86
-
87
- const query = this._knex("cameras");
88
-
89
- if (name && name.trim() !== "") {
90
- query.where("name", "ilike", `%${name.trim()}%`);
91
- }
92
-
93
- const [countResult] = await query.clone().count("* as count");
94
- const totalCount = +countResult.count;
95
- const cameras = await query.clone().limit(limit).offset(offset);
96
-
97
- return {
98
- success: true,
99
- data: cameras,
100
- page,
101
- limit,
102
- count: cameras.length,
103
- totalCount,
104
- totalPages: Math.ceil(totalCount / limit),
105
- };
106
- }
107
-
108
- async getVideosByCamera(
109
- cameraId: number,
110
- page: number,
111
- limit: number,
112
- ): Promise<IDataPaginator<any>> {
113
- const offset = (page - 1) * limit;
114
-
115
- const query = this._knex("videos as v")
116
- .innerJoin("folders as f", "v.folderId", "f.id")
117
- .where("v.cameraId", cameraId)
118
- .select("v.*", this._knex.raw("to_jsonb(f.*) as folder"));
119
-
120
- const [countResult] = await query.clone().clearSelect().count("* as count");
121
- const totalCount = +countResult.count;
122
- const videos = await query.clone().limit(limit).offset(offset);
123
-
124
- return {
125
- success: true,
126
- data: videos,
127
- page,
128
- limit,
129
- count: videos.length,
130
- totalCount,
131
- totalPages: Math.ceil(totalCount / limit),
132
- };
133
- }
134
79
  }
@@ -0,0 +1,402 @@
1
+ import { Knex } from "knex";
2
+ import { IBaseDAO, IDataPaginator } from "../../d.types";
3
+ import {
4
+ IReportConfiguration,
5
+ IReportConfigurationData,
6
+ IReportConfigurationInput,
7
+ IValidationResult,
8
+ } from "../../interfaces/report-configuration/report-configuration.interfaces";
9
+ import KnexManager from "../../KnexConnection";
10
+
11
+ /**
12
+ * Mapping from detection labels to FHWA vehicle classes (1-13)
13
+ *
14
+ * FHWA Classes:
15
+ * - Class 1: Motorcycles
16
+ * - Class 2: Cars (passenger cars)
17
+ * - Class 3: Pickup trucks, vans, motorized vehicles
18
+ * - Class 4: Buses
19
+ * - Class 5: Work vans (2-axle, 6-tire single units)
20
+ * - Classes 6-8: Single unit trucks
21
+ * - Classes 9-13: Articulated trucks (semi-trailers, multi-trailers)
22
+ *
23
+ * Non-motorized vehicles (pedestrian, bicycle, non_motorized_vehicle) are EXCLUDED
24
+ */
25
+ const DETECTION_LABEL_TO_FHWA: Record<string, number[]> = {
26
+ motorcycle: [1],
27
+ car: [2],
28
+ pickup_truck: [3],
29
+ motorized_vehicle: [3], // Maps to Class 3 (same as pickup_truck)
30
+ bus: [4],
31
+ work_van: [5],
32
+ single_unit_truck: [6, 7, 8], // Classes 6-8
33
+ articulated_truck: [9, 10, 11, 12, 13], // Classes 9-13
34
+ // pedestrian, bicycle, non_motorized_vehicle are EXCLUDED
35
+ };
36
+
37
+ export class ReportConfigurationDAO implements IBaseDAO<IReportConfiguration> {
38
+ private _knex: Knex<any, unknown[]> = KnexManager.getConnection();
39
+ private tableName = "report_configurations";
40
+
41
+ /**
42
+ * Create a new report configuration
43
+ */
44
+ async create(item: IReportConfigurationInput): Promise<IReportConfiguration> {
45
+ // Validate configuration before creating
46
+ const validation = this.validateConfiguration(item.configuration);
47
+ if (!validation.valid) {
48
+ throw new Error(`Invalid configuration: ${validation.errors.join(", ")}`);
49
+ }
50
+
51
+ const [createdConfig] = await this._knex(this.tableName)
52
+ .insert({
53
+ name: item.name,
54
+ description: item.description,
55
+ configuration: JSON.stringify(item.configuration),
56
+ })
57
+ .returning("*");
58
+
59
+ return this._deserialize(createdConfig);
60
+ }
61
+
62
+ /**
63
+ * Get configuration by numeric ID
64
+ */
65
+ async getById(id: number): Promise<IReportConfiguration | null> {
66
+ const config = await this._knex(this.tableName).where({ id }).first();
67
+ return config ? this._deserialize(config) : null;
68
+ }
69
+
70
+ /**
71
+ * Get configuration by UUID
72
+ */
73
+ async getByUuid(uuid: string): Promise<IReportConfiguration | null> {
74
+ const config = await this._knex(this.tableName).where({ uuid }).first();
75
+ return config ? this._deserialize(config) : null;
76
+ }
77
+
78
+ /**
79
+ * Get configuration by name
80
+ */
81
+ async getByName(name: string): Promise<IReportConfiguration | null> {
82
+ const config = await this._knex(this.tableName)
83
+ .whereRaw("LOWER(name) = LOWER(?)", [name])
84
+ .first();
85
+ return config ? this._deserialize(config) : null;
86
+ }
87
+
88
+ /**
89
+ * Update a configuration
90
+ */
91
+ async update(
92
+ id: number,
93
+ item: Partial<IReportConfigurationInput>,
94
+ ): Promise<IReportConfiguration | null> {
95
+ // If configuration is being updated, validate it
96
+ if (item.configuration) {
97
+ const validation = this.validateConfiguration(item.configuration);
98
+ if (!validation.valid) {
99
+ throw new Error(
100
+ `Invalid configuration: ${validation.errors.join(", ")}`,
101
+ );
102
+ }
103
+ }
104
+
105
+ const updateData: any = {};
106
+ if (item.name !== undefined) updateData.name = item.name;
107
+ if (item.description !== undefined)
108
+ updateData.description = item.description;
109
+ if (item.configuration !== undefined)
110
+ updateData.configuration = JSON.stringify(item.configuration);
111
+
112
+ const [updatedConfig] = await this._knex(this.tableName)
113
+ .where({ id })
114
+ .update(updateData)
115
+ .returning("*");
116
+
117
+ return updatedConfig ? this._deserialize(updatedConfig) : null;
118
+ }
119
+
120
+ /**
121
+ * Delete a configuration
122
+ * Prevents deletion of the last configuration (business logic protection)
123
+ */
124
+ async delete(id: number): Promise<boolean> {
125
+ // Count total configurations
126
+ const [{ count }] = await this._knex(this.tableName).count("* as count");
127
+
128
+ if (parseInt(count as string) <= 1) {
129
+ throw new Error(
130
+ "Cannot delete the last configuration. At least one configuration must exist.",
131
+ );
132
+ }
133
+
134
+ const result = await this._knex(this.tableName).where({ id }).del();
135
+ return result > 0;
136
+ }
137
+
138
+ /**
139
+ * Get all configurations with pagination
140
+ */
141
+ async getAll(
142
+ page: number,
143
+ limit: number,
144
+ ): Promise<IDataPaginator<IReportConfiguration>> {
145
+ const offset = (page - 1) * limit;
146
+
147
+ const [countResult] = await this._knex(this.tableName).count("* as count");
148
+ const totalCount = +countResult.count;
149
+ const configs = await this._knex(this.tableName)
150
+ .limit(limit)
151
+ .offset(offset)
152
+ .orderBy("created_at", "desc");
153
+
154
+ return {
155
+ success: true,
156
+ data: configs.map((c) => this._deserialize(c)),
157
+ page,
158
+ limit,
159
+ count: configs.length,
160
+ totalCount,
161
+ totalPages: Math.ceil(totalCount / limit),
162
+ };
163
+ }
164
+
165
+ /**
166
+ * Validate a report configuration
167
+ *
168
+ * Rules:
169
+ * - Minimum 2, maximum 7 custom classes
170
+ * - Custom class names must be 1-30 characters
171
+ * - FHWA classes must be in range 1-13
172
+ * - Each FHWA class can only be mapped to one custom class (no duplicates)
173
+ * - Each custom class must have at least one FHWA class
174
+ */
175
+ validateConfiguration(config: IReportConfigurationData): IValidationResult {
176
+ const errors: string[] = [];
177
+
178
+ // Validate version exists
179
+ if (!config.version) {
180
+ errors.push("Configuration version is required");
181
+ }
182
+
183
+ // Validate custom classes array
184
+ if (!config.customClasses || !Array.isArray(config.customClasses)) {
185
+ errors.push("customClasses must be an array");
186
+ return { valid: false, errors };
187
+ }
188
+
189
+ // Min 2, max 7 custom classes
190
+ if (config.customClasses.length < 2) {
191
+ errors.push("Minimum 2 custom classes required");
192
+ }
193
+ if (config.customClasses.length > 7) {
194
+ errors.push("Maximum 7 custom classes allowed");
195
+ }
196
+
197
+ // Check name length (max 30 chars) and FHWA classes validity
198
+ const allFhwaClasses: number[] = [];
199
+ config.customClasses.forEach((cls, idx) => {
200
+ if (!cls.name || cls.name.length === 0) {
201
+ errors.push(`Custom class ${idx + 1}: name cannot be empty`);
202
+ }
203
+ if (cls.name && cls.name.length > 30) {
204
+ errors.push(`Custom class ${idx + 1}: name exceeds 30 characters`);
205
+ }
206
+
207
+ if (!Array.isArray(cls.fhwaClasses) || cls.fhwaClasses.length === 0) {
208
+ errors.push(
209
+ `Custom class ${idx + 1}: must have at least one FHWA class`,
210
+ );
211
+ } else {
212
+ cls.fhwaClasses.forEach((fhwa) => {
213
+ if (!Number.isInteger(fhwa) || fhwa < 1 || fhwa > 13) {
214
+ errors.push(
215
+ `Custom class ${idx + 1}: FHWA class ${fhwa} is invalid (must be 1-13)`,
216
+ );
217
+ }
218
+ allFhwaClasses.push(fhwa);
219
+ });
220
+ }
221
+ });
222
+
223
+ // Check for duplicate FHWA classes (many-to-one only)
224
+ const uniqueFhwaClasses = new Set(allFhwaClasses);
225
+ if (uniqueFhwaClasses.size !== allFhwaClasses.length) {
226
+ const duplicates = allFhwaClasses.filter(
227
+ (item, index) => allFhwaClasses.indexOf(item) !== index,
228
+ );
229
+ errors.push(
230
+ `Duplicate FHWA classes detected: ${[...new Set(duplicates)].join(", ")}. Each FHWA class can only be mapped to one custom class.`,
231
+ );
232
+ }
233
+
234
+ return { valid: errors.length === 0, errors };
235
+ }
236
+
237
+ /**
238
+ * Apply configuration transformation to detection results
239
+ *
240
+ * Two-step transformation:
241
+ * 1. Detection labels → FHWA classes (using DETECTION_LABEL_TO_FHWA mapping)
242
+ * 2. FHWA classes → Custom classes (using configuration)
243
+ *
244
+ * @param detectionResults - Raw detection results with labels as keys and counts as values
245
+ * Example: { 'car': 150, 'articulated_truck': 23, 'motorcycle': 5 }
246
+ * @param config - The report configuration to apply
247
+ * @returns Transformed results with custom class names as keys and counts as values
248
+ * Example: { 'Cars': 155, 'Heavy Trucks': 23 }
249
+ */
250
+ applyConfiguration(
251
+ detectionResults: Record<string, number>,
252
+ config: IReportConfiguration,
253
+ ): Record<string, number> {
254
+ // Step 1: Detection labels → FHWA classes
255
+ const fhwaClassCounts: Record<number, number> = {};
256
+
257
+ for (const [label, count] of Object.entries(detectionResults)) {
258
+ const fhwaClasses = DETECTION_LABEL_TO_FHWA[label];
259
+ if (fhwaClasses && fhwaClasses.length > 0) {
260
+ fhwaClasses.forEach((fhwaClass) => {
261
+ fhwaClassCounts[fhwaClass] =
262
+ (fhwaClassCounts[fhwaClass] || 0) + count;
263
+ });
264
+ }
265
+ // Labels not in DETECTION_LABEL_TO_FHWA are silently ignored (e.g., pedestrian, bicycle)
266
+ }
267
+
268
+ // Step 2: FHWA classes → Custom classes
269
+ const customClassCounts: Record<string, number> = {};
270
+
271
+ config.configuration.customClasses.forEach((customClass) => {
272
+ let total = 0;
273
+ customClass.fhwaClasses.forEach((fhwaClass) => {
274
+ total += fhwaClassCounts[fhwaClass] || 0;
275
+ });
276
+ customClassCounts[customClass.name] = total;
277
+ });
278
+
279
+ return customClassCounts;
280
+ }
281
+
282
+ /**
283
+ * Transform nested vehicle structure with custom class mapping
284
+ *
285
+ * Handles both ATR (lane-based) and TMC (direction/turn-based) formats
286
+ * Preserves all nesting levels while transforming detection labels to custom classes
287
+ *
288
+ * @param vehiclesStructure - Nested vehicles object with detection labels as keys
289
+ * ATR: { "car": { "0": 45, "1": 50 }, ... }
290
+ * TMC: { "car": { "NORTH": { "straight": 10 }, ... }, ... }
291
+ * @param config - Report configuration with custom class mappings
292
+ * @returns Transformed structure with custom class names as keys
293
+ */
294
+ applyConfigurationToNestedStructure(
295
+ vehiclesStructure: Record<string, any>,
296
+ config: IReportConfiguration,
297
+ ): Record<string, any> {
298
+ // Build reverse mapping: detection label → custom class name
299
+ const detectionToCustomClass: Record<string, string> = {};
300
+
301
+ for (const customClass of config.configuration.customClasses) {
302
+ // For each FHWA class in this custom class
303
+ for (const fhwaClass of customClass.fhwaClasses) {
304
+ // Find all detection labels that map to this FHWA class
305
+ for (const [label, fhwaClasses] of Object.entries(
306
+ DETECTION_LABEL_TO_FHWA,
307
+ )) {
308
+ if (fhwaClasses.includes(fhwaClass)) {
309
+ detectionToCustomClass[label] = customClass.name;
310
+ }
311
+ }
312
+ }
313
+ }
314
+
315
+ // Initialize empty structure for each custom class
316
+ const result: Record<string, any> = {};
317
+ for (const customClass of config.configuration.customClasses) {
318
+ result[customClass.name] = {};
319
+ }
320
+
321
+ // Iterate through detection labels in input structure
322
+ for (const [detectionLabel, nestedData] of Object.entries(
323
+ vehiclesStructure,
324
+ )) {
325
+ const customClassName = detectionToCustomClass[detectionLabel];
326
+
327
+ // Skip labels not mapped to any custom class (e.g., pedestrian, bicycle)
328
+ if (!customClassName) {
329
+ continue;
330
+ }
331
+
332
+ // Deep merge nested data into custom class accumulator
333
+ result[customClassName] = this._deepMergeNumericData(
334
+ result[customClassName],
335
+ nestedData,
336
+ );
337
+ }
338
+
339
+ return result;
340
+ }
341
+
342
+ /**
343
+ * Deep merge numeric data at arbitrary nesting levels
344
+ *
345
+ * Recursively merges two nested structures, summing numeric leaf values
346
+ * Handles ATR format (2 levels: vehicle → lane → count)
347
+ * Handles TMC format (3 levels: vehicle → direction → turn → count)
348
+ *
349
+ * @param target - Target accumulator object
350
+ * @param source - Source data to merge into target
351
+ * @returns Merged object with summed numeric values
352
+ */
353
+ private _deepMergeNumericData(target: any, source: any): any {
354
+ // Base case: if source is a number, add it to target
355
+ if (typeof source === "number") {
356
+ return (typeof target === "number" ? target : 0) + source;
357
+ }
358
+
359
+ // If source is not an object, return target unchanged
360
+ if (typeof source !== "object" || source === null) {
361
+ return target;
362
+ }
363
+
364
+ // Ensure target is an object
365
+ if (typeof target !== "object" || target === null) {
366
+ target = {};
367
+ }
368
+
369
+ // Recursively merge each key in source
370
+ for (const [key, value] of Object.entries(source)) {
371
+ target[key] = this._deepMergeNumericData(target[key], value);
372
+ }
373
+
374
+ return target;
375
+ }
376
+
377
+ /**
378
+ * Get the FHWA mapping constant (for reference/debugging)
379
+ */
380
+ getDetectionLabelToFhwaMapping(): Record<string, number[]> {
381
+ return { ...DETECTION_LABEL_TO_FHWA };
382
+ }
383
+
384
+ /**
385
+ * Deserialize database row to IReportConfiguration interface
386
+ * Converts snake_case to camelCase and parses JSONB
387
+ */
388
+ private _deserialize(row: any): IReportConfiguration {
389
+ return {
390
+ id: row.id,
391
+ uuid: row.uuid,
392
+ name: row.name,
393
+ description: row.description,
394
+ configuration:
395
+ typeof row.configuration === "string"
396
+ ? JSON.parse(row.configuration)
397
+ : row.configuration,
398
+ created_at: row.created_at,
399
+ updated_at: row.updated_at,
400
+ };
401
+ }
402
+ }
@@ -222,13 +222,13 @@ export class VideoDAO implements IBaseDAO<IVideo> {
222
222
  // Check if metadata has at least one key with pt1 and pt2 properties
223
223
  query = query.whereRaw(`
224
224
  EXISTS (
225
- SELECT 1
225
+ SELECT 1
226
226
  FROM jsonb_each(metadata) as entry(key, value)
227
- WHERE key != 'lanes'
227
+ WHERE key != 'lanes'
228
228
  AND key != 'finish_line'
229
229
  AND jsonb_typeof(value) = 'object'
230
- AND value ? 'pt1'
231
- AND value ? 'pt2'
230
+ AND jsonb_exists(value, 'pt1')
231
+ AND jsonb_exists(value, 'pt2')
232
232
  AND jsonb_typeof(value->'pt1') = 'array'
233
233
  AND jsonb_typeof(value->'pt2') = 'array'
234
234
  AND jsonb_array_length(value->'pt1') = 2
@@ -245,51 +245,4 @@ export class VideoDAO implements IBaseDAO<IVideo> {
245
245
  throw error;
246
246
  }
247
247
  }
248
-
249
- async getVideoIdsByFolderId(folderId: number): Promise<number[]> {
250
- const videos = await this._knex("video")
251
- .where("folderId", folderId)
252
- .select("id");
253
- return videos.map((video) => video.id);
254
- }
255
-
256
- async bulkUpdateCamera(
257
- videoIds: number[],
258
- cameraId: number | null,
259
- trx?: Knex.Transaction,
260
- ): Promise<number> {
261
- const knexInstance = trx || this._knex;
262
- const result = await knexInstance("video").whereIn("id", videoIds).update({
263
- cameraId: cameraId,
264
- updated_at: knexInstance.fn.now(),
265
- });
266
- return result;
267
- }
268
-
269
- async getVideosByCameraIdWithFolder(
270
- cameraId: number,
271
- page: number,
272
- limit: number,
273
- ): Promise<IDataPaginator<IVideo>> {
274
- const offset = (page - 1) * limit;
275
-
276
- const query = this._knex("video as v")
277
- .innerJoin("folders as f", "v.folderId", "f.id")
278
- .where("v.cameraId", cameraId)
279
- .select("v.*", this._knex.raw("to_jsonb(f.*) as folder"));
280
-
281
- const [countResult] = await query.clone().clearSelect().count("* as count");
282
- const totalCount = +countResult.count;
283
- const videos = await query.clone().limit(limit).offset(offset);
284
-
285
- return {
286
- success: true,
287
- data: videos,
288
- page,
289
- limit,
290
- count: videos.length,
291
- totalCount,
292
- totalPages: Math.ceil(totalCount / limit),
293
- };
294
- }
295
248
  }
package/src/index.ts CHANGED
@@ -4,6 +4,7 @@ export { CameraDAO } from "./dao/camera/camera.dao";
4
4
  export { ChatDAO } from "./dao/chat/chat.dao";
5
5
  export { FolderDAO } from "./dao/folder/folder.dao";
6
6
  export { MessageDAO } from "./dao/message/message.dao";
7
+ export { ReportConfigurationDAO } from "./dao/report-configuration/report-configuration.dao";
7
8
  export { StudyDAO } from "./dao/study/study.dao";
8
9
  export { UserDAO } from "./dao/user/user.dao";
9
10
  export { UserPushNotificationTokenDAO } from "./dao/user-push-notification-token/user-push-notification-token.dao";
@@ -25,6 +26,13 @@ export {
25
26
  IMessageCreate,
26
27
  IMessageUpdate,
27
28
  } from "./interfaces/message/message.interfaces";
29
+ export {
30
+ IReportConfiguration,
31
+ IReportConfigurationData,
32
+ IReportConfigurationInput,
33
+ ICustomClass,
34
+ IValidationResult,
35
+ } from "./interfaces/report-configuration/report-configuration.interfaces";
28
36
  export { IStudy } from "./interfaces/study/study.interfaces";
29
37
  export { IUser } from "./interfaces/user/user.interfaces";
30
38
  export { IUserPushNotificationToken } from "./interfaces/user-push-notification-token/user-push-notification-token.interfaces";
@@ -0,0 +1,30 @@
1
+ export interface IReportConfiguration {
2
+ id: number;
3
+ uuid: string;
4
+ name: string;
5
+ description?: string;
6
+ configuration: IReportConfigurationData;
7
+ created_at: Date;
8
+ updated_at: Date;
9
+ }
10
+
11
+ export interface IReportConfigurationData {
12
+ version: string;
13
+ customClasses: ICustomClass[];
14
+ }
15
+
16
+ export interface ICustomClass {
17
+ name: string; // Max 30 chars
18
+ fhwaClasses: number[]; // Array of 1-13
19
+ }
20
+
21
+ export interface IReportConfigurationInput {
22
+ name: string;
23
+ description?: string;
24
+ configuration: IReportConfigurationData;
25
+ }
26
+
27
+ export interface IValidationResult {
28
+ valid: boolean;
29
+ errors: string[];
30
+ }