mirror of
https://github.com/typeorm/typeorm.git
synced 2025-12-08 21:26:23 +00:00
feat: Support Expo SQLite Next
* Add ExpoDriverFactory to create ExpoLegacyDriver * Add driver for new Expo SQLite API --------- Co-authored-by: Ruben Grimm <pmk1c@users.noreply.github.com> Co-authored-by: Lucian Mocanu <alumni@users.noreply.github.com>
This commit is contained in:
parent
3d79786a92
commit
7b242e1698
@ -10,7 +10,7 @@ import { NativescriptDriver } from "./nativescript/NativescriptDriver"
|
||||
import { SqljsDriver } from "./sqljs/SqljsDriver"
|
||||
import { MysqlDriver } from "./mysql/MysqlDriver"
|
||||
import { PostgresDriver } from "./postgres/PostgresDriver"
|
||||
import { ExpoDriver } from "./expo/ExpoDriver"
|
||||
import { ExpoDriverFactory } from "./expo/ExpoDriverFactory"
|
||||
import { AuroraMysqlDriver } from "./aurora-mysql/AuroraMysqlDriver"
|
||||
import { AuroraPostgresDriver } from "./aurora-postgres/AuroraPostgresDriver"
|
||||
import { Driver } from "./Driver"
|
||||
@ -59,7 +59,7 @@ export class DriverFactory {
|
||||
case "mongodb":
|
||||
return new MongoDriver(connection)
|
||||
case "expo":
|
||||
return new ExpoDriver(connection)
|
||||
return new ExpoDriverFactory(connection).create()
|
||||
case "aurora-mysql":
|
||||
return new AuroraMysqlDriver(connection)
|
||||
case "aurora-postgres":
|
||||
|
||||
@ -3,90 +3,32 @@ import { ExpoConnectionOptions } from "./ExpoConnectionOptions"
|
||||
import { ExpoQueryRunner } from "./ExpoQueryRunner"
|
||||
import { QueryRunner } from "../../query-runner/QueryRunner"
|
||||
import { DataSource } from "../../data-source/DataSource"
|
||||
import { ReplicationMode } from "../types/ReplicationMode"
|
||||
|
||||
export class ExpoDriver extends AbstractSqliteDriver {
|
||||
options: ExpoConnectionOptions
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Constructor
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
constructor(connection: DataSource) {
|
||||
super(connection)
|
||||
|
||||
this.database = this.options.database
|
||||
|
||||
// load sqlite package
|
||||
this.sqlite = this.options.driver
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Public Methods
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Closes connection with database.
|
||||
*/
|
||||
async disconnect(): Promise<void> {
|
||||
return new Promise<void>((ok, fail) => {
|
||||
try {
|
||||
this.queryRunner = undefined
|
||||
this.databaseConnection._db.close()
|
||||
this.databaseConnection = undefined
|
||||
ok()
|
||||
} catch (error) {
|
||||
fail(error)
|
||||
}
|
||||
})
|
||||
this.queryRunner = undefined
|
||||
await this.databaseConnection.closeAsync()
|
||||
this.databaseConnection = undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a query runner used to execute database queries.
|
||||
*/
|
||||
createQueryRunner(mode: ReplicationMode): QueryRunner {
|
||||
createQueryRunner(): QueryRunner {
|
||||
if (!this.queryRunner) this.queryRunner = new ExpoQueryRunner(this)
|
||||
|
||||
return this.queryRunner
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Protected Methods
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Creates connection with the database.
|
||||
*/
|
||||
protected createDatabaseConnection() {
|
||||
return new Promise<void>((ok, fail) => {
|
||||
try {
|
||||
const databaseConnection = this.sqlite.openDatabase(
|
||||
this.options.database,
|
||||
)
|
||||
/*
|
||||
// we need to enable foreign keys in sqlite to make sure all foreign key related features
|
||||
// working properly. this also makes onDelete work with sqlite.
|
||||
*/
|
||||
databaseConnection.transaction(
|
||||
(tsx: any) => {
|
||||
tsx.executeSql(
|
||||
`PRAGMA foreign_keys = ON`,
|
||||
[],
|
||||
(t: any, result: any) => {
|
||||
ok(databaseConnection)
|
||||
},
|
||||
(t: any, err: any) => {
|
||||
fail({ transaction: t, error: err })
|
||||
},
|
||||
)
|
||||
},
|
||||
(err: any) => {
|
||||
fail(err)
|
||||
},
|
||||
)
|
||||
} catch (error) {
|
||||
fail(error)
|
||||
}
|
||||
})
|
||||
protected async createDatabaseConnection() {
|
||||
this.databaseConnection = await this.sqlite.openDatabaseAsync(
|
||||
this.options.database,
|
||||
)
|
||||
await this.databaseConnection.runAsync("PRAGMA foreign_keys = ON")
|
||||
return this.databaseConnection
|
||||
}
|
||||
}
|
||||
|
||||
23
src/driver/expo/ExpoDriverFactory.ts
Normal file
23
src/driver/expo/ExpoDriverFactory.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { DataSource } from "../../data-source"
|
||||
import { ExpoDriver } from "./ExpoDriver"
|
||||
import { ExpoLegacyDriver } from "./legacy/ExpoLegacyDriver"
|
||||
|
||||
export class ExpoDriverFactory {
|
||||
connection: DataSource
|
||||
|
||||
constructor(connection: DataSource) {
|
||||
this.connection = connection
|
||||
}
|
||||
|
||||
create(): ExpoDriver | ExpoLegacyDriver {
|
||||
if (this.isLegacyDriver) {
|
||||
return new ExpoLegacyDriver(this.connection)
|
||||
}
|
||||
|
||||
return new ExpoDriver(this.connection)
|
||||
}
|
||||
|
||||
private get isLegacyDriver(): boolean {
|
||||
return !("openDatabaseAsync" in this.connection.options.driver)
|
||||
}
|
||||
}
|
||||
@ -1,49 +1,14 @@
|
||||
import { QueryRunnerAlreadyReleasedError } from "../../error/QueryRunnerAlreadyReleasedError"
|
||||
import { QueryFailedError } from "../../error/QueryFailedError"
|
||||
import { AbstractSqliteQueryRunner } from "../sqlite-abstract/AbstractSqliteQueryRunner"
|
||||
import { TransactionNotStartedError } from "../../error/TransactionNotStartedError"
|
||||
import { ExpoDriver } from "./ExpoDriver"
|
||||
import { Broadcaster } from "../../subscriber/Broadcaster"
|
||||
import { QueryResult } from "../../query-runner/QueryResult"
|
||||
import { BroadcasterResult } from "../../subscriber/BroadcasterResult"
|
||||
|
||||
// Needed to satisfy the Typescript compiler
|
||||
interface IResultSet {
|
||||
insertId: number | undefined
|
||||
rowsAffected: number
|
||||
rows: {
|
||||
length: number
|
||||
item: (idx: number) => any
|
||||
_array: any[]
|
||||
}
|
||||
}
|
||||
interface ITransaction {
|
||||
executeSql: (
|
||||
sql: string,
|
||||
args: any[] | undefined,
|
||||
ok: (tsx: ITransaction, resultSet: IResultSet) => void,
|
||||
fail: (tsx: ITransaction, err: any) => void,
|
||||
) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs queries on a single sqlite database connection.
|
||||
*/
|
||||
export class ExpoQueryRunner extends AbstractSqliteQueryRunner {
|
||||
/**
|
||||
* Database driver used by connection.
|
||||
*/
|
||||
driver: ExpoDriver
|
||||
|
||||
/**
|
||||
* Database transaction object
|
||||
*/
|
||||
private transaction?: ITransaction
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Constructor
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
constructor(driver: ExpoDriver) {
|
||||
super()
|
||||
this.driver = driver
|
||||
@ -51,111 +16,14 @@ export class ExpoQueryRunner extends AbstractSqliteQueryRunner {
|
||||
this.broadcaster = new Broadcaster(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts transaction. Within Expo, all database operations happen in a
|
||||
* transaction context, so issuing a `BEGIN TRANSACTION` command is
|
||||
* redundant and will result in the following error:
|
||||
*
|
||||
* `Error: Error code 1: cannot start a transaction within a transaction`
|
||||
*
|
||||
* Instead, we keep track of a `Transaction` object in `this.transaction`
|
||||
* and continue using the same object until we wish to commit the
|
||||
* transaction.
|
||||
*/
|
||||
async startTransaction(): Promise<void> {
|
||||
this.isTransactionActive = true
|
||||
try {
|
||||
await this.broadcaster.broadcast("BeforeTransactionStart")
|
||||
} catch (err) {
|
||||
this.isTransactionActive = false
|
||||
throw err
|
||||
}
|
||||
|
||||
this.transactionDepth += 1
|
||||
|
||||
await this.broadcaster.broadcast("AfterTransactionStart")
|
||||
}
|
||||
|
||||
/**
|
||||
* Commits transaction.
|
||||
* Error will be thrown if transaction was not started.
|
||||
* Since Expo will automatically commit the transaction once all the
|
||||
* callbacks of the transaction object have been completed, "committing" a
|
||||
* transaction in this driver's context means that we delete the transaction
|
||||
* object and set the stage for the next transaction.
|
||||
*/
|
||||
async commitTransaction(): Promise<void> {
|
||||
if (
|
||||
!this.isTransactionActive &&
|
||||
typeof this.transaction === "undefined"
|
||||
)
|
||||
throw new TransactionNotStartedError()
|
||||
|
||||
await this.broadcaster.broadcast("BeforeTransactionCommit")
|
||||
|
||||
this.transaction = undefined
|
||||
this.isTransactionActive = false
|
||||
|
||||
this.transactionDepth -= 1
|
||||
|
||||
await this.broadcaster.broadcast("AfterTransactionCommit")
|
||||
}
|
||||
|
||||
/**
|
||||
* Rollbacks transaction.
|
||||
* Error will be thrown if transaction was not started.
|
||||
* This method's functionality is identical to `commitTransaction()` because
|
||||
* the transaction lifecycle is handled within the Expo transaction object.
|
||||
* Issuing separate statements for `COMMIT` or `ROLLBACK` aren't necessary.
|
||||
*/
|
||||
async rollbackTransaction(): Promise<void> {
|
||||
if (
|
||||
!this.isTransactionActive &&
|
||||
typeof this.transaction === "undefined"
|
||||
)
|
||||
throw new TransactionNotStartedError()
|
||||
|
||||
await this.broadcaster.broadcast("BeforeTransactionRollback")
|
||||
|
||||
this.transaction = undefined
|
||||
this.isTransactionActive = false
|
||||
|
||||
this.transactionDepth -= 1
|
||||
|
||||
await this.broadcaster.broadcast("AfterTransactionRollback")
|
||||
}
|
||||
|
||||
/**
|
||||
* Called before migrations are run.
|
||||
*/
|
||||
async beforeMigration(): Promise<void> {
|
||||
const databaseConnection = await this.connect()
|
||||
return new Promise((ok, fail) => {
|
||||
databaseConnection.exec(
|
||||
[{ sql: "PRAGMA foreign_keys = OFF", args: [] }],
|
||||
false,
|
||||
(err: any) => (err ? fail(err) : ok()),
|
||||
)
|
||||
})
|
||||
await this.query("PRAGMA foreign_keys = OFF")
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after migrations are run.
|
||||
*/
|
||||
async afterMigration(): Promise<void> {
|
||||
const databaseConnection = await this.connect()
|
||||
return new Promise((ok, fail) => {
|
||||
databaseConnection.exec(
|
||||
[{ sql: "PRAGMA foreign_keys = ON", args: [] }],
|
||||
false,
|
||||
(err: any) => (err ? fail(err) : ok()),
|
||||
)
|
||||
})
|
||||
await this.query("PRAGMA foreign_keys = ON")
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a given SQL query.
|
||||
*/
|
||||
async query(
|
||||
query: string,
|
||||
parameters?: any[],
|
||||
@ -163,117 +31,79 @@ export class ExpoQueryRunner extends AbstractSqliteQueryRunner {
|
||||
): Promise<any> {
|
||||
if (this.isReleased) throw new QueryRunnerAlreadyReleasedError()
|
||||
|
||||
return new Promise<any>(async (ok, fail) => {
|
||||
const databaseConnection = await this.connect()
|
||||
const broadcasterResult = new BroadcasterResult()
|
||||
const databaseConnection = await this.connect()
|
||||
const broadcasterResult = new BroadcasterResult()
|
||||
|
||||
this.driver.connection.logger.logQuery(query, parameters, this)
|
||||
this.broadcaster.broadcastBeforeQueryEvent(
|
||||
this.driver.connection.logger.logQuery(query, parameters, this)
|
||||
this.broadcaster.broadcastBeforeQueryEvent(
|
||||
broadcasterResult,
|
||||
query,
|
||||
parameters,
|
||||
)
|
||||
|
||||
const queryStartTime = +new Date()
|
||||
|
||||
const statement = await databaseConnection.prepareAsync(query)
|
||||
try {
|
||||
const rawResult = await statement.executeAsync(parameters)
|
||||
|
||||
const maxQueryExecutionTime =
|
||||
this.driver.options.maxQueryExecutionTime
|
||||
const queryEndTime = +new Date()
|
||||
const queryExecutionTime = queryEndTime - queryStartTime
|
||||
|
||||
this.broadcaster.broadcastAfterQueryEvent(
|
||||
broadcasterResult,
|
||||
query,
|
||||
parameters,
|
||||
true,
|
||||
queryExecutionTime,
|
||||
rawResult,
|
||||
undefined,
|
||||
)
|
||||
await broadcasterResult.wait()
|
||||
|
||||
const queryStartTime = +new Date()
|
||||
// All Expo SQL queries are executed in a transaction context
|
||||
databaseConnection.transaction(
|
||||
async (transaction: ITransaction) => {
|
||||
if (typeof this.transaction === "undefined") {
|
||||
await this.startTransaction()
|
||||
this.transaction = transaction
|
||||
}
|
||||
this.transaction.executeSql(
|
||||
query,
|
||||
parameters,
|
||||
async (t: ITransaction, raw: IResultSet) => {
|
||||
// log slow queries if maxQueryExecution time is set
|
||||
const maxQueryExecutionTime =
|
||||
this.driver.options.maxQueryExecutionTime
|
||||
const queryEndTime = +new Date()
|
||||
const queryExecutionTime =
|
||||
queryEndTime - queryStartTime
|
||||
if (
|
||||
maxQueryExecutionTime &&
|
||||
queryExecutionTime > maxQueryExecutionTime
|
||||
) {
|
||||
this.driver.connection.logger.logQuerySlow(
|
||||
queryExecutionTime,
|
||||
query,
|
||||
parameters,
|
||||
this,
|
||||
)
|
||||
}
|
||||
|
||||
this.broadcaster.broadcastAfterQueryEvent(
|
||||
broadcasterResult,
|
||||
query,
|
||||
parameters,
|
||||
true,
|
||||
queryExecutionTime,
|
||||
raw,
|
||||
undefined,
|
||||
)
|
||||
await broadcasterResult.wait()
|
||||
const result = new QueryResult()
|
||||
result.affected = rawResult.changes
|
||||
result.records = await rawResult.getAllAsync()
|
||||
result.raw = query.startsWith("INSERT INTO")
|
||||
? rawResult.lastInsertRowId
|
||||
: result.records
|
||||
|
||||
if (
|
||||
maxQueryExecutionTime &&
|
||||
queryExecutionTime > maxQueryExecutionTime
|
||||
) {
|
||||
this.driver.connection.logger.logQuerySlow(
|
||||
queryExecutionTime,
|
||||
query,
|
||||
parameters,
|
||||
this,
|
||||
)
|
||||
}
|
||||
|
||||
const result = new QueryResult()
|
||||
|
||||
if (raw?.hasOwnProperty("rowsAffected")) {
|
||||
result.affected = raw.rowsAffected
|
||||
}
|
||||
|
||||
if (raw?.hasOwnProperty("rows")) {
|
||||
const resultSet = []
|
||||
for (let i = 0; i < raw.rows.length; i++) {
|
||||
resultSet.push(raw.rows.item(i))
|
||||
}
|
||||
|
||||
result.raw = resultSet
|
||||
result.records = resultSet
|
||||
}
|
||||
|
||||
// return id of inserted row, if query was insert statement.
|
||||
if (query.startsWith("INSERT INTO")) {
|
||||
result.raw = raw.insertId
|
||||
}
|
||||
|
||||
if (useStructuredResult) {
|
||||
ok(result)
|
||||
} else {
|
||||
ok(result.raw)
|
||||
}
|
||||
},
|
||||
async (t: ITransaction, err: any) => {
|
||||
this.driver.connection.logger.logQueryError(
|
||||
err,
|
||||
query,
|
||||
parameters,
|
||||
this,
|
||||
)
|
||||
this.broadcaster.broadcastAfterQueryEvent(
|
||||
broadcasterResult,
|
||||
query,
|
||||
parameters,
|
||||
false,
|
||||
undefined,
|
||||
undefined,
|
||||
err,
|
||||
)
|
||||
await broadcasterResult.wait()
|
||||
|
||||
fail(new QueryFailedError(query, parameters, err))
|
||||
},
|
||||
)
|
||||
},
|
||||
async (err: any) => {
|
||||
await this.rollbackTransaction()
|
||||
fail(err)
|
||||
},
|
||||
() => {
|
||||
this.isTransactionActive = false
|
||||
this.transaction = undefined
|
||||
},
|
||||
return useStructuredResult ? result : result.raw
|
||||
} catch (err) {
|
||||
this.driver.connection.logger.logQueryError(
|
||||
err,
|
||||
query,
|
||||
parameters,
|
||||
this,
|
||||
)
|
||||
})
|
||||
this.broadcaster.broadcastAfterQueryEvent(
|
||||
broadcasterResult,
|
||||
query,
|
||||
parameters,
|
||||
false,
|
||||
0,
|
||||
undefined,
|
||||
err,
|
||||
)
|
||||
await broadcasterResult.wait()
|
||||
|
||||
throw new QueryFailedError(query, parameters, err)
|
||||
} finally {
|
||||
await statement.finalizeAsync()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
93
src/driver/expo/legacy/ExpoLegacyDriver.ts
Normal file
93
src/driver/expo/legacy/ExpoLegacyDriver.ts
Normal file
@ -0,0 +1,93 @@
|
||||
import { AbstractSqliteDriver } from "../../sqlite-abstract/AbstractSqliteDriver"
|
||||
import { ExpoConnectionOptions } from "../ExpoConnectionOptions"
|
||||
import { ExpoLegacyQueryRunner } from "./ExpoLegacyQueryRunner"
|
||||
import { QueryRunner } from "../../../query-runner/QueryRunner"
|
||||
import { DataSource } from "../../../data-source/DataSource"
|
||||
import { ReplicationMode } from "../../types/ReplicationMode"
|
||||
|
||||
export class ExpoLegacyDriver extends AbstractSqliteDriver {
|
||||
options: ExpoConnectionOptions
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Constructor
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
constructor(connection: DataSource) {
|
||||
super(connection)
|
||||
|
||||
this.database = this.options.database
|
||||
|
||||
// load sqlite package
|
||||
this.sqlite = this.options.driver
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Public Methods
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Closes connection with database.
|
||||
*/
|
||||
async disconnect(): Promise<void> {
|
||||
return new Promise<void>((ok, fail) => {
|
||||
try {
|
||||
this.queryRunner = undefined
|
||||
this.databaseConnection._db.close()
|
||||
this.databaseConnection = undefined
|
||||
ok()
|
||||
} catch (error) {
|
||||
fail(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a query runner used to execute database queries.
|
||||
*/
|
||||
createQueryRunner(mode: ReplicationMode): QueryRunner {
|
||||
if (!this.queryRunner)
|
||||
this.queryRunner = new ExpoLegacyQueryRunner(this)
|
||||
|
||||
return this.queryRunner
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Protected Methods
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Creates connection with the database.
|
||||
*/
|
||||
protected createDatabaseConnection() {
|
||||
return new Promise<void>((ok, fail) => {
|
||||
try {
|
||||
const databaseConnection = this.sqlite.openDatabase(
|
||||
this.options.database,
|
||||
)
|
||||
/*
|
||||
// we need to enable foreign keys in sqlite to make sure all foreign key related features
|
||||
// working properly. this also makes onDelete work with sqlite.
|
||||
*/
|
||||
databaseConnection.transaction(
|
||||
(tsx: any) => {
|
||||
tsx.executeSql(
|
||||
`PRAGMA foreign_keys = ON`,
|
||||
[],
|
||||
(t: any, result: any) => {
|
||||
ok(databaseConnection)
|
||||
},
|
||||
(t: any, err: any) => {
|
||||
fail({ transaction: t, error: err })
|
||||
},
|
||||
)
|
||||
},
|
||||
(err: any) => {
|
||||
fail(err)
|
||||
},
|
||||
)
|
||||
} catch (error) {
|
||||
fail(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
279
src/driver/expo/legacy/ExpoLegacyQueryRunner.ts
Normal file
279
src/driver/expo/legacy/ExpoLegacyQueryRunner.ts
Normal file
@ -0,0 +1,279 @@
|
||||
import { QueryRunnerAlreadyReleasedError } from "../../../error/QueryRunnerAlreadyReleasedError"
|
||||
import { QueryFailedError } from "../../../error/QueryFailedError"
|
||||
import { AbstractSqliteQueryRunner } from "../../sqlite-abstract/AbstractSqliteQueryRunner"
|
||||
import { TransactionNotStartedError } from "../../../error/TransactionNotStartedError"
|
||||
import { ExpoLegacyDriver } from "./ExpoLegacyDriver"
|
||||
import { Broadcaster } from "../../../subscriber/Broadcaster"
|
||||
import { QueryResult } from "../../../query-runner/QueryResult"
|
||||
import { BroadcasterResult } from "../../../subscriber/BroadcasterResult"
|
||||
|
||||
// Needed to satisfy the Typescript compiler
|
||||
interface IResultSet {
|
||||
insertId: number | undefined
|
||||
rowsAffected: number
|
||||
rows: {
|
||||
length: number
|
||||
item: (idx: number) => any
|
||||
_array: any[]
|
||||
}
|
||||
}
|
||||
interface ITransaction {
|
||||
executeSql: (
|
||||
sql: string,
|
||||
args: any[] | undefined,
|
||||
ok: (tsx: ITransaction, resultSet: IResultSet) => void,
|
||||
fail: (tsx: ITransaction, err: any) => void,
|
||||
) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs queries on a single sqlite database connection.
|
||||
*/
|
||||
export class ExpoLegacyQueryRunner extends AbstractSqliteQueryRunner {
|
||||
/**
|
||||
* Database driver used by connection.
|
||||
*/
|
||||
driver: ExpoLegacyDriver
|
||||
|
||||
/**
|
||||
* Database transaction object
|
||||
*/
|
||||
private transaction?: ITransaction
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Constructor
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
constructor(driver: ExpoLegacyDriver) {
|
||||
super()
|
||||
this.driver = driver
|
||||
this.connection = driver.connection
|
||||
this.broadcaster = new Broadcaster(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts transaction. Within Expo, all database operations happen in a
|
||||
* transaction context, so issuing a `BEGIN TRANSACTION` command is
|
||||
* redundant and will result in the following error:
|
||||
*
|
||||
* `Error: Error code 1: cannot start a transaction within a transaction`
|
||||
*
|
||||
* Instead, we keep track of a `Transaction` object in `this.transaction`
|
||||
* and continue using the same object until we wish to commit the
|
||||
* transaction.
|
||||
*/
|
||||
async startTransaction(): Promise<void> {
|
||||
this.isTransactionActive = true
|
||||
try {
|
||||
await this.broadcaster.broadcast("BeforeTransactionStart")
|
||||
} catch (err) {
|
||||
this.isTransactionActive = false
|
||||
throw err
|
||||
}
|
||||
|
||||
this.transactionDepth += 1
|
||||
|
||||
await this.broadcaster.broadcast("AfterTransactionStart")
|
||||
}
|
||||
|
||||
/**
|
||||
* Commits transaction.
|
||||
* Error will be thrown if transaction was not started.
|
||||
* Since Expo will automatically commit the transaction once all the
|
||||
* callbacks of the transaction object have been completed, "committing" a
|
||||
* transaction in this driver's context means that we delete the transaction
|
||||
* object and set the stage for the next transaction.
|
||||
*/
|
||||
async commitTransaction(): Promise<void> {
|
||||
if (
|
||||
!this.isTransactionActive &&
|
||||
typeof this.transaction === "undefined"
|
||||
)
|
||||
throw new TransactionNotStartedError()
|
||||
|
||||
await this.broadcaster.broadcast("BeforeTransactionCommit")
|
||||
|
||||
this.transaction = undefined
|
||||
this.isTransactionActive = false
|
||||
|
||||
this.transactionDepth -= 1
|
||||
|
||||
await this.broadcaster.broadcast("AfterTransactionCommit")
|
||||
}
|
||||
|
||||
/**
|
||||
* Rollbacks transaction.
|
||||
* Error will be thrown if transaction was not started.
|
||||
* This method's functionality is identical to `commitTransaction()` because
|
||||
* the transaction lifecycle is handled within the Expo transaction object.
|
||||
* Issuing separate statements for `COMMIT` or `ROLLBACK` aren't necessary.
|
||||
*/
|
||||
async rollbackTransaction(): Promise<void> {
|
||||
if (
|
||||
!this.isTransactionActive &&
|
||||
typeof this.transaction === "undefined"
|
||||
)
|
||||
throw new TransactionNotStartedError()
|
||||
|
||||
await this.broadcaster.broadcast("BeforeTransactionRollback")
|
||||
|
||||
this.transaction = undefined
|
||||
this.isTransactionActive = false
|
||||
|
||||
this.transactionDepth -= 1
|
||||
|
||||
await this.broadcaster.broadcast("AfterTransactionRollback")
|
||||
}
|
||||
|
||||
/**
|
||||
* Called before migrations are run.
|
||||
*/
|
||||
async beforeMigration(): Promise<void> {
|
||||
const databaseConnection = await this.connect()
|
||||
return new Promise((ok, fail) => {
|
||||
databaseConnection.exec(
|
||||
[{ sql: "PRAGMA foreign_keys = OFF", args: [] }],
|
||||
false,
|
||||
(err: any) => (err ? fail(err) : ok()),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after migrations are run.
|
||||
*/
|
||||
async afterMigration(): Promise<void> {
|
||||
const databaseConnection = await this.connect()
|
||||
return new Promise((ok, fail) => {
|
||||
databaseConnection.exec(
|
||||
[{ sql: "PRAGMA foreign_keys = ON", args: [] }],
|
||||
false,
|
||||
(err: any) => (err ? fail(err) : ok()),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a given SQL query.
|
||||
*/
|
||||
async query(
|
||||
query: string,
|
||||
parameters?: any[],
|
||||
useStructuredResult = false,
|
||||
): Promise<any> {
|
||||
if (this.isReleased) throw new QueryRunnerAlreadyReleasedError()
|
||||
|
||||
return new Promise<any>(async (ok, fail) => {
|
||||
const databaseConnection = await this.connect()
|
||||
const broadcasterResult = new BroadcasterResult()
|
||||
|
||||
this.driver.connection.logger.logQuery(query, parameters, this)
|
||||
this.broadcaster.broadcastBeforeQueryEvent(
|
||||
broadcasterResult,
|
||||
query,
|
||||
parameters,
|
||||
)
|
||||
|
||||
const queryStartTime = +new Date()
|
||||
// All Expo SQL queries are executed in a transaction context
|
||||
databaseConnection.transaction(
|
||||
async (transaction: ITransaction) => {
|
||||
if (typeof this.transaction === "undefined") {
|
||||
await this.startTransaction()
|
||||
this.transaction = transaction
|
||||
}
|
||||
this.transaction.executeSql(
|
||||
query,
|
||||
parameters,
|
||||
async (t: ITransaction, raw: IResultSet) => {
|
||||
// log slow queries if maxQueryExecution time is set
|
||||
const maxQueryExecutionTime =
|
||||
this.driver.options.maxQueryExecutionTime
|
||||
const queryEndTime = +new Date()
|
||||
const queryExecutionTime =
|
||||
queryEndTime - queryStartTime
|
||||
|
||||
this.broadcaster.broadcastAfterQueryEvent(
|
||||
broadcasterResult,
|
||||
query,
|
||||
parameters,
|
||||
true,
|
||||
queryExecutionTime,
|
||||
raw,
|
||||
undefined,
|
||||
)
|
||||
await broadcasterResult.wait()
|
||||
|
||||
if (
|
||||
maxQueryExecutionTime &&
|
||||
queryExecutionTime > maxQueryExecutionTime
|
||||
) {
|
||||
this.driver.connection.logger.logQuerySlow(
|
||||
queryExecutionTime,
|
||||
query,
|
||||
parameters,
|
||||
this,
|
||||
)
|
||||
}
|
||||
|
||||
const result = new QueryResult()
|
||||
|
||||
if (raw?.hasOwnProperty("rowsAffected")) {
|
||||
result.affected = raw.rowsAffected
|
||||
}
|
||||
|
||||
if (raw?.hasOwnProperty("rows")) {
|
||||
let resultSet = []
|
||||
for (let i = 0; i < raw.rows.length; i++) {
|
||||
resultSet.push(raw.rows.item(i))
|
||||
}
|
||||
|
||||
result.raw = resultSet
|
||||
result.records = resultSet
|
||||
}
|
||||
|
||||
// return id of inserted row, if query was insert statement.
|
||||
if (query.startsWith("INSERT INTO")) {
|
||||
result.raw = raw.insertId
|
||||
}
|
||||
|
||||
if (useStructuredResult) {
|
||||
ok(result)
|
||||
} else {
|
||||
ok(result.raw)
|
||||
}
|
||||
},
|
||||
async (t: ITransaction, err: any) => {
|
||||
this.driver.connection.logger.logQueryError(
|
||||
err,
|
||||
query,
|
||||
parameters,
|
||||
this,
|
||||
)
|
||||
this.broadcaster.broadcastAfterQueryEvent(
|
||||
broadcasterResult,
|
||||
query,
|
||||
parameters,
|
||||
false,
|
||||
undefined,
|
||||
undefined,
|
||||
err,
|
||||
)
|
||||
await broadcasterResult.wait()
|
||||
|
||||
fail(new QueryFailedError(query, parameters, err))
|
||||
},
|
||||
)
|
||||
},
|
||||
async (err: any) => {
|
||||
await this.rollbackTransaction()
|
||||
fail(err)
|
||||
},
|
||||
() => {
|
||||
this.isTransactionActive = false
|
||||
this.transaction = undefined
|
||||
},
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user