1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-24 10:27:10 +02:00
joplin/ReactNativeClient/lib/database.js

362 lines
9.5 KiB
JavaScript
Raw Normal View History

const Logger = require('lib/Logger').default;
const { time } = require('lib/time-utils.js');
const Mutex = require('async-mutex').Mutex;
const shim = require('lib/shim').default;
2017-05-07 23:02:17 +02:00
class Database {
2017-06-11 23:11:14 +02:00
constructor(driver) {
2017-05-11 22:14:01 +02:00
this.debugMode_ = false;
2017-06-11 23:11:14 +02:00
this.driver_ = driver;
2017-06-23 23:32:24 +02:00
this.logger_ = new Logger();
2017-10-07 18:30:27 +02:00
this.logExcludedQueryTypes_ = [];
this.batchTransactionMutex_ = new Mutex();
this.profilingEnabled_ = false;
this.queryId_ = 1;
2017-10-07 18:30:27 +02:00
}
setLogExcludedQueryTypes(v) {
this.logExcludedQueryTypes_ = v;
2017-06-23 23:32:24 +02:00
}
// Converts the SQLite error to a regular JS error
// so that it prints a stacktrace when passed to
// console.error()
2017-07-04 20:09:47 +02:00
sqliteErrorToJsError(error, sql = null, params = null) {
2017-07-05 23:29:00 +02:00
return this.driver().sqliteErrorToJsError(error, sql, params);
2017-06-23 23:32:24 +02:00
}
setLogger(l) {
this.logger_ = l;
}
logger() {
return this.logger_;
2017-05-11 22:14:01 +02:00
}
2017-05-07 23:02:17 +02:00
2017-06-11 23:11:14 +02:00
driver() {
return this.driver_;
}
2017-07-04 20:09:47 +02:00
async open(options) {
Desktop: Resolves #176: Added experimental WYSIWYG editor (#2556) * Trying to get TuiEditor to work * Tests with TinyMCE * Fixed build * Improved asset loading * Added support for Joplin source blocks * Added support for Joplin source blocks * Better integration * Make sure noteDidUpdate event is always dispatched at the right time * Minor tweaks * Fixed tests * Add support for checkboxes * Minor refactoring * Added support for file attachments * Add support for fenced code blocks * Fix new line issue on code block * Added support for Fountain scripts * Refactoring * Better handling of saving and loading notes * Fix saving and loading ntoes * Handle multi-note selection and fixed new note creation issue * Fixed newline issue in test * Fixed newline issue in test * Improve saving and loading * Improve saving and loading note * Removed undeeded prop * Fixed issue when new note being saved is incorrectly reloaded * Refactoring and improve saving of note when unmounting component * Fixed TypeScript error * Small changes * Improved further handling of saving and loading notes * Handle provisional notes and fixed various saving and loading bugs * Adding back support for HTML notes * Added support for HTML notes * Better handling of editable nodes * Preserve image HTML tag when the size is set * Handle switching between editor when the note has note finished saving * Handle templates * Handle templates * Handle loading note that is being saved * Handle note being reloaded via sync * Clean up * Clean up and improved logging * Fixed TS error * Fixed a few issues * Fixed test * Logging * Various improvements * Add blockquote support * Moved CWD operation to shim * Removed deleted files * Added support for Joplin commands
2020-03-10 01:24:57 +02:00
try {
await this.driver().open(options);
} catch (error) {
throw new Error(`Cannot open database: ${error.message}: ${JSON.stringify(options)}`);
}
this.logger().info('Database was open successfully');
2017-06-11 23:11:14 +02:00
}
2017-06-25 14:49:46 +02:00
escapeField(field) {
if (field == '*') return '*';
const p = field.split('.');
2019-09-19 23:51:18 +02:00
if (p.length == 1) return `\`${field}\``;
if (p.length == 2) return `${p[0]}.\`${p[1]}\``;
2019-07-29 15:43:53 +02:00
2019-09-19 23:51:18 +02:00
throw new Error(`Invalid field format: ${field}`);
2017-06-25 14:49:46 +02:00
}
escapeFields(fields) {
if (fields == '*') return '*';
2017-07-03 20:58:01 +02:00
const output = [];
2017-06-25 14:49:46 +02:00
for (let i = 0; i < fields.length; i++) {
output.push(this.escapeField(fields[i]));
}
return output;
}
2017-07-03 20:58:01 +02:00
async tryCall(callName, sql, params) {
if (typeof sql === 'object') {
2017-07-02 17:46:03 +02:00
params = sql.params;
sql = sql.sql;
}
2017-06-27 01:20:01 +02:00
let waitTime = 50;
let totalWaitTime = 0;
const callStartTime = Date.now();
let profilingTimeoutId = null;
2017-06-27 01:20:01 +02:00
while (true) {
try {
this.logQuery(sql, params);
const queryId = this.queryId_++;
if (this.profilingEnabled_) {
console.info(`SQL START ${queryId}`, sql, params);
profilingTimeoutId = shim.setInterval(() => {
console.warn(`SQL ${queryId} has been running for ${Date.now() - callStartTime}: ${sql}`);
}, 3000);
}
const result = await this.driver()[callName](sql, params);
if (this.profilingEnabled_) {
shim.clearInterval(profilingTimeoutId);
profilingTimeoutId = null;
const elapsed = Date.now() - callStartTime;
if (elapsed > 10) console.info(`SQL END ${queryId}`, elapsed, sql, params);
}
2017-07-03 20:58:01 +02:00
return result; // No exception was thrown
2017-06-27 01:20:01 +02:00
} catch (error) {
if (error && (error.code == 'SQLITE_IOERR' || error.code == 'SQLITE_BUSY')) {
2017-07-03 20:58:01 +02:00
if (totalWaitTime >= 20000) throw this.sqliteErrorToJsError(error, sql, params);
// NOTE: don't put logger statements here because it might log to the database, which
// could result in an error being thrown again.
// this.logger().warn(sprintf('Error %s: will retry in %s milliseconds', error.code, waitTime));
// this.logger().warn('Error was: ' + error.toString());
2017-06-27 01:20:01 +02:00
await time.msleep(waitTime);
totalWaitTime += waitTime;
waitTime *= 1.5;
} else {
throw this.sqliteErrorToJsError(error, sql, params);
2017-07-03 20:58:01 +02:00
}
} finally {
if (profilingTimeoutId) shim.clearInterval(profilingTimeoutId);
2017-06-27 01:20:01 +02:00
}
}
2017-06-11 23:11:14 +02:00
}
2017-05-07 23:02:17 +02:00
2017-07-03 20:58:01 +02:00
async selectOne(sql, params = null) {
return this.tryCall('selectOne', sql, params);
2017-07-03 20:58:01 +02:00
}
async loadExtension(/* path */) {
return; // Disabled for now as fuzzy search extension is not in use
// let result = null;
// try {
// result = await this.driver().loadExtension(path);
// return result;
// } catch (e) {
// throw new Error(`Could not load extension ${path}`);
// }
2020-09-06 14:07:00 +02:00
}
2017-07-03 20:58:01 +02:00
async selectAll(sql, params = null) {
return this.tryCall('selectAll', sql, params);
2017-07-03 20:58:01 +02:00
}
async selectAllFields(sql, params, field) {
const rows = await this.tryCall('selectAll', sql, params);
const output = [];
for (let i = 0; i < rows.length; i++) {
const v = rows[i][field];
2019-09-19 23:51:18 +02:00
if (!v) throw new Error(`No such field: ${field}. Query was: ${sql}`);
output.push(rows[i][field]);
}
return output;
}
2017-07-03 20:58:01 +02:00
async exec(sql, params = null) {
return this.tryCall('exec', sql, params);
2017-07-03 20:58:01 +02:00
}
async transactionExecBatch(queries) {
if (queries.length <= 0) return;
2017-06-15 01:14:15 +02:00
if (queries.length == 1) {
const q = this.wrapQuery(queries[0]);
await this.exec(q.sql, q.params);
return;
2017-06-15 01:14:15 +02:00
}
// There can be only one transaction running at a time so use a mutex
const release = await this.batchTransactionMutex_.acquire();
2017-06-15 01:14:15 +02:00
try {
await this.exec('BEGIN TRANSACTION');
2017-06-14 21:59:46 +02:00
for (let i = 0; i < queries.length; i++) {
const query = this.wrapQuery(queries[i]);
await this.exec(query.sql, query.params);
}
await this.exec('COMMIT');
} catch (error) {
await this.exec('ROLLBACK');
throw error;
} finally {
release();
2017-06-11 23:11:14 +02:00
}
2017-05-12 22:17:23 +02:00
}
2017-05-20 00:16:50 +02:00
static enumId(type, s) {
if (type == 'settings') {
if (s == 'int') return 1;
if (s == 'string') return 2;
2017-05-12 22:17:23 +02:00
}
if (type == 'fieldType') {
2017-12-12 23:58:57 +02:00
if (s) s = s.toUpperCase();
if (s == 'INTEGER') s = 'INT';
2019-09-19 23:51:18 +02:00
if (!(`TYPE_${s}` in this)) throw new Error(`Unkonwn fieldType: ${s}`);
return this[`TYPE_${s}`];
2017-05-20 00:16:50 +02:00
}
if (type == 'syncTarget') {
if (s == 'memory') return 1;
if (s == 'filesystem') return 2;
if (s == 'onedrive') return 3;
}
2019-09-19 23:51:18 +02:00
throw new Error(`Unknown enum type or value: ${type}, ${s}`);
2017-05-07 23:02:17 +02:00
}
2017-12-07 20:12:46 +02:00
static enumName(type, id) {
if (type === 'fieldType') {
if (id === Database.TYPE_UNKNOWN) return 'unknown';
if (id === Database.TYPE_INT) return 'int';
if (id === Database.TYPE_TEXT) return 'text';
if (id === Database.TYPE_NUMERIC) return 'numeric';
2019-09-19 23:51:18 +02:00
throw new Error(`Invalid type id: ${id}`);
2017-12-07 20:12:46 +02:00
}
}
2017-05-20 00:16:50 +02:00
static formatValue(type, value) {
if (value === null || value === undefined) return null;
if (type == this.TYPE_INT) return Number(value);
if (type == this.TYPE_TEXT) return value;
if (type == this.TYPE_NUMERIC) return Number(value);
2019-09-19 23:51:18 +02:00
throw new Error(`Unknown type: ${type}`);
2017-05-20 00:16:50 +02:00
}
2017-05-07 23:02:17 +02:00
sqlStringToLines(sql) {
const output = [];
const lines = sql.split('\n');
let statement = '';
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line == '') continue;
2019-07-29 15:43:53 +02:00
if (line.substr(0, 2) == '--') continue;
2017-07-19 21:15:55 +02:00
statement += line.trim();
if (line[line.length - 1] == ',') statement += ' ';
if (line[line.length - 1] == ';') {
2017-05-07 23:02:17 +02:00
output.push(statement);
statement = '';
2017-05-07 23:02:17 +02:00
}
}
return output;
}
2017-05-11 22:14:01 +02:00
logQuery(sql, params = null) {
2017-10-07 18:30:27 +02:00
if (this.logExcludedQueryTypes_.length) {
const temp = sql.toLowerCase();
for (let i = 0; i < this.logExcludedQueryTypes_.length; i++) {
if (temp.indexOf(this.logExcludedQueryTypes_[i].toLowerCase()) === 0) return;
}
}
2017-06-25 13:39:42 +02:00
this.logger().debug(sql);
2017-06-25 14:49:46 +02:00
if (params !== null && params.length) this.logger().debug(JSON.stringify(params));
2017-05-18 22:31:40 +02:00
}
2017-05-11 22:14:01 +02:00
static insertQuery(tableName, data) {
if (!data || !Object.keys(data).length) throw new Error('Data is empty');
2017-06-18 01:49:52 +02:00
2019-07-29 15:43:53 +02:00
let keySql = '';
let valueSql = '';
const params = [];
for (const key in data) {
2017-05-11 22:14:01 +02:00
if (!data.hasOwnProperty(key)) continue;
if (key[key.length - 1] == '_') continue;
if (keySql != '') keySql += ', ';
if (valueSql != '') valueSql += ', ';
2019-09-19 23:51:18 +02:00
keySql += `\`${key}\``;
valueSql += '?';
2017-05-11 22:14:01 +02:00
params.push(data[key]);
2017-05-10 21:51:43 +02:00
}
2017-05-11 22:14:01 +02:00
return {
2019-09-19 23:51:18 +02:00
sql: `INSERT INTO \`${tableName}\` (${keySql}) VALUES (${valueSql})`,
2017-05-11 22:14:01 +02:00
params: params,
};
2017-05-10 21:51:43 +02:00
}
2017-05-12 21:54:06 +02:00
static updateQuery(tableName, data, where) {
if (!data || !Object.keys(data).length) throw new Error('Data is empty');
2017-06-18 01:49:52 +02:00
let sql = '';
const params = [];
for (const key in data) {
2017-05-12 21:54:06 +02:00
if (!data.hasOwnProperty(key)) continue;
if (key[key.length - 1] == '_') continue;
if (sql != '') sql += ', ';
2019-09-19 23:51:18 +02:00
sql += `\`${key}\`=?`;
2017-05-12 21:54:06 +02:00
params.push(data[key]);
}
if (typeof where != 'string') {
const s = [];
for (const n in where) {
if (!where.hasOwnProperty(n)) continue;
params.push(where[n]);
2019-09-19 23:51:18 +02:00
s.push(`\`${n}\`=?`);
}
where = s.join(' AND ');
2017-05-12 21:54:06 +02:00
}
return {
2019-09-19 23:51:18 +02:00
sql: `UPDATE \`${tableName}\` SET ${sql} WHERE ${where}`,
2017-05-12 21:54:06 +02:00
params: params,
};
}
2017-07-25 23:55:26 +02:00
alterColumnQueries(tableName, fields) {
const fieldsNoType = [];
for (const n in fields) {
if (!fields.hasOwnProperty(n)) continue;
fieldsNoType.push(n);
}
const fieldsWithType = [];
for (const n in fields) {
if (!fields.hasOwnProperty(n)) continue;
2019-09-19 23:51:18 +02:00
fieldsWithType.push(`${this.escapeField(n)} ${fields[n]}`);
2019-07-29 15:43:53 +02:00
}
2017-07-25 23:55:26 +02:00
let sql = `
CREATE TEMPORARY TABLE _BACKUP_TABLE_NAME_(_FIELDS_TYPE_);
INSERT INTO _BACKUP_TABLE_NAME_ SELECT _FIELDS_NO_TYPE_ FROM _TABLE_NAME_;
2017-07-25 23:55:26 +02:00
DROP TABLE _TABLE_NAME_;
CREATE TABLE _TABLE_NAME_(_FIELDS_TYPE_);
INSERT INTO _TABLE_NAME_ SELECT _FIELDS_NO_TYPE_ FROM _BACKUP_TABLE_NAME_;
2017-07-25 23:55:26 +02:00
DROP TABLE _BACKUP_TABLE_NAME_;
`;
2019-09-19 23:51:18 +02:00
sql = sql.replace(/_BACKUP_TABLE_NAME_/g, this.escapeField(`${tableName}_backup`));
2017-07-25 23:55:26 +02:00
sql = sql.replace(/_TABLE_NAME_/g, this.escapeField(tableName));
sql = sql.replace(/_FIELDS_NO_TYPE_/g, this.escapeFields(fieldsNoType).join(','));
sql = sql.replace(/_FIELDS_TYPE_/g, fieldsWithType.join(','));
2017-07-25 23:55:26 +02:00
2019-07-29 15:43:53 +02:00
return sql.trim().split('\n');
2017-07-25 23:55:26 +02:00
}
2019-07-29 15:43:53 +02:00
2017-06-11 23:11:14 +02:00
wrapQueries(queries) {
const output = [];
2017-06-11 23:11:14 +02:00
for (let i = 0; i < queries.length; i++) {
output.push(this.wrapQuery(queries[i]));
}
return output;
}
wrapQuery(sql, params = null) {
2019-09-19 23:51:18 +02:00
if (!sql) throw new Error(`Cannot wrap empty string: ${sql}`);
2017-06-11 23:11:14 +02:00
if (sql.constructor === Array) {
const output = {};
2017-06-11 23:11:14 +02:00
output.sql = sql[0];
output.params = sql.length >= 2 ? sql[1] : null;
return output;
} else if (typeof sql === 'string') {
2017-06-11 23:11:14 +02:00
return { sql: sql, params: params };
} else {
return sql; // Already wrapped
}
2017-05-12 22:17:23 +02:00
}
2017-05-07 23:02:17 +02:00
}
Database.TYPE_UNKNOWN = 0;
Database.TYPE_INT = 1;
Database.TYPE_TEXT = 2;
2017-06-15 20:18:48 +02:00
Database.TYPE_NUMERIC = 3;
2019-07-29 15:43:53 +02:00
module.exports = { Database };