sqljs-documentstore
Version:
Minimal, encrypted, sql friendly typed document store, with support for indexed columns. Protects against transactional conflicts
210 lines • 11.9 kB
JavaScript
"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