feat: version 2 beta
This commit is contained in:
175
adapters/postgres/providers/event.ts
Normal file
175
adapters/postgres/providers/event.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
import type { Helper } from "postgres";
|
||||
|
||||
import type { EventRecord } from "../../../libraries/event.ts";
|
||||
import type { EventsProvider } from "../../../types/adapter.ts";
|
||||
import type { EventReadOptions } from "../../../types/query.ts";
|
||||
import type { PostgresDatabase } from "../database.ts";
|
||||
|
||||
type PGEventRecord = Omit<EventRecord, "data" | "meta"> & { data: string; meta: string };
|
||||
|
||||
export class PostgresEventsProvider implements EventsProvider {
|
||||
constructor(
|
||||
readonly db: PostgresDatabase,
|
||||
readonly schema?: string,
|
||||
) {}
|
||||
|
||||
get table(): Helper<string, []> {
|
||||
if (this.schema !== undefined) {
|
||||
return this.db.sql(`${this.schema}.events`);
|
||||
}
|
||||
return this.db.sql("public.events");
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert a new event record to the events table.
|
||||
*
|
||||
* @param record - Event record to insert.
|
||||
*/
|
||||
async insert(record: EventRecord): Promise<void> {
|
||||
await this.db.sql`INSERT INTO ${this.table} ${this.db.sql(this.#toDriver(record))}`.catch((error) => {
|
||||
throw new Error(`EventStore > 'events.insert' failed with postgres error: ${error.message}`);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert many new event records to the events table.
|
||||
*
|
||||
* @param records - Event records to insert.
|
||||
* @param batchSize - Batch size for the insert loop.
|
||||
*/
|
||||
async insertMany(records: EventRecord[], batchSize: number = 1_000): Promise<void> {
|
||||
await this.db.sql
|
||||
.begin(async (sql) => {
|
||||
for (let i = 0; i < records.length; i += batchSize) {
|
||||
await sql`INSERT INTO ${this.table} ${this.db.sql(records.slice(i, i + batchSize).map(this.#toDriver))}`;
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
throw new Error(`EventStore > 'events.insertMany' failed with postgres error: ${error.message}`);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve all the events in the events table. Optionally a cursor and direction
|
||||
* can be provided to reduce the list of events returned.
|
||||
*
|
||||
* @param options - Find options.
|
||||
*/
|
||||
async get(options: EventReadOptions): Promise<EventRecord[]> {
|
||||
if (options !== undefined) {
|
||||
const { filter, cursor, direction, limit } = options;
|
||||
return this.db.sql<PGEventRecord[]>`
|
||||
SELECT * FROM ${this.table}
|
||||
WHERE
|
||||
${filter?.types ? this.#withTypes(filter.types) : this.db.sql``}
|
||||
${cursor ? this.#withCursor(cursor, direction) : this.db.sql``}
|
||||
ORDER BY created ASC
|
||||
${limit ? this.#withLimit(limit) : this.db.sql``}
|
||||
`.then(this.#fromDriver);
|
||||
}
|
||||
return this.db.sql<PGEventRecord[]>`SELECT * FROM ${this.table} ORDER BY created ASC`.then(this.#fromDriver);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get events within the given stream.
|
||||
*
|
||||
* @param stream - Stream to fetch events for.
|
||||
* @param options - Read options for modifying the result.
|
||||
*/
|
||||
async getByStream(stream: string, { filter, cursor, direction, limit }: EventReadOptions = {}): Promise<EventRecord[]> {
|
||||
return this.db.sql<PGEventRecord[]>`
|
||||
SELECT * FROM ${this.table}
|
||||
WHERE
|
||||
stream = ${stream}
|
||||
${filter?.types ? this.#withTypes(filter.types) : this.db.sql``}
|
||||
${cursor ? this.#withCursor(cursor, direction) : this.db.sql``}
|
||||
ORDER BY created ASC
|
||||
${limit ? this.#withLimit(limit) : this.db.sql``}
|
||||
`.then(this.#fromDriver);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get events within given list of streams.
|
||||
*
|
||||
* @param streams - Stream to get events for.
|
||||
* @param options - Read options for modifying the result.
|
||||
*/
|
||||
async getByStreams(streams: string[], { filter, cursor, direction, limit }: EventReadOptions = {}): Promise<EventRecord[]> {
|
||||
return this.db.sql<PGEventRecord[]>`
|
||||
SELECT * FROM ${this.table}
|
||||
WHERE
|
||||
stream IN ${this.db.sql(streams)}
|
||||
${filter?.types ? this.#withTypes(filter.types) : this.db.sql``}
|
||||
${cursor ? this.#withCursor(cursor, direction) : this.db.sql``}
|
||||
ORDER BY created ASC
|
||||
${limit ? this.#withLimit(limit) : this.db.sql``}
|
||||
`.then(this.#fromDriver);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single event by its id.
|
||||
*
|
||||
* @param id - Event id.
|
||||
*/
|
||||
async getById(id: string): Promise<EventRecord | undefined> {
|
||||
return this.db.sql<PGEventRecord[]>`SELECT * FROM ${this.table} WHERE id = ${id}`.then(this.#fromDriver).then(([record]) => record);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given event is outdated in relation to the local event data.
|
||||
*/
|
||||
async checkOutdated({ stream, type, created }: EventRecord): Promise<boolean> {
|
||||
const count = await await this.db.sql`
|
||||
SELECT COUNT(*) AS count
|
||||
FROM ${this.table}
|
||||
WHERE
|
||||
stream = ${stream}
|
||||
AND type = ${type}
|
||||
AND created > ${created}
|
||||
`.then((result: any) => Number(result[0]));
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------------
|
||||
| Utilities
|
||||
|--------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
#withTypes(types: string[]) {
|
||||
return this.db.sql`AND type IN ${this.db.sql(types)}`;
|
||||
}
|
||||
|
||||
#withCursor(cursor: string, direction?: 1 | -1 | "asc" | "desc") {
|
||||
if (direction === "desc" || direction === -1) {
|
||||
return this.db.sql`AND created < ${cursor}`;
|
||||
}
|
||||
return this.db.sql`AND created > ${cursor}`;
|
||||
}
|
||||
|
||||
#withLimit(limit: number) {
|
||||
return this.db.sql`LIMIT ${limit}`;
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------------
|
||||
| Parsers
|
||||
|--------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
#fromDriver(records: PGEventRecord[]): EventRecord[] {
|
||||
return records.map((record) => {
|
||||
record.data = typeof record.data === "string" ? JSON.parse(record.data) : record.data;
|
||||
record.meta = typeof record.meta === "string" ? JSON.parse(record.meta) : record.meta;
|
||||
return record as unknown as EventRecord;
|
||||
});
|
||||
}
|
||||
|
||||
#toDriver(record: EventRecord): PGEventRecord {
|
||||
return {
|
||||
...record,
|
||||
data: JSON.stringify(record.data),
|
||||
meta: JSON.stringify(record.meta),
|
||||
};
|
||||
}
|
||||
}
|
||||
140
adapters/postgres/providers/relations.ts
Normal file
140
adapters/postgres/providers/relations.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import type { Helper } from "postgres";
|
||||
|
||||
import type { Relation, RelationPayload, RelationsProvider } from "../../../types/adapter.ts";
|
||||
import type { PostgresDatabase } from "../database.ts";
|
||||
|
||||
export class PostgresRelationsProvider implements RelationsProvider {
|
||||
constructor(
|
||||
readonly db: PostgresDatabase,
|
||||
readonly schema?: string,
|
||||
) {}
|
||||
|
||||
get table(): Helper<string, []> {
|
||||
if (this.schema !== undefined) {
|
||||
return this.db.sql(`${this.schema}.relations`);
|
||||
}
|
||||
return this.db.sql("public.relations");
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle incoming relation operations.
|
||||
*
|
||||
* @param relations - List of relation operations to execute.
|
||||
*/
|
||||
async handle(relations: Relation[]): Promise<void> {
|
||||
await Promise.all([
|
||||
this.insertMany(relations.filter((relation) => relation.op === "insert")),
|
||||
this.removeMany(relations.filter((relation) => relation.op === "remove")),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add stream to the relations table.
|
||||
*
|
||||
* @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((error) => {
|
||||
throw new Error(`EventStore > 'relations.insert' failed with postgres error: ${error.message}`);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add stream to many relational keys onto the relations table.
|
||||
*
|
||||
* @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}`);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of event streams registered under the given relational key.
|
||||
*
|
||||
* @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}`
|
||||
.then((rows) => rows.map(({ stream }) => stream))
|
||||
.catch((error) => {
|
||||
throw new Error(`EventStore > 'relations.getByKey' failed with postgres error: ${error.message}`);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of event streams registered under the given relational keys.
|
||||
*
|
||||
* @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)}`
|
||||
.then((rows) => rows.map(({ stream }) => stream))
|
||||
.catch((error) => {
|
||||
throw new Error(`EventStore > 'relations.getByKeys' failed with postgres error: ${error.message}`);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a stream from the relational table.
|
||||
*
|
||||
* @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) => {
|
||||
throw new Error(`EventStore > 'relations.remove' failed with postgres error: ${error.message}`);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes multiple relational entries.
|
||||
*
|
||||
* @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}`);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all relations bound to the given relational keys.
|
||||
*
|
||||
* @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) => {
|
||||
throw new Error(`EventStore > 'relations.removeByKeys' failed with postgres error: ${error.message}`);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all relations bound to the given streams.
|
||||
*
|
||||
* @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) => {
|
||||
throw new Error(`EventStore > 'relations.removeByStreams' failed with postgres error: ${error.message}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
82
adapters/postgres/providers/snapshot.ts
Normal file
82
adapters/postgres/providers/snapshot.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import type { Helper } from "postgres";
|
||||
|
||||
import type { Snapshot, SnapshotsProvider } from "../../../types/adapter.ts";
|
||||
import type { PostgresDatabase } from "../database.ts";
|
||||
|
||||
type PGSnapshot = Omit<Snapshot, "state"> & { state: string };
|
||||
|
||||
export class PostgresSnapshotsProvider implements SnapshotsProvider {
|
||||
constructor(
|
||||
readonly db: PostgresDatabase,
|
||||
readonly schema?: string,
|
||||
) {}
|
||||
|
||||
get table(): Helper<string, []> {
|
||||
if (this.schema !== undefined) {
|
||||
return this.db.sql(`${this.schema}.snapshots`);
|
||||
}
|
||||
return this.db.sql("public.snapshots");
|
||||
}
|
||||
|
||||
/**
|
||||
* Add snapshot state under given reducer stream.
|
||||
*
|
||||
* @param name - Name of the reducer the snapshot is attached to.
|
||||
* @param stream - Stream the snapshot is attached to.
|
||||
* @param cursor - Cursor timestamp for the last event used in the snapshot.
|
||||
* @param state - State of the reduced events.
|
||||
*/
|
||||
async insert(name: string, stream: string, cursor: string, state: any): Promise<void> {
|
||||
await this.db.sql`
|
||||
INSERT INTO ${this.table} ${this.db.sql(this.#toDriver({ name, stream, cursor, state }))}`.catch((error) => {
|
||||
throw new Error(`EventStore > 'snapshots.insert' failed with postgres error: ${error.message}`);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get snapshot state by stream.
|
||||
*
|
||||
* @param name - Name of the reducer which the state was created.
|
||||
* @param stream - Stream the state was reduced for.
|
||||
*/
|
||||
async getByStream(name: string, stream: string): Promise<Snapshot | undefined> {
|
||||
return this.db.sql<PGSnapshot[]>`SELECT * FROM ${this.table} WHERE name = ${name} AND stream = ${stream}`
|
||||
.then(this.#fromDriver)
|
||||
.then(([snapshot]) => snapshot)
|
||||
.catch((error) => {
|
||||
throw new Error(`EventStore > 'snapshots.getByStream' failed with postgres error: ${error.message}`);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a snapshot for the given reducer stream.
|
||||
*
|
||||
* @param name - Name of the reducer the snapshot is attached to.
|
||||
* @param stream - Stream to remove from snapshots.
|
||||
*/
|
||||
async remove(name: string, stream: string): Promise<void> {
|
||||
await this.db.sql`DELETE FROM ${this.table} WHERE name = ${name} AND stream = ${stream}`.catch((error) => {
|
||||
throw new Error(`EventStore > 'snapshots.remove' failed with postgres error: ${error.message}`);
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------------
|
||||
| Parsers
|
||||
|--------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
#fromDriver(snapshots: PGSnapshot[]): Snapshot[] {
|
||||
return snapshots.map((snapshot) => {
|
||||
snapshot.state = typeof snapshot.state === "string" ? JSON.parse(snapshot.state) : snapshot.state;
|
||||
return snapshot as unknown as Snapshot;
|
||||
});
|
||||
}
|
||||
|
||||
#toDriver(snapshot: Snapshot): object {
|
||||
return {
|
||||
...snapshot,
|
||||
state: JSON.stringify(snapshot.state),
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user