@trafficgroup/knex-rel 0.0.29 → 0.1.1

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 CHANGED
@@ -1,304 +1,129 @@
1
- # Database Implementation Plan: Video Minute Results Storage
1
+ # Lane/Annotation Reuse Implementation Plan
2
2
 
3
3
  ## Schema Changes
4
4
 
5
- ### New Table: video_minute_results
5
+ ### Tables
6
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
- ```
7
+ - **video**: Add `annotation_source_id` column (nullable INTEGER, FK to video.id)
20
8
 
21
- **Indexes:**
9
+ ### Modifications
22
10
 
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
- ```
11
+ - **video table**:
12
+ - Add `annotation_source_id INTEGER NULL REFERENCES video(id) ON DELETE SET NULL`
13
+ - Add index on `annotation_source_id` for performance
14
+ - Add composite index on `(folderId, videoType)` for template lookup optimization
34
15
 
35
16
  ## Migrations
36
17
 
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
- ```
18
+ ### 20250917143052_add_annotation_source_to_video.ts
89
19
 
90
- **Safety Level:** Medium - Creates new table and adds nullable column to existing table
20
+ - **Up operations**:
21
+ - Add `annotation_source_id` column to video table
22
+ - Add foreign key constraint to video(id) with SET NULL on delete
23
+ - Add index on `annotation_source_id`
24
+ - Add composite index on `(folderId, videoType)` for efficient template queries
25
+ - **Down operations**:
26
+ - Drop indexes
27
+ - Drop foreign key constraint
28
+ - Drop column
29
+ - **Safety level**: LOW RISK - Adding nullable column with proper constraints
91
30
 
92
31
  ## DAOs
93
32
 
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
- }
33
+ ### Modify: VideoDAO
34
+
35
+ - **File**: src/dao/video/video.dao.ts
36
+ - **New methods**:
37
+ - `getTemplateVideos(folderId: number, videoType: string): Promise<IVideo[]>` - Get videos from same folder/type that can serve as templates (have metadata and completed status)
38
+ - `getVideosUsingTemplate(templateVideoId: number): Promise<IVideo[]>` - Get videos that used a specific video as template
39
+ - `setAnnotationSource(videoId: number, sourceVideoId: number | null): Promise<IVideo | null>` - Set/update annotation source
40
+ - **Performance optimizations**:
41
+ - Use JOINs to fetch related annotation source data in main queries
42
+ - Eliminate N+1 queries when fetching videos with their annotation sources
43
+ - Optimized template lookup using composite index
44
+
45
+ ### Query Patterns
46
+
47
+ ```sql
48
+ -- Get template videos (same folder + type, completed, with metadata)
49
+ SELECT v.* FROM video v
50
+ WHERE v.folderId = ?
51
+ AND v.videoType = ?
52
+ AND v.status = 'COMPLETED'
53
+ AND v.metadata IS NOT NULL
54
+ AND v.metadata != '{}'
55
+ ORDER BY v.created_at DESC;
56
+
57
+ -- Get videos using specific template
58
+ SELECT v.* FROM video v
59
+ WHERE v.annotation_source_id = ?;
143
60
  ```
144
61
 
145
- **Performance Optimizations:**
62
+ ## Interfaces
146
63
 
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
64
+ ### Modify: IVideo.ts
151
65
 
152
- ### Update: VideoDAO
66
+ - **File**: src/interfaces/video/video.interfaces.ts
67
+ - **New properties**:
68
+ - `annotationSourceId?: number;` - ID of video whose annotations were copied
69
+ - `annotationSource?: IVideo;` - Optional populated annotation source video object
70
+ - **Export**: Update index.ts exports
153
71
 
154
- **File:** `src/dao/video/video.dao.ts` (add duration_seconds support)
72
+ ## Implementation Order
155
73
 
156
- **New Methods:**
74
+ 1. **Interfaces** → Update IVideo interface with new fields
75
+ 2. **Migrations** → Create and run migration to add database column
76
+ 3. **DAOs** → Add new methods to VideoDAO for template management
77
+ 4. **Exports** → Update index.ts to export new interface changes
78
+ 5. **Build** → Compile and test changes
157
79
 
158
- ```typescript
159
- async updateDuration(id: number, durationSeconds: number): Promise<IVideo | null>
160
- async getVideosWithoutMinuteData(page: number, limit: number): Promise<IDataPaginator<IVideo>>
161
- ```
80
+ ## Business Logic Constraints
162
81
 
163
- ## Interfaces
82
+ ### Template Selection Rules
164
83
 
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
- ```
84
+ - Only videos from **same folder** (`folderId` match)
85
+ - Only videos of **same type** (`videoType` match: TMC → TMC, ATR → ATR)
86
+ - Only **COMPLETED** videos with non-empty metadata can be templates
87
+ - Template videos must have lane configuration in metadata field
191
88
 
192
- ### Update: IVideo.ts
89
+ ### Data Integrity
193
90
 
194
- ```typescript
195
- export interface IVideo {
196
- // ... existing fields
197
- durationSeconds?: number; // Add optional duration field
198
- }
199
- ```
91
+ - `annotation_source_id` references `video.id` with CASCADE DELETE behavior set to SET NULL
92
+ - If template video is deleted, dependent videos keep their annotations but lose the source reference
93
+ - Self-referencing constraint: video cannot reference itself as annotation source
200
94
 
201
- ### Export Updates
95
+ ## Performance Considerations
202
96
 
203
- Update `src/index.ts` to include new interfaces and updated DAO.
97
+ ### Indexes
204
98
 
205
- ## Implementation Order
99
+ - `annotation_source_id` - for JOIN operations and template lookups
100
+ - `(folderId, videoType)` - composite index for efficient template discovery
101
+ - Existing `uuid` index maintained for external API calls
206
102
 
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
- ```
103
+ ### Query Optimization
231
104
 
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
- ```
105
+ - Template lookup query uses composite index for O(log n) performance
106
+ - JOIN operations for fetching annotation source data in single query
107
+ - Pagination maintained for all list operations
268
108
 
269
109
  ## Risks & Validation
270
110
 
271
111
  ### Migration Safety
272
112
 
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
113
+ - **LOW RISK**: Adding nullable column with proper constraints
114
+ - **Rollback plan**: Down migration removes column and constraints cleanly
115
+ - **Data preservation**: No existing data affected
276
116
 
277
117
  ### Pattern Compliance
278
118
 
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
119
+ - **100% compliant** with existing DAO patterns (IBaseDAO implementation)
120
+ - **100% compliant** with naming conventions (snake_case DB, camelCase TS)
121
+ - **100% compliant** with interface patterns (I prefix, proper exports)
122
+ - **100% compliant** with migration patterns (timestamp prefix, up/down functions)
298
123
 
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
124
+ ### Testing Strategy
303
125
 
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.
126
+ - Verify foreign key constraints work correctly
127
+ - Test template discovery with same folder/type filtering
128
+ - Validate NULL handling for videos without annotation sources
129
+ - Performance test template lookup queries with large datasets