"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
Object.defineProperty(exports, "__esModule", { value: true });
const Debug = require("debug");
const events_1 = require("events");
const redis = require("redis");
const LinkError_1 = require("./errors/LinkError");
exports.LinkError = LinkError_1.LinkError;
const ValidationError_1 = require("./errors/ValidationError");
exports.ValidationError = ValidationError_1.ValidationError;
const helpers_1 = require("./helpers");
const middleware_1 = require("./middleware");
const model_1 = require("./model");
const model_header_1 = require("./model.header");
exports.boolProperty = model_header_1.boolProperty;
exports.dateProperty = model_header_1.dateProperty;
exports.floatProperty = model_header_1.floatProperty;
exports.integerProperty = model_header_1.integerProperty;
exports.jsonProperty = model_header_1.jsonProperty;
exports.numberProperty = model_header_1.numberProperty;
exports.stringProperty = model_header_1.stringProperty;
exports.timeProperty = model_header_1.timeProperty;
exports.timestampProperty = model_header_1.timestampProperty;
const typed_redis_helper_1 = require("./typed-redis-helper");
const debug = Debug('nohm:index');
const debugPubSub = Debug('nohm:pubSub');
const PUBSUB_ALL_PATTERN = '*:*';
// this is the exported extendable version - still needs to be registered to receive proper methods
class NohmModelExtendable extends model_1.NohmModel {
/**
* DO NOT OVERWRITE THIS; USED INTERNALLY
*
* @protected
*/
_initOptions() {
// overwritten in NohmClass.model/register
throw new Error('Class is not extended properly. Use the return Nohm.register() instead of your class directly.');
}
/**
* DO NOT OVERWRITE THIS; USED INTERNALLY
*
* @protected
*/
prefix(_prefix) {
// overwritten in NohmClass.model/register
throw new Error('Class is not extended properly. Use the return Nohm.register() instead of your class directly.');
}
/**
* DO NOT OVERWRITE THIS; USED INTERNALLY
*
* @protected
*/
rawPrefix() {
// overwritten in NohmClass.model/register
throw new Error('Class is not extended properly. Use the return Nohm.register() instead of your class directly.');
}
}
exports.NohmModel = NohmModelExtendable;
function staticImplements() {
return (_constructor) => {
// no op decorator
};
}
/**
* Some generic definitions for Nohm
*
* @namespace Nohm
*/
/**
* Nohm specific Errors
*
* @namespace NohmErrors
*/
/**
* Main Nohm class. Holds models, generic configuration and can generate the middleware for client validations.
*
* Can be instantiated multiple times if you want different configurations, but usually you only need the default
* that is exported as `require('nohm').nohm`.
*
* @example
* // To instantiate another you can do this:
* const NohmClass = require('nohm').NohmClass;
* const myNohm = new NohmClass({ prefix: 'SomePrefix' });
*
* @class NohmClass
*/
class NohmClass {
constructor({ prefix, client, meta, publish }) {
this.LinkError = LinkError_1.LinkError;
this.ValidationError = ValidationError_1.ValidationError;
this.publish = false;
debug('Creating NohmClass.', arguments);
this.setPrefix(prefix);
if (client) {
this.setClient(client);
}
this.modelCache = {};
this.extraValidators = [];
this.meta = meta || true;
this.isPublishSubscribed = false;
if (typeof publish !== 'undefined') {
if (typeof publish !== 'boolean') {
this.setPublish(true);
this.setPubSubClient(publish);
}
else {
this.setPublish(publish);
}
}
}
/**
* Set the Nohm global redis client.
* Note: this will not affect models that have a client set on their own.
*/
setPrefix(prefix = 'nohm') {
debug('Setting new prefix.', prefix);
this.prefix = helpers_1.getPrefix(prefix);
}
/**
* Set the Nohm global redis client.
* Note: this will not affect models that have a client set on their own.
*/
setClient(client) {
debug('Setting new redis client. Connected: %s; Address: %s.', client && (client.connected || client.status === 'ready'), client && client.address);
// ioredis uses .status string instead of .connected boolean
if (client && !(client.connected || client.status === 'ready')) {
this
.logError(`WARNING: setClient() received a redis client that is not connected yet.
Consider waiting for an established connection before setting it. Status (if ioredis): ${client.status}
, connected (if node_redis): ${client.connected}`);
}
else if (!client) {
// TODO: maybe remove this, since it is also creating an unconnected client and is the only reason
// why we have the hard dependency on the "redis" package.
client = redis.createClient();
}
this.client = client;
}
logError(err) {
if (err) {
console.error({
// TODO: Make this a wrapped NohmError if not already an error
message: err,
name: 'Nohm Error',
});
}
}
/**
* Creates and returns a new model class with the given name and options.
* If you're using Typescript it is strongly advised to use Nohm.register() instead.
*
* @param {string} modelName Name of the model. This needs to be unique and is used in data storage.
* Thus <b>changing this will invalidate existing data</b>!
* @param {IModelDefinitions} options This is an object containing the actual model definitions.
* These are: properties, methods (optional) and the client (optional) to be used.
* @param {boolean} temp When true, this model is not added to the internal model cache,
* meaning methods like factory() and getModels() cannot access them.
* This is mostly useful for meta things like migrations.
* @returns {NohmStaticModel}
*/
model(modelName, options, temp = false) {
if (!modelName) {
this.logError('When creating a new model you have to provide a name!');
}
debug('Registering new model using model().', modelName, options, temp);
// tslint:disable-next-line:no-this-assignment
const self = this; // well then...
let metaVersion = '';
/**
* The static Model Class, used to get instances or operate on multiple records.
*
* @class NohmStaticModel
*/
let CreatedClass =
// tslint:disable-next-line:max-classes-per-file
class CreatedClass extends NohmModelExtendable {
/**
* Creates an instance of CreatedClass.
*
* @ignore
* @memberof NohmStaticModel
*/
constructor() {
super();
/**
* Redis client that is set for this Model.
* Defaults to the NohmClass client it was registered in.
*
* @memberof NohmStaticModel
* @type {RedisClient}
*/
this.client = self.client;
this.nohmClass = self;
this.options = options;
/**
* Name of the model, used for database keys and relation values
*
* @memberof NohmStaticModel
* @type {string}
*/
this.modelName = modelName;
if (self.meta) {
if (!metaVersion) {
// cache it to prevent constant regeneration
metaVersion = this.generateMetaVersion();
}
this.meta = {
inDb: false,
properties: this.options.properties,
version: metaVersion,
};
}
}
/* This (and .register()) is the only place where this method should exist.
An alternative would be to pass the options as a special argument to super, but that would have the downside
of making subclasses of subclasses impossible and restricting constructor argument freedom. */
_initOptions() {
this.options = options || { properties: {} };
Object.getPrototypeOf(this).definitions = this.options.properties;
this.meta = {
inDb: false,
properties: {},
version: '',
};
if (!this.client) {
this.client = self.client;
}
}
prefix(prefix) {
return self.prefix[prefix] + modelName;
}
rawPrefix() {
return self.prefix;
}
/**
* Creates a new model instance and loads it with the given id.
*
* @static
* @param {*} id ID of the model to be loaded
* @throws {Error('not found')} If no record exists of the given id,
* an error is thrown with the message 'not found'
* @alias load
* @memberof! NohmStaticModel
* @returns {Promise<NohmModel>}
*/
static async load(id) {
const model = await self.factory(modelName);
await model.load(id);
return model;
}
/**
* Loads an Array of NohmModels via the given ids. Any ids that do not exist will just be ignored.
* If any of the ids do not exist in the database, they are left out instead of throwing an error.
* Thus if no ids exist an empty error is returned.
*
* @static
* @param {Array<string>} ids Array of IDs of the models to be loaded
* @alias loadMany
* @memberof! NohmStaticModel
* @returns {Promise<NohmModel>}
*/
static async loadMany(ids) {
if (!Array.isArray(ids) || ids.length === 0) {
return [];
}
const loadPromises = ids.map(async (id) => {
try {
return await self.factory(modelName, id);
}
catch (err) {
if (err && err.message === 'not found') {
return;
}
else {
throw err;
}
}
});
const loadedModels = await Promise.all(loadPromises);
return loadedModels.filter((model) => typeof model !== 'undefined');
}
/**
* Finds ids of objects and loads them into full NohmModels.
*
* @static
* @param {ISearchOptions} searches
* @alias findAndLoad
* @memberof! NohmStaticModel
* @returns {Promise<Array<NohmModel>>}
*/
static async findAndLoad(searches) {
const dummy = await self.factory(modelName);
const ids = await dummy.find(searches);
if (ids.length === 0) {
return [];
}
const loadPromises = ids.map((id) => {
return self.factory(modelName, id);
});
return Promise.all(loadPromises);
}
/**
* Sort the given ids or all stored ids by their SortOptions
*
* @static
* @see NohmModel.sort
* @param {ISortOptions<IDictionary>} [sortOptions={}] Search options
* @alias sort
* @memberof! NohmStaticModel
* @returns {Promise<Array<string>>} Array of ids
*/
static async sort(sortOptions = {}, ids = false) {
const dummy = await self.factory(modelName);
return dummy.sort(sortOptions, ids);
}
/**
* Search for ids
*
* @static
* @see NohmModel.find
* @param {ISearchOptions} [searches={}] Search options
* @alias find
* @memberof NohmStaticModel
* @returns {Promise<Array<string>>} Array of ids
*/
static async find(searches = {}) {
const dummy = await self.factory(modelName);
return dummy.find(searches);
}
/**
* Loads a NohmModels via the given id.
*
* @static
* @param {*} id ID of the model to be loaded
* @alias remove
* @memberof NohmStaticModel
* @returns {Promise<NohmModel>}
*/
static async remove(id, silent) {
const model = await self.factory(modelName);
model.id = id;
await model.remove(silent);
}
};
CreatedClass = __decorate([
staticImplements()
// tslint:disable-next-line:max-classes-per-file
], CreatedClass);
if (!temp) {
this.modelCache[modelName] = CreatedClass;
}
return CreatedClass;
}
/**
* Creates, registers and returns a new model class from a given class.
* When using Typescript this is the preferred method of creating new models over using Nohm.model().
*
* @param {NohmModel} subClass Complete model class, needs to extend NohmModel.
* @param {boolean} temp When true, this model is not added to the internal model cache,
* meaning methods like factory() and getModels() cannot access them.
* This is mostly useful for meta things like migrations.
* @returns {NohmStaticModel}
*
* @example
* // Typescript
* import { Nohm, NohmModel, TTypedDefinitions } from 'nohm';
*
* // this interface is useful for having typings in .property() and .allProperties() etc. but is optional
* interface IUserModelProps {
* name: string;
* }
*
* class UserModelClass extends NohmModel<IUserModelProps> {
* protected static modelName = 'user'; // used in redis to store the keys
*
* // the TTypedDefinitions generic makes sure that our definitions have the same keys as
* // defined in our property interface.
* // If you don't want to use the generic, you have to use the exported {type}Property types
* // to get around the tsc throwing an error.
* // TODO: look into the error thrown by tsc when leaving out TTypedDefinitions and using 'sometype' as type
* protected static definitions: TTypedDefinitions<IUserModelProps> = {
* name: {
* defaultValue: 'testName',
* type: 'string', // you have to manually make sure this matches the IUserModelProps type!
* validations: [
* 'notEmpty',
* ],
* },
* };
* public async foo() {
* const test = bar.property('name'); // no error and test typed to string
*
* await bar.validate();
* bar.errors.name; // no error and typed
*
* // accessing unknown props does not work,
* // because we specified that UserModel only has properties of IUserModelProps
* bar.property('foo'); // typescript errors
* bar.errors.foo; // typescript error
* };
* }
* const userModel = Nohm.register(UserModelClass);
* // typescript now knows about bar.foo() and all the standard nohm methods like bar.property();
* const bar = new userModel();
* bar.foo(); // no error
* bar.allProperties().name === 'testName'; // no error
*/
register(subClass, temp = false) {
var CreatedClass_1;
// tslint:disable-next-line:no-this-assignment
const self = this; // well then...
const modelName = subClass.modelName;
if (!modelName) {
throw new Error('A class passed to nohm.register() did not have static a modelName property.');
}
if (!subClass.definitions) {
throw new Error('A class passed to nohm.register() did not have static property definitions.');
}
debug('Registering new model using register().', modelName, subClass.definitions, temp);
let metaVersion = '';
// tslint:disable-next-line:max-classes-per-file
let CreatedClass = CreatedClass_1 = class CreatedClass extends subClass {
constructor(...args) {
super(...args);
this.nohmClass = self;
this.modelName = modelName;
if (self.meta) {
if (!metaVersion) {
// cache it to prevent constant regeneration
metaVersion = this.generateMetaVersion();
}
this.meta = {
inDb: false,
properties: this.options.properties,
version: metaVersion,
};
}
}
/* This (and .model()) is the only place where this method should exist.
An alternative would be to pass the options as a special argument to super, but that would have the downside
of making subclasses of subclasses impossible. */
_initOptions() {
this.options = {
idGenerator: subClass.idGenerator,
properties: {},
};
if (!this.client) {
this.client = self.client;
}
this.meta = {
inDb: false,
properties: {},
version: '',
};
if (!this.options.idGenerator) {
this.options.idGenerator = 'default';
}
}
prefix(prefix) {
return self.prefix[prefix] + this.modelName;
}
rawPrefix() {
return self.prefix;
}
getDefinitions() {
const definitions = CreatedClass_1.definitions;
if (!definitions) {
throw new Error(`Model was not defined with proper static definitions: '${modelName}'`);
}
return definitions;
}
/**
* Loads a NohmModels via the given id.
*
* @param {*} id ID of the model to be loaded
* @returns {Promise<NohmModel|void>}
*/
static async load(id) {
const model = await self.factory(modelName);
await model.load(id);
return model;
}
/**
* Loads an Array of NohmModels via the given ids. Any ids that do not exist will just be ignored.
*
* @param {Array<string>} ids Array of IDs of the models to be loaded
* @returns {Promise<NohmModel>}
*/
static async loadMany(ids) {
if (!Array.isArray(ids) || ids.length === 0) {
return [];
}
const loadPromises = ids.map(async (id) => {
try {
return await self.factory(modelName, id);
}
catch (err) {
if (err && err.message === 'not found') {
return;
}
else {
throw err;
}
}
});
const loadedModels = await Promise.all(loadPromises);
return loadedModels.filter((model) => typeof model !== 'undefined');
}
/**
* Finds ids of objects and loads them into full NohmModels.
*
* @param {ISearchOptions} searches
* @returns {Promise<Array<NohmModel>>}
*/
static async findAndLoad(searches) {
const dummy = await self.factory(modelName);
const ids = await dummy.find(searches);
if (ids.length === 0) {
return [];
}
const loadPromises = ids.map((id) => {
return self.factory(dummy.modelName, id);
});
return Promise.all(loadPromises);
}
/**
* Sort the given ids or all stored ids by their SortOptions
*
* @see NohmModel.sort
* @static
* @param {ISortOptions<IDictionary>} [sortOptions={}] Search options
* @returns {Promise<Array<string>>} Array of ids
*/
static async sort(options = {}, ids = false) {
const dummy = await self.factory(modelName);
return dummy.sort(options, ids);
}
/**
* Search for ids
*
* @see NohmModel.find
* @static
* @param {ISearchOptions} [searches={}] Search options
* @returns {Promise<Array<string>>} Array of ids
*/
static async find(searches = {}) {
const dummy = await self.factory(modelName);
return dummy.find(searches);
}
/**
* Loads a NohmModels via the given id.
*
* @param {*} id ID of the model to be loaded
* @returns {Promise<NohmModel>}
*/
static async remove(id, silent) {
const model = await self.factory(modelName);
model.id = id;
await model.remove(silent);
}
};
CreatedClass = CreatedClass_1 = __decorate([
staticImplements()
], CreatedClass);
if (!temp) {
this.modelCache[modelName] = CreatedClass;
}
return CreatedClass;
}
/**
* Get all model classes that are registered via .register() or .model()
*
* @returns {Array<NohmModelStatic>}
*/
getModels() {
return this.modelCache;
}
/**
* Creates a new instance of the model with the given modelName.
* When given an id as second parameter it also loads it.
*
* @param {string} name Name of the model, must match the modelName of one of your defined models.
* @param {*} [id] ID of a record you want to load.
* @returns {Promise<NohmModel>}
* @throws {Error('Model %name not found.')} Rejects when there is no registered model with the given modelName.
* @throws {Error('not found')} If no record exists of the given id,
* an error is thrown with the message 'not found'
* @memberof NohmClass
*/
async factory(name, id) {
if (typeof arguments[1] === 'function' ||
typeof arguments[2] === 'function') {
throw new Error('Not implemented: factory does not support callback method anymore.');
}
else {
debug(`Factory is creating a new instance of '%s' with id %s.`, name, id);
const model = this.modelCache[name];
if (!model) {
throw new Error(`Model '${name}' not found.`);
}
const instance = new model();
if (id) {
await instance.load(id);
return instance;
}
else {
return instance;
}
}
}
/**
* DO NOT USE THIS UNLESS YOU ARE ABSOLUTELY SURE ABOUT IT!
*
* Deletes any keys from the db that start with the set nohm prefixes.
*
* DO NOT USE THIS UNLESS YOU ARE ABSOLUTELY SURE ABOUT IT!
*
* @param {Object} [client] You can specify the redis client to use. Default: Nohm.client
*/
async purgeDb(client = this.client) {
async function delKeys(prefix) {
const foundKeys = await typed_redis_helper_1.keys(client, prefix + '*');
if (foundKeys.length === 0) {
return;
}
await typed_redis_helper_1.del(client, foundKeys);
}
const deletes = [];
debug(`PURGING DATABASE!`, client && client.connected, client && client.address, this.prefix);
Object.keys(this.prefix).forEach((key) => {
const prefix = this.prefix[key];
if (typeof prefix === 'object') {
Object.keys(prefix).forEach((innerKey) => {
const innerPrefix = prefix[innerKey];
deletes.push(delKeys(innerPrefix));
});
}
else {
deletes.push(delKeys(prefix));
}
});
await Promise.all(deletes);
}
setExtraValidations(files) {
debug(`Setting extra validation files`, files);
if (!Array.isArray(files)) {
files = [files];
}
files.forEach((path) => {
if (this.extraValidators.indexOf(path) === -1) {
this.extraValidators.push(path);
const validators = require(path);
Object.keys(validators).forEach((_name) => {
// TODO for v1: check if this needs to be implemented
// this.__validators[name] = validators[name];
});
}
});
}
getExtraValidatorFileNames() {
return this.extraValidators;
}
middleware(options) {
return middleware_1.middleware(options, this);
}
getPublish() {
return this.publish;
}
setPublish(publish) {
debug(`Setting publish mode to '%o'.`, !!publish);
this.publish = !!publish;
}
getPubSubClient() {
return this.publishClient;
}
setPubSubClient(client) {
debug(`Setting pubSub client. Connected: '%s'; Address: '%s'.`, client && client.connected, client && client.address);
this.publishClient = client;
return this.initPubSub();
}
async initPubSub() {
if (!this.getPubSubClient()) {
throw new Error('A second redis client must set via nohm.setPubSubClient before using pub/sub methods.');
}
else if (this.isPublishSubscribed === true) {
// already in pubsub mode, don't need to initialize it again.
return;
}
this.publishEventEmitter = new events_1.EventEmitter();
this.publishEventEmitter.setMaxListeners(0); // TODO: check if this is sensible
this.isPublishSubscribed = true;
await typed_redis_helper_1.psubscribe(this.publishClient, this.prefix.channel + PUBSUB_ALL_PATTERN);
debugPubSub(`Redis PSUBSCRIBE for '%s'.`, this.prefix.channel + PUBSUB_ALL_PATTERN);
this.publishClient.on('pmessage', (_pattern, channel, message) => {
const suffix = channel.slice(this.prefix.channel.length);
const parts = suffix.match(/([^:]+)/g); // Pattern = _prefix_:channel:_modelname_:_action_
if (!parts) {
this.logError(`An erroneous channel has been captured: ${channel}.`);
return;
}
const modelName = parts[0];
const action = parts[1];
let payload = {};
try {
payload = message ? JSON.parse(message) : {};
debugPubSub(`Redis published message for model '%s' with action '%s' and message: '%j'.`, modelName, action, payload);
}
catch (e) {
this.logError(`A published message is not valid JSON. Was : "${message}"`);
return;
}
this.publishEventEmitter.emit(`${modelName}:${action}`, payload);
});
}
async subscribeEvent(eventName, callback) {
await this.initPubSub();
debugPubSub(`Redis subscribing to event '%s'.`, eventName);
this.publishEventEmitter.on(eventName, callback);
}
async subscribeEventOnce(eventName, callback) {
await this.initPubSub();
debugPubSub(`Redis subscribing once to event '%s'.`, eventName);
this.publishEventEmitter.once(eventName, callback);
}
unsubscribeEvent(eventName, fn) {
if (this.publishEventEmitter) {
debugPubSub(`Redis unsubscribing from event '%s' with fn?: %s.`, eventName, fn);
if (!fn) {
this.publishEventEmitter.removeAllListeners(eventName);
}
else {
this.publishEventEmitter.removeListener(eventName, fn);
}
}
}
async closePubSub() {
if (this.isPublishSubscribed === true) {
debugPubSub(`Redis PUNSUBSCRIBE for '%s'.`, this.prefix.channel + PUBSUB_ALL_PATTERN);
this.isPublishSubscribed = false;
await typed_redis_helper_1.punsubscribe(this.publishClient, this.prefix.channel + PUBSUB_ALL_PATTERN);
}
return this.publishClient;
}
}
exports.NohmClass = NohmClass;
const nohm = new NohmClass({});
exports.nohm = nohm;
exports.Nohm = nohm;
exports.default = nohm;
//# sourceMappingURL=index.js.map