jsbin/lib/db/sqlite.js
Josh Fee fa8c9ddc55 fix: generate longer IDs when using sqlite
The sqlite ID generation code was intending to recursively try longer
IDs until one was found without collision, however the recursive call
didn't actually try longer IDs, and just made 10 attempts at finding a
3 character ID (the default).
2015-07-15 17:21:32 -04:00

614 lines
16 KiB
JavaScript

var sqlite3 = require('sqlite3').verbose(),
templates = require('./sql_templates'),
utils = require('../utils'),
fs = require('fs');
var noop = function () {};
module.exports = utils.inherit(Object, {
defaults: null,
constructor: function SQLite(options) {
this.defaults = {html: '', css: '', javascript: ''};
this.database = options.location;
},
connect: function (fn) {
var self = this;
this.connection = new sqlite3.Database(this.database, function () {
fs.readFile(__dirname + '/../../build/full-db-v3.sqlite.sql', 'utf8', function (err, sql) {
if (err) {
return fn(err);
}
self.connection.serialize(function () {
sql = sql.trim();
if (sql) {
self.connection.exec(sql, fn);
} else {
fn();
}
});
});
});
},
disconnect: function (fn) {
this.connection.close();
fn();
},
getBin: function (params, fn) {
var values = [params.id, params.revision, params.revision],
_this = this;
this.connection.get(templates.getBin, values, function (err, result) {
if (err) {
return fn(err);
}
if (result) {
result = _this.convertBinDates(result);
}
fn(null, result && _this.applyBinDefaults(result));
});
},
setBin: function (params, fn) {
var now = new Date(), values = [
params.javascript || '',
params.css || '',
params.html || '',
now,
now,
params.url,
params.revision,
params.streamingKey,
params.settings
], sql = templates.setBin;
this.connection.run(sql, values, function (err) {
if (err || !this.changes) {
return fn(err);
}
fn(null, this.lastID);
});
},
setBinOwner: function (params, fn) {
var sql = templates.setBinOwner,
values = [params.name, params.url, params.revision, new Date(), params.summary, params.html, params.css, params.javascript, params.visibility || 'public'];
// TODO: Re-factor common callbacks into helpers.
this.connection.run(sql, values, function (err) {
if (err || !this.changes) {
return fn(err);
}
fn(null, this.lastID);
});
},
setBinPanel: function (panel, params, fn) {
var values = [
params[panel],
params.settings,
new Date(),
params.url,
params.revision,
params.streamingKey
],
allowed = {html: 1, css: 1, javascript: 1},
sql = templates.setBinPanel.replace(':panel', panel);
if (allowed[panel]) {
this.connection.run(sql, values, function (err) {
if (err || !this.changes) {
return fn(err || 'no-entry');
}
fn(null, this.lastID);
});
} else {
fn('invalid-panel');
}
},
getLatestBin: function (params, fn) {
var values = [params.id],
sql = templates.getLatestBin,
_this = this;
this.connection.get(sql, values, function (err, result) {
if (err) {
return fn(err);
}
if (result) {
result = _this.convertBinDates(result);
}
fn(null, result && _this.applyBinDefaults(result));
});
},
getLatestBinForUser: function (id, n, fn) {
var sql = templates.getLatestBinForUser,
query = this.connection.get.bind(this.connection),
_this = this;
query(sql, [id, n], function (err, result) {
var sql = templates.getBinByUrlAndRevision;
if (err) {
return fn(err);
}
if (typeof result === 'undefined') {
return fn(null, null);
}
_this.getBin({ id: result.url, revision: result.revision }, fn);
});
},
getBinsByUser: function (id, fn) {
var sql = templates.getBinsByUser,
_this = this;
this.connection.all(sql, [id], function (err, results) {
if (err) {
return fn(err);
}
var sql = templates.getBinByUrlAndRevision,
collected = [];
// i.e. if they've never saved anything before
results.forEach(function (result) {
collected.push(_this.applyBinDefaults(result));
});
fn(null, collected);
});
},
// Get all bins from the owners field
getAllOwners: function (fn) {
// Get all the 'owned' bins
this.connection.run(templates.getAllOwners, [], fn);
},
getOwnersBlock: function (start, size, fn) {
// Get all the 'owned' bins
this.connection.run(templates.getOwnersBlock, [start, size], fn);
},
generateBinId: function (length, attempts, fn) {
attempts = attempts || 1;
var id = utils.shortcode( attempts + 2 ), sqlite = this;
if (attempts <= 10) {
this.connection.get(templates.binExists, [id], function (err, result) {
if (err) {
fn(err);
} else if (result) {
sqlite.generateBinId(length, attempts + 1, fn);
} else {
fn(null, id);
}
});
} else {
fn(new Error("too-many-tries"));
}
},
// getOne('<sql template>', [constraint1, constraint2, ...], fn)
getOne: function (queryKey, params, fn) {
this.connection.get(templates[queryKey], params, function (err, result) {
if (err) {
fn(err, null);
} else {
fn(null, result);
}
});
},
deleteUser: function (id, fn) {
this.connection.run(templates.deleteUser, [id], fn);
},
getUser: function (id, fn) {
var _this = this;
this.connection.get(templates.getUser, [id], function (err, result) {
if (err) {
return fn(err);
}
if (!result) {
_this.connection.get(templates.getUserByEmail, [id], function (err, result) {
if (err) {
return fn(err);
}
if (result) {
result = _this.convertUserDates(result);
}
fn(null, result);
});
} else {
if (result) {
result = _this.convertUserDates(result);
}
fn(null, result);
}
});
},
getUserByApiKey: function (apiKey, fn) {
var _this = this;
this.connection.get(templates.getUserByApiKey, [apiKey], function (err, result) {
if (err) {
return fn(err);
}
if (result) {
result = _this.convertUserDates(result);
}
fn(null, result);
});
},
getUserByEmail: function (email, fn) {
var _this = this;
this.connection.get(templates.getUserByEmail, [email], function (err, result) {
if (err) {
return fn(err);
}
if (result) {
result = _this.convertUserDates(result);
}
fn(null, result);
});
},
setUser: function (params, fn) {
var now = new Date(), values = [
params.name,
params.key,
params.email,
now,
now,
now,
params.github_token,
params.github_id,
params.flagged || false,
], sql = templates.setUser;
this.connection.run(sql, values, function (err) {
if (err) {
return fn(err);
}
fn(null, this.lastID);
});
},
touchOwners: function (params, fn) {
// params.date is only for use when populating the summary field
var values = [params.date || new Date(), params.name, params.url, params.revision];
this.connection.run(templates.touchOwners, values, function (err) {
if (err) {
return fn(err);
}
if (typeof fn === 'function') {
fn(null);
}
});
},
updateOwners: function (params, fn) {
// params.date is only for use when populating the summary field
var values = [params.date || new Date(), params.summary, params.panel_open, params.name, params.url, params.revision];
var panel = params.panel,
allowed = {html: 1, css: 1, javascript: 1},
sql = templates.updateOwners.replace(':panel', panel);
if (allowed[panel]) {
this.connection.run(sql, values, function (err) {
if (err) {
return fn(err);
}
if (typeof fn === 'function') {
fn(null);
}
});
} else {
fn('invalid-panel');
}
},
populateOwners: function (params, fn) {
// params.date is only for use when populating the summary field
var values = [params.date || new Date(), params.summary, params.html, params.css, params.javascript, params.name, params.url, params.revision];
this.connection.run(templates.populateOwners, values, function (err, result) {
if (err) {
return fn(err);
}
if (typeof fn === 'function') {
fn(null);
}
});
},
touchLogin: function (id, fn) {
var now = new Date();
this.connection.run(templates.touchLogin, [now, id], function (err) {
if (err) {
return fn(err);
}
fn(null);
});
},
updateUserEmail: function (id, email, fn) {
var now = new Date();
this.connection.run(templates.updateUserEmail, [email, now, id], function (err) {
if (err) {
return fn(err);
}
fn(null);
});
},
updateUserSettings: function(id, settings, fn) {
this.connection.run(templates.updateUserSettings, [JSON.stringify(settings), id], function(err){
if (err) {
return fn(err);
}
fn(null);
});
},
updateUserGithubData: function (id, ghId, token, fn) {
var now = new Date();
this.connection.run(templates.updateUserGithubData, [ghId, token, now, id], function (err) {
if (err) {
return fn(err);
}
fn(null);
});
},
updateUserDropboxData: function (id, token, fn) {
fn(null);
},
updateUserKey: function (id, key, fn) {
var now = new Date();
this.connection.run(templates.updateUserKey, [key, now, id], function (err) {
if (err) {
return fn(err);
}
fn(null);
});
},
// Different to updateUserKey() in that it also sets the created timestamp
// which is required to differentiate between a JSBin 2 user and a new
// one.
upgradeUserKey: function (id, key, fn) {
var now = new Date();
this.connection.run(templates.upgradeUserKey, [key, now, now, id], function (err) {
if (err) {
return fn(err);
}
fn(null);
});
},
getUserByForgotToken: function (token, fn) {
var sql = templates.getUserForForgotToken,
_this = this;
this.connection.get(sql, [token, new Date()], function (err, result) {
if (err) {
return fn(err);
}
if (result) {
result = _this.convertUserDates(result);
}
fn(null, result);
});
},
setForgotToken: function (user, token, fn) {
var sql = templates.setForgotToken,
expires = this.expireDate(),
params = [user, token, expires, new Date()];
this.connection.run(sql, params, function (err) {
if (err) {
return fn(err);
}
fn();
});
},
expireForgotToken: function (token, fn) {
var sql = templates.deleteExpiredForgotToken;
// Allow all old tokens to be expired with same call.
if (typeof token === 'function') {
fn = token;
token = null;
}
this.connection.run(sql, [new Date(), token, null], function (err, results) {
fn(err || null);
});
},
expireForgotTokenByUser: function (user, fn) {
var sql = templates.deleteExpiredForgotToken;
this.connection.run(sql, [new Date(), null, user], function (err) {
fn(err || null);
});
},
expireDate: function () {
var expires = new Date();
expires.setUTCDate(expires.getUTCDate() + 1);
return expires;
},
applyBinDefaults: function (bin) {
for (var prop in this.defaults) {
if (bin[prop] == null) { // Using == to catch null and undefined.
bin[prop] = this.defaults[prop];
}
}
this.convertDates(bin, 'last_updated');
if (!bin.last_updated || isNaN(bin.last_updated.getTime())) bin.last_updated = new Date('2012-07-23 00:00:00');
try {
bin.settings = JSON.parse(bin.settings || '{}');
} catch (e) {
// this is likely because the settings were screwed in a beta build
bin.settings = {};
}
return bin;
},
convertUserDates: function (user) {
return this.convertDates(user, 'created', 'updated', 'last_login');
},
convertBinDates: function (bin) {
return this.convertDates(bin, 'created', 'last_viewed');
},
convertDates: function (obj/* keys */) {
var keys = [].slice.call(arguments, 1);
keys.forEach(function (key) {
if (obj && obj[key]) {
var date = new Date();
date.setTime(obj[key]);
obj[key] = date;
}
});
return obj;
},
reportBin: function (params, fn) {
var now = new Date(), values = [
now,
params.url,
params.revision
], sql = templates.reportBin;
this.connection.run(sql, values, function (err) {
if (err) {
return fn(err);
}
fn(null);
});
},
archiveBin: function (bin, fn) {
var values = [bin.archive, bin.name, bin.url, bin.revision],
sql = templates.archiveBin;
this.connection.run(sql, values, function (err, result) {
if (err || !this.changes) {
return fn(err);
}
fn(null, result);
});
},
isOwnerOf: function (params, fn) {
var values = [
params.name || '',
params.url
], sql = templates.isOwnerOf;
// note: .get gets one row
this.connection.get(sql, values, function (err, result) {
if (err) {
return fn(err);
}
if (typeof result === 'undefined') {
return fn(null, { found: false });
} else {
return fn(null, { found: true, isowner: result.owner === 1, result: result });
}
});
},
getUserBinCount: function (id, fn) {
var values = [id],
sql = templates.getUserBinCount;
this.connection.get(sql, values, function (err, result) {
if (err) {
return fn(err);
}
if (typeof result === 'undefined') {
return fn(null, { found: false, total: 0 });
} else {
return fn(null, { found: true, total: result.total });
}
});
},
getBinMetadata: function(bin, fn) {
var sql = templates.getBinMetadata;
this.connection.get(sql, [bin.url, bin.revision], function(err, result) {
if (err) {
return fn(err);
}
fn(null, (result && result.length > 0 && result[0]) ? result[0] : {
visibility: 'public',
name: 'anonymous'
});
});
},
setBinVisibility: function(bin, name, value, fn) {
var sql = templates.setBinVisibility, params = [
value, name, bin.url
];
if (!bin.metadata || bin.metadata.name !== name) {
return fn(301);
}
this.connection.run(sql, params, function(err, result) {
if (err) {
return fn(500);
}
fn(err, result);
});
},
setCustomer: noop,
setCustomerActive: noop,
getCustomerByStripeId: noop,
getCustomerByUser: noop,
getUserListing: function (user, fn) {
var sql = templates.userListing;
this.connection.get(sql, [user], fn);
},
setProAccount: function(id, pro, fn){
this.connection.run(templates.setProAccount, [pro, new Date(), id], fn);
},
updateBinData: updateMultipleFields(templates.updateBinData, templates.sandboxColumns),
updateOwnersData: updateMultipleFields(templates.updateOwnersData, templates.ownersColumns),
updateOwnershipData: updateMultipleFields(templates.updateOwnershipData, templates.ownershipColumns),
saveBookmark: function (params, fn) {
var sql = templates.saveBookmark;
this.connection.run(sql, [params.name, params.url, params.revision, params.type], fn);
},
getBookmark: function (params, fn) {
var sql = templates.getBookmark;
this.connection.get(sql, [params.name, params.type], function (error, result) {
if (error || !result.length) {
return fn(error || { notfound: true });
}
fn(null, result);
});
},
getAssetsForUser: noop,
deleteAsset: noop,
saveAsset: noop
});
function updateMultipleFields(sqlTemplate, columnsArray) {
return function (bin, params, fn) {
var values = [];
var queries = Object.keys(params).map(function(key) {
if (columnsArray.indexOf(key) === -1) {
throw new Error('Warning: attempt to update sandbox table with invalid field "' + key + '"');
}
values.push(params[key]);
return '`' + key + '`=?';
});
values.push(bin.url);
values.push(bin.revision);
var sql = sqlTemplate.replace('`:field`=?', queries.join(', '));
this.connection.run(sql, values, fn);
};
}