@nozbe/watermelondb
Version:
Build powerful React Native and React web apps that scale from hundreds to tens of thousands of records and remain fast
514 lines (499 loc) • 17.4 kB
JavaScript
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
exports.__esModule = true;
exports.default = void 0;
exports.setExperimentalAllowsFatalError = setExperimentalAllowsFatalError;
var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));
var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
var _logger = _interopRequireDefault(require("../../../utils/common/logger"));
var _invariant = _interopRequireDefault(require("../../../utils/common/invariant"));
var _RawRecord = require("../../../RawRecord");
var _lokiExtensions = require("./lokiExtensions");
var _executeQuery = require("./executeQuery");
// don't import the whole utils/ here!
var SCHEMA_VERSION_KEY = '_loki_schema_version';
var experimentalAllowsFatalError = false;
function setExperimentalAllowsFatalError() {
experimentalAllowsFatalError = true;
}
var DatabaseDriver = exports.default = /*#__PURE__*/function () {
// (experimental) if true, DatabaseDriver is in a broken state and should not be used anymore
function DatabaseDriver(options) {
this.cachedRecords = new Map();
this._isBroken = false;
var {
schema: schema,
migrations: migrations
} = options;
this.options = options;
this.schema = schema;
this.migrations = migrations;
}
var _proto = DatabaseDriver.prototype;
_proto.setUp = function () {
return new Promise(function ($return, $error) {
return Promise.resolve(this._openDatabase()).then(function () {
try {
return Promise.resolve(this._migrateIfNeeded()).then(function () {
try {
return $return();
} catch ($boundEx) {
return $error($boundEx);
}
}, $error);
} catch ($boundEx) {
return $error($boundEx);
}
}.bind(this), $error);
}.bind(this));
};
_proto.isCached = function (table, id) {
var cachedSet = this.cachedRecords.get(table);
return cachedSet ? cachedSet.has(id) : false;
};
_proto.markAsCached = function (table, id) {
var cachedSet = this.cachedRecords.get(table);
if (cachedSet) {
cachedSet.add(id);
} else {
this.cachedRecords.set(table, new Set([id]));
}
};
_proto.removeFromCache = function (table, id) {
var cachedSet = this.cachedRecords.get(table);
if (cachedSet) {
cachedSet.delete(id);
}
};
_proto.clearCachedRecords = function () {
this.cachedRecords = new Map();
};
_proto.getCache = function (table) {
var cache = this.cachedRecords.get(table);
if (cache) {
return cache;
}
var newCache = new Set([]);
this.cachedRecords.set(table, newCache);
return newCache;
};
_proto.find = function (table, id) {
if (this.isCached(table, id)) {
return id;
}
var raw = this.loki.getCollection(table).by('id', id);
if (!raw) {
return null;
}
this.markAsCached(table, id);
return (0, _RawRecord.sanitizedRaw)(raw, this.schema.tables[table]);
};
_proto.query = function (_query) {
var records = (0, _executeQuery.executeQuery)(_query, this.loki);
return this._compactQueryResults(records, _query.table);
};
_proto.queryIds = function (query) {
return (0, _executeQuery.executeQuery)(query, this.loki).map(function (record) {
return record.id;
});
};
_proto.unsafeQueryRaw = function (query) {
return (0, _executeQuery.executeQuery)(query, this.loki);
};
_proto.count = function (query) {
return (0, _executeQuery.executeCount)(query, this.loki);
};
_proto.batch = function (operations) {
var _this = this;
// NOTE: Mutations to LokiJS db are *not* transactional!
// This is terrible and lame for a database, but there's just no simple and good solution to this
// Loki transactions rely on making a full copy of the data, and reverting to it if something breaks.
// This is just unbearable for production-sized databases (too much memory required)
// It could be done with some sort of advanced journaling/CoW structure scheme, but that would
// be very complicated (in itself a source of bugs), and possibly quite expensive cpu-wise
//
// So instead, we assume that writes MUST succeed. If they don't, we put DatabaseDriver in a "broken"
// state, refuse to persist or further mutate the DB, and notify the app (and user) about it.
//
// It can be assumed that Loki-level mutations that fail are WatermelonDB bugs that must be fixed
this._assertNotBroken();
try {
var recordsToCreate = {};
operations.forEach(function (operation) {
var [type, table, raw] = operation;
switch (type) {
case 'create':
if (!recordsToCreate[table]) {
recordsToCreate[table] = [];
}
recordsToCreate[table].push(raw);
break;
default:
break;
}
});
// We're doing a second pass, because batch insert is much faster in Loki
Object.entries(recordsToCreate).forEach(function (args) {
var [table, raws] = args;
var shouldRebuildIndexAfterInsert = 1000 <= raws.length; // only profitable for large inserts
_this.loki.getCollection(table).insert(raws, shouldRebuildIndexAfterInsert);
var cache = _this.getCache(table);
raws.forEach(function (raw) {
cache.add(raw.id);
});
});
operations.forEach(function (operation) {
var [type, table, rawOrId] = operation;
var collection = _this.loki.getCollection(table);
switch (type) {
case 'update':
// Loki identifies records using internal $loki ID so we must find the saved record first
var lokiId = collection.by('id', rawOrId.id).$loki;
var raw = rawOrId;
raw.$loki = lokiId;
collection.update(raw);
break;
case 'markAsDeleted':
var id = rawOrId;
var record = collection.by('id', id);
if (record) {
record._status = 'deleted';
collection.update(record);
_this.removeFromCache(table, id);
}
break;
case 'destroyPermanently':
var _id = rawOrId;
var _record = collection.by('id', _id);
_record && collection.remove(_record);
_this.removeFromCache(table, _id);
break;
default:
break;
}
});
} catch (error) {
this._fatalError(error);
}
};
_proto.getDeletedRecords = function (table) {
return this.loki.getCollection(table).find({
_status: {
$eq: 'deleted'
}
}).map(function (record) {
return record.id;
});
};
_proto.unsafeExecute = function (operations) {
if ('production' !== process.env.NODE_ENV) {
(0, _invariant.default)(operations && 'object' === typeof operations && 1 === Object.keys(operations).length && 'function' === typeof operations.loki, 'unsafeExecute expects an { loki: loki => { ... } } object');
}
var lokiBlock = operations.loki;
lokiBlock(this.loki);
};
_proto.unsafeResetDatabase = function () {
return new Promise(function ($return, $error) {
return Promise.resolve((0, _lokiExtensions.deleteDatabase)(this.loki)).then(function () {
try {
this.cachedRecords.clear();
_logger.default.log('[Loki] Database is now reset');
return Promise.resolve(this._openDatabase()).then(function () {
try {
this._setUpSchema();
return $return();
} catch ($boundEx) {
return $error($boundEx);
}
}.bind(this), $error);
} catch ($boundEx) {
return $error($boundEx);
}
}.bind(this), $error);
}.bind(this));
}
// *** LocalStorage ***
;
_proto.getLocal = function (key) {
var record = this._findLocal(key);
return record ? record.value : null;
};
_proto.setLocal = function (key, value) {
this._assertNotBroken();
try {
var record = this._findLocal(key);
if (record) {
record.value = value;
this._localStorage.update(record);
} else {
this._localStorage.insert({
key: key,
value: value
});
}
} catch (error) {
this._fatalError(error);
}
};
_proto.removeLocal = function (key) {
this._assertNotBroken();
try {
var record = this._findLocal(key);
if (record) {
this._localStorage.remove(record);
}
} catch (error) {
this._fatalError(error);
}
}
// *** Internals ***
;
_proto._openDatabase = function () {
return new Promise(function ($return, $error) {
_logger.default.log('[Loki] Initializing IndexedDB');
return Promise.resolve((0, _lokiExtensions.newLoki)(this.options)).then(function ($await_13) {
try {
this.loki = $await_13;
_logger.default.log('[Loki] Database loaded');
return $return();
} catch ($boundEx) {
return $error($boundEx);
}
}.bind(this), $error);
}.bind(this));
};
_proto._setUpSchema = function () {
var _this2 = this;
_logger.default.log('[Loki] Setting up schema');
// Add collections
var tables = Object.values(this.schema.tables);
tables.forEach(function (tableSchema) {
_this2._addCollection(tableSchema);
});
this.loki.addCollection('local_storage', {
unique: ['key'],
indices: [],
disableMeta: true
});
// Set database version
this._databaseVersion = this.schema.version;
_logger.default.log('[Loki] Database collections set up');
};
_proto._addCollection = function (tableSchema) {
var {
name: name,
columnArray: columnArray
} = tableSchema;
var indexedColumns = columnArray.reduce(function (indexes, column) {
return column.isIndexed ? indexes.concat([column.name]) : indexes;
}, []);
this.loki.addCollection(name, {
unique: ['id'],
indices: ['_status'].concat((0, _toConsumableArray2.default)(indexedColumns)),
disableMeta: true
});
};
_proto._migrateIfNeeded = function () {
return new Promise(function ($return, $error) {
var dbVersion, schemaVersion, migrationSteps;
dbVersion = this._databaseVersion;
schemaVersion = this.schema.version;
if (dbVersion === schemaVersion) {
return $If_5.call(this);
} // All good!
else {
if (0 === dbVersion) {
_logger.default.log('[Loki] Empty database, setting up');
return Promise.resolve(this.unsafeResetDatabase()).then(function () {
try {
return $If_6.call(this);
} catch ($boundEx) {
return $error($boundEx);
}
}.bind(this), $error);
} else {
if (0 < dbVersion && dbVersion < schemaVersion) {
_logger.default.log('[Loki] Database has old schema version. Migration is required.');
migrationSteps = this._getMigrationSteps(dbVersion);
if (migrationSteps) {
_logger.default.log("[Loki] Migrating from version ".concat(dbVersion, " to ").concat(this.schema.version, "..."));
var $Try_4_Post = function () {
try {
return $If_8.call(this);
} catch ($boundEx) {
return $error($boundEx);
}
}.bind(this);
var $Try_4_Catch = function $Try_4_Catch(error) {
try {
_logger.default.error('[Loki] Migration failed', error);
throw error;
} catch ($boundEx) {
return $error($boundEx);
}
};
try {
return Promise.resolve(this._migrate(migrationSteps)).then(function () {
try {
return $Try_4_Post();
} catch ($boundEx) {
return $Try_4_Catch($boundEx);
}
}, $Try_4_Catch);
} catch (error) {
$Try_4_Catch(error)
}
} else {
_logger.default.warn('[Loki] Migrations not available for this version range, resetting database instead');
return Promise.resolve(this.unsafeResetDatabase()).then(function () {
try {
return $If_8.call(this);
} catch ($boundEx) {
return $error($boundEx);
}
}.bind(this), $error);
}
function $If_8() {
return $If_7.call(this);
}
} else {
_logger.default.warn("[Loki] Database has newer version ".concat(dbVersion, " than app schema ").concat(schemaVersion, ". Resetting database."));
return Promise.resolve(this.unsafeResetDatabase()).then(function () {
try {
return $If_7.call(this);
} catch ($boundEx) {
return $error($boundEx);
}
}.bind(this), $error);
}
function $If_7() {
return $If_6.call(this);
}
}
function $If_6() {
return $If_5.call(this);
}
}
function $If_5() {
return $return();
}
}.bind(this));
};
_proto._getMigrationSteps = function (fromVersion) {
// TODO: Remove this after migrations are shipped
var {
migrations: migrations
} = this;
if (!migrations) {
return null;
}
var {
stepsForMigration: stepsForMigration
} = require('../../../Schema/migrations/stepsForMigration');
return stepsForMigration({
migrations: migrations,
fromVersion: fromVersion,
toVersion: this.schema.version
});
};
_proto._migrate = function (steps) {
return new Promise(function ($return) {
var _this3 = this;
steps.forEach(function (step) {
if ('create_table' === step.type) {
_this3._executeCreateTableMigration(step);
} else if ('add_columns' === step.type) {
_this3._executeAddColumnsMigration(step);
} else if (!('sql' === step.type)) {
throw new Error("Unsupported migration step ".concat(step.type));
} // ignore
});
// Set database version
this._databaseVersion = this.schema.version;
_logger.default.log("[Loki] Migration successful");
return $return();
}.bind(this));
};
_proto._executeCreateTableMigration = function ({
schema: schema
}) {
this._addCollection(schema);
};
_proto._executeAddColumnsMigration = function ({
table: table,
columns: columns
}) {
var collection = this.loki.getCollection(table);
// update ALL records in the collection, adding new fields
collection.findAndUpdate({}, function (record) {
columns.forEach(function (column) {
(0, _RawRecord.setRawSanitized)(record, column.name, null, column);
});
});
// add indexes, if needed
columns.forEach(function (column) {
if (column.isIndexed) {
collection.ensureIndex(column.name);
}
});
}
// Maps records to their IDs if the record is already cached on JS side
;
_proto._compactQueryResults = function (records, table) {
var _this4 = this;
var cache = this.getCache(table);
return records.map(function (raw) {
var {
id: id
} = raw;
if (cache.has(id)) {
return id;
}
cache.add(id);
return (0, _RawRecord.sanitizedRaw)(raw, _this4.schema.tables[table]);
});
};
_proto._findLocal = function (key) {
var localStorage = this._localStorage;
return localStorage && localStorage.by('key', key);
};
_proto._assertNotBroken = function () {
if (this._isBroken) {
throw new Error('DatabaseDriver is in a broken state, bailing...');
}
}
// (experimental)
// TODO: Setup, migrations, delete database should also break driver
;
_proto._fatalError = function (error) {
if (!experimentalAllowsFatalError) {
_logger.default.warn('DatabaseDriver is broken, but experimentalAllowsFatalError has not been enabled to do anything about it...');
throw error;
}
// Stop further mutations
this._isBroken = true;
// Disable Loki autosave
(0, _lokiExtensions.lokiFatalError)(this.loki);
// Notify handler
_logger.default.error('DatabaseDriver is broken. App must be reloaded before continuing.');
var handler = this.options._onFatalError;
handler && handler(error);
// Rethrow error
throw error;
};
return (0, _createClass2.default)(DatabaseDriver, [{
key: "_databaseVersion",
get: function get() {
var databaseVersionRaw = this.getLocal(SCHEMA_VERSION_KEY) || '';
return parseInt(databaseVersionRaw, 10) || 0;
},
set: function set(version) {
this.setLocal(SCHEMA_VERSION_KEY, "".concat(version));
}
}, {
key: "_localStorage",
get: function get() {
return this.loki.getCollection('local_storage');
}
}]);
}();