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

Implemented createSystematically and added isSystematically options

Taichi Masuyama 3 лет назад
Родитель
Сommit
b37fc4ac1d

+ 12 - 4
packages/app/src/server/models/obsolete-page.js

@@ -247,15 +247,23 @@ export const getPageSchema = (crowi) => {
     return this.populate('revision');
   };
 
-  pageSchema.methods.applyScope = function(user, grant, grantUserGroupId) {
-    // reset
+  pageSchema.methods.applyScope = function(user, grant, grantUserGroupId, grantedUsers, options = {}) {
+    // Validate
+    if (grant === GRANT_OWNER && options.isSystematically && grantedUsers?.length !== 1) {
+      throw Error('The grantedUsers must exist when (GRANT_OWNER && isSystematically).');
+    }
+    if (grant === GRANT_OWNER && !options.isSystematically && user == null) {
+      throw Error('The user must exist when (GRANT_OWNER && !isSystematically).');
+    }
+
+    // Reset
     this.grantedUsers = [];
     this.grantedGroup = null;
 
     this.grant = grant || GRANT_PUBLIC;
 
-    if (grant !== GRANT_PUBLIC && grant !== GRANT_USER_GROUP && grant !== GRANT_RESTRICTED) {
-      this.grantedUsers.push(user._id);
+    if (grant === GRANT_OWNER) {
+      this.grantedUsers.push(options.isSystematically ? grantedUsers[0] : user._id);
     }
 
     if (grant === GRANT_USER_GROUP) {

+ 63 - 22
packages/app/src/server/models/page.ts

@@ -10,15 +10,17 @@ import mongoose, {
 import mongoosePaginate from 'mongoose-paginate-v2';
 import uniqueValidator from 'mongoose-unique-validator';
 
+import { IPage, IPageHasId } from '~/interfaces/page';
 import { IUserHasId } from '~/interfaces/user';
 import { ObjectIdLike } from '~/server/interfaces/mongoose-utils';
 
-import { IPage, IPageHasId } from '../../interfaces/page';
+
 import loggerFactory from '../../utils/logger';
 import Crowi from '../crowi';
 
 import { getPageSchema, extractToAncestorsPaths, populateDataToShowRevision } from './obsolete-page';
 import { PageRedirectModel } from './page-redirect';
+import assert from 'assert';
 
 const { addTrailingSlash, normalizePath } = pathUtils;
 const { isTopPage, collectAncestorPaths } = pagePathUtils;
@@ -51,11 +53,12 @@ type PaginatedPages = {
   offset: number
 }
 
-export type CreateMethod = (path: string, body: string, user, options) => Promise<PageDocument & { _id: any }>
+export type CreateMethod = (path: string, body: string, user, options: PageCreateOptions) => Promise<PageDocument & { _id: any }>
 export interface PageModel extends Model<PageDocument> {
   [x: string]: any; // for obsolete methods
-  createEmptyPagesByPaths(paths: string[], user: any | null, onlyMigratedAsExistingPages?: boolean, andFilter?): Promise<void>
-  getParentAndFillAncestors(path: string, user): Promise<PageDocument & { _id: any }>
+  // eslint-disable-next-line max-len
+  createEmptyPagesByPaths(paths: string[], user: any | null, onlyMigratedAsExistingPages?: boolean, onlyGrantedAsExistingPages?: boolean, andFilter?): Promise<void>
+  getParentAndFillAncestors(path: string, user, options?: { isSystematically?: boolean }): Promise<PageDocument & { _id: any }>
   findByIdsAndViewer(pageIds: ObjectIdLike[], user, userGroups?, includeEmpty?: boolean): Promise<PageDocument[]>
   findByPathAndViewer(path: string | null, user, userGroups?, useFindOne?: boolean, includeEmpty?: boolean): Promise<PageDocument | PageDocument[] | null>
   findTargetAndAncestorsByPathOrId(pathOrId: string): Promise<TargetAndAncestorsResult>
@@ -65,6 +68,7 @@ export interface PageModel extends Model<PageDocument> {
   generateGrantCondition(
     user, userGroups, showAnyoneKnowsLink?: boolean, showPagesRestrictedByOwner?: boolean, showPagesRestrictedByGroup?: boolean,
   ): { $or: any[] }
+  createSystematically(path: string, mrkdwn: string, options: PageCreateOptions): Promise<PageDocument | null>
 
   PageQueryBuilder: typeof PageQueryBuilder
 
@@ -486,7 +490,13 @@ export class PageQueryBuilder {
  * @param onlyMigratedAsExistingPages Determine whether to include non-migrated pages as existing pages. If a page exists,
  * an empty page will not be created at that page's path.
  */
-schema.statics.createEmptyPagesByPaths = async function(paths: string[], user: any | null, onlyMigratedAsExistingPages = true, filter?): Promise<void> {
+schema.statics.createEmptyPagesByPaths = async function(
+    paths: string[],
+    user: any | null,
+    onlyMigratedAsExistingPages = true,
+    onlyGrantedAsExistingPages = true,
+    andFilter?,
+): Promise<void> {
   const aggregationPipeline: any[] = [];
   // 1. Filter by paths
   aggregationPipeline.push({ $match: { path: { $in: paths } } });
@@ -503,17 +513,19 @@ schema.statics.createEmptyPagesByPaths = async function(paths: string[], user: a
     });
   }
   // 3. Add custom pipeline
-  if (filter != null) {
-    aggregationPipeline.push({ $match: filter });
+  if (andFilter != null) {
+    aggregationPipeline.push({ $match: andFilter });
   }
   // 4. Add grant conditions
   let userGroups = null;
-  if (user != null) {
-    const UserGroupRelation = mongoose.model('UserGroupRelation') as any;
-    userGroups = await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user);
+  if (onlyGrantedAsExistingPages) {
+    if (user != null) {
+      const UserGroupRelation = mongoose.model('UserGroupRelation') as any;
+      userGroups = await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user);
+    }
+    const grantCondition = this.generateGrantCondition(user, userGroups);
+    aggregationPipeline.push({ $match: grantCondition });
   }
-  const grantCondition = this.generateGrantCondition(user, userGroups);
-  aggregationPipeline.push({ $match: grantCondition });
 
   // Run aggregation
   const existingPages = await this.aggregate(aggregationPipeline);
@@ -606,7 +618,7 @@ schema.statics.replaceTargetWithPage = async function(exPage, pageToReplaceWith?
  * @param path string
  * @returns Promise<PageDocument>
  */
-schema.statics.getParentAndFillAncestors = async function(path: string, user): Promise<PageDocument> {
+schema.statics.getParentAndFillAncestors = async function(path: string, user, options?: { isSystematically?: boolean }): Promise<PageDocument> {
   const parentPath = nodePath.dirname(path);
 
   const builder1 = new PageQueryBuilder(this.find({ path: parentPath }), true);
@@ -625,7 +637,8 @@ schema.statics.getParentAndFillAncestors = async function(path: string, user): P
   const ancestorPaths = collectAncestorPaths(path); // paths of parents need to be created
 
   // just create ancestors with empty pages
-  await this.createEmptyPagesByPaths(ancestorPaths, user);
+  const onlyGrantedAsExistingPages = options?.isSystematically;
+  await this.createEmptyPagesByPaths(ancestorPaths, user, true, onlyGrantedAsExistingPages);
 
   // find ancestors
   const builder2 = new PageQueryBuilder(this.find(), true);
@@ -848,11 +861,11 @@ schema.statics.findAncestorsChildrenByPathAndViewer = async function(path: strin
 /*
  * Utils from obsolete-page.js
  */
-async function pushRevision(pageData, newRevision, user) {
+async function pushRevision(pageData, newRevision, user, options?: { isSystematically?: boolean }) {
   await newRevision.save();
 
   pageData.revision = newRevision;
-  pageData.lastUpdateUser = user;
+  pageData.lastUpdateUser = options?.isSystematically ? null : user;
   pageData.updatedAt = Date.now();
 
   return pageData.save();
@@ -1063,7 +1076,9 @@ schema.statics.generateGrantCondition = generateGrantCondition;
 export type PageCreateOptions = {
   format?: string
   grantUserGroupId?: ObjectIdLike
+  grantedUserIds?: ObjectIdLike[]
   grant?: number
+  isSystematically?: boolean
 }
 
 /*
@@ -1075,7 +1090,32 @@ export default (crowi: Crowi): any => {
     pageEvent = crowi.event('page');
   }
 
+  /**
+   * Use this method when the system needs to create a page. Only available when v5 compatible
+   */
+  schema.statics.createSystematically = async function(path: string, mrkdwn: string, options: PageCreateOptions) {
+    if (crowi.pageGrantService == null || crowi.configManager == null || crowi.pageService == null) {
+      throw Error('Crowi is not setup');
+    }
+
+    const isV5Compatible = crowi.configManager.getConfig('crowi', 'app:isV5Compatible');
+    if (!isV5Compatible) {
+      throw Error('This method is only available when v5 compatibale.');
+    }
+
+    const user = null;
+
+    options.isSystematically = true;
+    return (this.create as CreateMethod)(path, mrkdwn, user, options);
+  };
+
   schema.statics.create = async function(path: string, body: string, user, options: PageCreateOptions = {}) {
+    const { isSystematically } = options;
+
+    if (user == null && !isSystematically) {
+      throw Error('Cannot call create() without a parameter "user" when shouldSkipUserValidation is false.');
+    }
+
     if (crowi.pageGrantService == null || crowi.configManager == null || crowi.pageService == null || crowi.pageOperationService == null) {
       throw Error('Crowi is not setup');
     }
@@ -1094,7 +1134,7 @@ export default (crowi: Crowi): any => {
     const Page = this;
     const Revision = crowi.model('Revision');
     const {
-      format = 'markdown', grantUserGroupId,
+      format = 'markdown', grantUserGroupId, grantedUserIds,
     } = options;
     let grant = options.grant;
 
@@ -1116,7 +1156,7 @@ export default (crowi: Crowi): any => {
     /*
      * UserGroup & Owner validation
      */
-    if (grant !== GRANT_RESTRICTED) {
+    if (!isSystematically && grant !== GRANT_RESTRICTED) {
       let isGrantNormalized = false;
       try {
         // It must check descendants as well if emptyTarget is not null
@@ -1160,11 +1200,12 @@ export default (crowi: Crowi): any => {
       page.parent = null;
     }
     else {
-      const parent = await Page.getParentAndFillAncestors(path, user);
+      const options = { isSystematically };
+      const parent = await Page.getParentAndFillAncestors(path, user, options);
       page.parent = parent._id;
     }
 
-    page.applyScope(user, grant, grantUserGroupId);
+    page.applyScope(user, grant, grantUserGroupId, grantedUserIds, { isSystematically });
 
     let savedPage = await page.save();
 
@@ -1182,8 +1223,8 @@ export default (crowi: Crowi): any => {
       logger.error('Failed to delete PageRedirect');
     }
 
-    const newRevision = Revision.prepareRevision(savedPage, body, null, user, { format });
-    savedPage = await pushRevision(savedPage, newRevision, user);
+    const newRevision = Revision.prepareRevision(savedPage, body, null, user, { format, isSystematically });
+    savedPage = await pushRevision(savedPage, newRevision, user, { isSystematically });
     await savedPage.populateDataToShowRevision();
 
     pageEvent.emit('create', savedPage, user);

+ 1 - 1
packages/app/src/server/models/revision.js

@@ -46,7 +46,7 @@ module.exports = function(crowi) {
     }
     const format = options.format || 'markdown';
 
-    if (!user._id) {
+    if (!options.isSystematically && !user._id) {
       throw new Error('Error: user should have _id');
     }
 

+ 36 - 10
packages/app/src/server/service/page.ts

@@ -1741,6 +1741,11 @@ class PageService {
     return;
   }
 
+  // TODO: implement this method
+  async deleteCompletelySystematically() {
+    return;
+  }
+
   async deleteCompletelyRecursivelyMainOperation(page, user, options, pageOpId: ObjectIdLike): Promise<void> {
     await this.deleteCompletelyDescendantsWithStream(page, user, options, false);
 
@@ -2284,17 +2289,24 @@ class PageService {
     }
 
     let page;
+    let systematicallyCreatedPage;
 
     const shouldCreateNewPage = pages[0] == null;
     if (shouldCreateNewPage) {
       const notEmptyParent = await Page.findNotEmptyParentRecursively(emptyPage);
 
-      page = await Page.create(
+      // TODO: implement systematicallyDeletePage method on PageService
+      systematicallyCreatedPage = await Page.createSystematically(
         path,
-        '',
-        user,
-        { grant: notEmptyParent.grant, grantedUsers: notEmptyParent.grantedUsers, grantedGroup: notEmptyParent.grantedGroup },
+        'This page was created by GROWI.',
+        {
+          grant: notEmptyParent.grant,
+          grantedUserIds: notEmptyParent.grantedUsers,
+          grantUserGroupId: notEmptyParent.grantedGroup,
+          isSystematically: true,
+        },
       );
+      page = systematicallyCreatedPage;
     }
     else {
       page = page[0];
@@ -2314,6 +2326,7 @@ class PageService {
       isGrantNormalized = await this.crowi.pageGrantService.isGrantNormalized(user, path, grant, grantedUserIds, grantedGroupId, shouldCheckDescendants);
     }
     catch (err) {
+      // TODO: delete systematicallyCreatedPage
       logger.error(`Failed to validate grant of page at "${path}"`, err);
       throw err;
     }
@@ -2336,12 +2349,20 @@ class PageService {
       });
     }
     catch (err) {
+      // TODO: delete systematicallyCreatedPage
       logger.error('Failed to create PageOperation document.', err);
       throw err;
     }
 
     // no await
-    this.normalizeParentRecursivelyMainOperation(page, user, pageOp._id);
+    (async() => {
+      // Remove the created page if no page has converted
+      const count = await this.normalizeParentRecursivelyMainOperation(page, user, pageOp._id);
+      if (count === 0) {
+        // TODO: delete systematicallyCreatedPage
+        // await this.deleteCompletelySystematically(systematicallyCreatedPage, user, {}, false);
+      }
+    })();
   }
 
   async normalizeParentByPageIdsRecursively(pageIds: ObjectIdLike[], user): Promise<void> {
@@ -2538,7 +2559,7 @@ class PageService {
     }
   }
 
-  async normalizeParentRecursivelyMainOperation(page, user, pageOpId: ObjectIdLike): Promise<void> {
+  async normalizeParentRecursivelyMainOperation(page, user, pageOpId: ObjectIdLike): Promise<number> {
     // Save prevDescendantCount for sub-operation
     const Page = mongoose.model('Page') as unknown as PageModel;
     const { PageQueryBuilder } = Page;
@@ -2548,8 +2569,9 @@ class PageService {
     const exPage = await builder.query.exec();
     const options = { prevDescendantCount: exPage?.descendantCount ?? 0 };
 
+    let count: number;
     try {
-      await this.normalizeParentRecursively([page.path], user);
+      count = await this.normalizeParentRecursively([page.path], user);
     }
     catch (err) {
       logger.error('V5 initial miration failed.', err);
@@ -2565,6 +2587,8 @@ class PageService {
     }
 
     await this.normalizeParentRecursivelySubOperation(page, user, pageOp._id, options);
+
+    return count;
   }
 
   async normalizeParentRecursivelySubOperation(page, user, pageOpId: ObjectIdLike, options: {prevDescendantCount: number}): Promise<void> {
@@ -2703,7 +2727,7 @@ class PageService {
    * @param user To be used to filter pages to update. If null, only public pages will be updated.
    * @returns Promise<void>
    */
-  async normalizeParentRecursively(paths: string[], user: any | null): Promise<void> {
+  async normalizeParentRecursively(paths: string[], user: any | null): Promise<number> {
     const Page = mongoose.model('Page') as unknown as PageModel;
 
     const ancestorPaths = paths.flatMap(p => collectAncestorPaths(p, []));
@@ -2769,7 +2793,7 @@ class PageService {
 
   private async _normalizeParentRecursively(
       pathOrRegExps: (RegExp | string)[], publicPathsToNormalize: string[], grantFiltersByUser: { $or: any[] }, user, count = 0, skiped = 0, isFirst = true,
-  ): Promise<void> {
+  ): Promise<number> {
     const BATCH_SIZE = 100;
     const PAGES_LIMIT = 1000;
 
@@ -2846,7 +2870,7 @@ class PageService {
           { path: { $nin: publicPathsToNormalize }, status: Page.STATUS_PUBLISHED },
         ];
         const filterForApplicableAncestors = { $or: orFilters };
-        await Page.createEmptyPagesByPaths(parentPaths, user, false, filterForApplicableAncestors);
+        await Page.createEmptyPagesByPaths(parentPaths, user, false, true, filterForApplicableAncestors);
 
         // 3. Find parents
         const addGrantCondition = (builder) => {
@@ -2939,6 +2963,8 @@ class PageService {
 
     // End
     socket.emit(SocketEventName.PMEnded, { isSucceeded: true });
+
+    return nextCount;
   }
 
   private async _v5NormalizeIndex() {