@trafficgroup/knex-rel 0.1.4 → 0.1.6

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.
@@ -1,6 +1,7 @@
1
1
  import { Knex } from "knex";
2
2
  import { IBaseDAO, IDataPaginator } from "../../d.types";
3
3
  import { ICamera } from "../../interfaces/camera/camera.interfaces";
4
+ import { IVideo } from "../../interfaces/video/video.interfaces";
4
5
  import KnexManager from "../../KnexConnection";
5
6
 
6
7
  export class CameraDAO implements IBaseDAO<ICamera> {
@@ -76,4 +77,78 @@ export class CameraDAO implements IBaseDAO<ICamera> {
76
77
  );
77
78
  return cameras;
78
79
  }
80
+
81
+ /**
82
+ * Get all cameras with optional search filter by name (case-insensitive partial match)
83
+ */
84
+ async getAllWithSearch(
85
+ page: number,
86
+ limit: number,
87
+ name?: string,
88
+ ): Promise<IDataPaginator<ICamera>> {
89
+ const offset = (page - 1) * limit;
90
+
91
+ let query = this._knex("cameras");
92
+
93
+ // Apply search filter if name provided (escape special chars to prevent pattern injection)
94
+ if (name && name.trim().length > 0) {
95
+ const escapedName = name.trim().replace(/[%_\\]/g, "\\$&");
96
+ query = query.where("name", "ilike", `%${escapedName}%`);
97
+ }
98
+
99
+ const [countResult] = await query.clone().count("* as count");
100
+ const totalCount = +countResult.count;
101
+ const cameras = await query
102
+ .clone()
103
+ .limit(limit)
104
+ .offset(offset)
105
+ .orderBy("name", "asc");
106
+
107
+ return {
108
+ success: true,
109
+ data: cameras,
110
+ page,
111
+ limit,
112
+ count: cameras.length,
113
+ totalCount,
114
+ totalPages: Math.ceil(totalCount / limit),
115
+ };
116
+ }
117
+
118
+ /**
119
+ * Get paginated videos for a specific camera with folder data included
120
+ */
121
+ async getVideosByCamera(
122
+ cameraId: number,
123
+ page: number,
124
+ limit: number,
125
+ ): Promise<IDataPaginator<IVideo>> {
126
+ const offset = (page - 1) * limit;
127
+
128
+ const query = this._knex("video as v")
129
+ .innerJoin("folders as f", "v.folderId", "f.id")
130
+ .select("v.*", this._knex.raw("to_jsonb(f.*) as folder"))
131
+ .where("v.cameraId", cameraId);
132
+
133
+ // Optimized count query without JOIN
134
+ const [countResult] = await this._knex("video as v")
135
+ .where("v.cameraId", cameraId)
136
+ .count("* as count");
137
+ const totalCount = +countResult.count;
138
+ const videos = await query
139
+ .clone()
140
+ .limit(limit)
141
+ .offset(offset)
142
+ .orderBy("v.created_at", "desc");
143
+
144
+ return {
145
+ success: true,
146
+ data: videos,
147
+ page,
148
+ limit,
149
+ count: videos.length,
150
+ totalCount,
151
+ totalPages: Math.ceil(totalCount / limit),
152
+ };
153
+ }
79
154
  }
@@ -200,6 +200,11 @@ export class ReportConfigurationDAO implements IBaseDAO<IReportConfiguration> {
200
200
  if (!cls.name || cls.name.length === 0) {
201
201
  errors.push(`Custom class ${idx + 1}: name cannot be empty`);
202
202
  }
203
+ if (cls.name && cls.name.toLowerCase() === "total") {
204
+ errors.push(
205
+ `Custom class ${idx + 1}: "Total" is a reserved name and cannot be used`,
206
+ );
207
+ }
203
208
  if (cls.name && cls.name.length > 30) {
204
209
  errors.push(`Custom class ${idx + 1}: name exceeds 30 characters`);
205
210
  }
@@ -336,6 +341,9 @@ export class ReportConfigurationDAO implements IBaseDAO<IReportConfiguration> {
336
341
  );
337
342
  }
338
343
 
344
+ // Add "Total" class that aggregates all custom classes
345
+ result["Total"] = this._createTotalClass(result);
346
+
339
347
  return result;
340
348
  }
341
349
 
@@ -374,6 +382,23 @@ export class ReportConfigurationDAO implements IBaseDAO<IReportConfiguration> {
374
382
  return target;
375
383
  }
376
384
 
385
+ /**
386
+ * Create "Total" vehicle class by summing all custom classes
387
+ * Works for both ATR and TMC formats through structure-agnostic deep merge
388
+ *
389
+ * @param customClassesData - Object with custom class names as keys
390
+ * @returns Aggregated nested structure summing all classes
391
+ */
392
+ private _createTotalClass(customClassesData: Record<string, any>): any {
393
+ let total: any = {};
394
+
395
+ for (const [className, nestedData] of Object.entries(customClassesData)) {
396
+ total = this._deepMergeNumericData(total, nestedData);
397
+ }
398
+
399
+ return total;
400
+ }
401
+
377
402
  /**
378
403
  * Get the FHWA mapping constant (for reference/debugging)
379
404
  */
@@ -245,4 +245,79 @@ export class VideoDAO implements IBaseDAO<IVideo> {
245
245
  throw error;
246
246
  }
247
247
  }
248
+
249
+ /**
250
+ * Get all video IDs for a specific folder (used for cascade operations)
251
+ */
252
+ async getVideoIdsByFolderId(folderId: number): Promise<number[]> {
253
+ const rows = await this._knex("video")
254
+ .where({ folderId })
255
+ .select("id")
256
+ .orderBy("id", "asc");
257
+
258
+ return rows.map((row) => row.id);
259
+ }
260
+
261
+ /**
262
+ * Bulk update camera assignment for multiple videos
263
+ * Supports optional transaction for service-level transaction management
264
+ */
265
+ async bulkUpdateCamera(
266
+ videoIds: number[],
267
+ cameraId: number | null,
268
+ trx?: Knex.Transaction,
269
+ ): Promise<number> {
270
+ if (!videoIds || videoIds.length === 0) {
271
+ return 0;
272
+ }
273
+
274
+ const query = trx || this._knex;
275
+
276
+ const result = await query("video")
277
+ .whereIn("id", videoIds)
278
+ .update({
279
+ cameraId: cameraId,
280
+ updated_at: query.fn.now(),
281
+ })
282
+ .returning("id");
283
+
284
+ return result.length;
285
+ }
286
+
287
+ /**
288
+ * Get videos by camera ID with folder information (paginated)
289
+ */
290
+ async getVideosByCameraIdWithFolder(
291
+ cameraId: number,
292
+ page: number,
293
+ limit: number,
294
+ ): Promise<IDataPaginator<IVideo>> {
295
+ const offset = (page - 1) * limit;
296
+
297
+ const query = this._knex("video as v")
298
+ .innerJoin("folders as f", "v.folderId", "f.id")
299
+ .select("v.*", this._knex.raw("to_jsonb(f.*) as folder"))
300
+ .where("v.cameraId", cameraId);
301
+
302
+ // Optimized count query without JOIN
303
+ const [countResult] = await this._knex("video as v")
304
+ .where("v.cameraId", cameraId)
305
+ .count("* as count");
306
+ const totalCount = +countResult.count;
307
+ const videos = await query
308
+ .clone()
309
+ .limit(limit)
310
+ .offset(offset)
311
+ .orderBy("v.created_at", "desc");
312
+
313
+ return {
314
+ success: true,
315
+ data: videos,
316
+ page,
317
+ limit,
318
+ count: videos.length,
319
+ totalCount,
320
+ totalPages: Math.ceil(totalCount / limit),
321
+ };
322
+ }
248
323
  }