mirror of
https://github.com/iflytek/skillhub.git
synced 2026-09-07 08:26:00 +00:00
fix(namespace): bound filtered namespace queries
Signed-off-by: dongmucat <1127093059@qq.com>
This commit is contained in:
parent
2fa7a53e2b
commit
a55264b130
4 changed files with 221 additions and 0 deletions
|
|
@ -163,6 +163,50 @@ public class NamespacePortalQueryAppService {
|
|||
return PageResponse.from(responsePage);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public PageResponse<MyNamespaceResponse> listMyNamespaces(Pageable pageable,
|
||||
Map<Long, NamespaceRole> userNamespaceRoles,
|
||||
Set<String> platformRoles,
|
||||
NamespaceStatus status,
|
||||
String query,
|
||||
String slug,
|
||||
Set<NamespaceRole> roles) {
|
||||
Map<Long, NamespaceRole> namespaceRoles = userNamespaceRoles != null ? userNamespaceRoles : Map.of();
|
||||
Set<NamespaceRole> requestedRoles = roles != null ? roles : Set.of();
|
||||
Pageable boundedPageable = normalizeMyNamespacesPageable(pageable);
|
||||
String normalizedQuery = normalizeFilter(query);
|
||||
String normalizedSlug = normalizeFilter(slug);
|
||||
|
||||
if (isSuperAdmin(platformRoles) && requestedRoles.isEmpty()) {
|
||||
Page<Namespace> visibleNamespaces = namespaceRepository.search(
|
||||
status,
|
||||
normalizedQuery,
|
||||
normalizedSlug,
|
||||
boundedPageable
|
||||
);
|
||||
return PageResponse.from(visibleNamespaces.map(namespace -> myNamespaceResponse(namespace, namespaceRoles)));
|
||||
}
|
||||
|
||||
List<Long> scopedNamespaceIds = namespaceRoles.entrySet().stream()
|
||||
.filter(entry -> requestedRoles.isEmpty() || requestedRoles.contains(entry.getValue()))
|
||||
.map(Map.Entry::getKey)
|
||||
.sorted()
|
||||
.toList();
|
||||
if (scopedNamespaceIds.isEmpty()) {
|
||||
Page<MyNamespaceResponse> empty = new PageImpl<>(List.of(), boundedPageable, 0);
|
||||
return PageResponse.from(empty);
|
||||
}
|
||||
|
||||
Page<Namespace> visibleNamespaces = namespaceRepository.searchByIdIn(
|
||||
scopedNamespaceIds,
|
||||
status,
|
||||
normalizedQuery,
|
||||
normalizedSlug,
|
||||
boundedPageable
|
||||
);
|
||||
return PageResponse.from(visibleNamespaces.map(namespace -> myNamespaceResponse(namespace, namespaceRoles)));
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public NamespaceResponse getNamespace(String slug, String userId, Map<Long, NamespaceRole> userNamespaceRoles) {
|
||||
return getNamespace(slug, userId, userNamespaceRoles, Set.of());
|
||||
|
|
@ -253,6 +297,13 @@ public class NamespacePortalQueryAppService {
|
|||
return PageRequest.of(page, size, Sort.by(NAMESPACE_SLUG_SORT).ascending());
|
||||
}
|
||||
|
||||
private String normalizeFilter(String value) {
|
||||
if (value == null || value.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
private boolean isSuperAdmin(Set<String> platformRoles) {
|
||||
return platformRoles != null && platformRoles.contains(SUPER_ADMIN_ROLE);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@ import static org.mockito.ArgumentMatchers.anyList;
|
|||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.iflytek.skillhub.domain.namespace.Namespace;
|
||||
|
|
@ -145,6 +148,127 @@ class NamespacePortalQueryAppServiceTest {
|
|||
assertThat(response.size()).isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void listMyNamespaces_superAdminWithRequestedRolesSearchesOnlyMatchingMembershipIds() {
|
||||
Namespace owned = namespace(1L, "team-ai");
|
||||
Pageable expectedPageable = PageRequest.of(0, 20);
|
||||
when(namespaceRepository.searchByIdIn(
|
||||
eq(List.of(1L)),
|
||||
eq(NamespaceStatus.ACTIVE),
|
||||
eq("team"),
|
||||
eq("team-ai"),
|
||||
any(Pageable.class)
|
||||
)).thenReturn(new PageImpl<>(List.of(owned), expectedPageable, 1));
|
||||
|
||||
var response = service.listMyNamespaces(
|
||||
expectedPageable,
|
||||
Map.of(1L, NamespaceRole.OWNER, 2L, NamespaceRole.MEMBER),
|
||||
Set.of("SUPER_ADMIN"),
|
||||
NamespaceStatus.ACTIVE,
|
||||
" team ",
|
||||
" team-ai ",
|
||||
Set.of(NamespaceRole.OWNER, NamespaceRole.ADMIN)
|
||||
);
|
||||
|
||||
assertThat(response.items()).extracting("slug").containsExactly("team-ai");
|
||||
assertThat(response.items()).extracting("currentUserRole").containsExactly(NamespaceRole.OWNER);
|
||||
verify(namespaceRepository).searchByIdIn(
|
||||
eq(List.of(1L)),
|
||||
eq(NamespaceStatus.ACTIVE),
|
||||
eq("team"),
|
||||
eq("team-ai"),
|
||||
any(Pageable.class)
|
||||
);
|
||||
verify(namespaceRepository, never()).search(any(), any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void listMyNamespaces_superAdminWithoutRequestedRolesUsesUnrestrictedFilteredSearch() {
|
||||
Namespace archived = namespace(2L, "ops-team");
|
||||
archived.setStatus(NamespaceStatus.ARCHIVED);
|
||||
Pageable expectedPageable = PageRequest.of(1, 10);
|
||||
when(namespaceRepository.search(
|
||||
eq(NamespaceStatus.ARCHIVED),
|
||||
eq("ops"),
|
||||
eq("ops-team"),
|
||||
any(Pageable.class)
|
||||
)).thenReturn(new PageImpl<>(List.of(archived), expectedPageable, 11));
|
||||
|
||||
var response = service.listMyNamespaces(
|
||||
expectedPageable,
|
||||
Map.of(),
|
||||
Set.of("SUPER_ADMIN"),
|
||||
NamespaceStatus.ARCHIVED,
|
||||
" ops ",
|
||||
" ops-team ",
|
||||
Set.of()
|
||||
);
|
||||
|
||||
assertThat(response.items()).extracting("slug").containsExactly("ops-team");
|
||||
assertThat(response.total()).isEqualTo(11);
|
||||
verify(namespaceRepository).search(
|
||||
eq(NamespaceStatus.ARCHIVED),
|
||||
eq("ops"),
|
||||
eq("ops-team"),
|
||||
any(Pageable.class)
|
||||
);
|
||||
verify(namespaceRepository, never()).searchByIdIn(anyList(), any(), any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void listMyNamespaces_nonSuperAdminWithoutRequestedRolesSearchesAllMembershipIds() {
|
||||
Namespace member = namespace(1L, "member-team");
|
||||
Namespace administered = namespace(2L, "admin-team");
|
||||
when(namespaceRepository.searchByIdIn(
|
||||
eq(List.of(1L, 2L)),
|
||||
eq(null),
|
||||
eq(null),
|
||||
eq(null),
|
||||
any(Pageable.class)
|
||||
)).thenReturn(new PageImpl<>(List.of(administered, member), PageRequest.of(0, 20), 2));
|
||||
|
||||
var response = service.listMyNamespaces(
|
||||
PageRequest.of(0, 20),
|
||||
Map.of(2L, NamespaceRole.ADMIN, 1L, NamespaceRole.MEMBER),
|
||||
Set.of(),
|
||||
null,
|
||||
" ",
|
||||
"\t",
|
||||
Set.of()
|
||||
);
|
||||
|
||||
assertThat(response.items()).extracting("slug").containsExactly("admin-team", "member-team");
|
||||
assertThat(response.items()).extracting("currentUserRole")
|
||||
.containsExactly(NamespaceRole.ADMIN, NamespaceRole.MEMBER);
|
||||
verify(namespaceRepository).searchByIdIn(
|
||||
eq(List.of(1L, 2L)),
|
||||
eq(null),
|
||||
eq(null),
|
||||
eq(null),
|
||||
any(Pageable.class)
|
||||
);
|
||||
verify(namespaceRepository, never()).search(any(), any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void listMyNamespaces_emptyRoleRestrictedScopeReturnsEmptyPageWithoutRepositoryQuery() {
|
||||
var response = service.listMyNamespaces(
|
||||
PageRequest.of(2, 10),
|
||||
Map.of(1L, NamespaceRole.MEMBER),
|
||||
Set.of("SUPER_ADMIN"),
|
||||
NamespaceStatus.ACTIVE,
|
||||
" team ",
|
||||
null,
|
||||
Set.of(NamespaceRole.OWNER, NamespaceRole.ADMIN)
|
||||
);
|
||||
|
||||
assertThat(response.items()).isEmpty();
|
||||
assertThat(response.total()).isZero();
|
||||
assertThat(response.page()).isEqualTo(2);
|
||||
assertThat(response.size()).isEqualTo(10);
|
||||
verifyNoInteractions(namespaceRepository);
|
||||
}
|
||||
|
||||
@Test
|
||||
void listMyNamespaces_superAdminCompatibilityCollectsAllRepositoryPages() {
|
||||
Namespace first = namespace(1L, "first");
|
||||
|
|
|
|||
|
|
@ -15,6 +15,14 @@ public interface NamespaceRepository {
|
|||
Optional<Namespace> findBySlug(String slug);
|
||||
Page<Namespace> findAll(Pageable pageable);
|
||||
Page<Namespace> findByStatus(NamespaceStatus status, Pageable pageable);
|
||||
Page<Namespace> search(NamespaceStatus status, String query, String slug, Pageable pageable);
|
||||
Page<Namespace> searchByIdIn(
|
||||
List<Long> ids,
|
||||
NamespaceStatus status,
|
||||
String query,
|
||||
String slug,
|
||||
Pageable pageable
|
||||
);
|
||||
Namespace save(Namespace namespace);
|
||||
void delete(Namespace namespace);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import com.iflytek.skillhub.domain.namespace.NamespaceStatus;
|
|||
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.util.List;
|
||||
|
|
@ -20,4 +22,40 @@ public interface NamespaceJpaRepository
|
|||
List<Namespace> findByIdIn(List<Long> ids);
|
||||
Optional<Namespace> findBySlug(String slug);
|
||||
Page<Namespace> findByStatus(NamespaceStatus status, Pageable pageable);
|
||||
|
||||
@Override
|
||||
@Query("""
|
||||
SELECT n
|
||||
FROM Namespace n
|
||||
WHERE (:status IS NULL OR n.status = :status)
|
||||
AND (
|
||||
:query IS NULL
|
||||
OR lower(n.slug) LIKE lower(concat('%', :query, '%'))
|
||||
OR lower(n.displayName) LIKE lower(concat('%', :query, '%'))
|
||||
)
|
||||
AND (:slug IS NULL OR n.slug = :slug)
|
||||
""")
|
||||
Page<Namespace> search(@Param("status") NamespaceStatus status,
|
||||
@Param("query") String query,
|
||||
@Param("slug") String slug,
|
||||
Pageable pageable);
|
||||
|
||||
@Override
|
||||
@Query("""
|
||||
SELECT n
|
||||
FROM Namespace n
|
||||
WHERE n.id IN :ids
|
||||
AND (:status IS NULL OR n.status = :status)
|
||||
AND (
|
||||
:query IS NULL
|
||||
OR lower(n.slug) LIKE lower(concat('%', :query, '%'))
|
||||
OR lower(n.displayName) LIKE lower(concat('%', :query, '%'))
|
||||
)
|
||||
AND (:slug IS NULL OR n.slug = :slug)
|
||||
""")
|
||||
Page<Namespace> searchByIdIn(@Param("ids") List<Long> ids,
|
||||
@Param("status") NamespaceStatus status,
|
||||
@Param("query") String query,
|
||||
@Param("slug") String slug,
|
||||
Pageable pageable);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue