mirror of https://github.com/halo-dev/halo
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
parent
cecdb3f9ef
commit
3f27f6f262
|
@ -84,4 +84,7 @@ public class Snapshot extends AbstractExtension {
|
|||
return Boolean.parseBoolean(annotations.get(Snapshot.KEEP_RAW_ANNO));
|
||||
}
|
||||
|
||||
public static String toSubjectRefKey(Ref subjectRef) {
|
||||
return subjectRef.getGroup() + "/" + subjectRef.getKind() + "/" + subjectRef.getName();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import java.util.ArrayList;
|
|||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Stream;
|
||||
import lombok.Data;
|
||||
|
@ -144,6 +145,15 @@ public class ListResult<T> implements Iterable<T>, Supplier<Stream<T>> {
|
|||
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
|
||||
public Stream<T> get() {
|
||||
return items.stream();
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
package run.halo.app.extension.router.selector;
|
||||
|
||||
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.QueryFactory;
|
||||
|
||||
public record FieldSelector(Query query) {
|
||||
public record FieldSelector(@NonNull Query query) {
|
||||
public FieldSelector(Query query) {
|
||||
this.query = Objects.requireNonNullElseGet(query, QueryFactory::all);
|
||||
}
|
||||
|
@ -12,4 +14,13 @@ public record FieldSelector(Query query) {
|
|||
public static FieldSelector of(Query 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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,6 +24,22 @@ public class LabelSelector implements Predicate<Map<String, String>> {
|
|||
.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() {
|
||||
return new LabelSelectorBuilder();
|
||||
}
|
||||
|
|
|
@ -96,6 +96,8 @@ public final class SelectorUtil {
|
|||
listOptions.setLabelSelector(new LabelSelector().setMatchers(labelMatchers));
|
||||
if (!fieldQuery.isEmpty()) {
|
||||
listOptions.setFieldSelector(FieldSelector.of(QueryFactory.and(fieldQuery)));
|
||||
} else {
|
||||
listOptions.setFieldSelector(FieldSelector.all());
|
||||
}
|
||||
return listOptions;
|
||||
}
|
||||
|
|
|
@ -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">informer机制之cache.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();
|
||||
}
|
||||
}
|
|
@ -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<Post> indexer = new Indexer<>();
|
||||
* indexer.addIndexFunc("category", post -> {
|
||||
* List<String> 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<Person> 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);
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -1,28 +1,23 @@
|
|||
package run.halo.app.content;
|
||||
|
||||
import static java.util.Comparator.comparing;
|
||||
import static run.halo.app.extension.router.selector.SelectorUtil.labelAndFieldSelectorToPredicate;
|
||||
import static run.halo.app.extension.router.selector.SelectorUtil.labelAndFieldSelectorToListOptions;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
import org.apache.commons.lang3.BooleanUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import run.halo.app.core.extension.content.Post;
|
||||
import run.halo.app.core.extension.endpoint.SortResolver;
|
||||
import run.halo.app.extension.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.selector.FieldSelector;
|
||||
import run.halo.app.extension.router.selector.LabelSelector;
|
||||
|
||||
/**
|
||||
* A query object for {@link Post} list.
|
||||
|
@ -52,36 +47,12 @@ public class PostQuery extends IListRequest.QueryListRequest {
|
|||
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
|
||||
public Post.PostPhase getPublishPhase() {
|
||||
String publishPhase = queryParams.getFirst("publishPhase");
|
||||
return Post.PostPhase.from(publishPhase);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Post.VisibleEnum getVisible() {
|
||||
String visible = queryParams.getFirst("visible");
|
||||
return Post.VisibleEnum.from(visible);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Schema(description = "Posts filtered by keyword.")
|
||||
public String getKeyword() {
|
||||
|
@ -96,127 +67,58 @@ public class PostQuery extends IListRequest.QueryListRequest {
|
|||
implementation = String.class,
|
||||
example = "creationTimestamp,desc"))
|
||||
public Sort getSort() {
|
||||
return SortResolver.defaultInstance.resolve(exchange);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Set<String> listToSet(List<String> param) {
|
||||
return param == null ? null : Set.copyOf(param);
|
||||
var sort = SortResolver.defaultInstance.resolve(exchange);
|
||||
sort = sort.and(Sort.by(Sort.Direction.DESC, "metadata.creationTimestamp"));
|
||||
sort = sort.and(Sort.by(Sort.Direction.DESC, "metadata.name"));
|
||||
return sort;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a comparator from the query object.
|
||||
* Build a list options from the query object.
|
||||
*
|
||||
* @return a comparator
|
||||
* @return a list options
|
||||
*/
|
||||
public Comparator<Post> toComparator() {
|
||||
var sort = getSort();
|
||||
var creationTimestampOrder = sort.getOrderFor("creationTimestamp");
|
||||
List<Comparator<Post>> comparators = new ArrayList<>();
|
||||
if (creationTimestampOrder != null) {
|
||||
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);
|
||||
public ListOptions toListOptions() {
|
||||
var listOptions =
|
||||
labelAndFieldSelectorToListOptions(getLabelSelector(), getFieldSelector());
|
||||
if (listOptions.getFieldSelector() == null) {
|
||||
listOptions.setFieldSelector(FieldSelector.all());
|
||||
}
|
||||
var labelSelectorBuilder = LabelSelector.builder();
|
||||
var fieldQuery = QueryFactory.all();
|
||||
|
||||
String keyword = getKeyword();
|
||||
if (keyword != null) {
|
||||
predicate = predicate.and(post -> {
|
||||
String excerpt = post.getStatusOrDefault().getExcerpt();
|
||||
return StringUtils.containsIgnoreCase(excerpt, keyword)
|
||||
|| StringUtils.containsIgnoreCase(post.getSpec().getSlug(), keyword)
|
||||
|| StringUtils.containsIgnoreCase(post.getSpec().getTitle(), keyword);
|
||||
});
|
||||
fieldQuery = QueryFactory.and(fieldQuery, QueryFactory.or(
|
||||
QueryFactory.contains("status.excerpt", keyword),
|
||||
QueryFactory.contains("spec.slug", keyword),
|
||||
QueryFactory.contains("spec.title", keyword)
|
||||
));
|
||||
}
|
||||
|
||||
Post.PostPhase publishPhase = getPublishPhase();
|
||||
if (publishPhase != null) {
|
||||
predicate = predicate.and(post -> {
|
||||
if (Post.PostPhase.PENDING_APPROVAL.equals(publishPhase)) {
|
||||
return !post.isPublished()
|
||||
&& Post.PostPhase.PENDING_APPROVAL.name()
|
||||
.equalsIgnoreCase(post.getStatusOrDefault().getPhase());
|
||||
}
|
||||
// published
|
||||
if (Post.PostPhase.PUBLISHED.equals(publishPhase)) {
|
||||
return post.isPublished();
|
||||
}
|
||||
// draft
|
||||
return !post.isPublished();
|
||||
});
|
||||
}
|
||||
|
||||
Post.VisibleEnum visible = getVisible();
|
||||
if (visible != null) {
|
||||
predicate =
|
||||
predicate.and(post -> visible.equals(post.getSpec().getVisible()));
|
||||
if (Post.PostPhase.PENDING_APPROVAL.equals(publishPhase)) {
|
||||
fieldQuery = QueryFactory.and(fieldQuery, QueryFactory.equal(
|
||||
"status.phase", Post.PostPhase.PENDING_APPROVAL.name())
|
||||
);
|
||||
labelSelectorBuilder.eq(Post.PUBLISHED_LABEL, BooleanUtils.FALSE);
|
||||
} else if (Post.PostPhase.PUBLISHED.equals(publishPhase)) {
|
||||
labelSelectorBuilder.eq(Post.PUBLISHED_LABEL, BooleanUtils.TRUE);
|
||||
} else {
|
||||
labelSelectorBuilder.eq(Post.PUBLISHED_LABEL, BooleanUtils.FALSE);
|
||||
}
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(username)) {
|
||||
Predicate<Post> isOwner = post -> Objects.equals(username, post.getSpec().getOwner());
|
||||
predicate = predicate.and(isOwner);
|
||||
fieldQuery = QueryFactory.and(fieldQuery, QueryFactory.equal(
|
||||
"spec.owner", username)
|
||||
);
|
||||
}
|
||||
return predicate;
|
||||
}
|
||||
|
||||
boolean contains(Collection<String> left, List<String> right) {
|
||||
// parameter is null, it means that ignore this condition
|
||||
if (left == null) {
|
||||
return true;
|
||||
}
|
||||
// else, it means that right is empty
|
||||
if (left.isEmpty()) {
|
||||
return right.isEmpty();
|
||||
}
|
||||
if (right == null) {
|
||||
return false;
|
||||
}
|
||||
return right.stream().anyMatch(left::contains);
|
||||
listOptions.setFieldSelector(listOptions.getFieldSelector().andQuery(fieldQuery));
|
||||
listOptions.setLabelSelector(
|
||||
listOptions.getLabelSelector().and(labelSelectorBuilder.build()));
|
||||
return listOptions;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package run.halo.app.content.impl;
|
||||
|
||||
import static run.halo.app.extension.index.query.QueryFactory.in;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
@ -8,6 +10,7 @@ import java.util.function.Function;
|
|||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.dao.OptimisticLockingFailureException;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.stereotype.Component;
|
||||
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.Tag;
|
||||
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.PageRequestImpl;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
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.ConditionStatus;
|
||||
import run.halo.app.metrics.CounterService;
|
||||
|
@ -58,16 +64,17 @@ public class PostServiceImpl extends AbstractContentService implements PostServi
|
|||
|
||||
@Override
|
||||
public Mono<ListResult<ListedPost>> listPost(PostQuery query) {
|
||||
return client.list(Post.class, query.toPredicate(),
|
||||
query.toComparator(), query.getPage(), query.getSize())
|
||||
.flatMap(listResult -> Flux.fromStream(
|
||||
listResult.get().map(this::getListedPost)
|
||||
)
|
||||
.concatMap(Function.identity())
|
||||
.collectList()
|
||||
.map(listedPosts -> new ListResult<>(listResult.getPage(), listResult.getSize(),
|
||||
listResult.getTotal(), listedPosts)
|
||||
)
|
||||
return client.listBy(Post.class, query.toListOptions(),
|
||||
PageRequestImpl.of(query.getPage(), query.getSize(), query.getSort())
|
||||
)
|
||||
.flatMap(listResult -> Flux.fromStream(listResult.get())
|
||||
.map(this::getListedPost)
|
||||
.concatMap(Function.identity())
|
||||
.collectList()
|
||||
.map(listedPosts -> new ListResult<>(listResult.getPage(), listResult.getSize(),
|
||||
listResult.getTotal(), listedPosts)
|
||||
)
|
||||
.defaultIfEmpty(ListResult.emptyResult())
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -144,16 +151,18 @@ public class PostServiceImpl extends AbstractContentService implements PostServi
|
|||
if (tagNames == null) {
|
||||
return Flux.empty();
|
||||
}
|
||||
return Flux.fromIterable(tagNames)
|
||||
.flatMapSequential(tagName -> client.fetch(Tag.class, tagName));
|
||||
var listOptions = new ListOptions();
|
||||
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) {
|
||||
if (categoryNames == null) {
|
||||
return Flux.empty();
|
||||
}
|
||||
return Flux.fromIterable(categoryNames)
|
||||
.flatMapSequential(categoryName -> client.fetch(Category.class, categoryName));
|
||||
var listOptions = new ListOptions();
|
||||
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) {
|
||||
|
|
|
@ -19,7 +19,7 @@ public class SnapshotServiceImpl implements SnapshotService {
|
|||
|
||||
private final ReactiveExtensionClient client;
|
||||
|
||||
private Clock clock;
|
||||
private final Clock clock;
|
||||
|
||||
public SnapshotServiceImpl(ReactiveExtensionClient client) {
|
||||
this.client = client;
|
||||
|
|
|
@ -2,6 +2,9 @@ package run.halo.app.core.extension.endpoint;
|
|||
|
||||
import static java.lang.Boolean.parseBoolean;
|
||||
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 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.User;
|
||||
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.PageRequestImpl;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.extension.router.selector.FieldSelector;
|
||||
|
||||
/**
|
||||
* Stats endpoint.
|
||||
|
@ -67,13 +73,16 @@ public class StatsEndpoint implements CustomEndpoint {
|
|||
stats.setUsers(count.intValue());
|
||||
return stats;
|
||||
}))
|
||||
.flatMap(stats -> client.list(Post.class, post -> !post.isDeleted(), null)
|
||||
.count()
|
||||
.map(count -> {
|
||||
stats.setPosts(count.intValue());
|
||||
return stats;
|
||||
})
|
||||
)
|
||||
.flatMap(stats -> {
|
||||
var listOptions = new ListOptions();
|
||||
listOptions.setFieldSelector(FieldSelector.of(
|
||||
and(isNull("metadata.deletionTimestamp"),
|
||||
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));
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
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.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
|
@ -13,6 +17,7 @@ import java.util.function.Function;
|
|||
import java.util.stream.Collectors;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.stereotype.Component;
|
||||
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.Post;
|
||||
import run.halo.app.extension.ExtensionClient;
|
||||
import run.halo.app.extension.ListOptions;
|
||||
import run.halo.app.extension.MetadataUtil;
|
||||
import run.halo.app.extension.controller.Controller;
|
||||
import run.halo.app.extension.controller.ControllerBuilder;
|
||||
import run.halo.app.extension.controller.Reconciler;
|
||||
import run.halo.app.extension.router.selector.FieldSelector;
|
||||
import run.halo.app.infra.utils.JsonUtils;
|
||||
|
||||
/**
|
||||
|
@ -138,7 +145,12 @@ public class CategoryReconciler implements Reconciler<Reconciler.Request> {
|
|||
.map(item -> item.getMetadata().getName())
|
||||
.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
|
||||
List<Post.CompactPost> compactPosts = posts.stream()
|
||||
|
@ -178,7 +190,7 @@ public class CategoryReconciler implements Reconciler<Reconciler.Request> {
|
|||
}
|
||||
|
||||
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()
|
||||
.collect(Collectors.toMap(category -> category.getMetadata().getName(),
|
||||
Function.identity()));
|
||||
|
|
|
@ -9,6 +9,7 @@ import com.google.common.hash.Hashing;
|
|||
import java.time.Instant;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
@ -18,6 +19,7 @@ import org.apache.commons.lang3.StringUtils;
|
|||
import org.jsoup.Jsoup;
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.stereotype.Component;
|
||||
import run.halo.app.content.ContentWrapper;
|
||||
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.extension.ExtensionClient;
|
||||
import run.halo.app.extension.ExtensionOperator;
|
||||
import run.halo.app.extension.ListOptions;
|
||||
import run.halo.app.extension.Ref;
|
||||
import run.halo.app.extension.controller.Controller;
|
||||
import run.halo.app.extension.controller.ControllerBuilder;
|
||||
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.ConditionStatus;
|
||||
import run.halo.app.infra.utils.HaloUtils;
|
||||
|
@ -189,8 +194,7 @@ public class PostReconciler implements Reconciler<Reconciler.Request> {
|
|||
var ref = Ref.of(post);
|
||||
// handle contributors
|
||||
var headSnapshot = post.getSpec().getHeadSnapshot();
|
||||
var contributors = client.list(Snapshot.class,
|
||||
snapshot -> ref.equals(snapshot.getSpec().getSubjectRef()), null)
|
||||
var contributors = listSnapshots(ref)
|
||||
.stream()
|
||||
.map(snapshot -> {
|
||||
Set<String> usernames = snapshot.getSpec().getContributors();
|
||||
|
@ -292,7 +296,7 @@ public class PostReconciler implements Reconciler<Reconciler.Request> {
|
|||
}
|
||||
var labels = post.getMetadata().getLabels();
|
||||
labels.put(Post.PUBLISHED_LABEL, Boolean.FALSE.toString());
|
||||
var status = post.getStatus();
|
||||
final var status = post.getStatus();
|
||||
|
||||
var condition = new Condition();
|
||||
condition.setType("CancelledPublish");
|
||||
|
@ -310,9 +314,7 @@ public class PostReconciler implements Reconciler<Reconciler.Request> {
|
|||
private void cleanUpResources(Post post) {
|
||||
// clean up snapshots
|
||||
final Ref ref = Ref.of(post);
|
||||
client.list(Snapshot.class,
|
||||
snapshot -> ref.equals(snapshot.getSpec().getSubjectRef()), null)
|
||||
.forEach(client::delete);
|
||||
listSnapshots(ref).forEach(client::delete);
|
||||
|
||||
// clean up comments
|
||||
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
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ import lombok.AllArgsConstructor;
|
|||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.jsoup.Jsoup;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.Assert;
|
||||
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.ExtensionOperator;
|
||||
import run.halo.app.extension.ExtensionUtil;
|
||||
import run.halo.app.extension.ListOptions;
|
||||
import run.halo.app.extension.MetadataUtil;
|
||||
import run.halo.app.extension.Ref;
|
||||
import run.halo.app.extension.controller.Controller;
|
||||
import run.halo.app.extension.controller.ControllerBuilder;
|
||||
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.ConditionList;
|
||||
import run.halo.app.infra.ConditionStatus;
|
||||
|
@ -243,9 +247,7 @@ public class SinglePageReconciler implements Reconciler<Reconciler.Request> {
|
|||
private void cleanUpResources(SinglePage singlePage) {
|
||||
// clean up snapshot
|
||||
Ref ref = Ref.of(singlePage);
|
||||
client.list(Snapshot.class,
|
||||
snapshot -> ref.equals(snapshot.getSpec().getSubjectRef()), null)
|
||||
.forEach(client::delete);
|
||||
listSnapshots(ref).forEach(client::delete);
|
||||
|
||||
// clean up comments
|
||||
client.list(Comment.class, comment -> comment.getSpec().getSubjectRef().equals(ref),
|
||||
|
@ -332,8 +334,7 @@ public class SinglePageReconciler implements Reconciler<Reconciler.Request> {
|
|||
|
||||
// handle contributors
|
||||
String headSnapshot = singlePage.getSpec().getHeadSnapshot();
|
||||
List<String> contributors = client.list(Snapshot.class,
|
||||
snapshot -> Ref.of(singlePage).equals(snapshot.getSpec().getSubjectRef()), null)
|
||||
List<String> contributors = listSnapshots(Ref.of(singlePage))
|
||||
.stream()
|
||||
.peek(snapshot -> {
|
||||
snapshot.getSpec().setContentPatch(StringUtils.EMPTY);
|
||||
|
@ -377,4 +378,11 @@ public class SinglePageReconciler implements Reconciler<Reconciler.Request> {
|
|||
return Objects.equals(true, singlePage.getSpec().getDeleted())
|
||||
|| 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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
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.util.HashSet;
|
||||
import java.util.Map;
|
||||
|
@ -7,17 +11,19 @@ import java.util.Set;
|
|||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.BooleanUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.stereotype.Component;
|
||||
import run.halo.app.content.PostIndexInformer;
|
||||
import run.halo.app.content.permalinks.TagPermalinkPolicy;
|
||||
import run.halo.app.core.extension.content.Constant;
|
||||
import run.halo.app.core.extension.content.Post;
|
||||
import run.halo.app.core.extension.content.Tag;
|
||||
import run.halo.app.extension.ExtensionClient;
|
||||
import run.halo.app.extension.ListOptions;
|
||||
import run.halo.app.extension.MetadataUtil;
|
||||
import run.halo.app.extension.controller.Controller;
|
||||
import run.halo.app.extension.controller.ControllerBuilder;
|
||||
import run.halo.app.extension.controller.Reconciler;
|
||||
import run.halo.app.extension.router.selector.FieldSelector;
|
||||
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 final ExtensionClient client;
|
||||
private final TagPermalinkPolicy tagPermalinkPolicy;
|
||||
private final PostIndexInformer postIndexInformer;
|
||||
|
||||
@Override
|
||||
public Result reconcile(Request request) {
|
||||
|
@ -128,20 +133,22 @@ public class TagReconciler implements Reconciler<Reconciler.Request> {
|
|||
}
|
||||
|
||||
private void populatePosts(Tag tag) {
|
||||
// populate post count
|
||||
Set<String> postNames = postIndexInformer.getByTagName(tag.getMetadata().getName());
|
||||
tag.getStatusOrDefault().setPostCount(postNames.size());
|
||||
// populate post-count
|
||||
var listOptions = new ListOptions();
|
||||
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
|
||||
Map<String, String> labelToQuery = Map.of(Post.PUBLISHED_LABEL, BooleanUtils.TRUE,
|
||||
Post.VISIBLE_LABEL, Post.VisibleEnum.PUBLIC.name(),
|
||||
Post.DELETED_LABEL, BooleanUtils.FALSE);
|
||||
Set<String> hasAllLabelPosts = postIndexInformer.getByLabels(labelToQuery);
|
||||
|
||||
// retain all posts that has all labels
|
||||
Set<String> postNamesWithTag = new HashSet<>(postNames);
|
||||
postNamesWithTag.retainAll(hasAllLabelPosts);
|
||||
tag.getStatusOrDefault().setVisiblePostCount(postNamesWithTag.size());
|
||||
var publicPosts = posts.stream()
|
||||
.filter(post -> post.getMetadata().getDeletionTimestamp() == null
|
||||
&& isFalse(post.getSpec().getDeleted())
|
||||
&& BooleanUtils.TRUE.equals(nullSafeLabels(post).get(Post.PUBLISHED_LABEL))
|
||||
&& Post.VisibleEnum.PUBLIC.equals(post.getSpec().getVisible())
|
||||
)
|
||||
.toList();
|
||||
tag.getStatusOrDefault().setVisiblePostCount(publicPosts.size());
|
||||
}
|
||||
|
||||
private boolean isDeleted(Tag tag) {
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
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.context.ApplicationListener;
|
||||
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.DefaultSchemeWatcherManager;
|
||||
import run.halo.app.extension.Secret;
|
||||
import run.halo.app.extension.index.IndexSpec;
|
||||
import run.halo.app.extension.index.IndexSpecRegistryImpl;
|
||||
import run.halo.app.migration.Backup;
|
||||
import run.halo.app.plugin.extensionpoint.ExtensionDefinition;
|
||||
|
@ -70,10 +76,102 @@ public class SchemeInitializer implements ApplicationListener<ApplicationContext
|
|||
schemeManager.register(Theme.class);
|
||||
schemeManager.register(Menu.class);
|
||||
schemeManager.register(MenuItem.class);
|
||||
schemeManager.register(Post.class);
|
||||
schemeManager.register(Category.class);
|
||||
schemeManager.register(Tag.class);
|
||||
schemeManager.register(Snapshot.class);
|
||||
schemeManager.register(Post.class, indexSpecs -> {
|
||||
indexSpecs.add(new IndexSpec()
|
||||
.setName("spec.title")
|
||||
.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(Reply.class);
|
||||
schemeManager.register(SinglePage.class);
|
||||
|
|
|
@ -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.parameter.Builder.parameterBuilder;
|
||||
import static run.halo.app.theme.endpoint.PublicApiUtils.containsElement;
|
||||
import static run.halo.app.theme.endpoint.PublicApiUtils.toAnotherListResult;
|
||||
|
||||
import io.swagger.v3.oas.annotations.enums.ParameterIn;
|
||||
import java.util.function.Predicate;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springdoc.webflux.core.fn.SpringdocRouteBuilder;
|
||||
import org.springframework.http.MediaType;
|
||||
|
@ -17,11 +15,11 @@ import org.springframework.web.reactive.function.server.ServerResponse;
|
|||
import org.springframework.web.server.ServerWebExchange;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.extension.content.Category;
|
||||
import run.halo.app.core.extension.content.Post;
|
||||
import run.halo.app.core.extension.endpoint.CustomEndpoint;
|
||||
import run.halo.app.extension.GroupVersion;
|
||||
import run.halo.app.extension.ListResult;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.extension.index.query.QueryFactory;
|
||||
import run.halo.app.extension.router.QueryParamBuildUtil;
|
||||
import run.halo.app.extension.router.SortableRequest;
|
||||
import run.halo.app.theme.finders.PostPublicQueryService;
|
||||
|
@ -93,13 +91,11 @@ public class CategoryQueryEndpoint implements CustomEndpoint {
|
|||
private Mono<ServerResponse> listPostsByCategoryName(ServerRequest request) {
|
||||
final var name = request.pathVariable("name");
|
||||
final var query = new PostPublicQuery(request.exchange());
|
||||
Predicate<Post> categoryContainsPredicate =
|
||||
post -> containsElement(post.getSpec().getCategories(), name);
|
||||
return postPublicQueryService.list(query.getPage(),
|
||||
query.getSize(),
|
||||
categoryContainsPredicate.and(query.toPredicate()),
|
||||
query.toComparator()
|
||||
)
|
||||
var listOptions = query.toListOptions();
|
||||
var newFieldSelector = listOptions.getFieldSelector()
|
||||
.andQuery(QueryFactory.equal("spec.categories", name));
|
||||
listOptions.setFieldSelector(newFieldSelector);
|
||||
return postPublicQueryService.list(listOptions, query.toPageRequest())
|
||||
.flatMap(result -> ServerResponse.ok()
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.bodyValue(result)
|
||||
|
@ -118,12 +114,7 @@ public class CategoryQueryEndpoint implements CustomEndpoint {
|
|||
|
||||
private Mono<ServerResponse> listCategories(ServerRequest request) {
|
||||
CategoryPublicQuery query = new CategoryPublicQuery(request.exchange());
|
||||
return client.list(Category.class,
|
||||
query.toPredicate(),
|
||||
query.toComparator(),
|
||||
query.getPage(),
|
||||
query.getSize()
|
||||
)
|
||||
return client.listBy(Category.class, query.toListOptions(), query.toPageRequest())
|
||||
.map(listResult -> toAnotherListResult(listResult, CategoryVo::from))
|
||||
.flatMap(result -> ServerResponse.ok()
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
|
|
|
@ -107,8 +107,7 @@ public class PostQueryEndpoint implements CustomEndpoint {
|
|||
|
||||
private Mono<ServerResponse> listPosts(ServerRequest request) {
|
||||
PostPublicQuery query = new PostPublicQuery(request.exchange());
|
||||
return postPublicQueryService.list(query.getPage(), query.getSize(), query.toPredicate(),
|
||||
query.toComparator())
|
||||
return postPublicQueryService.list(query.toListOptions(), query.toPageRequest())
|
||||
.flatMap(result -> ServerResponse.ok().contentType(MediaType.APPLICATION_JSON)
|
||||
.bodyValue(result)
|
||||
);
|
||||
|
|
|
@ -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.parameter.Builder.parameterBuilder;
|
||||
import static run.halo.app.theme.endpoint.PublicApiUtils.containsElement;
|
||||
|
||||
import io.swagger.v3.oas.annotations.enums.ParameterIn;
|
||||
import java.util.function.Predicate;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springdoc.webflux.core.fn.SpringdocRouteBuilder;
|
||||
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.server.ServerWebExchange;
|
||||
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.endpoint.CustomEndpoint;
|
||||
import run.halo.app.extension.GroupVersion;
|
||||
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.SortableRequest;
|
||||
import run.halo.app.theme.finders.PostPublicQueryService;
|
||||
|
@ -37,6 +36,7 @@ import run.halo.app.theme.finders.vo.TagVo;
|
|||
@RequiredArgsConstructor
|
||||
public class TagQueryEndpoint implements CustomEndpoint {
|
||||
|
||||
private final ReactiveExtensionClient client;
|
||||
private final TagFinder tagFinder;
|
||||
private final PostPublicQueryService postPublicQueryService;
|
||||
|
||||
|
@ -102,13 +102,11 @@ public class TagQueryEndpoint implements CustomEndpoint {
|
|||
private Mono<ServerResponse> listPostsByTagName(ServerRequest request) {
|
||||
final var name = request.pathVariable("name");
|
||||
final var query = new PostPublicQuery(request.exchange());
|
||||
final Predicate<Post> containsTagPredicate =
|
||||
post -> containsElement(post.getSpec().getTags(), name);
|
||||
return postPublicQueryService.list(query.getPage(),
|
||||
query.getSize(),
|
||||
containsTagPredicate.and(query.toPredicate()),
|
||||
query.toComparator()
|
||||
)
|
||||
var listOptions = query.toListOptions();
|
||||
var newFieldSelector = listOptions.getFieldSelector()
|
||||
.andQuery(QueryFactory.equal("spec.tags", name));
|
||||
listOptions.setFieldSelector(newFieldSelector);
|
||||
return postPublicQueryService.list(listOptions, query.toPageRequest())
|
||||
.flatMap(result -> ServerResponse.ok()
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.bodyValue(result)
|
||||
|
@ -117,11 +115,12 @@ public class TagQueryEndpoint implements CustomEndpoint {
|
|||
|
||||
private Mono<ServerResponse> listTags(ServerRequest request) {
|
||||
var query = new TagPublicQuery(request.exchange());
|
||||
return tagFinder.list(query.getPage(),
|
||||
query.getSize(),
|
||||
query.toPredicate(),
|
||||
query.toComparator()
|
||||
)
|
||||
return client.listBy(Tag.class, query.toListOptions(), query.toPageRequest())
|
||||
.map(result -> {
|
||||
var tagVos = tagFinder.convertToVo(result.getItems());
|
||||
return new ListResult<>(result.getPage(), result.getSize(),
|
||||
result.getTotal(), tagVos);
|
||||
})
|
||||
.flatMap(result -> ServerResponse.ok()
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.bodyValue(result)
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
package run.halo.app.theme.finders;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.function.Predicate;
|
||||
import org.springframework.lang.NonNull;
|
||||
import reactor.core.publisher.Mono;
|
||||
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.PageRequest;
|
||||
import run.halo.app.theme.ReactivePostContentHandler;
|
||||
import run.halo.app.theme.finders.vo.ContentVo;
|
||||
import run.halo.app.theme.finders.vo.ListedPostVo;
|
||||
|
@ -14,17 +14,13 @@ import run.halo.app.theme.finders.vo.PostVo;
|
|||
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 size page size
|
||||
* @param postPredicate post predicate
|
||||
* @param comparator post comparator
|
||||
* @return list result
|
||||
* @param listOptions additional list options
|
||||
* @param page page request must not be null
|
||||
* @return a list of listed post vo
|
||||
*/
|
||||
Mono<ListResult<ListedPostVo>> list(Integer page, Integer size,
|
||||
Predicate<Post> postPredicate,
|
||||
Comparator<Post> comparator);
|
||||
Mono<ListResult<ListedPostVo>> list(ListOptions listOptions, PageRequest page);
|
||||
|
||||
/**
|
||||
* Converts post to listed post vo.
|
||||
|
|
|
@ -24,8 +24,11 @@ public interface TagFinder {
|
|||
|
||||
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,
|
||||
@Nullable Predicate<Tag> predicate, @Nullable Comparator<Tag> comparator);
|
||||
|
||||
List<TagVo> convertToVo(List<Tag> tags);
|
||||
|
||||
Flux<TagVo> listAll();
|
||||
}
|
||||
|
|
|
@ -11,11 +11,16 @@ import java.util.stream.Collectors;
|
|||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.ObjectUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
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.index.query.QueryFactory;
|
||||
import run.halo.app.extension.router.selector.FieldSelector;
|
||||
import run.halo.app.theme.finders.CategoryFinder;
|
||||
import run.halo.app.theme.finders.Finder;
|
||||
import run.halo.app.theme.finders.vo.CategoryTreeVo;
|
||||
|
@ -51,10 +56,17 @@ public class CategoryFinderImpl implements CategoryFinder {
|
|||
.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
|
||||
public Mono<ListResult<CategoryVo>> list(Integer page, Integer size) {
|
||||
return client.list(Category.class, null,
|
||||
defaultComparator(), pageNullSafe(page), sizeNullSafe(size))
|
||||
return client.listBy(Category.class, new ListOptions(),
|
||||
PageRequestImpl.of(pageNullSafe(page), sizeNullSafe(size), defaultSort())
|
||||
)
|
||||
.map(list -> {
|
||||
List<CategoryVo> categoryVos = list.get()
|
||||
.map(CategoryVo::from)
|
||||
|
@ -65,12 +77,6 @@ public class CategoryFinderImpl implements CategoryFinder {
|
|||
.defaultIfEmpty(new ListResult<>(page, size, 0L, List.of()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<CategoryVo> listAll() {
|
||||
return client.list(Category.class, null, defaultComparator())
|
||||
.map(CategoryVo::from);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<CategoryTreeVo> listAsTree() {
|
||||
return this.toCategoryTreeVoFlux(null);
|
||||
|
@ -82,20 +88,9 @@ public class CategoryFinderImpl implements CategoryFinder {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Mono<CategoryVo> getParentByName(String name) {
|
||||
if (StringUtils.isBlank(name)) {
|
||||
return Mono.empty();
|
||||
}
|
||||
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);
|
||||
public Flux<CategoryVo> listAll() {
|
||||
return client.listAll(Category.class, new ListOptions(), defaultSort())
|
||||
.map(CategoryVo::from);
|
||||
}
|
||||
|
||||
Flux<CategoryTreeVo> toCategoryTreeVoFlux(String name) {
|
||||
|
@ -169,6 +164,22 @@ public class CategoryFinderImpl implements CategoryFinder {
|
|||
.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) {
|
||||
return ObjectUtils.defaultIfNull(page, 1);
|
||||
}
|
||||
|
|
|
@ -1,25 +1,26 @@
|
|||
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.Deque;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.apache.commons.lang3.ObjectUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.springframework.util.comparator.Comparators;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.util.Assert;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
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.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.theme.finders.Finder;
|
||||
import run.halo.app.theme.finders.PostFinder;
|
||||
|
@ -64,33 +65,32 @@ public class PostFinderImpl implements PostFinder {
|
|||
return postPublicQueryService.getContent(postName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<NavigationPostVo> cursor(String currentName) {
|
||||
// TODO Optimize the post names query here
|
||||
return postPredicateResolver.getPredicate()
|
||||
.flatMapMany(postPredicate ->
|
||||
client.list(Post.class, postPredicate, defaultComparator())
|
||||
)
|
||||
.map(post -> post.getMetadata().getName())
|
||||
.collectList()
|
||||
.flatMap(postNames -> Mono.just(NavigationPostVo.builder())
|
||||
.flatMap(builder -> getByName(currentName)
|
||||
.doOnNext(builder::current)
|
||||
.thenReturn(builder)
|
||||
)
|
||||
.flatMap(builder -> {
|
||||
Pair<String, String> previousNextPair =
|
||||
postPreviousNextPair(postNames, currentName);
|
||||
String previousPostName = previousNextPair.getLeft();
|
||||
String nextPostName = previousNextPair.getRight();
|
||||
return fetchByName(previousPostName)
|
||||
.doOnNext(builder::previous)
|
||||
.then(fetchByName(nextPostName))
|
||||
.doOnNext(builder::next)
|
||||
.thenReturn(builder);
|
||||
})
|
||||
.map(NavigationPostVo.NavigationPostVoBuilder::build))
|
||||
.defaultIfEmpty(NavigationPostVo.empty());
|
||||
static Sort defaultSort() {
|
||||
return Sort.by(Sort.Order.desc("spec.pinned"),
|
||||
Sort.Order.desc("spec.priority"),
|
||||
Sort.Order.desc("spec.publishTime"),
|
||||
Sort.Order.desc("metadata.name")
|
||||
);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
static LinkNavigation findPostNavigation(List<String> postNames, String target) {
|
||||
Assert.notNull(target, "Target post name must not be null");
|
||||
for (int i = 0; i < postNames.size(); i++) {
|
||||
var item = postNames.get(i);
|
||||
if (target.equals(item)) {
|
||||
var prevLink = (i > 0) ? postNames.get(i - 1) : null;
|
||||
var nextLink = (i < postNames.size() - 1) ? postNames.get(i + 1) : null;
|
||||
return new LinkNavigation(prevLink, target, nextLink);
|
||||
}
|
||||
}
|
||||
return new LinkNavigation(null, target, null);
|
||||
}
|
||||
|
||||
static Sort archiveSort() {
|
||||
return Sort.by(Sort.Order.desc("spec.publishTime"),
|
||||
Sort.Order.desc("metadata.name")
|
||||
);
|
||||
}
|
||||
|
||||
private Mono<PostVo> fetchByName(String name) {
|
||||
|
@ -102,106 +102,76 @@ public class PostFinderImpl implements PostFinder {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Flux<ListedPostVo> listAll() {
|
||||
return postPredicateResolver.getPredicate()
|
||||
.flatMapMany(predicate -> client.list(Post.class, predicate, defaultComparator()))
|
||||
.concatMap(postPublicQueryService::convertToListedVo);
|
||||
}
|
||||
|
||||
static Pair<String, String> postPreviousNextPair(List<String> postNames,
|
||||
String currentName) {
|
||||
FixedSizeSlidingWindow<String> window = new FixedSizeSlidingWindow<>(3);
|
||||
for (String postName : postNames) {
|
||||
window.add(postName);
|
||||
if (!window.isFull()) {
|
||||
continue;
|
||||
}
|
||||
int index = window.indexOf(currentName);
|
||||
if (index == -1) {
|
||||
continue;
|
||||
}
|
||||
// got expected window
|
||||
if (index < 2) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
public Mono<NavigationPostVo> cursor(String currentName) {
|
||||
return postPredicateResolver.getListOptions()
|
||||
.flatMapMany(postListOption ->
|
||||
client.listAll(Post.class, postListOption, defaultSort())
|
||||
)
|
||||
.map(post -> post.getMetadata().getName())
|
||||
.collectList()
|
||||
.flatMap(postNames -> Mono.just(NavigationPostVo.builder())
|
||||
.flatMap(builder -> getByName(currentName)
|
||||
.doOnNext(builder::current)
|
||||
.thenReturn(builder)
|
||||
)
|
||||
.flatMap(builder -> {
|
||||
var previousNextPair = findPostNavigation(postNames, currentName);
|
||||
String previousPostName = previousNextPair.prev();
|
||||
String nextPostName = previousNextPair.next();
|
||||
return fetchByName(previousPostName)
|
||||
.doOnNext(builder::previous)
|
||||
.then(fetchByName(nextPostName))
|
||||
.doOnNext(builder::next)
|
||||
.thenReturn(builder);
|
||||
})
|
||||
.map(NavigationPostVo.NavigationPostVoBuilder::build))
|
||||
.defaultIfEmpty(NavigationPostVo.empty());
|
||||
}
|
||||
|
||||
@Override
|
||||
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
|
||||
public Mono<ListResult<ListedPostVo>> listByCategory(Integer page, Integer size,
|
||||
String categoryName) {
|
||||
return postPublicQueryService.list(page, size,
|
||||
post -> contains(post.getSpec().getCategories(), categoryName), defaultComparator());
|
||||
var fieldQuery = QueryFactory.all();
|
||||
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
|
||||
public Mono<ListResult<ListedPostVo>> listByTag(Integer page, Integer size, String tag) {
|
||||
return postPublicQueryService.list(page, size,
|
||||
post -> contains(post.getSpec().getTags(), tag), defaultComparator());
|
||||
var fieldQuery = QueryFactory.all();
|
||||
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
|
||||
public Mono<ListResult<ListedPostVo>> listByOwner(Integer page, Integer size, String owner) {
|
||||
return postPublicQueryService.list(page, size,
|
||||
post -> post.getSpec().getOwner().equals(owner), defaultComparator());
|
||||
var fieldQuery = QueryFactory.all();
|
||||
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
|
||||
|
@ -217,23 +187,23 @@ public class PostFinderImpl implements PostFinder {
|
|||
@Override
|
||||
public Mono<ListResult<PostArchiveVo>> archives(Integer page, Integer size, String year,
|
||||
String month) {
|
||||
return postPublicQueryService.list(page, size, post -> {
|
||||
Map<String, String> labels = post.getMetadata().getLabels();
|
||||
if (labels == null) {
|
||||
return false;
|
||||
}
|
||||
boolean yearMatch = StringUtils.isBlank(year)
|
||||
|| year.equals(labels.get(Post.ARCHIVE_YEAR_LABEL));
|
||||
boolean monthMatch = StringUtils.isBlank(month)
|
||||
|| month.equals(labels.get(Post.ARCHIVE_MONTH_LABEL));
|
||||
return yearMatch && monthMatch;
|
||||
}, archiveComparator())
|
||||
var listOptions = new ListOptions();
|
||||
var labelSelectorBuilder = LabelSelector.builder();
|
||||
if (StringUtils.isNotBlank(year)) {
|
||||
labelSelectorBuilder.eq(Post.ARCHIVE_YEAR_LABEL, year);
|
||||
}
|
||||
if (StringUtils.isNotBlank(month)) {
|
||||
labelSelectorBuilder.eq(Post.ARCHIVE_MONTH_LABEL, month);
|
||||
}
|
||||
listOptions.setLabelSelector(labelSelectorBuilder.build());
|
||||
var pageRequest = PageRequestImpl.of(pageNullSafe(page), sizeNullSafe(size), archiveSort());
|
||||
return postPublicQueryService.list(listOptions, pageRequest)
|
||||
.map(list -> {
|
||||
Map<String, List<ListedPostVo>> yearPosts = list.get()
|
||||
.collect(Collectors.groupingBy(
|
||||
post -> HaloUtils.getYearText(post.getSpec().getPublishTime())));
|
||||
List<PostArchiveVo> postArchives =
|
||||
yearPosts.entrySet().stream().map(entry -> {
|
||||
List<PostArchiveVo> postArchives = yearPosts.entrySet().stream()
|
||||
.map(entry -> {
|
||||
String key = entry.getKey();
|
||||
// archives by month
|
||||
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(),
|
||||
postArchives);
|
||||
})
|
||||
.defaultIfEmpty(new ListResult<>(page, size, 0, List.of()));
|
||||
.defaultIfEmpty(ListResult.emptyResult());
|
||||
}
|
||||
|
||||
private boolean contains(List<String> c, String key) {
|
||||
if (StringUtils.isBlank(key) || c == null) {
|
||||
return false;
|
||||
}
|
||||
return c.contains(key);
|
||||
@Override
|
||||
public Flux<ListedPostVo> listAll() {
|
||||
return postPredicateResolver.getListOptions()
|
||||
.flatMapMany(listOptions -> client.listAll(Post.class, listOptions, defaultSort()))
|
||||
.concatMap(postPublicQueryService::convertToListedVo);
|
||||
}
|
||||
|
||||
static Comparator<Post> defaultComparator() {
|
||||
Function<Post, Boolean> pinned =
|
||||
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();
|
||||
int pageNullSafe(Integer page) {
|
||||
return ObjectUtils.defaultIfNull(page, 1);
|
||||
}
|
||||
|
||||
static Comparator<Post> archiveComparator() {
|
||||
Function<Post, Instant> publishTime =
|
||||
post -> post.getSpec().getPublishTime();
|
||||
Function<Post, String> name = post -> post.getMetadata().getName();
|
||||
return Comparator.comparing(publishTime, Comparators.nullsLow())
|
||||
.thenComparing(name)
|
||||
.reversed();
|
||||
int sizeNullSafe(Integer size) {
|
||||
return ObjectUtils.defaultIfNull(size, 10);
|
||||
}
|
||||
|
||||
record LinkNavigation(String prev, String current, String next) {
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,8 @@
|
|||
package run.halo.app.theme.finders.impl;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.ObjectUtils;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.stereotype.Component;
|
||||
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.PostService;
|
||||
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.PageRequest;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.metrics.CounterService;
|
||||
import run.halo.app.metrics.MeterUtils;
|
||||
|
@ -52,13 +51,21 @@ public class PostPublicQueryServiceImpl implements PostPublicQueryService {
|
|||
private final ReactiveQueryPostPredicateResolver postPredicateResolver;
|
||||
|
||||
@Override
|
||||
public Mono<ListResult<ListedPostVo>> list(Integer page, Integer size,
|
||||
Predicate<Post> postPredicate, Comparator<Post> comparator) {
|
||||
return postPredicateResolver.getPredicate()
|
||||
.map(predicate -> predicate.and(postPredicate == null ? post -> true : postPredicate))
|
||||
.flatMap(predicate -> client.list(Post.class, predicate,
|
||||
comparator, pageNullSafe(page), sizeNullSafe(size))
|
||||
)
|
||||
public Mono<ListResult<ListedPostVo>> list(ListOptions queryOptions, PageRequest page) {
|
||||
return postPredicateResolver.getListOptions()
|
||||
.map(option -> {
|
||||
var fieldSelector = queryOptions.getFieldSelector();
|
||||
if (fieldSelector != null) {
|
||||
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())
|
||||
.concatMap(post -> convertToListedVo(post)
|
||||
.flatMap(postVo -> populateStats(postVo)
|
||||
|
@ -70,9 +77,10 @@ public class PostPublicQueryServiceImpl implements PostPublicQueryService {
|
|||
postVos)
|
||||
)
|
||||
)
|
||||
.defaultIfEmpty(new ListResult<>(page, size, 0L, List.of()));
|
||||
.defaultIfEmpty(ListResult.emptyResult());
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Mono<ListedPostVo> convertToListedVo(@NonNull Post post) {
|
||||
Assert.notNull(post, "Post must not be null");
|
||||
|
@ -180,12 +188,4 @@ public class PostPublicQueryServiceImpl implements PostPublicQueryService {
|
|||
)
|
||||
.defaultIfEmpty(StatsVo.empty());
|
||||
}
|
||||
|
||||
int pageNullSafe(Integer page) {
|
||||
return ObjectUtils.defaultIfNull(page, 1);
|
||||
}
|
||||
|
||||
int sizeNullSafe(Integer size) {
|
||||
return ObjectUtils.defaultIfNull(size, 10);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,20 @@
|
|||
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 reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.extension.Counter;
|
||||
import run.halo.app.core.extension.content.Category;
|
||||
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.router.selector.FieldSelector;
|
||||
import run.halo.app.extension.router.selector.LabelSelector;
|
||||
import run.halo.app.theme.finders.Finder;
|
||||
import run.halo.app.theme.finders.SiteStatsFinder;
|
||||
import run.halo.app.theme.finders.vo.SiteStatsVo;
|
||||
|
@ -40,14 +49,22 @@ public class SiteStatsFinderImpl implements SiteStatsFinder {
|
|||
}
|
||||
|
||||
Mono<Integer> postCount() {
|
||||
return client.list(Post.class, post -> !post.isDeleted() && post.isPublished(), null)
|
||||
.count()
|
||||
.map(Long::intValue);
|
||||
var listOptions = new ListOptions();
|
||||
listOptions.setLabelSelector(LabelSelector.builder()
|
||||
.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() {
|
||||
return client.list(Category.class, null, null)
|
||||
.count()
|
||||
return client.listBy(Category.class, new ListOptions(), PageRequestImpl.ofSize(1))
|
||||
.map(ListResult::getTotal)
|
||||
.map(Long::intValue);
|
||||
}
|
||||
|
||||
|
|
|
@ -6,11 +6,16 @@ import java.util.Optional;
|
|||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
import org.apache.commons.lang3.ObjectUtils;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.extension.content.Tag;
|
||||
import run.halo.app.extension.ListOptions;
|
||||
import run.halo.app.extension.ListResult;
|
||||
import run.halo.app.extension.PageRequest;
|
||||
import run.halo.app.extension.PageRequestImpl;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.theme.finders.Finder;
|
||||
import run.halo.app.theme.finders.TagFinder;
|
||||
|
@ -48,7 +53,8 @@ public class TagFinderImpl implements TagFinder {
|
|||
|
||||
@Override
|
||||
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
|
||||
|
@ -68,13 +74,34 @@ public class TagFinderImpl implements TagFinder {
|
|||
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
|
||||
public Flux<TagVo> listAll() {
|
||||
return client.list(Tag.class, null,
|
||||
DEFAULT_COMPARATOR.reversed())
|
||||
return client.listAll(Tag.class, new ListOptions(),
|
||||
Sort.by(Sort.Order.desc("metadata.creationTimestamp")))
|
||||
.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) {
|
||||
return ObjectUtils.defaultIfNull(page, 1);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
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.util.Objects;
|
||||
import java.util.function.Predicate;
|
||||
|
@ -9,6 +14,9 @@ import org.springframework.stereotype.Component;
|
|||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.extension.content.Post;
|
||||
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;
|
||||
|
||||
/**
|
||||
|
@ -34,6 +42,28 @@ public class DefaultQueryPostPredicateResolver implements ReactiveQueryPostPredi
|
|||
.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() {
|
||||
return ReactiveSecurityContextHolder.getContext()
|
||||
.map(SecurityContext::getAuthentication)
|
||||
|
|
|
@ -5,6 +5,7 @@ import lombok.RequiredArgsConstructor;
|
|||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.stereotype.Component;
|
||||
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.Extension;
|
||||
import run.halo.app.extension.ExtensionClient;
|
||||
import run.halo.app.extension.ListOptions;
|
||||
import run.halo.app.extension.MetadataOperator;
|
||||
import run.halo.app.extension.MetadataUtil;
|
||||
import run.halo.app.theme.DefaultTemplateEnum;
|
||||
|
@ -52,7 +54,7 @@ public class ExtensionPermalinkPatternUpdater
|
|||
|
||||
private void updatePostPermalink(String 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));
|
||||
}
|
||||
|
||||
|
@ -70,13 +72,13 @@ public class ExtensionPermalinkPatternUpdater
|
|||
|
||||
private void updateCategoryPermalink(String 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));
|
||||
}
|
||||
|
||||
private void updateTagPermalink(String 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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package run.halo.app.theme.router;
|
|||
import java.util.function.Predicate;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.extension.content.Post;
|
||||
import run.halo.app.extension.ListOptions;
|
||||
|
||||
/**
|
||||
* The reactive query post predicate resolver.
|
||||
|
@ -13,4 +14,6 @@ import run.halo.app.core.extension.content.Post;
|
|||
public interface ReactiveQueryPostPredicateResolver {
|
||||
|
||||
Mono<Predicate<Post>> getPredicate();
|
||||
|
||||
Mono<ListOptions> getListOptions();
|
||||
}
|
||||
|
|
|
@ -17,7 +17,12 @@ import org.springframework.web.reactive.function.server.ServerResponse;
|
|||
import org.springframework.web.server.i18n.LocaleContextResolver;
|
||||
import reactor.core.publisher.Mono;
|
||||
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.index.query.QueryFactory;
|
||||
import run.halo.app.extension.router.selector.FieldSelector;
|
||||
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
|
||||
import run.halo.app.infra.SystemSetting;
|
||||
import run.halo.app.infra.exception.NotFoundException;
|
||||
|
@ -81,10 +86,18 @@ public class CategoryPostRouteFactory implements RouteFactory {
|
|||
}
|
||||
|
||||
Mono<CategoryVo> fetchBySlug(String slug) {
|
||||
return client.list(Category.class, category -> category.getSpec().getSlug().equals(slug)
|
||||
&& category.getMetadata().getDeletionTimestamp() == null, null)
|
||||
.next()
|
||||
.map(CategoryVo::from);
|
||||
var listOptions = new ListOptions();
|
||||
listOptions.setFieldSelector(FieldSelector.of(
|
||||
QueryFactory.and(
|
||||
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,
|
||||
|
|
|
@ -15,6 +15,7 @@ import lombok.AllArgsConstructor;
|
|||
import lombok.Data;
|
||||
import lombok.Getter;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.lang.NonNull;
|
||||
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.extension.MetadataUtil;
|
||||
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.utils.JsonUtils;
|
||||
import run.halo.app.theme.DefaultTemplateEnum;
|
||||
|
@ -159,11 +161,14 @@ public class PostRouteFactory implements RouteFactory {
|
|||
}
|
||||
|
||||
private Flux<Post> fetchPostsBySlug(String slug) {
|
||||
return queryPostPredicateResolver.getPredicate()
|
||||
.flatMapMany(predicate -> client.list(Post.class,
|
||||
predicate.and(post -> matchIfPresent(slug, post.getSpec().getSlug())),
|
||||
null)
|
||||
);
|
||||
return queryPostPredicateResolver.getListOptions()
|
||||
.flatMapMany(listOptions -> {
|
||||
if (StringUtils.isNotBlank(slug)) {
|
||||
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) {
|
||||
|
|
|
@ -15,7 +15,12 @@ import org.springframework.web.reactive.function.server.ServerResponse;
|
|||
import org.springframework.web.server.i18n.LocaleContextResolver;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.extension.content.Tag;
|
||||
import run.halo.app.extension.ListOptions;
|
||||
import run.halo.app.extension.ListResult;
|
||||
import run.halo.app.extension.PageRequestImpl;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.extension.index.query.QueryFactory;
|
||||
import run.halo.app.extension.router.selector.FieldSelector;
|
||||
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
|
||||
import run.halo.app.infra.SystemSetting;
|
||||
import run.halo.app.infra.exception.NotFoundException;
|
||||
|
@ -95,9 +100,14 @@ public class TagPostRouteFactory implements RouteFactory {
|
|||
}
|
||||
|
||||
private Mono<TagVo> tagBySlug(String slug) {
|
||||
return client.list(Tag.class, tag -> tag.getSpec().getSlug().equals(slug)
|
||||
&& tag.getMetadata().getDeletionTimestamp() == null, null)
|
||||
.next()
|
||||
var listOptions = new ListOptions();
|
||||
listOptions.setFieldSelector(FieldSelector.of(
|
||||
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()))
|
||||
.switchIfEmpty(
|
||||
Mono.error(new NotFoundException("Tag not found with slug: " + slug)));
|
||||
|
|
|
@ -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'");
|
||||
}
|
||||
}
|
|
@ -3,6 +3,7 @@ package run.halo.app.content;
|
|||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
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.MultiValueMap;
|
||||
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}.
|
||||
|
@ -32,61 +33,20 @@ class PostQueryTest {
|
|||
.build();
|
||||
|
||||
PostQuery postQuery = new PostQuery(request, "faker");
|
||||
var spec = new Post.PostSpec();
|
||||
var post = new Post();
|
||||
post.setSpec(spec);
|
||||
|
||||
spec.setOwner("another-faker");
|
||||
assertThat(postQuery.toPredicate().test(post)).isFalse();
|
||||
var listOptions = postQuery.toListOptions();
|
||||
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");
|
||||
assertThat(postQuery.toPredicate().test(post)).isTrue();
|
||||
}
|
||||
|
||||
@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();
|
||||
entry = List.of(Map.entry("another-faker", "user1"));
|
||||
indexView = new QueryIndexViewImpl(Map.of("spec.owner", entry, "metadata.name", nameEntry));
|
||||
assertThat(listOptions.getFieldSelector().query().matches(indexView)).isEmpty();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,18 +10,19 @@ import static org.mockito.Mockito.verify;
|
|||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.json.JSONException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import run.halo.app.content.TestPost;
|
||||
import run.halo.app.content.permalinks.CategoryPermalinkPolicy;
|
||||
import run.halo.app.core.extension.content.Category;
|
||||
import run.halo.app.core.extension.content.Post;
|
||||
import run.halo.app.extension.ExtensionClient;
|
||||
import run.halo.app.extension.ListOptions;
|
||||
import run.halo.app.extension.Metadata;
|
||||
import run.halo.app.extension.controller.Reconciler;
|
||||
|
||||
|
@ -43,7 +44,7 @@ class CategoryReconcilerTest {
|
|||
private CategoryReconciler categoryReconciler;
|
||||
|
||||
@Test
|
||||
void reconcileStatusPostForCategoryA() throws JSONException {
|
||||
void reconcileStatusPostForCategoryA() {
|
||||
reconcileStatusPostPilling("category-A");
|
||||
|
||||
ArgumentCaptor<Category> captor = ArgumentCaptor.forClass(Category.class);
|
||||
|
@ -54,7 +55,7 @@ class CategoryReconcilerTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
void reconcileStatusPostForCategoryB() throws JSONException {
|
||||
void reconcileStatusPostForCategoryB() {
|
||||
reconcileStatusPostPilling("category-B");
|
||||
ArgumentCaptor<Category> captor = ArgumentCaptor.forClass(Category.class);
|
||||
verify(client, times(3)).update(captor.capture());
|
||||
|
@ -64,7 +65,7 @@ class CategoryReconcilerTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
void reconcileStatusPostForCategoryC() throws JSONException {
|
||||
void reconcileStatusPostForCategoryC() {
|
||||
reconcileStatusPostPilling("category-C");
|
||||
ArgumentCaptor<Category> captor = ArgumentCaptor.forClass(Category.class);
|
||||
verify(client, times(3)).update(captor.capture());
|
||||
|
@ -74,7 +75,7 @@ class CategoryReconcilerTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
void reconcileStatusPostForCategoryD() throws JSONException {
|
||||
void reconcileStatusPostForCategoryD() {
|
||||
reconcileStatusPostPilling("category-D");
|
||||
ArgumentCaptor<Category> captor = ArgumentCaptor.forClass(Category.class);
|
||||
verify(client, times(3)).update(captor.capture());
|
||||
|
@ -89,9 +90,9 @@ class CategoryReconcilerTest {
|
|||
.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());
|
||||
lenient().when(client.list(eq(Category.class), any(), any()))
|
||||
lenient().when(client.listAll(eq(Category.class), any(), any()))
|
||||
.thenReturn(categories());
|
||||
|
||||
Reconciler.Result result =
|
||||
|
|
|
@ -85,7 +85,7 @@ class PostReconcilerTest {
|
|||
Snapshot snapshotV2 = TestPost.snapshotV2();
|
||||
snapshotV1.getSpec().setContributors(Set.of("guqing"));
|
||||
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));
|
||||
|
||||
ArgumentCaptor<Post> captor = ArgumentCaptor.forClass(Post.class);
|
||||
|
@ -126,7 +126,7 @@ class PostReconcilerTest {
|
|||
Snapshot snapshotV1 = TestPost.snapshotV1();
|
||||
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));
|
||||
|
||||
ArgumentCaptor<Post> captor = ArgumentCaptor.forClass(Post.class);
|
||||
|
@ -162,7 +162,7 @@ class PostReconcilerTest {
|
|||
when(client.fetch(eq(Snapshot.class), eq(post.getSpec().getReleaseSnapshot())))
|
||||
.thenReturn(Optional.of(snapshotV2));
|
||||
|
||||
when(client.list(eq(Snapshot.class), any(), any()))
|
||||
when(client.listAll(eq(Snapshot.class), any(), any()))
|
||||
.thenReturn(List.of());
|
||||
|
||||
ArgumentCaptor<Post> captor = ArgumentCaptor.forClass(Post.class);
|
||||
|
@ -191,7 +191,7 @@ class PostReconcilerTest {
|
|||
.rawType("markdown")
|
||||
.build()));
|
||||
|
||||
when(client.list(eq(Snapshot.class), any(), any()))
|
||||
when(client.listAll(eq(Snapshot.class), any(), any()))
|
||||
.thenReturn(List.of());
|
||||
|
||||
ArgumentCaptor<Post> captor = ArgumentCaptor.forClass(Post.class);
|
||||
|
|
|
@ -95,7 +95,7 @@ class SinglePageReconcilerTest {
|
|||
Snapshot snapshotV2 = TestPost.snapshotV2();
|
||||
snapshotV1.getSpec().setContributors(Set.of("guqing"));
|
||||
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));
|
||||
when(externalUrlSupplier.get()).thenReturn(URI.create(""));
|
||||
|
||||
|
@ -156,7 +156,7 @@ class SinglePageReconcilerTest {
|
|||
when(client.fetch(eq(Snapshot.class), eq(page.getSpec().getReleaseSnapshot())))
|
||||
.thenReturn(Optional.of(snapshotV2));
|
||||
|
||||
when(client.list(eq(Snapshot.class), any(), any()))
|
||||
when(client.listAll(eq(Snapshot.class), any(), any()))
|
||||
.thenReturn(List.of());
|
||||
|
||||
ArgumentCaptor<SinglePage> captor = ArgumentCaptor.forClass(SinglePage.class);
|
||||
|
@ -186,7 +186,7 @@ class SinglePageReconcilerTest {
|
|||
.build())
|
||||
);
|
||||
|
||||
when(client.list(eq(Snapshot.class), any(), any()))
|
||||
when(client.listAll(eq(Snapshot.class), any(), any()))
|
||||
.thenReturn(List.of());
|
||||
|
||||
ArgumentCaptor<SinglePage> captor = ArgumentCaptor.forClass(SinglePage.class);
|
||||
|
|
|
@ -10,15 +10,14 @@ import static org.mockito.Mockito.when;
|
|||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import run.halo.app.content.PostIndexInformer;
|
||||
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.extension.ExtensionClient;
|
||||
import run.halo.app.extension.Metadata;
|
||||
|
@ -37,9 +36,6 @@ class TagReconcilerTest {
|
|||
@Mock
|
||||
private TagPermalinkPolicy tagPermalinkPolicy;
|
||||
|
||||
@Mock
|
||||
private PostIndexInformer postIndexInformer;
|
||||
|
||||
@InjectMocks
|
||||
private TagReconciler tagReconciler;
|
||||
|
||||
|
@ -48,8 +44,7 @@ class TagReconcilerTest {
|
|||
Tag tag = tag();
|
||||
when(client.fetch(eq(Tag.class), eq("fake-tag")))
|
||||
.thenReturn(Optional.of(tag));
|
||||
when(postIndexInformer.getByTagName(eq("fake-tag")))
|
||||
.thenReturn(Set.of());
|
||||
when(client.listAll(eq(Post.class), any(), any())).thenReturn(List.of());
|
||||
when(tagPermalinkPolicy.permalink(any()))
|
||||
.thenAnswer(arg -> "/tags/" + tag.getSpec().getSlug());
|
||||
ArgumentCaptor<Tag> captor = ArgumentCaptor.forClass(Tag.class);
|
||||
|
@ -85,8 +80,8 @@ class TagReconcilerTest {
|
|||
Tag tag = tag();
|
||||
when(client.fetch(eq(Tag.class), eq("fake-tag")))
|
||||
.thenReturn(Optional.of(tag));
|
||||
when(postIndexInformer.getByTagName(eq("fake-tag")))
|
||||
.thenReturn(Set.of("fake-post-1", "fake-post-3"));
|
||||
when(client.listAll(eq(Post.class), any(), any()))
|
||||
.thenReturn(List.of(createPost("fake-post-1"), createPost("fake-post-2")));
|
||||
|
||||
ArgumentCaptor<Tag> captor = ArgumentCaptor.forClass(Tag.class);
|
||||
tagReconciler.reconcile(new TagReconciler.Request("fake-tag"));
|
||||
|
@ -96,6 +91,14 @@ class TagReconcilerTest {
|
|||
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 = new Tag();
|
||||
tag.setMetadata(new Metadata());
|
||||
|
|
|
@ -6,6 +6,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
|||
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class ListResultTest {
|
||||
|
@ -53,6 +54,15 @@ class ListResultTest {
|
|||
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) {
|
||||
var result = ListResult.subList(list, 0, 0);
|
||||
assertEquals(list, result);
|
||||
|
|
|
@ -2,7 +2,6 @@ package run.halo.app.theme.endpoint;
|
|||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
|
@ -19,8 +18,10 @@ import org.springframework.web.reactive.function.server.ServerResponse;
|
|||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.extension.content.Category;
|
||||
import run.halo.app.extension.GroupVersion;
|
||||
import run.halo.app.extension.ListOptions;
|
||||
import run.halo.app.extension.ListResult;
|
||||
import run.halo.app.extension.Metadata;
|
||||
import run.halo.app.extension.PageRequest;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.theme.finders.PostPublicQueryService;
|
||||
import run.halo.app.theme.finders.vo.ListedPostVo;
|
||||
|
@ -52,7 +53,7 @@ class CategoryQueryEndpointTest {
|
|||
@Test
|
||||
void listCategories() {
|
||||
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));
|
||||
|
||||
webTestClient.get()
|
||||
|
@ -84,7 +85,7 @@ class CategoryQueryEndpointTest {
|
|||
@Test
|
||||
void listPostsByCategoryName() {
|
||||
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));
|
||||
|
||||
webTestClient.get()
|
||||
|
|
|
@ -2,7 +2,6 @@ package run.halo.app.theme.endpoint;
|
|||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.verify;
|
||||
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.ListResult;
|
||||
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.PostPublicQueryService;
|
||||
import run.halo.app.theme.finders.vo.ListedPostVo;
|
||||
|
@ -55,7 +55,7 @@ class PostQueryEndpointTest {
|
|||
@Test
|
||||
public void listPosts() {
|
||||
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));
|
||||
|
||||
webClient.get().uri("/posts")
|
||||
|
@ -65,7 +65,7 @@ class PostQueryEndpointTest {
|
|||
.expectBody()
|
||||
.jsonPath("$.items").isArray();
|
||||
|
||||
verify(postPublicQueryService).list(anyInt(), anyInt(), any(), any());
|
||||
verify(postPublicQueryService).list(any(), any(PageRequest.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -2,7 +2,6 @@ package run.halo.app.theme.finders.impl;
|
|||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
|
@ -18,11 +17,14 @@ import org.junit.jupiter.api.extension.ExtendWith;
|
|||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.skyscreamer.jsonassert.JSONAssert;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
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.Metadata;
|
||||
import run.halo.app.extension.PageRequest;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.infra.utils.JsonUtils;
|
||||
import run.halo.app.theme.finders.vo.CategoryTreeVo;
|
||||
|
@ -85,7 +87,7 @@ class CategoryFinderImplTest {
|
|||
categories().stream()
|
||||
.sorted(CategoryFinderImpl.defaultComparator())
|
||||
.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));
|
||||
ListResult<CategoryVo> list = categoryFinder.list(1, 10).block();
|
||||
assertThat(list.getItems()).hasSize(3);
|
||||
|
@ -95,7 +97,7 @@ class CategoryFinderImplTest {
|
|||
|
||||
@Test
|
||||
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()));
|
||||
List<CategoryTreeVo> treeVos = categoryFinder.listAsTree().collectList().block();
|
||||
assertThat(treeVos).hasSize(1);
|
||||
|
@ -103,7 +105,7 @@ class CategoryFinderImplTest {
|
|||
|
||||
@Test
|
||||
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()));
|
||||
List<CategoryTreeVo> treeVos = categoryFinder.listAsTree("E").collectList().block();
|
||||
assertThat(treeVos.get(0).getMetadata().getName()).isEqualTo("E");
|
||||
|
@ -119,7 +121,7 @@ class CategoryFinderImplTest {
|
|||
*/
|
||||
@Test
|
||||
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()));
|
||||
List<CategoryTreeVo> treeVos = categoryFinder.listAsTree().collectList().block();
|
||||
String s = visualizeTree(treeVos);
|
||||
|
|
|
@ -2,7 +2,6 @@ package run.halo.app.theme.finders.impl;
|
|||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.time.Instant;
|
||||
|
@ -11,8 +10,6 @@ import java.util.HashMap;
|
|||
import java.util.List;
|
||||
import java.util.Map;
|
||||
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.extension.ExtendWith;
|
||||
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.extension.ListResult;
|
||||
import run.halo.app.extension.Metadata;
|
||||
import run.halo.app.extension.PageRequest;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.metrics.CounterService;
|
||||
import run.halo.app.theme.finders.CategoryFinder;
|
||||
|
@ -67,15 +65,6 @@ class PostFinderImplTest {
|
|||
@InjectMocks
|
||||
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
|
||||
void predicate() {
|
||||
Predicate<Post> predicate = new DefaultQueryPostPredicateResolver().getPredicate().block();
|
||||
|
@ -93,7 +82,7 @@ class PostFinderImplTest {
|
|||
.map(ListedPostVo::from)
|
||||
.toList();
|
||||
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));
|
||||
|
||||
ListResult<PostArchiveVo> archives = postFinder.archives(1, 10).block();
|
||||
|
@ -112,22 +101,6 @@ class PostFinderImplTest {
|
|||
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
|
||||
void postPreviousNextPair() {
|
||||
List<String> postNames = new ArrayList<>();
|
||||
|
@ -136,28 +109,27 @@ class PostFinderImplTest {
|
|||
}
|
||||
|
||||
// post-0, post-1, post-2
|
||||
Pair<String, String> previousNextPair =
|
||||
PostFinderImpl.postPreviousNextPair(postNames, "post-0");
|
||||
assertThat(previousNextPair.getLeft()).isNull();
|
||||
assertThat(previousNextPair.getRight()).isEqualTo("post-1");
|
||||
var previousNextPair = PostFinderImpl.findPostNavigation(postNames, "post-0");
|
||||
assertThat(previousNextPair.prev()).isNull();
|
||||
assertThat(previousNextPair.next()).isEqualTo("post-1");
|
||||
|
||||
previousNextPair = PostFinderImpl.postPreviousNextPair(postNames, "post-1");
|
||||
assertThat(previousNextPair.getLeft()).isEqualTo("post-0");
|
||||
assertThat(previousNextPair.getRight()).isEqualTo("post-2");
|
||||
previousNextPair = PostFinderImpl.findPostNavigation(postNames, "post-1");
|
||||
assertThat(previousNextPair.prev()).isEqualTo("post-0");
|
||||
assertThat(previousNextPair.next()).isEqualTo("post-2");
|
||||
|
||||
// post-1, post-2, post-3
|
||||
previousNextPair = PostFinderImpl.postPreviousNextPair(postNames, "post-2");
|
||||
assertThat(previousNextPair.getLeft()).isEqualTo("post-1");
|
||||
assertThat(previousNextPair.getRight()).isEqualTo("post-3");
|
||||
previousNextPair = PostFinderImpl.findPostNavigation(postNames, "post-2");
|
||||
assertThat(previousNextPair.prev()).isEqualTo("post-1");
|
||||
assertThat(previousNextPair.next()).isEqualTo("post-3");
|
||||
|
||||
// post-7, post-8, post-9
|
||||
previousNextPair = PostFinderImpl.postPreviousNextPair(postNames, "post-8");
|
||||
assertThat(previousNextPair.getLeft()).isEqualTo("post-7");
|
||||
assertThat(previousNextPair.getRight()).isEqualTo("post-9");
|
||||
previousNextPair = PostFinderImpl.findPostNavigation(postNames, "post-8");
|
||||
assertThat(previousNextPair.prev()).isEqualTo("post-7");
|
||||
assertThat(previousNextPair.next()).isEqualTo("post-9");
|
||||
|
||||
previousNextPair = PostFinderImpl.postPreviousNextPair(postNames, "post-9");
|
||||
assertThat(previousNextPair.getLeft()).isEqualTo("post-8");
|
||||
assertThat(previousNextPair.getRight()).isNull();
|
||||
previousNextPair = PostFinderImpl.findPostNavigation(postNames, "post-9");
|
||||
assertThat(previousNextPair.prev()).isEqualTo("post-8");
|
||||
assertThat(previousNextPair.next()).isNull();
|
||||
}
|
||||
|
||||
List<Post> postsForArchives() {
|
||||
|
|
|
@ -16,9 +16,11 @@ import org.junit.jupiter.api.extension.ExtendWith;
|
|||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.skyscreamer.jsonassert.JSONAssert;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
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.ReactiveExtensionClient;
|
||||
import run.halo.app.infra.utils.JsonUtils;
|
||||
|
@ -77,7 +79,7 @@ class TagFinderImplTest {
|
|||
|
||||
@Test
|
||||
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(
|
||||
tags().stream().sorted(TagFinderImpl.DEFAULT_COMPARATOR.reversed()).toList()
|
||||
)
|
||||
|
|
|
@ -4,16 +4,18 @@ import static org.mockito.ArgumentMatchers.any;
|
|||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
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.PageRequest;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.theme.finders.PostFinder;
|
||||
import run.halo.app.theme.finders.TagFinder;
|
||||
|
@ -39,7 +41,8 @@ class TagPostRouteFactoryTest extends RouteFactoryTestSuite {
|
|||
|
||||
@Test
|
||||
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.get()
|
||||
|
@ -52,7 +55,8 @@ class TagPostRouteFactoryTest extends RouteFactoryTestSuite {
|
|||
tag.getMetadata().setName("fake-tag-name");
|
||||
tag.setSpec(new Tag.TagSpec());
|
||||
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())))
|
||||
.thenReturn(Mono.just(TagVo.from(tag)));
|
||||
webTestClient.get()
|
||||
|
|
|
@ -59,11 +59,11 @@ const {
|
|||
return data.items;
|
||||
},
|
||||
refetchInterval: (data) => {
|
||||
const deletingPosts = data?.filter(
|
||||
const deletingPosts = data?.some(
|
||||
(post) =>
|
||||
!!post.post.metadata.deletionTimestamp || !post.post.spec.deleted
|
||||
);
|
||||
return deletingPosts?.length ? 1000 : false;
|
||||
return deletingPosts ? 1000 : false;
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -113,21 +113,23 @@ const {
|
|||
keyword,
|
||||
],
|
||||
queryFn: async () => {
|
||||
let categories: string[] | undefined;
|
||||
let tags: string[] | undefined;
|
||||
let contributors: string[] | undefined;
|
||||
const labelSelector: string[] = ["content.halo.run/deleted=false"];
|
||||
const fieldSelector: string[] = [];
|
||||
|
||||
if (selectedCategory.value) {
|
||||
categories = [selectedCategory.value];
|
||||
fieldSelector.push(`spec.categories=${selectedCategory.value}`);
|
||||
}
|
||||
|
||||
if (selectedTag.value) {
|
||||
tags = [selectedTag.value];
|
||||
fieldSelector.push(`spec.tags=${selectedTag.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) {
|
||||
|
@ -138,14 +140,11 @@ const {
|
|||
|
||||
const { data } = await apiClient.post.listPosts({
|
||||
labelSelector,
|
||||
fieldSelector,
|
||||
page: page.value,
|
||||
size: size.value,
|
||||
visible: selectedVisible.value,
|
||||
sort: [selectedSort.value].filter(Boolean) as string[],
|
||||
keyword: keyword.value,
|
||||
category: categories,
|
||||
tag: tags,
|
||||
contributor: contributors,
|
||||
});
|
||||
|
||||
total.value = data.total;
|
||||
|
@ -155,7 +154,7 @@ const {
|
|||
return data.items;
|
||||
},
|
||||
refetchInterval: (data) => {
|
||||
const abnormalPosts = data?.filter((post) => {
|
||||
const abnormalPosts = data?.some((post) => {
|
||||
const { spec, metadata, status } = post.post;
|
||||
return (
|
||||
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'),
|
||||
value: 'publishTime,desc',
|
||||
value: 'spec.publishTime,desc',
|
||||
},
|
||||
{
|
||||
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'),
|
||||
value: 'creationTimestamp,desc',
|
||||
value: 'metadata.creationTimestamp,desc',
|
||||
},
|
||||
{
|
||||
label: t('core.post.filters.sort.items.create_time_asc'),
|
||||
value: 'creationTimestamp,asc',
|
||||
value: 'metadata.creationTimestamp,asc',
|
||||
},
|
||||
]"
|
||||
/>
|
||||
|
|
|
@ -32,6 +32,7 @@ export function usePostCategory(): usePostCategoryReturn {
|
|||
await apiClient.extension.category.listcontentHaloRunV1alpha1Category({
|
||||
page: 0,
|
||||
size: 0,
|
||||
sort: ["metadata.creationTimestamp,desc"],
|
||||
});
|
||||
|
||||
return data.items;
|
||||
|
|
|
@ -26,6 +26,7 @@ export function usePostTag(): usePostTagReturn {
|
|||
await apiClient.extension.tag.listcontentHaloRunV1alpha1Tag({
|
||||
page: 0,
|
||||
size: 0,
|
||||
sort: ["metadata.creationTimestamp,desc"],
|
||||
});
|
||||
|
||||
return data.items;
|
||||
|
|
|
@ -21,7 +21,7 @@ const { data } = useQuery<ListedPost[]>({
|
|||
`${postLabels.DELETED}=false`,
|
||||
`${postLabels.PUBLISHED}=true`,
|
||||
],
|
||||
sort: ["publishTime,desc"],
|
||||
sort: ["spec.publishTime,desc"],
|
||||
page: 1,
|
||||
size: 10,
|
||||
});
|
||||
|
|
|
@ -222,8 +222,6 @@ export const ApiConsoleHaloRunV1alpha1PostApiAxiosParamCreator = function (
|
|||
},
|
||||
/**
|
||||
* List posts.
|
||||
* @param {Array<string>} [category]
|
||||
* @param {Array<string>} [contributor]
|
||||
* @param {Array<string>} [fieldSelector] Field selector for filtering.
|
||||
* @param {string} [keyword] Posts filtered by keyword.
|
||||
* @param {Array<string>} [labelSelector] Label selector for filtering.
|
||||
|
@ -231,14 +229,10 @@ export const ApiConsoleHaloRunV1alpha1PostApiAxiosParamCreator = function (
|
|||
* @param {'DRAFT' | 'PENDING_APPROVAL' | 'PUBLISHED' | 'FAILED'} [publishPhase]
|
||||
* @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>} [tag]
|
||||
* @param {'PUBLIC' | 'INTERNAL' | 'PRIVATE'} [visible]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
listPosts: async (
|
||||
category?: Array<string>,
|
||||
contributor?: Array<string>,
|
||||
fieldSelector?: Array<string>,
|
||||
keyword?: string,
|
||||
labelSelector?: Array<string>,
|
||||
|
@ -246,8 +240,6 @@ export const ApiConsoleHaloRunV1alpha1PostApiAxiosParamCreator = function (
|
|||
publishPhase?: "DRAFT" | "PENDING_APPROVAL" | "PUBLISHED" | "FAILED",
|
||||
size?: number,
|
||||
sort?: Array<string>,
|
||||
tag?: Array<string>,
|
||||
visible?: "PUBLIC" | "INTERNAL" | "PRIVATE",
|
||||
options: AxiosRequestConfig = {}
|
||||
): Promise<RequestArgs> => {
|
||||
const localVarPath = `/apis/api.console.halo.run/v1alpha1/posts`;
|
||||
|
@ -274,14 +266,6 @@ export const ApiConsoleHaloRunV1alpha1PostApiAxiosParamCreator = function (
|
|||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration);
|
||||
|
||||
if (category) {
|
||||
localVarQueryParameter["category"] = Array.from(category);
|
||||
}
|
||||
|
||||
if (contributor) {
|
||||
localVarQueryParameter["contributor"] = Array.from(contributor);
|
||||
}
|
||||
|
||||
if (fieldSelector) {
|
||||
localVarQueryParameter["fieldSelector"] = fieldSelector;
|
||||
}
|
||||
|
@ -310,14 +294,6 @@ export const ApiConsoleHaloRunV1alpha1PostApiAxiosParamCreator = function (
|
|||
localVarQueryParameter["sort"] = Array.from(sort);
|
||||
}
|
||||
|
||||
if (tag) {
|
||||
localVarQueryParameter["tag"] = Array.from(tag);
|
||||
}
|
||||
|
||||
if (visible !== undefined) {
|
||||
localVarQueryParameter["visible"] = visible;
|
||||
}
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions =
|
||||
baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
|
@ -710,8 +686,6 @@ export const ApiConsoleHaloRunV1alpha1PostApiFp = function (
|
|||
},
|
||||
/**
|
||||
* List posts.
|
||||
* @param {Array<string>} [category]
|
||||
* @param {Array<string>} [contributor]
|
||||
* @param {Array<string>} [fieldSelector] Field selector for filtering.
|
||||
* @param {string} [keyword] Posts filtered by keyword.
|
||||
* @param {Array<string>} [labelSelector] Label selector for filtering.
|
||||
|
@ -719,14 +693,10 @@ export const ApiConsoleHaloRunV1alpha1PostApiFp = function (
|
|||
* @param {'DRAFT' | 'PENDING_APPROVAL' | 'PUBLISHED' | 'FAILED'} [publishPhase]
|
||||
* @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>} [tag]
|
||||
* @param {'PUBLIC' | 'INTERNAL' | 'PRIVATE'} [visible]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async listPosts(
|
||||
category?: Array<string>,
|
||||
contributor?: Array<string>,
|
||||
fieldSelector?: Array<string>,
|
||||
keyword?: string,
|
||||
labelSelector?: Array<string>,
|
||||
|
@ -734,15 +704,11 @@ export const ApiConsoleHaloRunV1alpha1PostApiFp = function (
|
|||
publishPhase?: "DRAFT" | "PENDING_APPROVAL" | "PUBLISHED" | "FAILED",
|
||||
size?: number,
|
||||
sort?: Array<string>,
|
||||
tag?: Array<string>,
|
||||
visible?: "PUBLIC" | "INTERNAL" | "PRIVATE",
|
||||
options?: AxiosRequestConfig
|
||||
): Promise<
|
||||
(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ListedPostList>
|
||||
> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.listPosts(
|
||||
category,
|
||||
contributor,
|
||||
fieldSelector,
|
||||
keyword,
|
||||
labelSelector,
|
||||
|
@ -750,8 +716,6 @@ export const ApiConsoleHaloRunV1alpha1PostApiFp = function (
|
|||
publishPhase,
|
||||
size,
|
||||
sort,
|
||||
tag,
|
||||
visible,
|
||||
options
|
||||
);
|
||||
return createRequestFunction(
|
||||
|
@ -954,8 +918,6 @@ export const ApiConsoleHaloRunV1alpha1PostApiFactory = function (
|
|||
): AxiosPromise<ListedPostList> {
|
||||
return localVarFp
|
||||
.listPosts(
|
||||
requestParameters.category,
|
||||
requestParameters.contributor,
|
||||
requestParameters.fieldSelector,
|
||||
requestParameters.keyword,
|
||||
requestParameters.labelSelector,
|
||||
|
@ -963,8 +925,6 @@ export const ApiConsoleHaloRunV1alpha1PostApiFactory = function (
|
|||
requestParameters.publishPhase,
|
||||
requestParameters.size,
|
||||
requestParameters.sort,
|
||||
requestParameters.tag,
|
||||
requestParameters.visible,
|
||||
options
|
||||
)
|
||||
.then((request) => request(axios, basePath));
|
||||
|
@ -1102,20 +1062,6 @@ export interface ApiConsoleHaloRunV1alpha1PostApiFetchPostReleaseContentRequest
|
|||
* @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.
|
||||
* @type {Array<string>}
|
||||
|
@ -1164,20 +1110,6 @@ export interface ApiConsoleHaloRunV1alpha1PostApiListPostsRequest {
|
|||
* @memberof ApiConsoleHaloRunV1alpha1PostApiListPosts
|
||||
*/
|
||||
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)
|
||||
.listPosts(
|
||||
requestParameters.category,
|
||||
requestParameters.contributor,
|
||||
requestParameters.fieldSelector,
|
||||
requestParameters.keyword,
|
||||
requestParameters.labelSelector,
|
||||
|
@ -1348,8 +1278,6 @@ export class ApiConsoleHaloRunV1alpha1PostApi extends BaseAPI {
|
|||
requestParameters.publishPhase,
|
||||
requestParameters.size,
|
||||
requestParameters.sort,
|
||||
requestParameters.tag,
|
||||
requestParameters.visible,
|
||||
options
|
||||
)
|
||||
.then((request) => request(this.axios, this.basePath));
|
||||
|
|
|
@ -222,8 +222,6 @@ export const UcApiContentHaloRunV1alpha1PostApiAxiosParamCreator = function (
|
|||
},
|
||||
/**
|
||||
* List posts owned by the current user.
|
||||
* @param {Array<string>} [category]
|
||||
* @param {Array<string>} [contributor]
|
||||
* @param {Array<string>} [fieldSelector] Field selector for filtering.
|
||||
* @param {string} [keyword] Posts filtered by keyword.
|
||||
* @param {Array<string>} [labelSelector] Label selector for filtering.
|
||||
|
@ -231,14 +229,10 @@ export const UcApiContentHaloRunV1alpha1PostApiAxiosParamCreator = function (
|
|||
* @param {'DRAFT' | 'PENDING_APPROVAL' | 'PUBLISHED' | 'FAILED'} [publishPhase]
|
||||
* @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>} [tag]
|
||||
* @param {'PUBLIC' | 'INTERNAL' | 'PRIVATE'} [visible]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
listMyPosts: async (
|
||||
category?: Array<string>,
|
||||
contributor?: Array<string>,
|
||||
fieldSelector?: Array<string>,
|
||||
keyword?: string,
|
||||
labelSelector?: Array<string>,
|
||||
|
@ -246,8 +240,6 @@ export const UcApiContentHaloRunV1alpha1PostApiAxiosParamCreator = function (
|
|||
publishPhase?: "DRAFT" | "PENDING_APPROVAL" | "PUBLISHED" | "FAILED",
|
||||
size?: number,
|
||||
sort?: Array<string>,
|
||||
tag?: Array<string>,
|
||||
visible?: "PUBLIC" | "INTERNAL" | "PRIVATE",
|
||||
options: AxiosRequestConfig = {}
|
||||
): Promise<RequestArgs> => {
|
||||
const localVarPath = `/apis/uc.api.content.halo.run/v1alpha1/posts`;
|
||||
|
@ -274,14 +266,6 @@ export const UcApiContentHaloRunV1alpha1PostApiAxiosParamCreator = function (
|
|||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration);
|
||||
|
||||
if (category) {
|
||||
localVarQueryParameter["category"] = Array.from(category);
|
||||
}
|
||||
|
||||
if (contributor) {
|
||||
localVarQueryParameter["contributor"] = Array.from(contributor);
|
||||
}
|
||||
|
||||
if (fieldSelector) {
|
||||
localVarQueryParameter["fieldSelector"] = fieldSelector;
|
||||
}
|
||||
|
@ -310,14 +294,6 @@ export const UcApiContentHaloRunV1alpha1PostApiAxiosParamCreator = function (
|
|||
localVarQueryParameter["sort"] = Array.from(sort);
|
||||
}
|
||||
|
||||
if (tag) {
|
||||
localVarQueryParameter["tag"] = Array.from(tag);
|
||||
}
|
||||
|
||||
if (visible !== undefined) {
|
||||
localVarQueryParameter["visible"] = visible;
|
||||
}
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions =
|
||||
baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
|
@ -653,8 +629,6 @@ export const UcApiContentHaloRunV1alpha1PostApiFp = function (
|
|||
},
|
||||
/**
|
||||
* List posts owned by the current user.
|
||||
* @param {Array<string>} [category]
|
||||
* @param {Array<string>} [contributor]
|
||||
* @param {Array<string>} [fieldSelector] Field selector for filtering.
|
||||
* @param {string} [keyword] Posts filtered by keyword.
|
||||
* @param {Array<string>} [labelSelector] Label selector for filtering.
|
||||
|
@ -662,14 +636,10 @@ export const UcApiContentHaloRunV1alpha1PostApiFp = function (
|
|||
* @param {'DRAFT' | 'PENDING_APPROVAL' | 'PUBLISHED' | 'FAILED'} [publishPhase]
|
||||
* @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>} [tag]
|
||||
* @param {'PUBLIC' | 'INTERNAL' | 'PRIVATE'} [visible]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async listMyPosts(
|
||||
category?: Array<string>,
|
||||
contributor?: Array<string>,
|
||||
fieldSelector?: Array<string>,
|
||||
keyword?: string,
|
||||
labelSelector?: Array<string>,
|
||||
|
@ -677,15 +647,11 @@ export const UcApiContentHaloRunV1alpha1PostApiFp = function (
|
|||
publishPhase?: "DRAFT" | "PENDING_APPROVAL" | "PUBLISHED" | "FAILED",
|
||||
size?: number,
|
||||
sort?: Array<string>,
|
||||
tag?: Array<string>,
|
||||
visible?: "PUBLIC" | "INTERNAL" | "PRIVATE",
|
||||
options?: AxiosRequestConfig
|
||||
): Promise<
|
||||
(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ListedPostList>
|
||||
> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.listMyPosts(
|
||||
category,
|
||||
contributor,
|
||||
fieldSelector,
|
||||
keyword,
|
||||
labelSelector,
|
||||
|
@ -693,8 +659,6 @@ export const UcApiContentHaloRunV1alpha1PostApiFp = function (
|
|||
publishPhase,
|
||||
size,
|
||||
sort,
|
||||
tag,
|
||||
visible,
|
||||
options
|
||||
);
|
||||
return createRequestFunction(
|
||||
|
@ -875,8 +839,6 @@ export const UcApiContentHaloRunV1alpha1PostApiFactory = function (
|
|||
): AxiosPromise<ListedPostList> {
|
||||
return localVarFp
|
||||
.listMyPosts(
|
||||
requestParameters.category,
|
||||
requestParameters.contributor,
|
||||
requestParameters.fieldSelector,
|
||||
requestParameters.keyword,
|
||||
requestParameters.labelSelector,
|
||||
|
@ -884,8 +846,6 @@ export const UcApiContentHaloRunV1alpha1PostApiFactory = function (
|
|||
requestParameters.publishPhase,
|
||||
requestParameters.size,
|
||||
requestParameters.sort,
|
||||
requestParameters.tag,
|
||||
requestParameters.visible,
|
||||
options
|
||||
)
|
||||
.then((request) => request(axios, basePath));
|
||||
|
@ -1008,20 +968,6 @@ export interface UcApiContentHaloRunV1alpha1PostApiGetMyPostDraftRequest {
|
|||
* @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.
|
||||
* @type {Array<string>}
|
||||
|
@ -1070,20 +1016,6 @@ export interface UcApiContentHaloRunV1alpha1PostApiListMyPostsRequest {
|
|||
* @memberof UcApiContentHaloRunV1alpha1PostApiListMyPosts
|
||||
*/
|
||||
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)
|
||||
.listMyPosts(
|
||||
requestParameters.category,
|
||||
requestParameters.contributor,
|
||||
requestParameters.fieldSelector,
|
||||
requestParameters.keyword,
|
||||
requestParameters.labelSelector,
|
||||
|
@ -1237,8 +1167,6 @@ export class UcApiContentHaloRunV1alpha1PostApi extends BaseAPI {
|
|||
requestParameters.publishPhase,
|
||||
requestParameters.size,
|
||||
requestParameters.sort,
|
||||
requestParameters.tag,
|
||||
requestParameters.visible,
|
||||
options
|
||||
)
|
||||
.then((request) => request(this.axios, this.basePath));
|
||||
|
|
|
@ -34,6 +34,12 @@ export interface PluginStatus {
|
|||
* @memberof PluginStatus
|
||||
*/
|
||||
entry?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof PluginStatus
|
||||
*/
|
||||
lastProbeState?: PluginStatusLastProbeStateEnum;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
|
@ -66,7 +72,7 @@ export interface PluginStatus {
|
|||
stylesheet?: string;
|
||||
}
|
||||
|
||||
export const PluginStatusPhaseEnum = {
|
||||
export const PluginStatusLastProbeStateEnum = {
|
||||
Created: "CREATED",
|
||||
Disabled: "DISABLED",
|
||||
Resolved: "RESOLVED",
|
||||
|
@ -75,5 +81,19 @@ export const PluginStatusPhaseEnum = {
|
|||
Failed: "FAILED",
|
||||
} 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 =
|
||||
(typeof PluginStatusPhaseEnum)[keyof typeof PluginStatusPhaseEnum];
|
||||
|
|
|
@ -149,7 +149,9 @@ const handleBuildSearchIndex = () => {
|
|||
});
|
||||
|
||||
apiClient.extension.category
|
||||
.listcontentHaloRunV1alpha1Category()
|
||||
.listcontentHaloRunV1alpha1Category({
|
||||
sort: ["metadata.creationTimestamp,desc"],
|
||||
})
|
||||
.then((response) => {
|
||||
response.data.items.forEach((category) => {
|
||||
fuse.add({
|
||||
|
@ -168,23 +170,27 @@ const handleBuildSearchIndex = () => {
|
|||
});
|
||||
});
|
||||
|
||||
apiClient.extension.tag.listcontentHaloRunV1alpha1Tag().then((response) => {
|
||||
response.data.items.forEach((tag) => {
|
||||
fuse.add({
|
||||
title: tag.spec.displayName,
|
||||
icon: {
|
||||
component: markRaw(IconBookRead),
|
||||
},
|
||||
group: t("core.components.global_search.groups.tag"),
|
||||
route: {
|
||||
name: "Tags",
|
||||
query: {
|
||||
name: tag.metadata.name,
|
||||
apiClient.extension.tag
|
||||
.listcontentHaloRunV1alpha1Tag({
|
||||
sort: ["metadata.creationTimestamp,desc"],
|
||||
})
|
||||
.then((response) => {
|
||||
response.data.items.forEach((tag) => {
|
||||
fuse.add({
|
||||
title: tag.spec.displayName,
|
||||
icon: {
|
||||
component: markRaw(IconBookRead),
|
||||
},
|
||||
},
|
||||
group: t("core.components.global_search.groups.tag"),
|
||||
route: {
|
||||
name: "Tags",
|
||||
query: {
|
||||
name: tag.metadata.name,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (currentUserHasPermission(["system:singlepages:view"])) {
|
||||
|
|
|
@ -15,7 +15,9 @@ declare module "@formkit/inputs" {
|
|||
function optionsHandler(node: FormKitNode) {
|
||||
node.on("created", async () => {
|
||||
const { data } =
|
||||
await apiClient.extension.category.listcontentHaloRunV1alpha1Category();
|
||||
await apiClient.extension.category.listcontentHaloRunV1alpha1Category({
|
||||
sort: ["metadata.creationTimestamp,desc"],
|
||||
});
|
||||
|
||||
node.props.options = data.items.map((category) => {
|
||||
return {
|
||||
|
|
|
@ -15,7 +15,9 @@ declare module "@formkit/inputs" {
|
|||
function optionsHandler(node: FormKitNode) {
|
||||
node.on("created", async () => {
|
||||
const { data } =
|
||||
await apiClient.extension.tag.listcontentHaloRunV1alpha1Tag();
|
||||
await apiClient.extension.tag.listcontentHaloRunV1alpha1Tag({
|
||||
sort: ["metadata.creationTimestamp,desc"],
|
||||
});
|
||||
|
||||
node.props.options = data.items.map((tag) => {
|
||||
return {
|
||||
|
|
|
@ -69,7 +69,7 @@ const {
|
|||
size.value = data.size;
|
||||
},
|
||||
refetchInterval: (data) => {
|
||||
const abnormalPosts = data?.items.filter((post) => {
|
||||
const hasAbnormalPost = data?.items.some((post) => {
|
||||
const { spec, metadata, status } = post.post;
|
||||
return (
|
||||
spec.deleted ||
|
||||
|
@ -78,7 +78,7 @@ const {
|
|||
);
|
||||
});
|
||||
|
||||
return abnormalPosts?.length ? 1000 : false;
|
||||
return hasAbnormalPost ? 1000 : false;
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
Loading…
Reference in New Issue