refactor: using indexes to query post lists (#5230)

#### What type of PR is this?
/kind feature
/area core
/area console
/milestone 2.12.x

#### What this PR does / why we need it:
使用索引功能来查询文章列表

how to test it?
1. 测试文章列表的筛选条件是否正确
2. 测试文章列表中关联的标签和分类信息是否正确
3. 测试仪表盘的文章数量统计是否正确
4. 测试分类关联文章的数量是否正确
5. 测试标签关联文章的文章是否正确
6. 测试主题端文章列表是否正确

#### Which issue(s) this PR fixes:
Fixes #5223

#### Does this PR introduce a user-facing change?
```release-note
使用高级索引功能检索文章以显著降低资源消耗并提供更快、更高效的文章检索体验
```
pull/5250/head v2.12.0-alpha.2
guqing 2024-01-25 12:17:12 +08:00 committed by GitHub
parent cecdb3f9ef
commit 3f27f6f262
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
58 changed files with 781 additions and 1577 deletions

View File

@ -84,4 +84,7 @@ public class Snapshot extends AbstractExtension {
return Boolean.parseBoolean(annotations.get(Snapshot.KEEP_RAW_ANNO)); return Boolean.parseBoolean(annotations.get(Snapshot.KEEP_RAW_ANNO));
} }
public static String toSubjectRefKey(Ref subjectRef) {
return subjectRef.getGroup() + "/" + subjectRef.getKind() + "/" + subjectRef.getName();
}
} }

View File

@ -8,6 +8,7 @@ import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.stream.Stream; import java.util.stream.Stream;
import lombok.Data; import lombok.Data;
@ -144,6 +145,15 @@ public class ListResult<T> implements Iterable<T>, Supplier<Stream<T>> {
return listSort; return listSort;
} }
/**
* Gets the first element of the list result.
*/
public static <T> Optional<T> first(ListResult<T> listResult) {
return Optional.ofNullable(listResult)
.map(ListResult::getItems)
.map(list -> list.isEmpty() ? null : list.get(0));
}
@Override @Override
public Stream<T> get() { public Stream<T> get() {
return items.stream(); return items.stream();

View File

@ -1,10 +1,12 @@
package run.halo.app.extension.router.selector; package run.halo.app.extension.router.selector;
import java.util.Objects; import java.util.Objects;
import org.springframework.lang.NonNull;
import org.springframework.util.Assert;
import run.halo.app.extension.index.query.Query; import run.halo.app.extension.index.query.Query;
import run.halo.app.extension.index.query.QueryFactory; import run.halo.app.extension.index.query.QueryFactory;
public record FieldSelector(Query query) { public record FieldSelector(@NonNull Query query) {
public FieldSelector(Query query) { public FieldSelector(Query query) {
this.query = Objects.requireNonNullElseGet(query, QueryFactory::all); this.query = Objects.requireNonNullElseGet(query, QueryFactory::all);
} }
@ -12,4 +14,13 @@ public record FieldSelector(Query query) {
public static FieldSelector of(Query query) { public static FieldSelector of(Query query) {
return new FieldSelector(query); return new FieldSelector(query);
} }
public static FieldSelector all() {
return new FieldSelector(QueryFactory.all());
}
public FieldSelector andQuery(Query other) {
Assert.notNull(other, "Query must not be null");
return of(QueryFactory.and(query(), other));
}
} }

View File

@ -24,6 +24,22 @@ public class LabelSelector implements Predicate<Map<String, String>> {
.allMatch(matcher -> matcher.test(labels.get(matcher.getKey()))); .allMatch(matcher -> matcher.test(labels.get(matcher.getKey())));
} }
/**
* Returns a new label selector that is the result of ANDing the current selector with the
* given selector.
*
* @param other the selector to AND with
* @return a new label selector
*/
public LabelSelector and(LabelSelector other) {
var labelSelector = new LabelSelector();
var matchers = new ArrayList<SelectorMatcher>();
matchers.addAll(this.matchers);
matchers.addAll(other.matchers);
labelSelector.setMatchers(matchers);
return labelSelector;
}
public static LabelSelectorBuilder builder() { public static LabelSelectorBuilder builder() {
return new LabelSelectorBuilder(); return new LabelSelectorBuilder();
} }

View File

@ -96,6 +96,8 @@ public final class SelectorUtil {
listOptions.setLabelSelector(new LabelSelector().setMatchers(labelMatchers)); listOptions.setLabelSelector(new LabelSelector().setMatchers(labelMatchers));
if (!fieldQuery.isEmpty()) { if (!fieldQuery.isEmpty()) {
listOptions.setFieldSelector(FieldSelector.of(QueryFactory.and(fieldQuery))); listOptions.setFieldSelector(FieldSelector.of(QueryFactory.and(fieldQuery)));
} else {
listOptions.setFieldSelector(FieldSelector.all());
} }
return listOptions; return listOptions;
} }

View File

@ -1,129 +0,0 @@
package run.halo.app.content;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.SetMultimap;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.springframework.lang.NonNull;
import org.springframework.util.Assert;
import run.halo.app.extension.Extension;
/**
* <p>A default implementation of {@link Indexer}.</p>
* <p>Note that this Indexer is not thread-safe, If multiple threads access this indexer
* concurrently and one of the threads modifies the indexer, it must be synchronized externally.</p>
*
* @param <T> the type of object to be indexed
* @author guqing
* @see
* <a href="https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/client-go/tools/cache/index.go">kubernetes index</a>
* @see <a href="https://juejin.cn/post/7132767272841510926">informercache.indexer</a>
* @since 2.0.0
*/
public class DefaultIndexer<T extends Extension> implements Indexer<T> {
private final Map<String, SetMultimap<String, String>> indices = new HashMap<>();
private final Map<String, IndexFunc<T>> indexFuncMap = new HashMap<>();
private final Map<String, SetMultimap<String, String>> indexValues = new HashMap<>();
@Override
public void addIndexFunc(String indexName, IndexFunc<T> indexFunc) {
indexFuncMap.put(indexName, indexFunc);
indices.put(indexName, HashMultimap.create());
indexValues.put(indexName, HashMultimap.create());
}
@Override
public Set<String> indexNames() {
return Set.copyOf(indexFuncMap.keySet());
}
@Override
public void add(String indexName, T obj) {
IndexFunc<T> indexFunc = getIndexFunc(indexName);
Set<String> indexKeys = indexFunc.apply(obj);
for (String indexKey : indexKeys) {
SetMultimap<String, String> index = indices.get(indexName);
index.put(indexKey, getObjectKey(obj));
SetMultimap<String, String> indexValue = indexValues.get(indexName);
indexValue.put(getObjectKey(obj), indexKey);
}
}
@NonNull
private IndexFunc<T> getIndexFunc(String indexName) {
IndexFunc<T> indexFunc = indexFuncMap.get(indexName);
if (indexFunc == null) {
throw new IllegalArgumentException(
"Index function not found for index name: " + indexName);
}
return indexFunc;
}
@Override
public void update(String indexName, T obj) {
IndexFunc<T> indexFunc = getIndexFunc(indexName);
Set<String> indexKeys = indexFunc.apply(obj);
Set<String> oldIndexKeys = new HashSet<>();
SetMultimap<String, String> indexValue = indexValues.get(indexName);
if (indexValue.containsKey(getObjectKey(obj))) {
oldIndexKeys.addAll(indexValue.get(getObjectKey(obj)));
}
// delete old index first
for (String oldIndexKey : oldIndexKeys) {
SetMultimap<String, String> index = indices.get(indexName);
index.remove(oldIndexKey, getObjectKey(obj));
indexValue.remove(getObjectKey(obj), oldIndexKey);
}
// add new index
for (String indexKey : indexKeys) {
SetMultimap<String, String> index = indices.get(indexName);
index.put(indexKey, getObjectKey(obj));
indexValue.put(getObjectKey(obj), indexKey);
}
}
@Override
public Set<String> getByIndex(String indexName, String indexKey) {
SetMultimap<String, String> index = indices.get(indexName);
if (index != null) {
return Set.copyOf(index.get(indexKey));
}
return Set.of();
}
@Override
public void delete(String indexName, T obj) {
IndexFunc<T> indexFunc = getIndexFunc(indexName);
SetMultimap<String, String> indexValue = indexValues.get(indexName);
Set<String> indexKeys = indexFunc.apply(obj);
for (String indexKey : indexKeys) {
String objectKey = getObjectKey(obj);
SetMultimap<String, String> index = indices.get(indexName);
index.remove(indexKey, objectKey);
indexValue.remove(indexKey, objectKey);
}
}
/**
* This method is only used for testing.
*
* @param indexName index name
* @return all indices of the given index name
*/
public Map<String, Collection<String>> getIndices(String indexName) {
return indices.get(indexName).asMap();
}
private String getObjectKey(T obj) {
Assert.notNull(obj, "Object must not be null");
Assert.notNull(obj.getMetadata(), "Object metadata must not be null");
Assert.notNull(obj.getMetadata().getName(), "Object name must not be null");
return obj.getMetadata().getName();
}
}

View File

@ -1,79 +0,0 @@
package run.halo.app.content;
import java.util.Set;
import run.halo.app.extension.Extension;
/**
* <p>Indexer is used to index objects by index name and index key.</p>
* <p>For example, if you want to index posts by category, you can use the following code:</p>
* <pre>
* Indexer&lt;Post&gt; indexer = new Indexer&lt;&gt;();
* indexer.addIndexFunc("category", post -&gt; {
* List&lt;String&gt; tags = post.getSpec().getTags();
* return tags == null ? Set.of() : Set.copyOf(tags);
* });
* indexer.add("category", post);
* indexer.getByIndex("category", "category-slug");
* indexer.update("category", post);
* indexer.delete("category", post);
* </pre>
*
* @param <T> the type of object to be indexed
* @author guqing
* @since 2.0.0
*/
public interface Indexer<T extends Extension> {
/**
* Adds an index function for a given index name.
*
* @param indexName The name of the index.
* @param indexFunc The function to use for indexing.
*/
void addIndexFunc(String indexName, DefaultIndexer.IndexFunc<T> indexFunc);
Set<String> indexNames();
/**
* The {@code add} method adds an object of type T to the index
* with the given name. It does this by first getting the index function for the given index
* name and applying it to the object to get a set of index keys. For each index key, it adds
* the object key to the index and the index key to the object's index values.
*
* <p>For example, if you want to index Person objects by name and age, you can use the
* following:</p>
* <pre>
* // Create an Indexer that indexes Person objects by name and age
* Indexer&lt;Person&gt; indexer = new Indexer<>();
* indexer.addIndexFunc("name", person -> Collections.singleton(person.getName()));
* indexer.addIndexFunc("age", person -> Collections.singleton(String.valueOf(person
* .getAge())));
*
* // Create some Person objects
* Person alice = new Person("Alice", 25);
* Person bob = new Person("Bob", 30);
*
* // Add the Person objects to the index
* indexer.add("name", alice);
* indexer.add("name", bob);
* indexer.add("age", alice);
* indexer.add("age", bob);
* </pre>
*
* @param indexName The name of the index.
* @param obj The function to use for indexing.
* @throws IllegalArgumentException if the index name is not found.
*/
void add(String indexName, T obj);
void update(String indexName, T obj);
Set<String> getByIndex(String indexName, String indexKey);
void delete(String indexName, T obj);
@FunctionalInterface
interface IndexFunc<T> {
Set<String> apply(T obj);
}
}

View File

@ -1,221 +0,0 @@
package run.halo.app.content;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.locks.StampedLock;
import java.util.function.BiConsumer;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import run.halo.app.core.extension.content.Post;
import run.halo.app.extension.DefaultExtensionMatcher;
import run.halo.app.extension.Extension;
import run.halo.app.extension.ExtensionClient;
import run.halo.app.extension.GroupVersionKind;
import run.halo.app.extension.Metadata;
import run.halo.app.extension.MetadataUtil;
import run.halo.app.extension.Unstructured;
import run.halo.app.extension.Watcher;
import run.halo.app.extension.controller.RequestSynchronizer;
/**
* <p>Monitor changes to {@link Post} resources and establish a local, in-memory cache in an
* Indexer.
* When changes to posts are detected, the Indexer is updated using the indexFunc to maintain
* its integrity.
* This enables quick retrieval of the unique identifier(It is usually {@link Metadata#getName()})
* for article objects using the getByIndex method when needed.</p>
*
* @author guqing
* @since 2.0.0
*/
@Component
public class PostIndexInformer implements ApplicationListener<ApplicationStartedEvent>,
DisposableBean {
public static final String TAG_POST_INDEXER = "tag-post-indexer";
public static final String LABEL_INDEXER_NAME = "post-label-indexer";
private final RequestSynchronizer synchronizer;
private final Indexer<Post> postIndexer;
private final PostWatcher postWatcher;
public PostIndexInformer(ExtensionClient client) {
postIndexer = new DefaultIndexer<>();
postIndexer.addIndexFunc(TAG_POST_INDEXER, post -> {
List<String> tags = post.getSpec().getTags();
return tags != null ? Set.copyOf(tags) : Set.of();
});
postIndexer.addIndexFunc(LABEL_INDEXER_NAME, labelIndexFunc());
this.postWatcher = new PostWatcher();
var emptyPost = new Post();
this.synchronizer = new RequestSynchronizer(true,
client,
emptyPost,
postWatcher,
DefaultExtensionMatcher.builder(client, emptyPost.groupVersionKind()).build()
);
}
private DefaultIndexer.IndexFunc<Post> labelIndexFunc() {
return post -> {
Map<String, String> labels = MetadataUtil.nullSafeLabels(post);
Set<String> indexKeys = new HashSet<>();
for (Map.Entry<String, String> entry : labels.entrySet()) {
indexKeys.add(labelKey(entry.getKey(), entry.getValue()));
}
return indexKeys;
};
}
public Set<String> getByTagName(String tagName) {
return postIndexer.getByIndex(TAG_POST_INDEXER, tagName);
}
public Set<String> getByLabels(Map<String, String> labels) {
if (labels == null) {
return Set.of();
}
Set<String> result = new HashSet<>();
for (Map.Entry<String, String> entry : labels.entrySet()) {
Set<String> values = postIndexer.getByIndex(LABEL_INDEXER_NAME,
labelKey(entry.getKey(), entry.getValue()));
if (values == null) {
// No objects have this label, no need to continue searching
return Set.of();
}
if (result.isEmpty()) {
result.addAll(values);
} else {
result.retainAll(values);
}
}
return result;
}
String labelKey(String labelName, String labelValue) {
return labelName + "=" + labelValue;
}
@Override
public void destroy() throws Exception {
if (postWatcher != null) {
postWatcher.dispose();
}
if (synchronizer != null) {
synchronizer.dispose();
}
}
@Override
public void onApplicationEvent(@NonNull ApplicationStartedEvent event) {
if (!synchronizer.isStarted()) {
synchronizer.start();
}
}
class PostWatcher implements Watcher {
private Runnable disposeHook;
private boolean disposed = false;
private final StampedLock lock = new StampedLock();
@Override
public void onAdd(Extension extension) {
if (!checkExtension(extension)) {
return;
}
handleIndicates(extension, postIndexer::add);
}
@Override
public void onUpdate(Extension oldExt, Extension newExt) {
if (!checkExtension(newExt)) {
return;
}
handleIndicates(newExt, postIndexer::update);
}
@Override
public void onDelete(Extension extension) {
if (!checkExtension(extension)) {
return;
}
handleIndicates(extension, postIndexer::delete);
}
@Override
public void registerDisposeHook(Runnable dispose) {
this.disposeHook = dispose;
}
@Override
public void dispose() {
if (isDisposed()) {
return;
}
this.disposed = true;
if (this.disposeHook != null) {
this.disposeHook.run();
}
}
@Override
public boolean isDisposed() {
return this.disposed;
}
void handleIndicates(Extension extension, BiConsumer<String, Post> consumer) {
Post post = convertTo(extension);
Set<String> indexNames = getIndexNames();
for (String indexName : indexNames) {
maintainIndicates(indexName, post, consumer);
}
}
Set<String> getIndexNames() {
long stamp = lock.tryOptimisticRead();
Set<String> indexNames = postIndexer.indexNames();
if (!lock.validate(stamp)) {
stamp = lock.readLock();
try {
return postIndexer.indexNames();
} finally {
lock.unlockRead(stamp);
}
}
return indexNames;
}
void maintainIndicates(String indexName, Post post, BiConsumer<String, Post> consumer) {
long stamp = lock.writeLock();
try {
consumer.accept(indexName, post);
} finally {
lock.unlockWrite(stamp);
}
}
}
private Post convertTo(Extension extension) {
if (extension instanceof Post) {
return (Post) extension;
}
return Unstructured.OBJECT_MAPPER.convertValue(extension, Post.class);
}
private boolean checkExtension(Extension extension) {
return !postWatcher.isDisposed()
&& extension.getMetadata().getDeletionTimestamp() == null
&& isPost(extension);
}
private boolean isPost(Extension extension) {
return GroupVersionKind.fromExtension(Post.class).equals(extension.groupVersionKind());
}
}

View File

@ -1,28 +1,23 @@
package run.halo.app.content; package run.halo.app.content;
import static java.util.Comparator.comparing; import static run.halo.app.extension.router.selector.SelectorUtil.labelAndFieldSelectorToListOptions;
import static run.halo.app.extension.router.selector.SelectorUtil.labelAndFieldSelectorToPredicate;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
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 org.apache.commons.lang3.BooleanUtils;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.Set;
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.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.util.CollectionUtils;
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.content.Post; import run.halo.app.core.extension.content.Post;
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.index.query.QueryFactory;
import run.halo.app.extension.router.IListRequest; import run.halo.app.extension.router.IListRequest;
import run.halo.app.extension.router.selector.FieldSelector;
import run.halo.app.extension.router.selector.LabelSelector;
/** /**
* A query object for {@link Post} list. * A query object for {@link Post} list.
@ -52,36 +47,12 @@ public class PostQuery extends IListRequest.QueryListRequest {
return username; return username;
} }
@Nullable
@Schema(name = "contributor")
public Set<String> getContributors() {
return listToSet(queryParams.get("contributor"));
}
@Nullable
@Schema(name = "category")
public Set<String> getCategories() {
return listToSet(queryParams.get("category"));
}
@Nullable
@Schema(name = "tag")
public Set<String> getTags() {
return listToSet(queryParams.get("tag"));
}
@Nullable @Nullable
public Post.PostPhase getPublishPhase() { public Post.PostPhase getPublishPhase() {
String publishPhase = queryParams.getFirst("publishPhase"); String publishPhase = queryParams.getFirst("publishPhase");
return Post.PostPhase.from(publishPhase); return Post.PostPhase.from(publishPhase);
} }
@Nullable
public Post.VisibleEnum getVisible() {
String visible = queryParams.getFirst("visible");
return Post.VisibleEnum.from(visible);
}
@Nullable @Nullable
@Schema(description = "Posts filtered by keyword.") @Schema(description = "Posts filtered by keyword.")
public String getKeyword() { public String getKeyword() {
@ -96,127 +67,58 @@ public class PostQuery 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);
} sort = sort.and(Sort.by(Sort.Direction.DESC, "metadata.creationTimestamp"));
sort = sort.and(Sort.by(Sort.Direction.DESC, "metadata.name"));
@Nullable return sort;
private Set<String> listToSet(List<String> param) {
return param == null ? null : Set.copyOf(param);
} }
/** /**
* Build a comparator from the query object. * Build a list options from the query object.
* *
* @return a comparator * @return a list options
*/ */
public Comparator<Post> toComparator() { public ListOptions toListOptions() {
var sort = getSort(); var listOptions =
var creationTimestampOrder = sort.getOrderFor("creationTimestamp"); labelAndFieldSelectorToListOptions(getLabelSelector(), getFieldSelector());
List<Comparator<Post>> comparators = new ArrayList<>(); if (listOptions.getFieldSelector() == null) {
if (creationTimestampOrder != null) { listOptions.setFieldSelector(FieldSelector.all());
Comparator<Post> comparator =
comparing(post -> post.getMetadata().getCreationTimestamp());
if (creationTimestampOrder.isDescending()) {
comparator = comparator.reversed();
}
comparators.add(comparator);
}
var publishTimeOrder = sort.getOrderFor("publishTime");
if (publishTimeOrder != null) {
Comparator<Object> nullsComparator = publishTimeOrder.isAscending()
? org.springframework.util.comparator.Comparators.nullsLow()
: org.springframework.util.comparator.Comparators.nullsHigh();
Comparator<Post> comparator =
comparing(post -> post.getSpec().getPublishTime(), nullsComparator);
if (publishTimeOrder.isDescending()) {
comparator = comparator.reversed();
}
comparators.add(comparator);
}
comparators.add(Comparators.compareCreationTimestamp(false));
comparators.add(Comparators.compareName(true));
return comparators.stream()
.reduce(Comparator::thenComparing)
.orElse(null);
}
/**
* Build a predicate from the query object.
*
* @return a predicate
*/
public Predicate<Post> toPredicate() {
Predicate<Post> predicate = labelAndFieldSelectorToPredicate(getLabelSelector(),
getFieldSelector());
if (!CollectionUtils.isEmpty(getCategories())) {
predicate =
predicate.and(post -> contains(getCategories(), post.getSpec().getCategories()));
}
if (!CollectionUtils.isEmpty(getTags())) {
predicate = predicate.and(post -> contains(getTags(), post.getSpec().getTags()));
}
if (!CollectionUtils.isEmpty(getContributors())) {
Predicate<Post> hasStatus = post -> post.getStatus() != null;
var containsContributors = hasStatus.and(
post -> contains(getContributors(), post.getStatus().getContributors())
);
predicate = predicate.and(containsContributors);
} }
var labelSelectorBuilder = LabelSelector.builder();
var fieldQuery = QueryFactory.all();
String keyword = getKeyword(); String keyword = getKeyword();
if (keyword != null) { if (keyword != null) {
predicate = predicate.and(post -> { fieldQuery = QueryFactory.and(fieldQuery, QueryFactory.or(
String excerpt = post.getStatusOrDefault().getExcerpt(); QueryFactory.contains("status.excerpt", keyword),
return StringUtils.containsIgnoreCase(excerpt, keyword) QueryFactory.contains("spec.slug", keyword),
|| StringUtils.containsIgnoreCase(post.getSpec().getSlug(), keyword) QueryFactory.contains("spec.title", keyword)
|| StringUtils.containsIgnoreCase(post.getSpec().getTitle(), keyword); ));
});
} }
Post.PostPhase publishPhase = getPublishPhase(); Post.PostPhase publishPhase = getPublishPhase();
if (publishPhase != null) { if (publishPhase != null) {
predicate = predicate.and(post -> { if (Post.PostPhase.PENDING_APPROVAL.equals(publishPhase)) {
if (Post.PostPhase.PENDING_APPROVAL.equals(publishPhase)) { fieldQuery = QueryFactory.and(fieldQuery, QueryFactory.equal(
return !post.isPublished() "status.phase", Post.PostPhase.PENDING_APPROVAL.name())
&& Post.PostPhase.PENDING_APPROVAL.name() );
.equalsIgnoreCase(post.getStatusOrDefault().getPhase()); labelSelectorBuilder.eq(Post.PUBLISHED_LABEL, BooleanUtils.FALSE);
} } else if (Post.PostPhase.PUBLISHED.equals(publishPhase)) {
// published labelSelectorBuilder.eq(Post.PUBLISHED_LABEL, BooleanUtils.TRUE);
if (Post.PostPhase.PUBLISHED.equals(publishPhase)) { } else {
return post.isPublished(); labelSelectorBuilder.eq(Post.PUBLISHED_LABEL, BooleanUtils.FALSE);
} }
// draft
return !post.isPublished();
});
}
Post.VisibleEnum visible = getVisible();
if (visible != null) {
predicate =
predicate.and(post -> visible.equals(post.getSpec().getVisible()));
} }
if (StringUtils.isNotBlank(username)) { if (StringUtils.isNotBlank(username)) {
Predicate<Post> isOwner = post -> Objects.equals(username, post.getSpec().getOwner()); fieldQuery = QueryFactory.and(fieldQuery, QueryFactory.equal(
predicate = predicate.and(isOwner); "spec.owner", username)
);
} }
return predicate;
}
boolean contains(Collection<String> left, List<String> right) { listOptions.setFieldSelector(listOptions.getFieldSelector().andQuery(fieldQuery));
// parameter is null, it means that ignore this condition listOptions.setLabelSelector(
if (left == null) { listOptions.getLabelSelector().and(labelSelectorBuilder.build()));
return true; return listOptions;
}
// else, it means that right is empty
if (left.isEmpty()) {
return right.isEmpty();
}
if (right == null) {
return false;
}
return right.stream().anyMatch(left::contains);
} }
} }

View File

@ -1,5 +1,7 @@
package run.halo.app.content.impl; package run.halo.app.content.impl;
import static run.halo.app.extension.index.query.QueryFactory.in;
import java.time.Duration; import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import java.util.List; import java.util.List;
@ -8,6 +10,7 @@ import java.util.function.Function;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.data.domain.Sort;
import org.springframework.lang.NonNull; import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.util.Assert; import org.springframework.util.Assert;
@ -27,9 +30,12 @@ import run.halo.app.core.extension.content.Category;
import run.halo.app.core.extension.content.Post; import run.halo.app.core.extension.content.Post;
import run.halo.app.core.extension.content.Tag; import run.halo.app.core.extension.content.Tag;
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.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.router.selector.FieldSelector;
import run.halo.app.infra.Condition; import run.halo.app.infra.Condition;
import run.halo.app.infra.ConditionStatus; import run.halo.app.infra.ConditionStatus;
import run.halo.app.metrics.CounterService; import run.halo.app.metrics.CounterService;
@ -58,16 +64,17 @@ public class PostServiceImpl extends AbstractContentService implements PostServi
@Override @Override
public Mono<ListResult<ListedPost>> listPost(PostQuery query) { public Mono<ListResult<ListedPost>> listPost(PostQuery query) {
return client.list(Post.class, query.toPredicate(), return client.listBy(Post.class, query.toListOptions(),
query.toComparator(), query.getPage(), query.getSize()) PageRequestImpl.of(query.getPage(), query.getSize(), query.getSort())
.flatMap(listResult -> Flux.fromStream( )
listResult.get().map(this::getListedPost) .flatMap(listResult -> Flux.fromStream(listResult.get())
) .map(this::getListedPost)
.concatMap(Function.identity()) .concatMap(Function.identity())
.collectList() .collectList()
.map(listedPosts -> new ListResult<>(listResult.getPage(), listResult.getSize(), .map(listedPosts -> new ListResult<>(listResult.getPage(), listResult.getSize(),
listResult.getTotal(), listedPosts) listResult.getTotal(), listedPosts)
) )
.defaultIfEmpty(ListResult.emptyResult())
); );
} }
@ -144,16 +151,18 @@ public class PostServiceImpl extends AbstractContentService implements PostServi
if (tagNames == null) { if (tagNames == null) {
return Flux.empty(); return Flux.empty();
} }
return Flux.fromIterable(tagNames) var listOptions = new ListOptions();
.flatMapSequential(tagName -> client.fetch(Tag.class, tagName)); listOptions.setFieldSelector(FieldSelector.of(in("metadata.name", tagNames)));
return client.listAll(Tag.class, listOptions, Sort.by("metadata.creationTimestamp"));
} }
private Flux<Category> listCategories(List<String> categoryNames) { private Flux<Category> listCategories(List<String> categoryNames) {
if (categoryNames == null) { if (categoryNames == null) {
return Flux.empty(); return Flux.empty();
} }
return Flux.fromIterable(categoryNames) var listOptions = new ListOptions();
.flatMapSequential(categoryName -> client.fetch(Category.class, categoryName)); listOptions.setFieldSelector(FieldSelector.of(in("metadata.name", categoryNames)));
return client.listAll(Category.class, listOptions, Sort.by("metadata.creationTimestamp"));
} }
private Flux<Contributor> listContributors(List<String> usernames) { private Flux<Contributor> listContributors(List<String> usernames) {

View File

@ -19,7 +19,7 @@ public class SnapshotServiceImpl implements SnapshotService {
private final ReactiveExtensionClient client; private final ReactiveExtensionClient client;
private Clock clock; private final Clock clock;
public SnapshotServiceImpl(ReactiveExtensionClient client) { public SnapshotServiceImpl(ReactiveExtensionClient client) {
this.client = client; this.client = client;

View File

@ -2,6 +2,9 @@ package run.halo.app.core.extension.endpoint;
import static java.lang.Boolean.parseBoolean; import static java.lang.Boolean.parseBoolean;
import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder; import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;
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.isNull;
import lombok.Data; import lombok.Data;
import org.springdoc.webflux.core.fn.SpringdocRouteBuilder; import org.springdoc.webflux.core.fn.SpringdocRouteBuilder;
@ -13,8 +16,11 @@ import reactor.core.publisher.Mono;
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.Post; import run.halo.app.core.extension.content.Post;
import run.halo.app.extension.ListOptions;
import run.halo.app.extension.MetadataUtil; import run.halo.app.extension.MetadataUtil;
import run.halo.app.extension.PageRequestImpl;
import run.halo.app.extension.ReactiveExtensionClient; import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.extension.router.selector.FieldSelector;
/** /**
* Stats endpoint. * Stats endpoint.
@ -67,13 +73,16 @@ public class StatsEndpoint implements CustomEndpoint {
stats.setUsers(count.intValue()); stats.setUsers(count.intValue());
return stats; return stats;
})) }))
.flatMap(stats -> client.list(Post.class, post -> !post.isDeleted(), null) .flatMap(stats -> {
.count() var listOptions = new ListOptions();
.map(count -> { listOptions.setFieldSelector(FieldSelector.of(
stats.setPosts(count.intValue()); and(isNull("metadata.deletionTimestamp"),
return stats; equal("spec.deleted", "false")))
}) );
) return client.listBy(Post.class, listOptions, PageRequestImpl.ofSize(1))
.doOnNext(list -> stats.setPosts((int) list.getTotal()))
.thenReturn(stats);
})
.flatMap(stats -> ServerResponse.ok().bodyValue(stats)); .flatMap(stats -> ServerResponse.ok().bodyValue(stats));
} }

View File

@ -1,5 +1,9 @@
package run.halo.app.core.extension.reconciler; package run.halo.app.core.extension.reconciler;
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.isNull;
import java.time.Duration; import java.time.Duration;
import java.util.ArrayDeque; import java.util.ArrayDeque;
import java.util.ArrayList; import java.util.ArrayList;
@ -13,6 +17,7 @@ import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
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.stereotype.Component; import org.springframework.stereotype.Component;
import run.halo.app.content.permalinks.CategoryPermalinkPolicy; import run.halo.app.content.permalinks.CategoryPermalinkPolicy;
@ -20,10 +25,12 @@ import run.halo.app.core.extension.content.Category;
import run.halo.app.core.extension.content.Constant; import run.halo.app.core.extension.content.Constant;
import run.halo.app.core.extension.content.Post; import run.halo.app.core.extension.content.Post;
import run.halo.app.extension.ExtensionClient; import run.halo.app.extension.ExtensionClient;
import run.halo.app.extension.ListOptions;
import run.halo.app.extension.MetadataUtil; import run.halo.app.extension.MetadataUtil;
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.Reconciler; import run.halo.app.extension.controller.Reconciler;
import run.halo.app.extension.router.selector.FieldSelector;
import run.halo.app.infra.utils.JsonUtils; import run.halo.app.infra.utils.JsonUtils;
/** /**
@ -138,7 +145,12 @@ public class CategoryReconciler implements Reconciler<Reconciler.Request> {
.map(item -> item.getMetadata().getName()) .map(item -> item.getMetadata().getName())
.toList(); .toList();
List<Post> posts = client.list(Post.class, post -> !post.isDeleted(), null); var postListOptions = new ListOptions();
postListOptions.setFieldSelector(FieldSelector.of(
and(isNull("metadata.deletionTimestamp"),
equal("spec.deleted", "false")))
);
var posts = client.listAll(Post.class, postListOptions, Sort.unsorted());
// populate post to status // populate post to status
List<Post.CompactPost> compactPosts = posts.stream() List<Post.CompactPost> compactPosts = posts.stream()
@ -178,7 +190,7 @@ public class CategoryReconciler implements Reconciler<Reconciler.Request> {
} }
private List<Category> listChildrenByName(String name) { private List<Category> listChildrenByName(String name) {
List<Category> categories = client.list(Category.class, null, null); var categories = client.listAll(Category.class, new ListOptions(), Sort.unsorted());
Map<String, Category> nameIdentityMap = categories.stream() Map<String, Category> nameIdentityMap = categories.stream()
.collect(Collectors.toMap(category -> category.getMetadata().getName(), .collect(Collectors.toMap(category -> category.getMetadata().getName(),
Function.identity())); Function.identity()));

View File

@ -9,6 +9,7 @@ import com.google.common.hash.Hashing;
import java.time.Instant; import java.time.Instant;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
@ -18,6 +19,7 @@ import org.apache.commons.lang3.StringUtils;
import org.jsoup.Jsoup; import org.jsoup.Jsoup;
import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import run.halo.app.content.ContentWrapper; import run.halo.app.content.ContentWrapper;
import run.halo.app.content.NotificationReasonConst; import run.halo.app.content.NotificationReasonConst;
@ -36,10 +38,13 @@ import run.halo.app.event.post.PostUpdatedEvent;
import run.halo.app.event.post.PostVisibleChangedEvent; import run.halo.app.event.post.PostVisibleChangedEvent;
import run.halo.app.extension.ExtensionClient; import run.halo.app.extension.ExtensionClient;
import run.halo.app.extension.ExtensionOperator; import run.halo.app.extension.ExtensionOperator;
import run.halo.app.extension.ListOptions;
import run.halo.app.extension.Ref; import run.halo.app.extension.Ref;
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.Reconciler; import run.halo.app.extension.controller.Reconciler;
import run.halo.app.extension.index.query.QueryFactory;
import run.halo.app.extension.router.selector.FieldSelector;
import run.halo.app.infra.Condition; import run.halo.app.infra.Condition;
import run.halo.app.infra.ConditionStatus; import run.halo.app.infra.ConditionStatus;
import run.halo.app.infra.utils.HaloUtils; import run.halo.app.infra.utils.HaloUtils;
@ -189,8 +194,7 @@ public class PostReconciler implements Reconciler<Reconciler.Request> {
var ref = Ref.of(post); var ref = Ref.of(post);
// handle contributors // handle contributors
var headSnapshot = post.getSpec().getHeadSnapshot(); var headSnapshot = post.getSpec().getHeadSnapshot();
var contributors = client.list(Snapshot.class, var contributors = listSnapshots(ref)
snapshot -> ref.equals(snapshot.getSpec().getSubjectRef()), null)
.stream() .stream()
.map(snapshot -> { .map(snapshot -> {
Set<String> usernames = snapshot.getSpec().getContributors(); Set<String> usernames = snapshot.getSpec().getContributors();
@ -292,7 +296,7 @@ public class PostReconciler implements Reconciler<Reconciler.Request> {
} }
var labels = post.getMetadata().getLabels(); var labels = post.getMetadata().getLabels();
labels.put(Post.PUBLISHED_LABEL, Boolean.FALSE.toString()); labels.put(Post.PUBLISHED_LABEL, Boolean.FALSE.toString());
var status = post.getStatus(); final var status = post.getStatus();
var condition = new Condition(); var condition = new Condition();
condition.setType("CancelledPublish"); condition.setType("CancelledPublish");
@ -310,9 +314,7 @@ public class PostReconciler implements Reconciler<Reconciler.Request> {
private void cleanUpResources(Post post) { private void cleanUpResources(Post post) {
// clean up snapshots // clean up snapshots
final Ref ref = Ref.of(post); final Ref ref = Ref.of(post);
client.list(Snapshot.class, listSnapshots(ref).forEach(client::delete);
snapshot -> ref.equals(snapshot.getSpec().getSubjectRef()), null)
.forEach(client::delete);
// clean up comments // clean up comments
client.list(Comment.class, comment -> ref.equals(comment.getSpec().getSubjectRef()), client.list(Comment.class, comment -> ref.equals(comment.getSpec().getSubjectRef()),
@ -330,4 +332,11 @@ public class PostReconciler implements Reconciler<Reconciler.Request> {
// TODO The default capture 150 words as excerpt // TODO The default capture 150 words as excerpt
return StringUtils.substring(text, 0, 150); return StringUtils.substring(text, 0, 150);
} }
List<Snapshot> listSnapshots(Ref ref) {
var snapshotListOptions = new ListOptions();
snapshotListOptions.setFieldSelector(FieldSelector.of(
QueryFactory.equal("spec.subjectRef", Snapshot.toSubjectRefKey(ref))));
return client.listAll(Snapshot.class, snapshotListOptions, Sort.unsorted());
}
} }

View File

@ -14,6 +14,7 @@ import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.jsoup.Jsoup; import org.jsoup.Jsoup;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import run.halo.app.content.NotificationReasonConst; import run.halo.app.content.NotificationReasonConst;
@ -26,11 +27,14 @@ import run.halo.app.core.extension.notification.Subscription;
import run.halo.app.extension.ExtensionClient; import run.halo.app.extension.ExtensionClient;
import run.halo.app.extension.ExtensionOperator; import run.halo.app.extension.ExtensionOperator;
import run.halo.app.extension.ExtensionUtil; import run.halo.app.extension.ExtensionUtil;
import run.halo.app.extension.ListOptions;
import run.halo.app.extension.MetadataUtil; import run.halo.app.extension.MetadataUtil;
import run.halo.app.extension.Ref; import run.halo.app.extension.Ref;
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.Reconciler; import run.halo.app.extension.controller.Reconciler;
import run.halo.app.extension.index.query.QueryFactory;
import run.halo.app.extension.router.selector.FieldSelector;
import run.halo.app.infra.Condition; import run.halo.app.infra.Condition;
import run.halo.app.infra.ConditionList; import run.halo.app.infra.ConditionList;
import run.halo.app.infra.ConditionStatus; import run.halo.app.infra.ConditionStatus;
@ -243,9 +247,7 @@ public class SinglePageReconciler implements Reconciler<Reconciler.Request> {
private void cleanUpResources(SinglePage singlePage) { private void cleanUpResources(SinglePage singlePage) {
// clean up snapshot // clean up snapshot
Ref ref = Ref.of(singlePage); Ref ref = Ref.of(singlePage);
client.list(Snapshot.class, listSnapshots(ref).forEach(client::delete);
snapshot -> ref.equals(snapshot.getSpec().getSubjectRef()), null)
.forEach(client::delete);
// clean up comments // clean up comments
client.list(Comment.class, comment -> comment.getSpec().getSubjectRef().equals(ref), client.list(Comment.class, comment -> comment.getSpec().getSubjectRef().equals(ref),
@ -332,8 +334,7 @@ public class SinglePageReconciler implements Reconciler<Reconciler.Request> {
// handle contributors // handle contributors
String headSnapshot = singlePage.getSpec().getHeadSnapshot(); String headSnapshot = singlePage.getSpec().getHeadSnapshot();
List<String> contributors = client.list(Snapshot.class, List<String> contributors = listSnapshots(Ref.of(singlePage))
snapshot -> Ref.of(singlePage).equals(snapshot.getSpec().getSubjectRef()), null)
.stream() .stream()
.peek(snapshot -> { .peek(snapshot -> {
snapshot.getSpec().setContentPatch(StringUtils.EMPTY); snapshot.getSpec().setContentPatch(StringUtils.EMPTY);
@ -377,4 +378,11 @@ public class SinglePageReconciler implements Reconciler<Reconciler.Request> {
return Objects.equals(true, singlePage.getSpec().getDeleted()) return Objects.equals(true, singlePage.getSpec().getDeleted())
|| singlePage.getMetadata().getDeletionTimestamp() != null; || singlePage.getMetadata().getDeletionTimestamp() != null;
} }
List<Snapshot> listSnapshots(Ref ref) {
var snapshotListOptions = new ListOptions();
snapshotListOptions.setFieldSelector(FieldSelector.of(
QueryFactory.equal("spec.subjectRef", Snapshot.toSubjectRefKey(ref))));
return client.listAll(Snapshot.class, snapshotListOptions, Sort.unsorted());
}
} }

View File

@ -1,5 +1,9 @@
package run.halo.app.core.extension.reconciler; package run.halo.app.core.extension.reconciler;
import static org.apache.commons.lang3.BooleanUtils.isFalse;
import static run.halo.app.extension.MetadataUtil.nullSafeLabels;
import static run.halo.app.extension.index.query.QueryFactory.equal;
import java.time.Duration; import java.time.Duration;
import java.util.HashSet; import java.util.HashSet;
import java.util.Map; import java.util.Map;
@ -7,17 +11,19 @@ import java.util.Set;
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.stereotype.Component; import org.springframework.stereotype.Component;
import run.halo.app.content.PostIndexInformer;
import run.halo.app.content.permalinks.TagPermalinkPolicy; import run.halo.app.content.permalinks.TagPermalinkPolicy;
import run.halo.app.core.extension.content.Constant; import run.halo.app.core.extension.content.Constant;
import run.halo.app.core.extension.content.Post; import run.halo.app.core.extension.content.Post;
import run.halo.app.core.extension.content.Tag; import run.halo.app.core.extension.content.Tag;
import run.halo.app.extension.ExtensionClient; import run.halo.app.extension.ExtensionClient;
import run.halo.app.extension.ListOptions;
import run.halo.app.extension.MetadataUtil; import run.halo.app.extension.MetadataUtil;
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.Reconciler; import run.halo.app.extension.controller.Reconciler;
import run.halo.app.extension.router.selector.FieldSelector;
import run.halo.app.infra.utils.JsonUtils; import run.halo.app.infra.utils.JsonUtils;
/** /**
@ -32,7 +38,6 @@ public class TagReconciler implements Reconciler<Reconciler.Request> {
private static final String FINALIZER_NAME = "tag-protection"; private static final String FINALIZER_NAME = "tag-protection";
private final ExtensionClient client; private final ExtensionClient client;
private final TagPermalinkPolicy tagPermalinkPolicy; private final TagPermalinkPolicy tagPermalinkPolicy;
private final PostIndexInformer postIndexInformer;
@Override @Override
public Result reconcile(Request request) { public Result reconcile(Request request) {
@ -128,20 +133,22 @@ public class TagReconciler implements Reconciler<Reconciler.Request> {
} }
private void populatePosts(Tag tag) { private void populatePosts(Tag tag) {
// populate post count // populate post-count
Set<String> postNames = postIndexInformer.getByTagName(tag.getMetadata().getName()); var listOptions = new ListOptions();
tag.getStatusOrDefault().setPostCount(postNames.size()); listOptions.setFieldSelector(FieldSelector.of(
equal("spec.tags", tag.getMetadata().getName()))
);
var posts = client.listAll(Post.class, listOptions, Sort.unsorted());
tag.getStatusOrDefault().setPostCount(posts.size());
// populate visible post count var publicPosts = posts.stream()
Map<String, String> labelToQuery = Map.of(Post.PUBLISHED_LABEL, BooleanUtils.TRUE, .filter(post -> post.getMetadata().getDeletionTimestamp() == null
Post.VISIBLE_LABEL, Post.VisibleEnum.PUBLIC.name(), && isFalse(post.getSpec().getDeleted())
Post.DELETED_LABEL, BooleanUtils.FALSE); && BooleanUtils.TRUE.equals(nullSafeLabels(post).get(Post.PUBLISHED_LABEL))
Set<String> hasAllLabelPosts = postIndexInformer.getByLabels(labelToQuery); && Post.VisibleEnum.PUBLIC.equals(post.getSpec().getVisible())
)
// retain all posts that has all labels .toList();
Set<String> postNamesWithTag = new HashSet<>(postNames); tag.getStatusOrDefault().setVisiblePostCount(publicPosts.size());
postNamesWithTag.retainAll(hasAllLabelPosts);
tag.getStatusOrDefault().setVisiblePostCount(postNamesWithTag.size());
} }
private boolean isDeleted(Tag tag) { private boolean isDeleted(Tag tag) {

View File

@ -1,5 +1,10 @@
package run.halo.app.infra; package run.halo.app.infra;
import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
import static run.halo.app.extension.index.IndexAttributeFactory.multiValueAttribute;
import static run.halo.app.extension.index.IndexAttributeFactory.simpleAttribute;
import java.util.Set;
import org.springframework.boot.context.event.ApplicationContextInitializedEvent; import org.springframework.boot.context.event.ApplicationContextInitializedEvent;
import org.springframework.context.ApplicationListener; import org.springframework.context.ApplicationListener;
import org.springframework.lang.NonNull; import org.springframework.lang.NonNull;
@ -38,6 +43,7 @@ import run.halo.app.extension.ConfigMap;
import run.halo.app.extension.DefaultSchemeManager; import run.halo.app.extension.DefaultSchemeManager;
import run.halo.app.extension.DefaultSchemeWatcherManager; import run.halo.app.extension.DefaultSchemeWatcherManager;
import run.halo.app.extension.Secret; import run.halo.app.extension.Secret;
import run.halo.app.extension.index.IndexSpec;
import run.halo.app.extension.index.IndexSpecRegistryImpl; import run.halo.app.extension.index.IndexSpecRegistryImpl;
import run.halo.app.migration.Backup; import run.halo.app.migration.Backup;
import run.halo.app.plugin.extensionpoint.ExtensionDefinition; import run.halo.app.plugin.extensionpoint.ExtensionDefinition;
@ -70,10 +76,102 @@ public class SchemeInitializer implements ApplicationListener<ApplicationContext
schemeManager.register(Theme.class); schemeManager.register(Theme.class);
schemeManager.register(Menu.class); schemeManager.register(Menu.class);
schemeManager.register(MenuItem.class); schemeManager.register(MenuItem.class);
schemeManager.register(Post.class); schemeManager.register(Post.class, indexSpecs -> {
schemeManager.register(Category.class); indexSpecs.add(new IndexSpec()
schemeManager.register(Tag.class); .setName("spec.title")
schemeManager.register(Snapshot.class); .setIndexFunc(simpleAttribute(Post.class, post -> post.getSpec().getTitle())));
indexSpecs.add(new IndexSpec()
.setName("spec.slug")
// Compatible with old data, hoping to set it to true in the future
.setUnique(false)
.setIndexFunc(simpleAttribute(Post.class, post -> post.getSpec().getSlug())));
indexSpecs.add(new IndexSpec()
.setName("spec.publishTime")
.setIndexFunc(simpleAttribute(Post.class, post -> {
var publishTime = post.getSpec().getPublishTime();
return publishTime == null ? null : publishTime.toString();
})));
indexSpecs.add(new IndexSpec()
.setName("spec.owner")
.setIndexFunc(simpleAttribute(Post.class, post -> post.getSpec().getOwner())));
indexSpecs.add(new IndexSpec()
.setName("spec.deleted")
.setIndexFunc(simpleAttribute(Post.class, post -> {
var deleted = post.getSpec().getDeleted();
return deleted == null ? "false" : deleted.toString();
})));
indexSpecs.add(new IndexSpec()
.setName("spec.pinned")
.setIndexFunc(simpleAttribute(Post.class, post -> {
var pinned = post.getSpec().getPinned();
return pinned == null ? "false" : pinned.toString();
})));
indexSpecs.add(new IndexSpec()
.setName("spec.priority")
.setIndexFunc(simpleAttribute(Post.class, post -> {
var priority = post.getSpec().getPriority();
return priority == null ? "0" : priority.toString();
})));
indexSpecs.add(new IndexSpec()
.setName("spec.visible")
.setIndexFunc(
simpleAttribute(Post.class, post -> post.getSpec().getVisible().name())));
indexSpecs.add(new IndexSpec()
.setName("spec.tags")
.setIndexFunc(multiValueAttribute(Post.class, post -> {
var tags = post.getSpec().getTags();
return tags == null ? Set.of() : Set.copyOf(tags);
})));
indexSpecs.add(new IndexSpec()
.setName("spec.categories")
.setIndexFunc(multiValueAttribute(Post.class, post -> {
var categories = post.getSpec().getCategories();
return categories == null ? Set.of() : Set.copyOf(categories);
})));
indexSpecs.add(new IndexSpec()
.setName("status.contributors")
.setIndexFunc(multiValueAttribute(Post.class, post -> {
var contributors = post.getStatusOrDefault().getContributors();
return contributors == null ? Set.of() : Set.copyOf(contributors);
})));
indexSpecs.add(new IndexSpec()
.setName("status.categories")
.setIndexFunc(
simpleAttribute(Post.class, post -> post.getStatusOrDefault().getExcerpt())));
indexSpecs.add(new IndexSpec()
.setName("status.phase")
.setIndexFunc(
simpleAttribute(Post.class, post -> post.getStatusOrDefault().getPhase())));
indexSpecs.add(new IndexSpec()
.setName("status.excerpt")
.setIndexFunc(
simpleAttribute(Post.class, post -> post.getStatusOrDefault().getExcerpt())));
});
schemeManager.register(Category.class, indexSpecs -> {
indexSpecs.add(new IndexSpec()
.setName("spec.slug")
.setIndexFunc(
simpleAttribute(Category.class, category -> category.getSpec().getSlug()))
);
indexSpecs.add(new IndexSpec()
.setName("spec.priority")
.setIndexFunc(simpleAttribute(Category.class,
category -> defaultIfNull(category.getSpec().getPriority(), 0).toString())));
});
schemeManager.register(Tag.class, indexSpecs -> {
indexSpecs.add(new IndexSpec()
.setName("spec.slug")
.setIndexFunc(simpleAttribute(Tag.class, tag -> tag.getSpec().getSlug()))
);
});
schemeManager.register(Snapshot.class, indexSpecs -> {
indexSpecs.add(new IndexSpec()
.setName("spec.subjectRef")
.setIndexFunc(simpleAttribute(Snapshot.class,
snapshot -> Snapshot.toSubjectRefKey(snapshot.getSpec().getSubjectRef()))
)
);
});
schemeManager.register(Comment.class); schemeManager.register(Comment.class);
schemeManager.register(Reply.class); schemeManager.register(Reply.class);
schemeManager.register(SinglePage.class); schemeManager.register(SinglePage.class);

View File

@ -2,11 +2,9 @@ package run.halo.app.theme.endpoint;
import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder; import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;
import static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder; import static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;
import static run.halo.app.theme.endpoint.PublicApiUtils.containsElement;
import static run.halo.app.theme.endpoint.PublicApiUtils.toAnotherListResult; import static run.halo.app.theme.endpoint.PublicApiUtils.toAnotherListResult;
import io.swagger.v3.oas.annotations.enums.ParameterIn; import io.swagger.v3.oas.annotations.enums.ParameterIn;
import java.util.function.Predicate;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springdoc.webflux.core.fn.SpringdocRouteBuilder; import org.springdoc.webflux.core.fn.SpringdocRouteBuilder;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
@ -17,11 +15,11 @@ import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import run.halo.app.core.extension.content.Category; import run.halo.app.core.extension.content.Category;
import run.halo.app.core.extension.content.Post;
import run.halo.app.core.extension.endpoint.CustomEndpoint; import run.halo.app.core.extension.endpoint.CustomEndpoint;
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.ReactiveExtensionClient; import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.extension.index.query.QueryFactory;
import run.halo.app.extension.router.QueryParamBuildUtil; import run.halo.app.extension.router.QueryParamBuildUtil;
import run.halo.app.extension.router.SortableRequest; import run.halo.app.extension.router.SortableRequest;
import run.halo.app.theme.finders.PostPublicQueryService; import run.halo.app.theme.finders.PostPublicQueryService;
@ -93,13 +91,11 @@ public class CategoryQueryEndpoint implements CustomEndpoint {
private Mono<ServerResponse> listPostsByCategoryName(ServerRequest request) { private Mono<ServerResponse> listPostsByCategoryName(ServerRequest request) {
final var name = request.pathVariable("name"); final var name = request.pathVariable("name");
final var query = new PostPublicQuery(request.exchange()); final var query = new PostPublicQuery(request.exchange());
Predicate<Post> categoryContainsPredicate = var listOptions = query.toListOptions();
post -> containsElement(post.getSpec().getCategories(), name); var newFieldSelector = listOptions.getFieldSelector()
return postPublicQueryService.list(query.getPage(), .andQuery(QueryFactory.equal("spec.categories", name));
query.getSize(), listOptions.setFieldSelector(newFieldSelector);
categoryContainsPredicate.and(query.toPredicate()), return postPublicQueryService.list(listOptions, query.toPageRequest())
query.toComparator()
)
.flatMap(result -> ServerResponse.ok() .flatMap(result -> ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.bodyValue(result) .bodyValue(result)
@ -118,12 +114,7 @@ public class CategoryQueryEndpoint implements CustomEndpoint {
private Mono<ServerResponse> listCategories(ServerRequest request) { private Mono<ServerResponse> listCategories(ServerRequest request) {
CategoryPublicQuery query = new CategoryPublicQuery(request.exchange()); CategoryPublicQuery query = new CategoryPublicQuery(request.exchange());
return client.list(Category.class, return client.listBy(Category.class, query.toListOptions(), query.toPageRequest())
query.toPredicate(),
query.toComparator(),
query.getPage(),
query.getSize()
)
.map(listResult -> toAnotherListResult(listResult, CategoryVo::from)) .map(listResult -> toAnotherListResult(listResult, CategoryVo::from))
.flatMap(result -> ServerResponse.ok() .flatMap(result -> ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)

View File

@ -107,8 +107,7 @@ public class PostQueryEndpoint implements CustomEndpoint {
private Mono<ServerResponse> listPosts(ServerRequest request) { private Mono<ServerResponse> listPosts(ServerRequest request) {
PostPublicQuery query = new PostPublicQuery(request.exchange()); PostPublicQuery query = new PostPublicQuery(request.exchange());
return postPublicQueryService.list(query.getPage(), query.getSize(), query.toPredicate(), return postPublicQueryService.list(query.toListOptions(), query.toPageRequest())
query.toComparator())
.flatMap(result -> ServerResponse.ok().contentType(MediaType.APPLICATION_JSON) .flatMap(result -> ServerResponse.ok().contentType(MediaType.APPLICATION_JSON)
.bodyValue(result) .bodyValue(result)
); );

View File

@ -2,10 +2,8 @@ package run.halo.app.theme.endpoint;
import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder; import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;
import static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder; import static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;
import static run.halo.app.theme.endpoint.PublicApiUtils.containsElement;
import io.swagger.v3.oas.annotations.enums.ParameterIn; import io.swagger.v3.oas.annotations.enums.ParameterIn;
import java.util.function.Predicate;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springdoc.webflux.core.fn.SpringdocRouteBuilder; import org.springdoc.webflux.core.fn.SpringdocRouteBuilder;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
@ -15,11 +13,12 @@ import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse; import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import run.halo.app.core.extension.content.Post;
import run.halo.app.core.extension.content.Tag; import run.halo.app.core.extension.content.Tag;
import run.halo.app.core.extension.endpoint.CustomEndpoint; import run.halo.app.core.extension.endpoint.CustomEndpoint;
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.ReactiveExtensionClient;
import run.halo.app.extension.index.query.QueryFactory;
import run.halo.app.extension.router.QueryParamBuildUtil; import run.halo.app.extension.router.QueryParamBuildUtil;
import run.halo.app.extension.router.SortableRequest; import run.halo.app.extension.router.SortableRequest;
import run.halo.app.theme.finders.PostPublicQueryService; import run.halo.app.theme.finders.PostPublicQueryService;
@ -37,6 +36,7 @@ import run.halo.app.theme.finders.vo.TagVo;
@RequiredArgsConstructor @RequiredArgsConstructor
public class TagQueryEndpoint implements CustomEndpoint { public class TagQueryEndpoint implements CustomEndpoint {
private final ReactiveExtensionClient client;
private final TagFinder tagFinder; private final TagFinder tagFinder;
private final PostPublicQueryService postPublicQueryService; private final PostPublicQueryService postPublicQueryService;
@ -102,13 +102,11 @@ public class TagQueryEndpoint implements CustomEndpoint {
private Mono<ServerResponse> listPostsByTagName(ServerRequest request) { private Mono<ServerResponse> listPostsByTagName(ServerRequest request) {
final var name = request.pathVariable("name"); final var name = request.pathVariable("name");
final var query = new PostPublicQuery(request.exchange()); final var query = new PostPublicQuery(request.exchange());
final Predicate<Post> containsTagPredicate = var listOptions = query.toListOptions();
post -> containsElement(post.getSpec().getTags(), name); var newFieldSelector = listOptions.getFieldSelector()
return postPublicQueryService.list(query.getPage(), .andQuery(QueryFactory.equal("spec.tags", name));
query.getSize(), listOptions.setFieldSelector(newFieldSelector);
containsTagPredicate.and(query.toPredicate()), return postPublicQueryService.list(listOptions, query.toPageRequest())
query.toComparator()
)
.flatMap(result -> ServerResponse.ok() .flatMap(result -> ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.bodyValue(result) .bodyValue(result)
@ -117,11 +115,12 @@ public class TagQueryEndpoint implements CustomEndpoint {
private Mono<ServerResponse> listTags(ServerRequest request) { private Mono<ServerResponse> listTags(ServerRequest request) {
var query = new TagPublicQuery(request.exchange()); var query = new TagPublicQuery(request.exchange());
return tagFinder.list(query.getPage(), return client.listBy(Tag.class, query.toListOptions(), query.toPageRequest())
query.getSize(), .map(result -> {
query.toPredicate(), var tagVos = tagFinder.convertToVo(result.getItems());
query.toComparator() return new ListResult<>(result.getPage(), result.getSize(),
) result.getTotal(), tagVos);
})
.flatMap(result -> ServerResponse.ok() .flatMap(result -> ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.bodyValue(result) .bodyValue(result)

View File

@ -1,11 +1,11 @@
package run.halo.app.theme.finders; package run.halo.app.theme.finders;
import java.util.Comparator;
import java.util.function.Predicate;
import org.springframework.lang.NonNull; import org.springframework.lang.NonNull;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import run.halo.app.core.extension.content.Post; import run.halo.app.core.extension.content.Post;
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.theme.ReactivePostContentHandler; import run.halo.app.theme.ReactivePostContentHandler;
import run.halo.app.theme.finders.vo.ContentVo; import run.halo.app.theme.finders.vo.ContentVo;
import run.halo.app.theme.finders.vo.ListedPostVo; import run.halo.app.theme.finders.vo.ListedPostVo;
@ -14,17 +14,13 @@ import run.halo.app.theme.finders.vo.PostVo;
public interface PostPublicQueryService { public interface PostPublicQueryService {
/** /**
* Lists posts page by predicate and comparator. * Lists public posts by the given list options and page request.
* *
* @param page page number * @param listOptions additional list options
* @param size page size * @param page page request must not be null
* @param postPredicate post predicate * @return a list of listed post vo
* @param comparator post comparator
* @return list result
*/ */
Mono<ListResult<ListedPostVo>> list(Integer page, Integer size, Mono<ListResult<ListedPostVo>> list(ListOptions listOptions, PageRequest page);
Predicate<Post> postPredicate,
Comparator<Post> comparator);
/** /**
* Converts post to listed post vo. * Converts post to listed post vo.

View File

@ -24,8 +24,11 @@ public interface TagFinder {
Mono<ListResult<TagVo>> list(@Nullable Integer page, @Nullable Integer size); Mono<ListResult<TagVo>> list(@Nullable Integer page, @Nullable Integer size);
@Deprecated(since = "2.12.0")
Mono<ListResult<TagVo>> list(@Nullable Integer page, @Nullable Integer size, Mono<ListResult<TagVo>> list(@Nullable Integer page, @Nullable Integer size,
@Nullable Predicate<Tag> predicate, @Nullable Comparator<Tag> comparator); @Nullable Predicate<Tag> predicate, @Nullable Comparator<Tag> comparator);
List<TagVo> convertToVo(List<Tag> tags);
Flux<TagVo> listAll(); Flux<TagVo> listAll();
} }

View File

@ -11,11 +11,16 @@ import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.data.domain.Sort;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import run.halo.app.core.extension.content.Category; import run.halo.app.core.extension.content.Category;
import run.halo.app.extension.ListOptions;
import run.halo.app.extension.ListResult; import run.halo.app.extension.ListResult;
import run.halo.app.extension.PageRequestImpl;
import run.halo.app.extension.ReactiveExtensionClient; import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.extension.index.query.QueryFactory;
import run.halo.app.extension.router.selector.FieldSelector;
import run.halo.app.theme.finders.CategoryFinder; import run.halo.app.theme.finders.CategoryFinder;
import run.halo.app.theme.finders.Finder; import run.halo.app.theme.finders.Finder;
import run.halo.app.theme.finders.vo.CategoryTreeVo; import run.halo.app.theme.finders.vo.CategoryTreeVo;
@ -51,10 +56,17 @@ public class CategoryFinderImpl implements CategoryFinder {
.flatMap(this::getByName); .flatMap(this::getByName);
} }
static Sort defaultSort() {
return Sort.by(Sort.Order.desc("spec.priority"),
Sort.Order.desc("metadata.creationTimestamp"),
Sort.Order.desc("metadata.name"));
}
@Override @Override
public Mono<ListResult<CategoryVo>> list(Integer page, Integer size) { public Mono<ListResult<CategoryVo>> list(Integer page, Integer size) {
return client.list(Category.class, null, return client.listBy(Category.class, new ListOptions(),
defaultComparator(), pageNullSafe(page), sizeNullSafe(size)) PageRequestImpl.of(pageNullSafe(page), sizeNullSafe(size), defaultSort())
)
.map(list -> { .map(list -> {
List<CategoryVo> categoryVos = list.get() List<CategoryVo> categoryVos = list.get()
.map(CategoryVo::from) .map(CategoryVo::from)
@ -65,12 +77,6 @@ public class CategoryFinderImpl implements CategoryFinder {
.defaultIfEmpty(new ListResult<>(page, size, 0L, List.of())); .defaultIfEmpty(new ListResult<>(page, size, 0L, List.of()));
} }
@Override
public Flux<CategoryVo> listAll() {
return client.list(Category.class, null, defaultComparator())
.map(CategoryVo::from);
}
@Override @Override
public Flux<CategoryTreeVo> listAsTree() { public Flux<CategoryTreeVo> listAsTree() {
return this.toCategoryTreeVoFlux(null); return this.toCategoryTreeVoFlux(null);
@ -82,20 +88,9 @@ public class CategoryFinderImpl implements CategoryFinder {
} }
@Override @Override
public Mono<CategoryVo> getParentByName(String name) { public Flux<CategoryVo> listAll() {
if (StringUtils.isBlank(name)) { return client.listAll(Category.class, new ListOptions(), defaultSort())
return Mono.empty(); .map(CategoryVo::from);
}
return client.list(Category.class,
category -> {
List<String> children = category.getSpec().getChildren();
if (children == null) {
return false;
}
return children.contains(name);
},
defaultComparator())
.next().map(CategoryVo::from);
} }
Flux<CategoryTreeVo> toCategoryTreeVoFlux(String name) { Flux<CategoryTreeVo> toCategoryTreeVoFlux(String name) {
@ -169,6 +164,22 @@ public class CategoryFinderImpl implements CategoryFinder {
.reversed(); .reversed();
} }
@Override
public Mono<CategoryVo> getParentByName(String name) {
if (StringUtils.isBlank(name)) {
return Mono.empty();
}
var listOptions = new ListOptions();
listOptions.setFieldSelector(FieldSelector.of(
QueryFactory.equal("spec.children", name)
));
return client.listBy(Category.class, listOptions,
PageRequestImpl.of(1, 1, defaultSort())
)
.map(ListResult::first)
.mapNotNull(item -> item.map(CategoryVo::from).orElse(null));
}
int pageNullSafe(Integer page) { int pageNullSafe(Integer page) {
return ObjectUtils.defaultIfNull(page, 1); return ObjectUtils.defaultIfNull(page, 1);
} }

View File

@ -1,25 +1,26 @@
package run.halo.app.theme.finders.impl; package run.halo.app.theme.finders.impl;
import java.time.Instant;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Comparator; import java.util.Comparator;
import java.util.Deque;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair; import org.springframework.data.domain.Sort;
import org.springframework.util.comparator.Comparators; import org.springframework.lang.NonNull;
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.core.extension.content.Post; import run.halo.app.core.extension.content.Post;
import run.halo.app.extension.ListOptions;
import run.halo.app.extension.ListResult; import run.halo.app.extension.ListResult;
import run.halo.app.extension.PageRequestImpl;
import run.halo.app.extension.ReactiveExtensionClient; import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.extension.exception.ExtensionNotFoundException; import run.halo.app.extension.exception.ExtensionNotFoundException;
import run.halo.app.extension.index.query.QueryFactory;
import run.halo.app.extension.router.selector.FieldSelector;
import run.halo.app.extension.router.selector.LabelSelector;
import run.halo.app.infra.utils.HaloUtils; import run.halo.app.infra.utils.HaloUtils;
import run.halo.app.theme.finders.Finder; import run.halo.app.theme.finders.Finder;
import run.halo.app.theme.finders.PostFinder; import run.halo.app.theme.finders.PostFinder;
@ -64,33 +65,32 @@ public class PostFinderImpl implements PostFinder {
return postPublicQueryService.getContent(postName); return postPublicQueryService.getContent(postName);
} }
@Override static Sort defaultSort() {
public Mono<NavigationPostVo> cursor(String currentName) { return Sort.by(Sort.Order.desc("spec.pinned"),
// TODO Optimize the post names query here Sort.Order.desc("spec.priority"),
return postPredicateResolver.getPredicate() Sort.Order.desc("spec.publishTime"),
.flatMapMany(postPredicate -> Sort.Order.desc("metadata.name")
client.list(Post.class, postPredicate, defaultComparator()) );
) }
.map(post -> post.getMetadata().getName())
.collectList() @NonNull
.flatMap(postNames -> Mono.just(NavigationPostVo.builder()) static LinkNavigation findPostNavigation(List<String> postNames, String target) {
.flatMap(builder -> getByName(currentName) Assert.notNull(target, "Target post name must not be null");
.doOnNext(builder::current) for (int i = 0; i < postNames.size(); i++) {
.thenReturn(builder) var item = postNames.get(i);
) if (target.equals(item)) {
.flatMap(builder -> { var prevLink = (i > 0) ? postNames.get(i - 1) : null;
Pair<String, String> previousNextPair = var nextLink = (i < postNames.size() - 1) ? postNames.get(i + 1) : null;
postPreviousNextPair(postNames, currentName); return new LinkNavigation(prevLink, target, nextLink);
String previousPostName = previousNextPair.getLeft(); }
String nextPostName = previousNextPair.getRight(); }
return fetchByName(previousPostName) return new LinkNavigation(null, target, null);
.doOnNext(builder::previous) }
.then(fetchByName(nextPostName))
.doOnNext(builder::next) static Sort archiveSort() {
.thenReturn(builder); return Sort.by(Sort.Order.desc("spec.publishTime"),
}) Sort.Order.desc("metadata.name")
.map(NavigationPostVo.NavigationPostVoBuilder::build)) );
.defaultIfEmpty(NavigationPostVo.empty());
} }
private Mono<PostVo> fetchByName(String name) { private Mono<PostVo> fetchByName(String name) {
@ -102,106 +102,76 @@ public class PostFinderImpl implements PostFinder {
} }
@Override @Override
public Flux<ListedPostVo> listAll() { public Mono<NavigationPostVo> cursor(String currentName) {
return postPredicateResolver.getPredicate() return postPredicateResolver.getListOptions()
.flatMapMany(predicate -> client.list(Post.class, predicate, defaultComparator())) .flatMapMany(postListOption ->
.concatMap(postPublicQueryService::convertToListedVo); client.listAll(Post.class, postListOption, defaultSort())
} )
.map(post -> post.getMetadata().getName())
static Pair<String, String> postPreviousNextPair(List<String> postNames, .collectList()
String currentName) { .flatMap(postNames -> Mono.just(NavigationPostVo.builder())
FixedSizeSlidingWindow<String> window = new FixedSizeSlidingWindow<>(3); .flatMap(builder -> getByName(currentName)
for (String postName : postNames) { .doOnNext(builder::current)
window.add(postName); .thenReturn(builder)
if (!window.isFull()) { )
continue; .flatMap(builder -> {
} var previousNextPair = findPostNavigation(postNames, currentName);
int index = window.indexOf(currentName); String previousPostName = previousNextPair.prev();
if (index == -1) { String nextPostName = previousNextPair.next();
continue; return fetchByName(previousPostName)
} .doOnNext(builder::previous)
// got expected window .then(fetchByName(nextPostName))
if (index < 2) { .doOnNext(builder::next)
break; .thenReturn(builder);
} })
} .map(NavigationPostVo.NavigationPostVoBuilder::build))
.defaultIfEmpty(NavigationPostVo.empty());
List<String> elements = window.elements();
// current post index
int index = elements.indexOf(currentName);
String previousPostName = null;
if (index > 0) {
previousPostName = elements.get(index - 1);
}
String nextPostName = null;
if (elements.size() - 1 > index) {
nextPostName = elements.get(index + 1);
}
return Pair.of(previousPostName, nextPostName);
}
static class FixedSizeSlidingWindow<T> {
Deque<T> queue;
int size;
public FixedSizeSlidingWindow(int size) {
this.size = size;
// FIFO
queue = new ArrayDeque<>(size);
}
/**
* Add element to the window.
* The element added first will be deleted when the element in the collection exceeds
* {@code size}.
*/
public void add(T t) {
if (queue.size() == size) {
// remove first
queue.poll();
}
// add to last
queue.add(t);
}
public int indexOf(T o) {
List<T> elements = elements();
return elements.indexOf(o);
}
public List<T> elements() {
return new ArrayList<>(queue);
}
public boolean isFull() {
return queue.size() == size;
}
} }
@Override @Override
public Mono<ListResult<ListedPostVo>> list(Integer page, Integer size) { public Mono<ListResult<ListedPostVo>> list(Integer page, Integer size) {
return postPublicQueryService.list(page, size, null, defaultComparator()); return postPublicQueryService.list(new ListOptions(), getPageRequest(page, size));
}
private PageRequestImpl getPageRequest(Integer page, Integer size) {
return PageRequestImpl.of(pageNullSafe(page), sizeNullSafe(size), defaultSort());
} }
@Override @Override
public Mono<ListResult<ListedPostVo>> listByCategory(Integer page, Integer size, public Mono<ListResult<ListedPostVo>> listByCategory(Integer page, Integer size,
String categoryName) { String categoryName) {
return postPublicQueryService.list(page, size, var fieldQuery = QueryFactory.all();
post -> contains(post.getSpec().getCategories(), categoryName), defaultComparator()); if (StringUtils.isNotBlank(categoryName)) {
fieldQuery =
QueryFactory.and(fieldQuery, QueryFactory.equal("spec.categories", categoryName));
}
var listOptions = new ListOptions();
listOptions.setFieldSelector(FieldSelector.of(fieldQuery));
return postPublicQueryService.list(listOptions, getPageRequest(page, size));
} }
@Override @Override
public Mono<ListResult<ListedPostVo>> listByTag(Integer page, Integer size, String tag) { public Mono<ListResult<ListedPostVo>> listByTag(Integer page, Integer size, String tag) {
return postPublicQueryService.list(page, size, var fieldQuery = QueryFactory.all();
post -> contains(post.getSpec().getTags(), tag), defaultComparator()); if (StringUtils.isNotBlank(tag)) {
fieldQuery =
QueryFactory.and(fieldQuery, QueryFactory.equal("spec.tags", tag));
}
var listOptions = new ListOptions();
listOptions.setFieldSelector(FieldSelector.of(fieldQuery));
return postPublicQueryService.list(listOptions, getPageRequest(page, size));
} }
@Override @Override
public Mono<ListResult<ListedPostVo>> listByOwner(Integer page, Integer size, String owner) { public Mono<ListResult<ListedPostVo>> listByOwner(Integer page, Integer size, String owner) {
return postPublicQueryService.list(page, size, var fieldQuery = QueryFactory.all();
post -> post.getSpec().getOwner().equals(owner), defaultComparator()); if (StringUtils.isNotBlank(owner)) {
fieldQuery =
QueryFactory.and(fieldQuery, QueryFactory.equal("spec.owner", owner));
}
var listOptions = new ListOptions();
listOptions.setFieldSelector(FieldSelector.of(fieldQuery));
return postPublicQueryService.list(listOptions, getPageRequest(page, size));
} }
@Override @Override
@ -217,23 +187,23 @@ public class PostFinderImpl implements PostFinder {
@Override @Override
public Mono<ListResult<PostArchiveVo>> archives(Integer page, Integer size, String year, public Mono<ListResult<PostArchiveVo>> archives(Integer page, Integer size, String year,
String month) { String month) {
return postPublicQueryService.list(page, size, post -> { var listOptions = new ListOptions();
Map<String, String> labels = post.getMetadata().getLabels(); var labelSelectorBuilder = LabelSelector.builder();
if (labels == null) { if (StringUtils.isNotBlank(year)) {
return false; labelSelectorBuilder.eq(Post.ARCHIVE_YEAR_LABEL, year);
} }
boolean yearMatch = StringUtils.isBlank(year) if (StringUtils.isNotBlank(month)) {
|| year.equals(labels.get(Post.ARCHIVE_YEAR_LABEL)); labelSelectorBuilder.eq(Post.ARCHIVE_MONTH_LABEL, month);
boolean monthMatch = StringUtils.isBlank(month) }
|| month.equals(labels.get(Post.ARCHIVE_MONTH_LABEL)); listOptions.setLabelSelector(labelSelectorBuilder.build());
return yearMatch && monthMatch; var pageRequest = PageRequestImpl.of(pageNullSafe(page), sizeNullSafe(size), archiveSort());
}, archiveComparator()) return postPublicQueryService.list(listOptions, pageRequest)
.map(list -> { .map(list -> {
Map<String, List<ListedPostVo>> yearPosts = list.get() Map<String, List<ListedPostVo>> yearPosts = list.get()
.collect(Collectors.groupingBy( .collect(Collectors.groupingBy(
post -> HaloUtils.getYearText(post.getSpec().getPublishTime()))); post -> HaloUtils.getYearText(post.getSpec().getPublishTime())));
List<PostArchiveVo> postArchives = List<PostArchiveVo> postArchives = yearPosts.entrySet().stream()
yearPosts.entrySet().stream().map(entry -> { .map(entry -> {
String key = entry.getKey(); String key = entry.getKey();
// archives by month // archives by month
Map<String, List<ListedPostVo>> monthPosts = entry.getValue().stream() Map<String, List<ListedPostVo>> monthPosts = entry.getValue().stream()
@ -260,37 +230,24 @@ public class PostFinderImpl implements PostFinder {
return new ListResult<>(list.getPage(), list.getSize(), list.getTotal(), return new ListResult<>(list.getPage(), list.getSize(), list.getTotal(),
postArchives); postArchives);
}) })
.defaultIfEmpty(new ListResult<>(page, size, 0, List.of())); .defaultIfEmpty(ListResult.emptyResult());
} }
private boolean contains(List<String> c, String key) { @Override
if (StringUtils.isBlank(key) || c == null) { public Flux<ListedPostVo> listAll() {
return false; return postPredicateResolver.getListOptions()
} .flatMapMany(listOptions -> client.listAll(Post.class, listOptions, defaultSort()))
return c.contains(key); .concatMap(postPublicQueryService::convertToListedVo);
} }
static Comparator<Post> defaultComparator() { int pageNullSafe(Integer page) {
Function<Post, Boolean> pinned = return ObjectUtils.defaultIfNull(page, 1);
post -> Objects.requireNonNullElse(post.getSpec().getPinned(), false);
Function<Post, Integer> priority =
post -> Objects.requireNonNullElse(post.getSpec().getPriority(), 0);
Function<Post, Instant> publishTime =
post -> post.getSpec().getPublishTime();
Function<Post, String> name = post -> post.getMetadata().getName();
return Comparator.comparing(pinned)
.thenComparing(priority)
.thenComparing(publishTime, Comparators.nullsLow())
.thenComparing(name)
.reversed();
} }
static Comparator<Post> archiveComparator() { int sizeNullSafe(Integer size) {
Function<Post, Instant> publishTime = return ObjectUtils.defaultIfNull(size, 10);
post -> post.getSpec().getPublishTime(); }
Function<Post, String> name = post -> post.getMetadata().getName();
return Comparator.comparing(publishTime, Comparators.nullsLow()) record LinkNavigation(String prev, String current, String next) {
.thenComparing(name)
.reversed();
} }
} }

View File

@ -1,11 +1,8 @@
package run.halo.app.theme.finders.impl; package run.halo.app.theme.finders.impl;
import java.util.Comparator;
import java.util.List; import java.util.List;
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.ObjectUtils;
import org.springframework.lang.NonNull; import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.util.Assert; import org.springframework.util.Assert;
@ -15,7 +12,9 @@ import reactor.core.publisher.Mono;
import run.halo.app.content.ContentWrapper; import run.halo.app.content.ContentWrapper;
import run.halo.app.content.PostService; import run.halo.app.content.PostService;
import run.halo.app.core.extension.content.Post; import run.halo.app.core.extension.content.Post;
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.ReactiveExtensionClient; import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.metrics.CounterService; import run.halo.app.metrics.CounterService;
import run.halo.app.metrics.MeterUtils; import run.halo.app.metrics.MeterUtils;
@ -52,13 +51,21 @@ public class PostPublicQueryServiceImpl implements PostPublicQueryService {
private final ReactiveQueryPostPredicateResolver postPredicateResolver; private final ReactiveQueryPostPredicateResolver postPredicateResolver;
@Override @Override
public Mono<ListResult<ListedPostVo>> list(Integer page, Integer size, public Mono<ListResult<ListedPostVo>> list(ListOptions queryOptions, PageRequest page) {
Predicate<Post> postPredicate, Comparator<Post> comparator) { return postPredicateResolver.getListOptions()
return postPredicateResolver.getPredicate() .map(option -> {
.map(predicate -> predicate.and(postPredicate == null ? post -> true : postPredicate)) var fieldSelector = queryOptions.getFieldSelector();
.flatMap(predicate -> client.list(Post.class, predicate, if (fieldSelector != null) {
comparator, pageNullSafe(page), sizeNullSafe(size)) option.setFieldSelector(option.getFieldSelector()
) .andQuery(fieldSelector.query()));
}
var labelSelector = queryOptions.getLabelSelector();
if (labelSelector != null) {
option.setLabelSelector(option.getLabelSelector().and(labelSelector));
}
return option;
})
.flatMap(listOptions -> client.listBy(Post.class, listOptions, page))
.flatMap(list -> Flux.fromStream(list.get()) .flatMap(list -> Flux.fromStream(list.get())
.concatMap(post -> convertToListedVo(post) .concatMap(post -> convertToListedVo(post)
.flatMap(postVo -> populateStats(postVo) .flatMap(postVo -> populateStats(postVo)
@ -70,9 +77,10 @@ public class PostPublicQueryServiceImpl implements PostPublicQueryService {
postVos) postVos)
) )
) )
.defaultIfEmpty(new ListResult<>(page, size, 0L, List.of())); .defaultIfEmpty(ListResult.emptyResult());
} }
@Override @Override
public Mono<ListedPostVo> convertToListedVo(@NonNull Post post) { public Mono<ListedPostVo> convertToListedVo(@NonNull Post post) {
Assert.notNull(post, "Post must not be null"); Assert.notNull(post, "Post must not be null");
@ -180,12 +188,4 @@ public class PostPublicQueryServiceImpl implements PostPublicQueryService {
) )
.defaultIfEmpty(StatsVo.empty()); .defaultIfEmpty(StatsVo.empty());
} }
int pageNullSafe(Integer page) {
return ObjectUtils.defaultIfNull(page, 1);
}
int sizeNullSafe(Integer size) {
return ObjectUtils.defaultIfNull(size, 10);
}
} }

View File

@ -1,11 +1,20 @@
package run.halo.app.theme.finders.impl; package run.halo.app.theme.finders.impl;
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.isNull;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import run.halo.app.core.extension.Counter; import run.halo.app.core.extension.Counter;
import run.halo.app.core.extension.content.Category; import run.halo.app.core.extension.content.Category;
import run.halo.app.core.extension.content.Post; import run.halo.app.core.extension.content.Post;
import run.halo.app.extension.ListOptions;
import run.halo.app.extension.ListResult;
import run.halo.app.extension.PageRequestImpl;
import run.halo.app.extension.ReactiveExtensionClient; import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.extension.router.selector.FieldSelector;
import run.halo.app.extension.router.selector.LabelSelector;
import run.halo.app.theme.finders.Finder; import run.halo.app.theme.finders.Finder;
import run.halo.app.theme.finders.SiteStatsFinder; import run.halo.app.theme.finders.SiteStatsFinder;
import run.halo.app.theme.finders.vo.SiteStatsVo; import run.halo.app.theme.finders.vo.SiteStatsVo;
@ -40,14 +49,22 @@ public class SiteStatsFinderImpl implements SiteStatsFinder {
} }
Mono<Integer> postCount() { Mono<Integer> postCount() {
return client.list(Post.class, post -> !post.isDeleted() && post.isPublished(), null) var listOptions = new ListOptions();
.count() listOptions.setLabelSelector(LabelSelector.builder()
.map(Long::intValue); .eq(Post.PUBLISHED_LABEL, "true")
.build());
var fieldQuery = and(
isNull("metadata.deletionTimestamp"),
equal("spec.deleted", "false")
);
listOptions.setFieldSelector(FieldSelector.of(fieldQuery));
return client.listBy(Post.class, listOptions, PageRequestImpl.ofSize(1))
.map(result -> (int) result.getTotal());
} }
Mono<Integer> categoryCount() { Mono<Integer> categoryCount() {
return client.list(Category.class, null, null) return client.listBy(Category.class, new ListOptions(), PageRequestImpl.ofSize(1))
.count() .map(ListResult::getTotal)
.map(Long::intValue); .map(Long::intValue);
} }

View File

@ -6,11 +6,16 @@ import java.util.Optional;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.ObjectUtils;
import org.springframework.data.domain.Sort;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.util.CollectionUtils;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import run.halo.app.core.extension.content.Tag; import run.halo.app.core.extension.content.Tag;
import run.halo.app.extension.ListOptions;
import run.halo.app.extension.ListResult; import run.halo.app.extension.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.theme.finders.Finder; import run.halo.app.theme.finders.Finder;
import run.halo.app.theme.finders.TagFinder; import run.halo.app.theme.finders.TagFinder;
@ -48,7 +53,8 @@ public class TagFinderImpl implements TagFinder {
@Override @Override
public Mono<ListResult<TagVo>> list(Integer page, Integer size) { public Mono<ListResult<TagVo>> list(Integer page, Integer size) {
return list(page, size, null, null); return listBy(new ListOptions(),
PageRequestImpl.of(pageNullSafe(page), sizeNullSafe(size)));
} }
@Override @Override
@ -68,13 +74,34 @@ public class TagFinderImpl implements TagFinder {
new ListResult<>(pageNullSafe(page), sizeNullSafe(size), 0L, List.of())); new ListResult<>(pageNullSafe(page), sizeNullSafe(size), 0L, List.of()));
} }
@Override
public List<TagVo> convertToVo(List<Tag> tags) {
if (CollectionUtils.isEmpty(tags)) {
return List.of();
}
return tags.stream()
.map(TagVo::from)
.collect(Collectors.toList());
}
@Override @Override
public Flux<TagVo> listAll() { public Flux<TagVo> listAll() {
return client.list(Tag.class, null, return client.listAll(Tag.class, new ListOptions(),
DEFAULT_COMPARATOR.reversed()) Sort.by(Sort.Order.desc("metadata.creationTimestamp")))
.map(TagVo::from); .map(TagVo::from);
} }
private Mono<ListResult<TagVo>> listBy(ListOptions listOptions, PageRequest pageRequest) {
return client.listBy(Tag.class, listOptions, pageRequest)
.map(result -> {
List<TagVo> tagVos = result.get()
.map(TagVo::from)
.collect(Collectors.toList());
return new ListResult<>(result.getPage(), result.getSize(), result.getTotal(),
tagVos);
});
}
int pageNullSafe(Integer page) { int pageNullSafe(Integer page) {
return ObjectUtils.defaultIfNull(page, 1); return ObjectUtils.defaultIfNull(page, 1);
} }

View File

@ -1,5 +1,10 @@
package run.halo.app.theme.router; package run.halo.app.theme.router;
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.isNull;
import static run.halo.app.extension.index.query.QueryFactory.or;
import java.security.Principal; import java.security.Principal;
import java.util.Objects; import java.util.Objects;
import java.util.function.Predicate; import java.util.function.Predicate;
@ -9,6 +14,9 @@ import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import run.halo.app.core.extension.content.Post; import run.halo.app.core.extension.content.Post;
import run.halo.app.extension.ExtensionUtil; import run.halo.app.extension.ExtensionUtil;
import run.halo.app.extension.ListOptions;
import run.halo.app.extension.router.selector.FieldSelector;
import run.halo.app.extension.router.selector.LabelSelector;
import run.halo.app.infra.AnonymousUserConst; import run.halo.app.infra.AnonymousUserConst;
/** /**
@ -34,6 +42,28 @@ public class DefaultQueryPostPredicateResolver implements ReactiveQueryPostPredi
.defaultIfEmpty(predicate.and(visiblePredicate)); .defaultIfEmpty(predicate.and(visiblePredicate));
} }
@Override
public Mono<ListOptions> getListOptions() {
var listOptions = new ListOptions();
listOptions.setLabelSelector(LabelSelector.builder()
.eq(Post.PUBLISHED_LABEL, "true").build());
var fieldQuery = and(
isNull("metadata.deletionTimestamp"),
equal("spec.deleted", "false")
);
var visibleQuery = equal("spec.visible", Post.VisibleEnum.PUBLIC.name());
return currentUserName()
.map(username -> and(fieldQuery,
or(visibleQuery, equal("spec.owner", username)))
)
.defaultIfEmpty(and(fieldQuery, visibleQuery))
.map(query -> {
listOptions.setFieldSelector(FieldSelector.of(query));
return listOptions;
});
}
Mono<String> currentUserName() { Mono<String> currentUserName() {
return ReactiveSecurityContextHolder.getContext() return ReactiveSecurityContextHolder.getContext()
.map(SecurityContext::getAuthentication) .map(SecurityContext::getAuthentication)

View File

@ -5,6 +5,7 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.context.ApplicationListener; import org.springframework.context.ApplicationListener;
import org.springframework.data.domain.Sort;
import org.springframework.lang.NonNull; import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import run.halo.app.core.extension.content.Category; import run.halo.app.core.extension.content.Category;
@ -14,6 +15,7 @@ import run.halo.app.core.extension.content.Tag;
import run.halo.app.extension.AbstractExtension; import run.halo.app.extension.AbstractExtension;
import run.halo.app.extension.Extension; import run.halo.app.extension.Extension;
import run.halo.app.extension.ExtensionClient; import run.halo.app.extension.ExtensionClient;
import run.halo.app.extension.ListOptions;
import run.halo.app.extension.MetadataOperator; import run.halo.app.extension.MetadataOperator;
import run.halo.app.extension.MetadataUtil; import run.halo.app.extension.MetadataUtil;
import run.halo.app.theme.DefaultTemplateEnum; import run.halo.app.theme.DefaultTemplateEnum;
@ -52,7 +54,7 @@ public class ExtensionPermalinkPatternUpdater
private void updatePostPermalink(String pattern) { private void updatePostPermalink(String pattern) {
log.debug("Update post permalink by new policy [{}]", pattern); log.debug("Update post permalink by new policy [{}]", pattern);
client.list(Post.class, null, null) client.listAll(Post.class, new ListOptions(), Sort.unsorted())
.forEach(post -> updateIfPermalinkPatternChanged(post, pattern)); .forEach(post -> updateIfPermalinkPatternChanged(post, pattern));
} }
@ -70,13 +72,13 @@ public class ExtensionPermalinkPatternUpdater
private void updateCategoryPermalink(String pattern) { private void updateCategoryPermalink(String pattern) {
log.debug("Update category and categories permalink by new policy [{}]", pattern); log.debug("Update category and categories permalink by new policy [{}]", pattern);
client.list(Category.class, null, null) client.listAll(Category.class, new ListOptions(), Sort.unsorted())
.forEach(category -> updateIfPermalinkPatternChanged(category, pattern)); .forEach(category -> updateIfPermalinkPatternChanged(category, pattern));
} }
private void updateTagPermalink(String pattern) { private void updateTagPermalink(String pattern) {
log.debug("Update tag and tags permalink by new policy [{}]", pattern); log.debug("Update tag and tags permalink by new policy [{}]", pattern);
client.list(Tag.class, null, null) client.listAll(Tag.class, new ListOptions(), Sort.unsorted())
.forEach(tag -> updateIfPermalinkPatternChanged(tag, pattern)); .forEach(tag -> updateIfPermalinkPatternChanged(tag, pattern));
} }
} }

View File

@ -3,6 +3,7 @@ package run.halo.app.theme.router;
import java.util.function.Predicate; import java.util.function.Predicate;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import run.halo.app.core.extension.content.Post; import run.halo.app.core.extension.content.Post;
import run.halo.app.extension.ListOptions;
/** /**
* The reactive query post predicate resolver. * The reactive query post predicate resolver.
@ -13,4 +14,6 @@ import run.halo.app.core.extension.content.Post;
public interface ReactiveQueryPostPredicateResolver { public interface ReactiveQueryPostPredicateResolver {
Mono<Predicate<Post>> getPredicate(); Mono<Predicate<Post>> getPredicate();
Mono<ListOptions> getListOptions();
} }

View File

@ -17,7 +17,12 @@ import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.i18n.LocaleContextResolver; import org.springframework.web.server.i18n.LocaleContextResolver;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import run.halo.app.core.extension.content.Category; import run.halo.app.core.extension.content.Category;
import run.halo.app.extension.ListOptions;
import run.halo.app.extension.ListResult;
import run.halo.app.extension.PageRequestImpl;
import run.halo.app.extension.ReactiveExtensionClient; import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.extension.index.query.QueryFactory;
import run.halo.app.extension.router.selector.FieldSelector;
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher; import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
import run.halo.app.infra.SystemSetting; import run.halo.app.infra.SystemSetting;
import run.halo.app.infra.exception.NotFoundException; import run.halo.app.infra.exception.NotFoundException;
@ -81,10 +86,18 @@ public class CategoryPostRouteFactory implements RouteFactory {
} }
Mono<CategoryVo> fetchBySlug(String slug) { Mono<CategoryVo> fetchBySlug(String slug) {
return client.list(Category.class, category -> category.getSpec().getSlug().equals(slug) var listOptions = new ListOptions();
&& category.getMetadata().getDeletionTimestamp() == null, null) listOptions.setFieldSelector(FieldSelector.of(
.next() QueryFactory.and(
.map(CategoryVo::from); QueryFactory.equal("spec.slug", slug),
QueryFactory.isNull("metadata.deletionTimestamp")
)
));
return client.listBy(Category.class, listOptions, PageRequestImpl.ofSize(1))
.mapNotNull(result -> ListResult.first(result)
.map(CategoryVo::from)
.orElse(null)
);
} }
private Mono<UrlContextListResult<ListedPostVo>> postListByCategoryName(String name, private Mono<UrlContextListResult<ListedPostVo>> postListByCategoryName(String name,

View File

@ -15,6 +15,7 @@ import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.Getter; import lombok.Getter;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.data.domain.Sort;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.lang.NonNull; import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@ -32,6 +33,7 @@ import reactor.core.publisher.Mono;
import run.halo.app.core.extension.content.Post; import run.halo.app.core.extension.content.Post;
import run.halo.app.extension.MetadataUtil; import run.halo.app.extension.MetadataUtil;
import run.halo.app.extension.ReactiveExtensionClient; import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.extension.index.query.QueryFactory;
import run.halo.app.infra.exception.NotFoundException; import run.halo.app.infra.exception.NotFoundException;
import run.halo.app.infra.utils.JsonUtils; import run.halo.app.infra.utils.JsonUtils;
import run.halo.app.theme.DefaultTemplateEnum; import run.halo.app.theme.DefaultTemplateEnum;
@ -159,11 +161,14 @@ public class PostRouteFactory implements RouteFactory {
} }
private Flux<Post> fetchPostsBySlug(String slug) { private Flux<Post> fetchPostsBySlug(String slug) {
return queryPostPredicateResolver.getPredicate() return queryPostPredicateResolver.getListOptions()
.flatMapMany(predicate -> client.list(Post.class, .flatMapMany(listOptions -> {
predicate.and(post -> matchIfPresent(slug, post.getSpec().getSlug())), if (StringUtils.isNotBlank(slug)) {
null) var other = QueryFactory.equal("spec.slug", slug);
); listOptions.setFieldSelector(listOptions.getFieldSelector().andQuery(other));
}
return client.listAll(Post.class, listOptions, Sort.unsorted());
});
} }
private boolean matchIfPresent(String variable, String target) { private boolean matchIfPresent(String variable, String target) {

View File

@ -15,7 +15,12 @@ import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.i18n.LocaleContextResolver; import org.springframework.web.server.i18n.LocaleContextResolver;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import run.halo.app.core.extension.content.Tag; import run.halo.app.core.extension.content.Tag;
import run.halo.app.extension.ListOptions;
import run.halo.app.extension.ListResult;
import run.halo.app.extension.PageRequestImpl;
import run.halo.app.extension.ReactiveExtensionClient; import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.extension.index.query.QueryFactory;
import run.halo.app.extension.router.selector.FieldSelector;
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher; import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
import run.halo.app.infra.SystemSetting; import run.halo.app.infra.SystemSetting;
import run.halo.app.infra.exception.NotFoundException; import run.halo.app.infra.exception.NotFoundException;
@ -95,9 +100,14 @@ public class TagPostRouteFactory implements RouteFactory {
} }
private Mono<TagVo> tagBySlug(String slug) { private Mono<TagVo> tagBySlug(String slug) {
return client.list(Tag.class, tag -> tag.getSpec().getSlug().equals(slug) var listOptions = new ListOptions();
&& tag.getMetadata().getDeletionTimestamp() == null, null) listOptions.setFieldSelector(FieldSelector.of(
.next() QueryFactory.and(QueryFactory.equal("spec.slug", slug),
QueryFactory.isNull("metadata.deletionTimestamp")
)
));
return client.listBy(Tag.class, listOptions, PageRequestImpl.ofSize(1))
.mapNotNull(result -> ListResult.first(result).orElse(null))
.flatMap(tag -> tagFinder.getByName(tag.getMetadata().getName())) .flatMap(tag -> tagFinder.getByName(tag.getMetadata().getName()))
.switchIfEmpty( .switchIfEmpty(
Mono.error(new NotFoundException("Tag not found with slug: " + slug))); Mono.error(new NotFoundException("Tag not found with slug: " + slug)));

View File

@ -1,368 +0,0 @@
package run.halo.app.content;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.json.JSONException;
import org.junit.jupiter.api.Test;
import org.skyscreamer.jsonassert.JSONAssert;
import run.halo.app.core.extension.content.Post;
import run.halo.app.extension.Metadata;
import run.halo.app.infra.utils.JsonUtils;
/**
* Tests for {@link DefaultIndexer}.
*
* @author guqing
* @since 2.0.0
*/
class DefaultIndexerTest {
@Test
public void testTagsIndexer() throws JSONException {
// Create a new Indexer that indexes Post objects by tags.
DefaultIndexer<Post> indexer = new DefaultIndexer<>();
String tagsIndexName = "tags";
indexer.addIndexFunc(tagsIndexName, post -> {
List<String> tags = post.getSpec().getTags();
return tags == null ? Set.of() : Set.copyOf(tags);
});
// Create some Post objects.
Post post1 = new Post();
post1.setMetadata(new Metadata());
post1.getMetadata().setName("post-1");
post1.setSpec(new Post.PostSpec());
post1.getSpec().setTags(List.of("t1", "t2"));
Post post2 = new Post();
post2.setMetadata(new Metadata());
post2.getMetadata().setName("post-2");
post2.setSpec(new Post.PostSpec());
post2.getSpec().setTags(List.of("t2", "t3"));
Post post3 = new Post();
post3.setMetadata(new Metadata());
post3.getMetadata().setName("post-3");
post3.setSpec(new Post.PostSpec());
post3.getSpec().setTags(List.of("t3", "t4"));
// Add the Post objects to the Indexer.
indexer.add(tagsIndexName, post1);
indexer.add(tagsIndexName, post2);
indexer.add(tagsIndexName, post3);
// Verify that the Indexer has the correct indices.
JSONAssert.assertEquals("""
{
"t4": [
"post-3"
],
"t3": [
"post-2",
"post-3"
],
"t2": [
"post-1",
"post-2"
],
"t1": [
"post-1"
]
}
""",
JsonUtils.objectToJson(indexer.getIndices("tags")),
true);
// Remove post2 from the Indexer.
indexer.delete(tagsIndexName, post2);
// Verify that the Indexer has the correct indices.
JSONAssert.assertEquals("""
{
"t1": [
"post-1"
],
"t2": [
"post-1"
],
"t3": [
"post-3"
],
"t4": [
"post-3"
]
}
""",
JsonUtils.objectToJson(indexer.getIndices("tags")),
true);
// Update post3 in the Indexer.
post3.getSpec().setTags(List.of("t4", "t5"));
indexer.update(tagsIndexName, post3);
// Verify that the Indexer has the correct indices.
JSONAssert.assertEquals("""
{
"t1": [
"post-1"
],
"t2": [
"post-1"
],
"t4": [
"post-3"
],
"t5": [
"post-3"
]
}
""",
JsonUtils.objectToJson(indexer.getIndices("tags")),
true);
}
@Test
public void testLabelIndexer() throws JSONException {
// Create a new Indexer.
DefaultIndexer<Post> indexer = new DefaultIndexer<>();
// Define the IndexFunc for labels.
DefaultIndexer.IndexFunc<Post> labelIndexFunc = labelIndexFunc();
// Add the label IndexFunc to the Indexer.
String labelsIndexName = "labels";
indexer.addIndexFunc(labelsIndexName, labelIndexFunc);
// Create some posts with labels.
Post post1 = new Post();
post1.setMetadata(new Metadata());
post1.getMetadata().setName("post-1");
post1.getMetadata().setLabels(Map.of("app", "myapp", "env", "prod"));
Post post2 = new Post();
post2.setMetadata(new Metadata());
post2.getMetadata().setName("post-2");
post2.getMetadata().setLabels(Map.of("app", "myapp", "env", "test"));
Post post3 = new Post();
post3.setMetadata(new Metadata());
post3.getMetadata().setName("post-3");
post3.getMetadata().setLabels(Map.of("app", "otherapp", "env", "prod"));
// Add the posts to the Indexer.
indexer.add(labelsIndexName, post1);
indexer.add(labelsIndexName, post2);
indexer.add(labelsIndexName, post3);
// Verify that the Indexer has the correct indices.
assertEquals(
Map.of(
"app=myapp", Set.of("post-1", "post-2"),
"app=otherapp", Set.of("post-3"),
"env=test", Set.of("post-2"),
"env=prod", Set.of("post-1", "post-3")
),
indexer.getIndices("labels"));
// Delete post2 from the Indexer.
indexer.delete(labelsIndexName, post2);
// Verify that the Indexer has the correct indices.
JSONAssert.assertEquals("""
{
"app=myapp": [
"post-1"
],
"env=prod": [
"post-1",
"post-3"
],
"app=otherapp": [
"post-3"
]
}
""",
JsonUtils.objectToJson(indexer.getIndices("labels")),
true);
// Update post2 in the Indexer.
post2.getMetadata().setLabels(Map.of("l1", "v1", "l2", "v2"));
indexer.update(labelsIndexName, post2);
// Verify that the Indexer has the correct indices.
JSONAssert.assertEquals("""
{
"app=myapp": [
"post-1"
],
"env=prod": [
"post-1",
"post-3"
],
"app=otherapp": [
"post-3"
],
"l1=v1": [
"post-2"
],
"l2=v2": [
"post-2"
]
}
""",
JsonUtils.objectToJson(indexer.getIndices("labels")),
true);
// Update post1 in the Indexer.
post1.getMetadata().setLabels(Map.of("l2", "v2", "l3", "v3"));
indexer.update(labelsIndexName, post1);
// Verify that the Indexer has the correct indices.
JSONAssert.assertEquals("""
{
"env=prod": [
"post-3"
],
"app=otherapp": [
"post-3"
],
"l1=v1": [
"post-2"
],
"l2=v2": [
"post-1",
"post-2"
],
"l3=v3": [
"post-1"
]
}
""",
JsonUtils.objectToJson(indexer.getIndices("labels")),
true);
}
@Test
void multiIndexName() {
// Create a new Indexer.
DefaultIndexer<Post> indexer = new DefaultIndexer<>();
// Define the IndexFunc for labels.
String labelsIndexName = "labels";
DefaultIndexer.IndexFunc<Post> labelIndexFunc = labelIndexFunc();
indexer.addIndexFunc(labelsIndexName, labelIndexFunc);
String tagsIndexName = "tags";
indexer.addIndexFunc(tagsIndexName, post -> {
List<String> tags = post.getSpec().getTags();
return tags == null ? Set.of() : Set.copyOf(tags);
});
Post post1 = new Post();
post1.setMetadata(new Metadata());
post1.getMetadata().setName("post-1");
post1.getMetadata().setLabels(Map.of("app", "myapp", "env", "prod"));
post1.setSpec(new Post.PostSpec());
post1.getSpec().setTags(List.of("t1", "t2"));
Post post2 = new Post();
post2.setMetadata(new Metadata());
post2.getMetadata().setName("post-2");
post2.getMetadata().setLabels(Map.of("app", "myapp", "env", "test"));
post2.setSpec(new Post.PostSpec());
post2.getSpec().setTags(List.of("t2", "t3"));
indexer.add(labelsIndexName, post1);
indexer.add(tagsIndexName, post1);
indexer.add(labelsIndexName, post2);
indexer.add(tagsIndexName, post2);
assertThat(indexer.getByIndex(labelsIndexName, "app=myapp"))
.containsExactlyInAnyOrder("post-1", "post-2");
assertThat(indexer.getByIndex(tagsIndexName, "t1"))
.containsExactlyInAnyOrder("post-1");
assertThat(indexer.getByIndex(labelsIndexName, "env=test"))
.containsExactlyInAnyOrder("post-2");
assertThat(indexer.getByIndex(tagsIndexName, "t2"))
.containsExactlyInAnyOrder("post-1", "post-2");
post2.getSpec().setTags(List.of("t1", "t4"));
indexer.update(tagsIndexName, post2);
assertThat(indexer.getByIndex(tagsIndexName, "t1"))
.containsExactlyInAnyOrder("post-1", "post-2");
assertThat(indexer.getByIndex(tagsIndexName, "t2"))
.containsExactlyInAnyOrder("post-1");
assertThat(indexer.getByIndex(tagsIndexName, "t4"))
.containsExactlyInAnyOrder("post-2");
}
private static DefaultIndexer.IndexFunc<Post> labelIndexFunc() {
return post -> {
Map<String, String> labels = post.getMetadata().getLabels();
Set<String> indexKeys = new HashSet<>();
if (labels != null) {
for (Map.Entry<String, String> entry : labels.entrySet()) {
indexKeys.add(entry.getKey() + "=" + entry.getValue());
}
}
return indexKeys;
};
}
@Test
void getByIndex() {
DefaultIndexer<Post> indexer = new DefaultIndexer<>();
String tagsIndexName = "tags";
indexer.addIndexFunc(tagsIndexName, post -> {
List<String> tags = post.getSpec().getTags();
return tags == null ? Set.of() : Set.copyOf(tags);
});
// Create some Post objects.
Post post1 = new Post();
post1.setMetadata(new Metadata());
post1.getMetadata().setName("post-1");
post1.setSpec(new Post.PostSpec());
post1.getSpec().setTags(List.of("t1", "t2"));
Post post2 = new Post();
post2.setMetadata(new Metadata());
post2.getMetadata().setName("post-2");
post2.setSpec(new Post.PostSpec());
post2.getSpec().setTags(List.of("t2", "t3"));
indexer.add(tagsIndexName, post1);
indexer.add(tagsIndexName, post2);
assertThat(indexer.getByIndex(tagsIndexName, "t1"))
.containsExactlyInAnyOrder("post-1");
assertThat(indexer.getByIndex(tagsIndexName, "t2"))
.containsExactlyInAnyOrder("post-1", "post-2");
assertThat(indexer.getByIndex(tagsIndexName, "t3"))
.containsExactlyInAnyOrder("post-2");
}
@Test
void addButNotIndexFunc() {
// Create some Post objects.
Post post1 = new Post();
post1.setMetadata(new Metadata());
post1.getMetadata().setName("post-1");
post1.setSpec(new Post.PostSpec());
post1.getSpec().setTags(List.of("t1", "t2"));
// Create a new Indexer that indexes Post objects by tags.
final DefaultIndexer<Post> indexer = new DefaultIndexer<>();
assertThrows(IllegalArgumentException.class, () -> {
indexer.add("fake-index-name", post1);
}, "Index function not found for index name 'fake-index-name'");
}
}

View File

@ -3,6 +3,7 @@ package run.halo.app.content;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -12,7 +13,7 @@ import org.springframework.mock.web.reactive.function.server.MockServerRequest;
import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap; import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebExchange;
import run.halo.app.core.extension.content.Post; import run.halo.app.extension.index.query.QueryIndexViewImpl;
/** /**
* Tests for {@link PostQuery}. * Tests for {@link PostQuery}.
@ -32,61 +33,20 @@ class PostQueryTest {
.build(); .build();
PostQuery postQuery = new PostQuery(request, "faker"); PostQuery postQuery = new PostQuery(request, "faker");
var spec = new Post.PostSpec();
var post = new Post();
post.setSpec(spec);
spec.setOwner("another-faker"); var listOptions = postQuery.toListOptions();
assertThat(postQuery.toPredicate().test(post)).isFalse(); assertThat(listOptions).isNotNull();
assertThat(listOptions.getFieldSelector()).isNotNull();
var nameEntry =
(Collection<Map.Entry<String, String>>) List.of(Map.entry("metadata.name", "faker"));
var entry = (Collection<Map.Entry<String, String>>) List.of(Map.entry("faker", "faker"));
var indexView =
new QueryIndexViewImpl(Map.of("spec.owner", entry, "metadata.name", nameEntry));
assertThat(listOptions.getFieldSelector().query().matches(indexView))
.containsExactly("faker");
spec.setOwner("faker"); entry = List.of(Map.entry("another-faker", "user1"));
assertThat(postQuery.toPredicate().test(post)).isTrue(); indexView = new QueryIndexViewImpl(Map.of("spec.owner", entry, "metadata.name", nameEntry));
} assertThat(listOptions.getFieldSelector().query().matches(indexView)).isEmpty();
@Test
void toPredicate() {
MultiValueMap<String, String> multiValueMap = new LinkedMultiValueMap<>();
multiValueMap.put("category", List.of("category1", "category2"));
MockServerRequest request = MockServerRequest.builder()
.queryParams(multiValueMap)
.exchange(mock(ServerWebExchange.class))
.build();
PostQuery postQuery = new PostQuery(request);
Post post = TestPost.postV1();
post.getSpec().setTags(null);
post.getStatusOrDefault().setContributors(null);
post.getSpec().setCategories(List.of("category1"));
boolean test = postQuery.toPredicate().test(post);
assertThat(test).isTrue();
post.getSpec().setTags(List.of("tag1"));
test = postQuery.toPredicate().test(post);
assertThat(test).isTrue();
// Do not include tags
multiValueMap.put("tag", List.of("tag2"));
post.getSpec().setTags(List.of("tag1"));
post.getSpec().setCategories(null);
test = postQuery.toPredicate().test(post);
assertThat(test).isFalse();
multiValueMap.put("tag", List.of());
multiValueMap.remove("category");
request = MockServerRequest.builder()
.exchange(mock(ServerWebExchange.class))
.queryParams(multiValueMap).build();
postQuery = new PostQuery(request);
post.getSpec().setTags(List.of());
test = postQuery.toPredicate().test(post);
assertThat(test).isTrue();
multiValueMap.put("labelSelector", List.of("hello"));
test = postQuery.toPredicate().test(post);
assertThat(test).isFalse();
post.getMetadata().setLabels(Map.of("hello", "world"));
test = postQuery.toPredicate().test(post);
assertThat(test).isTrue();
} }
} }

View File

@ -10,18 +10,19 @@ import static org.mockito.Mockito.verify;
import java.time.Duration; import java.time.Duration;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import org.json.JSONException;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks; import org.mockito.InjectMocks;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.domain.Sort;
import run.halo.app.content.TestPost; import run.halo.app.content.TestPost;
import run.halo.app.content.permalinks.CategoryPermalinkPolicy; import run.halo.app.content.permalinks.CategoryPermalinkPolicy;
import run.halo.app.core.extension.content.Category; import run.halo.app.core.extension.content.Category;
import run.halo.app.core.extension.content.Post; import run.halo.app.core.extension.content.Post;
import run.halo.app.extension.ExtensionClient; import run.halo.app.extension.ExtensionClient;
import run.halo.app.extension.ListOptions;
import run.halo.app.extension.Metadata; import run.halo.app.extension.Metadata;
import run.halo.app.extension.controller.Reconciler; import run.halo.app.extension.controller.Reconciler;
@ -43,7 +44,7 @@ class CategoryReconcilerTest {
private CategoryReconciler categoryReconciler; private CategoryReconciler categoryReconciler;
@Test @Test
void reconcileStatusPostForCategoryA() throws JSONException { void reconcileStatusPostForCategoryA() {
reconcileStatusPostPilling("category-A"); reconcileStatusPostPilling("category-A");
ArgumentCaptor<Category> captor = ArgumentCaptor.forClass(Category.class); ArgumentCaptor<Category> captor = ArgumentCaptor.forClass(Category.class);
@ -54,7 +55,7 @@ class CategoryReconcilerTest {
} }
@Test @Test
void reconcileStatusPostForCategoryB() throws JSONException { void reconcileStatusPostForCategoryB() {
reconcileStatusPostPilling("category-B"); reconcileStatusPostPilling("category-B");
ArgumentCaptor<Category> captor = ArgumentCaptor.forClass(Category.class); ArgumentCaptor<Category> captor = ArgumentCaptor.forClass(Category.class);
verify(client, times(3)).update(captor.capture()); verify(client, times(3)).update(captor.capture());
@ -64,7 +65,7 @@ class CategoryReconcilerTest {
} }
@Test @Test
void reconcileStatusPostForCategoryC() throws JSONException { void reconcileStatusPostForCategoryC() {
reconcileStatusPostPilling("category-C"); reconcileStatusPostPilling("category-C");
ArgumentCaptor<Category> captor = ArgumentCaptor.forClass(Category.class); ArgumentCaptor<Category> captor = ArgumentCaptor.forClass(Category.class);
verify(client, times(3)).update(captor.capture()); verify(client, times(3)).update(captor.capture());
@ -74,7 +75,7 @@ class CategoryReconcilerTest {
} }
@Test @Test
void reconcileStatusPostForCategoryD() throws JSONException { void reconcileStatusPostForCategoryD() {
reconcileStatusPostPilling("category-D"); reconcileStatusPostPilling("category-D");
ArgumentCaptor<Category> captor = ArgumentCaptor.forClass(Category.class); ArgumentCaptor<Category> captor = ArgumentCaptor.forClass(Category.class);
verify(client, times(3)).update(captor.capture()); verify(client, times(3)).update(captor.capture());
@ -89,9 +90,9 @@ class CategoryReconcilerTest {
.thenReturn(Optional.of(category)); .thenReturn(Optional.of(category));
}); });
lenient().when(client.list(eq(Post.class), any(), any())) lenient().when(client.listAll(eq(Post.class), any(ListOptions.class), any(Sort.class)))
.thenReturn(posts()); .thenReturn(posts());
lenient().when(client.list(eq(Category.class), any(), any())) lenient().when(client.listAll(eq(Category.class), any(), any()))
.thenReturn(categories()); .thenReturn(categories());
Reconciler.Result result = Reconciler.Result result =

View File

@ -85,7 +85,7 @@ class PostReconcilerTest {
Snapshot snapshotV2 = TestPost.snapshotV2(); Snapshot snapshotV2 = TestPost.snapshotV2();
snapshotV1.getSpec().setContributors(Set.of("guqing")); snapshotV1.getSpec().setContributors(Set.of("guqing"));
snapshotV2.getSpec().setContributors(Set.of("guqing", "zhangsan")); snapshotV2.getSpec().setContributors(Set.of("guqing", "zhangsan"));
when(client.list(eq(Snapshot.class), any(), any())) when(client.listAll(eq(Snapshot.class), any(), any()))
.thenReturn(List.of(snapshotV1, snapshotV2)); .thenReturn(List.of(snapshotV1, snapshotV2));
ArgumentCaptor<Post> captor = ArgumentCaptor.forClass(Post.class); ArgumentCaptor<Post> captor = ArgumentCaptor.forClass(Post.class);
@ -126,7 +126,7 @@ class PostReconcilerTest {
Snapshot snapshotV1 = TestPost.snapshotV1(); Snapshot snapshotV1 = TestPost.snapshotV1();
snapshotV1.getSpec().setContributors(Set.of("guqing")); snapshotV1.getSpec().setContributors(Set.of("guqing"));
when(client.list(eq(Snapshot.class), any(), any())) when(client.listAll(eq(Snapshot.class), any(), any()))
.thenReturn(List.of(snapshotV1, snapshotV2)); .thenReturn(List.of(snapshotV1, snapshotV2));
ArgumentCaptor<Post> captor = ArgumentCaptor.forClass(Post.class); ArgumentCaptor<Post> captor = ArgumentCaptor.forClass(Post.class);
@ -162,7 +162,7 @@ class PostReconcilerTest {
when(client.fetch(eq(Snapshot.class), eq(post.getSpec().getReleaseSnapshot()))) when(client.fetch(eq(Snapshot.class), eq(post.getSpec().getReleaseSnapshot())))
.thenReturn(Optional.of(snapshotV2)); .thenReturn(Optional.of(snapshotV2));
when(client.list(eq(Snapshot.class), any(), any())) when(client.listAll(eq(Snapshot.class), any(), any()))
.thenReturn(List.of()); .thenReturn(List.of());
ArgumentCaptor<Post> captor = ArgumentCaptor.forClass(Post.class); ArgumentCaptor<Post> captor = ArgumentCaptor.forClass(Post.class);
@ -191,7 +191,7 @@ class PostReconcilerTest {
.rawType("markdown") .rawType("markdown")
.build())); .build()));
when(client.list(eq(Snapshot.class), any(), any())) when(client.listAll(eq(Snapshot.class), any(), any()))
.thenReturn(List.of()); .thenReturn(List.of());
ArgumentCaptor<Post> captor = ArgumentCaptor.forClass(Post.class); ArgumentCaptor<Post> captor = ArgumentCaptor.forClass(Post.class);

View File

@ -95,7 +95,7 @@ class SinglePageReconcilerTest {
Snapshot snapshotV2 = TestPost.snapshotV2(); Snapshot snapshotV2 = TestPost.snapshotV2();
snapshotV1.getSpec().setContributors(Set.of("guqing")); snapshotV1.getSpec().setContributors(Set.of("guqing"));
snapshotV2.getSpec().setContributors(Set.of("guqing", "zhangsan")); snapshotV2.getSpec().setContributors(Set.of("guqing", "zhangsan"));
when(client.list(eq(Snapshot.class), any(), any())) when(client.listAll(eq(Snapshot.class), any(), any()))
.thenReturn(List.of(snapshotV1, snapshotV2)); .thenReturn(List.of(snapshotV1, snapshotV2));
when(externalUrlSupplier.get()).thenReturn(URI.create("")); when(externalUrlSupplier.get()).thenReturn(URI.create(""));
@ -156,7 +156,7 @@ class SinglePageReconcilerTest {
when(client.fetch(eq(Snapshot.class), eq(page.getSpec().getReleaseSnapshot()))) when(client.fetch(eq(Snapshot.class), eq(page.getSpec().getReleaseSnapshot())))
.thenReturn(Optional.of(snapshotV2)); .thenReturn(Optional.of(snapshotV2));
when(client.list(eq(Snapshot.class), any(), any())) when(client.listAll(eq(Snapshot.class), any(), any()))
.thenReturn(List.of()); .thenReturn(List.of());
ArgumentCaptor<SinglePage> captor = ArgumentCaptor.forClass(SinglePage.class); ArgumentCaptor<SinglePage> captor = ArgumentCaptor.forClass(SinglePage.class);
@ -186,7 +186,7 @@ class SinglePageReconcilerTest {
.build()) .build())
); );
when(client.list(eq(Snapshot.class), any(), any())) when(client.listAll(eq(Snapshot.class), any(), any()))
.thenReturn(List.of()); .thenReturn(List.of());
ArgumentCaptor<SinglePage> captor = ArgumentCaptor.forClass(SinglePage.class); ArgumentCaptor<SinglePage> captor = ArgumentCaptor.forClass(SinglePage.class);

View File

@ -10,15 +10,14 @@ import static org.mockito.Mockito.when;
import java.time.Instant; import java.time.Instant;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.Set;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks; import org.mockito.InjectMocks;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import run.halo.app.content.PostIndexInformer;
import run.halo.app.content.permalinks.TagPermalinkPolicy; import run.halo.app.content.permalinks.TagPermalinkPolicy;
import run.halo.app.core.extension.content.Post;
import run.halo.app.core.extension.content.Tag; import run.halo.app.core.extension.content.Tag;
import run.halo.app.extension.ExtensionClient; import run.halo.app.extension.ExtensionClient;
import run.halo.app.extension.Metadata; import run.halo.app.extension.Metadata;
@ -37,9 +36,6 @@ class TagReconcilerTest {
@Mock @Mock
private TagPermalinkPolicy tagPermalinkPolicy; private TagPermalinkPolicy tagPermalinkPolicy;
@Mock
private PostIndexInformer postIndexInformer;
@InjectMocks @InjectMocks
private TagReconciler tagReconciler; private TagReconciler tagReconciler;
@ -48,8 +44,7 @@ class TagReconcilerTest {
Tag tag = tag(); Tag tag = tag();
when(client.fetch(eq(Tag.class), eq("fake-tag"))) when(client.fetch(eq(Tag.class), eq("fake-tag")))
.thenReturn(Optional.of(tag)); .thenReturn(Optional.of(tag));
when(postIndexInformer.getByTagName(eq("fake-tag"))) when(client.listAll(eq(Post.class), any(), any())).thenReturn(List.of());
.thenReturn(Set.of());
when(tagPermalinkPolicy.permalink(any())) when(tagPermalinkPolicy.permalink(any()))
.thenAnswer(arg -> "/tags/" + tag.getSpec().getSlug()); .thenAnswer(arg -> "/tags/" + tag.getSpec().getSlug());
ArgumentCaptor<Tag> captor = ArgumentCaptor.forClass(Tag.class); ArgumentCaptor<Tag> captor = ArgumentCaptor.forClass(Tag.class);
@ -85,8 +80,8 @@ class TagReconcilerTest {
Tag tag = tag(); Tag tag = tag();
when(client.fetch(eq(Tag.class), eq("fake-tag"))) when(client.fetch(eq(Tag.class), eq("fake-tag")))
.thenReturn(Optional.of(tag)); .thenReturn(Optional.of(tag));
when(postIndexInformer.getByTagName(eq("fake-tag"))) when(client.listAll(eq(Post.class), any(), any()))
.thenReturn(Set.of("fake-post-1", "fake-post-3")); .thenReturn(List.of(createPost("fake-post-1"), createPost("fake-post-2")));
ArgumentCaptor<Tag> captor = ArgumentCaptor.forClass(Tag.class); ArgumentCaptor<Tag> captor = ArgumentCaptor.forClass(Tag.class);
tagReconciler.reconcile(new TagReconciler.Request("fake-tag")); tagReconciler.reconcile(new TagReconciler.Request("fake-tag"));
@ -96,6 +91,14 @@ class TagReconcilerTest {
assertThat(allValues.get(1).getStatusOrDefault().getVisiblePostCount()).isEqualTo(0); assertThat(allValues.get(1).getStatusOrDefault().getVisiblePostCount()).isEqualTo(0);
} }
Post createPost(String name) {
var post = new Post();
post.setMetadata(new Metadata());
post.getMetadata().setName(name);
post.setSpec(new Post.PostSpec());
return post;
}
Tag tag() { Tag tag() {
Tag tag = new Tag(); Tag tag = new Tag();
tag.setMetadata(new Metadata()); tag.setMetadata(new Metadata());

View File

@ -6,6 +6,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
import java.lang.reflect.ParameterizedType; import java.lang.reflect.ParameterizedType;
import java.util.List; import java.util.List;
import java.util.Optional;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
class ListResultTest { class ListResultTest {
@ -53,6 +54,15 @@ class ListResultTest {
assertSubList(list); assertSubList(list);
} }
@Test
void firstTest() {
var listResult = new ListResult<>(List.of());
assertEquals(Optional.empty(), ListResult.first(listResult));
listResult = new ListResult<>(1, 10, 1, List.of("A"));
assertEquals(Optional.of("A"), ListResult.first(listResult));
}
private void assertSubList(List<Integer> list) { private void assertSubList(List<Integer> list) {
var result = ListResult.subList(list, 0, 0); var result = ListResult.subList(list, 0, 0);
assertEquals(list, result); assertEquals(list, result);

View File

@ -2,7 +2,6 @@ package run.halo.app.theme.endpoint;
import static org.assertj.core.api.Assertions.assertThat; 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.when; import static org.mockito.Mockito.when;
@ -19,8 +18,10 @@ import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import run.halo.app.core.extension.content.Category; import run.halo.app.core.extension.content.Category;
import run.halo.app.extension.GroupVersion; import run.halo.app.extension.GroupVersion;
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.theme.finders.PostPublicQueryService; import run.halo.app.theme.finders.PostPublicQueryService;
import run.halo.app.theme.finders.vo.ListedPostVo; import run.halo.app.theme.finders.vo.ListedPostVo;
@ -52,7 +53,7 @@ class CategoryQueryEndpointTest {
@Test @Test
void listCategories() { void listCategories() {
ListResult<Category> listResult = new ListResult<>(List.of()); ListResult<Category> listResult = new ListResult<>(List.of());
when(client.list(eq(Category.class), any(), any(), anyInt(), anyInt())) when(client.listBy(eq(Category.class), any(ListOptions.class), any(PageRequest.class)))
.thenReturn(Mono.just(listResult)); .thenReturn(Mono.just(listResult));
webTestClient.get() webTestClient.get()
@ -84,7 +85,7 @@ class CategoryQueryEndpointTest {
@Test @Test
void listPostsByCategoryName() { void listPostsByCategoryName() {
ListResult<ListedPostVo> listResult = new ListResult<>(List.of()); ListResult<ListedPostVo> listResult = new ListResult<>(List.of());
when(postPublicQueryService.list(anyInt(), anyInt(), any(), any())) when(postPublicQueryService.list(any(), any(PageRequest.class)))
.thenReturn(Mono.just(listResult)); .thenReturn(Mono.just(listResult));
webTestClient.get() webTestClient.get()

View File

@ -2,7 +2,6 @@ package run.halo.app.theme.endpoint;
import static org.assertj.core.api.Assertions.assertThat; 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.anyString; import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@ -20,6 +19,7 @@ import reactor.core.publisher.Mono;
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.Metadata; import run.halo.app.extension.Metadata;
import run.halo.app.extension.PageRequest;
import run.halo.app.theme.finders.PostFinder; import run.halo.app.theme.finders.PostFinder;
import run.halo.app.theme.finders.PostPublicQueryService; import run.halo.app.theme.finders.PostPublicQueryService;
import run.halo.app.theme.finders.vo.ListedPostVo; import run.halo.app.theme.finders.vo.ListedPostVo;
@ -55,7 +55,7 @@ class PostQueryEndpointTest {
@Test @Test
public void listPosts() { public void listPosts() {
ListResult<ListedPostVo> result = new ListResult<>(List.of()); ListResult<ListedPostVo> result = new ListResult<>(List.of());
when(postPublicQueryService.list(anyInt(), anyInt(), any(), any())) when(postPublicQueryService.list(any(), any(PageRequest.class)))
.thenReturn(Mono.just(result)); .thenReturn(Mono.just(result));
webClient.get().uri("/posts") webClient.get().uri("/posts")
@ -65,7 +65,7 @@ class PostQueryEndpointTest {
.expectBody() .expectBody()
.jsonPath("$.items").isArray(); .jsonPath("$.items").isArray();
verify(postPublicQueryService).list(anyInt(), anyInt(), any(), any()); verify(postPublicQueryService).list(any(), any(PageRequest.class));
} }
@Test @Test

View File

@ -2,7 +2,6 @@ package run.halo.app.theme.finders.impl;
import static org.assertj.core.api.Assertions.assertThat; 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.when; import static org.mockito.Mockito.when;
@ -18,11 +17,14 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import org.skyscreamer.jsonassert.JSONAssert; import org.skyscreamer.jsonassert.JSONAssert;
import org.springframework.data.domain.Sort;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import run.halo.app.core.extension.content.Category; import run.halo.app.core.extension.content.Category;
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.infra.utils.JsonUtils; import run.halo.app.infra.utils.JsonUtils;
import run.halo.app.theme.finders.vo.CategoryTreeVo; import run.halo.app.theme.finders.vo.CategoryTreeVo;
@ -85,7 +87,7 @@ class CategoryFinderImplTest {
categories().stream() categories().stream()
.sorted(CategoryFinderImpl.defaultComparator()) .sorted(CategoryFinderImpl.defaultComparator())
.toList()); .toList());
when(client.list(eq(Category.class), eq(null), any(), anyInt(), anyInt())) when(client.listBy(eq(Category.class), any(ListOptions.class), any(PageRequest.class)))
.thenReturn(Mono.just(categories)); .thenReturn(Mono.just(categories));
ListResult<CategoryVo> list = categoryFinder.list(1, 10).block(); ListResult<CategoryVo> list = categoryFinder.list(1, 10).block();
assertThat(list.getItems()).hasSize(3); assertThat(list.getItems()).hasSize(3);
@ -95,7 +97,7 @@ class CategoryFinderImplTest {
@Test @Test
void listAsTree() { void listAsTree() {
when(client.list(eq(Category.class), eq(null), any())) when(client.listAll(eq(Category.class), any(ListOptions.class), any(Sort.class)))
.thenReturn(Flux.fromIterable(categoriesForTree())); .thenReturn(Flux.fromIterable(categoriesForTree()));
List<CategoryTreeVo> treeVos = categoryFinder.listAsTree().collectList().block(); List<CategoryTreeVo> treeVos = categoryFinder.listAsTree().collectList().block();
assertThat(treeVos).hasSize(1); assertThat(treeVos).hasSize(1);
@ -103,7 +105,7 @@ class CategoryFinderImplTest {
@Test @Test
void listSubTreeByName() { void listSubTreeByName() {
when(client.list(eq(Category.class), eq(null), any())) when(client.listAll(eq(Category.class), any(ListOptions.class), any(Sort.class)))
.thenReturn(Flux.fromIterable(categoriesForTree())); .thenReturn(Flux.fromIterable(categoriesForTree()));
List<CategoryTreeVo> treeVos = categoryFinder.listAsTree("E").collectList().block(); List<CategoryTreeVo> treeVos = categoryFinder.listAsTree("E").collectList().block();
assertThat(treeVos.get(0).getMetadata().getName()).isEqualTo("E"); assertThat(treeVos.get(0).getMetadata().getName()).isEqualTo("E");
@ -119,7 +121,7 @@ class CategoryFinderImplTest {
*/ */
@Test @Test
void listAsTreeMore() { void listAsTreeMore() {
when(client.list(eq(Category.class), eq(null), any())) when(client.listAll(eq(Category.class), any(ListOptions.class), any(Sort.class)))
.thenReturn(Flux.fromIterable(moreCategories())); .thenReturn(Flux.fromIterable(moreCategories()));
List<CategoryTreeVo> treeVos = categoryFinder.listAsTree().collectList().block(); List<CategoryTreeVo> treeVos = categoryFinder.listAsTree().collectList().block();
String s = visualizeTree(treeVos); String s = visualizeTree(treeVos);

View File

@ -2,7 +2,6 @@ package run.halo.app.theme.finders.impl;
import static org.assertj.core.api.Assertions.assertThat; 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.Mockito.when; import static org.mockito.Mockito.when;
import java.time.Instant; import java.time.Instant;
@ -11,8 +10,6 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.function.Predicate; import java.util.function.Predicate;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.logging.log4j.util.Strings;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks; import org.mockito.InjectMocks;
@ -23,6 +20,7 @@ import run.halo.app.content.PostService;
import run.halo.app.core.extension.content.Post; import run.halo.app.core.extension.content.Post;
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.metrics.CounterService; import run.halo.app.metrics.CounterService;
import run.halo.app.theme.finders.CategoryFinder; import run.halo.app.theme.finders.CategoryFinder;
@ -67,15 +65,6 @@ class PostFinderImplTest {
@InjectMocks @InjectMocks
private PostFinderImpl postFinder; private PostFinderImpl postFinder;
@Test
void compare() {
List<String> strings = posts().stream().sorted(PostFinderImpl.defaultComparator())
.map(post -> post.getMetadata().getName())
.toList();
assertThat(strings).isEqualTo(
List.of("post-6", "post-2", "post-1", "post-5", "post-4", "post-3"));
}
@Test @Test
void predicate() { void predicate() {
Predicate<Post> predicate = new DefaultQueryPostPredicateResolver().getPredicate().block(); Predicate<Post> predicate = new DefaultQueryPostPredicateResolver().getPredicate().block();
@ -93,7 +82,7 @@ class PostFinderImplTest {
.map(ListedPostVo::from) .map(ListedPostVo::from)
.toList(); .toList();
ListResult<ListedPostVo> listResult = new ListResult<>(1, 10, 3, listedPostVos); ListResult<ListedPostVo> listResult = new ListResult<>(1, 10, 3, listedPostVos);
when(publicQueryService.list(anyInt(), anyInt(), any(), any())) when(publicQueryService.list(any(), any(PageRequest.class)))
.thenReturn(Mono.just(listResult)); .thenReturn(Mono.just(listResult));
ListResult<PostArchiveVo> archives = postFinder.archives(1, 10).block(); ListResult<PostArchiveVo> archives = postFinder.archives(1, 10).block();
@ -112,22 +101,6 @@ class PostFinderImplTest {
assertThat(items.get(1).getMonths().get(0).getMonth()).isEqualTo("01"); assertThat(items.get(1).getMonths().get(0).getMonth()).isEqualTo("01");
} }
@Test
void fixedSizeSlidingWindow() {
PostFinderImpl.FixedSizeSlidingWindow<Integer>
window = new PostFinderImpl.FixedSizeSlidingWindow<>(3);
List<String> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
window.add(i);
list.add(Strings.join(window.elements(), ','));
}
assertThat(list).isEqualTo(
List.of("0", "0,1", "0,1,2", "1,2,3", "2,3,4", "3,4,5", "4,5,6", "5,6,7", "6,7,8",
"7,8,9")
);
}
@Test @Test
void postPreviousNextPair() { void postPreviousNextPair() {
List<String> postNames = new ArrayList<>(); List<String> postNames = new ArrayList<>();
@ -136,28 +109,27 @@ class PostFinderImplTest {
} }
// post-0, post-1, post-2 // post-0, post-1, post-2
Pair<String, String> previousNextPair = var previousNextPair = PostFinderImpl.findPostNavigation(postNames, "post-0");
PostFinderImpl.postPreviousNextPair(postNames, "post-0"); assertThat(previousNextPair.prev()).isNull();
assertThat(previousNextPair.getLeft()).isNull(); assertThat(previousNextPair.next()).isEqualTo("post-1");
assertThat(previousNextPair.getRight()).isEqualTo("post-1");
previousNextPair = PostFinderImpl.postPreviousNextPair(postNames, "post-1"); previousNextPair = PostFinderImpl.findPostNavigation(postNames, "post-1");
assertThat(previousNextPair.getLeft()).isEqualTo("post-0"); assertThat(previousNextPair.prev()).isEqualTo("post-0");
assertThat(previousNextPair.getRight()).isEqualTo("post-2"); assertThat(previousNextPair.next()).isEqualTo("post-2");
// post-1, post-2, post-3 // post-1, post-2, post-3
previousNextPair = PostFinderImpl.postPreviousNextPair(postNames, "post-2"); previousNextPair = PostFinderImpl.findPostNavigation(postNames, "post-2");
assertThat(previousNextPair.getLeft()).isEqualTo("post-1"); assertThat(previousNextPair.prev()).isEqualTo("post-1");
assertThat(previousNextPair.getRight()).isEqualTo("post-3"); assertThat(previousNextPair.next()).isEqualTo("post-3");
// post-7, post-8, post-9 // post-7, post-8, post-9
previousNextPair = PostFinderImpl.postPreviousNextPair(postNames, "post-8"); previousNextPair = PostFinderImpl.findPostNavigation(postNames, "post-8");
assertThat(previousNextPair.getLeft()).isEqualTo("post-7"); assertThat(previousNextPair.prev()).isEqualTo("post-7");
assertThat(previousNextPair.getRight()).isEqualTo("post-9"); assertThat(previousNextPair.next()).isEqualTo("post-9");
previousNextPair = PostFinderImpl.postPreviousNextPair(postNames, "post-9"); previousNextPair = PostFinderImpl.findPostNavigation(postNames, "post-9");
assertThat(previousNextPair.getLeft()).isEqualTo("post-8"); assertThat(previousNextPair.prev()).isEqualTo("post-8");
assertThat(previousNextPair.getRight()).isNull(); assertThat(previousNextPair.next()).isNull();
} }
List<Post> postsForArchives() { List<Post> postsForArchives() {

View File

@ -16,9 +16,11 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import org.skyscreamer.jsonassert.JSONAssert; import org.skyscreamer.jsonassert.JSONAssert;
import org.springframework.data.domain.Sort;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import run.halo.app.core.extension.content.Tag; import run.halo.app.core.extension.content.Tag;
import run.halo.app.extension.ListOptions;
import run.halo.app.extension.Metadata; import run.halo.app.extension.Metadata;
import run.halo.app.extension.ReactiveExtensionClient; import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.infra.utils.JsonUtils; import run.halo.app.infra.utils.JsonUtils;
@ -77,7 +79,7 @@ class TagFinderImplTest {
@Test @Test
void listAll() { void listAll() {
when(client.list(eq(Tag.class), eq(null), any())) when(client.listAll(eq(Tag.class), any(ListOptions.class), any(Sort.class)))
.thenReturn(Flux.fromIterable( .thenReturn(Flux.fromIterable(
tags().stream().sorted(TagFinderImpl.DEFAULT_COMPARATOR.reversed()).toList() tags().stream().sorted(TagFinderImpl.DEFAULT_COMPARATOR.reversed()).toList()
) )

View File

@ -4,16 +4,18 @@ 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;
import java.util.List;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks; import org.mockito.InjectMocks;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.test.web.reactive.server.WebTestClient;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import run.halo.app.core.extension.content.Tag; import run.halo.app.core.extension.content.Tag;
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.theme.finders.PostFinder; import run.halo.app.theme.finders.PostFinder;
import run.halo.app.theme.finders.TagFinder; import run.halo.app.theme.finders.TagFinder;
@ -39,7 +41,8 @@ class TagPostRouteFactoryTest extends RouteFactoryTestSuite {
@Test @Test
void create() { void create() {
when(client.list(eq(Tag.class), any(), eq(null))).thenReturn(Flux.empty()); when(client.listBy(eq(Tag.class), any(), any(PageRequest.class)))
.thenReturn(Mono.just(ListResult.emptyResult()));
WebTestClient webTestClient = getWebTestClient(tagPostRouteFactory.create("/new-tags")); WebTestClient webTestClient = getWebTestClient(tagPostRouteFactory.create("/new-tags"));
webTestClient.get() webTestClient.get()
@ -52,7 +55,8 @@ class TagPostRouteFactoryTest extends RouteFactoryTestSuite {
tag.getMetadata().setName("fake-tag-name"); tag.getMetadata().setName("fake-tag-name");
tag.setSpec(new Tag.TagSpec()); tag.setSpec(new Tag.TagSpec());
tag.getSpec().setSlug("tag-slug-2"); tag.getSpec().setSlug("tag-slug-2");
when(client.list(eq(Tag.class), any(), eq(null))).thenReturn(Flux.just(tag)); when(client.listBy(eq(Tag.class), any(), any(PageRequest.class)))
.thenReturn(Mono.just(new ListResult<>(List.of(tag))));
when(tagFinder.getByName(eq(tag.getMetadata().getName()))) when(tagFinder.getByName(eq(tag.getMetadata().getName())))
.thenReturn(Mono.just(TagVo.from(tag))); .thenReturn(Mono.just(TagVo.from(tag)));
webTestClient.get() webTestClient.get()

View File

@ -59,11 +59,11 @@ const {
return data.items; return data.items;
}, },
refetchInterval: (data) => { refetchInterval: (data) => {
const deletingPosts = data?.filter( const deletingPosts = data?.some(
(post) => (post) =>
!!post.post.metadata.deletionTimestamp || !post.post.spec.deleted !!post.post.metadata.deletionTimestamp || !post.post.spec.deleted
); );
return deletingPosts?.length ? 1000 : false; return deletingPosts ? 1000 : false;
}, },
}); });

View File

@ -113,21 +113,23 @@ const {
keyword, keyword,
], ],
queryFn: async () => { queryFn: async () => {
let categories: string[] | undefined;
let tags: string[] | undefined;
let contributors: string[] | undefined;
const labelSelector: string[] = ["content.halo.run/deleted=false"]; const labelSelector: string[] = ["content.halo.run/deleted=false"];
const fieldSelector: string[] = [];
if (selectedCategory.value) { if (selectedCategory.value) {
categories = [selectedCategory.value]; fieldSelector.push(`spec.categories=${selectedCategory.value}`);
} }
if (selectedTag.value) { if (selectedTag.value) {
tags = [selectedTag.value]; fieldSelector.push(`spec.tags=${selectedTag.value}`);
} }
if (selectedContributor.value) { if (selectedContributor.value) {
contributors = [selectedContributor.value]; fieldSelector.push(`status.contributors=${selectedContributor.value}`);
}
if (selectedVisible.value) {
fieldSelector.push(`spec.visible=${selectedVisible.value}`);
} }
if (selectedPublishStatus.value !== undefined) { if (selectedPublishStatus.value !== undefined) {
@ -138,14 +140,11 @@ const {
const { data } = await apiClient.post.listPosts({ const { data } = await apiClient.post.listPosts({
labelSelector, labelSelector,
fieldSelector,
page: page.value, page: page.value,
size: size.value, size: size.value,
visible: selectedVisible.value,
sort: [selectedSort.value].filter(Boolean) as string[], sort: [selectedSort.value].filter(Boolean) as string[],
keyword: keyword.value, keyword: keyword.value,
category: categories,
tag: tags,
contributor: contributors,
}); });
total.value = data.total; total.value = data.total;
@ -155,7 +154,7 @@ const {
return data.items; return data.items;
}, },
refetchInterval: (data) => { refetchInterval: (data) => {
const abnormalPosts = data?.filter((post) => { const abnormalPosts = data?.some((post) => {
const { spec, metadata, status } = post.post; const { spec, metadata, status } = post.post;
return ( return (
spec.deleted || spec.deleted ||
@ -164,7 +163,7 @@ const {
); );
}); });
return abnormalPosts?.length ? 1000 : false; return abnormalPosts ? 1000 : false;
}, },
}); });
@ -407,19 +406,19 @@ watch(selectedPostNames, (newValue) => {
}, },
{ {
label: t('core.post.filters.sort.items.publish_time_desc'), label: t('core.post.filters.sort.items.publish_time_desc'),
value: 'publishTime,desc', value: 'spec.publishTime,desc',
}, },
{ {
label: t('core.post.filters.sort.items.publish_time_asc'), label: t('core.post.filters.sort.items.publish_time_asc'),
value: 'publishTime,asc', value: 'spec.publishTime,asc',
}, },
{ {
label: t('core.post.filters.sort.items.create_time_desc'), label: t('core.post.filters.sort.items.create_time_desc'),
value: 'creationTimestamp,desc', value: 'metadata.creationTimestamp,desc',
}, },
{ {
label: t('core.post.filters.sort.items.create_time_asc'), label: t('core.post.filters.sort.items.create_time_asc'),
value: 'creationTimestamp,asc', value: 'metadata.creationTimestamp,asc',
}, },
]" ]"
/> />

View File

@ -32,6 +32,7 @@ export function usePostCategory(): usePostCategoryReturn {
await apiClient.extension.category.listcontentHaloRunV1alpha1Category({ await apiClient.extension.category.listcontentHaloRunV1alpha1Category({
page: 0, page: 0,
size: 0, size: 0,
sort: ["metadata.creationTimestamp,desc"],
}); });
return data.items; return data.items;

View File

@ -26,6 +26,7 @@ export function usePostTag(): usePostTagReturn {
await apiClient.extension.tag.listcontentHaloRunV1alpha1Tag({ await apiClient.extension.tag.listcontentHaloRunV1alpha1Tag({
page: 0, page: 0,
size: 0, size: 0,
sort: ["metadata.creationTimestamp,desc"],
}); });
return data.items; return data.items;

View File

@ -21,7 +21,7 @@ const { data } = useQuery<ListedPost[]>({
`${postLabels.DELETED}=false`, `${postLabels.DELETED}=false`,
`${postLabels.PUBLISHED}=true`, `${postLabels.PUBLISHED}=true`,
], ],
sort: ["publishTime,desc"], sort: ["spec.publishTime,desc"],
page: 1, page: 1,
size: 10, size: 10,
}); });

View File

@ -222,8 +222,6 @@ export const ApiConsoleHaloRunV1alpha1PostApiAxiosParamCreator = function (
}, },
/** /**
* List posts. * List posts.
* @param {Array<string>} [category]
* @param {Array<string>} [contributor]
* @param {Array<string>} [fieldSelector] Field selector for filtering. * @param {Array<string>} [fieldSelector] Field selector for filtering.
* @param {string} [keyword] Posts filtered by keyword. * @param {string} [keyword] Posts filtered by keyword.
* @param {Array<string>} [labelSelector] Label selector for filtering. * @param {Array<string>} [labelSelector] Label selector for filtering.
@ -231,14 +229,10 @@ export const ApiConsoleHaloRunV1alpha1PostApiAxiosParamCreator = function (
* @param {'DRAFT' | 'PENDING_APPROVAL' | 'PUBLISHED' | 'FAILED'} [publishPhase] * @param {'DRAFT' | 'PENDING_APPROVAL' | 'PUBLISHED' | 'FAILED'} [publishPhase]
* @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,publishTime * @param {Array<string>} [sort] Sort property and direction of the list result. Supported fields: creationTimestamp,publishTime
* @param {Array<string>} [tag]
* @param {'PUBLIC' | 'INTERNAL' | 'PRIVATE'} [visible]
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
listPosts: async ( listPosts: async (
category?: Array<string>,
contributor?: Array<string>,
fieldSelector?: Array<string>, fieldSelector?: Array<string>,
keyword?: string, keyword?: string,
labelSelector?: Array<string>, labelSelector?: Array<string>,
@ -246,8 +240,6 @@ export const ApiConsoleHaloRunV1alpha1PostApiAxiosParamCreator = function (
publishPhase?: "DRAFT" | "PENDING_APPROVAL" | "PUBLISHED" | "FAILED", publishPhase?: "DRAFT" | "PENDING_APPROVAL" | "PUBLISHED" | "FAILED",
size?: number, size?: number,
sort?: Array<string>, sort?: Array<string>,
tag?: Array<string>,
visible?: "PUBLIC" | "INTERNAL" | "PRIVATE",
options: AxiosRequestConfig = {} options: AxiosRequestConfig = {}
): Promise<RequestArgs> => { ): Promise<RequestArgs> => {
const localVarPath = `/apis/api.console.halo.run/v1alpha1/posts`; const localVarPath = `/apis/api.console.halo.run/v1alpha1/posts`;
@ -274,14 +266,6 @@ export const ApiConsoleHaloRunV1alpha1PostApiAxiosParamCreator = function (
// http bearer authentication required // http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration); await setBearerAuthToObject(localVarHeaderParameter, configuration);
if (category) {
localVarQueryParameter["category"] = Array.from(category);
}
if (contributor) {
localVarQueryParameter["contributor"] = Array.from(contributor);
}
if (fieldSelector) { if (fieldSelector) {
localVarQueryParameter["fieldSelector"] = fieldSelector; localVarQueryParameter["fieldSelector"] = fieldSelector;
} }
@ -310,14 +294,6 @@ export const ApiConsoleHaloRunV1alpha1PostApiAxiosParamCreator = function (
localVarQueryParameter["sort"] = Array.from(sort); localVarQueryParameter["sort"] = Array.from(sort);
} }
if (tag) {
localVarQueryParameter["tag"] = Array.from(tag);
}
if (visible !== undefined) {
localVarQueryParameter["visible"] = visible;
}
setSearchParams(localVarUrlObj, localVarQueryParameter); setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = let headersFromBaseOptions =
baseOptions && baseOptions.headers ? baseOptions.headers : {}; baseOptions && baseOptions.headers ? baseOptions.headers : {};
@ -710,8 +686,6 @@ export const ApiConsoleHaloRunV1alpha1PostApiFp = function (
}, },
/** /**
* List posts. * List posts.
* @param {Array<string>} [category]
* @param {Array<string>} [contributor]
* @param {Array<string>} [fieldSelector] Field selector for filtering. * @param {Array<string>} [fieldSelector] Field selector for filtering.
* @param {string} [keyword] Posts filtered by keyword. * @param {string} [keyword] Posts filtered by keyword.
* @param {Array<string>} [labelSelector] Label selector for filtering. * @param {Array<string>} [labelSelector] Label selector for filtering.
@ -719,14 +693,10 @@ export const ApiConsoleHaloRunV1alpha1PostApiFp = function (
* @param {'DRAFT' | 'PENDING_APPROVAL' | 'PUBLISHED' | 'FAILED'} [publishPhase] * @param {'DRAFT' | 'PENDING_APPROVAL' | 'PUBLISHED' | 'FAILED'} [publishPhase]
* @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,publishTime * @param {Array<string>} [sort] Sort property and direction of the list result. Supported fields: creationTimestamp,publishTime
* @param {Array<string>} [tag]
* @param {'PUBLIC' | 'INTERNAL' | 'PRIVATE'} [visible]
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
async listPosts( async listPosts(
category?: Array<string>,
contributor?: Array<string>,
fieldSelector?: Array<string>, fieldSelector?: Array<string>,
keyword?: string, keyword?: string,
labelSelector?: Array<string>, labelSelector?: Array<string>,
@ -734,15 +704,11 @@ export const ApiConsoleHaloRunV1alpha1PostApiFp = function (
publishPhase?: "DRAFT" | "PENDING_APPROVAL" | "PUBLISHED" | "FAILED", publishPhase?: "DRAFT" | "PENDING_APPROVAL" | "PUBLISHED" | "FAILED",
size?: number, size?: number,
sort?: Array<string>, sort?: Array<string>,
tag?: Array<string>,
visible?: "PUBLIC" | "INTERNAL" | "PRIVATE",
options?: AxiosRequestConfig options?: AxiosRequestConfig
): Promise< ): Promise<
(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ListedPostList> (axios?: AxiosInstance, basePath?: string) => AxiosPromise<ListedPostList>
> { > {
const localVarAxiosArgs = await localVarAxiosParamCreator.listPosts( const localVarAxiosArgs = await localVarAxiosParamCreator.listPosts(
category,
contributor,
fieldSelector, fieldSelector,
keyword, keyword,
labelSelector, labelSelector,
@ -750,8 +716,6 @@ export const ApiConsoleHaloRunV1alpha1PostApiFp = function (
publishPhase, publishPhase,
size, size,
sort, sort,
tag,
visible,
options options
); );
return createRequestFunction( return createRequestFunction(
@ -954,8 +918,6 @@ export const ApiConsoleHaloRunV1alpha1PostApiFactory = function (
): AxiosPromise<ListedPostList> { ): AxiosPromise<ListedPostList> {
return localVarFp return localVarFp
.listPosts( .listPosts(
requestParameters.category,
requestParameters.contributor,
requestParameters.fieldSelector, requestParameters.fieldSelector,
requestParameters.keyword, requestParameters.keyword,
requestParameters.labelSelector, requestParameters.labelSelector,
@ -963,8 +925,6 @@ export const ApiConsoleHaloRunV1alpha1PostApiFactory = function (
requestParameters.publishPhase, requestParameters.publishPhase,
requestParameters.size, requestParameters.size,
requestParameters.sort, requestParameters.sort,
requestParameters.tag,
requestParameters.visible,
options options
) )
.then((request) => request(axios, basePath)); .then((request) => request(axios, basePath));
@ -1102,20 +1062,6 @@ export interface ApiConsoleHaloRunV1alpha1PostApiFetchPostReleaseContentRequest
* @interface ApiConsoleHaloRunV1alpha1PostApiListPostsRequest * @interface ApiConsoleHaloRunV1alpha1PostApiListPostsRequest
*/ */
export interface ApiConsoleHaloRunV1alpha1PostApiListPostsRequest { export interface ApiConsoleHaloRunV1alpha1PostApiListPostsRequest {
/**
*
* @type {Array<string>}
* @memberof ApiConsoleHaloRunV1alpha1PostApiListPosts
*/
readonly category?: Array<string>;
/**
*
* @type {Array<string>}
* @memberof ApiConsoleHaloRunV1alpha1PostApiListPosts
*/
readonly contributor?: Array<string>;
/** /**
* Field selector for filtering. * Field selector for filtering.
* @type {Array<string>} * @type {Array<string>}
@ -1164,20 +1110,6 @@ export interface ApiConsoleHaloRunV1alpha1PostApiListPostsRequest {
* @memberof ApiConsoleHaloRunV1alpha1PostApiListPosts * @memberof ApiConsoleHaloRunV1alpha1PostApiListPosts
*/ */
readonly sort?: Array<string>; readonly sort?: Array<string>;
/**
*
* @type {Array<string>}
* @memberof ApiConsoleHaloRunV1alpha1PostApiListPosts
*/
readonly tag?: Array<string>;
/**
*
* @type {'PUBLIC' | 'INTERNAL' | 'PRIVATE'}
* @memberof ApiConsoleHaloRunV1alpha1PostApiListPosts
*/
readonly visible?: "PUBLIC" | "INTERNAL" | "PRIVATE";
} }
/** /**
@ -1339,8 +1271,6 @@ export class ApiConsoleHaloRunV1alpha1PostApi extends BaseAPI {
) { ) {
return ApiConsoleHaloRunV1alpha1PostApiFp(this.configuration) return ApiConsoleHaloRunV1alpha1PostApiFp(this.configuration)
.listPosts( .listPosts(
requestParameters.category,
requestParameters.contributor,
requestParameters.fieldSelector, requestParameters.fieldSelector,
requestParameters.keyword, requestParameters.keyword,
requestParameters.labelSelector, requestParameters.labelSelector,
@ -1348,8 +1278,6 @@ export class ApiConsoleHaloRunV1alpha1PostApi extends BaseAPI {
requestParameters.publishPhase, requestParameters.publishPhase,
requestParameters.size, requestParameters.size,
requestParameters.sort, requestParameters.sort,
requestParameters.tag,
requestParameters.visible,
options options
) )
.then((request) => request(this.axios, this.basePath)); .then((request) => request(this.axios, this.basePath));

View File

@ -222,8 +222,6 @@ export const UcApiContentHaloRunV1alpha1PostApiAxiosParamCreator = function (
}, },
/** /**
* List posts owned by the current user. * List posts owned by the current user.
* @param {Array<string>} [category]
* @param {Array<string>} [contributor]
* @param {Array<string>} [fieldSelector] Field selector for filtering. * @param {Array<string>} [fieldSelector] Field selector for filtering.
* @param {string} [keyword] Posts filtered by keyword. * @param {string} [keyword] Posts filtered by keyword.
* @param {Array<string>} [labelSelector] Label selector for filtering. * @param {Array<string>} [labelSelector] Label selector for filtering.
@ -231,14 +229,10 @@ export const UcApiContentHaloRunV1alpha1PostApiAxiosParamCreator = function (
* @param {'DRAFT' | 'PENDING_APPROVAL' | 'PUBLISHED' | 'FAILED'} [publishPhase] * @param {'DRAFT' | 'PENDING_APPROVAL' | 'PUBLISHED' | 'FAILED'} [publishPhase]
* @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,publishTime * @param {Array<string>} [sort] Sort property and direction of the list result. Supported fields: creationTimestamp,publishTime
* @param {Array<string>} [tag]
* @param {'PUBLIC' | 'INTERNAL' | 'PRIVATE'} [visible]
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
listMyPosts: async ( listMyPosts: async (
category?: Array<string>,
contributor?: Array<string>,
fieldSelector?: Array<string>, fieldSelector?: Array<string>,
keyword?: string, keyword?: string,
labelSelector?: Array<string>, labelSelector?: Array<string>,
@ -246,8 +240,6 @@ export const UcApiContentHaloRunV1alpha1PostApiAxiosParamCreator = function (
publishPhase?: "DRAFT" | "PENDING_APPROVAL" | "PUBLISHED" | "FAILED", publishPhase?: "DRAFT" | "PENDING_APPROVAL" | "PUBLISHED" | "FAILED",
size?: number, size?: number,
sort?: Array<string>, sort?: Array<string>,
tag?: Array<string>,
visible?: "PUBLIC" | "INTERNAL" | "PRIVATE",
options: AxiosRequestConfig = {} options: AxiosRequestConfig = {}
): Promise<RequestArgs> => { ): Promise<RequestArgs> => {
const localVarPath = `/apis/uc.api.content.halo.run/v1alpha1/posts`; const localVarPath = `/apis/uc.api.content.halo.run/v1alpha1/posts`;
@ -274,14 +266,6 @@ export const UcApiContentHaloRunV1alpha1PostApiAxiosParamCreator = function (
// http bearer authentication required // http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration); await setBearerAuthToObject(localVarHeaderParameter, configuration);
if (category) {
localVarQueryParameter["category"] = Array.from(category);
}
if (contributor) {
localVarQueryParameter["contributor"] = Array.from(contributor);
}
if (fieldSelector) { if (fieldSelector) {
localVarQueryParameter["fieldSelector"] = fieldSelector; localVarQueryParameter["fieldSelector"] = fieldSelector;
} }
@ -310,14 +294,6 @@ export const UcApiContentHaloRunV1alpha1PostApiAxiosParamCreator = function (
localVarQueryParameter["sort"] = Array.from(sort); localVarQueryParameter["sort"] = Array.from(sort);
} }
if (tag) {
localVarQueryParameter["tag"] = Array.from(tag);
}
if (visible !== undefined) {
localVarQueryParameter["visible"] = visible;
}
setSearchParams(localVarUrlObj, localVarQueryParameter); setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = let headersFromBaseOptions =
baseOptions && baseOptions.headers ? baseOptions.headers : {}; baseOptions && baseOptions.headers ? baseOptions.headers : {};
@ -653,8 +629,6 @@ export const UcApiContentHaloRunV1alpha1PostApiFp = function (
}, },
/** /**
* List posts owned by the current user. * List posts owned by the current user.
* @param {Array<string>} [category]
* @param {Array<string>} [contributor]
* @param {Array<string>} [fieldSelector] Field selector for filtering. * @param {Array<string>} [fieldSelector] Field selector for filtering.
* @param {string} [keyword] Posts filtered by keyword. * @param {string} [keyword] Posts filtered by keyword.
* @param {Array<string>} [labelSelector] Label selector for filtering. * @param {Array<string>} [labelSelector] Label selector for filtering.
@ -662,14 +636,10 @@ export const UcApiContentHaloRunV1alpha1PostApiFp = function (
* @param {'DRAFT' | 'PENDING_APPROVAL' | 'PUBLISHED' | 'FAILED'} [publishPhase] * @param {'DRAFT' | 'PENDING_APPROVAL' | 'PUBLISHED' | 'FAILED'} [publishPhase]
* @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,publishTime * @param {Array<string>} [sort] Sort property and direction of the list result. Supported fields: creationTimestamp,publishTime
* @param {Array<string>} [tag]
* @param {'PUBLIC' | 'INTERNAL' | 'PRIVATE'} [visible]
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
async listMyPosts( async listMyPosts(
category?: Array<string>,
contributor?: Array<string>,
fieldSelector?: Array<string>, fieldSelector?: Array<string>,
keyword?: string, keyword?: string,
labelSelector?: Array<string>, labelSelector?: Array<string>,
@ -677,15 +647,11 @@ export const UcApiContentHaloRunV1alpha1PostApiFp = function (
publishPhase?: "DRAFT" | "PENDING_APPROVAL" | "PUBLISHED" | "FAILED", publishPhase?: "DRAFT" | "PENDING_APPROVAL" | "PUBLISHED" | "FAILED",
size?: number, size?: number,
sort?: Array<string>, sort?: Array<string>,
tag?: Array<string>,
visible?: "PUBLIC" | "INTERNAL" | "PRIVATE",
options?: AxiosRequestConfig options?: AxiosRequestConfig
): Promise< ): Promise<
(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ListedPostList> (axios?: AxiosInstance, basePath?: string) => AxiosPromise<ListedPostList>
> { > {
const localVarAxiosArgs = await localVarAxiosParamCreator.listMyPosts( const localVarAxiosArgs = await localVarAxiosParamCreator.listMyPosts(
category,
contributor,
fieldSelector, fieldSelector,
keyword, keyword,
labelSelector, labelSelector,
@ -693,8 +659,6 @@ export const UcApiContentHaloRunV1alpha1PostApiFp = function (
publishPhase, publishPhase,
size, size,
sort, sort,
tag,
visible,
options options
); );
return createRequestFunction( return createRequestFunction(
@ -875,8 +839,6 @@ export const UcApiContentHaloRunV1alpha1PostApiFactory = function (
): AxiosPromise<ListedPostList> { ): AxiosPromise<ListedPostList> {
return localVarFp return localVarFp
.listMyPosts( .listMyPosts(
requestParameters.category,
requestParameters.contributor,
requestParameters.fieldSelector, requestParameters.fieldSelector,
requestParameters.keyword, requestParameters.keyword,
requestParameters.labelSelector, requestParameters.labelSelector,
@ -884,8 +846,6 @@ export const UcApiContentHaloRunV1alpha1PostApiFactory = function (
requestParameters.publishPhase, requestParameters.publishPhase,
requestParameters.size, requestParameters.size,
requestParameters.sort, requestParameters.sort,
requestParameters.tag,
requestParameters.visible,
options options
) )
.then((request) => request(axios, basePath)); .then((request) => request(axios, basePath));
@ -1008,20 +968,6 @@ export interface UcApiContentHaloRunV1alpha1PostApiGetMyPostDraftRequest {
* @interface UcApiContentHaloRunV1alpha1PostApiListMyPostsRequest * @interface UcApiContentHaloRunV1alpha1PostApiListMyPostsRequest
*/ */
export interface UcApiContentHaloRunV1alpha1PostApiListMyPostsRequest { export interface UcApiContentHaloRunV1alpha1PostApiListMyPostsRequest {
/**
*
* @type {Array<string>}
* @memberof UcApiContentHaloRunV1alpha1PostApiListMyPosts
*/
readonly category?: Array<string>;
/**
*
* @type {Array<string>}
* @memberof UcApiContentHaloRunV1alpha1PostApiListMyPosts
*/
readonly contributor?: Array<string>;
/** /**
* Field selector for filtering. * Field selector for filtering.
* @type {Array<string>} * @type {Array<string>}
@ -1070,20 +1016,6 @@ export interface UcApiContentHaloRunV1alpha1PostApiListMyPostsRequest {
* @memberof UcApiContentHaloRunV1alpha1PostApiListMyPosts * @memberof UcApiContentHaloRunV1alpha1PostApiListMyPosts
*/ */
readonly sort?: Array<string>; readonly sort?: Array<string>;
/**
*
* @type {Array<string>}
* @memberof UcApiContentHaloRunV1alpha1PostApiListMyPosts
*/
readonly tag?: Array<string>;
/**
*
* @type {'PUBLIC' | 'INTERNAL' | 'PRIVATE'}
* @memberof UcApiContentHaloRunV1alpha1PostApiListMyPosts
*/
readonly visible?: "PUBLIC" | "INTERNAL" | "PRIVATE";
} }
/** /**
@ -1228,8 +1160,6 @@ export class UcApiContentHaloRunV1alpha1PostApi extends BaseAPI {
) { ) {
return UcApiContentHaloRunV1alpha1PostApiFp(this.configuration) return UcApiContentHaloRunV1alpha1PostApiFp(this.configuration)
.listMyPosts( .listMyPosts(
requestParameters.category,
requestParameters.contributor,
requestParameters.fieldSelector, requestParameters.fieldSelector,
requestParameters.keyword, requestParameters.keyword,
requestParameters.labelSelector, requestParameters.labelSelector,
@ -1237,8 +1167,6 @@ export class UcApiContentHaloRunV1alpha1PostApi extends BaseAPI {
requestParameters.publishPhase, requestParameters.publishPhase,
requestParameters.size, requestParameters.size,
requestParameters.sort, requestParameters.sort,
requestParameters.tag,
requestParameters.visible,
options options
) )
.then((request) => request(this.axios, this.basePath)); .then((request) => request(this.axios, this.basePath));

View File

@ -34,6 +34,12 @@ export interface PluginStatus {
* @memberof PluginStatus * @memberof PluginStatus
*/ */
entry?: string; entry?: string;
/**
*
* @type {string}
* @memberof PluginStatus
*/
lastProbeState?: PluginStatusLastProbeStateEnum;
/** /**
* *
* @type {string} * @type {string}
@ -66,7 +72,7 @@ export interface PluginStatus {
stylesheet?: string; stylesheet?: string;
} }
export const PluginStatusPhaseEnum = { export const PluginStatusLastProbeStateEnum = {
Created: "CREATED", Created: "CREATED",
Disabled: "DISABLED", Disabled: "DISABLED",
Resolved: "RESOLVED", Resolved: "RESOLVED",
@ -75,5 +81,19 @@ export const PluginStatusPhaseEnum = {
Failed: "FAILED", Failed: "FAILED",
} as const; } as const;
export type PluginStatusLastProbeStateEnum =
(typeof PluginStatusLastProbeStateEnum)[keyof typeof PluginStatusLastProbeStateEnum];
export const PluginStatusPhaseEnum = {
Pending: "PENDING",
Starting: "STARTING",
Created: "CREATED",
Disabled: "DISABLED",
Resolved: "RESOLVED",
Started: "STARTED",
Stopped: "STOPPED",
Failed: "FAILED",
Unknown: "UNKNOWN",
} as const;
export type PluginStatusPhaseEnum = export type PluginStatusPhaseEnum =
(typeof PluginStatusPhaseEnum)[keyof typeof PluginStatusPhaseEnum]; (typeof PluginStatusPhaseEnum)[keyof typeof PluginStatusPhaseEnum];

View File

@ -149,7 +149,9 @@ const handleBuildSearchIndex = () => {
}); });
apiClient.extension.category apiClient.extension.category
.listcontentHaloRunV1alpha1Category() .listcontentHaloRunV1alpha1Category({
sort: ["metadata.creationTimestamp,desc"],
})
.then((response) => { .then((response) => {
response.data.items.forEach((category) => { response.data.items.forEach((category) => {
fuse.add({ fuse.add({
@ -168,23 +170,27 @@ const handleBuildSearchIndex = () => {
}); });
}); });
apiClient.extension.tag.listcontentHaloRunV1alpha1Tag().then((response) => { apiClient.extension.tag
response.data.items.forEach((tag) => { .listcontentHaloRunV1alpha1Tag({
fuse.add({ sort: ["metadata.creationTimestamp,desc"],
title: tag.spec.displayName, })
icon: { .then((response) => {
component: markRaw(IconBookRead), response.data.items.forEach((tag) => {
}, fuse.add({
group: t("core.components.global_search.groups.tag"), title: tag.spec.displayName,
route: { icon: {
name: "Tags", component: markRaw(IconBookRead),
query: {
name: tag.metadata.name,
}, },
}, group: t("core.components.global_search.groups.tag"),
route: {
name: "Tags",
query: {
name: tag.metadata.name,
},
},
});
}); });
}); });
});
} }
if (currentUserHasPermission(["system:singlepages:view"])) { if (currentUserHasPermission(["system:singlepages:view"])) {

View File

@ -15,7 +15,9 @@ declare module "@formkit/inputs" {
function optionsHandler(node: FormKitNode) { function optionsHandler(node: FormKitNode) {
node.on("created", async () => { node.on("created", async () => {
const { data } = const { data } =
await apiClient.extension.category.listcontentHaloRunV1alpha1Category(); await apiClient.extension.category.listcontentHaloRunV1alpha1Category({
sort: ["metadata.creationTimestamp,desc"],
});
node.props.options = data.items.map((category) => { node.props.options = data.items.map((category) => {
return { return {

View File

@ -15,7 +15,9 @@ declare module "@formkit/inputs" {
function optionsHandler(node: FormKitNode) { function optionsHandler(node: FormKitNode) {
node.on("created", async () => { node.on("created", async () => {
const { data } = const { data } =
await apiClient.extension.tag.listcontentHaloRunV1alpha1Tag(); await apiClient.extension.tag.listcontentHaloRunV1alpha1Tag({
sort: ["metadata.creationTimestamp,desc"],
});
node.props.options = data.items.map((tag) => { node.props.options = data.items.map((tag) => {
return { return {

View File

@ -69,7 +69,7 @@ const {
size.value = data.size; size.value = data.size;
}, },
refetchInterval: (data) => { refetchInterval: (data) => {
const abnormalPosts = data?.items.filter((post) => { const hasAbnormalPost = data?.items.some((post) => {
const { spec, metadata, status } = post.post; const { spec, metadata, status } = post.post;
return ( return (
spec.deleted || spec.deleted ||
@ -78,7 +78,7 @@ const {
); );
}); });
return abnormalPosts?.length ? 1000 : false; return hasAbnormalPost ? 1000 : false;
}, },
}); });
</script> </script>