UNPKG

@fabrix/spool-broadcast

Version:

Spool: broadcast for Fabrix to implement CQRS and Event Sourcing

727 lines 28.1 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 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) : new P(function (resolve) { resolve(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 }); const common_1 = require("@fabrix/fabrix/dist/common"); const lodash_1 = require("lodash"); const regexdot_1 = require("@fabrix/regexdot"); const v4_1 = __importDefault(require("uuid/v4")); const helpers_1 = require("./utils/helpers"); const replaceParams = function (type = '', keys = [], object = {}, previous = {}) { if (keys !== false && typeof keys !== 'boolean' && lodash_1.isArray(keys)) { if (!lodash_1.isArray(object)) { keys.forEach(k => { if (k && object && typeof object[k] !== 'undefined' && object[k] !== null) { type = type.replace(`:${k}`, `${object[k]}`); } else if (k && previous && typeof previous[k] !== 'undefined' && previous[k] !== null) { type = type.replace(`:${k}`, `${previous[k]}`); } }); } else if (lodash_1.isArray(object)) { const o = object[0]; const p = previous[0]; keys.forEach(k => { if (k && o && typeof o[k] !== 'undefined' && o[k] !== null) { type = type.replace(`:${k}`, `${o[k]}`); } else if (k && p && typeof p[k] !== 'undefined' && p[k] !== null) { type = type.replace(`:${k}`, `${p[k]}`); } }); } } return type; }; const raw = function (val) { if (typeof val !== 'undefined' && (typeof val === 'string' || typeof val === 'boolean' || typeof val === 'number' || typeof val === 'bigint')) { } else if (typeof val !== 'undefined' && val instanceof common_1.FabrixModel) { val = JSON.parse(JSON.stringify(val)); } else if (typeof val !== 'undefined' && lodash_1.isObject(val)) { val = JSON.parse(JSON.stringify(val)); } else if (typeof val !== 'undefined' && lodash_1.isArray(val)) { val = JSON.parse(JSON.stringify(val)); } return val; }; class BroadcastCommand extends common_1.FabrixGeneric { constructor(app, broadcaster, { req, command_type, event_type, object, correlation_uuid, causation_uuid, data, metadata = {}, hooks = [], version = 0, version_app, chain_events = [], correlation_type, explain = {} }, options = {}) { super(app); this.explain = {}; this.chain_before = []; this.chain_saga = []; this.chain_after = []; this.complete = false; this.breakException = null; if (typeof object === 'string') { object = app.models[object]; } if (!(object instanceof common_1.FabrixModel)) { throw new Error(`Fatal: Command ${command_type} object is not an instance of a Model`); } if (typeof object.toBinaryData === 'undefined') { throw new Error(`Fatal: Command ${command_type} object ${object.constructor.name} does not have a toBinaryData function`); } if (typeof object.toBinaryMetadata === 'undefined') { throw new Error(`Fatal: Command ${command_type} object ${object.constructor.name} does not have a toBinaryMetadata function`); } if (!app.models[object.constructor.name]) { throw new Error(`Fatal: Object is not a valid app model, make sure that it is an app model eg: app.models.${object.constructor.name}`); } if (!command_type || typeof command_type !== 'string') { throw new Error('Fatal: command_type string is required'); } this._list = lodash_1.isArray(data); if (this._list) { if (!data.every(d => d._options && d._options.isStaged)) { throw new Error(`Fatal: commands only accept staged data eg: ${object.constructor.name}.stage(data, { isNewRecord: false })`); } } else { if (!data._options && !data._options.isStaged) { throw new Error(`Fatal: commands only accept staged data eg: ${object.constructor.name}.stage(data, { isNewRecord: false })`); } } this.broadcaster = broadcaster; this.req = req; this.command_uuid = this.generateUUID(correlation_uuid); this.correlation_uuid = correlation_uuid; this.causation_uuid = causation_uuid; this.object = object; this.data = data; this.data_updates = this._list ? data.map(d => d.get({ plain: true })) : data.get({ plain: true }); this.data_previous = this._list ? [] : {}; this.data_applied = this._list ? [] : {}; this.data_changed = this._list ? [] : {}; this._metadata = metadata || {}; this._hooks = hooks || []; this.version = version; this.version_app = version_app || this.app.pkg.version; this.created_at = new Date(Date.now()).toISOString(); this.chain_events = chain_events; this.command_type = command_type; this.explain = explain; this.correlation_type = correlation_type; if (correlation_type) { this.explain[this.correlation_type] = this.explain[this.correlation_type] || {}; this.explain[this.correlation_type][this.command_type] = this.explain[this.correlation_type][this.command_type] || {}; } else { this.explain[this.command_type] = {}; } if (event_type) { this._event_type = event_type; } if (options.beforeHooks) { if (typeof options.beforeHooks === 'string') { const saga = this.getSagaFromHandler(options.beforeHooks); const method = this.getSagaMethodFromHandler(options.beforeHooks); const _saga = this.getSagaFromString(saga); if (_saga && _saga[method]) { this.beforeHooks = _saga[method]; } else if (_saga) { this.beforeHooks = _saga.before; } else { this.beforeHooks = this.app.sagas.BroadcastSaga.before; } } } else { this.beforeHooks = this.app.sagas.BroadcastSaga.before; } this.options = options; } get command_type() { return this._command_type; } set command_type(command_type) { const { pattern, keys } = regexdot_1.regexdot(command_type); const pattern_raw = command_type; this.pattern = pattern; this.pattern_raw = pattern_raw; this._command_type = replaceParams(command_type, keys, this.data, this.data_previous); } get chain() { return [ ...this.chain_before, ...this.chain_saga, ...this.chain_after ]; } generateUUID(correlationUUID) { if (typeof correlationUUID !== 'undefined' && correlationUUID !== '' && correlationUUID) { return correlationUUID; } return v4_1.default(); } _reload(_data, options) { return __awaiter(this, void 0, void 0, function* () { if (_data && typeof _data.reload !== 'function') { throw new Error('Data does not have reload function'); } if (_data.isReloaded) { return Promise.resolve([_data, _data]); } else if (_data.isSynthetic) { return Promise.resolve([_data, _data]); } else if (_data.isNewRecord) { _data.isReloaded = true; return Promise.resolve([_data, null]); } else { return _data.reload(options) .then((_previous) => { _data.isReloaded = true; return [_data, _previous]; }) .catch(err => { this.app.log.error(`Command ${this.command_uuid} reload`, err); return Promise.reject(err); }); } }); } reload(options) { return __awaiter(this, void 0, void 0, function* () { if (this._list) { return this.process(new Map(), this.data, (d, i) => { return this._reload(d, options) .then(([_current, _previous]) => { if (_previous) { _previous.attributes .forEach(k => { this.previous(`${i}.${k}`, _previous[k]); }); } else { _current.attributes .forEach(k => { this.previous(`${i}.${k}`, null); this.apply(`${i}.${k}`, d[k]); }); } return d; }); }) .then((results) => { return this.data; }); } else { return this._reload(this.data, options) .then(([_current, _previous]) => { if (_previous) { _previous.attributes .forEach(k => { this.previous(`${k}`, _previous[k]); }); } else { _current.attributes .forEach(k => { this.previous(`${k}`, null); this.apply(`${k}`, this.data[k]); }); } return this.data; }); } }); } _approvedUpdates(_data, _updates, approved = []) { const applied = {}, previous = {}; Object.keys(JSON.parse(JSON.stringify(_updates))).forEach((k, i) => { if (approved.indexOf(k) > -1) { if (!lodash_1.isEqual(_data[k], _updates[k])) { applied[k] = _updates[k]; previous[k] = _data[k]; } } }); return [previous, applied]; } approveUpdates(approved = []) { if (this._list) { this.data.forEach((d, i) => { const [previous, applied] = this._approvedUpdates(this.data[i], this.data_updates[i], approved); Object.keys(applied) .forEach(k => { this.apply(`${i}.${k}`, applied[k]); }); }); } else { const [previous, applied] = this._approvedUpdates(this.data, this.data_updates, approved); Object.keys(applied) .forEach(k => { this.apply(`${k}`, applied[k]); }); } return this.data; } approveUpdate(approved) { return this.approveUpdates([approved]); } _approvedChanges(_data, _updates, approved = []) { const applied = {}, updated = {}; Object.keys(JSON.parse(JSON.stringify(_data))).forEach((k, i) => { if (approved.indexOf(k) > -1) { applied[k] = _data[k]; if (!lodash_1.isEqual(_data[k], _updates[k])) { updated[k] = _data[k]; } } }); return [updated, applied]; } approveChanges(approved = []) { if (this._list) { this.data.forEach((d, i) => { const [updated, applied] = this._approvedChanges(this.data[i], this.data_updates[i], approved); Object.keys(updated) .forEach(k => { this.update(`${i}.${k}`, applied[k]); }); Object.keys(applied) .forEach(k => { this.apply(`${i}.${k}`, applied[k]); }); }); } else { const [updated, applied] = this._approvedChanges(this.data, this.data_updates, approved); Object.keys(updated) .forEach(k => { this.update(`${k}`, applied[k]); }); Object.keys(applied) .forEach(k => { this.apply(`${k}`, applied[k]); }); } return this.data; } approveChange(approved) { return this.approveChanges([approved]); } previous(path, value) { return lodash_1.set(this.data_previous, path, value, null); } update(path, value) { return lodash_1.set(this.data_updates, path, value); } change(path, value) { return lodash_1.set(this.data_changed, path, value, null); } apply(path, value) { if (typeof value === 'undefined') { value = lodash_1.get(this.data, path, null); } const current = lodash_1.get(this.data_previous, path, null); const rawCurrent = raw(current); const rawValue = raw(value); if (!lodash_1.isEqual(rawCurrent, rawValue)) { this.change(path, current); } this.update(path, value); lodash_1.set(this.data_applied, path, value); lodash_1.set(this.data, path, value); return lodash_1.get(this.data, path); } unapplied() { let unapplied; if (this._list) { unapplied = []; this.data.forEach((d, i) => { const current = this.data[i].attributes; const applied = Object.keys(this.data_applied[i]); unapplied.push(lodash_1.xor(applied, current)); }); } else { const current = this.data.attributes; const applied = Object.keys(this.data_applied); unapplied = lodash_1.xor(applied, current); } return unapplied; } updated() { let unapplied; if (this._list) { unapplied = []; this.data.forEach((d, i) => { const current = this.data[i].attributes; const applied = Object.keys(this.data_changed[i]); unapplied.push(applied); }); } else { const current = this.data.attributes; const applied = Object.keys(this.data_changed); unapplied = applied; } return unapplied; } createdAt() { if (this._list) { this.data.forEach((d, i) => { if (d && d.isNewRecord) { this.apply(`${i}.created_at`, new Date(Date.now()).toISOString()); } }); } else if (this.data && this.data.isNewRecord) { this.apply(`created_at`, new Date(Date.now()).toISOString()); } return this; } updatedAt() { if (this._list) { this.data.forEach((d, i) => { if (d && this.hasChanges(i)) { this.apply(`${i}.updated_at`, new Date(Date.now()).toISOString()); } }); } else if (this.data && this.hasChanges()) { this.apply(`updated_at`, new Date(Date.now()).toISOString()); } return this; } deletedAt() { if (this._list) { this.data.forEach((d, i) => { if (d) { this.apply(`${i}.deleted_at`, new Date(Date.now()).toISOString()); } }); } else if (this.data) { this.apply(`deleted_at`, new Date(Date.now()).toISOString()); } return this; } changes(key) { let changes = []; if (this._list) { this.data.forEach((d, i) => { if (d.isNewRecord) { changes[i] = [...(changes[i] || []), ...d.attributes]; } else if (this.data_changed && this.data_changed[i]) { return changes[i] = [...(changes[i] || []), ...Object.keys(this.data_changed[i])]; } }); } else { if (this.data.isNewRecord) { changes = [...changes, ...this.data.attributes]; } else if (Object.keys(this.data_changed || {}).length > 0) { changes = [...changes, ...Object.keys(this.data_changed)]; } } if (!lodash_1.isArray(changes)) { throw new Error('metadata changes should be an array'); } if (key) { if (this._list) { return this.data.map((d, i) => { return changes[i].includes(key); }); } else { if (changes.includes(key)) { return key; } else { return false; } } } else { return changes; } } hasChanges(index) { const changes = this.changes(); if (this._list && typeof index !== 'undefined') { if (changes[index] && changes[index].length > 0) { return true; } else { return false; } } else { if (changes && changes.length > 0) { return true; } else { return false; } } } changedPreviousData() { let previous; if (this._list) { const changes = this.changes(); previous = []; changes.forEach((d, i) => { previous[i] = {}; d.forEach(_d => { if (this.data_previous[i]) { previous[i][_d] = this.data_previous[i][_d]; } else { previous[i][_d] = null; } }); }); } else { const changes = this.changes(); previous = {}; changes.forEach((d, i) => { previous[d] = this.data_previous[d]; }); } return previous; } get hooks() { return this._hooks || []; } set hooks(values) { this._hooks = values; } get metadata() { return Object.assign({}, (this._metadata || {}), { changes: this.changes(), previous: this.changedPreviousData(), hooks: this.hooks }); } set metadata(metadata) { this._metadata = metadata; return; } restage() { if (this._list) { this.data = this.data.map(d => { return this.object.stage(d, d._options); }); } else { this.data = this.object.stage(this.data, this.data._options); } } getSagaFromString(handler) { return lodash_1.get(this.app.sagas, handler); } getSagaFromHandler(handler) { return lodash_1.isString(handler) ? handler.split('.')[0] : handler; } getSagaMethodFromHandler(handler) { return lodash_1.isString(handler) ? handler.split('.')[1] : handler; } process(managers, ...args) { let serial = true; if (serial) { return this.broadcastSeries(...args); } else { return this.broadcastParallel(...args); } } broadcastSeries(...args) { return this.app.broadcastSeries(...args); } broadcastParallel(...args) { return Promise.all([...args]); } broadcast(validator, options) { if (!this.beforeHooks) { throw new this.app.errors.GenericError('E_BAD_REQUEST', 'command.broadcast can not be used if not constructed with options.beforeHooks', `${this.name} Command Error`); } if (!this._event_type) { throw new this.app.errors.GenericError('E_BAD_REQUEST', 'command.broadcast can not be used if not constructed with an event_type', `${this.name} Command Error`); } return this.beforeHooks(this, validator, options) .then(([_command, _options]) => { const event = this.broadcaster.buildEvent({ event_type: this._event_type, correlation_uuid: _command.command_uuid, command: _command }); return this.broadcaster.broadcast(event, _options); }); } } exports.BroadcastCommand = BroadcastCommand; BroadcastCommand.prototype.changed = function (str) { if (str && this.metadata && this.metadata.changes) { return !!this.metadata.changes[str]; } else if (!str && this.metadata && this.metadata.changes) { return this.metadata.changes; } return false; }; BroadcastCommand.prototype.toJSON = function (str) { const res = { correlation_uuid: this.correlation_uuid, causation_uuid: this.causation_uuid, command_type: `${this.command_type}`, pattern: `${this.pattern}`, pattern_raw: `${this.pattern_raw}`, object: `${this.object.constructor.name}${this._list ? '.list' : ''}`, data: JSON.parse(JSON.stringify(this.data)), data_changed: JSON.parse(JSON.stringify(this.data_changed)), data_previous: JSON.parse(JSON.stringify(this.data_previous)), metadata: this.metadata, version: this.version, version_app: this.version_app, created_at: this.created_at }; return res; }; BroadcastCommand.prototype.toEVENT = function (str) { const res = { correlation_uuid: this.correlation_uuid || this.command_uuid, causation_uuid: this.causation_uuid, command_type: `${this.command_type}`, pattern: this.pattern, pattern_raw: `${this.pattern_raw}`, object: this.object, data: this.data, data_changed: this.data_changed, data_previous: this.data_previous, metadata: this.metadata, version: this.version, version_app: this.version_app, created_at: this.created_at, chain_before: this.chain_before, chain_saga: this.chain_saga, chain_after: this.chain_after, chain_events: this.chain_events, }; return res; }; BroadcastCommand.prototype.getDataValue = function (str) { if (str === 'object') { return `${this.object.constructor.name}${this._list ? '.list' : ''}`; } return this[str]; }; BroadcastCommand.prototype.mergeData = function (method, handler, command) { if (lodash_1.isObject(handler.merge) && handler.merge.as && !handler.merge.each) { return this.mergeAs(method, handler, command); } else if (lodash_1.isObject(handler.merge) && handler.merge.as && handler.merge.each) { return this.mergeEachAs(method, handler, command); } else { if (handler.expects_response && handler.expects_response !== '*' && command.getDataValue('object') !== handler.expects_response) { throw new Error(`Hook: ${method} merge expected ${handler.expects_response} but got ${command.getDataValue('object')} for ${command.command_type}`); } if (!command || !command.data) { this.app.log.debug('BroadcastCommand.mergeData was passed empty data'); return this; } if (this.data === command.data) { return this; } if (lodash_1.isArray(command.data) && !lodash_1.isArray(this.data)) { throw new Error('unhandled expected command data to be an object to match data'); } if (!lodash_1.isArray(command.data) && lodash_1.isArray(this.data)) { throw new Error('unhandled expected command data to be an array to match data'); } if (lodash_1.isArray(this.data) && lodash_1.isArray(command.data)) { this.app.log.error('Unhandled Array merge attempted'); } else { const cleanData = command.data.toJSON ? command.data.toJSON() : command.data; this.app.log.silly(this.command_type, 'merging values', Object.keys(this.data.toJSON()), '->', cleanData); Object.keys(cleanData).forEach(k => { this.apply(k, command.data[k] !== 'undefined' ? command.data[k] : this.data[k]); }); } this.app.log.debug(`Handler ${method} merged ${this.command_type} -> ${command.command_type}`); return this; } }; BroadcastCommand.prototype.handleData = function (method, handler, command) { helpers_1.helpers.handle(this.data, command.data, handler.data); return this; }; BroadcastCommand.prototype.handleMetadata = function (method, handler, command) { helpers_1.helpers.handle(this.metadata, command.metadata, handler.metadata); return this; }; BroadcastCommand.prototype.mergeAs = function (method, handler, command) { if ((!handler.merge && !handler.mergeAs) || !command || !command.data) { this.app.log.debug(`BroadcastCommand.mergeAs was passed empty handler mergeAs property or command.data for ${command.command_type}`); return this; } if (handler.expects_response && handler.expects_response !== '*' && command.getDataValue('object') !== handler.expects_response) { throw new Error(`mergeAs expected ${handler.expects_response} but got ${command.getDataValue('object')} for ${command.command_type}`); } const as = handler.mergeAs || handler.merge.as; this.data[as] = command.data; this.app.log.debug(`Handler ${method} merged each ${this.command_type} -> ${command.command_type}`, ` as ${as}`); return this; }; BroadcastCommand.prototype.mergeEachAs = function (method, handler, command) { if ((!handler.merge && !handler.mergeEachAs) || !command || !command.data) { this.app.log.debug(`BroadcastCommand.mergeEachAs was passed empty handler mergeEachAs property or command.data for ${command.command_type}`); return this; } this.app.log.debug(`Handler ${method} merged each ${this.command_type} -> ${command.command_type}`, ` as ${handler.mergeEachAs || handler.merge.as}`); return this; }; BroadcastCommand.prototype.includeAs = function (method, handler, command) { if (!handler.includeAs || !command || !command.data) { this.app.log.debug(`BroadcastCommand.includeAs was passed empty includeAs property or command.data for ${command.command_type}`); return this; } if (handler.expects_response && handler.expects_response !== '*' && command.getDataValue('object') !== handler.expects_response) { throw new Error(`includeAs expected ${handler.expects_response} but got ${command.getDataValue('object')} for ${command.command_type}`); } this.includes = this.includes || {}; this.includes[handler.includeAs] = command.data; return this; }; //# sourceMappingURL=BroadcastCommand.js.map