UNPKG

@sekiban/postgres

Version:

PostgreSQL storage provider for Sekiban Event Sourcing framework

264 lines (263 loc) 9.54 kB
// src/postgres-event-store.ts import { ResultAsync } from "neverthrow"; import { StorageError, ConnectionError, SortableUniqueId } from "@sekiban/core"; var PostgresEventStore = class { constructor(pool) { this.pool = pool; } /** * Initialize the storage provider */ initialize() { return ResultAsync.fromPromise( (async () => { try { 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 '' ) `); 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 ConnectionError( `Failed to initialize PostgreSQL: ${error instanceof Error ? error.message : "Unknown error"}`, error instanceof Error ? error : void 0 ); } })(), (error) => error instanceof StorageError ? error : new ConnectionError( `Failed to initialize PostgreSQL: ${error instanceof Error ? error.message : "Unknown error"}`, error instanceof Error ? error : void 0 ) ); } /** * Get events based on retrieval information */ getEvents(eventRetrievalInfo) { return ResultAsync.fromPromise( this.doGetEvents(eventRetrievalInfo), (error) => new StorageError( `Failed to query events: ${error instanceof Error ? error.message : "Unknown error"}`, "QUERY_FAILED", error instanceof Error ? error : void 0 ) ); } async doGetEvents(eventRetrievalInfo) { const { query, params } = this.buildQuery(eventRetrievalInfo); const result = await this.pool.query(query, params); let events = result.rows.map((row) => JSON.parse(row.payload)); if (eventRetrievalInfo.sortableIdCondition) { events = events.filter( (e) => !eventRetrievalInfo.sortableIdCondition.outsideOfRange(e.id) ); } events.sort((a, b) => SortableUniqueId.compare(a.id, b.id)); return events; } buildQuery(eventRetrievalInfo) { const conditions = []; const params = []; let paramIndex = 1; let query = "SELECT * FROM events"; if (eventRetrievalInfo.rootPartitionKey.hasValueProperty) { conditions.push(`root_partition_key = $${paramIndex++}`); params.push(eventRetrievalInfo.rootPartitionKey.getValue()); } 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); } } if (eventRetrievalInfo.aggregateId.hasValueProperty) { conditions.push(`aggregate_id = $${paramIndex++}`); params.push(eventRetrievalInfo.aggregateId.getValue()); } if (conditions.length > 0) { query += " WHERE " + conditions.join(" AND "); } query += " ORDER BY id ASC"; 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 ResultAsync.fromPromise( this.doSaveEvents(events), (error) => new StorageError( `Failed to save events: ${error instanceof Error ? error.message : "Unknown error"}`, "SAVE_FAILED", error instanceof Error ? error : void 0 ) ); if (result.isErr()) { throw result.error; } } async doSaveEvents(events) { let client = null; try { client = await this.pool.connect(); await client.query("BEGIN"); try { for (const event of events) { 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 || /* @__PURE__ */ 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; } } await client.query("COMMIT"); } catch (error) { await client.query("ROLLBACK"); throw error; } } finally { if (client) { client.release(); } } } /** * Close the storage provider */ close() { return ResultAsync.fromPromise( this.pool.end(), (error) => new StorageError( `Failed to close PostgreSQL connection: ${error instanceof Error ? error.message : "Unknown error"}`, "CLOSE_FAILED", error instanceof Error ? error : void 0 ) ); } }; // src/postgres-storage-provider.ts import { Pool } from "pg"; import { ResultAsync as ResultAsync2, errAsync as errAsync2 } from "neverthrow"; import { StorageError as StorageError2, ConnectionError as ConnectionError2, EventStoreFactory } from "@sekiban/core"; function createPostgresEventStore(config) { if (!config.connectionString) { return errAsync2(new StorageError2("Connection string is required for PostgreSQL provider", "INVALID_CONFIG")); } return ResultAsync2.fromPromise( (async () => { try { const pool = new Pool({ connectionString: config.connectionString, max: 10, // Maximum number of clients in the pool idleTimeoutMillis: 3e4, // How long a client is allowed to remain idle connectionTimeoutMillis: config.timeoutMs || 2e3 }); const client = await pool.connect(); client.release(); const eventStore = new PostgresEventStore(pool); await eventStore.initialize(); return eventStore; } catch (error) { throw new ConnectionError2( `Failed to create PostgreSQL event store: ${error instanceof Error ? error.message : "Unknown error"}`, error instanceof Error ? error : void 0 ); } })(), (error) => error instanceof StorageError2 ? error : new ConnectionError2( `Failed to create PostgreSQL event store: ${error instanceof Error ? error.message : "Unknown error"}`, error instanceof Error ? error : void 0 ) ); } if (typeof EventStoreFactory !== "undefined") { EventStoreFactory.register("PostgreSQL", createPostgresEventStore); } export { PostgresEventStore, createPostgresEventStore }; //# sourceMappingURL=index.mjs.map