Scholar.Search = function(){ this._sql = null; this._sqlParams = null; this._maxSearchConditionID = 0; this._conditions = []; this._savedSearchID = null; this._savedSearchName = null; } /* * Set the name for the saved search * * Must be called before save() for new searches */ Scholar.Search.prototype.setName = function(name){ if (!name){ throw("Invalid saved search name '" + name + '"'); } this._savedSearchName = name; } /* * Load a saved search from the DB */ Scholar.Search.prototype.load = function(savedSearchID){ var sql = "SELECT savedSearchName, MAX(searchConditionID) AS maxID " + "FROM savedSearches NATURAL JOIN savedSearchConditions " + "WHERE savedSearchID=" + savedSearchID + " GROUP BY savedSearchID"; var row = Scholar.DB.rowQuery(sql); if (!row){ throw('Saved search ' + savedSearchID + ' does not exist'); } this._sql = null; this._sqlParams = null; this._maxSearchConditionID = row['maxID']; this._conditions = []; this._savedSearchID = savedSearchID; this._savedSearchName = row['savedSearchName']; var conditions = Scholar.DB.query("SELECT * FROM savedSearchConditions " + "WHERE savedSearchID=" + savedSearchID + " ORDER BY searchConditionID"); for (var i in conditions){ this._conditions[conditions[i]['searchConditionID']] = { condition: conditions[i]['condition'], operator: conditions[i]['operator'], value: conditions[i]['value'] }; } } /* * Save the search to the DB and return a savedSearchID * * For new searches, setName() must be called before saving */ Scholar.Search.prototype.save = function(){ if (!this._savedSearchName){ throw('Name not provided for saved search'); } Scholar.DB.beginTransaction(); if (this._savedSearchID){ var sql = "UPDATE savedSearches SET savedSearchName=? WHERE savedSearchID=?"; Scholar.DB.query(sql, [this._savedSearchName, this._savedSearchID]); Scholar.DB.query("DELETE FROM savedSearchConditions " + "WHERE savedSearchID=" + this._savedSearchID); } else { this._savedSearchID = Scholar.getRandomID('savedSearches', 'savedSearchID'); var sql = "INSERT INTO savedSearches (savedSearchID, savedSearchName) " + "VALUES (?,?)"; Scholar.DB.query(sql, [this._savedSearchID, {string: this._savedSearchName}]); } // TODO: use proper bound parameters once DB class is updated for (var i in this._conditions){ var sql = "INSERT INTO savedSearchConditions (savedSearchID, " + "searchConditionID, condition, operator, value) VALUES (?,?,?,?,?)"; var sqlParams = [this._savedSearchID, i, this._conditions[i]['condition'], this._conditions[i]['operator'] ? this._conditions[i]['operator'] : null, this._conditions[i]['value'] ? this._conditions[i]['value'] : null]; Scholar.DB.query(sql, sqlParams); } Scholar.DB.commitTransaction(); return this._savedSearchID; } Scholar.Search.prototype.addCondition = function(condition, operator, value){ if (!Scholar.SearchConditions.hasOperator(condition, operator)){ throw ("Invalid operator '" + operator + "' for condition " + condition); } var searchConditionID = this._maxSearchConditionID++; this._conditions[searchConditionID] = { condition: condition, operator: operator, value: value }; this._sql = null; this._sqlParams = null; return searchConditionID; } Scholar.Search.prototype.updateCondition = function(searchConditionID, condition, operator, value){ if (typeof this._conditions[searchConditionID] == 'undefined'){ throw ('Invalid searchConditionID ' + searchConditionID + ' in updateCondition()'); } if (!Scholar.SearchConditions.hasOperator(condition, operator)){ throw ("Invalid operator '" + operator + "' for condition " + condition); } this._conditions[searchConditionID] = { condition: condition, operator: operator, value: value }; this._sql = null; this._sqlParams = null; } Scholar.Search.prototype.removeCondition = function(searchConditionID){ if (typeof this._conditions[searchConditionID] == 'undefined'){ throw ('Invalid searchConditionID ' + searchConditionID + ' in removeCondition()'); } delete this._conditions[searchConditionID]; var i = searchConditionID + 1; while (typeof this._conditions[i] != 'undefined'){ this._conditions[i-1] = this._conditions[i]; delete this._conditions[i]; i++; } this._maxSearchConditionID--; } /* * Returns an array with 'condition', 'operator', and 'value' * for the given searchConditionID */ Scholar.Search.prototype.getSearchCondition = function(searchConditionID){ return this._conditions[searchConditionID]; } /* * Returns a multidimensional array of conditions/operator/value sets * used in the search, indexed by searchConditionID */ Scholar.Search.prototype.getSearchConditions = function(){ // TODO: make copy return this._conditions; } /* * Run the search and return an array of item ids for results */ Scholar.Search.prototype.search = function(){ if (!this._sql){ this._buildQuery(); } return Scholar.DB.columnQuery(this._sql, this._sqlParams); } /* * Get the SQL string for the search */ Scholar.Search.prototype.getSQL = function(){ if (!this._sql){ this._buildQuery(); } return this._sql; } /* * Build the SQL query for the search */ Scholar.Search.prototype._buildQuery = function(){ var sql = 'SELECT itemID FROM items'; var sqlParams = []; var tables = []; for (var i in this._conditions){ var data = Scholar.SearchConditions.get(this._conditions[i]['condition']); // Group standard conditions by table if (data['table']){ if (!tables[data['table']]){ tables[data['table']] = []; } tables[data['table']].push({ name: data['name'], alias: data['name']!=this._conditions[i]['condition'] ? this._conditions[i]['condition'] : false, field: data['field'], operator: this._conditions[i]['operator'], value: this._conditions[i]['value'] }); var hasConditions = true; } // Handle special conditions else { switch (data['name']){ // Collection to search in case 'context': var parentCollectionID = this._conditions[i]['value']; continue; // Search subfolders case 'recursive': var recursive = this._conditions[i]['operator']=='true'; continue; // Join mode ('any' or 'all') case 'joinMode': var joinMode = this._conditions[i]['operator'].toUpperCase(); continue; } throw ('Unhandled special condition ' + this._conditions[i]['condition']); } } if (hasConditions){ sql += " WHERE "; // Join conditions using appropriate operator if (joinMode=='ALL'){ var binOp = ' AND '; } else { var binOp = ' OR '; } for (i in tables){ for (var j in tables[i]){ // Special table handling switch (i){ case 'items': break; default: sql += 'itemID IN (SELECT itemID FROM ' + i + ' WHERE ('; var openParens = 2; } // For itemData fields, include fieldID if (tables[i][j]['name']=='field'){ sql += 'fieldID=? AND '; sqlParams.push(Scholar.ItemFields.getID(tables[i][j]['alias'])); } sql += tables[i][j]['field']; switch (tables[i][j]['operator']){ case 'contains': sql += ' LIKE ?'; sqlParams.push('%' + tables[i][j]['value'] + '%'); break; case 'doesNotContain': sql += ' NOT LIKE ?'; sqlParams.push('%' + tables[i][j]['value'] + '%'); break; case 'is': sql += '=?'; sqlParams.push(tables[i][j]['value']); break; case 'isNot': sql += '!=?'; sqlParams.push(tables[i][j]['value']); break; case 'greaterThan': sql += '>?'; sqlParams.push({int:tables[i][j]['value']}); break; case 'lessThan': sql += '0; k--){ sql += ')'; } sql += binOp; } } sql = sql.substring(0, sql.length-binOp.length); } if (parentCollectionID){ sql = "SELECT itemID FROM (" + sql + ") WHERE itemID IN " + "(SELECT itemID FROM collectionItems WHERE collectionID IN (" sql += "?,"; sqlParams.push({int:parentCollectionID}); if (recursive){ var col = Scholar.Collections.get(parentCollectionID); var descendents = col.getDescendents(false, 'collection'); if (descendents){ for (var i in descendents){ sql += '?,'; sqlParams.push(descendents[i]['id']); } } } // Strip final comma sql = sql.substring(0, sql.length-1) + "))"; } this._sql = sql; this._sqlParams = sqlParams.length ? sqlParams : null; } Scholar.Searches = new function(){ this.getAll = getAll; this.erase = erase; /* * Returns an array of saved searches with 'id' and 'name', ordered by name */ function getAll(){ var sql = "SELECT savedSearchID AS id, savedSearchName AS name " + "FROM savedSearches ORDER BY name"; return Scholar.DB.query(sql); } /* * Delete a given saved search from the DB */ function erase(savedSearchID){ Scholar.DB.beginTransaction(); var sql = "DELETE FROM savedSearchConditions WHERE savedSearchID=" + savedSearchID; Scholar.DB.query(sql); var sql = "DELETE FROM savedSearches WHERE savedSearchID=" + savedSearchID; Scholar.DB.query(sql); Scholar.DB.commitTransaction(); } } Scholar.SearchConditions = new function(){ this.get = get; this.getStandardConditions = getStandardConditions; this.hasOperator = hasOperator; var _initialized = false; var _conditions = []; var _standardConditions = []; /* * Define the advanced search operators */ var _operators = { // Standard is: true, isNot: true, contains: true, doesNotContain: true, lessThan: true, greaterThan: true, isBefore: true, isAfter: true, // Special any: true, all: true, true: true, false: true }; /* * Define and set up the available advanced search conditions */ function _init(){ _conditions = [ // // Special conditions // // Context (i.e. collection id to search within) { name: 'context' }, // Search recursively { name: 'recursive', operators: { true: true, false: true } }, // Join mode { name: 'joinMode', operators: { any: true, all: true } }, // // Standard conditions // { name: 'title', operators: { contains: true, doesNotContain: true }, table: 'items', field: 'title' }, { name: 'itemType', operators: { is: true, isNot: true }, table: 'items', field: 'itemTypeID' }, { name: 'field', operators: { is: true, isNot: true, contains: true, doesNotContain: true }, table: 'itemData', field: 'value', aliases: Scholar.DB.columnQuery("SELECT fieldName FROM fields"), template: true // mark for special handling } ]; // Index conditions by name and aliases for (var i in _conditions){ _conditions[_conditions[i]['name']] = _conditions[i]; if (_conditions[i]['aliases']){ for (var j in _conditions[i]['aliases']){ _conditions[_conditions[i]['aliases'][j]] = _conditions[i]; } } _conditions[_conditions[i]['name']] = _conditions[i]; delete _conditions[i]; } // Separate standard conditions for menu display for (var i in _conditions){ // Standard conditions a have associated tables if (_conditions[i]['table'] && // If a template condition, not the original (e.g. 'field') (!_conditions[i]['template'] || i!=_conditions[i]['name'])){ _standardConditions.push({ name: i, operators: _conditions[i]['operators'] }); } } _initialized = true; } /* * Get condition data */ function get(condition){ if (!_initialized){ _init(); } return _conditions[condition]; } /* * Returns array of possible conditions * * Does not include special conditions, only ones that would show in a drop-down list */ function getStandardConditions(){ if (!_initialized){ _init(); } // TODO: return copy instead return _standardConditions; } /* * Check if an operator is valid for a given condition */ function hasOperator(condition, operator){ if (!_initialized){ _init(); } if (!_conditions[condition]){ throw ("Invalid condition '" + condition + "' in hasOperator()"); } if (!operator && typeof _conditions[condition]['operators'] == 'undefined'){ return true; } return !!_conditions[condition]['operators'][operator]; } }