@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/dist/dao/VideoMinuteResultDAO.d.ts +101 -0
- package/dist/dao/VideoMinuteResultDAO.js +366 -0
- package/dist/dao/VideoMinuteResultDAO.js.map +1 -1
- package/dist/dao/camera/camera.dao.d.ts +13 -0
- package/dist/dao/camera/camera.dao.js +93 -0
- package/dist/dao/camera/camera.dao.js.map +1 -0
- package/dist/dao/video/video.dao.d.ts +5 -0
- package/dist/dao/video/video.dao.js +46 -0
- package/dist/dao/video/video.dao.js.map +1 -1
- package/dist/index.d.ts +12 -10
- package/dist/index.js +13 -11
- package/dist/index.js.map +1 -1
- package/dist/interfaces/camera/camera.interfaces.d.ts +9 -0
- package/dist/interfaces/camera/camera.interfaces.js +3 -0
- package/dist/interfaces/camera/camera.interfaces.js.map +1 -0
- package/dist/interfaces/folder/folder.interfaces.d.ts +3 -0
- package/dist/interfaces/video/video.interfaces.d.ts +8 -0
- package/migrations/20250910015452_migration.ts +34 -0
- package/migrations/20250911000000_migration.ts +61 -0
- package/migrations/20250917144153_migration.ts +37 -0
- package/package.json +1 -1
- package/plan.md +90 -265
- package/src/dao/VideoMinuteResultDAO.ts +517 -0
- package/src/dao/camera/camera.dao.ts +79 -0
- package/src/dao/video/video.dao.ts +49 -0
- package/src/index.ts +12 -10
- package/src/interfaces/camera/camera.interfaces.ts +9 -0
- package/src/interfaces/folder/folder.interfaces.ts +3 -0
- package/src/interfaces/video/video.interfaces.ts +13 -0
package/plan.md
CHANGED
|
@@ -1,304 +1,129 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Lane/Annotation Reuse Implementation Plan
|
|
2
2
|
|
|
3
3
|
## Schema Changes
|
|
4
4
|
|
|
5
|
-
###
|
|
5
|
+
### Tables
|
|
6
6
|
|
|
7
|
-
|
|
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
|
-
|
|
9
|
+
### Modifications
|
|
22
10
|
|
|
23
|
-
-
|
|
24
|
-
- `
|
|
25
|
-
-
|
|
26
|
-
-
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
-
###
|
|
95
|
-
|
|
96
|
-
**File
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
62
|
+
## Interfaces
|
|
146
63
|
|
|
147
|
-
|
|
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
|
-
|
|
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
|
-
|
|
72
|
+
## Implementation Order
|
|
155
73
|
|
|
156
|
-
**
|
|
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
|
-
|
|
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
|
-
|
|
82
|
+
### Template Selection Rules
|
|
164
83
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
###
|
|
89
|
+
### Data Integrity
|
|
193
90
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
95
|
+
## Performance Considerations
|
|
202
96
|
|
|
203
|
-
|
|
97
|
+
### Indexes
|
|
204
98
|
|
|
205
|
-
|
|
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
|
-
|
|
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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
- **
|
|
274
|
-
- **
|
|
275
|
-
- **
|
|
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
|
-
- **
|
|
280
|
-
- **
|
|
281
|
-
- **
|
|
282
|
-
- **
|
|
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
|
-
|
|
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
|
-
|
|
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
|