Home Identifier Source Repository

src/lib/filter-by-where-clause.js

import assertKeys from './assert-keys'
import compareCourseToQualification from './compare-course-to-qualification'
import filter from 'lodash/collection/filter'
import forEach from 'lodash/collection/forEach'
import has from 'lodash/object/has'
import isPlainObject from 'lodash/lang/isPlainObject'
import max from 'lodash/collection/max'
import min from 'lodash/collection/min'
import pluck from 'lodash/collection/pluck'
import simplifyCourse from './simplify-course'
import uniq from 'lodash/array/uniq'

export default function filterByWhereClause(baseList, clause, fullList) {
	// When filtering by an and-clause, we need access to both the
	// entire list of courses, and the result of the prior iteration.
	// To simplify future invocations, we default to `fullList = list`
	if (!fullList) {
		fullList = baseList
	}

	// There are only two types of where-clauses: boolean, and qualification.
	// Boolean where-clauses are comprised of a set of qualifications.

	// This function always reduces down to a call to filterByQualification
	if (clause.$type === 'qualification') {
		return filterByQualification(baseList, clause, fullList)
	}

	// either an and- or or-clause.
	else if (clause.$type === 'boolean') {
		// and-clauses become the result of applying each invocation to the
		// result of the prior one. they are the list of unique courses which
		// meet all of the qualifications.
		if (has(clause, '$and')) {
			let filtered = baseList
			forEach(clause.$and, q => {
				filtered = filterByWhereClause(filtered, q, fullList)
			})
			return filtered
		}

		// or-clauses are the list of unique courses that meet one or more
		// of the qualifications.
		else if (has(clause, '$or')) {
			let filtrations = []
			forEach(clause.$or, q => {
				filtrations = filtrations.concat(filterByWhereClause(baseList, q))
			})

			// join together the list of lists of possibilities,
			// then uniquify them by way of turning them into the simplified representations.
			return uniq(filtrations, simplifyCourse)
		}

		// only 'and' and 'or' are currently supported.
		else {
			throw new TypeError(`filterByWhereClause(): neither $or nor $and could be found in ${JSON.stringify(clause)}`)
		}
	}

	// where-clauses *must* be either a 'boolean' or a 'qualification'
	else {
		throw new TypeError(`filterByWhereClause(): wth kind of type is a "${clause.$type}" clause?`)
	}
}

const qualificationFunctionLookup = {
	max: max,
	min: min,
}

export function filterByQualification(list, qualification, fullList) {
	assertKeys(qualification, '$key', '$operator', '$value')
	const value = qualification.$value

	if (isPlainObject(value)) {
		if (value.$type === 'function') {
			const func = qualificationFunctionLookup[value.$name]

			if (!func) {
				throw new ReferenceError(`filterByQualification(): ${value.$name} is not a valid function to call.`)
			}

			const completeList = fullList || list
			const filtered = filterByWhereClause(completeList, value.$where)
			const items = pluck(filtered, value.$prop)
			const computed = func(items)
			// console.log('looked at', completeList)
			// console.log('reduced to', filtered)
			// console.log('came up with', computed)
			value['$computed-value'] = computed
		}
		else if (value.$type === 'boolean') {
			if (!(value.hasOwnProperty('$or') || value.hasOwnProperty('$and'))) {
				throw new TypeError(`compareCourseToQualification(): neither $or nor $and could be found in ${JSON.stringify(value)}`)
			}
		}
		else {
			throw new TypeError(`filterByQualification(): ${value.$type} is not a valid type for a query.`)
		}
	}

	const filtered = filter(list, course =>
		compareCourseToQualification(course, qualification))

	return filtered
}