Taichi Masuyama преди 4 години
родител
ревизия
5f6ff8b734
променени са 2 файла, в които са добавени 166 реда и са изтрити 24 реда
  1. 25 6
      packages/app/src/server/models/page-operation.ts
  2. 141 18
      packages/app/src/server/service/page.ts

+ 25 - 6
packages/app/src/server/models/page-operation.ts

@@ -6,11 +6,12 @@ import { getOrCreateModel } from '@growi/core';
 import {
 import {
   IPageForResuming, IUserForResuming, IOptionsForResuming,
   IPageForResuming, IUserForResuming, IOptionsForResuming,
 } from '~/server/interfaces/page-operation';
 } from '~/server/interfaces/page-operation';
+import { ObjectIdLike } from '../interfaces/mongoose-utils';
 
 
 type IObjectId = mongoose.Types.ObjectId;
 type IObjectId = mongoose.Types.ObjectId;
 const ObjectId = mongoose.Schema.Types.ObjectId;
 const ObjectId = mongoose.Schema.Types.ObjectId;
 
 
-const PageActionType = {
+export const PageActionType = {
   Rename: 'Rename',
   Rename: 'Rename',
   Duplicate: 'Duplicate',
   Duplicate: 'Duplicate',
   Delete: 'Delete',
   Delete: 'Delete',
@@ -18,14 +19,20 @@ const PageActionType = {
   Revert: 'Revert',
   Revert: 'Revert',
   NormalizeParent: 'NormalizeParent',
   NormalizeParent: 'NormalizeParent',
 } as const;
 } as const;
-
 export type PageActionType = typeof PageActionType[keyof typeof PageActionType];
 export type PageActionType = typeof PageActionType[keyof typeof PageActionType];
 
 
+export const PageActionStage = {
+  Main: 'Main',
+  Sub: 'Sub',
+} as const;
+export type PageActionStage = typeof PageActionStage[keyof typeof PageActionStage];
+
 /*
 /*
  * Main Schema
  * Main Schema
  */
  */
 export interface IPageOperation {
 export interface IPageOperation {
   actionType: PageActionType,
   actionType: PageActionType,
+  actionStage: PageActionStage,
   fromPath: string,
   fromPath: string,
   toPath?: string,
   toPath?: string,
   page: IPageForResuming,
   page: IPageForResuming,
@@ -41,11 +48,11 @@ export interface PageOperationModel extends Model<PageOperationDocument> {
 }
 }
 
 
 const pageSchemaForResuming = new Schema<IPageForResuming>({
 const pageSchemaForResuming = new Schema<IPageForResuming>({
-  _id: { type: ObjectId, ref: 'Page' },
+  _id: { type: ObjectId, ref: 'Page', index: true },
   parent: { type: ObjectId, ref: 'Page' },
   parent: { type: ObjectId, ref: 'Page' },
   descendantCount: { type: Number },
   descendantCount: { type: Number },
   isEmpty: { type: Boolean },
   isEmpty: { type: Boolean },
-  path: { type: String, required: true },
+  path: { type: String, required: true, index: true },
   revision: { type: ObjectId, ref: 'Revision' },
   revision: { type: ObjectId, ref: 'Revision' },
   status: { type: String },
   status: { type: String },
   grant: { type: Number },
   grant: { type: Number },
@@ -71,12 +78,24 @@ const schema = new Schema<PageOperationDocument, PageOperationModel>({
     required: true,
     required: true,
     index: true,
     index: true,
   },
   },
-  fromPath: { type: String, required: true },
-  toPath: { type: String },
+  actionStage: {
+    type: String,
+    enum: PageActionStage,
+    required: true,
+    index: true,
+  },
+  fromPath: { type: String, required: true, index: true },
+  toPath: { type: String, index: true },
   page: { type: pageSchemaForResuming, required: true },
   page: { type: pageSchemaForResuming, required: true },
   user: { type: userSchemaForResuming, required: true },
   user: { type: userSchemaForResuming, required: true },
   options: { type: optionsSchemaForResuming },
   options: { type: optionsSchemaForResuming },
   incForUpdatingDescendantCount: { type: Number },
   incForUpdatingDescendantCount: { type: Number },
 });
 });
 
 
+schema.statics.findByIdAndUpdatePageActionStage = async function(pageOpId: ObjectIdLike, stage: PageActionStage) {
+  return this.findByIdAndUpdate(pageOpId, {
+    $set: { actionStage: stage },
+  }, { new: true });
+};
+
 export default getOrCreateModel<PageOperationDocument, PageOperationModel>('PageOperation', schema);
 export default getOrCreateModel<PageOperationDocument, PageOperationModel>('PageOperation', schema);

+ 141 - 18
packages/app/src/server/service/page.ts

@@ -21,6 +21,7 @@ import { ObjectIdLike } from '../interfaces/mongoose-utils';
 import { IUserHasId } from '~/interfaces/user';
 import { IUserHasId } from '~/interfaces/user';
 import { Ref } from '~/interfaces/common';
 import { Ref } from '~/interfaces/common';
 import { HasObjectId } from '~/interfaces/has-object-id';
 import { HasObjectId } from '~/interfaces/has-object-id';
+import { PageActionStage, PageActionType, PageOperationModel } from '../models/page-operation';
 
 
 const debug = require('debug')('growi:services:page');
 const debug = require('debug')('growi:services:page');
 
 
@@ -332,12 +333,29 @@ class PageService {
     /*
     /*
      * Resumable Operation
      * Resumable Operation
      */
      */
-    const renamedPage = await this.renameMainOperation(page, newPagePath, user, options);
+    const PageOperation = mongoose.model('PageOperation') as unknown as PageOperationModel;
+    let pageOp;
+    try {
+      pageOp = await PageOperation.create({
+        actionType: PageActionType.Rename,
+        actionStage: PageActionStage.Main,
+        page,
+        user,
+        fromPath: page.path,
+        toPath: newPagePath,
+        options,
+      });
+    }
+    catch (err) {
+      logger.error('Failed to create PageOperation document.', err);
+      throw err;
+    }
+    const renamedPage = await this.renameMainOperation(page, newPagePath, user, options, pageOp._id);
 
 
     return renamedPage;
     return renamedPage;
   }
   }
 
 
-  async renameMainOperation(page, newPagePath: string, user, options) {
+  async renameMainOperation(page, newPagePath: string, user, options, pageOpId: ObjectIdLike) {
     const Page = mongoose.model('Page') as unknown as PageModel;
     const Page = mongoose.model('Page') as unknown as PageModel;
 
 
     const updateMetadata = options.updateMetadata || false;
     const updateMetadata = options.updateMetadata || false;
@@ -394,15 +412,22 @@ class PageService {
     const renamedPage = await Page.findByIdAndUpdate(page._id, { $set: update }, { new: true });
     const renamedPage = await Page.findByIdAndUpdate(page._id, { $set: update }, { new: true });
     this.pageEvent.emit('rename', page, user);
     this.pageEvent.emit('rename', page, user);
 
 
+    // Set to Sub
+    const PageOperation = mongoose.model('PageOperation') as unknown as PageOperationModel;
+    const pageOp = await PageOperation.findByIdAndUpdatePageActionStage(pageOpId, PageActionStage.Sub);
+    if (pageOp == null) {
+      throw Error('PageOperation document not found');
+    }
+
     /*
     /*
      * Sub Operation
      * Sub Operation
      */
      */
-    this.renameSubOperation(page, newPagePath, user, options, renamedPage);
+    this.renameSubOperation(page, newPagePath, user, options, renamedPage, pageOp._id);
 
 
     return renamedPage;
     return renamedPage;
   }
   }
 
 
-  async renameSubOperation(page, newPagePath: string, user, options, renamedPage): Promise<void> {
+  async renameSubOperation(page, newPagePath: string, user, options, renamedPage, pageOpId: ObjectIdLike): Promise<void> {
     const exParentId = page.parent;
     const exParentId = page.parent;
 
 
     // update descendants first
     // update descendants first
@@ -415,6 +440,9 @@ class PageService {
     // increase ancestore's descendantCount
     // increase ancestore's descendantCount
     const nToIncrease = (renamedPage.isEmpty ? 0 : 1) + page.descendantCount;
     const nToIncrease = (renamedPage.isEmpty ? 0 : 1) + page.descendantCount;
     await this.updateDescendantCountOfAncestors(renamedPage._id, nToIncrease, false);
     await this.updateDescendantCountOfAncestors(renamedPage._id, nToIncrease, false);
+
+    const PageOperation = mongoose.model('PageOperation') as unknown as PageOperationModel;
+    await PageOperation.findByIdAndDelete(pageOpId);
   }
   }
 
 
   // !!renaming always include descendant pages!!
   // !!renaming always include descendant pages!!
@@ -754,7 +782,23 @@ class PageService {
       /*
       /*
        * Resumable Operation
        * Resumable Operation
        */
        */
-      this.duplicateRecursivelyMainOperation(page, newPagePath, user);
+      const PageOperation = mongoose.model('PageOperation') as unknown as PageOperationModel;
+      let pageOp;
+      try {
+        pageOp = await PageOperation.create({
+          actionType: PageActionType.Duplicate,
+          actionStage: PageActionStage.Main,
+          page,
+          user,
+          fromPath: page.path,
+          toPath: newPagePath,
+        });
+      }
+      catch (err) {
+        logger.error('Failed to create PageOperation document.', err);
+        throw err;
+      }
+      this.duplicateRecursivelyMainOperation(page, newPagePath, user, pageOp._id);
     }
     }
 
 
     const result = serializePageSecurely(duplicatedTarget);
     const result = serializePageSecurely(duplicatedTarget);
@@ -762,7 +806,7 @@ class PageService {
     return result;
     return result;
   }
   }
 
 
-  async duplicateRecursivelyMainOperation(page, newPagePath: string, user): Promise<void> {
+  async duplicateRecursivelyMainOperation(page, newPagePath: string, user, pageOpId: ObjectIdLike): Promise<void> {
     const nDuplicatedPages = await this.duplicateDescendantsWithStream(page, newPagePath, user, false);
     const nDuplicatedPages = await this.duplicateDescendantsWithStream(page, newPagePath, user, false);
 
 
     // normalize parent of descendant pages
     // normalize parent of descendant pages
@@ -778,13 +822,20 @@ class PageService {
       }
       }
     }
     }
 
 
+    // Set to Sub
+    const PageOperation = mongoose.model('PageOperation') as unknown as PageOperationModel;
+    const pageOp = await PageOperation.findByIdAndUpdatePageActionStage(pageOpId, PageActionStage.Sub);
+    if (pageOp == null) {
+      throw Error('PageOperation document not found');
+    }
+
     /*
     /*
      * Sub Operation
      * Sub Operation
      */
      */
-    await this.duplicateRecursivelySubOperation(newPagePath, nDuplicatedPages);
+    await this.duplicateRecursivelySubOperation(newPagePath, nDuplicatedPages, pageOp._id);
   }
   }
 
 
-  async duplicateRecursivelySubOperation(newPagePath: string, nDuplicatedPages: number): Promise<void> {
+  async duplicateRecursivelySubOperation(newPagePath: string, nDuplicatedPages: number, pageOpId: ObjectIdLike): Promise<void> {
     const Page = mongoose.model('Page');
     const Page = mongoose.model('Page');
     const newTarget = await Page.findOne({ path: newPagePath }); // only one page will be found since duplicating to existing path is forbidden
     const newTarget = await Page.findOne({ path: newPagePath }); // only one page will be found since duplicating to existing path is forbidden
     if (newTarget == null) {
     if (newTarget == null) {
@@ -792,6 +843,9 @@ class PageService {
     }
     }
 
 
     await this.updateDescendantCountOfAncestors(newTarget._id, nDuplicatedPages, false);
     await this.updateDescendantCountOfAncestors(newTarget._id, nDuplicatedPages, false);
+
+    const PageOperation = mongoose.model('PageOperation') as unknown as PageOperationModel;
+    await PageOperation.findByIdAndDelete(pageOpId);
   }
   }
 
 
   async duplicateV4(page, newPagePath, user, isRecursively) {
   async duplicateV4(page, newPagePath, user, isRecursively) {
@@ -1112,10 +1166,27 @@ class PageService {
     await this.removeLeafEmptyPages(parent);
     await this.removeLeafEmptyPages(parent);
 
 
     if (isRecursively) {
     if (isRecursively) {
+      const PageOperation = mongoose.model('PageOperation') as unknown as PageOperationModel;
+      const newPath = Page.getDeletedPageName(page.path);
+      let pageOp;
+      try {
+        pageOp = await PageOperation.create({
+          actionType: PageActionType.Delete,
+          actionStage: PageActionStage.Main,
+          page,
+          user,
+          fromPath: page.path,
+          toPath: newPath,
+        });
+      }
+      catch (err) {
+        logger.error('Failed to create PageOperation document.', err);
+        throw err;
+      }
       /*
       /*
-       * Sub Operation
+       * Resumable Operation
        */
        */
-      this.deleteRecursivelyMainOperation(page, user);
+      this.deleteRecursivelyMainOperation(page, user, pageOp._id);
     }
     }
 
 
     return deletedPage;
     return deletedPage;
@@ -1151,9 +1222,12 @@ class PageService {
     await this.updateDescendantCountOfAncestors(page._id, -page.descendantCount, false);
     await this.updateDescendantCountOfAncestors(page._id, -page.descendantCount, false);
   }
   }
 
 
-  async deleteRecursivelyMainOperation(page, user): Promise<void> {
+  async deleteRecursivelyMainOperation(page, user, pageOpId: ObjectIdLike): Promise<void> {
     await this.deleteDescendantsWithStream(page, user, false);
     await this.deleteDescendantsWithStream(page, user, false);
 
 
+    const PageOperation = mongoose.model('PageOperation') as unknown as PageOperationModel;
+    await PageOperation.findByIdAndDelete(pageOpId);
+
     // no sub operation available
     // no sub operation available
   }
   }
 
 
@@ -1403,18 +1477,37 @@ class PageService {
     }
     }
 
 
     if (isRecursively) {
     if (isRecursively) {
+      const PageOperation = mongoose.model('PageOperation') as unknown as PageOperationModel;
+      let pageOp;
+      try {
+        pageOp = await PageOperation.create({
+          actionType: PageActionType.DeleteCompletely,
+          actionStage: PageActionStage.Main,
+          page,
+          user,
+          fromPath: page.path,
+          options,
+        });
+      }
+      catch (err) {
+        logger.error('Failed to create PageOperation document.', err);
+        throw err;
+      }
       /*
       /*
        * Main Operation
        * Main Operation
        */
        */
-      this.deleteCompletelyRecursivelyMainOperation(page, user, options);
+      this.deleteCompletelyRecursivelyMainOperation(page, user, options, pageOp._id);
     }
     }
 
 
     return;
     return;
   }
   }
 
 
-  async deleteCompletelyRecursivelyMainOperation(page, user, options): Promise<void> {
+  async deleteCompletelyRecursivelyMainOperation(page, user, options, pageOpId: ObjectIdLike): Promise<void> {
     await this.deleteCompletelyDescendantsWithStream(page, user, options, false);
     await this.deleteCompletelyDescendantsWithStream(page, user, options, false);
 
 
+    const PageOperation = mongoose.model('PageOperation') as unknown as PageOperationModel;
+    await PageOperation.findByIdAndDelete(pageOpId);
+
     // no sub operation available
     // no sub operation available
   }
   }
 
 
@@ -1585,16 +1678,33 @@ class PageService {
       await this.updateDescendantCountOfAncestors(parent._id, 1, true);
       await this.updateDescendantCountOfAncestors(parent._id, 1, true);
     }
     }
     else {
     else {
+      const PageOperation = mongoose.model('PageOperation') as unknown as PageOperationModel;
+      let pageOp;
+      try {
+        pageOp = await PageOperation.create({
+          actionType: PageActionType.Revert,
+          actionStage: PageActionStage.Main,
+          page,
+          user,
+          fromPath: page.path,
+          toPath: newPath,
+          options,
+        });
+      }
+      catch (err) {
+        logger.error('Failed to create PageOperation document.', err);
+        throw err;
+      }
       /*
       /*
-       * Sub Operation
+       * Resumable Operation
        */
        */
-      this.revertRecursivelyMainOperation(page, user, options);
+      this.revertRecursivelyMainOperation(page, user, options, pageOp._id);
     }
     }
 
 
     return updatedPage;
     return updatedPage;
   }
   }
 
 
-  async revertRecursivelyMainOperation(page, user, options): Promise<void> {
+  async revertRecursivelyMainOperation(page, user, options, pageOpId: ObjectIdLike): Promise<void> {
     const Page = mongoose.model('Page') as unknown as PageModel;
     const Page = mongoose.model('Page') as unknown as PageModel;
 
 
     await this.revertDeletedDescendantsWithStream(page, user, options, false);
     await this.revertDeletedDescendantsWithStream(page, user, options, false);
@@ -1613,10 +1723,20 @@ class PageService {
       }
       }
     }
     }
 
 
-    await this.revertRecursivelySubOperation(page, newPath);
+    // Set to Sub
+    const PageOperation = mongoose.model('PageOperation') as unknown as PageOperationModel;
+    const pageOp = await PageOperation.findByIdAndUpdatePageActionStage(pageOpId, PageActionStage.Sub);
+    if (pageOp == null) {
+      throw Error('PageOperation document not found');
+    }
+
+    /*
+     * Sub Operation
+     */
+    await this.revertRecursivelySubOperation(page, newPath, pageOp._id);
   }
   }
 
 
-  async revertRecursivelySubOperation(page, newPath: string): Promise<void> {
+  async revertRecursivelySubOperation(page, newPath: string, pageOpId: ObjectIdLike): Promise<void> {
     const Page = mongoose.model('Page') as unknown as PageModel;
     const Page = mongoose.model('Page') as unknown as PageModel;
 
 
     const newTarget = await Page.findOne({ path: newPath }); // only one page will be found since duplicating to existing path is forbidden
     const newTarget = await Page.findOne({ path: newPath }); // only one page will be found since duplicating to existing path is forbidden
@@ -1627,6 +1747,9 @@ class PageService {
 
 
     // update descendantCount of ancestors'
     // update descendantCount of ancestors'
     await this.updateDescendantCountOfAncestors(page.parent, newTarget.descendantCount + 1, true);
     await this.updateDescendantCountOfAncestors(page.parent, newTarget.descendantCount + 1, true);
+
+    const PageOperation = mongoose.model('PageOperation') as unknown as PageOperationModel;
+    await PageOperation.findByIdAndDelete(pageOpId);
   }
   }
 
 
   private async revertDeletedPageV4(page, user, options = {}, isRecursively = false) {
   private async revertDeletedPageV4(page, user, options = {}, isRecursively = false) {