Başlangıç

Soft Delete System

Soft Delete Sistemi Dokümantasyonu

Genel Bakış

Bu projede, verilerin kalıcı olarak silinmesi yerine "soft delete" (yumuşak silme) mekanizması kullanılmaktadır. Soft delete, kayıtların veritabanından fiziksel olarak silinmesi yerine deletedAt ve deletedBy alanları ile işaretlenmesi anlamına gelir. Bu sayede:

  • Veri kaybı önlenir
  • Silme işlemi geri alınabilir (restore)
  • Audit trail (denetim izi) tutulur
  • Normal sorgularda silinmiş kayıtlar görünmez

Mimari Yapı

1. Soft Delete Plugin

Soft delete işlevselliği, Mongoose için yazılmış global bir plugin aracılığıyla sağlanmaktadır.

Dosya: src/common/plugins/soft-delete.plugin.ts

Özellikler:

  • Otomatik Alan Ekleme: Tüm schema'lara deletedAt (Date) ve deletedBy (String) alanlarını ekler
  • Global Filtreleme: Tüm find, findOne, count ve findOneAndUpdate sorgularına otomatik filtre uygular
  • Instance Metodları: Her document'e softDelete() ve restore() metodlarını ekler

Plugin Kullanımı:

Plugin, app.module.ts içinde global olarak tüm Mongoose connection'ına eklenir:

MongooseModule.forRootAsync({
  connectionFactory: (connection) => {
    connection.plugin(SoftDeletePlugin);
    return connection;
  },
})

2. Filtreleme Mekanizması

Plugin, sorgulara otomatik olarak filtre uygular. Varsayılan davranış:

  • Normal sorgular: Sadece deletedAt: null olan (silinmemiş) kayıtları döndürür
  • Silinmiş kayıtları dahil etmek için: Query'ye showDeleted: true parametresi eklenir
  • Sadece silinmiş kayıtları görmek için: Query'ye onlyDeleted: true parametresi eklenir

Örnek Kullanımlar:

// Normal sorgu - sadece aktif kayıtlar
const activeStreams = await LiveStreamModel.find({ status: 'ACTIVE' });

// Silinmiş kayıtlar dahil
const allStreams = await LiveStreamModel.find({ 
  status: 'ACTIVE', 
  showDeleted: true 
});

// Sadece silinmiş kayıtlar
const deletedStreams = await LiveStreamModel.find({ 
  onlyDeleted: true 
});

3. Schema Tanımlamaları

LiveStream schema'sında soft delete alanları manuel olarak tanımlanmıştır:

@Prop({ type: Date, default: null })
deletedAt?: Date | null;

@Prop({ type: String, default: null })
deletedBy?: string | null;

Bu alanlar plugin tarafından da eklenir, ancak schema'da açıkça tanımlanması TypeScript tip güvenliği ve Swagger dokümantasyonu için önemlidir.

Live Stream Soft Delete İmplementasyonu

1. Repository Metodları

Dosya: src/live-stream/repository/live-stream.repository.ts

softDeleteById(id: string, userId: string)

Live stream'i soft delete eder.

Parametreler:

  • id: Silinecek live stream ID'si
  • userId: Silme işlemini yapan kullanıcı ID'si

İşlem Adımları:

  1. ID geçerliliği kontrol edilir
  2. Live stream bulunur ve populate edilir
  3. Plugin'in softDelete() metodu çağrılır
  4. Document tekrar populate edilir (save sonrası referanslar kaybolabilir)

Kod Referansı:

  async softDeleteById(
    id: string,
    userId: string,
  ): Promise<LiveStreamDocument | null> {
    if (!Types.ObjectId.isValid(id)) {
      return null;
    }

    const liveStream = await this.liveStreamModel
      .findById(id)
      .populate(this.liveStreamUserPopulate)
      .exec();

    if (!liveStream) {
      return null;
    }

    // Plugin'in eklediği softDelete metodunu kullan
    await (liveStream as any).softDelete(userId);

    // save() işleminden sonra populate edilmiş referanslar kaybolmuş olabilir
    // Document'i tekrar populate et
    await liveStream.populate(this.liveStreamUserPopulate);

    return liveStream;
  }

findByIdWithDeleted(id: string)

Soft delete edilmiş kayıtlar dahil ID ile bulur.

Kullanım: Soft delete kontrolü yapılırken veya restore işlemi öncesinde kullanılır.

Kod Referansı:

  async findByIdWithDeleted(id: string): Promise<LiveStreamDocument | null> {
    if (!Types.ObjectId.isValid(id)) {
      return null;
    }

    const liveStream = await this.liveStreamModel
      .findOne({ _id: new Types.ObjectId(id), showDeleted: true } as any)
      .populate(this.liveStreamUserPopulate)
      .exec();

    return liveStream ?? null;
  }

restoreById(id: string)

Soft delete edilmiş kaydı geri getirir (restore).

Kod Referansı:

  async restoreById(id: string): Promise<LiveStreamDocument | null> {
    if (!Types.ObjectId.isValid(id)) {
      return null;
    }

    const liveStream = await this.findByIdWithDeleted(id);

    if (!liveStream) {
      return null;
    }

    // Plugin'in eklediği restore metodunu kullan
    await (liveStream as any).restore();

    // Güncellenmiş document'i tekrar çek
    const restoredLiveStream = await this.liveStreamModel
      .findById(id)
      .populate(this.liveStreamUserPopulate)
      .exec();

    return restoredLiveStream ?? null;
  }

2. Use Case: SoftDeleteLiveStreamUseCase

Dosya: src/live-stream/use-cases/soft-delete-live-stream.usecase.ts

İş mantığı ve validasyonları içerir.

Validasyonlar:

  1. Live Stream Kontrolü: Live stream ID'si geçerli olmalı ve bulunmalı
  2. Zaten Silinmiş Kontrolü: Zaten soft delete edilmiş yayınlar tekrar silinemez
  3. Yetki Kontrolü: Sadece yayını oluşturan kişi (creator) silebilir
  4. Aktif Yayın Kontrolü: Aktif yayınlar silinemez, önce sonlandırılmalıdır

Kod Referansı:

  async execute(
    userId: string,
    liveStreamId: string,
  ): Promise<LiveStreamResponseDto> {
    if (!liveStreamId) {
      throw new NotFoundException('Canlı yayın bulunamadı');
    }

    // Yayın bilgisini populate ile al (soft delete edilmiş olsa bile)
    const liveStream = await this.liveStreamRepo.findByIdWithDeleted(
      liveStreamId,
    );

    if (!liveStream) {
      throw new NotFoundException('Canlı yayın bulunamadı');
    }

    // Zaten soft delete edilmiş mi kontrol et
    if (liveStream.deletedAt) {
      throw new ConflictException('Yayın zaten silinmiş');
    }

    // Creator kontrolü
    const creatorId = LiveStreamMapper.extractId(
      liveStream.creator as unknown,
    );

    if (!creatorId) {
      this.logger.warn(
        `Creator ID could not be extracted from live stream ${liveStreamId}`,
      );
      throw new ForbiddenException(
        'Bu yayını sadece yayını oluşturan kişi silebilir',
      );
    }

    // MongoDB ObjectId karşılaştırması için normalize et
    let isCreator = false;
    try {
      const creatorObjectId = Types.ObjectId.isValid(creatorId)
        ? new Types.ObjectId(creatorId)
        : null;
      const userObjectId = Types.ObjectId.isValid(userId)
        ? new Types.ObjectId(userId)
        : null;

      if (creatorObjectId && userObjectId) {
        isCreator = creatorObjectId.equals(userObjectId);
      } else {
        // ObjectId'ye çevrilemezse string karşılaştırması yap
        const normalizedCreatorId = creatorId.trim().toLowerCase();
        const normalizedUserId = userId.trim().toLowerCase();
        isCreator = normalizedCreatorId === normalizedUserId;
      }
    } catch (error) {
      // Hata durumunda string karşılaştırması yap
      const normalizedCreatorId = creatorId.trim().toLowerCase();
      const normalizedUserId = userId.trim().toLowerCase();
      isCreator = normalizedCreatorId === normalizedUserId;
    }

    if (!isCreator) {
      this.logger.debug(
        `User ${userId} is not the creator of live stream ${liveStreamId}. Creator ID: ${creatorId}`,
      );
      throw new ForbiddenException(
        'Bu yayını sadece yayını oluşturan kişi silebilir',
      );
    }

    // Aktif yayınlar soft delete edilemez (önce end edilmeli)
    if (liveStream.status === LiveStreamStatus.ACTIVE) {
      throw new ConflictException(
        'Aktif yayınlar silinemez. Önce yayını sonlandırın.',
      );
    }

    // Soft delete işlemini yap
    const deletedLiveStream = await this.liveStreamRepo.softDeleteById(
      liveStreamId,
      userId,
    );

    if (!deletedLiveStream) {
      throw new NotFoundException('Yayın silinirken bir hata oluştu');
    }

    this.logger.log(
      `Live stream ${liveStreamId} soft deleted by creator ${userId}`,
    );

    return LiveStreamMapper.mapToResponseDto(deletedLiveStream);
  }

3. Controller Endpoint

Dosya: src/live-stream/live-stream.controller.ts

Endpoint: DELETE /live-stream/:id

Özellikler:

  • JWT authentication gerekli
  • Sadece yayını oluşturan kişi silebilir
  • Aktif yayınlar silinemez

Kod Referansı:

  @Delete(':id')
  @UseGuards(JwtAuthGuard)
  @ApiSoftDeleteLiveStream()
  async softDeleteLiveStream(
    @CurrentUser() user: JwtPayload,
    @Param('id') id: string,
  ): Promise<LiveStreamResponseDto> {
    return this.softDeleteLiveStreamUseCase.execute(user._id, id);
  }

Hata Durumları

404 Not Found

  • Live stream bulunamadığında
  • Geçersiz ID formatı

403 Forbidden

  • Kullanıcı yayını oluşturan kişi değilse
  • Creator ID çıkarılamazsa

409 Conflict

  • Yayın zaten soft delete edilmişse
  • Aktif yayın silinmeye çalışılırsa

Kullanım Senaryoları

Senaryo 1: Normal Silme İşlemi

  1. Kullanıcı yayını sonlandırır (status: ENDED)
  2. Kullanıcı yayını silmek ister
  3. Sistem validasyonları kontrol eder:
    • Kullanıcı creator mı? ✓
    • Yayın aktif mi? ✗ (ENDED olmalı)
    • Zaten silinmiş mi? ✗
  4. Soft delete işlemi gerçekleştirilir
  5. deletedAt ve deletedBy alanları set edilir

Senaryo 2: Aktif Yayın Silme Denemesi

  1. Kullanıcı aktif bir yayını silmeye çalışır
  2. Sistem status === ACTIVE kontrolü yapar
  3. 409 Conflict hatası döner: "Aktif yayınlar silinemez. Önce yayını sonlandırın."

Senaryo 3: Yetkisiz Silme Denemesi

  1. Başka bir kullanıcı yayını silmeye çalışır
  2. Sistem creator kontrolü yapar
  3. 403 Forbidden hatası döner: "Bu yayını sadece yayını oluşturan kişi silebilir"

Sorgulama Davranışları

Normal Sorgular

Tüm normal sorgular otomatik olarak soft delete edilmiş kayıtları filtreler:

// Sadece aktif (silinmemiş) yayınlar döner
const streams = await LiveStreamModel.find({ status: 'ENDED' });

Silinmiş Kayıtları Dahil Etme

// Silinmiş kayıtlar dahil tüm yayınlar
const allStreams = await LiveStreamModel.find({ 
  showDeleted: true 
});

Sadece Silinmiş Kayıtları Getirme

// Sadece silinmiş yayınlar
const deletedStreams = await LiveStreamModel.find({ 
  onlyDeleted: true 
});

Restore İşlemi

Soft delete edilmiş bir kaydı geri getirmek için restoreById metodu kullanılabilir:

const restoredStream = await liveStreamRepo.restoreById(streamId);

Restore işlemi:

  • deletedAt alanını null yapar
  • deletedBy alanını null yapar
  • Kaydı normal sorgularda görünür hale getirir

Plugin Metodları

softDelete(deletedBy?: string)

Document instance'ında çağrılır:

const stream = await LiveStreamModel.findById(id);
await stream.softDelete(userId);

restore()

Document instance'ında çağrılır:

const stream = await LiveStreamModel.findOne({ _id: id, showDeleted: true });
await stream.restore();

Önemli Notlar

  1. Global Plugin: SoftDeletePlugin tüm Mongoose connection'ına global olarak eklenir, bu yüzden tüm schema'lar otomatik olarak soft delete desteği alır.

  2. Otomatik Filtreleme: Normal sorgular otomatik olarak deletedAt: null filtresi ekler. Bu davranışı değiştirmek için showDeleted: true kullanılmalıdır.

  3. Populate Sonrası: softDelete() veya restore() çağrıldıktan sonra save() işlemi populate edilmiş referansları kaybedebilir. Bu yüzden repository metodlarında tekrar populate edilir.

  4. Aktif Yayınlar: Aktif yayınlar soft delete edilemez. Önce end-my-live-stream endpoint'i ile yayın sonlandırılmalıdır.

  5. Creator Kontrolü: Sadece yayını oluşturan kişi (creator) soft delete işlemi yapabilir. Bu kontrol hem ObjectId hem de string karşılaştırması ile yapılır.

  6. Audit Trail: deletedBy alanı sayesinde hangi kullanıcının silme işlemini yaptığı takip edilebilir.

İlgili Dosyalar

  • Plugin: src/common/plugins/soft-delete.plugin.ts
  • Repository: src/live-stream/repository/live-stream.repository.ts
  • Use Case: src/live-stream/use-cases/soft-delete-live-stream.usecase.ts
  • Controller: src/live-stream/live-stream.controller.ts
  • Schema: src/live-stream/schemas/live-stream.schema.ts
  • Decorator: src/live-stream/decorators/api-soft-delete-live-stream.decorator.ts

On this page