typeorm
Version:
Data-Mapper ORM for TypeScript and ES2021+. Supports MySQL/MariaDB, PostgreSQL, MS SQL Server, Oracle, SAP HANA, SQLite, MongoDB databases.
514 lines (512 loc) • 22.5 kB
JavaScript
import { registerQueryBuilders } from "../query-builder";
import { DefaultNamingStrategy } from "../naming-strategy/DefaultNamingStrategy";
import { CannotConnectAlreadyConnectedError, CannotExecuteNotConnectedError, EntityMetadataNotFoundError, QueryRunnerProviderAlreadyReleasedError, TypeORMError, } from "../error";
import { MigrationExecutor } from "../migration/MigrationExecutor";
import { EntityMetadataValidator } from "../metadata-builder/EntityMetadataValidator";
import { EntityManagerFactory } from "../entity-manager/EntityManagerFactory";
import { DriverFactory } from "../driver/DriverFactory";
import { ConnectionMetadataBuilder } from "../connection/ConnectionMetadataBuilder";
import { SelectQueryBuilder } from "../query-builder/SelectQueryBuilder";
import { LoggerFactory } from "../logger/LoggerFactory";
import { QueryResultCacheFactory } from "../cache/QueryResultCacheFactory";
import { RelationLoader } from "../query-builder/RelationLoader";
import { ObjectUtils } from "../util/ObjectUtils";
import { RelationIdLoader } from "../query-builder/RelationIdLoader";
import { DriverUtils } from "../driver/DriverUtils";
import { InstanceChecker } from "../util/InstanceChecker";
import { buildSqlTag } from "../util/SqlTagUtils";
registerQueryBuilders();
/**
* DataSource is a pre-defined connection configuration to a specific database.
* You can have multiple data sources connected (with multiple connections in it),
* connected to multiple databases in your application.
*
* Before, it was called `Connection`, but now `Connection` is deprecated
* because `Connection` isn't the best name for what it's actually is.
*/
export class DataSource {
// -------------------------------------------------------------------------
// Constructor
// -------------------------------------------------------------------------
constructor(options) {
this["@instanceof"] = Symbol.for("DataSource");
/**
* Migration instances that are registered for this connection.
*/
this.migrations = [];
/**
* Entity subscriber instances that are registered for this connection.
*/
this.subscribers = [];
/**
* All entity metadatas that are registered for this connection.
*/
this.entityMetadatas = [];
/**
* All entity metadatas that are registered for this connection.
* This is a copy of #.entityMetadatas property -> used for more performant searches.
*/
this.entityMetadatasMap = new Map();
registerQueryBuilders();
this.name = options.name || "default";
this.options = options;
this.logger = new LoggerFactory().create(this.options.logger, this.options.logging);
this.driver = new DriverFactory().create(this);
this.manager = this.createEntityManager();
this.namingStrategy =
options.namingStrategy || new DefaultNamingStrategy();
this.metadataTableName = options.metadataTableName || "typeorm_metadata";
this.queryResultCache = options.cache
? new QueryResultCacheFactory(this).create()
: undefined;
this.relationLoader = new RelationLoader(this);
this.relationIdLoader = new RelationIdLoader(this);
this.isInitialized = false;
}
// -------------------------------------------------------------------------
// Public Accessors
// -------------------------------------------------------------------------
/**
Indicates if DataSource is initialized or not.
*
* @deprecated use .isInitialized instead
*/
get isConnected() {
return this.isInitialized;
}
/**
* Gets the mongodb entity manager that allows to perform mongodb-specific repository operations
* with any entity in this connection.
*
* Available only in mongodb connections.
*/
get mongoManager() {
if (!InstanceChecker.isMongoEntityManager(this.manager))
throw new TypeORMError(`MongoEntityManager is only available for MongoDB databases.`);
return this.manager;
}
/**
* Gets a sql.js specific Entity Manager that allows to perform special load and save operations
*
* Available only in connection with the sqljs driver.
*/
get sqljsManager() {
if (!InstanceChecker.isSqljsEntityManager(this.manager))
throw new TypeORMError(`SqljsEntityManager is only available for Sqljs databases.`);
return this.manager;
}
// -------------------------------------------------------------------------
// Public Methods
// -------------------------------------------------------------------------
/**
* Updates current connection options with provided options.
*/
setOptions(options) {
Object.assign(this.options, options);
if (options.logger || options.logging) {
this.logger = new LoggerFactory().create(options.logger || this.options.logger, options.logging || this.options.logging);
}
if (options.namingStrategy) {
this.namingStrategy = options.namingStrategy;
}
if (options.cache) {
this.queryResultCache = new QueryResultCacheFactory(this).create();
}
// todo: we must update the database in the driver as well, if it was set by setOptions method
// in the future we need to refactor the code and remove "database" from the driver, and instead
// use database (and options) from a single place - data source.
if (options.database) {
this.driver.database = DriverUtils.buildDriverOptions(this.options).database;
}
// todo: need to take a look if we need to update schema and other "poor" properties
return this;
}
/**
* Performs connection to the database.
* This method should be called once on application bootstrap.
* This method not necessarily creates database connection (depend on database type),
* but it also can setup a connection pool with database to use.
*/
async initialize() {
if (this.isInitialized)
throw new CannotConnectAlreadyConnectedError(this.name);
// 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();
// set connected status for the current connection
ObjectUtils.assign(this, { isInitialized: true });
try {
// build all metadatas registered in the current connection
await this.buildMetadatas();
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 - 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;
}
return this;
}
/**
* Performs connection to the database.
* This method should be called once on application bootstrap.
* This method not necessarily creates database connection (depend on database type),
* but it also can setup a connection pool with database to use.
*
* @deprecated use .initialize method instead
*/
async connect() {
return this.initialize();
}
/**
* Closes connection with the database.
* Once connection is closed, you cannot use repositories or perform any operations except opening connection again.
*/
async destroy() {
if (!this.isInitialized)
throw new CannotExecuteNotConnectedError(this.name);
await this.driver.disconnect();
// disconnect from the cache-specific database if cache was enabled
if (this.queryResultCache)
await this.queryResultCache.disconnect();
ObjectUtils.assign(this, { isInitialized: false });
}
/**
* Closes connection with the database.
* Once connection is closed, you cannot use repositories or perform any operations except opening connection again.
*
* @deprecated use .destroy method instead
*/
async close() {
return this.destroy();
}
/**
* Creates database schema for all entities registered in this connection.
* Can be used only after connection to the database is established.
*
* @param dropBeforeSync If set to true then it drops the database with all its tables and data
*/
async synchronize(dropBeforeSync = false) {
if (!this.isInitialized)
throw new CannotExecuteNotConnectedError(this.name);
if (dropBeforeSync)
await this.dropDatabase();
const schemaBuilder = this.driver.createSchemaBuilder();
await schemaBuilder.build();
}
/**
* Drops the database and all its data.
* Be careful with this method on production since this method will erase all your database tables and their data.
* Can be used only after connection to the database is established.
*/
// TODO rename
async dropDatabase() {
const queryRunner = this.createQueryRunner();
try {
if (this.driver.options.type === "mssql" ||
DriverUtils.isMySQLFamily(this.driver) ||
this.driver.options.type === "aurora-mysql" ||
DriverUtils.isSQLiteFamily(this.driver)) {
const databases = [];
this.entityMetadatas.forEach((metadata) => {
if (metadata.database &&
databases.indexOf(metadata.database) === -1)
databases.push(metadata.database);
});
if (databases.length === 0 && this.driver.database) {
databases.push(this.driver.database);
}
if (databases.length === 0) {
await queryRunner.clearDatabase();
}
else {
for (const database of databases) {
await queryRunner.clearDatabase(database);
}
}
}
else {
await queryRunner.clearDatabase();
}
}
finally {
await queryRunner.release();
}
}
/**
* Runs all pending migrations.
* Can be used only after connection to the database is established.
*/
async runMigrations(options) {
if (!this.isInitialized)
throw new CannotExecuteNotConnectedError(this.name);
const migrationExecutor = new MigrationExecutor(this);
migrationExecutor.transaction =
options?.transaction ||
this.options?.migrationsTransactionMode ||
"all";
migrationExecutor.fake = (options && options.fake) || false;
const successMigrations = await migrationExecutor.executePendingMigrations();
return successMigrations;
}
/**
* Reverts last executed migration.
* Can be used only after connection to the database is established.
*/
async undoLastMigration(options) {
if (!this.isInitialized)
throw new CannotExecuteNotConnectedError(this.name);
const migrationExecutor = new MigrationExecutor(this);
migrationExecutor.transaction =
(options && options.transaction) || "all";
migrationExecutor.fake = (options && options.fake) || false;
await migrationExecutor.undoLastMigration();
}
/**
* Lists all migrations and whether they have been run.
* Returns true if there are pending migrations
*/
async showMigrations() {
if (!this.isInitialized) {
throw new CannotExecuteNotConnectedError(this.name);
}
const migrationExecutor = new MigrationExecutor(this);
return await migrationExecutor.showMigrations();
}
/**
* Checks if entity metadata exist for the given entity class, target name or table name.
*/
hasMetadata(target) {
return !!this.findMetadata(target);
}
/**
* Gets entity metadata for the given entity class or schema name.
*/
getMetadata(target) {
const metadata = this.findMetadata(target);
if (!metadata)
throw new EntityMetadataNotFoundError(target);
return metadata;
}
/**
* Gets repository for the given entity.
*/
getRepository(target) {
return this.manager.getRepository(target);
}
/**
* Gets tree repository for the given entity class or name.
* Only tree-type entities can have a TreeRepository, like ones decorated with @Tree decorator.
*/
getTreeRepository(target) {
return this.manager.getTreeRepository(target);
}
/**
* Gets mongodb-specific repository for the given entity class or name.
* Works only if connection is mongodb-specific.
*/
getMongoRepository(target) {
if (!(this.driver.options.type === "mongodb"))
throw new TypeORMError(`You can use getMongoRepository only for MongoDB connections.`);
return this.manager.getRepository(target);
}
/**
* Gets custom entity repository marked with @EntityRepository decorator.
*
* @deprecated use Repository.extend function to create a custom repository
*/
getCustomRepository(customRepository) {
return this.manager.getCustomRepository(customRepository);
}
async transaction(isolationOrRunInTransaction, runInTransactionParam) {
return this.manager.transaction(isolationOrRunInTransaction, runInTransactionParam);
}
/**
* Executes raw SQL query and returns raw database results.
*
* @see [Official docs](https://typeorm.io/data-source-api) for examples.
*/
async query(query, parameters, queryRunner) {
if (InstanceChecker.isMongoEntityManager(this.manager))
throw new TypeORMError(`Queries aren't supported by MongoDB.`);
if (queryRunner && queryRunner.isReleased)
throw new QueryRunnerProviderAlreadyReleasedError();
const usedQueryRunner = queryRunner || this.createQueryRunner();
try {
return await usedQueryRunner.query(query, parameters); // await is needed here because we are using finally
}
finally {
if (!queryRunner)
await usedQueryRunner.release();
}
}
/**
* Tagged template function that executes raw SQL query and returns raw database results.
* Template expressions are automatically transformed into database parameters.
* Raw query execution is supported only by relational databases (MongoDB is not supported).
* Note: Don't call this as a regular function, it is meant to be used with backticks to tag a template literal.
* Example: dataSource.sql`SELECT * FROM table_name WHERE id = ${id}`
*/
async sql(strings, ...values) {
const { query, parameters } = buildSqlTag({
driver: this.driver,
strings: strings,
expressions: values,
});
return await this.query(query, parameters);
}
/**
* Creates a new query builder that can be used to build a SQL query.
*/
createQueryBuilder(entityOrRunner, alias, queryRunner) {
if (InstanceChecker.isMongoEntityManager(this.manager))
throw new TypeORMError(`Query Builder is not supported by MongoDB.`);
if (alias) {
alias = DriverUtils.buildAlias(this.driver, undefined, alias);
const metadata = this.getMetadata(entityOrRunner);
return new SelectQueryBuilder(this, queryRunner)
.select(alias)
.from(metadata.target, alias);
}
else {
return new SelectQueryBuilder(this, entityOrRunner);
}
}
/**
* Creates a query runner used for perform queries on a single database connection.
* Using query runners you can control your queries to execute using single database connection and
* manually control your database transaction.
*
* Mode is used in replication mode and indicates whatever you want to connect
* to master database or any of slave databases.
* If you perform writes you must use master database,
* if you perform reads you can use slave databases.
*/
createQueryRunner(mode = "master") {
const queryRunner = this.driver.createQueryRunner(mode);
const manager = this.createEntityManager(queryRunner);
Object.assign(queryRunner, { manager: manager });
return queryRunner;
}
/**
* Gets entity metadata of the junction table (many-to-many table).
*/
getManyToManyMetadata(entityTarget, relationPropertyPath) {
const relationMetadata = this.getMetadata(entityTarget).findRelationWithPropertyPath(relationPropertyPath);
if (!relationMetadata)
throw new TypeORMError(`Relation "${relationPropertyPath}" was not found in ${entityTarget} entity.`);
if (!relationMetadata.isManyToMany)
throw new TypeORMError(`Relation "${entityTarget}#${relationPropertyPath}" does not have a many-to-many relationship.` +
`You can use this method only on many-to-many relations.`);
return relationMetadata.junctionEntityMetadata;
}
/**
* Creates an Entity Manager for the current connection with the help of the EntityManagerFactory.
*/
createEntityManager(queryRunner) {
return new EntityManagerFactory().create(this, queryRunner);
}
// -------------------------------------------------------------------------
// Protected Methods
// -------------------------------------------------------------------------
/**
* Finds exist entity metadata by the given entity class, target name or table name.
*/
findMetadata(target) {
const metadataFromMap = this.entityMetadatasMap.get(target);
if (metadataFromMap)
return metadataFromMap;
for (const [_, metadata] of this.entityMetadatasMap) {
if (InstanceChecker.isEntitySchema(target) &&
metadata.name === target.options.name) {
return metadata;
}
if (typeof target === "string") {
if (target.indexOf(".") !== -1) {
if (metadata.tablePath === target) {
return metadata;
}
}
else {
if (metadata.name === target ||
metadata.tableName === target) {
return metadata;
}
}
}
if (ObjectUtils.isObjectWithName(target) &&
typeof target.name === "string") {
if (target.name.indexOf(".") !== -1) {
if (metadata.tablePath === target.name) {
return metadata;
}
}
else {
if (metadata.name === target.name ||
metadata.tableName === target.name) {
return metadata;
}
}
}
}
return undefined;
}
/**
* Builds metadatas for all registered classes inside this connection.
*/
async buildMetadatas() {
const connectionMetadataBuilder = new ConnectionMetadataBuilder(this);
const entityMetadataValidator = new EntityMetadataValidator();
// create subscribers instances if they are not disallowed from high-level (for example they can disallowed from migrations run process)
const flattenedSubscribers = ObjectUtils.mixedListToArray(this.options.subscribers || []);
const subscribers = await connectionMetadataBuilder.buildSubscribers(flattenedSubscribers);
ObjectUtils.assign(this, { subscribers: subscribers });
// build entity metadatas
const flattenedEntities = ObjectUtils.mixedListToArray(this.options.entities || []);
const entityMetadatas = await connectionMetadataBuilder.buildEntityMetadatas(flattenedEntities);
ObjectUtils.assign(this, {
entityMetadatas: entityMetadatas,
entityMetadatasMap: new Map(entityMetadatas.map((metadata) => [metadata.target, metadata])),
});
// create migration instances
const flattenedMigrations = ObjectUtils.mixedListToArray(this.options.migrations || []);
const migrations = await connectionMetadataBuilder.buildMigrations(flattenedMigrations);
ObjectUtils.assign(this, { migrations: migrations });
// validate all created entity metadatas to make sure user created entities are valid and correct
entityMetadataValidator.validateMany(this.entityMetadatas.filter((metadata) => metadata.tableType !== "view"), this.driver);
// set current data source to the entities
for (const entityMetadata of entityMetadatas) {
if (InstanceChecker.isBaseEntityConstructor(entityMetadata.target)) {
entityMetadata.target.useDataSource(this);
}
}
}
/**
* Get the replication mode SELECT queries should use for this datasource by default
*/
defaultReplicationModeForReads() {
if ("replication" in this.driver.options &&
this.driver.options.replication) {
const value = this.driver.options.replication.defaultMode;
if (value) {
return value;
}
}
return "slave";
}
}
//# sourceMappingURL=DataSource.js.map