Kaynağa Gözat

Added interface for validating terms

Taichi Masuyama 4 yıl önce
ebeveyn
işleme
61222691d5

+ 15 - 3
packages/app/src/server/interfaces/search.ts

@@ -14,22 +14,34 @@ export type QueryTerms = {
   not_tag: string[],
 }
 
-export type ParsedQuery = { queryString: string, terms?: QueryTerms, delegatorName?: string }
+export type ParsedQuery = { queryString: string, terms: QueryTerms, delegatorName?: string }
 
 export interface SearchQueryParser {
   parseSearchQuery(queryString: string): Promise<ParsedQuery>
 }
 
-export interface SearchResolver{
+export interface SearchResolver {
   resolve(parsedQuery: ParsedQuery): Promise<[SearchDelegator, SearchableData | null]>
 }
 
-export interface SearchDelegator<T = unknown> {
+export interface SearchDelegator<T = unknown, KEY extends AllTermsKey = AllTermsKey, QTERMS = unknown> {
   name?: SearchDelegatorName
   search(data: SearchableData | null, user, userGroups, option): Promise<ISearchResult<T>>
+  validateTerms(terms: QueryTerms): UnavailableTermsKey<KEY>[],
+  excludeUnavailableTerms(terms: QueryTerms): QTERMS,
 }
 
 export type SearchableData = {
   queryString: string
   terms: QueryTerms
 }
+
+// Terms Key types
+export type AllTermsKey = keyof QueryTerms;
+export type UnavailableTermsKey<K extends AllTermsKey> = Exclude<AllTermsKey, K>;
+export type ESTermsKey = 'match' | 'not_match' | 'phrase' | 'not_phrase' | 'prefix' | 'not_prefix' | 'tag' | 'not_tag';
+export type MongoTermsKey = 'match' | 'not_match' | 'prefix' | 'not_prefix';
+
+// Query Terms types
+export type ESQueryTerms = Pick<QueryTerms, ESTermsKey>;
+export type MongoQueryTerms = Pick<QueryTerms, MongoTermsKey>;

+ 3 - 2
packages/app/src/server/routes/search.js

@@ -113,7 +113,7 @@ module.exports = function(crowi, app) {
   api.search = async function(req, res) {
     const user = req.user;
     const {
-      q = null, type = null, sort = null, order = null,
+      q = null, nq = null, type = null, sort = null, order = null,
     } = req.query;
     let paginateOpts;
 
@@ -147,7 +147,8 @@ module.exports = function(crowi, app) {
     let delegatorName;
     try {
       const keyword = decodeURIComponent(q);
-      [searchResult, delegatorName] = await searchService.searchKeyword(keyword, user, userGroups, searchOpts);
+      const nqString = decodeURIComponent(nq);
+      [searchResult, delegatorName] = await searchService.searchKeyword(keyword, nqString, user, userGroups, searchOpts); // TODOT:
     }
     catch (err) {
       logger.error('Failed to search', err);

+ 23 - 2
packages/app/src/server/service/search-delegator/elasticsearch.ts

@@ -11,7 +11,7 @@ import { createBatchStream } from '../../util/batch-stream';
 import loggerFactory from '~/utils/logger';
 import { PageModel } from '../../models/page';
 import {
-  SearchDelegator, SearchableData, QueryTerms,
+  SearchDelegator, SearchableData, QueryTerms, UnavailableTermsKey, ESQueryTerms, ESTermsKey,
 } from '../../interfaces/search';
 import { SearchDelegatorName } from '~/interfaces/named-query';
 import {
@@ -40,7 +40,7 @@ const ES_SORT_ORDER = {
 
 type Data = any;
 
-class ElasticsearchDelegator implements SearchDelegator<Data> {
+class ElasticsearchDelegator implements SearchDelegator<Data, ESTermsKey, ESQueryTerms> {
 
   name!: SearchDelegatorName.DEFAULT
 
@@ -965,6 +965,10 @@ class ElasticsearchDelegator implements SearchDelegator<Data> {
   async search(data: SearchableData, user, userGroups, option): Promise<ISearchResult<unknown>> {
     const { queryString, terms } = data;
 
+    if (terms == null) {
+      throw Error('Cannnot process search since terms is undefined.');
+    }
+
     const from = option.offset || null;
     const size = option.limit || null;
     const sort = option.sort || null;
@@ -984,6 +988,23 @@ class ElasticsearchDelegator implements SearchDelegator<Data> {
     return this.searchKeyword(query);
   }
 
+  validateTerms(terms: QueryTerms): UnavailableTermsKey<ESTermsKey>[] {
+    return [];
+  }
+
+  excludeUnavailableTerms(terms: QueryTerms): ESQueryTerms {
+    return {
+      match: [],
+      not_match: [],
+      phrase: [],
+      not_phrase: [],
+      prefix: [],
+      not_prefix: [],
+      tag: [],
+      not_tag: [],
+    };
+  }
+
   async syncPageUpdated(page, user) {
     logger.debug('SearchClient.syncPageUpdated', page.path);
 

+ 21 - 3
packages/app/src/server/service/search-delegator/private-legacy-pages.ts

@@ -4,13 +4,14 @@ import { PageModel, PageDocument } from '~/server/models/page';
 import { SearchDelegatorName } from '~/interfaces/named-query';
 import { IPage } from '~/interfaces/page';
 import {
-  SearchableData, SearchDelegator,
+  QueryTerms, MongoTermsKey,
+  SearchableData, SearchDelegator, UnavailableTermsKey, MongoQueryTerms,
 } from '../../interfaces/search';
 import { serializeUserSecurely } from '../../models/serializers/user-serializer';
 import { ISearchResult } from '~/interfaces/search';
 
 
-class PrivateLegacyPagesDelegator implements SearchDelegator<IPage> {
+class PrivateLegacyPagesDelegator implements SearchDelegator<IPage, MongoTermsKey, MongoQueryTerms> {
 
   name!: SearchDelegatorName.PRIVATE_LEGACY_PAGES
 
@@ -18,7 +19,7 @@ class PrivateLegacyPagesDelegator implements SearchDelegator<IPage> {
     this.name = SearchDelegatorName.PRIVATE_LEGACY_PAGES;
   }
 
-  async search(_data: SearchableData | null, user, userGroups, option): Promise<ISearchResult<IPage>> {
+  async search(data: SearchableData | null, user, userGroups, option): Promise<ISearchResult<IPage>> {
     const { offset, limit } = option;
 
     if (offset == null || limit == null) {
@@ -36,6 +37,10 @@ class PrivateLegacyPagesDelegator implements SearchDelegator<IPage> {
     await countQueryBuilder.addConditionAsMigratablePages(user);
     const findQueryBuilder = new PageQueryBuilder(Page.find());
     await findQueryBuilder.addConditionAsMigratablePages(user);
+    // if (false) {
+    //   countQueryBuilder.addConditionToListWithDescendants(prefix);
+    //   findQueryBuilder.addConditionToListWithDescendants(prefix);
+    // }
 
     const total = await countQueryBuilder.query.count();
 
@@ -59,6 +64,19 @@ class PrivateLegacyPagesDelegator implements SearchDelegator<IPage> {
     };
   }
 
+  validateTerms(terms: QueryTerms): UnavailableTermsKey<MongoTermsKey>[] {
+    return [];
+  }
+
+  excludeUnavailableTerms(terms: QueryTerms): MongoQueryTerms {
+    return {
+      match: [''],
+      not_match: [''],
+      prefix: [''],
+      not_prefix: [''],
+    };
+  }
+
 }
 
 export default PrivateLegacyPagesDelegator;

+ 16 - 23
packages/app/src/server/service/search.ts

@@ -15,6 +15,7 @@ import PrivateLegacyPagesDelegator from './search-delegator/private-legacy-pages
 
 import { PageModel } from '../models/page';
 import { serializeUserSecurely } from '../models/serializers/user-serializer';
+import { nqStringRegExp, parseNQString } from '~/utils/named-query';
 
 // eslint-disable-next-line no-unused-vars
 const logger = loggerFactory('growi:service:search');
@@ -47,7 +48,7 @@ class SearchService implements SearchQueryParser, SearchResolver {
 
   isErrorOccuredOnSearching: boolean | null
 
-  fullTextSearchDelegator: any & SearchDelegator
+  fullTextSearchDelegator: any & ElasticsearchDelegator
 
   nqDelegators: {[key in SearchDelegatorName]: SearchDelegator}
 
@@ -97,10 +98,10 @@ class SearchService implements SearchQueryParser, SearchResolver {
     logger.info('No elasticsearch URI is specified so that full text search is disabled.');
   }
 
-  generateNQDelegators(defaultDelegator: SearchDelegator): {[key in SearchDelegatorName]: SearchDelegator} {
+  generateNQDelegators(defaultDelegator: ElasticsearchDelegator): {[key in SearchDelegatorName]: SearchDelegator} {
     return {
       [SearchDelegatorName.DEFAULT]: defaultDelegator,
-      [SearchDelegatorName.PRIVATE_LEGACY_PAGES]: new PrivateLegacyPagesDelegator(),
+      [SearchDelegatorName.PRIVATE_LEGACY_PAGES]: new PrivateLegacyPagesDelegator() as SearchDelegator,
     };
   }
 
@@ -192,18 +193,15 @@ class SearchService implements SearchQueryParser, SearchResolver {
   }
 
   async parseSearchQuery(_queryString: string): Promise<ParsedQuery> {
-    const regexp = new RegExp(/^\[nq:.+\]$/g); // https://regex101.com/r/FzDUvT/1
-    const replaceRegexp = new RegExp(/\[nq:|\]/g);
-
     const queryString = normalizeQueryString(_queryString);
 
     // when Normal Query
-    if (!regexp.test(queryString)) {
+    if (!nqStringRegExp.test(queryString)) {
       return { queryString, terms: this.parseQueryString(queryString) };
     }
 
     // when Named Query
-    const name = queryString.replace(replaceRegexp, '');
+    const name = parseNQString(queryString);
     const nq = await NamedQuery.findOne({ name });
 
     // will delegate to full-text search
@@ -224,24 +222,19 @@ class SearchService implements SearchQueryParser, SearchResolver {
     return parsedQuery;
   }
 
-  async resolve(parsedQuery: ParsedQuery): Promise<[SearchDelegator, SearchableData | null]> {
-    const { queryString, terms, delegatorName } = parsedQuery;
-    if (delegatorName != null) {
-      const nqDelegator = this.nqDelegators[delegatorName];
-      if (nqDelegator != null) {
-        return [nqDelegator, null];
-      }
-    }
+  async resolve(parsedQuery: ParsedQuery): Promise<[SearchDelegator, SearchableData]> {
+    const { queryString, terms, delegatorName = SearchDelegatorName.DEFAULT } = parsedQuery;
+    const nqDeledator = this.nqDelegators[delegatorName];
 
     const data = {
       queryString,
-      terms: terms as QueryTerms,
+      terms,
     };
-    return [this.nqDelegators[SearchDelegatorName.DEFAULT], data];
+    return [nqDeledator, data];
   }
 
-  async searchKeyword(keyword: string, user, userGroups, searchOpts): Promise<[ISearchResult<unknown>, string]> {
-    let parsedQuery;
+  async searchKeyword(keyword: string, user, userGroups, searchOpts): Promise<[ISearchResult<unknown>, string | null]> {
+    let parsedQuery: ParsedQuery;
     // parse
     try {
       parsedQuery = await this.parseSearchQuery(keyword);
@@ -251,8 +244,8 @@ class SearchService implements SearchQueryParser, SearchResolver {
       throw err;
     }
 
-    let delegator;
-    let data;
+    let delegator: SearchDelegator;
+    let data: SearchableData;
     // resolve
     try {
       [delegator, data] = await this.resolve(parsedQuery);
@@ -262,7 +255,7 @@ class SearchService implements SearchQueryParser, SearchResolver {
       throw err;
     }
 
-    return [await delegator.search(data, user, userGroups, searchOpts), delegator.name];
+    return [await delegator.search(data, user, userGroups, searchOpts), delegator.name ?? null];
   }
 
   parseQueryString(queryString: string): QueryTerms {

+ 12 - 0
packages/app/src/utils/named-query.ts

@@ -0,0 +1,12 @@
+export const nqStringRegExp = new RegExp(/^\[nq:.+\]$/g); // https://regex101.com/r/FzDUvT/1
+export const nqReplaceRegExp = new RegExp(/\[nq:|\]/g);
+
+export const isNQ = (queryString: string): boolean => {
+  return nqStringRegExp.test(queryString);
+};
+
+export const parseNQString = (queryString: string): string => {
+  if (!isNQ(queryString)) throw Error('This queryString does not have the named query format.');
+
+  return queryString.replace(nqReplaceRegExp, '');
+};