Sfoglia il codice sorgente

Merge branch 'master' into feat/new-assistant-wizard

Shun Miyazawa 8 mesi fa
parent
commit
d4f233e09a
50 ha cambiato i file con 2164 aggiunte e 1279 eliminazioni
  1. 3 3
      .devcontainer/compose.yml
  2. 10 0
      apps/app/.eslintrc.js
  3. 11 4
      apps/app/jest.config.js
  4. 18 15
      apps/app/next.config.js
  5. 1 4
      apps/app/nodemon.json
  6. 4 2
      apps/app/package.json
  7. 14 14
      apps/app/playwright.config.ts
  8. 3 2
      apps/app/playwright/60-home/home.spec.ts
  9. 0 119
      apps/app/resource/search/mappings-es7.json
  10. 129 0
      apps/app/resource/search/mappings-es7.ts
  11. 0 118
      apps/app/resource/search/mappings-es8-for-ci.json
  12. 0 119
      apps/app/resource/search/mappings-es8.json
  13. 128 0
      apps/app/resource/search/mappings-es8.ts
  14. 127 0
      apps/app/resource/search/mappings-es9-for-ci.ts
  15. 128 0
      apps/app/resource/search/mappings-es9.ts
  16. 1 0
      apps/app/src/client/components/Me/AccessTokenScopeList.tsx
  17. 33 8
      apps/app/src/components/PageView/PageView.tsx
  18. 1 11
      apps/app/src/features/page-bulk-export/server/service/page-bulk-export-job-cron/index.ts
  19. 3 1
      apps/app/src/server/routes/apiv3/search.js
  20. 2 2
      apps/app/src/server/service/config-manager/config-definition.ts
  21. 77 0
      apps/app/src/server/service/search-delegator/elasticsearch-client-delegator/es7-client-delegator.ts
  22. 54 0
      apps/app/src/server/service/search-delegator/elasticsearch-client-delegator/es8-client-delegator.ts
  23. 54 0
      apps/app/src/server/service/search-delegator/elasticsearch-client-delegator/es9-client-delegator.ts
  24. 60 0
      apps/app/src/server/service/search-delegator/elasticsearch-client-delegator/get-client.ts
  25. 2 0
      apps/app/src/server/service/search-delegator/elasticsearch-client-delegator/index.ts
  26. 59 0
      apps/app/src/server/service/search-delegator/elasticsearch-client-delegator/interfaces.ts
  27. 0 113
      apps/app/src/server/service/search-delegator/elasticsearch-client-types.ts
  28. 0 125
      apps/app/src/server/service/search-delegator/elasticsearch-client.ts
  29. 186 173
      apps/app/src/server/service/search-delegator/elasticsearch.ts
  30. 1 1
      apps/app/src/services/renderer/recommended-whitelist.ts
  31. 2 5
      apps/app/test/integration/crowi/crowi.test.js
  32. 43 15
      apps/app/test/integration/global-setup.js
  33. 1 2
      apps/app/test/integration/global-teardown.js
  34. 101 89
      apps/app/test/integration/middlewares/login-required.test.js
  35. 15 14
      apps/app/test/integration/migrations/20210913153942-migrate-slack-app-integration-schema.test.ts
  36. 40 22
      apps/app/test/integration/models/page-redirect.test.js
  37. 98 50
      apps/app/test/integration/models/page.test.js
  38. 38 12
      apps/app/test/integration/models/update-post.test.js
  39. 47 25
      apps/app/test/integration/models/user.test.js
  40. 444 124
      apps/app/test/integration/models/v5.page.test.js
  41. 2 4
      apps/app/test/integration/setup-crowi.ts
  42. 2 2
      apps/app/test/integration/setup.js
  43. 1 1
      apps/app/test/integration/tsconfig.json
  44. 3 5
      apps/app/tsconfig.build.client.json
  45. 1 1
      apps/app/tsconfig.build.server.json
  46. 3 9
      apps/app/tsconfig.json
  47. 1 3
      apps/app/vite.styles-prebuilt.config.ts
  48. 23 45
      apps/app/vitest.workspace.mts
  49. 12 2
      biome.json
  50. 178 15
      pnpm-lock.yaml

+ 3 - 3
.devcontainer/compose.yml

@@ -26,10 +26,10 @@ services:
   #   cloned from https://github.com/weseek/growi-docker-compose.git
   #   cloned from https://github.com/weseek/growi-docker-compose.git
   elasticsearch:
   elasticsearch:
     build:
     build:
-      context: ../../growi-docker-compose/elasticsearch/v8
+      context: ../../growi-docker-compose/elasticsearch/v9
       dockerfile: ./Dockerfile
       dockerfile: ./Dockerfile
       args:
       args:
-        - version=8.7.0
+        - version=9.0.3
     restart: unless-stopped
     restart: unless-stopped
     ports:
     ports:
       - 9200
       - 9200
@@ -43,7 +43,7 @@ services:
         hard: -1
         hard: -1
     volumes:
     volumes:
       - /usr/share/elasticsearch/data
       - /usr/share/elasticsearch/data
-      - ../../growi-docker-compose/elasticsearch/v8/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml
+      - ../../growi-docker-compose/elasticsearch/v9/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml
 
 
 volumes:
 volumes:
   pnpm-store:
   pnpm-store:

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

@@ -16,6 +16,16 @@ module.exports = {
     'src/linter-checker/**',
     'src/linter-checker/**',
     'tmp/**',
     'tmp/**',
     'next-env.d.ts',
     'next-env.d.ts',
+    'next.config.js',
+    'playwright.config.ts',
+    'test/integration/global-setup.js',
+    'test/integration/global-teardown.js',
+    'test/integration/setup-crowi.ts',
+    'test/integration/crowi/**',
+    'test/integration/middlewares/**',
+    'test/integration/migrations/**',
+    'test/integration/models/**',
+    'test/integration/setup.js',
   ],
   ],
   settings: {
   settings: {
     // resolve path aliases by eslint-import-resolver-typescript
     // resolve path aliases by eslint-import-resolver-typescript

+ 11 - 4
apps/app/jest.config.js

@@ -22,9 +22,14 @@ module.exports = {
 
 
       rootDir: '.',
       rootDir: '.',
       roots: ['<rootDir>'],
       roots: ['<rootDir>'],
-      testMatch: ['<rootDir>/test/integration/**/*.test.ts', '<rootDir>/test/integration/**/*.test.js'],
+      testMatch: [
+        '<rootDir>/test/integration/**/*.test.ts',
+        '<rootDir>/test/integration/**/*.test.js',
+      ],
       // https://regex101.com/r/jTaxYS/1
       // https://regex101.com/r/jTaxYS/1
-      modulePathIgnorePatterns: ['<rootDir>/test/integration/*.*/v5(..*)*.[t|j]s'],
+      modulePathIgnorePatterns: [
+        '<rootDir>/test/integration/*.*/v5(..*)*.[t|j]s',
+      ],
       testEnvironment: 'node',
       testEnvironment: 'node',
       globalSetup: '<rootDir>/test/integration/global-setup.js',
       globalSetup: '<rootDir>/test/integration/global-setup.js',
       globalTeardown: '<rootDir>/test/integration/global-teardown.js',
       globalTeardown: '<rootDir>/test/integration/global-teardown.js',
@@ -43,7 +48,10 @@ module.exports = {
 
 
       rootDir: '.',
       rootDir: '.',
       roots: ['<rootDir>'],
       roots: ['<rootDir>'],
-      testMatch: ['<rootDir>/test/integration/**/v5.*.test.ts', '<rootDir>/test/integration/**/v5.*.test.js'],
+      testMatch: [
+        '<rootDir>/test/integration/**/v5.*.test.ts',
+        '<rootDir>/test/integration/**/v5.*.test.js',
+      ],
 
 
       testEnvironment: 'node',
       testEnvironment: 'node',
       globalSetup: '<rootDir>/test/integration/global-setup.js',
       globalSetup: '<rootDir>/test/integration/global-setup.js',
@@ -75,5 +83,4 @@ module.exports = {
     '/resource/',
     '/resource/',
     '/node_modules/',
     '/node_modules/',
   ],
   ],
-
 };
 };

+ 18 - 15
apps/app/next.config.js

@@ -8,8 +8,10 @@
 const path = require('path');
 const path = require('path');
 
 
 const { withSuperjson } = require('next-superjson');
 const { withSuperjson } = require('next-superjson');
-const { PHASE_PRODUCTION_BUILD, PHASE_PRODUCTION_SERVER } = require('next/constants');
-
+const {
+  PHASE_PRODUCTION_BUILD,
+  PHASE_PRODUCTION_SERVER,
+} = require('next/constants');
 
 
 const getTranspilePackages = () => {
 const getTranspilePackages = () => {
   const { listPrefixedPackages } = require('./src/utils/next.config.utils');
   const { listPrefixedPackages } = require('./src/utils/next.config.utils');
@@ -56,7 +58,14 @@ const getTranspilePackages = () => {
     'github-slugger',
     'github-slugger',
     'html-url-attributes',
     'html-url-attributes',
     'estree-util-is-identifier-name',
     'estree-util-is-identifier-name',
-    ...listPrefixedPackages(['remark-', 'rehype-', 'hast-', 'mdast-', 'micromark-', 'unist-']),
+    ...listPrefixedPackages([
+      'remark-',
+      'rehype-',
+      'hast-',
+      'mdast-',
+      'micromark-',
+      'unist-',
+    ]),
   ];
   ];
 
 
   // const eazyLogger = require('eazy-logger');
   // const eazyLogger = require('eazy-logger');
@@ -84,13 +93,11 @@ const optimizePackageImports = [
   '@growi/ui',
   '@growi/ui',
 ];
 ];
 
 
-module.exports = async(phase, { defaultConfig }) => {
-
+module.exports = async (phase, { defaultConfig }) => {
   const { i18n, localePath } = require('./config/next-i18next.config');
   const { i18n, localePath } = require('./config/next-i18next.config');
 
 
   /** @type {import('next').NextConfig} */
   /** @type {import('next').NextConfig} */
   const nextConfig = {
   const nextConfig = {
-
     reactStrictMode: true,
     reactStrictMode: true,
     poweredByHeader: false,
     poweredByHeader: false,
     pageExtensions: ['page.tsx', 'page.ts', 'page.jsx', 'page.js'],
     pageExtensions: ['page.tsx', 'page.ts', 'page.jsx', 'page.js'],
@@ -103,9 +110,8 @@ module.exports = async(phase, { defaultConfig }) => {
     typescript: {
     typescript: {
       tsconfigPath: 'tsconfig.build.client.json',
       tsconfigPath: 'tsconfig.build.client.json',
     },
     },
-    transpilePackages: phase !== PHASE_PRODUCTION_SERVER
-      ? getTranspilePackages()
-      : undefined,
+    transpilePackages:
+      phase !== PHASE_PRODUCTION_SERVER ? getTranspilePackages() : undefined,
     experimental: {
     experimental: {
       optimizePackageImports,
       optimizePackageImports,
     },
     },
@@ -150,7 +156,6 @@ module.exports = async(phase, { defaultConfig }) => {
 
 
       return config;
       return config;
     },
     },
-
   };
   };
 
 
   // production server
   // production server
@@ -159,11 +164,9 @@ module.exports = async(phase, { defaultConfig }) => {
   }
   }
 
 
   const withBundleAnalyzer = require('@next/bundle-analyzer')({
   const withBundleAnalyzer = require('@next/bundle-analyzer')({
-    enabled: phase === PHASE_PRODUCTION_BUILD
-      && (
-        process.env.ANALYZE === 'true'
-          || process.env.ANALYZE === '1'
-      ),
+    enabled:
+      phase === PHASE_PRODUCTION_BUILD &&
+      (process.env.ANALYZE === 'true' || process.env.ANALYZE === '1'),
   });
   });
 
 
   return withBundleAnalyzer(withSuperjson()(nextConfig));
   return withBundleAnalyzer(withSuperjson()(nextConfig));

+ 1 - 4
apps/app/nodemon.json

@@ -1,9 +1,6 @@
 {
 {
   "ext": "js,ts,json",
   "ext": "js,ts,json",
-  "watch": [
-    ".",
-    "../../packages/**/dist"
-  ],
+  "watch": [".", "../../packages/**/dist"],
   "ignore": [
   "ignore": [
     ".next",
     ".next",
     "public/static",
     "public/static",

+ 4 - 2
apps/app/package.json

@@ -28,6 +28,7 @@
     "launch-dev:ci": "cross-env NODE_ENV=development pnpm run dev:migrate && pnpm run ts-node src/server/app.ts --ci",
     "launch-dev:ci": "cross-env NODE_ENV=development pnpm run dev:migrate && pnpm run ts-node src/server/app.ts --ci",
     "lint:typecheck": "vue-tsc --noEmit",
     "lint:typecheck": "vue-tsc --noEmit",
     "lint:eslint": "eslint --quiet \"**/*.{js,mjs,jsx,ts,mts,tsx}\"",
     "lint:eslint": "eslint --quiet \"**/*.{js,mjs,jsx,ts,mts,tsx}\"",
+    "lint:biome": "biome check",
     "lint:styles": "stylelint \"src/**/*.scss\"",
     "lint:styles": "stylelint \"src/**/*.scss\"",
     "lint:openapi:apiv3": "node node_modules/swagger2openapi/oas-validate tmp/openapi-spec-apiv3.json",
     "lint:openapi:apiv3": "node node_modules/swagger2openapi/oas-validate tmp/openapi-spec-apiv3.json",
     "lint:openapi:apiv1": "node node_modules/swagger2openapi/oas-validate tmp/openapi-spec-apiv1.json",
     "lint:openapi:apiv1": "node node_modules/swagger2openapi/oas-validate tmp/openapi-spec-apiv1.json",
@@ -69,8 +70,9 @@
     "@azure/storage-blob": "^12.16.0",
     "@azure/storage-blob": "^12.16.0",
     "@browser-bunyan/console-formatted-stream": "^1.8.0",
     "@browser-bunyan/console-formatted-stream": "^1.8.0",
     "@cspell/dynamic-import": "^8.15.4",
     "@cspell/dynamic-import": "^8.15.4",
-    "@elastic/elasticsearch7": "npm:@elastic/elasticsearch@^7.17.0",
-    "@elastic/elasticsearch8": "npm:@elastic/elasticsearch@^8.7.0",
+    "@elastic/elasticsearch7": "npm:@elastic/elasticsearch@^7.17.4",
+    "@elastic/elasticsearch8": "npm:@elastic/elasticsearch@^8.18.2",
+    "@elastic/elasticsearch9": "npm:@elastic/elasticsearch@^9.0.3",
     "@godaddy/terminus": "^4.9.0",
     "@godaddy/terminus": "^4.9.0",
     "@google-cloud/storage": "^5.8.5",
     "@google-cloud/storage": "^5.8.5",
     "@growi/core": "workspace:^",
     "@growi/core": "workspace:^",

+ 14 - 14
apps/app/playwright.config.ts

@@ -10,18 +10,20 @@ const storageState = fs.existsSync(authFile) ? authFile : undefined;
 
 
 const supportedBrowsers = ['chromium', 'firefox', 'webkit'] as const;
 const supportedBrowsers = ['chromium', 'firefox', 'webkit'] as const;
 
 
-const projects: Array<Project> = supportedBrowsers.map(browser => ({
+const projects: Array<Project> = supportedBrowsers.map((browser) => ({
   name: browser,
   name: browser,
   use: { ...devices[`Desktop ${browser}`], storageState },
   use: { ...devices[`Desktop ${browser}`], storageState },
   testIgnore: /(10-installer|21-basic-features-for-guest)\/.*\.spec\.ts/,
   testIgnore: /(10-installer|21-basic-features-for-guest)\/.*\.spec\.ts/,
   dependencies: ['setup', 'auth'],
   dependencies: ['setup', 'auth'],
 }));
 }));
 
 
-const projectsForGuestMode: Array<Project> = supportedBrowsers.map(browser => ({
-  name: `${browser}/guest-mode`,
-  use: { ...devices[`Desktop ${browser}`] }, // Do not use storageState
-  testMatch: /21-basic-features-for-guest\/.*\.spec\.ts/,
-}));
+const projectsForGuestMode: Array<Project> = supportedBrowsers.map(
+  (browser) => ({
+    name: `${browser}/guest-mode`,
+    use: { ...devices[`Desktop ${browser}`] }, // Do not use storageState
+    testMatch: /21-basic-features-for-guest\/.*\.spec\.ts/,
+  }),
+);
 
 
 /**
 /**
  * Read environment variables from file.
  * Read environment variables from file.
@@ -48,12 +50,7 @@ export default defineConfig({
   /* Opt out of parallel tests on CI. */
   /* Opt out of parallel tests on CI. */
   workers: process.env.CI ? 1 : undefined,
   workers: process.env.CI ? 1 : undefined,
   /* Reporter to use. See https://playwright.dev/docs/test-reporters */
   /* Reporter to use. See https://playwright.dev/docs/test-reporters */
-  reporter: process.env.CI
-    ? [
-      ['github'],
-      ['blob'],
-    ]
-    : 'list',
+  reporter: process.env.CI ? [['github'], ['blob']] : 'list',
 
 
   webServer: {
   webServer: {
     command: 'pnpm run server',
     command: 'pnpm run server',
@@ -79,7 +76,11 @@ export default defineConfig({
   /* Configure projects for major browsers */
   /* Configure projects for major browsers */
   projects: [
   projects: [
     // Setup project
     // Setup project
-    { name: 'setup', testMatch: /.*\.setup\.ts/, testIgnore: /auth\.setup\.ts/ },
+    {
+      name: 'setup',
+      testMatch: /.*\.setup\.ts/,
+      testIgnore: /auth\.setup\.ts/,
+    },
     { name: 'auth', testMatch: /auth\.setup\.ts/ },
     { name: 'auth', testMatch: /auth\.setup\.ts/ },
 
 
     {
     {
@@ -113,5 +114,4 @@ export default defineConfig({
     //   use: { ...devices['Desktop Chrome'], channel: 'chrome' },
     //   use: { ...devices['Desktop Chrome'], channel: 'chrome' },
     // },
     // },
   ],
   ],
-
 });
 });

+ 3 - 2
apps/app/playwright/60-home/home.spec.ts

@@ -94,10 +94,11 @@ test('Access Access Token setting', async({ page }) => {
 
 
   // Expect a success toaster to be displayed when new Access Token is generated
   // Expect a success toaster to be displayed when new Access Token is generated
   await page.getByTestId('btn-accesstoken-toggleform').click();
   await page.getByTestId('btn-accesstoken-toggleform').click();
-  await page.getByTestId('grw-accesstoken-textare-description').fill('dummy');
+  await page.getByTestId('grw-accesstoken-textarea-description').fill('dummy');
+  await page.getByTestId('grw-accesstoken-checkbox-read:*').check();
   await page.getByTestId('grw-accesstoken-create-button').click();
   await page.getByTestId('grw-accesstoken-create-button').click();
   await expect(page.locator('.Toastify__toast')).toBeVisible();
   await expect(page.locator('.Toastify__toast')).toBeVisible();
-  await expect(page.locator('grw-accesstokne-new-token-display')).toBeVisible();
+  await expect(page.getByTestId('grw-accesstoken-new-token-display')).toBeVisible();
 
 
   // Expect a success toaster to be displayed when the Access Token is deleted
   // Expect a success toaster to be displayed when the Access Token is deleted
   await page.getByTestId('grw-accesstoken-delete-button').click();
   await page.getByTestId('grw-accesstoken-delete-button').click();

+ 0 - 119
apps/app/resource/search/mappings-es7.json

@@ -1,119 +0,0 @@
-{
-  "settings": {
-    "analysis": {
-      "filter": {
-        "english_stop": {
-          "type":       "stop",
-          "stopwords":  "_english_"
-        }
-      },
-      "tokenizer": {
-        "edge_ngram_tokenizer": {
-          "type": "edge_ngram",
-          "min_gram": 2,
-          "max_gram": 20,
-          "token_chars": ["letter", "digit"]
-        }
-      },
-      "analyzer": {
-        "japanese": {
-          "tokenizer": "kuromoji_tokenizer",
-          "char_filter" : ["icu_normalizer"]
-        },
-        "english_edge_ngram": {
-          "tokenizer": "edge_ngram_tokenizer",
-          "filter": [
-            "lowercase",
-            "english_stop"
-          ]
-        }
-      }
-    }
-  },
-  "mappings": {
-    "properties" : {
-      "path": {
-        "type": "text",
-        "fields": {
-          "raw": {
-            "type": "text",
-            "analyzer": "keyword"
-          },
-          "ja": {
-            "type": "text",
-            "analyzer": "japanese"
-          },
-          "en": {
-            "type": "text",
-            "analyzer": "english_edge_ngram",
-            "search_analyzer": "standard"
-          }
-        }
-      },
-      "body": {
-        "type": "text",
-        "fields": {
-          "ja": {
-            "type": "text",
-            "analyzer": "japanese"
-          },
-          "en": {
-            "type": "text",
-            "analyzer": "english_edge_ngram",
-            "search_analyzer": "standard"
-          }
-        }
-      },
-      "body_embedded": {
-        "type": "dense_vector",
-        "dims": 768
-      },
-      "comments": {
-        "type": "text",
-        "fields": {
-          "ja": {
-            "type": "text",
-            "analyzer": "japanese"
-          },
-          "en": {
-            "type": "text",
-            "analyzer": "english_edge_ngram",
-            "search_analyzer": "standard"
-          }
-        }
-      },
-      "username": {
-        "type": "keyword"
-      },
-      "comment_count": {
-        "type": "integer"
-      },
-      "bookmark_count": {
-        "type": "integer"
-      },
-      "like_count": {
-        "type": "integer"
-      },
-      "grant": {
-        "type": "integer"
-      },
-      "granted_users": {
-        "type": "keyword"
-      },
-      "granted_groups": {
-        "type": "keyword"
-      },
-      "created_at": {
-        "type": "date",
-        "format": "dateOptionalTime"
-      },
-      "updated_at": {
-        "type": "date",
-        "format": "dateOptionalTime"
-      },
-      "tag_names": {
-        "type": "keyword"
-      }
-    }
-  }
-}

+ 129 - 0
apps/app/resource/search/mappings-es7.ts

@@ -0,0 +1,129 @@
+// TODO: https://redmine.weseek.co.jp/issues/168446
+import type { estypes } from '@elastic/elasticsearch7';
+
+type Mappings = {
+  settings: NonNullable<estypes.IndicesCreateRequest['body']>['settings'];
+  mappings: NonNullable<estypes.IndicesCreateRequest['body']>['mappings'];
+}
+
+export const mappings: Mappings = {
+  settings: {
+    analysis: {
+      filter: {
+        english_stop: {
+          type:       'stop',
+          stopwords:  '_english_',
+        },
+      },
+      tokenizer: {
+        edge_ngram_tokenizer: {
+          type: 'edge_ngram',
+          min_gram: 2,
+          max_gram: 20,
+          token_chars: ['letter', 'digit'],
+        },
+      },
+      analyzer: {
+        japanese: {
+          type: 'custom',
+          tokenizer: 'kuromoji_tokenizer',
+          char_filter: ['icu_normalizer'],
+        },
+        english_edge_ngram: {
+          type: 'custom',
+          tokenizer: 'edge_ngram_tokenizer',
+          filter: [
+            'lowercase',
+            'english_stop',
+          ],
+        },
+      },
+    },
+  },
+  mappings: {
+    properties: {
+      path: {
+        type: 'text',
+        fields: {
+          raw: {
+            type: 'text',
+            analyzer: 'keyword',
+          },
+          ja: {
+            type: 'text',
+            analyzer: 'japanese',
+          },
+          en: {
+            type: 'text',
+            analyzer: 'english_edge_ngram',
+            search_analyzer: 'standard',
+          },
+        },
+      },
+      body: {
+        type: 'text',
+        fields: {
+          ja: {
+            type: 'text',
+            analyzer: 'japanese',
+          },
+          en: {
+            type: 'text',
+            analyzer: 'english_edge_ngram',
+            search_analyzer: 'standard',
+          },
+        },
+      },
+      body_embedded: {
+        type: 'dense_vector',
+        dims: 768,
+      },
+      comments: {
+        type: 'text',
+        fields: {
+          ja: {
+            type: 'text',
+            analyzer: 'japanese',
+          },
+          en: {
+            type: 'text',
+            analyzer: 'english_edge_ngram',
+            search_analyzer: 'standard',
+          },
+        },
+      },
+      username: {
+        type: 'keyword',
+      },
+      comment_count: {
+        type: 'integer',
+      },
+      bookmark_count: {
+        type: 'integer',
+      },
+      like_count: {
+        type: 'integer',
+      },
+      grant: {
+        type: 'integer',
+      },
+      granted_users: {
+        type: 'keyword',
+      },
+      granted_groups: {
+        type: 'keyword',
+      },
+      created_at: {
+        type: 'date',
+        format: 'dateOptionalTime',
+      },
+      updated_at: {
+        type: 'date',
+        format: 'dateOptionalTime',
+      },
+      tag_names: {
+        type: 'keyword',
+      },
+    },
+  },
+};

+ 0 - 118
apps/app/resource/search/mappings-es8-for-ci.json

@@ -1,118 +0,0 @@
-{
-  "settings": {
-    "analysis": {
-      "filter": {
-        "english_stop": {
-          "type":       "stop",
-          "stopwords":  "_english_"
-        }
-      },
-      "tokenizer": {
-        "edge_ngram_tokenizer": {
-          "type": "edge_ngram",
-          "min_gram": 2,
-          "max_gram": 20,
-          "token_chars": ["letter", "digit"]
-        }
-      },
-      "analyzer": {
-        "japanese": {
-          "tokenizer": "edge_ngram_tokenizer",
-          "filter": [
-            "lowercase",
-            "english_stop"
-          ]
-        },
-        "english_edge_ngram": {
-          "tokenizer": "edge_ngram_tokenizer",
-          "filter": [
-            "lowercase",
-            "english_stop"
-          ]
-        }
-      }
-    }
-  },
-  "mappings": {
-    "properties" : {
-      "path": {
-        "type": "text",
-        "fields": {
-          "raw": {
-            "type": "text",
-            "analyzer": "keyword"
-          },
-          "ja": {
-            "type": "text",
-            "analyzer": "japanese"
-          },
-          "en": {
-            "type": "text",
-            "analyzer": "english_edge_ngram",
-            "search_analyzer": "standard"
-          }
-        }
-      },
-      "body": {
-        "type": "text",
-        "fields": {
-          "ja": {
-            "type": "text",
-            "analyzer": "japanese"
-          },
-          "en": {
-            "type": "text",
-            "analyzer": "english_edge_ngram",
-            "search_analyzer": "standard"
-          }
-        }
-      },
-      "comments": {
-        "type": "text",
-        "fields": {
-          "ja": {
-            "type": "text",
-            "analyzer": "japanese"
-          },
-          "en": {
-            "type": "text",
-            "analyzer": "english_edge_ngram",
-            "search_analyzer": "standard"
-          }
-        }
-      },
-      "username": {
-        "type": "keyword"
-      },
-      "comment_count": {
-        "type": "integer"
-      },
-      "bookmark_count": {
-        "type": "integer"
-      },
-      "like_count": {
-        "type": "integer"
-      },
-      "grant": {
-        "type": "integer"
-      },
-      "granted_users": {
-        "type": "keyword"
-      },
-      "granted_groups": {
-        "type": "keyword"
-      },
-      "created_at": {
-        "type": "date",
-        "format": "date_optional_time"
-      },
-      "updated_at": {
-        "type": "date",
-        "format": "date_optional_time"
-      },
-      "tag_names": {
-        "type": "keyword"
-      }
-    }
-  }
-}

+ 0 - 119
apps/app/resource/search/mappings-es8.json

@@ -1,119 +0,0 @@
-{
-  "settings": {
-    "analysis": {
-      "filter": {
-        "english_stop": {
-          "type":       "stop",
-          "stopwords":  "_english_"
-        }
-      },
-      "tokenizer": {
-        "edge_ngram_tokenizer": {
-          "type": "edge_ngram",
-          "min_gram": 2,
-          "max_gram": 20,
-          "token_chars": ["letter", "digit"]
-        }
-      },
-      "analyzer": {
-        "japanese": {
-          "tokenizer": "kuromoji_tokenizer",
-          "char_filter" : ["icu_normalizer"]
-        },
-        "english_edge_ngram": {
-          "tokenizer": "edge_ngram_tokenizer",
-          "filter": [
-            "lowercase",
-            "english_stop"
-          ]
-        }
-      }
-    }
-  },
-  "mappings": {
-    "properties" : {
-      "path": {
-        "type": "text",
-        "fields": {
-          "raw": {
-            "type": "text",
-            "analyzer": "keyword"
-          },
-          "ja": {
-            "type": "text",
-            "analyzer": "japanese"
-          },
-          "en": {
-            "type": "text",
-            "analyzer": "english_edge_ngram",
-            "search_analyzer": "standard"
-          }
-        }
-      },
-      "body": {
-        "type": "text",
-        "fields": {
-          "ja": {
-            "type": "text",
-            "analyzer": "japanese"
-          },
-          "en": {
-            "type": "text",
-            "analyzer": "english_edge_ngram",
-            "search_analyzer": "standard"
-          }
-        }
-      },
-      "body_embedded": {
-        "type": "dense_vector",
-        "dims": 768
-      },
-      "comments": {
-        "type": "text",
-        "fields": {
-          "ja": {
-            "type": "text",
-            "analyzer": "japanese"
-          },
-          "en": {
-            "type": "text",
-            "analyzer": "english_edge_ngram",
-            "search_analyzer": "standard"
-          }
-        }
-      },
-      "username": {
-        "type": "keyword"
-      },
-      "comment_count": {
-        "type": "integer"
-      },
-      "bookmark_count": {
-        "type": "integer"
-      },
-      "like_count": {
-        "type": "integer"
-      },
-      "grant": {
-        "type": "integer"
-      },
-      "granted_users": {
-        "type": "keyword"
-      },
-      "granted_groups": {
-        "type": "keyword"
-      },
-      "created_at": {
-        "type": "date",
-        "format": "date_optional_time"
-      },
-      "updated_at": {
-        "type": "date",
-        "format": "date_optional_time"
-      },
-      "tag_names": {
-        "type": "keyword"
-      }
-    }
-  }
-}

+ 128 - 0
apps/app/resource/search/mappings-es8.ts

@@ -0,0 +1,128 @@
+import type { estypes } from '@elastic/elasticsearch8';
+
+type Mappings = {
+  settings: estypes.IndicesCreateRequest['settings'];
+  mappings: estypes.IndicesCreateRequest['mappings'];
+}
+
+export const mappings: Mappings = {
+  settings: {
+    analysis: {
+      filter: {
+        english_stop: {
+          type:       'stop',
+          stopwords:  '_english_',
+        },
+      },
+      tokenizer: {
+        edge_ngram_tokenizer: {
+          type: 'edge_ngram',
+          min_gram: 2,
+          max_gram: 20,
+          token_chars: ['letter', 'digit'],
+        },
+      },
+      analyzer: {
+        japanese: {
+          type: 'custom',
+          tokenizer: 'kuromoji_tokenizer',
+          char_filter: ['icu_normalizer'],
+        },
+        english_edge_ngram: {
+          type: 'custom',
+          tokenizer: 'edge_ngram_tokenizer',
+          filter: [
+            'lowercase',
+            'english_stop',
+          ],
+        },
+      },
+    },
+  },
+  mappings: {
+    properties: {
+      path: {
+        type: 'text',
+        fields: {
+          raw: {
+            type: 'text',
+            analyzer: 'keyword',
+          },
+          ja: {
+            type: 'text',
+            analyzer: 'japanese',
+          },
+          en: {
+            type: 'text',
+            analyzer: 'english_edge_ngram',
+            search_analyzer: 'standard',
+          },
+        },
+      },
+      body: {
+        type: 'text',
+        fields: {
+          ja: {
+            type: 'text',
+            analyzer: 'japanese',
+          },
+          en: {
+            type: 'text',
+            analyzer: 'english_edge_ngram',
+            search_analyzer: 'standard',
+          },
+        },
+      },
+      body_embedded: {
+        type: 'dense_vector',
+        dims: 768,
+      },
+      comments: {
+        type: 'text',
+        fields: {
+          ja: {
+            type: 'text',
+            analyzer: 'japanese',
+          },
+          en: {
+            type: 'text',
+            analyzer: 'english_edge_ngram',
+            search_analyzer: 'standard',
+          },
+        },
+      },
+      username: {
+        type: 'keyword',
+      },
+      comment_count: {
+        type: 'integer',
+      },
+      bookmark_count: {
+        type: 'integer',
+      },
+      like_count: {
+        type: 'integer',
+      },
+      grant: {
+        type: 'integer',
+      },
+      granted_users: {
+        type: 'keyword',
+      },
+      granted_groups: {
+        type: 'keyword',
+      },
+      created_at: {
+        type: 'date',
+        format: 'date_optional_time',
+      },
+      updated_at: {
+        type: 'date',
+        format: 'date_optional_time',
+      },
+      tag_names: {
+        type: 'keyword',
+      },
+    },
+  },
+};

+ 127 - 0
apps/app/resource/search/mappings-es9-for-ci.ts

@@ -0,0 +1,127 @@
+import type { estypes } from '@elastic/elasticsearch9';
+
+type Mappings = {
+  settings: estypes.IndicesCreateRequest['settings'];
+  mappings: estypes.IndicesCreateRequest['mappings'];
+}
+
+export const mappings: Mappings = {
+  settings: {
+    analysis: {
+      filter: {
+        english_stop: {
+          type:       'stop',
+          stopwords:  '_english_',
+        },
+      },
+      tokenizer: {
+        edge_ngram_tokenizer: {
+          type: 'edge_ngram',
+          min_gram: 2,
+          max_gram: 20,
+          token_chars: ['letter', 'digit'],
+        },
+      },
+      analyzer: {
+        japanese: {
+          type: 'custom',
+          tokenizer: 'edge_ngram_tokenizer',
+          filter: [
+            'lowercase',
+            'english_stop',
+          ],
+        },
+        english_edge_ngram: {
+          type: 'custom',
+          tokenizer: 'edge_ngram_tokenizer',
+          filter: [
+            'lowercase',
+            'english_stop',
+          ],
+        },
+      },
+    },
+  },
+  mappings: {
+    properties: {
+      path: {
+        type: 'text',
+        fields: {
+          raw: {
+            type: 'text',
+            analyzer: 'keyword',
+          },
+          ja: {
+            type: 'text',
+            analyzer: 'japanese',
+          },
+          en: {
+            type: 'text',
+            analyzer: 'english_edge_ngram',
+            search_analyzer: 'standard',
+          },
+        },
+      },
+      body: {
+        type: 'text',
+        fields: {
+          ja: {
+            type: 'text',
+            analyzer: 'japanese',
+          },
+          en: {
+            type: 'text',
+            analyzer: 'english_edge_ngram',
+            search_analyzer: 'standard',
+          },
+        },
+      },
+      comments: {
+        type: 'text',
+        fields: {
+          ja: {
+            type: 'text',
+            analyzer: 'japanese',
+          },
+          en: {
+            type: 'text',
+            analyzer: 'english_edge_ngram',
+            search_analyzer: 'standard',
+          },
+        },
+      },
+      username: {
+        type: 'keyword',
+      },
+      comment_count: {
+        type: 'integer',
+      },
+      bookmark_count: {
+        type: 'integer',
+      },
+      like_count: {
+        type: 'integer',
+      },
+      grant: {
+        type: 'integer',
+      },
+      granted_users: {
+        type: 'keyword',
+      },
+      granted_groups: {
+        type: 'keyword',
+      },
+      created_at: {
+        type: 'date',
+        format: 'date_optional_time',
+      },
+      updated_at: {
+        type: 'date',
+        format: 'date_optional_time',
+      },
+      tag_names: {
+        type: 'keyword',
+      },
+    },
+  },
+};

+ 128 - 0
apps/app/resource/search/mappings-es9.ts

@@ -0,0 +1,128 @@
+import type { estypes } from '@elastic/elasticsearch9';
+
+type Mappings = {
+  settings: estypes.IndicesCreateRequest['settings'];
+  mappings: estypes.IndicesCreateRequest['mappings'];
+}
+
+export const mappings: Mappings = {
+  settings: {
+    analysis: {
+      filter: {
+        english_stop: {
+          type:       'stop',
+          stopwords:  '_english_',
+        },
+      },
+      tokenizer: {
+        edge_ngram_tokenizer: {
+          type: 'edge_ngram',
+          min_gram: 2,
+          max_gram: 20,
+          token_chars: ['letter', 'digit'],
+        },
+      },
+      analyzer: {
+        japanese: {
+          type: 'custom',
+          tokenizer: 'kuromoji_tokenizer',
+          char_filter: ['icu_normalizer'],
+        },
+        english_edge_ngram: {
+          type: 'custom',
+          tokenizer: 'edge_ngram_tokenizer',
+          filter: [
+            'lowercase',
+            'english_stop',
+          ],
+        },
+      },
+    },
+  },
+  mappings: {
+    properties: {
+      path: {
+        type: 'text',
+        fields: {
+          raw: {
+            type: 'text',
+            analyzer: 'keyword',
+          },
+          ja: {
+            type: 'text',
+            analyzer: 'japanese',
+          },
+          en: {
+            type: 'text',
+            analyzer: 'english_edge_ngram',
+            search_analyzer: 'standard',
+          },
+        },
+      },
+      body: {
+        type: 'text',
+        fields: {
+          ja: {
+            type: 'text',
+            analyzer: 'japanese',
+          },
+          en: {
+            type: 'text',
+            analyzer: 'english_edge_ngram',
+            search_analyzer: 'standard',
+          },
+        },
+      },
+      body_embedded: {
+        type: 'dense_vector',
+        dims: 768,
+      },
+      comments: {
+        type: 'text',
+        fields: {
+          ja: {
+            type: 'text',
+            analyzer: 'japanese',
+          },
+          en: {
+            type: 'text',
+            analyzer: 'english_edge_ngram',
+            search_analyzer: 'standard',
+          },
+        },
+      },
+      username: {
+        type: 'keyword',
+      },
+      comment_count: {
+        type: 'integer',
+      },
+      bookmark_count: {
+        type: 'integer',
+      },
+      like_count: {
+        type: 'integer',
+      },
+      grant: {
+        type: 'integer',
+      },
+      granted_users: {
+        type: 'keyword',
+      },
+      granted_groups: {
+        type: 'keyword',
+      },
+      created_at: {
+        type: 'date',
+        format: 'date_optional_time',
+      },
+      updated_at: {
+        type: 'date',
+        format: 'date_optional_time',
+      },
+      tag_names: {
+        type: 'keyword',
+      },
+    },
+  },
+};

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

@@ -68,6 +68,7 @@ export const AccessTokenScopeList: React.FC<AccessTokenScopeListProps> = ({
           <div key={scopeKey} className={`row my-1 ${moduleClass}`}>
           <div key={scopeKey} className={`row my-1 ${moduleClass}`}>
             <div className="col-md-5 indentation">
             <div className="col-md-5 indentation">
               <input
               <input
+                data-testid={`grw-accesstoken-checkbox-${scopeValue}`}
                 className={`form-check-input indentation indentation-level-${level}`}
                 className={`form-check-input indentation indentation-level-${level}`}
                 type="checkbox"
                 type="checkbox"
                 id={scopeValue as string}
                 id={scopeValue as string}

+ 33 - 8
apps/app/src/components/PageView/PageView.tsx

@@ -48,8 +48,6 @@ export const PageView = (props: Props): JSX.Element => {
 
 
   const commentsContainerRef = useRef<HTMLDivElement>(null);
   const commentsContainerRef = useRef<HTMLDivElement>(null);
 
 
-  const [isCommentsLoaded, setCommentsLoaded] = useState(false);
-
   const {
   const {
     pagePath, initialPage, rendererConfig, className,
     pagePath, initialPage, rendererConfig, className,
   } = props;
   } = props;
@@ -72,21 +70,49 @@ export const PageView = (props: Props): JSX.Element => {
   const markdown = page?.revision?.body;
   const markdown = page?.revision?.body;
   const isSlide = useSlidesByFrontmatter(markdown, rendererConfig.isEnabledMarp);
   const isSlide = useSlidesByFrontmatter(markdown, rendererConfig.isEnabledMarp);
 
 
+  const [currentPageId, setCurrentPageId] = useState<string | undefined>(page?._id);
+
+  useEffect(() => {
+    if (page?._id !== undefined) {
+      setCurrentPageId(page._id);
+    }
+  }, [page?._id]);
 
 
   // ***************************  Auto Scroll  ***************************
   // ***************************  Auto Scroll  ***************************
   useEffect(() => {
   useEffect(() => {
+    if (currentPageId == null) {
+      return;
+    }
+
     // do nothing if hash is empty
     // do nothing if hash is empty
     const { hash } = window.location;
     const { hash } = window.location;
     if (hash.length === 0) {
     if (hash.length === 0) {
       return;
       return;
     }
     }
 
 
-    const targetId = hash.slice(1);
+    const contentContainer = document.getElementById('page-view-content-container');
+    if (contentContainer == null) return;
+
+    const targetId = decodeURIComponent(hash.slice(1));
+    const target = document.getElementById(targetId);
+    if (target != null) {
+      target.scrollIntoView();
+      return;
+    }
+
+    const observer = new MutationObserver(() => {
+      const target = document.getElementById(targetId);
+      if (target != null) {
+        target.scrollIntoView();
+        observer.disconnect();
+      }
+    });
+
+    observer.observe(contentContainer, { childList: true, subtree: true });
 
 
-    const target = document.getElementById(decodeURIComponent(targetId));
-    target?.scrollIntoView();
+    return () => observer.disconnect();
+  }, [currentPageId]);
 
 
-  }, [isCommentsLoaded]);
   // *******************************  end  *******************************
   // *******************************  end  *******************************
 
 
   const specialContents = useMemo(() => {
   const specialContents = useMemo(() => {
@@ -145,7 +171,6 @@ export const PageView = (props: Props): JSX.Element => {
                 pageId={page._id}
                 pageId={page._id}
                 pagePath={pagePath}
                 pagePath={pagePath}
                 revision={page.revision}
                 revision={page.revision}
-                onLoaded={() => setCommentsLoaded(true)}
               />
               />
             </div>
             </div>
           ) }
           ) }
@@ -168,7 +193,7 @@ export const PageView = (props: Props): JSX.Element => {
       {specialContents == null && (
       {specialContents == null && (
         <>
         <>
           {(isUsersHomepagePath && page?.creator != null) && <UserInfo author={page.creator} />}
           {(isUsersHomepagePath && page?.creator != null) && <UserInfo author={page.creator} />}
-          <div className="flex-expand-vert">
+          <div id="page-view-content-container" className="flex-expand-vert">
             <Contents />
             <Contents />
           </div>
           </div>
         </>
         </>

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

@@ -103,18 +103,8 @@ class PageBulkExportJobCronService extends CronService implements IPageBulkExpor
    * @param isHtmlPath whether the tmp output path is for html files
    * @param isHtmlPath whether the tmp output path is for html files
    */
    */
   getTmpOutputDir(pageBulkExportJob: PageBulkExportJobDocument, isHtmlPath = false): string {
   getTmpOutputDir(pageBulkExportJob: PageBulkExportJobDocument, isHtmlPath = false): string {
-    const isGrowiCloud = configManager.getConfig('app:growiCloudUri') != null;
-    const appId = configManager.getConfig('app:growiAppIdForCloud')?.toString();
     const jobId = pageBulkExportJob._id.toString();
     const jobId = pageBulkExportJob._id.toString();
-
-    if (isGrowiCloud) {
-      if (appId == null) {
-        throw new Error('appId is required for bulk export on GROWI.cloud');
-      }
-    }
-
-    const basePath = path.join(this.tmpOutputRootDir, appId ?? '');
-    return isHtmlPath ? path.join(basePath, 'html', jobId) : path.join(basePath, jobId);
+    return isHtmlPath ? path.join(this.tmpOutputRootDir, 'html', jobId) : path.join(this.tmpOutputRootDir, jobId);
   }
   }
 
 
   /**
   /**

+ 3 - 1
apps/app/src/server/routes/apiv3/search.js

@@ -1,7 +1,7 @@
+import { SCOPE } from '@growi/core/dist/interfaces';
 import { ErrorV3 } from '@growi/core/dist/models';
 import { ErrorV3 } from '@growi/core/dist/models';
 
 
 import { SupportedAction } from '~/interfaces/activity';
 import { SupportedAction } from '~/interfaces/activity';
-import { SCOPE } from '@growi/core/dist/interfaces';
 import { accessTokenParser } from '~/server/middlewares/access-token-parser';
 import { accessTokenParser } from '~/server/middlewares/access-token-parser';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
@@ -139,6 +139,7 @@ module.exports = (crowi) => {
         return res.status(200).send({ info });
         return res.status(200).send({ info });
       }
       }
       catch (err) {
       catch (err) {
+        logger.error(err);
         return res.apiv3Err(err, 503);
         return res.apiv3Err(err, 503);
       }
       }
     });
     });
@@ -171,6 +172,7 @@ module.exports = (crowi) => {
         return res.status(200).send();
         return res.status(200).send();
       }
       }
       catch (err) {
       catch (err) {
+        logger.error(err);
         return res.apiv3Err(err, 503);
         return res.apiv3Err(err, 503);
       }
       }
     });
     });

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

@@ -433,9 +433,9 @@ export const CONFIG_DEFINITIONS = {
     envVarName: 'FILE_UPLOAD_TOTAL_LIMIT',
     envVarName: 'FILE_UPLOAD_TOTAL_LIMIT',
     defaultValue: Infinity,
     defaultValue: Infinity,
   }),
   }),
-  'app:elasticsearchVersion': defineConfig<number>({
+  'app:elasticsearchVersion': defineConfig<7|8|9>({
     envVarName: 'ELASTICSEARCH_VERSION',
     envVarName: 'ELASTICSEARCH_VERSION',
-    defaultValue: 8,
+    defaultValue: 9,
   }),
   }),
   'app:elasticsearchUri': defineConfig<string | undefined>({
   'app:elasticsearchUri': defineConfig<string | undefined>({
     envVarName: 'ELASTICSEARCH_URI',
     envVarName: 'ELASTICSEARCH_URI',

+ 77 - 0
apps/app/src/server/service/search-delegator/elasticsearch-client-delegator/es7-client-delegator.ts

@@ -0,0 +1,77 @@
+// TODO: https://redmine.weseek.co.jp/issues/168446
+import {
+  Client,
+  type ClientOptions,
+  type ApiResponse,
+  type RequestParams,
+  type estypes,
+} from '@elastic/elasticsearch7';
+
+import type { ES7SearchQuery } from './interfaces';
+
+export class ES7ClientDelegator {
+
+  private client: Client;
+
+  delegatorVersion = 7 as const;
+
+  constructor(options: ClientOptions, rejectUnauthorized: boolean) {
+    this.client = new Client({ ...options, ssl: { rejectUnauthorized } });
+  }
+
+  async bulk(params: RequestParams.Bulk): Promise<estypes.BulkResponse> {
+    const res = (await this.client.bulk(params)).body as estypes.BulkResponse;
+    return res;
+  }
+
+  cat = {
+    aliases: (params: RequestParams.CatAliases): Promise<ApiResponse<estypes.CatAliasesResponse>> => this.client.cat.aliases(params),
+    indices: (params: RequestParams.CatIndices): Promise<ApiResponse<estypes.CatIndicesResponse>> => this.client.cat.indices(params),
+  };
+
+  cluster = {
+    health: (): Promise<ApiResponse<estypes.ClusterHealthResponse>> => this.client.cluster.health(),
+  };
+
+  indices = {
+    create: (params: RequestParams.IndicesCreate): Promise<ApiResponse<estypes.IndicesCreateResponse>> => this.client.indices.create(params),
+    delete: (params: RequestParams.IndicesDelete): Promise<ApiResponse<estypes.IndicesDeleteResponse>> => this.client.indices.delete(params),
+    exists: async(params: RequestParams.IndicesExists): Promise<estypes.IndicesExistsResponse> => {
+      return (await this.client.indices.exists(params)).body;
+    },
+    existsAlias: async(params: RequestParams.IndicesExistsAlias): Promise<estypes.IndicesExistsAliasResponse> => {
+      return (await this.client.indices.existsAlias(params)).body;
+    },
+    putAlias: (params: RequestParams.IndicesPutAlias): Promise<ApiResponse<estypes.IndicesUpdateAliasesResponse>> => this.client.indices.putAlias(params),
+    getAlias: async(params: RequestParams.IndicesGetAlias): Promise<estypes.IndicesGetAliasResponse> => {
+      return (await this.client.indices.getAlias<estypes.IndicesGetAliasResponse>(params)).body;
+    },
+    updateAliases: (params: RequestParams.IndicesUpdateAliases['body']): Promise<ApiResponse<estypes.IndicesUpdateAliasesResponse>> => {
+      return this.client.indices.updateAliases({ body: params });
+    },
+    validateQuery: async(params: RequestParams.IndicesValidateQuery<{ query?: estypes.QueryDslQueryContainer }>)
+      : Promise<estypes.IndicesValidateQueryResponse> => {
+      return (await this.client.indices.validateQuery<estypes.IndicesValidateQueryResponse>(params)).body;
+    },
+    stats: async(params: RequestParams.IndicesStats): Promise<estypes.IndicesStatsResponse> => {
+      return (await this.client.indices.stats<estypes.IndicesStatsResponse>(params)).body;
+    },
+  };
+
+  nodes = {
+    info: (): Promise<ApiResponse<estypes.NodesInfoResponse>> => this.client.nodes.info(),
+  };
+
+  ping(): Promise<ApiResponse<estypes.PingResponse>> {
+    return this.client.ping();
+  }
+
+  reindex(indexName: string, tmpIndexName: string): Promise<ApiResponse<estypes.ReindexResponse>> {
+    return this.client.reindex({ wait_for_completion: false, body: { source: { index: indexName }, dest: { index: tmpIndexName } } });
+  }
+
+  async search(params: ES7SearchQuery): Promise<estypes.SearchResponse> {
+    return (await this.client.search<estypes.SearchResponse>(params)).body;
+  }
+
+}

+ 54 - 0
apps/app/src/server/service/search-delegator/elasticsearch-client-delegator/es8-client-delegator.ts

@@ -0,0 +1,54 @@
+import { Client, type ClientOptions, type estypes } from '@elastic/elasticsearch8';
+
+export class ES8ClientDelegator {
+
+  private client: Client;
+
+  delegatorVersion = 8 as const;
+
+  constructor(options: ClientOptions, rejectUnauthorized: boolean) {
+    this.client = new Client({ ...options, tls: { rejectUnauthorized } });
+  }
+
+  bulk(params: estypes.BulkRequest): Promise<estypes.BulkResponse> {
+    return this.client.bulk(params);
+  }
+
+  cat = {
+    aliases: (params: estypes.CatAliasesRequest): Promise<estypes.CatAliasesResponse> => this.client.cat.aliases(params),
+    indices: (params: estypes.CatIndicesRequest): Promise<estypes.CatIndicesResponse> => this.client.cat.indices(params),
+  };
+
+  cluster = {
+    health: (): Promise<estypes.ClusterHealthResponse> => this.client.cluster.health(),
+  };
+
+  indices = {
+    create: (params: estypes.IndicesCreateRequest): Promise<estypes.IndicesCreateResponse> => this.client.indices.create(params),
+    delete: (params: estypes.IndicesDeleteRequest): Promise<estypes.IndicesDeleteResponse> => this.client.indices.delete(params),
+    exists: (params: estypes.IndicesExistsRequest): Promise<estypes.IndicesExistsResponse> => this.client.indices.exists(params),
+    existsAlias: (params: estypes.IndicesExistsAliasRequest): Promise<estypes.IndicesExistsAliasResponse> => this.client.indices.existsAlias(params),
+    putAlias: (params: estypes.IndicesPutAliasRequest): Promise<estypes.IndicesPutAliasResponse> => this.client.indices.putAlias(params),
+    getAlias: (params: estypes.IndicesGetAliasRequest): Promise<estypes.IndicesGetAliasResponse> => this.client.indices.getAlias(params),
+    updateAliases: (params: estypes.IndicesUpdateAliasesRequest): Promise<estypes.IndicesUpdateAliasesResponse> => this.client.indices.updateAliases(params),
+    validateQuery: (params: estypes.IndicesValidateQueryRequest): Promise<estypes.IndicesValidateQueryResponse> => this.client.indices.validateQuery(params),
+    stats: (params: estypes.IndicesStatsRequest): Promise<estypes.IndicesStatsResponse> => this.client.indices.stats(params),
+  };
+
+  nodes = {
+    info: (): Promise<estypes.NodesInfoResponse> => this.client.nodes.info(),
+  };
+
+  ping(): Promise<estypes.PingResponse> {
+    return this.client.ping();
+  }
+
+  reindex(indexName: string, tmpIndexName: string): Promise<estypes.ReindexResponse> {
+    return this.client.reindex({ wait_for_completion: false, source: { index: indexName }, dest: { index: tmpIndexName } });
+  }
+
+  search(params: estypes.SearchRequest): Promise<estypes.SearchResponse> {
+    return this.client.search(params);
+  }
+
+}

+ 54 - 0
apps/app/src/server/service/search-delegator/elasticsearch-client-delegator/es9-client-delegator.ts

@@ -0,0 +1,54 @@
+import { Client, type ClientOptions, type estypes } from '@elastic/elasticsearch9';
+
+export class ES9ClientDelegator {
+
+  private client: Client;
+
+  delegatorVersion = 9 as const;
+
+  constructor(options: ClientOptions, rejectUnauthorized: boolean) {
+    this.client = new Client({ ...options, tls: { rejectUnauthorized } });
+  }
+
+  bulk(params: estypes.BulkRequest): Promise<estypes.BulkResponse> {
+    return this.client.bulk(params);
+  }
+
+  cat = {
+    aliases: (params: estypes.CatAliasesRequest): Promise<estypes.CatAliasesResponse> => this.client.cat.aliases(params),
+    indices: (params: estypes.CatIndicesRequest): Promise<estypes.CatIndicesResponse> => this.client.cat.indices(params),
+  };
+
+  cluster = {
+    health: (): Promise<estypes.ClusterHealthResponse> => this.client.cluster.health(),
+  };
+
+  indices = {
+    create: (params: estypes.IndicesCreateRequest): Promise<estypes.IndicesCreateResponse> => this.client.indices.create(params),
+    delete: (params: estypes.IndicesDeleteRequest): Promise<estypes.IndicesDeleteResponse> => this.client.indices.delete(params),
+    exists: (params: estypes.IndicesExistsRequest): Promise<estypes.IndicesExistsResponse> => this.client.indices.exists(params),
+    existsAlias: (params: estypes.IndicesExistsAliasRequest): Promise<estypes.IndicesExistsAliasResponse> => this.client.indices.existsAlias(params),
+    putAlias: (params: estypes.IndicesPutAliasRequest): Promise<estypes.IndicesPutAliasResponse> => this.client.indices.putAlias(params),
+    getAlias: (params: estypes.IndicesGetAliasRequest): Promise<estypes.IndicesGetAliasResponse> => this.client.indices.getAlias(params),
+    updateAliases: (params: estypes.IndicesUpdateAliasesRequest): Promise<estypes.IndicesUpdateAliasesResponse> => this.client.indices.updateAliases(params),
+    validateQuery: (params: estypes.IndicesValidateQueryRequest): Promise<estypes.IndicesValidateQueryResponse> => this.client.indices.validateQuery(params),
+    stats: (params: estypes.IndicesStatsRequest): Promise<estypes.IndicesStatsResponse> => this.client.indices.stats(params),
+  };
+
+  nodes = {
+    info: (): Promise<estypes.NodesInfoResponse> => this.client.nodes.info(),
+  };
+
+  ping(): Promise<estypes.PingResponse> {
+    return this.client.ping();
+  }
+
+  reindex(indexName: string, tmpIndexName: string): Promise<estypes.ReindexResponse> {
+    return this.client.reindex({ wait_for_completion: false, source: { index: indexName }, dest: { index: tmpIndexName } });
+  }
+
+  search(params: estypes.SearchRequest): Promise<estypes.SearchResponse> {
+    return this.client.search(params);
+  }
+
+}

+ 60 - 0
apps/app/src/server/service/search-delegator/elasticsearch-client-delegator/get-client.ts

@@ -0,0 +1,60 @@
+import type { ClientOptions as ES7ClientOptions } from '@elastic/elasticsearch7';
+import type { ClientOptions as ES8ClientOptions } from '@elastic/elasticsearch8';
+import type { ClientOptions as ES9ClientOptions } from '@elastic/elasticsearch9';
+
+import { type ES7ClientDelegator } from './es7-client-delegator';
+import { type ES8ClientDelegator } from './es8-client-delegator';
+import { type ES9ClientDelegator } from './es9-client-delegator';
+import type { ElasticsearchClientDelegator } from './interfaces';
+
+type GetDelegatorOptions = {
+  version: 7;
+  options: ES7ClientOptions;
+  rejectUnauthorized: boolean;
+} | {
+  version: 8;
+  options: ES8ClientOptions;
+  rejectUnauthorized: boolean;
+} | {
+  version: 9;
+  options: ES9ClientOptions;
+  rejectUnauthorized: boolean;
+}
+
+type IsAny<T> = 'dummy' extends (T & 'dummy') ? true : false;
+type Delegator<Opts extends GetDelegatorOptions> =
+  IsAny<Opts> extends true
+    ? ElasticsearchClientDelegator
+    : Opts extends { version: 7 }
+      ? ES7ClientDelegator
+      : Opts extends { version: 8 }
+        ? ES8ClientDelegator
+        : Opts extends { version: 9 }
+          ? ES9ClientDelegator
+          : ElasticsearchClientDelegator
+
+let instance: ElasticsearchClientDelegator | null = null;
+export const getClient = async<Opts extends GetDelegatorOptions>(opts: Opts): Promise<Delegator<Opts>> => {
+  if (instance == null) {
+    if (opts.version === 7) {
+      await import('./es7-client-delegator').then(({ ES7ClientDelegator }) => {
+        instance = new ES7ClientDelegator(opts.options, opts.rejectUnauthorized);
+        return instance;
+      });
+    }
+    if (opts.version === 8) {
+      await import('./es8-client-delegator').then(({ ES8ClientDelegator }) => {
+        instance = new ES8ClientDelegator(opts.options, opts.rejectUnauthorized);
+        return instance;
+      });
+    }
+    if (opts.version === 9) {
+      await import('./es9-client-delegator').then(({ ES9ClientDelegator }) => {
+        instance = new ES9ClientDelegator(opts.options, opts.rejectUnauthorized);
+        return instance;
+      });
+    }
+  }
+
+  return instance as Delegator<Opts>;
+};

+ 2 - 0
apps/app/src/server/service/search-delegator/elasticsearch-client-delegator/index.ts

@@ -0,0 +1,2 @@
+export * from './get-client';
+export * from './interfaces';

+ 59 - 0
apps/app/src/server/service/search-delegator/elasticsearch-client-delegator/interfaces.ts

@@ -0,0 +1,59 @@
+import type { estypes as ES7types, RequestParams } from '@elastic/elasticsearch7';
+import type { estypes as ES8types } from '@elastic/elasticsearch8';
+import type { estypes as ES9types } from '@elastic/elasticsearch9';
+
+import type { ES7ClientDelegator } from './es7-client-delegator';
+import type { ES8ClientDelegator } from './es8-client-delegator';
+import type { ES9ClientDelegator } from './es9-client-delegator';
+
+export type ElasticsearchClientDelegator = ES7ClientDelegator | ES8ClientDelegator | ES9ClientDelegator;
+
+
+// type guard
+// TODO: https://redmine.weseek.co.jp/issues/168446
+export const isES7ClientDelegator = (delegator: ElasticsearchClientDelegator): delegator is ES7ClientDelegator => {
+  return delegator.delegatorVersion === 7;
+};
+
+export const isES8ClientDelegator = (delegator: ElasticsearchClientDelegator): delegator is ES8ClientDelegator => {
+  return delegator.delegatorVersion === 8;
+};
+
+export const isES9ClientDelegator = (delegator: ElasticsearchClientDelegator): delegator is ES9ClientDelegator => {
+  return delegator.delegatorVersion === 9;
+};
+
+
+// Official library-derived interface
+// TODO: https://redmine.weseek.co.jp/issues/168446
+export type ES7SearchQuery = RequestParams.Search<{
+  query: ES7types.QueryDslQueryContainer
+  sort?: ES7types.Sort
+  highlight?: ES7types.SearchHighlight
+}>
+
+export interface ES8SearchQuery {
+  index: ES8types.IndexName
+  _source: ES8types.Fields
+  from?: number;
+  size?: number;
+  body: {
+    query: ES8types.QueryDslQueryContainer;
+    sort?: ES8types.Sort
+    highlight?: ES8types.SearchHighlight;
+  };
+}
+
+export interface ES9SearchQuery {
+  index: ES9types.IndexName
+  _source: ES9types.Fields
+  from?: number;
+  size?: number;
+  body: {
+    query: ES9types.QueryDslQueryContainer;
+    sort?: ES9types.Sort
+    highlight?: ES9types.SearchHighlight;
+  };
+}
+
+export type SearchQuery = ES7SearchQuery | ES8SearchQuery | ES9SearchQuery;

+ 0 - 113
apps/app/src/server/service/search-delegator/elasticsearch-client-types.ts

@@ -1,113 +0,0 @@
-/* eslint-disable camelcase */
-export type NodesInfoResponse = {
-  nodes: Record<
-    string,
-    {
-      version: string
-      plugins: Plugin[]
-    }
-  >
-}
-
-export type CatIndicesResponse = {
-  index: string
-}[]
-
-export type IndicesExistsResponse = boolean
-
-export type IndicesExistsAliasResponse = boolean
-
-export type CatAliasesResponse = {
-  alias: string
-  index: string
-  filter: string
-}[]
-
-export type BulkResponse = {
-  took: number
-  errors: boolean
-  items: Record<string, any>[]
-}
-
-export type SearchResponse = {
-  took: number
-  timed_out: boolean
-  _shards: {
-    total: number
-    successful: number
-    skipped: number
-    failed: number
-  }
-  hits: {
-    total: number | {
-      value: number
-      relation: string
-    } // 6.x.x | 7.x.x
-    max_score: number | null
-    hits: Record<string, {
-      _index: string
-      _type: string
-      _id: string
-      _score: number
-      _source: any
-    }>[]
-  }
-}
-
-export type ValidateQueryResponse = {
-  valid: boolean,
-  _shards: {
-    total: number,
-    successful: number,
-    failed: number
-  },
-  explanations: Record<string, any>[]
-}
-
-export type ClusterHealthResponse = {
-  cluster_name: string,
-  status: string,
-  timed_out: boolean,
-  number_of_nodes: number,
-  number_of_data_nodes: number,
-  active_primary_shards: number,
-  active_shards: number,
-  relocating_shards: number,
-  initializing_shards: number,
-  unassigned_shards: number,
-  delayed_unassigned_shards: number,
-  number_of_pending_tasks: number,
-  number_of_in_flight_fetch: number,
-  task_max_waiting_in_queue_millis: number,
-  active_shards_percent_as_number: number
-}
-
-export type IndicesStatsResponse = {
-  _shards: {
-    total: number,
-    successful: number,
-    failed: number
-  },
-  _all: {
-    primaries: any,
-    total: any
-  },
-  indices: any
-}
-
-export type ReindexResponse = {
-  took: number,
-  timed_out: boolean,
-  total: number,
-  updated: number,
-  created: number,
-  deleted: number,
-  batches: number,
-  noops: number,
-  version_conflicts: number,
-  retries: number,
-  throttled_millis: number,
-  requests_per_second: number,
-  throttled_until_millis: number,
-  failures: any | null
-}

+ 0 - 125
apps/app/src/server/service/search-delegator/elasticsearch-client.ts

@@ -1,125 +0,0 @@
-/* eslint-disable implicit-arrow-linebreak */
-/* eslint-disable no-confusing-arrow */
-import type {
-  ClientOptions as ES7ClientOptions,
-  ApiResponse as ES7ApiResponse,
-  RequestParams as ES7RequestParams,
-} from '@elastic/elasticsearch7';
-import {
-  Client as ES7Client,
-} from '@elastic/elasticsearch7';
-import type { ClientOptions as ES8ClientOptions, estypes } from '@elastic/elasticsearch8';
-import { Client as ES8Client } from '@elastic/elasticsearch8';
-
-import type {
-  BulkResponse,
-  CatAliasesResponse,
-  CatIndicesResponse,
-  IndicesExistsResponse,
-  IndicesExistsAliasResponse,
-  NodesInfoResponse,
-  SearchResponse,
-  ValidateQueryResponse,
-  ClusterHealthResponse,
-  IndicesStatsResponse,
-  ReindexResponse,
-} from './elasticsearch-client-types';
-
-
-type ElasticsearchClientParams =
-  | [ isES7: true, options: ES7ClientOptions, rejectUnauthorized: boolean ]
-  | [ isES7: false, options: ES8ClientOptions, rejectUnauthorized: boolean ]
-
-export default class ElasticsearchClient {
-
-  private client: ES7Client | ES8Client;
-
-  constructor(...params: ElasticsearchClientParams) {
-    const [isES7, options, rejectUnauthorized] = params;
-
-    this.client = isES7
-      ? new ES7Client({ ...options, ssl: { rejectUnauthorized } })
-      : new ES8Client({ ...options, tls: { rejectUnauthorized } });
-  }
-
-  async bulk(params: ES7RequestParams.Bulk & estypes.BulkRequest): Promise<BulkResponse | estypes.BulkResponse> {
-    return this.client instanceof ES7Client ? (await this.client.bulk(params)).body as BulkResponse : this.client.bulk(params);
-  }
-
-  // TODO: cat is not used in current Implementation, remove cat?
-  cat = {
-    aliases: (params: ES7RequestParams.CatAliases & estypes.CatAliasesRequest): Promise<ES7ApiResponse<CatAliasesResponse> | estypes.CatAliasesResponse> =>
-      this.client instanceof ES7Client ? this.client.cat.aliases(params) : this.client.cat.aliases(params),
-
-    indices: (params: ES7RequestParams.CatIndices & estypes.CatIndicesRequest): Promise<ES7ApiResponse<CatIndicesResponse> | estypes.CatAliasesResponse> =>
-      this.client instanceof ES7Client ? this.client.cat.indices(params) : this.client.cat.indices(params),
-  };
-
-  cluster = {
-    health: ()
-    : Promise<ES7ApiResponse<ClusterHealthResponse> | estypes.ClusterHealthResponse> =>
-      this.client instanceof ES7Client ? this.client.cluster.health() : this.client.cluster.health(),
-  };
-
-  indices = {
-    // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
-    create: (params: ES7RequestParams.IndicesCreate & estypes.IndicesCreateRequest) =>
-      this.client instanceof ES7Client ? this.client.indices.create(params) : this.client.indices.create(params),
-
-    // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
-    delete: (params: ES7RequestParams.IndicesDelete & estypes.IndicesDeleteRequest) =>
-      this.client instanceof ES7Client ? this.client.indices.delete(params) : this.client.indices.delete(params),
-
-    exists: async(params: ES7RequestParams.IndicesExists & estypes.IndicesExistsRequest)
-    : Promise<IndicesExistsResponse | estypes.IndicesExistsResponse> =>
-      this.client instanceof ES7Client ? (await this.client.indices.exists(params)).body as IndicesExistsResponse : this.client.indices.exists(params),
-
-    existsAlias: async(params: ES7RequestParams.IndicesExistsAlias & estypes.IndicesExistsAliasRequest)
-    : Promise<IndicesExistsAliasResponse | estypes.IndicesExistsAliasResponse> =>
-      this.client instanceof ES7Client
-        ? (await this.client.indices.existsAlias(params)).body as IndicesExistsAliasResponse
-        : this.client.indices.existsAlias(params),
-
-    // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
-    putAlias: (params: ES7RequestParams.IndicesPutAlias & estypes.IndicesPutAliasRequest) =>
-      this.client instanceof ES7Client ? this.client.indices.putAlias(params) : this.client.indices.putAlias(params),
-
-    // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
-    getAlias: async(params: ES7RequestParams.IndicesGetAlias & estypes.IndicesGetAliasRequest) =>
-      this.client instanceof ES7Client ? (await this.client.indices.getAlias(params)).body : this.client.indices.getAlias(params),
-
-    // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
-    updateAliases: (params: ES7RequestParams.IndicesUpdateAliases & estypes.IndicesUpdateAliasesRequest) =>
-      this.client instanceof ES7Client ? this.client.indices.updateAliases(params) : this.client.indices.updateAliases(params),
-
-    validateQuery: async(params: ES7RequestParams.IndicesValidateQuery & estypes.IndicesValidateQueryRequest)
-    : Promise<ValidateQueryResponse | estypes.IndicesValidateQueryResponse> =>
-      // eslint-disable-next-line max-len
-      this.client instanceof ES7Client ? (await this.client.indices.validateQuery(params)).body as ValidateQueryResponse : this.client.indices.validateQuery(params),
-
-    stats: async(params: ES7RequestParams.IndicesStats & estypes.IndicesStatsRequest)
-    : Promise<IndicesStatsResponse | estypes.IndicesStatsResponse> =>
-      this.client instanceof ES7Client ? (await this.client.indices.stats(params)).body as IndicesStatsResponse : this.client.indices.stats(params),
-  };
-
-  nodes = {
-    info: (): Promise<ES7ApiResponse<NodesInfoResponse> | estypes.NodesInfoResponse> =>
-      (this.client instanceof ES7Client ? this.client.nodes.info() : this.client.nodes.info()),
-  };
-
-  // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
-  ping() {
-    return this.client instanceof ES7Client ? this.client.ping() : this.client.ping();
-  }
-
-  reindex(indexName: string, tmpIndexName: string): Promise<ES7ApiResponse<ReindexResponse> | estypes.ReindexResponse> {
-    return this.client instanceof ES7Client
-      ? this.client.reindex({ wait_for_completion: false, body: { source: { index: indexName }, dest: { index: tmpIndexName } } })
-      : this.client.reindex({ wait_for_completion: false, source: { index: indexName }, dest: { index: tmpIndexName } });
-  }
-
-  async search(params: ES7RequestParams.Search & estypes.SearchRequest): Promise<SearchResponse | estypes.SearchResponse> {
-    return this.client instanceof ES7Client ? (await this.client.search(params)).body as SearchResponse : this.client.search(params);
-  }
-
-}

+ 186 - 173
apps/app/src/server/service/search-delegator/elasticsearch.ts

@@ -11,6 +11,7 @@ import type { ISearchResult, ISearchResultData } from '~/interfaces/search';
 import { SORT_AXIS, SORT_ORDER } from '~/interfaces/search';
 import { SORT_AXIS, SORT_ORDER } from '~/interfaces/search';
 import { SocketEventName } from '~/interfaces/websocket';
 import { SocketEventName } from '~/interfaces/websocket';
 import PageTagRelation from '~/server/models/page-tag-relation';
 import PageTagRelation from '~/server/models/page-tag-relation';
+import type { SocketIoService } from '~/server/service/socket-io';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import type {
 import type {
@@ -20,12 +21,20 @@ import type { PageModel } from '../../models/page';
 import { createBatchStream } from '../../util/batch-stream';
 import { createBatchStream } from '../../util/batch-stream';
 import { configManager } from '../config-manager';
 import { configManager } from '../config-manager';
 import type { UpdateOrInsertPagesOpts } from '../interfaces/search';
 import type { UpdateOrInsertPagesOpts } from '../interfaces/search';
-// // import { embed, openaiClient, fileUpload } from '../openai';
-// import { getOrCreateSearchAssistant } from '../openai/assistant';
 
 
 import { aggregatePipelineToIndex } from './aggregate-to-index';
 import { aggregatePipelineToIndex } from './aggregate-to-index';
 import type { AggregatedPage, BulkWriteBody, BulkWriteCommand } from './bulk-write';
 import type { AggregatedPage, BulkWriteBody, BulkWriteCommand } from './bulk-write';
-import ElasticsearchClient from './elasticsearch-client';
+import {
+  getClient,
+  isES7ClientDelegator,
+  isES8ClientDelegator,
+  isES9ClientDelegator,
+  type SearchQuery,
+  type ES7SearchQuery,
+  type ES8SearchQuery,
+  type ES9SearchQuery,
+  type ElasticsearchClientDelegator,
+} from './elasticsearch-client-delegator';
 
 
 const logger = loggerFactory('growi:service:search-delegator:elasticsearch');
 const logger = loggerFactory('growi:service:search-delegator:elasticsearch');
 
 
@@ -43,7 +52,7 @@ const ES_SORT_AXIS = {
 const ES_SORT_ORDER = {
 const ES_SORT_ORDER = {
   [DESC]: 'desc',
   [DESC]: 'desc',
   [ASC]: 'asc',
   [ASC]: 'asc',
-};
+} as const;
 
 
 const AVAILABLE_KEYS = ['match', 'not_match', 'phrase', 'not_phrase', 'prefix', 'not_prefix', 'tag', 'not_tag'];
 const AVAILABLE_KEYS = ['match', 'not_match', 'phrase', 'not_phrase', 'prefix', 'not_prefix', 'tag', 'not_tag'];
 
 
@@ -53,64 +62,41 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
 
 
   name!: SearchDelegatorName.DEFAULT;
   name!: SearchDelegatorName.DEFAULT;
 
 
-  socketIoService!: any;
-
-  isElasticsearchV7: boolean;
-
-  isElasticsearchReindexOnBoot: boolean;
+  private socketIoService!: SocketIoService;
 
 
-  elasticsearch: any;
+  // TODO: https://redmine.weseek.co.jp/issues/168446
+  private isElasticsearchV7: boolean;
 
 
-  client: ElasticsearchClient;
+  private isElasticsearchReindexOnBoot: boolean;
 
 
-  queries: any;
+  private elasticsearchVersion: 7 | 8 | 9;
 
 
-  indexName: string;
+  private client: ElasticsearchClientDelegator;
 
 
-  esUri: string | undefined;
+  private indexName: string;
 
 
-  constructor(socketIoService) {
+  constructor(socketIoService: SocketIoService) {
     this.name = SearchDelegatorName.DEFAULT;
     this.name = SearchDelegatorName.DEFAULT;
     this.socketIoService = socketIoService;
     this.socketIoService = socketIoService;
 
 
-    const elasticsearchVersion: number = configManager.getConfig('app:elasticsearchVersion');
+    const elasticsearchVersion = configManager.getConfig('app:elasticsearchVersion');
 
 
-    if (elasticsearchVersion !== 7 && elasticsearchVersion !== 8) {
+    if (elasticsearchVersion !== 7 && elasticsearchVersion !== 8 && elasticsearchVersion !== 9) {
       throw new Error('Unsupported Elasticsearch version. Please specify a valid number to \'ELASTICSEARCH_VERSION\'');
       throw new Error('Unsupported Elasticsearch version. Please specify a valid number to \'ELASTICSEARCH_VERSION\'');
     }
     }
 
 
     this.isElasticsearchV7 = elasticsearchVersion === 7;
     this.isElasticsearchV7 = elasticsearchVersion === 7;
 
 
-    this.isElasticsearchReindexOnBoot = configManager.getConfig('app:elasticsearchReindexOnBoot');
+    this.elasticsearchVersion = elasticsearchVersion;
 
 
-    // In Elasticsearch RegExp, we don't need to used ^ and $.
-    // Ref: https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-regexp-query.html#_standard_operators
-    this.queries = {
-      PORTAL: {
-        regexp: {
-          'path.raw': '.*/',
-        },
-      },
-      PUBLIC: {
-        regexp: {
-          'path.raw': '.*[^/]',
-        },
-      },
-      USER: {
-        prefix: {
-          'path.raw': '/user/',
-        },
-      },
-    };
-
-    this.initClient();
+    this.isElasticsearchReindexOnBoot = configManager.getConfig('app:elasticsearchReindexOnBoot');
   }
   }
 
 
-  get aliasName() {
+  get aliasName(): string {
     return `${this.indexName}-alias`;
     return `${this.indexName}-alias`;
   }
   }
 
 
-  initClient() {
+  async initClient(): Promise<void> {
     const { host, auth, indexName } = this.getConnectionInfo();
     const { host, auth, indexName } = this.getConnectionInfo();
 
 
     const rejectUnauthorized = configManager.getConfig('app:elasticsearchRejectUnauthorized');
     const rejectUnauthorized = configManager.getConfig('app:elasticsearchRejectUnauthorized');
@@ -121,7 +107,7 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
       requestTimeout: configManager.getConfig('app:elasticsearchRequestTimeout'),
       requestTimeout: configManager.getConfig('app:elasticsearchRequestTimeout'),
     };
     };
 
 
-    this.client = new ElasticsearchClient(this.isElasticsearchV7, options, rejectUnauthorized);
+    this.client = await getClient({ version: this.elasticsearchVersion, options, rejectUnauthorized });
     this.indexName = indexName;
     this.indexName = indexName;
   }
   }
 
 
@@ -135,7 +121,7 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
    */
    */
   getConnectionInfo() {
   getConnectionInfo() {
     let indexName = 'crowi';
     let indexName = 'crowi';
-    let host = this.esUri;
+    let host: string | undefined;
     let auth;
     let auth;
 
 
     const elasticsearchUri = configManager.getConfig('app:elasticsearchUri');
     const elasticsearchUri = configManager.getConfig('app:elasticsearchUri');
@@ -161,6 +147,7 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
   }
   }
 
 
   async init(): Promise<void> {
   async init(): Promise<void> {
+    await this.initClient();
     const normalizeIndices = await this.normalizeIndices();
     const normalizeIndices = await this.normalizeIndices();
     if (this.isElasticsearchReindexOnBoot) {
     if (this.isElasticsearchReindexOnBoot) {
       try {
       try {
@@ -190,7 +177,7 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
     let esVersion = 'unknown';
     let esVersion = 'unknown';
     const esNodeInfos = {};
     const esNodeInfos = {};
 
 
-    for (const [nodeName, nodeInfo] of Object.entries<any>(info)) {
+    for (const [nodeName, nodeInfo] of Object.entries(info)) {
       esVersion = nodeInfo.version;
       esVersion = nodeInfo.version;
 
 
       const filteredInfo = {
       const filteredInfo = {
@@ -269,7 +256,7 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
   /**
   /**
    * rebuild index
    * rebuild index
    */
    */
-  async rebuildIndex() {
+  async rebuildIndex(): Promise<void> {
     const { client, indexName, aliasName } = this;
     const { client, indexName, aliasName } = this;
 
 
     const tmpIndexName = `${indexName}-tmp`;
     const tmpIndexName = `${indexName}-tmp`;
@@ -281,12 +268,10 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
 
 
       // update alias
       // update alias
       await client.indices.updateAliases({
       await client.indices.updateAliases({
-        body: {
-          actions: [
-            { add: { alias: aliasName, index: tmpIndexName } },
-            { remove: { alias: aliasName, index: indexName } },
-          ],
-        },
+        actions: [
+          { add: { alias: aliasName, index: tmpIndexName } },
+          { remove: { alias: aliasName, index: indexName } },
+        ],
       });
       });
 
 
       // flush index
       // flush index
@@ -312,7 +297,7 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
 
 
   }
   }
 
 
-  async normalizeIndices() {
+  async normalizeIndices(): Promise<void> {
     const { client, indexName, aliasName } = this;
     const { client, indexName, aliasName } = this;
 
 
     const tmpIndexName = `${indexName}-tmp`;
     const tmpIndexName = `${indexName}-tmp`;
@@ -339,19 +324,36 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
     }
     }
   }
   }
 
 
-  async createIndex(index) {
-    let mappings = this.isElasticsearchV7
-      ? require('^/resource/search/mappings-es7.json')
-      : require('^/resource/search/mappings-es8.json');
+  async createIndex(index: string) {
+    // TODO: https://redmine.weseek.co.jp/issues/168446
+    if (isES7ClientDelegator(this.client)) {
+      const { mappings } = await import('^/resource/search/mappings-es7');
+      return this.client.indices.create({
+        index,
+        body: {
+          ...mappings,
+        },
+      });
+    }
 
 
-    if (process.env.CI) {
-      mappings = require('^/resource/search/mappings-es8-for-ci.json');
+    if (isES8ClientDelegator(this.client)) {
+      const { mappings } = await import('^/resource/search/mappings-es8');
+      return this.client.indices.create({
+        index,
+        ...mappings,
+      });
     }
     }
 
 
-    return this.client.indices.create({
-      index,
-      body: mappings,
-    });
+    if (isES9ClientDelegator(this.client)) {
+      const { mappings } = process.env.CI == null
+        ? await import('^/resource/search/mappings-es9')
+        : await import('^/resource/search/mappings-es9-for-ci');
+
+      return this.client.indices.create({
+        index,
+        ...mappings,
+      });
+    }
   }
   }
 
 
   /**
   /**
@@ -397,7 +399,7 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
     return [command, document];
     return [command, document];
   }
   }
 
 
-  prepareBodyForDelete(body, page) {
+  prepareBodyForDelete(body, page): void {
     if (!Array.isArray(body)) {
     if (!Array.isArray(body)) {
       throw new Error('Body must be an array.');
       throw new Error('Body must be an array.');
     }
     }
@@ -434,7 +436,7 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
   /**
   /**
    * @param {function} queryFactory factory method to generate a Mongoose Query instance
    * @param {function} queryFactory factory method to generate a Mongoose Query instance
    */
    */
-  async updateOrInsertPages(queryFactory, option: UpdateOrInsertPagesOpts = {}) {
+  async updateOrInsertPages(queryFactory, option: UpdateOrInsertPagesOpts = {}): Promise<void> {
     const { shouldEmitProgress = false, invokeGarbageCollection = false } = option;
     const { shouldEmitProgress = false, invokeGarbageCollection = false } = option;
 
 
     const Page = mongoose.model<IPage, PageModel>('Page');
     const Page = mongoose.model<IPage, PageModel>('Page');
@@ -481,28 +483,6 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
       },
       },
     });
     });
 
 
-    // const appendEmbeddingStream = new Transform({
-    //   objectMode: true,
-    //   async transform(chunk: AggregatedPage[], encoding, callback) {
-    //     // append embedding
-    //     for await (const doc of chunk) {
-    //       doc.revisionBodyEmbedded = (await embed(doc.revision.body, doc.creator?.username))[0].embedding;
-    //     }
-
-    //     this.push(chunk);
-    //     callback();
-    //   },
-    // });
-
-    // const appendFileUploadedStream = new Transform({
-    //   objectMode: true,
-    //   async transform(chunk, encoding, callback) {
-    //     await fileUpload(chunk);
-    //     this.push(chunk);
-    //     callback();
-    //   },
-    // });
-
     let count = 0;
     let count = 0;
     const writeStream = new Writable({
     const writeStream = new Writable({
       objectMode: true,
       objectMode: true,
@@ -558,8 +538,6 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
       readStream,
       readStream,
       batchStream,
       batchStream,
       appendTagNamesStream,
       appendTagNamesStream,
-      // appendEmbeddingStream,
-      // appendFileUploadedStream,
       writeStream,
       writeStream,
     );
     );
   }
   }
@@ -581,26 +559,74 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
    *   data: [ pages ...],
    *   data: [ pages ...],
    * }
    * }
    */
    */
-  async searchKeyword(query): Promise<ISearchResult<ISearchResultData>> {
+  async searchKeyword(query: SearchQuery): Promise<ISearchResult<ISearchResultData>> {
 
 
     // for debug
     // for debug
     if (process.env.NODE_ENV === 'development') {
     if (process.env.NODE_ENV === 'development') {
       logger.debug('query: ', JSON.stringify(query, null, 2));
       logger.debug('query: ', JSON.stringify(query, null, 2));
 
 
-      const validateQueryResponse = await this.client.indices.validateQuery({
-        index: query.index,
-        type: query.type,
-        explain: true,
-        body: {
-          query: query.body.query,
-        },
-      });
+
+      const validateQueryResponse = await (async() => {
+        if (isES7ClientDelegator(this.client)) {
+          const es7SearchQuery = query as ES7SearchQuery;
+          return this.client.indices.validateQuery({
+            explain: true,
+            index: es7SearchQuery.index,
+            body: {
+              query: es7SearchQuery.body?.query,
+            },
+          });
+        }
+
+        if (isES8ClientDelegator(this.client)) {
+          const es8SearchQuery = query as ES8SearchQuery;
+          return this.client.indices.validateQuery({
+            explain: true,
+            index: es8SearchQuery.index,
+            query: es8SearchQuery.body.query,
+          });
+        }
+
+        if (isES9ClientDelegator(this.client)) {
+          const es9SearchQuery = query as ES9SearchQuery;
+          return this.client.indices.validateQuery({
+            explain: true,
+            index: es9SearchQuery.index,
+            query: es9SearchQuery.body.query,
+          });
+        }
+
+        throw new Error('Unsupported Elasticsearch version');
+      })();
+
 
 
       // for debug
       // for debug
       logger.debug('ES result: ', validateQueryResponse);
       logger.debug('ES result: ', validateQueryResponse);
     }
     }
 
 
-    const searchResponse = await this.client.search(query);
+    const searchResponse = await (async() => {
+      if (isES7ClientDelegator(this.client)) {
+        return this.client.search(query as ES7SearchQuery);
+      }
+
+      if (isES8ClientDelegator(this.client)) {
+        return this.client.search(query as ES8SearchQuery);
+      }
+
+      if (isES9ClientDelegator(this.client)) {
+        const { body, ...rest } = query as ES9SearchQuery;
+        return this.client.search({
+          ...rest,
+          // Elimination of the body property since ES9
+          // https://raw.githubusercontent.com/elastic/elasticsearch-js/2f6200eb397df0e54d23848d769a93614ee1fb45/docs/release-notes/breaking-changes.md
+          query: body.query,
+          sort: body.sort,
+          highlight: body.highlight,
+        });
+      }
+
+      throw new Error('Unsupported Elasticsearch version');
+    })();
 
 
     const _total = searchResponse?.hits?.total;
     const _total = searchResponse?.hits?.total;
     let total = 0;
     let total = 0;
@@ -627,45 +653,49 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
 
 
   /**
   /**
    * create search query for Elasticsearch
    * create search query for Elasticsearch
-   *
-   * @param {object | undefined} option optional paramas
    * @returns {object} query object
    * @returns {object} query object
    */
    */
-  createSearchQuery(option?) {
-    let fields = ['path', 'bookmark_count', 'comment_count', 'seenUsers_count', 'updated_at', 'tag_names', 'comments'];
-    if (option) {
-      fields = option.fields || fields;
-    }
+  createSearchQuery(): SearchQuery {
+    const fields = ['path', 'bookmark_count', 'comment_count', 'seenUsers_count', 'updated_at', 'tag_names', 'comments'];
 
 
     // sort by score
     // sort by score
-    // eslint-disable-next-line prefer-const
-    let query = {
+    const query: SearchQuery = {
       index: this.aliasName,
       index: this.aliasName,
       _source: fields,
       _source: fields,
       body: {
       body: {
-        query: {}, // query
+        query: {
+          bool: {},
+        },
       },
       },
     };
     };
 
 
     return query;
     return query;
   }
   }
 
 
-  appendResultSize(query, from?, size?) {
+  appendResultSize(query: SearchQuery, from?: number, size?: number): void {
     query.from = from || DEFAULT_OFFSET;
     query.from = from || DEFAULT_OFFSET;
     query.size = size || DEFAULT_LIMIT;
     query.size = size || DEFAULT_LIMIT;
   }
   }
 
 
-  appendSortOrder(query, sortAxis: SORT_AXIS, sortOrder: SORT_ORDER) {
+  appendSortOrder(query: SearchQuery, sortAxis: SORT_AXIS, sortOrder: SORT_ORDER): void {
+    if (query.body == null) {
+      throw new Error('query.body is not initialized');
+    }
+
     // default sort order is score descending
     // default sort order is score descending
     const sort = ES_SORT_AXIS[sortAxis] || ES_SORT_AXIS[RELATION_SCORE];
     const sort = ES_SORT_AXIS[sortAxis] || ES_SORT_AXIS[RELATION_SCORE];
     const order = ES_SORT_ORDER[sortOrder] || ES_SORT_ORDER[DESC];
     const order = ES_SORT_ORDER[sortOrder] || ES_SORT_ORDER[DESC];
-    query.body.sort = { [sort]: { order } };
+
+    query.body.sort = {
+      [sort]: { order },
+    };
+
   }
   }
 
 
-  initializeBoolQuery(query) {
+  initializeBoolQuery(query: SearchQuery): SearchQuery {
     // query is created by createSearchQuery()
     // query is created by createSearchQuery()
-    if (!query.body.query.bool) {
-      query.body.query.bool = {};
+    if (query?.body?.query?.bool == null) {
+      throw new Error('query.body.query.bool is not initialized');
     }
     }
 
 
     const isInitialized = (query) => { return !!query && Array.isArray(query) };
     const isInitialized = (query) => { return !!query && Array.isArray(query) };
@@ -682,14 +712,30 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
     return query;
     return query;
   }
   }
 
 
-  appendCriteriaForQueryString(query, parsedKeywords: ESQueryTerms): void {
+  appendCriteriaForQueryString(query: SearchQuery, parsedKeywords: ESQueryTerms): void {
     query = this.initializeBoolQuery(query); // eslint-disable-line no-param-reassign
     query = this.initializeBoolQuery(query); // eslint-disable-line no-param-reassign
 
 
+    if (query.body?.query?.bool == null) {
+      throw new Error('query.body.query.bool is not initialized');
+    }
+
+    if (query.body?.query?.bool.must == null || !Array.isArray(query.body?.query?.bool.must)) {
+      throw new Error('query.body.query.bool.must is not initialized');
+    }
+
+    if (query.body?.query?.bool.must_not == null || !Array.isArray(query.body?.query?.bool.must_not)) {
+      throw new Error('query.body.query.bool.must_not is not initialized');
+    }
+
+    if (query.body?.query?.bool.filter == null || !Array.isArray(query.body?.query?.bool.filter)) {
+      throw new Error('query.body.query.bool.filter is not initialized');
+    }
+
     if (parsedKeywords.match.length > 0) {
     if (parsedKeywords.match.length > 0) {
       const q = {
       const q = {
         multi_match: {
         multi_match: {
           query: parsedKeywords.match.join(' '),
           query: parsedKeywords.match.join(' '),
-          type: 'most_fields',
+          type: 'most_fields' as const,
           fields: ['path.ja^2', 'path.en^2', 'body.ja', 'body.en', 'comments.ja', 'comments.en'],
           fields: ['path.ja^2', 'path.en^2', 'body.ja', 'body.en', 'comments.ja', 'comments.en'],
         },
         },
       };
       };
@@ -701,18 +747,18 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
         multi_match: {
         multi_match: {
           query: parsedKeywords.not_match.join(' '),
           query: parsedKeywords.not_match.join(' '),
           fields: ['path.ja', 'path.en', 'body.ja', 'body.en', 'comments.ja', 'comments.en'],
           fields: ['path.ja', 'path.en', 'body.ja', 'body.en', 'comments.ja', 'comments.en'],
-          operator: 'or',
+          operator: 'or' as const,
         },
         },
       };
       };
       query.body.query.bool.must_not.push(q);
       query.body.query.bool.must_not.push(q);
     }
     }
 
 
     if (parsedKeywords.phrase.length > 0) {
     if (parsedKeywords.phrase.length > 0) {
-      parsedKeywords.phrase.forEach((phrase) => {
+      for (const phrase of parsedKeywords.phrase) {
         const phraseQuery = {
         const phraseQuery = {
           multi_match: {
           multi_match: {
-            query: phrase, // each phrase is quoteted words like "This is GROWI"
-            type: 'phrase',
+            query: phrase, // query is created by createSearchQuery()
+            type: 'phrase' as const,
             fields: [
             fields: [
               // Not use "*.ja" fields here, because we want to analyze (parse) search words
               // Not use "*.ja" fields here, because we want to analyze (parse) search words
               'path.raw^2',
               'path.raw^2',
@@ -722,15 +768,15 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
           },
           },
         };
         };
         query.body.query.bool.must.push(phraseQuery);
         query.body.query.bool.must.push(phraseQuery);
-      });
+      }
     }
     }
 
 
     if (parsedKeywords.not_phrase.length > 0) {
     if (parsedKeywords.not_phrase.length > 0) {
-      parsedKeywords.not_phrase.forEach((phrase) => {
+      for (const phrase of parsedKeywords.not_phrase) {
         const notPhraseQuery = {
         const notPhraseQuery = {
           multi_match: {
           multi_match: {
             query: phrase, // each phrase is quoteted words
             query: phrase, // each phrase is quoteted words
-            type: 'phrase',
+            type: 'phrase' as const,
             fields: [
             fields: [
               // Not use "*.ja" fields here, because we want to analyze (parse) search words
               // Not use "*.ja" fields here, because we want to analyze (parse) search words
               'path.raw^2',
               'path.raw^2',
@@ -739,7 +785,7 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
           },
           },
         };
         };
         query.body.query.bool.must_not.push(notPhraseQuery);
         query.body.query.bool.must_not.push(notPhraseQuery);
-      });
+      }
     }
     }
 
 
     if (parsedKeywords.prefix.length > 0) {
     if (parsedKeywords.prefix.length > 0) {
@@ -771,12 +817,16 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
     }
     }
   }
   }
 
 
-  async filterPagesByViewer(query, user, userGroups) {
+  async filterPagesByViewer(query: SearchQuery, user, userGroups): Promise<void> {
     const showPagesRestrictedByOwner = !configManager.getConfig('security:list-policy:hideRestrictedByOwner');
     const showPagesRestrictedByOwner = !configManager.getConfig('security:list-policy:hideRestrictedByOwner');
     const showPagesRestrictedByGroup = !configManager.getConfig('security:list-policy:hideRestrictedByGroup');
     const showPagesRestrictedByGroup = !configManager.getConfig('security:list-policy:hideRestrictedByGroup');
 
 
     query = this.initializeBoolQuery(query); // eslint-disable-line no-param-reassign
     query = this.initializeBoolQuery(query); // eslint-disable-line no-param-reassign
 
 
+    if (query.body?.query?.bool?.filter == null || !Array.isArray(query.body?.query?.bool?.filter)) {
+      throw new Error('query.body.query.bool is not initialized');
+    }
+
     const Page = mongoose.model('Page') as unknown as PageModel;
     const Page = mongoose.model('Page') as unknown as PageModel;
     const {
     const {
       GRANT_PUBLIC, GRANT_SPECIFIED, GRANT_OWNER, GRANT_USER_GROUP,
       GRANT_PUBLIC, GRANT_SPECIFIED, GRANT_OWNER, GRANT_USER_GROUP,
@@ -835,7 +885,7 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
     query.body.query.bool.filter.push({ bool: { should: grantConditions } });
     query.body.query.bool.filter.push({ bool: { should: grantConditions } });
   }
   }
 
 
-  async appendFunctionScore(query, queryString) {
+  async appendFunctionScore(query, queryString): Promise<void> {
     const User = mongoose.model('User');
     const User = mongoose.model('User');
     const count = await User.count({}) || 1;
     const count = await User.count({}) || 1;
 
 
@@ -859,43 +909,11 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
     };
     };
   }
   }
 
 
-  // async appendVectorScore(query, queryString: string, username?: string): Promise<void> {
-
-  //   const searchAssistant = await getOrCreateSearchAssistant();
-
-  //   // generate keywords for vector
-  //   const run = await openaiClient.beta.threads.createAndRunPoll({
-  //     assistant_id: searchAssistant.id,
-  //     thread: {
-  //       messages: [
-  //         { role: 'user', content: 'globalLang: "en_US", userLang: "ja_JP", user_input: "武井さんがジョインしたのはいつですか?"' },
-  //         { role: 'assistant', content: '武井さん 武井 takei yuki ジョイン join 入社 加入 雇用開始 年月日 start date join employee' },
-  //         { role: 'user', content: `globalLang: "en_US", userLang: "ja_JP", user_input: "${queryString}"` },
-  //       ],
-  //     },
-  //   });
-  //   const messages = await openaiClient.beta.threads.messages.list(run.thread_id, {
-  //     limit: 1,
-  //   });
-  //   const content = messages.data[0].content[0];
-  //   const keywordsForVector = content.type === 'text' ? content.text.value : queryString;
-
-  //   logger.debug('keywordsFor: ', keywordsForVector);
-
-  //   const queryVector = (await embed(queryString, username))[0].embedding;
-
-  //   query.body.query = {
-  //     script_score: {
-  //       query: { ...query.body.query },
-  //       script: {
-  //         source: "cosineSimilarity(params.query_vector, 'body_embedded') + 1.0",
-  //         params: { query_vector: queryVector },
-  //       },
-  //     },
-  //   };
-  // }
-
-  appendHighlight(query) {
+  appendHighlight(query: SearchQuery): void {
+    if (query.body == null) {
+      throw new Error('query.body is not initialized');
+    }
+
     query.body.highlight = {
     query.body.highlight = {
       fragmenter: 'simple',
       fragmenter: 'simple',
       pre_tags: ["<em class='highlighted-keyword'>"],
       pre_tags: ["<em class='highlighted-keyword'>"],
@@ -928,15 +946,10 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
 
 
     const query = this.createSearchQuery();
     const query = this.createSearchQuery();
 
 
-    if (option?.vector) {
-      // await this.filterPagesByViewer(query, user, userGroups);
-      // await this.appendVectorScore(query, queryString, user?.username);
-    }
-    else {
-      this.appendCriteriaForQueryString(query, terms);
-      await this.filterPagesByViewer(query, user, userGroups);
-      await this.appendFunctionScore(query, queryString);
-    }
+    this.appendCriteriaForQueryString(query, terms);
+    await this.filterPagesByViewer(query, user, userGroups);
+    await this.appendFunctionScore(query, queryString);
+
 
 
     this.appendResultSize(query, from, size);
     this.appendResultSize(query, from, size);
 
 
@@ -967,7 +980,7 @@ class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQuer
   }
   }
 
 
   // remove pages whitch should nod Indexed
   // remove pages whitch should nod Indexed
-  async syncPagesUpdated(pages, user) {
+  async syncPagesUpdated(pages, user): Promise<void> {
     const shoudDeletePages: any[] = [];
     const shoudDeletePages: any[] = [];
 
 
     // delete if page should not indexed
     // delete if page should not indexed

+ 1 - 1
apps/app/src/services/renderer/recommended-whitelist.ts

@@ -47,7 +47,7 @@ export const tagNames: Array<string> = [
 export const attributes: Attributes = deepmerge(
 export const attributes: Attributes = deepmerge(
   relaxedSchemaAttributes,
   relaxedSchemaAttributes,
   {
   {
-    iframe: ['allow', 'referrerpolicy', 'sandbox', 'src', 'srcdoc'],
+    iframe: ['allow', 'referrerpolicy', 'sandbox', 'src'],
     video: ['controls', 'src', 'muted', 'preload', 'width', 'height', 'autoplay'],
     video: ['controls', 'src', 'muted', 'preload', 'width', 'height', 'autoplay'],
     // The special value 'data*' as a property name can be used to allow all data properties.
     // The special value 'data*' as a property name can be used to allow all data properties.
     // see: https://github.com/syntax-tree/hast-util-sanitize/
     // see: https://github.com/syntax-tree/hast-util-sanitize/

+ 2 - 5
apps/app/test/integration/crowi/crowi.test.js

@@ -3,21 +3,18 @@ import packageJson from '^/package.json';
 const { getInstance } = require('../setup-crowi');
 const { getInstance } = require('../setup-crowi');
 
 
 describe('Test for Crowi application context', () => {
 describe('Test for Crowi application context', () => {
-
   describe('construction', () => {
   describe('construction', () => {
-    test('initialize crowi context', async() => {
+    test('initialize crowi context', async () => {
       const crowi = await getInstance();
       const crowi = await getInstance();
       expect(crowi.version).toBe(packageJson.version);
       expect(crowi.version).toBe(packageJson.version);
       expect(typeof crowi.env).toBe('object');
       expect(typeof crowi.env).toBe('object');
     });
     });
 
 
-    test('config getter, setter', async() => {
+    test('config getter, setter', async () => {
       const crowi = await getInstance();
       const crowi = await getInstance();
       expect(crowi.getConfig()).toEqual({});
       expect(crowi.getConfig()).toEqual({});
       crowi.setConfig({ test: 1 });
       crowi.setConfig({ test: 1 });
       expect(crowi.getConfig()).toEqual({ test: 1 });
       expect(crowi.getConfig()).toEqual({ test: 1 });
     });
     });
-
   });
   });
-
 });
 });

+ 43 - 15
apps/app/test/integration/global-setup.js

@@ -4,10 +4,10 @@ import { getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
 
 
 // check env
 // check env
 if (process.env.NODE_ENV !== 'test') {
 if (process.env.NODE_ENV !== 'test') {
-  throw new Error('\'process.env.NODE_ENV\' must be \'test\'');
+  throw new Error("'process.env.NODE_ENV' must be 'test'");
 }
 }
 
 
-module.exports = async() => {
+module.exports = async () => {
   await mongoose.connect(getMongoUri(), mongoOptions);
   await mongoose.connect(getMongoUri(), mongoOptions);
 
 
   // drop database
   // drop database
@@ -17,25 +17,51 @@ module.exports = async() => {
   const pageCollection = mongoose.connection.collection('pages');
   const pageCollection = mongoose.connection.collection('pages');
   const userCollection = mongoose.connection.collection('users');
   const userCollection = mongoose.connection.collection('users');
   const userGroupCollection = mongoose.connection.collection('usergroups');
   const userGroupCollection = mongoose.connection.collection('usergroups');
-  const userGroupRelationsCollection = mongoose.connection.collection('usergrouprelations');
+  const userGroupRelationsCollection =
+    mongoose.connection.collection('usergrouprelations');
 
 
   // create global user & rootPage
   // create global user & rootPage
-  const globalUser = (await userCollection.insertMany([{ name: 'globalUser', username: 'globalUser', email: 'globalUser@example.com' }]))[0];
+  const globalUser = (
+    await userCollection.insertMany([
+      {
+        name: 'globalUser',
+        username: 'globalUser',
+        email: 'globalUser@example.com',
+      },
+    ])
+  )[0];
   const gGroupUserId1 = new mongoose.Types.ObjectId();
   const gGroupUserId1 = new mongoose.Types.ObjectId();
   const gGroupUserId2 = new mongoose.Types.ObjectId();
   const gGroupUserId2 = new mongoose.Types.ObjectId();
   const gGroupUserId3 = new mongoose.Types.ObjectId();
   const gGroupUserId3 = new mongoose.Types.ObjectId();
 
 
   await userCollection.insertMany([
   await userCollection.insertMany([
-    { name: 'v5DummyUser1', username: 'v5DummyUser1', email: 'v5DummyUser1@example.com' },
-    { name: 'v5DummyUser2', username: 'v5DummyUser2', email: 'v5DummyUser2@example.com' },
     {
     {
-      _id: gGroupUserId1, name: 'gGroupUser1', username: 'gGroupUser1', email: 'gGroupUser1@example.com',
+      name: 'v5DummyUser1',
+      username: 'v5DummyUser1',
+      email: 'v5DummyUser1@example.com',
     },
     },
     {
     {
-      _id: gGroupUserId2, name: 'gGroupUser2', username: 'gGroupUser2', email: 'gGroupUser2@example.com',
+      name: 'v5DummyUser2',
+      username: 'v5DummyUser2',
+      email: 'v5DummyUser2@example.com',
     },
     },
     {
     {
-      _id: gGroupUserId3, name: 'gGroupUser3', username: 'gGroupUser3', email: 'gGroupUser3@example.com',
+      _id: gGroupUserId1,
+      name: 'gGroupUser1',
+      username: 'gGroupUser1',
+      email: 'gGroupUser1@example.com',
+    },
+    {
+      _id: gGroupUserId2,
+      name: 'gGroupUser2',
+      username: 'gGroupUser2',
+      email: 'gGroupUser2@example.com',
+    },
+    {
+      _id: gGroupUserId3,
+      name: 'gGroupUser3',
+      username: 'gGroupUser3',
+      email: 'gGroupUser3@example.com',
     },
     },
   ]);
   ]);
   const gGroupIdIsolate = new mongoose.Types.ObjectId();
   const gGroupIdIsolate = new mongoose.Types.ObjectId();
@@ -104,12 +130,14 @@ module.exports = async() => {
       createdAt: new Date(),
       createdAt: new Date(),
     },
     },
   ]);
   ]);
-  await pageCollection.insertMany([{
-    path: '/',
-    grant: 1,
-    creator: globalUser,
-    lastUpdateUser: globalUser,
-  }]);
+  await pageCollection.insertMany([
+    {
+      path: '/',
+      grant: 1,
+      creator: globalUser,
+      lastUpdateUser: globalUser,
+    },
+  ]);
 
 
   await mongoose.disconnect();
   await mongoose.disconnect();
 };
 };

+ 1 - 2
apps/app/test/integration/global-teardown.js

@@ -5,5 +5,4 @@
  * are NOT available in setup scripts
  * are NOT available in setup scripts
  *********************************************************** */
  *********************************************************** */
 
 
-module.exports = async() => {
-};
+module.exports = async () => {};

+ 101 - 89
apps/app/test/integration/middlewares/login-required.test.js

@@ -10,11 +10,17 @@ describe('loginRequired', () => {
   let loginRequired;
   let loginRequired;
   let loginRequiredWithFallback;
   let loginRequiredWithFallback;
 
 
-  beforeEach(async() => {
+  beforeEach(async () => {
     crowi = await getInstance();
     crowi = await getInstance();
-    loginRequiredStrictly = require('~/server/middlewares/login-required')(crowi);
+    loginRequiredStrictly = require('~/server/middlewares/login-required')(
+      crowi,
+    );
     loginRequired = require('~/server/middlewares/login-required')(crowi, true);
     loginRequired = require('~/server/middlewares/login-required')(crowi, true);
-    loginRequiredWithFallback = require('~/server/middlewares/login-required')(crowi, false, fallbackMock);
+    loginRequiredWithFallback = require('~/server/middlewares/login-required')(
+      crowi,
+      false,
+      fallbackMock,
+    );
   });
   });
 
 
   describe('not strict mode', () => {
   describe('not strict mode', () => {
@@ -29,7 +35,7 @@ describe('loginRequired', () => {
 
 
       let isGuestAllowedToReadSpy;
       let isGuestAllowedToReadSpy;
 
 
-      beforeEach(async() => {
+      beforeEach(async () => {
         // setup req
         // setup req
         req = {
         req = {
           originalUrl: 'original url 1',
           originalUrl: 'original url 1',
@@ -38,37 +44,40 @@ describe('loginRequired', () => {
         // reset session object
         // reset session object
         req.session = {};
         req.session = {};
         // prepare spy for AclService.isGuestAllowedToRead
         // prepare spy for AclService.isGuestAllowedToRead
-        isGuestAllowedToReadSpy = jest.spyOn(crowi.aclService, 'isGuestAllowedToRead')
+        isGuestAllowedToReadSpy = jest
+          .spyOn(crowi.aclService, 'isGuestAllowedToRead')
           .mockImplementation(() => false);
           .mockImplementation(() => false);
       });
       });
 
 
       /* eslint-disable indent */
       /* eslint-disable indent */
       test.each`
       test.each`
-        userStatus  | expectedPath
-        ${1}        | ${'/login/error/registered'}
-        ${3}        | ${'/login/error/suspended'}
-        ${5}        | ${'/invited'}
-      `('redirect to \'$expectedPath\' when user.status is \'$userStatus\'', ({ userStatus, expectedPath }) => {
-
-        req.user = {
-          _id: 'user id',
-          status: userStatus,
-        };
-
-        const result = loginRequired(req, res, next);
-
-        expect(isGuestAllowedToReadSpy).not.toHaveBeenCalled();
-        expect(next).not.toHaveBeenCalled();
-        expect(fallbackMock).not.toHaveBeenCalled();
-        expect(res.sendStatus).not.toHaveBeenCalled();
-        expect(res.redirect).toHaveBeenCalledTimes(1);
-        expect(res.redirect).toHaveBeenCalledWith(expectedPath);
-        expect(result).toBe('redirect');
-        expect(req.session.redirectTo).toBe(undefined);
-      });
+        userStatus | expectedPath
+        ${1}       | ${'/login/error/registered'}
+        ${3}       | ${'/login/error/suspended'}
+        ${5}       | ${'/invited'}
+      `(
+        "redirect to '$expectedPath' when user.status is '$userStatus'",
+        ({ userStatus, expectedPath }) => {
+          req.user = {
+            _id: 'user id',
+            status: userStatus,
+          };
+
+          const result = loginRequired(req, res, next);
+
+          expect(isGuestAllowedToReadSpy).not.toHaveBeenCalled();
+          expect(next).not.toHaveBeenCalled();
+          expect(fallbackMock).not.toHaveBeenCalled();
+          expect(res.sendStatus).not.toHaveBeenCalled();
+          expect(res.redirect).toHaveBeenCalledTimes(1);
+          expect(res.redirect).toHaveBeenCalledWith(expectedPath);
+          expect(result).toBe('redirect');
+          expect(req.session.redirectTo).toBe(undefined);
+        },
+      );
       /* eslint-disable indent */
       /* eslint-disable indent */
 
 
-      test('redirect to \'/login\' when the user does not loggedin', () => {
+      test("redirect to '/login' when the user does not loggedin", () => {
         req.baseUrl = '/path/that/requires/loggedin';
         req.baseUrl = '/path/that/requires/loggedin';
 
 
         const result = loginRequired(req, res, next);
         const result = loginRequired(req, res, next);
@@ -84,7 +93,6 @@ describe('loginRequired', () => {
       });
       });
 
 
       test('pass anyone into sharedPage', () => {
       test('pass anyone into sharedPage', () => {
-
         req.isSharedPage = true;
         req.isSharedPage = true;
 
 
         const result = loginRequired(req, res, next);
         const result = loginRequired(req, res, next);
@@ -96,7 +104,6 @@ describe('loginRequired', () => {
         expect(res.redirect).not.toHaveBeenCalled();
         expect(res.redirect).not.toHaveBeenCalled();
         expect(result).toBe('next');
         expect(result).toBe('next');
       });
       });
-
     });
     });
 
 
     describe('and when aclService.isGuestAllowedToRead() returns true', () => {
     describe('and when aclService.isGuestAllowedToRead() returns true', () => {
@@ -104,7 +111,7 @@ describe('loginRequired', () => {
 
 
       let isGuestAllowedToReadSpy;
       let isGuestAllowedToReadSpy;
 
 
-      beforeEach(async() => {
+      beforeEach(async () => {
         // setup req
         // setup req
         req = {
         req = {
           originalUrl: 'original url 1',
           originalUrl: 'original url 1',
@@ -113,38 +120,40 @@ describe('loginRequired', () => {
         // reset session object
         // reset session object
         req.session = {};
         req.session = {};
         // prepare spy for AclService.isGuestAllowedToRead
         // prepare spy for AclService.isGuestAllowedToRead
-        isGuestAllowedToReadSpy = jest.spyOn(crowi.aclService, 'isGuestAllowedToRead')
+        isGuestAllowedToReadSpy = jest
+          .spyOn(crowi.aclService, 'isGuestAllowedToRead')
           .mockImplementation(() => true);
           .mockImplementation(() => true);
       });
       });
 
 
       /* eslint-disable indent */
       /* eslint-disable indent */
       test.each`
       test.each`
-        userStatus  | expectedPath
-        ${1}        | ${'/login/error/registered'}
-        ${3}        | ${'/login/error/suspended'}
-        ${5}        | ${'/invited'}
-      `('redirect to \'$expectedPath\' when user.status is \'$userStatus\'', ({ userStatus, expectedPath }) => {
-
-        req.user = {
-          _id: 'user id',
-          status: userStatus,
-        };
-
-        const result = loginRequired(req, res, next);
-
-        expect(isGuestAllowedToReadSpy).not.toHaveBeenCalled();
-        expect(next).not.toHaveBeenCalled();
-        expect(fallbackMock).not.toHaveBeenCalled();
-        expect(res.sendStatus).not.toHaveBeenCalled();
-        expect(res.redirect).toHaveBeenCalledTimes(1);
-        expect(res.redirect).toHaveBeenCalledWith(expectedPath);
-        expect(result).toBe('redirect');
-        expect(req.session.redirectTo).toBe(undefined);
-      });
+        userStatus | expectedPath
+        ${1}       | ${'/login/error/registered'}
+        ${3}       | ${'/login/error/suspended'}
+        ${5}       | ${'/invited'}
+      `(
+        "redirect to '$expectedPath' when user.status is '$userStatus'",
+        ({ userStatus, expectedPath }) => {
+          req.user = {
+            _id: 'user id',
+            status: userStatus,
+          };
+
+          const result = loginRequired(req, res, next);
+
+          expect(isGuestAllowedToReadSpy).not.toHaveBeenCalled();
+          expect(next).not.toHaveBeenCalled();
+          expect(fallbackMock).not.toHaveBeenCalled();
+          expect(res.sendStatus).not.toHaveBeenCalled();
+          expect(res.redirect).toHaveBeenCalledTimes(1);
+          expect(res.redirect).toHaveBeenCalledWith(expectedPath);
+          expect(result).toBe('redirect');
+          expect(req.session.redirectTo).toBe(undefined);
+        },
+      );
       /* eslint-disable indent */
       /* eslint-disable indent */
 
 
       test('pass guest user', () => {
       test('pass guest user', () => {
-
         const result = loginRequired(req, res, next);
         const result = loginRequired(req, res, next);
 
 
         expect(isGuestAllowedToReadSpy).toHaveBeenCalledTimes(1);
         expect(isGuestAllowedToReadSpy).toHaveBeenCalledTimes(1);
@@ -156,7 +165,6 @@ describe('loginRequired', () => {
       });
       });
 
 
       test('pass anyone into sharedPage', () => {
       test('pass anyone into sharedPage', () => {
-
         req.isSharedPage = true;
         req.isSharedPage = true;
 
 
         const result = loginRequired(req, res, next);
         const result = loginRequired(req, res, next);
@@ -168,12 +176,9 @@ describe('loginRequired', () => {
         expect(res.redirect).not.toHaveBeenCalled();
         expect(res.redirect).not.toHaveBeenCalled();
         expect(result).toBe('next');
         expect(result).toBe('next');
       });
       });
-
     });
     });
-
   });
   });
 
 
-
   describe('strict mode', () => {
   describe('strict mode', () => {
     // setup req/res/next
     // setup req/res/next
     const req = {
     const req = {
@@ -188,14 +193,17 @@ describe('loginRequired', () => {
 
 
     let isGuestAllowedToReadSpy;
     let isGuestAllowedToReadSpy;
 
 
-    beforeEach(async() => {
+    beforeEach(async () => {
       // reset session object
       // reset session object
       req.session = {};
       req.session = {};
       // spy for AclService.isGuestAllowedToRead
       // spy for AclService.isGuestAllowedToRead
-      isGuestAllowedToReadSpy = jest.spyOn(crowi.aclService, 'isGuestAllowedToRead');
+      isGuestAllowedToReadSpy = jest.spyOn(
+        crowi.aclService,
+        'isGuestAllowedToRead',
+      );
     });
     });
 
 
-    test('send status 403 when \'req.baseUrl\' starts with \'_api\'', () => {
+    test("send status 403 when 'req.baseUrl' starts with '_api'", () => {
       req.baseUrl = '/_api/someapi';
       req.baseUrl = '/_api/someapi';
 
 
       const result = loginRequiredStrictly(req, res, next);
       const result = loginRequiredStrictly(req, res, next);
@@ -209,7 +217,7 @@ describe('loginRequired', () => {
       expect(result).toBe('sendStatus');
       expect(result).toBe('sendStatus');
     });
     });
 
 
-    test('redirect to \'/login\' when the user does not loggedin', () => {
+    test("redirect to '/login' when the user does not loggedin", () => {
       req.baseUrl = '/path/that/requires/loggedin';
       req.baseUrl = '/path/that/requires/loggedin';
 
 
       const result = loginRequiredStrictly(req, res, next);
       const result = loginRequiredStrictly(req, res, next);
@@ -245,30 +253,33 @@ describe('loginRequired', () => {
 
 
     /* eslint-disable indent */
     /* eslint-disable indent */
     test.each`
     test.each`
-      userStatus  | expectedPath
-      ${1}        | ${'/login/error/registered'}
-      ${3}        | ${'/login/error/suspended'}
-      ${5}        | ${'/invited'}
-    `('redirect to \'$expectedPath\' when user.status is \'$userStatus\'', ({ userStatus, expectedPath }) => {
-      req.user = {
-        _id: 'user id',
-        status: userStatus,
-      };
+      userStatus | expectedPath
+      ${1}       | ${'/login/error/registered'}
+      ${3}       | ${'/login/error/suspended'}
+      ${5}       | ${'/invited'}
+    `(
+      "redirect to '$expectedPath' when user.status is '$userStatus'",
+      ({ userStatus, expectedPath }) => {
+        req.user = {
+          _id: 'user id',
+          status: userStatus,
+        };
 
 
-      const result = loginRequiredStrictly(req, res, next);
+        const result = loginRequiredStrictly(req, res, next);
 
 
-      expect(isGuestAllowedToReadSpy).not.toHaveBeenCalled();
-      expect(next).not.toHaveBeenCalled();
-      expect(fallbackMock).not.toHaveBeenCalled();
-      expect(res.sendStatus).not.toHaveBeenCalled();
-      expect(res.redirect).toHaveBeenCalledTimes(1);
-      expect(res.redirect).toHaveBeenCalledWith(expectedPath);
-      expect(result).toBe('redirect');
-      expect(req.session.redirectTo).toBe(undefined);
-    });
+        expect(isGuestAllowedToReadSpy).not.toHaveBeenCalled();
+        expect(next).not.toHaveBeenCalled();
+        expect(fallbackMock).not.toHaveBeenCalled();
+        expect(res.sendStatus).not.toHaveBeenCalled();
+        expect(res.redirect).toHaveBeenCalledTimes(1);
+        expect(res.redirect).toHaveBeenCalledWith(expectedPath);
+        expect(result).toBe('redirect');
+        expect(req.session.redirectTo).toBe(undefined);
+      },
+    );
     /* eslint-disable indent */
     /* eslint-disable indent */
 
 
-    test('redirect to \'/login\' when user.status is \'STATUS_DELETED\'', () => {
+    test("redirect to '/login' when user.status is 'STATUS_DELETED'", () => {
       const User = crowi.model('User');
       const User = crowi.model('User');
 
 
       req.baseUrl = '/path/that/requires/loggedin';
       req.baseUrl = '/path/that/requires/loggedin';
@@ -288,7 +299,6 @@ describe('loginRequired', () => {
       expect(result).toBe('redirect');
       expect(result).toBe('redirect');
       expect(req.session.redirectTo).toBe(undefined);
       expect(req.session.redirectTo).toBe(undefined);
     });
     });
-
   });
   });
 
 
   describe('specified fallback', () => {
   describe('specified fallback', () => {
@@ -305,14 +315,17 @@ describe('loginRequired', () => {
 
 
     let isGuestAllowedToReadSpy;
     let isGuestAllowedToReadSpy;
 
 
-    beforeEach(async() => {
+    beforeEach(async () => {
       // reset session object
       // reset session object
       req.session = {};
       req.session = {};
       // spy for AclService.isGuestAllowedToRead
       // spy for AclService.isGuestAllowedToRead
-      isGuestAllowedToReadSpy = jest.spyOn(crowi.aclService, 'isGuestAllowedToRead');
-          });
+      isGuestAllowedToReadSpy = jest.spyOn(
+        crowi.aclService,
+        'isGuestAllowedToRead',
+      );
+    });
 
 
-    test('invoke fallback when \'req.path\' starts with \'_api\'', () => {
+    test("invoke fallback when 'req.path' starts with '_api'", () => {
       req.path = '/_api/someapi';
       req.path = '/_api/someapi';
 
 
       const result = loginRequiredWithFallback(req, res, next);
       const result = loginRequiredWithFallback(req, res, next);
@@ -339,6 +352,5 @@ describe('loginRequired', () => {
       expect(fallbackMock).toHaveBeenCalledWith(req, res, next);
       expect(fallbackMock).toHaveBeenCalledWith(req, res, next);
       expect(result).toBe('fallback');
       expect(result).toBe('fallback');
     });
     });
-
   });
   });
 });
 });

+ 15 - 14
apps/app/test/integration/migrations/20210913153942-migrate-slack-app-integration-schema.test.ts

@@ -1,29 +1,35 @@
-import { Collection } from 'mongodb';
+import type { Collection } from 'mongodb';
 import mongoose from 'mongoose';
 import mongoose from 'mongoose';
 
 
 import migrate from '~/migrations/20210913153942-migrate-slack-app-integration-schema';
 import migrate from '~/migrations/20210913153942-migrate-slack-app-integration-schema';
 
 
 describe('migrate-slack-app-integration-schema', () => {
 describe('migrate-slack-app-integration-schema', () => {
-
   let collection: Collection;
   let collection: Collection;
 
 
-  beforeAll(async() => {
+  beforeAll(async () => {
     collection = mongoose.connection.collection('slackappintegrations');
     collection = mongoose.connection.collection('slackappintegrations');
 
 
     await collection.insertMany([
     await collection.insertMany([
       {
       {
-        tokenGtoP: 'tokenGtoP1', tokenPtoG: 'tokenPtoG1', permissionsForBroadcastUseCommands: { foo: true }, permissionsForSingleUseCommands: { bar: true },
+        tokenGtoP: 'tokenGtoP1',
+        tokenPtoG: 'tokenPtoG1',
+        permissionsForBroadcastUseCommands: { foo: true },
+        permissionsForSingleUseCommands: { bar: true },
       },
       },
       {
       {
-        tokenGtoP: 'tokenGtoP2', tokenPtoG: 'tokenPtoG2', supportedCommandsForBroadcastUse: ['foo'], supportedCommandsForSingleUse: ['bar'],
+        tokenGtoP: 'tokenGtoP2',
+        tokenPtoG: 'tokenPtoG2',
+        supportedCommandsForBroadcastUse: ['foo'],
+        supportedCommandsForSingleUse: ['bar'],
       },
       },
       {
       {
-        tokenGtoP: 'tokenGtoP3', tokenPtoG: 'tokenPtoG3',
+        tokenGtoP: 'tokenGtoP3',
+        tokenPtoG: 'tokenPtoG3',
       },
       },
     ]);
     ]);
   });
   });
 
 
-  test('up is applied successfully', async() => {
+  test('up is applied successfully', async () => {
     // setup
     // setup
     const doc1 = await collection.findOne({ tokenGtoP: 'tokenGtoP1' });
     const doc1 = await collection.findOne({ tokenGtoP: 'tokenGtoP1' });
     const doc2 = await collection.findOne({ tokenGtoP: 'tokenGtoP2' });
     const doc2 = await collection.findOne({ tokenGtoP: 'tokenGtoP2' });
@@ -46,12 +52,8 @@ describe('migrate-slack-app-integration-schema', () => {
       _id: doc2?._id,
       _id: doc2?._id,
       tokenGtoP: 'tokenGtoP2',
       tokenGtoP: 'tokenGtoP2',
       tokenPtoG: 'tokenPtoG2',
       tokenPtoG: 'tokenPtoG2',
-      supportedCommandsForBroadcastUse: [
-        'foo',
-      ],
-      supportedCommandsForSingleUse: [
-        'bar',
-      ],
+      supportedCommandsForBroadcastUse: ['foo'],
+      supportedCommandsForSingleUse: ['bar'],
     });
     });
     expect(doc3).toStrictEqual({
     expect(doc3).toStrictEqual({
       _id: doc3?._id,
       _id: doc3?._id,
@@ -116,5 +118,4 @@ describe('migrate-slack-app-integration-schema', () => {
       },
       },
     });
     });
   });
   });
-
 });
 });

+ 40 - 22
apps/app/test/integration/models/page-redirect.test.js

@@ -7,19 +7,19 @@ describe('PageRedirect', () => {
   let crowi;
   let crowi;
   let PageRedirect;
   let PageRedirect;
 
 
-  beforeAll(async() => {
+  beforeAll(async () => {
     crowi = await getInstance();
     crowi = await getInstance();
 
 
     PageRedirect = mongoose.model('PageRedirect');
     PageRedirect = mongoose.model('PageRedirect');
   });
   });
 
 
-  beforeEach(async() => {
+  beforeEach(async () => {
     // clear collection
     // clear collection
     await PageRedirect.deleteMany({});
     await PageRedirect.deleteMany({});
   });
   });
 
 
   describe('.removePageRedirectsByToPath', () => {
   describe('.removePageRedirectsByToPath', () => {
-    test('works fine', async() => {
+    test('works fine', async () => {
       // setup:
       // setup:
       await PageRedirect.insertMany([
       await PageRedirect.insertMany([
         { fromPath: '/org/path1', toPath: '/path1' },
         { fromPath: '/org/path1', toPath: '/path1' },
@@ -28,48 +28,66 @@ describe('PageRedirect', () => {
         { fromPath: '/org/path33', toPath: '/org/path333' },
         { fromPath: '/org/path33', toPath: '/org/path333' },
         { fromPath: '/org/path333', toPath: '/path3' },
         { fromPath: '/org/path333', toPath: '/path3' },
       ]);
       ]);
-      expect(await PageRedirect.findOne({ fromPath: '/org/path1' })).not.toBeNull();
-      expect(await PageRedirect.findOne({ fromPath: '/org/path2' })).not.toBeNull();
-      expect(await PageRedirect.findOne({ fromPath: '/org/path3' })).not.toBeNull();
-      expect(await PageRedirect.findOne({ fromPath: '/org/path33' })).not.toBeNull();
-      expect(await PageRedirect.findOne({ fromPath: '/org/path333' })).not.toBeNull();
+      expect(
+        await PageRedirect.findOne({ fromPath: '/org/path1' }),
+      ).not.toBeNull();
+      expect(
+        await PageRedirect.findOne({ fromPath: '/org/path2' }),
+      ).not.toBeNull();
+      expect(
+        await PageRedirect.findOne({ fromPath: '/org/path3' }),
+      ).not.toBeNull();
+      expect(
+        await PageRedirect.findOne({ fromPath: '/org/path33' }),
+      ).not.toBeNull();
+      expect(
+        await PageRedirect.findOne({ fromPath: '/org/path333' }),
+      ).not.toBeNull();
 
 
       // when:
       // when:
       // remove all documents that have { toPath: '/path/3' }
       // remove all documents that have { toPath: '/path/3' }
       await PageRedirect.removePageRedirectsByToPath('/path3');
       await PageRedirect.removePageRedirectsByToPath('/path3');
 
 
       // then:
       // then:
-      expect(await PageRedirect.findOne({ fromPath: '/org/path1' })).not.toBeNull();
-      expect(await PageRedirect.findOne({ fromPath: '/org/path2' })).not.toBeNull();
+      expect(
+        await PageRedirect.findOne({ fromPath: '/org/path1' }),
+      ).not.toBeNull();
+      expect(
+        await PageRedirect.findOne({ fromPath: '/org/path2' }),
+      ).not.toBeNull();
       expect(await PageRedirect.findOne({ fromPath: '/org/path3' })).toBeNull();
       expect(await PageRedirect.findOne({ fromPath: '/org/path3' })).toBeNull();
-      expect(await PageRedirect.findOne({ fromPath: '/org/path33' })).toBeNull();
-      expect(await PageRedirect.findOne({ fromPath: '/org/path333' })).toBeNull();
+      expect(
+        await PageRedirect.findOne({ fromPath: '/org/path33' }),
+      ).toBeNull();
+      expect(
+        await PageRedirect.findOne({ fromPath: '/org/path333' }),
+      ).toBeNull();
     });
     });
   });
   });
 
 
   describe('.retrievePageRedirectEndpoints', () => {
   describe('.retrievePageRedirectEndpoints', () => {
-    test('shoud return null when data is not found', async() => {
+    test('shoud return null when data is not found', async () => {
       // setup:
       // setup:
       expect(await PageRedirect.findOne({ fromPath: '/path1' })).toBeNull();
       expect(await PageRedirect.findOne({ fromPath: '/path1' })).toBeNull();
 
 
       // when:
       // when:
       // retrieve
       // retrieve
-      const endpoints = await PageRedirect.retrievePageRedirectEndpoints('/path1');
+      const endpoints =
+        await PageRedirect.retrievePageRedirectEndpoints('/path1');
 
 
       // then:
       // then:
       expect(endpoints).toBeNull();
       expect(endpoints).toBeNull();
     });
     });
 
 
-    test('shoud return IPageRedirectEnds (start and end is the same)', async() => {
+    test('shoud return IPageRedirectEnds (start and end is the same)', async () => {
       // setup:
       // setup:
-      await PageRedirect.insertMany([
-        { fromPath: '/path1', toPath: '/path2' },
-      ]);
+      await PageRedirect.insertMany([{ fromPath: '/path1', toPath: '/path2' }]);
       expect(await PageRedirect.findOne({ fromPath: '/path1' })).not.toBeNull();
       expect(await PageRedirect.findOne({ fromPath: '/path1' })).not.toBeNull();
 
 
       // when:
       // when:
       // retrieve
       // retrieve
-      const endpoints = await PageRedirect.retrievePageRedirectEndpoints('/path1');
+      const endpoints =
+        await PageRedirect.retrievePageRedirectEndpoints('/path1');
 
 
       // then:
       // then:
       expect(endpoints).not.toBeNull();
       expect(endpoints).not.toBeNull();
@@ -81,7 +99,7 @@ describe('PageRedirect', () => {
       expect(endpoints.end.toPath).toEqual('/path2');
       expect(endpoints.end.toPath).toEqual('/path2');
     });
     });
 
 
-    test('shoud return IPageRedirectEnds', async() => {
+    test('shoud return IPageRedirectEnds', async () => {
       // setup:
       // setup:
       await PageRedirect.insertMany([
       await PageRedirect.insertMany([
         { fromPath: '/path1', toPath: '/path2' },
         { fromPath: '/path1', toPath: '/path2' },
@@ -94,7 +112,8 @@ describe('PageRedirect', () => {
 
 
       // when:
       // when:
       // retrieve
       // retrieve
-      const endpoints = await PageRedirect.retrievePageRedirectEndpoints('/path1');
+      const endpoints =
+        await PageRedirect.retrievePageRedirectEndpoints('/path1');
 
 
       // then:
       // then:
       expect(endpoints).not.toBeNull();
       expect(endpoints).not.toBeNull();
@@ -106,5 +125,4 @@ describe('PageRedirect', () => {
       expect(endpoints.end.toPath).toEqual('/path4');
       expect(endpoints.end.toPath).toEqual('/path4');
     });
     });
   });
   });
-
 });
 });

+ 98 - 50
apps/app/test/integration/models/page.test.js

@@ -17,7 +17,7 @@ describe('Page', () => {
   let UserGroup;
   let UserGroup;
   let UserGroupRelation;
   let UserGroupRelation;
 
 
-  beforeAll(async() => {
+  beforeAll(async () => {
     crowi = await getInstance();
     crowi = await getInstance();
 
 
     User = mongoose.model('User');
     User = mongoose.model('User');
@@ -26,11 +26,22 @@ describe('Page', () => {
     Page = mongoose.model('Page');
     Page = mongoose.model('Page');
     PageQueryBuilder = Page.PageQueryBuilder;
     PageQueryBuilder = Page.PageQueryBuilder;
 
 
-
     await User.insertMany([
     await User.insertMany([
-      { name: 'Anon 0', username: 'anonymous0', email: 'anonymous0@example.com' },
-      { name: 'Anon 1', username: 'anonymous1', email: 'anonymous1@example.com' },
-      { name: 'Anon 2', username: 'anonymous2', email: 'anonymous2@example.com' },
+      {
+        name: 'Anon 0',
+        username: 'anonymous0',
+        email: 'anonymous0@example.com',
+      },
+      {
+        name: 'Anon 1',
+        username: 'anonymous1',
+        email: 'anonymous1@example.com',
+      },
+      {
+        name: 'Anon 2',
+        username: 'anonymous2',
+        email: 'anonymous2@example.com',
+      },
     ]);
     ]);
 
 
     await UserGroup.insertMany([
     await UserGroup.insertMany([
@@ -126,7 +137,7 @@ describe('Page', () => {
 
 
   describe('.isPublic', () => {
   describe('.isPublic', () => {
     describe('with a public page', () => {
     describe('with a public page', () => {
-      test('should return true', async() => {
+      test('should return true', async () => {
         const page = await Page.findOne({ path: '/grant/public' });
         const page = await Page.findOne({ path: '/grant/public' });
         expect(page.isPublic()).toEqual(true);
         expect(page.isPublic()).toEqual(true);
       });
       });
@@ -134,7 +145,7 @@ describe('Page', () => {
 
 
     ['restricted', 'specified', 'owner'].forEach((grant) => {
     ['restricted', 'specified', 'owner'].forEach((grant) => {
       describe(`with a ${grant} page`, () => {
       describe(`with a ${grant} page`, () => {
-        test('should return false', async() => {
+        test('should return false', async () => {
           const page = await Page.findOne({ path: `/grant/${grant}` });
           const page = await Page.findOne({ path: `/grant/${grant}` });
           expect(page.isPublic()).toEqual(false);
           expect(page.isPublic()).toEqual(false);
         });
         });
@@ -152,20 +163,22 @@ describe('Page', () => {
     test('should return reverted trash page name', () => {
     test('should return reverted trash page name', () => {
       expect(Page.getRevertDeletedPageName('/hoge')).toEqual('/hoge');
       expect(Page.getRevertDeletedPageName('/hoge')).toEqual('/hoge');
       expect(Page.getRevertDeletedPageName('/trash/hoge')).toEqual('/hoge');
       expect(Page.getRevertDeletedPageName('/trash/hoge')).toEqual('/hoge');
-      expect(Page.getRevertDeletedPageName('/trash/hoge/trash')).toEqual('/hoge/trash');
+      expect(Page.getRevertDeletedPageName('/trash/hoge/trash')).toEqual(
+        '/hoge/trash',
+      );
     });
     });
   });
   });
 
 
   describe('.isAccessiblePageByViewer', () => {
   describe('.isAccessiblePageByViewer', () => {
     describe('with a granted page', () => {
     describe('with a granted page', () => {
-      test('should return true with granted user', async() => {
+      test('should return true with granted user', async () => {
         const user = await User.findOne({ email: 'anonymous0@example.com' });
         const user = await User.findOne({ email: 'anonymous0@example.com' });
         const page = await Page.findOne({ path: '/user/anonymous0/memo' });
         const page = await Page.findOne({ path: '/user/anonymous0/memo' });
 
 
         const bool = await Page.isAccessiblePageByViewer(page.id, user);
         const bool = await Page.isAccessiblePageByViewer(page.id, user);
         expect(bool).toEqual(true);
         expect(bool).toEqual(true);
       });
       });
-      test('should return false without user', async() => {
+      test('should return false without user', async () => {
         const user = null;
         const user = null;
         const page = await Page.findOne({ path: '/user/anonymous0/memo' });
         const page = await Page.findOne({ path: '/user/anonymous0/memo' });
 
 
@@ -175,14 +188,14 @@ describe('Page', () => {
     });
     });
 
 
     describe('with a public page', () => {
     describe('with a public page', () => {
-      test('should return true with user', async() => {
+      test('should return true with user', async () => {
         const user = await User.findOne({ email: 'anonymous1@example.com' });
         const user = await User.findOne({ email: 'anonymous1@example.com' });
         const page = await Page.findOne({ path: '/grant/public' });
         const page = await Page.findOne({ path: '/grant/public' });
 
 
         const bool = await Page.isAccessiblePageByViewer(page.id, user);
         const bool = await Page.isAccessiblePageByViewer(page.id, user);
         expect(bool).toEqual(true);
         expect(bool).toEqual(true);
       });
       });
-      test('should return true with out', async() => {
+      test('should return true with out', async () => {
         const user = null;
         const user = null;
         const page = await Page.findOne({ path: '/grant/public' });
         const page = await Page.findOne({ path: '/grant/public' });
 
 
@@ -192,14 +205,14 @@ describe('Page', () => {
     });
     });
 
 
     describe('with a restricted page', () => {
     describe('with a restricted page', () => {
-      test('should return false with user who has no grant', async() => {
+      test('should return false with user who has no grant', async () => {
         const user = await User.findOne({ email: 'anonymous1@example.com' });
         const user = await User.findOne({ email: 'anonymous1@example.com' });
         const page = await Page.findOne({ path: '/grant/owner' });
         const page = await Page.findOne({ path: '/grant/owner' });
 
 
         const bool = await Page.isAccessiblePageByViewer(page.id, user);
         const bool = await Page.isAccessiblePageByViewer(page.id, user);
         expect(bool).toEqual(false);
         expect(bool).toEqual(false);
       });
       });
-      test('should return false without user', async() => {
+      test('should return false without user', async () => {
         const user = null;
         const user = null;
         const page = await Page.findOne({ path: '/grant/owner' });
         const page = await Page.findOne({ path: '/grant/owner' });
 
 
@@ -211,28 +224,28 @@ describe('Page', () => {
 
 
   describe('.findPage', () => {
   describe('.findPage', () => {
     describe('findByIdAndViewer', () => {
     describe('findByIdAndViewer', () => {
-      test('should find page (public)', async() => {
+      test('should find page (public)', async () => {
         const expectedPage = await Page.findOne({ path: '/grant/public' });
         const expectedPage = await Page.findOne({ path: '/grant/public' });
         const page = await Page.findByIdAndViewer(expectedPage.id, testUser0);
         const page = await Page.findByIdAndViewer(expectedPage.id, testUser0);
         expect(page).not.toBeNull();
         expect(page).not.toBeNull();
         expect(page.path).toEqual(expectedPage.path);
         expect(page.path).toEqual(expectedPage.path);
       });
       });
 
 
-      test('should find page (anyone knows link)', async() => {
+      test('should find page (anyone knows link)', async () => {
         const expectedPage = await Page.findOne({ path: '/grant/restricted' });
         const expectedPage = await Page.findOne({ path: '/grant/restricted' });
         const page = await Page.findByIdAndViewer(expectedPage.id, testUser1);
         const page = await Page.findByIdAndViewer(expectedPage.id, testUser1);
         expect(page).not.toBeNull();
         expect(page).not.toBeNull();
         expect(page.path).toEqual(expectedPage.path);
         expect(page.path).toEqual(expectedPage.path);
       });
       });
 
 
-      test('should find page (only me)', async() => {
+      test('should find page (only me)', async () => {
         const expectedPage = await Page.findOne({ path: '/grant/owner' });
         const expectedPage = await Page.findOne({ path: '/grant/owner' });
         const page = await Page.findByIdAndViewer(expectedPage.id, testUser0);
         const page = await Page.findByIdAndViewer(expectedPage.id, testUser0);
         expect(page).not.toBeNull();
         expect(page).not.toBeNull();
         expect(page.path).toEqual(expectedPage.path);
         expect(page.path).toEqual(expectedPage.path);
       });
       });
 
 
-      test('should not be found by grant (only me)', async() => {
+      test('should not be found by grant (only me)', async () => {
         const expectedPage = await Page.findOne({ path: '/grant/owner' });
         const expectedPage = await Page.findOne({ path: '/grant/owner' });
         const page = await Page.findByIdAndViewer(expectedPage.id, testUser1);
         const page = await Page.findByIdAndViewer(expectedPage.id, testUser1);
         expect(page).toBeNull();
         expect(page).toBeNull();
@@ -240,14 +253,14 @@ describe('Page', () => {
     });
     });
 
 
     describe('findByIdAndViewer granted userGroup', () => {
     describe('findByIdAndViewer granted userGroup', () => {
-      test('should find page', async() => {
+      test('should find page', async () => {
         const expectedPage = await Page.findOne({ path: '/grant/groupacl' });
         const expectedPage = await Page.findOne({ path: '/grant/groupacl' });
         const page = await Page.findByIdAndViewer(expectedPage.id, testUser0);
         const page = await Page.findByIdAndViewer(expectedPage.id, testUser0);
         expect(page).not.toBeNull();
         expect(page).not.toBeNull();
         expect(page.path).toEqual(expectedPage.path);
         expect(page.path).toEqual(expectedPage.path);
       });
       });
 
 
-      test('should not be found by grant', async() => {
+      test('should not be found by grant', async () => {
         const expectedPage = await Page.findOne({ path: '/grant/groupacl' });
         const expectedPage = await Page.findOne({ path: '/grant/groupacl' });
         const page = await Page.findByIdAndViewer(expectedPage.id, testUser2);
         const page = await Page.findByIdAndViewer(expectedPage.id, testUser2);
         expect(page).toBeNull();
         expect(page).toBeNull();
@@ -256,7 +269,7 @@ describe('Page', () => {
   });
   });
 
 
   describe('PageQueryBuilder.addConditionToListWithDescendants', () => {
   describe('PageQueryBuilder.addConditionToListWithDescendants', () => {
-    test('can retrieve descendants of /page', async() => {
+    test('can retrieve descendants of /page', async () => {
       const builder = new PageQueryBuilder(Page.find());
       const builder = new PageQueryBuilder(Page.find());
       builder.addConditionToListWithDescendants('/page');
       builder.addConditionToListWithDescendants('/page');
 
 
@@ -265,11 +278,13 @@ describe('Page', () => {
       // assert totalCount
       // assert totalCount
       expect(result.length).toEqual(1);
       expect(result.length).toEqual(1);
       // assert paths
       // assert paths
-      const pagePaths = result.map((page) => { return page.path });
+      const pagePaths = result.map((page) => {
+        return page.path;
+      });
       expect(pagePaths).toContainEqual('/page/child/without/parents');
       expect(pagePaths).toContainEqual('/page/child/without/parents');
     });
     });
 
 
-    test('can retrieve descendants of /page1', async() => {
+    test('can retrieve descendants of /page1', async () => {
       const builder = new PageQueryBuilder(Page.find());
       const builder = new PageQueryBuilder(Page.find());
       builder.addConditionToListWithDescendants('/page1/');
       builder.addConditionToListWithDescendants('/page1/');
 
 
@@ -278,14 +293,16 @@ describe('Page', () => {
       // assert totalCount
       // assert totalCount
       expect(result.length).toEqual(2);
       expect(result.length).toEqual(2);
       // assert paths
       // assert paths
-      const pagePaths = result.map((page) => { return page.path });
+      const pagePaths = result.map((page) => {
+        return page.path;
+      });
       expect(pagePaths).toContainEqual('/page1');
       expect(pagePaths).toContainEqual('/page1');
       expect(pagePaths).toContainEqual('/page1/child1');
       expect(pagePaths).toContainEqual('/page1/child1');
     });
     });
   });
   });
 
 
   describe('PageQueryBuilder.addConditionToListOnlyDescendants', () => {
   describe('PageQueryBuilder.addConditionToListOnlyDescendants', () => {
-    test('can retrieve only descendants of /page', async() => {
+    test('can retrieve only descendants of /page', async () => {
       const builder = new PageQueryBuilder(Page.find());
       const builder = new PageQueryBuilder(Page.find());
       builder.addConditionToListOnlyDescendants('/page');
       builder.addConditionToListOnlyDescendants('/page');
 
 
@@ -294,11 +311,13 @@ describe('Page', () => {
       // assert totalCount
       // assert totalCount
       expect(result.length).toEqual(1);
       expect(result.length).toEqual(1);
       // assert paths
       // assert paths
-      const pagePaths = result.map((page) => { return page.path });
+      const pagePaths = result.map((page) => {
+        return page.path;
+      });
       expect(pagePaths).toContainEqual('/page/child/without/parents');
       expect(pagePaths).toContainEqual('/page/child/without/parents');
     });
     });
 
 
-    test('can retrieve only descendants of /page1', async() => {
+    test('can retrieve only descendants of /page1', async () => {
       const builder = new PageQueryBuilder(Page.find());
       const builder = new PageQueryBuilder(Page.find());
       builder.addConditionToListOnlyDescendants('/page1');
       builder.addConditionToListOnlyDescendants('/page1');
 
 
@@ -307,13 +326,15 @@ describe('Page', () => {
       // assert totalCount
       // assert totalCount
       expect(result.length).toEqual(1);
       expect(result.length).toEqual(1);
       // assert paths
       // assert paths
-      const pagePaths = result.map((page) => { return page.path });
+      const pagePaths = result.map((page) => {
+        return page.path;
+      });
       expect(pagePaths).toContainEqual('/page1/child1');
       expect(pagePaths).toContainEqual('/page1/child1');
     });
     });
   });
   });
 
 
   describe('PageQueryBuilder.addConditionToListByStartWith', () => {
   describe('PageQueryBuilder.addConditionToListByStartWith', () => {
-    test('can retrieve pages which starts with /page', async() => {
+    test('can retrieve pages which starts with /page', async () => {
       const builder = new PageQueryBuilder(Page.find());
       const builder = new PageQueryBuilder(Page.find());
       builder.addConditionToListByStartWith('/page');
       builder.addConditionToListByStartWith('/page');
 
 
@@ -322,7 +343,9 @@ describe('Page', () => {
       // assert totalCount
       // assert totalCount
       expect(result.length).toEqual(4);
       expect(result.length).toEqual(4);
       // assert paths
       // assert paths
-      const pagePaths = result.map((page) => { return page.path });
+      const pagePaths = result.map((page) => {
+        return page.path;
+      });
       expect(pagePaths).toContainEqual('/page/child/without/parents');
       expect(pagePaths).toContainEqual('/page/child/without/parents');
       expect(pagePaths).toContainEqual('/page1');
       expect(pagePaths).toContainEqual('/page1');
       expect(pagePaths).toContainEqual('/page1/child1');
       expect(pagePaths).toContainEqual('/page1/child1');
@@ -331,7 +354,7 @@ describe('Page', () => {
   });
   });
 
 
   describe('.findListWithDescendants', () => {
   describe('.findListWithDescendants', () => {
-    test('can retrieve all pages with testUser0', async() => {
+    test('can retrieve all pages with testUser0', async () => {
       const result = await Page.findListWithDescendants('/grant', testUser0);
       const result = await Page.findListWithDescendants('/grant', testUser0);
       const { pages } = result;
       const { pages } = result;
 
 
@@ -339,7 +362,9 @@ describe('Page', () => {
       expect(pages.length).toEqual(5);
       expect(pages.length).toEqual(5);
 
 
       // assert paths
       // assert paths
-      const pagePaths = await pages.map((page) => { return page.path });
+      const pagePaths = await pages.map((page) => {
+        return page.path;
+      });
       expect(pagePaths).toContainEqual('/grant/groupacl');
       expect(pagePaths).toContainEqual('/grant/groupacl');
       expect(pagePaths).toContainEqual('/grant/specified');
       expect(pagePaths).toContainEqual('/grant/specified');
       expect(pagePaths).toContainEqual('/grant/owner');
       expect(pagePaths).toContainEqual('/grant/owner');
@@ -347,7 +372,7 @@ describe('Page', () => {
       expect(pagePaths).toContainEqual('/grant');
       expect(pagePaths).toContainEqual('/grant');
     });
     });
 
 
-    test('can retrieve all pages with testUser1', async() => {
+    test('can retrieve all pages with testUser1', async () => {
       const result = await Page.findListWithDescendants('/grant', testUser1);
       const result = await Page.findListWithDescendants('/grant', testUser1);
       const { pages } = result;
       const { pages } = result;
 
 
@@ -355,7 +380,9 @@ describe('Page', () => {
       expect(pages.length).toEqual(5);
       expect(pages.length).toEqual(5);
 
 
       // assert paths
       // assert paths
-      const pagePaths = await pages.map((page) => { return page.path });
+      const pagePaths = await pages.map((page) => {
+        return page.path;
+      });
       expect(pagePaths).toContainEqual('/grant/groupacl');
       expect(pagePaths).toContainEqual('/grant/groupacl');
       expect(pagePaths).toContainEqual('/grant/specified');
       expect(pagePaths).toContainEqual('/grant/specified');
       expect(pagePaths).toContainEqual('/grant/owner');
       expect(pagePaths).toContainEqual('/grant/owner');
@@ -363,7 +390,7 @@ describe('Page', () => {
       expect(pagePaths).toContainEqual('/grant');
       expect(pagePaths).toContainEqual('/grant');
     });
     });
 
 
-    test('can retrieve all pages with testUser2', async() => {
+    test('can retrieve all pages with testUser2', async () => {
       const result = await Page.findListWithDescendants('/grant', testUser2);
       const result = await Page.findListWithDescendants('/grant', testUser2);
       const { pages } = result;
       const { pages } = result;
 
 
@@ -371,7 +398,9 @@ describe('Page', () => {
       expect(pages.length).toEqual(5);
       expect(pages.length).toEqual(5);
 
 
       // assert paths
       // assert paths
-      const pagePaths = await pages.map((page) => { return page.path });
+      const pagePaths = await pages.map((page) => {
+        return page.path;
+      });
       expect(pagePaths).toContainEqual('/grant/groupacl');
       expect(pagePaths).toContainEqual('/grant/groupacl');
       expect(pagePaths).toContainEqual('/grant/specified');
       expect(pagePaths).toContainEqual('/grant/specified');
       expect(pagePaths).toContainEqual('/grant/owner');
       expect(pagePaths).toContainEqual('/grant/owner');
@@ -379,7 +408,7 @@ describe('Page', () => {
       expect(pagePaths).toContainEqual('/grant');
       expect(pagePaths).toContainEqual('/grant');
     });
     });
 
 
-    test('can retrieve all pages without user', async() => {
+    test('can retrieve all pages without user', async () => {
       const result = await Page.findListWithDescendants('/grant', null);
       const result = await Page.findListWithDescendants('/grant', null);
       const { pages } = result;
       const { pages } = result;
 
 
@@ -387,7 +416,9 @@ describe('Page', () => {
       expect(pages.length).toEqual(5);
       expect(pages.length).toEqual(5);
 
 
       // assert paths
       // assert paths
-      const pagePaths = await pages.map((page) => { return page.path });
+      const pagePaths = await pages.map((page) => {
+        return page.path;
+      });
       expect(pagePaths).toContainEqual('/grant/groupacl');
       expect(pagePaths).toContainEqual('/grant/groupacl');
       expect(pagePaths).toContainEqual('/grant/specified');
       expect(pagePaths).toContainEqual('/grant/specified');
       expect(pagePaths).toContainEqual('/grant/owner');
       expect(pagePaths).toContainEqual('/grant/owner');
@@ -397,14 +428,19 @@ describe('Page', () => {
   });
   });
 
 
   describe('.findManageableListWithDescendants', () => {
   describe('.findManageableListWithDescendants', () => {
-    test('can retrieve all pages with testUser0', async() => {
-      const pages = await Page.findManageableListWithDescendants(parentPage, testUser0);
+    test('can retrieve all pages with testUser0', async () => {
+      const pages = await Page.findManageableListWithDescendants(
+        parentPage,
+        testUser0,
+      );
 
 
       // assert totalCount
       // assert totalCount
       expect(pages.length).toEqual(5);
       expect(pages.length).toEqual(5);
 
 
       // assert paths
       // assert paths
-      const pagePaths = await pages.map((page) => { return page.path });
+      const pagePaths = await pages.map((page) => {
+        return page.path;
+      });
       expect(pagePaths).toContainEqual('/grant/groupacl');
       expect(pagePaths).toContainEqual('/grant/groupacl');
       expect(pagePaths).toContainEqual('/grant/specified');
       expect(pagePaths).toContainEqual('/grant/specified');
       expect(pagePaths).toContainEqual('/grant/owner');
       expect(pagePaths).toContainEqual('/grant/owner');
@@ -412,37 +448,49 @@ describe('Page', () => {
       expect(pagePaths).toContainEqual('/grant');
       expect(pagePaths).toContainEqual('/grant');
     });
     });
 
 
-    test('can retrieve group page and public page which starts with testUser1', async() => {
-      const pages = await Page.findManageableListWithDescendants(parentPage, testUser1);
+    test('can retrieve group page and public page which starts with testUser1', async () => {
+      const pages = await Page.findManageableListWithDescendants(
+        parentPage,
+        testUser1,
+      );
 
 
       // assert totalCount
       // assert totalCount
       expect(pages.length).toEqual(3);
       expect(pages.length).toEqual(3);
 
 
       // assert paths
       // assert paths
-      const pagePaths = await pages.map((page) => { return page.path });
+      const pagePaths = await pages.map((page) => {
+        return page.path;
+      });
       expect(pagePaths).toContainEqual('/grant/groupacl');
       expect(pagePaths).toContainEqual('/grant/groupacl');
       expect(pagePaths).toContainEqual('/grant/public');
       expect(pagePaths).toContainEqual('/grant/public');
       expect(pagePaths).toContainEqual('/grant');
       expect(pagePaths).toContainEqual('/grant');
     });
     });
 
 
-    test('can retrieve only public page which starts with testUser2', async() => {
-      const pages = await Page.findManageableListWithDescendants(parentPage, testUser2);
+    test('can retrieve only public page which starts with testUser2', async () => {
+      const pages = await Page.findManageableListWithDescendants(
+        parentPage,
+        testUser2,
+      );
 
 
       // assert totalCount
       // assert totalCount
       expect(pages.length).toEqual(2);
       expect(pages.length).toEqual(2);
 
 
       // assert paths
       // assert paths
-      const pagePaths = await pages.map((page) => { return page.path });
+      const pagePaths = await pages.map((page) => {
+        return page.path;
+      });
       expect(pagePaths).toContainEqual('/grant/public');
       expect(pagePaths).toContainEqual('/grant/public');
       expect(pagePaths).toContainEqual('/grant');
       expect(pagePaths).toContainEqual('/grant');
     });
     });
 
 
-    test('can retrieve only public page which starts without user', async() => {
-      const pages = await Page.findManageableListWithDescendants(parentPage, null);
+    test('can retrieve only public page which starts without user', async () => {
+      const pages = await Page.findManageableListWithDescendants(
+        parentPage,
+        null,
+      );
 
 
       // assert totalCount
       // assert totalCount
       expect(pages).toBeNull();
       expect(pages).toBeNull();
     });
     });
   });
   });
-
 });
 });

+ 38 - 12
apps/app/test/integration/models/update-post.test.js

@@ -1,16 +1,32 @@
 import UpdatePost from '../../../src/server/models/update-post';
 import UpdatePost from '../../../src/server/models/update-post';
 
 
 describe('UpdatePost', () => {
 describe('UpdatePost', () => {
-
   describe('.createPrefixesByPathPattern', () => {
   describe('.createPrefixesByPathPattern', () => {
     describe('with a path', () => {
     describe('with a path', () => {
       test('should return right patternPrfixes', () => {
       test('should return right patternPrfixes', () => {
-        expect(UpdatePost.createPrefixesByPathPattern('/*')).toEqual(['*', '*']);
-        expect(UpdatePost.createPrefixesByPathPattern('/user/*/日報*')).toEqual(['user', '*']);
-        expect(UpdatePost.createPrefixesByPathPattern('/project/hoge/*')).toEqual(['project', 'hoge']);
-        expect(UpdatePost.createPrefixesByPathPattern('/*/MTG/*')).toEqual(['*', 'MTG']);
-        expect(UpdatePost.createPrefixesByPathPattern('自己紹介')).toEqual(['*', '*']);
-        expect(UpdatePost.createPrefixesByPathPattern('/user/aoi/メモ/2016/02/10/xxx')).toEqual(['user', 'aoi']);
+        expect(UpdatePost.createPrefixesByPathPattern('/*')).toEqual([
+          '*',
+          '*',
+        ]);
+        expect(UpdatePost.createPrefixesByPathPattern('/user/*/日報*')).toEqual(
+          ['user', '*'],
+        );
+        expect(
+          UpdatePost.createPrefixesByPathPattern('/project/hoge/*'),
+        ).toEqual(['project', 'hoge']);
+        expect(UpdatePost.createPrefixesByPathPattern('/*/MTG/*')).toEqual([
+          '*',
+          'MTG',
+        ]);
+        expect(UpdatePost.createPrefixesByPathPattern('自己紹介')).toEqual([
+          '*',
+          '*',
+        ]);
+        expect(
+          UpdatePost.createPrefixesByPathPattern(
+            '/user/aoi/メモ/2016/02/10/xxx',
+          ),
+        ).toEqual(['user', 'aoi']);
       });
       });
     });
     });
   });
   });
@@ -19,11 +35,21 @@ describe('UpdatePost', () => {
     describe('with a pattern', () => {
     describe('with a pattern', () => {
       test('should return right regexp', () => {
       test('should return right regexp', () => {
         expect(UpdatePost.getRegExpByPattern('/*')).toEqual(/^\/.*/);
         expect(UpdatePost.getRegExpByPattern('/*')).toEqual(/^\/.*/);
-        expect(UpdatePost.getRegExpByPattern('/user/*/日報*')).toEqual(/^\/user\/.*\/日報.*/);
-        expect(UpdatePost.getRegExpByPattern('/project/hoge/*')).toEqual(/^\/project\/hoge\/.*/);
-        expect(UpdatePost.getRegExpByPattern('/*/MTG/*')).toEqual(/^\/.*\/MTG\/.*/);
-        expect(UpdatePost.getRegExpByPattern('自己紹介')).toEqual(/^\/.*自己紹介.*/);
-        expect(UpdatePost.getRegExpByPattern('/user/aoi/メモ/2016/02/10/xxx')).toEqual(/^\/user\/aoi\/メモ\/2016\/02\/10\/xxx/);
+        expect(UpdatePost.getRegExpByPattern('/user/*/日報*')).toEqual(
+          /^\/user\/.*\/日報.*/,
+        );
+        expect(UpdatePost.getRegExpByPattern('/project/hoge/*')).toEqual(
+          /^\/project\/hoge\/.*/,
+        );
+        expect(UpdatePost.getRegExpByPattern('/*/MTG/*')).toEqual(
+          /^\/.*\/MTG\/.*/,
+        );
+        expect(UpdatePost.getRegExpByPattern('自己紹介')).toEqual(
+          /^\/.*自己紹介.*/,
+        );
+        expect(
+          UpdatePost.getRegExpByPattern('/user/aoi/メモ/2016/02/10/xxx'),
+        ).toEqual(/^\/user\/aoi\/メモ\/2016\/02\/10\/xxx/);
       });
       });
     });
     });
   });
   });

+ 47 - 25
apps/app/test/integration/models/user.test.js

@@ -2,7 +2,6 @@ const mongoose = require('mongoose');
 
 
 const { getInstance } = require('../setup-crowi');
 const { getInstance } = require('../setup-crowi');
 
 
-
 describe('User', () => {
 describe('User', () => {
   // eslint-disable-next-line no-unused-vars
   // eslint-disable-next-line no-unused-vars
   let crowi;
   let crowi;
@@ -10,7 +9,7 @@ describe('User', () => {
 
 
   let adminusertestToBeRemovedId;
   let adminusertestToBeRemovedId;
 
 
-  beforeAll(async() => {
+  beforeAll(async () => {
     crowi = await getInstance();
     crowi = await getInstance();
     User = mongoose.model('User');
     User = mongoose.model('User');
 
 
@@ -52,7 +51,9 @@ describe('User', () => {
     ]);
     ]);
 
 
     // delete adminusertestToBeRemoved
     // delete adminusertestToBeRemoved
-    const adminusertestToBeRemoved = await User.findOne({ username: 'adminusertestToBeRemoved' });
+    const adminusertestToBeRemoved = await User.findOne({
+      username: 'adminusertestToBeRemoved',
+    });
     adminusertestToBeRemovedId = adminusertestToBeRemoved._id;
     adminusertestToBeRemovedId = adminusertestToBeRemoved._id;
     await adminusertestToBeRemoved.statusDelete();
     await adminusertestToBeRemoved.statusDelete();
   });
   });
@@ -60,28 +61,35 @@ describe('User', () => {
   describe('Create and Find.', () => {
   describe('Create and Find.', () => {
     describe('The user', () => {
     describe('The user', () => {
       test('should created with createUserByEmailAndPassword', (done) => {
       test('should created with createUserByEmailAndPassword', (done) => {
-        User.createUserByEmailAndPassword('Example2 for User Test', 'usertest2', 'usertest2@example.com', 'usertest2pass', 'en_US', (err, userData) => {
-          expect(err).toBeNull();
-          expect(userData).toBeInstanceOf(User);
-          expect(userData.name).toBe('Example2 for User Test');
-          done();
-        });
+        User.createUserByEmailAndPassword(
+          'Example2 for User Test',
+          'usertest2',
+          'usertest2@example.com',
+          'usertest2pass',
+          'en_US',
+          (err, userData) => {
+            expect(err).toBeNull();
+            expect(userData).toBeInstanceOf(User);
+            expect(userData.name).toBe('Example2 for User Test');
+            done();
+          },
+        );
       });
       });
 
 
-      test('should be found by findUserByUsername', async() => {
+      test('should be found by findUserByUsername', async () => {
         const user = await User.findUserByUsername('usertest');
         const user = await User.findUserByUsername('usertest');
         expect(user).toBeInstanceOf(User);
         expect(user).toBeInstanceOf(User);
         expect(user.name).toBe('Example for User Test');
         expect(user.name).toBe('Example for User Test');
       });
       });
     });
     });
-
   });
   });
 
 
   describe('Delete.', () => {
   describe('Delete.', () => {
-
     describe('Deleted users', () => {
     describe('Deleted users', () => {
-      test('should have correct attributes', async() => {
-        const adminusertestToBeRemoved = await User.findOne({ _id: adminusertestToBeRemovedId });
+      test('should have correct attributes', async () => {
+        const adminusertestToBeRemoved = await User.findOne({
+          _id: adminusertestToBeRemovedId,
+        });
 
 
         expect(adminusertestToBeRemoved).toBeInstanceOf(User);
         expect(adminusertestToBeRemoved).toBeInstanceOf(User);
         expect(adminusertestToBeRemoved.name).toBe('');
         expect(adminusertestToBeRemoved.name).toBe('');
@@ -94,22 +102,36 @@ describe('User', () => {
   });
   });
 
 
   describe('User.findAdmins', () => {
   describe('User.findAdmins', () => {
-    test('should retrieves only active users', async() => {
+    test('should retrieves only active users', async () => {
       const users = await User.findAdmins();
       const users = await User.findAdmins();
-      const adminusertestActive = users.find(user => user.username === 'adminusertest1');
-      const adminusertestSuspended = users.find(user => user.username === 'adminusertest2');
-      const adminusertestToBeRemoved = users.find(user => user._id.toString() === adminusertestToBeRemovedId.toString());
+      const adminusertestActive = users.find(
+        (user) => user.username === 'adminusertest1',
+      );
+      const adminusertestSuspended = users.find(
+        (user) => user.username === 'adminusertest2',
+      );
+      const adminusertestToBeRemoved = users.find(
+        (user) => user._id.toString() === adminusertestToBeRemovedId.toString(),
+      );
 
 
       expect(adminusertestActive).toBeInstanceOf(User);
       expect(adminusertestActive).toBeInstanceOf(User);
       expect(adminusertestSuspended).toBeUndefined();
       expect(adminusertestSuspended).toBeUndefined();
       expect(adminusertestToBeRemoved).toBeUndefined();
       expect(adminusertestToBeRemoved).toBeUndefined();
     });
     });
 
 
-    test('with \'includesInactive\' option should retrieves suspended users', async() => {
-      const users = await User.findAdmins({ status: [User.STATUS_ACTIVE, User.STATUS_SUSPENDED] });
-      const adminusertestActive = users.find(user => user.username === 'adminusertest1');
-      const adminusertestSuspended = users.find(user => user.username === 'adminusertest2');
-      const adminusertestToBeRemoved = users.find(user => user._id.toString() === adminusertestToBeRemovedId.toString());
+    test("with 'includesInactive' option should retrieves suspended users", async () => {
+      const users = await User.findAdmins({
+        status: [User.STATUS_ACTIVE, User.STATUS_SUSPENDED],
+      });
+      const adminusertestActive = users.find(
+        (user) => user.username === 'adminusertest1',
+      );
+      const adminusertestSuspended = users.find(
+        (user) => user.username === 'adminusertest2',
+      );
+      const adminusertestToBeRemoved = users.find(
+        (user) => user._id.toString() === adminusertestToBeRemovedId.toString(),
+      );
 
 
       expect(adminusertestActive).toBeInstanceOf(User);
       expect(adminusertestActive).toBeInstanceOf(User);
       expect(adminusertestSuspended).toBeInstanceOf(User);
       expect(adminusertestSuspended).toBeInstanceOf(User);
@@ -119,14 +141,14 @@ describe('User', () => {
 
 
   describe('User Utilities', () => {
   describe('User Utilities', () => {
     describe('Get user exists from user page path', () => {
     describe('Get user exists from user page path', () => {
-      test('found', async() => {
+      test('found', async () => {
         const userPagePath = '/user/usertest';
         const userPagePath = '/user/usertest';
         const isExist = await User.isExistUserByUserPagePath(userPagePath);
         const isExist = await User.isExistUserByUserPagePath(userPagePath);
 
 
         expect(isExist).toBe(true);
         expect(isExist).toBe(true);
       });
       });
 
 
-      test('not found', async() => {
+      test('not found', async () => {
         const userPagePath = '/user/usertest-hoge';
         const userPagePath = '/user/usertest-hoge';
         const isExist = await User.isExistUserByUserPagePath(userPagePath);
         const isExist = await User.isExistUserByUserPagePath(userPagePath);
 
 

File diff suppressed because it is too large
+ 444 - 124
apps/app/test/integration/models/v5.page.test.js


+ 2 - 4
apps/app/test/integration/setup-crowi.ts

@@ -5,16 +5,14 @@ import { setupModelsDependentOnCrowi } from '../../src/server/crowi/setup-models
 
 
 let _instance: Crowi;
 let _instance: Crowi;
 
 
-const initCrowi = async(crowi: Crowi) => {
+const initCrowi = async (crowi: Crowi) => {
   crowi.models = await setupModelsDependentOnCrowi(crowi);
   crowi.models = await setupModelsDependentOnCrowi(crowi);
   await crowi.setupConfigManager();
   await crowi.setupConfigManager();
 
 
   await crowi.setupSocketIoService();
   await crowi.setupSocketIoService();
   await crowi.socketIoService.attachServer(new Server()); // attach dummy server
   await crowi.socketIoService.attachServer(new Server()); // attach dummy server
 
 
-  await Promise.all([
-    crowi.setUpApp(),
-  ]);
+  await Promise.all([crowi.setUpApp()]);
 
 
   await Promise.all([
   await Promise.all([
     crowi.setupPassport(),
     crowi.setupPassport(),

+ 2 - 2
apps/app/test/integration/setup.js

@@ -14,11 +14,11 @@ mongoose.Promise = global.Promise;
 
 
 jest.setTimeout(30000); // default 5000
 jest.setTimeout(30000); // default 5000
 
 
-beforeAll(async() => {
+beforeAll(async () => {
   await mongoose.connect(getMongoUri(), mongoOptions);
   await mongoose.connect(getMongoUri(), mongoOptions);
 });
 });
 
 
-afterAll(async() => {
+afterAll(async () => {
   await mongoose.disconnect();
   await mongoose.disconnect();
   gc();
   gc();
 });
 });

+ 1 - 1
apps/app/test/integration/tsconfig.json

@@ -2,5 +2,5 @@
   "extends": "../../tsconfig.json",
   "extends": "../../tsconfig.json",
   "compilerOptions": {
   "compilerOptions": {
     "module": "CommonJS"
     "module": "CommonJS"
-  },
+  }
 }
 }

+ 3 - 5
apps/app/tsconfig.build.client.json

@@ -1,9 +1,7 @@
 {
 {
   "$schema": "http://json.schemastore.org/tsconfig",
   "$schema": "http://json.schemastore.org/tsconfig",
   "extends": "./tsconfig.json",
   "extends": "./tsconfig.json",
-  "include": [
-    ".next/types/**/*.ts"
-  ],
+  "include": [".next/types/**/*.ts"],
   "compilerOptions": {
   "compilerOptions": {
     "strict": false,
     "strict": false,
     "strictNullChecks": true,
     "strictNullChecks": true,
@@ -14,8 +12,8 @@
     "baseUrl": ".",
     "baseUrl": ".",
     "paths": {
     "paths": {
       "~/*": ["./src/*"],
       "~/*": ["./src/*"],
-      "^/*": ["./*"],
+      "^/*": ["./*"]
     },
     },
-    "plugins": [{"name": "next"}]
+    "plugins": [{ "name": "next" }]
   }
   }
 }
 }

+ 1 - 1
apps/app/tsconfig.build.server.json

@@ -13,7 +13,7 @@
     "baseUrl": ".",
     "baseUrl": ".",
     "paths": {
     "paths": {
       "~/*": ["./src/*"],
       "~/*": ["./src/*"],
-      "^/*": ["./*"],
+      "^/*": ["./*"]
     }
     }
   },
   },
   "exclude": [
   "exclude": [

+ 3 - 9
apps/app/tsconfig.json

@@ -4,14 +4,12 @@
   "compilerOptions": {
   "compilerOptions": {
     "jsx": "preserve",
     "jsx": "preserve",
     "resolveJsonModule": true,
     "resolveJsonModule": true,
-    "types": [
-      "vitest/globals"
-    ],
+    "types": ["vitest/globals"],
 
 
     "baseUrl": ".",
     "baseUrl": ".",
     "paths": {
     "paths": {
       "~/*": ["./src/*"],
       "~/*": ["./src/*"],
-      "^/*": ["./*"],
+      "^/*": ["./*"]
     },
     },
 
 
     /* TODO: remove below flags for strict checking */
     /* TODO: remove below flags for strict checking */
@@ -29,11 +27,7 @@
       { "transform": "typescript-transform-paths", "afterDeclarations": true }
       { "transform": "typescript-transform-paths", "afterDeclarations": true }
     ]
     ]
   },
   },
-  "include": [
-    "next-env.d.ts",
-    "config",
-    "src"
-  ],
+  "include": ["next-env.d.ts", "config", "src"],
   "ts-node": {
   "ts-node": {
     "transpileOnly": true,
     "transpileOnly": true,
     "swc": true,
     "swc": true,

+ 1 - 3
apps/app/vite.styles-prebuilt.config.ts

@@ -6,9 +6,7 @@ export default defineConfig({
   build: {
   build: {
     outDir: 'src/styles/prebuilt',
     outDir: 'src/styles/prebuilt',
     rollupOptions: {
     rollupOptions: {
-      input: [
-        '/src/styles/vendor.scss',
-      ],
+      input: ['/src/styles/vendor.scss'],
       output: {
       output: {
         assetFileNames: '[name].[ext]', // not attach hash
         assetFileNames: '[name].[ext]', // not attach hash
       },
       },

+ 23 - 45
apps/app/vitest.workspace.mts

@@ -1,65 +1,43 @@
 import react from '@vitejs/plugin-react';
 import react from '@vitejs/plugin-react';
 import tsconfigPaths from 'vite-tsconfig-paths';
 import tsconfigPaths from 'vite-tsconfig-paths';
-import {
-  defineConfig, defineWorkspace, mergeConfig,
-} from 'vitest/config';
+import { defineConfig, defineWorkspace, mergeConfig } from 'vitest/config';
 
 
 const configShared = defineConfig({
 const configShared = defineConfig({
-  plugins: [
-    tsconfigPaths(),
-  ],
+  plugins: [tsconfigPaths()],
   test: {
   test: {
     clearMocks: true,
     clearMocks: true,
     globals: true,
     globals: true,
-    exclude: [
-      'test/**',
-      'test-with-vite/**',
-      'playwright/**',
-    ],
+    exclude: ['test/**', 'test-with-vite/**', 'playwright/**'],
   },
   },
 });
 });
 
 
 export default defineWorkspace([
 export default defineWorkspace([
-
   // unit test
   // unit test
-  mergeConfig(
-    configShared,
-    {
-      test: {
-        name: 'app-unit',
-        environment: 'node',
-        include: ['**/*.spec.{ts,js}'],
-      },
+  mergeConfig(configShared, {
+    test: {
+      name: 'app-unit',
+      environment: 'node',
+      include: ['**/*.spec.{ts,js}'],
     },
     },
-  ),
+  }),
 
 
   // integration test
   // integration test
-  mergeConfig(
-    configShared,
-    {
-      test: {
-        name: 'app-integration',
-        environment: 'node',
-        include: ['**/*.integ.ts'],
-        setupFiles: [
-          './test-with-vite/setup/mongoms.ts',
-        ],
-      },
+  mergeConfig(configShared, {
+    test: {
+      name: 'app-integration',
+      environment: 'node',
+      include: ['**/*.integ.ts'],
+      setupFiles: ['./test-with-vite/setup/mongoms.ts'],
     },
     },
-  ),
+  }),
 
 
   // component test
   // component test
-  mergeConfig(
-    configShared,
-    {
-      plugins: [react()],
-      test: {
-        name: 'app-components',
-        environment: 'happy-dom',
-        include: [
-          '**/*.spec.{tsx,jsx}',
-        ],
-      },
+  mergeConfig(configShared, {
+    plugins: [react()],
+    test: {
+      name: 'app-components',
+      environment: 'happy-dom',
+      include: ['**/*.spec.{tsx,jsx}'],
     },
     },
-  ),
+  }),
 ]);
 ]);

+ 12 - 2
biome.json

@@ -13,16 +13,26 @@
       "!**/.turbo/**",
       "!**/.turbo/**",
       "!**/.vscode/**",
       "!**/.vscode/**",
       "!**/turbo.json",
       "!**/turbo.json",
+      "!**/.next/**",
       "!bin/**",
       "!bin/**",
       "!tsconfig.base.json",
       "!tsconfig.base.json",
       "!**/.devcontainer/**",
       "!**/.devcontainer/**",
       "!**/.eslintrc.js",
       "!**/.eslintrc.js",
       "!**/.stylelintrc.json",
       "!**/.stylelintrc.json",
       "!**/package.json",
       "!**/package.json",
-      "!apps/app/**",
       "!apps/slackbot-proxy/src/public/bootstrap/**",
       "!apps/slackbot-proxy/src/public/bootstrap/**",
       "!packages/editor/**",
       "!packages/editor/**",
-      "!packages/pdf-converter-client/src/index.ts"
+      "!packages/pdf-converter-client/src/index.ts",
+      "!apps/app/bin/**",
+      "!apps/app/config/**",
+      "!apps/app/docker/**",
+      "!apps/app/playwright/**",
+      "!apps/app/public/**",
+      "!apps/app/resource/**",
+      "!apps/app/src/**",
+      "!apps/app/test/integration/service/**",
+      "!apps/app/test-with-vite/**",
+      "!apps/app/tmp/**"
     ]
     ]
   },
   },
   "formatter": {
   "formatter": {

+ 178 - 15
pnpm-lock.yaml

@@ -221,11 +221,14 @@ importers:
         specifier: ^8.15.4
         specifier: ^8.15.4
         version: 8.15.4
         version: 8.15.4
       '@elastic/elasticsearch7':
       '@elastic/elasticsearch7':
-        specifier: npm:@elastic/elasticsearch@^7.17.0
+        specifier: npm:@elastic/elasticsearch@^7.17.4
         version: '@elastic/elasticsearch@7.17.13'
         version: '@elastic/elasticsearch@7.17.13'
       '@elastic/elasticsearch8':
       '@elastic/elasticsearch8':
-        specifier: npm:@elastic/elasticsearch@^8.7.0
-        version: '@elastic/elasticsearch@8.14.0'
+        specifier: npm:@elastic/elasticsearch@^8.18.2
+        version: '@elastic/elasticsearch@8.18.2'
+      '@elastic/elasticsearch9':
+        specifier: npm:@elastic/elasticsearch@^9.0.3
+        version: '@elastic/elasticsearch@9.0.3'
       '@godaddy/terminus':
       '@godaddy/terminus':
         specifier: ^4.9.0
         specifier: ^4.9.0
         version: 4.12.1
         version: 4.12.1
@@ -2738,14 +2741,22 @@ packages:
     resolution: {integrity: sha512-GMXtFVqd3FgUlTtPL/GDc+3GhwvfZ0kSuegCvVVqb58kd+0I6U6u7PL8QFRLHtwzqLEBmYLdwr4PRkBAWKGlzA==}
     resolution: {integrity: sha512-GMXtFVqd3FgUlTtPL/GDc+3GhwvfZ0kSuegCvVVqb58kd+0I6U6u7PL8QFRLHtwzqLEBmYLdwr4PRkBAWKGlzA==}
     engines: {node: '>=12'}
     engines: {node: '>=12'}
 
 
-  '@elastic/elasticsearch@8.14.0':
-    resolution: {integrity: sha512-MGrgCI4y+Ozssf5Q2IkVJlqt5bUMnKIICG2qxeOfrJNrVugMCBCAQypyesmSSocAtNm8IX3LxfJ3jQlFHmKe2w==}
+  '@elastic/elasticsearch@8.18.2':
+    resolution: {integrity: sha512-2pOc/hGdxkbaDavfAlnUfjJdVsFRCGqg7fpsWJfJ2UzpgViIyojdViHg8zOCT1J14lAwvDgb9CNETWa3SBZRfw==}
     engines: {node: '>=18'}
     engines: {node: '>=18'}
 
 
-  '@elastic/transport@8.6.1':
-    resolution: {integrity: sha512-3vGs4W3wP5oeIT/4j1vcvd+t7m6ndP0uyb5GDY23LQCmbtI5Oq0aQwD9gb09KJbLFLUbI7db9vMFPzKavSFA5g==}
+  '@elastic/elasticsearch@9.0.3':
+    resolution: {integrity: sha512-aagnssrVQi538wExO0Au169amtq68sXSwQMyzblQVAsqcmbqRTtzmGhKOjnDP0LK3ml0Mtje1uX+Vda7RhqDsA==}
     engines: {node: '>=18'}
     engines: {node: '>=18'}
 
 
+  '@elastic/transport@8.9.7':
+    resolution: {integrity: sha512-zdLkkahbWM/O1MAZ0rAu0xg+JJUgRoAGSOf2TWLkdDk42BMqOfwVG+Qz1ZnbhfydkpWiSmGMhbRBhqqlqWdwog==}
+    engines: {node: '>=18'}
+
+  '@elastic/transport@9.0.2':
+    resolution: {integrity: sha512-7okzzK9wP+qIFAw49/jAFYYHpJHBsDYfFt6dI2OBU8PRHEFCBqAPErTH5GBtgrs6rx/U1798kton5Ofv/tIHdw==}
+    engines: {node: '>=20'}
+
   '@emnapi/core@1.2.0':
   '@emnapi/core@1.2.0':
     resolution: {integrity: sha512-E7Vgw78I93we4ZWdYCb4DGAwRROGkMIXk7/y87UmANR+J6qsWusmC3gLt0H+O0KOt5e6O38U8oJamgbudrES/w==}
     resolution: {integrity: sha512-E7Vgw78I93we4ZWdYCb4DGAwRROGkMIXk7/y87UmANR+J6qsWusmC3gLt0H+O0KOt5e6O38U8oJamgbudrES/w==}
 
 
@@ -5336,6 +5347,12 @@ packages:
   '@types/color-name@1.1.1':
   '@types/color-name@1.1.1':
     resolution: {integrity: sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==}
     resolution: {integrity: sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==}
 
 
+  '@types/command-line-args@5.2.3':
+    resolution: {integrity: sha512-uv0aG6R0Y8WHZLTamZwtfsDLVRnOa+n+n5rEvFWL5Na5gZ8V2Teab/duDPFzIIIhs9qizDpcavCusCLJZu62Kw==}
+
+  '@types/command-line-usage@5.0.4':
+    resolution: {integrity: sha512-BwR5KP3Es/CSht0xqBcUXS3qCAUVXwpRKsV2+arxeb65atasuXG9LykC9Ab10Cw3s2raH92ZqOeILaQbsB2ACg==}
+
   '@types/connect@3.4.38':
   '@types/connect@3.4.38':
     resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
     resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
 
 
@@ -6149,6 +6166,10 @@ packages:
     resolution: {integrity: sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==}
     resolution: {integrity: sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==}
     engines: {node: '>= 8'}
     engines: {node: '>= 8'}
 
 
+  apache-arrow@19.0.1:
+    resolution: {integrity: sha512-APmMLzS4qbTivLrPdQXexGM4JRr+0g62QDaobzEvip/FdQIrv2qLy0mD5Qdmw4buydtVJgbFeKR8f59I6PPGDg==}
+    hasBin: true
+
   app-root-path@3.1.0:
   app-root-path@3.1.0:
     resolution: {integrity: sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==}
     resolution: {integrity: sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==}
     engines: {node: '>= 6.0.0'}
     engines: {node: '>= 6.0.0'}
@@ -6196,6 +6217,10 @@ packages:
     resolution: {integrity: sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==}
     resolution: {integrity: sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==}
     engines: {node: '>= 0.4'}
     engines: {node: '>= 0.4'}
 
 
+  array-back@6.2.2:
+    resolution: {integrity: sha512-gUAZ7HPyb4SJczXAMUXMGAvI976JoK3qEx9v1FTmeYuJj0IBiaKttG1ydtGKdkfqWkIkouke7nG8ufGy77+Cvw==}
+    engines: {node: '>=12.17'}
+
   array-buffer-byte-length@1.0.1:
   array-buffer-byte-length@1.0.1:
     resolution: {integrity: sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==}
     resolution: {integrity: sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==}
     engines: {node: '>= 0.4'}
     engines: {node: '>= 0.4'}
@@ -6690,6 +6715,10 @@ packages:
   chainsaw@0.1.0:
   chainsaw@0.1.0:
     resolution: {integrity: sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==}
     resolution: {integrity: sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==}
 
 
+  chalk-template@0.4.0:
+    resolution: {integrity: sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==}
+    engines: {node: '>=12'}
+
   chalk@1.1.3:
   chalk@1.1.3:
     resolution: {integrity: sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==}
     resolution: {integrity: sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==}
     engines: {node: '>=0.10.0'}
     engines: {node: '>=0.10.0'}
@@ -6954,6 +6983,19 @@ packages:
   comma-separated-tokens@2.0.2:
   comma-separated-tokens@2.0.2:
     resolution: {integrity: sha512-G5yTt3KQN4Yn7Yk4ed73hlZ1evrFKXeUW3086p3PRFNp7m2vIjI6Pg+Kgb+oyzhd9F2qdcoj67+y3SdxL5XWsg==}
     resolution: {integrity: sha512-G5yTt3KQN4Yn7Yk4ed73hlZ1evrFKXeUW3086p3PRFNp7m2vIjI6Pg+Kgb+oyzhd9F2qdcoj67+y3SdxL5XWsg==}
 
 
+  command-line-args@6.0.1:
+    resolution: {integrity: sha512-Jr3eByUjqyK0qd8W0SGFW1nZwqCaNCtbXjRo2cRJC1OYxWl3MZ5t1US3jq+cO4sPavqgw4l9BMGX0CBe+trepg==}
+    engines: {node: '>=12.20'}
+    peerDependencies:
+      '@75lb/nature': latest
+    peerDependenciesMeta:
+      '@75lb/nature':
+        optional: true
+
+  command-line-usage@7.0.3:
+    resolution: {integrity: sha512-PqMLy5+YGwhMh1wS04mVG44oqDsgyLRSKJBdOo1bnYhMKBW65gZF1dRp2OZRhiTjgUHljy99qkO7bsctLaw35Q==}
+    engines: {node: '>=12.20.0'}
+
   commander@10.0.1:
   commander@10.0.1:
     resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==}
     resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==}
     engines: {node: '>=14'}
     engines: {node: '>=14'}
@@ -8782,6 +8824,15 @@ packages:
     resolution: {integrity: sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==}
     resolution: {integrity: sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==}
     engines: {node: '>=8'}
     engines: {node: '>=8'}
 
 
+  find-replace@5.0.2:
+    resolution: {integrity: sha512-Y45BAiE3mz2QsrN2fb5QEtO4qb44NcS7en/0y9PEVsg351HsLeVclP8QPMH79Le9sH3rs5RSwJu99W0WPZO43Q==}
+    engines: {node: '>=14'}
+    peerDependencies:
+      '@75lb/nature': latest
+    peerDependenciesMeta:
+      '@75lb/nature':
+        optional: true
+
   find-up-simple@1.0.0:
   find-up-simple@1.0.0:
     resolution: {integrity: sha512-q7Us7kcjj2VMePAa02hDAF6d+MzsdsAWEwYyOpwUtlerRBkOEPBCRZrAV4XfcSN8fHAgaD0hP7miwoay6DCprw==}
     resolution: {integrity: sha512-q7Us7kcjj2VMePAa02hDAF6d+MzsdsAWEwYyOpwUtlerRBkOEPBCRZrAV4XfcSN8fHAgaD0hP7miwoay6DCprw==}
     engines: {node: '>=18'}
     engines: {node: '>=18'}
@@ -8817,6 +8868,9 @@ packages:
     resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==}
     resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==}
     hasBin: true
     hasBin: true
 
 
+  flatbuffers@24.12.23:
+    resolution: {integrity: sha512-dLVCAISd5mhls514keQzmEG6QHmUUsNuWsb4tFafIUwvvgDjXhtfAYSKOzt5SWOy+qByV5pbsDZ+Vb7HUOBEdA==}
+
   flatted@3.3.1:
   flatted@3.3.1:
     resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==}
     resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==}
 
 
@@ -10132,6 +10186,10 @@ packages:
   json-bigint@1.0.0:
   json-bigint@1.0.0:
     resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==}
     resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==}
 
 
+  json-bignum@0.0.3:
+    resolution: {integrity: sha512-2WHyXj3OfHSgNyuzDbSxI1w2jgw5gkWSWhS7Qg4bWXx1nLk3jnbwfUeS0PSba3IzpTUWdHxBieELUzXRjQB2zg==}
+    engines: {node: '>=0.8'}
+
   json-buffer@3.0.1:
   json-buffer@3.0.1:
     resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
     resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
 
 
@@ -12951,6 +13009,12 @@ packages:
   secure-json-parse@2.7.0:
   secure-json-parse@2.7.0:
     resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==}
     resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==}
 
 
+  secure-json-parse@3.0.2:
+    resolution: {integrity: sha512-H6nS2o8bWfpFEV6U38sOSjS7bTbdgbCGU9wEM6W14P5H0QOsz94KCusifV44GpHDTu2nqZbuDNhTzu+mjDSw1w==}
+
+  secure-json-parse@4.0.0:
+    resolution: {integrity: sha512-dxtLJO6sc35jWidmLxo7ij+Eg48PM/kleBsxpC8QJE0qJICe+KawkDQmvCMZUr9u7WKVHgMW6vy3fQ7zMiFZMA==}
+
   semver@5.5.1:
   semver@5.5.1:
     resolution: {integrity: sha512-PqpAxfrEhlSUWge8dwIp4tZnQ25DIOthpiaHNIthsjEFQD6EvqUKUDM7L8O2rShkFccYo1VjJR0coWfNkCubRw==}
     resolution: {integrity: sha512-PqpAxfrEhlSUWge8dwIp4tZnQ25DIOthpiaHNIthsjEFQD6EvqUKUDM7L8O2rShkFccYo1VjJR0coWfNkCubRw==}
     hasBin: true
     hasBin: true
@@ -13648,6 +13712,10 @@ packages:
     resolution: {integrity: sha512-CSZRtSRZ8RhJGMtWyLRqlarmWPPlsgZJHtV6cz0VTHNOg+R7UBoE2eNPQmB5Qrhtk3RX2AAcJmVwMXFULVQSwg==}
     resolution: {integrity: sha512-CSZRtSRZ8RhJGMtWyLRqlarmWPPlsgZJHtV6cz0VTHNOg+R7UBoE2eNPQmB5Qrhtk3RX2AAcJmVwMXFULVQSwg==}
     engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
     engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
 
 
+  table-layout@4.1.1:
+    resolution: {integrity: sha512-iK5/YhZxq5GO5z8wb0bY1317uDF3Zjpha0QFFLA8/trAoiLbQD0HUbMesEaxyzUgDxi2QlcbM8IvqOlEjgoXBA==}
+    engines: {node: '>=12.17'}
+
   table@6.8.2:
   table@6.8.2:
     resolution: {integrity: sha512-w2sfv80nrAh2VCbqR5AK27wswXhqcck2AhfnNW76beQXskGZ1V12GwS//yYVa3d3fcvAip2OUnbDAjW2k3v9fA==}
     resolution: {integrity: sha512-w2sfv80nrAh2VCbqR5AK27wswXhqcck2AhfnNW76beQXskGZ1V12GwS//yYVa3d3fcvAip2OUnbDAjW2k3v9fA==}
     engines: {node: '>=10.0.0'}
     engines: {node: '>=10.0.0'}
@@ -14141,6 +14209,10 @@ packages:
     engines: {node: '>=14.17'}
     engines: {node: '>=14.17'}
     hasBin: true
     hasBin: true
 
 
+  typical@7.3.0:
+    resolution: {integrity: sha512-ya4mg/30vm+DOWfBg4YK3j2WD6TWtRkCbasOJr40CseYENzCUby/7rIvXA99JGsQHeNxLbnXdyLLxKSv3tauFw==}
+    engines: {node: '>=12.17'}
+
   typpy@2.3.11:
   typpy@2.3.11:
     resolution: {integrity: sha512-Jh/fykZSaxeKO0ceMAs6agki9T5TNA9kiIR6fzKbvafKpIw8UlNlHhzuqKyi5lfJJ5VojJOx9tooIbyy7vHV/g==}
     resolution: {integrity: sha512-Jh/fykZSaxeKO0ceMAs6agki9T5TNA9kiIR6fzKbvafKpIw8UlNlHhzuqKyi5lfJJ5VojJOx9tooIbyy7vHV/g==}
 
 
@@ -14194,10 +14266,14 @@ packages:
   undici-types@6.21.0:
   undici-types@6.21.0:
     resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
     resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
 
 
-  undici@6.19.2:
-    resolution: {integrity: sha512-JfjKqIauur3Q6biAtHJ564e3bWa8VvT+7cSiOJHFbX4Erv6CLGDpg8z+Fmg/1OI/47RA+GI2QZaF48SSaLvyBA==}
+  undici@6.21.3:
+    resolution: {integrity: sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==}
     engines: {node: '>=18.17'}
     engines: {node: '>=18.17'}
 
 
+  undici@7.10.0:
+    resolution: {integrity: sha512-u5otvFBOBZvmdjWLVW+5DAc9Nkq8f24g0O9oY7qw2JVIF1VocIFoyz9JFkuVOS2j41AufeO0xnlweJ2RLT8nGw==}
+    engines: {node: '>=20.18.1'}
+
   unicorn-magic@0.1.0:
   unicorn-magic@0.1.0:
     resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==}
     resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==}
     engines: {node: '>=18'}
     engines: {node: '>=18'}
@@ -14700,6 +14776,10 @@ packages:
   wordwrap@1.0.0:
   wordwrap@1.0.0:
     resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==}
     resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==}
 
 
+  wordwrapjs@5.1.0:
+    resolution: {integrity: sha512-JNjcULU2e4KJwUNv6CHgI46UvDGitb6dGryHajXTDiLgg1/RiGoPSDw4kZfYnwGtEXf2ZMeIewDQgFGzkCB2Sg==}
+    engines: {node: '>=12.17'}
+
   wrap-ansi@6.2.0:
   wrap-ansi@6.2.0:
     resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
     resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
     engines: {node: '>=8'}
     engines: {node: '>=8'}
@@ -16833,21 +16913,45 @@ snapshots:
     transitivePeerDependencies:
     transitivePeerDependencies:
       - supports-color
       - supports-color
 
 
-  '@elastic/elasticsearch@8.14.0':
+  '@elastic/elasticsearch@8.18.2':
+    dependencies:
+      '@elastic/transport': 8.9.7
+      apache-arrow: 19.0.1
+      tslib: 2.8.1
+    transitivePeerDependencies:
+      - '@75lb/nature'
+      - supports-color
+
+  '@elastic/elasticsearch@9.0.3':
+    dependencies:
+      '@elastic/transport': 9.0.2
+      apache-arrow: 19.0.1
+      tslib: 2.8.1
+    transitivePeerDependencies:
+      - '@75lb/nature'
+      - supports-color
+
+  '@elastic/transport@8.9.7':
     dependencies:
     dependencies:
-      '@elastic/transport': 8.6.1
+      '@opentelemetry/api': 1.9.0
+      debug: 4.4.1(supports-color@5.5.0)
+      hpagent: 1.2.0
+      ms: 2.1.3
+      secure-json-parse: 3.0.2
       tslib: 2.8.1
       tslib: 2.8.1
+      undici: 6.21.3
     transitivePeerDependencies:
     transitivePeerDependencies:
       - supports-color
       - supports-color
 
 
-  '@elastic/transport@8.6.1':
+  '@elastic/transport@9.0.2':
     dependencies:
     dependencies:
+      '@opentelemetry/api': 1.9.0
       debug: 4.4.1(supports-color@5.5.0)
       debug: 4.4.1(supports-color@5.5.0)
       hpagent: 1.2.0
       hpagent: 1.2.0
       ms: 2.1.3
       ms: 2.1.3
-      secure-json-parse: 2.7.0
+      secure-json-parse: 4.0.0
       tslib: 2.8.1
       tslib: 2.8.1
-      undici: 6.19.2
+      undici: 7.10.0
     transitivePeerDependencies:
     transitivePeerDependencies:
       - supports-color
       - supports-color
 
 
@@ -20430,6 +20534,10 @@ snapshots:
 
 
   '@types/color-name@1.1.1': {}
   '@types/color-name@1.1.1': {}
 
 
+  '@types/command-line-args@5.2.3': {}
+
+  '@types/command-line-usage@5.0.4': {}
+
   '@types/connect@3.4.38':
   '@types/connect@3.4.38':
     dependencies:
     dependencies:
       '@types/node': 22.15.21
       '@types/node': 22.15.21
@@ -21491,6 +21599,20 @@ snapshots:
       normalize-path: 3.0.0
       normalize-path: 3.0.0
       picomatch: 2.3.1
       picomatch: 2.3.1
 
 
+  apache-arrow@19.0.1:
+    dependencies:
+      '@swc/helpers': 0.5.15
+      '@types/command-line-args': 5.2.3
+      '@types/command-line-usage': 5.0.4
+      '@types/node': 20.14.0
+      command-line-args: 6.0.1
+      command-line-usage: 7.0.3
+      flatbuffers: 24.12.23
+      json-bignum: 0.0.3
+      tslib: 2.8.1
+    transitivePeerDependencies:
+      - '@75lb/nature'
+
   app-root-path@3.1.0: {}
   app-root-path@3.1.0: {}
 
 
   append-field@1.0.0: {}
   append-field@1.0.0: {}
@@ -21557,6 +21679,8 @@ snapshots:
 
 
   aria-query@5.3.1: {}
   aria-query@5.3.1: {}
 
 
+  array-back@6.2.2: {}
+
   array-buffer-byte-length@1.0.1:
   array-buffer-byte-length@1.0.1:
     dependencies:
     dependencies:
       call-bind: 1.0.7
       call-bind: 1.0.7
@@ -22217,6 +22341,10 @@ snapshots:
     dependencies:
     dependencies:
       traverse: 0.3.9
       traverse: 0.3.9
 
 
+  chalk-template@0.4.0:
+    dependencies:
+      chalk: 4.1.2
+
   chalk@1.1.3:
   chalk@1.1.3:
     dependencies:
     dependencies:
       ansi-styles: 2.2.1
       ansi-styles: 2.2.1
@@ -22500,6 +22628,20 @@ snapshots:
 
 
   comma-separated-tokens@2.0.2: {}
   comma-separated-tokens@2.0.2: {}
 
 
+  command-line-args@6.0.1:
+    dependencies:
+      array-back: 6.2.2
+      find-replace: 5.0.2
+      lodash.camelcase: 4.3.0
+      typical: 7.3.0
+
+  command-line-usage@7.0.3:
+    dependencies:
+      array-back: 6.2.2
+      chalk-template: 0.4.0
+      table-layout: 4.1.1
+      typical: 7.3.0
+
   commander@10.0.1:
   commander@10.0.1:
     optional: true
     optional: true
 
 
@@ -24317,6 +24459,8 @@ snapshots:
       make-dir: 3.1.0
       make-dir: 3.1.0
       pkg-dir: 4.2.0
       pkg-dir: 4.2.0
 
 
+  find-replace@5.0.2: {}
+
   find-up-simple@1.0.0: {}
   find-up-simple@1.0.0: {}
 
 
   find-up@1.1.2:
   find-up@1.1.2:
@@ -24355,6 +24499,8 @@ snapshots:
 
 
   flat@5.0.2: {}
   flat@5.0.2: {}
 
 
+  flatbuffers@24.12.23: {}
+
   flatted@3.3.1: {}
   flatted@3.3.1: {}
 
 
   fn-args@5.0.0: {}
   fn-args@5.0.0: {}
@@ -26010,6 +26156,8 @@ snapshots:
     dependencies:
     dependencies:
       bignumber.js: 9.1.2
       bignumber.js: 9.1.2
 
 
+  json-bignum@0.0.3: {}
+
   json-buffer@3.0.1: {}
   json-buffer@3.0.1: {}
 
 
   json-parse-better-errors@1.0.1: {}
   json-parse-better-errors@1.0.1: {}
@@ -29524,6 +29672,10 @@ snapshots:
 
 
   secure-json-parse@2.7.0: {}
   secure-json-parse@2.7.0: {}
 
 
+  secure-json-parse@3.0.2: {}
+
+  secure-json-parse@4.0.0: {}
+
   semver@5.5.1: {}
   semver@5.5.1: {}
 
 
   semver@5.7.1: {}
   semver@5.7.1: {}
@@ -30409,6 +30561,11 @@ snapshots:
       '@pkgr/utils': 2.3.0
       '@pkgr/utils': 2.3.0
       tslib: 2.8.1
       tslib: 2.8.1
 
 
+  table-layout@4.1.1:
+    dependencies:
+      array-back: 6.2.2
+      wordwrapjs: 5.1.0
+
   table@6.8.2:
   table@6.8.2:
     dependencies:
     dependencies:
       ajv: 8.17.1
       ajv: 8.17.1
@@ -30901,6 +31058,8 @@ snapshots:
 
 
   typescript@5.4.2: {}
   typescript@5.4.2: {}
 
 
+  typical@7.3.0: {}
+
   typpy@2.3.11:
   typpy@2.3.11:
     dependencies:
     dependencies:
       function.name: 1.0.12
       function.name: 1.0.12
@@ -30950,7 +31109,9 @@ snapshots:
 
 
   undici-types@6.21.0: {}
   undici-types@6.21.0: {}
 
 
-  undici@6.19.2: {}
+  undici@6.21.3: {}
+
+  undici@7.10.0: {}
 
 
   unicorn-magic@0.1.0: {}
   unicorn-magic@0.1.0: {}
 
 
@@ -31547,6 +31708,8 @@ snapshots:
 
 
   wordwrap@1.0.0: {}
   wordwrap@1.0.0: {}
 
 
+  wordwrapjs@5.1.0: {}
+
   wrap-ansi@6.2.0:
   wrap-ansi@6.2.0:
     dependencies:
     dependencies:
       ansi-styles: 4.2.1
       ansi-styles: 4.2.1

Some files were not shown because too many files changed in this diff