UNPKG

sqljs-documentstore

Version:

Minimal, encrypted, sql friendly typed document store, with support for indexed columns. Protects against transactional conflicts

210 lines 11.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.dbRow = exports.TypedDocumentStore = void 0; exports.isTypedDocumentStore = isTypedDocumentStore; /* eslint-disable no-use-before-define */ /* eslint-disable lines-between-class-members */ var _ = require("lodash"); var sqljsHelpers_1 = require("./sqljsHelpers"); var TypedDocumentStore = /** @class */ (function () { function TypedDocumentStore(_db, tableName, docType /* used for generic inference magic */, indexedFields) { if (indexedFields === void 0) { indexedFields = {}; } this._db = _db; this.tableName = tableName; this.indexedFields = indexedFields; //helper fields for sql template strings below this._indexColumns = _.keys(this.indexedFields); this._indexColumnNamesSql = this._indexColumns.map(function (c) { return ", ".concat(c); }).join(''); this._indexQuestionMarksSql = ', ?'.repeat(this._indexColumns.length); this._updateParamsSql = _.map(this._indexColumns, function (name, index) { return ", ".concat(name, "=?").concat(index + [exports.dbRow.id, exports.dbRow.json].length + 1); }).join(''); //actual sql template strings this.insertSql = "insert into ".concat(this.tableName, " (").concat(exports.dbRow.id, ", ").concat(exports.dbRow.json).concat(this._indexColumnNamesSql, ") values (?, ?").concat(this._indexQuestionMarksSql, ");"); this.setSql = "insert into ".concat(this.tableName, " (").concat(exports.dbRow.id, ", ").concat(exports.dbRow.json).concat(this._indexColumnNamesSql, ") values (?, ?").concat(this._indexQuestionMarksSql, ") ") + "on conflict(".concat(exports.dbRow.id, ") do update set ").concat(exports.dbRow.json, "=?2 ").concat(this._updateParamsSql, " where ").concat(exports.dbRow.id, "=?1;"); } Object.defineProperty(TypedDocumentStore.prototype, "db", { get: function () { return this._db(); }, enumerable: false, configurable: true }); Object.defineProperty(TypedDocumentStore.prototype, "asInterface", { get: function () { return this; }, enumerable: false, configurable: true }); /** * ensure table (schema) exists */ TypedDocumentStore.prototype.init = function (options) { var _this = this; if (options === void 0) { options = { autoMigrateIndexChanges: true }; } var tableExists = sqljsHelpers_1.sqljsHelpers.isTable(this.db, this.tableName); if (!tableExists) { this.db.txn("".concat(this.tableName, " create table"), function (txnId) { var createTableSql = "create table if not exists ".concat(_this.tableName, " (").concat(exports.dbRow.id, " primary key not null, ").concat(exports.dbRow.json, " ").concat(_this._indexColumnNamesSql, ");"); _this.db.run(txnId, createTableSql); }); } else if (options.autoMigrateIndexChanges) { //check for and append missing columns var result = sqljsHelpers_1.sqljsHelpers.query(this.db, "PRAGMA table_info('".concat(this.tableName, "')")); if (!result) { console.warn("unable to get table_info for ".concat(this.tableName)); return; } var existingColumns_1 = new Set(result.map(function (x) { return x.name; })); var missingColumns_1 = _.filter(this._indexColumns, function (columnName) { return !existingColumns_1.has(columnName); }); if (missingColumns_1.length === 0) { return; } console.log("adding missing columns to ".concat(this.tableName), missingColumns_1); this.db.txn("".concat(this.tableName, " add missing columns"), function (txnId) { missingColumns_1.forEach(function (columnName) { return _this.db.run(txnId, "alter table ".concat(_this.tableName, " add column ").concat(columnName, ";")); }); _this.rebuildIndexes(txnId); }); } }; TypedDocumentStore.prototype.get = function (id) { var result = this._tryGet(id); if (result === null || result === undefined) throw new Error("".concat(TypedDocumentStore.name, "<").concat(this.tableName, ">.get(").concat(id, ") was undefined")); return result; }; TypedDocumentStore.prototype.tryGet = function (id) { return this._tryGet(id); }; TypedDocumentStore.prototype._tryGet = function (id) { var rows = sqljsHelpers_1.sqljsHelpers.query(this.db, "select ".concat(exports.dbRow.id, ", ").concat(exports.dbRow.json, " from ").concat(this.tableName, " where ").concat(exports.dbRow.id, " = ?;"), [id]); if (rows.length == 0) return undefined; return JSON.parse(rows[0].json); }; TypedDocumentStore.prototype.getMany = function (ids) { var results = this._tryGetMany(ids); var missingIds = ids.filter(function (_, i) { return results[i] === null || results[i] === undefined; }); if (missingIds.length > 0) throw new Error("".concat(TypedDocumentStore.name, "<").concat(this.tableName, ">.getMany(...) was undefined for ids ").concat(missingIds.join(', '))); return results; }; /** * returns ids => in order set of results, if no result for given id, array item will be null * prefer use of 'getMany' if all items are expected to exist */ TypedDocumentStore.prototype.tryGetMany = function (ids) { return this._tryGetMany(ids); }; TypedDocumentStore.prototype._tryGetMany = function (ids) { if (ids.length === 0) return []; var queryResult = sqljsHelpers_1.sqljsHelpers.query(this.db, "select ".concat(exports.dbRow.id, ", ").concat(exports.dbRow.json, " from ").concat(this.tableName, " where ").concat(exports.dbRow.id, " in (").concat('?,'.repeat(ids.length).slice(0, -1), ");"), ids); var rowById = _.keyBy(queryResult, function (row) { return row.id; }); var currentId; try { return ids.map(function (id) { currentId = id; if (!(id in rowById)) return undefined; var row = rowById[id]; return JSON.parse(row.json); }); } catch (e) { throw Error("error while parsing document ".concat(this.tableName, " id: ").concat(currentId)); } }; TypedDocumentStore.prototype.exists = function (id) { var _a, _b; var result = (this.db.exec("select 1 from ".concat(this.tableName, " where ").concat(exports.dbRow.id, " = ?;"), [id])); return ((_b = (_a = result.values) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0) > 0; }; TypedDocumentStore.prototype.getAll = function () { var results = sqljsHelpers_1.sqljsHelpers.query(this.db, "select ".concat(exports.dbRow.json, " from ").concat(this.tableName, ";")); return results.map(function (x) { return JSON.parse(x.json); }); }; /** * Query document store by indexed columns. Provide a sql fragment of where clause and array of parameters. Can be abused to do joins, etc. * @example .query(x => `where ${x.name} like ? and ${x.active} = ?`, [nameSearchValue, isActive]); * @example .query(x => `where ${x.name} like ?1 and ${x.active} = ?2`, [nameSearchValue, isActive]); */ TypedDocumentStore.prototype.query = function (whereSql, params) { var querySql = "select ".concat(exports.dbRow.id, ", ").concat(exports.dbRow.json, " from ").concat(this.tableName, " ").concat(whereSql(this._buildQueryObject()), ";"); var results = sqljsHelpers_1.sqljsHelpers.query(this.db, querySql, params); return results.map(function (x) { return JSON.parse(x.json); }); }; /** * Return just index values, helpful for doing fast queries on indexed fields without needing to fetch and deserialize the entire object */ TypedDocumentStore.prototype.queryIndexes = function (whereSql, params) { var querySql = "select ".concat(exports.dbRow.id).concat(this._indexColumnNamesSql, " from ").concat(this.tableName, " ").concat(whereSql !== undefined ? whereSql(this._buildQueryObject()) : '', ";"); var results = sqljsHelpers_1.sqljsHelpers.query(this.db, querySql, params); return results; }; TypedDocumentStore.prototype.count = function () { var result = this.db.exec("select count(1) from ".concat(this.tableName, ";")); return result[0].values[0][0]; }; /** * insert or update a single document */ TypedDocumentStore.prototype.set = function (txnId, value) { this.db.run(txnId, this.setSql, this._buildParams(value)); }; /** * insert or update many documents, prefer use of insertMany if data is expected to not exist */ TypedDocumentStore.prototype.setMany = function (txnId, values) { var _this = this; if (values.length === 0) return; values.forEach(function (value) { return _this.db.run(txnId, _this.setSql, _this._buildParams(value)); }); }; TypedDocumentStore.prototype.insertMany = function (txnId, values) { var _this = this; if (values.length === 0) return; values.forEach(function (value) { return _this.db.run(txnId, _this.insertSql, _this._buildParams(value)); }); }; /** * fetch, modify, and update a document */ TypedDocumentStore.prototype.update = function (txnId, id, updateAction) { var value = this.get(id); updateAction(value); this.set(txnId, value); }; /** * like update, but falls back to initializer value if document doesn't already exist */ TypedDocumentStore.prototype.upsert = function (txnId, initializer, updateAction) { var _a; var value = (_a = this.tryGet(initializer.id)) !== null && _a !== void 0 ? _a : initializer; updateAction(value); this.set(txnId, value); }; TypedDocumentStore.prototype.remove = function (txnId, id) { this.db.run(txnId, "delete from ".concat(this.tableName, " where ").concat(exports.dbRow.id, " = ?;"), sqljsHelpers_1.sqljsHelpers.sanitizeParams([id])); }; TypedDocumentStore.prototype.removeMany = function (txnId, ids) { if (ids.length === 0) return; this.db.run(txnId, "delete from ".concat(this.tableName, " where ").concat(exports.dbRow.id, " in (").concat('?,'.repeat(ids.length).slice(0, -1), ");"), sqljsHelpers_1.sqljsHelpers.sanitizeParams(ids)); }; TypedDocumentStore.prototype.removeAll = function (txnId) { this.db.run(txnId, "delete from ".concat(this.tableName, ";")); }; TypedDocumentStore.prototype._buildQueryObject = function () { var queryObject = {}; this._indexColumns.forEach(function (columnName) { return ((queryObject[columnName]) = columnName); }); return queryObject; }; TypedDocumentStore.prototype._buildParams = function (value) { // eslint-disable-next-line prefer-destructuring var id = value.id; var json = JSON.stringify(value); var indexParams = this._indexValues(value); return sqljsHelpers_1.sqljsHelpers.sanitizeParams(_.concat([id, json], indexParams)); }; TypedDocumentStore.prototype._indexValues = function (obj) { return _.map(this.indexedFields, function (accessor) { return accessor(obj); }); }; TypedDocumentStore.prototype.rebuildIndexes = function (txnId) { this.setMany(txnId, this.getAll()); }; return TypedDocumentStore; }()); exports.TypedDocumentStore = TypedDocumentStore; ; ; function isTypedDocumentStore(obj) { var x = obj; return x.init !== undefined; } exports.dbRow = { id: 'id', json: 'json' }; //# sourceMappingURL=TypedDocumentStore.js.map