Browse Source

improve performance

ryoji-s 2 years ago
parent
commit
73d67f9405

+ 26 - 5
apps/app/src/server/routes/apiv3/page-listing.ts

@@ -3,6 +3,7 @@ import type {
 } from '@growi/core';
 import { isIPageInfoForEntity } from '@growi/core';
 import { ErrorV3 } from '@growi/core/dist/models';
+import { isUsersHomepage } from '@growi/core/dist/utils/page-path-utils';
 import express, { Request, Router } from 'express';
 import { query, oneOf } from 'express-validator';
 import mongoose from 'mongoose';
@@ -140,18 +141,18 @@ const routerFactory = (crowi: Crowi): Router => {
       const idToPageInfoMap: Record<string, IPageInfo | IPageInfoForListing> = {};
 
       const isGuestUser = req.user == null;
-      for (const page of pages) {
+
+      const filteredPages = pages.filter(page => !isUsersHomepage(page.path));
+      for (const page of filteredPages) {
         // construct isIPageInfoForListing
-        // eslint-disable-next-line no-await-in-loop
-        const basicPageInfo = await pageService.constructBasicPageInfo(page, isGuestUser);
+        const basicPageInfo = pageService.constructBasicPageInfo(page, isGuestUser);
 
         const pageInfo = (!isIPageInfoForEntity(basicPageInfo))
           ? basicPageInfo
           // create IPageInfoForListing
           : {
             ...basicPageInfo,
-            // eslint-disable-next-line no-await-in-loop
-            isAbleToDeleteCompletely: await pageService.canDeleteCompletely(page.path, (page.creator as IUserHasId)?._id, req.user, false), // use normal delete config
+            isAbleToDeleteCompletely: pageService.canDeleteCompletely(page.path, (page.creator as IUserHasId)?._id, req.user, false), // use normal delete config
             bookmarkCount: bookmarkCountMap != null ? bookmarkCountMap[page._id] : undefined,
             revisionShortBody: shortBodiesMap != null ? shortBodiesMap[page._id] : undefined,
           } as IPageInfoForListing;
@@ -159,6 +160,26 @@ const routerFactory = (crowi: Crowi): Router => {
         idToPageInfoMap[page._id] = pageInfo;
       }
 
+      const userHomepages = pages.filter(page => isUsersHomepage(page.path));
+      const promiseArray = userHomepages.map(async(page) => {
+        // construct isIPageInfoForListing
+        const basicPageInfo = await pageService.constructBasicPageInfoPromise(page, isGuestUser);
+
+        const pageInfo = (!isIPageInfoForEntity(basicPageInfo))
+          ? basicPageInfo
+          // create IPageInfoForListing
+          : {
+            ...basicPageInfo,
+            isAbleToDeleteCompletely: await pageService.canDeleteCompletelyPromise(page.path, (page.creator as IUserHasId)?._id, req.user, false), // use normal delete config
+            bookmarkCount: bookmarkCountMap != null ? bookmarkCountMap[page._id] : undefined,
+            revisionShortBody: shortBodiesMap != null ? shortBodiesMap[page._id] : undefined,
+          } as IPageInfoForListing;
+
+        idToPageInfoMap[page._id] = pageInfo;
+      });
+
+      await Promise.all(promiseArray);
+
       return res.apiv3(idToPageInfoMap);
     }
     catch (err) {

+ 32 - 4
apps/app/src/server/routes/apiv3/pages.js

@@ -1,7 +1,9 @@
 
 import { PageGrant } from '@growi/core';
 import { ErrorV3 } from '@growi/core/dist/models';
-import { isCreatablePage, isTrashPage, isUserPage } from '@growi/core/dist/utils/page-path-utils';
+import {
+  isCreatablePage, isTrashPage, isUserPage, isUsersHomepage,
+} from '@growi/core/dist/utils/page-path-utils';
 import { normalizePath, addHeadingSlash, attachTitleHeader } from '@growi/core/dist/utils/path-utils';
 
 import { SupportedTargetModel, SupportedAction } from '~/interfaces/activity';
@@ -641,7 +643,16 @@ module.exports = (crowi) => {
 
     const pagesInTrash = await crowi.pageService.findAllTrashPages(req.user);
 
-    const deletablePages = await crowi.pageService.filterPagesByCanDeleteCompletely(pagesInTrash, req.user, true);
+    let deletablePages;
+    deletablePages = pagesInTrash
+      .filter(page => !isUsersHomepage(page.path))
+      .map(page => crowi.pageService.filterPagesByCanDeleteCompletely(page, req.user, true));
+
+    const usersHomepages = pagesInTrash.filter(page => isUsersHomepage(page.path));
+    deletablePages = await Promise.all(
+      usersHomepages
+        .map(page => page.isEmpty || crowi.pageService.canDeleteCompletelyPromise(page.path, page.creator, req.user, true)),
+    );
 
     if (deletablePages.length === 0) {
       const msg = 'No pages can be deleted.';
@@ -910,14 +921,31 @@ module.exports = (crowi) => {
      * Delete Completely
      */
     if (isCompletely) {
-      pagesCanBeDeleted = await crowi.pageService.filterPagesByCanDeleteCompletely(pagesToDelete, req.user, isRecursively);
+      pagesCanBeDeleted = pagesToDelete
+        .filter(page => !isUsersHomepage(page.path))
+        .map(page => crowi.pageService.filterPagesByCanDeleteCompletely(page, req.user, isRecursively));
+
+      const usersHomepages = pagesToDelete.filter(page => isUsersHomepage(page.path));
+      pagesCanBeDeleted = await Promise.all(
+        usersHomepages
+          .map(page => page.isEmpty || crowi.pageService.canDeleteCompletelyPromise(page.path, page.creator, req.user, isRecursively)),
+      );
     }
     /*
      * Trash
      */
     else {
       pagesCanBeDeleted = pagesToDelete.filter(p => p.isEmpty || p.isUpdatable(pageIdToRevisionIdMap[p._id].toString()));
-      pagesCanBeDeleted = await crowi.pageService.filterPagesByCanDelete(pagesToDelete, req.user, isRecursively);
+
+      pagesCanBeDeleted = pagesToDelete
+        .filter(page => !isUsersHomepage(page.path))
+        .map(page => crowi.pageService.filterPagesByCanDelete(page, req.user, isRecursively));
+
+      const usersHomepages = pagesToDelete.filter(page => isUsersHomepage(page.path));
+      pagesCanBeDeleted = await Promise.all(
+        usersHomepages
+          .map(page => page.isEmpty || crowi.pageService.canDeletePromise(page.path, page.creator, req.user, isRecursively)),
+      );
     }
 
     if (pagesCanBeDeleted.length === 0) {

+ 2 - 2
apps/app/src/server/routes/page.js

@@ -762,7 +762,7 @@ module.exports = function(crowi, app) {
 
     try {
       if (isCompletely) {
-        if (!await crowi.pageService.canDeleteCompletely(page.path, creator, req.user, isRecursively)) {
+        if (!await crowi.pageService.canDeleteCompletelyPromise(page.path, creator, req.user, isRecursively)) {
           return res.json(ApiResponse.error('You can not delete this page completely', 'user_not_admin'));
         }
         await crowi.pageService.deleteCompletely(page, req.user, options, isRecursively, false, activityParameters);
@@ -778,7 +778,7 @@ module.exports = function(crowi, app) {
           return res.json(ApiResponse.error('Someone could update this page, so couldn\'t delete.', 'outdated'));
         }
 
-        if (!await crowi.pageService.canDelete(page.path, creator, req.user, isRecursively)) {
+        if (!await crowi.pageService.canDeletePromise(page.path, creator, req.user, isRecursively)) {
           return res.json(ApiResponse.error('You can not delete this page', 'user_not_admin'));
         }
 

+ 72 - 24
apps/app/src/server/service/page.ts

@@ -165,7 +165,33 @@ class PageService {
     this.pageEvent.on('addSeenUsers', this.pageEvent.onAddSeenUsers);
   }
 
-  async canDeleteCompletely(path: string, creatorId: ObjectIdLike, operator: any | null, isRecursively: boolean): Promise<boolean> {
+  canDeleteCompletely(path: string, creatorId: ObjectIdLike, operator: any | null, isRecursively: boolean): boolean {
+    if (operator == null || isTopPage(path) || isUsersTopPage(path) || isUsersHomepage(path)) {
+      return false;
+    }
+
+    const pageCompleteDeletionAuthority = this.crowi.configManager.getConfig('crowi', 'security:pageCompleteDeletionAuthority');
+    const pageRecursiveCompleteDeletionAuthority = this.crowi.configManager.getConfig('crowi', 'security:pageRecursiveCompleteDeletionAuthority');
+
+    const [singleAuthority, recursiveAuthority] = prepareDeleteConfigValuesForCalc(pageCompleteDeletionAuthority, pageRecursiveCompleteDeletionAuthority);
+
+    return this.canDeleteLogic(creatorId, operator, isRecursively, singleAuthority, recursiveAuthority);
+  }
+
+  canDelete(path: string, creatorId: ObjectIdLike, operator: any | null, isRecursively: boolean): boolean {
+    if (operator == null || isTopPage(path) || isUsersTopPage(path) || isUsersHomepage(path)) {
+      return false;
+    }
+
+    const pageDeletionAuthority = this.crowi.configManager.getConfig('crowi', 'security:pageDeletionAuthority');
+    const pageRecursiveDeletionAuthority = this.crowi.configManager.getConfig('crowi', 'security:pageRecursiveDeletionAuthority');
+
+    const [singleAuthority, recursiveAuthority] = prepareDeleteConfigValuesForCalc(pageDeletionAuthority, pageRecursiveDeletionAuthority);
+
+    return this.canDeleteLogic(creatorId, operator, isRecursively, singleAuthority, recursiveAuthority);
+  }
+
+  async canDeleteCompletelyPromise(path: string, creatorId: ObjectIdLike, operator: any | null, isRecursively: boolean): Promise<boolean> {
     if (operator == null || isTopPage(path) || isUsersTopPage(path)) {
       return false;
     }
@@ -192,7 +218,7 @@ class PageService {
     return this.canDeleteLogic(creatorId, operator, isRecursively, singleAuthority, recursiveAuthority);
   }
 
-  async canDelete(path: string, creatorId: ObjectIdLike, operator: any | null, isRecursively: boolean): Promise<boolean> {
+  async canDeletePromise(path: string, creatorId: ObjectIdLike, operator: any | null, isRecursively: boolean): Promise<boolean> {
     if (operator == null || isTopPage(path) || isUsersTopPage(path)) {
       return false;
     }
@@ -251,26 +277,12 @@ class PageService {
     return false;
   }
 
-  async filterPagesByCanDeleteCompletely(pages, user, isRecursively: boolean): Promise<boolean[]> {
-    const filteredPages: boolean[] = [];
-    for (const p of pages) {
-      // eslint-disable-next-line no-await-in-loop
-      const canDeleteCompletely = await this.canDeleteCompletely(p.path, p.creator, user, isRecursively);
-      filteredPages.push(p.isEmpty || canDeleteCompletely);
-    }
-
-    return filteredPages;
+  filterPagesByCanDeleteCompletely(pages, user, isRecursively: boolean) {
+    return pages.filter(p => p.isEmpty || this.canDeleteCompletely(p.path, p.creator, user, isRecursively));
   }
 
-  async filterPagesByCanDelete(pages, user, isRecursively: boolean): Promise<boolean[]> {
-    const filteredPages: boolean[] = [];
-    for (const p of pages) {
-      // eslint-disable-next-line no-await-in-loop
-      const canDelete = await this.canDelete(p.path, p.creator, user, isRecursively);
-      return p.isEmpty || canDelete;
-    }
-
-    return filteredPages;
+  filterPagesByCanDelete(pages, user, isRecursively: boolean) {
+    return pages.filter(p => p.isEmpty || this.canDelete(p.path, p.creator, user, isRecursively));
   }
 
   // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
@@ -306,7 +318,7 @@ class PageService {
     }
 
     const isGuestUser = user == null;
-    const pageInfo = await this.constructBasicPageInfo(page, isGuestUser);
+    const pageInfo = await this.constructBasicPageInfoPromise(page, isGuestUser);
 
     const Bookmark = this.crowi.model('Bookmark');
     const bookmarkCount = await Bookmark.countByPageId(pageId);
@@ -335,8 +347,8 @@ class PageService {
       const notEmptyClosestAncestor = await Page.findNonEmptyClosestAncestor(page.path);
       creatorId = notEmptyClosestAncestor.creator;
     }
-    const isDeletable = await this.canDelete(page.path, creatorId, user, false);
-    const isAbleToDeleteCompletely = await this.canDeleteCompletely(page.path, creatorId, user, false); // use normal delete config
+    const isDeletable = await this.canDeletePromise(page.path, creatorId, user, false);
+    const isAbleToDeleteCompletely = await this.canDeleteCompletelyPromise(page.path, creatorId, user, false); // use normal delete config
 
     return {
       data: page,
@@ -2449,7 +2461,43 @@ class PageService {
     });
   }
 
-  async constructBasicPageInfo(page: PageDocument, isGuestUser?: boolean): Promise<IPageInfo | IPageInfoForEntity> {
+  constructBasicPageInfo(page: PageDocument, isGuestUser?: boolean): IPageInfo | IPageInfoForEntity {
+    let isDeletable = true;
+    if (isGuestUser || isTopPage(page.path) || isUsersTopPage(page.path) || isUsersHomepage(page.path)) {
+      isDeletable = false;
+    }
+
+    if (page.isEmpty) {
+      return {
+        isV5Compatible: true,
+        isEmpty: true,
+        isDeletable: false,
+        isAbleToDeleteCompletely: false,
+        isRevertible: false,
+      };
+    }
+
+    const likers = page.liker.slice(0, 15) as Ref<IUserHasId>[];
+    const seenUsers = page.seenUsers.slice(0, 15) as Ref<IUserHasId>[];
+
+    return {
+      isV5Compatible: isTopPage(page.path) || page.parent != null,
+      isEmpty: false,
+      sumOfLikers: page.liker.length,
+      likerIds: this.extractStringIds(likers),
+      seenUserIds: this.extractStringIds(seenUsers),
+      sumOfSeenUsers: page.seenUsers.length,
+      isDeletable,
+      isAbleToDeleteCompletely: false,
+      isRevertible: isTrashPage(page.path),
+      contentAge: page.getContentAge(),
+      descendantCount: page.descendantCount,
+      commentCount: page.commentCount,
+    };
+
+  }
+
+  async constructBasicPageInfoPromise(page: PageDocument, isGuestUser?: boolean): Promise<IPageInfo | IPageInfoForEntity> {
     let isDeletable = true;
     if (isGuestUser || isTopPage(page.path) || isUsersTopPage(page.path)) {
       isDeletable = false;