Przeglądaj źródła

Merge branch 'master' into support/148793-replace-tests-with-playwright

Shun Miyazawa 1 rok temu
rodzic
commit
74f9e71744

+ 1 - 0
.devcontainer/devcontainer.json

@@ -19,6 +19,7 @@
     "eamodio.gitlens",
     "github.vscode-pull-request-github",
     "cschleiden.vscode-github-actions",
+    "cweijan.vscode-database-client2",
     "mongodb.mongodb-vscode",
     "msjsdiag.debugger-for-chrome",
     "firefox-devtools.vscode-firefox-debug",

+ 0 - 1
apps/app/src/client/components/PageControls/PageControls.tsx

@@ -1,4 +1,3 @@
-import type { MouseEventHandler } from 'react';
 import React, {
   memo, useCallback, useEffect, useMemo, useRef,
 } from 'react';

+ 2 - 4
apps/app/src/server/crowi/index.js

@@ -34,6 +34,7 @@ import PageOperationService from '../service/page-operation';
 import PassportService from '../service/passport';
 import SearchService from '../service/search';
 import { SlackIntegrationService } from '../service/slack-integration';
+import { SocketIoService } from '../service/socket-io';
 import UserGroupService from '../service/user-group';
 import { UserNotificationService } from '../service/user-notification';
 import { initializeYjsService } from '../service/yjs';
@@ -301,10 +302,7 @@ Crowi.prototype.setupS2sMessagingService = async function() {
 };
 
 Crowi.prototype.setupSocketIoService = async function() {
-  const SocketIoService = require('../service/socket-io');
-  if (this.socketIoService == null) {
-    this.socketIoService = new SocketIoService(this);
-  }
+  this.socketIoService = new SocketIoService(this);
 };
 
 Crowi.prototype.setupModels = async function() {

+ 1 - 1
apps/app/src/server/service/config-manager.ts

@@ -214,7 +214,7 @@ class ConfigManagerImpl implements ConfigManager, S2sMessageHandlable {
     }
   }
 
-  async removeConfigsInTheSameNamespace(namespace, configKeys: string[], withoutPublishingS2sMessage?) {
+  async removeConfigsInTheSameNamespace(namespace, configKeys: readonly string[], withoutPublishingS2sMessage?) {
     const queries: any[] = [];
     for (const key of configKeys) {
       queries.push({

+ 28 - 23
apps/app/src/server/service/file-uploader/aws.ts

@@ -1,3 +1,5 @@
+import type { ReadStream } from 'fs';
+
 import type { GetObjectCommandInput, HeadObjectCommandInput } from '@aws-sdk/client-s3';
 import {
   S3Client,
@@ -142,6 +144,32 @@ class AwsFileUploader extends AbstractFileUploader {
       : ResponseMode.REDIRECT;
   }
 
+  /**
+   * @inheritdoc
+   */
+  override async uploadAttachment(readStream: ReadStream, attachment: IAttachmentDocument): Promise<void> {
+    if (!this.getIsUploadable()) {
+      throw new Error('AWS is not configured.');
+    }
+
+    logger.debug(`File uploading: fileName=${attachment.fileName}`);
+
+    const s3 = S3Factory();
+
+    const filePath = getFilePathOnStorage(attachment);
+    const contentHeaders = new ContentHeaders(attachment);
+
+    await s3.send(new PutObjectCommand({
+      Bucket: getS3Bucket(),
+      Key: filePath,
+      Body: readStream,
+      ACL: getS3PutObjectCannedAcl(),
+      // put type and the file name for reference information when uploading
+      ContentType: contentHeaders.contentType?.value.toString(),
+      ContentDisposition: contentHeaders.contentDisposition?.value.toString(),
+    }));
+  }
+
   /**
    * @inheritdoc
    */
@@ -280,29 +308,6 @@ module.exports = (crowi) => {
     return s3.send(new DeleteObjectCommand(params));
   };
 
-  (lib as any).uploadAttachment = async function(fileStream, attachment) {
-    if (!lib.getIsUploadable()) {
-      throw new Error('AWS is not configured.');
-    }
-
-    logger.debug(`File uploading: fileName=${attachment.fileName}`);
-
-    const s3 = S3Factory();
-
-    const filePath = getFilePathOnStorage(attachment);
-    const contentHeaders = new ContentHeaders(attachment);
-
-    return s3.send(new PutObjectCommand({
-      Bucket: getS3Bucket(),
-      Key: filePath,
-      Body: fileStream,
-      ACL: getS3PutObjectCannedAcl(),
-      // put type and the file name for reference information when uploading
-      ContentType: contentHeaders.contentType?.value.toString(),
-      ContentDisposition: contentHeaders.contentDisposition?.value.toString(),
-    }));
-  };
-
   lib.saveFile = async function({ filePath, contentType, data }) {
     const s3 = S3Factory();
 

+ 32 - 24
apps/app/src/server/service/file-uploader/azure.ts

@@ -1,11 +1,16 @@
-import { ClientSecretCredential, TokenCredential } from '@azure/identity';
-import {
-  generateBlobSASQueryParameters,
-  BlobServiceClient,
+import type { ReadStream } from 'fs';
+
+import type { TokenCredential } from '@azure/identity';
+import { ClientSecretCredential } from '@azure/identity';
+import type {
   BlobClient,
   BlockBlobClient,
   BlobDeleteOptions,
   ContainerClient,
+} from '@azure/storage-blob';
+import {
+  generateBlobSASQueryParameters,
+  BlobServiceClient,
   ContainerSASPermissions,
   SASProtocol,
   type BlobDeleteIfExistsResponse,
@@ -94,6 +99,29 @@ class AzureFileUploader extends AbstractFileUploader {
     throw new Error('Method not implemented.');
   }
 
+  /**
+   * @inheritdoc
+   */
+  override async uploadAttachment(readStream: ReadStream, attachment: IAttachmentDocument): Promise<void> {
+    if (!this.getIsUploadable()) {
+      throw new Error('Azure is not configured.');
+    }
+
+    logger.debug(`File uploading: fileName=${attachment.fileName}`);
+    const filePath = getFilePathOnStorage(attachment);
+    const containerClient = await getContainerClient();
+    const blockBlobClient: BlockBlobClient = containerClient.getBlockBlobClient(filePath);
+    const contentHeaders = new ContentHeaders(attachment);
+
+    await blockBlobClient.uploadStream(readStream, undefined, undefined, {
+      blobHTTPHeaders: {
+        // put type and the file name for reference information when uploading
+        blobContentType: contentHeaders.contentType?.value.toString(),
+        blobContentDisposition: contentHeaders.contentDisposition?.value.toString(),
+      },
+    });
+  }
+
   /**
    * @inheritdoc
    */
@@ -218,26 +246,6 @@ module.exports = (crowi) => {
     }
   };
 
-  (lib as any).uploadAttachment = async function(readStream, attachment) {
-    if (!lib.getIsUploadable()) {
-      throw new Error('Azure is not configured.');
-    }
-
-    logger.debug(`File uploading: fileName=${attachment.fileName}`);
-    const filePath = getFilePathOnStorage(attachment);
-    const containerClient = await getContainerClient();
-    const blockBlobClient: BlockBlobClient = containerClient.getBlockBlobClient(filePath);
-    const contentHeaders = new ContentHeaders(attachment);
-
-    return blockBlobClient.uploadStream(readStream, undefined, undefined, {
-      blobHTTPHeaders: {
-        // put type and the file name for reference information when uploading
-        blobContentType: contentHeaders.contentType?.value.toString(),
-        blobContentDisposition: contentHeaders.contentDisposition?.value.toString(),
-      },
-    });
-  };
-
   lib.saveFile = async function({ filePath, contentType, data }) {
     const containerClient = await getContainerClient();
     const blockBlobClient: BlockBlobClient = containerClient.getBlockBlobClient(filePath);

+ 4 - 0
apps/app/src/server/service/file-uploader/file-uploader.ts

@@ -1,4 +1,5 @@
 import { randomUUID } from 'crypto';
+import type { ReadStream } from 'fs';
 
 import type { Response } from 'express';
 
@@ -36,6 +37,7 @@ export interface FileUploader {
   getTotalFileSize(): Promise<number>,
   doCheckLimit(uploadFileSize: number, maxFileSize: number, totalLimit: number): Promise<ICheckLimitResult>,
   determineResponseMode(): ResponseMode,
+  uploadAttachment(readStream: ReadStream, attachment: IAttachmentDocument): Promise<void>,
   respond(res: Response, attachment: IAttachmentDocument, opts?: RespondOptions): void,
   findDeliveryFile(attachment: IAttachmentDocument): Promise<NodeJS.ReadableStream>,
   generateTemporaryUrl(attachment: IAttachmentDocument, opts?: RespondOptions): Promise<TemporaryUrl>,
@@ -151,6 +153,8 @@ export abstract class AbstractFileUploader implements FileUploader {
     return ResponseMode.RELAY;
   }
 
+ abstract uploadAttachment(readStream: ReadStream, attachment: IAttachmentDocument): Promise<void>;
+
   /**
    * Respond to the HTTP request.
    */

+ 24 - 19
apps/app/src/server/service/file-uploader/gcs.ts

@@ -1,3 +1,5 @@
+import type { ReadStream } from 'fs';
+
 import { Storage } from '@google-cloud/storage';
 import urljoin from 'url-join';
 
@@ -94,6 +96,28 @@ class GcsFileUploader extends AbstractFileUploader {
       : ResponseMode.REDIRECT;
   }
 
+  /**
+   * @inheritdoc
+   */
+  override async uploadAttachment(readStream: ReadStream, attachment: IAttachmentDocument): Promise<void> {
+    if (!this.getIsUploadable()) {
+      throw new Error('GCS is not configured.');
+    }
+
+    logger.debug(`File uploading: fileName=${attachment.fileName}`);
+
+    const gcs = getGcsInstance();
+    const myBucket = gcs.bucket(getGcsBucket());
+    const filePath = getFilePathOnStorage(attachment);
+    const contentHeaders = new ContentHeaders(attachment);
+
+    await myBucket.upload(readStream.path.toString(), {
+      destination: filePath,
+      // put type and the file name for reference information when uploading
+      contentType: contentHeaders.contentType?.value.toString(),
+    });
+  }
+
   /**
    * @inheritdoc
    */
@@ -201,25 +225,6 @@ module.exports = function(crowi: Crowi) {
     });
   };
 
-  (lib as any).uploadAttachment = function(fileStream, attachment) {
-    if (!lib.getIsUploadable()) {
-      throw new Error('GCS is not configured.');
-    }
-
-    logger.debug(`File uploading: fileName=${attachment.fileName}`);
-
-    const gcs = getGcsInstance();
-    const myBucket = gcs.bucket(getGcsBucket());
-    const filePath = getFilePathOnStorage(attachment);
-    const contentHeaders = new ContentHeaders(attachment);
-
-    return myBucket.upload(fileStream.path, {
-      destination: filePath,
-      // put type and the file name for reference information when uploading
-      contentType: contentHeaders.contentType?.value.toString(),
-    });
-  };
-
   lib.saveFile = async function({ filePath, contentType, data }) {
     const gcs = getGcsInstance();
     const myBucket = gcs.bucket(getGcsBucket());

+ 30 - 24
apps/app/src/server/service/file-uploader/gridfs.ts

@@ -1,3 +1,4 @@
+import type { ReadStream } from 'fs';
 import { Readable } from 'stream';
 import util from 'util';
 
@@ -16,6 +17,17 @@ import { ContentHeaders } from './utils';
 const logger = loggerFactory('growi:service:fileUploaderGridfs');
 
 
+const COLLECTION_NAME = 'attachmentFiles';
+const CHUNK_COLLECTION_NAME = `${COLLECTION_NAME}.chunks`;
+
+// instantiate mongoose-gridfs
+const AttachmentFile = createModel({
+  modelName: COLLECTION_NAME,
+  bucketName: COLLECTION_NAME,
+  connection: mongoose.connection,
+});
+
+
 // TODO: rewrite this module to be a type-safe implementation
 class GridfsFileUploader extends AbstractFileUploader {
 
@@ -47,6 +59,24 @@ class GridfsFileUploader extends AbstractFileUploader {
     throw new Error('Method not implemented.');
   }
 
+  /**
+   * @inheritdoc
+   */
+  override async uploadAttachment(readStream: ReadStream, attachment: IAttachmentDocument): Promise<void> {
+    logger.debug(`File uploading: fileName=${attachment.fileName}`);
+
+    const contentHeaders = new ContentHeaders(attachment);
+
+    return AttachmentFile.promisifiedWrite(
+      {
+        // put type and the file name for reference information when uploading
+        filename: attachment.fileName,
+        contentType: contentHeaders.contentType?.value.toString(),
+      },
+      readStream,
+    );
+  }
+
   /**
    * @inheritdoc
    */
@@ -73,15 +103,6 @@ class GridfsFileUploader extends AbstractFileUploader {
 
 module.exports = function(crowi) {
   const lib = new GridfsFileUploader(crowi);
-  const COLLECTION_NAME = 'attachmentFiles';
-  const CHUNK_COLLECTION_NAME = `${COLLECTION_NAME}.chunks`;
-
-  // instantiate mongoose-gridfs
-  const AttachmentFile = createModel({
-    modelName: COLLECTION_NAME,
-    bucketName: COLLECTION_NAME,
-    connection: mongoose.connection,
-  });
 
   // get Collection instance of chunk
   const chunkCollection = mongoose.connection.collection(CHUNK_COLLECTION_NAME);
@@ -150,21 +171,6 @@ module.exports = function(crowi) {
     return lib.doCheckLimit(uploadFileSize, maxFileSize, totalLimit);
   };
 
-  (lib as any).uploadAttachment = async function(fileStream, attachment) {
-    logger.debug(`File uploading: fileName=${attachment.fileName}`);
-
-    const contentHeaders = new ContentHeaders(attachment);
-
-    return AttachmentFile.promisifiedWrite(
-      {
-        // put type and the file name for reference information when uploading
-        filename: attachment.fileName,
-        contentType: contentHeaders.contentType?.value.toString(),
-      },
-      fileStream,
-    );
-  };
-
   lib.saveFile = async function({ filePath, contentType, data }) {
     const readable = new Readable();
     readable.push(data);

+ 9 - 1
apps/app/src/server/service/file-uploader/local.ts

@@ -1,3 +1,4 @@
+import type { ReadStream } from 'fs';
 import { Readable } from 'stream';
 
 import type { Response } from 'express';
@@ -71,6 +72,13 @@ class LocalFileUploader extends AbstractFileUploader {
       : ResponseMode.RELAY;
   }
 
+  /**
+   * @inheritdoc
+   */
+  override async uploadAttachment(readStream: ReadStream, attachment: IAttachmentDocument): Promise<void> {
+    throw new Error('Method not implemented.');
+  }
+
   /**
    * @inheritdoc
    */
@@ -146,7 +154,7 @@ module.exports = function(crowi) {
     return fs.unlinkSync(filePath);
   };
 
-  (lib as any).uploadAttachment = async function(fileStream, attachment) {
+  lib.uploadAttachment = async function(fileStream, attachment) {
     logger.debug(`File uploading: fileName=${attachment.fileName}`);
 
     const filePath = getFilePathOnStorage(attachment);

+ 24 - 24
apps/app/src/server/service/g2g-transfer.ts

@@ -1,6 +1,7 @@
-import { createReadStream, ReadStream } from 'fs';
+import type { ReadStream } from 'fs';
+import { createReadStream } from 'fs';
 import { basename } from 'path';
-import { Readable } from 'stream';
+import type { Readable } from 'stream';
 
 // eslint-disable-next-line no-restricted-imports
 import rawAxios, { type AxiosRequestConfig } from 'axios';
@@ -201,10 +202,10 @@ interface Receiver {
   updateFileUploadConfigs(fileUploadConfigs: FileUploadConfigs): Promise<void>
   /**
    * Upload attachment file
-   * @param {Readable} content Pushed attachment data from source GROWI
+   * @param {ReadStream} content Pushed attachment data from source GROWI
    * @param {any} attachmentMap Map-ped Attachment instance
    */
-  receiveAttachment(content: Readable, attachmentMap: any): Promise<void>
+  receiveAttachment(content: ReadStream, attachmentMap: any): Promise<void>
 }
 
 /**
@@ -325,7 +326,7 @@ export class G2GTransferPusherService implements Pusher {
   public async transferAttachments(tk: TransferKey): Promise<void> {
     const BATCH_SIZE = 100;
     const { fileUploadService, socketIoService } = this.crowi;
-    const socket = socketIoService.getAdminSocket();
+    const socket = socketIoService?.getAdminSocket();
     const filesFromSrcGROWI = await this.listFilesInStorage(tk);
 
     /**
@@ -390,7 +391,7 @@ export class G2GTransferPusherService implements Pusher {
         }
         catch (err) {
           logger.warn(`Error occured when getting Attachment(ID=${attachment.id}), skipping: `, err);
-          socket.emit('admin:g2gError', {
+          socket?.emit('admin:g2gError', {
             message: `Error occured when uploading Attachment(ID=${attachment.id})`,
             key: `Error occured when uploading Attachment(ID=${attachment.id})`,
             // TODO: emit error with params
@@ -404,7 +405,7 @@ export class G2GTransferPusherService implements Pusher {
         }
         catch (err) {
           logger.error(`Error occured when uploading attachment(ID=${attachment.id})`, err);
-          socket.emit('admin:g2gError', {
+          socket?.emit('admin:g2gError', {
             message: `Error occured when uploading Attachment(ID=${attachment.id})`,
             key: `Error occured when uploading Attachment(ID=${attachment.id})`,
             // TODO: emit error with params
@@ -417,9 +418,9 @@ export class G2GTransferPusherService implements Pusher {
 
   // eslint-disable-next-line max-len
   public async startTransfer(tk: TransferKey, user: any, collections: string[], optionsMap: any, destGROWIInfo: IDataGROWIInfo): Promise<void> {
-    const socket = this.crowi.socketIoService.getAdminSocket();
+    const socket = this.crowi.socketIoService?.getAdminSocket();
 
-    socket.emit('admin:g2gProgress', {
+    socket?.emit('admin:g2gProgress', {
       mongo: G2G_PROGRESS_STATUS.IN_PROGRESS,
       attachments: G2G_PROGRESS_STATUS.PENDING,
     });
@@ -439,11 +440,11 @@ export class G2GTransferPusherService implements Pusher {
     }
     catch (err) {
       logger.error(err);
-      socket.emit('admin:g2gProgress', {
+      socket?.emit('admin:g2gProgress', {
         mongo: G2G_PROGRESS_STATUS.ERROR,
         attachments: G2G_PROGRESS_STATUS.PENDING,
       });
-      socket.emit('admin:g2gError', { message: 'Failed to generate GROWI archive file', key: 'admin:g2g:error_generate_growi_archive' });
+      socket?.emit('admin:g2gError', { message: 'Failed to generate GROWI archive file', key: 'admin:g2g:error_generate_growi_archive' });
       throw err;
     }
 
@@ -462,15 +463,15 @@ export class G2GTransferPusherService implements Pusher {
     }
     catch (err) {
       logger.error(err);
-      socket.emit('admin:g2gProgress', {
+      socket?.emit('admin:g2gProgress', {
         mongo: G2G_PROGRESS_STATUS.ERROR,
         attachments: G2G_PROGRESS_STATUS.PENDING,
       });
-      socket.emit('admin:g2gError', { message: 'Failed to send GROWI archive file to the destination GROWI', key: 'admin:g2g:error_send_growi_archive' });
+      socket?.emit('admin:g2gError', { message: 'Failed to send GROWI archive file to the destination GROWI', key: 'admin:g2g:error_send_growi_archive' });
       throw err;
     }
 
-    socket.emit('admin:g2gProgress', {
+    socket?.emit('admin:g2gProgress', {
       mongo: G2G_PROGRESS_STATUS.COMPLETED,
       attachments: G2G_PROGRESS_STATUS.IN_PROGRESS,
     });
@@ -480,15 +481,15 @@ export class G2GTransferPusherService implements Pusher {
     }
     catch (err) {
       logger.error(err);
-      socket.emit('admin:g2gProgress', {
+      socket?.emit('admin:g2gProgress', {
         mongo: G2G_PROGRESS_STATUS.COMPLETED,
         attachments: G2G_PROGRESS_STATUS.ERROR,
       });
-      socket.emit('admin:g2gError', { message: 'Failed to transfer attachments', key: 'admin:g2g:error_upload_attachment' });
+      socket?.emit('admin:g2gError', { message: 'Failed to transfer attachments', key: 'admin:g2g:error_upload_attachment' });
       throw err;
     }
 
-    socket.emit('admin:g2gProgress', {
+    socket?.emit('admin:g2gProgress', {
       mongo: G2G_PROGRESS_STATUS.COMPLETED,
       attachments: G2G_PROGRESS_STATUS.COMPLETED,
     });
@@ -516,9 +517,9 @@ export class G2GTransferPusherService implements Pusher {
  */
 export class G2GTransferReceiverService implements Receiver {
 
-  crowi: any;
+  crowi: Crowi;
 
-  constructor(crowi: any) {
+  constructor(crowi: Crowi) {
     this.crowi = crowi;
   }
 
@@ -539,7 +540,7 @@ export class G2GTransferReceiverService implements Receiver {
   }
 
   public async answerGROWIInfo(): Promise<IDataGROWIInfo> {
-    const { version, configManager, fileUploadService } = this.crowi;
+    const { version, fileUploadService } = this.crowi;
     const userUpperLimit = configManager.getConfig('crowi', 'security:userUpperLimit');
     const fileUploadDisabled = configManager.getConfig('crowi', 'app:fileUploadDisabled');
     const fileUploadTotalLimit = fileUploadService.getFileUploadTotalLimit();
@@ -636,7 +637,7 @@ export class G2GTransferReceiverService implements Receiver {
       importSettingsMap: { [key: string]: ImportSettings; },
       sourceGROWIUploadConfigs: FileUploadConfigs,
   ): Promise<void> {
-    const { configManager, importService, appService } = this.crowi;
+    const { importService, appService } = this.crowi;
     /** whether to keep current file upload configs */
     const shouldKeepUploadConfigs = configManager.getConfig('crowi', 'app:fileUploadType') !== 'none';
 
@@ -664,7 +665,6 @@ export class G2GTransferReceiverService implements Receiver {
   }
 
   public async getFileUploadConfigs(): Promise<FileUploadConfigs> {
-    const { configManager } = this.crowi;
     const fileUploadConfigs = Object.fromEntries(UPLOAD_CONFIG_KEYS.map((key) => {
       return [key, configManager.getConfigFromDB('crowi', key)];
     })) as FileUploadConfigs;
@@ -673,7 +673,7 @@ export class G2GTransferReceiverService implements Receiver {
   }
 
   public async updateFileUploadConfigs(fileUploadConfigs: FileUploadConfigs): Promise<void> {
-    const { appService, configManager } = this.crowi;
+    const { appService } = this.crowi;
 
     await configManager.removeConfigsInTheSameNamespace('crowi', Object.keys(fileUploadConfigs));
     await configManager.updateConfigsInTheSameNamespace('crowi', fileUploadConfigs);
@@ -681,7 +681,7 @@ export class G2GTransferReceiverService implements Receiver {
     await appService.setupAfterInstall();
   }
 
-  public async receiveAttachment(content: Readable, attachmentMap): Promise<void> {
+  public async receiveAttachment(content: ReadStream, attachmentMap): Promise<void> {
     const { fileUploadService } = this.crowi;
     return fileUploadService.uploadAttachment(content, attachmentMap);
   }

+ 2 - 1
apps/app/src/server/service/in-app-notification.ts

@@ -18,10 +18,11 @@ import Subscription from '~/server/models/subscription';
 import loggerFactory from '~/utils/logger';
 
 import type Crowi from '../crowi';
-import { RoomPrefix, getRoomNameWithId } from '../util/socket-io-helpers';
+
 
 import { generateSnapshot } from './in-app-notification/in-app-notification-utils';
 import { preNotifyService, type PreNotify } from './pre-notify';
+import { RoomPrefix, getRoomNameWithId } from './socket-io/helper';
 
 
 const { STATUS_UNREAD, STATUS_UNOPENED, STATUS_OPENED } = InAppNotificationStatuses;

+ 23 - 2
apps/app/src/server/service/search-delegator/aggregate-to-index.ts

@@ -51,11 +51,32 @@ export const aggregatePipelineToIndex = (maxBodyLengthToIndex: number, query?: Q
 
     // join Comment
     {
+      // MongoDB 5.0 or later can use concise syntax
+      // https://www.mongodb.com/docs/v6.0/reference/operator/aggregation/lookup/#correlated-subqueries-using-concise-syntax
+      // $lookup: {
+      //   from: 'comments',
+      //   localField: '_id',
+      //   foreignField: 'page',
+      //   pipeline: [
+      //     {
+      //       $addFields: {
+      //         commentLength: { $strLenCP: '$comment' },
+      //       },
+      //     },
+      //   ],
+      //   as: 'comments',
+      // },
       $lookup: {
         from: 'comments',
-        localField: '_id',
-        foreignField: 'page',
+        let: { pageId: '$_id' },
         pipeline: [
+          {
+            $match: {
+              $expr: {
+                $eq: ['$page', '$$pageId'],
+              },
+            },
+          },
           {
             $addFields: {
               commentLength: { $strLenCP: '$comment' },

+ 0 - 0
apps/app/src/server/util/socket-io-helpers.ts → apps/app/src/server/service/socket-io/helper.ts


+ 1 - 0
apps/app/src/server/service/socket-io/index.ts

@@ -0,0 +1 @@
+export * from './socket-io';

+ 13 - 23
apps/app/src/server/service/socket-io.ts → apps/app/src/server/service/socket-io/socket-io.ts

@@ -9,10 +9,10 @@ import { Server } from 'socket.io';
 import { SocketEventName } from '~/interfaces/websocket';
 import loggerFactory from '~/utils/logger';
 
-import type Crowi from '../crowi';
-import { RoomPrefix, getRoomNameWithId } from '../util/socket-io-helpers';
+import type Crowi from '../../crowi';
+import { configManager } from '../config-manager';
 
-import { configManager } from './config-manager';
+import { RoomPrefix, getRoomNameWithId } from './helper';
 
 
 const logger = loggerFactory('growi:service:socket-io');
@@ -23,7 +23,7 @@ type RequestWithUser = IncomingMessage & { user: IUserHasId };
 /**
  * Serve socket.io for server-to-client messaging
  */
-class SocketIoService {
+export class SocketIoService {
 
   crowi: Crowi;
 
@@ -34,12 +34,12 @@ class SocketIoService {
   adminNamespace: Namespace;
 
 
-  constructor(crowi) {
+  constructor(crowi: Crowi) {
     this.crowi = crowi;
     this.guestClients = new Set();
   }
 
-  get isInitialized() {
+  get isInitialized(): boolean {
     return (this.io != null);
   }
 
@@ -83,27 +83,19 @@ class SocketIoService {
 
   /**
    * use passport session
-   * @see https://socket.io/docs/v4/middlewares/#Compatibility-with-Express-middleware
+   * @see https://socket.io/docs/v4/middlewares/#compatibility-with-express-middleware
    */
-  setupSessionMiddleware() {
-    const wrap = middleware => (socket, next) => middleware(socket.request, {}, next);
-
-    this.io.use(wrap(expressSession(this.crowi.sessionConfig)));
-    this.io.use(wrap(passport.initialize()));
-    this.io.use(wrap(passport.session()));
-
-    // express and passport session on main socket doesn't shared to child namespace socket
-    // need to define the session for specific namespace
-    this.getAdminSocket().use(wrap(expressSession(this.crowi.sessionConfig)));
-    this.getAdminSocket().use(wrap(passport.initialize()));
-    this.getAdminSocket().use(wrap(passport.session()));
+  setupSessionMiddleware(): void {
+    this.io.engine.use(expressSession(this.crowi.sessionConfig));
+    this.io.engine.use(passport.initialize());
+    this.io.engine.use(passport.session());
   }
 
   /**
    * use loginRequired middleware
    */
   setupLoginRequiredMiddleware() {
-    const loginRequired = require('../middlewares/login-required')(this.crowi, true, (req, res, next) => {
+    const loginRequired = require('../../middlewares/login-required')(this.crowi, true, (req, res, next) => {
       next(new Error('Login is required to connect.'));
     });
 
@@ -117,7 +109,7 @@ class SocketIoService {
    * use adminRequired middleware
    */
   setupAdminRequiredMiddleware() {
-    const adminRequired = require('../middlewares/admin-required')(this.crowi, (req, res, next) => {
+    const adminRequired = require('../../middlewares/admin-required')(this.crowi, (req, res, next) => {
       next(new Error('Admin priviledge is required to connect.'));
     });
 
@@ -243,5 +235,3 @@ class SocketIoService {
   }
 
 }
-
-module.exports = SocketIoService;

+ 3 - 3
apps/app/src/server/service/system-events/sync-page-status.ts

@@ -2,9 +2,9 @@ import loggerFactory from '~/utils/logger';
 
 import { S2cMessagePageUpdated } from '../../models/vo/s2c-message';
 import S2sMessage from '../../models/vo/s2s-message';
-import { RoomPrefix, getRoomNameWithId } from '../../util/socket-io-helpers';
-import { S2sMessagingService } from '../s2s-messaging/base';
-import { S2sMessageHandlable } from '../s2s-messaging/handlable';
+import type { S2sMessagingService } from '../s2s-messaging/base';
+import type { S2sMessageHandlable } from '../s2s-messaging/handlable';
+import { RoomPrefix, getRoomNameWithId } from '../socket-io/helper';
 
 const logger = loggerFactory('growi:service:system-events:SyncPageStatusService');
 

+ 3 - 3
apps/app/src/server/service/yjs/yjs.ts

@@ -1,5 +1,6 @@
 import type { IncomingMessage } from 'http';
 
+
 import type { IPage, IUserHasId } from '@growi/core';
 import { YDocStatus } from '@growi/core/dist/consts';
 import mongoose from 'mongoose';
@@ -9,7 +10,7 @@ import { YSocketIO, type Document as Ydoc } from 'y-socket.io/dist/server';
 
 import { SocketEventName } from '~/interfaces/websocket';
 import type { SyncLatestRevisionBody } from '~/interfaces/yjs';
-import { RoomPrefix, getRoomNameWithId } from '~/server/util/socket-io-helpers';
+import { RoomPrefix, getRoomNameWithId } from '~/server/service/socket-io/helper';
 import loggerFactory from '~/utils/logger';
 
 import type { PageModel } from '../../models/page';
@@ -73,9 +74,8 @@ class YjsService implements IYjsService {
     // create indexes
     createIndexes(MONGODB_PERSISTENCE_COLLECTION_NAME);
 
-    // TODO: https://redmine.weseek.co.jp/issues/150529
     // register middlewares
-    // this.registerAccessiblePageChecker(ysocketio);
+    this.registerAccessiblePageChecker(ysocketio);
 
     ysocketio.on('document-loaded', async(doc: Document) => {
       const pageId = doc.name;

+ 4 - 1
packages/core/src/interfaces/page.ts

@@ -102,7 +102,10 @@ export type IPageInfoAll = IPageInfo | IPageInfoForEntity | IPageInfoForOperatio
 
 // eslint-disable-next-line @typescript-eslint/no-explicit-any
 export const isIPageInfoForEntity = (pageInfo: any | undefined): pageInfo is IPageInfoForEntity => {
-  return pageInfo != null;
+  return pageInfo != null && pageInfo instanceof Object
+    && ('commentCount' in pageInfo)
+    && ('bookmarkCount' in pageInfo)
+    && ('descendantCount' in pageInfo);
 };
 
 // eslint-disable-next-line @typescript-eslint/no-explicit-any