feat: add transaction support to postgres provider

This commit is contained in:
2025-10-05 21:51:04 +00:00
parent d55573b2db
commit 737d22ecf5
6 changed files with 97 additions and 68 deletions

View File

@@ -1,7 +1,7 @@
import type { Helper } from "postgres";
import type { Relation, RelationPayload, RelationsProvider } from "../../../types/adapter.ts";
import type { PostgresDatabase } from "../database.ts";
import type { Options, PostgresDatabase } from "../database.ts";
export class PostgresRelationsProvider implements RelationsProvider {
constructor(
@@ -34,8 +34,9 @@ export class PostgresRelationsProvider implements RelationsProvider {
* @param key - Relational key to add stream to.
* @param stream - Stream to add to the key.
*/
async insert(key: string, stream: string): Promise<void> {
await this.db.sql`INSERT INTO ${this.table} (key, stream) VALUES (${key}, ${stream}) ON CONFLICT DO NOTHING`.catch(
async insert(key: string, stream: string, { tx }: Options = {}): Promise<void> {
await (tx ??
this.db.sql)`INSERT INTO ${this.table} (key, stream) VALUES (${key}, ${stream}) ON CONFLICT DO NOTHING`.catch(
(error) => {
throw new Error(`EventStore > 'relations.insert' failed with postgres error: ${error.message}`);
},
@@ -48,17 +49,24 @@ export class PostgresRelationsProvider implements RelationsProvider {
* @param relations - Relations to insert.
* @param batchSize - Batch size for the insert loop.
*/
async insertMany(relations: RelationPayload[], batchSize: number = 1_000): Promise<void> {
await this.db.sql
.begin(async (sql) => {
for (let i = 0; i < relations.length; i += batchSize) {
const values = relations.slice(i, i + batchSize).map(({ key, stream }) => [key, stream]);
await sql`INSERT INTO ${this.table} (key, stream) VALUES ${sql(values)} ON CONFLICT DO NOTHING`;
}
})
.catch((error) => {
throw new Error(`EventStore > 'relations.insertMany' failed with postgres error: ${error.message}`);
});
async insertMany(relations: RelationPayload[], batchSize: number = 1_000, { tx }: Options = {}): Promise<void> {
if (tx !== undefined) {
for (let i = 0; i < relations.length; i += batchSize) {
const values = relations.slice(i, i + batchSize).map(({ key, stream }) => [key, stream]);
await tx`INSERT INTO ${this.table} (key, stream) VALUES ${this.db.sql(values)} ON CONFLICT DO NOTHING`;
}
} else {
await this.db.sql
.begin(async (sql) => {
for (let i = 0; i < relations.length; i += batchSize) {
const values = relations.slice(i, i + batchSize).map(({ key, stream }) => [key, stream]);
await sql`INSERT INTO ${this.table} (key, stream) VALUES ${sql(values)} ON CONFLICT DO NOTHING`;
}
})
.catch((error) => {
throw new Error(`EventStore > 'relations.insertMany' failed with postgres error: ${error.message}`);
});
}
}
/**
@@ -66,8 +74,8 @@ export class PostgresRelationsProvider implements RelationsProvider {
*
* @param key - Relational key to get event streams for.
*/
async getByKey(key: string): Promise<string[]> {
return this.db.sql`SELECT stream FROM ${this.table} WHERE key = ${key}`
async getByKey(key: string, { tx }: Options = {}): Promise<string[]> {
return (tx ?? this.db.sql)`SELECT stream FROM ${this.table} WHERE key = ${key}`
.then((rows) => rows.map(({ stream }) => stream))
.catch((error) => {
throw new Error(`EventStore > 'relations.getByKey' failed with postgres error: ${error.message}`);
@@ -79,8 +87,8 @@ export class PostgresRelationsProvider implements RelationsProvider {
*
* @param keys - Relational keys to get event streams for.
*/
async getByKeys(keys: string[]): Promise<string[]> {
return this.db.sql`SELECT DISTINCT stream FROM ${this.table} WHERE key IN ${this.db.sql(keys)}`
async getByKeys(keys: string[], { tx }: Options = {}): Promise<string[]> {
return (tx ?? this.db.sql)`SELECT DISTINCT stream FROM ${this.table} WHERE key IN ${this.db.sql(keys)}`
.then((rows) => rows.map(({ stream }) => stream))
.catch((error) => {
throw new Error(`EventStore > 'relations.getByKeys' failed with postgres error: ${error.message}`);
@@ -93,8 +101,8 @@ export class PostgresRelationsProvider implements RelationsProvider {
* @param key - Relational key to remove stream from.
* @param stream - Stream to remove from relation.
*/
async remove(key: string, stream: string): Promise<void> {
await this.db.sql`DELETE FROM ${this.table} WHERE key = ${key} AND stream = ${stream}`.catch((error) => {
async remove(key: string, stream: string, { tx }: Options = {}): Promise<void> {
await (tx ?? this.db.sql)`DELETE FROM ${this.table} WHERE key = ${key} AND stream = ${stream}`.catch((error) => {
throw new Error(`EventStore > 'relations.remove' failed with postgres error: ${error.message}`);
});
}
@@ -105,19 +113,28 @@ export class PostgresRelationsProvider implements RelationsProvider {
* @param relations - Relations to remove stream from.
* @param batchSize - Batch size for the insert loop.
*/
async removeMany(relations: RelationPayload[], batchSize: number = 1_000): Promise<void> {
await this.db.sql
.begin(async (sql) => {
for (let i = 0; i < relations.length; i += batchSize) {
const conditions = relations
.slice(i, i + batchSize)
.map(({ key, stream }) => `(key = '${key}' AND stream = '${stream}')`);
await sql`DELETE FROM ${this.table} WHERE ${this.db.sql.unsafe(conditions.join(" OR "))}`;
}
})
.catch((error) => {
throw new Error(`EventStore > 'relations.removeMany' failed with postgres error: ${error.message}`);
});
async removeMany(relations: RelationPayload[], batchSize: number = 1_000, { tx }: Options = {}): Promise<void> {
if (tx !== undefined) {
for (let i = 0; i < relations.length; i += batchSize) {
const conditions = relations
.slice(i, i + batchSize)
.map(({ key, stream }) => `(key = '${key}' AND stream = '${stream}')`);
await tx`DELETE FROM ${this.table} WHERE ${this.db.sql.unsafe(conditions.join(" OR "))}`;
}
} else {
await this.db.sql
.begin(async (sql) => {
for (let i = 0; i < relations.length; i += batchSize) {
const conditions = relations
.slice(i, i + batchSize)
.map(({ key, stream }) => `(key = '${key}' AND stream = '${stream}')`);
await sql`DELETE FROM ${this.table} WHERE ${this.db.sql.unsafe(conditions.join(" OR "))}`;
}
})
.catch((error) => {
throw new Error(`EventStore > 'relations.removeMany' failed with postgres error: ${error.message}`);
});
}
}
/**
@@ -125,8 +142,8 @@ export class PostgresRelationsProvider implements RelationsProvider {
*
* @param keys - Relational keys to remove from the relational table.
*/
async removeByKeys(keys: string[]): Promise<void> {
await this.db.sql`DELETE FROM ${this.table} WHERE key IN ${this.db.sql(keys)}`.catch((error) => {
async removeByKeys(keys: string[], { tx }: Options = {}): Promise<void> {
await (tx ?? this.db.sql)`DELETE FROM ${this.table} WHERE key IN ${this.db.sql(keys)}`.catch((error) => {
throw new Error(`EventStore > 'relations.removeByKeys' failed with postgres error: ${error.message}`);
});
}
@@ -136,8 +153,8 @@ export class PostgresRelationsProvider implements RelationsProvider {
*
* @param streams - Streams to remove from the relational table.
*/
async removeByStreams(streams: string[]): Promise<void> {
await this.db.sql`DELETE FROM ${this.table} WHERE stream IN ${this.db.sql(streams)}`.catch((error) => {
async removeByStreams(streams: string[], { tx }: Options = {}): Promise<void> {
await (tx ?? this.db.sql)`DELETE FROM ${this.table} WHERE stream IN ${this.db.sql(streams)}`.catch((error) => {
throw new Error(`EventStore > 'relations.removeByStreams' failed with postgres error: ${error.message}`);
});
}