@sekiban/postgres
Version:
PostgreSQL storage provider for Sekiban Event Sourcing framework
205 lines (204 loc) • 9.53 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.PostgresEventStore = void 0;
const neverthrow_1 = require("neverthrow");
const core_1 = require("@sekiban/core");
/**
* PostgreSQL implementation of IEventStore
* Implements both IEventReader and IEventWriter interfaces
*/
class PostgresEventStore {
constructor(pool) {
this.pool = pool;
}
/**
* Initialize the storage provider
*/
initialize() {
return neverthrow_1.ResultAsync.fromPromise((async () => {
try {
// Create events table if it doesn't exist (matching C# DbEvent structure)
await this.pool.query(`
CREATE TABLE IF NOT EXISTS events (
id UUID PRIMARY KEY,
payload JSON NOT NULL,
sortable_unique_id VARCHAR(255) NOT NULL,
version INTEGER NOT NULL,
aggregate_id UUID NOT NULL,
root_partition_key VARCHAR(255) NOT NULL,
"timestamp" TIMESTAMP NOT NULL,
partition_key VARCHAR(255) NOT NULL,
aggregate_group VARCHAR(255) NOT NULL,
payload_type_name VARCHAR(255) NOT NULL,
causation_id VARCHAR(255) NOT NULL DEFAULT '',
correlation_id VARCHAR(255) NOT NULL DEFAULT '',
executed_user VARCHAR(255) NOT NULL DEFAULT ''
)
`);
// Create indexes for efficient querying
await this.pool.query(`
CREATE INDEX IF NOT EXISTS idx_events_partition_key
ON events(partition_key)
`);
await this.pool.query(`
CREATE INDEX IF NOT EXISTS idx_events_root_partition
ON events(root_partition_key)
`);
await this.pool.query(`
CREATE INDEX IF NOT EXISTS idx_events_aggregate
ON events(aggregate_group, aggregate_id)
`);
await this.pool.query(`
CREATE INDEX IF NOT EXISTS idx_events_timestamp
ON events("timestamp")
`);
await this.pool.query(`
CREATE INDEX IF NOT EXISTS idx_events_sortable_unique_id
ON events(sortable_unique_id)
`);
}
catch (error) {
throw new core_1.ConnectionError(`Failed to initialize PostgreSQL: ${error instanceof Error ? error.message : 'Unknown error'}`, error instanceof Error ? error : undefined);
}
})(), (error) => error instanceof core_1.StorageError ? error : new core_1.ConnectionError(`Failed to initialize PostgreSQL: ${error instanceof Error ? error.message : 'Unknown error'}`, error instanceof Error ? error : undefined));
}
/**
* Get events based on retrieval information
*/
getEvents(eventRetrievalInfo) {
return neverthrow_1.ResultAsync.fromPromise(this.doGetEvents(eventRetrievalInfo), (error) => new core_1.StorageError(`Failed to query events: ${error instanceof Error ? error.message : 'Unknown error'}`, 'QUERY_FAILED', error instanceof Error ? error : undefined));
}
async doGetEvents(eventRetrievalInfo) {
const { query, params } = this.buildQuery(eventRetrievalInfo);
const result = await this.pool.query(query, params);
// Parse events from JSON payload (matching C# structure)
let events = result.rows.map(row => JSON.parse(row.payload));
// Apply sortable ID conditions in memory since PostgreSQL doesn't understand our custom ID format
if (eventRetrievalInfo.sortableIdCondition) {
events = events.filter(e => !eventRetrievalInfo.sortableIdCondition.outsideOfRange(e.id));
}
// Sort by sortable ID
events.sort((a, b) => core_1.SortableUniqueId.compare(a.id, b.id));
return events;
}
buildQuery(eventRetrievalInfo) {
const conditions = [];
const params = [];
let paramIndex = 1;
// Base query - select all columns to match C# structure
let query = 'SELECT * FROM events';
// Filter by root partition key
if (eventRetrievalInfo.rootPartitionKey.hasValueProperty) {
conditions.push(`root_partition_key = $${paramIndex++}`);
params.push(eventRetrievalInfo.rootPartitionKey.getValue());
}
// Filter by aggregate stream (group)
if (eventRetrievalInfo.aggregateStream.hasValueProperty) {
const streamNames = eventRetrievalInfo.aggregateStream.getValue().getStreamNames();
if (streamNames.length === 1) {
conditions.push(`aggregate_group = $${paramIndex++}`);
params.push(streamNames[0]);
}
else if (streamNames.length > 1) {
const placeholders = streamNames.map(() => `$${paramIndex++}`).join(', ');
conditions.push(`aggregate_group IN (${placeholders})`);
params.push(...streamNames);
}
}
// Filter by aggregate ID
if (eventRetrievalInfo.aggregateId.hasValueProperty) {
conditions.push(`aggregate_id = $${paramIndex++}`);
params.push(eventRetrievalInfo.aggregateId.getValue());
}
// Add WHERE clause if there are conditions
if (conditions.length > 0) {
query += ' WHERE ' + conditions.join(' AND ');
}
// Order by ID (which is sortable)
query += ' ORDER BY id ASC';
// Add LIMIT if specified
if (eventRetrievalInfo.maxCount.hasValueProperty) {
query += ` LIMIT $${paramIndex++}`;
params.push(eventRetrievalInfo.maxCount.getValue());
}
return { query, params };
}
/**
* Save events to storage
*/
async saveEvents(events) {
const result = await neverthrow_1.ResultAsync.fromPromise(this.doSaveEvents(events), (error) => new core_1.StorageError(`Failed to save events: ${error instanceof Error ? error.message : 'Unknown error'}`, 'SAVE_FAILED', error instanceof Error ? error : undefined));
if (result.isErr()) {
throw result.error;
}
}
async doSaveEvents(events) {
let client = null;
try {
client = await this.pool.connect();
// Start transaction
await client.query('BEGIN');
try {
// Insert each event (matching C# DbEvent structure)
for (const event of events) {
// Extract metadata with defaults
const metadata = event.metadata;
const causationId = metadata.causationId || '';
const correlationId = metadata.correlationId || '';
const executedUser = metadata.executedUser || '';
const insertQuery = `INSERT INTO events (
id, payload, sortable_unique_id,
version, aggregate_id, root_partition_key,
"timestamp", partition_key, aggregate_group,
payload_type_name, causation_id, correlation_id,
executed_user
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)`;
const insertParams = [
event.id, // id should already be a UUID
JSON.stringify(event.payload || event.eventData || event), // Store the event data as payload
typeof event.sortableUniqueId === 'string' ? event.sortableUniqueId : event.sortableUniqueId?.value || event.sortableUniqueId?.toString(),
event.version,
event.aggregateId,
event.partitionKeys?.rootPartitionKey || 'default',
event.timestamp || new Date(),
event.partitionKeys?.partitionKey || event.partitionKey,
event.partitionKeys?.group || event.aggregateGroup || 'default',
event.eventType, // payload_type_name
causationId,
correlationId,
executedUser
];
try {
await client.query(insertQuery, insertParams);
}
catch (queryError) {
console.error('SQL Error executing INSERT:', queryError.message);
console.error('SQL Error Code:', queryError.code);
console.error('SQL Error Position:', queryError.position);
console.error('SQL Error Detail:', queryError.detail);
throw queryError;
}
}
// Commit transaction
await client.query('COMMIT');
}
catch (error) {
// Rollback on error
await client.query('ROLLBACK');
throw error;
}
}
finally {
if (client) {
client.release();
}
}
}
/**
* Close the storage provider
*/
close() {
return neverthrow_1.ResultAsync.fromPromise(this.pool.end(), (error) => new core_1.StorageError(`Failed to close PostgreSQL connection: ${error instanceof Error ? error.message : 'Unknown error'}`, 'CLOSE_FAILED', error instanceof Error ? error : undefined));
}
}
exports.PostgresEventStore = PostgresEventStore;