mirror of https://github.com/halo-dev/halo
refactor: using index mechanisms to optimize comment queries (#5453)
#### What type of PR is this? /kind improvement /area core /milestone 2.14.x #### What this PR does / why we need it: 使用索引机制优化评论数据查询以提高效率 how to test it? - 测试 console 评论列表和筛选条件是否正确 - 测试主题端评论显示是否正确 #### Does this PR introduce a user-facing change? ```release-note 使用索引机制优化评论数据查询以提高效率 ```pull/5494/head
parent
92f2229460
commit
20d80f8f65
|
@ -116,6 +116,10 @@ public class Comment extends AbstractExtension {
|
|||
public String getAnnotation(String key) {
|
||||
return annotations == null ? null : annotations.get(key);
|
||||
}
|
||||
|
||||
public static String ownerIdentity(String kind, String name) {
|
||||
return kind + "#" + name;
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
|
@ -132,6 +136,10 @@ public class Comment extends AbstractExtension {
|
|||
private Boolean hasNewReply;
|
||||
}
|
||||
|
||||
public static String toSubjectRefKey(Ref subjectRef) {
|
||||
return subjectRef.getGroup() + "/" + subjectRef.getKind() + "/" + subjectRef.getName();
|
||||
}
|
||||
|
||||
public static int getUnreadReplyCount(List<Reply> replies, Instant lastReadTime) {
|
||||
if (CollectionUtils.isEmpty(replies)) {
|
||||
return 0;
|
||||
|
|
|
@ -120,7 +120,7 @@ public class QueryFactory {
|
|||
return new And(queries);
|
||||
}
|
||||
|
||||
public static And and(Query query1, Query query2) {
|
||||
public static Query and(Query query1, Query query2) {
|
||||
Collection<Query> queries = Arrays.asList(query1, query2);
|
||||
return new And(queries);
|
||||
}
|
||||
|
|
|
@ -1,29 +1,24 @@
|
|||
package run.halo.app.content.comment;
|
||||
|
||||
import static java.util.Comparator.comparing;
|
||||
import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
|
||||
import static run.halo.app.extension.router.selector.SelectorUtil.labelAndFieldSelectorToPredicate;
|
||||
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.equal;
|
||||
import static run.halo.app.extension.router.selector.SelectorUtil.labelAndFieldSelectorToListOptions;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
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 org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.data.domain.Sort;
|
||||
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.Comparators;
|
||||
import run.halo.app.extension.Extension;
|
||||
import run.halo.app.extension.Ref;
|
||||
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.selector.FieldSelector;
|
||||
|
||||
/**
|
||||
* Query criteria for comment list.
|
||||
|
@ -34,12 +29,6 @@ import run.halo.app.extension.router.IListRequest;
|
|||
public class CommentQuery extends IListRequest.QueryListRequest {
|
||||
|
||||
private final ServerWebExchange exchange;
|
||||
static final Function<Comment, Instant> LAST_REPLY_TIME_FUNC =
|
||||
comment -> {
|
||||
Instant lastReplyTime = comment.getStatusOrDefault().getLastReplyTime();
|
||||
return Optional.ofNullable(lastReplyTime)
|
||||
.orElse(comment.getSpec().getCreationTime());
|
||||
};
|
||||
|
||||
public CommentQuery(ServerRequest request) {
|
||||
super(request.queryParams());
|
||||
|
@ -52,26 +41,6 @@ public class CommentQuery extends IListRequest.QueryListRequest {
|
|||
return StringUtils.isBlank(keyword) ? null : keyword;
|
||||
}
|
||||
|
||||
@Schema(description = "Comments approved.")
|
||||
public Boolean getApproved() {
|
||||
return convertBooleanOrNull(queryParams.getFirst("approved"));
|
||||
}
|
||||
|
||||
@Schema(description = "The comment is hidden from the theme side.")
|
||||
public Boolean getHidden() {
|
||||
return convertBooleanOrNull(queryParams.getFirst("hidden"));
|
||||
}
|
||||
|
||||
@Schema(description = "Send notifications when there are new replies.")
|
||||
public Boolean getAllowNotification() {
|
||||
return convertBooleanOrNull(queryParams.getFirst("allowNotification"));
|
||||
}
|
||||
|
||||
@Schema(description = "Comment top display.")
|
||||
public Boolean getTop() {
|
||||
return convertBooleanOrNull(queryParams.getFirst("top"));
|
||||
}
|
||||
|
||||
@Schema(description = "Commenter kind.")
|
||||
public String getOwnerKind() {
|
||||
String ownerKind = queryParams.getFirst("ownerKind");
|
||||
|
@ -84,18 +53,6 @@ public class CommentQuery extends IListRequest.QueryListRequest {
|
|||
return StringUtils.isBlank(ownerName) ? null : ownerName;
|
||||
}
|
||||
|
||||
@Schema(description = "Comment subject kind.")
|
||||
public String getSubjectKind() {
|
||||
String subjectKind = queryParams.getFirst("subjectKind");
|
||||
return StringUtils.isBlank(subjectKind) ? null : subjectKind;
|
||||
}
|
||||
|
||||
@Schema(description = "Comment subject name.")
|
||||
public String getSubjectName() {
|
||||
String subjectName = queryParams.getFirst("subjectName");
|
||||
return StringUtils.isBlank(subjectName) ? null : subjectName;
|
||||
}
|
||||
|
||||
@ArraySchema(uniqueItems = true,
|
||||
arraySchema = @Schema(name = "sort",
|
||||
description = "Sort property and direction of the list result. Supported fields: "
|
||||
|
@ -104,139 +61,35 @@ public class CommentQuery extends IListRequest.QueryListRequest {
|
|||
implementation = String.class,
|
||||
example = "creationTimestamp,desc"))
|
||||
public Sort getSort() {
|
||||
return SortResolver.defaultInstance.resolve(exchange);
|
||||
var sort = SortResolver.defaultInstance.resolve(exchange);
|
||||
return sort.and(Sort.by("spec.creationTime", "metadata.name").descending());
|
||||
}
|
||||
|
||||
public PageRequest toPageRequest() {
|
||||
return PageRequestImpl.of(getPage(), getSize(), getSort());
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a comparator from the query.
|
||||
*
|
||||
* @return comparator
|
||||
* Convert to list options.
|
||||
*/
|
||||
public Comparator<Comment> toComparator() {
|
||||
var sort = getSort();
|
||||
var creationTimestampOrder = sort.getOrderFor("creationTimestamp");
|
||||
List<Comparator<Comment>> comparators = new ArrayList<>();
|
||||
if (creationTimestampOrder != null) {
|
||||
Comparator<Comment> comparator =
|
||||
comparing(comment -> comment.getMetadata().getCreationTimestamp());
|
||||
if (creationTimestampOrder.isDescending()) {
|
||||
comparator = comparator.reversed();
|
||||
}
|
||||
comparators.add(comparator);
|
||||
}
|
||||
|
||||
var replyCountOrder = sort.getOrderFor("replyCount");
|
||||
if (replyCountOrder != null) {
|
||||
Comparator<Comment> comparator = comparing(
|
||||
comment -> defaultIfNull(comment.getStatusOrDefault().getReplyCount(), 0));
|
||||
if (replyCountOrder.isDescending()) {
|
||||
comparator = comparator.reversed();
|
||||
}
|
||||
comparators.add(comparator);
|
||||
}
|
||||
|
||||
var lastReplyTimeOrder = sort.getOrderFor("lastReplyTime");
|
||||
if (lastReplyTimeOrder == null) {
|
||||
lastReplyTimeOrder = new Sort.Order(Sort.Direction.DESC, "lastReplyTime");
|
||||
}
|
||||
Comparator<Comment> comparator = comparing(LAST_REPLY_TIME_FUNC,
|
||||
Comparators.nullsComparator(lastReplyTimeOrder.isAscending()));
|
||||
if (lastReplyTimeOrder.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 from the query.
|
||||
*
|
||||
* @return predicate
|
||||
*/
|
||||
Predicate<Comment> toPredicate() {
|
||||
Predicate<Comment> predicate = comment -> true;
|
||||
public ListOptions toListOptions() {
|
||||
var listOptions =
|
||||
labelAndFieldSelectorToListOptions(getLabelSelector(), getFieldSelector());
|
||||
var fieldQuery = listOptions.getFieldSelector().query();
|
||||
|
||||
String keyword = getKeyword();
|
||||
if (keyword != null) {
|
||||
predicate = predicate.and(comment -> {
|
||||
String raw = comment.getSpec().getRaw();
|
||||
return StringUtils.containsIgnoreCase(raw, keyword);
|
||||
});
|
||||
}
|
||||
|
||||
Boolean approved = getApproved();
|
||||
if (approved != null) {
|
||||
predicate =
|
||||
predicate.and(comment -> Objects.equals(comment.getSpec().getApproved(), approved));
|
||||
}
|
||||
Boolean hidden = getHidden();
|
||||
if (hidden != null) {
|
||||
predicate =
|
||||
predicate.and(comment -> Objects.equals(comment.getSpec().getHidden(), hidden));
|
||||
}
|
||||
|
||||
Boolean top = getTop();
|
||||
if (top != null) {
|
||||
predicate = predicate.and(comment -> Objects.equals(comment.getSpec().getTop(), top));
|
||||
}
|
||||
|
||||
Boolean allowNotification = getAllowNotification();
|
||||
if (allowNotification != null) {
|
||||
predicate = predicate.and(
|
||||
comment -> Objects.equals(comment.getSpec().getAllowNotification(),
|
||||
allowNotification));
|
||||
}
|
||||
|
||||
String ownerKind = getOwnerKind();
|
||||
if (ownerKind != null) {
|
||||
predicate = predicate.and(comment -> {
|
||||
Comment.CommentOwner owner = comment.getSpec().getOwner();
|
||||
return Objects.equals(owner.getKind(), ownerKind);
|
||||
});
|
||||
if (StringUtils.isNotBlank(keyword)) {
|
||||
fieldQuery = and(fieldQuery, contains("spec.raw", keyword));
|
||||
}
|
||||
|
||||
String ownerName = getOwnerName();
|
||||
if (ownerName != null) {
|
||||
predicate = predicate.and(comment -> {
|
||||
Comment.CommentOwner owner = comment.getSpec().getOwner();
|
||||
if (Comment.CommentOwner.KIND_EMAIL.equals(owner.getKind())) {
|
||||
return Objects.equals(owner.getKind(), ownerKind)
|
||||
&& (StringUtils.containsIgnoreCase(owner.getName(), ownerName)
|
||||
|| StringUtils.containsIgnoreCase(owner.getDisplayName(), ownerName));
|
||||
}
|
||||
return Objects.equals(owner.getKind(), ownerKind)
|
||||
&& StringUtils.containsIgnoreCase(owner.getName(), ownerName);
|
||||
});
|
||||
if (StringUtils.isNotBlank(ownerName)) {
|
||||
String ownerKind = StringUtils.defaultIfBlank(getOwnerKind(), User.KIND);
|
||||
fieldQuery = and(fieldQuery,
|
||||
equal("spec.owner", Comment.CommentOwner.ownerIdentity(ownerKind, ownerName)));
|
||||
}
|
||||
|
||||
String subjectKind = getSubjectKind();
|
||||
if (subjectKind != null) {
|
||||
predicate = predicate.and(comment -> {
|
||||
Ref subjectRef = comment.getSpec().getSubjectRef();
|
||||
return Objects.equals(subjectRef.getKind(), subjectKind);
|
||||
});
|
||||
}
|
||||
|
||||
String subjectName = getSubjectName();
|
||||
if (subjectName != null) {
|
||||
predicate = predicate.and(comment -> {
|
||||
Ref subjectRef = comment.getSpec().getSubjectRef();
|
||||
return Objects.equals(subjectRef.getKind(), subjectKind)
|
||||
&& StringUtils.containsIgnoreCase(subjectRef.getName(), subjectName);
|
||||
});
|
||||
}
|
||||
|
||||
Predicate<Extension> labelAndFieldSelectorPredicate =
|
||||
labelAndFieldSelectorToPredicate(getLabelSelector(),
|
||||
getFieldSelector());
|
||||
return predicate.and(labelAndFieldSelectorPredicate);
|
||||
}
|
||||
|
||||
private Boolean convertBooleanOrNull(String value) {
|
||||
return StringUtils.isBlank(value) ? null : Boolean.parseBoolean(value);
|
||||
listOptions.setFieldSelector(FieldSelector.of(fieldQuery));
|
||||
return listOptions;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,9 +50,8 @@ public class CommentServiceImpl implements CommentService {
|
|||
|
||||
@Override
|
||||
public Mono<ListResult<ListedComment>> listComment(CommentQuery commentQuery) {
|
||||
return this.client.list(Comment.class, commentQuery.toPredicate(),
|
||||
commentQuery.toComparator(),
|
||||
commentQuery.getPage(), commentQuery.getSize())
|
||||
return this.client.listBy(Comment.class, commentQuery.toListOptions(),
|
||||
commentQuery.toPageRequest())
|
||||
.flatMap(comments -> Flux.fromStream(comments.get()
|
||||
.map(this::toListedComment))
|
||||
.concatMap(Function.identity())
|
||||
|
@ -138,32 +137,21 @@ public class CommentServiceImpl implements CommentService {
|
|||
}
|
||||
|
||||
private Mono<ListedComment> toListedComment(Comment comment) {
|
||||
ListedComment.ListedCommentBuilder commentBuilder = ListedComment.builder()
|
||||
.comment(comment);
|
||||
return Mono.just(commentBuilder)
|
||||
.flatMap(builder -> {
|
||||
Comment.CommentOwner owner = comment.getSpec().getOwner();
|
||||
var builder = ListedComment.builder().comment(comment);
|
||||
// not empty
|
||||
return getCommentOwnerInfo(owner)
|
||||
.map(builder::owner);
|
||||
})
|
||||
.flatMap(builder -> getCommentSubject(comment.getSpec().getSubjectRef())
|
||||
.map(subject -> {
|
||||
builder.subject(subject);
|
||||
return builder;
|
||||
})
|
||||
.switchIfEmpty(Mono.just(builder))
|
||||
)
|
||||
.map(ListedComment.ListedCommentBuilder::build)
|
||||
.flatMap(lc -> fetchStats(comment)
|
||||
.doOnNext(lc::setStats)
|
||||
.thenReturn(lc));
|
||||
var ownerInfoMono = getCommentOwnerInfo(comment.getSpec().getOwner())
|
||||
.doOnNext(builder::owner);
|
||||
var subjectMono = getCommentSubject(comment.getSpec().getSubjectRef())
|
||||
.doOnNext(builder::subject);
|
||||
var statsMono = fetchStats(comment.getMetadata().getName())
|
||||
.doOnNext(builder::stats);
|
||||
return Mono.when(ownerInfoMono, subjectMono, statsMono)
|
||||
.then(Mono.fromSupplier(builder::build));
|
||||
}
|
||||
|
||||
Mono<CommentStats> fetchStats(Comment comment) {
|
||||
Assert.notNull(comment, "The comment must not be null.");
|
||||
String name = comment.getMetadata().getName();
|
||||
return counterService.getByName(MeterUtils.nameOf(Comment.class, name))
|
||||
Mono<CommentStats> fetchStats(String commentName) {
|
||||
Assert.notNull(commentName, "The commentName must not be null.");
|
||||
return counterService.getByName(MeterUtils.nameOf(Comment.class, commentName))
|
||||
.map(counter -> CommentStats.builder()
|
||||
.upvote(counter.getUpvote())
|
||||
.build()
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package run.halo.app.infra;
|
||||
|
||||
import static org.apache.commons.lang3.BooleanUtils.isTrue;
|
||||
import static org.apache.commons.lang3.BooleanUtils.toStringTrueFalse;
|
||||
import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
|
||||
import static run.halo.app.extension.index.IndexAttributeFactory.multiValueAttribute;
|
||||
import static run.halo.app.extension.index.IndexAttributeFactory.simpleAttribute;
|
||||
|
@ -206,7 +208,69 @@ public class SchemeInitializer implements ApplicationListener<ApplicationContext
|
|||
)
|
||||
);
|
||||
});
|
||||
schemeManager.register(Comment.class);
|
||||
schemeManager.register(Comment.class, indexSpecs -> {
|
||||
indexSpecs.add(new IndexSpec()
|
||||
.setName("spec.creationTime")
|
||||
.setIndexFunc(simpleAttribute(Comment.class,
|
||||
comment -> comment.getSpec().getCreationTime().toString())
|
||||
));
|
||||
indexSpecs.add(new IndexSpec()
|
||||
.setName("spec.approved")
|
||||
.setIndexFunc(simpleAttribute(Comment.class,
|
||||
comment -> toStringTrueFalse(isTrue(comment.getSpec().getApproved())))
|
||||
));
|
||||
indexSpecs.add(new IndexSpec()
|
||||
.setName("spec.owner")
|
||||
.setIndexFunc(simpleAttribute(Comment.class, comment -> {
|
||||
var owner = comment.getSpec().getOwner();
|
||||
return Comment.CommentOwner.ownerIdentity(owner.getKind(), owner.getName());
|
||||
})));
|
||||
indexSpecs.add(new IndexSpec()
|
||||
.setName("spec.subjectRef")
|
||||
.setIndexFunc(simpleAttribute(Comment.class,
|
||||
comment -> Comment.toSubjectRefKey(comment.getSpec().getSubjectRef()))
|
||||
));
|
||||
indexSpecs.add(new IndexSpec()
|
||||
.setName("spec.top")
|
||||
.setIndexFunc(simpleAttribute(Comment.class,
|
||||
comment -> toStringTrueFalse(isTrue(comment.getSpec().getTop())))
|
||||
));
|
||||
indexSpecs.add(new IndexSpec()
|
||||
.setName("spec.hidden")
|
||||
.setIndexFunc(simpleAttribute(Comment.class,
|
||||
comment -> toStringTrueFalse(isTrue(comment.getSpec().getHidden())))
|
||||
));
|
||||
indexSpecs.add(new IndexSpec()
|
||||
.setName("spec.priority")
|
||||
.setIndexFunc(simpleAttribute(Comment.class,
|
||||
comment -> {
|
||||
var isTop = comment.getSpec().getTop();
|
||||
// only top comments have priority
|
||||
if (!isTop) {
|
||||
return "0";
|
||||
}
|
||||
return defaultIfNull(comment.getSpec().getPriority(), 0).toString();
|
||||
})
|
||||
));
|
||||
indexSpecs.add(new IndexSpec()
|
||||
.setName("spec.raw")
|
||||
.setIndexFunc(simpleAttribute(Comment.class,
|
||||
comment -> comment.getSpec().getRaw())
|
||||
));
|
||||
indexSpecs.add(new IndexSpec()
|
||||
.setName("status.lastReplyTime")
|
||||
.setIndexFunc(simpleAttribute(Comment.class, comment -> {
|
||||
var lastReplyTime = comment.getStatusOrDefault().getLastReplyTime();
|
||||
return defaultIfNull(lastReplyTime,
|
||||
comment.getSpec().getCreationTime()).toString();
|
||||
})));
|
||||
indexSpecs.add(new IndexSpec()
|
||||
.setName("status.replyCount")
|
||||
.setIndexFunc(simpleAttribute(Comment.class, comment -> {
|
||||
var replyCount = comment.getStatusOrDefault().getReplyCount();
|
||||
return defaultIfNull(replyCount, 0).toString();
|
||||
})));
|
||||
});
|
||||
schemeManager.register(Reply.class);
|
||||
schemeManager.register(SinglePage.class);
|
||||
// storage.halo.run
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package run.halo.app.theme.endpoint;
|
||||
|
||||
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
|
||||
import static java.util.Comparator.comparing;
|
||||
import static org.apache.commons.lang3.BooleanUtils.isFalse;
|
||||
import static org.apache.commons.lang3.BooleanUtils.isTrue;
|
||||
import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;
|
||||
|
@ -16,8 +15,6 @@ 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.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
@ -43,9 +40,10 @@ import run.halo.app.core.extension.content.Comment;
|
|||
import run.halo.app.core.extension.content.Reply;
|
||||
import run.halo.app.core.extension.endpoint.CustomEndpoint;
|
||||
import run.halo.app.core.extension.endpoint.SortResolver;
|
||||
import run.halo.app.extension.Comparators;
|
||||
import run.halo.app.extension.GroupVersion;
|
||||
import run.halo.app.extension.ListResult;
|
||||
import run.halo.app.extension.PageRequest;
|
||||
import run.halo.app.extension.PageRequestImpl;
|
||||
import run.halo.app.extension.Ref;
|
||||
import run.halo.app.extension.router.IListRequest;
|
||||
import run.halo.app.extension.router.QueryParamBuildUtil;
|
||||
|
@ -213,9 +211,7 @@ public class CommentFinderEndpoint implements CustomEndpoint {
|
|||
|
||||
Mono<ServerResponse> listComments(ServerRequest request) {
|
||||
CommentQuery commentQuery = new CommentQuery(request);
|
||||
var comparator = commentQuery.toComparator();
|
||||
return commentPublicQueryService.list(commentQuery.toRef(), commentQuery.getPage(),
|
||||
commentQuery.getSize(), comparator)
|
||||
return commentPublicQueryService.list(commentQuery.toRef(), commentQuery.toPageRequest())
|
||||
.flatMap(list -> ServerResponse.ok().bodyValue(list));
|
||||
}
|
||||
|
||||
|
@ -302,26 +298,12 @@ public class CommentFinderEndpoint implements CustomEndpoint {
|
|||
return ref;
|
||||
}
|
||||
|
||||
String emptyToNull(String str) {
|
||||
return StringUtils.isBlank(str) ? null : str;
|
||||
public PageRequest toPageRequest() {
|
||||
return PageRequestImpl.of(getPage(), getSize(), getSort());
|
||||
}
|
||||
|
||||
public Comparator<Comment> toComparator() {
|
||||
var sort = getSort();
|
||||
var ctOrder = sort.getOrderFor("creationTimestamp");
|
||||
List<Comparator<Comment>> comparators = new ArrayList<>();
|
||||
if (ctOrder != null) {
|
||||
Comparator<Comment> comparator =
|
||||
comparing(comment -> comment.getMetadata().getCreationTimestamp());
|
||||
if (ctOrder.isDescending()) {
|
||||
comparator = comparator.reversed();
|
||||
}
|
||||
comparators.add(comparator);
|
||||
comparators.add(Comparators.compareName(true));
|
||||
}
|
||||
return comparators.stream()
|
||||
.reduce(Comparator::thenComparing)
|
||||
.orElse(null);
|
||||
String emptyToNull(String str) {
|
||||
return StringUtils.isBlank(str) ? null : str;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,9 +3,9 @@ package run.halo.app.theme.finders;
|
|||
import java.util.Comparator;
|
||||
import org.springframework.lang.Nullable;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.extension.content.Comment;
|
||||
import run.halo.app.core.extension.content.Reply;
|
||||
import run.halo.app.extension.ListResult;
|
||||
import run.halo.app.extension.PageRequest;
|
||||
import run.halo.app.extension.Ref;
|
||||
import run.halo.app.theme.finders.vo.CommentVo;
|
||||
import run.halo.app.theme.finders.vo.ReplyVo;
|
||||
|
@ -21,8 +21,7 @@ public interface CommentPublicQueryService {
|
|||
Mono<ListResult<CommentVo>> list(Ref ref, @Nullable Integer page,
|
||||
@Nullable Integer size);
|
||||
|
||||
Mono<ListResult<CommentVo>> list(Ref ref, @Nullable Integer page,
|
||||
@Nullable Integer size, @Nullable Comparator<Comment> comparator);
|
||||
Mono<ListResult<CommentVo>> list(Ref ref, @Nullable PageRequest pageRequest);
|
||||
|
||||
Mono<ListResult<ReplyVo>> listReply(String commentName, @Nullable Integer page,
|
||||
@Nullable Integer size);
|
||||
|
|
|
@ -2,34 +2,42 @@ package run.halo.app.theme.finders.impl;
|
|||
|
||||
|
||||
import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
|
||||
import static run.halo.app.extension.index.query.QueryFactory.and;
|
||||
import static run.halo.app.extension.index.query.QueryFactory.equal;
|
||||
import static run.halo.app.extension.index.query.QueryFactory.or;
|
||||
|
||||
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.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.BooleanUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.comparator.Comparators;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.content.comment.OwnerInfo;
|
||||
import run.halo.app.content.comment.ReplyService;
|
||||
import run.halo.app.core.extension.User;
|
||||
import run.halo.app.core.extension.content.Comment;
|
||||
import run.halo.app.core.extension.content.Reply;
|
||||
import run.halo.app.core.extension.service.UserService;
|
||||
import run.halo.app.extension.AbstractExtension;
|
||||
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.extension.Ref;
|
||||
import run.halo.app.extension.index.query.QueryFactory;
|
||||
import run.halo.app.extension.router.selector.FieldSelector;
|
||||
import run.halo.app.infra.AnonymousUserConst;
|
||||
import run.halo.app.metrics.CounterService;
|
||||
import run.halo.app.metrics.MeterUtils;
|
||||
|
@ -43,6 +51,7 @@ import run.halo.app.theme.finders.vo.ReplyVo;
|
|||
* comment public query service implementation.
|
||||
*
|
||||
* @author LIlGG
|
||||
* @author guqing
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
|
@ -61,30 +70,31 @@ public class CommentPublicQueryServiceImpl implements CommentPublicQueryService
|
|||
|
||||
@Override
|
||||
public Mono<ListResult<CommentVo>> list(Ref ref, Integer page, Integer size) {
|
||||
return list(ref, page, size, defaultComparator());
|
||||
return list(ref, PageRequestImpl.of(pageNullSafe(page), sizeNullSafe(size), defaultSort()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<ListResult<CommentVo>> list(Ref ref, Integer page, Integer size,
|
||||
Comparator<Comment> comparator) {
|
||||
final Comparator<Comment> commentComparator =
|
||||
Objects.isNull(comparator) ? defaultComparator()
|
||||
: comparator.thenComparing(defaultComparator());
|
||||
return fixedCommentPredicate(ref)
|
||||
.flatMap(fixedPredicate ->
|
||||
client.list(Comment.class, fixedPredicate,
|
||||
commentComparator,
|
||||
pageNullSafe(page), sizeNullSafe(size))
|
||||
.flatMap(list -> Flux.fromStream(list.get().map(this::toCommentVo))
|
||||
public Mono<ListResult<CommentVo>> list(Ref ref, PageRequest pageParam) {
|
||||
var pageRequest = Optional.ofNullable(pageParam)
|
||||
.map(page -> page.withSort(page.getSort().and(defaultSort())))
|
||||
.orElse(PageRequestImpl.ofSize(0));
|
||||
return fixedCommentFieldQuery(ref)
|
||||
.flatMap(fixedFieldQuery -> {
|
||||
var listOptions = new ListOptions();
|
||||
listOptions.setFieldSelector(fixedFieldQuery);
|
||||
return client.listBy(Comment.class, listOptions, pageRequest)
|
||||
.flatMap(listResult -> Flux.fromStream(listResult.get())
|
||||
.map(this::toCommentVo)
|
||||
.concatMap(Function.identity())
|
||||
.collectList()
|
||||
.map(commentVos -> new ListResult<>(list.getPage(), list.getSize(),
|
||||
list.getTotal(),
|
||||
.map(commentVos -> new ListResult<>(listResult.getPage(),
|
||||
listResult.getSize(),
|
||||
listResult.getTotal(),
|
||||
commentVos)
|
||||
)
|
||||
)
|
||||
.defaultIfEmpty(new ListResult<>(page, size, 0L, List.of()))
|
||||
);
|
||||
})
|
||||
.defaultIfEmpty(ListResult.emptyResult());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -193,28 +203,31 @@ public class CommentPublicQueryServiceImpl implements CommentPublicQueryService
|
|||
.map(OwnerInfo::from);
|
||||
}
|
||||
|
||||
private Mono<Predicate<Comment>> fixedCommentPredicate(@Nullable Ref ref) {
|
||||
Predicate<Comment> basePredicate =
|
||||
comment -> comment.getMetadata().getDeletionTimestamp() == null;
|
||||
private Mono<FieldSelector> fixedCommentFieldQuery(@Nullable Ref ref) {
|
||||
return Mono.fromSupplier(
|
||||
() -> {
|
||||
var baseQuery = QueryFactory.isNull("metadata.deletionTimestamp");
|
||||
if (ref != null) {
|
||||
basePredicate = basePredicate
|
||||
.and(comment -> comment.getSpec().getSubjectRef().equals(ref));
|
||||
baseQuery =
|
||||
and(baseQuery,
|
||||
equal("spec.subjectRef", Comment.toSubjectRefKey(ref)));
|
||||
}
|
||||
|
||||
// is approved and not hidden
|
||||
Predicate<Comment> approvedPredicate =
|
||||
comment -> BooleanUtils.isFalse(comment.getSpec().getHidden())
|
||||
&& BooleanUtils.isTrue(comment.getSpec().getApproved());
|
||||
return getCurrentUserWithoutAnonymous()
|
||||
.map(username -> {
|
||||
Predicate<Comment> isOwner = comment -> {
|
||||
Comment.CommentOwner owner = comment.getSpec().getOwner();
|
||||
return owner != null && StringUtils.equals(username, owner.getName());
|
||||
};
|
||||
return approvedPredicate.or(isOwner);
|
||||
return baseQuery;
|
||||
})
|
||||
.defaultIfEmpty(approvedPredicate)
|
||||
.map(basePredicate::and);
|
||||
.flatMap(query -> {
|
||||
var approvedQuery = and(
|
||||
equal("spec.approved", BooleanUtils.TRUE),
|
||||
equal("spec.hidden", BooleanUtils.FALSE)
|
||||
);
|
||||
// we should list all comments that the user owns
|
||||
return getCurrentUserWithoutAnonymous()
|
||||
.map(username -> or(approvedQuery, equal("spec.owner",
|
||||
Comment.CommentOwner.ownerIdentity(User.KIND, username)))
|
||||
)
|
||||
.defaultIfEmpty(approvedQuery)
|
||||
.map(compositeQuery -> and(query, compositeQuery));
|
||||
})
|
||||
.map(FieldSelector::of);
|
||||
}
|
||||
|
||||
private Mono<Predicate<Reply>> fixedReplyPredicate(String commentName) {
|
||||
|
@ -247,43 +260,12 @@ public class CommentPublicQueryServiceImpl implements CommentPublicQueryService
|
|||
.filter(username -> !AnonymousUserConst.PRINCIPAL.equals(username));
|
||||
}
|
||||
|
||||
static Comparator<Comment> defaultComparator() {
|
||||
return new CommentComparator();
|
||||
}
|
||||
|
||||
static class CommentComparator implements Comparator<Comment> {
|
||||
@Override
|
||||
public int compare(Comment c1, Comment c2) {
|
||||
boolean c1Top = BooleanUtils.isTrue(c1.getSpec().getTop());
|
||||
boolean c2Top = BooleanUtils.isTrue(c2.getSpec().getTop());
|
||||
|
||||
// c1 top = true && c2 top = false
|
||||
if (c1Top && !c2Top) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// c1 top = false && c2 top = true
|
||||
if (!c1Top && c2Top) {
|
||||
return 1;
|
||||
}
|
||||
// c1 top = c2 top = true || c1 top = c2 top = false
|
||||
var priorityComparator = Comparator.<Comment, Integer>comparing(
|
||||
comment -> defaultIfNull(comment.getSpec().getPriority(), 0));
|
||||
|
||||
var creationTimeComparator = Comparator.<Comment, Instant>comparing(
|
||||
comment -> comment.getSpec().getCreationTime(),
|
||||
Comparators.nullsHigh(Comparator.<Instant>reverseOrder()));
|
||||
|
||||
var nameComparator = Comparator.<Comment, String>comparing(
|
||||
comment -> comment.getMetadata().getName());
|
||||
|
||||
if (c1Top) {
|
||||
return priorityComparator.thenComparing(creationTimeComparator)
|
||||
.thenComparing(nameComparator)
|
||||
.compare(c1, c2);
|
||||
}
|
||||
return creationTimeComparator.thenComparing(nameComparator).compare(c1, c2);
|
||||
}
|
||||
static Sort defaultSort() {
|
||||
return Sort.by(Sort.Order.desc("spec.top"),
|
||||
Sort.Order.asc("spec.priority"),
|
||||
Sort.Order.desc("spec.creationTime"),
|
||||
Sort.Order.asc("metadata.name")
|
||||
);
|
||||
}
|
||||
|
||||
int pageNullSafe(Integer page) {
|
||||
|
|
|
@ -1,361 +0,0 @@
|
|||
package run.halo.app.content.comment;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import lombok.NonNull;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.mock.web.reactive.function.server.MockServerRequest;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import run.halo.app.core.extension.content.Comment;
|
||||
import run.halo.app.extension.Metadata;
|
||||
|
||||
/**
|
||||
* Tests for {@link CommentQuery}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class CommentQueryTest {
|
||||
|
||||
@Test
|
||||
void getKeyword() {
|
||||
MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
|
||||
queryParams.add("keyword", "test");
|
||||
CommentQuery commentQuery = getCommentQuery(queryParams);
|
||||
assertThat(commentQuery.getKeyword()).isEqualTo("test");
|
||||
queryParams.clear();
|
||||
|
||||
queryParams.add("keyword", "");
|
||||
assertThat(commentQuery.getKeyword()).isNull();
|
||||
queryParams.clear();
|
||||
|
||||
queryParams.add("keyword", null);
|
||||
assertThat(commentQuery.getKeyword()).isNull();
|
||||
queryParams.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getApproved() {
|
||||
MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
|
||||
queryParams.add("approved", "true");
|
||||
CommentQuery commentQuery = getCommentQuery(queryParams);
|
||||
assertThat(commentQuery.getApproved()).isTrue();
|
||||
queryParams.clear();
|
||||
|
||||
queryParams.add("approved", "");
|
||||
assertThat(commentQuery.getApproved()).isNull();
|
||||
queryParams.clear();
|
||||
|
||||
queryParams.add("approved", "1");
|
||||
assertThat(commentQuery.getApproved()).isFalse();
|
||||
queryParams.clear();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private CommentQuery getCommentQuery(MultiValueMap<String, String> queryParams) {
|
||||
ServerWebExchange exchange = mock(ServerWebExchange.class);
|
||||
MockServerRequest request = MockServerRequest.builder()
|
||||
.queryParams(queryParams)
|
||||
.exchange(exchange)
|
||||
.build();
|
||||
ServerHttpRequest httpRequest = mock(ServerHttpRequest.class);
|
||||
lenient().when(exchange.getRequest()).thenReturn(httpRequest);
|
||||
lenient().when(httpRequest.getQueryParams()).thenReturn(queryParams);
|
||||
return new CommentQuery(request);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getHidden() {
|
||||
MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
|
||||
queryParams.add("hidden", "true");
|
||||
CommentQuery commentQuery = getCommentQuery(queryParams);
|
||||
assertThat(commentQuery.getHidden()).isTrue();
|
||||
queryParams.clear();
|
||||
|
||||
queryParams.add("hidden", "");
|
||||
assertThat(commentQuery.getHidden()).isNull();
|
||||
queryParams.clear();
|
||||
|
||||
queryParams.add("hidden", "1");
|
||||
assertThat(commentQuery.getHidden()).isFalse();
|
||||
queryParams.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAllowNotification() {
|
||||
MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
|
||||
queryParams.add("allowNotification", "true");
|
||||
CommentQuery commentQuery = getCommentQuery(queryParams);
|
||||
assertThat(commentQuery.getAllowNotification()).isTrue();
|
||||
queryParams.clear();
|
||||
|
||||
queryParams.add("allowNotification", "");
|
||||
assertThat(commentQuery.getAllowNotification()).isNull();
|
||||
queryParams.clear();
|
||||
|
||||
queryParams.add("allowNotification", "1");
|
||||
assertThat(commentQuery.getAllowNotification()).isFalse();
|
||||
queryParams.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getTop() {
|
||||
MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
|
||||
queryParams.add("top", "true");
|
||||
CommentQuery commentQuery = getCommentQuery(queryParams);
|
||||
assertThat(commentQuery.getTop()).isTrue();
|
||||
queryParams.clear();
|
||||
|
||||
queryParams.add("top", "");
|
||||
assertThat(commentQuery.getTop()).isNull();
|
||||
queryParams.clear();
|
||||
|
||||
queryParams.add("top", "1");
|
||||
assertThat(commentQuery.getTop()).isFalse();
|
||||
queryParams.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getOwnerKind() {
|
||||
MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
|
||||
queryParams.add("ownerKind", "test-owner-kind");
|
||||
CommentQuery commentQuery = getCommentQuery(queryParams);
|
||||
assertThat(commentQuery.getOwnerKind()).isEqualTo("test-owner-kind");
|
||||
queryParams.clear();
|
||||
|
||||
queryParams.add("ownerKind", "");
|
||||
assertThat(commentQuery.getOwnerKind()).isNull();
|
||||
queryParams.clear();
|
||||
|
||||
queryParams.add("ownerKind", null);
|
||||
assertThat(commentQuery.getOwnerKind()).isNull();
|
||||
queryParams.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getOwnerName() {
|
||||
MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
|
||||
queryParams.add("ownerName", "test-owner-name");
|
||||
CommentQuery commentQuery = getCommentQuery(queryParams);
|
||||
assertThat(commentQuery.getOwnerName()).isEqualTo("test-owner-name");
|
||||
queryParams.clear();
|
||||
|
||||
queryParams.add("ownerName", "");
|
||||
assertThat(commentQuery.getOwnerName()).isNull();
|
||||
queryParams.clear();
|
||||
|
||||
queryParams.add("ownerName", null);
|
||||
assertThat(commentQuery.getOwnerName()).isNull();
|
||||
queryParams.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getSubjectKind() {
|
||||
MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
|
||||
queryParams.add("subjectKind", "test-subject-kind");
|
||||
CommentQuery commentQuery = getCommentQuery(queryParams);
|
||||
assertThat(commentQuery.getSubjectKind()).isEqualTo("test-subject-kind");
|
||||
queryParams.clear();
|
||||
|
||||
queryParams.add("subjectKind", "");
|
||||
assertThat(commentQuery.getSubjectKind()).isNull();
|
||||
queryParams.clear();
|
||||
|
||||
queryParams.add("subjectKind", null);
|
||||
assertThat(commentQuery.getSubjectKind()).isNull();
|
||||
queryParams.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getSubjectName() {
|
||||
MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
|
||||
queryParams.add("subjectName", "test-subject-name");
|
||||
CommentQuery commentQuery = getCommentQuery(queryParams);
|
||||
assertThat(commentQuery.getSubjectName()).isEqualTo("test-subject-name");
|
||||
queryParams.clear();
|
||||
|
||||
queryParams.add("subjectName", "");
|
||||
assertThat(commentQuery.getSubjectName()).isNull();
|
||||
queryParams.clear();
|
||||
|
||||
queryParams.add("subjectName", null);
|
||||
assertThat(commentQuery.getSubjectName()).isNull();
|
||||
queryParams.clear();
|
||||
}
|
||||
|
||||
@Nested
|
||||
class CommentSortTest {
|
||||
@Test
|
||||
void sortByCreateTimeAsc() {
|
||||
MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
|
||||
queryParams.set("sort", "creationTimestamp,asc");
|
||||
CommentQuery commentQuery = getCommentQuery(queryParams);
|
||||
Comparator<Comment> createTimeSorter = commentQuery.toComparator();
|
||||
List<String> commentNames = comments().stream()
|
||||
.sorted(createTimeSorter)
|
||||
.map(comment -> comment.getMetadata().getName())
|
||||
.toList();
|
||||
assertThat(commentNames).isEqualTo(List.of("B", "C", "A"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sortByCreateTimeDesc() {
|
||||
MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
|
||||
queryParams.add("sort", "creationTimestamp,desc");
|
||||
Comparator<Comment> createTimeSorter = getCommentQuery(queryParams).toComparator();
|
||||
List<String> commentNames = comments().stream()
|
||||
.sorted(createTimeSorter)
|
||||
.map(comment -> comment.getMetadata().getName())
|
||||
.toList();
|
||||
assertThat(commentNames).isEqualTo(List.of("A", "B", "C"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sortByReplyCountAsc() {
|
||||
MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
|
||||
queryParams.add("sort", "replyCount,asc");
|
||||
Comparator<Comment> comparator = getCommentQuery(queryParams).toComparator();
|
||||
List<String> commentNames = comments().stream()
|
||||
.sorted(comparator)
|
||||
.map(comment -> comment.getMetadata().getName())
|
||||
.toList();
|
||||
assertThat(commentNames).isEqualTo(List.of("A", "B", "C"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sortByReplyCountDesc() {
|
||||
MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
|
||||
queryParams.add("sort", "replyCount,desc");
|
||||
Comparator<Comment> comparator = getCommentQuery(queryParams).toComparator();
|
||||
List<String> commentNames = comments().stream()
|
||||
.sorted(comparator)
|
||||
.map(comment -> comment.getMetadata().getName())
|
||||
.toList();
|
||||
assertThat(commentNames).isEqualTo(List.of("C", "B", "A"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sortByLastReplyTimeAsc() {
|
||||
MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
|
||||
queryParams.add("sort", "lastReplyTime,asc");
|
||||
Comparator<Comment> comparator = getCommentQuery(queryParams).toComparator();
|
||||
List<String> commentNames = comments().stream()
|
||||
.sorted(comparator)
|
||||
.map(comment -> comment.getMetadata().getName())
|
||||
.toList();
|
||||
assertThat(commentNames).isEqualTo(List.of("C", "A", "B"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sortByLastReplyTimeDesc() {
|
||||
MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
|
||||
queryParams.add("sort", "lastReplyTime,desc");
|
||||
Comparator<Comment> comparator = getCommentQuery(queryParams).toComparator();
|
||||
List<String> commentNames = comments().stream()
|
||||
.sorted(comparator)
|
||||
.map(comment -> comment.getMetadata().getName())
|
||||
.toList();
|
||||
assertThat(commentNames).isEqualTo(List.of("B", "A", "C"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sortByDefaultDesc() {
|
||||
MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
|
||||
Comparator<Comment> comparator = getCommentQuery(queryParams).toComparator();
|
||||
List<String> commentNames = comments().stream()
|
||||
.sorted(comparator)
|
||||
.map(comment -> comment.getMetadata().getName())
|
||||
.toList();
|
||||
assertThat(commentNames).isEqualTo(List.of("B", "A", "C"));
|
||||
|
||||
List<String> commentList = commentsIncludeNoReply().stream()
|
||||
.sorted(comparator)
|
||||
.map(comment -> comment.getMetadata().getName())
|
||||
.toList();
|
||||
assertThat(commentList).isEqualTo(List.of("D", "E", "B", "A", "C"));
|
||||
}
|
||||
|
||||
List<Comment> comments() {
|
||||
final Instant now = Instant.now();
|
||||
Comment commentA = new Comment();
|
||||
commentA.setMetadata(new Metadata());
|
||||
commentA.getMetadata().setName("A");
|
||||
// create time
|
||||
commentA.getMetadata().setCreationTimestamp(now.plusSeconds(10));
|
||||
commentA.setSpec(new Comment.CommentSpec());
|
||||
commentA.getSpec().setCreationTime(commentA.getMetadata().getCreationTimestamp());
|
||||
|
||||
commentA.setStatus(new Comment.CommentStatus());
|
||||
// last reply time
|
||||
commentA.getStatus().setLastReplyTime(now.plusSeconds(5));
|
||||
// reply count
|
||||
commentA.getStatus().setReplyCount(3);
|
||||
|
||||
Comment commentB = new Comment();
|
||||
commentB.setMetadata(new Metadata());
|
||||
commentB.getMetadata().setName("B");
|
||||
commentB.getMetadata().setCreationTimestamp(now.plusSeconds(5));
|
||||
commentB.setSpec(new Comment.CommentSpec());
|
||||
commentB.setStatus(new Comment.CommentStatus());
|
||||
commentB.getStatus().setLastReplyTime(now.plusSeconds(15));
|
||||
commentB.getStatus().setReplyCount(8);
|
||||
commentB.getSpec().setCreationTime(commentB.getMetadata().getCreationTimestamp());
|
||||
|
||||
Comment commentC = new Comment();
|
||||
commentC.setMetadata(new Metadata());
|
||||
commentC.getMetadata().setName("C");
|
||||
|
||||
commentC.getMetadata().setCreationTimestamp(now.plusSeconds(5));
|
||||
|
||||
commentC.setSpec(new Comment.CommentSpec());
|
||||
commentC.setStatus(new Comment.CommentStatus());
|
||||
commentC.getStatus().setLastReplyTime(now.plusSeconds(3));
|
||||
commentC.getStatus().setReplyCount(10);
|
||||
commentC.getSpec().setCreationTime(commentC.getMetadata().getCreationTimestamp());
|
||||
|
||||
return List.of(commentA, commentB, commentC);
|
||||
}
|
||||
|
||||
List<Comment> commentsIncludeNoReply() {
|
||||
|
||||
final Instant now = Instant.now();
|
||||
Comment commentD = new Comment();
|
||||
commentD.setMetadata(new Metadata());
|
||||
commentD.getMetadata().setName("D");
|
||||
|
||||
commentD.getMetadata().setCreationTimestamp(now.plusSeconds(50));
|
||||
|
||||
commentD.setSpec(new Comment.CommentSpec());
|
||||
commentD.getSpec().setCreationTime(commentD.getMetadata().getCreationTimestamp());
|
||||
commentD.setStatus(new Comment.CommentStatus());
|
||||
|
||||
Comment commentE = new Comment();
|
||||
commentE.setMetadata(new Metadata());
|
||||
commentE.getMetadata().setName("E");
|
||||
|
||||
commentE.getMetadata().setCreationTimestamp(now.plusSeconds(20));
|
||||
commentE.setSpec(new Comment.CommentSpec());
|
||||
commentE.getSpec().setCreationTime(commentE.getMetadata().getCreationTimestamp());
|
||||
commentE.setStatus(new Comment.CommentStatus());
|
||||
|
||||
List<Comment> comments = new ArrayList<>(comments());
|
||||
comments.add(commentD);
|
||||
comments.add(commentE);
|
||||
|
||||
return comments;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,8 +1,6 @@
|
|||
package run.halo.app.content.comment;
|
||||
|
||||
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.lenient;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
@ -12,7 +10,6 @@ import static org.mockito.Mockito.when;
|
|||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Predicate;
|
||||
import org.json.JSONException;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
@ -37,8 +34,10 @@ import run.halo.app.core.extension.User;
|
|||
import run.halo.app.core.extension.content.Comment;
|
||||
import run.halo.app.core.extension.content.Post;
|
||||
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.Metadata;
|
||||
import run.halo.app.extension.PageRequest;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.extension.Ref;
|
||||
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
|
||||
|
@ -81,7 +80,7 @@ class CommentServiceImplTest {
|
|||
lenient().when(environmentFetcher.fetchComment()).thenReturn(Mono.just(commentSetting));
|
||||
|
||||
ListResult<Comment> comments = new ListResult<>(1, 10, 3, comments());
|
||||
when(client.list(eq(Comment.class), any(), any(), anyInt(), anyInt()))
|
||||
when(client.listBy(eq(Comment.class), any(ListOptions.class), any(PageRequest.class)))
|
||||
.thenReturn(Mono.just(comments));
|
||||
|
||||
when(userService.getUserOrGhost(eq("A-owner")))
|
||||
|
@ -128,8 +127,7 @@ class CommentServiceImplTest {
|
|||
ServerHttpRequest httpRequest = mock(ServerHttpRequest.class);
|
||||
when(exchange.getRequest()).thenReturn(httpRequest);
|
||||
when(httpRequest.getQueryParams()).thenReturn(queryParams);
|
||||
Mono<ListResult<ListedComment>> listResultMono =
|
||||
commentService.listComment(new CommentQuery(request));
|
||||
final var listResultMono = commentService.listComment(new CommentQuery(request));
|
||||
Counter counterA = new Counter();
|
||||
counterA.setUpvote(3);
|
||||
String commentACounter = MeterUtils.nameOf(Comment.class, "A");
|
||||
|
@ -214,49 +212,6 @@ class CommentServiceImplTest {
|
|||
true);
|
||||
}
|
||||
|
||||
@Test
|
||||
void commentPredicate() {
|
||||
MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
|
||||
queryParams.add("keyword", "hello");
|
||||
queryParams.add("approved", "true");
|
||||
queryParams.add("hidden", "false");
|
||||
queryParams.add("allowNotification", "true");
|
||||
queryParams.add("top", "false");
|
||||
queryParams.add("ownerKind", "User");
|
||||
queryParams.add("ownerName", "fake-user");
|
||||
queryParams.add("subjectKind", "Post");
|
||||
queryParams.add("subjectName", "fake-post");
|
||||
|
||||
MockServerRequest request = MockServerRequest.builder()
|
||||
.queryParams(queryParams)
|
||||
.exchange(mock(ServerWebExchange.class))
|
||||
.build();
|
||||
final Predicate<Comment> predicate = new CommentQuery(request).toPredicate();
|
||||
|
||||
Comment comment = comment("A");
|
||||
comment.getSpec().setRaw("hello-world");
|
||||
comment.getSpec().setApproved(true);
|
||||
comment.getSpec().setHidden(false);
|
||||
comment.getSpec().setAllowNotification(true);
|
||||
comment.getSpec().setTop(false);
|
||||
Comment.CommentOwner commentOwner = new Comment.CommentOwner();
|
||||
commentOwner.setKind("User");
|
||||
commentOwner.setName("fake-user");
|
||||
commentOwner.setDisplayName("fake-user-display-name");
|
||||
comment.getSpec().setOwner(commentOwner);
|
||||
comment.getSpec().setSubjectRef(Ref.of(post()));
|
||||
assertThat(predicate.test(comment)).isTrue();
|
||||
|
||||
queryParams.remove("keyword");
|
||||
queryParams.add("keyword", "nothing");
|
||||
request = MockServerRequest.builder()
|
||||
.queryParams(queryParams)
|
||||
.exchange(mock(ServerWebExchange.class))
|
||||
.build();
|
||||
final Predicate<Comment> predicateTwo = new CommentQuery(request).toPredicate();
|
||||
assertThat(predicateTwo.test(comment)).isFalse();
|
||||
}
|
||||
|
||||
private List<Comment> comments() {
|
||||
Comment a = comment("A");
|
||||
a.getSpec().getOwner().setKind(Comment.CommentOwner.KIND_EMAIL);
|
||||
|
|
|
@ -31,6 +31,7 @@ import run.halo.app.content.comment.ReplyService;
|
|||
import run.halo.app.core.extension.content.Comment;
|
||||
import run.halo.app.core.extension.content.Reply;
|
||||
import run.halo.app.extension.ListResult;
|
||||
import run.halo.app.extension.PageRequest;
|
||||
import run.halo.app.extension.Ref;
|
||||
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
|
||||
import run.halo.app.theme.finders.CommentFinder;
|
||||
|
@ -77,7 +78,7 @@ class CommentFinderEndpointTest {
|
|||
|
||||
@Test
|
||||
void listComments() {
|
||||
when(commentPublicQueryService.list(any(), anyInt(), anyInt(), any()))
|
||||
when(commentPublicQueryService.list(any(), any(PageRequest.class)))
|
||||
.thenReturn(Mono.just(new ListResult<>(1, 10, 0, List.of())));
|
||||
|
||||
Ref ref = new Ref();
|
||||
|
@ -86,21 +87,20 @@ class CommentFinderEndpointTest {
|
|||
ref.setKind("Post");
|
||||
ref.setName("test");
|
||||
webTestClient.get()
|
||||
.uri(uriBuilder -> {
|
||||
return uriBuilder.path("/comments")
|
||||
.uri(uriBuilder -> uriBuilder.path("/comments")
|
||||
.queryParam("group", ref.getGroup())
|
||||
.queryParam("version", ref.getVersion())
|
||||
.queryParam("kind", ref.getKind())
|
||||
.queryParam("name", ref.getName())
|
||||
.queryParam("page", 1)
|
||||
.queryParam("size", 10)
|
||||
.build();
|
||||
})
|
||||
.build())
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk();
|
||||
ArgumentCaptor<Ref> refCaptor = ArgumentCaptor.forClass(Ref.class);
|
||||
verify(commentPublicQueryService, times(1)).list(refCaptor.capture(), eq(1), eq(10), any());
|
||||
verify(commentPublicQueryService, times(1))
|
||||
.list(refCaptor.capture(), any(PageRequest.class));
|
||||
Ref value = refCaptor.getValue();
|
||||
assertThat(value).isEqualTo(ref);
|
||||
}
|
||||
|
|
|
@ -9,7 +9,6 @@ import java.time.Instant;
|
|||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import org.json.JSONException;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
|
@ -33,7 +32,6 @@ import run.halo.app.core.extension.service.UserService;
|
|||
import run.halo.app.extension.GroupVersionKind;
|
||||
import run.halo.app.extension.ListResult;
|
||||
import run.halo.app.extension.Metadata;
|
||||
import run.halo.app.extension.MetadataOperator;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.extension.Ref;
|
||||
import run.halo.app.infra.AnonymousUserConst;
|
||||
|
@ -70,103 +68,6 @@ class CommentPublicQueryServiceImplTest {
|
|||
|
||||
@Nested
|
||||
class ListCommentTest {
|
||||
@Test
|
||||
void listWhenUserNotLogin() {
|
||||
// Mock
|
||||
mockWhenListComment();
|
||||
|
||||
Ref ref = Ref.of("fake-post", GroupVersionKind.fromExtension(Post.class));
|
||||
commentPublicQueryService.list(ref, 1, 10)
|
||||
.as(StepVerifier::create)
|
||||
.consumeNextWith(listResult -> {
|
||||
assertThat(listResult.getTotal()).isEqualTo(2);
|
||||
assertThat(listResult.getItems().size()).isEqualTo(2);
|
||||
assertThat(listResult.getItems().get(0).getMetadata().getName())
|
||||
.isEqualTo("comment-approved");
|
||||
})
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(username = AnonymousUserConst.PRINCIPAL)
|
||||
void listWhenUserIsAnonymous() {
|
||||
// Mock
|
||||
mockWhenListComment();
|
||||
|
||||
Ref ref = Ref.of("fake-post", GroupVersionKind.fromExtension(Post.class));
|
||||
commentPublicQueryService.list(ref, 1, 10)
|
||||
.as(StepVerifier::create)
|
||||
.consumeNextWith(listResult -> {
|
||||
assertThat(listResult.getTotal()).isEqualTo(2);
|
||||
assertThat(listResult.getItems().size()).isEqualTo(2);
|
||||
assertThat(listResult.getItems().get(0).getMetadata().getName())
|
||||
.isEqualTo("comment-approved");
|
||||
assertThat(listResult.getItems().get(0).getStats().getUpvote()).isEqualTo(9);
|
||||
})
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(username = "fake-user")
|
||||
void listWhenUserLoggedIn() {
|
||||
mockWhenListComment();
|
||||
|
||||
Ref ref = Ref.of("fake-post", GroupVersionKind.fromExtension(Post.class));
|
||||
commentPublicQueryService.list(ref, 1, 10)
|
||||
.as(StepVerifier::create)
|
||||
.consumeNextWith(listResult -> {
|
||||
assertThat(listResult.getTotal()).isEqualTo(3);
|
||||
assertThat(listResult.getItems().size()).isEqualTo(3);
|
||||
assertThat(listResult.getItems().get(0).getMetadata().getName())
|
||||
.isEqualTo("comment-not-approved");
|
||||
assertThat(listResult.getItems().get(1).getMetadata().getName())
|
||||
.isEqualTo("comment-approved");
|
||||
})
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void commentComparator() {
|
||||
// 1, now + 1s, top, 0
|
||||
// 2, now + 2s, top, 1
|
||||
// 3, now + 3s, top, 2
|
||||
// 4, now + 4s, top, 2
|
||||
// 5, now + 4s, top, 3
|
||||
// 6, now + 1s, no, 0
|
||||
// 7, now + 2s, no, 0
|
||||
// 8, now + 3s, no, 0
|
||||
// 9, now + 3s, no, 0
|
||||
// 10, null, no, 0
|
||||
// 11, null, no, 1
|
||||
// 12, null, no, 3
|
||||
// 13, now + 3s, no, 3
|
||||
Instant now = Instant.now();
|
||||
var comment1 = commentForCompare("1", now.plusSeconds(1), true, 0);
|
||||
var comment2 = commentForCompare("2", now.plusSeconds(2), true, 1);
|
||||
var comment3 = commentForCompare("3", now.plusSeconds(3), true, 2);
|
||||
var comment4 = commentForCompare("4", now.plusSeconds(4), true, 2);
|
||||
var comment5 = commentForCompare("5", now.plusSeconds(4), true, 3);
|
||||
var comment6 = commentForCompare("6", now.plusSeconds(4), true, 3);
|
||||
var comment7 = commentForCompare("7", now.plusSeconds(1), false, 0);
|
||||
var comment8 = commentForCompare("8", now.plusSeconds(2), false, 0);
|
||||
var comment9 = commentForCompare("9", now.plusSeconds(3), false, 0);
|
||||
var comment10 = commentForCompare("10", now.plusSeconds(3), false, 0);
|
||||
var comment11 = commentForCompare("11", null, false, 0);
|
||||
var comment12 = commentForCompare("12", null, false, 1);
|
||||
var comment13 = commentForCompare("13", null, false, 3);
|
||||
var comment14 = commentForCompare("14", now.plusSeconds(3), false, 3);
|
||||
|
||||
var result = Stream.of(comment1, comment2, comment3, comment4, comment5, comment6,
|
||||
comment7, comment8, comment9, comment10, comment11, comment12, comment13,
|
||||
comment14)
|
||||
.sorted(CommentPublicQueryServiceImpl.defaultComparator())
|
||||
.map(Comment::getMetadata)
|
||||
.map(MetadataOperator::getName)
|
||||
.collect(Collectors.joining(", "));
|
||||
assertThat(result).isEqualTo("1, 2, 4, 3, 5, 6, 10, 14, 9, 8, 7, 11, 12, 13");
|
||||
}
|
||||
|
||||
@Test
|
||||
void desensitizeComment() throws JSONException {
|
||||
var commentOwner = new Comment.CommentOwner();
|
||||
|
|
|
@ -0,0 +1,318 @@
|
|||
package run.halo.app.theme.finders.impl;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.security.test.context.support.WithMockUser;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
import run.halo.app.core.extension.User;
|
||||
import run.halo.app.core.extension.content.Comment;
|
||||
import run.halo.app.core.extension.content.Post;
|
||||
import run.halo.app.extension.Extension;
|
||||
import run.halo.app.extension.ExtensionStoreUtil;
|
||||
import run.halo.app.extension.GroupVersionKind;
|
||||
import run.halo.app.extension.ListOptions;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.extension.Ref;
|
||||
import run.halo.app.extension.SchemeManager;
|
||||
import run.halo.app.extension.index.IndexerFactory;
|
||||
import run.halo.app.extension.store.ReactiveExtensionStoreClient;
|
||||
import run.halo.app.infra.AnonymousUserConst;
|
||||
import run.halo.app.infra.utils.JsonUtils;
|
||||
|
||||
@DirtiesContext
|
||||
@SpringBootTest
|
||||
class CommentPublicQueryServiceIntegrationTest {
|
||||
|
||||
@Autowired
|
||||
private SchemeManager schemeManager;
|
||||
|
||||
@Autowired
|
||||
private ReactiveExtensionClient client;
|
||||
|
||||
@Autowired
|
||||
private ReactiveExtensionStoreClient storeClient;
|
||||
|
||||
@Autowired
|
||||
private IndexerFactory indexerFactory;
|
||||
|
||||
Mono<Extension> deleteImmediately(Extension extension) {
|
||||
var name = extension.getMetadata().getName();
|
||||
var scheme = schemeManager.get(extension.getClass());
|
||||
// un-index
|
||||
var indexer = indexerFactory.getIndexer(extension.groupVersionKind());
|
||||
indexer.unIndexRecord(extension.getMetadata().getName());
|
||||
|
||||
// delete from db
|
||||
var storeName = ExtensionStoreUtil.buildStoreName(scheme, name);
|
||||
return storeClient.delete(storeName, extension.getMetadata().getVersion())
|
||||
.thenReturn(extension);
|
||||
}
|
||||
|
||||
@Nested
|
||||
class CommentListTest {
|
||||
private final List<Comment> storedComments = commentsForStore();
|
||||
|
||||
@Autowired
|
||||
private CommentPublicQueryServiceImpl commentPublicQueryService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
Flux.fromIterable(storedComments)
|
||||
.flatMap(comment -> client.create(comment))
|
||||
.as(StepVerifier::create)
|
||||
.expectNextCount(storedComments.size())
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
Flux.fromIterable(storedComments)
|
||||
.flatMap(CommentPublicQueryServiceIntegrationTest.this::deleteImmediately)
|
||||
.as(StepVerifier::create)
|
||||
.expectNextCount(storedComments.size())
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void listWhenUserNotLogin() {
|
||||
Ref ref = Ref.of("fake-post", GroupVersionKind.fromExtension(Post.class));
|
||||
commentPublicQueryService.list(ref, 1, 10)
|
||||
.as(StepVerifier::create)
|
||||
.consumeNextWith(listResult -> {
|
||||
assertThat(listResult.getTotal()).isEqualTo(2);
|
||||
assertThat(listResult.getItems().size()).isEqualTo(2);
|
||||
assertThat(listResult.getItems().get(0).getMetadata().getName())
|
||||
.isEqualTo("comment-approved");
|
||||
})
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(username = AnonymousUserConst.PRINCIPAL)
|
||||
void listWhenUserIsAnonymous() {
|
||||
Ref ref = Ref.of("fake-post", GroupVersionKind.fromExtension(Post.class));
|
||||
commentPublicQueryService.list(ref, 1, 10)
|
||||
.as(StepVerifier::create)
|
||||
.consumeNextWith(listResult -> {
|
||||
assertThat(listResult.getTotal()).isEqualTo(2);
|
||||
assertThat(listResult.getItems().size()).isEqualTo(2);
|
||||
assertThat(listResult.getItems().get(0).getMetadata().getName())
|
||||
.isEqualTo("comment-approved");
|
||||
})
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(username = "fake-user")
|
||||
void listWhenUserLoggedIn() {
|
||||
Ref ref = Ref.of("fake-post", GroupVersionKind.fromExtension(Post.class));
|
||||
commentPublicQueryService.list(ref, 1, 10)
|
||||
.as(StepVerifier::create)
|
||||
.consumeNextWith(listResult -> {
|
||||
assertThat(listResult.getTotal()).isEqualTo(3);
|
||||
assertThat(listResult.getItems().size()).isEqualTo(3);
|
||||
assertThat(listResult.getItems().get(0).getMetadata().getName())
|
||||
.isEqualTo("comment-approved");
|
||||
assertThat(listResult.getItems().get(1).getMetadata().getName())
|
||||
.isEqualTo("comment-approved-but-another-owner");
|
||||
assertThat(listResult.getItems().get(2).getMetadata().getName())
|
||||
.isEqualTo("comment-not-approved");
|
||||
})
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
List<Comment> commentsForStore() {
|
||||
// Mock
|
||||
Comment commentNotApproved = fakeComment();
|
||||
commentNotApproved.getMetadata().setName("comment-not-approved");
|
||||
commentNotApproved.getSpec().setApproved(false);
|
||||
|
||||
Comment commentApproved = fakeComment();
|
||||
commentApproved.getMetadata().setName("comment-approved");
|
||||
commentApproved.getSpec().setApproved(true);
|
||||
|
||||
Comment notApprovedWithAnonymous = fakeComment();
|
||||
notApprovedWithAnonymous.getMetadata().setName("comment-not-approved-anonymous");
|
||||
notApprovedWithAnonymous.getSpec().setApproved(false);
|
||||
notApprovedWithAnonymous.getSpec().getOwner().setName(AnonymousUserConst.PRINCIPAL);
|
||||
|
||||
Comment commentApprovedButAnotherOwner = fakeComment();
|
||||
commentApprovedButAnotherOwner.getMetadata()
|
||||
.setName("comment-approved-but-another-owner");
|
||||
commentApprovedButAnotherOwner.getSpec().setApproved(true);
|
||||
commentApprovedButAnotherOwner.getSpec().getOwner().setName("another");
|
||||
|
||||
Comment commentNotApprovedAndAnotherOwner = fakeComment();
|
||||
commentNotApprovedAndAnotherOwner.getMetadata()
|
||||
.setName("comment-not-approved-and-another");
|
||||
commentNotApprovedAndAnotherOwner.getSpec().setApproved(false);
|
||||
commentNotApprovedAndAnotherOwner.getSpec().getOwner().setName("another");
|
||||
|
||||
Comment notApprovedAndAnotherRef = fakeComment();
|
||||
notApprovedAndAnotherRef.getMetadata()
|
||||
.setName("comment-not-approved-and-another-ref");
|
||||
notApprovedAndAnotherRef.getSpec().setApproved(false);
|
||||
Ref anotherRef =
|
||||
Ref.of("another-fake-post", GroupVersionKind.fromExtension(Post.class));
|
||||
notApprovedAndAnotherRef.getSpec().setSubjectRef(anotherRef);
|
||||
|
||||
return List.of(
|
||||
commentNotApproved,
|
||||
commentApproved,
|
||||
commentApprovedButAnotherOwner,
|
||||
commentNotApprovedAndAnotherOwner,
|
||||
notApprovedWithAnonymous,
|
||||
notApprovedAndAnotherRef
|
||||
);
|
||||
}
|
||||
|
||||
Comment fakeComment() {
|
||||
Comment comment = createComment();
|
||||
comment.getMetadata().setDeletionTimestamp(null);
|
||||
comment.getMetadata().setName("fake-comment");
|
||||
|
||||
comment.getSpec().setRaw("fake-raw");
|
||||
comment.getSpec().setContent("fake-content");
|
||||
comment.getSpec().setHidden(false);
|
||||
comment.getSpec()
|
||||
.setSubjectRef(Ref.of("fake-post", GroupVersionKind.fromExtension(Post.class)));
|
||||
Comment.CommentOwner commentOwner = new Comment.CommentOwner();
|
||||
commentOwner.setKind(User.KIND);
|
||||
commentOwner.setName("fake-user");
|
||||
commentOwner.setDisplayName("fake-display-name");
|
||||
comment.getSpec().setOwner(commentOwner);
|
||||
return comment;
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class CommentDefaultSortTest {
|
||||
private final List<Comment> commentList = createCommentList();
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
Flux.fromIterable(commentList)
|
||||
.flatMap(comment -> client.create(comment))
|
||||
.as(StepVerifier::create)
|
||||
.expectNextCount(commentList.size())
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
Flux.fromIterable(commentList)
|
||||
.flatMap(CommentPublicQueryServiceIntegrationTest.this::deleteImmediately)
|
||||
.as(StepVerifier::create)
|
||||
.expectNextCount(commentList.size())
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void sortTest() {
|
||||
var comments =
|
||||
client.listAll(Comment.class, new ListOptions(),
|
||||
CommentPublicQueryServiceImpl.defaultSort())
|
||||
.collectList()
|
||||
.block();
|
||||
assertThat(comments).isNotNull();
|
||||
|
||||
var result = comments.stream()
|
||||
.map(comment -> comment.getMetadata().getName())
|
||||
.collect(Collectors.joining(", "));
|
||||
assertThat(result).isEqualTo("1, 2, 4, 3, 5, 6, 9, 10, 14, 8, 7, 11, 12, 13");
|
||||
}
|
||||
|
||||
List<Comment> createCommentList() {
|
||||
// 1, now + 1s, top, 0
|
||||
// 2, now + 2s, top, 1
|
||||
// 3, now + 3s, top, 2
|
||||
// 4, now + 4s, top, 2
|
||||
// 5, now + 4s, top, 3
|
||||
// 6, now + 1s, no, 0
|
||||
// 7, now + 2s, no, 0
|
||||
// 8, now + 3s, no, 0
|
||||
// 9, now + 3s, no, 0
|
||||
// 10, null, no, 0
|
||||
// 11, null, no, 1
|
||||
// 12, null, no, 3
|
||||
// 13, now + 3s, no, 3
|
||||
Instant now = Instant.now();
|
||||
var comment1 = commentForCompare("1", now.plusSeconds(1), true, 0);
|
||||
var comment2 = commentForCompare("2", now.plusSeconds(2), true, 1);
|
||||
var comment3 = commentForCompare("3", now.plusSeconds(3), true, 2);
|
||||
var comment4 = commentForCompare("4", now.plusSeconds(4), true, 2);
|
||||
var comment5 = commentForCompare("5", now.plusSeconds(4), true, 3);
|
||||
var comment6 = commentForCompare("6", now.plusSeconds(4), true, 3);
|
||||
var comment7 = commentForCompare("7", now.plusSeconds(1), false, 0);
|
||||
var comment8 = commentForCompare("8", now.plusSeconds(2), false, 0);
|
||||
var comment9 = commentForCompare("9", now.plusSeconds(3), false, 0);
|
||||
var comment10 = commentForCompare("10", now.plusSeconds(3), false, 0);
|
||||
var comment11 = commentForCompare("11", now, false, 0);
|
||||
var comment12 = commentForCompare("12", now, false, 1);
|
||||
var comment13 = commentForCompare("13", now, false, 3);
|
||||
var comment14 = commentForCompare("14", now.plusSeconds(3), false, 3);
|
||||
|
||||
return List.of(comment1, comment2, comment3, comment4, comment5, comment6, comment7,
|
||||
comment8, comment9, comment10, comment11, comment12, comment13, comment14);
|
||||
}
|
||||
|
||||
Comment commentForCompare(String name, Instant creationTime, boolean top, int priority) {
|
||||
var comment = createComment();
|
||||
comment.getMetadata().setName(name);
|
||||
comment.getMetadata().setCreationTimestamp(creationTime);
|
||||
comment.getSpec().setCreationTime(creationTime);
|
||||
comment.getSpec().setTop(top);
|
||||
comment.getSpec().setPriority(priority);
|
||||
return comment;
|
||||
}
|
||||
}
|
||||
|
||||
Comment createComment() {
|
||||
return JsonUtils.jsonToObject("""
|
||||
{
|
||||
"spec": {
|
||||
"raw": "fake-raw",
|
||||
"content": "fake-content",
|
||||
"owner": {
|
||||
"kind": "User",
|
||||
"name": "fake-user"
|
||||
},
|
||||
"userAgent": "",
|
||||
"ipAddress": "",
|
||||
"approvedTime": "2024-02-28T09:15:16.095Z",
|
||||
"creationTime": "2024-02-28T06:23:42.923294424Z",
|
||||
"priority": 0,
|
||||
"top": false,
|
||||
"allowNotification": false,
|
||||
"approved": true,
|
||||
"hidden": false,
|
||||
"subjectRef": {
|
||||
"group": "content.halo.run",
|
||||
"version": "v1alpha1",
|
||||
"kind": "SinglePage",
|
||||
"name": "67"
|
||||
},
|
||||
"lastReadTime": "2024-02-29T03:39:04.230Z"
|
||||
},
|
||||
"apiVersion": "content.halo.run/v1alpha1",
|
||||
"kind": "Comment",
|
||||
"metadata": {
|
||||
"name": "fake-comment",
|
||||
"creationTimestamp": "2024-02-28T06:23:42.923439037Z"
|
||||
}
|
||||
}
|
||||
""", Comment.class);
|
||||
}
|
||||
}
|
|
@ -89,10 +89,22 @@ const {
|
|||
keyword,
|
||||
],
|
||||
queryFn: async () => {
|
||||
const fieldSelectorMap: Record<string, string | boolean | undefined> = {
|
||||
"spec.approved": selectedApprovedStatus.value,
|
||||
};
|
||||
|
||||
const fieldSelector = Object.entries(fieldSelectorMap)
|
||||
.map(([key, value]) => {
|
||||
if (value !== undefined) {
|
||||
return `${key}=${value}`;
|
||||
}
|
||||
})
|
||||
.filter(Boolean) as string[];
|
||||
|
||||
const { data } = await apiClient.comment.listComments({
|
||||
fieldSelector,
|
||||
page: page.value,
|
||||
size: size.value,
|
||||
approved: selectedApprovedStatus.value,
|
||||
sort: [selectedSort.value].filter(Boolean) as string[],
|
||||
keyword: keyword.value,
|
||||
ownerName: selectedUser.value,
|
||||
|
@ -105,10 +117,10 @@ const {
|
|||
return data.items;
|
||||
},
|
||||
refetchInterval(data) {
|
||||
const deletingComments = data?.filter(
|
||||
const hasDeletingData = data?.some(
|
||||
(comment) => !!comment.comment.metadata.deletionTimestamp
|
||||
);
|
||||
return deletingComments?.length ? 1000 : false;
|
||||
return hasDeletingData ? 1000 : false;
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -299,33 +311,33 @@ const handleApproveInBatch = async () => {
|
|||
label: t(
|
||||
'core.comment.filters.sort.items.last_reply_time_desc'
|
||||
),
|
||||
value: 'lastReplyTime,desc',
|
||||
value: 'status.lastReplyTime,desc',
|
||||
},
|
||||
{
|
||||
label: t(
|
||||
'core.comment.filters.sort.items.last_reply_time_asc'
|
||||
),
|
||||
value: 'lastReplyTime,asc',
|
||||
value: 'status.lastReplyTime,asc',
|
||||
},
|
||||
{
|
||||
label: t(
|
||||
'core.comment.filters.sort.items.reply_count_desc'
|
||||
),
|
||||
value: 'replyCount,desc',
|
||||
value: 'status.replyCount,desc',
|
||||
},
|
||||
{
|
||||
label: t('core.comment.filters.sort.items.reply_count_asc'),
|
||||
value: 'replyCount,asc',
|
||||
value: 'status.replyCount,asc',
|
||||
},
|
||||
{
|
||||
label: t(
|
||||
'core.comment.filters.sort.items.create_time_desc'
|
||||
),
|
||||
value: 'creationTimestamp,desc',
|
||||
value: 'metadata.creationTimestamp,desc',
|
||||
},
|
||||
{
|
||||
label: t('core.comment.filters.sort.items.create_time_asc'),
|
||||
value: 'creationTimestamp,asc',
|
||||
value: 'metadata.creationTimestamp,asc',
|
||||
},
|
||||
]"
|
||||
/>
|
||||
|
|
|
@ -179,10 +179,7 @@ export const ApiConsoleHaloRunV1alpha1CommentApiAxiosParamCreator = function (
|
|||
},
|
||||
/**
|
||||
* List comments.
|
||||
* @param {boolean} [allowNotification] Send notifications when there are new replies.
|
||||
* @param {boolean} [approved] Comments approved.
|
||||
* @param {Array<string>} [fieldSelector] Field selector for filtering.
|
||||
* @param {boolean} [hidden] The comment is hidden from the theme side.
|
||||
* @param {string} [keyword] Comments filtered by keyword.
|
||||
* @param {Array<string>} [labelSelector] Label selector for filtering.
|
||||
* @param {string} [ownerKind] Commenter kind.
|
||||
|
@ -190,17 +187,11 @@ export const ApiConsoleHaloRunV1alpha1CommentApiAxiosParamCreator = function (
|
|||
* @param {number} [page] The page number. Zero indicates no page.
|
||||
* @param {number} [size] Size of one page. Zero indicates no limit.
|
||||
* @param {Array<string>} [sort] Sort property and direction of the list result. Supported fields: creationTimestamp,replyCount,lastReplyTime
|
||||
* @param {string} [subjectKind] Comment subject kind.
|
||||
* @param {string} [subjectName] Comment subject name.
|
||||
* @param {boolean} [top] Comment top display.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
listComments: async (
|
||||
allowNotification?: boolean,
|
||||
approved?: boolean,
|
||||
fieldSelector?: Array<string>,
|
||||
hidden?: boolean,
|
||||
keyword?: string,
|
||||
labelSelector?: Array<string>,
|
||||
ownerKind?: string,
|
||||
|
@ -208,9 +199,6 @@ export const ApiConsoleHaloRunV1alpha1CommentApiAxiosParamCreator = function (
|
|||
page?: number,
|
||||
size?: number,
|
||||
sort?: Array<string>,
|
||||
subjectKind?: string,
|
||||
subjectName?: string,
|
||||
top?: boolean,
|
||||
options: AxiosRequestConfig = {}
|
||||
): Promise<RequestArgs> => {
|
||||
const localVarPath = `/apis/api.console.halo.run/v1alpha1/comments`;
|
||||
|
@ -237,22 +225,10 @@ export const ApiConsoleHaloRunV1alpha1CommentApiAxiosParamCreator = function (
|
|||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration);
|
||||
|
||||
if (allowNotification !== undefined) {
|
||||
localVarQueryParameter["allowNotification"] = allowNotification;
|
||||
}
|
||||
|
||||
if (approved !== undefined) {
|
||||
localVarQueryParameter["approved"] = approved;
|
||||
}
|
||||
|
||||
if (fieldSelector) {
|
||||
localVarQueryParameter["fieldSelector"] = fieldSelector;
|
||||
}
|
||||
|
||||
if (hidden !== undefined) {
|
||||
localVarQueryParameter["hidden"] = hidden;
|
||||
}
|
||||
|
||||
if (keyword !== undefined) {
|
||||
localVarQueryParameter["keyword"] = keyword;
|
||||
}
|
||||
|
@ -281,18 +257,6 @@ export const ApiConsoleHaloRunV1alpha1CommentApiAxiosParamCreator = function (
|
|||
localVarQueryParameter["sort"] = Array.from(sort);
|
||||
}
|
||||
|
||||
if (subjectKind !== undefined) {
|
||||
localVarQueryParameter["subjectKind"] = subjectKind;
|
||||
}
|
||||
|
||||
if (subjectName !== undefined) {
|
||||
localVarQueryParameter["subjectName"] = subjectName;
|
||||
}
|
||||
|
||||
if (top !== undefined) {
|
||||
localVarQueryParameter["top"] = top;
|
||||
}
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions =
|
||||
baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
|
@ -371,10 +335,7 @@ export const ApiConsoleHaloRunV1alpha1CommentApiFp = function (
|
|||
},
|
||||
/**
|
||||
* List comments.
|
||||
* @param {boolean} [allowNotification] Send notifications when there are new replies.
|
||||
* @param {boolean} [approved] Comments approved.
|
||||
* @param {Array<string>} [fieldSelector] Field selector for filtering.
|
||||
* @param {boolean} [hidden] The comment is hidden from the theme side.
|
||||
* @param {string} [keyword] Comments filtered by keyword.
|
||||
* @param {Array<string>} [labelSelector] Label selector for filtering.
|
||||
* @param {string} [ownerKind] Commenter kind.
|
||||
|
@ -382,17 +343,11 @@ export const ApiConsoleHaloRunV1alpha1CommentApiFp = function (
|
|||
* @param {number} [page] The page number. Zero indicates no page.
|
||||
* @param {number} [size] Size of one page. Zero indicates no limit.
|
||||
* @param {Array<string>} [sort] Sort property and direction of the list result. Supported fields: creationTimestamp,replyCount,lastReplyTime
|
||||
* @param {string} [subjectKind] Comment subject kind.
|
||||
* @param {string} [subjectName] Comment subject name.
|
||||
* @param {boolean} [top] Comment top display.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async listComments(
|
||||
allowNotification?: boolean,
|
||||
approved?: boolean,
|
||||
fieldSelector?: Array<string>,
|
||||
hidden?: boolean,
|
||||
keyword?: string,
|
||||
labelSelector?: Array<string>,
|
||||
ownerKind?: string,
|
||||
|
@ -400,9 +355,6 @@ export const ApiConsoleHaloRunV1alpha1CommentApiFp = function (
|
|||
page?: number,
|
||||
size?: number,
|
||||
sort?: Array<string>,
|
||||
subjectKind?: string,
|
||||
subjectName?: string,
|
||||
top?: boolean,
|
||||
options?: AxiosRequestConfig
|
||||
): Promise<
|
||||
(
|
||||
|
@ -411,10 +363,7 @@ export const ApiConsoleHaloRunV1alpha1CommentApiFp = function (
|
|||
) => AxiosPromise<ListedCommentList>
|
||||
> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.listComments(
|
||||
allowNotification,
|
||||
approved,
|
||||
fieldSelector,
|
||||
hidden,
|
||||
keyword,
|
||||
labelSelector,
|
||||
ownerKind,
|
||||
|
@ -422,9 +371,6 @@ export const ApiConsoleHaloRunV1alpha1CommentApiFp = function (
|
|||
page,
|
||||
size,
|
||||
sort,
|
||||
subjectKind,
|
||||
subjectName,
|
||||
top,
|
||||
options
|
||||
);
|
||||
return createRequestFunction(
|
||||
|
@ -492,10 +438,7 @@ export const ApiConsoleHaloRunV1alpha1CommentApiFactory = function (
|
|||
): AxiosPromise<ListedCommentList> {
|
||||
return localVarFp
|
||||
.listComments(
|
||||
requestParameters.allowNotification,
|
||||
requestParameters.approved,
|
||||
requestParameters.fieldSelector,
|
||||
requestParameters.hidden,
|
||||
requestParameters.keyword,
|
||||
requestParameters.labelSelector,
|
||||
requestParameters.ownerKind,
|
||||
|
@ -503,9 +446,6 @@ export const ApiConsoleHaloRunV1alpha1CommentApiFactory = function (
|
|||
requestParameters.page,
|
||||
requestParameters.size,
|
||||
requestParameters.sort,
|
||||
requestParameters.subjectKind,
|
||||
requestParameters.subjectName,
|
||||
requestParameters.top,
|
||||
options
|
||||
)
|
||||
.then((request) => request(axios, basePath));
|
||||
|
@ -554,20 +494,6 @@ export interface ApiConsoleHaloRunV1alpha1CommentApiCreateReplyRequest {
|
|||
* @interface ApiConsoleHaloRunV1alpha1CommentApiListCommentsRequest
|
||||
*/
|
||||
export interface ApiConsoleHaloRunV1alpha1CommentApiListCommentsRequest {
|
||||
/**
|
||||
* Send notifications when there are new replies.
|
||||
* @type {boolean}
|
||||
* @memberof ApiConsoleHaloRunV1alpha1CommentApiListComments
|
||||
*/
|
||||
readonly allowNotification?: boolean;
|
||||
|
||||
/**
|
||||
* Comments approved.
|
||||
* @type {boolean}
|
||||
* @memberof ApiConsoleHaloRunV1alpha1CommentApiListComments
|
||||
*/
|
||||
readonly approved?: boolean;
|
||||
|
||||
/**
|
||||
* Field selector for filtering.
|
||||
* @type {Array<string>}
|
||||
|
@ -575,13 +501,6 @@ export interface ApiConsoleHaloRunV1alpha1CommentApiListCommentsRequest {
|
|||
*/
|
||||
readonly fieldSelector?: Array<string>;
|
||||
|
||||
/**
|
||||
* The comment is hidden from the theme side.
|
||||
* @type {boolean}
|
||||
* @memberof ApiConsoleHaloRunV1alpha1CommentApiListComments
|
||||
*/
|
||||
readonly hidden?: boolean;
|
||||
|
||||
/**
|
||||
* Comments filtered by keyword.
|
||||
* @type {string}
|
||||
|
@ -630,27 +549,6 @@ export interface ApiConsoleHaloRunV1alpha1CommentApiListCommentsRequest {
|
|||
* @memberof ApiConsoleHaloRunV1alpha1CommentApiListComments
|
||||
*/
|
||||
readonly sort?: Array<string>;
|
||||
|
||||
/**
|
||||
* Comment subject kind.
|
||||
* @type {string}
|
||||
* @memberof ApiConsoleHaloRunV1alpha1CommentApiListComments
|
||||
*/
|
||||
readonly subjectKind?: string;
|
||||
|
||||
/**
|
||||
* Comment subject name.
|
||||
* @type {string}
|
||||
* @memberof ApiConsoleHaloRunV1alpha1CommentApiListComments
|
||||
*/
|
||||
readonly subjectName?: string;
|
||||
|
||||
/**
|
||||
* Comment top display.
|
||||
* @type {boolean}
|
||||
* @memberof ApiConsoleHaloRunV1alpha1CommentApiListComments
|
||||
*/
|
||||
readonly top?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -709,10 +607,7 @@ export class ApiConsoleHaloRunV1alpha1CommentApi extends BaseAPI {
|
|||
) {
|
||||
return ApiConsoleHaloRunV1alpha1CommentApiFp(this.configuration)
|
||||
.listComments(
|
||||
requestParameters.allowNotification,
|
||||
requestParameters.approved,
|
||||
requestParameters.fieldSelector,
|
||||
requestParameters.hidden,
|
||||
requestParameters.keyword,
|
||||
requestParameters.labelSelector,
|
||||
requestParameters.ownerKind,
|
||||
|
@ -720,9 +615,6 @@ export class ApiConsoleHaloRunV1alpha1CommentApi extends BaseAPI {
|
|||
requestParameters.page,
|
||||
requestParameters.size,
|
||||
requestParameters.sort,
|
||||
requestParameters.subjectKind,
|
||||
requestParameters.subjectName,
|
||||
requestParameters.top,
|
||||
options
|
||||
)
|
||||
.then((request) => request(this.axios, this.basePath));
|
||||
|
|
Loading…
Reference in New Issue