From e643ee2ed11d364dd27b1e97a3d792c35cb64df6 Mon Sep 17 00:00:00 2001 From: "Patrik J. Braun" Date: Sat, 16 Jan 2021 23:37:14 +0100 Subject: [PATCH] improving advanced search --- .../model/database/sql/SearchManager.ts | 138 ++++++++++--- src/common/Utils.ts | 20 ++ src/common/entities/SearchQueryDTO.ts | 81 ++++++-- test/backend/unit/model/sql/SearchManager.ts | 188 +++++++++++++++++- test/backend/unit/model/sql/TestHelper.ts | 11 + 5 files changed, 375 insertions(+), 63 deletions(-) diff --git a/src/backend/model/database/sql/SearchManager.ts b/src/backend/model/database/sql/SearchManager.ts index c1d87afc..077f2a03 100644 --- a/src/backend/model/database/sql/SearchManager.ts +++ b/src/backend/model/database/sql/SearchManager.ts @@ -15,17 +15,19 @@ import { DateSearch, DistanceSearch, OrientationSearch, - OrientationSearchTypes, ORSearchQuery, RatingSearch, ResolutionSearch, + SearchListQuery, SearchQueryDTO, SearchQueryTypes, + SomeOfSearchQuery, TextSearch, TextSearchQueryTypes } from '../../../../common/entities/SearchQueryDTO'; import {GalleryManager} from './GalleryManager'; import {ObjectManagers} from '../../ObjectManagers'; +import {Utils} from '../../../../common/Utils'; export class SearchManager implements ISearchManager { @@ -145,6 +147,7 @@ export class SearchManager implements ISearchManager { async aSearch(query: SearchQueryDTO) { query = this.flattenSameOfQueries(query); + console.log(JSON.stringify(query, null, 4)); query = await this.getGPSData(query); const connection = await SQLConnection.getConnection(); @@ -367,10 +370,17 @@ export class SearchManager implements ISearchManager { textParam['minLat' + paramCounter.value] = minLat; textParam['maxLon' + paramCounter.value] = maxLon; textParam['minLon' + paramCounter.value] = minLon; - 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); + if (!(query).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); + } 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); + } return q; }); @@ -379,17 +389,22 @@ export class SearchManager implements ISearchManager { if (typeof (query).before === 'undefined' && typeof (query).after === 'undefined') { throw new Error('Invalid search query: Date Query should contain before or after value'); } - if (typeof (query).before !== 'undefined') { - const textParam: any = {}; - textParam['before' + paramCounter.value] = (query).before; - q.where(`media.metadata.creationDate <= :before${paramCounter.value}`, textParam); - } + const whereFN = (query).negate ? 'orWhere' : 'andWhere'; + const relation = (query).negate ? '<' : '>='; + const relationRev = (query).negate ? '>' : '<='; if (typeof (query).after !== 'undefined') { const textParam: any = {}; textParam['after' + paramCounter.value] = (query).after; - q.andWhere(`media.metadata.creationDate >= :after${paramCounter.value}`, textParam); + q.where(`media.metadata.creationDate ${relation} :after${paramCounter.value}`, textParam); } + + if (typeof (query).before !== 'undefined') { + const textParam: any = {}; + textParam['before' + paramCounter.value] = (query).before; + q[whereFN](`media.metadata.creationDate ${relationRev} :before${paramCounter.value}`, textParam); + } + paramCounter.value++; return q; }); @@ -399,16 +414,20 @@ export class SearchManager implements ISearchManager { if (typeof (query).min === 'undefined' && typeof (query).max === 'undefined') { throw new Error('Invalid search query: Rating Query should contain min or max value'); } + + const whereFN = (query).negate ? 'orWhere' : 'andWhere'; + const relation = (query).negate ? '<' : '>='; + const relationRev = (query).negate ? '>' : '<='; if (typeof (query).min !== 'undefined') { const textParam: any = {}; textParam['min' + paramCounter.value] = (query).min; - q.where(`media.metadata.rating >= :min${paramCounter.value}`, textParam); + q.where(`media.metadata.rating ${relation} :min${paramCounter.value}`, textParam); } if (typeof (query).max !== 'undefined') { const textParam: any = {}; textParam['max' + paramCounter.value] = (query).max; - q.andWhere(`media.metadata.rating <= :max${paramCounter.value}`, textParam); + q[whereFN](`media.metadata.rating ${relationRev} :max${paramCounter.value}`, textParam); } paramCounter.value++; return q; @@ -419,16 +438,20 @@ export class SearchManager implements ISearchManager { if (typeof (query).min === 'undefined' && typeof (query).max === 'undefined') { throw new Error('Invalid search query: Rating Query should contain min or max value'); } + + const whereFN = (query).negate ? 'orWhere' : 'andWhere'; + const relation = (query).negate ? '<' : '>='; + const relationRev = (query).negate ? '>' : '<='; if (typeof (query).min !== 'undefined') { const textParam: any = {}; textParam['min' + paramCounter.value] = (query).min * 1000 * 1000; - q.where(`media.metadata.size.width * media.metadata.size.height >= :min${paramCounter.value}`, textParam); + q.where(`media.metadata.size.width * media.metadata.size.height ${relation} :min${paramCounter.value}`, textParam); } if (typeof (query).max !== 'undefined') { const textParam: any = {}; textParam['max' + paramCounter.value] = (query).max * 1000 * 1000; - q.andWhere(`media.metadata.size.width * media.metadata.size.height <= :max${paramCounter.value}`, textParam); + q[whereFN](`media.metadata.size.width * media.metadata.size.height ${relationRev} :max${paramCounter.value}`, textParam); } paramCounter.value++; return q; @@ -436,11 +459,10 @@ export class SearchManager implements ISearchManager { case SearchQueryTypes.orientation: return new Brackets(q => { - if ((query).orientation === OrientationSearchTypes.landscape) { + if ((query).landscape) { q.where('media.metadata.size.width >= media.metadata.size.height'); - } - if ((query).orientation === OrientationSearchTypes.portrait) { - q.andWhere('media.metadata.size.width <= media.metadata.size.height'); + } else { + q.where('media.metadata.size.width <= media.metadata.size.height'); } paramCounter.value++; return q; @@ -458,6 +480,11 @@ export class SearchManager implements ISearchManager { return (query).matchType === TextSearchQueryTypes.exact_match ? str : `%${str}%`; }; + const LIKE = (query).negate ? 'NOT LIKE' : 'LIKE'; + // if the expression is negated, we use AND instead of OR as nowhere should that match + const whereFN = (query).negate ? 'andWhere' : 'orWhere'; + const whereFNRev = (query).negate ? 'orWhere' : 'andWhere'; + const textParam: any = {}; paramCounter.value++; textParam['text' + paramCounter.value] = createMatchString((query).text); @@ -468,17 +495,17 @@ export class SearchManager implements ISearchManager { textParam['fullPath' + paramCounter.value] = createMatchString(dirPathStr); - q.orWhere(`directory.path LIKE :fullPath${paramCounter.value} COLLATE utf8_general_ci`, + q[whereFN](`directory.path ${LIKE} :fullPath${paramCounter.value} COLLATE utf8_general_ci`, textParam); const directoryPath = GalleryManager.parseRelativeDirePath(dirPathStr); - q.orWhere(new Brackets(dq => { + q[whereFN](new Brackets(dq => { textParam['dirName' + paramCounter.value] = createMatchString(directoryPath.name); - dq.where(`directory.name LIKE :dirName${paramCounter.value} COLLATE utf8_general_ci`, + dq[whereFNRev](`directory.name ${LIKE} :dirName${paramCounter.value} COLLATE utf8_general_ci`, textParam); if (dirPathStr.includes('/')) { textParam['parentName' + paramCounter.value] = createMatchString(directoryPath.parent); - dq.andWhere(`directory.path LIKE :parentName${paramCounter.value} COLLATE utf8_general_ci`, + dq[whereFNRev](`directory.path ${LIKE} :parentName${paramCounter.value} COLLATE utf8_general_ci`, textParam); } return dq; @@ -486,29 +513,39 @@ export class SearchManager implements ISearchManager { } if (query.type === SearchQueryTypes.any_text || query.type === SearchQueryTypes.file_name) { - q.orWhere(`media.name LIKE :text${paramCounter.value} COLLATE utf8_general_ci`, + q[whereFN](`media.name ${LIKE} :text${paramCounter.value} COLLATE utf8_general_ci`, textParam); } if (query.type === SearchQueryTypes.any_text || query.type === SearchQueryTypes.caption) { - q.orWhere(`media.metadata.caption LIKE :text${paramCounter.value} COLLATE utf8_general_ci`, + q[whereFN](`media.metadata.caption ${LIKE} :text${paramCounter.value} COLLATE utf8_general_ci`, textParam); } if (query.type === SearchQueryTypes.any_text || query.type === SearchQueryTypes.person) { - q.orWhere(`person.name LIKE :text${paramCounter.value} COLLATE utf8_general_ci`, - textParam); + if (!(query).negate) { + q[whereFN](`person.name ${LIKE} :text${paramCounter.value} COLLATE utf8_general_ci`, + textParam); + } else { + // because of the Left JOIN on the faces, we also need to check for NULL + q[whereFN](new Brackets(dq => { + dq.where(`person.name ${LIKE} :text${paramCounter.value} COLLATE utf8_general_ci`, + textParam); + dq.orWhere(`person.name is NULL`); + return dq; + })); + } } if (query.type === SearchQueryTypes.any_text || query.type === SearchQueryTypes.position) { - q.orWhere(`media.metadata.positionData.country LIKE :text${paramCounter.value} COLLATE utf8_general_ci`, + q[whereFN](`media.metadata.positionData.country ${LIKE} :text${paramCounter.value} COLLATE utf8_general_ci`, textParam) - .orWhere(`media.metadata.positionData.state LIKE :text${paramCounter.value} COLLATE utf8_general_ci`, - textParam) - .orWhere(`media.metadata.positionData.city LIKE :text${paramCounter.value} COLLATE utf8_general_ci`, - textParam); + [whereFN](`media.metadata.positionData.state ${LIKE} :text${paramCounter.value} COLLATE utf8_general_ci`, + textParam) + [whereFN](`media.metadata.positionData.city ${LIKE} :text${paramCounter.value} COLLATE utf8_general_ci`, + textParam); } if (query.type === SearchQueryTypes.any_text || query.type === SearchQueryTypes.keyword) { - q.orWhere(`media.metadata.keywords LIKE :text${paramCounter.value} COLLATE utf8_general_ci`, + q[whereFN](`media.metadata.keywords ${LIKE} :text${paramCounter.value} COLLATE utf8_general_ci`, textParam); } return q; @@ -516,6 +553,41 @@ export class SearchManager implements ISearchManager { } private flattenSameOfQueries(query: SearchQueryDTO): SearchQueryDTO { + switch (query.type) { + case SearchQueryTypes.AND: + case SearchQueryTypes.OR: + return { + type: query.type, + list: (query).list.map(q => this.flattenSameOfQueries(q)) + }; + case SearchQueryTypes.SOME_OF: + const someOfQ = query; + someOfQ.min = someOfQ.min || 1; + + if (someOfQ.min === 1) { + return this.flattenSameOfQueries({ + type: SearchQueryTypes.OR, + list: (someOfQ).list + }); + } + + if (someOfQ.min === (query).list.length) { + return this.flattenSameOfQueries({ + type: SearchQueryTypes.AND, + list: (someOfQ).list + }); + } + + const combinations: SearchQueryDTO[][] = Utils.getAnyX(someOfQ.min, (query).list); + + + return this.flattenSameOfQueries({ + type: SearchQueryTypes.OR, + list: combinations.map(c => { + type: SearchQueryTypes.AND, list: c + }) + }); + } return query; } diff --git a/src/common/Utils.ts b/src/common/Utils.ts index 8ea7fa0d..2041de51 100644 --- a/src/common/Utils.ts +++ b/src/common/Utils.ts @@ -240,4 +240,24 @@ export class Utils { (value <= 3.402823466 * E && value >= 1.175494351 * nE)); } + public static getAnyX(num: number, arr: any[], start = 0): any[][] { + if (num <= 0 || num > arr.length || start >= arr.length) { + return []; + } + if (num <= 1) { + return arr.slice(start).map(e => [e]); + } + if (num === arr.length - start) { + return [arr.slice(start)]; + } + const ret: any[][] = []; + for (let i = start; i < arr.length; ++i) { + Utils.getAnyX(num - 1, arr, i + 1).forEach(a => { + a.push(arr[i]); + ret.push(a); + }); + } + return ret; + } + } diff --git a/src/common/entities/SearchQueryDTO.ts b/src/common/entities/SearchQueryDTO.ts index 6c8290ca..7121999d 100644 --- a/src/common/entities/SearchQueryDTO.ts +++ b/src/common/entities/SearchQueryDTO.ts @@ -24,30 +24,47 @@ export enum TextSearchQueryTypes { exact_match = 1, like = 2 } -export enum OrientationSearchTypes { - portrait = 1, landscape = 2 + +export namespace SearchQueryDTO { + export const negate = (query: SearchQueryDTO): SearchQueryDTO => { + switch (query.type) { + case SearchQueryTypes.AND: + query.type = SearchQueryTypes.OR; + (query).list = (query).list.map(q => SearchQueryDTO.negate(q)); + return query; + case SearchQueryTypes.OR: + query.type = SearchQueryTypes.AND; + (query).list = (query).list.map(q => SearchQueryDTO.negate(q)); + return query; + + case SearchQueryTypes.orientation: + (query).landscape = !(query).landscape; + return query; + + case SearchQueryTypes.date: + case SearchQueryTypes.rating: + case SearchQueryTypes.resolution: + case SearchQueryTypes.distance: + case SearchQueryTypes.any_text: + case SearchQueryTypes.person: + case SearchQueryTypes.position: + case SearchQueryTypes.keyword: + case SearchQueryTypes.caption: + case SearchQueryTypes.file_name: + case SearchQueryTypes.directory: + (query).negate = !(query).negate; + return query; + + case SearchQueryTypes.SOME_OF: + throw new Error('Some of not supported'); + } + }; } export interface SearchQueryDTO { type: SearchQueryTypes; } -export interface ANDSearchQuery extends SearchQueryDTO { - type: SearchQueryTypes.AND; - list: SearchQueryDTO[]; -} - -export interface ORSearchQuery extends SearchQueryDTO { - type: SearchQueryTypes.OR; - list: SearchQueryDTO[]; -} - -export interface SomeOfSearchQuery extends SearchQueryDTO, RangeSearchQuery { - type: SearchQueryTypes.SOME_OF; - list: NegatableSearchQuery[]; - min?: number; // at least this amount of items - max?: number; // maximum this amount of items -} export interface NegatableSearchQuery extends SearchQueryDTO { negate?: boolean; // if true negates the expression @@ -58,6 +75,26 @@ export interface RangeSearchQuery extends SearchQueryDTO { max?: number; } +export interface SearchListQuery extends SearchQueryDTO { + list: SearchQueryDTO[]; +} + + +export interface ANDSearchQuery extends SearchQueryDTO, SearchListQuery { + type: SearchQueryTypes.AND; + list: SearchQueryDTO[]; +} + +export interface ORSearchQuery extends SearchQueryDTO, SearchListQuery { + type: SearchQueryTypes.OR; + list: SearchQueryDTO[]; +} + +export interface SomeOfSearchQuery extends SearchQueryDTO, SearchListQuery { + type: SearchQueryTypes.SOME_OF; + list: NegatableSearchQuery[]; + min?: number; // at least this amount of items +} export interface TextSearch extends NegatableSearchQuery { type: SearchQueryTypes.any_text | @@ -87,20 +124,20 @@ export interface DateSearch extends NegatableSearchQuery { before?: number; } -export interface RatingSearch extends NegatableSearchQuery, RangeSearchQuery { +export interface RatingSearch extends RangeSearchQuery, NegatableSearchQuery { type: SearchQueryTypes.rating; min?: number; max?: number; } -export interface ResolutionSearch extends NegatableSearchQuery, RangeSearchQuery { +export interface ResolutionSearch extends RangeSearchQuery, NegatableSearchQuery { type: SearchQueryTypes.resolution; min?: number; // in megapixels max?: number; // in megapixels } -export interface OrientationSearch extends NegatableSearchQuery { +export interface OrientationSearch { type: SearchQueryTypes.orientation; - orientation: OrientationSearchTypes; + landscape: boolean; } diff --git a/test/backend/unit/model/sql/SearchManager.ts b/test/backend/unit/model/sql/SearchManager.ts index 4139986c..804eea6b 100644 --- a/test/backend/unit/model/sql/SearchManager.ts +++ b/test/backend/unit/model/sql/SearchManager.ts @@ -9,12 +9,12 @@ import { DateSearch, DistanceSearch, OrientationSearch, - OrientationSearchTypes, ORSearchQuery, RatingSearch, ResolutionSearch, SearchQueryDTO, SearchQueryTypes, + SomeOfSearchQuery, TextSearch, TextSearchQueryTypes } from '../../../../../src/common/entities/SearchQueryDTO'; @@ -98,10 +98,11 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => { const connection = await SQLConnection.getConnection(); ObjectManagers.getInstance().IndexingManager.indexDirectory = () => Promise.resolve(null); + const im = new IndexingManagerTest(); await im.saveToDB(directory); - await im.saveToDB(subDir); - await im.saveToDB(subDir2); + // await im.saveToDB(subDir); + // await im.saveToDB(subDir2); if (ObjectManagers.getInstance().IndexingManager && ObjectManagers.getInstance().IndexingManager.IsSavingInProgress) { @@ -389,7 +390,7 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => { searchText: null, searchType: null, directories: [], - media: [p, p2], + media: [p, p2, p4], metaFile: [], resultOverflow: false })); @@ -467,7 +468,7 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => { searchText: null, searchType: null, directories: [], - media: [p, p2], + media: [p, p2, p4], metaFile: [], resultOverflow: false })); @@ -487,7 +488,7 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => { searchText: null, searchType: null, directories: [], - media: [p, p2], + media: [p, p2, p4], metaFile: [], resultOverflow: false })); @@ -515,6 +516,84 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => { }); + + it('should minimum of', async () => { + const sm = new SearchManager(); + + let query: SomeOfSearchQuery = { + type: SearchQueryTypes.SOME_OF, + list: [{text: 'jpg', type: SearchQueryTypes.file_name}, + {text: 'mp4', type: SearchQueryTypes.file_name}] + }; + + expect(Utils.clone(await sm.aSearch(query))).to.deep.equalInAnyOrder(removeDir({ + searchText: null, + searchType: null, + directories: [], + media: [p, p2, p_faceLess, p4, v], + metaFile: [], + resultOverflow: false + })); + + query = { + type: SearchQueryTypes.SOME_OF, + list: [{text: 'R2', type: SearchQueryTypes.person}, + {text: 'Anakin', type: SearchQueryTypes.person}, + {text: 'Luke', type: SearchQueryTypes.person}] + }; + + expect(Utils.clone(await sm.aSearch(query))).to.deep.equalInAnyOrder(removeDir({ + searchText: null, + searchType: null, + directories: [], + media: [p, p2, p4], + metaFile: [], + resultOverflow: false + })); + + + query.min = 2; + + expect(Utils.clone(await sm.aSearch(query))).to.deep.equalInAnyOrder(removeDir({ + searchText: null, + searchType: null, + directories: [], + media: [p, p2, p4], + metaFile: [], + resultOverflow: false + })); + + query.min = 3; + + expect(Utils.clone(await sm.aSearch(query))).to.deep.equalInAnyOrder(removeDir({ + searchText: null, + searchType: null, + directories: [], + media: [], + metaFile: [], + resultOverflow: false + })); + + query = { + type: SearchQueryTypes.SOME_OF, + min: 3, + list: [{text: 'sw', type: SearchQueryTypes.file_name}, + {text: 'R2', type: SearchQueryTypes.person}, + {text: 'Kamino', type: SearchQueryTypes.position}, + {text: 'Han', type: SearchQueryTypes.person}] + }; + + expect(Utils.clone(await sm.aSearch(query))).to.deep.equalInAnyOrder(removeDir({ + searchText: null, + searchType: null, + directories: [], + media: [p2], + metaFile: [], + resultOverflow: false + })); + + }); + describe('should search text', async () => { it('as any', async () => { const sm = new SearchManager(); @@ -529,6 +608,17 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => { resultOverflow: false })); + + expect(Utils.clone(await sm.aSearch({text: 'sw', negate: true, type: SearchQueryTypes.any_text}))) + .to.deep.equalInAnyOrder(removeDir({ + searchText: null, + searchType: null, + directories: [], + media: [], + metaFile: [], + resultOverflow: false + })); + expect(Utils.clone(await sm.aSearch({text: 'Boba', type: SearchQueryTypes.any_text}))) .to.deep.equalInAnyOrder(removeDir({ searchText: null, @@ -539,6 +629,28 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => { resultOverflow: false })); + expect(Utils.clone(await sm.aSearch({text: 'Boba', negate: true, type: SearchQueryTypes.any_text}))) + .to.deep.equalInAnyOrder(removeDir({ + searchText: null, + searchType: null, + directories: [], + media: [p2, p_faceLess, p4], + metaFile: [], + resultOverflow: false + })); + + // all should have faces + const sRet = await sm.aSearch({text: 'Boba', negate: true, type: SearchQueryTypes.any_text}); + for (let i = 0; i < sRet.media.length; ++i) { + if (sRet.media[i].id === p_faceLess.id) { + continue; + } + console.log(sRet.media[i]); + expect((sRet.media[i]).metadata.faces).to.be.not.an('undefined'); + expect((sRet.media[i]).metadata.faces).to.be.lengthOf.above(1); + } + + expect(Utils.clone(await sm.aSearch({ text: 'Boba', type: SearchQueryTypes.any_text, @@ -815,6 +927,21 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => { resultOverflow: false })); + expect(Utils.clone(await sm.aSearch({ + before: p.metadata.creationDate, + after: p.metadata.creationDate, + negate: true, + type: SearchQueryTypes.date + }))) + .to.deep.equalInAnyOrder(removeDir({ + searchText: null, + searchType: null, + directories: [], + media: [p2, p_faceLess, p4, v], + metaFile: [], + resultOverflow: false + })); + expect(Utils.clone(await sm.aSearch({ before: p.metadata.creationDate + 1000000000, after: 0, type: SearchQueryTypes.date @@ -854,6 +981,16 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => { resultOverflow: false })); + expect(Utils.clone(await sm.aSearch({min: 0, max: 5, negate: true, type: SearchQueryTypes.rating}))) + .to.deep.equalInAnyOrder(removeDir({ + searchText: null, + searchType: null, + directories: [], + media: [], + metaFile: [], + resultOverflow: false + })); + expect(Utils.clone(await sm.aSearch({min: 2, max: 2, type: SearchQueryTypes.rating}))) .to.deep.equalInAnyOrder(removeDir({ searchText: null, @@ -864,6 +1001,15 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => { resultOverflow: false })); + expect(Utils.clone(await sm.aSearch({min: 2, max: 2, negate: true, type: SearchQueryTypes.rating}))) + .to.deep.equalInAnyOrder(removeDir({ + searchText: null, + searchType: null, + directories: [], + media: [p, p_faceLess], + metaFile: [], + resultOverflow: false + })); }); @@ -900,6 +1046,16 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => { resultOverflow: false })); + expect(Utils.clone(await sm.aSearch({min: 2, max: 3, negate: true, type: SearchQueryTypes.resolution}))) + .to.deep.equalInAnyOrder(removeDir({ + searchText: null, + searchType: null, + directories: [], + media: [p, v, p4], + metaFile: [], + resultOverflow: false + })); + expect(Utils.clone(await sm.aSearch({min: 3, type: SearchQueryTypes.resolution}))) .to.deep.equalInAnyOrder(removeDir({ searchText: null, @@ -917,7 +1073,7 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => { const sm = new SearchManager(); expect(Utils.clone(await sm.aSearch({ - orientation: OrientationSearchTypes.portrait, + landscape: false, type: SearchQueryTypes.orientation }))) .to.deep.equalInAnyOrder(removeDir({ @@ -930,7 +1086,7 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => { })); expect(Utils.clone(await sm.aSearch({ - orientation: OrientationSearchTypes.landscape, + landscape: true, type: SearchQueryTypes.orientation }))) .to.deep.equalInAnyOrder(removeDir({ @@ -984,6 +1140,22 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => { resultOverflow: false })); + + expect(Utils.clone(await sm.aSearch({ + from: {GPSData: {latitude: 0, longitude: 0}}, + distance: 112 * 10, // number of km per degree = ~111km + negate: true, + type: SearchQueryTypes.distance + }))) + .to.deep.equalInAnyOrder(removeDir({ + searchText: null, + searchType: null, + directories: [], + media: [p_faceLess, p4], + metaFile: [], + resultOverflow: false + })); + expect(Utils.clone(await sm.aSearch({ from: {GPSData: {latitude: 10, longitude: 10}}, distance: 1, diff --git a/test/backend/unit/model/sql/TestHelper.ts b/test/backend/unit/model/sql/TestHelper.ts index 8decc91d..aab31175 100644 --- a/test/backend/unit/model/sql/TestHelper.ts +++ b/test/backend/unit/model/sql/TestHelper.ts @@ -148,6 +148,9 @@ export class TestHelper { }, { box: {height: 10, width: 10, left: 105, top: 105}, name: 'Arvíztűrő Tükörfúrógép' + }, { + box: {height: 10, width: 10, left: 201, top: 201}, + name: 'R2-D2' }] as any[]; return p; } @@ -178,6 +181,9 @@ export class TestHelper { }, { box: {height: 10, width: 10, left: 101, top: 101}, name: 'Obivan Kenobi' + }, { + box: {height: 10, width: 10, left: 201, top: 201}, + name: 'R2-D2' }] as any[]; return p; } @@ -224,6 +230,7 @@ export class TestHelper { p.metadata.creationDate = Date.now() - 4000; p.metadata.size.height = 3000; p.metadata.size.width = 2000; + p.metadata.faces = [{ box: {height: 10, width: 10, left: 10, top: 10}, name: 'Kylo Ren' @@ -233,7 +240,11 @@ export class TestHelper { }, { box: {height: 10, width: 10, left: 101, top: 101}, name: 'Obivan Kenobi' + }, { + box: {height: 10, width: 10, left: 201, top: 201}, + name: 'R2-D2' }] as any[]; + return p; }