@fabrix/spool-broadcast
Version:
Spool: broadcast for Fabrix to implement CQRS and Event Sourcing
727 lines • 28.1 kB
JavaScript
"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