Compare commits

..

No commits in common. "master" and "v1.1.0" have entirely different histories.

3 changed files with 134 additions and 266 deletions

View File

@ -1,52 +1,9 @@
'use strict'; 'use strict';
/*global Promise*/
var PromiseA = Promise;
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?
var PromiseA = require('bluebird');
var promises = []; var promises = [];
var earr = []; var earr = [];
var debug = false; var debug = false;
@ -55,6 +12,34 @@ 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) {
@ -162,10 +147,14 @@ function wrap(db, dir, dbsMap) {
dir.indices.forEach(normalizeColumn); dir.indices.forEach(normalizeColumn);
DB._indices = dir.indices; DB._indices = dir.indices;
DB._indicesMap = {};
DB._indices.forEach(function (col) { function simpleParse(row) {
DB._indicesMap[col.name] = col; if (!row) {
}); return null;
}
return simpleMap([row])[0] || null;
}
function simpleMap(rows) { function simpleMap(rows) {
if (!rows) { if (!rows) {
@ -180,10 +169,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];
@ -209,93 +198,6 @@ 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);
@ -327,129 +229,34 @@ function wrap(db, dir, dbsMap) {
return PromiseA.reject(err); return PromiseA.reject(err);
} }
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'));
}
}
if (obj && keys.length) { if (obj && keys.length) {
var conditions = keys.map(function (key) { sql += 'WHERE ';
var dbKey = db.escape(snakeCase(key));
var value = obj[key];
if (null === value) {
return dbKey + ' IS NULL';
}
var split, cmd; keys.forEach(function (key, i) {
if (typeof value === 'string') { if (i !== 0) {
value = value.trim(); sql += 'AND ';
if (['IS NULL', 'IS NOT NULL'].indexOf(value.toUpperCase()) !== -1) {
return dbKey + ' ' + value.toUpperCase();
} }
if (null === obj[key]) {
split = value.split(' '); sql += db.escape(snakeCase(key)) + " IS null";
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 else {
// to include the quotes (we'll quote it again later) so we strip them out here. // TODO check that key is some type? ignore undefined?
if (cmd) { sql += db.escape(snakeCase(key)) + " = '" + db.escape(obj[key]) + "'";
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 else if (null !== obj || (params && !params.limit)) {
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;
}
// IMPORTANT: " is not the same to sqlite as '.
// // " is exact and necessary
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) {
sql += " ORDER BY \"updated_at\" DESC ";
} else if (DB._indicesMap.created_at) {
sql += " ORDER BY \"created_at\" DESC ";
} }
if (isFinite(params.limit)) { if (params.limit) {
sql += " LIMIT " + parseInt(params.limit, 10); sql += " LIMIT " + parseInt(params.limit, 10);
} }
} }
@ -494,13 +301,8 @@ function wrap(db, dir, dbsMap) {
DB.save = function (data, oldId) { DB.save = function (data, oldId) {
if (!data[idnameCased] && !oldId) { if (!data[idnameCased] && !oldId) {
// NOTE saving the id both in the object and the id for now // NOTE saving the id both in the object and the id for now
data[idnameCased] = require('crypto').randomBytes(16).toString('hex').split(''); var UUID = require('node-uuid');
data[idnameCased].splice(8, 0, '-'); data[idnameCased] = UUID.v4();
data[idnameCased].splice(8 + 1 + 4, 0, '-');
data[idnameCased].splice(8 + 1 + 4 + 1 + 4, 0, '-');
data[idnameCased].splice(8 + 1 + 4 + 1 + 4 + 1 + 4, 0, '-');
data[idnameCased][14] = 4; // TODO look at the mock uuid in the Go code I wrote
data[idnameCased] = data[idnameCased].join('');
return DB.create(data[idnameCased], data).then(function (/*stats*/) { return DB.create(data[idnameCased], data).then(function (/*stats*/) {
//data._rowid = stats.id; //data._rowid = stats.id;
return data; return data;
@ -571,6 +373,80 @@ 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();
@ -683,14 +559,9 @@ function wrap(db, dir, dbsMap) {
}); });
} }
function promisify(key) {
if ('function' !== typeof db[key] || /Async$/.test(key) || db[key + 'Async']) { return; }
db[key + 'Async'] = require('util').promisify(db[key]);
}
if (!db.__masterquest_init) { if (!db.__masterquest_init) {
db.__masterquest_init = true; db.__masterquest_init = true;
Object.keys(db).forEach(promisify); db = PromiseA.promisifyAll(db);
['run', 'all'].forEach(promisify);
db.__masterquest_init = true; db.__masterquest_init = true;
db.escape = function (str) { db.escape = function (str) {
// TODO? literals for true,false,null // TODO? literals for true,false,null

5
package-lock.json generated
View File

@ -1,5 +0,0 @@
{
"name": "masterquest-sqlite3",
"version": "1.3.0",
"lockfileVersion": 1
}

View File

@ -1,6 +1,6 @@
{ {
"name": "masterquest-sqlite3", "name": "masterquest-sqlite3",
"version": "1.3.1", "version": "1.1.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": {
@ -8,10 +8,12 @@
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://git.coolaj86.com:coolaj86/masterquest-sqlite3.js" "url": "git@github.com:coolaj86/node-masterquest-sqlite3.git"
}, },
"author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)", "author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)",
"license": "(MIT OR Apache-2.0)", "license": "(MIT OR Apache-2.0)",
"homepage": "https://git.coolaj86.com:coolaj86/masterquest-sqlite3.js", "homepage": "https://github.com/coolaj86/masterquest-sqlite3",
"dependencies": {} "dependencies": {
"bluebird": "^3.0.5"
}
} }