Home Reference Source

lib/ArrayPromise.js

import {splitQuery, splitWords} from './utils';
import {NoSuchColumnError} from './errors';


/**
 * Promise like object for contents array.
 *
 * Almost methods are the same interface as {@link IndexedFTS} and {@link IFTSTransaction}.
 * But this class will processing all contents without using indexes.
 * Please consider using {@link IFTSTransaction} directly if it can.
 */
export default class IFTSArrayPromise {
	/**
	 * @param {Set<string>} indexes - index names.
	 * @param {Promise<object[]>} promise - Promise for wrapping.
	 */
	constructor(indexes, promise) {
		/** @type {Set<string>} */
		this.indexes = indexes;

		/** @type {Promise<object[]>} */
		this.promise = promise;
	}

	/**
	 * Make resolved promise.
	 *
	 * @param {Set<string>} indexes - index names.
	 * @param {object[]} value - value for promise.
	 *
	 * @return {IFTSArrayPromise}
	 */
	static resolve(indexes, value=[]) {
		return new IFTSArrayPromise(indexes, Promise.resolve(value));
	}

	/**
	 * Make rejected promise.
	 *
	 * @param {Set<string>} indexes - index names.
	 * @param {object} value - value for promise.
	 *
	 * @return {IFTSArrayPromise}
	 */
	static reject(indexes, value=null) {
		return new IFTSArrayPromise(indexes, Promise.reject(value));
	}

	/**
	 * Set next function.
	 *
	 * @param {function(contents: object[]): *} fun - next function.
	 *
	 * @return {Promise}
	 */
	then(fun) {
		return this.promise.then(fun);
	}

	/**
	 * Set error handling function.
	 *
	 * @param {function(error: *): *} fun - error handling function.
	 *
	 * @return {Promise}
	 */
	catch(fun) {
		return this.promise.catch(fun);
	}

	/**
	 * Do something process for each elements and make a new IFTSArrayPromise.
	 *
	 * @param {function(content: object, index: Number): object} fun - function for processing element.
	 *
	 * @return {IFTSArrayPromise}
	 */
	map(fun) {
		return new IFTSArrayPromise(this.indexes, this.then(xs => xs.map(fun)));
	}

	/**
	 * Filtering elements by function and make a new IFTSArrayPromise.
	 *
	 * @param {function(content: object, index: Number): boolean} fun - function for filtering element.
	 *
	 * @return {IFTSArrayPromise}
	 */
	filter(fun) {
		return new IFTSArrayPromise(this.indexes, this.then(xs => xs.filter(fun)));
	}

	/**
	 * Sort contents.
	 *
	 * @param {object} column - the column for sorting.
	 * @param {'asc'|'desc'} [order='asc'] - sort order.
	 * @param {Number} [offset=0] - starting offset of the result.
	 * @param {Number} [limit] - maximum number of result length. will unlimited if omitted.
	 *
	 * @return {IFTSArrayPromise} sorted contents.
	 */
	sort(column, order='asc', offset=0, limit=undefined) {
		if (!this.indexes.has(column)) {
			return IFTSArrayPromise.reject(this.indexes, new NoSuchColumnError(column));
		}

		return new IFTSArrayPromise(this.indexes, this.then(xs => Array.prototype.concat.call([], xs).sort((x, y) => {
			if (x[column] < y[column]) {
				return order === 'desc' ? 1 : -1;
			} else if (x[column] > y[column]) {
				return order === 'desc' ? -1 : 1;
			} else {
				return 0;
			}
		}).slice(offset, limit === undefined ? undefined : offset + limit)));
	}

	/**
	 * Checking index of column are exists and do {@link IFTSArrayPromise#filter}.
	 *
	 * @ignore
	 */
	_checkAndFilter(column, fun) {
		if (!this.indexes.has(column)) {
			return IFTSArrayPromise.reject(this.indexes, new NoSuchColumnError(column));
		}

		return this.filter(fun);
	}

	/**
	 * Get contents that have fully matched property.
	 *
	 * @param {object} column - column name for search.
	 * @param {object} value - value for search.
	 *
	 * @return {IFTSArrayPromise} matched contents. may reject with {@link NoSuchColumnError}.
	 */
	equals(column, value) {
		return this._checkAndFilter(column, x => x[column] === value);
	}

	/**
	 * Get contents that have property lower than value.
	 *
	 * @param {object} column - column name for search.
	 * @param {object} value - value for search.
	 *
	 * @return {IFTSArrayPromise} matched contents. may reject with {@link NoSuchColumnError}.
	 */
	lower(column, value) {
		return this._checkAndFilter(column, x => x[column] < value);
	}

	/**
	 * Get contents that have property greater than value.
	 *
	 * @param {object} column - column name for search.
	 * @param {object} value - value for search.
	 *
	 * @return {IFTSArrayPromise} matched contents. may reject with {@link NoSuchColumnError}.
	 */
	greater(column, value) {
		return this._checkAndFilter(column, x => x[column] > value);
	}

	/**
	 * Get contents that have property lower than value or equals value.
	 *
	 * @param {object} column - column name for search.
	 * @param {object} value - value for search.
	 *
	 * @return {IFTSArrayPromise} matched contents. may reject with {@link NoSuchColumnError}.
	 */
	lowerOrEquals(column, value) {
		return this._checkAndFilter(column, x => x[column] <= value);
	}

	/**
	 * Get contents that have property greater than value or equals value.
	 *
	 * @param {object} column - column name for search.
	 * @param {object} value - value for search.
	 *
	 * @return {IFTSArrayPromise} matched contents. may reject with {@link NoSuchColumnError}.
	 */
	greaterOrEquals(column, value) {
		return this._checkAndFilter(column, x => x[column] >= value);
	}

	/**
	 * Get contents that have property is between argument values.
	 *
	 * @param {object} column - column name for search.
	 * @param {object} lower - minimal value.
	 * @param {object} upper - maximum value.
	 *
	 * @return {IFTSArrayPromise} matched contents. may reject with {@link NoSuchColumnError}.
	 */
	between(column, lower, upper) {
		return this._checkAndFilter(column, x => lower <= x[column] && x[column] <= upper);
	}

	/**
	 * Get contents that have matched property by full-text search.
	 *
	 * This method can search even if didn't made ngram index.
	 *
	 * WARNING: This method always processes all contents without using indexes.
	 * Please consider using {@link IFTSTransaction#search}.
	 *
	 *
	 * @param {object|object[]} columns - column names for search.
	 * @param {string} query - query for search.
	 * @param {object} [options] - optional arguments.
	 * @param {boolean} [options.ignoreCase=false] - ignore case if true. default is false.
	 *
	 * @return {IFTSArrayPromise} matched contents.
	 */
	search(columns, query, options={}) {
		if (typeof columns === 'string') {
			columns = [columns];
		}

		for (let c of columns) {
			if (!this.indexes.has(c)) {
				return IFTSArrayPromise.reject(this.indexes, new NoSuchColumnError(c));
			}
		}

		query = options.ignoreCase ? query.toLowerCase() : query;
		const queries = [];
		for (let q in splitQuery(query)) {
			queries.push(q);
		}

		const toLowerIfNeed = options.ignoreCase ? (x => x.toLowerCase()) : (x => x);

		return this.filter(data => queries.every(q => columns.some(col => toLowerIfNeed(data[col]).includes(q))));
	}

	/**
	 * Find contents that have fully matched word in property.
	 *
	 * This method can search even if didn't made word index.
	 *
	 * WARNING: This method always processes all contents without using indexes.
	 * Please consider using {@link IFTSTransaction#searchWord}.
	 *
	 *
	 * @param {object|object[]} columns - column names for search.
	 * @param {string} query - query for search.
	 * @param {object} [options] - optional arguments.
	 * @param {boolean} [options.ignoreCase=false] - ignore case if true. default is false.
	 *
	 * @return {IFTSArrayPromise} matched contents. may reject with {@link NoSuchColumnError}.
	 */
	searchWord(columns, query, options={}) {
		if (typeof columns === 'string') {
			columns = [columns];
		}

		for (let c of columns) {
			if (!this.indexes.has(c)) {
				return IFTSArrayPromise.reject(this.indexes, new NoSuchColumnError(c));
			}
		}

		query = options.ignoreCase ? query.toLowerCase() : query;
		const queries = splitWords(query);

		const toLowerIfNeed = options.ignoreCase ? (x => x.toLowerCase()) : (x => x);

		return this.filter(data => queries.every(q => columns.some(col => {
			return splitWords(toLowerIfNeed(data[col])).includes(q);
		})));
	}
}