src/helpers/Query.js
import ContentService from '../services/ContentService';
import Content from './content/Content';
import { validate, isValid } from '../utilities/ValidationUtility';
/**
* Helper class representing a Query object that can be passed on to the findDynamicContent method
*/
class Query {
/**
* Constructs the Query class
*
* @param {!(String|Content)} resource - The type of content it is.
* @param {Array} [orderBy=[]] - an array objects that define the logic to order the
* results by
* @param {Number} [page=0] - the page number of the results given
* @param {Number} [perPage=0] - the number of content to return per page
* @param {Boolean} [minimal=false] - returns only the filtered attributes specified
* @param {Boolean} [brandRelative=false] - returns only the contents that belong to brands the
* mobile_user/device favorited
*
* @example
* const query = new Query('acronyms')
* .ascending('name')
* .setResource('type')
* .addSearchValue('blue')
* .addSearchValue('green')
* .addSearchValue('red')
* .clearSearchValues()
* .searchByAddressMode()
* .setSwLat('10.0')
* .setSwLong('9.9')
* .setNeLat('9.9')
* .limit(10)
* .showMinimal()
* .showBrandRelative()
* .referenceModule('reference')
* .find();
*/
constructor(resource, orderBy = [], page = 0, perPage = 0, minimal = false,
brandRelative = false) {
/** @type {String} */
this._id = undefined;
/** @type {String|Content} */
this._resource = resource;
/** @type {Object[]} */
this._filters = [];
if (!Array.isArray(orderBy)) {
throw new Error('Query construction error: orderBy is not an array');
}
/** @type {Array} */
this.orderBy = orderBy;
/** @type {Number} */
this.page = page;
/** @type {Number} */
this.perPage = perPage;
/** @type {Boolean} */
this.minimal = minimal;
/** @type {Boolean} */
this.brandRelative = brandRelative;
/** @type {Boolean} */
this.whole_word = false;
/** @type {Boolean} */
this.case_sensitive = false;
/** @type {String} */
this.reference_module = '';
// Set default search mode to 'search'
this.newFilter();
}
/** @type {Object} */
get filter() {
return this._filters[this._filters.length - 1];
}
/** @type {Array<Object>} */
get filters() {
return this._filters;
}
/**
* @param {!(String|Object)} resource - Must be a string or have the attribute 'resource'
* @type {String}
*/
set resource(resource) {
// check param values are correct
if (!(resource instanceof Content) && typeof resource !== 'string'
&& !(resource instanceof String)) {
throw new Error('Query setResource err: resource must either be a String or a Content');
}
// If of type Content, grab the resource attribute
if (resource instanceof Content) {
this._resource = resource.resource;
} else {
if (resource.length < 1) {
throw new Error('Query setResource err: resource must be a valid String');
}
this._resource = resource;
}
}
/** @type {String} */
get resource() {
return this._resource;
}
/**
* Function to pass in a Dynamic Content to search. Takes a Dynamic Content
* response and checks for the 'content_type'.
*
* @param {Object} resource - Either an object of type Content or a String permanent link value.
* @return {Query} this
*
* @example
* const q = new Query('labs');
* q.setResource('acronyms');
*/
setResource(resource) {
this.resource = resource;
return this;
}
/**
* Validates all possible types of all filters in Query for a filter call.
*
* @throws {Clinical6Error} - If missing required parameters or Query fields
* @return {Boolean} - True if this is a valid Query
*
* @example
* const q = new Query('acronyms');
* q.validate();
*/
validate() {
this._filters.forEach((filter) => {
const allBox = filter.sw_lat && filter.sw_lng && filter.ne_lat && filter.ne_lng;
const anyBox = filter.sw_lat || filter.sw_lng || filter.ne_lat || filter.ne_lng;
const allRadius = filter.radius && filter.exact_lat && filter.exact_lng;
const anyRadius = filter.radius || filter.exact_lat || filter.exact_lng;
switch (filter.attribute) {
case 'address':
break;
case 'date':
validate('Query.validate mode date',
isValid(!(!filter.min && !filter.max), 'filter min or max values not specified'));
break;
case 'location':
if ((!filter.values || (filter.values && filter.values.length < 1))
&& !anyBox && !anyRadius) {
throw new Error('location address value not specified');
} else if (anyRadius && !allRadius) {
throw new Error('location radius exact lng and exact lat not specified');
} else if (anyBox && !allBox) {
throw new Error('location not all box coordinates specified');
}
break;
case 'numeric':
validate('Query.validate mode numeric',
isValid(!(!filter.value && !filter.values
&& !filter.min && !filter.max),
'numeric min, max, or values not specified'));
break;
case 'search':
validate('Query.validate mode search',
isValid((filter.values && filter.values.length > 0), 'please add search values to Query'));
break;
case 'tag':
validate('Query.validate mode tag',
isValid((filter.values && filter.values.length > 0), 'please add search values to Query'));
break;
case 'tag_names':
validate('Query.validate mode tag_names',
isValid((filter.values && filter.values.length > 0), 'please add search values to Query'));
break;
default: break;
}
});
}
/**
* Creates a new filter with the attribute
*
* @param {String} attribute - The new attribute of the filter
* @return {Query} - this
*
* @example
* const q = new Query('acronyms');
* q.searchByLocationMode(); // updates existing current filter
* q.newFilter('tag'); // creates entirel new filter entry with the attribute 'tag'
*/
newFilter(attribute = 'search') {
/** @type {Object} */
this.filters.push({
attribute
});
return this;
}
/**
* Executes the search. Prior to calling this method, search values must be added.
* Additionally, a search method should be set. By default, the search mode will be 'search'.
*
* @return {Promise<Content[]>} - A promise with a list of content.
*
* @example
* const q = new Query('acronyms');
* q.find();
*/
find() {
return (new ContentService()).find(this);
}
/** Query Search Values */
/**
* Adds a search value to search for in the Dynamic Content.
*
* @param {String | Number} searchValue - the string or number value to add.
* @return {Query} this
*/
addSearchValue(searchValue) {
if (!this.filter.values) {
this.filter.values = [];
}
this.filter.values.push(searchValue);
return this;
}
/**
* Removes the provided search value from the array of search values. Throws
* an error if the provided value is not in the search array.
*
* @param {String} searchValue - the string value to remove.
* @return {Query} this
*/
removeSearchValue(searchValue) {
if (this.filter.values) {
const index = this.filter.values.indexOf(searchValue);
if (index < 0) {
throw new Error('Query removeSearchValue err: searchValue not found');
}
this.filter.values.splice(index, 1);
}
return this;
}
/**
* Clears the array of search values.
*
* @return {Query} this
*/
clearSearchValues() {
this.filter.values = [];
return this;
}
/**
* Sets the search mode to 'search'. Searches a Dyanmic Content to find
* the values in filter.values.
*
* @return {Query} this
*/
searchMode() {
this.filter.attribute = 'search';
return this;
}
/**
* Sets the search mode to 'starts_with'. Searches a Dyanmic Content to find
* matches that start with the values provided in filter.values.
*
* @return {Query} this
*/
startsWithMode() {
this.filter.search_method = 'starts_with';
return this;
}
/**
* Sets the search mode to 'ends_with'. Searches a Dyanmic Content to find
* matches that end with the values provided in searchValues.
*
* @return {Query} this
*/
endsWithMode() {
this.filter.search_method = 'ends_with';
return this;
}
/**
* Sets the search mode to 'whole_word'. Searches a Dyanmic Content to find
* matches that end with the values provided in searchValues.
*
* @return {Query} this
*/
wholeWordMode() {
this.filter.search_method = 'whole_word';
return this;
}
/**
* Creates a filter that would force the search to check for whole words
*
* @return {Query} this
*/
wholeWordSearch() {
/** @type {Boolean} */
this.filter.whole_word = true;
return this;
}
/**
* Creates a filter that would force the search to be case sensitive
*
* @return {Query} this
*/
makeCaseSensitive() {
/** @type {Boolean} */
this.filter.case_sensitive = true;
return this;
}
/**
* Sets the max of filter
*
* @param {Number|String} num - number to set max value to
* @return {Query} this
*/
lessThanOrEqualTo(num) {
/** @type {Number|String} */
this.filter.max = num;
return this;
}
/**
* Sets the min of the filter
*
* @param {Number} num - number to set min value to
* @return {Query} this
*/
greaterThanOrEqualTo(num) {
/** @type {Number|String} */
this.filter.min = num;
return this;
}
/**
* Sets the exact number to filter by for search by number.
*
* @param {Number} num - number to filter by
* @return {Query} this
*/
setValue(num) {
/** @type {Number|String} */
this.filter.value = num;
return this;
}
/**
* Builds a query order ascending order based on the column givenf
*
* @param {String} col - the column/attribute to sort by
* @return {Query} this
*/
ascending(col) {
const orderBy = {};
orderBy.column = col;
orderBy.direction = 'asc';
this.orderBy.push(orderBy);
return this;
}
/**
* Builds a query order descending order based on the column given
*
* @param {String} col - the column/attribute to sort by
* @return {Query} this
*/
descending(col) {
const orderBy = {};
orderBy.column = col;
orderBy.direction = 'desc';
this.orderBy.push(orderBy);
return this;
}
/**
* Builds a query with limit of how many results to show
*
* @param {Number} num - number of results to show
* @return {Query} this
*/
limit(num) {
if (Number.isNaN(num)) {
throw new Error('Query limit error: num is not a number');
}
this.page = 1;
this.perPage = num;
return this;
}
/**
* Builds a query that returns only the minimal attributes specified in the filters
*
* @return {Query} this
*/
showMinimal() {
this.minimal = true;
return this;
}
/**
* Builds the query that returns only the contents that belong to brands the
* mobile_user/device favorited
*
* @return {Query} this
*/
showBrandRelative() {
this.brandRelative = true;
return this;
}
/**
* Builds the query to specify the module that will be used to look for favorited contents
*
* @param {String} val - the string value of the module
* @return {Query} this
*/
referenceModule(val) {
this.reference_module = val;
return this;
}
/**
* Sets the search method to 'address'
*
* @return {Query} this
*/
searchByAddressMode() {
this.filter.attribute = 'address';
return this;
}
/**
* Sets the search method to 'date'.
*
* @return {Query} this
*/
searchByDateMode() {
this.filter.attribute = 'date';
return this;
}
/**
* Set the search method to 'location'
*
* @return {Query} this
*/
searchByLocationMode() {
this.filter.attribute = 'location';
return this;
}
/**
* Sets the search method to 'numeric'.
*
* @return {Query} this
*/
searchByNumberMode() {
this.filter.attribute = 'numeric';
return this;
}
/**
* Sets the search method to 'tag'.
*
* @return {Query} this
*/
searchByTagIDMode() {
this.filter.attribute = 'tag';
return this;
}
/**
* Sets the search method to 'tag_names'.
*
* @return {Query} this
*/
searchByTagNamesMode() {
this.filter.attribute = 'tag_names';
return this;
}
/**
* Sets the radius for location search.
*
* @param {Number|String} radius - number to set the radius value to
* @return {Query} this
*/
setRadius(radius) {
/** @type {Number|String} */
this.filter.radius = radius;
return this;
}
/**
* Sets the exact latitude value for location search/
*
* @param {Number|String} latitude - number to set exact latitude value to
* @return {Query} this
*/
setExactLatitude(latitude) {
/** @type {Number|String} */
this.filter.exact_lat = latitude;
return this;
}
/**
* Sets the exact longitude value for location search.
*
* @param {Number|String} longitude - number to set exact longitude value to
* @return {Query} this
*/
setExactLongitude(longitude) {
/** @type {Number|String} */
this.filter.exact_lng = longitude;
return this;
}
/**
* Sets the SW latitude value for location search
*
* @param {Number|String} latitude - number to set SW latitude value to
* @return {Query} this
*/
setSwLat(latitude) {
/** @type {Number|String} */
this.filter.sw_lat = latitude;
return this;
}
/**
* Sets the SW longitude value for location search.
*
* @param {Number|String} longitude - number to set SW longitude value to
* @return {Query} this
*/
setSwLong(longitude) {
/** @type {Number|String} */
this.filter.sw_lng = longitude;
return this;
}
/**
* Sets the NE latitude value for location search.
*
* @param {Number|String} latitude - number to set NE latitude value to
* @return {Query} this
*/
setNeLat(latitude) {
/** @type {Number|String} */
this.filter.ne_lat = latitude;
return this;
}
/**
* Sets the NE longitude value for location search.
*
* @param {Number|String} longitude - number to set NE longitude value to
* @return {Query} this
*/
setNeLong(longitude) {
/** @type {Number|String} */
this.filter.ne_lng = longitude;
return this;
}
/**
* Sets the NW longitude value for location search.
*
* @param {Number|String} longitude - number to set NW longitude value to
* @return {Query} this
*/
setNwLong(longitude) {
/** @type {Number|String} */
this.filter.nw_lng = longitude;
return this;
}
}
export default Query;