manually merge @tigerbot's updates (operator support and a few other things)
This commit is contained in:
parent
c59c0f5e8f
commit
cfd516e24f
367
lib/dbwrap.js
367
lib/dbwrap.js
|
@ -1,5 +1,47 @@
|
||||||
'use strict';
|
'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) {
|
function wrap(db, dir, dbsMap) {
|
||||||
// TODO if I put a failure right here,
|
// TODO if I put a failure right here,
|
||||||
// why doesn't the unhandled promise rejection fire?
|
// why doesn't the unhandled promise rejection fire?
|
||||||
|
@ -12,34 +54,6 @@ function wrap(db, dir, dbsMap) {
|
||||||
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);
|
// PRAGMA schema.table_info(table-name);
|
||||||
//
|
//
|
||||||
function sqlite3GetColumns(tablename, columns, cb) {
|
function sqlite3GetColumns(tablename, columns, cb) {
|
||||||
|
@ -152,14 +166,6 @@ function wrap(db, dir, dbsMap) {
|
||||||
DB._indicesMap[col.name] = col;
|
DB._indicesMap[col.name] = col;
|
||||||
});
|
});
|
||||||
|
|
||||||
function simpleParse(row) {
|
|
||||||
if (!row) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return simpleMap([row])[0] || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function simpleMap(rows) {
|
function simpleMap(rows) {
|
||||||
if (!rows) {
|
if (!rows) {
|
||||||
return [];
|
return [];
|
||||||
|
@ -173,10 +179,10 @@ function wrap(db, dir, dbsMap) {
|
||||||
|
|
||||||
if (row.json) {
|
if (row.json) {
|
||||||
obj = JSON.parse(row.json);
|
obj = JSON.parse(row.json);
|
||||||
delete row.json;
|
|
||||||
} else {
|
} else {
|
||||||
obj = {};
|
obj = {};
|
||||||
}
|
}
|
||||||
|
delete row.json;
|
||||||
|
|
||||||
obj[idnameCased] = row[idname];
|
obj[idnameCased] = row[idname];
|
||||||
delete row[idname];
|
delete row[idname];
|
||||||
|
@ -202,6 +208,93 @@ function wrap(db, dir, dbsMap) {
|
||||||
return results;
|
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) {
|
DB.migrate = function (columns) {
|
||||||
columns.forEach(normalizeColumn);
|
columns.forEach(normalizeColumn);
|
||||||
|
|
||||||
|
@ -233,36 +326,124 @@ function wrap(db, dir, dbsMap) {
|
||||||
return PromiseA.reject(err);
|
return PromiseA.reject(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (obj && keys.length) {
|
if (params && params.limit) {
|
||||||
sql += 'WHERE ';
|
params.limit = parseInt(params.limit, 10);
|
||||||
|
// remember to check for the case of NaN
|
||||||
keys.forEach(function (key, i) {
|
if (!params.limit || params.limit <= 0) {
|
||||||
if (i !== 0) {
|
return PromiseA.reject(new Error('limit must be a positive integer'));
|
||||||
sql += 'AND ';
|
}
|
||||||
}
|
|
||||||
if (null === obj[key]) {
|
|
||||||
sql += db.escape(snakeCase(key)) + " IS null";
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// TODO check that key is some type? ignore undefined?
|
|
||||||
sql += db.escape(snakeCase(key)) + " = '" + db.escape(obj[key]) + "'";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
else if (null !== obj || (params && !params.limit)) {
|
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
|
||||||
|
var split, cmd;
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
value = value.trim();
|
||||||
|
if (['IS NULL', 'IS NOT NULL'].indexOf(value.toUpperCase()) !== -1) {
|
||||||
|
return dbKey + ' ' + value.toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
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(',') + ')';
|
||||||
|
}
|
||||||
|
// 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>> })"));
|
return PromiseA.reject(new Error("to find all you must explicitly specify find(null, { limit: <<int>> })"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params) {
|
if (params) {
|
||||||
|
if (typeof params.orderByDesc === 'string' && !params.orderBy) {
|
||||||
|
params.orderBy = params.orderByDesc;
|
||||||
|
params.orderByDesc = true;
|
||||||
|
}
|
||||||
if (params.orderBy) {
|
if (params.orderBy) {
|
||||||
sql += " ORDER BY \"" + db.escape(snakeCase(params.orderBy)) + "\" ";
|
sql += " ORDER BY '" + db.escape(snakeCase(params.orderBy)) + "' ";
|
||||||
if (params.orderByDesc) {
|
if (params.orderByDesc) {
|
||||||
sql += "DESC ";
|
sql += "DESC ";
|
||||||
}
|
}
|
||||||
} else if (DB._indicesMap.updated_at) {
|
} else if (DB._indicesMap.updated_at) {
|
||||||
sql += " ORDER BY \"updated_at\" DESC ";
|
sql += " ORDER BY 'updated_at' DESC ";
|
||||||
} else if (DB._indicesMap.created_at) {
|
} else if (DB._indicesMap.created_at) {
|
||||||
sql += " ORDER BY \"created_at\" DESC ";
|
sql += " ORDER BY 'created_at' DESC ";
|
||||||
}
|
}
|
||||||
if (isFinite(params.limit)) {
|
if (isFinite(params.limit)) {
|
||||||
sql += " LIMIT " + parseInt(params.limit, 10);
|
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) {
|
DB.set = function (id, obj, oldId) {
|
||||||
obj.updatedAt = Date.now();
|
obj.updatedAt = Date.now();
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "masterquest-sqlite3",
|
"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.",
|
"description": "A NoSQL / SQLite3 Hybrid. All your indices are belong to us. Master Quest.",
|
||||||
"main": "lib/dbwrap",
|
"main": "lib/dbwrap",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
Loading…
Reference in New Issue