@mikro-orm/core
Version:
TypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, PostgreSQL and SQLite databases as well as usage with vanilla JavaScript.
237 lines (236 loc) • 10.2 kB
JavaScript
import { Utils } from '../utils/Utils.js';
import { ScalarReference } from '../entity/Reference.js';
/** Abstract base class for database connections, providing transaction and query execution support. */
export class Connection {
config;
type;
metadata;
platform;
options;
logger;
connected = false;
get #connectionLabel() {
return {
type: this.type,
name: this.options.name || this.config.get('name') || this.options.host || this.options.dbName,
};
}
constructor(config, options, type = 'write') {
this.config = config;
this.type = type;
this.logger = this.config.getLogger();
this.platform = this.config.getPlatform();
if (options) {
this.options = Utils.copy(options);
}
else {
const props = [
'dbName',
'clientUrl',
'host',
'port',
'user',
'password',
'multipleStatements',
'pool',
'schema',
'driverOptions',
];
this.options = props.reduce((o, i) => {
o[i] = this.config.get(i);
return o;
}, {});
}
}
/**
* Closes the database connection (aka disconnect)
*/
async close(force) {
Object.keys(this.options)
.filter(k => k !== 'name')
.forEach(k => delete this.options[k]);
}
/**
* Ensure the connection exists, this is used to support lazy connect when using `new MikroORM()` instead of the async `init` method.
*/
async ensureConnection() {
if (!this.connected) {
await this.connect();
}
}
/**
* Execute raw SQL queries, handy from running schema dump loaded from a file.
* This method doesn't support transactions, as opposed to `orm.schema.execute()`, which is used internally.
*/
async executeDump(dump) {
throw new Error(`Executing SQL dumps is not supported by current driver`);
}
async onConnect() {
const schemaGenerator = this.config.getExtension('@mikro-orm/schema-generator');
if (this.type === 'write' && schemaGenerator) {
if (this.config.get('ensureDatabase')) {
const options = this.config.get('ensureDatabase');
await schemaGenerator.ensureDatabase(typeof options === 'boolean' ? {} : { ...options, forceCheck: true });
}
if (this.config.get('ensureIndexes')) {
await schemaGenerator.ensureIndexes();
}
}
}
/** Executes a callback inside a transaction, committing on success and rolling back on failure. */
async transactional(cb, options) {
throw new Error(`Transactions are not supported by current driver`);
}
/** Begins a new database transaction and returns the transaction context. */
async begin(options) {
throw new Error(`Transactions are not supported by current driver`);
}
/** Commits the given transaction. */
async commit(ctx, eventBroadcaster, loggerContext) {
throw new Error(`Transactions are not supported by current driver`);
}
/** Rolls back the given transaction. */
async rollback(ctx, eventBroadcaster, loggerContext) {
throw new Error(`Transactions are not supported by current driver`);
}
/** @internal — public callers go through {@link EntityManager.callRoutine}. */
async callRoutine(routine, args, ctx) {
throw new Error(`Stored routines are not supported by the current driver`);
}
/**
* Unwraps a routine argument (resolving any `ScalarReference` wrapper) and, when the param
* declares a `customType`, marshals it through `convertToDatabaseValue`. `undefined` is
* normalised to `null` so every driver sees the same shape.
*
* @internal
*/
convertRoutineInbound(value, param) {
const resolved = value instanceof ScalarReference ? value.unwrap() : value;
const coerced = resolved === undefined ? null : resolved;
if (coerced === null || !param?.customType) {
return coerced;
}
return param.customType.convertToDatabaseValue(coerced, this.platform);
}
/**
* Converts a raw database value to its JS representation via the supplied `customType`, when
* one is declared. Used to marshal scalar function returns and OUT/INOUT values back to the
* caller before they land in a `ScalarReference` or `em.callRoutine`'s return value.
*
* @internal
*/
convertRoutineOutbound(value, customType) {
if (value === null || value === undefined || !customType) {
return value;
}
return customType.convertToJSValue(value, this.platform);
}
/**
* Executes a scalar function routine as `select <qualified>(?, ?, ...) as value`, marshalling
* IN params on the way in and the return value on the way out. The qualified name is built
* from `routine.schema`, falling back to the platform's default schema (e.g. `dbo` on MSSQL),
* which gives MySQL/SQLite a bare name and MSSQL/Oracle the mandatory `schema.name` form.
*
* @internal
*/
async callRoutineFunction(routine, args, ctx) {
const schema = routine.schema ?? this.platform.getDefaultSchemaName();
const qualified = (schema ? `${this.platform.quoteIdentifier(schema)}.` : '') + this.platform.quoteIdentifier(routine.name);
const placeholders = routine.params.map(() => '?').join(', ');
const positional = routine.params.map(p => this.convertRoutineInbound(args[p.name], p));
const rows = (await this.execute(`select ${qualified}(${placeholders}) as value`, positional, 'all', ctx));
return this.convertRoutineOutbound(rows[0]?.value, routine.returnCustomType);
}
/**
* Walks a result row produced by an OUT/INOUT-param SELECT and writes each value into the
* caller's `ScalarReference` slot. Non-reference args are ignored (the user opted out of
* receiving the OUT value).
*
* @internal
*/
applyRoutineOutParams(row, outParams, args) {
for (const param of outParams) {
const ref = args[param.name];
if (ref instanceof ScalarReference) {
ref.set(this.convertRoutineOutbound(row[param.name], param.customType));
}
}
}
/** Parses and returns the resolved connection configuration (host, port, user, etc.). */
getConnectionOptions() {
const ret = {};
if (this.options.clientUrl) {
const url = new URL(this.options.clientUrl);
this.options.host = ret.host = this.options.host ?? decodeURIComponent(url.hostname);
this.options.port = ret.port = this.options.port ?? +url.port;
this.options.user = ret.user = this.options.user ?? decodeURIComponent(url.username);
this.options.password = ret.password = this.options.password ?? decodeURIComponent(url.password);
this.options.dbName = ret.database = this.options.dbName ?? decodeURIComponent(url.pathname).replace(/^\//, '');
if (this.options.schema || url.searchParams.has('schema')) {
this.options.schema = ret.schema = this.options.schema ?? decodeURIComponent(url.searchParams.get('schema'));
this.config.set('schema', ret.schema);
}
}
else {
const url = new URL(this.config.get('clientUrl'));
this.options.host = ret.host = this.options.host ?? this.config.get('host', decodeURIComponent(url.hostname));
this.options.port = ret.port = this.options.port ?? this.config.get('port', +url.port);
this.options.user = ret.user = this.options.user ?? this.config.get('user', decodeURIComponent(url.username));
this.options.password = ret.password =
this.options.password ?? this.config.get('password', decodeURIComponent(url.password));
this.options.dbName = ret.database =
this.options.dbName ?? this.config.get('dbName', decodeURIComponent(url.pathname).replace(/^\//, ''));
}
return ret;
}
/** Sets the metadata storage on this connection. */
setMetadata(metadata) {
this.metadata = metadata;
}
/** Sets the platform abstraction on this connection. */
setPlatform(platform) {
this.platform = platform;
}
/** Returns the platform abstraction for this connection. */
getPlatform() {
return this.platform;
}
async executeQuery(query, cb, context) {
const now = Date.now();
try {
const res = await cb();
const took = Date.now() - now;
const results = Array.isArray(res) ? res.length : undefined;
const affected = Utils.isPlainObject(res) ? res.affectedRows : undefined;
this.logQuery(query, { ...context, took, results, affected });
return res;
}
catch (e) {
const took = Date.now() - now;
this.logQuery(query, { ...context, took, level: 'error' });
throw e;
}
}
logQuery(query, context = {}) {
const connection = this.#connectionLabel;
this.logger.logQuery({
level: 'info',
connection,
...context,
query,
});
const threshold = this.config.get('slowQueryThreshold');
if (threshold != null && (context.took ?? 0) >= threshold) {
this.config.getSlowQueryLogger().logQuery({
...context,
// `enabled: true` bypasses the debug-mode check in isEnabled(),
// ensuring slow query logs are always emitted regardless of the `debug` setting.
enabled: true,
level: context.level ?? 'warning',
namespace: 'slow-query',
connection,
query,
});
}
}
}