feat: Introduce Meilisearch (#10755)
* wip * wip * Update SearchService.ts * Update SearchService.ts * wip * wip * Update SearchService.ts * Update CHANGELOG.md * wip * Update SearchService.ts * Update docker-compose.yml.example
This commit is contained in:
		
							parent
							
								
									5f62cefe31
								
							
						
					
					
						commit
						5c08f2b93b
					
				
					 18 changed files with 257 additions and 91 deletions
				
			
		| 
						 | 
					@ -95,15 +95,13 @@ redis:
 | 
				
			||||||
#  #prefix: example-prefix
 | 
					#  #prefix: example-prefix
 | 
				
			||||||
#  #db: 1
 | 
					#  #db: 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#   ┌─────────────────────────────┐
 | 
					#   ┌───────────────────────────┐
 | 
				
			||||||
#───┘ Elasticsearch configuration └─────────────────────────────
 | 
					#───┘ MeiliSearch configuration └─────────────────────────────
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#elasticsearch:
 | 
					#meilisearch:
 | 
				
			||||||
#  host: localhost
 | 
					#  host: meilisearch
 | 
				
			||||||
#  port: 9200
 | 
					#  port: 7700
 | 
				
			||||||
#  ssl: false
 | 
					#  apiKey: ''
 | 
				
			||||||
#  user:
 | 
					 | 
				
			||||||
#  pass:
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
#   ┌───────────────┐
 | 
					#   ┌───────────────┐
 | 
				
			||||||
#───┘ ID generation └───────────────────────────────────────────
 | 
					#───┘ ID generation └───────────────────────────────────────────
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -95,15 +95,13 @@ redis:
 | 
				
			||||||
#  #prefix: example-prefix
 | 
					#  #prefix: example-prefix
 | 
				
			||||||
#  #db: 1
 | 
					#  #db: 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#   ┌─────────────────────────────┐
 | 
					#   ┌───────────────────────────┐
 | 
				
			||||||
#───┘ Elasticsearch configuration └─────────────────────────────
 | 
					#───┘ MeiliSearch configuration └─────────────────────────────
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#elasticsearch:
 | 
					#meilisearch:
 | 
				
			||||||
#  host: localhost
 | 
					#  host: localhost
 | 
				
			||||||
#  port: 9200
 | 
					#  port: 7700
 | 
				
			||||||
#  ssl: false
 | 
					#  apiKey: ''
 | 
				
			||||||
#  user: 
 | 
					 | 
				
			||||||
#  pass: 
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
#   ┌───────────────┐
 | 
					#   ┌───────────────┐
 | 
				
			||||||
#───┘ ID generation └───────────────────────────────────────────
 | 
					#───┘ ID generation └───────────────────────────────────────────
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -95,15 +95,13 @@ redis:
 | 
				
			||||||
#  #prefix: example-prefix
 | 
					#  #prefix: example-prefix
 | 
				
			||||||
#  #db: 1
 | 
					#  #db: 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#   ┌─────────────────────────────┐
 | 
					#   ┌───────────────────────────┐
 | 
				
			||||||
#───┘ Elasticsearch configuration └─────────────────────────────
 | 
					#───┘ MeiliSearch configuration └─────────────────────────────
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#elasticsearch:
 | 
					#meilisearch:
 | 
				
			||||||
#  host: localhost
 | 
					#  host: meilisearch
 | 
				
			||||||
#  port: 9200
 | 
					#  port: 7700
 | 
				
			||||||
#  ssl: false
 | 
					#  apiKey: ''
 | 
				
			||||||
#  user:
 | 
					 | 
				
			||||||
#  pass:
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
#   ┌───────────────┐
 | 
					#   ┌───────────────┐
 | 
				
			||||||
#───┘ ID generation └───────────────────────────────────────────
 | 
					#───┘ ID generation └───────────────────────────────────────────
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -8,7 +8,6 @@ build/
 | 
				
			||||||
built/
 | 
					built/
 | 
				
			||||||
db/
 | 
					db/
 | 
				
			||||||
docker-compose.yml
 | 
					docker-compose.yml
 | 
				
			||||||
elasticsearch/
 | 
					 | 
				
			||||||
node_modules/
 | 
					node_modules/
 | 
				
			||||||
packages/*/node_modules
 | 
					packages/*/node_modules
 | 
				
			||||||
redis/
 | 
					redis/
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										2
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							| 
						 | 
					@ -44,7 +44,7 @@ built
 | 
				
			||||||
/data
 | 
					/data
 | 
				
			||||||
/.cache-loader
 | 
					/.cache-loader
 | 
				
			||||||
/db
 | 
					/db
 | 
				
			||||||
/elasticsearch
 | 
					/meili_data
 | 
				
			||||||
npm-debug.log
 | 
					npm-debug.log
 | 
				
			||||||
*.pem
 | 
					*.pem
 | 
				
			||||||
run.bat
 | 
					run.bat
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -17,6 +17,7 @@
 | 
				
			||||||
- Node.js 18.6.0以上が必要になりました
 | 
					- Node.js 18.6.0以上が必要になりました
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### General
 | 
					### General
 | 
				
			||||||
 | 
					- Meilisearchを全文検索に使用できるようになりました
 | 
				
			||||||
- 新規登録前に簡潔なルールをユーザーに表示できる、サーバールール機能を追加
 | 
					- 新規登録前に簡潔なルールをユーザーに表示できる、サーバールール機能を追加
 | 
				
			||||||
- ユーザーへの自分用メモ機能
 | 
					- ユーザーへの自分用メモ機能
 | 
				
			||||||
  * ユーザーに対して、自分だけが見られるメモを追加できるようになりました。  
 | 
					  * ユーザーに対して、自分だけが見られるメモを追加できるようになりました。  
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -116,15 +116,13 @@ redis:
 | 
				
			||||||
#  #prefix: example-prefix
 | 
					#  #prefix: example-prefix
 | 
				
			||||||
#  #db: 1
 | 
					#  #db: 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#   ┌─────────────────────────────┐
 | 
					#   ┌───────────────────────────┐
 | 
				
			||||||
#───┘ Elasticsearch configuration └─────────────────────────────
 | 
					#───┘ MeiliSearch configuration └─────────────────────────────
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#elasticsearch:
 | 
					#meilisearch:
 | 
				
			||||||
#  host: localhost
 | 
					#  host: localhost
 | 
				
			||||||
#  port: 9200
 | 
					#  port: 7700
 | 
				
			||||||
#  ssl: false
 | 
					#  apiKey: ''
 | 
				
			||||||
#  user:
 | 
					 | 
				
			||||||
#  pass:
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
#   ┌───────────────┐
 | 
					#   ┌───────────────┐
 | 
				
			||||||
#───┘ ID generation └───────────────────────────────────────────
 | 
					#───┘ ID generation └───────────────────────────────────────────
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -7,7 +7,7 @@ services:
 | 
				
			||||||
    links:
 | 
					    links:
 | 
				
			||||||
      - db
 | 
					      - db
 | 
				
			||||||
      - redis
 | 
					      - redis
 | 
				
			||||||
#      - es
 | 
					#     - meilisearch
 | 
				
			||||||
    depends_on:
 | 
					    depends_on:
 | 
				
			||||||
      db:
 | 
					      db:
 | 
				
			||||||
        condition: service_healthy
 | 
					        condition: service_healthy
 | 
				
			||||||
| 
						 | 
					@ -48,16 +48,18 @@ services:
 | 
				
			||||||
      interval: 5s
 | 
					      interval: 5s
 | 
				
			||||||
      retries: 20
 | 
					      retries: 20
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#  es:
 | 
					#  meilisearch:
 | 
				
			||||||
#    restart: always
 | 
					#    restart: always
 | 
				
			||||||
#    image: docker.elastic.co/elasticsearch/elasticsearch-oss:6.4.2
 | 
					#    image: getmeili/meilisearch:v1.1.1
 | 
				
			||||||
#    environment:
 | 
					#    environment:
 | 
				
			||||||
#      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
 | 
					#      - MEILI_NO_ANALYTICS=true
 | 
				
			||||||
#      - "TAKE_FILE_OWNERSHIP=111"
 | 
					#      - MEILI_ENV=production
 | 
				
			||||||
 | 
					#    env_file:
 | 
				
			||||||
 | 
					#      - .config/meilisearch.env
 | 
				
			||||||
#    networks:
 | 
					#    networks:
 | 
				
			||||||
#      - internal_network
 | 
					#      - internal_network
 | 
				
			||||||
#    volumes:
 | 
					#    volumes:
 | 
				
			||||||
#      - ./elasticsearch:/usr/share/elasticsearch/data
 | 
					#      - ./meili_data:/meili_data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
networks:
 | 
					networks:
 | 
				
			||||||
  internal_network:
 | 
					  internal_network:
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -91,6 +91,7 @@
 | 
				
			||||||
		"jsdom": "21.1.1",
 | 
							"jsdom": "21.1.1",
 | 
				
			||||||
		"json5": "2.2.3",
 | 
							"json5": "2.2.3",
 | 
				
			||||||
		"jsonld": "8.1.1",
 | 
							"jsonld": "8.1.1",
 | 
				
			||||||
 | 
							"meilisearch": "0.32.3",
 | 
				
			||||||
		"jsrsasign": "10.8.6",
 | 
							"jsrsasign": "10.8.6",
 | 
				
			||||||
		"mfm-js": "0.23.3",
 | 
							"mfm-js": "0.23.3",
 | 
				
			||||||
		"mime-types": "2.1.35",
 | 
							"mime-types": "2.1.35",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,6 +2,7 @@ import { setTimeout } from 'node:timers/promises';
 | 
				
			||||||
import { Global, Inject, Module } from '@nestjs/common';
 | 
					import { Global, Inject, Module } from '@nestjs/common';
 | 
				
			||||||
import * as Redis from 'ioredis';
 | 
					import * as Redis from 'ioredis';
 | 
				
			||||||
import { DataSource } from 'typeorm';
 | 
					import { DataSource } from 'typeorm';
 | 
				
			||||||
 | 
					import { MeiliSearch } from 'meilisearch';
 | 
				
			||||||
import { DI } from './di-symbols.js';
 | 
					import { DI } from './di-symbols.js';
 | 
				
			||||||
import { loadConfig } from './config.js';
 | 
					import { loadConfig } from './config.js';
 | 
				
			||||||
import { createPostgresDataSource } from './postgres.js';
 | 
					import { createPostgresDataSource } from './postgres.js';
 | 
				
			||||||
| 
						 | 
					@ -22,6 +23,21 @@ const $db: Provider = {
 | 
				
			||||||
	inject: [DI.config],
 | 
						inject: [DI.config],
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const $meilisearch: Provider = {
 | 
				
			||||||
 | 
						provide: DI.meilisearch,
 | 
				
			||||||
 | 
						useFactory: (config) => {
 | 
				
			||||||
 | 
							if (config.meilisearch) {
 | 
				
			||||||
 | 
								return new MeiliSearch({
 | 
				
			||||||
 | 
									host: `http://${config.meilisearch.host}:${config.meilisearch.port}`, 
 | 
				
			||||||
 | 
									apiKey: config.meilisearch.apiKey,
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								return null;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						inject: [DI.config],
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const $redis: Provider = {
 | 
					const $redis: Provider = {
 | 
				
			||||||
	provide: DI.redis,
 | 
						provide: DI.redis,
 | 
				
			||||||
	useFactory: (config) => {
 | 
						useFactory: (config) => {
 | 
				
			||||||
| 
						 | 
					@ -73,8 +89,8 @@ const $redisForSub: Provider = {
 | 
				
			||||||
@Global()
 | 
					@Global()
 | 
				
			||||||
@Module({
 | 
					@Module({
 | 
				
			||||||
	imports: [RepositoryModule],
 | 
						imports: [RepositoryModule],
 | 
				
			||||||
	providers: [$config, $db, $redis, $redisForPub, $redisForSub],
 | 
						providers: [$config, $db, $meilisearch, $redis, $redisForPub, $redisForSub],
 | 
				
			||||||
	exports: [$config, $db, $redis, $redisForPub, $redisForSub, RepositoryModule],
 | 
						exports: [$config, $db, $meilisearch, $redis, $redisForPub, $redisForSub, RepositoryModule],
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
export class GlobalModule implements OnApplicationShutdown {
 | 
					export class GlobalModule implements OnApplicationShutdown {
 | 
				
			||||||
	constructor(
 | 
						constructor(
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -57,13 +57,10 @@ export type Source = {
 | 
				
			||||||
		db?: number;
 | 
							db?: number;
 | 
				
			||||||
		prefix?: string;
 | 
							prefix?: string;
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
	elasticsearch: {
 | 
						meilisearch?: {
 | 
				
			||||||
		host: string;
 | 
							host: string;
 | 
				
			||||||
		port: number;
 | 
							port: string;
 | 
				
			||||||
		ssl?: boolean;
 | 
							apiKey: string;
 | 
				
			||||||
		user?: string;
 | 
					 | 
				
			||||||
		pass?: string;
 | 
					 | 
				
			||||||
		index?: string;
 | 
					 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	proxy?: string;
 | 
						proxy?: string;
 | 
				
			||||||
| 
						 | 
					@ -139,6 +136,7 @@ const path = process.env.MISSKEY_CONFIG_YML
 | 
				
			||||||
	: process.env.NODE_ENV === 'test'
 | 
						: process.env.NODE_ENV === 'test'
 | 
				
			||||||
		? resolve(dir, 'test.yml')
 | 
							? resolve(dir, 'test.yml')
 | 
				
			||||||
		: resolve(dir, 'default.yml');
 | 
							: resolve(dir, 'default.yml');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function loadConfig() {
 | 
					export function loadConfig() {
 | 
				
			||||||
	const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../built/meta.json`, 'utf-8'));
 | 
						const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../built/meta.json`, 'utf-8'));
 | 
				
			||||||
	const clientManifestExists = fs.existsSync(_dirname + '/../../../built/_vite_/manifest.json');
 | 
						const clientManifestExists = fs.existsSync(_dirname + '/../../../built/_vite_/manifest.json');
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -50,6 +50,7 @@ import { WebhookService } from './WebhookService.js';
 | 
				
			||||||
import { ProxyAccountService } from './ProxyAccountService.js';
 | 
					import { ProxyAccountService } from './ProxyAccountService.js';
 | 
				
			||||||
import { UtilityService } from './UtilityService.js';
 | 
					import { UtilityService } from './UtilityService.js';
 | 
				
			||||||
import { FileInfoService } from './FileInfoService.js';
 | 
					import { FileInfoService } from './FileInfoService.js';
 | 
				
			||||||
 | 
					import { SearchService } from './SearchService.js';
 | 
				
			||||||
import { ChartLoggerService } from './chart/ChartLoggerService.js';
 | 
					import { ChartLoggerService } from './chart/ChartLoggerService.js';
 | 
				
			||||||
import FederationChart from './chart/charts/federation.js';
 | 
					import FederationChart from './chart/charts/federation.js';
 | 
				
			||||||
import NotesChart from './chart/charts/notes.js';
 | 
					import NotesChart from './chart/charts/notes.js';
 | 
				
			||||||
| 
						 | 
					@ -171,6 +172,8 @@ const $VideoProcessingService: Provider = { provide: 'VideoProcessingService', u
 | 
				
			||||||
const $WebhookService: Provider = { provide: 'WebhookService', useExisting: WebhookService };
 | 
					const $WebhookService: Provider = { provide: 'WebhookService', useExisting: WebhookService };
 | 
				
			||||||
const $UtilityService: Provider = { provide: 'UtilityService', useExisting: UtilityService };
 | 
					const $UtilityService: Provider = { provide: 'UtilityService', useExisting: UtilityService };
 | 
				
			||||||
const $FileInfoService: Provider = { provide: 'FileInfoService', useExisting: FileInfoService };
 | 
					const $FileInfoService: Provider = { provide: 'FileInfoService', useExisting: FileInfoService };
 | 
				
			||||||
 | 
					const $SearchService: Provider = { provide: 'SearchService', useExisting: SearchService };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const $ChartLoggerService: Provider = { provide: 'ChartLoggerService', useExisting: ChartLoggerService };
 | 
					const $ChartLoggerService: Provider = { provide: 'ChartLoggerService', useExisting: ChartLoggerService };
 | 
				
			||||||
const $FederationChart: Provider = { provide: 'FederationChart', useExisting: FederationChart };
 | 
					const $FederationChart: Provider = { provide: 'FederationChart', useExisting: FederationChart };
 | 
				
			||||||
const $NotesChart: Provider = { provide: 'NotesChart', useExisting: NotesChart };
 | 
					const $NotesChart: Provider = { provide: 'NotesChart', useExisting: NotesChart };
 | 
				
			||||||
| 
						 | 
					@ -295,6 +298,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 | 
				
			||||||
		WebhookService,
 | 
							WebhookService,
 | 
				
			||||||
		UtilityService,
 | 
							UtilityService,
 | 
				
			||||||
		FileInfoService,
 | 
							FileInfoService,
 | 
				
			||||||
 | 
							SearchService,
 | 
				
			||||||
		ChartLoggerService,
 | 
							ChartLoggerService,
 | 
				
			||||||
		FederationChart,
 | 
							FederationChart,
 | 
				
			||||||
		NotesChart,
 | 
							NotesChart,
 | 
				
			||||||
| 
						 | 
					@ -413,6 +417,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 | 
				
			||||||
		$WebhookService,
 | 
							$WebhookService,
 | 
				
			||||||
		$UtilityService,
 | 
							$UtilityService,
 | 
				
			||||||
		$FileInfoService,
 | 
							$FileInfoService,
 | 
				
			||||||
 | 
							$SearchService,
 | 
				
			||||||
		$ChartLoggerService,
 | 
							$ChartLoggerService,
 | 
				
			||||||
		$FederationChart,
 | 
							$FederationChart,
 | 
				
			||||||
		$NotesChart,
 | 
							$NotesChart,
 | 
				
			||||||
| 
						 | 
					@ -532,6 +537,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 | 
				
			||||||
		WebhookService,
 | 
							WebhookService,
 | 
				
			||||||
		UtilityService,
 | 
							UtilityService,
 | 
				
			||||||
		FileInfoService,
 | 
							FileInfoService,
 | 
				
			||||||
 | 
							SearchService,
 | 
				
			||||||
		FederationChart,
 | 
							FederationChart,
 | 
				
			||||||
		NotesChart,
 | 
							NotesChart,
 | 
				
			||||||
		UsersChart,
 | 
							UsersChart,
 | 
				
			||||||
| 
						 | 
					@ -649,6 +655,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 | 
				
			||||||
		$WebhookService,
 | 
							$WebhookService,
 | 
				
			||||||
		$UtilityService,
 | 
							$UtilityService,
 | 
				
			||||||
		$FileInfoService,
 | 
							$FileInfoService,
 | 
				
			||||||
 | 
							$SearchService,
 | 
				
			||||||
		$FederationChart,
 | 
							$FederationChart,
 | 
				
			||||||
		$NotesChart,
 | 
							$NotesChart,
 | 
				
			||||||
		$UsersChart,
 | 
							$UsersChart,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -46,6 +46,7 @@ import { bindThis } from '@/decorators.js';
 | 
				
			||||||
import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js';
 | 
					import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js';
 | 
				
			||||||
import { RoleService } from '@/core/RoleService.js';
 | 
					import { RoleService } from '@/core/RoleService.js';
 | 
				
			||||||
import { MetaService } from '@/core/MetaService.js';
 | 
					import { MetaService } from '@/core/MetaService.js';
 | 
				
			||||||
 | 
					import { SearchService } from '@/core/SearchService.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const mutedWordsCache = new MemorySingleCache<{ userId: UserProfile['userId']; mutedWords: UserProfile['mutedWords']; }[]>(1000 * 60 * 5);
 | 
					const mutedWordsCache = new MemorySingleCache<{ userId: UserProfile['userId']; mutedWords: UserProfile['mutedWords']; }[]>(1000 * 60 * 5);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -198,6 +199,7 @@ export class NoteCreateService implements OnApplicationShutdown {
 | 
				
			||||||
		private apRendererService: ApRendererService,
 | 
							private apRendererService: ApRendererService,
 | 
				
			||||||
		private roleService: RoleService,
 | 
							private roleService: RoleService,
 | 
				
			||||||
		private metaService: MetaService,
 | 
							private metaService: MetaService,
 | 
				
			||||||
 | 
							private searchService: SearchService,
 | 
				
			||||||
		private notesChart: NotesChart,
 | 
							private notesChart: NotesChart,
 | 
				
			||||||
		private perUserNotesChart: PerUserNotesChart,
 | 
							private perUserNotesChart: PerUserNotesChart,
 | 
				
			||||||
		private activeUsersChart: ActiveUsersChart,
 | 
							private activeUsersChart: ActiveUsersChart,
 | 
				
			||||||
| 
						 | 
					@ -728,17 +730,9 @@ export class NoteCreateService implements OnApplicationShutdown {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	@bindThis
 | 
						@bindThis
 | 
				
			||||||
	private index(note: Note) {
 | 
						private index(note: Note) {
 | 
				
			||||||
		if (note.text == null || this.config.elasticsearch == null) return;
 | 
							if (note.text == null && note.cw == null) return;
 | 
				
			||||||
		/*
 | 
							
 | 
				
			||||||
	es!.index({
 | 
							this.searchService.indexNote(note);
 | 
				
			||||||
		index: this.config.elasticsearch.index ?? 'misskey_note',
 | 
					 | 
				
			||||||
		id: note.id.toString(),
 | 
					 | 
				
			||||||
		body: {
 | 
					 | 
				
			||||||
			text: normalizeForSearch(note.text),
 | 
					 | 
				
			||||||
			userId: note.userId,
 | 
					 | 
				
			||||||
			userHost: note.userHost,
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	});*/
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	@bindThis
 | 
						@bindThis
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										166
									
								
								packages/backend/src/core/SearchService.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										166
									
								
								packages/backend/src/core/SearchService.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,166 @@
 | 
				
			||||||
 | 
					import { Inject, Injectable } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { In } from 'typeorm';
 | 
				
			||||||
 | 
					import { DI } from '@/di-symbols.js';
 | 
				
			||||||
 | 
					import type { Config } from '@/config.js';
 | 
				
			||||||
 | 
					import { bindThis } from '@/decorators.js';
 | 
				
			||||||
 | 
					import { Note } from '@/models/entities/Note.js';
 | 
				
			||||||
 | 
					import { User } from '@/models/index.js';
 | 
				
			||||||
 | 
					import type { NotesRepository } from '@/models/index.js';
 | 
				
			||||||
 | 
					import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
 | 
				
			||||||
 | 
					import { QueryService } from '@/core/QueryService.js';
 | 
				
			||||||
 | 
					import { IdService } from '@/core/IdService.js';
 | 
				
			||||||
 | 
					import type { Index, MeiliSearch } from 'meilisearch';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type K = string;
 | 
				
			||||||
 | 
					type V = string | number | boolean;
 | 
				
			||||||
 | 
					type Q =
 | 
				
			||||||
 | 
						{ op: '=', k: K, v: V } |
 | 
				
			||||||
 | 
						{ op: '!=', k: K, v: V } |
 | 
				
			||||||
 | 
						{ op: '>', k: K, v: number } |
 | 
				
			||||||
 | 
						{ op: '<', k: K, v: number } |
 | 
				
			||||||
 | 
						{ op: '>=', k: K, v: number } |
 | 
				
			||||||
 | 
						{ op: '<=', k: K, v: number } |
 | 
				
			||||||
 | 
						{ op: 'and', qs: Q[] } |
 | 
				
			||||||
 | 
						{ op: 'or', qs: Q[] } |
 | 
				
			||||||
 | 
						{ op: 'not', q: Q };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function compileValue(value: V): string {
 | 
				
			||||||
 | 
						if (typeof value === 'string') {
 | 
				
			||||||
 | 
							return `'${value}'`; // TODO: escape
 | 
				
			||||||
 | 
						} else if (typeof value === 'number') {
 | 
				
			||||||
 | 
							return value.toString();
 | 
				
			||||||
 | 
						} else if (typeof value === 'boolean') {
 | 
				
			||||||
 | 
							return value.toString();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						throw new Error('unrecognized value');
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function compileQuery(q: Q): string {
 | 
				
			||||||
 | 
						switch (q.op) {
 | 
				
			||||||
 | 
							case '=': return `(${q.k} = ${compileValue(q.v)})`;
 | 
				
			||||||
 | 
							case '!=': return `(${q.k} != ${compileValue(q.v)})`;
 | 
				
			||||||
 | 
							case '>': return `(${q.k} > ${compileValue(q.v)})`;
 | 
				
			||||||
 | 
							case '<': return `(${q.k} < ${compileValue(q.v)})`;
 | 
				
			||||||
 | 
							case '>=': return `(${q.k} >= ${compileValue(q.v)})`;
 | 
				
			||||||
 | 
							case '<=': return `(${q.k} <= ${compileValue(q.v)})`;
 | 
				
			||||||
 | 
							case 'and': return q.qs.length === 0 ? '' : `(${ q.qs.map(_q => compileQuery(_q)).join(' AND ') })`;
 | 
				
			||||||
 | 
							case 'or': return q.qs.length === 0 ? '' : `(${ q.qs.map(_q => compileQuery(_q)).join(' OR ') })`;
 | 
				
			||||||
 | 
							case 'not': return `(NOT ${compileQuery(q.q)})`;
 | 
				
			||||||
 | 
							default: throw new Error('unrecognized query operator');
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Injectable()
 | 
				
			||||||
 | 
					export class SearchService {
 | 
				
			||||||
 | 
						private meilisearchNoteIndex: Index | null = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						constructor(
 | 
				
			||||||
 | 
							@Inject(DI.config)
 | 
				
			||||||
 | 
							private config: Config,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							@Inject(DI.meilisearch)
 | 
				
			||||||
 | 
							private meilisearch: MeiliSearch | null,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							@Inject(DI.notesRepository)
 | 
				
			||||||
 | 
							private notesRepository: NotesRepository,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							private queryService: QueryService,
 | 
				
			||||||
 | 
							private idService: IdService,
 | 
				
			||||||
 | 
						) {
 | 
				
			||||||
 | 
							if (meilisearch) {
 | 
				
			||||||
 | 
								this.meilisearchNoteIndex = meilisearch.index('notes');
 | 
				
			||||||
 | 
								this.meilisearchNoteIndex.updateSettings({
 | 
				
			||||||
 | 
									searchableAttributes: [
 | 
				
			||||||
 | 
										'text',
 | 
				
			||||||
 | 
										'cw',
 | 
				
			||||||
 | 
									],
 | 
				
			||||||
 | 
									sortableAttributes: [
 | 
				
			||||||
 | 
										'createdAt',
 | 
				
			||||||
 | 
									],
 | 
				
			||||||
 | 
									filterableAttributes: [
 | 
				
			||||||
 | 
										'createdAt',
 | 
				
			||||||
 | 
										'userId',
 | 
				
			||||||
 | 
										'userHost',
 | 
				
			||||||
 | 
										'channelId',
 | 
				
			||||||
 | 
									],
 | 
				
			||||||
 | 
									typoTolerance: {
 | 
				
			||||||
 | 
										enabled: false,
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
									pagination: {
 | 
				
			||||||
 | 
										maxTotalHits: 10000,
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@bindThis
 | 
				
			||||||
 | 
						public async indexNote(note: Note): Promise<void> {
 | 
				
			||||||
 | 
							if (this.meilisearch) {
 | 
				
			||||||
 | 
								this.meilisearchNoteIndex!.addDocuments([{
 | 
				
			||||||
 | 
									id: note.id,
 | 
				
			||||||
 | 
									createdAt: note.createdAt.getTime(),
 | 
				
			||||||
 | 
									userId: note.userId,
 | 
				
			||||||
 | 
									userHost: note.userHost,
 | 
				
			||||||
 | 
									channelId: note.channelId,
 | 
				
			||||||
 | 
									cw: note.cw,
 | 
				
			||||||
 | 
									text: note.text,
 | 
				
			||||||
 | 
								}], {
 | 
				
			||||||
 | 
									primaryKey: 'id',
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@bindThis
 | 
				
			||||||
 | 
						public async searchNote(q: string, me: User | null, opts: {
 | 
				
			||||||
 | 
							userId?: Note['userId'] | null;
 | 
				
			||||||
 | 
							channelId?: Note['channelId'] | null;
 | 
				
			||||||
 | 
						}, pagination: {
 | 
				
			||||||
 | 
							untilId?: Note['id'];
 | 
				
			||||||
 | 
							sinceId?: Note['id'];
 | 
				
			||||||
 | 
							limit?: number;
 | 
				
			||||||
 | 
						}): Promise<Note[]> {
 | 
				
			||||||
 | 
							if (this.meilisearch) {
 | 
				
			||||||
 | 
								const filter: Q = {
 | 
				
			||||||
 | 
									op: 'and',
 | 
				
			||||||
 | 
									qs: [],
 | 
				
			||||||
 | 
								};
 | 
				
			||||||
 | 
								if (pagination.untilId) filter.qs.push({ op: '<', k: 'createdAt', v: this.idService.parse(pagination.untilId).date.getTime() });
 | 
				
			||||||
 | 
								if (pagination.sinceId) filter.qs.push({ op: '>', k: 'createdAt', v: this.idService.parse(pagination.sinceId).date.getTime() });
 | 
				
			||||||
 | 
								if (opts.userId) filter.qs.push({ op: '=', k: 'userId', v: opts.userId });
 | 
				
			||||||
 | 
								if (opts.channelId) filter.qs.push({ op: '=', k: 'channelId', v: opts.channelId });
 | 
				
			||||||
 | 
								const res = await this.meilisearchNoteIndex!.search(q, {
 | 
				
			||||||
 | 
									sort: ['createdAt:desc'],
 | 
				
			||||||
 | 
									matchingStrategy: 'all',
 | 
				
			||||||
 | 
									attributesToRetrieve: ['id', 'createdAt'],
 | 
				
			||||||
 | 
									filter: compileQuery(filter),
 | 
				
			||||||
 | 
									limit: pagination.limit,
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
								if (res.hits.length === 0) return [];
 | 
				
			||||||
 | 
								return await this.notesRepository.findBy({
 | 
				
			||||||
 | 
									id: In(res.hits.map(x => x.id)),
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), pagination.sinceId, pagination.untilId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if (opts.userId) {
 | 
				
			||||||
 | 
									query.andWhere('note.userId = :userId', { userId: opts.userId });
 | 
				
			||||||
 | 
								} else if (opts.channelId) {
 | 
				
			||||||
 | 
									query.andWhere('note.channelId = :channelId', { channelId: opts.channelId });
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								query
 | 
				
			||||||
 | 
									.andWhere('note.text ILIKE :q', { q: `%${ sqlLikeEscape(q) }%` })
 | 
				
			||||||
 | 
									.innerJoinAndSelect('note.user', 'user')
 | 
				
			||||||
 | 
									.leftJoinAndSelect('note.reply', 'reply')
 | 
				
			||||||
 | 
									.leftJoinAndSelect('note.renote', 'renote')
 | 
				
			||||||
 | 
									.leftJoinAndSelect('reply.user', 'replyUser')
 | 
				
			||||||
 | 
									.leftJoinAndSelect('renote.user', 'renoteUser');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								this.queryService.generateVisibilityQuery(query, me);
 | 
				
			||||||
 | 
								if (me) this.queryService.generateMutedUserQuery(query, me);
 | 
				
			||||||
 | 
								if (me) this.queryService.generateBlockedUserQuery(query, me);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								return await query.take(pagination.limit).getMany();
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,7 @@
 | 
				
			||||||
export const DI = {
 | 
					export const DI = {
 | 
				
			||||||
	config: Symbol('config'),
 | 
						config: Symbol('config'),
 | 
				
			||||||
	db: Symbol('db'),
 | 
						db: Symbol('db'),
 | 
				
			||||||
 | 
						meilisearch: Symbol('meilisearch'),
 | 
				
			||||||
	redis: Symbol('redis'),
 | 
						redis: Symbol('redis'),
 | 
				
			||||||
	redisForPub: Symbol('redisForPub'),
 | 
						redisForPub: Symbol('redisForPub'),
 | 
				
			||||||
	redisForSub: Symbol('redisForSub'),
 | 
						redisForSub: Symbol('redisForSub'),
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -201,10 +201,6 @@ export const meta = {
 | 
				
			||||||
						type: 'boolean',
 | 
											type: 'boolean',
 | 
				
			||||||
						optional: false, nullable: false,
 | 
											optional: false, nullable: false,
 | 
				
			||||||
					},
 | 
										},
 | 
				
			||||||
					elasticsearch: {
 | 
					 | 
				
			||||||
						type: 'boolean',
 | 
					 | 
				
			||||||
						optional: false, nullable: false,
 | 
					 | 
				
			||||||
					},
 | 
					 | 
				
			||||||
					hcaptcha: {
 | 
										hcaptcha: {
 | 
				
			||||||
						type: 'boolean',
 | 
											type: 'boolean',
 | 
				
			||||||
						optional: false, nullable: false,
 | 
											optional: false, nullable: false,
 | 
				
			||||||
| 
						 | 
					@ -331,7 +327,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 | 
				
			||||||
				response.features = {
 | 
									response.features = {
 | 
				
			||||||
					registration: !instance.disableRegistration,
 | 
										registration: !instance.disableRegistration,
 | 
				
			||||||
					emailRequiredForSignup: instance.emailRequiredForSignup,
 | 
										emailRequiredForSignup: instance.emailRequiredForSignup,
 | 
				
			||||||
					elasticsearch: this.config.elasticsearch ? true : false,
 | 
					 | 
				
			||||||
					hcaptcha: instance.enableHcaptcha,
 | 
										hcaptcha: instance.enableHcaptcha,
 | 
				
			||||||
					recaptcha: instance.enableRecaptcha,
 | 
										recaptcha: instance.enableRecaptcha,
 | 
				
			||||||
					turnstile: instance.enableTurnstile,
 | 
										turnstile: instance.enableTurnstile,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,11 +1,10 @@
 | 
				
			||||||
import { Inject, Injectable } from '@nestjs/common';
 | 
					import { Inject, Injectable } from '@nestjs/common';
 | 
				
			||||||
import type { NotesRepository } from '@/models/index.js';
 | 
					import type { NotesRepository } from '@/models/index.js';
 | 
				
			||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
 | 
					import { Endpoint } from '@/server/api/endpoint-base.js';
 | 
				
			||||||
import { QueryService } from '@/core/QueryService.js';
 | 
					import { SearchService } from '@/core/SearchService.js';
 | 
				
			||||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
 | 
					import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
 | 
				
			||||||
import type { Config } from '@/config.js';
 | 
					import type { Config } from '@/config.js';
 | 
				
			||||||
import { DI } from '@/di-symbols.js';
 | 
					import { DI } from '@/di-symbols.js';
 | 
				
			||||||
import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
 | 
					 | 
				
			||||||
import { RoleService } from '@/core/RoleService.js';
 | 
					import { RoleService } from '@/core/RoleService.js';
 | 
				
			||||||
import { ApiError } from '../../error.js';
 | 
					import { ApiError } from '../../error.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -61,11 +60,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 | 
				
			||||||
		@Inject(DI.config)
 | 
							@Inject(DI.config)
 | 
				
			||||||
		private config: Config,
 | 
							private config: Config,
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
		@Inject(DI.notesRepository)
 | 
					 | 
				
			||||||
		private notesRepository: NotesRepository,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		private noteEntityService: NoteEntityService,
 | 
							private noteEntityService: NoteEntityService,
 | 
				
			||||||
		private queryService: QueryService,
 | 
							private searchService: SearchService,
 | 
				
			||||||
		private roleService: RoleService,
 | 
							private roleService: RoleService,
 | 
				
			||||||
	) {
 | 
						) {
 | 
				
			||||||
		super(meta, paramDef, async (ps, me) => {
 | 
							super(meta, paramDef, async (ps, me) => {
 | 
				
			||||||
| 
						 | 
					@ -74,27 +70,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 | 
				
			||||||
				throw new ApiError(meta.errors.unavailable);
 | 
									throw new ApiError(meta.errors.unavailable);
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
			const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId);
 | 
								const notes = await this.searchService.searchNote(ps.query, me, {
 | 
				
			||||||
 | 
									userId: ps.userId,
 | 
				
			||||||
			if (ps.userId) {
 | 
									channelId: ps.channelId,
 | 
				
			||||||
				query.andWhere('note.userId = :userId', { userId: ps.userId });
 | 
								}, {
 | 
				
			||||||
			} else if (ps.channelId) {
 | 
									untilId: ps.untilId,
 | 
				
			||||||
				query.andWhere('note.channelId = :channelId', { channelId: ps.channelId });
 | 
									sinceId: ps.sinceId,
 | 
				
			||||||
			}
 | 
									limit: ps.limit,
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
			query
 | 
					 | 
				
			||||||
				.andWhere('note.text ILIKE :q', { q: `%${ sqlLikeEscape(ps.query) }%` })
 | 
					 | 
				
			||||||
				.innerJoinAndSelect('note.user', 'user')
 | 
					 | 
				
			||||||
				.leftJoinAndSelect('note.reply', 'reply')
 | 
					 | 
				
			||||||
				.leftJoinAndSelect('note.renote', 'renote')
 | 
					 | 
				
			||||||
				.leftJoinAndSelect('reply.user', 'replyUser')
 | 
					 | 
				
			||||||
				.leftJoinAndSelect('renote.user', 'renoteUser');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			this.queryService.generateVisibilityQuery(query, me);
 | 
					 | 
				
			||||||
			if (me) this.queryService.generateMutedUserQuery(query, me);
 | 
					 | 
				
			||||||
			if (me) this.queryService.generateBlockedUserQuery(query, me);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			const notes = await query.take(ps.limit).getMany();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
			return await this.noteEntityService.packMany(notes, me);
 | 
								return await this.noteEntityService.packMany(notes, me);
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										15
									
								
								pnpm-lock.yaml
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										15
									
								
								pnpm-lock.yaml
									
										
									
										generated
									
									
									
								
							| 
						 | 
					@ -229,6 +229,9 @@ importers:
 | 
				
			||||||
      jsrsasign:
 | 
					      jsrsasign:
 | 
				
			||||||
        specifier: 10.8.6
 | 
					        specifier: 10.8.6
 | 
				
			||||||
        version: 10.8.6
 | 
					        version: 10.8.6
 | 
				
			||||||
 | 
					      meilisearch:
 | 
				
			||||||
 | 
					        specifier: 0.32.3
 | 
				
			||||||
 | 
					        version: 0.32.3
 | 
				
			||||||
      mfm-js:
 | 
					      mfm-js:
 | 
				
			||||||
        specifier: 0.23.3
 | 
					        specifier: 0.23.3
 | 
				
			||||||
        version: 0.23.3
 | 
					        version: 0.23.3
 | 
				
			||||||
| 
						 | 
					@ -9582,7 +9585,6 @@ packages:
 | 
				
			||||||
      node-fetch: 2.6.7
 | 
					      node-fetch: 2.6.7
 | 
				
			||||||
    transitivePeerDependencies:
 | 
					    transitivePeerDependencies:
 | 
				
			||||||
      - encoding
 | 
					      - encoding
 | 
				
			||||||
    dev: true
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /cross-spawn@5.1.0:
 | 
					  /cross-spawn@5.1.0:
 | 
				
			||||||
    resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==}
 | 
					    resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==}
 | 
				
			||||||
| 
						 | 
					@ -14496,6 +14498,14 @@ packages:
 | 
				
			||||||
    engines: {node: '>= 0.6'}
 | 
					    engines: {node: '>= 0.6'}
 | 
				
			||||||
    dev: true
 | 
					    dev: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /meilisearch@0.32.3:
 | 
				
			||||||
 | 
					    resolution: {integrity: sha512-EOgfBuRE5SiIPIpEDYe2HO0D7a4z5bexIgaAdJFma/dH5hx1kwO+u/qb2g3qKyjG+iA3l8MlmTj/Xd72uahaAw==}
 | 
				
			||||||
 | 
					    dependencies:
 | 
				
			||||||
 | 
					      cross-fetch: 3.1.5
 | 
				
			||||||
 | 
					    transitivePeerDependencies:
 | 
				
			||||||
 | 
					      - encoding
 | 
				
			||||||
 | 
					    dev: false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /memoizerific@1.11.3:
 | 
					  /memoizerific@1.11.3:
 | 
				
			||||||
    resolution: {integrity: sha512-/EuHYwAPdLtXwAwSZkh/Gutery6pD2KYd44oQLhAvQp/50mpyduZh8Q7PYHXTCJ+wuXxt7oij2LXyIJOOYFPog==}
 | 
					    resolution: {integrity: sha512-/EuHYwAPdLtXwAwSZkh/Gutery6pD2KYd44oQLhAvQp/50mpyduZh8Q7PYHXTCJ+wuXxt7oij2LXyIJOOYFPog==}
 | 
				
			||||||
    dependencies:
 | 
					    dependencies:
 | 
				
			||||||
| 
						 | 
					@ -14657,6 +14667,7 @@ packages:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /minimist@1.2.7:
 | 
					  /minimist@1.2.7:
 | 
				
			||||||
    resolution: {integrity: sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==}
 | 
					    resolution: {integrity: sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==}
 | 
				
			||||||
 | 
					    dev: false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /minimist@1.2.8:
 | 
					  /minimist@1.2.8:
 | 
				
			||||||
    resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
 | 
					    resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
 | 
				
			||||||
| 
						 | 
					@ -19700,7 +19711,7 @@ packages:
 | 
				
			||||||
      axios: 0.27.2(debug@4.3.4)
 | 
					      axios: 0.27.2(debug@4.3.4)
 | 
				
			||||||
      joi: 17.7.0
 | 
					      joi: 17.7.0
 | 
				
			||||||
      lodash: 4.17.21
 | 
					      lodash: 4.17.21
 | 
				
			||||||
      minimist: 1.2.7
 | 
					      minimist: 1.2.8
 | 
				
			||||||
      rxjs: 7.8.1
 | 
					      rxjs: 7.8.1
 | 
				
			||||||
    transitivePeerDependencies:
 | 
					    transitivePeerDependencies:
 | 
				
			||||||
      - debug
 | 
					      - debug
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue