Skip to content

SoftDeletableRepository

Tổng quan

SoftDeletableRepository là một lớp repository cơ sở triển khai mẫu xóa mềm. Thay vì xóa vật lý các bản ghi khỏi cơ sở dữ liệu, các thao tác xóa đặt dấu thời gian deletedAt. Các bản ghi có thể được khôi phục sau đó hoặc xóa cứng vĩnh viễn.

Tất cả repository thực thể nghiệp vụ trong hệ thống BANA đều kế thừa lớp này.

Nguồn: packages/core/src/base/repository/soft-deletable.repository.ts

Xem tham chiếu IGNIS Repositories để biết chi tiết về lớp cha DefaultCRUDRepository.

Định nghĩa Lớp

typescript
import { DefaultCRUDRepository, IExtraOptions, TTableObject, TTableInsert } from '@venizia/ignis';

export type TSoftDeletableTableSchema = TTableSchemaWithId & {
  deletedAt: AnyPgColumn<{ data: Date | null }>;
};

export class SoftDeletableRepository<
  EntitySchema extends TSoftDeletableTableSchema,
  DataObject extends TTableObject<EntitySchema> = TTableObject<EntitySchema>,
  PersistObject extends TTableInsert<EntitySchema> = TTableInsert<EntitySchema>,
  ExtraOptions extends IExtraOptions = IExtraOptions,
> extends DefaultCRUDRepository<EntitySchema, DataObject, PersistObject, ExtraOptions> {

  // Read (overridden)
  findById(opts): Promise<R | null>;       // with isStrict option

  // Delete (overridden — soft delete by default)
  deleteById(opts): Promise<TCount & { data: R }>;
  deleteAll(opts): Promise<TCount & { data: R[] }>;
  deleteBy(opts): Promise<TCount & { data: R[] }>;

  // Restore (new methods)
  restoreById(opts): Promise<TCount & { data: R }>;
  restoreAll(opts): Promise<TCount & { data: R[] }>;
  restoreBy(opts): Promise<TCount & { data: R[] }>;
}

Phân cấp Kế thừa

Sơ đồ Trạng thái Bản ghi

Luồng Xóa Mềm

Yêu cầu Model

Để một model hoạt động với SoftDeletableRepository, nó phải đáp ứng ba yêu cầu:

1. Schema phải bao gồm cột deletedAt

typescript
import { pgTable, text, timestamp } from 'drizzle-orm/pg-core';
import { generateCommonColumnDefs } from '@nx/core';

export const CategorySchema = pgTable('Category', {
  ...generateCommonColumnDefs(), // includes id, createdAt, modifiedAt, deletedAt
  name: text('name').notNull(),
  // ...
});

2. Model phải đặt defaultFilter để loại trừ bản ghi đã xóa

typescript
import { BaseEntity, model } from '@venizia/ignis';

@model({
  type: 'entity',
  settings: {
    defaultFilter: { where: { deletedAt: null } },
    hiddenProperties: ['createdAt', 'modifiedAt', 'deletedAt'],
  },
})
export class Category extends BaseEntity<typeof CategorySchema> {
  static override TABLE_NAME = 'Category';
  static override schema = CategorySchema;
}

3. Repository phải kế thừa SoftDeletableRepository

typescript
import { repository } from '@venizia/ignis';
import { SoftDeletableRepository } from '@nx/core';
import { PostgresCoreDataSource } from '@/datasources';

@repository({ dataSource: PostgresCoreDataSource, model: Category })
export class CategoryRepository extends SoftDeletableRepository<
  typeof CategorySchema,
  TCategory,
  TCategoryPersist
> {}

Tham chiếu API

Thao tác Đọc

findById

Ghi đè phương thức cha để thêm chế độ nghiêm ngặt. Khi isStrict: true, lỗi 404 Not Found được ném ra nếu bản ghi không tồn tại.

typescript
// Non-strict (default) — returns null if not found
const product = await repository.findById({
  id: 'product-123',
});
// Returns: TProduct | null

// Strict mode — throws 404 if not found
const product = await repository.findById({
  id: 'product-123',
  options: { isStrict: true },
});
// Returns: TProduct (or throws HTTP 404)

Chữ ký phương thức:

typescript
// Non-strict overload
findById<R = DataObject>(opts: {
  id: IdType;
  filter?: Omit<TFilter<DataObject>, 'where'>;
  options?: ExtraOptions & { isStrict?: false };
}): Promise<TNullable<R>>;

// Strict overload
findById<R = DataObject>(opts: {
  id: IdType;
  filter?: Omit<TFilter<DataObject>, 'where'>;
  options?: ExtraOptions & { isStrict?: true };
}): Promise<R>;

Thao tác Xóa

Tất cả phương thức xóa thực hiện xóa mềm theo mặc định (đặt deletedAt = new Date()). Truyền shouldHardDelete: true để xóa vĩnh viễn bản ghi thay thế.

deleteById

Xóa mềm một bản ghi theo ID.

typescript
// Soft delete (default)
const result = await repository.deleteById({
  id: 'product-123',
});
// result: { count: 1, data: { id: 'product-123', deletedAt: '2026-01-20T...' } }

// Without returning data
const result = await repository.deleteById({
  id: 'product-123',
  options: { shouldReturn: false },
});
// result: { count: 1, data: null }

// Hard delete (permanent removal)
const result = await repository.deleteById({
  id: 'product-123',
  options: { shouldHardDelete: true },
});

Chữ ký phương thức:

typescript
deleteById(opts: {
  id: IdType;
  options: ExtraOptions & { shouldReturn: false; shouldHardDelete?: boolean };
}): Promise<TCount & { data: undefined | null }>;

deleteById<R = DataObject>(opts: {
  id: IdType;
  options?: ExtraOptions & { shouldReturn?: true; shouldHardDelete?: boolean };
}): Promise<TCount & { data: R }>;

deleteAll

Xóa mềm tất cả bản ghi khớp với bộ lọc where.

typescript
// Soft delete by filter
const result = await repository.deleteAll({
  where: { merchantId: 'merchant-123' },
});
// result: { count: 5, data: [...softDeletedRecords] }

// Without filter (requires force: true)
const result = await repository.deleteAll({
  where: {},
  options: { force: true },
});

// Hard delete
const result = await repository.deleteAll({
  where: { status: 'archived' },
  options: { shouldHardDelete: true },
});

Chữ ký phương thức:

typescript
deleteAll(opts: {
  where?: TWhere<DataObject>;
  options: ExtraOptions & { shouldReturn: false; force?: boolean; shouldHardDelete?: boolean };
}): Promise<TCount & { data: undefined | null }>;

deleteAll<R = DataObject>(opts: {
  where?: TWhere<DataObject>;
  options?: ExtraOptions & { shouldReturn?: true; force?: boolean; shouldHardDelete?: boolean };
}): Promise<TCount & { data: Array<R> }>;

deleteBy

Xóa mềm các bản ghi khớp với điều kiện cụ thể. Hành vi giống hệt deleteAll nhưng tham số where là bắt buộc.

typescript
const result = await repository.deleteBy({
  where: { status: 'inactive', merchantId: 'merchant-123' },
});

Chữ ký phương thức:

typescript
deleteBy(opts: {
  where: TWhere<DataObject>;
  options: ExtraOptions & { shouldReturn: false; force?: boolean; shouldHardDelete?: boolean };
}): Promise<TCount & { data: undefined | null }>;

deleteBy<R = DataObject>(opts: {
  where: TWhere<DataObject>;
  options?: ExtraOptions & { shouldReturn?: true; force?: boolean; shouldHardDelete?: boolean };
}): Promise<TCount & { data: Array<R> }>;

Thao tác Khôi phục

Các phương thức khôi phục đặt deletedAt = null trên các bản ghi đã xóa mềm. Chúng sử dụng shouldSkipDefaultFilter: true nội bộ để có thể tìm các bản ghi bị loại trừ bởi defaultFilter.

restoreById

Khôi phục một bản ghi đã xóa mềm theo ID.

typescript
const result = await repository.restoreById({
  id: 'product-123',
});
// result: { count: 1, data: { id: 'product-123', deletedAt: null, ... } }

// Without returning data
const result = await repository.restoreById({
  id: 'product-123',
  options: { shouldReturn: false },
});

Chữ ký phương thức:

typescript
restoreById<R = DataObject>(opts: {
  id: IdType;
  options?: ExtraOptions & { shouldReturn?: boolean };
}): Promise<TCount & { data: TNullable<R> }>;

restoreAll

Khôi phục tất cả bản ghi đã xóa mềm khớp với bộ lọc.

typescript
const result = await repository.restoreAll({
  where: { merchantId: 'merchant-123' },
});
// Restores all soft-deleted records for that merchant

// Without filter (requires force: true)
const result = await repository.restoreAll({
  where: {},
  options: { force: true },
});

Chữ ký phương thức:

typescript
restoreAll<R = DataObject>(opts: {
  where?: TWhere<DataObject>;
  options?: ExtraOptions & { shouldReturn?: boolean; force?: boolean };
}): Promise<TCount & { data: TNullable<Array<R>> }>;

restoreBy

Ủy quyền cho restoreAll. Được cung cấp để nhất quán API với deleteBy.

typescript
const result = await repository.restoreBy({
  where: { merchantId: 'merchant-123', status: 'active' },
});

Chữ ký phương thức:

typescript
restoreBy<R = DataObject>(opts: {
  where: TWhere<DataObject>;
  options?: ExtraOptions & { shouldReturn?: boolean; force?: boolean };
}): Promise<TCount & { data: TNullable<Array<R>> }>;

Tóm tắt API

Phương thứcHành vi Mặc địnhTùy chọn Chính
findByIdTrả về null nếu không tìm thấyisStrict: true ném lỗi 404
deleteByIdĐặt deletedAt = new Date()shouldHardDelete: true để xóa vĩnh viễn
deleteAllĐặt deletedAt = new Date()shouldHardDelete, force
deleteByĐặt deletedAt = new Date()shouldHardDelete, force
restoreByIdĐặt deletedAt = nullshouldReturn
restoreAllĐặt deletedAt = nullshouldReturn, force
restoreByỦy quyền cho restoreAllshouldReturn, force

Ví dụ Sử dụng

Định nghĩa Repository Hoàn chỉnh

typescript
import { repository } from '@venizia/ignis';
import { SoftDeletableRepository } from '@nx/core';
import { PostgresCoreDataSource } from '@/datasources';
import { Category, TCategory, TCategoryPersist, TCategorySchema } from '@nx/core/models';

@repository({ dataSource: PostgresCoreDataSource, model: Category })
export class CategoryRepository extends SoftDeletableRepository<
  TCategorySchema,
  TCategory,
  TCategoryPersist
> {
  // Custom query — defaultFilter automatically excludes deleted records
  async findByMerchant(merchantId: string): Promise<TCategory[]> {
    return this.find({
      filter: { where: { merchantId } },
    });
  }

  // Query including soft-deleted records
  async findAllIncludingDeleted(merchantId: string): Promise<TCategory[]> {
    return this.find({
      filter: { where: { merchantId } },
      options: { shouldSkipDefaultFilter: true },
    });
  }
}

Service với Xóa Mềm

typescript
import { inject, BaseService } from '@venizia/ignis';

export class CategoryService extends BaseService {
  constructor(
    @inject({ key: 'repositories.CategoryRepository' })
    private categoryRepository: CategoryRepository,
  ) {
    super({ scope: CategoryService.name });
  }

  async deleteCategory(id: string) {
    // Soft delete — sets deletedAt, record remains in DB
    return this.categoryRepository.deleteById({ id });
  }

  async restoreCategory(id: string) {
    // Restore — clears deletedAt
    return this.categoryRepository.restoreById({ id });
  }

  async permanentlyDelete(id: string) {
    // Hard delete — physically removes the record
    return this.categoryRepository.deleteById({
      id,
      options: { shouldHardDelete: true },
    });
  }

  async getCategoryOrFail(id: string) {
    // Strict mode — throws 404 if not found or soft-deleted
    return this.categoryRepository.findById({
      id,
      options: { isStrict: true },
    });
  }
}

Transaction với Xóa Mềm

typescript
await this.categoryRepository.dataSource.withTransaction(async (tx) => {
  // Soft delete parent
  await this.categoryRepository.deleteById({
    id: parentId,
    options: { transaction: tx },
  });

  // Soft delete all children
  await this.categoryRepository.deleteAll({
    where: { parentId },
    options: { transaction: tx },
  });
});

Hành vi Truy vấn

Bộ lọc Mặc định

Khi defaultFilter: { where: { deletedAt: null } } được cấu hình trên model, tất cả truy vấn tự động loại trừ các bản ghi đã xóa mềm:

typescript
// Automatically excludes deleted records
const categories = await repository.find({
  filter: { where: { merchantId: 'merchant-123' } },
});
// SQL: SELECT * FROM "Category" WHERE merchant_id = ? AND deleted_at IS NULL

Bỏ qua Bộ lọc Mặc định

Sử dụng shouldSkipDefaultFilter: true để bao gồm các bản ghi đã xóa mềm:

typescript
// Includes all records regardless of deletedAt
const all = await repository.find({
  filter: { where: { merchantId: 'merchant-123' } },
  options: { shouldSkipDefaultFilter: true },
});
// SQL: SELECT * FROM "Category" WHERE merchant_id = ?

Truy vấn Chỉ Bản ghi Đã xóa

typescript
const deleted = await repository.find({
  filter: {
    where: {
      merchantId: 'merchant-123',
      deletedAt: { neq: null },
    },
  },
  options: { shouldSkipDefaultFilter: true },
});

Thực hành Tốt nhất

1. Luôn kế thừa SoftDeletableRepository cho thực thể nghiệp vụ

typescript
// Correct
@repository({ dataSource: PostgresCoreDataSource, model: Product })
export class ProductRepository extends SoftDeletableRepository<...> {}

// Avoid — no soft-delete, no restore, no isStrict
@repository({ dataSource: PostgresCoreDataSource, model: Product })
export class ProductRepository extends DefaultCRUDRepository<...> {}

2. Luôn cấu hình defaultFilter trên model

typescript
@model({
  type: 'entity',
  settings: {
    defaultFilter: { where: { deletedAt: null } },
    hiddenProperties: ['createdAt', 'modifiedAt', 'deletedAt'],
  },
})
export class Product extends BaseEntity<typeof ProductSchema> {}

3. Ưu tiên xóa mềm; dành xóa cứng cho dọn dẹp

typescript
// Normal operation — soft delete
await repository.deleteById({ id });

// Cleanup job — hard delete old soft-deleted records
await repository.deleteAll({
  where: { deletedAt: { lt: thirtyDaysAgo } },
  options: { shouldHardDelete: true, shouldSkipDefaultFilter: true, force: true },
});

4. Sử dụng isStrict cho tra cứu cấp controller

typescript
// Throws 404 automatically — no manual null checking needed
const product = await repository.findById({
  id: params.id,
  options: { isStrict: true },
});

Tài liệu Liên quan

Proprietary and Confidential. Unauthorized copying, distribution, or use of this software is strictly prohibited.