From 7a55860b6925621e13adfba7cb1d1edcfaee4a4e Mon Sep 17 00:00:00 2001 From: guqing Date: Mon, 22 Jan 2024 17:36:15 +0800 Subject: [PATCH 1/9] refactor: using indexes to query post lists --- .../app/core/extension/content/Snapshot.java | 3 + .../router/selector/FieldSelector.java | 13 +- .../router/selector/LabelSelector.java | 16 + .../router/selector/SelectorUtil.java | 2 + .../run/halo/app/content/DefaultIndexer.java | 129 ------ .../java/run/halo/app/content/Indexer.java | 79 ---- .../halo/app/content/PostIndexInformer.java | 221 ----------- .../java/run/halo/app/content/PostQuery.java | 143 ++----- .../app/content/impl/PostServiceImpl.java | 37 +- .../app/content/impl/SnapshotServiceImpl.java | 2 +- .../extension/endpoint/StatsEndpoint.java | 22 +- .../reconciler/CategoryReconciler.java | 13 +- .../extension/reconciler/PostReconciler.java | 21 +- .../reconciler/SinglePageReconciler.java | 18 +- .../extension/reconciler/TagReconciler.java | 39 +- .../app/extension/index/IndexEntryImpl.java | 5 +- .../index/IndexedQueryEngineImpl.java | 20 +- .../run/halo/app/infra/SchemeInitializer.java | 82 +++- .../theme/endpoint/CategoryQueryEndpoint.java | 16 +- .../app/theme/endpoint/PostQueryEndpoint.java | 3 +- .../app/theme/endpoint/TagQueryEndpoint.java | 16 +- .../theme/finders/PostPublicQueryService.java | 18 +- .../theme/finders/impl/PostFinderImpl.java | 253 +++++------- .../impl/PostPublicQueryServiceImpl.java | 38 +- .../finders/impl/SiteStatsFinderImpl.java | 21 +- .../DefaultQueryPostPredicateResolver.java | 29 ++ .../ExtensionPermalinkPatternUpdater.java | 4 +- .../ReactiveQueryPostPredicateResolver.java | 3 + .../router/factories/PostRouteFactory.java | 15 +- .../halo/app/content/DefaultIndexerTest.java | 368 ------------------ .../run/halo/app/content/PostQueryTest.java | 72 +--- .../reconciler/CategoryReconcilerTest.java | 13 +- .../reconciler/PostReconcilerTest.java | 8 +- .../reconciler/SinglePageReconcilerTest.java | 6 +- .../reconciler/TagReconcilerTest.java | 21 +- .../endpoint/CategoryQueryEndpointTest.java | 3 +- .../theme/endpoint/PostQueryEndpointTest.java | 6 +- .../finders/impl/PostFinderImplTest.java | 62 +-- 38 files changed, 548 insertions(+), 1292 deletions(-) delete mode 100644 application/src/main/java/run/halo/app/content/DefaultIndexer.java delete mode 100644 application/src/main/java/run/halo/app/content/Indexer.java delete mode 100644 application/src/main/java/run/halo/app/content/PostIndexInformer.java delete mode 100644 application/src/test/java/run/halo/app/content/DefaultIndexerTest.java diff --git a/api/src/main/java/run/halo/app/core/extension/content/Snapshot.java b/api/src/main/java/run/halo/app/core/extension/content/Snapshot.java index 0694a23b82..3e58127913 100644 --- a/api/src/main/java/run/halo/app/core/extension/content/Snapshot.java +++ b/api/src/main/java/run/halo/app/core/extension/content/Snapshot.java @@ -84,4 +84,7 @@ public static boolean isBaseSnapshot(@NonNull Snapshot snapshot) { return Boolean.parseBoolean(annotations.get(Snapshot.KEEP_RAW_ANNO)); } + public static String toSubjectRefKey(Ref subjectRef) { + return subjectRef.getGroup() + "/" + subjectRef.getKind() + "/" + subjectRef.getName(); + } } diff --git a/api/src/main/java/run/halo/app/extension/router/selector/FieldSelector.java b/api/src/main/java/run/halo/app/extension/router/selector/FieldSelector.java index a0a90252a9..44d261f97c 100644 --- a/api/src/main/java/run/halo/app/extension/router/selector/FieldSelector.java +++ b/api/src/main/java/run/halo/app/extension/router/selector/FieldSelector.java @@ -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 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)); + } } diff --git a/api/src/main/java/run/halo/app/extension/router/selector/LabelSelector.java b/api/src/main/java/run/halo/app/extension/router/selector/LabelSelector.java index 82be7dfab9..e2cead3b47 100644 --- a/api/src/main/java/run/halo/app/extension/router/selector/LabelSelector.java +++ b/api/src/main/java/run/halo/app/extension/router/selector/LabelSelector.java @@ -24,6 +24,22 @@ public boolean test(@NonNull Map labels) { .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(); + matchers.addAll(this.matchers); + matchers.addAll(other.matchers); + labelSelector.setMatchers(matchers); + return labelSelector; + } + public static LabelSelectorBuilder builder() { return new LabelSelectorBuilder(); } diff --git a/api/src/main/java/run/halo/app/extension/router/selector/SelectorUtil.java b/api/src/main/java/run/halo/app/extension/router/selector/SelectorUtil.java index bad729cbc2..d5066706b9 100644 --- a/api/src/main/java/run/halo/app/extension/router/selector/SelectorUtil.java +++ b/api/src/main/java/run/halo/app/extension/router/selector/SelectorUtil.java @@ -96,6 +96,8 @@ public static ListOptions labelAndFieldSelectorToListOptions( listOptions.setLabelSelector(new LabelSelector().setMatchers(labelMatchers)); if (!fieldQuery.isEmpty()) { listOptions.setFieldSelector(FieldSelector.of(QueryFactory.and(fieldQuery))); + } else { + listOptions.setFieldSelector(FieldSelector.all()); } return listOptions; } diff --git a/application/src/main/java/run/halo/app/content/DefaultIndexer.java b/application/src/main/java/run/halo/app/content/DefaultIndexer.java deleted file mode 100644 index de47bec441..0000000000 --- a/application/src/main/java/run/halo/app/content/DefaultIndexer.java +++ /dev/null @@ -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; - -/** - *

A default implementation of {@link Indexer}.

- *

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.

- * - * @param the type of object to be indexed - * @author guqing - * @see - * kubernetes index - * @see informer机制之cache.indexer机制 - * @since 2.0.0 - */ -public class DefaultIndexer implements Indexer { - private final Map> indices = new HashMap<>(); - private final Map> indexFuncMap = new HashMap<>(); - private final Map> indexValues = new HashMap<>(); - - @Override - public void addIndexFunc(String indexName, IndexFunc indexFunc) { - indexFuncMap.put(indexName, indexFunc); - indices.put(indexName, HashMultimap.create()); - indexValues.put(indexName, HashMultimap.create()); - } - - @Override - public Set indexNames() { - return Set.copyOf(indexFuncMap.keySet()); - } - - @Override - public void add(String indexName, T obj) { - IndexFunc indexFunc = getIndexFunc(indexName); - Set indexKeys = indexFunc.apply(obj); - for (String indexKey : indexKeys) { - SetMultimap index = indices.get(indexName); - index.put(indexKey, getObjectKey(obj)); - - SetMultimap indexValue = indexValues.get(indexName); - indexValue.put(getObjectKey(obj), indexKey); - } - } - - @NonNull - private IndexFunc getIndexFunc(String indexName) { - IndexFunc 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 indexFunc = getIndexFunc(indexName); - Set indexKeys = indexFunc.apply(obj); - Set oldIndexKeys = new HashSet<>(); - SetMultimap indexValue = indexValues.get(indexName); - if (indexValue.containsKey(getObjectKey(obj))) { - oldIndexKeys.addAll(indexValue.get(getObjectKey(obj))); - } - // delete old index first - for (String oldIndexKey : oldIndexKeys) { - SetMultimap index = indices.get(indexName); - index.remove(oldIndexKey, getObjectKey(obj)); - indexValue.remove(getObjectKey(obj), oldIndexKey); - } - // add new index - for (String indexKey : indexKeys) { - SetMultimap index = indices.get(indexName); - index.put(indexKey, getObjectKey(obj)); - - indexValue.put(getObjectKey(obj), indexKey); - } - } - - @Override - public Set getByIndex(String indexName, String indexKey) { - SetMultimap 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 indexFunc = getIndexFunc(indexName); - SetMultimap indexValue = indexValues.get(indexName); - Set indexKeys = indexFunc.apply(obj); - for (String indexKey : indexKeys) { - String objectKey = getObjectKey(obj); - SetMultimap 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> 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(); - } -} diff --git a/application/src/main/java/run/halo/app/content/Indexer.java b/application/src/main/java/run/halo/app/content/Indexer.java deleted file mode 100644 index 6dde49ce2d..0000000000 --- a/application/src/main/java/run/halo/app/content/Indexer.java +++ /dev/null @@ -1,79 +0,0 @@ -package run.halo.app.content; - -import java.util.Set; -import run.halo.app.extension.Extension; - -/** - *

Indexer is used to index objects by index name and index key.

- *

For example, if you want to index posts by category, you can use the following code:

- *
- *     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);
- * 
- * - * @param the type of object to be indexed - * @author guqing - * @since 2.0.0 - */ -public interface Indexer { - - /** - * 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 indexFunc); - - Set 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. - * - *

For example, if you want to index Person objects by name and age, you can use the - * following:

- *
-     * // 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);
-     *  
- * - * @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 getByIndex(String indexName, String indexKey); - - void delete(String indexName, T obj); - - @FunctionalInterface - interface IndexFunc { - Set apply(T obj); - } -} diff --git a/application/src/main/java/run/halo/app/content/PostIndexInformer.java b/application/src/main/java/run/halo/app/content/PostIndexInformer.java deleted file mode 100644 index a79075580d..0000000000 --- a/application/src/main/java/run/halo/app/content/PostIndexInformer.java +++ /dev/null @@ -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; - -/** - *

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.

- * - * @author guqing - * @since 2.0.0 - */ -@Component -public class PostIndexInformer implements ApplicationListener, - 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 postIndexer; - - private final PostWatcher postWatcher; - - public PostIndexInformer(ExtensionClient client) { - postIndexer = new DefaultIndexer<>(); - postIndexer.addIndexFunc(TAG_POST_INDEXER, post -> { - List 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 labelIndexFunc() { - return post -> { - Map labels = MetadataUtil.nullSafeLabels(post); - Set indexKeys = new HashSet<>(); - for (Map.Entry entry : labels.entrySet()) { - indexKeys.add(labelKey(entry.getKey(), entry.getValue())); - } - return indexKeys; - }; - } - - public Set getByTagName(String tagName) { - return postIndexer.getByIndex(TAG_POST_INDEXER, tagName); - } - - public Set getByLabels(Map labels) { - if (labels == null) { - return Set.of(); - } - Set result = new HashSet<>(); - for (Map.Entry entry : labels.entrySet()) { - Set 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 consumer) { - Post post = convertTo(extension); - Set indexNames = getIndexNames(); - for (String indexName : indexNames) { - maintainIndicates(indexName, post, consumer); - } - } - - Set getIndexNames() { - long stamp = lock.tryOptimisticRead(); - Set 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 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()); - } -} diff --git a/application/src/main/java/run/halo/app/content/PostQuery.java b/application/src/main/java/run/halo/app/content/PostQuery.java index c5992d2aa2..12fa2a1c69 100644 --- a/application/src/main/java/run/halo/app/content/PostQuery.java +++ b/application/src/main/java/run/halo/app/content/PostQuery.java @@ -1,28 +1,25 @@ 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. @@ -105,118 +102,58 @@ private Set listToSet(List param) { } /** - * Build a comparator from the query object. + * Build a list options from the query object. * - * @return a comparator + * @return a list options */ - public Comparator toComparator() { - var sort = getSort(); - var creationTimestampOrder = sort.getOrderFor("creationTimestamp"); - List> comparators = new ArrayList<>(); - if (creationTimestampOrder != null) { - Comparator comparator = - comparing(post -> post.getMetadata().getCreationTimestamp()); - if (creationTimestampOrder.isDescending()) { - comparator = comparator.reversed(); - } - comparators.add(comparator); - } - - var publishTimeOrder = sort.getOrderFor("publishTime"); - if (publishTimeOrder != null) { - Comparator nullsComparator = publishTimeOrder.isAscending() - ? org.springframework.util.comparator.Comparators.nullsLow() - : org.springframework.util.comparator.Comparators.nullsHigh(); - Comparator 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 toPredicate() { - Predicate 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 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(); - }); + 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); + } } Post.VisibleEnum visible = getVisible(); if (visible != null) { - predicate = - predicate.and(post -> visible.equals(post.getSpec().getVisible())); + fieldQuery = QueryFactory.and(fieldQuery, QueryFactory.equal( + "spec.visible", visible.name()) + ); } if (StringUtils.isNotBlank(username)) { - Predicate 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 left, List 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; } } diff --git a/application/src/main/java/run/halo/app/content/impl/PostServiceImpl.java b/application/src/main/java/run/halo/app/content/impl/PostServiceImpl.java index 3892d765cb..86ff032286 100644 --- a/application/src/main/java/run/halo/app/content/impl/PostServiceImpl.java +++ b/application/src/main/java/run/halo/app/content/impl/PostServiceImpl.java @@ -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 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.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 PostServiceImpl(ReactiveExtensionClient client, CounterService counterSer @Override public Mono> 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 @@ private Flux listTags(List tagNames) { 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 listCategories(List 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 listContributors(List usernames) { diff --git a/application/src/main/java/run/halo/app/content/impl/SnapshotServiceImpl.java b/application/src/main/java/run/halo/app/content/impl/SnapshotServiceImpl.java index 00e758d6e7..d9ff3b3e0c 100644 --- a/application/src/main/java/run/halo/app/content/impl/SnapshotServiceImpl.java +++ b/application/src/main/java/run/halo/app/content/impl/SnapshotServiceImpl.java @@ -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; diff --git a/application/src/main/java/run/halo/app/core/extension/endpoint/StatsEndpoint.java b/application/src/main/java/run/halo/app/core/extension/endpoint/StatsEndpoint.java index 8ea04b4a76..c00e53062f 100644 --- a/application/src/main/java/run/halo/app/core/extension/endpoint/StatsEndpoint.java +++ b/application/src/main/java/run/halo/app/core/extension/endpoint/StatsEndpoint.java @@ -2,6 +2,8 @@ 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 lombok.Data; import org.springdoc.webflux.core.fn.SpringdocRouteBuilder; @@ -13,8 +15,11 @@ 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 +72,16 @@ Mono getStats(ServerRequest request) { 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(equal("metadata.deletionTimestamp", null), + 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)); } diff --git a/application/src/main/java/run/halo/app/core/extension/reconciler/CategoryReconciler.java b/application/src/main/java/run/halo/app/core/extension/reconciler/CategoryReconciler.java index 85b7a929ba..48935c569c 100644 --- a/application/src/main/java/run/halo/app/core/extension/reconciler/CategoryReconciler.java +++ b/application/src/main/java/run/halo/app/core/extension/reconciler/CategoryReconciler.java @@ -1,5 +1,8 @@ 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 java.time.Duration; import java.util.ArrayDeque; import java.util.ArrayList; @@ -13,6 +16,7 @@ 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 +24,12 @@ 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 +144,12 @@ private void populatePosts(Category category) { .map(item -> item.getMetadata().getName()) .toList(); - List posts = client.list(Post.class, post -> !post.isDeleted(), null); + var postListOptions = new ListOptions(); + postListOptions.setFieldSelector(FieldSelector.of( + and(equal("metadata.deletionTimestamp", null), + equal("spec.deleted", "false"))) + ); + var posts = client.listAll(Post.class, postListOptions, Sort.unsorted()); // populate post to status List compactPosts = posts.stream() diff --git a/application/src/main/java/run/halo/app/core/extension/reconciler/PostReconciler.java b/application/src/main/java/run/halo/app/core/extension/reconciler/PostReconciler.java index fafdd67a2c..8c9a950951 100644 --- a/application/src/main/java/run/halo/app/core/extension/reconciler/PostReconciler.java +++ b/application/src/main/java/run/halo/app/core/extension/reconciler/PostReconciler.java @@ -9,6 +9,7 @@ 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.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.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 Result reconcile(Request 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 usernames = snapshot.getSpec().getContributors(); @@ -292,7 +296,7 @@ void unPublishPost(Post post, Set events) { } 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 @@ void unPublishPost(Post post, Set events) { 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 @@ private String getExcerpt(String htmlContent) { // TODO The default capture 150 words as excerpt return StringUtils.substring(text, 0, 150); } + + List 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()); + } } diff --git a/application/src/main/java/run/halo/app/core/extension/reconciler/SinglePageReconciler.java b/application/src/main/java/run/halo/app/core/extension/reconciler/SinglePageReconciler.java index ad46213b5e..06eb01e52c 100644 --- a/application/src/main/java/run/halo/app/core/extension/reconciler/SinglePageReconciler.java +++ b/application/src/main/java/run/halo/app/core/extension/reconciler/SinglePageReconciler.java @@ -14,6 +14,7 @@ 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.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 @@ private void publishFailed(String name, Throwable error) { 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 @@ private void reconcileStatus(String name) { // handle contributors String headSnapshot = singlePage.getSpec().getHeadSnapshot(); - List contributors = client.list(Snapshot.class, - snapshot -> Ref.of(singlePage).equals(snapshot.getSpec().getSubjectRef()), null) + List contributors = listSnapshots(Ref.of(singlePage)) .stream() .peek(snapshot -> { snapshot.getSpec().setContentPatch(StringUtils.EMPTY); @@ -377,4 +378,11 @@ private boolean isDeleted(SinglePage singlePage) { return Objects.equals(true, singlePage.getSpec().getDeleted()) || singlePage.getMetadata().getDeletionTimestamp() != null; } + + List 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()); + } } diff --git a/application/src/main/java/run/halo/app/core/extension/reconciler/TagReconciler.java b/application/src/main/java/run/halo/app/core/extension/reconciler/TagReconciler.java index 887ca0ae08..3015e6229a 100644 --- a/application/src/main/java/run/halo/app/core/extension/reconciler/TagReconciler.java +++ b/application/src/main/java/run/halo/app/core/extension/reconciler/TagReconciler.java @@ -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 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 { 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 @@ private void reconcileStatusPosts(String tagName) { } private void populatePosts(Tag tag) { - // populate post count - Set postNames = postIndexInformer.getByTagName(tag.getMetadata().getName()); - tag.getStatusOrDefault().setPostCount(postNames.size()); - - // populate visible post count - Map labelToQuery = Map.of(Post.PUBLISHED_LABEL, BooleanUtils.TRUE, - Post.VISIBLE_LABEL, Post.VisibleEnum.PUBLIC.name(), - Post.DELETED_LABEL, BooleanUtils.FALSE); - Set hasAllLabelPosts = postIndexInformer.getByLabels(labelToQuery); - - // retain all posts that has all labels - Set postNamesWithTag = new HashSet<>(postNames); - postNamesWithTag.retainAll(hasAllLabelPosts); - tag.getStatusOrDefault().setVisiblePostCount(postNamesWithTag.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()); + + 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) { diff --git a/application/src/main/java/run/halo/app/extension/index/IndexEntryImpl.java b/application/src/main/java/run/halo/app/extension/index/IndexEntryImpl.java index 262b557a92..9a1b918883 100644 --- a/application/src/main/java/run/halo/app/extension/index/IndexEntryImpl.java +++ b/application/src/main/java/run/halo/app/extension/index/IndexEntryImpl.java @@ -1,6 +1,5 @@ package run.halo.app.extension.index; -import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.ListMultimap; import com.google.common.collect.MultimapBuilder; import java.util.Collection; @@ -151,7 +150,9 @@ public Collection> entries() { public Collection> immutableEntries() { readLock.lock(); try { - return ImmutableListMultimap.copyOf(indexKeyObjectNamesMap).entries(); + return indexKeyObjectNamesMap.entries().stream() + .map(entry -> Map.entry(entry.getKey(), entry.getValue())) + .toList(); } finally { readLock.unlock(); } diff --git a/application/src/main/java/run/halo/app/extension/index/IndexedQueryEngineImpl.java b/application/src/main/java/run/halo/app/extension/index/IndexedQueryEngineImpl.java index 5dd092e97a..0f71ea21e9 100644 --- a/application/src/main/java/run/halo/app/extension/index/IndexedQueryEngineImpl.java +++ b/application/src/main/java/run/halo/app/extension/index/IndexedQueryEngineImpl.java @@ -62,12 +62,30 @@ static IndexEntry getIndexEntry(String fieldPath, Map fieldP return fieldPathEntryMap.get(fieldPath); } + static List paginate(List list, int page, int size) { + if (list == null) { + return new ArrayList<>(); + } + + if (size == 0) { + return new ArrayList<>(list); + } + + int fromIndex = (page - 1) * size; + if (fromIndex >= list.size() || fromIndex < 0) { + return new ArrayList<>(); + } + + int toIndex = Math.min(fromIndex + size, list.size()); + return new ArrayList<>(list.subList(fromIndex, toIndex)); + } + @Override public ListResult retrieve(GroupVersionKind type, ListOptions options, PageRequest page) { var indexer = indexerFactory.getIndexer(type); var allMatchedResult = doRetrieve(indexer, options, page.getSort()); - var list = ListResult.subList(allMatchedResult, page.getPageNumber(), page.getPageSize()); + var list = paginate(allMatchedResult, page.getPageNumber(), page.getPageSize()); return new ListResult<>(page.getPageNumber(), page.getPageSize(), allMatchedResult.size(), list); } diff --git a/application/src/main/java/run/halo/app/infra/SchemeInitializer.java b/application/src/main/java/run/halo/app/infra/SchemeInitializer.java index dfaabce05d..f87d48637e 100644 --- a/application/src/main/java/run/halo/app/infra/SchemeInitializer.java +++ b/application/src/main/java/run/halo/app/infra/SchemeInitializer.java @@ -1,5 +1,9 @@ package run.halo.app.infra; +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 +42,7 @@ 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 +75,83 @@ public void onApplicationEvent(@NonNull ApplicationContextInitializedEvent event schemeManager.register(Theme.class); schemeManager.register(Menu.class); schemeManager.register(MenuItem.class); - schemeManager.register(Post.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()))); + }); schemeManager.register(Category.class); schemeManager.register(Tag.class); - schemeManager.register(Snapshot.class); + 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); diff --git a/application/src/main/java/run/halo/app/theme/endpoint/CategoryQueryEndpoint.java b/application/src/main/java/run/halo/app/theme/endpoint/CategoryQueryEndpoint.java index de9831b798..4e21b26120 100644 --- a/application/src/main/java/run/halo/app/theme/endpoint/CategoryQueryEndpoint.java +++ b/application/src/main/java/run/halo/app/theme/endpoint/CategoryQueryEndpoint.java @@ -2,11 +2,9 @@ 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.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 RouterFunction endpoint() { private Mono listPostsByCategoryName(ServerRequest request) { final var name = request.pathVariable("name"); final var query = new PostPublicQuery(request.exchange()); - Predicate 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) diff --git a/application/src/main/java/run/halo/app/theme/endpoint/PostQueryEndpoint.java b/application/src/main/java/run/halo/app/theme/endpoint/PostQueryEndpoint.java index 632ef1c50b..c0e922f44e 100644 --- a/application/src/main/java/run/halo/app/theme/endpoint/PostQueryEndpoint.java +++ b/application/src/main/java/run/halo/app/theme/endpoint/PostQueryEndpoint.java @@ -107,8 +107,7 @@ private Mono getPostByName(ServerRequest request) { private Mono 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) ); diff --git a/application/src/main/java/run/halo/app/theme/endpoint/TagQueryEndpoint.java b/application/src/main/java/run/halo/app/theme/endpoint/TagQueryEndpoint.java index c9f1e086e7..c3ed1baada 100644 --- a/application/src/main/java/run/halo/app/theme/endpoint/TagQueryEndpoint.java +++ b/application/src/main/java/run/halo/app/theme/endpoint/TagQueryEndpoint.java @@ -2,10 +2,8 @@ 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,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.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.index.query.QueryFactory; import run.halo.app.extension.router.QueryParamBuildUtil; import run.halo.app.extension.router.SortableRequest; import run.halo.app.theme.finders.PostPublicQueryService; @@ -102,13 +100,11 @@ private Mono getTagByName(ServerRequest request) { private Mono listPostsByTagName(ServerRequest request) { final var name = request.pathVariable("name"); final var query = new PostPublicQuery(request.exchange()); - final Predicate 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) diff --git a/application/src/main/java/run/halo/app/theme/finders/PostPublicQueryService.java b/application/src/main/java/run/halo/app/theme/finders/PostPublicQueryService.java index 92c4e5f90e..e1ef8e6804 100644 --- a/application/src/main/java/run/halo/app/theme/finders/PostPublicQueryService.java +++ b/application/src/main/java/run/halo/app/theme/finders/PostPublicQueryService.java @@ -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 @@ 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> list(Integer page, Integer size, - Predicate postPredicate, - Comparator comparator); + Mono> list(ListOptions listOptions, PageRequest page); /** * Converts post to listed post vo. diff --git a/application/src/main/java/run/halo/app/theme/finders/impl/PostFinderImpl.java b/application/src/main/java/run/halo/app/theme/finders/impl/PostFinderImpl.java index 09b5c312a0..c936f8a8dc 100644 --- a/application/src/main/java/run/halo/app/theme/finders/impl/PostFinderImpl.java +++ b/application/src/main/java/run/halo/app/theme/finders/impl/PostFinderImpl.java @@ -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,12 +65,47 @@ public Mono content(String postName) { return postPublicQueryService.getContent(postName); } + 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 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 fetchByName(String name) { + if (StringUtils.isBlank(name)) { + return Mono.empty(); + } + return getByName(name) + .onErrorResume(ExtensionNotFoundException.class::isInstance, (error) -> Mono.empty()); + } + @Override public Mono cursor(String currentName) { - // TODO Optimize the post names query here - return postPredicateResolver.getPredicate() - .flatMapMany(postPredicate -> - client.list(Post.class, postPredicate, defaultComparator()) + return postPredicateResolver.getListOptions() + .flatMapMany(postListOption -> + client.listAll(Post.class, postListOption, defaultSort()) ) .map(post -> post.getMetadata().getName()) .collectList() @@ -79,10 +115,9 @@ public Mono cursor(String currentName) { .thenReturn(builder) ) .flatMap(builder -> { - Pair previousNextPair = - postPreviousNextPair(postNames, currentName); - String previousPostName = previousNextPair.getLeft(); - String nextPostName = previousNextPair.getRight(); + var previousNextPair = findPostNavigation(postNames, currentName); + String previousPostName = previousNextPair.prev(); + String nextPostName = previousNextPair.next(); return fetchByName(previousPostName) .doOnNext(builder::previous) .then(fetchByName(nextPostName)) @@ -93,115 +128,50 @@ public Mono cursor(String currentName) { .defaultIfEmpty(NavigationPostVo.empty()); } - private Mono fetchByName(String name) { - if (StringUtils.isBlank(name)) { - return Mono.empty(); - } - return getByName(name) - .onErrorResume(ExtensionNotFoundException.class::isInstance, (error) -> Mono.empty()); - } - @Override - public Flux listAll() { - return postPredicateResolver.getPredicate() - .flatMapMany(predicate -> client.list(Post.class, predicate, defaultComparator())) - .concatMap(postPublicQueryService::convertToListedVo); - } - - static Pair postPreviousNextPair(List postNames, - String currentName) { - FixedSizeSlidingWindow 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 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 { - Deque 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 elements = elements(); - return elements.indexOf(o); - } - - public List elements() { - return new ArrayList<>(queue); - } - - public boolean isFull() { - return queue.size() == size; - } + public Mono> list(Integer page, Integer size) { + return postPublicQueryService.list(new ListOptions(), getPageRequest(page, size)); } - @Override - public Mono> list(Integer page, Integer size) { - return postPublicQueryService.list(page, size, null, defaultComparator()); + private PageRequestImpl getPageRequest(Integer page, Integer size) { + return PageRequestImpl.of(pageNullSafe(page), sizeNullSafe(size), defaultSort()); } @Override public Mono> 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> 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> 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 Mono> archives(Integer page, Integer size, Stri @Override public Mono> archives(Integer page, Integer size, String year, String month) { - return postPublicQueryService.list(page, size, post -> { - Map 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> yearPosts = list.get() .collect(Collectors.groupingBy( post -> HaloUtils.getYearText(post.getSpec().getPublishTime()))); - List postArchives = - yearPosts.entrySet().stream().map(entry -> { + List postArchives = yearPosts.entrySet().stream() + .map(entry -> { String key = entry.getKey(); // archives by month Map> monthPosts = entry.getValue().stream() @@ -260,37 +230,24 @@ public Mono> archives(Integer page, Integer size, Stri 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 c, String key) { - if (StringUtils.isBlank(key) || c == null) { - return false; - } - return c.contains(key); + @Override + public Flux listAll() { + return postPredicateResolver.getListOptions() + .flatMapMany(listOptions -> client.listAll(Post.class, listOptions, defaultSort())) + .concatMap(postPublicQueryService::convertToListedVo); + } + + int pageNullSafe(Integer page) { + return ObjectUtils.defaultIfNull(page, 1); } - static Comparator defaultComparator() { - Function pinned = - post -> Objects.requireNonNullElse(post.getSpec().getPinned(), false); - Function priority = - post -> Objects.requireNonNullElse(post.getSpec().getPriority(), 0); - Function publishTime = - post -> post.getSpec().getPublishTime(); - Function name = post -> post.getMetadata().getName(); - return Comparator.comparing(pinned) - .thenComparing(priority) - .thenComparing(publishTime, Comparators.nullsLow()) - .thenComparing(name) - .reversed(); + int sizeNullSafe(Integer size) { + return ObjectUtils.defaultIfNull(size, 10); } - static Comparator archiveComparator() { - Function publishTime = - post -> post.getSpec().getPublishTime(); - Function name = post -> post.getMetadata().getName(); - return Comparator.comparing(publishTime, Comparators.nullsLow()) - .thenComparing(name) - .reversed(); + record LinkNavigation(String prev, String current, String next) { } } diff --git a/application/src/main/java/run/halo/app/theme/finders/impl/PostPublicQueryServiceImpl.java b/application/src/main/java/run/halo/app/theme/finders/impl/PostPublicQueryServiceImpl.java index 5bc024a4bd..7b5701d3c1 100644 --- a/application/src/main/java/run/halo/app/theme/finders/impl/PostPublicQueryServiceImpl.java +++ b/application/src/main/java/run/halo/app/theme/finders/impl/PostPublicQueryServiceImpl.java @@ -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 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> list(Integer page, Integer size, - Predicate postPredicate, Comparator 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> 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 @@ comparator, pageNullSafe(page), sizeNullSafe(size)) postVos) ) ) - .defaultIfEmpty(new ListResult<>(page, size, 0L, List.of())); + .defaultIfEmpty(ListResult.emptyResult()); } + @Override public Mono convertToListedVo(@NonNull Post post) { Assert.notNull(post, "Post must not be null"); @@ -180,12 +188,4 @@ private Mono populateStats(T postVo) { ) .defaultIfEmpty(StatsVo.empty()); } - - int pageNullSafe(Integer page) { - return ObjectUtils.defaultIfNull(page, 1); - } - - int sizeNullSafe(Integer size) { - return ObjectUtils.defaultIfNull(size, 10); - } } diff --git a/application/src/main/java/run/halo/app/theme/finders/impl/SiteStatsFinderImpl.java b/application/src/main/java/run/halo/app/theme/finders/impl/SiteStatsFinderImpl.java index a24440cd56..52f2d02794 100644 --- a/application/src/main/java/run/halo/app/theme/finders/impl/SiteStatsFinderImpl.java +++ b/application/src/main/java/run/halo/app/theme/finders/impl/SiteStatsFinderImpl.java @@ -1,11 +1,18 @@ 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 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.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,9 +47,17 @@ public Mono getStats() { } Mono 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( + equal("metadata.deletionTimestamp", null), + equal("spec.deleted", "false") + ); + listOptions.setFieldSelector(FieldSelector.of(fieldQuery)); + return client.listBy(Post.class, listOptions, PageRequestImpl.ofSize(1)) + .map(result -> (int) result.getTotal()); } Mono categoryCount() { diff --git a/application/src/main/java/run/halo/app/theme/router/DefaultQueryPostPredicateResolver.java b/application/src/main/java/run/halo/app/theme/router/DefaultQueryPostPredicateResolver.java index d111014f0c..cf6e8690ce 100644 --- a/application/src/main/java/run/halo/app/theme/router/DefaultQueryPostPredicateResolver.java +++ b/application/src/main/java/run/halo/app/theme/router/DefaultQueryPostPredicateResolver.java @@ -1,5 +1,9 @@ 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.or; + import java.security.Principal; import java.util.Objects; import java.util.function.Predicate; @@ -9,6 +13,9 @@ 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 +41,28 @@ public Mono> getPredicate() { .defaultIfEmpty(predicate.and(visiblePredicate)); } + @Override + public Mono getListOptions() { + var listOptions = new ListOptions(); + listOptions.setLabelSelector(LabelSelector.builder() + .eq(Post.PUBLISHED_LABEL, "true").build()); + + var fieldQuery = and( + equal("metadata.deletionTimestamp", null), + 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 currentUserName() { return ReactiveSecurityContextHolder.getContext() .map(SecurityContext::getAuthentication) diff --git a/application/src/main/java/run/halo/app/theme/router/ExtensionPermalinkPatternUpdater.java b/application/src/main/java/run/halo/app/theme/router/ExtensionPermalinkPatternUpdater.java index 369d9be44d..01b7f3b68b 100644 --- a/application/src/main/java/run/halo/app/theme/router/ExtensionPermalinkPatternUpdater.java +++ b/application/src/main/java/run/halo/app/theme/router/ExtensionPermalinkPatternUpdater.java @@ -5,6 +5,7 @@ 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.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 void onApplicationEvent(@NonNull PermalinkRuleChangedEvent event) { 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)); } diff --git a/application/src/main/java/run/halo/app/theme/router/ReactiveQueryPostPredicateResolver.java b/application/src/main/java/run/halo/app/theme/router/ReactiveQueryPostPredicateResolver.java index ee754dc602..1d571ce01c 100644 --- a/application/src/main/java/run/halo/app/theme/router/ReactiveQueryPostPredicateResolver.java +++ b/application/src/main/java/run/halo/app/theme/router/ReactiveQueryPostPredicateResolver.java @@ -3,6 +3,7 @@ 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 @@ public interface ReactiveQueryPostPredicateResolver { Mono> getPredicate(); + + Mono getListOptions(); } diff --git a/application/src/main/java/run/halo/app/theme/router/factories/PostRouteFactory.java b/application/src/main/java/run/halo/app/theme/router/factories/PostRouteFactory.java index 5874c84c47..a13f58575d 100644 --- a/application/src/main/java/run/halo/app/theme/router/factories/PostRouteFactory.java +++ b/application/src/main/java/run/halo/app/theme/router/factories/PostRouteFactory.java @@ -15,6 +15,7 @@ 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 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 @@ private Flux fetchPostsByName(String name) { } private Flux 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) { diff --git a/application/src/test/java/run/halo/app/content/DefaultIndexerTest.java b/application/src/test/java/run/halo/app/content/DefaultIndexerTest.java deleted file mode 100644 index 46918818d3..0000000000 --- a/application/src/test/java/run/halo/app/content/DefaultIndexerTest.java +++ /dev/null @@ -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 indexer = new DefaultIndexer<>(); - String tagsIndexName = "tags"; - indexer.addIndexFunc(tagsIndexName, post -> { - List 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 indexer = new DefaultIndexer<>(); - - // Define the IndexFunc for labels. - DefaultIndexer.IndexFunc 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 indexer = new DefaultIndexer<>(); - - // Define the IndexFunc for labels. - String labelsIndexName = "labels"; - DefaultIndexer.IndexFunc labelIndexFunc = labelIndexFunc(); - indexer.addIndexFunc(labelsIndexName, labelIndexFunc); - - String tagsIndexName = "tags"; - indexer.addIndexFunc(tagsIndexName, post -> { - List 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 labelIndexFunc() { - return post -> { - Map labels = post.getMetadata().getLabels(); - Set indexKeys = new HashSet<>(); - if (labels != null) { - for (Map.Entry entry : labels.entrySet()) { - indexKeys.add(entry.getKey() + "=" + entry.getValue()); - } - } - return indexKeys; - }; - } - - @Test - void getByIndex() { - DefaultIndexer indexer = new DefaultIndexer<>(); - String tagsIndexName = "tags"; - indexer.addIndexFunc(tagsIndexName, post -> { - List 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 indexer = new DefaultIndexer<>(); - assertThrows(IllegalArgumentException.class, () -> { - indexer.add("fake-index-name", post1); - }, "Index function not found for index name 'fake-index-name'"); - } -} diff --git a/application/src/test/java/run/halo/app/content/PostQueryTest.java b/application/src/test/java/run/halo/app/content/PostQueryTest.java index 54ec0fc94b..d8b50bfc47 100644 --- a/application/src/test/java/run/halo/app/content/PostQueryTest.java +++ b/application/src/test/java/run/halo/app/content/PostQueryTest.java @@ -3,6 +3,7 @@ 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.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 @@ void userScopedQueryTest() { .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(); - - spec.setOwner("faker"); - assertThat(postQuery.toPredicate().test(post)).isTrue(); - } - - @Test - void toPredicate() { - MultiValueMap 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(); + var listOptions = postQuery.toListOptions(); + assertThat(listOptions).isNotNull(); + assertThat(listOptions.getFieldSelector()).isNotNull(); + var nameEntry = + (Collection>) List.of(Map.entry("metadata.name", "faker")); + var entry = (Collection>) 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"); + + 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(); } } diff --git a/application/src/test/java/run/halo/app/core/extension/reconciler/CategoryReconcilerTest.java b/application/src/test/java/run/halo/app/core/extension/reconciler/CategoryReconcilerTest.java index edfd2854d8..f379b7c090 100644 --- a/application/src/test/java/run/halo/app/core/extension/reconciler/CategoryReconcilerTest.java +++ b/application/src/test/java/run/halo/app/core/extension/reconciler/CategoryReconcilerTest.java @@ -10,18 +10,19 @@ 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 captor = ArgumentCaptor.forClass(Category.class); @@ -54,7 +55,7 @@ void reconcileStatusPostForCategoryA() throws JSONException { } @Test - void reconcileStatusPostForCategoryB() throws JSONException { + void reconcileStatusPostForCategoryB() { reconcileStatusPostPilling("category-B"); ArgumentCaptor captor = ArgumentCaptor.forClass(Category.class); verify(client, times(3)).update(captor.capture()); @@ -64,7 +65,7 @@ void reconcileStatusPostForCategoryB() throws JSONException { } @Test - void reconcileStatusPostForCategoryC() throws JSONException { + void reconcileStatusPostForCategoryC() { reconcileStatusPostPilling("category-C"); ArgumentCaptor captor = ArgumentCaptor.forClass(Category.class); verify(client, times(3)).update(captor.capture()); @@ -74,7 +75,7 @@ void reconcileStatusPostForCategoryC() throws JSONException { } @Test - void reconcileStatusPostForCategoryD() throws JSONException { + void reconcileStatusPostForCategoryD() { reconcileStatusPostPilling("category-D"); ArgumentCaptor captor = ArgumentCaptor.forClass(Category.class); verify(client, times(3)).update(captor.capture()); @@ -89,7 +90,7 @@ private void reconcileStatusPostPilling(String reconcileCategoryName) { .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())) .thenReturn(categories()); diff --git a/application/src/test/java/run/halo/app/core/extension/reconciler/PostReconcilerTest.java b/application/src/test/java/run/halo/app/core/extension/reconciler/PostReconcilerTest.java index 18cad76cec..be15971402 100644 --- a/application/src/test/java/run/halo/app/core/extension/reconciler/PostReconcilerTest.java +++ b/application/src/test/java/run/halo/app/core/extension/reconciler/PostReconcilerTest.java @@ -85,7 +85,7 @@ void reconcile() { 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 captor = ArgumentCaptor.forClass(Post.class); @@ -126,7 +126,7 @@ void reconcileExcerpt() { 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 captor = ArgumentCaptor.forClass(Post.class); @@ -162,7 +162,7 @@ void reconcileLastModifyTimeWhenPostIsPublished() { 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 captor = ArgumentCaptor.forClass(Post.class); @@ -191,7 +191,7 @@ void reconcileLastModifyTimeWhenPostIsNotPublished() { .rawType("markdown") .build())); - when(client.list(eq(Snapshot.class), any(), any())) + when(client.listAll(eq(Snapshot.class), any(), any())) .thenReturn(List.of()); ArgumentCaptor captor = ArgumentCaptor.forClass(Post.class); diff --git a/application/src/test/java/run/halo/app/core/extension/reconciler/SinglePageReconcilerTest.java b/application/src/test/java/run/halo/app/core/extension/reconciler/SinglePageReconcilerTest.java index 6579c5374a..9ee8f91397 100644 --- a/application/src/test/java/run/halo/app/core/extension/reconciler/SinglePageReconcilerTest.java +++ b/application/src/test/java/run/halo/app/core/extension/reconciler/SinglePageReconcilerTest.java @@ -95,7 +95,7 @@ void reconcile() { 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 @@ void reconcileLastModifyTimeWhenPageIsPublished() { 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 captor = ArgumentCaptor.forClass(SinglePage.class); @@ -186,7 +186,7 @@ void reconcileLastModifyTimeWhenPageIsNotPublished() { .build()) ); - when(client.list(eq(Snapshot.class), any(), any())) + when(client.listAll(eq(Snapshot.class), any(), any())) .thenReturn(List.of()); ArgumentCaptor captor = ArgumentCaptor.forClass(SinglePage.class); diff --git a/application/src/test/java/run/halo/app/core/extension/reconciler/TagReconcilerTest.java b/application/src/test/java/run/halo/app/core/extension/reconciler/TagReconcilerTest.java index 9f9b52e8f3..38b1cff16f 100644 --- a/application/src/test/java/run/halo/app/core/extension/reconciler/TagReconcilerTest.java +++ b/application/src/test/java/run/halo/app/core/extension/reconciler/TagReconcilerTest.java @@ -10,15 +10,14 @@ 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 @@ void reconcile() { 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 captor = ArgumentCaptor.forClass(Tag.class); @@ -85,8 +80,8 @@ void reconcileStatusPosts() { 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 captor = ArgumentCaptor.forClass(Tag.class); tagReconciler.reconcile(new TagReconciler.Request("fake-tag")); @@ -96,6 +91,14 @@ void reconcileStatusPosts() { 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()); diff --git a/application/src/test/java/run/halo/app/theme/endpoint/CategoryQueryEndpointTest.java b/application/src/test/java/run/halo/app/theme/endpoint/CategoryQueryEndpointTest.java index a44c02778d..0ef8f2eee6 100644 --- a/application/src/test/java/run/halo/app/theme/endpoint/CategoryQueryEndpointTest.java +++ b/application/src/test/java/run/halo/app/theme/endpoint/CategoryQueryEndpointTest.java @@ -21,6 +21,7 @@ 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.extension.ReactiveExtensionClient; import run.halo.app.theme.finders.PostPublicQueryService; import run.halo.app.theme.finders.vo.ListedPostVo; @@ -84,7 +85,7 @@ void getByName() { @Test void listPostsByCategoryName() { ListResult 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() diff --git a/application/src/test/java/run/halo/app/theme/endpoint/PostQueryEndpointTest.java b/application/src/test/java/run/halo/app/theme/endpoint/PostQueryEndpointTest.java index 4b4404b3e0..f39f1396b3 100644 --- a/application/src/test/java/run/halo/app/theme/endpoint/PostQueryEndpointTest.java +++ b/application/src/test/java/run/halo/app/theme/endpoint/PostQueryEndpointTest.java @@ -2,7 +2,6 @@ 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 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 @@ public void setUp() { @Test public void listPosts() { ListResult 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 @@ public void listPosts() { .expectBody() .jsonPath("$.items").isArray(); - verify(postPublicQueryService).list(anyInt(), anyInt(), any(), any()); + verify(postPublicQueryService).list(any(), any(PageRequest.class)); } @Test diff --git a/application/src/test/java/run/halo/app/theme/finders/impl/PostFinderImplTest.java b/application/src/test/java/run/halo/app/theme/finders/impl/PostFinderImplTest.java index a9b5d01ed1..405d5b6ddc 100644 --- a/application/src/test/java/run/halo/app/theme/finders/impl/PostFinderImplTest.java +++ b/application/src/test/java/run/halo/app/theme/finders/impl/PostFinderImplTest.java @@ -2,7 +2,6 @@ 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.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.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 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 predicate = new DefaultQueryPostPredicateResolver().getPredicate().block(); @@ -93,7 +82,7 @@ void archives() { .map(ListedPostVo::from) .toList(); ListResult 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 archives = postFinder.archives(1, 10).block(); @@ -112,22 +101,6 @@ void archives() { assertThat(items.get(1).getMonths().get(0).getMonth()).isEqualTo("01"); } - @Test - void fixedSizeSlidingWindow() { - PostFinderImpl.FixedSizeSlidingWindow - window = new PostFinderImpl.FixedSizeSlidingWindow<>(3); - - List 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 postNames = new ArrayList<>(); @@ -136,28 +109,27 @@ void postPreviousNextPair() { } // post-0, post-1, post-2 - Pair 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 postsForArchives() { From a42261b7f993c23ddf22fa9951253076bc28de38 Mon Sep 17 00:00:00 2001 From: Ryan Wang Date: Tue, 23 Jan 2024 11:16:34 +0800 Subject: [PATCH 2/9] Refine ui Signed-off-by: Ryan Wang --- .../contents/posts/DeletedPostList.vue | 4 +-- .../modules/contents/posts/PostList.vue | 31 +++++++++---------- .../posts/widgets/RecentPublishedWidget.vue | 2 +- 3 files changed, 18 insertions(+), 19 deletions(-) diff --git a/console/console-src/modules/contents/posts/DeletedPostList.vue b/console/console-src/modules/contents/posts/DeletedPostList.vue index 3df872d771..f69f7ac9ab 100644 --- a/console/console-src/modules/contents/posts/DeletedPostList.vue +++ b/console/console-src/modules/contents/posts/DeletedPostList.vue @@ -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; }, }); diff --git a/console/console-src/modules/contents/posts/PostList.vue b/console/console-src/modules/contents/posts/PostList.vue index 85968934b4..1f68d49e09 100644 --- a/console/console-src/modules/contents/posts/PostList.vue +++ b/console/console-src/modules/contents/posts/PostList.vue @@ -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', }, ]" /> diff --git a/console/console-src/modules/contents/posts/widgets/RecentPublishedWidget.vue b/console/console-src/modules/contents/posts/widgets/RecentPublishedWidget.vue index 5b0eefd176..753d60052a 100644 --- a/console/console-src/modules/contents/posts/widgets/RecentPublishedWidget.vue +++ b/console/console-src/modules/contents/posts/widgets/RecentPublishedWidget.vue @@ -21,7 +21,7 @@ const { data } = useQuery({ `${postLabels.DELETED}=false`, `${postLabels.PUBLISHED}=true`, ], - sort: ["publishTime,desc"], + sort: ["spec.publishTime,desc"], page: 1, size: 10, }); From 5faccd9250c235b26bc9ab98ba6be1c770da5db1 Mon Sep 17 00:00:00 2001 From: guqing Date: Tue, 23 Jan 2024 11:17:10 +0800 Subject: [PATCH 3/9] chore: add status.expert to index spec --- .../src/main/java/run/halo/app/infra/SchemeInitializer.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/application/src/main/java/run/halo/app/infra/SchemeInitializer.java b/application/src/main/java/run/halo/app/infra/SchemeInitializer.java index f87d48637e..7b1eba70b3 100644 --- a/application/src/main/java/run/halo/app/infra/SchemeInitializer.java +++ b/application/src/main/java/run/halo/app/infra/SchemeInitializer.java @@ -141,6 +141,10 @@ public void onApplicationEvent(@NonNull ApplicationContextInitializedEvent event .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); schemeManager.register(Tag.class); From f32598517b197b689af8561c1d0fbb3067cb735a Mon Sep 17 00:00:00 2001 From: guqing Date: Tue, 23 Jan 2024 17:02:28 +0800 Subject: [PATCH 4/9] refactor: remove unused query params for post --- .../java/run/halo/app/content/PostQuery.java | 38 ------------------- .../app/extension/index/IndexEntryImpl.java | 5 +-- 2 files changed, 2 insertions(+), 41 deletions(-) diff --git a/application/src/main/java/run/halo/app/content/PostQuery.java b/application/src/main/java/run/halo/app/content/PostQuery.java index 12fa2a1c69..6d515ecd14 100644 --- a/application/src/main/java/run/halo/app/content/PostQuery.java +++ b/application/src/main/java/run/halo/app/content/PostQuery.java @@ -5,8 +5,6 @@ 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.List; -import java.util.Set; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; import org.springframework.data.domain.Sort; @@ -49,36 +47,12 @@ public String getUsername() { return username; } - @Nullable - @Schema(name = "contributor") - public Set getContributors() { - return listToSet(queryParams.get("contributor")); - } - - @Nullable - @Schema(name = "category") - public Set getCategories() { - return listToSet(queryParams.get("category")); - } - - @Nullable - @Schema(name = "tag") - public Set 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,11 +70,6 @@ public Sort getSort() { return SortResolver.defaultInstance.resolve(exchange); } - @Nullable - private Set listToSet(List param) { - return param == null ? null : Set.copyOf(param); - } - /** * Build a list options from the query object. * @@ -138,13 +107,6 @@ public ListOptions toListOptions() { } } - Post.VisibleEnum visible = getVisible(); - if (visible != null) { - fieldQuery = QueryFactory.and(fieldQuery, QueryFactory.equal( - "spec.visible", visible.name()) - ); - } - if (StringUtils.isNotBlank(username)) { fieldQuery = QueryFactory.and(fieldQuery, QueryFactory.equal( "spec.owner", username) diff --git a/application/src/main/java/run/halo/app/extension/index/IndexEntryImpl.java b/application/src/main/java/run/halo/app/extension/index/IndexEntryImpl.java index 9a1b918883..262b557a92 100644 --- a/application/src/main/java/run/halo/app/extension/index/IndexEntryImpl.java +++ b/application/src/main/java/run/halo/app/extension/index/IndexEntryImpl.java @@ -1,5 +1,6 @@ package run.halo.app.extension.index; +import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.ListMultimap; import com.google.common.collect.MultimapBuilder; import java.util.Collection; @@ -150,9 +151,7 @@ public Collection> entries() { public Collection> immutableEntries() { readLock.lock(); try { - return indexKeyObjectNamesMap.entries().stream() - .map(entry -> Map.entry(entry.getKey(), entry.getValue())) - .toList(); + return ImmutableListMultimap.copyOf(indexKeyObjectNamesMap).entries(); } finally { readLock.unlock(); } From d8e6803710bc9f7aa7d241afabbb17b0ea647886 Mon Sep 17 00:00:00 2001 From: guqing Date: Tue, 23 Jan 2024 17:23:53 +0800 Subject: [PATCH 5/9] refactor: empty value filtering for post query --- .../src/main/java/run/halo/app/content/PostQuery.java | 6 +++++- .../run/halo/app/core/extension/endpoint/StatsEndpoint.java | 3 ++- .../app/core/extension/reconciler/CategoryReconciler.java | 3 ++- .../halo/app/theme/finders/impl/SiteStatsFinderImpl.java | 3 ++- .../app/theme/router/DefaultQueryPostPredicateResolver.java | 3 ++- 5 files changed, 13 insertions(+), 5 deletions(-) diff --git a/application/src/main/java/run/halo/app/content/PostQuery.java b/application/src/main/java/run/halo/app/content/PostQuery.java index 6d515ecd14..e62b24681c 100644 --- a/application/src/main/java/run/halo/app/content/PostQuery.java +++ b/application/src/main/java/run/halo/app/content/PostQuery.java @@ -67,7 +67,11 @@ public String getKeyword() { implementation = String.class, example = "creationTimestamp,desc")) public Sort getSort() { - return SortResolver.defaultInstance.resolve(exchange); + var sort = SortResolver.defaultInstance.resolve(exchange); + sort.and(Sort.Order.desc("metadata.creationTimestamp"), + Sort.Order.desc("metadata.name") + ); + return sort; } /** diff --git a/application/src/main/java/run/halo/app/core/extension/endpoint/StatsEndpoint.java b/application/src/main/java/run/halo/app/core/extension/endpoint/StatsEndpoint.java index c00e53062f..2102d0bb4d 100644 --- a/application/src/main/java/run/halo/app/core/extension/endpoint/StatsEndpoint.java +++ b/application/src/main/java/run/halo/app/core/extension/endpoint/StatsEndpoint.java @@ -4,6 +4,7 @@ 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; @@ -75,7 +76,7 @@ Mono getStats(ServerRequest request) { .flatMap(stats -> { var listOptions = new ListOptions(); listOptions.setFieldSelector(FieldSelector.of( - and(equal("metadata.deletionTimestamp", null), + and(isNull("metadata.deletionTimestamp"), equal("spec.deleted", "false"))) ); return client.listBy(Post.class, listOptions, PageRequestImpl.ofSize(1)) diff --git a/application/src/main/java/run/halo/app/core/extension/reconciler/CategoryReconciler.java b/application/src/main/java/run/halo/app/core/extension/reconciler/CategoryReconciler.java index 48935c569c..bcee3e310c 100644 --- a/application/src/main/java/run/halo/app/core/extension/reconciler/CategoryReconciler.java +++ b/application/src/main/java/run/halo/app/core/extension/reconciler/CategoryReconciler.java @@ -2,6 +2,7 @@ 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; @@ -146,7 +147,7 @@ private void populatePosts(Category category) { var postListOptions = new ListOptions(); postListOptions.setFieldSelector(FieldSelector.of( - and(equal("metadata.deletionTimestamp", null), + and(isNull("metadata.deletionTimestamp"), equal("spec.deleted", "false"))) ); var posts = client.listAll(Post.class, postListOptions, Sort.unsorted()); diff --git a/application/src/main/java/run/halo/app/theme/finders/impl/SiteStatsFinderImpl.java b/application/src/main/java/run/halo/app/theme/finders/impl/SiteStatsFinderImpl.java index 52f2d02794..2966f6a626 100644 --- a/application/src/main/java/run/halo/app/theme/finders/impl/SiteStatsFinderImpl.java +++ b/application/src/main/java/run/halo/app/theme/finders/impl/SiteStatsFinderImpl.java @@ -2,6 +2,7 @@ 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; @@ -52,7 +53,7 @@ Mono postCount() { .eq(Post.PUBLISHED_LABEL, "true") .build()); var fieldQuery = and( - equal("metadata.deletionTimestamp", null), + isNull("metadata.deletionTimestamp"), equal("spec.deleted", "false") ); listOptions.setFieldSelector(FieldSelector.of(fieldQuery)); diff --git a/application/src/main/java/run/halo/app/theme/router/DefaultQueryPostPredicateResolver.java b/application/src/main/java/run/halo/app/theme/router/DefaultQueryPostPredicateResolver.java index cf6e8690ce..d86a393a9b 100644 --- a/application/src/main/java/run/halo/app/theme/router/DefaultQueryPostPredicateResolver.java +++ b/application/src/main/java/run/halo/app/theme/router/DefaultQueryPostPredicateResolver.java @@ -2,6 +2,7 @@ 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; @@ -48,7 +49,7 @@ public Mono getListOptions() { .eq(Post.PUBLISHED_LABEL, "true").build()); var fieldQuery = and( - equal("metadata.deletionTimestamp", null), + isNull("metadata.deletionTimestamp"), equal("spec.deleted", "false") ); var visibleQuery = equal("spec.visible", Post.VisibleEnum.PUBLIC.name()); From f49eb9ed9759af17a481619e865cfe494f4be179 Mon Sep 17 00:00:00 2001 From: Ryan Wang Date: Wed, 24 Jan 2024 10:53:03 +0800 Subject: [PATCH 6/9] Generate api client Signed-off-by: Ryan Wang --- .../api-console-halo-run-v1alpha1-post-api.ts | 72 ------------------- ...-api-content-halo-run-v1alpha1-post-api.ts | 72 ------------------- .../api-client/src/models/plugin-status.ts | 20 ++++++ 3 files changed, 20 insertions(+), 144 deletions(-) diff --git a/console/packages/api-client/src/api/api-console-halo-run-v1alpha1-post-api.ts b/console/packages/api-client/src/api/api-console-halo-run-v1alpha1-post-api.ts index f2f8206249..9637415376 100644 --- a/console/packages/api-client/src/api/api-console-halo-run-v1alpha1-post-api.ts +++ b/console/packages/api-client/src/api/api-console-halo-run-v1alpha1-post-api.ts @@ -222,8 +222,6 @@ export const ApiConsoleHaloRunV1alpha1PostApiAxiosParamCreator = function ( }, /** * List posts. - * @param {Array} [category] - * @param {Array} [contributor] * @param {Array} [fieldSelector] Field selector for filtering. * @param {string} [keyword] Posts filtered by keyword. * @param {Array} [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} [sort] Sort property and direction of the list result. Supported fields: creationTimestamp,publishTime - * @param {Array} [tag] - * @param {'PUBLIC' | 'INTERNAL' | 'PRIVATE'} [visible] * @param {*} [options] Override http request option. * @throws {RequiredError} */ listPosts: async ( - category?: Array, - contributor?: Array, fieldSelector?: Array, keyword?: string, labelSelector?: Array, @@ -246,8 +240,6 @@ export const ApiConsoleHaloRunV1alpha1PostApiAxiosParamCreator = function ( publishPhase?: "DRAFT" | "PENDING_APPROVAL" | "PUBLISHED" | "FAILED", size?: number, sort?: Array, - tag?: Array, - visible?: "PUBLIC" | "INTERNAL" | "PRIVATE", options: AxiosRequestConfig = {} ): Promise => { 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} [category] - * @param {Array} [contributor] * @param {Array} [fieldSelector] Field selector for filtering. * @param {string} [keyword] Posts filtered by keyword. * @param {Array} [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} [sort] Sort property and direction of the list result. Supported fields: creationTimestamp,publishTime - * @param {Array} [tag] - * @param {'PUBLIC' | 'INTERNAL' | 'PRIVATE'} [visible] * @param {*} [options] Override http request option. * @throws {RequiredError} */ async listPosts( - category?: Array, - contributor?: Array, fieldSelector?: Array, keyword?: string, labelSelector?: Array, @@ -734,15 +704,11 @@ export const ApiConsoleHaloRunV1alpha1PostApiFp = function ( publishPhase?: "DRAFT" | "PENDING_APPROVAL" | "PUBLISHED" | "FAILED", size?: number, sort?: Array, - tag?: Array, - visible?: "PUBLIC" | "INTERNAL" | "PRIVATE", options?: AxiosRequestConfig ): Promise< (axios?: AxiosInstance, basePath?: string) => AxiosPromise > { 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 { 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} - * @memberof ApiConsoleHaloRunV1alpha1PostApiListPosts - */ - readonly category?: Array; - - /** - * - * @type {Array} - * @memberof ApiConsoleHaloRunV1alpha1PostApiListPosts - */ - readonly contributor?: Array; - /** * Field selector for filtering. * @type {Array} @@ -1164,20 +1110,6 @@ export interface ApiConsoleHaloRunV1alpha1PostApiListPostsRequest { * @memberof ApiConsoleHaloRunV1alpha1PostApiListPosts */ readonly sort?: Array; - - /** - * - * @type {Array} - * @memberof ApiConsoleHaloRunV1alpha1PostApiListPosts - */ - readonly tag?: Array; - - /** - * - * @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)); diff --git a/console/packages/api-client/src/api/uc-api-content-halo-run-v1alpha1-post-api.ts b/console/packages/api-client/src/api/uc-api-content-halo-run-v1alpha1-post-api.ts index f33c113d84..e3534c87e2 100644 --- a/console/packages/api-client/src/api/uc-api-content-halo-run-v1alpha1-post-api.ts +++ b/console/packages/api-client/src/api/uc-api-content-halo-run-v1alpha1-post-api.ts @@ -222,8 +222,6 @@ export const UcApiContentHaloRunV1alpha1PostApiAxiosParamCreator = function ( }, /** * List posts owned by the current user. - * @param {Array} [category] - * @param {Array} [contributor] * @param {Array} [fieldSelector] Field selector for filtering. * @param {string} [keyword] Posts filtered by keyword. * @param {Array} [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} [sort] Sort property and direction of the list result. Supported fields: creationTimestamp,publishTime - * @param {Array} [tag] - * @param {'PUBLIC' | 'INTERNAL' | 'PRIVATE'} [visible] * @param {*} [options] Override http request option. * @throws {RequiredError} */ listMyPosts: async ( - category?: Array, - contributor?: Array, fieldSelector?: Array, keyword?: string, labelSelector?: Array, @@ -246,8 +240,6 @@ export const UcApiContentHaloRunV1alpha1PostApiAxiosParamCreator = function ( publishPhase?: "DRAFT" | "PENDING_APPROVAL" | "PUBLISHED" | "FAILED", size?: number, sort?: Array, - tag?: Array, - visible?: "PUBLIC" | "INTERNAL" | "PRIVATE", options: AxiosRequestConfig = {} ): Promise => { 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} [category] - * @param {Array} [contributor] * @param {Array} [fieldSelector] Field selector for filtering. * @param {string} [keyword] Posts filtered by keyword. * @param {Array} [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} [sort] Sort property and direction of the list result. Supported fields: creationTimestamp,publishTime - * @param {Array} [tag] - * @param {'PUBLIC' | 'INTERNAL' | 'PRIVATE'} [visible] * @param {*} [options] Override http request option. * @throws {RequiredError} */ async listMyPosts( - category?: Array, - contributor?: Array, fieldSelector?: Array, keyword?: string, labelSelector?: Array, @@ -677,15 +647,11 @@ export const UcApiContentHaloRunV1alpha1PostApiFp = function ( publishPhase?: "DRAFT" | "PENDING_APPROVAL" | "PUBLISHED" | "FAILED", size?: number, sort?: Array, - tag?: Array, - visible?: "PUBLIC" | "INTERNAL" | "PRIVATE", options?: AxiosRequestConfig ): Promise< (axios?: AxiosInstance, basePath?: string) => AxiosPromise > { 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 { 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} - * @memberof UcApiContentHaloRunV1alpha1PostApiListMyPosts - */ - readonly category?: Array; - - /** - * - * @type {Array} - * @memberof UcApiContentHaloRunV1alpha1PostApiListMyPosts - */ - readonly contributor?: Array; - /** * Field selector for filtering. * @type {Array} @@ -1070,20 +1016,6 @@ export interface UcApiContentHaloRunV1alpha1PostApiListMyPostsRequest { * @memberof UcApiContentHaloRunV1alpha1PostApiListMyPosts */ readonly sort?: Array; - - /** - * - * @type {Array} - * @memberof UcApiContentHaloRunV1alpha1PostApiListMyPosts - */ - readonly tag?: Array; - - /** - * - * @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)); diff --git a/console/packages/api-client/src/models/plugin-status.ts b/console/packages/api-client/src/models/plugin-status.ts index f053aa508e..7fa0e4f6b8 100644 --- a/console/packages/api-client/src/models/plugin-status.ts +++ b/console/packages/api-client/src/models/plugin-status.ts @@ -34,6 +34,12 @@ export interface PluginStatus { * @memberof PluginStatus */ entry?: string; + /** + * + * @type {string} + * @memberof PluginStatus + */ + lastProbeState?: PluginStatusLastProbeStateEnum; /** * * @type {string} @@ -66,13 +72,27 @@ export interface PluginStatus { stylesheet?: string; } +export const PluginStatusLastProbeStateEnum = { + Created: "CREATED", + Disabled: "DISABLED", + Resolved: "RESOLVED", + Started: "STARTED", + Stopped: "STOPPED", + 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 = From 38b428823c9484c79612d8813a2c2f76503fd8a5 Mon Sep 17 00:00:00 2001 From: Ryan Wang Date: Wed, 24 Jan 2024 11:12:18 +0800 Subject: [PATCH 7/9] Improve refetchInterval Signed-off-by: Ryan Wang --- console/uc-src/modules/contents/posts/PostList.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/console/uc-src/modules/contents/posts/PostList.vue b/console/uc-src/modules/contents/posts/PostList.vue index 3a9646c914..f9c9bf9268 100644 --- a/console/uc-src/modules/contents/posts/PostList.vue +++ b/console/uc-src/modules/contents/posts/PostList.vue @@ -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; }, }); From a24792df105474bcd80237c1b6a161cb720564ea Mon Sep 17 00:00:00 2001 From: guqing Date: Wed, 24 Jan 2024 12:05:48 +0800 Subject: [PATCH 8/9] refactor: using indexes to query tags and categories --- .../run/halo/app/extension/ListResult.java | 10 ++++ .../java/run/halo/app/content/PostQuery.java | 5 +- .../reconciler/CategoryReconciler.java | 2 +- .../run/halo/app/infra/SchemeInitializer.java | 20 ++++++- .../theme/endpoint/CategoryQueryEndpoint.java | 7 +-- .../app/theme/endpoint/TagQueryEndpoint.java | 13 +++-- .../run/halo/app/theme/finders/TagFinder.java | 3 + .../finders/impl/CategoryFinderImpl.java | 55 +++++++++++-------- .../finders/impl/SiteStatsFinderImpl.java | 5 +- .../app/theme/finders/impl/TagFinderImpl.java | 33 ++++++++++- .../ExtensionPermalinkPatternUpdater.java | 4 +- .../factories/CategoryPostRouteFactory.java | 21 +++++-- .../router/factories/TagPostRouteFactory.java | 16 +++++- .../reconciler/CategoryReconcilerTest.java | 2 +- .../halo/app/extension/ListResultTest.java | 10 ++++ .../endpoint/CategoryQueryEndpointTest.java | 4 +- .../finders/impl/CategoryFinderImplTest.java | 12 ++-- .../theme/finders/impl/TagFinderImplTest.java | 4 +- .../factories/TagPostRouteFactoryTest.java | 10 +++- 19 files changed, 171 insertions(+), 65 deletions(-) diff --git a/api/src/main/java/run/halo/app/extension/ListResult.java b/api/src/main/java/run/halo/app/extension/ListResult.java index cc64ef9170..679e0ead38 100644 --- a/api/src/main/java/run/halo/app/extension/ListResult.java +++ b/api/src/main/java/run/halo/app/extension/ListResult.java @@ -8,6 +8,7 @@ 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 static List subList(List list, int page, int size) { return listSort; } + /** + * Gets the first element of the list result. + */ + public static Optional first(ListResult listResult) { + return Optional.ofNullable(listResult) + .map(ListResult::getItems) + .map(list -> list.isEmpty() ? null : list.get(0)); + } + @Override public Stream get() { return items.stream(); diff --git a/application/src/main/java/run/halo/app/content/PostQuery.java b/application/src/main/java/run/halo/app/content/PostQuery.java index e62b24681c..1e9dff49d2 100644 --- a/application/src/main/java/run/halo/app/content/PostQuery.java +++ b/application/src/main/java/run/halo/app/content/PostQuery.java @@ -68,9 +68,8 @@ public String getKeyword() { example = "creationTimestamp,desc")) public Sort getSort() { var sort = SortResolver.defaultInstance.resolve(exchange); - sort.and(Sort.Order.desc("metadata.creationTimestamp"), - Sort.Order.desc("metadata.name") - ); + sort = sort.and(Sort.by(Sort.Direction.DESC, "metadata.creationTimestamp")); + sort = sort.and(Sort.by(Sort.Direction.DESC, "metadata.name")); return sort; } diff --git a/application/src/main/java/run/halo/app/core/extension/reconciler/CategoryReconciler.java b/application/src/main/java/run/halo/app/core/extension/reconciler/CategoryReconciler.java index bcee3e310c..26b138b87d 100644 --- a/application/src/main/java/run/halo/app/core/extension/reconciler/CategoryReconciler.java +++ b/application/src/main/java/run/halo/app/core/extension/reconciler/CategoryReconciler.java @@ -190,7 +190,7 @@ private boolean includes(@Nullable List categoryRefs, List categ } private List listChildrenByName(String name) { - List categories = client.list(Category.class, null, null); + var categories = client.listAll(Category.class, new ListOptions(), Sort.unsorted()); Map nameIdentityMap = categories.stream() .collect(Collectors.toMap(category -> category.getMetadata().getName(), Function.identity())); diff --git a/application/src/main/java/run/halo/app/infra/SchemeInitializer.java b/application/src/main/java/run/halo/app/infra/SchemeInitializer.java index 7b1eba70b3..729d07f185 100644 --- a/application/src/main/java/run/halo/app/infra/SchemeInitializer.java +++ b/application/src/main/java/run/halo/app/infra/SchemeInitializer.java @@ -1,5 +1,6 @@ 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; @@ -146,8 +147,23 @@ public void onApplicationEvent(@NonNull ApplicationContextInitializedEvent event .setIndexFunc( simpleAttribute(Post.class, post -> post.getStatusOrDefault().getExcerpt()))); }); - schemeManager.register(Category.class); - schemeManager.register(Tag.class); + 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") diff --git a/application/src/main/java/run/halo/app/theme/endpoint/CategoryQueryEndpoint.java b/application/src/main/java/run/halo/app/theme/endpoint/CategoryQueryEndpoint.java index 4e21b26120..b0b6101c54 100644 --- a/application/src/main/java/run/halo/app/theme/endpoint/CategoryQueryEndpoint.java +++ b/application/src/main/java/run/halo/app/theme/endpoint/CategoryQueryEndpoint.java @@ -114,12 +114,7 @@ private Mono getByName(ServerRequest request) { private Mono 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) diff --git a/application/src/main/java/run/halo/app/theme/endpoint/TagQueryEndpoint.java b/application/src/main/java/run/halo/app/theme/endpoint/TagQueryEndpoint.java index c3ed1baada..88377f32a4 100644 --- a/application/src/main/java/run/halo/app/theme/endpoint/TagQueryEndpoint.java +++ b/application/src/main/java/run/halo/app/theme/endpoint/TagQueryEndpoint.java @@ -17,6 +17,7 @@ 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; @@ -35,6 +36,7 @@ @RequiredArgsConstructor public class TagQueryEndpoint implements CustomEndpoint { + private final ReactiveExtensionClient client; private final TagFinder tagFinder; private final PostPublicQueryService postPublicQueryService; @@ -113,11 +115,12 @@ private Mono listPostsByTagName(ServerRequest request) { private Mono 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) diff --git a/application/src/main/java/run/halo/app/theme/finders/TagFinder.java b/application/src/main/java/run/halo/app/theme/finders/TagFinder.java index 9c157e5e5d..4590f02321 100644 --- a/application/src/main/java/run/halo/app/theme/finders/TagFinder.java +++ b/application/src/main/java/run/halo/app/theme/finders/TagFinder.java @@ -24,8 +24,11 @@ public interface TagFinder { Mono> list(@Nullable Integer page, @Nullable Integer size); + @Deprecated(since = "2.12.0") Mono> list(@Nullable Integer page, @Nullable Integer size, @Nullable Predicate predicate, @Nullable Comparator comparator); + List convertToVo(List tags); + Flux listAll(); } diff --git a/application/src/main/java/run/halo/app/theme/finders/impl/CategoryFinderImpl.java b/application/src/main/java/run/halo/app/theme/finders/impl/CategoryFinderImpl.java index 43502d798f..1bfc4c893b 100644 --- a/application/src/main/java/run/halo/app/theme/finders/impl/CategoryFinderImpl.java +++ b/application/src/main/java/run/halo/app/theme/finders/impl/CategoryFinderImpl.java @@ -11,11 +11,16 @@ 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 Flux getByNames(List names) { .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> 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 categoryVos = list.get() .map(CategoryVo::from) @@ -65,12 +77,6 @@ public Mono> list(Integer page, Integer size) { .defaultIfEmpty(new ListResult<>(page, size, 0L, List.of())); } - @Override - public Flux listAll() { - return client.list(Category.class, null, defaultComparator()) - .map(CategoryVo::from); - } - @Override public Flux listAsTree() { return this.toCategoryTreeVoFlux(null); @@ -82,20 +88,9 @@ public Flux listAsTree(String name) { } @Override - public Mono getParentByName(String name) { - if (StringUtils.isBlank(name)) { - return Mono.empty(); - } - return client.list(Category.class, - category -> { - List children = category.getSpec().getChildren(); - if (children == null) { - return false; - } - return children.contains(name); - }, - defaultComparator()) - .next().map(CategoryVo::from); + public Flux listAll() { + return client.listAll(Category.class, new ListOptions(), defaultSort()) + .map(CategoryVo::from); } Flux toCategoryTreeVoFlux(String name) { @@ -169,6 +164,22 @@ static Comparator defaultComparator() { .reversed(); } + @Override + public Mono 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); } diff --git a/application/src/main/java/run/halo/app/theme/finders/impl/SiteStatsFinderImpl.java b/application/src/main/java/run/halo/app/theme/finders/impl/SiteStatsFinderImpl.java index 2966f6a626..30038539f9 100644 --- a/application/src/main/java/run/halo/app/theme/finders/impl/SiteStatsFinderImpl.java +++ b/application/src/main/java/run/halo/app/theme/finders/impl/SiteStatsFinderImpl.java @@ -10,6 +10,7 @@ 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; @@ -62,8 +63,8 @@ Mono postCount() { } Mono 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); } diff --git a/application/src/main/java/run/halo/app/theme/finders/impl/TagFinderImpl.java b/application/src/main/java/run/halo/app/theme/finders/impl/TagFinderImpl.java index d1c86b6421..0f6a135a2b 100644 --- a/application/src/main/java/run/halo/app/theme/finders/impl/TagFinderImpl.java +++ b/application/src/main/java/run/halo/app/theme/finders/impl/TagFinderImpl.java @@ -6,11 +6,16 @@ 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 Flux getByNames(List names) { @Override public Mono> 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 @@ comparatorToUse, pageNullSafe(page), sizeNullSafe(size)) new ListResult<>(pageNullSafe(page), sizeNullSafe(size), 0L, List.of())); } + @Override + public List convertToVo(List tags) { + if (CollectionUtils.isEmpty(tags)) { + return List.of(); + } + return tags.stream() + .map(TagVo::from) + .collect(Collectors.toList()); + } + @Override public Flux 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> listBy(ListOptions listOptions, PageRequest pageRequest) { + return client.listBy(Tag.class, listOptions, pageRequest) + .map(result -> { + List 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); } diff --git a/application/src/main/java/run/halo/app/theme/router/ExtensionPermalinkPatternUpdater.java b/application/src/main/java/run/halo/app/theme/router/ExtensionPermalinkPatternUpdater.java index 01b7f3b68b..45d673ba92 100644 --- a/application/src/main/java/run/halo/app/theme/router/ExtensionPermalinkPatternUpdater.java +++ b/application/src/main/java/run/halo/app/theme/router/ExtensionPermalinkPatternUpdater.java @@ -72,13 +72,13 @@ private void updateIfPermalinkPatternChanged(AbstractExtension extension, String 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)); } } diff --git a/application/src/main/java/run/halo/app/theme/router/factories/CategoryPostRouteFactory.java b/application/src/main/java/run/halo/app/theme/router/factories/CategoryPostRouteFactory.java index 4b013c0617..3bc378cdae 100644 --- a/application/src/main/java/run/halo/app/theme/router/factories/CategoryPostRouteFactory.java +++ b/application/src/main/java/run/halo/app/theme/router/factories/CategoryPostRouteFactory.java @@ -17,7 +17,12 @@ 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 @@ HandlerFunction handlerFunction() { } Mono 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> postListByCategoryName(String name, diff --git a/application/src/main/java/run/halo/app/theme/router/factories/TagPostRouteFactory.java b/application/src/main/java/run/halo/app/theme/router/factories/TagPostRouteFactory.java index 1257030eb5..7c92eb0e10 100644 --- a/application/src/main/java/run/halo/app/theme/router/factories/TagPostRouteFactory.java +++ b/application/src/main/java/run/halo/app/theme/router/factories/TagPostRouteFactory.java @@ -15,7 +15,12 @@ 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 @@ private Mono> postList(String name, Integer p } private Mono 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))); diff --git a/application/src/test/java/run/halo/app/core/extension/reconciler/CategoryReconcilerTest.java b/application/src/test/java/run/halo/app/core/extension/reconciler/CategoryReconcilerTest.java index f379b7c090..e6d16c47be 100644 --- a/application/src/test/java/run/halo/app/core/extension/reconciler/CategoryReconcilerTest.java +++ b/application/src/test/java/run/halo/app/core/extension/reconciler/CategoryReconcilerTest.java @@ -92,7 +92,7 @@ private void reconcileStatusPostPilling(String reconcileCategoryName) { 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 = diff --git a/application/src/test/java/run/halo/app/extension/ListResultTest.java b/application/src/test/java/run/halo/app/extension/ListResultTest.java index 98a118ec5d..d4455dcf4a 100644 --- a/application/src/test/java/run/halo/app/extension/ListResultTest.java +++ b/application/src/test/java/run/halo/app/extension/ListResultTest.java @@ -6,6 +6,7 @@ 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 @@ void subListWhenSizeIsZero() { 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 list) { var result = ListResult.subList(list, 0, 0); assertEquals(list, result); diff --git a/application/src/test/java/run/halo/app/theme/endpoint/CategoryQueryEndpointTest.java b/application/src/test/java/run/halo/app/theme/endpoint/CategoryQueryEndpointTest.java index 0ef8f2eee6..9ba60c6647 100644 --- a/application/src/test/java/run/halo/app/theme/endpoint/CategoryQueryEndpointTest.java +++ b/application/src/test/java/run/halo/app/theme/endpoint/CategoryQueryEndpointTest.java @@ -2,7 +2,6 @@ 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,6 +18,7 @@ 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; @@ -53,7 +53,7 @@ void setUp() { @Test void listCategories() { ListResult 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() diff --git a/application/src/test/java/run/halo/app/theme/finders/impl/CategoryFinderImplTest.java b/application/src/test/java/run/halo/app/theme/finders/impl/CategoryFinderImplTest.java index 3d5bc12801..cbe8317572 100644 --- a/application/src/test/java/run/halo/app/theme/finders/impl/CategoryFinderImplTest.java +++ b/application/src/test/java/run/halo/app/theme/finders/impl/CategoryFinderImplTest.java @@ -2,7 +2,6 @@ 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.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 @@ void list() { 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 list = categoryFinder.list(1, 10).block(); assertThat(list.getItems()).hasSize(3); @@ -95,7 +97,7 @@ void list() { @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 treeVos = categoryFinder.listAsTree().collectList().block(); assertThat(treeVos).hasSize(1); @@ -103,7 +105,7 @@ void listAsTree() { @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 treeVos = categoryFinder.listAsTree("E").collectList().block(); assertThat(treeVos.get(0).getMetadata().getName()).isEqualTo("E"); @@ -119,7 +121,7 @@ void listSubTreeByName() { */ @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 treeVos = categoryFinder.listAsTree().collectList().block(); String s = visualizeTree(treeVos); diff --git a/application/src/test/java/run/halo/app/theme/finders/impl/TagFinderImplTest.java b/application/src/test/java/run/halo/app/theme/finders/impl/TagFinderImplTest.java index 84984a841a..8008174cc8 100644 --- a/application/src/test/java/run/halo/app/theme/finders/impl/TagFinderImplTest.java +++ b/application/src/test/java/run/halo/app/theme/finders/impl/TagFinderImplTest.java @@ -16,9 +16,11 @@ 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 @@ void getByName() throws JSONException { @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() ) diff --git a/application/src/test/java/run/halo/app/theme/router/factories/TagPostRouteFactoryTest.java b/application/src/test/java/run/halo/app/theme/router/factories/TagPostRouteFactoryTest.java index 1110880bd1..c0b1cd496a 100644 --- a/application/src/test/java/run/halo/app/theme/router/factories/TagPostRouteFactoryTest.java +++ b/application/src/test/java/run/halo/app/theme/router/factories/TagPostRouteFactoryTest.java @@ -4,16 +4,18 @@ 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 @@ void create() { 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() From c3c65676647b568ce763bdaba1b61d8f6769867d Mon Sep 17 00:00:00 2001 From: Ryan Wang Date: Wed, 24 Jan 2024 12:47:36 +0800 Subject: [PATCH 9/9] Add sort for cateogries and tags fetch Signed-off-by: Ryan Wang --- .../composables/use-post-category.ts | 1 + .../posts/tags/composables/use-post-tag.ts | 1 + .../global-search/GlobalSearchModal.vue | 36 +++++++++++-------- .../src/formkit/inputs/category-checkbox.ts | 4 ++- console/src/formkit/inputs/tag-checkbox.ts | 4 ++- 5 files changed, 29 insertions(+), 17 deletions(-) diff --git a/console/console-src/modules/contents/posts/categories/composables/use-post-category.ts b/console/console-src/modules/contents/posts/categories/composables/use-post-category.ts index 6ac4316ab3..74fb64c3a0 100644 --- a/console/console-src/modules/contents/posts/categories/composables/use-post-category.ts +++ b/console/console-src/modules/contents/posts/categories/composables/use-post-category.ts @@ -32,6 +32,7 @@ export function usePostCategory(): usePostCategoryReturn { await apiClient.extension.category.listcontentHaloRunV1alpha1Category({ page: 0, size: 0, + sort: ["metadata.creationTimestamp,desc"], }); return data.items; diff --git a/console/console-src/modules/contents/posts/tags/composables/use-post-tag.ts b/console/console-src/modules/contents/posts/tags/composables/use-post-tag.ts index 11c4f8a812..494e36ac61 100644 --- a/console/console-src/modules/contents/posts/tags/composables/use-post-tag.ts +++ b/console/console-src/modules/contents/posts/tags/composables/use-post-tag.ts @@ -26,6 +26,7 @@ export function usePostTag(): usePostTagReturn { await apiClient.extension.tag.listcontentHaloRunV1alpha1Tag({ page: 0, size: 0, + sort: ["metadata.creationTimestamp,desc"], }); return data.items; diff --git a/console/src/components/global-search/GlobalSearchModal.vue b/console/src/components/global-search/GlobalSearchModal.vue index 343ec8b056..f52487dd0b 100644 --- a/console/src/components/global-search/GlobalSearchModal.vue +++ b/console/src/components/global-search/GlobalSearchModal.vue @@ -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"])) { diff --git a/console/src/formkit/inputs/category-checkbox.ts b/console/src/formkit/inputs/category-checkbox.ts index 037269d195..b0ed968cd5 100644 --- a/console/src/formkit/inputs/category-checkbox.ts +++ b/console/src/formkit/inputs/category-checkbox.ts @@ -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 { diff --git a/console/src/formkit/inputs/tag-checkbox.ts b/console/src/formkit/inputs/tag-checkbox.ts index 197f72eb43..6e02d8c31f 100644 --- a/console/src/formkit/inputs/tag-checkbox.ts +++ b/console/src/formkit/inputs/tag-checkbox.ts @@ -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 {