Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/docs/data-source/1-data-source.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ establishes the initial database connection or connection pool depending on the

To establish the initial connection/connection pool, you must call the `initialize` method of your `DataSource` instance.

`initialize` must only be called once: calling it again while a previous call is still in progress, or after it has
succeeded, rejects with a `CannotConnectAlreadyConnectedError`. If initialization fails, the partially established
connection is cleaned up and `initialize` can safely be called again.

Disconnection (closing all connections in the pool) occurs when the `destroy` method is called.

Generally, you call the `initialize` method of the `DataSource` instance on the application bootstrap,
Expand Down
85 changes: 55 additions & 30 deletions packages/typeorm/src/data-source/DataSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ export class DataSource {

readonly relationIdLoader: RelationIdLoader

private isInitializing = false

// -------------------------------------------------------------------------
// Constructor
// -------------------------------------------------------------------------
Expand Down Expand Up @@ -236,45 +238,68 @@ export class DataSource {
* but it also can setup a connection pool with database to use.
*/
async initialize(): Promise<this> {
if (this.isInitialized) throw new CannotConnectAlreadyConnectedError()
if (this.isInitialized || this.isInitializing)
throw new CannotConnectAlreadyConnectedError()

// validate isolationLevel before connecting
validateIsolationLevel(
this.driver.supportedIsolationLevels,
this.options.isolationLevel,
)
this.isInitializing = true
try {
// validate isolationLevel before connecting
validateIsolationLevel(
this.driver.supportedIsolationLevels,
this.options.isolationLevel,
)

// connect to the database via its driver
await this.driver.connect()
// connect to the database via its driver
await this.driver.connect()

// connect to the cache-specific database if cache is enabled
if (this.queryResultCache) await this.queryResultCache.connect()
try {
// connect to the cache-specific database if cache is enabled
if (this.queryResultCache) await this.queryResultCache.connect()

// set connected status for the current connection
ObjectUtils.assign(this, { isInitialized: true })
// set connected status for the current connection
ObjectUtils.assign(this, { isInitialized: true })

try {
// build all metadatas registered in the current connection
await this.buildMetadatas()
// build all metadatas registered in the current connection
await this.buildMetadatas()

await this.driver.afterConnect()
await this.driver.afterConnect()

// if option is set - drop schema once connection is done
if (this.options.dropSchema) await this.dropDatabase()
// if option is set - drop schema once connection is done
if (this.options.dropSchema) await this.dropDatabase()

// if option is set - automatically synchronize a schema
if (this.options.migrationsRun)
await this.runMigrations({
transaction: this.options.migrationsTransactionMode,
})
// if option is set - automatically synchronize a schema
if (this.options.migrationsRun)
await this.runMigrations({
transaction: this.options.migrationsTransactionMode,
})

// if option is set - automatically synchronize a schema
if (this.options.synchronize) await this.synchronize()
} catch (error) {
// if for some reason build metadata fail (for example validation error during entity metadata check)
// connection needs to be closed
await this.destroy()
throw error
// if option is set - automatically synchronize a schema
if (this.options.synchronize) await this.synchronize()
} catch (error) {
// if for some reason build metadata fail (for example validation error during entity metadata check)
// connection needs to be closed
if (this.isInitialized) {
await this.destroy()
} else {
// clean up whatever partially connected, without letting
// a cleanup failure mask the original error
if (this.queryResultCache) {
try {
await this.queryResultCache.disconnect()
} catch {
// ignore
}
}
try {
await this.driver.disconnect()
} catch {
// ignore
}
}
throw error
}
} finally {
this.isInitializing = false
}

return this
Expand Down
107 changes: 107 additions & 0 deletions packages/typeorm/test/functional/data-source/data-source.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { expect } from "chai"
import "reflect-metadata"
import sinon from "sinon"
import {
CannotConnectAlreadyConnectedError,
CannotExecuteNotConnectedError,
} from "../../../src"
import type { QueryResultCache } from "../../../src/cache/QueryResultCache"
import { DataSource } from "../../../src/data-source/DataSource"
import type { PostgresDataSourceOptions } from "../../../src/driver/postgres/PostgresDataSourceOptions"
import { EntityManager } from "../../../src/entity-manager/EntityManager"
Expand All @@ -30,6 +32,111 @@ import { SiteLocation } from "./entity/SiteLocation"
import { Site } from "./entity/Site"

describe("DataSource", () => {
// regression test for github issue #12705
describe("when cache initialization fails", () => {
it("should disconnect the database driver", async () => {
const cacheConnectionError = new Error(
"query cache connection failed",
)
const failingCacheProvider: QueryResultCache = {
connect() {
return Promise.reject(cacheConnectionError)
},
async disconnect() {},
async synchronize() {},
getFromCache() {
return Promise.resolve(undefined)
},
async storeInCache() {},
isExpired() {
return false
},
async clear() {},
async remove() {},
}
const dataSource = new DataSource({
type: "better-sqlite3",
database: ":memory:",
cache: {
provider: () => failingCacheProvider,
},
})
const disconnectSpy = sinon.spy(dataSource.driver, "disconnect")
const cacheDisconnectSpy = sinon.spy(
failingCacheProvider,
"disconnect",
)

try {
await expect(dataSource.initialize()).to.be.rejectedWith(
cacheConnectionError,
)
expect(disconnectSpy).to.have.been.calledOnce
expect(cacheDisconnectSpy).to.have.been.calledOnce
expect(dataSource.isInitialized).to.be.false
} finally {
if (disconnectSpy.notCalled) {
await dataSource.driver.disconnect()
}
}
})
})

// regression test for github issue #12705
describe("while initialization is in progress", () => {
it("should reject another initialization attempt", async () => {
const dataSource = new DataSource({
type: "better-sqlite3",
database: ":memory:",
})
const originalConnect = dataSource.driver.connect.bind(
dataSource.driver,
)
let allowConnection!: () => void
const connectionGate = new Promise<void>((resolve) => {
allowConnection = resolve
})
let reportConnectionStarted!: () => void
const connectionStarted = new Promise<void>((resolve) => {
reportConnectionStarted = resolve
})
const connectStub = sinon.stub(dataSource.driver, "connect")
connectStub.onFirstCall().callsFake(async () => {
reportConnectionStarted()
await connectionGate
await originalConnect()
})
connectStub
.onSecondCall()
.rejects(new Error("driver connect called more than once"))

const firstInitialization = dataSource.initialize()
await connectionStarted
const secondInitialization = dataSource.initialize()
allowConnection()

try {
// assert the rejection first so a handler is attached to the
// already-rejected promise within the same tick it was created
await expect(secondInitialization).to.be.rejectedWith(
CannotConnectAlreadyConnectedError,
)
await expect(firstInitialization).to.be.fulfilled
expect(connectStub).to.have.been.calledOnce
} finally {
if (dataSource.isInitialized) {
await dataSource.destroy()
} else {
try {
await dataSource.driver.disconnect()
} catch {
// the driver may never have connected
}
}
}
})
})

describe("before connection is established", () => {
let dataSource: DataSource
before(() => {
Expand Down
Loading