dvijaykrishnan commited on
Commit
58864c6
·
1 Parent(s): c00e226

feat: Implement "Add to Workbench" functionality for videos, including a new database column, UI component, server action, and a debug API route.

Browse files
drizzle/0014_windy_shadowcat.sql CHANGED
@@ -1,2 +1,2 @@
1
- ALTER TABLE "youtube_videos" ADD COLUMN "width" integer;--> statement-breakpoint
2
- ALTER TABLE "youtube_videos" ADD COLUMN "height" integer;
 
1
+ -- ALTER TABLE "youtube_videos" ADD COLUMN "width" integer;--> statement-breakpoint
2
+ -- ALTER TABLE "youtube_videos" ADD COLUMN "height" integer;
drizzle/0015_orange_black_knight.sql ADDED
@@ -0,0 +1 @@
 
 
1
+ ALTER TABLE "youtube_videos" ADD COLUMN "is_in_workbench" boolean DEFAULT true NOT NULL;
drizzle/meta/0015_snapshot.json ADDED
@@ -0,0 +1,2085 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "id": "2c5eb6dc-754a-47f3-bf8b-6c37e56fa9c7",
3
+ "prevId": "aba5313b-d2f7-4ae7-b648-32e502ec4823",
4
+ "version": "7",
5
+ "dialect": "postgresql",
6
+ "tables": {
7
+ "public.account": {
8
+ "name": "account",
9
+ "schema": "",
10
+ "columns": {
11
+ "id": {
12
+ "name": "id",
13
+ "type": "text",
14
+ "primaryKey": true,
15
+ "notNull": true
16
+ },
17
+ "account_id": {
18
+ "name": "account_id",
19
+ "type": "text",
20
+ "primaryKey": false,
21
+ "notNull": true
22
+ },
23
+ "provider_id": {
24
+ "name": "provider_id",
25
+ "type": "text",
26
+ "primaryKey": false,
27
+ "notNull": true
28
+ },
29
+ "user_id": {
30
+ "name": "user_id",
31
+ "type": "text",
32
+ "primaryKey": false,
33
+ "notNull": true
34
+ },
35
+ "access_token": {
36
+ "name": "access_token",
37
+ "type": "text",
38
+ "primaryKey": false,
39
+ "notNull": false
40
+ },
41
+ "refresh_token": {
42
+ "name": "refresh_token",
43
+ "type": "text",
44
+ "primaryKey": false,
45
+ "notNull": false
46
+ },
47
+ "id_token": {
48
+ "name": "id_token",
49
+ "type": "text",
50
+ "primaryKey": false,
51
+ "notNull": false
52
+ },
53
+ "access_token_expires_at": {
54
+ "name": "access_token_expires_at",
55
+ "type": "timestamp",
56
+ "primaryKey": false,
57
+ "notNull": false
58
+ },
59
+ "refresh_token_expires_at": {
60
+ "name": "refresh_token_expires_at",
61
+ "type": "timestamp",
62
+ "primaryKey": false,
63
+ "notNull": false
64
+ },
65
+ "scope": {
66
+ "name": "scope",
67
+ "type": "text",
68
+ "primaryKey": false,
69
+ "notNull": false
70
+ },
71
+ "password": {
72
+ "name": "password",
73
+ "type": "text",
74
+ "primaryKey": false,
75
+ "notNull": false
76
+ },
77
+ "created_at": {
78
+ "name": "created_at",
79
+ "type": "timestamp",
80
+ "primaryKey": false,
81
+ "notNull": true
82
+ },
83
+ "updated_at": {
84
+ "name": "updated_at",
85
+ "type": "timestamp",
86
+ "primaryKey": false,
87
+ "notNull": true
88
+ }
89
+ },
90
+ "indexes": {},
91
+ "foreignKeys": {
92
+ "account_user_id_users_id_fk": {
93
+ "name": "account_user_id_users_id_fk",
94
+ "tableFrom": "account",
95
+ "tableTo": "users",
96
+ "columnsFrom": [
97
+ "user_id"
98
+ ],
99
+ "columnsTo": [
100
+ "id"
101
+ ],
102
+ "onDelete": "no action",
103
+ "onUpdate": "no action"
104
+ }
105
+ },
106
+ "compositePrimaryKeys": {},
107
+ "uniqueConstraints": {},
108
+ "policies": {},
109
+ "checkConstraints": {},
110
+ "isRLSEnabled": false
111
+ },
112
+ "public.admin_moderation": {
113
+ "name": "admin_moderation",
114
+ "schema": "",
115
+ "columns": {
116
+ "id": {
117
+ "name": "id",
118
+ "type": "text",
119
+ "primaryKey": true,
120
+ "notNull": true
121
+ },
122
+ "detection_id": {
123
+ "name": "detection_id",
124
+ "type": "text",
125
+ "primaryKey": false,
126
+ "notNull": true
127
+ },
128
+ "admin_id": {
129
+ "name": "admin_id",
130
+ "type": "text",
131
+ "primaryKey": false,
132
+ "notNull": true
133
+ },
134
+ "action": {
135
+ "name": "action",
136
+ "type": "admin_moderation_type",
137
+ "typeSchema": "public",
138
+ "primaryKey": false,
139
+ "notNull": true
140
+ },
141
+ "reason_code": {
142
+ "name": "reason_code",
143
+ "type": "reason_code",
144
+ "typeSchema": "public",
145
+ "primaryKey": false,
146
+ "notNull": false
147
+ },
148
+ "original_values": {
149
+ "name": "original_values",
150
+ "type": "jsonb",
151
+ "primaryKey": false,
152
+ "notNull": true
153
+ },
154
+ "corrected_values": {
155
+ "name": "corrected_values",
156
+ "type": "jsonb",
157
+ "primaryKey": false,
158
+ "notNull": false
159
+ },
160
+ "train_ai_flag": {
161
+ "name": "train_ai_flag",
162
+ "type": "boolean",
163
+ "primaryKey": false,
164
+ "notNull": true,
165
+ "default": true
166
+ },
167
+ "created_at": {
168
+ "name": "created_at",
169
+ "type": "timestamp (3) with time zone",
170
+ "primaryKey": false,
171
+ "notNull": true,
172
+ "default": "now()"
173
+ },
174
+ "updated_at": {
175
+ "name": "updated_at",
176
+ "type": "timestamp (3) with time zone",
177
+ "primaryKey": false,
178
+ "notNull": true,
179
+ "default": "now()"
180
+ }
181
+ },
182
+ "indexes": {},
183
+ "foreignKeys": {
184
+ "admin_moderation_detection_id_detected_objects_id_fk": {
185
+ "name": "admin_moderation_detection_id_detected_objects_id_fk",
186
+ "tableFrom": "admin_moderation",
187
+ "tableTo": "detected_objects",
188
+ "columnsFrom": [
189
+ "detection_id"
190
+ ],
191
+ "columnsTo": [
192
+ "id"
193
+ ],
194
+ "onDelete": "cascade",
195
+ "onUpdate": "no action"
196
+ },
197
+ "admin_moderation_admin_id_users_id_fk": {
198
+ "name": "admin_moderation_admin_id_users_id_fk",
199
+ "tableFrom": "admin_moderation",
200
+ "tableTo": "users",
201
+ "columnsFrom": [
202
+ "admin_id"
203
+ ],
204
+ "columnsTo": [
205
+ "id"
206
+ ],
207
+ "onDelete": "no action",
208
+ "onUpdate": "no action"
209
+ }
210
+ },
211
+ "compositePrimaryKeys": {},
212
+ "uniqueConstraints": {},
213
+ "policies": {},
214
+ "checkConstraints": {},
215
+ "isRLSEnabled": false
216
+ },
217
+ "public.affiliate_proposals": {
218
+ "name": "affiliate_proposals",
219
+ "schema": "",
220
+ "columns": {
221
+ "id": {
222
+ "name": "id",
223
+ "type": "text",
224
+ "primaryKey": true,
225
+ "notNull": true
226
+ },
227
+ "object_id": {
228
+ "name": "object_id",
229
+ "type": "text",
230
+ "primaryKey": false,
231
+ "notNull": false
232
+ },
233
+ "video_id": {
234
+ "name": "video_id",
235
+ "type": "text",
236
+ "primaryKey": false,
237
+ "notNull": true
238
+ },
239
+ "creator_id": {
240
+ "name": "creator_id",
241
+ "type": "text",
242
+ "primaryKey": false,
243
+ "notNull": true
244
+ },
245
+ "submitter_name": {
246
+ "name": "submitter_name",
247
+ "type": "text",
248
+ "primaryKey": false,
249
+ "notNull": false
250
+ },
251
+ "submitter_email": {
252
+ "name": "submitter_email",
253
+ "type": "text",
254
+ "primaryKey": false,
255
+ "notNull": false
256
+ },
257
+ "product_url": {
258
+ "name": "product_url",
259
+ "type": "text",
260
+ "primaryKey": false,
261
+ "notNull": true
262
+ },
263
+ "affiliate_url": {
264
+ "name": "affiliate_url",
265
+ "type": "text",
266
+ "primaryKey": false,
267
+ "notNull": true
268
+ },
269
+ "product_name": {
270
+ "name": "product_name",
271
+ "type": "text",
272
+ "primaryKey": false,
273
+ "notNull": true
274
+ },
275
+ "price": {
276
+ "name": "price",
277
+ "type": "real",
278
+ "primaryKey": false,
279
+ "notNull": false
280
+ },
281
+ "image_url": {
282
+ "name": "image_url",
283
+ "type": "text",
284
+ "primaryKey": false,
285
+ "notNull": false
286
+ },
287
+ "note": {
288
+ "name": "note",
289
+ "type": "text",
290
+ "primaryKey": false,
291
+ "notNull": false
292
+ },
293
+ "status": {
294
+ "name": "status",
295
+ "type": "affiliate_submission_status",
296
+ "typeSchema": "public",
297
+ "primaryKey": false,
298
+ "notNull": true,
299
+ "default": "'PENDING'"
300
+ },
301
+ "created_at": {
302
+ "name": "created_at",
303
+ "type": "timestamp (3) with time zone",
304
+ "primaryKey": false,
305
+ "notNull": true,
306
+ "default": "now()"
307
+ },
308
+ "updated_at": {
309
+ "name": "updated_at",
310
+ "type": "timestamp (3) with time zone",
311
+ "primaryKey": false,
312
+ "notNull": true,
313
+ "default": "now()"
314
+ }
315
+ },
316
+ "indexes": {},
317
+ "foreignKeys": {
318
+ "affiliate_proposals_object_id_detected_objects_id_fk": {
319
+ "name": "affiliate_proposals_object_id_detected_objects_id_fk",
320
+ "tableFrom": "affiliate_proposals",
321
+ "tableTo": "detected_objects",
322
+ "columnsFrom": [
323
+ "object_id"
324
+ ],
325
+ "columnsTo": [
326
+ "id"
327
+ ],
328
+ "onDelete": "set null",
329
+ "onUpdate": "no action"
330
+ },
331
+ "affiliate_proposals_video_id_youtube_videos_id_fk": {
332
+ "name": "affiliate_proposals_video_id_youtube_videos_id_fk",
333
+ "tableFrom": "affiliate_proposals",
334
+ "tableTo": "youtube_videos",
335
+ "columnsFrom": [
336
+ "video_id"
337
+ ],
338
+ "columnsTo": [
339
+ "id"
340
+ ],
341
+ "onDelete": "cascade",
342
+ "onUpdate": "no action"
343
+ },
344
+ "affiliate_proposals_creator_id_users_id_fk": {
345
+ "name": "affiliate_proposals_creator_id_users_id_fk",
346
+ "tableFrom": "affiliate_proposals",
347
+ "tableTo": "users",
348
+ "columnsFrom": [
349
+ "creator_id"
350
+ ],
351
+ "columnsTo": [
352
+ "id"
353
+ ],
354
+ "onDelete": "no action",
355
+ "onUpdate": "no action"
356
+ }
357
+ },
358
+ "compositePrimaryKeys": {},
359
+ "uniqueConstraints": {},
360
+ "policies": {},
361
+ "checkConstraints": {},
362
+ "isRLSEnabled": false
363
+ },
364
+ "public.affiliate_revenue": {
365
+ "name": "affiliate_revenue",
366
+ "schema": "",
367
+ "columns": {
368
+ "id": {
369
+ "name": "id",
370
+ "type": "text",
371
+ "primaryKey": true,
372
+ "notNull": true
373
+ },
374
+ "marketplace_match_id": {
375
+ "name": "marketplace_match_id",
376
+ "type": "text",
377
+ "primaryKey": false,
378
+ "notNull": true
379
+ },
380
+ "amount": {
381
+ "name": "amount",
382
+ "type": "real",
383
+ "primaryKey": false,
384
+ "notNull": true
385
+ },
386
+ "recorded_at": {
387
+ "name": "recorded_at",
388
+ "type": "timestamp (3) with time zone",
389
+ "primaryKey": false,
390
+ "notNull": true,
391
+ "default": "now()"
392
+ },
393
+ "order_id": {
394
+ "name": "order_id",
395
+ "type": "text",
396
+ "primaryKey": false,
397
+ "notNull": false
398
+ },
399
+ "created_at": {
400
+ "name": "created_at",
401
+ "type": "timestamp (3) with time zone",
402
+ "primaryKey": false,
403
+ "notNull": true,
404
+ "default": "now()"
405
+ },
406
+ "updated_at": {
407
+ "name": "updated_at",
408
+ "type": "timestamp (3) with time zone",
409
+ "primaryKey": false,
410
+ "notNull": true,
411
+ "default": "now()"
412
+ }
413
+ },
414
+ "indexes": {
415
+ "affiliate_revenue_marketplace_match_id_idx": {
416
+ "name": "affiliate_revenue_marketplace_match_id_idx",
417
+ "columns": [
418
+ {
419
+ "expression": "marketplace_match_id",
420
+ "isExpression": false,
421
+ "asc": true,
422
+ "nulls": "last"
423
+ }
424
+ ],
425
+ "isUnique": false,
426
+ "concurrently": false,
427
+ "method": "btree",
428
+ "with": {}
429
+ },
430
+ "affiliate_revenue_recorded_at_idx": {
431
+ "name": "affiliate_revenue_recorded_at_idx",
432
+ "columns": [
433
+ {
434
+ "expression": "recorded_at",
435
+ "isExpression": false,
436
+ "asc": true,
437
+ "nulls": "last"
438
+ }
439
+ ],
440
+ "isUnique": false,
441
+ "concurrently": false,
442
+ "method": "btree",
443
+ "with": {}
444
+ }
445
+ },
446
+ "foreignKeys": {
447
+ "affiliate_revenue_marketplace_match_id_marketplace_matches_id_fk": {
448
+ "name": "affiliate_revenue_marketplace_match_id_marketplace_matches_id_fk",
449
+ "tableFrom": "affiliate_revenue",
450
+ "tableTo": "marketplace_matches",
451
+ "columnsFrom": [
452
+ "marketplace_match_id"
453
+ ],
454
+ "columnsTo": [
455
+ "id"
456
+ ],
457
+ "onDelete": "cascade",
458
+ "onUpdate": "no action"
459
+ }
460
+ },
461
+ "compositePrimaryKeys": {},
462
+ "uniqueConstraints": {},
463
+ "policies": {},
464
+ "checkConstraints": {},
465
+ "isRLSEnabled": false
466
+ },
467
+ "public.bounty_pledges": {
468
+ "name": "bounty_pledges",
469
+ "schema": "",
470
+ "columns": {
471
+ "id": {
472
+ "name": "id",
473
+ "type": "text",
474
+ "primaryKey": true,
475
+ "notNull": true
476
+ },
477
+ "product_id": {
478
+ "name": "product_id",
479
+ "type": "text",
480
+ "primaryKey": false,
481
+ "notNull": true
482
+ },
483
+ "marketplace_match_id": {
484
+ "name": "marketplace_match_id",
485
+ "type": "text",
486
+ "primaryKey": false,
487
+ "notNull": true
488
+ },
489
+ "email_hash": {
490
+ "name": "email_hash",
491
+ "type": "text",
492
+ "primaryKey": false,
493
+ "notNull": true
494
+ },
495
+ "encrypted_email": {
496
+ "name": "encrypted_email",
497
+ "type": "text",
498
+ "primaryKey": false,
499
+ "notNull": true
500
+ },
501
+ "pledge_amount": {
502
+ "name": "pledge_amount",
503
+ "type": "real",
504
+ "primaryKey": false,
505
+ "notNull": true
506
+ },
507
+ "currency": {
508
+ "name": "currency",
509
+ "type": "text",
510
+ "primaryKey": false,
511
+ "notNull": true,
512
+ "default": "'USD'"
513
+ },
514
+ "status": {
515
+ "name": "status",
516
+ "type": "bounty_pledge_status",
517
+ "typeSchema": "public",
518
+ "primaryKey": false,
519
+ "notNull": true,
520
+ "default": "'ACTIVE'"
521
+ },
522
+ "consent_timestamp": {
523
+ "name": "consent_timestamp",
524
+ "type": "timestamp (3) with time zone",
525
+ "primaryKey": false,
526
+ "notNull": true,
527
+ "default": "now()"
528
+ },
529
+ "consent_ip": {
530
+ "name": "consent_ip",
531
+ "type": "text",
532
+ "primaryKey": false,
533
+ "notNull": false
534
+ },
535
+ "unsubscribe_token": {
536
+ "name": "unsubscribe_token",
537
+ "type": "text",
538
+ "primaryKey": false,
539
+ "notNull": true
540
+ },
541
+ "notified_at": {
542
+ "name": "notified_at",
543
+ "type": "timestamp (3) with time zone",
544
+ "primaryKey": false,
545
+ "notNull": false
546
+ },
547
+ "created_at": {
548
+ "name": "created_at",
549
+ "type": "timestamp (3) with time zone",
550
+ "primaryKey": false,
551
+ "notNull": true,
552
+ "default": "now()"
553
+ },
554
+ "updated_at": {
555
+ "name": "updated_at",
556
+ "type": "timestamp (3) with time zone",
557
+ "primaryKey": false,
558
+ "notNull": true,
559
+ "default": "now()"
560
+ }
561
+ },
562
+ "indexes": {
563
+ "bounty_pledges_product_id_idx": {
564
+ "name": "bounty_pledges_product_id_idx",
565
+ "columns": [
566
+ {
567
+ "expression": "product_id",
568
+ "isExpression": false,
569
+ "asc": true,
570
+ "nulls": "last"
571
+ }
572
+ ],
573
+ "isUnique": false,
574
+ "concurrently": false,
575
+ "method": "btree",
576
+ "with": {}
577
+ },
578
+ "bounty_pledges_status_idx": {
579
+ "name": "bounty_pledges_status_idx",
580
+ "columns": [
581
+ {
582
+ "expression": "status",
583
+ "isExpression": false,
584
+ "asc": true,
585
+ "nulls": "last"
586
+ }
587
+ ],
588
+ "isUnique": false,
589
+ "concurrently": false,
590
+ "method": "btree",
591
+ "with": {}
592
+ },
593
+ "bounty_pledges_email_hash_idx": {
594
+ "name": "bounty_pledges_email_hash_idx",
595
+ "columns": [
596
+ {
597
+ "expression": "email_hash",
598
+ "isExpression": false,
599
+ "asc": true,
600
+ "nulls": "last"
601
+ }
602
+ ],
603
+ "isUnique": false,
604
+ "concurrently": false,
605
+ "method": "btree",
606
+ "with": {}
607
+ }
608
+ },
609
+ "foreignKeys": {
610
+ "bounty_pledges_marketplace_match_id_marketplace_matches_id_fk": {
611
+ "name": "bounty_pledges_marketplace_match_id_marketplace_matches_id_fk",
612
+ "tableFrom": "bounty_pledges",
613
+ "tableTo": "marketplace_matches",
614
+ "columnsFrom": [
615
+ "marketplace_match_id"
616
+ ],
617
+ "columnsTo": [
618
+ "id"
619
+ ],
620
+ "onDelete": "cascade",
621
+ "onUpdate": "no action"
622
+ }
623
+ },
624
+ "compositePrimaryKeys": {},
625
+ "uniqueConstraints": {},
626
+ "policies": {},
627
+ "checkConstraints": {},
628
+ "isRLSEnabled": false
629
+ },
630
+ "public.detected_objects": {
631
+ "name": "detected_objects",
632
+ "schema": "",
633
+ "columns": {
634
+ "id": {
635
+ "name": "id",
636
+ "type": "text",
637
+ "primaryKey": true,
638
+ "notNull": true
639
+ },
640
+ "video_id": {
641
+ "name": "video_id",
642
+ "type": "text",
643
+ "primaryKey": false,
644
+ "notNull": true
645
+ },
646
+ "object_name": {
647
+ "name": "object_name",
648
+ "type": "text",
649
+ "primaryKey": false,
650
+ "notNull": true
651
+ },
652
+ "category": {
653
+ "name": "category",
654
+ "type": "object_category",
655
+ "typeSchema": "public",
656
+ "primaryKey": false,
657
+ "notNull": true
658
+ },
659
+ "confidence_score": {
660
+ "name": "confidence_score",
661
+ "type": "real",
662
+ "primaryKey": false,
663
+ "notNull": true
664
+ },
665
+ "frame_timestamp": {
666
+ "name": "frame_timestamp",
667
+ "type": "integer",
668
+ "primaryKey": false,
669
+ "notNull": true
670
+ },
671
+ "detection_metadata": {
672
+ "name": "detection_metadata",
673
+ "type": "jsonb",
674
+ "primaryKey": false,
675
+ "notNull": false
676
+ },
677
+ "thumbnail_url": {
678
+ "name": "thumbnail_url",
679
+ "type": "text",
680
+ "primaryKey": false,
681
+ "notNull": false
682
+ },
683
+ "status": {
684
+ "name": "status",
685
+ "type": "detection_status",
686
+ "typeSchema": "public",
687
+ "primaryKey": false,
688
+ "notNull": true,
689
+ "default": "'pending_review'"
690
+ },
691
+ "moderation_status": {
692
+ "name": "moderation_status",
693
+ "type": "moderation_status",
694
+ "typeSchema": "public",
695
+ "primaryKey": false,
696
+ "notNull": true,
697
+ "default": "'PENDING'"
698
+ },
699
+ "moderated_at": {
700
+ "name": "moderated_at",
701
+ "type": "timestamp (3) with time zone",
702
+ "primaryKey": false,
703
+ "notNull": false
704
+ },
705
+ "moderated_by": {
706
+ "name": "moderated_by",
707
+ "type": "text",
708
+ "primaryKey": false,
709
+ "notNull": false
710
+ },
711
+ "moderation_metadata": {
712
+ "name": "moderation_metadata",
713
+ "type": "jsonb",
714
+ "primaryKey": false,
715
+ "notNull": false
716
+ },
717
+ "created_at": {
718
+ "name": "created_at",
719
+ "type": "timestamp (3) with time zone",
720
+ "primaryKey": false,
721
+ "notNull": true,
722
+ "default": "now()"
723
+ },
724
+ "updated_at": {
725
+ "name": "updated_at",
726
+ "type": "timestamp (3) with time zone",
727
+ "primaryKey": false,
728
+ "notNull": true,
729
+ "default": "now()"
730
+ }
731
+ },
732
+ "indexes": {},
733
+ "foreignKeys": {
734
+ "detected_objects_video_id_youtube_videos_id_fk": {
735
+ "name": "detected_objects_video_id_youtube_videos_id_fk",
736
+ "tableFrom": "detected_objects",
737
+ "tableTo": "youtube_videos",
738
+ "columnsFrom": [
739
+ "video_id"
740
+ ],
741
+ "columnsTo": [
742
+ "id"
743
+ ],
744
+ "onDelete": "cascade",
745
+ "onUpdate": "no action"
746
+ },
747
+ "detected_objects_moderated_by_users_id_fk": {
748
+ "name": "detected_objects_moderated_by_users_id_fk",
749
+ "tableFrom": "detected_objects",
750
+ "tableTo": "users",
751
+ "columnsFrom": [
752
+ "moderated_by"
753
+ ],
754
+ "columnsTo": [
755
+ "id"
756
+ ],
757
+ "onDelete": "no action",
758
+ "onUpdate": "no action"
759
+ }
760
+ },
761
+ "compositePrimaryKeys": {},
762
+ "uniqueConstraints": {},
763
+ "policies": {},
764
+ "checkConstraints": {},
765
+ "isRLSEnabled": false
766
+ },
767
+ "public.interest_pledges": {
768
+ "name": "interest_pledges",
769
+ "schema": "",
770
+ "columns": {
771
+ "id": {
772
+ "name": "id",
773
+ "type": "text",
774
+ "primaryKey": true,
775
+ "notNull": true
776
+ },
777
+ "marketplace_match_id": {
778
+ "name": "marketplace_match_id",
779
+ "type": "text",
780
+ "primaryKey": false,
781
+ "notNull": false
782
+ },
783
+ "detected_object_id": {
784
+ "name": "detected_object_id",
785
+ "type": "text",
786
+ "primaryKey": false,
787
+ "notNull": false
788
+ },
789
+ "email_hash": {
790
+ "name": "email_hash",
791
+ "type": "text",
792
+ "primaryKey": false,
793
+ "notNull": true
794
+ },
795
+ "email_encrypted": {
796
+ "name": "email_encrypted",
797
+ "type": "text",
798
+ "primaryKey": false,
799
+ "notNull": true
800
+ },
801
+ "status": {
802
+ "name": "status",
803
+ "type": "interest_pledge_status",
804
+ "typeSchema": "public",
805
+ "primaryKey": false,
806
+ "notNull": true,
807
+ "default": "'ACTIVE'"
808
+ },
809
+ "consent_timestamp": {
810
+ "name": "consent_timestamp",
811
+ "type": "timestamp (3) with time zone",
812
+ "primaryKey": false,
813
+ "notNull": true,
814
+ "default": "now()"
815
+ },
816
+ "consent_ip": {
817
+ "name": "consent_ip",
818
+ "type": "text",
819
+ "primaryKey": false,
820
+ "notNull": false
821
+ },
822
+ "unsubscribe_token": {
823
+ "name": "unsubscribe_token",
824
+ "type": "text",
825
+ "primaryKey": false,
826
+ "notNull": true
827
+ },
828
+ "consent_metadata": {
829
+ "name": "consent_metadata",
830
+ "type": "jsonb",
831
+ "primaryKey": false,
832
+ "notNull": false
833
+ },
834
+ "notified_at": {
835
+ "name": "notified_at",
836
+ "type": "timestamp (3) with time zone",
837
+ "primaryKey": false,
838
+ "notNull": false
839
+ },
840
+ "created_at": {
841
+ "name": "created_at",
842
+ "type": "timestamp (3) with time zone",
843
+ "primaryKey": false,
844
+ "notNull": true,
845
+ "default": "now()"
846
+ },
847
+ "updated_at": {
848
+ "name": "updated_at",
849
+ "type": "timestamp (3) with time zone",
850
+ "primaryKey": false,
851
+ "notNull": true,
852
+ "default": "now()"
853
+ }
854
+ },
855
+ "indexes": {
856
+ "interest_pledges_marketplace_match_id_idx": {
857
+ "name": "interest_pledges_marketplace_match_id_idx",
858
+ "columns": [
859
+ {
860
+ "expression": "marketplace_match_id",
861
+ "isExpression": false,
862
+ "asc": true,
863
+ "nulls": "last"
864
+ }
865
+ ],
866
+ "isUnique": false,
867
+ "concurrently": false,
868
+ "method": "btree",
869
+ "with": {}
870
+ },
871
+ "interest_pledges_detected_object_id_idx": {
872
+ "name": "interest_pledges_detected_object_id_idx",
873
+ "columns": [
874
+ {
875
+ "expression": "detected_object_id",
876
+ "isExpression": false,
877
+ "asc": true,
878
+ "nulls": "last"
879
+ }
880
+ ],
881
+ "isUnique": false,
882
+ "concurrently": false,
883
+ "method": "btree",
884
+ "with": {}
885
+ },
886
+ "interest_pledges_unsubscribe_token_idx": {
887
+ "name": "interest_pledges_unsubscribe_token_idx",
888
+ "columns": [
889
+ {
890
+ "expression": "unsubscribe_token",
891
+ "isExpression": false,
892
+ "asc": true,
893
+ "nulls": "last"
894
+ }
895
+ ],
896
+ "isUnique": true,
897
+ "concurrently": false,
898
+ "method": "btree",
899
+ "with": {}
900
+ }
901
+ },
902
+ "foreignKeys": {
903
+ "interest_pledges_marketplace_match_id_marketplace_matches_id_fk": {
904
+ "name": "interest_pledges_marketplace_match_id_marketplace_matches_id_fk",
905
+ "tableFrom": "interest_pledges",
906
+ "tableTo": "marketplace_matches",
907
+ "columnsFrom": [
908
+ "marketplace_match_id"
909
+ ],
910
+ "columnsTo": [
911
+ "id"
912
+ ],
913
+ "onDelete": "cascade",
914
+ "onUpdate": "no action"
915
+ },
916
+ "interest_pledges_detected_object_id_detected_objects_id_fk": {
917
+ "name": "interest_pledges_detected_object_id_detected_objects_id_fk",
918
+ "tableFrom": "interest_pledges",
919
+ "tableTo": "detected_objects",
920
+ "columnsFrom": [
921
+ "detected_object_id"
922
+ ],
923
+ "columnsTo": [
924
+ "id"
925
+ ],
926
+ "onDelete": "cascade",
927
+ "onUpdate": "no action"
928
+ }
929
+ },
930
+ "compositePrimaryKeys": {},
931
+ "uniqueConstraints": {},
932
+ "policies": {},
933
+ "checkConstraints": {},
934
+ "isRLSEnabled": false
935
+ },
936
+ "public.marketplace_matches": {
937
+ "name": "marketplace_matches",
938
+ "schema": "",
939
+ "columns": {
940
+ "id": {
941
+ "name": "id",
942
+ "type": "text",
943
+ "primaryKey": true,
944
+ "notNull": true
945
+ },
946
+ "object_id": {
947
+ "name": "object_id",
948
+ "type": "text",
949
+ "primaryKey": false,
950
+ "notNull": true
951
+ },
952
+ "marketplace": {
953
+ "name": "marketplace",
954
+ "type": "marketplace_type",
955
+ "typeSchema": "public",
956
+ "primaryKey": false,
957
+ "notNull": true
958
+ },
959
+ "product_id": {
960
+ "name": "product_id",
961
+ "type": "text",
962
+ "primaryKey": false,
963
+ "notNull": true
964
+ },
965
+ "product_name": {
966
+ "name": "product_name",
967
+ "type": "text",
968
+ "primaryKey": false,
969
+ "notNull": true
970
+ },
971
+ "price": {
972
+ "name": "price",
973
+ "type": "real",
974
+ "primaryKey": false,
975
+ "notNull": true
976
+ },
977
+ "availability_status": {
978
+ "name": "availability_status",
979
+ "type": "availability_status",
980
+ "typeSchema": "public",
981
+ "primaryKey": false,
982
+ "notNull": true
983
+ },
984
+ "affiliate_url": {
985
+ "name": "affiliate_url",
986
+ "type": "text",
987
+ "primaryKey": false,
988
+ "notNull": true
989
+ },
990
+ "image_url": {
991
+ "name": "image_url",
992
+ "type": "text",
993
+ "primaryKey": false,
994
+ "notNull": false
995
+ },
996
+ "link_status": {
997
+ "name": "link_status",
998
+ "type": "link_status",
999
+ "typeSchema": "public",
1000
+ "primaryKey": false,
1001
+ "notNull": true,
1002
+ "default": "'ACTIVE'"
1003
+ },
1004
+ "last_checked_at": {
1005
+ "name": "last_checked_at",
1006
+ "type": "timestamp (3) with time zone",
1007
+ "primaryKey": false,
1008
+ "notNull": false
1009
+ },
1010
+ "check_attempts": {
1011
+ "name": "check_attempts",
1012
+ "type": "integer",
1013
+ "primaryKey": false,
1014
+ "notNull": true,
1015
+ "default": 0
1016
+ },
1017
+ "check_metadata": {
1018
+ "name": "check_metadata",
1019
+ "type": "jsonb",
1020
+ "primaryKey": false,
1021
+ "notNull": false
1022
+ },
1023
+ "matched_at": {
1024
+ "name": "matched_at",
1025
+ "type": "timestamp (3) with time zone",
1026
+ "primaryKey": false,
1027
+ "notNull": true,
1028
+ "default": "now()"
1029
+ },
1030
+ "created_at": {
1031
+ "name": "created_at",
1032
+ "type": "timestamp (3) with time zone",
1033
+ "primaryKey": false,
1034
+ "notNull": true,
1035
+ "default": "now()"
1036
+ },
1037
+ "updated_at": {
1038
+ "name": "updated_at",
1039
+ "type": "timestamp (3) with time zone",
1040
+ "primaryKey": false,
1041
+ "notNull": true,
1042
+ "default": "now()"
1043
+ }
1044
+ },
1045
+ "indexes": {
1046
+ "marketplace_matches_link_status_idx": {
1047
+ "name": "marketplace_matches_link_status_idx",
1048
+ "columns": [
1049
+ {
1050
+ "expression": "link_status",
1051
+ "isExpression": false,
1052
+ "asc": true,
1053
+ "nulls": "last"
1054
+ }
1055
+ ],
1056
+ "isUnique": false,
1057
+ "concurrently": false,
1058
+ "method": "btree",
1059
+ "with": {}
1060
+ },
1061
+ "marketplace_matches_last_checked_at_idx": {
1062
+ "name": "marketplace_matches_last_checked_at_idx",
1063
+ "columns": [
1064
+ {
1065
+ "expression": "last_checked_at",
1066
+ "isExpression": false,
1067
+ "asc": true,
1068
+ "nulls": "last"
1069
+ }
1070
+ ],
1071
+ "isUnique": false,
1072
+ "concurrently": false,
1073
+ "method": "btree",
1074
+ "with": {}
1075
+ }
1076
+ },
1077
+ "foreignKeys": {
1078
+ "marketplace_matches_object_id_detected_objects_id_fk": {
1079
+ "name": "marketplace_matches_object_id_detected_objects_id_fk",
1080
+ "tableFrom": "marketplace_matches",
1081
+ "tableTo": "detected_objects",
1082
+ "columnsFrom": [
1083
+ "object_id"
1084
+ ],
1085
+ "columnsTo": [
1086
+ "id"
1087
+ ],
1088
+ "onDelete": "cascade",
1089
+ "onUpdate": "no action"
1090
+ }
1091
+ },
1092
+ "compositePrimaryKeys": {},
1093
+ "uniqueConstraints": {},
1094
+ "policies": {},
1095
+ "checkConstraints": {},
1096
+ "isRLSEnabled": false
1097
+ },
1098
+ "public.product_clicks": {
1099
+ "name": "product_clicks",
1100
+ "schema": "",
1101
+ "columns": {
1102
+ "id": {
1103
+ "name": "id",
1104
+ "type": "text",
1105
+ "primaryKey": true,
1106
+ "notNull": true
1107
+ },
1108
+ "marketplace_match_id": {
1109
+ "name": "marketplace_match_id",
1110
+ "type": "text",
1111
+ "primaryKey": false,
1112
+ "notNull": true
1113
+ },
1114
+ "clicked_at": {
1115
+ "name": "clicked_at",
1116
+ "type": "timestamp (3) with time zone",
1117
+ "primaryKey": false,
1118
+ "notNull": true,
1119
+ "default": "now()"
1120
+ },
1121
+ "viewer_ip": {
1122
+ "name": "viewer_ip",
1123
+ "type": "text",
1124
+ "primaryKey": false,
1125
+ "notNull": false
1126
+ },
1127
+ "user_agent": {
1128
+ "name": "user_agent",
1129
+ "type": "text",
1130
+ "primaryKey": false,
1131
+ "notNull": false
1132
+ },
1133
+ "referrer": {
1134
+ "name": "referrer",
1135
+ "type": "text",
1136
+ "primaryKey": false,
1137
+ "notNull": false
1138
+ },
1139
+ "created_at": {
1140
+ "name": "created_at",
1141
+ "type": "timestamp (3) with time zone",
1142
+ "primaryKey": false,
1143
+ "notNull": true,
1144
+ "default": "now()"
1145
+ },
1146
+ "updated_at": {
1147
+ "name": "updated_at",
1148
+ "type": "timestamp (3) with time zone",
1149
+ "primaryKey": false,
1150
+ "notNull": true,
1151
+ "default": "now()"
1152
+ }
1153
+ },
1154
+ "indexes": {
1155
+ "product_clicks_marketplace_match_id_idx": {
1156
+ "name": "product_clicks_marketplace_match_id_idx",
1157
+ "columns": [
1158
+ {
1159
+ "expression": "marketplace_match_id",
1160
+ "isExpression": false,
1161
+ "asc": true,
1162
+ "nulls": "last"
1163
+ }
1164
+ ],
1165
+ "isUnique": false,
1166
+ "concurrently": false,
1167
+ "method": "btree",
1168
+ "with": {}
1169
+ },
1170
+ "product_clicks_clicked_at_idx": {
1171
+ "name": "product_clicks_clicked_at_idx",
1172
+ "columns": [
1173
+ {
1174
+ "expression": "clicked_at",
1175
+ "isExpression": false,
1176
+ "asc": true,
1177
+ "nulls": "last"
1178
+ }
1179
+ ],
1180
+ "isUnique": false,
1181
+ "concurrently": false,
1182
+ "method": "btree",
1183
+ "with": {}
1184
+ }
1185
+ },
1186
+ "foreignKeys": {
1187
+ "product_clicks_marketplace_match_id_marketplace_matches_id_fk": {
1188
+ "name": "product_clicks_marketplace_match_id_marketplace_matches_id_fk",
1189
+ "tableFrom": "product_clicks",
1190
+ "tableTo": "marketplace_matches",
1191
+ "columnsFrom": [
1192
+ "marketplace_match_id"
1193
+ ],
1194
+ "columnsTo": [
1195
+ "id"
1196
+ ],
1197
+ "onDelete": "cascade",
1198
+ "onUpdate": "no action"
1199
+ }
1200
+ },
1201
+ "compositePrimaryKeys": {},
1202
+ "uniqueConstraints": {},
1203
+ "policies": {},
1204
+ "checkConstraints": {},
1205
+ "isRLSEnabled": false
1206
+ },
1207
+ "public.product_requests": {
1208
+ "name": "product_requests",
1209
+ "schema": "",
1210
+ "columns": {
1211
+ "id": {
1212
+ "name": "id",
1213
+ "type": "text",
1214
+ "primaryKey": true,
1215
+ "notNull": true
1216
+ },
1217
+ "video_id": {
1218
+ "name": "video_id",
1219
+ "type": "text",
1220
+ "primaryKey": false,
1221
+ "notNull": true
1222
+ },
1223
+ "creator_id": {
1224
+ "name": "creator_id",
1225
+ "type": "text",
1226
+ "primaryKey": false,
1227
+ "notNull": true
1228
+ },
1229
+ "viewer_name": {
1230
+ "name": "viewer_name",
1231
+ "type": "text",
1232
+ "primaryKey": false,
1233
+ "notNull": false
1234
+ },
1235
+ "viewer_email": {
1236
+ "name": "viewer_email",
1237
+ "type": "text",
1238
+ "primaryKey": false,
1239
+ "notNull": false
1240
+ },
1241
+ "note": {
1242
+ "name": "note",
1243
+ "type": "text",
1244
+ "primaryKey": false,
1245
+ "notNull": true
1246
+ },
1247
+ "image_url": {
1248
+ "name": "image_url",
1249
+ "type": "text",
1250
+ "primaryKey": false,
1251
+ "notNull": false
1252
+ },
1253
+ "frame_timestamp": {
1254
+ "name": "frame_timestamp",
1255
+ "type": "integer",
1256
+ "primaryKey": false,
1257
+ "notNull": false
1258
+ },
1259
+ "status": {
1260
+ "name": "status",
1261
+ "type": "request_status",
1262
+ "typeSchema": "public",
1263
+ "primaryKey": false,
1264
+ "notNull": true,
1265
+ "default": "'PENDING'"
1266
+ },
1267
+ "created_at": {
1268
+ "name": "created_at",
1269
+ "type": "timestamp (3) with time zone",
1270
+ "primaryKey": false,
1271
+ "notNull": true,
1272
+ "default": "now()"
1273
+ },
1274
+ "updated_at": {
1275
+ "name": "updated_at",
1276
+ "type": "timestamp (3) with time zone",
1277
+ "primaryKey": false,
1278
+ "notNull": true,
1279
+ "default": "now()"
1280
+ }
1281
+ },
1282
+ "indexes": {},
1283
+ "foreignKeys": {
1284
+ "product_requests_video_id_youtube_videos_id_fk": {
1285
+ "name": "product_requests_video_id_youtube_videos_id_fk",
1286
+ "tableFrom": "product_requests",
1287
+ "tableTo": "youtube_videos",
1288
+ "columnsFrom": [
1289
+ "video_id"
1290
+ ],
1291
+ "columnsTo": [
1292
+ "id"
1293
+ ],
1294
+ "onDelete": "cascade",
1295
+ "onUpdate": "no action"
1296
+ },
1297
+ "product_requests_creator_id_users_id_fk": {
1298
+ "name": "product_requests_creator_id_users_id_fk",
1299
+ "tableFrom": "product_requests",
1300
+ "tableTo": "users",
1301
+ "columnsFrom": [
1302
+ "creator_id"
1303
+ ],
1304
+ "columnsTo": [
1305
+ "id"
1306
+ ],
1307
+ "onDelete": "no action",
1308
+ "onUpdate": "no action"
1309
+ }
1310
+ },
1311
+ "compositePrimaryKeys": {},
1312
+ "uniqueConstraints": {},
1313
+ "policies": {},
1314
+ "checkConstraints": {},
1315
+ "isRLSEnabled": false
1316
+ },
1317
+ "public.session": {
1318
+ "name": "session",
1319
+ "schema": "",
1320
+ "columns": {
1321
+ "id": {
1322
+ "name": "id",
1323
+ "type": "text",
1324
+ "primaryKey": true,
1325
+ "notNull": true
1326
+ },
1327
+ "expires_at": {
1328
+ "name": "expires_at",
1329
+ "type": "timestamp",
1330
+ "primaryKey": false,
1331
+ "notNull": true
1332
+ },
1333
+ "token": {
1334
+ "name": "token",
1335
+ "type": "text",
1336
+ "primaryKey": false,
1337
+ "notNull": true
1338
+ },
1339
+ "created_at": {
1340
+ "name": "created_at",
1341
+ "type": "timestamp",
1342
+ "primaryKey": false,
1343
+ "notNull": true
1344
+ },
1345
+ "updated_at": {
1346
+ "name": "updated_at",
1347
+ "type": "timestamp",
1348
+ "primaryKey": false,
1349
+ "notNull": true
1350
+ },
1351
+ "ip_address": {
1352
+ "name": "ip_address",
1353
+ "type": "text",
1354
+ "primaryKey": false,
1355
+ "notNull": false
1356
+ },
1357
+ "user_agent": {
1358
+ "name": "user_agent",
1359
+ "type": "text",
1360
+ "primaryKey": false,
1361
+ "notNull": false
1362
+ },
1363
+ "user_id": {
1364
+ "name": "user_id",
1365
+ "type": "text",
1366
+ "primaryKey": false,
1367
+ "notNull": true
1368
+ }
1369
+ },
1370
+ "indexes": {},
1371
+ "foreignKeys": {
1372
+ "session_user_id_users_id_fk": {
1373
+ "name": "session_user_id_users_id_fk",
1374
+ "tableFrom": "session",
1375
+ "tableTo": "users",
1376
+ "columnsFrom": [
1377
+ "user_id"
1378
+ ],
1379
+ "columnsTo": [
1380
+ "id"
1381
+ ],
1382
+ "onDelete": "no action",
1383
+ "onUpdate": "no action"
1384
+ }
1385
+ },
1386
+ "compositePrimaryKeys": {},
1387
+ "uniqueConstraints": {
1388
+ "session_token_unique": {
1389
+ "name": "session_token_unique",
1390
+ "nullsNotDistinct": false,
1391
+ "columns": [
1392
+ "token"
1393
+ ]
1394
+ }
1395
+ },
1396
+ "policies": {},
1397
+ "checkConstraints": {},
1398
+ "isRLSEnabled": false
1399
+ },
1400
+ "public.users": {
1401
+ "name": "users",
1402
+ "schema": "",
1403
+ "columns": {
1404
+ "id": {
1405
+ "name": "id",
1406
+ "type": "text",
1407
+ "primaryKey": true,
1408
+ "notNull": true
1409
+ },
1410
+ "name": {
1411
+ "name": "name",
1412
+ "type": "text",
1413
+ "primaryKey": false,
1414
+ "notNull": true
1415
+ },
1416
+ "email": {
1417
+ "name": "email",
1418
+ "type": "text",
1419
+ "primaryKey": false,
1420
+ "notNull": true
1421
+ },
1422
+ "email_verified": {
1423
+ "name": "email_verified",
1424
+ "type": "boolean",
1425
+ "primaryKey": false,
1426
+ "notNull": true
1427
+ },
1428
+ "image": {
1429
+ "name": "image",
1430
+ "type": "text",
1431
+ "primaryKey": false,
1432
+ "notNull": false
1433
+ },
1434
+ "created_at": {
1435
+ "name": "created_at",
1436
+ "type": "timestamp",
1437
+ "primaryKey": false,
1438
+ "notNull": true
1439
+ },
1440
+ "updated_at": {
1441
+ "name": "updated_at",
1442
+ "type": "timestamp",
1443
+ "primaryKey": false,
1444
+ "notNull": true
1445
+ }
1446
+ },
1447
+ "indexes": {},
1448
+ "foreignKeys": {},
1449
+ "compositePrimaryKeys": {},
1450
+ "uniqueConstraints": {
1451
+ "users_email_unique": {
1452
+ "name": "users_email_unique",
1453
+ "nullsNotDistinct": false,
1454
+ "columns": [
1455
+ "email"
1456
+ ]
1457
+ }
1458
+ },
1459
+ "policies": {},
1460
+ "checkConstraints": {},
1461
+ "isRLSEnabled": false
1462
+ },
1463
+ "public.verification": {
1464
+ "name": "verification",
1465
+ "schema": "",
1466
+ "columns": {
1467
+ "id": {
1468
+ "name": "id",
1469
+ "type": "text",
1470
+ "primaryKey": true,
1471
+ "notNull": true
1472
+ },
1473
+ "identifier": {
1474
+ "name": "identifier",
1475
+ "type": "text",
1476
+ "primaryKey": false,
1477
+ "notNull": true
1478
+ },
1479
+ "value": {
1480
+ "name": "value",
1481
+ "type": "text",
1482
+ "primaryKey": false,
1483
+ "notNull": true
1484
+ },
1485
+ "expires_at": {
1486
+ "name": "expires_at",
1487
+ "type": "timestamp",
1488
+ "primaryKey": false,
1489
+ "notNull": true
1490
+ },
1491
+ "created_at": {
1492
+ "name": "created_at",
1493
+ "type": "timestamp",
1494
+ "primaryKey": false,
1495
+ "notNull": false
1496
+ },
1497
+ "updated_at": {
1498
+ "name": "updated_at",
1499
+ "type": "timestamp",
1500
+ "primaryKey": false,
1501
+ "notNull": false
1502
+ }
1503
+ },
1504
+ "indexes": {},
1505
+ "foreignKeys": {},
1506
+ "compositePrimaryKeys": {},
1507
+ "uniqueConstraints": {},
1508
+ "policies": {},
1509
+ "checkConstraints": {},
1510
+ "isRLSEnabled": false
1511
+ },
1512
+ "public.video_scan_jobs": {
1513
+ "name": "video_scan_jobs",
1514
+ "schema": "",
1515
+ "columns": {
1516
+ "id": {
1517
+ "name": "id",
1518
+ "type": "text",
1519
+ "primaryKey": true,
1520
+ "notNull": true
1521
+ },
1522
+ "channel_id": {
1523
+ "name": "channel_id",
1524
+ "type": "text",
1525
+ "primaryKey": false,
1526
+ "notNull": true
1527
+ },
1528
+ "user_id": {
1529
+ "name": "user_id",
1530
+ "type": "text",
1531
+ "primaryKey": false,
1532
+ "notNull": true
1533
+ },
1534
+ "status": {
1535
+ "name": "status",
1536
+ "type": "video_scan_status",
1537
+ "typeSchema": "public",
1538
+ "primaryKey": false,
1539
+ "notNull": true,
1540
+ "default": "'pending'"
1541
+ },
1542
+ "progress": {
1543
+ "name": "progress",
1544
+ "type": "integer",
1545
+ "primaryKey": false,
1546
+ "notNull": true,
1547
+ "default": 0
1548
+ },
1549
+ "total_videos": {
1550
+ "name": "total_videos",
1551
+ "type": "integer",
1552
+ "primaryKey": false,
1553
+ "notNull": false
1554
+ },
1555
+ "scanned_videos": {
1556
+ "name": "scanned_videos",
1557
+ "type": "integer",
1558
+ "primaryKey": false,
1559
+ "notNull": true,
1560
+ "default": 0
1561
+ },
1562
+ "error_message": {
1563
+ "name": "error_message",
1564
+ "type": "text",
1565
+ "primaryKey": false,
1566
+ "notNull": false
1567
+ },
1568
+ "inngest_run_id": {
1569
+ "name": "inngest_run_id",
1570
+ "type": "text",
1571
+ "primaryKey": false,
1572
+ "notNull": false
1573
+ },
1574
+ "created_at": {
1575
+ "name": "created_at",
1576
+ "type": "timestamp (3) with time zone",
1577
+ "primaryKey": false,
1578
+ "notNull": true,
1579
+ "default": "now()"
1580
+ },
1581
+ "updated_at": {
1582
+ "name": "updated_at",
1583
+ "type": "timestamp (3) with time zone",
1584
+ "primaryKey": false,
1585
+ "notNull": true,
1586
+ "default": "now()"
1587
+ }
1588
+ },
1589
+ "indexes": {},
1590
+ "foreignKeys": {
1591
+ "video_scan_jobs_channel_id_youtube_channels_id_fk": {
1592
+ "name": "video_scan_jobs_channel_id_youtube_channels_id_fk",
1593
+ "tableFrom": "video_scan_jobs",
1594
+ "tableTo": "youtube_channels",
1595
+ "columnsFrom": [
1596
+ "channel_id"
1597
+ ],
1598
+ "columnsTo": [
1599
+ "id"
1600
+ ],
1601
+ "onDelete": "cascade",
1602
+ "onUpdate": "no action"
1603
+ },
1604
+ "video_scan_jobs_user_id_users_id_fk": {
1605
+ "name": "video_scan_jobs_user_id_users_id_fk",
1606
+ "tableFrom": "video_scan_jobs",
1607
+ "tableTo": "users",
1608
+ "columnsFrom": [
1609
+ "user_id"
1610
+ ],
1611
+ "columnsTo": [
1612
+ "id"
1613
+ ],
1614
+ "onDelete": "no action",
1615
+ "onUpdate": "no action"
1616
+ }
1617
+ },
1618
+ "compositePrimaryKeys": {},
1619
+ "uniqueConstraints": {},
1620
+ "policies": {},
1621
+ "checkConstraints": {},
1622
+ "isRLSEnabled": false
1623
+ },
1624
+ "public.youtube_channels": {
1625
+ "name": "youtube_channels",
1626
+ "schema": "",
1627
+ "columns": {
1628
+ "id": {
1629
+ "name": "id",
1630
+ "type": "text",
1631
+ "primaryKey": true,
1632
+ "notNull": true
1633
+ },
1634
+ "creator_id": {
1635
+ "name": "creator_id",
1636
+ "type": "text",
1637
+ "primaryKey": false,
1638
+ "notNull": true
1639
+ },
1640
+ "channel_id": {
1641
+ "name": "channel_id",
1642
+ "type": "text",
1643
+ "primaryKey": false,
1644
+ "notNull": true
1645
+ },
1646
+ "channel_name": {
1647
+ "name": "channel_name",
1648
+ "type": "text",
1649
+ "primaryKey": false,
1650
+ "notNull": true
1651
+ },
1652
+ "creator_slug": {
1653
+ "name": "creator_slug",
1654
+ "type": "text",
1655
+ "primaryKey": false,
1656
+ "notNull": true
1657
+ },
1658
+ "subscriber_count": {
1659
+ "name": "subscriber_count",
1660
+ "type": "integer",
1661
+ "primaryKey": false,
1662
+ "notNull": false
1663
+ },
1664
+ "thumbnail_url": {
1665
+ "name": "thumbnail_url",
1666
+ "type": "text",
1667
+ "primaryKey": false,
1668
+ "notNull": false
1669
+ },
1670
+ "connected_at": {
1671
+ "name": "connected_at",
1672
+ "type": "timestamp",
1673
+ "primaryKey": false,
1674
+ "notNull": true,
1675
+ "default": "now()"
1676
+ },
1677
+ "sync_status": {
1678
+ "name": "sync_status",
1679
+ "type": "sync_status",
1680
+ "typeSchema": "public",
1681
+ "primaryKey": false,
1682
+ "notNull": true,
1683
+ "default": "'idle'"
1684
+ },
1685
+ "channel_type": {
1686
+ "name": "channel_type",
1687
+ "type": "channel_type",
1688
+ "typeSchema": "public",
1689
+ "primaryKey": false,
1690
+ "notNull": true,
1691
+ "default": "'external'"
1692
+ },
1693
+ "platform": {
1694
+ "name": "platform",
1695
+ "type": "platform",
1696
+ "typeSchema": "public",
1697
+ "primaryKey": false,
1698
+ "notNull": true,
1699
+ "default": "'youtube'"
1700
+ }
1701
+ },
1702
+ "indexes": {
1703
+ "creator_channel_idx": {
1704
+ "name": "creator_channel_idx",
1705
+ "columns": [
1706
+ {
1707
+ "expression": "creator_id",
1708
+ "isExpression": false,
1709
+ "asc": true,
1710
+ "nulls": "last"
1711
+ },
1712
+ {
1713
+ "expression": "channel_id",
1714
+ "isExpression": false,
1715
+ "asc": true,
1716
+ "nulls": "last"
1717
+ }
1718
+ ],
1719
+ "isUnique": true,
1720
+ "concurrently": false,
1721
+ "method": "btree",
1722
+ "with": {}
1723
+ }
1724
+ },
1725
+ "foreignKeys": {
1726
+ "youtube_channels_creator_id_users_id_fk": {
1727
+ "name": "youtube_channels_creator_id_users_id_fk",
1728
+ "tableFrom": "youtube_channels",
1729
+ "tableTo": "users",
1730
+ "columnsFrom": [
1731
+ "creator_id"
1732
+ ],
1733
+ "columnsTo": [
1734
+ "id"
1735
+ ],
1736
+ "onDelete": "no action",
1737
+ "onUpdate": "no action"
1738
+ }
1739
+ },
1740
+ "compositePrimaryKeys": {},
1741
+ "uniqueConstraints": {
1742
+ "youtube_channels_creator_slug_unique": {
1743
+ "name": "youtube_channels_creator_slug_unique",
1744
+ "nullsNotDistinct": false,
1745
+ "columns": [
1746
+ "creator_slug"
1747
+ ]
1748
+ }
1749
+ },
1750
+ "policies": {},
1751
+ "checkConstraints": {},
1752
+ "isRLSEnabled": false
1753
+ },
1754
+ "public.youtube_videos": {
1755
+ "name": "youtube_videos",
1756
+ "schema": "",
1757
+ "columns": {
1758
+ "id": {
1759
+ "name": "id",
1760
+ "type": "text",
1761
+ "primaryKey": true,
1762
+ "notNull": true
1763
+ },
1764
+ "channel_id": {
1765
+ "name": "channel_id",
1766
+ "type": "text",
1767
+ "primaryKey": false,
1768
+ "notNull": true
1769
+ },
1770
+ "video_id": {
1771
+ "name": "video_id",
1772
+ "type": "text",
1773
+ "primaryKey": false,
1774
+ "notNull": true
1775
+ },
1776
+ "title": {
1777
+ "name": "title",
1778
+ "type": "text",
1779
+ "primaryKey": false,
1780
+ "notNull": true
1781
+ },
1782
+ "description": {
1783
+ "name": "description",
1784
+ "type": "text",
1785
+ "primaryKey": false,
1786
+ "notNull": false
1787
+ },
1788
+ "thumbnail_url": {
1789
+ "name": "thumbnail_url",
1790
+ "type": "text",
1791
+ "primaryKey": false,
1792
+ "notNull": false
1793
+ },
1794
+ "duration": {
1795
+ "name": "duration",
1796
+ "type": "text",
1797
+ "primaryKey": false,
1798
+ "notNull": false
1799
+ },
1800
+ "view_count": {
1801
+ "name": "view_count",
1802
+ "type": "integer",
1803
+ "primaryKey": false,
1804
+ "notNull": false
1805
+ },
1806
+ "availability_status": {
1807
+ "name": "availability_status",
1808
+ "type": "text",
1809
+ "primaryKey": false,
1810
+ "notNull": true,
1811
+ "default": "'unknown'"
1812
+ },
1813
+ "scan_status": {
1814
+ "name": "scan_status",
1815
+ "type": "video_scan_status",
1816
+ "typeSchema": "public",
1817
+ "primaryKey": false,
1818
+ "notNull": true,
1819
+ "default": "'pending'"
1820
+ },
1821
+ "published_at": {
1822
+ "name": "published_at",
1823
+ "type": "timestamp (3) with time zone",
1824
+ "primaryKey": false,
1825
+ "notNull": false
1826
+ },
1827
+ "platform": {
1828
+ "name": "platform",
1829
+ "type": "platform",
1830
+ "typeSchema": "public",
1831
+ "primaryKey": false,
1832
+ "notNull": true,
1833
+ "default": "'youtube'"
1834
+ },
1835
+ "url": {
1836
+ "name": "url",
1837
+ "type": "text",
1838
+ "primaryKey": false,
1839
+ "notNull": false
1840
+ },
1841
+ "width": {
1842
+ "name": "width",
1843
+ "type": "integer",
1844
+ "primaryKey": false,
1845
+ "notNull": false
1846
+ },
1847
+ "height": {
1848
+ "name": "height",
1849
+ "type": "integer",
1850
+ "primaryKey": false,
1851
+ "notNull": false
1852
+ },
1853
+ "is_in_workbench": {
1854
+ "name": "is_in_workbench",
1855
+ "type": "boolean",
1856
+ "primaryKey": false,
1857
+ "notNull": true,
1858
+ "default": true
1859
+ },
1860
+ "created_at": {
1861
+ "name": "created_at",
1862
+ "type": "timestamp (3) with time zone",
1863
+ "primaryKey": false,
1864
+ "notNull": true,
1865
+ "default": "now()"
1866
+ },
1867
+ "updated_at": {
1868
+ "name": "updated_at",
1869
+ "type": "timestamp (3) with time zone",
1870
+ "primaryKey": false,
1871
+ "notNull": true,
1872
+ "default": "now()"
1873
+ }
1874
+ },
1875
+ "indexes": {
1876
+ "channel_video_idx": {
1877
+ "name": "channel_video_idx",
1878
+ "columns": [
1879
+ {
1880
+ "expression": "channel_id",
1881
+ "isExpression": false,
1882
+ "asc": true,
1883
+ "nulls": "last"
1884
+ },
1885
+ {
1886
+ "expression": "video_id",
1887
+ "isExpression": false,
1888
+ "asc": true,
1889
+ "nulls": "last"
1890
+ }
1891
+ ],
1892
+ "isUnique": true,
1893
+ "concurrently": false,
1894
+ "method": "btree",
1895
+ "with": {}
1896
+ }
1897
+ },
1898
+ "foreignKeys": {
1899
+ "youtube_videos_channel_id_youtube_channels_id_fk": {
1900
+ "name": "youtube_videos_channel_id_youtube_channels_id_fk",
1901
+ "tableFrom": "youtube_videos",
1902
+ "tableTo": "youtube_channels",
1903
+ "columnsFrom": [
1904
+ "channel_id"
1905
+ ],
1906
+ "columnsTo": [
1907
+ "id"
1908
+ ],
1909
+ "onDelete": "cascade",
1910
+ "onUpdate": "no action"
1911
+ }
1912
+ },
1913
+ "compositePrimaryKeys": {},
1914
+ "uniqueConstraints": {},
1915
+ "policies": {},
1916
+ "checkConstraints": {},
1917
+ "isRLSEnabled": false
1918
+ }
1919
+ },
1920
+ "enums": {
1921
+ "public.admin_moderation_type": {
1922
+ "name": "admin_moderation_type",
1923
+ "schema": "public",
1924
+ "values": [
1925
+ "corrected",
1926
+ "marked_incorrect"
1927
+ ]
1928
+ },
1929
+ "public.affiliate_submission_status": {
1930
+ "name": "affiliate_submission_status",
1931
+ "schema": "public",
1932
+ "values": [
1933
+ "PENDING",
1934
+ "APPROVED",
1935
+ "REJECTED"
1936
+ ]
1937
+ },
1938
+ "public.availability_status": {
1939
+ "name": "availability_status",
1940
+ "schema": "public",
1941
+ "values": [
1942
+ "IN_STOCK",
1943
+ "SOLD_OUT",
1944
+ "DISCONTINUED"
1945
+ ]
1946
+ },
1947
+ "public.bounty_pledge_status": {
1948
+ "name": "bounty_pledge_status",
1949
+ "schema": "public",
1950
+ "values": [
1951
+ "ACTIVE",
1952
+ "WITHDRAWN",
1953
+ "FULFILLED"
1954
+ ]
1955
+ },
1956
+ "public.channel_type": {
1957
+ "name": "channel_type",
1958
+ "schema": "public",
1959
+ "values": [
1960
+ "verified",
1961
+ "external"
1962
+ ]
1963
+ },
1964
+ "public.detection_status": {
1965
+ "name": "detection_status",
1966
+ "schema": "public",
1967
+ "values": [
1968
+ "pending_review",
1969
+ "approved",
1970
+ "rejected",
1971
+ "flagged"
1972
+ ]
1973
+ },
1974
+ "public.interest_pledge_status": {
1975
+ "name": "interest_pledge_status",
1976
+ "schema": "public",
1977
+ "values": [
1978
+ "ACTIVE",
1979
+ "NOTIFIED",
1980
+ "EXPIRED"
1981
+ ]
1982
+ },
1983
+ "public.link_status": {
1984
+ "name": "link_status",
1985
+ "schema": "public",
1986
+ "values": [
1987
+ "ACTIVE",
1988
+ "CHECKING",
1989
+ "BROKEN"
1990
+ ]
1991
+ },
1992
+ "public.marketplace_type": {
1993
+ "name": "marketplace_type",
1994
+ "schema": "public",
1995
+ "values": [
1996
+ "amazon",
1997
+ "ebay",
1998
+ "etsy"
1999
+ ]
2000
+ },
2001
+ "public.moderation_status": {
2002
+ "name": "moderation_status",
2003
+ "schema": "public",
2004
+ "values": [
2005
+ "PENDING",
2006
+ "APPROVED",
2007
+ "REJECTED"
2008
+ ]
2009
+ },
2010
+ "public.object_category": {
2011
+ "name": "object_category",
2012
+ "schema": "public",
2013
+ "values": [
2014
+ "Tech",
2015
+ "Fashion",
2016
+ "Furniture",
2017
+ "Audio",
2018
+ "Other",
2019
+ "Person",
2020
+ "Apparel"
2021
+ ]
2022
+ },
2023
+ "public.platform": {
2024
+ "name": "platform",
2025
+ "schema": "public",
2026
+ "values": [
2027
+ "youtube",
2028
+ "instagram",
2029
+ "tiktok",
2030
+ "facebook"
2031
+ ]
2032
+ },
2033
+ "public.reason_code": {
2034
+ "name": "reason_code",
2035
+ "schema": "public",
2036
+ "values": [
2037
+ "wrong_object",
2038
+ "wrong_category",
2039
+ "false_positive",
2040
+ "unclear_image",
2041
+ "duplicate",
2042
+ "out_of_scope"
2043
+ ]
2044
+ },
2045
+ "public.request_status": {
2046
+ "name": "request_status",
2047
+ "schema": "public",
2048
+ "values": [
2049
+ "PENDING",
2050
+ "FULFILLED",
2051
+ "DISMISSED"
2052
+ ]
2053
+ },
2054
+ "public.sync_status": {
2055
+ "name": "sync_status",
2056
+ "schema": "public",
2057
+ "values": [
2058
+ "idle",
2059
+ "syncing",
2060
+ "errored"
2061
+ ]
2062
+ },
2063
+ "public.video_scan_status": {
2064
+ "name": "video_scan_status",
2065
+ "schema": "public",
2066
+ "values": [
2067
+ "pending",
2068
+ "in_progress",
2069
+ "completed",
2070
+ "failed",
2071
+ "idle"
2072
+ ]
2073
+ }
2074
+ },
2075
+ "schemas": {},
2076
+ "sequences": {},
2077
+ "roles": {},
2078
+ "policies": {},
2079
+ "views": {},
2080
+ "_meta": {
2081
+ "columns": {},
2082
+ "schemas": {},
2083
+ "tables": {}
2084
+ }
2085
+ }
drizzle/meta/_journal.json CHANGED
@@ -106,6 +106,13 @@
106
  "when": 1770636319933,
107
  "tag": "0014_windy_shadowcat",
108
  "breakpoints": true
 
 
 
 
 
 
 
109
  }
110
  ]
111
  }
 
106
  "when": 1770636319933,
107
  "tag": "0014_windy_shadowcat",
108
  "breakpoints": true
109
+ },
110
+ {
111
+ "idx": 15,
112
+ "version": "7",
113
+ "when": 1770685990509,
114
+ "tag": "0015_orange_black_knight",
115
+ "breakpoints": true
116
  }
117
  ]
118
  }
src/app/api/debug/video/route.ts ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import { db } from "@/lib/db";
3
+ import { youtubeVideos, users } from "@/lib/db/schema";
4
+ import { eq } from "drizzle-orm";
5
+ import { NextResponse } from "next/server";
6
+
7
+ export async function GET(request: Request) {
8
+ const { searchParams } = new URL(request.url);
9
+ const videoId = searchParams.get('id');
10
+ const deleteId = searchParams.get('deleteId');
11
+
12
+ if (deleteId) {
13
+ await db.delete(youtubeVideos).where(eq(youtubeVideos.id, deleteId));
14
+ return NextResponse.json({ success: true, deleted: deleteId });
15
+ }
16
+
17
+ if (!videoId) return NextResponse.json({ error: "Missing id" }, { status: 400 });
18
+
19
+ const videos = await db.select().from(youtubeVideos).where(eq(youtubeVideos.videoId, videoId));
20
+
21
+ const usersList = await db.query.users.findMany();
22
+
23
+ return NextResponse.json({ videos, users: usersList });
24
+ }
src/app/showcase/[videoId]/page.tsx CHANGED
@@ -18,6 +18,7 @@ import { ArrowLeft, ShieldCheck, LayoutDashboard } from "lucide-react";
18
  import { auth } from "@/lib/auth";
19
  import { headers } from "next/headers";
20
  import { SearchBar } from '@/features/vault/components/search-bar';
 
21
 
22
  interface ShowcasePageProps {
23
  params: Promise<{
@@ -43,7 +44,12 @@ export default async function ShowcasePage({ params }: ShowcasePageProps) {
43
  const editUrl = `/showcase/${videoId}/moderate`;
44
 
45
  // Check ownership: only the creator who owns this video can access showcase
46
- if (!isAuthenticated || session.user.id !== vault.channel.creatorId) {
 
 
 
 
 
47
  // Redirect to public vault page instead, signaling that it came from the showcase/claim flow
48
  redirect(`/vault/${vault.channel.creatorSlug}/video/${videoId}?claim=true`);
49
  }
@@ -53,21 +59,39 @@ export default async function ShowcasePage({ params }: ShowcasePageProps) {
53
  const canonicalUrl = video?.url || `https://www.youtube.com/watch?v=${videoId}`;
54
  const shouldHideLogo = ['instagram', 'facebook', 'tiktok'].includes(platform);
55
 
 
 
 
 
56
  return (
57
  <Suspense fallback={<VaultGridSkeleton />}>
58
  <div className="min-h-screen bg-background pb-20 overflow-x-hidden">
59
  {/* Demo Banner */}
60
  <div className="bg-primary/10 border-b border-primary/20 backdrop-blur-md sticky top-0 z-50">
61
- <div className="container mx-auto px-4 py-3 flex items-center justify-end gap-2">
62
- <Button asChild size="sm" variant="ghost" className="rounded-full text-xs gap-1.5 text-muted-foreground hover:text-foreground">
63
- <Link href="/dashboard">
64
- <LayoutDashboard className="h-3.5 w-3.5" />
65
- Dashboard
66
- </Link>
67
- </Button>
68
- <Button asChild size="sm" className="rounded-full shadow-lg shadow-primary/20">
69
- <Link href={`/vault/${vault.channel.creatorSlug}/video/${videoId}`}>View Public Page</Link>
70
- </Button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  </div>
72
  </div>
73
 
 
18
  import { auth } from "@/lib/auth";
19
  import { headers } from "next/headers";
20
  import { SearchBar } from '@/features/vault/components/search-bar';
21
+ import { AddToWorkbenchButton } from '@/features/vault/components/add-to-workbench-button';
22
 
23
  interface ShowcasePageProps {
24
  params: Promise<{
 
44
  const editUrl = `/showcase/${videoId}/moderate`;
45
 
46
  // Check ownership: only the creator who owns this video can access showcase
47
+ // CRITICAL FIX: Allow authenticated users to view the showcase if it's currently
48
+ // a "demo" or "fallback" record (owned by demo-creator or similar).
49
+ const isDemoRecord = vault.channel.creatorId.startsWith('demo');
50
+ const isOwner = session?.user?.id === vault.channel.creatorId;
51
+
52
+ if (!isAuthenticated || (!isOwner && !isDemoRecord)) {
53
  // Redirect to public vault page instead, signaling that it came from the showcase/claim flow
54
  redirect(`/vault/${vault.channel.creatorSlug}/video/${videoId}?claim=true`);
55
  }
 
59
  const canonicalUrl = video?.url || `https://www.youtube.com/watch?v=${videoId}`;
60
  const shouldHideLogo = ['instagram', 'facebook', 'tiktok'].includes(platform);
61
 
62
+ // Show "Add to Workbench" only if it's the owner (or a logged-in user viewing a demo record)
63
+ // and NOT already in workbench (preview mode)
64
+ const showAddButton = isAuthenticated && (isOwner || isDemoRecord) && video.isInWorkbench === false;
65
+
66
  return (
67
  <Suspense fallback={<VaultGridSkeleton />}>
68
  <div className="min-h-screen bg-background pb-20 overflow-x-hidden">
69
  {/* Demo Banner */}
70
  <div className="bg-primary/10 border-b border-primary/20 backdrop-blur-md sticky top-0 z-50">
71
+ <div className="container mx-auto px-4 py-3 flex items-center justify-between gap-2">
72
+ {showAddButton ? (
73
+ <div className="flex items-center gap-2">
74
+ <Badge variant="secondary" className="bg-amber-500/10 text-amber-600 border-amber-500/20 text-[10px] uppercase font-bold tracking-wider">
75
+ Preview Mode
76
+ </Badge>
77
+ <span className="text-xs text-muted-foreground hidden sm:inline">Add to workbench to save permanently</span>
78
+ </div>
79
+ ) : <div />}
80
+
81
+ <div className="flex items-center gap-2">
82
+ {showAddButton && (
83
+ <AddToWorkbenchButton videoId={videoId} />
84
+ )}
85
+ <Button asChild size="sm" variant="ghost" className="rounded-full text-xs gap-1.5 text-muted-foreground hover:text-foreground">
86
+ <Link href="/dashboard">
87
+ <LayoutDashboard className="h-3.5 w-3.5" />
88
+ Dashboard
89
+ </Link>
90
+ </Button>
91
+ <Button asChild size="sm" className="rounded-full shadow-lg shadow-primary/20">
92
+ <Link href={`/vault/${vault.channel.creatorSlug}/video/${videoId}`}>View Public Page</Link>
93
+ </Button>
94
+ </div>
95
  </div>
96
  </div>
97
 
src/features/dashboard/actions/fetch-videos.ts CHANGED
@@ -52,7 +52,8 @@ export async function fetchVideos({
52
  // Base Conditions
53
  const whereConditions: SQL[] = [
54
  eq(youtubeChannels.creatorId, userId),
55
- ne(youtubeVideos.availabilityStatus, 'private') // Ensure private videos are hidden
 
56
  ];
57
 
58
  if (filterPlatform !== "all") {
@@ -78,6 +79,9 @@ export async function fetchVideos({
78
  default: orderByClause = direction(youtubeVideos.publishedAt);
79
  }
80
 
 
 
 
81
  // Having Logic for Tab filtering
82
  const havingConditions: SQL[] = [];
83
  if (tab === "active") {
@@ -124,7 +128,7 @@ export async function fetchVideos({
124
  .where(and(...whereConditions))
125
  .groupBy(youtubeVideos.id, youtubeChannels.id)
126
  .having(havingConditions.length > 0 ? and(...havingConditions) : undefined)
127
- .orderBy(orderByClause)
128
  .limit(limit + 1) // Fetch one extra to check for next page
129
  .offset(offset);
130
 
 
52
  // Base Conditions
53
  const whereConditions: SQL[] = [
54
  eq(youtubeChannels.creatorId, userId),
55
+ ne(youtubeVideos.availabilityStatus, 'private'), // Ensure private videos are hidden
56
+ eq(youtubeVideos.isInWorkbench, true) // Only show videos explicitly added to workbench
57
  ];
58
 
59
  if (filterPlatform !== "all") {
 
79
  default: orderByClause = direction(youtubeVideos.publishedAt);
80
  }
81
 
82
+ // Always use createdAt as secondary sort to ensure stable and recent sorting for same-day videos
83
+ const finalOrderBy = [orderByClause, desc(youtubeVideos.createdAt)];
84
+
85
  // Having Logic for Tab filtering
86
  const havingConditions: SQL[] = [];
87
  if (tab === "active") {
 
128
  .where(and(...whereConditions))
129
  .groupBy(youtubeVideos.id, youtubeChannels.id)
130
  .having(havingConditions.length > 0 ? and(...havingConditions) : undefined)
131
+ .orderBy(...finalOrderBy)
132
  .limit(limit + 1) // Fetch one extra to check for next page
133
  .offset(offset);
134
 
src/features/vault/actions/add-to-workbench.ts ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use server";
2
+
3
+ import { auth } from "@/lib/auth";
4
+ import { headers } from "next/headers";
5
+ import { db } from "@/lib/db";
6
+ import { youtubeVideos, youtubeChannels } from "@/lib/db/schema";
7
+ import { eq, and } from "drizzle-orm";
8
+ import { revalidatePath } from "next/cache";
9
+
10
+ export async function addToWorkbenchAction(videoId: string) {
11
+ const session = await auth.api.getSession({
12
+ headers: await headers()
13
+ });
14
+
15
+ if (!session?.user) {
16
+ return { success: false, error: "Unauthorized" };
17
+ }
18
+
19
+ const userId = session.user.id;
20
+
21
+ try {
22
+ // 1. Verify ownership (video must belong to a channel owned by the user)
23
+ // We find the video and join with channel to check creatorId
24
+ const video = await db
25
+ .select({
26
+ id: youtubeVideos.id,
27
+ channelId: youtubeVideos.channelId,
28
+ scanStatus: youtubeVideos.scanStatus
29
+ })
30
+ .from(youtubeVideos)
31
+ .innerJoin(youtubeChannels, eq(youtubeVideos.channelId, youtubeChannels.id))
32
+ .where(and(
33
+ eq(youtubeVideos.videoId, videoId),
34
+ eq(youtubeChannels.creatorId, userId)
35
+ ))
36
+ .limit(1);
37
+
38
+ if (!video || video.length === 0) {
39
+ return { success: false, error: "Video not found or you do not own it." };
40
+ }
41
+
42
+ // 2. Update status
43
+ await db.update(youtubeVideos)
44
+ .set({
45
+ isInWorkbench: true,
46
+ updatedAt: new Date()
47
+ })
48
+ .where(eq(youtubeVideos.id, video[0].id));
49
+
50
+ console.log(`[addToWorkbench] User ${userId} added video ${videoId} to workbench.`);
51
+
52
+ // 3. Revalidate dashboard and showcase
53
+ revalidatePath("/dashboard");
54
+ revalidatePath(`/showcase/${videoId}`);
55
+
56
+ return { success: true };
57
+ } catch (error) {
58
+ console.error("[addToWorkbench] Error:", error);
59
+ return { success: false, error: "Failed to update video status." };
60
+ }
61
+ }
src/features/vault/actions/quick-analyze.ts CHANGED
@@ -51,7 +51,8 @@ export async function quickAddVideoAction(formData: FormData) {
51
  const result = await ShowcaseService.ingestAndAnalyze(videoId, userId, {
52
  skipAnalysis: true,
53
  platform: platform,
54
- url: url
 
55
  });
56
 
57
  if (result.success) {
 
51
  const result = await ShowcaseService.ingestAndAnalyze(videoId, userId, {
52
  skipAnalysis: true,
53
  platform: platform,
54
+ url: url,
55
+ isInWorkbench: false // Preview mode: Don't add to dashboard yet
56
  });
57
 
58
  if (result.success) {
src/features/vault/components/add-to-workbench-button.tsx ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { Button } from "@/components/ui/button";
5
+ import { Loader2, PlusCircle, CheckCircle } from "lucide-react";
6
+ import { addToWorkbenchAction } from "../actions/add-to-workbench";
7
+ import { toast } from "sonner";
8
+ import { useRouter } from "next/navigation";
9
+
10
+ interface AddToWorkbenchButtonProps {
11
+ videoId: string;
12
+ }
13
+
14
+ export function AddToWorkbenchButton({ videoId }: AddToWorkbenchButtonProps) {
15
+ const [loading, setLoading] = useState(false);
16
+ const [success, setSuccess] = useState(false);
17
+ const router = useRouter();
18
+
19
+ const handleAdd = async () => {
20
+ setLoading(true);
21
+ try {
22
+ const result = await addToWorkbenchAction(videoId);
23
+ if (result.success) {
24
+ setSuccess(true);
25
+ toast.success("Added to Workbench", {
26
+ description: "This video is now tracked in your dashboard.",
27
+ });
28
+ router.refresh();
29
+ } else {
30
+ toast.error("Error", {
31
+ description: result.error || "Failed to add video.",
32
+ });
33
+ }
34
+ } catch (error) {
35
+ toast.error("Error", {
36
+ description: "Something went wrong.",
37
+ });
38
+ } finally {
39
+ setLoading(false);
40
+ }
41
+ };
42
+
43
+ if (success) {
44
+ return (
45
+ <Button variant="outline" size="sm" className="gap-2 text-green-600 border-green-200 bg-green-50 pointer-events-none">
46
+ <CheckCircle className="h-4 w-4" />
47
+ Added to Workbench
48
+ </Button>
49
+ );
50
+ }
51
+
52
+ return (
53
+ <Button
54
+ onClick={handleAdd}
55
+ disabled={loading}
56
+ size="sm"
57
+ className="gap-2 bg-emerald-600 hover:bg-emerald-700 text-white shadow-md shadow-emerald-900/10"
58
+ >
59
+ {loading ? (
60
+ <Loader2 className="h-4 w-4 animate-spin" />
61
+ ) : (
62
+ <PlusCircle className="h-4 w-4" />
63
+ )}
64
+ Add to Workbench
65
+ </Button>
66
+ );
67
+ }
src/features/vault/components/video-embed.tsx CHANGED
@@ -64,13 +64,15 @@ export function VideoEmbed({
64
  }
65
 
66
  if (platform === 'tiktok' && url) {
 
 
67
  // TikTok embed: https://www.tiktok.com/embed/v2/{videoId}
68
  return (
69
  <div className={`relative group w-full ${aspectRatioClass} rounded-2xl overflow-hidden shadow-2xl border border-white/10 glass bg-black/20 mb-8`} style={customStyle}>
70
  <iframe
71
  width="100%"
72
  height="100%"
73
- src={`https://www.tiktok.com/embed/v2/${videoId}`}
74
  title={title || "TikTok video player"}
75
  frameBorder="0"
76
  allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
@@ -82,13 +84,21 @@ export function VideoEmbed({
82
  }
83
 
84
  if (platform === 'instagram' && url) {
85
- // Instagram embed: https://www.instagram.com/p/${videoId}/embed
 
 
 
 
 
 
 
 
86
  return (
87
  <div className={`relative group w-full ${aspectRatioClass} rounded-2xl overflow-hidden shadow-2xl border border-white/10 glass bg-black/20 mb-8`} style={customStyle}>
88
  <iframe
89
  width="100%"
90
  height="100%"
91
- src={`https://www.instagram.com/p/${videoId}/embed`}
92
  title={title || "Instagram post"}
93
  frameBorder="0"
94
  allowFullScreen
 
64
  }
65
 
66
  if (platform === 'tiktok' && url) {
67
+ // Strip internal 'tt-' or 'tiktok-' prefix if present
68
+ const cleanId = videoId.replace(/^(?:tt|tiktok)-/, '');
69
  // TikTok embed: https://www.tiktok.com/embed/v2/{videoId}
70
  return (
71
  <div className={`relative group w-full ${aspectRatioClass} rounded-2xl overflow-hidden shadow-2xl border border-white/10 glass bg-black/20 mb-8`} style={customStyle}>
72
  <iframe
73
  width="100%"
74
  height="100%"
75
+ src={`https://www.tiktok.com/embed/v2/${cleanId}`}
76
  title={title || "TikTok video player"}
77
  frameBorder="0"
78
  allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
 
84
  }
85
 
86
  if (platform === 'instagram' && url) {
87
+ // Strip internal 'ig-' or 'instagram-' prefix if present
88
+ const cleanId = videoId.replace(/^(?:ig|instagram)-/, '');
89
+
90
+ // Determine endpoint based on URL (Reels need /reel/ endpoint sometimes, though /p/ usually works)
91
+ // Adding /captioned often makes it more robust against "Post removed" errors for embeds
92
+ const isReel = url.includes('/reel') || url.includes('/reels');
93
+ const endpoint = isReel ? 'reel' : 'p';
94
+
95
+ // Instagram embed: https://www.instagram.com/{p|reel}/${videoId}/embed/captioned
96
  return (
97
  <div className={`relative group w-full ${aspectRatioClass} rounded-2xl overflow-hidden shadow-2xl border border-white/10 glass bg-black/20 mb-8`} style={customStyle}>
98
  <iframe
99
  width="100%"
100
  height="100%"
101
+ src={`https://www.instagram.com/${endpoint}/${cleanId}/embed/captioned`}
102
  title={title || "Instagram post"}
103
  frameBorder="0"
104
  allowFullScreen
src/features/vault/services/showcase.service.ts CHANGED
@@ -114,7 +114,9 @@ export class ShowcaseService {
114
  // Self-healing: If subscriberCount is missing or 0, trigger a background refresh
115
  if (!channel.subscriberCount && !SHOWCASE_CONFIG[videoId]) {
116
  // We don't await this to keep the response fast, but it will fix it for the next view
117
- this.ingestAndAnalyze(videoId, null, {
 
 
118
  platform: video.platform || 'youtube',
119
  url: video.url || undefined
120
  }).catch(console.error);
@@ -188,7 +190,7 @@ export class ShowcaseService {
188
  if (isStuck && transformedDetections.length === 0) {
189
  console.log(`Self-healing: Re-triggering stuck analysis for video ${videoId}`);
190
  // Don't await, let it run in background. The polling UI will pick up the status change.
191
- this.ingestAndAnalyze(videoId, null, {
192
  platform: video.platform || 'youtube',
193
  url: video.url || undefined
194
  }).catch(console.error);
@@ -264,7 +266,8 @@ export class ShowcaseService {
264
  platform: video.platform as any || 'youtube',
265
  url: video.url || (video.platform === 'facebook' ? `https://www.facebook.com/watch?v=${video.videoId}` : video.platform === 'tiktok' ? `https://www.tiktok.com/video/${video.videoId}` : video.platform === 'instagram' ? `https://www.instagram.com/reels/${video.videoId}` : `https://www.youtube.com/watch?v=${video.videoId}`),
266
  width: video.width,
267
- height: video.height
 
268
  }],
269
  categories: Array.from(
270
  transformedDetections.reduce((acc, d) => {
@@ -303,7 +306,8 @@ export class ShowcaseService {
303
  platform: (video.platform as any) || 'youtube',
304
  url: video.url || (video.platform === 'facebook' ? `https://www.facebook.com/watch?v=${video.videoId}` : video.platform === 'tiktok' ? `https://www.tiktok.com/video/${video.videoId}` : video.platform === 'instagram' ? `https://www.instagram.com/reels/${video.videoId}` : `https://www.youtube.com/watch?v=${video.videoId}`),
305
  width: video.width,
306
- height: video.height
 
307
  }],
308
  categories: []
309
  },
@@ -313,8 +317,10 @@ export class ShowcaseService {
313
  }
314
 
315
  // 3. Fallback: Fetch public metadata via OEmbed if not in DB at all
 
 
316
  return {
317
- ...(await this.getOEmbedData(videoId)),
318
  isProcessing: false // It's not in DB, so it's "not started" (or we should trigger it)
319
  };
320
  }
@@ -412,7 +418,7 @@ export class ShowcaseService {
412
  static async ingestAndAnalyze(
413
  videoId: string,
414
  userId?: string | null,
415
- options: { skipAnalysis?: boolean; platform?: string; url?: string } = {}
416
  ): Promise<{ success: boolean; error?: string }> {
417
  // Improved platform detection
418
  let platform = options.platform;
@@ -579,37 +585,33 @@ export class ShowcaseService {
579
  }
580
  }
581
 
582
- // 3. Check if video already exists FOR THIS USER (Multi-Tenancy)
583
- // If userId is provided, we check for a video owned by them.
584
- // If not (Showcase mode), we look for a video owned by DEMO_USER_ID or any user (Showcase is shared until claimed).
585
- // Actually, for multi-user, we want to allow many users to have the same videoId.
586
-
587
  const effectiveUserId = userId || DEMO_USER_ID;
588
 
589
- const userChannels = await db.query.youtubeChannels.findMany({
590
- where: eq(youtubeChannels.creatorId, effectiveUserId)
 
591
  });
592
- const userChannelIds = userChannels.map(c => c.id);
593
 
594
- const existing = userChannelIds.length > 0
595
  ? await db.query.youtubeVideos.findFirst({
596
  where: and(
597
  eq(youtubeVideos.videoId, videoId),
598
  eq(youtubeVideos.platform, platform as any),
599
- inArray(youtubeVideos.channelId, userChannelIds)
600
  ),
601
  })
602
  : null;
603
 
604
  // [STORY: PREVENT DUPLICATES]
605
- // If the user already has this video, return it immediately.
606
- // Do NOT proceed to channel creation logic, as that risks creating shadow channels.
607
  if (existing) {
608
- console.log(`[ingestAndAnalyze] Video ${videoId} already exists for user ${effectiveUserId} with ID ${existing.id}. Returning existing record.`);
609
-
610
- // Optional: Trigger a lightweight refresh if it's old?
611
- // For now, we just return it to be fast and safe.
612
- return { success: true };
 
613
  }
614
 
615
  // 4. Find or Create/Update the appropriate Shadow Channel for this author
@@ -671,6 +673,7 @@ export class ShowcaseService {
671
  // If we didn't find the channel by exact ID (shadow-handle), try to find it by NAME/SLUG
672
  // within the user's existing channels. This prevents "The Hindu" vs "The Hindu Official" splits.
673
  if (!channel) {
 
674
  const existingChannelByName = userChannels.find(c =>
675
  c.channelName.toLowerCase() === data.author_name.toLowerCase() ||
676
  c.creatorSlug === expectedSlug
@@ -756,7 +759,8 @@ export class ShowcaseService {
756
  };
757
 
758
  const viewCount = santizeInt(data.view_count);
759
- const publishedAt = sanitizeDate(data.published_at);
 
760
  const width = santizeInt(data.width);
761
  const height = santizeInt(data.height);
762
 
@@ -775,7 +779,8 @@ export class ShowcaseService {
775
  platform: platform as any,
776
  url: canonicalUrl,
777
  width,
778
- height
 
779
  }).onConflictDoUpdate({
780
  target: [youtubeVideos.channelId, youtubeVideos.videoId],
781
  set: {
@@ -786,6 +791,9 @@ export class ShowcaseService {
786
  publishedAt: sql`COALESCE(excluded.published_at, youtube_videos.published_at)`,
787
  url: sql`COALESCE(excluded.url, youtube_videos.url)`,
788
  platform: sql`excluded.platform`,
 
 
 
789
  updatedAt: new Date()
790
  }
791
  }).returning();
@@ -847,7 +855,14 @@ export class ShowcaseService {
847
  private static async getOEmbedData(videoId: string, platform: string = 'youtube', url?: string) {
848
  try {
849
  if (platform !== 'youtube') {
850
- const targetUrl = url || (platform === 'facebook' ? `https://www.facebook.com/watch?v=${videoId.replace('facebook-', '')}` : '');
 
 
 
 
 
 
 
851
  if (!targetUrl) throw new Error("URL required for non-YouTube OEmbed lookup");
852
 
853
  const socialMetadata = await SocialIngestService.fetchMetadata(targetUrl);
@@ -874,7 +889,8 @@ export class ShowcaseService {
874
  platform: platform as any,
875
  url: targetUrl,
876
  width: socialMetadata.width || null,
877
- height: socialMetadata.height || null
 
878
  }],
879
  categories: []
880
  },
@@ -934,7 +950,8 @@ export class ShowcaseService {
934
  platform: 'youtube' as const,
935
  url: `https://www.youtube.com/watch?v=${videoId}`,
936
  width: data.width || null,
937
- height: data.height || null
 
938
  }],
939
  categories: []
940
  },
@@ -970,7 +987,8 @@ export class ShowcaseService {
970
  platform: platform as any,
971
  url: targetUrl,
972
  width: null,
973
- height: null
 
974
  }
975
  ],
976
  categories: []
 
114
  // Self-healing: If subscriberCount is missing or 0, trigger a background refresh
115
  if (!channel.subscriberCount && !SHOWCASE_CONFIG[videoId]) {
116
  // We don't await this to keep the response fast, but it will fix it for the next view
117
+ // CRITICAL FIX: Pass the actual channel.creatorId to prevent creating a duplicate
118
+ // "shadow" channel for the demo user if this video belongs to a real user.
119
+ this.ingestAndAnalyze(videoId, channel.creatorId, {
120
  platform: video.platform || 'youtube',
121
  url: video.url || undefined
122
  }).catch(console.error);
 
190
  if (isStuck && transformedDetections.length === 0) {
191
  console.log(`Self-healing: Re-triggering stuck analysis for video ${videoId}`);
192
  // Don't await, let it run in background. The polling UI will pick up the status change.
193
+ this.ingestAndAnalyze(videoId, channel.creatorId, {
194
  platform: video.platform || 'youtube',
195
  url: video.url || undefined
196
  }).catch(console.error);
 
266
  platform: video.platform as any || 'youtube',
267
  url: video.url || (video.platform === 'facebook' ? `https://www.facebook.com/watch?v=${video.videoId}` : video.platform === 'tiktok' ? `https://www.tiktok.com/video/${video.videoId}` : video.platform === 'instagram' ? `https://www.instagram.com/reels/${video.videoId}` : `https://www.youtube.com/watch?v=${video.videoId}`),
268
  width: video.width,
269
+ height: video.height,
270
+ isInWorkbench: video.isInWorkbench ?? false
271
  }],
272
  categories: Array.from(
273
  transformedDetections.reduce((acc, d) => {
 
306
  platform: (video.platform as any) || 'youtube',
307
  url: video.url || (video.platform === 'facebook' ? `https://www.facebook.com/watch?v=${video.videoId}` : video.platform === 'tiktok' ? `https://www.tiktok.com/video/${video.videoId}` : video.platform === 'instagram' ? `https://www.instagram.com/reels/${video.videoId}` : `https://www.youtube.com/watch?v=${video.videoId}`),
308
  width: video.width,
309
+ height: video.height,
310
+ isInWorkbench: video.isInWorkbench ?? false
311
  }],
312
  categories: []
313
  },
 
317
  }
318
 
319
  // 3. Fallback: Fetch public metadata via OEmbed if not in DB at all
320
+ const platform = videoId.startsWith('ig-') ? 'instagram' : videoId.startsWith('fb-') ? 'facebook' : videoId.startsWith('tt-') ? 'tiktok' : 'youtube';
321
+
322
  return {
323
+ ...(await this.getOEmbedData(videoId, platform)),
324
  isProcessing: false // It's not in DB, so it's "not started" (or we should trigger it)
325
  };
326
  }
 
418
  static async ingestAndAnalyze(
419
  videoId: string,
420
  userId?: string | null,
421
+ options: { skipAnalysis?: boolean; platform?: string; url?: string; isInWorkbench?: boolean } = {}
422
  ): Promise<{ success: boolean; error?: string }> {
423
  // Improved platform detection
424
  let platform = options.platform;
 
585
  }
586
  }
587
 
 
 
 
 
 
588
  const effectiveUserId = userId || DEMO_USER_ID;
589
 
590
+ // 3. Find existing video (either already owned by user or a demo record to claim)
591
+ const allUserChannels = await db.query.youtubeChannels.findMany({
592
+ where: inArray(youtubeChannels.creatorId, [effectiveUserId, DEMO_USER_ID])
593
  });
594
+ const allChannelIds = allUserChannels.map(c => c.id);
595
 
596
+ const existing = allChannelIds.length > 0
597
  ? await db.query.youtubeVideos.findFirst({
598
  where: and(
599
  eq(youtubeVideos.videoId, videoId),
600
  eq(youtubeVideos.platform, platform as any),
601
+ inArray(youtubeVideos.channelId, allChannelIds)
602
  ),
603
  })
604
  : null;
605
 
606
  // [STORY: PREVENT DUPLICATES]
607
+ // If the user already has this video AND it's not a demo record, return immediately.
 
608
  if (existing) {
609
+ const videoChannel = allUserChannels.find(c => c.id === existing.channelId);
610
+ if (videoChannel && videoChannel.creatorId === userId) {
611
+ console.log(`[ingestAndAnalyze] Video ${videoId} already exists and is owned by user ${userId}. Returning existing record.`);
612
+ return { success: true };
613
+ }
614
+ console.log(`[ingestAndAnalyze] Video ${videoId} exists as a demo record (owned by ${videoChannel?.creatorId}). Proceeding to claim.`);
615
  }
616
 
617
  // 4. Find or Create/Update the appropriate Shadow Channel for this author
 
673
  // If we didn't find the channel by exact ID (shadow-handle), try to find it by NAME/SLUG
674
  // within the user's existing channels. This prevents "The Hindu" vs "The Hindu Official" splits.
675
  if (!channel) {
676
+ const userChannels = allUserChannels.filter(c => c.creatorId === effectiveUserId);
677
  const existingChannelByName = userChannels.find(c =>
678
  c.channelName.toLowerCase() === data.author_name.toLowerCase() ||
679
  c.creatorSlug === expectedSlug
 
759
  };
760
 
761
  const viewCount = santizeInt(data.view_count);
762
+ // Fallback to current date if platform doesn't provide one (prevents burial in workbench)
763
+ const publishedAt = sanitizeDate(data.published_at) || new Date();
764
  const width = santizeInt(data.width);
765
  const height = santizeInt(data.height);
766
 
 
779
  platform: platform as any,
780
  url: canonicalUrl,
781
  width,
782
+ height,
783
+ isInWorkbench: options.isInWorkbench ?? true // Default to true if not specified
784
  }).onConflictDoUpdate({
785
  target: [youtubeVideos.channelId, youtubeVideos.videoId],
786
  set: {
 
791
  publishedAt: sql`COALESCE(excluded.published_at, youtube_videos.published_at)`,
792
  url: sql`COALESCE(excluded.url, youtube_videos.url)`,
793
  platform: sql`excluded.platform`,
794
+ // Only update isInWorkbench if it's explicitly passed as true (e.g. "Add to Workbench" action)
795
+ // If false (preview), we don't want to accidentally hide an existing tracked video
796
+ isInWorkbench: options.isInWorkbench === true ? true : sql`youtube_videos.is_in_workbench`,
797
  updatedAt: new Date()
798
  }
799
  }).returning();
 
855
  private static async getOEmbedData(videoId: string, platform: string = 'youtube', url?: string) {
856
  try {
857
  if (platform !== 'youtube') {
858
+ let targetUrl = url;
859
+ if (!targetUrl) {
860
+ const cleanId = videoId.replace(/^(?:ig|instagram|fb|facebook|tt|tiktok)-/, '');
861
+ if (platform === 'facebook') targetUrl = `https://www.facebook.com/watch?v=${cleanId}`;
862
+ if (platform === 'instagram') targetUrl = `https://www.instagram.com/reels/${cleanId}`;
863
+ if (platform === 'tiktok') targetUrl = `https://www.tiktok.com/video/${cleanId}`;
864
+ }
865
+
866
  if (!targetUrl) throw new Error("URL required for non-YouTube OEmbed lookup");
867
 
868
  const socialMetadata = await SocialIngestService.fetchMetadata(targetUrl);
 
889
  platform: platform as any,
890
  url: targetUrl,
891
  width: socialMetadata.width || null,
892
+ height: socialMetadata.height || null,
893
+ isInWorkbench: false
894
  }],
895
  categories: []
896
  },
 
950
  platform: 'youtube' as const,
951
  url: `https://www.youtube.com/watch?v=${videoId}`,
952
  width: data.width || null,
953
+ height: data.height || null,
954
+ isInWorkbench: false
955
  }],
956
  categories: []
957
  },
 
987
  platform: platform as any,
988
  url: targetUrl,
989
  width: null,
990
+ height: null,
991
+ isInWorkbench: false
992
  }
993
  ],
994
  categories: []
src/features/vault/types/vault.types.ts CHANGED
@@ -19,6 +19,7 @@ export interface VaultVideo {
19
  url: string | null;
20
  width: number | null;
21
  height: number | null;
 
22
  }
23
 
24
  export interface CreatorVault {
 
19
  url: string | null;
20
  width: number | null;
21
  height: number | null;
22
+ isInWorkbench?: boolean | null;
23
  }
24
 
25
  export interface CreatorVault {
src/lib/db/schema.ts CHANGED
@@ -165,6 +165,7 @@ export const youtubeVideos = pgTable('youtube_videos', {
165
  url: text('url'), // Canonical URL for TikTok/Insta
166
  width: integer('width'),
167
  height: integer('height'),
 
168
  ...timestamps,
169
  }, (table) => ({
170
  channelVideoIdx: uniqueIndex('channel_video_idx').on(table.channelId, table.videoId),
 
165
  url: text('url'), // Canonical URL for TikTok/Insta
166
  width: integer('width'),
167
  height: integer('height'),
168
+ isInWorkbench: boolean('is_in_workbench').default(true).notNull(),
169
  ...timestamps,
170
  }, (table) => ({
171
  channelVideoIdx: uniqueIndex('channel_video_idx').on(table.channelId, table.videoId),