Yuki Takei 1 год назад
Родитель
Сommit
34b6cede5a

+ 1 - 1
apps/app/src/features/external-user-group/server/routes/apiv3/external-user-group.ts

@@ -30,7 +30,7 @@ interface AuthorizedRequest extends Request {
 module.exports = (crowi: Crowi): Router => {
   const loginRequiredStrictly = require('~/server/middlewares/login-required')(crowi);
   const adminRequired = require('~/server/middlewares/admin-required')(crowi);
-  const addActivity = generateAddActivityMiddleware(crowi);
+  const addActivity = generateAddActivityMiddleware();
 
   const activityEvent = crowi.event('activity');
 

+ 3 - 0
apps/app/src/server/crowi/index.js

@@ -78,6 +78,9 @@ class Crowi {
   /** @type {import('../service/page').IPageService} */
   pageService;
 
+  /** @type {import('../service/page-grant').default} */
+  pageGrantService;
+
   /** @type {import('../service/page-operation').default} */
   pageOperationService;
 

+ 2 - 2
apps/app/src/server/models/page.ts

@@ -272,7 +272,7 @@ export class PageQueryBuilder {
   /**
    * generate the query to find the pages '{path}/*' (exclude '{path}' self).
    */
-  addConditionToListOnlyDescendants(path: string, option): PageQueryBuilder {
+  addConditionToListOnlyDescendants(path: string): PageQueryBuilder {
     // exclude the target page
     this.query = this.query.and({ path: { $ne: path } });
 
@@ -1156,7 +1156,7 @@ schema.methods.makeWip = function(disableTtl: boolean) {
  * Merge obsolete page model methods and define new methods which depend on crowi instance
  */
 
-export default function PageModel(crowi: Crowi): any {
+export default function PageModel(crowi: Crowi | null): any {
   // add old page schema methods
   const pageSchema = getPageSchema(crowi);
   schema.methods = { ...pageSchema.methods, ...schema.methods };

+ 1 - 1
apps/app/src/server/models/user.js

@@ -22,7 +22,7 @@ const uniqueValidator = require('mongoose-unique-validator');
 
 const logger = loggerFactory('growi:models:user');
 
-/** @param {import('~/server/crowi').default} crowi Crowi instance */
+/** @param {import('~/server/crowi').default | null} crowi Crowi instance */
 const factory = (crowi) => {
 
   const userModelExists = getModelSafely('User');

+ 1 - 1
apps/app/src/server/routes/apiv3/in-app-notification.ts

@@ -16,7 +16,7 @@ const router = express.Router();
 /** @param {import('~/server/crowi').default} crowi Crowi instance */
 module.exports = (crowi) => {
   const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
-  const addActivity = generateAddActivityMiddleware(crowi);
+  const addActivity = generateAddActivityMiddleware();
 
   const inAppNotificationService = crowi.inAppNotificationService;
 

+ 1 - 1
apps/app/src/server/routes/apiv3/installer.ts

@@ -21,7 +21,7 @@ const logger = loggerFactory('growi:routes:apiv3:installer');
 type FormRequest = Request & { form: any, logIn: any };
 
 module.exports = (crowi: Crowi): Router => {
-  const addActivity = generateAddActivityMiddleware(crowi);
+  const addActivity = generateAddActivityMiddleware();
 
   const activityEvent = crowi.event('activity');
 

+ 1 - 1
apps/app/src/server/routes/apiv3/page/create-page.ts

@@ -214,7 +214,7 @@ export const createPageHandlersFactory: CreatePageHandlersFactory = (crowi) => {
     }
   }
 
-  const addActivity = generateAddActivityMiddleware(crowi);
+  const addActivity = generateAddActivityMiddleware();
 
   return [
     accessTokenParser, loginRequiredStrictly, excludeReadOnlyUser, addActivity,

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

@@ -114,7 +114,7 @@ module.exports = (crowi) => {
   const loginRequired = require('../../../middlewares/login-required')(crowi, true);
   const loginRequiredStrictly = require('../../../middlewares/login-required')(crowi);
   const certifySharedPage = require('../../../middlewares/certify-shared-page')(crowi);
-  const addActivity = generateAddActivityMiddleware(crowi);
+  const addActivity = generateAddActivityMiddleware();
 
   const globalNotificationService = crowi.getGlobalNotificationService();
   const Page = mongoose.model<IPage, PageModel>('Page');

+ 1 - 1
apps/app/src/server/routes/apiv3/page/update-page.ts

@@ -130,7 +130,7 @@ export const updatePageHandlersFactory: UpdatePageHandlersFactory = (crowi) => {
     }
   }
 
-  const addActivity = generateAddActivityMiddleware(crowi);
+  const addActivity = generateAddActivityMiddleware();
 
   return [
     accessTokenParser, loginRequiredStrictly, excludeReadOnlyUser, addActivity,

+ 4 - 2
apps/app/src/server/routes/apiv3/user-activation.ts

@@ -1,8 +1,10 @@
 import path from 'path';
 
+import type { IUser } from '@growi/core';
 import { ErrorV3 } from '@growi/core/dist/models';
 import { format, subSeconds } from 'date-fns';
 import { body, validationResult } from 'express-validator';
+import mongoose from 'mongoose';
 
 import { SupportedAction } from '~/interfaces/activity';
 import { RegistrationMode } from '~/interfaces/registration-mode';
@@ -69,7 +71,7 @@ async function sendEmailToAllAdmins(userData, admins, appTitle, mailService, tem
 }
 
 export const completeRegistrationAction = (crowi: Crowi) => {
-  const User = crowi.model('User');
+  const User = mongoose.model<IUser, { isEmailValid, isRegisterable, createUserByEmailAndPassword, findAdmins }>('User');
   const activityEvent = crowi.event('activity');
   const {
     aclService,
@@ -244,7 +246,7 @@ async function makeRegistrationEmailToken(email, crowi: Crowi) {
 }
 
 export const registerAction = (crowi: Crowi) => {
-  const User = crowi.model('User');
+  const User = mongoose.model<IUser, { isRegisterableEmail, isEmailValid }>('User');
 
   return async function(req, res) {
     const registerForm = req.body.registerForm || {};

+ 7 - 2
apps/app/src/server/routes/ogp.ts

@@ -40,13 +40,18 @@ module.exports = function(crowi: Crowi) {
     return /^\/attachment\/.+/.test(userImageUrlCached);
   };
 
-  const getBufferedUserImage = async(userImageUrlCached: string): Promise<Buffer> => {
+  const getBufferedUserImage = async(userImageUrlCached: string): Promise<Buffer | null> => {
 
     let bufferedUserImage: Buffer;
 
     if (isUserImageAttachment(userImageUrlCached)) {
       const { fileUploadService } = crowi;
       const attachment = await Attachment.findById(userImageUrlCached);
+
+      if (attachment == null) {
+        return null;
+      }
+
       const fileStream = await fileUploadService.findDeliveryFile(attachment);
       bufferedUserImage = await convertStreamToBuffer(fileStream);
       return bufferedUserImage;
@@ -85,7 +90,7 @@ module.exports = function(crowi: Crowi) {
           userName = user.username;
           userImage = user.imageUrlCached !== DEFAULT_USER_IMAGE_URL
             ? bufferedDefaultUserImageCache
-            : await getBufferedUserImage(user.imageUrlCached);
+            : await getBufferedUserImage(user.imageUrlCached) ?? bufferedDefaultUserImageCache;
         }
       }
     }

+ 4 - 1
apps/app/src/server/service/page-operation.ts

@@ -1,4 +1,6 @@
+import type { IPage } from '@growi/core';
 import { pagePathUtils } from '@growi/core/dist/utils';
+import mongoose from 'mongoose';
 
 import type { IPageOperationProcessInfo, IPageOperationProcessData } from '~/interfaces/page-operation';
 import { PageActionType, PageActionStage } from '~/interfaces/page-operation';
@@ -8,6 +10,7 @@ import loggerFactory from '~/utils/logger';
 
 import type Crowi from '../crowi';
 import type { ObjectIdLike } from '../interfaces/mongoose-utils';
+import type { PageModel } from '../models/page';
 import { collectAncestorPaths } from '../util/collect-ancestor-paths';
 
 const logger = loggerFactory('growi:services:page-operation');
@@ -58,7 +61,7 @@ class PageOperationService {
   private async executeAllRenameOperationBySystem(pageOps: PageOperationDocument[]): Promise<void> {
     if (pageOps.length === 0) return;
 
-    const Page = this.crowi.model('Page');
+    const Page = mongoose.model<IPage, PageModel>('Page');
 
     for await (const pageOp of pageOps) {
 

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

@@ -78,7 +78,7 @@ export const deleteCompletelyUserHomeBySystem = async(userHomepagePath: string,
     // Find descendant pages with system deletion condition
     const builder = new PageQueryBuilder(Page.find(), true)
       .addConditionForSystemDeletion()
-      .addConditionToListOnlyDescendants(userHomepage.path, {});
+      .addConditionToListOnlyDescendants(userHomepage.path);
 
     // Stream processing to delete descendant pages
     // ────────┤ start │─────────

+ 15 - 11
apps/app/src/server/service/page/index.ts

@@ -25,6 +25,7 @@ import type { ExternalUserGroupDocument } from '~/features/external-user-group/s
 import ExternalUserGroupRelation from '~/features/external-user-group/server/models/external-user-group-relation';
 import { isAiEnabled } from '~/features/openai/server/services';
 import { SupportedAction } from '~/interfaces/activity';
+import type { BookmarkedPage } from '~/interfaces/bookmark-info';
 import { V5ConversionErrCode } from '~/interfaces/errors/v5-conversion-error';
 import type { IOptionsForCreate, IOptionsForUpdate } from '~/interfaces/page';
 import type { IPageDeleteConfigValueToProcessValidation } from '~/interfaces/page-delete-config';
@@ -437,7 +438,7 @@ class PageService implements IPageService {
     const isGuestUser = user == null;
     const pageInfo = this.constructBasicPageInfo(page, isGuestUser);
 
-    const Bookmark = this.crowi.model('Bookmark');
+    const Bookmark = mongoose.model<BookmarkedPage, { countByPageId, findByPageIdAndUserId }>('Bookmark');
     const bookmarkCount = await Bookmark.countByPageId(pageId);
 
     const metadataForGuest = {
@@ -501,7 +502,7 @@ class PageService implements IPageService {
    */
   private async generateReadStreamToOperateOnlyDescendants(targetPagePath, userToOperate) {
 
-    const Page = this.crowi.model('Page');
+    const Page = mongoose.model<IPage, PageModel>('Page');
     const { PageQueryBuilder } = Page;
 
     const builder = new PageQueryBuilder(Page.find(), true)
@@ -2165,7 +2166,7 @@ class PageService implements IPageService {
 
   // use the same process in both v4 and v5
   private async revertDeletedDescendants(pages, user) {
-    const Page = this.crowi.model('Page');
+    const Page = mongoose.model<IPage, PageModel>('Page');
 
     const revertPageOperations: any[] = [];
     const fromPathsToDelete: string[] = [];
@@ -2202,7 +2203,7 @@ class PageService implements IPageService {
     /*
      * Common Operation
      */
-    const Page = this.crowi.model('Page');
+    const Page = mongoose.model<IPage, PageModel>('Page');
 
     const parameters = {
       ip: activityParameters.ip,
@@ -2361,7 +2362,7 @@ class PageService implements IPageService {
   }
 
   private async revertDeletedPageV4(page, user, options = {}, isRecursively = false) {
-    const Page = this.crowi.model('Page');
+    const Page = mongoose.model<IPage, PageModel>('Page');
 
     const newPath = Page.getRevertDeletedPageName(page.path);
     const originPage = await Page.findByPath(newPath);
@@ -2389,7 +2390,7 @@ class PageService implements IPageService {
   }
 
   private async applyScopesToDescendantsWithStream(parentPage, user, isV4 = false) {
-    const Page = this.crowi.model('Page');
+    const Page = mongoose.model<IPage, PageModel>('Page');
     const builder = new Page.PageQueryBuilder(Page.find());
     builder.addConditionToListOnlyDescendants(parentPage.path);
 
@@ -2526,7 +2527,10 @@ class PageService implements IPageService {
 
 
   async handlePrivatePagesForGroupsToDelete(
-      groupsToDelete: UserGroupDocument[] | ExternalUserGroupDocument[], action: PageActionOnGroupDelete, transferToUserGroup: IGrantedGroup, user,
+      groupsToDelete: UserGroupDocument[] | ExternalUserGroupDocument[],
+      action: PageActionOnGroupDelete,
+      transferToUserGroup: IGrantedGroup | undefined,
+      user: IUser,
   ): Promise<void> {
     const Page = mongoose.model<IPage, PageModel>('Page');
     const pages = await Page.find({ grantedGroups: { $elemMatch: { item: { $in: groupsToDelete } } } });
@@ -3431,7 +3435,7 @@ class PageService implements IPageService {
    */
   async updateDescendantCountOfSelfAndDescendants(path: string): Promise<void> {
     const BATCH_SIZE = 200;
-    const Page = this.crowi.model('Page');
+    const Page = mongoose.model<IPage, PageModel>('Page');
     const { PageQueryBuilder } = Page;
 
     const builder = new PageQueryBuilder(Page.find(), true);
@@ -3448,7 +3452,7 @@ class PageService implements IPageService {
    */
   async updateDescendantCountOfPagesWithPaths(paths: string[]): Promise<void> {
     const BATCH_SIZE = 200;
-    const Page = this.crowi.model('Page');
+    const Page = mongoose.model<IPage, PageModel>('Page');
     const { PageQueryBuilder } = Page;
 
     const builder = new PageQueryBuilder(Page.find(), true);
@@ -3463,7 +3467,7 @@ class PageService implements IPageService {
    * Recount descendantCount of pages one by one
    */
   async recountAndUpdateDescendantCountOfPages(pageCursor: Cursor<any>, batchSize:number): Promise<void> {
-    const Page = this.crowi.model('Page');
+    const Page = mongoose.model<IPage, PageModel>('Page');
     const batchStream = createBatchStream(batchSize);
     const recountWriteStream = new Writable({
       objectMode: true,
@@ -3484,7 +3488,7 @@ class PageService implements IPageService {
 
   // update descendantCount of all pages that are ancestors of a provided pageId by count
   async updateDescendantCountOfAncestors(pageId: ObjectIdLike, inc: number, shouldIncludeTarget: boolean): Promise<void> {
-    const Page = this.crowi.model('Page');
+    const Page = mongoose.model<IPage, PageModel>('Page');
     const ancestors = await Page.findAncestorsUsingParentRecursively(pageId, shouldIncludeTarget);
     const ancestorPageIds = ancestors.map(p => p._id);
 

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

@@ -3,15 +3,20 @@ import type EventEmitter from 'events';
 import type {
   HasObjectId,
   IDataWithMeta,
+  IGrantedGroup,
   IPageInfo, IPageInfoAll, IPageInfoForEntity, IUser,
 } from '@growi/core';
 import type { HydratedDocument, Types } from 'mongoose';
 
+import type { ExternalUserGroupDocument } from '~/features/external-user-group/server/models/external-user-group';
 import type { IOptionsForCreate, IOptionsForUpdate } from '~/interfaces/page';
 import type { PopulatedGrantedGroup } from '~/interfaces/page-grant';
+import type { PageActionOnGroupDelete } from '~/interfaces/user-group';
 import type { CurrentPageYjsData } from '~/interfaces/yjs';
 import type { ObjectIdLike } from '~/server/interfaces/mongoose-utils';
 import type { PageDocument } from '~/server/models/page';
+import type { PageOperationDocument } from '~/server/models/page-operation';
+import type { UserGroupDocument } from '~/server/models/user-group';
 
 export interface IPageService {
   create(path: string, body: string, user: HasObjectId, options: IOptionsForCreate): Promise<HydratedDocument<PageDocument>>,
@@ -30,6 +35,13 @@ export interface IPageService {
   findChildrenByParentPathOrIdAndViewer(
     parentPathOrId: string, user, userGroups?, showPagesRestrictedByOwner?: boolean, showPagesRestrictedByGroup?: boolean,
   ): Promise<PageDocument[]>,
+  resumeRenameSubOperation(renamedPage: PageDocument, pageOp: PageOperationDocument, activity?): Promise<void>
+  handlePrivatePagesForGroupsToDelete(
+    groupsToDelete: UserGroupDocument[] | ExternalUserGroupDocument[],
+    action: PageActionOnGroupDelete,
+    transferToUserGroup: IGrantedGroup | undefined,
+    user: IUser,
+): Promise<void>
   shortBodiesMapByPageIds(pageIds?: Types.ObjectId[], user?): Promise<Record<string, string | null>>,
   constructBasicPageInfo(page: PageDocument, isGuestUser?: boolean): IPageInfo | Omit<IPageInfoForEntity, 'bookmarkCount'>,
   normalizeAllPublicPages(): Promise<void>,

+ 2 - 2
apps/app/src/server/service/pre-notify.ts

@@ -1,10 +1,10 @@
 import type {
   IPage, IUser, Ref,
 } from '@growi/core';
+import mongoose from 'mongoose';
 
 import type { ActivityDocument } from '../models/activity';
 import Subscription from '../models/subscription';
-import userModelFactory from '../models/user';
 
 export type PreNotifyProps = {
   notificationTargetUsers?: Ref<IUser>[],
@@ -33,7 +33,7 @@ class PreNotifyService implements IPreNotifyService {
     const preNotify = async(props: PreNotifyProps) => {
       const { notificationTargetUsers } = props;
 
-      const User = userModelFactory();
+      const User = mongoose.model<IUser, { find, STATUS_ACTIVE }>('User');
       const actionUser = activity.user;
       const target = activity.target;
       const subscribedUsers = await Subscription.getSubscription(target as unknown as Ref<IPage>);

+ 5 - 2
apps/app/src/server/service/slack-event-handler/link-shared.ts

@@ -1,12 +1,15 @@
+import { PageGrant, type IPage } from '@growi/core';
 import type { GrowiBotEvent } from '@growi/slack';
 import { generateLastUpdateMrkdwn } from '@growi/slack/dist/utils/generate-last-update-markdown';
 import type {
   MessageAttachment, LinkUnfurls, WebClient,
 } from '@slack/web-api';
+import mongoose from 'mongoose';
 import urljoin from 'url-join';
 
 import type Crowi from '~/server/crowi';
 import type { EventActionsPermission } from '~/server/interfaces/slack-integration/events';
+import type { PageModel } from '~/server/models/page';
 import loggerFactory from '~/utils/logger';
 
 import type {
@@ -122,7 +125,7 @@ export class LinkSharedEventHandler implements SlackEventHandler<UnfurlRequestEv
     const ids = pathOrIds.filter(pathOrId => idRegExp.test(pathOrId)).map(id => id.replace('/', '')); // remove a slash
 
     // get pages with revision
-    const Page = this.crowi.model('Page');
+    const Page = mongoose.model<IPage, PageModel>('Page');
     const { PageQueryBuilder } = Page;
 
     const pageQueryBuilderByPaths = new PageQueryBuilder(Page.find());
@@ -154,7 +157,7 @@ export class LinkSharedEventHandler implements SlackEventHandler<UnfurlRequestEv
 
     pages.forEach((page) => {
       // not send non-public page
-      if (page.grant !== Page.GRANT_PUBLIC) {
+      if (page.grant !== PageGrant.GRANT_PUBLIC) {
         return unfurlData.push({
           isPublic: false, isPermalink, id: page._id.toString(), path: page.path,
         });

+ 2 - 2
apps/app/src/server/service/user-group.ts

@@ -1,6 +1,6 @@
 import type { IUser, IGrantedGroup } from '@growi/core';
 import type { DeleteResult } from 'mongodb';
-import type { Model } from 'mongoose';
+import mongoose, { type Model } from 'mongoose';
 
 import type { PageActionOnGroupDelete } from '~/interfaces/user-group';
 import type { ObjectIdLike } from '~/server/interfaces/mongoose-utils';
@@ -148,7 +148,7 @@ class UserGroupService implements IUserGroupService {
   }
 
   async removeUserByUsername(userGroupId: ObjectIdLike, username: string): Promise<{user: IUser, deletedGroupsCount: number}> {
-    const User = this.crowi.model('User');
+    const User = mongoose.model<IUser, { findUserByUsername }>('User');
 
     const [userGroup, user] = await Promise.all([
       UserGroup.findById(userGroupId),