import {ObjectType} from '../../model/object-type.model';
import {DatePipe} from '@angular/common';
//export type QueryScope = 'indexdate' | 'content' | 'all';
export enum QueryScope {
ALL = 'all',
CONTENT = 'content',
INDEX_DATA = 'indexdata'
}
/**
* The SearchQuery Class
*
* See {@link SearchService} for Usage
*/
export class SearchQuery {
/**
* Base Parameters for a Search Query
*/
public static BASE_PARAMS = {
TYPE: 'type',
CREATOR: 'creator',
CREATED: 'created',
MODIFIER: 'modifier',
MODIFIED: 'modified',
FILESIZE: 'filesize',
FILENAME: 'filename',
MIMETYPEGROUP: 'mimetypegroup',
FOLDER: 'folder'
};
/**
* Time period buckets property names
*/
public static TIME_PERIOD_BUCKETS = {
TODAY: 'today',
YESTERDAY: 'yesterday',
THIS_WEEK: 'thisweek',
LAST_WEEK: 'lastweek',
THIS_MONTH: 'thismonth',
LAST_MONTH: 'lastmonth',
THIS_YEAR: 'thisyear',
LAST_YEAR: 'lastyear'
};
/**
* Relative date buckets property names
*/
public static RELATIVE_DATE_BUCKETS = {
TODAY: 'today',
YESTERDAY: 'yesterday',
TOMORROW: 'tomorrow',
THIS_WEEK: 'thisweek',
LAST_WEEK: 'lastweek',
NEXT_WEEK: 'nextweek',
THIS_MONTH: 'thismonth',
LAST_MONTH: 'lastmonth',
NEXT_MONTH: 'nextmonth',
THIS_QUARTER: 'thisquarter',
LAST_QUARTER: 'lastquarter',
NEXT_QUARTER: 'nextquarter',
THIS_YEAR: 'thisyear',
LAST_YEAR: 'lastyear',
NEXT_YEAR: 'nextyear'
};
/**
* mimetype buckets property names
*/
public static MIMETYPEGROUP_BUCKETS = {
PDF: 'pdf',
MAIL: 'mail',
WORD: 'word',
IMAGE: 'image',
OCTET_STREAM: 'octet-stream',
TXT: 'txt',
EXCEL: 'excel',
POWERPOINT: 'powerpoint',
VIDEO: 'video',
HTML: 'html',
AUDIO: 'audio',
ZIP: 'zip'
};
/**
* filesize buckets property names
*/
public static FILE_SIZE_BUCKETS = {
NONE: 'none',
LT1MB: 'lt1MB',
LT10MB: 'lt10MB',
LT100MB: 'lt100MB',
GT100MB: 'gt100MB'
};
// list of things that may have changed the current query
public static UPDATE_CAUSE = {
TOGGLED_TYPE: 'cause.type.toggled',
TOGGLED_EXPERTMODE: 'cause.expertmode.toggled',
TERM_SET: 'cause.term.set',
TYPES_SET: 'cause.types.set',
CONTEXT_FOLDER_SET: 'cause.contextfolder.set',
TOGGLED_FILTER: 'cause.filter.toggled',
ADDED_FILTER: 'cause.filter.added',
REMOVED_FILTER: 'cause.filter.removed',
};
/**
* Detault Search mode Index
*/
public static DEFAULT_SEARCH_MODE = 'idxs';
/**
* Search mode for Full test Search
*/
public static FULLTEXT_SEARCH_MODE = 'fts';
/**
* fulltext search term
*/
public term: string;
/**
* Array of fields to request from the search service, leaving this blank will return the fields specified in the result field configuration
*/
public fields: string[];
/**
* List of object types to be fetched by the query. Leaving this blank will return results from any type
*/
public types: ObjectType[] = [];
/**
* List of context folder types
*/
public contextFolderTypes: ObjectType[] = [];
/**
* List of filters to define the query
*/
public filters: SearchFilter[] = [];
/**
* Whether or not to retrieve suggestions
*/
public suggest: boolean;
/**
* Whether or not the term is an expert mode query
*/
public expertMode = false;
/**
* The scope of the query
*/
public scope: QueryScope;
/**
* The search mode to be used (SearchQuery.DEFAULT_SEARCH_MODE or SearchQuery.FULLTEXT_SEARCH_MODE)
*/
public searchMode: string = SearchQuery.DEFAULT_SEARCH_MODE;
/**
* Additional options to be applied to the query
*/
public sortOptions: SortOption[] = [];
/**
* Resolve references with their title instead of their actual value
*/
public resolveReference = true;
/**
* The state of the current query
*/
public state: SearchState = new SearchState();
public highlighting: boolean = false;
public withContext: boolean = false;
public name?: string;
// Indicator what caused the update of a SearchQuery object.
// Usefull for components that subscribed to a query, and need to
// track the kind of changes made
__updateCause: string;
constructor() {}
/**
* Adds a new target type to the query
*
* @param type Object type to be added
*/
public addType(type: ObjectType) {
if (!this.types.find(t => t.id === type.id)) {
this.types.push(type);
}
}
/**
* Removes a type from the target types list
*
* @param type The object type to be removed
*/
public removeType(type: ObjectType) {
this.types = this.types.filter(t => t.id !== type.id);
}
/**
* Adds or removes the given type based on the current settings
*
* @param type The object type to be toggled
*/
public toggleType(type: ObjectType) {
if (this.types.find(t => t.id === type.id)) {
this.removeType(type);
} else {
this.types.push(type);
}
}
/**
* Adds a new filter to the query. If there already is a filter set for the given property, it will be overridden.
*
* @param filter The filter to be added
*/
public addFilter(filter: SearchFilter) {
if (!filter) {
return;
}
if (!this.getFilter(filter.property)) {
this.filters.push(filter);
} else {
// if there already is a filter set for the target property
// we'll just override it with the new value
this.removeFilter(filter.property);
this.filters.push(filter);
}
}
/**
* Removes a filter from the query.
*
* @param filterPropertyName The filter (its property name) to be removed
*/
public removeFilter(filterPropertyName: string) {
this.filters = this.filters.filter(f => f.property !== filterPropertyName);
}
/**
* Adds or removes the given filter based on the current settings
*
* @param filter The filter to be toggled
*/
public toggleFilter(filter: SearchFilter, override?: boolean) {
if (this.getFilter(filter.property)) {
this.removeFilter(filter.property);
if (override) {
this.filters.push(filter);
}
} else {
this.filters.push(filter);
}
}
/**
* Retrieves a filter by its property name.
*
* @param propertyName The filters property name (qname of form element)
* @returns Search Filter Object
*/
public getFilter(propertyName: string): SearchFilter {
return this.filters.find(f => f.property === propertyName);
}
public getTableFilters(propertyName: string): SearchFilter[] {
return this.filters.filter(f => f.property.startsWith(propertyName + '.'));
}
/**
* Adding new Sort Options
* Sort Options are only added if they are not already present
*
* @param name
* @param order
* @param missing
*/
public addSortOption(name: string, order: string, missing?: string) {
if (!this.sortOptions.find(s => s.field === name)) {
this.sortOptions.push(new SortOption(name, order, missing));
}
}
/**
*
* @param name
*/
public removeSortOption(name: string) {
this.sortOptions = this.sortOptions.filter(s => s.field !== name);
}
/**
* Generate a query JSON from the current SearchQuery that can be send to the search service.
*
* @param resolveReference in case of csv export the resolving of references is set by configuration
*/
public getQueryJson(resolveReference?: boolean) {
// Convert the current query object to a query JSON that can be consumed by the search service backend.
// To keep it short, we'll only add properties that are set and differ from default values
const queryJson: any = {};
// add type filters
if (this.types.length > 0) {
queryJson.types = this.types.map(t =>
typeof t === 'string' ? t : t.name
);
}
// add context folder type filters
if (this.contextFolderTypes.length > 0) {
queryJson.contextfoldertypes = this.contextFolderTypes.map(t => t.name);
}
// filters
if (this.filters.length > 0) {
queryJson.filters = {};
this.filters.forEach(f => {
queryJson.filters[f.property] = f.toQuery();
});
}
if (this.term) {
queryJson.term = this.term;
}
// search fields
if (this.fields) {
queryJson.fields = this.fields;
}
if (this.name) {
queryJson.name = this.name;
}
// add options when they differ from the default values
if (this.applyOptions()) {
queryJson.options = {};
queryJson.options.scope = this.scope;
if (this.suggest) {
queryJson.options.suggest = true;
}
if (this.expertMode) {
queryJson.options.expertmode = true;
}
queryJson.options.resolvereference = resolveReference !== undefined ? resolveReference : true;
if (this.searchMode !== SearchQuery.DEFAULT_SEARCH_MODE) {
queryJson.options.searchmode = this.searchMode;
}
if (this.sortOptions && this.sortOptions.length) {
queryJson.options.sort = {};
this.sortOptions.forEach((sortOption: SortOption) => {
if (sortOption.order) {
queryJson.options.sort[sortOption.field] = {
order: sortOption.order
};
}
});
}
if (this.highlighting) {
queryJson.options.highlighting = this.highlighting;
}
if (this.withContext) {
queryJson.options.withcontext = this.withContext;
}
}
return queryJson;
}
// determines whether or not the query send to the search service should contain options
private applyOptions(): boolean {
return (
this.expertMode ||
!!this.scope ||
this.suggest ||
this.highlighting ||
this.resolveReference ||
this.withContext ||
this.searchMode !== SearchQuery.DEFAULT_SEARCH_MODE ||
this.sortOptions.length > 0
);
}
}
/**
* @ignore
* Creates the Model of Sort Options
*/
export class SortOption {
constructor(
public field: string,
public order: string,
public missing?: string
) {}
}
/**
* @ignore
* Creates the Model of Field Definitions
*/
export class FieldDefinition {
constructor(
public elements: any[] = [],
public sortorder: any[] = [],
public grouporder: any[] = [],
public pinned: any[] = [],
public alignmentx: {qname: string, value: string}[] = [],
public mode?: string
) {}
}
/**
* @ignore
* Class representing the state of a query
*/
export class SearchState {
public count: {value: number, relation?: string};
public aggregations;
public lastChange?: string;
/**
* @ignore
*/
constructor() {
this.aggregations = {
type: new Map<string, number>(),
contextType: new Map<string, number>(),
created: [],
modified: [],
mimetypegroup: [],
filesize: []
};
}
get totalCount() {
return Array.from(this.aggregations.type.values()).reduce((acc: number, cur: number) => acc + cur, 0) as number;
}
get isEmpty() {
return !(this.count && this.count.value);
}
addAggregations(type: string, key: string, value: number, agg?) {
switch (type) {
case 'contexttype': {
this.aggregations.contextType.set(key, value);
break;
}
case SearchQuery.BASE_PARAMS.TYPE: {
this.aggregations.type.set(key, value);
break;
}
case SearchQuery.BASE_PARAMS.CREATED: {
this.aggregations.created.push({
key: key,
value: value
});
break;
}
case SearchQuery.BASE_PARAMS.MODIFIED: {
this.aggregations.modified.push({
key: key,
value: value
});
break;
}
case SearchQuery.BASE_PARAMS.MIMETYPEGROUP: {
this.aggregations.mimetypegroup.push({
key: key,
value: value
});
break;
}
case SearchQuery.BASE_PARAMS.FILESIZE: {
this.aggregations.filesize.push({
key: key,
value: value,
from: agg.from,
to: agg.to || Infinity,
label: key.replace('gt', '> ').replace('lt', '< ')
});
break;
}
}
}
}
/**
* @ignore
* Creates the Model of Search Results
*/
export class SearchResult {
public count: {value: number, relation?: string};
public suggests: string[];
public hits: any[];
public typeName: string;
public fields: FieldDefinition;
constructor() {}
}
export class SearchFilter {
/**
* available operators for a search filter
*/
public static OPERATOR = {
/** equal */
EQUAL: 'eq',
/** match (for string with classification "path6666") */
MATCH: 'match',
/** match at least one of the provided values (value has to be an array) */
IN: 'in',
/** greater than */
GREATER_THAN: 'gt',
/** greater than or equal */
GREATER_OR_EQUAL: 'gte', //
LESS_THAN: 'lt', // less than
LESS_OR_EQUAL: 'lte', // less than or equal
INTERVAL: 'gtlt', // interval
INTERVAL_INCLUDE_BOTH: 'gtelte', // interval include left and right
INTERVAL_INCLUDE_TO: 'gtlte', // interval include right
INTERVAL_INCLUDE_FROM: 'gtelt', // interval include left
RANGE: 'rg', // aggegation ranges
MORE: 'mr',
USE_NOT: 'usenot'
};
/**
* Constructor for creating a new SearchFilter.
*
* @param property The qualified name of the field this filter should apply to.
* @param operator Operator indicating how to handle the filters value(s). See SearchFilter.OPERATOR for available operators.
* @param firstValue The filters value
* @param secondValue Optional second value for filters that for example define ranges of values
* @param transformDate Flag to transform date hours and minutes based on operator
*/
constructor(
public property: string,
public operator: string,
public firstValue: any,
public secondValue?: any,
public transformDate?: {time?: boolean; timezone?: boolean},
public useNot?: boolean,
public actualOperator?: string
) { }
formatDate(value: any) {
return this.transformDate && value
? this.transformDate.timezone
? new DatePipe('en').transform(value, 'yyyy-MM-ddTZ')
: this.transformDate.time
? new Date(value).toISOString().slice(0, 16) + 'Z'
: new DatePipe('en').transform(value, 'yyyy-MM-dd')
: value;
}
toQuery() {
const v1 = this.operator === 'mr' ? this.firstValue : this.formatDate(this.firstValue);
const o = this.operator === 'mr' ? 'rg' : (this.useNot ? this.actualOperator : this.operator);
return {
o,
v1,
...(this.secondValue && {v2: this.formatDate(this.secondValue)}),
...(this.useNot && { usenot: true, actualOperator: this.actualOperator })
};
}
toDateTimeQuery() {
if (this.transformDate && this.transformDate.timezone && this.firstValue) {
let firstValue = this.firstValue;
let secondValue = this.secondValue;
const endDate = new Date(secondValue || firstValue);
endDate.setHours(23, 59, 59, 999);
if (this.operator === SearchFilter.OPERATOR.LESS_OR_EQUAL) {
firstValue = endDate.toISOString();
} else {
const startDate = new Date(firstValue);
if (
this.operator === SearchFilter.OPERATOR.GREATER_THAN ||
this.operator === SearchFilter.OPERATOR.INTERVAL
) {
startDate.setHours(23, 59, 59, 999);
} else {
startDate.setHours(0, 0, 0, 0);
}
firstValue = startDate.toISOString();
}
if (
this.operator === SearchFilter.OPERATOR.INTERVAL_INCLUDE_BOTH ||
this.operator === SearchFilter.OPERATOR.EQUAL
) {
secondValue = endDate.toISOString();
}
return {
o:
this.operator === SearchFilter.OPERATOR.EQUAL
? SearchFilter.OPERATOR.INTERVAL_INCLUDE_BOTH
: this.operator,
v1: firstValue,
...(secondValue && {v2: secondValue})
};
} else {
return this.toQuery();
}
}
/**
* @ignore
*/
match(
property: string,
operator: string,
firstValue: any,
secondValue?: any
) {
return (
this.property === property &&
this.operator === operator &&
this.secondValue === secondValue &&
(this.firstValue instanceof Array
? !!this.firstValue.find(v => v === firstValue)
: this.firstValue === firstValue)
);
}
}