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

Merge branch 'master' into support/156162-170954-app-utils-biome

Futa Arai 7 месяцев назад
Родитель
Сommit
2c0bf0b288
70 измененных файлов с 1308 добавлено и 843 удалено
  1. 2 0
      apps/app/.eslintrc.js
  2. 52 24
      apps/app/src/features/page-bulk-export/client/components/PageBulkExportSelectModal.tsx
  3. 15 7
      apps/app/src/features/page-bulk-export/client/stores/modal.tsx
  4. 27 19
      apps/app/src/features/page-bulk-export/interfaces/page-bulk-export.ts
  5. 38 19
      apps/app/src/features/page-bulk-export/server/models/page-bulk-export-job.ts
  6. 26 10
      apps/app/src/features/page-bulk-export/server/models/page-bulk-export-page-snapshot.ts
  7. 38 16
      apps/app/src/features/page-bulk-export/server/routes/apiv3/page-bulk-export.ts
  8. 19 10
      apps/app/src/features/page-bulk-export/server/service/check-page-bulk-export-job-in-progress-cron.ts
  9. 71 28
      apps/app/src/features/page-bulk-export/server/service/page-bulk-export-job-clean-up-cron.integ.ts
  10. 63 26
      apps/app/src/features/page-bulk-export/server/service/page-bulk-export-job-clean-up-cron.ts
  11. 0 4
      apps/app/src/features/page-bulk-export/server/service/page-bulk-export-job-cron/errors.ts
  12. 111 52
      apps/app/src/features/page-bulk-export/server/service/page-bulk-export-job-cron/index.ts
  13. 33 17
      apps/app/src/features/page-bulk-export/server/service/page-bulk-export-job-cron/request-pdf-converter.ts
  14. 33 11
      apps/app/src/features/page-bulk-export/server/service/page-bulk-export-job-cron/steps/compress-and-upload.ts
  15. 22 17
      apps/app/src/features/page-bulk-export/server/service/page-bulk-export-job-cron/steps/create-page-snapshots-async.ts
  16. 61 34
      apps/app/src/features/page-bulk-export/server/service/page-bulk-export-job-cron/steps/export-pages-to-fs-async.ts
  17. 53 26
      apps/app/src/features/page-bulk-export/server/service/page-bulk-export.ts
  18. 9 9
      apps/app/src/interfaces/access-token.ts
  19. 126 65
      apps/app/src/interfaces/activity.ts
  20. 1 2
      apps/app/src/interfaces/admin.ts
  21. 5 5
      apps/app/src/interfaces/apiv3/attachment.ts
  22. 22 24
      apps/app/src/interfaces/apiv3/page.ts
  23. 6 5
      apps/app/src/interfaces/attachment.ts
  24. 18 18
      apps/app/src/interfaces/bookmark-info.ts
  25. 9 9
      apps/app/src/interfaces/cdn.ts
  26. 15 18
      apps/app/src/interfaces/comment.ts
  27. 2 2
      apps/app/src/interfaces/common.ts
  28. 4 7
      apps/app/src/interfaces/crowi-request.ts
  29. 4 4
      apps/app/src/interfaces/customize.ts
  30. 15 15
      apps/app/src/interfaces/editor-methods.ts
  31. 3 1
      apps/app/src/interfaces/errors/external-account-login-error.ts
  32. 4 2
      apps/app/src/interfaces/errors/forgot-password.ts
  33. 4 2
      apps/app/src/interfaces/errors/login-error.ts
  34. 4 2
      apps/app/src/interfaces/errors/user-activation.ts
  35. 1 1
      apps/app/src/interfaces/errors/v3-error.ts
  36. 2 1
      apps/app/src/interfaces/errors/v5-conversion-error.ts
  37. 2 1
      apps/app/src/interfaces/external-auth-provider.ts
  38. 8 6
      apps/app/src/interfaces/file-uploader.ts
  39. 4 3
      apps/app/src/interfaces/g2g-transfer.ts
  40. 16 16
      apps/app/src/interfaces/github-api.ts
  41. 17 17
      apps/app/src/interfaces/in-app-notification.ts
  42. 1 1
      apps/app/src/interfaces/indeterminate-input-elm.ts
  43. 5 5
      apps/app/src/interfaces/ldap.ts
  44. 4 5
      apps/app/src/interfaces/named-query.ts
  45. 22 6
      apps/app/src/interfaces/page-delete-config.ts
  46. 17 14
      apps/app/src/interfaces/page-grant.ts
  47. 5 7
      apps/app/src/interfaces/page-listing-results.ts
  48. 10 8
      apps/app/src/interfaces/page-operation.ts
  49. 4 4
      apps/app/src/interfaces/page-tag-relation.ts
  50. 53 43
      apps/app/src/interfaces/page.ts
  51. 4 4
      apps/app/src/interfaces/paging-result.ts
  52. 2 1
      apps/app/src/interfaces/registration-mode.ts
  53. 13 8
      apps/app/src/interfaces/renderer-options.ts
  54. 54 54
      apps/app/src/interfaces/res/admin/app-settings.ts
  55. 18 18
      apps/app/src/interfaces/search.ts
  56. 7 6
      apps/app/src/interfaces/services/rehype-sanitize.ts
  57. 10 10
      apps/app/src/interfaces/services/renderer.ts
  58. 6 6
      apps/app/src/interfaces/share-link.ts
  59. 2 3
      apps/app/src/interfaces/sidebar-config.ts
  60. 18 18
      apps/app/src/interfaces/tag.ts
  61. 1 1
      apps/app/src/interfaces/theme.ts
  62. 4 4
      apps/app/src/interfaces/transfer-key.ts
  63. 20 14
      apps/app/src/interfaces/ui.ts
  64. 36 25
      apps/app/src/interfaces/user-group-response.ts
  65. 8 3
      apps/app/src/interfaces/user-group.ts
  66. 1 2
      apps/app/src/interfaces/user-trigger-notification.ts
  67. 3 3
      apps/app/src/interfaces/user-ui-settings.ts
  68. 9 7
      apps/app/src/interfaces/websocket.ts
  69. 6 6
      apps/app/src/interfaces/yjs.ts
  70. 0 2
      biome.json

+ 2 - 0
apps/app/.eslintrc.js

@@ -37,7 +37,9 @@ module.exports = {
     'src/features/search/**',
     'src/features/plantuml/**',
     'src/features/external-user-group/**',
+    'src/features/page-bulk-export/**',
     'src/features/opentelemetry/**',
+    'src/interfaces/**',
     'src/utils/**',
   ],
   settings: {

+ 52 - 24
apps/app/src/features/page-bulk-export/client/components/PageBulkExportSelectModal.tsx

@@ -1,15 +1,14 @@
-import { useState, type JSX } from 'react';
-
 import { format } from 'date-fns/format';
 import { useTranslation } from 'next-i18next';
-import { Modal, ModalHeader, ModalBody } from 'reactstrap';
+import { type JSX, useState } from 'react';
+import { Modal, ModalBody, ModalHeader } from 'reactstrap';
 
 import { apiv3Post } from '~/client/util/apiv3-client';
 import { toastError, toastSuccess } from '~/client/util/toastr';
 import { usePageBulkExportSelectModal } from '~/features/page-bulk-export/client/stores/modal';
 import { PageBulkExportFormat } from '~/features/page-bulk-export/interfaces/page-bulk-export';
-import { useIsPdfBulkExportEnabled } from '~/stores-universal/context';
 import { useCurrentPagePath } from '~/stores/page';
+import { useIsPdfBulkExportEnabled } from '~/stores-universal/context';
 
 const PageBulkExportSelectModal = (): JSX.Element => {
   const { t } = useTranslation();
@@ -18,35 +17,40 @@ const PageBulkExportSelectModal = (): JSX.Element => {
   const { data: isPdfBulkExportEnabled } = useIsPdfBulkExportEnabled();
 
   const [isRestartModalOpened, setIsRestartModalOpened] = useState(false);
-  const [formatMemoForRestart, setFormatMemoForRestart] = useState<PageBulkExportFormat | undefined>(undefined);
-  const [duplicateJobInfo, setDuplicateJobInfo] = useState<{createdAt: string} | undefined>(undefined);
+  const [formatMemoForRestart, setFormatMemoForRestart] = useState<
+    PageBulkExportFormat | undefined
+  >(undefined);
+  const [duplicateJobInfo, setDuplicateJobInfo] = useState<
+    { createdAt: string } | undefined
+  >(undefined);
 
-  const startBulkExport = async(format: PageBulkExportFormat) => {
+  const startBulkExport = async (format: PageBulkExportFormat) => {
     try {
       setFormatMemoForRestart(format);
       await apiv3Post('/page-bulk-export', { path: currentPagePath, format });
       toastSuccess(t('page_export.bulk_export_started'));
-    }
-    catch (e) {
+    } catch (e) {
       const errorCode = e?.[0].code ?? 'page_export.failed_to_export';
       if (errorCode === 'page_export.duplicate_bulk_export_job_error') {
         setDuplicateJobInfo(e[0].args.duplicateJob);
         setIsRestartModalOpened(true);
-      }
-      else {
+      } else {
         toastError(t(errorCode));
       }
     }
     close();
   };
 
-  const restartBulkExport = async() => {
+  const restartBulkExport = async () => {
     if (formatMemoForRestart != null) {
       try {
-        await apiv3Post('/page-bulk-export', { path: currentPagePath, format: formatMemoForRestart, restartJob: true });
+        await apiv3Post('/page-bulk-export', {
+          path: currentPagePath,
+          format: formatMemoForRestart,
+          restartJob: true,
+        });
         toastSuccess(t('page_export.bulk_export_started'));
-      }
-      catch (e) {
+      } catch (e) {
         toastError(t('page_export.failed_to_export'));
       }
       setIsRestartModalOpened(false);
@@ -63,7 +67,10 @@ const PageBulkExportSelectModal = (): JSX.Element => {
           <ModalBody>
             <p className="card custom-card bg-warning-subtle pt-3 px-3">
               {t('page_export.bulk_export_download_explanation')}
-              <span className="mt-3"><span className="material-symbols-outlined me-1">warning</span>{t('Warning')}</span>
+              <span className="mt-3">
+                <span className="material-symbols-outlined me-1">warning</span>
+                {t('Warning')}
+              </span>
               <ul className="mt-2">
                 <li>{t('page_export.bulk_export_exec_time_warning')}</li>
                 <li>{t('page_export.large_bulk_export_warning')}</li>
@@ -71,11 +78,19 @@ const PageBulkExportSelectModal = (): JSX.Element => {
             </p>
             {t('page_export.choose_export_format')}:
             <div className="d-flex justify-content-center mt-3">
-              <button className="btn btn-primary" type="button" onClick={() => startBulkExport(PageBulkExportFormat.md)}>
+              <button
+                className="btn btn-primary"
+                type="button"
+                onClick={() => startBulkExport(PageBulkExportFormat.md)}
+              >
                 {t('page_export.markdown')}
               </button>
               {isPdfBulkExportEnabled && (
-                <button className="btn btn-primary ms-2" type="button" onClick={() => startBulkExport(PageBulkExportFormat.pdf)}>
+                <button
+                  className="btn btn-primary ms-2"
+                  type="button"
+                  onClick={() => startBulkExport(PageBulkExportFormat.pdf)}
+                >
                   PDF
                 </button>
               )}
@@ -84,7 +99,10 @@ const PageBulkExportSelectModal = (): JSX.Element => {
         </Modal>
       )}
 
-      <Modal isOpen={isRestartModalOpened} toggle={() => setIsRestartModalOpened(false)}>
+      <Modal
+        isOpen={isRestartModalOpened}
+        toggle={() => setIsRestartModalOpened(false)}
+      >
         <ModalHeader tag="h4" toggle={() => setIsRestartModalOpened(false)}>
           {t('page_export.export_in_progress')}
         </ModalHeader>
@@ -93,20 +111,30 @@ const PageBulkExportSelectModal = (): JSX.Element => {
           <div className="text-danger">
             {t('page_export.export_cancel_warning')}:
           </div>
-          { duplicateJobInfo && (
+          {duplicateJobInfo && (
             <div className="my-1">
               <ul>
-                { formatMemoForRestart && (
+                {formatMemoForRestart && (
                   <li>
-                    {t('page_export.format')}: {formatMemoForRestart === PageBulkExportFormat.md ? t('page_export.markdown') : 'PDF'}
+                    {t('page_export.format')}:{' '}
+                    {formatMemoForRestart === PageBulkExportFormat.md
+                      ? t('page_export.markdown')
+                      : 'PDF'}
                   </li>
                 )}
-                <li>{t('page_export.started_on')}: {format(new Date(duplicateJobInfo.createdAt), 'MM/dd HH:mm')}</li>
+                <li>
+                  {t('page_export.started_on')}:{' '}
+                  {format(new Date(duplicateJobInfo.createdAt), 'MM/dd HH:mm')}
+                </li>
               </ul>
             </div>
           )}
           <div className="d-flex justify-content-center mt-3">
-            <button className="btn btn-primary" type="button" onClick={() => restartBulkExport()}>
+            <button
+              className="btn btn-primary"
+              type="button"
+              onClick={() => restartBulkExport()}
+            >
               {t('page_export.restart')}
             </button>
           </div>

+ 15 - 7
apps/app/src/features/page-bulk-export/client/stores/modal.tsx

@@ -3,17 +3,25 @@ import type { SWRResponse } from 'swr';
 import { useStaticSWR } from '../../../../stores/use-static-swr';
 
 type PageBulkExportSelectModalStatus = {
-  isOpened: boolean,
-}
+  isOpened: boolean;
+};
 
 type PageBulkExportSelectModalUtils = {
-  open(): Promise<void>,
-  close(): Promise<void>,
-}
+  open(): Promise<void>;
+  close(): Promise<void>;
+};
 
-export const usePageBulkExportSelectModal = (): SWRResponse<PageBulkExportSelectModalStatus, Error> & PageBulkExportSelectModalUtils => {
+export const usePageBulkExportSelectModal = (): SWRResponse<
+  PageBulkExportSelectModalStatus,
+  Error
+> &
+  PageBulkExportSelectModalUtils => {
   const initialStatus: PageBulkExportSelectModalStatus = { isOpened: false };
-  const swrResponse = useStaticSWR<PageBulkExportSelectModalStatus, Error>('pageBulkExportSelectModal', undefined, { fallbackData: initialStatus });
+  const swrResponse = useStaticSWR<PageBulkExportSelectModalStatus, Error>(
+    'pageBulkExportSelectModal',
+    undefined,
+    { fallbackData: initialStatus },
+  );
 
   return {
     ...swrResponse,

+ 27 - 19
apps/app/src/features/page-bulk-export/interfaces/page-bulk-export.ts

@@ -1,6 +1,10 @@
 import type {
   HasObjectId,
-  IAttachment, IPage, IRevision, IUser, Ref,
+  IAttachment,
+  IPage,
+  IRevision,
+  IUser,
+  Ref,
 } from '@growi/core';
 
 export const PageBulkExportFormat = {
@@ -8,7 +12,8 @@ export const PageBulkExportFormat = {
   pdf: 'pdf',
 } as const;
 
-export type PageBulkExportFormat = typeof PageBulkExportFormat[keyof typeof PageBulkExportFormat]
+export type PageBulkExportFormat =
+  (typeof PageBulkExportFormat)[keyof typeof PageBulkExportFormat];
 
 export const PageBulkExportJobInProgressStatus = {
   initializing: 'initializing', // preparing for export
@@ -22,28 +27,31 @@ export const PageBulkExportJobStatus = {
   failed: 'failed',
 } as const;
 
-export type PageBulkExportJobStatus = typeof PageBulkExportJobStatus[keyof typeof PageBulkExportJobStatus]
+export type PageBulkExportJobStatus =
+  (typeof PageBulkExportJobStatus)[keyof typeof PageBulkExportJobStatus];
 
 export interface IPageBulkExportJob {
-  user: Ref<IUser>, // user that started export job
-  page: Ref<IPage>, // the root page of page tree to export
-  lastExportedPagePath?: string, // the path of page that was exported to the fs last
-  format: PageBulkExportFormat,
-  completedAt?: Date, // the date at which job was completed
-  attachment?: Ref<IAttachment>,
-  status: PageBulkExportJobStatus,
-  statusOnPreviousCronExec?: PageBulkExportJobStatus, // status on previous cron execution
-  revisionListHash?: string, // Hash created from the list of revision IDs. Used to detect existing duplicate uploads.
-  restartFlag: boolean, // flag to restart the job
-  createdAt?: Date,
-  updatedAt?: Date
+  user: Ref<IUser>; // user that started export job
+  page: Ref<IPage>; // the root page of page tree to export
+  lastExportedPagePath?: string; // the path of page that was exported to the fs last
+  format: PageBulkExportFormat;
+  completedAt?: Date; // the date at which job was completed
+  attachment?: Ref<IAttachment>;
+  status: PageBulkExportJobStatus;
+  statusOnPreviousCronExec?: PageBulkExportJobStatus; // status on previous cron execution
+  revisionListHash?: string; // Hash created from the list of revision IDs. Used to detect existing duplicate uploads.
+  restartFlag: boolean; // flag to restart the job
+  createdAt?: Date;
+  updatedAt?: Date;
 }
 
-export interface IPageBulkExportJobHasId extends IPageBulkExportJob, HasObjectId {}
+export interface IPageBulkExportJobHasId
+  extends IPageBulkExportJob,
+    HasObjectId {}
 
 // snapshot of page info to upload
 export interface IPageBulkExportPageSnapshot {
-  pageBulkExportJob: Ref<IPageBulkExportJob>,
-  path: string, // page path when export was stared
-  revision: Ref<IRevision>, // page revision when export was stared
+  pageBulkExportJob: Ref<IPageBulkExportJob>;
+  path: string; // page path when export was stared
+  revision: Ref<IRevision>; // page revision when export was stared
 }

+ 38 - 19
apps/app/src/features/page-bulk-export/server/models/page-bulk-export-job.ts

@@ -3,27 +3,46 @@ import { type Document, type Model, Schema } from 'mongoose';
 import { getOrCreateModel } from '~/server/util/mongoose-utils';
 
 import type { IPageBulkExportJob } from '../../interfaces/page-bulk-export';
-import { PageBulkExportFormat, PageBulkExportJobStatus } from '../../interfaces/page-bulk-export';
+import {
+  PageBulkExportFormat,
+  PageBulkExportJobStatus,
+} from '../../interfaces/page-bulk-export';
 
-export interface PageBulkExportJobDocument extends IPageBulkExportJob, Document {}
+export interface PageBulkExportJobDocument
+  extends IPageBulkExportJob,
+    Document {}
 
-export type PageBulkExportJobModel = Model<PageBulkExportJobDocument>
+export type PageBulkExportJobModel = Model<PageBulkExportJobDocument>;
 
-const pageBulkExportJobSchema = new Schema<PageBulkExportJobDocument>({
-  user: { type: Schema.Types.ObjectId, ref: 'User', required: true },
-  page: { type: Schema.Types.ObjectId, ref: 'Page', required: true },
-  lastExportedPagePath: { type: String },
-  format: { type: String, enum: Object.values(PageBulkExportFormat), required: true },
-  completedAt: { type: Date },
-  attachment: { type: Schema.Types.ObjectId, ref: 'Attachment' },
-  status: {
-    type: String, enum: Object.values(PageBulkExportJobStatus), required: true, default: PageBulkExportJobStatus.initializing,
+const pageBulkExportJobSchema = new Schema<PageBulkExportJobDocument>(
+  {
+    user: { type: Schema.Types.ObjectId, ref: 'User', required: true },
+    page: { type: Schema.Types.ObjectId, ref: 'Page', required: true },
+    lastExportedPagePath: { type: String },
+    format: {
+      type: String,
+      enum: Object.values(PageBulkExportFormat),
+      required: true,
+    },
+    completedAt: { type: Date },
+    attachment: { type: Schema.Types.ObjectId, ref: 'Attachment' },
+    status: {
+      type: String,
+      enum: Object.values(PageBulkExportJobStatus),
+      required: true,
+      default: PageBulkExportJobStatus.initializing,
+    },
+    statusOnPreviousCronExec: {
+      type: String,
+      enum: Object.values(PageBulkExportJobStatus),
+    },
+    restartFlag: { type: Boolean, required: true, default: false },
+    revisionListHash: { type: String },
   },
-  statusOnPreviousCronExec: {
-    type: String, enum: Object.values(PageBulkExportJobStatus),
-  },
-  restartFlag: { type: Boolean, required: true, default: false },
-  revisionListHash: { type: String },
-}, { timestamps: true });
+  { timestamps: true },
+);
 
-export default getOrCreateModel<PageBulkExportJobDocument, PageBulkExportJobModel>('PageBulkExportJob', pageBulkExportJobSchema);
+export default getOrCreateModel<
+  PageBulkExportJobDocument,
+  PageBulkExportJobModel
+>('PageBulkExportJob', pageBulkExportJobSchema);

+ 26 - 10
apps/app/src/features/page-bulk-export/server/models/page-bulk-export-page-snapshot.ts

@@ -4,16 +4,32 @@ import { getOrCreateModel } from '~/server/util/mongoose-utils';
 
 import type { IPageBulkExportPageSnapshot } from '../../interfaces/page-bulk-export';
 
-export interface PageBulkExportPageSnapshotDocument extends IPageBulkExportPageSnapshot, Document {}
+export interface PageBulkExportPageSnapshotDocument
+  extends IPageBulkExportPageSnapshot,
+    Document {}
 
-export type PageBulkExportPageSnapshotModel = Model<PageBulkExportPageSnapshotDocument>
+export type PageBulkExportPageSnapshotModel =
+  Model<PageBulkExportPageSnapshotDocument>;
 
-const pageBulkExportPageInfoSchema = new Schema<PageBulkExportPageSnapshotDocument>({
-  pageBulkExportJob: { type: Schema.Types.ObjectId, ref: 'PageBulkExportJob', required: true },
-  path: { type: String, required: true },
-  revision: { type: Schema.Types.ObjectId, ref: 'Revision', required: true },
-}, { timestamps: true });
+const pageBulkExportPageInfoSchema =
+  new Schema<PageBulkExportPageSnapshotDocument>(
+    {
+      pageBulkExportJob: {
+        type: Schema.Types.ObjectId,
+        ref: 'PageBulkExportJob',
+        required: true,
+      },
+      path: { type: String, required: true },
+      revision: {
+        type: Schema.Types.ObjectId,
+        ref: 'Revision',
+        required: true,
+      },
+    },
+    { timestamps: true },
+  );
 
-export default getOrCreateModel<PageBulkExportPageSnapshotDocument, PageBulkExportPageSnapshotModel>(
-  'PageBulkExportPageSnapshot', pageBulkExportPageInfoSchema,
-);
+export default getOrCreateModel<
+  PageBulkExportPageSnapshotDocument,
+  PageBulkExportPageSnapshotModel
+>('PageBulkExportPageSnapshot', pageBulkExportPageInfoSchema);

+ 38 - 16
apps/app/src/features/page-bulk-export/server/routes/apiv3/page-bulk-export.ts

@@ -8,19 +8,24 @@ import type Crowi from '~/server/crowi';
 import type { ApiV3Response } from '~/server/routes/apiv3/interfaces/apiv3-response';
 import loggerFactory from '~/utils/logger';
 
-import { pageBulkExportService, DuplicateBulkExportJobError } from '../../service/page-bulk-export';
+import {
+  DuplicateBulkExportJobError,
+  pageBulkExportService,
+} from '../../service/page-bulk-export';
 
 const logger = loggerFactory('growi:routes:apiv3:page-bulk-export');
 
 const router = Router();
 
 interface AuthorizedRequest extends Request {
-  user?: any
+  user?: any;
 }
 
 module.exports = (crowi: Crowi): Router => {
   const accessTokenParser = crowi.accessTokenParser;
-  const loginRequiredStrictly = require('~/server/middlewares/login-required')(crowi);
+  const loginRequiredStrictly = require('~/server/middlewares/login-required')(
+    crowi,
+  );
 
   const validators = {
     pageBulkExport: [
@@ -30,8 +35,12 @@ module.exports = (crowi: Crowi): Router => {
     ],
   };
 
-  router.post('/', accessTokenParser([SCOPE.WRITE.FEATURES.PAGE_BULK_EXPORT]),
-    loginRequiredStrictly, validators.pageBulkExport, async(req: AuthorizedRequest, res: ApiV3Response) => {
+  router.post(
+    '/',
+    accessTokenParser([SCOPE.WRITE.FEATURES.PAGE_BULK_EXPORT]),
+    loginRequiredStrictly,
+    validators.pageBulkExport,
+    async (req: AuthorizedRequest, res: ApiV3Response) => {
       const errors = validationResult(req);
       if (!errors.isEmpty()) {
         return res.status(400).json({ errors: errors.array() });
@@ -40,22 +49,35 @@ module.exports = (crowi: Crowi): Router => {
       const { path, format, restartJob } = req.body;
 
       try {
-        await pageBulkExportService?.createOrResetBulkExportJob(path, format, req.user, restartJob);
+        await pageBulkExportService?.createOrResetBulkExportJob(
+          path,
+          format,
+          req.user,
+          restartJob,
+        );
         return res.apiv3({}, 204);
-      }
-      catch (err) {
+      } catch (err) {
         logger.error(err);
         if (err instanceof DuplicateBulkExportJobError) {
-          return res.apiv3Err(new ErrorV3(
-            'Duplicate bulk export job is in progress',
-            'page_export.duplicate_bulk_export_job_error', undefined,
-            { duplicateJob: { createdAt: err.duplicateJob.createdAt } },
-          ), 409);
+          return res.apiv3Err(
+            new ErrorV3(
+              'Duplicate bulk export job is in progress',
+              'page_export.duplicate_bulk_export_job_error',
+              undefined,
+              { duplicateJob: { createdAt: err.duplicateJob.createdAt } },
+            ),
+            409,
+          );
         }
-        return res.apiv3Err(new ErrorV3('Failed to start bulk export', 'page_export.failed_to_export'));
+        return res.apiv3Err(
+          new ErrorV3(
+            'Failed to start bulk export',
+            'page_export.failed_to_export',
+          ),
+        );
       }
-    });
+    },
+  );
 
   return router;
-
 };

+ 19 - 10
apps/app/src/features/page-bulk-export/server/service/check-page-bulk-export-job-in-progress-cron.ts

@@ -7,36 +7,45 @@ import PageBulkExportJob from '../models/page-bulk-export-job';
 
 import { pageBulkExportJobCronService } from './page-bulk-export-job-cron';
 
-const logger = loggerFactory('growi:service:check-page-bulk-export-job-in-progress-cron');
+const logger = loggerFactory(
+  'growi:service:check-page-bulk-export-job-in-progress-cron',
+);
 
 /**
  * Manages cronjob which checks if PageBulkExportJob in progress exists.
  * If it does, and PageBulkExportJobCronService is not running, start PageBulkExportJobCronService
  */
 class CheckPageBulkExportJobInProgressCronService extends CronService {
-
   override getCronSchedule(): string {
-    return configManager.getConfig('app:checkPageBulkExportJobInProgressCronSchedule');
+    return configManager.getConfig(
+      'app:checkPageBulkExportJobInProgressCronSchedule',
+    );
   }
 
   override async executeJob(): Promise<void> {
     // TODO: remove growiCloudUri condition when bulk export can be relased for GROWI.cloud (https://redmine.weseek.co.jp/issues/163220)
-    const isBulkExportPagesEnabled = configManager.getConfig('app:isBulkExportPagesEnabled') && configManager.getConfig('app:growiCloudUri') == null;
+    const isBulkExportPagesEnabled =
+      configManager.getConfig('app:isBulkExportPagesEnabled') &&
+      configManager.getConfig('app:growiCloudUri') == null;
     if (!isBulkExportPagesEnabled) return;
 
     const pageBulkExportJobInProgress = await PageBulkExportJob.findOne({
-      $or: Object.values(PageBulkExportJobInProgressStatus).map(status => ({ status })),
+      $or: Object.values(PageBulkExportJobInProgressStatus).map((status) => ({
+        status,
+      })),
     });
     const pageBulkExportInProgressExists = pageBulkExportJobInProgress != null;
 
-    if (pageBulkExportInProgressExists && !pageBulkExportJobCronService?.isJobRunning()) {
+    if (
+      pageBulkExportInProgressExists &&
+      !pageBulkExportJobCronService?.isJobRunning()
+    ) {
       pageBulkExportJobCronService?.startCron();
-    }
-    else if (!pageBulkExportInProgressExists) {
+    } else if (!pageBulkExportInProgressExists) {
       pageBulkExportJobCronService?.stopCron();
     }
   }
-
 }
 
-export const checkPageBulkExportJobInProgressCronService = new CheckPageBulkExportJobInProgressCronService(); // singleton instance
+export const checkPageBulkExportJobInProgressCronService =
+  new CheckPageBulkExportJobInProgressCronService(); // singleton instance

+ 71 - 28
apps/app/src/features/page-bulk-export/server/service/page-bulk-export-job-clean-up-cron.integ.ts

@@ -3,20 +3,28 @@ import mongoose from 'mongoose';
 import type Crowi from '~/server/crowi';
 import { configManager } from '~/server/service/config-manager';
 
-import { PageBulkExportFormat, PageBulkExportJobStatus } from '../../interfaces/page-bulk-export';
+import {
+  PageBulkExportFormat,
+  PageBulkExportJobStatus,
+} from '../../interfaces/page-bulk-export';
 import PageBulkExportJob from '../models/page-bulk-export-job';
 
-import instanciatePageBulkExportJobCleanUpCronService, { pageBulkExportJobCleanUpCronService } from './page-bulk-export-job-clean-up-cron';
+import instanciatePageBulkExportJobCleanUpCronService, {
+  pageBulkExportJobCleanUpCronService,
+} from './page-bulk-export-job-clean-up-cron';
 
 // TODO: use actual user model after ~/server/models/user.js becomes importable in vitest
 // ref: https://github.com/vitest-dev/vitest/issues/846
-const userSchema = new mongoose.Schema({
-  name: { type: String },
-  username: { type: String, required: true, unique: true },
-  email: { type: String, unique: true, sparse: true },
-}, {
-  timestamps: true,
-});
+const userSchema = new mongoose.Schema(
+  {
+    name: { type: String },
+    username: { type: String, required: true, unique: true },
+    email: { type: String, unique: true, sparse: true },
+  },
+  {
+    timestamps: true,
+  },
+);
 const User = mongoose.model('User', userSchema);
 
 vi.mock('./page-bulk-export-job-cron', () => {
@@ -29,9 +37,10 @@ vi.mock('./page-bulk-export-job-cron', () => {
 
 describe('PageBulkExportJobCleanUpCronService', () => {
   const crowi = {} as Crowi;
+  // biome-ignore lint/suspicious/noImplicitAnyLet: ignore
   let user;
 
-  beforeAll(async() => {
+  beforeAll(async () => {
     await configManager.loadConfigs();
     user = await User.create({
       name: 'Example for PageBulkExportJobCleanUpCronService Test',
@@ -41,7 +50,7 @@ describe('PageBulkExportJobCleanUpCronService', () => {
     instanciatePageBulkExportJobCleanUpCronService(crowi);
   });
 
-  beforeEach(async() => {
+  beforeEach(async () => {
     await PageBulkExportJob.deleteMany();
   });
 
@@ -51,8 +60,11 @@ describe('PageBulkExportJobCleanUpCronService', () => {
     const jobId2 = new mongoose.Types.ObjectId();
     const jobId3 = new mongoose.Types.ObjectId();
     const jobId4 = new mongoose.Types.ObjectId();
-    beforeEach(async() => {
-      await configManager.updateConfig('app:bulkExportJobExpirationSeconds', 86400); // 1 day
+    beforeEach(async () => {
+      await configManager.updateConfig(
+        'app:bulkExportJobExpirationSeconds',
+        86400,
+      ); // 1 day
 
       await PageBulkExportJob.insertMany([
         {
@@ -80,12 +92,16 @@ describe('PageBulkExportJobCleanUpCronService', () => {
           createdAt: new Date(Date.now() - 86400 * 1000 - 2),
         },
         {
-          _id: jobId4, user, page: new mongoose.Types.ObjectId(), format: PageBulkExportFormat.md, status: PageBulkExportJobStatus.failed,
+          _id: jobId4,
+          user,
+          page: new mongoose.Types.ObjectId(),
+          format: PageBulkExportFormat.md,
+          status: PageBulkExportJobStatus.failed,
         },
       ]);
     });
 
-    test('should delete expired jobs', async() => {
+    test('should delete expired jobs', async () => {
       expect(await PageBulkExportJob.find()).toHaveLength(4);
 
       // act
@@ -94,7 +110,9 @@ describe('PageBulkExportJobCleanUpCronService', () => {
 
       // assert
       expect(jobs).toHaveLength(2);
-      expect(jobs.map(job => job._id).sort()).toStrictEqual([jobId1, jobId4].sort());
+      expect(jobs.map((job) => job._id).sort()).toStrictEqual(
+        [jobId1, jobId4].sort(),
+      );
     });
   });
 
@@ -104,8 +122,11 @@ describe('PageBulkExportJobCleanUpCronService', () => {
     const jobId2 = new mongoose.Types.ObjectId();
     const jobId3 = new mongoose.Types.ObjectId();
     const jobId4 = new mongoose.Types.ObjectId();
-    beforeEach(async() => {
-      await configManager.updateConfig('app:bulkExportDownloadExpirationSeconds', 86400); // 1 day
+    beforeEach(async () => {
+      await configManager.updateConfig(
+        'app:bulkExportDownloadExpirationSeconds',
+        86400,
+      ); // 1 day
 
       await PageBulkExportJob.insertMany([
         {
@@ -125,15 +146,23 @@ describe('PageBulkExportJobCleanUpCronService', () => {
           completedAt: new Date(Date.now() - 86400 * 1000 - 1),
         },
         {
-          _id: jobId3, user, page: new mongoose.Types.ObjectId(), format: PageBulkExportFormat.md, status: PageBulkExportJobStatus.initializing,
+          _id: jobId3,
+          user,
+          page: new mongoose.Types.ObjectId(),
+          format: PageBulkExportFormat.md,
+          status: PageBulkExportJobStatus.initializing,
         },
         {
-          _id: jobId4, user, page: new mongoose.Types.ObjectId(), format: PageBulkExportFormat.md, status: PageBulkExportJobStatus.failed,
+          _id: jobId4,
+          user,
+          page: new mongoose.Types.ObjectId(),
+          format: PageBulkExportFormat.md,
+          status: PageBulkExportJobStatus.failed,
         },
       ]);
     });
 
-    test('should delete download expired jobs', async() => {
+    test('should delete download expired jobs', async () => {
       expect(await PageBulkExportJob.find()).toHaveLength(4);
 
       // act
@@ -142,7 +171,9 @@ describe('PageBulkExportJobCleanUpCronService', () => {
 
       // assert
       expect(jobs).toHaveLength(3);
-      expect(jobs.map(job => job._id).sort()).toStrictEqual([jobId1, jobId3, jobId4].sort());
+      expect(jobs.map((job) => job._id).sort()).toStrictEqual(
+        [jobId1, jobId3, jobId4].sort(),
+      );
     });
   });
 
@@ -151,21 +182,33 @@ describe('PageBulkExportJobCleanUpCronService', () => {
     const jobId1 = new mongoose.Types.ObjectId();
     const jobId2 = new mongoose.Types.ObjectId();
     const jobId3 = new mongoose.Types.ObjectId();
-    beforeEach(async() => {
+    beforeEach(async () => {
       await PageBulkExportJob.insertMany([
         {
-          _id: jobId1, user, page: new mongoose.Types.ObjectId(), format: PageBulkExportFormat.md, status: PageBulkExportJobStatus.failed,
+          _id: jobId1,
+          user,
+          page: new mongoose.Types.ObjectId(),
+          format: PageBulkExportFormat.md,
+          status: PageBulkExportJobStatus.failed,
         },
         {
-          _id: jobId2, user, page: new mongoose.Types.ObjectId(), format: PageBulkExportFormat.md, status: PageBulkExportJobStatus.initializing,
+          _id: jobId2,
+          user,
+          page: new mongoose.Types.ObjectId(),
+          format: PageBulkExportFormat.md,
+          status: PageBulkExportJobStatus.initializing,
         },
         {
-          _id: jobId3, user, page: new mongoose.Types.ObjectId(), format: PageBulkExportFormat.md, status: PageBulkExportJobStatus.failed,
+          _id: jobId3,
+          user,
+          page: new mongoose.Types.ObjectId(),
+          format: PageBulkExportFormat.md,
+          status: PageBulkExportJobStatus.failed,
         },
       ]);
     });
 
-    test('should delete failed export jobs', async() => {
+    test('should delete failed export jobs', async () => {
       expect(await PageBulkExportJob.find()).toHaveLength(3);
 
       // act
@@ -174,7 +217,7 @@ describe('PageBulkExportJobCleanUpCronService', () => {
 
       // assert
       expect(jobs).toHaveLength(1);
-      expect(jobs.map(job => job._id)).toStrictEqual([jobId2]);
+      expect(jobs.map((job) => job._id)).toStrictEqual([jobId2]);
     });
   });
 });

+ 63 - 26
apps/app/src/features/page-bulk-export/server/service/page-bulk-export-job-clean-up-cron.ts

@@ -5,19 +5,23 @@ import { configManager } from '~/server/service/config-manager';
 import CronService from '~/server/service/cron';
 import loggerFactory from '~/utils/logger';
 
-import { PageBulkExportJobInProgressStatus, PageBulkExportJobStatus } from '../../interfaces/page-bulk-export';
+import {
+  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 { pageBulkExportJobCronService } from './page-bulk-export-job-cron';
 
-const logger = loggerFactory('growi:service:page-bulk-export-job-clean-up-cron');
+const logger = loggerFactory(
+  'growi:service:page-bulk-export-job-clean-up-cron',
+);
 
 /**
  * Manages cronjob which deletes unnecessary bulk export jobs
  */
 class PageBulkExportJobCleanUpCronService extends CronService {
-
   crowi: Crowi;
 
   constructor(crowi: Crowi) {
@@ -41,14 +45,25 @@ class PageBulkExportJobCleanUpCronService extends CronService {
    * Delete bulk export jobs which are on-going and has passed the limit time for execution
    */
   async deleteExpiredExportJobs() {
-    const exportJobExpirationSeconds = configManager.getConfig('app:bulkExportJobExpirationSeconds');
+    const exportJobExpirationSeconds = configManager.getConfig(
+      'app:bulkExportJobExpirationSeconds',
+    );
     const expiredExportJobs = await PageBulkExportJob.find({
-      $or: Object.values(PageBulkExportJobInProgressStatus).map(status => ({ status })),
-      createdAt: { $lt: new Date(Date.now() - exportJobExpirationSeconds * 1000) },
+      $or: Object.values(PageBulkExportJobInProgressStatus).map((status) => ({
+        status,
+      })),
+      createdAt: {
+        $lt: new Date(Date.now() - exportJobExpirationSeconds * 1000),
+      },
     });
 
     if (pageBulkExportJobCronService != null) {
-      await this.cleanUpAndDeleteBulkExportJobs(expiredExportJobs, pageBulkExportJobCronService.cleanUpExportJobResources.bind(pageBulkExportJobCronService));
+      await this.cleanUpAndDeleteBulkExportJobs(
+        expiredExportJobs,
+        pageBulkExportJobCronService.cleanUpExportJobResources.bind(
+          pageBulkExportJobCronService,
+        ),
+      );
     }
   }
 
@@ -56,63 +71,85 @@ class PageBulkExportJobCleanUpCronService extends CronService {
    * Delete bulk export jobs which have completed but the due time for downloading has passed
    */
   async deleteDownloadExpiredExportJobs() {
-    const downloadExpirationSeconds = configManager.getConfig('app:bulkExportDownloadExpirationSeconds');
-    const thresholdDate = new Date(Date.now() - downloadExpirationSeconds * 1000);
+    const downloadExpirationSeconds = configManager.getConfig(
+      'app:bulkExportDownloadExpirationSeconds',
+    );
+    const thresholdDate = new Date(
+      Date.now() - downloadExpirationSeconds * 1000,
+    );
     const downloadExpiredExportJobs = await PageBulkExportJob.find({
       status: PageBulkExportJobStatus.completed,
       completedAt: { $lt: thresholdDate },
     });
 
-    const cleanUp = async(job: PageBulkExportJobDocument) => {
+    const cleanUp = async (job: PageBulkExportJobDocument) => {
       await pageBulkExportJobCronService?.cleanUpExportJobResources(job);
 
-      const hasSameAttachmentAndDownloadNotExpired = await PageBulkExportJob.findOne({
-        attachment: job.attachment,
-        _id: { $ne: job._id },
-        completedAt: { $gte: thresholdDate },
-      });
+      const hasSameAttachmentAndDownloadNotExpired =
+        await PageBulkExportJob.findOne({
+          attachment: job.attachment,
+          _id: { $ne: job._id },
+          completedAt: { $gte: thresholdDate },
+        });
       if (hasSameAttachmentAndDownloadNotExpired == null) {
         // delete attachment if no other export job (which download has not expired) has re-used it
         await this.crowi.attachmentService?.removeAttachment(job.attachment);
       }
     };
 
-    await this.cleanUpAndDeleteBulkExportJobs(downloadExpiredExportJobs, cleanUp);
+    await this.cleanUpAndDeleteBulkExportJobs(
+      downloadExpiredExportJobs,
+      cleanUp,
+    );
   }
 
   /**
    * Delete bulk export jobs which have failed
    */
   async deleteFailedExportJobs() {
-    const failedExportJobs = await PageBulkExportJob.find({ status: PageBulkExportJobStatus.failed });
+    const failedExportJobs = await PageBulkExportJob.find({
+      status: PageBulkExportJobStatus.failed,
+    });
 
     if (pageBulkExportJobCronService != null) {
-      await this.cleanUpAndDeleteBulkExportJobs(failedExportJobs, pageBulkExportJobCronService.cleanUpExportJobResources.bind(pageBulkExportJobCronService));
+      await this.cleanUpAndDeleteBulkExportJobs(
+        failedExportJobs,
+        pageBulkExportJobCronService.cleanUpExportJobResources.bind(
+          pageBulkExportJobCronService,
+        ),
+      );
     }
   }
 
   async cleanUpAndDeleteBulkExportJobs(
-      pageBulkExportJobs: HydratedDocument<PageBulkExportJobDocument>[],
-      cleanUp: (job: PageBulkExportJobDocument) => Promise<void>,
+    pageBulkExportJobs: HydratedDocument<PageBulkExportJobDocument>[],
+    cleanUp: (job: PageBulkExportJobDocument) => Promise<void>,
   ): Promise<void> {
-    const results = await Promise.allSettled(pageBulkExportJobs.map(job => cleanUp(job)));
+    const results = await Promise.allSettled(
+      pageBulkExportJobs.map((job) => cleanUp(job)),
+    );
     results.forEach((result) => {
       if (result.status === 'rejected') logger.error(result.reason);
     });
 
     // Only batch delete jobs which have been successfully cleaned up
     // Clean up failed jobs will be retried in the next cron execution
-    const cleanedUpJobs = pageBulkExportJobs.filter((_, index) => results[index].status === 'fulfilled');
+    const cleanedUpJobs = pageBulkExportJobs.filter(
+      (_, index) => results[index].status === 'fulfilled',
+    );
     if (cleanedUpJobs.length > 0) {
-      const cleanedUpJobIds = cleanedUpJobs.map(job => job._id);
+      const cleanedUpJobIds = cleanedUpJobs.map((job) => job._id);
       await PageBulkExportJob.deleteMany({ _id: { $in: cleanedUpJobIds } });
     }
   }
-
 }
 
 // eslint-disable-next-line import/no-mutable-exports
-export let pageBulkExportJobCleanUpCronService: PageBulkExportJobCleanUpCronService | undefined; // singleton instance
+export let pageBulkExportJobCleanUpCronService:
+  | PageBulkExportJobCleanUpCronService
+  | undefined; // singleton instance
 export default function instanciate(crowi: Crowi): void {
-  pageBulkExportJobCleanUpCronService = new PageBulkExportJobCleanUpCronService(crowi);
+  pageBulkExportJobCleanUpCronService = new PageBulkExportJobCleanUpCronService(
+    crowi,
+  );
 }

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

@@ -1,15 +1,11 @@
 export class BulkExportJobExpiredError extends Error {
-
   constructor() {
     super('Bulk export job has expired');
   }
-
 }
 
 export class BulkExportJobRestartedError extends Error {
-
   constructor() {
     super('Bulk export job has restarted');
   }
-
 }

+ 111 - 52
apps/app/src/features/page-bulk-export/server/service/page-bulk-export-job-cron/index.ts

@@ -1,12 +1,10 @@
+import type { IUser } from '@growi/core';
+import { getIdForRef, isPopulated } from '@growi/core';
 import fs from 'fs';
+import mongoose from 'mongoose';
 import path from 'path';
 import type { Readable } from 'stream';
 
-import type { IUser } from '@growi/core';
-import { isPopulated, getIdForRef } from '@growi/core';
-import mongoose from 'mongoose';
-
-
 import type { SupportedActionType } from '~/interfaces/activity';
 import { SupportedAction, SupportedTargetModel } from '~/interfaces/activity';
 import type Crowi from '~/server/crowi';
@@ -17,19 +15,24 @@ import CronService from '~/server/service/cron';
 import { preNotifyService } from '~/server/service/pre-notify';
 import loggerFactory from '~/utils/logger';
 
-import { PageBulkExportFormat, PageBulkExportJobInProgressStatus, PageBulkExportJobStatus } from '../../../interfaces/page-bulk-export';
+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 PageBulkExportPageSnapshot from '../../models/page-bulk-export-page-snapshot';
 
-
-import { BulkExportJobExpiredError, BulkExportJobRestartedError } from './errors';
+import {
+  BulkExportJobExpiredError,
+  BulkExportJobRestartedError,
+} from './errors';
 import { requestPdfConverter } from './request-pdf-converter';
 import { compressAndUpload } from './steps/compress-and-upload';
 import { createPageSnapshotsAsync } from './steps/create-page-snapshots-async';
 import { exportPagesToFsAsync } from './steps/export-pages-to-fs-async';
 
-
 const logger = loggerFactory('growi:service:page-bulk-export-job-cron');
 
 export interface IPageBulkExportJobCronService {
@@ -39,17 +42,28 @@ export interface IPageBulkExportJobCronService {
   compressExtension: string;
   setStreamInExecution(jobId: ObjectIdLike, stream: Readable): void;
   removeStreamInExecution(jobId: ObjectIdLike): void;
-  handleError(err: Error | null, pageBulkExportJob: PageBulkExportJobDocument): void;
-  notifyExportResultAndCleanUp(action: SupportedActionType, pageBulkExportJob: PageBulkExportJobDocument): Promise<void>;
-  getTmpOutputDir(pageBulkExportJob: PageBulkExportJobDocument, isHtmlPath?: boolean): string;
+  handleError(
+    err: Error | null,
+    pageBulkExportJob: PageBulkExportJobDocument,
+  ): void;
+  notifyExportResultAndCleanUp(
+    action: SupportedActionType,
+    pageBulkExportJob: PageBulkExportJobDocument,
+  ): Promise<void>;
+  getTmpOutputDir(
+    pageBulkExportJob: PageBulkExportJobDocument,
+    isHtmlPath?: boolean,
+  ): string;
 }
 
 /**
  * Manages cronjob which proceeds PageBulkExportJobs in progress.
  * If PageBulkExportJob finishes the current step, the next step will be started on the next cron execution.
  */
-class PageBulkExportJobCronService extends CronService implements IPageBulkExportJobCronService {
-
+class PageBulkExportJobCronService
+  extends CronService
+  implements IPageBulkExportJobCronService
+{
   crowi: Crowi;
 
   activityEvent: any;
@@ -76,7 +90,9 @@ class PageBulkExportJobCronService extends CronService implements IPageBulkExpor
     super();
     this.crowi = crowi;
     this.activityEvent = crowi.event('activity');
-    this.parallelExecLimit = configManager.getConfig('app:pageBulkExportParallelExecLimit');
+    this.parallelExecLimit = configManager.getConfig(
+      'app:pageBulkExportParallelExecLimit',
+    );
   }
 
   override getCronSchedule(): string {
@@ -85,8 +101,12 @@ class PageBulkExportJobCronService extends CronService implements IPageBulkExpor
 
   override async executeJob(): Promise<void> {
     const pageBulkExportJobsInProgress = await PageBulkExportJob.find({
-      $or: Object.values(PageBulkExportJobInProgressStatus).map(status => ({ status })),
-    }).sort({ createdAt: 1 }).limit(this.parallelExecLimit);
+      $or: Object.values(PageBulkExportJobInProgressStatus).map((status) => ({
+        status,
+      })),
+    })
+      .sort({ createdAt: 1 })
+      .limit(this.parallelExecLimit);
 
     pageBulkExportJobsInProgress.forEach((pageBulkExportJob) => {
       this.proceedBulkExportJob(pageBulkExportJob);
@@ -102,9 +122,14 @@ class PageBulkExportJobCronService extends CronService implements IPageBulkExpor
    * @param pageBulkExportJob page bulk export job in execution
    * @param isHtmlPath whether the tmp output path is for html files
    */
-  getTmpOutputDir(pageBulkExportJob: PageBulkExportJobDocument, isHtmlPath = false): string {
+  getTmpOutputDir(
+    pageBulkExportJob: PageBulkExportJobDocument,
+    isHtmlPath = false,
+  ): string {
     const jobId = pageBulkExportJob._id.toString();
-    return isHtmlPath ? path.join(this.tmpOutputRootDir, 'html', jobId) : path.join(this.tmpOutputRootDir, jobId);
+    return isHtmlPath
+      ? path.join(this.tmpOutputRootDir, 'html', jobId)
+      : path.join(this.tmpOutputRootDir, jobId);
   }
 
   /**
@@ -143,12 +168,17 @@ class PageBulkExportJobCronService extends CronService implements IPageBulkExpor
         await pageBulkExportJob.save();
       }
 
-      if (pageBulkExportJob.status === PageBulkExportJobStatus.exporting && pageBulkExportJob.format === PageBulkExportFormat.pdf) {
+      if (
+        pageBulkExportJob.status === PageBulkExportJobStatus.exporting &&
+        pageBulkExportJob.format === PageBulkExportFormat.pdf
+      ) {
         await requestPdfConverter(pageBulkExportJob);
       }
 
       // return if job is still the same status as the previous cron exec
-      if (pageBulkExportJob.status === pageBulkExportJob.statusOnPreviousCronExec) {
+      if (
+        pageBulkExportJob.status === pageBulkExportJob.statusOnPreviousCronExec
+      ) {
         return;
       }
 
@@ -161,17 +191,21 @@ class PageBulkExportJobCronService extends CronService implements IPageBulkExpor
 
       if (pageBulkExportJob.status === PageBulkExportJobStatus.initializing) {
         await createPageSnapshotsAsync.bind(this)(user, pageBulkExportJob);
-      }
-      else if (pageBulkExportJob.status === PageBulkExportJobStatus.exporting) {
+      } else if (
+        pageBulkExportJob.status === PageBulkExportJobStatus.exporting
+      ) {
         await exportPagesToFsAsync.bind(this)(pageBulkExportJob);
-      }
-      else if (pageBulkExportJob.status === PageBulkExportJobStatus.uploading) {
+      } else if (
+        pageBulkExportJob.status === PageBulkExportJobStatus.uploading
+      ) {
         compressAndUpload.bind(this)(user, pageBulkExportJob);
       }
-    }
-    catch (err) {
+    } catch (err) {
       logger.error(err);
-      await this.notifyExportResultAndCleanUp(SupportedAction.ACTION_PAGE_BULK_EXPORT_FAILED, pageBulkExportJob);
+      await this.notifyExportResultAndCleanUp(
+        SupportedAction.ACTION_PAGE_BULK_EXPORT_FAILED,
+        pageBulkExportJob,
+      );
     }
   }
 
@@ -180,20 +214,27 @@ class PageBulkExportJobCronService extends CronService implements IPageBulkExpor
    * @param err error
    * @param pageBulkExportJob PageBulkExportJob executed in the pipeline
    */
-  async handleError(err: Error | null, pageBulkExportJob: PageBulkExportJobDocument) {
+  async handleError(
+    err: Error | null,
+    pageBulkExportJob: PageBulkExportJobDocument,
+  ) {
     if (err == null) return;
 
     if (err instanceof BulkExportJobExpiredError) {
       logger.error(err);
-      await this.notifyExportResultAndCleanUp(SupportedAction.ACTION_PAGE_BULK_EXPORT_JOB_EXPIRED, pageBulkExportJob);
-    }
-    else if (err instanceof BulkExportJobRestartedError) {
+      await this.notifyExportResultAndCleanUp(
+        SupportedAction.ACTION_PAGE_BULK_EXPORT_JOB_EXPIRED,
+        pageBulkExportJob,
+      );
+    } else if (err instanceof BulkExportJobRestartedError) {
       logger.info(err.message);
       await this.cleanUpExportJobResources(pageBulkExportJob);
-    }
-    else {
+    } else {
       logger.error(err);
-      await this.notifyExportResultAndCleanUp(SupportedAction.ACTION_PAGE_BULK_EXPORT_FAILED, pageBulkExportJob);
+      await this.notifyExportResultAndCleanUp(
+        SupportedAction.ACTION_PAGE_BULK_EXPORT_FAILED,
+        pageBulkExportJob,
+      );
     }
   }
 
@@ -203,17 +244,18 @@ class PageBulkExportJobCronService extends CronService implements IPageBulkExpor
    * @param pageBulkExportJob the page bulk export job
    */
   async notifyExportResultAndCleanUp(
-      action: SupportedActionType,
-      pageBulkExportJob: PageBulkExportJobDocument,
+    action: SupportedActionType,
+    pageBulkExportJob: PageBulkExportJobDocument,
   ): Promise<void> {
-    pageBulkExportJob.status = action === SupportedAction.ACTION_PAGE_BULK_EXPORT_COMPLETED
-      ? PageBulkExportJobStatus.completed : PageBulkExportJobStatus.failed;
+    pageBulkExportJob.status =
+      action === SupportedAction.ACTION_PAGE_BULK_EXPORT_COMPLETED
+        ? PageBulkExportJobStatus.completed
+        : PageBulkExportJobStatus.failed;
 
     try {
       await pageBulkExportJob.save();
       await this.notifyExportResult(pageBulkExportJob, action);
-    }
-    catch (err) {
+    } catch (err) {
       logger.error(err);
     }
     // execute independently of notif process resolve/reject
@@ -225,13 +267,15 @@ class PageBulkExportJobCronService extends CronService implements IPageBulkExpor
    * - delete page snapshots
    * - remove the temporal output directory
    */
-  async cleanUpExportJobResources(pageBulkExportJob: PageBulkExportJobDocument, restarted = false) {
+  async cleanUpExportJobResources(
+    pageBulkExportJob: PageBulkExportJobDocument,
+    restarted = false,
+  ) {
     const streamInExecution = this.getStreamInExecution(pageBulkExportJob._id);
     if (streamInExecution != null) {
       if (restarted) {
         streamInExecution.destroy(new BulkExportJobRestartedError());
-      }
-      else {
+      } else {
         streamInExecution.destroy(new BulkExportJobExpiredError());
       }
       this.removeStreamInExecution(pageBulkExportJob._id);
@@ -239,13 +283,19 @@ class PageBulkExportJobCronService extends CronService implements IPageBulkExpor
 
     const promises = [
       PageBulkExportPageSnapshot.deleteMany({ pageBulkExportJob }),
-      fs.promises.rm(this.getTmpOutputDir(pageBulkExportJob), { recursive: true, force: true }),
+      fs.promises.rm(this.getTmpOutputDir(pageBulkExportJob), {
+        recursive: true,
+        force: true,
+      }),
     ];
 
     // clean up html files exported for PDF conversion
     if (pageBulkExportJob.format === PageBulkExportFormat.pdf) {
       promises.push(
-        fs.promises.rm(this.getTmpOutputDir(pageBulkExportJob, true), { recursive: true, force: true }),
+        fs.promises.rm(this.getTmpOutputDir(pageBulkExportJob, true), {
+          recursive: true,
+          force: true,
+        }),
       );
     }
 
@@ -256,7 +306,8 @@ class PageBulkExportJobCronService extends CronService implements IPageBulkExpor
   }
 
   private async notifyExportResult(
-      pageBulkExportJob: PageBulkExportJobDocument, action: SupportedActionType,
+    pageBulkExportJob: PageBulkExportJobDocument,
+    action: SupportedActionType,
   ) {
     const activity = await this.crowi.activityService.createActivity({
       action,
@@ -264,18 +315,26 @@ class PageBulkExportJobCronService extends CronService implements IPageBulkExpor
       target: pageBulkExportJob,
       user: pageBulkExportJob.user,
       snapshot: {
-        username: isPopulated(pageBulkExportJob.user) ? pageBulkExportJob.user.username : '',
+        username: isPopulated(pageBulkExportJob.user)
+          ? pageBulkExportJob.user.username
+          : '',
       },
     });
-    const getAdditionalTargetUsers = async(activity: ActivityDocument) => [activity.user];
-    const preNotify = preNotifyService.generatePreNotify(activity, getAdditionalTargetUsers);
+    const getAdditionalTargetUsers = async (activity: ActivityDocument) => [
+      activity.user,
+    ];
+    const preNotify = preNotifyService.generatePreNotify(
+      activity,
+      getAdditionalTargetUsers,
+    );
     this.activityEvent.emit('updated', activity, pageBulkExportJob, preNotify);
   }
-
 }
 
 // eslint-disable-next-line import/no-mutable-exports
-export let pageBulkExportJobCronService: PageBulkExportJobCronService | undefined; // singleton instance
+export let pageBulkExportJobCronService:
+  | PageBulkExportJobCronService
+  | undefined; // singleton instance
 export default function instanciate(crowi: Crowi): void {
   pageBulkExportJobCronService = new PageBulkExportJobCronService(crowi);
 }

+ 33 - 17
apps/app/src/features/page-bulk-export/server/service/page-bulk-export-job-cron/request-pdf-converter.ts

@@ -1,4 +1,8 @@
-import { PdfCtrlSyncJobStatus202Status, PdfCtrlSyncJobStatusBodyStatus, pdfCtrlSyncJobStatus } from '@growi/pdf-converter-client';
+import {
+  PdfCtrlSyncJobStatus202Status,
+  PdfCtrlSyncJobStatusBodyStatus,
+  pdfCtrlSyncJobStatus,
+} from '@growi/pdf-converter-client';
 
 import { configManager } from '~/server/service/config-manager';
 
@@ -12,7 +16,9 @@ import { BulkExportJobExpiredError } from './errors';
  * Request PDF converter and start pdf convert for the pageBulkExportJob,
  * or sync pdf convert status if already started.
  */
-export async function requestPdfConverter(pageBulkExportJob: PageBulkExportJobDocument): Promise<void> {
+export async function requestPdfConverter(
+  pageBulkExportJob: PageBulkExportJobDocument,
+): Promise<void> {
   const jobCreatedAt = pageBulkExportJob.createdAt;
   if (jobCreatedAt == null) {
     throw new Error('createdAt is not set');
@@ -20,15 +26,24 @@ export async function requestPdfConverter(pageBulkExportJob: PageBulkExportJobDo
 
   const isGrowiCloud = configManager.getConfig('app:growiCloudUri') != null;
   const appId = configManager.getConfig('app:growiAppIdForCloud');
-  if (isGrowiCloud && (appId == null)) {
+  if (isGrowiCloud && appId == null) {
     throw new Error('appId is required for bulk export on GROWI.cloud');
   }
 
-  const exportJobExpirationSeconds = configManager.getConfig('app:bulkExportJobExpirationSeconds');
-  const bulkExportJobExpirationDate = new Date(jobCreatedAt.getTime() + exportJobExpirationSeconds * 1000);
-  let pdfConvertStatus: PdfCtrlSyncJobStatusBodyStatus = PdfCtrlSyncJobStatusBodyStatus.HTML_EXPORT_IN_PROGRESS;
+  const exportJobExpirationSeconds = configManager.getConfig(
+    'app:bulkExportJobExpirationSeconds',
+  );
+  const bulkExportJobExpirationDate = new Date(
+    jobCreatedAt.getTime() + exportJobExpirationSeconds * 1000,
+  );
+  let pdfConvertStatus: PdfCtrlSyncJobStatusBodyStatus =
+    PdfCtrlSyncJobStatusBodyStatus.HTML_EXPORT_IN_PROGRESS;
 
-  const lastExportPagePath = (await PageBulkExportPageSnapshot.findOne({ pageBulkExportJob }).sort({ path: -1 }))?.path;
+  const lastExportPagePath = (
+    await PageBulkExportPageSnapshot.findOne({ pageBulkExportJob }).sort({
+      path: -1,
+    })
+  )?.path;
   if (lastExportPagePath == null) {
     throw new Error('lastExportPagePath is missing');
   }
@@ -46,22 +61,23 @@ export async function requestPdfConverter(pageBulkExportJob: PageBulkExportJobDo
       pdfConvertStatus = PdfCtrlSyncJobStatusBodyStatus.FAILED;
     }
 
-    const res = await pdfCtrlSyncJobStatus({
-      appId,
-      jobId: pageBulkExportJob._id.toString(),
-      expirationDate: bulkExportJobExpirationDate.toISOString(),
-      status: pdfConvertStatus,
-    }, { baseURL: configManager.getConfig('app:pageBulkExportPdfConverterUri') });
+    const res = await pdfCtrlSyncJobStatus(
+      {
+        appId,
+        jobId: pageBulkExportJob._id.toString(),
+        expirationDate: bulkExportJobExpirationDate.toISOString(),
+        status: pdfConvertStatus,
+      },
+      { baseURL: configManager.getConfig('app:pageBulkExportPdfConverterUri') },
+    );
 
     if (res.data.status === PdfCtrlSyncJobStatus202Status.PDF_EXPORT_DONE) {
       pageBulkExportJob.status = PageBulkExportJobStatus.uploading;
       await pageBulkExportJob.save();
-    }
-    else if (res.data.status === PdfCtrlSyncJobStatus202Status.FAILED) {
+    } else if (res.data.status === PdfCtrlSyncJobStatus202Status.FAILED) {
       throw new Error('PDF export failed');
     }
-  }
-  catch (err) {
+  } catch (err) {
     // Only set as failure when host is ready but failed.
     // If host is not ready, the request should be retried on the next cron execution.
     if (!['ENOTFOUND', 'ECONNREFUSED'].includes(err.code)) {

+ 33 - 11
apps/app/src/features/page-bulk-export/server/service/page-bulk-export-job-cron/steps/compress-and-upload.ts

@@ -8,11 +8,12 @@ import type { IAttachmentDocument } from '~/server/models/attachment';
 import { Attachment } from '~/server/models/attachment';
 import type { FileUploader } from '~/server/service/file-uploader';
 import loggerFactory from '~/utils/logger';
-
-import type { IPageBulkExportJobCronService } from '..';
 import type { PageBulkExportJobDocument } from '../../../models/page-bulk-export-job';
+import type { IPageBulkExportJobCronService } from '..';
 
-const logger = loggerFactory('growi:service:page-bulk-export-job-cron:compress-and-upload-async');
+const logger = loggerFactory(
+  'growi:service:page-bulk-export-job-cron:compress-and-upload-async',
+);
 
 function setUpPageArchiver(): Archiver {
   const pageArchiver = archiver('tar', {
@@ -29,7 +30,10 @@ function setUpPageArchiver(): Archiver {
 }
 
 async function postProcess(
-    this: IPageBulkExportJobCronService, pageBulkExportJob: PageBulkExportJobDocument, attachment: IAttachmentDocument, fileSize: number,
+  this: IPageBulkExportJobCronService,
+  pageBulkExportJob: PageBulkExportJobDocument,
+  attachment: IAttachmentDocument,
+  fileSize: number,
 ): Promise<void> {
   attachment.fileSize = fileSize;
   await attachment.save();
@@ -40,18 +44,33 @@ async function postProcess(
   await pageBulkExportJob.save();
 
   this.removeStreamInExecution(pageBulkExportJob._id);
-  await this.notifyExportResultAndCleanUp(SupportedAction.ACTION_PAGE_BULK_EXPORT_COMPLETED, pageBulkExportJob);
+  await this.notifyExportResultAndCleanUp(
+    SupportedAction.ACTION_PAGE_BULK_EXPORT_COMPLETED,
+    pageBulkExportJob,
+  );
 }
 
 /**
  * Execute a pipeline that reads the page files from the temporal fs directory, compresses them, and uploads to the cloud storage
  */
-export async function compressAndUpload(this: IPageBulkExportJobCronService, user, pageBulkExportJob: PageBulkExportJobDocument): Promise<void> {
+export async function compressAndUpload(
+  this: IPageBulkExportJobCronService,
+  user,
+  pageBulkExportJob: PageBulkExportJobDocument,
+): Promise<void> {
   const pageArchiver = setUpPageArchiver();
 
-  if (pageBulkExportJob.revisionListHash == null) throw new Error('revisionListHash is not set');
+  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 attachment = Attachment.createWithoutSave(
+    null,
+    user,
+    originalName,
+    this.compressExtension,
+    0,
+    AttachmentType.PAGE_BULK_EXPORT,
+  );
 
   const fileUploadService: FileUploader = this.crowi.fileUploadService;
 
@@ -61,10 +80,13 @@ export async function compressAndUpload(this: IPageBulkExportJobCronService, use
 
   try {
     await fileUploadService.uploadAttachment(pageArchiver, attachment);
-  }
-  catch (e) {
+  } catch (e) {
     logger.error(e);
     this.handleError(e, pageBulkExportJob);
   }
-  await postProcess.bind(this)(pageBulkExportJob, attachment, pageArchiver.pointer());
+  await postProcess.bind(this)(
+    pageBulkExportJob,
+    attachment,
+    pageArchiver.pointer(),
+  );
 }

+ 22 - 17
apps/app/src/features/page-bulk-export/server/service/page-bulk-export-job-cron/steps/create-page-snapshots-async.ts

@@ -1,20 +1,21 @@
-import { createHash } from 'crypto';
-import { Writable, pipeline } from 'stream';
-
-import { getIdForRef, getIdStringForRef } from '@growi/core';
 import type { IPage } from '@growi/core';
+import { getIdForRef, getIdStringForRef } from '@growi/core';
+import { createHash } from 'crypto';
 import mongoose from 'mongoose';
+import { pipeline, Writable } from 'stream';
 
 import { PageBulkExportJobStatus } from '~/features/page-bulk-export/interfaces/page-bulk-export';
 import { SupportedAction } from '~/interfaces/activity';
 import type { PageDocument, PageModel } from '~/server/models/page';
-
-import type { IPageBulkExportJobCronService } from '..';
 import type { PageBulkExportJobDocument } from '../../../models/page-bulk-export-job';
 import PageBulkExportJob from '../../../models/page-bulk-export-job';
 import PageBulkExportPageSnapshot from '../../../models/page-bulk-export-page-snapshot';
+import type { IPageBulkExportJobCronService } from '..';
 
-async function reuseDuplicateExportIfExists(this: IPageBulkExportJobCronService, pageBulkExportJob: PageBulkExportJobDocument) {
+async function reuseDuplicateExportIfExists(
+  this: IPageBulkExportJobCronService,
+  pageBulkExportJob: PageBulkExportJobDocument,
+) {
   const duplicateExportJob = await PageBulkExportJob.findOne({
     user: pageBulkExportJob.user,
     page: pageBulkExportJob.page,
@@ -28,7 +29,10 @@ async function reuseDuplicateExportIfExists(this: IPageBulkExportJobCronService,
     pageBulkExportJob.status = PageBulkExportJobStatus.completed;
     await pageBulkExportJob.save();
 
-    await this.notifyExportResultAndCleanUp(SupportedAction.ACTION_PAGE_BULK_EXPORT_COMPLETED, pageBulkExportJob);
+    await this.notifyExportResultAndCleanUp(
+      SupportedAction.ACTION_PAGE_BULK_EXPORT_COMPLETED,
+      pageBulkExportJob,
+    );
   }
 }
 
@@ -36,7 +40,11 @@ async function reuseDuplicateExportIfExists(this: IPageBulkExportJobCronService,
  * Start a pipeline that creates a snapshot for each page that is to be exported in the pageBulkExportJob.
  * 'revisionListHash' is calulated and saved to the pageBulkExportJob at the end of the pipeline.
  */
-export async function createPageSnapshotsAsync(this: IPageBulkExportJobCronService, user, pageBulkExportJob: PageBulkExportJobDocument): Promise<void> {
+export async function createPageSnapshotsAsync(
+  this: IPageBulkExportJobCronService,
+  user,
+  pageBulkExportJob: PageBulkExportJobDocument,
+): Promise<void> {
   const Page = mongoose.model<IPage, PageModel>('Page');
 
   // if the process of creating snapshots was interrupted, delete the snapshots and create from the start
@@ -54,15 +62,14 @@ export async function createPageSnapshotsAsync(this: IPageBulkExportJobCronServi
   const builder = await new PageQueryBuilder(Page.find())
     .addConditionToListWithDescendants(basePage.path)
     .addViewerCondition(user);
-  const pagesReadable = builder
-    .query
+  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) => {
+    write: async (page: PageDocument, encoding, callback) => {
       try {
         if (page.revision != null) {
           revisionListHash.update(getIdStringForRef(page.revision));
@@ -72,22 +79,20 @@ export async function createPageSnapshotsAsync(this: IPageBulkExportJobCronServi
           path: page.path,
           revision: page.revision,
         });
-      }
-      catch (err) {
+      } catch (err) {
         callback(err);
         return;
       }
       callback();
     },
-    final: async(callback) => {
+    final: async (callback) => {
       try {
         pageBulkExportJob.revisionListHash = revisionListHash.digest('hex');
         pageBulkExportJob.status = PageBulkExportJobStatus.exporting;
         await pageBulkExportJob.save();
 
         await reuseDuplicateExportIfExists.bind(this)(pageBulkExportJob);
-      }
-      catch (err) {
+      } catch (err) {
         callback(err);
         return;
       }

+ 61 - 34
apps/app/src/features/page-bulk-export/server/service/page-bulk-export-job-cron/steps/export-pages-to-fs-async.ts

@@ -1,26 +1,31 @@
-import fs from 'fs';
-import path from 'path';
-import { Writable, pipeline } from 'stream';
-
 import { dynamicImport } from '@cspell/dynamic-import';
 import { isPopulated } from '@growi/core';
-import { getParentPath, normalizePath } from '@growi/core/dist/utils/path-utils';
+import {
+  getParentPath,
+  normalizePath,
+} from '@growi/core/dist/utils/path-utils';
+import fs from 'fs';
 import type { Root } from 'mdast';
+import path from 'path';
 import type * as RemarkHtml from 'remark-html';
 import type * as RemarkParse from 'remark-parse';
+import { pipeline, Writable } from 'stream';
 import type * as Unified from 'unified';
 
-import { PageBulkExportFormat, PageBulkExportJobStatus } from '~/features/page-bulk-export/interfaces/page-bulk-export';
-
-import type { IPageBulkExportJobCronService } from '..';
+import {
+  PageBulkExportFormat,
+  PageBulkExportJobStatus,
+} from '~/features/page-bulk-export/interfaces/page-bulk-export';
 import type { PageBulkExportJobDocument } 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 type { IPageBulkExportJobCronService } from '..';
 
-async function convertMdToHtml(md: string, htmlConverter: Unified.Processor<Root, undefined, undefined, Root, string>): Promise<string> {
-  const htmlString = (await htmlConverter
-    .process(md))
-    .toString();
+async function convertMdToHtml(
+  md: string,
+  htmlConverter: Unified.Processor<Root, undefined, undefined, Root, string>,
+): Promise<string> {
+  const htmlString = (await htmlConverter.process(md)).toString();
 
   return htmlString;
 }
@@ -28,13 +33,24 @@ async function convertMdToHtml(md: string, htmlConverter: Unified.Processor<Root
 /**
  * Get a Writable that writes the page body temporarily to fs
  */
-async function getPageWritable(this: IPageBulkExportJobCronService, pageBulkExportJob: PageBulkExportJobDocument): Promise<Writable> {
-  const unified = (await dynamicImport<typeof Unified>('unified', __dirname)).unified;
-  const remarkParse = (await dynamicImport<typeof RemarkParse>('remark-parse', __dirname)).default;
-  const remarkHtml = (await dynamicImport<typeof RemarkHtml>('remark-html', __dirname)).default;
+async function getPageWritable(
+  this: IPageBulkExportJobCronService,
+  pageBulkExportJob: PageBulkExportJobDocument,
+): Promise<Writable> {
+  const unified = (await dynamicImport<typeof Unified>('unified', __dirname))
+    .unified;
+  const remarkParse = (
+    await dynamicImport<typeof RemarkParse>('remark-parse', __dirname)
+  ).default;
+  const remarkHtml = (
+    await dynamicImport<typeof RemarkHtml>('remark-html', __dirname)
+  ).default;
 
   const isHtmlPath = pageBulkExportJob.format === PageBulkExportFormat.pdf;
-  const format = pageBulkExportJob.format === PageBulkExportFormat.pdf ? 'html' : pageBulkExportJob.format;
+  const format =
+    pageBulkExportJob.format === PageBulkExportFormat.pdf
+      ? 'html'
+      : pageBulkExportJob.format;
   const outputDir = this.getTmpOutputDir(pageBulkExportJob, isHtmlPath);
   // define before the stream starts to avoid creating multiple instances
   const htmlConverter = unified()
@@ -43,7 +59,11 @@ async function getPageWritable(this: IPageBulkExportJobCronService, pageBulkExpo
     .use(remarkHtml);
   return new Writable({
     objectMode: true,
-    write: async(page: PageBulkExportPageSnapshotDocument, encoding, callback) => {
+    write: async (
+      page: PageBulkExportPageSnapshotDocument,
+      encoding,
+      callback,
+    ) => {
       try {
         const revision = page.revision;
 
@@ -56,22 +76,23 @@ async function getPageWritable(this: IPageBulkExportJobCronService, pageBulkExpo
           await fs.promises.mkdir(fileOutputParentPath, { recursive: true });
           if (pageBulkExportJob.format === PageBulkExportFormat.md) {
             await fs.promises.writeFile(fileOutputPath, markdownBody);
-          }
-          else {
-            const htmlString = await convertMdToHtml(markdownBody, htmlConverter);
+          } else {
+            const htmlString = await convertMdToHtml(
+              markdownBody,
+              htmlConverter,
+            );
             await fs.promises.writeFile(fileOutputPath, htmlString);
           }
           pageBulkExportJob.lastExportedPagePath = page.path;
           await pageBulkExportJob.save();
         }
-      }
-      catch (err) {
+      } catch (err) {
         callback(err);
         return;
       }
       callback();
     },
-    final: async(callback) => {
+    final: async (callback) => {
       try {
         // If the format is md, the export process ends here.
         // If the format is pdf, pdf conversion in pdf-converter has to finish.
@@ -79,8 +100,7 @@ async function getPageWritable(this: IPageBulkExportJobCronService, pageBulkExpo
           pageBulkExportJob.status = PageBulkExportJobStatus.uploading;
           await pageBulkExportJob.save();
         }
-      }
-      catch (err) {
+      } catch (err) {
         callback(err);
         return;
       }
@@ -93,14 +113,21 @@ async function getPageWritable(this: IPageBulkExportJobCronService, pageBulkExpo
  * 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.
  */
-export async function exportPagesToFsAsync(this: IPageBulkExportJobCronService, 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()
+export async function exportPagesToFsAsync(
+  this: IPageBulkExportJobCronService,
+  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 = await getPageWritable.bind(this)(pageBulkExportJob);

+ 53 - 26
apps/app/src/features/page-bulk-export/server/service/page-bulk-export.ts

@@ -1,78 +1,105 @@
-import {
-  type IPage, SubscriptionStatusType,
-} from '@growi/core';
+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 type { PageBulkExportFormat } from '../../interfaces/page-bulk-export';
-import { PageBulkExportJobInProgressStatus, PageBulkExportJobStatus } from '../../interfaces/page-bulk-export';
+import {
+  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>;
+  createOrResetBulkExportJob: (
+    basePagePath: string,
+    currentUser,
+    restartJob?: boolean,
+  ) => Promise<void>;
 }
 
 class PageBulkExportService implements IPageBulkExportService {
-
   /**
    * Create a new page bulk export job or reset the existing one
    */
-  async createOrResetBulkExportJob(basePagePath: string, format: PageBulkExportFormat, currentUser, restartJob = false): Promise<void> {
+  async createOrResetBulkExportJob(
+    basePagePath: string,
+    format: PageBulkExportFormat,
+    currentUser,
+    restartJob = false,
+  ): Promise<void> {
     const Page = mongoose.model<IPage, PageModel>('Page');
-    const basePage = await Page.findByPathAndViewer(basePagePath, currentUser, null, true);
+    const basePage = await Page.findByPathAndViewer(
+      basePagePath,
+      currentUser,
+      null,
+      true,
+    );
 
     if (basePage == null) {
       throw new Error('Base page not found or not accessible');
     }
 
-    const duplicatePageBulkExportJobInProgress: HydratedDocument<PageBulkExportJobDocument> | null = await PageBulkExportJob.findOne({
-      user: { $eq: currentUser },
-      page: basePage,
-      format: { $eq: format },
-      $or: Object.values(PageBulkExportJobInProgressStatus).map(status => ({ status })),
-    });
+    const duplicatePageBulkExportJobInProgress: HydratedDocument<PageBulkExportJobDocument> | null =
+      await PageBulkExportJob.findOne({
+        user: { $eq: currentUser },
+        page: basePage,
+        format: { $eq: format },
+        $or: Object.values(PageBulkExportJobInProgressStatus).map((status) => ({
+          status,
+        })),
+      });
     if (duplicatePageBulkExportJobInProgress != null) {
       if (restartJob) {
         this.resetBulkExportJob(duplicatePageBulkExportJobInProgress);
         return;
       }
-      throw new DuplicateBulkExportJobError(duplicatePageBulkExportJobInProgress);
+      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);
+    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> {
+  async resetBulkExportJob(
+    pageBulkExportJob: HydratedDocument<PageBulkExportJobDocument>,
+  ): Promise<void> {
     pageBulkExportJob.restartFlag = true;
     await pageBulkExportJob.save();
   }
-
 }
 
-export const pageBulkExportService: PageBulkExportService = new PageBulkExportService(); // singleton instance
+export const pageBulkExportService: PageBulkExportService =
+  new PageBulkExportService(); // singleton instance

+ 9 - 9
apps/app/src/interfaces/access-token.ts

@@ -1,16 +1,16 @@
 import type { Scope } from '@growi/core/dist/interfaces';
 
 export type IAccessTokenInfo = {
-  expiredAt: Date,
-  description: string,
-  scopes: Scope[],
-}
+  expiredAt: Date;
+  description: string;
+  scopes: Scope[];
+};
 
 export type IResGenerateAccessToken = IAccessTokenInfo & {
-  token: string,
-  _id: string,
-}
+  token: string;
+  _id: string;
+};
 
 export type IResGetAccessToken = IAccessTokenInfo & {
-  _id: string,
-}
+  _id: string;
+};

+ 126 - 65
apps/app/src/interfaces/activity.ts

@@ -1,4 +1,4 @@
-import type { Ref, HasObjectId, IUser } from '@growi/core';
+import type { HasObjectId, IUser, Ref } from '@growi/core';
 
 // Model
 const MODEL_PAGE = 'Page';
@@ -8,7 +8,8 @@ const MODEL_PAGE_BULK_EXPORT_JOB = 'PageBulkExportJob';
 
 // Action
 const ACTION_UNSETTLED = 'UNSETTLED';
-const ACTION_USER_REGISTRATION_APPROVAL_REQUEST = 'USER_REGISTRATION_APPROVAL_REQUEST';
+const ACTION_USER_REGISTRATION_APPROVAL_REQUEST =
+  'USER_REGISTRATION_APPROVAL_REQUEST';
 const ACTION_USER_REGISTRATION_SUCCESS = 'USER_REGISTRATION_SUCCESS';
 const ACTION_USER_LOGIN_WITH_LOCAL = 'USER_LOGIN_WITH_LOCAL';
 const ACTION_USER_LOGIN_WITH_LDAP = 'USER_LOGIN_WITH_LDAP';
@@ -28,7 +29,8 @@ const ACTION_USER_PASSWORD_UPDATE = 'USER_PASSWORD_UPDATE';
 const ACTION_USER_ACCESS_TOKEN_CREATE = 'USER_ACCESS_TOKEN_CREATE';
 const ACTION_USER_ACCESS_TOKEN_DELETE = 'USER_ACCESS_TOKEN_DELETE';
 const ACTION_USER_EDITOR_SETTINGS_UPDATE = 'USER_EDITOR_SETTINGS_UPDATE';
-const ACTION_USER_IN_APP_NOTIFICATION_SETTINGS_UPDATE = 'USER_IN_APP_NOTIFICATION_SETTINGS_UPDATE';
+const ACTION_USER_IN_APP_NOTIFICATION_SETTINGS_UPDATE =
+  'USER_IN_APP_NOTIFICATION_SETTINGS_UPDATE';
 const ACTION_PAGE_VIEW = 'PAGE_VIEW';
 const ACTION_PAGE_USER_HOME_VIEW = 'PAGE_USER_HOME_VIEW';
 const ACTION_PAGE_NOT_FOUND = 'PAGE_NOT_FOUND';
@@ -48,7 +50,8 @@ const ACTION_PAGE_REVERT = 'PAGE_REVERT';
 const ACTION_PAGE_EMPTY_TRASH = 'PAGE_EMPTY_TRASH';
 const ACTION_PAGE_RECURSIVELY_RENAME = 'PAGE_RECURSIVELY_RENAME';
 const ACTION_PAGE_RECURSIVELY_DELETE = 'PAGE_RECURSIVELY_DELETE';
-const ACTION_PAGE_RECURSIVELY_DELETE_COMPLETELY = 'PAGE_RECURSIVELY_DELETE_COMPLETELY';
+const ACTION_PAGE_RECURSIVELY_DELETE_COMPLETELY =
+  'PAGE_RECURSIVELY_DELETE_COMPLETELY';
 const ACTION_PAGE_RECURSIVELY_REVERT = 'PAGE_RECURSIVELY_REVERT';
 const ACTION_PAGE_SUBSCRIBE = 'PAGE_SUBSCRIBE';
 const ACTION_PAGE_UNSUBSCRIBE = 'PAGE_UNSUBSCRIBE';
@@ -57,7 +60,8 @@ const ACTION_PAGE_BULK_EXPORT_COMPLETED = 'PAGE_BULK_EXPORT_COMPLETED';
 const ACTION_PAGE_BULK_EXPORT_FAILED = 'PAGE_BULK_EXPORT_FAILED';
 const ACTION_PAGE_BULK_EXPORT_JOB_EXPIRED = 'PAGE_BULK_EXPORT_JOB_EXPIRED';
 const ACTION_TAG_UPDATE = 'TAG_UPDATE';
-const ACTION_IN_APP_NOTIFICATION_ALL_STATUSES_OPEN = 'IN_APP_NOTIFICATION_ALL_STATUSES_OPEN';
+const ACTION_IN_APP_NOTIFICATION_ALL_STATUSES_OPEN =
+  'IN_APP_NOTIFICATION_ALL_STATUSES_OPEN';
 const ACTION_COMMENT_CREATE = 'COMMENT_CREATE';
 const ACTION_COMMENT_UPDATE = 'COMMENT_UPDATE';
 const ACTION_COMMENT_REMOVE = 'COMMENT_REMOVE';
@@ -78,8 +82,10 @@ const ACTION_ADMIN_SITE_URL_UPDATE = 'ADMIN_SITE_URL_UPDATE';
 const ACTION_ADMIN_MAIL_SMTP_UPDATE = 'ADMIN_MAIL_SMTP_UPDATE';
 const ACTION_ADMIN_MAIL_SES_UPDATE = 'ADMIN_MAIL_SES_UPDATE';
 const ACTION_ADMIN_MAIL_TEST_SUBMIT = 'ADMIN_MAIL_TEST_SUBMIT';
-const ACTION_ADMIN_FILE_UPLOAD_CONFIG_UPDATE = 'ADMIN_FILE_UPLOAD_CONFIG_UPDATE';
-const ACTION_ADMIN_PAGE_BULK_EXPORT_SETTINGS_UPDATE = 'ADMIN_PAGE_BULK_EXPORT_SETTINGS_UPDATE';
+const ACTION_ADMIN_FILE_UPLOAD_CONFIG_UPDATE =
+  'ADMIN_FILE_UPLOAD_CONFIG_UPDATE';
+const ACTION_ADMIN_PAGE_BULK_EXPORT_SETTINGS_UPDATE =
+  'ADMIN_PAGE_BULK_EXPORT_SETTINGS_UPDATE';
 const ACTION_ADMIN_MAINTENANCEMODE_ENABLED = 'ADMIN_MAINTENANCEMODE_ENABLED';
 const ACTION_ADMIN_MAINTENANCEMODE_DISABLED = 'ADMIN_MAINTENANCEMODE_DISABLED';
 const ACTION_ADMIN_SECURITY_SETTINGS_UPDATE = 'ADMIN_SECURITY_SETTINGS_UPDATE';
@@ -103,9 +109,11 @@ const ACTION_ADMIN_AUTH_GOOGLE_UPDATE = 'ADMIN_AUTH_GOOGLE_UPDATE';
 const ACTION_ADMIN_AUTH_GITHUB_ENABLED = 'ADMIN_AUTH_GITHUB_ENABLED';
 const ACTION_ADMIN_AUTH_GITHUB_DISABLED = 'ADMIN_AUTH_GITHUB_DISABLED';
 const ACTION_ADMIN_AUTH_GITHUB_UPDATE = 'ADMIN_AUTH_GITHUB_UPDATE';
-const ACTION_ADMIN_MARKDOWN_LINE_BREAK_UPDATE = 'ADMIN_MARKDOWN_LINE_BREAK_UPDATE';
+const ACTION_ADMIN_MARKDOWN_LINE_BREAK_UPDATE =
+  'ADMIN_MARKDOWN_LINE_BREAK_UPDATE';
 const ACTION_ADMIN_MARKDOWN_INDENT_UPDATE = 'ADMIN_MARKDOWN_INDENT_UPDATE';
-const ACTION_ADMIN_MARKDOWN_PRESENTATION_UPDATE = 'ADMIN_MARKDOWN_PRESENTATION_UPDATE';
+const ACTION_ADMIN_MARKDOWN_PRESENTATION_UPDATE =
+  'ADMIN_MARKDOWN_PRESENTATION_UPDATE';
 const ACTION_ADMIN_MARKDOWN_XSS_UPDATE = 'ADMIN_MARKDOWN_XSS_UPDATE';
 const ACTION_ADMIN_LAYOUT_UPDATE = 'ADMIN_LAYOUT_UPDATE';
 const ACTION_ADMIN_THEME_UPDATE = 'ADMIN_THEME_UPDATE';
@@ -118,37 +126,52 @@ const ACTION_ADMIN_CUSTOM_CSS_UPDATE = 'ADMIN_CUSTOM_CSS_UPDATE';
 const ACTION_ADMIN_CUSTOM_SCRIPT_UPDATE = 'ADMIN_CUSTOM_SCRIPT_UPDATE';
 const ACTION_ADMIN_ARCHIVE_DATA_UPLOAD = 'ADMIN_ARCHIVE_DATA_UPLOAD';
 const ACTION_ADMIN_GROWI_DATA_IMPORTED = 'ADMIN_GROWI_DATA_IMPORTED';
-const ACTION_ADMIN_UPLOADED_GROWI_DATA_DISCARDED = 'ADMIN_UPLOADED_GROWI_DATA_DISCARDED';
+const ACTION_ADMIN_UPLOADED_GROWI_DATA_DISCARDED =
+  'ADMIN_UPLOADED_GROWI_DATA_DISCARDED';
 const ACTION_ADMIN_ESA_DATA_IMPORTED = 'ADMIN_ESA_DATA_IMPORTED';
 const ACTION_ADMIN_ESA_DATA_UPDATED = 'ADMIN_ESA_DATA_UPDATED';
-const ACTION_ADMIN_CONNECTION_TEST_OF_ESA_DATA = 'ADMIN_CONNECTION_TEST_OF_ESA_DATA';
+const ACTION_ADMIN_CONNECTION_TEST_OF_ESA_DATA =
+  'ADMIN_CONNECTION_TEST_OF_ESA_DATA';
 const ACTION_ADMIN_QIITA_DATA_IMPORTED = 'ADMIN_QIITA_DATA_IMPORTED';
 const ACTION_ADMIN_QIITA_DATA_UPDATED = 'ADMIN_QIITA_DATA_UPDATED';
-const ACTION_ADMIN_CONNECTION_TEST_OF_QIITA_DATA = 'ADMIN_CONNECTION_TEST_OF_QIITA_DATA';
+const ACTION_ADMIN_CONNECTION_TEST_OF_QIITA_DATA =
+  'ADMIN_CONNECTION_TEST_OF_QIITA_DATA';
 const ACTION_ADMIN_ARCHIVE_DATA_CREATE = 'ADMIN_ARCHIVE_DATA_CREATE';
 const ACTION_ADMIN_ARCHIVE_DATA_DOWNLOAD = 'ADMIN_ARCHIVE_DATA_DOWNLOAD';
 const ACTION_ADMIN_ARCHIVE_DATA_DELETE = 'ADMIN_ARCHIVE_DATA_DELETE';
-const ACTION_ADMIN_USER_NOTIFICATION_SETTINGS_ADD = 'ADMIN_USER_NOTIFICATION_SETTINGS_ADD';
-const ACTION_ADMIN_USER_NOTIFICATION_SETTINGS_DELETE = 'ADMIN_USER_NOTIFICATION_SETTINGS_DELETE';
-const ACTION_ADMIN_GLOBAL_NOTIFICATION_SETTINGS_ADD = 'ADMIN_GLOBAL_NOTIFICATION_SETTINGS_ADD';
-const ACTION_ADMIN_GLOBAL_NOTIFICATION_SETTINGS_UPDATE = 'ADMIN_GLOBAL_NOTIFICATION_SETTINGS_UPDATE';
-const ACTION_ADMIN_NOTIFICATION_GRANT_SETTINGS_UPDATE = 'ADMIN_NOTIFICATION_GRANT_SETTINGS_UPDATE';
-const ACTION_ADMIN_GLOBAL_NOTIFICATION_SETTINGS_ENABLED = 'ADMIN_GLOBAL_NOTIFICATION_SETTINGS_ENABLED';
-const ACTION_ADMIN_GLOBAL_NOTIFICATION_SETTINGS_DISABLED = 'ADMIN_GLOBAL_NOTIFICATION_SETTINGS_DISABLED';
-const ACTION_ADMIN_GLOBAL_NOTIFICATION_SETTINGS_DELETE = 'ADMIN_GLOBAL_NOTIFICATION_SETTINGS_DELETE';
+const ACTION_ADMIN_USER_NOTIFICATION_SETTINGS_ADD =
+  'ADMIN_USER_NOTIFICATION_SETTINGS_ADD';
+const ACTION_ADMIN_USER_NOTIFICATION_SETTINGS_DELETE =
+  'ADMIN_USER_NOTIFICATION_SETTINGS_DELETE';
+const ACTION_ADMIN_GLOBAL_NOTIFICATION_SETTINGS_ADD =
+  'ADMIN_GLOBAL_NOTIFICATION_SETTINGS_ADD';
+const ACTION_ADMIN_GLOBAL_NOTIFICATION_SETTINGS_UPDATE =
+  'ADMIN_GLOBAL_NOTIFICATION_SETTINGS_UPDATE';
+const ACTION_ADMIN_NOTIFICATION_GRANT_SETTINGS_UPDATE =
+  'ADMIN_NOTIFICATION_GRANT_SETTINGS_UPDATE';
+const ACTION_ADMIN_GLOBAL_NOTIFICATION_SETTINGS_ENABLED =
+  'ADMIN_GLOBAL_NOTIFICATION_SETTINGS_ENABLED';
+const ACTION_ADMIN_GLOBAL_NOTIFICATION_SETTINGS_DISABLED =
+  'ADMIN_GLOBAL_NOTIFICATION_SETTINGS_DISABLED';
+const ACTION_ADMIN_GLOBAL_NOTIFICATION_SETTINGS_DELETE =
+  'ADMIN_GLOBAL_NOTIFICATION_SETTINGS_DELETE';
 const ACTION_ADMIN_SLACK_WORKSPACE_CREATE = 'ADMIN_SLACK_WORKSPACE_CREATE';
 const ACTION_ADMIN_SLACK_WORKSPACE_DELETE = 'ADMIN_SLACK_WORKSPACE_DELETE';
 const ACTION_ADMIN_SLACK_BOT_TYPE_UPDATE = 'ADMIN_SLACK_BOT_TYPE_UPDATE';
 const ACTION_ADMIN_SLACK_BOT_TYPE_DELETE = 'ADMIN_SLACK_BOT_TYPE_DELETE';
-const ACTION_ADMIN_SLACK_ACCESS_TOKEN_REGENERATE = 'ADMIN_SLACK_ACCESS_TOKEN_REGENERATE';
+const ACTION_ADMIN_SLACK_ACCESS_TOKEN_REGENERATE =
+  'ADMIN_SLACK_ACCESS_TOKEN_REGENERATE';
 const ACTION_ADMIN_SLACK_MAKE_APP_PRIMARY = 'ADMIN_SLACK_MAKE_APP_PRIMARY';
 const ACTION_ADMIN_SLACK_PERMISSION_UPDATE = 'ADMIN_SLACK_PERMISSION_UPDATE';
 const ACTION_ADMIN_SLACK_PROXY_URI_UPDATE = 'ADMIN_SLACK_PROXY_URI_UPDATE';
 const ACTION_ADMIN_SLACK_RELATION_TEST = 'ADMIN_SLACK_RELATION_TEST';
-const ACTION_ADMIN_SLACK_WITHOUT_PROXY_SETTINGS_UPDATE = 'ADMIN_SLACK_WITHOUT_PROXY_SETTINGS_UPDATE';
-const ACTION_ADMIN_SLACK_WITHOUT_PROXY_PERMISSION_UPDATE = 'ADMIN_SLACK_WITHOUT_PROXY_PERMISSION_UPDATE';
+const ACTION_ADMIN_SLACK_WITHOUT_PROXY_SETTINGS_UPDATE =
+  'ADMIN_SLACK_WITHOUT_PROXY_SETTINGS_UPDATE';
+const ACTION_ADMIN_SLACK_WITHOUT_PROXY_PERMISSION_UPDATE =
+  'ADMIN_SLACK_WITHOUT_PROXY_PERMISSION_UPDATE';
 const ACTION_ADMIN_SLACK_WITHOUT_PROXY_TEST = 'ADMIN_SLACK_WITHOUT_PROXY_TEST';
-const ACTION_ADMIN_SLACK_CONFIGURATION_SETTING_UPDATE = 'ADMIN_SLACK_CONFIGURATION_SETTING_UPDATE';
+const ACTION_ADMIN_SLACK_CONFIGURATION_SETTING_UPDATE =
+  'ADMIN_SLACK_CONFIGURATION_SETTING_UPDATE';
 const ACTION_ADMIN_USERS_INVITE = 'ADMIN_USERS_INVITE';
 const ACTION_ADMIN_USERS_PASSWORD_RESET = 'ADMIN_USERS_PASSWORD_RESET';
 const ACTION_ADMIN_USERS_ACTIVATE = 'ADMIN_USERS_ACTIVATE';
@@ -157,7 +180,8 @@ const ACTION_ADMIN_USERS_REVOKE_ADMIN = 'ADMIN_USERS_REVOKE_ADMIN';
 const ACTION_ADMIN_USERS_GRANT_READ_ONLY = 'ADMIN_USERS_GRANT_READ_ONLY';
 const ACTION_ADMIN_USERS_REVOKE_READ_ONLY = 'ADMIN_USERS_REVOKE_READ_ONLY';
 const ACTION_ADMIN_USERS_DEACTIVATE = 'ADMIN_USERS_DEACTIVATE';
-const ACTION_ADMIN_USERS_SEND_INVITATION_EMAIL = 'ADMIN_USERS_SEND_INVITATION_EMAIL';
+const ACTION_ADMIN_USERS_SEND_INVITATION_EMAIL =
+  'ADMIN_USERS_SEND_INVITATION_EMAIL';
 const ACTION_ADMIN_USERS_REMOVE = 'ADMIN_USERS_REMOVE';
 const ACTION_ADMIN_USER_GROUP_CREATE = 'ADMIN_USER_GROUP_CREATE';
 const ACTION_ADMIN_USER_GROUP_UPDATE = 'ADMIN_USER_GROUP_UPDATE';
@@ -167,7 +191,6 @@ const ACTION_ADMIN_SEARCH_CONNECTION = 'ADMIN_SEARCH_CONNECTION';
 const ACTION_ADMIN_SEARCH_INDICES_NORMALIZE = 'ADMIN_SEARCH_INDICES_NORMALIZE';
 const ACTION_ADMIN_SEARCH_INDICES_REBUILD = 'ADMIN_SEARCH_INDICES_REBUILD';
 
-
 export const SupportedTargetModel = {
   MODEL_PAGE,
   MODEL_USER,
@@ -378,7 +401,8 @@ export const ActionGroupSize = {
   Medium: 'MEDIUM',
   Large: 'LARGE',
 } as const;
-export type ActionGroupSize = typeof ActionGroupSize[keyof typeof ActionGroupSize];
+export type ActionGroupSize =
+  (typeof ActionGroupSize)[keyof typeof ActionGroupSize];
 
 export const SmallActionGroup = {
   ACTION_USER_LOGIN_WITH_LOCAL,
@@ -544,7 +568,6 @@ export const LargeActionGroup = {
   ACTION_ADMIN_SEARCH_INDICES_REBUILD,
 } as const;
 
-
 /*
  * Array
  */
@@ -557,53 +580,91 @@ export const AllMediumGroupActions = Object.values(MediumActionGroup);
 export const AllLargeGroupActions = Object.values(LargeActionGroup);
 
 // Action categories(for SelectActionDropdown.tsx)
-const pageRegExp = new RegExp(`^${SupportedActionCategory.PAGE.toUpperCase()}_`);
-const commentRegExp = new RegExp(`^${SupportedActionCategory.COMMENT.toUpperCase()}_`);
+const pageRegExp = new RegExp(
+  `^${SupportedActionCategory.PAGE.toUpperCase()}_`,
+);
+const commentRegExp = new RegExp(
+  `^${SupportedActionCategory.COMMENT.toUpperCase()}_`,
+);
 const tagRegExp = new RegExp(`^${SupportedActionCategory.TAG.toUpperCase()}_`);
-const attachmentRegExp = RegExp(`^${SupportedActionCategory.ATTACHMENT.toUpperCase()}_`);
-const shareLinkRegExp = RegExp(`^${SupportedActionCategory.SHARE_LINK.toUpperCase()}_`);
-const inAppNotificationRegExp = RegExp(`^${SupportedActionCategory.IN_APP_NOTIFICATION.toUpperCase()}_`);
-const searchRegExp = RegExp(`^${SupportedActionCategory.SEARCH.toUpperCase()}_`);
-const userRegExp = new RegExp(`^${SupportedActionCategory.USER.toUpperCase()}_`);
-const adminRegExp = new RegExp(`^${SupportedActionCategory.ADMIN.toUpperCase()}_`);
+const attachmentRegExp = RegExp(
+  `^${SupportedActionCategory.ATTACHMENT.toUpperCase()}_`,
+);
+const shareLinkRegExp = RegExp(
+  `^${SupportedActionCategory.SHARE_LINK.toUpperCase()}_`,
+);
+const inAppNotificationRegExp = RegExp(
+  `^${SupportedActionCategory.IN_APP_NOTIFICATION.toUpperCase()}_`,
+);
+const searchRegExp = RegExp(
+  `^${SupportedActionCategory.SEARCH.toUpperCase()}_`,
+);
+const userRegExp = new RegExp(
+  `^${SupportedActionCategory.USER.toUpperCase()}_`,
+);
+const adminRegExp = new RegExp(
+  `^${SupportedActionCategory.ADMIN.toUpperCase()}_`,
+);
 
-export const PageActions = AllSupportedActions.filter(action => action.match(pageRegExp));
-export const CommentActions = AllSupportedActions.filter(action => action.match(commentRegExp));
-export const TagActions = AllSupportedActions.filter(action => action.match(tagRegExp));
-export const AttachmentActions = AllSupportedActions.filter(action => action.match(attachmentRegExp));
-export const ShareLinkActions = AllSupportedActions.filter(action => action.match(shareLinkRegExp));
-export const InAppNotificationActions = AllSupportedActions.filter(action => action.match(inAppNotificationRegExp));
-export const SearchActions = AllSupportedActions.filter(action => action.match(searchRegExp));
-export const UserActions = AllSupportedActions.filter(action => action.match(userRegExp));
-export const AdminActions = AllSupportedActions.filter(action => action.match(adminRegExp));
+export const PageActions = AllSupportedActions.filter((action) =>
+  action.match(pageRegExp),
+);
+export const CommentActions = AllSupportedActions.filter((action) =>
+  action.match(commentRegExp),
+);
+export const TagActions = AllSupportedActions.filter((action) =>
+  action.match(tagRegExp),
+);
+export const AttachmentActions = AllSupportedActions.filter((action) =>
+  action.match(attachmentRegExp),
+);
+export const ShareLinkActions = AllSupportedActions.filter((action) =>
+  action.match(shareLinkRegExp),
+);
+export const InAppNotificationActions = AllSupportedActions.filter((action) =>
+  action.match(inAppNotificationRegExp),
+);
+export const SearchActions = AllSupportedActions.filter((action) =>
+  action.match(searchRegExp),
+);
+export const UserActions = AllSupportedActions.filter((action) =>
+  action.match(userRegExp),
+);
+export const AdminActions = AllSupportedActions.filter((action) =>
+  action.match(adminRegExp),
+);
 
 /*
  * Type
  */
-export type SupportedTargetModelType = typeof SupportedTargetModel[keyof typeof SupportedTargetModel];
-export type SupportedEventModelType = typeof SupportedEventModel[keyof typeof SupportedEventModel];
-export type SupportedActionType = typeof SupportedAction[keyof typeof SupportedAction];
-export type SupportedActionCategoryType = typeof SupportedActionCategory[keyof typeof SupportedActionCategory]
+export type SupportedTargetModelType =
+  (typeof SupportedTargetModel)[keyof typeof SupportedTargetModel];
+export type SupportedEventModelType =
+  (typeof SupportedEventModel)[keyof typeof SupportedEventModel];
+export type SupportedActionType =
+  (typeof SupportedAction)[keyof typeof SupportedAction];
+export type SupportedActionCategoryType =
+  (typeof SupportedActionCategory)[keyof typeof SupportedActionCategory];
 
-export type ISnapshot = Partial<Pick<IUser, 'username'>>
+export type ISnapshot = Partial<Pick<IUser, 'username'>>;
 
 export type IActivity = {
-  user?: Ref<IUser>
-  ip?: string
-  endpoint?: string
-  targetModel?: SupportedTargetModelType
-  target?: string
-  eventModel?: SupportedEventModelType
-  event?: string
-  action: SupportedActionType
-  createdAt: Date
-  snapshot?: ISnapshot
-}
+  user?: Ref<IUser>;
+  ip?: string;
+  endpoint?: string;
+  targetModel?: SupportedTargetModelType;
+  target?: string;
+  eventModel?: SupportedEventModelType;
+  event?: string;
+  action: SupportedActionType;
+  createdAt: Date;
+  snapshot?: ISnapshot;
+};
 
 export type IActivityHasId = IActivity & HasObjectId;
 
 export type ISearchFilter = {
-  usernames?: string[]
-  dates?: {startDate: string | null, endDate: string | null}
-  actions?: SupportedActionType[]
-}
+  usernames?: string[];
+  dates?: { startDate: string | null; endDate: string | null };
+  actions?: SupportedActionType[];
+};

+ 1 - 2
apps/app/src/interfaces/admin.ts

@@ -1,4 +1,3 @@
-
 export interface updateConfigMethodForAdmin<T> {
-  update: (arg: T) => void
+  update: (arg: T) => void;
 }

+ 5 - 5
apps/app/src/interfaces/apiv3/attachment.ts

@@ -3,13 +3,13 @@ import type { IAttachment, IPage, IRevision } from '@growi/core';
 import type { ICheckLimitResult } from '../attachment';
 
 export type IApiv3GetAttachmentLimitParams = {
-  fileSize: number,
+  fileSize: number;
 };
 
 export type IApiv3GetAttachmentLimitResponse = ICheckLimitResult;
 
 export type IApiv3PostAttachmentResponse = {
-  page: IPage,
-  revision: IRevision,
-  attachment: IAttachment,
-}
+  page: IPage;
+  revision: IRevision;
+  attachment: IAttachment;
+};

+ 22 - 24
apps/app/src/interfaces/apiv3/page.ts

@@ -1,43 +1,41 @@
-import type {
-  IPageHasId, IRevisionHasId, ITag, Origin,
-} from '@growi/core';
+import type { IPageHasId, IRevisionHasId, ITag, Origin } from '@growi/core';
 
 import type { IOptionsForCreate, IOptionsForUpdate } from '../page';
 
 export type IApiv3PageCreateParams = IOptionsForCreate & {
-  path?: string,
-  parentPath?: string,
-  optionalParentPath?: string,
+  path?: string;
+  parentPath?: string;
+  optionalParentPath?: string;
 
-  body?: string,
-  pageTags?: string[],
+  body?: string;
+  pageTags?: string[];
 
-  origin?: Origin,
+  origin?: Origin;
 
-  isSlackEnabled?: boolean,
-  slackChannels?: string,
+  isSlackEnabled?: boolean;
+  slackChannels?: string;
 };
 
 export type IApiv3PageCreateResponse = {
-  page: IPageHasId,
-  tags: ITag[],
-  revision: IRevisionHasId,
+  page: IPageHasId;
+  tags: ITag[];
+  revision: IRevisionHasId;
 };
 
 export type IApiv3PageUpdateParams = IOptionsForUpdate & {
-  pageId: string,
-  revisionId?: string,
-  body: string,
-
-  origin?: Origin,
-  isSlackEnabled?: boolean,
-  slackChannels?: string,
-  wip?: boolean
+  pageId: string;
+  revisionId?: string;
+  body: string;
+
+  origin?: Origin;
+  isSlackEnabled?: boolean;
+  slackChannels?: string;
+  wip?: boolean;
 };
 
 export type IApiv3PageUpdateResponse = {
-  page: IPageHasId,
-  revision: IRevisionHasId,
+  page: IPageHasId;
+  revision: IRevisionHasId;
 };
 
 export const PageUpdateErrorCode = {

+ 6 - 5
apps/app/src/interfaces/attachment.ts

@@ -13,13 +13,14 @@ export const AttachmentMethodType = {
   local: 'local',
   none: 'none',
 } as const;
-export type AttachmentMethodType = typeof AttachmentMethodType[keyof typeof AttachmentMethodType]
+export type AttachmentMethodType =
+  (typeof AttachmentMethodType)[keyof typeof AttachmentMethodType];
 
 export type IResAttachmentList = {
-  paginateResult: PaginateResult<IAttachmentHasId>
+  paginateResult: PaginateResult<IAttachmentHasId>;
 };
 
 export type ICheckLimitResult = {
-  isUploadable: boolean,
-  errorMessage?: string,
-}
+  isUploadable: boolean;
+  errorMessage?: string;
+};

+ 18 - 18
apps/app/src/interfaces/bookmark-info.ts

@@ -1,25 +1,25 @@
-import type { Ref, IPageHasId, IUser } from '@growi/core';
+import type { IPageHasId, IUser, Ref } from '@growi/core';
 
 export interface IBookmarkInfo {
-  sumOfBookmarks: number,
-  isBookmarked: boolean,
-  bookmarkedUsers: IUser[],
-  pageId: string,
+  sumOfBookmarks: number;
+  isBookmarked: boolean;
+  bookmarkedUsers: IUser[];
+  pageId: string;
 }
 
 export interface BookmarkedPage {
-  _id: string,
-  page: IPageHasId | null,
-  user: Ref<IUser>,
-  createdAt: Date,
+  _id: string;
+  page: IPageHasId | null;
+  user: Ref<IUser>;
+  createdAt: Date;
 }
 
-export type MyBookmarkList = BookmarkedPage[]
+export type MyBookmarkList = BookmarkedPage[];
 
 export interface IBookmarkFolder {
-  name: string
-  owner: Ref<IUser>
-  parent?: Ref<this>
+  name: string;
+  owner: Ref<IUser>;
+  parent?: Ref<this>;
 }
 
 export interface BookmarkFolderItems extends IBookmarkFolder {
@@ -34,13 +34,13 @@ export const DRAG_ITEM_TYPE = {
 } as const;
 
 interface BookmarkDragItem {
-  bookmarkFolder: BookmarkFolderItems
-  level: number
-  root: string
+  bookmarkFolder: BookmarkFolderItems;
+  level: number;
+  root: string;
 }
 
 export interface DragItemDataType extends BookmarkDragItem, IPageHasId {
-  parentFolder: BookmarkFolderItems | null
+  parentFolder: BookmarkFolderItems | null;
 }
 
-export type DragItemType = typeof DRAG_ITEM_TYPE[keyof typeof DRAG_ITEM_TYPE];
+export type DragItemType = (typeof DRAG_ITEM_TYPE)[keyof typeof DRAG_ITEM_TYPE];

+ 9 - 9
apps/app/src/interfaces/cdn.ts

@@ -1,17 +1,17 @@
 export type CdnManifestArgs = {
-  integrity?: string,
-  async?: boolean,
-  defer?: boolean,
+  integrity?: string;
+  async?: boolean;
+  defer?: boolean;
 };
 
 export type CdnManifest = {
-  name: string,
-  url: string,
-  groups?: string[],
-  args?: CdnManifestArgs,
+  name: string;
+  url: string;
+  groups?: string[];
+  args?: CdnManifestArgs;
 };
 
 export type CdnResource = {
-  manifest: CdnManifest,
-  outDir: string,
+  manifest: CdnManifest;
+  outDir: string;
 };

+ 15 - 18
apps/app/src/interfaces/comment.ts

@@ -1,29 +1,26 @@
-import type {
-  Ref, HasObjectId,
-  IPage, IRevision, IUser,
-} from '@growi/core';
+import type { HasObjectId, IPage, IRevision, IUser, Ref } from '@growi/core';
 
 export type IComment = {
-  page: Ref<IPage>,
-  creator: Ref<IUser>,
-  revision: Ref<IRevision>,
+  page: Ref<IPage>;
+  creator: Ref<IUser>;
+  revision: Ref<IRevision>;
   comment: string;
-  commentPosition: number,
-  replyTo?: string,
-  createdAt: Date,
-  updatedAt: Date,
+  commentPosition: number;
+  replyTo?: string;
+  createdAt: Date;
+  updatedAt: Date;
 };
 
 export interface ICommentPostArgs {
   commentForm: {
-    comment: string,
-    revisionId: string,
-    replyTo: string|undefined
-  },
+    comment: string;
+    revisionId: string;
+    replyTo: string | undefined;
+  };
   slackNotificationForm: {
-    isSlackEnabled: boolean|undefined,
-    slackChannels: string|undefined,
-  },
+    isSlackEnabled: boolean | undefined;
+    slackChannels: string | undefined;
+  };
 }
 
 export type ICommentHasId = IComment & HasObjectId;

+ 2 - 2
apps/app/src/interfaces/common.ts

@@ -5,5 +5,5 @@
 import type { ReactNode } from 'react';
 
 export type HasChildren<T = ReactNode> = {
-  children?: T
-}
+  children?: T;
+};

+ 4 - 7
apps/app/src/interfaces/crowi-request.ts

@@ -4,18 +4,15 @@ import type { HydratedDocument } from 'mongoose';
 
 import type Crowi from '~/server/crowi';
 
-
 export interface CrowiProperties {
+  user?: HydratedDocument<IUser>;
 
-  user?: HydratedDocument<IUser>,
-
-  crowi: Crowi,
+  crowi: Crowi;
 
-  session: any,
+  session: any;
 
   // provided by csurf
-  csrfToken: () => string,
-
+  csrfToken: () => string;
 }
 
 export interface CrowiRequest extends CrowiProperties, Request {}

+ 4 - 4
apps/app/src/interfaces/customize.ts

@@ -1,10 +1,10 @@
 import type { GrowiThemeMetadata } from '@growi/core';
 
 export type IResLayoutSetting = {
-  isContainerFluid: boolean,
+  isContainerFluid: boolean;
 };
 
 export type IResGrowiTheme = {
-  currentTheme: string,
-  pluginThemesMetadatas: GrowiThemeMetadata[],
-}
+  currentTheme: string;
+  pluginThemesMetadatas: GrowiThemeMetadata[];
+};

+ 15 - 15
apps/app/src/interfaces/editor-methods.ts

@@ -1,22 +1,22 @@
 import type { JSX } from 'react';
 
 export interface IEditorMethods {
-  forceToFocus: () => void,
-  setValue: (newValue: string) => void,
-  setCaretLine: (line: number) => void,
-  setScrollTopByLine: (line: number) => void,
-  insertText: (text: string) => void,
-  terminateUploadingState: () => void,
+  forceToFocus: () => void;
+  setValue: (newValue: string) => void;
+  setCaretLine: (line: number) => void;
+  setScrollTopByLine: (line: number) => void;
+  insertText: (text: string) => void;
+  terminateUploadingState: () => void;
 }
 
 export interface IEditorInnerMethods {
-  getStrFromBol(): void,
-  getStrToEol: () => void,
-  getStrFromBolToSelectedUpperPos: () => void,
-  replaceBolToCurrentPos: (text: string) => void,
-  replaceLine: (text: string) => void,
-  insertLinebreak: () => void,
-  dispatchSave: () => void,
-  dispatchPasteFiles: (event: Event) => void,
-  getNavbarItems: () => JSX.Element[],
+  getStrFromBol(): void;
+  getStrToEol: () => void;
+  getStrFromBolToSelectedUpperPos: () => void;
+  replaceBolToCurrentPos: (text: string) => void;
+  replaceLine: (text: string) => void;
+  insertLinebreak: () => void;
+  dispatchSave: () => void;
+  dispatchPasteFiles: (event: Event) => void;
+  getNavbarItems: () => JSX.Element[];
 }

+ 3 - 1
apps/app/src/interfaces/errors/external-account-login-error.ts

@@ -3,6 +3,8 @@ import type { ExternalAccountLoginError } from '~/models/vo/external-account-log
 export type IExternalAccountLoginError = ExternalAccountLoginError;
 
 // type guard
-export const isExternalAccountLoginError = (args: any): args is IExternalAccountLoginError => {
+export const isExternalAccountLoginError = (
+  args: any,
+): args is IExternalAccountLoginError => {
   return (args as IExternalAccountLoginError).message != null;
 };

+ 4 - 2
apps/app/src/interfaces/errors/forgot-password.ts

@@ -1,7 +1,9 @@
 export const forgotPasswordErrorCode = {
   PASSWORD_RESET_IS_UNAVAILABLE: 'password-reset-is-unavailable',
   TOKEN_NOT_FOUND: 'token-not-found',
-  PASSWORD_RESET_ORDER_IS_NOT_APPROPRIATE: 'password-reset-order-is-not-appropriate',
+  PASSWORD_RESET_ORDER_IS_NOT_APPROPRIATE:
+    'password-reset-order-is-not-appropriate',
 } as const;
 
-export type forgotPasswordErrorCode = typeof forgotPasswordErrorCode[keyof typeof forgotPasswordErrorCode]
+export type forgotPasswordErrorCode =
+  (typeof forgotPasswordErrorCode)[keyof typeof forgotPasswordErrorCode];

+ 4 - 2
apps/app/src/interfaces/errors/login-error.ts

@@ -1,5 +1,7 @@
 export const LoginErrorCode = {
-  PROVIDER_DUPLICATED_USERNAME_EXCEPTION: 'provider-duplicated-username-exception',
+  PROVIDER_DUPLICATED_USERNAME_EXCEPTION:
+    'provider-duplicated-username-exception',
 } as const;
 
-export type LoginErrorCode = typeof LoginErrorCode[keyof typeof LoginErrorCode];
+export type LoginErrorCode =
+  (typeof LoginErrorCode)[keyof typeof LoginErrorCode];

+ 4 - 2
apps/app/src/interfaces/errors/user-activation.ts

@@ -1,7 +1,9 @@
 export const UserActivationErrorCode = {
   TOKEN_NOT_FOUND: 'token-not-found',
   INVALID_TOKEN: 'token-is-invalid',
-  USER_REGISTRATION_ORDER_IS_NOT_APPROPRIATE: 'user-registration-order-is-not-appropriate',
+  USER_REGISTRATION_ORDER_IS_NOT_APPROPRIATE:
+    'user-registration-order-is-not-appropriate',
 } as const;
 
-export type UserActivationErrorCode = typeof UserActivationErrorCode[keyof typeof UserActivationErrorCode];
+export type UserActivationErrorCode =
+  (typeof UserActivationErrorCode)[keyof typeof UserActivationErrorCode];

+ 1 - 1
apps/app/src/interfaces/errors/v3-error.ts

@@ -1,3 +1,3 @@
 import type { ErrorV3 } from '@growi/core/dist/models';
 
-export type IErrorV3 = ErrorV3
+export type IErrorV3 = ErrorV3;

+ 2 - 1
apps/app/src/interfaces/errors/v5-conversion-error.ts

@@ -5,4 +5,5 @@ export const V5ConversionErrCode = {
   FORBIDDEN: 'Forbidden',
 } as const;
 
-export type V5ConversionErrCode = typeof V5ConversionErrCode[keyof typeof V5ConversionErrCode];
+export type V5ConversionErrCode =
+  (typeof V5ConversionErrCode)[keyof typeof V5ConversionErrCode];

+ 2 - 1
apps/app/src/interfaces/external-auth-provider.ts

@@ -6,4 +6,5 @@ export const IExternalAuthProviderType = {
   github: 'github',
 } as const;
 
-export type IExternalAuthProviderType = typeof IExternalAuthProviderType[keyof typeof IExternalAuthProviderType]
+export type IExternalAuthProviderType =
+  (typeof IExternalAuthProviderType)[keyof typeof IExternalAuthProviderType];

+ 8 - 6
apps/app/src/interfaces/file-uploader.ts

@@ -7,22 +7,24 @@ export const FileUploadType = {
   local: 'local',
 } as const;
 
-export type FileUploadType = typeof FileUploadType[keyof typeof FileUploadType]
+export type FileUploadType =
+  (typeof FileUploadType)[keyof typeof FileUploadType];
 
 // file upload type strings you can specify in the env variable
 export const FileUploadTypeForEnvVar = {
   ...FileUploadType,
-  mongo:   'mongo',
+  mongo: 'mongo',
   mongodb: 'mongodb',
-  gcp:     'gcp',
+  gcp: 'gcp',
 } as const;
 
-export type FileUploadTypeForEnvVar = typeof FileUploadTypeForEnvVar[keyof typeof FileUploadTypeForEnvVar]
+export type FileUploadTypeForEnvVar =
+  (typeof FileUploadTypeForEnvVar)[keyof typeof FileUploadTypeForEnvVar];
 
 // mapping from env variable to actual module name
 export const EnvToModuleMappings = {
   ...FileUploadTypeForEnvVar,
-  mongo:   'gridfs',
+  mongo: 'gridfs',
   mongodb: 'gridfs',
-  gcp:     'gcs',
+  gcp: 'gcs',
 } as const;

+ 4 - 3
apps/app/src/interfaces/g2g-transfer.ts

@@ -12,12 +12,13 @@ export const G2G_PROGRESS_STATUS = {
 /**
  * G2G transfer progress status
  */
-export type G2GProgressStatus = typeof G2G_PROGRESS_STATUS[keyof typeof G2G_PROGRESS_STATUS];
+export type G2GProgressStatus =
+  (typeof G2G_PROGRESS_STATUS)[keyof typeof G2G_PROGRESS_STATUS];
 
 /**
  * G2G transfer progress
  */
 export interface G2GProgress {
- mongo: G2GProgressStatus;
- attachments: G2GProgressStatus;
+  mongo: G2GProgressStatus;
+  attachments: G2GProgressStatus;
 }

+ 16 - 16
apps/app/src/interfaces/github-api.ts

@@ -1,21 +1,21 @@
 export type SearchResult = {
-  total_count: number,
-  imcomplete_results: boolean,
+  total_count: number;
+  imcomplete_results: boolean;
   items: SearchResultItem[];
-}
+};
 
 export type SearchResultItem = {
-  id: number,
-  name: string,
+  id: number;
+  name: string;
   owner: {
-    login: string,
-    html_url: string,
-    avatar_url: string,
-  },
-  fullName: string,
-  htmlUrl: string,
-  description: string,
-  topics: string[],
-  homepage: string,
-  stargazersCount: number,
-}
+    login: string;
+    html_url: string;
+    avatar_url: string;
+  };
+  fullName: string;
+  htmlUrl: string;
+  description: string;
+  topics: string[];
+  homepage: string;
+  stargazersCount: number;
+};

+ 17 - 17
apps/app/src/interfaces/in-app-notification.ts

@@ -1,6 +1,6 @@
 import type { IUser } from '@growi/core';
 
-import type { SupportedTargetModelType, SupportedActionType } from './activity';
+import type { SupportedActionType, SupportedTargetModelType } from './activity';
 
 export enum InAppNotificationStatuses {
   STATUS_UNOPENED = 'UNOPENED',
@@ -8,22 +8,22 @@ export enum InAppNotificationStatuses {
 }
 
 export interface IInAppNotification<T = unknown> {
-  user: IUser
-  targetModel: SupportedTargetModelType
-  target: T
-  action: SupportedActionType
-  status: InAppNotificationStatuses
-  actionUsers: IUser[]
-  createdAt: Date
-  snapshot: string
-  parsedSnapshot?: any
+  user: IUser;
+  targetModel: SupportedTargetModelType;
+  target: T;
+  action: SupportedActionType;
+  status: InAppNotificationStatuses;
+  actionUsers: IUser[];
+  createdAt: Date;
+  snapshot: string;
+  parsedSnapshot?: any;
 }
 
 /*
-* Note:
-* Need to use mongoose PaginateResult as a type after upgrading mongoose v6.0.0.
-* Until then, use the original "PaginateResult".
-*/
+ * Note:
+ * Need to use mongoose PaginateResult as a type after upgrading mongoose v6.0.0.
+ * Until then, use the original "PaginateResult".
+ */
 export interface PaginateResult<T> {
   docs: T[];
   hasNextPage: boolean;
@@ -39,11 +39,11 @@ export interface PaginateResult<T> {
 }
 
 /*
-* In App Notification Settings
-*/
+ * In App Notification Settings
+ */
 
 export enum subscribeRuleNames {
-  PAGE_CREATE = 'PAGE_CREATE'
+  PAGE_CREATE = 'PAGE_CREATE',
 }
 
 export enum SubscribeRuleDescriptions {

+ 1 - 1
apps/app/src/interfaces/indeterminate-input-elm.ts

@@ -1,3 +1,3 @@
 export interface IndeterminateInputElement extends HTMLInputElement {
-  indeterminate:boolean
+  indeterminate: boolean;
 }

+ 5 - 5
apps/app/src/interfaces/ldap.ts

@@ -1,7 +1,7 @@
 export interface IResTestLdap {
-  err?: any,
-  message: string,
-  status: string,
-  ldapConfiguration?: any,
-  ldapAccountInfo?: any,
+  err?: any;
+  message: string;
+  status: string;
+  ldapConfiguration?: any;
+  ldapAccountInfo?: any;
 }

+ 4 - 5
apps/app/src/interfaces/named-query.ts

@@ -1,13 +1,12 @@
 import type { IUser } from '@growi/core';
 
-
 export enum SearchDelegatorName {
   DEFAULT = 'FullTextSearch',
   PRIVATE_LEGACY_PAGES = 'PrivateLegacyPages',
 }
 export interface INamedQuery {
-  name: string
-  aliasOf?: string
-  delegatorName?: SearchDelegatorName
-  creator?: IUser
+  name: string;
+  aliasOf?: string;
+  delegatorName?: SearchDelegatorName;
+  creator?: IUser;
 }

+ 22 - 6
apps/app/src/interfaces/page-delete-config.ts

@@ -4,34 +4,50 @@ export const PageDeleteConfigValue = {
   AdminOnly: 'adminOnly',
   Inherit: 'inherit',
 } as const;
-export type IPageDeleteConfigValue = typeof PageDeleteConfigValue[keyof typeof PageDeleteConfigValue];
+export type IPageDeleteConfigValue =
+  (typeof PageDeleteConfigValue)[keyof typeof PageDeleteConfigValue];
 
-export type IPageDeleteConfigValueToProcessValidation = Exclude<IPageDeleteConfigValue, typeof PageDeleteConfigValue.Inherit>;
+export type IPageDeleteConfigValueToProcessValidation = Exclude<
+  IPageDeleteConfigValue,
+  typeof PageDeleteConfigValue.Inherit
+>;
 
 export const PageSingleDeleteConfigValue = {
   Anyone: 'anyOne', // must be "anyOne" (not "anyone") for backward compatibility
   AdminAndAuthor: 'adminAndAuthor',
   AdminOnly: 'adminOnly',
 } as const;
-export type PageSingleDeleteConfigValue = Exclude<IPageDeleteConfigValue, typeof PageDeleteConfigValue.Inherit>;
+export type PageSingleDeleteConfigValue = Exclude<
+  IPageDeleteConfigValue,
+  typeof PageDeleteConfigValue.Inherit
+>;
 
 export const PageSingleDeleteCompConfigValue = {
   Anyone: 'anyOne', // must be "anyOne" (not "anyone") for backward compatibility
   AdminAndAuthor: 'adminAndAuthor',
   AdminOnly: 'adminOnly',
 } as const;
-export type PageSingleDeleteCompConfigValue = Exclude<IPageDeleteConfigValue, typeof PageDeleteConfigValue.Inherit>;
+export type PageSingleDeleteCompConfigValue = Exclude<
+  IPageDeleteConfigValue,
+  typeof PageDeleteConfigValue.Inherit
+>;
 
 export const PageRecursiveDeleteConfigValue = {
   AdminAndAuthor: 'adminAndAuthor',
   AdminOnly: 'adminOnly',
   Inherit: 'inherit',
 } as const;
-export type PageRecursiveDeleteConfigValue = Exclude<IPageDeleteConfigValue, typeof PageDeleteConfigValue.Anyone>;
+export type PageRecursiveDeleteConfigValue = Exclude<
+  IPageDeleteConfigValue,
+  typeof PageDeleteConfigValue.Anyone
+>;
 
 export const PageRecursiveDeleteCompConfigValue = {
   AdminAndAuthor: 'adminAndAuthor',
   AdminOnly: 'adminOnly',
   Inherit: 'inherit',
 } as const;
-export type PageRecursiveDeleteCompConfigValue = Exclude<IPageDeleteConfigValue, typeof PageDeleteConfigValue.Anyone>;
+export type PageRecursiveDeleteCompConfigValue = Exclude<
+  IPageDeleteConfigValue,
+  typeof PageDeleteConfigValue.Anyone
+>;

+ 17 - 14
apps/app/src/interfaces/page-grant.ts

@@ -1,29 +1,32 @@
-import type { PageGrant, GroupType } from '@growi/core';
+import type { GroupType, PageGrant } from '@growi/core';
 
 import type { ExternalUserGroupDocument } from '~/features/external-user-group/server/models/external-user-group';
 import type { UserGroupDocument } from '~/server/models/user-group';
 
 import type { IPageGrantData } from './page';
 
-
 type UserGroupType = typeof GroupType.userGroup;
 type ExternalUserGroupType = typeof GroupType.externalUserGroup;
-export type PopulatedGrantedGroup = {type: UserGroupType, item: UserGroupDocument } | {type: ExternalUserGroupType, item: ExternalUserGroupDocument }
+export type PopulatedGrantedGroup =
+  | { type: UserGroupType; item: UserGroupDocument }
+  | { type: ExternalUserGroupType; item: ExternalUserGroupDocument };
 export type IDataApplicableGroup = {
-  applicableGroups?: PopulatedGrantedGroup[]
-}
+  applicableGroups?: PopulatedGrantedGroup[];
+};
 
 export type IDataApplicableGrant = null | IDataApplicableGroup;
-export type IRecordApplicableGrant = Partial<Record<PageGrant, IDataApplicableGrant>>
+export type IRecordApplicableGrant = Partial<
+  Record<PageGrant, IDataApplicableGrant>
+>;
 export type IResApplicableGrant = {
-  data?: IRecordApplicableGrant
-}
+  data?: IRecordApplicableGrant;
+};
 export type IResGrantData = {
-  isForbidden: boolean,
-  currentPageGrant: IPageGrantData,
-  parentPageGrant?: IPageGrantData
-}
+  isForbidden: boolean;
+  currentPageGrant: IPageGrantData;
+  parentPageGrant?: IPageGrantData;
+};
 export type IResCurrentGrantData = {
-  isGrantNormalized: boolean,
-  grantData: IResGrantData
+  isGrantNormalized: boolean;
+  grantData: IResGrantData;
 };

+ 5 - 7
apps/app/src/interfaces/page-listing-results.ts

@@ -2,23 +2,21 @@ import type { IPageHasId } from '@growi/core';
 
 import type { IPageForItem } from './page';
 
-
 type ParentPath = string;
 
 export interface RootPageResult {
-  rootPage: IPageHasId
+  rootPage: IPageHasId;
 }
 
 export interface AncestorsChildrenResult {
-  ancestorsChildren: Record<ParentPath, Partial<IPageForItem>[]>
+  ancestorsChildren: Record<ParentPath, Partial<IPageForItem>[]>;
 }
 
-
 export interface ChildrenResult {
-  children: Partial<IPageForItem>[]
+  children: Partial<IPageForItem>[];
 }
 
 export interface V5MigrationStatus {
-  isV5Compatible : boolean,
-  migratablePagesCount: number
+  isV5Compatible: boolean;
+  migratablePagesCount: number;
 }

+ 10 - 8
apps/app/src/interfaces/page-operation.ts

@@ -8,21 +8,23 @@ export const PageActionType = {
   Revert: 'Revert',
   NormalizeParent: 'NormalizeParent',
 } as const;
-export type PageActionType = typeof PageActionType[keyof typeof PageActionType];
+export type PageActionType =
+  (typeof PageActionType)[keyof typeof PageActionType];
 
 export const PageActionStage = {
   Main: 'Main',
   Sub: 'Sub',
 } as const;
-export type PageActionStage = typeof PageActionStage[keyof typeof PageActionStage];
+export type PageActionStage =
+  (typeof PageActionStage)[keyof typeof PageActionStage];
 
 export type IPageOperationProcessData = {
   [key in PageActionType]?: {
-    [PageActionStage.Main]?: { isProcessable: boolean },
-    [PageActionStage.Sub]?: { isProcessable: boolean },
-  }
-}
+    [PageActionStage.Main]?: { isProcessable: boolean };
+    [PageActionStage.Sub]?: { isProcessable: boolean };
+  };
+};
 
 export type IPageOperationProcessInfo = {
-  [pageId: string]: IPageOperationProcessData,
-}
+  [pageId: string]: IPageOperationProcessData;
+};

+ 4 - 4
apps/app/src/interfaces/page-tag-relation.ts

@@ -1,7 +1,7 @@
 import type { IPage, ITag } from '@growi/core';
 
 export type IPageTagRelation = {
-  relatedPage: IPage,
-  relatedTag: ITag,
-  isPageTrashed: boolean,
-}
+  relatedPage: IPage;
+  relatedTag: ITag;
+  isPageTrashed: boolean;
+};

+ 53 - 43
apps/app/src/interfaces/page.ts

@@ -1,5 +1,10 @@
 import type {
-  GroupType, IGrantedGroup, IPageHasId, Nullable, PageGrant, Origin,
+  GroupType,
+  IGrantedGroup,
+  IPageHasId,
+  Nullable,
+  Origin,
+  PageGrant,
 } from '@growi/core';
 
 import type { ExternalGroupProviderType } from '~/features/external-user-group/interfaces/external-user-group';
@@ -7,77 +12,82 @@ import type { ExternalGroupProviderType } from '~/features/external-user-group/i
 import type { IPageOperationProcessData } from './page-operation';
 
 export {
-  isIPageInfoForEntity, isIPageInfoForOperation, isIPageInfoForListing,
+  isIPageInfoForEntity,
+  isIPageInfoForListing,
+  isIPageInfoForOperation,
 } from '@growi/core';
 
-export type IPageForItem = Partial<IPageHasId & {processData?: IPageOperationProcessData}>;
+export type IPageForItem = Partial<
+  IPageHasId & { processData?: IPageOperationProcessData }
+>;
 
 export const UserGroupPageGrantStatus = {
   isGranted: 'isGranted',
   notGranted: 'notGranted',
   cannotGrant: 'cannotGrant',
 };
-type UserGroupPageGrantStatus = typeof UserGroupPageGrantStatus[keyof typeof UserGroupPageGrantStatus];
+type UserGroupPageGrantStatus =
+  (typeof UserGroupPageGrantStatus)[keyof typeof UserGroupPageGrantStatus];
 export type UserRelatedGroupsData = {
-  id: string,
-  name: string,
-  type: GroupType,
-  provider?: ExternalGroupProviderType,
-  status: UserGroupPageGrantStatus,
-}
+  id: string;
+  name: string;
+  type: GroupType;
+  provider?: ExternalGroupProviderType;
+  status: UserGroupPageGrantStatus;
+};
 export type GroupGrantData = {
-  userRelatedGroups: UserRelatedGroupsData[],
+  userRelatedGroups: UserRelatedGroupsData[];
   nonUserRelatedGrantedGroups: {
-    id: string,
-    name: string,
-    type: GroupType,
-    provider?: ExternalGroupProviderType,
-  }[],
-}
+    id: string;
+    name: string;
+    type: GroupType;
+    provider?: ExternalGroupProviderType;
+  }[];
+};
 // current grant data of page
 export type IPageGrantData = {
-  grant: PageGrant,
-  groupGrantData?: GroupGrantData,
-}
+  grant: PageGrant;
+  groupGrantData?: GroupGrantData;
+};
 // grant selected by user which is not yet applied
 export type IPageSelectedGrant = {
-  grant: PageGrant,
-  userRelatedGrantedGroups?: IGrantedGroup[]
-}
+  grant: PageGrant;
+  userRelatedGrantedGroups?: IGrantedGroup[];
+};
 
 export type IDeleteSinglePageApiv1Result = {
-  ok: boolean
-  path: string,
-  isRecursively: Nullable<true>,
-  isCompletely: Nullable<true>,
+  ok: boolean;
+  path: string;
+  isRecursively: Nullable<true>;
+  isCompletely: Nullable<true>;
 };
 
 export type IDeleteManyPageApiv3Result = {
-  paths: string[],
-  isRecursively: Nullable<true>,
-  isCompletely: Nullable<true>,
+  paths: string[];
+  isRecursively: Nullable<true>;
+  isCompletely: Nullable<true>;
 };
 
 export type IOptionsForUpdate = {
-  origin?: Origin
-  wip?: boolean,
-  grant?: PageGrant,
-  userRelatedGrantUserGroupIds?: IGrantedGroup[],
+  origin?: Origin;
+  wip?: boolean;
+  grant?: PageGrant;
+  userRelatedGrantUserGroupIds?: IGrantedGroup[];
   // isSyncRevisionToHackmd?: boolean,
-  overwriteScopesOfDescendants?: boolean,
+  overwriteScopesOfDescendants?: boolean;
 };
 
 export type IOptionsForCreate = {
-  grant?: PageGrant,
-  grantUserGroupIds?: IGrantedGroup[],
-  onlyInheritUserRelatedGrantedGroups?: boolean,
-  overwriteScopesOfDescendants?: boolean,
+  grant?: PageGrant;
+  grantUserGroupIds?: IGrantedGroup[];
+  onlyInheritUserRelatedGrantedGroups?: boolean;
+  overwriteScopesOfDescendants?: boolean;
 
-  origin?: Origin
-  wip?: boolean,
+  origin?: Origin;
+  wip?: boolean;
 };
 
 export type IPagePathWithDescendantCount = {
-  path: string,
-  descendantCount: number,
+  path: string;
+  descendantCount: number;
 };

+ 4 - 4
apps/app/src/interfaces/paging-result.ts

@@ -1,5 +1,5 @@
 export type IPagingResult<T> = {
-  items: T[],
-  totalCount: number,
-  limit: number,
-}
+  items: T[];
+  totalCount: number;
+  limit: number;
+};

+ 2 - 1
apps/app/src/interfaces/registration-mode.ts

@@ -4,4 +4,5 @@ export const RegistrationMode = {
   CLOSED: 'Closed',
 } as const;
 
-export type RegistrationMode = typeof RegistrationMode[keyof typeof RegistrationMode];
+export type RegistrationMode =
+  (typeof RegistrationMode)[keyof typeof RegistrationMode];

+ 13 - 8
apps/app/src/interfaces/renderer-options.ts

@@ -1,17 +1,22 @@
 import type { ComponentType } from 'react';
 
-import type { Options as ReactMarkdownOptions, Components } from 'react-markdown';
+import type {
+  Components,
+  Options as ReactMarkdownOptions,
+} from 'react-markdown';
 import type { PluggableList } from 'unified';
 
-export type RendererOptions = Omit<ReactMarkdownOptions, 'remarkPlugins' | 'rehypePlugins' | 'components' | 'children'> & {
-  remarkPlugins: PluggableList,
-  rehypePlugins: PluggableList,
+export type RendererOptions = Omit<
+  ReactMarkdownOptions,
+  'remarkPlugins' | 'rehypePlugins' | 'components' | 'children'
+> & {
+  remarkPlugins: PluggableList;
+  rehypePlugins: PluggableList;
   components?:
     | Partial<
-        Components
-        & {
-          [elem: string]: ComponentType<any>,
+        Components & {
+          [elem: string]: ComponentType<any>;
         }
       >
-    | undefined
+    | undefined;
 };

+ 54 - 54
apps/app/src/interfaces/res/admin/app-settings.ts

@@ -1,66 +1,66 @@
 export type IResAppSettings = {
-  title: string,
-  confidential: string,
-  globalLang: string,
-  isEmailPublishedForNewUser: boolean,
-  fileUpload: string,
-  isV5Compatible: boolean,
-  siteUrl: string,
-  envSiteUrl: string,
-  isMailerSetup: boolean,
-  fromAddress: string,
+  title: string;
+  confidential: string;
+  globalLang: string;
+  isEmailPublishedForNewUser: boolean;
+  fileUpload: string;
+  isV5Compatible: boolean;
+  siteUrl: string;
+  envSiteUrl: string;
+  isMailerSetup: boolean;
+  fromAddress: string;
 
-  transmissionMethod: string,
-  smtpHost: string,
-  smtpPort: string | number, // TODO: check
-  smtpUser: string,
-  smtpPassword: string,
-  sesAccessKeyId: string,
-  sesSecretAccessKey: string,
+  transmissionMethod: string;
+  smtpHost: string;
+  smtpPort: string | number; // TODO: check
+  smtpUser: string;
+  smtpPassword: string;
+  sesAccessKeyId: string;
+  sesSecretAccessKey: string;
 
-  fileUploadType: string,
-  envFileUploadType: string,
-  useOnlyEnvVarForFileUploadType: boolean,
+  fileUploadType: string;
+  envFileUploadType: string;
+  useOnlyEnvVarForFileUploadType: boolean;
 
-  s3Region: string,
-  s3CustomEndpoint: string,
-  s3Bucket:string,
-  s3AccessKeyId: string,
-  s3SecretAccessKey: string,
-  s3ReferenceFileWithRelayMode: string,
+  s3Region: string;
+  s3CustomEndpoint: string;
+  s3Bucket: string;
+  s3AccessKeyId: string;
+  s3SecretAccessKey: string;
+  s3ReferenceFileWithRelayMode: string;
 
-  gcsUseOnlyEnvVars: boolean,
-  gcsApiKeyJsonPath: string,
-  gcsBucket: string,
-  gcsUploadNamespace: string,
-  gcsReferenceFileWithRelayMode: string,
+  gcsUseOnlyEnvVars: boolean;
+  gcsApiKeyJsonPath: string;
+  gcsBucket: string;
+  gcsUploadNamespace: string;
+  gcsReferenceFileWithRelayMode: string;
 
-  envGcsApiKeyJsonPath: string,
-  envGcsBucket: string,
-  envGcsUploadNamespace: string,
+  envGcsApiKeyJsonPath: string;
+  envGcsBucket: string;
+  envGcsUploadNamespace: string;
 
-  azureUseOnlyEnvVars: boolean,
-  azureTenantId: string,
-  azureClientId: string,
-  azureClientSecret: string,
-  azureStorageAccountName: string,
-  azureStorageContainerName: string,
-  azureReferenceFileWithRelayMode: string,
+  azureUseOnlyEnvVars: boolean;
+  azureTenantId: string;
+  azureClientId: string;
+  azureClientSecret: string;
+  azureStorageAccountName: string;
+  azureStorageContainerName: string;
+  azureReferenceFileWithRelayMode: string;
 
-  envAzureTenantId: string,
-  envAzureClientId: string,
-  envAzureClientSecret: string,
-  envAzureStorageAccountName: string,
-  envAzureStorageContainerName: string,
+  envAzureTenantId: string;
+  envAzureClientId: string;
+  envAzureClientSecret: string;
+  envAzureStorageAccountName: string;
+  envAzureStorageContainerName: string;
 
-  isEnabledPlugins: boolean,
+  isEnabledPlugins: boolean;
 
-  isAppSiteUrlHashed: boolean,
+  isAppSiteUrlHashed: boolean;
 
-  isMaintenanceMode: boolean,
+  isMaintenanceMode: boolean;
 
-  isBulkExportPagesEnabled: boolean,
-  envIsBulkExportPagesEnabled: boolean,
-  bulkExportDownloadExpirationSeconds: number,
-  useOnlyEnvVarsForIsBulkExportPagesEnabled: boolean,
-}
+  isBulkExportPagesEnabled: boolean;
+  envIsBulkExportPagesEnabled: boolean;
+  bulkExportDownloadExpirationSeconds: number;
+  useOnlyEnvVarsForIsBulkExportPagesEnabled: boolean;
+};

+ 18 - 18
apps/app/src/interfaces/search.ts

@@ -1,12 +1,12 @@
 import type { IDataWithMeta, IPageHasId } from '@growi/core';
 
 export type IPageSearchMeta = {
-  bookmarkCount?: number,
+  bookmarkCount?: number;
   elasticSearchResult?: {
     snippet?: string | null;
     highlightedPath?: string | null;
   };
-}
+};
 
 // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
 export const isIPageSearchMeta = (meta: any): meta is IPageSearchMeta => {
@@ -15,38 +15,38 @@ export const isIPageSearchMeta = (meta: any): meta is IPageSearchMeta => {
 
 export type ISearchResultMeta = {
   meta: {
-    took?: number
-    total: number
-    hitsCount: number
-  },
-}
+    took?: number;
+    total: number;
+    hitsCount: number;
+  };
+};
 
 export type ISearchResultData = {
-  _id: string
-  _score: number
-  _source: any
-  _highlight: any
-}
+  _id: string;
+  _score: number;
+  _source: any;
+  _highlight: any;
+};
 
 export type ISearchResult<T> = ISearchResultMeta & {
-  data: T[],
-}
+  data: T[];
+};
 
 export type IPageWithSearchMeta = IDataWithMeta<IPageHasId, IPageSearchMeta>;
 
 export type IFormattedSearchResult = ISearchResultMeta & {
-  data: IPageWithSearchMeta[],
-}
+  data: IPageWithSearchMeta[];
+};
 
 export const SORT_AXIS = {
   RELATION_SCORE: 'relationScore',
   CREATED_AT: 'createdAt',
   UPDATED_AT: 'updatedAt',
 } as const;
-export type SORT_AXIS = typeof SORT_AXIS[keyof typeof SORT_AXIS];
+export type SORT_AXIS = (typeof SORT_AXIS)[keyof typeof SORT_AXIS];
 
 export const SORT_ORDER = {
   DESC: 'desc',
   ASC: 'asc',
 } as const;
-export type SORT_ORDER = typeof SORT_ORDER[keyof typeof SORT_ORDER];
+export type SORT_ORDER = (typeof SORT_ORDER)[keyof typeof SORT_ORDER];

+ 7 - 6
apps/app/src/interfaces/services/rehype-sanitize.ts

@@ -7,11 +7,12 @@ export const RehypeSanitizeType = {
   CUSTOM: 'Custom',
 } as const;
 
-export type RehypeSanitizeType = typeof RehypeSanitizeType[keyof typeof RehypeSanitizeType];
+export type RehypeSanitizeType =
+  (typeof RehypeSanitizeType)[keyof typeof RehypeSanitizeType];
 
 export type RehypeSanitizeConfiguration = {
-  isEnabledXssPrevention: boolean,
-  sanitizeType: RehypeSanitizeType,
-  customTagWhitelist?: Array<string> | null,
-  customAttrWhitelist?: Attributes | null,
-}
+  isEnabledXssPrevention: boolean;
+  sanitizeType: RehypeSanitizeType;
+  customTagWhitelist?: Array<string> | null;
+  customAttrWhitelist?: Attributes | null;
+};

+ 10 - 10
apps/app/src/interfaces/services/renderer.ts

@@ -1,18 +1,18 @@
 import type { RehypeSanitizeConfiguration } from './rehype-sanitize';
 
 export type RendererConfig = {
-  isSharedPage?: boolean
-  isEnabledLinebreaks: boolean,
-  isEnabledLinebreaksInComments: boolean,
-  adminPreferredIndentSize: number,
-  isIndentSizeForced: boolean,
-  highlightJsStyleBorder: boolean,
-  isEnabledMarp: boolean,
+  isSharedPage?: boolean;
+  isEnabledLinebreaks: boolean;
+  isEnabledLinebreaksInComments: boolean;
+  adminPreferredIndentSize: number;
+  isIndentSizeForced: boolean;
+  highlightJsStyleBorder: boolean;
+  isEnabledMarp: boolean;
 
-  drawioUri: string,
-  plantumlUri: string,
+  drawioUri: string;
+  plantumlUri: string;
 } & RehypeSanitizeConfiguration;
 
 export type RendererConfigExt = RendererConfig & {
-  isDarkMode?: boolean,
+  isDarkMode?: boolean;
 };

+ 6 - 6
apps/app/src/interfaces/share-link.ts

@@ -1,15 +1,15 @@
-import type { IPageHasId, HasObjectId } from '@growi/core';
+import type { HasObjectId, IPageHasId } from '@growi/core';
 
 // Todo: specify more detailed Type
 export type IResShareLinkList = {
-  shareLinksResult: any[],
+  shareLinksResult: any[];
 };
 
 export type IShareLink = {
-  relatedPage: IPageHasId,
-  createdAt: Date,
-  expiredAt?: Date,
-  description: string,
+  relatedPage: IPageHasId;
+  createdAt: Date;
+  expiredAt?: Date;
+  description: string;
 };
 
 export type IShareLinkHasId = IShareLink & HasObjectId;

+ 2 - 3
apps/app/src/interfaces/sidebar-config.ts

@@ -1,5 +1,4 @@
-
 export interface ISidebarConfig {
-  isSidebarCollapsedMode: boolean,
-  isSidebarClosedAtDockMode?: boolean,
+  isSidebarCollapsedMode: boolean;
+  isSidebarClosedAtDockMode?: boolean;
 }

+ 18 - 18
apps/app/src/interfaces/tag.ts

@@ -1,31 +1,31 @@
-import type { ITag, IPageHasId } from '@growi/core';
+import type { IPageHasId, ITag } from '@growi/core';
 
-export type IDataTagCount = ITag & {count: number}
+export type IDataTagCount = ITag & { count: number };
 
 export type IPageTagsInfo = {
-  tags : string[],
-}
+  tags: string[];
+};
 
 export type IListTagNamesByPage = string[];
 
 export type IResTagsUpdateApiv1 = {
-  ok: boolean,
-  savedPage: IPageHasId,
-  tags: string[],
-}
+  ok: boolean;
+  savedPage: IPageHasId;
+  tags: string[];
+};
 
 export type IResTagsSearchApiv1 = {
-  ok: boolean,
-  tags: string[],
-}
+  ok: boolean;
+  tags: string[];
+};
 
 export type IResGetPageTags = {
-  ok: boolean,
-  tags: string[],
-}
+  ok: boolean;
+  tags: string[];
+};
 
 export type IResTagsListApiv1 = {
-  ok: boolean,
-  data: IDataTagCount[],
-  totalCount: number,
-}
+  ok: boolean;
+  data: IDataTagCount[];
+  totalCount: number;
+};

+ 1 - 1
apps/app/src/interfaces/theme.ts

@@ -1,4 +1,4 @@
 export const PrismThemes = {
   OneLight: 'one-light',
 } as const;
-export type PrismThemes = typeof PrismThemes[keyof typeof PrismThemes];
+export type PrismThemes = (typeof PrismThemes)[keyof typeof PrismThemes];

+ 4 - 4
apps/app/src/interfaces/transfer-key.ts

@@ -1,6 +1,6 @@
 export interface ITransferKey<ID = string> {
-  _id: ID
-  expireAt: Date
-  keyString: string,
-  key: string,
+  _id: ID;
+  expireAt: Date;
+  keyString: string;
+  key: string;
 }

+ 20 - 14
apps/app/src/interfaces/ui.ts

@@ -1,16 +1,14 @@
-import type { JSX } from 'react';
-
 import type { Nullable } from '@growi/core';
+import type { JSX } from 'react';
 
 import type { IPageForItem } from '~/interfaces/page';
 
-
 export const SidebarMode = {
   DRAWER: 'drawer',
   COLLAPSED: 'collapsed',
   DOCK: 'dock',
 } as const;
-export type SidebarMode = typeof SidebarMode[keyof typeof SidebarMode];
+export type SidebarMode = (typeof SidebarMode)[keyof typeof SidebarMode];
 
 export const SidebarContentsType = {
   CUSTOM: 'custom',
@@ -22,22 +20,30 @@ export const SidebarContentsType = {
   AI_ASSISTANT: 'aiAssistant',
 } as const;
 export const AllSidebarContentsType = Object.values(SidebarContentsType);
-export type SidebarContentsType = typeof SidebarContentsType[keyof typeof SidebarContentsType];
-
+export type SidebarContentsType =
+  (typeof SidebarContentsType)[keyof typeof SidebarContentsType];
 
 export type ICustomTabContent = {
-  Content?: () => JSX.Element,
-  i18n?: string,
-  Icon?: () => JSX.Element,
-  isLinkEnabled?: boolean | ((content: ICustomTabContent) => boolean),
+  Content?: () => JSX.Element;
+  i18n?: string;
+  Icon?: () => JSX.Element;
+  isLinkEnabled?: boolean | ((content: ICustomTabContent) => boolean);
 };
 
 export type ICustomNavTabMappings = { [key: string]: ICustomTabContent };
 
-
-export type OnDeletedFunction = (idOrPaths: string | string[], isRecursively: Nullable<true>, isCompletely: Nullable<true>) => void;
+export type OnDeletedFunction = (
+  idOrPaths: string | string[],
+  isRecursively: Nullable<true>,
+  isCompletely: Nullable<true>,
+) => void;
 export type OnRenamedFunction = (path: string) => void;
 export type OnDuplicatedFunction = (fromPath: string, toPath: string) => void;
 export type OnPutBackedFunction = (path: string) => void;
-export type onDeletedBookmarkFolderFunction = (bookmarkFolderId: string) => void;
-export type OnSelectedFunction = (page: IPageForItem, isIncludeSubPage: boolean) => void;
+export type onDeletedBookmarkFolderFunction = (
+  bookmarkFolderId: string,
+) => void;
+export type OnSelectedFunction = (
+  page: IPageForItem,
+  isIncludeSubPage: boolean,
+) => void;

+ 36 - 25
apps/app/src/interfaces/user-group-response.ts

@@ -1,49 +1,60 @@
 import type {
-  HasObjectId, Ref,
+  HasObjectId,
   IPageHasId,
-  IUserGroup, IUserGroupHasId, IUserGroupRelationHasId, IUserHasId,
+  IUserGroup,
+  IUserGroupHasId,
+  IUserGroupRelationHasId,
+  IUserHasId,
+  Ref,
 } from '@growi/core';
 
-
 export type UserGroupResult = {
-  userGroup: IUserGroupHasId,
-}
+  userGroup: IUserGroupHasId;
+};
 
-export type UserGroupListResult<TUSERGROUP extends IUserGroupHasId = IUserGroupHasId> = {
-  userGroups: TUSERGROUP[],
+export type UserGroupListResult<
+  TUSERGROUP extends IUserGroupHasId = IUserGroupHasId,
+> = {
+  userGroups: TUSERGROUP[];
 };
 
-export type ChildUserGroupListResult<TUSERGROUP extends IUserGroupHasId = IUserGroupHasId> = {
-  childUserGroups: TUSERGROUP[],
-  grandChildUserGroups: TUSERGROUP[],
+export type ChildUserGroupListResult<
+  TUSERGROUP extends IUserGroupHasId = IUserGroupHasId,
+> = {
+  childUserGroups: TUSERGROUP[];
+  grandChildUserGroups: TUSERGROUP[];
 };
 
-export type UserGroupRelationListResult<TUSERGROUPRELATION extends IUserGroupRelationHasId = IUserGroupRelationHasId> = {
-  userGroupRelations: TUSERGROUPRELATION[],
+export type UserGroupRelationListResult<
+  TUSERGROUPRELATION extends IUserGroupRelationHasId = IUserGroupRelationHasId,
+> = {
+  userGroupRelations: TUSERGROUPRELATION[];
 };
 
-export type IUserGroupRelationHasIdPopulatedUser<TUSERGROUP extends IUserGroup = IUserGroup> = {
-  relatedGroup: Ref<TUSERGROUP>,
-  relatedUser: IUserHasId,
-  createdAt: Date,
+export type IUserGroupRelationHasIdPopulatedUser<
+  TUSERGROUP extends IUserGroup = IUserGroup,
+> = {
+  relatedGroup: Ref<TUSERGROUP>;
+  relatedUser: IUserHasId;
+  createdAt: Date;
 } & HasObjectId;
 
 export type UserGroupRelationsResult = {
-  userGroupRelations: IUserGroupRelationHasIdPopulatedUser[],
+  userGroupRelations: IUserGroupRelationHasIdPopulatedUser[];
 };
 
 export type UserGroupPagesResult = {
-  pages: IPageHasId[],
-}
+  pages: IPageHasId[];
+};
 
 export type SelectableParentUserGroupsResult = {
-  selectableParentGroups: IUserGroupHasId[],
-}
+  selectableParentGroups: IUserGroupHasId[];
+};
 
 export type SelectableUserChildGroupsResult = {
-  selectableChildGroups: IUserGroupHasId[],
-}
+  selectableChildGroups: IUserGroupHasId[];
+};
 
 export type AncestorUserGroupsResult = {
-  ancestorUserGroups: IUserGroupHasId[],
-}
+  ancestorUserGroups: IUserGroupHasId[];
+};

+ 8 - 3
apps/app/src/interfaces/user-group.ts

@@ -4,7 +4,12 @@ export const SearchTypes = {
   BACKWORD: 'backword',
 } as const;
 
-export type SearchType = typeof SearchTypes[keyof typeof SearchTypes];
+export type SearchType = (typeof SearchTypes)[keyof typeof SearchTypes];
 
-export const PageActionOnGroupDelete = { publicize: 'publicize', delete: 'delete', transfer: 'transfer' } as const;
-export type PageActionOnGroupDelete = typeof PageActionOnGroupDelete[keyof typeof PageActionOnGroupDelete];
+export const PageActionOnGroupDelete = {
+  publicize: 'publicize',
+  delete: 'delete',
+  transfer: 'transfer',
+} as const;
+export type PageActionOnGroupDelete =
+  (typeof PageActionOnGroupDelete)[keyof typeof PageActionOnGroupDelete];

+ 1 - 2
apps/app/src/interfaces/user-trigger-notification.ts

@@ -1,3 +1,2 @@
-
 type SlackChannel = string;
-export type SlackChannels = {[updatePost: string]: SlackChannel[]}
+export type SlackChannels = { [updatePost: string]: SlackChannel[] };

+ 3 - 3
apps/app/src/interfaces/user-ui-settings.ts

@@ -1,7 +1,7 @@
 import type { SidebarContentsType } from './ui';
 
 export interface IUserUISettings {
-  currentSidebarContents: SidebarContentsType,
-  currentProductNavWidth: number,
-  preferCollapsedModeByUser: boolean,
+  currentSidebarContents: SidebarContentsType;
+  currentProductNavWidth: number;
+  preferCollapsedModeByUser: boolean;
 }

+ 9 - 7
apps/app/src/interfaces/websocket.ts

@@ -11,10 +11,10 @@ const generateGroupSyncEvents = () => {
   });
   return events as {
     [key in ExternalGroupProviderType]: {
-      GroupSyncProgress: string,
-      GroupSyncCompleted: string,
-      GroupSyncFailed: string,
-    }
+      GroupSyncProgress: string;
+      GroupSyncCompleted: string;
+      GroupSyncFailed: string;
+    };
   };
 };
 
@@ -51,9 +51,11 @@ export const SocketEventName = {
 
   // Yjs
   YjsAwarenessStateSizeUpdated: 'yjs:awareness-state-size-update',
-  YjsHasYdocsNewerThanLatestRevisionUpdated: 'yjs:has-ydocs-newer-than-latest-revision-update',
+  YjsHasYdocsNewerThanLatestRevisionUpdated:
+    'yjs:has-ydocs-newer-than-latest-revision-update',
 } as const;
-export type SocketEventName = typeof SocketEventName[keyof typeof SocketEventName];
+export type SocketEventName =
+  (typeof SocketEventName)[keyof typeof SocketEventName];
 
 type PageId = string;
 type DescendantCount = number;
@@ -68,4 +70,4 @@ export type PMMigratingData = { count: number };
 export type PMErrorCountData = { skip: number };
 export type PMEndedData = { isSucceeded: boolean };
 
-export type PageMigrationErrorData = { paths: string[] }
+export type PageMigrationErrorData = { paths: string[] };

+ 6 - 6
apps/app/src/interfaces/yjs.ts

@@ -1,9 +1,9 @@
 export type CurrentPageYjsData = {
-  hasYdocsNewerThanLatestRevision?: boolean,
-  awarenessStateSize?: number,
-}
+  hasYdocsNewerThanLatestRevision?: boolean;
+  awarenessStateSize?: number;
+};
 
 export type SyncLatestRevisionBody = {
-  synced: boolean,
-  isYjsDataBroken?: boolean,
-}
+  synced: boolean;
+  isYjsDataBroken?: boolean;
+};

+ 0 - 2
biome.json

@@ -29,9 +29,7 @@
       "!apps/app/src/components/**",
       "!apps/app/src/features/growi-plugin/**",
       "!apps/app/src/features/openai/**",
-      "!apps/app/src/features/page-bulk-export/**",
       "!apps/app/src/features/rate-limiter/**",
-      "!apps/app/src/interfaces/**",
       "!apps/app/src/models/**",
       "!apps/app/src/pages/**",
       "!apps/app/src/server/**",