UNPKG

@sekiban/postgres

Version:

PostgreSQL storage provider for Sekiban Event Sourcing framework

1 lines 16.7 kB
{"version":3,"sources":["../src/postgres-event-store.ts","../src/postgres-storage-provider.ts"],"sourcesContent":["import { Pool, PoolClient } from 'pg';\nimport { ResultAsync, okAsync, errAsync } from 'neverthrow';\nimport {\n IEvent,\n IEventReader,\n IEventWriter,\n IEventStore,\n EventRetrievalInfo,\n StorageError,\n ConnectionError,\n SortableUniqueId\n} from '@sekiban/core';\n\n/**\n * PostgreSQL implementation of IEventStore\n * Implements both IEventReader and IEventWriter interfaces\n */\nexport class PostgresEventStore implements IEventStore {\n constructor(private pool: Pool) {}\n\n /**\n * Initialize the storage provider\n */\n initialize(): ResultAsync<void, StorageError> {\n return ResultAsync.fromPromise(\n (async () => {\n try {\n // Create events table if it doesn't exist (matching C# DbEvent structure)\n await this.pool.query(`\n CREATE TABLE IF NOT EXISTS events (\n id UUID PRIMARY KEY,\n payload JSON NOT NULL,\n sortable_unique_id VARCHAR(255) NOT NULL,\n version INTEGER NOT NULL,\n aggregate_id UUID NOT NULL,\n root_partition_key VARCHAR(255) NOT NULL,\n \"timestamp\" TIMESTAMP NOT NULL,\n partition_key VARCHAR(255) NOT NULL,\n aggregate_group VARCHAR(255) NOT NULL,\n payload_type_name VARCHAR(255) NOT NULL,\n causation_id VARCHAR(255) NOT NULL DEFAULT '',\n correlation_id VARCHAR(255) NOT NULL DEFAULT '',\n executed_user VARCHAR(255) NOT NULL DEFAULT ''\n )\n `);\n\n // Create indexes for efficient querying\n await this.pool.query(`\n CREATE INDEX IF NOT EXISTS idx_events_partition_key \n ON events(partition_key)\n `);\n \n await this.pool.query(`\n CREATE INDEX IF NOT EXISTS idx_events_root_partition \n ON events(root_partition_key)\n `);\n \n await this.pool.query(`\n CREATE INDEX IF NOT EXISTS idx_events_aggregate \n ON events(aggregate_group, aggregate_id)\n `);\n \n await this.pool.query(`\n CREATE INDEX IF NOT EXISTS idx_events_timestamp \n ON events(\"timestamp\")\n `);\n \n await this.pool.query(`\n CREATE INDEX IF NOT EXISTS idx_events_sortable_unique_id \n ON events(sortable_unique_id)\n `);\n } catch (error) {\n throw new ConnectionError(\n `Failed to initialize PostgreSQL: ${error instanceof Error ? error.message : 'Unknown error'}`,\n error instanceof Error ? error : undefined\n );\n }\n })(),\n (error) => error instanceof StorageError ? error : new ConnectionError(\n `Failed to initialize PostgreSQL: ${error instanceof Error ? error.message : 'Unknown error'}`,\n error instanceof Error ? error : undefined\n )\n );\n }\n\n /**\n * Get events based on retrieval information\n */\n getEvents(eventRetrievalInfo: EventRetrievalInfo): ResultAsync<readonly IEvent[], Error> {\n return ResultAsync.fromPromise(\n this.doGetEvents(eventRetrievalInfo),\n (error) => new StorageError(\n `Failed to query events: ${error instanceof Error ? error.message : 'Unknown error'}`,\n 'QUERY_FAILED',\n error instanceof Error ? error : undefined\n )\n );\n }\n\n private async doGetEvents(eventRetrievalInfo: EventRetrievalInfo): Promise<readonly IEvent[]> {\n const { query, params } = this.buildQuery(eventRetrievalInfo);\n const result = await this.pool.query(query, params);\n \n // Parse events from JSON payload (matching C# structure)\n let events = result.rows.map(row => JSON.parse(row.payload) as IEvent);\n \n // Apply sortable ID conditions in memory since PostgreSQL doesn't understand our custom ID format\n if (eventRetrievalInfo.sortableIdCondition) {\n events = events.filter(e => \n !eventRetrievalInfo.sortableIdCondition.outsideOfRange(e.id)\n );\n }\n\n // Sort by sortable ID\n events.sort((a, b) => SortableUniqueId.compare(a.id, b.id));\n\n return events;\n }\n\n private buildQuery(eventRetrievalInfo: EventRetrievalInfo): { query: string; params: any[] } {\n const conditions: string[] = [];\n const params: any[] = [];\n let paramIndex = 1;\n\n // Base query - select all columns to match C# structure\n let query = 'SELECT * FROM events';\n\n // Filter by root partition key\n if (eventRetrievalInfo.rootPartitionKey.hasValueProperty) {\n conditions.push(`root_partition_key = $${paramIndex++}`);\n params.push(eventRetrievalInfo.rootPartitionKey.getValue());\n }\n\n // Filter by aggregate stream (group)\n if (eventRetrievalInfo.aggregateStream.hasValueProperty) {\n const streamNames = eventRetrievalInfo.aggregateStream.getValue().getStreamNames();\n if (streamNames.length === 1) {\n conditions.push(`aggregate_group = $${paramIndex++}`);\n params.push(streamNames[0]);\n } else if (streamNames.length > 1) {\n const placeholders = streamNames.map(() => `$${paramIndex++}`).join(', ');\n conditions.push(`aggregate_group IN (${placeholders})`);\n params.push(...streamNames);\n }\n }\n\n // Filter by aggregate ID\n if (eventRetrievalInfo.aggregateId.hasValueProperty) {\n conditions.push(`aggregate_id = $${paramIndex++}`);\n params.push(eventRetrievalInfo.aggregateId.getValue());\n }\n\n // Add WHERE clause if there are conditions\n if (conditions.length > 0) {\n query += ' WHERE ' + conditions.join(' AND ');\n }\n\n // Order by ID (which is sortable)\n query += ' ORDER BY id ASC';\n\n // Add LIMIT if specified\n if (eventRetrievalInfo.maxCount.hasValueProperty) {\n query += ` LIMIT $${paramIndex++}`;\n params.push(eventRetrievalInfo.maxCount.getValue());\n }\n\n return { query, params };\n }\n\n /**\n * Save events to storage\n */\n async saveEvents<TEvent extends IEvent>(events: TEvent[]): Promise<void> {\n const result = await ResultAsync.fromPromise(\n this.doSaveEvents(events),\n (error) => new StorageError(\n `Failed to save events: ${error instanceof Error ? error.message : 'Unknown error'}`,\n 'SAVE_FAILED',\n error instanceof Error ? error : undefined\n )\n );\n \n if (result.isErr()) {\n throw result.error;\n }\n }\n \n private async doSaveEvents<TEvent extends IEvent>(events: TEvent[]): Promise<void> {\n let client: PoolClient | null = null;\n \n try {\n client = await this.pool.connect();\n \n // Start transaction\n await client.query('BEGIN');\n \n try {\n // Insert each event (matching C# DbEvent structure)\n for (const event of events) {\n // Extract metadata with defaults\n const metadata = event.metadata;\n const causationId = metadata.causationId || '';\n const correlationId = metadata.correlationId || '';\n const executedUser = metadata.executedUser || '';\n \n const insertQuery = `INSERT INTO events (\n id, payload, sortable_unique_id,\n version, aggregate_id, root_partition_key,\n \"timestamp\", partition_key, aggregate_group,\n payload_type_name, causation_id, correlation_id,\n executed_user\n ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)`;\n \n const insertParams = [\n event.id, // id should already be a UUID\n JSON.stringify(event.payload || event.eventData || event), // Store the event data as payload\n typeof event.sortableUniqueId === 'string' ? event.sortableUniqueId : event.sortableUniqueId?.value || event.sortableUniqueId?.toString(),\n event.version,\n event.aggregateId,\n event.partitionKeys?.rootPartitionKey || 'default',\n event.timestamp || new Date(),\n event.partitionKeys?.partitionKey || event.partitionKey,\n event.partitionKeys?.group || event.aggregateGroup || 'default',\n event.eventType, // payload_type_name\n causationId,\n correlationId,\n executedUser\n ];\n \n try {\n await client.query(insertQuery, insertParams);\n } catch (queryError: any) {\n console.error('SQL Error executing INSERT:', queryError.message);\n console.error('SQL Error Code:', queryError.code);\n console.error('SQL Error Position:', queryError.position);\n console.error('SQL Error Detail:', queryError.detail);\n throw queryError;\n }\n }\n \n // Commit transaction\n await client.query('COMMIT');\n } catch (error) {\n // Rollback on error\n await client.query('ROLLBACK');\n throw error;\n }\n } finally {\n if (client) {\n client.release();\n }\n }\n }\n\n /**\n * Close the storage provider\n */\n close(): ResultAsync<void, StorageError> {\n return ResultAsync.fromPromise(\n this.pool.end(),\n (error) => new StorageError(\n `Failed to close PostgreSQL connection: ${error instanceof Error ? error.message : 'Unknown error'}`,\n 'CLOSE_FAILED',\n error instanceof Error ? error : undefined\n )\n );\n }\n}","import { Pool } from 'pg';\nimport { ResultAsync, okAsync, errAsync } from 'neverthrow';\nimport {\n IEventStore,\n StorageProviderConfig,\n StorageError,\n ConnectionError,\n EventStoreFactory\n} from '@sekiban/core';\nimport { PostgresEventStore } from './postgres-event-store';\n\n/**\n * Creates a PostgreSQL event store\n */\nexport function createPostgresEventStore(config: StorageProviderConfig): ResultAsync<IEventStore, StorageError> {\n if (!config.connectionString) {\n return errAsync(new StorageError('Connection string is required for PostgreSQL provider', 'INVALID_CONFIG'));\n }\n\n return ResultAsync.fromPromise(\n (async () => {\n try {\n // Create PostgreSQL pool\n const pool = new Pool({\n connectionString: config.connectionString,\n max: 10, // Maximum number of clients in the pool\n idleTimeoutMillis: 30000, // How long a client is allowed to remain idle\n connectionTimeoutMillis: config.timeoutMs || 2000\n });\n\n // Test the connection\n const client = await pool.connect();\n client.release();\n\n // Create event store\n const eventStore = new PostgresEventStore(pool);\n\n // Initialize the event store\n await eventStore.initialize();\n\n return eventStore;\n } catch (error) {\n throw new ConnectionError(\n `Failed to create PostgreSQL event store: ${error instanceof Error ? error.message : 'Unknown error'}`,\n error instanceof Error ? error : undefined\n );\n }\n })(),\n (error) => error instanceof StorageError ? error : new ConnectionError(\n `Failed to create PostgreSQL event store: ${error instanceof Error ? error.message : 'Unknown error'}`,\n error instanceof Error ? error : undefined\n )\n );\n}\n\n// Register the PostgreSQL provider with the factory\nif (typeof EventStoreFactory !== 'undefined') {\n EventStoreFactory.register('PostgreSQL', createPostgresEventStore);\n}"],"mappings":"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","names":["ResultAsync","errAsync","StorageError","ConnectionError","errAsync","StorageError","ResultAsync","ConnectionError"]}