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

Merge pull request #9460 from weseek/imprv/158220-158276-clean-up-page-bulk-export-service

Imprv/158220 158276 clean up page bulk export service
Yuki Takei 1 год назад
Родитель
Сommit
dd407c8d06

+ 4 - 1
.devcontainer/devcontainer.json

@@ -37,7 +37,10 @@
         "vitest.explorer",
         "ms-playwright.playwright"
       ],
-    }
+      "settings": {
+        "terminal.integrated.defaultProfile.linux": "bash"
+      }
+    },
   },
 
   // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.

+ 1 - 3
apps/app/package.json

@@ -115,7 +115,6 @@
     "ejs": "^3.1.10",
     "esa-node": "^0.2.2",
     "escape-string-regexp": "^4.0.0",
-    "eslint-plugin-regex": "^1.8.0",
     "expose-gc": "^1.0.0",
     "express": "^4.20.0",
     "express-bunyan-logger": "^1.3.3",
@@ -161,7 +160,7 @@
     "next": "^14.2.13",
     "next-dynamic-loading-props": "^0.1.1",
     "next-i18next": "^15.3.1",
-    "next-superjson": "^0.0.4",
+    "next-superjson": "^1.0.7",
     "next-themes": "^0.2.1",
     "nocache": "^4.0.0",
     "node-cron": "^3.0.2",
@@ -283,7 +282,6 @@
     "downshift": "^8.2.3",
     "eazy-logger": "^3.1.0",
     "eslint-plugin-jest": "^26.5.3",
-    "eslint-plugin-regex": "^1.8.0",
     "fslightbox-react": "^1.7.6",
     "handsontable": "=6.2.2",
     "happy-dom": "^15.7.4",

+ 2 - 7
apps/app/src/features/page-bulk-export/server/routes/apiv3/page-bulk-export.ts

@@ -7,8 +7,7 @@ import type Crowi from '~/server/crowi';
 import type { ApiV3Response } from '~/server/routes/apiv3/interfaces/apiv3-response';
 import loggerFactory from '~/utils/logger';
 
-import { pageBulkExportService } from '../../service/page-bulk-export';
-import { DuplicateBulkExportJobError } from '../../service/page-bulk-export/errors';
+import { pageBulkExportService, DuplicateBulkExportJobError } from '../../service/page-bulk-export';
 
 const logger = loggerFactory('growi:routes:apiv3:page-bulk-export');
 
@@ -36,13 +35,9 @@ module.exports = (crowi: Crowi): Router => {
     }
 
     const { path, format, restartJob } = req.body;
-    const activityParameters = {
-      ip: req.ip,
-      endpoint: req.originalUrl,
-    };
 
     try {
-      await pageBulkExportService?.createAndExecuteOrRestartBulkExportJob(path, req.user, activityParameters, restartJob);
+      await pageBulkExportService?.createOrResetBulkExportJob(path, req.user, restartJob);
       return res.apiv3({}, 204);
     }
     catch (err) {

+ 82 - 0
apps/app/src/features/page-bulk-export/server/service/page-bulk-export.ts

@@ -0,0 +1,82 @@
+import {
+  type IPage, SubscriptionStatusType,
+} from '@growi/core';
+import type { HydratedDocument } from 'mongoose';
+import mongoose from 'mongoose';
+
+
+import { SupportedTargetModel } from '~/interfaces/activity';
+import type { PageModel } from '~/server/models/page';
+import Subscription from '~/server/models/subscription';
+import loggerFactory from '~/utils/logger';
+
+import { PageBulkExportFormat, PageBulkExportJobInProgressStatus, PageBulkExportJobStatus } from '../../interfaces/page-bulk-export';
+import type { PageBulkExportJobDocument } from '../models/page-bulk-export-job';
+import PageBulkExportJob from '../models/page-bulk-export-job';
+
+const logger = loggerFactory('growi:services:PageBulkExportService');
+
+export class DuplicateBulkExportJobError extends Error {
+
+  duplicateJob: HydratedDocument<PageBulkExportJobDocument>;
+
+  constructor(duplicateJob: HydratedDocument<PageBulkExportJobDocument>) {
+    super('Duplicate bulk export job is in progress');
+    this.duplicateJob = duplicateJob;
+  }
+
+}
+
+export interface IPageBulkExportService {
+  createOrResetBulkExportJob: (basePagePath: string, currentUser, restartJob?: boolean) => Promise<void>;
+}
+
+class PageBulkExportService implements IPageBulkExportService {
+
+  // temporal path of local fs to output page files before upload
+  // TODO: If necessary, change to a proper path in https://redmine.weseek.co.jp/issues/149512
+  tmpOutputRootDir = '/tmp/page-bulk-export';
+
+  /**
+   * Create a new page bulk export job or reset the existing one
+   */
+  async createOrResetBulkExportJob(basePagePath: string, currentUser, restartJob = false): Promise<void> {
+    const Page = mongoose.model<IPage, PageModel>('Page');
+    const basePage = await Page.findByPathAndViewer(basePagePath, currentUser, null, true);
+
+    if (basePage == null) {
+      throw new Error('Base page not found or not accessible');
+    }
+
+    const format = PageBulkExportFormat.md;
+    const duplicatePageBulkExportJobInProgress: HydratedDocument<PageBulkExportJobDocument> | null = await PageBulkExportJob.findOne({
+      user: currentUser,
+      page: basePage,
+      format,
+      $or: Object.values(PageBulkExportJobInProgressStatus).map(status => ({ status })),
+    });
+    if (duplicatePageBulkExportJobInProgress != null) {
+      if (restartJob) {
+        this.resetBulkExportJob(duplicatePageBulkExportJobInProgress);
+        return;
+      }
+      throw new DuplicateBulkExportJobError(duplicatePageBulkExportJobInProgress);
+    }
+    const pageBulkExportJob: HydratedDocument<PageBulkExportJobDocument> = await PageBulkExportJob.create({
+      user: currentUser, page: basePage, format, status: PageBulkExportJobStatus.initializing,
+    });
+
+    await Subscription.upsertSubscription(currentUser, SupportedTargetModel.MODEL_PAGE_BULK_EXPORT_JOB, pageBulkExportJob, SubscriptionStatusType.SUBSCRIBE);
+  }
+
+  /**
+   * Reset page bulk export job in progress
+   */
+  async resetBulkExportJob(pageBulkExportJob: HydratedDocument<PageBulkExportJobDocument>): Promise<void> {
+    pageBulkExportJob.restartFlag = true;
+    await pageBulkExportJob.save();
+  }
+
+}
+
+export const pageBulkExportService: PageBulkExportService = new PageBulkExportService(); // singleton instance

+ 0 - 30
apps/app/src/features/page-bulk-export/server/service/page-bulk-export/errors.ts

@@ -1,30 +0,0 @@
-import type { HydratedDocument } from 'mongoose';
-
-import type { PageBulkExportJobDocument } from '../../models/page-bulk-export-job';
-
-export class DuplicateBulkExportJobError extends Error {
-
-  duplicateJob: HydratedDocument<PageBulkExportJobDocument>;
-
-  constructor(duplicateJob: HydratedDocument<PageBulkExportJobDocument>) {
-    super('Duplicate bulk export job is in progress');
-    this.duplicateJob = duplicateJob;
-  }
-
-}
-
-export class BulkExportJobExpiredError extends Error {
-
-  constructor() {
-    super('Bulk export job has expired');
-  }
-
-}
-
-export class BulkExportJobRestartedError extends Error {
-
-  constructor() {
-    super('Bulk export job has restarted');
-  }
-
-}

+ 0 - 462
apps/app/src/features/page-bulk-export/server/service/page-bulk-export/index.ts

@@ -1,462 +0,0 @@
-import { createHash } from 'crypto';
-import fs from 'fs';
-import path from 'path';
-import { Writable } from 'stream';
-import { pipeline as pipelinePromise } from 'stream/promises';
-
-import type { IUser } from '@growi/core';
-import {
-  getIdForRef, getIdStringForRef, type IPage, isPopulated, SubscriptionStatusType,
-} from '@growi/core';
-import { getParentPath, normalizePath } from '@growi/core/dist/utils/path-utils';
-import type { Archiver } from 'archiver';
-import archiver from 'archiver';
-import gc from 'expose-gc/function';
-import type { HydratedDocument } from 'mongoose';
-import mongoose from 'mongoose';
-
-import type { SupportedActionType } from '~/interfaces/activity';
-import { SupportedAction, SupportedTargetModel } from '~/interfaces/activity';
-import { AttachmentType, FilePathOnStoragePrefix } from '~/server/interfaces/attachment';
-import type { ActivityDocument } from '~/server/models/activity';
-import type { IAttachmentDocument } from '~/server/models/attachment';
-import { Attachment } from '~/server/models/attachment';
-import type { PageModel, PageDocument } from '~/server/models/page';
-import Subscription from '~/server/models/subscription';
-import type { FileUploader } from '~/server/service/file-uploader';
-import type { IMultipartUploader } from '~/server/service/file-uploader/multipart-uploader';
-import { preNotifyService } from '~/server/service/pre-notify';
-import { getBufferToFixedSizeTransform } from '~/server/util/stream';
-import loggerFactory from '~/utils/logger';
-
-import { PageBulkExportFormat, PageBulkExportJobInProgressStatus, PageBulkExportJobStatus } from '../../../interfaces/page-bulk-export';
-import type { PageBulkExportJobDocument } from '../../models/page-bulk-export-job';
-import PageBulkExportJob from '../../models/page-bulk-export-job';
-import type { PageBulkExportPageSnapshotDocument } from '../../models/page-bulk-export-page-snapshot';
-import PageBulkExportPageSnapshot from '../../models/page-bulk-export-page-snapshot';
-
-import { BulkExportJobExpiredError, BulkExportJobRestartedError, DuplicateBulkExportJobError } from './errors';
-import { PageBulkExportJobManager } from './page-bulk-export-job-manager';
-
-
-const logger = loggerFactory('growi:services:PageBulkExportService');
-
-export type ActivityParameters ={
-  ip?: string;
-  endpoint: string;
-}
-
-export interface IPageBulkExportService {
-  executePageBulkExportJob: (pageBulkExportJob: HydratedDocument<PageBulkExportJobDocument>, activityParameters?: ActivityParameters) => Promise<void>
-}
-
-class PageBulkExportService implements IPageBulkExportService {
-
-  crowi: any;
-
-  activityEvent: any;
-
-  // multipart upload max part size
-  maxPartSize = 5 * 1024 * 1024; // 5MB
-
-  pageBatchSize = 100;
-
-  compressExtension = 'tar.gz';
-
-  pageBulkExportJobManager: PageBulkExportJobManager;
-
-  // temporal path of local fs to output page files before upload
-  // TODO: If necessary, change to a proper path in https://redmine.weseek.co.jp/issues/149512
-  tmpOutputRootDir = '/tmp/page-bulk-export';
-
-  pageModel: PageModel;
-
-  constructor(crowi) {
-    this.crowi = crowi;
-    this.activityEvent = crowi.event('activity');
-    this.pageModel = mongoose.model<IPage, PageModel>('Page');
-    this.pageBulkExportJobManager = new PageBulkExportJobManager(this);
-  }
-
-  /**
-   * Create a new page bulk export job and execute it
-   */
-  async createAndExecuteOrRestartBulkExportJob(basePagePath: string, currentUser, activityParameters: ActivityParameters, restartJob = false): Promise<void> {
-    const basePage = await this.pageModel.findByPathAndViewer(basePagePath, currentUser, null, true);
-
-    if (basePage == null) {
-      throw new Error('Base page not found or not accessible');
-    }
-
-    const format = PageBulkExportFormat.md;
-    const duplicatePageBulkExportJobInProgress: HydratedDocument<PageBulkExportJobDocument> | null = await PageBulkExportJob.findOne({
-      user: currentUser,
-      page: basePage,
-      format,
-      $or: Object.values(PageBulkExportJobInProgressStatus).map(status => ({ status })),
-    });
-    if (duplicatePageBulkExportJobInProgress != null) {
-      if (restartJob) {
-        this.restartBulkExportJob(duplicatePageBulkExportJobInProgress, activityParameters);
-        return;
-      }
-      throw new DuplicateBulkExportJobError(duplicatePageBulkExportJobInProgress);
-    }
-    const pageBulkExportJob: HydratedDocument<PageBulkExportJobDocument> = await PageBulkExportJob.create({
-      user: currentUser, page: basePage, format, status: PageBulkExportJobStatus.initializing,
-    });
-
-    await Subscription.upsertSubscription(currentUser, SupportedTargetModel.MODEL_PAGE_BULK_EXPORT_JOB, pageBulkExportJob, SubscriptionStatusType.SUBSCRIBE);
-
-    this.pageBulkExportJobManager.addJob(pageBulkExportJob, activityParameters);
-  }
-
-  /**
-   * Restart page bulk export job in progress from the beginning
-   */
-  async restartBulkExportJob(pageBulkExportJob: HydratedDocument<PageBulkExportJobDocument>, activityParameters: ActivityParameters): Promise<void> {
-    await this.cleanUpExportJobResources(pageBulkExportJob, true);
-
-    pageBulkExportJob.status = PageBulkExportJobStatus.initializing;
-    await pageBulkExportJob.save();
-    this.pageBulkExportJobManager.addJob(pageBulkExportJob, activityParameters);
-  }
-
-  /**
-   * Execute a page bulk export job. This method can also resume a previously inturrupted job.
-   */
-  async executePageBulkExportJob(pageBulkExportJob: HydratedDocument<PageBulkExportJobDocument>, activityParameters?: ActivityParameters): Promise<void> {
-    try {
-      const User = mongoose.model<IUser>('User');
-      const user = await User.findById(getIdForRef(pageBulkExportJob.user));
-
-      if (pageBulkExportJob.status === PageBulkExportJobStatus.initializing) {
-        await this.createPageSnapshots(user, pageBulkExportJob);
-
-        const duplicateExportJob = await PageBulkExportJob.findOne({
-          user: pageBulkExportJob.user,
-          page: pageBulkExportJob.page,
-          format: pageBulkExportJob.format,
-          status: PageBulkExportJobStatus.completed,
-          revisionListHash: pageBulkExportJob.revisionListHash,
-        });
-        if (duplicateExportJob != null) {
-          // if an upload with the exact same contents exists, re-use the same attachment of that upload
-          pageBulkExportJob.attachment = duplicateExportJob.attachment;
-          pageBulkExportJob.status = PageBulkExportJobStatus.completed;
-        }
-        else {
-          pageBulkExportJob.status = PageBulkExportJobStatus.exporting;
-        }
-        await pageBulkExportJob.save();
-      }
-      if (pageBulkExportJob.status === PageBulkExportJobStatus.exporting) {
-        await this.exportPagesToFS(pageBulkExportJob);
-        pageBulkExportJob.status = PageBulkExportJobStatus.uploading;
-        await pageBulkExportJob.save();
-      }
-      if (pageBulkExportJob.status === PageBulkExportJobStatus.uploading) {
-        await this.compressAndUpload(user, pageBulkExportJob);
-      }
-    }
-    catch (err) {
-      if (err instanceof BulkExportJobExpiredError) {
-        logger.error(err);
-        await this.notifyExportResultAndCleanUp(SupportedAction.ACTION_PAGE_BULK_EXPORT_JOB_EXPIRED, pageBulkExportJob, activityParameters);
-      }
-      else if (err instanceof BulkExportJobRestartedError) {
-        logger.info(err.message);
-        await this.cleanUpExportJobResources(pageBulkExportJob);
-      }
-      else {
-        logger.error(err);
-        await this.notifyExportResultAndCleanUp(SupportedAction.ACTION_PAGE_BULK_EXPORT_FAILED, pageBulkExportJob, activityParameters);
-      }
-      return;
-    }
-
-    await this.notifyExportResultAndCleanUp(SupportedAction.ACTION_PAGE_BULK_EXPORT_COMPLETED, pageBulkExportJob, activityParameters);
-  }
-
-  /**
-   * Notify the user of the export result, and cleanup the resources used in the export process
-   * @param action whether the export was successful
-   * @param pageBulkExportJob the page bulk export job
-   * @param activityParameters parameters to record user activity
-   */
-  private async notifyExportResultAndCleanUp(
-      action: SupportedActionType,
-      pageBulkExportJob: PageBulkExportJobDocument,
-      activityParameters?: ActivityParameters,
-  ): Promise<void> {
-    pageBulkExportJob.status = action === SupportedAction.ACTION_PAGE_BULK_EXPORT_COMPLETED
-      ? PageBulkExportJobStatus.completed : PageBulkExportJobStatus.failed;
-
-    try {
-      await pageBulkExportJob.save();
-      await this.notifyExportResult(pageBulkExportJob, action, activityParameters);
-    }
-    catch (err) {
-      logger.error(err);
-    }
-    // execute independently of notif process resolve/reject
-    await this.cleanUpExportJobResources(pageBulkExportJob);
-  }
-
-  /**
-   * Create a snapshot for each page that is to be exported in the pageBulkExportJob.
-   * Also calulate revisionListHash and save it to the pageBulkExportJob.
-   */
-  private async createPageSnapshots(user, pageBulkExportJob: PageBulkExportJobDocument): Promise<void> {
-    // if the process of creating snapshots was interrupted, delete the snapshots and create from the start
-    await PageBulkExportPageSnapshot.deleteMany({ pageBulkExportJob });
-
-    const basePage = await this.pageModel.findById(getIdForRef(pageBulkExportJob.page));
-    if (basePage == null) {
-      throw new Error('Base page not found');
-    }
-
-    const revisionListHash = createHash('sha256');
-
-    // create a Readable for pages to be exported
-    const { PageQueryBuilder } = this.pageModel;
-    const builder = await new PageQueryBuilder(this.pageModel.find())
-      .addConditionToListWithDescendants(basePage.path)
-      .addViewerCondition(user);
-    const pagesReadable = builder
-      .query
-      .lean()
-      .cursor({ batchSize: this.pageBatchSize });
-
-    // create a Writable that creates a snapshot for each page
-    const pageSnapshotsWritable = new Writable({
-      objectMode: true,
-      write: async(page: PageDocument, encoding, callback) => {
-        try {
-          if (page.revision != null) {
-            revisionListHash.update(getIdStringForRef(page.revision));
-          }
-          await PageBulkExportPageSnapshot.create({
-            pageBulkExportJob,
-            path: page.path,
-            revision: page.revision,
-          });
-        }
-        catch (err) {
-          callback(err);
-          return;
-        }
-        callback();
-      },
-    });
-
-    this.pageBulkExportJobManager.updateJobStream(pageBulkExportJob._id, pagesReadable);
-
-    await pipelinePromise(pagesReadable, pageSnapshotsWritable);
-
-    pageBulkExportJob.revisionListHash = revisionListHash.digest('hex');
-    await pageBulkExportJob.save();
-  }
-
-  /**
-   * Export pages to the file system before compressing and uploading to the cloud storage.
-   * The export will resume from the last exported page if the process was interrupted.
-   */
-  private async exportPagesToFS(pageBulkExportJob: PageBulkExportJobDocument): Promise<void> {
-    const findQuery = pageBulkExportJob.lastExportedPagePath != null ? {
-      pageBulkExportJob,
-      path: { $gt: pageBulkExportJob.lastExportedPagePath },
-    } : { pageBulkExportJob };
-    const pageSnapshotsReadable = PageBulkExportPageSnapshot
-      .find(findQuery)
-      .populate('revision').sort({ path: 1 }).lean()
-      .cursor({ batchSize: this.pageBatchSize });
-
-    const pagesWritable = this.getPageWritable(pageBulkExportJob);
-
-    this.pageBulkExportJobManager.updateJobStream(pageBulkExportJob._id, pageSnapshotsReadable);
-
-    return pipelinePromise(pageSnapshotsReadable, pagesWritable);
-  }
-
-  /**
-   * Get a Writable that writes the page body temporarily to fs
-   */
-  private getPageWritable(pageBulkExportJob: PageBulkExportJobDocument): Writable {
-    const outputDir = this.getTmpOutputDir(pageBulkExportJob);
-    return new Writable({
-      objectMode: true,
-      write: async(page: PageBulkExportPageSnapshotDocument, encoding, callback) => {
-        try {
-          const revision = page.revision;
-
-          if (revision != null && isPopulated(revision)) {
-            const markdownBody = revision.body;
-            const pathNormalized = `${normalizePath(page.path)}.${PageBulkExportFormat.md}`;
-            const fileOutputPath = path.join(outputDir, pathNormalized);
-            const fileOutputParentPath = getParentPath(fileOutputPath);
-
-            await fs.promises.mkdir(fileOutputParentPath, { recursive: true });
-            await fs.promises.writeFile(fileOutputPath, markdownBody);
-            pageBulkExportJob.lastExportedPagePath = page.path;
-            await pageBulkExportJob.save();
-          }
-        }
-        catch (err) {
-          callback(err);
-          return;
-        }
-        callback();
-      },
-    });
-  }
-
-  /**
-   * Execute a pipeline that reads the page files from the temporal fs directory, compresses them, and uploads to the cloud storage
-   */
-  private async compressAndUpload(user, pageBulkExportJob: PageBulkExportJobDocument): Promise<void> {
-    const pageArchiver = this.setUpPageArchiver();
-    const bufferToPartSizeTransform = getBufferToFixedSizeTransform(this.maxPartSize);
-
-    if (pageBulkExportJob.revisionListHash == null) throw new Error('revisionListHash is not set');
-    const originalName = `${pageBulkExportJob.revisionListHash}.${this.compressExtension}`;
-    const attachment = Attachment.createWithoutSave(null, user, originalName, this.compressExtension, 0, AttachmentType.PAGE_BULK_EXPORT);
-    const uploadKey = `${FilePathOnStoragePrefix.pageBulkExport}/${attachment.fileName}`;
-
-    const fileUploadService: FileUploader = this.crowi.fileUploadService;
-    // if the process of uploading was interrupted, delete and start from the start
-    if (pageBulkExportJob.uploadKey != null && pageBulkExportJob.uploadId != null) {
-      await fileUploadService.abortPreviousMultipartUpload(pageBulkExportJob.uploadKey, pageBulkExportJob.uploadId);
-    }
-
-    // init multipart upload
-    const multipartUploader: IMultipartUploader = fileUploadService.createMultipartUploader(uploadKey, this.maxPartSize);
-    await multipartUploader.initUpload();
-    pageBulkExportJob.uploadKey = uploadKey;
-    pageBulkExportJob.uploadId = multipartUploader.uploadId;
-    await pageBulkExportJob.save();
-
-    const multipartUploadWritable = this.getMultipartUploadWritable(multipartUploader, pageBulkExportJob, attachment);
-
-    const compressAndUploadPromise = pipelinePromise(pageArchiver, bufferToPartSizeTransform, multipartUploadWritable);
-    pageArchiver.directory(this.getTmpOutputDir(pageBulkExportJob), false);
-    pageArchiver.finalize();
-
-    await compressAndUploadPromise;
-  }
-
-  private setUpPageArchiver(): Archiver {
-    const pageArchiver = archiver('tar', {
-      gzip: true,
-    });
-
-    // good practice to catch warnings (ie stat failures and other non-blocking errors)
-    pageArchiver.on('warning', (err) => {
-      if (err.code === 'ENOENT') logger.error(err);
-      else throw err;
-    });
-
-    return pageArchiver;
-  }
-
-  private getMultipartUploadWritable(
-      multipartUploader: IMultipartUploader,
-      pageBulkExportJob: PageBulkExportJobDocument,
-      attachment: IAttachmentDocument,
-  ): Writable {
-    let partNumber = 1;
-
-    return new Writable({
-      write: async(part: Buffer, encoding, callback) => {
-        try {
-          await multipartUploader.uploadPart(part, partNumber);
-          partNumber += 1;
-          // First aid to prevent unexplained memory leaks
-          logger.info('global.gc() invoked.');
-          gc();
-        }
-        catch (err) {
-          await multipartUploader.abortUpload();
-          callback(err);
-          return;
-        }
-        callback();
-      },
-      final: async(callback) => {
-        try {
-          await multipartUploader.completeUpload();
-
-          const fileSize = await multipartUploader.getUploadedFileSize();
-          attachment.fileSize = fileSize;
-          await attachment.save();
-
-          pageBulkExportJob.completedAt = new Date();
-          pageBulkExportJob.attachment = attachment._id;
-          await pageBulkExportJob.save();
-        }
-        catch (err) {
-          callback(err);
-          return;
-        }
-        callback();
-      },
-    });
-  }
-
-  /**
-   * Get the output directory on the fs to temporarily store page files before compressing and uploading
-   */
-  private getTmpOutputDir(pageBulkExportJob: PageBulkExportJobDocument): string {
-    return `${this.tmpOutputRootDir}/${pageBulkExportJob._id}`;
-  }
-
-  async notifyExportResult(
-      pageBulkExportJob: PageBulkExportJobDocument, action: SupportedActionType, activityParameters?: ActivityParameters,
-  ) {
-    const activity = await this.crowi.activityService.createActivity({
-      ...activityParameters,
-      action,
-      targetModel: SupportedTargetModel.MODEL_PAGE_BULK_EXPORT_JOB,
-      target: pageBulkExportJob,
-      user: pageBulkExportJob.user,
-      snapshot: {
-        username: isPopulated(pageBulkExportJob.user) ? pageBulkExportJob.user.username : '',
-      },
-    });
-    const getAdditionalTargetUsers = async(activity: ActivityDocument) => [activity.user];
-    const preNotify = preNotifyService.generatePreNotify(activity, getAdditionalTargetUsers);
-    this.activityEvent.emit('updated', activity, pageBulkExportJob, preNotify);
-  }
-
-  /**
-   * Do the following in parallel:
-   * - delete page snapshots
-   * - remove the temporal output directory
-   * - abort multipart upload
-   */
-  async cleanUpExportJobResources(pageBulkExportJob: PageBulkExportJobDocument, restarted = false) {
-    this.pageBulkExportJobManager.removeJobInProgressAndQueueNextJob(pageBulkExportJob._id, restarted);
-
-    const promises = [
-      PageBulkExportPageSnapshot.deleteMany({ pageBulkExportJob }),
-      fs.promises.rm(this.getTmpOutputDir(pageBulkExportJob), { recursive: true, force: true }),
-    ];
-
-    const fileUploadService: FileUploader = this.crowi.fileUploadService;
-    if (pageBulkExportJob.uploadKey != null && pageBulkExportJob.uploadId != null) {
-      promises.push(fileUploadService.abortPreviousMultipartUpload(pageBulkExportJob.uploadKey, pageBulkExportJob.uploadId));
-    }
-
-    const results = await Promise.allSettled(promises);
-    results.forEach((result) => {
-      if (result.status === 'rejected') logger.error(result.reason);
-    });
-  }
-
-}
-
-// eslint-disable-next-line import/no-mutable-exports
-export let pageBulkExportService: PageBulkExportService | undefined; // singleton instance
-export default function instanciate(crowi): void {
-  pageBulkExportService = new PageBulkExportService(crowi);
-}

+ 0 - 231
apps/app/src/features/page-bulk-export/server/service/page-bulk-export/page-bulk-export-job-manager.spec.ts

@@ -1,231 +0,0 @@
-import { Readable } from 'stream';
-import { finished } from 'stream/promises';
-
-import type { HydratedDocument } from 'mongoose';
-
-import { configManager } from '~/server/service/config-manager';
-
-import type { PageBulkExportJobDocument } from '../../models/page-bulk-export-job';
-
-import { BulkExportJobExpiredError, BulkExportJobRestartedError } from './errors';
-import { PageBulkExportJobManager } from './page-bulk-export-job-manager';
-
-describe('PageBulkExportJobManager', () => {
-  let pageBulkExportServiceMock;
-  let jobManager: PageBulkExportJobManager;
-
-  beforeAll(() => {
-    vi.spyOn(configManager, 'getConfig').mockImplementation((namespace, key) => {
-      if (namespace === 'crowi' && key === 'app:pageBulkExportParallelExecLimit') {
-        return 3;
-      }
-      return undefined; // or whatever the default return value should be
-    });
-  });
-
-  beforeEach(() => {
-    pageBulkExportServiceMock = {
-      executePageBulkExportJob: vi.fn(),
-    };
-    jobManager = new PageBulkExportJobManager(pageBulkExportServiceMock);
-  });
-
-  describe('canExecuteNextJob', () => {
-    it('should return true if jobs in progress are less than the limit', () => {
-      // act, assert
-      expect(jobManager.canExecuteNextJob()).toBe(true);
-    });
-
-    it('should return false if jobs in progress exceed the limit', () => {
-      // arrange
-      jobManager.jobsInProgress = {
-        job1: { stream: undefined },
-        job2: { stream: undefined },
-        job3: { stream: undefined },
-      };
-
-      // act, assert
-      expect(jobManager.canExecuteNextJob()).toBe(false);
-    });
-  });
-
-  describe('getJobInProgress', () => {
-    it('should return the info of job in progress', () => {
-      // arrange
-      const jobId = 'job1';
-      jobManager.jobsInProgress[jobId] = { stream: undefined };
-
-      // act, assert
-      expect(jobManager.getJobInProgress(jobId)).toEqual({ stream: undefined });
-    });
-
-    it('should return undefined if job is not in progress', () => {
-      // arrange
-      const jobId = 'job1';
-
-      // act, assert
-      expect(jobManager.getJobInProgress(jobId)).toBeUndefined();
-    });
-  });
-
-  describe('addJob', () => {
-    it('should add the job to jobsInProgress if under the parallelExecLimit', () => {
-      // arrange
-      const job = { _id: 'job1' } as HydratedDocument<PageBulkExportJobDocument>;
-      expect(jobManager.jobQueue.length).toBe(0);
-
-      // act
-      jobManager.addJob(job, { endpoint: '/test/endpoint' });
-
-      // assert
-      expect(jobManager.jobQueue.length).toBe(0);
-      expect(jobManager.jobsInProgress[job._id.toString()]).toEqual({ stream: undefined });
-      expect(pageBulkExportServiceMock.executePageBulkExportJob).toHaveBeenCalledWith(job, { endpoint: '/test/endpoint' });
-    });
-
-    it('should queue the job if the parallelExecLimit is reached', () => {
-      // arrange
-      jobManager.jobsInProgress = {
-        job1: { stream: undefined },
-        job2: { stream: undefined },
-        job3: { stream: undefined },
-      };
-      const job = { _id: 'job2' } as HydratedDocument<PageBulkExportJobDocument>;
-      expect(jobManager.jobQueue.length).toBe(0);
-
-      // act
-      jobManager.addJob(job);
-
-      // assert
-      expect(jobManager.jobQueue.length).toBe(1);
-      expect(jobManager.jobQueue[0]).toEqual({ job });
-      expect(pageBulkExportServiceMock.executePageBulkExportJob).not.toHaveBeenCalled();
-    });
-  });
-
-  describe('updateJobStream', () => {
-    it('should set a new stream when there are no streams executing for the job', () => {
-      // arrange
-      const jobId = 'job1';
-      const mockStream = new Readable();
-      jobManager.jobsInProgress[jobId] = { stream: undefined };
-
-      // act
-      jobManager.updateJobStream(jobId, mockStream);
-
-      // assert
-      expect(jobManager.jobsInProgress[jobId].stream).toBe(mockStream);
-    });
-
-    it('should set a new stream when previous stream is finished', async() => {
-      // arrange
-      const jobId = 'job1';
-      const oldStream = new Readable({
-        read(size) {
-          // End the stream immediately
-          this.push(null);
-        },
-      });
-      oldStream.read();
-      await finished(oldStream);
-      const newStream = vi.fn().mockImplementation(() => {
-        const stream = new Readable();
-        stream.destroy = vi.fn();
-        return stream;
-      })() as unknown as Readable;
-      jobManager.addJob({ _id: jobId } as HydratedDocument<PageBulkExportJobDocument>);
-
-      // act
-      jobManager.updateJobStream(jobId, oldStream);
-
-      // assert
-      expect(oldStream.readableEnded).toBe(true);
-      jobManager.updateJobStream(jobId, newStream);
-      expect(jobManager.getJobInProgress(jobId)?.stream).toBe(newStream);
-    });
-
-    it('should destroy non-finished stream with an error before setting a new stream', () => {
-      // arrange
-      const jobId = 'job1';
-      const oldStream = vi.fn().mockImplementation(() => {
-        const stream = new Readable();
-        stream.destroy = vi.fn();
-        return stream;
-      })();
-      const newStream = new Readable();
-      const destroySpy = vi.spyOn(oldStream, 'destroy');
-      jobManager.addJob({ _id: jobId } as HydratedDocument<PageBulkExportJobDocument>);
-      jobManager.updateJobStream(jobId, oldStream);
-
-      // act
-      jobManager.updateJobStream(jobId, newStream);
-      expect(destroySpy).toHaveBeenCalledWith(expect.any(Error));
-
-      // assert
-      expect(jobManager.getJobInProgress(jobId)?.stream).toBe(newStream);
-    });
-
-    it('should destroy the new stream with BulkExportJobExpiredError if job is not in progress', () => {
-      // arrange
-      const jobId = 'job1';
-      const newStream = vi.fn().mockImplementation(() => {
-        const stream = new Readable();
-        stream.destroy = vi.fn();
-        return stream;
-      })();
-      const destroySpy = vi.spyOn(newStream, 'destroy');
-
-      // act
-      jobManager.updateJobStream(jobId, newStream);
-
-      // assert
-      expect(destroySpy).toHaveBeenCalledWith(expect.any(BulkExportJobExpiredError));
-    });
-  });
-
-  describe('removeJobInProgressAndQueueNextJob', () => {
-    it('should remove the job in progress and queue the next job', () => {
-      // arrange
-      const jobId = 'job1';
-      const mockStream = vi.fn().mockImplementation(() => {
-        const stream = new Readable();
-        stream.destroy = vi.fn();
-        return stream;
-      })();
-      vi.spyOn(mockStream, 'destroy');
-      const nextJob = { _id: 'job2' } as HydratedDocument<PageBulkExportJobDocument>;
-      jobManager.jobsInProgress[jobId] = { stream: mockStream };
-      jobManager.jobQueue.push({ job: nextJob });
-      expect(jobManager.jobQueue.length).toBe(1);
-
-      // act
-      jobManager.removeJobInProgressAndQueueNextJob(jobId);
-
-      // assert
-      expect(jobManager.jobQueue.length).toBe(0);
-      expect(mockStream.destroy).toHaveBeenCalledWith(expect.any(BulkExportJobExpiredError));
-      expect(jobManager.jobsInProgress[jobId]).toBeUndefined();
-      expect(jobManager.jobsInProgress[nextJob._id.toString()]).toEqual({ stream: undefined });
-      expect(pageBulkExportServiceMock.executePageBulkExportJob).toHaveBeenCalledWith(nextJob, undefined);
-    });
-
-    it('should destroy the stream with a BulkExportJobRestartedError if job was restarted', () => {
-      // arrange
-      const jobId = 'job1';
-      const mockStream = vi.fn().mockImplementation(() => {
-        const stream = new Readable();
-        stream.destroy = vi.fn();
-        return stream;
-      })();
-      vi.spyOn(mockStream, 'destroy');
-      jobManager.jobsInProgress[jobId] = { stream: mockStream };
-
-      // act
-      jobManager.removeJobInProgressAndQueueNextJob(jobId, true);
-
-      // assert
-      expect(mockStream.destroy).toHaveBeenCalledWith(expect.any(BulkExportJobRestartedError));
-      expect(jobManager.jobsInProgress[jobId]).toBeUndefined();
-    });
-  });
-});

+ 0 - 125
apps/app/src/features/page-bulk-export/server/service/page-bulk-export/page-bulk-export-job-manager.ts

@@ -1,125 +0,0 @@
-import type { Readable } from 'stream';
-
-import type { HydratedDocument } from 'mongoose';
-
-import type { ObjectIdLike } from '~/server/interfaces/mongoose-utils';
-import { configManager } from '~/server/service/config-manager';
-
-import type { PageBulkExportJobDocument } from '../../models/page-bulk-export-job';
-
-import { BulkExportJobExpiredError, BulkExportJobRestartedError } from './errors';
-
-import type { ActivityParameters, IPageBulkExportService } from '.';
-
-/**
- * Manage PageBulkExportJob execution.
- * - Keep track of jobs being executed and enable destroying the stream if the job is terminated
- * - Limit the number of jobs being executed in parallel
- * - Queue jobs to be executed in order
- */
-export class PageBulkExportJobManager {
-
-  pageBulkExportService: IPageBulkExportService;
-
-  private parallelExecLimit: number;
-
-  // contains jobs being executed and it's information
-  // the key is the _id of PageBulkExportJob and the value contains the stream of the job
-  jobsInProgress: {
-    [key: string]: { stream: Readable | undefined };
-  } = {};
-
-  // jobs waiting to be executed in order
-  jobQueue: { job: HydratedDocument<PageBulkExportJobDocument>, activityParameters?: ActivityParameters }[] = [];
-
-  constructor(pageBulkExportService: IPageBulkExportService) {
-    this.pageBulkExportService = pageBulkExportService;
-    this.parallelExecLimit = configManager.getConfig('crowi', 'app:pageBulkExportParallelExecLimit');
-  }
-
-  canExecuteNextJob(): boolean {
-    return Object.keys(this.jobsInProgress).length < this.parallelExecLimit;
-  }
-
-  /**
-   * Get the information of a job in progress.
-   * A getter method that includes "undefined" in the return type
-   */
-  getJobInProgress(jobId: ObjectIdLike): { stream: Readable | undefined } | undefined {
-    return this.jobsInProgress[jobId.toString()];
-  }
-
-  /**
-   * Add a job to the queue or execute it if the number of jobs in progress is less than the limit
-   * @param job job to add or execute
-   * @param activityParameters parameters to record user activity
-   */
-  addJob(job: HydratedDocument<PageBulkExportJobDocument>, activityParameters?: ActivityParameters): void {
-    if (this.canExecuteNextJob()) {
-      this.jobsInProgress[job._id.toString()] = { stream: undefined };
-      this.pageBulkExportService.executePageBulkExportJob(job, activityParameters);
-    }
-    else {
-      this.jobQueue.push({ job, activityParameters });
-    }
-  }
-
-  /**
-   * Update the info of which stream is being executed for a job
-   * @param jobId id of job to update
-   * @param stream the new stream being executed for the job
-   */
-  updateJobStream(jobId: ObjectIdLike, stream: Readable): void {
-    const jobInProgress = this.getJobInProgress(jobId);
-    if (jobInProgress != null) {
-      if (jobInProgress.stream != null && !jobInProgress.stream.readableEnded) {
-        jobInProgress.stream.destroy(new Error('Stream not finished before next stream started'));
-      }
-      jobInProgress.stream = stream;
-    }
-    else {
-      // job was terminated beforehand, so destroy the stream
-      stream.destroy(new BulkExportJobExpiredError());
-    }
-  }
-
-  /**
-   * Remove a job in execution and queue the next job if there are any
-   * @param jobId id of job to remove
-   * @param isJobRestarted whether or not the job was restarted
-   */
-  removeJobInProgressAndQueueNextJob(jobId: ObjectIdLike, isJobRestarted = false): void {
-    this.removeJobInProgress(jobId, isJobRestarted);
-
-    if (this.jobQueue.length > 0) {
-      while (this.canExecuteNextJob() && this.jobQueue.length > 0) {
-        const nextJob = this.jobQueue.shift();
-        if (nextJob != null) {
-          this.jobsInProgress[nextJob.job._id.toString()] = { stream: undefined };
-          this.pageBulkExportService.executePageBulkExportJob(nextJob.job, nextJob.activityParameters);
-        }
-      }
-    }
-  }
-
-  /**
-   * Remove a job in execution and destroy it's stream process
-   * @param jobId id of job to remove
-   * @param isJobRestarted whether or not the job was restarted
-   */
-  private removeJobInProgress(jobId: ObjectIdLike, isJobRestarted = false): void {
-    const jobInProgress = this.getJobInProgress(jobId);
-    if (jobInProgress == null) return;
-
-    if (jobInProgress.stream != null) {
-      if (isJobRestarted) {
-        jobInProgress.stream.destroy(new BulkExportJobRestartedError());
-      }
-      else {
-        jobInProgress.stream.destroy(new BulkExportJobExpiredError());
-      }
-    }
-    delete this.jobsInProgress[jobId.toString()];
-  }
-
-}

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

@@ -13,10 +13,7 @@ import pkg from '^/package.json';
 import { KeycloakUserGroupSyncService } from '~/features/external-user-group/server/service/keycloak-user-group-sync';
 import { LdapUserGroupSyncService } from '~/features/external-user-group/server/service/ldap-user-group-sync';
 import { startCronIfEnabled as startOpenaiCronIfEnabled } from '~/features/openai/server/services/cron';
-import { PageBulkExportJobInProgressStatus } from '~/features/page-bulk-export/interfaces/page-bulk-export';
-import PageBulkExportJob from '~/features/page-bulk-export/server/models/page-bulk-export-job';
 import { checkPageBulkExportJobInProgressCronService } from '~/features/page-bulk-export/server/service/check-page-bulk-export-job-in-progress-cron';
-import instanciatePageBulkExportService, { pageBulkExportService } from '~/features/page-bulk-export/server/service/page-bulk-export';
 import instanciatePageBulkExportJobCleanUpCronService, {
   pageBulkExportJobCleanUpCronService,
 } from '~/features/page-bulk-export/server/service/page-bulk-export-job-clean-up-cron';
@@ -177,7 +174,6 @@ Crowi.prototype.init = async function() {
     this.setupUserGroupService(),
     this.setupExport(),
     this.setupImport(),
-    this.setupPageBulkExportService(),
     this.setupGrowiPluginService(),
     this.setupPageService(),
     this.setupInAppNotificationService(),
@@ -198,8 +194,6 @@ Crowi.prototype.init = async function() {
   ]);
 
   await normalizeData();
-
-  this.resumeIncompletePageBulkExportJobs();
 };
 
 /**
@@ -690,10 +684,6 @@ Crowi.prototype.setupExport = async function() {
   instanciateExportService(this);
 };
 
-Crowi.prototype.setupPageBulkExportService = async function() {
-  instanciatePageBulkExportService(this);
-};
-
 Crowi.prototype.setupImport = async function() {
   initializeImportService(this);
 };
@@ -786,11 +776,4 @@ Crowi.prototype.setupExternalUserGroupSyncService = function() {
   this.keycloakUserGroupSyncService = new KeycloakUserGroupSyncService(this.s2sMessagingService, this.socketIoService);
 };
 
-Crowi.prototype.resumeIncompletePageBulkExportJobs = async function() {
-  const jobs = await PageBulkExportJob.find({
-    $or: Object.values(PageBulkExportJobInProgressStatus).map(status => ({ status })),
-  });
-  jobs.forEach(job => pageBulkExportService?.pageBulkExportJobManager?.addJob(job));
-};
-
 export default Crowi;

+ 0 - 1
apps/slackbot-proxy/package.json

@@ -76,7 +76,6 @@
     "@types/bunyan": "^1.8.11",
     "bootstrap": "=5.3.2",
     "browser-bunyan": "^1.6.3",
-    "eslint-plugin-regex": "^1.8.0",
     "morgan": "^1.10.0"
   }
 }

+ 1 - 0
package.json

@@ -72,6 +72,7 @@
     "eslint-plugin-react-hooks": "^4.6.0",
     "eslint-plugin-rulesdir": "^0.2.2",
     "eslint-plugin-vitest": "^0.2.3",
+    "eslint-plugin-regex": "^1.8.0",
     "glob": "^8.1.0",
     "mock-require": "^3.0.3",
     "nodemon": "^3.1.3",

+ 0 - 1
packages/core/package.json

@@ -73,7 +73,6 @@
     "escape-string-regexp": "^4.0.0"
   },
   "devDependencies": {
-    "eslint-plugin-regex": "^1.8.0",
     "mongoose": "^6.11.3",
     "socket.io-client": "^4.7.5",
     "swr": "^2.2.2"

+ 0 - 1
packages/presentation/package.json

@@ -47,7 +47,6 @@
     "@types/mdast": "^4.0.4",
     "@types/reveal.js": "^4.4.1",
     "@types/unist": "^3.0.3",
-    "eslint-plugin-regex": "^1.8.0",
     "hast-util-sanitize": "^5.0.1",
     "hast-util-select": "^6.0.2",
     "mdast-util-frontmatter": "^2.0.1",

+ 0 - 1
packages/remark-attachment-refs/package.json

@@ -60,7 +60,6 @@
     "@types/bunyan": "^1.8.11",
     "@types/hast": "^3.0.4",
     "csstype": "^3.0.2",
-    "eslint-plugin-regex": "^1.8.0",
     "hast-util-sanitize": "^5.0.1",
     "hast-util-select": "^6.0.2",
     "npm-run-all": "^4.1.5",

+ 0 - 1
packages/remark-drawio/package.json

@@ -35,7 +35,6 @@
     "@types/mdast": "^4.0.4",
     "@types/react": "^18.2.14",
     "@types/react-dom": "^18.2.6",
-    "eslint-plugin-regex": "^1.8.0",
     "hast-util-sanitize": "^5.0.1",
     "pako": "^2.1.0",
     "throttle-debounce": "^5.0.0",

+ 0 - 1
packages/remark-lsx/package.json

@@ -49,7 +49,6 @@
     "@types/hast": "^3.0.4",
     "axios": "^0.24.0",
     "is-absolute-url": "^4.0.1",
-    "eslint-plugin-regex": "^1.8.0",
     "hast-util-sanitize": "^5.0.1",
     "hast-util-select": "^6.0.2",
     "unified": "^11.0.0",

+ 1 - 2
packages/slack/package.json

@@ -68,7 +68,6 @@
   "devDependencies": {
     "@slack/types": "^2.14.0",
     "@types/express": "^4",
-    "@types/qs": "^6.9.16",
-    "eslint-plugin-regex": "^1.8.0"
+    "@types/qs": "^6.9.16"
   }
 }

+ 193 - 101
pnpm-lock.yaml

@@ -26,7 +26,7 @@ importers:
         version: 1.46.0
       '@swc-node/register':
         specifier: ^1.9.1
-        version: 1.10.0(@swc/core@1.5.25(@swc/helpers@0.5.11))(@swc/types@0.1.7)(typescript@5.0.4)
+        version: 1.10.0(@swc/core@1.5.25(@swc/helpers@0.5.11))(@swc/types@0.1.12)(typescript@5.0.4)
       '@swc/core':
         specifier: ^1.5.25
         version: 1.5.25(@swc/helpers@0.5.11)
@@ -93,6 +93,9 @@ importers:
       eslint-plugin-react-hooks:
         specifier: ^4.6.0
         version: 4.6.0(eslint@8.41.0)
+      eslint-plugin-regex:
+        specifier: ^1.8.0
+        version: 1.10.0(eslint@8.41.0)
       eslint-plugin-rulesdir:
         specifier: ^0.2.2
         version: 0.2.2
@@ -351,9 +354,6 @@ importers:
       escape-string-regexp:
         specifier: ^4.0.0
         version: 4.0.0
-      eslint-plugin-regex:
-        specifier: ^1.8.0
-        version: 1.10.0(eslint@8.41.0)
       expose-gc:
         specifier: ^1.0.0
         version: 1.0.0
@@ -490,8 +490,8 @@ importers:
         specifier: ^15.3.1
         version: 15.3.1(i18next@23.16.5)(next@14.2.13(@babel/core@7.24.6)(@playwright/test@1.46.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.77.6))(react-i18next@15.1.1(i18next@23.16.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react@18.2.0)
       next-superjson:
-        specifier: ^0.0.4
-        version: 0.0.4(next@14.2.13(@babel/core@7.24.6)(@playwright/test@1.46.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.77.6))(superjson@1.13.3)(webpack@5.92.1(@swc/core@1.5.25(@swc/helpers@0.5.11)))
+        specifier: ^1.0.7
+        version: 1.0.7(@swc/helpers@0.5.11)(next@14.2.13(@babel/core@7.24.6)(@playwright/test@1.46.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.77.6))(superjson@1.13.3)
       next-themes:
         specifier: ^0.2.1
         version: 0.2.1(next@14.2.13(@babel/core@7.24.6)(@playwright/test@1.46.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.77.6))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -738,7 +738,7 @@ importers:
         version: 2.11.8
       '@swc-node/jest':
         specifier: ^1.8.1
-        version: 1.8.3(@swc/core@1.5.25(@swc/helpers@0.5.11))(@swc/types@0.1.7)(typescript@5.4.2)
+        version: 1.8.3(@swc/core@1.5.25(@swc/helpers@0.5.11))(@swc/types@0.1.12)(typescript@5.4.2)
       '@swc/jest':
         specifier: ^0.2.36
         version: 0.2.36(@swc/core@1.5.25(@swc/helpers@0.5.11))
@@ -1043,9 +1043,6 @@ importers:
       bootstrap:
         specifier: '=5.3.2'
         version: 5.3.2(@popperjs/core@2.11.8)
-      eslint-plugin-regex:
-        specifier: ^1.8.0
-        version: 1.10.0(eslint@8.41.0)
       morgan:
         specifier: ^1.10.0
         version: 1.10.0
@@ -1059,9 +1056,6 @@ importers:
         specifier: ^4.0.0
         version: 4.0.0
     devDependencies:
-      eslint-plugin-regex:
-        specifier: ^1.8.0
-        version: 1.10.0(eslint@8.41.0)
       mongoose:
         specifier: ^6.11.3
         version: 6.13.0(@aws-sdk/client-sso-oidc@3.600.0)
@@ -1272,9 +1266,6 @@ importers:
       '@types/unist':
         specifier: ^3.0.3
         version: 3.0.3
-      eslint-plugin-regex:
-        specifier: ^1.8.0
-        version: 1.10.0(eslint@8.41.0)
       hast-util-sanitize:
         specifier: ^5.0.1
         version: 5.0.1
@@ -1397,9 +1388,6 @@ importers:
       csstype:
         specifier: ^3.0.2
         version: 3.1.3
-      eslint-plugin-regex:
-        specifier: ^1.8.0
-        version: 1.10.0(eslint@8.41.0)
       hast-util-sanitize:
         specifier: ^5.0.1
         version: 5.0.1
@@ -1434,9 +1422,6 @@ importers:
       '@types/react-dom':
         specifier: ^18.2.6
         version: 18.3.0
-      eslint-plugin-regex:
-        specifier: ^1.8.0
-        version: 1.10.0(eslint@8.41.0)
       hast-util-sanitize:
         specifier: ^5.0.1
         version: 5.0.1
@@ -1577,9 +1562,6 @@ importers:
       axios:
         specifier: ^0.24.0
         version: 0.24.0
-      eslint-plugin-regex:
-        specifier: ^1.8.0
-        version: 1.10.0(eslint@8.41.0)
       hast-util-sanitize:
         specifier: ^5.0.1
         version: 5.0.1
@@ -1653,9 +1635,6 @@ importers:
       '@types/qs':
         specifier: ^6.9.16
         version: 6.9.17
-      eslint-plugin-regex:
-        specifier: ^1.8.0
-        version: 1.10.0(eslint@8.41.0)
 
   packages/ui:
     dependencies:
@@ -3770,66 +3749,135 @@ packages:
   '@swc-node/sourcemap-support@0.5.0':
     resolution: {integrity: sha512-fbhjL5G0YvFoWwNhWleuBUfotiX+USiA9oJqu9STFw+Hb0Cgnddn+HVS/K5fI45mn92e8V+cHD2jgFjk4w2T9Q==}
 
+  '@swc/core-darwin-arm64@1.4.17':
+    resolution: {integrity: sha512-HVl+W4LezoqHBAYg2JCqR+s9ife9yPfgWSj37iIawLWzOmuuJ7jVdIB7Ee2B75bEisSEKyxRlTl6Y1Oq3owBgw==}
+    engines: {node: '>=10'}
+    cpu: [arm64]
+    os: [darwin]
+
   '@swc/core-darwin-arm64@1.5.25':
     resolution: {integrity: sha512-YbD0SBgVJS2DM0vwJTU5m7+wOyCjHPBDMf3nCBJQzFZzOLzK11eRW7SzU2jhJHr9HI9sKcNFfN4lIC2Sj+4inA==}
     engines: {node: '>=10'}
     cpu: [arm64]
     os: [darwin]
 
+  '@swc/core-darwin-x64@1.4.17':
+    resolution: {integrity: sha512-WYRO9Fdzq4S/he8zjW5I95G1zcvyd9yyD3Tgi4/ic84P5XDlSMpBDpBLbr/dCPjmSg7aUXxNQqKqGkl6dQxYlA==}
+    engines: {node: '>=10'}
+    cpu: [x64]
+    os: [darwin]
+
   '@swc/core-darwin-x64@1.5.25':
     resolution: {integrity: sha512-OhP4TROT6gQuozn+ah0Y4UidSdgDmxwtQq3lgCUIAxJYErJAQ82/Y0kve2UaNmkSGjOHU+/b4siHPrYTkXOk0Q==}
     engines: {node: '>=10'}
     cpu: [x64]
     os: [darwin]
 
+  '@swc/core-linux-arm-gnueabihf@1.4.17':
+    resolution: {integrity: sha512-cgbvpWOvtMH0XFjvwppUCR+Y+nf6QPaGu6AQ5hqCP+5Lv2zO5PG0RfasC4zBIjF53xgwEaaWmGP5/361P30X8Q==}
+    engines: {node: '>=10'}
+    cpu: [arm]
+    os: [linux]
+
   '@swc/core-linux-arm-gnueabihf@1.5.25':
     resolution: {integrity: sha512-tNmUfrAHxN2gvYPyYNnHx2CYlPO7DGAUuK/bZrqawu++djcg+atAV3eI3XYJgmHId7/sYAlDQ9wjkrOLofFjVg==}
     engines: {node: '>=10'}
     cpu: [arm]
     os: [linux]
 
+  '@swc/core-linux-arm64-gnu@1.4.17':
+    resolution: {integrity: sha512-l7zHgaIY24cF9dyQ/FOWbmZDsEj2a9gRFbmgx2u19e3FzOPuOnaopFj0fRYXXKCmtdx+anD750iBIYnTR+pq/Q==}
+    engines: {node: '>=10'}
+    cpu: [arm64]
+    os: [linux]
+
   '@swc/core-linux-arm64-gnu@1.5.25':
     resolution: {integrity: sha512-stzpke+bRaNFM/HrZPRjX0aQZ86S/2DChVCwb8NAV1n5lu9mz1CS750y7WbbtX/KZjk92FsCeRy2qwkvjI0gWw==}
     engines: {node: '>=10'}
     cpu: [arm64]
     os: [linux]
 
+  '@swc/core-linux-arm64-musl@1.4.17':
+    resolution: {integrity: sha512-qhH4gr9gAlVk8MBtzXbzTP3BJyqbAfUOATGkyUtohh85fPXQYuzVlbExix3FZXTwFHNidGHY8C+ocscI7uDaYw==}
+    engines: {node: '>=10'}
+    cpu: [arm64]
+    os: [linux]
+
   '@swc/core-linux-arm64-musl@1.5.25':
     resolution: {integrity: sha512-UckUfDYedish/bj2V1jgQDGgouLhyRpG7jgF3mp8jHir11V2K6JiTyjFoz99eOiclS3+hNdr4QLJ+ifrQMJNZw==}
     engines: {node: '>=10'}
     cpu: [arm64]
     os: [linux]
 
+  '@swc/core-linux-x64-gnu@1.4.17':
+    resolution: {integrity: sha512-vRDFATL1oN5oZMImkwbgSHEkp8xG1ofEASBypze01W1Tqto8t+yo6gsp69wzCZBlxldsvPpvFZW55Jq0Rn+UnA==}
+    engines: {node: '>=10'}
+    cpu: [x64]
+    os: [linux]
+
   '@swc/core-linux-x64-gnu@1.5.25':
     resolution: {integrity: sha512-LwbJEgNT3lXbvz4WFzVNXNvs8DvxpoXjMZk9K9Hig8tmZQJKHC2qZTGomcyK5EFzfj2HBuBXZnAEW8ZT9PcEaA==}
     engines: {node: '>=10'}
     cpu: [x64]
     os: [linux]
 
+  '@swc/core-linux-x64-musl@1.4.17':
+    resolution: {integrity: sha512-zQNPXAXn3nmPqv54JVEN8k2JMEcMTQ6veVuU0p5O+A7KscJq+AGle/7ZQXzpXSfUCXlLMX4wvd+rwfGhh3J4cw==}
+    engines: {node: '>=10'}
+    cpu: [x64]
+    os: [linux]
+
   '@swc/core-linux-x64-musl@1.5.25':
     resolution: {integrity: sha512-rsepMTgml0EkswWkBpg3Wrjj5eqjwTzZN5omAn1klzXSZnClTrfeHvBuoIJYVr1yx+jmBkqySgME2p7+magUAw==}
     engines: {node: '>=10'}
     cpu: [x64]
     os: [linux]
 
+  '@swc/core-win32-arm64-msvc@1.4.17':
+    resolution: {integrity: sha512-z86n7EhOwyzxwm+DLE5NoLkxCTme2lq7QZlDjbQyfCxOt6isWz8rkW5QowTX8w9Rdmk34ncrjSLvnHOeLY17+w==}
+    engines: {node: '>=10'}
+    cpu: [arm64]
+    os: [win32]
+
   '@swc/core-win32-arm64-msvc@1.5.25':
     resolution: {integrity: sha512-DJDsLBsRBV3uQBShRK2x6fqzABp9RLNVxDUpTTvUjc7qywJ8vS/yn+POK/zCyVEqLagf1z/8D5CEQ+RAIJq1NA==}
     engines: {node: '>=10'}
     cpu: [arm64]
     os: [win32]
 
+  '@swc/core-win32-ia32-msvc@1.4.17':
+    resolution: {integrity: sha512-JBwuSTJIgiJJX6wtr4wmXbfvOswHFj223AumUrK544QV69k60FJ9q2adPW9Csk+a8wm1hLxq4HKa2K334UHJ/g==}
+    engines: {node: '>=10'}
+    cpu: [ia32]
+    os: [win32]
+
   '@swc/core-win32-ia32-msvc@1.5.25':
     resolution: {integrity: sha512-BARL1ulHol53MEKC1ZVWM3A3FP757UUgG5Q8v97za+4a1SaIgbwvAQyHDxMYWi9+ij+OapK8YnWjJcFa17g8dw==}
     engines: {node: '>=10'}
     cpu: [ia32]
     os: [win32]
 
+  '@swc/core-win32-x64-msvc@1.4.17':
+    resolution: {integrity: sha512-jFkOnGQamtVDBm3MF5Kq1lgW8vx4Rm1UvJWRUfg+0gx7Uc3Jp3QMFeMNw/rDNQYRDYPG3yunCC+2463ycd5+dg==}
+    engines: {node: '>=10'}
+    cpu: [x64]
+    os: [win32]
+
   '@swc/core-win32-x64-msvc@1.5.25':
     resolution: {integrity: sha512-o+MHUWrQI9iR6EusEV8eNU2Ezi3KtlhUR4gfptQN5MbVzlgjTvQbhiKpE1GYOxp+0BLBbKRwITKOcdhxfEJ2Uw==}
     engines: {node: '>=10'}
     cpu: [x64]
     os: [win32]
 
+  '@swc/core@1.4.17':
+    resolution: {integrity: sha512-tq+mdWvodMBNBBZbwFIMTVGYHe9N7zvEaycVVjfvAx20k1XozHbHhRv+9pEVFJjwRxLdXmtvFZd3QZHRAOpoNQ==}
+    engines: {node: '>=10'}
+    peerDependencies:
+      '@swc/helpers': ^0.5.0
+    peerDependenciesMeta:
+      '@swc/helpers':
+        optional: true
+
   '@swc/core@1.5.25':
     resolution: {integrity: sha512-qdGEIdLVoTjEQ7w72UyyQ0wLFY4XbHfZiidmPHKJQsvSXzdpHXxPdlTCea/mY4AhMqo/M+pvkJSXJAxZnFl7qw==}
     engines: {node: '>=10'}
@@ -3854,6 +3902,9 @@ packages:
     peerDependencies:
       '@swc/core': '*'
 
+  '@swc/types@0.1.12':
+    resolution: {integrity: sha512-wBJA+SdtkbFhHjTMYH+dEH1y4VpfGdAc2Kw/LK09i9bXd/K6j6PkDcFCEzb6iVfZMkPRrl/q0e3toqTAJdkIVA==}
+
   '@swc/types@0.1.7':
     resolution: {integrity: sha512-scHWahbHF0eyj3JsxG9CFJgFdFNaVQCNAimBlT6PzS3n/HptxqREjsm4OH6AN3lYcffZYSPxXW8ua2BEHp0lJQ==}
 
@@ -4818,13 +4869,6 @@ packages:
     resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==}
     engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
 
-  babel-plugin-superjson-next@0.4.5:
-    resolution: {integrity: sha512-k7S99Qpsbi3OSdlCMXEiklzxepM6QbYEIUsrjgSkpx+ksT0iNfdY2r1kCzBK2UjG8fLN6NZEKpDA8XpG2pbDSA==}
-    engines: {node: '>=10'}
-    peerDependencies:
-      next: '>=9.0.0'
-      superjson: 1.x
-
   babel-preset-current-node-syntax@1.0.1:
     resolution: {integrity: sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==}
     peerDependencies:
@@ -9089,8 +9133,14 @@ packages:
       react: '>= 17.0.2'
       react-i18next: '>= 13.5.0'
 
-  next-superjson@0.0.4:
-    resolution: {integrity: sha512-PYtoHbPcZYED8Vm9YCIQIZi/arANNnf6grwjkPuJXzWdY1TxJxrn9dCPmVj6ALvPn9YcDThwEA9WvHq/NyzMvw==}
+  next-superjson-plugin@0.6.3:
+    resolution: {integrity: sha512-gipGROzbbn1Koq84AZQodIvBdORp9dytIDv07SguwXdxnJb6v05KCmHVNU9L6AWqxjP14qNIWCNdKRDhnGRZrg==}
+    peerDependencies:
+      next: ^13.0 || ^14.0
+      superjson: ^1 || ^2
+
+  next-superjson@1.0.7:
+    resolution: {integrity: sha512-07zs+A+oyCmpJm4qwo5M8pjnBIrYgnd2eox77wafB1shdCSxbaHNwejCKMK2e2sPtJ1u+iNPG1bG4mO6xwOz6g==}
     peerDependencies:
       next: '>=10'
 
@@ -13404,7 +13454,7 @@ snapshots:
       '@babel/traverse': 7.24.6
       '@babel/types': 7.25.6
       convert-source-map: 2.0.0
-      debug: 4.3.7
+      debug: 4.3.7(supports-color@5.5.0)
       gensync: 1.0.0-beta.2
       json5: 2.2.3
       semver: 6.3.1
@@ -13595,7 +13645,7 @@ snapshots:
       '@babel/helper-split-export-declaration': 7.24.6
       '@babel/parser': 7.25.6
       '@babel/types': 7.25.6
-      debug: 4.3.7
+      debug: 4.3.7(supports-color@5.5.0)
       globals: 11.12.0
     transitivePeerDependencies:
       - supports-color
@@ -14100,7 +14150,7 @@ snapshots:
 
   '@elastic/elasticsearch@7.17.13':
     dependencies:
-      debug: 4.3.7
+      debug: 4.3.7(supports-color@5.5.0)
       hpagent: 0.1.2
       ms: 2.1.3
       secure-json-parse: 2.7.0
@@ -14116,7 +14166,7 @@ snapshots:
 
   '@elastic/transport@8.6.1':
     dependencies:
-      debug: 4.3.7
+      debug: 4.3.7(supports-color@5.5.0)
       hpagent: 1.2.0
       ms: 2.1.3
       secure-json-parse: 2.7.0
@@ -14227,7 +14277,7 @@ snapshots:
   '@eslint/eslintrc@2.0.3':
     dependencies:
       ajv: 6.12.6
-      debug: 4.3.7
+      debug: 4.3.7(supports-color@5.5.0)
       espree: 9.6.1
       globals: 13.24.0
       ignore: 5.3.1
@@ -14300,7 +14350,7 @@ snapshots:
   '@humanwhocodes/config-array@0.11.8':
     dependencies:
       '@humanwhocodes/object-schema': 1.2.1
-      debug: 4.3.7
+      debug: 4.3.7(supports-color@5.5.0)
       minimatch: 3.1.2
     transitivePeerDependencies:
       - supports-color
@@ -14316,7 +14366,7 @@ snapshots:
       '@antfu/install-pkg': 0.4.1
       '@antfu/utils': 0.7.10
       '@iconify/types': 2.0.0
-      debug: 4.3.7
+      debug: 4.3.7(supports-color@5.5.0)
       kolorist: 1.8.0
       local-pkg: 0.5.0
       mlly: 1.7.1
@@ -15739,25 +15789,25 @@ snapshots:
 
   '@sqltools/formatter@1.2.5': {}
 
-  '@swc-node/core@1.13.1(@swc/core@1.5.25(@swc/helpers@0.5.11))(@swc/types@0.1.7)':
+  '@swc-node/core@1.13.1(@swc/core@1.5.25(@swc/helpers@0.5.11))(@swc/types@0.1.12)':
     dependencies:
       '@swc/core': 1.5.25(@swc/helpers@0.5.11)
-      '@swc/types': 0.1.7
+      '@swc/types': 0.1.12
 
-  '@swc-node/jest@1.8.3(@swc/core@1.5.25(@swc/helpers@0.5.11))(@swc/types@0.1.7)(typescript@5.4.2)':
+  '@swc-node/jest@1.8.3(@swc/core@1.5.25(@swc/helpers@0.5.11))(@swc/types@0.1.12)(typescript@5.4.2)':
     dependencies:
       '@node-rs/xxhash': 1.7.3
-      '@swc-node/core': 1.13.1(@swc/core@1.5.25(@swc/helpers@0.5.11))(@swc/types@0.1.7)
-      '@swc-node/register': 1.10.0(@swc/core@1.5.25(@swc/helpers@0.5.11))(@swc/types@0.1.7)(typescript@5.4.2)
+      '@swc-node/core': 1.13.1(@swc/core@1.5.25(@swc/helpers@0.5.11))(@swc/types@0.1.12)
+      '@swc-node/register': 1.10.0(@swc/core@1.5.25(@swc/helpers@0.5.11))(@swc/types@0.1.12)(typescript@5.4.2)
       '@swc/core': 1.5.25(@swc/helpers@0.5.11)
-      '@swc/types': 0.1.7
+      '@swc/types': 0.1.12
       typescript: 5.4.2
     transitivePeerDependencies:
       - supports-color
 
-  '@swc-node/register@1.10.0(@swc/core@1.5.25(@swc/helpers@0.5.11))(@swc/types@0.1.7)(typescript@5.0.4)':
+  '@swc-node/register@1.10.0(@swc/core@1.5.25(@swc/helpers@0.5.11))(@swc/types@0.1.12)(typescript@5.0.4)':
     dependencies:
-      '@swc-node/core': 1.13.1(@swc/core@1.5.25(@swc/helpers@0.5.11))(@swc/types@0.1.7)
+      '@swc-node/core': 1.13.1(@swc/core@1.5.25(@swc/helpers@0.5.11))(@swc/types@0.1.12)
       '@swc-node/sourcemap-support': 0.5.0
       '@swc/core': 1.5.25(@swc/helpers@0.5.11)
       colorette: 2.0.20
@@ -15769,13 +15819,13 @@ snapshots:
       - '@swc/types'
       - supports-color
 
-  '@swc-node/register@1.10.0(@swc/core@1.5.25(@swc/helpers@0.5.11))(@swc/types@0.1.7)(typescript@5.4.2)':
+  '@swc-node/register@1.10.0(@swc/core@1.5.25(@swc/helpers@0.5.11))(@swc/types@0.1.12)(typescript@5.4.2)':
     dependencies:
-      '@swc-node/core': 1.13.1(@swc/core@1.5.25(@swc/helpers@0.5.11))(@swc/types@0.1.7)
+      '@swc-node/core': 1.13.1(@swc/core@1.5.25(@swc/helpers@0.5.11))(@swc/types@0.1.12)
       '@swc-node/sourcemap-support': 0.5.0
       '@swc/core': 1.5.25(@swc/helpers@0.5.11)
       colorette: 2.0.20
-      debug: 4.3.7
+      debug: 4.3.7(supports-color@5.5.0)
       pirates: 4.0.6
       tslib: 2.8.0
       typescript: 5.4.2
@@ -15788,36 +15838,83 @@ snapshots:
       source-map-support: 0.5.21
       tslib: 2.8.0
 
+  '@swc/core-darwin-arm64@1.4.17':
+    optional: true
+
   '@swc/core-darwin-arm64@1.5.25':
     optional: true
 
+  '@swc/core-darwin-x64@1.4.17':
+    optional: true
+
   '@swc/core-darwin-x64@1.5.25':
     optional: true
 
+  '@swc/core-linux-arm-gnueabihf@1.4.17':
+    optional: true
+
   '@swc/core-linux-arm-gnueabihf@1.5.25':
     optional: true
 
+  '@swc/core-linux-arm64-gnu@1.4.17':
+    optional: true
+
   '@swc/core-linux-arm64-gnu@1.5.25':
     optional: true
 
+  '@swc/core-linux-arm64-musl@1.4.17':
+    optional: true
+
   '@swc/core-linux-arm64-musl@1.5.25':
     optional: true
 
+  '@swc/core-linux-x64-gnu@1.4.17':
+    optional: true
+
   '@swc/core-linux-x64-gnu@1.5.25':
     optional: true
 
+  '@swc/core-linux-x64-musl@1.4.17':
+    optional: true
+
   '@swc/core-linux-x64-musl@1.5.25':
     optional: true
 
+  '@swc/core-win32-arm64-msvc@1.4.17':
+    optional: true
+
   '@swc/core-win32-arm64-msvc@1.5.25':
     optional: true
 
+  '@swc/core-win32-ia32-msvc@1.4.17':
+    optional: true
+
   '@swc/core-win32-ia32-msvc@1.5.25':
     optional: true
 
+  '@swc/core-win32-x64-msvc@1.4.17':
+    optional: true
+
   '@swc/core-win32-x64-msvc@1.5.25':
     optional: true
 
+  '@swc/core@1.4.17(@swc/helpers@0.5.11)':
+    dependencies:
+      '@swc/counter': 0.1.3
+      '@swc/types': 0.1.12
+    optionalDependencies:
+      '@swc/core-darwin-arm64': 1.4.17
+      '@swc/core-darwin-x64': 1.4.17
+      '@swc/core-linux-arm-gnueabihf': 1.4.17
+      '@swc/core-linux-arm64-gnu': 1.4.17
+      '@swc/core-linux-arm64-musl': 1.4.17
+      '@swc/core-linux-x64-gnu': 1.4.17
+      '@swc/core-linux-x64-musl': 1.4.17
+      '@swc/core-win32-arm64-msvc': 1.4.17
+      '@swc/core-win32-ia32-msvc': 1.4.17
+      '@swc/core-win32-x64-msvc': 1.4.17
+      '@swc/helpers': 0.5.11
+
   '@swc/core@1.5.25(@swc/helpers@0.5.11)':
     dependencies:
       '@swc/counter': 0.1.3
@@ -15853,6 +15950,10 @@ snapshots:
       '@swc/counter': 0.1.3
       jsonc-parser: 3.2.0
 
+  '@swc/types@0.1.12':
+    dependencies:
+      '@swc/counter': 0.1.3
+
   '@swc/types@0.1.7':
     dependencies:
       '@swc/counter': 0.1.3
@@ -16386,7 +16487,7 @@ snapshots:
       '@typescript-eslint/scope-manager': 5.59.7
       '@typescript-eslint/type-utils': 5.59.7(eslint@8.41.0)(typescript@5.4.2)
       '@typescript-eslint/utils': 5.59.7(eslint@8.41.0)(typescript@5.4.2)
-      debug: 4.3.7
+      debug: 4.3.7(supports-color@5.5.0)
       eslint: 8.41.0
       grapheme-splitter: 1.0.4
       ignore: 5.3.1
@@ -16416,7 +16517,7 @@ snapshots:
       '@typescript-eslint/scope-manager': 5.59.7
       '@typescript-eslint/types': 5.59.7
       '@typescript-eslint/typescript-estree': 5.59.7(typescript@5.4.2)
-      debug: 4.3.7
+      debug: 4.3.7(supports-color@5.5.0)
       eslint: 8.41.0
     optionalDependencies:
       typescript: 5.4.2
@@ -16445,7 +16546,7 @@ snapshots:
     dependencies:
       '@typescript-eslint/typescript-estree': 5.59.7(typescript@5.4.2)
       '@typescript-eslint/utils': 5.59.7(eslint@8.41.0)(typescript@5.4.2)
-      debug: 4.3.7
+      debug: 4.3.7(supports-color@5.5.0)
       eslint: 8.41.0
       tsutils: 3.21.0(typescript@5.4.2)
     optionalDependencies:
@@ -16474,7 +16575,7 @@ snapshots:
     dependencies:
       '@typescript-eslint/types': 5.59.7
       '@typescript-eslint/visitor-keys': 5.59.7
-      debug: 4.3.7
+      debug: 4.3.7(supports-color@5.5.0)
       globby: 11.1.0
       is-glob: 4.0.3
       semver: 7.6.3
@@ -16860,13 +16961,13 @@ snapshots:
 
   agent-base@6.0.2:
     dependencies:
-      debug: 4.3.7
+      debug: 4.3.7(supports-color@5.5.0)
     transitivePeerDependencies:
       - supports-color
 
   agent-base@7.1.1:
     dependencies:
-      debug: 4.3.7
+      debug: 4.3.7(supports-color@5.5.0)
     transitivePeerDependencies:
       - supports-color
 
@@ -17213,14 +17314,6 @@ snapshots:
       '@types/babel__core': 7.20.5
       '@types/babel__traverse': 7.0.7
 
-  babel-plugin-superjson-next@0.4.5(next@14.2.13(@babel/core@7.24.6)(@playwright/test@1.46.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.77.6))(superjson@1.13.3):
-    dependencies:
-      '@babel/helper-module-imports': 7.24.6
-      '@babel/types': 7.25.6
-      hoist-non-react-statics: 3.3.2
-      next: 14.2.13(@babel/core@7.24.6)(@playwright/test@1.46.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.77.6)
-      superjson: 1.13.3
-
   babel-preset-current-node-syntax@1.0.1(@babel/core@7.24.6):
     dependencies:
       '@babel/core': 7.24.6
@@ -17940,7 +18033,7 @@ snapshots:
 
   connect-mongo@4.6.0(express-session@1.18.0)(mongodb@4.17.2(@aws-sdk/client-sso-oidc@3.600.0)):
     dependencies:
-      debug: 4.3.7
+      debug: 4.3.7(supports-color@5.5.0)
       express-session: 1.18.0
       kruptein: 3.0.6
       mongodb: 4.17.2(@aws-sdk/client-sso-oidc@3.600.0)
@@ -18420,10 +18513,6 @@ snapshots:
     dependencies:
       ms: 2.1.3
 
-  debug@4.3.7:
-    dependencies:
-      ms: 2.1.3
-
   debug@4.3.7(supports-color@5.5.0):
     dependencies:
       ms: 2.1.3
@@ -18718,7 +18807,7 @@ snapshots:
   engine.io-client@6.6.2:
     dependencies:
       '@socket.io/component-emitter': 3.1.2
-      debug: 4.3.7
+      debug: 4.3.7(supports-color@5.5.0)
       engine.io-parser: 5.2.3
       ws: 8.17.1
       xmlhttprequest-ssl: 2.1.2
@@ -18738,7 +18827,7 @@ snapshots:
       base64id: 2.0.0
       cookie: 0.7.2
       cors: 2.8.5
-      debug: 4.3.7
+      debug: 4.3.7(supports-color@5.5.0)
       engine.io-parser: 5.2.3
       ws: 8.17.1
     transitivePeerDependencies:
@@ -19184,7 +19273,7 @@ snapshots:
       ajv: 6.12.6
       chalk: 4.1.2
       cross-spawn: 7.0.3
-      debug: 4.3.7
+      debug: 4.3.7(supports-color@5.5.0)
       doctrine: 3.0.0
       escape-string-regexp: 4.0.0
       eslint-scope: 7.2.0
@@ -19524,7 +19613,7 @@ snapshots:
 
   follow-redirects@1.15.9(debug@4.3.7):
     optionalDependencies:
-      debug: 4.3.7
+      debug: 4.3.7(supports-color@5.5.0)
 
   follow-redirects@1.5.10:
     dependencies:
@@ -20172,14 +20261,14 @@ snapshots:
     dependencies:
       '@tootallnate/once': 2.0.0
       agent-base: 6.0.2
-      debug: 4.3.7
+      debug: 4.3.7(supports-color@5.5.0)
     transitivePeerDependencies:
       - supports-color
 
   http-proxy-agent@7.0.2:
     dependencies:
       agent-base: 7.1.1
-      debug: 4.3.7
+      debug: 4.3.7(supports-color@5.5.0)
     transitivePeerDependencies:
       - supports-color
 
@@ -20202,14 +20291,14 @@ snapshots:
   https-proxy-agent@5.0.1:
     dependencies:
       agent-base: 6.0.2
-      debug: 4.3.7
+      debug: 4.3.7(supports-color@5.5.0)
     transitivePeerDependencies:
       - supports-color
 
   https-proxy-agent@7.0.5:
     dependencies:
       agent-base: 7.1.1
-      debug: 4.3.7
+      debug: 4.3.7(supports-color@5.5.0)
     transitivePeerDependencies:
       - supports-color
 
@@ -20568,7 +20657,7 @@ snapshots:
 
   istanbul-lib-source-maps@4.0.1:
     dependencies:
-      debug: 4.3.7
+      debug: 4.3.7(supports-color@5.5.0)
       istanbul-lib-coverage: 3.2.2
       source-map: 0.6.1
     transitivePeerDependencies:
@@ -21967,7 +22056,7 @@ snapshots:
   micromark@4.0.0:
     dependencies:
       '@types/debug': 4.1.7
-      debug: 4.3.7
+      debug: 4.3.7(supports-color@5.5.0)
       decode-named-character-reference: 1.0.2
       devlop: 1.1.0
       micromark-core-commonmark: 2.0.1
@@ -22130,7 +22219,7 @@ snapshots:
     dependencies:
       async-mutex: 0.4.1
       camelcase: 6.3.0
-      debug: 4.3.7
+      debug: 4.3.7(supports-color@5.5.0)
       find-cache-dir: 3.3.2
       follow-redirects: 1.15.9(debug@4.3.7)
       https-proxy-agent: 7.0.5
@@ -22238,7 +22327,7 @@ snapshots:
 
   mquery@4.0.3:
     dependencies:
-      debug: 4.3.7
+      debug: 4.3.7(supports-color@5.5.0)
     transitivePeerDependencies:
       - supports-color
 
@@ -22328,7 +22417,7 @@ snapshots:
 
   new-find-package-json@2.0.0:
     dependencies:
-      debug: 4.3.7
+      debug: 4.3.7(supports-color@5.5.0)
     transitivePeerDependencies:
       - supports-color
 
@@ -22348,18 +22437,21 @@ snapshots:
       react: 18.2.0
       react-i18next: 15.1.1(i18next@23.16.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
 
-  next-superjson@0.0.4(next@14.2.13(@babel/core@7.24.6)(@playwright/test@1.46.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.77.6))(superjson@1.13.3)(webpack@5.92.1(@swc/core@1.5.25(@swc/helpers@0.5.11))):
+  next-superjson-plugin@0.6.3(next@14.2.13(@babel/core@7.24.6)(@playwright/test@1.46.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.77.6))(superjson@1.13.3):
     dependencies:
-      '@babel/core': 7.24.6
-      '@babel/plugin-syntax-jsx': 7.24.7(@babel/core@7.24.6)
-      '@babel/plugin-syntax-typescript': 7.24.7(@babel/core@7.24.6)
-      babel-loader: 8.3.0(@babel/core@7.24.6)(webpack@5.92.1(@swc/core@1.5.25(@swc/helpers@0.5.11)))
-      babel-plugin-superjson-next: 0.4.5(next@14.2.13(@babel/core@7.24.6)(@playwright/test@1.46.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.77.6))(superjson@1.13.3)
+      hoist-non-react-statics: 3.3.2
+      next: 14.2.13(@babel/core@7.24.6)(@playwright/test@1.46.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.77.6)
+      superjson: 1.13.3
+
+  next-superjson@1.0.7(@swc/helpers@0.5.11)(next@14.2.13(@babel/core@7.24.6)(@playwright/test@1.46.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.77.6))(superjson@1.13.3):
+    dependencies:
+      '@swc/core': 1.4.17(@swc/helpers@0.5.11)
+      '@swc/types': 0.1.12
       next: 14.2.13(@babel/core@7.24.6)(@playwright/test@1.46.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.77.6)
+      next-superjson-plugin: 0.6.3(next@14.2.13(@babel/core@7.24.6)(@playwright/test@1.46.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.77.6))(superjson@1.13.3)
     transitivePeerDependencies:
+      - '@swc/helpers'
       - superjson
-      - supports-color
-      - webpack
 
   next-themes@0.2.1(next@14.2.13(@babel/core@7.24.6)(@playwright/test@1.46.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.77.6))(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
     dependencies:
@@ -22859,7 +22951,7 @@ snapshots:
   passport-saml@3.2.4:
     dependencies:
       '@xmldom/xmldom': 0.7.13
-      debug: 4.3.7
+      debug: 4.3.7(supports-color@5.5.0)
       passport-strategy: 1.0.0
       xml-crypto: 2.1.5
       xml-encryption: 2.0.0
@@ -23846,7 +23938,7 @@ snapshots:
 
   retry-request@4.2.2:
     dependencies:
-      debug: 4.3.7
+      debug: 4.3.7(supports-color@5.5.0)
       extend: 3.0.2
     transitivePeerDependencies:
       - supports-color
@@ -24236,7 +24328,7 @@ snapshots:
 
   socket.io-adapter@2.5.5:
     dependencies:
-      debug: 4.3.7
+      debug: 4.3.7(supports-color@5.5.0)
       ws: 8.17.1
     transitivePeerDependencies:
       - bufferutil
@@ -24246,7 +24338,7 @@ snapshots:
   socket.io-client@4.8.1:
     dependencies:
       '@socket.io/component-emitter': 3.1.2
-      debug: 4.3.7
+      debug: 4.3.7(supports-color@5.5.0)
       engine.io-client: 6.6.2
       socket.io-parser: 4.2.4
     transitivePeerDependencies:
@@ -24257,7 +24349,7 @@ snapshots:
   socket.io-parser@4.2.4:
     dependencies:
       '@socket.io/component-emitter': 3.1.2
-      debug: 4.3.7
+      debug: 4.3.7(supports-color@5.5.0)
     transitivePeerDependencies:
       - supports-color
 
@@ -24266,7 +24358,7 @@ snapshots:
       accepts: 1.3.8
       base64id: 2.0.0
       cors: 2.8.5
-      debug: 4.3.7
+      debug: 4.3.7(supports-color@5.5.0)
       engine.io: 6.6.2
       socket.io-adapter: 2.5.5
       socket.io-parser: 4.2.4