feat(api): GET /dictionaries/{name}/changelog — schema versions timeline (backend #1)

Backend #1 из pending-endpoints brief. Frontend HistoryTabContent +
TimeTravelModal используют этот endpoint для отображения версий схемы
во времени с per-version property-level diff summary.

Implementation:
- AuditLogger.definitionCreated / definitionUpdated — пишет в audit_log
  с event_type DEFINITION_CREATED / DEFINITION_UPDATED. payload_before/
  after = JsonNode schema. record_id / business_key — NULL (dict-level event).
- DictionaryDefinitionService.create / updateSchema hook в audit logger
  (autowired, null-safe для tests).
- AuditLogRepository.findByDictionaryIdAndEventTypes — query by dict_id +
  event_type IN (...), ORDER BY event_time DESC, paginated.
- ChangelogService.findChangelog — собирает entries, computes top-level
  property-key diff (added/removed/changed). Первая entry (newest) =
  isCurrent + schemaVersion из DictionaryDefinition; старые entries
  возвращают version=null (semver-в-аудит backfill — separate migration).
- DictionaryChangelogController — GET /dictionaries/{name}/changelog?limit=50

Errors:
- 404 dictionary_not_found когда dict missing OR scope hidden (consistent
  с другими endpoints).

Tests +9: dict-not-found, scope-hidden, empty-audit, first-is-current,
diff-added/removed/changed/null-before/identical.
This commit is contained in:
Zimin A.N.
2026-05-11 23:04:09 +03:00
parent ff05ef9a9f
commit ebc90d76e9
6 changed files with 486 additions and 4 deletions
@@ -14,6 +14,22 @@ public interface AuditLogRepository extends JpaRepository<AuditLog, Long> {
List<AuditLog> findByDictionaryIdOrderByEventTimeDesc(UUID dictionaryId);
/**
* Audit entries для конкретного dict, отфильтрованные по event_type IN
* списке. Используется ChangelogService — берёт только DEFINITION_*
* events, skip RECORD_* шум.
*/
@Query("""
SELECT a FROM AuditLog a
WHERE a.dictionaryId = :dictionaryId
AND a.eventType IN :eventTypes
ORDER BY a.eventTime DESC
""")
List<AuditLog> findByDictionaryIdAndEventTypes(
@Param("dictionaryId") UUID dictionaryId,
@Param("eventTypes") List<String> eventTypes,
Pageable pageable);
List<AuditLog> findByRecordIdOrderByEventTimeDesc(UUID recordId);
List<AuditLog> findByUserIdOrderByEventTimeDesc(String userId);