@trafficgroup/knex-rel 0.0.29 → 0.1.0

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/plan.md DELETED
@@ -1,304 +0,0 @@
1
- # Database Implementation Plan: Video Minute Results Storage
2
-
3
- ## Schema Changes
4
-
5
- ### New Table: video_minute_results
6
-
7
- ```sql
8
- CREATE TABLE video_minute_results (
9
- id BIGSERIAL PRIMARY KEY,
10
- uuid UUID NOT NULL DEFAULT gen_random_uuid(),
11
- video_id INTEGER NOT NULL REFERENCES video(id) ON DELETE CASCADE,
12
- minute_number INTEGER NOT NULL,
13
- results JSONB NOT NULL,
14
- created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
15
- updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
16
- CONSTRAINT unique_video_minute UNIQUE (video_id, minute_number),
17
- CONSTRAINT valid_minute_number CHECK (minute_number >= 0)
18
- );
19
- ```
20
-
21
- **Indexes:**
22
-
23
- - `idx_video_minute_results_video_id` on `video_id` (frequent JOINs with video table)
24
- - `idx_video_minute_results_uuid` on `uuid` (external API lookups)
25
- - `idx_video_minute_results_minute_number` on `minute_number` (range queries)
26
- - `idx_video_minute_results_video_minute` on `(video_id, minute_number)` (composite lookups)
27
-
28
- ### Modifications to Existing Tables
29
-
30
- ```sql
31
- -- Add duration tracking to video table
32
- ALTER TABLE video ADD COLUMN duration_seconds INTEGER;
33
- ```
34
-
35
- ## Migrations
36
-
37
- ### 20250823HHMMSS_create_video_minute_results.ts
38
-
39
- ```typescript
40
- export async function up(knex: Knex): Promise<void> {
41
- // Create video_minute_results table
42
- await knex.schema.createTable("video_minute_results", (table) => {
43
- table.bigIncrements("id").primary();
44
- table
45
- .uuid("uuid")
46
- .defaultTo(knex.raw("gen_random_uuid()"))
47
- .notNullable()
48
- .unique();
49
- table
50
- .integer("video_id")
51
- .unsigned()
52
- .notNullable()
53
- .references("id")
54
- .inTable("video")
55
- .onDelete("CASCADE");
56
- table.integer("minute_number").notNullable();
57
- table.jsonb("results").notNullable();
58
- table.timestamps(true, true);
59
-
60
- // Constraints
61
- table.unique(["video_id", "minute_number"], {
62
- indexName: "unique_video_minute",
63
- });
64
- table.check("minute_number >= 0", [], "valid_minute_number");
65
-
66
- // Indexes
67
- table.index("video_id", "idx_video_minute_results_video_id");
68
- table.index("uuid", "idx_video_minute_results_uuid");
69
- table.index("minute_number", "idx_video_minute_results_minute_number");
70
- table.index(
71
- ["video_id", "minute_number"],
72
- "idx_video_minute_results_video_minute",
73
- );
74
- });
75
-
76
- // Add duration_seconds to video table
77
- await knex.schema.alterTable("video", (table) => {
78
- table.integer("duration_seconds").nullable();
79
- });
80
- }
81
-
82
- export async function down(knex: Knex): Promise<void> {
83
- await knex.schema.dropTableIfExists("video_minute_results");
84
- await knex.schema.alterTable("video", (table) => {
85
- table.dropColumn("duration_seconds");
86
- });
87
- }
88
- ```
89
-
90
- **Safety Level:** Medium - Creates new table and adds nullable column to existing table
91
-
92
- ## DAOs
93
-
94
- ### Update: VideoMinuteResultDAO
95
-
96
- **File:** `src/dao/VideoMinuteResultDAO.ts` (replace existing mock implementation)
97
-
98
- **Methods:**
99
-
100
- ```typescript
101
- export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
102
- private knex = KnexManager.getConnection();
103
- private tableName = "video_minute_results";
104
-
105
- // Standard CRUD operations
106
- async create(data: IVideoMinuteResultInput): Promise<IVideoMinuteResult>;
107
- async getById(id: number): Promise<IVideoMinuteResult | null>;
108
- async getByUuid(uuid: string): Promise<IVideoMinuteResult | null>;
109
- async getAll(
110
- page: number,
111
- limit: number,
112
- ): Promise<IDataPaginator<IVideoMinuteResult>>;
113
- async update(
114
- id: number,
115
- data: Partial<IVideoMinuteResult>,
116
- ): Promise<IVideoMinuteResult | null>;
117
- async delete(id: number): Promise<boolean>;
118
-
119
- // Specialized batch operations (eliminate N+1 queries)
120
- async createBatch(
121
- videoId: number,
122
- minuteResults: IVideoMinuteResultInput[],
123
- ): Promise<IVideoMinuteResult[]>;
124
- async getMinuteResultsForVideo(
125
- videoId: number,
126
- startMinute?: number,
127
- endMinute?: number,
128
- page?: number,
129
- limit?: number,
130
- ): Promise<IDataPaginator<IVideoMinuteResult>>;
131
- async getMinuteResultsByVideoUuid(
132
- videoUuid: string,
133
- startMinute?: number,
134
- endMinute?: number,
135
- page?: number,
136
- limit?: number,
137
- ): Promise<IDataPaginator<IVideoMinuteResult>>;
138
- async deleteByVideoId(videoId: number): Promise<boolean>;
139
- async getVideoMinuteRange(
140
- videoId: number,
141
- ): Promise<{ minMinute: number; maxMinute: number } | null>;
142
- }
143
- ```
144
-
145
- **Performance Optimizations:**
146
-
147
- - Batch inserts using `knex.batchInsert()` for 5-minute batches
148
- - Single query with JOINs for video+minute data retrieval
149
- - Range queries with BETWEEN for minute filtering
150
- - Pagination with proper OFFSET/LIMIT
151
-
152
- ### Update: VideoDAO
153
-
154
- **File:** `src/dao/video/video.dao.ts` (add duration_seconds support)
155
-
156
- **New Methods:**
157
-
158
- ```typescript
159
- async updateDuration(id: number, durationSeconds: number): Promise<IVideo | null>
160
- async getVideosWithoutMinuteData(page: number, limit: number): Promise<IDataPaginator<IVideo>>
161
- ```
162
-
163
- ## Interfaces
164
-
165
- ### Update: IVideoMinuteResult.ts
166
-
167
- ```typescript
168
- export interface IVideoMinuteResult extends IBaseEntity {
169
- id: number;
170
- uuid: string;
171
- videoId: number;
172
- minuteNumber: number; // Renamed from 'minute' for clarity
173
- results: Record<string, any>;
174
- createdAt: string;
175
- updatedAt: string;
176
- }
177
-
178
- export interface IVideoMinuteResultInput {
179
- videoId: number;
180
- minuteNumber: number;
181
- results: Record<string, any>;
182
- }
183
-
184
- export interface IVideoMinuteBatch {
185
- videoId: number;
186
- startMinute: number;
187
- endMinute: number;
188
- minuteResults: IVideoMinuteResultInput[];
189
- }
190
- ```
191
-
192
- ### Update: IVideo.ts
193
-
194
- ```typescript
195
- export interface IVideo {
196
- // ... existing fields
197
- durationSeconds?: number; // Add optional duration field
198
- }
199
- ```
200
-
201
- ### Export Updates
202
-
203
- Update `src/index.ts` to include new interfaces and updated DAO.
204
-
205
- ## Implementation Order
206
-
207
- 1. **Interfaces** → Update IVideoMinuteResult and IVideo interfaces
208
- 2. **Migration** → Create 20250823HHMMSS_create_video_minute_results.ts
209
- 3. **DAO Implementation** → Replace VideoMinuteResultDAO mock methods with real database operations
210
- 4. **DAO Enhancement** → Add duration support to VideoDAO
211
- 5. **Exports** → Update index.ts exports
212
- 6. **Build & Test** → Compile TypeScript and run tests
213
-
214
- ## Query Performance Strategy
215
-
216
- ### Batch Insert Optimization
217
-
218
- ```typescript
219
- async createBatch(videoId: number, minuteResults: IVideoMinuteResultInput[]): Promise<IVideoMinuteResult[]> {
220
- const batchData = minuteResults.map(result => ({
221
- ...result,
222
- videoId,
223
- uuid: knex.raw('gen_random_uuid()'),
224
- created_at: knex.fn.now(),
225
- updated_at: knex.fn.now()
226
- }));
227
-
228
- return await this.knex.batchInsert(this.tableName, batchData, 50).returning('*');
229
- }
230
- ```
231
-
232
- ### Optimized Minute Range Query
233
-
234
- ```typescript
235
- async getMinuteResultsForVideo(
236
- videoId: number,
237
- startMinute: number = 0,
238
- endMinute?: number,
239
- page: number = 1,
240
- limit: number = 100
241
- ): Promise<IDataPaginator<IVideoMinuteResult>> {
242
- const query = this.knex(this.tableName + ' as vmr')
243
- .innerJoin('video as v', 'vmr.video_id', 'v.id')
244
- .select('vmr.*', 'v.name as video_name', 'v.uuid as video_uuid')
245
- .where('vmr.video_id', videoId)
246
- .where('vmr.minute_number', '>=', startMinute);
247
-
248
- if (endMinute !== undefined) {
249
- query.where('vmr.minute_number', '<=', endMinute);
250
- }
251
-
252
- // Standard pagination logic with count optimization
253
- const [{ count }] = await query.clone().clearSelect().count('* as count');
254
- const offset = (page - 1) * limit;
255
- const data = await query.orderBy('vmr.minute_number', 'asc').limit(limit).offset(offset);
256
-
257
- return {
258
- success: true,
259
- data,
260
- page,
261
- limit,
262
- count: data.length,
263
- totalCount: parseInt(count as string),
264
- totalPages: Math.ceil(parseInt(count as string) / limit)
265
- };
266
- }
267
- ```
268
-
269
- ## Risks & Validation
270
-
271
- ### Migration Safety
272
-
273
- - **Risk:** Table creation failure in production
274
- - **Mitigation:** Test migration on staging with production data volume
275
- - **Rollback:** `down()` function drops table and column safely
276
-
277
- ### Pattern Compliance
278
-
279
- - **UUID-only External Communication:** ✅ All external API methods use UUID lookups
280
- - **Foreign Key Integrity:** ✅ CASCADE delete ensures orphaned records cleanup
281
- - **Naming Conventions:** ✅ Follows snake_case for DB, camelCase for TypeScript
282
- - **DAO Methods:** ✅ Standard CRUD + specialized batch operations
283
- - **Performance:** ✅ Proper indexing for all query patterns
284
-
285
- ### Data Integrity Validation
286
-
287
- ```typescript
288
- async validateMinuteDataIntegrity(videoId: number): Promise<boolean> {
289
- const video = await this.knex('video').where('id', videoId).first();
290
- const minuteResults = await this.knex(this.tableName).where('video_id', videoId);
291
-
292
- // Aggregate minute results and compare with video.results
293
- return this.compareAggregatedResults(video.results, minuteResults);
294
- }
295
- ```
296
-
297
- ### Performance Validation
298
-
299
- - Batch inserts handle 5-minute chunks efficiently (50-item batches)
300
- - Indexes support all query patterns: video_id lookups, UUID lookups, minute ranges
301
- - JOIN operations optimized for N+1 query elimination
302
- - Pagination implemented with proper count optimization
303
-
304
- This plan ensures 100% pattern compliance while optimizing for the specific use case of minute-by-minute video detection storage with efficient batch processing capabilities.