mirror of https://github.com/fantasticit/think.git
feat: document version support
parent
dbd257caa6
commit
d84603c54f
|
@ -58,6 +58,8 @@ CREATE DATABASE `think` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_c
|
||||||
|
|
||||||
#### 可选:Redis
|
#### 可选:Redis
|
||||||
|
|
||||||
|
如果需要文档版本服务,请在 `@think/config` 的 `yaml` 配置中进行 `db.redis` 的配置。
|
||||||
|
|
||||||
```
|
```
|
||||||
docker pull redis:latest
|
docker pull redis:latest
|
||||||
docker run --name think-redis -p 6379:6379 -d redis --appendonly yes --requirepass "root"
|
docker run --name think-redis -p 6379:6379 -d redis --appendonly yes --requirepass "root"
|
||||||
|
|
|
@ -21,7 +21,7 @@ import { DataRender } from 'components/data-render';
|
||||||
import { joinUser } from 'components/document/collaboration';
|
import { joinUser } from 'components/document/collaboration';
|
||||||
import { Banner } from 'components/banner';
|
import { Banner } from 'components/banner';
|
||||||
import { debounce } from 'helpers/debounce';
|
import { debounce } from 'helpers/debounce';
|
||||||
import { changeTitle } from './index';
|
import { em, changeTitle, USE_DATA_VERSION } from './index';
|
||||||
import styles from './index.module.scss';
|
import styles from './index.module.scss';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
|
@ -64,7 +64,7 @@ export const Editor: React.FC<IProps> = ({ user, documentId, authority, classNam
|
||||||
const title = transaction.doc.content.firstChild.content.firstChild.textContent;
|
const title = transaction.doc.content.firstChild.content.firstChild.textContent;
|
||||||
changeTitle(title);
|
changeTitle(title);
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}, 200),
|
}, 50),
|
||||||
});
|
});
|
||||||
const [loading, toggleLoading] = useToggle(true);
|
const [loading, toggleLoading] = useToggle(true);
|
||||||
|
|
||||||
|
@ -89,6 +89,16 @@ export const Editor: React.FC<IProps> = ({ user, documentId, authority, classNam
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!editor) return;
|
||||||
|
const handler = (data) => editor.commands.setContent(data);
|
||||||
|
em.on(USE_DATA_VERSION, handler);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
em.off(USE_DATA_VERSION, handler);
|
||||||
|
};
|
||||||
|
}, [editor]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DataRender
|
<DataRender
|
||||||
loading={loading}
|
loading={loading}
|
||||||
|
|
|
@ -20,13 +20,18 @@ import styles from './index.module.scss';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
const em = new EventEmitter();
|
export const em = new EventEmitter();
|
||||||
const TITLE_CHANGE_EVENT = 'TITLE_CHANGE_EVENT';
|
const TITLE_CHANGE_EVENT = 'TITLE_CHANGE_EVENT';
|
||||||
|
export const USE_DATA_VERSION = 'USE_DATA_VERSION';
|
||||||
|
|
||||||
export const changeTitle = (title) => {
|
export const changeTitle = (title) => {
|
||||||
em.emit(TITLE_CHANGE_EVENT, title);
|
em.emit(TITLE_CHANGE_EVENT, title);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const useVersion = (data) => {
|
||||||
|
em.emit(USE_DATA_VERSION, data);
|
||||||
|
};
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
documentId: string;
|
documentId: string;
|
||||||
}
|
}
|
||||||
|
@ -90,7 +95,7 @@ export const DocumentEditor: React.FC<IProps> = ({ documentId }) => {
|
||||||
<DocumentCollaboration key="collaboration" wikiId={document.wikiId} documentId={documentId} />
|
<DocumentCollaboration key="collaboration" wikiId={document.wikiId} documentId={documentId} />
|
||||||
)}
|
)}
|
||||||
<DocumentShare key="share" documentId={documentId} />
|
<DocumentShare key="share" documentId={documentId} />
|
||||||
<DocumentVersion key="version" documentId={documentId} />
|
<DocumentVersion key="version" documentId={documentId} onSelect={useVersion} />
|
||||||
<DocumentStar key="star" documentId={documentId} />
|
<DocumentStar key="star" documentId={documentId} />
|
||||||
<Popover key="style" zIndex={1061} position="bottomLeft" content={<DocumentStyle />}>
|
<Popover key="style" zIndex={1061} position="bottomLeft" content={<DocumentStyle />}>
|
||||||
<Button icon={<IconArticle />} theme="borderless" type="tertiary" />
|
<Button icon={<IconArticle />} theme="borderless" type="tertiary" />
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { DocumentShare } from 'components/document/share';
|
||||||
import { DocumentStar } from 'components/document/star';
|
import { DocumentStar } from 'components/document/star';
|
||||||
import { DocumentCollaboration } from 'components/document/collaboration';
|
import { DocumentCollaboration } from 'components/document/collaboration';
|
||||||
import { DocumentStyle } from 'components/document/style';
|
import { DocumentStyle } from 'components/document/style';
|
||||||
|
import { DocumentVersion } from 'components/document/version';
|
||||||
import { CommentEditor } from 'components/document/comments';
|
import { CommentEditor } from 'components/document/comments';
|
||||||
import { useDocumentStyle } from 'hooks/use-document-style';
|
import { useDocumentStyle } from 'hooks/use-document-style';
|
||||||
import { useWindowSize } from 'hooks/use-window-size';
|
import { useWindowSize } from 'hooks/use-window-size';
|
||||||
|
@ -16,7 +17,6 @@ import { useUser } from 'data/user';
|
||||||
import { useDocumentDetail } from 'data/document';
|
import { useDocumentDetail } from 'data/document';
|
||||||
import { DocumentSkeleton } from 'tiptap';
|
import { DocumentSkeleton } from 'tiptap';
|
||||||
import { Editor } from './editor';
|
import { Editor } from './editor';
|
||||||
import { CreateUser } from './user';
|
|
||||||
import styles from './index.module.scss';
|
import styles from './index.module.scss';
|
||||||
|
|
||||||
const { Header } = Layout;
|
const { Header } = Layout;
|
||||||
|
@ -80,6 +80,7 @@ export const DocumentReader: React.FC<IProps> = ({ documentId }) => {
|
||||||
{authority && authority.readable && (
|
{authority && authority.readable && (
|
||||||
<>
|
<>
|
||||||
<DocumentShare key="share" documentId={documentId} />
|
<DocumentShare key="share" documentId={documentId} />
|
||||||
|
<DocumentVersion key="version" documentId={documentId} />
|
||||||
<DocumentStar key="star" documentId={documentId} />
|
<DocumentStar key="star" documentId={documentId} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -0,0 +1,71 @@
|
||||||
|
.headerWrap {
|
||||||
|
margin: 0 -24px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1000;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-bottom: 1px solid var(--semi-color-border);
|
||||||
|
|
||||||
|
> div {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.contentWrap {
|
||||||
|
margin: 0 -24px;
|
||||||
|
height: calc(100vh - 56px);
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
|
||||||
|
> aside {
|
||||||
|
width: 240px;
|
||||||
|
height: 100%;
|
||||||
|
padding: 12px 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-right: 1px solid var(--semi-color-border);
|
||||||
|
overflow: auto;
|
||||||
|
|
||||||
|
> ul {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
|
||||||
|
> li {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 16px;
|
||||||
|
color: var(--semi-color-text-0);
|
||||||
|
font-size: 14px;
|
||||||
|
border-radius: var(--semi-border-radius-small);
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--semi-color-primary-light-default);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
color: var(--semi-color-primary);
|
||||||
|
background-color: var(--semi-color-primary-light-default);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> main {
|
||||||
|
flex: 1;
|
||||||
|
background-color: var(--semi-color-nav-bg);
|
||||||
|
padding: 24px 0;
|
||||||
|
overflow: auto;
|
||||||
|
|
||||||
|
.editorWrap {
|
||||||
|
min-height: 100%;
|
||||||
|
padding: 12px 24px;
|
||||||
|
background-color: var(--semi-color-bg-2);
|
||||||
|
border: 1px solid var(--semi-color-border);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,24 +1,23 @@
|
||||||
import React, { useMemo, useState, useEffect, useCallback } from 'react';
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
import { Button, Modal, Input, Typography, Toast, Layout, Nav } from '@douyinfe/semi-ui';
|
import { Button, Modal, Typography } from '@douyinfe/semi-ui';
|
||||||
|
import { IconClose } from '@douyinfe/semi-icons';
|
||||||
import { useEditor, EditorContent } from '@tiptap/react';
|
import { useEditor, EditorContent } from '@tiptap/react';
|
||||||
|
import cls from 'classnames';
|
||||||
import { IconLink } from '@douyinfe/semi-icons';
|
|
||||||
import { isPublicDocument } from '@think/domains';
|
|
||||||
import { DEFAULT_EXTENSION, DocumentWithTitle } from 'tiptap';
|
import { DEFAULT_EXTENSION, DocumentWithTitle } from 'tiptap';
|
||||||
import { safeJSONParse } from 'helpers/json';
|
import { safeJSONParse } from 'helpers/json';
|
||||||
import { ShareIllustration } from 'illustrations/share';
|
|
||||||
import { DataRender } from 'components/data-render';
|
import { DataRender } from 'components/data-render';
|
||||||
import { useToggle } from 'hooks/use-toggle';
|
import { useToggle } from 'hooks/use-toggle';
|
||||||
import { useDocumentVersion } from 'data/document';
|
import { useDocumentVersion } from 'data/document';
|
||||||
|
import styles from './index.module.scss';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
documentId: string;
|
documentId: string;
|
||||||
|
onSelect?: (data) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Title } = Typography;
|
||||||
const { Header, Footer, Sider, Content } = Layout;
|
|
||||||
|
|
||||||
export const DocumentVersion: React.FC<IProps> = ({ documentId }) => {
|
export const DocumentVersion: React.FC<IProps> = ({ documentId, onSelect }) => {
|
||||||
const [visible, toggleVisible] = useToggle(false);
|
const [visible, toggleVisible] = useToggle(false);
|
||||||
const { data, loading, error, refresh } = useDocumentVersion(documentId);
|
const { data, loading, error, refresh } = useDocumentVersion(documentId);
|
||||||
const [selectedVersion, setSelectedVersion] = useState(null);
|
const [selectedVersion, setSelectedVersion] = useState(null);
|
||||||
|
@ -29,6 +28,11 @@ export const DocumentVersion: React.FC<IProps> = ({ documentId }) => {
|
||||||
content: {},
|
content: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const close = useCallback(() => {
|
||||||
|
toggleVisible(false);
|
||||||
|
setSelectedVersion(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const select = useCallback(
|
const select = useCallback(
|
||||||
(version) => {
|
(version) => {
|
||||||
setSelectedVersion(version);
|
setSelectedVersion(version);
|
||||||
|
@ -37,12 +41,25 @@ export const DocumentVersion: React.FC<IProps> = ({ documentId }) => {
|
||||||
[editor]
|
[editor]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const restore = useCallback(() => {
|
||||||
|
if (!selectedVersion || !onSelect) return;
|
||||||
|
onSelect(safeJSONParse(selectedVersion.data, { default: {} }).default);
|
||||||
|
close();
|
||||||
|
}, [selectedVersion]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
refresh();
|
refresh();
|
||||||
}
|
}
|
||||||
}, [visible]);
|
}, [visible]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!editor) return;
|
||||||
|
if (!data.length) return;
|
||||||
|
if (selectedVersion) return;
|
||||||
|
select(data[0]);
|
||||||
|
}, [editor, data, selectedVersion]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button type="primary" theme="light" onClick={toggleVisible}>
|
<Button type="primary" theme="light" onClick={toggleVisible}>
|
||||||
|
@ -52,30 +69,64 @@ export const DocumentVersion: React.FC<IProps> = ({ documentId }) => {
|
||||||
title="历史记录"
|
title="历史记录"
|
||||||
fullScreen
|
fullScreen
|
||||||
visible={visible}
|
visible={visible}
|
||||||
onOk={() => toggleVisible(false)}
|
style={{ padding: 0 }}
|
||||||
onCancel={() => toggleVisible(false)}
|
bodyStyle={{ padding: 0 }}
|
||||||
|
header={
|
||||||
|
<div className={styles.headerWrap}>
|
||||||
|
<div>
|
||||||
|
<Button icon={<IconClose />} onClick={close} />
|
||||||
|
<Title heading={5} style={{ marginLeft: 12 }}>
|
||||||
|
版本记录
|
||||||
|
</Title>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
theme="light"
|
||||||
|
type="primary"
|
||||||
|
style={{ marginRight: 8 }}
|
||||||
|
disabled={loading || error}
|
||||||
|
onClick={() => refresh()}
|
||||||
>
|
>
|
||||||
<Layout style={{ height: 'calc(100vh - 72px)', overflow: 'hidden' }}>
|
刷新
|
||||||
<Sider>
|
</Button>
|
||||||
<Nav
|
{onSelect && (
|
||||||
bodyStyle={{ height: 'calc(100vh - 96px)', overflow: 'auto' }}
|
<Button type="primary" theme="solid" disabled={!selectedVersion} onClick={restore}>
|
||||||
defaultOpenKeys={['job']}
|
恢复此记录
|
||||||
items={data.map(({ version, data }) => {
|
</Button>
|
||||||
return { itemKey: version, text: version, onClick: () => select({ version, data }) };
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
footer={null}
|
||||||
|
>
|
||||||
|
<DataRender
|
||||||
|
loading={loading}
|
||||||
|
error={error}
|
||||||
|
normalContent={() => (
|
||||||
|
<div className={styles.contentWrap}>
|
||||||
|
<aside>
|
||||||
|
<ul>
|
||||||
|
{data.map(({ version, data }) => {
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
key={version}
|
||||||
|
className={cls(selectedVersion && selectedVersion.version === version && styles.selected)}
|
||||||
|
onClick={() => select({ version, data })}
|
||||||
|
>
|
||||||
|
{version}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
})}
|
})}
|
||||||
/>
|
</ul>
|
||||||
</Sider>
|
</aside>
|
||||||
<Content>
|
<main>
|
||||||
<Layout className="components-layout-demo">
|
<div className={cls('container', styles.editorWrap)}>
|
||||||
<Header>Header</Header>
|
|
||||||
<Content>
|
|
||||||
<div className="container" style={{ paddingBottom: 48 }}>
|
|
||||||
<EditorContent editor={editor} />
|
<EditorContent editor={editor} />
|
||||||
</div>
|
</div>
|
||||||
</Content>
|
</main>
|
||||||
</Layout>
|
</div>
|
||||||
</Content>
|
)}
|
||||||
</Layout>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -84,7 +84,8 @@ export const useDocumentDetail = (documentId, options = null) => {
|
||||||
export const useDocumentVersion = (documentId) => {
|
export const useDocumentVersion = (documentId) => {
|
||||||
const { data, error, mutate } = useSWR<Array<{ version: string; data: string }>>(
|
const { data, error, mutate } = useSWR<Array<{ version: string; data: string }>>(
|
||||||
`/document/version/${documentId}`,
|
`/document/version/${documentId}`,
|
||||||
(url) => HttpClient.get(url)
|
(url) => HttpClient.get(url),
|
||||||
|
{ errorRetryCount: 0 }
|
||||||
);
|
);
|
||||||
const loading = !data && !error;
|
const loading = !data && !error;
|
||||||
return { data: data || [], loading, error, refresh: mutate };
|
return { data: data || [], loading, error, refresh: mutate };
|
||||||
|
|
|
@ -3,6 +3,8 @@ server:
|
||||||
prefix: '/api'
|
prefix: '/api'
|
||||||
port: 5001
|
port: 5001
|
||||||
collaborationPort: 5003
|
collaborationPort: 5003
|
||||||
|
# 最大版本记录数
|
||||||
|
maxDocumentVersion: 20
|
||||||
|
|
||||||
client:
|
client:
|
||||||
assetPrefix: '/'
|
assetPrefix: '/'
|
||||||
|
@ -23,7 +25,6 @@ db:
|
||||||
redis:
|
redis:
|
||||||
host: '127.0.0.1'
|
host: '127.0.0.1'
|
||||||
port: '6379'
|
port: '6379'
|
||||||
db: 0
|
|
||||||
password: 'root'
|
password: 'root'
|
||||||
|
|
||||||
# oss 文件存储服务
|
# oss 文件存储服务
|
||||||
|
|
|
@ -1,52 +1,100 @@
|
||||||
import { Injectable, HttpException, HttpStatus, Inject, forwardRef } from '@nestjs/common';
|
import { Injectable, HttpException, HttpStatus } from '@nestjs/common';
|
||||||
import Redis from 'ioredis';
|
import Redis from 'ioredis';
|
||||||
import { DocumentStatus, IDocument } from '@think/domains';
|
import { IDocument } from '@think/domains';
|
||||||
import { getConfig } from '@think/config';
|
import { getConfig } from '@think/config';
|
||||||
import * as lodash from 'lodash';
|
import * as lodash from 'lodash';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DocumentVersionService {
|
export class DocumentVersionService {
|
||||||
private redis: Redis;
|
private redis: Redis;
|
||||||
|
private max: number = 0;
|
||||||
|
private error: string | null = '文档版本服务启动中';
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.init();
|
this.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
private versionDataToArray(data: Record<string, string>): Array<{ version: string; data: string }> {
|
private versionDataToArray(
|
||||||
|
data: Record<string, string>
|
||||||
|
): Array<{ originVerison: string; version: string; data: string }> {
|
||||||
return Object.keys(data)
|
return Object.keys(data)
|
||||||
.sort((a, b) => +b - +a)
|
.sort((a, b) => +b - +a)
|
||||||
.map((key) => ({ version: new Date(+key).toLocaleString(), data: data[key] }));
|
.map((key) => ({ originVerison: key, version: new Date(+key).toLocaleString(), data: data[key] }));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async init() {
|
private async init() {
|
||||||
const config = getConfig();
|
const config = getConfig();
|
||||||
const redisConfig = lodash.get(config, 'db.redis', {});
|
const redisConfig = lodash.get(config, 'db.redis', null);
|
||||||
|
|
||||||
|
if (!redisConfig) {
|
||||||
|
console.error('Redis 未配置,无法启动文档版本服务');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.max = lodash.get(config, 'server.maxDocumentVersion', 0);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const redis = new Redis(redisConfig);
|
const redis = new Redis({
|
||||||
|
...redisConfig,
|
||||||
|
db: 0,
|
||||||
|
showFriendlyErrorStack: true,
|
||||||
|
lazyConnect: true,
|
||||||
|
});
|
||||||
|
redis.on('ready', () => {
|
||||||
|
console.log('文档版本服务启动成功');
|
||||||
this.redis = redis;
|
this.redis = redis;
|
||||||
} catch (e) {
|
this.error = null;
|
||||||
|
});
|
||||||
|
redis.on('error', (e) => {
|
||||||
|
console.error(`Redis 启动失败: "${e}"`);
|
||||||
|
});
|
||||||
|
redis.connect().catch((e) => {
|
||||||
this.redis = null;
|
this.redis = null;
|
||||||
}
|
this.error = 'Redis 启动失败:无法提供文档版本服务';
|
||||||
}
|
|
||||||
|
|
||||||
public async getDocumentVersions(documentId: IDocument['id']): Promise<Array<{ version: string; data: string }>> {
|
|
||||||
if (!this.redis) return [];
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
this.redis.hgetall(documentId, (err, ret) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err);
|
|
||||||
} else {
|
|
||||||
resolve(ret ? this.versionDataToArray(ret) : []);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 max 删除多余缓存数据
|
||||||
|
* @param key
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
public async checkCacheLength(documentId) {
|
||||||
|
if (this.max <= 0) return;
|
||||||
|
const res = await this.redis.hgetall(documentId);
|
||||||
|
if (!res) return;
|
||||||
|
const data = this.versionDataToArray(res);
|
||||||
|
|
||||||
|
while (data.length > this.max) {
|
||||||
|
const lastVersion = data.pop().originVerison;
|
||||||
|
await this.redis.hdel(documentId, lastVersion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存文档版本数据
|
||||||
|
* @param documentId
|
||||||
|
* @param data
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
public async storeDocumentVersion(documentId: IDocument['id'], data: IDocument['content']) {
|
public async storeDocumentVersion(documentId: IDocument['id'], data: IDocument['content']) {
|
||||||
if (!this.redis) return;
|
if (!this.redis) return;
|
||||||
const version = '' + Date.now();
|
const version = '' + Date.now();
|
||||||
this.redis.hsetnx(documentId, version, data);
|
await this.redis.hsetnx(documentId, version, data);
|
||||||
|
await this.checkCacheLength(documentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文档版本数据
|
||||||
|
* @param documentId
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
public async getDocumentVersions(documentId: IDocument['id']): Promise<Array<{ version: string; data: string }>> {
|
||||||
|
if (this.error || !this.redis) {
|
||||||
|
throw new HttpException(this.error, HttpStatus.NOT_IMPLEMENTED);
|
||||||
|
}
|
||||||
|
const res = await this.redis.hgetall(documentId);
|
||||||
|
return res ? this.versionDataToArray(res) : [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue