Compare commits
21 Commits
3e9ff90418
...
808f16edfb
Author | SHA1 | Date |
---|---|---|
Takagi | 808f16edfb | |
LIlGG | b48476448b | |
John Niang | fe809c10a1 | |
guqing | 9b3f00dab0 | |
John Niang | fa286f74ee | |
John Niang | 7ea414dd6d | |
Ryan Wang | 2178bd8b80 | |
Ryan Wang | 1f71532327 | |
croatialu | 2bcab942c1 | |
guqing | 5770ad4c55 | |
Ryan Wang | 966558d1ce | |
Ryan Wang | d1d4705705 | |
Cedric | 546d63740b | |
guqing | d86ddf4a04 | |
Takagi | 3916d5b8e5 | |
Takagi | 8abae05be7 | |
guqing | 0e17d53ede | |
Ryan Wang | 58f82d2cc2 | |
guqing | 1ade8493da | |
Takagi | cb6836aa8c | |
guqing | c0de807b9e |
|
@ -19,6 +19,10 @@ on:
|
|||
types:
|
||||
- published
|
||||
|
||||
concurrency:
|
||||
group: ${{github.workflow}} - ${{github.ref}}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
if: github.event_name == 'pull_request' || github.event_name == 'push'
|
||||
|
@ -30,6 +34,7 @@ jobs:
|
|||
- name: Check Halo
|
||||
run: ./gradlew check
|
||||
- name: Upload coverage reports to Codecov
|
||||
if: github.repository == 'halo-dev/halo'
|
||||
uses: codecov/codecov-action@v4
|
||||
|
||||
build:
|
||||
|
@ -51,6 +56,7 @@ jobs:
|
|||
- name: Build Halo
|
||||
run: ./gradlew clean && ./gradlew downloadPluginPresets && ./gradlew build -x check
|
||||
- name: Upload Artifacts
|
||||
if: github.repository == 'halo-dev/halo'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: halo-artifacts
|
||||
|
@ -69,6 +75,7 @@ jobs:
|
|||
name: halo-artifacts
|
||||
path: application/build/libs
|
||||
- name: Upload Artifacts
|
||||
if: github.repository == 'halo-dev/halo'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: gh release upload ${{ github.event.release.tag_name }} application/build/libs/*
|
||||
|
|
|
@ -23,4 +23,6 @@ ENV JVM_OPTS="-Xmx256m -Xms256m" \
|
|||
RUN ln -sf /usr/share/zoneinfo/$TZ /etc/localtime \
|
||||
&& echo $TZ > /etc/timezone
|
||||
|
||||
Expose 8090
|
||||
|
||||
ENTRYPOINT ["sh", "-c", "java ${JVM_OPTS} org.springframework.boot.loader.launch.JarLauncher ${0} ${@}"]
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
## 快速开始
|
||||
|
||||
```bash
|
||||
docker run -d --name halo -p 8090:8090 -v ~/.halo2:/root/.halo2 halohub/halo:2.14
|
||||
docker run -d --name halo -p 8090:8090 -v ~/.halo2:/root/.halo2 halohub/halo:2.15
|
||||
```
|
||||
|
||||
以上仅作为体验使用,详细部署文档请查阅:<https://docs.halo.run/getting-started/install/docker-compose>
|
||||
|
|
|
@ -2933,6 +2933,80 @@
|
|||
}
|
||||
},
|
||||
"/apis/api.console.halo.run/v1alpha1/posts/{name}/content": {
|
||||
"delete": {
|
||||
"description": "Delete a content for post.",
|
||||
"operationId": "deletePostContent",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "name",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "snapshotName",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"default": {
|
||||
"content": {
|
||||
"*/*": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ContentWrapper"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "default response"
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"api.console.halo.run/v1alpha1/Post"
|
||||
]
|
||||
},
|
||||
"get": {
|
||||
"description": "Fetch content of post.",
|
||||
"operationId": "fetchPostContent",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "name",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "snapshotName",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"default": {
|
||||
"content": {
|
||||
"*/*": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ContentWrapper"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "default response"
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"api.console.halo.run/v1alpha1/Post"
|
||||
]
|
||||
},
|
||||
"put": {
|
||||
"description": "Update a post\u0027s content.",
|
||||
"operationId": "UpdatePostContent",
|
||||
|
@ -3094,6 +3168,81 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"/apis/api.console.halo.run/v1alpha1/posts/{name}/revert-content": {
|
||||
"put": {
|
||||
"description": "Revert to specified snapshot for post content.",
|
||||
"operationId": "revertToSpecifiedSnapshotForPost",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "name",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/RevertSnapshotForPostParam"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"default": {
|
||||
"content": {
|
||||
"*/*": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Post"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "default response"
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"api.console.halo.run/v1alpha1/Post"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/apis/api.console.halo.run/v1alpha1/posts/{name}/snapshot": {
|
||||
"get": {
|
||||
"description": "List all snapshots for post content.",
|
||||
"operationId": "listPostSnapshots",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "name",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"default": {
|
||||
"content": {
|
||||
"*/*": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ListedSnapshotDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "default response"
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"api.console.halo.run/v1alpha1/Post"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/apis/api.console.halo.run/v1alpha1/posts/{name}/unpublish": {
|
||||
"put": {
|
||||
"description": "Publish a post.",
|
||||
|
@ -3399,6 +3548,80 @@
|
|||
}
|
||||
},
|
||||
"/apis/api.console.halo.run/v1alpha1/singlepages/{name}/content": {
|
||||
"delete": {
|
||||
"description": "Delete a content for post.",
|
||||
"operationId": "deleteSinglePageContent",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "name",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "snapshotName",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"default": {
|
||||
"content": {
|
||||
"*/*": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ContentWrapper"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "default response"
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"api.console.halo.run/v1alpha1/SinglePage"
|
||||
]
|
||||
},
|
||||
"get": {
|
||||
"description": "Fetch content of single page.",
|
||||
"operationId": "fetchSinglePageContent",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "name",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "snapshotName",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"default": {
|
||||
"content": {
|
||||
"*/*": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ContentWrapper"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "default response"
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"api.console.halo.run/v1alpha1/SinglePage"
|
||||
]
|
||||
},
|
||||
"put": {
|
||||
"description": "Update a single page\u0027s content.",
|
||||
"operationId": "UpdateSinglePageContent",
|
||||
|
@ -3532,6 +3755,81 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"/apis/api.console.halo.run/v1alpha1/singlepages/{name}/revert-content": {
|
||||
"put": {
|
||||
"description": "Revert to specified snapshot for single page content.",
|
||||
"operationId": "revertToSpecifiedSnapshotForSinglePage",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "name",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/RevertSnapshotForSingleParam"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"default": {
|
||||
"content": {
|
||||
"*/*": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Post"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "default response"
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"api.console.halo.run/v1alpha1/SinglePage"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/apis/api.console.halo.run/v1alpha1/singlepages/{name}/snapshot": {
|
||||
"get": {
|
||||
"description": "List all snapshots for single page content.",
|
||||
"operationId": "listSinglePageSnapshots",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "name",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"default": {
|
||||
"content": {
|
||||
"*/*": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ListedSnapshotDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "default response"
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"api.console.halo.run/v1alpha1/SinglePage"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/apis/api.console.halo.run/v1alpha1/stats": {
|
||||
"get": {
|
||||
"description": "Get stats.",
|
||||
|
@ -13890,6 +14188,24 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"ContentUpdateParam": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "string"
|
||||
},
|
||||
"raw": {
|
||||
"type": "string"
|
||||
},
|
||||
"rawType": {
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ContentVo": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -14687,6 +15003,10 @@
|
|||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"expression": {
|
||||
"type": "string",
|
||||
"description": "The expression to be interested in"
|
||||
},
|
||||
"reasonType": {
|
||||
"type": "string",
|
||||
"description": "The name of the reason definition to be interested in"
|
||||
|
@ -15373,6 +15693,36 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"ListedSnapshotDto": {
|
||||
"required": [
|
||||
"metadata",
|
||||
"spec"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"metadata": {
|
||||
"$ref": "#/components/schemas/Metadata"
|
||||
},
|
||||
"spec": {
|
||||
"$ref": "#/components/schemas/ListedSnapshotSpec"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ListedSnapshotSpec": {
|
||||
"required": [
|
||||
"owner"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"modifyTime": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"owner": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ListedUser": {
|
||||
"required": [
|
||||
"roles",
|
||||
|
@ -16952,7 +17302,7 @@
|
|||
"type": "object",
|
||||
"properties": {
|
||||
"content": {
|
||||
"$ref": "#/components/schemas/Content"
|
||||
"$ref": "#/components/schemas/ContentUpdateParam"
|
||||
},
|
||||
"post": {
|
||||
"$ref": "#/components/schemas/Post"
|
||||
|
@ -17954,6 +18304,30 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"RevertSnapshotForPostParam": {
|
||||
"required": [
|
||||
"snapshotName"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"snapshotName": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"RevertSnapshotForSingleParam": {
|
||||
"required": [
|
||||
"snapshotName"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"snapshotName": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Role": {
|
||||
"required": [
|
||||
"apiVersion",
|
||||
|
@ -18595,7 +18969,7 @@
|
|||
"type": "object",
|
||||
"properties": {
|
||||
"content": {
|
||||
"$ref": "#/components/schemas/Content"
|
||||
"$ref": "#/components/schemas/ContentUpdateParam"
|
||||
},
|
||||
"page": {
|
||||
"$ref": "#/components/schemas/SinglePage"
|
||||
|
@ -19975,13 +20349,17 @@
|
|||
},
|
||||
"VerifyCodeRequest": {
|
||||
"required": [
|
||||
"code"
|
||||
"code",
|
||||
"password"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": {
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"password": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -11,7 +11,6 @@ import lombok.Builder;
|
|||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import run.halo.app.extension.AbstractExtension;
|
||||
import run.halo.app.extension.GVK;
|
||||
|
||||
|
@ -59,6 +58,44 @@ public class Subscription extends AbstractExtension {
|
|||
@Schema(requiredMode = REQUIRED, description = "The subject name of reason type to be"
|
||||
+ " interested in")
|
||||
private ReasonSubject subject;
|
||||
|
||||
@Schema(requiredMode = NOT_REQUIRED, description = "The expression to be interested in")
|
||||
private String expression;
|
||||
|
||||
/**
|
||||
* <p>Since 2.15.0, we have added a new field <code>expression</code> to the
|
||||
* <code>InterestReason</code> object, so <code>subject</code> can be null.</p>
|
||||
* <p>In this particular scenario, when the <code>subject</code> is null, we assign it a
|
||||
* default <code>ReasonSubject</code> object. The properties of this object are set to
|
||||
* specific values that do not occur in actual applications, thus we can consider this as
|
||||
* <code>nonexistent data</code>.
|
||||
* The purpose of this approach is to maintain backward compatibility, even if the
|
||||
* <code>subject</code> can be null in the new version of the code.</p>
|
||||
*/
|
||||
public static void ensureSubjectHasValue(InterestReason interestReason) {
|
||||
if (interestReason.getSubject() == null) {
|
||||
interestReason.setSubject(createFallbackSubject());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given reason subject is a fallback subject.
|
||||
*/
|
||||
public static boolean isFallbackSubject(ReasonSubject reasonSubject) {
|
||||
if (reasonSubject == null) {
|
||||
return true;
|
||||
}
|
||||
var fallback = createFallbackSubject();
|
||||
return fallback.getKind().equals(reasonSubject.getKind())
|
||||
&& fallback.getApiVersion().equals(reasonSubject.getApiVersion());
|
||||
}
|
||||
|
||||
static ReasonSubject createFallbackSubject() {
|
||||
return ReasonSubject.builder()
|
||||
.apiVersion("notification.halo.run/v1alpha1")
|
||||
.kind("NonexistentKind")
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
|
@ -85,10 +122,14 @@ public class Subscription extends AbstractExtension {
|
|||
}
|
||||
|
||||
@Data
|
||||
@ToString
|
||||
@Schema(name = "SubscriptionSubscriber")
|
||||
public static class Subscriber {
|
||||
private String name;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -107,15 +107,15 @@ tasks.named('jacocoTestReport', JacocoReport) {
|
|||
}
|
||||
|
||||
ext.presetPluginUrls = [
|
||||
'https://github.com/halo-dev/plugin-comment-widget/releases/download/v1.9.0/plugin-comment-widget-1.9.0.jar' : 'plugin-comment-widget.jar',
|
||||
'https://github.com/halo-dev/plugin-search-widget/releases/download/v1.3.1/plugin-search-widget-1.3.1.jar' : 'plugin-search-widget.jar',
|
||||
'https://github.com/halo-dev/plugin-comment-widget/releases/download/v2.1.0/plugin-comment-widget-2.1.0.jar' : 'plugin-comment-widget.jar',
|
||||
'https://github.com/halo-dev/plugin-search-widget/releases/download/v1.4.0/plugin-search-widget-1.4.0.jar' : 'plugin-search-widget.jar',
|
||||
'https://github.com/halo-dev/plugin-sitemap/releases/download/v1.1.1/plugin-sitemap-1.1.1.jar' : 'plugin-sitemap.jar',
|
||||
'https://github.com/halo-dev/plugin-feed/releases/download/v1.2.1/plugin-feed-1.2.1.jar' : 'plugin-feed.jar',
|
||||
'https://github.com/halo-dev/plugin-feed/releases/download/v1.2.2/plugin-feed-1.2.2.jar' : 'plugin-feed.jar',
|
||||
|
||||
// Currently, plugin-app-store is not open source, so we need to download it from the official website.
|
||||
// Please see https://github.com/halo-dev/plugin-app-store/issues/55
|
||||
// https://www.halo.run/store/apps/app-VYJbF
|
||||
'https://www.halo.run/store/apps/app-VYJbF/releases/download/app-release-cWbLS/assets/app-release-cWbLS-fZYSx': 'appstore.jar',
|
||||
'https://www.halo.run/store/apps/app-VYJbF/releases/download/app-release-yfjXe/assets/app-release-yfjXe-kOzZZ': 'appstore.jar',
|
||||
]
|
||||
|
||||
tasks.register('downloadPluginPresets', Download) {
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
package run.halo.app.content;
|
||||
|
||||
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.security.Principal;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
|
@ -8,15 +12,20 @@ import lombok.AllArgsConstructor;
|
|||
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.Nullable;
|
||||
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.util.Assert;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.util.retry.Retry;
|
||||
import run.halo.app.core.extension.content.Snapshot;
|
||||
import run.halo.app.extension.ListOptions;
|
||||
import run.halo.app.extension.MetadataUtil;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.extension.Ref;
|
||||
import run.halo.app.extension.router.selector.FieldSelector;
|
||||
|
||||
/**
|
||||
* Abstract Service for {@link Snapshot}.
|
||||
|
@ -62,14 +71,27 @@ public abstract class AbstractContentService {
|
|||
}
|
||||
|
||||
protected Mono<ContentWrapper> draftContent(@Nullable String baseSnapshotName,
|
||||
ContentRequest contentRequest,
|
||||
@Nullable String parentSnapshotName) {
|
||||
return create(baseSnapshotName, contentRequest, parentSnapshotName)
|
||||
.flatMap(head -> {
|
||||
String baseSnapshotNameToUse =
|
||||
StringUtils.defaultIfBlank(baseSnapshotName, head.getMetadata().getName());
|
||||
return restoredContent(baseSnapshotNameToUse, head);
|
||||
});
|
||||
}
|
||||
|
||||
protected Mono<ContentWrapper> draftContent(String baseSnapshotName, ContentRequest content) {
|
||||
return this.draftContent(baseSnapshotName, content, content.headSnapshotName());
|
||||
}
|
||||
|
||||
private Mono<Snapshot> create(@Nullable String baseSnapshotName,
|
||||
ContentRequest contentRequest,
|
||||
@Nullable String parentSnapshotName) {
|
||||
Snapshot snapshot = contentRequest.toSnapshot();
|
||||
snapshot.getMetadata().setName(UUID.randomUUID().toString());
|
||||
snapshot.getSpec().setParentSnapshotName(parentSnapshotName);
|
||||
|
||||
final String baseSnapshotNameToUse =
|
||||
StringUtils.defaultIfBlank(baseSnapshotName, snapshot.getMetadata().getName());
|
||||
return client.fetch(Snapshot.class, baseSnapshotName)
|
||||
.doOnNext(this::checkBaseSnapshot)
|
||||
.defaultIfEmpty(snapshot)
|
||||
|
@ -77,19 +99,13 @@ public abstract class AbstractContentService {
|
|||
contentRequest)
|
||||
)
|
||||
.flatMap(source -> getContextUsername()
|
||||
.map(username -> {
|
||||
.doOnNext(username -> {
|
||||
Snapshot.addContributor(source, username);
|
||||
source.getSpec().setOwner(username);
|
||||
return source;
|
||||
})
|
||||
.defaultIfEmpty(source)
|
||||
.thenReturn(source)
|
||||
)
|
||||
.flatMap(snapshotToCreate -> client.create(snapshotToCreate)
|
||||
.flatMap(head -> restoredContent(baseSnapshotNameToUse, head)));
|
||||
}
|
||||
|
||||
protected Mono<ContentWrapper> draftContent(String baseSnapshotName, ContentRequest content) {
|
||||
return this.draftContent(baseSnapshotName, content, content.headSnapshotName());
|
||||
.flatMap(client::create);
|
||||
}
|
||||
|
||||
protected Mono<ContentWrapper> updateContent(String baseSnapshotName,
|
||||
|
@ -98,17 +114,23 @@ public abstract class AbstractContentService {
|
|||
Assert.notNull(baseSnapshotName, "The baseSnapshotName must not be null");
|
||||
Assert.notNull(contentRequest.headSnapshotName(), "The headSnapshotName must not be null");
|
||||
return Mono.defer(() -> client.fetch(Snapshot.class, contentRequest.headSnapshotName())
|
||||
.flatMap(headSnapshot -> {
|
||||
var oldVersion = contentRequest.version();
|
||||
var version = headSnapshot.getMetadata().getVersion();
|
||||
if (hasConflict(oldVersion, version)) {
|
||||
// draft a new snapshot as the head snapshot
|
||||
return create(baseSnapshotName, contentRequest,
|
||||
contentRequest.headSnapshotName());
|
||||
}
|
||||
return Mono.just(headSnapshot);
|
||||
})
|
||||
.flatMap(headSnapshot -> client.fetch(Snapshot.class, baseSnapshotName)
|
||||
.map(baseSnapshot -> determineRawAndContentPatch(headSnapshot, baseSnapshot,
|
||||
contentRequest)
|
||||
)
|
||||
.map(baseSnapshot -> determineRawAndContentPatch(headSnapshot,
|
||||
baseSnapshot, contentRequest))
|
||||
)
|
||||
.flatMap(headSnapshot -> getContextUsername()
|
||||
.map(username -> {
|
||||
Snapshot.addContributor(headSnapshot, username);
|
||||
return headSnapshot;
|
||||
})
|
||||
.defaultIfEmpty(headSnapshot)
|
||||
.doOnNext(username -> Snapshot.addContributor(headSnapshot, username))
|
||||
.thenReturn(headSnapshot)
|
||||
)
|
||||
.flatMap(client::update)
|
||||
)
|
||||
|
@ -117,6 +139,19 @@ public abstract class AbstractContentService {
|
|||
.flatMap(head -> restoredContent(baseSnapshotName, head));
|
||||
}
|
||||
|
||||
protected Flux<Snapshot> listSnapshotsBy(Ref ref) {
|
||||
var snapshotListOptions = new ListOptions();
|
||||
var query = and(isNull("metadata.deletionTimestamp"),
|
||||
equal("spec.subjectRef", Snapshot.toSubjectRefKey(ref)));
|
||||
snapshotListOptions.setFieldSelector(FieldSelector.of(query));
|
||||
var sort = Sort.by("metadata.creationTimestamp", "metadata.name").descending();
|
||||
return client.listAll(Snapshot.class, snapshotListOptions, sort);
|
||||
}
|
||||
|
||||
boolean hasConflict(Long oldVersion, Long newVersion) {
|
||||
return oldVersion != null && !newVersion.equals(oldVersion);
|
||||
}
|
||||
|
||||
protected Mono<ContentWrapper> restoredContent(String baseSnapshotName, Snapshot headSnapshot) {
|
||||
return client.fetch(Snapshot.class, baseSnapshotName)
|
||||
.doOnNext(this::checkBaseSnapshot)
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
package run.halo.app.content;
|
||||
|
||||
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED;
|
||||
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.util.HashMap;
|
||||
import lombok.Builder;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import run.halo.app.core.extension.content.Snapshot;
|
||||
import run.halo.app.extension.Metadata;
|
||||
|
@ -13,8 +15,10 @@ import run.halo.app.extension.Ref;
|
|||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Builder
|
||||
public record ContentRequest(@Schema(requiredMode = REQUIRED) Ref subjectRef,
|
||||
String headSnapshotName,
|
||||
@Schema(requiredMode = NOT_REQUIRED) Long version,
|
||||
@Schema(requiredMode = REQUIRED) String raw,
|
||||
@Schema(requiredMode = REQUIRED) String content,
|
||||
@Schema(requiredMode = REQUIRED) String rawType) {
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
package run.halo.app.content;
|
||||
|
||||
public record ContentUpdateParam(Long version, String raw, String content, String rawType) {
|
||||
|
||||
public static ContentUpdateParam from(Content content) {
|
||||
return new ContentUpdateParam(null, content.raw(), content.content(),
|
||||
content.rawType());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
package run.halo.app.content;
|
||||
|
||||
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.time.Instant;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
import run.halo.app.core.extension.content.Snapshot;
|
||||
import run.halo.app.extension.MetadataOperator;
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public class ListedSnapshotDto {
|
||||
@Schema(requiredMode = REQUIRED)
|
||||
private MetadataOperator metadata;
|
||||
|
||||
@Schema(requiredMode = REQUIRED)
|
||||
private Spec spec;
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@Schema(name = "ListedSnapshotSpec")
|
||||
public static class Spec {
|
||||
@Schema(requiredMode = REQUIRED)
|
||||
private String owner;
|
||||
|
||||
private Instant modifyTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates from snapshot.
|
||||
*/
|
||||
public static ListedSnapshotDto from(Snapshot snapshot) {
|
||||
return new ListedSnapshotDto()
|
||||
.setMetadata(snapshot.getMetadata())
|
||||
.setSpec(new Spec()
|
||||
.setOwner(snapshot.getSpec().getOwner())
|
||||
.setModifyTime(snapshot.getSpec().getLastModifyTime())
|
||||
);
|
||||
}
|
||||
}
|
|
@ -14,12 +14,12 @@ import run.halo.app.extension.Ref;
|
|||
* @since 2.0.0
|
||||
*/
|
||||
public record PostRequest(@Schema(requiredMode = REQUIRED) @NonNull Post post,
|
||||
Content content) {
|
||||
ContentUpdateParam content) {
|
||||
|
||||
public ContentRequest contentRequest() {
|
||||
Ref subjectRef = Ref.of(post);
|
||||
return new ContentRequest(subjectRef, post.getSpec().getHeadSnapshot(), content.raw(),
|
||||
content.content(), content.rawType());
|
||||
return new ContentRequest(subjectRef, post.getSpec().getHeadSnapshot(), content.version(),
|
||||
content.raw(), content.content(), content.rawType());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package run.halo.app.content;
|
||||
|
||||
import org.springframework.lang.NonNull;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.extension.content.Post;
|
||||
import run.halo.app.extension.ListResult;
|
||||
|
@ -31,6 +32,8 @@ public interface PostService {
|
|||
|
||||
Mono<ContentWrapper> getContent(String snapshotName, String baseSnapshotName);
|
||||
|
||||
Flux<ListedSnapshotDto> listSnapshots(String name);
|
||||
|
||||
Mono<Post> publish(Post post);
|
||||
|
||||
Mono<Post> unpublish(Post post);
|
||||
|
@ -43,4 +46,8 @@ public interface PostService {
|
|||
* @return full post data or empty.
|
||||
*/
|
||||
Mono<Post> getByUsername(String postName, String username);
|
||||
|
||||
Mono<Post> revertToSpecifiedSnapshot(String postName, String snapshotName);
|
||||
|
||||
Mono<ContentWrapper> deleteContent(String postName, String snapshotName);
|
||||
}
|
||||
|
|
|
@ -13,12 +13,12 @@ import run.halo.app.extension.Ref;
|
|||
* @since 2.0.0
|
||||
*/
|
||||
public record SinglePageRequest(@Schema(requiredMode = REQUIRED) SinglePage page,
|
||||
@Schema(requiredMode = REQUIRED) Content content) {
|
||||
@Schema(requiredMode = REQUIRED) ContentUpdateParam content) {
|
||||
|
||||
public ContentRequest contentRequest() {
|
||||
Ref subjectRef = Ref.of(page);
|
||||
return new ContentRequest(subjectRef, page.getSpec().getHeadSnapshot(), content.raw(),
|
||||
content.content(), content.rawType());
|
||||
return new ContentRequest(subjectRef, page.getSpec().getHeadSnapshot(), content.version(),
|
||||
content.raw(), content.content(), content.rawType());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package run.halo.app.content;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.extension.content.SinglePage;
|
||||
import run.halo.app.extension.ListResult;
|
||||
|
@ -18,9 +19,15 @@ public interface SinglePageService {
|
|||
|
||||
Mono<ContentWrapper> getContent(String snapshotName, String baseSnapshotName);
|
||||
|
||||
Flux<ListedSnapshotDto> listSnapshots(String pageName);
|
||||
|
||||
Mono<ListResult<ListedSinglePage>> list(SinglePageQuery listRequest);
|
||||
|
||||
Mono<SinglePage> draft(SinglePageRequest pageRequest);
|
||||
|
||||
Mono<SinglePage> update(SinglePageRequest pageRequest);
|
||||
|
||||
Mono<SinglePage> revertToSpecifiedSnapshot(String pageName, String snapshotName);
|
||||
|
||||
Mono<ContentWrapper> deleteContent(String postName, String snapshotName);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package run.halo.app.content.comment;
|
||||
|
||||
import static org.apache.commons.lang3.StringUtils.defaultIfBlank;
|
||||
import static run.halo.app.content.comment.ReplyNotificationSubscriptionHelper.identityFrom;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import java.util.Map;
|
||||
|
@ -29,7 +30,6 @@ import run.halo.app.extension.Ref;
|
|||
import run.halo.app.infra.ExternalLinkProcessor;
|
||||
import run.halo.app.infra.utils.JsonUtils;
|
||||
import run.halo.app.notification.NotificationReasonEmitter;
|
||||
import run.halo.app.notification.UserIdentity;
|
||||
import run.halo.app.plugin.ExtensionComponentsFinder;
|
||||
import run.halo.app.plugin.extensionpoint.ExtensionGetter;
|
||||
|
||||
|
@ -114,6 +114,7 @@ public class CommentNotificationReasonPublisher {
|
|||
builder -> {
|
||||
var attributes = CommentOnPostReasonData.builder()
|
||||
.postName(subjectRef.getName())
|
||||
.postOwner(post.getSpec().getOwner())
|
||||
.postTitle(post.getSpec().getTitle())
|
||||
.postUrl(postUrl)
|
||||
.commenter(owner.getDisplayName())
|
||||
|
@ -144,8 +145,9 @@ public class CommentNotificationReasonPublisher {
|
|||
}
|
||||
|
||||
@Builder
|
||||
record CommentOnPostReasonData(String postName, String postTitle, String postUrl,
|
||||
String commenter, String content, String commentName) {
|
||||
record CommentOnPostReasonData(String postName, String postOwner, String postTitle,
|
||||
String postUrl, String commenter, String content,
|
||||
String commentName) {
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -180,6 +182,7 @@ public class CommentNotificationReasonPublisher {
|
|||
builder -> {
|
||||
var attributes = CommentOnPageReasonData.builder()
|
||||
.pageName(subjectRef.getName())
|
||||
.pageOwner(singlePage.getSpec().getOwner())
|
||||
.pageTitle(singlePage.getSpec().getTitle())
|
||||
.pageUrl(pageUrl)
|
||||
.commenter(defaultIfBlank(owner.getDisplayName(), owner.getName()))
|
||||
|
@ -210,8 +213,9 @@ public class CommentNotificationReasonPublisher {
|
|||
}
|
||||
|
||||
@Builder
|
||||
record CommentOnPageReasonData(String pageName, String pageTitle, String pageUrl,
|
||||
String commenter, String content, String commentName) {
|
||||
record CommentOnPageReasonData(String pageName, String pageOwner, String pageTitle,
|
||||
String pageUrl, String commenter, String content,
|
||||
String commentName) {
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -224,13 +228,6 @@ public class CommentNotificationReasonPublisher {
|
|||
}
|
||||
}
|
||||
|
||||
static UserIdentity identityFrom(Comment.CommentOwner owner) {
|
||||
if (Comment.CommentOwner.KIND_EMAIL.equals(owner.getKind())) {
|
||||
return UserIdentity.anonymousWithEmail(owner.getName());
|
||||
}
|
||||
return UserIdentity.of(owner.getName());
|
||||
}
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
static class NewReplyReasonPublisher {
|
||||
|
@ -272,6 +269,10 @@ public class CommentNotificationReasonPublisher {
|
|||
.orElse(null);
|
||||
var replyOwner = reply.getSpec().getOwner();
|
||||
|
||||
var repliedOwner = quoteReplyOptional
|
||||
.map(quoteReply -> quoteReply.getSpec().getOwner())
|
||||
.orElseGet(() -> comment.getSpec().getOwner());
|
||||
|
||||
var reasonAttributesBuilder = NewReplyReasonData.builder()
|
||||
.commentContent(comment.getSpec().getContent())
|
||||
.isQuoteReply(isQuoteReply)
|
||||
|
@ -279,7 +280,9 @@ public class CommentNotificationReasonPublisher {
|
|||
.commentName(comment.getMetadata().getName())
|
||||
.replier(defaultIfBlank(replyOwner.getDisplayName(), replyOwner.getName()))
|
||||
.content(reply.getSpec().getContent())
|
||||
.replyName(reply.getMetadata().getName());
|
||||
.replyName(reply.getMetadata().getName())
|
||||
.replyOwner(identityFrom(replyOwner).name())
|
||||
.repliedOwner(identityFrom(repliedOwner).name());
|
||||
|
||||
getCommentSubjectDisplay(comment.getSpec().getSubjectRef())
|
||||
.ifPresent(subject -> {
|
||||
|
@ -337,7 +340,7 @@ public class CommentNotificationReasonPublisher {
|
|||
String commentSubjectUrl, boolean isQuoteReply,
|
||||
String quoteContent,
|
||||
String commentName, String replier, String content,
|
||||
String replyName) {
|
||||
String replyName, String replyOwner, String repliedOwner) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,10 +4,12 @@ import static run.halo.app.extension.index.query.QueryFactory.and;
|
|||
import static run.halo.app.extension.index.query.QueryFactory.equal;
|
||||
import static run.halo.app.extension.index.query.QueryFactory.isNull;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import org.apache.commons.lang3.BooleanUtils;
|
||||
import org.springframework.dao.OptimisticLockingFailureException;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
|
||||
|
@ -15,6 +17,7 @@ import org.springframework.stereotype.Component;
|
|||
import org.springframework.util.Assert;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.util.retry.Retry;
|
||||
import run.halo.app.core.extension.User;
|
||||
import run.halo.app.core.extension.content.Comment;
|
||||
import run.halo.app.core.extension.service.RoleService;
|
||||
|
@ -147,17 +150,33 @@ public class CommentServiceImpl implements CommentService {
|
|||
@Override
|
||||
public Mono<Void> removeBySubject(@NonNull Ref subjectRef) {
|
||||
Assert.notNull(subjectRef, "The subjectRef must not be null.");
|
||||
return cleanupComments(subjectRef, 200);
|
||||
}
|
||||
|
||||
private Mono<Void> cleanupComments(Ref subjectRef, int batchSize) {
|
||||
// ascending order by creation time and name
|
||||
var pageRequest = PageRequestImpl.of(1, 200,
|
||||
final var pageRequest = PageRequestImpl.of(1, batchSize,
|
||||
Sort.by("metadata.creationTimestamp", "metadata.name"));
|
||||
return Flux.defer(() -> listCommentsByRef(subjectRef, pageRequest))
|
||||
.expand(page -> page.hasNext()
|
||||
? listCommentsByRef(subjectRef, pageRequest.next())
|
||||
: Mono.empty()
|
||||
// forever loop first page until no more to delete
|
||||
return listCommentsByRef(subjectRef, pageRequest)
|
||||
.flatMap(page -> Flux.fromIterable(page.getItems())
|
||||
.flatMap(this::deleteWithRetry)
|
||||
.then(page.hasNext() ? cleanupComments(subjectRef, batchSize) : Mono.empty())
|
||||
);
|
||||
}
|
||||
|
||||
private Mono<Comment> deleteWithRetry(Comment item) {
|
||||
return client.delete(item)
|
||||
.onErrorResume(OptimisticLockingFailureException.class,
|
||||
e -> attemptToDelete(item.getMetadata().getName()));
|
||||
}
|
||||
|
||||
private Mono<Comment> attemptToDelete(String name) {
|
||||
return Mono.defer(() -> client.fetch(Comment.class, name)
|
||||
.flatMap(client::delete)
|
||||
)
|
||||
.flatMap(page -> Flux.fromIterable(page.getItems()))
|
||||
.flatMap(client::delete)
|
||||
.then();
|
||||
.retryWhen(Retry.backoff(8, Duration.ofMillis(100))
|
||||
.filter(OptimisticLockingFailureException.class::isInstance));
|
||||
}
|
||||
|
||||
Mono<ListResult<Comment>> listCommentsByRef(Ref subjectRef, PageRequest pageRequest) {
|
||||
|
|
|
@ -9,7 +9,7 @@ import run.halo.app.core.extension.content.Comment;
|
|||
import run.halo.app.core.extension.content.Reply;
|
||||
import run.halo.app.core.extension.notification.Subscription;
|
||||
import run.halo.app.notification.NotificationCenter;
|
||||
import run.halo.app.notification.SubscriberEmailResolver;
|
||||
import run.halo.app.notification.UserIdentity;
|
||||
|
||||
/**
|
||||
* Reply notification subscription helper.
|
||||
|
@ -22,7 +22,6 @@ import run.halo.app.notification.SubscriberEmailResolver;
|
|||
public class ReplyNotificationSubscriptionHelper {
|
||||
|
||||
private final NotificationCenter notificationCenter;
|
||||
private final SubscriberEmailResolver subscriberEmailResolver;
|
||||
|
||||
/**
|
||||
* Subscribe new reply reason for comment.
|
||||
|
@ -30,13 +29,7 @@ public class ReplyNotificationSubscriptionHelper {
|
|||
* @param comment comment
|
||||
*/
|
||||
public void subscribeNewReplyReasonForComment(Comment comment) {
|
||||
var reasonSubject = Subscription.ReasonSubject.builder()
|
||||
.apiVersion(comment.getApiVersion())
|
||||
.kind(comment.getKind())
|
||||
.name(comment.getMetadata().getName())
|
||||
.build();
|
||||
subscribeReply(reasonSubject,
|
||||
Identity.fromCommentOwner(comment.getSpec().getOwner()));
|
||||
subscribeReply(identityFrom(comment.getSpec().getOwner()));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -45,50 +38,36 @@ public class ReplyNotificationSubscriptionHelper {
|
|||
* @param reply reply
|
||||
*/
|
||||
public void subscribeNewReplyReasonForReply(Reply reply) {
|
||||
var reasonSubject = Subscription.ReasonSubject.builder()
|
||||
.apiVersion(reply.getApiVersion())
|
||||
.kind(reply.getKind())
|
||||
.name(reply.getMetadata().getName())
|
||||
.build();
|
||||
var subjectOwner = reply.getSpec().getOwner();
|
||||
subscribeReply(reasonSubject,
|
||||
Identity.fromCommentOwner(subjectOwner));
|
||||
subscribeReply(identityFrom(subjectOwner));
|
||||
}
|
||||
|
||||
void subscribeReply(Subscription.ReasonSubject reasonSubject,
|
||||
Identity identity) {
|
||||
void subscribeReply(UserIdentity identity) {
|
||||
var subscriber = createSubscriber(identity);
|
||||
if (subscriber == null) {
|
||||
return;
|
||||
}
|
||||
var interestReason = new Subscription.InterestReason();
|
||||
interestReason.setReasonType(NotificationReasonConst.SOMEONE_REPLIED_TO_YOU);
|
||||
interestReason.setSubject(reasonSubject);
|
||||
interestReason.setExpression("props.repliedOwner == '%s'".formatted(identity.name()));
|
||||
notificationCenter.subscribe(subscriber, interestReason).block();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Subscription.Subscriber createSubscriber(Identity author) {
|
||||
private Subscription.Subscriber createSubscriber(UserIdentity author) {
|
||||
if (StringUtils.isBlank(author.name())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Subscription.Subscriber subscriber;
|
||||
if (author.isEmail()) {
|
||||
subscriber = subscriberEmailResolver.ofEmail(author.name());
|
||||
} else {
|
||||
subscriber = new Subscription.Subscriber();
|
||||
subscriber.setName(author.name());
|
||||
}
|
||||
Subscription.Subscriber subscriber = new Subscription.Subscriber();
|
||||
subscriber.setName(author.name());
|
||||
return subscriber;
|
||||
}
|
||||
|
||||
record Identity(String name, boolean isEmail) {
|
||||
public static Identity fromCommentOwner(Comment.CommentOwner commentOwner) {
|
||||
if (Comment.CommentOwner.KIND_EMAIL.equals(commentOwner.getKind())) {
|
||||
return new Identity(commentOwner.getName(), true);
|
||||
}
|
||||
return new Identity(commentOwner.getName(), false);
|
||||
public static UserIdentity identityFrom(Comment.CommentOwner owner) {
|
||||
if (Comment.CommentOwner.KIND_EMAIL.equals(owner.getKind())) {
|
||||
return UserIdentity.anonymousWithEmail(owner.getName());
|
||||
}
|
||||
return UserIdentity.of(owner.getName());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,18 +5,21 @@ 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.router.selector.SelectorUtil.labelAndFieldSelectorToPredicate;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.BooleanUtils;
|
||||
import org.springframework.dao.OptimisticLockingFailureException;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.Assert;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.util.retry.Retry;
|
||||
import run.halo.app.core.extension.User;
|
||||
import run.halo.app.core.extension.content.Comment;
|
||||
import run.halo.app.core.extension.content.Reply;
|
||||
|
@ -118,17 +121,33 @@ public class ReplyServiceImpl implements ReplyService {
|
|||
@Override
|
||||
public Mono<Void> removeAllByComment(String commentName) {
|
||||
Assert.notNull(commentName, "The commentName must not be null.");
|
||||
return cleanupComments(commentName, 200);
|
||||
}
|
||||
|
||||
private Mono<Void> cleanupComments(String commentName, int batchSize) {
|
||||
// ascending order by creation time and name
|
||||
var pageRequest = PageRequestImpl.of(1, 200,
|
||||
final var pageRequest = PageRequestImpl.of(1, batchSize,
|
||||
Sort.by("metadata.creationTimestamp", "metadata.name"));
|
||||
return Flux.defer(() -> listRepliesByComment(commentName, pageRequest))
|
||||
.expand(page -> page.hasNext()
|
||||
? listRepliesByComment(commentName, pageRequest.next())
|
||||
: Mono.empty()
|
||||
// forever loop first page until no more to delete
|
||||
return listRepliesByComment(commentName, pageRequest)
|
||||
.flatMap(page -> Flux.fromIterable(page.getItems())
|
||||
.flatMap(this::deleteWithRetry)
|
||||
.then(page.hasNext() ? cleanupComments(commentName, batchSize) : Mono.empty())
|
||||
);
|
||||
}
|
||||
|
||||
private Mono<Reply> deleteWithRetry(Reply item) {
|
||||
return client.delete(item)
|
||||
.onErrorResume(OptimisticLockingFailureException.class,
|
||||
e -> attemptToDelete(item.getMetadata().getName()));
|
||||
}
|
||||
|
||||
private Mono<Reply> attemptToDelete(String name) {
|
||||
return Mono.defer(() -> client.fetch(Reply.class, name)
|
||||
.flatMap(client::delete)
|
||||
)
|
||||
.flatMap(page -> Flux.fromIterable(page.getItems()))
|
||||
.flatMap(client::delete)
|
||||
.then();
|
||||
.retryWhen(Retry.backoff(8, Duration.ofMillis(100))
|
||||
.filter(OptimisticLockingFailureException.class::isInstance));
|
||||
}
|
||||
|
||||
Mono<ListResult<Reply>> listRepliesByComment(String commentName, PageRequest pageRequest) {
|
||||
|
|
|
@ -7,6 +7,7 @@ import java.time.Instant;
|
|||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.UnaryOperator;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.dao.OptimisticLockingFailureException;
|
||||
|
@ -14,6 +15,7 @@ import org.springframework.data.domain.Sort;
|
|||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.server.ServerWebInputException;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.util.retry.Retry;
|
||||
|
@ -22,12 +24,14 @@ import run.halo.app.content.ContentRequest;
|
|||
import run.halo.app.content.ContentWrapper;
|
||||
import run.halo.app.content.Contributor;
|
||||
import run.halo.app.content.ListedPost;
|
||||
import run.halo.app.content.ListedSnapshotDto;
|
||||
import run.halo.app.content.PostQuery;
|
||||
import run.halo.app.content.PostRequest;
|
||||
import run.halo.app.content.PostService;
|
||||
import run.halo.app.content.Stats;
|
||||
import run.halo.app.core.extension.content.Category;
|
||||
import run.halo.app.core.extension.content.Post;
|
||||
import run.halo.app.core.extension.content.Snapshot;
|
||||
import run.halo.app.core.extension.content.Tag;
|
||||
import run.halo.app.core.extension.service.UserService;
|
||||
import run.halo.app.extension.ListOptions;
|
||||
|
@ -173,6 +177,7 @@ public class PostServiceImpl extends AbstractContentService implements PostServi
|
|||
}
|
||||
var contentRequest =
|
||||
new ContentRequest(Ref.of(post), post.getSpec().getHeadSnapshot(),
|
||||
null,
|
||||
postRequest.content().raw(), postRequest.content().content(),
|
||||
postRequest.content().rawType());
|
||||
return draftContent(post.getSpec().getBaseSnapshot(), contentRequest)
|
||||
|
@ -261,6 +266,13 @@ public class PostServiceImpl extends AbstractContentService implements PostServi
|
|||
return getContent(releaseSnapshot, post.getSpec().getBaseSnapshot());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<ListedSnapshotDto> listSnapshots(String name) {
|
||||
return client.fetch(Post.class, name)
|
||||
.flatMapMany(page -> listSnapshotsBy(Ref.of(page)))
|
||||
.map(ListedSnapshotDto::from);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Post> publish(Post post) {
|
||||
var spec = post.getSpec();
|
||||
|
@ -284,4 +296,84 @@ public class PostServiceImpl extends AbstractContentService implements PostServi
|
|||
.filter(post -> post.getSpec() != null)
|
||||
.filter(post -> Objects.equals(username, post.getSpec().getOwner()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Post> revertToSpecifiedSnapshot(String postName, String snapshotName) {
|
||||
return client.get(Post.class, postName)
|
||||
.filter(post -> {
|
||||
var head = post.getSpec().getHeadSnapshot();
|
||||
return !StringUtils.equals(head, snapshotName);
|
||||
})
|
||||
.flatMap(post -> {
|
||||
var baseSnapshot = post.getSpec().getBaseSnapshot();
|
||||
return getContent(snapshotName, baseSnapshot)
|
||||
.map(content -> ContentRequest.builder()
|
||||
.subjectRef(Ref.of(post))
|
||||
.headSnapshotName(post.getSpec().getHeadSnapshot())
|
||||
.content(content.getContent())
|
||||
.raw(content.getRaw())
|
||||
.rawType(content.getRawType())
|
||||
.build()
|
||||
)
|
||||
.flatMap(contentRequest -> draftContent(baseSnapshot, contentRequest))
|
||||
.flatMap(content -> {
|
||||
post.getSpec().setHeadSnapshot(content.getSnapshotName());
|
||||
return publishPostWithRetry(post);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<ContentWrapper> deleteContent(String postName, String snapshotName) {
|
||||
return client.get(Post.class, postName)
|
||||
.flatMap(post -> {
|
||||
var headSnapshotName = post.getSpec().getHeadSnapshot();
|
||||
if (StringUtils.equals(headSnapshotName, snapshotName)) {
|
||||
return updatePostWithRetry(post, record -> {
|
||||
// update head to release
|
||||
record.getSpec().setHeadSnapshot(record.getSpec().getReleaseSnapshot());
|
||||
return record;
|
||||
});
|
||||
}
|
||||
return Mono.just(post);
|
||||
})
|
||||
.flatMap(post -> {
|
||||
var baseSnapshotName = post.getSpec().getBaseSnapshot();
|
||||
var releaseSnapshotName = post.getSpec().getReleaseSnapshot();
|
||||
if (StringUtils.equals(releaseSnapshotName, snapshotName)) {
|
||||
return Mono.error(new ServerWebInputException(
|
||||
"The snapshot to delete is the release snapshot, please"
|
||||
+ " revert to another snapshot first."));
|
||||
}
|
||||
if (StringUtils.equals(baseSnapshotName, snapshotName)) {
|
||||
return Mono.error(
|
||||
new ServerWebInputException("The first snapshot cannot be deleted."));
|
||||
}
|
||||
return client.fetch(Snapshot.class, snapshotName)
|
||||
.flatMap(client::delete)
|
||||
.flatMap(deleted -> restoredContent(baseSnapshotName, deleted));
|
||||
});
|
||||
}
|
||||
|
||||
private Mono<Post> updatePostWithRetry(Post post, UnaryOperator<Post> func) {
|
||||
return client.update(func.apply(post))
|
||||
.onErrorResume(OptimisticLockingFailureException.class,
|
||||
e -> Mono.defer(() -> client.get(Post.class, post.getMetadata().getName())
|
||||
.map(func)
|
||||
.flatMap(client::update)
|
||||
)
|
||||
.retryWhen(Retry.backoff(8, Duration.ofMillis(100))
|
||||
.filter(OptimisticLockingFailureException.class::isInstance))
|
||||
);
|
||||
}
|
||||
|
||||
Mono<Post> publishPostWithRetry(Post post) {
|
||||
return publish(post)
|
||||
.onErrorResume(OptimisticLockingFailureException.class,
|
||||
e -> Mono.defer(() -> client.get(Post.class, post.getMetadata().getName())
|
||||
.flatMap(this::publish))
|
||||
.retryWhen(Retry.backoff(8, Duration.ofMillis(100))
|
||||
.filter(OptimisticLockingFailureException.class::isInstance))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,11 +5,13 @@ import java.time.Instant;
|
|||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.UnaryOperator;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.dao.OptimisticLockingFailureException;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.server.ServerWebInputException;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.util.retry.Retry;
|
||||
|
@ -18,12 +20,14 @@ import run.halo.app.content.ContentRequest;
|
|||
import run.halo.app.content.ContentWrapper;
|
||||
import run.halo.app.content.Contributor;
|
||||
import run.halo.app.content.ListedSinglePage;
|
||||
import run.halo.app.content.ListedSnapshotDto;
|
||||
import run.halo.app.content.SinglePageQuery;
|
||||
import run.halo.app.content.SinglePageRequest;
|
||||
import run.halo.app.content.SinglePageService;
|
||||
import run.halo.app.content.Stats;
|
||||
import run.halo.app.core.extension.content.Post;
|
||||
import run.halo.app.core.extension.content.SinglePage;
|
||||
import run.halo.app.core.extension.content.Snapshot;
|
||||
import run.halo.app.core.extension.service.UserService;
|
||||
import run.halo.app.extension.ListResult;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
|
@ -73,6 +77,13 @@ public class SinglePageServiceImpl extends AbstractContentService implements Sin
|
|||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<ListedSnapshotDto> listSnapshots(String pageName) {
|
||||
return client.fetch(SinglePage.class, pageName)
|
||||
.flatMapMany(page -> listSnapshotsBy(Ref.of(page)))
|
||||
.map(ListedSnapshotDto::from);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<ListResult<ListedSinglePage>> list(SinglePageQuery query) {
|
||||
return client.list(SinglePage.class, query.toPredicate(),
|
||||
|
@ -103,6 +114,7 @@ public class SinglePageServiceImpl extends AbstractContentService implements Sin
|
|||
.flatMap(page -> {
|
||||
var contentRequest =
|
||||
new ContentRequest(Ref.of(page), page.getSpec().getHeadSnapshot(),
|
||||
null,
|
||||
pageRequest.content().raw(), pageRequest.content().content(),
|
||||
pageRequest.content().rawType());
|
||||
return draftContent(page.getSpec().getBaseSnapshot(), contentRequest)
|
||||
|
@ -163,6 +175,96 @@ public class SinglePageServiceImpl extends AbstractContentService implements Sin
|
|||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<SinglePage> revertToSpecifiedSnapshot(String pageName, String snapshotName) {
|
||||
return client.get(SinglePage.class, pageName)
|
||||
.filter(page -> {
|
||||
var head = page.getSpec().getHeadSnapshot();
|
||||
return !StringUtils.equals(head, snapshotName);
|
||||
})
|
||||
.flatMap(page -> {
|
||||
var baseSnapshot = page.getSpec().getBaseSnapshot();
|
||||
return getContent(snapshotName, baseSnapshot)
|
||||
.map(content -> ContentRequest.builder()
|
||||
.subjectRef(Ref.of(page))
|
||||
.headSnapshotName(page.getSpec().getHeadSnapshot())
|
||||
.content(content.getContent())
|
||||
.raw(content.getRaw())
|
||||
.rawType(content.getRawType())
|
||||
.build()
|
||||
)
|
||||
.flatMap(contentRequest -> draftContent(baseSnapshot, contentRequest))
|
||||
.flatMap(content -> {
|
||||
page.getSpec().setHeadSnapshot(content.getSnapshotName());
|
||||
return publishPageWithRetry(page);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<ContentWrapper> deleteContent(String pageName, String snapshotName) {
|
||||
return client.get(SinglePage.class, pageName)
|
||||
.flatMap(page -> {
|
||||
var headSnapshotName = page.getSpec().getHeadSnapshot();
|
||||
if (StringUtils.equals(headSnapshotName, snapshotName)) {
|
||||
return updatePageWithRetry(page, record -> {
|
||||
// update head to release
|
||||
page.getSpec().setHeadSnapshot(page.getSpec().getReleaseSnapshot());
|
||||
return record;
|
||||
});
|
||||
}
|
||||
return Mono.just(page);
|
||||
})
|
||||
.flatMap(page -> {
|
||||
var baseSnapshotName = page.getSpec().getBaseSnapshot();
|
||||
var releaseSnapshotName = page.getSpec().getReleaseSnapshot();
|
||||
if (StringUtils.equals(releaseSnapshotName, snapshotName)) {
|
||||
return Mono.error(new ServerWebInputException(
|
||||
"The snapshot to delete is the release snapshot, please"
|
||||
+ " revert to another snapshot first."));
|
||||
}
|
||||
if (StringUtils.equals(baseSnapshotName, snapshotName)) {
|
||||
return Mono.error(
|
||||
new ServerWebInputException("The first snapshot cannot be deleted."));
|
||||
}
|
||||
return client.fetch(Snapshot.class, snapshotName)
|
||||
.flatMap(client::delete)
|
||||
.flatMap(deleted -> restoredContent(baseSnapshotName, deleted));
|
||||
});
|
||||
}
|
||||
|
||||
private Mono<SinglePage> updatePageWithRetry(SinglePage page, UnaryOperator<SinglePage> func) {
|
||||
return client.update(func.apply(page))
|
||||
.onErrorResume(OptimisticLockingFailureException.class,
|
||||
e -> Mono.defer(() -> client.get(SinglePage.class, page.getMetadata().getName())
|
||||
.map(func)
|
||||
.flatMap(client::update)
|
||||
)
|
||||
.retryWhen(Retry.backoff(8, Duration.ofMillis(100))
|
||||
.filter(OptimisticLockingFailureException.class::isInstance))
|
||||
);
|
||||
}
|
||||
|
||||
private Mono<SinglePage> publish(SinglePage singlePage) {
|
||||
var spec = singlePage.getSpec();
|
||||
spec.setPublish(true);
|
||||
if (spec.getHeadSnapshot() == null) {
|
||||
spec.setHeadSnapshot(spec.getBaseSnapshot());
|
||||
}
|
||||
spec.setReleaseSnapshot(spec.getHeadSnapshot());
|
||||
return client.update(singlePage);
|
||||
}
|
||||
|
||||
Mono<SinglePage> publishPageWithRetry(SinglePage page) {
|
||||
return publish(page)
|
||||
.onErrorResume(OptimisticLockingFailureException.class,
|
||||
e -> Mono.defer(() -> client.get(SinglePage.class, page.getMetadata().getName())
|
||||
.flatMap(this::publish))
|
||||
.retryWhen(Retry.backoff(8, Duration.ofMillis(100))
|
||||
.filter(OptimisticLockingFailureException.class::isInstance))
|
||||
);
|
||||
}
|
||||
|
||||
private Mono<ListedSinglePage> getListedSinglePage(SinglePage singlePage) {
|
||||
Assert.notNull(singlePage, "The singlePage must not be null.");
|
||||
var listedSinglePage = new ListedSinglePage()
|
||||
|
|
|
@ -6,6 +6,7 @@ import static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;
|
|||
import static org.springdoc.core.fn.builders.requestbody.Builder.requestBodyBuilder;
|
||||
|
||||
import io.swagger.v3.oas.annotations.enums.ParameterIn;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.time.Duration;
|
||||
import java.util.Objects;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
@ -20,12 +21,15 @@ import org.springframework.web.reactive.function.server.RouterFunction;
|
|||
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import org.springframework.web.server.ServerErrorException;
|
||||
import org.springframework.web.server.ServerWebInputException;
|
||||
import reactor.core.Exceptions;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.util.retry.Retry;
|
||||
import run.halo.app.content.Content;
|
||||
import run.halo.app.content.ContentUpdateParam;
|
||||
import run.halo.app.content.ContentWrapper;
|
||||
import run.halo.app.content.ListedPost;
|
||||
import run.halo.app.content.ListedSnapshotDto;
|
||||
import run.halo.app.content.PostQuery;
|
||||
import run.halo.app.content.PostRequest;
|
||||
import run.halo.app.content.PostService;
|
||||
|
@ -75,6 +79,23 @@ public class PostEndpoint implements CustomEndpoint {
|
|||
.response(responseBuilder()
|
||||
.implementation(ContentWrapper.class))
|
||||
)
|
||||
.GET("posts/{name}/content", this::fetchContent,
|
||||
builder -> builder.operationId("fetchPostContent")
|
||||
.description("Fetch content of post.")
|
||||
.tag(tag)
|
||||
.parameter(parameterBuilder().name("name")
|
||||
.in(ParameterIn.PATH)
|
||||
.required(true)
|
||||
.implementation(String.class)
|
||||
)
|
||||
.parameter(parameterBuilder()
|
||||
.name("snapshotName")
|
||||
.in(ParameterIn.QUERY)
|
||||
.required(true)
|
||||
.implementation(String.class))
|
||||
.response(responseBuilder()
|
||||
.implementation(ContentWrapper.class))
|
||||
)
|
||||
.GET("posts/{name}/release-content", this::fetchReleaseContent,
|
||||
builder -> builder.operationId("fetchPostReleaseContent")
|
||||
.description("Fetch release content of post.")
|
||||
|
@ -87,6 +108,17 @@ public class PostEndpoint implements CustomEndpoint {
|
|||
.response(responseBuilder()
|
||||
.implementation(ContentWrapper.class))
|
||||
)
|
||||
.GET("posts/{name}/snapshot", this::listSnapshots,
|
||||
builder -> builder.operationId("listPostSnapshots")
|
||||
.description("List all snapshots for post content.")
|
||||
.tag(tag)
|
||||
.parameter(parameterBuilder().name("name")
|
||||
.in(ParameterIn.PATH)
|
||||
.required(true)
|
||||
.implementation(String.class))
|
||||
.response(responseBuilder()
|
||||
.implementationArray(ListedSnapshotDto.class))
|
||||
)
|
||||
.POST("posts", this::draftPost,
|
||||
builder -> builder.operationId("DraftPost")
|
||||
.description("Draft a post.")
|
||||
|
@ -137,6 +169,24 @@ public class PostEndpoint implements CustomEndpoint {
|
|||
.response(responseBuilder()
|
||||
.implementation(Post.class))
|
||||
)
|
||||
.PUT("posts/{name}/revert-content", this::revertToSpecifiedSnapshot,
|
||||
builder -> builder.operationId("revertToSpecifiedSnapshotForPost")
|
||||
.description("Revert to specified snapshot for post content.")
|
||||
.tag(tag)
|
||||
.parameter(parameterBuilder().name("name")
|
||||
.in(ParameterIn.PATH)
|
||||
.required(true)
|
||||
.implementation(String.class))
|
||||
.requestBody(requestBodyBuilder()
|
||||
.required(true)
|
||||
.content(contentBuilder()
|
||||
.mediaType(MediaType.APPLICATION_JSON_VALUE)
|
||||
.schema(Builder.schemaBuilder()
|
||||
.implementation(RevertSnapshotParam.class))
|
||||
))
|
||||
.response(responseBuilder()
|
||||
.implementation(Post.class))
|
||||
)
|
||||
.PUT("posts/{name}/publish", this::publishPost,
|
||||
builder -> builder.operationId("PublishPost")
|
||||
.description("Publish a post.")
|
||||
|
@ -168,9 +218,64 @@ public class PostEndpoint implements CustomEndpoint {
|
|||
.parameter(parameterBuilder().name("name")
|
||||
.in(ParameterIn.PATH)
|
||||
.required(true)))
|
||||
.DELETE("posts/{name}/content", this::deleteContent,
|
||||
builder -> builder.operationId("deletePostContent")
|
||||
.description("Delete a content for post.")
|
||||
.tag(tag)
|
||||
.parameter(parameterBuilder().name("name")
|
||||
.in(ParameterIn.PATH)
|
||||
.required(true)
|
||||
.implementation(String.class)
|
||||
)
|
||||
.parameter(parameterBuilder()
|
||||
.name("snapshotName")
|
||||
.in(ParameterIn.QUERY)
|
||||
.required(true)
|
||||
.implementation(String.class))
|
||||
.response(responseBuilder()
|
||||
.implementation(ContentWrapper.class))
|
||||
)
|
||||
.build();
|
||||
}
|
||||
|
||||
private Mono<ServerResponse> deleteContent(ServerRequest request) {
|
||||
final var postName = request.pathVariable("name");
|
||||
final var snapshotName = request.queryParam("snapshotName").orElseThrow();
|
||||
return postService.deleteContent(postName, snapshotName)
|
||||
.flatMap(content -> ServerResponse.ok().bodyValue(content));
|
||||
}
|
||||
|
||||
private Mono<ServerResponse> revertToSpecifiedSnapshot(ServerRequest request) {
|
||||
final var postName = request.pathVariable("name");
|
||||
return request.bodyToMono(RevertSnapshotParam.class)
|
||||
.switchIfEmpty(
|
||||
Mono.error(new ServerWebInputException("Required request body is missing.")))
|
||||
.flatMap(param -> postService.revertToSpecifiedSnapshot(postName, param.snapshotName))
|
||||
.flatMap(post -> ServerResponse.ok().bodyValue(post));
|
||||
}
|
||||
|
||||
@Schema(name = "RevertSnapshotForPostParam")
|
||||
record RevertSnapshotParam(
|
||||
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, minLength = 1) String snapshotName) {
|
||||
}
|
||||
|
||||
private Mono<ServerResponse> fetchContent(ServerRequest request) {
|
||||
final var name = request.pathVariable("name");
|
||||
final var snapshotName = request.queryParam("snapshotName").orElseThrow();
|
||||
return client.fetch(Post.class, name)
|
||||
.flatMap(post -> {
|
||||
var baseSnapshot = post.getSpec().getBaseSnapshot();
|
||||
return postService.getContent(snapshotName, baseSnapshot);
|
||||
})
|
||||
.flatMap(content -> ServerResponse.ok().bodyValue(content));
|
||||
}
|
||||
|
||||
private Mono<ServerResponse> listSnapshots(ServerRequest request) {
|
||||
String name = request.pathVariable("name");
|
||||
var resultFlux = postService.listSnapshots(name);
|
||||
return ServerResponse.ok().body(resultFlux, ListedSnapshotDto.class);
|
||||
}
|
||||
|
||||
private Mono<ServerResponse> fetchReleaseContent(ServerRequest request) {
|
||||
final var name = request.pathVariable("name");
|
||||
return postService.getReleaseContent(name)
|
||||
|
@ -191,7 +296,7 @@ public class PostEndpoint implements CustomEndpoint {
|
|||
|
||||
Mono<ServerResponse> updateContent(ServerRequest request) {
|
||||
String postName = request.pathVariable("name");
|
||||
return request.bodyToMono(Content.class)
|
||||
return request.bodyToMono(ContentUpdateParam.class)
|
||||
.flatMap(content -> Mono.defer(() -> client.fetch(Post.class, postName)
|
||||
.flatMap(post -> {
|
||||
PostRequest postRequest = new PostRequest(post, content);
|
||||
|
|
|
@ -6,6 +6,7 @@ import static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;
|
|||
import static org.springdoc.core.fn.builders.requestbody.Builder.requestBodyBuilder;
|
||||
|
||||
import io.swagger.v3.oas.annotations.enums.ParameterIn;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.time.Duration;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
@ -18,12 +19,15 @@ import org.springframework.stereotype.Component;
|
|||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import org.springframework.web.server.ServerWebInputException;
|
||||
import org.thymeleaf.util.StringUtils;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.util.retry.Retry;
|
||||
import run.halo.app.content.Content;
|
||||
import run.halo.app.content.ContentUpdateParam;
|
||||
import run.halo.app.content.ContentWrapper;
|
||||
import run.halo.app.content.ListedSinglePage;
|
||||
import run.halo.app.content.ListedSnapshotDto;
|
||||
import run.halo.app.content.SinglePageQuery;
|
||||
import run.halo.app.content.SinglePageRequest;
|
||||
import run.halo.app.content.SinglePageService;
|
||||
|
@ -85,6 +89,33 @@ public class SinglePageEndpoint implements CustomEndpoint {
|
|||
.response(responseBuilder()
|
||||
.implementation(ContentWrapper.class))
|
||||
)
|
||||
.GET("singlepages/{name}/content", this::fetchContent,
|
||||
builder -> builder.operationId("fetchSinglePageContent")
|
||||
.description("Fetch content of single page.")
|
||||
.tag(tag)
|
||||
.parameter(parameterBuilder().name("name")
|
||||
.in(ParameterIn.PATH)
|
||||
.required(true)
|
||||
.implementation(String.class)
|
||||
)
|
||||
.parameter(parameterBuilder().name("snapshotName")
|
||||
.in(ParameterIn.QUERY)
|
||||
.required(true)
|
||||
.implementation(String.class))
|
||||
.response(responseBuilder()
|
||||
.implementation(ContentWrapper.class))
|
||||
)
|
||||
.GET("singlepages/{name}/snapshot", this::listSnapshots,
|
||||
builder -> builder.operationId("listSinglePageSnapshots")
|
||||
.description("List all snapshots for single page content.")
|
||||
.tag(tag)
|
||||
.parameter(parameterBuilder().name("name")
|
||||
.in(ParameterIn.PATH)
|
||||
.required(true)
|
||||
.implementation(String.class))
|
||||
.response(responseBuilder()
|
||||
.implementationArray(ListedSnapshotDto.class))
|
||||
)
|
||||
.POST("singlepages", this::draftSinglePage,
|
||||
builder -> builder.operationId("DraftSinglePage")
|
||||
.description("Draft a single page.")
|
||||
|
@ -135,6 +166,24 @@ public class SinglePageEndpoint implements CustomEndpoint {
|
|||
.response(responseBuilder()
|
||||
.implementation(Post.class))
|
||||
)
|
||||
.PUT("singlepages/{name}/revert-content", this::revertToSpecifiedSnapshot,
|
||||
builder -> builder.operationId("revertToSpecifiedSnapshotForSinglePage")
|
||||
.description("Revert to specified snapshot for single page content.")
|
||||
.tag(tag)
|
||||
.parameter(parameterBuilder().name("name")
|
||||
.in(ParameterIn.PATH)
|
||||
.required(true)
|
||||
.implementation(String.class))
|
||||
.requestBody(requestBodyBuilder()
|
||||
.required(true)
|
||||
.content(contentBuilder()
|
||||
.mediaType(MediaType.APPLICATION_JSON_VALUE)
|
||||
.schema(Builder.schemaBuilder()
|
||||
.implementation(RevertSnapshotParam.class))
|
||||
))
|
||||
.response(responseBuilder()
|
||||
.implementation(Post.class))
|
||||
)
|
||||
.PUT("singlepages/{name}/publish", this::publishSinglePage,
|
||||
builder -> builder.operationId("PublishSinglePage")
|
||||
.description("Publish a single page.")
|
||||
|
@ -146,9 +195,64 @@ public class SinglePageEndpoint implements CustomEndpoint {
|
|||
.response(responseBuilder()
|
||||
.implementation(SinglePage.class))
|
||||
)
|
||||
.DELETE("singlepages/{name}/content", this::deleteContent,
|
||||
builder -> builder.operationId("deleteSinglePageContent")
|
||||
.description("Delete a content for post.")
|
||||
.tag(tag)
|
||||
.parameter(parameterBuilder().name("name")
|
||||
.in(ParameterIn.PATH)
|
||||
.required(true)
|
||||
.implementation(String.class)
|
||||
)
|
||||
.parameter(parameterBuilder()
|
||||
.name("snapshotName")
|
||||
.in(ParameterIn.QUERY)
|
||||
.required(true)
|
||||
.implementation(String.class))
|
||||
.response(responseBuilder()
|
||||
.implementation(ContentWrapper.class))
|
||||
)
|
||||
.build();
|
||||
}
|
||||
|
||||
private Mono<ServerResponse> deleteContent(ServerRequest request) {
|
||||
final var postName = request.pathVariable("name");
|
||||
final var snapshotName = request.queryParam("snapshotName").orElseThrow();
|
||||
return singlePageService.deleteContent(postName, snapshotName)
|
||||
.flatMap(content -> ServerResponse.ok().bodyValue(content));
|
||||
}
|
||||
|
||||
private Mono<ServerResponse> revertToSpecifiedSnapshot(ServerRequest request) {
|
||||
final var postName = request.pathVariable("name");
|
||||
return request.bodyToMono(RevertSnapshotParam.class)
|
||||
.switchIfEmpty(
|
||||
Mono.error(new ServerWebInputException("Required request body is missing.")))
|
||||
.flatMap(
|
||||
param -> singlePageService.revertToSpecifiedSnapshot(postName, param.snapshotName))
|
||||
.flatMap(page -> ServerResponse.ok().bodyValue(page));
|
||||
}
|
||||
|
||||
@Schema(name = "RevertSnapshotForSingleParam")
|
||||
record RevertSnapshotParam(
|
||||
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, minLength = 1) String snapshotName) {
|
||||
}
|
||||
|
||||
private Mono<ServerResponse> fetchContent(ServerRequest request) {
|
||||
final var snapshotName = request.queryParam("snapshotName").orElseThrow();
|
||||
return client.fetch(SinglePage.class, request.pathVariable("name"))
|
||||
.flatMap(page -> {
|
||||
var baseSnapshot = page.getSpec().getBaseSnapshot();
|
||||
return singlePageService.getContent(snapshotName, baseSnapshot);
|
||||
})
|
||||
.flatMap(content -> ServerResponse.ok().bodyValue(content));
|
||||
}
|
||||
|
||||
private Mono<ServerResponse> listSnapshots(ServerRequest request) {
|
||||
final var name = request.pathVariable("name");
|
||||
var resultFlux = singlePageService.listSnapshots(name);
|
||||
return ServerResponse.ok().body(resultFlux, ListedSnapshotDto.class);
|
||||
}
|
||||
|
||||
private Mono<ServerResponse> fetchReleaseContent(ServerRequest request) {
|
||||
final var name = request.pathVariable("name");
|
||||
return singlePageService.getReleaseContent(name)
|
||||
|
@ -169,7 +273,7 @@ public class SinglePageEndpoint implements CustomEndpoint {
|
|||
|
||||
Mono<ServerResponse> updateContent(ServerRequest request) {
|
||||
String pageName = request.pathVariable("name");
|
||||
return request.bodyToMono(Content.class)
|
||||
return request.bodyToMono(ContentUpdateParam.class)
|
||||
.flatMap(content -> Mono.defer(() -> client.fetch(SinglePage.class, pageName)
|
||||
.flatMap(page -> {
|
||||
SinglePageRequest pageRequest = new SinglePageRequest(page, content);
|
||||
|
|
|
@ -258,26 +258,38 @@ public class UserEndpoint implements CustomEndpoint {
|
|||
.switchIfEmpty(Mono.error(
|
||||
() -> new ServerWebInputException("Request body is required."))
|
||||
)
|
||||
.flatMap(verifyEmailRequest -> ReactiveSecurityContextHolder.getContext()
|
||||
.map(SecurityContext::getAuthentication)
|
||||
.map(Principal::getName)
|
||||
.map(username -> Tuples.of(username, verifyEmailRequest.code()))
|
||||
)
|
||||
.flatMap(tuple2 -> {
|
||||
var username = tuple2.getT1();
|
||||
var code = tuple2.getT2();
|
||||
return Mono.just(username)
|
||||
.transformDeferred(verificationEmailRateLimiter(username))
|
||||
.flatMap(name -> emailVerificationService.verify(username, code))
|
||||
.onErrorMap(RequestNotPermitted.class, RateLimitExceededException::new);
|
||||
})
|
||||
.flatMap(this::doVerifyCode)
|
||||
.then(ServerResponse.ok().build());
|
||||
}
|
||||
|
||||
private Mono<Void> doVerifyCode(VerifyCodeRequest verifyCodeRequest) {
|
||||
return ReactiveSecurityContextHolder.getContext()
|
||||
.map(SecurityContext::getAuthentication)
|
||||
.map(Principal::getName)
|
||||
.flatMap(username -> verifyPasswordAndCode(username, verifyCodeRequest));
|
||||
}
|
||||
|
||||
private Mono<Void> verifyPasswordAndCode(String username, VerifyCodeRequest verifyCodeRequest) {
|
||||
return userService.confirmPassword(username, verifyCodeRequest.password())
|
||||
.filter(Boolean::booleanValue)
|
||||
.switchIfEmpty(Mono.error(new UnsatisfiedAttributeValueException(
|
||||
"Password is incorrect.", "problemDetail.user.password.notMatch", null)))
|
||||
.flatMap(verified -> verifyEmailCode(username, verifyCodeRequest.code()));
|
||||
}
|
||||
|
||||
private Mono<Void> verifyEmailCode(String username, String code) {
|
||||
return Mono.just(username)
|
||||
.transformDeferred(verificationEmailRateLimiter(username))
|
||||
.flatMap(name -> emailVerificationService.verify(username, code))
|
||||
.onErrorMap(RequestNotPermitted.class, RateLimitExceededException::new);
|
||||
}
|
||||
|
||||
public record EmailVerifyRequest(@Schema(requiredMode = REQUIRED) String email) {
|
||||
}
|
||||
|
||||
public record VerifyCodeRequest(@Schema(requiredMode = REQUIRED, minLength = 1) String code) {
|
||||
public record VerifyCodeRequest(
|
||||
@Schema(requiredMode = REQUIRED) String password,
|
||||
@Schema(requiredMode = REQUIRED, minLength = 1) String code) {
|
||||
}
|
||||
|
||||
private Mono<ServerResponse> sendEmailVerificationCode(ServerRequest request) {
|
||||
|
|
|
@ -67,12 +67,11 @@ public class CommentReconciler implements Reconciler<Reconciler.Request> {
|
|||
return;
|
||||
}
|
||||
if (addFinalizers(comment.getMetadata(), Set.of(FINALIZER_NAME))) {
|
||||
replyNotificationSubscriptionHelper.subscribeNewReplyReasonForComment(comment);
|
||||
client.update(comment);
|
||||
eventPublisher.publishEvent(new CommentCreatedEvent(this, comment));
|
||||
}
|
||||
|
||||
replyNotificationSubscriptionHelper.subscribeNewReplyReasonForComment(comment);
|
||||
|
||||
compatibleCreationTime(comment);
|
||||
Comment.CommentStatus status = comment.getStatusOrDefault();
|
||||
status.setHasNewReply(defaultIfNull(status.getUnreadReplyCount(), 0) > 0);
|
||||
|
|
|
@ -12,6 +12,7 @@ import static run.halo.app.plugin.PluginExtensionLoaderUtils.lookupExtensions;
|
|||
import static run.halo.app.plugin.PluginUtils.generateFileName;
|
||||
import static run.halo.app.plugin.PluginUtils.isDevelopmentMode;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.net.MalformedURLException;
|
||||
|
@ -21,17 +22,24 @@ import java.nio.file.Files;
|
|||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.pf4j.PluginManager;
|
||||
import org.pf4j.PluginRuntimeException;
|
||||
import org.pf4j.PluginState;
|
||||
import org.pf4j.PluginStateEvent;
|
||||
import org.pf4j.PluginStateListener;
|
||||
import org.pf4j.PluginWrapper;
|
||||
import org.pf4j.RuntimeMode;
|
||||
import org.springframework.core.io.DefaultResourceLoader;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.ResourceUtils;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
import run.halo.app.core.extension.Plugin;
|
||||
|
@ -50,10 +58,13 @@ import run.halo.app.extension.controller.Reconciler.Request;
|
|||
import run.halo.app.extension.controller.RequeueException;
|
||||
import run.halo.app.infra.Condition;
|
||||
import run.halo.app.infra.ConditionStatus;
|
||||
import run.halo.app.infra.utils.JsonParseException;
|
||||
import run.halo.app.infra.utils.JsonUtils;
|
||||
import run.halo.app.infra.utils.PathUtils;
|
||||
import run.halo.app.infra.utils.YamlUnstructuredLoader;
|
||||
import run.halo.app.plugin.PluginConst;
|
||||
import run.halo.app.plugin.PluginProperties;
|
||||
import run.halo.app.plugin.SpringPluginManager;
|
||||
|
||||
/**
|
||||
* Plugin reconciler.
|
||||
|
@ -66,19 +77,23 @@ import run.halo.app.plugin.PluginProperties;
|
|||
@Component
|
||||
public class PluginReconciler implements Reconciler<Request> {
|
||||
private static final String FINALIZER_NAME = "plugin-protection";
|
||||
private static final String DEPENDENTS_ANNO_KEY = "plugin.halo.run/dependents-snapshot";
|
||||
private final ExtensionClient client;
|
||||
private final PluginManager pluginManager;
|
||||
private final SpringPluginManager pluginManager;
|
||||
|
||||
private final PluginProperties pluginProperties;
|
||||
|
||||
private Clock clock;
|
||||
|
||||
public PluginReconciler(ExtensionClient client, PluginManager pluginManager,
|
||||
public PluginReconciler(ExtensionClient client, SpringPluginManager pluginManager,
|
||||
PluginProperties pluginProperties) {
|
||||
this.client = client;
|
||||
this.pluginManager = pluginManager;
|
||||
this.pluginProperties = pluginProperties;
|
||||
this.clock = Clock.systemUTC();
|
||||
|
||||
this.pluginManager.addPluginStateListener(new PluginStartedListener());
|
||||
this.pluginManager.addPluginStateListener(new PluginStoppedListener());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -95,6 +110,11 @@ public class PluginReconciler implements Reconciler<Request> {
|
|||
return client.fetch(Plugin.class, request.name())
|
||||
.map(plugin -> {
|
||||
if (ExtensionUtil.isDeleted(plugin)) {
|
||||
if (!checkDependents(plugin)) {
|
||||
client.update(plugin);
|
||||
// Check dependents every 10 seconds
|
||||
return Result.requeue(Duration.ofSeconds(10));
|
||||
}
|
||||
// CleanUp resources and remove finalizer.
|
||||
if (removeFinalizers(plugin.getMetadata(), Set.of(FINALIZER_NAME))) {
|
||||
cleanupResources(plugin);
|
||||
|
@ -117,10 +137,10 @@ public class PluginReconciler implements Reconciler<Request> {
|
|||
|
||||
if (requestToEnable(plugin)) {
|
||||
// Start
|
||||
startPlugin(plugin);
|
||||
enablePlugin(plugin);
|
||||
} else {
|
||||
// stop the plugin and disable it
|
||||
stopAndDisablePlugin(plugin);
|
||||
disablePlugin(plugin);
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
// populate condition
|
||||
|
@ -145,6 +165,28 @@ public class PluginReconciler implements Reconciler<Request> {
|
|||
.orElseGet(Result::doNotRetry);
|
||||
}
|
||||
|
||||
private boolean checkDependents(Plugin plugin) {
|
||||
var pluginId = plugin.getMetadata().getName();
|
||||
var dependents = pluginManager.getDependents(pluginId);
|
||||
if (CollectionUtils.isEmpty(dependents)) {
|
||||
return true;
|
||||
}
|
||||
var status = plugin.statusNonNull();
|
||||
var condition = Condition.builder()
|
||||
.type(PluginState.FAILED.toString())
|
||||
.reason("DependentsExist")
|
||||
.message(
|
||||
"The plugin has dependents %s, please delete them first."
|
||||
.formatted(dependents.stream().map(PluginWrapper::getPluginId).toList())
|
||||
)
|
||||
.status(ConditionStatus.FALSE)
|
||||
.lastTransitionTime(clock.instant())
|
||||
.build();
|
||||
nullSafeConditions(status).addAndEvictFIFO(condition);
|
||||
status.setPhase(Plugin.Phase.FAILED);
|
||||
return false;
|
||||
}
|
||||
|
||||
private void syncPluginState(Plugin plugin) {
|
||||
var pluginName = plugin.getMetadata().getName();
|
||||
var p = pluginManager.getPlugin(pluginName);
|
||||
|
@ -191,32 +233,87 @@ public class PluginReconciler implements Reconciler<Request> {
|
|||
}
|
||||
}
|
||||
|
||||
private void startPlugin(Plugin plugin) {
|
||||
private void enablePlugin(Plugin plugin) {
|
||||
// start the plugin
|
||||
var pluginName = plugin.getMetadata().getName();
|
||||
var wrapper = pluginManager.getPlugin(pluginName);
|
||||
log.info("Starting plugin {}", pluginName);
|
||||
plugin.statusNonNull().setPhase(Plugin.Phase.STARTING);
|
||||
if (!PluginState.STARTED.equals(wrapper.getPluginState())) {
|
||||
var pluginState = pluginManager.startPlugin(pluginName);
|
||||
if (!PluginState.STARTED.equals(pluginState)) {
|
||||
throw new IllegalStateException("Failed to start plugin " + pluginName);
|
||||
}
|
||||
plugin.statusNonNull().setLastStartTime(clock.instant());
|
||||
var condition = Condition.builder()
|
||||
.type(PluginState.STARTED.toString())
|
||||
.reason(PluginState.STARTED.toString())
|
||||
.message("Started successfully")
|
||||
.lastTransitionTime(clock.instant())
|
||||
.status(ConditionStatus.TRUE)
|
||||
.build();
|
||||
nullSafeConditions(plugin.statusNonNull()).addAndEvictFIFO(condition);
|
||||
var pluginState = pluginManager.startPlugin(pluginName);
|
||||
if (!PluginState.STARTED.equals(pluginState)) {
|
||||
throw new IllegalStateException("Failed to start plugin " + pluginName);
|
||||
}
|
||||
|
||||
var dependents = getAndRemoveDependents(plugin);
|
||||
log.info("Starting dependents {} for plugin {}", dependents, pluginName);
|
||||
dependents.stream()
|
||||
.sorted(Comparator.reverseOrder())
|
||||
.forEach(dependent -> {
|
||||
if (pluginManager.getPlugin(dependent) != null) {
|
||||
pluginManager.startPlugin(dependent);
|
||||
}
|
||||
});
|
||||
log.info("Started dependents {} for plugin {}", dependents, pluginName);
|
||||
|
||||
plugin.statusNonNull().setLastStartTime(clock.instant());
|
||||
var condition = Condition.builder()
|
||||
.type(PluginState.STARTED.toString())
|
||||
.reason(PluginState.STARTED.toString())
|
||||
.message("Started successfully")
|
||||
.lastTransitionTime(clock.instant())
|
||||
.status(ConditionStatus.TRUE)
|
||||
.build();
|
||||
nullSafeConditions(plugin.statusNonNull()).addAndEvictFIFO(condition);
|
||||
plugin.statusNonNull().setPhase(Plugin.Phase.STARTED);
|
||||
|
||||
log.info("Started plugin {}", pluginName);
|
||||
}
|
||||
|
||||
private void stopAndDisablePlugin(Plugin plugin) {
|
||||
private List<String> getAndRemoveDependents(Plugin plugin) {
|
||||
var pluginName = plugin.getMetadata().getName();
|
||||
var annotations = plugin.getMetadata().getAnnotations();
|
||||
if (annotations == null) {
|
||||
return List.of();
|
||||
}
|
||||
var dependentsAnno = annotations.remove(DEPENDENTS_ANNO_KEY);
|
||||
List<String> dependents = List.of();
|
||||
if (StringUtils.isNotBlank(dependentsAnno)) {
|
||||
try {
|
||||
dependents = JsonUtils.jsonToObject(dependentsAnno, new TypeReference<>() {
|
||||
});
|
||||
} catch (JsonParseException ignored) {
|
||||
log.error("Failed to parse dependents annotation {} for plugin {}",
|
||||
dependentsAnno, pluginName);
|
||||
// Keep going to start the plugin.
|
||||
}
|
||||
}
|
||||
return dependents;
|
||||
}
|
||||
|
||||
private void setDependents(Plugin plugin) {
|
||||
var pluginName = plugin.getMetadata().getName();
|
||||
var annotations = plugin.getMetadata().getAnnotations();
|
||||
if (annotations == null) {
|
||||
annotations = new HashMap<>();
|
||||
plugin.getMetadata().setAnnotations(annotations);
|
||||
}
|
||||
if (!annotations.containsKey(DEPENDENTS_ANNO_KEY)) {
|
||||
// get dependents
|
||||
var dependents = pluginManager.getDependents(pluginName)
|
||||
.stream()
|
||||
.filter(pluginWrapper ->
|
||||
Objects.equals(PluginState.STARTED, pluginWrapper.getPluginState())
|
||||
)
|
||||
.map(PluginWrapper::getPluginId)
|
||||
.toList();
|
||||
|
||||
annotations.put(DEPENDENTS_ANNO_KEY, JsonUtils.objectToJson(dependents));
|
||||
}
|
||||
}
|
||||
|
||||
private void disablePlugin(Plugin plugin) {
|
||||
var pluginName = plugin.getMetadata().getName();
|
||||
if (pluginManager.getPlugin(pluginName) != null) {
|
||||
setDependents(plugin);
|
||||
pluginManager.disablePlugin(pluginName);
|
||||
}
|
||||
plugin.statusNonNull().setPhase(Plugin.Phase.DISABLED);
|
||||
|
@ -284,29 +381,29 @@ public class PluginReconciler implements Reconciler<Request> {
|
|||
}
|
||||
|
||||
private void loadOrReload(Plugin plugin) {
|
||||
// TODO Try to check dependencies before.
|
||||
var pluginName = plugin.getMetadata().getName();
|
||||
try {
|
||||
var p = pluginManager.getPlugin(pluginName);
|
||||
var requestToReload = requestToReload(plugin);
|
||||
if (requestToReload) {
|
||||
log.info("Unloading plugin {}", pluginName);
|
||||
if (p != null) {
|
||||
pluginManager.unloadPlugin(pluginName);
|
||||
}
|
||||
}
|
||||
if (p == null || requestToReload) {
|
||||
log.info("Loading plugin {}", pluginName);
|
||||
var p = pluginManager.getPlugin(pluginName);
|
||||
var requestToReload = requestToReload(plugin);
|
||||
if (requestToReload) {
|
||||
if (p != null) {
|
||||
var loadLocation = plugin.getStatus().getLoadLocation();
|
||||
pluginManager.loadPlugin(Paths.get(loadLocation));
|
||||
log.info("Loaded plugin {}", pluginName);
|
||||
setDependents(plugin);
|
||||
var unloaded = pluginManager.reloadPlugin(pluginName, Paths.get(loadLocation));
|
||||
if (!unloaded) {
|
||||
throw new PluginRuntimeException("Failed to reload plugin " + pluginName);
|
||||
}
|
||||
p = pluginManager.getPlugin(pluginName);
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
// unload the plugin
|
||||
if (pluginManager.getPlugin(pluginName) != null) {
|
||||
pluginManager.unloadPlugin(pluginName);
|
||||
}
|
||||
throw t;
|
||||
}
|
||||
if (p != null && pluginManager.getUnresolvedPlugins().contains(p)) {
|
||||
pluginManager.unloadPlugin(pluginName);
|
||||
p = null;
|
||||
}
|
||||
if (p == null) {
|
||||
var loadLocation = plugin.getStatus().getLoadLocation();
|
||||
log.info("Loading plugin {} from {}", pluginName, loadLocation);
|
||||
pluginManager.loadPlugin(Paths.get(loadLocation));
|
||||
log.info("Loaded plugin {} from {}", pluginName, loadLocation);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -480,4 +577,38 @@ public class PluginReconciler implements Reconciler<Request> {
|
|||
return pluginName + "-system-generated-reverse-proxy";
|
||||
}
|
||||
|
||||
public class PluginStartedListener implements PluginStateListener {
|
||||
|
||||
@Override
|
||||
public void pluginStateChanged(PluginStateEvent event) {
|
||||
if (PluginState.STARTED.equals(event.getPluginState())) {
|
||||
var pluginId = event.getPlugin().getPluginId();
|
||||
client.fetch(Plugin.class, pluginId)
|
||||
.ifPresent(plugin -> {
|
||||
if (!Objects.equals(true, plugin.getSpec().getEnabled())) {
|
||||
plugin.getSpec().setEnabled(true);
|
||||
client.update(plugin);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class PluginStoppedListener implements PluginStateListener {
|
||||
|
||||
@Override
|
||||
public void pluginStateChanged(PluginStateEvent event) {
|
||||
if (PluginState.STOPPED.equals(event.getPluginState())) {
|
||||
var pluginId = event.getPlugin().getPluginId();
|
||||
client.fetch(Plugin.class, pluginId)
|
||||
.ifPresent(plugin -> {
|
||||
if (!requestToReload(plugin)
|
||||
&& Objects.equals(true, plugin.getSpec().getEnabled())) {
|
||||
plugin.getSpec().setEnabled(false);
|
||||
client.update(plugin);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -246,11 +246,8 @@ public class PostReconciler implements Reconciler<Reconciler.Request> {
|
|||
|
||||
var interestReason = new Subscription.InterestReason();
|
||||
interestReason.setReasonType(NotificationReasonConst.NEW_COMMENT_ON_POST);
|
||||
interestReason.setSubject(Subscription.ReasonSubject.builder()
|
||||
.apiVersion(post.getApiVersion())
|
||||
.kind(post.getKind())
|
||||
.name(post.getMetadata().getName())
|
||||
.build());
|
||||
interestReason.setExpression(
|
||||
"props.postOwner == '%s'".formatted(post.getSpec().getOwner()));
|
||||
notificationCenter.subscribe(subscriber, interestReason).block();
|
||||
}
|
||||
|
||||
|
|
|
@ -46,6 +46,7 @@ public class ReplyReconciler implements Reconciler<Reconciler.Request> {
|
|||
return;
|
||||
}
|
||||
if (addFinalizers(reply.getMetadata(), Set.of(FINALIZER_NAME))) {
|
||||
replyNotificationSubscriptionHelper.subscribeNewReplyReasonForReply(reply);
|
||||
client.update(reply);
|
||||
eventPublisher.publishEvent(new ReplyCreatedEvent(this, reply));
|
||||
}
|
||||
|
@ -64,8 +65,6 @@ public class ReplyReconciler implements Reconciler<Reconciler.Request> {
|
|||
|
||||
client.update(reply);
|
||||
|
||||
replyNotificationSubscriptionHelper.subscribeNewReplyReasonForReply(reply);
|
||||
|
||||
eventPublisher.publishEvent(new ReplyChangedEvent(this, reply));
|
||||
});
|
||||
return new Result(false, null);
|
||||
|
|
|
@ -107,11 +107,8 @@ public class SinglePageReconciler implements Reconciler<Reconciler.Request> {
|
|||
|
||||
var interestReason = new Subscription.InterestReason();
|
||||
interestReason.setReasonType(NotificationReasonConst.NEW_COMMENT_ON_PAGE);
|
||||
interestReason.setSubject(Subscription.ReasonSubject.builder()
|
||||
.apiVersion(page.getApiVersion())
|
||||
.kind(page.getKind())
|
||||
.name(page.getMetadata().getName())
|
||||
.build());
|
||||
interestReason.setExpression(
|
||||
"props.pageOwner == '%s'".formatted(page.getSpec().getOwner()));
|
||||
notificationCenter.subscribe(subscriber, interestReason).block();
|
||||
}
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ import org.springframework.web.reactive.function.server.ServerResponse;
|
|||
import org.springframework.web.server.ServerWebInputException;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.content.Content;
|
||||
import run.halo.app.content.ContentUpdateParam;
|
||||
import run.halo.app.content.ListedPost;
|
||||
import run.halo.app.content.PostQuery;
|
||||
import run.halo.app.content.PostRequest;
|
||||
|
@ -280,7 +281,7 @@ public class UcPostEndpoint implements CustomEndpoint {
|
|||
}
|
||||
post.getSpec().setOwner(username);
|
||||
}))
|
||||
.map(post -> new PostRequest(post, getContent(post)))
|
||||
.map(post -> new PostRequest(post, ContentUpdateParam.from(getContent(post))))
|
||||
.flatMap(postService::draftPost);
|
||||
return ServerResponse.ok().body(createdPost, Post.class);
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ import org.springframework.stereotype.Component;
|
|||
import org.springframework.util.CollectionUtils;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.extension.ExtensionUtil;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.extension.Unstructured;
|
||||
import run.halo.app.infra.properties.HaloProperties;
|
||||
|
@ -96,7 +97,13 @@ public class ExtensionResourceInitializer implements ApplicationListener<Applica
|
|||
extension.getMetadata().setVersion(existingExt.getMetadata().getVersion());
|
||||
return extensionClient.update(extension);
|
||||
})
|
||||
.switchIfEmpty(Mono.defer(() -> extensionClient.create(extension)));
|
||||
.switchIfEmpty(Mono.defer(() -> {
|
||||
if (ExtensionUtil.isDeleted(extension)) {
|
||||
// skip deleted extension
|
||||
return Mono.empty();
|
||||
}
|
||||
return extensionClient.create(extension);
|
||||
}));
|
||||
}
|
||||
|
||||
private List<Resource> listResources(String location) {
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
package run.halo.app.infra;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.extension.Extension;
|
||||
import run.halo.app.extension.ListOptions;
|
||||
|
||||
/**
|
||||
* Reactive extension paginated operator to handle extensions by pagination.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.15.0
|
||||
*/
|
||||
public interface ReactiveExtensionPaginatedOperator {
|
||||
|
||||
/**
|
||||
* <p>Deletes all data, including any new entries added during the execution of this method.</p>
|
||||
* <p>This method continuously monitors and removes data that appears throughout its runtime,
|
||||
* ensuring that even data created during the deletion process is also removed.</p>
|
||||
*/
|
||||
<E extends Extension> Mono<Void> deleteContinuously(Class<E> type,
|
||||
ListOptions listOptions);
|
||||
|
||||
/**
|
||||
* <p>Deletes only the data that existed at the start of the operation.</p>
|
||||
* <p>This method takes a snapshot of the data at the beginning and deletes only that dataset;
|
||||
* any data added after the method starts will not be affected or removed.</p>
|
||||
*/
|
||||
<E extends Extension> Flux<E> deleteInitialBatch(Class<E> type,
|
||||
ListOptions listOptions);
|
||||
|
||||
/**
|
||||
* <p>Note that: This method can not be used for <code>deletion</code> operation, because
|
||||
* deletion operation will change the total records.</p>
|
||||
*/
|
||||
<E extends Extension> Flux<E> list(Class<E> type, ListOptions listOptions);
|
||||
}
|
|
@ -0,0 +1,132 @@
|
|||
package run.halo.app.infra;
|
||||
|
||||
import static run.halo.app.extension.index.query.QueryFactory.isNull;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.dao.OptimisticLockingFailureException;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.stereotype.Component;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.util.retry.Retry;
|
||||
import run.halo.app.extension.Extension;
|
||||
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;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class ReactiveExtensionPaginatedOperatorImpl implements ReactiveExtensionPaginatedOperator {
|
||||
private static final int DEFAULT_PAGE_SIZE = 200;
|
||||
private final ReactiveExtensionClient client;
|
||||
|
||||
@Override
|
||||
public <E extends Extension> Mono<Void> deleteContinuously(Class<E> type,
|
||||
ListOptions listOptions) {
|
||||
var pageRequest = createPageRequest();
|
||||
return cleanupContinuously(type, listOptions, pageRequest);
|
||||
}
|
||||
|
||||
private <E extends Extension> Mono<Void> cleanupContinuously(Class<E> type,
|
||||
ListOptions listOptions,
|
||||
PageRequest pageRequest) {
|
||||
// forever loop first page until no more to delete
|
||||
return pageBy(type, listOptions, pageRequest)
|
||||
.flatMap(page -> Flux.fromIterable(page.getItems())
|
||||
.flatMap(client::delete)
|
||||
.then(page.hasNext() ? cleanupContinuously(type, listOptions, pageRequest)
|
||||
: Mono.empty())
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <E extends Extension> Flux<E> deleteInitialBatch(Class<E> type,
|
||||
ListOptions listOptions) {
|
||||
var pageRequest = createPageRequest();
|
||||
var newFieldQuery = listOptions.getFieldSelector()
|
||||
.andQuery(isNull("metadata.deletionTimestamp"));
|
||||
listOptions.setFieldSelector(newFieldQuery);
|
||||
final Instant now = Instant.now();
|
||||
|
||||
return pageBy(type, listOptions, pageRequest)
|
||||
// forever loop first page until no more to delete
|
||||
.expand(result -> result.hasNext()
|
||||
? pageBy(type, listOptions, pageRequest) : Mono.empty())
|
||||
.flatMap(result -> Flux.fromIterable(result.getItems()))
|
||||
.takeWhile(item -> shouldTakeNext(item, now))
|
||||
.flatMap(this::deleteWithRetry);
|
||||
}
|
||||
|
||||
static <E extends Extension> boolean shouldTakeNext(E item, Instant now) {
|
||||
var creationTimestamp = item.getMetadata().getCreationTimestamp();
|
||||
return creationTimestamp.isBefore(now)
|
||||
|| creationTimestamp.equals(now);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
<E extends Extension> Mono<E> deleteWithRetry(E item) {
|
||||
return client.delete(item)
|
||||
.onErrorResume(OptimisticLockingFailureException.class,
|
||||
e -> attemptToDelete((Class<E>) item.getClass(), item.getMetadata().getName()));
|
||||
}
|
||||
|
||||
private <E extends Extension> Mono<E> attemptToDelete(Class<E> type, String name) {
|
||||
return Mono.defer(() -> client.fetch(type, name)
|
||||
.flatMap(client::delete)
|
||||
)
|
||||
.retryWhen(Retry.backoff(8, Duration.ofMillis(100))
|
||||
.filter(OptimisticLockingFailureException.class::isInstance));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <E extends Extension> Flux<E> list(Class<E> type, ListOptions listOptions) {
|
||||
var pageRequest = createPageRequest();
|
||||
return list(type, listOptions, pageRequest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Paginated list all items to avoid memory overflow.
|
||||
* <pre>
|
||||
* 1. Retrieve data multiple times until all data is consumed.
|
||||
* 2. Fetch next page if current page has more data and consumed records is less than total
|
||||
* records.
|
||||
* 3. Take while consumed records is less than total records.
|
||||
* 4. totalRecords from first page to ensure new inserted data will not be counted in during
|
||||
* querying to avoid infinite loop.
|
||||
* </pre>
|
||||
*/
|
||||
private <E extends Extension> Flux<E> list(Class<E> type, ListOptions listOptions,
|
||||
PageRequest pageRequest) {
|
||||
final var now = Instant.now();
|
||||
return pageBy(type, listOptions, pageRequest)
|
||||
.expand(result -> {
|
||||
if (result.hasNext()) {
|
||||
// fetch next page
|
||||
var nextPage = nextPage(result, pageRequest.getSort());
|
||||
return pageBy(type, listOptions, nextPage);
|
||||
} else {
|
||||
return Mono.empty();
|
||||
}
|
||||
})
|
||||
.flatMap(page -> Flux.fromIterable(page.getItems()))
|
||||
.takeWhile(item -> shouldTakeNext(item, now));
|
||||
}
|
||||
|
||||
static <E extends Extension> PageRequest nextPage(ListResult<E> result, Sort sort) {
|
||||
return PageRequestImpl.of(result.getPage() + 1, result.getSize(), sort);
|
||||
}
|
||||
|
||||
private PageRequest createPageRequest() {
|
||||
return PageRequestImpl.of(1, DEFAULT_PAGE_SIZE,
|
||||
Sort.by("metadata.creationTimestamp", "metadata.name"));
|
||||
}
|
||||
|
||||
private <E extends Extension> Mono<ListResult<E>> pageBy(Class<E> type, ListOptions listOptions,
|
||||
PageRequest pageRequest) {
|
||||
return client.listBy(type, listOptions, pageRequest);
|
||||
}
|
||||
}
|
|
@ -438,6 +438,11 @@ public class SchemeInitializer implements ApplicationListener<ApplicationContext
|
|||
.setIndexFunc(simpleAttribute(Subscription.class,
|
||||
subscription -> subscription.getSpec().getReason().getSubject().toString()))
|
||||
);
|
||||
indexSpecs.add(new IndexSpec()
|
||||
.setName("spec.reason.expression")
|
||||
.setIndexFunc(simpleAttribute(Subscription.class,
|
||||
subscription -> subscription.getSpec().getReason().getExpression()))
|
||||
);
|
||||
indexSpecs.add(new IndexSpec()
|
||||
.setName("spec.subscriber")
|
||||
.setIndexFunc(simpleAttribute(Subscription.class,
|
||||
|
|
|
@ -1,23 +1,14 @@
|
|||
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;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.function.BiPredicate;
|
||||
import java.util.function.Function;
|
||||
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;
|
||||
|
@ -27,15 +18,8 @@ import run.halo.app.core.extension.notification.NotifierDescriptor;
|
|||
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.ListResult;
|
||||
import run.halo.app.extension.Metadata;
|
||||
import run.halo.app.extension.PageRequest;
|
||||
import run.halo.app.extension.PageRequestImpl;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.extension.index.query.Query;
|
||||
import run.halo.app.extension.router.selector.FieldSelector;
|
||||
import run.halo.app.notification.endpoint.SubscriptionRouter;
|
||||
|
||||
/**
|
||||
|
@ -55,37 +39,33 @@ public class DefaultNotificationCenter implements NotificationCenter {
|
|||
private final UserNotificationPreferenceService userNotificationPreferenceService;
|
||||
private final NotificationTemplateRender notificationTemplateRender;
|
||||
private final SubscriptionRouter subscriptionRouter;
|
||||
private final RecipientResolver recipientResolver;
|
||||
private final SubscriptionService subscriptionService;
|
||||
|
||||
@Override
|
||||
public Mono<Void> notify(Reason reason) {
|
||||
var reasonSubject = reason.getSpec().getSubject();
|
||||
var subscriptionReasonSubject = Subscription.ReasonSubject.builder()
|
||||
.apiVersion(reasonSubject.getApiVersion())
|
||||
.kind(reasonSubject.getKind())
|
||||
.name(reasonSubject.getName())
|
||||
.build();
|
||||
return listObservers(reason.getSpec().getReasonType(), subscriptionReasonSubject)
|
||||
.doOnNext(subscription -> {
|
||||
return recipientResolver.resolve(reason)
|
||||
.doOnNext(subscriber -> {
|
||||
log.debug("Dispatching notification to subscriber [{}] for reason [{}]",
|
||||
subscription.getSpec().getSubscriber(), reason.getMetadata().getName());
|
||||
subscriber, reason.getMetadata().getName());
|
||||
})
|
||||
.publishOn(Schedulers.boundedElastic())
|
||||
.flatMap(subscription -> dispatchNotification(reason, subscription))
|
||||
.flatMap(subscriber -> dispatchNotification(reason, subscriber))
|
||||
.then();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Subscription> subscribe(Subscription.Subscriber subscriber,
|
||||
Subscription.InterestReason reason) {
|
||||
return listSubscription(subscriber, reason)
|
||||
.next()
|
||||
.switchIfEmpty(Mono.defer(() -> {
|
||||
return unsubscribe(subscriber, reason)
|
||||
.then(Mono.defer(() -> {
|
||||
var subscription = new Subscription();
|
||||
subscription.setMetadata(new Metadata());
|
||||
subscription.getMetadata().setGenerateName("subscription-");
|
||||
subscription.setSpec(new Subscription.Spec());
|
||||
subscription.getSpec().setUnsubscribeToken(Subscription.generateUnsubscribeToken());
|
||||
subscription.getSpec().setSubscriber(subscriber);
|
||||
Subscription.InterestReason.ensureSubjectHasValue(reason);
|
||||
subscription.getSpec().setReason(reason);
|
||||
return client.create(subscription);
|
||||
}));
|
||||
|
@ -93,75 +73,47 @@ public class DefaultNotificationCenter implements NotificationCenter {
|
|||
|
||||
@Override
|
||||
public Mono<Void> unsubscribe(Subscription.Subscriber subscriber) {
|
||||
// pagination query all subscriptions of the subscriber to avoid large data
|
||||
var pageRequest = PageRequestImpl.of(1, 200,
|
||||
Sort.by("metadata.creationTimestamp", "metadata.name"));
|
||||
return Flux.defer(() -> pageSubscriptionBy(subscriber, pageRequest))
|
||||
.expand(page -> page.hasNext()
|
||||
? pageSubscriptionBy(subscriber, pageRequest.next())
|
||||
: Mono.empty()
|
||||
)
|
||||
.flatMap(page -> Flux.fromIterable(page.getItems()))
|
||||
.flatMap(client::delete)
|
||||
.then();
|
||||
return subscriptionService.remove(subscriber).then();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> unsubscribe(Subscription.Subscriber subscriber,
|
||||
Subscription.InterestReason reason) {
|
||||
return listSubscription(subscriber, reason)
|
||||
.flatMap(client::delete)
|
||||
.then();
|
||||
return subscriptionService.remove(subscriber, reason).then();
|
||||
}
|
||||
|
||||
Mono<ListResult<Subscription>> pageSubscriptionBy(Subscription.Subscriber subscriber,
|
||||
PageRequest pageRequest) {
|
||||
var listOptions = new ListOptions();
|
||||
var fieldQuery = equal("spec.subscriber", subscriber.getName());
|
||||
listOptions.setFieldSelector(FieldSelector.of(fieldQuery));
|
||||
return client.listBy(Subscription.class, listOptions, pageRequest);
|
||||
}
|
||||
|
||||
Flux<Subscription> listSubscription(Subscription.Subscriber subscriber,
|
||||
Subscription.InterestReason reason) {
|
||||
var listOptions = new ListOptions();
|
||||
var fieldQuery = and(
|
||||
getSubscriptionFieldQuery(reason.getReasonType(), reason.getSubject()),
|
||||
equal("spec.subscriber", subscriber.getName())
|
||||
);
|
||||
listOptions.setFieldSelector(FieldSelector.of(fieldQuery));
|
||||
return client.listAll(Subscription.class, listOptions, defaultSort());
|
||||
}
|
||||
|
||||
Flux<String> getNotifiersBySubscriber(Subscription.Subscriber subscriber, Reason reason) {
|
||||
Flux<String> getNotifiersBySubscriber(Subscriber subscriber, Reason reason) {
|
||||
var reasonType = reason.getSpec().getReasonType();
|
||||
return userNotificationPreferenceService.getByUser(subscriber.getName())
|
||||
return userNotificationPreferenceService.getByUser(subscriber.name())
|
||||
.map(UserNotificationPreference::getReasonTypeNotifier)
|
||||
.map(reasonTypeNotification -> reasonTypeNotification.getNotifiers(reasonType))
|
||||
.flatMapMany(Flux::fromIterable);
|
||||
}
|
||||
|
||||
Mono<Void> dispatchNotification(Reason reason, Subscription subscription) {
|
||||
var subscriber = subscription.getSpec().getSubscriber();
|
||||
Mono<Void> dispatchNotification(Reason reason, Subscriber subscriber) {
|
||||
return getNotifiersBySubscriber(subscriber, reason)
|
||||
.flatMap(notifierName -> client.fetch(NotifierDescriptor.class, notifierName))
|
||||
.flatMap(descriptor -> prepareNotificationElement(subscription, reason, descriptor))
|
||||
.flatMap(descriptor -> prepareNotificationElement(subscriber, reason, descriptor))
|
||||
.flatMap(element -> {
|
||||
var dispatchMono = sendNotification(element);
|
||||
if (subscriber.isAnonymous()) {
|
||||
return dispatchMono;
|
||||
}
|
||||
// create notification for user
|
||||
var innerNofificationMono = createNotification(element);
|
||||
return Mono.when(dispatchMono, innerNofificationMono);
|
||||
})
|
||||
.then();
|
||||
}
|
||||
|
||||
Mono<NotificationElement> prepareNotificationElement(Subscription subscription, Reason reason,
|
||||
Mono<NotificationElement> prepareNotificationElement(Subscriber subscriber, Reason reason,
|
||||
NotifierDescriptor descriptor) {
|
||||
return getLocaleFromSubscriber(subscription)
|
||||
.flatMap(locale -> inferenceTemplate(reason, subscription, locale))
|
||||
return getLocaleFromSubscriber(subscriber)
|
||||
.flatMap(locale -> inferenceTemplate(reason, subscriber, locale))
|
||||
.map(notificationContent -> NotificationElement.builder()
|
||||
.descriptor(descriptor)
|
||||
.reason(reason)
|
||||
.subscription(subscription)
|
||||
.subscriber(subscriber)
|
||||
.reasonType(notificationContent.reasonType())
|
||||
.notificationTitle(notificationContent.title())
|
||||
.reasonAttributes(notificationContent.reasonAttributes())
|
||||
|
@ -173,7 +125,7 @@ public class DefaultNotificationCenter implements NotificationCenter {
|
|||
|
||||
Mono<Void> sendNotification(NotificationElement notificationElement) {
|
||||
var descriptor = notificationElement.descriptor();
|
||||
var subscription = notificationElement.subscription();
|
||||
var subscriber = notificationElement.subscriber();
|
||||
final var notifierExtName = descriptor.getSpec().getNotifierExtName();
|
||||
return notificationContextFrom(notificationElement)
|
||||
.flatMap(notificationContext -> notificationSender.sendNotification(notifierExtName,
|
||||
|
@ -181,7 +133,7 @@ public class DefaultNotificationCenter implements NotificationCenter {
|
|||
.onErrorResume(throwable -> {
|
||||
log.error(
|
||||
"Failed to send notification to subscriber [{}] through notifier [{}]",
|
||||
subscription.getSpec().getSubscriber(),
|
||||
subscriber,
|
||||
descriptor.getSpec().getDisplayName(),
|
||||
throwable);
|
||||
return Mono.empty();
|
||||
|
@ -192,9 +144,8 @@ public class DefaultNotificationCenter implements NotificationCenter {
|
|||
|
||||
Mono<Notification> createNotification(NotificationElement notificationElement) {
|
||||
var reason = notificationElement.reason();
|
||||
var subscription = notificationElement.subscription();
|
||||
var subscriber = subscription.getSpec().getSubscriber();
|
||||
return client.fetch(User.class, subscriber.getName())
|
||||
var subscriber = notificationElement.subscriber();
|
||||
return client.fetch(User.class, subscriber.name())
|
||||
.flatMap(user -> {
|
||||
Notification notification = new Notification();
|
||||
notification.setMetadata(new Metadata());
|
||||
|
@ -203,7 +154,7 @@ public class DefaultNotificationCenter implements NotificationCenter {
|
|||
notification.getSpec().setTitle(notificationElement.notificationTitle());
|
||||
notification.getSpec().setRawContent(notificationElement.notificationRawBody());
|
||||
notification.getSpec().setHtmlContent(notificationElement.notificationHtmlBody);
|
||||
notification.getSpec().setRecipient(subscriber.getName());
|
||||
notification.getSpec().setRecipient(subscriber.name());
|
||||
notification.getSpec().setReason(reason.getMetadata().getName());
|
||||
notification.getSpec().setUnread(true);
|
||||
return client.create(notification);
|
||||
|
@ -223,7 +174,7 @@ public class DefaultNotificationCenter implements NotificationCenter {
|
|||
final var descriptorName = element.descriptor().getMetadata().getName();
|
||||
final var reason = element.reason();
|
||||
final var descriptor = element.descriptor();
|
||||
final var subscription = element.subscription();
|
||||
final var subscriber = element.subscriber();
|
||||
|
||||
var messagePayload = new NotificationContext.MessagePayload();
|
||||
messagePayload.setTitle(element.notificationTitle());
|
||||
|
@ -232,7 +183,7 @@ public class DefaultNotificationCenter implements NotificationCenter {
|
|||
messagePayload.setAttributes(element.reasonAttributes());
|
||||
|
||||
var message = new NotificationContext.Message();
|
||||
message.setRecipient(subscription.getSpec().getSubscriber().getName());
|
||||
message.setRecipient(subscriber.name());
|
||||
message.setPayload(messagePayload);
|
||||
message.setTimestamp(reason.getMetadata().getCreationTimestamp());
|
||||
var reasonSubject = reason.getSpec().getSubject();
|
||||
|
@ -270,25 +221,25 @@ public class DefaultNotificationCenter implements NotificationCenter {
|
|||
});
|
||||
}
|
||||
|
||||
Mono<NotificationContent> inferenceTemplate(Reason reason, Subscription subscription,
|
||||
Mono<NotificationContent> inferenceTemplate(Reason reason, Subscriber subscriber,
|
||||
Locale locale) {
|
||||
var reasonTypeName = reason.getSpec().getReasonType();
|
||||
var subscriber = subscription.getSpec().getSubscriber();
|
||||
return getReasonType(reasonTypeName)
|
||||
.flatMap(reasonType -> notificationTemplateSelector.select(reasonTypeName, locale)
|
||||
.flatMap(template -> {
|
||||
final var templateContent = template.getSpec().getTemplate();
|
||||
var model = toReasonAttributes(reason);
|
||||
var identity = UserIdentity.of(subscriber.getName());
|
||||
var subscriberInfo = new HashMap<>();
|
||||
if (identity.isAnonymous()) {
|
||||
subscriberInfo.put("displayName", identity.getEmail().orElse(""));
|
||||
if (subscriber.isAnonymous()) {
|
||||
subscriberInfo.put("displayName", subscriber.getEmail().orElseThrow());
|
||||
} else {
|
||||
subscriberInfo.put("displayName", "@" + identity.name());
|
||||
subscriberInfo.put("displayName", "@" + subscriber.username());
|
||||
}
|
||||
subscriberInfo.put("id", subscriber.getName());
|
||||
subscriberInfo.put("id", subscriber.name());
|
||||
model.put("subscriber", subscriberInfo);
|
||||
model.put("unsubscribeUrl", getUnsubscribeUrl(subscription));
|
||||
|
||||
var unsubscriptionMono = getUnsubscribeUrl(subscriber.subscriptionName())
|
||||
.doOnNext(url -> model.put("unsubscribeUrl", url));
|
||||
|
||||
var builder = NotificationContent.builder()
|
||||
.reasonType(reasonType)
|
||||
|
@ -305,7 +256,7 @@ public class DefaultNotificationCenter implements NotificationCenter {
|
|||
var htmlBodyMono = notificationTemplateRender
|
||||
.render(templateContent.getHtmlBody(), model)
|
||||
.doOnNext(builder::htmlBody);
|
||||
return Mono.when(titleMono, rawBodyMono, htmlBodyMono)
|
||||
return Mono.when(unsubscriptionMono, titleMono, rawBodyMono, htmlBodyMono)
|
||||
.then(Mono.fromSupplier(builder::build));
|
||||
})
|
||||
);
|
||||
|
@ -316,13 +267,14 @@ public class DefaultNotificationCenter implements NotificationCenter {
|
|||
ReasonAttributes reasonAttributes) {
|
||||
}
|
||||
|
||||
String getUnsubscribeUrl(Subscription subscription) {
|
||||
return subscriptionRouter.getUnsubscribeUrl(subscription);
|
||||
Mono<String> getUnsubscribeUrl(String subscriptionName) {
|
||||
return client.get(Subscription.class, subscriptionName)
|
||||
.map(subscriptionRouter::getUnsubscribeUrl);
|
||||
}
|
||||
|
||||
@Builder
|
||||
record NotificationElement(ReasonType reasonType, Reason reason,
|
||||
Subscription subscription, NotifierDescriptor descriptor,
|
||||
Subscriber subscriber, NotifierDescriptor descriptor,
|
||||
String notificationTitle,
|
||||
String notificationRawBody,
|
||||
String notificationHtmlBody,
|
||||
|
@ -333,80 +285,8 @@ public class DefaultNotificationCenter implements NotificationCenter {
|
|||
return client.get(ReasonType.class, reasonTypeName);
|
||||
}
|
||||
|
||||
Mono<Locale> getLocaleFromSubscriber(Subscription subscription) {
|
||||
Mono<Locale> getLocaleFromSubscriber(Subscriber subscriber) {
|
||||
// TODO get locale from subscriber
|
||||
return Mono.just(Locale.getDefault());
|
||||
}
|
||||
|
||||
Flux<Subscription> listObservers(String reasonTypeName,
|
||||
Subscription.ReasonSubject reasonSubject) {
|
||||
Assert.notNull(reasonTypeName, "The reasonTypeName must not be null");
|
||||
Assert.notNull(reasonSubject, "The reasonSubject must not be null");
|
||||
final var listOptions = new ListOptions();
|
||||
var fieldQuery = getSubscriptionFieldQuery(reasonTypeName, reasonSubject);
|
||||
listOptions.setFieldSelector(FieldSelector.of(fieldQuery));
|
||||
return distinctByKey(client.listAll(Subscription.class, listOptions, defaultSort()));
|
||||
}
|
||||
|
||||
private static Query getSubscriptionFieldQuery(String reasonTypeName,
|
||||
Subscription.ReasonSubject reasonSubject) {
|
||||
var matchAllSubject = new Subscription.ReasonSubject();
|
||||
matchAllSubject.setKind(reasonSubject.getKind());
|
||||
matchAllSubject.setApiVersion(reasonSubject.getApiVersion());
|
||||
return 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())
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
// 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() {
|
||||
return (a, b) -> {
|
||||
if (!a.getSpec().getSubscriber().equals(b.getSpec().getSubscriber())) {
|
||||
return false;
|
||||
}
|
||||
var reasonA = a.getSpec().getReason();
|
||||
var reasonB = b.getSpec().getReason();
|
||||
if (!reasonA.getReasonType().equals(reasonB.getReasonType())) {
|
||||
return false;
|
||||
}
|
||||
var ars = reasonA.getSubject();
|
||||
var brs = reasonB.getSubject();
|
||||
var gvkForA =
|
||||
GroupVersionKind.fromAPIVersionAndKind(ars.getApiVersion(), ars.getKind());
|
||||
var gvkForB =
|
||||
GroupVersionKind.fromAPIVersionAndKind(brs.getApiVersion(), brs.getKind());
|
||||
|
||||
if (!gvkForA.groupKind().equals(gvkForB.groupKind())) {
|
||||
return false;
|
||||
}
|
||||
// name is blank present match all
|
||||
if (StringUtils.isBlank(ars.getName()) || StringUtils.isBlank(brs.getName())) {
|
||||
return true;
|
||||
}
|
||||
return ars.getName().equals(brs.getName());
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,13 +21,12 @@ import run.halo.app.extension.ReactiveExtensionClient;
|
|||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class DefaultSubscriberEmailResolver implements SubscriberEmailResolver {
|
||||
private static final String SEPARATOR = "#";
|
||||
|
||||
private final ReactiveExtensionClient client;
|
||||
|
||||
@Override
|
||||
public Mono<String> resolve(Subscription.Subscriber subscriber) {
|
||||
if (isEmailSubscriber(subscriber)) {
|
||||
var identity = UserIdentity.of(subscriber.getName());
|
||||
if (identity.isAnonymous()) {
|
||||
return Mono.fromSupplier(() -> getEmail(subscriber));
|
||||
}
|
||||
return client.fetch(User.class, subscriber.getName())
|
||||
|
@ -44,20 +43,14 @@ public class DefaultSubscriberEmailResolver implements SubscriberEmailResolver {
|
|||
return subscriber;
|
||||
}
|
||||
|
||||
static boolean isEmailSubscriber(Subscription.Subscriber subscriber) {
|
||||
return UserIdentity.of(subscriber.getName()).isAnonymous();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
String getEmail(Subscription.Subscriber subscriber) {
|
||||
if (!isEmailSubscriber(subscriber)) {
|
||||
throw new IllegalStateException("The subscriber is not an email subscriber");
|
||||
var identity = UserIdentity.of(subscriber.getName());
|
||||
if (!identity.isAnonymous()) {
|
||||
throw new IllegalStateException("The subscriber is not an anonymous subscriber");
|
||||
}
|
||||
var subscriberName = subscriber.getName();
|
||||
String email = subscriberName.substring(subscriberName.indexOf(SEPARATOR) + 1);
|
||||
if (StringUtils.isBlank(email)) {
|
||||
throw new IllegalStateException("The subscriber does not have an email");
|
||||
}
|
||||
return email;
|
||||
return identity.getEmail()
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.orElseThrow(() -> new IllegalStateException("The subscriber does not have an email"));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
package run.halo.app.notification;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import run.halo.app.core.extension.notification.Reason;
|
||||
|
||||
public interface RecipientResolver {
|
||||
|
||||
Flux<Subscriber> resolve(Reason reason);
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
package run.halo.app.notification;
|
||||
|
||||
import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
|
||||
|
||||
import com.google.common.base.Throwables;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.BooleanUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.context.expression.MapAccessor;
|
||||
import org.springframework.core.convert.support.DefaultConversionService;
|
||||
import org.springframework.expression.EvaluationContext;
|
||||
import org.springframework.expression.EvaluationException;
|
||||
import org.springframework.expression.Expression;
|
||||
import org.springframework.expression.ExpressionParser;
|
||||
import org.springframework.expression.ParseException;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.expression.spel.support.DataBindingPropertyAccessor;
|
||||
import org.springframework.expression.spel.support.SimpleEvaluationContext;
|
||||
import org.springframework.integration.json.JsonPropertyAccessor;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.Assert;
|
||||
import reactor.core.publisher.Flux;
|
||||
import run.halo.app.core.extension.notification.Reason;
|
||||
import run.halo.app.core.extension.notification.Subscription;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class RecipientResolverImpl implements RecipientResolver {
|
||||
private final ExpressionParser expressionParser = new SpelExpressionParser();
|
||||
private final EvaluationContext evaluationContext = createEvaluationContext();
|
||||
private final SubscriptionService subscriptionService;
|
||||
|
||||
@Override
|
||||
public Flux<Subscriber> resolve(Reason reason) {
|
||||
var reasonType = reason.getSpec().getReasonType();
|
||||
return subscriptionService.listByPerPage(reasonType)
|
||||
.filter(this::isNotDisabled)
|
||||
.filter(subscription -> {
|
||||
var interestReason = subscription.getSpec().getReason();
|
||||
if (hasSubject(interestReason)) {
|
||||
return subjectMatch(subscription, reason.getSpec().getSubject());
|
||||
} else if (StringUtils.isNotBlank(interestReason.getExpression())) {
|
||||
return expressionMatch(subscription.getMetadata().getName(),
|
||||
interestReason.getExpression(), reason);
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.map(subscription -> {
|
||||
var id = UserIdentity.of(subscription.getSpec().getSubscriber().getName());
|
||||
return new Subscriber(id, subscription.getMetadata().getName());
|
||||
})
|
||||
.distinct(Subscriber::name);
|
||||
}
|
||||
|
||||
boolean hasSubject(Subscription.InterestReason interestReason) {
|
||||
return !Subscription.InterestReason.isFallbackSubject(interestReason.getSubject());
|
||||
}
|
||||
|
||||
boolean expressionMatch(String subscriptionName, String expressionStr, Reason reason) {
|
||||
try {
|
||||
Expression expression =
|
||||
expressionParser.parseExpression(expressionStr);
|
||||
var result = expression.getValue(evaluationContext,
|
||||
exprRootObject(reason),
|
||||
Boolean.class);
|
||||
return BooleanUtils.isTrue(result);
|
||||
} catch (ParseException | EvaluationException e) {
|
||||
log.debug("Failed to parse or evaluate expression for subscription [{}], skip it.",
|
||||
subscriptionName, Throwables.getRootCause(e));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Object> exprRootObject(Reason reason) {
|
||||
var map = new HashMap<String, Object>(3, 1);
|
||||
map.put("props", defaultIfNull(reason.getSpec().getAttributes(), new ReasonAttributes()));
|
||||
map.put("subject", reason.getSpec().getSubject());
|
||||
map.put("author", reason.getSpec().getAuthor());
|
||||
return Collections.unmodifiableMap(map);
|
||||
}
|
||||
|
||||
static boolean subjectMatch(Subscription subscription, Reason.Subject reasonSubject) {
|
||||
Assert.notNull(subscription, "The subscription must not be null");
|
||||
Assert.notNull(reasonSubject, "The reasonSubject must not be null");
|
||||
final var sourceSubject = subscription.getSpec().getReason().getSubject();
|
||||
|
||||
var matchSubject = new Subscription.ReasonSubject();
|
||||
matchSubject.setKind(reasonSubject.getKind());
|
||||
matchSubject.setApiVersion(reasonSubject.getApiVersion());
|
||||
|
||||
if (StringUtils.isBlank(sourceSubject.getName())) {
|
||||
return sourceSubject.equals(matchSubject);
|
||||
}
|
||||
matchSubject.setName(reasonSubject.getName());
|
||||
return sourceSubject.equals(matchSubject);
|
||||
}
|
||||
|
||||
boolean isNotDisabled(Subscription subscription) {
|
||||
return !subscription.getSpec().isDisabled();
|
||||
}
|
||||
|
||||
EvaluationContext createEvaluationContext() {
|
||||
return SimpleEvaluationContext.forPropertyAccessors(
|
||||
DataBindingPropertyAccessor.forReadOnlyAccess(),
|
||||
new MapAccessor(),
|
||||
new JsonPropertyAccessor()
|
||||
)
|
||||
.withConversionService(DefaultConversionService.getSharedInstance())
|
||||
.build();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package run.halo.app.notification;
|
||||
|
||||
import java.util.Optional;
|
||||
import org.springframework.util.Assert;
|
||||
import run.halo.app.infra.AnonymousUserConst;
|
||||
|
||||
public record Subscriber(UserIdentity identity, String subscriptionName) {
|
||||
public Subscriber {
|
||||
Assert.notNull(identity, "The subscriber must not be null");
|
||||
Assert.hasText(subscriptionName, "The subscription name must not be blank");
|
||||
}
|
||||
|
||||
public String name() {
|
||||
return identity.name();
|
||||
}
|
||||
|
||||
public String username() {
|
||||
return identity.isAnonymous() ? AnonymousUserConst.PRINCIPAL : identity.name();
|
||||
}
|
||||
|
||||
public boolean isAnonymous() {
|
||||
return identity.isAnonymous();
|
||||
}
|
||||
|
||||
public Optional<String> getEmail() {
|
||||
return identity.getEmail();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,155 @@
|
|||
package run.halo.app.notification;
|
||||
|
||||
import static run.halo.app.content.NotificationReasonConst.NEW_COMMENT_ON_PAGE;
|
||||
import static run.halo.app.content.NotificationReasonConst.NEW_COMMENT_ON_POST;
|
||||
import static run.halo.app.content.NotificationReasonConst.SOMEONE_REPLIED_TO_YOU;
|
||||
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.in;
|
||||
import static run.halo.app.extension.index.query.QueryFactory.isNull;
|
||||
import static run.halo.app.extension.index.query.QueryFactory.startsWith;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.context.event.ApplicationStartedEvent;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Component;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.extension.User;
|
||||
import run.halo.app.core.extension.notification.Subscription;
|
||||
import run.halo.app.extension.ListOptions;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.extension.router.selector.FieldSelector;
|
||||
import run.halo.app.infra.AnonymousUserConst;
|
||||
import run.halo.app.infra.ReactiveExtensionPaginatedOperator;
|
||||
import run.halo.app.infra.ReactiveExtensionPaginatedOperatorImpl;
|
||||
|
||||
/**
|
||||
* Subscription migration to adapt to the new expression subscribe mechanism.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.15.0
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class SubscriptionMigration implements ApplicationListener<ApplicationStartedEvent> {
|
||||
private final NotificationCenter notificationCenter;
|
||||
private final ReactiveExtensionClient client;
|
||||
private final SubscriptionService subscriptionService;
|
||||
private final ReactiveExtensionPaginatedOperator paginatedOperator;
|
||||
|
||||
@Override
|
||||
@Async
|
||||
public void onApplicationEvent(@NonNull ApplicationStartedEvent event) {
|
||||
handleAnonymousSubscription();
|
||||
cleanupUserSubscription();
|
||||
}
|
||||
|
||||
private void cleanupUserSubscription() {
|
||||
var listOptions = new ListOptions();
|
||||
var query = isNull("metadata.deletionTimestamp");
|
||||
listOptions.setFieldSelector(FieldSelector.of(query));
|
||||
var iterator =
|
||||
new ReactiveExtensionPaginatedOperatorImpl(client);
|
||||
iterator.list(User.class, listOptions)
|
||||
.map(user -> user.getMetadata().getName())
|
||||
.flatMap(this::removeInternalSubscriptionForUser)
|
||||
.then()
|
||||
.doOnSuccess(unused -> log.info("Cleanup user subscription completed"))
|
||||
.block();
|
||||
}
|
||||
|
||||
private void handleAnonymousSubscription() {
|
||||
log.debug("Start to collating anonymous subscription...");
|
||||
Set<String> anonymousSubscribers = new HashSet<>();
|
||||
deleteAnonymousSubscription(subscription -> {
|
||||
var name = subscription.getSpec().getSubscriber().getName();
|
||||
anonymousSubscribers.add(name);
|
||||
}).block();
|
||||
if (anonymousSubscribers.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// anonymous only subscribe some-one-replied-to-you reason
|
||||
for (String anonymousSubscriber : anonymousSubscribers) {
|
||||
createSubscription(anonymousSubscriber,
|
||||
SOMEONE_REPLIED_TO_YOU,
|
||||
"props.repliedOwner == '%s'".formatted(anonymousSubscriber)).block();
|
||||
}
|
||||
log.info("Collating anonymous subscription completed.");
|
||||
}
|
||||
|
||||
private Mono<Void> deleteAnonymousSubscription(Consumer<Subscription> consumer) {
|
||||
var listOptions = new ListOptions();
|
||||
var query = and(startsWith("spec.subscriber", AnonymousUserConst.PRINCIPAL),
|
||||
isNull("spec.reason.expression"),
|
||||
isNull("metadata.deletionTimestamp"),
|
||||
in("spec.reason.reasonType", Set.of(NEW_COMMENT_ON_POST,
|
||||
NEW_COMMENT_ON_PAGE,
|
||||
SOMEONE_REPLIED_TO_YOU))
|
||||
);
|
||||
listOptions.setFieldSelector(FieldSelector.of(query));
|
||||
return paginatedOperator.deleteInitialBatch(Subscription.class, listOptions)
|
||||
.doOnNext(consumer)
|
||||
.doOnNext(subscription -> log.debug("Deleted anonymous subscription: {}",
|
||||
subscription.getMetadata().getName())
|
||||
)
|
||||
.then();
|
||||
}
|
||||
|
||||
private Mono<Void> removeInternalSubscriptionForUser(String username) {
|
||||
log.debug("Start to collating internal subscription for user: {}", username);
|
||||
var subscriber = new Subscription.Subscriber();
|
||||
subscriber.setName(username);
|
||||
|
||||
var listOptions = new ListOptions();
|
||||
var fieldQuery = and(isNull("metadata.deletionTimestamp"),
|
||||
equal("spec.subscriber", subscriber.toString()),
|
||||
in("spec.reason.reasonType", Set.of(
|
||||
NEW_COMMENT_ON_POST,
|
||||
NEW_COMMENT_ON_PAGE,
|
||||
SOMEONE_REPLIED_TO_YOU
|
||||
))
|
||||
);
|
||||
listOptions.setFieldSelector(FieldSelector.of(fieldQuery));
|
||||
|
||||
return subscriptionService.removeBy(listOptions)
|
||||
.map(subscription -> {
|
||||
var name = subscription.getSpec().getSubscriber().getName();
|
||||
var reason = subscription.getSpec().getReason();
|
||||
String expression = switch (reason.getReasonType()) {
|
||||
case NEW_COMMENT_ON_POST -> "props.postOwner == '%s'".formatted(name);
|
||||
case NEW_COMMENT_ON_PAGE -> "props.pageOwner == '%s'".formatted(name);
|
||||
case SOMEONE_REPLIED_TO_YOU -> "props.repliedOwner == '%s'".formatted(name);
|
||||
// never happen
|
||||
default -> null;
|
||||
};
|
||||
return new SubscriptionSummary(name, reason.getReasonType(), expression);
|
||||
})
|
||||
.distinct()
|
||||
.flatMap(summary -> createSubscription(summary.subscriber(), summary.reasonType(),
|
||||
summary.expression()))
|
||||
.then()
|
||||
.doOnSuccess(unused ->
|
||||
log.debug("Collating internal subscription for user: {} completed", username));
|
||||
}
|
||||
|
||||
Mono<Void> createSubscription(String name, String reasonType, String expression) {
|
||||
var interestReason = new Subscription.InterestReason();
|
||||
interestReason.setReasonType(reasonType);
|
||||
interestReason.setExpression(expression);
|
||||
var subscriber = new Subscription.Subscriber();
|
||||
subscriber.setName(name);
|
||||
log.debug("Create subscription for user: {} with reasonType: {}", name, reasonType);
|
||||
return notificationCenter.subscribe(subscriber, interestReason).then();
|
||||
}
|
||||
|
||||
record SubscriptionSummary(String subscriber, String reasonType, String expression) {
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package run.halo.app.notification;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.extension.notification.Subscription;
|
||||
import run.halo.app.extension.ListOptions;
|
||||
|
||||
public interface SubscriptionService {
|
||||
|
||||
/**
|
||||
* <p>List subscriptions by page one by one.only consume one page then next page will be
|
||||
* loaded.</p>
|
||||
* <p>Note that: result can not be used to delete the subscription, it is only used to query the
|
||||
* subscription.</p>
|
||||
*/
|
||||
Flux<Subscription> listByPerPage(String reasonType);
|
||||
|
||||
Mono<Void> remove(Subscription.Subscriber subscriber,
|
||||
Subscription.InterestReason interestReasons);
|
||||
|
||||
Mono<Void> remove(Subscription.Subscriber subscriber);
|
||||
|
||||
Mono<Subscription> remove(Subscription subscription);
|
||||
|
||||
Flux<Subscription> removeBy(ListOptions listOptions);
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
package run.halo.app.notification;
|
||||
|
||||
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.startsWith;
|
||||
|
||||
import java.time.Duration;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.dao.OptimisticLockingFailureException;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.Assert;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.util.retry.Retry;
|
||||
import run.halo.app.core.extension.notification.Subscription;
|
||||
import run.halo.app.extension.ListOptions;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.extension.index.query.Query;
|
||||
import run.halo.app.extension.router.selector.FieldSelector;
|
||||
import run.halo.app.infra.ReactiveExtensionPaginatedOperator;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class SubscriptionServiceImpl implements SubscriptionService {
|
||||
private final ReactiveExtensionClient client;
|
||||
private final ReactiveExtensionPaginatedOperator paginatedOperator;
|
||||
|
||||
@Override
|
||||
public Mono<Void> remove(Subscription.Subscriber subscriber,
|
||||
Subscription.InterestReason interestReason) {
|
||||
Assert.notNull(subscriber, "The subscriber must not be null");
|
||||
Assert.notNull(interestReason, "The interest reason must not be null");
|
||||
var reasonType = interestReason.getReasonType();
|
||||
var expression = interestReason.getExpression();
|
||||
var subject = interestReason.getSubject();
|
||||
|
||||
var listOptions = new ListOptions();
|
||||
var fieldQuery = and(isNull("metadata.deletionTimestamp"),
|
||||
equal("spec.subscriber", subscriber.toString()),
|
||||
equal("spec.reason.reasonType", reasonType));
|
||||
|
||||
if (subject != null) {
|
||||
fieldQuery = and(fieldQuery, reasonSubjectMatch(subject));
|
||||
}
|
||||
if (StringUtils.isNotBlank(expression)) {
|
||||
fieldQuery = and(fieldQuery, equal("spec.reason.expression", expression));
|
||||
}
|
||||
listOptions.setFieldSelector(FieldSelector.of(fieldQuery));
|
||||
return paginatedOperator.deleteInitialBatch(Subscription.class, listOptions).then();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> remove(Subscription.Subscriber subscriber) {
|
||||
var listOptions = new ListOptions();
|
||||
var fieldQuery = and(isNull("metadata.deletionTimestamp"),
|
||||
equal("spec.subscriber", subscriber.toString()));
|
||||
listOptions.setFieldSelector(FieldSelector.of(fieldQuery));
|
||||
return paginatedOperator.deleteInitialBatch(Subscription.class, listOptions)
|
||||
.then();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Subscription> remove(Subscription subscription) {
|
||||
return client.delete(subscription)
|
||||
.onErrorResume(OptimisticLockingFailureException.class,
|
||||
e -> attemptToDelete(subscription.getMetadata().getName()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<Subscription> removeBy(ListOptions listOptions) {
|
||||
return paginatedOperator.deleteInitialBatch(Subscription.class, listOptions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<Subscription> listByPerPage(String reasonType) {
|
||||
final var listOptions = new ListOptions();
|
||||
var fieldQuery = and(isNull("metadata.deletionTimestamp"),
|
||||
equal("spec.reason.reasonType", reasonType));
|
||||
listOptions.setFieldSelector(FieldSelector.of(fieldQuery));
|
||||
return paginatedOperator.list(Subscription.class, listOptions);
|
||||
}
|
||||
|
||||
private Mono<Subscription> attemptToDelete(String subscriptionName) {
|
||||
return Mono.defer(() -> client.fetch(Subscription.class, subscriptionName)
|
||||
.flatMap(client::delete)
|
||||
)
|
||||
.retryWhen(Retry.backoff(8, Duration.ofMillis(100))
|
||||
.filter(OptimisticLockingFailureException.class::isInstance));
|
||||
}
|
||||
|
||||
Query reasonSubjectMatch(Subscription.ReasonSubject reasonSubject) {
|
||||
Assert.notNull(reasonSubject, "The reasonSubject must not be null");
|
||||
if (StringUtils.isNotBlank(reasonSubject.getName())) {
|
||||
return equal("spec.reason.subject", reasonSubject.toString());
|
||||
}
|
||||
var matchAllSubject = new Subscription.ReasonSubject();
|
||||
matchAllSubject.setKind(reasonSubject.getKind());
|
||||
matchAllSubject.setApiVersion(reasonSubject.getApiVersion());
|
||||
return startsWith("spec.reason.subject", matchAllSubject.toString());
|
||||
}
|
||||
}
|
|
@ -2,7 +2,11 @@ package run.halo.app.plugin;
|
|||
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Stack;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.pf4j.CompoundPluginLoader;
|
||||
|
@ -18,6 +22,7 @@ import org.pf4j.PluginDescriptorFinder;
|
|||
import org.pf4j.PluginFactory;
|
||||
import org.pf4j.PluginLoader;
|
||||
import org.pf4j.PluginRepository;
|
||||
import org.pf4j.PluginRuntimeException;
|
||||
import org.pf4j.PluginStatusProvider;
|
||||
import org.pf4j.PluginWrapper;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
|
@ -157,4 +162,50 @@ public class HaloPluginManager extends DefaultPluginManager implements SpringPlu
|
|||
return sharedContext.get();
|
||||
}
|
||||
|
||||
public boolean reloadPlugin(String pluginId, Path loadLocation) {
|
||||
log.info("Reloading plugin {} from {}", pluginId, loadLocation);
|
||||
|
||||
var dependents = getDependents(pluginId);
|
||||
dependents.forEach(plugin -> unloadPlugin(plugin.getPluginId(), false));
|
||||
|
||||
var unloaded = unloadPlugin(pluginId, false);
|
||||
if (!unloaded) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// load the root plugin
|
||||
var loadedPluginId = loadPlugin(loadLocation);
|
||||
|
||||
if (!Objects.equals(pluginId, loadedPluginId)) {
|
||||
throw new PluginRuntimeException("""
|
||||
The plugin {} is reloaded successfully, \
|
||||
but the plugin id is different from the original one.
|
||||
""");
|
||||
}
|
||||
|
||||
// load all dependents with reverse order
|
||||
dependents.stream()
|
||||
.map(PluginWrapper::getPluginPath)
|
||||
.sorted(Comparator.reverseOrder())
|
||||
.forEach(this::loadPlugin);
|
||||
|
||||
log.info("Reloaded plugin {} from {}", loadedPluginId, loadLocation);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PluginWrapper> getDependents(String pluginId) {
|
||||
var dependents = new ArrayList<PluginWrapper>();
|
||||
var stack = new Stack<String>();
|
||||
dependencyResolver.getDependents(pluginId).forEach(stack::push);
|
||||
while (!stack.isEmpty()) {
|
||||
var dependent = stack.pop();
|
||||
var pluginWrapper = getPlugin(dependent);
|
||||
if (pluginWrapper != null) {
|
||||
dependents.add(pluginWrapper);
|
||||
dependencyResolver.getDependents(dependent).forEach(stack::push);
|
||||
}
|
||||
}
|
||||
return dependents;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
package run.halo.app.plugin;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import org.pf4j.PluginManager;
|
||||
import org.pf4j.PluginWrapper;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
|
||||
public interface SpringPluginManager extends PluginManager {
|
||||
|
@ -9,4 +12,21 @@ public interface SpringPluginManager extends PluginManager {
|
|||
|
||||
ApplicationContext getSharedContext();
|
||||
|
||||
/**
|
||||
* Reload the plugin and the plugins that depend on it.
|
||||
*
|
||||
* @param pluginId plugin id
|
||||
* @param loadLocation new load location
|
||||
* @return true if reload successfully, otherwise false
|
||||
*/
|
||||
boolean reloadPlugin(String pluginId, Path loadLocation);
|
||||
|
||||
/**
|
||||
* Get all dependents recursively.
|
||||
*
|
||||
* @param pluginId plugin id
|
||||
* @return a list of plugin wrapper. The order of the list is from the farthest dependent to
|
||||
* the nearest dependent.
|
||||
*/
|
||||
List<PluginWrapper> getDependents(String pluginId);
|
||||
}
|
||||
|
|
|
@ -52,6 +52,7 @@ problemDetail.user.email.verify.maxAttempts=Too many verification attempts, plea
|
|||
problemDetail.user.password.unsatisfied=The password does not meet the specifications.
|
||||
problemDetail.user.username.unsatisfied=The username does not meet the specifications.
|
||||
problemDetail.user.oldPassword.notMatch=The old password does not match.
|
||||
problemDetail.user.password.notMatch=The password does not match.
|
||||
problemDetail.user.signUpFailed.disallowed=System does not allow new users to register.
|
||||
problemDetail.user.duplicateName=The username {0} already exists, please rename it and retry.
|
||||
problemDetail.comment.turnedOff=The comment function has been turned off.
|
||||
|
|
|
@ -29,6 +29,7 @@ problemDetail.user.email.verify.maxAttempts=尝试次数过多,请稍候再试
|
|||
problemDetail.user.password.unsatisfied=密码不符合规范。
|
||||
problemDetail.user.username.unsatisfied=用户名不符合规范。
|
||||
problemDetail.user.oldPassword.notMatch=旧密码不匹配。
|
||||
problemDetail.user.password.notMatch=密码不匹配。
|
||||
problemDetail.user.signUpFailed.disallowed=系统不允许注册新用户。
|
||||
problemDetail.user.duplicateName=用户名 {0} 已存在,请更换用户名后重试。
|
||||
problemDetail.plugin.version.unsatisfied.requires=插件要求一个最小的系统版本为 {0}, 但当前版本为 {1}。
|
||||
|
|
|
@ -79,6 +79,9 @@ spec:
|
|||
- name: postName
|
||||
type: string
|
||||
description: "The name of the post."
|
||||
- name: postOwner
|
||||
type: string
|
||||
description: "The user name of the post owner."
|
||||
- name: postTitle
|
||||
type: string
|
||||
- name: postUrl
|
||||
|
@ -107,6 +110,9 @@ spec:
|
|||
- name: pageName
|
||||
type: string
|
||||
description: "The name of the single page."
|
||||
- name: pageOwner
|
||||
type: string
|
||||
description: "The user name of the page owner."
|
||||
- name: pageTitle
|
||||
type: string
|
||||
- name: pageUrl
|
||||
|
@ -144,6 +150,12 @@ spec:
|
|||
type: boolean
|
||||
- name: commentContent
|
||||
type: string
|
||||
- name: repliedOwner
|
||||
type: string
|
||||
description: "The owner of the comment or reply that has been replied to."
|
||||
- name: replyOwner
|
||||
type: string
|
||||
description: "The user who created the current reply."
|
||||
- name: replier
|
||||
type: string
|
||||
description: "The display name of the replier."
|
||||
|
|
|
@ -16,7 +16,7 @@ rules:
|
|||
resources: [ "posts" ]
|
||||
verbs: [ "*" ]
|
||||
- apiGroups: [ "api.console.halo.run" ]
|
||||
resources: [ "posts", "posts/publish", "posts/unpublish", "posts/recycle", "posts/content", "indices/post" ]
|
||||
resources: [ "posts", "posts/publish", "posts/unpublish", "posts/recycle", "posts/content", "indices/post", "posts/revert-content" ]
|
||||
verbs: [ "create", "patch", "update", "delete", "deletecollection" ]
|
||||
---
|
||||
apiVersion: v1alpha1
|
||||
|
@ -37,5 +37,5 @@ rules:
|
|||
resources: [ "posts" ]
|
||||
verbs: [ "get", "list" ]
|
||||
- apiGroups: [ "api.console.halo.run" ]
|
||||
resources: [ "posts", "posts/head-content", "posts/release-content" ]
|
||||
resources: [ "posts", "posts/head-content", "posts/release-content", "posts/snapshot", "posts/content" ]
|
||||
verbs: [ "get", "list" ]
|
||||
|
|
|
@ -15,7 +15,7 @@ rules:
|
|||
resources: [ "singlepages" ]
|
||||
verbs: [ "*" ]
|
||||
- apiGroups: [ "api.console.halo.run" ]
|
||||
resources: [ "singlepages", "singlepages/publish", "singlepages/content" ]
|
||||
resources: [ "singlepages", "singlepages/publish", "singlepages/content", "singlepages/revert-content" ]
|
||||
verbs: [ "create", "patch", "update", "delete", "deletecollection" ]
|
||||
---
|
||||
apiVersion: v1alpha1
|
||||
|
@ -35,5 +35,5 @@ rules:
|
|||
resources: [ "singlepages" ]
|
||||
verbs: [ "get", "list" ]
|
||||
- apiGroups: [ "api.console.halo.run" ]
|
||||
resources: [ "singlepages", "singlepages/head-content", "singlepages/release-content" ]
|
||||
resources: [ "singlepages", "singlepages/head-content", "singlepages/release-content", "singlepages/snapshot", "singlepages/content" ]
|
||||
verbs: [ "get", "list" ]
|
||||
|
|
|
@ -23,3 +23,6 @@ rules:
|
|||
- apiGroups: [ "content.halo.run" ]
|
||||
resources: [ "tags" ]
|
||||
verbs: [ "get", "list" ]
|
||||
- apiGroups: [ "api.console.halo.run" ]
|
||||
resources: [ "tags" ]
|
||||
verbs: [ "get", "list" ]
|
||||
|
|
|
@ -24,7 +24,7 @@ class ContentRequestTest {
|
|||
ref.setKind(Post.KIND);
|
||||
ref.setGroup("content.halo.run");
|
||||
ref.setName("test-post");
|
||||
contentRequest = new ContentRequest(ref, "snapshot-1", """
|
||||
contentRequest = new ContentRequest(ref, "snapshot-1", null, """
|
||||
Four score and seven
|
||||
years ago our fathers
|
||||
|
||||
|
|
|
@ -454,21 +454,6 @@ class CommentNotificationReasonPublisherTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void identityFromTest() {
|
||||
var owner = new Comment.CommentOwner();
|
||||
owner.setKind(User.KIND);
|
||||
owner.setName("fake-user");
|
||||
|
||||
assertThat(CommentNotificationReasonPublisher.identityFrom(owner))
|
||||
.isEqualTo(UserIdentity.of(owner.getName()));
|
||||
|
||||
owner.setKind(Comment.CommentOwner.KIND_EMAIL);
|
||||
owner.setName("example@example.com");
|
||||
assertThat(CommentNotificationReasonPublisher.identityFrom(owner))
|
||||
.isEqualTo(UserIdentity.anonymousWithEmail(owner.getName()));
|
||||
}
|
||||
|
||||
static Comment createComment() {
|
||||
var comment = new Comment();
|
||||
comment.setMetadata(new Metadata());
|
||||
|
|
|
@ -0,0 +1,159 @@
|
|||
package run.halo.app.content.comment;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.mock.mockito.SpyBean;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
import run.halo.app.core.extension.content.Comment;
|
||||
import run.halo.app.extension.Extension;
|
||||
import run.halo.app.extension.ExtensionStoreUtil;
|
||||
import run.halo.app.extension.GroupVersionKind;
|
||||
import run.halo.app.extension.PageRequestImpl;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.extension.Ref;
|
||||
import run.halo.app.extension.SchemeManager;
|
||||
import run.halo.app.extension.index.IndexerFactory;
|
||||
import run.halo.app.extension.store.ReactiveExtensionStoreClient;
|
||||
import run.halo.app.infra.utils.JsonUtils;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link CommentServiceImpl}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.15.0
|
||||
*/
|
||||
class CommentServiceImplIntegrationTest {
|
||||
|
||||
@Nested
|
||||
@DirtiesContext
|
||||
@SpringBootTest
|
||||
class CommentRemoveTest {
|
||||
private final List<Comment> storedComments = createComments(350);
|
||||
|
||||
@Autowired
|
||||
private SchemeManager schemeManager;
|
||||
|
||||
@SpyBean
|
||||
private ReactiveExtensionClient reactiveClient;
|
||||
|
||||
@Autowired
|
||||
private ReactiveExtensionStoreClient storeClient;
|
||||
|
||||
@Autowired
|
||||
private IndexerFactory indexerFactory;
|
||||
|
||||
@SpyBean
|
||||
private CommentServiceImpl commentService;
|
||||
|
||||
Mono<Extension> deleteImmediately(Extension extension) {
|
||||
var name = extension.getMetadata().getName();
|
||||
var scheme = schemeManager.get(extension.getClass());
|
||||
// un-index
|
||||
var indexer = indexerFactory.getIndexer(extension.groupVersionKind());
|
||||
indexer.unIndexRecord(extension.getMetadata().getName());
|
||||
|
||||
// delete from db
|
||||
var storeName = ExtensionStoreUtil.buildStoreName(scheme, name);
|
||||
return storeClient.delete(storeName, extension.getMetadata().getVersion())
|
||||
.thenReturn(extension);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
Flux.fromIterable(storedComments)
|
||||
.flatMap(post -> reactiveClient.create(post))
|
||||
.as(StepVerifier::create)
|
||||
.expectNextCount(storedComments.size())
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
Flux.fromIterable(storedComments)
|
||||
.flatMap(this::deleteImmediately)
|
||||
.as(StepVerifier::create)
|
||||
.expectNextCount(storedComments.size())
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void commentBatchDeletionTest() {
|
||||
Ref ref = Ref.of("67",
|
||||
GroupVersionKind.fromAPIVersionAndKind("content.halo.run/v1alpha1", "SinglePage"));
|
||||
commentService.removeBySubject(ref)
|
||||
.as(StepVerifier::create)
|
||||
.verifyComplete();
|
||||
|
||||
verify(reactiveClient, times(storedComments.size())).delete(any(Comment.class));
|
||||
verify(commentService, times(2)).listCommentsByRef(eq(ref), any());
|
||||
|
||||
commentService.listCommentsByRef(ref, PageRequestImpl.ofSize(1))
|
||||
.as(StepVerifier::create)
|
||||
.consumeNextWith(result -> {
|
||||
assertThat(result.getTotal()).isEqualTo(0);
|
||||
})
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
List<Comment> createComments(int size) {
|
||||
List<Comment> comments = new ArrayList<>(size);
|
||||
for (int i = 0; i < size; i++) {
|
||||
var comment = createComment();
|
||||
comment.getMetadata().setName("comment-" + i);
|
||||
comments.add(comment);
|
||||
}
|
||||
return comments;
|
||||
}
|
||||
}
|
||||
|
||||
Comment createComment() {
|
||||
return JsonUtils.jsonToObject("""
|
||||
{
|
||||
"spec": {
|
||||
"raw": "fake-raw",
|
||||
"content": "fake-content",
|
||||
"owner": {
|
||||
"kind": "User",
|
||||
"name": "fake-user"
|
||||
},
|
||||
"userAgent": "",
|
||||
"ipAddress": "",
|
||||
"approvedTime": "2024-02-28T09:15:16.095Z",
|
||||
"creationTime": "2024-02-28T06:23:42.923294424Z",
|
||||
"priority": 0,
|
||||
"top": false,
|
||||
"allowNotification": false,
|
||||
"approved": true,
|
||||
"hidden": false,
|
||||
"subjectRef": {
|
||||
"group": "content.halo.run",
|
||||
"version": "v1alpha1",
|
||||
"kind": "SinglePage",
|
||||
"name": "67"
|
||||
},
|
||||
"lastReadTime": "2024-02-29T03:39:04.230Z"
|
||||
},
|
||||
"apiVersion": "content.halo.run/v1alpha1",
|
||||
"kind": "Comment",
|
||||
"metadata": {
|
||||
"generateName": "comment-"
|
||||
}
|
||||
}
|
||||
""", Comment.class);
|
||||
}
|
||||
}
|
|
@ -7,6 +7,7 @@ import static org.mockito.Mockito.doNothing;
|
|||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static run.halo.app.content.comment.ReplyNotificationSubscriptionHelper.identityFrom;
|
||||
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
@ -24,9 +25,8 @@ import run.halo.app.core.extension.notification.Subscription;
|
|||
import run.halo.app.extension.GroupVersionKind;
|
||||
import run.halo.app.extension.Metadata;
|
||||
import run.halo.app.extension.Ref;
|
||||
import run.halo.app.infra.AnonymousUserConst;
|
||||
import run.halo.app.notification.NotificationCenter;
|
||||
import run.halo.app.notification.SubscriberEmailResolver;
|
||||
import run.halo.app.notification.UserIdentity;
|
||||
|
||||
/**
|
||||
* Tests for {@link ReplyNotificationSubscriptionHelper}.
|
||||
|
@ -40,9 +40,6 @@ class ReplyNotificationSubscriptionHelperTest {
|
|||
@Mock
|
||||
NotificationCenter notificationCenter;
|
||||
|
||||
@Mock
|
||||
SubscriberEmailResolver subscriberEmailResolver;
|
||||
|
||||
@InjectMocks
|
||||
ReplyNotificationSubscriptionHelper notificationSubscriptionHelper;
|
||||
|
||||
|
@ -51,17 +48,12 @@ class ReplyNotificationSubscriptionHelperTest {
|
|||
var comment = createComment();
|
||||
var spyNotificationSubscriptionHelper = spy(notificationSubscriptionHelper);
|
||||
|
||||
doNothing().when(spyNotificationSubscriptionHelper).subscribeReply(any(), any());
|
||||
doNothing().when(spyNotificationSubscriptionHelper).subscribeReply(any(UserIdentity.class));
|
||||
|
||||
spyNotificationSubscriptionHelper.subscribeNewReplyReasonForComment(comment);
|
||||
|
||||
var reasonSubject = Subscription.ReasonSubject.builder()
|
||||
.apiVersion(comment.getApiVersion())
|
||||
.kind(comment.getKind())
|
||||
.name(comment.getMetadata().getName())
|
||||
.build();
|
||||
verify(spyNotificationSubscriptionHelper).subscribeReply(eq(reasonSubject),
|
||||
eq(ReplyNotificationSubscriptionHelper.Identity.fromCommentOwner(
|
||||
verify(spyNotificationSubscriptionHelper).subscribeReply(
|
||||
eq(ReplyNotificationSubscriptionHelper.identityFrom(
|
||||
comment.getSpec().getOwner()))
|
||||
);
|
||||
}
|
||||
|
@ -80,17 +72,12 @@ class ReplyNotificationSubscriptionHelperTest {
|
|||
|
||||
var spyNotificationSubscriptionHelper = spy(notificationSubscriptionHelper);
|
||||
|
||||
doNothing().when(spyNotificationSubscriptionHelper).subscribeReply(any(), any());
|
||||
doNothing().when(spyNotificationSubscriptionHelper).subscribeReply(any(UserIdentity.class));
|
||||
|
||||
spyNotificationSubscriptionHelper.subscribeNewReplyReasonForReply(reply);
|
||||
|
||||
var reasonSubject = Subscription.ReasonSubject.builder()
|
||||
.apiVersion(reply.getApiVersion())
|
||||
.kind(reply.getKind())
|
||||
.name(reply.getMetadata().getName())
|
||||
.build();
|
||||
verify(spyNotificationSubscriptionHelper).subscribeReply(eq(reasonSubject),
|
||||
eq(ReplyNotificationSubscriptionHelper.Identity.fromCommentOwner(
|
||||
verify(spyNotificationSubscriptionHelper).subscribeReply(
|
||||
eq(ReplyNotificationSubscriptionHelper.identityFrom(
|
||||
reply.getSpec().getOwner()))
|
||||
);
|
||||
}
|
||||
|
@ -98,48 +85,38 @@ class ReplyNotificationSubscriptionHelperTest {
|
|||
@Test
|
||||
void subscribeReplyTest() {
|
||||
var comment = createComment();
|
||||
var reasonSubject = Subscription.ReasonSubject.builder()
|
||||
.apiVersion(comment.getApiVersion())
|
||||
.kind(comment.getKind())
|
||||
.name(comment.getMetadata().getName())
|
||||
.build();
|
||||
var identity = ReplyNotificationSubscriptionHelper.Identity.fromCommentOwner(
|
||||
var identity = ReplyNotificationSubscriptionHelper.identityFrom(
|
||||
comment.getSpec().getOwner());
|
||||
|
||||
when(notificationCenter.subscribe(any(), any())).thenReturn(Mono.empty());
|
||||
|
||||
var subscriber = new Subscription.Subscriber();
|
||||
subscriber.setName(AnonymousUserConst.PRINCIPAL + "#" + identity.name());
|
||||
when(subscriberEmailResolver.ofEmail(eq(identity.name())))
|
||||
.thenReturn(subscriber);
|
||||
subscriber.setName(identity.name());
|
||||
|
||||
notificationSubscriptionHelper.subscribeReply(reasonSubject, identity);
|
||||
notificationSubscriptionHelper.subscribeReply(identity);
|
||||
|
||||
var interestReason = new Subscription.InterestReason();
|
||||
interestReason.setReasonType(NotificationReasonConst.SOMEONE_REPLIED_TO_YOU);
|
||||
interestReason.setSubject(reasonSubject);
|
||||
interestReason.setExpression("props.repliedOwner == '%s'".formatted(subscriber.getName()));
|
||||
verify(notificationCenter).subscribe(eq(subscriber), eq(interestReason));
|
||||
verify(subscriberEmailResolver).ofEmail(eq(identity.name()));
|
||||
}
|
||||
|
||||
@Nested
|
||||
class IdentityTest {
|
||||
|
||||
@Test
|
||||
void createForCommentOwner() {
|
||||
var commentOwner = new Comment.CommentOwner();
|
||||
commentOwner.setKind(Comment.CommentOwner.KIND_EMAIL);
|
||||
commentOwner.setName("example@example.com");
|
||||
void identityFromTest() {
|
||||
var owner = new Comment.CommentOwner();
|
||||
owner.setKind(User.KIND);
|
||||
owner.setName("fake-user");
|
||||
|
||||
var sub = ReplyNotificationSubscriptionHelper.Identity.fromCommentOwner(commentOwner);
|
||||
assertThat(sub.isEmail()).isTrue();
|
||||
assertThat(sub.name()).isEqualTo(commentOwner.getName());
|
||||
assertThat(identityFrom(owner))
|
||||
.isEqualTo(UserIdentity.of(owner.getName()));
|
||||
|
||||
commentOwner.setKind(User.KIND);
|
||||
commentOwner.setName("fake-user");
|
||||
sub = ReplyNotificationSubscriptionHelper.Identity.fromCommentOwner(commentOwner);
|
||||
assertThat(sub.isEmail()).isFalse();
|
||||
assertThat(sub.name()).isEqualTo(commentOwner.getName());
|
||||
owner.setKind(Comment.CommentOwner.KIND_EMAIL);
|
||||
owner.setName("example@example.com");
|
||||
assertThat(identityFrom(owner))
|
||||
.isEqualTo(UserIdentity.anonymousWithEmail(owner.getName()));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,152 @@
|
|||
package run.halo.app.content.comment;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.mock.mockito.SpyBean;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
import run.halo.app.core.extension.content.Reply;
|
||||
import run.halo.app.extension.Extension;
|
||||
import run.halo.app.extension.ExtensionStoreUtil;
|
||||
import run.halo.app.extension.PageRequestImpl;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.extension.SchemeManager;
|
||||
import run.halo.app.extension.index.IndexerFactory;
|
||||
import run.halo.app.extension.store.ReactiveExtensionStoreClient;
|
||||
import run.halo.app.infra.utils.JsonUtils;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link ReplyServiceImpl}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.15.0
|
||||
*/
|
||||
class ReplyServiceImplIntegrationTest {
|
||||
|
||||
@Nested
|
||||
@DirtiesContext
|
||||
@SpringBootTest
|
||||
class ReplyRemoveTest {
|
||||
private final List<Reply> storedReplies = createReplies(320);
|
||||
|
||||
private List<Reply> createReplies(int size) {
|
||||
List<Reply> replies = new ArrayList<>(size);
|
||||
for (int i = 0; i < size; i++) {
|
||||
var reply = JsonUtils.jsonToObject(fakeReplyJson(), Reply.class);
|
||||
reply.getMetadata().setName("reply-" + i);
|
||||
replies.add(reply);
|
||||
}
|
||||
return replies;
|
||||
}
|
||||
|
||||
@Autowired
|
||||
private SchemeManager schemeManager;
|
||||
|
||||
@SpyBean
|
||||
private ReactiveExtensionClient reactiveClient;
|
||||
|
||||
@Autowired
|
||||
private ReactiveExtensionStoreClient storeClient;
|
||||
|
||||
@Autowired
|
||||
private IndexerFactory indexerFactory;
|
||||
|
||||
@SpyBean
|
||||
private ReplyServiceImpl replyService;
|
||||
|
||||
Mono<Extension> deleteImmediately(Extension extension) {
|
||||
var name = extension.getMetadata().getName();
|
||||
var scheme = schemeManager.get(extension.getClass());
|
||||
// un-index
|
||||
var indexer = indexerFactory.getIndexer(extension.groupVersionKind());
|
||||
indexer.unIndexRecord(extension.getMetadata().getName());
|
||||
|
||||
// delete from db
|
||||
var storeName = ExtensionStoreUtil.buildStoreName(scheme, name);
|
||||
return storeClient.delete(storeName, extension.getMetadata().getVersion())
|
||||
.thenReturn(extension);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
Flux.fromIterable(storedReplies)
|
||||
.flatMap(post -> reactiveClient.create(post))
|
||||
.as(StepVerifier::create)
|
||||
.expectNextCount(storedReplies.size())
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
Flux.fromIterable(storedReplies)
|
||||
.flatMap(this::deleteImmediately)
|
||||
.as(StepVerifier::create)
|
||||
.expectNextCount(storedReplies.size())
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
void removeAllByComment() {
|
||||
String commentName = "fake-comment";
|
||||
replyService.removeAllByComment(commentName)
|
||||
.as(StepVerifier::create)
|
||||
.verifyComplete();
|
||||
|
||||
verify(reactiveClient, times(storedReplies.size())).delete(any(Reply.class));
|
||||
verify(replyService, times(2)).listRepliesByComment(eq(commentName), any());
|
||||
|
||||
replyService.listRepliesByComment(commentName, PageRequestImpl.ofSize(1))
|
||||
.as(StepVerifier::create)
|
||||
.consumeNextWith(result -> assertThat(result.getTotal()).isEqualTo(0))
|
||||
.verifyComplete();
|
||||
}
|
||||
}
|
||||
|
||||
String fakeReplyJson() {
|
||||
return """
|
||||
{
|
||||
"metadata":{
|
||||
"name":"fake-reply"
|
||||
},
|
||||
"spec":{
|
||||
"raw":"fake-raw",
|
||||
"content":"fake-content",
|
||||
"owner":{
|
||||
"kind":"User",
|
||||
"name":"fake-user",
|
||||
"displayName":"fake-display-name"
|
||||
},
|
||||
"creationTime": "2024-03-11T06:23:42.923294424Z",
|
||||
"ipAddress":"",
|
||||
"approved": true,
|
||||
"hidden": false,
|
||||
"allowNotification": false,
|
||||
"top": false,
|
||||
"priority": 0,
|
||||
"commentName":"fake-comment"
|
||||
},
|
||||
"owner":{
|
||||
"kind":"User",
|
||||
"displayName":"fake-display-name"
|
||||
},
|
||||
"stats":{
|
||||
"upvote":0
|
||||
}
|
||||
}
|
||||
""";
|
||||
}
|
||||
}
|
|
@ -24,6 +24,7 @@ import org.springframework.test.web.reactive.server.WebTestClient;
|
|||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.extension.User;
|
||||
import run.halo.app.core.extension.service.EmailVerificationService;
|
||||
import run.halo.app.core.extension.service.UserService;
|
||||
import run.halo.app.extension.Metadata;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
|
||||
|
@ -43,6 +44,10 @@ class EmailVerificationCodeTest {
|
|||
ReactiveExtensionClient client;
|
||||
@Mock
|
||||
EmailVerificationService emailVerificationService;
|
||||
|
||||
@Mock
|
||||
UserService userService;
|
||||
|
||||
@InjectMocks
|
||||
UserEndpoint endpoint;
|
||||
|
||||
|
@ -97,9 +102,11 @@ class EmailVerificationCodeTest {
|
|||
void verifyEmail() {
|
||||
when(emailVerificationService.verify(anyString(), anyString()))
|
||||
.thenReturn(Mono.empty());
|
||||
when(userService.confirmPassword(anyString(), anyString()))
|
||||
.thenReturn(Mono.just(true));
|
||||
webClient.post()
|
||||
.uri("/users/-/verify-email")
|
||||
.bodyValue(Map.of("code", "fake-code-1"))
|
||||
.bodyValue(Map.of("code", "fake-code-1", "password", "123456"))
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk();
|
||||
|
@ -107,7 +114,7 @@ class EmailVerificationCodeTest {
|
|||
// request again to trigger rate limit
|
||||
webClient.post()
|
||||
.uri("/users/-/verify-email")
|
||||
.bodyValue(Map.of("code", "fake-code-2"))
|
||||
.bodyValue(Map.of("code", "fake-code-2", "password", "123456"))
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isEqualTo(HttpStatus.TOO_MANY_REQUESTS);
|
||||
|
|
|
@ -18,7 +18,7 @@ import org.springframework.context.ApplicationEventPublisher;
|
|||
import org.springframework.dao.OptimisticLockingFailureException;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.content.Content;
|
||||
import run.halo.app.content.ContentUpdateParam;
|
||||
import run.halo.app.content.PostRequest;
|
||||
import run.halo.app.content.PostService;
|
||||
import run.halo.app.content.TestPost;
|
||||
|
@ -177,6 +177,6 @@ class PostEndpointTest {
|
|||
}
|
||||
|
||||
PostRequest postRequest(Post post) {
|
||||
return new PostRequest(post, new Content("B", "<p>B</p>", "MARKDOWN"));
|
||||
return new PostRequest(post, new ContentUpdateParam(null, "B", "<p>B</p>", "MARKDOWN"));
|
||||
}
|
||||
}
|
|
@ -7,7 +7,7 @@ import static org.junit.jupiter.api.Assertions.assertNull;
|
|||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
|
@ -40,7 +40,6 @@ import org.junit.jupiter.api.io.TempDir;
|
|||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.pf4j.PluginManager;
|
||||
import org.pf4j.PluginState;
|
||||
import org.pf4j.PluginWrapper;
|
||||
import org.pf4j.RuntimeMode;
|
||||
|
@ -56,6 +55,7 @@ import run.halo.app.extension.controller.Reconciler.Request;
|
|||
import run.halo.app.extension.controller.RequeueException;
|
||||
import run.halo.app.infra.ConditionStatus;
|
||||
import run.halo.app.plugin.PluginProperties;
|
||||
import run.halo.app.plugin.SpringPluginManager;
|
||||
|
||||
/**
|
||||
* Tests for {@link PluginReconciler}.
|
||||
|
@ -67,7 +67,7 @@ import run.halo.app.plugin.PluginProperties;
|
|||
class PluginReconcilerTest {
|
||||
|
||||
@Mock
|
||||
PluginManager pluginManager;
|
||||
SpringPluginManager pluginManager;
|
||||
|
||||
@Mock
|
||||
ExtensionClient client;
|
||||
|
@ -185,41 +185,6 @@ class PluginReconcilerTest {
|
|||
mode for plugin fake-plugin.""", gotException.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUnloadIfFailedToLoad() {
|
||||
var fakePlugin = createPlugin(name, plugin -> {
|
||||
var spec = plugin.getSpec();
|
||||
spec.setVersion("1.2.3");
|
||||
spec.setLogo("fake-logo.svg");
|
||||
spec.setEnabled(true);
|
||||
});
|
||||
|
||||
when(client.fetch(Plugin.class, name)).thenReturn(Optional.of(fakePlugin));
|
||||
when(pluginManager.getPluginsRoots()).thenReturn(List.of(tempPath));
|
||||
when(pluginManager.getPlugin(name))
|
||||
// before loading
|
||||
.thenReturn(null)
|
||||
.thenReturn(mock(PluginWrapper.class))
|
||||
;
|
||||
var expectException = mock(RuntimeException.class);
|
||||
when(expectException.getMessage()).thenReturn("Something went wrong.");
|
||||
doThrow(expectException).when(pluginManager).loadPlugin(any(Path.class));
|
||||
|
||||
var gotException = assertThrows(RuntimeException.class,
|
||||
() -> reconciler.reconcile(new Request(name)));
|
||||
|
||||
assertEquals(expectException, gotException);
|
||||
var condition = fakePlugin.getStatus().getConditions().peek();
|
||||
assertEquals("FAILED", condition.getType());
|
||||
assertEquals(ConditionStatus.FALSE, condition.getStatus());
|
||||
assertEquals("UnexpectedState", condition.getReason());
|
||||
assertEquals(expectException.getMessage(), condition.getMessage());
|
||||
assertEquals(clock.instant(), condition.getLastTransitionTime());
|
||||
verify(pluginManager, times(3)).getPlugin(name);
|
||||
verify(pluginManager).loadPlugin(any(Path.class));
|
||||
verify(pluginManager).unloadPlugin(name);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReloadIfReloadAnnotationPresent() {
|
||||
var fakePlugin = createPlugin(name, plugin -> {
|
||||
|
@ -233,15 +198,14 @@ class PluginReconcilerTest {
|
|||
when(client.fetch(Plugin.class, name)).thenReturn(Optional.of(fakePlugin));
|
||||
when(pluginManager.getPluginsRoots()).thenReturn(List.of(tempPath));
|
||||
when(pluginManager.getPlugin(name)).thenReturn(mock(PluginWrapper.class));
|
||||
when(pluginManager.unloadPlugin(name)).thenReturn(true);
|
||||
when(pluginManager.reloadPlugin(eq(name), any(Path.class))).thenReturn(true);
|
||||
when(pluginManager.startPlugin(name)).thenReturn(PluginState.STARTED);
|
||||
|
||||
var result = reconciler.reconcile(new Request(name));
|
||||
assertFalse(result.reEnqueue());
|
||||
|
||||
verify(pluginManager).unloadPlugin(name);
|
||||
var loadLocation = Paths.get(fakePlugin.getStatus().getLoadLocation());
|
||||
verify(pluginManager).loadPlugin(loadLocation);
|
||||
verify(pluginManager).reloadPlugin(name, loadLocation);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -262,11 +226,7 @@ class PluginReconcilerTest {
|
|||
.thenReturn(null)
|
||||
// get setting extension
|
||||
.thenReturn(mockPluginWrapperForSetting())
|
||||
.thenReturn(mockPluginWrapperForStaticResources())
|
||||
// before starting
|
||||
.thenReturn(mockPluginWrapper(PluginState.RESOLVED))
|
||||
// sync plugin state
|
||||
.thenReturn(mockPluginWrapper(PluginState.STARTED));
|
||||
.thenReturn(mockPluginWrapperForStaticResources());
|
||||
when(pluginManager.startPlugin(name)).thenReturn(PluginState.FAILED);
|
||||
|
||||
var e = assertThrows(IllegalStateException.class,
|
||||
|
@ -293,8 +253,6 @@ class PluginReconcilerTest {
|
|||
// get setting extension
|
||||
.thenReturn(mockPluginWrapperForSetting())
|
||||
.thenReturn(mockPluginWrapperForStaticResources())
|
||||
// before starting
|
||||
.thenReturn(mockPluginWrapper(PluginState.RESOLVED))
|
||||
// sync plugin state
|
||||
.thenReturn(mockPluginWrapper(PluginState.STARTED));
|
||||
when(pluginManager.startPlugin(name)).thenReturn(PluginState.STARTED);
|
||||
|
@ -325,7 +283,7 @@ class PluginReconcilerTest {
|
|||
|
||||
verify(pluginManager).startPlugin(name);
|
||||
verify(pluginManager).loadPlugin(loadLocation);
|
||||
verify(pluginManager, times(5)).getPlugin(name);
|
||||
verify(pluginManager, times(4)).getPlugin(name);
|
||||
verify(client).update(fakePlugin);
|
||||
verify(client).fetch(Setting.class, settingName);
|
||||
verify(client).create(any(Setting.class));
|
||||
|
|
|
@ -149,7 +149,7 @@ class PostReconcilerTest {
|
|||
when(client.fetch(eq(Post.class), eq(name)))
|
||||
.thenReturn(Optional.of(post));
|
||||
when(postService.getContent(eq(post.getSpec().getReleaseSnapshot()),
|
||||
eq(post.getSpec().getBaseSnapshot())))
|
||||
eq(post.getSpec().getBaseSnapshot())))
|
||||
.thenReturn(Mono.just(ContentWrapper.builder()
|
||||
.snapshotName(post.getSpec().getHeadSnapshot())
|
||||
.raw("hello world")
|
||||
|
@ -215,11 +215,7 @@ class PostReconcilerTest {
|
|||
assertArg(argReason -> {
|
||||
var interestReason = new Subscription.InterestReason();
|
||||
interestReason.setReasonType(NotificationReasonConst.NEW_COMMENT_ON_POST);
|
||||
interestReason.setSubject(Subscription.ReasonSubject.builder()
|
||||
.apiVersion(post.getApiVersion())
|
||||
.kind(post.getKind())
|
||||
.name(post.getMetadata().getName())
|
||||
.build());
|
||||
interestReason.setExpression("props.postOwner == 'null'");
|
||||
assertThat(argReason).isEqualTo(interestReason);
|
||||
}));
|
||||
}
|
||||
|
|
|
@ -232,11 +232,7 @@ class SinglePageReconcilerTest {
|
|||
assertArg(argReason -> {
|
||||
var interestReason = new Subscription.InterestReason();
|
||||
interestReason.setReasonType(NotificationReasonConst.NEW_COMMENT_ON_PAGE);
|
||||
interestReason.setSubject(Subscription.ReasonSubject.builder()
|
||||
.apiVersion(page.getApiVersion())
|
||||
.kind(page.getKind())
|
||||
.name(page.getMetadata().getName())
|
||||
.build());
|
||||
interestReason.setExpression("props.pageOwner == 'null'");
|
||||
assertThat(argReason).isEqualTo(interestReason);
|
||||
}));
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ import org.mockito.InjectMocks;
|
|||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.extension.Role;
|
||||
import run.halo.app.core.extension.RoleBinding;
|
||||
import run.halo.app.core.extension.User;
|
||||
|
@ -31,6 +32,7 @@ import run.halo.app.extension.Metadata;
|
|||
import run.halo.app.extension.controller.Reconciler;
|
||||
import run.halo.app.infra.AnonymousUserConst;
|
||||
import run.halo.app.infra.ExternalUrlSupplier;
|
||||
import run.halo.app.notification.NotificationCenter;
|
||||
|
||||
/**
|
||||
* Tests for {@link UserReconciler}.
|
||||
|
@ -46,6 +48,9 @@ class UserReconcilerTest {
|
|||
@Mock
|
||||
private ExtensionClient client;
|
||||
|
||||
@Mock
|
||||
private NotificationCenter notificationCenter;
|
||||
|
||||
@Mock
|
||||
private RoleService roleService;
|
||||
|
||||
|
@ -54,6 +59,7 @@ class UserReconcilerTest {
|
|||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
lenient().when(notificationCenter.unsubscribe(any(), any())).thenReturn(Mono.empty());
|
||||
lenient().when(roleService.listRoleRefs(any())).thenReturn(Flux.empty());
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
package run.halo.app.infra;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
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.data.domain.Sort;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
import run.halo.app.extension.FakeExtension;
|
||||
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;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class ReactiveExtensionPaginatedOperatorImplTest {
|
||||
|
||||
@Mock
|
||||
private ReactiveExtensionClient client;
|
||||
|
||||
@InjectMocks
|
||||
private ReactiveExtensionPaginatedOperatorImpl service;
|
||||
|
||||
@Nested
|
||||
class ListTest {
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
Instant now = Instant.now();
|
||||
var items = new ArrayList<>();
|
||||
// Generate 900 items
|
||||
for (int j = 0; j < 9; j++) {
|
||||
items.addAll(generateItems(100, now));
|
||||
}
|
||||
// mock new items during the process
|
||||
Instant otherNow = now.plusSeconds(1000);
|
||||
items.addAll(generateItems(90, otherNow));
|
||||
|
||||
when(client.listBy(any(), any(), any())).thenAnswer(invocation -> {
|
||||
PageRequest pageRequest = invocation.getArgument(2);
|
||||
int pageNumber = pageRequest.getPageNumber();
|
||||
var list = ListResult.subList(items, pageNumber, pageRequest.getPageSize());
|
||||
var result = new ListResult<>(pageNumber, pageRequest.getPageSize(),
|
||||
items.size(), list);
|
||||
return Mono.just(result);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void listTest() {
|
||||
StepVerifier.create(service.list(FakeExtension.class, new ListOptions()))
|
||||
.expectNextCount(900)
|
||||
.verifyComplete();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void nextPageTest() {
|
||||
var result = new ListResult<FakeExtension>(1, 10, 30, List.of());
|
||||
var sort = Sort.by("metadata.creationTimestamp");
|
||||
var nextPage = ReactiveExtensionPaginatedOperatorImpl.nextPage(result, sort);
|
||||
assertThat(nextPage.getPageNumber()).isEqualTo(2);
|
||||
assertThat(nextPage.getPageSize()).isEqualTo(10);
|
||||
assertThat(nextPage.getSort()).isEqualTo(sort);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldTakeNextTest() {
|
||||
var now = Instant.now();
|
||||
var item = new FakeExtension();
|
||||
item.setMetadata(new Metadata());
|
||||
item.getMetadata().setCreationTimestamp(now);
|
||||
var result = ReactiveExtensionPaginatedOperatorImpl.shouldTakeNext(item, now);
|
||||
assertThat(result).isTrue();
|
||||
|
||||
item.getMetadata().setCreationTimestamp(now.minusSeconds(1));
|
||||
result = ReactiveExtensionPaginatedOperatorImpl.shouldTakeNext(item, now);
|
||||
assertThat(result).isTrue();
|
||||
|
||||
item.getMetadata().setCreationTimestamp(now.plusSeconds(1));
|
||||
result = ReactiveExtensionPaginatedOperatorImpl.shouldTakeNext(item, now);
|
||||
assertThat(result).isFalse();
|
||||
}
|
||||
|
||||
private List<FakeExtension> generateItems(int count, Instant creationTimestamp) {
|
||||
List<FakeExtension> items = new ArrayList<>();
|
||||
for (int i = 0; i < count; i++) {
|
||||
var item = new FakeExtension();
|
||||
item.setMetadata(new Metadata());
|
||||
item.getMetadata().setCreationTimestamp(creationTimestamp);
|
||||
items.add(item);
|
||||
}
|
||||
return items;
|
||||
}
|
||||
}
|
|
@ -13,7 +13,6 @@ import static org.mockito.Mockito.when;
|
|||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
|
@ -29,12 +28,8 @@ import run.halo.app.core.extension.notification.NotifierDescriptor;
|
|||
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.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.infra.utils.JsonUtils;
|
||||
|
||||
/**
|
||||
* Tests for {@link DefaultNotificationCenter}.
|
||||
|
@ -60,6 +55,12 @@ class DefaultNotificationCenterTest {
|
|||
@Mock
|
||||
private NotificationSender notificationSender;
|
||||
|
||||
@Mock
|
||||
private RecipientResolver recipientResolver;
|
||||
|
||||
@Mock
|
||||
private SubscriptionService subscriptionService;
|
||||
|
||||
@InjectMocks
|
||||
private DefaultNotificationCenter notificationCenter;
|
||||
|
||||
|
@ -78,21 +79,17 @@ class DefaultNotificationCenterTest {
|
|||
reason.setMetadata(new Metadata());
|
||||
reason.getMetadata().setName("reason-a");
|
||||
|
||||
var subscriptionReasonSubject = createNewReplyOnCommentSubject();
|
||||
|
||||
var spyNotificationCenter = spy(notificationCenter);
|
||||
var subscriptions = createSubscriptions();
|
||||
doReturn(Flux.fromIterable(subscriptions))
|
||||
.when(spyNotificationCenter).listObservers(eq("new-reply-on-comment"),
|
||||
eq(subscriptionReasonSubject));
|
||||
var subscriber = new Subscriber(UserIdentity.anonymousWithEmail("A"), "fake-name");
|
||||
when(recipientResolver.resolve(reason)).thenReturn(Flux.just(subscriber));
|
||||
|
||||
doReturn(Mono.empty()).when(spyNotificationCenter)
|
||||
.dispatchNotification(eq(reason), any());
|
||||
|
||||
spyNotificationCenter.notify(reason).block();
|
||||
|
||||
verify(spyNotificationCenter).dispatchNotification(eq(reason), any());
|
||||
verify(spyNotificationCenter).listObservers(eq("new-reply-on-comment"),
|
||||
eq(subscriptionReasonSubject));
|
||||
verify(recipientResolver).resolve(eq(reason));
|
||||
}
|
||||
|
||||
List<Subscription> createSubscriptions() {
|
||||
|
@ -129,69 +126,16 @@ class DefaultNotificationCenterTest {
|
|||
|
||||
var reason = subscription.getSpec().getReason();
|
||||
|
||||
doReturn(Flux.just(subscription))
|
||||
.when(spyNotificationCenter).listSubscription(eq(subscriber), eq(reason));
|
||||
doReturn(Mono.empty())
|
||||
.when(spyNotificationCenter).unsubscribe(eq(subscriber), eq(reason));
|
||||
|
||||
when(client.create(any(Subscription.class))).thenReturn(Mono.empty());
|
||||
|
||||
spyNotificationCenter.subscribe(subscriber, reason).block();
|
||||
|
||||
verify(client, times(0)).create(any(Subscription.class));
|
||||
|
||||
// not exists subscription will create a new subscription
|
||||
var newReason = JsonUtils.deepCopy(reason);
|
||||
newReason.setReasonType("fake-reason-type");
|
||||
doReturn(Flux.empty())
|
||||
.when(spyNotificationCenter).listSubscription(eq(subscriber), eq(newReason));
|
||||
spyNotificationCenter.subscribe(subscriber, newReason).block();
|
||||
verify(client).create(any(Subscription.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUnsubscribe() {
|
||||
Subscription.Subscriber subscriber = new Subscription.Subscriber();
|
||||
subscriber.setName("anonymousUser#A");
|
||||
var spyNotificationCenter = spy(notificationCenter);
|
||||
var subscriptions = createSubscriptions();
|
||||
|
||||
doReturn(Mono.just(new ListResult<>(subscriptions)))
|
||||
.when(spyNotificationCenter).pageSubscriptionBy(eq(subscriber), any(PageRequest.class));
|
||||
|
||||
when(client.delete(any(Subscription.class))).thenReturn(Mono.empty());
|
||||
|
||||
spyNotificationCenter.unsubscribe(subscriber).block();
|
||||
|
||||
verify(client).delete(any(Subscription.class));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testUnsubscribeWithReason() {
|
||||
var spyNotificationCenter = spy(notificationCenter);
|
||||
var subscriptions = createSubscriptions();
|
||||
|
||||
var subscription = subscriptions.get(0);
|
||||
|
||||
var subscriber = subscription.getSpec().getSubscriber();
|
||||
|
||||
var reason = subscription.getSpec().getReason();
|
||||
|
||||
var newReason = JsonUtils.deepCopy(reason);
|
||||
newReason.setReasonType("fake-reason-type");
|
||||
doReturn(Flux.empty())
|
||||
.when(spyNotificationCenter).listSubscription(eq(subscriber), eq(newReason));
|
||||
when(client.delete(any(Subscription.class))).thenReturn(Mono.empty());
|
||||
spyNotificationCenter.unsubscribe(subscriber, newReason).block();
|
||||
verify(client, times(0)).delete(any(Subscription.class));
|
||||
|
||||
doReturn(Flux.just(subscription))
|
||||
.when(spyNotificationCenter).listSubscription(eq(subscriber), eq(reason));
|
||||
|
||||
// exists subscription will be deleted
|
||||
spyNotificationCenter.unsubscribe(subscriber, reason).block();
|
||||
verify(client).delete(any(Subscription.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetNotifiersBySubscriber() {
|
||||
UserNotificationPreference preference = new UserNotificationPreference();
|
||||
|
@ -203,8 +147,7 @@ class DefaultNotificationCenterTest {
|
|||
reason.getMetadata().setName("reason-a");
|
||||
reason.setSpec(new Reason.Spec());
|
||||
reason.getSpec().setReasonType("new-reply-on-comment");
|
||||
var subscriber = new Subscription.Subscriber();
|
||||
subscriber.setName("anonymousUser#A");
|
||||
var subscriber = new Subscriber(UserIdentity.anonymousWithEmail("A"), "fake-name");
|
||||
|
||||
notificationCenter.getNotifiersBySubscriber(subscriber, reason)
|
||||
.collectList()
|
||||
|
@ -215,7 +158,7 @@ class DefaultNotificationCenterTest {
|
|||
})
|
||||
.verifyComplete();
|
||||
|
||||
verify(userNotificationPreferenceService).getByUser(eq(subscriber.getName()));
|
||||
verify(userNotificationPreferenceService).getByUser(eq(subscriber.name()));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -234,7 +177,6 @@ class DefaultNotificationCenterTest {
|
|||
.when(spyNotificationCenter).prepareNotificationElement(any(), any(), any());
|
||||
|
||||
doReturn(Mono.empty()).when(spyNotificationCenter).sendNotification(any());
|
||||
doReturn(Mono.empty()).when(spyNotificationCenter).createNotification(any());
|
||||
|
||||
var reason = new Reason();
|
||||
reason.setMetadata(new Metadata());
|
||||
|
@ -243,11 +185,15 @@ class DefaultNotificationCenterTest {
|
|||
reason.getSpec().setReasonType("new-reply-on-comment");
|
||||
|
||||
var subscription = createSubscriptions().get(0);
|
||||
spyNotificationCenter.dispatchNotification(reason, subscription).block();
|
||||
var subscriptionName = subscription.getMetadata().getName();
|
||||
var subscriber =
|
||||
new Subscriber(UserIdentity.of(subscription.getSpec().getSubscriber().getName()),
|
||||
subscriptionName);
|
||||
spyNotificationCenter.dispatchNotification(reason, subscriber).block();
|
||||
|
||||
verify(client).fetch(eq(NotifierDescriptor.class), eq("email-notifier"));
|
||||
verify(spyNotificationCenter).sendNotification(any());
|
||||
verify(spyNotificationCenter).createNotification(any());
|
||||
verify(spyNotificationCenter, times(0)).createNotification(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -282,7 +228,7 @@ class DefaultNotificationCenterTest {
|
|||
var element = mock(DefaultNotificationCenter.NotificationElement.class);
|
||||
var mockDescriptor = mock(NotifierDescriptor.class);
|
||||
when(element.descriptor()).thenReturn(mockDescriptor);
|
||||
when(element.subscription()).thenReturn(mock(Subscription.class));
|
||||
when(element.subscriber()).thenReturn(mock(Subscriber.class));
|
||||
var notifierDescriptorSpec = mock(NotifierDescriptor.Spec.class);
|
||||
when(mockDescriptor.getSpec()).thenReturn(notifierDescriptorSpec);
|
||||
when(notifierDescriptorSpec.getNotifierExtName()).thenReturn("fake-notifier-ext");
|
||||
|
@ -299,9 +245,12 @@ class DefaultNotificationCenterTest {
|
|||
var subscription = createSubscriptions().get(0);
|
||||
var user = mock(User.class);
|
||||
|
||||
var subscriberName = subscription.getSpec().getSubscriber().getName();
|
||||
when(client.fetch(eq(User.class), eq(subscriberName))).thenReturn(Mono.just(user));
|
||||
when(element.subscription()).thenReturn(subscription);
|
||||
var subscriptionName = subscription.getMetadata().getName();
|
||||
var subscriber =
|
||||
new Subscriber(UserIdentity.of(subscription.getSpec().getSubscriber().getName()),
|
||||
subscriptionName);
|
||||
when(client.fetch(eq(User.class), eq(subscriber.name()))).thenReturn(Mono.just(user));
|
||||
when(element.subscriber()).thenReturn(subscriber);
|
||||
|
||||
when(client.create(any(Notification.class))).thenReturn(Mono.empty());
|
||||
|
||||
|
@ -314,7 +263,7 @@ class DefaultNotificationCenterTest {
|
|||
|
||||
notificationCenter.createNotification(element).block();
|
||||
|
||||
verify(client).fetch(eq(User.class), eq(subscriberName));
|
||||
verify(client).fetch(eq(User.class), eq(subscriber.name()));
|
||||
verify(client).create(any(Notification.class));
|
||||
}
|
||||
|
||||
|
@ -334,8 +283,8 @@ class DefaultNotificationCenterTest {
|
|||
|
||||
doReturn(Mono.just(reasonType))
|
||||
.when(spyNotificationCenter).getReasonType(eq(reasonTypeName));
|
||||
doReturn("fake-url")
|
||||
.when(spyNotificationCenter).getUnsubscribeUrl(any());
|
||||
doReturn(Mono.just("fake-unsubscribe-url"))
|
||||
.when(spyNotificationCenter).getUnsubscribeUrl(anyString());
|
||||
|
||||
final var locale = Locale.CHINESE;
|
||||
|
||||
|
@ -356,98 +305,17 @@ class DefaultNotificationCenterTest {
|
|||
when(notificationTemplateSelector.select(eq(reasonTypeName), any()))
|
||||
.thenReturn(Mono.just(template));
|
||||
|
||||
var subscription = new Subscription();
|
||||
subscription.setSpec(new Subscription.Spec());
|
||||
var subscriber = new Subscription.Subscriber();
|
||||
subscriber.setName("anonymousUser#A");
|
||||
subscription.getSpec().setSubscriber(subscriber);
|
||||
var subscriber = new Subscriber(UserIdentity.anonymousWithEmail("A"), "fake-name");
|
||||
|
||||
spyNotificationCenter.inferenceTemplate(reason, subscription, locale).block();
|
||||
spyNotificationCenter.inferenceTemplate(reason, subscriber, locale).block();
|
||||
|
||||
verify(spyNotificationCenter).getReasonType(eq(reasonTypeName));
|
||||
verify(notificationTemplateSelector).select(eq(reasonTypeName), any());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void listObserverWhenDuplicateSubscribers() {
|
||||
var sourceSubscriptions = createSubscriptions();
|
||||
var subscriptionA = sourceSubscriptions.get(0);
|
||||
var subscriptionB = JsonUtils.deepCopy(subscriptionA);
|
||||
|
||||
var subscriptionC = JsonUtils.deepCopy(subscriptionA);
|
||||
subscriptionC.getSpec().getReason().getSubject().setName(null);
|
||||
|
||||
var subscriptions = Flux.just(subscriptionA, subscriptionB, subscriptionC);
|
||||
|
||||
DefaultNotificationCenter.distinctByKey(subscriptions)
|
||||
.as(StepVerifier::create)
|
||||
.expectNext(subscriptionA)
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Nested
|
||||
class SubscriptionDistinctKeyPredicateTest {
|
||||
|
||||
@Test
|
||||
void differentSubjectName() {
|
||||
var subscriptionA = createSubscriptions().get(0);
|
||||
var subscriptionB = JsonUtils.deepCopy(subscriptionA);
|
||||
assertThat(DefaultNotificationCenter.subscriptionDistinctKeyPredicate()
|
||||
.test(subscriptionA, subscriptionB)).isTrue();
|
||||
|
||||
subscriptionB.getSpec().getReason().getSubject().setName("other");
|
||||
assertThat(DefaultNotificationCenter.subscriptionDistinctKeyPredicate()
|
||||
.test(subscriptionA, subscriptionB)).isFalse();
|
||||
|
||||
subscriptionB.getSpec().getReason().getSubject().setName(null);
|
||||
assertThat(DefaultNotificationCenter.subscriptionDistinctKeyPredicate()
|
||||
.test(subscriptionA, subscriptionB)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void differentSubjectApiVersion() {
|
||||
var subscriptionA = createSubscriptions().get(0);
|
||||
var subscriptionB = JsonUtils.deepCopy(subscriptionA);
|
||||
assertThat(DefaultNotificationCenter.subscriptionDistinctKeyPredicate()
|
||||
.test(subscriptionA, subscriptionB)).isTrue();
|
||||
|
||||
var subjectA = subscriptionA.getSpec().getReason().getSubject();
|
||||
var gvForA = GroupVersion.parseAPIVersion(subjectA.getApiVersion());
|
||||
subscriptionB.getSpec().getReason().getSubject()
|
||||
.setApiVersion(gvForA.group() + "/otherVersion");
|
||||
assertThat(DefaultNotificationCenter.subscriptionDistinctKeyPredicate()
|
||||
.test(subscriptionA, subscriptionB)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void differentReasonType() {
|
||||
var subscriptionA = createSubscriptions().get(0);
|
||||
var subscriptionB = JsonUtils.deepCopy(subscriptionA);
|
||||
assertThat(DefaultNotificationCenter.subscriptionDistinctKeyPredicate()
|
||||
.test(subscriptionA, subscriptionB)).isTrue();
|
||||
|
||||
subscriptionB.getSpec().getReason().setReasonType("other");
|
||||
assertThat(DefaultNotificationCenter.subscriptionDistinctKeyPredicate()
|
||||
.test(subscriptionA, subscriptionB)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void differentSubscriber() {
|
||||
var subscriptionA = createSubscriptions().get(0);
|
||||
var subscriptionB = JsonUtils.deepCopy(subscriptionA);
|
||||
assertThat(DefaultNotificationCenter.subscriptionDistinctKeyPredicate()
|
||||
.test(subscriptionA, subscriptionB)).isTrue();
|
||||
|
||||
subscriptionB.getSpec().getSubscriber().setName("other");
|
||||
assertThat(DefaultNotificationCenter.subscriptionDistinctKeyPredicate()
|
||||
.test(subscriptionA, subscriptionB)).isFalse();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void getLocaleFromSubscriberTest() {
|
||||
var subscription = mock(Subscription.class);
|
||||
var subscription = mock(Subscriber.class);
|
||||
|
||||
notificationCenter.getLocaleFromSubscriber(subscription)
|
||||
.as(StepVerifier::create)
|
||||
|
|
|
@ -0,0 +1,180 @@
|
|||
package run.halo.app.notification;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
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 reactor.core.publisher.Flux;
|
||||
import reactor.test.StepVerifier;
|
||||
import run.halo.app.core.extension.notification.Reason;
|
||||
import run.halo.app.core.extension.notification.Subscription;
|
||||
import run.halo.app.extension.Metadata;
|
||||
|
||||
/**
|
||||
* Tests for {@link RecipientResolverImpl}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.15.0
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class RecipientResolverImplTest {
|
||||
|
||||
@Mock
|
||||
private SubscriptionService subscriptionService;
|
||||
|
||||
@InjectMocks
|
||||
private RecipientResolverImpl recipientResolver;
|
||||
|
||||
@Test
|
||||
void testExpressionMatch() {
|
||||
var subscriber1 = new Subscription.Subscriber();
|
||||
subscriber1.setName("test");
|
||||
final var subscription1 = createSubscription(subscriber1);
|
||||
subscription1.getMetadata().setName("test-subscription");
|
||||
subscription1.getSpec().getReason().setSubject(null);
|
||||
subscription1.getSpec().getReason().setExpression("props.owner == 'test'");
|
||||
|
||||
var subscriber2 = new Subscription.Subscriber();
|
||||
subscriber2.setName("guqing");
|
||||
final var subscription2 = createSubscription(subscriber2);
|
||||
subscription2.getMetadata().setName("guqing-subscription");
|
||||
subscription2.getSpec().getReason().setSubject(null);
|
||||
subscription2.getSpec().getReason().setExpression("props.owner == 'guqing'");
|
||||
|
||||
var reason = new Reason();
|
||||
reason.setSpec(new Reason.Spec());
|
||||
reason.getSpec().setReasonType("new-comment-on-post");
|
||||
reason.getSpec().setSubject(new Reason.Subject());
|
||||
reason.getSpec().getSubject().setApiVersion("content.halo.run/v1alpha1");
|
||||
reason.getSpec().getSubject().setKind("Post");
|
||||
reason.getSpec().getSubject().setName("fake-post");
|
||||
var reasonAttributes = new ReasonAttributes();
|
||||
reasonAttributes.put("owner", "guqing");
|
||||
reason.getSpec().setAttributes(reasonAttributes);
|
||||
|
||||
when(subscriptionService.listByPerPage(anyString()))
|
||||
.thenReturn(Flux.just(subscription1, subscription2));
|
||||
|
||||
recipientResolver.resolve(reason)
|
||||
.as(StepVerifier::create)
|
||||
.expectNext(new Subscriber(UserIdentity.of("guqing"), "guqing-subscription"))
|
||||
.verifyComplete();
|
||||
|
||||
verify(subscriptionService).listByPerPage(anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSubjectMatch() {
|
||||
var subscriber = new Subscription.Subscriber();
|
||||
subscriber.setName("test");
|
||||
Subscription subscription = createSubscription(subscriber);
|
||||
|
||||
when(subscriptionService.listByPerPage(anyString()))
|
||||
.thenReturn(Flux.just(subscription));
|
||||
|
||||
var reason = new Reason();
|
||||
reason.setSpec(new Reason.Spec());
|
||||
reason.getSpec().setReasonType("new-comment-on-post");
|
||||
reason.getSpec().setSubject(new Reason.Subject());
|
||||
reason.getSpec().getSubject().setApiVersion("content.halo.run/v1alpha1");
|
||||
reason.getSpec().getSubject().setKind("Post");
|
||||
reason.getSpec().getSubject().setName("fake-post");
|
||||
|
||||
recipientResolver.resolve(reason)
|
||||
.as(StepVerifier::create)
|
||||
.expectNext(new Subscriber(UserIdentity.of("test"), "fake-subscription"))
|
||||
.verifyComplete();
|
||||
|
||||
verify(subscriptionService).listByPerPage(anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void distinct() {
|
||||
// same subscriber to different subscriptions
|
||||
var subscriber = new Subscription.Subscriber();
|
||||
subscriber.setName("test");
|
||||
|
||||
final var subscription1 = createSubscription(subscriber);
|
||||
subscription1.getMetadata().setName("sub-1");
|
||||
|
||||
final var subscription2 = createSubscription(subscriber);
|
||||
subscription2.getMetadata().setName("sub-2");
|
||||
subscription2.getSpec().getReason().setSubject(null);
|
||||
subscription2.getSpec().getReason().setExpression("props.owner == 'guqing'");
|
||||
|
||||
when(subscriptionService.listByPerPage(anyString()))
|
||||
.thenReturn(Flux.just(subscription1, subscription2));
|
||||
|
||||
var reason = new Reason();
|
||||
reason.setSpec(new Reason.Spec());
|
||||
reason.getSpec().setReasonType("new-comment-on-post");
|
||||
reason.getSpec().setSubject(new Reason.Subject());
|
||||
reason.getSpec().getSubject().setApiVersion("content.halo.run/v1alpha1");
|
||||
reason.getSpec().getSubject().setKind("Post");
|
||||
reason.getSpec().getSubject().setName("fake-post");
|
||||
var reasonAttributes = new ReasonAttributes();
|
||||
reasonAttributes.put("owner", "guqing");
|
||||
reason.getSpec().setAttributes(reasonAttributes);
|
||||
|
||||
recipientResolver.resolve(reason)
|
||||
.as(StepVerifier::create)
|
||||
.expectNextCount(1)
|
||||
.verifyComplete();
|
||||
|
||||
verify(subscriptionService).listByPerPage(anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void subjectMatchTest() {
|
||||
var subscriber = new Subscription.Subscriber();
|
||||
subscriber.setName("test");
|
||||
|
||||
final var subscription = createSubscription(subscriber);
|
||||
|
||||
// match all name subscription
|
||||
var subject = new Reason.Subject();
|
||||
subject.setApiVersion("content.halo.run/v1alpha1");
|
||||
subject.setKind("Post");
|
||||
subject.setName("fake-post");
|
||||
assertThat(RecipientResolverImpl.subjectMatch(subscription, subject)).isTrue();
|
||||
|
||||
// different kind
|
||||
subject = new Reason.Subject();
|
||||
subject.setApiVersion("content.halo.run/v1alpha1");
|
||||
subject.setKind("SinglePage");
|
||||
subject.setName("fake-post");
|
||||
assertThat(RecipientResolverImpl.subjectMatch(subscription, subject)).isFalse();
|
||||
|
||||
// special case
|
||||
subscription.getSpec().getReason().getSubject().setName("other-post");
|
||||
subject = new Reason.Subject();
|
||||
subject.setApiVersion("content.halo.run/v1alpha1");
|
||||
subject.setKind("Post");
|
||||
subject.setName("fake-post");
|
||||
assertThat(RecipientResolverImpl.subjectMatch(subscription, subject)).isFalse();
|
||||
subject.setName("other-post");
|
||||
assertThat(RecipientResolverImpl.subjectMatch(subscription, subject)).isTrue();
|
||||
}
|
||||
|
||||
private static Subscription createSubscription(Subscription.Subscriber subscriber) {
|
||||
Subscription subscription = new Subscription();
|
||||
subscription.setMetadata(new Metadata());
|
||||
subscription.getMetadata().setName("fake-subscription");
|
||||
subscription.setSpec(new Subscription.Spec());
|
||||
subscription.getSpec().setSubscriber(subscriber);
|
||||
|
||||
var interestReason = new Subscription.InterestReason();
|
||||
interestReason.setReasonType("new-comment-on-post");
|
||||
interestReason.setSubject(new Subscription.ReasonSubject());
|
||||
interestReason.getSubject().setApiVersion("content.halo.run/v1alpha1");
|
||||
interestReason.getSubject().setKind("Post");
|
||||
subscription.getSpec().setReason(interestReason);
|
||||
return subscription;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
package run.halo.app.notification;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
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.dao.OptimisticLockingFailureException;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
import run.halo.app.core.extension.notification.Subscription;
|
||||
import run.halo.app.extension.Metadata;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
|
||||
/**
|
||||
* Tests for {@link SubscriptionServiceImpl}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.15.0
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class SubscriptionServiceImplTest {
|
||||
|
||||
@Mock
|
||||
private ReactiveExtensionClient client;
|
||||
|
||||
@InjectMocks
|
||||
private SubscriptionServiceImpl subscriptionService;
|
||||
|
||||
@Test
|
||||
void remove() {
|
||||
var i = new AtomicLong(1L);
|
||||
when(client.delete(any(Subscription.class))).thenAnswer(invocation -> {
|
||||
var subscription = (Subscription) invocation.getArgument(0);
|
||||
if (i.get() != subscription.getMetadata().getVersion()) {
|
||||
return Mono.error(new OptimisticLockingFailureException("fake-exception"));
|
||||
}
|
||||
return Mono.just(subscription);
|
||||
});
|
||||
|
||||
var subscription = new Subscription();
|
||||
subscription.setMetadata(new Metadata());
|
||||
subscription.getMetadata().setName("fake-subscription");
|
||||
subscription.getMetadata().setVersion(0L);
|
||||
|
||||
when(client.fetch(eq(Subscription.class), eq("fake-subscription")))
|
||||
.thenAnswer(invocation -> {
|
||||
if (i.incrementAndGet() > 3) {
|
||||
subscription.getMetadata().setVersion(i.get());
|
||||
} else {
|
||||
subscription.getMetadata().setVersion(i.get() - 1);
|
||||
}
|
||||
return Mono.just(subscription);
|
||||
});
|
||||
|
||||
subscriptionService.remove(subscription)
|
||||
.as(StepVerifier::create)
|
||||
.expectNextCount(1)
|
||||
.verifyComplete();
|
||||
|
||||
// give version=0, but the real version is 1
|
||||
// give version=1, but the real version is 2
|
||||
// give version=2, but the real version is 3
|
||||
// give version=3, but the real version is 3 (delete success)
|
||||
verify(client, times(3)).fetch(eq(Subscription.class), eq("fake-subscription"));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,171 @@
|
|||
package run.halo.app.notification;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.atLeast;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static run.halo.app.extension.index.query.QueryFactory.isNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.mock.mockito.SpyBean;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
import run.halo.app.core.extension.notification.Subscription;
|
||||
import run.halo.app.extension.Extension;
|
||||
import run.halo.app.extension.ExtensionStoreUtil;
|
||||
import run.halo.app.extension.ListOptions;
|
||||
import run.halo.app.extension.PageRequestImpl;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.extension.SchemeManager;
|
||||
import run.halo.app.extension.index.IndexerFactory;
|
||||
import run.halo.app.extension.router.selector.FieldSelector;
|
||||
import run.halo.app.extension.store.ReactiveExtensionStoreClient;
|
||||
import run.halo.app.infra.utils.JsonUtils;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link SubscriptionService}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.15.0
|
||||
*/
|
||||
@DirtiesContext
|
||||
@SpringBootTest
|
||||
class SubscriptionServiceIntegrationTest {
|
||||
|
||||
@Autowired
|
||||
private SchemeManager schemeManager;
|
||||
|
||||
@SpyBean
|
||||
private ReactiveExtensionClient client;
|
||||
|
||||
@Autowired
|
||||
private ReactiveExtensionStoreClient storeClient;
|
||||
|
||||
@Autowired
|
||||
private IndexerFactory indexerFactory;
|
||||
|
||||
Mono<Extension> deleteImmediately(Extension extension) {
|
||||
var name = extension.getMetadata().getName();
|
||||
var scheme = schemeManager.get(extension.getClass());
|
||||
// un-index
|
||||
var indexer = indexerFactory.getIndexer(extension.groupVersionKind());
|
||||
indexer.unIndexRecord(extension.getMetadata().getName());
|
||||
|
||||
// delete from db
|
||||
var storeName = ExtensionStoreUtil.buildStoreName(scheme, name);
|
||||
return storeClient.delete(storeName, extension.getMetadata().getVersion())
|
||||
.thenReturn(extension);
|
||||
}
|
||||
|
||||
@Nested
|
||||
class RemoveInitialBatchTest {
|
||||
static int size = 310;
|
||||
private final List<Subscription> storedSubscriptions = subscriptionsForStore();
|
||||
|
||||
@Autowired
|
||||
private SubscriptionService subscriptionService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
Flux.fromIterable(storedSubscriptions)
|
||||
.flatMap(comment -> client.create(comment))
|
||||
.as(StepVerifier::create)
|
||||
.expectNextCount(storedSubscriptions.size())
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
Flux.fromIterable(storedSubscriptions)
|
||||
.flatMap(SubscriptionServiceIntegrationTest.this::deleteImmediately)
|
||||
.as(StepVerifier::create)
|
||||
.expectNextCount(storedSubscriptions.size())
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
private List<Subscription> subscriptionsForStore() {
|
||||
List<Subscription> subscriptions = new ArrayList<>(size);
|
||||
for (int i = 0; i < size; i++) {
|
||||
var subscription = createSubscription();
|
||||
subscription.getMetadata().setName("subscription-" + i);
|
||||
subscriptions.add(subscription);
|
||||
}
|
||||
return subscriptions;
|
||||
}
|
||||
|
||||
@Test
|
||||
void removeTest() {
|
||||
var subscriber = new Subscription.Subscriber();
|
||||
subscriber.setName("admin");
|
||||
var interestReason = new Subscription.InterestReason();
|
||||
interestReason.setReasonType("new-comment-on-post");
|
||||
var subject = new Subscription.ReasonSubject();
|
||||
subject.setApiVersion("content.halo.run/v1alpha1");
|
||||
subject.setKind("Post");
|
||||
interestReason.setSubject(subject);
|
||||
|
||||
subscriptionService.remove(subscriber, interestReason).block();
|
||||
|
||||
verify(client, atLeast(size)).delete(any(Subscription.class));
|
||||
assertCleanedUp();
|
||||
}
|
||||
|
||||
@Test
|
||||
void removeBySubscriberTest() {
|
||||
var subscriber = new Subscription.Subscriber();
|
||||
subscriber.setName("admin");
|
||||
|
||||
subscriptionService.remove(subscriber).block();
|
||||
verify(client, atLeast(size)).delete(any(Subscription.class));
|
||||
assertCleanedUp();
|
||||
}
|
||||
|
||||
private void assertCleanedUp() {
|
||||
var listOptions = new ListOptions();
|
||||
listOptions.setFieldSelector(FieldSelector.of(isNull("metadata.deletionTimestamp")));
|
||||
client.listBy(Subscription.class, listOptions, PageRequestImpl.ofSize(1))
|
||||
.as(StepVerifier::create)
|
||||
.consumeNextWith(result -> {
|
||||
assertThat(result.getTotal()).isEqualTo(0);
|
||||
assertThat(result.getItems()).isEmpty();
|
||||
})
|
||||
.verifyComplete();
|
||||
}
|
||||
}
|
||||
|
||||
Subscription createSubscription() {
|
||||
return JsonUtils.jsonToObject("""
|
||||
{
|
||||
"spec": {
|
||||
"subscriber": {
|
||||
"name": "admin"
|
||||
},
|
||||
"unsubscribeToken": "423530c9-bec7-446e-b73b-dd98ac00ba2b",
|
||||
"reason": {
|
||||
"reasonType": "new-comment-on-post",
|
||||
"subject": {
|
||||
"name": "5152aea5-c2e8-4717-8bba-2263d46e19d5",
|
||||
"apiVersion": "content.halo.run/v1alpha1",
|
||||
"kind": "Post"
|
||||
}
|
||||
},
|
||||
"disabled": false
|
||||
},
|
||||
"apiVersion": "notification.halo.run/v1alpha1",
|
||||
"kind": "Subscription",
|
||||
"metadata": {
|
||||
"generateName": "subscription-"
|
||||
}
|
||||
}
|
||||
""", Subscription.class);
|
||||
}
|
||||
}
|
|
@ -19,7 +19,8 @@
|
|||
设计一个通知功能,可以根据以下目标,实现订阅和推送通知:
|
||||
|
||||
- 支持扩展多种通知方式,例如邮件、短信、Slack 等。
|
||||
- 支持通知条件并可扩展,例如 Halo 有新文章发布事件如果用户订阅了新文章发布事件但付费订阅插件决定了此文章只有付费用户才可收到通知、按照付费等级不同决定是否发送新文章通知给对应用户等需要通过实现通知条件的扩展点来满足对应需求。
|
||||
- 支持通知条件并可扩展,例如 Halo
|
||||
有新文章发布事件如果用户订阅了新文章发布事件但付费订阅插件决定了此文章只有付费用户才可收到通知、按照付费等级不同决定是否发送新文章通知给对应用户等需要通过实现通知条件的扩展点来满足对应需求。
|
||||
- 支持定制化选项,例如是否开启通知、通知时段等。
|
||||
- 支持通知流程,例如通知的发送、接收、查看、标记等。
|
||||
- 通知内容支持多语言。
|
||||
|
@ -97,7 +98,8 @@ spec:
|
|||
|
||||
#### Subscription
|
||||
|
||||
`Subscription` 自定义模型,定义了特定事件时与要被通知的订阅者之间的关系, 其中 `subscriber` 表示订阅者用户, `unsubscribeToken` 表示退订时的身份验证 token, `reason` 订阅者感兴趣的事件。
|
||||
`Subscription` 自定义模型,定义了特定事件时与要被通知的订阅者之间的关系, 其中 `subscriber`
|
||||
表示订阅者用户, `unsubscribeToken` 表示退订时的身份验证 token, `reason` 订阅者感兴趣的事件。
|
||||
|
||||
用户可以通过 `Subscription` 来订阅自己感兴趣的事件,当事件触发时会收到通知:
|
||||
|
||||
|
@ -116,13 +118,24 @@ spec:
|
|||
apiVersion: content.halo.run/v1alpha1
|
||||
kind: Post
|
||||
name: 'post-axgu'
|
||||
# expression: 'props.owner == "guqing"'
|
||||
```
|
||||
|
||||
订阅退订链接 API 规则:`/apis/api.notification.halo.run/v1alpha1/subscriptions/{name}/unsubscribe?token={unsubscribeToken}`。
|
||||
- `spec.reason.subject`:用于根据事件的主体的匹配感兴趣的事件,如果不指定 name 则表示匹配主体与 kind 和 apiVersion
|
||||
相同的一类事件。
|
||||
- `spec.expression`:根据表达式匹配感兴趣的事件,例如 `props.owner == "guqing"` 表示只有当事件的属性(reason attributes)的
|
||||
owner 等于 guqing 时才会触发通知。表达式符合 SpEL
|
||||
表达式语法,但结果只能是布尔值。参考:[增强 Subscription 模型以支持表达式匹配](https://github.com/halo-dev/halo/issues/5632)
|
||||
|
||||
> 当 `spec.expression` 和 `spec.reason.subject` 同时存在时,以 `spec.reason.subject` 的结果为准,不建议同时使用。
|
||||
|
||||
订阅退订链接 API
|
||||
规则:`/apis/api.notification.halo.run/v1alpha1/subscriptions/{name}/unsubscribe?token={unsubscribeToken}`。
|
||||
|
||||
#### 用户通知偏好设置
|
||||
|
||||
通过在用户偏好设置的 ConfigMap 中存储一个 `notification` key 用于保存事件类型与通知方式的关系设置,当用户订阅了如 'new-comment-on-post' 事件时会获取对应的通知方式来给用户发送通知。
|
||||
通过在用户偏好设置的 ConfigMap 中存储一个 `notification` key 用于保存事件类型与通知方式的关系设置,当用户订阅了如 '
|
||||
new-comment-on-post' 事件时会获取对应的通知方式来给用户发送通知。
|
||||
|
||||
```yaml
|
||||
apiVersion: v1alpha1
|
||||
|
@ -153,7 +166,8 @@ data:
|
|||
|
||||
#### Notification 站内通知
|
||||
|
||||
当用户订阅到事件后会创建 `Notification`, 它与通知方式(notifier)无关,`recipient` 为用户名,类似站内通知,如用户 `guqing` 订阅了评论事件那么当监听到评论事件时会创建一条记录可以在个人中心的通知列表看到一条通知消息。
|
||||
当用户订阅到事件后会创建 `Notification`, 它与通知方式(notifier)无关,`recipient` 为用户名,类似站内通知,如用户 `guqing`
|
||||
订阅了评论事件那么当监听到评论事件时会创建一条记录可以在个人中心的通知列表看到一条通知消息。
|
||||
|
||||
```yaml
|
||||
apiVersion: notification.halo.run/v1alpha1
|
||||
|
@ -177,6 +191,7 @@ spec:
|
|||
`GET /apis/api.notification.halo.run/v1alpha1/userspaces/{username}/notifications`
|
||||
2. 将通知标记为已读:`PUT /apis/api.notification.halo.run/v1alpha1/userspaces/{username}/notifications/mark-as-read`
|
||||
3.
|
||||
|
||||
批量将通知标记为已读:`PUT /apis/api.notification.halo.run/v1alpha1/userspaces/{username}/notifications/mark-specified-as-read`
|
||||
|
||||
#### 通知模板
|
||||
|
@ -185,14 +200,18 @@ spec:
|
|||
它通过定义 `reasonSelector` 来引用事件类别,当事件触发时会根据用户的语言偏好和触发事件的类别来选择一个最佳的通知模板。
|
||||
选择通知模板的规则为:
|
||||
|
||||
1. 根据用户设置的语言,选择从通知模板中定义的 `spec.reasonSelector.language` 的值从更具体到不太具体的顺序(例如,gl_ES 的值将比 gl 的值具有更高的优先级)。
|
||||
2. 当通过语言成功匹配到模板时,匹配到的结果可能不止一个,如 `language` 为 `zh_CN` 的模板有三个那么会根据 `NotificationTemplate` 的 `metadata.creationTimestamp` 字段来选择一个最新的模板。
|
||||
1. 根据用户设置的语言,选择从通知模板中定义的 `spec.reasonSelector.language` 的值从更具体到不太具体的顺序(例如,gl_ES 的值将比
|
||||
gl 的值具有更高的优先级)。
|
||||
2. 当通过语言成功匹配到模板时,匹配到的结果可能不止一个,如 `language` 为 `zh_CN`
|
||||
的模板有三个那么会根据 `NotificationTemplate` 的 `metadata.creationTimestamp` 字段来选择一个最新的模板。
|
||||
|
||||
这样的规则有助于用户可以个性化定制某些事件的模板内容。
|
||||
|
||||
模板语法使用 ThymeleafEngine 渲染,纯文本模板使用 `textual` 模板模式,语法参考: [usingthymeleaf.html#textual-syntax](https://www.thymeleaf.org/doc/tutorials/3.1/usingthymeleaf.html#textual-syntax)
|
||||
模板语法使用 ThymeleafEngine 渲染,纯文本模板使用 `textual`
|
||||
模板模式,语法参考: [usingthymeleaf.html#textual-syntax](https://www.thymeleaf.org/doc/tutorials/3.1/usingthymeleaf.html#textual-syntax)
|
||||
|
||||
`HTML` 则使用标准表达式语法在标签属性中取值,语法参考:[standard-expression-syntax](https://www.thymeleaf.org/doc/tutorials/3.1/usingthymeleaf.html#standard-expression-syntax)
|
||||
`HTML`
|
||||
则使用标准表达式语法在标签属性中取值,语法参考:[standard-expression-syntax](https://www.thymeleaf.org/doc/tutorials/3.1/usingthymeleaf.html#standard-expression-syntax)
|
||||
|
||||
在通知中心渲染模板时会在 `ReasonAttributes` 中提供额外属性包括:
|
||||
|
||||
|
@ -224,11 +243,12 @@ spec:
|
|||
|
||||
#### 通知器声明及扩展
|
||||
|
||||
`NotifierDescriptor` 自定义模型用于声明通知器,通过它来描述通知器的名称、描述和关联的 `ExtensionDefinition` 名称,让用户可以在用户界面知道通知器是什么以及它可以做什么,
|
||||
`NotifierDescriptor` 自定义模型用于声明通知器,通过它来描述通知器的名称、描述和关联的 `ExtensionDefinition`
|
||||
名称,让用户可以在用户界面知道通知器是什么以及它可以做什么,
|
||||
还让 NotificationCenter 知道如何加载通知器和准备通知器需要的设置以发送通知。
|
||||
|
||||
```yaml
|
||||
apiVersion: notification.halo.run/v1alpha1
|
||||
apiVersion: notification.halo.run/v1alpha1
|
||||
kind: NotifierDescriptor
|
||||
metadata:
|
||||
name: email-notifier
|
||||
|
@ -261,52 +281,52 @@ spec:
|
|||
```java
|
||||
public interface ReactiveNotifier extends ExtensionPoint {
|
||||
|
||||
/**
|
||||
* Notify user.
|
||||
*
|
||||
* @param context notification context must not be null
|
||||
*/
|
||||
Mono<Void> notify(NotificationContext context);
|
||||
/**
|
||||
* Notify user.
|
||||
*
|
||||
* @param context notification context must not be null
|
||||
*/
|
||||
Mono<Void> notify(NotificationContext context);
|
||||
}
|
||||
|
||||
@Data
|
||||
public class NotificationContext {
|
||||
private Message message;
|
||||
private Message message;
|
||||
|
||||
private ObjectNode receiverConfig;
|
||||
private ObjectNode receiverConfig;
|
||||
|
||||
private ObjectNode senderConfig;
|
||||
private ObjectNode senderConfig;
|
||||
|
||||
@Data
|
||||
static class Message {
|
||||
private MessagePayload payload;
|
||||
@Data
|
||||
static class Message {
|
||||
private MessagePayload payload;
|
||||
|
||||
private Subject subject;
|
||||
private Subject subject;
|
||||
|
||||
private String recipient;
|
||||
private String recipient;
|
||||
|
||||
private Instant timestamp;
|
||||
}
|
||||
private Instant timestamp;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class Subject {
|
||||
private String apiVersion;
|
||||
private String kind;
|
||||
private String name;
|
||||
private String title;
|
||||
private String url;
|
||||
}
|
||||
@Data
|
||||
public static class Subject {
|
||||
private String apiVersion;
|
||||
private String kind;
|
||||
private String name;
|
||||
private String title;
|
||||
private String url;
|
||||
}
|
||||
|
||||
@Data
|
||||
static class MessagePayload {
|
||||
private String title;
|
||||
@Data
|
||||
static class MessagePayload {
|
||||
private String title;
|
||||
|
||||
private String rawBody;
|
||||
|
||||
private String htmlBody;
|
||||
private String rawBody;
|
||||
|
||||
private ReasonAttributes attributes;
|
||||
}
|
||||
private String htmlBody;
|
||||
|
||||
private ReasonAttributes attributes;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
@ -1 +1 @@
|
|||
version=2.15.0-SNAPSHOT
|
||||
version=2.16.0-SNAPSHOT
|
||||
|
|
|
@ -1,5 +1,18 @@
|
|||
plugins {
|
||||
id "com.github.node-gradle.node"
|
||||
id 'idea'
|
||||
id 'com.github.node-gradle.node'
|
||||
}
|
||||
|
||||
idea {
|
||||
module {
|
||||
excludeDirs += file('node_modules/')
|
||||
excludeDirs += file('packages').listFiles().collect {
|
||||
file(it.path + '/node_modules/')
|
||||
}
|
||||
excludeDirs += file('packages').listFiles().collect {
|
||||
file(it.path + '/dist/')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register('clean', Delete) {
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import {
|
||||
Dialog,
|
||||
IconEye,
|
||||
IconHistoryLine,
|
||||
IconPages,
|
||||
IconSave,
|
||||
IconSendPlaneFill,
|
||||
|
@ -452,6 +453,22 @@ async function handleUploadImage(file: File, options?: AxiosRequestConfig) {
|
|||
:allow-forced-select="!isUpdateMode"
|
||||
@select="handleChangeEditorProvider"
|
||||
/>
|
||||
<VButton
|
||||
v-if="isUpdateMode"
|
||||
size="sm"
|
||||
type="default"
|
||||
@click="
|
||||
$router.push({
|
||||
name: 'SinglePageSnapshots',
|
||||
query: { name: routeQueryName },
|
||||
})
|
||||
"
|
||||
>
|
||||
<template #icon>
|
||||
<IconHistoryLine class="h-full w-full" />
|
||||
</template>
|
||||
{{ $t("core.page_editor.actions.snapshots") }}
|
||||
</VButton>
|
||||
<VButton
|
||||
size="sm"
|
||||
type="default"
|
||||
|
|
|
@ -0,0 +1,182 @@
|
|||
<script setup lang="ts">
|
||||
import {
|
||||
Dialog,
|
||||
IconHistoryLine,
|
||||
Toast,
|
||||
VButton,
|
||||
VCard,
|
||||
VLoading,
|
||||
VPageHeader,
|
||||
VSpace,
|
||||
} from "@halo-dev/components";
|
||||
import { useQuery, useQueryClient } from "@tanstack/vue-query";
|
||||
import { useRoute } from "vue-router";
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
import { computed, watch } from "vue";
|
||||
import { OverlayScrollbarsComponent } from "overlayscrollbars-vue";
|
||||
import { useRouteQuery } from "@vueuse/router";
|
||||
import SnapshotContent from "./components/SnapshotContent.vue";
|
||||
import SnapshotListItem from "./components/SnapshotListItem.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const route = useRoute();
|
||||
const { t } = useI18n();
|
||||
|
||||
const singlePageName = computed(() => route.query.name as string);
|
||||
|
||||
const { data: singlePage } = useQuery({
|
||||
queryKey: ["singlePage-by-name", singlePageName],
|
||||
queryFn: async () => {
|
||||
const { data } =
|
||||
await apiClient.extension.singlePage.getcontentHaloRunV1alpha1SinglePage({
|
||||
name: singlePageName.value,
|
||||
});
|
||||
return data;
|
||||
},
|
||||
enabled: computed(() => !!singlePageName.value),
|
||||
});
|
||||
|
||||
const { data: snapshots, isLoading } = useQuery({
|
||||
queryKey: ["singlePage-snapshots-by-singlePage-name", singlePageName],
|
||||
queryFn: async () => {
|
||||
const { data } = await apiClient.singlePage.listSinglePageSnapshots({
|
||||
name: singlePageName.value,
|
||||
});
|
||||
return data;
|
||||
},
|
||||
refetchInterval(data) {
|
||||
const hasDeletingData = data?.some(
|
||||
(item) => !!item.metadata.deletionTimestamp
|
||||
);
|
||||
return hasDeletingData ? 1000 : false;
|
||||
},
|
||||
enabled: computed(() => !!singlePageName.value),
|
||||
});
|
||||
|
||||
const selectedSnapshotName = useRouteQuery<string | undefined>("snapshot-name");
|
||||
|
||||
watch(
|
||||
() => snapshots.value,
|
||||
(value) => {
|
||||
if (value && !selectedSnapshotName.value) {
|
||||
selectedSnapshotName.value = value[0].metadata.name;
|
||||
}
|
||||
|
||||
// Reset selectedSnapshotName if the selected snapshot is deleted
|
||||
if (
|
||||
!value?.some(
|
||||
(snapshot) => snapshot.metadata.name === selectedSnapshotName.value
|
||||
)
|
||||
) {
|
||||
selectedSnapshotName.value = value?.[0].metadata.name;
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
function handleCleanup() {
|
||||
Dialog.warning({
|
||||
title: t("core.page_snapshots.operations.cleanup.title"),
|
||||
description: t("core.page_snapshots.operations.cleanup.description"),
|
||||
confirmText: t("core.common.buttons.confirm"),
|
||||
cancelText: t("core.common.buttons.cancel"),
|
||||
async onConfirm() {
|
||||
const { releaseSnapshot, baseSnapshot, headSnapshot } =
|
||||
singlePage.value?.spec || {};
|
||||
const snapshotsToDelete = snapshots.value?.filter((snapshot) => {
|
||||
const { name } = snapshot.metadata;
|
||||
return ![releaseSnapshot, baseSnapshot, headSnapshot]
|
||||
.filter(Boolean)
|
||||
.includes(name);
|
||||
});
|
||||
|
||||
if (!snapshotsToDelete?.length) {
|
||||
Toast.info(t("core.page_snapshots.operations.cleanup.toast_empty"));
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < snapshotsToDelete?.length; i++) {
|
||||
await apiClient.singlePage.deleteSinglePageContent({
|
||||
name: singlePageName.value,
|
||||
snapshotName: snapshotsToDelete[i].metadata.name,
|
||||
});
|
||||
}
|
||||
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ["singlePage-snapshots-by-singlePage-name", singlePageName],
|
||||
});
|
||||
|
||||
Toast.success(t("core.page_snapshots.operations.cleanup.toast_success"));
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VPageHeader :title="singlePage?.spec.title">
|
||||
<template #icon>
|
||||
<IconHistoryLine class="mr-2 self-center" />
|
||||
</template>
|
||||
<template #actions>
|
||||
<VSpace>
|
||||
<VButton size="sm" @click="$router.back()">
|
||||
{{ $t("core.common.buttons.back") }}
|
||||
</VButton>
|
||||
<VButton size="sm" type="danger" @click="handleCleanup">
|
||||
{{ $t("core.page_snapshots.operations.cleanup.button") }}
|
||||
</VButton>
|
||||
</VSpace>
|
||||
</template>
|
||||
</VPageHeader>
|
||||
|
||||
<div class="m-0 md:m-4">
|
||||
<VCard
|
||||
style="height: calc(100vh - 5.5rem)"
|
||||
:body-class="['h-full', '!p-0']"
|
||||
>
|
||||
<div class="grid h-full grid-cols-12 divide-y sm:divide-x sm:divide-y-0">
|
||||
<div
|
||||
class="relative col-span-12 h-full overflow-auto sm:col-span-6 lg:col-span-3 xl:col-span-2"
|
||||
>
|
||||
<OverlayScrollbarsComponent
|
||||
element="div"
|
||||
:options="{ scrollbars: { autoHide: 'scroll' } }"
|
||||
class="h-full w-full"
|
||||
defer
|
||||
>
|
||||
<VLoading v-if="isLoading" />
|
||||
<Transition v-else appear name="fade">
|
||||
<ul
|
||||
class="box-border h-full w-full divide-y divide-gray-100"
|
||||
role="list"
|
||||
>
|
||||
<li
|
||||
v-for="snapshot in snapshots"
|
||||
:key="snapshot.metadata.name"
|
||||
@click="selectedSnapshotName = snapshot.metadata.name"
|
||||
>
|
||||
<SnapshotListItem
|
||||
:snapshot="snapshot"
|
||||
:single-page="singlePage"
|
||||
:selected-snapshot-name="selectedSnapshotName"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</Transition>
|
||||
</OverlayScrollbarsComponent>
|
||||
</div>
|
||||
<div
|
||||
class="col-span-12 h-full overflow-auto sm:col-span-6 lg:col-span-9 xl:col-span-10"
|
||||
>
|
||||
<SnapshotContent
|
||||
:single-page-name="singlePageName"
|
||||
:snapshot-name="selectedSnapshotName"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</VCard>
|
||||
</div>
|
||||
</template>
|
|
@ -117,7 +117,7 @@ const handlePublishClick = () => {
|
|||
};
|
||||
|
||||
// Fix me:
|
||||
// Force update post settings,
|
||||
// Force update singlePage settings,
|
||||
// because currently there may be errors caused by changes in version due to asynchronous processing.
|
||||
const { mutateAsync: singlePageUpdateMutate } = usePageUpdateMutate();
|
||||
|
||||
|
|
|
@ -0,0 +1,113 @@
|
|||
<script setup lang="ts">
|
||||
import { useQuery } from "@tanstack/vue-query";
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
import { computed, toRefs } from "vue";
|
||||
import { Toast, VLoading } from "@halo-dev/components";
|
||||
import { OverlayScrollbarsComponent } from "overlayscrollbars-vue";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
singlePageName?: string;
|
||||
snapshotName?: string;
|
||||
}>(),
|
||||
{
|
||||
singlePageName: undefined,
|
||||
snapshotName: undefined,
|
||||
}
|
||||
);
|
||||
|
||||
const { singlePageName, snapshotName } = toRefs(props);
|
||||
|
||||
const { data: snapshot, isLoading } = useQuery({
|
||||
queryKey: ["singlePage-snapshot-by-name", singlePageName, snapshotName],
|
||||
queryFn: async () => {
|
||||
if (!singlePageName.value || !snapshotName.value) {
|
||||
throw new Error("singlePageName and snapshotName are required");
|
||||
}
|
||||
|
||||
const { data } = await apiClient.singlePage.fetchSinglePageContent({
|
||||
name: singlePageName.value,
|
||||
snapshotName: snapshotName.value,
|
||||
});
|
||||
return data;
|
||||
},
|
||||
onError(err) {
|
||||
if (err instanceof Error) {
|
||||
Toast.error(err.message);
|
||||
}
|
||||
},
|
||||
enabled: computed(() => !!singlePageName.value && !!snapshotName.value),
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<OverlayScrollbarsComponent
|
||||
element="div"
|
||||
:options="{ scrollbars: { autoHide: 'scroll' } }"
|
||||
class="h-full w-full"
|
||||
defer
|
||||
>
|
||||
<VLoading v-if="isLoading" />
|
||||
<div
|
||||
v-else
|
||||
class="snapshot-content markdown-body h-full w-full p-4"
|
||||
v-html="snapshot?.content"
|
||||
></div>
|
||||
</OverlayScrollbarsComponent>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
::v-deep(.snapshot-content) {
|
||||
p {
|
||||
margin-top: 0.75em;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: #0d0d0d;
|
||||
padding: 0.75rem 1rem;
|
||||
margin: 0;
|
||||
|
||||
code {
|
||||
background: none;
|
||||
font-size: 0.8rem;
|
||||
padding: 0 !important;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
ul[data-type="taskList"] {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
|
||||
> label {
|
||||
flex: 0 0 auto;
|
||||
margin-right: 0.5rem;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
> div {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: disc !important;
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style: decimal !important;
|
||||
}
|
||||
|
||||
code br {
|
||||
display: initial;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,140 @@
|
|||
<script setup lang="ts">
|
||||
import type { ListedSnapshotDto, SinglePage } from "@halo-dev/api-client";
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
import { Dialog, Toast, VButton, VStatusDot, VTag } from "@halo-dev/components";
|
||||
import { useQueryClient } from "@tanstack/vue-query";
|
||||
import { computed } from "vue";
|
||||
import { relativeTimeTo } from "@/utils/date";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { t } = useI18n();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
singlePage?: SinglePage;
|
||||
snapshot: ListedSnapshotDto;
|
||||
selectedSnapshotName?: string;
|
||||
}>(),
|
||||
{
|
||||
singlePage: undefined,
|
||||
selectedSnapshotName: undefined,
|
||||
}
|
||||
);
|
||||
|
||||
async function handleRestore() {
|
||||
Dialog.warning({
|
||||
title: t("core.page_snapshots.operations.revert.title"),
|
||||
description: t("core.page_snapshots.operations.revert.description"),
|
||||
confirmText: t("core.common.buttons.confirm"),
|
||||
cancelText: t("core.common.buttons.cancel"),
|
||||
async onConfirm() {
|
||||
await apiClient.singlePage.revertToSpecifiedSnapshotForSinglePage({
|
||||
name: props.singlePage?.metadata.name as string,
|
||||
revertSnapshotForSingleParam: {
|
||||
snapshotName: props.snapshot.metadata.name,
|
||||
},
|
||||
});
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ["singlePage-snapshots-by-singlePage-name"],
|
||||
});
|
||||
Toast.success(t("core.page_snapshots.operations.revert.toast_success"));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function handleDelete() {
|
||||
Dialog.warning({
|
||||
title: t("core.page_snapshots.operations.delete.title"),
|
||||
description: t("core.page_snapshots.operations.delete.description"),
|
||||
confirmText: t("core.common.buttons.confirm"),
|
||||
cancelText: t("core.common.buttons.cancel"),
|
||||
async onConfirm() {
|
||||
await apiClient.singlePage.deleteSinglePageContent({
|
||||
name: props.singlePage?.metadata.name as string,
|
||||
snapshotName: props.snapshot.metadata.name,
|
||||
});
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ["singlePage-snapshots-by-singlePage-name"],
|
||||
});
|
||||
Toast.success(t("core.common.toast.delete_success"));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const isSelected = computed(() => {
|
||||
return props.selectedSnapshotName === props.snapshot.metadata.name;
|
||||
});
|
||||
|
||||
const isReleased = computed(() => {
|
||||
return (
|
||||
props.singlePage?.spec.releaseSnapshot === props.snapshot.metadata.name
|
||||
);
|
||||
});
|
||||
|
||||
const isHead = computed(() => {
|
||||
const { headSnapshot, releaseSnapshot } = props.singlePage?.spec || {};
|
||||
return (
|
||||
headSnapshot !== releaseSnapshot &&
|
||||
headSnapshot === props.snapshot.metadata.name
|
||||
);
|
||||
});
|
||||
|
||||
const isBase = computed(() => {
|
||||
return props.singlePage?.spec.baseSnapshot === props.snapshot.metadata.name;
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
class="group relative flex cursor-pointer flex-col gap-5 p-4"
|
||||
:class="{ 'bg-gray-50': isSelected }"
|
||||
>
|
||||
<div
|
||||
v-if="isSelected"
|
||||
class="absolute inset-y-0 left-0 w-0.5 bg-primary"
|
||||
></div>
|
||||
<div class="flex items-center justify-between">
|
||||
<div
|
||||
class="truncate text-sm"
|
||||
:class="{
|
||||
'font-semibold': isSelected,
|
||||
}"
|
||||
>
|
||||
{{ relativeTimeTo(snapshot.metadata.creationTimestamp) }}
|
||||
</div>
|
||||
<div class="inline-flex flex-none items-center space-x-3">
|
||||
<VTag v-if="isReleased" theme="primary">
|
||||
{{ $t("core.page_snapshots.status.released") }}
|
||||
</VTag>
|
||||
<VTag v-if="isHead">
|
||||
{{ $t("core.page_snapshots.status.draft") }}
|
||||
</VTag>
|
||||
<VTag v-if="isBase">
|
||||
{{ $t("core.page_snapshots.status.base") }}
|
||||
</VTag>
|
||||
<VStatusDot
|
||||
v-if="snapshot.metadata.deletionTimestamp"
|
||||
v-tooltip="$t('core.common.status.deleting')"
|
||||
state="warning"
|
||||
animate
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex h-6 items-end justify-between gap-2">
|
||||
<div class="flex-1 truncate text-xs text-gray-600">
|
||||
{{ snapshot.spec.owner }}
|
||||
</div>
|
||||
<div
|
||||
v-if="!isReleased"
|
||||
class="hidden flex-none space-x-2 group-hover:block"
|
||||
>
|
||||
<VButton v-if="!isHead" size="xs" @click="handleRestore()">
|
||||
{{ $t("core.page_snapshots.operations.revert.button") }}
|
||||
</VButton>
|
||||
<VButton v-if="!isBase" size="xs" type="danger" @click="handleDelete">
|
||||
{{ $t("core.common.buttons.delete") }}
|
||||
</VButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -35,7 +35,7 @@ export function usePageUpdateMutate() {
|
|||
},
|
||||
retry: 3,
|
||||
onError: (error) => {
|
||||
console.error("Failed to update post", error);
|
||||
console.error("Failed to update singlePage", error);
|
||||
Toast.error(t("core.common.toast.server_internal_error"));
|
||||
},
|
||||
});
|
||||
|
|
|
@ -6,6 +6,7 @@ import SinglePageEditor from "./SinglePageEditor.vue";
|
|||
import SinglePageStatsWidget from "./widgets/SinglePageStatsWidget.vue";
|
||||
import { IconPages } from "@halo-dev/components";
|
||||
import { markRaw } from "vue";
|
||||
import SinglePageSnapshots from "./SinglePageSnapshots.vue";
|
||||
|
||||
export default definePlugin({
|
||||
components: {
|
||||
|
@ -54,6 +55,17 @@ export default definePlugin({
|
|||
permissions: ["system:singlepages:manage"],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "snapshots",
|
||||
name: "SinglePageSnapshots",
|
||||
component: SinglePageSnapshots,
|
||||
meta: {
|
||||
title: "core.page_snapshots.title",
|
||||
searchable: false,
|
||||
hideFooter: true,
|
||||
permissions: ["system:singlepages:manage"],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
|
|
@ -3,6 +3,7 @@ import {
|
|||
Dialog,
|
||||
IconBookRead,
|
||||
IconEye,
|
||||
IconHistoryLine,
|
||||
IconSave,
|
||||
IconSendPlaneFill,
|
||||
IconSettings,
|
||||
|
@ -480,6 +481,19 @@ async function handleUploadImage(file: File, options?: AxiosRequestConfig) {
|
|||
:allow-forced-select="!isUpdateMode"
|
||||
@select="handleChangeEditorProvider"
|
||||
/>
|
||||
<VButton
|
||||
v-if="isUpdateMode"
|
||||
size="sm"
|
||||
type="default"
|
||||
@click="
|
||||
$router.push({ name: 'PostSnapshots', query: { name: name } })
|
||||
"
|
||||
>
|
||||
<template #icon>
|
||||
<IconHistoryLine class="h-full w-full" />
|
||||
</template>
|
||||
{{ $t("core.post_editor.actions.snapshots") }}
|
||||
</VButton>
|
||||
<VButton
|
||||
size="sm"
|
||||
type="default"
|
||||
|
|
|
@ -0,0 +1,182 @@
|
|||
<script setup lang="ts">
|
||||
import {
|
||||
Dialog,
|
||||
IconHistoryLine,
|
||||
Toast,
|
||||
VButton,
|
||||
VCard,
|
||||
VLoading,
|
||||
VPageHeader,
|
||||
VSpace,
|
||||
} from "@halo-dev/components";
|
||||
import { useQuery, useQueryClient } from "@tanstack/vue-query";
|
||||
import { useRoute } from "vue-router";
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
import { computed, watch } from "vue";
|
||||
import { OverlayScrollbarsComponent } from "overlayscrollbars-vue";
|
||||
import { useRouteQuery } from "@vueuse/router";
|
||||
import SnapshotContent from "@console/modules/contents/posts/components/SnapshotContent.vue";
|
||||
import SnapshotListItem from "@console/modules/contents/posts/components/SnapshotListItem.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { t } = useI18n();
|
||||
const route = useRoute();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const postName = computed(() => route.query.name as string);
|
||||
|
||||
const { data: post } = useQuery({
|
||||
queryKey: ["post-by-name", postName],
|
||||
queryFn: async () => {
|
||||
const { data } =
|
||||
await apiClient.extension.post.getcontentHaloRunV1alpha1Post({
|
||||
name: postName.value,
|
||||
});
|
||||
return data;
|
||||
},
|
||||
enabled: computed(() => !!postName.value),
|
||||
});
|
||||
|
||||
const { data: snapshots, isLoading } = useQuery({
|
||||
queryKey: ["post-snapshots-by-post-name", postName],
|
||||
queryFn: async () => {
|
||||
const { data } = await apiClient.post.listPostSnapshots({
|
||||
name: postName.value,
|
||||
});
|
||||
return data;
|
||||
},
|
||||
refetchInterval(data) {
|
||||
const hasDeletingData = data?.some(
|
||||
(item) => !!item.metadata.deletionTimestamp
|
||||
);
|
||||
return hasDeletingData ? 1000 : false;
|
||||
},
|
||||
enabled: computed(() => !!postName.value),
|
||||
});
|
||||
|
||||
const selectedSnapshotName = useRouteQuery<string | undefined>("snapshot-name");
|
||||
|
||||
watch(
|
||||
() => snapshots.value,
|
||||
(value) => {
|
||||
if (value && !selectedSnapshotName.value) {
|
||||
selectedSnapshotName.value = value[0].metadata.name;
|
||||
}
|
||||
|
||||
// Reset selectedSnapshotName if the selected snapshot is deleted
|
||||
if (
|
||||
!value?.some(
|
||||
(snapshot) => snapshot.metadata.name === selectedSnapshotName.value
|
||||
)
|
||||
) {
|
||||
selectedSnapshotName.value = value?.[0].metadata.name;
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
function handleCleanup() {
|
||||
Dialog.warning({
|
||||
title: t("core.post_snapshots.operations.cleanup.title"),
|
||||
description: t("core.post_snapshots.operations.cleanup.description"),
|
||||
confirmText: t("core.common.buttons.confirm"),
|
||||
cancelText: t("core.common.buttons.cancel"),
|
||||
async onConfirm() {
|
||||
const { releaseSnapshot, baseSnapshot, headSnapshot } =
|
||||
post.value?.spec || {};
|
||||
const snapshotsToDelete = snapshots.value?.filter((snapshot) => {
|
||||
const { name } = snapshot.metadata;
|
||||
return ![releaseSnapshot, baseSnapshot, headSnapshot]
|
||||
.filter(Boolean)
|
||||
.includes(name);
|
||||
});
|
||||
|
||||
if (!snapshotsToDelete?.length) {
|
||||
Toast.info(t("core.post_snapshots.operations.cleanup.toast_empty"));
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < snapshotsToDelete?.length; i++) {
|
||||
await apiClient.post.deletePostContent({
|
||||
name: postName.value,
|
||||
snapshotName: snapshotsToDelete[i].metadata.name,
|
||||
});
|
||||
}
|
||||
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ["post-snapshots-by-post-name", postName],
|
||||
});
|
||||
|
||||
Toast.success(t("core.post_snapshots.operations.cleanup.toast_success"));
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VPageHeader :title="post?.spec.title">
|
||||
<template #icon>
|
||||
<IconHistoryLine class="mr-2 self-center" />
|
||||
</template>
|
||||
<template #actions>
|
||||
<VSpace>
|
||||
<VButton size="sm" @click="$router.back()">
|
||||
{{ $t("core.common.buttons.back") }}
|
||||
</VButton>
|
||||
<VButton size="sm" type="danger" @click="handleCleanup">
|
||||
{{ $t("core.post_snapshots.operations.cleanup.button") }}
|
||||
</VButton>
|
||||
</VSpace>
|
||||
</template>
|
||||
</VPageHeader>
|
||||
|
||||
<div class="m-0 md:m-4">
|
||||
<VCard
|
||||
style="height: calc(100vh - 5.5rem)"
|
||||
:body-class="['h-full', '!p-0']"
|
||||
>
|
||||
<div class="grid h-full grid-cols-12 divide-y sm:divide-x sm:divide-y-0">
|
||||
<div
|
||||
class="relative col-span-12 h-full overflow-auto sm:col-span-6 lg:col-span-3 xl:col-span-2"
|
||||
>
|
||||
<OverlayScrollbarsComponent
|
||||
element="div"
|
||||
:options="{ scrollbars: { autoHide: 'scroll' } }"
|
||||
class="h-full w-full"
|
||||
defer
|
||||
>
|
||||
<VLoading v-if="isLoading" />
|
||||
<Transition v-else appear name="fade">
|
||||
<ul
|
||||
class="box-border h-full w-full divide-y divide-gray-100"
|
||||
role="list"
|
||||
>
|
||||
<li
|
||||
v-for="snapshot in snapshots"
|
||||
:key="snapshot.metadata.name"
|
||||
@click="selectedSnapshotName = snapshot.metadata.name"
|
||||
>
|
||||
<SnapshotListItem
|
||||
:snapshot="snapshot"
|
||||
:post="post"
|
||||
:selected-snapshot-name="selectedSnapshotName"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</Transition>
|
||||
</OverlayScrollbarsComponent>
|
||||
</div>
|
||||
<div
|
||||
class="col-span-12 h-full overflow-auto sm:col-span-6 lg:col-span-9 xl:col-span-10"
|
||||
>
|
||||
<SnapshotContent
|
||||
:post-name="postName"
|
||||
:snapshot-name="selectedSnapshotName"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</VCard>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,113 @@
|
|||
<script setup lang="ts">
|
||||
import { useQuery } from "@tanstack/vue-query";
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
import { computed, toRefs } from "vue";
|
||||
import { Toast, VLoading } from "@halo-dev/components";
|
||||
import { OverlayScrollbarsComponent } from "overlayscrollbars-vue";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
postName?: string;
|
||||
snapshotName?: string;
|
||||
}>(),
|
||||
{
|
||||
postName: undefined,
|
||||
snapshotName: undefined,
|
||||
}
|
||||
);
|
||||
|
||||
const { postName, snapshotName } = toRefs(props);
|
||||
|
||||
const { data: snapshot, isLoading } = useQuery({
|
||||
queryKey: ["post-snapshot-by-name", postName, snapshotName],
|
||||
queryFn: async () => {
|
||||
if (!postName.value || !snapshotName.value) {
|
||||
throw new Error("postName and snapshotName are required");
|
||||
}
|
||||
|
||||
const { data } = await apiClient.post.fetchPostContent({
|
||||
name: postName.value,
|
||||
snapshotName: snapshotName.value,
|
||||
});
|
||||
return data;
|
||||
},
|
||||
onError(err) {
|
||||
if (err instanceof Error) {
|
||||
Toast.error(err.message);
|
||||
}
|
||||
},
|
||||
enabled: computed(() => !!postName.value && !!snapshotName.value),
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<OverlayScrollbarsComponent
|
||||
element="div"
|
||||
:options="{ scrollbars: { autoHide: 'scroll' } }"
|
||||
class="h-full w-full"
|
||||
defer
|
||||
>
|
||||
<VLoading v-if="isLoading" />
|
||||
<div
|
||||
v-else
|
||||
class="snapshot-content markdown-body h-full w-full p-4"
|
||||
v-html="snapshot?.content"
|
||||
></div>
|
||||
</OverlayScrollbarsComponent>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
::v-deep(.snapshot-content) {
|
||||
p {
|
||||
margin-top: 0.75em;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: #0d0d0d;
|
||||
padding: 0.75rem 1rem;
|
||||
margin: 0;
|
||||
|
||||
code {
|
||||
background: none;
|
||||
font-size: 0.8rem;
|
||||
padding: 0 !important;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
ul[data-type="taskList"] {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
|
||||
> label {
|
||||
flex: 0 0 auto;
|
||||
margin-right: 0.5rem;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
> div {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: disc !important;
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style: decimal !important;
|
||||
}
|
||||
|
||||
code br {
|
||||
display: initial;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,138 @@
|
|||
<script setup lang="ts">
|
||||
import type { ListedSnapshotDto, Post } from "@halo-dev/api-client";
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
import { Dialog, Toast, VButton, VStatusDot, VTag } from "@halo-dev/components";
|
||||
import { useQueryClient } from "@tanstack/vue-query";
|
||||
import { computed } from "vue";
|
||||
import { relativeTimeTo } from "@/utils/date";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { t } = useI18n();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
post?: Post;
|
||||
snapshot: ListedSnapshotDto;
|
||||
selectedSnapshotName?: string;
|
||||
}>(),
|
||||
{
|
||||
post: undefined,
|
||||
selectedSnapshotName: undefined,
|
||||
}
|
||||
);
|
||||
|
||||
async function handleRestore() {
|
||||
Dialog.warning({
|
||||
title: t("core.post_snapshots.operations.revert.title"),
|
||||
description: t("core.post_snapshots.operations.revert.description"),
|
||||
confirmText: t("core.common.buttons.confirm"),
|
||||
cancelText: t("core.common.buttons.cancel"),
|
||||
async onConfirm() {
|
||||
await apiClient.post.revertToSpecifiedSnapshotForPost({
|
||||
name: props.post?.metadata.name as string,
|
||||
revertSnapshotForPostParam: {
|
||||
snapshotName: props.snapshot.metadata.name,
|
||||
},
|
||||
});
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ["post-snapshots-by-post-name"],
|
||||
});
|
||||
Toast.success(t("core.post_snapshots.operations.revert.toast_success"));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function handleDelete() {
|
||||
Dialog.warning({
|
||||
title: t("core.post_snapshots.operations.delete.title"),
|
||||
description: t("core.post_snapshots.operations.delete.description"),
|
||||
confirmText: t("core.common.buttons.confirm"),
|
||||
cancelText: t("core.common.buttons.cancel"),
|
||||
async onConfirm() {
|
||||
await apiClient.post.deletePostContent({
|
||||
name: props.post?.metadata.name as string,
|
||||
snapshotName: props.snapshot.metadata.name,
|
||||
});
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ["post-snapshots-by-post-name"],
|
||||
});
|
||||
Toast.success(t("core.common.toast.delete_success"));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const isSelected = computed(() => {
|
||||
return props.selectedSnapshotName === props.snapshot.metadata.name;
|
||||
});
|
||||
|
||||
const isReleased = computed(() => {
|
||||
return props.post?.spec.releaseSnapshot === props.snapshot.metadata.name;
|
||||
});
|
||||
|
||||
const isHead = computed(() => {
|
||||
const { headSnapshot, releaseSnapshot } = props.post?.spec || {};
|
||||
return (
|
||||
headSnapshot !== releaseSnapshot &&
|
||||
headSnapshot === props.snapshot.metadata.name
|
||||
);
|
||||
});
|
||||
|
||||
const isBase = computed(() => {
|
||||
return props.post?.spec.baseSnapshot === props.snapshot.metadata.name;
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
class="group relative flex cursor-pointer flex-col gap-5 p-4"
|
||||
:class="{ 'bg-gray-50': isSelected }"
|
||||
>
|
||||
<div
|
||||
v-if="isSelected"
|
||||
class="absolute inset-y-0 left-0 w-0.5 bg-primary"
|
||||
></div>
|
||||
<div class="flex items-center justify-between">
|
||||
<div
|
||||
class="truncate text-sm"
|
||||
:class="{
|
||||
'font-semibold': isSelected,
|
||||
}"
|
||||
>
|
||||
{{ relativeTimeTo(snapshot.metadata.creationTimestamp) }}
|
||||
</div>
|
||||
<div class="inline-flex flex-none items-center space-x-3">
|
||||
<VTag v-if="isReleased" theme="primary">
|
||||
{{ $t("core.post_snapshots.status.released") }}
|
||||
</VTag>
|
||||
<VTag v-if="isHead">
|
||||
{{ $t("core.post_snapshots.status.draft") }}
|
||||
</VTag>
|
||||
<VTag v-if="isBase">
|
||||
{{ $t("core.post_snapshots.status.base") }}
|
||||
</VTag>
|
||||
<VStatusDot
|
||||
v-if="snapshot.metadata.deletionTimestamp"
|
||||
v-tooltip="$t('core.common.status.deleting')"
|
||||
state="warning"
|
||||
animate
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex h-6 items-end justify-between gap-2">
|
||||
<div class="flex-1 truncate text-xs text-gray-600">
|
||||
{{ snapshot.spec.owner }}
|
||||
</div>
|
||||
<div
|
||||
v-if="!isReleased"
|
||||
class="hidden flex-none space-x-2 group-hover:block"
|
||||
>
|
||||
<VButton v-if="!isHead" size="xs" @click="handleRestore()">
|
||||
{{ $t("core.post_snapshots.operations.revert.button") }}
|
||||
</VButton>
|
||||
<VButton v-if="!isBase" size="xs" type="danger" @click="handleDelete">
|
||||
{{ $t("core.common.buttons.delete") }}
|
||||
</VButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -10,6 +10,7 @@ import TagList from "./tags/TagList.vue";
|
|||
import PostStatsWidget from "./widgets/PostStatsWidget.vue";
|
||||
import RecentPublishedWidget from "./widgets/RecentPublishedWidget.vue";
|
||||
import { markRaw } from "vue";
|
||||
import PostSnapshots from "./PostSnapshots.vue";
|
||||
|
||||
export default definePlugin({
|
||||
components: {
|
||||
|
@ -60,6 +61,17 @@ export default definePlugin({
|
|||
permissions: ["system:posts:manage"],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "snapshots",
|
||||
name: "PostSnapshots",
|
||||
component: PostSnapshots,
|
||||
meta: {
|
||||
title: "core.post_snapshots.title",
|
||||
searchable: false,
|
||||
hideFooter: true,
|
||||
permissions: ["system:posts:manage"],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "categories",
|
||||
component: BlankLayout,
|
||||
|
|
|
@ -154,14 +154,11 @@ const { startFields, endFields } = useEntityFieldItemExtensionPoint<Plugin>(
|
|||
!enabled || (enabled && phase === PluginStatusPhaseEnum.Started);
|
||||
|
||||
const getStatusDotState = () => {
|
||||
if (
|
||||
enabled &&
|
||||
phase !==
|
||||
(PluginStatusPhaseEnum.Started || PluginStatusPhaseEnum.Failed)
|
||||
) {
|
||||
return "default";
|
||||
if (enabled && phase === PluginStatusPhaseEnum.Failed) {
|
||||
return "error";
|
||||
}
|
||||
return "error";
|
||||
|
||||
return "default";
|
||||
};
|
||||
|
||||
return [
|
||||
|
|
|
@ -33,16 +33,8 @@ export function usePluginLifeCycle(
|
|||
const { enabled } = plugin.value.spec || {};
|
||||
const { phase } = plugin.value.status || {};
|
||||
|
||||
// Starting up
|
||||
if (
|
||||
enabled &&
|
||||
phase !== (PluginStatusPhaseEnum.Started || PluginStatusPhaseEnum.Failed)
|
||||
) {
|
||||
return t("core.common.status.starting_up");
|
||||
}
|
||||
|
||||
// Starting failed
|
||||
if (!isStarted.value) {
|
||||
if (enabled && phase === PluginStatusPhaseEnum.Failed) {
|
||||
const lastCondition = plugin.value.status?.conditions?.[0];
|
||||
|
||||
return (
|
||||
|
@ -51,6 +43,14 @@ export function usePluginLifeCycle(
|
|||
.join(":") || "Unknown"
|
||||
);
|
||||
}
|
||||
|
||||
// Starting up
|
||||
if (
|
||||
enabled &&
|
||||
phase !== (PluginStatusPhaseEnum.Started || PluginStatusPhaseEnum.Failed)
|
||||
) {
|
||||
return t("core.common.status.starting_up");
|
||||
}
|
||||
};
|
||||
|
||||
const { isLoading: changingStatus, mutate: changeStatus } = useMutation({
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@halo-dev/api-client",
|
||||
"version": "2.15.0",
|
||||
"version": "2.16.0",
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"build": "unbuild",
|
||||
|
|
|
@ -120,6 +120,7 @@ models/condition.ts
|
|||
models/config-map-list.ts
|
||||
models/config-map-ref.ts
|
||||
models/config-map.ts
|
||||
models/content-update-param.ts
|
||||
models/content-vo.ts
|
||||
models/content-wrapper.ts
|
||||
models/content.ts
|
||||
|
@ -168,6 +169,8 @@ models/listed-single-page-list.ts
|
|||
models/listed-single-page-vo-list.ts
|
||||
models/listed-single-page-vo.ts
|
||||
models/listed-single-page.ts
|
||||
models/listed-snapshot-dto.ts
|
||||
models/listed-snapshot-spec.ts
|
||||
models/listed-user.ts
|
||||
models/login-history.ts
|
||||
models/mark-specified-request.ts
|
||||
|
@ -240,6 +243,7 @@ models/register-verify-email-request.ts
|
|||
models/reply-list.ts
|
||||
models/reply-request.ts
|
||||
models/reply-spec.ts
|
||||
models/reply-status.ts
|
||||
models/reply-vo-list.ts
|
||||
models/reply-vo.ts
|
||||
models/reply.ts
|
||||
|
@ -247,6 +251,8 @@ models/reset-password-request.ts
|
|||
models/reverse-proxy-list.ts
|
||||
models/reverse-proxy-rule.ts
|
||||
models/reverse-proxy.ts
|
||||
models/revert-snapshot-for-post-param.ts
|
||||
models/revert-snapshot-for-single-param.ts
|
||||
models/role-binding-list.ts
|
||||
models/role-binding.ts
|
||||
models/role-list.ts
|
||||
|
|
|
@ -28,15 +28,67 @@ import { ContentWrapper } from '../models';
|
|||
// @ts-ignore
|
||||
import { ListedPostList } from '../models';
|
||||
// @ts-ignore
|
||||
import { ListedSnapshotDto } from '../models';
|
||||
// @ts-ignore
|
||||
import { Post } from '../models';
|
||||
// @ts-ignore
|
||||
import { PostRequest } from '../models';
|
||||
// @ts-ignore
|
||||
import { RevertSnapshotForPostParam } from '../models';
|
||||
/**
|
||||
* ApiConsoleHaloRunV1alpha1PostApi - axios parameter creator
|
||||
* @export
|
||||
*/
|
||||
export const ApiConsoleHaloRunV1alpha1PostApiAxiosParamCreator = function (configuration?: Configuration) {
|
||||
return {
|
||||
/**
|
||||
* Delete a content for post.
|
||||
* @param {string} name
|
||||
* @param {string} snapshotName
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
deletePostContent: async (name: string, snapshotName: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'name' is not null or undefined
|
||||
assertParamExists('deletePostContent', 'name', name)
|
||||
// verify required parameter 'snapshotName' is not null or undefined
|
||||
assertParamExists('deletePostContent', 'snapshotName', snapshotName)
|
||||
const localVarPath = `/apis/api.console.halo.run/v1alpha1/posts/{name}/content`
|
||||
.replace(`{${"name"}}`, encodeURIComponent(String(name)));
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication BasicAuth required
|
||||
// http basic authentication required
|
||||
setBasicAuthToObject(localVarRequestOptions, configuration)
|
||||
|
||||
// authentication BearerAuth required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
if (snapshotName !== undefined) {
|
||||
localVarQueryParameter['snapshotName'] = snapshotName;
|
||||
}
|
||||
|
||||
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Draft a post.
|
||||
* @param {PostRequest} postRequest
|
||||
|
@ -80,6 +132,54 @@ export const ApiConsoleHaloRunV1alpha1PostApiAxiosParamCreator = function (confi
|
|||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Fetch content of post.
|
||||
* @param {string} name
|
||||
* @param {string} snapshotName
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
fetchPostContent: async (name: string, snapshotName: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'name' is not null or undefined
|
||||
assertParamExists('fetchPostContent', 'name', name)
|
||||
// verify required parameter 'snapshotName' is not null or undefined
|
||||
assertParamExists('fetchPostContent', 'snapshotName', snapshotName)
|
||||
const localVarPath = `/apis/api.console.halo.run/v1alpha1/posts/{name}/content`
|
||||
.replace(`{${"name"}}`, encodeURIComponent(String(name)));
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication BasicAuth required
|
||||
// http basic authentication required
|
||||
setBasicAuthToObject(localVarRequestOptions, configuration)
|
||||
|
||||
// authentication BearerAuth required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
if (snapshotName !== undefined) {
|
||||
localVarQueryParameter['snapshotName'] = snapshotName;
|
||||
}
|
||||
|
||||
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Fetch head content of post.
|
||||
* @param {string} name
|
||||
|
@ -153,6 +253,47 @@ export const ApiConsoleHaloRunV1alpha1PostApiAxiosParamCreator = function (confi
|
|||
|
||||
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* List all snapshots for post content.
|
||||
* @param {string} name
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
listPostSnapshots: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'name' is not null or undefined
|
||||
assertParamExists('listPostSnapshots', 'name', name)
|
||||
const localVarPath = `/apis/api.console.halo.run/v1alpha1/posts/{name}/snapshot`
|
||||
.replace(`{${"name"}}`, encodeURIComponent(String(name)));
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication BasicAuth required
|
||||
// http basic authentication required
|
||||
setBasicAuthToObject(localVarRequestOptions, configuration)
|
||||
|
||||
// authentication BearerAuth required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
@ -321,6 +462,53 @@ export const ApiConsoleHaloRunV1alpha1PostApiAxiosParamCreator = function (confi
|
|||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Revert to specified snapshot for post content.
|
||||
* @param {string} name
|
||||
* @param {RevertSnapshotForPostParam} revertSnapshotForPostParam
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
revertToSpecifiedSnapshotForPost: async (name: string, revertSnapshotForPostParam: RevertSnapshotForPostParam, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'name' is not null or undefined
|
||||
assertParamExists('revertToSpecifiedSnapshotForPost', 'name', name)
|
||||
// verify required parameter 'revertSnapshotForPostParam' is not null or undefined
|
||||
assertParamExists('revertToSpecifiedSnapshotForPost', 'revertSnapshotForPostParam', revertSnapshotForPostParam)
|
||||
const localVarPath = `/apis/api.console.halo.run/v1alpha1/posts/{name}/revert-content`
|
||||
.replace(`{${"name"}}`, encodeURIComponent(String(name)));
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication BasicAuth required
|
||||
// http basic authentication required
|
||||
setBasicAuthToObject(localVarRequestOptions, configuration)
|
||||
|
||||
// authentication BearerAuth required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
|
||||
|
||||
localVarHeaderParameter['Content-Type'] = 'application/json';
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
localVarRequestOptions.data = serializeDataIfNeeded(revertSnapshotForPostParam, localVarRequestOptions, configuration)
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Publish a post.
|
||||
* @param {string} name
|
||||
|
@ -466,6 +654,19 @@ export const ApiConsoleHaloRunV1alpha1PostApiAxiosParamCreator = function (confi
|
|||
export const ApiConsoleHaloRunV1alpha1PostApiFp = function(configuration?: Configuration) {
|
||||
const localVarAxiosParamCreator = ApiConsoleHaloRunV1alpha1PostApiAxiosParamCreator(configuration)
|
||||
return {
|
||||
/**
|
||||
* Delete a content for post.
|
||||
* @param {string} name
|
||||
* @param {string} snapshotName
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async deletePostContent(name: string, snapshotName: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ContentWrapper>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.deletePostContent(name, snapshotName, options);
|
||||
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
|
||||
const localVarOperationServerBasePath = operationServerMap['ApiConsoleHaloRunV1alpha1PostApi.deletePostContent']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
* Draft a post.
|
||||
* @param {PostRequest} postRequest
|
||||
|
@ -478,6 +679,19 @@ export const ApiConsoleHaloRunV1alpha1PostApiFp = function(configuration?: Confi
|
|||
const localVarOperationServerBasePath = operationServerMap['ApiConsoleHaloRunV1alpha1PostApi.draftPost']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
* Fetch content of post.
|
||||
* @param {string} name
|
||||
* @param {string} snapshotName
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async fetchPostContent(name: string, snapshotName: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ContentWrapper>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.fetchPostContent(name, snapshotName, options);
|
||||
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
|
||||
const localVarOperationServerBasePath = operationServerMap['ApiConsoleHaloRunV1alpha1PostApi.fetchPostContent']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
* Fetch head content of post.
|
||||
* @param {string} name
|
||||
|
@ -502,6 +716,18 @@ export const ApiConsoleHaloRunV1alpha1PostApiFp = function(configuration?: Confi
|
|||
const localVarOperationServerBasePath = operationServerMap['ApiConsoleHaloRunV1alpha1PostApi.fetchPostReleaseContent']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
* List all snapshots for post content.
|
||||
* @param {string} name
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async listPostSnapshots(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<ListedSnapshotDto>>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.listPostSnapshots(name, options);
|
||||
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
|
||||
const localVarOperationServerBasePath = operationServerMap['ApiConsoleHaloRunV1alpha1PostApi.listPostSnapshots']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
* List posts.
|
||||
* @param {number} [page] Page number. Default is 0.
|
||||
|
@ -545,6 +771,19 @@ export const ApiConsoleHaloRunV1alpha1PostApiFp = function(configuration?: Confi
|
|||
const localVarOperationServerBasePath = operationServerMap['ApiConsoleHaloRunV1alpha1PostApi.recyclePost']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
* Revert to specified snapshot for post content.
|
||||
* @param {string} name
|
||||
* @param {RevertSnapshotForPostParam} revertSnapshotForPostParam
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async revertToSpecifiedSnapshotForPost(name: string, revertSnapshotForPostParam: RevertSnapshotForPostParam, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Post>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.revertToSpecifiedSnapshotForPost(name, revertSnapshotForPostParam, options);
|
||||
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
|
||||
const localVarOperationServerBasePath = operationServerMap['ApiConsoleHaloRunV1alpha1PostApi.revertToSpecifiedSnapshotForPost']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
* Publish a post.
|
||||
* @param {string} name
|
||||
|
@ -593,6 +832,15 @@ export const ApiConsoleHaloRunV1alpha1PostApiFp = function(configuration?: Confi
|
|||
export const ApiConsoleHaloRunV1alpha1PostApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
|
||||
const localVarFp = ApiConsoleHaloRunV1alpha1PostApiFp(configuration)
|
||||
return {
|
||||
/**
|
||||
* Delete a content for post.
|
||||
* @param {ApiConsoleHaloRunV1alpha1PostApiDeletePostContentRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
deletePostContent(requestParameters: ApiConsoleHaloRunV1alpha1PostApiDeletePostContentRequest, options?: RawAxiosRequestConfig): AxiosPromise<ContentWrapper> {
|
||||
return localVarFp.deletePostContent(requestParameters.name, requestParameters.snapshotName, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
* Draft a post.
|
||||
* @param {ApiConsoleHaloRunV1alpha1PostApiDraftPostRequest} requestParameters Request parameters.
|
||||
|
@ -602,6 +850,15 @@ export const ApiConsoleHaloRunV1alpha1PostApiFactory = function (configuration?:
|
|||
draftPost(requestParameters: ApiConsoleHaloRunV1alpha1PostApiDraftPostRequest, options?: RawAxiosRequestConfig): AxiosPromise<Post> {
|
||||
return localVarFp.draftPost(requestParameters.postRequest, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
* Fetch content of post.
|
||||
* @param {ApiConsoleHaloRunV1alpha1PostApiFetchPostContentRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
fetchPostContent(requestParameters: ApiConsoleHaloRunV1alpha1PostApiFetchPostContentRequest, options?: RawAxiosRequestConfig): AxiosPromise<ContentWrapper> {
|
||||
return localVarFp.fetchPostContent(requestParameters.name, requestParameters.snapshotName, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
* Fetch head content of post.
|
||||
* @param {ApiConsoleHaloRunV1alpha1PostApiFetchPostHeadContentRequest} requestParameters Request parameters.
|
||||
|
@ -620,6 +877,15 @@ export const ApiConsoleHaloRunV1alpha1PostApiFactory = function (configuration?:
|
|||
fetchPostReleaseContent(requestParameters: ApiConsoleHaloRunV1alpha1PostApiFetchPostReleaseContentRequest, options?: RawAxiosRequestConfig): AxiosPromise<ContentWrapper> {
|
||||
return localVarFp.fetchPostReleaseContent(requestParameters.name, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
* List all snapshots for post content.
|
||||
* @param {ApiConsoleHaloRunV1alpha1PostApiListPostSnapshotsRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
listPostSnapshots(requestParameters: ApiConsoleHaloRunV1alpha1PostApiListPostSnapshotsRequest, options?: RawAxiosRequestConfig): AxiosPromise<Array<ListedSnapshotDto>> {
|
||||
return localVarFp.listPostSnapshots(requestParameters.name, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
* List posts.
|
||||
* @param {ApiConsoleHaloRunV1alpha1PostApiListPostsRequest} requestParameters Request parameters.
|
||||
|
@ -647,6 +913,15 @@ export const ApiConsoleHaloRunV1alpha1PostApiFactory = function (configuration?:
|
|||
recyclePost(requestParameters: ApiConsoleHaloRunV1alpha1PostApiRecyclePostRequest, options?: RawAxiosRequestConfig): AxiosPromise<void> {
|
||||
return localVarFp.recyclePost(requestParameters.name, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
* Revert to specified snapshot for post content.
|
||||
* @param {ApiConsoleHaloRunV1alpha1PostApiRevertToSpecifiedSnapshotForPostRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
revertToSpecifiedSnapshotForPost(requestParameters: ApiConsoleHaloRunV1alpha1PostApiRevertToSpecifiedSnapshotForPostRequest, options?: RawAxiosRequestConfig): AxiosPromise<Post> {
|
||||
return localVarFp.revertToSpecifiedSnapshotForPost(requestParameters.name, requestParameters.revertSnapshotForPostParam, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
* Publish a post.
|
||||
* @param {ApiConsoleHaloRunV1alpha1PostApiUnpublishPostRequest} requestParameters Request parameters.
|
||||
|
@ -677,6 +952,27 @@ export const ApiConsoleHaloRunV1alpha1PostApiFactory = function (configuration?:
|
|||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Request parameters for deletePostContent operation in ApiConsoleHaloRunV1alpha1PostApi.
|
||||
* @export
|
||||
* @interface ApiConsoleHaloRunV1alpha1PostApiDeletePostContentRequest
|
||||
*/
|
||||
export interface ApiConsoleHaloRunV1alpha1PostApiDeletePostContentRequest {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ApiConsoleHaloRunV1alpha1PostApiDeletePostContent
|
||||
*/
|
||||
readonly name: string
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ApiConsoleHaloRunV1alpha1PostApiDeletePostContent
|
||||
*/
|
||||
readonly snapshotName: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameters for draftPost operation in ApiConsoleHaloRunV1alpha1PostApi.
|
||||
* @export
|
||||
|
@ -691,6 +987,27 @@ export interface ApiConsoleHaloRunV1alpha1PostApiDraftPostRequest {
|
|||
readonly postRequest: PostRequest
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameters for fetchPostContent operation in ApiConsoleHaloRunV1alpha1PostApi.
|
||||
* @export
|
||||
* @interface ApiConsoleHaloRunV1alpha1PostApiFetchPostContentRequest
|
||||
*/
|
||||
export interface ApiConsoleHaloRunV1alpha1PostApiFetchPostContentRequest {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ApiConsoleHaloRunV1alpha1PostApiFetchPostContent
|
||||
*/
|
||||
readonly name: string
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ApiConsoleHaloRunV1alpha1PostApiFetchPostContent
|
||||
*/
|
||||
readonly snapshotName: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameters for fetchPostHeadContent operation in ApiConsoleHaloRunV1alpha1PostApi.
|
||||
* @export
|
||||
|
@ -719,6 +1036,20 @@ export interface ApiConsoleHaloRunV1alpha1PostApiFetchPostReleaseContentRequest
|
|||
readonly name: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameters for listPostSnapshots operation in ApiConsoleHaloRunV1alpha1PostApi.
|
||||
* @export
|
||||
* @interface ApiConsoleHaloRunV1alpha1PostApiListPostSnapshotsRequest
|
||||
*/
|
||||
export interface ApiConsoleHaloRunV1alpha1PostApiListPostSnapshotsRequest {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ApiConsoleHaloRunV1alpha1PostApiListPostSnapshots
|
||||
*/
|
||||
readonly name: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameters for listPosts operation in ApiConsoleHaloRunV1alpha1PostApi.
|
||||
* @export
|
||||
|
@ -810,6 +1141,27 @@ export interface ApiConsoleHaloRunV1alpha1PostApiRecyclePostRequest {
|
|||
readonly name: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameters for revertToSpecifiedSnapshotForPost operation in ApiConsoleHaloRunV1alpha1PostApi.
|
||||
* @export
|
||||
* @interface ApiConsoleHaloRunV1alpha1PostApiRevertToSpecifiedSnapshotForPostRequest
|
||||
*/
|
||||
export interface ApiConsoleHaloRunV1alpha1PostApiRevertToSpecifiedSnapshotForPostRequest {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ApiConsoleHaloRunV1alpha1PostApiRevertToSpecifiedSnapshotForPost
|
||||
*/
|
||||
readonly name: string
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {RevertSnapshotForPostParam}
|
||||
* @memberof ApiConsoleHaloRunV1alpha1PostApiRevertToSpecifiedSnapshotForPost
|
||||
*/
|
||||
readonly revertSnapshotForPostParam: RevertSnapshotForPostParam
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameters for unpublishPost operation in ApiConsoleHaloRunV1alpha1PostApi.
|
||||
* @export
|
||||
|
@ -873,6 +1225,17 @@ export interface ApiConsoleHaloRunV1alpha1PostApiUpdatePostContentRequest {
|
|||
* @extends {BaseAPI}
|
||||
*/
|
||||
export class ApiConsoleHaloRunV1alpha1PostApi extends BaseAPI {
|
||||
/**
|
||||
* Delete a content for post.
|
||||
* @param {ApiConsoleHaloRunV1alpha1PostApiDeletePostContentRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof ApiConsoleHaloRunV1alpha1PostApi
|
||||
*/
|
||||
public deletePostContent(requestParameters: ApiConsoleHaloRunV1alpha1PostApiDeletePostContentRequest, options?: RawAxiosRequestConfig) {
|
||||
return ApiConsoleHaloRunV1alpha1PostApiFp(this.configuration).deletePostContent(requestParameters.name, requestParameters.snapshotName, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Draft a post.
|
||||
* @param {ApiConsoleHaloRunV1alpha1PostApiDraftPostRequest} requestParameters Request parameters.
|
||||
|
@ -884,6 +1247,17 @@ export class ApiConsoleHaloRunV1alpha1PostApi extends BaseAPI {
|
|||
return ApiConsoleHaloRunV1alpha1PostApiFp(this.configuration).draftPost(requestParameters.postRequest, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch content of post.
|
||||
* @param {ApiConsoleHaloRunV1alpha1PostApiFetchPostContentRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof ApiConsoleHaloRunV1alpha1PostApi
|
||||
*/
|
||||
public fetchPostContent(requestParameters: ApiConsoleHaloRunV1alpha1PostApiFetchPostContentRequest, options?: RawAxiosRequestConfig) {
|
||||
return ApiConsoleHaloRunV1alpha1PostApiFp(this.configuration).fetchPostContent(requestParameters.name, requestParameters.snapshotName, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch head content of post.
|
||||
* @param {ApiConsoleHaloRunV1alpha1PostApiFetchPostHeadContentRequest} requestParameters Request parameters.
|
||||
|
@ -906,6 +1280,17 @@ export class ApiConsoleHaloRunV1alpha1PostApi extends BaseAPI {
|
|||
return ApiConsoleHaloRunV1alpha1PostApiFp(this.configuration).fetchPostReleaseContent(requestParameters.name, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* List all snapshots for post content.
|
||||
* @param {ApiConsoleHaloRunV1alpha1PostApiListPostSnapshotsRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof ApiConsoleHaloRunV1alpha1PostApi
|
||||
*/
|
||||
public listPostSnapshots(requestParameters: ApiConsoleHaloRunV1alpha1PostApiListPostSnapshotsRequest, options?: RawAxiosRequestConfig) {
|
||||
return ApiConsoleHaloRunV1alpha1PostApiFp(this.configuration).listPostSnapshots(requestParameters.name, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* List posts.
|
||||
* @param {ApiConsoleHaloRunV1alpha1PostApiListPostsRequest} requestParameters Request parameters.
|
||||
|
@ -939,6 +1324,17 @@ export class ApiConsoleHaloRunV1alpha1PostApi extends BaseAPI {
|
|||
return ApiConsoleHaloRunV1alpha1PostApiFp(this.configuration).recyclePost(requestParameters.name, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Revert to specified snapshot for post content.
|
||||
* @param {ApiConsoleHaloRunV1alpha1PostApiRevertToSpecifiedSnapshotForPostRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof ApiConsoleHaloRunV1alpha1PostApi
|
||||
*/
|
||||
public revertToSpecifiedSnapshotForPost(requestParameters: ApiConsoleHaloRunV1alpha1PostApiRevertToSpecifiedSnapshotForPostRequest, options?: RawAxiosRequestConfig) {
|
||||
return ApiConsoleHaloRunV1alpha1PostApiFp(this.configuration).revertToSpecifiedSnapshotForPost(requestParameters.name, requestParameters.revertSnapshotForPostParam, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish a post.
|
||||
* @param {ApiConsoleHaloRunV1alpha1PostApiUnpublishPostRequest} requestParameters Request parameters.
|
||||
|
|
|
@ -28,8 +28,12 @@ import { ContentWrapper } from '../models';
|
|||
// @ts-ignore
|
||||
import { ListedSinglePageList } from '../models';
|
||||
// @ts-ignore
|
||||
import { ListedSnapshotDto } from '../models';
|
||||
// @ts-ignore
|
||||
import { Post } from '../models';
|
||||
// @ts-ignore
|
||||
import { RevertSnapshotForSingleParam } from '../models';
|
||||
// @ts-ignore
|
||||
import { SinglePage } from '../models';
|
||||
// @ts-ignore
|
||||
import { SinglePageRequest } from '../models';
|
||||
|
@ -39,6 +43,54 @@ import { SinglePageRequest } from '../models';
|
|||
*/
|
||||
export const ApiConsoleHaloRunV1alpha1SinglePageApiAxiosParamCreator = function (configuration?: Configuration) {
|
||||
return {
|
||||
/**
|
||||
* Delete a content for post.
|
||||
* @param {string} name
|
||||
* @param {string} snapshotName
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
deleteSinglePageContent: async (name: string, snapshotName: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'name' is not null or undefined
|
||||
assertParamExists('deleteSinglePageContent', 'name', name)
|
||||
// verify required parameter 'snapshotName' is not null or undefined
|
||||
assertParamExists('deleteSinglePageContent', 'snapshotName', snapshotName)
|
||||
const localVarPath = `/apis/api.console.halo.run/v1alpha1/singlepages/{name}/content`
|
||||
.replace(`{${"name"}}`, encodeURIComponent(String(name)));
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication BasicAuth required
|
||||
// http basic authentication required
|
||||
setBasicAuthToObject(localVarRequestOptions, configuration)
|
||||
|
||||
// authentication BearerAuth required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
if (snapshotName !== undefined) {
|
||||
localVarQueryParameter['snapshotName'] = snapshotName;
|
||||
}
|
||||
|
||||
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Draft a single page.
|
||||
* @param {SinglePageRequest} singlePageRequest
|
||||
|
@ -82,6 +134,54 @@ export const ApiConsoleHaloRunV1alpha1SinglePageApiAxiosParamCreator = function
|
|||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Fetch content of single page.
|
||||
* @param {string} name
|
||||
* @param {string} snapshotName
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
fetchSinglePageContent: async (name: string, snapshotName: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'name' is not null or undefined
|
||||
assertParamExists('fetchSinglePageContent', 'name', name)
|
||||
// verify required parameter 'snapshotName' is not null or undefined
|
||||
assertParamExists('fetchSinglePageContent', 'snapshotName', snapshotName)
|
||||
const localVarPath = `/apis/api.console.halo.run/v1alpha1/singlepages/{name}/content`
|
||||
.replace(`{${"name"}}`, encodeURIComponent(String(name)));
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication BasicAuth required
|
||||
// http basic authentication required
|
||||
setBasicAuthToObject(localVarRequestOptions, configuration)
|
||||
|
||||
// authentication BearerAuth required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
if (snapshotName !== undefined) {
|
||||
localVarQueryParameter['snapshotName'] = snapshotName;
|
||||
}
|
||||
|
||||
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Fetch head content of single page.
|
||||
* @param {string} name
|
||||
|
@ -155,6 +255,47 @@ export const ApiConsoleHaloRunV1alpha1SinglePageApiAxiosParamCreator = function
|
|||
|
||||
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* List all snapshots for single page content.
|
||||
* @param {string} name
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
listSinglePageSnapshots: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'name' is not null or undefined
|
||||
assertParamExists('listSinglePageSnapshots', 'name', name)
|
||||
const localVarPath = `/apis/api.console.halo.run/v1alpha1/singlepages/{name}/snapshot`
|
||||
.replace(`{${"name"}}`, encodeURIComponent(String(name)));
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication BasicAuth required
|
||||
// http basic authentication required
|
||||
setBasicAuthToObject(localVarRequestOptions, configuration)
|
||||
|
||||
// authentication BearerAuth required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
@ -287,6 +428,53 @@ export const ApiConsoleHaloRunV1alpha1SinglePageApiAxiosParamCreator = function
|
|||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Revert to specified snapshot for single page content.
|
||||
* @param {string} name
|
||||
* @param {RevertSnapshotForSingleParam} revertSnapshotForSingleParam
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
revertToSpecifiedSnapshotForSinglePage: async (name: string, revertSnapshotForSingleParam: RevertSnapshotForSingleParam, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'name' is not null or undefined
|
||||
assertParamExists('revertToSpecifiedSnapshotForSinglePage', 'name', name)
|
||||
// verify required parameter 'revertSnapshotForSingleParam' is not null or undefined
|
||||
assertParamExists('revertToSpecifiedSnapshotForSinglePage', 'revertSnapshotForSingleParam', revertSnapshotForSingleParam)
|
||||
const localVarPath = `/apis/api.console.halo.run/v1alpha1/singlepages/{name}/revert-content`
|
||||
.replace(`{${"name"}}`, encodeURIComponent(String(name)));
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication BasicAuth required
|
||||
// http basic authentication required
|
||||
setBasicAuthToObject(localVarRequestOptions, configuration)
|
||||
|
||||
// authentication BearerAuth required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
|
||||
|
||||
localVarHeaderParameter['Content-Type'] = 'application/json';
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
localVarRequestOptions.data = serializeDataIfNeeded(revertSnapshotForSingleParam, localVarRequestOptions, configuration)
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Update a single page.
|
||||
* @param {string} name
|
||||
|
@ -391,6 +579,19 @@ export const ApiConsoleHaloRunV1alpha1SinglePageApiAxiosParamCreator = function
|
|||
export const ApiConsoleHaloRunV1alpha1SinglePageApiFp = function(configuration?: Configuration) {
|
||||
const localVarAxiosParamCreator = ApiConsoleHaloRunV1alpha1SinglePageApiAxiosParamCreator(configuration)
|
||||
return {
|
||||
/**
|
||||
* Delete a content for post.
|
||||
* @param {string} name
|
||||
* @param {string} snapshotName
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async deleteSinglePageContent(name: string, snapshotName: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ContentWrapper>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.deleteSinglePageContent(name, snapshotName, options);
|
||||
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
|
||||
const localVarOperationServerBasePath = operationServerMap['ApiConsoleHaloRunV1alpha1SinglePageApi.deleteSinglePageContent']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
* Draft a single page.
|
||||
* @param {SinglePageRequest} singlePageRequest
|
||||
|
@ -403,6 +604,19 @@ export const ApiConsoleHaloRunV1alpha1SinglePageApiFp = function(configuration?:
|
|||
const localVarOperationServerBasePath = operationServerMap['ApiConsoleHaloRunV1alpha1SinglePageApi.draftSinglePage']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
* Fetch content of single page.
|
||||
* @param {string} name
|
||||
* @param {string} snapshotName
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async fetchSinglePageContent(name: string, snapshotName: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ContentWrapper>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.fetchSinglePageContent(name, snapshotName, options);
|
||||
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
|
||||
const localVarOperationServerBasePath = operationServerMap['ApiConsoleHaloRunV1alpha1SinglePageApi.fetchSinglePageContent']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
* Fetch head content of single page.
|
||||
* @param {string} name
|
||||
|
@ -427,6 +641,18 @@ export const ApiConsoleHaloRunV1alpha1SinglePageApiFp = function(configuration?:
|
|||
const localVarOperationServerBasePath = operationServerMap['ApiConsoleHaloRunV1alpha1SinglePageApi.fetchSinglePageReleaseContent']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
* List all snapshots for single page content.
|
||||
* @param {string} name
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async listSinglePageSnapshots(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<ListedSnapshotDto>>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.listSinglePageSnapshots(name, options);
|
||||
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
|
||||
const localVarOperationServerBasePath = operationServerMap['ApiConsoleHaloRunV1alpha1SinglePageApi.listSinglePageSnapshots']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
* List single pages.
|
||||
* @param {number} [page] Page number. Default is 0.
|
||||
|
@ -459,6 +685,19 @@ export const ApiConsoleHaloRunV1alpha1SinglePageApiFp = function(configuration?:
|
|||
const localVarOperationServerBasePath = operationServerMap['ApiConsoleHaloRunV1alpha1SinglePageApi.publishSinglePage']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
* Revert to specified snapshot for single page content.
|
||||
* @param {string} name
|
||||
* @param {RevertSnapshotForSingleParam} revertSnapshotForSingleParam
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async revertToSpecifiedSnapshotForSinglePage(name: string, revertSnapshotForSingleParam: RevertSnapshotForSingleParam, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Post>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.revertToSpecifiedSnapshotForSinglePage(name, revertSnapshotForSingleParam, options);
|
||||
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
|
||||
const localVarOperationServerBasePath = operationServerMap['ApiConsoleHaloRunV1alpha1SinglePageApi.revertToSpecifiedSnapshotForSinglePage']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
* Update a single page.
|
||||
* @param {string} name
|
||||
|
@ -495,6 +734,15 @@ export const ApiConsoleHaloRunV1alpha1SinglePageApiFp = function(configuration?:
|
|||
export const ApiConsoleHaloRunV1alpha1SinglePageApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
|
||||
const localVarFp = ApiConsoleHaloRunV1alpha1SinglePageApiFp(configuration)
|
||||
return {
|
||||
/**
|
||||
* Delete a content for post.
|
||||
* @param {ApiConsoleHaloRunV1alpha1SinglePageApiDeleteSinglePageContentRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
deleteSinglePageContent(requestParameters: ApiConsoleHaloRunV1alpha1SinglePageApiDeleteSinglePageContentRequest, options?: RawAxiosRequestConfig): AxiosPromise<ContentWrapper> {
|
||||
return localVarFp.deleteSinglePageContent(requestParameters.name, requestParameters.snapshotName, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
* Draft a single page.
|
||||
* @param {ApiConsoleHaloRunV1alpha1SinglePageApiDraftSinglePageRequest} requestParameters Request parameters.
|
||||
|
@ -504,6 +752,15 @@ export const ApiConsoleHaloRunV1alpha1SinglePageApiFactory = function (configura
|
|||
draftSinglePage(requestParameters: ApiConsoleHaloRunV1alpha1SinglePageApiDraftSinglePageRequest, options?: RawAxiosRequestConfig): AxiosPromise<SinglePage> {
|
||||
return localVarFp.draftSinglePage(requestParameters.singlePageRequest, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
* Fetch content of single page.
|
||||
* @param {ApiConsoleHaloRunV1alpha1SinglePageApiFetchSinglePageContentRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
fetchSinglePageContent(requestParameters: ApiConsoleHaloRunV1alpha1SinglePageApiFetchSinglePageContentRequest, options?: RawAxiosRequestConfig): AxiosPromise<ContentWrapper> {
|
||||
return localVarFp.fetchSinglePageContent(requestParameters.name, requestParameters.snapshotName, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
* Fetch head content of single page.
|
||||
* @param {ApiConsoleHaloRunV1alpha1SinglePageApiFetchSinglePageHeadContentRequest} requestParameters Request parameters.
|
||||
|
@ -522,6 +779,15 @@ export const ApiConsoleHaloRunV1alpha1SinglePageApiFactory = function (configura
|
|||
fetchSinglePageReleaseContent(requestParameters: ApiConsoleHaloRunV1alpha1SinglePageApiFetchSinglePageReleaseContentRequest, options?: RawAxiosRequestConfig): AxiosPromise<ContentWrapper> {
|
||||
return localVarFp.fetchSinglePageReleaseContent(requestParameters.name, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
* List all snapshots for single page content.
|
||||
* @param {ApiConsoleHaloRunV1alpha1SinglePageApiListSinglePageSnapshotsRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
listSinglePageSnapshots(requestParameters: ApiConsoleHaloRunV1alpha1SinglePageApiListSinglePageSnapshotsRequest, options?: RawAxiosRequestConfig): AxiosPromise<Array<ListedSnapshotDto>> {
|
||||
return localVarFp.listSinglePageSnapshots(requestParameters.name, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
* List single pages.
|
||||
* @param {ApiConsoleHaloRunV1alpha1SinglePageApiListSinglePagesRequest} requestParameters Request parameters.
|
||||
|
@ -540,6 +806,15 @@ export const ApiConsoleHaloRunV1alpha1SinglePageApiFactory = function (configura
|
|||
publishSinglePage(requestParameters: ApiConsoleHaloRunV1alpha1SinglePageApiPublishSinglePageRequest, options?: RawAxiosRequestConfig): AxiosPromise<SinglePage> {
|
||||
return localVarFp.publishSinglePage(requestParameters.name, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
* Revert to specified snapshot for single page content.
|
||||
* @param {ApiConsoleHaloRunV1alpha1SinglePageApiRevertToSpecifiedSnapshotForSinglePageRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
revertToSpecifiedSnapshotForSinglePage(requestParameters: ApiConsoleHaloRunV1alpha1SinglePageApiRevertToSpecifiedSnapshotForSinglePageRequest, options?: RawAxiosRequestConfig): AxiosPromise<Post> {
|
||||
return localVarFp.revertToSpecifiedSnapshotForSinglePage(requestParameters.name, requestParameters.revertSnapshotForSingleParam, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
* Update a single page.
|
||||
* @param {ApiConsoleHaloRunV1alpha1SinglePageApiUpdateDraftSinglePageRequest} requestParameters Request parameters.
|
||||
|
@ -561,6 +836,27 @@ export const ApiConsoleHaloRunV1alpha1SinglePageApiFactory = function (configura
|
|||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Request parameters for deleteSinglePageContent operation in ApiConsoleHaloRunV1alpha1SinglePageApi.
|
||||
* @export
|
||||
* @interface ApiConsoleHaloRunV1alpha1SinglePageApiDeleteSinglePageContentRequest
|
||||
*/
|
||||
export interface ApiConsoleHaloRunV1alpha1SinglePageApiDeleteSinglePageContentRequest {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ApiConsoleHaloRunV1alpha1SinglePageApiDeleteSinglePageContent
|
||||
*/
|
||||
readonly name: string
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ApiConsoleHaloRunV1alpha1SinglePageApiDeleteSinglePageContent
|
||||
*/
|
||||
readonly snapshotName: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameters for draftSinglePage operation in ApiConsoleHaloRunV1alpha1SinglePageApi.
|
||||
* @export
|
||||
|
@ -575,6 +871,27 @@ export interface ApiConsoleHaloRunV1alpha1SinglePageApiDraftSinglePageRequest {
|
|||
readonly singlePageRequest: SinglePageRequest
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameters for fetchSinglePageContent operation in ApiConsoleHaloRunV1alpha1SinglePageApi.
|
||||
* @export
|
||||
* @interface ApiConsoleHaloRunV1alpha1SinglePageApiFetchSinglePageContentRequest
|
||||
*/
|
||||
export interface ApiConsoleHaloRunV1alpha1SinglePageApiFetchSinglePageContentRequest {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ApiConsoleHaloRunV1alpha1SinglePageApiFetchSinglePageContent
|
||||
*/
|
||||
readonly name: string
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ApiConsoleHaloRunV1alpha1SinglePageApiFetchSinglePageContent
|
||||
*/
|
||||
readonly snapshotName: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameters for fetchSinglePageHeadContent operation in ApiConsoleHaloRunV1alpha1SinglePageApi.
|
||||
* @export
|
||||
|
@ -603,6 +920,20 @@ export interface ApiConsoleHaloRunV1alpha1SinglePageApiFetchSinglePageReleaseCon
|
|||
readonly name: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameters for listSinglePageSnapshots operation in ApiConsoleHaloRunV1alpha1SinglePageApi.
|
||||
* @export
|
||||
* @interface ApiConsoleHaloRunV1alpha1SinglePageApiListSinglePageSnapshotsRequest
|
||||
*/
|
||||
export interface ApiConsoleHaloRunV1alpha1SinglePageApiListSinglePageSnapshotsRequest {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ApiConsoleHaloRunV1alpha1SinglePageApiListSinglePageSnapshots
|
||||
*/
|
||||
readonly name: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameters for listSinglePages operation in ApiConsoleHaloRunV1alpha1SinglePageApi.
|
||||
* @export
|
||||
|
@ -687,6 +1018,27 @@ export interface ApiConsoleHaloRunV1alpha1SinglePageApiPublishSinglePageRequest
|
|||
readonly name: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameters for revertToSpecifiedSnapshotForSinglePage operation in ApiConsoleHaloRunV1alpha1SinglePageApi.
|
||||
* @export
|
||||
* @interface ApiConsoleHaloRunV1alpha1SinglePageApiRevertToSpecifiedSnapshotForSinglePageRequest
|
||||
*/
|
||||
export interface ApiConsoleHaloRunV1alpha1SinglePageApiRevertToSpecifiedSnapshotForSinglePageRequest {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ApiConsoleHaloRunV1alpha1SinglePageApiRevertToSpecifiedSnapshotForSinglePage
|
||||
*/
|
||||
readonly name: string
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {RevertSnapshotForSingleParam}
|
||||
* @memberof ApiConsoleHaloRunV1alpha1SinglePageApiRevertToSpecifiedSnapshotForSinglePage
|
||||
*/
|
||||
readonly revertSnapshotForSingleParam: RevertSnapshotForSingleParam
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameters for updateDraftSinglePage operation in ApiConsoleHaloRunV1alpha1SinglePageApi.
|
||||
* @export
|
||||
|
@ -736,6 +1088,17 @@ export interface ApiConsoleHaloRunV1alpha1SinglePageApiUpdateSinglePageContentRe
|
|||
* @extends {BaseAPI}
|
||||
*/
|
||||
export class ApiConsoleHaloRunV1alpha1SinglePageApi extends BaseAPI {
|
||||
/**
|
||||
* Delete a content for post.
|
||||
* @param {ApiConsoleHaloRunV1alpha1SinglePageApiDeleteSinglePageContentRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof ApiConsoleHaloRunV1alpha1SinglePageApi
|
||||
*/
|
||||
public deleteSinglePageContent(requestParameters: ApiConsoleHaloRunV1alpha1SinglePageApiDeleteSinglePageContentRequest, options?: RawAxiosRequestConfig) {
|
||||
return ApiConsoleHaloRunV1alpha1SinglePageApiFp(this.configuration).deleteSinglePageContent(requestParameters.name, requestParameters.snapshotName, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Draft a single page.
|
||||
* @param {ApiConsoleHaloRunV1alpha1SinglePageApiDraftSinglePageRequest} requestParameters Request parameters.
|
||||
|
@ -747,6 +1110,17 @@ export class ApiConsoleHaloRunV1alpha1SinglePageApi extends BaseAPI {
|
|||
return ApiConsoleHaloRunV1alpha1SinglePageApiFp(this.configuration).draftSinglePage(requestParameters.singlePageRequest, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch content of single page.
|
||||
* @param {ApiConsoleHaloRunV1alpha1SinglePageApiFetchSinglePageContentRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof ApiConsoleHaloRunV1alpha1SinglePageApi
|
||||
*/
|
||||
public fetchSinglePageContent(requestParameters: ApiConsoleHaloRunV1alpha1SinglePageApiFetchSinglePageContentRequest, options?: RawAxiosRequestConfig) {
|
||||
return ApiConsoleHaloRunV1alpha1SinglePageApiFp(this.configuration).fetchSinglePageContent(requestParameters.name, requestParameters.snapshotName, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch head content of single page.
|
||||
* @param {ApiConsoleHaloRunV1alpha1SinglePageApiFetchSinglePageHeadContentRequest} requestParameters Request parameters.
|
||||
|
@ -769,6 +1143,17 @@ export class ApiConsoleHaloRunV1alpha1SinglePageApi extends BaseAPI {
|
|||
return ApiConsoleHaloRunV1alpha1SinglePageApiFp(this.configuration).fetchSinglePageReleaseContent(requestParameters.name, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* List all snapshots for single page content.
|
||||
* @param {ApiConsoleHaloRunV1alpha1SinglePageApiListSinglePageSnapshotsRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof ApiConsoleHaloRunV1alpha1SinglePageApi
|
||||
*/
|
||||
public listSinglePageSnapshots(requestParameters: ApiConsoleHaloRunV1alpha1SinglePageApiListSinglePageSnapshotsRequest, options?: RawAxiosRequestConfig) {
|
||||
return ApiConsoleHaloRunV1alpha1SinglePageApiFp(this.configuration).listSinglePageSnapshots(requestParameters.name, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* List single pages.
|
||||
* @param {ApiConsoleHaloRunV1alpha1SinglePageApiListSinglePagesRequest} requestParameters Request parameters.
|
||||
|
@ -791,6 +1176,17 @@ export class ApiConsoleHaloRunV1alpha1SinglePageApi extends BaseAPI {
|
|||
return ApiConsoleHaloRunV1alpha1SinglePageApiFp(this.configuration).publishSinglePage(requestParameters.name, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Revert to specified snapshot for single page content.
|
||||
* @param {ApiConsoleHaloRunV1alpha1SinglePageApiRevertToSpecifiedSnapshotForSinglePageRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof ApiConsoleHaloRunV1alpha1SinglePageApi
|
||||
*/
|
||||
public revertToSpecifiedSnapshotForSinglePage(requestParameters: ApiConsoleHaloRunV1alpha1SinglePageApiRevertToSpecifiedSnapshotForSinglePageRequest, options?: RawAxiosRequestConfig) {
|
||||
return ApiConsoleHaloRunV1alpha1SinglePageApiFp(this.configuration).revertToSpecifiedSnapshotForSinglePage(requestParameters.name, requestParameters.revertSnapshotForSingleParam, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a single page.
|
||||
* @param {ApiConsoleHaloRunV1alpha1SinglePageApiUpdateDraftSinglePageRequest} requestParameters Request parameters.
|
||||
|
|
|
@ -31,16 +31,16 @@ export const ApiConsoleHaloRunV1alpha1TagApiAxiosParamCreator = function (config
|
|||
return {
|
||||
/**
|
||||
* List Post Tags.
|
||||
* @param {Array<string>} [fieldSelector] Field selector for filtering.
|
||||
* @param {string} [keyword] Keyword for searching.
|
||||
* @param {Array<string>} [labelSelector] Label selector for filtering.
|
||||
* @param {number} [page] The page number. Zero indicates no page.
|
||||
* @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, name
|
||||
* @param {number} [page] Page number. Default is 0.
|
||||
* @param {number} [size] Size number. Default is 0.
|
||||
* @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!=true
|
||||
* @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name==halo
|
||||
* @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.
|
||||
* @param {string} [keyword] Post tags filtered by keyword.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
listPostTags: async (fieldSelector?: Array<string>, keyword?: string, labelSelector?: Array<string>, page?: number, size?: number, sort?: Array<string>, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
listPostTags: async (page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, keyword?: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
const localVarPath = `/apis/api.console.halo.run/v1alpha1/tags`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
|
@ -61,18 +61,6 @@ export const ApiConsoleHaloRunV1alpha1TagApiAxiosParamCreator = function (config
|
|||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
if (fieldSelector) {
|
||||
localVarQueryParameter['fieldSelector'] = fieldSelector;
|
||||
}
|
||||
|
||||
if (keyword !== undefined) {
|
||||
localVarQueryParameter['keyword'] = keyword;
|
||||
}
|
||||
|
||||
if (labelSelector) {
|
||||
localVarQueryParameter['labelSelector'] = labelSelector;
|
||||
}
|
||||
|
||||
if (page !== undefined) {
|
||||
localVarQueryParameter['page'] = page;
|
||||
}
|
||||
|
@ -81,8 +69,20 @@ export const ApiConsoleHaloRunV1alpha1TagApiAxiosParamCreator = function (config
|
|||
localVarQueryParameter['size'] = size;
|
||||
}
|
||||
|
||||
if (labelSelector) {
|
||||
localVarQueryParameter['labelSelector'] = labelSelector;
|
||||
}
|
||||
|
||||
if (fieldSelector) {
|
||||
localVarQueryParameter['fieldSelector'] = fieldSelector;
|
||||
}
|
||||
|
||||
if (sort) {
|
||||
localVarQueryParameter['sort'] = Array.from(sort);
|
||||
localVarQueryParameter['sort'] = sort;
|
||||
}
|
||||
|
||||
if (keyword !== undefined) {
|
||||
localVarQueryParameter['keyword'] = keyword;
|
||||
}
|
||||
|
||||
|
||||
|
@ -108,17 +108,17 @@ export const ApiConsoleHaloRunV1alpha1TagApiFp = function(configuration?: Config
|
|||
return {
|
||||
/**
|
||||
* List Post Tags.
|
||||
* @param {Array<string>} [fieldSelector] Field selector for filtering.
|
||||
* @param {string} [keyword] Keyword for searching.
|
||||
* @param {Array<string>} [labelSelector] Label selector for filtering.
|
||||
* @param {number} [page] The page number. Zero indicates no page.
|
||||
* @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, name
|
||||
* @param {number} [page] Page number. Default is 0.
|
||||
* @param {number} [size] Size number. Default is 0.
|
||||
* @param {Array<string>} [labelSelector] Label selector. e.g.: hidden!=true
|
||||
* @param {Array<string>} [fieldSelector] Field selector. e.g.: metadata.name==halo
|
||||
* @param {Array<string>} [sort] Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.
|
||||
* @param {string} [keyword] Post tags filtered by keyword.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async listPostTags(fieldSelector?: Array<string>, keyword?: string, labelSelector?: Array<string>, page?: number, size?: number, sort?: Array<string>, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<TagList>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.listPostTags(fieldSelector, keyword, labelSelector, page, size, sort, options);
|
||||
async listPostTags(page?: number, size?: number, labelSelector?: Array<string>, fieldSelector?: Array<string>, sort?: Array<string>, keyword?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<TagList>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.listPostTags(page, size, labelSelector, fieldSelector, sort, keyword, options);
|
||||
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
|
||||
const localVarOperationServerBasePath = operationServerMap['ApiConsoleHaloRunV1alpha1TagApi.listPostTags']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
|
@ -140,7 +140,7 @@ export const ApiConsoleHaloRunV1alpha1TagApiFactory = function (configuration?:
|
|||
* @throws {RequiredError}
|
||||
*/
|
||||
listPostTags(requestParameters: ApiConsoleHaloRunV1alpha1TagApiListPostTagsRequest = {}, options?: RawAxiosRequestConfig): AxiosPromise<TagList> {
|
||||
return localVarFp.listPostTags(requestParameters.fieldSelector, requestParameters.keyword, requestParameters.labelSelector, requestParameters.page, requestParameters.size, requestParameters.sort, options).then((request) => request(axios, basePath));
|
||||
return localVarFp.listPostTags(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, requestParameters.keyword, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
@ -152,46 +152,46 @@ export const ApiConsoleHaloRunV1alpha1TagApiFactory = function (configuration?:
|
|||
*/
|
||||
export interface ApiConsoleHaloRunV1alpha1TagApiListPostTagsRequest {
|
||||
/**
|
||||
* Field selector for filtering.
|
||||
* @type {Array<string>}
|
||||
* @memberof ApiConsoleHaloRunV1alpha1TagApiListPostTags
|
||||
*/
|
||||
readonly fieldSelector?: Array<string>
|
||||
|
||||
/**
|
||||
* Keyword for searching.
|
||||
* @type {string}
|
||||
* @memberof ApiConsoleHaloRunV1alpha1TagApiListPostTags
|
||||
*/
|
||||
readonly keyword?: string
|
||||
|
||||
/**
|
||||
* Label selector for filtering.
|
||||
* @type {Array<string>}
|
||||
* @memberof ApiConsoleHaloRunV1alpha1TagApiListPostTags
|
||||
*/
|
||||
readonly labelSelector?: Array<string>
|
||||
|
||||
/**
|
||||
* The page number. Zero indicates no page.
|
||||
* Page number. Default is 0.
|
||||
* @type {number}
|
||||
* @memberof ApiConsoleHaloRunV1alpha1TagApiListPostTags
|
||||
*/
|
||||
readonly page?: number
|
||||
|
||||
/**
|
||||
* Size of one page. Zero indicates no limit.
|
||||
* Size number. Default is 0.
|
||||
* @type {number}
|
||||
* @memberof ApiConsoleHaloRunV1alpha1TagApiListPostTags
|
||||
*/
|
||||
readonly size?: number
|
||||
|
||||
/**
|
||||
* Sort property and direction of the list result. Supported fields: creationTimestamp, name
|
||||
* Label selector. e.g.: hidden!=true
|
||||
* @type {Array<string>}
|
||||
* @memberof ApiConsoleHaloRunV1alpha1TagApiListPostTags
|
||||
*/
|
||||
readonly labelSelector?: Array<string>
|
||||
|
||||
/**
|
||||
* Field selector. e.g.: metadata.name==halo
|
||||
* @type {Array<string>}
|
||||
* @memberof ApiConsoleHaloRunV1alpha1TagApiListPostTags
|
||||
*/
|
||||
readonly fieldSelector?: Array<string>
|
||||
|
||||
/**
|
||||
* Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported.
|
||||
* @type {Array<string>}
|
||||
* @memberof ApiConsoleHaloRunV1alpha1TagApiListPostTags
|
||||
*/
|
||||
readonly sort?: Array<string>
|
||||
|
||||
/**
|
||||
* Post tags filtered by keyword.
|
||||
* @type {string}
|
||||
* @memberof ApiConsoleHaloRunV1alpha1TagApiListPostTags
|
||||
*/
|
||||
readonly keyword?: string
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -209,7 +209,7 @@ export class ApiConsoleHaloRunV1alpha1TagApi extends BaseAPI {
|
|||
* @memberof ApiConsoleHaloRunV1alpha1TagApi
|
||||
*/
|
||||
public listPostTags(requestParameters: ApiConsoleHaloRunV1alpha1TagApiListPostTagsRequest = {}, options?: RawAxiosRequestConfig) {
|
||||
return ApiConsoleHaloRunV1alpha1TagApiFp(this.configuration).listPostTags(requestParameters.fieldSelector, requestParameters.keyword, requestParameters.labelSelector, requestParameters.page, requestParameters.size, requestParameters.sort, options).then((request) => request(this.axios, this.basePath));
|
||||
return ApiConsoleHaloRunV1alpha1TagApiFp(this.configuration).listPostTags(requestParameters.page, requestParameters.size, requestParameters.labelSelector, requestParameters.fieldSelector, requestParameters.sort, requestParameters.keyword, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -32,6 +32,12 @@ export interface CommentStatus {
|
|||
* @memberof CommentStatus
|
||||
*/
|
||||
'lastReplyTime'?: string;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof CommentStatus
|
||||
*/
|
||||
'observedVersion'?: number;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Halo Next API
|
||||
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
|
||||
*
|
||||
* The version of the OpenAPI document: 2.0.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface ContentUpdateParam
|
||||
*/
|
||||
export interface ContentUpdateParam {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ContentUpdateParam
|
||||
*/
|
||||
'content'?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ContentUpdateParam
|
||||
*/
|
||||
'raw'?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ContentUpdateParam
|
||||
*/
|
||||
'rawType'?: string;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof ContentUpdateParam
|
||||
*/
|
||||
'version'?: number;
|
||||
}
|
||||
|
|
@ -39,6 +39,7 @@ export * from './config-map';
|
|||
export * from './config-map-list';
|
||||
export * from './config-map-ref';
|
||||
export * from './content';
|
||||
export * from './content-update-param';
|
||||
export * from './content-vo';
|
||||
export * from './content-wrapper';
|
||||
export * from './contributor';
|
||||
|
@ -85,6 +86,8 @@ export * from './listed-single-page';
|
|||
export * from './listed-single-page-list';
|
||||
export * from './listed-single-page-vo';
|
||||
export * from './listed-single-page-vo-list';
|
||||
export * from './listed-snapshot-dto';
|
||||
export * from './listed-snapshot-spec';
|
||||
export * from './listed-user';
|
||||
export * from './login-history';
|
||||
export * from './mark-specified-request';
|
||||
|
@ -158,12 +161,15 @@ export * from './reply';
|
|||
export * from './reply-list';
|
||||
export * from './reply-request';
|
||||
export * from './reply-spec';
|
||||
export * from './reply-status';
|
||||
export * from './reply-vo';
|
||||
export * from './reply-vo-list';
|
||||
export * from './reset-password-request';
|
||||
export * from './reverse-proxy';
|
||||
export * from './reverse-proxy-list';
|
||||
export * from './reverse-proxy-rule';
|
||||
export * from './revert-snapshot-for-post-param';
|
||||
export * from './revert-snapshot-for-single-param';
|
||||
export * from './role';
|
||||
export * from './role-binding';
|
||||
export * from './role-binding-list';
|
||||
|
|
|
@ -23,6 +23,12 @@ import { InterestReasonSubject } from './interest-reason-subject';
|
|||
* @interface InterestReason
|
||||
*/
|
||||
export interface InterestReason {
|
||||
/**
|
||||
* The expression to be interested in
|
||||
* @type {string}
|
||||
* @memberof InterestReason
|
||||
*/
|
||||
'expression'?: string;
|
||||
/**
|
||||
* The name of the reason definition to be interested in
|
||||
* @type {string}
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Halo Next API
|
||||
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
|
||||
*
|
||||
* The version of the OpenAPI document: 2.0.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import { ListedSnapshotSpec } from './listed-snapshot-spec';
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import { Metadata } from './metadata';
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface ListedSnapshotDto
|
||||
*/
|
||||
export interface ListedSnapshotDto {
|
||||
/**
|
||||
*
|
||||
* @type {Metadata}
|
||||
* @memberof ListedSnapshotDto
|
||||
*/
|
||||
'metadata': Metadata;
|
||||
/**
|
||||
*
|
||||
* @type {ListedSnapshotSpec}
|
||||
* @memberof ListedSnapshotDto
|
||||
*/
|
||||
'spec': ListedSnapshotSpec;
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Halo Next API
|
||||
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
|
||||
*
|
||||
* The version of the OpenAPI document: 2.0.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface ListedSnapshotSpec
|
||||
*/
|
||||
export interface ListedSnapshotSpec {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ListedSnapshotSpec
|
||||
*/
|
||||
'modifyTime'?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ListedSnapshotSpec
|
||||
*/
|
||||
'owner': string;
|
||||
}
|
||||
|
|
@ -79,8 +79,7 @@ export const PluginStatusLastProbeStateEnum = {
|
|||
Resolved: 'RESOLVED',
|
||||
Started: 'STARTED',
|
||||
Stopped: 'STOPPED',
|
||||
Failed: 'FAILED',
|
||||
Unloaded: 'UNLOADED'
|
||||
Failed: 'FAILED'
|
||||
} as const;
|
||||
|
||||
export type PluginStatusLastProbeStateEnum = typeof PluginStatusLastProbeStateEnum[keyof typeof PluginStatusLastProbeStateEnum];
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import { Content } from './content';
|
||||
import { ContentUpdateParam } from './content-update-param';
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import { Post } from './post';
|
||||
|
@ -28,10 +28,10 @@ import { Post } from './post';
|
|||
export interface PostRequest {
|
||||
/**
|
||||
*
|
||||
* @type {Content}
|
||||
* @type {ContentUpdateParam}
|
||||
* @memberof PostRequest
|
||||
*/
|
||||
'content'?: Content;
|
||||
'content'?: ContentUpdateParam;
|
||||
/**
|
||||
*
|
||||
* @type {Post}
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Halo Next API
|
||||
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
|
||||
*
|
||||
* The version of the OpenAPI document: 2.0.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface ReplyStatus
|
||||
*/
|
||||
export interface ReplyStatus {
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof ReplyStatus
|
||||
*/
|
||||
'observedVersion'?: number;
|
||||
}
|
||||
|
|
@ -19,6 +19,9 @@ import { Metadata } from './metadata';
|
|||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import { ReplySpec } from './reply-spec';
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import { ReplyStatus } from './reply-status';
|
||||
|
||||
/**
|
||||
*
|
||||
|
@ -50,5 +53,11 @@ export interface Reply {
|
|||
* @memberof Reply
|
||||
*/
|
||||
'spec': ReplySpec;
|
||||
/**
|
||||
*
|
||||
* @type {ReplyStatus}
|
||||
* @memberof Reply
|
||||
*/
|
||||
'status': ReplyStatus;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Halo Next API
|
||||
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
|
||||
*
|
||||
* The version of the OpenAPI document: 2.0.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface RevertSnapshotForPostParam
|
||||
*/
|
||||
export interface RevertSnapshotForPostParam {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof RevertSnapshotForPostParam
|
||||
*/
|
||||
'snapshotName': string;
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Halo Next API
|
||||
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
|
||||
*
|
||||
* The version of the OpenAPI document: 2.0.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface RevertSnapshotForSingleParam
|
||||
*/
|
||||
export interface RevertSnapshotForSingleParam {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof RevertSnapshotForSingleParam
|
||||
*/
|
||||
'snapshotName': string;
|
||||
}
|
||||
|
|
@ -15,7 +15,7 @@
|
|||
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import { Content } from './content';
|
||||
import { ContentUpdateParam } from './content-update-param';
|
||||
// May contain unused imports in some cases
|
||||
// @ts-ignore
|
||||
import { SinglePage } from './single-page';
|
||||
|
@ -28,10 +28,10 @@ import { SinglePage } from './single-page';
|
|||
export interface SinglePageRequest {
|
||||
/**
|
||||
*
|
||||
* @type {Content}
|
||||
* @type {ContentUpdateParam}
|
||||
* @memberof SinglePageRequest
|
||||
*/
|
||||
'content': Content;
|
||||
'content': ContentUpdateParam;
|
||||
/**
|
||||
*
|
||||
* @type {SinglePage}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue