diff --git a/src/backend/model/database/SearchManager.ts b/src/backend/model/database/SearchManager.ts index a67a18b3..609dc776 100644 --- a/src/backend/model/database/SearchManager.ts +++ b/src/backend/model/database/SearchManager.ts @@ -48,7 +48,7 @@ export class SearchManager { private queryIdBase = 0; private static autoCompleteItemsUnique( - array: Array + array: Array ): Array { const a = array.concat(); for (let i = 0; i < a.length; ++i) { @@ -63,8 +63,8 @@ export class SearchManager { } async autocomplete( - text: string, - type: SearchQueryTypes + text: string, + type: SearchQueryTypes ): Promise { const connection = await SQLConnection.getConnection(); @@ -76,184 +76,184 @@ export class SearchManager { const partialResult: AutoCompleteItem[][] = []; if ( - type === SearchQueryTypes.any_text || - type === SearchQueryTypes.keyword + type === SearchQueryTypes.any_text || + type === SearchQueryTypes.keyword ) { const acList: AutoCompleteItem[] = []; ( - await photoRepository - .createQueryBuilder('photo') - .select('DISTINCT(photo.metadata.keywords)') - .where('photo.metadata.keywords LIKE :text COLLATE ' + SQL_COLLATE, { + await photoRepository + .createQueryBuilder('photo') + .select('DISTINCT(photo.metadata.keywords)') + .where('photo.metadata.keywords LIKE :text COLLATE ' + SQL_COLLATE, { + text: '%' + text + '%', + }) + .limit(Config.Search.AutoComplete.ItemsPerCategory.keyword) + .getRawMany() + ) + .map( + (r): Array => + (r.metadataKeywords as string).split(',') as Array + ) + .forEach((keywords): void => { + acList.push( + ...this.encapsulateAutoComplete( + keywords.filter( + (k): boolean => + k.toLowerCase().indexOf(text.toLowerCase()) !== -1 + ), + SearchQueryTypes.keyword + ) + ); + }); + partialResult.push(acList); + } + + if ( + type === SearchQueryTypes.any_text || + type === SearchQueryTypes.person + ) { + partialResult.push( + this.encapsulateAutoComplete( + ( + await personRepository + .createQueryBuilder('person') + .select('DISTINCT(person.name), person.count') + .where('person.name LIKE :text COLLATE ' + SQL_COLLATE, { text: '%' + text + '%', }) - .limit(Config.Search.AutoComplete.ItemsPerCategory.keyword) + .limit( + Config.Search.AutoComplete.ItemsPerCategory.person + ) + .orderBy('person.count', 'DESC') .getRawMany() - ) - .map( - (r): Array => - (r.metadataKeywords as string).split(',') as Array - ) - .forEach((keywords): void => { - acList.push( - ...this.encapsulateAutoComplete( - keywords.filter( - (k): boolean => - k.toLowerCase().indexOf(text.toLowerCase()) !== -1 - ), - SearchQueryTypes.keyword - ) - ); - }); - partialResult.push(acList); - } - - if ( - type === SearchQueryTypes.any_text || - type === SearchQueryTypes.person - ) { - partialResult.push( - this.encapsulateAutoComplete( - ( - await personRepository - .createQueryBuilder('person') - .select('DISTINCT(person.name), person.count') - .where('person.name LIKE :text COLLATE ' + SQL_COLLATE, { - text: '%' + text + '%', - }) - .limit( - Config.Search.AutoComplete.ItemsPerCategory.person - ) - .orderBy('person.count', 'DESC') - .getRawMany() - ).map((r) => r.name), - SearchQueryTypes.person - ) + ).map((r) => r.name), + SearchQueryTypes.person + ) ); } if ( - type === SearchQueryTypes.any_text || - type === SearchQueryTypes.position || - type === SearchQueryTypes.distance + type === SearchQueryTypes.any_text || + type === SearchQueryTypes.position || + type === SearchQueryTypes.distance ) { const acList: AutoCompleteItem[] = []; ( - await photoRepository - .createQueryBuilder('photo') - .select( - 'photo.metadata.positionData.country as country, ' + - 'photo.metadata.positionData.state as state, photo.metadata.positionData.city as city' - ) - .where( - 'photo.metadata.positionData.country LIKE :text COLLATE ' + - SQL_COLLATE, - {text: '%' + text + '%'} - ) - .orWhere( - 'photo.metadata.positionData.state LIKE :text COLLATE ' + - SQL_COLLATE, - {text: '%' + text + '%'} - ) - .orWhere( - 'photo.metadata.positionData.city LIKE :text COLLATE ' + - SQL_COLLATE, - {text: '%' + text + '%'} - ) - .groupBy( - 'photo.metadata.positionData.country, photo.metadata.positionData.state, photo.metadata.positionData.city' - ) - .limit(Config.Search.AutoComplete.ItemsPerCategory.position) - .getRawMany() - ) - .filter((pm): boolean => !!pm) - .map( - (pm): Array => - [pm.city || '', pm.country || '', pm.state || ''] as Array + await photoRepository + .createQueryBuilder('photo') + .select( + 'photo.metadata.positionData.country as country, ' + + 'photo.metadata.positionData.state as state, photo.metadata.positionData.city as city' ) - .forEach((positions): void => { - acList.push( - ...this.encapsulateAutoComplete( - positions.filter( - (p): boolean => - p.toLowerCase().indexOf(text.toLowerCase()) !== -1 - ), - type === SearchQueryTypes.distance - ? type - : SearchQueryTypes.position - ) - ); - }); + .where( + 'photo.metadata.positionData.country LIKE :text COLLATE ' + + SQL_COLLATE, + {text: '%' + text + '%'} + ) + .orWhere( + 'photo.metadata.positionData.state LIKE :text COLLATE ' + + SQL_COLLATE, + {text: '%' + text + '%'} + ) + .orWhere( + 'photo.metadata.positionData.city LIKE :text COLLATE ' + + SQL_COLLATE, + {text: '%' + text + '%'} + ) + .groupBy( + 'photo.metadata.positionData.country, photo.metadata.positionData.state, photo.metadata.positionData.city' + ) + .limit(Config.Search.AutoComplete.ItemsPerCategory.position) + .getRawMany() + ) + .filter((pm): boolean => !!pm) + .map( + (pm): Array => + [pm.city || '', pm.country || '', pm.state || ''] as Array + ) + .forEach((positions): void => { + acList.push( + ...this.encapsulateAutoComplete( + positions.filter( + (p): boolean => + p.toLowerCase().indexOf(text.toLowerCase()) !== -1 + ), + type === SearchQueryTypes.distance + ? type + : SearchQueryTypes.position + ) + ); + }); partialResult.push(acList); } if ( - type === SearchQueryTypes.any_text || - type === SearchQueryTypes.file_name + type === SearchQueryTypes.any_text || + type === SearchQueryTypes.file_name ) { partialResult.push( - this.encapsulateAutoComplete( - ( - await mediaRepository - .createQueryBuilder('media') - .select('DISTINCT(media.name)') - .where('media.name LIKE :text COLLATE ' + SQL_COLLATE, { - text: '%' + text + '%', - }) - .limit( - Config.Search.AutoComplete.ItemsPerCategory.fileName - ) - .getRawMany() - ).map((r) => r.name), - SearchQueryTypes.file_name - ) + this.encapsulateAutoComplete( + ( + await mediaRepository + .createQueryBuilder('media') + .select('DISTINCT(media.name)') + .where('media.name LIKE :text COLLATE ' + SQL_COLLATE, { + text: '%' + text + '%', + }) + .limit( + Config.Search.AutoComplete.ItemsPerCategory.fileName + ) + .getRawMany() + ).map((r) => r.name), + SearchQueryTypes.file_name + ) ); } if ( - type === SearchQueryTypes.any_text || - type === SearchQueryTypes.caption + type === SearchQueryTypes.any_text || + type === SearchQueryTypes.caption ) { partialResult.push( - this.encapsulateAutoComplete( - ( - await photoRepository - .createQueryBuilder('media') - .select('DISTINCT(media.metadata.caption) as caption') - .where( - 'media.metadata.caption LIKE :text COLLATE ' + SQL_COLLATE, - {text: '%' + text + '%'} - ) - .limit( - Config.Search.AutoComplete.ItemsPerCategory.caption - ) - .getRawMany() - ).map((r) => r.caption), - SearchQueryTypes.caption - ) + this.encapsulateAutoComplete( + ( + await photoRepository + .createQueryBuilder('media') + .select('DISTINCT(media.metadata.caption) as caption') + .where( + 'media.metadata.caption LIKE :text COLLATE ' + SQL_COLLATE, + {text: '%' + text + '%'} + ) + .limit( + Config.Search.AutoComplete.ItemsPerCategory.caption + ) + .getRawMany() + ).map((r) => r.caption), + SearchQueryTypes.caption + ) ); } if ( - type === SearchQueryTypes.any_text || - type === SearchQueryTypes.directory + type === SearchQueryTypes.any_text || + type === SearchQueryTypes.directory ) { partialResult.push( - this.encapsulateAutoComplete( - ( - await directoryRepository - .createQueryBuilder('dir') - .select('DISTINCT(dir.name)') - .where('dir.name LIKE :text COLLATE ' + SQL_COLLATE, { - text: '%' + text + '%', - }) - .limit( - Config.Search.AutoComplete.ItemsPerCategory.directory - ) - .getRawMany() - ).map((r) => r.name), - SearchQueryTypes.directory - ) + this.encapsulateAutoComplete( + ( + await directoryRepository + .createQueryBuilder('dir') + .select('DISTINCT(dir.name)') + .where('dir.name LIKE :text COLLATE ' + SQL_COLLATE, { + text: '%' + text + '%', + }) + .limit( + Config.Search.AutoComplete.ItemsPerCategory.directory + ) + .getRawMany() + ).map((r) => r.name), + SearchQueryTypes.directory + ) ); } @@ -290,13 +290,13 @@ export class SearchManager { }; result.media = await connection - .getRepository(MediaEntity) - .createQueryBuilder('media') - .select(['media', ...this.DIRECTORY_SELECT]) - .where(this.buildWhereQuery(query)) - .leftJoin('media.directory', 'directory') - .limit(Config.Search.maxMediaResult + 1) - .getMany(); + .getRepository(MediaEntity) + .createQueryBuilder('media') + .select(['media', ...this.DIRECTORY_SELECT]) + .where(this.buildWhereQuery(query)) + .leftJoin('media.directory', 'directory') + .limit(Config.Search.maxMediaResult + 1) + .getMany(); if (result.media.length > Config.Search.maxMediaResult) { @@ -307,31 +307,31 @@ export class SearchManager { if (Config.Search.listMetafiles === true) { const dIds = Array.from(new Set(result.media.map(m => (m.directory as unknown as { id: number }).id))); result.metaFile = await connection - .getRepository(FileEntity) - .createQueryBuilder('file') - .select(['file', ...this.DIRECTORY_SELECT]) - .where(`file.directoryId IN(${dIds})`) - .leftJoin('file.directory', 'directory') - .getMany(); + .getRepository(FileEntity) + .createQueryBuilder('file') + .select(['file', ...this.DIRECTORY_SELECT]) + .where(`file.directoryId IN(${dIds})`) + .leftJoin('file.directory', 'directory') + .getMany(); } if (Config.Search.listDirectories === true) { const dirQuery = this.filterDirectoryQuery(query); if (dirQuery !== null) { result.directories = await connection - .getRepository(DirectoryEntity) - .createQueryBuilder('directory') - .where(this.buildWhereQuery(dirQuery, true)) - .leftJoinAndSelect('directory.cover', 'cover') - .leftJoinAndSelect('cover.directory', 'coverDirectory') - .limit(Config.Search.maxDirectoryResult + 1) - .select([ - 'directory', - 'cover.name', - 'coverDirectory.name', - 'coverDirectory.path', - ]) - .getMany(); + .getRepository(DirectoryEntity) + .createQueryBuilder('directory') + .where(this.buildWhereQuery(dirQuery, true)) + .leftJoinAndSelect('directory.cover', 'cover') + .leftJoinAndSelect('cover.directory', 'coverDirectory') + .limit(Config.Search.maxDirectoryResult + 1) + .select([ + 'directory', + 'cover.name', + 'coverDirectory.name', + 'coverDirectory.path', + ]) + .getMany(); // setting covers if (result.directories) { @@ -340,7 +340,7 @@ export class SearchManager { } } if ( - result.directories.length > Config.Search.maxDirectoryResult + result.directories.length > Config.Search.maxDirectoryResult ) { result.resultOverflow = true; } @@ -352,8 +352,8 @@ export class SearchManager { public static setSorting( - query: SelectQueryBuilder, - sortings: SortingMethod[] + query: SelectQueryBuilder, + sortings: SortingMethod[] ): SelectQueryBuilder { if (!sortings || !Array.isArray(sortings)) { return query; @@ -394,11 +394,11 @@ export class SearchManager { public async getNMedia(query: SearchQueryDTO, sortings: SortingMethod[], take: number, photoOnly = false) { const connection = await SQLConnection.getConnection(); const sqlQuery: SelectQueryBuilder = connection - .getRepository(photoOnly ? PhotoEntity : MediaEntity) - .createQueryBuilder('media') - .select(['media', ...this.DIRECTORY_SELECT]) - .innerJoin('media.directory', 'directory') - .where(await this.prepareAndBuildWhereQuery(query)); + .getRepository(photoOnly ? PhotoEntity : MediaEntity) + .createQueryBuilder('media') + .select(['media', ...this.DIRECTORY_SELECT]) + .innerJoin('media.directory', 'directory') + .where(await this.prepareAndBuildWhereQuery(query)); SearchManager.setSorting(sqlQuery, sortings); return sqlQuery.limit(take).getMany(); @@ -409,16 +409,16 @@ export class SearchManager { const connection = await SQLConnection.getConnection(); return await connection - .getRepository(MediaEntity) - .createQueryBuilder('media') - .innerJoin('media.directory', 'directory') - .where(await this.prepareAndBuildWhereQuery(query)) - .getCount(); + .getRepository(MediaEntity) + .createQueryBuilder('media') + .innerJoin('media.directory', 'directory') + .where(await this.prepareAndBuildWhereQuery(query)) + .getCount(); } public async prepareAndBuildWhereQuery( - queryIN: SearchQueryDTO, - directoryOnly = false + queryIN: SearchQueryDTO, + directoryOnly = false ): Promise { const query = await this.prepareQuery(queryIN); return this.buildWhereQuery(query, directoryOnly); @@ -438,24 +438,24 @@ export class SearchManager { * @private */ public buildWhereQuery( - query: SearchQueryDTO, - directoryOnly = false + query: SearchQueryDTO, + directoryOnly = false ): Brackets { const queryId = (query as SearchQueryDTOWithID).queryId; switch (query.type) { case SearchQueryTypes.AND: return new Brackets((q): unknown => { (query as ANDSearchQuery).list.forEach((sq) => { - q.andWhere(this.buildWhereQuery(sq, directoryOnly)); - } + q.andWhere(this.buildWhereQuery(sq, directoryOnly)); + } ); return q; }); case SearchQueryTypes.OR: return new Brackets((q): unknown => { (query as ANDSearchQuery).list.forEach((sq) => { - q.orWhere(this.buildWhereQuery(sq, directoryOnly)); - } + q.orWhere(this.buildWhereQuery(sq, directoryOnly)); + } ); return q; }); @@ -478,30 +478,30 @@ export class SearchManager { }; const minLat = trimRange( - (query as DistanceSearch).from.GPSData.latitude - - (query as DistanceSearch).distance * latDelta, - -90, - 90 + (query as DistanceSearch).from.GPSData.latitude - + (query as DistanceSearch).distance * latDelta, + -90, + 90 ); const maxLat = trimRange( - (query as DistanceSearch).from.GPSData.latitude + - (query as DistanceSearch).distance * latDelta, - -90, - 90 + (query as DistanceSearch).from.GPSData.latitude + + (query as DistanceSearch).distance * latDelta, + -90, + 90 ); const minLon = trimRange( - (query as DistanceSearch).from.GPSData.longitude - - ((query as DistanceSearch).distance * lonDelta) / - Math.cos(minLat * (Math.PI / 180)), - -180, - 180 + (query as DistanceSearch).from.GPSData.longitude - + ((query as DistanceSearch).distance * lonDelta) / + Math.cos(minLat * (Math.PI / 180)), + -180, + 180 ); const maxLon = trimRange( - (query as DistanceSearch).from.GPSData.longitude + - ((query as DistanceSearch).distance * lonDelta) / - Math.cos(maxLat * (Math.PI / 180)), - -180, - 180 + (query as DistanceSearch).from.GPSData.longitude + + ((query as DistanceSearch).distance * lonDelta) / + Math.cos(maxLat * (Math.PI / 180)), + -180, + 180 ); return new Brackets((q): unknown => { @@ -512,37 +512,37 @@ export class SearchManager { textParam['minLon' + queryId] = minLon; if (!(query as DistanceSearch).negate) { q.where( - `media.metadata.positionData.GPSData.latitude < :maxLat${queryId}`, - textParam + `media.metadata.positionData.GPSData.latitude < :maxLat${queryId}`, + textParam ); q.andWhere( - `media.metadata.positionData.GPSData.latitude > :minLat${queryId}`, - textParam + `media.metadata.positionData.GPSData.latitude > :minLat${queryId}`, + textParam ); q.andWhere( - `media.metadata.positionData.GPSData.longitude < :maxLon${queryId}`, - textParam + `media.metadata.positionData.GPSData.longitude < :maxLon${queryId}`, + textParam ); q.andWhere( - `media.metadata.positionData.GPSData.longitude > :minLon${queryId}`, - textParam + `media.metadata.positionData.GPSData.longitude > :minLon${queryId}`, + textParam ); } else { q.where( - `media.metadata.positionData.GPSData.latitude > :maxLat${queryId}`, - textParam + `media.metadata.positionData.GPSData.latitude > :maxLat${queryId}`, + textParam ); q.orWhere( - `media.metadata.positionData.GPSData.latitude < :minLat${queryId}`, - textParam + `media.metadata.positionData.GPSData.latitude < :minLat${queryId}`, + textParam ); q.orWhere( - `media.metadata.positionData.GPSData.longitude > :maxLon${queryId}`, - textParam + `media.metadata.positionData.GPSData.longitude > :maxLon${queryId}`, + textParam ); q.orWhere( - `media.metadata.positionData.GPSData.longitude < :minLon${queryId}`, - textParam + `media.metadata.positionData.GPSData.longitude < :minLon${queryId}`, + textParam ); } return q; @@ -555,7 +555,7 @@ export class SearchManager { return new Brackets((q): unknown => { if (typeof (query as FromDateSearch).value === 'undefined') { throw new Error( - 'Invalid search query: Date Query should contain from value' + 'Invalid search query: Date Query should contain from value' ); } const relation = (query as TextSearch).negate ? '<' : '>='; @@ -563,8 +563,8 @@ export class SearchManager { const textParam: { [key: string]: unknown } = {}; textParam['from' + queryId] = (query as FromDateSearch).value; q.where( - `media.metadata.creationDate ${relation} :from${queryId}`, - textParam + `media.metadata.creationDate ${relation} :from${queryId}`, + textParam ); return q; @@ -577,7 +577,7 @@ export class SearchManager { return new Brackets((q): unknown => { if (typeof (query as ToDateSearch).value === 'undefined') { throw new Error( - 'Invalid search query: Date Query should contain to value' + 'Invalid search query: Date Query should contain to value' ); } const relation = (query as TextSearch).negate ? '>' : '<='; @@ -585,8 +585,8 @@ export class SearchManager { const textParam: { [key: string]: unknown } = {}; textParam['to' + queryId] = (query as ToDateSearch).value; q.where( - `media.metadata.creationDate ${relation} :to${queryId}`, - textParam + `media.metadata.creationDate ${relation} :to${queryId}`, + textParam ); return q; @@ -599,7 +599,7 @@ export class SearchManager { return new Brackets((q): unknown => { if (typeof (query as MinRatingSearch).value === 'undefined') { throw new Error( - 'Invalid search query: Rating Query should contain minvalue' + 'Invalid search query: Rating Query should contain minvalue' ); } @@ -608,8 +608,8 @@ export class SearchManager { const textParam: { [key: string]: unknown } = {}; textParam['min' + queryId] = (query as MinRatingSearch).value; q.where( - `media.metadata.rating ${relation} :min${queryId}`, - textParam + `media.metadata.rating ${relation} :min${queryId}`, + textParam ); return q; @@ -621,7 +621,7 @@ export class SearchManager { return new Brackets((q): unknown => { if (typeof (query as MaxRatingSearch).value === 'undefined') { throw new Error( - 'Invalid search query: Rating Query should contain max value' + 'Invalid search query: Rating Query should contain max value' ); } @@ -631,8 +631,8 @@ export class SearchManager { const textParam: { [key: string]: unknown } = {}; textParam['max' + queryId] = (query as MaxRatingSearch).value; q.where( - `media.metadata.rating ${relation} :max${queryId}`, - textParam + `media.metadata.rating ${relation} :max${queryId}`, + textParam ); } return q; @@ -645,7 +645,7 @@ export class SearchManager { return new Brackets((q): unknown => { if (typeof (query as MinPersonCountSearch).value === 'undefined') { throw new Error( - 'Invalid search query: Person count Query should contain minvalue' + 'Invalid search query: Person count Query should contain minvalue' ); } @@ -654,8 +654,8 @@ export class SearchManager { const textParam: { [key: string]: unknown } = {}; textParam['min' + queryId] = (query as MinPersonCountSearch).value; q.where( - `media.metadata.personsLength ${relation} :min${queryId}`, - textParam + `media.metadata.personsLength ${relation} :min${queryId}`, + textParam ); return q; @@ -667,7 +667,7 @@ export class SearchManager { return new Brackets((q): unknown => { if (typeof (query as MaxPersonCountSearch).value === 'undefined') { throw new Error( - 'Invalid search query: Person count Query should contain max value' + 'Invalid search query: Person count Query should contain max value' ); } @@ -677,8 +677,8 @@ export class SearchManager { const textParam: { [key: string]: unknown } = {}; textParam['max' + queryId] = (query as MaxPersonCountSearch).value; q.where( - `media.metadata.personsLength ${relation} :max${queryId}`, - textParam + `media.metadata.personsLength ${relation} :max${queryId}`, + textParam ); } return q; @@ -691,7 +691,7 @@ export class SearchManager { return new Brackets((q): unknown => { if (typeof (query as MinResolutionSearch).value === 'undefined') { throw new Error( - 'Invalid search query: Resolution Query should contain min value' + 'Invalid search query: Resolution Query should contain min value' ); } @@ -699,10 +699,10 @@ export class SearchManager { const textParam: { [key: string]: unknown } = {}; textParam['min' + queryId] = - (query as MinResolutionSearch).value * 1000 * 1000; + (query as MinResolutionSearch).value * 1000 * 1000; q.where( - `media.metadata.size.width * media.metadata.size.height ${relation} :min${queryId}`, - textParam + `media.metadata.size.width * media.metadata.size.height ${relation} :min${queryId}`, + textParam ); return q; @@ -715,7 +715,7 @@ export class SearchManager { return new Brackets((q): unknown => { if (typeof (query as MaxResolutionSearch).value === 'undefined') { throw new Error( - 'Invalid search query: Rating Query should contain min or max value' + 'Invalid search query: Rating Query should contain min or max value' ); } @@ -723,10 +723,10 @@ export class SearchManager { const textParam: { [key: string]: unknown } = {}; textParam['max' + queryId] = - (query as MaxResolutionSearch).value * 1000 * 1000; + (query as MaxResolutionSearch).value * 1000 * 1000; q.where( - `media.metadata.size.width * media.metadata.size.height ${relation} :max${queryId}`, - textParam + `media.metadata.size.width * media.metadata.size.height ${relation} :max${queryId}`, + textParam ); return q; @@ -754,9 +754,9 @@ export class SearchManager { return new Brackets((q): unknown => { // Fixed frequency if ((tq.frequency === DatePatternFrequency.years_ago || - tq.frequency === DatePatternFrequency.months_ago || - tq.frequency === DatePatternFrequency.weeks_ago || - tq.frequency === DatePatternFrequency.days_ago)) { + tq.frequency === DatePatternFrequency.months_ago || + tq.frequency === DatePatternFrequency.weeks_ago || + tq.frequency === DatePatternFrequency.days_ago)) { if (isNaN(tq.agoNumber)) { throw new Error('ago number is missing on date patter search query with frequency: ' + DatePatternFrequency[tq.frequency] + ', ago number: ' + tq.agoNumber); @@ -790,16 +790,16 @@ export class SearchManager { if (tq.negate) { q.where( - `media.metadata.creationDate >= :to${queryId}`, - textParam + `media.metadata.creationDate >= :to${queryId}`, + textParam ).orWhere(`media.metadata.creationDate < :from${queryId}`, - textParam); + textParam); } else { q.where( - `media.metadata.creationDate < :to${queryId}`, - textParam + `media.metadata.creationDate < :to${queryId}`, + textParam ).andWhere(`media.metadata.creationDate >= :from${queryId}`, - textParam); + textParam); } } else { @@ -807,43 +807,70 @@ export class SearchManager { const textParam: { [key: string]: unknown } = {}; textParam['diff' + queryId] = tq.daysLength; + const addWhere = (duration: string, crossesDateBoundary: boolean) => { - const relationTop = tq.negate ? '>' : '<='; - const relationBottom = tq.negate ? '<=' : '>'; + const relationEql = tq.negate ? '!=' : '='; + + // reminder: !(a&&b) = (!a || !b), that is what happening here if negate is true + const relationTop = tq.negate ? '>' : '<='; + const relationBottom = tq.negate ? '<=' : '>'; + // this is an XoR. during date boundary crossing we swap boolean logic again + const whereFN = !!tq.negate !== crossesDateBoundary ? 'orWhere' : 'andWhere'; - const addWhere = (duration: string) => { if (Config.Database.type === DatabaseType.sqlite) { - q.where( + if (tq.daysLength == 0) { + q.where( + `CAST(strftime('${duration}',media.metadataCreationDate/1000, 'unixepoch') AS INTEGER) ${relationEql} CAST(strftime('${duration}','now') AS INTEGER)` + ); + } else { + q.where( `CAST(strftime('${duration}',media.metadataCreationDate/1000, 'unixepoch') AS INTEGER) ${relationTop} CAST(strftime('${duration}','now') AS INTEGER)` - ).andWhere(`CAST(strftime('${duration}',media.metadataCreationDate/1000, 'unixepoch') AS INTEGER) ${relationBottom} CAST(strftime('${duration}','now','-:diff${queryId} day') AS INTEGER)`, + )[whereFN](`CAST(strftime('${duration}',media.metadataCreationDate/1000, 'unixepoch') AS INTEGER) ${relationBottom} CAST(strftime('${duration}','now','-:diff${queryId} day') AS INTEGER)`, textParam); + } } else { - - q.where( + if (tq.daysLength == 0) { + q.where( + `CAST(FROM_UNIXTIME(media.metadataCreationDate/1000, '${duration}') AS SIGNED) ${relationEql} CAST(DATE_FORMAT(CURDATE(),'${duration}') AS SIGNED)` + ); + } else { + q.where( `CAST(FROM_UNIXTIME(media.metadataCreationDate/1000, '${duration}') AS SIGNED) ${relationTop} CAST(DATE_FORMAT(CURDATE(),'${duration}') AS SIGNED)` - ).andWhere(`CAST(FROM_UNIXTIME(media.metadataCreationDate/1000, '${duration}') AS SIGNED) ${relationBottom} CAST(DATE_FORMAT((DATE_ADD(curdate(), INTERVAL -:diff${queryId} DAY)),'${duration}') AS SIGNED)`, + )[whereFN](`CAST(FROM_UNIXTIME(media.metadataCreationDate/1000, '${duration}') AS SIGNED) ${relationBottom} CAST(DATE_FORMAT((DATE_ADD(curdate(), INTERVAL -:diff${queryId} DAY)),'${duration}') AS SIGNED)`, textParam); + } } }; switch (tq.frequency) { case DatePatternFrequency.every_year: - if (tq.daysLength >= 365) { + if (tq.daysLength >= 365) { // trivial result includes all photos + if (tq.negate) { + q.andWhere('FALSE'); + } return q; } - addWhere('%j'); + const d = new Date(); + const dayOfYear = Math.floor((d.getTime() - new Date(d.getFullYear(), 0, 0).getTime()) / 1000 / 60 / 60 / 24); + addWhere('%j', dayOfYear - tq.daysLength < 0); break; case DatePatternFrequency.every_month: - if (tq.daysLength >= 31) { + if (tq.daysLength >= 31) { // trivial result includes all photos + if (tq.negate) { + q.andWhere('FALSE'); + } return q; } - addWhere('%d'); + addWhere('%d', (new Date()).getUTCDate() - tq.daysLength < 0); break; case DatePatternFrequency.every_week: - if (tq.daysLength >= 7) { + if (tq.daysLength >= 7) { // trivial result includes all photos + if (tq.negate) { + q.andWhere('FALSE'); + } return q; } - addWhere('%w'); + addWhere('%w', (new Date()).getUTCDay() - tq.daysLength < 0); break; } @@ -861,8 +888,8 @@ export class SearchManager { return new Brackets((q: WhereExpression) => { const createMatchString = (str: string): string => { if ( - (query as TextSearch).matchType === - TextSearchQueryMatchTypes.exact_match + (query as TextSearch).matchType === + TextSearchQueryMatchTypes.exact_match ) { return str; } @@ -882,148 +909,148 @@ export class SearchManager { const textParam: { [key: string]: unknown } = {}; textParam['text' + queryId] = createMatchString( - (query as TextSearch).text + (query as TextSearch).text ); if ( - query.type === SearchQueryTypes.any_text || - query.type === SearchQueryTypes.directory + query.type === SearchQueryTypes.any_text || + query.type === SearchQueryTypes.directory ) { const dirPathStr = (query as TextSearch).text.replace( - new RegExp('\\\\', 'g'), - '/' + new RegExp('\\\\', 'g'), + '/' ); textParam['fullPath' + queryId] = createMatchString(dirPathStr); q[whereFN]( - `directory.path ${LIKE} :fullPath${queryId} COLLATE ` + SQL_COLLATE, - textParam + `directory.path ${LIKE} :fullPath${queryId} COLLATE ` + SQL_COLLATE, + textParam ); const directoryPath = GalleryManager.parseRelativeDirePath(dirPathStr); q[whereFN]( - new Brackets((dq): unknown => { - textParam['dirName' + queryId] = createMatchString( - directoryPath.name + new Brackets((dq): unknown => { + textParam['dirName' + queryId] = createMatchString( + directoryPath.name + ); + dq[whereFNRev]( + `directory.name ${LIKE} :dirName${queryId} COLLATE ${SQL_COLLATE}`, + textParam + ); + if (dirPathStr.includes('/')) { + textParam['parentName' + queryId] = createMatchString( + directoryPath.parent ); dq[whereFNRev]( - `directory.name ${LIKE} :dirName${queryId} COLLATE ${SQL_COLLATE}`, - textParam + `directory.path ${LIKE} :parentName${queryId} COLLATE ${SQL_COLLATE}`, + textParam ); - if (dirPathStr.includes('/')) { - textParam['parentName' + queryId] = createMatchString( - directoryPath.parent - ); - dq[whereFNRev]( - `directory.path ${LIKE} :parentName${queryId} COLLATE ${SQL_COLLATE}`, - textParam - ); - } - return dq; - }) + } + return dq; + }) ); } if ( - (query.type === SearchQueryTypes.any_text && !directoryOnly) || - query.type === SearchQueryTypes.file_name + (query.type === SearchQueryTypes.any_text && !directoryOnly) || + query.type === SearchQueryTypes.file_name ) { q[whereFN]( - `media.name ${LIKE} :text${queryId} COLLATE ${SQL_COLLATE}`, - textParam + `media.name ${LIKE} :text${queryId} COLLATE ${SQL_COLLATE}`, + textParam ); } if ( - (query.type === SearchQueryTypes.any_text && !directoryOnly) || - query.type === SearchQueryTypes.caption + (query.type === SearchQueryTypes.any_text && !directoryOnly) || + query.type === SearchQueryTypes.caption ) { q[whereFN]( - `media.metadata.caption ${LIKE} :text${queryId} COLLATE ${SQL_COLLATE}`, - textParam + `media.metadata.caption ${LIKE} :text${queryId} COLLATE ${SQL_COLLATE}`, + textParam ); } if ( - (query.type === SearchQueryTypes.any_text && !directoryOnly) || - query.type === SearchQueryTypes.position + (query.type === SearchQueryTypes.any_text && !directoryOnly) || + query.type === SearchQueryTypes.position ) { q[whereFN]( - `media.metadata.positionData.country ${LIKE} :text${queryId} COLLATE ${SQL_COLLATE}`, - textParam + `media.metadata.positionData.country ${LIKE} :text${queryId} COLLATE ${SQL_COLLATE}`, + textParam )[whereFN]( - `media.metadata.positionData.state ${LIKE} :text${queryId} COLLATE ${SQL_COLLATE}`, - textParam + `media.metadata.positionData.state ${LIKE} :text${queryId} COLLATE ${SQL_COLLATE}`, + textParam )[whereFN]( - `media.metadata.positionData.city ${LIKE} :text${queryId} COLLATE ${SQL_COLLATE}`, - textParam + `media.metadata.positionData.city ${LIKE} :text${queryId} COLLATE ${SQL_COLLATE}`, + textParam ); } // Matching for array type fields const matchArrayField = (fieldName: string): void => { q[whereFN]( - new Brackets((qbr): void => { - if ( - (query as TextSearch).matchType !== - TextSearchQueryMatchTypes.exact_match - ) { - qbr[whereFN]( - `${fieldName} ${LIKE} :text${queryId} COLLATE ${SQL_COLLATE}`, - textParam - ); - } else { - qbr[whereFN]( - new Brackets((qb): void => { - 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 - }`; + new Brackets((qbr): void => { + if ( + (query as TextSearch).matchType !== + TextSearchQueryMatchTypes.exact_match + ) { + qbr[whereFN]( + `${fieldName} ${LIKE} :text${queryId} COLLATE ${SQL_COLLATE}`, + textParam + ); + } else { + qbr[whereFN]( + new Brackets((qb): void => { + 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${queryId} COLLATE ${SQL_COLLATE}`, - textParam - ); - qb[whereFN]( - `${fieldName} ${LIKE} :Ctext${queryId} COLLATE ${SQL_COLLATE}`, - textParam - ); - qb[whereFN]( - `${fieldName} ${LIKE} :textC${queryId} COLLATE ${SQL_COLLATE}`, - textParam - ); - qb[whereFN]( - `${fieldName} ${LIKE} :text_exact${queryId} COLLATE ${SQL_COLLATE}`, - textParam - ); - }) - ); - } - if ((query as TextSearch).negate) { - qbr.orWhere(`${fieldName} IS NULL`); - } - }) + qb[whereFN]( + `${fieldName} ${LIKE} :CtextC${queryId} COLLATE ${SQL_COLLATE}`, + textParam + ); + qb[whereFN]( + `${fieldName} ${LIKE} :Ctext${queryId} COLLATE ${SQL_COLLATE}`, + textParam + ); + qb[whereFN]( + `${fieldName} ${LIKE} :textC${queryId} COLLATE ${SQL_COLLATE}`, + textParam + ); + qb[whereFN]( + `${fieldName} ${LIKE} :text_exact${queryId} COLLATE ${SQL_COLLATE}`, + textParam + ); + }) + ); + } + if ((query as TextSearch).negate) { + qbr.orWhere(`${fieldName} IS NULL`); + } + }) ); }; if ( - (query.type === SearchQueryTypes.any_text && !directoryOnly) || - query.type === SearchQueryTypes.person + (query.type === SearchQueryTypes.any_text && !directoryOnly) || + query.type === SearchQueryTypes.person ) { matchArrayField('media.metadata.persons'); } if ( - (query.type === SearchQueryTypes.any_text && !directoryOnly) || - query.type === SearchQueryTypes.keyword + (query.type === SearchQueryTypes.any_text && !directoryOnly) || + query.type === SearchQueryTypes.keyword ) { matchArrayField('media.metadata.keywords'); } @@ -1038,7 +1065,7 @@ export class SearchManager { return { type: query.type, list: ((query as SearchListQuery).list || []).map( - (q): SearchQueryDTO => this.flattenSameOfQueries(q) + (q): SearchQueryDTO => this.flattenSameOfQueries(q) ), } as SearchListQuery; case SearchQueryTypes.SOME_OF: @@ -1060,9 +1087,9 @@ export class SearchManager { } const getAllCombinations = ( - num: number, - arr: SearchQueryDTO[], - start = 0 + num: number, + arr: SearchQueryDTO[], + start = 0 ): SearchQueryDTO[] => { if (num <= 0 || num > arr.length || start >= arr.length) { return null; @@ -1112,8 +1139,8 @@ export class SearchManager { return this.flattenSameOfQueries({ type: SearchQueryTypes.OR, list: getAllCombinations( - someOfQ.min, - (query as SearchListQuery).list + someOfQ.min, + (query as SearchListQuery).list ), } as ORSearchQuery); } @@ -1126,8 +1153,8 @@ export class SearchManager { * Witch SOME_OF query the number of WHERE constrains have O(N!) complexity */ private assignQueryIDs( - queryIN: SearchQueryDTO, - id = {value: 1} + queryIN: SearchQueryDTO, + id = {value: 1} ): SearchQueryDTO { // It is possible that one SQL query contains multiple searchQueries // (like: where ( AND ()) @@ -1140,12 +1167,12 @@ export class SearchManager { } if ((queryIN as SearchListQuery).list) { (queryIN as SearchListQuery).list.forEach((q) => - this.assignQueryIDs(q, id) + this.assignQueryIDs(q, id) ); return queryIN; } (queryIN as SearchQueryDTOWithID).queryId = - this.queryIdBase + '_' + id.value; + this.queryIdBase + '_' + id.value; id.value++; return queryIN; } @@ -1159,7 +1186,7 @@ export class SearchManager { const andRet = { type: SearchQueryTypes.AND, list: (query as SearchListQuery).list.map((q) => - this.filterDirectoryQuery(q) + this.filterDirectoryQuery(q) ), } as ANDSearchQuery; // if any of the queries contain non dir query thw whole and query is a non dir query @@ -1172,8 +1199,8 @@ export class SearchManager { const orRet = { type: SearchQueryTypes.OR, list: (query as SearchListQuery).list - .map((q) => this.filterDirectoryQuery(q)) - .filter((q) => q !== null), + .map((q) => this.filterDirectoryQuery(q)) + .filter((q) => q !== null), } as ORSearchQuery; if (orRet.list.length === 0) { return null; @@ -1194,31 +1221,31 @@ export class SearchManager { private async getGPSData(query: SearchQueryDTO): Promise { if ((query as ANDSearchQuery | ORSearchQuery).list) { for ( - let i = 0; - i < (query as ANDSearchQuery | ORSearchQuery).list.length; - ++i + let i = 0; + i < (query as ANDSearchQuery | ORSearchQuery).list.length; + ++i ) { (query as ANDSearchQuery | ORSearchQuery).list[i] = - await this.getGPSData( - (query as ANDSearchQuery | ORSearchQuery).list[i] - ); + await this.getGPSData( + (query as ANDSearchQuery | ORSearchQuery).list[i] + ); } } if ( - query.type === SearchQueryTypes.distance && - (query as DistanceSearch).from.text + query.type === SearchQueryTypes.distance && + (query as DistanceSearch).from.text ) { (query as DistanceSearch).from.GPSData = - await ObjectManagers.getInstance().LocationManager.getGPSData( - (query as DistanceSearch).from.text - ); + await ObjectManagers.getInstance().LocationManager.getGPSData( + (query as DistanceSearch).from.text + ); } return query; } private encapsulateAutoComplete( - values: string[], - type: SearchQueryTypes + values: string[], + type: SearchQueryTypes ): Array { const res: AutoCompleteItem[] = []; values.forEach((value): void => { diff --git a/test/TestHelper.ts b/test/TestHelper.ts index 0e3353f7..18d64cfa 100644 --- a/test/TestHelper.ts +++ b/test/TestHelper.ts @@ -46,6 +46,34 @@ export class TestHelper { return dir; } + + public static getBasePhotoEntry(dir: DirectoryPathDTO, name = 'base media.jpg'): PhotoEntity { + const sd = new MediaDimensionEntity(); + sd.height = 400; + sd.width = 200; + const m = new PhotoMetadataEntity(); + m.caption = null; + m.size = sd; + m.creationDate = 1656069387772; + m.fileSize = 123456789; + // m.rating = 0; no rating by default + + // TODO: remove when typeorm is fixed + m.duration = null; + m.bitRate = null; + + + const d = new PhotoEntity(); + d.name = name; + d.directory = (dir as any); + if ((dir as DirectoryBaseDTO).media) { + (dir as DirectoryBaseDTO).media.push(d); + (dir as DirectoryBaseDTO).mediaCount++; + } + d.metadata = m; + return d; + } + public static getPhotoEntry(dir: DirectoryPathDTO): PhotoEntity { const sd = new MediaDimensionEntity(); sd.height = 400; diff --git a/test/backend/DBTestHelper.ts b/test/backend/DBTestHelper.ts index 0f296933..ecfd0962 100644 --- a/test/backend/DBTestHelper.ts +++ b/test/backend/DBTestHelper.ts @@ -2,7 +2,7 @@ import {Config} from '../../src/common/config/private/Config'; import * as path from 'path'; import * as fs from 'fs'; import {SQLConnection} from '../../src/backend/model/database/SQLConnection'; -import {DatabaseType} from '../../src/common/config/private/PrivateConfig'; +import {DatabaseType, LogLevel} from '../../src/common/config/private/PrivateConfig'; import {ProjectPath} from '../../src/backend/ProjectPath'; import {DirectoryBaseDTO, ParentDirectoryDTO, SubDirectoryDTO} from '../../src/common/entities/DirectoryDTO'; import {ObjectManagers} from '../../src/backend/model/ObjectManagers'; @@ -129,7 +129,7 @@ export class DBTestHelper { const gm = new GalleryManagerTest(); const dir = await gm.getParentDirFromId(connection, - (await gm.getDirIdAndTime(connection, directory.name, path.join(path.dirname('.'), path.sep))).id); + (await gm.getDirIdAndTime(connection, directory.name, path.join(directory.path, path.sep))).id); const populateDir = async (d: DirectoryBaseDTO) => { for (let i = 0; i < d.directories.length; i++) { diff --git a/test/backend/unit/model/sql/SearchManager.spec.ts b/test/backend/unit/model/sql/SearchManager.spec.ts index 63935fe7..c4e93ecf 100644 --- a/test/backend/unit/model/sql/SearchManager.spec.ts +++ b/test/backend/unit/model/sql/SearchManager.spec.ts @@ -38,6 +38,7 @@ import {Config} from '../../../../../src/common/config/private/Config'; import {SearchQueryParser} from '../../../../../src/common/SearchQueryParser'; import {FileDTO} from '../../../../../src/common/entities/FileDTO'; import {SortByTypes} from '../../../../../src/common/entities/SortingMethods'; +import {LogLevel} from '../../../../../src/common/config/private/PrivateConfig'; // eslint-disable-next-line @typescript-eslint/no-var-requires const deepEqualInAnyOrder = require('deep-equal-in-any-order'); @@ -103,6 +104,8 @@ describe('SearchManager', (sqlHelper: DBTestHelper) => { let p2: PhotoDTO; let pFaceLess: PhotoDTO; let p4: PhotoDTO; +// let p5: PhotoDTO; + // let p6: PhotoDTO; let gpx: FileDTO; @@ -124,7 +127,6 @@ describe('SearchManager', (sqlHelper: DBTestHelper) => { pFaceLessTmp.metadata.creationDate = Date.now() - 60 * 60 * 24 * 32 * 1000; dir = await DBTestHelper.persistTestDir(directory); - subDir = dir.directories[0]; subDir2 = dir.directories[1]; p = (dir.media.filter(m => m.name === p.name)[0] as any); @@ -429,7 +431,6 @@ describe('SearchManager', (sqlHelper: DBTestHelper) => { }); - it('should minimum of', async () => { const sm = new SearchManager(); @@ -925,122 +926,340 @@ describe('SearchManager', (sqlHelper: DBTestHelper) => { } as SearchResultDTO)); }); + describe('search date pattern', async () => { + let p5: PhotoDTO; + let p6: PhotoDTO; + let p7: PhotoDTO; + let sm: SearchManager; - it('should search date pattern', async () => { - const sm = new SearchManager(); + before(async () => { + await sqlHelper.clearDB(); + await setUpSqlDB(); + p5 = TestHelper.getBasePhotoEntry(subDir2, 'p5-23h-ago.jpg'); + p5.metadata.creationDate = Date.now() - 60 * 60 * 23 * 1000; + p6 = TestHelper.getBasePhotoEntry(subDir2, 'p6-300d-ago.jpg'); + p6.metadata.creationDate = Date.now() - 60 * 60 * 24 * 300 * 1000; + p7 = TestHelper.getBasePhotoEntry(subDir2, 'p7-1y-1min-ago.jpg'); + const d = new Date(); + d.setUTCFullYear(d.getUTCFullYear() - 1); + d.setUTCMinutes(d.getUTCMinutes() - 1); + p7.metadata.creationDate = d.getTime(); - let query: DatePatternSearch = { - daysLength: 0, - frequency: DatePatternFrequency.every_year, - type: SearchQueryTypes.date_pattern - }; + subDir2 = await DBTestHelper.persistTestDir(subDir2) as any; + p4 = subDir2.media[0]; + p4.directory = subDir2; + p5 = subDir2.media[1]; + p5.directory = subDir2; + p6 = subDir2.media[2]; + p6.directory = subDir2; + p7 = subDir2.media[3]; + p7.directory = subDir2; + Config.Search.listDirectories = false; + Config.Search.listMetafiles = false; - expect(Utils.clone(await sm.search(query))) - .to.deep.equalInAnyOrder(removeDir({ - searchQuery: query, - directories: [], - media: [], - metaFile: [], - resultOverflow: false - } as SearchResultDTO)); + sm = new SearchManager(); + }); + + //TODO: this is flaky test for mysql + it('last-0-days:every-year', async () => { - query = { - daysLength: 1, - frequency: DatePatternFrequency.every_year, - type: SearchQueryTypes.date_pattern - } as DatePatternSearch; + let query: DatePatternSearch = { + daysLength: 0, + frequency: DatePatternFrequency.every_year, + type: SearchQueryTypes.date_pattern + }; - expect(Utils.clone(await sm.search(query))) - .to.deep.equalInAnyOrder(removeDir({ - searchQuery: query, - directories: [], - media: [p], - metaFile: [], - resultOverflow: false - } as SearchResultDTO)); + expect(Utils.clone(await sm.search(query))) + .to.deep.equalInAnyOrder(removeDir({ + searchQuery: query, + directories: [], + media: [p, p7], + metaFile: [], + resultOverflow: false + } as SearchResultDTO)); - query = { - daysLength: 2, - frequency: DatePatternFrequency.every_year, - type: SearchQueryTypes.date_pattern - } as DatePatternSearch; + query = { + daysLength: 0, + negate: true, + frequency: DatePatternFrequency.every_year, + type: SearchQueryTypes.date_pattern + }; - expect(Utils.clone(await sm.search(query))) - .to.deep.equalInAnyOrder(removeDir({ - searchQuery: query, - directories: [], - media: [p, p2, p4], - metaFile: [], - resultOverflow: false - } as SearchResultDTO)); + expect(Utils.clone(await sm.search(query))) + .to.deep.equalInAnyOrder(removeDir({ + searchQuery: query, + directories: [], + media: [p2, p4, pFaceLess, v, p5, p6], + metaFile: [], + resultOverflow: false + } as SearchResultDTO)); + + }); + + //TODO: this is flaky test for mysql + it('last-1-days:every-year', async () => { + let query: DatePatternSearch = { + daysLength: 1, + frequency: DatePatternFrequency.every_year, + type: SearchQueryTypes.date_pattern + } as DatePatternSearch; + + expect(Utils.clone(await sm.search(query))) + .to.deep.equalInAnyOrder(removeDir({ + searchQuery: query, + directories: [], + media: [p, p7], + metaFile: [], + resultOverflow: false + } as SearchResultDTO)); - query = { - daysLength: 1, - agoNumber: 10, - frequency: DatePatternFrequency.days_ago, - type: SearchQueryTypes.date_pattern - } as DatePatternSearch; + query = { + daysLength: 1, + negate: true, + frequency: DatePatternFrequency.every_year, + type: SearchQueryTypes.date_pattern + } as DatePatternSearch; - expect(Utils.clone(await sm.search(query))) - .to.deep.equalInAnyOrder(removeDir({ - searchQuery: query, - directories: [], - media: [], - metaFile: [], - resultOverflow: false - } as SearchResultDTO)); + expect(Utils.clone(await sm.search(query))) + .to.deep.equalInAnyOrder(removeDir({ + searchQuery: query, + directories: [], + media: [p2, p4, pFaceLess, v, p5, p6], + metaFile: [], + resultOverflow: false + } as SearchResultDTO)); - query = { - daysLength: 3, - agoNumber: 1, - frequency: DatePatternFrequency.months_ago, - type: SearchQueryTypes.date_pattern - } as DatePatternSearch; + }); + it('last-2-days:every-year', async () => { + let query = { + daysLength: 2, + frequency: DatePatternFrequency.every_year, + type: SearchQueryTypes.date_pattern + } as DatePatternSearch; - expect(Utils.clone(await sm.search(query))) - .to.deep.equalInAnyOrder(removeDir({ - searchQuery: query, - directories: [], - media: [pFaceLess], - metaFile: [], - resultOverflow: false - } as SearchResultDTO)); + expect(Utils.clone(await sm.search(query))) + .to.deep.equalInAnyOrder(removeDir({ + searchQuery: query, + directories: [], + media: [p, p2, p4, p5, p7], + metaFile: [], + resultOverflow: false + } as SearchResultDTO)); - query = { - daysLength: 366, - frequency: DatePatternFrequency.every_year, - type: SearchQueryTypes.date_pattern - } as DatePatternSearch; + query = { + daysLength: 2, + negate: true, + frequency: DatePatternFrequency.every_year, + type: SearchQueryTypes.date_pattern + } as DatePatternSearch; - expect(Utils.clone(await sm.search(query))) - .to.deep.equalInAnyOrder(removeDir({ - searchQuery: query, - directories: [], - media: [p, p2, p4, pFaceLess, v], - metaFile: [], - resultOverflow: false - } as SearchResultDTO)); + expect(Utils.clone(await sm.search(query))) + .to.deep.equalInAnyOrder(removeDir({ + searchQuery: query, + directories: [], + media: [v, pFaceLess, p6], + metaFile: [], + resultOverflow: false + } as SearchResultDTO)); - query = { - daysLength: 32, - frequency: DatePatternFrequency.every_month, - type: SearchQueryTypes.date_pattern - } as DatePatternSearch; + }); + it('last-1-days:10-days-ago', async () => { - expect(Utils.clone(await sm.search(query))) - .to.deep.equalInAnyOrder(removeDir({ - searchQuery: query, - directories: [], - media: [p, p2, p4, pFaceLess, v], - metaFile: [], - resultOverflow: false - } as SearchResultDTO)); + let query = { + daysLength: 1, + agoNumber: 10, + frequency: DatePatternFrequency.days_ago, + type: SearchQueryTypes.date_pattern + } as DatePatternSearch; + expect(Utils.clone(await sm.search(query))) + .to.deep.equalInAnyOrder(removeDir({ + searchQuery: query, + directories: [], + media: [], + metaFile: [], + resultOverflow: false + } as SearchResultDTO)); + + + query = { + daysLength: 1, + agoNumber: 10, + negate: true, + frequency: DatePatternFrequency.days_ago, + type: SearchQueryTypes.date_pattern + } as DatePatternSearch; + + expect(Utils.clone(await sm.search(query))) + .to.deep.equalInAnyOrder(removeDir({ + searchQuery: query, + directories: [], + media: [p, p2, p4, pFaceLess, v, p5, p6, p7], + metaFile: [], + resultOverflow: false + } as SearchResultDTO)); + + }); + it('last-3-days:1-month-ago', async () => { + let query = { + daysLength: 3, + agoNumber: 1, + frequency: DatePatternFrequency.months_ago, + type: SearchQueryTypes.date_pattern + } as DatePatternSearch; + + expect(Utils.clone(await sm.search(query))) + .to.deep.equalInAnyOrder(removeDir({ + searchQuery: query, + directories: [], + media: [pFaceLess], + metaFile: [], + resultOverflow: false + } as SearchResultDTO)); + + query = { + daysLength: 3, + agoNumber: 1, + negate: true, + frequency: DatePatternFrequency.months_ago, + type: SearchQueryTypes.date_pattern + } as DatePatternSearch; + + expect(Utils.clone(await sm.search(query))) + .to.deep.equalInAnyOrder(removeDir({ + searchQuery: query, + directories: [], + media: [p, p2, p4, v, p5, p6, p7], + metaFile: [], + resultOverflow: false + } as SearchResultDTO)); + + }); + + it('last-3-days:12-month-ago', async () => { + let query = { + daysLength: 3, + agoNumber: 12, + frequency: DatePatternFrequency.months_ago, + type: SearchQueryTypes.date_pattern + } as DatePatternSearch; + + expect(Utils.clone(await sm.search(query))) + .to.deep.equalInAnyOrder(removeDir({ + searchQuery: query, + directories: [], + media: [p4, p7], + metaFile: [], + resultOverflow: false + } as SearchResultDTO)); + + query = { + daysLength: 3, + agoNumber: 12, + negate: true, + frequency: DatePatternFrequency.months_ago, + type: SearchQueryTypes.date_pattern + } as DatePatternSearch; + + expect(Utils.clone(await sm.search(query))) + .to.deep.equalInAnyOrder(removeDir({ + searchQuery: query, + directories: [], + media: [p, p2, v, p5, p6, pFaceLess], + metaFile: [], + resultOverflow: false + } as SearchResultDTO)); + + }); + it('last-366-days:every-year', async () => { + let query = { + daysLength: 366, + frequency: DatePatternFrequency.every_year, + type: SearchQueryTypes.date_pattern + } as DatePatternSearch; + + expect(Utils.clone(await sm.search(query))) + .to.deep.equalInAnyOrder(removeDir({ + searchQuery: query, + directories: [], + media: [p, p2, p4, pFaceLess, v, p5, p6, p7], + metaFile: [], + resultOverflow: false + } as SearchResultDTO)); + + query = { + daysLength: 366, + negate: true, + frequency: DatePatternFrequency.every_year, + type: SearchQueryTypes.date_pattern + } as DatePatternSearch; + + expect(Utils.clone(await sm.search(query))) + .to.deep.equalInAnyOrder(removeDir({ + searchQuery: query, + directories: [], + media: [], + metaFile: [], + resultOverflow: false + } as SearchResultDTO)); + + }); + it('last-32-days:every-month', async () => { + const query = { + daysLength: 32, + frequency: DatePatternFrequency.every_month, + type: SearchQueryTypes.date_pattern + } as DatePatternSearch; + + expect(Utils.clone(await sm.search(query))) + .to.deep.equalInAnyOrder(removeDir({ + searchQuery: query, + directories: [], + media: [p, p2, p4, pFaceLess, v, p5, p6, p7], + metaFile: [], + resultOverflow: false + } as SearchResultDTO)); + + + }); + it('last-364-days:every-year', async () => { + let query = { + daysLength: 364, + frequency: DatePatternFrequency.every_year, + type: SearchQueryTypes.date_pattern + } as DatePatternSearch; + + expect(Utils.clone(await sm.search(query))) + .to.deep.equalInAnyOrder(removeDir({ + searchQuery: query, + directories: [], + media: [p, p2, p4, pFaceLess, v, p5, p6, p7], + metaFile: [], + resultOverflow: false + } as SearchResultDTO)); + + query = { + daysLength: 364, + negate: true, + frequency: DatePatternFrequency.every_year, + type: SearchQueryTypes.date_pattern + } as DatePatternSearch; + + expect(Utils.clone(await sm.search(query))) + .to.deep.equalInAnyOrder(removeDir({ + searchQuery: query, + directories: [], + media: [], + metaFile: [], + resultOverflow: false + } as SearchResultDTO)); + + + }); }); - it('should search rating', async () => { const sm = new SearchManager();