UNPKG

sequelize-store

Version:
244 lines (243 loc) 10.6 kB
"use strict"; 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;