Browse Source

Merge pull request #10638 from growilabs/fix/176283-externalize-findpageandmetadatabyviewer-to-external-module

imprv: Externalize findPageAndMetaDataByViewer to external module
Yuki Takei 3 months ago
parent
commit
a61ae92a7c

+ 6 - 3
apps/app/src/pages/[[...path]]/page-data-props.ts

@@ -15,11 +15,12 @@ import assert from 'assert';
 import type { HydratedDocument, model } from 'mongoose';
 import type { HydratedDocument, model } from 'mongoose';
 
 
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
-import type { PageDocument, PageModel } from '~/server/models/page';
+import type { PageModel } from '~/server/models/page';
 import type {
 import type {
   IPageRedirect,
   IPageRedirect,
   PageRedirectModel,
   PageRedirectModel,
 } from '~/server/models/page-redirect';
 } from '~/server/models/page-redirect';
+import { findPageAndMetaDataByViewer } from '~/server/service/page/find-page-and-meta-data-by-viewer';
 
 
 import type { CommonEachProps } from '../common-props';
 import type { CommonEachProps } from '../common-props';
 import type {
 import type {
@@ -131,7 +132,7 @@ export async function getPageDataForInitial(
   let pathFromUrl = `/${pathFromQuery.join('/')}`;
   let pathFromUrl = `/${pathFromQuery.join('/')}`;
   pathFromUrl = pathFromUrl === '//' ? '/' : pathFromUrl;
   pathFromUrl = pathFromUrl === '//' ? '/' : pathFromUrl;
 
 
-  const { pageService, configManager } = crowi;
+  const { pageService, pageGrantService, configManager } = crowi;
 
 
   const pageId = _isPermalink(pathFromUrl)
   const pageId = _isPermalink(pathFromUrl)
     ? removeHeadingSlash(pathFromUrl)
     ? removeHeadingSlash(pathFromUrl)
@@ -154,7 +155,9 @@ export async function getPageDataForInitial(
   }
   }
 
 
   // Get full page data
   // Get full page data
-  const pageWithMeta = await pageService.findPageAndMetaDataByViewer(
+  const pageWithMeta = await findPageAndMetaDataByViewer(
+    pageService,
+    pageGrantService,
     pageId,
     pageId,
     resolvedPagePath,
     resolvedPagePath,
     user,
     user,

+ 5 - 2
apps/app/src/pages/share/[[...path]]/page-data-props.ts

@@ -7,6 +7,7 @@ import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { IShareLink } from '~/interfaces/share-link';
 import type { IShareLink } from '~/interfaces/share-link';
 import type { PageModel } from '~/server/models/page';
 import type { PageModel } from '~/server/models/page';
 import type { ShareLinkModel } from '~/server/models/share-link';
 import type { ShareLinkModel } from '~/server/models/share-link';
+import { findPageAndMetaDataByViewer } from '~/server/service/page/find-page-and-meta-data-by-viewer';
 
 
 import type { ShareLinkPageStatesProps } from './types';
 import type { ShareLinkPageStatesProps } from './types';
 
 
@@ -34,7 +35,7 @@ export const getPageDataForInitial = async (
 ): Promise<GetServerSidePropsResult<ShareLinkPageStatesProps>> => {
 ): Promise<GetServerSidePropsResult<ShareLinkPageStatesProps>> => {
   const req = context.req as CrowiRequest;
   const req = context.req as CrowiRequest;
   const { crowi, params } = req;
   const { crowi, params } = req;
-  const { pageService, configManager } = crowi;
+  const { pageService, pageGrantService, configManager } = crowi;
 
 
   if (mongooseModel == null) {
   if (mongooseModel == null) {
     mongooseModel = (await import('mongoose')).model;
     mongooseModel = (await import('mongoose')).model;
@@ -56,7 +57,9 @@ export const getPageDataForInitial = async (
   }
   }
 
 
   const pageId = getIdStringForRef(shareLink.relatedPage);
   const pageId = getIdStringForRef(shareLink.relatedPage);
-  const pageWithMeta = await pageService.findPageAndMetaDataByViewer(
+  const pageWithMeta = await findPageAndMetaDataByViewer(
+    pageService,
+    pageGrantService,
     pageId,
     pageId,
     null,
     null,
     undefined, // no user for share link
     undefined, // no user for share link

+ 16 - 5
apps/app/src/server/routes/apiv3/page/index.ts

@@ -39,6 +39,7 @@ import ShareLink from '~/server/models/share-link';
 import Subscription from '~/server/models/subscription';
 import Subscription from '~/server/models/subscription';
 import { configManager } from '~/server/service/config-manager';
 import { configManager } from '~/server/service/config-manager';
 import { exportService } from '~/server/service/export';
 import { exportService } from '~/server/service/export';
+import { findPageAndMetaDataByViewer } from '~/server/service/page/find-page-and-meta-data-by-viewer';
 import type { IPageGrantService } from '~/server/service/page-grant';
 import type { IPageGrantService } from '~/server/service/page-grant';
 import { preNotifyService } from '~/server/service/pre-notify';
 import { preNotifyService } from '~/server/service/pre-notify';
 import { normalizeLatestRevisionIfBroken } from '~/server/service/revision/normalize-latest-revision-if-broken';
 import { normalizeLatestRevisionIfBroken } from '~/server/service/revision/normalize-latest-revision-if-broken';
@@ -94,13 +95,13 @@ module.exports = (crowi: Crowi) => {
 
 
   const globalNotificationService = crowi.getGlobalNotificationService();
   const globalNotificationService = crowi.getGlobalNotificationService();
   const Page = mongoose.model<IPage, PageModel>('Page');
   const Page = mongoose.model<IPage, PageModel>('Page');
-  const { pageService } = crowi;
+  const { pageService, pageGrantService } = crowi;
 
 
   const activityEvent = crowi.event('activity');
   const activityEvent = crowi.event('activity');
 
 
   const validator = {
   const validator = {
     getPage: [
     getPage: [
-      query('pageId').optional().isString(),
+      query('pageId').isMongoId().optional().isString(),
       query('path').optional().isString(),
       query('path').optional().isString(),
       query('findAll').optional().isBoolean(),
       query('findAll').optional().isBoolean(),
       query('shareLinkId').optional().isMongoId(),
       query('shareLinkId').optional().isMongoId(),
@@ -262,7 +263,9 @@ module.exports = (crowi: Crowi) => {
             return res.apiv3Err('ShareLink is not found', 404);
             return res.apiv3Err('ShareLink is not found', 404);
           }
           }
           return respondWithSinglePage(
           return respondWithSinglePage(
-            await pageService.findPageAndMetaDataByViewer(
+            await findPageAndMetaDataByViewer(
+              pageService,
+              pageGrantService,
               getIdStringForRef(shareLink.relatedPage),
               getIdStringForRef(shareLink.relatedPage),
               path,
               path,
               user,
               user,
@@ -290,7 +293,13 @@ module.exports = (crowi: Crowi) => {
         }
         }
 
 
         return respondWithSinglePage(
         return respondWithSinglePage(
-          await pageService.findPageAndMetaDataByViewer(pageId, path, user),
+          await findPageAndMetaDataByViewer(
+            pageService,
+            pageGrantService,
+            pageId,
+            path,
+            user,
+          ),
         );
         );
       } catch (err) {
       } catch (err) {
         logger.error('get-page-failed', err);
         logger.error('get-page-failed', err);
@@ -583,7 +592,9 @@ module.exports = (crowi: Crowi) => {
       const { pageId } = req.query;
       const { pageId } = req.query;
 
 
       try {
       try {
-        const { meta } = await pageService.findPageAndMetaDataByViewer(
+        const { meta } = await findPageAndMetaDataByViewer(
+          pageService,
+          pageGrantService,
           pageId,
           pageId,
           null,
           null,
           user,
           user,

+ 174 - 0
apps/app/src/server/service/page/find-page-and-meta-data-by-viewer.ts

@@ -0,0 +1,174 @@
+import type {
+  IDataWithRequiredMeta,
+  IPageInfo,
+  IPageInfoExt,
+  IPageInfoForEmpty,
+  IPageInfoForOperation,
+  IPageNotFoundInfo,
+  IUser,
+} from '@growi/core/dist/interfaces';
+import { isIPageInfoForEntity } from '@growi/core/dist/interfaces';
+import { pagePathUtils } from '@growi/core/dist/utils';
+import assert from 'assert';
+import type { HydratedDocument } from 'mongoose';
+import mongoose from 'mongoose';
+
+import type { BookmarkedPage } from '~/interfaces/bookmark-info';
+import type { PageDocument, PageModel } from '~/server/models/page';
+import type { IPageGrantService } from '~/server/service/page-grant';
+
+import Subscription from '../../models/subscription';
+import type { IPageService } from './page-service';
+
+export async function findPageAndMetaDataByViewer(
+  pageService: IPageService,
+  pageGrantService: IPageGrantService,
+
+  pageId: string | null, // either pageId or path must be specified
+  path: string | null, // either pageId or path must be specified
+  user?: HydratedDocument<IUser>,
+  isSharedPage = false,
+): Promise<
+  | IDataWithRequiredMeta<HydratedDocument<PageDocument>, IPageInfoExt>
+  | IDataWithRequiredMeta<null, IPageNotFoundInfo>
+> {
+  assert(pageId != null || path != null);
+
+  const Page = mongoose.model<HydratedDocument<PageDocument>, PageModel>(
+    'Page',
+  );
+
+  let page: HydratedDocument<PageDocument> | null;
+  if (pageId != null) {
+    // prioritized
+    page = await Page.findByIdAndViewer(pageId, user, null, true);
+  } else {
+    page = await Page.findByPathAndViewer(path, user, null, true, true);
+  }
+
+  // not found or forbidden
+  if (page == null) {
+    const count =
+      pageId != null
+        ? await Page.count({ _id: { $eq: pageId } })
+        : await Page.count({ path: { $eq: path } });
+    const isForbidden = count > 0;
+    return {
+      data: null,
+      meta: {
+        isNotFound: true,
+        isForbidden,
+      } satisfies IPageNotFoundInfo,
+    };
+  }
+
+  const isGuestUser = user == null;
+  const basicPageInfo = pageService.constructBasicPageInfo(page, isGuestUser);
+
+  if (isSharedPage) {
+    return {
+      data: page,
+      meta: {
+        ...basicPageInfo,
+        isMovable: false,
+        isDeletable: false,
+        isAbleToDeleteCompletely: false,
+        isRevertible: false,
+        bookmarkCount: 0,
+      } satisfies IPageInfo,
+    };
+  }
+
+  const Bookmark = mongoose.model<
+    BookmarkedPage,
+    { countDocuments; findByPageIdAndUserId }
+  >('Bookmark');
+  const bookmarkCount: number = await Bookmark.countDocuments({
+    page: { $eq: pageId },
+  });
+
+  const pageInfo = {
+    ...basicPageInfo,
+    bookmarkCount,
+  };
+
+  if (isGuestUser) {
+    return {
+      data: page,
+      meta: {
+        ...pageInfo,
+        isDeletable: false,
+        isAbleToDeleteCompletely: false,
+      } satisfies IPageInfo,
+    };
+  }
+
+  const creatorId = await pageService.getCreatorIdForCanDelete(page);
+
+  const userRelatedGroups = await pageGrantService.getUserRelatedGroups(user);
+
+  const canDeleteUserHomepage = await (async () => {
+    // Not a user homepage
+    if (!pagePathUtils.isUsersHomepage(page.path)) {
+      return true;
+    }
+
+    if (!pageService.canDeleteUserHomepageByConfig()) {
+      return false;
+    }
+
+    return await pageService.isUsersHomepageOwnerAbsent(page.path);
+  })();
+
+  const isDeletable =
+    canDeleteUserHomepage &&
+    pageService.canDelete(page, creatorId, user, false);
+
+  const isAbleToDeleteCompletely =
+    canDeleteUserHomepage &&
+    pageService.canDeleteCompletely(
+      page,
+      creatorId,
+      user,
+      false,
+      userRelatedGroups,
+    ); // use normal delete config
+
+  const isBookmarked: boolean = isGuestUser
+    ? false
+    : (await Bookmark.findByPageIdAndUserId(pageId, user._id)) != null;
+
+  if (pageInfo.isEmpty) {
+    return {
+      data: page,
+      meta: {
+        ...pageInfo,
+        isDeletable,
+        isAbleToDeleteCompletely,
+        isBookmarked,
+      } satisfies IPageInfoForEmpty,
+    };
+  }
+
+  // IPageInfoForEmpty and IPageInfoForEntity are mutually exclusive
+  // so hereafter we can safely
+  assert(isIPageInfoForEntity(pageInfo));
+
+  const isLiked: boolean = page.isLiked(user);
+  const subscription = await Subscription.findByUserIdAndTargetId(
+    user._id,
+    page._id,
+  );
+
+  return {
+    data: page,
+    meta: {
+      ...pageInfo,
+      isDeletable,
+      isAbleToDeleteCompletely,
+      isBookmarked,
+      isLiked,
+      subscriptionStatus: subscription?.status,
+    } satisfies IPageInfoForOperation,
+  };
+}

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

@@ -531,151 +531,6 @@ class PageService implements IPageService {
     return this.filterPages(pages, user, isRecursively, this.canDelete);
     return this.filterPages(pages, user, isRecursively, this.canDelete);
   }
   }
 
 
-  // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
-  async findPageAndMetaDataByViewer(
-    pageId: string | null, // either pageId or path must be specified
-    path: string | null, // either pageId or path must be specified
-    user?: HydratedDocument<IUser>,
-    isSharedPage = false,
-  ): Promise<
-    | IDataWithRequiredMeta<HydratedDocument<PageDocument>, IPageInfoExt>
-    | IDataWithRequiredMeta<null, IPageNotFoundInfo>
-  > {
-    assert(pageId != null || path != null);
-
-    const Page = mongoose.model<HydratedDocument<PageDocument>, PageModel>(
-      'Page',
-    );
-
-    let page: HydratedDocument<PageDocument> | null;
-    if (pageId != null) {
-      // prioritized
-      page = await Page.findByIdAndViewer(pageId, user, null, true);
-    } else {
-      page = await Page.findByPathAndViewer(path, user, null, true, true);
-    }
-
-    // not found or forbidden
-    if (page == null) {
-      const count =
-        pageId != null
-          ? await Page.count({ _id: pageId })
-          : await Page.count({ path });
-      const isForbidden = count > 0;
-      return {
-        data: null,
-        meta: {
-          isNotFound: true,
-          isForbidden,
-        } satisfies IPageNotFoundInfo,
-      };
-    }
-
-    const isGuestUser = user == null;
-    const basicPageInfo = this.constructBasicPageInfo(page, isGuestUser);
-
-    if (isSharedPage) {
-      return {
-        data: page,
-        meta: {
-          ...basicPageInfo,
-          isMovable: false,
-          isDeletable: false,
-          isAbleToDeleteCompletely: false,
-          isRevertible: false,
-          bookmarkCount: 0,
-        } satisfies IPageInfo,
-      };
-    }
-
-    const Bookmark = mongoose.model<
-      BookmarkedPage,
-      { countDocuments; findByPageIdAndUserId }
-    >('Bookmark');
-    const bookmarkCount: number = await Bookmark.countDocuments({
-      page: pageId,
-    });
-
-    const pageInfo = {
-      ...basicPageInfo,
-      bookmarkCount,
-    };
-
-    if (isGuestUser) {
-      return {
-        data: page,
-        meta: {
-          ...pageInfo,
-          isDeletable: false,
-          isAbleToDeleteCompletely: false,
-        } satisfies IPageInfo,
-      };
-    }
-
-    const creatorId = await this.getCreatorIdForCanDelete(page);
-
-    const userRelatedGroups =
-      await this.pageGrantService.getUserRelatedGroups(user);
-
-    const canDeleteUserHomepage = await (async () => {
-      // Not a user homepage
-      if (!pagePathUtils.isUsersHomepage(page.path)) {
-        return true;
-      }
-
-      if (!this.canDeleteUserHomepageByConfig()) {
-        return false;
-      }
-
-      return await this.isUsersHomepageOwnerAbsent(page.path);
-    })();
-
-    const isDeletable =
-      canDeleteUserHomepage && this.canDelete(page, creatorId, user, false);
-
-    const isAbleToDeleteCompletely =
-      canDeleteUserHomepage &&
-      this.canDeleteCompletely(page, creatorId, user, false, userRelatedGroups); // use normal delete config
-
-    const isBookmarked: boolean = isGuestUser
-      ? false
-      : (await Bookmark.findByPageIdAndUserId(pageId, user._id)) != null;
-
-    if (pageInfo.isEmpty) {
-      return {
-        data: page,
-        meta: {
-          ...pageInfo,
-          isDeletable,
-          isAbleToDeleteCompletely,
-          isBookmarked,
-        } satisfies IPageInfoForEmpty,
-      };
-    }
-
-    // IPageInfoForEmpty and IPageInfoForEntity are mutually exclusive
-    // so hereafter we can safely
-    assert(isIPageInfoForEntity(pageInfo));
-
-    const isLiked: boolean = page.isLiked(user);
-    const subscription = await Subscription.findByUserIdAndTargetId(
-      user._id,
-      page._id,
-    );
-
-    return {
-      data: page,
-      meta: {
-        ...pageInfo,
-        isDeletable,
-        isAbleToDeleteCompletely,
-        isBookmarked,
-        isLiked,
-        subscriptionStatus: subscription?.status,
-      } satisfies IPageInfoForOperation,
-    };
-  }
-
   private shouldUseV4ProcessForRevert(page): boolean {
   private shouldUseV4ProcessForRevert(page): boolean {
     const Page = mongoose.model('Page') as unknown as PageModel;
     const Page = mongoose.model('Page') as unknown as PageModel;
 
 

+ 6 - 18
apps/app/src/server/service/page/page-service.ts

@@ -62,24 +62,6 @@ export interface IPageService {
     pages: ObjectIdLike[],
     pages: ObjectIdLike[],
     user: IUser | undefined,
     user: IUser | undefined,
   ) => Promise<void>;
   ) => Promise<void>;
-  findPageAndMetaDataByViewer(
-    pageId: string,
-    path: string | null,
-    user?: HydratedDocument<IUser>,
-    isSharedPage?: boolean,
-  ): Promise<
-    | IDataWithRequiredMeta<HydratedDocument<PageDocument>, IPageInfoExt>
-    | IDataWithRequiredMeta<null, IPageNotFoundInfo>
-  >;
-  findPageAndMetaDataByViewer(
-    pageId: string | null,
-    path: string,
-    user?: HydratedDocument<IUser>,
-    isSharedPage?: boolean,
-  ): Promise<
-    | IDataWithRequiredMeta<HydratedDocument<PageDocument>, IPageInfoExt>
-    | IDataWithRequiredMeta<null, IPageNotFoundInfo>
-  >;
   resumeRenameSubOperation(
   resumeRenameSubOperation(
     renamedPage: PageDocument,
     renamedPage: PageDocument,
     pageOp: PageOperationDocument,
     pageOp: PageOperationDocument,
@@ -212,4 +194,10 @@ export interface IPageService {
     options: IOptionsForCreate,
     options: IOptionsForCreate,
     pageOpId: ObjectIdLike,
     pageOpId: ObjectIdLike,
   ): Promise<void>;
   ): Promise<void>;
+
+  getCreatorIdForCanDelete(page: PageDocument): Promise<ObjectIdLike | null>;
+
+  canDeleteUserHomepageByConfig(): boolean;
+
+  isUsersHomepageOwnerAbsent(path: string): Promise<boolean>;
 }
 }