From e58429708f1f554436e2d37dbfb4f4288c5bb447 Mon Sep 17 00:00:00 2001 From: Arseny Yankovsky Date: Sat, 16 May 2020 16:41:11 +0200 Subject: [PATCH] feat: Aurora Data API - Postgres Support (#5651) * Data API Postgres WIP * Refactored the code to be more supportable --- src/connection/ConnectionOptions.ts | 2 + src/driver/DriverFactory.ts | 3 + .../AuroraDataApiPostgresConnectionOptions.ts | 34 +++++ .../AuroraDataApiPostgresQueryRunner.ts | 138 ++++++++++++++++++ src/driver/postgres/PostgresDriver.ts | 118 ++++++++++++++- src/driver/types/DatabaseType.ts | 1 + src/error/MissingDriverError.ts | 4 +- .../order-by/query-builder-order-by.ts | 2 +- 8 files changed, 298 insertions(+), 4 deletions(-) create mode 100644 src/driver/aurora-data-api-pg/AuroraDataApiPostgresConnectionOptions.ts create mode 100644 src/driver/aurora-data-api-pg/AuroraDataApiPostgresQueryRunner.ts diff --git a/src/connection/ConnectionOptions.ts b/src/connection/ConnectionOptions.ts index 2d223f74d6..1c37c7ce84 100644 --- a/src/connection/ConnectionOptions.ts +++ b/src/connection/ConnectionOptions.ts @@ -12,6 +12,7 @@ import {NativescriptConnectionOptions} from "../driver/nativescript/Nativescript import {ExpoConnectionOptions} from "../driver/expo/ExpoConnectionOptions"; import {AuroraDataApiConnectionOptions} from "../driver/aurora-data-api/AuroraDataApiConnectionOptions"; import {SapConnectionOptions} from "../driver/sap/SapConnectionOptions"; +import {AuroraDataApiPostgresConnectionOptions} from "../driver/aurora-data-api-pg/AuroraDataApiPostgresConnectionOptions"; /** @@ -33,4 +34,5 @@ export type ConnectionOptions = SqljsConnectionOptions| MongoConnectionOptions| AuroraDataApiConnectionOptions| + AuroraDataApiPostgresConnectionOptions| ExpoConnectionOptions; diff --git a/src/driver/DriverFactory.ts b/src/driver/DriverFactory.ts index f6aa97b017..80f58df4ef 100644 --- a/src/driver/DriverFactory.ts +++ b/src/driver/DriverFactory.ts @@ -15,6 +15,7 @@ import {AuroraDataApiDriver} from "./aurora-data-api/AuroraDataApiDriver"; import {Driver} from "./Driver"; import {Connection} from "../connection/Connection"; import {SapDriver} from "./sap/SapDriver"; +import {AuroraDataApiPostgresDriver} from "./postgres/PostgresDriver"; /** * Helps to create drivers. @@ -57,6 +58,8 @@ export class DriverFactory { return new ExpoDriver(connection); case "aurora-data-api": return new AuroraDataApiDriver(connection); + case "aurora-data-api-pg": + return new AuroraDataApiPostgresDriver(connection); default: throw new MissingDriverError(type); } diff --git a/src/driver/aurora-data-api-pg/AuroraDataApiPostgresConnectionOptions.ts b/src/driver/aurora-data-api-pg/AuroraDataApiPostgresConnectionOptions.ts new file mode 100644 index 0000000000..c0f110f2ca --- /dev/null +++ b/src/driver/aurora-data-api-pg/AuroraDataApiPostgresConnectionOptions.ts @@ -0,0 +1,34 @@ +import {BaseConnectionOptions} from "../../connection/BaseConnectionOptions"; + +/** + * Postgres-specific connection options. + */ +export interface AuroraDataApiPostgresConnectionOptions extends BaseConnectionOptions { + + /** + * Database type. + */ + readonly type: "aurora-data-api-pg"; + + readonly region: string; + + readonly secretArn: string; + + readonly resourceArn: string; + + readonly database: string; + + /** + * The Postgres extension to use to generate UUID columns. Defaults to uuid-ossp. + * If pgcrypto is selected, TypeORM will use the gen_random_uuid() function from this extension. + * If uuid-ossp is selected, TypeORM will use the uuid_generate_v4() function from this extension. + */ + readonly uuidExtension?: "pgcrypto" | "uuid-ossp"; + + + /* + * Function handling errors thrown by drivers pool. + * Defaults to logging error with `warn` level. + */ + readonly poolErrorHandler?: (err: any) => any; +} diff --git a/src/driver/aurora-data-api-pg/AuroraDataApiPostgresQueryRunner.ts b/src/driver/aurora-data-api-pg/AuroraDataApiPostgresQueryRunner.ts new file mode 100644 index 0000000000..2a553c134c --- /dev/null +++ b/src/driver/aurora-data-api-pg/AuroraDataApiPostgresQueryRunner.ts @@ -0,0 +1,138 @@ +import {QueryRunnerAlreadyReleasedError} from "../../error/QueryRunnerAlreadyReleasedError"; +import {TransactionAlreadyStartedError} from "../../error/TransactionAlreadyStartedError"; +import {TransactionNotStartedError} from "../../error/TransactionNotStartedError"; +import {QueryRunner} from "../../query-runner/QueryRunner"; +import {IsolationLevel} from "../types/IsolationLevel"; +import {AuroraDataApiPostgresDriver} from "../postgres/PostgresDriver"; +import {PostgresQueryRunner} from "../postgres/PostgresQueryRunner"; + +class PostgresQueryRunnerWrapper extends PostgresQueryRunner { + driver: any; + + constructor(driver: any, mode: "master"|"slave") { + super(driver, mode); + } +} + +/** + * Runs queries on a single postgres database connection. + */ +export class AuroraDataApiPostgresQueryRunner extends PostgresQueryRunnerWrapper implements QueryRunner { + + // ------------------------------------------------------------------------- + // Public Implemented Properties + // ------------------------------------------------------------------------- + + /** + * Database driver used by connection. + */ + driver: AuroraDataApiPostgresDriver; + + // ------------------------------------------------------------------------- + // Protected Properties + // ------------------------------------------------------------------------- + + /** + * Promise used to obtain a database connection for a first time. + */ + protected databaseConnectionPromise: Promise; + + /** + * Special callback provided by a driver used to release a created connection. + */ + protected releaseCallback: Function; + + // ------------------------------------------------------------------------- + // Constructor + // ------------------------------------------------------------------------- + + constructor(driver: AuroraDataApiPostgresDriver, mode: "master"|"slave" = "master") { + super(driver, mode); + } + + // ------------------------------------------------------------------------- + // Public Methods + // ------------------------------------------------------------------------- + + /** + * Creates/uses database connection from the connection pool to perform further operations. + * Returns obtained database connection. + */ + connect(): Promise { + if (this.databaseConnection) + return Promise.resolve(this.databaseConnection); + + if (this.databaseConnectionPromise) + return this.databaseConnectionPromise; + + if (this.mode === "slave" && this.driver.isReplicated) { + this.databaseConnectionPromise = this.driver.obtainSlaveConnection().then(([ connection, release]: any[]) => { + this.driver.connectedQueryRunners.push(this); + this.databaseConnection = connection; + this.releaseCallback = release; + return this.databaseConnection; + }); + + } else { // master + this.databaseConnectionPromise = this.driver.obtainMasterConnection().then(([connection, release]: any[]) => { + this.driver.connectedQueryRunners.push(this); + this.databaseConnection = connection; + this.releaseCallback = release; + return this.databaseConnection; + }); + } + + return this.databaseConnectionPromise; + } + + /** + * Starts transaction on the current connection. + */ + async startTransaction(isolationLevel?: IsolationLevel): Promise { + if (this.isTransactionActive) + throw new TransactionAlreadyStartedError(); + + this.isTransactionActive = true; + await this.driver.client.startTransaction(); + } + + /** + * Commits transaction. + * Error will be thrown if transaction was not started. + */ + async commitTransaction(): Promise { + if (!this.isTransactionActive) + throw new TransactionNotStartedError(); + + await this.driver.client.commitTransaction(); + this.isTransactionActive = false; + } + + /** + * Rollbacks transaction. + * Error will be thrown if transaction was not started. + */ + async rollbackTransaction(): Promise { + if (!this.isTransactionActive) + throw new TransactionNotStartedError(); + + await this.driver.client.rollbackTransaction(); + this.isTransactionActive = false; + } + + /** + * Executes a given SQL query. + */ + async query(query: string, parameters?: any[]): Promise { + if (this.isReleased) + throw new QueryRunnerAlreadyReleasedError(); + + const result = await this.driver.client.query(query, parameters); + + if (result.records) { + return result.records; + } + + return result; + } +} diff --git a/src/driver/postgres/PostgresDriver.ts b/src/driver/postgres/PostgresDriver.ts index 7be91177b9..15ac662493 100644 --- a/src/driver/postgres/PostgresDriver.ts +++ b/src/driver/postgres/PostgresDriver.ts @@ -19,6 +19,8 @@ import {PostgresConnectionCredentialsOptions} from "./PostgresConnectionCredenti import {EntityMetadata} from "../../metadata/EntityMetadata"; import {OrmUtils} from "../../util/OrmUtils"; import {ApplyValueTransformers} from "../../util/ApplyValueTransformers"; +import {AuroraDataApiPostgresConnectionOptions} from "../aurora-data-api-pg/AuroraDataApiPostgresConnectionOptions"; +import {AuroraDataApiPostgresQueryRunner} from "../aurora-data-api-pg/AuroraDataApiPostgresQueryRunner"; /** * Organizes communication with PostgreSQL DBMS. @@ -248,7 +250,11 @@ export class PostgresDriver implements Driver { // Constructor // ------------------------------------------------------------------------- - constructor(connection: Connection) { + constructor(connection?: Connection) { + if (!connection) { + return; + } + this.connection = connection; this.options = connection.options as PostgresConnectionOptions; this.isReplicated = this.options.replication ? true : false; @@ -972,3 +978,113 @@ export class PostgresDriver implements Driver { } } + +abstract class PostgresWrapper extends PostgresDriver { + options: any; + + abstract createQueryRunner(mode: "master"|"slave"): any; +} + +/** + * Organizes communication with PostgreSQL DBMS. + */ +export class AuroraDataApiPostgresDriver extends PostgresWrapper { + + // ------------------------------------------------------------------------- + // Public Properties + // ------------------------------------------------------------------------- + + /** + * Connection used by driver. + */ + connection: Connection; + + /** + * Aurora Data API underlying library. + */ + DataApiDriver: any; + + client: any; + + // ------------------------------------------------------------------------- + // Public Implemented Properties + // ------------------------------------------------------------------------- + + /** + * Connection options. + */ + options: AuroraDataApiPostgresConnectionOptions; + + /** + * Master database used to perform all write queries. + */ + database?: string; + + // ------------------------------------------------------------------------- + // Constructor + // ------------------------------------------------------------------------- + + constructor(connection: Connection) { + super(); + this.connection = connection; + this.options = connection.options as AuroraDataApiPostgresConnectionOptions; + this.isReplicated = false; + + // load data-api package + this.loadDependencies(); + + this.client = new this.DataApiDriver( + this.options.region, + this.options.secretArn, + this.options.resourceArn, + this.options.database, + (query: string, parameters?: any[]) => this.connection.logger.logQuery(query, parameters), + ); + } + + // ------------------------------------------------------------------------- + // Public Implemented Methods + // ------------------------------------------------------------------------- + + /** + * Performs connection to the database. + * Based on pooling options, it can either create connection immediately, + * either create a pool and create connection when needed. + */ + async connect(): Promise { + } + + /** + * Closes connection with database. + */ + async disconnect(): Promise { + } + + /** + * Creates a query runner used to execute database queries. + */ + createQueryRunner(mode: "master"|"slave" = "master") { + return new AuroraDataApiPostgresQueryRunner(this, mode); + } + + // ------------------------------------------------------------------------- + // Protected Methods + // ------------------------------------------------------------------------- + + /** + * If driver dependency is not given explicitly, then try to load it via "require". + */ + protected loadDependencies(): void { + const { pg } = PlatformTools.load("typeorm-aurora-data-api-driver"); + + this.DataApiDriver = pg; + } + + /** + * Executes given query. + */ + protected executeQuery(connection: any, query: string) { + return this.client.query(query); + } + +} diff --git a/src/driver/types/DatabaseType.ts b/src/driver/types/DatabaseType.ts index 68a3045013..4426c9829d 100644 --- a/src/driver/types/DatabaseType.ts +++ b/src/driver/types/DatabaseType.ts @@ -16,4 +16,5 @@ export type DatabaseType = "mssql"| "mongodb"| "aurora-data-api"| + "aurora-data-api-pg"| "expo"; diff --git a/src/error/MissingDriverError.ts b/src/error/MissingDriverError.ts index 01abd5492c..a5634f4679 100644 --- a/src/error/MissingDriverError.ts +++ b/src/error/MissingDriverError.ts @@ -7,7 +7,7 @@ export class MissingDriverError extends Error { constructor(driverType: string) { super(); Object.setPrototypeOf(this, MissingDriverError.prototype); - this.message = `Wrong driver: "${driverType}" given. Supported drivers are: "cordova", "expo", "mariadb", "mongodb", "mssql", "mysql", "oracle", "postgres", "sqlite", "sqljs", "react-native".`; + this.message = `Wrong driver: "${driverType}" given. Supported drivers are: "cordova", "expo", "mariadb", "mongodb", "mssql", "mysql", "oracle", "postgres", "sqlite", "sqljs", "react-native", "aurora-data-api", "aurora-data-api-pg".`; } -} \ No newline at end of file +} diff --git a/test/functional/query-builder/order-by/query-builder-order-by.ts b/test/functional/query-builder/order-by/query-builder-order-by.ts index 8ff9242396..4db3c69a99 100644 --- a/test/functional/query-builder/order-by/query-builder-order-by.ts +++ b/test/functional/query-builder/order-by/query-builder-order-by.ts @@ -135,4 +135,4 @@ describe("query builder > order-by", () => { expect(loadedPost2!.num2).to.be.equal(2); }))); -}); \ No newline at end of file +});