sequelize-store
Version:
Key Value store backed by Sequelize
244 lines (243 loc) • 10.6 kB
JavaScript
;
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 __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.init = exports.purge = exports.getObject = exports.reset = exports.getEndPromise = void 0;
const sequelize_1 = require("sequelize");
const queue_1 = __importDefault(require("queue"));
const definitions_1 = require("./definitions");
const errors_1 = require("./errors");
class StoreEntry extends sequelize_1.Model {
parseValue(schema) {
if (!schema[this.key]) {
throw new errors_1.EntryError(`There is no entry in schema for key ${this.key}`);
}
const type = definitions_1.getType(schema[this.key]);
return definitions_1.parseType(this.value, type);
}
}
const DbStoreDefinition = {
key: { type: sequelize_1.DataTypes.STRING, primaryKey: true },
value: { type: sequelize_1.DataTypes.STRING }
};
const STORE_MODEL_NAME = 'sequelizeStore-dbstore';
function validateSchema(schema) {
if (!schema) {
throw new errors_1.SchemaError('Schema has to be defined!');
}
if (Object.keys(schema).length === 0) {
throw new errors_1.SchemaError('Schema must not be empty!');
}
for (const [keyName, valueDefinition] of Object.entries(schema)) {
if (typeof keyName !== 'string') {
throw new errors_1.SchemaError(`Key name "${keyName}" is not a string!`);
}
if (typeof valueDefinition === 'string' || valueDefinition instanceof String) {
if (!definitions_1.isType(valueDefinition)) {
throw new errors_1.SchemaError(`Key ${keyName} is defined with unknown type ${valueDefinition}`);
}
}
else {
if (typeof valueDefinition !== 'object') {
throw new errors_1.SchemaError(`Key's ${keyName} definition needs to be either string or object!`);
}
if (!definitions_1.isValueOptions(valueDefinition)) {
throw new errors_1.SchemaError(`Key's ${keyName} definition is not valid!`);
}
}
}
}
function actionNotAllowed(action) {
return () => {
throw new Error(`Sorry ${action} action is not supported for SequelizeStore object`);
};
}
let localStore;
let proxyObject;
const dbQueue = queue_1.default({ autostart: true, concurrency: 1 });
dbQueue.on('error', (e) => {
throw new errors_1.EntryError(`There was an error during updating database of SequelizeStore: ${e}`);
});
/**
* Function that returns a Promise that is resolved when the DB processing queue
* is finished with all the pending transactions. If queue is empty Promise is
* resolved right away.
*
* Be aware that since the queue is in "autostart" mode if you modify the store
* after the Promise resolution the queue will start again processing the requests
* but this Promise won't "unresolve" as that is restriction of Promises.
*
* This function should be always at the end of your application life-cycle in order
* to guarantee that all information is persisted!
*/
function getEndPromise() {
if (dbQueue.length === 0) {
return Promise.resolve();
}
return new Promise(resolve => {
dbQueue.on('end', resolve);
});
}
exports.getEndPromise = getEndPromise;
/**
* Function mainly for testing.
* It resets the internal store object that is always returned by the getObject().
* Hence init() can be then called again with new schema.
* !!! Be aware !!! If used without understanding this might break things!
*/
function reset() {
proxyObject = undefined;
}
exports.reset = reset;
/**
* Returns the Store's object that uses the Schema defined in init().
* Always return the same object so can be called from anywhere as many times you need.
*
* @param scope - It is possible to get an object that has scoped the namespace to some prefix defined by this parameter.
*/
function getObject(scope) {
if (!proxyObject) {
throw new Error('SequelizeStore was not initialized!');
}
if (scope) {
if (typeof scope !== 'string') {
throw new TypeError('Scope has to be a string!');
}
return new Proxy(proxyObject, {
get(target, name) {
if (typeof name === 'symbol') {
throw new errors_1.EntryError('Symbols are not supported by SequelizeStore');
}
return Reflect.get(target, `${scope}${name}`);
},
set(target, name, value) {
if (typeof name === 'symbol') {
throw new errors_1.EntryError('Symbols are not supported by SequelizeStore');
}
target[`${scope}${name}`] = value;
return true;
},
deleteProperty(target, name) {
if (typeof name === 'symbol') {
throw new errors_1.EntryError('Symbols are not supported by SequelizeStore');
}
delete target[`${scope}${name}`];
return true;
}
});
}
return proxyObject;
}
exports.getObject = getObject;
/**
* Purge database of all data and also purges the local cache.
*/
function purge() {
if (!proxyObject) {
throw new Error('SequelizeStore was not initialized!');
}
dbQueue.push(() => StoreEntry.destroy({ where: {}, truncate: true }));
localStore = {};
}
exports.purge = purge;
/**
* Initialize Sequelize Store for usage.
* This adds the Sequelize Storage model to Sequelize and validate Schema.
*
* @param sequelize - Instance of Sequelize to be used for the Store
* @param schema - Object that define scheme of the Store.
* @param tableName - Name of the table to be used for storing the data.
*/
function init(sequelize, schema, { tableName = 'data-store' } = {}) {
return __awaiter(this, void 0, void 0, function* () {
if (proxyObject) {
return;
}
if (!sequelize) {
throw new Error('We need Sequelize instance!');
}
if (!sequelize.isDefined(STORE_MODEL_NAME)) {
StoreEntry.init(DbStoreDefinition, { sequelize, tableName, timestamps: false, modelName: STORE_MODEL_NAME });
yield StoreEntry.sync();
}
validateSchema(schema);
localStore = {};
for (const entry of yield StoreEntry.findAll()) {
localStore[entry.key] = entry.parseValue(schema);
}
proxyObject = new Proxy(localStore, {
get(target, name) {
if (typeof name === 'symbol') {
throw new errors_1.EntryError('Symbols are not supported by SequelizeStore');
}
const propertyDefinitions = schema[name];
if (!propertyDefinitions) {
// This is needed in order for the proxyObject to be returned from async function,
// which checks if the object is "thenable" in order to recursively resolve promises.
// https://stackoverflow.com/questions/48318843/why-does-await-trigger-then-on-a-proxy-returned-by-an-async-function
if (name === 'then') {
return undefined;
}
throw new errors_1.EntryError(`Property ${name} was not defined in Store's schema!`);
}
if (localStore[name] !== undefined) {
return localStore[name];
}
if (definitions_1.isValueOptions(propertyDefinitions) && propertyDefinitions.default !== undefined) {
return propertyDefinitions.default;
}
return undefined;
},
set(target, name, value) {
if (typeof name === 'symbol') {
throw new errors_1.EntryError('Symbols are not supported by SequelizeStore');
}
const propertyDefinitions = schema[name];
if (!propertyDefinitions) {
throw new errors_1.EntryError(`Property ${name} was not defined in Store's schema!`);
}
const expectedType = definitions_1.getType(propertyDefinitions);
if (!definitions_1.validateType(value, expectedType)) {
throw new TypeError(`Invalid type for ${name}! Expected ${expectedType} type.`);
}
if (typeof value === 'object' && typeof value !== 'string') {
localStore[name] = Object.freeze(value);
value = JSON.stringify(value);
}
else {
localStore[name] = value;
}
dbQueue.push(() => StoreEntry.upsert({ key: name, value: value.toString() }));
return true;
},
deleteProperty(target, name) {
if (typeof name === 'symbol') {
throw new errors_1.EntryError('Symbols are not supported by SequelizeStore');
}
const propertyDefinitions = schema[name];
if (!propertyDefinitions) {
throw new errors_1.EntryError(`Property ${name} was not defined in Store's schema!`);
}
delete localStore[name];
dbQueue.push(() => StoreEntry.destroy({ where: { key: name } }));
return true;
},
getPrototypeOf: actionNotAllowed('getPrototypeOf'),
setPrototypeOf: actionNotAllowed('setPrototypeOf'),
defineProperty: actionNotAllowed('defineProperty'),
apply: actionNotAllowed('apply'),
construct: actionNotAllowed('construct')
});
});
}
exports.init = init;