refactor: optimize reply queries using index mechanisms (#5497)

#### What type of PR is this?
/kind improvement
/area core
/milestone 2.14.x

#### What this PR does / why we need it:
使用索引机制优化回复功能的查询以提高查询速度

#### Does this PR introduce a user-facing change?
```release-note
使用索引机制优化回复功能的查询以提高查询速度
```
pull/5504/head
guqing 2024-03-13 16:44:08 +08:00 committed by GitHub
parent 956f4ef3f3
commit e704e09807
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 409 additions and 291 deletions

View File

@ -56,13 +56,16 @@ public class CommentQuery extends IListRequest.QueryListRequest {
@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: "
+ "creationTimestamp,replyCount,lastReplyTime"), + "metadata.creationTimestamp,status.replyCount,status.lastReplyTime"),
schema = @Schema(description = "like field,asc or field,desc", schema = @Schema(description = "like field,asc or field,desc",
implementation = String.class, implementation = String.class,
example = "creationTimestamp,desc")) example = "creationTimestamp,desc"))
public Sort getSort() { public Sort getSort() {
var sort = SortResolver.defaultInstance.resolve(exchange); var sort = SortResolver.defaultInstance.resolve(exchange);
return sort.and(Sort.by("spec.creationTime", "metadata.name").descending()); return sort.and(Sort.by("status.lastReplyTime",
"spec.creationTime",
"metadata.name"
).descending());
} }
public PageRequest toPageRequest() { public PageRequest toPageRequest() {

View File

@ -1,10 +1,18 @@
package run.halo.app.content.comment; package run.halo.app.content.comment;
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.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.util.MultiValueMap; import org.springframework.data.domain.Sort;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.ServerWebInputException;
import run.halo.app.core.extension.content.Reply; import run.halo.app.core.extension.content.Reply;
import run.halo.app.extension.router.IListRequest; import run.halo.app.extension.ListOptions;
import run.halo.app.extension.PageRequest;
import run.halo.app.extension.PageRequestImpl;
import run.halo.app.extension.router.SortableRequest;
/** /**
* Query criteria for {@link Reply} list. * Query criteria for {@link Reply} list.
@ -12,15 +20,35 @@ import run.halo.app.extension.router.IListRequest;
* @author guqing * @author guqing
* @since 2.0.0 * @since 2.0.0
*/ */
public class ReplyQuery extends IListRequest.QueryListRequest { public class ReplyQuery extends SortableRequest {
public ReplyQuery(MultiValueMap<String, String> queryParams) { public ReplyQuery(ServerWebExchange exchange) {
super(queryParams); super(exchange);
} }
@Schema(description = "Replies filtered by commentName.") @Schema(description = "Replies filtered by commentName.")
public String getCommentName() { public String getCommentName() {
String commentName = queryParams.getFirst("commentName"); String commentName = queryParams.getFirst("commentName");
return StringUtils.isBlank(commentName) ? null : commentName; if (StringUtils.isBlank(commentName)) {
throw new ServerWebInputException("The required parameter 'commentName' is missing.");
}
return commentName;
}
/**
* Build list options from query criteria.
*/
public ListOptions toListOptions() {
var listOptions =
labelAndFieldSelectorToListOptions(getLabelSelector(), getFieldSelector());
var newFieldSelector = listOptions.getFieldSelector()
.andQuery(equal("spec.commentName", getCommentName()));
listOptions.setFieldSelector(newFieldSelector);
return listOptions;
}
public PageRequest toPageRequest() {
var sort = getSort().and(Sort.by("spec.creationTime").ascending());
return PageRequestImpl.of(getPage(), getSize(), sort);
} }
} }

View File

@ -79,9 +79,7 @@ public class ReplyServiceImpl implements ReplyService {
@Override @Override
public Mono<ListResult<ListedReply>> list(ReplyQuery query) { public Mono<ListResult<ListedReply>> list(ReplyQuery query) {
return client.list(Reply.class, getReplyPredicate(query), return client.listBy(Reply.class, query.toListOptions(), query.toPageRequest())
ReplyService.creationTimeAscComparator(),
query.getPage(), query.getSize())
.flatMap(list -> Flux.fromStream(list.get() .flatMap(list -> Flux.fromStream(list.get()
.map(this::toListedReply)) .map(this::toListedReply))
.concatMap(Function.identity()) .concatMap(Function.identity())

View File

@ -48,7 +48,7 @@ public class ReplyEndpoint implements CustomEndpoint {
} }
Mono<ServerResponse> listReplies(ServerRequest request) { Mono<ServerResponse> listReplies(ServerRequest request) {
ReplyQuery replyQuery = new ReplyQuery(request.queryParams()); ReplyQuery replyQuery = new ReplyQuery(request.exchange());
return replyService.list(replyQuery) return replyService.list(replyQuery)
.flatMap(listedReplies -> ServerResponse.ok().bodyValue(listedReplies)); .flatMap(listedReplies -> ServerResponse.ok().bodyValue(listedReplies));
} }

View File

@ -1,5 +1,6 @@
package run.halo.app.core.extension.reconciler; package run.halo.app.core.extension.reconciler;
import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
import static run.halo.app.extension.ExtensionUtil.addFinalizers; import static run.halo.app.extension.ExtensionUtil.addFinalizers;
import java.util.Set; import java.util.Set;
@ -45,6 +46,15 @@ public class ReplyReconciler implements Reconciler<Reconciler.Request> {
eventPublisher.publishEvent(new ReplyCreatedEvent(this, reply)); eventPublisher.publishEvent(new ReplyCreatedEvent(this, reply));
} }
if (reply.getSpec().getCreationTime() == null) {
reply.getSpec().setCreationTime(
defaultIfNull(reply.getSpec().getApprovedTime(),
reply.getMetadata().getCreationTimestamp()
)
);
}
client.update(reply);
replyNotificationSubscriptionHelper.subscribeNewReplyReasonForReply(reply); replyNotificationSubscriptionHelper.subscribeNewReplyReasonForReply(reply);
eventPublisher.publishEvent(new ReplyChangedEvent(this, reply)); eventPublisher.publishEvent(new ReplyChangedEvent(this, reply));

View File

@ -223,7 +223,8 @@ public class SchemeInitializer implements ApplicationListener<ApplicationContext
indexSpecs.add(new IndexSpec() indexSpecs.add(new IndexSpec()
.setName("spec.creationTime") .setName("spec.creationTime")
.setIndexFunc(simpleAttribute(Comment.class, .setIndexFunc(simpleAttribute(Comment.class,
comment -> comment.getSpec().getCreationTime().toString()) comment -> defaultIfNull(comment.getSpec().getCreationTime(),
comment.getMetadata().getCreationTimestamp()).toString())
)); ));
indexSpecs.add(new IndexSpec() indexSpecs.add(new IndexSpec()
.setName("spec.approved") .setName("spec.approved")
@ -282,7 +283,35 @@ public class SchemeInitializer implements ApplicationListener<ApplicationContext
return defaultIfNull(replyCount, 0).toString(); return defaultIfNull(replyCount, 0).toString();
}))); })));
}); });
schemeManager.register(Reply.class); schemeManager.register(Reply.class, indexSpecs -> {
indexSpecs.add(new IndexSpec()
.setName("spec.creationTime")
.setIndexFunc(simpleAttribute(Reply.class,
reply -> defaultIfNull(reply.getSpec().getCreationTime(),
reply.getMetadata().getCreationTimestamp()).toString())
));
indexSpecs.add(new IndexSpec()
.setName("spec.commentName")
.setIndexFunc(simpleAttribute(Reply.class,
reply -> reply.getSpec().getCommentName())
));
indexSpecs.add(new IndexSpec()
.setName("spec.hidden")
.setIndexFunc(simpleAttribute(Reply.class,
reply -> toStringTrueFalse(isTrue(reply.getSpec().getHidden())))
));
indexSpecs.add(new IndexSpec()
.setName("spec.approved")
.setIndexFunc(simpleAttribute(Reply.class,
reply -> toStringTrueFalse(isTrue(reply.getSpec().getApproved())))
));
indexSpecs.add(new IndexSpec()
.setName("spec.owner")
.setIndexFunc(simpleAttribute(Reply.class, reply -> {
var owner = reply.getSpec().getOwner();
return Comment.CommentOwner.ownerIdentity(owner.getKind(), owner.getName());
})));
});
schemeManager.register(SinglePage.class); schemeManager.register(SinglePage.class);
// storage.halo.run // storage.halo.run
schemeManager.register(Group.class); schemeManager.register(Group.class);

View File

@ -1,28 +1,35 @@
package run.halo.app.metrics; package run.halo.app.metrics;
import static org.apache.commons.lang3.BooleanUtils.isFalse;
import static org.apache.commons.lang3.BooleanUtils.isTrue;
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.greaterThan;
import static run.halo.app.extension.index.query.QueryFactory.isNull;
import java.time.Duration; import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import java.util.List; import java.util.Optional;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.BooleanUtils;
import org.springframework.context.SmartLifecycle; import org.springframework.context.SmartLifecycle;
import org.springframework.context.event.EventListener; import org.springframework.context.event.EventListener;
import org.springframework.data.domain.Sort;
import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
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.event.post.ReplyEvent; import run.halo.app.event.post.ReplyEvent;
import run.halo.app.extension.ExtensionClient; import run.halo.app.extension.ExtensionClient;
import run.halo.app.extension.ListOptions;
import run.halo.app.extension.PageRequestImpl;
import run.halo.app.extension.controller.Controller; import run.halo.app.extension.controller.Controller;
import run.halo.app.extension.controller.ControllerBuilder; import run.halo.app.extension.controller.ControllerBuilder;
import run.halo.app.extension.controller.DefaultController; import run.halo.app.extension.controller.DefaultController;
import run.halo.app.extension.controller.DefaultQueue; import run.halo.app.extension.controller.DefaultQueue;
import run.halo.app.extension.controller.Reconciler; import run.halo.app.extension.controller.Reconciler;
import run.halo.app.extension.controller.RequestQueue; import run.halo.app.extension.controller.RequestQueue;
import run.halo.app.extension.index.query.Query;
import run.halo.app.extension.router.selector.FieldSelector;
/** /**
* Update the comment status after receiving the reply event. * Update the comment status after receiving the reply event.
@ -54,35 +61,48 @@ public class ReplyEventReconciler implements Reconciler<ReplyEvent>, SmartLifecy
// if the comment has been deleted, then do nothing. // if the comment has been deleted, then do nothing.
.filter(comment -> comment.getMetadata().getDeletionTimestamp() == null) .filter(comment -> comment.getMetadata().getDeletionTimestamp() == null)
.ifPresent(comment -> { .ifPresent(comment -> {
// order by reply creation time desc to get first as last reply time // order by reply creation time desc to get first as last reply time
List<Reply> replies = client.list(Reply.class, var baseQuery = and(
record -> commentName.equals(record.getSpec().getCommentName()) equal("spec.commentName", commentName),
&& record.getMetadata().getDeletionTimestamp() == null, isNull("metadata.deletionTimestamp")
ReplyService.creationTimeAscComparator().reversed()); );
var pageRequest = PageRequestImpl.ofSize(1).withSort(
Sort.by("spec.creationTime", "metadata.name").descending()
);
final Comment.CommentStatus status = comment.getStatusOrDefault();
Comment.CommentStatus status = comment.getStatusOrDefault(); var replyPageResult =
client.listBy(Reply.class, listOptionsWithFieldQuery(baseQuery), pageRequest);
// total reply count // total reply count
status.setReplyCount(replies.size()); status.setReplyCount((int) replyPageResult.getTotal());
long visibleReplyCount = replies.stream() // calculate last reply time from total replies(top 1)
.filter(reply -> isTrue(reply.getSpec().getApproved()) Instant lastReplyTime = replyPageResult.get()
&& isFalse(reply.getSpec().getHidden()) .map(reply -> reply.getSpec().getCreationTime())
)
.count();
status.setVisibleReplyCount((int) visibleReplyCount);
// calculate last reply time
Instant lastReplyTime = replies.stream()
.findFirst() .findFirst()
.map(reply -> defaultIfNull(reply.getSpec().getCreationTime(),
reply.getMetadata().getCreationTimestamp())
)
.orElse(null); .orElse(null);
status.setLastReplyTime(lastReplyTime); status.setLastReplyTime(lastReplyTime);
Instant lastReadTime = comment.getSpec().getLastReadTime(); // calculate visible reply count(only approved and not hidden)
status.setUnreadReplyCount(Comment.getUnreadReplyCount(replies, lastReadTime)); var visibleReplyPageResult =
client.listBy(Reply.class, listOptionsWithFieldQuery(and(
baseQuery,
equal("spec.approved", BooleanUtils.TRUE),
equal("spec.hidden", BooleanUtils.FALSE)
)), pageRequest);
status.setVisibleReplyCount((int) visibleReplyPageResult.getTotal());
// calculate unread reply count(after last read time)
var unReadQuery = Optional.ofNullable(comment.getSpec().getLastReadTime())
.map(lastReadTime -> and(
baseQuery,
greaterThan("spec.creationTime", lastReadTime.toString())
))
.orElse(baseQuery);
var unReadPageResult =
client.listBy(Reply.class, listOptionsWithFieldQuery(unReadQuery), pageRequest);
status.setUnreadReplyCount((int) unReadPageResult.getTotal());
status.setHasNewReply(defaultIfNull(status.getUnreadReplyCount(), 0) > 0); status.setHasNewReply(defaultIfNull(status.getUnreadReplyCount(), 0) > 0);
client.update(comment); client.update(comment);
@ -90,6 +110,12 @@ public class ReplyEventReconciler implements Reconciler<ReplyEvent>, SmartLifecy
return new Result(false, null); return new Result(false, null);
} }
static ListOptions listOptionsWithFieldQuery(Query query) {
var listOptions = new ListOptions();
listOptions.setFieldSelector(FieldSelector.of(query));
return listOptions;
}
@Override @Override
public Controller setupWith(ControllerBuilder builder) { public Controller setupWith(ControllerBuilder builder) {
return new DefaultController<>( return new DefaultController<>(

View File

@ -1,9 +1,7 @@
package run.halo.app.theme.finders; package run.halo.app.theme.finders;
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.Reply;
import run.halo.app.extension.ListResult; import run.halo.app.extension.ListResult;
import run.halo.app.extension.PageRequest; import run.halo.app.extension.PageRequest;
import run.halo.app.extension.Ref; import run.halo.app.extension.Ref;
@ -26,6 +24,5 @@ public interface CommentPublicQueryService {
Mono<ListResult<ReplyVo>> listReply(String commentName, @Nullable Integer page, Mono<ListResult<ReplyVo>> listReply(String commentName, @Nullable Integer page,
@Nullable Integer size); @Nullable Integer size);
Mono<ListResult<ReplyVo>> listReply(String commentName, @Nullable Integer page, Mono<ListResult<ReplyVo>> listReply(String commentName, PageRequest pageRequest);
@Nullable Integer size, @Nullable Comparator<Reply> comparator);
} }

View File

@ -4,17 +4,14 @@ 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.and;
import static run.halo.app.extension.index.query.QueryFactory.equal; import static run.halo.app.extension.index.query.QueryFactory.equal;
import static run.halo.app.extension.index.query.QueryFactory.isNull;
import static run.halo.app.extension.index.query.QueryFactory.or; import static run.halo.app.extension.index.query.QueryFactory.or;
import java.security.Principal; import java.security.Principal;
import java.util.Comparator;
import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.function.Function; import java.util.function.Function;
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.springframework.data.domain.Sort; 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;
@ -24,7 +21,6 @@ import org.springframework.util.Assert;
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.core.extension.User; 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;
@ -36,7 +32,7 @@ import run.halo.app.extension.PageRequest;
import run.halo.app.extension.PageRequestImpl; 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.index.query.Query;
import run.halo.app.extension.router.selector.FieldSelector; 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;
@ -70,18 +66,19 @@ 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, PageRequestImpl.of(pageNullSafe(page), sizeNullSafe(size), defaultSort())); return list(ref,
PageRequestImpl.of(pageNullSafe(page), sizeNullSafe(size), defaultCommentSort()));
} }
@Override @Override
public Mono<ListResult<CommentVo>> list(Ref ref, PageRequest pageParam) { public Mono<ListResult<CommentVo>> list(Ref ref, PageRequest pageParam) {
var pageRequest = Optional.ofNullable(pageParam) var pageRequest = Optional.ofNullable(pageParam)
.map(page -> page.withSort(page.getSort().and(defaultSort()))) .map(page -> page.withSort(page.getSort().and(defaultCommentSort())))
.orElse(PageRequestImpl.ofSize(0)); .orElse(PageRequestImpl.ofSize(0));
return fixedCommentFieldQuery(ref) return fixedCommentFieldSelector(ref)
.flatMap(fixedFieldQuery -> { .flatMap(fieldSelector -> {
var listOptions = new ListOptions(); var listOptions = new ListOptions();
listOptions.setFieldSelector(fixedFieldQuery); listOptions.setFieldSelector(fieldSelector);
return client.listBy(Comment.class, listOptions, pageRequest) return client.listBy(Comment.class, listOptions, pageRequest)
.flatMap(listResult -> Flux.fromStream(listResult.get()) .flatMap(listResult -> Flux.fromStream(listResult.get())
.map(this::toCommentVo) .map(this::toCommentVo)
@ -99,26 +96,29 @@ public class CommentPublicQueryServiceImpl implements CommentPublicQueryService
@Override @Override
public Mono<ListResult<ReplyVo>> listReply(String commentName, Integer page, Integer size) { public Mono<ListResult<ReplyVo>> listReply(String commentName, Integer page, Integer size) {
return listReply(commentName, page, size, ReplyService.creationTimeAscComparator()); return listReply(commentName, PageRequestImpl.of(pageNullSafe(page), sizeNullSafe(size),
defaultReplySort()));
} }
@Override @Override
public Mono<ListResult<ReplyVo>> listReply(String commentName, Integer page, Integer size, public Mono<ListResult<ReplyVo>> listReply(String commentName, PageRequest pageParam) {
Comparator<Reply> comparator) { return fixedReplyFieldSelector(commentName)
return fixedReplyPredicate(commentName) .flatMap(fieldSelector -> {
.flatMap(fixedPredicate -> var listOptions = new ListOptions();
client.list(Reply.class, fixedPredicate, listOptions.setFieldSelector(fieldSelector);
comparator, var pageRequest = Optional.ofNullable(pageParam)
pageNullSafe(page), sizeNullSafe(size)) .map(page -> page.withSort(page.getSort().and(defaultReplySort())))
.orElse(PageRequestImpl.ofSize(0));
return client.listBy(Reply.class, listOptions, pageRequest)
.flatMap(list -> Flux.fromStream(list.get().map(this::toReplyVo)) .flatMap(list -> Flux.fromStream(list.get().map(this::toReplyVo))
.concatMap(Function.identity()) .concatMap(Function.identity())
.collectList() .collectList()
.map(replyVos -> new ListResult<>(list.getPage(), list.getSize(), .map(replyVos -> new ListResult<>(list.getPage(), list.getSize(),
list.getTotal(), list.getTotal(),
replyVos)) replyVos))
) );
.defaultIfEmpty(new ListResult<>(page, size, 0L, List.of())) })
); .defaultIfEmpty(ListResult.emptyResult());
} }
Mono<CommentVo> toCommentVo(Comment comment) { Mono<CommentVo> toCommentVo(Comment comment) {
@ -203,10 +203,10 @@ public class CommentPublicQueryServiceImpl implements CommentPublicQueryService
.map(OwnerInfo::from); .map(OwnerInfo::from);
} }
private Mono<FieldSelector> fixedCommentFieldQuery(@Nullable Ref ref) { private Mono<FieldSelector> fixedCommentFieldSelector(@Nullable Ref ref) {
return Mono.fromSupplier( return Mono.fromSupplier(
() -> { () -> {
var baseQuery = QueryFactory.isNull("metadata.deletionTimestamp"); var baseQuery = isNull("metadata.deletionTimestamp");
if (ref != null) { if (ref != null) {
baseQuery = baseQuery =
and(baseQuery, and(baseQuery,
@ -214,43 +214,35 @@ public class CommentPublicQueryServiceImpl implements CommentPublicQueryService
} }
return baseQuery; return baseQuery;
}) })
.flatMap(query -> { .flatMap(this::concatVisibleQuery)
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); .map(FieldSelector::of);
} }
private Mono<Predicate<Reply>> fixedReplyPredicate(String commentName) { private Mono<Query> concatVisibleQuery(Query query) {
Assert.notNull(query, "The query must not be null");
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));
}
private Mono<FieldSelector> fixedReplyFieldSelector(String commentName) {
Assert.notNull(commentName, "The commentName must not be null"); Assert.notNull(commentName, "The commentName must not be null");
// The comment name must be equal to the comment name of the reply // The comment name must be equal to the comment name of the reply
Predicate<Reply> commentNamePredicate =
reply -> reply.getSpec().getCommentName().equals(commentName)
&& reply.getMetadata().getDeletionTimestamp() == null;
// is approved and not hidden // is approved and not hidden
Predicate<Reply> approvedPredicate = return Mono.fromSupplier(() -> and(
reply -> BooleanUtils.isFalse(reply.getSpec().getHidden()) equal("spec.commentName", commentName),
&& BooleanUtils.isTrue(reply.getSpec().getApproved()); isNull("metadata.deletionTimestamp")
return getCurrentUserWithoutAnonymous() ))
.map(username -> { .flatMap(this::concatVisibleQuery)
Predicate<Reply> isOwner = reply -> { .map(FieldSelector::of);
Comment.CommentOwner owner = reply.getSpec().getOwner();
return owner != null && StringUtils.equals(username, owner.getName());
};
return approvedPredicate.or(isOwner);
})
.defaultIfEmpty(approvedPredicate)
.map(commentNamePredicate::and);
} }
Mono<String> getCurrentUserWithoutAnonymous() { Mono<String> getCurrentUserWithoutAnonymous() {
@ -260,7 +252,7 @@ public class CommentPublicQueryServiceImpl implements CommentPublicQueryService
.filter(username -> !AnonymousUserConst.PRINCIPAL.equals(username)); .filter(username -> !AnonymousUserConst.PRINCIPAL.equals(username));
} }
static Sort defaultSort() { static Sort defaultCommentSort() {
return Sort.by(Sort.Order.desc("spec.top"), return Sort.by(Sort.Order.desc("spec.top"),
Sort.Order.asc("spec.priority"), Sort.Order.asc("spec.priority"),
Sort.Order.desc("spec.creationTime"), Sort.Order.desc("spec.creationTime"),
@ -268,6 +260,12 @@ public class CommentPublicQueryServiceImpl implements CommentPublicQueryService
); );
} }
static Sort defaultReplySort() {
return Sort.by(Sort.Order.asc("spec.creationTime"),
Sort.Order.asc("metadata.name")
);
}
int pageNullSafe(Integer page) { int pageNullSafe(Integer page) {
return defaultIfNull(page, 1); return defaultIfNull(page, 1);
} }

View File

@ -1,6 +1,5 @@
package run.halo.app.theme.finders.impl; package run.halo.app.theme.finders.impl;
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.eq; import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@ -19,15 +18,12 @@ import org.mockito.InjectMocks;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.stubbing.Answer; import org.mockito.stubbing.Answer;
import org.skyscreamer.jsonassert.JSONAssert; import org.skyscreamer.jsonassert.JSONAssert;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.context.junit.jupiter.SpringExtension;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import run.halo.app.core.extension.Counter; import run.halo.app.core.extension.Counter;
import run.halo.app.core.extension.User; 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.content.Reply;
import run.halo.app.core.extension.service.UserService; 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;
@ -214,190 +210,6 @@ class CommentPublicQueryServiceImplTest {
} }
} }
@Nested
class ListReplyTest {
@Test
void listWhenUserNotLogin() {
// Mock
mockWhenListRely();
commentPublicQueryService.listReply("fake-comment", 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("reply-approved");
assertThat(listResult.getItems().get(0).getStats().getUpvote()).isEqualTo(9);
})
.verifyComplete();
}
@Test
@WithMockUser(username = AnonymousUserConst.PRINCIPAL)
void listWhenUserIsAnonymous() {
// Mock
mockWhenListRely();
commentPublicQueryService.listReply("fake-comment", 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("reply-approved");
})
.verifyComplete();
}
@Test
@WithMockUser(username = "fake-user")
void listWhenUserLoggedIn() {
mockWhenListRely();
commentPublicQueryService.listReply("fake-comment", 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("reply-not-approved");
assertThat(listResult.getItems().get(1).getMetadata().getName())
.isEqualTo("reply-approved");
})
.verifyComplete();
}
@Test
void desensitizeReply() throws JSONException {
var reply = createReply();
reply.getSpec().getOwner()
.setAnnotations(new HashMap<>() {
{
put(Comment.CommentOwner.KIND_EMAIL, "mail@halo.run");
}
});
reply.getSpec().setIpAddress("127.0.0.1");
Counter counter = new Counter();
counter.setUpvote(0);
when(counterService.getByName(any())).thenReturn(Mono.just(counter));
var result = commentPublicQueryService.toReplyVo(reply).block();
result.getMetadata().setCreationTimestamp(null);
result.getSpec().setCreationTime(null);
JSONAssert.assertEquals("""
{
"metadata":{
"name":"fake-reply"
},
"spec":{
"raw":"fake-raw",
"content":"fake-content",
"owner":{
"kind":"User",
"name":"",
"displayName":"fake-display-name",
"annotations":{
}
},
"ipAddress":"",
"hidden":false,
"commentName":"fake-comment"
},
"owner":{
"kind":"User",
"displayName":"fake-display-name"
},
"stats":{
"upvote":0
}
}
""",
JsonUtils.objectToJson(result),
true);
}
@SuppressWarnings("unchecked")
private void mockWhenListRely() {
// Mock
Reply notApproved = createReply();
notApproved.getMetadata().setName("reply-not-approved");
notApproved.getSpec().setApproved(false);
Reply approved = createReply();
approved.getMetadata().setName("reply-approved");
approved.getSpec().setApproved(true);
Reply notApprovedWithAnonymous = createReply();
notApprovedWithAnonymous.getMetadata().setName("reply-not-approved-anonymous");
notApprovedWithAnonymous.getSpec().setApproved(false);
notApprovedWithAnonymous.getSpec().getOwner().setName(AnonymousUserConst.PRINCIPAL);
Reply approvedButAnotherOwner = createReply();
approvedButAnotherOwner.getMetadata()
.setName("reply-approved-but-another-owner");
approvedButAnotherOwner.getSpec().setApproved(true);
approvedButAnotherOwner.getSpec().getOwner().setName("another");
Reply notApprovedAndAnotherOwner = createReply();
notApprovedAndAnotherOwner.getMetadata()
.setName("reply-not-approved-and-another");
notApprovedAndAnotherOwner.getSpec().setApproved(false);
notApprovedAndAnotherOwner.getSpec().getOwner().setName("another");
Reply notApprovedAndAnotherCommentName = createReply();
notApprovedAndAnotherCommentName.getMetadata()
.setName("reply-approved-and-another-comment-name");
notApprovedAndAnotherCommentName.getSpec().setApproved(false);
notApprovedAndAnotherCommentName.getSpec().setCommentName("another-fake-comment");
when(client.list(eq(Reply.class), any(),
any(),
eq(1),
eq(10))
).thenAnswer((Answer<Mono<ListResult<Reply>>>) invocation -> {
Predicate<Reply> predicate =
invocation.getArgument(1, Predicate.class);
List<Reply> replies = Stream.of(
notApproved,
approved,
approvedButAnotherOwner,
notApprovedAndAnotherOwner,
notApprovedWithAnonymous,
notApprovedAndAnotherCommentName
).filter(predicate).toList();
return Mono.just(new ListResult<>(1, 10, replies.size(), replies));
});
extractedUser();
when(client.fetch(eq(User.class), any())).thenReturn(Mono.just(createUser()));
Counter counter = new Counter();
counter.setUpvote(9);
when(counterService.getByName(any())).thenReturn(Mono.just(counter));
}
Reply createReply() {
Reply reply = new Reply();
reply.setMetadata(new Metadata());
reply.getMetadata().setName("fake-reply");
reply.setSpec(new Reply.ReplySpec());
reply.getSpec().setRaw("fake-raw");
reply.getSpec().setContent("fake-content");
reply.getSpec().setHidden(false);
reply.getSpec().setCommentName("fake-comment");
Comment.CommentOwner commentOwner = new Comment.CommentOwner();
commentOwner.setKind(User.KIND);
commentOwner.setName("fake-user");
commentOwner.setDisplayName("fake-display-name");
reply.getSpec().setOwner(commentOwner);
return reply;
}
}
private void extractedUser() { private void extractedUser() {
User another = createUser(); User another = createUser();
another.getMetadata().setName("another"); another.getMetadata().setName("another");

View File

@ -2,13 +2,18 @@ package run.halo.app.theme.finders.impl;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.time.Instant; import java.time.Instant;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.json.JSONException;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.skyscreamer.jsonassert.JSONAssert;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.test.context.support.WithMockUser; import org.springframework.security.test.context.support.WithMockUser;
@ -19,6 +24,7 @@ import reactor.test.StepVerifier;
import run.halo.app.core.extension.User; 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.content.Reply;
import run.halo.app.extension.Extension; import run.halo.app.extension.Extension;
import run.halo.app.extension.ExtensionStoreUtil; import run.halo.app.extension.ExtensionStoreUtil;
import run.halo.app.extension.GroupVersionKind; import run.halo.app.extension.GroupVersionKind;
@ -223,7 +229,7 @@ class CommentPublicQueryServiceIntegrationTest {
void sortTest() { void sortTest() {
var comments = var comments =
client.listAll(Comment.class, new ListOptions(), client.listAll(Comment.class, new ListOptions(),
CommentPublicQueryServiceImpl.defaultSort()) CommentPublicQueryServiceImpl.defaultCommentSort())
.collectList() .collectList()
.block(); .block();
assertThat(comments).isNotNull(); assertThat(comments).isNotNull();
@ -279,6 +285,192 @@ class CommentPublicQueryServiceIntegrationTest {
} }
} }
@Nested
class ListReplyTest {
private final List<Reply> storedReplies = mockRelies();
@Autowired
private CommentPublicQueryServiceImpl commentPublicQueryService;
@BeforeEach
void setUp() {
Flux.fromIterable(storedReplies)
.flatMap(reply -> client.create(reply))
.as(StepVerifier::create)
.expectNextCount(storedReplies.size())
.verifyComplete();
}
@AfterEach
void tearDown() {
Flux.fromIterable(storedReplies)
.flatMap(CommentPublicQueryServiceIntegrationTest.this::deleteImmediately)
.as(StepVerifier::create)
.expectNextCount(storedReplies.size())
.verifyComplete();
}
@Test
void listWhenUserNotLogin() {
commentPublicQueryService.listReply("fake-comment", 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("reply-approved");
})
.verifyComplete();
}
@Test
@WithMockUser(username = AnonymousUserConst.PRINCIPAL)
void listWhenUserIsAnonymous() {
commentPublicQueryService.listReply("fake-comment", 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("reply-approved");
})
.verifyComplete();
}
@Test
@WithMockUser(username = "fake-user")
void listWhenUserLoggedIn() {
commentPublicQueryService.listReply("fake-comment", 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("reply-approved");
assertThat(listResult.getItems().get(1).getMetadata().getName())
.isEqualTo("reply-approved-but-another-owner");
assertThat(listResult.getItems().get(2).getMetadata().getName())
.isEqualTo("reply-not-approved");
})
.verifyComplete();
}
@Test
void desensitizeReply() throws JSONException {
var reply = createReply();
reply.getSpec().getOwner()
.setAnnotations(new HashMap<>() {
{
put(Comment.CommentOwner.KIND_EMAIL, "mail@halo.run");
}
});
reply.getSpec().setIpAddress("127.0.0.1");
var result = commentPublicQueryService.toReplyVo(reply).block();
result.getMetadata().setCreationTimestamp(null);
var jsonObject = JsonUtils.jsonToObject(fakeReplyJson(), JsonNode.class);
((ObjectNode) jsonObject.get("owner"))
.put("displayName", "已删除用户");
JSONAssert.assertEquals(jsonObject.toString(),
JsonUtils.objectToJson(result),
true);
}
String fakeReplyJson() {
return """
{
"metadata":{
"name":"fake-reply"
},
"spec":{
"raw":"fake-raw",
"content":"fake-content",
"owner":{
"kind":"User",
"name":"",
"displayName":"fake-display-name",
"annotations":{
}
},
"creationTime": "2024-03-11T06:23:42.923294424Z",
"ipAddress":"",
"hidden": false,
"allowNotification": false,
"top": false,
"priority": 0,
"commentName":"fake-comment"
},
"owner":{
"kind":"User",
"displayName":"fake-display-name"
},
"stats":{
"upvote":0
}
}
""";
}
private List<Reply> mockRelies() {
// Mock
Reply notApproved = createReply();
notApproved.getMetadata().setName("reply-not-approved");
notApproved.getSpec().setApproved(false);
Reply approved = createReply();
approved.getMetadata().setName("reply-approved");
approved.getSpec().setApproved(true);
Reply notApprovedWithAnonymous = createReply();
notApprovedWithAnonymous.getMetadata().setName("reply-not-approved-anonymous");
notApprovedWithAnonymous.getSpec().setApproved(false);
notApprovedWithAnonymous.getSpec().getOwner().setName(AnonymousUserConst.PRINCIPAL);
Reply approvedButAnotherOwner = createReply();
approvedButAnotherOwner.getMetadata()
.setName("reply-approved-but-another-owner");
approvedButAnotherOwner.getSpec().setApproved(true);
approvedButAnotherOwner.getSpec().getOwner().setName("another");
Reply notApprovedAndAnotherOwner = createReply();
notApprovedAndAnotherOwner.getMetadata()
.setName("reply-not-approved-and-another");
notApprovedAndAnotherOwner.getSpec().setApproved(false);
notApprovedAndAnotherOwner.getSpec().getOwner().setName("another");
Reply notApprovedAndAnotherCommentName = createReply();
notApprovedAndAnotherCommentName.getMetadata()
.setName("reply-approved-and-another-comment-name");
notApprovedAndAnotherCommentName.getSpec().setApproved(false);
notApprovedAndAnotherCommentName.getSpec().setCommentName("another-fake-comment");
return List.of(
notApproved,
approved,
approvedButAnotherOwner,
notApprovedAndAnotherOwner,
notApprovedWithAnonymous,
notApprovedAndAnotherCommentName
);
}
Reply createReply() {
var reply = JsonUtils.jsonToObject(fakeReplyJson(), Reply.class);
reply.getMetadata().setName("fake-reply");
reply.getSpec().setRaw("fake-raw");
reply.getSpec().setContent("fake-content");
reply.getSpec().setHidden(false);
reply.getSpec().setCommentName("fake-comment");
Comment.CommentOwner commentOwner = new Comment.CommentOwner();
commentOwner.setKind(User.KIND);
commentOwner.setName("fake-user");
commentOwner.setDisplayName("fake-display-name");
reply.getSpec().setOwner(commentOwner);
return reply;
}
}
Comment createComment() { Comment createComment() {
return JsonUtils.jsonToObject(""" return JsonUtils.jsonToObject("""
{ {

View File

@ -186,7 +186,7 @@ export const ApiConsoleHaloRunV1alpha1CommentApiAxiosParamCreator = function (
* @param {string} [ownerName] Commenter name. * @param {string} [ownerName] Commenter name.
* @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: metadata.creationTimestamp,status.replyCount,status.lastReplyTime
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
@ -342,7 +342,7 @@ export const ApiConsoleHaloRunV1alpha1CommentApiFp = function (
* @param {string} [ownerName] Commenter name. * @param {string} [ownerName] Commenter name.
* @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: metadata.creationTimestamp,status.replyCount,status.lastReplyTime
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
@ -544,7 +544,7 @@ export interface ApiConsoleHaloRunV1alpha1CommentApiListCommentsRequest {
readonly size?: number; readonly size?: number;
/** /**
* Sort property and direction of the list result. Supported fields: creationTimestamp,replyCount,lastReplyTime * Sort property and direction of the list result. Supported fields: metadata.creationTimestamp,status.replyCount,status.lastReplyTime
* @type {Array<string>} * @type {Array<string>}
* @memberof ApiConsoleHaloRunV1alpha1CommentApiListComments * @memberof ApiConsoleHaloRunV1alpha1CommentApiListComments
*/ */

View File

@ -54,6 +54,7 @@ export const ApiConsoleHaloRunV1alpha1ReplyApiAxiosParamCreator = function (
* @param {Array<string>} [labelSelector] Label selector for filtering. * @param {Array<string>} [labelSelector] Label selector for filtering.
* @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. Support sorting based on attribute name path.
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
@ -63,6 +64,7 @@ export const ApiConsoleHaloRunV1alpha1ReplyApiAxiosParamCreator = function (
labelSelector?: Array<string>, labelSelector?: Array<string>,
page?: number, page?: number,
size?: number, size?: number,
sort?: Array<string>,
options: AxiosRequestConfig = {} options: AxiosRequestConfig = {}
): Promise<RequestArgs> => { ): Promise<RequestArgs> => {
const localVarPath = `/apis/api.console.halo.run/v1alpha1/replies`; const localVarPath = `/apis/api.console.halo.run/v1alpha1/replies`;
@ -109,6 +111,10 @@ export const ApiConsoleHaloRunV1alpha1ReplyApiAxiosParamCreator = function (
localVarQueryParameter["size"] = size; localVarQueryParameter["size"] = size;
} }
if (sort) {
localVarQueryParameter["sort"] = Array.from(sort);
}
setSearchParams(localVarUrlObj, localVarQueryParameter); setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = let headersFromBaseOptions =
baseOptions && baseOptions.headers ? baseOptions.headers : {}; baseOptions && baseOptions.headers ? baseOptions.headers : {};
@ -143,6 +149,7 @@ export const ApiConsoleHaloRunV1alpha1ReplyApiFp = function (
* @param {Array<string>} [labelSelector] Label selector for filtering. * @param {Array<string>} [labelSelector] Label selector for filtering.
* @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. Support sorting based on attribute name path.
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
@ -152,6 +159,7 @@ export const ApiConsoleHaloRunV1alpha1ReplyApiFp = function (
labelSelector?: Array<string>, labelSelector?: Array<string>,
page?: number, page?: number,
size?: number, size?: number,
sort?: Array<string>,
options?: AxiosRequestConfig options?: AxiosRequestConfig
): Promise< ): Promise<
( (
@ -165,6 +173,7 @@ export const ApiConsoleHaloRunV1alpha1ReplyApiFp = function (
labelSelector, labelSelector,
page, page,
size, size,
sort,
options options
); );
return createRequestFunction( return createRequestFunction(
@ -205,6 +214,7 @@ export const ApiConsoleHaloRunV1alpha1ReplyApiFactory = function (
requestParameters.labelSelector, requestParameters.labelSelector,
requestParameters.page, requestParameters.page,
requestParameters.size, requestParameters.size,
requestParameters.sort,
options options
) )
.then((request) => request(axios, basePath)); .then((request) => request(axios, basePath));
@ -252,6 +262,13 @@ export interface ApiConsoleHaloRunV1alpha1ReplyApiListRepliesRequest {
* @memberof ApiConsoleHaloRunV1alpha1ReplyApiListReplies * @memberof ApiConsoleHaloRunV1alpha1ReplyApiListReplies
*/ */
readonly size?: number; readonly size?: number;
/**
* Sort property and direction of the list result. Support sorting based on attribute name path.
* @type {Array<string>}
* @memberof ApiConsoleHaloRunV1alpha1ReplyApiListReplies
*/
readonly sort?: Array<string>;
} }
/** /**
@ -279,6 +296,7 @@ export class ApiConsoleHaloRunV1alpha1ReplyApi extends BaseAPI {
requestParameters.labelSelector, requestParameters.labelSelector,
requestParameters.page, requestParameters.page,
requestParameters.size, requestParameters.size,
requestParameters.sort,
options options
) )
.then((request) => request(this.axios, this.basePath)); .then((request) => request(this.axios, this.basePath));

View File

@ -79,6 +79,7 @@ export const PluginStatusLastProbeStateEnum = {
Started: "STARTED", Started: "STARTED",
Stopped: "STOPPED", Stopped: "STOPPED",
Failed: "FAILED", Failed: "FAILED",
Unloaded: "UNLOADED",
} as const; } as const;
export type PluginStatusLastProbeStateEnum = export type PluginStatusLastProbeStateEnum =

View File

@ -18,6 +18,12 @@
* @interface TagStatus * @interface TagStatus
*/ */
export interface TagStatus { export interface TagStatus {
/**
*
* @type {number}
* @memberof TagStatus
*/
observedVersion?: number;
/** /**
* *
* @type {string} * @type {string}