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) vedeletedBy(String) alanlarını ekler - Global Filtreleme: Tüm
find,findOne,countvefindOneAndUpdatesorgularına otomatik filtre uygular - Instance Metodları: Her document'e
softDelete()verestore()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: nullolan (silinmemiş) kayıtları döndürür - Silinmiş kayıtları dahil etmek için: Query'ye
showDeleted: trueparametresi eklenir - Sadece silinmiş kayıtları görmek için: Query'ye
onlyDeleted: trueparametresi 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'siuserId: Silme işlemini yapan kullanıcı ID'si
İşlem Adımları:
- ID geçerliliği kontrol edilir
- Live stream bulunur ve populate edilir
- Plugin'in
softDelete()metodu çağrılır - 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:
- Live Stream Kontrolü: Live stream ID'si geçerli olmalı ve bulunmalı
- Zaten Silinmiş Kontrolü: Zaten soft delete edilmiş yayınlar tekrar silinemez
- Yetki Kontrolü: Sadece yayını oluşturan kişi (creator) silebilir
- 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
- Kullanıcı yayını sonlandırır (
status: ENDED) - Kullanıcı yayını silmek ister
- Sistem validasyonları kontrol eder:
- Kullanıcı creator mı? ✓
- Yayın aktif mi? ✗ (ENDED olmalı)
- Zaten silinmiş mi? ✗
- Soft delete işlemi gerçekleştirilir
deletedAtvedeletedByalanları set edilir
Senaryo 2: Aktif Yayın Silme Denemesi
- Kullanıcı aktif bir yayını silmeye çalışır
- Sistem
status === ACTIVEkontrolü yapar 409 Conflicthatası döner: "Aktif yayınlar silinemez. Önce yayını sonlandırın."
Senaryo 3: Yetkisiz Silme Denemesi
- Başka bir kullanıcı yayını silmeye çalışır
- Sistem creator kontrolü yapar
403 Forbiddenhatası 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:
deletedAtalanınınullyapardeletedByalanınınullyapar- 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
-
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.
-
Otomatik Filtreleme: Normal sorgular otomatik olarak
deletedAt: nullfiltresi ekler. Bu davranışı değiştirmek içinshowDeleted: truekullanılmalıdır. -
Populate Sonrası:
softDelete()veyarestore()çağrıldıktan sonrasave()işlemi populate edilmiş referansları kaybedebilir. Bu yüzden repository metodlarında tekrar populate edilir. -
Aktif Yayınlar: Aktif yayınlar soft delete edilemez. Önce
end-my-live-streamendpoint'i ile yayın sonlandırılmalıdır. -
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.
-
Audit Trail:
deletedByalanı 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