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

refactor GridFS file uploader to implement model caching and improve resource management

Yuki Takei 6 месяцев назад
Родитель
Сommit
db1722079e

+ 0 - 427
.serena/memories/file-uploader-memory-leak-analysis-report.md

@@ -1,427 +0,0 @@
-# ファイルアップローダー メモリリーク分析レポート
-
-## 概要
-AWS S3とGridFSファイルアップローダーにおけるメモリリークの可能性を詳細分析した結果です。
-
----
-
-## 🔍 AWS S3ファイルアップローダー (`/workspace/growi/apps/app/src/server/service/file-uploader/aws/index.ts`)
-
-### 🔴 高リスク:メモリリークの可能性が高い箇所
-
-#### 1. S3Client インスタンスの重複作成
-**場所**: 行 82-92, 複数箇所で呼ばれている  
-**問題コード**:
-```typescript
-const S3Factory = (): S3Client => {
-  return new S3Client({
-    credentials: accessKeyId != null && secretAccessKey != null
-      ? { accessKeyId, secretAccessKey }
-      : undefined,
-    // ...
-  });
-};
-```
-
-**問題点**:
-- 各メソッド呼び出しで新しい`S3Client`インスタンスを作成
-- 内部的なHTTP接続プールが適切に共有されない
-- 複数のクライアントが同時に存在し、リソースが重複
-- AWS SDK内部のコネクションプールが累積
-
-**影響度**: 高 - 頻繁なAPI呼び出し時にコネクション数増大
-
-#### 2. ページネーション処理での配列蓄積
-**場所**: 行 336-356  
-**問題コード**:
-```typescript
-(lib as any).listFiles = async function() {
-  const files: FileMeta[] = [];
-  // ...
-  while (shouldContinue) {
-    const { Contents = [], IsTruncated, NextMarker } = await s3.send(/*...*/);
-    files.push(...Contents.map(({ Key, Size }) => ({
-      name: Key as string,
-      size: Size as number,
-    })));
-  }
-};
-```
-
-**問題点**:
-- 大量のファイルが存在する場合、`files`配列が巨大になる
-- `Contents.map()`で一時的なオブジェクトを大量作成
-- メモリ制限なしの無制限蓄積
-- S3バケット内のファイル数に比例してメモリ消費
-
-**影響度**: 高 - 大量ファイル環境で致命的
-
-### 🟡 中リスク:条件によってメモリリークが発生する可能性
-
-#### 3. マルチパートアップロード処理
-**場所**: 行 248-260  
-**問題コード**:
-```typescript
-override async abortPreviousMultipartUpload(uploadKey: string, uploadId: string) {
-  try {
-    await S3Factory().send(new AbortMultipartUploadCommand({/*...*/}));
-  }
-  catch (e) {
-    if (e.response?.status !== 404) {
-      throw e;
-    }
-  }
-}
-```
-
-**問題点**:
-- 新しいS3Clientインスタンスを作成(重複作成問題)
-- アボート失敗時のリソース残存の可能性
-
-**影響度**: 中 - 大ファイルアップロード時のみ
-
----
-
-## 🔍 GridFS ファイルアップローダー (`/workspace/growi/apps/app/src/server/service/file-uploader/gridfs.ts`)
-
-### 🔴 高リスク:メモリリークの可能性が高い箇所
-
-#### 1. Global Mongoose Connection への依存
-**場所**: 行 19-23  
-**問題コード**:
-```typescript
-const AttachmentFile = createModel({
-  modelName: COLLECTION_NAME,
-  bucketName: COLLECTION_NAME,
-  connection: mongoose.connection, // グローバル接続への依存
-});
-```
-
-**問題点**:
-- グローバルMongoose接続への強い依存
-- 接続ライフサイクルの制御が困難
-- アプリケーション終了時の適切なクリーンアップが保証されない
-- 接続状態の変化に対する適応性不足
-
-**影響度**: 高 - アプリケーションライフサイクル全体に影響
-
-#### 2. Collection 参照の直接取得
-**場所**: 行 78-79  
-**問題コード**:
-```typescript
-const chunkCollection = mongoose.connection.collection(CHUNK_COLLECTION_NAME);
-```
-
-**問題点**:
-- Mongoose接続から直接コレクション参照を取得
-- 参照のライフサイクル管理が不明確
-- 接続が閉じられても参照が残る可能性
-- MongoDB接続プールとの非同期性
-
-**影響度**: 高 - データベース接続リソースリーク
-
-#### 3. Promisified メソッドのバインド
-**場所**: 行 81-82  
-**問題コード**:
-```typescript
-AttachmentFile.promisifiedWrite = util.promisify(AttachmentFile.write).bind(AttachmentFile);
-AttachmentFile.promisifiedUnlink = util.promisify(AttachmentFile.unlink).bind(AttachmentFile);
-```
-
-**問題点**:
-- `bind()`によるクロージャ作成
-- `AttachmentFile`への循環参照の可能性
-- プロミス化されたメソッドがオリジナルコンテキストを保持
-- グローバルオブジェクトの動的変更
-
-**影響度**: 高 - アプリケーション全体に影響
-
-### 🟡 中リスク:条件によってメモリリークが発生する可能性
-
-#### 4. ストリーム作成での適切でない処理
-**場所**: 行 128-132  
-**問題コード**:
-```typescript
-lib.saveFile = async function({ filePath, contentType, data }) {
-  const readable = new Readable();
-  readable.push(data);
-  readable.push(null); // EOF
-  return AttachmentFile.promisifiedWrite({/*...*/}, readable);
-};
-```
-
-**問題点**:
-- 一時的なReadableストリームの作成
-- 大きなデータに対してメモリ上にバッファリング
-- ストリームのエラーハンドリングが不十分
-- データサイズによる急激なメモリ消費
-
-**影響度**: 中 - 大ファイル処理時に顕著
-
-#### 5. ファイル検索での例外処理
-**場所**: 行 142-150  
-**問題コード**:
-```typescript
-lib.findDeliveryFile = async function(attachment) {
-  const attachmentFile = await AttachmentFile.findOne({ filename: filenameValue });
-  if (attachmentFile == null) {
-    throw new Error(/*...*/);
-  }
-  return AttachmentFile.read({ _id: attachmentFile._id });
-};
-```
-
-**問題点**:
-- 返されたストリームの適切でない管理
-- エラー時のリソースクリーンアップ不足
-
-**影響度**: 中 - ファイル読み込み頻度に依存
-
----
-
-## 📋 推奨される修正案
-
-### AWS S3 ファイルアップローダー 修正案
-
-#### 1. S3Client のシングルトン化(最優先)
-```typescript
-class AwsFileUploader extends AbstractFileUploader {
-  private static s3Client: S3Client | null = null;
-  
-  private getS3Client(): S3Client {
-    if (AwsFileUploader.s3Client == null) {
-      AwsFileUploader.s3Client = new S3Client({
-        credentials: accessKeyId != null && secretAccessKey != null
-          ? { accessKeyId, secretAccessKey }
-          : undefined,
-        region: s3Region,
-        endpoint: s3CustomEndpoint,
-        forcePathStyle: s3CustomEndpoint != null,
-      });
-    }
-    return AwsFileUploader.s3Client;
-  }
-  
-  // アプリケーション終了時のクリーンアップ
-  static async cleanup() {
-    if (AwsFileUploader.s3Client) {
-      await AwsFileUploader.s3Client.destroy();
-      AwsFileUploader.s3Client = null;
-    }
-  }
-}
-```
-
-#### 2. ページネーション処理の改善
-```typescript
-(lib as any).listFiles = async function* () { // Generator関数として実装
-  const s3 = this.getS3Client();
-  let nextMarker: string | undefined;
-  const BATCH_SIZE = 1000; // バッチサイズ制限
-  
-  do {
-    const { Contents = [], IsTruncated, NextMarker } = await s3.send(new ListObjectsCommand({
-      Bucket: getS3Bucket(),
-      Marker: nextMarker,
-      MaxKeys: BATCH_SIZE, // S3の一回のレスポンス制限
-    }));
-    
-    // バッチ単位で yield(メモリ効率化)
-    yield Contents.map(({ Key, Size }) => ({
-      name: Key as string,
-      size: Size as number,
-    }));
-    
-    nextMarker = IsTruncated ? NextMarker : undefined;
-  } while (nextMarker);
-};
-```
-
-#### 3. ストリーム処理の改善
-```typescript
-override async findDeliveryFile(attachment: IAttachmentDocument): Promise<NodeJS.ReadableStream> {
-  if (!this.getIsReadable()) {
-    throw new Error('AWS is not configured.');
-  }
-
-  const s3 = this.getS3Client(); // シングルトンクライアント使用
-  const filePath = getFilePathOnStorage(attachment);
-
-  const params = {
-    Bucket: getS3Bucket(),
-    Key: filePath,
-  };
-
-  // check file exists
-  const isExists = await isFileExists(s3, params);
-  if (!isExists) {
-    throw new Error(`Any object that relate to the Attachment (${filePath}) does not exist in AWS S3`);
-  }
-
-  try {
-    const response = await s3.send(new GetObjectCommand(params));
-    const body = response.Body;
-
-    if (body == null) {
-      throw new Error(`S3 returned null for the Attachment (${filePath})`);
-    }
-
-    const stream = 'stream' in body
-      ? body.stream() as unknown as NodeJS.ReadableStream
-      : body as unknown as NodeJS.ReadableStream;
-    
-    // エラーハンドリング追加
-    stream.on('error', (err) => {
-      logger.error('Stream error:', err);
-      stream.destroy();
-    });
-
-    return stream;
-  }
-  catch (err) {
-    logger.error(err);
-    throw new Error(`Couldn't get file from AWS for the Attachment (${attachment._id.toString()})`);
-  }
-}
-```
-
-### GridFS ファイルアップローダー 修正案
-
-#### 1. 接続管理の改善(最優先)
-```typescript
-class GridfsFileUploader extends AbstractFileUploader {
-  private attachmentFileModel: any = null;
-  private chunkCollection: any = null;
-  private isInitialized = false;
-  
-  constructor(crowi: Crowi) {
-    super(crowi);
-  }
-  
-  private async initializeModels() {
-    if (this.isInitialized) return;
-    
-    // 接続状態チェック
-    if (mongoose.connection.readyState !== 1) {
-      throw new Error('MongoDB connection is not ready');
-    }
-    
-    this.attachmentFileModel = createModel({
-      modelName: COLLECTION_NAME,
-      bucketName: COLLECTION_NAME,
-      connection: mongoose.connection,
-    });
-    
-    this.chunkCollection = mongoose.connection.collection(CHUNK_COLLECTION_NAME);
-    this.isInitialized = true;
-  }
-  
-  // 各メソッドで初期化チェック
-  async uploadAttachment(readable: Readable, attachment: IAttachmentDocument): Promise<void> {
-    await this.initializeModels();
-    // ... 処理続行
-  }
-  
-  // クリーンアップメソッド
-  async cleanup() {
-    this.attachmentFileModel = null;
-    this.chunkCollection = null;
-    this.isInitialized = false;
-  }
-}
-```
-
-#### 2. ストリーム処理の改善
-```typescript
-lib.saveFile = async function({ filePath, contentType, data }) {
-  await this.initializeModels();
-  
-  return new Promise((resolve, reject) => {
-    const readable = new Readable({
-      read() {
-        this.push(data);
-        this.push(null);
-      }
-    });
-    
-    readable.on('error', (err) => {
-      logger.error('Readable stream error:', err);
-      readable.destroy();
-      reject(err);
-    });
-    
-    this.attachmentFileModel.promisifiedWrite({
-      filename: filePath,
-      contentType,
-    }, readable)
-      .then(resolve)
-      .catch(reject)
-      .finally(() => {
-        readable.destroy(); // 明示的なクリーンアップ
-      });
-  });
-};
-```
-
-#### 3. プロミス化処理の改善
-```typescript
-// グローバル変更ではなく、インスタンスメソッドとして実装
-private setupPromisifiedMethods() {
-  if (!this.attachmentFileModel.promisifiedWrite) {
-    this.attachmentFileModel.promisifiedWrite = util.promisify(
-      this.attachmentFileModel.write
-    ).bind(this.attachmentFileModel);
-    
-    this.attachmentFileModel.promisifiedUnlink = util.promisify(
-      this.attachmentFileModel.unlink
-    ).bind(this.attachmentFileModel);
-  }
-}
-```
-
----
-
-## 🎯 優先順位と対応方針
-
-### 即座に対応すべき項目(高リスク)
-1. **AWS S3Client のシングルトン化** - リソース重複の解消
-2. **GridFS グローバル接続依存の改善** - 接続管理の明確化
-3. **ページネーション処理のメモリ効率化** - 大量データ対応
-
-### 短期間で対応すべき項目(中リスク)
-4. **ストリーム処理のエラーハンドリング強化**
-5. **リソースクリーンアップの明示化**
-6. **プロミス化処理の安全化**
-
-### 中長期で検討すべき項目
-7. **Generator関数による非同期イテレーション導入**
-8. **メモリ使用量監視の追加**
-9. **接続プール設定の最適化**
-
-## 📊 影響予測
-
-### 修正前のリスク
-- **AWS S3**: 同時接続数増大による接続プール枯渇
-- **GridFS**: MongoDB接続リソースリーク
-- **共通**: 大量ファイル処理時のメモリ不足
-
-### 修正後の改善予想
-- **メモリ使用量**: 70-80% 削減予想
-- **接続リソース**: 90% 以上の効率化
-- **安定性**: エラー耐性の大幅向上
-
-## 🔍 継続監視項目
-
-- AWS S3接続プールの使用状況
-- GridFS接続とコレクション参照の状態
-- 大量ファイル処理時のメモリ使用量
-- ストリーム処理での例外発生率
-- ファイルアップロード/ダウンロードのスループット
-
----
-**作成日**: 2025年9月12日  
-**対象ファイル**: 
-- `/workspace/growi/apps/app/src/server/service/file-uploader/aws/index.ts`
-- `/workspace/growi/apps/app/src/server/service/file-uploader/gridfs.ts`  
-**分析者**: GitHub Copilot  
-**重要度**: 高(ファイル処理の安定性とパフォーマンスに直結)

+ 1 - 1
apps/app/package.json

@@ -166,7 +166,7 @@
     "mkdirp": "^1.0.3",
     "mongodb": "^4.17.2",
     "mongoose": "^6.13.6",
-    "mongoose-gridfs": "^1.2.42",
+    "mongoose-gridfs": "^1.3.0",
     "mongoose-paginate-v2": "^1.3.9",
     "mongoose-unique-validator": "^2.0.3",
     "multer": "~1.4.0",

+ 109 - 49
apps/app/src/server/service/file-uploader/gridfs.ts

@@ -16,16 +16,63 @@ import { createContentHeaders, getContentHeaderValue } 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,
-});
+type PromisifiedUtils = {
+  read: (options?: object) => Readable;
+  write: (file: object, stream: Readable, done?: Function) => void;
+  unlink: (file: object, done?: Function) => void;
+  promisifiedWrite: (file: object, readable: Readable) => Promise<any>;
+  promisifiedUnlink: (file: object) => Promise<any>;
+}
+
+type AttachmentFileModel = mongoose.Model<any> & PromisifiedUtils;
+
+// Cache holders to avoid repeated model creation and manage lifecycle
+let cachedAttachmentFileModel: AttachmentFileModel;
+let cachedChunkCollection: mongoose.Collection;
+let cachedConnection: mongoose.Connection; // Track the connection instance itself
+
+/**
+ * Initialize GridFS models with connection instance monitoring
+ * This prevents memory leaks from repeated model creation
+ */
+function initializeGridFSModels(): { attachmentFileModel: AttachmentFileModel, chunkCollection: mongoose.Collection } {
+  // Check if we can reuse cached models by comparing connection instance
+  if (cachedAttachmentFileModel != null && cachedChunkCollection != null && cachedConnection === mongoose.connection) {
+    return { attachmentFileModel: cachedAttachmentFileModel, chunkCollection: cachedChunkCollection };
+  }
+
+  // Check connection state
+  if (mongoose.connection.readyState !== 1) {
+    throw new Error('MongoDB connection is not ready for GridFS operations');
+  }
+
+  // Create new model instances
+  const attachmentFileModel = createModel({
+    modelName: COLLECTION_NAME,
+    bucketName: COLLECTION_NAME,
+    connection: mongoose.connection,
+  });
+
+  const chunkCollection = mongoose.connection.collection(CHUNK_COLLECTION_NAME);
+
+  // Setup promisified methods on the model instance (not globally)
+  if (!attachmentFileModel.promisifiedWrite) {
+    attachmentFileModel.promisifiedWrite = util.promisify(attachmentFileModel.write).bind(attachmentFileModel);
+    attachmentFileModel.promisifiedUnlink = util.promisify(attachmentFileModel.unlink).bind(attachmentFileModel);
+  }
+
+  // Cache the instances
+  cachedAttachmentFileModel = attachmentFileModel;
+  cachedChunkCollection = chunkCollection;
+  cachedConnection = mongoose.connection;
+
+  logger.debug('GridFS models initialized successfully');
+
+  return { attachmentFileModel, chunkCollection };
+}
 
 
 // TODO: rewrite this module to be a type-safe implementation
@@ -65,9 +112,10 @@ class GridfsFileUploader extends AbstractFileUploader {
   override async uploadAttachment(readable: Readable, attachment: IAttachmentDocument): Promise<void> {
     logger.debug(`File uploading: fileName=${attachment.fileName}`);
 
+    const { attachmentFileModel } = initializeGridFSModels();
     const contentHeaders = createContentHeaders(attachment);
 
-    return AttachmentFile.promisifiedWrite(
+    return attachmentFileModel.promisifiedWrite(
       {
         // put type and the file name for reference information when uploading
         filename: attachment.fileName,
@@ -104,60 +152,42 @@ class GridfsFileUploader extends AbstractFileUploader {
 module.exports = function(crowi: Crowi) {
   const lib = new GridfsFileUploader(crowi);
 
-  // get Collection instance of chunk
-  const chunkCollection = mongoose.connection.collection(CHUNK_COLLECTION_NAME);
-
-  // create promisified method
-  AttachmentFile.promisifiedWrite = util.promisify(AttachmentFile.write).bind(AttachmentFile);
-  AttachmentFile.promisifiedUnlink = util.promisify(AttachmentFile.unlink).bind(AttachmentFile);
-
   lib.isValidUploadSettings = function() {
     return true;
   };
 
   (lib as any).deleteFile = async function(attachment) {
+    const { attachmentFileModel } = initializeGridFSModels();
     const filenameValue = attachment.fileName;
 
-    const attachmentFile = await AttachmentFile.findOne({ filename: filenameValue });
+    const attachmentFile = await attachmentFileModel.findOne({ filename: filenameValue });
 
     if (attachmentFile == null) {
       logger.warn(`Any AttachmentFile that relate to the Attachment (${attachment._id.toString()}) does not exist in GridFS`);
       return;
     }
-    return AttachmentFile.promisifiedUnlink({ _id: attachmentFile._id });
+
+    return attachmentFileModel.promisifiedUnlink({ _id: attachmentFile._id });
   };
 
+  /**
+   * Bulk delete files since unlink method of mongoose-gridfs does not support bulk operation
+   */
   (lib as any).deleteFiles = async function(attachments) {
+    const { attachmentFileModel, chunkCollection } = initializeGridFSModels();
+
     const filenameValues = attachments.map((attachment) => {
       return attachment.fileName;
     });
-    const fileIdObjects = await AttachmentFile.find({ filename: { $in: filenameValues } }, { _id: 1 });
+    const fileIdObjects = await attachmentFileModel.find({ filename: { $in: filenameValues } }, { _id: 1 });
     const idsRelatedFiles = fileIdObjects.map((obj) => { return obj._id });
 
     return Promise.all([
-      AttachmentFile.deleteMany({ filename: { $in: filenameValues } }),
+      attachmentFileModel.deleteMany({ filename: { $in: filenameValues } }),
       chunkCollection.deleteMany({ files_id: { $in: idsRelatedFiles } }),
     ]);
   };
 
-  /**
-   * get size of data uploaded files using (Promise wrapper)
-   */
-  // const getCollectionSize = () => {
-  //   return new Promise((resolve, reject) => {
-  //     chunkCollection.stats((err, data) => {
-  //       if (err) {
-  //         // return 0 if not exist
-  //         if (err.errmsg.includes('not found')) {
-  //           return resolve(0);
-  //         }
-  //         return reject(err);
-  //       }
-  //       return resolve(data.size);
-  //     });
-  //   });
-  // };
-
   /**
    * check the file size limit
    *
@@ -172,17 +202,44 @@ module.exports = function(crowi: Crowi) {
   };
 
   lib.saveFile = async function({ filePath, contentType, data }) {
-    const readable = new Readable();
-    readable.push(data);
-    readable.push(null); // EOF
+    const { attachmentFileModel } = initializeGridFSModels();
 
-    return AttachmentFile.promisifiedWrite(
-      {
-        filename: filePath,
-        contentType,
+    // Create a readable stream from the data
+    const readable = new Readable({
+      read() {
+        this.push(data);
+        this.push(null); // EOF
       },
-      readable,
-    );
+    });
+
+    try {
+      // Add error handling to prevent resource leaks
+      readable.on('error', (err) => {
+        logger.error('Readable stream error:', err);
+        readable.destroy();
+        throw err;
+      });
+
+      // Use async/await for cleaner code
+      const result = await attachmentFileModel.promisifiedWrite(
+        {
+          filename: filePath,
+          contentType,
+        },
+        readable,
+      );
+
+      return result;
+    }
+    catch (error) {
+      throw error;
+    }
+    finally {
+      // Explicit cleanup to prevent memory leaks
+      if (typeof readable.destroy === 'function') {
+        readable.destroy();
+      }
+    }
   };
 
   /**
@@ -192,23 +249,26 @@ module.exports = function(crowi: Crowi) {
    * @return {stream.Readable} readable stream
    */
   lib.findDeliveryFile = async function(attachment) {
+    const { attachmentFileModel } = initializeGridFSModels();
     const filenameValue = attachment.fileName;
 
-    const attachmentFile = await AttachmentFile.findOne({ filename: filenameValue });
+    const attachmentFile = await attachmentFileModel.findOne({ filename: filenameValue });
 
     if (attachmentFile == null) {
       throw new Error(`Any AttachmentFile that relate to the Attachment (${attachment._id.toString()}) does not exist in GridFS`);
     }
 
     // return stream.Readable
-    return AttachmentFile.read({ _id: attachmentFile._id });
+    return attachmentFileModel.read({ _id: attachmentFile._id });
   };
 
   /**
    * List files in storage
    */
   (lib as any).listFiles = async function() {
-    const attachmentFiles = await AttachmentFile.find();
+    const { attachmentFileModel } = initializeGridFSModels();
+
+    const attachmentFiles = await attachmentFileModel.find();
     return attachmentFiles.map(({ filename: name, length: size }) => ({
       name, size,
     }));

+ 76 - 70
pnpm-lock.yaml

@@ -509,8 +509,8 @@ importers:
         specifier: ^6.13.6
         version: 6.13.8(@aws-sdk/client-sso-oidc@3.600.0)
       mongoose-gridfs:
-        specifier: ^1.2.42
-        version: 1.2.42(mongoose@6.13.8(@aws-sdk/client-sso-oidc@3.600.0))
+        specifier: ^1.3.0
+        version: 1.3.0(@aws-sdk/client-sso-oidc@3.600.0)(mongoose@6.13.8(@aws-sdk/client-sso-oidc@3.600.0))
       mongoose-paginate-v2:
         specifier: ^1.3.9
         version: 1.8.2
@@ -3400,23 +3400,26 @@ packages:
   '@lezer/yaml@1.0.3':
     resolution: {integrity: sha512-GuBLekbw9jDBDhGur82nuwkxKQ+a3W5H0GfaAthDXcAu+XdpS43VlnxA9E9hllkpSP5ellRDKjLLj7Lu9Wr6xA==}
 
-  '@lykmapipo/common@0.34.3':
-    resolution: {integrity: sha512-rdLJkeatlCWEZFXC142V/fLAuKHREJcfPSC7OAjpn4DEvANfmvCgwLl+gwLLwFsn8lwnBDGo+7Y6pwirw86FpA==}
+  '@lykmapipo/common@0.44.5':
+    resolution: {integrity: sha512-xkG/1aaOPMdyMKwJ4reNTlwN/s2drXmgfF7Siwwz/SHjKFOnonALmKURLyKuZ8FmSByP5ohaMjPCnzfO4YMM1A==}
     engines: {node: '>=8.11.1', npm: '>=5.6.0'}
 
-  '@lykmapipo/env@0.17.8':
-    resolution: {integrity: sha512-wtwBhTACxMZ342j1CSUUXtiNQOH+yl+8vyptBXcQtZCz5QCHMO7sInCgtKUezEduaY9evs/aNINwrcTVA485dQ==}
+  '@lykmapipo/env@0.17.39':
+    resolution: {integrity: sha512-0V2x4+Lao/7SzxQcO4LCYLjtrrUS06fNJXVLcyUBAN8JnuM3kbo0GR1mujaiV72Q9/1bIdZDxfJW2OnNyKJGdQ==}
     engines: {node: '>=8.11.1', npm: '>=5.6.0'}
 
-  '@lykmapipo/mongoose-common@0.35.0':
-    resolution: {integrity: sha512-XvbiTSkhI8bhfHw4slXpWxbRsDe27XhM0946JMySGcgG7T1Ohe+I+C8nTKzsORU5EcBdyyYgHgTIpdc55oXlcg==}
-    peerDependencies:
-      mongoose: '>=5.9.17'
+  '@lykmapipo/mongoose-common@0.40.0':
+    resolution: {integrity: sha512-dU32a3iq0nSSWkPTqr4LA+gcC2NfpyGZr1pd9YFn1jfpw9M2Y0qfGhugzaQJ3rP8w3zGJHt8k3+6WLOLLaDylQ==}
+    engines: {node: '>=8.11.1', npm: '>=5.6.0'}
 
-  '@lykmapipo/phone@0.6.5':
-    resolution: {integrity: sha512-b3x17Rn7E/20hf7RFbd2szwa05C/SIRCnjgcFoOi3YYLkIlKIWU/IB596EHmx8nYiX9XYb+RIdvvcx2WhgR/8A==}
+  '@lykmapipo/mongoose-connection@0.5.2':
+    resolution: {integrity: sha512-9ykz/IoraBBZmF3IndHM/QJO6VSb5GRO8jg3F+ZEzr0qoYYQpLRUgl4HzCYkgKXWES2ccjx4XWoruD2zWDCAbg==}
     engines: {node: '>=8.11.1', npm: '>=5.6.0'}
 
+  '@lykmapipo/phone@0.7.16':
+    resolution: {integrity: sha512-YkHyZav72pgXpa0oBqVxud8Mdw/T9LmopMMECzTUTQ6UqXeehd9UuDcQauBY3vC9+MjHL0I9Cd2yF9+24/AZQQ==}
+    engines: {node: '>=14.5.0', npm: '>=6.14.5'}
+
   '@manypkg/find-root@1.1.0':
     resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==}
 
@@ -6625,8 +6628,8 @@ packages:
   browser-bunyan@1.8.0:
     resolution: {integrity: sha512-Et1TaRUm8m2oy4OTi69g0qAM8wqpofACUgkdBnj1Kq2aC8Wpl8w+lNevebPG6zKH2w0Aq+BHiAXWwjm0/QbkaQ==}
 
-  browser-or-node@1.2.1:
-    resolution: {integrity: sha512-sVIA0cysIED0nbmNOm7sZzKfgN1rpFmrqvLZaFWspaBAftfQcezlC81G6j6U2RJf4Lh66zFxrCeOsvkUXIcPWg==}
+  browser-or-node@3.0.0:
+    resolution: {integrity: sha512-iczIdVJzGEYhP5DqQxYM9Hh7Ztpqqi+CXZpSmX8ALFs9ecXkQIeqRyM6TfxEfMVpwhl3dSuDvxdzzo9sUOIVBQ==}
 
   browser-sync-client@3.0.4:
     resolution: {integrity: sha512-+ew5ubXzGRKVjquBL3u6najS40TG7GxCdyBll0qSRc/n+JRV9gb/yDdRL1IAgRHqjnJTdqeBKKIQabjvjRSYRQ==}
@@ -9300,8 +9303,8 @@ packages:
     resolution: {integrity: sha512-5Rk7iLNDFhFeBYc3s8l1CqzbEBcdhwR193RlD4vSNFajIcINKI8W8P0JLmBpwymHqqWbX34pJDQu39cSy/6RsA==}
     engines: {node: '>=10'}
 
-  google-libphonenumber@3.2.10:
-    resolution: {integrity: sha512-TsckE9O8QgqaIeaOXPjcJa4/kX3BzFdO1oCbMfmUpRZckml4xJhjJVxaT9Mdt/VrZZkT9lX44eHAEWfJK1tHtw==}
+  google-libphonenumber@3.2.42:
+    resolution: {integrity: sha512-60jm6Lu72WmlUJXUBJmmuZlHG2vDJ2gQ9pL5gcFsSe1Q4eigsm0Z1ayNHjMgqGUl0zey8JqKtO4QCHPV+5LCNQ==}
     engines: {node: '>=0.10'}
 
   google-p12-pem@3.1.4:
@@ -9700,9 +9703,9 @@ packages:
   infer-owner@1.0.4:
     resolution: {integrity: sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==}
 
-  inflection@1.12.0:
-    resolution: {integrity: sha512-lRy4DxuIFWXlJU7ed8UiTJOSTqStqYdEb4CEbtXfNbkdj3nH1L+reUWiE10VWcJS2yR7tge8Z74pJjtBjNwj0w==}
-    engines: {'0': node >= 0.4.0}
+  inflection@3.0.2:
+    resolution: {integrity: sha512-+Bg3+kg+J6JUWn8J6bzFmOWkTQ6L/NHfDRSYU+EVvuKHDxUDHAXgqixHfVlzuBQaPOTac8hn43aPhMNk6rMe3g==}
+    engines: {node: '>=18.0.0'}
 
   inflight@1.0.6:
     resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
@@ -11335,30 +11338,26 @@ packages:
       socks:
         optional: true
 
-  mongoose-gridfs@1.2.42:
-    resolution: {integrity: sha512-n0yGLrWHeEW5PpR1xvB7bSSqcOnXdWSuwkQyEW8+u98eHfffD2kKT7Re2bxMvIBzOK76Q32uyYkOTzH+Y6MwZQ==}
-    engines: {node: '>=8.6.0', npm: '>=5.3.0'}
+  mongoose-gridfs@1.3.0:
+    resolution: {integrity: sha512-5Rrgb00LN5mRC1s+ddeQ032nGVyRUM6bbX5nqDgWsAJPcpYCjaarnKIlOf+rDxSSAhbn7GzZ3fUvddG+6OX88Q==}
+    engines: {node: '>=8.11.1', npm: '>=5.6.0'}
     peerDependencies:
-      mongoose: '>=5.9.15'
+      mongoose: '>=6.0.7'
 
   mongoose-paginate-v2@1.8.2:
     resolution: {integrity: sha512-T/Z3qKyKnPUa6UkH1IjHxdYnYApCAKk9zb2C0GF5hg3QETcI62AUAUQGCBE2tIw7fF4feUaDARMajj/bersyvg==}
     engines: {node: '>=4.0.0'}
 
-  mongoose-schema-jsonschema@2.0.1:
-    resolution: {integrity: sha512-OHXK/tSziSSuNXKxsjvDyYwnGVB+/c5Dn7p2sI6Vri0vTJm13Nime68YwK8m1j9jgkqh2ZXiO5TyVXTQHtxG8Q==}
-    peerDependencies:
-      mongoose: ^5.0.0 || ^6.0.0
-
   mongoose-unique-validator@2.0.3:
     resolution: {integrity: sha512-3/8pmvAC1acBZS6eWKAWQUiZBlARE1wyWtjga4iQ2wDJeOfRlIKmAvTNHSZXKaAf7RCRUd7wh7as6yWAOrjpQg==}
     peerDependencies:
       mongoose: ^5.2.1
 
-  mongoose-valid8@1.6.18:
-    resolution: {integrity: sha512-0MgK1sD9HXAK7I2lyFRlwNMfZ8+Ahx7rH0Hg6sJyXiXMCazK6Mw4lNcdX0ISjuKkI7joORz2T5Eyw6cJ3q5vQQ==}
+  mongoose-valid8@1.7.1:
+    resolution: {integrity: sha512-65Zf+md73TkMNMUQ3tJzOtEm3MxJW15bpy+lBomqep7FNtiMjNoMzbN0P3/1FCpbqIkXDpeWH7WtyE2+D0tmhg==}
+    engines: {node: '>=8.11.1', npm: '>=5.6.0'}
     peerDependencies:
-      mongoose: '>=5.9.15'
+      mongoose: '>=6.0.7'
 
   mongoose@6.13.8:
     resolution: {integrity: sha512-JHKco/533CyVrqCbyQsnqMpLn8ZCiKrPDTd2mvo2W7ygIvhygWjX2wj+RPjn6upZZgw0jC6U51RD7kUsyK8NBg==}
@@ -11983,10 +11982,6 @@ packages:
     resolution: {integrity: sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==}
     engines: {node: '>=18'}
 
-  parse-ms@2.1.0:
-    resolution: {integrity: sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==}
-    engines: {node: '>=6'}
-
   parse5-htmlparser2-tree-adapter@6.0.1:
     resolution: {integrity: sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==}
 
@@ -12502,8 +12497,8 @@ packages:
   randombytes@2.1.0:
     resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
 
-  randomcolor@0.5.4:
-    resolution: {integrity: sha512-nYd4nmTuuwMFzHL6W+UWR5fNERGZeVauho8mrJDUSXdNDbao4rbrUwhuLgKC/j8VCS5+34Ria8CsTDuBjrIrQA==}
+  randomcolor@0.6.2:
+    resolution: {integrity: sha512-Mn6TbyYpFgwFuQ8KJKqf3bqqY9O1y37/0jgSK/61PUxV4QfIMv0+K2ioq8DfOjkBslcjwSzRfIDEXfzA9aCx7A==}
 
   range-parser@1.2.1:
     resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
@@ -17795,48 +17790,62 @@ snapshots:
       '@lezer/highlight': 1.2.1
       '@lezer/lr': 1.4.2
 
-  '@lykmapipo/common@0.34.3':
+  '@lykmapipo/common@0.44.5':
     dependencies:
       auto-parse: 1.8.0
-      browser-or-node: 1.2.1
+      browser-or-node: 3.0.0
       flat: 5.0.2
-      inflection: 1.12.0
+      inflection: 3.0.2
       lodash: 4.17.21
       mime: 3.0.0
       moment: 2.30.1
       object-hash: 2.2.0
-      parse-json: 5.2.0
-      parse-ms: 2.1.0
-      randomcolor: 0.5.4
+      randomcolor: 0.6.2
       statuses: 2.0.1
       string-template: 1.0.0
       striptags: 3.2.0
       uuid: 11.1.0
 
-  '@lykmapipo/env@0.17.8':
+  '@lykmapipo/env@0.17.39':
     dependencies:
-      '@lykmapipo/common': 0.34.3
+      '@lykmapipo/common': 0.44.5
       dotenv: 16.4.5
       dotenv-expand: 5.1.0
       lodash: 4.17.21
       rc: 1.2.8
       semver: 7.6.3
 
-  '@lykmapipo/mongoose-common@0.35.0(mongoose@6.13.8(@aws-sdk/client-sso-oidc@3.600.0))':
+  '@lykmapipo/mongoose-common@0.40.0(@aws-sdk/client-sso-oidc@3.600.0)':
     dependencies:
-      '@lykmapipo/common': 0.34.3
-      '@lykmapipo/env': 0.17.8
+      '@lykmapipo/common': 0.44.5
+      '@lykmapipo/env': 0.17.39
+      '@lykmapipo/mongoose-connection': 0.5.2(@aws-sdk/client-sso-oidc@3.600.0)
       async: 3.2.4
       lodash: 4.17.21
       mongoose: 6.13.8(@aws-sdk/client-sso-oidc@3.600.0)
-      mongoose-schema-jsonschema: 2.0.1(mongoose@6.13.8(@aws-sdk/client-sso-oidc@3.600.0))
-      mongoose-valid8: 1.6.18(mongoose@6.13.8(@aws-sdk/client-sso-oidc@3.600.0))
+      mongoose-valid8: 1.7.1(mongoose@6.13.8(@aws-sdk/client-sso-oidc@3.600.0))
+    transitivePeerDependencies:
+      - '@aws-sdk/client-sso-oidc'
+      - aws-crt
+      - supports-color
 
-  '@lykmapipo/phone@0.6.5':
+  '@lykmapipo/mongoose-connection@0.5.2(@aws-sdk/client-sso-oidc@3.600.0)':
     dependencies:
-      '@lykmapipo/common': 0.34.3
-      '@lykmapipo/env': 0.17.8
-      google-libphonenumber: 3.2.10
+      '@lykmapipo/common': 0.44.5
+      '@lykmapipo/env': 0.17.39
+      async: 3.2.4
+      lodash: 4.17.21
+      mongoose: 6.13.8(@aws-sdk/client-sso-oidc@3.600.0)
+    transitivePeerDependencies:
+      - '@aws-sdk/client-sso-oidc'
+      - aws-crt
+      - supports-color
+
+  '@lykmapipo/phone@0.7.16':
+    dependencies:
+      '@lykmapipo/common': 0.44.5
+      '@lykmapipo/env': 0.17.39
+      google-libphonenumber: 3.2.42
       lodash: 4.17.21
 
   '@manypkg/find-root@1.1.0':
@@ -22307,7 +22316,7 @@ snapshots:
       '@browser-bunyan/console-raw-stream': 1.8.0
       '@browser-bunyan/levels': 1.8.0
 
-  browser-or-node@1.2.1: {}
+  browser-or-node@3.0.0: {}
 
   browser-sync-client@3.0.4:
     dependencies:
@@ -25162,7 +25171,7 @@ snapshots:
       - encoding
       - supports-color
 
-  google-libphonenumber@3.2.10: {}
+  google-libphonenumber@3.2.42: {}
 
   google-p12-pem@3.1.4:
     dependencies:
@@ -25686,7 +25695,7 @@ snapshots:
 
   infer-owner@1.0.4: {}
 
-  inflection@1.12.0: {}
+  inflection@3.0.2: {}
 
   inflight@1.0.6:
     dependencies:
@@ -27750,30 +27759,29 @@ snapshots:
       '@aws-sdk/credential-providers': 3.600.0(@aws-sdk/client-sso-oidc@3.600.0)
       socks: 2.8.3
 
-  mongoose-gridfs@1.2.42(mongoose@6.13.8(@aws-sdk/client-sso-oidc@3.600.0)):
+  mongoose-gridfs@1.3.0(@aws-sdk/client-sso-oidc@3.600.0)(mongoose@6.13.8(@aws-sdk/client-sso-oidc@3.600.0)):
     dependencies:
-      '@lykmapipo/mongoose-common': 0.35.0(mongoose@6.13.8(@aws-sdk/client-sso-oidc@3.600.0))
+      '@lykmapipo/mongoose-common': 0.40.0(@aws-sdk/client-sso-oidc@3.600.0)
       lodash: 4.17.21
       mongoose: 6.13.8(@aws-sdk/client-sso-oidc@3.600.0)
       stream-read: 1.1.2
+    transitivePeerDependencies:
+      - '@aws-sdk/client-sso-oidc'
+      - aws-crt
+      - supports-color
 
   mongoose-paginate-v2@1.8.2: {}
 
-  mongoose-schema-jsonschema@2.0.1(mongoose@6.13.8(@aws-sdk/client-sso-oidc@3.600.0)):
-    dependencies:
-      mongoose: 6.13.8(@aws-sdk/client-sso-oidc@3.600.0)
-      pluralize: 8.0.0
-
   mongoose-unique-validator@2.0.3(mongoose@6.13.8(@aws-sdk/client-sso-oidc@3.600.0)):
     dependencies:
       lodash.foreach: 4.5.0
       lodash.get: 4.4.2
       mongoose: 6.13.8(@aws-sdk/client-sso-oidc@3.600.0)
 
-  mongoose-valid8@1.6.18(mongoose@6.13.8(@aws-sdk/client-sso-oidc@3.600.0)):
+  mongoose-valid8@1.7.1(mongoose@6.13.8(@aws-sdk/client-sso-oidc@3.600.0)):
     dependencies:
-      '@lykmapipo/env': 0.17.8
-      '@lykmapipo/phone': 0.6.5
+      '@lykmapipo/env': 0.17.39
+      '@lykmapipo/phone': 0.7.16
       lodash: 4.17.21
       mongoose: 6.13.8(@aws-sdk/client-sso-oidc@3.600.0)
       validator: 13.12.0
@@ -28573,8 +28581,6 @@ snapshots:
       index-to-position: 1.1.0
       type-fest: 4.41.0
 
-  parse-ms@2.1.0: {}
-
   parse5-htmlparser2-tree-adapter@6.0.1:
     dependencies:
       parse5: 6.0.1
@@ -29060,7 +29066,7 @@ snapshots:
     dependencies:
       safe-buffer: 5.2.1
 
-  randomcolor@0.5.4: {}
+  randomcolor@0.6.2: {}
 
   range-parser@1.2.1: {}