masterquest-sqlite3.js/lib/dbwrap.js

557 lines
14 KiB
JavaScript
Raw Normal View History

2015-08-28 03:33:46 +00:00
'use strict';
function wrap(db, dir) {
// TODO if I put a failure right here,
// why doesn't the unhandled promise rejection fire?
var PromiseA = require('bluebird');
var promises = [];
2015-12-01 04:44:52 +00:00
var earr = [];
2015-08-28 03:33:46 +00:00
var dbsMap = {};
var debug = false;
2015-12-01 04:44:52 +00:00
db.escape = function (str) {
return (str||'').replace(/'/g, "''");
};
2015-08-28 03:33:46 +00:00
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) {
2015-10-20 07:32:09 +00:00
str = str.replace(
2015-08-28 03:33:46 +00:00
/_([a-z])/g
, function (g) {
2015-10-20 07:32:09 +00:00
return g[1].toUpperCase();
}
);
return str;
2015-08-28 03:33:46 +00:00
}
function upperCamelCase(str) {
// TODO handle UTF-8 properly (use codePointAt, don't use slice)
2015-10-20 07:32:09 +00:00
return str.charAt(0).toUpperCase() + camelCase(str).slice(1);
2015-08-28 03:33:46 +00:00
}
2015-12-01 04:44:52 +00:00
// PRAGMA schema.table_info(table-name);
//
function sqlite3GetColumns(tablename, columns, cb) {
var sql = "PRAGMA table_info(" + db.escape(tablename) + ")";
db.all(sql, earr, function (err, result) {
if (err) {
console.error('[Error] query columns');
console.error(err.stack);
cb(err);
return;
}
if (debug) {
console.log('sqlite3 rows 0');
console.log(result);
}
2015-12-01 04:44:52 +00:00
function alterTable() {
var column = columns.pop();
var sql;
if (!column) {
cb(null);
return;
}
if ((result.rows||result).some(function (row) {
return (row.column_name || row.name) === snakeCase(column.name);
})) {
alterTable();
return;
}
sql = "ALTER TABLE " + db.escape(tablename)
+ " ADD COLUMN "
+ db.escape(column.name) + " " + db.escape(column.type)
+ " DEFAULT null"
;
if (debug) {
console.log('sqlite3 1');
console.log(sql);
}
2015-12-01 04:44:52 +00:00
db.all(sql, earr, function (err, results) {
if (err) {
console.error("[Error] add column '" + tablename + "'");
console.error(err.stack);
cb(err);
return;
}
if (debug) {
console.log('sqlite3 rows 1');
console.log(results);
}
2015-12-01 04:44:52 +00:00
alterTable();
});
}
columns = columns.slice(0);
2015-12-01 04:44:52 +00:00
alterTable();
});
}
function normalizeColumn(col, i, arr) {
if ('string' === typeof col) {
col = arr[i] = { name: col, type: 'text' };
}
if (!col.type) {
col.type = 'text';
}
col.type = col.type.toLowerCase(); // oh postgres...
col.name = snakeCase(col.name);
return col;
}
function createTable(dir) {
if (!dir.modelname && !dir.tablename) {
throw new Error('Please specify dir.modelname');
2015-12-01 04:44:52 +00:00
}
if (!dir.tablename) {
dir.tablename = snakeCase(dir.modelname);
2015-12-01 04:44:52 +00:00
}
if (!dir.modelname) {
dir.modelname = upperCamelCase(dir.tablename);
}
if (!dir.indices) {
dir.indices = [];
2015-12-01 04:44:52 +00:00
}
2015-08-28 03:33:46 +00:00
var DB = {};
var tablename = (db.escape(dir.tablename || 'data'));
var idname = (db.escape(dir.idname || 'id'));
var idnameCased = (camelCase(dir.idname || 'id'));
2015-12-01 04:44:52 +00:00
dir.indices.forEach(normalizeColumn);
2015-08-28 03:33:46 +00:00
db = PromiseA.promisifyAll(db);
if (dir && dir.verbose || db.verbose) {
2015-08-28 03:33:46 +00:00
console.log('Getting Verbose up in here');
db.on('trace', function (str) {
console.log('SQL:', str);
});
db.on('profile', function (sql, ms) {
console.log('Profile:', ms);
});
}
function simpleParse(row) {
if (!row) {
return null;
}
2015-10-20 08:47:43 +00:00
return simpleMap([row])[0] || null;
2015-08-28 03:33:46 +00:00
}
function simpleMap(rows) {
if (!rows) {
return [];
}
var results = rows.map(function (row, i) {
// set up for garbage collection
rows[i] = null;
var obj;
if (row.json) {
obj = JSON.parse(row.json);
2015-10-20 08:47:43 +00:00
delete row.json;
2015-08-28 03:33:46 +00:00
} else {
obj = {};
}
2015-09-22 02:14:07 +00:00
obj[idnameCased] = row[idname];
2015-10-20 08:47:43 +00:00
delete row[idname];
Object.keys(row).forEach(function (fieldname) {
// TODO warn if overriding proper field? (shouldn't be possible)
obj[camelCase(fieldname)] = row[fieldname];
});
return obj;
2015-08-28 03:33:46 +00:00
});
// set up for garbage collection
rows.length = 0;
rows = null;
return results;
}
2015-12-01 04:44:52 +00:00
DB.migrate = function (columns) {
columns.forEach(normalizeColumn);
return new PromiseA(function (resolve, reject) {
sqlite3GetColumns(tablename, columns, function (err) {
if (err) {
reject(err);
return;
}
resolve();
});
});
};
DB.find = function (obj, params) {
var sql = 'SELECT * FROM \'' + tablename + '\' ';
var keys = obj && Object.keys(obj);
2015-08-28 03:33:46 +00:00
if (obj && keys.length) {
sql += 'WHERE ';
keys.forEach(function (key, i) {
if (i !== 0) {
sql += 'AND ';
}
if (null === obj[key]) {
sql += db.escape(snakeCase(key)) + " IS '" + db.escape(obj[key]) + "'";
2015-12-01 04:44:52 +00:00
}
else {
sql += db.escape(snakeCase(key)) + " = '" + db.escape(obj[key]) + "'";
2015-12-01 04:44:52 +00:00
}
});
}
else if (null !== obj || (params && !params.limit)) {
return PromiseA.reject(new Error("to find all you must explicitly specify find(null, { limit: <<int>> })"));
}
2015-08-28 03:33:46 +00:00
2015-09-22 02:14:07 +00:00
if (params) {
if (params.orderBy) {
sql += " ORDER BY \"" + db.escape(snakeCase(params.orderBy) + "\" ");
2015-09-22 02:14:07 +00:00
if (params.orderByDesc) {
sql += 'DESC ';
2015-09-22 02:14:07 +00:00
}
}
if (params.limit) {
sql += " LIMIT " + parseInt(params.limit, 10);
}
2015-09-22 02:14:07 +00:00
}
2015-09-22 02:51:08 +00:00
return db.allAsync(sql, []).then(simpleMap);
2015-08-28 03:33:46 +00:00
};
DB.get = function (id) {
var sql = "SELECT * FROM " + tablename + " WHERE " + idname + " = ?";
var values = [id];
return db.getAsync(sql, values).then(function (rows) {
if (Array.isArray(rows)) {
if (!rows.length) {
return null;
}
return rows[0] || null;
}
return rows;
}).then(simpleParse);
};
DB.upsert = function (id, data) {
2015-11-17 08:29:45 +00:00
if (!data) {
data = id;
id = data[idnameCased];
}
2015-08-28 03:33:46 +00:00
return DB.set(id, data).then(function (result) {
var success = result.changes >= 1;
if (success) {
return result;
}
return DB.create(id, data);
});
};
DB.save = function (data) {
2015-09-22 02:14:07 +00:00
if (!data[idnameCased]) {
2015-08-28 03:33:46 +00:00
// NOTE saving the id both in the object and the id for now
var UUID = require('node-uuid');
2015-09-22 02:14:07 +00:00
data[idnameCased] = UUID.v4();
return DB.create(data[idnameCased], data).then(function (/*stats*/) {
2015-08-28 03:33:46 +00:00
//data._rowid = stats.id;
return data;
});
}
2015-09-22 02:14:07 +00:00
return DB.set(data[idnameCased], data).then(function (result) {
2015-08-28 03:33:46 +00:00
var success = result.changes >= 1;
if (success) {
return result;
2015-09-22 02:14:07 +00:00
} else {
2015-10-06 06:31:07 +00:00
//console.log('[debug result of set]', result.sql);
2015-09-22 02:14:07 +00:00
delete result.sql;
2015-08-28 03:33:46 +00:00
}
2015-09-22 02:14:07 +00:00
return null;
2015-08-28 03:33:46 +00:00
});
};
2015-09-22 02:14:07 +00:00
DB.create = function (id, obj) {
if (!obj) {
obj = id;
id = obj[idnameCased];
}
if (!id) {
return PromiseA.reject(new Error("no id supplied"));
}
2015-08-28 03:33:46 +00:00
return new PromiseA(function (resolve, reject) {
2015-09-22 02:14:07 +00:00
var json = JSON.stringify(obj);
var data = JSON.parse(json);
var sql;
// removes known fields from data
sql = strainUpdate(id, data, function sqlTpl(fieldable) {
return "INSERT INTO " + tablename + " (" + fieldable.join(', ') + ", " + idname + ")"
//+ " VALUES ('" + vals.join("', '") + "')"
+ " VALUES (" + fieldable.map(function () { return '?'; }).join(", ") + ", ?)"
;
});
2015-08-28 03:33:46 +00:00
2015-10-06 06:31:07 +00:00
//console.log('[debug] DB.create() sql:', sql);
db.run(sql, [], function (err) {
2015-08-28 03:33:46 +00:00
if (err) {
reject(err);
return;
}
// NOTE changes is 1 even if the value of the updated record stays the same
// (PostgreSQL would return 0 in that case)
// thus if changes is 0 then it failed, otherwise it succeeded
/*
console.log('[log db wrapper insert]');
console.log(this); // sql, lastID, changes
console.log(this.sql);
console.log('insert lastID', this.lastID); // sqlite's internal rowId
console.log('insert changes', this.changes);
*/
//this.id = id;
resolve(this);
});
});
};
// pull indices from object
function strainUpdate(id, data/*, vals*/, cb) {
var fieldable = [];
var json;
var sql;
var vals = [];
2015-08-28 03:33:46 +00:00
['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)];
2015-08-28 03:33:46 +00:00
//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);
}
2015-08-28 03:33:46 +00:00
delete data[col.name];
delete data[camelCase(col.name)];
});
delete data[idnameCased];
2015-08-28 03:33:46 +00:00
if (!fieldable.length || Object.keys(data).length) {
2015-08-28 03:33:46 +00:00
json = JSON.stringify(data);
fieldable.push("json");
2015-08-28 03:33:46 +00:00
//fieldable.push("json = '" + db.escape(json) + "'");
vals.push(json);
}
vals.push(id);
sql = cb(fieldable);
while (vals.length) {
sql = sql.replace(/\?/, "'" + db.escape(vals.shift()) + "'");
}
return sql;
}
2015-08-28 03:33:46 +00:00
DB.set = function (id, obj) {
var json = JSON.stringify(obj);
var data = JSON.parse(json);
2015-08-28 03:33:46 +00:00
return new PromiseA(function (resolve, reject) {
function sqlTpl(fieldable) {
// this will always at least have one fieldable value: json
return "UPDATE " + tablename + " SET "
+ (fieldable.join(' = ?, ') + " = ?")
+ " WHERE " + idname + " = ?"
;
2015-08-28 03:33:46 +00:00
}
//var vals = [];
// removes known fields from data
var sql = strainUpdate(id, data/*, vals*/, sqlTpl);
2015-10-06 06:31:07 +00:00
//console.log('[debug] DB.set() sql:', sql);
db.run(sql, /*vals*/[], function (err) {
2015-10-06 06:31:07 +00:00
//console.log('[debug] error:', err);
2015-08-28 03:33:46 +00:00
if (err) {
reject(err);
return;
}
// it isn't possible to tell if the update succeeded or failed
// only if the update resulted in a change or not
/*
console.log('[log db wrapper set]');
console.log(this); // sql, lastID, changes
console.log(this.sql);
console.log('update lastID', this.lastID); // always 0 (except on INSERT)
console.log('update changes', this.changes);
*/
resolve(this);
});
});
};
DB.destroy = function (id) {
if ('object' === typeof id) {
2015-09-22 02:14:07 +00:00
id = id[idnameCased];
2015-08-28 03:33:46 +00:00
}
return new PromiseA(function (resolve, reject) {
var sql = "DELETE FROM " + tablename + " WHERE " + idname + " = ?";
var values = [id];
db.run(sql, values, function (err) {
if (err) {
reject(err);
return;
}
// it isn't possible to tell if the update succeeded or failed
// only if the update resulted in a change or not
/*
console.log('[log db wrapper delete]');
console.log(this); // sql, lastID, changes
console.log(this.sql);
console.log('delete lastID', this.lastID); // always 0 (except on INSERT)
console.log('delete changes', this.changes);
*/
resolve(this);
});
});
};
DB._db = db;
return new PromiseA(function (resolve, reject) {
var indexable = [idname + ' TEXT'];
var sql;
dir.indices.forEach(function (col) {
2015-08-28 03:33:46 +00:00
if ('string' === typeof col) {
col = { name: col, type: 'TEXT' };
}
if (!col.type) {
col.type = 'TEXT';
}
indexable.push(
db.escape(snakeCase(col.name))
+ ' ' + db.escape(col.type)
);
});
indexable.push('json TEXT');
sql = "CREATE TABLE IF NOT EXISTS '" + tablename + "' "
+ "(" + indexable.join(', ') + ", PRIMARY KEY(" + idname + "))"
;
2015-12-01 04:44:52 +00:00
db.runAsync(sql).then(function () {
sqlite3GetColumns(tablename, dir.indices, function (err) {
2015-12-01 04:44:52 +00:00
if (err) {
console.error('[Error] dbwrap get columns');
console.error(err.stack);
reject(err);
return;
}
2015-08-28 03:33:46 +00:00
2015-12-01 04:44:52 +00:00
resolve(DB);
});
}, reject);
});
2015-08-28 03:33:46 +00:00
}
dir.forEach(function (dir) {
promises.push(createTable(dir).then(function (dbw) {
2015-08-28 03:33:46 +00:00
dbsMap[dir.modelname] = dbw;
2015-08-28 03:33:46 +00:00
return dbw;
}));
});
dbsMap.sql = db;
2015-12-01 04:44:52 +00:00
return PromiseA.all(promises).then(function (/*dbs*/) {
2015-08-28 03:33:46 +00:00
return dbsMap;
});
}
module.exports.wrap = wrap;