Просмотр исходного кода

Merge pull request #8926 from weseek/feat/149417-implement-api-to-sync-latest-revision-body-to-yjs-draft

feat: Implement API to sync latest revision body to Yjs draft
Shun Miyazawa 1 год назад
Родитель
Сommit
93a022e50c

+ 3 - 0
apps/app/src/server/routes/apiv3/page/index.ts

@@ -29,6 +29,7 @@ import { checkPageExistenceHandlersFactory } from './check-page-existence';
 import { createPageHandlersFactory } from './create-page';
 import { getYjsDataHandlerFactory } from './get-yjs-data';
 import { publishPageHandlersFactory } from './publish-page';
+import { syncLatestRevisionBodyToYjsDraftHandlerFactory } from './sync-latest-revision-body-to-yjs-draft';
 import { unpublishPageHandlersFactory } from './unpublish-page';
 import { updatePageHandlersFactory } from './update-page';
 
@@ -951,5 +952,7 @@ module.exports = (crowi) => {
 
   router.get('/:pageId/yjs-data', getYjsDataHandlerFactory(crowi));
 
+  router.put('/:pageId/sync-latest-revision-body-to-yjs-draft', syncLatestRevisionBodyToYjsDraftHandlerFactory(crowi));
+
   return router;
 };

+ 57 - 0
apps/app/src/server/routes/apiv3/page/sync-latest-revision-body-to-yjs-draft.ts

@@ -0,0 +1,57 @@
+import type { IPage, IUserHasId } from '@growi/core';
+import { ErrorV3 } from '@growi/core/dist/models';
+import type { Request, RequestHandler } from 'express';
+import type { ValidationChain } from 'express-validator';
+import { param } from 'express-validator';
+import mongoose from 'mongoose';
+
+import type Crowi from '~/server/crowi';
+import type { PageModel } from '~/server/models/page';
+import loggerFactory from '~/utils/logger';
+
+import { apiV3FormValidator } from '../../../middlewares/apiv3-form-validator';
+import type { ApiV3Response } from '../interfaces/apiv3-response';
+
+
+const logger = loggerFactory('growi:routes:apiv3:page:sync-latest-revision-body-to-yjs-draft');
+
+type SyncLatestRevisionBodyToYjsDraftHandlerFactory = (crowi: Crowi) => RequestHandler[];
+
+type ReqParams = {
+  pageId: string,
+}
+interface Req extends Request<ReqParams, ApiV3Response> {
+  user: IUserHasId,
+}
+export const syncLatestRevisionBodyToYjsDraftHandlerFactory: SyncLatestRevisionBodyToYjsDraftHandlerFactory = (crowi) => {
+  const Page = mongoose.model<IPage, PageModel>('Page');
+  const accessTokenParser = require('../../../middlewares/access-token-parser')(crowi);
+  const loginRequiredStrictly = require('../../../middlewares/login-required')(crowi);
+
+  // define validators for req.params
+  const validator: ValidationChain[] = [
+    param('pageId').isMongoId().withMessage('The param "pageId" must be specified'),
+  ];
+
+  return [
+    accessTokenParser, loginRequiredStrictly,
+    validator, apiV3FormValidator,
+    async(req: Req, res: ApiV3Response) => {
+      const { pageId } = req.params;
+
+      // check whether accessible
+      if (!(await Page.isAccessiblePageByViewer(pageId, req.user))) {
+        return res.apiv3Err(new ErrorV3('Current user is not accessible to this page.', 'forbidden-page'), 403);
+      }
+
+      try {
+        await crowi.pageService.syncLatestRevisionBodyToYjsDraft(pageId);
+        return res.apiv3({ });
+      }
+      catch (err) {
+        logger.error(err);
+        return res.apiv3Err(err);
+      }
+    },
+  ];
+};

+ 5 - 0
apps/app/src/server/service/page/index.ts

@@ -67,6 +67,7 @@ import { preNotifyService } from '../pre-notify';
 import { BULK_REINDEX_SIZE, LIMIT_FOR_MULTIPLE_PAGE_OP } from './consts';
 import type { IPageService } from './page-service';
 import { shouldUseV4Process } from './should-use-v4-process';
+import { syncLatestRevisionBodyToYjsDraft } from './sync-latest-revision-body-to-yjs-draft';
 
 export * from './page-service';
 
@@ -4458,6 +4459,10 @@ class PageService implements IPageService {
     };
   }
 
+  async syncLatestRevisionBodyToYjsDraft(pageId: string): Promise<void> {
+    await syncLatestRevisionBodyToYjsDraft(pageId);
+  }
+
   async hasRevisionBodyDiff(pageId: string, comparisonTarget?: string): Promise<boolean> {
     if (comparisonTarget == null) {
       return false;

+ 1 - 0
apps/app/src/server/service/page/page-service.ts

@@ -32,4 +32,5 @@ export interface IPageService {
     page: PageDocument, creatorId: ObjectIdLike | null, operator: any | null, userRelatedGroups: PopulatedGrantedGroup[]
   ): boolean,
   getYjsData(pageId: string, revisionBody?: string): Promise<CurrentPageYjsData>,
+  syncLatestRevisionBodyToYjsDraft(pageId: string): Promise<void>,
 }

+ 13 - 0
apps/app/src/server/service/page/sync-latest-revision-body-to-yjs-draft.ts

@@ -0,0 +1,13 @@
+import type { IRevisionHasId } from '@growi/core';
+import mongoose from 'mongoose';
+
+import { getYjsConnectionManager } from '~/server/service/yjs-connection-manager';
+
+export const syncLatestRevisionBodyToYjsDraft = async(pageId: string): Promise<void> => {
+  const yjsConnectionManager = getYjsConnectionManager();
+  const Revision = mongoose.model<IRevisionHasId>('Revision');
+  const revision = await Revision.findOne({ pageId }).sort({ createdAt: -1 });
+  if (revision != null) {
+    await yjsConnectionManager.handleYDocUpdate(pageId, revision.body);
+  }
+};

+ 17 - 9
apps/app/src/server/service/yjs-connection-manager.ts

@@ -25,6 +25,10 @@ class YjsConnectionManager {
     return this.ysocketio;
   }
 
+  get mdbInstance(): MongodbPersistence {
+    return this.mdb;
+  }
+
   private constructor(io: Server) {
     this.ysocketio = new YSocketIO(io);
     this.ysocketio.initialize();
@@ -55,7 +59,6 @@ class YjsConnectionManager {
     }
 
     const persistedYdoc = await this.getPersistedYdoc(pageId);
-    const persistedStateVector = Y.encodeStateVector(persistedYdoc);
 
     await this.mdb.flushDocument(pageId);
 
@@ -65,13 +68,7 @@ class YjsConnectionManager {
       currentYdoc.getText('codemirror').insert(0, initialValue);
     }
 
-    const diff = Y.encodeStateAsUpdate(currentYdoc, persistedStateVector);
-
-    if (diff.reduce((prev, curr) => prev + curr, 0) > 0) {
-      this.mdb.storeUpdate(pageId, diff);
-    }
-
-    Y.applyUpdate(currentYdoc, Y.encodeStateAsUpdate(persistedYdoc));
+    await this.syncWithPersistedYdoc(pageId, currentYdoc, persistedYdoc);
 
     currentYdoc.on('update', async(update) => {
       await this.mdb.storeUpdate(pageId, update);
@@ -95,7 +92,18 @@ class YjsConnectionManager {
     const currentMarkdownLength = currentYdoc.getText('codemirror').length;
     currentYdoc.getText('codemirror').delete(0, currentMarkdownLength);
     currentYdoc.getText('codemirror').insert(0, newValue);
-    Y.encodeStateAsUpdate(currentYdoc);
+
+    const persistedYdoc = await this.getPersistedYdoc(pageId);
+    await this.syncWithPersistedYdoc(pageId, currentYdoc, persistedYdoc);
+  }
+
+  private async syncWithPersistedYdoc(pageId: string, currentYdoc: Ydoc, persistedYdoc: Y.Doc): Promise<void> {
+    const persistedStateVector = Y.encodeStateVector(persistedYdoc);
+    const diff = Y.encodeStateAsUpdate(currentYdoc, persistedStateVector);
+    if (diff.reduce((prev, curr) => prev + curr, 0) > 0) {
+      await this.mdb.storeUpdate(pageId, diff);
+    }
+    Y.applyUpdate(currentYdoc, Y.encodeStateAsUpdate(persistedYdoc));
   }
 
   public getCurrentYdoc(pageId: string): Ydoc | undefined {