fix: use JdbcTemplate for audit queries (match server pattern)
Replace JPQL @Query with dynamic SQL via JdbcTemplate to avoid Hibernate null parameter type issues (bytea vs text). Conditionally appends WHERE clauses only for non-null filters, matching the proven pattern from cameleer3-server's PostgresAuditRepository. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,6 @@
|
||||
package net.siegeln.cameleer.saas.audit;
|
||||
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.time.Instant;
|
||||
@@ -12,31 +8,9 @@ import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Repository
|
||||
public interface AuditRepository extends JpaRepository<AuditEntity, UUID> {
|
||||
public interface AuditRepository extends JpaRepository<AuditEntity, UUID>, AuditRepositoryCustom {
|
||||
|
||||
List<AuditEntity> findByTenantIdAndCreatedAtBetween(UUID tenantId, Instant from, Instant to);
|
||||
|
||||
List<AuditEntity> findByActorId(UUID actorId);
|
||||
|
||||
@Query("""
|
||||
SELECT a FROM AuditEntity a
|
||||
WHERE (:tenantId IS NULL OR a.tenantId = :tenantId)
|
||||
AND (:action IS NULL OR a.action = :action)
|
||||
AND (:result IS NULL OR a.result = :result)
|
||||
AND (:from IS NULL OR a.createdAt >= :from)
|
||||
AND (:to IS NULL OR a.createdAt <= :to)
|
||||
AND (:search = ''
|
||||
OR LOWER(a.actorEmail) LIKE LOWER(CONCAT('%', :search, '%'))
|
||||
OR LOWER(a.resource) LIKE LOWER(CONCAT('%', :search, '%')))
|
||||
ORDER BY a.createdAt DESC
|
||||
""")
|
||||
Page<AuditEntity> findFiltered(
|
||||
@Param("tenantId") UUID tenantId,
|
||||
@Param("action") String action,
|
||||
@Param("result") String result,
|
||||
@Param("from") Instant from,
|
||||
@Param("to") Instant to,
|
||||
@Param("search") String search,
|
||||
Pageable pageable
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package net.siegeln.cameleer.saas.audit;
|
||||
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface AuditRepositoryCustom {
|
||||
|
||||
Page<AuditDto.AuditLogEntry> findFiltered(UUID tenantId, String action, String result,
|
||||
Instant from, Instant to, String search,
|
||||
Pageable pageable);
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package net.siegeln.cameleer.saas.audit;
|
||||
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageImpl;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Timestamp;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Component
|
||||
public class AuditRepositoryImpl implements AuditRepositoryCustom {
|
||||
|
||||
private final JdbcTemplate jdbc;
|
||||
|
||||
public AuditRepositoryImpl(JdbcTemplate jdbc) {
|
||||
this.jdbc = jdbc;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Page<AuditDto.AuditLogEntry> findFiltered(UUID tenantId, String action, String result,
|
||||
Instant from, Instant to, String search,
|
||||
Pageable pageable) {
|
||||
StringBuilder where = new StringBuilder("WHERE 1=1");
|
||||
List<Object> params = new ArrayList<>();
|
||||
|
||||
if (tenantId != null) {
|
||||
where.append(" AND tenant_id = ?");
|
||||
params.add(tenantId);
|
||||
}
|
||||
if (action != null && !action.isBlank()) {
|
||||
where.append(" AND action = ?");
|
||||
params.add(action);
|
||||
}
|
||||
if (result != null && !result.isBlank()) {
|
||||
where.append(" AND result = ?");
|
||||
params.add(result);
|
||||
}
|
||||
if (from != null) {
|
||||
where.append(" AND created_at >= ?");
|
||||
params.add(Timestamp.from(from));
|
||||
}
|
||||
if (to != null) {
|
||||
where.append(" AND created_at <= ?");
|
||||
params.add(Timestamp.from(to));
|
||||
}
|
||||
if (search != null && !search.isBlank()) {
|
||||
where.append(" AND (actor_email ILIKE ? OR resource ILIKE ?)");
|
||||
String like = "%" + search + "%";
|
||||
params.add(like);
|
||||
params.add(like);
|
||||
}
|
||||
|
||||
String countSql = "SELECT COUNT(*) FROM audit_log " + where;
|
||||
Long total = jdbc.queryForObject(countSql, Long.class, params.toArray());
|
||||
long totalCount = total != null ? total : 0;
|
||||
|
||||
String dataSql = "SELECT * FROM audit_log " + where
|
||||
+ " ORDER BY created_at DESC LIMIT ? OFFSET ?";
|
||||
List<Object> dataParams = new ArrayList<>(params);
|
||||
dataParams.add(pageable.getPageSize());
|
||||
dataParams.add(pageable.getOffset());
|
||||
|
||||
List<AuditDto.AuditLogEntry> items = jdbc.query(dataSql, (rs, rowNum) -> mapRow(rs), dataParams.toArray());
|
||||
return new PageImpl<>(items, pageable, totalCount);
|
||||
}
|
||||
|
||||
private AuditDto.AuditLogEntry mapRow(ResultSet rs) throws SQLException {
|
||||
Timestamp ts = rs.getTimestamp("created_at");
|
||||
return new AuditDto.AuditLogEntry(
|
||||
rs.getObject("id", UUID.class),
|
||||
rs.getString("actor_email"),
|
||||
rs.getObject("tenant_id", UUID.class),
|
||||
rs.getString("action"),
|
||||
rs.getString("resource"),
|
||||
rs.getString("environment"),
|
||||
rs.getString("result"),
|
||||
rs.getString("source_ip"),
|
||||
ts != null ? ts.toInstant() : null
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -34,10 +34,9 @@ public class AuditService {
|
||||
auditRepository.save(entry);
|
||||
}
|
||||
|
||||
public Page<AuditEntity> search(UUID tenantId, String action, String result,
|
||||
Instant from, Instant to, String search,
|
||||
Pageable pageable) {
|
||||
String safeSearch = (search != null && !search.isBlank()) ? search.trim() : "";
|
||||
return auditRepository.findFiltered(tenantId, action, result, from, to, safeSearch, pageable);
|
||||
public Page<AuditDto.AuditLogEntry> search(UUID tenantId, String action, String result,
|
||||
Instant from, Instant to, String search,
|
||||
Pageable pageable) {
|
||||
return auditRepository.findFiltered(tenantId, action, result, from, to, search, pageable);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package net.siegeln.cameleer.saas.portal;
|
||||
|
||||
import net.siegeln.cameleer.saas.audit.AuditDto.AuditLogEntry;
|
||||
import net.siegeln.cameleer.saas.audit.AuditDto.AuditLogPage;
|
||||
import net.siegeln.cameleer.saas.audit.AuditService;
|
||||
import net.siegeln.cameleer.saas.config.TenantContext;
|
||||
@@ -39,12 +38,8 @@ public class TenantAuditController {
|
||||
var pageResult = auditService.search(tenantId, action, result, from, to, search,
|
||||
PageRequest.of(page, size));
|
||||
|
||||
var entries = pageResult.getContent().stream()
|
||||
.map(AuditLogEntry::from)
|
||||
.toList();
|
||||
|
||||
return ResponseEntity.ok(new AuditLogPage(
|
||||
entries, pageResult.getNumber(), pageResult.getSize(),
|
||||
pageResult.getContent(), pageResult.getNumber(), pageResult.getSize(),
|
||||
pageResult.getTotalElements(), pageResult.getTotalPages()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package net.siegeln.cameleer.saas.vendor;
|
||||
|
||||
import net.siegeln.cameleer.saas.audit.AuditDto.AuditLogEntry;
|
||||
import net.siegeln.cameleer.saas.audit.AuditDto.AuditLogPage;
|
||||
import net.siegeln.cameleer.saas.audit.AuditService;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
@@ -40,12 +39,8 @@ public class VendorAuditController {
|
||||
var pageResult = auditService.search(tenantId, action, result, from, to, search,
|
||||
PageRequest.of(page, size));
|
||||
|
||||
var entries = pageResult.getContent().stream()
|
||||
.map(AuditLogEntry::from)
|
||||
.toList();
|
||||
|
||||
return ResponseEntity.ok(new AuditLogPage(
|
||||
entries, pageResult.getNumber(), pageResult.getSize(),
|
||||
pageResult.getContent(), pageResult.getNumber(), pageResult.getSize(),
|
||||
pageResult.getTotalElements(), pageResult.getTotalPages()));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user