Taichi Masuyama 4 년 전
부모
커밋
0b907294e2

+ 1 - 0
packages/app/src/interfaces/page.ts

@@ -3,6 +3,7 @@ import { IRevision } from './revision';
 import { ITag } from './tag';
 
 export type IPage = {
+  _id?: any,
   path: string,
   status: string,
   revision: any & IRevision,

+ 9 - 0
packages/app/src/server/models/obsolete-page.js

@@ -219,6 +219,15 @@ export class PageQueryBuilder {
     return this;
   }
 
+  /*
+   * Add this condition when get any ancestor pages including the target's parent
+   */
+  addConditionToSortAncestorPages() {
+    this.query = this.query.sort('-path');
+
+    return this;
+  }
+
   addConditionToListByPathsArray(paths) {
     this.query = this.query
       .and({

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

@@ -35,6 +35,9 @@ export interface PageDocument extends IPage, Document {}
 export interface PageModel extends Model<PageDocument> {
   createEmptyPagesByPaths(paths: string[]): Promise<void>
   getParentIdAndFillAncestors(path: string): Promise<string | null>
+  findByPathAndViewerV5(path: string | null, user, userGroups): Promise<IPage[]>
+  findSiblingsByPathAndViewer(path: string | null, user, userGroups): Promise<IPage[]>
+  findAncestorsById(path: string): Promise<IPage[]>
 }
 
 const ObjectId = mongoose.Schema.Types.ObjectId;
@@ -79,6 +82,8 @@ schema.plugin(uniqueValidator);
  * Methods
  */
 const collectAncestorPaths = (path: string, ancestorPaths: string[] = []): string[] => {
+  if (isTopPage(path)) return [];
+
   const parentPath = nodePath.dirname(path);
   ancestorPaths.push(parentPath);
 
@@ -127,6 +132,7 @@ schema.statics.getParentIdAndFillAncestors = async function(path: string): Promi
   const builder = new PageQueryBuilder(this.find({}, { _id: 1, path: 1 }));
   const ancestors = await builder
     .addConditionToListByPathsArray(ancestorPaths)
+    .addConditionToSortAncestorPages()
     .query
     .lean()
     .exec();
@@ -157,23 +163,66 @@ schema.statics.getParentIdAndFillAncestors = async function(path: string): Promi
   return parentId;
 };
 
-schema.statics.findByPathAndViewerV5 = async function(path: string | null, user, userGroups): Promise<IPage[]> {
-  if (path == null) {
-    throw new Error('path is required.');
-  }
-
+const addViewerCondition = async(queryBuilder: PageQueryBuilder, user, userGroups): Promise<void> => {
   let relatedUserGroups = userGroups;
   if (user != null && relatedUserGroups == null) {
     const UserGroupRelation: any = mongoose.model('UserGroupRelation');
     relatedUserGroups = await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user);
   }
 
-  const queryBuilder = new PageQueryBuilder(this.find({ path }));
   queryBuilder.addConditionToFilteringByViewer(user, relatedUserGroups, true);
+};
+
+schema.statics.findByPathAndViewerV5 = async function(path: string | null, user, userGroups): Promise<IPage[]> {
+  if (path == null) {
+    throw new Error('path is required.');
+  }
+
+  const queryBuilder = new PageQueryBuilder(this.find({ path }));
+  await addViewerCondition(queryBuilder, user, userGroups);
 
   return queryBuilder.query.exec();
 };
 
+schema.statics.findSiblingsByPathAndViewer = async function(path: string | null, user, userGroups): Promise<IPage[]> {
+  if (path == null) {
+    throw new Error('path is required.');
+  }
+
+  const parentPath = nodePath.dirname(path);
+
+  // regexr.com/6889f
+  // ex. /parent/any_child OR /any_level1
+  let regexp = new RegExp(`^${parentPath}(\\/[^/]+)\\/?$`, 'g');
+  // ex. / OR /any_level1
+  if (isTopPage(path)) regexp = /^\/[^/]*$/g;
+
+  const queryBuilder = new PageQueryBuilder(this.find({ path: regexp }));
+  await addViewerCondition(queryBuilder, user, userGroups);
+
+  return queryBuilder.query.lean().exec();
+};
+
+schema.statics.findAncestorsByPath = async function(path: string): Promise<IPage[]> {
+  const ancestorPaths = collectAncestorPaths(path);
+
+  // Do not populate
+  const queryBuilder = new PageQueryBuilder(this.find());
+  const _ancestors: IPage[] = await queryBuilder
+    .addConditionToListByPathsArray(ancestorPaths)
+    .addConditionToSortAncestorPages()
+    .query
+    .lean()
+    .exec();
+
+  // no same path pages
+  const ancestorsMap: Map<string, IPage> = new Map();
+  _ancestors.forEach(page => ancestorsMap.set(page.path, page));
+  const ancestors = Array.from(ancestorsMap.values());
+
+  return ancestors;
+};
+
 
 /*
  * Merge obsolete page model methods and define new methods which depend on crowi instance

+ 2 - 2
packages/app/src/server/routes/apiv3/apiv3-response.ts

@@ -1,6 +1,6 @@
 import { Response } from 'express';
 
 export interface ApiV3Response extends Response {
-  apiV3(): any
-  apiV3Err(): any
+  apiv3?(obj?: any, status?: number): any
+  apiv3Err?(_err: any, status?: number, info?: any): any
 }

+ 54 - 5
packages/app/src/server/routes/apiv3/page-tree.ts

@@ -1,19 +1,68 @@
 import express, {
-  NextFunction, Request, RequestHandler, Response, Router,
+  NextFunction, Request, RequestHandler, Router,
 } from 'express';
+import { body, query, param } from 'express-validator';
+import { IPage } from '../../../interfaces/page';
+
+import ErrorV3 from '../../models/vo/error-apiv3';
+import loggerFactory from '../../../utils/logger';
 import Crowi from '../../crowi';
 import { ApiV3Response } from './apiv3-response';
 
+const logger = loggerFactory('growi:routes:apiv3:page-tree');
+
+/*
+ * Types & Interfaces
+ */
+interface AuthorizedRequest extends Request {
+  user?: any
+}
 
-const getPagesAroundTarget = (req: Request, res: ApiV3Response): any => {
-  return res.apiV3();
+/*
+ * Validators
+ */
+const validator = {
+  getPagesAroundTarget: [
+    query('id').isMongoId().withMessage('id is required'),
+    query('path').isMongoId().withMessage('path is required'),
+  ],
 };
 
+/*
+ * Routes
+ */
 export default (crowi: Crowi): Router => {
   const accessTokenParser = require('../../middlewares/access-token-parser')(crowi);
   // Do not use loginRequired with isGuestAllowed true since page tree may show private pages' title
   const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
 
-  return express.Router()
-    .get('/pages', accessTokenParser, loginRequiredStrictly, getPagesAroundTarget);
+  const router = express.Router();
+
+
+  // eslint-disable-next-line max-len
+  router.get('/pages', accessTokenParser, loginRequiredStrictly, ...validator.getPagesAroundTarget, async(req: AuthorizedRequest, res: ApiV3Response): Promise<any> => {
+    const { id, path } = req.query;
+
+    const Page = crowi.model('Page');
+
+    let siblings: IPage[];
+    let ancestors: IPage[];
+    try {
+      siblings = await Page.findSiblingsByPathAndViewer(path, req.user);
+      ancestors = await Page.findAncestorsByPath(path);
+    }
+    catch (err) {
+      logger.error('Error occurred while finding pages.', err);
+      return res.apiv3Err(new ErrorV3('Error occurred while finding pages.'));
+    }
+    const target = siblings.filter(page => page._id.toString() === id)?.[0];
+
+    if (target == null) {
+      throw Error('Target must exist.');
+    }
+
+    return res.apiv3({ target, ancestors, pages: siblings });
+  });
+
+  return router;
 };