feat(dict): soft-delete с recovery (Phase 1)

This commit is contained in:
Александр Зимин
2026-06-08 14:27:34 +00:00
parent 4516559e21
commit 0ccdff95d2
6 changed files with 217 additions and 43 deletions
@@ -112,6 +112,22 @@ public class DictionaryDefinition {
@Column(name = "version")
private Long version;
/**
* Soft-delete timestamp. NULL = active dictionary. Non-null = deleted (set
* by SchemaDeletionApproval after 2nd admin approve). Records/drafts
* остаются физически в БД (recovery возможен через restore()).
*
* <p>Permanent purge (drop row + cascade на child tables) — отдельный
* admin-only endpoint после approval+30d retention. По умолчанию
* list/findByName excludeает deleted, restore сбрасывает в NULL.
*/
@Column(name = "deleted_at")
private OffsetDateTime deletedAt;
/** Admin sub claim that approved the deletion (audit trail). */
@Column(name = "deleted_by", length = 64)
private String deletedBy;
protected DictionaryDefinition() {}
public DictionaryDefinition(UUID id, String name, DataScope scope, JsonNode schemaJson) {
@@ -139,6 +155,19 @@ public class DictionaryDefinition {
public String getCreatedBy() { return createdBy; }
public String getUpdatedBy() { return updatedBy; }
public Long getVersion() { return version; }
public OffsetDateTime getDeletedAt() { return deletedAt; }
public String getDeletedBy() { return deletedBy; }
public boolean isDeleted() { return deletedAt != null; }
public void markDeleted(String approverSub) {
this.deletedAt = OffsetDateTime.now();
this.deletedBy = approverSub;
}
public void restore() {
this.deletedAt = null;
this.deletedBy = null;
}
public void setDisplayName(String v) { this.displayName = v; }
public void setDescription(String v) { this.description = v; }
@@ -9,7 +9,33 @@ import java.util.UUID;
public interface DictionaryDefinitionRepository extends JpaRepository<DictionaryDefinition, UUID> {
Optional<DictionaryDefinition> findByName(String name);
/**
* Active (non-deleted) dict by name. Soft-deleted (deleted_at IS NOT NULL)
* excluded — выглядит как 404 для regular consumers. Admin Корзина UI
* использует {@link #findByNameIncludingDeleted} для restore flow.
*/
@org.springframework.data.jpa.repository.Query(
"SELECT d FROM DictionaryDefinition d WHERE d.name = :name AND d.deletedAt IS NULL")
Optional<DictionaryDefinition> findByName(@org.springframework.data.repository.query.Param("name") String name);
/** Includes soft-deleted — для admin restore/purge flows. */
@org.springframework.data.jpa.repository.Query(
"SELECT d FROM DictionaryDefinition d WHERE d.name = :name")
Optional<DictionaryDefinition> findByNameIncludingDeleted(@org.springframework.data.repository.query.Param("name") String name);
/**
* All active (non-deleted) dictionaries. JpaRepository.findAll() возвращает
* ВСЕ rows (включая deleted) — controllers должны использовать этот метод
* для regular list.
*/
@org.springframework.data.jpa.repository.Query(
"SELECT d FROM DictionaryDefinition d WHERE d.deletedAt IS NULL")
List<DictionaryDefinition> findAllActive();
/** Soft-deleted only — admin Корзина page. */
@org.springframework.data.jpa.repository.Query(
"SELECT d FROM DictionaryDefinition d WHERE d.deletedAt IS NOT NULL ORDER BY d.deletedAt DESC")
List<DictionaryDefinition> findAllDeleted();
List<DictionaryDefinition> findByBundle(String bundle);