@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/dist/dao/VideoMinuteResultDAO.d.ts +101 -0
- package/dist/dao/VideoMinuteResultDAO.js +366 -0
- package/dist/dao/VideoMinuteResultDAO.js.map +1 -1
- package/dist/interfaces/video/video.interfaces.d.ts +4 -0
- package/migrations/20250910015452_migration.ts +34 -0
- package/package.json +1 -1
- package/src/dao/VideoMinuteResultDAO.ts +517 -0
- package/src/interfaces/video/video.interfaces.ts +9 -0
- package/plan.md +0 -304
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.
|