From 3882298807782c5e1cbacb03663b11d7119a4112 Mon Sep 17 00:00:00 2001 From: Oskar Dudycz Date: Tue, 30 Jul 2024 12:03:41 +0200 Subject: [PATCH 01/30] Added initial types definition for Pongo Sessions and transactions --- src/packages/pongo/src/main/index.ts | 1 + src/packages/pongo/src/main/pongoClient.ts | 11 +++++- src/packages/pongo/src/main/pongoSession.ts | 5 +++ .../pongo/src/main/typing/operations.ts | 37 +++++++++++++++++++ src/packages/pongo/src/mongo/mongoClient.ts | 20 +++++++++- 5 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 src/packages/pongo/src/main/pongoSession.ts diff --git a/src/packages/pongo/src/main/index.ts b/src/packages/pongo/src/main/index.ts index a88ac3a9..c269caf9 100644 --- a/src/packages/pongo/src/main/index.ts +++ b/src/packages/pongo/src/main/index.ts @@ -1,3 +1,4 @@ export * from './dbClient'; export * from './pongoClient'; +export * from './pongoSession'; export * from './typing'; diff --git a/src/packages/pongo/src/main/pongoClient.ts b/src/packages/pongo/src/main/pongoClient.ts index 6fab983b..ebe6d3a5 100644 --- a/src/packages/pongo/src/main/pongoClient.ts +++ b/src/packages/pongo/src/main/pongoClient.ts @@ -1,7 +1,7 @@ import { getDatabaseNameOrDefault } from '@event-driven-io/dumbo'; import pg from 'pg'; import { getDbClient, type DbClient } from './dbClient'; -import type { PongoClient, PongoDb } from './typing/operations'; +import type { PongoClient, PongoDb, PongoSession } from './typing/operations'; export const pongoClient = ( connectionString: string, @@ -40,6 +40,15 @@ export const pongoClient = ( .get(dbName)! ); }, + + startSession(): PongoSession { + throw new Error('Not Implemented!'); + }, + withSession( + _callback: (session: PongoSession) => Promise, + ): Promise { + return Promise.reject('Not Implemented!'); + }, }; return pongoClient; diff --git a/src/packages/pongo/src/main/pongoSession.ts b/src/packages/pongo/src/main/pongoSession.ts new file mode 100644 index 00000000..93a874f3 --- /dev/null +++ b/src/packages/pongo/src/main/pongoSession.ts @@ -0,0 +1,5 @@ +import type { PongoSession } from './typing'; + +export const pongoSession = (): PongoSession => { + throw new Error('Not Implemented!'); +}; diff --git a/src/packages/pongo/src/main/typing/operations.ts b/src/packages/pongo/src/main/typing/operations.ts index 85c0a864..6686d2bb 100644 --- a/src/packages/pongo/src/main/typing/operations.ts +++ b/src/packages/pongo/src/main/typing/operations.ts @@ -4,6 +4,43 @@ export interface PongoClient { close(): Promise; db(dbName?: string): PongoDb; + + startSession(): PongoSession; + + withSession( + callback: (session: PongoSession) => Promise, + ): Promise; +} + +export declare interface TransactionOptions { + get snapshotEnabled(): boolean; + maxCommitTimeMS?: number; +} + +export interface Transaction { + options: TransactionOptions; + get isStarting(): boolean; + get isActive(): boolean; + get isCommitted(): boolean; +} + +export interface PongoSession { + hasEnded: boolean; + explicit: boolean; + defaultTransactionOptions: TransactionOptions; + transaction: Transaction; + get snapshotEnabled(): boolean; + + endSession(): Promise; + incrementTransactionNumber(): void; + inTransaction(): boolean; + startTransaction(options?: TransactionOptions): void; + commitTransaction(): Promise; + abortTransaction(): Promise; + withTransaction( + fn: (session: PongoSession) => Promise, + options?: TransactionOptions, + ): Promise; } export interface PongoDb { diff --git a/src/packages/pongo/src/mongo/mongoClient.ts b/src/packages/pongo/src/mongo/mongoClient.ts index 28049b69..6ca80e1f 100644 --- a/src/packages/pongo/src/mongo/mongoClient.ts +++ b/src/packages/pongo/src/mongo/mongoClient.ts @@ -1,4 +1,5 @@ -// src/MongoClientShim.ts +import type { ClientSessionOptions } from 'http2'; +import type { ClientSession, WithSessionCallback } from 'mongodb'; import pg from 'pg'; import { pongoClient, type PongoClient } from '../main'; import { Db } from './mongoDb'; @@ -25,4 +26,21 @@ export class MongoClient { db(dbName?: string): Db { return new Db(this.pongoClient.db(dbName)); } + startSession(_options?: ClientSessionOptions): ClientSession { + throw new Error('Not implemented!'); + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + withSession(_executor: WithSessionCallback): Promise; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + withSession( + _options: ClientSessionOptions, + _executor: WithSessionCallback, + ): Promise; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + withSession( + _optionsOrExecutor: ClientSessionOptions | WithSessionCallback, + _executor?: WithSessionCallback, + ): Promise { + return Promise.reject('Not Implemented!'); + } } From 4244779203b238ea5088ae86a03a0853e2b5abb0 Mon Sep 17 00:00:00 2001 From: Oskar Dudycz Date: Tue, 30 Jul 2024 13:31:35 +0200 Subject: [PATCH 02/30] Added typed DbClient options making the first step towards supporting multiple database types --- src/packages/pongo/src/main/dbClient.ts | 34 +++++++++-- src/packages/pongo/src/main/pongoClient.ts | 65 ++++++++++++++++----- src/packages/pongo/src/main/pongoSession.ts | 5 +- src/packages/pongo/src/postgres/client.ts | 26 ++++++--- 4 files changed, 104 insertions(+), 26 deletions(-) diff --git a/src/packages/pongo/src/main/dbClient.ts b/src/packages/pongo/src/main/dbClient.ts index 1bb06afa..ed9e81df 100644 --- a/src/packages/pongo/src/main/dbClient.ts +++ b/src/packages/pongo/src/main/dbClient.ts @@ -1,13 +1,39 @@ -import { postgresClient, type PongoClientOptions } from '../postgres'; +import { + isPostgresClientOptions, + postgresDbClient, + type PostgresDbClientOptions, +} from '../postgres'; import type { PongoCollection, PongoDocument } from './typing/operations'; -export interface DbClient { +export type PongoDbClientOptions< + DbType extends string = string, + Additional = unknown, +> = { + type: DbType; + connectionString: string; + dbName: string | undefined; +} & Additional; + +export interface DbClient< + DbClientOptions extends PongoDbClientOptions = PongoDbClientOptions, +> { + options: DbClientOptions; connect(): Promise; close(): Promise; collection: (name: string) => PongoCollection; } -export const getDbClient = (options: PongoClientOptions): DbClient => { +export type AllowedDbClientOptions = PostgresDbClientOptions; + +export const getDbClient = < + DbClientOptions extends AllowedDbClientOptions = AllowedDbClientOptions, +>( + options: DbClientOptions, +): DbClient => { + const { type } = options; // This is the place where in the future could come resolution of other database types - return postgresClient(options); + if (!isPostgresClientOptions(options)) + throw new Error(`Unsupported db type: ${type}`); + + return postgresDbClient(options) as DbClient; }; diff --git a/src/packages/pongo/src/main/pongoClient.ts b/src/packages/pongo/src/main/pongoClient.ts index ebe6d3a5..c35391fb 100644 --- a/src/packages/pongo/src/main/pongoClient.ts +++ b/src/packages/pongo/src/main/pongoClient.ts @@ -1,18 +1,37 @@ import { getDatabaseNameOrDefault } from '@event-driven-io/dumbo'; import pg from 'pg'; -import { getDbClient, type DbClient } from './dbClient'; +import type { PostgresDbClientOptions } from '../postgres'; +import { + getDbClient, + type AllowedDbClientOptions, + type DbClient, +} from './dbClient'; import type { PongoClient, PongoDb, PongoSession } from './typing/operations'; -export const pongoClient = ( +export type PongoClientOptions = { client?: pg.PoolClient | pg.Client }; + +export const pongoClient = < + DbClientOptions extends AllowedDbClientOptions = AllowedDbClientOptions, +>( connectionString: string, - options: { client?: pg.PoolClient | pg.Client } = {}, + options: PongoClientOptions = {}, ): PongoClient => { const defaultDbName = getDatabaseNameOrDefault(connectionString); - const dbClients: Map = new Map(); + const dbClients: Map> = new Map(); - const dbClient = getDbClient({ connectionString, client: options.client }); + const dbClient = getDbClient( + clientToDbOptions({ + connectionString, + dbName: defaultDbName, + clientOptions: options, + }), + ); dbClients.set(defaultDbName, dbClient); + const startSession = (): PongoSession => { + throw new Error('Not Implemented!'); + }; + const pongoClient: PongoClient = { connect: async () => { await dbClient.connect(); @@ -31,19 +50,18 @@ export const pongoClient = ( dbClients .set( dbName, - getDbClient({ - connectionString, - dbName: dbName, - client: options.client, - }), + getDbClient( + clientToDbOptions({ + connectionString, + dbName: defaultDbName, + clientOptions: options, + }), + ), ) .get(dbName)! ); }, - - startSession(): PongoSession { - throw new Error('Not Implemented!'); - }, + startSession, withSession( _callback: (session: PongoSession) => Promise, ): Promise { @@ -53,3 +71,22 @@ export const pongoClient = ( return pongoClient; }; + +export const clientToDbOptions = < + DbClientOptions extends AllowedDbClientOptions = AllowedDbClientOptions, +>(options: { + connectionString: string; + dbName: string; + clientOptions: PongoClientOptions; +}): DbClientOptions => { + const postgreSQLOptions: PostgresDbClientOptions = { + type: 'PostgreSQL', + connectionString: options.connectionString, + dbName: options.dbName, + ...(options.clientOptions.client + ? { client: options.clientOptions.client } + : {}), + }; + + return postgreSQLOptions as DbClientOptions; +}; diff --git a/src/packages/pongo/src/main/pongoSession.ts b/src/packages/pongo/src/main/pongoSession.ts index 93a874f3..4918d8fd 100644 --- a/src/packages/pongo/src/main/pongoSession.ts +++ b/src/packages/pongo/src/main/pongoSession.ts @@ -1,5 +1,8 @@ +import type { PongoClientOptions } from './pongoClient'; import type { PongoSession } from './typing'; -export const pongoSession = (): PongoSession => { +export const pongoSession = ( + _clientOptions: PongoClientOptions, +): PongoSession => { throw new Error('Not Implemented!'); }; diff --git a/src/packages/pongo/src/postgres/client.ts b/src/packages/pongo/src/postgres/client.ts index bcd40a36..01e17524 100644 --- a/src/packages/pongo/src/postgres/client.ts +++ b/src/packages/pongo/src/postgres/client.ts @@ -4,22 +4,34 @@ import { getPool, } from '@event-driven-io/dumbo'; import pg from 'pg'; -import { type DbClient, type PongoDocument } from '../main'; +import { + type DbClient, + type PongoDbClientOptions, + type PongoDocument, +} from '../main'; import { postgresCollection } from './postgresCollection'; -export type PongoClientOptions = { - connectionString: string; - dbName?: string | undefined; - client?: pg.PoolClient | pg.Client | undefined; -}; +export type PostgresDbClientOptions = PongoDbClientOptions< + 'PostgreSQL', + { + client?: pg.PoolClient | pg.Client | undefined; + } +>; + +export const isPostgresClientOptions = ( + options: PongoDbClientOptions, +): options is PostgresDbClientOptions => options.type === 'PostgreSQL'; -export const postgresClient = (options: PongoClientOptions): DbClient => { +export const postgresDbClient = ( + options: PostgresDbClientOptions, +): DbClient => { const { connectionString, dbName, client } = options; const managesPoolLifetime = !client; const poolOrClient = client ?? getPool({ connectionString, database: dbName }); return { + options, connect: () => Promise.resolve(), close: () => managesPoolLifetime From 1ce277e717a1007326431f745a2933902844d157 Mon Sep 17 00:00:00 2001 From: Oskar Dudycz Date: Wed, 31 Jul 2024 14:14:24 +0200 Subject: [PATCH 03/30] Added abstractions for connection and transaction handling --- src/packages/dumbo/src/connections/client.ts | 19 -- .../dumbo/src/connections/connection.ts | 21 +++ src/packages/dumbo/src/connections/index.ts | 3 +- .../src/connections/pg/connection.int.spec.ts | 93 ++++++++++ .../dumbo/src/connections/pg/connection.ts | 174 ++++++++++++++++++ .../dumbo/src/connections/pg/index.ts | 1 + src/packages/dumbo/src/connections/pool.ts | 2 +- ...ns.e2e.spec.ts => connections.int.spec.ts} | 0 8 files changed, 292 insertions(+), 21 deletions(-) delete mode 100644 src/packages/dumbo/src/connections/client.ts create mode 100644 src/packages/dumbo/src/connections/connection.ts create mode 100644 src/packages/dumbo/src/connections/pg/connection.int.spec.ts create mode 100644 src/packages/dumbo/src/connections/pg/connection.ts create mode 100644 src/packages/dumbo/src/connections/pg/index.ts rename src/packages/dumbo/src/execute/{connections.e2e.spec.ts => connections.int.spec.ts} (100%) diff --git a/src/packages/dumbo/src/connections/client.ts b/src/packages/dumbo/src/connections/client.ts deleted file mode 100644 index f216c6c0..00000000 --- a/src/packages/dumbo/src/connections/client.ts +++ /dev/null @@ -1,19 +0,0 @@ -import pg from 'pg'; -import { endPool, getPool } from './pool'; - -export interface PostgresClient { - connect(): Promise; - close(): Promise; -} - -export const postgresClient = ( - connectionString: string, - database?: string, -): PostgresClient => { - const pool = getPool({ connectionString, database }); - - return { - connect: () => pool.connect(), - close: () => endPool({ connectionString, database }), - }; -}; diff --git a/src/packages/dumbo/src/connections/connection.ts b/src/packages/dumbo/src/connections/connection.ts new file mode 100644 index 00000000..56b9cb5a --- /dev/null +++ b/src/packages/dumbo/src/connections/connection.ts @@ -0,0 +1,21 @@ +export type Transaction< + ConnectorType extends string = string, + DbClient = unknown, +> = { + type: ConnectorType; + client: Promise; + begin: () => Promise; + commit: () => Promise; + rollback: () => Promise; +}; + +export type Connection< + ConnectorType extends string = string, + DbClient = unknown, +> = { + type: ConnectorType; + open: () => Promise; + close: () => Promise; + + beginTransaction: () => Promise>; +}; diff --git a/src/packages/dumbo/src/connections/index.ts b/src/packages/dumbo/src/connections/index.ts index bf76b94f..e67b8262 100644 --- a/src/packages/dumbo/src/connections/index.ts +++ b/src/packages/dumbo/src/connections/index.ts @@ -1,3 +1,4 @@ -export * from './client'; +export * from './connection'; export * from './connectionString'; +export * from './pg'; export * from './pool'; diff --git a/src/packages/dumbo/src/connections/pg/connection.int.spec.ts b/src/packages/dumbo/src/connections/pg/connection.int.spec.ts new file mode 100644 index 00000000..77b6349f --- /dev/null +++ b/src/packages/dumbo/src/connections/pg/connection.int.spec.ts @@ -0,0 +1,93 @@ +import { + PostgreSqlContainer, + type StartedPostgreSqlContainer, +} from '@testcontainers/postgresql'; +import { after, before, describe, it } from 'node:test'; +import pg from 'pg'; +import { pgConnection } from '.'; +import { executeSQL } from '../../execute'; +import { rawSql } from '../../sql'; +import { endPool, getPool } from '../pool'; + +void describe('PostgreSQL connection', () => { + let postgres: StartedPostgreSqlContainer; + let connectionString: string; + + before(async () => { + postgres = await new PostgreSqlContainer().start(); + connectionString = postgres.getConnectionUri(); + }); + + after(async () => { + await postgres.stop(); + }); + + void describe('executeSQL', () => { + void it('connects using pool', async () => { + const connection = pgConnection({ connectionString }); + + try { + await executeSQL(connection.pool, rawSql('SELECT 1')); + } catch (error) { + console.log(error); + } finally { + await connection.close(); + } + }); + + void it('connects using connected pool client', async () => { + const connection = pgConnection({ connectionString }); + const poolClient = await connection.open(); + + try { + await executeSQL(poolClient, rawSql('SELECT 1')); + } finally { + await connection.close(); + } + }); + + void it('connects using existing pool', async () => { + const pool = getPool(connectionString); + const connection = pgConnection({ connectionString, pool }); + + try { + await executeSQL(pool, rawSql('SELECT 1')); + } finally { + await connection.close(); + await endPool({ connectionString }); + } + }); + + void it('connects using client', async () => { + const connection = pgConnection({ + connectionString, + type: 'client', + }); + const client = await connection.open(); + + try { + await executeSQL(client, rawSql('SELECT 1')); + } finally { + await connection.close(); + } + }); + + void it('connects using connected client', async () => { + const existingClient = new pg.Client({ connectionString }); + await existingClient.connect(); + + const connection = pgConnection({ + connectionString, + client: existingClient, + }); + const client = await connection.open(); + + try { + await executeSQL(client, rawSql('SELECT 1')); + } finally { + await connection.close(); + await existingClient.end(); + } + }); + }); +}); diff --git a/src/packages/dumbo/src/connections/pg/connection.ts b/src/packages/dumbo/src/connections/pg/connection.ts new file mode 100644 index 00000000..c499f591 --- /dev/null +++ b/src/packages/dumbo/src/connections/pg/connection.ts @@ -0,0 +1,174 @@ +import pg from 'pg'; +import type { Connection, Transaction } from '../connection'; +import { endPool, getPool } from '../pool'; + +export const NodePostgresConnectorType = 'PostgreSQL:pg'; +export type NodePostgresConnector = 'PostgreSQL:pg'; + +export type NodePostgresClient = pg.PoolClient | pg.Client; + +export type NodePostgresPoolOrClient = pg.Pool | pg.PoolClient | pg.Client; + +export type NodePostgresPoolConnection = Connection< + 'PostgreSQL:pg', + pg.PoolClient +> & { + pool: pg.Pool; +}; + +export type NodePostgresClientConnection = Connection< + NodePostgresConnector, + pg.Client +> & { + client: Promise; +}; + +export type NodePostgresTransaction< + DbClient extends NodePostgresPoolOrClient = NodePostgresPoolOrClient, +> = Transaction; + +export type NodePostgresConnection = NodePostgresPoolConnection; + +export const NodePostgresTransaction = < + DbClient extends NodePostgresPoolOrClient = NodePostgresPoolOrClient, +>( + client: Promise, +): NodePostgresTransaction => ({ + type: NodePostgresConnectorType, + client, + begin: async () => { + await (await client).query('BEGIN'); + }, + commit: async () => { + await (await client).query('COMMIT'); + }, + rollback: async () => { + await (await client).query('ROLLBACK'); + }, +}); + +export const NodePostgresPoolConnection = (options: { + connectionString: string; + database?: string; + pool?: pg.Pool; +}): NodePostgresPoolConnection => { + const { connectionString, database, pool: existingPool } = options; + const pool = existingPool + ? existingPool + : getPool({ connectionString, database }); + + let poolClient: pg.PoolClient | null = null; + const connect = async () => + (poolClient = poolClient ?? (await pool.connect())); + + return { + type: NodePostgresConnectorType, + pool, + open: connect, + close: async () => { + if (poolClient) { + poolClient.release(); + poolClient = null; + } + + if (!existingPool) await endPool({ connectionString, database }); + }, + beginTransaction: () => Promise.resolve(NodePostgresTransaction(connect())), + }; +}; + +export const nodePostgresClientConnection = (options: { + connectionString: string; + database?: string; + client?: pg.Client; +}): NodePostgresClientConnection => { + const { connectionString, database, client: existingClient } = options; + + let client: pg.Client | null = existingClient ?? null; + + const getClient = async () => { + if (client) return client; + + client = new pg.Client({ connectionString, database }); + + if (!existingClient) await client.connect(); + + return client; + }; + + return { + type: NodePostgresConnectorType, + get client() { + return getClient(); + }, + open: getClient, + close: async () => { + const connectedClient = await getClient(); + + if (!existingClient) await connectedClient.end(); + }, + beginTransaction: () => + Promise.resolve(NodePostgresTransaction(getClient())), + }; +}; + +export function pgConnection(options: { + connectionString: string; + database?: string; + type: 'pooled'; + pool: pg.Pool; +}): NodePostgresPoolConnection; +export function pgConnection(options: { + connectionString: string; + database?: string; + pool: pg.Pool; +}): NodePostgresPoolConnection; +export function pgConnection(options: { + connectionString: string; + database?: string; + type: 'pooled'; +}): NodePostgresPoolConnection; +export function pgConnection(options: { + connectionString: string; + database?: string; +}): NodePostgresPoolConnection; +export function pgConnection(options: { + connectionString: string; + database?: string; + type: 'client'; + client: pg.Client; +}): NodePostgresClientConnection; +export function pgConnection(options: { + connectionString: string; + database?: string; + client: pg.Client; +}): NodePostgresClientConnection; +export function pgConnection(options: { + connectionString: string; + database?: string; + type: 'client'; +}): NodePostgresClientConnection; +export function pgConnection(options: { + connectionString: string; + database?: string; + type?: 'pooled' | 'client'; + pool?: pg.Pool; + client?: pg.Client; +}): NodePostgresPoolConnection | NodePostgresClientConnection { + const { connectionString, database } = options; + + if (options.type === 'client' || 'client' in options) + return nodePostgresClientConnection({ + connectionString, + ...(database ? { database } : {}), + ...('client' in options && options.client + ? { client: options.client } + : {}), + }); + + return NodePostgresPoolConnection({ + connectionString, + ...(database ? { database } : {}), + ...('pool' in options && options.pool ? { pool: options.pool } : {}), + }); +} diff --git a/src/packages/dumbo/src/connections/pg/index.ts b/src/packages/dumbo/src/connections/pg/index.ts new file mode 100644 index 00000000..0fb32617 --- /dev/null +++ b/src/packages/dumbo/src/connections/pg/index.ts @@ -0,0 +1 @@ +export * from './connection'; diff --git a/src/packages/dumbo/src/connections/pool.ts b/src/packages/dumbo/src/connections/pool.ts index 444713fd..01e175ac 100644 --- a/src/packages/dumbo/src/connections/pool.ts +++ b/src/packages/dumbo/src/connections/pool.ts @@ -57,11 +57,11 @@ export const endPool = async ({ export const onEndPool = async (lookupKey: string, pool: pg.Pool) => { try { await pool.end(); - pools.delete(lookupKey); } catch (error) { console.log(`Error while closing the connection pool: ${lookupKey}`); console.log(error); } + pools.delete(lookupKey); }; export const endAllPools = () => diff --git a/src/packages/dumbo/src/execute/connections.e2e.spec.ts b/src/packages/dumbo/src/execute/connections.int.spec.ts similarity index 100% rename from src/packages/dumbo/src/execute/connections.e2e.spec.ts rename to src/packages/dumbo/src/execute/connections.int.spec.ts From d872d8a6b29f7daffc0bc3a546289359d70ecb37 Mon Sep 17 00:00:00 2001 From: Oskar Dudycz Date: Wed, 31 Jul 2024 15:16:57 +0200 Subject: [PATCH 04/30] Refactored connection and execution to allow injecting custom implementation --- .../dumbo/src/connections/pg/connection.ts | 10 +- src/packages/dumbo/src/execute/execute.ts | 197 ++++++++++++++++++ src/packages/dumbo/src/execute/index.ts | 177 +--------------- .../execute/{ => pg}/connections.int.spec.ts | 4 +- src/packages/dumbo/src/execute/pg/execute.ts | 52 +++++ src/packages/dumbo/src/execute/pg/index.ts | 1 + 6 files changed, 260 insertions(+), 181 deletions(-) create mode 100644 src/packages/dumbo/src/execute/execute.ts rename src/packages/dumbo/src/execute/{ => pg}/connections.int.spec.ts (95%) create mode 100644 src/packages/dumbo/src/execute/pg/execute.ts create mode 100644 src/packages/dumbo/src/execute/pg/index.ts diff --git a/src/packages/dumbo/src/connections/pg/connection.ts b/src/packages/dumbo/src/connections/pg/connection.ts index c499f591..9fb5ad92 100644 --- a/src/packages/dumbo/src/connections/pg/connection.ts +++ b/src/packages/dumbo/src/connections/pg/connection.ts @@ -27,9 +27,11 @@ export type NodePostgresTransaction< DbClient extends NodePostgresPoolOrClient = NodePostgresPoolOrClient, > = Transaction; -export type NodePostgresConnection = NodePostgresPoolConnection; +export type NodePostgresConnection = + | NodePostgresPoolConnection + | NodePostgresClientConnection; -export const NodePostgresTransaction = < +export const nodePostgresTransaction = < DbClient extends NodePostgresPoolOrClient = NodePostgresPoolOrClient, >( client: Promise, @@ -73,7 +75,7 @@ export const NodePostgresPoolConnection = (options: { if (!existingPool) await endPool({ connectionString, database }); }, - beginTransaction: () => Promise.resolve(NodePostgresTransaction(connect())), + beginTransaction: () => Promise.resolve(nodePostgresTransaction(connect())), }; }; @@ -108,7 +110,7 @@ export const nodePostgresClientConnection = (options: { if (!existingClient) await connectedClient.end(); }, beginTransaction: () => - Promise.resolve(NodePostgresTransaction(getClient())), + Promise.resolve(nodePostgresTransaction(getClient())), }; }; diff --git a/src/packages/dumbo/src/execute/execute.ts b/src/packages/dumbo/src/execute/execute.ts new file mode 100644 index 00000000..551402aa --- /dev/null +++ b/src/packages/dumbo/src/execute/execute.ts @@ -0,0 +1,197 @@ +import type { Connection } from '../connections'; +import type { SQL } from '../sql'; + +export const execute = async < + Result = void, + ConnectionType extends Connection = Connection, +>( + connection: ConnectionType, + handle: (client: ReturnType) => Promise, +) => { + const client = connection.open(); + + try { + return await handle(client as ReturnType); + } finally { + await connection.close(); + } +}; + +export const executeInTransaction = async < + Result = void, + ConnectionType extends Connection = Connection, +>( + connection: ConnectionType, + handle: ( + client: ReturnType, + ) => Promise<{ success: boolean; result: Result }>, +): Promise => + execute(connection, async (client) => { + const transaction = await connection.beginTransaction(); + + try { + const { success, result } = await handle(client); + + if (success) await transaction.commit(); + else await transaction.rollback(); + + return result; + } catch (e) { + await transaction.rollback(); + throw e; + } + }); + +export interface QueryResultRow { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [column: string]: any; +} + +export type QueryResult = { + rowCount: number | null; + rows: Result[]; +}; + +export type SQLExecutor = { + type: ConnectionType['type']; + query( + client: ReturnType, + sql: SQL, + ): Promise>; +}; + +const getExecutor = ( + _connectorType: ConnectionType['type'], +): SQLExecutor => ({ + type: '', + query: ( + _client: ReturnType, + _queryTextOrConfig: SQL, + ): Promise> => Promise.reject('Not Implemented!'), +}); + +export const executeSQL = async < + Result extends QueryResultRow = QueryResultRow, + ConnectionType extends Connection = Connection, +>( + connection: ConnectionType, + sql: SQL, +): Promise> => + execute(connection, (client) => + getExecutor(connection.type).query(client, sql), + ); + +export const executeSQLInTransaction = async < + Result extends QueryResultRow = QueryResultRow, + ConnectionType extends Connection = Connection, +>( + connection: ConnectionType, + sql: SQL, +) => { + console.log(sql); + return executeInTransaction(connection, async (client) => ({ + success: true, + result: await getExecutor(connection.type).query(client, sql), + })); +}; + +export const executeSQLBatchInTransaction = async < + Result extends QueryResultRow = QueryResultRow, + ConnectionType extends Connection = Connection, +>( + connection: ConnectionType, + ...sqls: SQL[] +) => + executeInTransaction(connection, async (client) => { + for (const sql of sqls) { + await getExecutor(connection.type).query(client, sql); + } + + return { success: true, result: undefined }; + }); + +export const firstOrNull = async < + Result extends QueryResultRow = QueryResultRow, +>( + getResult: Promise>, +): Promise => { + const result = await getResult; + + return result.rows.length > 0 ? result.rows[0] ?? null : null; +}; + +export const first = async ( + getResult: Promise>, +): Promise => { + const result = await getResult; + + if (result.rows.length === 0) + throw new Error("Query didn't return any result"); + + return result.rows[0]!; +}; + +export const singleOrNull = async < + Result extends QueryResultRow = QueryResultRow, +>( + getResult: Promise>, +): Promise => { + const result = await getResult; + + if (result.rows.length > 1) throw new Error('Query had more than one result'); + + return result.rows.length > 0 ? result.rows[0] ?? null : null; +}; + +export const single = async ( + getResult: Promise>, +): Promise => { + const result = await getResult; + + if (result.rows.length === 0) + throw new Error("Query didn't return any result"); + + if (result.rows.length > 1) throw new Error('Query had more than one result'); + + return result.rows[0]!; +}; + +export const mapRows = async < + Result extends QueryResultRow = QueryResultRow, + Mapped = unknown, +>( + getResult: Promise>, + map: (row: Result) => Mapped, +): Promise => { + const result = await getResult; + + return result.rows.map(map); +}; + +export const toCamelCase = (snakeStr: string): string => + snakeStr.replace(/_([a-z])/g, (g) => g[1]?.toUpperCase() ?? ''); + +export const mapToCamelCase = >( + obj: T, +): T => { + const newObj: Record = {}; + for (const key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + newObj[toCamelCase(key)] = obj[key]; + } + } + return newObj as T; +}; + +export type ExistsSQLQueryResult = { exists: boolean }; + +export const exists = async ( + connection: ConnectionType, + sql: SQL, +): Promise => { + const result = await single( + executeSQL(connection, sql), + ); + + return result.exists === true; +}; diff --git a/src/packages/dumbo/src/execute/index.ts b/src/packages/dumbo/src/execute/index.ts index 3721868e..a4a1698f 100644 --- a/src/packages/dumbo/src/execute/index.ts +++ b/src/packages/dumbo/src/execute/index.ts @@ -1,175 +1,2 @@ -import pg from 'pg'; -import type { SQL } from '../sql'; - -export const isPgPool = ( - poolOrClient: pg.Pool | pg.PoolClient | pg.Client, -): poolOrClient is pg.Pool => { - return poolOrClient instanceof pg.Pool; -}; - -export const isPgClient = ( - poolOrClient: pg.Pool | pg.PoolClient | pg.Client, -): poolOrClient is pg.Client => poolOrClient instanceof pg.Client; - -export const isPgPoolClient = ( - poolOrClient: pg.Pool | pg.PoolClient | pg.Client, -): poolOrClient is pg.PoolClient => - 'release' in poolOrClient && typeof poolOrClient.release === 'function'; - -export const execute = async ( - poolOrClient: pg.Pool | pg.PoolClient | pg.Client, - handle: (client: pg.PoolClient | pg.Client) => Promise, -) => { - const client = isPgPool(poolOrClient) - ? await poolOrClient.connect() - : poolOrClient; - - try { - return await handle(client); - } finally { - // release only if client wasn't injected externally - if (isPgPool(poolOrClient) && isPgPoolClient(client)) client.release(); - } -}; - -export const executeInTransaction = async ( - poolOrClient: pg.Pool | pg.PoolClient | pg.Client, - handle: ( - client: pg.PoolClient | pg.Client, - ) => Promise<{ success: boolean; result: Result }>, -): Promise => - execute(poolOrClient, async (client) => { - try { - await client.query('BEGIN'); - - const { success, result } = await handle(client); - - if (success) await client.query('COMMIT'); - else await client.query('ROLLBACK'); - - return result; - } catch (e) { - await client.query('ROLLBACK'); - throw e; - } - }); - -export const executeSQL = async < - Result extends pg.QueryResultRow = pg.QueryResultRow, ->( - poolOrClient: pg.Pool | pg.PoolClient | pg.Client, - sql: SQL, -): Promise> => - execute(poolOrClient, (client) => client.query(sql)); - -export const executeSQLInTransaction = async < - Result extends pg.QueryResultRow = pg.QueryResultRow, ->( - pool: pg.Pool | pg.PoolClient | pg.Client, - sql: SQL, -) => { - console.log(sql); - return executeInTransaction(pool, async (client) => ({ - success: true, - result: await client.query(sql), - })); -}; - -export const executeSQLBatchInTransaction = async < - Result extends pg.QueryResultRow = pg.QueryResultRow, ->( - pool: pg.Pool | pg.PoolClient | pg.Client, - ...sqls: SQL[] -) => - executeInTransaction(pool, async (client) => { - for (const sql of sqls) { - await client.query(sql); - } - - return { success: true, result: undefined }; - }); - -export const firstOrNull = async < - Result extends pg.QueryResultRow = pg.QueryResultRow, ->( - getResult: Promise>, -): Promise => { - const result = await getResult; - - return result.rows.length > 0 ? result.rows[0] ?? null : null; -}; - -export const first = async < - Result extends pg.QueryResultRow = pg.QueryResultRow, ->( - getResult: Promise>, -): Promise => { - const result = await getResult; - - if (result.rows.length === 0) - throw new Error("Query didn't return any result"); - - return result.rows[0]!; -}; - -export const singleOrNull = async < - Result extends pg.QueryResultRow = pg.QueryResultRow, ->( - getResult: Promise>, -): Promise => { - const result = await getResult; - - if (result.rows.length > 1) throw new Error('Query had more than one result'); - - return result.rows.length > 0 ? result.rows[0] ?? null : null; -}; - -export const single = async < - Result extends pg.QueryResultRow = pg.QueryResultRow, ->( - getResult: Promise>, -): Promise => { - const result = await getResult; - - if (result.rows.length === 0) - throw new Error("Query didn't return any result"); - - if (result.rows.length > 1) throw new Error('Query had more than one result'); - - return result.rows[0]!; -}; - -export const mapRows = async < - Result extends pg.QueryResultRow = pg.QueryResultRow, - Mapped = unknown, ->( - getResult: Promise>, - map: (row: Result) => Mapped, -): Promise => { - const result = await getResult; - - return result.rows.map(map); -}; - -export const toCamelCase = (snakeStr: string): string => - snakeStr.replace(/_([a-z])/g, (g) => g[1]?.toUpperCase() ?? ''); - -export const mapToCamelCase = >( - obj: T, -): T => { - const newObj: Record = {}; - for (const key in obj) { - if (Object.prototype.hasOwnProperty.call(obj, key)) { - newObj[toCamelCase(key)] = obj[key]; - } - } - return newObj as T; -}; - -export type ExistsSQLQueryResult = { exists: boolean }; - -export const exists = async (pool: pg.Pool, sql: SQL): Promise => { - const result = await single(executeSQL(pool, sql)); - - return result.exists === true; -}; +export * from './execute'; +export * from './pg'; diff --git a/src/packages/dumbo/src/execute/connections.int.spec.ts b/src/packages/dumbo/src/execute/pg/connections.int.spec.ts similarity index 95% rename from src/packages/dumbo/src/execute/connections.int.spec.ts rename to src/packages/dumbo/src/execute/pg/connections.int.spec.ts index 25968ee8..ca692cda 100644 --- a/src/packages/dumbo/src/execute/connections.int.spec.ts +++ b/src/packages/dumbo/src/execute/pg/connections.int.spec.ts @@ -4,8 +4,8 @@ import { } from '@testcontainers/postgresql'; import { after, before, describe, it } from 'node:test'; import pg from 'pg'; -import { executeSQL } from '.'; -import { rawSql } from '../sql'; +import { executeSQL } from '..'; +import { rawSql } from '../../sql'; void describe('PostgreSQL connection', () => { let postgres: StartedPostgreSqlContainer; diff --git a/src/packages/dumbo/src/execute/pg/execute.ts b/src/packages/dumbo/src/execute/pg/execute.ts new file mode 100644 index 00000000..fe328cf6 --- /dev/null +++ b/src/packages/dumbo/src/execute/pg/execute.ts @@ -0,0 +1,52 @@ +import pg from 'pg'; +import { + NodePostgresConnectorType, + type NodePostgresConnection, +} from '../../connections'; +import type { SQL } from '../../sql'; +import type { QueryResult, QueryResultRow, SQLExecutor } from '../execute'; + +export const isPgPool = ( + poolOrClient: pg.Pool | pg.PoolClient | pg.Client, +): poolOrClient is pg.Pool => { + return poolOrClient instanceof pg.Pool; +}; + +export const isPgClient = ( + poolOrClient: pg.Pool | pg.PoolClient | pg.Client, +): poolOrClient is pg.Client => poolOrClient instanceof pg.Client; + +export const isPgPoolClient = ( + poolOrClient: pg.Pool | pg.PoolClient | pg.Client, +): poolOrClient is pg.PoolClient => + 'release' in poolOrClient && typeof poolOrClient.release === 'function'; + +export const execute = async ( + poolOrClient: pg.Pool | pg.PoolClient | pg.Client, + handle: (client: pg.PoolClient | pg.Client) => Promise, +) => { + const client = isPgPool(poolOrClient) + ? await poolOrClient.connect() + : poolOrClient; + + try { + return await handle(client); + } finally { + // release only if client wasn't injected externally + if (isPgPool(poolOrClient) && isPgPoolClient(client)) client.release(); + } +}; + +export type NodePostgresSQLExecutor = SQLExecutor; + +export const nodePostgresSQLExecutor = (): NodePostgresSQLExecutor => ({ + type: NodePostgresConnectorType, + query: async ( + client: Promise | Promise, + sql: SQL, + ): Promise> => { + const result = await (await client).query(sql); + + return { rowCount: result.rowCount, rows: result.rows }; + }, +}); diff --git a/src/packages/dumbo/src/execute/pg/index.ts b/src/packages/dumbo/src/execute/pg/index.ts new file mode 100644 index 00000000..7941eda6 --- /dev/null +++ b/src/packages/dumbo/src/execute/pg/index.ts @@ -0,0 +1 @@ +export * from './execute'; From 5b6372fb2b95b2d890c05ba6c9ef7e6dacf9f54b Mon Sep 17 00:00:00 2001 From: Oskar Dudycz Date: Thu, 1 Aug 2024 11:02:03 +0200 Subject: [PATCH 05/30] Finished the first draft the new connection management --- package-lock.json | 6 + src/package-lock.json | 8 +- src/package.json | 2 +- src/packages/dumbo/package.json | 2 +- .../dumbo/src/connections/connection.ts | 19 +- src/packages/dumbo/src/connections/execute.ts | 78 ++++++ src/packages/dumbo/src/connections/index.ts | 2 + .../src/connections/pg/connection.int.spec.ts | 44 ++- .../dumbo/src/connections/pg/connection.ts | 201 ++++---------- .../dumbo/src/connections/pg/index.ts | 2 + src/packages/dumbo/src/connections/pg/pool.ts | 260 ++++++++++++++++++ .../dumbo/src/connections/pg/transaction.ts | 37 +++ src/packages/dumbo/src/connections/pool.ts | 97 +------ .../dumbo/src/connections/transaction.ts | 103 +++++++ src/packages/dumbo/src/execute/execute.ts | 172 +++++------- .../src/execute/pg/connections.int.spec.ts | 118 ++++---- src/packages/dumbo/src/execute/pg/execute.ts | 18 +- src/packages/dumbo/src/sql/schema.ts | 18 +- src/packages/dumbo/tsconfig.json | 2 +- src/packages/pongo/package.json | 2 +- src/packages/pongo/src/postgres/client.ts | 25 +- .../pongo/src/postgres/postgresCollection.ts | 15 +- 22 files changed, 763 insertions(+), 468 deletions(-) create mode 100644 package-lock.json create mode 100644 src/packages/dumbo/src/connections/execute.ts create mode 100644 src/packages/dumbo/src/connections/pg/pool.ts create mode 100644 src/packages/dumbo/src/connections/pg/transaction.ts create mode 100644 src/packages/dumbo/src/connections/transaction.ts diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..4d649352 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "Pongo", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/src/package-lock.json b/src/package-lock.json index 29202c8e..30ddb0e1 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -1,12 +1,12 @@ { "name": "@event-driven-io/pongo-core", - "version": "0.7.0", + "version": "0.8.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@event-driven-io/pongo-core", - "version": "0.7.0", + "version": "0.8.0", "workspaces": [ "packages/dumbo", "packages/pongo" @@ -6652,7 +6652,7 @@ }, "packages/dumbo": { "name": "@event-driven-io/dumbo", - "version": "0.5.0", + "version": "0.6.0", "devDependencies": { "@types/node": "20.11.30" }, @@ -6678,7 +6678,7 @@ "@types/node": "20.11.30" }, "peerDependencies": { - "@event-driven-io/dumbo": "^0.5.0", + "@event-driven-io/dumbo": "^0.6.0", "@types/mongodb": "^4.0.7", "@types/pg": "^8.11.6", "@types/pg-format": "^1.0.5", diff --git a/src/package.json b/src/package.json index 1c78d3f4..e8c9f3ff 100644 --- a/src/package.json +++ b/src/package.json @@ -1,6 +1,6 @@ { "name": "@event-driven-io/pongo-core", - "version": "0.7.0", + "version": "0.8.0", "description": "Pongo - Mongo with strong consistency on top of Postgres", "type": "module", "engines": { diff --git a/src/packages/dumbo/package.json b/src/packages/dumbo/package.json index be0a5956..113b57c1 100644 --- a/src/packages/dumbo/package.json +++ b/src/packages/dumbo/package.json @@ -1,6 +1,6 @@ { "name": "@event-driven-io/dumbo", - "version": "0.5.0", + "version": "0.6.0", "description": "Dumbo - tools for dealing with PostgreSQL", "type": "module", "scripts": { diff --git a/src/packages/dumbo/src/connections/connection.ts b/src/packages/dumbo/src/connections/connection.ts index 56b9cb5a..1de26592 100644 --- a/src/packages/dumbo/src/connections/connection.ts +++ b/src/packages/dumbo/src/connections/connection.ts @@ -1,21 +1,12 @@ -export type Transaction< - ConnectorType extends string = string, - DbClient = unknown, -> = { - type: ConnectorType; - client: Promise; - begin: () => Promise; - commit: () => Promise; - rollback: () => Promise; -}; +import type { WithSQLExecutor } from './execute'; +import type { TransactionFactory } from './transaction'; export type Connection< ConnectorType extends string = string, DbClient = unknown, > = { type: ConnectorType; - open: () => Promise; + connect: () => Promise; close: () => Promise; - - beginTransaction: () => Promise>; -}; +} & WithSQLExecutor & + TransactionFactory; diff --git a/src/packages/dumbo/src/connections/execute.ts b/src/packages/dumbo/src/connections/execute.ts new file mode 100644 index 00000000..166c568d --- /dev/null +++ b/src/packages/dumbo/src/connections/execute.ts @@ -0,0 +1,78 @@ +import { type SQL } from '../sql'; +import type { Connection } from './connection'; + +export interface QueryResultRow { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [column: string]: any; +} + +export type QueryResult = { + rowCount: number | null; + rows: Result[]; +}; + +export type SQLExecutor< + ConnectorType extends string = string, + DbClient = unknown, +> = { + type: ConnectorType; + query( + client: DbClient, + sql: SQL, + ): Promise>; +}; + +export type WithSQLExecutor = { + execute: { + query( + sql: SQL, + ): Promise>; + }; +}; + +export const withSqlExecutor = < + DbClient = unknown, + Executor extends SQLExecutor = SQLExecutor, +>( + sqlExecutor: Executor, + options: { + connect: () => Promise; + close?: (client: DbClient, error?: unknown) => Promise; + }, +): WithSQLExecutor => ({ + execute: { + query: async (sql: SQL) => { + const { connect, close } = options; + const client = await connect(); + + try { + const result = await sqlExecutor.query(client, sql); + if (close) await close(client); + return result; + } catch (error) { + if (close) await close(client, error); + + throw error; + } + }, + }, +}); + +export const queryWithNewConnection = async < + ConnectionType extends Connection, + Result extends QueryResultRow = QueryResultRow, +>( + connectionFactory: { + open: () => Promise; + }, + sql: SQL, +) => { + const { open } = connectionFactory; + const connection = await open(); + + try { + return await connection.execute.query(sql); + } finally { + await connection.close(); + } +}; diff --git a/src/packages/dumbo/src/connections/index.ts b/src/packages/dumbo/src/connections/index.ts index e67b8262..4d32ab97 100644 --- a/src/packages/dumbo/src/connections/index.ts +++ b/src/packages/dumbo/src/connections/index.ts @@ -1,4 +1,6 @@ export * from './connection'; export * from './connectionString'; +export * from './execute'; export * from './pg'; export * from './pool'; +export * from './transaction'; diff --git a/src/packages/dumbo/src/connections/pg/connection.int.spec.ts b/src/packages/dumbo/src/connections/pg/connection.int.spec.ts index 77b6349f..6395454b 100644 --- a/src/packages/dumbo/src/connections/pg/connection.int.spec.ts +++ b/src/packages/dumbo/src/connections/pg/connection.int.spec.ts @@ -4,10 +4,9 @@ import { } from '@testcontainers/postgresql'; import { after, before, describe, it } from 'node:test'; import pg from 'pg'; -import { pgConnection } from '.'; -import { executeSQL } from '../../execute'; +import { nodePostgresPool } from '.'; import { rawSql } from '../../sql'; -import { endPool, getPool } from '../pool'; +import { endPool, getPool } from './pool'; void describe('PostgreSQL connection', () => { let postgres: StartedPostgreSqlContainer; @@ -24,51 +23,45 @@ void describe('PostgreSQL connection', () => { void describe('executeSQL', () => { void it('connects using pool', async () => { - const connection = pgConnection({ connectionString }); + const pool = nodePostgresPool({ connectionString }); + const connection = await pool.open(); try { - await executeSQL(connection.pool, rawSql('SELECT 1')); + await connection.execute.query(rawSql('SELECT 1')); } catch (error) { console.log(error); } finally { await connection.close(); - } - }); - - void it('connects using connected pool client', async () => { - const connection = pgConnection({ connectionString }); - const poolClient = await connection.open(); - - try { - await executeSQL(poolClient, rawSql('SELECT 1')); - } finally { - await connection.close(); + await pool.close(); } }); void it('connects using existing pool', async () => { - const pool = getPool(connectionString); - const connection = pgConnection({ connectionString, pool }); + const nativePool = getPool(connectionString); + const pool = nodePostgresPool({ connectionString, pool: nativePool }); + const connection = await pool.open(); try { - await executeSQL(pool, rawSql('SELECT 1')); + await connection.execute.query(rawSql('SELECT 1')); } finally { await connection.close(); + await pool.close(); await endPool({ connectionString }); } }); void it('connects using client', async () => { - const connection = pgConnection({ + const pool = nodePostgresPool({ connectionString, type: 'client', }); - const client = await connection.open(); + const connection = await pool.open(); try { - await executeSQL(client, rawSql('SELECT 1')); + await connection.execute.query(rawSql('SELECT 1')); } finally { await connection.close(); + await pool.close(); } }); @@ -76,16 +69,17 @@ void describe('PostgreSQL connection', () => { const existingClient = new pg.Client({ connectionString }); await existingClient.connect(); - const connection = pgConnection({ + const pool = nodePostgresPool({ connectionString, client: existingClient, }); - const client = await connection.open(); + const connection = await pool.open(); try { - await executeSQL(client, rawSql('SELECT 1')); + await connection.execute.query(rawSql('SELECT 1')); } finally { await connection.close(); + await pool.close(); await existingClient.end(); } }); diff --git a/src/packages/dumbo/src/connections/pg/connection.ts b/src/packages/dumbo/src/connections/pg/connection.ts index 9fb5ad92..8b9ffe12 100644 --- a/src/packages/dumbo/src/connections/pg/connection.ts +++ b/src/packages/dumbo/src/connections/pg/connection.ts @@ -1,6 +1,9 @@ import pg from 'pg'; -import type { Connection, Transaction } from '../connection'; -import { endPool, getPool } from '../pool'; +import { nodePostgresSQLExecutor } from '../../execute'; +import type { Connection } from '../connection'; +import { withSqlExecutor } from '../execute'; +import { transactionFactory } from '../transaction'; +import { nodePostgresTransaction } from './transaction'; export const NodePostgresConnectorType = 'PostgreSQL:pg'; export type NodePostgresConnector = 'PostgreSQL:pg'; @@ -9,168 +12,78 @@ export type NodePostgresClient = pg.PoolClient | pg.Client; export type NodePostgresPoolOrClient = pg.Pool | pg.PoolClient | pg.Client; -export type NodePostgresPoolConnection = Connection< - 'PostgreSQL:pg', - pg.PoolClient -> & { - pool: pg.Pool; -}; - export type NodePostgresClientConnection = Connection< NodePostgresConnector, pg.Client -> & { - client: Promise; -}; +>; -export type NodePostgresTransaction< - DbClient extends NodePostgresPoolOrClient = NodePostgresPoolOrClient, -> = Transaction; +export type NodePostgresPoolClientConnection = Connection< + NodePostgresConnector, + pg.PoolClient +>; export type NodePostgresConnection = - | NodePostgresPoolConnection + | NodePostgresPoolClientConnection | NodePostgresClientConnection; -export const nodePostgresTransaction = < - DbClient extends NodePostgresPoolOrClient = NodePostgresPoolOrClient, ->( - client: Promise, -): NodePostgresTransaction => ({ - type: NodePostgresConnectorType, - client, - begin: async () => { - await (await client).query('BEGIN'); - }, - commit: async () => { - await (await client).query('COMMIT'); - }, - rollback: async () => { - await (await client).query('ROLLBACK'); - }, -}); - -export const NodePostgresPoolConnection = (options: { - connectionString: string; - database?: string; - pool?: pg.Pool; -}): NodePostgresPoolConnection => { - const { connectionString, database, pool: existingPool } = options; - const pool = existingPool - ? existingPool - : getPool({ connectionString, database }); - - let poolClient: pg.PoolClient | null = null; - const connect = async () => - (poolClient = poolClient ?? (await pool.connect())); +export type NodePostgresPoolClientOptions = { + type: 'PoolClient'; + connect: Promise; + close: (client: pg.PoolClient) => Promise; +}; - return { - type: NodePostgresConnectorType, - pool, - open: connect, - close: async () => { - if (poolClient) { - poolClient.release(); - poolClient = null; - } - - if (!existingPool) await endPool({ connectionString, database }); - }, - beginTransaction: () => Promise.resolve(nodePostgresTransaction(connect())), - }; +export type NodePostgresClientOptions = { + type: 'Client'; + connect: Promise; + close: (client: pg.Client) => Promise; }; -export const nodePostgresClientConnection = (options: { - connectionString: string; - database?: string; - client?: pg.Client; -}): NodePostgresClientConnection => { - const { connectionString, database, client: existingClient } = options; +export const nodePostgresClientConnection = ( + options: NodePostgresClientOptions, +): NodePostgresClientConnection => { + const { connect, close } = options; - let client: pg.Client | null = existingClient ?? null; + let client: pg.Client | null = null; - const getClient = async () => { - if (client) return client; + const getClient = async () => client ?? (client = await connect); + + return { + type: NodePostgresConnectorType, + connect: getClient, + close: () => (client ? close(client) : Promise.resolve()), + ...transactionFactory(getClient, nodePostgresTransaction), + ...withSqlExecutor(nodePostgresSQLExecutor(), { connect: getClient }), + }; +}; - client = new pg.Client({ connectionString, database }); +export const nodePostgresPoolClientConnection = ( + options: NodePostgresPoolClientOptions, +): NodePostgresPoolClientConnection => { + const { connect, close } = options; - if (!existingClient) await client.connect(); + let client: pg.PoolClient | null = null; - return client; - }; + const getClient = async () => client ?? (client = await connect); return { type: NodePostgresConnectorType, - get client() { - return getClient(); - }, - open: getClient, - close: async () => { - const connectedClient = await getClient(); - - if (!existingClient) await connectedClient.end(); - }, - beginTransaction: () => - Promise.resolve(nodePostgresTransaction(getClient())), + connect: getClient, + close: () => (client ? close(client) : Promise.resolve()), + ...transactionFactory(getClient, nodePostgresTransaction), + ...withSqlExecutor(nodePostgresSQLExecutor(), { connect: getClient }), }; }; -export function pgConnection(options: { - connectionString: string; - database?: string; - type: 'pooled'; - pool: pg.Pool; -}): NodePostgresPoolConnection; -export function pgConnection(options: { - connectionString: string; - database?: string; - pool: pg.Pool; -}): NodePostgresPoolConnection; -export function pgConnection(options: { - connectionString: string; - database?: string; - type: 'pooled'; -}): NodePostgresPoolConnection; -export function pgConnection(options: { - connectionString: string; - database?: string; -}): NodePostgresPoolConnection; -export function pgConnection(options: { - connectionString: string; - database?: string; - type: 'client'; - client: pg.Client; -}): NodePostgresClientConnection; -export function pgConnection(options: { - connectionString: string; - database?: string; - client: pg.Client; -}): NodePostgresClientConnection; -export function pgConnection(options: { - connectionString: string; - database?: string; - type: 'client'; -}): NodePostgresClientConnection; -export function pgConnection(options: { - connectionString: string; - database?: string; - type?: 'pooled' | 'client'; - pool?: pg.Pool; - client?: pg.Client; -}): NodePostgresPoolConnection | NodePostgresClientConnection { - const { connectionString, database } = options; - - if (options.type === 'client' || 'client' in options) - return nodePostgresClientConnection({ - connectionString, - ...(database ? { database } : {}), - ...('client' in options && options.client - ? { client: options.client } - : {}), - }); - - return NodePostgresPoolConnection({ - connectionString, - ...(database ? { database } : {}), - ...('pool' in options && options.pool ? { pool: options.pool } : {}), - }); +export function nodePostgresConnection( + options: NodePostgresPoolClientOptions, +): NodePostgresPoolClientConnection; +export function nodePostgresConnection( + options: NodePostgresClientOptions, +): NodePostgresClientConnection; +export function nodePostgresConnection( + options: NodePostgresPoolClientOptions | NodePostgresClientOptions, +): NodePostgresPoolClientConnection | NodePostgresClientConnection { + return options.type === 'Client' + ? nodePostgresClientConnection(options) + : nodePostgresPoolClientConnection(options); } diff --git a/src/packages/dumbo/src/connections/pg/index.ts b/src/packages/dumbo/src/connections/pg/index.ts index 0fb32617..477c3fed 100644 --- a/src/packages/dumbo/src/connections/pg/index.ts +++ b/src/packages/dumbo/src/connections/pg/index.ts @@ -1 +1,3 @@ export * from './connection'; +export * from './pool'; +export * from './transaction'; diff --git a/src/packages/dumbo/src/connections/pg/pool.ts b/src/packages/dumbo/src/connections/pg/pool.ts new file mode 100644 index 00000000..826975f4 --- /dev/null +++ b/src/packages/dumbo/src/connections/pg/pool.ts @@ -0,0 +1,260 @@ +import pg from 'pg'; +import { type SQL } from '../../sql'; +import { + defaultPostgreSqlDatabase, + getDatabaseNameOrDefault, +} from '../connectionString'; +import { queryWithNewConnection } from '../execute'; +import type { ConnectionPool } from '../pool'; +import type { Transaction } from '../transaction'; +import { + nodePostgresConnection, + NodePostgresConnectorType, + type NodePostgresClientConnection, + type NodePostgresConnector, + type NodePostgresPoolClientConnection, +} from './connection'; + +export type NodePostgresNativePool = + ConnectionPool; + +export type NodePostgresExplicitClientPool = + ConnectionPool; + +export const nodePostgresNativePool = (options: { + connectionString: string; + database?: string; + pool?: pg.Pool; +}): NodePostgresNativePool => { + const { connectionString, database, pool: existingPool } = options; + const pool = existingPool + ? existingPool + : getPool({ connectionString, database }); + + const getConnection = () => { + const connect = pool.connect(); + + return nodePostgresConnection({ + type: 'PoolClient', + connect, + close: (client) => Promise.resolve(client.release()), + }); + }; + + const open = () => Promise.resolve(getConnection()); + const close = async () => { + if (!existingPool) await endPool({ connectionString, database }); + }; + + return { + type: NodePostgresConnectorType, + open, + close, + execute: { + query: async (sql: SQL) => queryWithNewConnection({ open }, sql), + }, + transaction: () => getConnection().transaction(), + inTransaction: async ( + handle: ( + transaction: Transaction, + ) => Promise<{ success: boolean; result: Result }>, + ): Promise => { + const connection = getConnection(); + try { + return await connection.inTransaction(handle); + } finally { + await connection.close(); + } + }, + }; +}; + +export const nodePostgresExplicitClientPool = (options: { + connectionString: string; + database?: string; + client?: pg.Client; +}): NodePostgresExplicitClientPool => { + const { connectionString, database, client: existingClient } = options; + + const getConnection = () => { + const connect = existingClient + ? Promise.resolve(existingClient) + : Promise.resolve(new pg.Client({ connectionString, database })).then( + async (client) => { + await client.connect(); + return client; + }, + ); + + return nodePostgresConnection({ + type: 'Client', + connect, + close: (client) => (existingClient ? Promise.resolve() : client.end()), + }); + }; + + const open = () => Promise.resolve(getConnection()); + const close = async () => { + if (!existingClient) await endPool({ connectionString, database }); + }; + + return { + type: NodePostgresConnectorType, + open, + close, + execute: { + query: (sql: SQL) => queryWithNewConnection({ open }, sql), + }, + transaction: () => getConnection().transaction(), + inTransaction: async ( + handle: ( + transaction: Transaction, + ) => Promise<{ success: boolean; result: Result }>, + ): Promise => { + const connection = getConnection(); + try { + return await connection.inTransaction(handle); + } finally { + await connection.close(); + } + }, + }; +}; + +export function nodePostgresPool(options: { + connectionString: string; + database?: string; + type: 'pooled'; + pool: pg.Pool; +}): NodePostgresNativePool; +export function nodePostgresPool(options: { + connectionString: string; + database?: string; + pool: pg.Pool; +}): NodePostgresNativePool; +export function nodePostgresPool(options: { + connectionString: string; + database?: string; + type: 'pooled'; +}): NodePostgresNativePool; +export function nodePostgresPool(options: { + connectionString: string; + database?: string; +}): NodePostgresNativePool; +export function nodePostgresPool(options: { + connectionString: string; + database?: string; + type: 'client'; + client: pg.Client; +}): NodePostgresExplicitClientPool; +export function nodePostgresPool(options: { + connectionString: string; + database?: string; + client: pg.Client; +}): NodePostgresExplicitClientPool; +export function nodePostgresPool(options: { + connectionString: string; + database?: string; + type: 'client'; +}): NodePostgresExplicitClientPool; +export function nodePostgresPool(options: { + connectionString: string; + database?: string; + type?: 'pooled' | 'client'; + pool?: pg.Pool; + client?: pg.Client; +}): NodePostgresNativePool | NodePostgresExplicitClientPool { + const { connectionString, database } = options; + + if (options.type === 'client' || 'client' in options) + return nodePostgresExplicitClientPool({ + connectionString, + ...(database ? { database } : {}), + ...('client' in options && options.client + ? { client: options.client } + : {}), + }); + + return nodePostgresNativePool({ + connectionString, + ...(database ? { database } : {}), + ...('pool' in options && options.pool ? { pool: options.pool } : {}), + }); +} + +const pools: Map = new Map(); +const usageCounter: Map = new Map(); + +export const getPool = ( + connectionStringOrOptions: string | pg.PoolConfig, +): pg.Pool => { + const connectionString = + typeof connectionStringOrOptions === 'string' + ? connectionStringOrOptions + : connectionStringOrOptions.connectionString!; + + const poolOptions = + typeof connectionStringOrOptions === 'string' + ? { connectionString } + : connectionStringOrOptions; + + const database = + poolOptions.database ?? + (poolOptions.connectionString + ? getDatabaseNameOrDefault(poolOptions.connectionString) + : undefined); + + const lookupKey = key(connectionString, database); + + updatePoolUsageCounter(lookupKey, 1); + + return ( + pools.get(lookupKey) ?? + pools.set(lookupKey, new pg.Pool(poolOptions)).get(lookupKey)! + ); +}; + +export const endPool = async ({ + connectionString, + database, + force, +}: { + connectionString: string; + database?: string | undefined; + force?: boolean; +}): Promise => { + database = database ?? getDatabaseNameOrDefault(connectionString); + const lookupKey = key(connectionString, database); + + const pool = pools.get(lookupKey); + if (pool && (updatePoolUsageCounter(lookupKey, -1) <= 0 || force === true)) { + await onEndPool(lookupKey, pool); + } +}; + +export const onEndPool = async (lookupKey: string, pool: pg.Pool) => { + try { + await pool.end(); + } catch (error) { + console.log(`Error while closing the connection pool: ${lookupKey}`); + console.log(error); + } + pools.delete(lookupKey); +}; + +export const endAllPools = () => + Promise.all( + [...pools.entries()].map(([lookupKey, pool]) => onEndPool(lookupKey, pool)), + ); + +const key = (connectionString: string, database: string | undefined) => + `${connectionString}|${database ?? defaultPostgreSqlDatabase}`; + +const updatePoolUsageCounter = (lookupKey: string, by: 1 | -1): number => { + const currentCounter = usageCounter.get(lookupKey) ?? 0; + const newCounter = currentCounter + by; + + usageCounter.set(lookupKey, currentCounter + by); + + return newCounter; +}; diff --git a/src/packages/dumbo/src/connections/pg/transaction.ts b/src/packages/dumbo/src/connections/pg/transaction.ts new file mode 100644 index 00000000..b8c94fad --- /dev/null +++ b/src/packages/dumbo/src/connections/pg/transaction.ts @@ -0,0 +1,37 @@ +import { nodePostgresSQLExecutor } from '../../execute'; +import { withSqlExecutor } from '../execute'; +import type { Transaction } from '../transaction'; +import { + NodePostgresConnectorType, + type NodePostgresConnector, + type NodePostgresPoolOrClient, +} from './connection'; + +export type NodePostgresTransaction = Transaction; + +export const nodePostgresTransaction = < + DbClient extends NodePostgresPoolOrClient = NodePostgresPoolOrClient, +>( + getClient: Promise, + options?: { close: (client: DbClient, error?: unknown) => Promise }, +): Transaction => ({ + type: NodePostgresConnectorType, + begin: async () => { + const client = await getClient; + await client.query('BEGIN'); + }, + commit: async () => { + const client = await getClient; + + await client.query('COMMIT'); + + if (options?.close) await options?.close(client); + }, + rollback: async (error?: unknown) => { + const client = await getClient; + await client.query('ROLLBACK'); + + if (options?.close) await options?.close(client, error); + }, + ...withSqlExecutor(nodePostgresSQLExecutor(), { connect: () => getClient }), +}); diff --git a/src/packages/dumbo/src/connections/pool.ts b/src/packages/dumbo/src/connections/pool.ts index 01e175ac..ced28e8c 100644 --- a/src/packages/dumbo/src/connections/pool.ts +++ b/src/packages/dumbo/src/connections/pool.ts @@ -1,82 +1,15 @@ -import pg from 'pg'; -import { - defaultPostgreSqlDatabase, - getDatabaseNameOrDefault, -} from './connectionString'; - -const pools: Map = new Map(); -const usageCounter: Map = new Map(); - -export const getPool = ( - connectionStringOrOptions: string | pg.PoolConfig, -): pg.Pool => { - const connectionString = - typeof connectionStringOrOptions === 'string' - ? connectionStringOrOptions - : connectionStringOrOptions.connectionString!; - - const poolOptions = - typeof connectionStringOrOptions === 'string' - ? { connectionString } - : connectionStringOrOptions; - - const database = - poolOptions.database ?? - (poolOptions.connectionString - ? getDatabaseNameOrDefault(poolOptions.connectionString) - : undefined); - - const lookupKey = key(connectionString, database); - - updatePoolUsageCounter(lookupKey, 1); - - return ( - pools.get(lookupKey) ?? - pools.set(lookupKey, new pg.Pool(poolOptions)).get(lookupKey)! - ); -}; - -export const endPool = async ({ - connectionString, - database, - force, -}: { - connectionString: string; - database?: string | undefined; - force?: boolean; -}): Promise => { - database = database ?? getDatabaseNameOrDefault(connectionString); - const lookupKey = key(connectionString, database); - - const pool = pools.get(lookupKey); - if (pool && (updatePoolUsageCounter(lookupKey, -1) <= 0 || force === true)) { - await onEndPool(lookupKey, pool); - } -}; - -export const onEndPool = async (lookupKey: string, pool: pg.Pool) => { - try { - await pool.end(); - } catch (error) { - console.log(`Error while closing the connection pool: ${lookupKey}`); - console.log(error); - } - pools.delete(lookupKey); -}; - -export const endAllPools = () => - Promise.all( - [...pools.entries()].map(([lookupKey, pool]) => onEndPool(lookupKey, pool)), - ); - -const key = (connectionString: string, database: string | undefined) => - `${connectionString}|${database ?? defaultPostgreSqlDatabase}`; - -const updatePoolUsageCounter = (lookupKey: string, by: 1 | -1): number => { - const currentCounter = usageCounter.get(lookupKey) ?? 0; - const newCounter = currentCounter + by; - - usageCounter.set(lookupKey, currentCounter + by); - - return newCounter; -}; +import { type Connection } from './connection'; +import type { WithSQLExecutor } from './execute'; +import type { TransactionFactory } from './transaction'; + +export type ConnectionPool = { + type: ConnectionType['type']; + open: () => Promise; + close: () => Promise; +} & WithSQLExecutor & + TransactionFactory; + +export type ConnectionPoolProvider< + ConnectionPoolType extends ConnectionPool = ConnectionPool, + ConnectionPoolOptions = unknown, +> = (options: ConnectionPoolOptions) => ConnectionPoolType; diff --git a/src/packages/dumbo/src/connections/transaction.ts b/src/packages/dumbo/src/connections/transaction.ts new file mode 100644 index 00000000..0badf710 --- /dev/null +++ b/src/packages/dumbo/src/connections/transaction.ts @@ -0,0 +1,103 @@ +import { type Connection } from './connection'; +import type { WithSQLExecutor } from './execute'; + +export type Transaction = { + type: ConnectorType; + begin: () => Promise; + commit: () => Promise; + rollback: (error?: unknown) => Promise; +} & WithSQLExecutor; + +export type TransactionFactory = { + transaction: () => Transaction; + + inTransaction: ( + handle: ( + transaction: Transaction, + ) => Promise<{ success: boolean; result: Result }>, + ) => Promise; +}; + +export const transactionFactory = < + ConnectorType extends string = string, + DbClient = unknown, +>( + connect: () => Promise, + initTransaction: (client: Promise) => Transaction, +): TransactionFactory => ({ + transaction: () => initTransaction(connect()), + inTransaction: async ( + handle: ( + transaction: Transaction, + ) => Promise<{ success: boolean; result: Result }>, + ): Promise => { + const transaction = initTransaction(connect()); + + await transaction.begin(); + + try { + const { success, result } = await handle(transaction); + + if (success) await transaction.commit(); + else await transaction.rollback(); + + return result; + } catch (e) { + await transaction.rollback(); + throw e; + } + }, +}); + +export const transactionFactoryWithNewConnection = < + ConnectionType extends Connection = Connection, +>( + connectionFactory: () => ConnectionType, + initTransaction: ( + client: Promise>, + options?: { + close: ( + client: ReturnType, + error?: unknown, + ) => Promise; + }, + ) => Transaction, +): TransactionFactory => ({ + transaction: () => { + const connection = connectionFactory(); + + return initTransaction( + connection.connect() as Promise>, + { + close: () => connection.close(), + }, + ); + }, + inTransaction: async ( + handle: ( + transaction: Transaction, + ) => Promise<{ success: boolean; result: Result }>, + ): Promise => { + const connection = connectionFactory(); + const transaction = initTransaction( + connection.connect() as Promise>, + { + close: () => connection.close(), + }, + ); + + await transaction.begin(); + + try { + const { success, result } = await handle(transaction); + + if (success) await transaction.commit(); + else await transaction.rollback(); + + return result; + } catch (e) { + await transaction.rollback(); + throw e; + } + }, +}); diff --git a/src/packages/dumbo/src/execute/execute.ts b/src/packages/dumbo/src/execute/execute.ts index 551402aa..7e85ca90 100644 --- a/src/packages/dumbo/src/execute/execute.ts +++ b/src/packages/dumbo/src/execute/execute.ts @@ -1,4 +1,5 @@ import type { Connection } from '../connections'; +import type { QueryResult, QueryResultRow } from '../connections/execute'; import type { SQL } from '../sql'; export const execute = async < @@ -6,109 +7,88 @@ export const execute = async < ConnectionType extends Connection = Connection, >( connection: ConnectionType, - handle: (client: ReturnType) => Promise, + handle: (client: ReturnType) => Promise, ) => { - const client = connection.open(); + const client = connection.connect(); try { - return await handle(client as ReturnType); + return await handle(client as ReturnType); } finally { await connection.close(); } }; -export const executeInTransaction = async < - Result = void, - ConnectionType extends Connection = Connection, ->( - connection: ConnectionType, - handle: ( - client: ReturnType, - ) => Promise<{ success: boolean; result: Result }>, -): Promise => - execute(connection, async (client) => { - const transaction = await connection.beginTransaction(); - - try { - const { success, result } = await handle(client); - - if (success) await transaction.commit(); - else await transaction.rollback(); - - return result; - } catch (e) { - await transaction.rollback(); - throw e; - } - }); - -export interface QueryResultRow { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - [column: string]: any; -} - -export type QueryResult = { - rowCount: number | null; - rows: Result[]; -}; - -export type SQLExecutor = { - type: ConnectionType['type']; - query( - client: ReturnType, - sql: SQL, - ): Promise>; -}; - -const getExecutor = ( - _connectorType: ConnectionType['type'], -): SQLExecutor => ({ - type: '', - query: ( - _client: ReturnType, - _queryTextOrConfig: SQL, - ): Promise> => Promise.reject('Not Implemented!'), -}); - -export const executeSQL = async < - Result extends QueryResultRow = QueryResultRow, - ConnectionType extends Connection = Connection, ->( - connection: ConnectionType, - sql: SQL, -): Promise> => - execute(connection, (client) => - getExecutor(connection.type).query(client, sql), - ); - -export const executeSQLInTransaction = async < - Result extends QueryResultRow = QueryResultRow, - ConnectionType extends Connection = Connection, ->( - connection: ConnectionType, - sql: SQL, -) => { - console.log(sql); - return executeInTransaction(connection, async (client) => ({ - success: true, - result: await getExecutor(connection.type).query(client, sql), - })); -}; - -export const executeSQLBatchInTransaction = async < - Result extends QueryResultRow = QueryResultRow, - ConnectionType extends Connection = Connection, ->( - connection: ConnectionType, - ...sqls: SQL[] -) => - executeInTransaction(connection, async (client) => { - for (const sql of sqls) { - await getExecutor(connection.type).query(client, sql); - } - - return { success: true, result: undefined }; - }); +// export const executeInTransaction = async < +// Result = void, +// ConnectionType extends Connection = Connection, +// >( +// connection: ConnectionType, +// handle: ( +// client: ReturnType, +// ) => Promise<{ success: boolean; result: Result }>, +// ): Promise => +// execute(connection, async (client) => { +// const transaction = await connection.transaction(); + +// try { +// const { success, result } = await handle(client); + +// if (success) await transaction.commit(); +// else await transaction.rollback(); + +// return result; +// } catch (e) { +// await transaction.rollback(); +// throw e; +// } +// }); + +// const getExecutor = ( +// _connectorType: ConnectionType['type'], +// ): SQLExecutor => ({ +// type: '', +// query: ( +// _client: ReturnType, +// _queryTextOrConfig: SQL, +// ): Promise> => Promise.reject('Not Implemented!'), +// }); + +// export const executeSQL = async < +// Result extends QueryResultRow = QueryResultRow, +// ConnectionType extends Connection = Connection, +// >( +// connection: ConnectionType, +// sql: SQL, +// ): Promise> => connection.execute.query(sql); + +// export const executeSQLInTransaction = async < +// Result extends QueryResultRow = QueryResultRow, +// ConnectionType extends Connection = Connection, +// >( +// connection: ConnectionType, +// sql: SQL, +// ) => { +// console.log(sql); +// return executeInTransaction(connection, async (client) => ({ +// success: true, +// result: await getExecutor(connection.type).query(client, sql), +// })); +// }; + +// export const executeSQLBatchInTransaction = async < +// Result extends QueryResultRow = QueryResultRow, +// ConnectionType extends Connection = Connection, +// >( +// connection: ConnectionType, +// ...sqls: SQL[] +// ) => +// executeInTransaction(connection, async (client) => { +// for (const sql of sqls) { +// await getExecutor(connection.type).query(client, sql); +// } + +// return { success: true, result: undefined }; +// }); export const firstOrNull = async < Result extends QueryResultRow = QueryResultRow, @@ -190,7 +170,7 @@ export const exists = async ( sql: SQL, ): Promise => { const result = await single( - executeSQL(connection, sql), + connection.execute.query(sql), ); return result.exists === true; diff --git a/src/packages/dumbo/src/execute/pg/connections.int.spec.ts b/src/packages/dumbo/src/execute/pg/connections.int.spec.ts index ca692cda..b6af85b6 100644 --- a/src/packages/dumbo/src/execute/pg/connections.int.spec.ts +++ b/src/packages/dumbo/src/execute/pg/connections.int.spec.ts @@ -1,59 +1,59 @@ -import { - PostgreSqlContainer, - type StartedPostgreSqlContainer, -} from '@testcontainers/postgresql'; -import { after, before, describe, it } from 'node:test'; -import pg from 'pg'; -import { executeSQL } from '..'; -import { rawSql } from '../../sql'; - -void describe('PostgreSQL connection', () => { - let postgres: StartedPostgreSqlContainer; - let connectionString: string; - - before(async () => { - postgres = await new PostgreSqlContainer().start(); - connectionString = postgres.getConnectionUri(); - }); - - after(async () => { - await postgres.stop(); - }); - - void describe('executeSQL', () => { - void it('connects using pool', async () => { - const pool = new pg.Pool({ connectionString }); - - try { - await executeSQL(pool, rawSql('SELECT 1')); - } catch (error) { - console.log(error); - } finally { - await pool.end(); - } - }); - - void it('connects using connected pool client', async () => { - const pool = new pg.Pool({ connectionString }); - const poolClient = await pool.connect(); - - try { - await executeSQL(poolClient, rawSql('SELECT 1')); - } finally { - poolClient.release(); - await pool.end(); - } - }); - - void it('connects using connected client', async () => { - const client = new pg.Client({ connectionString }); - await client.connect(); - - try { - await executeSQL(client, rawSql('SELECT 1')); - } finally { - await client.end(); - } - }); - }); -}); +// import { +// PostgreSqlContainer, +// type StartedPostgreSqlContainer, +// } from '@testcontainers/postgresql'; +// import { after, before, describe, it } from 'node:test'; +// import pg from 'pg'; +// import { executeSQL } from '..'; +// import { rawSql } from '../../sql'; + +// void describe('PostgreSQL connection', () => { +// let postgres: StartedPostgreSqlContainer; +// let connectionString: string; + +// before(async () => { +// postgres = await new PostgreSqlContainer().start(); +// connectionString = postgres.getConnectionUri(); +// }); + +// after(async () => { +// await postgres.stop(); +// }); + +// void describe('executeSQL', () => { +// void it('connects using pool', async () => { +// const pool = new pg.Pool({ connectionString }); + +// try { +// await executeSQL(pool, rawSql('SELECT 1')); +// } catch (error) { +// console.log(error); +// } finally { +// await pool.end(); +// } +// }); + +// void it('connects using connected pool client', async () => { +// const pool = new pg.Pool({ connectionString }); +// const poolClient = await pool.connect(); + +// try { +// await executeSQL(poolClient, rawSql('SELECT 1')); +// } finally { +// poolClient.release(); +// await pool.end(); +// } +// }); + +// void it('connects using connected client', async () => { +// const client = new pg.Client({ connectionString }); +// await client.connect(); + +// try { +// await executeSQL(client, rawSql('SELECT 1')); +// } finally { +// await client.end(); +// } +// }); +// }); +// }); diff --git a/src/packages/dumbo/src/execute/pg/execute.ts b/src/packages/dumbo/src/execute/pg/execute.ts index fe328cf6..4e96d72f 100644 --- a/src/packages/dumbo/src/execute/pg/execute.ts +++ b/src/packages/dumbo/src/execute/pg/execute.ts @@ -1,10 +1,13 @@ import pg from 'pg'; import { NodePostgresConnectorType, - type NodePostgresConnection, + type NodePostgresClient, + type NodePostgresConnector, + type QueryResult, + type QueryResultRow, + type SQLExecutor, } from '../../connections'; import type { SQL } from '../../sql'; -import type { QueryResult, QueryResultRow, SQLExecutor } from '../execute'; export const isPgPool = ( poolOrClient: pg.Pool | pg.PoolClient | pg.Client, @@ -21,7 +24,7 @@ export const isPgPoolClient = ( ): poolOrClient is pg.PoolClient => 'release' in poolOrClient && typeof poolOrClient.release === 'function'; -export const execute = async ( +export const nodePostgresExecute = async ( poolOrClient: pg.Pool | pg.PoolClient | pg.Client, handle: (client: pg.PoolClient | pg.Client) => Promise, ) => { @@ -37,15 +40,18 @@ export const execute = async ( } }; -export type NodePostgresSQLExecutor = SQLExecutor; +export type NodePostgresSQLExecutor = SQLExecutor< + NodePostgresConnector, + NodePostgresClient +>; export const nodePostgresSQLExecutor = (): NodePostgresSQLExecutor => ({ type: NodePostgresConnectorType, query: async ( - client: Promise | Promise, + client: NodePostgresClient, sql: SQL, ): Promise> => { - const result = await (await client).query(sql); + const result = await client.query(sql); return { rowCount: result.rowCount, rows: result.rows }; }, diff --git a/src/packages/dumbo/src/sql/schema.ts b/src/packages/dumbo/src/sql/schema.ts index 8c6960f9..78db1220 100644 --- a/src/packages/dumbo/src/sql/schema.ts +++ b/src/packages/dumbo/src/sql/schema.ts @@ -1,6 +1,4 @@ -import pg from 'pg'; import { sql, type SQL } from '.'; -import { exists } from '../execute'; export * from './schema'; export const tableExistsSQL = (tableName: string): SQL => @@ -13,10 +11,10 @@ export const tableExistsSQL = (tableName: string): SQL => tableName, ); -export const tableExists = async ( - pool: pg.Pool, - tableName: string, -): Promise => exists(pool, tableExistsSQL(tableName)); +// export const tableExists = async ( +// pool: pg.Pool, +// tableName: string, +// ): Promise => exists(pool, tableExistsSQL(tableName)); export const functionExistsSQL = (functionName: string): SQL => sql( @@ -30,7 +28,7 @@ export const functionExistsSQL = (functionName: string): SQL => functionName, ); -export const functionExists = async ( - pool: pg.Pool, - functionName: string, -): Promise => exists(pool, functionExistsSQL(functionName)); +// export const functionExists = async ( +// pool: pg.Pool, +// functionName: string, +// ): Promise => exists(pool, functionExistsSQL(functionName)); diff --git a/src/packages/dumbo/tsconfig.json b/src/packages/dumbo/tsconfig.json index 55f4a2cd..33a41328 100644 --- a/src/packages/dumbo/tsconfig.json +++ b/src/packages/dumbo/tsconfig.json @@ -1,6 +1,6 @@ { "extends": "../../tsconfig.shared.json", - "include": ["./src/**/*"], + "include": ["./src/**/**/*"], "compilerOptions": { "composite": true, "outDir": "./dist" /* Redirect output structure to the directory. */, diff --git a/src/packages/pongo/package.json b/src/packages/pongo/package.json index 16372950..9247b22f 100644 --- a/src/packages/pongo/package.json +++ b/src/packages/pongo/package.json @@ -47,7 +47,7 @@ "dist" ], "peerDependencies": { - "@event-driven-io/dumbo": "^0.5.0", + "@event-driven-io/dumbo": "^0.6.0", "@types/mongodb": "^4.0.7", "@types/pg": "^8.11.6", "@types/pg-format": "^1.0.5", diff --git a/src/packages/pongo/src/postgres/client.ts b/src/packages/pongo/src/postgres/client.ts index 01e17524..9fc3dfc6 100644 --- a/src/packages/pongo/src/postgres/client.ts +++ b/src/packages/pongo/src/postgres/client.ts @@ -1,9 +1,7 @@ import { - endPool, getDatabaseNameOrDefault, - getPool, + nodePostgresPool, } from '@event-driven-io/dumbo'; -import pg from 'pg'; import { type DbClient, type PongoDbClientOptions, @@ -11,12 +9,7 @@ import { } from '../main'; import { postgresCollection } from './postgresCollection'; -export type PostgresDbClientOptions = PongoDbClientOptions< - 'PostgreSQL', - { - client?: pg.PoolClient | pg.Client | undefined; - } ->; +export type PostgresDbClientOptions = PongoDbClientOptions<'PostgreSQL'>; export const isPostgresClientOptions = ( options: PongoDbClientOptions, @@ -25,22 +18,18 @@ export const isPostgresClientOptions = ( export const postgresDbClient = ( options: PostgresDbClientOptions, ): DbClient => { - const { connectionString, dbName, client } = options; - const managesPoolLifetime = !client; - const poolOrClient = - client ?? getPool({ connectionString, database: dbName }); + const { connectionString, dbName } = options; + + const pool = nodePostgresPool(options); return { options, connect: () => Promise.resolve(), - close: () => - managesPoolLifetime - ? endPool({ connectionString, database: dbName }) - : Promise.resolve(), + close: () => pool.close(), collection: (name: string) => postgresCollection(name, { dbName: dbName ?? getDatabaseNameOrDefault(connectionString), - poolOrClient, + pool, }), }; }; diff --git a/src/packages/pongo/src/postgres/postgresCollection.ts b/src/packages/pongo/src/postgres/postgresCollection.ts index 4ea0a94e..d23bc72e 100644 --- a/src/packages/pongo/src/postgres/postgresCollection.ts +++ b/src/packages/pongo/src/postgres/postgresCollection.ts @@ -1,4 +1,9 @@ -import { executeSQL, single, sql, type SQL } from '@event-driven-io/dumbo'; +import { + single, + sql, + type ConnectionPool, + type SQL, +} from '@event-driven-io/dumbo'; import pg from 'pg'; import format from 'pg-format'; import { v4 as uuid } from 'uuid'; @@ -20,13 +25,11 @@ import { buildUpdateQuery } from './update'; export const postgresCollection = ( collectionName: string, - { - dbName, - poolOrClient: clientOrPool, - }: { dbName: string; poolOrClient: pg.Pool | pg.PoolClient | pg.Client }, + { dbName, pool }: { dbName: string; pool: ConnectionPool }, ): PongoCollection => { const execute = (sql: SQL) => - executeSQL(clientOrPool, sql); + pool.execute.query(sql); + const SqlFor = collectionSQLBuilder(collectionName); const createCollection = execute(SqlFor.createCollection()); From 02f3216c241e6c87a68744bd2567576ab80ad0c9 Mon Sep 17 00:00:00 2001 From: Oskar Dudycz Date: Fri, 2 Aug 2024 12:38:25 +0200 Subject: [PATCH 06/30] Refactored file structure to reflect database types and providers That should allow easier extension with other database types or drivers. --- .../src/{ => core}/connections/connection.ts | 0 .../src/{ => core}/connections/execute.ts | 0 .../dumbo/src/{ => core}/connections/index.ts | 2 - .../dumbo/src/{ => core}/connections/pool.ts | 0 .../src/{ => core}/connections/transaction.ts | 0 .../dumbo/src/{ => core}/execute/execute.ts | 24 ++++---- .../src/{execute/pg => core/execute}/index.ts | 0 src/packages/dumbo/src/core/index.ts | 3 + .../dumbo/src/{ => core}/sql/index.ts | 2 +- .../src/execute/pg/connections.int.spec.ts | 59 ------------------- src/packages/dumbo/src/index.ts | 5 +- .../core}/connections/connectionString.ts | 3 +- .../src/postgres/core/connections/index.ts | 1 + src/packages/dumbo/src/postgres/core/index.ts | 2 + .../dumbo/src/postgres/core/schema/index.ts | 1 + .../dumbo/src/postgres/core/schema/schema.ts | 37 ++++++++++++ src/packages/dumbo/src/postgres/index.ts | 2 + .../pg/connections}/connection.int.spec.ts | 2 +- .../pg/connections}/connection.ts | 10 ++-- .../pg => postgres/pg/connections}/index.ts | 0 .../pg => postgres/pg/connections}/pool.ts | 12 ++-- .../pg/connections}/transaction.ts | 5 +- .../pg => postgres/pg/execute}/execute.ts | 26 ++++---- .../src/{ => postgres/pg}/execute/index.ts | 1 - src/packages/dumbo/src/postgres/pg/index.ts | 2 + src/packages/dumbo/src/sql/schema.ts | 34 ----------- src/packages/pongo/src/main/dbClient.ts | 1 + .../main/pongoClient.connections.e2e.spec.ts | 4 +- src/packages/pongo/src/main/pongoClient.ts | 9 +-- src/packages/pongo/src/postgres/client.ts | 4 +- 30 files changed, 103 insertions(+), 148 deletions(-) rename src/packages/dumbo/src/{ => core}/connections/connection.ts (100%) rename src/packages/dumbo/src/{ => core}/connections/execute.ts (100%) rename src/packages/dumbo/src/{ => core}/connections/index.ts (65%) rename src/packages/dumbo/src/{ => core}/connections/pool.ts (100%) rename src/packages/dumbo/src/{ => core}/connections/transaction.ts (100%) rename src/packages/dumbo/src/{ => core}/execute/execute.ts (95%) rename src/packages/dumbo/src/{execute/pg => core/execute}/index.ts (100%) create mode 100644 src/packages/dumbo/src/core/index.ts rename src/packages/dumbo/src/{ => core}/sql/index.ts (82%) delete mode 100644 src/packages/dumbo/src/execute/pg/connections.int.spec.ts rename src/packages/dumbo/src/{ => postgres/core}/connections/connectionString.ts (76%) create mode 100644 src/packages/dumbo/src/postgres/core/connections/index.ts create mode 100644 src/packages/dumbo/src/postgres/core/index.ts create mode 100644 src/packages/dumbo/src/postgres/core/schema/index.ts create mode 100644 src/packages/dumbo/src/postgres/core/schema/schema.ts create mode 100644 src/packages/dumbo/src/postgres/index.ts rename src/packages/dumbo/src/{connections/pg => postgres/pg/connections}/connection.int.spec.ts (98%) rename src/packages/dumbo/src/{connections/pg => postgres/pg/connections}/connection.ts (92%) rename src/packages/dumbo/src/{connections/pg => postgres/pg/connections}/index.ts (100%) rename src/packages/dumbo/src/{connections/pg => postgres/pg/connections}/pool.ts (97%) rename src/packages/dumbo/src/{connections/pg => postgres/pg/connections}/transaction.ts (86%) rename src/packages/dumbo/src/{execute/pg => postgres/pg/execute}/execute.ts (79%) rename src/packages/dumbo/src/{ => postgres/pg}/execute/index.ts (55%) create mode 100644 src/packages/dumbo/src/postgres/pg/index.ts delete mode 100644 src/packages/dumbo/src/sql/schema.ts diff --git a/src/packages/dumbo/src/connections/connection.ts b/src/packages/dumbo/src/core/connections/connection.ts similarity index 100% rename from src/packages/dumbo/src/connections/connection.ts rename to src/packages/dumbo/src/core/connections/connection.ts diff --git a/src/packages/dumbo/src/connections/execute.ts b/src/packages/dumbo/src/core/connections/execute.ts similarity index 100% rename from src/packages/dumbo/src/connections/execute.ts rename to src/packages/dumbo/src/core/connections/execute.ts diff --git a/src/packages/dumbo/src/connections/index.ts b/src/packages/dumbo/src/core/connections/index.ts similarity index 65% rename from src/packages/dumbo/src/connections/index.ts rename to src/packages/dumbo/src/core/connections/index.ts index 4d32ab97..ae6a3842 100644 --- a/src/packages/dumbo/src/connections/index.ts +++ b/src/packages/dumbo/src/core/connections/index.ts @@ -1,6 +1,4 @@ export * from './connection'; -export * from './connectionString'; export * from './execute'; -export * from './pg'; export * from './pool'; export * from './transaction'; diff --git a/src/packages/dumbo/src/connections/pool.ts b/src/packages/dumbo/src/core/connections/pool.ts similarity index 100% rename from src/packages/dumbo/src/connections/pool.ts rename to src/packages/dumbo/src/core/connections/pool.ts diff --git a/src/packages/dumbo/src/connections/transaction.ts b/src/packages/dumbo/src/core/connections/transaction.ts similarity index 100% rename from src/packages/dumbo/src/connections/transaction.ts rename to src/packages/dumbo/src/core/connections/transaction.ts diff --git a/src/packages/dumbo/src/execute/execute.ts b/src/packages/dumbo/src/core/execute/execute.ts similarity index 95% rename from src/packages/dumbo/src/execute/execute.ts rename to src/packages/dumbo/src/core/execute/execute.ts index 7e85ca90..831820e1 100644 --- a/src/packages/dumbo/src/execute/execute.ts +++ b/src/packages/dumbo/src/core/execute/execute.ts @@ -1,6 +1,5 @@ import type { Connection } from '../connections'; import type { QueryResult, QueryResultRow } from '../connections/execute'; -import type { SQL } from '../sql'; export const execute = async < Result = void, @@ -136,6 +135,16 @@ export const single = async ( return result.rows[0]!; }; +export type ExistsSQLQueryResult = { exists: boolean }; + +export const exists = async ( + getResult: Promise>, +): Promise => { + const result = await single(getResult); + + return result.exists === true; +}; + export const mapRows = async < Result extends QueryResultRow = QueryResultRow, Mapped = unknown, @@ -162,16 +171,3 @@ export const mapToCamelCase = >( } return newObj as T; }; - -export type ExistsSQLQueryResult = { exists: boolean }; - -export const exists = async ( - connection: ConnectionType, - sql: SQL, -): Promise => { - const result = await single( - connection.execute.query(sql), - ); - - return result.exists === true; -}; diff --git a/src/packages/dumbo/src/execute/pg/index.ts b/src/packages/dumbo/src/core/execute/index.ts similarity index 100% rename from src/packages/dumbo/src/execute/pg/index.ts rename to src/packages/dumbo/src/core/execute/index.ts diff --git a/src/packages/dumbo/src/core/index.ts b/src/packages/dumbo/src/core/index.ts new file mode 100644 index 00000000..9a0a1d2b --- /dev/null +++ b/src/packages/dumbo/src/core/index.ts @@ -0,0 +1,3 @@ +export * from './connections'; +export * from './execute'; +export * from './sql'; diff --git a/src/packages/dumbo/src/sql/index.ts b/src/packages/dumbo/src/core/sql/index.ts similarity index 82% rename from src/packages/dumbo/src/sql/index.ts rename to src/packages/dumbo/src/core/sql/index.ts index 6e56a11c..d2d975d9 100644 --- a/src/packages/dumbo/src/sql/index.ts +++ b/src/packages/dumbo/src/core/sql/index.ts @@ -1,5 +1,5 @@ import format from 'pg-format'; -export * from './schema'; +// TODO: add core formatter, when adding other database type export type SQL = string & { __brand: 'sql' }; diff --git a/src/packages/dumbo/src/execute/pg/connections.int.spec.ts b/src/packages/dumbo/src/execute/pg/connections.int.spec.ts deleted file mode 100644 index b6af85b6..00000000 --- a/src/packages/dumbo/src/execute/pg/connections.int.spec.ts +++ /dev/null @@ -1,59 +0,0 @@ -// import { -// PostgreSqlContainer, -// type StartedPostgreSqlContainer, -// } from '@testcontainers/postgresql'; -// import { after, before, describe, it } from 'node:test'; -// import pg from 'pg'; -// import { executeSQL } from '..'; -// import { rawSql } from '../../sql'; - -// void describe('PostgreSQL connection', () => { -// let postgres: StartedPostgreSqlContainer; -// let connectionString: string; - -// before(async () => { -// postgres = await new PostgreSqlContainer().start(); -// connectionString = postgres.getConnectionUri(); -// }); - -// after(async () => { -// await postgres.stop(); -// }); - -// void describe('executeSQL', () => { -// void it('connects using pool', async () => { -// const pool = new pg.Pool({ connectionString }); - -// try { -// await executeSQL(pool, rawSql('SELECT 1')); -// } catch (error) { -// console.log(error); -// } finally { -// await pool.end(); -// } -// }); - -// void it('connects using connected pool client', async () => { -// const pool = new pg.Pool({ connectionString }); -// const poolClient = await pool.connect(); - -// try { -// await executeSQL(poolClient, rawSql('SELECT 1')); -// } finally { -// poolClient.release(); -// await pool.end(); -// } -// }); - -// void it('connects using connected client', async () => { -// const client = new pg.Client({ connectionString }); -// await client.connect(); - -// try { -// await executeSQL(client, rawSql('SELECT 1')); -// } finally { -// await client.end(); -// } -// }); -// }); -// }); diff --git a/src/packages/dumbo/src/index.ts b/src/packages/dumbo/src/index.ts index 9a0a1d2b..680958ef 100644 --- a/src/packages/dumbo/src/index.ts +++ b/src/packages/dumbo/src/index.ts @@ -1,3 +1,2 @@ -export * from './connections'; -export * from './execute'; -export * from './sql'; +export * from './core'; +export * from './postgres'; diff --git a/src/packages/dumbo/src/connections/connectionString.ts b/src/packages/dumbo/src/postgres/core/connections/connectionString.ts similarity index 76% rename from src/packages/dumbo/src/connections/connectionString.ts rename to src/packages/dumbo/src/postgres/core/connections/connectionString.ts index e817accc..67c149cb 100644 --- a/src/packages/dumbo/src/connections/connectionString.ts +++ b/src/packages/dumbo/src/postgres/core/connections/connectionString.ts @@ -1,6 +1,5 @@ import pgcs from 'pg-connection-string'; - -export const defaultPostgreSqlDatabase = 'postgres'; +import { defaultPostgreSqlDatabase } from '../schema'; export const getDatabaseNameOrDefault = (connectionString: string) => pgcs.parse(connectionString).database ?? defaultPostgreSqlDatabase; diff --git a/src/packages/dumbo/src/postgres/core/connections/index.ts b/src/packages/dumbo/src/postgres/core/connections/index.ts new file mode 100644 index 00000000..f6e062b4 --- /dev/null +++ b/src/packages/dumbo/src/postgres/core/connections/index.ts @@ -0,0 +1 @@ +export * from './connectionString'; diff --git a/src/packages/dumbo/src/postgres/core/index.ts b/src/packages/dumbo/src/postgres/core/index.ts new file mode 100644 index 00000000..bc58f08f --- /dev/null +++ b/src/packages/dumbo/src/postgres/core/index.ts @@ -0,0 +1,2 @@ +export * from './connections'; +export * from './schema'; diff --git a/src/packages/dumbo/src/postgres/core/schema/index.ts b/src/packages/dumbo/src/postgres/core/schema/index.ts new file mode 100644 index 00000000..e27a6e2f --- /dev/null +++ b/src/packages/dumbo/src/postgres/core/schema/index.ts @@ -0,0 +1 @@ +export * from './schema'; diff --git a/src/packages/dumbo/src/postgres/core/schema/schema.ts b/src/packages/dumbo/src/postgres/core/schema/schema.ts new file mode 100644 index 00000000..109ff596 --- /dev/null +++ b/src/packages/dumbo/src/postgres/core/schema/schema.ts @@ -0,0 +1,37 @@ +import { exists, type ConnectionPool } from '../../../core'; +import { sql, type SQL } from '../../../core/sql'; +export * from './schema'; + +export const defaultPostgreSqlDatabase = 'postgres'; + +export const tableExistsSQL = (tableName: string): SQL => + sql( + ` + SELECT EXISTS ( + SELECT FROM pg_tables + WHERE tablename = %L + ) AS exists;`, + tableName, + ); + +export const tableExists = async ( + pool: ConnectionPool, + tableName: string, +): Promise => exists(pool.execute.query(tableExistsSQL(tableName))); + +export const functionExistsSQL = (functionName: string): SQL => + sql( + ` + SELECT EXISTS ( + SELECT FROM pg_proc + WHERE + proname = %L + ) AS exists; + `, + functionName, + ); + +export const functionExists = async ( + pool: ConnectionPool, + tableName: string, +): Promise => exists(pool.execute.query(functionExistsSQL(tableName))); diff --git a/src/packages/dumbo/src/postgres/index.ts b/src/packages/dumbo/src/postgres/index.ts new file mode 100644 index 00000000..ab7158f6 --- /dev/null +++ b/src/packages/dumbo/src/postgres/index.ts @@ -0,0 +1,2 @@ +export * from './core'; +export * from './pg'; diff --git a/src/packages/dumbo/src/connections/pg/connection.int.spec.ts b/src/packages/dumbo/src/postgres/pg/connections/connection.int.spec.ts similarity index 98% rename from src/packages/dumbo/src/connections/pg/connection.int.spec.ts rename to src/packages/dumbo/src/postgres/pg/connections/connection.int.spec.ts index 6395454b..0366ed49 100644 --- a/src/packages/dumbo/src/connections/pg/connection.int.spec.ts +++ b/src/packages/dumbo/src/postgres/pg/connections/connection.int.spec.ts @@ -5,7 +5,7 @@ import { import { after, before, describe, it } from 'node:test'; import pg from 'pg'; import { nodePostgresPool } from '.'; -import { rawSql } from '../../sql'; +import { rawSql } from '../../../core'; import { endPool, getPool } from './pool'; void describe('PostgreSQL connection', () => { diff --git a/src/packages/dumbo/src/connections/pg/connection.ts b/src/packages/dumbo/src/postgres/pg/connections/connection.ts similarity index 92% rename from src/packages/dumbo/src/connections/pg/connection.ts rename to src/packages/dumbo/src/postgres/pg/connections/connection.ts index 8b9ffe12..127a1c79 100644 --- a/src/packages/dumbo/src/connections/pg/connection.ts +++ b/src/packages/dumbo/src/postgres/pg/connections/connection.ts @@ -1,8 +1,10 @@ import pg from 'pg'; -import { nodePostgresSQLExecutor } from '../../execute'; -import type { Connection } from '../connection'; -import { withSqlExecutor } from '../execute'; -import { transactionFactory } from '../transaction'; +import { + transactionFactory, + withSqlExecutor, + type Connection, +} from '../../../core'; +import { nodePostgresSQLExecutor } from '../execute'; import { nodePostgresTransaction } from './transaction'; export const NodePostgresConnectorType = 'PostgreSQL:pg'; diff --git a/src/packages/dumbo/src/connections/pg/index.ts b/src/packages/dumbo/src/postgres/pg/connections/index.ts similarity index 100% rename from src/packages/dumbo/src/connections/pg/index.ts rename to src/packages/dumbo/src/postgres/pg/connections/index.ts diff --git a/src/packages/dumbo/src/connections/pg/pool.ts b/src/packages/dumbo/src/postgres/pg/connections/pool.ts similarity index 97% rename from src/packages/dumbo/src/connections/pg/pool.ts rename to src/packages/dumbo/src/postgres/pg/connections/pool.ts index 826975f4..ac247622 100644 --- a/src/packages/dumbo/src/connections/pg/pool.ts +++ b/src/packages/dumbo/src/postgres/pg/connections/pool.ts @@ -1,12 +1,14 @@ import pg from 'pg'; -import { type SQL } from '../../sql'; +import { + queryWithNewConnection, + type ConnectionPool, + type SQL, + type Transaction, +} from '../../../core'; import { defaultPostgreSqlDatabase, getDatabaseNameOrDefault, -} from '../connectionString'; -import { queryWithNewConnection } from '../execute'; -import type { ConnectionPool } from '../pool'; -import type { Transaction } from '../transaction'; +} from '../../core'; import { nodePostgresConnection, NodePostgresConnectorType, diff --git a/src/packages/dumbo/src/connections/pg/transaction.ts b/src/packages/dumbo/src/postgres/pg/connections/transaction.ts similarity index 86% rename from src/packages/dumbo/src/connections/pg/transaction.ts rename to src/packages/dumbo/src/postgres/pg/connections/transaction.ts index b8c94fad..8fa1dc33 100644 --- a/src/packages/dumbo/src/connections/pg/transaction.ts +++ b/src/packages/dumbo/src/postgres/pg/connections/transaction.ts @@ -1,6 +1,5 @@ -import { nodePostgresSQLExecutor } from '../../execute'; -import { withSqlExecutor } from '../execute'; -import type { Transaction } from '../transaction'; +import { withSqlExecutor, type Transaction } from '../../../core'; +import { nodePostgresSQLExecutor } from '../execute'; import { NodePostgresConnectorType, type NodePostgresConnector, diff --git a/src/packages/dumbo/src/execute/pg/execute.ts b/src/packages/dumbo/src/postgres/pg/execute/execute.ts similarity index 79% rename from src/packages/dumbo/src/execute/pg/execute.ts rename to src/packages/dumbo/src/postgres/pg/execute/execute.ts index 4e96d72f..23c34434 100644 --- a/src/packages/dumbo/src/execute/pg/execute.ts +++ b/src/packages/dumbo/src/postgres/pg/execute/execute.ts @@ -1,25 +1,27 @@ import pg from 'pg'; import { - NodePostgresConnectorType, - type NodePostgresClient, - type NodePostgresConnector, type QueryResult, type QueryResultRow, + type SQL, type SQLExecutor, -} from '../../connections'; -import type { SQL } from '../../sql'; +} from '../../../core'; +import { + NodePostgresConnectorType, + type NodePostgresClient, + type NodePostgresConnector, +} from '../connections'; -export const isPgPool = ( +export const isNodePostgresNativePool = ( poolOrClient: pg.Pool | pg.PoolClient | pg.Client, ): poolOrClient is pg.Pool => { return poolOrClient instanceof pg.Pool; }; -export const isPgClient = ( +export const isNodePostgresClient = ( poolOrClient: pg.Pool | pg.PoolClient | pg.Client, ): poolOrClient is pg.Client => poolOrClient instanceof pg.Client; -export const isPgPoolClient = ( +export const isNodePostgresPoolClient = ( poolOrClient: pg.Pool | pg.PoolClient | pg.Client, ): poolOrClient is pg.PoolClient => 'release' in poolOrClient && typeof poolOrClient.release === 'function'; @@ -28,7 +30,7 @@ export const nodePostgresExecute = async ( poolOrClient: pg.Pool | pg.PoolClient | pg.Client, handle: (client: pg.PoolClient | pg.Client) => Promise, ) => { - const client = isPgPool(poolOrClient) + const client = isNodePostgresNativePool(poolOrClient) ? await poolOrClient.connect() : poolOrClient; @@ -36,7 +38,11 @@ export const nodePostgresExecute = async ( return await handle(client); } finally { // release only if client wasn't injected externally - if (isPgPool(poolOrClient) && isPgPoolClient(client)) client.release(); + if ( + isNodePostgresNativePool(poolOrClient) && + isNodePostgresPoolClient(client) + ) + client.release(); } }; diff --git a/src/packages/dumbo/src/execute/index.ts b/src/packages/dumbo/src/postgres/pg/execute/index.ts similarity index 55% rename from src/packages/dumbo/src/execute/index.ts rename to src/packages/dumbo/src/postgres/pg/execute/index.ts index a4a1698f..7941eda6 100644 --- a/src/packages/dumbo/src/execute/index.ts +++ b/src/packages/dumbo/src/postgres/pg/execute/index.ts @@ -1,2 +1 @@ export * from './execute'; -export * from './pg'; diff --git a/src/packages/dumbo/src/postgres/pg/index.ts b/src/packages/dumbo/src/postgres/pg/index.ts new file mode 100644 index 00000000..be2685b8 --- /dev/null +++ b/src/packages/dumbo/src/postgres/pg/index.ts @@ -0,0 +1,2 @@ +export * from './connections'; +export * from './execute'; diff --git a/src/packages/dumbo/src/sql/schema.ts b/src/packages/dumbo/src/sql/schema.ts deleted file mode 100644 index 78db1220..00000000 --- a/src/packages/dumbo/src/sql/schema.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { sql, type SQL } from '.'; -export * from './schema'; - -export const tableExistsSQL = (tableName: string): SQL => - sql( - ` - SELECT EXISTS ( - SELECT FROM pg_tables - WHERE tablename = %L - ) AS exists;`, - tableName, - ); - -// export const tableExists = async ( -// pool: pg.Pool, -// tableName: string, -// ): Promise => exists(pool, tableExistsSQL(tableName)); - -export const functionExistsSQL = (functionName: string): SQL => - sql( - ` - SELECT EXISTS ( - SELECT FROM pg_proc - WHERE - proname = %L - ) AS exists; - `, - functionName, - ); - -// export const functionExists = async ( -// pool: pg.Pool, -// functionName: string, -// ): Promise => exists(pool, functionExistsSQL(functionName)); diff --git a/src/packages/pongo/src/main/dbClient.ts b/src/packages/pongo/src/main/dbClient.ts index ed9e81df..2057da64 100644 --- a/src/packages/pongo/src/main/dbClient.ts +++ b/src/packages/pongo/src/main/dbClient.ts @@ -17,6 +17,7 @@ export type PongoDbClientOptions< export interface DbClient< DbClientOptions extends PongoDbClientOptions = PongoDbClientOptions, > { + databaseName: string; options: DbClientOptions; connect(): Promise; close(): Promise; diff --git a/src/packages/pongo/src/main/pongoClient.connections.e2e.spec.ts b/src/packages/pongo/src/main/pongoClient.connections.e2e.spec.ts index 95f17e5e..e829df41 100644 --- a/src/packages/pongo/src/main/pongoClient.connections.e2e.spec.ts +++ b/src/packages/pongo/src/main/pongoClient.connections.e2e.spec.ts @@ -1,4 +1,4 @@ -import { isPgPool } from '@event-driven-io/dumbo'; +import { isNodePostgresNativePool } from '@event-driven-io/dumbo'; import { PostgreSqlContainer, type StartedPostgreSqlContainer, @@ -31,7 +31,7 @@ void describe('Pongo collection', () => { ) => { const pongo = pongoClient( connectionString, - isPgPool(poolOrClient) + isNodePostgresNativePool(poolOrClient) ? undefined : { client: poolOrClient, diff --git a/src/packages/pongo/src/main/pongoClient.ts b/src/packages/pongo/src/main/pongoClient.ts index c35391fb..3740cffa 100644 --- a/src/packages/pongo/src/main/pongoClient.ts +++ b/src/packages/pongo/src/main/pongoClient.ts @@ -1,4 +1,3 @@ -import { getDatabaseNameOrDefault } from '@event-driven-io/dumbo'; import pg from 'pg'; import type { PostgresDbClientOptions } from '../postgres'; import { @@ -16,17 +15,15 @@ export const pongoClient = < connectionString: string, options: PongoClientOptions = {}, ): PongoClient => { - const defaultDbName = getDatabaseNameOrDefault(connectionString); const dbClients: Map> = new Map(); const dbClient = getDbClient( clientToDbOptions({ connectionString, - dbName: defaultDbName, clientOptions: options, }), ); - dbClients.set(defaultDbName, dbClient); + dbClients.set(dbClient.databaseName, dbClient); const startSession = (): PongoSession => { throw new Error('Not Implemented!'); @@ -53,7 +50,7 @@ export const pongoClient = < getDbClient( clientToDbOptions({ connectionString, - dbName: defaultDbName, + dbName, clientOptions: options, }), ), @@ -76,7 +73,7 @@ export const clientToDbOptions = < DbClientOptions extends AllowedDbClientOptions = AllowedDbClientOptions, >(options: { connectionString: string; - dbName: string; + dbName?: string; clientOptions: PongoClientOptions; }): DbClientOptions => { const postgreSQLOptions: PostgresDbClientOptions = { diff --git a/src/packages/pongo/src/postgres/client.ts b/src/packages/pongo/src/postgres/client.ts index 9fc3dfc6..0b85bea1 100644 --- a/src/packages/pongo/src/postgres/client.ts +++ b/src/packages/pongo/src/postgres/client.ts @@ -19,16 +19,18 @@ export const postgresDbClient = ( options: PostgresDbClientOptions, ): DbClient => { const { connectionString, dbName } = options; + const databaseName = dbName ?? getDatabaseNameOrDefault(connectionString); const pool = nodePostgresPool(options); return { + databaseName, options, connect: () => Promise.resolve(), close: () => pool.close(), collection: (name: string) => postgresCollection(name, { - dbName: dbName ?? getDatabaseNameOrDefault(connectionString), + dbName: databaseName, pool, }), }; From 7604a78f28bcdf6798f778c2be0f21b2aa893d74 Mon Sep 17 00:00:00 2001 From: Oskar Dudycz Date: Fri, 2 Aug 2024 12:47:55 +0200 Subject: [PATCH 07/30] Moved query related types and functions to the dedicated folder --- .../dumbo/src/core/connections/execute.ts | 11 +-- .../dumbo/src/core/execute/execute.ts | 84 ------------------- src/packages/dumbo/src/core/index.ts | 1 + src/packages/dumbo/src/core/query/index.ts | 3 + src/packages/dumbo/src/core/query/mappers.ts | 28 +++++++ src/packages/dumbo/src/core/query/query.ts | 9 ++ .../dumbo/src/core/query/selectors.ts | 57 +++++++++++++ 7 files changed, 99 insertions(+), 94 deletions(-) create mode 100644 src/packages/dumbo/src/core/query/index.ts create mode 100644 src/packages/dumbo/src/core/query/mappers.ts create mode 100644 src/packages/dumbo/src/core/query/query.ts create mode 100644 src/packages/dumbo/src/core/query/selectors.ts diff --git a/src/packages/dumbo/src/core/connections/execute.ts b/src/packages/dumbo/src/core/connections/execute.ts index 166c568d..845e15d0 100644 --- a/src/packages/dumbo/src/core/connections/execute.ts +++ b/src/packages/dumbo/src/core/connections/execute.ts @@ -1,16 +1,7 @@ +import type { QueryResult, QueryResultRow } from '../query'; import { type SQL } from '../sql'; import type { Connection } from './connection'; -export interface QueryResultRow { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - [column: string]: any; -} - -export type QueryResult = { - rowCount: number | null; - rows: Result[]; -}; - export type SQLExecutor< ConnectorType extends string = string, DbClient = unknown, diff --git a/src/packages/dumbo/src/core/execute/execute.ts b/src/packages/dumbo/src/core/execute/execute.ts index 831820e1..f2bb688c 100644 --- a/src/packages/dumbo/src/core/execute/execute.ts +++ b/src/packages/dumbo/src/core/execute/execute.ts @@ -1,5 +1,4 @@ import type { Connection } from '../connections'; -import type { QueryResult, QueryResultRow } from '../connections/execute'; export const execute = async < Result = void, @@ -88,86 +87,3 @@ export const execute = async < // return { success: true, result: undefined }; // }); - -export const firstOrNull = async < - Result extends QueryResultRow = QueryResultRow, ->( - getResult: Promise>, -): Promise => { - const result = await getResult; - - return result.rows.length > 0 ? result.rows[0] ?? null : null; -}; - -export const first = async ( - getResult: Promise>, -): Promise => { - const result = await getResult; - - if (result.rows.length === 0) - throw new Error("Query didn't return any result"); - - return result.rows[0]!; -}; - -export const singleOrNull = async < - Result extends QueryResultRow = QueryResultRow, ->( - getResult: Promise>, -): Promise => { - const result = await getResult; - - if (result.rows.length > 1) throw new Error('Query had more than one result'); - - return result.rows.length > 0 ? result.rows[0] ?? null : null; -}; - -export const single = async ( - getResult: Promise>, -): Promise => { - const result = await getResult; - - if (result.rows.length === 0) - throw new Error("Query didn't return any result"); - - if (result.rows.length > 1) throw new Error('Query had more than one result'); - - return result.rows[0]!; -}; - -export type ExistsSQLQueryResult = { exists: boolean }; - -export const exists = async ( - getResult: Promise>, -): Promise => { - const result = await single(getResult); - - return result.exists === true; -}; - -export const mapRows = async < - Result extends QueryResultRow = QueryResultRow, - Mapped = unknown, ->( - getResult: Promise>, - map: (row: Result) => Mapped, -): Promise => { - const result = await getResult; - - return result.rows.map(map); -}; - -export const toCamelCase = (snakeStr: string): string => - snakeStr.replace(/_([a-z])/g, (g) => g[1]?.toUpperCase() ?? ''); - -export const mapToCamelCase = >( - obj: T, -): T => { - const newObj: Record = {}; - for (const key in obj) { - if (Object.prototype.hasOwnProperty.call(obj, key)) { - newObj[toCamelCase(key)] = obj[key]; - } - } - return newObj as T; -}; diff --git a/src/packages/dumbo/src/core/index.ts b/src/packages/dumbo/src/core/index.ts index 9a0a1d2b..6d98948e 100644 --- a/src/packages/dumbo/src/core/index.ts +++ b/src/packages/dumbo/src/core/index.ts @@ -1,3 +1,4 @@ export * from './connections'; export * from './execute'; +export * from './query'; export * from './sql'; diff --git a/src/packages/dumbo/src/core/query/index.ts b/src/packages/dumbo/src/core/query/index.ts new file mode 100644 index 00000000..f5096775 --- /dev/null +++ b/src/packages/dumbo/src/core/query/index.ts @@ -0,0 +1,3 @@ +export * from './mappers'; +export * from './query'; +export * from './selectors'; diff --git a/src/packages/dumbo/src/core/query/mappers.ts b/src/packages/dumbo/src/core/query/mappers.ts new file mode 100644 index 00000000..6e28035b --- /dev/null +++ b/src/packages/dumbo/src/core/query/mappers.ts @@ -0,0 +1,28 @@ +import type { QueryResult, QueryResultRow } from './query'; + +export const mapRows = async < + Result extends QueryResultRow = QueryResultRow, + Mapped = unknown, +>( + getResult: Promise>, + map: (row: Result) => Mapped, +): Promise => { + const result = await getResult; + + return result.rows.map(map); +}; + +export const toCamelCase = (snakeStr: string): string => + snakeStr.replace(/_([a-z])/g, (g) => g[1]?.toUpperCase() ?? ''); + +export const mapToCamelCase = >( + obj: T, +): T => { + const newObj: Record = {}; + for (const key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + newObj[toCamelCase(key)] = obj[key]; + } + } + return newObj as T; +}; diff --git a/src/packages/dumbo/src/core/query/query.ts b/src/packages/dumbo/src/core/query/query.ts new file mode 100644 index 00000000..1e7fb50d --- /dev/null +++ b/src/packages/dumbo/src/core/query/query.ts @@ -0,0 +1,9 @@ +export interface QueryResultRow { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [column: string]: any; +} + +export type QueryResult = { + rowCount: number | null; + rows: Result[]; +}; diff --git a/src/packages/dumbo/src/core/query/selectors.ts b/src/packages/dumbo/src/core/query/selectors.ts new file mode 100644 index 00000000..580be757 --- /dev/null +++ b/src/packages/dumbo/src/core/query/selectors.ts @@ -0,0 +1,57 @@ +import type { QueryResult, QueryResultRow } from './query'; + +export const firstOrNull = async < + Result extends QueryResultRow = QueryResultRow, +>( + getResult: Promise>, +): Promise => { + const result = await getResult; + + return result.rows.length > 0 ? result.rows[0] ?? null : null; +}; + +export const first = async ( + getResult: Promise>, +): Promise => { + const result = await getResult; + + if (result.rows.length === 0) + throw new Error("Query didn't return any result"); + + return result.rows[0]!; +}; + +export const singleOrNull = async < + Result extends QueryResultRow = QueryResultRow, +>( + getResult: Promise>, +): Promise => { + const result = await getResult; + + if (result.rows.length > 1) throw new Error('Query had more than one result'); + + return result.rows.length > 0 ? result.rows[0] ?? null : null; +}; + +export const single = async ( + getResult: Promise>, +): Promise => { + const result = await getResult; + + if (result.rows.length === 0) + throw new Error("Query didn't return any result"); + + if (result.rows.length > 1) throw new Error('Query had more than one result'); + + return result.rows[0]!; +}; + +export type ExistsSQLQueryResult = { exists: boolean }; + +export const exists = async ( + getResult: Promise>, +): Promise => { + const result = await single(getResult); + + return result.exists === true; +}; From 02459dc3634e306e4821800c1079bbc041834a1c Mon Sep 17 00:00:00 2001 From: Oskar Dudycz Date: Fri, 2 Aug 2024 14:06:50 +0200 Subject: [PATCH 08/30] Added explicit batch queries and commands --- .../dumbo/src/core/connections/connection.ts | 2 +- .../dumbo/src/core/connections/execute.ts | 69 ------ .../dumbo/src/core/connections/index.ts | 1 - .../dumbo/src/core/connections/pool.ts | 2 +- .../dumbo/src/core/connections/transaction.ts | 2 +- .../dumbo/src/core/execute/execute.ts | 228 ++++++++++++------ .../dumbo/src/postgres/pg/connections/pool.ts | 11 +- .../dumbo/src/postgres/pg/execute/execute.ts | 36 ++- 8 files changed, 186 insertions(+), 165 deletions(-) delete mode 100644 src/packages/dumbo/src/core/connections/execute.ts diff --git a/src/packages/dumbo/src/core/connections/connection.ts b/src/packages/dumbo/src/core/connections/connection.ts index 1de26592..74a40c09 100644 --- a/src/packages/dumbo/src/core/connections/connection.ts +++ b/src/packages/dumbo/src/core/connections/connection.ts @@ -1,4 +1,4 @@ -import type { WithSQLExecutor } from './execute'; +import type { WithSQLExecutor } from '../execute'; import type { TransactionFactory } from './transaction'; export type Connection< diff --git a/src/packages/dumbo/src/core/connections/execute.ts b/src/packages/dumbo/src/core/connections/execute.ts deleted file mode 100644 index 845e15d0..00000000 --- a/src/packages/dumbo/src/core/connections/execute.ts +++ /dev/null @@ -1,69 +0,0 @@ -import type { QueryResult, QueryResultRow } from '../query'; -import { type SQL } from '../sql'; -import type { Connection } from './connection'; - -export type SQLExecutor< - ConnectorType extends string = string, - DbClient = unknown, -> = { - type: ConnectorType; - query( - client: DbClient, - sql: SQL, - ): Promise>; -}; - -export type WithSQLExecutor = { - execute: { - query( - sql: SQL, - ): Promise>; - }; -}; - -export const withSqlExecutor = < - DbClient = unknown, - Executor extends SQLExecutor = SQLExecutor, ->( - sqlExecutor: Executor, - options: { - connect: () => Promise; - close?: (client: DbClient, error?: unknown) => Promise; - }, -): WithSQLExecutor => ({ - execute: { - query: async (sql: SQL) => { - const { connect, close } = options; - const client = await connect(); - - try { - const result = await sqlExecutor.query(client, sql); - if (close) await close(client); - return result; - } catch (error) { - if (close) await close(client, error); - - throw error; - } - }, - }, -}); - -export const queryWithNewConnection = async < - ConnectionType extends Connection, - Result extends QueryResultRow = QueryResultRow, ->( - connectionFactory: { - open: () => Promise; - }, - sql: SQL, -) => { - const { open } = connectionFactory; - const connection = await open(); - - try { - return await connection.execute.query(sql); - } finally { - await connection.close(); - } -}; diff --git a/src/packages/dumbo/src/core/connections/index.ts b/src/packages/dumbo/src/core/connections/index.ts index ae6a3842..477c3fed 100644 --- a/src/packages/dumbo/src/core/connections/index.ts +++ b/src/packages/dumbo/src/core/connections/index.ts @@ -1,4 +1,3 @@ export * from './connection'; -export * from './execute'; export * from './pool'; export * from './transaction'; diff --git a/src/packages/dumbo/src/core/connections/pool.ts b/src/packages/dumbo/src/core/connections/pool.ts index ced28e8c..dad54953 100644 --- a/src/packages/dumbo/src/core/connections/pool.ts +++ b/src/packages/dumbo/src/core/connections/pool.ts @@ -1,5 +1,5 @@ +import type { WithSQLExecutor } from '../execute'; import { type Connection } from './connection'; -import type { WithSQLExecutor } from './execute'; import type { TransactionFactory } from './transaction'; export type ConnectionPool = { diff --git a/src/packages/dumbo/src/core/connections/transaction.ts b/src/packages/dumbo/src/core/connections/transaction.ts index 0badf710..04c98af5 100644 --- a/src/packages/dumbo/src/core/connections/transaction.ts +++ b/src/packages/dumbo/src/core/connections/transaction.ts @@ -1,5 +1,5 @@ +import type { WithSQLExecutor } from '../execute'; import { type Connection } from './connection'; -import type { WithSQLExecutor } from './execute'; export type Transaction = { type: ConnectorType; diff --git a/src/packages/dumbo/src/core/execute/execute.ts b/src/packages/dumbo/src/core/execute/execute.ts index f2bb688c..0cf2bbd1 100644 --- a/src/packages/dumbo/src/core/execute/execute.ts +++ b/src/packages/dumbo/src/core/execute/execute.ts @@ -1,78 +1,3 @@ -import type { Connection } from '../connections'; - -export const execute = async < - Result = void, - ConnectionType extends Connection = Connection, ->( - connection: ConnectionType, - handle: (client: ReturnType) => Promise, -) => { - const client = connection.connect(); - - try { - return await handle(client as ReturnType); - } finally { - await connection.close(); - } -}; - -// export const executeInTransaction = async < -// Result = void, -// ConnectionType extends Connection = Connection, -// >( -// connection: ConnectionType, -// handle: ( -// client: ReturnType, -// ) => Promise<{ success: boolean; result: Result }>, -// ): Promise => -// execute(connection, async (client) => { -// const transaction = await connection.transaction(); - -// try { -// const { success, result } = await handle(client); - -// if (success) await transaction.commit(); -// else await transaction.rollback(); - -// return result; -// } catch (e) { -// await transaction.rollback(); -// throw e; -// } -// }); - -// const getExecutor = ( -// _connectorType: ConnectionType['type'], -// ): SQLExecutor => ({ -// type: '', -// query: ( -// _client: ReturnType, -// _queryTextOrConfig: SQL, -// ): Promise> => Promise.reject('Not Implemented!'), -// }); - -// export const executeSQL = async < -// Result extends QueryResultRow = QueryResultRow, -// ConnectionType extends Connection = Connection, -// >( -// connection: ConnectionType, -// sql: SQL, -// ): Promise> => connection.execute.query(sql); - -// export const executeSQLInTransaction = async < -// Result extends QueryResultRow = QueryResultRow, -// ConnectionType extends Connection = Connection, -// >( -// connection: ConnectionType, -// sql: SQL, -// ) => { -// console.log(sql); -// return executeInTransaction(connection, async (client) => ({ -// success: true, -// result: await getExecutor(connection.type).query(client, sql), -// })); -// }; - // export const executeSQLBatchInTransaction = async < // Result extends QueryResultRow = QueryResultRow, // ConnectionType extends Connection = Connection, @@ -84,6 +9,157 @@ export const execute = async < // for (const sql of sqls) { // await getExecutor(connection.type).query(client, sql); // } - // return { success: true, result: undefined }; // }); + +import type { Connection } from '../connections'; +import type { QueryResult, QueryResultRow } from '../query'; +import { type SQL } from '../sql'; + +export type SQLExecutor< + ConnectorType extends string = string, + DbClient = unknown, +> = { + type: ConnectorType; + query( + client: DbClient, + sql: SQL, + ): Promise>; + batchQuery( + client: DbClient, + sqls: SQL[], + ): Promise[]>; + command( + client: DbClient, + sql: SQL, + ): Promise>; + batchCommand( + client: DbClient, + sqls: SQL[], + ): Promise[]>; +}; + +export type WithSQLExecutor = { + execute: { + query( + sql: SQL, + ): Promise>; + batchQuery( + sqls: SQL[], + ): Promise[]>; + command( + sql: SQL, + ): Promise>; + batchCommand( + sqls: SQL[], + ): Promise[]>; + }; +}; + +export const withSqlExecutor = < + DbClient = unknown, + Executor extends SQLExecutor = SQLExecutor, +>( + sqlExecutor: Executor, + // TODO: In the longer term we should have different options for query and command + options: { + connect: () => Promise; + close?: (client: DbClient, error?: unknown) => Promise; + }, +): WithSQLExecutor => { + return { + execute: { + query: (sql) => + executeInNewDbClient( + (client) => sqlExecutor.query(client, sql), + options, + ), + batchQuery: (sqls) => + executeInNewDbClient( + (client) => sqlExecutor.batchQuery(client, sqls), + options, + ), + command: (sql) => + executeInNewDbClient( + (client) => sqlExecutor.command(client, sql), + options, + ), + batchCommand: (sqls) => + executeInNewDbClient( + (client) => sqlExecutor.batchQuery(client, sqls), + options, + ), + }, + }; +}; + +export const withSqlExecutorInNewConnection = < + ConnectionType extends Connection, +>(options: { + open: () => Promise; +}): WithSQLExecutor => { + return { + execute: { + query: (sql) => + executeInNewConnection( + (connection) => connection.execute.query(sql), + options, + ), + batchQuery: (sqls) => + executeInNewConnection( + (connection) => connection.execute.batchQuery(sqls), + options, + ), + command: (sql) => + executeInNewConnection( + (connection) => connection.execute.command(sql), + options, + ), + batchCommand: (sqls) => + executeInNewConnection( + (connection) => connection.execute.batchCommand(sqls), + options, + ), + }, + }; +}; + +export const executeInNewDbClient = async < + DbClient = unknown, + Result = unknown, +>( + handle: (client: DbClient) => Promise, + options: { + connect: () => Promise; + close?: (client: DbClient, error?: unknown) => Promise; + }, +): Promise => { + const { connect, close } = options; + const client = await connect(); + try { + return await handle(client); + } catch (error) { + if (close) await close(client, error); + + throw error; + } +}; + +export const executeInNewConnection = async < + ConnectionType extends Connection, + Result extends QueryResultRow = QueryResultRow, +>( + handle: (connection: ConnectionType) => Promise, + options: { + open: () => Promise; + }, +) => { + const { open } = options; + const connection = await open(); + + try { + return await handle(connection); + } finally { + await connection.close(); + } +}; diff --git a/src/packages/dumbo/src/postgres/pg/connections/pool.ts b/src/packages/dumbo/src/postgres/pg/connections/pool.ts index ac247622..3e5d082b 100644 --- a/src/packages/dumbo/src/postgres/pg/connections/pool.ts +++ b/src/packages/dumbo/src/postgres/pg/connections/pool.ts @@ -1,8 +1,7 @@ import pg from 'pg'; import { - queryWithNewConnection, + withSqlExecutorInNewConnection, type ConnectionPool, - type SQL, type Transaction, } from '../../../core'; import { @@ -52,9 +51,7 @@ export const nodePostgresNativePool = (options: { type: NodePostgresConnectorType, open, close, - execute: { - query: async (sql: SQL) => queryWithNewConnection({ open }, sql), - }, + ...withSqlExecutorInNewConnection({ open }), transaction: () => getConnection().transaction(), inTransaction: async ( handle: ( @@ -104,9 +101,7 @@ export const nodePostgresExplicitClientPool = (options: { type: NodePostgresConnectorType, open, close, - execute: { - query: (sql: SQL) => queryWithNewConnection({ open }, sql), - }, + ...withSqlExecutorInNewConnection({ open }), transaction: () => getConnection().transaction(), inTransaction: async ( handle: ( diff --git a/src/packages/dumbo/src/postgres/pg/execute/execute.ts b/src/packages/dumbo/src/postgres/pg/execute/execute.ts index 23c34434..03b581be 100644 --- a/src/packages/dumbo/src/postgres/pg/execute/execute.ts +++ b/src/packages/dumbo/src/postgres/pg/execute/execute.ts @@ -53,12 +53,32 @@ export type NodePostgresSQLExecutor = SQLExecutor< export const nodePostgresSQLExecutor = (): NodePostgresSQLExecutor => ({ type: NodePostgresConnectorType, - query: async ( - client: NodePostgresClient, - sql: SQL, - ): Promise> => { - const result = await client.query(sql); - - return { rowCount: result.rowCount, rows: result.rows }; - }, + query: batch, + batchQuery: batch, + command: batch, + batchCommand: batch, }); + +function batch( + client: NodePostgresClient, + sqlOrSqls: SQL, +): Promise>; +function batch( + client: NodePostgresClient, + sqlOrSqls: SQL[], +): Promise[]>; +async function batch( + client: NodePostgresClient, + sqlOrSqls: SQL | SQL[], +): Promise | QueryResult[]> { + const sqls = Array.isArray(sqlOrSqls) ? sqlOrSqls : [sqlOrSqls]; + const results: QueryResult[] = Array>( + sqls.length, + ); + //TODO: make it smarter at some point + for (let i = 0; i < sqls.length; i++) { + const result = await client.query(sqls[i]!); + results[i] = { rowCount: result.rowCount, rows: result.rows }; + } + return Array.isArray(sqlOrSqls) ? results : results[0]!; +} From 9116fc592c9d6b7725f79abf10c3a9881ad7a53a Mon Sep 17 00:00:00 2001 From: Oskar Dudycz Date: Fri, 2 Aug 2024 14:19:31 +0200 Subject: [PATCH 09/30] Added typing for NodePostgres pool options --- .../dumbo/src/postgres/pg/connections/pool.ts | 93 +++++++++++-------- src/packages/pongo/src/mongo/mongoClient.ts | 12 +-- .../pongo/src/postgres/postgresCollection.ts | 4 +- 3 files changed, 62 insertions(+), 47 deletions(-) diff --git a/src/packages/dumbo/src/postgres/pg/connections/pool.ts b/src/packages/dumbo/src/postgres/pg/connections/pool.ts index 3e5d082b..8e437a03 100644 --- a/src/packages/dumbo/src/postgres/pg/connections/pool.ts +++ b/src/packages/dumbo/src/postgres/pg/connections/pool.ts @@ -118,52 +118,67 @@ export const nodePostgresExplicitClientPool = (options: { }; }; -export function nodePostgresPool(options: { - connectionString: string; - database?: string; - type: 'pooled'; - pool: pg.Pool; -}): NodePostgresNativePool; -export function nodePostgresPool(options: { - connectionString: string; - database?: string; - pool: pg.Pool; -}): NodePostgresNativePool; -export function nodePostgresPool(options: { - connectionString: string; - database?: string; - type: 'pooled'; -}): NodePostgresNativePool; -export function nodePostgresPool(options: { - connectionString: string; - database?: string; -}): NodePostgresNativePool; -export function nodePostgresPool(options: { - connectionString: string; - database?: string; - type: 'client'; - client: pg.Client; -}): NodePostgresExplicitClientPool; -export function nodePostgresPool(options: { - connectionString: string; - database?: string; - client: pg.Client; -}): NodePostgresExplicitClientPool; +export type NodePostgresPoolPooledOptions = + | { + connectionString: string; + database?: string; + type: 'pooled'; + pool: pg.Pool; + } + | { + connectionString: string; + database?: string; + pool: pg.Pool; + } + | { + connectionString: string; + database?: string; + type: 'pooled'; + } + | { + connectionString: string; + database?: string; + }; + +export type NodePostgresPoolNotPooledOptions = + | { + connectionString: string; + database?: string; + type: 'client'; + client: pg.Client; + } + | { + connectionString: string; + database?: string; + client: pg.Client; + } + | { + connectionString: string; + database?: string; + type: 'client'; + }; + +export type NodePostgresPoolOptions = + | NodePostgresPoolPooledOptions + | NodePostgresPoolNotPooledOptions; + +export function nodePostgresPool( + options: NodePostgresPoolPooledOptions, +): NodePostgresNativePool; +export function nodePostgresPool( + options: NodePostgresPoolNotPooledOptions, +): NodePostgresExplicitClientPool; export function nodePostgresPool(options: { connectionString: string; database?: string; type: 'client'; }): NodePostgresExplicitClientPool; -export function nodePostgresPool(options: { - connectionString: string; - database?: string; - type?: 'pooled' | 'client'; - pool?: pg.Pool; - client?: pg.Client; -}): NodePostgresNativePool | NodePostgresExplicitClientPool { +export function nodePostgresPool( + options: NodePostgresPoolOptions, +): NodePostgresNativePool | NodePostgresExplicitClientPool { const { connectionString, database } = options; - if (options.type === 'client' || 'client' in options) + if (('type' in options && options.type === 'client') || 'client' in options) return nodePostgresExplicitClientPool({ connectionString, ...(database ? { database } : {}), diff --git a/src/packages/pongo/src/mongo/mongoClient.ts b/src/packages/pongo/src/mongo/mongoClient.ts index 6ca80e1f..31dfef2d 100644 --- a/src/packages/pongo/src/mongo/mongoClient.ts +++ b/src/packages/pongo/src/mongo/mongoClient.ts @@ -1,16 +1,16 @@ import type { ClientSessionOptions } from 'http2'; import type { ClientSession, WithSessionCallback } from 'mongodb'; -import pg from 'pg'; -import { pongoClient, type PongoClient } from '../main'; +import { + pongoClient, + type PongoClient, + type PongoClientOptions, +} from '../main'; import { Db } from './mongoDb'; export class MongoClient { private pongoClient: PongoClient; - constructor( - connectionString: string, - options: { client?: pg.PoolClient | pg.Client } = {}, - ) { + constructor(connectionString: string, options: PongoClientOptions = {}) { this.pongoClient = pongoClient(connectionString, options); } diff --git a/src/packages/pongo/src/postgres/postgresCollection.ts b/src/packages/pongo/src/postgres/postgresCollection.ts index d23bc72e..43a88b16 100644 --- a/src/packages/pongo/src/postgres/postgresCollection.ts +++ b/src/packages/pongo/src/postgres/postgresCollection.ts @@ -2,9 +2,9 @@ import { single, sql, type ConnectionPool, + type QueryResultRow, type SQL, } from '@event-driven-io/dumbo'; -import pg from 'pg'; import format from 'pg-format'; import { v4 as uuid } from 'uuid'; import { @@ -27,7 +27,7 @@ export const postgresCollection = ( collectionName: string, { dbName, pool }: { dbName: string; pool: ConnectionPool }, ): PongoCollection => { - const execute = (sql: SQL) => + const execute = (sql: SQL) => pool.execute.query(sql); const SqlFor = collectionSQLBuilder(collectionName); From db712ad848b89bf912dedc9daeaef4e128759026 Mon Sep 17 00:00:00 2001 From: Oskar Dudycz Date: Fri, 2 Aug 2024 14:24:59 +0200 Subject: [PATCH 10/30] Generalised pool settings --- src/packages/dumbo/src/index.ts | 6 ++++ src/packages/dumbo/src/postgres/index.ts | 4 +++ .../pg/connections/connection.int.spec.ts | 2 +- .../dumbo/src/postgres/pg/connections/pool.ts | 15 +++----- src/packages/pongo/src/main/pongoClient.ts | 34 ++++++++++++++++--- src/packages/pongo/src/postgres/client.ts | 8 +++-- 6 files changed, 51 insertions(+), 18 deletions(-) diff --git a/src/packages/dumbo/src/index.ts b/src/packages/dumbo/src/index.ts index 680958ef..3e67549b 100644 --- a/src/packages/dumbo/src/index.ts +++ b/src/packages/dumbo/src/index.ts @@ -1,2 +1,8 @@ +import { postgresPool, type NodePostgresPoolOptions } from './postgres'; + export * from './core'; export * from './postgres'; + +export type PoolOptions = NodePostgresPoolOptions; +export const connectionPool = (_type: 'PostgreSQL', options: PoolOptions) => + postgresPool(options); diff --git a/src/packages/dumbo/src/postgres/index.ts b/src/packages/dumbo/src/postgres/index.ts index ab7158f6..3b4d2110 100644 --- a/src/packages/dumbo/src/postgres/index.ts +++ b/src/packages/dumbo/src/postgres/index.ts @@ -1,2 +1,6 @@ export * from './core'; export * from './pg'; +import { type NodePostgresPoolOptions, nodePostgresPool } from './pg'; + +export type PostgresPoolOptions = NodePostgresPoolOptions; +export const postgresPool = nodePostgresPool; diff --git a/src/packages/dumbo/src/postgres/pg/connections/connection.int.spec.ts b/src/packages/dumbo/src/postgres/pg/connections/connection.int.spec.ts index 0366ed49..1408cedf 100644 --- a/src/packages/dumbo/src/postgres/pg/connections/connection.int.spec.ts +++ b/src/packages/dumbo/src/postgres/pg/connections/connection.int.spec.ts @@ -53,7 +53,7 @@ void describe('PostgreSQL connection', () => { void it('connects using client', async () => { const pool = nodePostgresPool({ connectionString, - type: 'client', + pooled: false, }); const connection = await pool.open(); diff --git a/src/packages/dumbo/src/postgres/pg/connections/pool.ts b/src/packages/dumbo/src/postgres/pg/connections/pool.ts index 8e437a03..28fef78b 100644 --- a/src/packages/dumbo/src/postgres/pg/connections/pool.ts +++ b/src/packages/dumbo/src/postgres/pg/connections/pool.ts @@ -122,7 +122,7 @@ export type NodePostgresPoolPooledOptions = | { connectionString: string; database?: string; - type: 'pooled'; + pooled: true; pool: pg.Pool; } | { @@ -133,7 +133,7 @@ export type NodePostgresPoolPooledOptions = | { connectionString: string; database?: string; - type: 'pooled'; + pooled: true; } | { connectionString: string; @@ -144,7 +144,7 @@ export type NodePostgresPoolNotPooledOptions = | { connectionString: string; database?: string; - type: 'client'; + pooled: false; client: pg.Client; } | { @@ -155,7 +155,7 @@ export type NodePostgresPoolNotPooledOptions = | { connectionString: string; database?: string; - type: 'client'; + pooled: false; }; export type NodePostgresPoolOptions = @@ -168,17 +168,12 @@ export function nodePostgresPool( export function nodePostgresPool( options: NodePostgresPoolNotPooledOptions, ): NodePostgresExplicitClientPool; -export function nodePostgresPool(options: { - connectionString: string; - database?: string; - type: 'client'; -}): NodePostgresExplicitClientPool; export function nodePostgresPool( options: NodePostgresPoolOptions, ): NodePostgresNativePool | NodePostgresExplicitClientPool { const { connectionString, database } = options; - if (('type' in options && options.type === 'client') || 'client' in options) + if (('pooled' in options && options.pooled === false) || 'client' in options) return nodePostgresExplicitClientPool({ connectionString, ...(database ? { database } : {}), diff --git a/src/packages/pongo/src/main/pongoClient.ts b/src/packages/pongo/src/main/pongoClient.ts index 3740cffa..bb64915f 100644 --- a/src/packages/pongo/src/main/pongoClient.ts +++ b/src/packages/pongo/src/main/pongoClient.ts @@ -7,7 +7,35 @@ import { } from './dbClient'; import type { PongoClient, PongoDb, PongoSession } from './typing/operations'; -export type PongoClientOptions = { client?: pg.PoolClient | pg.Client }; +export type PooledPongoClientOptions = + | { + pool: pg.Pool; + } + | { + pooled: true; + } + | { + pool: pg.Pool; + pooled: true; + } + // eslint-disable-next-line @typescript-eslint/ban-types + | {}; + +export type NotPooledPongoOptions = + | { + client: pg.Client; + } + | { + pooled: false; + } + | { + client: pg.Client; + pooled: false; + }; + +export type PongoClientOptions = + | PooledPongoClientOptions + | NotPooledPongoOptions; export const pongoClient = < DbClientOptions extends AllowedDbClientOptions = AllowedDbClientOptions, @@ -80,9 +108,7 @@ export const clientToDbOptions = < type: 'PostgreSQL', connectionString: options.connectionString, dbName: options.dbName, - ...(options.clientOptions.client - ? { client: options.clientOptions.client } - : {}), + ...options.clientOptions, }; return postgreSQLOptions as DbClientOptions; diff --git a/src/packages/pongo/src/postgres/client.ts b/src/packages/pongo/src/postgres/client.ts index 0b85bea1..215f60f0 100644 --- a/src/packages/pongo/src/postgres/client.ts +++ b/src/packages/pongo/src/postgres/client.ts @@ -1,6 +1,7 @@ import { getDatabaseNameOrDefault, - nodePostgresPool, + postgresPool, + type PostgresPoolOptions, } from '@event-driven-io/dumbo'; import { type DbClient, @@ -9,7 +10,8 @@ import { } from '../main'; import { postgresCollection } from './postgresCollection'; -export type PostgresDbClientOptions = PongoDbClientOptions<'PostgreSQL'>; +export type PostgresDbClientOptions = PongoDbClientOptions<'PostgreSQL'> & + PostgresPoolOptions; export const isPostgresClientOptions = ( options: PongoDbClientOptions, @@ -21,7 +23,7 @@ export const postgresDbClient = ( const { connectionString, dbName } = options; const databaseName = dbName ?? getDatabaseNameOrDefault(connectionString); - const pool = nodePostgresPool(options); + const pool = postgresPool(options); return { databaseName, From 02b478ba37277408fba39a7104ba9634edb1ceae Mon Sep 17 00:00:00 2001 From: Oskar Dudycz Date: Fri, 2 Aug 2024 15:32:02 +0200 Subject: [PATCH 11/30] Added dummy session implementation --- src/packages/pongo/src/main/pongoClient.ts | 17 ++- src/packages/pongo/src/main/pongoSession.ts | 109 +++++++++++++++++- .../pongo/src/main/typing/operations.ts | 14 +-- 3 files changed, 122 insertions(+), 18 deletions(-) diff --git a/src/packages/pongo/src/main/pongoClient.ts b/src/packages/pongo/src/main/pongoClient.ts index bb64915f..69547f38 100644 --- a/src/packages/pongo/src/main/pongoClient.ts +++ b/src/packages/pongo/src/main/pongoClient.ts @@ -5,6 +5,7 @@ import { type AllowedDbClientOptions, type DbClient, } from './dbClient'; +import { pongoSession } from './pongoSession'; import type { PongoClient, PongoDb, PongoSession } from './typing/operations'; export type PooledPongoClientOptions = @@ -86,11 +87,17 @@ export const pongoClient = < .get(dbName)! ); }, - startSession, - withSession( - _callback: (session: PongoSession) => Promise, - ): Promise { - return Promise.reject('Not Implemented!'); + startSession: pongoSession, + withSession: async ( + callback: (session: PongoSession) => Promise, + ): Promise => { + const session = pongoSession(); + + try { + return await callback(session); + } finally { + await session.endSession(); + } }, }; diff --git a/src/packages/pongo/src/main/pongoSession.ts b/src/packages/pongo/src/main/pongoSession.ts index 4918d8fd..f3dc420b 100644 --- a/src/packages/pongo/src/main/pongoSession.ts +++ b/src/packages/pongo/src/main/pongoSession.ts @@ -1,8 +1,105 @@ -import type { PongoClientOptions } from './pongoClient'; -import type { PongoSession } from './typing'; +import type { + PongoSession, + PongoTransaction, + PongoTransactionOptions, +} from './typing'; -export const pongoSession = ( - _clientOptions: PongoClientOptions, -): PongoSession => { - throw new Error('Not Implemented!'); +export type PongoSessionOptions = { + explicit?: boolean; + defaultTransactionOptions: PongoTransactionOptions; +}; + +export const pongoSession = (options?: PongoSessionOptions): PongoSession => { + const explicit = options?.explicit === true; + const defaultTransactionOptions: PongoTransactionOptions = + options?.defaultTransactionOptions ?? { + get snapshotEnabled() { + return false; + }, + }; + + let transaction: PongoTransaction | null = null; + let hasEnded = false; + + const startTransaction = (options?: PongoTransactionOptions) => { + if (transaction?.isActive === true) + throw new Error('Active transaction already exists!'); + + transaction = { + isStarting: false, + isActive: true, + isCommitted: false, + options: options ?? defaultTransactionOptions, + }; + }; + const commitTransaction = () => { + if (transaction?.isActive !== true) + return Promise.reject('No active transaction!'); + + transaction = { + isStarting: false, + isActive: false, + isCommitted: true, + options: transaction.options, + }; + return Promise.resolve(); + }; + const abortTransaction = () => { + if (transaction?.isActive !== true) + return Promise.reject('No active transaction!'); + + transaction = { + isStarting: false, + isActive: false, + isCommitted: false, + options: transaction.options, + }; + return Promise.resolve(); + }; + + const session = { + get hasEnded() { + return hasEnded; + }, + explicit, + defaultTransactionOptions: defaultTransactionOptions ?? { + get snapshotEnabled() { + return false; + }, + }, + get transaction() { + return transaction; + }, + get snapshotEnabled() { + return defaultTransactionOptions.snapshotEnabled; + }, + endSession: (): Promise => { + if (hasEnded) return Promise.resolve(); + hasEnded = true; + + return Promise.resolve(); + }, + incrementTransactionNumber: () => {}, + inTransaction: () => transaction !== null, + startTransaction, + commitTransaction, + abortTransaction, + withTransaction: async ( + fn: (session: PongoSession) => Promise, + options?: PongoTransactionOptions, + ): Promise => { + startTransaction(options); + + try { + const result = await fn(session); + await commitTransaction(); + return result; + } catch (error) { + await abortTransaction(); + throw error; + } + }, + }; + + return session; }; diff --git a/src/packages/pongo/src/main/typing/operations.ts b/src/packages/pongo/src/main/typing/operations.ts index 6686d2bb..7cc12fa7 100644 --- a/src/packages/pongo/src/main/typing/operations.ts +++ b/src/packages/pongo/src/main/typing/operations.ts @@ -12,13 +12,13 @@ export interface PongoClient { ): Promise; } -export declare interface TransactionOptions { +export declare interface PongoTransactionOptions { get snapshotEnabled(): boolean; maxCommitTimeMS?: number; } -export interface Transaction { - options: TransactionOptions; +export interface PongoTransaction { + options: PongoTransactionOptions; get isStarting(): boolean; get isActive(): boolean; get isCommitted(): boolean; @@ -27,19 +27,19 @@ export interface Transaction { export interface PongoSession { hasEnded: boolean; explicit: boolean; - defaultTransactionOptions: TransactionOptions; - transaction: Transaction; + defaultTransactionOptions: PongoTransactionOptions; + transaction: PongoTransaction | null; get snapshotEnabled(): boolean; endSession(): Promise; incrementTransactionNumber(): void; inTransaction(): boolean; - startTransaction(options?: TransactionOptions): void; + startTransaction(options?: PongoTransactionOptions): void; commitTransaction(): Promise; abortTransaction(): Promise; withTransaction( fn: (session: PongoSession) => Promise, - options?: TransactionOptions, + options?: PongoTransactionOptions, ): Promise; } From 3cd26297a8d9656797675cf8f64dd69f01956368 Mon Sep 17 00:00:00 2001 From: Oskar Dudycz Date: Fri, 2 Aug 2024 15:38:58 +0200 Subject: [PATCH 12/30] Extended PongoTransaction to have reference to current db and database transaction --- src/packages/pongo/src/main/pongoClient.ts | 4 ---- src/packages/pongo/src/main/pongoSession.ts | 6 ++++++ src/packages/pongo/src/main/typing/operations.ts | 4 ++++ 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/packages/pongo/src/main/pongoClient.ts b/src/packages/pongo/src/main/pongoClient.ts index 69547f38..ad001982 100644 --- a/src/packages/pongo/src/main/pongoClient.ts +++ b/src/packages/pongo/src/main/pongoClient.ts @@ -54,10 +54,6 @@ export const pongoClient = < ); dbClients.set(dbClient.databaseName, dbClient); - const startSession = (): PongoSession => { - throw new Error('Not Implemented!'); - }; - const pongoClient: PongoClient = { connect: async () => { await dbClient.connect(); diff --git a/src/packages/pongo/src/main/pongoSession.ts b/src/packages/pongo/src/main/pongoSession.ts index f3dc420b..92729aad 100644 --- a/src/packages/pongo/src/main/pongoSession.ts +++ b/src/packages/pongo/src/main/pongoSession.ts @@ -26,6 +26,8 @@ export const pongoSession = (options?: PongoSessionOptions): PongoSession => { throw new Error('Active transaction already exists!'); transaction = { + db: null, + transaction: null, isStarting: false, isActive: true, isCommitted: false, @@ -37,6 +39,8 @@ export const pongoSession = (options?: PongoSessionOptions): PongoSession => { return Promise.reject('No active transaction!'); transaction = { + db: null, + transaction: null, isStarting: false, isActive: false, isCommitted: true, @@ -49,6 +53,8 @@ export const pongoSession = (options?: PongoSessionOptions): PongoSession => { return Promise.reject('No active transaction!'); transaction = { + db: null, + transaction: null, isStarting: false, isActive: false, isCommitted: false, diff --git a/src/packages/pongo/src/main/typing/operations.ts b/src/packages/pongo/src/main/typing/operations.ts index 7cc12fa7..b5b07188 100644 --- a/src/packages/pongo/src/main/typing/operations.ts +++ b/src/packages/pongo/src/main/typing/operations.ts @@ -1,3 +1,5 @@ +import type { Transaction } from '@event-driven-io/dumbo'; + export interface PongoClient { connect(): Promise; @@ -18,6 +20,8 @@ export declare interface PongoTransactionOptions { } export interface PongoTransaction { + db: PongoDb | null; + transaction: Transaction | null; options: PongoTransactionOptions; get isStarting(): boolean; get isActive(): boolean; From 01dc3a0c392c60115e624bda04206668f6c02d83 Mon Sep 17 00:00:00 2001 From: Oskar Dudycz Date: Sat, 3 Aug 2024 10:00:55 +0200 Subject: [PATCH 13/30] Exposed SQLExecturo that's not aware of client existence --- .../dumbo/src/core/execute/execute.ts | 34 ++++++++++--------- .../dumbo/src/postgres/pg/execute/execute.ts | 4 +-- src/packages/pongo/src/main/dbClient.ts | 2 +- 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/src/packages/dumbo/src/core/execute/execute.ts b/src/packages/dumbo/src/core/execute/execute.ts index 0cf2bbd1..b32dde96 100644 --- a/src/packages/dumbo/src/core/execute/execute.ts +++ b/src/packages/dumbo/src/core/execute/execute.ts @@ -16,7 +16,7 @@ import type { Connection } from '../connections'; import type { QueryResult, QueryResultRow } from '../query'; import { type SQL } from '../sql'; -export type SQLExecutor< +export type DbSQLExecutor< ConnectorType extends string = string, DbClient = unknown, > = { @@ -39,26 +39,28 @@ export type SQLExecutor< ): Promise[]>; }; +export type SQLExecutor = { + query( + sql: SQL, + ): Promise>; + batchQuery( + sqls: SQL[], + ): Promise[]>; + command( + sql: SQL, + ): Promise>; + batchCommand( + sqls: SQL[], + ): Promise[]>; +}; + export type WithSQLExecutor = { - execute: { - query( - sql: SQL, - ): Promise>; - batchQuery( - sqls: SQL[], - ): Promise[]>; - command( - sql: SQL, - ): Promise>; - batchCommand( - sqls: SQL[], - ): Promise[]>; - }; + execute: SQLExecutor; }; export const withSqlExecutor = < DbClient = unknown, - Executor extends SQLExecutor = SQLExecutor, + Executor extends DbSQLExecutor = DbSQLExecutor, >( sqlExecutor: Executor, // TODO: In the longer term we should have different options for query and command diff --git a/src/packages/dumbo/src/postgres/pg/execute/execute.ts b/src/packages/dumbo/src/postgres/pg/execute/execute.ts index 03b581be..ef4bf39b 100644 --- a/src/packages/dumbo/src/postgres/pg/execute/execute.ts +++ b/src/packages/dumbo/src/postgres/pg/execute/execute.ts @@ -1,9 +1,9 @@ import pg from 'pg'; import { + type DbSQLExecutor, type QueryResult, type QueryResultRow, type SQL, - type SQLExecutor, } from '../../../core'; import { NodePostgresConnectorType, @@ -46,7 +46,7 @@ export const nodePostgresExecute = async ( } }; -export type NodePostgresSQLExecutor = SQLExecutor< +export type NodePostgresSQLExecutor = DbSQLExecutor< NodePostgresConnector, NodePostgresClient >; diff --git a/src/packages/pongo/src/main/dbClient.ts b/src/packages/pongo/src/main/dbClient.ts index 2057da64..d85a9169 100644 --- a/src/packages/pongo/src/main/dbClient.ts +++ b/src/packages/pongo/src/main/dbClient.ts @@ -3,7 +3,7 @@ import { postgresDbClient, type PostgresDbClientOptions, } from '../postgres'; -import type { PongoCollection, PongoDocument } from './typing/operations'; +import type { PongoCollection, PongoDocument } from './typing'; export type PongoDbClientOptions< DbType extends string = string, From 8ebecb09c19e95bce11f99780d6d24ab3fcc2a89 Mon Sep 17 00:00:00 2001 From: Oskar Dudycz Date: Sat, 3 Aug 2024 10:14:47 +0200 Subject: [PATCH 14/30] Refactored the executor setup --- .../dumbo/src/core/execute/execute.ts | 112 +++++++----------- .../src/postgres/pg/connections/connection.ts | 6 +- .../dumbo/src/postgres/pg/connections/pool.ts | 6 +- .../postgres/pg/connections/transaction.ts | 4 +- 4 files changed, 50 insertions(+), 78 deletions(-) diff --git a/src/packages/dumbo/src/core/execute/execute.ts b/src/packages/dumbo/src/core/execute/execute.ts index b32dde96..2aa260c4 100644 --- a/src/packages/dumbo/src/core/execute/execute.ts +++ b/src/packages/dumbo/src/core/execute/execute.ts @@ -1,17 +1,3 @@ -// export const executeSQLBatchInTransaction = async < -// Result extends QueryResultRow = QueryResultRow, -// ConnectionType extends Connection = Connection, -// >( -// connection: ConnectionType, -// ...sqls: SQL[] -// ) => -// executeInTransaction(connection, async (client) => { -// for (const sql of sqls) { -// await getExecutor(connection.type).query(client, sql); -// } -// return { success: true, result: undefined }; -// }); - import type { Connection } from '../connections'; import type { QueryResult, QueryResultRow } from '../query'; import { type SQL } from '../sql'; @@ -58,73 +44,59 @@ export type WithSQLExecutor = { execute: SQLExecutor; }; -export const withSqlExecutor = < +export const sqlExecutor = < DbClient = unknown, - Executor extends DbSQLExecutor = DbSQLExecutor, + DbExecutor extends DbSQLExecutor = DbSQLExecutor, >( - sqlExecutor: Executor, + sqlExecutor: DbExecutor, // TODO: In the longer term we should have different options for query and command options: { connect: () => Promise; close?: (client: DbClient, error?: unknown) => Promise; }, -): WithSQLExecutor => { - return { - execute: { - query: (sql) => - executeInNewDbClient( - (client) => sqlExecutor.query(client, sql), - options, - ), - batchQuery: (sqls) => - executeInNewDbClient( - (client) => sqlExecutor.batchQuery(client, sqls), - options, - ), - command: (sql) => - executeInNewDbClient( - (client) => sqlExecutor.command(client, sql), - options, - ), - batchCommand: (sqls) => - executeInNewDbClient( - (client) => sqlExecutor.batchQuery(client, sqls), - options, - ), - }, - }; -}; +): SQLExecutor => ({ + query: (sql) => + executeInNewDbClient((client) => sqlExecutor.query(client, sql), options), + batchQuery: (sqls) => + executeInNewDbClient( + (client) => sqlExecutor.batchQuery(client, sqls), + options, + ), + command: (sql) => + executeInNewDbClient((client) => sqlExecutor.command(client, sql), options), + batchCommand: (sqls) => + executeInNewDbClient( + (client) => sqlExecutor.batchQuery(client, sqls), + options, + ), +}); -export const withSqlExecutorInNewConnection = < +export const sqlExecutorInNewConnection = < ConnectionType extends Connection, >(options: { open: () => Promise; -}): WithSQLExecutor => { - return { - execute: { - query: (sql) => - executeInNewConnection( - (connection) => connection.execute.query(sql), - options, - ), - batchQuery: (sqls) => - executeInNewConnection( - (connection) => connection.execute.batchQuery(sqls), - options, - ), - command: (sql) => - executeInNewConnection( - (connection) => connection.execute.command(sql), - options, - ), - batchCommand: (sqls) => - executeInNewConnection( - (connection) => connection.execute.batchCommand(sqls), - options, - ), - }, - }; -}; +}): SQLExecutor => ({ + query: (sql) => + executeInNewConnection( + (connection) => connection.execute.query(sql), + options, + ), + batchQuery: (sqls) => + executeInNewConnection( + (connection) => connection.execute.batchQuery(sqls), + options, + ), + command: (sql) => + executeInNewConnection( + (connection) => connection.execute.command(sql), + options, + ), + batchCommand: (sqls) => + executeInNewConnection( + (connection) => connection.execute.batchCommand(sqls), + options, + ), +}); export const executeInNewDbClient = async < DbClient = unknown, diff --git a/src/packages/dumbo/src/postgres/pg/connections/connection.ts b/src/packages/dumbo/src/postgres/pg/connections/connection.ts index 127a1c79..4f4acdc4 100644 --- a/src/packages/dumbo/src/postgres/pg/connections/connection.ts +++ b/src/packages/dumbo/src/postgres/pg/connections/connection.ts @@ -1,7 +1,7 @@ import pg from 'pg'; import { + sqlExecutor, transactionFactory, - withSqlExecutor, type Connection, } from '../../../core'; import { nodePostgresSQLExecutor } from '../execute'; @@ -54,7 +54,7 @@ export const nodePostgresClientConnection = ( connect: getClient, close: () => (client ? close(client) : Promise.resolve()), ...transactionFactory(getClient, nodePostgresTransaction), - ...withSqlExecutor(nodePostgresSQLExecutor(), { connect: getClient }), + execute: sqlExecutor(nodePostgresSQLExecutor(), { connect: getClient }), }; }; @@ -72,7 +72,7 @@ export const nodePostgresPoolClientConnection = ( connect: getClient, close: () => (client ? close(client) : Promise.resolve()), ...transactionFactory(getClient, nodePostgresTransaction), - ...withSqlExecutor(nodePostgresSQLExecutor(), { connect: getClient }), + execute: sqlExecutor(nodePostgresSQLExecutor(), { connect: getClient }), }; }; diff --git a/src/packages/dumbo/src/postgres/pg/connections/pool.ts b/src/packages/dumbo/src/postgres/pg/connections/pool.ts index 28fef78b..8c8f30c1 100644 --- a/src/packages/dumbo/src/postgres/pg/connections/pool.ts +++ b/src/packages/dumbo/src/postgres/pg/connections/pool.ts @@ -1,6 +1,6 @@ import pg from 'pg'; import { - withSqlExecutorInNewConnection, + sqlExecutorInNewConnection, type ConnectionPool, type Transaction, } from '../../../core'; @@ -51,7 +51,7 @@ export const nodePostgresNativePool = (options: { type: NodePostgresConnectorType, open, close, - ...withSqlExecutorInNewConnection({ open }), + execute: sqlExecutorInNewConnection({ open }), transaction: () => getConnection().transaction(), inTransaction: async ( handle: ( @@ -101,7 +101,7 @@ export const nodePostgresExplicitClientPool = (options: { type: NodePostgresConnectorType, open, close, - ...withSqlExecutorInNewConnection({ open }), + execute: sqlExecutorInNewConnection({ open }), transaction: () => getConnection().transaction(), inTransaction: async ( handle: ( diff --git a/src/packages/dumbo/src/postgres/pg/connections/transaction.ts b/src/packages/dumbo/src/postgres/pg/connections/transaction.ts index 8fa1dc33..6d026ab1 100644 --- a/src/packages/dumbo/src/postgres/pg/connections/transaction.ts +++ b/src/packages/dumbo/src/postgres/pg/connections/transaction.ts @@ -1,4 +1,4 @@ -import { withSqlExecutor, type Transaction } from '../../../core'; +import { sqlExecutor, type Transaction } from '../../../core'; import { nodePostgresSQLExecutor } from '../execute'; import { NodePostgresConnectorType, @@ -32,5 +32,5 @@ export const nodePostgresTransaction = < if (options?.close) await options?.close(client, error); }, - ...withSqlExecutor(nodePostgresSQLExecutor(), { connect: () => getClient }), + execute: sqlExecutor(nodePostgresSQLExecutor(), { connect: () => getClient }), }); From 4e68d09ee8f30f5c2c3f4acf44e5363248282bb1 Mon Sep 17 00:00:00 2001 From: Oskar Dudycz Date: Sat, 3 Aug 2024 10:42:10 +0200 Subject: [PATCH 15/30] Simplified the executor creation by using reusable factory with new conneciton --- .../dumbo/src/core/connections/transaction.ts | 100 ++++++------------ .../src/postgres/pg/connections/connection.ts | 6 +- .../dumbo/src/postgres/pg/connections/pool.ts | 48 ++------- 3 files changed, 47 insertions(+), 107 deletions(-) diff --git a/src/packages/dumbo/src/core/connections/transaction.ts b/src/packages/dumbo/src/core/connections/transaction.ts index 04c98af5..099cddfa 100644 --- a/src/packages/dumbo/src/core/connections/transaction.ts +++ b/src/packages/dumbo/src/core/connections/transaction.ts @@ -18,7 +18,31 @@ export type TransactionFactory = { ) => Promise; }; -export const transactionFactory = < +export const executeInTransaction = async < + ConnectorType extends string = string, + Result = unknown, +>( + transaction: Transaction, + handle: ( + transaction: Transaction, + ) => Promise<{ success: boolean; result: Result }>, +): Promise => { + await transaction.begin(); + + try { + const { success, result } = await handle(transaction); + + if (success) await transaction.commit(); + else await transaction.rollback(); + + return result; + } catch (e) { + await transaction.rollback(); + throw e; + } +}; + +export const transactionFactoryWithDbClient = < ConnectorType extends string = string, DbClient = unknown, >( @@ -26,78 +50,22 @@ export const transactionFactory = < initTransaction: (client: Promise) => Transaction, ): TransactionFactory => ({ transaction: () => initTransaction(connect()), - inTransaction: async ( - handle: ( - transaction: Transaction, - ) => Promise<{ success: boolean; result: Result }>, - ): Promise => { - const transaction = initTransaction(connect()); - - await transaction.begin(); - - try { - const { success, result } = await handle(transaction); - - if (success) await transaction.commit(); - else await transaction.rollback(); - - return result; - } catch (e) { - await transaction.rollback(); - throw e; - } - }, + inTransaction: (handle) => + executeInTransaction(initTransaction(connect()), handle), }); export const transactionFactoryWithNewConnection = < ConnectionType extends Connection = Connection, >( - connectionFactory: () => ConnectionType, - initTransaction: ( - client: Promise>, - options?: { - close: ( - client: ReturnType, - error?: unknown, - ) => Promise; - }, - ) => Transaction, + connect: () => ConnectionType, ): TransactionFactory => ({ - transaction: () => { - const connection = connectionFactory(); - - return initTransaction( - connection.connect() as Promise>, - { - close: () => connection.close(), - }, - ); - }, - inTransaction: async ( - handle: ( - transaction: Transaction, - ) => Promise<{ success: boolean; result: Result }>, - ): Promise => { - const connection = connectionFactory(); - const transaction = initTransaction( - connection.connect() as Promise>, - { - close: () => connection.close(), - }, - ); - - await transaction.begin(); - + transaction: () => connect().transaction(), + inTransaction: async (handle) => { + const connection = connect(); try { - const { success, result } = await handle(transaction); - - if (success) await transaction.commit(); - else await transaction.rollback(); - - return result; - } catch (e) { - await transaction.rollback(); - throw e; + return await connection.inTransaction(handle); + } finally { + await connection.close(); } }, }); diff --git a/src/packages/dumbo/src/postgres/pg/connections/connection.ts b/src/packages/dumbo/src/postgres/pg/connections/connection.ts index 4f4acdc4..87f26d7f 100644 --- a/src/packages/dumbo/src/postgres/pg/connections/connection.ts +++ b/src/packages/dumbo/src/postgres/pg/connections/connection.ts @@ -1,7 +1,7 @@ import pg from 'pg'; import { sqlExecutor, - transactionFactory, + transactionFactoryWithDbClient, type Connection, } from '../../../core'; import { nodePostgresSQLExecutor } from '../execute'; @@ -53,7 +53,7 @@ export const nodePostgresClientConnection = ( type: NodePostgresConnectorType, connect: getClient, close: () => (client ? close(client) : Promise.resolve()), - ...transactionFactory(getClient, nodePostgresTransaction), + ...transactionFactoryWithDbClient(getClient, nodePostgresTransaction), execute: sqlExecutor(nodePostgresSQLExecutor(), { connect: getClient }), }; }; @@ -71,7 +71,7 @@ export const nodePostgresPoolClientConnection = ( type: NodePostgresConnectorType, connect: getClient, close: () => (client ? close(client) : Promise.resolve()), - ...transactionFactory(getClient, nodePostgresTransaction), + ...transactionFactoryWithDbClient(getClient, nodePostgresTransaction), execute: sqlExecutor(nodePostgresSQLExecutor(), { connect: getClient }), }; }; diff --git a/src/packages/dumbo/src/postgres/pg/connections/pool.ts b/src/packages/dumbo/src/postgres/pg/connections/pool.ts index 8c8f30c1..c3c2d339 100644 --- a/src/packages/dumbo/src/postgres/pg/connections/pool.ts +++ b/src/packages/dumbo/src/postgres/pg/connections/pool.ts @@ -1,8 +1,8 @@ import pg from 'pg'; import { sqlExecutorInNewConnection, + transactionFactoryWithNewConnection, type ConnectionPool, - type Transaction, } from '../../../core'; import { defaultPostgreSqlDatabase, @@ -12,7 +12,6 @@ import { nodePostgresConnection, NodePostgresConnectorType, type NodePostgresClientConnection, - type NodePostgresConnector, type NodePostgresPoolClientConnection, } from './connection'; @@ -27,24 +26,21 @@ export const nodePostgresNativePool = (options: { database?: string; pool?: pg.Pool; }): NodePostgresNativePool => { - const { connectionString, database, pool: existingPool } = options; - const pool = existingPool - ? existingPool + const { connectionString, database, pool: ambientPool } = options; + const pool = ambientPool + ? ambientPool : getPool({ connectionString, database }); - const getConnection = () => { - const connect = pool.connect(); - - return nodePostgresConnection({ + const getConnection = () => + nodePostgresConnection({ type: 'PoolClient', - connect, + connect: pool.connect(), close: (client) => Promise.resolve(client.release()), }); - }; const open = () => Promise.resolve(getConnection()); const close = async () => { - if (!existingPool) await endPool({ connectionString, database }); + if (!ambientPool) await endPool({ connectionString, database }); }; return { @@ -52,19 +48,7 @@ export const nodePostgresNativePool = (options: { open, close, execute: sqlExecutorInNewConnection({ open }), - transaction: () => getConnection().transaction(), - inTransaction: async ( - handle: ( - transaction: Transaction, - ) => Promise<{ success: boolean; result: Result }>, - ): Promise => { - const connection = getConnection(); - try { - return await connection.inTransaction(handle); - } finally { - await connection.close(); - } - }, + ...transactionFactoryWithNewConnection(getConnection), }; }; @@ -102,19 +86,7 @@ export const nodePostgresExplicitClientPool = (options: { open, close, execute: sqlExecutorInNewConnection({ open }), - transaction: () => getConnection().transaction(), - inTransaction: async ( - handle: ( - transaction: Transaction, - ) => Promise<{ success: boolean; result: Result }>, - ): Promise => { - const connection = getConnection(); - try { - return await connection.inTransaction(handle); - } finally { - await connection.close(); - } - }, + ...transactionFactoryWithNewConnection(getConnection), }; }; From 15806e02840d301df0aaef9a78fbd5b9170f1d11 Mon Sep 17 00:00:00 2001 From: Oskar Dudycz Date: Sat, 3 Aug 2024 10:51:19 +0200 Subject: [PATCH 16/30] Renamed existing to ambient in pool setup to make code intention more explicit --- src/packages/dumbo/src/postgres/pg/connections/pool.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/packages/dumbo/src/postgres/pg/connections/pool.ts b/src/packages/dumbo/src/postgres/pg/connections/pool.ts index c3c2d339..a4d1e63a 100644 --- a/src/packages/dumbo/src/postgres/pg/connections/pool.ts +++ b/src/packages/dumbo/src/postgres/pg/connections/pool.ts @@ -57,11 +57,11 @@ export const nodePostgresExplicitClientPool = (options: { database?: string; client?: pg.Client; }): NodePostgresExplicitClientPool => { - const { connectionString, database, client: existingClient } = options; + const { connectionString, database, client: ambientClient } = options; const getConnection = () => { - const connect = existingClient - ? Promise.resolve(existingClient) + const connect = ambientClient + ? Promise.resolve(ambientClient) : Promise.resolve(new pg.Client({ connectionString, database })).then( async (client) => { await client.connect(); @@ -72,13 +72,13 @@ export const nodePostgresExplicitClientPool = (options: { return nodePostgresConnection({ type: 'Client', connect, - close: (client) => (existingClient ? Promise.resolve() : client.end()), + close: (client) => (ambientClient ? Promise.resolve() : client.end()), }); }; const open = () => Promise.resolve(getConnection()); const close = async () => { - if (!existingClient) await endPool({ connectionString, database }); + if (!ambientClient) await endPool({ connectionString, database }); }; return { From 1a3f9a90ba5b1be208667a655f2d75065756981e Mon Sep 17 00:00:00 2001 From: Oskar Dudycz Date: Sat, 3 Aug 2024 10:58:38 +0200 Subject: [PATCH 17/30] Injected SQLExecutor instead of connection pool to Pongo PostgresCollection Collection only needs sql execution, so there's no need to inject the whole pool. This will also make easier injecting other executors from session's transaction. --- .../src/postgres/{client.ts => dbClient.ts} | 12 ++--- src/packages/pongo/src/postgres/index.ts | 2 +- .../pongo/src/postgres/postgresCollection.ts | 44 +++++++++++-------- 3 files changed, 30 insertions(+), 28 deletions(-) rename src/packages/pongo/src/postgres/{client.ts => dbClient.ts} (80%) diff --git a/src/packages/pongo/src/postgres/client.ts b/src/packages/pongo/src/postgres/dbClient.ts similarity index 80% rename from src/packages/pongo/src/postgres/client.ts rename to src/packages/pongo/src/postgres/dbClient.ts index 215f60f0..7c1a026e 100644 --- a/src/packages/pongo/src/postgres/client.ts +++ b/src/packages/pongo/src/postgres/dbClient.ts @@ -3,11 +3,7 @@ import { postgresPool, type PostgresPoolOptions, } from '@event-driven-io/dumbo'; -import { - type DbClient, - type PongoDbClientOptions, - type PongoDocument, -} from '../main'; +import { type DbClient, type PongoDbClientOptions } from '../main'; import { postgresCollection } from './postgresCollection'; export type PostgresDbClientOptions = PongoDbClientOptions<'PostgreSQL'> & @@ -30,10 +26,10 @@ export const postgresDbClient = ( options, connect: () => Promise.resolve(), close: () => pool.close(), - collection: (name: string) => - postgresCollection(name, { + collection: (name) => + postgresCollection(name, { dbName: databaseName, - pool, + sqlExecutor: pool.execute, }), }; }; diff --git a/src/packages/pongo/src/postgres/index.ts b/src/packages/pongo/src/postgres/index.ts index 17b4d874..effd6b9a 100644 --- a/src/packages/pongo/src/postgres/index.ts +++ b/src/packages/pongo/src/postgres/index.ts @@ -1,4 +1,4 @@ -export * from './client'; +export * from './dbClient'; export * from './filter'; export * from './postgresCollection'; export * from './update'; diff --git a/src/packages/pongo/src/postgres/postgresCollection.ts b/src/packages/pongo/src/postgres/postgresCollection.ts index 43a88b16..f4520559 100644 --- a/src/packages/pongo/src/postgres/postgresCollection.ts +++ b/src/packages/pongo/src/postgres/postgresCollection.ts @@ -1,9 +1,8 @@ import { single, sql, - type ConnectionPool, - type QueryResultRow, type SQL, + type SQLExecutor, } from '@event-driven-io/dumbo'; import format from 'pg-format'; import { v4 as uuid } from 'uuid'; @@ -25,14 +24,11 @@ import { buildUpdateQuery } from './update'; export const postgresCollection = ( collectionName: string, - { dbName, pool }: { dbName: string; pool: ConnectionPool }, + { dbName, sqlExecutor }: { dbName: string; sqlExecutor: SQLExecutor }, ): PongoCollection => { - const execute = (sql: SQL) => - pool.execute.query(sql); - const SqlFor = collectionSQLBuilder(collectionName); - const createCollection = execute(SqlFor.createCollection()); + const createCollection = sqlExecutor.command(SqlFor.createCollection()); const collection = { dbName, @@ -45,7 +41,9 @@ export const postgresCollection = ( const _id = uuid(); - const result = await execute(SqlFor.insertOne({ _id, ...document })); + const result = await sqlExecutor.command( + SqlFor.insertOne({ _id, ...document }), + ); return result.rowCount ? { insertedId: _id, acknowledged: true } @@ -59,7 +57,7 @@ export const postgresCollection = ( ...doc, })); - const result = await execute(SqlFor.insertMany(rows)); + const result = await sqlExecutor.command(SqlFor.insertMany(rows)); return { acknowledged: result.rowCount === rows.length, @@ -73,7 +71,9 @@ export const postgresCollection = ( ): Promise => { await createCollection; - const result = await execute(SqlFor.updateOne(filter, update)); + const result = await sqlExecutor.command( + SqlFor.updateOne(filter, update), + ); return result.rowCount ? { acknowledged: true, modifiedCount: result.rowCount } : { acknowledged: false, modifiedCount: 0 }; @@ -84,7 +84,9 @@ export const postgresCollection = ( ): Promise => { await createCollection; - const result = await execute(SqlFor.replaceOne(filter, document)); + const result = await sqlExecutor.command( + SqlFor.replaceOne(filter, document), + ); return result.rowCount ? { acknowledged: true, modifiedCount: result.rowCount } : { acknowledged: false, modifiedCount: 0 }; @@ -95,7 +97,9 @@ export const postgresCollection = ( ): Promise => { await createCollection; - const result = await execute(SqlFor.updateMany(filter, update)); + const result = await sqlExecutor.command( + SqlFor.updateMany(filter, update), + ); return result.rowCount ? { acknowledged: true, modifiedCount: result.rowCount } : { acknowledged: false, modifiedCount: 0 }; @@ -103,7 +107,7 @@ export const postgresCollection = ( deleteOne: async (filter?: PongoFilter): Promise => { await createCollection; - const result = await execute(SqlFor.deleteOne(filter ?? {})); + const result = await sqlExecutor.command(SqlFor.deleteOne(filter ?? {})); return result.rowCount ? { acknowledged: true, deletedCount: result.rowCount } : { acknowledged: false, deletedCount: 0 }; @@ -111,7 +115,7 @@ export const postgresCollection = ( deleteMany: async (filter?: PongoFilter): Promise => { await createCollection; - const result = await execute(SqlFor.deleteMany(filter ?? {})); + const result = await sqlExecutor.command(SqlFor.deleteMany(filter ?? {})); return result.rowCount ? { acknowledged: true, deletedCount: result.rowCount } : { acknowledged: false, deletedCount: 0 }; @@ -119,7 +123,7 @@ export const postgresCollection = ( findOne: async (filter?: PongoFilter): Promise => { await createCollection; - const result = await execute(SqlFor.findOne(filter ?? {})); + const result = await sqlExecutor.query(SqlFor.findOne(filter ?? {})); return (result.rows[0]?.data ?? null) as T | null; }, findOneAndDelete: async (filter: PongoFilter): Promise => { @@ -190,25 +194,27 @@ export const postgresCollection = ( find: async (filter?: PongoFilter): Promise => { await createCollection; - const result = await execute(SqlFor.find(filter ?? {})); + const result = await sqlExecutor.query(SqlFor.find(filter ?? {})); return result.rows.map((row) => row.data as T); }, countDocuments: async (filter?: PongoFilter): Promise => { await createCollection; const { count } = await single( - execute<{ count: number }>(SqlFor.countDocuments(filter ?? {})), + sqlExecutor.query<{ count: number }>( + SqlFor.countDocuments(filter ?? {}), + ), ); return count; }, drop: async (): Promise => { await createCollection; - const result = await execute(SqlFor.drop()); + const result = await sqlExecutor.command(SqlFor.drop()); return (result?.rowCount ?? 0) > 0; }, rename: async (newName: string): Promise> => { await createCollection; - await execute(SqlFor.rename(newName)); + await sqlExecutor.command(SqlFor.rename(newName)); collectionName = newName; return collection; }, From 96ae0f8ab74fde775d98d88e11c2d06eee32fba4 Mon Sep 17 00:00:00 2001 From: Oskar Dudycz Date: Sat, 3 Aug 2024 11:12:18 +0200 Subject: [PATCH 18/30] Renamed main => core in Pongo to align with introduced in Dumbo convention Renamed postgresCollection to pongoCollection, as it'll be generalised in the follow-up commit --- src/packages/pongo/src/{main => core}/dbClient.ts | 0 src/packages/pongo/src/{main => core}/index.ts | 0 .../pongoClient.connections.e2e.spec.ts | 0 src/packages/pongo/src/{main => core}/pongoClient.ts | 0 src/packages/pongo/src/{main => core}/pongoSession.ts | 0 .../pongo/src/{main => core}/typing/entries.ts | 0 src/packages/pongo/src/{main => core}/typing/index.ts | 0 .../pongo/src/{main => core}/typing/operations.ts | 0 src/packages/pongo/src/index.ts | 2 +- src/packages/pongo/src/mongo/mongoClient.ts | 2 +- src/packages/pongo/src/mongo/mongoCollection.ts | 2 +- src/packages/pongo/src/mongo/mongoDb.ts | 2 +- src/packages/pongo/src/postgres/dbClient.ts | 6 +++--- src/packages/pongo/src/postgres/filter/index.ts | 4 ++-- .../pongo/src/postgres/filter/queryOperators.ts | 2 +- src/packages/pongo/src/postgres/postgresCollection.ts | 11 ++++++++--- src/packages/pongo/src/postgres/update/index.ts | 4 ++-- 17 files changed, 20 insertions(+), 15 deletions(-) rename src/packages/pongo/src/{main => core}/dbClient.ts (100%) rename src/packages/pongo/src/{main => core}/index.ts (100%) rename src/packages/pongo/src/{main => core}/pongoClient.connections.e2e.spec.ts (100%) rename src/packages/pongo/src/{main => core}/pongoClient.ts (100%) rename src/packages/pongo/src/{main => core}/pongoSession.ts (100%) rename src/packages/pongo/src/{main => core}/typing/entries.ts (100%) rename src/packages/pongo/src/{main => core}/typing/index.ts (100%) rename src/packages/pongo/src/{main => core}/typing/operations.ts (100%) diff --git a/src/packages/pongo/src/main/dbClient.ts b/src/packages/pongo/src/core/dbClient.ts similarity index 100% rename from src/packages/pongo/src/main/dbClient.ts rename to src/packages/pongo/src/core/dbClient.ts diff --git a/src/packages/pongo/src/main/index.ts b/src/packages/pongo/src/core/index.ts similarity index 100% rename from src/packages/pongo/src/main/index.ts rename to src/packages/pongo/src/core/index.ts diff --git a/src/packages/pongo/src/main/pongoClient.connections.e2e.spec.ts b/src/packages/pongo/src/core/pongoClient.connections.e2e.spec.ts similarity index 100% rename from src/packages/pongo/src/main/pongoClient.connections.e2e.spec.ts rename to src/packages/pongo/src/core/pongoClient.connections.e2e.spec.ts diff --git a/src/packages/pongo/src/main/pongoClient.ts b/src/packages/pongo/src/core/pongoClient.ts similarity index 100% rename from src/packages/pongo/src/main/pongoClient.ts rename to src/packages/pongo/src/core/pongoClient.ts diff --git a/src/packages/pongo/src/main/pongoSession.ts b/src/packages/pongo/src/core/pongoSession.ts similarity index 100% rename from src/packages/pongo/src/main/pongoSession.ts rename to src/packages/pongo/src/core/pongoSession.ts diff --git a/src/packages/pongo/src/main/typing/entries.ts b/src/packages/pongo/src/core/typing/entries.ts similarity index 100% rename from src/packages/pongo/src/main/typing/entries.ts rename to src/packages/pongo/src/core/typing/entries.ts diff --git a/src/packages/pongo/src/main/typing/index.ts b/src/packages/pongo/src/core/typing/index.ts similarity index 100% rename from src/packages/pongo/src/main/typing/index.ts rename to src/packages/pongo/src/core/typing/index.ts diff --git a/src/packages/pongo/src/main/typing/operations.ts b/src/packages/pongo/src/core/typing/operations.ts similarity index 100% rename from src/packages/pongo/src/main/typing/operations.ts rename to src/packages/pongo/src/core/typing/operations.ts diff --git a/src/packages/pongo/src/index.ts b/src/packages/pongo/src/index.ts index f2e15245..e3141bb6 100644 --- a/src/packages/pongo/src/index.ts +++ b/src/packages/pongo/src/index.ts @@ -1,3 +1,3 @@ -export * from './main'; +export * from './core'; export * from './mongo'; export * from './postgres'; diff --git a/src/packages/pongo/src/mongo/mongoClient.ts b/src/packages/pongo/src/mongo/mongoClient.ts index 31dfef2d..626d010b 100644 --- a/src/packages/pongo/src/mongo/mongoClient.ts +++ b/src/packages/pongo/src/mongo/mongoClient.ts @@ -4,7 +4,7 @@ import { pongoClient, type PongoClient, type PongoClientOptions, -} from '../main'; +} from '../core'; import { Db } from './mongoDb'; export class MongoClient { diff --git a/src/packages/pongo/src/mongo/mongoCollection.ts b/src/packages/pongo/src/mongo/mongoCollection.ts index 5804d456..99c94706 100644 --- a/src/packages/pongo/src/mongo/mongoCollection.ts +++ b/src/packages/pongo/src/mongo/mongoCollection.ts @@ -64,7 +64,7 @@ import type { PongoCollection, PongoFilter, PongoUpdate, -} from '../main'; +} from '../core'; import { FindCursor } from './findCursor'; export class Collection implements MongoCollection { diff --git a/src/packages/pongo/src/mongo/mongoDb.ts b/src/packages/pongo/src/mongo/mongoDb.ts index a8834421..dd9f72a3 100644 --- a/src/packages/pongo/src/mongo/mongoDb.ts +++ b/src/packages/pongo/src/mongo/mongoDb.ts @@ -3,7 +3,7 @@ import { ObjectId, type Document, } from 'mongodb'; -import type { DocumentHandler, PongoDb } from '../main'; +import type { DocumentHandler, PongoDb } from '../core'; import { Collection } from './mongoCollection'; export class Db { diff --git a/src/packages/pongo/src/postgres/dbClient.ts b/src/packages/pongo/src/postgres/dbClient.ts index 7c1a026e..8edfb81f 100644 --- a/src/packages/pongo/src/postgres/dbClient.ts +++ b/src/packages/pongo/src/postgres/dbClient.ts @@ -3,8 +3,8 @@ import { postgresPool, type PostgresPoolOptions, } from '@event-driven-io/dumbo'; -import { type DbClient, type PongoDbClientOptions } from '../main'; -import { postgresCollection } from './postgresCollection'; +import { type DbClient, type PongoDbClientOptions } from '../core'; +import { pongoCollection } from './postgresCollection'; export type PostgresDbClientOptions = PongoDbClientOptions<'PostgreSQL'> & PostgresPoolOptions; @@ -27,7 +27,7 @@ export const postgresDbClient = ( connect: () => Promise.resolve(), close: () => pool.close(), collection: (name) => - postgresCollection(name, { + pongoCollection(name, { dbName: databaseName, sqlExecutor: pool.execute, }), diff --git a/src/packages/pongo/src/postgres/filter/index.ts b/src/packages/pongo/src/postgres/filter/index.ts index 467e0931..0fea691d 100644 --- a/src/packages/pongo/src/postgres/filter/index.ts +++ b/src/packages/pongo/src/postgres/filter/index.ts @@ -1,5 +1,5 @@ -import type { PongoFilter } from '../../main'; -import { entries } from '../../main/typing'; +import type { PongoFilter } from '../../core'; +import { entries } from '../../core/typing'; import { Operators, handleOperator, hasOperators } from './queryOperators'; export * from './queryOperators'; diff --git a/src/packages/pongo/src/postgres/filter/queryOperators.ts b/src/packages/pongo/src/postgres/filter/queryOperators.ts index fe5d3a72..977bb577 100644 --- a/src/packages/pongo/src/postgres/filter/queryOperators.ts +++ b/src/packages/pongo/src/postgres/filter/queryOperators.ts @@ -1,5 +1,5 @@ import format from 'pg-format'; -import { entries } from '../../main/typing'; +import { entries } from '../../core/typing'; export const Operators = { $eq: '$eq', diff --git a/src/packages/pongo/src/postgres/postgresCollection.ts b/src/packages/pongo/src/postgres/postgresCollection.ts index f4520559..1c58ed20 100644 --- a/src/packages/pongo/src/postgres/postgresCollection.ts +++ b/src/packages/pongo/src/postgres/postgresCollection.ts @@ -18,13 +18,18 @@ import { type PongoUpdateResult, type WithId, type WithoutId, -} from '../main'; +} from '../core'; import { constructFilterQuery } from './filter'; import { buildUpdateQuery } from './update'; -export const postgresCollection = ( +export type PongoCollectionOptions = { + dbName: string; + sqlExecutor: SQLExecutor; +}; + +export const pongoCollection = ( collectionName: string, - { dbName, sqlExecutor }: { dbName: string; sqlExecutor: SQLExecutor }, + { dbName, sqlExecutor }: PongoCollectionOptions, ): PongoCollection => { const SqlFor = collectionSQLBuilder(collectionName); diff --git a/src/packages/pongo/src/postgres/update/index.ts b/src/packages/pongo/src/postgres/update/index.ts index c213e940..8a217281 100644 --- a/src/packages/pongo/src/postgres/update/index.ts +++ b/src/packages/pongo/src/postgres/update/index.ts @@ -1,6 +1,6 @@ import { sql, type SQL } from '@event-driven-io/dumbo'; -import type { $inc, $push, $set, $unset, PongoUpdate } from '../../main'; -import { entries } from '../../main/typing'; +import type { $inc, $push, $set, $unset, PongoUpdate } from '../../core'; +import { entries } from '../../core/typing'; export const buildUpdateQuery = (update: PongoUpdate): SQL => entries(update).reduce((currentUpdateQuery, [op, value]) => { From ccf17fe6ec32dc7c9651e94f39cc7ef18bc1d33a Mon Sep 17 00:00:00 2001 From: Oskar Dudycz Date: Sat, 3 Aug 2024 11:23:25 +0200 Subject: [PATCH 19/30] Made pongoCollection generic by introducing injectable sql builder --- src/packages/pongo/src/core/index.ts | 1 + .../pongoCollection.ts} | 175 ++++-------------- .../src/postgres/collectionSqlBuilder.ts | 112 +++++++++++ src/packages/pongo/src/postgres/dbClient.ts | 14 +- src/packages/pongo/src/postgres/index.ts | 2 +- 5 files changed, 162 insertions(+), 142 deletions(-) rename src/packages/pongo/src/{postgres/postgresCollection.ts => core/pongoCollection.ts} (52%) create mode 100644 src/packages/pongo/src/postgres/collectionSqlBuilder.ts diff --git a/src/packages/pongo/src/core/index.ts b/src/packages/pongo/src/core/index.ts index c269caf9..48ecb492 100644 --- a/src/packages/pongo/src/core/index.ts +++ b/src/packages/pongo/src/core/index.ts @@ -1,4 +1,5 @@ export * from './dbClient'; export * from './pongoClient'; +export * from './pongoCollection'; export * from './pongoSession'; export * from './typing'; diff --git a/src/packages/pongo/src/postgres/postgresCollection.ts b/src/packages/pongo/src/core/pongoCollection.ts similarity index 52% rename from src/packages/pongo/src/postgres/postgresCollection.ts rename to src/packages/pongo/src/core/pongoCollection.ts index 1c58ed20..d5418273 100644 --- a/src/packages/pongo/src/postgres/postgresCollection.ts +++ b/src/packages/pongo/src/core/pongoCollection.ts @@ -1,10 +1,4 @@ -import { - single, - sql, - type SQL, - type SQLExecutor, -} from '@event-driven-io/dumbo'; -import format from 'pg-format'; +import { single, type SQL, type SQLExecutor } from '@event-driven-io/dumbo'; import { v4 as uuid } from 'uuid'; import { type DocumentHandler, @@ -18,22 +12,22 @@ import { type PongoUpdateResult, type WithId, type WithoutId, -} from '../core'; -import { constructFilterQuery } from './filter'; -import { buildUpdateQuery } from './update'; +} from '.'; export type PongoCollectionOptions = { + collectionName: string; dbName: string; sqlExecutor: SQLExecutor; + sqlBuilder: PongoCollectionSQLBuilder; }; -export const pongoCollection = ( - collectionName: string, - { dbName, sqlExecutor }: PongoCollectionOptions, -): PongoCollection => { - const SqlFor = collectionSQLBuilder(collectionName); - - const createCollection = sqlExecutor.command(SqlFor.createCollection()); +export const pongoCollection = ({ + collectionName, + dbName, + sqlExecutor: { command, query }, + sqlBuilder: SqlFor, +}: PongoCollectionOptions): PongoCollection => { + const createCollection = command(SqlFor.createCollection()); const collection = { dbName, @@ -46,9 +40,7 @@ export const pongoCollection = ( const _id = uuid(); - const result = await sqlExecutor.command( - SqlFor.insertOne({ _id, ...document }), - ); + const result = await command(SqlFor.insertOne({ _id, ...document })); return result.rowCount ? { insertedId: _id, acknowledged: true } @@ -62,7 +54,7 @@ export const pongoCollection = ( ...doc, })); - const result = await sqlExecutor.command(SqlFor.insertMany(rows)); + const result = await command(SqlFor.insertMany(rows)); return { acknowledged: result.rowCount === rows.length, @@ -76,9 +68,7 @@ export const pongoCollection = ( ): Promise => { await createCollection; - const result = await sqlExecutor.command( - SqlFor.updateOne(filter, update), - ); + const result = await command(SqlFor.updateOne(filter, update)); return result.rowCount ? { acknowledged: true, modifiedCount: result.rowCount } : { acknowledged: false, modifiedCount: 0 }; @@ -89,9 +79,7 @@ export const pongoCollection = ( ): Promise => { await createCollection; - const result = await sqlExecutor.command( - SqlFor.replaceOne(filter, document), - ); + const result = await command(SqlFor.replaceOne(filter, document)); return result.rowCount ? { acknowledged: true, modifiedCount: result.rowCount } : { acknowledged: false, modifiedCount: 0 }; @@ -102,9 +90,7 @@ export const pongoCollection = ( ): Promise => { await createCollection; - const result = await sqlExecutor.command( - SqlFor.updateMany(filter, update), - ); + const result = await command(SqlFor.updateMany(filter, update)); return result.rowCount ? { acknowledged: true, modifiedCount: result.rowCount } : { acknowledged: false, modifiedCount: 0 }; @@ -112,7 +98,7 @@ export const pongoCollection = ( deleteOne: async (filter?: PongoFilter): Promise => { await createCollection; - const result = await sqlExecutor.command(SqlFor.deleteOne(filter ?? {})); + const result = await command(SqlFor.deleteOne(filter ?? {})); return result.rowCount ? { acknowledged: true, deletedCount: result.rowCount } : { acknowledged: false, deletedCount: 0 }; @@ -120,7 +106,7 @@ export const pongoCollection = ( deleteMany: async (filter?: PongoFilter): Promise => { await createCollection; - const result = await sqlExecutor.command(SqlFor.deleteMany(filter ?? {})); + const result = await command(SqlFor.deleteMany(filter ?? {})); return result.rowCount ? { acknowledged: true, deletedCount: result.rowCount } : { acknowledged: false, deletedCount: 0 }; @@ -128,7 +114,7 @@ export const pongoCollection = ( findOne: async (filter?: PongoFilter): Promise => { await createCollection; - const result = await sqlExecutor.query(SqlFor.findOne(filter ?? {})); + const result = await query(SqlFor.findOne(filter ?? {})); return (result.rows[0]?.data ?? null) as T | null; }, findOneAndDelete: async (filter: PongoFilter): Promise => { @@ -199,27 +185,25 @@ export const pongoCollection = ( find: async (filter?: PongoFilter): Promise => { await createCollection; - const result = await sqlExecutor.query(SqlFor.find(filter ?? {})); + const result = await query(SqlFor.find(filter ?? {})); return result.rows.map((row) => row.data as T); }, countDocuments: async (filter?: PongoFilter): Promise => { await createCollection; const { count } = await single( - sqlExecutor.query<{ count: number }>( - SqlFor.countDocuments(filter ?? {}), - ), + query<{ count: number }>(SqlFor.countDocuments(filter ?? {})), ); return count; }, drop: async (): Promise => { await createCollection; - const result = await sqlExecutor.command(SqlFor.drop()); + const result = await command(SqlFor.drop()); return (result?.rowCount ?? 0) > 0; }, rename: async (newName: string): Promise> => { await createCollection; - await sqlExecutor.command(SqlFor.rename(newName)); + await command(SqlFor.rename(newName)); collectionName = newName; return collection; }, @@ -228,101 +212,18 @@ export const pongoCollection = ( return collection; }; -export const collectionSQLBuilder = (collectionName: string) => ({ - createCollection: (): SQL => - sql( - `CREATE TABLE IF NOT EXISTS %I ( - _id TEXT PRIMARY KEY, - data JSONB NOT NULL, - metadata JSONB NOT NULL DEFAULT '{}', - _version BIGINT NOT NULL DEFAULT 1, - _partition TEXT NOT NULL DEFAULT 'png_global', - _archived BOOLEAN NOT NULL DEFAULT FALSE, - _created TIMESTAMPTZ NOT NULL DEFAULT now(), - _updated TIMESTAMPTZ NOT NULL DEFAULT now() - )`, - collectionName, - ), - insertOne: (document: WithId): SQL => - sql( - 'INSERT INTO %I (_id, data) VALUES (%L, %L)', - collectionName, - document._id, - JSON.stringify(document), - ), - insertMany: (documents: WithId[]): SQL => { - const values = documents - .map((doc) => format('(%L, %L)', doc._id, JSON.stringify(doc))) - .join(', '); - return sql('INSERT INTO %I (_id, data) VALUES %s', collectionName, values); - }, - updateOne: (filter: PongoFilter, update: PongoUpdate): SQL => { - const filterQuery = constructFilterQuery(filter); - const updateQuery = buildUpdateQuery(update); - - return sql( - `WITH cte AS ( - SELECT _id FROM %I WHERE %s LIMIT 1 - ) - UPDATE %I SET data = %s FROM cte WHERE %I._id = cte._id`, - collectionName, - filterQuery, - collectionName, - updateQuery, - collectionName, - ); - }, - replaceOne: (filter: PongoFilter, document: WithoutId): SQL => { - const filterQuery = constructFilterQuery(filter); - - return sql( - `UPDATE %I SET data = %L || jsonb_build_object('_id', data->>'_id') WHERE %s`, - collectionName, - JSON.stringify(document), - filterQuery, - ); - }, - updateMany: (filter: PongoFilter, update: PongoUpdate): SQL => { - const filterQuery = constructFilterQuery(filter); - const updateQuery = buildUpdateQuery(update); - - return sql( - 'UPDATE %I SET data = %s WHERE %s', - collectionName, - updateQuery, - filterQuery, - ); - }, - deleteOne: (filter: PongoFilter): SQL => { - const filterQuery = constructFilterQuery(filter); - return sql('DELETE FROM %I WHERE %s', collectionName, filterQuery); - }, - deleteMany: (filter: PongoFilter): SQL => { - const filterQuery = constructFilterQuery(filter); - return sql('DELETE FROM %I WHERE %s', collectionName, filterQuery); - }, - findOne: (filter: PongoFilter): SQL => { - const filterQuery = constructFilterQuery(filter); - return sql( - 'SELECT data FROM %I WHERE %s LIMIT 1', - collectionName, - filterQuery, - ); - }, - find: (filter: PongoFilter): SQL => { - const filterQuery = constructFilterQuery(filter); - return sql('SELECT data FROM %I WHERE %s', collectionName, filterQuery); - }, - countDocuments: (filter: PongoFilter): SQL => { - const filterQuery = constructFilterQuery(filter); - return sql( - 'SELECT COUNT(1) as count FROM %I WHERE %s', - collectionName, - filterQuery, - ); - }, - rename: (newName: string): SQL => - sql('ALTER TABLE %I RENAME TO %I', collectionName, newName), - drop: (targetName: string = collectionName): SQL => - sql('DROP TABLE IF EXISTS %I', targetName), -}); +export type PongoCollectionSQLBuilder = { + createCollection: () => SQL; + insertOne: (document: WithId) => SQL; + insertMany: (documents: WithId[]) => SQL; + updateOne: (filter: PongoFilter, update: PongoUpdate) => SQL; + replaceOne: (filter: PongoFilter, document: WithoutId) => SQL; + updateMany: (filter: PongoFilter, update: PongoUpdate) => SQL; + deleteOne: (filter: PongoFilter) => SQL; + deleteMany: (filter: PongoFilter) => SQL; + findOne: (filter: PongoFilter) => SQL; + find: (filter: PongoFilter) => SQL; + countDocuments: (filter: PongoFilter) => SQL; + rename: (newName: string) => SQL; + drop: () => SQL; +}; diff --git a/src/packages/pongo/src/postgres/collectionSqlBuilder.ts b/src/packages/pongo/src/postgres/collectionSqlBuilder.ts new file mode 100644 index 00000000..dbd366cc --- /dev/null +++ b/src/packages/pongo/src/postgres/collectionSqlBuilder.ts @@ -0,0 +1,112 @@ +import { sql, type SQL } from '@event-driven-io/dumbo'; +import format from 'pg-format'; +import { + type PongoFilter, + type PongoUpdate, + type WithId, + type WithoutId, +} from '../core'; +import type { PongoCollectionSQLBuilder } from '../core/pongoCollection'; +import { constructFilterQuery } from './filter'; +import { buildUpdateQuery } from './update'; + +export const postgresSQLBuilder = ( + collectionName: string, +): PongoCollectionSQLBuilder => ({ + createCollection: (): SQL => + sql( + `CREATE TABLE IF NOT EXISTS %I ( + _id TEXT PRIMARY KEY, + data JSONB NOT NULL, + metadata JSONB NOT NULL DEFAULT '{}', + _version BIGINT NOT NULL DEFAULT 1, + _partition TEXT NOT NULL DEFAULT 'png_global', + _archived BOOLEAN NOT NULL DEFAULT FALSE, + _created TIMESTAMPTZ NOT NULL DEFAULT now(), + _updated TIMESTAMPTZ NOT NULL DEFAULT now() + )`, + collectionName, + ), + insertOne: (document: WithId): SQL => + sql( + 'INSERT INTO %I (_id, data) VALUES (%L, %L)', + collectionName, + document._id, + JSON.stringify(document), + ), + insertMany: (documents: WithId[]): SQL => { + const values = documents + .map((doc) => format('(%L, %L)', doc._id, JSON.stringify(doc))) + .join(', '); + return sql('INSERT INTO %I (_id, data) VALUES %s', collectionName, values); + }, + updateOne: (filter: PongoFilter, update: PongoUpdate): SQL => { + const filterQuery = constructFilterQuery(filter); + const updateQuery = buildUpdateQuery(update); + + return sql( + `WITH cte AS ( + SELECT _id FROM %I WHERE %s LIMIT 1 + ) + UPDATE %I SET data = %s FROM cte WHERE %I._id = cte._id`, + collectionName, + filterQuery, + collectionName, + updateQuery, + collectionName, + ); + }, + replaceOne: (filter: PongoFilter, document: WithoutId): SQL => { + const filterQuery = constructFilterQuery(filter); + + return sql( + `UPDATE %I SET data = %L || jsonb_build_object('_id', data->>'_id') WHERE %s`, + collectionName, + JSON.stringify(document), + filterQuery, + ); + }, + updateMany: (filter: PongoFilter, update: PongoUpdate): SQL => { + const filterQuery = constructFilterQuery(filter); + const updateQuery = buildUpdateQuery(update); + + return sql( + 'UPDATE %I SET data = %s WHERE %s', + collectionName, + updateQuery, + filterQuery, + ); + }, + deleteOne: (filter: PongoFilter): SQL => { + const filterQuery = constructFilterQuery(filter); + return sql('DELETE FROM %I WHERE %s', collectionName, filterQuery); + }, + deleteMany: (filter: PongoFilter): SQL => { + const filterQuery = constructFilterQuery(filter); + return sql('DELETE FROM %I WHERE %s', collectionName, filterQuery); + }, + findOne: (filter: PongoFilter): SQL => { + const filterQuery = constructFilterQuery(filter); + return sql( + 'SELECT data FROM %I WHERE %s LIMIT 1', + collectionName, + filterQuery, + ); + }, + find: (filter: PongoFilter): SQL => { + const filterQuery = constructFilterQuery(filter); + return sql('SELECT data FROM %I WHERE %s', collectionName, filterQuery); + }, + countDocuments: (filter: PongoFilter): SQL => { + const filterQuery = constructFilterQuery(filter); + return sql( + 'SELECT COUNT(1) as count FROM %I WHERE %s', + collectionName, + filterQuery, + ); + }, + rename: (newName: string): SQL => + sql('ALTER TABLE %I RENAME TO %I', collectionName, newName), + drop: (targetName: string = collectionName): SQL => + sql('DROP TABLE IF EXISTS %I', targetName), +}); diff --git a/src/packages/pongo/src/postgres/dbClient.ts b/src/packages/pongo/src/postgres/dbClient.ts index 8edfb81f..c954ea31 100644 --- a/src/packages/pongo/src/postgres/dbClient.ts +++ b/src/packages/pongo/src/postgres/dbClient.ts @@ -3,8 +3,12 @@ import { postgresPool, type PostgresPoolOptions, } from '@event-driven-io/dumbo'; -import { type DbClient, type PongoDbClientOptions } from '../core'; -import { pongoCollection } from './postgresCollection'; +import { + pongoCollection, + type DbClient, + type PongoDbClientOptions, +} from '../core'; +import { postgresSQLBuilder } from './collectionSqlBuilder'; export type PostgresDbClientOptions = PongoDbClientOptions<'PostgreSQL'> & PostgresPoolOptions; @@ -26,10 +30,12 @@ export const postgresDbClient = ( options, connect: () => Promise.resolve(), close: () => pool.close(), - collection: (name) => - pongoCollection(name, { + collection: (collectionName) => + pongoCollection({ + collectionName, dbName: databaseName, sqlExecutor: pool.execute, + sqlBuilder: postgresSQLBuilder(collectionName), }), }; }; diff --git a/src/packages/pongo/src/postgres/index.ts b/src/packages/pongo/src/postgres/index.ts index effd6b9a..cfca35e9 100644 --- a/src/packages/pongo/src/postgres/index.ts +++ b/src/packages/pongo/src/postgres/index.ts @@ -1,4 +1,4 @@ +export * from './collectionSqlBuilder'; export * from './dbClient'; export * from './filter'; -export * from './postgresCollection'; export * from './update'; From 1a9a9ee1539d3e57cc460970d4258f75af926b5d Mon Sep 17 00:00:00 2001 From: Oskar Dudycz Date: Sat, 3 Aug 2024 11:26:56 +0200 Subject: [PATCH 20/30] Nested sql building in Pongo postgres provider --- src/packages/dumbo/src/postgres/core/schema/schema.ts | 3 +-- src/packages/pongo/src/postgres/dbClient.ts | 2 +- src/packages/pongo/src/postgres/index.ts | 4 +--- .../src/postgres/{ => sqlBuilder}/filter/index.ts | 3 +-- .../postgres/{ => sqlBuilder}/filter/queryOperators.ts | 2 +- .../{collectionSqlBuilder.ts => sqlBuilder/index.ts} | 4 ++-- .../src/postgres/{ => sqlBuilder}/update/index.ts | 10 ++++++++-- 7 files changed, 15 insertions(+), 13 deletions(-) rename src/packages/pongo/src/postgres/{ => sqlBuilder}/filter/index.ts (91%) rename src/packages/pongo/src/postgres/{ => sqlBuilder}/filter/queryOperators.ts (98%) rename src/packages/pongo/src/postgres/{collectionSqlBuilder.ts => sqlBuilder/index.ts} (97%) rename src/packages/pongo/src/postgres/{ => sqlBuilder}/update/index.ts (93%) diff --git a/src/packages/dumbo/src/postgres/core/schema/schema.ts b/src/packages/dumbo/src/postgres/core/schema/schema.ts index 109ff596..964928ce 100644 --- a/src/packages/dumbo/src/postgres/core/schema/schema.ts +++ b/src/packages/dumbo/src/postgres/core/schema/schema.ts @@ -1,5 +1,4 @@ -import { exists, type ConnectionPool } from '../../../core'; -import { sql, type SQL } from '../../../core/sql'; +import { exists, sql, type ConnectionPool, type SQL } from '../../../core'; export * from './schema'; export const defaultPostgreSqlDatabase = 'postgres'; diff --git a/src/packages/pongo/src/postgres/dbClient.ts b/src/packages/pongo/src/postgres/dbClient.ts index c954ea31..af2b2b97 100644 --- a/src/packages/pongo/src/postgres/dbClient.ts +++ b/src/packages/pongo/src/postgres/dbClient.ts @@ -8,7 +8,7 @@ import { type DbClient, type PongoDbClientOptions, } from '../core'; -import { postgresSQLBuilder } from './collectionSqlBuilder'; +import { postgresSQLBuilder } from './sqlBuilder'; export type PostgresDbClientOptions = PongoDbClientOptions<'PostgreSQL'> & PostgresPoolOptions; diff --git a/src/packages/pongo/src/postgres/index.ts b/src/packages/pongo/src/postgres/index.ts index cfca35e9..7d7adf16 100644 --- a/src/packages/pongo/src/postgres/index.ts +++ b/src/packages/pongo/src/postgres/index.ts @@ -1,4 +1,2 @@ -export * from './collectionSqlBuilder'; export * from './dbClient'; -export * from './filter'; -export * from './update'; +export * from './sqlBuilder'; diff --git a/src/packages/pongo/src/postgres/filter/index.ts b/src/packages/pongo/src/postgres/sqlBuilder/filter/index.ts similarity index 91% rename from src/packages/pongo/src/postgres/filter/index.ts rename to src/packages/pongo/src/postgres/sqlBuilder/filter/index.ts index 0fea691d..7f442e4d 100644 --- a/src/packages/pongo/src/postgres/filter/index.ts +++ b/src/packages/pongo/src/postgres/sqlBuilder/filter/index.ts @@ -1,5 +1,4 @@ -import type { PongoFilter } from '../../core'; -import { entries } from '../../core/typing'; +import { entries, type PongoFilter } from '../../../core'; import { Operators, handleOperator, hasOperators } from './queryOperators'; export * from './queryOperators'; diff --git a/src/packages/pongo/src/postgres/filter/queryOperators.ts b/src/packages/pongo/src/postgres/sqlBuilder/filter/queryOperators.ts similarity index 98% rename from src/packages/pongo/src/postgres/filter/queryOperators.ts rename to src/packages/pongo/src/postgres/sqlBuilder/filter/queryOperators.ts index 977bb577..17561033 100644 --- a/src/packages/pongo/src/postgres/filter/queryOperators.ts +++ b/src/packages/pongo/src/postgres/sqlBuilder/filter/queryOperators.ts @@ -1,5 +1,5 @@ import format from 'pg-format'; -import { entries } from '../../core/typing'; +import { entries } from '../../../core'; export const Operators = { $eq: '$eq', diff --git a/src/packages/pongo/src/postgres/collectionSqlBuilder.ts b/src/packages/pongo/src/postgres/sqlBuilder/index.ts similarity index 97% rename from src/packages/pongo/src/postgres/collectionSqlBuilder.ts rename to src/packages/pongo/src/postgres/sqlBuilder/index.ts index dbd366cc..59fec171 100644 --- a/src/packages/pongo/src/postgres/collectionSqlBuilder.ts +++ b/src/packages/pongo/src/postgres/sqlBuilder/index.ts @@ -1,12 +1,12 @@ import { sql, type SQL } from '@event-driven-io/dumbo'; import format from 'pg-format'; +import type { PongoCollectionSQLBuilder } from '../../core'; import { type PongoFilter, type PongoUpdate, type WithId, type WithoutId, -} from '../core'; -import type { PongoCollectionSQLBuilder } from '../core/pongoCollection'; +} from '../../core'; import { constructFilterQuery } from './filter'; import { buildUpdateQuery } from './update'; diff --git a/src/packages/pongo/src/postgres/update/index.ts b/src/packages/pongo/src/postgres/sqlBuilder/update/index.ts similarity index 93% rename from src/packages/pongo/src/postgres/update/index.ts rename to src/packages/pongo/src/postgres/sqlBuilder/update/index.ts index 8a217281..94a55c18 100644 --- a/src/packages/pongo/src/postgres/update/index.ts +++ b/src/packages/pongo/src/postgres/sqlBuilder/update/index.ts @@ -1,6 +1,12 @@ import { sql, type SQL } from '@event-driven-io/dumbo'; -import type { $inc, $push, $set, $unset, PongoUpdate } from '../../core'; -import { entries } from '../../core/typing'; +import { + entries, + type $inc, + type $push, + type $set, + type $unset, + type PongoUpdate, +} from '../../../core'; export const buildUpdateQuery = (update: PongoUpdate): SQL => entries(update).reduce((currentUpdateQuery, [op, value]) => { From b68edb1856ede4cd0d7c710498b30c2abc4a9054 Mon Sep 17 00:00:00 2001 From: Oskar Dudycz Date: Sat, 3 Aug 2024 11:34:40 +0200 Subject: [PATCH 21/30] Generalised typing for query operators --- .../pongo/src/core/collection/index.ts | 2 ++ .../core/{ => collection}/pongoCollection.ts | 2 +- .../pongo/src/core/collection/query.ts | 26 +++++++++++++++++ src/packages/pongo/src/core/index.ts | 2 +- .../src/postgres/sqlBuilder/filter/index.ts | 11 +++++-- .../sqlBuilder/filter/queryOperators.ts | 29 +------------------ .../pongo/src/postgres/sqlBuilder/index.ts | 2 +- 7 files changed, 40 insertions(+), 34 deletions(-) create mode 100644 src/packages/pongo/src/core/collection/index.ts rename src/packages/pongo/src/core/{ => collection}/pongoCollection.ts (99%) create mode 100644 src/packages/pongo/src/core/collection/query.ts diff --git a/src/packages/pongo/src/core/collection/index.ts b/src/packages/pongo/src/core/collection/index.ts new file mode 100644 index 00000000..95c7dd4a --- /dev/null +++ b/src/packages/pongo/src/core/collection/index.ts @@ -0,0 +1,2 @@ +export * from './pongoCollection'; +export * from './query'; diff --git a/src/packages/pongo/src/core/pongoCollection.ts b/src/packages/pongo/src/core/collection/pongoCollection.ts similarity index 99% rename from src/packages/pongo/src/core/pongoCollection.ts rename to src/packages/pongo/src/core/collection/pongoCollection.ts index d5418273..db031c09 100644 --- a/src/packages/pongo/src/core/pongoCollection.ts +++ b/src/packages/pongo/src/core/collection/pongoCollection.ts @@ -12,7 +12,7 @@ import { type PongoUpdateResult, type WithId, type WithoutId, -} from '.'; +} from '..'; export type PongoCollectionOptions = { collectionName: string; diff --git a/src/packages/pongo/src/core/collection/query.ts b/src/packages/pongo/src/core/collection/query.ts new file mode 100644 index 00000000..aad59467 --- /dev/null +++ b/src/packages/pongo/src/core/collection/query.ts @@ -0,0 +1,26 @@ +export const QueryOperators = { + $eq: '$eq', + $gt: '$gt', + $gte: '$gte', + $lt: '$lt', + $lte: '$lte', + $ne: '$ne', + $in: '$in', + $nin: '$nin', + $elemMatch: '$elemMatch', + $all: '$all', + $size: '$size', +}; + +export const OperatorMap = { + $gt: '>', + $gte: '>=', + $lt: '<', + $lte: '<=', + $ne: '!=', +}; + +export const isOperator = (key: string) => key.startsWith('$'); + +export const hasOperators = (value: Record) => + Object.keys(value).some(isOperator); diff --git a/src/packages/pongo/src/core/index.ts b/src/packages/pongo/src/core/index.ts index 48ecb492..27d2f657 100644 --- a/src/packages/pongo/src/core/index.ts +++ b/src/packages/pongo/src/core/index.ts @@ -1,5 +1,5 @@ +export * from './collection'; export * from './dbClient'; export * from './pongoClient'; -export * from './pongoCollection'; export * from './pongoSession'; export * from './typing'; diff --git a/src/packages/pongo/src/postgres/sqlBuilder/filter/index.ts b/src/packages/pongo/src/postgres/sqlBuilder/filter/index.ts index 7f442e4d..a67ff721 100644 --- a/src/packages/pongo/src/postgres/sqlBuilder/filter/index.ts +++ b/src/packages/pongo/src/postgres/sqlBuilder/filter/index.ts @@ -1,5 +1,10 @@ -import { entries, type PongoFilter } from '../../../core'; -import { Operators, handleOperator, hasOperators } from './queryOperators'; +import { + entries, + hasOperators, + QueryOperators, + type PongoFilter, +} from '../../../core'; +import { handleOperator } from './queryOperators'; export * from './queryOperators'; @@ -24,7 +29,7 @@ const constructComplexFilterQuery = ( .map( ([nestedKey, val]) => isEquality - ? handleOperator(`${key}.${nestedKey}`, Operators.$eq, val) // regular value + ? handleOperator(`${key}.${nestedKey}`, QueryOperators.$eq, val) // regular value : handleOperator(key, nestedKey, val), // operator ) .join(` ${AND} `); diff --git a/src/packages/pongo/src/postgres/sqlBuilder/filter/queryOperators.ts b/src/packages/pongo/src/postgres/sqlBuilder/filter/queryOperators.ts index 17561033..8dd05077 100644 --- a/src/packages/pongo/src/postgres/sqlBuilder/filter/queryOperators.ts +++ b/src/packages/pongo/src/postgres/sqlBuilder/filter/queryOperators.ts @@ -1,32 +1,5 @@ import format from 'pg-format'; -import { entries } from '../../../core'; - -export const Operators = { - $eq: '$eq', - $gt: '$gt', - $gte: '$gte', - $lt: '$lt', - $lte: '$lte', - $ne: '$ne', - $in: '$in', - $nin: '$nin', - $elemMatch: '$elemMatch', - $all: '$all', - $size: '$size', -}; - -const OperatorMap = { - $gt: '>', - $gte: '>=', - $lt: '<', - $lte: '<=', - $ne: '!=', -}; - -export const isOperator = (key: string) => key.startsWith('$'); - -export const hasOperators = (value: Record) => - Object.keys(value).some(isOperator); +import { entries, OperatorMap } from '../../../core'; export const handleOperator = ( path: string, diff --git a/src/packages/pongo/src/postgres/sqlBuilder/index.ts b/src/packages/pongo/src/postgres/sqlBuilder/index.ts index 59fec171..cb5a3d57 100644 --- a/src/packages/pongo/src/postgres/sqlBuilder/index.ts +++ b/src/packages/pongo/src/postgres/sqlBuilder/index.ts @@ -1,7 +1,7 @@ import { sql, type SQL } from '@event-driven-io/dumbo'; import format from 'pg-format'; -import type { PongoCollectionSQLBuilder } from '../../core'; import { + type PongoCollectionSQLBuilder, type PongoFilter, type PongoUpdate, type WithId, From 175546958a3374b9354c9b70ed162bebbc42662d Mon Sep 17 00:00:00 2001 From: Oskar Dudycz Date: Sat, 3 Aug 2024 11:59:54 +0200 Subject: [PATCH 22/30] Made possible to inject Session to collection operations Note: it's not yet used. That'll be added in the follow up commits. --- .../src/core/collection/pongoCollection.ts | 153 ++++++++++++------ .../pongo/src/core/typing/operations.ts | 64 ++++++-- 2 files changed, 156 insertions(+), 61 deletions(-) diff --git a/src/packages/pongo/src/core/collection/pongoCollection.ts b/src/packages/pongo/src/core/collection/pongoCollection.ts index db031c09..1ce1a350 100644 --- a/src/packages/pongo/src/core/collection/pongoCollection.ts +++ b/src/packages/pongo/src/core/collection/pongoCollection.ts @@ -1,6 +1,12 @@ -import { single, type SQL, type SQLExecutor } from '@event-driven-io/dumbo'; +import { + single, + type QueryResultRow, + type SQL, + type SQLExecutor, +} from '@event-driven-io/dumbo'; import { v4 as uuid } from 'uuid'; import { + type CollectionOperationOptions, type DocumentHandler, type PongoCollection, type PongoDeleteResult, @@ -24,37 +30,55 @@ export type PongoCollectionOptions = { export const pongoCollection = ({ collectionName, dbName, - sqlExecutor: { command, query }, + sqlExecutor, sqlBuilder: SqlFor, }: PongoCollectionOptions): PongoCollection => { - const createCollection = command(SqlFor.createCollection()); + const command = (sql: SQL, _options?: CollectionOperationOptions) => + sqlExecutor.command(sql); + const query = ( + sql: SQL, + _options?: CollectionOperationOptions, + ) => sqlExecutor.query(sql); + + const createCollectionPromise = command(SqlFor.createCollection()); + const createCollection = (options?: CollectionOperationOptions) => + options?.session ? createCollectionPromise : createCollectionPromise; const collection = { dbName, collectionName, - createCollection: async () => { - await createCollection; + createCollection: async (options?: CollectionOperationOptions) => { + await createCollection(options); }, - insertOne: async (document: T): Promise => { - await createCollection; + insertOne: async ( + document: T, + options?: CollectionOperationOptions, + ): Promise => { + await createCollection(options); const _id = uuid(); - const result = await command(SqlFor.insertOne({ _id, ...document })); + const result = await command( + SqlFor.insertOne({ _id, ...document }), + options, + ); return result.rowCount ? { insertedId: _id, acknowledged: true } : { insertedId: null, acknowledged: false }; }, - insertMany: async (documents: T[]): Promise => { - await createCollection; + insertMany: async ( + documents: T[], + options?: CollectionOperationOptions, + ): Promise => { + await createCollection(options); const rows = documents.map((doc) => ({ _id: uuid(), ...doc, })); - const result = await command(SqlFor.insertMany(rows)); + const result = await command(SqlFor.insertMany(rows), options); return { acknowledged: result.rowCount === rows.length, @@ -65,10 +89,11 @@ export const pongoCollection = ({ updateOne: async ( filter: PongoFilter, update: PongoUpdate, + options?: CollectionOperationOptions, ): Promise => { - await createCollection; + await createCollection(options); - const result = await command(SqlFor.updateOne(filter, update)); + const result = await command(SqlFor.updateOne(filter, update), options); return result.rowCount ? { acknowledged: true, modifiedCount: result.rowCount } : { acknowledged: false, modifiedCount: 0 }; @@ -76,10 +101,14 @@ export const pongoCollection = ({ replaceOne: async ( filter: PongoFilter, document: WithoutId, + options?: CollectionOperationOptions, ): Promise => { - await createCollection; + await createCollection(options); - const result = await command(SqlFor.replaceOne(filter, document)); + const result = await command( + SqlFor.replaceOne(filter, document), + options, + ); return result.rowCount ? { acknowledged: true, modifiedCount: result.rowCount } : { acknowledged: false, modifiedCount: 0 }; @@ -87,122 +116,148 @@ export const pongoCollection = ({ updateMany: async ( filter: PongoFilter, update: PongoUpdate, + options?: CollectionOperationOptions, ): Promise => { - await createCollection; + await createCollection(options); - const result = await command(SqlFor.updateMany(filter, update)); + const result = await command(SqlFor.updateMany(filter, update), options); return result.rowCount ? { acknowledged: true, modifiedCount: result.rowCount } : { acknowledged: false, modifiedCount: 0 }; }, - deleteOne: async (filter?: PongoFilter): Promise => { - await createCollection; + deleteOne: async ( + filter?: PongoFilter, + options?: CollectionOperationOptions, + ): Promise => { + await createCollection(options); - const result = await command(SqlFor.deleteOne(filter ?? {})); + const result = await command(SqlFor.deleteOne(filter ?? {}), options); return result.rowCount ? { acknowledged: true, deletedCount: result.rowCount } : { acknowledged: false, deletedCount: 0 }; }, - deleteMany: async (filter?: PongoFilter): Promise => { - await createCollection; + deleteMany: async ( + filter?: PongoFilter, + options?: CollectionOperationOptions, + ): Promise => { + await createCollection(options); - const result = await command(SqlFor.deleteMany(filter ?? {})); + const result = await command(SqlFor.deleteMany(filter ?? {}), options); return result.rowCount ? { acknowledged: true, deletedCount: result.rowCount } : { acknowledged: false, deletedCount: 0 }; }, - findOne: async (filter?: PongoFilter): Promise => { - await createCollection; + findOne: async ( + filter?: PongoFilter, + options?: CollectionOperationOptions, + ): Promise => { + await createCollection(options); - const result = await query(SqlFor.findOne(filter ?? {})); + const result = await query(SqlFor.findOne(filter ?? {}), options); return (result.rows[0]?.data ?? null) as T | null; }, - findOneAndDelete: async (filter: PongoFilter): Promise => { - await createCollection; + findOneAndDelete: async ( + filter: PongoFilter, + options?: CollectionOperationOptions, + ): Promise => { + await createCollection(options); - const existingDoc = await collection.findOne(filter); + const existingDoc = await collection.findOne(filter, options); if (existingDoc === null) return null; - await collection.deleteOne(filter); + await collection.deleteOne(filter, options); return existingDoc; }, findOneAndReplace: async ( filter: PongoFilter, replacement: WithoutId, + options?: CollectionOperationOptions, ): Promise => { - await createCollection; + await createCollection(options); - const existingDoc = await collection.findOne(filter); + const existingDoc = await collection.findOne(filter, options); if (existingDoc === null) return null; - await collection.replaceOne(filter, replacement); + await collection.replaceOne(filter, replacement, options); return existingDoc; }, findOneAndUpdate: async ( filter: PongoFilter, update: PongoUpdate, + options?: CollectionOperationOptions, ): Promise => { - await createCollection; + await createCollection(options); - const existingDoc = await collection.findOne(filter); + const existingDoc = await collection.findOne(filter, options); if (existingDoc === null) return null; - await collection.updateOne(filter, update); + await collection.updateOne(filter, update, options); return existingDoc; }, handle: async ( id: string, handle: DocumentHandler, + options?: CollectionOperationOptions, ): Promise => { - await createCollection; + await createCollection(options); const byId: PongoFilter = { _id: id }; - const existing = await collection.findOne(byId); + const existing = await collection.findOne(byId, options); const result = await handle(existing); if (!existing && result) { const newDoc = { ...result, _id: id }; - await collection.insertOne({ ...newDoc, _id: id }); + await collection.insertOne({ ...newDoc, _id: id }, options); return newDoc; } if (existing && !result) { - await collection.deleteOne(byId); + await collection.deleteOne(byId, options); return null; } - if (existing && result) await collection.replaceOne(byId, result); + if (existing && result) + await collection.replaceOne(byId, result, options); return result; }, - find: async (filter?: PongoFilter): Promise => { - await createCollection; + find: async ( + filter?: PongoFilter, + options?: CollectionOperationOptions, + ): Promise => { + await createCollection(options); const result = await query(SqlFor.find(filter ?? {})); return result.rows.map((row) => row.data as T); }, - countDocuments: async (filter?: PongoFilter): Promise => { - await createCollection; + countDocuments: async ( + filter?: PongoFilter, + options?: CollectionOperationOptions, + ): Promise => { + await createCollection(options); const { count } = await single( query<{ count: number }>(SqlFor.countDocuments(filter ?? {})), ); return count; }, - drop: async (): Promise => { - await createCollection; + drop: async (options?: CollectionOperationOptions): Promise => { + await createCollection(options); const result = await command(SqlFor.drop()); return (result?.rowCount ?? 0) > 0; }, - rename: async (newName: string): Promise> => { - await createCollection; + rename: async ( + newName: string, + options?: CollectionOperationOptions, + ): Promise> => { + await createCollection(options); await command(SqlFor.rename(newName)); collectionName = newName; return collection; diff --git a/src/packages/pongo/src/core/typing/operations.ts b/src/packages/pongo/src/core/typing/operations.ts index b5b07188..2b11ecac 100644 --- a/src/packages/pongo/src/core/typing/operations.ts +++ b/src/packages/pongo/src/core/typing/operations.ts @@ -51,41 +51,81 @@ export interface PongoDb { collection(name: string): PongoCollection; } +export type CollectionOperationOptions = { + session?: PongoSession; +}; + export interface PongoCollection { readonly dbName: string; readonly collectionName: string; - createCollection(): Promise; - insertOne(document: T): Promise; - insertMany(documents: T[]): Promise; + createCollection(options?: CollectionOperationOptions): Promise; + insertOne( + document: T, + options?: CollectionOperationOptions, + ): Promise; + insertMany( + documents: T[], + options?: CollectionOperationOptions, + ): Promise; updateOne( filter: PongoFilter, update: PongoUpdate, + options?: CollectionOperationOptions, ): Promise; replaceOne( filter: PongoFilter, document: WithoutId, + options?: CollectionOperationOptions, ): Promise; updateMany( filter: PongoFilter, update: PongoUpdate, + options?: CollectionOperationOptions, ): Promise; - deleteOne(filter?: PongoFilter): Promise; - deleteMany(filter?: PongoFilter): Promise; - findOne(filter?: PongoFilter): Promise; - find(filter?: PongoFilter): Promise; - findOneAndDelete(filter: PongoFilter): Promise; + deleteOne( + filter?: PongoFilter, + options?: CollectionOperationOptions, + ): Promise; + deleteMany( + filter?: PongoFilter, + options?: CollectionOperationOptions, + ): Promise; + findOne( + filter?: PongoFilter, + options?: CollectionOperationOptions, + ): Promise; + find( + filter?: PongoFilter, + options?: CollectionOperationOptions, + ): Promise; + findOneAndDelete( + filter: PongoFilter, + options?: CollectionOperationOptions, + ): Promise; findOneAndReplace( filter: PongoFilter, replacement: WithoutId, + options?: CollectionOperationOptions, ): Promise; findOneAndUpdate( filter: PongoFilter, update: PongoUpdate, + options?: CollectionOperationOptions, + ): Promise; + countDocuments( + filter?: PongoFilter, + options?: CollectionOperationOptions, + ): Promise; + drop(options?: CollectionOperationOptions): Promise; + rename( + newName: string, + options?: CollectionOperationOptions, + ): Promise>; + handle( + id: string, + handle: DocumentHandler, + options?: CollectionOperationOptions, ): Promise; - countDocuments(filter?: PongoFilter): Promise; - drop(): Promise; - rename(newName: string): Promise>; - handle(id: string, handle: DocumentHandler): Promise; } export type HasId = { _id: string }; From 11c4036ff4ff3dba78efaa06ec3a93edb32026a9 Mon Sep 17 00:00:00 2001 From: Oskar Dudycz Date: Sat, 3 Aug 2024 12:33:37 +0200 Subject: [PATCH 23/30] Used database transaction for Pongo Renamed also Transaction to DatabaseTransaction to make explicit that's related to database --- .../dumbo/src/core/connections/connection.ts | 4 +- .../dumbo/src/core/connections/pool.ts | 4 +- .../dumbo/src/core/connections/transaction.ts | 31 +++++----- .../postgres/pg/connections/transaction.ts | 7 ++- src/packages/pongo/src/core/dbClient.ts | 8 +-- src/packages/pongo/src/core/pongoSession.ts | 56 +++++++++++++++---- .../pongo/src/core/typing/operations.ts | 8 ++- src/packages/pongo/src/mongo/mongoDb.ts | 4 ++ src/packages/pongo/src/postgres/dbClient.ts | 1 + 9 files changed, 83 insertions(+), 40 deletions(-) diff --git a/src/packages/dumbo/src/core/connections/connection.ts b/src/packages/dumbo/src/core/connections/connection.ts index 74a40c09..8d28498a 100644 --- a/src/packages/dumbo/src/core/connections/connection.ts +++ b/src/packages/dumbo/src/core/connections/connection.ts @@ -1,5 +1,5 @@ import type { WithSQLExecutor } from '../execute'; -import type { TransactionFactory } from './transaction'; +import type { DatabaseTransactionFactory } from './transaction'; export type Connection< ConnectorType extends string = string, @@ -9,4 +9,4 @@ export type Connection< connect: () => Promise; close: () => Promise; } & WithSQLExecutor & - TransactionFactory; + DatabaseTransactionFactory; diff --git a/src/packages/dumbo/src/core/connections/pool.ts b/src/packages/dumbo/src/core/connections/pool.ts index dad54953..8edbfde4 100644 --- a/src/packages/dumbo/src/core/connections/pool.ts +++ b/src/packages/dumbo/src/core/connections/pool.ts @@ -1,13 +1,13 @@ import type { WithSQLExecutor } from '../execute'; import { type Connection } from './connection'; -import type { TransactionFactory } from './transaction'; +import type { DatabaseTransactionFactory } from './transaction'; export type ConnectionPool = { type: ConnectionType['type']; open: () => Promise; close: () => Promise; } & WithSQLExecutor & - TransactionFactory; + DatabaseTransactionFactory; export type ConnectionPoolProvider< ConnectionPoolType extends ConnectionPool = ConnectionPool, diff --git a/src/packages/dumbo/src/core/connections/transaction.ts b/src/packages/dumbo/src/core/connections/transaction.ts index 099cddfa..e55b1027 100644 --- a/src/packages/dumbo/src/core/connections/transaction.ts +++ b/src/packages/dumbo/src/core/connections/transaction.ts @@ -1,30 +1,31 @@ import type { WithSQLExecutor } from '../execute'; import { type Connection } from './connection'; -export type Transaction = { +export type DatabaseTransaction = { type: ConnectorType; begin: () => Promise; commit: () => Promise; rollback: (error?: unknown) => Promise; } & WithSQLExecutor; -export type TransactionFactory = { - transaction: () => Transaction; +export type DatabaseTransactionFactory = + { + transaction: () => DatabaseTransaction; - inTransaction: ( - handle: ( - transaction: Transaction, - ) => Promise<{ success: boolean; result: Result }>, - ) => Promise; -}; + inTransaction: ( + handle: ( + transaction: DatabaseTransaction, + ) => Promise<{ success: boolean; result: Result }>, + ) => Promise; + }; export const executeInTransaction = async < ConnectorType extends string = string, Result = unknown, >( - transaction: Transaction, + transaction: DatabaseTransaction, handle: ( - transaction: Transaction, + transaction: DatabaseTransaction, ) => Promise<{ success: boolean; result: Result }>, ): Promise => { await transaction.begin(); @@ -47,8 +48,10 @@ export const transactionFactoryWithDbClient = < DbClient = unknown, >( connect: () => Promise, - initTransaction: (client: Promise) => Transaction, -): TransactionFactory => ({ + initTransaction: ( + client: Promise, + ) => DatabaseTransaction, +): DatabaseTransactionFactory => ({ transaction: () => initTransaction(connect()), inTransaction: (handle) => executeInTransaction(initTransaction(connect()), handle), @@ -58,7 +61,7 @@ export const transactionFactoryWithNewConnection = < ConnectionType extends Connection = Connection, >( connect: () => ConnectionType, -): TransactionFactory => ({ +): DatabaseTransactionFactory => ({ transaction: () => connect().transaction(), inTransaction: async (handle) => { const connection = connect(); diff --git a/src/packages/dumbo/src/postgres/pg/connections/transaction.ts b/src/packages/dumbo/src/postgres/pg/connections/transaction.ts index 6d026ab1..a68d9dcd 100644 --- a/src/packages/dumbo/src/postgres/pg/connections/transaction.ts +++ b/src/packages/dumbo/src/postgres/pg/connections/transaction.ts @@ -1,4 +1,4 @@ -import { sqlExecutor, type Transaction } from '../../../core'; +import { sqlExecutor, type DatabaseTransaction } from '../../../core'; import { nodePostgresSQLExecutor } from '../execute'; import { NodePostgresConnectorType, @@ -6,14 +6,15 @@ import { type NodePostgresPoolOrClient, } from './connection'; -export type NodePostgresTransaction = Transaction; +export type NodePostgresTransaction = + DatabaseTransaction; export const nodePostgresTransaction = < DbClient extends NodePostgresPoolOrClient = NodePostgresPoolOrClient, >( getClient: Promise, options?: { close: (client: DbClient, error?: unknown) => Promise }, -): Transaction => ({ +): DatabaseTransaction => ({ type: NodePostgresConnectorType, begin: async () => { const client = await getClient; diff --git a/src/packages/pongo/src/core/dbClient.ts b/src/packages/pongo/src/core/dbClient.ts index d85a9169..5aaad0df 100644 --- a/src/packages/pongo/src/core/dbClient.ts +++ b/src/packages/pongo/src/core/dbClient.ts @@ -1,9 +1,10 @@ +import type { ConnectionPool } from '@event-driven-io/dumbo'; import { isPostgresClientOptions, postgresDbClient, type PostgresDbClientOptions, } from '../postgres'; -import type { PongoCollection, PongoDocument } from './typing'; +import type { PongoDb } from './typing'; export type PongoDbClientOptions< DbType extends string = string, @@ -16,12 +17,11 @@ export type PongoDbClientOptions< export interface DbClient< DbClientOptions extends PongoDbClientOptions = PongoDbClientOptions, -> { - databaseName: string; +> extends PongoDb { + pool: ConnectionPool; options: DbClientOptions; connect(): Promise; close(): Promise; - collection: (name: string) => PongoCollection; } export type AllowedDbClientOptions = PostgresDbClientOptions; diff --git a/src/packages/pongo/src/core/pongoSession.ts b/src/packages/pongo/src/core/pongoSession.ts index 92729aad..474e11cd 100644 --- a/src/packages/pongo/src/core/pongoSession.ts +++ b/src/packages/pongo/src/core/pongoSession.ts @@ -1,3 +1,5 @@ +import { type DatabaseTransaction } from '@event-driven-io/dumbo'; +import type { DbClient } from './dbClient'; import type { PongoSession, PongoTransaction, @@ -9,6 +11,47 @@ export type PongoSessionOptions = { defaultTransactionOptions: PongoTransactionOptions; }; +const pongoTransaction = ( + options: PongoTransactionOptions, +): PongoTransaction => { + const isStarting = false; + const isActive = true; + const isCommitted = false; + let databaseName: string | null; + let transaction: DatabaseTransaction | null = null; + + return { + get isStarting() { + return isStarting; + }, + get isActive() { + return isActive; + }, + get isCommitted() { + return isCommitted; + }, + get sqlExecutor() { + if (transaction === null) + throw new Error('No database transaction was started'); + + return transaction.execute; + }, + useDatabase: (db: DbClient) => { + if (transaction && databaseName !== db.databaseName) + throw new Error( + "There's already other database assigned to transaction", + ); + + if (transaction && databaseName === db.databaseName) return; + + databaseName = db.databaseName; + + transaction = db.pool.transaction(); + }, + options, + }; +}; + export const pongoSession = (options?: PongoSessionOptions): PongoSession => { const explicit = options?.explicit === true; const defaultTransactionOptions: PongoTransactionOptions = @@ -25,22 +68,13 @@ export const pongoSession = (options?: PongoSessionOptions): PongoSession => { if (transaction?.isActive === true) throw new Error('Active transaction already exists!'); - transaction = { - db: null, - transaction: null, - isStarting: false, - isActive: true, - isCommitted: false, - options: options ?? defaultTransactionOptions, - }; + return pongoTransaction(options ?? defaultTransactionOptions); }; const commitTransaction = () => { if (transaction?.isActive !== true) return Promise.reject('No active transaction!'); transaction = { - db: null, - transaction: null, isStarting: false, isActive: false, isCommitted: true, @@ -53,8 +87,6 @@ export const pongoSession = (options?: PongoSessionOptions): PongoSession => { return Promise.reject('No active transaction!'); transaction = { - db: null, - transaction: null, isStarting: false, isActive: false, isCommitted: false, diff --git a/src/packages/pongo/src/core/typing/operations.ts b/src/packages/pongo/src/core/typing/operations.ts index 2b11ecac..749ffcb3 100644 --- a/src/packages/pongo/src/core/typing/operations.ts +++ b/src/packages/pongo/src/core/typing/operations.ts @@ -1,4 +1,5 @@ -import type { Transaction } from '@event-driven-io/dumbo'; +import type { SQLExecutor } from '@event-driven-io/dumbo'; +import type { DbClient } from '../dbClient'; export interface PongoClient { connect(): Promise; @@ -20,8 +21,8 @@ export declare interface PongoTransactionOptions { } export interface PongoTransaction { - db: PongoDb | null; - transaction: Transaction | null; + useDatabase: (database: DbClient) => void; + get sqlExecutor(): SQLExecutor; options: PongoTransactionOptions; get isStarting(): boolean; get isActive(): boolean; @@ -48,6 +49,7 @@ export interface PongoSession { } export interface PongoDb { + get databaseName(): string; collection(name: string): PongoCollection; } diff --git a/src/packages/pongo/src/mongo/mongoDb.ts b/src/packages/pongo/src/mongo/mongoDb.ts index dd9f72a3..2cd77ce4 100644 --- a/src/packages/pongo/src/mongo/mongoDb.ts +++ b/src/packages/pongo/src/mongo/mongoDb.ts @@ -9,6 +9,10 @@ import { Collection } from './mongoCollection'; export class Db { constructor(private pongoDb: PongoDb) {} + get databaseName(): string { + return this.pongoDb.databaseName; + } + collection( collectionName: string, ): MongoCollection & { diff --git a/src/packages/pongo/src/postgres/dbClient.ts b/src/packages/pongo/src/postgres/dbClient.ts index af2b2b97..f4bb266f 100644 --- a/src/packages/pongo/src/postgres/dbClient.ts +++ b/src/packages/pongo/src/postgres/dbClient.ts @@ -26,6 +26,7 @@ export const postgresDbClient = ( const pool = postgresPool(options); return { + pool, databaseName, options, connect: () => Promise.resolve(), From 12f6e29491e7ecb69be6f94ed32cd327fe815d2c Mon Sep 17 00:00:00 2001 From: Oskar Dudycz Date: Sat, 3 Aug 2024 12:52:55 +0200 Subject: [PATCH 24/30] Implemented Pongo Transaction handling --- src/packages/pongo/src/core/pongoSession.ts | 92 ++++++++++--------- .../pongo/src/core/typing/operations.ts | 11 ++- 2 files changed, 56 insertions(+), 47 deletions(-) diff --git a/src/packages/pongo/src/core/pongoSession.ts b/src/packages/pongo/src/core/pongoSession.ts index 474e11cd..a76d980a 100644 --- a/src/packages/pongo/src/core/pongoSession.ts +++ b/src/packages/pongo/src/core/pongoSession.ts @@ -1,8 +1,8 @@ import { type DatabaseTransaction } from '@event-driven-io/dumbo'; import type { DbClient } from './dbClient'; import type { + PongoDbTransaction, PongoSession, - PongoTransaction, PongoTransactionOptions, } from './typing'; @@ -13,30 +13,14 @@ export type PongoSessionOptions = { const pongoTransaction = ( options: PongoTransactionOptions, -): PongoTransaction => { - const isStarting = false; - const isActive = true; - const isCommitted = false; - let databaseName: string | null; +): PongoDbTransaction => { + let isCommitted = false; + let isRolledBack = false; + let databaseName: string | null = null; let transaction: DatabaseTransaction | null = null; return { - get isStarting() { - return isStarting; - }, - get isActive() { - return isActive; - }, - get isCommitted() { - return isCommitted; - }, - get sqlExecutor() { - if (transaction === null) - throw new Error('No database transaction was started'); - - return transaction.execute; - }, - useDatabase: (db: DbClient) => { + startDbTransaction: async (db: DbClient) => { if (transaction && databaseName !== db.databaseName) throw new Error( "There's already other database assigned to transaction", @@ -45,8 +29,42 @@ const pongoTransaction = ( if (transaction && databaseName === db.databaseName) return; databaseName = db.databaseName; - transaction = db.pool.transaction(); + await transaction.begin(); + }, + commit: async () => { + if (isCommitted) return; + if (!isRolledBack) throw new Error('Transaction is not active!'); + if (!transaction) throw new Error('No database transaction started!'); + + isCommitted = true; + + await transaction.commit(); + + transaction = null; + }, + rollback: async (error?: unknown) => { + if (isCommitted) throw new Error('Cannot rollback commited transaction!'); + if (!isRolledBack) return; + if (!transaction) throw new Error('No database transaction started!'); + + isRolledBack = true; + + await transaction.rollback(error); + + transaction = null; + }, + databaseName, + isStarting: false, + isCommitted, + get isActive() { + return !isCommitted && !isRolledBack; + }, + get sqlExecutor() { + if (transaction === null) + throw new Error('No database transaction was started'); + + return transaction.execute; }, options, }; @@ -61,38 +79,26 @@ export const pongoSession = (options?: PongoSessionOptions): PongoSession => { }, }; - let transaction: PongoTransaction | null = null; + let transaction: PongoDbTransaction | null = null; let hasEnded = false; const startTransaction = (options?: PongoTransactionOptions) => { if (transaction?.isActive === true) throw new Error('Active transaction already exists!'); - return pongoTransaction(options ?? defaultTransactionOptions); + transaction = pongoTransaction(options ?? defaultTransactionOptions); }; - const commitTransaction = () => { + const commitTransaction = async () => { if (transaction?.isActive !== true) - return Promise.reject('No active transaction!'); + throw new Error('No active transaction!'); - transaction = { - isStarting: false, - isActive: false, - isCommitted: true, - options: transaction.options, - }; - return Promise.resolve(); + await transaction.commit(); }; - const abortTransaction = () => { + const abortTransaction = async () => { if (transaction?.isActive !== true) - return Promise.reject('No active transaction!'); + throw new Error('No active transaction!'); - transaction = { - isStarting: false, - isActive: false, - isCommitted: false, - options: transaction.options, - }; - return Promise.resolve(); + await transaction.rollback(); }; const session = { diff --git a/src/packages/pongo/src/core/typing/operations.ts b/src/packages/pongo/src/core/typing/operations.ts index 749ffcb3..a38b69e5 100644 --- a/src/packages/pongo/src/core/typing/operations.ts +++ b/src/packages/pongo/src/core/typing/operations.ts @@ -20,10 +20,13 @@ export declare interface PongoTransactionOptions { maxCommitTimeMS?: number; } -export interface PongoTransaction { - useDatabase: (database: DbClient) => void; - get sqlExecutor(): SQLExecutor; +export interface PongoDbTransaction { + get databaseName(): string | null; options: PongoTransactionOptions; + startDbTransaction: (database: DbClient) => Promise; + commit: () => Promise; + rollback: (error?: unknown) => Promise; + get sqlExecutor(): SQLExecutor; get isStarting(): boolean; get isActive(): boolean; get isCommitted(): boolean; @@ -33,7 +36,7 @@ export interface PongoSession { hasEnded: boolean; explicit: boolean; defaultTransactionOptions: PongoTransactionOptions; - transaction: PongoTransaction | null; + transaction: PongoDbTransaction | null; get snapshotEnabled(): boolean; endSession(): Promise; From 04c24b1439f7f949f260329255612c7bd0bec1e3 Mon Sep 17 00:00:00 2001 From: Oskar Dudycz Date: Sat, 3 Aug 2024 13:27:12 +0200 Subject: [PATCH 25/30] Merged DbClient into PongoDb --- .../src/core/collection/pongoCollection.ts | 15 +++++-- src/packages/pongo/src/core/dbClient.ts | 40 ------------------- src/packages/pongo/src/core/index.ts | 2 +- src/packages/pongo/src/core/pongoClient.ts | 14 +++---- src/packages/pongo/src/core/pongoDb.ts | 27 +++++++++++++ src/packages/pongo/src/core/pongoSession.ts | 8 ++-- .../pongo/src/core/typing/operations.ts | 15 +++++-- src/packages/pongo/src/postgres/dbClient.ts | 12 +++--- 8 files changed, 66 insertions(+), 67 deletions(-) delete mode 100644 src/packages/pongo/src/core/dbClient.ts create mode 100644 src/packages/pongo/src/core/pongoDb.ts diff --git a/src/packages/pongo/src/core/collection/pongoCollection.ts b/src/packages/pongo/src/core/collection/pongoCollection.ts index 1ce1a350..ec5c628a 100644 --- a/src/packages/pongo/src/core/collection/pongoCollection.ts +++ b/src/packages/pongo/src/core/collection/pongoCollection.ts @@ -33,12 +33,19 @@ export const pongoCollection = ({ sqlExecutor, sqlBuilder: SqlFor, }: PongoCollectionOptions): PongoCollection => { - const command = (sql: SQL, _options?: CollectionOperationOptions) => - sqlExecutor.command(sql); + const command = (sql: SQL, options?: CollectionOperationOptions) => { + const execute = options?.session?.transaction?.sqlExecutor ?? sqlExecutor; + + return execute.command(sql); + }; const query = ( sql: SQL, - _options?: CollectionOperationOptions, - ) => sqlExecutor.query(sql); + options?: CollectionOperationOptions, + ) => { + const execute = options?.session?.transaction?.sqlExecutor ?? sqlExecutor; + + return execute.query(sql); + }; const createCollectionPromise = command(SqlFor.createCollection()); const createCollection = (options?: CollectionOperationOptions) => diff --git a/src/packages/pongo/src/core/dbClient.ts b/src/packages/pongo/src/core/dbClient.ts deleted file mode 100644 index 5aaad0df..00000000 --- a/src/packages/pongo/src/core/dbClient.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type { ConnectionPool } from '@event-driven-io/dumbo'; -import { - isPostgresClientOptions, - postgresDbClient, - type PostgresDbClientOptions, -} from '../postgres'; -import type { PongoDb } from './typing'; - -export type PongoDbClientOptions< - DbType extends string = string, - Additional = unknown, -> = { - type: DbType; - connectionString: string; - dbName: string | undefined; -} & Additional; - -export interface DbClient< - DbClientOptions extends PongoDbClientOptions = PongoDbClientOptions, -> extends PongoDb { - pool: ConnectionPool; - options: DbClientOptions; - connect(): Promise; - close(): Promise; -} - -export type AllowedDbClientOptions = PostgresDbClientOptions; - -export const getDbClient = < - DbClientOptions extends AllowedDbClientOptions = AllowedDbClientOptions, ->( - options: DbClientOptions, -): DbClient => { - const { type } = options; - // This is the place where in the future could come resolution of other database types - if (!isPostgresClientOptions(options)) - throw new Error(`Unsupported db type: ${type}`); - - return postgresDbClient(options) as DbClient; -}; diff --git a/src/packages/pongo/src/core/index.ts b/src/packages/pongo/src/core/index.ts index 27d2f657..1098d388 100644 --- a/src/packages/pongo/src/core/index.ts +++ b/src/packages/pongo/src/core/index.ts @@ -1,5 +1,5 @@ export * from './collection'; -export * from './dbClient'; export * from './pongoClient'; +export * from './pongoDb'; export * from './pongoSession'; export * from './typing'; diff --git a/src/packages/pongo/src/core/pongoClient.ts b/src/packages/pongo/src/core/pongoClient.ts index ad001982..3f9ef522 100644 --- a/src/packages/pongo/src/core/pongoClient.ts +++ b/src/packages/pongo/src/core/pongoClient.ts @@ -1,10 +1,6 @@ import pg from 'pg'; import type { PostgresDbClientOptions } from '../postgres'; -import { - getDbClient, - type AllowedDbClientOptions, - type DbClient, -} from './dbClient'; +import { getPongoDb, type AllowedDbClientOptions } from './pongoDb'; import { pongoSession } from './pongoSession'; import type { PongoClient, PongoDb, PongoSession } from './typing/operations'; @@ -44,9 +40,9 @@ export const pongoClient = < connectionString: string, options: PongoClientOptions = {}, ): PongoClient => { - const dbClients: Map> = new Map(); + const dbClients: Map = new Map(); - const dbClient = getDbClient( + const dbClient = getPongoDb( clientToDbOptions({ connectionString, clientOptions: options, @@ -72,7 +68,7 @@ export const pongoClient = < dbClients .set( dbName, - getDbClient( + getPongoDb( clientToDbOptions({ connectionString, dbName, @@ -108,7 +104,7 @@ export const clientToDbOptions = < clientOptions: PongoClientOptions; }): DbClientOptions => { const postgreSQLOptions: PostgresDbClientOptions = { - type: 'PostgreSQL', + dbType: 'PostgreSQL', connectionString: options.connectionString, dbName: options.dbName, ...options.clientOptions, diff --git a/src/packages/pongo/src/core/pongoDb.ts b/src/packages/pongo/src/core/pongoDb.ts new file mode 100644 index 00000000..bc536855 --- /dev/null +++ b/src/packages/pongo/src/core/pongoDb.ts @@ -0,0 +1,27 @@ +import { + isPostgresClientOptions, + postgresDb, + type PostgresDbClientOptions, +} from '../postgres'; +import type { PongoDb } from './typing'; + +export type PongoDbClientOptions = { + connectionString: string; + dbType: DbType; + dbName: string | undefined; +}; + +export type AllowedDbClientOptions = PostgresDbClientOptions; + +export const getPongoDb = < + DbClientOptions extends AllowedDbClientOptions = AllowedDbClientOptions, +>( + options: DbClientOptions, +): PongoDb => { + const { dbType: type } = options; + // This is the place where in the future could come resolution of other database types + if (!isPostgresClientOptions(options)) + throw new Error(`Unsupported db type: ${type}`); + + return postgresDb(options); +}; diff --git a/src/packages/pongo/src/core/pongoSession.ts b/src/packages/pongo/src/core/pongoSession.ts index a76d980a..ad182808 100644 --- a/src/packages/pongo/src/core/pongoSession.ts +++ b/src/packages/pongo/src/core/pongoSession.ts @@ -1,6 +1,6 @@ import { type DatabaseTransaction } from '@event-driven-io/dumbo'; -import type { DbClient } from './dbClient'; import type { + PongoDb, PongoDbTransaction, PongoSession, PongoTransactionOptions, @@ -20,17 +20,19 @@ const pongoTransaction = ( let transaction: DatabaseTransaction | null = null; return { - startDbTransaction: async (db: DbClient) => { + useDatabase: async (db: PongoDb): Promise => { if (transaction && databaseName !== db.databaseName) throw new Error( "There's already other database assigned to transaction", ); - if (transaction && databaseName === db.databaseName) return; + if (transaction && databaseName === db.databaseName) return transaction; databaseName = db.databaseName; transaction = db.pool.transaction(); await transaction.begin(); + + return transaction; }, commit: async () => { if (isCommitted) return; diff --git a/src/packages/pongo/src/core/typing/operations.ts b/src/packages/pongo/src/core/typing/operations.ts index a38b69e5..e0386598 100644 --- a/src/packages/pongo/src/core/typing/operations.ts +++ b/src/packages/pongo/src/core/typing/operations.ts @@ -1,5 +1,8 @@ -import type { SQLExecutor } from '@event-driven-io/dumbo'; -import type { DbClient } from '../dbClient'; +import type { + ConnectionPool, + DatabaseTransaction, + SQLExecutor, +} from '@event-driven-io/dumbo'; export interface PongoClient { connect(): Promise; @@ -23,7 +26,7 @@ export declare interface PongoTransactionOptions { export interface PongoDbTransaction { get databaseName(): string | null; options: PongoTransactionOptions; - startDbTransaction: (database: DbClient) => Promise; + useDatabase: (database: PongoDb) => Promise; commit: () => Promise; rollback: (error?: unknown) => Promise; get sqlExecutor(): SQLExecutor; @@ -51,8 +54,12 @@ export interface PongoSession { ): Promise; } -export interface PongoDb { +export interface PongoDb { + get databaseType(): DbType; get databaseName(): string; + pool: ConnectionPool; + connect(): Promise; + close(): Promise; collection(name: string): PongoCollection; } diff --git a/src/packages/pongo/src/postgres/dbClient.ts b/src/packages/pongo/src/postgres/dbClient.ts index f4bb266f..9bac58f4 100644 --- a/src/packages/pongo/src/postgres/dbClient.ts +++ b/src/packages/pongo/src/postgres/dbClient.ts @@ -5,7 +5,7 @@ import { } from '@event-driven-io/dumbo'; import { pongoCollection, - type DbClient, + type PongoDb, type PongoDbClientOptions, } from '../core'; import { postgresSQLBuilder } from './sqlBuilder'; @@ -15,20 +15,20 @@ export type PostgresDbClientOptions = PongoDbClientOptions<'PostgreSQL'> & export const isPostgresClientOptions = ( options: PongoDbClientOptions, -): options is PostgresDbClientOptions => options.type === 'PostgreSQL'; +): options is PostgresDbClientOptions => options.dbType === 'PostgreSQL'; -export const postgresDbClient = ( +export const postgresDb = ( options: PostgresDbClientOptions, -): DbClient => { +): PongoDb<'PostgreSQL'> => { const { connectionString, dbName } = options; const databaseName = dbName ?? getDatabaseNameOrDefault(connectionString); const pool = postgresPool(options); return { - pool, + databaseType: options.dbType, databaseName, - options, + pool, connect: () => Promise.resolve(), close: () => pool.close(), collection: (collectionName) => From 4062dadd59c99b36599f0794c1b42fa61ff06a39 Mon Sep 17 00:00:00 2001 From: Oskar Dudycz Date: Sat, 3 Aug 2024 13:36:07 +0200 Subject: [PATCH 26/30] Made dumbo types like Connection, Pool, Transaction interfaces That'll make easier extensions --- .../dumbo/src/core/connections/connection.ts | 8 ++--- .../dumbo/src/core/connections/pool.ts | 7 +++-- .../dumbo/src/core/connections/transaction.ts | 30 ++++++++++--------- .../dumbo/src/core/execute/execute.ts | 14 ++++----- 4 files changed, 31 insertions(+), 28 deletions(-) diff --git a/src/packages/dumbo/src/core/connections/connection.ts b/src/packages/dumbo/src/core/connections/connection.ts index 8d28498a..39639de7 100644 --- a/src/packages/dumbo/src/core/connections/connection.ts +++ b/src/packages/dumbo/src/core/connections/connection.ts @@ -1,12 +1,12 @@ import type { WithSQLExecutor } from '../execute'; import type { DatabaseTransactionFactory } from './transaction'; -export type Connection< +export interface Connection< ConnectorType extends string = string, DbClient = unknown, -> = { +> extends WithSQLExecutor, + DatabaseTransactionFactory { type: ConnectorType; connect: () => Promise; close: () => Promise; -} & WithSQLExecutor & - DatabaseTransactionFactory; +} diff --git a/src/packages/dumbo/src/core/connections/pool.ts b/src/packages/dumbo/src/core/connections/pool.ts index 8edbfde4..2eca47fc 100644 --- a/src/packages/dumbo/src/core/connections/pool.ts +++ b/src/packages/dumbo/src/core/connections/pool.ts @@ -2,12 +2,13 @@ import type { WithSQLExecutor } from '../execute'; import { type Connection } from './connection'; import type { DatabaseTransactionFactory } from './transaction'; -export type ConnectionPool = { +export interface ConnectionPool + extends WithSQLExecutor, + DatabaseTransactionFactory { type: ConnectionType['type']; open: () => Promise; close: () => Promise; -} & WithSQLExecutor & - DatabaseTransactionFactory; +} export type ConnectionPoolProvider< ConnectionPoolType extends ConnectionPool = ConnectionPool, diff --git a/src/packages/dumbo/src/core/connections/transaction.ts b/src/packages/dumbo/src/core/connections/transaction.ts index e55b1027..b2f9d03a 100644 --- a/src/packages/dumbo/src/core/connections/transaction.ts +++ b/src/packages/dumbo/src/core/connections/transaction.ts @@ -1,23 +1,25 @@ import type { WithSQLExecutor } from '../execute'; import { type Connection } from './connection'; -export type DatabaseTransaction = { +export interface DatabaseTransaction + extends WithSQLExecutor { type: ConnectorType; begin: () => Promise; commit: () => Promise; rollback: (error?: unknown) => Promise; -} & WithSQLExecutor; +} -export type DatabaseTransactionFactory = - { - transaction: () => DatabaseTransaction; +export interface DatabaseTransactionFactory< + ConnectorType extends string = string, +> { + transaction: () => DatabaseTransaction; - inTransaction: ( - handle: ( - transaction: DatabaseTransaction, - ) => Promise<{ success: boolean; result: Result }>, - ) => Promise; - }; + withTransaction: ( + handle: ( + transaction: DatabaseTransaction, + ) => Promise<{ success: boolean; result: Result }>, + ) => Promise; +} export const executeInTransaction = async < ConnectorType extends string = string, @@ -53,7 +55,7 @@ export const transactionFactoryWithDbClient = < ) => DatabaseTransaction, ): DatabaseTransactionFactory => ({ transaction: () => initTransaction(connect()), - inTransaction: (handle) => + withTransaction: (handle) => executeInTransaction(initTransaction(connect()), handle), }); @@ -63,10 +65,10 @@ export const transactionFactoryWithNewConnection = < connect: () => ConnectionType, ): DatabaseTransactionFactory => ({ transaction: () => connect().transaction(), - inTransaction: async (handle) => { + withTransaction: async (handle) => { const connection = connect(); try { - return await connection.inTransaction(handle); + return await connection.withTransaction(handle); } finally { await connection.close(); } diff --git a/src/packages/dumbo/src/core/execute/execute.ts b/src/packages/dumbo/src/core/execute/execute.ts index 2aa260c4..79c0941d 100644 --- a/src/packages/dumbo/src/core/execute/execute.ts +++ b/src/packages/dumbo/src/core/execute/execute.ts @@ -2,10 +2,10 @@ import type { Connection } from '../connections'; import type { QueryResult, QueryResultRow } from '../query'; import { type SQL } from '../sql'; -export type DbSQLExecutor< +export interface DbSQLExecutor< ConnectorType extends string = string, DbClient = unknown, -> = { +> { type: ConnectorType; query( client: DbClient, @@ -23,9 +23,9 @@ export type DbSQLExecutor< client: DbClient, sqls: SQL[], ): Promise[]>; -}; +} -export type SQLExecutor = { +export interface SQLExecutor { query( sql: SQL, ): Promise>; @@ -38,11 +38,11 @@ export type SQLExecutor = { batchCommand( sqls: SQL[], ): Promise[]>; -}; +} -export type WithSQLExecutor = { +export interface WithSQLExecutor { execute: SQLExecutor; -}; +} export const sqlExecutor = < DbClient = unknown, From d5fd6f5ce3dee18dcb7c78930e741e40ec9eb6de Mon Sep 17 00:00:00 2001 From: Oskar Dudycz Date: Sat, 3 Aug 2024 13:49:01 +0200 Subject: [PATCH 27/30] Added and used consistently Pongo connector type across Dumbo and Pongo --- src/packages/dumbo/src/index.ts | 12 +++++++++--- src/packages/dumbo/src/postgres/index.ts | 7 ++++++- src/packages/pongo/src/core/pongoClient.ts | 3 ++- src/packages/pongo/src/core/pongoDb.ts | 6 +++--- src/packages/pongo/src/core/typing/operations.ts | 6 ++++-- src/packages/pongo/src/postgres/dbClient.ts | 13 +++++++++---- 6 files changed, 33 insertions(+), 14 deletions(-) diff --git a/src/packages/dumbo/src/index.ts b/src/packages/dumbo/src/index.ts index 3e67549b..40debc8f 100644 --- a/src/packages/dumbo/src/index.ts +++ b/src/packages/dumbo/src/index.ts @@ -1,8 +1,14 @@ -import { postgresPool, type NodePostgresPoolOptions } from './postgres'; +import { + postgresPool, + type PostgresConnector, + type PostgresPoolOptions, +} from './postgres'; export * from './core'; export * from './postgres'; -export type PoolOptions = NodePostgresPoolOptions; -export const connectionPool = (_type: 'PostgreSQL', options: PoolOptions) => +export type ConnectorType = PostgresConnector; +export type PoolOptions = PostgresPoolOptions; + +export const connectionPool = (_type: ConnectorType, options: PoolOptions) => postgresPool(options); diff --git a/src/packages/dumbo/src/postgres/index.ts b/src/packages/dumbo/src/postgres/index.ts index 3b4d2110..727916be 100644 --- a/src/packages/dumbo/src/postgres/index.ts +++ b/src/packages/dumbo/src/postgres/index.ts @@ -1,6 +1,11 @@ export * from './core'; export * from './pg'; -import { type NodePostgresPoolOptions, nodePostgresPool } from './pg'; +import { + type NodePostgresConnector, + type NodePostgresPoolOptions, + nodePostgresPool, +} from './pg'; +export type PostgresConnector = NodePostgresConnector; export type PostgresPoolOptions = NodePostgresPoolOptions; export const postgresPool = nodePostgresPool; diff --git a/src/packages/pongo/src/core/pongoClient.ts b/src/packages/pongo/src/core/pongoClient.ts index 3f9ef522..82728743 100644 --- a/src/packages/pongo/src/core/pongoClient.ts +++ b/src/packages/pongo/src/core/pongoClient.ts @@ -1,3 +1,4 @@ +import { NodePostgresConnectorType } from '@event-driven-io/dumbo'; import pg from 'pg'; import type { PostgresDbClientOptions } from '../postgres'; import { getPongoDb, type AllowedDbClientOptions } from './pongoDb'; @@ -104,7 +105,7 @@ export const clientToDbOptions = < clientOptions: PongoClientOptions; }): DbClientOptions => { const postgreSQLOptions: PostgresDbClientOptions = { - dbType: 'PostgreSQL', + connectorType: NodePostgresConnectorType, connectionString: options.connectionString, dbName: options.dbName, ...options.clientOptions, diff --git a/src/packages/pongo/src/core/pongoDb.ts b/src/packages/pongo/src/core/pongoDb.ts index bc536855..6978cdd0 100644 --- a/src/packages/pongo/src/core/pongoDb.ts +++ b/src/packages/pongo/src/core/pongoDb.ts @@ -5,9 +5,9 @@ import { } from '../postgres'; import type { PongoDb } from './typing'; -export type PongoDbClientOptions = { +export type PongoDbClientOptions = { + connectorType: ConnectorType; connectionString: string; - dbType: DbType; dbName: string | undefined; }; @@ -18,7 +18,7 @@ export const getPongoDb = < >( options: DbClientOptions, ): PongoDb => { - const { dbType: type } = options; + const { connectorType: type } = options; // This is the place where in the future could come resolution of other database types if (!isPostgresClientOptions(options)) throw new Error(`Unsupported db type: ${type}`); diff --git a/src/packages/pongo/src/core/typing/operations.ts b/src/packages/pongo/src/core/typing/operations.ts index e0386598..2f69397c 100644 --- a/src/packages/pongo/src/core/typing/operations.ts +++ b/src/packages/pongo/src/core/typing/operations.ts @@ -1,6 +1,7 @@ import type { ConnectionPool, DatabaseTransaction, + DatabaseTransactionFactory, SQLExecutor, } from '@event-driven-io/dumbo'; @@ -54,8 +55,9 @@ export interface PongoSession { ): Promise; } -export interface PongoDb { - get databaseType(): DbType; +export interface PongoDb + extends DatabaseTransactionFactory { + get connectorType(): ConnectorType; get databaseName(): string; pool: ConnectionPool; connect(): Promise; diff --git a/src/packages/pongo/src/postgres/dbClient.ts b/src/packages/pongo/src/postgres/dbClient.ts index 9bac58f4..93eb01cf 100644 --- a/src/packages/pongo/src/postgres/dbClient.ts +++ b/src/packages/pongo/src/postgres/dbClient.ts @@ -1,6 +1,8 @@ import { getDatabaseNameOrDefault, + NodePostgresConnectorType, postgresPool, + type PostgresConnector, type PostgresPoolOptions, } from '@event-driven-io/dumbo'; import { @@ -10,23 +12,24 @@ import { } from '../core'; import { postgresSQLBuilder } from './sqlBuilder'; -export type PostgresDbClientOptions = PongoDbClientOptions<'PostgreSQL'> & +export type PostgresDbClientOptions = PongoDbClientOptions & PostgresPoolOptions; export const isPostgresClientOptions = ( options: PongoDbClientOptions, -): options is PostgresDbClientOptions => options.dbType === 'PostgreSQL'; +): options is PostgresDbClientOptions => + options.connectorType === NodePostgresConnectorType; export const postgresDb = ( options: PostgresDbClientOptions, -): PongoDb<'PostgreSQL'> => { +): PongoDb => { const { connectionString, dbName } = options; const databaseName = dbName ?? getDatabaseNameOrDefault(connectionString); const pool = postgresPool(options); return { - databaseType: options.dbType, + connectorType: options.connectorType, databaseName, pool, connect: () => Promise.resolve(), @@ -38,5 +41,7 @@ export const postgresDb = ( sqlExecutor: pool.execute, sqlBuilder: postgresSQLBuilder(collectionName), }), + transaction: () => pool.transaction(), + withTransaction: (handle) => pool.withTransaction(handle), }; }; From ac438e6de65444670da0e42064039ed009486956 Mon Sep 17 00:00:00 2001 From: Oskar Dudycz Date: Sat, 3 Aug 2024 14:27:52 +0200 Subject: [PATCH 28/30] Used transaction from options in Pongo Operations --- .../src/core/collection/pongoCollection.ts | 58 +++++++--- src/packages/pongo/src/core/index.ts | 1 + src/packages/pongo/src/core/pongoSession.ts | 100 +++++------------- .../pongo/src/core/pongoTransaction.ts | 67 ++++++++++++ .../pongo/src/core/typing/operations.ts | 4 +- src/packages/pongo/src/postgres/dbClient.ts | 7 +- 6 files changed, 142 insertions(+), 95 deletions(-) create mode 100644 src/packages/pongo/src/core/pongoTransaction.ts diff --git a/src/packages/pongo/src/core/collection/pongoCollection.ts b/src/packages/pongo/src/core/collection/pongoCollection.ts index ec5c628a..c7f7667f 100644 --- a/src/packages/pongo/src/core/collection/pongoCollection.ts +++ b/src/packages/pongo/src/core/collection/pongoCollection.ts @@ -1,5 +1,6 @@ import { single, + type DatabaseTransaction, type QueryResultRow, type SQL, type SQLExecutor, @@ -9,6 +10,7 @@ import { type CollectionOperationOptions, type DocumentHandler, type PongoCollection, + type PongoDb, type PongoDeleteResult, type PongoDocument, type PongoFilter, @@ -20,39 +22,63 @@ import { type WithoutId, } from '..'; -export type PongoCollectionOptions = { +export type PongoCollectionOptions = { + db: PongoDb; collectionName: string; - dbName: string; sqlExecutor: SQLExecutor; sqlBuilder: PongoCollectionSQLBuilder; }; -export const pongoCollection = ({ +const enlistIntoTransactionIfActive = async < + ConnectorType extends string = string, +>( + db: PongoDb, + options: CollectionOperationOptions | undefined, +): Promise => { + const transaction = options?.session?.transaction; + + if (!transaction || !transaction.isActive) return null; + + return await transaction.enlistDatabase(db); +}; + +const transactionExecutorOrDefault = async < + ConnectorType extends string = string, +>( + db: PongoDb, + options: CollectionOperationOptions | undefined, + defaultSqlExecutor: SQLExecutor, +): Promise => { + const existingTransaction = await enlistIntoTransactionIfActive(db, options); + return existingTransaction?.execute ?? defaultSqlExecutor; +}; + +export const pongoCollection = < + T extends PongoDocument, + ConnectorType extends string = string, +>({ + db, collectionName, - dbName, sqlExecutor, sqlBuilder: SqlFor, -}: PongoCollectionOptions): PongoCollection => { - const command = (sql: SQL, options?: CollectionOperationOptions) => { - const execute = options?.session?.transaction?.sqlExecutor ?? sqlExecutor; +}: PongoCollectionOptions): PongoCollection => { + const command = async (sql: SQL, options?: CollectionOperationOptions) => + (await transactionExecutorOrDefault(db, options, sqlExecutor)).command(sql); - return execute.command(sql); - }; - const query = ( + const query = async ( sql: SQL, options?: CollectionOperationOptions, - ) => { - const execute = options?.session?.transaction?.sqlExecutor ?? sqlExecutor; - - return execute.query(sql); - }; + ) => + (await transactionExecutorOrDefault(db, options, sqlExecutor)).query( + sql, + ); const createCollectionPromise = command(SqlFor.createCollection()); const createCollection = (options?: CollectionOperationOptions) => options?.session ? createCollectionPromise : createCollectionPromise; const collection = { - dbName, + dbName: db.databaseName, collectionName, createCollection: async (options?: CollectionOperationOptions) => { await createCollection(options); diff --git a/src/packages/pongo/src/core/index.ts b/src/packages/pongo/src/core/index.ts index 1098d388..33149fc6 100644 --- a/src/packages/pongo/src/core/index.ts +++ b/src/packages/pongo/src/core/index.ts @@ -2,4 +2,5 @@ export * from './collection'; export * from './pongoClient'; export * from './pongoDb'; export * from './pongoSession'; +export * from './pongoTransaction'; export * from './typing'; diff --git a/src/packages/pongo/src/core/pongoSession.ts b/src/packages/pongo/src/core/pongoSession.ts index ad182808..77b36718 100644 --- a/src/packages/pongo/src/core/pongoSession.ts +++ b/src/packages/pongo/src/core/pongoSession.ts @@ -1,6 +1,5 @@ -import { type DatabaseTransaction } from '@event-driven-io/dumbo'; +import { pongoTransaction } from './pongoTransaction'; import type { - PongoDb, PongoDbTransaction, PongoSession, PongoTransactionOptions, @@ -11,66 +10,22 @@ export type PongoSessionOptions = { defaultTransactionOptions: PongoTransactionOptions; }; -const pongoTransaction = ( - options: PongoTransactionOptions, -): PongoDbTransaction => { - let isCommitted = false; - let isRolledBack = false; - let databaseName: string | null = null; - let transaction: DatabaseTransaction | null = null; +const isActive = ( + transaction: PongoDbTransaction | null, +): transaction is PongoDbTransaction => transaction?.isActive === true; - return { - useDatabase: async (db: PongoDb): Promise => { - if (transaction && databaseName !== db.databaseName) - throw new Error( - "There's already other database assigned to transaction", - ); +function assertInActiveTransaction( + transaction: PongoDbTransaction | null, +): asserts transaction is PongoDbTransaction { + if (!isActive(transaction)) throw new Error('No active transaction exists!'); +} - if (transaction && databaseName === db.databaseName) return transaction; - - databaseName = db.databaseName; - transaction = db.pool.transaction(); - await transaction.begin(); - - return transaction; - }, - commit: async () => { - if (isCommitted) return; - if (!isRolledBack) throw new Error('Transaction is not active!'); - if (!transaction) throw new Error('No database transaction started!'); - - isCommitted = true; - - await transaction.commit(); - - transaction = null; - }, - rollback: async (error?: unknown) => { - if (isCommitted) throw new Error('Cannot rollback commited transaction!'); - if (!isRolledBack) return; - if (!transaction) throw new Error('No database transaction started!'); - - isRolledBack = true; - - await transaction.rollback(error); - - transaction = null; - }, - databaseName, - isStarting: false, - isCommitted, - get isActive() { - return !isCommitted && !isRolledBack; - }, - get sqlExecutor() { - if (transaction === null) - throw new Error('No database transaction was started'); - - return transaction.execute; - }, - options, - }; -}; +function assertNotInActiveTransaction( + transaction: PongoDbTransaction | null, +): asserts transaction is null { + if (isActive(transaction)) + throw new Error('Active transaction already exists!'); +} export const pongoSession = (options?: PongoSessionOptions): PongoSession => { const explicit = options?.explicit === true; @@ -85,24 +40,28 @@ export const pongoSession = (options?: PongoSessionOptions): PongoSession => { let hasEnded = false; const startTransaction = (options?: PongoTransactionOptions) => { - if (transaction?.isActive === true) - throw new Error('Active transaction already exists!'); + assertNotInActiveTransaction(transaction); transaction = pongoTransaction(options ?? defaultTransactionOptions); }; const commitTransaction = async () => { - if (transaction?.isActive !== true) - throw new Error('No active transaction!'); + assertInActiveTransaction(transaction); await transaction.commit(); }; const abortTransaction = async () => { - if (transaction?.isActive !== true) - throw new Error('No active transaction!'); + assertInActiveTransaction(transaction); await transaction.rollback(); }; + const endSession = async (): Promise => { + if (hasEnded) return; + hasEnded = true; + + if (isActive(transaction)) await transaction.rollback(); + }; + const session = { get hasEnded() { return hasEnded; @@ -119,14 +78,9 @@ export const pongoSession = (options?: PongoSessionOptions): PongoSession => { get snapshotEnabled() { return defaultTransactionOptions.snapshotEnabled; }, - endSession: (): Promise => { - if (hasEnded) return Promise.resolve(); - hasEnded = true; - - return Promise.resolve(); - }, + endSession, incrementTransactionNumber: () => {}, - inTransaction: () => transaction !== null, + inTransaction: () => isActive(transaction), startTransaction, commitTransaction, abortTransaction, diff --git a/src/packages/pongo/src/core/pongoTransaction.ts b/src/packages/pongo/src/core/pongoTransaction.ts new file mode 100644 index 00000000..95b74319 --- /dev/null +++ b/src/packages/pongo/src/core/pongoTransaction.ts @@ -0,0 +1,67 @@ +import type { DatabaseTransaction } from '@event-driven-io/dumbo'; +import type { + PongoDb, + PongoDbTransaction, + PongoTransactionOptions, +} from './typing'; + +export const pongoTransaction = ( + options: PongoTransactionOptions, +): PongoDbTransaction => { + let isCommitted = false; + let isRolledBack = false; + let databaseName: string | null = null; + let transaction: DatabaseTransaction | null = null; + + return { + enlistDatabase: async (db: PongoDb): Promise => { + if (transaction && databaseName !== db.databaseName) + throw new Error( + "There's already other database assigned to transaction", + ); + + if (transaction && databaseName === db.databaseName) return transaction; + + databaseName = db.databaseName; + transaction = db.transaction(); + await transaction.begin(); + + return transaction; + }, + commit: async () => { + if (isCommitted) return; + if (!isRolledBack) throw new Error('Transaction is not active!'); + if (!transaction) throw new Error('No database transaction started!'); + + isCommitted = true; + + await transaction.commit(); + + transaction = null; + }, + rollback: async (error?: unknown) => { + if (isCommitted) throw new Error('Cannot rollback commited transaction!'); + if (!isRolledBack) return; + if (!transaction) throw new Error('No database transaction started!'); + + isRolledBack = true; + + await transaction.rollback(error); + + transaction = null; + }, + databaseName, + isStarting: false, + isCommitted, + get isActive() { + return !isCommitted && !isRolledBack; + }, + get sqlExecutor() { + if (transaction === null) + throw new Error('No database transaction was started'); + + return transaction.execute; + }, + options, + }; +}; diff --git a/src/packages/pongo/src/core/typing/operations.ts b/src/packages/pongo/src/core/typing/operations.ts index 2f69397c..947c07a2 100644 --- a/src/packages/pongo/src/core/typing/operations.ts +++ b/src/packages/pongo/src/core/typing/operations.ts @@ -1,5 +1,4 @@ import type { - ConnectionPool, DatabaseTransaction, DatabaseTransactionFactory, SQLExecutor, @@ -27,7 +26,7 @@ export declare interface PongoTransactionOptions { export interface PongoDbTransaction { get databaseName(): string | null; options: PongoTransactionOptions; - useDatabase: (database: PongoDb) => Promise; + enlistDatabase: (database: PongoDb) => Promise; commit: () => Promise; rollback: (error?: unknown) => Promise; get sqlExecutor(): SQLExecutor; @@ -59,7 +58,6 @@ export interface PongoDb extends DatabaseTransactionFactory { get connectorType(): ConnectorType; get databaseName(): string; - pool: ConnectionPool; connect(): Promise; close(): Promise; collection(name: string): PongoCollection; diff --git a/src/packages/pongo/src/postgres/dbClient.ts b/src/packages/pongo/src/postgres/dbClient.ts index 93eb01cf..895ba78d 100644 --- a/src/packages/pongo/src/postgres/dbClient.ts +++ b/src/packages/pongo/src/postgres/dbClient.ts @@ -28,20 +28,21 @@ export const postgresDb = ( const pool = postgresPool(options); - return { + const db: PongoDb = { connectorType: options.connectorType, databaseName, - pool, connect: () => Promise.resolve(), close: () => pool.close(), collection: (collectionName) => pongoCollection({ collectionName, - dbName: databaseName, + db, sqlExecutor: pool.execute, sqlBuilder: postgresSQLBuilder(collectionName), }), transaction: () => pool.transaction(), withTransaction: (handle) => pool.withTransaction(handle), }; + + return db; }; From e24b870ecdafab21beabafb6d4c7eb03d3c917b6 Mon Sep 17 00:00:00 2001 From: Oskar Dudycz Date: Sat, 3 Aug 2024 14:51:03 +0200 Subject: [PATCH 29/30] Added Postgres Pongo e2e tests --- .../pongo/src/e2e/postgres.e2e.spec.ts | 1004 +++++++++++++++++ 1 file changed, 1004 insertions(+) create mode 100644 src/packages/pongo/src/e2e/postgres.e2e.spec.ts diff --git a/src/packages/pongo/src/e2e/postgres.e2e.spec.ts b/src/packages/pongo/src/e2e/postgres.e2e.spec.ts new file mode 100644 index 00000000..5ab11d93 --- /dev/null +++ b/src/packages/pongo/src/e2e/postgres.e2e.spec.ts @@ -0,0 +1,1004 @@ +import { + PostgreSqlContainer, + type StartedPostgreSqlContainer, +} from '@testcontainers/postgresql'; +import assert from 'assert'; +import console from 'console'; +import { after, before, describe, it } from 'node:test'; +import { v4 as uuid } from 'uuid'; +import { + MongoClient, + pongoClient, + type Db, + type ObjectId, + type PongoClient, + type PongoDb, +} from '..'; + +type History = { street: string }; +type Address = { + city: string; + street?: string; + zip?: string; + history?: History[]; +}; + +type User = { + _id?: string; + name: string; + age: number; + address?: Address; + tags?: string[]; +}; + +void describe('MongoDB Compatibility Tests', () => { + let postgres: StartedPostgreSqlContainer; + let postgresConnectionString: string; + let client: PongoClient; + let shim: MongoClient; + + let pongoDb: PongoDb; + let mongoDb: Db; + + before(async () => { + postgres = await new PostgreSqlContainer().start(); + postgresConnectionString = postgres.getConnectionUri(); + client = pongoClient(postgresConnectionString); + shim = new MongoClient(postgresConnectionString); + await client.connect(); + await shim.connect(); + + const dbName = postgres.getDatabase(); + + pongoDb = client.db(dbName); + mongoDb = shim.db(dbName); + }); + + after(async () => { + try { + await client.close(); + await shim.close(); + await postgres.stop(); + } catch (error) { + console.log(error); + } + }); + + void describe('Insert Operations', () => { + void it('should insert a document into both PostgreSQL and MongoDB', async () => { + const pongoCollection = pongoDb.collection('insertOne'); + const mongoCollection = mongoDb.collection('shiminsertOne'); + const doc = { name: 'Anita', age: 25 }; + const pongoInsertResult = await pongoCollection.insertOne(doc); + const mongoInsertResult = await mongoCollection.insertOne(doc); + assert(pongoInsertResult.insertedId); + assert(mongoInsertResult.insertedId); + const pongoDoc = await pongoCollection.findOne({ + _id: pongoInsertResult.insertedId, + }); + const mongoDoc = await mongoCollection.findOne({ + _id: mongoInsertResult.insertedId, + }); + assert.deepStrictEqual( + { + name: pongoDoc!.name, + age: pongoDoc!.age, + }, + { + name: mongoDoc!.name, + age: mongoDoc!.age, + }, + ); + }); + + void it('should insert many documents into both PostgreSQL and MongoDB', async () => { + const pongoCollection = pongoDb.collection('insertMany'); + const mongoCollection = mongoDb.collection('shiminsertMany'); + const docs = [ + { name: 'David', age: 40 }, + { name: 'Eve', age: 45 }, + { name: 'Frank', age: 50 }, + ]; + const pongoInsertResult = await pongoCollection.insertMany(docs); + const mongoInsertResult = await mongoCollection.insertMany(docs); + const pongoIds = Object.values(pongoInsertResult.insertedIds); + const mongoIds = Object.values(mongoInsertResult.insertedIds); + assert.equal(pongoInsertResult.insertedCount, docs.length); + assert.equal(mongoInsertResult.insertedCount, docs.length); + const pongoDocs = await pongoCollection.find({ + _id: { $in: pongoIds }, + }); + const mongoDocs = await mongoCollection + .find({ + _id: { $in: mongoIds }, + }) + .toArray(); + assert.deepStrictEqual( + pongoDocs.map((doc) => ({ + name: doc.name, + age: doc.age, + })), + mongoDocs.map((doc) => ({ + name: doc.name, + age: doc.age, + })), + ); + }); + }); + + void describe('Update Operations', () => { + void it('should update a document in both PostgreSQL and MongoDB', async () => { + const pongoCollection = pongoDb.collection('updateOne'); + const mongoCollection = mongoDb.collection('shimupdateOne'); + const doc = { name: 'Roger', age: 30 }; + + const pongoInsertResult = await pongoCollection.insertOne(doc); + const mongoInsertResult = await mongoCollection.insertOne(doc); + + const update = { $set: { age: 31 } }; + + await pongoCollection.updateOne( + { _id: pongoInsertResult.insertedId! }, + update, + ); + await mongoCollection.updateOne( + { _id: mongoInsertResult.insertedId }, + update, + ); + + const pongoDoc = await pongoCollection.findOne({ + _id: pongoInsertResult.insertedId!, + }); + const mongoDoc = await mongoCollection.findOne({ + _id: mongoInsertResult.insertedId, + }); + + assert.equal(mongoDoc?.age, 31); + assert.deepStrictEqual( + { + name: pongoDoc!.name, + age: pongoDoc!.age, + }, + { + name: mongoDoc!.name, + age: mongoDoc!.age, + }, + ); + }); + + void it('should update a multiple properties in document in both PostgreSQL and MongoDB', async () => { + const pongoCollection = pongoDb.collection('updateOneMultiple'); + const mongoCollection = mongoDb.collection('shimupdateOneMultiple'); + const doc = { name: 'Roger', age: 30 }; + + const pongoInsertResult = await pongoCollection.insertOne(doc); + const mongoInsertResult = await mongoCollection.insertOne(doc); + + const update = { $set: { age: 31, tags: [] } }; + + await pongoCollection.updateOne( + { _id: pongoInsertResult.insertedId! }, + update, + ); + await mongoCollection.updateOne( + { _id: mongoInsertResult.insertedId }, + update, + ); + + const pongoDoc = await pongoCollection.findOne({ + _id: pongoInsertResult.insertedId!, + }); + const mongoDoc = await mongoCollection.findOne({ + _id: mongoInsertResult.insertedId, + }); + + assert.equal(mongoDoc?.age, 31); + assert.deepEqual(mongoDoc?.tags, []); + assert.deepStrictEqual( + { + name: pongoDoc!.name, + age: pongoDoc!.age, + tags: pongoDoc!.tags, + }, + { + name: mongoDoc!.name, + age: mongoDoc!.age, + tags: mongoDoc!.tags, + }, + ); + }); + + void it('should update documents in both PostgreSQL and MongoDB', async () => { + const pongoCollection = pongoDb.collection('updateMany'); + const mongoCollection = mongoDb.collection('shimupdateMany'); + + const docs = [ + { name: 'David', age: 40 }, + { name: 'Eve', age: 45 }, + { name: 'Frank', age: 50 }, + ]; + + const pongoInsertResult = await pongoCollection.insertMany(docs); + const mongoInsertResult = await mongoCollection.insertMany(docs); + + const pongoIds = Object.values(pongoInsertResult.insertedIds); + const mongoIds = Object.values(mongoInsertResult.insertedIds); + + const update = { $set: { age: 31 } }; + + const pongoUpdateResult = await pongoCollection.updateMany( + { _id: { $in: pongoIds } }, + update, + ); + const mongoUpdateResult = await mongoCollection.updateMany( + { _id: { $in: mongoIds } }, + update, + ); + + assert.equal(3, pongoUpdateResult.modifiedCount); + assert.equal(3, mongoUpdateResult.modifiedCount); + + const pongoDocs = await pongoCollection.find({ + _id: { $in: pongoIds }, + }); + const mongoDocs = await mongoCollection + .find({ + _id: { $in: mongoIds }, + }) + .toArray(); + + assert.deepStrictEqual( + pongoDocs.map((doc) => ({ + name: doc.name, + age: doc.age, + })), + mongoDocs.map((doc) => ({ + name: doc.name, + age: doc.age, + })), + ); + }); + + void it('should update a document in both PostgreSQL and MongoDB using $unset', async () => { + const pongoCollection = pongoDb.collection('testCollection'); + const mongoCollection = mongoDb.collection('shimtestCollection'); + const doc = { name: 'Roger', age: 30, address: { city: 'Wonderland' } }; + + const pongoInsertResult = await pongoCollection.insertOne(doc); + const mongoInsertResult = await mongoCollection.insertOne(doc); + + const { modifiedCount } = await pongoCollection.updateOne( + { _id: pongoInsertResult.insertedId! }, + { $unset: { address: '' } }, + ); + assert.equal(modifiedCount, 1); + await mongoCollection.updateOne( + { _id: mongoInsertResult.insertedId }, + { $unset: { address: '' } }, + ); + + const pongoDoc = await pongoCollection.findOne({ + _id: pongoInsertResult.insertedId!, + }); + const mongoDoc = await mongoCollection.findOne({ + _id: mongoInsertResult.insertedId, + }); + + assert.deepStrictEqual( + { + name: pongoDoc!.name, + age: pongoDoc!.age, + address: undefined, + }, + { + name: mongoDoc!.name, + age: mongoDoc!.age, + address: undefined, + }, + ); + }); + + void it('should update a document in both PostgreSQL and MongoDB using $inc', async () => { + const pongoCollection = pongoDb.collection('testCollection'); + const mongoCollection = mongoDb.collection('shimtestCollection'); + const doc = { name: 'Roger', age: 30 }; + + const pongoInsertResult = await pongoCollection.insertOne(doc); + const mongoInsertResult = await mongoCollection.insertOne(doc); + + const update = { $inc: { age: 1 } }; + + const { modifiedCount } = await pongoCollection.updateOne( + { _id: pongoInsertResult.insertedId! }, + update, + ); + assert.equal(modifiedCount, 1); + await mongoCollection.updateOne( + { _id: mongoInsertResult.insertedId }, + update, + ); + + const pongoDoc = await pongoCollection.findOne({ + _id: pongoInsertResult.insertedId!, + }); + const mongoDoc = await mongoCollection.findOne({ + _id: mongoInsertResult.insertedId, + }); + + assert.deepStrictEqual( + { + name: pongoDoc!.name, + age: 31, + }, + { + name: mongoDoc!.name, + age: 31, + }, + ); + }); + + void it('should update a document in both PostgreSQL and MongoDB using $push', async () => { + const pongoCollection = pongoDb.collection('testCollection'); + const mongoCollection = mongoDb.collection('shimtestCollection'); + const doc = { name: 'Roger', age: 30 }; + + const pongoInsertResult = await pongoCollection.insertOne(doc); + const mongoInsertResult = await mongoCollection.insertOne(doc); + let pongoDoc = await pongoCollection.findOne({ + _id: pongoInsertResult.insertedId!, + }); + // Push to non existing + let updateResult = await pongoCollection.updateOne( + { _id: pongoInsertResult.insertedId! }, + //TODO: fix $push type definition to allow non-array + { $push: { tags: 'tag1' as unknown as string[] } }, + ); + assert.equal(updateResult.modifiedCount, 1); + await mongoCollection.updateOne( + { _id: mongoInsertResult.insertedId }, + { $push: { tags: 'tag1' } }, + ); + pongoDoc = await pongoCollection.findOne({ + _id: pongoInsertResult.insertedId!, + }); + + // Push to existing + updateResult = await pongoCollection.updateOne( + { _id: pongoInsertResult.insertedId! }, + { $push: { tags: 'tag2' as unknown as string[] } }, + ); + assert.equal(updateResult.modifiedCount, 1); + await mongoCollection.updateOne( + { _id: mongoInsertResult.insertedId }, + { $push: { tags: 'tag2' } }, + ); + + pongoDoc = await pongoCollection.findOne({ + _id: pongoInsertResult.insertedId!, + }); + const mongoDoc = await mongoCollection.findOne({ + _id: mongoInsertResult.insertedId, + }); + + assert.deepStrictEqual( + { + name: pongoDoc!.name, + age: pongoDoc!.age, + tags: ['tag1', 'tag2'], + }, + { + name: mongoDoc!.name, + age: mongoDoc!.age, + tags: ['tag1', 'tag2'], + }, + ); + }); + }); + + void describe('Replace Operations', () => { + void it('should replace a document in both PostgreSQL and MongoDB', async () => { + const pongoCollection = pongoDb.collection('updateOne'); + const mongoCollection = mongoDb.collection('shimupdateOne'); + const doc = { name: 'Roger', age: 30 }; + + const pongoInsertResult = await pongoCollection.insertOne(doc); + const mongoInsertResult = await mongoCollection.insertOne(doc); + + const replacement = { name: 'Not Roger', age: 100, tags: ['tag2'] }; + + await pongoCollection.replaceOne( + { _id: pongoInsertResult.insertedId! }, + replacement, + ); + await mongoCollection.replaceOne( + { _id: mongoInsertResult.insertedId }, + replacement, + ); + + const pongoDoc = await pongoCollection.findOne({ + _id: pongoInsertResult.insertedId!, + }); + const mongoDoc = await mongoCollection.findOne({ + _id: mongoInsertResult.insertedId, + }); + + assert.strictEqual(mongoDoc?.name, replacement.name); + assert.deepEqual(mongoDoc?.age, replacement.age); + assert.deepEqual(mongoDoc?.tags, replacement.tags); + assert.deepStrictEqual( + { + name: pongoDoc!.name, + age: pongoDoc!.age, + tags: pongoDoc!.tags, + }, + { + name: mongoDoc.name, + age: mongoDoc.age, + tags: mongoDoc.tags, + }, + ); + }); + }); + + void describe('Delete Operations', () => { + void it('should delete a document from both PostgreSQL and MongoDB', async () => { + const pongoCollection = pongoDb.collection('testCollection'); + const mongoCollection = mongoDb.collection('shimtestCollection'); + const doc = { name: 'Cruella', age: 35 }; + + const pongoInsertResult = await pongoCollection.insertOne(doc); + const mongoInsertResult = await mongoCollection.insertOne(doc); + + const { deletedCount } = await pongoCollection.deleteOne({ + _id: pongoInsertResult.insertedId!, + }); + assert.equal(deletedCount, 1); + await mongoCollection.deleteOne({ _id: mongoInsertResult.insertedId }); + + const pongoDoc = await pongoCollection.findOne({ + _id: pongoInsertResult.insertedId!, + }); + const mongoDoc = await mongoCollection.findOne({ + _id: mongoInsertResult.insertedId, + }); + + assert.strictEqual(pongoDoc, null); + assert.strictEqual(mongoDoc, null); + }); + + void it('should delete documents in both PostgreSQL and MongoDB', async () => { + const pongoCollection = pongoDb.collection('updateMany'); + const mongoCollection = mongoDb.collection('shimupdateMany'); + + const docs = [ + { name: 'David', age: 40 }, + { name: 'Eve', age: 45 }, + { name: 'Frank', age: 50 }, + ]; + + const pongoInsertResult = await pongoCollection.insertMany(docs); + const mongoInsertResult = await mongoCollection.insertMany(docs); + + const pongoIds = Object.values(pongoInsertResult.insertedIds); + const mongoIds = Object.values(mongoInsertResult.insertedIds); + + const pongoDeleteResult = await pongoCollection.deleteMany({ + _id: { $in: pongoIds }, + }); + const mongoUpdateResult = await mongoCollection.deleteMany({ + _id: { $in: mongoIds }, + }); + + assert.equal(3, pongoDeleteResult.deletedCount); + assert.equal(3, mongoUpdateResult.deletedCount); + + const pongoDocs = await pongoCollection.find({ + _id: { $in: pongoIds }, + }); + const mongoDocs = await mongoCollection + .find({ + _id: { $in: mongoIds }, + }) + .toArray(); + + assert.equal(0, pongoDocs.length); + assert.equal(0, mongoDocs.length); + + assert.deepStrictEqual( + pongoDocs.map((doc) => ({ + name: doc.name, + age: 31, + })), + mongoDocs.map((doc) => ({ + name: doc.name, + age: 31, + })), + ); + }); + }); + + void describe('Find Operations', () => { + void it('should find documents with a filter in both PostgreSQL and MongoDB', async () => { + const pongoCollection = pongoDb.collection('testCollection'); + const mongoCollection = mongoDb.collection('shimtestCollection'); + const docs = [ + { name: 'David', age: 40 }, + { name: 'Eve', age: 45 }, + { name: 'Frank', age: 50 }, + ]; + + await pongoCollection.insertOne(docs[0]!); + await pongoCollection.insertOne(docs[1]!); + await pongoCollection.insertOne(docs[2]!); + + await mongoCollection.insertOne(docs[0]!); + await mongoCollection.insertOne(docs[1]!); + await mongoCollection.insertOne(docs[2]!); + + const pongoDocs = await pongoCollection.find({ age: { $gte: 45 } }); + const mongoDocs = await mongoCollection + .find({ age: { $gte: 45 } }) + .toArray(); + + assert.strictEqual(pongoDocs.length, 2); + + assert.deepStrictEqual( + pongoDocs.map((d) => ({ name: d.name, age: d.age })), + mongoDocs.map((d) => ({ name: d.name, age: d.age })), + ); + }); + + void it('should find one document with a filter in both PostgreSQL and MongoDB', async () => { + const pongoCollection = pongoDb.collection('testCollection'); + const mongoCollection = mongoDb.collection('shimtestCollection'); + const doc = { name: 'Grace', age: 55 }; + + await pongoCollection.insertOne(doc); + await mongoCollection.insertOne(doc); + + const pongoDoc = await pongoCollection.findOne({ name: 'Grace' }); + const mongoDoc = await mongoCollection.findOne({ name: 'Grace' }); + + assert.deepStrictEqual( + { + name: pongoDoc!.name, + age: pongoDoc!.age, + }, + { + name: mongoDoc!.name, + age: mongoDoc!.age, + }, + ); + }); + + void it.skip('should find documents with a nested property filter in both PostgreSQL and MongoDB', async () => { + const pongoCollection = pongoDb.collection( + 'findWithNestedProperty', + ); + const mongoCollection = mongoDb.collection( + 'shimfindWithNestedProperty', + ); + + const docs = [ + { + name: 'David', + age: 40, + address: { city: 'Dreamland', zip: '12345' }, + }, + { name: 'Eve', age: 45, address: { city: 'Wonderland', zip: '67890' } }, + { + name: 'Frank', + age: 50, + address: { city: 'Nightmare', zip: '54321' }, + }, + ]; + + await pongoCollection.insertOne(docs[0]!); + await pongoCollection.insertOne(docs[1]!); + await pongoCollection.insertOne(docs[2]!); + + await mongoCollection.insertOne(docs[0]!); + await mongoCollection.insertOne(docs[1]!); + await mongoCollection.insertOne(docs[2]!); + + const pongoDocs = await pongoCollection.find({ + // TODO: fix filter typing + //'address.city': 'Wonderland', + }); + const mongoDocs = await mongoCollection + .find({ 'address.city': 'Wonderland' }) + .toArray(); + + assert.deepStrictEqual( + pongoDocs.map((d) => ({ + name: d.name, + age: d.age, + address: d.address, + })), + mongoDocs.map((d) => ({ + name: d.name, + age: d.age, + address: d.address, + })), + ); + }); + + void it.skip('should find documents with multiple nested property filters in both PostgreSQL and MongoDB', async () => { + const pongoCollection = pongoDb.collection( + 'findWithMultipleNestedProperties', + ); + const mongoCollection = mongoDb.collection( + 'shimfindWithMultipleNestedProperties', + ); + + const docs = [ + { + name: 'Anita', + age: 25, + address: { city: 'Wonderland', street: 'Main St' }, + }, + { + name: 'Roger', + age: 30, + address: { city: 'Wonderland', street: 'Elm St' }, + }, + { + name: 'Cruella', + age: 35, + address: { city: 'Dreamland', street: 'Oak St' }, + }, + ]; + + await pongoCollection.insertOne(docs[0]!); + await pongoCollection.insertOne(docs[1]!); + await pongoCollection.insertOne(docs[2]!); + + await mongoCollection.insertOne(docs[0]!); + await mongoCollection.insertOne(docs[1]!); + await mongoCollection.insertOne(docs[2]!); + + const pongoDocs = await pongoCollection.find({ + // TODO: fix filter typing + //'address.city': 'Wonderland', + //'address.street': 'Elm St', + }); + const mongoDocs = await mongoCollection + .find({ 'address.city': 'Wonderland', 'address.street': 'Elm St' }) + .toArray(); + + assert.deepStrictEqual( + pongoDocs.map((d) => ({ + name: d.name, + age: d.age, + address: d.address, + })), + mongoDocs.map((d) => ({ + name: d.name, + age: d.age, + address: d.address, + })), + ); + }); + + void it('should find documents with multiple nested property object filters in both PostgreSQL and MongoDB', async () => { + const pongoCollection = pongoDb.collection('testCollection'); + const mongoCollection = mongoDb.collection('shimtestCollection'); + + const docs = [ + { + name: 'Anita', + age: 25, + address: { city: 'Wonderland', street: 'Main St' }, + }, + { + name: 'Roger', + age: 30, + address: { city: 'Wonderland', street: 'Elm St' }, + }, + { + name: 'Cruella', + age: 35, + address: { city: 'Dreamland', street: 'Oak St' }, + }, + ]; + + await pongoCollection.insertOne(docs[0]!); + await pongoCollection.insertOne(docs[1]!); + await pongoCollection.insertOne(docs[2]!); + + await mongoCollection.insertOne(docs[0]!); + await mongoCollection.insertOne(docs[1]!); + await mongoCollection.insertOne(docs[2]!); + + //const pongoDocs: User[] = []; + const pongoDocs = await pongoCollection.find({ + address: { city: 'Wonderland', street: 'Elm St' }, + }); + const mongoDocs = await mongoCollection + .find({ address: { city: 'Wonderland', street: 'Elm St' } }) + .toArray(); + + assert.deepStrictEqual( + pongoDocs.map((d) => ({ + name: d.name, + age: d.age, + address: d.address, + })), + mongoDocs.map((d) => ({ + name: d.name, + age: d.age, + address: d.address, + })), + ); + }); + + void it.skip('should find documents with an array filter in both PostgreSQL and MongoDB', async () => { + const pongoCollection = pongoDb.collection('findWithArrayFilter'); + const mongoCollection = mongoDb.collection( + 'shimfindWithArrayFilter', + ); + + const docs = [ + { name: 'Anita', age: 25, tags: ['tag1', 'tag2'] }, + { name: 'Roger', age: 30, tags: ['tag2', 'tag3'] }, + { name: 'Cruella', age: 35, tags: ['tag1', 'tag3'] }, + ]; + + await pongoCollection.insertOne(docs[0]!); + await pongoCollection.insertOne(docs[1]!); + await pongoCollection.insertOne(docs[2]!); + + await mongoCollection.insertOne(docs[0]!); + await mongoCollection.insertOne(docs[1]!); + await mongoCollection.insertOne(docs[2]!); + + const pongoDocs = await pongoCollection.find({ + // TODO: fix filter typing + // tags: 'tag1' + }); + const mongoDocs = await mongoCollection.find({ tags: 'tag1' }).toArray(); + + assert.deepStrictEqual( + pongoDocs.map((d) => ({ name: d.name, age: d.age, tags: d.tags })), + mongoDocs.map((d) => ({ name: d.name, age: d.age, tags: d.tags })), + ); + }); + + void it.skip('should find documents with multiple array filters in both PostgreSQL and MongoDB', async () => { + const pongoCollection = pongoDb.collection( + 'findWithMultipleArrayFilters', + ); + const mongoCollection = mongoDb.collection( + 'shimfindWithMultipleArrayFilters', + ); + + const docs = [ + { name: 'Anita', age: 25, tags: ['tag1', 'tag2'] }, + { name: 'Roger', age: 30, tags: ['tag2', 'tag3'] }, + { name: 'Cruella', age: 35, tags: ['tag1', 'tag3'] }, + ]; + + await pongoCollection.insertOne(docs[0]!); + await pongoCollection.insertOne(docs[1]!); + await pongoCollection.insertOne(docs[2]!); + + await mongoCollection.insertOne(docs[0]!); + await mongoCollection.insertOne(docs[1]!); + await mongoCollection.insertOne(docs[2]!); + + const pongoDocs = await pongoCollection.find({ + // TODO: fix filter typing + //tags: { $all: ['tag1', 'tag2'] }, + }); + const mongoDocs = await mongoCollection + .find({ tags: { $all: ['tag1', 'tag2'] } }) + .toArray(); + + assert.deepStrictEqual( + pongoDocs.map((d) => ({ name: d.name, age: d.age, tags: d.tags })), + mongoDocs.map((d) => ({ name: d.name, age: d.age, tags: d.tags })), + ); + }); + + void it.skip('should find documents with an array element match filter in both PostgreSQL and MongoDB', async () => { + const pongoCollection = pongoDb.collection('testCollection'); + const mongoCollection = mongoDb.collection('shimtestCollection'); + + const docs = [ + { name: 'Anita', age: 25, tags: ['tag1', 'tag2'] }, + { name: 'Roger', age: 30, tags: ['tag2', 'tag3'] }, + { name: 'Cruella', age: 35, tags: ['tag1', 'tag3'] }, + ]; + + await pongoCollection.insertOne(docs[0]!); + await pongoCollection.insertOne(docs[1]!); + await pongoCollection.insertOne(docs[2]!); + + await mongoCollection.insertOne(docs[0]!); + await mongoCollection.insertOne(docs[1]!); + await mongoCollection.insertOne(docs[2]!); + + const pongoDocs = await pongoCollection.find({ + // TODO: fix filter typing + //tags: { $elemMatch: { $eq: 'tag1' } }, + }); + const mongoDocs = await mongoCollection + .find({ tags: { $elemMatch: { $eq: 'tag1' } } }) + .toArray(); + + assert.deepStrictEqual( + pongoDocs.map((d) => ({ name: d.name, age: d.age, tags: d.tags })), + mongoDocs.map((d) => ({ name: d.name, age: d.age, tags: d.tags })), + ); + }); + + void it.skip('should find documents with a nested array element match filter in both PostgreSQL and MongoDB', async () => { + const pongoCollection = pongoDb.collection( + 'findWithElemMatchFilter', + ); + const mongoCollection = mongoDb.collection( + 'shimfindWithElemMatchFilter', + ); + + const docs = [ + { + name: 'Anita', + age: 25, + address: { + city: 'Wonderland', + zip: '12345', + history: [{ street: 'Main St' }, { street: 'Elm St' }], + }, + }, + { + name: 'Roger', + age: 30, + address: { + city: 'Wonderland', + zip: '67890', + history: [{ street: 'Main St' }, { street: 'Oak St' }], + }, + }, + { + name: 'Cruella', + age: 35, + address: { + city: 'Dreamland', + zip: '54321', + history: [{ street: 'Elm St' }], + }, + }, + ]; + + await pongoCollection.insertOne(docs[0]!); + await pongoCollection.insertOne(docs[1]!); + await pongoCollection.insertOne(docs[2]!); + + await mongoCollection.insertOne(docs[0]!); + await mongoCollection.insertOne(docs[1]!); + await mongoCollection.insertOne(docs[2]!); + + const pongoDocs = await pongoCollection.find({ + // TODO: fix filter typing + //'address.history': { $elemMatch: { street: 'Elm St' } }, + }); + const mongoDocs = await mongoCollection + .find({ 'address.history': { $elemMatch: { street: 'Elm St' } } }) + .toArray(); + + assert.deepStrictEqual( + pongoDocs.map((d) => ({ + name: d.name, + age: d.age, + address: d.address, + })), + mongoDocs.map((d) => ({ + name: d.name, + age: d.age, + address: d.address, + })), + ); + }); + }); + + void describe('Handle Operations', () => { + void it('should insert a new document if it does not exist', async () => { + const pongoCollection = pongoDb.collection('handleCollection'); + const nonExistingId = uuid() as unknown as ObjectId; + + const newDoc: User = { name: 'John', age: 25 }; + + const handle = (_existing: User | null) => newDoc; + + const resultPongo = await pongoCollection.handle(nonExistingId, handle); + assert.deepStrictEqual(resultPongo, { ...newDoc, _id: nonExistingId }); + + const pongoDoc = await pongoCollection.findOne({ + _id: nonExistingId, + }); + + assert.deepStrictEqual(pongoDoc, { ...newDoc, _id: nonExistingId }); + }); + + void it('should replace an existing document', async () => { + const pongoCollection = pongoDb.collection('handleCollection'); + + const existingDoc: User = { name: 'John', age: 25 }; + const updatedDoc: User = { name: 'John', age: 30 }; + + const pongoInsertResult = await pongoCollection.insertOne(existingDoc); + + const handle = (_existing: User | null) => updatedDoc; + + const resultPongo = await pongoCollection.handle( + pongoInsertResult.insertedId!, + handle, + ); + + assert.deepStrictEqual(resultPongo, { + ...updatedDoc, + }); + + const pongoDoc = await pongoCollection.findOne({ + _id: pongoInsertResult.insertedId!, + }); + + assert.deepStrictEqual(pongoDoc, { + ...updatedDoc, + _id: pongoInsertResult.insertedId, + }); + }); + + void it('should delete an existing document if the handler returns null', async () => { + const pongoCollection = pongoDb.collection('handleCollection'); + + const existingDoc: User = { name: 'John', age: 25 }; + + const pongoInsertResult = await pongoCollection.insertOne(existingDoc); + + const handle = (_existing: User | null) => null; + + const resultPongo = await pongoCollection.handle( + pongoInsertResult.insertedId!, + handle, + ); + + assert.strictEqual(resultPongo, null); + + const pongoDoc = await pongoCollection.findOne({ + _id: pongoInsertResult.insertedId!, + }); + + assert.strictEqual(pongoDoc, null); + }); + + void it('should do nothing if the handler returns the existing document unchanged', async () => { + const pongoCollection = pongoDb.collection('handleCollection'); + + const existingDoc: User = { name: 'John', age: 25 }; + + const pongoInsertResult = await pongoCollection.insertOne(existingDoc); + + const handle = (existing: User | null) => existing; + + const resultPongo = await pongoCollection.handle( + pongoInsertResult.insertedId!, + handle, + ); + + assert.deepStrictEqual(resultPongo, { + ...existingDoc, + _id: pongoInsertResult.insertedId, + }); + + const pongoDoc = await pongoCollection.findOne({ + _id: pongoInsertResult.insertedId!, + }); + + assert.deepStrictEqual(pongoDoc, { + ...existingDoc, + _id: pongoInsertResult.insertedId, + }); + }); + }); +}); From 2db3bc604401401806373d1113941184213cd1f1 Mon Sep 17 00:00:00 2001 From: Oskar Dudycz Date: Sat, 3 Aug 2024 15:34:03 +0200 Subject: [PATCH 30/30] Fixed commiting transaction --- .../dumbo/src/core/connections/transaction.ts | 37 ++++-- .../src/core/collection/pongoCollection.ts | 4 +- .../pongo/src/core/pongoTransaction.ts | 8 +- .../pongo/src/e2e/postgres.e2e.spec.ts | 109 +++++++++++++++--- src/packages/pongo/src/mongo/mongoClient.ts | 20 +++- .../pongo/src/mongo/mongoCollection.ts | 101 +++++++++++----- 6 files changed, 217 insertions(+), 62 deletions(-) diff --git a/src/packages/dumbo/src/core/connections/transaction.ts b/src/packages/dumbo/src/core/connections/transaction.ts index b2f9d03a..4415911e 100644 --- a/src/packages/dumbo/src/core/connections/transaction.ts +++ b/src/packages/dumbo/src/core/connections/transaction.ts @@ -59,18 +59,41 @@ export const transactionFactoryWithDbClient = < executeInTransaction(initTransaction(connect()), handle), }); +const wrapInConnectionClosure = async < + ConnectionType extends Connection = Connection, + Result = unknown, +>( + connection: ConnectionType, + handle: () => Promise, +) => { + try { + return await handle(); + } finally { + await connection.close(); + } +}; + export const transactionFactoryWithNewConnection = < ConnectionType extends Connection = Connection, >( connect: () => ConnectionType, ): DatabaseTransactionFactory => ({ - transaction: () => connect().transaction(), - withTransaction: async (handle) => { + transaction: () => { + const connection = connect(); + const transaction = connection.transaction(); + + return { + ...transaction, + commit: () => + wrapInConnectionClosure(connection, () => transaction.commit()), + rollback: () => + wrapInConnectionClosure(connection, () => transaction.rollback()), + }; + }, + withTransaction: (handle) => { const connection = connect(); - try { - return await connection.withTransaction(handle); - } finally { - await connection.close(); - } + return wrapInConnectionClosure(connection, () => + connection.withTransaction(handle), + ); }, }); diff --git a/src/packages/pongo/src/core/collection/pongoCollection.ts b/src/packages/pongo/src/core/collection/pongoCollection.ts index c7f7667f..ccf60cc4 100644 --- a/src/packages/pongo/src/core/collection/pongoCollection.ts +++ b/src/packages/pongo/src/core/collection/pongoCollection.ts @@ -75,7 +75,9 @@ export const pongoCollection = < const createCollectionPromise = command(SqlFor.createCollection()); const createCollection = (options?: CollectionOperationOptions) => - options?.session ? createCollectionPromise : createCollectionPromise; + options?.session + ? command(SqlFor.createCollection(), options) + : createCollectionPromise; const collection = { dbName: db.databaseName, diff --git a/src/packages/pongo/src/core/pongoTransaction.ts b/src/packages/pongo/src/core/pongoTransaction.ts index 95b74319..df9180d5 100644 --- a/src/packages/pongo/src/core/pongoTransaction.ts +++ b/src/packages/pongo/src/core/pongoTransaction.ts @@ -29,9 +29,9 @@ export const pongoTransaction = ( return transaction; }, commit: async () => { - if (isCommitted) return; - if (!isRolledBack) throw new Error('Transaction is not active!'); if (!transaction) throw new Error('No database transaction started!'); + if (isCommitted) return; + if (isRolledBack) throw new Error('Transaction is not active!'); isCommitted = true; @@ -40,9 +40,9 @@ export const pongoTransaction = ( transaction = null; }, rollback: async (error?: unknown) => { - if (isCommitted) throw new Error('Cannot rollback commited transaction!'); - if (!isRolledBack) return; if (!transaction) throw new Error('No database transaction started!'); + if (isCommitted) throw new Error('Cannot rollback commited transaction!'); + if (isRolledBack) return; isRolledBack = true; diff --git a/src/packages/pongo/src/e2e/postgres.e2e.spec.ts b/src/packages/pongo/src/e2e/postgres.e2e.spec.ts index 5ab11d93..b91d98b4 100644 --- a/src/packages/pongo/src/e2e/postgres.e2e.spec.ts +++ b/src/packages/pongo/src/e2e/postgres.e2e.spec.ts @@ -58,6 +58,7 @@ void describe('MongoDB Compatibility Tests', () => { try { await client.close(); await shim.close(); + //await endAllPools(); await postgres.stop(); } catch (error) { console.log(error); @@ -127,7 +128,7 @@ void describe('MongoDB Compatibility Tests', () => { }); void describe('Update Operations', () => { - void it('should update a document in both PostgreSQL and MongoDB', async () => { + void it('should update a document', async () => { const pongoCollection = pongoDb.collection('updateOne'); const mongoCollection = mongoDb.collection('shimupdateOne'); const doc = { name: 'Roger', age: 30 }; @@ -166,7 +167,7 @@ void describe('MongoDB Compatibility Tests', () => { ); }); - void it('should update a multiple properties in document in both PostgreSQL and MongoDB', async () => { + void it('should update a multiple properties in document', async () => { const pongoCollection = pongoDb.collection('updateOneMultiple'); const mongoCollection = mongoDb.collection('shimupdateOneMultiple'); const doc = { name: 'Roger', age: 30 }; @@ -208,7 +209,7 @@ void describe('MongoDB Compatibility Tests', () => { ); }); - void it('should update documents in both PostgreSQL and MongoDB', async () => { + void it('should update documents', async () => { const pongoCollection = pongoDb.collection('updateMany'); const mongoCollection = mongoDb.collection('shimupdateMany'); @@ -259,7 +260,7 @@ void describe('MongoDB Compatibility Tests', () => { ); }); - void it('should update a document in both PostgreSQL and MongoDB using $unset', async () => { + void it('should update a document using $unset', async () => { const pongoCollection = pongoDb.collection('testCollection'); const mongoCollection = mongoDb.collection('shimtestCollection'); const doc = { name: 'Roger', age: 30, address: { city: 'Wonderland' } }; @@ -298,7 +299,7 @@ void describe('MongoDB Compatibility Tests', () => { ); }); - void it('should update a document in both PostgreSQL and MongoDB using $inc', async () => { + void it('should update a document using $inc', async () => { const pongoCollection = pongoDb.collection('testCollection'); const mongoCollection = mongoDb.collection('shimtestCollection'); const doc = { name: 'Roger', age: 30 }; @@ -337,7 +338,7 @@ void describe('MongoDB Compatibility Tests', () => { ); }); - void it('should update a document in both PostgreSQL and MongoDB using $push', async () => { + void it('should update a document using $push', async () => { const pongoCollection = pongoDb.collection('testCollection'); const mongoCollection = mongoDb.collection('shimtestCollection'); const doc = { name: 'Roger', age: 30 }; @@ -396,7 +397,7 @@ void describe('MongoDB Compatibility Tests', () => { }); void describe('Replace Operations', () => { - void it('should replace a document in both PostgreSQL and MongoDB', async () => { + void it('should replace a document', async () => { const pongoCollection = pongoDb.collection('updateOne'); const mongoCollection = mongoDb.collection('shimupdateOne'); const doc = { name: 'Roger', age: 30 }; @@ -466,7 +467,7 @@ void describe('MongoDB Compatibility Tests', () => { assert.strictEqual(mongoDoc, null); }); - void it('should delete documents in both PostgreSQL and MongoDB', async () => { + void it('should delete documents', async () => { const pongoCollection = pongoDb.collection('updateMany'); const mongoCollection = mongoDb.collection('shimupdateMany'); @@ -515,10 +516,84 @@ void describe('MongoDB Compatibility Tests', () => { })), ); }); + + void it('should delete documents in transaction', async () => { + const docs = [ + { name: 'David', age: 40 }, + { name: 'Eve', age: 45 }, + { name: 'Frank', age: 50 }, + ]; + + await client.withSession((session) => + session.withTransaction(async () => { + const pongoCollection = pongoDb.collection('updateMany'); + + const pongoInsertResult = await pongoCollection.insertMany(docs, { + session, + }); + const pongoIds = Object.values(pongoInsertResult.insertedIds); + + const pongoDeleteResult = await pongoCollection.deleteMany( + { + _id: { $in: pongoIds }, + }, + { + session, + }, + ); + + assert.equal(3, pongoDeleteResult.deletedCount); + + const pongoDocs = await pongoCollection.find( + { + _id: { $in: pongoIds }, + }, + { + session, + }, + ); + assert.equal(0, pongoDocs.length); + }), + ); + await shim.withSession((session) => + session.withTransaction(async () => { + const mongoCollection = mongoDb.collection('updateMany'); + + const mongoInsertResult = await mongoCollection.insertMany(docs, { + session, + }); + const mongoIds = Object.values(mongoInsertResult.insertedIds); + + const mongoUpdateResult = await mongoCollection.deleteMany( + { + _id: { $in: mongoIds }, + }, + { + session, + }, + ); + + assert.equal(3, mongoUpdateResult.deletedCount); + + const mongoDocs = await mongoCollection + .find( + { + _id: { $in: mongoIds }, + }, + { + session, + }, + ) + .toArray(); + + assert.equal(0, mongoDocs.length); + }), + ); + }); }); void describe('Find Operations', () => { - void it('should find documents with a filter in both PostgreSQL and MongoDB', async () => { + void it('should find documents with a filter', async () => { const pongoCollection = pongoDb.collection('testCollection'); const mongoCollection = mongoDb.collection('shimtestCollection'); const docs = [ @@ -548,7 +623,7 @@ void describe('MongoDB Compatibility Tests', () => { ); }); - void it('should find one document with a filter in both PostgreSQL and MongoDB', async () => { + void it('should find one document with a filter', async () => { const pongoCollection = pongoDb.collection('testCollection'); const mongoCollection = mongoDb.collection('shimtestCollection'); const doc = { name: 'Grace', age: 55 }; @@ -571,7 +646,7 @@ void describe('MongoDB Compatibility Tests', () => { ); }); - void it.skip('should find documents with a nested property filter in both PostgreSQL and MongoDB', async () => { + void it.skip('should find documents with a nested property filter', async () => { const pongoCollection = pongoDb.collection( 'findWithNestedProperty', ); @@ -623,7 +698,7 @@ void describe('MongoDB Compatibility Tests', () => { ); }); - void it.skip('should find documents with multiple nested property filters in both PostgreSQL and MongoDB', async () => { + void it.skip('should find documents with multiple nested property filters', async () => { const pongoCollection = pongoDb.collection( 'findWithMultipleNestedProperties', ); @@ -680,7 +755,7 @@ void describe('MongoDB Compatibility Tests', () => { ); }); - void it('should find documents with multiple nested property object filters in both PostgreSQL and MongoDB', async () => { + void it('should find documents with multiple nested property object filters', async () => { const pongoCollection = pongoDb.collection('testCollection'); const mongoCollection = mongoDb.collection('shimtestCollection'); @@ -732,7 +807,7 @@ void describe('MongoDB Compatibility Tests', () => { ); }); - void it.skip('should find documents with an array filter in both PostgreSQL and MongoDB', async () => { + void it.skip('should find documents with an array filter', async () => { const pongoCollection = pongoDb.collection('findWithArrayFilter'); const mongoCollection = mongoDb.collection( 'shimfindWithArrayFilter', @@ -764,7 +839,7 @@ void describe('MongoDB Compatibility Tests', () => { ); }); - void it.skip('should find documents with multiple array filters in both PostgreSQL and MongoDB', async () => { + void it.skip('should find documents with multiple array filters', async () => { const pongoCollection = pongoDb.collection( 'findWithMultipleArrayFilters', ); @@ -800,7 +875,7 @@ void describe('MongoDB Compatibility Tests', () => { ); }); - void it.skip('should find documents with an array element match filter in both PostgreSQL and MongoDB', async () => { + void it.skip('should find documents with an array element match filter', async () => { const pongoCollection = pongoDb.collection('testCollection'); const mongoCollection = mongoDb.collection('shimtestCollection'); @@ -832,7 +907,7 @@ void describe('MongoDB Compatibility Tests', () => { ); }); - void it.skip('should find documents with a nested array element match filter in both PostgreSQL and MongoDB', async () => { + void it.skip('should find documents with a nested array element match filter', async () => { const pongoCollection = pongoDb.collection( 'findWithElemMatchFilter', ); diff --git a/src/packages/pongo/src/mongo/mongoClient.ts b/src/packages/pongo/src/mongo/mongoClient.ts index 626d010b..f575e034 100644 --- a/src/packages/pongo/src/mongo/mongoClient.ts +++ b/src/packages/pongo/src/mongo/mongoClient.ts @@ -2,6 +2,7 @@ import type { ClientSessionOptions } from 'http2'; import type { ClientSession, WithSessionCallback } from 'mongodb'; import { pongoClient, + pongoSession, type PongoClient, type PongoClientOptions, } from '../core'; @@ -27,7 +28,7 @@ export class MongoClient { return new Db(this.pongoClient.db(dbName)); } startSession(_options?: ClientSessionOptions): ClientSession { - throw new Error('Not implemented!'); + return pongoSession() as unknown as ClientSession; } // eslint-disable-next-line @typescript-eslint/no-explicit-any withSession(_executor: WithSessionCallback): Promise; @@ -37,10 +38,19 @@ export class MongoClient { _executor: WithSessionCallback, ): Promise; // eslint-disable-next-line @typescript-eslint/no-explicit-any - withSession( - _optionsOrExecutor: ClientSessionOptions | WithSessionCallback, - _executor?: WithSessionCallback, + async withSession( + optionsOrExecutor: ClientSessionOptions | WithSessionCallback, + executor?: WithSessionCallback, ): Promise { - return Promise.reject('Not Implemented!'); + const callback = + typeof optionsOrExecutor === 'function' ? optionsOrExecutor : executor!; + + const session = pongoSession() as unknown as ClientSession; + + try { + return await callback(session); + } finally { + await session.endSession(); + } } } diff --git a/src/packages/pongo/src/mongo/mongoCollection.ts b/src/packages/pongo/src/mongo/mongoCollection.ts index 99c94706..6b1e453f 100644 --- a/src/packages/pongo/src/mongo/mongoCollection.ts +++ b/src/packages/pongo/src/mongo/mongoCollection.ts @@ -60,13 +60,22 @@ import type { } from 'mongodb'; import type { Key } from 'readline'; import type { + CollectionOperationOptions, DocumentHandler, PongoCollection, PongoFilter, + PongoSession, PongoUpdate, } from '../core'; import { FindCursor } from './findCursor'; +const toCollectionOperationOptions = ( + options: OperationOptions | undefined, +): CollectionOperationOptions | undefined => + options?.session + ? { session: options.session as unknown as PongoSession } + : undefined; + export class Collection implements MongoCollection { private collection: PongoCollection; @@ -102,9 +111,12 @@ export class Collection implements MongoCollection { } async insertOne( doc: OptionalUnlessRequiredId, - _options?: InsertOneOptions | undefined, + options?: InsertOneOptions | undefined, ): Promise> { - const result = await this.collection.insertOne(doc as T); + const result = await this.collection.insertOne( + doc as T, + toCollectionOperationOptions(options), + ); return { acknowledged: result.acknowledged, insertedId: result.insertedId as unknown as InferIdType, @@ -112,9 +124,12 @@ export class Collection implements MongoCollection { } async insertMany( docs: OptionalUnlessRequiredId[], - _options?: BulkWriteOptions | undefined, + options?: BulkWriteOptions | undefined, ): Promise> { - const result = await this.collection.insertMany(docs as T[]); + const result = await this.collection.insertMany( + docs as T[], + toCollectionOperationOptions(options), + ); return { acknowledged: result.acknowledged, insertedIds: result.insertedIds as unknown as InferIdType[], @@ -130,11 +145,12 @@ export class Collection implements MongoCollection { async updateOne( filter: Filter, update: Document[] | UpdateFilter, - _options?: UpdateOptions | undefined, + options?: UpdateOptions | undefined, ): Promise> { const result = await this.collection.updateOne( filter as unknown as PongoFilter, update as unknown as PongoUpdate, + toCollectionOperationOptions(options), ); return { @@ -148,21 +164,23 @@ export class Collection implements MongoCollection { replaceOne( filter: Filter, document: WithoutId, - _options?: ReplaceOptions | undefined, + options?: ReplaceOptions | undefined, ): Promise> { return this.collection.replaceOne( filter as unknown as PongoFilter, document, + toCollectionOperationOptions(options), ); } async updateMany( filter: Filter, update: Document[] | UpdateFilter, - _options?: UpdateOptions | undefined, + options?: UpdateOptions | undefined, ): Promise> { const result = await this.collection.updateMany( filter as unknown as PongoFilter, update as unknown as PongoUpdate, + toCollectionOperationOptions(options), ); return { @@ -175,9 +193,12 @@ export class Collection implements MongoCollection { } async deleteOne( filter?: Filter | undefined, - _options?: DeleteOptions | undefined, + options?: DeleteOptions | undefined, ): Promise { - const result = await this.collection.deleteOne(filter as PongoFilter); + const result = await this.collection.deleteOne( + filter as PongoFilter, + toCollectionOperationOptions(options), + ); return { acknowledged: result.acknowledged, @@ -186,9 +207,12 @@ export class Collection implements MongoCollection { } async deleteMany( filter?: Filter | undefined, - _options?: DeleteOptions | undefined, + options?: DeleteOptions | undefined, ): Promise { - const result = await this.collection.deleteMany(filter as PongoFilter); + const result = await this.collection.deleteMany( + filter as PongoFilter, + toCollectionOperationOptions(options), + ); return { acknowledged: result.acknowledged, @@ -197,14 +221,17 @@ export class Collection implements MongoCollection { } async rename( newName: string, - _options?: RenameOptions | undefined, + options?: RenameOptions | undefined, ): Promise> { - await this.collection.rename(newName); + await this.collection.rename( + newName, + toCollectionOperationOptions(options), + ); return this as unknown as Collection; } - drop(_options?: DropCollectionOptions | undefined): Promise { - return this.collection.drop(); + drop(options?: DropCollectionOptions | undefined): Promise { + return this.collection.drop(toCollectionOperationOptions(options)); } findOne(): Promise | null>; findOne(filter: Filter): Promise | null>; @@ -220,9 +247,12 @@ export class Collection implements MongoCollection { ): Promise; async findOne( filter?: unknown, - _options?: unknown, + options?: FindOptions | undefined, ): Promise | T | null> { - return this.collection.findOne(filter as PongoFilter); + return this.collection.findOne( + filter as PongoFilter, + toCollectionOperationOptions(options), + ); } find(): MongoFindCursor>; find( @@ -235,10 +265,13 @@ export class Collection implements MongoCollection { ): MongoFindCursor; find( filter?: unknown, - _options?: unknown, + options?: FindOptions | undefined, ): MongoFindCursor> | MongoFindCursor { return new FindCursor( - this.collection.find(filter as PongoFilter), + this.collection.find( + filter as PongoFilter, + toCollectionOperationOptions(options), + ), ) as unknown as MongoFindCursor; } options(_options?: OperationOptions | undefined): Promise { @@ -301,15 +334,21 @@ export class Collection implements MongoCollection { throw new Error('Method not implemented.'); } estimatedDocumentCount( - _options?: EstimatedDocumentCountOptions | undefined, + options?: EstimatedDocumentCountOptions | undefined, ): Promise { - return this.collection.countDocuments(); + return this.collection.countDocuments( + {}, + toCollectionOperationOptions(options), + ); } countDocuments( filter?: Filter | undefined, - _options?: CountDocumentsOptions | undefined, + options?: CountDocumentsOptions | undefined, ): Promise { - return this.collection.countDocuments(filter as PongoFilter); + return this.collection.countDocuments( + filter as PongoFilter, + toCollectionOperationOptions(options), + ); } distinct>( key: Key, @@ -379,10 +418,11 @@ export class Collection implements MongoCollection { findOneAndDelete(filter: Filter): Promise | null>; findOneAndDelete( filter: unknown, - _options?: unknown, + options?: FindOneAndDeleteOptions, ): Promise | null | ModifyResult> { return this.collection.findOneAndDelete( filter as PongoFilter, + toCollectionOperationOptions(options), ) as Promise | null>; } findOneAndReplace( @@ -407,11 +447,12 @@ export class Collection implements MongoCollection { findOneAndReplace( filter: unknown, replacement: unknown, - _options?: unknown, + options?: FindOneAndReplaceOptions | undefined, ): Promise | null | ModifyResult> { return this.collection.findOneAndReplace( filter as PongoFilter, replacement as WithoutId, + toCollectionOperationOptions(options), ) as Promise | null>; } findOneAndUpdate( @@ -436,11 +477,12 @@ export class Collection implements MongoCollection { findOneAndUpdate( filter: unknown, update: unknown, - _options?: unknown, + options?: FindOneAndUpdateOptions | undefined, ): Promise | null | ModifyResult> { return this.collection.findOneAndUpdate( filter as PongoFilter, update as PongoUpdate, + toCollectionOperationOptions(options), ) as Promise | null>; } aggregate( @@ -470,9 +512,12 @@ export class Collection implements MongoCollection { } count( filter?: Filter | undefined, - _options?: CountOptions | undefined, + options?: CountOptions | undefined, ): Promise { - return this.collection.countDocuments((filter as PongoFilter) ?? {}); + return this.collection.countDocuments( + (filter as PongoFilter) ?? {}, + toCollectionOperationOptions(options), + ); } listSearchIndexes( options?: ListSearchIndexesOptions | undefined,