mirror of
https://github.com/bpatrik/pigallery2.git
synced 2025-01-02 03:37:54 +02:00
Improving SOME_OF search to support expressions with more parameters
This commit is contained in:
parent
404b82e12b
commit
b126022454
@ -47,6 +47,7 @@ export class AlbumManager implements IAlbumManager {
|
||||
public async getAlbums(): Promise<AlbumBaseDTO[]> {
|
||||
const connection = await SQLConnection.getConnection();
|
||||
const albums = await connection.getRepository(AlbumBaseEntity).find();
|
||||
|
||||
for (const a of albums) {
|
||||
await AlbumManager.fillPreviewToAlbum(a);
|
||||
}
|
||||
|
@ -27,12 +27,12 @@ import {
|
||||
} from '../../../../common/entities/SearchQueryDTO';
|
||||
import {GalleryManager} from './GalleryManager';
|
||||
import {ObjectManagers} from '../../ObjectManagers';
|
||||
import {Utils} from '../../../../common/Utils';
|
||||
import {PhotoDTO} from '../../../../common/entities/PhotoDTO';
|
||||
import {DatabaseType} from '../../../../common/config/private/PrivateConfig';
|
||||
import {ISQLGalleryManager} from './IGalleryManager';
|
||||
import {ISQLSearchManager} from './ISearchManager';
|
||||
import {MediaDTO} from '../../../../common/entities/MediaDTO';
|
||||
import {Utils} from '../../../../common/Utils';
|
||||
|
||||
export class SearchManager implements ISQLSearchManager {
|
||||
|
||||
@ -139,8 +139,7 @@ export class SearchManager implements ISQLSearchManager {
|
||||
}
|
||||
|
||||
async search(queryIN: SearchQueryDTO): Promise<SearchResultDTO> {
|
||||
let query = this.flattenSameOfQueries(queryIN);
|
||||
query = await this.getGPSData(query);
|
||||
const query = await this.prepareQuery(queryIN);
|
||||
const connection = await SQLConnection.getConnection();
|
||||
|
||||
const result: SearchResultDTO = {
|
||||
@ -225,8 +224,7 @@ export class SearchManager implements ISQLSearchManager {
|
||||
}
|
||||
|
||||
public async getPreview(queryIN: SearchQueryDTO): Promise<MediaDTO> {
|
||||
let query = this.flattenSameOfQueries(queryIN);
|
||||
query = await this.getGPSData(query);
|
||||
const query = await this.prepareQuery(queryIN);
|
||||
const connection = await SQLConnection.getConnection();
|
||||
|
||||
return await connection
|
||||
@ -239,6 +237,28 @@ export class SearchManager implements ISQLSearchManager {
|
||||
.getOne();
|
||||
}
|
||||
|
||||
private async prepareQuery(queryIN: SearchQueryDTO): Promise<SearchQueryDTO> {
|
||||
let query: SearchQueryDTO = this.assignQueryIDs(Utils.clone(queryIN)); // assign local ids before flattening SOME_OF queries
|
||||
query = this.flattenSameOfQueries(query);
|
||||
query = await this.getGPSData(query);
|
||||
return query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigning IDs to search queries. It is a help / simplification to typeorm,
|
||||
* so less parameters are needed to pass down to SQL.
|
||||
* Witch SOME_OF query the number of WHERE constrains have O(N!) complexity
|
||||
*/
|
||||
private assignQueryIDs(queryIN: SearchQueryDTO, id = {value: 1}): SearchQueryDTO {
|
||||
if ((queryIN as SearchListQuery).list) {
|
||||
(queryIN as SearchListQuery).list.forEach(q => this.assignQueryIDs(q, id));
|
||||
return queryIN;
|
||||
}
|
||||
(queryIN as SearchQueryDTOWithID).queryId = id.value;
|
||||
id.value++;
|
||||
return queryIN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns only those part of a query tree that only contains directory related search queries
|
||||
*/
|
||||
@ -297,16 +317,17 @@ export class SearchManager implements ISQLSearchManager {
|
||||
* @param directoryOnly Only builds directory related queries
|
||||
* @private
|
||||
*/
|
||||
private buildWhereQuery(query: SearchQueryDTO, directoryOnly = false, paramCounter = {value: 0}): Brackets {
|
||||
private buildWhereQuery(query: SearchQueryDTO, directoryOnly = false): Brackets {
|
||||
const queryId = (query as SearchQueryDTOWithID).queryId;
|
||||
switch (query.type) {
|
||||
case SearchQueryTypes.AND:
|
||||
return new Brackets((q): any => {
|
||||
(query as ANDSearchQuery).list.forEach((sq): any => q.andWhere(this.buildWhereQuery(sq, directoryOnly, paramCounter)));
|
||||
(query as ANDSearchQuery).list.forEach((sq): any => q.andWhere(this.buildWhereQuery(sq, directoryOnly)));
|
||||
return q;
|
||||
});
|
||||
case SearchQueryTypes.OR:
|
||||
return new Brackets((q): any => {
|
||||
(query as ANDSearchQuery).list.forEach((sq): any => q.orWhere(this.buildWhereQuery(sq, directoryOnly, paramCounter)));
|
||||
(query as ANDSearchQuery).list.forEach((sq): any => q.orWhere(this.buildWhereQuery(sq, directoryOnly)));
|
||||
return q;
|
||||
});
|
||||
|
||||
@ -340,21 +361,20 @@ export class SearchManager implements ISQLSearchManager {
|
||||
|
||||
return new Brackets((q): any => {
|
||||
const textParam: any = {};
|
||||
paramCounter.value++;
|
||||
textParam['maxLat' + paramCounter.value] = maxLat;
|
||||
textParam['minLat' + paramCounter.value] = minLat;
|
||||
textParam['maxLon' + paramCounter.value] = maxLon;
|
||||
textParam['minLon' + paramCounter.value] = minLon;
|
||||
textParam['maxLat' + queryId] = maxLat;
|
||||
textParam['minLat' + queryId] = minLat;
|
||||
textParam['maxLon' + queryId] = maxLon;
|
||||
textParam['minLon' + queryId] = minLon;
|
||||
if (!(query as DistanceSearch).negate) {
|
||||
q.where(`media.metadata.positionData.GPSData.latitude < :maxLat${paramCounter.value}`, textParam);
|
||||
q.andWhere(`media.metadata.positionData.GPSData.latitude > :minLat${paramCounter.value}`, textParam);
|
||||
q.andWhere(`media.metadata.positionData.GPSData.longitude < :maxLon${paramCounter.value}`, textParam);
|
||||
q.andWhere(`media.metadata.positionData.GPSData.longitude > :minLon${paramCounter.value}`, textParam);
|
||||
q.where(`media.metadata.positionData.GPSData.latitude < :maxLat${queryId}`, textParam);
|
||||
q.andWhere(`media.metadata.positionData.GPSData.latitude > :minLat${queryId}`, textParam);
|
||||
q.andWhere(`media.metadata.positionData.GPSData.longitude < :maxLon${queryId}`, textParam);
|
||||
q.andWhere(`media.metadata.positionData.GPSData.longitude > :minLon${queryId}`, textParam);
|
||||
} else {
|
||||
q.where(`media.metadata.positionData.GPSData.latitude > :maxLat${paramCounter.value}`, textParam);
|
||||
q.orWhere(`media.metadata.positionData.GPSData.latitude < :minLat${paramCounter.value}`, textParam);
|
||||
q.orWhere(`media.metadata.positionData.GPSData.longitude > :maxLon${paramCounter.value}`, textParam);
|
||||
q.orWhere(`media.metadata.positionData.GPSData.longitude < :minLon${paramCounter.value}`, textParam);
|
||||
q.where(`media.metadata.positionData.GPSData.latitude > :maxLat${queryId}`, textParam);
|
||||
q.orWhere(`media.metadata.positionData.GPSData.latitude < :minLat${queryId}`, textParam);
|
||||
q.orWhere(`media.metadata.positionData.GPSData.longitude > :maxLon${queryId}`, textParam);
|
||||
q.orWhere(`media.metadata.positionData.GPSData.longitude < :minLon${queryId}`, textParam);
|
||||
}
|
||||
return q;
|
||||
});
|
||||
@ -370,11 +390,9 @@ export class SearchManager implements ISQLSearchManager {
|
||||
const relation = (query as TextSearch).negate ? '<' : '>=';
|
||||
|
||||
const textParam: any = {};
|
||||
textParam['from' + paramCounter.value] = (query as FromDateSearch).value;
|
||||
q.where(`media.metadata.creationDate ${relation} :from${paramCounter.value}`, textParam);
|
||||
textParam['from' + queryId] = (query as FromDateSearch).value;
|
||||
q.where(`media.metadata.creationDate ${relation} :from${queryId}`, textParam);
|
||||
|
||||
|
||||
paramCounter.value++;
|
||||
return q;
|
||||
});
|
||||
|
||||
@ -389,10 +407,9 @@ export class SearchManager implements ISQLSearchManager {
|
||||
const relation = (query as TextSearch).negate ? '>' : '<=';
|
||||
|
||||
const textParam: any = {};
|
||||
textParam['to' + paramCounter.value] = (query as ToDateSearch).value;
|
||||
q.where(`media.metadata.creationDate ${relation} :to${paramCounter.value}`, textParam);
|
||||
textParam['to' + queryId] = (query as ToDateSearch).value;
|
||||
q.where(`media.metadata.creationDate ${relation} :to${queryId}`, textParam);
|
||||
|
||||
paramCounter.value++;
|
||||
return q;
|
||||
});
|
||||
|
||||
@ -408,10 +425,9 @@ export class SearchManager implements ISQLSearchManager {
|
||||
const relation = (query as TextSearch).negate ? '<' : '>=';
|
||||
|
||||
const textParam: any = {};
|
||||
textParam['min' + paramCounter.value] = (query as MinRatingSearch).value;
|
||||
q.where(`media.metadata.rating ${relation} :min${paramCounter.value}`, textParam);
|
||||
textParam['min' + queryId] = (query as MinRatingSearch).value;
|
||||
q.where(`media.metadata.rating ${relation} :min${queryId}`, textParam);
|
||||
|
||||
paramCounter.value++;
|
||||
return q;
|
||||
});
|
||||
case SearchQueryTypes.max_rating:
|
||||
@ -427,10 +443,9 @@ export class SearchManager implements ISQLSearchManager {
|
||||
|
||||
if (typeof (query as MaxRatingSearch).value !== 'undefined') {
|
||||
const textParam: any = {};
|
||||
textParam['max' + paramCounter.value] = (query as MaxRatingSearch).value;
|
||||
q.where(`media.metadata.rating ${relation} :max${paramCounter.value}`, textParam);
|
||||
textParam['max' + queryId] = (query as MaxRatingSearch).value;
|
||||
q.where(`media.metadata.rating ${relation} :max${queryId}`, textParam);
|
||||
}
|
||||
paramCounter.value++;
|
||||
return q;
|
||||
});
|
||||
|
||||
@ -446,11 +461,10 @@ export class SearchManager implements ISQLSearchManager {
|
||||
const relation = (query as TextSearch).negate ? '<' : '>=';
|
||||
|
||||
const textParam: any = {};
|
||||
textParam['min' + paramCounter.value] = (query as MinResolutionSearch).value * 1000 * 1000;
|
||||
q.where(`media.metadata.size.width * media.metadata.size.height ${relation} :min${paramCounter.value}`, textParam);
|
||||
textParam['min' + queryId] = (query as MinResolutionSearch).value * 1000 * 1000;
|
||||
q.where(`media.metadata.size.width * media.metadata.size.height ${relation} :min${queryId}`, textParam);
|
||||
|
||||
|
||||
paramCounter.value++;
|
||||
return q;
|
||||
});
|
||||
|
||||
@ -466,10 +480,9 @@ export class SearchManager implements ISQLSearchManager {
|
||||
const relation = (query as TextSearch).negate ? '>' : '<=';
|
||||
|
||||
const textParam: any = {};
|
||||
textParam['max' + paramCounter.value] = (query as MaxResolutionSearch).value * 1000 * 1000;
|
||||
q.where(`media.metadata.size.width * media.metadata.size.height ${relation} :max${paramCounter.value}`, textParam);
|
||||
textParam['max' + queryId] = (query as MaxResolutionSearch).value * 1000 * 1000;
|
||||
q.where(`media.metadata.size.width * media.metadata.size.height ${relation} :max${queryId}`, textParam);
|
||||
|
||||
paramCounter.value++;
|
||||
return q;
|
||||
});
|
||||
|
||||
@ -483,7 +496,6 @@ export class SearchManager implements ISQLSearchManager {
|
||||
} else {
|
||||
q.where('media.metadata.size.width <= media.metadata.size.height');
|
||||
}
|
||||
paramCounter.value++;
|
||||
return q;
|
||||
});
|
||||
|
||||
@ -514,26 +526,25 @@ export class SearchManager implements ISQLSearchManager {
|
||||
const whereFNRev = (query as TextSearch).negate ? 'orWhere' : 'andWhere';
|
||||
|
||||
const textParam: any = {};
|
||||
paramCounter.value++;
|
||||
textParam['text' + paramCounter.value] = createMatchString((query as TextSearch).text);
|
||||
textParam['text' + queryId] = createMatchString((query as TextSearch).text);
|
||||
|
||||
if (query.type === SearchQueryTypes.any_text ||
|
||||
query.type === SearchQueryTypes.directory) {
|
||||
const dirPathStr = ((query as TextSearch).text).replace(new RegExp('\\\\', 'g'), '/');
|
||||
|
||||
|
||||
textParam['fullPath' + paramCounter.value] = createMatchString(dirPathStr);
|
||||
q[whereFN](`directory.path ${LIKE} :fullPath${paramCounter.value} COLLATE utf8_general_ci`,
|
||||
textParam['fullPath' + queryId] = createMatchString(dirPathStr);
|
||||
q[whereFN](`directory.path ${LIKE} :fullPath${queryId} COLLATE utf8_general_ci`,
|
||||
textParam);
|
||||
|
||||
const directoryPath = GalleryManager.parseRelativeDirePath(dirPathStr);
|
||||
q[whereFN](new Brackets((dq): any => {
|
||||
textParam['dirName' + paramCounter.value] = createMatchString(directoryPath.name);
|
||||
dq[whereFNRev](`directory.name ${LIKE} :dirName${paramCounter.value} COLLATE utf8_general_ci`,
|
||||
textParam['dirName' + queryId] = createMatchString(directoryPath.name);
|
||||
dq[whereFNRev](`directory.name ${LIKE} :dirName${queryId} COLLATE utf8_general_ci`,
|
||||
textParam);
|
||||
if (dirPathStr.includes('/')) {
|
||||
textParam['parentName' + paramCounter.value] = createMatchString(directoryPath.parent);
|
||||
dq[whereFNRev](`directory.path ${LIKE} :parentName${paramCounter.value} COLLATE utf8_general_ci`,
|
||||
textParam['parentName' + queryId] = createMatchString(directoryPath.parent);
|
||||
dq[whereFNRev](`directory.path ${LIKE} :parentName${queryId} COLLATE utf8_general_ci`,
|
||||
textParam);
|
||||
}
|
||||
return dq;
|
||||
@ -541,21 +552,21 @@ export class SearchManager implements ISQLSearchManager {
|
||||
}
|
||||
|
||||
if ((query.type === SearchQueryTypes.any_text && !directoryOnly) || query.type === SearchQueryTypes.file_name) {
|
||||
q[whereFN](`media.name ${LIKE} :text${paramCounter.value} COLLATE utf8_general_ci`,
|
||||
q[whereFN](`media.name ${LIKE} :text${queryId} COLLATE utf8_general_ci`,
|
||||
textParam);
|
||||
}
|
||||
|
||||
if ((query.type === SearchQueryTypes.any_text && !directoryOnly) || query.type === SearchQueryTypes.caption) {
|
||||
q[whereFN](`media.metadata.caption ${LIKE} :text${paramCounter.value} COLLATE utf8_general_ci`,
|
||||
q[whereFN](`media.metadata.caption ${LIKE} :text${queryId} COLLATE utf8_general_ci`,
|
||||
textParam);
|
||||
}
|
||||
|
||||
if ((query.type === SearchQueryTypes.any_text && !directoryOnly) || query.type === SearchQueryTypes.position) {
|
||||
q[whereFN](`media.metadata.positionData.country ${LIKE} :text${paramCounter.value} COLLATE utf8_general_ci`,
|
||||
q[whereFN](`media.metadata.positionData.country ${LIKE} :text${queryId} COLLATE utf8_general_ci`,
|
||||
textParam)
|
||||
[whereFN](`media.metadata.positionData.state ${LIKE} :text${paramCounter.value} COLLATE utf8_general_ci`,
|
||||
[whereFN](`media.metadata.positionData.state ${LIKE} :text${queryId} COLLATE utf8_general_ci`,
|
||||
textParam)
|
||||
[whereFN](`media.metadata.positionData.city ${LIKE} :text${paramCounter.value} COLLATE utf8_general_ci`,
|
||||
[whereFN](`media.metadata.positionData.city ${LIKE} :text${queryId} COLLATE utf8_general_ci`,
|
||||
textParam);
|
||||
}
|
||||
|
||||
@ -563,22 +574,22 @@ export class SearchManager implements ISQLSearchManager {
|
||||
const matchArrayField = (fieldName: string): void => {
|
||||
q[whereFN](new Brackets((qbr): void => {
|
||||
if ((query as TextSearch).matchType !== TextSearchQueryMatchTypes.exact_match) {
|
||||
qbr[whereFN](`${fieldName} ${LIKE} :text${paramCounter.value} COLLATE utf8_general_ci`,
|
||||
qbr[whereFN](`${fieldName} ${LIKE} :text${queryId} COLLATE utf8_general_ci`,
|
||||
textParam);
|
||||
} else {
|
||||
qbr[whereFN](new Brackets((qb): void => {
|
||||
textParam['CtextC' + paramCounter.value] = `%,${(query as TextSearch).text},%`;
|
||||
textParam['Ctext' + paramCounter.value] = `%,${(query as TextSearch).text}`;
|
||||
textParam['textC' + paramCounter.value] = `${(query as TextSearch).text},%`;
|
||||
textParam['text_exact' + paramCounter.value] = `${(query as TextSearch).text}`;
|
||||
textParam['CtextC' + queryId] = `%,${(query as TextSearch).text},%`;
|
||||
textParam['Ctext' + queryId] = `%,${(query as TextSearch).text}`;
|
||||
textParam['textC' + queryId] = `${(query as TextSearch).text},%`;
|
||||
textParam['text_exact' + queryId] = `${(query as TextSearch).text}`;
|
||||
|
||||
qb[whereFN](`${fieldName} ${LIKE} :CtextC${paramCounter.value} COLLATE utf8_general_ci`,
|
||||
qb[whereFN](`${fieldName} ${LIKE} :CtextC${queryId} COLLATE utf8_general_ci`,
|
||||
textParam);
|
||||
qb[whereFN](`${fieldName} ${LIKE} :Ctext${paramCounter.value} COLLATE utf8_general_ci`,
|
||||
qb[whereFN](`${fieldName} ${LIKE} :Ctext${queryId} COLLATE utf8_general_ci`,
|
||||
textParam);
|
||||
qb[whereFN](`${fieldName} ${LIKE} :textC${paramCounter.value} COLLATE utf8_general_ci`,
|
||||
qb[whereFN](`${fieldName} ${LIKE} :textC${queryId} COLLATE utf8_general_ci`,
|
||||
textParam);
|
||||
qb[whereFN](`${fieldName} ${LIKE} :text_exact${paramCounter.value} COLLATE utf8_general_ci`,
|
||||
qb[whereFN](`${fieldName} ${LIKE} :text_exact${queryId} COLLATE utf8_general_ci`,
|
||||
textParam);
|
||||
}));
|
||||
}
|
||||
@ -626,14 +637,41 @@ export class SearchManager implements ISQLSearchManager {
|
||||
} as ANDSearchQuery);
|
||||
}
|
||||
|
||||
const combinations: SearchQueryDTO[][] = Utils.getAnyX(someOfQ.min, (query as SearchListQuery).list);
|
||||
const getAllCombinations = (num: number, arr: SearchQueryDTO[], start = 0): SearchQueryDTO[] => {
|
||||
if (num <= 0 || num > arr.length || start >= arr.length) {
|
||||
return [];
|
||||
}
|
||||
if (num <= 1) {
|
||||
return arr.slice(start);
|
||||
}
|
||||
if (num === arr.length - start) {
|
||||
return arr.slice(start);
|
||||
}
|
||||
const ret: ANDSearchQuery[] = [];
|
||||
for (let i = start; i < arr.length - num + 1; ++i) {
|
||||
const subRes = getAllCombinations(num - 1, arr, i + 1);
|
||||
const and: ANDSearchQuery = {
|
||||
type: SearchQueryTypes.AND,
|
||||
list: [
|
||||
arr[i],
|
||||
subRes.length === 1 ? subRes[0] : (
|
||||
{
|
||||
type: SearchQueryTypes.OR,
|
||||
list: subRes
|
||||
} as ORSearchQuery)
|
||||
]
|
||||
};
|
||||
|
||||
ret.push(and);
|
||||
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
|
||||
|
||||
return this.flattenSameOfQueries({
|
||||
type: SearchQueryTypes.OR,
|
||||
list: combinations.map((c): ANDSearchQuery => ({
|
||||
type: SearchQueryTypes.AND, list: c
|
||||
} as ANDSearchQuery))
|
||||
list: getAllCombinations(someOfQ.min, (query as SearchListQuery).list)
|
||||
} as ORSearchQuery);
|
||||
|
||||
}
|
||||
@ -650,3 +688,7 @@ export class SearchManager implements ISQLSearchManager {
|
||||
|
||||
|
||||
}
|
||||
|
||||
export interface SearchQueryDTOWithID extends SearchQueryDTO {
|
||||
queryId: number;
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ export class MediaMetadataEntity implements MediaMetadata {
|
||||
* you do not want to see 2AM next to a photo that was taken during lunch
|
||||
*/
|
||||
@Column('bigint', {
|
||||
unsigned: true, transformer: {
|
||||
transformer: {
|
||||
from: v => parseInt(v, 10),
|
||||
to: v => v
|
||||
}
|
||||
|
@ -1 +1,4 @@
|
||||
export const DataStructureVersion = 23;
|
||||
/**
|
||||
* This version indicates that the SQL sql/entities/*Entity.ts files got changed and the db needs to be recreated
|
||||
*/
|
||||
export const DataStructureVersion = 24;
|
||||
|
@ -49,7 +49,7 @@ export class Utils {
|
||||
static shallowClone<T>(object: T): T {
|
||||
const c: any = {};
|
||||
for (const e of Object.entries(object)) {
|
||||
c[e[0]] = [1];
|
||||
c[e[0]] = e[1];
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
@ -736,7 +736,7 @@ describe('SearchManager', (sqlHelper: DBTestHelper) => {
|
||||
|
||||
query = ({
|
||||
text: '/wars dir/Return of the Jedi',
|
||||
// matchType: TextSearchQueryMatchTypes.like,
|
||||
// matchType: TextSearchQueryMatchTypes.like,
|
||||
type: SearchQueryTypes.directory
|
||||
} as TextSearch);
|
||||
|
||||
@ -1114,10 +1114,32 @@ describe('SearchManager', (sqlHelper: DBTestHelper) => {
|
||||
|
||||
});
|
||||
|
||||
(it('should execute complex SOME_OF querry', async () => {
|
||||
const sm = new SearchManager();
|
||||
|
||||
const query: SomeOfSearchQuery = {
|
||||
type: SearchQueryTypes.SOME_OF,
|
||||
min: 5,
|
||||
//
|
||||
list: 'abcdefghijklmnopqrstu'.split('').map(t => ({
|
||||
type: SearchQueryTypes.file_name,
|
||||
text: t
|
||||
} as TextSearch))
|
||||
};
|
||||
expect(removeDir(await sm.search(query)))
|
||||
.to.deep.equalInAnyOrder(removeDir({
|
||||
searchQuery: query,
|
||||
directories: [],
|
||||
media: [v],
|
||||
metaFile: [],
|
||||
resultOverflow: false
|
||||
} as SearchResultDTO));
|
||||
}) as any).timeout(40000);
|
||||
|
||||
it('search result should return directory', async () => {
|
||||
const sm = new SearchManager();
|
||||
|
||||
let query = {
|
||||
const query = {
|
||||
text: subDir.name,
|
||||
type: SearchQueryTypes.any_text
|
||||
} as TextSearch;
|
||||
@ -1129,8 +1151,6 @@ describe('SearchManager', (sqlHelper: DBTestHelper) => {
|
||||
metaFile: [],
|
||||
resultOverflow: false
|
||||
} as SearchResultDTO));
|
||||
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user