Cleanup code with SortableRequest (#6540)

#### What type of PR is this?

/kind cleanup
/kind improvement
/area core
/milestone 2.19.0

#### What this PR does / why we need it:

This PR refactors some requests with sort parameter by reusing SortableRequest, and refactors some queries with indexer.

#### Does this PR introduce a user-facing change?

```release-note
None
```
pull/6542/head
John Niang 2024-08-29 10:39:24 +08:00 committed by GitHub
parent 15a3e78e61
commit f61f846a7f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 452 additions and 753 deletions

View File

@ -1,5 +1,8 @@
package run.halo.app.extension;
import static org.springframework.data.domain.Sort.Order.asc;
import static org.springframework.data.domain.Sort.Order.desc;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
@ -53,8 +56,8 @@ public enum ExtensionUtil {
*/
public static Sort defaultSort() {
return Sort.by(
Sort.Order.desc("metadata.creationTimestamp"),
Sort.Order.asc("metadata.name")
desc("metadata.creationTimestamp"),
asc("metadata.name")
);
}

View File

@ -1,10 +1,9 @@
package run.halo.app.extension.router;
import static org.springframework.data.domain.Sort.Order.asc;
import static org.springframework.data.domain.Sort.Order.desc;
import static run.halo.app.extension.Comparators.compareCreationTimestamp;
import static run.halo.app.extension.Comparators.compareName;
import static run.halo.app.extension.Comparators.nullsComparator;
import static run.halo.app.extension.ExtensionUtil.defaultSort;
import static run.halo.app.extension.router.selector.SelectorUtil.labelAndFieldSelectorToListOptions;
import static run.halo.app.extension.router.selector.SelectorUtil.labelAndFieldSelectorToPredicate;
@ -43,9 +42,7 @@ public class SortableRequest extends IListRequest.QueryListRequest {
example = "metadata.creationTimestamp,desc"))
public Sort getSort() {
return SortResolver.defaultInstance.resolve(exchange)
.and(Sort.by(desc("metadata.creationTimestamp"),
asc("metadata.name"))
);
.and(defaultSort());
}
/**

View File

@ -1,27 +1,23 @@
package run.halo.app.content;
import static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;
import static run.halo.app.core.extension.content.Post.PUBLISHED_LABEL;
import static run.halo.app.core.extension.content.Post.PostPhase.PENDING_APPROVAL;
import static run.halo.app.extension.index.query.QueryFactory.contains;
import static run.halo.app.extension.index.query.QueryFactory.equal;
import static run.halo.app.extension.index.query.QueryFactory.or;
import static run.halo.app.extension.router.QueryParamBuildUtil.sortParameter;
import static run.halo.app.extension.router.selector.SelectorUtil.labelAndFieldSelectorToListOptions;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
import org.apache.commons.lang3.BooleanUtils;
import java.util.Optional;
import org.apache.commons.lang3.StringUtils;
import org.springdoc.core.fn.builders.operation.Builder;
import org.springframework.data.domain.Sort;
import org.springframework.lang.Nullable;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.server.ServerWebExchange;
import run.halo.app.core.extension.content.Post;
import run.halo.app.core.extension.endpoint.SortResolver;
import run.halo.app.extension.ListOptions;
import run.halo.app.extension.index.query.QueryFactory;
import run.halo.app.extension.router.IListRequest;
import run.halo.app.extension.router.selector.FieldSelector;
import run.halo.app.extension.router.selector.LabelSelector;
import run.halo.app.extension.router.SortableRequest;
/**
* A query object for {@link Post} list.
@ -29,9 +25,7 @@ import run.halo.app.extension.router.selector.LabelSelector;
* @author guqing
* @since 2.0.0
*/
public class PostQuery extends IListRequest.QueryListRequest {
private final ServerWebExchange exchange;
public class PostQuery extends SortableRequest {
private final String username;
@ -40,21 +34,13 @@ public class PostQuery extends IListRequest.QueryListRequest {
}
public PostQuery(ServerRequest request, @Nullable String username) {
super(request.queryParams());
this.exchange = request.exchange();
super(request.exchange());
this.username = username;
}
@Schema(hidden = true)
@JsonIgnore
public String getUsername() {
return username;
}
@Nullable
public Post.PostPhase getPublishPhase() {
String publishPhase = queryParams.getFirst("publishPhase");
return Post.PostPhase.from(publishPhase);
public String getPublishPhase() {
return queryParams.getFirst("publishPhase");
}
@Nullable
@ -64,72 +50,48 @@ public class PostQuery extends IListRequest.QueryListRequest {
}
@Nullable
@Schema(description = "Posts filtered by keyword.")
public String getKeyword() {
return StringUtils.defaultIfBlank(queryParams.getFirst("keyword"), null);
}
@ArraySchema(uniqueItems = true,
arraySchema = @Schema(name = "sort",
description = "Sort property and direction of the list result. Supported fields: "
+ "creationTimestamp,publishTime"),
schema = @Schema(description = "like field,asc or field,desc",
implementation = String.class,
example = "creationTimestamp,desc"))
public Sort getSort() {
var sort = SortResolver.defaultInstance.resolve(exchange);
sort = sort.and(Sort.by(Sort.Direction.DESC, "metadata.creationTimestamp"));
sort = sort.and(Sort.by(Sort.Direction.DESC, "metadata.name"));
return sort;
}
/**
* Build a list options from the query object.
*
* @return a list options
*/
@Override
public ListOptions toListOptions() {
var listOptions =
labelAndFieldSelectorToListOptions(getLabelSelector(), getFieldSelector());
if (listOptions.getFieldSelector() == null) {
listOptions.setFieldSelector(FieldSelector.all());
}
var labelSelectorBuilder = LabelSelector.builder();
var fieldQuery = QueryFactory.all();
var builder = ListOptions.builder(super.toListOptions());
String keyword = getKeyword();
if (keyword != null) {
fieldQuery = QueryFactory.and(fieldQuery, QueryFactory.or(
QueryFactory.contains("status.excerpt", keyword),
QueryFactory.contains("spec.slug", keyword),
QueryFactory.contains("spec.title", keyword)
));
}
Optional.ofNullable(getKeyword())
.filter(StringUtils::isNotBlank)
.ifPresent(keyword -> builder.andQuery(or(
contains("status.excerpt", keyword),
contains("spec.slug", keyword),
contains("spec.title", keyword)
)));
Post.PostPhase publishPhase = getPublishPhase();
if (publishPhase != null) {
if (Post.PostPhase.PENDING_APPROVAL.equals(publishPhase)) {
fieldQuery = QueryFactory.and(fieldQuery, QueryFactory.equal(
"status.phase", Post.PostPhase.PENDING_APPROVAL.name())
Optional.ofNullable(getPublishPhase())
.filter(StringUtils::isNotBlank)
.map(Post.PostPhase::from)
.ifPresent(phase -> {
if (PENDING_APPROVAL.equals(phase)) {
builder.andQuery(equal("status.phase", phase.name()));
}
var labelSelector = builder.labelSelector();
Optional.of(phase)
.filter(Post.PostPhase.PUBLISHED::equals)
.ifPresentOrElse(
published -> labelSelector.eq(PUBLISHED_LABEL, Boolean.TRUE.toString()),
() -> labelSelector.notEq(PUBLISHED_LABEL, Boolean.TRUE.toString())
);
labelSelectorBuilder.eq(Post.PUBLISHED_LABEL, BooleanUtils.FALSE);
} else if (Post.PostPhase.PUBLISHED.equals(publishPhase)) {
labelSelectorBuilder.eq(Post.PUBLISHED_LABEL, BooleanUtils.TRUE);
} else {
labelSelectorBuilder.eq(Post.PUBLISHED_LABEL, BooleanUtils.FALSE);
}
}
});
if (StringUtils.isNotBlank(username)) {
fieldQuery = QueryFactory.and(fieldQuery, QueryFactory.equal(
"spec.owner", username)
);
}
Optional.ofNullable(username)
.filter(StringUtils::isNotBlank)
.ifPresent(username -> builder.andQuery(equal("spec.owner", username)));
listOptions.setFieldSelector(listOptions.getFieldSelector().andQuery(fieldQuery));
listOptions.setLabelSelector(
listOptions.getLabelSelector().and(labelSelectorBuilder.build()));
return listOptions;
return builder.build();
}
public static void buildParameters(Builder builder) {

View File

@ -1,30 +1,25 @@
package run.halo.app.content;
import static java.util.Comparator.comparing;
import static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;
import static run.halo.app.core.extension.content.Post.PostPhase.PENDING_APPROVAL;
import static run.halo.app.core.extension.content.SinglePage.PUBLISHED_LABEL;
import static run.halo.app.extension.index.query.QueryFactory.equal;
import static run.halo.app.extension.index.query.QueryFactory.in;
import static run.halo.app.extension.index.query.QueryFactory.or;
import static run.halo.app.extension.router.QueryParamBuildUtil.sortParameter;
import static run.halo.app.extension.router.selector.SelectorUtil.labelAndFieldSelectorToPredicate;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.function.Predicate;
import java.util.Optional;
import org.apache.commons.lang3.StringUtils;
import org.springdoc.core.fn.builders.operation.Builder;
import org.springframework.data.domain.Sort;
import org.springframework.lang.Nullable;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.server.ServerWebExchange;
import run.halo.app.core.extension.content.Post;
import run.halo.app.core.extension.content.SinglePage;
import run.halo.app.core.extension.endpoint.SortResolver;
import run.halo.app.extension.Comparators;
import run.halo.app.extension.ListOptions;
import run.halo.app.extension.index.query.QueryFactory;
import run.halo.app.extension.router.IListRequest;
import run.halo.app.extension.router.SortableRequest;
/**
* Query parameter for {@link SinglePage} list.
@ -32,148 +27,67 @@ import run.halo.app.extension.router.IListRequest;
* @author guqing
* @since 2.0.0
*/
public class SinglePageQuery extends IListRequest.QueryListRequest {
private final ServerWebExchange exchange;
public class SinglePageQuery extends SortableRequest {
public SinglePageQuery(ServerRequest request) {
super(request.queryParams());
this.exchange = request.exchange();
super(request.exchange());
}
@Nullable
@Schema(name = "contributor")
public Set<String> getContributors() {
List<String> contributorList = queryParams.get("contributor");
return contributorList == null ? null : Set.copyOf(contributorList);
@Override
public ListOptions toListOptions() {
var builder = ListOptions.builder(super.toListOptions());
Optional.ofNullable(queryParams.getFirst("keyword"))
.filter(StringUtils::isNotBlank)
.ifPresent(keyword -> builder.andQuery(or(
QueryFactory.contains("spec.title", keyword),
QueryFactory.contains("spec.slug", keyword),
QueryFactory.contains("status.excerpt", keyword)
)));
Optional.ofNullable(queryParams.getFirst("publishPhase"))
.filter(StringUtils::isNotBlank)
.map(Post.PostPhase::from)
.ifPresent(phase -> {
if (PENDING_APPROVAL.equals(phase)) {
builder.andQuery(equal("status.phase", phase.name()));
}
var labelSelector = builder.labelSelector();
Optional.of(phase)
.filter(Post.PostPhase.PUBLISHED::equals)
.ifPresentOrElse(
published -> labelSelector.eq(PUBLISHED_LABEL, Boolean.TRUE.toString()),
() -> labelSelector.notEq(PUBLISHED_LABEL, Boolean.TRUE.toString())
);
});
Optional.ofNullable(queryParams.getFirst("visible"))
.filter(StringUtils::isNotBlank)
.map(Post.VisibleEnum::from)
.ifPresent(visible -> builder.andQuery(equal("spec.visible", visible.name())));
Optional.ofNullable(queryParams.get("contributor"))
.filter(contributors -> !contributors.isEmpty())
.ifPresent(contributors -> builder.andQuery(in("status.contributors", contributors)));
return builder.build();
}
@Nullable
public Post.PostPhase getPublishPhase() {
String publishPhase = queryParams.getFirst("publishPhase");
return Post.PostPhase.from(publishPhase);
}
@Nullable
public Post.VisibleEnum getVisible() {
String visible = queryParams.getFirst("visible");
return Post.VisibleEnum.from(visible);
}
@Nullable
@Schema(description = "SinglePages filtered by keyword.")
public String getKeyword() {
return StringUtils.defaultIfBlank(queryParams.getFirst("keyword"), null);
}
@ArraySchema(uniqueItems = true,
arraySchema = @Schema(name = "sort",
description = "Sort property and direction of the list result. Supported fields: "
+ "creationTimestamp,publishTime"),
schema = @Schema(description = "like field,asc or field,desc",
implementation = String.class,
example = "creationTimestamp,desc"))
@Override
public Sort getSort() {
return SortResolver.defaultInstance.resolve(exchange);
var sort = super.getSort();
var orders = sort.stream()
.map(order -> {
if ("creationTimestamp".equals(order.getProperty())) {
return order.withProperty("metadata.creationTimestamp");
}
/**
* Build a comparator for {@link SinglePageQuery}.
*
* @return comparator
*/
public Comparator<SinglePage> toComparator() {
var sort = getSort();
var creationTimestampOrder = sort.getOrderFor("creationTimestamp");
List<Comparator<SinglePage>> comparators = new ArrayList<>();
if (creationTimestampOrder != null) {
Comparator<SinglePage> comparator =
comparing(page -> page.getMetadata().getCreationTimestamp());
if (creationTimestampOrder.isDescending()) {
comparator = comparator.reversed();
if ("publishTime".equals(order.getProperty())) {
return order.withProperty("spec.publishTime");
}
comparators.add(comparator);
}
var publishTimeOrder = sort.getOrderFor("publishTime");
if (publishTimeOrder != null) {
Comparator<Object> nullsComparator = publishTimeOrder.isAscending()
? org.springframework.util.comparator.Comparators.nullsLow()
: org.springframework.util.comparator.Comparators.nullsHigh();
Comparator<SinglePage> comparator =
comparing(page -> page.getSpec().getPublishTime(), nullsComparator);
if (publishTimeOrder.isDescending()) {
comparator = comparator.reversed();
}
comparators.add(comparator);
}
comparators.add(Comparators.compareCreationTimestamp(false));
comparators.add(Comparators.compareName(true));
return comparators.stream()
.reduce(Comparator::thenComparing)
.orElse(null);
}
/**
* Build a predicate for {@link SinglePageQuery}.
*
* @return predicate
*/
public Predicate<SinglePage> toPredicate() {
Predicate<SinglePage> paramPredicate = singlePage -> contains(getContributors(),
singlePage.getStatusOrDefault().getContributors());
String keyword = getKeyword();
if (keyword != null) {
paramPredicate = paramPredicate.and(page -> {
String excerpt = page.getStatusOrDefault().getExcerpt();
return StringUtils.containsIgnoreCase(excerpt, keyword)
|| StringUtils.containsIgnoreCase(page.getSpec().getSlug(), keyword)
|| StringUtils.containsIgnoreCase(page.getSpec().getTitle(), keyword);
});
}
Post.PostPhase publishPhase = getPublishPhase();
if (publishPhase != null) {
paramPredicate = paramPredicate.and(page -> {
if (Post.PostPhase.PENDING_APPROVAL.equals(publishPhase)) {
return !page.isPublished()
&& Post.PostPhase.PENDING_APPROVAL.name()
.equalsIgnoreCase(page.getStatusOrDefault().getPhase());
}
// published
if (Post.PostPhase.PUBLISHED.equals(publishPhase)) {
return page.isPublished();
}
// draft
return !page.isPublished();
});
}
Post.VisibleEnum visible = getVisible();
if (visible != null) {
paramPredicate =
paramPredicate.and(post -> visible.equals(post.getSpec().getVisible()));
}
Predicate<SinglePage> predicate = labelAndFieldSelectorToPredicate(getLabelSelector(),
getFieldSelector());
return predicate.and(paramPredicate);
}
boolean contains(Collection<String> left, List<String> right) {
// parameter is null, it means that ignore this condition
if (left == null) {
return true;
}
// else, it means that right is empty
if (left.isEmpty()) {
return right.isEmpty();
}
if (right == null) {
return false;
}
return right.stream().anyMatch(left::contains);
return order;
})
.toList();
return Sort.by(orders);
}
public static void buildParameters(Builder builder) {

View File

@ -1,26 +1,23 @@
package run.halo.app.content.comment;
import static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;
import static run.halo.app.extension.index.query.QueryFactory.and;
import static org.springframework.data.domain.Sort.Order.desc;
import static run.halo.app.extension.index.query.QueryFactory.contains;
import static run.halo.app.extension.index.query.QueryFactory.equal;
import static run.halo.app.extension.router.selector.SelectorUtil.labelAndFieldSelectorToListOptions;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import java.util.Optional;
import org.apache.commons.lang3.StringUtils;
import org.springdoc.core.fn.builders.operation.Builder;
import org.springframework.data.domain.Sort;
import org.springframework.lang.Nullable;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.server.ServerWebExchange;
import run.halo.app.core.extension.User;
import run.halo.app.core.extension.content.Comment;
import run.halo.app.core.extension.endpoint.SortResolver;
import run.halo.app.extension.ListOptions;
import run.halo.app.extension.PageRequest;
import run.halo.app.extension.PageRequestImpl;
import run.halo.app.extension.router.IListRequest;
import run.halo.app.extension.router.QueryParamBuildUtil;
import run.halo.app.extension.router.selector.FieldSelector;
import run.halo.app.extension.router.SortableRequest;
/**
* Query criteria for comment list.
@ -28,64 +25,56 @@ import run.halo.app.extension.router.selector.FieldSelector;
* @author guqing
* @since 2.0.0
*/
public class CommentQuery extends IListRequest.QueryListRequest {
private final ServerWebExchange exchange;
public class CommentQuery extends SortableRequest {
public CommentQuery(ServerRequest request) {
super(request.queryParams());
this.exchange = request.exchange();
super(request.exchange());
}
@Nullable
public String getKeyword() {
String keyword = queryParams.getFirst("keyword");
return StringUtils.isBlank(keyword) ? null : keyword;
return queryParams.getFirst("keyword");
}
@Nullable
public String getOwnerKind() {
String ownerKind = queryParams.getFirst("ownerKind");
return StringUtils.isBlank(ownerKind) ? null : ownerKind;
return queryParams.getFirst("ownerKind");
}
@Nullable
public String getOwnerName() {
String ownerName = queryParams.getFirst("ownerName");
return StringUtils.isBlank(ownerName) ? null : ownerName;
return queryParams.getFirst("ownerName");
}
@Override
public Sort getSort() {
var sort = SortResolver.defaultInstance.resolve(exchange);
return sort.and(Sort.by("status.lastReplyTime",
"spec.creationTime",
"metadata.name"
).descending());
}
public PageRequest toPageRequest() {
return PageRequestImpl.of(getPage(), getSize(), getSort());
// set default sort by last reply time
return super.getSort().and(Sort.by(desc("status.lastReplyTime")));
}
/**
* Convert to list options.
*/
@Override
public ListOptions toListOptions() {
var listOptions =
labelAndFieldSelectorToListOptions(getLabelSelector(), getFieldSelector());
var fieldQuery = listOptions.getFieldSelector().query();
var builder = ListOptions.builder(super.toListOptions());
String keyword = getKeyword();
if (StringUtils.isNotBlank(keyword)) {
fieldQuery = and(fieldQuery, contains("spec.raw", keyword));
}
Optional.ofNullable(getKeyword())
.filter(StringUtils::isNotBlank)
.ifPresent(keyword -> builder.andQuery(contains("spec.raw", keyword)));
String ownerName = getOwnerName();
if (StringUtils.isNotBlank(ownerName)) {
String ownerKind = StringUtils.defaultIfBlank(getOwnerKind(), User.KIND);
fieldQuery = and(fieldQuery,
equal("spec.owner", Comment.CommentOwner.ownerIdentity(ownerKind, ownerName)));
}
Optional.ofNullable(getOwnerName())
.filter(StringUtils::isNotBlank)
.ifPresent(ownerName -> {
var ownerKind = Optional.ofNullable(getOwnerKind())
.filter(StringUtils::isNotBlank)
.orElse(User.KIND);
builder.andQuery(
equal("spec.owner", Comment.CommentOwner.ownerIdentity(ownerKind, ownerName))
);
});
listOptions.setFieldSelector(FieldSelector.of(fieldQuery));
return listOptions;
return builder.build();
}
public static void buildParameters(Builder builder) {

View File

@ -38,7 +38,6 @@ import run.halo.app.core.extension.service.UserService;
import run.halo.app.extension.ListOptions;
import run.halo.app.extension.ListResult;
import run.halo.app.extension.MetadataOperator;
import run.halo.app.extension.PageRequestImpl;
import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.extension.Ref;
import run.halo.app.extension.router.selector.FieldSelector;
@ -73,9 +72,9 @@ public class PostServiceImpl extends AbstractContentService implements PostServi
@Override
public Mono<ListResult<ListedPost>> listPost(PostQuery query) {
return buildListOptions(query)
.flatMap(listOptions -> client.listBy(Post.class, listOptions,
PageRequestImpl.of(query.getPage(), query.getSize(), query.getSort())
))
.flatMap(listOptions ->
client.listBy(Post.class, listOptions, query.toPageRequest())
)
.flatMap(listResult -> Flux.fromStream(listResult.get())
.map(this::getListedPost)
.concatMap(Function.identity())

View File

@ -86,16 +86,15 @@ public class SinglePageServiceImpl extends AbstractContentService implements Sin
@Override
public Mono<ListResult<ListedSinglePage>> list(SinglePageQuery query) {
return client.list(SinglePage.class, query.toPredicate(),
query.toComparator(), query.getPage(), query.getSize())
.flatMap(listResult -> Flux.fromStream(
listResult.get().map(this::getListedSinglePage)
)
return client.listBy(SinglePage.class, query.toListOptions(), query.toPageRequest())
.flatMap(listResult -> Flux.fromStream(listResult.get().map(this::getListedSinglePage))
.concatMap(Function.identity())
.collectList()
.map(listedSinglePages -> new ListResult<>(listResult.getPage(),
.map(listedSinglePages -> new ListResult<>(
listResult.getPage(),
listResult.getSize(),
listResult.getTotal(), listedSinglePages)
listResult.getTotal(),
listedSinglePages)
)
);
}

View File

@ -10,26 +10,20 @@ import static org.springdoc.core.fn.builders.schema.Builder.schemaBuilder;
import static org.springframework.boot.convert.ApplicationConversionService.getSharedInstance;
import static org.springframework.web.reactive.function.server.RequestPredicates.contentType;
import static run.halo.app.extension.ListResult.generateGenericClass;
import static run.halo.app.extension.index.query.QueryFactory.all;
import static run.halo.app.extension.index.query.QueryFactory.and;
import static run.halo.app.extension.index.query.QueryFactory.contains;
import static run.halo.app.extension.index.query.QueryFactory.in;
import static run.halo.app.extension.index.query.QueryFactory.isNull;
import static run.halo.app.extension.index.query.QueryFactory.not;
import static run.halo.app.extension.index.query.QueryFactory.startsWith;
import static run.halo.app.extension.router.selector.SelectorUtil.labelAndFieldSelectorToListOptions;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
import java.net.URL;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.BooleanUtils;
import org.springdoc.core.fn.builders.operation.Builder;
import org.springdoc.webflux.core.fn.SpringdocRouteBuilder;
import org.springframework.data.domain.Sort;
@ -45,21 +39,19 @@ import org.springframework.web.reactive.function.BodyExtractors;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.ServerWebInputException;
import reactor.core.publisher.Mono;
import run.halo.app.core.extension.attachment.Attachment;
import run.halo.app.core.extension.attachment.Group;
import run.halo.app.core.extension.endpoint.CustomEndpoint;
import run.halo.app.core.extension.endpoint.SortResolver;
import run.halo.app.core.extension.service.AttachmentService;
import run.halo.app.extension.ListOptions;
import run.halo.app.extension.PageRequestImpl;
import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.extension.index.query.QueryFactory;
import run.halo.app.extension.router.IListRequest;
import run.halo.app.extension.router.IListRequest.QueryListRequest;
import run.halo.app.extension.router.QueryParamBuildUtil;
import run.halo.app.extension.router.SortableRequest;
import run.halo.app.extension.router.selector.LabelSelector;
@Slf4j
@ -131,7 +123,7 @@ public class AttachmentEndpoint implements CustomEndpoint {
.response(
responseBuilder().implementation(generateGenericClass(Attachment.class))
);
ISearchRequest.buildParameters(builder);
SearchRequest.buildParameters(builder);
}
)
.build();
@ -159,33 +151,58 @@ public class AttachmentEndpoint implements CustomEndpoint {
);
}
public interface ISearchRequest extends IListRequest {
public static class SearchRequest extends SortableRequest {
@Schema(description = "Keyword for searching.")
Optional<String> getKeyword();
public SearchRequest(ServerRequest request) {
super(request.exchange());
}
@Schema(description = "Filter attachments without group. This parameter will ignore group"
+ " parameter.")
Optional<Boolean> getUngrouped();
public Optional<String> getKeyword() {
return Optional.ofNullable(queryParams.getFirst("keyword"))
.filter(StringUtils::hasText);
}
@ArraySchema(uniqueItems = true,
arraySchema = @Schema(name = "accepts",
description = "Acceptable media types."),
schema = @Schema(description = "like image/*, video/mp4, text/*",
implementation = String.class,
example = "image/*"))
List<String> getAccepts();
public Optional<Boolean> getUngrouped() {
return Optional.ofNullable(queryParams.getFirst("ungrouped"))
.map(ungroupedStr -> getSharedInstance().convert(ungroupedStr, Boolean.class));
}
@ArraySchema(uniqueItems = true,
arraySchema = @Schema(name = "sort",
description = "Sort property and direction of the list result. Supported fields: "
+ "creationTimestamp, size"),
schema = @Schema(description = "like field,asc or field,desc",
implementation = String.class,
example = "creationTimestamp,desc"))
Sort getSort();
public Optional<List<String>> getAccepts() {
return Optional.ofNullable(queryParams.get("accepts"))
.filter(accepts -> !accepts.isEmpty()
&& !accepts.contains("*")
&& !accepts.contains("*/*")
);
}
static void buildParameters(Builder builder) {
public ListOptions toListOptions(List<String> hiddenGroups) {
var builder = ListOptions.builder(super.toListOptions());
getKeyword().ifPresent(keyword -> {
builder.andQuery(contains("spec.displayName", keyword));
});
getUngrouped()
.filter(ungrouped -> ungrouped)
.ifPresent(ungrouped -> builder.andQuery(isNull("spec.groupName")));
if (!CollectionUtils.isEmpty(hiddenGroups)) {
builder.andQuery(not(in("spec.groupName", hiddenGroups)));
}
getAccepts().flatMap(accepts -> accepts.stream()
.filter(StringUtils::hasText)
.map(accept -> accept.replace("/*", "/").toLowerCase())
.distinct()
.map(accept -> startsWith("spec.mediaType", accept))
.reduce(QueryFactory::or)
)
.ifPresent(builder::andQuery);
return builder.build();
}
public static void buildParameters(Builder builder) {
IListRequest.buildParameters(builder);
builder.parameter(QueryParamBuildUtil.sortParameter())
.parameter(parameterBuilder()
@ -220,82 +237,6 @@ public class AttachmentEndpoint implements CustomEndpoint {
}
}
public static class SearchRequest extends QueryListRequest implements ISearchRequest {
private final ServerWebExchange exchange;
public SearchRequest(ServerRequest request) {
super(request.queryParams());
this.exchange = request.exchange();
}
@Override
public Optional<String> getKeyword() {
return Optional.ofNullable(queryParams.getFirst("keyword"))
.filter(StringUtils::hasText);
}
@Override
public Optional<Boolean> getUngrouped() {
return Optional.ofNullable(queryParams.getFirst("ungrouped"))
.map(ungroupedStr -> getSharedInstance().convert(ungroupedStr, Boolean.class));
}
@Override
public List<String> getAccepts() {
return queryParams.getOrDefault("accepts", Collections.emptyList());
}
@Override
public Sort getSort() {
var sort = SortResolver.defaultInstance.resolve(exchange);
sort = sort.and(Sort.by(
Sort.Order.desc("metadata.creationTimestamp"),
Sort.Order.asc("metadata.name")
));
return sort;
}
public ListOptions toListOptions(List<String> hiddenGroups) {
final var listOptions =
labelAndFieldSelectorToListOptions(getLabelSelector(), getFieldSelector());
var fieldQuery = all();
if (getKeyword().isPresent()) {
fieldQuery = and(fieldQuery, contains("spec.displayName", getKeyword().get()));
}
if (getUngrouped().isPresent() && BooleanUtils.isTrue(getUngrouped().get())) {
fieldQuery = and(fieldQuery, isNull("spec.groupName"));
}
if (!hiddenGroups.isEmpty()) {
fieldQuery = and(fieldQuery, not(in("spec.groupName", hiddenGroups)));
}
if (hasAccepts()) {
var acceptFieldQueryOptional = getAccepts().stream()
.filter(StringUtils::hasText)
.map((accept -> accept.replace("/*", "/").toLowerCase()))
.distinct()
.map(accept -> startsWith("spec.mediaType", accept))
.reduce(QueryFactory::or);
if (acceptFieldQueryOptional.isPresent()) {
fieldQuery = and(fieldQuery, acceptFieldQueryOptional.get());
}
}
listOptions.setFieldSelector(listOptions.getFieldSelector().andQuery(fieldQuery));
return listOptions;
}
private boolean hasAccepts() {
return !CollectionUtils.isEmpty(getAccepts())
&& !getAccepts().contains("*")
&& !getAccepts().contains("*/*");
}
}
public record UploadFromUrlRequest(@Schema(requiredMode = REQUIRED) URL url,
@Schema(requiredMode = REQUIRED) String policyName,
String groupName,

View File

@ -2,7 +2,6 @@ package run.halo.app.core.extension.endpoint;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import static java.util.Comparator.comparing;
import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;
import static org.springdoc.core.fn.builders.content.Builder.contentBuilder;
import static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;
@ -12,12 +11,13 @@ import static org.springframework.boot.convert.ApplicationConversionService.getS
import static org.springframework.core.io.buffer.DataBufferUtils.write;
import static org.springframework.web.reactive.function.server.RequestPredicates.contentType;
import static run.halo.app.extension.ListResult.generateGenericClass;
import static run.halo.app.extension.index.query.QueryFactory.contains;
import static run.halo.app.extension.index.query.QueryFactory.equal;
import static run.halo.app.extension.index.query.QueryFactory.or;
import static run.halo.app.extension.router.QueryParamBuildUtil.sortParameter;
import static run.halo.app.extension.router.selector.SelectorUtil.labelAndFieldSelectorToPredicate;
import static run.halo.app.infra.utils.FileUtils.deleteFileSilently;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.FileNotFoundException;
import java.io.IOException;
@ -27,13 +27,9 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Predicate;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.reactivestreams.Publisher;
@ -57,7 +53,6 @@ import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.reactive.resource.NoResourceFoundException;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.ServerWebInputException;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Scheduler;
@ -67,11 +62,11 @@ import run.halo.app.core.extension.Plugin;
import run.halo.app.core.extension.Setting;
import run.halo.app.core.extension.service.PluginService;
import run.halo.app.core.extension.theme.SettingUtils;
import run.halo.app.extension.Comparators;
import run.halo.app.extension.ConfigMap;
import run.halo.app.extension.ListOptions;
import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.extension.router.IListRequest;
import run.halo.app.extension.router.IListRequest.QueryListRequest;
import run.halo.app.extension.router.SortableRequest;
import run.halo.app.infra.ReactiveUrlDataBufferFetcher;
import run.halo.app.plugin.PluginNotFoundException;
@ -548,13 +543,10 @@ public class PluginEndpoint implements CustomEndpoint, InitializingBean {
.flatMap(resourceClosure);
}
public static class ListRequest extends QueryListRequest {
private final ServerWebExchange exchange;
public static class ListRequest extends SortableRequest {
public ListRequest(ServerRequest request) {
super(request.queryParams());
this.exchange = request.exchange();
super(request.exchange());
}
@Schema(name = "keyword", description = "Keyword of plugin name or description")
@ -568,69 +560,35 @@ public class PluginEndpoint implements CustomEndpoint, InitializingBean {
return enabled == null ? null : getSharedInstance().convert(enabled, Boolean.class);
}
@ArraySchema(uniqueItems = true,
arraySchema = @Schema(name = "sort",
description = "Sort property and direction of the list result. Supported fields: "
+ "creationTimestamp"),
schema = @Schema(description = "like field,asc or field,desc",
implementation = String.class,
example = "creationTimestamp,desc"))
@Override
public Sort getSort() {
return SortResolver.defaultInstance.resolve(exchange);
var orders = super.getSort().stream()
.map(order -> {
if ("creationTimestamp".equals(order.getProperty())) {
return order.withProperty("metadata.creationTimestamp");
}
return order;
})
.toList();
return Sort.by(orders);
}
public Predicate<Plugin> toPredicate() {
Predicate<Plugin> displayNamePredicate = plugin -> {
var keyword = getKeyword();
if (!StringUtils.hasText(keyword)) {
return true;
}
var displayName = plugin.getSpec().getDisplayName();
if (!StringUtils.hasText(displayName)) {
return false;
}
return displayName.toLowerCase().contains(keyword.trim().toLowerCase());
};
Predicate<Plugin> descriptionPredicate = plugin -> {
var keyword = getKeyword();
if (!StringUtils.hasText(keyword)) {
return true;
}
var description = plugin.getSpec().getDescription();
if (!StringUtils.hasText(description)) {
return false;
}
return description.toLowerCase().contains(keyword.trim().toLowerCase());
};
Predicate<Plugin> enablePredicate = plugin -> {
var enabled = getEnabled();
if (enabled == null) {
return true;
}
return Objects.equals(enabled, plugin.getSpec().getEnabled());
};
return displayNamePredicate.or(descriptionPredicate)
.and(enablePredicate)
.and(labelAndFieldSelectorToPredicate(getLabelSelector(), getFieldSelector()));
}
@Override
public ListOptions toListOptions() {
var builder = ListOptions.builder(super.toListOptions());
public Comparator<Plugin> toComparator() {
var sort = getSort();
var ctOrder = sort.getOrderFor("creationTimestamp");
List<Comparator<Plugin>> comparators = new ArrayList<>();
if (ctOrder != null) {
Comparator<Plugin> comparator =
comparing(plugin -> plugin.getMetadata().getCreationTimestamp());
if (ctOrder.isDescending()) {
comparator = comparator.reversed();
}
comparators.add(comparator);
}
comparators.add(Comparators.compareCreationTimestamp(false));
comparators.add(Comparators.compareName(true));
return comparators.stream()
.reduce(Comparator::thenComparing)
.orElse(null);
Optional.ofNullable(queryParams.getFirst("keyword"))
.filter(StringUtils::hasText)
.ifPresent(keyword -> builder.andQuery(or(
contains("spec.displayName", keyword),
contains("spec.description", keyword)
)));
Optional.ofNullable(queryParams.getFirst("enabled"))
.map(Boolean::parseBoolean)
.ifPresent(enabled -> builder.andQuery(equal("spec.enabled", enabled.toString())));
return builder.build();
}
public static void buildParameters(Builder builder) {
@ -654,15 +612,11 @@ public class PluginEndpoint implements CustomEndpoint, InitializingBean {
Mono<ServerResponse> list(ServerRequest request) {
return Mono.just(request)
.map(ListRequest::new)
.flatMap(listRequest -> {
var predicate = listRequest.toPredicate();
var comparator = listRequest.toComparator();
return client.list(Plugin.class,
predicate,
comparator,
listRequest.getPage(),
listRequest.getSize());
})
.flatMap(listRequest -> client.listBy(
Plugin.class,
listRequest.toListOptions(),
listRequest.toPageRequest()
))
.flatMap(listResult -> ServerResponse.ok().bodyValue(listResult));
}

View File

@ -403,7 +403,7 @@ public class PostEndpoint implements CustomEndpoint {
/**
* Convenient for testing, to avoid waiting too long for post published when testing.
*/
public void setMaxAttemptsWaitForPublish(int maxAttempts) {
void setMaxAttemptsWaitForPublish(int maxAttempts) {
this.maxAttemptsWaitForPublish = maxAttempts;
}
}

View File

@ -2,33 +2,29 @@ package run.halo.app.core.extension.endpoint;
import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;
import static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;
import static run.halo.app.extension.index.query.QueryFactory.all;
import static run.halo.app.extension.index.query.QueryFactory.contains;
import static run.halo.app.extension.index.query.QueryFactory.or;
import static run.halo.app.extension.router.QueryParamBuildUtil.sortParameter;
import static run.halo.app.extension.router.selector.SelectorUtil.labelAndFieldSelectorToListOptions;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springdoc.core.fn.builders.operation.Builder;
import org.springdoc.webflux.core.fn.SpringdocRouteBuilder;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import run.halo.app.core.extension.content.Tag;
import run.halo.app.extension.ListOptions;
import run.halo.app.extension.ListResult;
import run.halo.app.extension.PageRequestImpl;
import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.extension.index.query.QueryFactory;
import run.halo.app.extension.router.IListRequest;
import run.halo.app.extension.router.SortableRequest;
/**
* post tag endpoint.
@ -68,61 +64,27 @@ public class TagEndpoint implements CustomEndpoint {
.flatMap(tags -> ServerResponse.ok().bodyValue(tags));
}
public interface ITagQuery extends IListRequest {
@Schema(description = "Keyword for searching.")
Optional<String> getKeyword();
@ArraySchema(uniqueItems = true,
arraySchema = @Schema(name = "sort",
description = "Sort property and direction of the list result. Supported fields: "
+ "creationTimestamp, name"),
schema = @Schema(description = "like field,asc or field,desc",
implementation = String.class,
example = "creationTimestamp,desc"))
Sort getSort();
}
public static class TagQuery extends IListRequest.QueryListRequest
implements ITagQuery {
private final ServerWebExchange exchange;
public static class TagQuery extends SortableRequest {
public TagQuery(ServerRequest request) {
super(request.queryParams());
this.exchange = request.exchange();
super(request.exchange());
}
@Override
public Optional<String> getKeyword() {
return Optional.ofNullable(queryParams.getFirst("keyword"))
.filter(StringUtils::hasText);
}
@Override
public Sort getSort() {
var sort = SortResolver.defaultInstance.resolve(exchange);
sort = sort.and(Sort.by(
Sort.Order.desc("metadata.creationTimestamp"),
Sort.Order.asc("metadata.name")
));
return sort;
}
public ListOptions toListOptions() {
final var listOptions =
labelAndFieldSelectorToListOptions(getLabelSelector(), getFieldSelector());
var fieldQuery = all();
if (getKeyword().isPresent()) {
fieldQuery = QueryFactory.and(fieldQuery, QueryFactory.or(
QueryFactory.contains("spec.displayName", getKeyword().get()),
QueryFactory.contains("spec.slug", getKeyword().get())
var builder = ListOptions.builder(super.toListOptions());
getKeyword().ifPresent(keyword -> builder.andQuery(
or(
contains("spec.displayName", keyword),
contains("spec.slug", keyword)
)
));
}
listOptions.setFieldSelector(listOptions.getFieldSelector().andQuery(fieldQuery));
return listOptions;
return builder.build();
}
public static void buildParameters(Builder builder) {

View File

@ -14,7 +14,6 @@ import static run.halo.app.extension.index.query.QueryFactory.contains;
import static run.halo.app.extension.index.query.QueryFactory.equal;
import static run.halo.app.extension.index.query.QueryFactory.in;
import static run.halo.app.extension.index.query.QueryFactory.or;
import static run.halo.app.extension.router.QueryParamBuildUtil.sortParameter;
import static run.halo.app.extension.router.selector.SelectorUtil.labelAndFieldSelectorToListOptions;
import static run.halo.app.security.authorization.AuthorityUtils.authoritiesToRoles;
@ -24,7 +23,6 @@ import io.github.resilience4j.ratelimiter.RateLimiterRegistry;
import io.github.resilience4j.ratelimiter.RequestNotPermitted;
import io.github.resilience4j.reactor.ratelimiter.operator.RateLimiterOperator;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
import java.security.Principal;
import java.time.Duration;
@ -48,7 +46,6 @@ import org.springdoc.core.fn.builders.operation.Builder;
import org.springdoc.webflux.core.fn.SpringdocRouteBuilder;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.data.domain.Sort;
import org.springframework.http.MediaType;
import org.springframework.http.codec.multipart.FilePart;
import org.springframework.http.codec.multipart.Part;
@ -65,7 +62,6 @@ import org.springframework.web.reactive.function.BodyExtractors;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.ServerWebInputException;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@ -84,7 +80,7 @@ import run.halo.app.extension.Metadata;
import run.halo.app.extension.MetadataUtil;
import run.halo.app.extension.PageRequestImpl;
import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.extension.router.IListRequest;
import run.halo.app.extension.router.SortableRequest;
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
import run.halo.app.infra.SystemSetting;
import run.halo.app.infra.ValidationUtils;
@ -687,13 +683,10 @@ public class UserEndpoint implements CustomEndpoint {
}
public static class ListRequest extends IListRequest.QueryListRequest {
private final ServerWebExchange exchange;
public static class ListRequest extends SortableRequest {
public ListRequest(ServerRequest request) {
super(request.queryParams());
this.exchange = request.exchange();
super(request.exchange());
}
@Schema(name = "keyword")
@ -706,19 +699,6 @@ public class UserEndpoint implements CustomEndpoint {
return queryParams.getFirst("role");
}
@ArraySchema(uniqueItems = true,
arraySchema = @Schema(name = "sort",
description = "Sort property and direction of the list result. Supported fields: "
+ "creationTimestamp"),
schema = @Schema(description = "like field,asc or field,desc",
implementation = String.class,
example = "creationTimestamp,desc"))
public Sort getSort() {
var sort = SortResolver.defaultInstance.resolve(exchange);
sort = sort.and(Sort.by("metadata.creationTimestamp", "metadata.name").descending());
return sort;
}
/**
* Converts query parameters to list options.
*/
@ -743,9 +723,8 @@ public class UserEndpoint implements CustomEndpoint {
}
public static void buildParameters(Builder builder) {
IListRequest.buildParameters(builder);
builder.parameter(sortParameter())
.parameter(parameterBuilder()
SortableRequest.buildParameters(builder);
builder.parameter(parameterBuilder()
.in(ParameterIn.QUERY)
.name("keyword")
.description("Keyword to search")
@ -770,8 +749,7 @@ public class UserEndpoint implements CustomEndpoint {
.map(UserEndpoint.ListRequest::new)
.flatMap(listRequest -> client.listBy(User.class, listRequest.toListOptions(),
PageRequestImpl.of(
listRequest.getPage(), listRequest.getSize(),
listRequest.getSort()
listRequest.getPage(), listRequest.getSize(), listRequest.getSort()
)
))
.flatMap(this::toListedUser)

View File

@ -8,6 +8,7 @@ import static run.halo.app.extension.index.IndexAttributeFactory.multiValueAttri
import static run.halo.app.extension.index.IndexAttributeFactory.simpleAttribute;
import com.fasterxml.jackson.core.type.TypeReference;
import java.time.Instant;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
@ -95,7 +96,33 @@ public class SchemeInitializer implements ApplicationListener<ApplicationContext
});
// plugin.halo.run
schemeManager.register(Plugin.class);
schemeManager.register(Plugin.class, is -> {
is.add(new IndexSpec()
.setName("spec.displayName")
.setIndexFunc(
simpleAttribute(Plugin.class, plugin -> Optional.ofNullable(plugin.getSpec())
.map(Plugin.PluginSpec::getDisplayName)
.orElse(null))
)
);
is.add(new IndexSpec()
.setName("spec.description")
.setIndexFunc(
simpleAttribute(Plugin.class, plugin -> Optional.ofNullable(plugin.getSpec())
.map(Plugin.PluginSpec::getDescription)
.orElse(null))
)
);
is.add(new IndexSpec()
.setName("spec.enabled")
.setIndexFunc(
simpleAttribute(Plugin.class, plugin -> Optional.ofNullable(plugin.getSpec())
.map(Plugin.PluginSpec::getEnabled)
.map(Object::toString)
.orElse(Boolean.FALSE.toString()))
)
);
});
schemeManager.register(SearchEngine.class);
schemeManager.register(ExtensionPointDefinition.class, indexSpecs -> {
indexSpecs.add(new IndexSpec()
@ -443,7 +470,85 @@ public class SchemeInitializer implements ApplicationListener<ApplicationContext
return null;
})));
});
schemeManager.register(SinglePage.class);
schemeManager.register(SinglePage.class, is -> {
is.add(new IndexSpec()
.setName("spec.publishTime")
.setIndexFunc(
simpleAttribute(SinglePage.class, page -> Optional.ofNullable(page.getSpec())
.map(SinglePage.SinglePageSpec::getPublishTime)
.map(Instant::toString)
.orElse(null))
)
);
is.add(new IndexSpec()
.setName("spec.title")
.setIndexFunc(
simpleAttribute(SinglePage.class, page -> Optional.ofNullable(page.getSpec())
.map(SinglePage.SinglePageSpec::getTitle)
.orElse(null))
)
);
is.add(new IndexSpec()
.setName("spec.slug")
.setUnique(true)
.setIndexFunc(
simpleAttribute(SinglePage.class, page -> Optional.ofNullable(page.getSpec())
.map(SinglePage.SinglePageSpec::getSlug)
.orElse(null))
)
);
is.add(new IndexSpec()
.setName("spec.visible")
.setIndexFunc(
simpleAttribute(SinglePage.class, page -> Optional.ofNullable(page.getSpec())
.map(SinglePage.SinglePageSpec::getVisible)
.map(Post.VisibleEnum::name)
.orElse(null))
)
);
is.add(new IndexSpec()
.setName("spec.pinned")
.setIndexFunc(
simpleAttribute(SinglePage.class, page -> Optional.ofNullable(page.getSpec())
.map(SinglePage.SinglePageSpec::getPinned)
.map(Object::toString)
.orElse(Boolean.FALSE.toString())))
);
is.add(new IndexSpec()
.setName("spec.priority")
.setIndexFunc(simpleAttribute(SinglePage.class,
page -> Optional.ofNullable(page.getSpec())
.map(SinglePage.SinglePageSpec::getPriority)
.map(Object::toString)
.orElse(Integer.toString(0)))
)
);
is.add(new IndexSpec()
.setName("status.excerpt")
.setIndexFunc(
simpleAttribute(SinglePage.class, page -> Optional.ofNullable(page.getStatus())
.map(SinglePage.SinglePageStatus::getExcerpt)
.orElse(null))
)
);
is.add(new IndexSpec()
.setName("status.phase")
.setIndexFunc(
simpleAttribute(SinglePage.class, page -> Optional.ofNullable(page.getStatus())
.map(SinglePage.SinglePageStatus::getPhase)
.orElse(null))
)
);
is.add(new IndexSpec()
.setName("status.contributors")
.setIndexFunc(multiValueAttribute(SinglePage.class,
page -> Optional.ofNullable(page.getStatus())
.map(SinglePage.SinglePageStatus::getContributors)
.map(Set::copyOf)
.orElse(null))
)
);
});
// storage.halo.run
schemeManager.register(Group.class);
schemeManager.register(Policy.class);

View File

@ -19,6 +19,7 @@ import run.halo.app.core.extension.endpoint.CustomEndpoint;
import run.halo.app.extension.GroupVersion;
import run.halo.app.extension.ListResult;
import run.halo.app.extension.router.SortableRequest;
import run.halo.app.theme.finders.SinglePageConversionService;
import run.halo.app.theme.finders.SinglePageFinder;
import run.halo.app.theme.finders.vo.ListedSinglePageVo;
import run.halo.app.theme.finders.vo.SinglePageVo;
@ -35,6 +36,8 @@ public class SinglePageQueryEndpoint implements CustomEndpoint {
private final SinglePageFinder singlePageFinder;
private final SinglePageConversionService singlePageConversionService;
@Override
public RouterFunction<ServerResponse> endpoint() {
var tag = "SinglePageV1alpha1Public";
@ -79,15 +82,8 @@ public class SinglePageQueryEndpoint implements CustomEndpoint {
private Mono<ServerResponse> listSinglePages(ServerRequest request) {
var query = new SinglePagePublicQuery(request.exchange());
return singlePageFinder.list(query.getPage(),
query.getSize(),
query.toPredicate(),
query.toComparator()
)
.flatMap(result -> ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(result)
);
return singlePageConversionService.listBy(query.toListOptions(), query.toPageRequest())
.flatMap(result -> ServerResponse.ok().bodyValue(result));
}
static class SinglePagePublicQuery extends SortableRequest {

View File

@ -3,6 +3,9 @@ package run.halo.app.theme.finders;
import org.springframework.lang.NonNull;
import reactor.core.publisher.Mono;
import run.halo.app.core.extension.content.SinglePage;
import run.halo.app.extension.ListOptions;
import run.halo.app.extension.ListResult;
import run.halo.app.extension.PageRequest;
import run.halo.app.theme.ReactiveSinglePageContentHandler;
import run.halo.app.theme.finders.vo.ContentVo;
import run.halo.app.theme.finders.vo.ListedSinglePageVo;
@ -52,4 +55,7 @@ public interface SinglePageConversionService {
Mono<ContentVo> getContent(String pageName);
Mono<ListedSinglePageVo> convertToListedVo(SinglePage singlePage);
Mono<ListResult<ListedSinglePageVo>> listBy(ListOptions listOptions, PageRequest pageRequest);
}

View File

@ -1,7 +1,5 @@
package run.halo.app.theme.finders;
import java.util.Comparator;
import java.util.function.Predicate;
import org.springframework.lang.Nullable;
import reactor.core.publisher.Mono;
import run.halo.app.core.extension.content.SinglePage;
@ -24,6 +22,4 @@ public interface SinglePageFinder {
Mono<ListResult<ListedSinglePageVo>> list(@Nullable Integer page, @Nullable Integer size);
Mono<ListResult<ListedSinglePageVo>> list(@Nullable Integer page, @Nullable Integer size,
@Nullable Predicate<SinglePage> predicate, @Nullable Comparator<SinglePage> comparator);
}

View File

@ -1,16 +1,29 @@
package run.halo.app.theme.finders.impl;
import static org.springframework.data.domain.Sort.Order.asc;
import static org.springframework.data.domain.Sort.Order.desc;
import static run.halo.app.core.extension.content.Post.VisibleEnum.PUBLIC;
import static run.halo.app.core.extension.content.SinglePage.PUBLISHED_LABEL;
import static run.halo.app.extension.ExtensionUtil.notDeleting;
import static run.halo.app.extension.index.query.QueryFactory.equal;
import java.util.List;
import java.util.function.Function;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Sort;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import run.halo.app.content.ContentWrapper;
import run.halo.app.content.SinglePageService;
import run.halo.app.core.extension.content.SinglePage;
import run.halo.app.extension.ListOptions;
import run.halo.app.extension.ListResult;
import run.halo.app.extension.PageRequest;
import run.halo.app.extension.PageRequestImpl;
import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.metrics.CounterService;
import run.halo.app.metrics.MeterUtils;
@ -102,6 +115,40 @@ public class SinglePageConversionServiceImpl implements SinglePageConversionServ
.flatMap(this::populateContributors);
}
@Override
public Mono<ListResult<ListedSinglePageVo>> listBy(ListOptions listOptions,
PageRequest pageRequest) {
// rewrite list options
var rewroteListOptions = ListOptions.builder(listOptions)
.andQuery(notDeleting())
.andQuery(equal("spec.deleted", Boolean.FALSE.toString()))
.andQuery(equal("spec.visible", PUBLIC.name()))
.labelSelector()
.eq(PUBLISHED_LABEL, Boolean.TRUE.toString())
.end()
.build();
// rewrite sort
var rewroteSort = pageRequest.getSort()
.and(Sort.by(
desc("spec.pinned"),
asc("spec.priority")
));
var rewrotePageRequest =
PageRequestImpl.of(pageRequest.getPageNumber(), pageRequest.getPageSize(), rewroteSort);
return client.listBy(SinglePage.class, rewroteListOptions, rewrotePageRequest)
.flatMap(list -> Flux.fromStream(list.get())
.concatMap(this::convertToListedVo)
.collectList()
.map(pageVos ->
new ListResult<>(list.getPage(), list.getSize(), list.getTotal(), pageVos)
)
);
}
Mono<SinglePageVo> convert(SinglePage singlePage, String snapshotName) {
Assert.notNull(singlePage, "Single page must not be null");
Assert.hasText(snapshotName, "Snapshot name must not be empty");

View File

@ -1,23 +1,17 @@
package run.halo.app.theme.finders.impl;
import java.security.Principal;
import java.time.Instant;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Predicate;
import lombok.AllArgsConstructor;
import org.apache.commons.lang3.ObjectUtils;
import org.springframework.lang.Nullable;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.security.core.context.SecurityContext;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import run.halo.app.core.extension.content.Post;
import run.halo.app.core.extension.content.SinglePage;
import run.halo.app.extension.ListOptions;
import run.halo.app.extension.ListResult;
import run.halo.app.extension.PageRequestImpl;
import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.infra.AnonymousUserConst;
import run.halo.app.theme.finders.Finder;
@ -37,10 +31,6 @@ import run.halo.app.theme.finders.vo.SinglePageVo;
@AllArgsConstructor
public class SinglePageFinderImpl implements SinglePageFinder {
public static final Predicate<SinglePage> FIXED_PREDICATE = page -> page.isPublished()
&& Objects.equals(false, page.getSpec().getDeleted())
&& Post.VisibleEnum.PUBLIC.equals(page.getSpec().getVisible());
private final ReactiveExtensionClient client;
private final SinglePageConversionService singlePagePublicQueryService;
@ -59,27 +49,10 @@ public class SinglePageFinderImpl implements SinglePageFinder {
@Override
public Mono<ListResult<ListedSinglePageVo>> list(Integer page, Integer size) {
return list(page, size, null, null);
}
@Override
public Mono<ListResult<ListedSinglePageVo>> list(@Nullable Integer page, @Nullable Integer size,
@Nullable Predicate<SinglePage> predicate, @Nullable Comparator<SinglePage> comparator) {
var predicateToUse = Optional.ofNullable(predicate)
.map(p -> p.and(FIXED_PREDICATE))
.orElse(FIXED_PREDICATE);
var comparatorToUse = Optional.ofNullable(comparator)
.orElse(defaultComparator());
return client.list(SinglePage.class, predicateToUse,
comparatorToUse, pageNullSafe(page), sizeNullSafe(size))
.flatMap(list -> Flux.fromStream(list.get())
.concatMap(singlePagePublicQueryService::convertToListedVo)
.collectList()
.map(pageVos -> new ListResult<>(list.getPage(), list.getSize(), list.getTotal(),
pageVos)
)
)
.defaultIfEmpty(new ListResult<>(0, 0, 0, List.of()));
return singlePagePublicQueryService.listBy(
new ListOptions(),
PageRequestImpl.of(page, size)
);
}
Mono<Predicate<SinglePage>> queryPredicate() {
@ -101,26 +74,4 @@ public class SinglePageFinderImpl implements SinglePageFinder {
.filter(name -> !AnonymousUserConst.isAnonymousUser(name));
}
static Comparator<SinglePage> defaultComparator() {
Function<SinglePage, Boolean> pinned =
page -> Objects.requireNonNullElse(page.getSpec().getPinned(), false);
Function<SinglePage, Integer> priority =
page -> Objects.requireNonNullElse(page.getSpec().getPriority(), 0);
Function<SinglePage, Instant> creationTimestamp =
page -> page.getMetadata().getCreationTimestamp();
Function<SinglePage, String> name = page -> page.getMetadata().getName();
return Comparator.comparing(pinned)
.thenComparing(priority)
.thenComparing(creationTimestamp)
.thenComparing(name)
.reversed();
}
int pageNullSafe(Integer page) {
return ObjectUtils.defaultIfNull(page, 1);
}
int sizeNullSafe(Integer size) {
return ObjectUtils.defaultIfNull(size, 10);
}
}

View File

@ -2,8 +2,6 @@ package run.halo.app.core.extension.endpoint;
import static java.util.Objects.requireNonNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isA;
import static org.mockito.ArgumentMatchers.same;
@ -23,9 +21,7 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
@ -50,8 +46,10 @@ import run.halo.app.core.extension.Plugin;
import run.halo.app.core.extension.Setting;
import run.halo.app.core.extension.service.PluginService;
import run.halo.app.extension.ConfigMap;
import run.halo.app.extension.ListOptions;
import run.halo.app.extension.ListResult;
import run.halo.app.extension.Metadata;
import run.halo.app.extension.PageRequest;
import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.infra.SystemVersionSupplier;
import run.halo.app.infra.utils.FileUtils;
@ -80,7 +78,7 @@ class PluginEndpointTest {
@Test
void shouldListEmptyPluginsWhenNoPlugins() {
when(client.list(same(Plugin.class), any(), any(), anyInt(), anyInt()))
when(client.listBy(same(Plugin.class), any(ListOptions.class), any(PageRequest.class)))
.thenReturn(Mono.just(ListResult.emptyResult()));
bindToRouterFunction(endpoint.endpoint())
@ -101,7 +99,7 @@ class PluginEndpointTest {
createPlugin("fake-plugin-3")
);
var expectResult = new ListResult<>(plugins);
when(client.list(same(Plugin.class), any(), any(), anyInt(), anyInt()))
when(client.listBy(same(Plugin.class), any(ListOptions.class), any(PageRequest.class)))
.thenReturn(Mono.just(expectResult));
bindToRouterFunction(endpoint.endpoint())
@ -126,7 +124,7 @@ class PluginEndpointTest {
expectPlugin
);
var expectResult = new ListResult<>(plugins);
when(client.list(same(Plugin.class), any(), any(), anyInt(), anyInt()))
when(client.listBy(same(Plugin.class), any(ListOptions.class), any(PageRequest.class)))
.thenReturn(Mono.just(expectResult));
bindToRouterFunction(endpoint.endpoint())
@ -134,27 +132,18 @@ class PluginEndpointTest {
.get().uri("/plugins?keyword=Expected")
.exchange()
.expectStatus().isOk();
verify(client).list(same(Plugin.class), argThat(
predicate -> predicate.test(expectPlugin)
&& !predicate.test(unexpectedPlugin1)
&& !predicate.test(unexpectedPlugin2)),
any(), anyInt(), anyInt());
}
@Test
void shouldFilterPluginsWhenEnabledProvided() {
var expectPlugin =
createPlugin("fake-plugin-2", "expected display name", "", true);
var unexpectedPlugin1 =
createPlugin("fake-plugin-1", "first fake display name", "", false);
var unexpectedPlugin2 =
createPlugin("fake-plugin-3", "second fake display name", "", false);
var plugins = List.of(
expectPlugin
);
var expectResult = new ListResult<>(plugins);
when(client.list(same(Plugin.class), any(), any(), anyInt(), anyInt()))
when(client.listBy(same(Plugin.class), any(ListOptions.class), any(PageRequest.class)))
.thenReturn(Mono.just(expectResult));
bindToRouterFunction(endpoint.endpoint())
@ -162,12 +151,6 @@ class PluginEndpointTest {
.get().uri("/plugins?enabled=true")
.exchange()
.expectStatus().isOk();
verify(client).list(same(Plugin.class), argThat(
predicate -> predicate.test(expectPlugin)
&& !predicate.test(unexpectedPlugin1)
&& !predicate.test(unexpectedPlugin2)),
any(), anyInt(), anyInt());
}
@Test
@ -175,7 +158,7 @@ class PluginEndpointTest {
var expectPlugin =
createPlugin("fake-plugin-2", "expected display name", "", true);
var expectResult = new ListResult<>(List.of(expectPlugin));
when(client.list(same(Plugin.class), any(), any(), anyInt(), anyInt()))
when(client.listBy(same(Plugin.class), any(ListOptions.class), any(PageRequest.class)))
.thenReturn(Mono.just(expectResult));
bindToRouterFunction(endpoint.endpoint())
@ -183,21 +166,6 @@ class PluginEndpointTest {
.get().uri("/plugins?sort=creationTimestamp,desc")
.exchange()
.expectStatus().isOk();
verify(client).list(same(Plugin.class), any(), argThat(comparator -> {
var now = Instant.now();
var plugins = new ArrayList<>(List.of(
createPlugin("fake-plugin-a", now),
createPlugin("fake-plugin-b", now.plusSeconds(1)),
createPlugin("fake-plugin-c", now.plusSeconds(2))
));
plugins.sort(comparator);
return Objects.deepEquals(plugins, List.of(
createPlugin("fake-plugin-c", now.plusSeconds(2)),
createPlugin("fake-plugin-b", now.plusSeconds(1)),
createPlugin("fake-plugin-a", now)
));
}), anyInt(), anyInt());
}
}

View File

@ -11,9 +11,8 @@ import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.csrf;
import static org.springframework.test.web.reactive.server.WebTestClient.bindToRouterFunction;
import static run.halo.app.extension.GroupVersionKind.fromExtension;
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockUser;
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.springSecurity;
import java.time.Instant;
import java.util.HashMap;
@ -24,19 +23,17 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
import org.springframework.boot.test.context.SpringBootTest;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.MediaType;
import org.springframework.http.client.MultipartBodyBuilder;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.reactive.function.BodyInserters;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import run.halo.app.core.extension.Role;
import run.halo.app.core.extension.RoleBinding;
import run.halo.app.core.extension.User;
import run.halo.app.core.extension.attachment.Attachment;
import run.halo.app.core.extension.service.AttachmentService;
@ -46,14 +43,12 @@ import run.halo.app.extension.ListResult;
import run.halo.app.extension.Metadata;
import run.halo.app.extension.PageRequest;
import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.extension.exception.ExtensionNotFoundException;
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
import run.halo.app.infra.SystemSetting;
import run.halo.app.infra.exception.UserNotFoundException;
import run.halo.app.infra.utils.JsonUtils;
@SpringBootTest
@AutoConfigureWebTestClient
@WithMockUser(username = "fake-user", password = "fake-password", roles = "fake-super-role")
@ExtendWith(MockitoExtension.class)
class UserEndpointTest {
WebTestClient webClient;
@ -78,9 +73,10 @@ class UserEndpointTest {
@BeforeEach
void setUp() {
// disable authorization
webClient = WebTestClient.bindToRouterFunction(endpoint.endpoint()).build()
.mutateWith(csrf());
webClient = WebTestClient.bindToRouterFunction(endpoint.endpoint())
.apply(springSecurity())
.build()
.mutateWith(mockUser("fake-user").password("fake-password").roles("fake-super-role"));
}
@Nested
@ -93,9 +89,7 @@ class UserEndpointTest {
when(client.listBy(same(User.class), any(), any(PageRequest.class)))
.thenReturn(Mono.just(ListResult.emptyResult()));
bindToRouterFunction(endpoint.endpoint())
.build()
.get().uri("/users")
webClient.get().uri("/users")
.exchange()
.expectStatus().isOk()
.expectBody()
@ -116,9 +110,7 @@ class UserEndpointTest {
when(client.listBy(same(User.class), any(), any(PageRequest.class)))
.thenReturn(Mono.just(expectResult));
bindToRouterFunction(endpoint.endpoint())
.build()
.get().uri("/users")
webClient.get().uri("/users")
.exchange()
.expectStatus().isOk()
.expectBody()
@ -150,9 +142,7 @@ class UserEndpointTest {
when(roleService.getRolesByUsernames(any())).thenReturn(Mono.just(Map.of()));
when(roleService.list(anySet())).thenReturn(Flux.empty());
bindToRouterFunction(endpoint.endpoint())
.build()
.get().uri("/users?role=guest")
webClient.get().uri("/users?role=guest")
.exchange()
.expectStatus().isOk();
}
@ -167,9 +157,7 @@ class UserEndpointTest {
when(roleService.getRolesByUsernames(any())).thenReturn(Mono.just(Map.of()));
when(roleService.list(anySet())).thenReturn(Flux.empty());
bindToRouterFunction(endpoint.endpoint())
.build()
.get().uri("/users?sort=creationTimestamp,desc")
webClient.get().uri("/users?sort=creationTimestamp,desc")
.exchange()
.expectStatus().isOk();
}
@ -190,16 +178,6 @@ class UserEndpointTest {
return user;
}
User createUser(String name, Instant creationTimestamp) {
var metadata = new Metadata();
metadata.setName(name);
metadata.setCreationTimestamp(creationTimestamp);
var spec = new User.UserSpec();
var user = new User();
user.setMetadata(metadata);
user.setSpec(spec);
return user;
}
}
@Nested
@ -209,8 +187,7 @@ class UserEndpointTest {
@Test
void shouldResponseErrorIfUserNotFound() {
when(userService.getUser("fake-user"))
.thenReturn(Mono.error(
new ExtensionNotFoundException(fromExtension(User.class), "fake-user")));
.thenReturn(Mono.error(new UserNotFoundException("fake-user")));
webClient.get().uri("/users/-")
.exchange()
.expectStatus().isNotFound();
@ -236,7 +213,6 @@ class UserEndpointTest {
.expectHeader().contentType(MediaType.APPLICATION_JSON)
.expectBody(UserEndpoint.DetailedUser.class)
.isEqualTo(new UserEndpoint.DetailedUser(user, List.of(role)));
// verify(roleService).list(eq(Set.of("role-A")));
}
}
@ -267,11 +243,9 @@ class UserEndpointTest {
@Test
void shouldGetErrorIfUsernameMismatch() {
var currentUser = createUser("fake-user");
var updatedUser = createUser("fake-user");
var requestUser = createUser("another-fake-user");
when(client.get(User.class, "fake-user")).thenReturn(Mono.just(currentUser));
when(client.update(currentUser)).thenReturn(Mono.just(updatedUser));
webClient.put().uri("/users/-")
.bodyValue(requestUser)
@ -324,8 +298,6 @@ class UserEndpointTest {
@Test
void shouldUpdateOtherPasswordCorrectly() {
var user = new User();
when(userService.confirmPassword("another-fake-user", "old-password"))
.thenReturn(Mono.just(true));
when(userService.updateWithRawPassword("another-fake-user", "new-password"))
.thenReturn(Mono.just(user));
webClient.put()
@ -347,14 +319,6 @@ class UserEndpointTest {
@DisplayName("GrantPermission")
class GrantPermissionEndpointTest {
@BeforeEach
void setUp() {
when(client.list(same(RoleBinding.class), any(), any())).thenReturn(Flux.empty());
when(client.get(User.class, "fake-user"))
.thenReturn(Mono.error(
new ExtensionNotFoundException(fromExtension(User.class), "fake-user")));
}
@Test
void shouldGetBadRequestIfRequestBodyIsEmpty() {
webClient.post().uri("/users/fake-user/permissions")
@ -395,7 +359,6 @@ class UserEndpointTest {
}
""", Role.class);
when(roleService.listPermissions(eq(Set.of("test-A")))).thenReturn(Flux.just(roleA));
when(roleService.listDependenciesFlux(anySet())).thenReturn(Flux.just(roleA));
when(roleService.getRolesByUsername("fake-user")).thenReturn(Flux.just("test-A"));
when(roleService.list(Set.of("test-A"), true)).thenReturn(Flux.just(roleA));
@ -416,8 +379,6 @@ class UserEndpointTest {
void createWhenNameDuplicate() {
when(userService.createUser(any(User.class), anySet()))
.thenReturn(Mono.just(new User()));
when(userService.updateWithRawPassword(anyString(), anyString()))
.thenReturn(Mono.just(new User()));
var userRequest = new UserEndpoint.CreateUserRequest("fake-user",
"fake-email",
"",

View File

@ -1,13 +1,10 @@
package run.halo.app.theme.endpoint;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@ -19,10 +16,8 @@ import org.springframework.test.web.reactive.server.WebTestClient;
import reactor.core.publisher.Mono;
import run.halo.app.core.extension.content.SinglePage;
import run.halo.app.extension.GroupVersion;
import run.halo.app.extension.ListResult;
import run.halo.app.extension.Metadata;
import run.halo.app.theme.finders.SinglePageFinder;
import run.halo.app.theme.finders.vo.ListedSinglePageVo;
import run.halo.app.theme.finders.vo.SinglePageVo;
/**
@ -47,30 +42,6 @@ class SinglePageQueryEndpointTest {
webTestClient = WebTestClient.bindToRouterFunction(endpoint.endpoint()).build();
}
@Test
void listSinglePages() {
ListedSinglePageVo test = ListedSinglePageVo.builder()
.metadata(metadata("test"))
.spec(new SinglePage.SinglePageSpec())
.build();
ListResult<ListedSinglePageVo> pageResult = new ListResult<>(List.of(test));
when(singlePageFinder.list(anyInt(), anyInt(), any(), any()))
.thenReturn(Mono.just(pageResult));
webTestClient.get()
.uri("/singlepages?page=0&size=10")
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_JSON)
.expectBody()
.jsonPath("$.total").isEqualTo(1)
.jsonPath("$.items[0].metadata.name").isEqualTo("test");
verify(singlePageFinder).list(eq(0), eq(10), any(), any());
}
@Test
void getByName() {
SinglePageVo singlePage = SinglePageVo.builder()