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

Merge pull request #8323 from weseek/support/update-page-service

support: Add test for delete-completely-user-home-by-system.ts
Yuki Takei 2 лет назад
Родитель
Сommit
69ecb715b8

+ 3 - 1
apps/app/src/server/events/user.ts

@@ -7,6 +7,8 @@ import mongoose from 'mongoose';
 import type { PageModel } from '~/server/models/page';
 import loggerFactory from '~/utils/logger';
 
+import { deleteCompletelyUserHomeBySystem } from '../service/page/delete-completely-user-home-by-system';
+
 const logger = loggerFactory('growi:events:user');
 
 class UserEvent extends EventEmitter {
@@ -30,7 +32,7 @@ class UserEvent extends EventEmitter {
       // Since the type of page.creator is 'any', we resort to the following comparison,
       // checking if page.creator.toString() is not equal to user._id.toString(). Our code covers null, string, or object types.
       if (page != null && page.creator != null && page.creator.toString() !== user._id.toString()) {
-        await this.crowi.pageService.deleteCompletelyUserHomeBySystem(userHomepagePath);
+        await deleteCompletelyUserHomeBySystem(userHomepagePath, this.crowi.pageService);
         page = null;
       }
 

+ 1 - 0
apps/app/src/server/models/page.ts

@@ -66,6 +66,7 @@ export type CreateMethod = (path: string, body: string, user, options: PageCreat
 export interface PageModel extends Model<PageDocument> {
   [x: string]: any; // for obsolete static methods
   findByIdsAndViewer(pageIds: ObjectIdLike[], user, userGroups?, includeEmpty?: boolean, includeAnyoneWithTheLink?: boolean): Promise<PageDocument[]>
+  findByPath(path: string, includeEmpty?: boolean): Promise<PageDocument | null>
   findByPathAndViewer(path: string | null, user, userGroups?, useFindOne?: true, includeEmpty?: boolean): Promise<PageDocument & HasObjectId | null>
   findByPathAndViewer(path: string | null, user, userGroups?, useFindOne?: false, includeEmpty?: boolean): Promise<(PageDocument & HasObjectId)[]>
   countByPathAndViewer(path: string | null, user, userGroups?, includeEmpty?:boolean): Promise<number>

+ 2 - 1
apps/app/src/server/routes/apiv3/users.js

@@ -8,6 +8,7 @@ import Activity from '~/server/models/activity';
 import ExternalAccount from '~/server/models/external-account';
 import UserGroupRelation from '~/server/models/user-group-relation';
 import { configManager } from '~/server/service/config-manager';
+import { deleteCompletelyUserHomeBySystem } from '~/server/service/page/delete-completely-user-home-by-system';
 import loggerFactory from '~/utils/logger';
 
 import { generateAddActivityMiddleware } from '../../middlewares/add-activity';
@@ -819,7 +820,7 @@ module.exports = (crowi) => {
       activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ADMIN_USERS_REMOVE });
 
       if (isUsersHomepageDeletionEnabled && isForceDeleteUserHomepageOnUserDeletion) {
-        crowi.pageService.deleteCompletelyUserHomeBySystem(homepagePath);
+        deleteCompletelyUserHomeBySystem(homepagePath, crowi.pageService);
       }
 
       return res.apiv3({ user: serializedUser });

+ 2 - 0
apps/app/src/server/service/page/consts.ts

@@ -0,0 +1,2 @@
+export const BULK_REINDEX_SIZE = 100;
+export const LIMIT_FOR_MULTIPLE_PAGE_OP = 20;

+ 121 - 0
apps/app/src/server/service/page/delete-completely-user-home-by-system.integ.ts

@@ -0,0 +1,121 @@
+import type EventEmitter from 'events';
+
+import mongoose from 'mongoose';
+import { vi } from 'vitest';
+import { mock } from 'vitest-mock-extended';
+
+import { getPageSchema } from '~/server/models/obsolete-page';
+import { configManager } from '~/server/service/config-manager';
+
+import pageModel from '../../models/page';
+
+import { deleteCompletelyUserHomeBySystem } from './delete-completely-user-home-by-system';
+import type { IPageService } from './page-service';
+
+// TODO: use actual user model after ~/server/models/user.js becomes importable in vitest
+// ref: https://github.com/vitest-dev/vitest/issues/846
+const userSchema = new mongoose.Schema({
+  name: { type: String },
+  username: { type: String, required: true, unique: true },
+  email: { type: String, unique: true, sparse: true },
+}, {
+  timestamps: true,
+});
+const User = mongoose.model('User', userSchema);
+
+describe('delete-completely-user-home-by-system test', () => {
+  let Page;
+
+  const initialEnv = process.env;
+
+  const userId1 = new mongoose.Types.ObjectId();
+  const user1HomepageId = new mongoose.Types.ObjectId();
+
+  beforeAll(async() => {
+    // setup page model
+    getPageSchema(null);
+    pageModel(null);
+    Page = mongoose.model('Page');
+
+    // setup config
+    await configManager.loadConfigs();
+    await configManager.updateConfigsInTheSameNamespace('crowi', { 'app:isV5Compatible': true });
+    const isV5Compatible = configManager.getConfig('crowi', 'app:isV5Compatible');
+    expect(isV5Compatible).toBeTruthy();
+
+    // setup user documents
+    const user1 = await User.create({
+      _id: userId1, name: 'user1', username: 'user1', email: 'user1@example.com',
+    });
+
+    // setup page documents
+    await Page.insertMany([
+      {
+        _id: user1HomepageId,
+        path: '/user/user1',
+        grant: Page.GRANT_PUBLIC,
+        creator: user1,
+        lastUpdateUser: user1,
+        parent: new mongoose.Types.ObjectId(),
+        descendantCount: 2,
+        isEmpty: false,
+        status: 'published',
+      },
+      {
+        path: '/user/user1/subpage1',
+        grant: Page.GRANT_PUBLIC,
+        creator: user1,
+        lastUpdateUser: user1,
+        parent: user1HomepageId,
+      },
+      {
+        path: '/user/user1/subpage2',
+        grant: Page.GRANT_PUBLIC,
+        creator: user1,
+        lastUpdateUser: user1,
+        parent: user1HomepageId,
+      },
+    ]);
+  });
+
+  afterAll(() => {
+    process.env = initialEnv;
+    Page.deleteMany({});
+  });
+
+  describe('deleteCompletelyUserHomeBySystem()', () => {
+    // setup
+    const mockUpdateDescendantCountOfAncestors = vi.fn().mockImplementation(() => Promise.resolve());
+    const mockDeleteCompletelyOperation = vi.fn().mockImplementation(() => Promise.resolve());
+    const mockPageEvent = mock<EventEmitter>();
+    const mockDeleteMultipleCompletely = vi.fn().mockImplementation(() => Promise.resolve());
+
+    const mockPageService: IPageService = {
+      updateDescendantCountOfAncestors: mockUpdateDescendantCountOfAncestors,
+      deleteCompletelyOperation: mockDeleteCompletelyOperation,
+      getEventEmitter: () => mockPageEvent,
+      deleteMultipleCompletely: mockDeleteMultipleCompletely,
+    };
+
+    it('should call used page service functions', async() => {
+      // when
+      const existsUserHomepagePath = '/user/user1';
+      await deleteCompletelyUserHomeBySystem(existsUserHomepagePath, mockPageService);
+
+      // then
+      expect(mockUpdateDescendantCountOfAncestors).toHaveBeenCalled();
+      expect(mockDeleteCompletelyOperation).toHaveBeenCalled();
+      expect(mockPageEvent.emit).toHaveBeenCalled();
+      expect(mockDeleteMultipleCompletely).toHaveBeenCalled();
+    });
+
+    it('should throw error if userHomepage is not exists', async() => {
+      // when
+      const notExistsUserHomepagePath = '/user/not_exists_user';
+      const deleteUserHomepageFunction = deleteCompletelyUserHomeBySystem(notExistsUserHomepagePath, mockPageService);
+
+      // then
+      expect(deleteUserHomepageFunction).rejects.toThrow('user homepage is not found.');
+    });
+  });
+});

+ 122 - 0
apps/app/src/server/service/page/delete-completely-user-home-by-system.ts

@@ -0,0 +1,122 @@
+import { Writable } from 'stream';
+
+import { getIdForRef } from '@growi/core';
+import type { IPage, Ref } from '@growi/core';
+import { isUsersHomepage } from '@growi/core/dist/utils/page-path-utils';
+import mongoose from 'mongoose';
+import streamToPromise from 'stream-to-promise';
+
+import type { PageModel } from '~/server/models/page';
+import { createBatchStream } from '~/server/util/batch-stream';
+import loggerFactory from '~/utils/logger';
+
+import { BULK_REINDEX_SIZE } from './consts';
+import type { IPageService } from './page-service';
+import { shouldUseV4Process } from './should-use-v4-process';
+
+const logger = loggerFactory('growi:services:page');
+
+
+type IPageUnderV5 = Omit<IPage, 'parent'> & { parent: Ref<IPage> }
+
+const _shouldUseV5Process = (page: IPage): page is IPageUnderV5 => {
+  return !shouldUseV4Process(page);
+};
+
+/**
+   * @description This function is intended to be used exclusively for forcibly deleting the user homepage by the system.
+   * It should only be called from within the appropriate context and with caution as it performs a system-level operation.
+   *
+   * @param {string} userHomepagePath - The path of the user's homepage.
+   * @returns {Promise<void>} - A Promise that resolves when the deletion is complete.
+   * @throws {Error} - If an error occurs during the deletion process.
+   */
+export const deleteCompletelyUserHomeBySystem = async(userHomepagePath: string, pageService: IPageService): Promise<void> => {
+  if (!isUsersHomepage(userHomepagePath)) {
+    const msg = 'input value is not user homepage path.';
+    logger.error(msg);
+    throw new Error(msg);
+  }
+
+  const Page = mongoose.model<IPage, PageModel>('Page');
+  const userHomepage = await Page.findByPath(userHomepagePath, true);
+
+  if (userHomepage == null) {
+    const msg = 'user homepage is not found.';
+    logger.error(msg);
+    throw new Error(msg);
+  }
+
+  const shouldUseV5Process = _shouldUseV5Process(userHomepage);
+
+  const ids = [userHomepage._id];
+  const paths = [userHomepage.path];
+
+  try {
+    if (shouldUseV5Process) {
+      // Ensure consistency of ancestors
+      const inc = userHomepage.isEmpty ? -userHomepage.descendantCount : -(userHomepage.descendantCount + 1);
+      await pageService.updateDescendantCountOfAncestors(getIdForRef(userHomepage.parent), inc, true);
+    }
+
+    // Delete the user's homepage
+    await pageService.deleteCompletelyOperation(ids, paths);
+
+    if (shouldUseV5Process) {
+      // Remove leaf empty pages
+      await Page.removeLeafEmptyPagesRecursively(getIdForRef(userHomepage.parent));
+    }
+
+    if (!userHomepage.isEmpty) {
+      // Emit an event for the search service
+      pageService.getEventEmitter().emit('deleteCompletely', userHomepage);
+    }
+
+    const { PageQueryBuilder } = Page;
+
+    // Find descendant pages with system deletion condition
+    const builder = new PageQueryBuilder(Page.find(), true)
+      .addConditionForSystemDeletion()
+      .addConditionToListOnlyDescendants(userHomepage.path, {});
+
+    // Stream processing to delete descendant pages
+    // ────────┤ start │─────────
+    const readStream = await builder
+      .query
+      .lean()
+      .cursor({ batchSize: BULK_REINDEX_SIZE });
+
+    let count = 0;
+
+    const writeStream = new Writable({
+      objectMode: true,
+      async write(batch, encoding, callback) {
+        try {
+          count += batch.length;
+          // Delete multiple pages completely
+          await pageService.deleteMultipleCompletely(batch, undefined);
+          logger.debug(`Adding pages progressing: (count=${count})`);
+        }
+        catch (err) {
+          logger.error('addAllPages error on add anyway: ', err);
+        }
+        callback();
+      },
+      final(callback) {
+        logger.debug(`Adding pages has completed: (totalCount=${count})`);
+        callback();
+      },
+    });
+
+    readStream
+      .pipe(createBatchStream(BULK_REINDEX_SIZE))
+      .pipe(writeStream);
+
+    await streamToPromise(writeStream);
+    // ────────┤ end │─────────
+  }
+  catch (err) {
+    logger.error('Error occurred while deleting user homepage and subpages.', err);
+    throw err;
+  }
+};

+ 45 - 146
apps/app/src/server/service/page.ts → apps/app/src/server/service/page/index.ts

@@ -1,3 +1,4 @@
+import type EventEmitter from 'events';
 import pathlib from 'path';
 import { Readable, Writable } from 'stream';
 
@@ -5,7 +6,7 @@ import type {
   Ref, HasObjectId, IUserHasId, IUser,
   IPage, IPageInfo, IPageInfoAll, IPageInfoForEntity, IPageWithMeta, IGrantedGroup,
 } from '@growi/core';
-import { PageGrant, PageStatus, getIdForRef } from '@growi/core';
+import { PageGrant, PageStatus } from '@growi/core';
 import {
   pagePathUtils, pathUtils,
 } from '@growi/core/dist/utils';
@@ -31,20 +32,26 @@ import { createBatchStream } from '~/server/util/batch-stream';
 import loggerFactory from '~/utils/logger';
 import { prepareDeleteConfigValuesForCalc } from '~/utils/page-delete-config';
 
-import { ObjectIdLike } from '../interfaces/mongoose-utils';
-import { Attachment } from '../models';
-import { PathAlreadyExistsError } from '../models/errors';
-import { IOptionsForCreate, IOptionsForUpdate } from '../models/interfaces/page-operation';
-import PageOperation, { PageOperationDocument } from '../models/page-operation';
-import { PageRedirectModel } from '../models/page-redirect';
-import { serializePageSecurely } from '../models/serializers/page-serializer';
-import ShareLink from '../models/share-link';
-import Subscription from '../models/subscription';
-import UserGroupRelation from '../models/user-group-relation';
-import { V5ConversionError } from '../models/vo/v5-conversion-error';
-import { divideByType } from '../util/granted-group';
-
-import { configManager } from './config-manager';
+import { ObjectIdLike } from '../../interfaces/mongoose-utils';
+import { Attachment } from '../../models';
+import { PathAlreadyExistsError } from '../../models/errors';
+import { IOptionsForCreate, IOptionsForUpdate } from '../../models/interfaces/page-operation';
+import PageOperation, { PageOperationDocument } from '../../models/page-operation';
+import { PageRedirectModel } from '../../models/page-redirect';
+import { serializePageSecurely } from '../../models/serializers/page-serializer';
+import ShareLink from '../../models/share-link';
+import Subscription from '../../models/subscription';
+import UserGroupRelation from '../../models/user-group-relation';
+import { V5ConversionError } from '../../models/vo/v5-conversion-error';
+import { divideByType } from '../../util/granted-group';
+import { configManager } from '../config-manager';
+
+import { BULK_REINDEX_SIZE, LIMIT_FOR_MULTIPLE_PAGE_OP } from './consts';
+import { IPageService } from './page-service';
+import { shouldUseV4Process } from './should-use-v4-process';
+
+export * from './page-service';
+
 
 const debug = require('debug')('growi:services:page');
 
@@ -56,9 +63,6 @@ const {
 
 const { addTrailingSlash } = pathUtils;
 
-const BULK_REINDEX_SIZE = 100;
-const LIMIT_FOR_MULTIPLE_PAGE_OP = 20;
-
 // TODO: improve type
 class PageCursorsForDescendantsFactory {
 
@@ -140,11 +144,16 @@ class PageCursorsForDescendantsFactory {
 
 }
 
-class PageService {
+
+class PageService implements IPageService {
 
   crowi: any;
 
-  pageEvent: any;
+  pageEvent: EventEmitter & {
+    onCreate,
+    onCreateMany,
+    onAddSeenUsers,
+  };
 
   tagEvent: any;
 
@@ -171,6 +180,10 @@ class PageService {
     this.pageEvent.on('addSeenUsers', this.pageEvent.onAddSeenUsers);
   }
 
+  getEventEmitter(): EventEmitter {
+    return this.pageEvent;
+  }
+
   canDeleteCompletely(path: string, creatorId: ObjectIdLike, operator: any | null, isRecursively: boolean): boolean {
     if (operator == null || isTopPage(path) || isUsersTopPage(path)) return false;
 
@@ -371,20 +384,6 @@ class PageService {
     };
   }
 
-  private shouldUseV4Process(page): boolean {
-    const Page = mongoose.model('Page') as unknown as PageModel;
-
-    const isTrashPage = page.status === Page.STATUS_DELETED;
-    const isPageMigrated = page.parent != null;
-    const isV5Compatible = this.crowi.configManager.getConfig('crowi', 'app:isV5Compatible');
-    const isRoot = isTopPage(page.path);
-    const isPageRestricted = page.grant === Page.GRANT_RESTRICTED;
-
-    const shouldUseV4Process = !isRoot && (!isV5Compatible || !isPageMigrated || isTrashPage || isPageRestricted);
-
-    return shouldUseV4Process;
-  }
-
   private shouldUseV4ProcessForRevert(page): boolean {
     const Page = mongoose.model('Page') as unknown as PageModel;
 
@@ -453,8 +452,8 @@ class PageService {
     }
 
     // Separate v4 & v5 process
-    const shouldUseV4Process = this.shouldUseV4Process(page);
-    if (shouldUseV4Process) {
+    const isShouldUseV4Process = shouldUseV4Process(page);
+    if (isShouldUseV4Process) {
       return this.renamePageV4(page, newPagePath, user, options);
     }
 
@@ -1014,8 +1013,8 @@ class PageService {
     newPagePath = this.crowi.xss.process(newPagePath); // eslint-disable-line no-param-reassign
 
     // 1. Separate v4 & v5 process
-    const shouldUseV4Process = this.shouldUseV4Process(page);
-    if (shouldUseV4Process) {
+    const isShouldUseV4Process = shouldUseV4Process(page);
+    if (isShouldUseV4Process) {
       return this.duplicateV4(page, newPagePath, user, isRecursively);
     }
 
@@ -1439,8 +1438,8 @@ class PageService {
     const Page = mongoose.model('Page') as PageModel;
 
     // Separate v4 & v5 process
-    const shouldUseV4Process = this.shouldUseV4Process(page);
-    if (shouldUseV4Process) {
+    const isShouldUseV4Process = shouldUseV4Process(page);
+    if (isShouldUseV4Process) {
       return this.deletePageV4(page, user, options, isRecursively);
     }
     // Validate
@@ -1761,7 +1760,7 @@ class PageService {
     return nDeletedNonEmptyPages;
   }
 
-  private async deleteCompletelyOperation(pageIds, pagePaths) {
+  async deleteCompletelyOperation(pageIds, pagePaths): Promise<void> {
     // Delete Bookmarks, Attachments, Revisions, Pages and emit delete
     const Bookmark = this.crowi.model('Bookmark');
     const Comment = this.crowi.model('Comment');
@@ -1773,7 +1772,7 @@ class PageService {
     const { attachmentService } = this.crowi;
     const attachments = await Attachment.find({ page: { $in: pageIds } });
 
-    return Promise.all([
+    await Promise.all([
       Bookmark.deleteMany({ page: { $in: pageIds } }),
       Comment.deleteMany({ page: { $in: pageIds } }),
       PageTagRelation.deleteMany({ relatedPage: { $in: pageIds } }),
@@ -1786,7 +1785,7 @@ class PageService {
   }
 
   // delete multiple pages
-  private async deleteMultipleCompletely(pages, user, options = {}) {
+  async deleteMultipleCompletely(pages, user) {
     const ids = pages.map(page => (page._id));
     const paths = pages.map(page => (page.path));
 
@@ -1814,8 +1813,8 @@ class PageService {
     }
 
     // v4 compatible process
-    const shouldUseV4Process = this.shouldUseV4Process(page);
-    if (shouldUseV4Process) {
+    const isShouldUseV4Process = shouldUseV4Process(page);
+    if (isShouldUseV4Process) {
       return this.deleteCompletelyV4(page, user, options, isRecursively, preventEmitting);
     }
 
@@ -1986,7 +1985,7 @@ class PageService {
 
         try {
           count += batch.length;
-          await deleteMultipleCompletely(batch, user, options);
+          await deleteMultipleCompletely(batch, user);
           const subscribedUsers = await Subscription.getSubscriptions(batch);
           subscribedUsers.forEach((eachUser) => {
             descendantsSubscribedSets.add(eachUser);
@@ -2038,106 +2037,6 @@ class PageService {
     }
   }
 
-  /**
-   * @description This function is intended to be used exclusively for forcibly deleting the user homepage by the system.
-   * It should only be called from within the appropriate context and with caution as it performs a system-level operation.
-   *
-   * @param {string} userHomepagePath - The path of the user's homepage.
-   * @returns {Promise<void>} - A Promise that resolves when the deletion is complete.
-   * @throws {Error} - If an error occurs during the deletion process.
-   */
-  async deleteCompletelyUserHomeBySystem(userHomepagePath: string): Promise<void> {
-    if (!isUsersHomepage(userHomepagePath)) {
-      const msg = 'input value is not user homepage path.';
-      logger.error(msg);
-      throw new Error(msg);
-    }
-
-    const Page = mongoose.model<IPage, PageModel>('Page');
-    const userHomepage = await Page.findByPath(userHomepagePath, true);
-
-    if (userHomepage == null) {
-      const msg = 'user homepage is not found.';
-      logger.error(msg);
-      throw new Error(msg);
-    }
-
-    const shouldUseV4Process = this.shouldUseV4Process(userHomepage);
-
-    const ids = [userHomepage._id];
-    const paths = [userHomepage.path];
-    const parentId = getIdForRef(userHomepage.parent);
-
-    try {
-      if (!shouldUseV4Process) {
-        // Ensure consistency of ancestors
-        const inc = userHomepage.isEmpty ? -userHomepage.descendantCount : -(userHomepage.descendantCount + 1);
-        await this.updateDescendantCountOfAncestors(parentId, inc, true);
-      }
-
-      // Delete the user's homepage
-      await this.deleteCompletelyOperation(ids, paths);
-
-      if (!shouldUseV4Process) {
-        // Remove leaf empty pages
-        await Page.removeLeafEmptyPagesRecursively(parentId);
-      }
-
-      if (!userHomepage.isEmpty) {
-        // Emit an event for the search service
-        this.pageEvent.emit('deleteCompletely', userHomepage);
-      }
-
-      const { PageQueryBuilder } = Page;
-
-      // Find descendant pages with system deletion condition
-      const builder = new PageQueryBuilder(Page.find(), true)
-        .addConditionForSystemDeletion()
-        .addConditionToListOnlyDescendants(userHomepage.path, {});
-
-      // Stream processing to delete descendant pages
-      // ────────┤ start │─────────
-      const readStream = await builder
-        .query
-        .lean()
-        .cursor({ batchSize: BULK_REINDEX_SIZE });
-
-      let count = 0;
-
-      const deleteMultipleCompletely = this.deleteMultipleCompletely.bind(this);
-      const writeStream = new Writable({
-        objectMode: true,
-        async write(batch, encoding, callback) {
-          try {
-            count += batch.length;
-            // Delete multiple pages completely
-            await deleteMultipleCompletely(batch, null, {});
-            logger.debug(`Adding pages progressing: (count=${count})`);
-          }
-          catch (err) {
-            logger.error('addAllPages error on add anyway: ', err);
-          }
-          callback();
-        },
-        final(callback) {
-          logger.debug(`Adding pages has completed: (totalCount=${count})`);
-          callback();
-        },
-      });
-
-      readStream
-        .pipe(createBatchStream(BULK_REINDEX_SIZE))
-        .pipe(writeStream);
-
-      await streamToPromise(writeStream);
-      // ────────┤ end │─────────
-    }
-    catch (err) {
-      logger.error('Error occurred while deleting user homepage and subpages.', err);
-      throw err;
-    }
-  }
-
   // use the same process in both v4 and v5
   private async revertDeletedDescendants(pages, user) {
     const Page = this.crowi.model('Page');

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

@@ -0,0 +1,12 @@
+import type EventEmitter from 'events';
+
+import type { IUser } from '@growi/core';
+
+import type { ObjectIdLike } from '~/server/interfaces/mongoose-utils';
+
+export interface IPageService {
+  updateDescendantCountOfAncestors: (pageId: ObjectIdLike, inc: number, shouldIncludeTarget: boolean) => Promise<void>,
+  deleteCompletelyOperation: (pageIds: string[], pagePaths: string[]) => Promise<void>,
+  getEventEmitter: () => EventEmitter,
+  deleteMultipleCompletely: (pages: ObjectIdLike[], user: IUser | undefined) => Promise<void>,
+}

+ 20 - 0
apps/app/src/server/service/page/should-use-v4-process.ts

@@ -0,0 +1,20 @@
+import type { IPage } from '@growi/core';
+import { isTopPage } from '@growi/core/dist/utils/page-path-utils';
+import mongoose from 'mongoose';
+
+import { PageModel } from '~/server/models/page';
+import { configManager } from '~/server/service/config-manager';
+
+export const shouldUseV4Process = (page: IPage): boolean => {
+  const Page = mongoose.model<IPage, PageModel>('Page');
+
+  const isTrashPage = page.status === Page.STATUS_DELETED;
+  const isPageMigrated = page.parent != null;
+  const isV5Compatible = configManager.getConfig('crowi', 'app:isV5Compatible');
+  const isRoot = isTopPage(page.path);
+  const isPageRestricted = page.grant === Page.GRANT_RESTRICTED;
+
+  const shouldUseV4Process = !isRoot && (!isV5Compatible || !isPageMigrated || isTrashPage || isPageRestricted);
+
+  return shouldUseV4Process;
+};