File size: 3,571 Bytes
d44ff09
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
import { Injectable, Inject, Logger, NotFoundException, ForbiddenException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { InjectQueue } from '@nestjs/bullmq';
import { Queue } from 'bullmq';
import { Export, Campaign } from '../../database/entities';
import { ExportPackagerService, ExportRequest } from './export-packager.service';

export const EXPORT_QUEUE = 'export';

@Injectable()
export class ExportService {
  private readonly logger = new Logger('Export');

  constructor(
    @InjectRepository(Export) private readonly exportRepo: Repository<Export>,
    @InjectRepository(Campaign) private readonly campaignRepo: Repository<Campaign>,
    private readonly packager: ExportPackagerService,
    @InjectQueue(EXPORT_QUEUE) private readonly queue: Queue,
  ) {}

  async createExport(userId: string, campaignId: string, req: ExportRequest) {
    // Ownership check
    const campaign = await this.campaignRepo.findOne({ where: { id: campaignId } });
    if (!campaign) throw new NotFoundException({ code: 'NOT_FOUND', message: 'Campaign not found' });
    if (campaign.user_id !== userId) throw new ForbiddenException({ code: 'FORBIDDEN', message: 'Not your campaign' });

    const exportRec = this.exportRepo.create({
      campaign_id: campaignId,
      format_selection: { formats: req.formats, bundle_as_zip: req.bundle_as_zip },
      storage_path: '',
      status: 'pending',
    });
    await this.exportRepo.save(exportRec);

    // Enqueue async packaging job (Section 5: BullMQ for export jobs).
    await this.queue.add('package', {
      exportId: exportRec.id,
      campaignId,
      formats: req.formats,
      bundle_as_zip: req.bundle_as_zip,
    });
    this.logger.log(`Export ${exportRec.id} queued (pending)`);

    return { export_id: exportRec.id, status: 'pending' as const };
  }

  async listExports(userId: string, campaignId: string) {
    const campaign = await this.campaignRepo.findOne({ where: { id: campaignId } });
    if (!campaign) throw new NotFoundException({ code: 'NOT_FOUND', message: 'Campaign not found' });
    if (campaign.user_id !== userId) throw new ForbiddenException({ code: 'FORBIDDEN', message: 'Not your campaign' });
    const exports = await this.exportRepo.find({
      where: { campaign_id: campaignId },
      order: { created_at: 'DESC' },
    });
    return { exports };
  }

  async getDownload(userId: string, exportId: string) {
    const exportRec = await this.exportRepo.findOne({ where: { id: exportId } });
    if (!exportRec) throw new NotFoundException({ code: 'NOT_FOUND', message: 'Export not found' });
    const campaign = await this.campaignRepo.findOne({ where: { id: exportRec.campaign_id } });
    if (!campaign || campaign.user_id !== userId) {
      throw new ForbiddenException({ code: 'FORBIDDEN', message: 'Not your export' });
    }
    if (exportRec.status !== 'completed') {
      throw new NotFoundException({ code: 'NOT_READY', message: `Export is ${exportRec.status}` });
    }
    return exportRec;
  }

  /** Called by the BullMQ processor after packaging completes. */
  async markCompleted(exportId: string, storagePath: string) {
    await this.exportRepo.update(exportId, { storage_path: storagePath, status: 'completed' });
    this.logger.log(`Export ${exportId} completed -> ${storagePath}`);
  }

  async markFailed(exportId: string, reason: string) {
    await this.exportRepo.update(exportId, { status: 'failed' });
    this.logger.error(`Export ${exportId} failed: ${reason}`);
  }
}