1
0
mirror of https://github.com/xuthus83/pigallery2.git synced 2025-01-14 14:43:17 +08:00

Implementing search query parsing and stringifying. #58

This commit is contained in:
Patrik J. Braun 2021-02-14 12:57:05 +01:00
parent 33b6fbf624
commit 47703b6b84
9 changed files with 651 additions and 272 deletions

View File

@ -11,18 +11,21 @@ import {Brackets, SelectQueryBuilder, WhereExpression} from 'typeorm';
import {Config} from '../../../../common/config/private/Config'; import {Config} from '../../../../common/config/private/Config';
import { import {
ANDSearchQuery, ANDSearchQuery,
DateSearch,
DistanceSearch, DistanceSearch,
FromDateSearch,
MaxRatingSearch,
MaxResolutionSearch,
MinRatingSearch,
MinResolutionSearch,
OrientationSearch, OrientationSearch,
ORSearchQuery, ORSearchQuery,
RatingSearch,
ResolutionSearch,
SearchListQuery, SearchListQuery,
SearchQueryDTO, SearchQueryDTO,
SearchQueryTypes, SearchQueryTypes,
SomeOfSearchQuery, SomeOfSearchQuery,
TextSearch, TextSearch,
TextSearchQueryMatchTypes TextSearchQueryMatchTypes,
ToDateSearch
} from '../../../../common/entities/SearchQueryDTO'; } from '../../../../common/entities/SearchQueryDTO';
import {GalleryManager} from './GalleryManager'; import {GalleryManager} from './GalleryManager';
import {ObjectManagers} from '../../ObjectManagers'; import {ObjectManagers} from '../../ObjectManagers';
@ -266,75 +269,100 @@ export class SearchManager implements ISearchManager {
return q; return q;
}); });
case SearchQueryTypes.date: case SearchQueryTypes.from_date:
return new Brackets(q => { return new Brackets(q => {
if (typeof (<DateSearch>query).before === 'undefined' && typeof (<DateSearch>query).after === 'undefined') { if (typeof (<FromDateSearch>query).value === 'undefined') {
throw new Error('Invalid search query: Date Query should contain before or after value'); throw new Error('Invalid search query: Date Query should contain from value');
} }
const whereFN = (<TextSearch>query).negate ? 'orWhere' : 'andWhere'; const whereFN = (<TextSearch>query).negate ? 'orWhere' : 'andWhere';
const relation = (<TextSearch>query).negate ? '<' : '>='; const relation = (<TextSearch>query).negate ? '<' : '>=';
const relationRev = (<TextSearch>query).negate ? '>' : '<='; const relationRev = (<TextSearch>query).negate ? '>' : '<=';
if (typeof (<DateSearch>query).after !== 'undefined') { const textParam: any = {};
const textParam: any = {}; textParam['from' + paramCounter.value] = (<FromDateSearch>query).value;
textParam['after' + paramCounter.value] = (<DateSearch>query).after; q.where(`media.metadata.creationDate ${relation} :from${paramCounter.value}`, textParam);
q.where(`media.metadata.creationDate ${relation} :after${paramCounter.value}`, textParam);
}
if (typeof (<DateSearch>query).before !== 'undefined') {
const textParam: any = {};
textParam['before' + paramCounter.value] = (<DateSearch>query).before;
q[whereFN](`media.metadata.creationDate ${relationRev} :before${paramCounter.value}`, textParam);
}
paramCounter.value++; paramCounter.value++;
return q; return q;
}); });
case SearchQueryTypes.rating: case SearchQueryTypes.to_date:
return new Brackets(q => { return new Brackets(q => {
if (typeof (<RatingSearch>query).min === 'undefined' && typeof (<RatingSearch>query).max === 'undefined') { if (typeof (<ToDateSearch>query).value === 'undefined') {
throw new Error('Invalid search query: Rating Query should contain min or max value'); throw new Error('Invalid search query: Date Query should contain to value');
}
const relation = (<TextSearch>query).negate ? '>' : '<=';
const textParam: any = {};
textParam['to' + paramCounter.value] = (<ToDateSearch>query).value;
q.where(`media.metadata.creationDate ${relation} :to${paramCounter.value}`, textParam);
paramCounter.value++;
return q;
});
case SearchQueryTypes.min_rating:
return new Brackets(q => {
if (typeof (<MinRatingSearch>query).value === 'undefined') {
throw new Error('Invalid search query: Rating Query should contain minvalue');
} }
const whereFN = (<TextSearch>query).negate ? 'orWhere' : 'andWhere';
const relation = (<TextSearch>query).negate ? '<' : '>='; const relation = (<TextSearch>query).negate ? '<' : '>=';
const relationRev = (<TextSearch>query).negate ? '>' : '<=';
if (typeof (<RatingSearch>query).min !== 'undefined') { const textParam: any = {};
const textParam: any = {}; textParam['min' + paramCounter.value] = (<MinRatingSearch>query).value;
textParam['min' + paramCounter.value] = (<RatingSearch>query).min; q.where(`media.metadata.rating ${relation} :min${paramCounter.value}`, textParam);
q.where(`media.metadata.rating ${relation} :min${paramCounter.value}`, textParam);
paramCounter.value++;
return q;
});
case SearchQueryTypes.max_rating:
return new Brackets(q => {
if (typeof (<MaxRatingSearch>query).value === 'undefined') {
throw new Error('Invalid search query: Rating Query should contain max value');
} }
if (typeof (<RatingSearch>query).max !== 'undefined') { const relation = (<TextSearch>query).negate ? '>' : '<=';
if (typeof (<MaxRatingSearch>query).value !== 'undefined') {
const textParam: any = {}; const textParam: any = {};
textParam['max' + paramCounter.value] = (<RatingSearch>query).max; textParam['max' + paramCounter.value] = (<MaxRatingSearch>query).value;
q[whereFN](`media.metadata.rating ${relationRev} :max${paramCounter.value}`, textParam); q.where(`media.metadata.rating ${relation} :max${paramCounter.value}`, textParam);
} }
paramCounter.value++; paramCounter.value++;
return q; return q;
}); });
case SearchQueryTypes.resolution: case SearchQueryTypes.min_resolution:
return new Brackets(q => { return new Brackets(q => {
if (typeof (<ResolutionSearch>query).min === 'undefined' && typeof (<ResolutionSearch>query).max === 'undefined') { if (typeof (<MinResolutionSearch>query).value === 'undefined') {
throw new Error('Invalid search query: Resolution Query should contain min value');
}
const relation = (<TextSearch>query).negate ? '<' : '>=';
const textParam: any = {};
textParam['min' + paramCounter.value] = (<MinResolutionSearch>query).value * 1000 * 1000;
q.where(`media.metadata.size.width * media.metadata.size.height ${relation} :min${paramCounter.value}`, textParam);
paramCounter.value++;
return q;
});
case SearchQueryTypes.max_resolution:
return new Brackets(q => {
if (typeof (<MaxResolutionSearch>query).value === 'undefined') {
throw new Error('Invalid search query: Rating Query should contain min or max value'); throw new Error('Invalid search query: Rating Query should contain min or max value');
} }
const whereFN = (<TextSearch>query).negate ? 'orWhere' : 'andWhere'; const relation = (<TextSearch>query).negate ? '>' : '<=';
const relation = (<TextSearch>query).negate ? '<' : '>=';
const relationRev = (<TextSearch>query).negate ? '>' : '<='; const textParam: any = {};
if (typeof (<ResolutionSearch>query).min !== 'undefined') { textParam['max' + paramCounter.value] = (<MaxResolutionSearch>query).value * 1000 * 1000;
const textParam: any = {}; q.where(`media.metadata.size.width * media.metadata.size.height ${relation} :max${paramCounter.value}`, textParam);
textParam['min' + paramCounter.value] = (<RatingSearch>query).min * 1000 * 1000;
q.where(`media.metadata.size.width * media.metadata.size.height ${relation} :min${paramCounter.value}`, textParam);
}
if (typeof (<ResolutionSearch>query).max !== 'undefined') {
const textParam: any = {};
textParam['max' + paramCounter.value] = (<RatingSearch>query).max * 1000 * 1000;
q[whereFN](`media.metadata.size.width * media.metadata.size.height ${relationRev} :max${paramCounter.value}`, textParam);
}
paramCounter.value++; paramCounter.value++;
return q; return q;
}); });

View File

@ -54,6 +54,11 @@ export class Utils {
return ret.substr(ret.length - length); return ret.substr(ret.length - length);
} }
/**
* Checks if the two input (let them be objects or arrays or just primitives) are equal
* @param object
* @param filter
*/
static equalsFilter(object: any, filter: any): boolean { static equalsFilter(object: any, filter: any): boolean {
if (typeof filter !== 'object' || filter == null) { if (typeof filter !== 'object' || filter == null) {
return object === filter; return object === filter;

View File

@ -1,13 +1,19 @@
import {GPSMetadata} from './PhotoDTO'; import {GPSMetadata} from './PhotoDTO';
import {Utils} from '../Utils';
export enum SearchQueryTypes { export enum SearchQueryTypes {
AND = 1, OR, SOME_OF, AND = 1, OR, SOME_OF,
// non-text metadata // non-text metadata
date = 10, // |- range types
rating, from_date = 10,
to_date,
min_rating,
max_rating,
min_resolution,
max_resolution,
distance, distance,
resolution,
orientation, orientation,
// TEXT search types // TEXT search types
@ -34,24 +40,33 @@ export const TextSearchQueryTypes = [
SearchQueryTypes.person, SearchQueryTypes.person,
SearchQueryTypes.position, SearchQueryTypes.position,
]; ];
export const MinRangeSearchQueryTypes = [
SearchQueryTypes.from_date,
SearchQueryTypes.min_rating,
SearchQueryTypes.min_resolution,
];
export const MaxRangeSearchQueryTypes = [
SearchQueryTypes.to_date,
SearchQueryTypes.max_rating,
SearchQueryTypes.max_resolution
];
export const RangeSearchQueryTypes = MinRangeSearchQueryTypes.concat(MaxRangeSearchQueryTypes);
export const MetadataSearchQueryTypes = [ export const MetadataSearchQueryTypes = [
// non-text metadata
SearchQueryTypes.date,
SearchQueryTypes.rating,
SearchQueryTypes.distance, SearchQueryTypes.distance,
SearchQueryTypes.resolution, SearchQueryTypes.orientation
SearchQueryTypes.orientation, ].concat(RangeSearchQueryTypes)
.concat(TextSearchQueryTypes);
// TEXT search types export const rangedTypePairs: any = {};
SearchQueryTypes.any_text, rangedTypePairs[SearchQueryTypes.from_date] = SearchQueryTypes.to_date;
SearchQueryTypes.caption, rangedTypePairs[SearchQueryTypes.min_rating] = SearchQueryTypes.max_rating;
SearchQueryTypes.directory, rangedTypePairs[SearchQueryTypes.min_resolution] = SearchQueryTypes.max_resolution;
SearchQueryTypes.file_name, // add the other direction too
SearchQueryTypes.keyword, for (const key of Object.keys(rangedTypePairs)) {
SearchQueryTypes.person, rangedTypePairs[rangedTypePairs[key]] = key;
SearchQueryTypes.position, }
];
export enum TextSearchQueryMatchTypes { export enum TextSearchQueryMatchTypes {
exact_match = 1, like = 2 exact_match = 1, like = 2
@ -59,6 +74,12 @@ export enum TextSearchQueryMatchTypes {
export namespace SearchQueryDTO { export namespace SearchQueryDTO {
export const getRangedQueryPair = (type: SearchQueryTypes): SearchQueryTypes => {
if (rangedTypePairs[type]) {
return rangedTypePairs[type];
}
throw new Error('Unknown ranged type');
};
export const negate = (query: SearchQueryDTO): SearchQueryDTO => { export const negate = (query: SearchQueryDTO): SearchQueryDTO => {
switch (query.type) { switch (query.type) {
case SearchQueryTypes.AND: case SearchQueryTypes.AND:
@ -74,9 +95,12 @@ export namespace SearchQueryDTO {
(<OrientationSearch>query).landscape = !(<OrientationSearch>query).landscape; (<OrientationSearch>query).landscape = !(<OrientationSearch>query).landscape;
return query; return query;
case SearchQueryTypes.date: case SearchQueryTypes.from_date:
case SearchQueryTypes.rating: case SearchQueryTypes.to_date:
case SearchQueryTypes.resolution: case SearchQueryTypes.min_rating:
case SearchQueryTypes.max_rating:
case SearchQueryTypes.min_resolution:
case SearchQueryTypes.max_resolution:
case SearchQueryTypes.distance: case SearchQueryTypes.distance:
case SearchQueryTypes.any_text: case SearchQueryTypes.any_text:
case SearchQueryTypes.person: case SearchQueryTypes.person:
@ -95,60 +119,211 @@ export namespace SearchQueryDTO {
throw new Error('Unknown type' + query.type); throw new Error('Unknown type' + query.type);
} }
}; };
export const parse = (str: string): SearchQueryDTO => {
console.log(str);
str = str.replace(/\s\s+/g, ' ') // remove double spaces
.replace(/:\s+/g, ':').replace(/\)(?=\S)/g, ') ').trim();
if (str.charAt(0) === '(' && str.charAt(str.length - 1) === ')') {
str = str.slice(1, str.length - 1);
}
const fistNonBRSpace = () => {
const bracketIn = [];
for (let i = 0; i < str.length; ++i) {
if (str.charAt(i) === '(') {
bracketIn.push(i);
continue;
}
if (str.charAt(i) === ')') {
bracketIn.pop();
continue;
}
if (bracketIn.length === 0 && str.charAt(i) === ' ') {
return i;
}
}
return str.length - 1;
};
// tokenize
const tokenEnd = fistNonBRSpace();
if (tokenEnd !== str.length - 1) {
if (str.startsWith(' and', tokenEnd)) {
return <ANDSearchQuery>{
type: SearchQueryTypes.AND,
list: [SearchQueryDTO.parse(str.slice(0, tokenEnd)), // trim brackets
SearchQueryDTO.parse(str.slice(tokenEnd + 4))]
};
} else {
let padding = 0;
if (str.startsWith(' or', tokenEnd)) {
padding = 3;
}
return <ORSearchQuery>{
type: SearchQueryTypes.OR,
list: [SearchQueryDTO.parse(str.slice(0, tokenEnd)), // trim brackets
SearchQueryDTO.parse(str.slice(tokenEnd + padding))]
};
}
}
if (str.startsWith('some-of:') ||
new RegExp(/^\d*-of:/).test(str)) {
const prefix = str.startsWith('some-of:') ? 'some-of:' : new RegExp(/^\d*-of:/).exec(str)[0];
let tmpList: any = SearchQueryDTO.parse(str.slice(prefix.length + 1, -1)); // trim brackets
const unfoldList = (q: SearchListQuery): SearchQueryDTO[] => {
if (q.list) {
return [].concat.apply([], q.list.map(e => unfoldList(<any>e))); // flatten array
}
return [q];
};
tmpList = unfoldList(<SearchListQuery>tmpList);
const ret = <SomeOfSearchQuery>{
type: SearchQueryTypes.SOME_OF,
list: tmpList
};
if (new RegExp(/^\d*-of:/).test(str)) {
ret.min = parseInt(new RegExp(/^\d*/).exec(str)[0], 10);
}
return ret;
}
if (str.startsWith('from:')) {
return <FromDateSearch>{
type: SearchQueryTypes.from_date,
value: Date.parse(str.slice('from:'.length + 1, str.length - 1))
};
}
if (str.startsWith('to:')) {
return <ToDateSearch>{
type: SearchQueryTypes.to_date,
value: Date.parse(str.slice('to:'.length + 1, str.length - 1))
};
}
if (str.startsWith('min-rating:')) {
return <MinRatingSearch>{
type: SearchQueryTypes.min_rating,
value: parseInt(str.slice('min-rating:'.length), 10)
};
}
if (str.startsWith('max-rating:')) {
return <MaxRatingSearch>{
type: SearchQueryTypes.max_rating,
value: parseInt(str.slice('max-rating:'.length), 10)
};
}
if (str.startsWith('min-resolution:')) {
return <MinResolutionSearch>{
type: SearchQueryTypes.min_resolution,
value: parseInt(str.slice('min-resolution:'.length), 10)
};
}
if (str.startsWith('max-resolution:')) {
return <MaxResolutionSearch>{
type: SearchQueryTypes.max_resolution,
value: parseInt(str.slice('max-resolution:'.length), 10)
};
}
if (new RegExp(/^\d*-km-from:/).test(str)) {
let from = str.slice(new RegExp(/^\d*-km-from:/).exec(str)[0].length);
if (from.charAt(0) === '(' && from.charAt(from.length - 1) === ')') {
from = from.slice(1, from.length - 1);
}
return <DistanceSearch>{
type: SearchQueryTypes.distance,
distance: parseInt(new RegExp(/^\d*/).exec(str)[0], 10),
from: {text: from}
};
}
if (str.startsWith('orientation:')) {
return <OrientationSearch>{
type: SearchQueryTypes.orientation,
landscape: str.slice('orientation:'.length) === 'landscape'
};
}
// parse text search
const tmp = TextSearchQueryTypes.map(type => ({
key: SearchQueryTypes[type] + ':',
queryTemplate: <TextSearch>{type: type, text: ''}
}));
for (let i = 0; i < tmp.length; ++i) {
if (str.startsWith(tmp[i].key)) {
const ret: TextSearch = Utils.clone(tmp[i].queryTemplate);
if (str.charAt(tmp[i].key.length) === '"' && str.charAt(str.length - 1) === '"') {
ret.text = str.slice(tmp[i].key.length + 1, str.length - 1);
ret.matchType = TextSearchQueryMatchTypes.exact_match;
} else if (str.charAt(tmp[i].key.length) === '(' && str.charAt(str.length - 1) === ')') {
ret.text = str.slice(tmp[i].key.length + 1, str.length - 1);
} else {
ret.text = str.slice(tmp[i].key.length);
}
return ret;
}
}
return <TextSearch>{type: SearchQueryTypes.any_text, text: str};
};
export const stringify = (query: SearchQueryDTO): string => { export const stringify = (query: SearchQueryDTO): string => {
if (!query || !query.type) { if (!query || !query.type) {
return ''; return '';
} }
switch (query.type) { switch (query.type) {
case SearchQueryTypes.AND: case SearchQueryTypes.AND:
return '(' + (<SearchListQuery>query).list.map(q => SearchQueryDTO.stringify(q)).join(' AND ') + ')'; return '(' + (<SearchListQuery>query).list.map(q => SearchQueryDTO.stringify(q)).join(' and ') + ')';
case SearchQueryTypes.OR: case SearchQueryTypes.OR:
return '(' + (<SearchListQuery>query).list.map(q => SearchQueryDTO.stringify(q)).join(' OR ') + ')'; return '(' + (<SearchListQuery>query).list.map(q => SearchQueryDTO.stringify(q)).join(' or ') + ')';
case SearchQueryTypes.SOME_OF: case SearchQueryTypes.SOME_OF:
if ((<SomeOfSearchQuery>query).min) { if ((<SomeOfSearchQuery>query).min) {
return (<SomeOfSearchQuery>query).min + ' OF: (' + return (<SomeOfSearchQuery>query).min + '-of:(' +
(<SearchListQuery>query).list.map(q => SearchQueryDTO.stringify(q)).join(', ') + ')'; (<SearchListQuery>query).list.map(q => SearchQueryDTO.stringify(q)).join(' ') + ')';
} }
return 'SOME OF: (' + return 'some-of:(' +
(<SearchListQuery>query).list.map(q => SearchQueryDTO.stringify(q)).join(', ') + ')'; (<SearchListQuery>query).list.map(q => SearchQueryDTO.stringify(q)).join(' ') + ')';
case SearchQueryTypes.orientation: case SearchQueryTypes.orientation:
return 'orientation: ' + (<OrientationSearch>query).landscape ? 'landscape' : 'portrait'; return 'orientation:' + ((<OrientationSearch>query).landscape ? 'landscape' : 'portrait');
case SearchQueryTypes.date: case SearchQueryTypes.from_date:
let ret = ''; if (!(<FromDateSearch>query).value) {
if ((<DateSearch>query).after) { return '';
ret += 'from: ' + (<DateSearch>query).after;
} }
if ((<DateSearch>query).before) { return 'from:(' + new Date((<FromDateSearch>query).value).toLocaleDateString() + ')'.trim();
ret += ' to: ' + (<DateSearch>query).before; case SearchQueryTypes.to_date:
if (!(<ToDateSearch>query).value) {
return '';
} }
return ret.trim(); return 'to:(' + new Date((<ToDateSearch>query).value).toLocaleDateString() + ')'.trim();
case SearchQueryTypes.rating: case SearchQueryTypes.min_rating:
let rating = ''; return 'min-rating:' + (isNaN((<RangeSearch>query).value) ? '' : (<RangeSearch>query).value);
if ((<RatingSearch>query).min) { case SearchQueryTypes.max_rating:
rating += 'min-rating: ' + (<RatingSearch>query).min; return 'max-rating:' + (isNaN((<RangeSearch>query).value) ? '' : (<RangeSearch>query).value);
} case SearchQueryTypes.min_resolution:
if ((<RatingSearch>query).max) { return 'min-resolution:' + (isNaN((<RangeSearch>query).value) ? '' : (<RangeSearch>query).value);
rating += ' max-rating: ' + (<RatingSearch>query).max; case SearchQueryTypes.max_resolution:
} return 'max-resolution:' + (isNaN((<RangeSearch>query).value) ? '' : (<RangeSearch>query).value);
return rating.trim();
case SearchQueryTypes.resolution:
let res = '';
if ((<ResolutionSearch>query).min) {
res += 'min-resolution: ' + (<ResolutionSearch>query).min;
}
if ((<RatingSearch>query).max) {
res += ' max-resolution: ' + (<ResolutionSearch>query).max;
}
return res.trim();
case SearchQueryTypes.distance: case SearchQueryTypes.distance:
return (<DistanceSearch>query).distance + ' km from: ' + (<DistanceSearch>query).from.text; if ((<DistanceSearch>query).from.text.indexOf(' ') !== -1) {
return (<DistanceSearch>query).distance + '-km-from:(' + (<DistanceSearch>query).from.text + ')';
}
return (<DistanceSearch>query).distance + '-km-from:' + (<DistanceSearch>query).from.text;
case SearchQueryTypes.any_text: case SearchQueryTypes.any_text:
if ((<TextSearch>query).matchType === TextSearchQueryMatchTypes.exact_match) {
return '"' + (<TextSearch>query).text + '"';
} else if ((<TextSearch>query).text.indexOf(' ') !== -1) {
return '(' + (<TextSearch>query).text + ')';
}
return (<TextSearch>query).text; return (<TextSearch>query).text;
case SearchQueryTypes.person: case SearchQueryTypes.person:
@ -160,6 +335,12 @@ export namespace SearchQueryDTO {
if (!(<TextSearch>query).text) { if (!(<TextSearch>query).text) {
return ''; return '';
} }
if ((<TextSearch>query).matchType === TextSearchQueryMatchTypes.exact_match) {
return SearchQueryTypes[query.type] + ':"' + (<TextSearch>query).text + '"';
} else if ((<TextSearch>query).text.indexOf(' ') !== -1) {
return SearchQueryTypes[query.type] + ':(' + (<TextSearch>query).text + ')';
}
return SearchQueryTypes[query.type] + ':' + (<TextSearch>query).text; return SearchQueryTypes[query.type] + ':' + (<TextSearch>query).text;
default: default:
@ -177,11 +358,6 @@ export interface NegatableSearchQuery extends SearchQueryDTO {
negate?: boolean; // if true negates the expression negate?: boolean; // if true negates the expression
} }
export interface RangeSearchQuery extends SearchQueryDTO {
min?: number;
max?: number;
}
export interface SearchListQuery extends SearchQueryDTO { export interface SearchListQuery extends SearchQueryDTO {
list: SearchQueryDTO[]; list: SearchQueryDTO[];
} }
@ -225,22 +401,42 @@ export interface DistanceSearch extends NegatableSearchQuery {
} }
export interface DateSearch extends NegatableSearchQuery { export interface RangeSearch extends NegatableSearchQuery {
type: SearchQueryTypes.date; value: number;
after?: number;
before?: number;
} }
export interface RatingSearch extends RangeSearchQuery, NegatableSearchQuery { export interface RangeSearchGroup extends ANDSearchQuery {
type: SearchQueryTypes.rating; list: RangeSearch[];
min?: number;
max?: number;
} }
export interface ResolutionSearch extends RangeSearchQuery, NegatableSearchQuery { export interface FromDateSearch extends RangeSearch {
type: SearchQueryTypes.resolution; type: SearchQueryTypes.from_date;
min?: number; // in megapixels value: number;
max?: number; // in megapixels }
export interface ToDateSearch extends RangeSearch {
type: SearchQueryTypes.to_date;
value: number;
}
export interface MinRatingSearch extends RangeSearch {
type: SearchQueryTypes.min_rating;
value: number;
}
export interface MaxRatingSearch extends RangeSearch {
type: SearchQueryTypes.max_rating;
value: number;
}
export interface MinResolutionSearch extends RangeSearch {
type: SearchQueryTypes.min_resolution;
value: number; // in megapixels
}
export interface MaxResolutionSearch extends RangeSearch {
type: SearchQueryTypes.max_resolution;
value: number; // in megapixels
} }
export interface OrientationSearch { export interface OrientationSearch {

View File

@ -1,6 +1,6 @@
<div class="row mt-1 mb-1" *ngIf="queryEntry"> <div class="row mt-1 mb-1" *ngIf="queryEntry">
<ng-container *ngIf="IsListQuery"> <ng-container *ngIf="IsListQuery">
<div class="input-group col-md-2"> <div class="input-group col-md-3">
<select <select
id="listSearchType" id="listSearchType"
name="listSearchType" name="listSearchType"
@ -24,20 +24,22 @@
name="someOfMinValue" name="someOfMinValue"
id="someOfMinValue" id="someOfMinValue"
required="required"> required="required">
<div class="col-md-3"></div> <div class="col-md-2"></div>
</ng-container> </ng-container>
<ng-container *ngIf="queryEntry.type != SearchQueryTypes.SOME_OF"> <ng-container *ngIf="queryEntry.type != SearchQueryTypes.SOME_OF">
<div class="col-md-9"></div> <div class="col-md-8"></div>
</ng-container> </ng-container>
<button [ngClass]="true? 'btn-danger':'btn-secondary'"
class="btn float-right col-md-1"> <button [ngClass]="'btn-danger'"
class="btn float-right col-md-1"
(click)="deleteItem()">
<span class="oi oi-trash" aria-hidden="true" aria-label="Delete"></span> <span class="oi oi-trash" aria-hidden="true" aria-label="Delete"></span>
</button> </button>
<div class="container query-list"> <div class="container query-list">
<app-gallery-search-query-entry *ngFor="let sq of AsListQuery.list; index as i" <app-gallery-search-query-entry *ngFor="let sq of AsListQuery.list; index as i"
[(ngModel)]="sq" [(ngModel)]="sq"
(delete)="itemDeleted(i)"> (delete)="itemDeleted(i)">
</app-gallery-search-query-entry> </app-gallery-search-query-entry>
</div> </div>
<button class="btn btn-primary mx-auto" (click)="addQuery()"> <button class="btn btn-primary mx-auto" (click)="addQuery()">
@ -45,7 +47,7 @@
</button> </button>
</ng-container> </ng-container>
<ng-container *ngIf="!IsListQuery"> <ng-container *ngIf="!IsListQuery">
<div class="input-group col-md-2"> <div class="input-group col-md-3">
<select <select
id="searchType" id="searchType"
name="searchType" name="searchType"
@ -56,18 +58,19 @@
</option> </option>
</select> </select>
</div> </div>
<div class="input-group col-md-9" *ngIf="IsTextQuery"> <div class="input-group col-md-8" *ngIf="IsTextQuery">
<input <input
id="searchField" id="searchField"
name="searchField" name="searchField"
placeholder="link" placeholder="Search text"
class="form-control input-md" class="form-control input-md"
[(ngModel)]="AsTextQuery.text" [(ngModel)]="AsTextQuery.text"
(change)="onChange(queryEntry)"
(ngModelChange)="onChange(queryEntry)" (ngModelChange)="onChange(queryEntry)"
type="text"/> type="text"/>
</div> </div>
<ng-container [ngSwitch]="queryEntry.type"> <ng-container [ngSwitch]="queryEntry.type">
<div *ngSwitchCase="SearchQueryTypes.distance" class="col-md-9 d-flex"> <div *ngSwitchCase="SearchQueryTypes.distance" class="col-md-8 d-flex">
<div class="input-group col-md-4"> <div class="input-group col-md-4">
<input type="number" class="form-control" placeholder="1" <input type="number" class="form-control" placeholder="1"
id="distance" id="distance"
@ -93,98 +96,94 @@
type="text"> type="text">
</div> </div>
</div> </div>
<div *ngSwitchCase="SearchQueryTypes.date" class="col-md-9 d-flex"> <!-- Range Search Query -->
<div class="input-group col-md-6"> <div *ngSwitchCase="SearchQueryTypes.from_date" class="col-md-8 input-group ">
<label class="col-md-4 control-label" for="maxResolution">From:</label> <label class="col-md-4 control-label" for="from_date">From:</label>
<input id="afterDate" <input id="from_date"
name="afterDate" name="from_date"
title="After" title="From date"
i18n-title i18n-title
class="form-control input-md" [ngModel]="AsRangeQuery.value | date:'yyyy-MM-dd'"
[(ngModel)]="AsDateQuery.after" (ngModelChange)="AsRangeQuery.value = $event; onChange(queryEntry) "
(ngModelChange)="onChange(queryEntry)" [value]="AsRangeQuery.value | date:'yyyy-MM-dd'" #from_date="ngModel"
type="date"> class="form-control input-md"
</div> type="date">
<div class="input-group col-md-6"> </div>
<label class="col-md-4 control-label" for="maxResolution">To:</label> <div *ngSwitchCase="SearchQueryTypes.to_date" class="col-md-8 input-group">
<input id="beforeDate" <label class="col-md-4 control-label" for="to_date">To:</label>
name="beforeDate" <input id="to_date"
title="Before" name="to_date"
i18n-title title="To date"
[(ngModel)]="AsDateQuery.before" i18n-title
(ngModelChange)="onChange(queryEntry)" [ngModel]="AsRangeQuery.value | date:'yyyy-MM-dd'"
class="form-control input-md" (ngModelChange)="AsRangeQuery.value = $event; onChange(queryEntry) "
type="date"> [value]="AsRangeQuery.value | date:'yyyy-MM-dd'" #to_date="ngModel"
class="form-control input-md"
type="date">
</div>
<div *ngSwitchCase="SearchQueryTypes.min_rating" class="col-md-8 input-group">
<label class="col-md-4 control-label" for="maxResolution">Min:</label>
<input id="minRating"
name="minRating"
title="Minimum Rating"
placeholder="0"
i18n-title
min="0"
max="5"
class="form-control input-md"
[(ngModel)]="AsRangeQuery.value"
(ngModelChange)="onChange(queryEntry)"
type="number">
</div>
<div *ngSwitchCase="SearchQueryTypes.max_rating" class="col-md-8 input-group">
<label class="col-md-4 control-label" for="maxResolution">Max:</label>
<input id="maxRating"
name="maxRating"
title="Maximum Rating"
placeholder="5"
i18n-title
min="0"
max="5"
class="form-control input-md"
[(ngModel)]="AsRangeQuery.value"
(ngModelChange)="onChange(queryEntry)"
type="number">
</div>
<div *ngSwitchCase="SearchQueryTypes.min_resolution" class="col-md-8 input-group">
<label class="col-md-4 control-label" for="maxResolution">Min:</label>
<input id="minResolution"
name="minResolution"
title="Minimum Rating"
placeholder="0"
i18n-title
min="0"
class="form-control input-md"
[(ngModel)]="AsRangeQuery.value"
(ngModelChange)="onChange(queryEntry)"
type="number">
<div class="input-group-append">
<span class="input-group-text">Mpx</span>
</div> </div>
</div> </div>
<div *ngSwitchCase="SearchQueryTypes.rating" class="col-md-9 d-flex">
<div class="input-group col-md-6">
<label class="col-md-4 control-label" for="maxResolution">Min:</label>
<input id="minRating"
name="minRating"
title="Minimum Rating"
placeholder="0"
i18n-title
min="0"
[max]="AsRatingQuery.max || 5"
class="form-control input-md"
[(ngModel)]="AsRatingQuery.min"
(ngModelChange)="onChange(queryEntry)"
type="number">
</div>
<div class="input-group col-md-6"> <div *ngSwitchCase="SearchQueryTypes.max_resolution" class="col-md-8 input-group">
<label class="col-md-4 control-label" for="maxResolution">Max:</label> <label class="col-md-4 control-label" for="maxResolution">Max:</label>
<input id="maxRating" <input id="maxResolution"
name="maxRating" name="maxResolution"
title="Maximum Rating" title="Maximum Rating"
placeholder="5" placeholder="5"
i18n-title i18n-title
[min]="AsRatingQuery.min || 0" [min]="0"
max="5" class="form-control input-md"
class="form-control input-md" [(ngModel)]="AsRangeQuery.value"
[(ngModel)]="AsRatingQuery.max" (ngModelChange)="onChange(queryEntry)"
(ngModelChange)="onChange(queryEntry)" type="number">
type="number"> <div class="input-group-append">
<span class="input-group-text">Mpx</span>
</div> </div>
</div> </div>
<div *ngSwitchCase="SearchQueryTypes.resolution" class="col-md-9 d-flex"> <div *ngSwitchCase="SearchQueryTypes.orientation" class="col-md-8 d-flex">
<div class="input-group col-md-6">
<label class="col-md-4 control-label" for="maxResolution">Min:</label>
<input id="minResolution"
name="minResolution"
title="Minimum Rating"
placeholder="0"
i18n-title
min="0"
class="form-control input-md"
[(ngModel)]="AsResolutionQuery.min"
(ngModelChange)="onChange(queryEntry)"
type="number">
<div class="input-group-append">
<span class="input-group-text">Mpx</span>
</div>
</div>
<div class="input-group col-md-6">
<label class="col-md-4 control-label" for="maxResolution">Max:</label>
<input id="maxResolution"
name="maxResolution"
title="Maximum Rating"
placeholder="5"
i18n-title
[min]="AsResolutionQuery.min || 0"
class="form-control input-md"
[(ngModel)]="AsResolutionQuery.max"
(ngModelChange)="onChange(queryEntry)"
type="number">
<div class="input-group-append">
<span class="input-group-text">Mpx</span>
</div>
</div>
</div>
<div *ngSwitchCase="SearchQueryTypes.orientation" class="col-md-9 d-flex">
<div class="input-group col-md-6"> <div class="input-group col-md-6">
<bSwitch <bSwitch
class="switch" class="switch"
@ -206,11 +205,11 @@
</div> </div>
</div> </div>
</ng-container> </ng-container>
<button [ngClass]="'btn-danger'"
<button [ngClass]="true? 'btn-danger':'btn-secondary'"
class="btn float-right col-md-1" class="btn float-right col-md-1"
(click)="deleteItem()"> (click)="deleteItem()">
<span class="oi oi-trash" aria-hidden="true" aria-label="Delete"></span> <span class="oi oi-trash" aria-hidden="true" aria-label="Delete"></span>
</button> </button>
</ng-container> </ng-container>
</div> </div>

View File

@ -1,11 +1,10 @@
import {Component, EventEmitter, forwardRef, OnChanges, Output} from '@angular/core'; import {Component, EventEmitter, forwardRef, OnChanges, Output} from '@angular/core';
import { import {
DateSearch,
DistanceSearch, DistanceSearch,
ListSearchQueryTypes, ListSearchQueryTypes,
OrientationSearch, OrientationSearch,
RatingSearch, RangeSearch,
ResolutionSearch, RangeSearchQueryTypes,
SearchListQuery, SearchListQuery,
SearchQueryDTO, SearchQueryDTO,
SearchQueryTypes, SearchQueryTypes,
@ -42,6 +41,11 @@ export class GallerySearchQueryEntryComponent implements ControlValueAccessor, V
constructor() { constructor() {
this.SearchQueryTypesEnum = Utils.enumToArray(SearchQueryTypes); this.SearchQueryTypesEnum = Utils.enumToArray(SearchQueryTypes);
// Range queries need to be added as AND with min and max sub entry
this.SearchQueryTypesEnum.filter(e => !RangeSearchQueryTypes.includes(e.key));
this.SearchQueryTypesEnum.push({value: 'Date', key: SearchQueryTypes.AND});
this.SearchQueryTypesEnum.push({value: 'Rating', key: SearchQueryTypes.AND});
this.SearchQueryTypesEnum.push({value: 'Resolution', key: SearchQueryTypes.AND});
} }
@ -49,6 +53,7 @@ export class GallerySearchQueryEntryComponent implements ControlValueAccessor, V
return this.queryEntry && TextSearchQueryTypes.includes(this.queryEntry.type); return this.queryEntry && TextSearchQueryTypes.includes(this.queryEntry.type);
} }
get IsListQuery(): boolean { get IsListQuery(): boolean {
return this.queryEntry && ListSearchQueryTypes.includes(this.queryEntry.type); return this.queryEntry && ListSearchQueryTypes.includes(this.queryEntry.type);
} }
@ -57,13 +62,10 @@ export class GallerySearchQueryEntryComponent implements ControlValueAccessor, V
return <any>this.queryEntry; return <any>this.queryEntry;
} }
get AsDateQuery(): DateSearch { get AsRangeQuery(): RangeSearch {
return <any>this.queryEntry; return <any>this.queryEntry;
} }
get AsResolutionQuery(): ResolutionSearch {
return <any>this.queryEntry;
}
get AsOrientationQuery(): OrientationSearch { get AsOrientationQuery(): OrientationSearch {
return <any>this.queryEntry; return <any>this.queryEntry;
@ -73,9 +75,6 @@ export class GallerySearchQueryEntryComponent implements ControlValueAccessor, V
return <any>this.queryEntry; return <any>this.queryEntry;
} }
get AsRatingQuery(): RatingSearch {
return <any>this.queryEntry;
}
get AsSomeOfQuery(): SomeOfSearchQuery { get AsSomeOfQuery(): SomeOfSearchQuery {
return <any>this.queryEntry; return <any>this.queryEntry;
@ -132,30 +131,35 @@ export class GallerySearchQueryEntryComponent implements ControlValueAccessor, V
} }
ngOnChanges(): void { ngOnChanges(): void {
// console.log('ngOnChanges', this.queryEntry); // console.log('ngOnChanges', this.queryEntry);
} }
public onChange(value: any): void {
// console.log('onChange', this.queryEntry);
}
public onTouched(): void { public onTouched(): void {
} }
public writeValue(obj: any): void { public writeValue(obj: any): void {
this.queryEntry = obj; this.queryEntry = obj;
// console.log('write value', this.queryEntry); // console.log('write value', this.queryEntry);
this.ngOnChanges(); this.ngOnChanges();
} }
public registerOnChange(fn: any): void { registerOnChange(fn: (_: any) => void): void {
// console.log('registerOnChange', fn); this.propagateChange = fn;
this.onChange = fn;
} }
public registerOnTouched(fn: any): void { registerOnTouched(fn: () => void): void {
this.onTouched = fn; this.propagateTouch = fn;
} }
public onChange(event: any) {
this.propagateChange(this.queryEntry);
}
private propagateChange = (_: any) => {
};
private propagateTouch = (_: any) => {
};
} }

View File

@ -8,6 +8,7 @@
(blur)="onFocusLost()" (blur)="onFocusLost()"
(focus)="onFocus()" (focus)="onFocus()"
[(ngModel)]="rawSearchText" [(ngModel)]="rawSearchText"
(ngModelChange)="validateRawSearchText()"
#name="ngModel" #name="ngModel"
size="30" size="30"
ngControl="search" ngControl="search"
@ -63,8 +64,8 @@
class="form-control" class="form-control"
i18n-placeholder i18n-placeholder
placeholder="Search" placeholder="Search"
disabled [(ngModel)]="rawSearchText"
[ngModel]="rawSearchText" (ngModelChange)="validateRawSearchText()"
size="30" size="30"
name="srch-term-preview" name="srch-term-preview"
id="srch-term-preview" id="srch-term-preview"
@ -72,6 +73,7 @@
<app-gallery-search-query-entry <app-gallery-search-query-entry
[(ngModel)]="searchQueryDTO" [(ngModel)]="searchQueryDTO"
(change)="onQueryChange()"
name="search-root" name="search-root"
(delete)="resetQuery()"> (delete)="resetQuery()">

View File

@ -21,6 +21,7 @@ export class GallerySearchComponent implements OnDestroy {
autoCompleteItems: AutoCompleteRenderItem[] = []; autoCompleteItems: AutoCompleteRenderItem[] = [];
public searchQueryDTO: SearchQueryDTO = <TextSearch>{type: SearchQueryTypes.any_text, text: ''}; public searchQueryDTO: SearchQueryDTO = <TextSearch>{type: SearchQueryTypes.any_text, text: ''};
public rawSearchText: string;
mouseOverAutoComplete = false; mouseOverAutoComplete = false;
readonly SearchQueryTypes: typeof SearchQueryTypes; readonly SearchQueryTypes: typeof SearchQueryTypes;
modalRef: BsModalRef; modalRef: BsModalRef;
@ -48,12 +49,6 @@ export class GallerySearchComponent implements OnDestroy {
}); });
} }
public get rawSearchText(): string {
return SearchQueryDTO.stringify(this.searchQueryDTO);
}
public set rawSearchText(val: string) {
}
get HTMLSearchQuery() { get HTMLSearchQuery() {
const searchQuery: any = {}; const searchQuery: any = {};
@ -81,7 +76,6 @@ export class GallerySearchComponent implements OnDestroy {
} }
public setMouseOverAutoComplete(value: boolean) { public setMouseOverAutoComplete(value: boolean) {
this.mouseOverAutoComplete = value; this.mouseOverAutoComplete = value;
} }
@ -111,6 +105,18 @@ export class GallerySearchComponent implements OnDestroy {
this.searchQueryDTO = <TextSearch>{text: '', type: SearchQueryTypes.any_text}; this.searchQueryDTO = <TextSearch>{text: '', type: SearchQueryTypes.any_text};
} }
onQueryChange() {
this.rawSearchText = SearchQueryDTO.stringify(this.searchQueryDTO);
}
validateRawSearchText() {
try {
this.searchQueryDTO = SearchQueryDTO.parse(this.rawSearchText);
console.log(this.searchQueryDTO);
} catch (e) {
console.error(e);
}
}
private emptyAutoComplete() { private emptyAutoComplete() {
this.autoCompleteItems = []; this.autoCompleteItems = [];

View File

@ -5,17 +5,20 @@ import {Utils} from '../../../../../src/common/Utils';
import {SQLTestHelper} from '../../../SQLTestHelper'; import {SQLTestHelper} from '../../../SQLTestHelper';
import { import {
ANDSearchQuery, ANDSearchQuery,
DateSearch,
DistanceSearch, DistanceSearch,
FromDateSearch,
MaxRatingSearch,
MaxResolutionSearch,
MinRatingSearch,
MinResolutionSearch,
OrientationSearch, OrientationSearch,
ORSearchQuery, ORSearchQuery,
RatingSearch,
ResolutionSearch,
SearchQueryDTO, SearchQueryDTO,
SearchQueryTypes, SearchQueryTypes,
SomeOfSearchQuery, SomeOfSearchQuery,
TextSearch, TextSearch,
TextSearchQueryMatchTypes TextSearchQueryMatchTypes,
ToDateSearch
} from '../../../../../src/common/entities/SearchQueryDTO'; } from '../../../../../src/common/entities/SearchQueryDTO';
import {IndexingManager} from '../../../../../src/backend/model/database/sql/IndexingManager'; import {IndexingManager} from '../../../../../src/backend/model/database/sql/IndexingManager';
import {DirectoryDTO} from '../../../../../src/common/entities/DirectoryDTO'; import {DirectoryDTO} from '../../../../../src/common/entities/DirectoryDTO';
@ -789,7 +792,7 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => {
it('should search date', async () => { it('should search date', async () => {
const sm = new SearchManager(); const sm = new SearchManager();
let query = <DateSearch>{before: 0, after: 0, type: SearchQueryTypes.date}; let query: any = <FromDateSearch>{value: 0, type: SearchQueryTypes.from_date};
expect(Utils.clone(await sm.search(query))) expect(Utils.clone(await sm.search(query)))
.to.deep.equalInAnyOrder(removeDir(<SearchResultDTO>{ .to.deep.equalInAnyOrder(removeDir(<SearchResultDTO>{
@ -800,9 +803,8 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => {
resultOverflow: false resultOverflow: false
})); }));
query = <DateSearch>{ query = <ToDateSearch>{
before: p.metadata.creationDate, value: p.metadata.creationDate, type: SearchQueryTypes.to_date
after: p.metadata.creationDate, type: SearchQueryTypes.date
}; };
expect(Utils.clone(await sm.search(query))) expect(Utils.clone(await sm.search(query)))
@ -814,11 +816,10 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => {
resultOverflow: false resultOverflow: false
})); }));
query = <DateSearch>{ query = <FromDateSearch>{
before: p.metadata.creationDate, value: p.metadata.creationDate,
after: p.metadata.creationDate,
negate: true, negate: true,
type: SearchQueryTypes.date type: SearchQueryTypes.from_date
}; };
expect(Utils.clone(await sm.search(query))) expect(Utils.clone(await sm.search(query)))
@ -830,15 +831,12 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => {
resultOverflow: false resultOverflow: false
})); }));
query = <DateSearch>{ query = <ToDateSearch>{
before: p.metadata.creationDate + 1000000000, value: p.metadata.creationDate + 1000000000,
after: 0, type: SearchQueryTypes.date type: SearchQueryTypes.to_date
}; };
expect(Utils.clone(await sm.search(<DateSearch>{ expect(Utils.clone(await sm.search(query)))
before: p.metadata.creationDate + 1000000000,
after: 0, type: SearchQueryTypes.date
})))
.to.deep.equalInAnyOrder(removeDir(<SearchResultDTO>{ .to.deep.equalInAnyOrder(removeDir(<SearchResultDTO>{
searchQuery: query, searchQuery: query,
directories: [], directories: [],
@ -853,7 +851,7 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => {
it('should search rating', async () => { it('should search rating', async () => {
const sm = new SearchManager(); const sm = new SearchManager();
let query = <RatingSearch>{min: 0, max: 0, type: SearchQueryTypes.rating}; let query: MinRatingSearch | MaxRatingSearch = <MinRatingSearch>{value: 0, type: SearchQueryTypes.min_rating};
expect(Utils.clone(await sm.search(query))) expect(Utils.clone(await sm.search(query)))
@ -865,7 +863,7 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => {
resultOverflow: false resultOverflow: false
})); }));
query = <RatingSearch>{min: 0, max: 5, type: SearchQueryTypes.rating}; query = <MaxRatingSearch>{value: 5, type: SearchQueryTypes.max_rating};
expect(Utils.clone(await sm.search(query))) expect(Utils.clone(await sm.search(query)))
.to.deep.equalInAnyOrder(removeDir(<SearchResultDTO>{ .to.deep.equalInAnyOrder(removeDir(<SearchResultDTO>{
searchQuery: query, searchQuery: query,
@ -875,7 +873,7 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => {
resultOverflow: false resultOverflow: false
})); }));
query = <RatingSearch>{min: 0, max: 5, negate: true, type: SearchQueryTypes.rating}; query = <MaxRatingSearch>{value: 5, negate: true, type: SearchQueryTypes.max_rating};
expect(Utils.clone(await sm.search(query))) expect(Utils.clone(await sm.search(query)))
.to.deep.equalInAnyOrder(removeDir(<SearchResultDTO>{ .to.deep.equalInAnyOrder(removeDir(<SearchResultDTO>{
searchQuery: query, searchQuery: query,
@ -885,7 +883,7 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => {
resultOverflow: false resultOverflow: false
})); }));
query = <RatingSearch>{min: 2, max: 2, type: SearchQueryTypes.rating}; query = <MinRatingSearch>{value: 2, type: SearchQueryTypes.min_rating};
expect(Utils.clone(await sm.search(query))) expect(Utils.clone(await sm.search(query)))
.to.deep.equalInAnyOrder(removeDir(<SearchResultDTO>{ .to.deep.equalInAnyOrder(removeDir(<SearchResultDTO>{
searchQuery: query, searchQuery: query,
@ -895,7 +893,7 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => {
resultOverflow: false resultOverflow: false
})); }));
query = <RatingSearch>{min: 2, max: 2, negate: true, type: SearchQueryTypes.rating}; query = <MinRatingSearch>{value: 2, negate: true, type: SearchQueryTypes.min_rating};
expect(Utils.clone(await sm.search(query))) expect(Utils.clone(await sm.search(query)))
.to.deep.equalInAnyOrder(removeDir(<SearchResultDTO>{ .to.deep.equalInAnyOrder(removeDir(<SearchResultDTO>{
searchQuery: query, searchQuery: query,
@ -910,7 +908,9 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => {
it('should search resolution', async () => { it('should search resolution', async () => {
const sm = new SearchManager(); const sm = new SearchManager();
let query = <ResolutionSearch>{min: 0, max: 0, type: SearchQueryTypes.resolution}; let query: MinResolutionSearch | MaxResolutionSearch =
<MinResolutionSearch>{value: 0, type: SearchQueryTypes.min_resolution};
expect(Utils.clone(await sm.search(query))) expect(Utils.clone(await sm.search(query)))
.to.deep.equalInAnyOrder(removeDir(<SearchResultDTO>{ .to.deep.equalInAnyOrder(removeDir(<SearchResultDTO>{
searchQuery: query, searchQuery: query,
@ -920,7 +920,7 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => {
resultOverflow: false resultOverflow: false
})); }));
query = <ResolutionSearch>{max: 1, type: SearchQueryTypes.resolution}; query = <MaxResolutionSearch>{value: 1, type: SearchQueryTypes.max_resolution};
expect(Utils.clone(await sm.search(query))) expect(Utils.clone(await sm.search(query)))
.to.deep.equalInAnyOrder(removeDir(<SearchResultDTO>{ .to.deep.equalInAnyOrder(removeDir(<SearchResultDTO>{
searchQuery: query, searchQuery: query,
@ -930,7 +930,7 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => {
resultOverflow: false resultOverflow: false
})); }));
query = <ResolutionSearch>{min: 2, max: 3, type: SearchQueryTypes.resolution}; query = <MinResolutionSearch>{value: 2, type: SearchQueryTypes.min_resolution};
expect(Utils.clone(await sm.search(query))) expect(Utils.clone(await sm.search(query)))
.to.deep.equalInAnyOrder(removeDir(<SearchResultDTO>{ .to.deep.equalInAnyOrder(removeDir(<SearchResultDTO>{
searchQuery: query, searchQuery: query,
@ -940,7 +940,7 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => {
resultOverflow: false resultOverflow: false
})); }));
query = <ResolutionSearch>{min: 2, max: 3, negate: true, type: SearchQueryTypes.resolution}; query = <MaxResolutionSearch>{value: 3, negate: true, type: SearchQueryTypes.max_resolution};
expect(Utils.clone(await sm.search(query))) expect(Utils.clone(await sm.search(query)))
.to.deep.equalInAnyOrder(removeDir(<SearchResultDTO>{ .to.deep.equalInAnyOrder(removeDir(<SearchResultDTO>{
searchQuery: query, searchQuery: query,
@ -950,7 +950,7 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => {
resultOverflow: false resultOverflow: false
})); }));
query = <ResolutionSearch>{min: 3, type: SearchQueryTypes.resolution}; query = <MinResolutionSearch>{value: 3, negate: true, type: SearchQueryTypes.min_resolution};
expect(Utils.clone(await sm.search(query))) expect(Utils.clone(await sm.search(query)))
.to.deep.equalInAnyOrder(removeDir(<SearchResultDTO>{ .to.deep.equalInAnyOrder(removeDir(<SearchResultDTO>{
searchQuery: query, searchQuery: query,
@ -1094,6 +1094,8 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => {
text: 'xyz', text: 'xyz',
type: SearchQueryTypes.keyword type: SearchQueryTypes.keyword
}; };
// tslint:disable-next-line
expect(await sm.getRandomPhoto(query)).to.not.exist; expect(await sm.getRandomPhoto(query)).to.not.exist;
query = <TextSearch>{ query = <TextSearch>{

View File

@ -0,0 +1,137 @@
import {expect} from 'chai';
import {
ANDSearchQuery,
DistanceSearch,
FromDateSearch,
MaxRatingSearch,
MaxResolutionSearch,
MinRatingSearch,
MinResolutionSearch,
OrientationSearch,
ORSearchQuery,
SearchQueryDTO,
SearchQueryTypes,
SomeOfSearchQuery,
TextSearch,
ToDateSearch
} from '../../../src/common/entities/SearchQueryDTO';
describe('SearchQueryDTO', () => {
const check = (query: SearchQueryDTO) => {
expect(SearchQueryDTO.parse(SearchQueryDTO.stringify(query))).to.deep.equals(query, SearchQueryDTO.stringify(query));
};
describe('should serialize and deserialize', () => {
it('Text search', () => {
check(<TextSearch>{type: SearchQueryTypes.any_text, text: 'test'});
check(<TextSearch>{type: SearchQueryTypes.person, text: 'person_test'});
check(<TextSearch>{type: SearchQueryTypes.directory, text: 'directory'});
check(<TextSearch>{type: SearchQueryTypes.keyword, text: 'big boom'});
check(<TextSearch>{type: SearchQueryTypes.caption, text: 'caption'});
check(<TextSearch>{type: SearchQueryTypes.file_name, text: 'filename'});
check(<TextSearch>{type: SearchQueryTypes.position, text: 'New York'});
});
it('Date search', () => {
check(<FromDateSearch>{type: SearchQueryTypes.from_date, value: (new Date(2020, 1, 1)).getTime()});
check(<ToDateSearch>{type: SearchQueryTypes.to_date, value: (new Date(2020, 1, 2)).getTime()});
});
it('Rating search', () => {
check(<MinRatingSearch>{type: SearchQueryTypes.min_rating, value: 10});
check(<MaxRatingSearch>{type: SearchQueryTypes.max_rating, value: 1});
});
it('Resolution search', () => {
check(<MinResolutionSearch>{type: SearchQueryTypes.min_resolution, value: 10});
check(<MaxResolutionSearch>{type: SearchQueryTypes.max_resolution, value: 5});
});
it('Distance search', () => {
check(<DistanceSearch>{type: SearchQueryTypes.distance, distance: 10, from: {text: 'New York'}});
});
it('OrientationSearch search', () => {
check(<OrientationSearch>{type: SearchQueryTypes.orientation, landscape: true});
check(<OrientationSearch>{type: SearchQueryTypes.orientation, landscape: false});
});
it('And search', () => {
check(<ANDSearchQuery>{
type: SearchQueryTypes.AND,
list: [
<TextSearch>{type: SearchQueryTypes.keyword, text: 'big boom'},
<TextSearch>{type: SearchQueryTypes.position, text: 'New York'}
]
});
check(<ANDSearchQuery>{
type: SearchQueryTypes.AND,
list: [
<TextSearch>{type: SearchQueryTypes.keyword, text: 'big boom'},
<ANDSearchQuery>{
type: SearchQueryTypes.AND,
list: [
<TextSearch>{type: SearchQueryTypes.caption, text: 'caption'},
<TextSearch>{type: SearchQueryTypes.position, text: 'New York'}
]
}
]
});
});
it('Or search', () => {
check(<ORSearchQuery>{
type: SearchQueryTypes.OR,
list: [
<TextSearch>{type: SearchQueryTypes.keyword, text: 'big boom'},
<TextSearch>{type: SearchQueryTypes.position, text: 'New York'}
]
});
check(<ORSearchQuery>{
type: SearchQueryTypes.OR,
list: [
<ORSearchQuery>{
type: SearchQueryTypes.OR,
list: [
<TextSearch>{type: SearchQueryTypes.keyword, text: 'big boom'},
<TextSearch>{type: SearchQueryTypes.person, text: 'person_test'}
]
},
<TextSearch>{type: SearchQueryTypes.position, text: 'New York'}
]
});
});
it('Some of search', () => {
check(<SomeOfSearchQuery>{
type: SearchQueryTypes.SOME_OF,
list: [
<TextSearch>{type: SearchQueryTypes.keyword, text: 'big boom'},
<TextSearch>{type: SearchQueryTypes.position, text: 'New York'}
]
});
check(<SomeOfSearchQuery>{
type: SearchQueryTypes.SOME_OF,
min: 2,
list: [
<TextSearch>{type: SearchQueryTypes.keyword, text: 'big boom'},
<TextSearch>{type: SearchQueryTypes.position, text: 'New York'},
<TextSearch>{type: SearchQueryTypes.caption, text: 'caption test'}
]
});
check(<SomeOfSearchQuery>{
type: SearchQueryTypes.SOME_OF,
min: 2,
list: [
<TextSearch>{type: SearchQueryTypes.keyword, text: 'big boom'},
<TextSearch>{type: SearchQueryTypes.person, text: 'person_test'},
<ANDSearchQuery>{
type: SearchQueryTypes.AND,
list: [
<TextSearch>{type: SearchQueryTypes.caption, text: 'caption'},
<TextSearch>{type: SearchQueryTypes.position, text: 'New York'}
]
}
]
});
});
});
});