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

Implemented single & recursive delete

Taichi Masuyama 4 лет назад
Родитель
Сommit
2ef3947ac7

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

@@ -162,10 +162,9 @@ schema.statics.createEmptyPage = async function(
  * Replace an existing page with an empty page.
  * It updates the children's parent to the new empty page's _id.
  * @param exPage a page document to be replaced
- * @param pageToReplaceWith (optional) a page document to replace with
  * @returns Promise<void>
  */
-schema.statics.replaceTargetWithPage = async function(exPage, pageToReplaceWith?): Promise<void> {
+schema.statics.replaceTargetWithEmptyPage = async function(exPage): Promise<void> {
   // find parent
   const parent = await this.findOne({ _id: exPage.parent });
   if (parent == null) {
@@ -173,10 +172,7 @@ schema.statics.replaceTargetWithPage = async function(exPage, pageToReplaceWith?
   }
 
   // create empty page at path
-  let newTarget = pageToReplaceWith;
-  if (newTarget == null) {
-    newTarget = await this.createEmptyPage(exPage.path, parent);
-  }
+  const newTarget = await this.createEmptyPage(exPage.path, parent);
 
   // find children by ex-page _id
   const children = await this.find({ parent: exPage._id });

+ 2 - 4
packages/app/src/server/models/revision.js

@@ -37,10 +37,8 @@ module.exports = function(crowi) {
       .exec();
   };
 
-  revisionSchema.statics.updateRevisionListByPath = async function(path, updateData) {
-    const Revision = this;
-
-    return Revision.updateMany({ path }, { $set: updateData });
+  revisionSchema.statics.updateRevisionListByPageId = async function(pageId, updateData) {
+    return this.updateMany({ pageId }, { $set: updateData });
   };
 
   revisionSchema.statics.prepareRevision = function(pageData, body, previousBody, user, options) {

+ 1 - 1
packages/app/src/server/routes/page.js

@@ -1161,7 +1161,7 @@ module.exports = function(crowi, app) {
 
     const options = {};
 
-    const page = await Page.findByIdAndViewer(pageId, req.user);
+    const page = await Page.findByIdAndViewerToEdit(pageId, req.user);
 
     if (page == null) {
       return res.json(ApiResponse.error(`Page '${pageId}' is not found or forbidden`, 'notfound_or_forbidden'));

+ 84 - 46
packages/app/src/server/service/page.ts

@@ -278,7 +278,6 @@ class PageService {
   private async renamePageV4(page, newPagePath, user, options) {
     const Page = this.crowi.model('Page');
     const Revision = this.crowi.model('Revision');
-    const path = page.path;
     const updateMetadata = options.updateMetadata || false;
 
     // sanitize path
@@ -298,7 +297,7 @@ class PageService {
     const renamedPage = await Page.findByIdAndUpdate(page._id, { $set: update }, { new: true });
 
     // update Rivisions
-    await Revision.updateRevisionListByPath(path, { path: newPagePath }, {});
+    await Revision.updateRevisionListByPageId(renamedPage._id, { pageId: renamedPage._id });
 
     /*
      * TODO: https://redmine.weseek.co.jp/issues/86577
@@ -848,9 +847,17 @@ class PageService {
    * Delete
    */
   async deletePage(page, user, options = {}, isRecursively = false) {
-    const Page = this.crowi.model('Page');
-    const PageTagRelation = this.crowi.model('PageTagRelation');
-    const Revision = this.crowi.model('Revision');
+    const Page = mongoose.model('Page') as PageModel;
+    const PageTagRelation = mongoose.model('PageTagRelation') as any; // TODO: Typescriptize model
+    const Revision = mongoose.model('Revision') as any; // TODO: Typescriptize model
+
+    // v4 compatible process
+    const isPageMigrated = page.parent != null;
+    const isV5Compatible = this.crowi.configManager.getConfig('crowi', 'app:isV5Compatible');
+    const useV4Process = !isV5Compatible || !isPageMigrated;
+    if (useV4Process) {
+      return this.deletePageV4(page, user, options, isRecursively);
+    }
 
     const newPath = Page.getDeletedPageName(page.path);
     const isTrashed = isTrashPage(page.path);
@@ -863,20 +870,73 @@ class PageService {
       throw new Error('Page is not deletable.');
     }
 
+    // replace with an empty page
+    const shouldReplace = isRecursively && await Page.exists({ parent: page._id });
+    if (shouldReplace) {
+      await Page.replaceTargetWithEmptyPage(page);
+    }
+
     if (isRecursively) {
-      this.deleteDescendantsWithStream(page, user, options);
+      this.deleteDescendantsWithStream(page, user); // use the same process in both version v4 and v5
     }
 
-    // update Rivisions
-    await Revision.updateRevisionListByPath(page.path, { path: newPath }, {});
+    // update Revisions
+    await Revision.updateRevisionListByPageId(page._id, { pageId: page._id });
     const deletedPage = await Page.findByIdAndUpdate(page._id, {
       $set: {
         path: newPath, status: Page.STATUS_DELETED, deleteUser: user._id, deletedAt: Date.now(),
       },
     }, { new: true });
     await PageTagRelation.updateMany({ relatedPage: page._id }, { $set: { isPageTrashed: true } });
-    const body = `redirect ${newPath}`;
-    await Page.create(page.path, body, user, { redirectTo: newPath });
+
+    /*
+     * TODO: https://redmine.weseek.co.jp/issues/86577
+     * bulkWrite PageRedirect documents
+     */
+    // const body = `redirect ${newPath}`;
+    // await Page.create(page.path, body, user, { redirectTo: newPath });
+
+    this.pageEvent.emit('delete', page, user);
+    this.pageEvent.emit('create', deletedPage, user);
+
+    return deletedPage;
+  }
+
+  private async deletePageV4(page, user, options = {}, isRecursively = false) {
+    const Page = mongoose.model('Page') as PageModel;
+    const PageTagRelation = mongoose.model('PageTagRelation') as any; // TODO: Typescriptize model
+    const Revision = mongoose.model('Revision') as any; // TODO: Typescriptize model
+
+    const newPath = Page.getDeletedPageName(page.path);
+    const isTrashed = isTrashPage(page.path);
+
+    if (isTrashed) {
+      throw new Error('This method does NOT support deleting trashed pages.');
+    }
+
+    if (!Page.isDeletableName(page.path)) {
+      throw new Error('Page is not deletable.');
+    }
+
+    if (isRecursively) {
+      this.deleteDescendantsWithStream(page, user);
+    }
+
+    // update Revisions
+    await Revision.updateRevisionListByPageId(page._id, { pageId: page._id });
+    const deletedPage = await Page.findByIdAndUpdate(page._id, {
+      $set: {
+        path: newPath, status: Page.STATUS_DELETED, deleteUser: user._id, deletedAt: Date.now(),
+      },
+    }, { new: true });
+    await PageTagRelation.updateMany({ relatedPage: page._id }, { $set: { isPageTrashed: true } });
+
+    /*
+     * TODO: https://redmine.weseek.co.jp/issues/86577
+     * bulkWrite PageRedirect documents
+     */
+    // const body = `redirect ${newPath}`;
+    // await Page.create(page.path, body, user, { redirectTo: newPath });
 
     this.pageEvent.emit('delete', page, user);
     this.pageEvent.emit('create', deletedPage, user);
@@ -920,52 +980,31 @@ class PageService {
   }
 
   private async deleteDescendants(pages, user) {
-    const Page = this.crowi.model('Page');
+    const Page = mongoose.model('Page') as PageModel;
 
-    const pageCollection = mongoose.connection.collection('pages');
-    const revisionCollection = mongoose.connection.collection('revisions');
-
-    const deletePageBulkOp = pageCollection.initializeUnorderedBulkOp();
-    const updateRevisionListOp = revisionCollection.initializeUnorderedBulkOp();
-    const createRediectRevisionBulkOp = revisionCollection.initializeUnorderedBulkOp();
-    const newPagesForRedirect: any[] = [];
+    const deletePageBulkOp: any[] = [];
 
     pages.forEach((page) => {
       const newPath = Page.getDeletedPageName(page.path);
-      const revisionId = new mongoose.Types.ObjectId();
-      const body = `redirect ${newPath}`;
 
-      deletePageBulkOp.find({ _id: page._id }).update({
-        $set: {
-          path: newPath, status: Page.STATUS_DELETED, deleteUser: user._id, deletedAt: Date.now(),
+      deletePageBulkOp.push({
+        updateOne: {
+          filter: { _id: page._id },
+          update: {
+            $set: {
+              path: newPath, status: Page.STATUS_DELETED, deleteUser: user._id, deletedAt: Date.now(),
+            },
+          },
         },
       });
-      updateRevisionListOp.find({ path: page.path }).update({ $set: { path: newPath } });
-      createRediectRevisionBulkOp.insert({
-        _id: revisionId, path: page.path, body, author: user._id, format: 'markdown',
-      });
-
-      newPagesForRedirect.push({
-        path: page.path,
-        creator: user._id,
-        grant: page.grant,
-        grantedGroup: page.grantedGroup,
-        grantedUsers: page.grantedUsers,
-        lastUpdateUser: user._id,
-        redirectTo: newPath,
-        revision: revisionId,
-      });
     });
 
     try {
-      await deletePageBulkOp.execute();
-      await updateRevisionListOp.execute();
-      await createRediectRevisionBulkOp.execute();
-      await Page.insertMany(newPagesForRedirect, { ordered: false });
+      await Page.bulkWrite(deletePageBulkOp);
     }
     catch (err) {
       if (err.code !== 11000) {
-        throw new Error(`Failed to revert pages: ${err}`);
+        throw new Error(`Failed to delete pages: ${err}`);
       }
     }
     finally {
@@ -976,8 +1015,7 @@ class PageService {
   /**
    * Create delete stream
    */
-  private async deleteDescendantsWithStream(targetPage, user, options = {}) {
-
+  private async deleteDescendantsWithStream(targetPage, user) {
     const readStream = await this.generateReadStreamToOperateOnlyDescendants(targetPage.path, user);
 
     const deleteDescendants = this.deleteDescendants.bind(this);
@@ -987,7 +1025,7 @@ class PageService {
       async write(batch, encoding, callback) {
         try {
           count += batch.length;
-          deleteDescendants(batch, user);
+          await deleteDescendants(batch, user);
           logger.debug(`Reverting pages progressing: (count=${count})`);
         }
         catch (err) {