sqljs-documentstore
Version:
Minimal, encrypted, sql friendly typed document store, with support for indexed columns. Protects against transactional conflicts
427 lines • 22.4 kB
JavaScript
"use strict";
var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
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 _ = __importStar(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 () {
return __awaiter(this, arguments, void 0, function (options) {
var tableExists, result, existingColumns_1, missingColumns_1;
var _this = this;
if (options === void 0) { options = { autoMigrateIndexChanges: true }; }
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
tableExists = sqljsHelpers_1.sqljsHelpers.isTable(this.db, this.tableName);
if (!!tableExists) return [3 /*break*/, 2];
return [4 /*yield*/, this.db.txnAsync("".concat(this.tableName, " create table"), function (txnId) { return __awaiter(_this, void 0, void 0, function () {
var createTableSql;
return __generator(this, function (_a) {
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);
return [2 /*return*/];
});
}); })];
case 1:
_a.sent();
return [3 /*break*/, 4];
case 2:
if (!options.autoMigrateIndexChanges) return [3 /*break*/, 4];
result = sqljsHelpers_1.sqljsHelpers.query(this.db, "PRAGMA table_info('".concat(this.tableName, "')"));
if (result.length === 0) {
console.warn("unable to get table_info for ".concat(this.tableName));
return [2 /*return*/];
}
existingColumns_1 = new Set(result.map(function (x) { return x.name; }));
missingColumns_1 = _.filter(this._indexColumns, function (columnName) { return !existingColumns_1.has(columnName); });
if (missingColumns_1.length === 0) {
return [2 /*return*/];
}
console.log("adding missing columns to ".concat(this.tableName), missingColumns_1);
return [4 /*yield*/, this.db.txnAsync("".concat(this.tableName, " add missing columns"), function (txnId) { return __awaiter(_this, void 0, void 0, function () {
var _this = this;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
missingColumns_1.forEach(function (columnName) { return _this.db.run(txnId, "alter table ".concat(_this.tableName, " add column ").concat(columnName, ";")); });
return [4 /*yield*/, this.rebuildIndexes(txnId)];
case 1:
_a.sent();
return [2 /*return*/];
}
});
}); })];
case 3:
_a.sent();
_a.label = 4;
case 4: return [2 /*return*/];
}
});
});
};
TypedDocumentStore.prototype.get = function (id) {
return __awaiter(this, void 0, void 0, function () {
var result;
return __generator(this, function (_a) {
result = this._tryGet(id);
if (result === null || result === undefined)
throw new Error("".concat(TypedDocumentStore.name, "<").concat(this.tableName, ">.get(").concat(id, ") was undefined"));
return [2 /*return*/, result];
});
});
};
TypedDocumentStore.prototype.tryGet = function (id) {
return __awaiter(this, void 0, void 0, function () {
return __generator(this, function (_a) {
return [2 /*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) {
return __awaiter(this, void 0, void 0, function () {
var results, missingIds;
return __generator(this, function (_a) {
results = this._tryGetMany(ids);
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 [2 /*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 __awaiter(this, void 0, void 0, function () {
return __generator(this, function (_a) {
return [2 /*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) {
return __awaiter(this, void 0, void 0, function () {
var result;
return __generator(this, function (_a) {
result = (this.db.exec("select count(1) from ".concat(this.tableName, " where ").concat(exports.dbRow.id, " = ?;"), [id]));
return [2 /*return*/, result[0].values[0][0] > 0];
});
});
};
TypedDocumentStore.prototype.getAll = function () {
return __awaiter(this, void 0, void 0, function () {
var results;
return __generator(this, function (_a) {
results = sqljsHelpers_1.sqljsHelpers.query(this.db, "select ".concat(exports.dbRow.json, " from ").concat(this.tableName, ";"));
return [2 /*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) {
return __awaiter(this, void 0, void 0, function () {
var querySql, results;
return __generator(this, function (_a) {
querySql = "select ".concat(exports.dbRow.id, ", ").concat(exports.dbRow.json, " from ").concat(this.tableName, " ").concat(whereSql(this._buildQueryObject()), ";");
results = sqljsHelpers_1.sqljsHelpers.query(this.db, querySql, params);
return [2 /*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) {
return __awaiter(this, void 0, void 0, function () {
var queryObject, querySql, results;
return __generator(this, function (_a) {
queryObject = __assign(__assign({}, this._buildQueryObject()), { id: exports.dbRow.id });
querySql = "select ".concat(exports.dbRow.id).concat(this._indexColumnNamesSql, " from ").concat(this.tableName, " ").concat(whereSql !== undefined ? whereSql(queryObject) : '', ";");
results = sqljsHelpers_1.sqljsHelpers.query(this.db, querySql, params);
return [2 /*return*/, results];
});
});
};
TypedDocumentStore.prototype.count = function () {
return __awaiter(this, void 0, void 0, function () {
var result;
return __generator(this, function (_a) {
result = this.db.exec("select count(1) from ".concat(this.tableName, ";"));
return [2 /*return*/, result[0].values[0][0]];
});
});
};
/**
* insert or update a single document
*/
TypedDocumentStore.prototype.set = function (txnId, value) {
return __awaiter(this, void 0, void 0, function () { return __generator(this, function (_a) {
this.db.run(txnId, this.setSql, this._buildParams(value));
return [2 /*return*/];
}); });
};
/**
* insert or update many documents, prefer use of insertMany if data is expected to not exist
*/
TypedDocumentStore.prototype.setMany = function (txnId, values) {
return __awaiter(this, void 0, void 0, function () {
var _this = this;
return __generator(this, function (_a) {
if (values.length === 0)
return [2 /*return*/];
values.forEach(function (value) { return _this.db.run(txnId, _this.setSql, _this._buildParams(value)); });
return [2 /*return*/];
});
});
};
TypedDocumentStore.prototype.insertMany = function (txnId, values) {
return __awaiter(this, void 0, void 0, function () {
var _this = this;
return __generator(this, function (_a) {
if (values.length === 0)
return [2 /*return*/];
values.forEach(function (value) { return _this.db.run(txnId, _this.insertSql, _this._buildParams(value)); });
return [2 /*return*/];
});
});
};
/**
* fetch, modify, and update a document
*/
TypedDocumentStore.prototype.update = function (txnId, id, updateAction) {
return __awaiter(this, void 0, void 0, function () {
var value;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, this.get(id)];
case 1:
value = _a.sent();
updateAction(value);
return [4 /*yield*/, this.set(txnId, value)];
case 2:
_a.sent();
return [2 /*return*/];
}
});
});
};
/**
* like update, but falls back to initializer value if document doesn't already exist
*/
TypedDocumentStore.prototype.upsert = function (txnId, initializer, updateAction) {
return __awaiter(this, void 0, void 0, function () {
var value;
var _a;
return __generator(this, function (_b) {
switch (_b.label) {
case 0: return [4 /*yield*/, this.tryGet(initializer.id)];
case 1:
value = (_a = _b.sent()) !== null && _a !== void 0 ? _a : initializer;
updateAction(value);
return [4 /*yield*/, this.set(txnId, value)];
case 2:
_b.sent();
return [2 /*return*/];
}
});
});
};
TypedDocumentStore.prototype.remove = function (txnId, id) {
return __awaiter(this, void 0, void 0, function () { return __generator(this, function (_a) {
this.db.run(txnId, "delete from ".concat(this.tableName, " where ").concat(exports.dbRow.id, " = ?;"), sqljsHelpers_1.sqljsHelpers.sanitizeParams([id]));
return [2 /*return*/];
}); });
};
TypedDocumentStore.prototype.removeMany = function (txnId, ids) {
return __awaiter(this, void 0, void 0, function () { return __generator(this, function (_a) {
if (ids.length === 0)
return [2 /*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));
return [2 /*return*/];
}); });
};
TypedDocumentStore.prototype.removeAll = function (txnId) {
return __awaiter(this, void 0, void 0, function () { return __generator(this, function (_a) {
this.db.run(txnId, "delete from ".concat(this.tableName, ";"));
return [2 /*return*/];
}); });
};
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) {
return __awaiter(this, void 0, void 0, function () { var _a, _b; return __generator(this, function (_c) {
switch (_c.label) {
case 0:
_a = this.setMany;
_b = [txnId];
return [4 /*yield*/, this.getAll()];
case 1: return [4 /*yield*/, _a.apply(this, _b.concat([_c.sent()]))];
case 2:
_c.sent();
return [2 /*return*/];
}
}); });
};
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