@sekiban/postgres
Version:
PostgreSQL storage provider for Sekiban Event Sourcing framework
264 lines (263 loc) • 9.54 kB
JavaScript
// 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