mirror of https://github.com/halo-dev/halo
refactor: optimize notification and subscription query using index (#5414)
### What type of PR is this? /kind improvement /area core /milestone 2.13.x ### What this PR does / why we need it: 使用索引机制优化通知和订阅查询以提高性能 how to test it 测试通知列表不报错即可 ### Does this PR introduce a user-facing change? ```release-note 使用索引机制优化通知和订阅查询以提高性能 ```pull/5422/head^2 v2.13.0-rc.1
parent
d2f569699b
commit
11114416fa
|
@ -0,0 +1,28 @@
|
|||
package run.halo.app.core.extension.notification;
|
||||
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Tests for {@link Subscription}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.13.0
|
||||
*/
|
||||
class SubscriptionTest {
|
||||
|
||||
@Test
|
||||
void reasonSubjectToStringTest() {
|
||||
Subscription.ReasonSubject subject = new Subscription.ReasonSubject();
|
||||
subject.setApiVersion("v1");
|
||||
subject.setKind("Kind");
|
||||
subject.setName("Name");
|
||||
|
||||
String expected = "Kind#v1/Name";
|
||||
String actual = subject.toString();
|
||||
|
||||
assertThat(actual).isEqualTo(expected);
|
||||
}
|
||||
}
|
|
@ -260,10 +260,58 @@ public class SchemeInitializer implements ApplicationListener<ApplicationContext
|
|||
// notification.halo.run
|
||||
schemeManager.register(ReasonType.class);
|
||||
schemeManager.register(Reason.class);
|
||||
schemeManager.register(NotificationTemplate.class);
|
||||
schemeManager.register(Subscription.class);
|
||||
schemeManager.register(NotificationTemplate.class, indexSpecs -> {
|
||||
indexSpecs.add(new IndexSpec()
|
||||
.setName("spec.reasonSelector.reasonType")
|
||||
.setIndexFunc(simpleAttribute(NotificationTemplate.class,
|
||||
template -> template.getSpec().getReasonSelector().getReasonType()))
|
||||
);
|
||||
});
|
||||
schemeManager.register(Subscription.class, indexSpecs -> {
|
||||
indexSpecs.add(new IndexSpec()
|
||||
.setName("spec.reason.reasonType")
|
||||
.setIndexFunc(simpleAttribute(Subscription.class,
|
||||
subscription -> subscription.getSpec().getReason().getReasonType()))
|
||||
);
|
||||
indexSpecs.add(new IndexSpec()
|
||||
.setName("spec.reason.subject")
|
||||
.setIndexFunc(simpleAttribute(Subscription.class,
|
||||
subscription -> subscription.getSpec().getReason().getSubject().toString()))
|
||||
);
|
||||
indexSpecs.add(new IndexSpec()
|
||||
.setName("spec.subscriber")
|
||||
.setIndexFunc(simpleAttribute(Subscription.class,
|
||||
subscription -> subscription.getSpec().getSubscriber().toString()))
|
||||
);
|
||||
});
|
||||
schemeManager.register(NotifierDescriptor.class);
|
||||
schemeManager.register(Notification.class);
|
||||
schemeManager.register(Notification.class, indexSpecs -> {
|
||||
indexSpecs.add(new IndexSpec()
|
||||
.setName("spec.unread")
|
||||
.setIndexFunc(simpleAttribute(Notification.class,
|
||||
notification -> String.valueOf(notification.getSpec().isUnread())))
|
||||
);
|
||||
indexSpecs.add(new IndexSpec()
|
||||
.setName("spec.reason")
|
||||
.setIndexFunc(simpleAttribute(Notification.class,
|
||||
notification -> notification.getSpec().getReason()))
|
||||
);
|
||||
indexSpecs.add(new IndexSpec()
|
||||
.setName("spec.recipient")
|
||||
.setIndexFunc(simpleAttribute(Notification.class,
|
||||
notification -> notification.getSpec().getRecipient()))
|
||||
);
|
||||
indexSpecs.add(new IndexSpec()
|
||||
.setName("spec.title")
|
||||
.setIndexFunc(simpleAttribute(Notification.class,
|
||||
notification -> notification.getSpec().getTitle()))
|
||||
);
|
||||
indexSpecs.add(new IndexSpec()
|
||||
.setName("spec.rawContent")
|
||||
.setIndexFunc(simpleAttribute(Notification.class,
|
||||
notification -> notification.getSpec().getRawContent()))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private static DefaultSchemeManager createSchemeManager(
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
package run.halo.app.notification;
|
||||
|
||||
import static org.apache.commons.lang3.StringUtils.defaultString;
|
||||
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.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
|
@ -12,7 +15,9 @@ import lombok.Builder;
|
|||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.Assert;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
|
@ -23,8 +28,10 @@ import run.halo.app.core.extension.notification.Reason;
|
|||
import run.halo.app.core.extension.notification.ReasonType;
|
||||
import run.halo.app.core.extension.notification.Subscription;
|
||||
import run.halo.app.extension.GroupVersionKind;
|
||||
import run.halo.app.extension.ListOptions;
|
||||
import run.halo.app.extension.Metadata;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.extension.router.selector.FieldSelector;
|
||||
import run.halo.app.notification.endpoint.SubscriptionRouter;
|
||||
|
||||
/**
|
||||
|
@ -98,9 +105,11 @@ public class DefaultNotificationCenter implements NotificationCenter {
|
|||
}
|
||||
|
||||
Flux<Subscription> listSubscription(Subscription.Subscriber subscriber) {
|
||||
return client.list(Subscription.class,
|
||||
subscription -> subscription.getSpec().getSubscriber().equals(subscriber),
|
||||
null);
|
||||
var listOptions = new ListOptions();
|
||||
listOptions.setFieldSelector(FieldSelector.of(
|
||||
equal("spec.subscriber", subscriber.toString()))
|
||||
);
|
||||
return client.listAll(Subscription.class, listOptions, defaultSort());
|
||||
}
|
||||
|
||||
Flux<String> getNotifiersBySubscriber(Subscription.Subscriber subscriber, Reason reason) {
|
||||
|
@ -310,31 +319,41 @@ public class DefaultNotificationCenter implements NotificationCenter {
|
|||
|
||||
Flux<Subscription> listObservers(String reasonTypeName,
|
||||
Subscription.ReasonSubject reasonSubject) {
|
||||
var distinctKeyPredicate = subscriptionDistinctKeyPredicate();
|
||||
return client.list(Subscription.class,
|
||||
subscription -> {
|
||||
var interestReason = subscription.getSpec().getReason();
|
||||
var sourceSubject = interestReason.getSubject();
|
||||
if (StringUtils.isBlank(sourceSubject.getName())) {
|
||||
return interestReason.getReasonType().equals(reasonTypeName)
|
||||
&& sourceSubject.getApiVersion().equals(reasonSubject.getApiVersion())
|
||||
&& sourceSubject.getKind().equals(reasonSubject.getKind());
|
||||
Assert.notNull(reasonTypeName, "The reasonTypeName must not be null");
|
||||
Assert.notNull(reasonSubject, "The reasonSubject must not be null");
|
||||
final var listOptions = new ListOptions();
|
||||
var matchAllSubject = new Subscription.ReasonSubject();
|
||||
matchAllSubject.setKind(reasonSubject.getKind());
|
||||
matchAllSubject.setApiVersion(reasonSubject.getApiVersion());
|
||||
var fieldQuery = and(equal("spec.reason.reasonType", reasonTypeName),
|
||||
or(equal("spec.reason.subject", reasonSubject.toString()),
|
||||
// source reason subject name is blank present match all
|
||||
equal("spec.reason.subject", matchAllSubject.toString())
|
||||
)
|
||||
);
|
||||
listOptions.setFieldSelector(FieldSelector.of(fieldQuery));
|
||||
return distinctByKey(client.listAll(Subscription.class, listOptions, defaultSort()));
|
||||
}
|
||||
|
||||
static Flux<Subscription> distinctByKey(Flux<Subscription> source) {
|
||||
final var distinctKeyPredicate = subscriptionDistinctKeyPredicate();
|
||||
return source.distinct(Function.identity(), HashSet<Subscription>::new,
|
||||
(set, val) -> {
|
||||
for (Subscription subscription : set) {
|
||||
if (distinctKeyPredicate.test(subscription, val)) {
|
||||
return false;
|
||||
}
|
||||
return interestReason.getReasonType().equals(reasonTypeName)
|
||||
&& interestReason.getSubject().equals(reasonSubject);
|
||||
}, null)
|
||||
.distinct(Function.identity(), HashSet<Subscription>::new,
|
||||
(set, val) -> {
|
||||
for (Subscription subscription : set) {
|
||||
if (distinctKeyPredicate.test(subscription, val)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// no duplicated return true
|
||||
set.add(val);
|
||||
return true;
|
||||
},
|
||||
HashSet::clear);
|
||||
}
|
||||
// no duplicated return true
|
||||
set.add(val);
|
||||
return true;
|
||||
},
|
||||
HashSet::clear);
|
||||
}
|
||||
|
||||
Sort defaultSort() {
|
||||
return Sort.by(Sort.Order.asc("metadata.creationTimestamp"),
|
||||
Sort.Order.asc("metadata.name"));
|
||||
}
|
||||
|
||||
static BiPredicate<Subscription, Subscription> subscriptionDistinctKeyPredicate() {
|
||||
|
|
|
@ -26,10 +26,7 @@ public class DefaultNotificationService implements UserNotificationService {
|
|||
|
||||
@Override
|
||||
public Mono<ListResult<Notification>> listByUser(String username, UserNotificationQuery query) {
|
||||
var predicate = query.toPredicate()
|
||||
.and(notification -> isRecipient(notification, username));
|
||||
return client.list(Notification.class, predicate, query.toComparator(),
|
||||
query.getPage(), query.getSize());
|
||||
return client.listBy(Notification.class, query.toListOptions(), query.toPageRequest());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package run.halo.app.notification;
|
||||
|
||||
import static org.apache.commons.lang3.StringUtils.defaultIfBlank;
|
||||
import static run.halo.app.extension.index.query.QueryFactory.equal;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
|
@ -12,11 +13,14 @@ import java.util.function.Predicate;
|
|||
import java.util.stream.Collectors;
|
||||
import lombok.NonNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.stereotype.Component;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.extension.notification.NotificationTemplate;
|
||||
import run.halo.app.extension.ListOptions;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.extension.router.selector.FieldSelector;
|
||||
|
||||
/**
|
||||
* A default implementation of {@link ReasonNotificationTemplateSelector}.
|
||||
|
@ -32,7 +36,11 @@ public class ReasonNotificationTemplateSelectorImpl implements ReasonNotificatio
|
|||
|
||||
@Override
|
||||
public Mono<NotificationTemplate> select(String reasonType, Locale locale) {
|
||||
return client.list(NotificationTemplate.class, matchReasonType(reasonType), null)
|
||||
var listOptions = new ListOptions();
|
||||
listOptions.setFieldSelector(FieldSelector.of(
|
||||
equal("spec.reasonSelector.reasonType", reasonType))
|
||||
);
|
||||
return client.listAll(NotificationTemplate.class, listOptions, Sort.unsorted())
|
||||
.collect(Collectors.groupingBy(
|
||||
getLanguageKey(),
|
||||
Collectors.maxBy(Comparator.comparing(t -> t.getMetadata().getCreationTimestamp()))
|
||||
|
|
|
@ -1,21 +1,23 @@
|
|||
package run.halo.app.notification;
|
||||
|
||||
import static java.util.Comparator.comparing;
|
||||
import static run.halo.app.extension.index.query.QueryFactory.and;
|
||||
import static run.halo.app.extension.index.query.QueryFactory.contains;
|
||||
import static run.halo.app.extension.index.query.QueryFactory.equal;
|
||||
import static run.halo.app.extension.index.query.QueryFactory.or;
|
||||
import static run.halo.app.extension.router.selector.SelectorUtil.labelAndFieldSelectorToListOptions;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.function.Predicate;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import run.halo.app.core.extension.endpoint.SortResolver;
|
||||
import run.halo.app.core.extension.notification.Notification;
|
||||
import run.halo.app.extension.Comparators;
|
||||
import run.halo.app.extension.ListOptions;
|
||||
import run.halo.app.extension.PageRequest;
|
||||
import run.halo.app.extension.PageRequestImpl;
|
||||
import run.halo.app.extension.router.IListRequest;
|
||||
import run.halo.app.extension.router.selector.FieldSelector;
|
||||
|
||||
/**
|
||||
* Notification query object for authenticated user.
|
||||
|
@ -27,9 +29,12 @@ public class UserNotificationQuery extends IListRequest.QueryListRequest {
|
|||
|
||||
private final ServerWebExchange exchange;
|
||||
|
||||
public UserNotificationQuery(ServerWebExchange exchange) {
|
||||
private final String username;
|
||||
|
||||
public UserNotificationQuery(ServerWebExchange exchange, String username) {
|
||||
super(exchange.getRequest().getQueryParams());
|
||||
this.exchange = exchange;
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
|
@ -37,79 +42,44 @@ public class UserNotificationQuery extends IListRequest.QueryListRequest {
|
|||
return StringUtils.defaultIfBlank(queryParams.getFirst("keyword"), null);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Schema(description = "true for unread, false for read, null for all")
|
||||
public Boolean getUnRead() {
|
||||
var unreadStr = queryParams.getFirst("unRead");
|
||||
return StringUtils.isBlank(unreadStr) ? null : Boolean.parseBoolean(unreadStr);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Schema(description = "Filter by notification reason")
|
||||
public String getReason() {
|
||||
return StringUtils.defaultIfBlank(queryParams.getFirst("reason"), null);
|
||||
}
|
||||
|
||||
@ArraySchema(uniqueItems = true,
|
||||
arraySchema = @Schema(name = "sort",
|
||||
description = "Sort property and direction of the list result. Supported fields: "
|
||||
+ "creationTimestamp"),
|
||||
+ "metadata.creationTimestamp"),
|
||||
schema = @Schema(description = "like field,asc or field,desc",
|
||||
implementation = String.class,
|
||||
example = "creationTimestamp,desc"))
|
||||
public Sort getSort() {
|
||||
return SortResolver.defaultInstance.resolve(exchange);
|
||||
var sort = SortResolver.defaultInstance.resolve(exchange);
|
||||
return sort.and(Sort.by(
|
||||
Sort.Order.desc("metadata.creationTimestamp"),
|
||||
Sort.Order.desc("metadata.name"))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a predicate from the query object.
|
||||
*
|
||||
* @return a predicate
|
||||
* Build a list options from the query object.
|
||||
*/
|
||||
public Predicate<Notification> toPredicate() {
|
||||
var unRead = getUnRead();
|
||||
var reason = getReason();
|
||||
Predicate<Notification> predicate = notification -> true;
|
||||
if (unRead != null) {
|
||||
predicate = predicate.and(notification
|
||||
-> notification.getSpec().isUnread() == unRead);
|
||||
public ListOptions toListOptions() {
|
||||
var listOptions =
|
||||
labelAndFieldSelectorToListOptions(getLabelSelector(), getFieldSelector());
|
||||
var filedQuery = listOptions.getFieldSelector().query();
|
||||
if (StringUtils.isNotBlank(getKeyword())) {
|
||||
filedQuery = and(filedQuery,
|
||||
or(
|
||||
contains("spec.title", getKeyword()),
|
||||
contains("spec.rawContent", getKeyword())
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (reason != null) {
|
||||
predicate = predicate.and(notification
|
||||
-> reason.equals(notification.getSpec().getReason()));
|
||||
if (StringUtils.isNotBlank(username)) {
|
||||
filedQuery = and(filedQuery, equal("spec.recipient", username));
|
||||
}
|
||||
|
||||
if (getKeyword() != null) {
|
||||
predicate = predicate.and(notification
|
||||
-> notification.getSpec().getTitle().contains(getKeyword())
|
||||
|| notification.getSpec().getHtmlContent().contains(getKeyword())
|
||||
|| notification.getSpec().getRawContent().contains(getKeyword()));
|
||||
}
|
||||
return predicate;
|
||||
listOptions.setFieldSelector(FieldSelector.of(filedQuery));
|
||||
return listOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a comparator from the query object.
|
||||
*
|
||||
* @return a comparator
|
||||
*/
|
||||
public Comparator<Notification> toComparator() {
|
||||
var sort = getSort();
|
||||
var creationTimestampOrder = sort.getOrderFor("creationTimestamp");
|
||||
List<Comparator<Notification>> comparators = new ArrayList<>();
|
||||
if (creationTimestampOrder != null) {
|
||||
Comparator<Notification> comparator =
|
||||
comparing(notification -> notification.getMetadata().getCreationTimestamp());
|
||||
if (creationTimestampOrder.isDescending()) {
|
||||
comparator = comparator.reversed();
|
||||
}
|
||||
comparators.add(comparator);
|
||||
}
|
||||
|
||||
comparators.add(Comparators.defaultComparator());
|
||||
return comparators.stream()
|
||||
.reduce(Comparator::thenComparing)
|
||||
.orElse(null);
|
||||
public PageRequest toPageRequest() {
|
||||
return PageRequestImpl.of(getPage(), getSize(), getSort());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -143,8 +143,8 @@ public class UserNotificationEndpoint implements CustomEndpoint {
|
|||
}
|
||||
|
||||
private Mono<ServerResponse> listNotification(ServerRequest request) {
|
||||
var query = new UserNotificationQuery(request.exchange());
|
||||
var username = request.pathVariable("username");
|
||||
var query = new UserNotificationQuery(request.exchange(), username);
|
||||
return notificationService.listByUser(username, query)
|
||||
.flatMap(notifications -> ServerResponse.ok().bodyValue(notifications));
|
||||
}
|
||||
|
|
|
@ -13,7 +13,6 @@ import static org.mockito.Mockito.when;
|
|||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.function.Predicate;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
@ -362,63 +361,8 @@ class DefaultNotificationCenterTest {
|
|||
verify(notificationTemplateSelector).select(eq(reasonTypeName), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void listSubscriptionTest() {
|
||||
var subscriptions = createSubscriptions();
|
||||
|
||||
when(client.list(eq(Subscription.class), any(Predicate.class), any()))
|
||||
.thenAnswer(answer -> {
|
||||
var predicate = (Predicate<Subscription>) answer.getArgument(1, Predicate.class);
|
||||
return Flux.fromIterable(subscriptions)
|
||||
.filter(predicate);
|
||||
});
|
||||
|
||||
var subscription = subscriptions.get(0);
|
||||
var subscriber = subscription.getSpec().getSubscriber();
|
||||
notificationCenter.listSubscription(subscriber)
|
||||
.as(StepVerifier::create)
|
||||
.expectNext(subscription)
|
||||
.verifyComplete();
|
||||
|
||||
verify(client).list(eq(Subscription.class), any(Predicate.class), any());
|
||||
|
||||
var otherSubscriber = JsonUtils.deepCopy(subscriber);
|
||||
otherSubscriber.setName("other");
|
||||
notificationCenter.listSubscription(otherSubscriber)
|
||||
.as(StepVerifier::create)
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void listObserversTest() {
|
||||
var subscriptions = createSubscriptions();
|
||||
|
||||
when(client.list(eq(Subscription.class), any(Predicate.class), any()))
|
||||
.thenAnswer(answer -> {
|
||||
var predicate = (Predicate<Subscription>) answer.getArgument(1, Predicate.class);
|
||||
return Flux.fromIterable(subscriptions)
|
||||
.filter(predicate);
|
||||
});
|
||||
|
||||
var subscription = subscriptions.get(0);
|
||||
var reasonTypeName = subscription.getSpec().getReason().getReasonType();
|
||||
var reasonSubject = subscription.getSpec().getReason().getSubject();
|
||||
notificationCenter.listObservers(reasonTypeName, reasonSubject)
|
||||
.as(StepVerifier::create)
|
||||
.expectNext(subscription)
|
||||
.verifyComplete();
|
||||
|
||||
verify(client).list(eq(Subscription.class), any(Predicate.class), any());
|
||||
|
||||
notificationCenter.listObservers("other-reason", reasonSubject)
|
||||
.as(StepVerifier::create)
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void listObserverWhenDuplicateSubscribers() {
|
||||
var sourceSubscriptions = createSubscriptions();
|
||||
var subscriptionA = sourceSubscriptions.get(0);
|
||||
|
@ -427,26 +371,11 @@ class DefaultNotificationCenterTest {
|
|||
var subscriptionC = JsonUtils.deepCopy(subscriptionA);
|
||||
subscriptionC.getSpec().getReason().getSubject().setName(null);
|
||||
|
||||
var subscriptions = List.of(subscriptionA, subscriptionB, subscriptionC);
|
||||
when(client.list(eq(Subscription.class), any(Predicate.class), any()))
|
||||
.thenAnswer(answer -> {
|
||||
var predicate = (Predicate<Subscription>) answer.getArgument(1, Predicate.class);
|
||||
return Flux.fromIterable(subscriptions)
|
||||
.filter(predicate);
|
||||
});
|
||||
var subscriptions = Flux.just(subscriptionA, subscriptionB, subscriptionC);
|
||||
|
||||
var subscription = subscriptions.get(0);
|
||||
var reasonTypeName = subscription.getSpec().getReason().getReasonType();
|
||||
var reasonSubject = subscription.getSpec().getReason().getSubject();
|
||||
notificationCenter.listObservers(reasonTypeName, reasonSubject)
|
||||
.as(StepVerifier::create)
|
||||
.expectNext(subscription)
|
||||
.verifyComplete();
|
||||
|
||||
verify(client).list(eq(Subscription.class), any(Predicate.class), any());
|
||||
|
||||
notificationCenter.listObservers("other-reason", reasonSubject)
|
||||
DefaultNotificationCenter.distinctByKey(subscriptions)
|
||||
.as(StepVerifier::create)
|
||||
.expectNext(subscriptionA)
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ import org.junit.jupiter.api.extension.ExtendWith;
|
|||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.test.StepVerifier;
|
||||
import run.halo.app.core.extension.notification.NotificationTemplate;
|
||||
|
@ -41,7 +42,7 @@ class ReasonNotificationTemplateSelectorImplTest {
|
|||
|
||||
@Test
|
||||
void select() {
|
||||
when(client.list(eq(NotificationTemplate.class), any(), any()))
|
||||
when(client.listAll(eq(NotificationTemplate.class), any(), any(Sort.class)))
|
||||
.thenReturn(Flux.fromIterable(templates()));
|
||||
// language priority: zh_CN -> zh -> default
|
||||
// if language is same, then compare creationTimestamp to get the latest one
|
||||
|
|
|
@ -28,7 +28,7 @@ const {
|
|||
username: currentUser?.metadata.name as string,
|
||||
page: 1,
|
||||
size: 20,
|
||||
unRead: true,
|
||||
fieldSelector: ["spec.unread=true"],
|
||||
});
|
||||
|
||||
return data.items;
|
||||
|
|
|
@ -229,6 +229,7 @@ models/reason-type-spec.ts
|
|||
models/reason-type.ts
|
||||
models/reason.ts
|
||||
models/ref.ts
|
||||
models/register-verify-email-request.ts
|
||||
models/reply-list.ts
|
||||
models/reply-request.ts
|
||||
models/reply-spec.ts
|
||||
|
|
|
@ -36,7 +36,6 @@ import {
|
|||
RequestArgs,
|
||||
BaseAPI,
|
||||
RequiredError,
|
||||
operationServerMap,
|
||||
} from "../base";
|
||||
// @ts-ignore
|
||||
import { PasswordResetEmailRequest } from "../models";
|
||||
|
@ -48,7 +47,6 @@ import { ResetPasswordRequest } from "../models";
|
|||
import { SignUpRequest } from "../models";
|
||||
// @ts-ignore
|
||||
import { User } from "../models";
|
||||
|
||||
/**
|
||||
* ApiHaloRunV1alpha1UserApi - axios parameter creator
|
||||
* @export
|
||||
|
@ -338,18 +336,12 @@ export const ApiHaloRunV1alpha1UserApiFp = function (
|
|||
resetPasswordRequest,
|
||||
options
|
||||
);
|
||||
const index = configuration?.serverIndex ?? 0;
|
||||
const operationBasePath =
|
||||
operationServerMap["ApiHaloRunV1alpha1UserApi.resetPasswordByToken"]?.[
|
||||
index
|
||||
]?.url;
|
||||
return (axios, basePath) =>
|
||||
createRequestFunction(
|
||||
localVarAxiosArgs,
|
||||
globalAxios,
|
||||
BASE_PATH,
|
||||
configuration
|
||||
)(axios, operationBasePath || basePath);
|
||||
return createRequestFunction(
|
||||
localVarAxiosArgs,
|
||||
globalAxios,
|
||||
BASE_PATH,
|
||||
configuration
|
||||
);
|
||||
},
|
||||
/**
|
||||
* Send password reset email when forgot password
|
||||
|
@ -368,18 +360,12 @@ export const ApiHaloRunV1alpha1UserApiFp = function (
|
|||
passwordResetEmailRequest,
|
||||
options
|
||||
);
|
||||
const index = configuration?.serverIndex ?? 0;
|
||||
const operationBasePath =
|
||||
operationServerMap[
|
||||
"ApiHaloRunV1alpha1UserApi.sendPasswordResetEmail"
|
||||
]?.[index]?.url;
|
||||
return (axios, basePath) =>
|
||||
createRequestFunction(
|
||||
localVarAxiosArgs,
|
||||
globalAxios,
|
||||
BASE_PATH,
|
||||
configuration
|
||||
)(axios, operationBasePath || basePath);
|
||||
return createRequestFunction(
|
||||
localVarAxiosArgs,
|
||||
globalAxios,
|
||||
BASE_PATH,
|
||||
configuration
|
||||
);
|
||||
},
|
||||
/**
|
||||
* Send registration verification email, which can be called when mustVerifyEmailOnRegistration in user settings is true
|
||||
|
@ -398,18 +384,12 @@ export const ApiHaloRunV1alpha1UserApiFp = function (
|
|||
registerVerifyEmailRequest,
|
||||
options
|
||||
);
|
||||
const index = configuration?.serverIndex ?? 0;
|
||||
const operationBasePath =
|
||||
operationServerMap[
|
||||
"ApiHaloRunV1alpha1UserApi.sendRegisterVerifyEmail"
|
||||
]?.[index]?.url;
|
||||
return (axios, basePath) =>
|
||||
createRequestFunction(
|
||||
localVarAxiosArgs,
|
||||
globalAxios,
|
||||
BASE_PATH,
|
||||
configuration
|
||||
)(axios, operationBasePath || basePath);
|
||||
return createRequestFunction(
|
||||
localVarAxiosArgs,
|
||||
globalAxios,
|
||||
BASE_PATH,
|
||||
configuration
|
||||
);
|
||||
},
|
||||
/**
|
||||
* Sign up a new user
|
||||
|
@ -427,16 +407,12 @@ export const ApiHaloRunV1alpha1UserApiFp = function (
|
|||
signUpRequest,
|
||||
options
|
||||
);
|
||||
const index = configuration?.serverIndex ?? 0;
|
||||
const operationBasePath =
|
||||
operationServerMap["ApiHaloRunV1alpha1UserApi.signUp"]?.[index]?.url;
|
||||
return (axios, basePath) =>
|
||||
createRequestFunction(
|
||||
localVarAxiosArgs,
|
||||
globalAxios,
|
||||
BASE_PATH,
|
||||
configuration
|
||||
)(axios, operationBasePath || basePath);
|
||||
return createRequestFunction(
|
||||
localVarAxiosArgs,
|
||||
globalAxios,
|
||||
BASE_PATH,
|
||||
configuration
|
||||
);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -176,10 +176,8 @@ export const ApiNotificationHaloRunV1alpha1NotificationApiAxiosParamCreator =
|
|||
* @param {string} [keyword]
|
||||
* @param {Array<string>} [labelSelector] Label selector for filtering.
|
||||
* @param {number} [page] The page number. Zero indicates no page.
|
||||
* @param {string} [reason] Filter by notification reason
|
||||
* @param {number} [size] Size of one page. Zero indicates no limit.
|
||||
* @param {Array<string>} [sort] Sort property and direction of the list result. Supported fields: creationTimestamp
|
||||
* @param {boolean} [unRead] true for unread, false for read, null for all
|
||||
* @param {Array<string>} [sort] Sort property and direction of the list result. Supported fields: metadata.creationTimestamp
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
|
@ -189,10 +187,8 @@ export const ApiNotificationHaloRunV1alpha1NotificationApiAxiosParamCreator =
|
|||
keyword?: string,
|
||||
labelSelector?: Array<string>,
|
||||
page?: number,
|
||||
reason?: string,
|
||||
size?: number,
|
||||
sort?: Array<string>,
|
||||
unRead?: boolean,
|
||||
options: AxiosRequestConfig = {}
|
||||
): Promise<RequestArgs> => {
|
||||
// verify required parameter 'username' is not null or undefined
|
||||
|
@ -241,10 +237,6 @@ export const ApiNotificationHaloRunV1alpha1NotificationApiAxiosParamCreator =
|
|||
localVarQueryParameter["page"] = page;
|
||||
}
|
||||
|
||||
if (reason !== undefined) {
|
||||
localVarQueryParameter["reason"] = reason;
|
||||
}
|
||||
|
||||
if (size !== undefined) {
|
||||
localVarQueryParameter["size"] = size;
|
||||
}
|
||||
|
@ -253,10 +245,6 @@ export const ApiNotificationHaloRunV1alpha1NotificationApiAxiosParamCreator =
|
|||
localVarQueryParameter["sort"] = Array.from(sort);
|
||||
}
|
||||
|
||||
if (unRead !== undefined) {
|
||||
localVarQueryParameter["unRead"] = unRead;
|
||||
}
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions =
|
||||
baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
|
@ -540,10 +528,8 @@ export const ApiNotificationHaloRunV1alpha1NotificationApiFp = function (
|
|||
* @param {string} [keyword]
|
||||
* @param {Array<string>} [labelSelector] Label selector for filtering.
|
||||
* @param {number} [page] The page number. Zero indicates no page.
|
||||
* @param {string} [reason] Filter by notification reason
|
||||
* @param {number} [size] Size of one page. Zero indicates no limit.
|
||||
* @param {Array<string>} [sort] Sort property and direction of the list result. Supported fields: creationTimestamp
|
||||
* @param {boolean} [unRead] true for unread, false for read, null for all
|
||||
* @param {Array<string>} [sort] Sort property and direction of the list result. Supported fields: metadata.creationTimestamp
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
|
@ -553,10 +539,8 @@ export const ApiNotificationHaloRunV1alpha1NotificationApiFp = function (
|
|||
keyword?: string,
|
||||
labelSelector?: Array<string>,
|
||||
page?: number,
|
||||
reason?: string,
|
||||
size?: number,
|
||||
sort?: Array<string>,
|
||||
unRead?: boolean,
|
||||
options?: AxiosRequestConfig
|
||||
): Promise<
|
||||
(
|
||||
|
@ -571,10 +555,8 @@ export const ApiNotificationHaloRunV1alpha1NotificationApiFp = function (
|
|||
keyword,
|
||||
labelSelector,
|
||||
page,
|
||||
reason,
|
||||
size,
|
||||
sort,
|
||||
unRead,
|
||||
options
|
||||
);
|
||||
return createRequestFunction(
|
||||
|
@ -732,10 +714,8 @@ export const ApiNotificationHaloRunV1alpha1NotificationApiFactory = function (
|
|||
requestParameters.keyword,
|
||||
requestParameters.labelSelector,
|
||||
requestParameters.page,
|
||||
requestParameters.reason,
|
||||
requestParameters.size,
|
||||
requestParameters.sort,
|
||||
requestParameters.unRead,
|
||||
options
|
||||
)
|
||||
.then((request) => request(axios, basePath));
|
||||
|
@ -873,13 +853,6 @@ export interface ApiNotificationHaloRunV1alpha1NotificationApiListUserNotificati
|
|||
*/
|
||||
readonly page?: number;
|
||||
|
||||
/**
|
||||
* Filter by notification reason
|
||||
* @type {string}
|
||||
* @memberof ApiNotificationHaloRunV1alpha1NotificationApiListUserNotifications
|
||||
*/
|
||||
readonly reason?: string;
|
||||
|
||||
/**
|
||||
* Size of one page. Zero indicates no limit.
|
||||
* @type {number}
|
||||
|
@ -888,18 +861,11 @@ export interface ApiNotificationHaloRunV1alpha1NotificationApiListUserNotificati
|
|||
readonly size?: number;
|
||||
|
||||
/**
|
||||
* Sort property and direction of the list result. Supported fields: creationTimestamp
|
||||
* Sort property and direction of the list result. Supported fields: metadata.creationTimestamp
|
||||
* @type {Array<string>}
|
||||
* @memberof ApiNotificationHaloRunV1alpha1NotificationApiListUserNotifications
|
||||
*/
|
||||
readonly sort?: Array<string>;
|
||||
|
||||
/**
|
||||
* true for unread, false for read, null for all
|
||||
* @type {boolean}
|
||||
* @memberof ApiNotificationHaloRunV1alpha1NotificationApiListUserNotifications
|
||||
*/
|
||||
readonly unRead?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1026,10 +992,8 @@ export class ApiNotificationHaloRunV1alpha1NotificationApi extends BaseAPI {
|
|||
requestParameters.keyword,
|
||||
requestParameters.labelSelector,
|
||||
requestParameters.page,
|
||||
requestParameters.reason,
|
||||
requestParameters.size,
|
||||
requestParameters.sort,
|
||||
requestParameters.unRead,
|
||||
options
|
||||
)
|
||||
.then((request) => request(this.axios, this.basePath));
|
||||
|
|
|
@ -73,16 +73,3 @@ export class RequiredError extends Error {
|
|||
this.name = "RequiredError";
|
||||
}
|
||||
}
|
||||
|
||||
interface ServerMap {
|
||||
[key: string]: {
|
||||
url: string,
|
||||
description: string,
|
||||
}[];
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
*/
|
||||
export const operationServerMap: ServerMap = {};
|
||||
|
|
|
@ -13,12 +13,19 @@
|
|||
*/
|
||||
|
||||
export interface ConfigurationParameters {
|
||||
apiKey?: string | Promise<string> | ((name: string) => string) | ((name: string) => Promise<string>);
|
||||
apiKey?:
|
||||
| string
|
||||
| Promise<string>
|
||||
| ((name: string) => string)
|
||||
| ((name: string) => Promise<string>);
|
||||
username?: string;
|
||||
password?: string;
|
||||
accessToken?: string | Promise<string> | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise<string>);
|
||||
accessToken?:
|
||||
| string
|
||||
| Promise<string>
|
||||
| ((name?: string, scopes?: string[]) => string)
|
||||
| ((name?: string, scopes?: string[]) => Promise<string>);
|
||||
basePath?: string;
|
||||
serverIndex?: number;
|
||||
baseOptions?: any;
|
||||
formDataCtor?: new () => any;
|
||||
}
|
||||
|
@ -29,7 +36,11 @@ export class Configuration {
|
|||
* @param name security name
|
||||
* @memberof Configuration
|
||||
*/
|
||||
apiKey?: string | Promise<string> | ((name: string) => string) | ((name: string) => Promise<string>);
|
||||
apiKey?:
|
||||
| string
|
||||
| Promise<string>
|
||||
| ((name: string) => string)
|
||||
| ((name: string) => Promise<string>);
|
||||
/**
|
||||
* parameter for basic security
|
||||
*
|
||||
|
@ -50,7 +61,11 @@ export class Configuration {
|
|||
* @param scopes oauth2 scope
|
||||
* @memberof Configuration
|
||||
*/
|
||||
accessToken?: string | Promise<string> | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise<string>);
|
||||
accessToken?:
|
||||
| string
|
||||
| Promise<string>
|
||||
| ((name?: string, scopes?: string[]) => string)
|
||||
| ((name?: string, scopes?: string[]) => Promise<string>);
|
||||
/**
|
||||
* override base path
|
||||
*
|
||||
|
@ -58,13 +73,6 @@ export class Configuration {
|
|||
* @memberof Configuration
|
||||
*/
|
||||
basePath?: string;
|
||||
/**
|
||||
* override server index
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof Configuration
|
||||
*/
|
||||
serverIndex?: number;
|
||||
/**
|
||||
* base options for axios calls
|
||||
*
|
||||
|
@ -87,7 +95,6 @@ export class Configuration {
|
|||
this.password = param.password;
|
||||
this.accessToken = param.accessToken;
|
||||
this.basePath = param.basePath;
|
||||
this.serverIndex = param.serverIndex;
|
||||
this.baseOptions = param.baseOptions;
|
||||
this.formDataCtor = param.formDataCtor;
|
||||
}
|
||||
|
@ -103,7 +110,14 @@ export class Configuration {
|
|||
* @return True if the given MIME is JSON, false otherwise.
|
||||
*/
|
||||
public isJsonMime(mime: string): boolean {
|
||||
const jsonMime: RegExp = new RegExp("^(application\/json|[^;/ \t]+\/[^;/ \t]+[+]json)[ \t]*(;.*)?$", "i");
|
||||
return mime !== null && (jsonMime.test(mime) || mime.toLowerCase() === "application/json-patch+json");
|
||||
const jsonMime: RegExp = new RegExp(
|
||||
"^(application/json|[^;/ \t]+/[^;/ \t]+[+]json)[ \t]*(;.*)?$",
|
||||
"i"
|
||||
);
|
||||
return (
|
||||
mime !== null &&
|
||||
(jsonMime.test(mime) ||
|
||||
mime.toLowerCase() === "application/json-patch+json")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,10 +24,10 @@ import { User } from "./user";
|
|||
export interface SignUpRequest {
|
||||
/**
|
||||
*
|
||||
* @type {any}
|
||||
* @type {string}
|
||||
* @memberof SignUpRequest
|
||||
*/
|
||||
password: any;
|
||||
password: string;
|
||||
/**
|
||||
*
|
||||
* @type {User}
|
||||
|
@ -36,8 +36,8 @@ export interface SignUpRequest {
|
|||
user: User;
|
||||
/**
|
||||
*
|
||||
* @type {any}
|
||||
* @type {string}
|
||||
* @memberof SignUpRequest
|
||||
*/
|
||||
verifyCode?: any;
|
||||
verifyCode?: string;
|
||||
}
|
||||
|
|
|
@ -31,7 +31,7 @@ const {
|
|||
queryFn: async () => {
|
||||
const { data } = await apiClient.notification.listUserNotifications({
|
||||
username: currentUser?.metadata.name as string,
|
||||
unRead: activeTab.value === "unread",
|
||||
fieldSelector: [`spec.unread=${activeTab.value === "unread"}`],
|
||||
});
|
||||
|
||||
return data;
|
||||
|
|
Loading…
Reference in New Issue