manually merge @tigerbot's updates (operator support and a few other things)
This commit is contained in:
		
							parent
							
								
									c59c0f5e8f
								
							
						
					
					
						commit
						cfd516e24f
					
				
							
								
								
									
										357
									
								
								lib/dbwrap.js
									
									
									
									
									
								
							
							
						
						
									
										357
									
								
								lib/dbwrap.js
									
									
									
									
									
								
							@ -1,5 +1,47 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
function lowerFirst(str) {
 | 
			
		||||
  return str.charAt(0).toLowerCase() + str.substr(1);
 | 
			
		||||
}
 | 
			
		||||
function snakeCase(str) {
 | 
			
		||||
  return lowerFirst(str).replace(/([A-Z])/g, function (match) {
 | 
			
		||||
    return "_" + match.toLowerCase();
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
function camelCase(str) {
 | 
			
		||||
  return str.replace(/_([a-z])/g, function (match) {
 | 
			
		||||
    return match[1].toUpperCase();
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
function upperCamelCase(str) {
 | 
			
		||||
  var camel = camelCase(str);
 | 
			
		||||
  return camel.charAt(0).toUpperCase() + camel.substr(1);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var searchConditions = {
 | 
			
		||||
  '=':  true,
 | 
			
		||||
  '==': true,
 | 
			
		||||
  '!=': true,
 | 
			
		||||
  '<>': true,
 | 
			
		||||
  '<':  true,
 | 
			
		||||
  '<=': true,
 | 
			
		||||
  '!<': true,
 | 
			
		||||
  '>':  true,
 | 
			
		||||
  '>=': true,
 | 
			
		||||
  '!>': true,
 | 
			
		||||
  'IS': true,
 | 
			
		||||
  'IS NOT': true,
 | 
			
		||||
 | 
			
		||||
  'IN':          true,
 | 
			
		||||
  'NOT IN':      true,
 | 
			
		||||
  'LIKE':        true,
 | 
			
		||||
  'NOT LIKE':    true,
 | 
			
		||||
  'GLOB':        true,
 | 
			
		||||
  'NOT GLOB':    true,
 | 
			
		||||
  'BETWEEN':     true,
 | 
			
		||||
  'NOT BETWEEN': true,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function wrap(db, dir, dbsMap) {
 | 
			
		||||
  // TODO if I put a failure right here,
 | 
			
		||||
  // why doesn't the unhandled promise rejection fire?
 | 
			
		||||
@ -12,34 +54,6 @@ function wrap(db, dir, dbsMap) {
 | 
			
		||||
    dbsMap = {};
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function lowerFirst(str) {
 | 
			
		||||
    return str.charAt(0).toLowerCase() + str.slice(1);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function snakeCase(str) {
 | 
			
		||||
    return lowerFirst(str).replace(
 | 
			
		||||
      /([A-Z])/g
 | 
			
		||||
    , function ($1) {
 | 
			
		||||
        return "_" + $1.toLowerCase();
 | 
			
		||||
      }
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function camelCase(str) {
 | 
			
		||||
    str = str.replace(
 | 
			
		||||
      /_([a-z])/g
 | 
			
		||||
    , function (g) {
 | 
			
		||||
        return g[1].toUpperCase();
 | 
			
		||||
      }
 | 
			
		||||
    );
 | 
			
		||||
    return str;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function upperCamelCase(str) {
 | 
			
		||||
    // TODO handle UTF-8 properly (use codePointAt, don't use slice)
 | 
			
		||||
    return str.charAt(0).toUpperCase() + camelCase(str).slice(1);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // PRAGMA schema.table_info(table-name);
 | 
			
		||||
  //
 | 
			
		||||
  function sqlite3GetColumns(tablename, columns, cb) {
 | 
			
		||||
@ -152,14 +166,6 @@ function wrap(db, dir, dbsMap) {
 | 
			
		||||
      DB._indicesMap[col.name] = col;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    function simpleParse(row) {
 | 
			
		||||
      if (!row) {
 | 
			
		||||
        return null;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return simpleMap([row])[0] || null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function simpleMap(rows) {
 | 
			
		||||
      if (!rows) {
 | 
			
		||||
        return [];
 | 
			
		||||
@ -173,10 +179,10 @@ function wrap(db, dir, dbsMap) {
 | 
			
		||||
 | 
			
		||||
        if (row.json) {
 | 
			
		||||
          obj = JSON.parse(row.json);
 | 
			
		||||
          delete row.json;
 | 
			
		||||
        } else {
 | 
			
		||||
          obj = {};
 | 
			
		||||
        }
 | 
			
		||||
        delete row.json;
 | 
			
		||||
 | 
			
		||||
        obj[idnameCased] = row[idname];
 | 
			
		||||
        delete row[idname];
 | 
			
		||||
@ -202,6 +208,93 @@ function wrap(db, dir, dbsMap) {
 | 
			
		||||
      return results;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function simpleParse(row) {
 | 
			
		||||
      if (!row) {
 | 
			
		||||
        return null;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return simpleMap([row])[0] || null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // pull indices from object
 | 
			
		||||
    function strainUpdate(id, data/*, vals*/, cb, oldId) {
 | 
			
		||||
      var fieldable = [];
 | 
			
		||||
      var sql;
 | 
			
		||||
      var vals = [];
 | 
			
		||||
 | 
			
		||||
      ['hasOne', 'hasMany', 'hasAndBelongsToMany', 'belongsTo', 'belongsToMany'].forEach(function (relname) {
 | 
			
		||||
        var rels = dir[relname];
 | 
			
		||||
 | 
			
		||||
        if (!rels) {
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!Array.isArray(rels)) {
 | 
			
		||||
          rels = [rels];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // don't save relationships
 | 
			
		||||
        rels.forEach(function (colname) {
 | 
			
		||||
          delete data[colname];
 | 
			
		||||
          delete data[camelCase(colname)];
 | 
			
		||||
          // TODO placehold relationships on find / get?
 | 
			
		||||
          // data[camelCase(colname)] = null;
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      dir.indices.forEach(function (col) {
 | 
			
		||||
        // We prioritze the raw name rather than the camelCase name because it's not in the object
 | 
			
		||||
        // we give for retrieved entries, so if it's present then the user put it there themselves.
 | 
			
		||||
        var val = data[col.name] || data[camelCase(col.name)];
 | 
			
		||||
 | 
			
		||||
        //if (col.name in data)
 | 
			
		||||
        if ('undefined' !== typeof val) {
 | 
			
		||||
          /*
 | 
			
		||||
          fieldable.push(
 | 
			
		||||
            db.escape(snakeCase(col.name))
 | 
			
		||||
          + " = '" + db.escape(val) + "'"
 | 
			
		||||
          );
 | 
			
		||||
          */
 | 
			
		||||
          fieldable.push(db.escape(snakeCase(col.name)));
 | 
			
		||||
          vals.push(val);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        delete data[col.name];
 | 
			
		||||
        delete data[camelCase(col.name)];
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      if (!oldId) {
 | 
			
		||||
        delete data[idnameCased];
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (!fieldable.length || Object.keys(data).length) {
 | 
			
		||||
        vals.push(JSON.stringify(data));
 | 
			
		||||
      } else {
 | 
			
		||||
        vals.push(null);
 | 
			
		||||
      }
 | 
			
		||||
      fieldable.push('json');
 | 
			
		||||
 | 
			
		||||
      vals.push(id);
 | 
			
		||||
 | 
			
		||||
      sql = cb(fieldable);
 | 
			
		||||
 | 
			
		||||
      if (debug) {
 | 
			
		||||
        console.log('[masterquest-sqlite3] dbwrap.js');
 | 
			
		||||
        console.log(sql);
 | 
			
		||||
        console.log(vals);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      vals.forEach(function (val) {
 | 
			
		||||
        if (null === val || 'number' === typeof val) {
 | 
			
		||||
          sql = sql.replace('?', String(val));
 | 
			
		||||
        } else {
 | 
			
		||||
          sql = sql.replace('?', "'" + db.escape(val) + "'");
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      return sql;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    DB.migrate = function (columns) {
 | 
			
		||||
      columns.forEach(normalizeColumn);
 | 
			
		||||
 | 
			
		||||
@ -233,36 +326,124 @@ function wrap(db, dir, dbsMap) {
 | 
			
		||||
        return PromiseA.reject(err);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (obj && keys.length) {
 | 
			
		||||
        sql += 'WHERE ';
 | 
			
		||||
      if (params && params.limit) {
 | 
			
		||||
        params.limit = parseInt(params.limit, 10);
 | 
			
		||||
        // remember to check for the case of NaN
 | 
			
		||||
        if (!params.limit || params.limit <= 0) {
 | 
			
		||||
          return PromiseA.reject(new Error('limit must be a positive integer'));
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
        keys.forEach(function (key, i) {
 | 
			
		||||
          if (i !== 0) {
 | 
			
		||||
            sql += 'AND ';
 | 
			
		||||
      if (obj && keys.length) {
 | 
			
		||||
        var conditions = keys.map(function (key) {
 | 
			
		||||
          var dbKey = db.escape(snakeCase(key));
 | 
			
		||||
          var value = obj[key];
 | 
			
		||||
          if (null === value) {
 | 
			
		||||
            return dbKey + ' IS NULL';
 | 
			
		||||
          }
 | 
			
		||||
          if (null === obj[key]) {
 | 
			
		||||
            sql += db.escape(snakeCase(key)) + " IS null";
 | 
			
		||||
 | 
			
		||||
          var split, cmd;
 | 
			
		||||
          if (typeof value === 'string') {
 | 
			
		||||
            value = value.trim();
 | 
			
		||||
            if (['IS NULL', 'IS NOT NULL'].indexOf(value.toUpperCase()) !== -1) {
 | 
			
		||||
              return dbKey + ' ' + value.toUpperCase();
 | 
			
		||||
            }
 | 
			
		||||
          else {
 | 
			
		||||
            // TODO check that key is some type? ignore undefined?
 | 
			
		||||
            sql += db.escape(snakeCase(key)) + " = '" + db.escape(obj[key]) + "'";
 | 
			
		||||
 | 
			
		||||
            split = value.split(' ');
 | 
			
		||||
            if (searchConditions[split[0].toUpperCase()]) {
 | 
			
		||||
              cmd = split[0].toUpperCase();
 | 
			
		||||
              value = split.slice(1).join(' ');
 | 
			
		||||
            } else if (searchConditions[split.slice(0, 2).join(' ').toUpperCase()]) {
 | 
			
		||||
              cmd = split.slice(0, 2).join(' ').toUpperCase();
 | 
			
		||||
              value = split.slice(2).join(' ');
 | 
			
		||||
            }
 | 
			
		||||
            // If we were given something like "BEGINS WITH 'something quoted'" we don't want
 | 
			
		||||
            // to include the quotes (we'll quote it again later) so we strip them out here.
 | 
			
		||||
            if (cmd) {
 | 
			
		||||
              value = value.replace(/^(['"])(.*)\1$/, '$2');
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          if (typeof value === 'object') {
 | 
			
		||||
            cmd = value.condition || value.relation || value.cmd;
 | 
			
		||||
            value = value.value;
 | 
			
		||||
            if (!cmd || !value) {
 | 
			
		||||
              err = new Error("'"+key+"' was an object, but missing condition and/or value");
 | 
			
		||||
              return;
 | 
			
		||||
            }
 | 
			
		||||
            if (typeof cmd !== 'string' || !searchConditions[cmd.toUpperCase()]) {
 | 
			
		||||
              err = new Error("'"+key+"' tried to use invalid condition '"+cmd+"'");
 | 
			
		||||
              return;
 | 
			
		||||
            } else {
 | 
			
		||||
              cmd = cmd.toUpperCase();
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          if (!cmd) {
 | 
			
		||||
            cmd = '=';
 | 
			
		||||
          }
 | 
			
		||||
          // The IN condition is special in that we can't quote the value as a single value,
 | 
			
		||||
          // so it requires a little more logic to actually work and still be sanitary.
 | 
			
		||||
          if (cmd === 'IN' || cmd === 'NOT IN') {
 | 
			
		||||
            if (typeof value === 'string') {
 | 
			
		||||
              value = value.split((params || {}).seperator || /[\s,]+/);
 | 
			
		||||
            }
 | 
			
		||||
            if (!Array.isArray(value)) {
 | 
			
		||||
              err = new Error("'"+key+"' has invalid value for use with 'IN'");
 | 
			
		||||
              return;
 | 
			
		||||
            }
 | 
			
		||||
            value = value.map(function (val) {
 | 
			
		||||
              return "'"+db.escape(val)+"'";
 | 
			
		||||
            });
 | 
			
		||||
            return dbKey + ' ' + cmd + ' (' + value.join(',') + ')';
 | 
			
		||||
          }
 | 
			
		||||
      else if (null !== obj || (params && !params.limit)) {
 | 
			
		||||
          // The BETWEEN condition is also special for the same reason as IN
 | 
			
		||||
          if (cmd === 'BETWEEN' || cmd === 'NOT BETWEEN') {
 | 
			
		||||
            if (typeof value === 'string') {
 | 
			
		||||
              value = value.split((params || {}).seperator || /[\s,]+(AND\s+)?/i);
 | 
			
		||||
            }
 | 
			
		||||
            if (!Array.isArray(value) || value.length !== 2) {
 | 
			
		||||
              err = new Error("'"+key+"' has invalid value for use with 'BETWEEN'");
 | 
			
		||||
              return;
 | 
			
		||||
            }
 | 
			
		||||
            value = value.map(function (val) {
 | 
			
		||||
              return "'"+db.escape(val)+"'";
 | 
			
		||||
            });
 | 
			
		||||
            return dbKey + ' ' + cmd + ' ' + value.join(' AND ');
 | 
			
		||||
          }
 | 
			
		||||
          // If we are supposed to compare to another field then make sure the name is correct,
 | 
			
		||||
          // and that we don't try to quote the name.
 | 
			
		||||
          if (typeof value === 'string' && /^[a-zA-Z0-9_]*$/.test(value)) {
 | 
			
		||||
            var snake = snakeCase(value);
 | 
			
		||||
            if (dir.indices.some(function (col) { return snake === col.name; })) {
 | 
			
		||||
              return dbKey + ' ' + cmd + ' ' + snake;
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          return dbKey + ' ' + cmd + " '" + db.escape(value) + "'";
 | 
			
		||||
        });
 | 
			
		||||
        if (err) {
 | 
			
		||||
          return PromiseA.reject(err);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        sql += 'WHERE ' + conditions.join(' AND ');
 | 
			
		||||
      }
 | 
			
		||||
      else if (null !== obj || !(params && params.limit)) {
 | 
			
		||||
        return PromiseA.reject(new Error("to find all you must explicitly specify find(null, { limit: <<int>> })"));
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (params) {
 | 
			
		||||
        if (typeof params.orderByDesc === 'string' && !params.orderBy) {
 | 
			
		||||
          params.orderBy = params.orderByDesc;
 | 
			
		||||
          params.orderByDesc = true;
 | 
			
		||||
        }
 | 
			
		||||
        if (params.orderBy) {
 | 
			
		||||
          sql += " ORDER BY \"" + db.escape(snakeCase(params.orderBy)) + "\" ";
 | 
			
		||||
          sql += " ORDER BY '" + db.escape(snakeCase(params.orderBy)) + "' ";
 | 
			
		||||
          if (params.orderByDesc) {
 | 
			
		||||
            sql += "DESC ";
 | 
			
		||||
          }
 | 
			
		||||
        } else if (DB._indicesMap.updated_at) {
 | 
			
		||||
          sql += " ORDER BY \"updated_at\" DESC ";
 | 
			
		||||
          sql += " ORDER BY 'updated_at' DESC ";
 | 
			
		||||
        } else if (DB._indicesMap.created_at) {
 | 
			
		||||
          sql += " ORDER BY \"created_at\" DESC ";
 | 
			
		||||
          sql += " ORDER BY 'created_at' DESC ";
 | 
			
		||||
        }
 | 
			
		||||
        if (isFinite(params.limit)) {
 | 
			
		||||
          sql += " LIMIT " + parseInt(params.limit, 10);
 | 
			
		||||
@ -380,80 +561,6 @@ function wrap(db, dir, dbsMap) {
 | 
			
		||||
      });
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // pull indices from object
 | 
			
		||||
    function strainUpdate(id, data/*, vals*/, cb, oldId) {
 | 
			
		||||
      var fieldable = [];
 | 
			
		||||
      var json;
 | 
			
		||||
      var sql;
 | 
			
		||||
      var vals = [];
 | 
			
		||||
 | 
			
		||||
      ['hasOne', 'hasMany', 'hasAndBelongsToMany', 'belongsTo', 'belongsToMany'].forEach(function (relname) {
 | 
			
		||||
        var rels = dir[relname];
 | 
			
		||||
 | 
			
		||||
        if (!rels) {
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!Array.isArray(rels)) {
 | 
			
		||||
          rels = [rels];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // don't save relationships
 | 
			
		||||
        rels.forEach(function (colname) {
 | 
			
		||||
          delete data[colname];
 | 
			
		||||
          delete data[camelCase(colname)];
 | 
			
		||||
          // TODO placehold relationships on find / get?
 | 
			
		||||
          // data[camelCase(colname)] = null;
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      dir.indices.forEach(function (col) {
 | 
			
		||||
        var val = data[camelCase(col.name)];
 | 
			
		||||
 | 
			
		||||
        //if (col.name in data)
 | 
			
		||||
        if ('undefined' !== typeof val) {
 | 
			
		||||
          /*
 | 
			
		||||
          fieldable.push(
 | 
			
		||||
            db.escape(snakeCase(col.name))
 | 
			
		||||
          + " = '" + db.escape(val) + "'"
 | 
			
		||||
          );
 | 
			
		||||
          */
 | 
			
		||||
          fieldable.push(db.escape(snakeCase(col.name)));
 | 
			
		||||
          vals.push(val);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        delete data[col.name];
 | 
			
		||||
        delete data[camelCase(col.name)];
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      if (!oldId) {
 | 
			
		||||
        delete data[idnameCased];
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (!fieldable.length || Object.keys(data).length) {
 | 
			
		||||
        json = JSON.stringify(data);
 | 
			
		||||
        fieldable.push("json");
 | 
			
		||||
        //fieldable.push("json = '" + db.escape(json) + "'");
 | 
			
		||||
        vals.push(json);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      vals.push(id);
 | 
			
		||||
 | 
			
		||||
      sql = cb(fieldable);
 | 
			
		||||
 | 
			
		||||
      if (debug) {
 | 
			
		||||
        console.log('[masterquest-sqlite3] dbwrap.js');
 | 
			
		||||
        console.log(sql);
 | 
			
		||||
        console.log(vals);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      while (vals.length) {
 | 
			
		||||
        sql = sql.replace(/\?/, "'" + db.escape(vals.shift()) + "'");
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return sql;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    DB.set = function (id, obj, oldId) {
 | 
			
		||||
      obj.updatedAt = Date.now();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "masterquest-sqlite3",
 | 
			
		||||
  "version": "1.1.0",
 | 
			
		||||
  "version": "1.2.0",
 | 
			
		||||
  "description": "A NoSQL / SQLite3 Hybrid. All your indices are belong to us. Master Quest.",
 | 
			
		||||
  "main": "lib/dbwrap",
 | 
			
		||||
  "scripts": {
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user