smart-saga-pattern
Version:
A library implementing the Saga pattern for microservice architecture
147 lines (146 loc) • 6.54 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.SagaBuilder = void 0;
const SagaDefinition_1 = require("../orchestration/SagaDefinition");
const decorators_1 = require("./decorators");
/**
* Builder for creating Saga definitions from decorated classes
*/
class SagaBuilder {
/**
* Builds a Saga definition from all registered steps
* @template T Type of the context
*/
static buildFromAllSteps() {
const steps = decorators_1.SagaStepContainer.getAllSteps();
// Sort steps by order
const sortedSteps = steps.sort((a, b) => {
const metadataA = Reflect.getMetadata(decorators_1.SAGA_STEP_METADATA, a);
const metadataB = Reflect.getMetadata(decorators_1.SAGA_STEP_METADATA, b);
return (metadataA.options.order || 0) - (metadataB.options.order || 0);
});
// Create a SagaDefinition builder
const builder = SagaDefinition_1.SagaDefinition.builder();
// Add each step to the builder
for (const stepClass of sortedSteps) {
this.addStepToBuilder(builder, stepClass);
}
return builder.build();
}
/**
* Builds a Saga definition from specific step classes
* @param stepClasses Step classes to include in the Saga
* @template T Type of the context
*/
static buildFromSteps(stepClasses) {
// Create a SagaDefinition builder
const builder = SagaDefinition_1.SagaDefinition.builder();
// Add each step to the builder
for (const stepClass of stepClasses) {
this.addStepToBuilder(builder, stepClass);
}
return builder.build();
}
/**
* Adds a step to a SagaDefinition builder
* @param builder SagaDefinition builder
* @param stepClass Step class to add
* @template T Type of the context
*/
static addStepToBuilder(builder, stepClass) {
// Get step metadata
const stepMetadata = Reflect.getMetadata(decorators_1.SAGA_STEP_METADATA, stepClass);
if (!stepMetadata) {
throw new Error(`Class ${stepClass.name} is not a valid Saga step`);
}
// Find invoke method
const invokeMethod = this.findMethodWithMetadata(stepClass, decorators_1.INVOKE_METADATA);
if (!invokeMethod) {
throw new Error(`Step ${stepClass.name} does not have an @Invoke method`);
}
// Find compensation method (optional)
const compensationMethod = this.findMethodWithMetadata(stepClass, decorators_1.COMPENSATION_METADATA);
// Create transaction function
const transaction = async (context, metadata) => {
// Create an instance of the step class
const instance = new stepClass();
// Create metadata if not provided
const transactionMetadata = metadata || {
id: `tx-${Date.now()}`,
name: stepClass.name,
timestamp: Date.now(),
};
// Log the context for debugging
console.log(`[${stepClass.name}] Transaction context:`, JSON.stringify(context, null, 2));
// Initialize step data if using structured context
if (context && typeof context === "object" && context.steps) {
// Check if we're using structured context
if (!context.steps[stepClass.name]) {
// Initialize empty step data
context.steps[stepClass.name] = {
input: {},
output: {},
compensation: {},
};
// Initialize input based on step name
if (context.global) {
// For DebitAccount (first step)
if (stepClass.name === "DebitAccount") {
context.steps[stepClass.name].input = {
accountId: context.global.sourceAccountId,
amount: context.global.amount,
};
}
}
}
}
// Call the invoke method
return await instance[invokeMethod](context, transactionMetadata);
};
// Create compensation function (if compensation method exists)
let compensation;
if (compensationMethod) {
compensation = async (context, transactionOutput, metadata) => {
// Create an instance of the step class
const instance = new stepClass();
// Create metadata
const compensationMetadata = {
id: `comp-${Date.now()}`,
name: `${stepClass.name} Compensation`,
timestamp: Date.now(),
transactionId: (transactionOutput === null || transactionOutput === void 0 ? void 0 : transactionOutput.transactionId) || "unknown",
};
// Log the context and transaction output for debugging
console.log(`[${stepClass.name}] Compensation context:`, JSON.stringify(context, null, 2));
console.log(`[${stepClass.name}] Transaction output:`, JSON.stringify(transactionOutput, null, 2));
// Call the compensation method
await instance[compensationMethod](context, transactionOutput, compensationMetadata);
};
}
// Add step to the builder
builder.step(stepClass.name, transaction, compensation, stepMetadata.options.requiresCompensation);
}
/**
* Finds a method with specific metadata in a class
* @param stepClass Class to search in
* @param metadataKey Metadata key to look for
*/
static findMethodWithMetadata(stepClass, metadataKey) {
// Get all property names of the prototype
const propertyNames = Object.getOwnPropertyNames(stepClass.prototype);
// Find the method with the metadata
for (const propertyName of propertyNames) {
// Skip constructor
if (propertyName === "constructor") {
continue;
}
// Check if the method has the metadata
const metadata = Reflect.getMetadata(metadataKey, stepClass, propertyName);
if (metadata) {
return propertyName;
}
}
return undefined;
}
}
exports.SagaBuilder = SagaBuilder;