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
guqing 2024-03-08 21:32:06 +08:00 committed by GitHub
parent 92f2229460
commit 20d80f8f65
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 544 additions and 951 deletions

View File

@ -116,6 +116,10 @@ public class Comment extends AbstractExtension {
public String getAnnotation(String key) { public String getAnnotation(String key) {
return annotations == null ? null : annotations.get(key); return annotations == null ? null : annotations.get(key);
} }
public static String ownerIdentity(String kind, String name) {
return kind + "#" + name;
}
} }
@Data @Data
@ -132,6 +136,10 @@ public class Comment extends AbstractExtension {
private Boolean hasNewReply; 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) { public static int getUnreadReplyCount(List<Reply> replies, Instant lastReadTime) {
if (CollectionUtils.isEmpty(replies)) { if (CollectionUtils.isEmpty(replies)) {
return 0; return 0;

View File

@ -120,7 +120,7 @@ public class QueryFactory {
return new And(queries); 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); Collection<Query> queries = Arrays.asList(query1, query2);
return new And(queries); return new And(queries);
} }

View File

@ -1,29 +1,24 @@
package run.halo.app.content.comment; package run.halo.app.content.comment;
import static java.util.Comparator.comparing; import static run.halo.app.extension.index.query.QueryFactory.and;
import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; import static run.halo.app.extension.index.query.QueryFactory.contains;
import static run.halo.app.extension.router.selector.SelectorUtil.labelAndFieldSelectorToPredicate; 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.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema; 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.apache.commons.lang3.StringUtils;
import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort;
import org.springframework.web.reactive.function.server.ServerRequest; import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.server.ServerWebExchange; 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.content.Comment;
import run.halo.app.core.extension.endpoint.SortResolver; 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.Extension; import run.halo.app.extension.PageRequest;
import run.halo.app.extension.Ref; import run.halo.app.extension.PageRequestImpl;
import run.halo.app.extension.router.IListRequest; import run.halo.app.extension.router.IListRequest;
import run.halo.app.extension.router.selector.FieldSelector;
/** /**
* Query criteria for comment list. * Query criteria for comment list.
@ -34,12 +29,6 @@ import run.halo.app.extension.router.IListRequest;
public class CommentQuery extends IListRequest.QueryListRequest { public class CommentQuery extends IListRequest.QueryListRequest {
private final ServerWebExchange exchange; 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) { public CommentQuery(ServerRequest request) {
super(request.queryParams()); super(request.queryParams());
@ -52,26 +41,6 @@ public class CommentQuery extends IListRequest.QueryListRequest {
return StringUtils.isBlank(keyword) ? null : keyword; 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.") @Schema(description = "Commenter kind.")
public String getOwnerKind() { public String getOwnerKind() {
String ownerKind = queryParams.getFirst("ownerKind"); String ownerKind = queryParams.getFirst("ownerKind");
@ -84,18 +53,6 @@ public class CommentQuery extends IListRequest.QueryListRequest {
return StringUtils.isBlank(ownerName) ? null : ownerName; 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(uniqueItems = true,
arraySchema = @Schema(name = "sort", arraySchema = @Schema(name = "sort",
description = "Sort property and direction of the list result. Supported fields: " description = "Sort property and direction of the list result. Supported fields: "
@ -104,139 +61,35 @@ public class CommentQuery extends IListRequest.QueryListRequest {
implementation = String.class, implementation = String.class,
example = "creationTimestamp,desc")) example = "creationTimestamp,desc"))
public Sort getSort() { 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. * Convert to list options.
*
* @return comparator
*/ */
public Comparator<Comment> toComparator() { public ListOptions toListOptions() {
var sort = getSort(); var listOptions =
var creationTimestampOrder = sort.getOrderFor("creationTimestamp"); labelAndFieldSelectorToListOptions(getLabelSelector(), getFieldSelector());
List<Comparator<Comment>> comparators = new ArrayList<>(); var fieldQuery = listOptions.getFieldSelector().query();
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;
String keyword = getKeyword(); String keyword = getKeyword();
if (keyword != null) { if (StringUtils.isNotBlank(keyword)) {
predicate = predicate.and(comment -> { fieldQuery = and(fieldQuery, contains("spec.raw", keyword));
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);
});
} }
String ownerName = getOwnerName(); String ownerName = getOwnerName();
if (ownerName != null) { if (StringUtils.isNotBlank(ownerName)) {
predicate = predicate.and(comment -> { String ownerKind = StringUtils.defaultIfBlank(getOwnerKind(), User.KIND);
Comment.CommentOwner owner = comment.getSpec().getOwner(); fieldQuery = and(fieldQuery,
if (Comment.CommentOwner.KIND_EMAIL.equals(owner.getKind())) { equal("spec.owner", Comment.CommentOwner.ownerIdentity(ownerKind, ownerName)));
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);
});
} }
String subjectKind = getSubjectKind(); listOptions.setFieldSelector(FieldSelector.of(fieldQuery));
if (subjectKind != null) { return listOptions;
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);
} }
} }

View File

@ -50,9 +50,8 @@ public class CommentServiceImpl implements CommentService {
@Override @Override
public Mono<ListResult<ListedComment>> listComment(CommentQuery commentQuery) { public Mono<ListResult<ListedComment>> listComment(CommentQuery commentQuery) {
return this.client.list(Comment.class, commentQuery.toPredicate(), return this.client.listBy(Comment.class, commentQuery.toListOptions(),
commentQuery.toComparator(), commentQuery.toPageRequest())
commentQuery.getPage(), commentQuery.getSize())
.flatMap(comments -> Flux.fromStream(comments.get() .flatMap(comments -> Flux.fromStream(comments.get()
.map(this::toListedComment)) .map(this::toListedComment))
.concatMap(Function.identity()) .concatMap(Function.identity())
@ -138,32 +137,21 @@ public class CommentServiceImpl implements CommentService {
} }
private Mono<ListedComment> toListedComment(Comment comment) { private Mono<ListedComment> toListedComment(Comment comment) {
ListedComment.ListedCommentBuilder commentBuilder = ListedComment.builder() var builder = ListedComment.builder().comment(comment);
.comment(comment); // not empty
return Mono.just(commentBuilder) var ownerInfoMono = getCommentOwnerInfo(comment.getSpec().getOwner())
.flatMap(builder -> { .doOnNext(builder::owner);
Comment.CommentOwner owner = comment.getSpec().getOwner(); var subjectMono = getCommentSubject(comment.getSpec().getSubjectRef())
// not empty .doOnNext(builder::subject);
return getCommentOwnerInfo(owner) var statsMono = fetchStats(comment.getMetadata().getName())
.map(builder::owner); .doOnNext(builder::stats);
}) return Mono.when(ownerInfoMono, subjectMono, statsMono)
.flatMap(builder -> getCommentSubject(comment.getSpec().getSubjectRef()) .then(Mono.fromSupplier(builder::build));
.map(subject -> {
builder.subject(subject);
return builder;
})
.switchIfEmpty(Mono.just(builder))
)
.map(ListedComment.ListedCommentBuilder::build)
.flatMap(lc -> fetchStats(comment)
.doOnNext(lc::setStats)
.thenReturn(lc));
} }
Mono<CommentStats> fetchStats(Comment comment) { Mono<CommentStats> fetchStats(String commentName) {
Assert.notNull(comment, "The comment must not be null."); Assert.notNull(commentName, "The commentName must not be null.");
String name = comment.getMetadata().getName(); return counterService.getByName(MeterUtils.nameOf(Comment.class, commentName))
return counterService.getByName(MeterUtils.nameOf(Comment.class, name))
.map(counter -> CommentStats.builder() .map(counter -> CommentStats.builder()
.upvote(counter.getUpvote()) .upvote(counter.getUpvote())
.build() .build()

View File

@ -1,5 +1,7 @@
package run.halo.app.infra; 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 org.apache.commons.lang3.ObjectUtils.defaultIfNull;
import static run.halo.app.extension.index.IndexAttributeFactory.multiValueAttribute; import static run.halo.app.extension.index.IndexAttributeFactory.multiValueAttribute;
import static run.halo.app.extension.index.IndexAttributeFactory.simpleAttribute; 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(Reply.class);
schemeManager.register(SinglePage.class); schemeManager.register(SinglePage.class);
// storage.halo.run // storage.halo.run

View File

@ -1,7 +1,6 @@
package run.halo.app.theme.endpoint; package run.halo.app.theme.endpoint;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; 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.isFalse;
import static org.apache.commons.lang3.BooleanUtils.isTrue; import static org.apache.commons.lang3.BooleanUtils.isTrue;
import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder; 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.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List; import java.util.List;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils; 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.content.Reply;
import run.halo.app.core.extension.endpoint.CustomEndpoint; import run.halo.app.core.extension.endpoint.CustomEndpoint;
import run.halo.app.core.extension.endpoint.SortResolver; 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.GroupVersion;
import run.halo.app.extension.ListResult; 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.Ref;
import run.halo.app.extension.router.IListRequest; import run.halo.app.extension.router.IListRequest;
import run.halo.app.extension.router.QueryParamBuildUtil; import run.halo.app.extension.router.QueryParamBuildUtil;
@ -213,9 +211,7 @@ public class CommentFinderEndpoint implements CustomEndpoint {
Mono<ServerResponse> listComments(ServerRequest request) { Mono<ServerResponse> listComments(ServerRequest request) {
CommentQuery commentQuery = new CommentQuery(request); CommentQuery commentQuery = new CommentQuery(request);
var comparator = commentQuery.toComparator(); return commentPublicQueryService.list(commentQuery.toRef(), commentQuery.toPageRequest())
return commentPublicQueryService.list(commentQuery.toRef(), commentQuery.getPage(),
commentQuery.getSize(), comparator)
.flatMap(list -> ServerResponse.ok().bodyValue(list)); .flatMap(list -> ServerResponse.ok().bodyValue(list));
} }
@ -302,26 +298,12 @@ public class CommentFinderEndpoint implements CustomEndpoint {
return ref; return ref;
} }
String emptyToNull(String str) { public PageRequest toPageRequest() {
return StringUtils.isBlank(str) ? null : str; return PageRequestImpl.of(getPage(), getSize(), getSort());
} }
public Comparator<Comment> toComparator() { String emptyToNull(String str) {
var sort = getSort(); return StringUtils.isBlank(str) ? null : str;
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);
} }
} }

View File

@ -3,9 +3,9 @@ package run.halo.app.theme.finders;
import java.util.Comparator; import java.util.Comparator;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import reactor.core.publisher.Mono; 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.core.extension.content.Reply;
import run.halo.app.extension.ListResult; import run.halo.app.extension.ListResult;
import run.halo.app.extension.PageRequest;
import run.halo.app.extension.Ref; import run.halo.app.extension.Ref;
import run.halo.app.theme.finders.vo.CommentVo; import run.halo.app.theme.finders.vo.CommentVo;
import run.halo.app.theme.finders.vo.ReplyVo; import run.halo.app.theme.finders.vo.ReplyVo;
@ -21,8 +21,7 @@ public interface CommentPublicQueryService {
Mono<ListResult<CommentVo>> list(Ref ref, @Nullable Integer page, Mono<ListResult<CommentVo>> list(Ref ref, @Nullable Integer page,
@Nullable Integer size); @Nullable Integer size);
Mono<ListResult<CommentVo>> list(Ref ref, @Nullable Integer page, Mono<ListResult<CommentVo>> list(Ref ref, @Nullable PageRequest pageRequest);
@Nullable Integer size, @Nullable Comparator<Comment> comparator);
Mono<ListResult<ReplyVo>> listReply(String commentName, @Nullable Integer page, Mono<ListResult<ReplyVo>> listReply(String commentName, @Nullable Integer page,
@Nullable Integer size); @Nullable Integer size);

View File

@ -2,34 +2,42 @@ package run.halo.app.theme.finders.impl;
import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; 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.security.Principal;
import java.time.Instant;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Optional;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Predicate; import java.util.function.Predicate;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.data.domain.Sort;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.security.core.context.ReactiveSecurityContextHolder; import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContext;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.comparator.Comparators;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import run.halo.app.content.comment.OwnerInfo; import run.halo.app.content.comment.OwnerInfo;
import run.halo.app.content.comment.ReplyService; 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.Comment;
import run.halo.app.core.extension.content.Reply; import run.halo.app.core.extension.content.Reply;
import run.halo.app.core.extension.service.UserService; import run.halo.app.core.extension.service.UserService;
import run.halo.app.extension.AbstractExtension; import run.halo.app.extension.AbstractExtension;
import run.halo.app.extension.ListOptions;
import run.halo.app.extension.ListResult; 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.ReactiveExtensionClient;
import run.halo.app.extension.Ref; 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.infra.AnonymousUserConst;
import run.halo.app.metrics.CounterService; import run.halo.app.metrics.CounterService;
import run.halo.app.metrics.MeterUtils; import run.halo.app.metrics.MeterUtils;
@ -43,6 +51,7 @@ import run.halo.app.theme.finders.vo.ReplyVo;
* comment public query service implementation. * comment public query service implementation.
* *
* @author LIlGG * @author LIlGG
* @author guqing
*/ */
@Component @Component
@RequiredArgsConstructor @RequiredArgsConstructor
@ -61,30 +70,31 @@ public class CommentPublicQueryServiceImpl implements CommentPublicQueryService
@Override @Override
public Mono<ListResult<CommentVo>> list(Ref ref, Integer page, Integer size) { 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 @Override
public Mono<ListResult<CommentVo>> list(Ref ref, Integer page, Integer size, public Mono<ListResult<CommentVo>> list(Ref ref, PageRequest pageParam) {
Comparator<Comment> comparator) { var pageRequest = Optional.ofNullable(pageParam)
final Comparator<Comment> commentComparator = .map(page -> page.withSort(page.getSort().and(defaultSort())))
Objects.isNull(comparator) ? defaultComparator() .orElse(PageRequestImpl.ofSize(0));
: comparator.thenComparing(defaultComparator()); return fixedCommentFieldQuery(ref)
return fixedCommentPredicate(ref) .flatMap(fixedFieldQuery -> {
.flatMap(fixedPredicate -> var listOptions = new ListOptions();
client.list(Comment.class, fixedPredicate, listOptions.setFieldSelector(fixedFieldQuery);
commentComparator, return client.listBy(Comment.class, listOptions, pageRequest)
pageNullSafe(page), sizeNullSafe(size)) .flatMap(listResult -> Flux.fromStream(listResult.get())
.flatMap(list -> Flux.fromStream(list.get().map(this::toCommentVo)) .map(this::toCommentVo)
.concatMap(Function.identity()) .concatMap(Function.identity())
.collectList() .collectList()
.map(commentVos -> new ListResult<>(list.getPage(), list.getSize(), .map(commentVos -> new ListResult<>(listResult.getPage(),
list.getTotal(), listResult.getSize(),
listResult.getTotal(),
commentVos) commentVos)
) )
) );
.defaultIfEmpty(new ListResult<>(page, size, 0L, List.of())) })
); .defaultIfEmpty(ListResult.emptyResult());
} }
@Override @Override
@ -193,28 +203,31 @@ public class CommentPublicQueryServiceImpl implements CommentPublicQueryService
.map(OwnerInfo::from); .map(OwnerInfo::from);
} }
private Mono<Predicate<Comment>> fixedCommentPredicate(@Nullable Ref ref) { private Mono<FieldSelector> fixedCommentFieldQuery(@Nullable Ref ref) {
Predicate<Comment> basePredicate = return Mono.fromSupplier(
comment -> comment.getMetadata().getDeletionTimestamp() == null; () -> {
if (ref != null) { var baseQuery = QueryFactory.isNull("metadata.deletionTimestamp");
basePredicate = basePredicate if (ref != null) {
.and(comment -> comment.getSpec().getSubjectRef().equals(ref)); baseQuery =
} and(baseQuery,
equal("spec.subjectRef", Comment.toSubjectRefKey(ref)));
// is approved and not hidden }
Predicate<Comment> approvedPredicate = return baseQuery;
comment -> BooleanUtils.isFalse(comment.getSpec().getHidden()) })
&& BooleanUtils.isTrue(comment.getSpec().getApproved()); .flatMap(query -> {
return getCurrentUserWithoutAnonymous() var approvedQuery = and(
.map(username -> { equal("spec.approved", BooleanUtils.TRUE),
Predicate<Comment> isOwner = comment -> { equal("spec.hidden", BooleanUtils.FALSE)
Comment.CommentOwner owner = comment.getSpec().getOwner(); );
return owner != null && StringUtils.equals(username, owner.getName()); // we should list all comments that the user owns
}; return getCurrentUserWithoutAnonymous()
return approvedPredicate.or(isOwner); .map(username -> or(approvedQuery, equal("spec.owner",
Comment.CommentOwner.ownerIdentity(User.KIND, username)))
)
.defaultIfEmpty(approvedQuery)
.map(compositeQuery -> and(query, compositeQuery));
}) })
.defaultIfEmpty(approvedPredicate) .map(FieldSelector::of);
.map(basePredicate::and);
} }
private Mono<Predicate<Reply>> fixedReplyPredicate(String commentName) { private Mono<Predicate<Reply>> fixedReplyPredicate(String commentName) {
@ -247,43 +260,12 @@ public class CommentPublicQueryServiceImpl implements CommentPublicQueryService
.filter(username -> !AnonymousUserConst.PRINCIPAL.equals(username)); .filter(username -> !AnonymousUserConst.PRINCIPAL.equals(username));
} }
static Comparator<Comment> defaultComparator() { static Sort defaultSort() {
return new CommentComparator(); return Sort.by(Sort.Order.desc("spec.top"),
} Sort.Order.asc("spec.priority"),
Sort.Order.desc("spec.creationTime"),
static class CommentComparator implements Comparator<Comment> { Sort.Order.asc("metadata.name")
@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);
}
} }
int pageNullSafe(Integer page) { int pageNullSafe(Integer page) {

View File

@ -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;
}
}
}

View File

@ -1,8 +1,6 @@
package run.halo.app.content.comment; 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.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
@ -12,7 +10,6 @@ import static org.mockito.Mockito.when;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.function.Predicate;
import org.json.JSONException; import org.json.JSONException;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; 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.Comment;
import run.halo.app.core.extension.content.Post; import run.halo.app.core.extension.content.Post;
import run.halo.app.core.extension.service.UserService; 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.ListResult;
import run.halo.app.extension.Metadata; import run.halo.app.extension.Metadata;
import run.halo.app.extension.PageRequest;
import run.halo.app.extension.ReactiveExtensionClient; import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.extension.Ref; import run.halo.app.extension.Ref;
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher; import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
@ -81,13 +80,13 @@ class CommentServiceImplTest {
lenient().when(environmentFetcher.fetchComment()).thenReturn(Mono.just(commentSetting)); lenient().when(environmentFetcher.fetchComment()).thenReturn(Mono.just(commentSetting));
ListResult<Comment> comments = new ListResult<>(1, 10, 3, comments()); 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)); .thenReturn(Mono.just(comments));
when(userService.getUserOrGhost(eq("A-owner"))) when(userService.getUserOrGhost(eq("A-owner")))
.thenReturn(Mono.just(createUser("A-owner"))); .thenReturn(Mono.just(createUser("A-owner")));
when(userService.getUserOrGhost(eq("B-owner"))) when(userService.getUserOrGhost(eq("B-owner")))
.thenReturn(Mono.just(createUser("B-owner"))); .thenReturn(Mono.just(createUser("B-owner")));
when(client.fetch(eq(User.class), eq("C-owner"))) when(client.fetch(eq(User.class), eq("C-owner")))
.thenReturn(Mono.empty()); .thenReturn(Mono.empty());
@ -128,8 +127,7 @@ class CommentServiceImplTest {
ServerHttpRequest httpRequest = mock(ServerHttpRequest.class); ServerHttpRequest httpRequest = mock(ServerHttpRequest.class);
when(exchange.getRequest()).thenReturn(httpRequest); when(exchange.getRequest()).thenReturn(httpRequest);
when(httpRequest.getQueryParams()).thenReturn(queryParams); when(httpRequest.getQueryParams()).thenReturn(queryParams);
Mono<ListResult<ListedComment>> listResultMono = final var listResultMono = commentService.listComment(new CommentQuery(request));
commentService.listComment(new CommentQuery(request));
Counter counterA = new Counter(); Counter counterA = new Counter();
counterA.setUpvote(3); counterA.setUpvote(3);
String commentACounter = MeterUtils.nameOf(Comment.class, "A"); String commentACounter = MeterUtils.nameOf(Comment.class, "A");
@ -170,7 +168,7 @@ class CommentServiceImplTest {
ArgumentCaptor<Comment> captor = ArgumentCaptor.forClass(Comment.class); ArgumentCaptor<Comment> captor = ArgumentCaptor.forClass(Comment.class);
when(client.fetch(eq(User.class), eq("B-owner"))) when(client.fetch(eq(User.class), eq("B-owner")))
.thenReturn(Mono.just(createUser("B-owner"))); .thenReturn(Mono.just(createUser("B-owner")));
Comment commentToCreate = commentRequest.toComment(); Comment commentToCreate = commentRequest.toComment();
commentToCreate.getMetadata().setName("fake"); commentToCreate.getMetadata().setName("fake");
Mono<Comment> commentMono = commentService.create(commentToCreate); Mono<Comment> commentMono = commentService.create(commentToCreate);
@ -214,49 +212,6 @@ class CommentServiceImplTest {
true); 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() { private List<Comment> comments() {
Comment a = comment("A"); Comment a = comment("A");
a.getSpec().getOwner().setKind(Comment.CommentOwner.KIND_EMAIL); a.getSpec().getOwner().setKind(Comment.CommentOwner.KIND_EMAIL);

View File

@ -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.Comment;
import run.halo.app.core.extension.content.Reply; import run.halo.app.core.extension.content.Reply;
import run.halo.app.extension.ListResult; import run.halo.app.extension.ListResult;
import run.halo.app.extension.PageRequest;
import run.halo.app.extension.Ref; import run.halo.app.extension.Ref;
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher; import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
import run.halo.app.theme.finders.CommentFinder; import run.halo.app.theme.finders.CommentFinder;
@ -77,7 +78,7 @@ class CommentFinderEndpointTest {
@Test @Test
void listComments() { 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()))); .thenReturn(Mono.just(new ListResult<>(1, 10, 0, List.of())));
Ref ref = new Ref(); Ref ref = new Ref();
@ -86,21 +87,20 @@ class CommentFinderEndpointTest {
ref.setKind("Post"); ref.setKind("Post");
ref.setName("test"); ref.setName("test");
webTestClient.get() webTestClient.get()
.uri(uriBuilder -> { .uri(uriBuilder -> uriBuilder.path("/comments")
return uriBuilder.path("/comments") .queryParam("group", ref.getGroup())
.queryParam("group", ref.getGroup()) .queryParam("version", ref.getVersion())
.queryParam("version", ref.getVersion()) .queryParam("kind", ref.getKind())
.queryParam("kind", ref.getKind()) .queryParam("name", ref.getName())
.queryParam("name", ref.getName()) .queryParam("page", 1)
.queryParam("page", 1) .queryParam("size", 10)
.queryParam("size", 10) .build())
.build();
})
.exchange() .exchange()
.expectStatus() .expectStatus()
.isOk(); .isOk();
ArgumentCaptor<Ref> refCaptor = ArgumentCaptor.forClass(Ref.class); 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(); Ref value = refCaptor.getValue();
assertThat(value).isEqualTo(ref); assertThat(value).isEqualTo(ref);
} }

View File

@ -9,7 +9,6 @@ import java.time.Instant;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import org.json.JSONException; import org.json.JSONException;
import org.junit.jupiter.api.BeforeEach; 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.GroupVersionKind;
import run.halo.app.extension.ListResult; import run.halo.app.extension.ListResult;
import run.halo.app.extension.Metadata; import run.halo.app.extension.Metadata;
import run.halo.app.extension.MetadataOperator;
import run.halo.app.extension.ReactiveExtensionClient; import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.extension.Ref; import run.halo.app.extension.Ref;
import run.halo.app.infra.AnonymousUserConst; import run.halo.app.infra.AnonymousUserConst;
@ -70,103 +68,6 @@ class CommentPublicQueryServiceImplTest {
@Nested @Nested
class ListCommentTest { 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 @Test
void desensitizeComment() throws JSONException { void desensitizeComment() throws JSONException {
var commentOwner = new Comment.CommentOwner(); var commentOwner = new Comment.CommentOwner();

View File

@ -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);
}
}

View File

@ -89,10 +89,22 @@ const {
keyword, keyword,
], ],
queryFn: async () => { 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({ const { data } = await apiClient.comment.listComments({
fieldSelector,
page: page.value, page: page.value,
size: size.value, size: size.value,
approved: selectedApprovedStatus.value,
sort: [selectedSort.value].filter(Boolean) as string[], sort: [selectedSort.value].filter(Boolean) as string[],
keyword: keyword.value, keyword: keyword.value,
ownerName: selectedUser.value, ownerName: selectedUser.value,
@ -105,10 +117,10 @@ const {
return data.items; return data.items;
}, },
refetchInterval(data) { refetchInterval(data) {
const deletingComments = data?.filter( const hasDeletingData = data?.some(
(comment) => !!comment.comment.metadata.deletionTimestamp (comment) => !!comment.comment.metadata.deletionTimestamp
); );
return deletingComments?.length ? 1000 : false; return hasDeletingData ? 1000 : false;
}, },
}); });
@ -299,33 +311,33 @@ const handleApproveInBatch = async () => {
label: t( label: t(
'core.comment.filters.sort.items.last_reply_time_desc' 'core.comment.filters.sort.items.last_reply_time_desc'
), ),
value: 'lastReplyTime,desc', value: 'status.lastReplyTime,desc',
}, },
{ {
label: t( label: t(
'core.comment.filters.sort.items.last_reply_time_asc' 'core.comment.filters.sort.items.last_reply_time_asc'
), ),
value: 'lastReplyTime,asc', value: 'status.lastReplyTime,asc',
}, },
{ {
label: t( label: t(
'core.comment.filters.sort.items.reply_count_desc' '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'), label: t('core.comment.filters.sort.items.reply_count_asc'),
value: 'replyCount,asc', value: 'status.replyCount,asc',
}, },
{ {
label: t( label: t(
'core.comment.filters.sort.items.create_time_desc' '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'), label: t('core.comment.filters.sort.items.create_time_asc'),
value: 'creationTimestamp,asc', value: 'metadata.creationTimestamp,asc',
}, },
]" ]"
/> />

View File

@ -179,10 +179,7 @@ export const ApiConsoleHaloRunV1alpha1CommentApiAxiosParamCreator = function (
}, },
/** /**
* List comments. * 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 {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 {string} [keyword] Comments filtered by keyword.
* @param {Array<string>} [labelSelector] Label selector for filtering. * @param {Array<string>} [labelSelector] Label selector for filtering.
* @param {string} [ownerKind] Commenter kind. * @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} [page] The page number. Zero indicates no page.
* @param {number} [size] Size of one page. Zero indicates no limit. * @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 {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. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
listComments: async ( listComments: async (
allowNotification?: boolean,
approved?: boolean,
fieldSelector?: Array<string>, fieldSelector?: Array<string>,
hidden?: boolean,
keyword?: string, keyword?: string,
labelSelector?: Array<string>, labelSelector?: Array<string>,
ownerKind?: string, ownerKind?: string,
@ -208,9 +199,6 @@ export const ApiConsoleHaloRunV1alpha1CommentApiAxiosParamCreator = function (
page?: number, page?: number,
size?: number, size?: number,
sort?: Array<string>, sort?: Array<string>,
subjectKind?: string,
subjectName?: string,
top?: boolean,
options: AxiosRequestConfig = {} options: AxiosRequestConfig = {}
): Promise<RequestArgs> => { ): Promise<RequestArgs> => {
const localVarPath = `/apis/api.console.halo.run/v1alpha1/comments`; const localVarPath = `/apis/api.console.halo.run/v1alpha1/comments`;
@ -237,22 +225,10 @@ export const ApiConsoleHaloRunV1alpha1CommentApiAxiosParamCreator = function (
// http bearer authentication required // http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration); await setBearerAuthToObject(localVarHeaderParameter, configuration);
if (allowNotification !== undefined) {
localVarQueryParameter["allowNotification"] = allowNotification;
}
if (approved !== undefined) {
localVarQueryParameter["approved"] = approved;
}
if (fieldSelector) { if (fieldSelector) {
localVarQueryParameter["fieldSelector"] = fieldSelector; localVarQueryParameter["fieldSelector"] = fieldSelector;
} }
if (hidden !== undefined) {
localVarQueryParameter["hidden"] = hidden;
}
if (keyword !== undefined) { if (keyword !== undefined) {
localVarQueryParameter["keyword"] = keyword; localVarQueryParameter["keyword"] = keyword;
} }
@ -281,18 +257,6 @@ export const ApiConsoleHaloRunV1alpha1CommentApiAxiosParamCreator = function (
localVarQueryParameter["sort"] = Array.from(sort); 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); setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = let headersFromBaseOptions =
baseOptions && baseOptions.headers ? baseOptions.headers : {}; baseOptions && baseOptions.headers ? baseOptions.headers : {};
@ -371,10 +335,7 @@ export const ApiConsoleHaloRunV1alpha1CommentApiFp = function (
}, },
/** /**
* List comments. * 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 {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 {string} [keyword] Comments filtered by keyword.
* @param {Array<string>} [labelSelector] Label selector for filtering. * @param {Array<string>} [labelSelector] Label selector for filtering.
* @param {string} [ownerKind] Commenter kind. * @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} [page] The page number. Zero indicates no page.
* @param {number} [size] Size of one page. Zero indicates no limit. * @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 {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. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
async listComments( async listComments(
allowNotification?: boolean,
approved?: boolean,
fieldSelector?: Array<string>, fieldSelector?: Array<string>,
hidden?: boolean,
keyword?: string, keyword?: string,
labelSelector?: Array<string>, labelSelector?: Array<string>,
ownerKind?: string, ownerKind?: string,
@ -400,9 +355,6 @@ export const ApiConsoleHaloRunV1alpha1CommentApiFp = function (
page?: number, page?: number,
size?: number, size?: number,
sort?: Array<string>, sort?: Array<string>,
subjectKind?: string,
subjectName?: string,
top?: boolean,
options?: AxiosRequestConfig options?: AxiosRequestConfig
): Promise< ): Promise<
( (
@ -411,10 +363,7 @@ export const ApiConsoleHaloRunV1alpha1CommentApiFp = function (
) => AxiosPromise<ListedCommentList> ) => AxiosPromise<ListedCommentList>
> { > {
const localVarAxiosArgs = await localVarAxiosParamCreator.listComments( const localVarAxiosArgs = await localVarAxiosParamCreator.listComments(
allowNotification,
approved,
fieldSelector, fieldSelector,
hidden,
keyword, keyword,
labelSelector, labelSelector,
ownerKind, ownerKind,
@ -422,9 +371,6 @@ export const ApiConsoleHaloRunV1alpha1CommentApiFp = function (
page, page,
size, size,
sort, sort,
subjectKind,
subjectName,
top,
options options
); );
return createRequestFunction( return createRequestFunction(
@ -492,10 +438,7 @@ export const ApiConsoleHaloRunV1alpha1CommentApiFactory = function (
): AxiosPromise<ListedCommentList> { ): AxiosPromise<ListedCommentList> {
return localVarFp return localVarFp
.listComments( .listComments(
requestParameters.allowNotification,
requestParameters.approved,
requestParameters.fieldSelector, requestParameters.fieldSelector,
requestParameters.hidden,
requestParameters.keyword, requestParameters.keyword,
requestParameters.labelSelector, requestParameters.labelSelector,
requestParameters.ownerKind, requestParameters.ownerKind,
@ -503,9 +446,6 @@ export const ApiConsoleHaloRunV1alpha1CommentApiFactory = function (
requestParameters.page, requestParameters.page,
requestParameters.size, requestParameters.size,
requestParameters.sort, requestParameters.sort,
requestParameters.subjectKind,
requestParameters.subjectName,
requestParameters.top,
options options
) )
.then((request) => request(axios, basePath)); .then((request) => request(axios, basePath));
@ -554,20 +494,6 @@ export interface ApiConsoleHaloRunV1alpha1CommentApiCreateReplyRequest {
* @interface ApiConsoleHaloRunV1alpha1CommentApiListCommentsRequest * @interface ApiConsoleHaloRunV1alpha1CommentApiListCommentsRequest
*/ */
export 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. * Field selector for filtering.
* @type {Array<string>} * @type {Array<string>}
@ -575,13 +501,6 @@ export interface ApiConsoleHaloRunV1alpha1CommentApiListCommentsRequest {
*/ */
readonly fieldSelector?: Array<string>; readonly fieldSelector?: Array<string>;
/**
* The comment is hidden from the theme side.
* @type {boolean}
* @memberof ApiConsoleHaloRunV1alpha1CommentApiListComments
*/
readonly hidden?: boolean;
/** /**
* Comments filtered by keyword. * Comments filtered by keyword.
* @type {string} * @type {string}
@ -630,27 +549,6 @@ export interface ApiConsoleHaloRunV1alpha1CommentApiListCommentsRequest {
* @memberof ApiConsoleHaloRunV1alpha1CommentApiListComments * @memberof ApiConsoleHaloRunV1alpha1CommentApiListComments
*/ */
readonly sort?: Array<string>; 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) return ApiConsoleHaloRunV1alpha1CommentApiFp(this.configuration)
.listComments( .listComments(
requestParameters.allowNotification,
requestParameters.approved,
requestParameters.fieldSelector, requestParameters.fieldSelector,
requestParameters.hidden,
requestParameters.keyword, requestParameters.keyword,
requestParameters.labelSelector, requestParameters.labelSelector,
requestParameters.ownerKind, requestParameters.ownerKind,
@ -720,9 +615,6 @@ export class ApiConsoleHaloRunV1alpha1CommentApi extends BaseAPI {
requestParameters.page, requestParameters.page,
requestParameters.size, requestParameters.size,
requestParameters.sort, requestParameters.sort,
requestParameters.subjectKind,
requestParameters.subjectName,
requestParameters.top,
options options
) )
.then((request) => request(this.axios, this.basePath)); .then((request) => request(this.axios, this.basePath));