Jelajahi Sumber

Implement API to return pagePath and descendantCount

Shun Miyazawa 1 tahun lalu
induk
melakukan
759ea76e9d

+ 9 - 9
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementModal.tsx

@@ -3,16 +3,16 @@ import React, {
 } from 'react';
 
 import {
-  type IGrantedGroup, type IPageHasId, isPopulated,
+  type IGrantedGroup, isPopulated,
 } from '@growi/core';
 import { useTranslation } from 'react-i18next';
 import { Modal, TabContent, TabPane } from 'reactstrap';
 
 import { toastError, toastSuccess } from '~/client/util/toastr';
 import { AiAssistantAccessScope, AiAssistantShareScope } from '~/features/openai/interfaces/ai-assistant';
-import type { IPageForItem } from '~/interfaces/page';
+import type { IPagePathWithDescendantCount, IPageForItem } from '~/interfaces/page';
 import type { PopulatedGrantedGroup } from '~/interfaces/page-grant';
-import { useSWRxPagesByPaths } from '~/stores/page-listing';
+import { useSWRxPagePathsWithDescendantCount } from '~/stores/page';
 import loggerFactory from '~/utils/logger';
 
 import type { SelectedPage } from '../../../../interfaces/selected-page';
@@ -44,11 +44,11 @@ const convertToPopulatedGrantedGroups = (selectedGroups: IGrantedGroup[]): Popul
   return populatedGrantedGroups;
 };
 
-const convertToSelectedPages = (pagePathPatterns: string[], fetchedPageData: IPageHasId[]): SelectedPage[] => {
+const convertToSelectedPages = (pagePathPatterns: string[], pagePathsWithDescendantCount: IPagePathWithDescendantCount[]): SelectedPage[] => {
   return pagePathPatterns.map((pagePathPattern) => {
     const isIncludeSubPage = pagePathPattern.endsWith('/*');
     const path = isIncludeSubPage ? pagePathPattern.slice(0, -2) : pagePathPattern;
-    const page = fetchedPageData.find(page => page.path === path);
+    const page = pagePathsWithDescendantCount.find(page => page.path === path);
     return {
       page: page ?? { path },
       isIncludeSubPage,
@@ -70,7 +70,7 @@ const AiAssistantManagementModalSubstance = (): JSX.Element => {
   const { t } = useTranslation();
   const { mutate: mutateAiAssistants } = useSWRxAiAssistants();
   const { data: aiAssistantManagementModalData, close: closeAiAssistantManagementModal } = useAiAssistantManagementModal();
-  const { data: fetchedPageData } = useSWRxPagesByPaths(
+  const { data: pagePathsWithDescendantCount } = useSWRxPagePathsWithDescendantCount(
     removeGlobPath(aiAssistantManagementModalData?.aiAssistantData?.pagePathPatterns) ?? null,
     undefined,
     true,
@@ -108,10 +108,10 @@ const AiAssistantManagementModalSubstance = (): JSX.Element => {
   }, [aiAssistant?.accessScope, aiAssistant?.additionalInstruction, aiAssistant?.description, aiAssistant?.grantedGroupsForAccessScope, aiAssistant?.grantedGroupsForShareScope, aiAssistant?.name, aiAssistant?.pagePathPatterns, aiAssistant?.shareScope, shouldEdit]);
 
   useEffect(() => {
-    if (shouldEdit && fetchedPageData != null) {
-      setSelectedPages(convertToSelectedPages(aiAssistant.pagePathPatterns, fetchedPageData));
+    if (shouldEdit && pagePathsWithDescendantCount != null) {
+      setSelectedPages(convertToSelectedPages(aiAssistant.pagePathPatterns, pagePathsWithDescendantCount));
     }
-  }, [aiAssistant?.pagePathPatterns, fetchedPageData, shouldEdit]);
+  }, [aiAssistant?.pagePathPatterns, pagePathsWithDescendantCount, shouldEdit]);
 
 
   /*

+ 5 - 0
apps/app/src/interfaces/page.ts

@@ -76,3 +76,8 @@ export type IOptionsForCreate = {
   origin?: Origin
   wip?: boolean,
 };
+
+export type IPagePathWithDescendantCount = {
+  path: string,
+  descendantCount: number,
+};

+ 55 - 4
apps/app/src/server/models/page.ts

@@ -23,7 +23,7 @@ import uniqueValidator from 'mongoose-unique-validator';
 
 import type { ExternalUserGroupDocument } from '~/features/external-user-group/server/models/external-user-group';
 import ExternalUserGroupRelation from '~/features/external-user-group/server/models/external-user-group-relation';
-import type { IOptionsForCreate } from '~/interfaces/page';
+import type { IOptionsForCreate, IPagePathWithDescendantCount } from '~/interfaces/page';
 import type { ObjectIdLike } from '~/server/interfaces/mongoose-utils';
 
 import loggerFactory from '../../utils/logger';
@@ -94,7 +94,10 @@ export interface PageModel extends Model<PageDocument> {
   findByPath(path: string, includeEmpty?: boolean): Promise<HydratedDocument<PageDocument> | null>
   findByPathAndViewer(path: string | null, user, userGroups?, useFindOne?: true, includeEmpty?: boolean): Promise<HydratedDocument<PageDocument> | null>
   findByPathAndViewer(path: string | null, user, userGroups?, useFindOne?: false, includeEmpty?: boolean): Promise<HydratedDocument<PageDocument>[]>
-  countByPathAndViewer(path: string | null, user, userGroups?, includeEmpty?:boolean): Promise<number>
+  descendantCountByPaths(
+    paths: string[], user, userGroups?, includeEmpty?: boolean, includeAnyoneWithTheLink?: boolean
+  ): Promise<IPagePathWithDescendantCount[]>
+  // countByPathAndViewer(path: string | null, user, userGroups?, includeEmpty?:boolean): Promise<number>
   findParentByPath(path: string | null): Promise<HydratedDocument<PageDocument> | null>
   findTargetAndAncestorsByPathOrId(pathOrId: string): Promise<TargetAndAncestorsResult>
   findRecentUpdatedPages(path: string, user, option: FindRecentUpdatedPagesOption, includeEmpty?: boolean): Promise<PaginatedPages>
@@ -186,7 +189,7 @@ schema.plugin(uniqueValidator);
 
 export class PageQueryBuilder {
 
-  query: any;
+  query: mongoose.FilterQuery<PageDocument>;
 
   constructor(query, includeEmpty = false) {
     this.query = query;
@@ -678,7 +681,7 @@ schema.statics.findByPathAndViewer = async function(
  */
 schema.statics.findByPathsAndViewer = async function(
     paths: string[], user, userGroups = null, includeEmpty = false, includeAnyoneWithTheLink = false,
-): Promise<PageDocument[]> {
+): Promise<{path: string, descendantCount: number}[]> {
   if (paths.length === 0) {
     throw new Error('paths are required.');
   }
@@ -691,6 +694,54 @@ schema.statics.findByPathsAndViewer = async function(
   return queryBuilder.query.exec();
 };
 
+schema.statics.descendantCountByPaths = async function(
+    paths: string[],
+    user,
+    userGroups = null,
+    includeEmpty = false,
+    includeAnyoneWithTheLink = false,
+): Promise<IPagePathWithDescendantCount[]> {
+  if (paths.length === 0) {
+    throw new Error('paths are required');
+  }
+
+  const baseQuery = this.find({ path: { $in: paths } });
+  const queryBuilder = new PageQueryBuilder(baseQuery, includeEmpty);
+
+  await queryBuilder.addViewerCondition(user, userGroups, includeAnyoneWithTheLink);
+
+  const conditions = queryBuilder.query._conditions;
+
+  const aggregationPipeline = [
+    {
+      $match: conditions,
+    },
+    {
+      $project: {
+        _id: 0,
+        path: 1,
+        descendantCount: 1,
+      },
+    },
+    {
+      $group: {
+        _id: '$path',
+        descendantCount: { $first: '$descendantCount' },
+      },
+    },
+    {
+      $project: {
+        _id: 0,
+        path: '$_id',
+        descendantCount: 1,
+      },
+    },
+  ];
+
+  const pages = await this.aggregate<IPagePathWithDescendantCount>(aggregationPipeline);
+  return pages;
+};
+
 schema.statics.countByPathAndViewer = async function(path: string | null, user, userGroups = null, includeEmpty = false): Promise<number> {
   if (path == null) {
     throw new Error('path is required.');

+ 4 - 4
apps/app/src/server/routes/apiv3/page/get-pages-by-page-paths.ts → apps/app/src/server/routes/apiv3/page/get-page-paths-with-descendant-count.ts

@@ -15,7 +15,7 @@ import type { ApiV3Response } from '../interfaces/apiv3-response';
 
 const logger = loggerFactory('growi:routes:apiv3:page:get-pages-by-page-paths');
 
-type GetPageByPagePaths = (crowi: Crowi) => RequestHandler[];
+type GetPagePathsWithDescendantCountFactory = (crowi: Crowi) => RequestHandler[];
 
 type ReqQuery = {
   paths: string[],
@@ -27,7 +27,7 @@ type ReqQuery = {
 interface Req extends Request<undefined, ApiV3Response, undefined, ReqQuery> {
   user: IUserHasId,
 }
-export const getPagesByPagePaths: GetPageByPagePaths = (crowi) => {
+export const getPagePathsWithDescendantCountFactory: GetPagePathsWithDescendantCountFactory = (crowi) => {
   const Page = mongoose.model<IPage, PageModel>('Page');
   const loginRequiredStrictly = require('../../../middlewares/login-required')(crowi);
 
@@ -52,8 +52,8 @@ export const getPagesByPagePaths: GetPageByPagePaths = (crowi) => {
         paths, userGroups, isIncludeEmpty, includeAnyoneWithTheLink,
       } = req.query;
       try {
-        const pages = await Page.findByPathsAndViewer(paths, req.user, userGroups, isIncludeEmpty, includeAnyoneWithTheLink);
-        return res.apiv3({ pages });
+        const pagePathsWithDescendantCount = await Page.descendantCountByPaths(paths, req.user, userGroups, isIncludeEmpty, includeAnyoneWithTheLink);
+        return res.apiv3({ pagePathsWithDescendantCount });
       }
       catch (err) {
         logger.error(err);

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

@@ -35,7 +35,7 @@ import type { ApiV3Response } from '../interfaces/apiv3-response';
 
 import { checkPageExistenceHandlersFactory } from './check-page-existence';
 import { createPageHandlersFactory } from './create-page';
-import { getPagesByPagePaths } from './get-pages-by-page-paths';
+import { getPagePathsWithDescendantCountFactory } from './get-page-paths-with-descendant-count';
 import { getYjsDataHandlerFactory } from './get-yjs-data';
 import { publishPageHandlersFactory } from './publish-page';
 import { syncLatestRevisionBodyToYjsDraftHandlerFactory } from './sync-latest-revision-body-to-yjs-draft';
@@ -267,7 +267,7 @@ module.exports = (crowi) => {
     return res.apiv3({ page, pages });
   });
 
-  router.get('/pages', getPagesByPagePaths(crowi));
+  router.get('/page-paths-with-descendant-count', getPagePathsWithDescendantCountFactory(crowi));
 
   router.get('/exist', checkPageExistenceHandlersFactory(crowi));
 

+ 0 - 13
apps/app/src/stores/page-listing.tsx

@@ -29,19 +29,6 @@ export const useSWRxPagesByPath = (path?: Nullable<string>): SWRResponse<IPageHa
   );
 };
 
-export const useSWRxPagesByPaths = (
-    paths?: string[], userGroups?: string[], isIncludeEmpty?: boolean, includeAnyoneWithTheLink?: boolean,
-): SWRResponse<IPageHasId[], Error> => {
-  return useSWR(
-    (paths != null && paths.length !== 0) ? ['/page/pages', paths, userGroups, isIncludeEmpty, includeAnyoneWithTheLink] : null,
-    ([endpoint, paths, userGroups, isIncludeEmpty, includeAnyoneWithTheLink]) => apiv3Get(
-      endpoint, {
-        paths, userGroups, isIncludeEmpty, includeAnyoneWithTheLink,
-      },
-    ).then(result => result.data.pages),
-  );
-};
-
 type RecentApiResult = {
   pages: IPageHasId[],
   totalCount: number,

+ 15 - 0
apps/app/src/stores/page.tsx

@@ -18,6 +18,7 @@ import useSWRMutation, { type SWRMutationResponse } from 'swr/mutation';
 
 import { apiGet } from '~/client/util/apiv1-client';
 import { apiv3Get } from '~/client/util/apiv3-client';
+import type { IPagePathWithDescendantCount } from '~/interfaces/page';
 import type { IRecordApplicableGrant, IResCurrentGrantData } from '~/interfaces/page-grant';
 import {
   useCurrentPathname, useShareLinkId, useIsGuestUser, useIsReadOnlyUser,
@@ -362,3 +363,17 @@ export const useIsRevisionOutdated = (): SWRResponse<boolean, Error> => {
     ([, remoteRevisionId, currentRevisionId]) => { return remoteRevisionId !== currentRevisionId },
   );
 };
+
+
+export const useSWRxPagePathsWithDescendantCount = (
+    paths?: string[], userGroups?: string[], isIncludeEmpty?: boolean, includeAnyoneWithTheLink?: boolean,
+): SWRResponse<IPagePathWithDescendantCount[], Error> => {
+  return useSWR(
+    (paths != null && paths.length !== 0) ? ['/page/page-paths-with-descendant-count', paths, userGroups, isIncludeEmpty, includeAnyoneWithTheLink] : null,
+    ([endpoint, paths, userGroups, isIncludeEmpty, includeAnyoneWithTheLink]) => apiv3Get(
+      endpoint, {
+        paths, userGroups, isIncludeEmpty, includeAnyoneWithTheLink,
+      },
+    ).then(result => result.data.pagePathsWithDescendantCount),
+  );
+};