clhq-dynamodb-module 1.1.0-alpha.90

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.
Files changed (130) hide show
  1. package/dist/app.module.d.ts +2 -0
  2. package/dist/app.module.js +88 -0
  3. package/dist/common/enums/index.d.ts +1 -0
  4. package/dist/common/enums/index.js +17 -0
  5. package/dist/common/enums/project-enums.d.ts +13 -0
  6. package/dist/common/enums/project-enums.js +18 -0
  7. package/dist/common/user.controller.d.ts +12 -0
  8. package/dist/common/user.controller.js +102 -0
  9. package/dist/dbservice/dynamodb-base.service.d.ts +23 -0
  10. package/dist/dbservice/dynamodb-base.service.js +105 -0
  11. package/dist/dbservice/index.d.ts +4 -0
  12. package/dist/dbservice/index.js +20 -0
  13. package/dist/dbservice/projects.service.d.ts +5 -0
  14. package/dist/dbservice/projects.service.js +25 -0
  15. package/dist/dbservice/users.service.d.ts +5 -0
  16. package/dist/dbservice/users.service.js +25 -0
  17. package/dist/dbservice/workspaces.service.d.ts +5 -0
  18. package/dist/dbservice/workspaces.service.js +25 -0
  19. package/dist/entities/ai-usage.entity.d.ts +16 -0
  20. package/dist/entities/ai-usage.entity.js +71 -0
  21. package/dist/entities/asset.entity.d.ts +59 -0
  22. package/dist/entities/asset.entity.js +185 -0
  23. package/dist/entities/auth.entity.d.ts +10 -0
  24. package/dist/entities/auth.entity.js +9 -0
  25. package/dist/entities/clip-effect.entity.d.ts +9 -0
  26. package/dist/entities/clip-effect.entity.js +53 -0
  27. package/dist/entities/clip.entity.d.ts +44 -0
  28. package/dist/entities/clip.entity.js +114 -0
  29. package/dist/entities/comment.entity.d.ts +18 -0
  30. package/dist/entities/comment.entity.js +83 -0
  31. package/dist/entities/common.entity.d.ts +21 -0
  32. package/dist/entities/common.entity.js +27 -0
  33. package/dist/entities/credit-transaction.entity.d.ts +15 -0
  34. package/dist/entities/credit-transaction.entity.js +66 -0
  35. package/dist/entities/editor-project.entity.d.ts +48 -0
  36. package/dist/entities/editor-project.entity.js +226 -0
  37. package/dist/entities/effect.entity.d.ts +22 -0
  38. package/dist/entities/effect.entity.js +87 -0
  39. package/dist/entities/index.d.ts +28 -0
  40. package/dist/entities/index.js +44 -0
  41. package/dist/entities/invite.entity.d.ts +17 -0
  42. package/dist/entities/invite.entity.js +73 -0
  43. package/dist/entities/keyframe.entity.d.ts +18 -0
  44. package/dist/entities/keyframe.entity.js +73 -0
  45. package/dist/entities/payment-transaction.entity.d.ts +40 -0
  46. package/dist/entities/payment-transaction.entity.js +138 -0
  47. package/dist/entities/payment.entity.d.ts +24 -0
  48. package/dist/entities/payment.entity.js +89 -0
  49. package/dist/entities/plan.entity.d.ts +13 -0
  50. package/dist/entities/plan.entity.js +77 -0
  51. package/dist/entities/project-version.entity.d.ts +11 -0
  52. package/dist/entities/project-version.entity.js +66 -0
  53. package/dist/entities/project.entity.d.ts +48 -0
  54. package/dist/entities/project.entity.js +175 -0
  55. package/dist/entities/reward-rule.entity.d.ts +15 -0
  56. package/dist/entities/reward-rule.entity.js +67 -0
  57. package/dist/entities/stripe-webhook-event.entity.d.ts +9 -0
  58. package/dist/entities/stripe-webhook-event.entity.js +54 -0
  59. package/dist/entities/subscription-plan.entity.d.ts +37 -0
  60. package/dist/entities/subscription-plan.entity.js +197 -0
  61. package/dist/entities/subscription-usage.entity.d.ts +15 -0
  62. package/dist/entities/subscription-usage.entity.js +83 -0
  63. package/dist/entities/timeline.entity.d.ts +8 -0
  64. package/dist/entities/timeline.entity.js +48 -0
  65. package/dist/entities/track.entity.d.ts +20 -0
  66. package/dist/entities/track.entity.js +81 -0
  67. package/dist/entities/user-onboarding-response.entity.d.ts +8 -0
  68. package/dist/entities/user-onboarding-response.entity.js +48 -0
  69. package/dist/entities/user-profile.entity.d.ts +51 -0
  70. package/dist/entities/user-profile.entity.js +200 -0
  71. package/dist/entities/user-subscription.entity.d.ts +44 -0
  72. package/dist/entities/user-subscription.entity.js +160 -0
  73. package/dist/entities/user.entity.d.ts +32 -0
  74. package/dist/entities/user.entity.js +133 -0
  75. package/dist/entities/workspace-member.entity.d.ts +8 -0
  76. package/dist/entities/workspace-member.entity.js +49 -0
  77. package/dist/entities/workspace.entity.d.ts +9 -0
  78. package/dist/entities/workspace.entity.js +54 -0
  79. package/dist/index.d.ts +4 -0
  80. package/dist/index.js +22 -0
  81. package/dist/main.d.ts +1 -0
  82. package/dist/main.js +10 -0
  83. package/dist/repositories/asset.repository.d.ts +15 -0
  84. package/dist/repositories/asset.repository.js +101 -0
  85. package/dist/repositories/base.repository.d.ts +31 -0
  86. package/dist/repositories/base.repository.js +172 -0
  87. package/dist/repositories/clip-effect.repository.d.ts +9 -0
  88. package/dist/repositories/clip-effect.repository.js +49 -0
  89. package/dist/repositories/clip.repository.d.ts +13 -0
  90. package/dist/repositories/clip.repository.js +101 -0
  91. package/dist/repositories/comment.repository.d.ts +12 -0
  92. package/dist/repositories/comment.repository.js +92 -0
  93. package/dist/repositories/editor-project.repository.d.ts +26 -0
  94. package/dist/repositories/editor-project.repository.js +107 -0
  95. package/dist/repositories/effect.repository.d.ts +10 -0
  96. package/dist/repositories/effect.repository.js +59 -0
  97. package/dist/repositories/index.d.ts +21 -0
  98. package/dist/repositories/index.js +37 -0
  99. package/dist/repositories/keyframe.repository.d.ts +9 -0
  100. package/dist/repositories/keyframe.repository.js +49 -0
  101. package/dist/repositories/payment-transaction.repository.d.ts +13 -0
  102. package/dist/repositories/payment-transaction.repository.js +123 -0
  103. package/dist/repositories/project-version.repository.d.ts +9 -0
  104. package/dist/repositories/project-version.repository.js +51 -0
  105. package/dist/repositories/project.repository.d.ts +17 -0
  106. package/dist/repositories/project.repository.js +132 -0
  107. package/dist/repositories/reward-rule.repository.d.ts +13 -0
  108. package/dist/repositories/reward-rule.repository.js +84 -0
  109. package/dist/repositories/subscription-plan.repository.d.ts +11 -0
  110. package/dist/repositories/subscription-plan.repository.js +49 -0
  111. package/dist/repositories/subscription-usage.repository.d.ts +12 -0
  112. package/dist/repositories/subscription-usage.repository.js +68 -0
  113. package/dist/repositories/timeline.repository.d.ts +9 -0
  114. package/dist/repositories/timeline.repository.js +53 -0
  115. package/dist/repositories/track.repository.d.ts +13 -0
  116. package/dist/repositories/track.repository.js +61 -0
  117. package/dist/repositories/user-onboarding-response.repository.d.ts +11 -0
  118. package/dist/repositories/user-onboarding-response.repository.js +58 -0
  119. package/dist/repositories/user-profile.repository.d.ts +18 -0
  120. package/dist/repositories/user-profile.repository.js +62 -0
  121. package/dist/repositories/user-subscription.repository.d.ts +13 -0
  122. package/dist/repositories/user-subscription.repository.js +52 -0
  123. package/dist/repositories/user.repository.d.ts +20 -0
  124. package/dist/repositories/user.repository.js +94 -0
  125. package/dist/repositories/workspace-member.repository.d.ts +10 -0
  126. package/dist/repositories/workspace-member.repository.js +37 -0
  127. package/dist/repositories/workspace.repository.d.ts +10 -0
  128. package/dist/repositories/workspace.repository.js +45 -0
  129. package/package.json +63 -0
  130. package/readme.entity.implementation.md +1560 -0
@@ -0,0 +1,1560 @@
1
+ ```
2
+ // ===== ENTITIES =====
3
+
4
+ // entities/workspace.entity.ts
5
+ import {
6
+ attribute,
7
+ table,
8
+ hashKey,
9
+ } from '@aws/dynamodb-data-mapper-annotations';
10
+ import { IsString, IsOptional, IsBoolean } from 'class-validator';
11
+
12
+ @table('workspaces')
13
+ export class Workspace {
14
+ @IsString()
15
+ @hashKey()
16
+ id: string;
17
+
18
+ @IsString()
19
+ @attribute()
20
+ name: string;
21
+
22
+ @IsString()
23
+ @IsOptional()
24
+ @attribute()
25
+ description?: string;
26
+
27
+ @IsString()
28
+ @attribute()
29
+ ownerId: string;
30
+
31
+ @IsBoolean()
32
+ @attribute()
33
+ isShared: boolean = false;
34
+
35
+ @IsString()
36
+ @IsOptional()
37
+ @attribute()
38
+ createdAt?: string;
39
+
40
+ @IsString()
41
+ @IsOptional()
42
+ @attribute()
43
+ updatedAt?: string;
44
+ }
45
+
46
+ // entities/project.entity.ts
47
+ import {
48
+ attribute,
49
+ table,
50
+ hashKey,
51
+ } from '@aws/dynamodb-data-mapper-annotations';
52
+ import { IsString, IsNumber, IsOptional, IsBoolean, IsEnum, IsArray } from 'class-validator';
53
+
54
+ export enum ProjectStatus {
55
+ DRAFT = 'draft',
56
+ IN_PROGRESS = 'in_progress',
57
+ RENDERING = 'rendering',
58
+ COMPLETED = 'completed',
59
+ FAILED = 'failed',
60
+ ARCHIVED = 'archived',
61
+ }
62
+
63
+ export enum ProjectVisibility {
64
+ PRIVATE = 'private',
65
+ WORKSPACE = 'workspace',
66
+ PUBLIC = 'public',
67
+ }
68
+
69
+ @table('projects')
70
+ export class Project {
71
+ @IsString()
72
+ @hashKey()
73
+ id: string;
74
+
75
+ @IsString()
76
+ @attribute()
77
+ workspaceId: string;
78
+
79
+ @IsString()
80
+ @attribute()
81
+ ownerId: string; // User who created the project
82
+
83
+ @IsString()
84
+ @attribute()
85
+ name: string;
86
+
87
+ @IsString()
88
+ @IsOptional()
89
+ @attribute()
90
+ description?: string;
91
+
92
+ @IsString()
93
+ @IsOptional()
94
+ @attribute()
95
+ thumbnailUrl?: string;
96
+
97
+ // Video specifications
98
+ @IsNumber()
99
+ @attribute()
100
+ durationMs: number = 0;
101
+
102
+ @IsNumber()
103
+ @attribute()
104
+ fps: number = 30;
105
+
106
+ @IsNumber()
107
+ @attribute()
108
+ resolutionWidth: number = 1920;
109
+
110
+ @IsNumber()
111
+ @attribute()
112
+ resolutionHeight: number = 1080;
113
+
114
+ @IsString()
115
+ @attribute()
116
+ backgroundColor: string = '#000000';
117
+
118
+ @IsString()
119
+ @attribute()
120
+ aspectRatio: string = '16:9'; // '16:9', '9:16', '1:1', '4:3'
121
+
122
+ // Project status and metadata
123
+ @IsEnum(ProjectStatus)
124
+ @attribute()
125
+ status: ProjectStatus = ProjectStatus.DRAFT;
126
+
127
+ @IsEnum(ProjectVisibility)
128
+ @attribute()
129
+ visibility: ProjectVisibility = ProjectVisibility.PRIVATE;
130
+
131
+ @IsNumber()
132
+ @attribute()
133
+ version: number = 1;
134
+
135
+ @IsBoolean()
136
+ @attribute()
137
+ isTemplate: boolean = false;
138
+
139
+ @IsBoolean()
140
+ @attribute()
141
+ isFavorite: boolean = false;
142
+
143
+ // Collaboration
144
+ @IsArray()
145
+ @IsOptional()
146
+ @attribute()
147
+ collaboratorIds?: string[];
148
+
149
+ @IsArray()
150
+ @IsOptional()
151
+ @attribute()
152
+ tags?: string[];
153
+
154
+ // Template specific
155
+ @IsString()
156
+ @IsOptional()
157
+ @attribute()
158
+ templateCategory?: string;
159
+
160
+ @IsBoolean()
161
+ @attribute()
162
+ isPremiumTemplate: boolean = false;
163
+
164
+ @IsNumber()
165
+ @attribute()
166
+ templateUsageCount: number = 0;
167
+
168
+ // Timestamps
169
+ @IsString()
170
+ @IsOptional()
171
+ @attribute()
172
+ lastOpenedAt?: string;
173
+
174
+ @IsString()
175
+ @IsOptional()
176
+ @attribute()
177
+ lastEditedBy?: string;
178
+
179
+ @IsString()
180
+ @IsOptional()
181
+ @attribute()
182
+ createdAt?: string;
183
+
184
+ @IsString()
185
+ @IsOptional()
186
+ @attribute()
187
+ updatedAt?: string;
188
+
189
+ // Project settings
190
+ @IsOptional()
191
+ @attribute()
192
+ settings?: {
193
+ autoSave?: boolean;
194
+ gridSnap?: boolean;
195
+ showTimecode?: boolean;
196
+ previewQuality?: 'low' | 'medium' | 'high';
197
+ audioLevels?: boolean;
198
+ };
199
+ }
200
+
201
+ // entities/asset.entity.ts
202
+ import {
203
+ attribute,
204
+ table,
205
+ hashKey,
206
+ } from '@aws/dynamodb-data-mapper-annotations';
207
+ import { IsString, IsNumber, IsOptional, IsBoolean, IsEnum, IsArray, IsObject } from 'class-validator';
208
+
209
+ export enum AssetType {
210
+ VIDEO = 'video',
211
+ AUDIO = 'audio',
212
+ IMAGE = 'image',
213
+ FONT = 'font',
214
+ SUBTITLE = 'subtitle',
215
+ TEMPLATE = 'template',
216
+ GRAPHIC = 'graphic',
217
+ }
218
+
219
+ export enum AssetStatus {
220
+ UPLOADING = 'uploading',
221
+ PROCESSING = 'processing',
222
+ READY = 'ready',
223
+ FAILED = 'failed',
224
+ DELETED = 'deleted',
225
+ }
226
+
227
+ @table('assets')
228
+ export class Asset {
229
+ @IsString()
230
+ @hashKey()
231
+ id: string;
232
+
233
+ @IsString()
234
+ @attribute()
235
+ workspaceId: string;
236
+
237
+ @IsString()
238
+ @attribute()
239
+ uploadedBy: string; // User ID
240
+
241
+ @IsString()
242
+ @IsOptional()
243
+ @attribute()
244
+ categoryId?: string;
245
+
246
+ @IsString()
247
+ @attribute()
248
+ name: string;
249
+
250
+ @IsString()
251
+ @IsOptional()
252
+ @attribute()
253
+ originalFilename?: string;
254
+
255
+ @IsEnum(AssetType)
256
+ @attribute()
257
+ fileType: AssetType;
258
+
259
+ @IsString()
260
+ @IsOptional()
261
+ @attribute()
262
+ mimeType?: string;
263
+
264
+ @IsNumber()
265
+ @attribute()
266
+ fileSize: number;
267
+
268
+ @IsString()
269
+ @attribute()
270
+ fileUrl: string;
271
+
272
+ @IsString()
273
+ @IsOptional()
274
+ @attribute()
275
+ thumbnailUrl?: string;
276
+
277
+ @IsString()
278
+ @IsOptional()
279
+ @attribute()
280
+ previewUrl?: string; // For video previews
281
+
282
+ // Media properties
283
+ @IsNumber()
284
+ @IsOptional()
285
+ @attribute()
286
+ durationMs?: number; // For video/audio
287
+
288
+ @IsNumber()
289
+ @IsOptional()
290
+ @attribute()
291
+ width?: number; // For image/video
292
+
293
+ @IsNumber()
294
+ @IsOptional()
295
+ @attribute()
296
+ height?: number; // For image/video
297
+
298
+ @IsNumber()
299
+ @IsOptional()
300
+ @attribute()
301
+ frameRate?: number; // For video
302
+
303
+ @IsString()
304
+ @IsOptional()
305
+ @attribute()
306
+ codec?: string; // For video/audio
307
+
308
+ @IsNumber()
309
+ @IsOptional()
310
+ @attribute()
311
+ bitrate?: number; // For video/audio
312
+
313
+ // Flexible metadata storage
314
+ @IsObject()
315
+ @IsOptional()
316
+ @attribute()
317
+ metadata?: {
318
+ colorSpace?: string;
319
+ hasAudio?: boolean;
320
+ audioChannels?: number;
321
+ audioSampleRate?: number;
322
+ videoCodec?: string;
323
+ audioCodec?: string;
324
+ creationTime?: string;
325
+ cameraModel?: string;
326
+ location?: {
327
+ latitude?: number;
328
+ longitude?: number;
329
+ };
330
+ exif?: Record<string, any>;
331
+ };
332
+
333
+ @IsEnum(AssetStatus)
334
+ @attribute()
335
+ uploadStatus: AssetStatus = AssetStatus.UPLOADING;
336
+
337
+ @IsArray()
338
+ @IsOptional()
339
+ @attribute()
340
+ tags?: string[];
341
+
342
+ @IsBoolean()
343
+ @attribute()
344
+ isPublic: boolean = false;
345
+
346
+ @IsBoolean()
347
+ @attribute()
348
+ isPremium: boolean = false;
349
+
350
+ // Usage tracking
351
+ @IsNumber()
352
+ @attribute()
353
+ usageCount: number = 0;
354
+
355
+ @IsString()
356
+ @IsOptional()
357
+ @attribute()
358
+ lastUsedAt?: string;
359
+
360
+ @IsString()
361
+ @IsOptional()
362
+ @attribute()
363
+ createdAt?: string;
364
+
365
+ @IsString()
366
+ @IsOptional()
367
+ @attribute()
368
+ updatedAt?: string;
369
+ }
370
+
371
+ // entities/timeline.entity.ts
372
+ import {
373
+ attribute,
374
+ table,
375
+ hashKey,
376
+ } from '@aws/dynamodb-data-mapper-annotations';
377
+ import { IsString, IsNumber, IsOptional } from 'class-validator';
378
+
379
+ @table('timelines')
380
+ export class Timeline {
381
+ @IsString()
382
+ @hashKey()
383
+ id: string;
384
+
385
+ @IsString()
386
+ @attribute()
387
+ projectId: string;
388
+
389
+ @IsString()
390
+ @attribute()
391
+ name: string = 'Main Timeline';
392
+
393
+ @IsNumber()
394
+ @attribute()
395
+ durationMs: number = 0;
396
+
397
+ @IsString()
398
+ @IsOptional()
399
+ @attribute()
400
+ createdAt?: string;
401
+
402
+ @IsString()
403
+ @IsOptional()
404
+ @attribute()
405
+ updatedAt?: string;
406
+ }
407
+
408
+ // entities/track.entity.ts
409
+ import {
410
+ attribute,
411
+ table,
412
+ hashKey,
413
+ } from '@aws/dynamodb-data-mapper-annotations';
414
+ import { IsString, IsNumber, IsOptional, IsBoolean, IsEnum } from 'class-validator';
415
+
416
+ export enum TrackType {
417
+ VIDEO = 'video',
418
+ AUDIO = 'audio',
419
+ SUBTITLE = 'subtitle',
420
+ OVERLAY = 'overlay',
421
+ TEXT = 'text',
422
+ }
423
+
424
+ @table('tracks')
425
+ export class Track {
426
+ @IsString()
427
+ @hashKey()
428
+ id: string;
429
+
430
+ @IsString()
431
+ @attribute()
432
+ timelineId: string;
433
+
434
+ @IsEnum(TrackType)
435
+ @attribute()
436
+ trackType: TrackType;
437
+
438
+ @IsString()
439
+ @IsOptional()
440
+ @attribute()
441
+ name?: string;
442
+
443
+ @IsNumber()
444
+ @attribute()
445
+ orderIndex: number;
446
+
447
+ @IsBoolean()
448
+ @attribute()
449
+ isLocked: boolean = false;
450
+
451
+ @IsBoolean()
452
+ @attribute()
453
+ isMuted: boolean = false;
454
+
455
+ @IsBoolean()
456
+ @attribute()
457
+ isVisible: boolean = true;
458
+
459
+ @IsNumber()
460
+ @attribute()
461
+ height: number = 60;
462
+
463
+ @IsString()
464
+ @attribute()
465
+ color: string = '#4A90E2';
466
+
467
+ @IsString()
468
+ @IsOptional()
469
+ @attribute()
470
+ createdAt?: string;
471
+ }
472
+
473
+ // entities/clip.entity.ts
474
+ import {
475
+ attribute,
476
+ table,
477
+ hashKey,
478
+ } from '@aws/dynamodb-data-mapper-annotations';
479
+ import { IsString, IsNumber, IsOptional, IsBoolean, IsObject } from 'class-validator';
480
+
481
+ @table('clips')
482
+ export class Clip {
483
+ @IsString()
484
+ @hashKey()
485
+ id: string;
486
+
487
+ @IsString()
488
+ @attribute()
489
+ trackId: string;
490
+
491
+ @IsString()
492
+ @IsOptional()
493
+ @attribute()
494
+ assetId?: string; // Can be null for generated clips (text, shapes, etc.)
495
+
496
+ @IsString()
497
+ @IsOptional()
498
+ @attribute()
499
+ name?: string;
500
+
501
+ // Timeline positioning
502
+ @IsNumber()
503
+ @attribute()
504
+ startTimeMs: number;
505
+
506
+ @IsNumber()
507
+ @attribute()
508
+ endTimeMs: number;
509
+
510
+ @IsNumber()
511
+ @attribute()
512
+ orderIndex: number;
513
+
514
+ // Asset trimming
515
+ @IsNumber()
516
+ @attribute()
517
+ assetStartTimeMs: number = 0; // Trim start
518
+
519
+ @IsNumber()
520
+ @IsOptional()
521
+ @attribute()
522
+ assetEndTimeMs?: number; // Trim end
523
+
524
+ // Audio/Video properties
525
+ @IsNumber()
526
+ @attribute()
527
+ volume: number = 1.0; // 0.0 to 2.0
528
+
529
+ @IsNumber()
530
+ @attribute()
531
+ opacity: number = 1.0; // 0.0 to 1.0
532
+
533
+ @IsNumber()
534
+ @attribute()
535
+ speed: number = 1.0; // 0.1 to 10.0
536
+
537
+ @IsBoolean()
538
+ @attribute()
539
+ isReversed: boolean = false;
540
+
541
+ // Visual transformations
542
+ @IsObject()
543
+ @IsOptional()
544
+ @attribute()
545
+ transform?: {
546
+ x?: number;
547
+ y?: number;
548
+ scaleX?: number;
549
+ scaleY?: number;
550
+ rotation?: number;
551
+ skewX?: number;
552
+ skewY?: number;
553
+ };
554
+
555
+ // Crop settings
556
+ @IsObject()
557
+ @IsOptional()
558
+ @attribute()
559
+ crop?: {
560
+ x?: number;
561
+ y?: number;
562
+ width?: number;
563
+ height?: number;
564
+ };
565
+
566
+ // Color correction
567
+ @IsObject()
568
+ @IsOptional()
569
+ @attribute()
570
+ colorCorrection?: {
571
+ brightness?: number;
572
+ contrast?: number;
573
+ saturation?: number;
574
+ hue?: number;
575
+ gamma?: number;
576
+ exposure?: number;
577
+ highlights?: number;
578
+ shadows?: number;
579
+ temperature?: number;
580
+ tint?: number;
581
+ };
582
+
583
+ @IsString()
584
+ @IsOptional()
585
+ @attribute()
586
+ createdAt?: string;
587
+
588
+ @IsString()
589
+ @IsOptional()
590
+ @attribute()
591
+ updatedAt?: string;
592
+ }
593
+
594
+ // entities/effect.entity.ts
595
+ import {
596
+ attribute,
597
+ table,
598
+ hashKey,
599
+ } from '@aws/dynamodb-data-mapper-annotations';
600
+ import { IsString, IsOptional, IsBoolean, IsEnum, IsObject } from 'class-validator';
601
+
602
+ export enum EffectType {
603
+ FILTER = 'filter',
604
+ TRANSITION = 'transition',
605
+ TEXT = 'text',
606
+ SHAPE = 'shape',
607
+ ANIMATION = 'animation',
608
+ COLOR_CORRECTION = 'color_correction',
609
+ AUDIO_EFFECT = 'audio_effect',
610
+ }
611
+
612
+ @table('effects')
613
+ export class Effect {
614
+ @IsString()
615
+ @hashKey()
616
+ id: string;
617
+
618
+ @IsString()
619
+ @IsOptional()
620
+ @attribute()
621
+ categoryId?: string;
622
+
623
+ @IsString()
624
+ @attribute()
625
+ name: string;
626
+
627
+ @IsString()
628
+ @IsOptional()
629
+ @attribute()
630
+ description?: string;
631
+
632
+ @IsEnum(EffectType)
633
+ @attribute()
634
+ effectType: EffectType;
635
+
636
+ @IsObject()
637
+ @IsOptional()
638
+ @attribute()
639
+ parametersSchema?: Record<string, any>; // JSON schema for parameters
640
+
641
+ @IsString()
642
+ @IsOptional()
643
+ @attribute()
644
+ thumbnailUrl?: string;
645
+
646
+ @IsString()
647
+ @IsOptional()
648
+ @attribute()
649
+ previewUrl?: string;
650
+
651
+ @IsBoolean()
652
+ @attribute()
653
+ isPremium: boolean = false;
654
+
655
+ @IsBoolean()
656
+ @attribute()
657
+ isActive: boolean = true;
658
+
659
+ @IsString()
660
+ @IsOptional()
661
+ @attribute()
662
+ createdAt?: string;
663
+ }
664
+
665
+ // entities/clip-effect.entity.ts
666
+ import {
667
+ attribute,
668
+ table,
669
+ hashKey,
670
+ } from '@aws/dynamodb-data-mapper-annotations';
671
+ import { IsString, IsNumber, IsOptional, IsBoolean, IsObject } from 'class-validator';
672
+
673
+ @table('clip_effects')
674
+ export class ClipEffect {
675
+ @IsString()
676
+ @hashKey()
677
+ id: string;
678
+
679
+ @IsString()
680
+ @attribute()
681
+ clipId: string;
682
+
683
+ @IsString()
684
+ @attribute()
685
+ effectId: string;
686
+
687
+ @IsObject()
688
+ @IsOptional()
689
+ @attribute()
690
+ parameters?: Record<string, any>; // Actual parameter values
691
+
692
+ @IsNumber()
693
+ @attribute()
694
+ orderIndex: number;
695
+
696
+ @IsBoolean()
697
+ @attribute()
698
+ isEnabled: boolean = true;
699
+
700
+ @IsString()
701
+ @IsOptional()
702
+ @attribute()
703
+ createdAt?: string;
704
+ }
705
+
706
+ // entities/keyframe.entity.ts
707
+ import {
708
+ attribute,
709
+ table,
710
+ hashKey,
711
+ } from '@aws/dynamodb-data-mapper-annotations';
712
+ import { IsString, IsNumber, IsOptional, IsEnum, IsObject, IsArray } from 'class-validator';
713
+
714
+ export enum EasingType {
715
+ LINEAR = 'linear',
716
+ EASE_IN = 'ease-in',
717
+ EASE_OUT = 'ease-out',
718
+ EASE_IN_OUT = 'ease-in-out',
719
+ BEZIER = 'bezier',
720
+ }
721
+
722
+ @table('keyframes')
723
+ export class Keyframe {
724
+ @IsString()
725
+ @hashKey()
726
+ id: string;
727
+
728
+ @IsString()
729
+ @IsOptional()
730
+ @attribute()
731
+ clipId?: string;
732
+
733
+ @IsString()
734
+ @IsOptional()
735
+ @attribute()
736
+ clipEffectId?: string;
737
+
738
+ @IsString()
739
+ @attribute()
740
+ propertyName: string; // e.g., 'opacity', 'x', 'y', 'scale'
741
+
742
+ @IsNumber()
743
+ @attribute()
744
+ timeMs: number;
745
+
746
+ @IsObject()
747
+ @attribute()
748
+ value: any; // Can store various data types
749
+
750
+ @IsEnum(EasingType)
751
+ @attribute()
752
+ easingType: EasingType = EasingType.LINEAR;
753
+
754
+ @IsArray()
755
+ @IsOptional()
756
+ @attribute()
757
+ bezierValues?: number[]; // For custom bezier curves [x1, y1, x2, y2]
758
+
759
+ @IsString()
760
+ @IsOptional()
761
+ @attribute()
762
+ createdAt?: string;
763
+ }
764
+
765
+ // entities/project-version.entity.ts
766
+ import {
767
+ attribute,
768
+ table,
769
+ hashKey,
770
+ } from '@aws/dynamodb-data-mapper-annotations';
771
+ import { IsString, IsNumber, IsOptional, IsObject } from 'class-validator';
772
+
773
+ @table('project_versions')
774
+ export class ProjectVersion {
775
+ @IsString()
776
+ @hashKey()
777
+ id: string;
778
+
779
+ @IsString()
780
+ @attribute()
781
+ projectId: string;
782
+
783
+ @IsNumber()
784
+ @attribute()
785
+ versionNumber: number;
786
+
787
+ @IsString()
788
+ @IsOptional()
789
+ @attribute()
790
+ name?: string;
791
+
792
+ @IsString()
793
+ @IsOptional()
794
+ @attribute()
795
+ description?: string;
796
+
797
+ @IsString()
798
+ @attribute()
799
+ createdBy: string; // User ID
800
+
801
+ @IsObject()
802
+ @IsOptional()
803
+ @attribute()
804
+ snapshotData?: Record<string, any>; // Serialized project state
805
+
806
+ @IsNumber()
807
+ @IsOptional()
808
+ @attribute()
809
+ fileSize?: number;
810
+
811
+ @IsString()
812
+ @IsOptional()
813
+ @attribute()
814
+ createdAt?: string;
815
+ }
816
+
817
+ // entities/comment.entity.ts
818
+ import {
819
+ attribute,
820
+ table,
821
+ hashKey,
822
+ } from '@aws/dynamodb-data-mapper-annotations';
823
+ import { IsString, IsNumber, IsOptional, IsEnum } from 'class-validator';
824
+
825
+ export enum CommentStatus {
826
+ OPEN = 'open',
827
+ RESOLVED = 'resolved',
828
+ ARCHIVED = 'archived',
829
+ }
830
+
831
+ @table('comments')
832
+ export class Comment {
833
+ @IsString()
834
+ @hashKey()
835
+ id: string;
836
+
837
+ @IsString()
838
+ @attribute()
839
+ projectId: string;
840
+
841
+ @IsString()
842
+ @attribute()
843
+ userId: string;
844
+
845
+ @IsString()
846
+ @IsOptional()
847
+ @attribute()
848
+ parentId?: string; // For reply threads
849
+
850
+ @IsString()
851
+ @attribute()
852
+ content: string;
853
+
854
+ @IsNumber()
855
+ @IsOptional()
856
+ @attribute()
857
+ timestampMs?: number; // Specific time in timeline
858
+
859
+ @IsNumber()
860
+ @IsOptional()
861
+ @attribute()
862
+ positionX?: number; // X coordinate for video comments
863
+
864
+ @IsNumber()
865
+ @IsOptional()
866
+ @attribute()
867
+ positionY?: number; // Y coordinate for video comments
868
+
869
+ @IsEnum(CommentStatus)
870
+ @attribute()
871
+ status: CommentStatus = CommentStatus.OPEN;
872
+
873
+ @IsString()
874
+ @IsOptional()
875
+ @attribute()
876
+ createdAt?: string;
877
+
878
+ @IsString()
879
+ @IsOptional()
880
+ @attribute()
881
+ updatedAt?: string;
882
+ }
883
+
884
+ // ===== REPOSITORIES =====
885
+
886
+ // repositories/workspace.repository.ts
887
+ import { Injectable, Logger } from '@nestjs/common';
888
+ import { DynamoDB } from '@aws-sdk/client-dynamodb';
889
+ import { BaseRepository } from './base.repository';
890
+ import { Workspace } from '../entities/workspace.entity';
891
+
892
+ @Injectable()
893
+ export class WorkspaceRepository extends BaseRepository<Workspace> {
894
+ private readonly logger = new Logger(WorkspaceRepository.name);
895
+
896
+ constructor(dynamoDbClient: DynamoDB) {
897
+ super(dynamoDbClient, `Workspace-${process.env.STAGE}`);
898
+ }
899
+
900
+ async findByOwnerId(ownerId: string): Promise<Workspace[]> {
901
+ const result = await this.dynamoDb.query({
902
+ TableName: this.tableName,
903
+ IndexName: 'ownerId-index',
904
+ KeyConditionExpression: 'ownerId = :ownerId',
905
+ ExpressionAttributeValues: {
906
+ ':ownerId': ownerId,
907
+ },
908
+ });
909
+ return (result.Items as Workspace[]) || [];
910
+ }
911
+
912
+ async findSharedWorkspaces(): Promise<Workspace[]> {
913
+ const result = await this.dynamoDb.query({
914
+ TableName: this.tableName,
915
+ IndexName: 'isShared-index',
916
+ KeyConditionExpression: 'isShared = :shared',
917
+ ExpressionAttributeValues: {
918
+ ':shared': true,
919
+ },
920
+ });
921
+ return (result.Items as Workspace[]) || [];
922
+ }
923
+
924
+ async findByNameContains(searchTerm: string, ownerId?: string): Promise<Workspace[]> {
925
+ const filterExpression = ownerId
926
+ ? 'contains(#name, :searchTerm) AND ownerId = :ownerId'
927
+ : 'contains(#name, :searchTerm)';
928
+
929
+ const expressionAttributeValues = ownerId
930
+ ? { ':searchTerm': searchTerm, ':ownerId': ownerId }
931
+ : { ':searchTerm': searchTerm };
932
+
933
+ const result = await this.dynamoDb.scan({
934
+ TableName: this.tableName,
935
+ FilterExpression: filterExpression,
936
+ ExpressionAttributeNames: {
937
+ '#name': 'name',
938
+ },
939
+ ExpressionAttributeValues: expressionAttributeValues,
940
+ });
941
+ return (result.Items as Workspace[]) || [];
942
+ }
943
+ }
944
+
945
+ // repositories/project.repository.ts
946
+ import { Injectable, Logger } from '@nestjs/common';
947
+ import { DynamoDB } from '@aws-sdk/client-dynamodb';
948
+ import { BaseRepository } from './base.repository';
949
+ import { Project, ProjectStatus, ProjectVisibility } from '../entities/project.entity';
950
+
951
+ @Injectable()
952
+ export class ProjectRepository extends BaseRepository<Project> {
953
+ private readonly logger = new Logger(ProjectRepository.name);
954
+
955
+ constructor(dynamoDbClient: DynamoDB) {
956
+ super(dynamoDbClient, `Project-${process.env.STAGE}`);
957
+ }
958
+
959
+ async findByWorkspaceId(workspaceId: string): Promise<Project[]> {
960
+ const result = await this.dynamoDb.query({
961
+ TableName: this.tableName,
962
+ IndexName: 'workspaceId-updatedAt-index',
963
+ KeyConditionExpression: 'workspaceId = :workspaceId',
964
+ ScanIndexForward: false, // Latest first
965
+ ExpressionAttributeValues: {
966
+ ':workspaceId': workspaceId,
967
+ },
968
+ });
969
+ return (result.Items as Project[]) || [];
970
+ }
971
+
972
+ async findByOwnerId(ownerId: string): Promise<Project[]> {
973
+ const result = await this.dynamoDb.query({
974
+ TableName: this.tableName,
975
+ IndexName: 'ownerId-updatedAt-index',
976
+ KeyConditionExpression: 'ownerId = :ownerId',
977
+ ScanIndexForward: false,
978
+ ExpressionAttributeValues: {
979
+ ':ownerId': ownerId,
980
+ },
981
+ });
982
+ return (result.Items as Project[]) || [];
983
+ }
984
+
985
+ async findByStatus(status: ProjectStatus): Promise<Project[]> {
986
+ const result = await this.dynamoDb.query({
987
+ TableName: this.tableName,
988
+ IndexName: 'status-updatedAt-index',
989
+ KeyConditionExpression: '#status = :status',
990
+ ExpressionAttributeNames: {
991
+ '#status': 'status',
992
+ },
993
+ ExpressionAttributeValues: {
994
+ ':status': status,
995
+ },
996
+ });
997
+ return (result.Items as Project[]) || [];
998
+ }
999
+
1000
+ async findTemplates(category?: string, isPremium?: boolean): Promise<Project[]> {
1001
+ let filterExpression = 'isTemplate = :isTemplate';
1002
+ const expressionAttributeValues: any = { ':isTemplate': true };
1003
+
1004
+ if (category) {
1005
+ filterExpression += ' AND templateCategory = :category';
1006
+ expressionAttributeValues[':category'] = category;
1007
+ }
1008
+
1009
+ if (isPremium !== undefined) {
1010
+ filterExpression += ' AND isPremiumTemplate = :isPremium';
1011
+ expressionAttributeValues[':isPremium'] = isPremium;
1012
+ }
1013
+
1014
+ const result = await this.dynamoDb.scan({
1015
+ TableName: this.tableName,
1016
+ FilterExpression: filterExpression,
1017
+ ExpressionAttributeValues: expressionAttributeValues,
1018
+ });
1019
+ return (result.Items as Project[]) || [];
1020
+ }
1021
+
1022
+ async findRecentProjects(userId: string, limit: number = 10): Promise<Project[]> {
1023
+ const result = await this.dynamoDb.query({
1024
+ TableName: this.tableName,
1025
+ IndexName: 'ownerId-lastOpenedAt-index',
1026
+ KeyConditionExpression: 'ownerId = :userId',
1027
+ ScanIndexForward: false,
1028
+ Limit: limit,
1029
+ ExpressionAttributeValues: {
1030
+ ':userId': userId,
1031
+ },
1032
+ });
1033
+ return (result.Items as Project[]) || [];
1034
+ }
1035
+
1036
+ async findFavoriteProjects(userId: string): Promise<Project[]> {
1037
+ const result = await this.dynamoDb.query({
1038
+ TableName: this.tableName,
1039
+ IndexName: 'ownerId-isFavorite-index',
1040
+ KeyConditionExpression: 'ownerId = :userId AND isFavorite = :favorite',
1041
+ ExpressionAttributeValues: {
1042
+ ':userId': userId,
1043
+ ':favorite': true,
1044
+ },
1045
+ });
1046
+ return (result.Items as Project[]) || [];
1047
+ }
1048
+
1049
+ async findByTags(tags: string[], workspaceId?: string): Promise<Project[]> {
1050
+ const tagConditions = tags.map((_, index) => `contains(tags, :tag${index})`).join(' OR ');
1051
+ let filterExpression = `(${tagConditions})`;
1052
+
1053
+ const expressionAttributeValues: any = {};
1054
+ tags.forEach((tag, index) => {
1055
+ expressionAttributeValues[`:tag${index}`] = tag;
1056
+ });
1057
+
1058
+ if (workspaceId) {
1059
+ filterExpression += ' AND workspaceId = :workspaceId';
1060
+ expressionAttributeValues[':workspaceId'] = workspaceId;
1061
+ }
1062
+
1063
+ const result = await this.dynamoDb.scan({
1064
+ TableName: this.tableName,
1065
+ FilterExpression: filterExpression,
1066
+ ExpressionAttributeValues: expressionAttributeValues,
1067
+ });
1068
+ return (result.Items as Project[]) || [];
1069
+ }
1070
+
1071
+ async updateLastOpened(projectId: string, userId: string): Promise<void> {
1072
+ await this.dynamoDb.update({
1073
+ TableName: this.tableName,
1074
+ Key: { id: projectId },
1075
+ UpdateExpression: 'SET lastOpenedAt = :timestamp, lastEditedBy = :userId',
1076
+ ExpressionAttributeValues: {
1077
+ ':timestamp': new Date().toISOString(),
1078
+ ':userId': userId,
1079
+ },
1080
+ });
1081
+ }
1082
+
1083
+ async incrementTemplateUsage(projectId: string): Promise<void> {
1084
+ await this.dynamoDb.update({
1085
+ TableName: this.tableName,
1086
+ Key: { id: projectId },
1087
+ UpdateExpression: 'ADD templateUsageCount :increment',
1088
+ ExpressionAttributeValues: {
1089
+ ':increment': 1,
1090
+ },
1091
+ });
1092
+ }
1093
+
1094
+ async searchProjects(searchTerm: string, workspaceId?: string, userId?: string): Promise<Project[]> {
1095
+ let filterExpression = 'contains(#name, :searchTerm) OR contains(description, :searchTerm)';
1096
+ const expressionAttributeNames: any = { '#name': 'name' };
1097
+ const expressionAttributeValues: any = { ':searchTerm': searchTerm };
1098
+
1099
+ if (workspaceId) {
1100
+ filterExpression += ' AND workspaceId = :workspaceId';
1101
+ expressionAttributeValues[':workspaceId'] = workspaceId;
1102
+ }
1103
+
1104
+ if (userId) {
1105
+ filterExpression += ' AND ownerId = :userId';
1106
+ expressionAttributeValues[':userId'] = userId;
1107
+ }
1108
+
1109
+ const result = await this.dynamoDb.scan({
1110
+ TableName: this.tableName,
1111
+ FilterExpression: filterExpression,
1112
+ ExpressionAttributeNames: expressionAttributeNames,
1113
+ ExpressionAttributeValues: expressionAttributeValues,
1114
+ });
1115
+ return (result.Items as Project[]) || [];
1116
+ }
1117
+ }
1118
+
1119
+ // repositories/asset.repository.ts
1120
+ import { Injectable, Logger } from '@nestjs/common';
1121
+ import { DynamoDB } from '@aws-sdk/client-dynamodb';
1122
+ import { BaseRepository } from './base.repository';
1123
+ import { Asset, AssetType, AssetStatus } from '../entities/asset.entity';
1124
+
1125
+ @Injectable()
1126
+ export class AssetRepository extends BaseRepository<Asset> {
1127
+ private readonly logger = new Logger(AssetRepository.name);
1128
+
1129
+ constructor(dynamoDbClient: DynamoDB) {
1130
+ super(dynamoDbClient, `Asset-${process.env.STAGE}`);
1131
+ }
1132
+
1133
+ async findByWorkspaceId(workspaceId: string): Promise<Asset[]> {
1134
+ const result = await this.dynamoDb.query({
1135
+ TableName: this.tableName,
1136
+ IndexName: 'workspaceId-createdAt-index',
1137
+ KeyConditionExpression: 'workspaceId = :workspaceId',
1138
+ ScanIndexForward: false,
1139
+ ExpressionAttributeValues: {
1140
+ ':workspaceId': workspaceId,
1141
+ },
1142
+ });
1143
+ return (result.Items as Asset[]) || [];
1144
+ }
1145
+
1146
+ async findByFileType(fileType: AssetType, workspaceId?: string): Promise<Asset[]> {
1147
+ if (workspaceId) {
1148
+ const result = await this.dynamoDb.query({
1149
+ TableName: this.tableName,
1150
+ IndexName: 'workspaceId-fileType-index',
1151
+ KeyConditionExpression: 'workspaceId = :workspaceId AND fileType = :fileType',
1152
+ ExpressionAttributeValues: {
1153
+ ':workspaceId': workspaceId,
1154
+ ':fileType': fileType,
1155
+ },
1156
+ });
1157
+ return (result.Items as Asset[]) || [];
1158
+ }
1159
+
1160
+ const result = await this.dynamoDb.query({
1161
+ TableName: this.tableName,
1162
+ IndexName: 'fileType-index',
1163
+ KeyConditionExpression: 'fileType = :fileType',
1164
+ ExpressionAttributeValues: {
1165
+ ':fileType': fileType,
1166
+ },
1167
+ });
1168
+ return (result.Items as Asset[]) || [];
1169
+ }
1170
+
1171
+ async findByUploadStatus(status: AssetStatus): Promise<Asset[]> {
1172
+ const result = await this.dynamoDb.query({
1173
+ TableName: this.tableName,
1174
+ IndexName: 'uploadStatus-index',
1175
+ KeyConditionExpression: 'uploadStatus = :status',
1176
+ ExpressionAttributeValues: {
1177
+ ':status': status,
1178
+ },
1179
+ });
1180
+ return (result.Items as Asset[]) || [];
1181
+ }
1182
+
1183
+ async findByUploadedBy(userId: string): Promise<Asset[]> {
1184
+ const result = await this.dynamoDb.query({
1185
+ TableName: this.tableName,
1186
+ IndexName: 'uploadedBy-createdAt-index',
1187
+ KeyConditionExpression: 'uploadedBy = :userId',
1188
+ ScanIndexForward: false,
1189
+ ExpressionAttributeValues: {
1190
+ ':userId': userId,
1191
+ },
1192
+ });
1193
+ return (result.Items as Asset[]) || [];
1194
+ }
1195
+
1196
+ async findByTags(tags: string[], workspaceId?: string): Promise<Asset[]> {
1197
+ const tagConditions = tags.map((_, index) => `contains(tags, :tag${index})`).join(' OR ');
1198
+ let filterExpression = `(${tagConditions})`;
1199
+
1200
+ const expressionAttributeValues: any = {};
1201
+ tags.forEach((tag, index) => {
1202
+ expressionAttributeValues[`:tag${index}`] = tag;
1203
+ });
1204
+
1205
+ if (workspaceId) {
1206
+ filterExpression += ' AND workspaceId = :workspaceId';
1207
+ expressionAttributeValues[':workspaceId'] = workspaceId;
1208
+ }
1209
+
1210
+ const result = await this.dynamoDb.scan({
1211
+ TableName: this.tableName,
1212
+ FilterExpression: filterExpression,
1213
+ ExpressionAttributeValues: expressionAttributeValues,
1214
+ });
1215
+ return (result.Items as Asset[]) || [];
1216
+ }
1217
+
1218
+ async searchAssets(searchTerm: string, workspaceId?: string): Promise<Asset[]> {
1219
+ let filterExpression = 'contains(#name, :searchTerm) OR contains(originalFilename, :searchTerm)';
1220
+ const expressionAttributeNames: any = { '#name': 'name' };
1221
+ const expressionAttributeValues: any = { ':searchTerm': searchTerm };
1222
+
1223
+ if (workspaceId) {
1224
+ filterExpression += ' AND workspaceId = :workspaceId';
1225
+ expressionAttributeValues[':workspaceId'] = workspaceId;
1226
+ }
1227
+
1228
+ const result = await this.dynamoDb.scan({
1229
+ TableName: this.tableName,
1230
+ FilterExpression: filterExpression,
1231
+ ExpressionAttributeNames: expressionAttributeNames,
1232
+ ExpressionAttributeValues: expressionAttributeValues,
1233
+ });
1234
+ return (result.Items as Asset[]) || [];
1235
+ }
1236
+
1237
+ async updateUsage(assetId: string): Promise<void> {
1238
+ await this.dynamoDb.update({
1239
+ TableName: this.tableName,
1240
+ Key: { id: assetId },
1241
+ UpdateExpression: 'ADD usageCount :increment SET lastUsedAt = :timestamp',
1242
+ ExpressionAttributeValues: {
1243
+ ':increment': 1,
1244
+ ':timestamp': new Date().toISOString(),
1245
+ },
1246
+ });
1247
+ }
1248
+
1249
+ async getWorkspaceStorageUsage(workspaceId: string): Promise<number> {
1250
+ const result = await this.dynamoDb.query({
1251
+ TableName: this.tableName,
1252
+ IndexName: 'workspaceId-index',
1253
+ KeyConditionExpression: 'workspaceId = :workspaceId',
1254
+ ProjectionExpression: 'fileSize',
1255
+ ExpressionAttributeValues: {
1256
+ ':workspaceId': workspaceId,
1257
+ },
1258
+ });
1259
+
1260
+ return (result.Items as Asset[])
1261
+ .reduce((total, asset) => total + (asset.fileSize || 0), 0);
1262
+ }
1263
+
1264
+ async findPublicAssets(fileType?: AssetType): Promise<Asset[]> {
1265
+ let filterExpression = 'isPublic = :isPublic';
1266
+ const expressionAttributeValues: any = { ':isPublic': true };
1267
+
1268
+ if (fileType) {
1269
+ filterExpression += ' AND fileType = :fileType';
1270
+ expressionAttributeValues[':fileType'] = fileType;
1271
+ }
1272
+
1273
+ const result = await this.dynamoDb.scan({
1274
+ TableName: this.tableName,
1275
+ FilterExpression: filterExpression,
1276
+ ExpressionAttributeValues: expressionAttributeValues,
1277
+ });
1278
+ return (result.Items as Asset[]) || [];
1279
+ }
1280
+ }
1281
+
1282
+ // repositories/timeline.repository.ts
1283
+ import { Injectable, Logger } from '@nestjs/common';
1284
+ import { DynamoDB } from '@aws-sdk/client-dynamodb';
1285
+ import { BaseRepository } from './base.repository';
1286
+ import { Timeline } from '../entities/timeline.entity';
1287
+
1288
+ @Injectable()
1289
+ export class TimelineRepository extends BaseRepository<Timeline> {
1290
+ private readonly logger = new Logger(TimelineRepository.name);
1291
+
1292
+ constructor(dynamoDbClient: DynamoDB) {
1293
+ super(dynamoDbClient, `Timeline-${process.env.STAGE}`);
1294
+ }
1295
+
1296
+ async findByProjectId(projectId: string): Promise<Timeline[]> {
1297
+ const result = await this.dynamoDb.query({
1298
+ TableName: this.tableName,
1299
+ IndexName: 'projectId-index',
1300
+ KeyConditionExpression: 'projectId = :projectId',
1301
+ ExpressionAttributeValues: {
1302
+ ':projectId': projectId,
1303
+ },
1304
+ });
1305
+ return (result.Items as Timeline[]) || [];
1306
+ }
1307
+
1308
+ async findMainTimelineByProjectId(projectId: string): Promise<Timeline | undefined> {
1309
+ const result = await this.dynamoDb.query({
1310
+ TableName: this.tableName,
1311
+ IndexName: 'projectId-name-index',
1312
+ KeyConditionExpression: 'projectId = :projectId AND #name = :name',
1313
+ ExpressionAttributeNames: {
1314
+ '#name': 'name',
1315
+ },
1316
+ ExpressionAttributeValues: {
1317
+ ':projectId': projectId,
1318
+ ':name': 'Main Timeline',
1319
+ },
1320
+ });
1321
+ return result.Items?.[0] as Timeline;
1322
+ }
1323
+ }
1324
+
1325
+ // repositories/track.repository.ts
1326
+ import { Injectable, Logger } from '@nestjs/common';
1327
+ import { DynamoDB } from '@aws-sdk/client-dynamodb';
1328
+ import { BaseRepository } from './base.repository';
1329
+ import { Track, TrackType } from '../entities/track.entity';
1330
+
1331
+ @Injectable()
1332
+ export class TrackRepository extends BaseRepository<Track> {
1333
+ private readonly logger = new Logger(TrackRepository.name);
1334
+
1335
+ constructor(dynamoDbClient: DynamoDB) {
1336
+ super(dynamoDbClient, `Track-${process.env.STAGE}`);
1337
+ }
1338
+
1339
+ async findByTimelineId(timelineId: string): Promise<Track[]> {
1340
+ const result = await this.dynamoDb.query({
1341
+ TableName: this.tableName,
1342
+ IndexName: 'timelineId-orderIndex-index',
1343
+ KeyConditionExpression: 'timelineId = :timelineId',
1344
+ ExpressionAttributeValues: {
1345
+ ':timelineId': timelineId,
1346
+ },
1347
+ });
1348
+ return (result.Items as Track[]) || [];
1349
+ }
1350
+
1351
+ async findByTrackType(timelineId: string, trackType: TrackType): Promise<Track[]> {
1352
+ const result = await this.dynamoDb.query({
1353
+ TableName: this.tableName,
1354
+ IndexName: 'timelineId-trackType-index',
1355
+ KeyConditionExpression: 'timelineId = :timelineId AND trackType = :trackType',
1356
+ ExpressionAttributeValues: {
1357
+ ':timelineId': timelineId,
1358
+ ':trackType': trackType,
1359
+ },
1360
+ });
1361
+ return (result.Items as Track[]) || [];
1362
+ }
1363
+
1364
+ async reorderTracks(timelineId: string, trackOrders: { id: string; orderIndex: number }[]): Promise<void> {
1365
+ const updatePromises = trackOrders.map(({ id, orderIndex }) =>
1366
+ this.dynamoDb.update({
1367
+ TableName: this.tableName,
1368
+ Key: { id },
1369
+ UpdateExpression: 'SET orderIndex = :orderIndex',
1370
+ ExpressionAttributeValues: {
1371
+ ':orderIndex': orderIndex,
1372
+ },
1373
+ })
1374
+ );
1375
+
1376
+ await Promise.all(updatePromises);
1377
+ }
1378
+ }
1379
+
1380
+ // repositories/clip.repository.ts
1381
+ import { Injectable, Logger } from '@nestjs/common';
1382
+ import { DynamoDB } from '@aws-sdk/client-dynamodb';
1383
+ import { BaseRepository } from './base.repository';
1384
+ import { Clip } from '../entities/clip.entity';
1385
+
1386
+ @Injectable()
1387
+ export class ClipRepository extends BaseRepository<Clip> {
1388
+ private readonly logger = new Logger(ClipRepository.name);
1389
+
1390
+ constructor(dynamoDbClient: DynamoDB) {
1391
+ super(dynamoDbClient, `Clip-${process.env.STAGE}`);
1392
+ }
1393
+
1394
+ async findByTrackId(trackId: string): Promise<Clip[]> {
1395
+ const result = await this.dynamoDb.query({
1396
+ TableName: this.tableName,
1397
+ IndexName: 'trackId-startTimeMs-index',
1398
+ KeyConditionExpression: 'trackId = :trackId',
1399
+ ExpressionAttributeValues: {
1400
+ ':trackId': trackId,
1401
+ },
1402
+ });
1403
+ return (result.Items as Clip[]) || [];
1404
+ }
1405
+
1406
+ async findByAssetId(assetId: string): Promise<Clip[]> {
1407
+ const result = await this.dynamoDb.query({
1408
+ TableName: this.tableName,
1409
+ IndexName: 'assetId-index',
1410
+ KeyConditionExpression: 'assetId = :assetId',
1411
+ ExpressionAttributeValues: {
1412
+ ':assetId': assetId,
1413
+ },
1414
+ });
1415
+ return (result.Items as Clip[]) || [];
1416
+ }
1417
+
1418
+ async findClipsInTimeRange(trackId: string, startTime: number, endTime: number): Promise<Clip[]> {
1419
+ const result = await this.dynamoDb.query({
1420
+ TableName: this.tableName,
1421
+ IndexName: 'trackId-startTimeMs-index',
1422
+ KeyConditionExpression: 'trackId = :trackId AND startTimeMs BETWEEN :startTime AND :endTime',
1423
+ ExpressionAttributeValues: {
1424
+ ':trackId': trackId,
1425
+ ':startTime': startTime,
1426
+ ':endTime': endTime,
1427
+ },
1428
+ });
1429
+ return (result.Items as Clip[]) || [];
1430
+ }
1431
+
1432
+ async findOverlappingClips(trackId: string, startTime: number, endTime: number): Promise<Clip[]> {
1433
+ const result = await this.dynamoDb.scan({
1434
+ TableName: this.tableName,
1435
+ FilterExpression: 'trackId = :trackId AND ((startTimeMs <= :startTime AND endTimeMs > :startTime) OR (startTimeMs < :endTime AND endTimeMs >= :endTime) OR (startTimeMs >= :startTime AND endTimeMs <= :endTime))',
1436
+ ExpressionAttributeValues: {
1437
+ ':trackId': trackId,
1438
+ ':startTime': startTime,
1439
+ ':endTime': endTime,
1440
+ },
1441
+ });
1442
+ return (result.Items as Clip[]) || [];
1443
+ }
1444
+
1445
+ async moveClip(clipId: string, newStartTime: number, newEndTime: number): Promise<void> {
1446
+ await this.dynamoDb.update({
1447
+ TableName: this.tableName,
1448
+ Key: { id: clipId },
1449
+ UpdateExpression: 'SET startTimeMs = :startTime, endTimeMs = :endTime, updatedAt = :updatedAt',
1450
+ ExpressionAttributeValues: {
1451
+ ':startTime': newStartTime,
1452
+ ':endTime': newEndTime,
1453
+ ':updatedAt': new Date().toISOString(),
1454
+ },
1455
+ });
1456
+ }
1457
+
1458
+ async duplicateClip(clipId: string, newTrackId?: string): Promise<Clip> {
1459
+ const originalClip = await this.findById(clipId);
1460
+ if (!originalClip) {
1461
+ throw new Error('Clip not found');
1462
+ }
1463
+
1464
+ const newClip: Clip = {
1465
+ ...originalClip,
1466
+ id: `${Date.now()}-${Math.random()}`, // Generate new ID
1467
+ trackId: newTrackId || originalClip.trackId,
1468
+ name: `${originalClip.name || 'Clip'} Copy`,
1469
+ createdAt: new Date().toISOString(),
1470
+ updatedAt: new Date().toISOString(),
1471
+ };
1472
+
1473
+ return await this.create(newClip);
1474
+ }
1475
+ }
1476
+
1477
+ // repositories/comment.repository.ts
1478
+ import { Injectable, Logger } from '@nestjs/common';
1479
+ import { DynamoDB } from '@aws-sdk/client-dynamodb';
1480
+ import { BaseRepository } from './base.repository';
1481
+ import { Comment, CommentStatus } from '../entities/comment.entity';
1482
+
1483
+ @Injectable()
1484
+ export class CommentRepository extends BaseRepository<Comment> {
1485
+ private readonly logger = new Logger(CommentRepository.name);
1486
+
1487
+ constructor(dynamoDbClient: DynamoDB) {
1488
+ super(dynamoDbClient, `Comment-${process.env.STAGE}`);
1489
+ }
1490
+
1491
+ async findByProjectId(projectId: string): Promise<Comment[]> {
1492
+ const result = await this.dynamoDb.query({
1493
+ TableName: this.tableName,
1494
+ IndexName: 'projectId-createdAt-index',
1495
+ KeyConditionExpression: 'projectId = :projectId',
1496
+ ScanIndexForward: false, // Latest first
1497
+ ExpressionAttributeValues: {
1498
+ ':projectId': projectId,
1499
+ },
1500
+ });
1501
+ return (result.Items as Comment[]) || [];
1502
+ }
1503
+
1504
+ async findByStatus(projectId: string, status: CommentStatus): Promise<Comment[]> {
1505
+ const result = await this.dynamoDb.query({
1506
+ TableName: this.tableName,
1507
+ IndexName: 'projectId-status-index',
1508
+ KeyConditionExpression: 'projectId = :projectId AND #status = :status',
1509
+ ExpressionAttributeNames: {
1510
+ '#status': 'status',
1511
+ },
1512
+ ExpressionAttributeValues: {
1513
+ ':projectId': projectId,
1514
+ ':status': status,
1515
+ },
1516
+ });
1517
+ return (result.Items as Comment[]) || [];
1518
+ }
1519
+
1520
+ async findReplies(parentId: string): Promise<Comment[]> {
1521
+ const result = await this.dynamoDb.query({
1522
+ TableName: this.tableName,
1523
+ IndexName: 'parentId-createdAt-index',
1524
+ KeyConditionExpression: 'parentId = :parentId',
1525
+ ExpressionAttributeValues: {
1526
+ ':parentId': parentId,
1527
+ },
1528
+ });
1529
+ return (result.Items as Comment[]) || [];
1530
+ }
1531
+
1532
+ async findByUserId(userId: string): Promise<Comment[]> {
1533
+ const result = await this.dynamoDb.query({
1534
+ TableName: this.tableName,
1535
+ IndexName: 'userId-createdAt-index',
1536
+ KeyConditionExpression: 'userId = :userId',
1537
+ ScanIndexForward: false,
1538
+ ExpressionAttributeValues: {
1539
+ ':userId': userId,
1540
+ },
1541
+ });
1542
+ return (result.Items as Comment[]) || [];
1543
+ }
1544
+
1545
+ async markAsResolved(commentId: string): Promise<void> {
1546
+ await this.dynamoDb.update({
1547
+ TableName: this.tableName,
1548
+ Key: { id: commentId },
1549
+ UpdateExpression: 'SET #status = :status, updatedAt = :updatedAt',
1550
+ ExpressionAttributeNames: {
1551
+ '#status': 'status',
1552
+ },
1553
+ ExpressionAttributeValues: {
1554
+ ':status': CommentStatus.RESOLVED,
1555
+ ':updatedAt': new Date().toISOString(),
1556
+ },
1557
+ });
1558
+ }
1559
+ }
1560
+ ```