event-store-projection-testing-framework
Version:
Test framework for Event Store projections
553 lines (476 loc) • 13 kB
JavaScript
;
var $projections = {
createEventProcessor: function(_log, _notify) {
var debugging = false;
var runDefaultHandler = true;
var eventHandlers = {};
var anyEventHandlers = [];
var deletedNotificationHandlers = [];
var createdNotificationHandlers = [];
var rawEventHandlers = [];
var transformers = [];
var getStatePartitionHandler = function() {
throw "GetStatePartition is not defined";
};
var sources = {
/* TODO: comment out default falses to reduce message size */
allStreams: false,
allEvents: true,
byStreams: false,
byCustomPartitions: false,
categories: [],
streams: [],
events: [],
options: {
definesStateTransform: false,
handlesDeletedNotifications: false,
producesResults: false,
definesFold: false,
resultStreamName: null,
partitionResultStreamNamePattern: null,
$includeLinks: false,
reorderEvents: false,
processingLag: 0,
biState: false
},
version: 4
};
var initStateHandler = function() {
return {};
};
var initSharedStateHandler = function() {
return {};
};
var projectionState = null;
var projectionSharedState = null;
var commandHandlers = {
set_debugging: function() {
debugging = true;
},
initialize: function() {
var initialState = initStateHandler();
projectionState = initialState;
return "OK";
},
initialize_shared: function() {
var initialState = initSharedStateHandler();
projectionSharedState = initialState;
return "OK";
},
get_state_partition: function(event,
isJson,
streamId,
eventType,
category,
sequenceNumber,
metadata,
linkMetadata) {
return getStatePartition(event,
isJson,
streamId,
eventType,
category,
sequenceNumber,
metadata,
linkMetadata);
},
process_event: function(event,
isJson,
streamId,
eventType,
category,
sequenceNumber,
metadata,
linkMetadata,
partition,
eventId) {
processEvent(event,
isJson,
streamId,
eventType,
category,
sequenceNumber,
metadata,
linkMetadata,
partition,
eventId);
var stateJson;
var finalResult;
if (!sources.options.biState) {
stateJson = JSON.stringify(projectionState);
return stateJson;
} else {
stateJson = JSON.stringify(projectionState);
var sharedStateJson = JSON.stringify(projectionSharedState);
finalResult = [stateJson, sharedStateJson];
return finalResult;
}
},
process_deleted_notification: function(partition, isSoftDeleted) {
processDeletedNotification(partition, isSoftDeleted);
var stateJson;
if (!sources.options.biState) {
stateJson = JSON.stringify(projectionState);
return stateJson;
} else {
throw "Bi-State projections do not support delete notifications";
}
},
process_created_notification: function(event,
isJson,
streamId,
eventType,
category,
sequenceNumber,
metadata,
linkMetadata,
partition) {
processCreatedNotification(event,
isJson,
streamId,
eventType,
category,
sequenceNumber,
metadata,
linkMetadata,
partition);
var stateJson;
stateJson = JSON.stringify(projectionState);
return stateJson;
},
transform_state_to_result: function() {
var result = projectionState;
for (var i = 0; i < transformers.length; i++) {
var by = transformers[i];
result = by(result);
if (result === null)
break;
}
return result !== null ? JSON.stringify(result) : null;
},
set_state: function(jsonState) {
var parsedState = JSON.parse(jsonState);
projectionState = parsedState;
return "OK";
},
set_shared_state: function(jsonState) {
var parsedState = JSON.parse(jsonState);
projectionSharedState = parsedState;
return "OK";
},
debugging_get_state: function() {
return JSON.stringify(projectionState);
},
get_sources: function() {
return JSON.stringify(sources);
}
};
function registerCommandHandlers($on) {
// this is the only way to pass parameters to the system module
for (var name in commandHandlers) {
$on(name, commandHandlers[name]);
}
}
function on_event(eventName, eventHandler) {
runDefaultHandler = false;
eventHandlers[eventName] = eventHandler;
sources.allEvents = false;
sources.events.push(eventName);
sources.options.definesFold = true;
}
function on_init_state(initHandler) {
initStateHandler = initHandler;
sources.options.definesFold = true;
}
function on_init_shared_state(initHandler) {
initSharedStateHandler = initHandler;
sources.options.definesFold = true;
}
function on_any(eventHandler) {
runDefaultHandler = false;
sources.allEvents = true;
anyEventHandlers.push(eventHandler);
sources.options.definesFold = true;
}
function on_deleted_notification(eventHandler) {
deletedNotificationHandlers.push(eventHandler);
sources.options.handlesDeletedNotifications = true;
sources.options.definesFold = true;
}
function on_created_notification(eventHandler) {
createdNotificationHandlers.push(eventHandler);
sources.options.handlesCreatedNotifications = true;
sources.options.definesFold = true;
}
function on_raw(eventHandler) {
runDefaultHandler = false;
sources.allEvents = true;
rawEventHandlers.push(eventHandler);
sources.options.definesFold = true;
}
function callHandler(handler, state, eventEnvelope) {
if (debugging)
debugger;
var newState = handler(state, eventEnvelope);
if (newState === undefined)
newState = state;
return newState;
};
function tryDeserializeBody(eventEnvelope) {
var eventRaw = eventEnvelope.bodyRaw;
try {
if (eventRaw == '') {
eventEnvelope.body = {};
} else if (typeof eventRaw === "object") { //TODO: why do we need this?
eventEnvelope.body = eventRaw;
eventEnvelope.isJson = true;
} else {
eventEnvelope.body = JSON.parse(eventRaw);
eventEnvelope.isJson = true;
}
eventEnvelope.data = eventEnvelope.body;
} catch (ex) {
_log("JSON Parsing error: " + ex);
eventEnvelope.jsonError = ex;
eventEnvelope.body = undefined;
eventEnvelope.data = undefined;
}
}
function envelope(body,
bodyRaw,
eventType,
streamId,
sequenceNumber,
metadataRaw,
linkMetadataRaw,
partition,
eventId) {
this.isJson = false;
this.data = body;
this.body = body;
this.bodyRaw = bodyRaw;;
this.eventType = eventType;
this.streamId = streamId;
this.sequenceNumber = sequenceNumber;
this.metadataRaw = metadataRaw;
this.linkMetadataRaw = linkMetadataRaw;
this.partition = partition;
this.metadata_ = null;
this.eventId = eventId;
}
Object.defineProperty(envelope.prototype,
"metadata",
{
get: function() {
if (!this.metadata_ && this.metadataRaw) {
this.metadata_ = JSON.parse(this.metadataRaw);
}
return this.metadata_;
}
});
Object.defineProperty(envelope.prototype,
"linkMetadata",
{
get: function() {
if (!this.linkMetadata_) {
if (this.linkMetadataRaw) {
this.linkMetadata_ = JSON.parse(this.linkMetadataRaw);
} else {
this.linkMetadata_ = {};
}
}
return this.linkMetadata_;
}
});
function getStatePartition(eventRaw,
isJson,
streamId,
eventType,
category,
sequenceNumber,
metadataRaw,
linkMetadataRaw) {
var eventHandler = getStatePartitionHandler;
var eventEnvelope = new envelope(null,
eventRaw,
eventType,
streamId,
sequenceNumber,
metadataRaw,
linkMetadataRaw,
null,
null);
if (isJson)
tryDeserializeBody(eventEnvelope);
var partition = eventHandler(eventEnvelope);
var result;
//TODO: warn/disable empty string
if (partition === undefined || partition === null || partition === "")
result = "";
else
result = partition.toString();
return result;
}
function defaultEventHandler(state, eventEnvelope) {
return eventEnvelope.isJson ? eventEnvelope.body : { $e: eventEnvelope.bodyRaw };
}
function processEvent(eventRaw,
isJson,
streamId,
eventType,
category,
sequenceNumber,
metadataRaw,
linkMetadataRaw,
partition,
eventId) {
var eventName = eventType;
var eventHandler;
var state = !sources.options.biState ? projectionState : [projectionState, projectionSharedState];
var index;
var eventEnvelope = new envelope(null,
eventRaw,
eventType,
streamId,
sequenceNumber,
metadataRaw,
linkMetadataRaw,
partition,
eventId);
// debug only
for (index = 0; index < rawEventHandlers.length; index++) {
eventHandler = rawEventHandlers[index];
state = callHandler(eventHandler, state, eventEnvelope);
}
eventHandler = eventHandlers[eventName];
if (isJson && (runDefaultHandler || eventHandler !== undefined || anyEventHandlers.length > 0)) {
tryDeserializeBody(eventEnvelope);
}
if (runDefaultHandler) {
state = callHandler(defaultEventHandler, state, eventEnvelope);
}
for (index = 0; index < anyEventHandlers.length; index++) {
eventHandler = anyEventHandlers[index];
state = callHandler(eventHandler, state, eventEnvelope);
}
eventHandler = eventHandlers[eventName];
if (eventHandler !== undefined) {
state = callHandler(eventHandler, state, eventEnvelope);
}
if (!sources.options.biState) {
projectionState = state;
} else {
projectionState = state[0];
projectionSharedState = state[1];
}
}
function processDeletedNotification(partition, isSoftDeleted) {
var eventEnvelope = { partition: partition, isSoftDeleted: isSoftDeleted };
var state = !sources.options.biState ? projectionState : [projectionState, projectionSharedState];
var index;
var eventHandler;
for (index = 0; index < deletedNotificationHandlers.length; index++) {
eventHandler = deletedNotificationHandlers[index];
state = callHandler(eventHandler, state, eventEnvelope);
}
if (!sources.options.biState) {
projectionState = state;
} else {
throw "Bi-State projections do not support delete notifications";
}
}
function processCreatedNotification(eventRaw,
isJson,
streamId,
eventType,
category,
sequenceNumber,
metadataRaw,
linkMetadataRaw,
partition) {
var eventHandler;
var state = !sources.options.biState ? projectionState : [projectionState, projectionSharedState];
var index;
var eventEnvelope = new envelope(null,
eventRaw,
eventType,
streamId,
sequenceNumber,
metadataRaw,
linkMetadataRaw,
partition);
if (isJson) {
tryDeserializeBody(eventEnvelope);
}
for (index = 0; index < createdNotificationHandlers.length; index++) {
eventHandler = createdNotificationHandlers[index];
state = callHandler(eventHandler, state, eventEnvelope);
}
if (!sources.options.biState) {
projectionState = state;
} else {
projectionState = state[0];
projectionSharedState = state[1];
}
}
function fromStream(sourceStream) {
sources.streams.push(sourceStream);
}
function fromCategory(sourceCategory) {
sources.categories.push(sourceCategory);
}
function byStream() {
sources.byStreams = true;
}
function partitionBy(eventHandler) {
getStatePartitionHandler = eventHandler;
sources.byCustomPartitions = true;
}
function $defines_state_transform() {
sources.options.definesStateTransform = true;
sources.options.producesResults = true;
}
function $outputState() {
sources.options.producesResults = true;
}
function chainTransformBy(by) {
transformers.push(by);
sources.options.definesStateTransform = true;
sources.options.producesResults = true;
}
function fromAll() {
sources.allStreams = true;
}
function emit(ev) {
_notify("emit", JSON.stringify(ev));
}
function options(opts) {
for (var name in opts) {
if (sources.options[name] === undefined)
throw "Unrecognized option: " + name;
sources.options[name] = opts[name];
}
}
return {
on_event: on_event,
on_init_state: on_init_state,
on_init_shared_state: on_init_shared_state,
on_any: on_any,
on_raw: on_raw,
on_deleted_notification: on_deleted_notification,
on_created_notification: on_created_notification,
fromAll: fromAll,
fromCategory: fromCategory,
fromStream: fromStream,
byStream: byStream,
partitionBy: partitionBy,
$defines_state_transform: $defines_state_transform,
$outputState: $outputState,
chainTransformBy: chainTransformBy,
emit: emit,
options: options,
register_command_handlers: registerCommandHandlers,
};
}
};
module.exports = $projections;