@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.
437 lines (436 loc) • 16.8 kB
JavaScript
import { NullCacheAdapter } from '../cache/NullCacheAdapter.js';
import { ObjectHydrator } from '../hydration/ObjectHydrator.js';
import { NullHighlighter } from '../utils/NullHighlighter.js';
import { DefaultLogger } from '../logging/DefaultLogger.js';
import { colors } from '../logging/colors.js';
import { Utils } from '../utils/Utils.js';
import { Routine } from '../metadata/Routine.js';
import { MetadataValidator } from '../metadata/MetadataValidator.js';
import { MetadataProvider } from '../metadata/MetadataProvider.js';
import { NotFoundError } from '../errors.js';
import { RequestContext } from './RequestContext.js';
import { DataloaderType, FlushMode, LoadStrategy, PopulateHint } from '../enums.js';
import { MemoryCacheAdapter } from '../cache/MemoryCacheAdapter.js';
import { EntityComparator } from './EntityComparator.js';
import { setEnv } from './env-vars.js';
const DEFAULTS = {
pool: {},
entities: [],
entitiesTs: [],
extensions: [],
subscribers: [],
routines: [],
filters: {},
discovery: {
warnWhenNoEntities: true,
checkDuplicateTableNames: true,
checkDuplicateFieldNames: true,
checkDuplicateEntities: true,
checkNonPersistentCompositeProps: true,
inferDefaultValues: true,
},
validateRequired: true,
context: (name) => RequestContext.getEntityManager(name),
contextName: 'default',
allowGlobalContext: false,
// eslint-disable-next-line no-console
logger: console.log.bind(console),
colors: true,
findOneOrFailHandler: (entityName, where) => NotFoundError.findOneFailed(entityName, where),
findExactlyOneOrFailHandler: (entityName, where) => NotFoundError.findExactlyOneFailed(entityName, where),
baseDir: globalThis.process?.cwd?.(),
hydrator: ObjectHydrator,
flushMode: FlushMode.AUTO,
loadStrategy: LoadStrategy.BALANCED,
dataloader: DataloaderType.NONE,
populateWhere: PopulateHint.ALL,
ignoreUndefinedInQuery: false,
onQuery: (sql) => sql,
autoJoinOneToOneOwner: true,
autoJoinRefsForFilters: true,
filtersOnRelations: true,
propagationOnPrototype: true,
populateAfterFlush: true,
serialization: {
includePrimaryKeys: true,
},
assign: {
updateNestedEntities: true,
updateByPrimaryKey: true,
mergeObjectProperties: false,
mergeEmbeddedProperties: true,
ignoreUndefined: false,
},
persistOnCreate: true,
upsertManaged: true,
forceEntityConstructor: false,
forceUndefined: false,
initNullableProperties: false,
forceUtcTimezone: true,
processOnCreateHooksEarly: true,
ensureDatabase: true,
ensureIndexes: false,
batchSize: 300,
debug: false,
ignoreDeprecations: false,
verbose: false,
driverOptions: {},
migrations: {
tableName: 'mikro_orm_migrations',
glob: '!(*.d).{js,ts,cjs}',
silent: false,
transactional: true,
allOrNothing: true,
dropTables: true,
safe: false,
snapshot: true,
emit: 'ts',
fileName: (timestamp, name) => `Migration${timestamp}${name ? '_' + name : ''}`,
},
schemaGenerator: {
createForeignKeyConstraints: true,
ignoreSchema: [],
skipTables: [],
skipViews: [],
skipColumns: {},
},
embeddables: {
prefixMode: 'relative',
},
entityGenerator: {
forceUndefined: true,
undefinedDefaults: false,
scalarTypeInDecorator: false,
bidirectionalRelations: true,
identifiedReferences: true,
scalarPropertiesForRelations: 'never',
entityDefinition: 'defineEntity',
decorators: 'legacy',
enumMode: 'dictionary',
/* v8 ignore next */
fileName: (className) => className,
onlyPurePivotTables: false,
outputPurePivotTables: false,
readOnlyPivotTables: false,
useCoreBaseEntity: false,
},
metadataCache: {},
resultCache: {
adapter: MemoryCacheAdapter,
expiration: 1000, // 1s
options: {},
},
metadataProvider: MetadataProvider,
highlighter: new NullHighlighter(),
seeder: {
defaultSeeder: 'DatabaseSeeder',
glob: '!(*.d).{js,ts}',
emit: 'ts',
fileName: (className) => className,
},
preferReadReplicas: true,
dynamicImportProvider: /* v8 ignore next */ (id) => import(id),
};
/** Holds and validates all ORM configuration options, providing access to drivers, loggers, cache adapters, and other services. */
export class Configuration {
#options;
#logger;
#slowQueryLogger;
#driver;
#platform;
#cache = new Map();
#extensions = new Map();
#routines = [];
#routinesNormalised = false;
constructor(options, validate = true) {
if (options.dynamicImportProvider) {
globalThis.dynamicImportProvider = options.dynamicImportProvider;
}
this.#options = Utils.mergeConfig({}, DEFAULTS, options);
if (validate) {
this.validateOptions();
}
this.#options.loggerFactory ??= DefaultLogger.create;
this.#logger = this.#options.loggerFactory({
debugMode: this.#options.debug,
ignoreDeprecations: this.#options.ignoreDeprecations,
usesReplicas: (this.#options.replicas?.length ?? 0) > 0,
highlighter: this.#options.highlighter,
writer: this.#options.logger,
});
const cf = this.#options.compiledFunctions;
if (cf && cf.__version !== Utils.getORMVersion()) {
this.#logger.warn('discovery', `Compiled functions were generated with MikroORM v${cf.__version ?? 'unknown'}, but the current version is v${Utils.getORMVersion()}. Please regenerate with \`npx mikro-orm compile\`.`);
}
if (this.#options.driver) {
this.#driver = new this.#options.driver(this);
this.#platform = this.#driver.getPlatform();
this.#platform.setConfig(this);
this.init(validate);
}
}
/** Returns the database platform instance. */
getPlatform() {
return this.#platform;
}
/**
* Gets specific configuration option. Falls back to specified `defaultValue` if provided.
*/
get(key, defaultValue) {
if (typeof this.#options[key] !== 'undefined') {
return this.#options[key];
}
return defaultValue;
}
/** Returns all configuration options. */
getAll() {
return this.#options;
}
/** Validates the `routines` config option on first access and throws on duplicate `(schema, name)` pairs or non-Routine entries. */
getRoutines() {
this.normaliseRoutines();
return this.#routines;
}
hasRoutine(routine) {
this.normaliseRoutines();
return this.#routines.includes(routine);
}
normaliseRoutines() {
if (this.#routinesNormalised) {
return;
}
const validator = new MetadataValidator();
const seenKeys = new Set();
// Stage in a local array so a mid-loop throw doesn't leave `#routines` partially populated;
// a retry after the user fixes the offending entry would otherwise duplicate earlier ones.
const collected = [];
for (const item of this.#options.routines ?? []) {
if (!Routine.is(item)) {
throw new Error(`'routines' entry is not a stored routine declaration. Use a Routine class instance.`);
}
validator.validateRoutineDefinition(item);
// Case-fold to match `SchemaComparator.compareRoutines`, which lower-cases the key so
// dialect-specific folding (Oracle uppercases, PG lowercases) round-trips; otherwise a
// user could register `'Foo'` and `'foo'` here and have the comparator silently collapse them.
const key = ((item.schema ? `${item.schema}.` : '') + item.name).toLowerCase();
if (seenKeys.has(key)) {
throw new Error(`Duplicate routine '${key}' declared more than once in the 'routines' config. Routine names must be unique within a schema.`);
}
seenKeys.add(key);
collected.push(item);
}
this.#routines.push(...collected);
this.#routinesNormalised = true;
}
/**
* Overrides specified configuration value.
*/
set(key, value) {
this.#options[key] = value;
this.sync();
}
/**
* Resets the configuration to its default value
*/
reset(key) {
this.#options[key] = DEFAULTS[key];
}
/**
* Gets Logger instance.
*/
getLogger() {
return this.#logger;
}
/**
* Gets the logger instance for slow queries.
* Falls back to the main logger if no custom slow query logger factory is configured.
*/
getSlowQueryLogger() {
this.#slowQueryLogger ??=
this.#options.slowQueryLoggerFactory?.({
debugMode: this.#options.debug,
writer: this.#options.logger,
highlighter: this.#options.highlighter,
usesReplicas: (this.#options.replicas?.length ?? 0) > 0,
}) ?? this.#logger;
return this.#slowQueryLogger;
}
/** Returns the configured dataloader type, normalizing boolean values. */
getDataloaderType() {
if (typeof this.#options.dataloader === 'boolean') {
return this.#options.dataloader ? DataloaderType.ALL : DataloaderType.NONE;
}
return this.#options.dataloader;
}
/** Returns the configured schema name, optionally skipping the platform's default schema. */
getSchema(skipDefaultSchema = false) {
if (skipDefaultSchema && this.#options.schema === this.#platform.getDefaultSchemaName()) {
return undefined;
}
return this.#options.schema;
}
/**
* Gets current database driver instance.
*/
getDriver() {
return this.#driver;
}
/** Registers a lazily-initialized extension by name. */
registerExtension(name, cb) {
this.#extensions.set(name, cb);
}
/** Returns a previously registered extension by name, initializing it on first access. */
getExtension(name) {
if (this.#cache.has(name)) {
return this.#cache.get(name);
}
const ext = this.#extensions.get(name);
/* v8 ignore next */
if (!ext) {
return undefined;
}
this.#cache.set(name, ext());
return this.#cache.get(name);
}
/**
* Gets instance of NamingStrategy. (cached)
*/
getNamingStrategy() {
return this.getCachedService(this.#options.namingStrategy || this.#platform.getNamingStrategy());
}
/**
* Gets instance of Hydrator. (cached)
*/
getHydrator(metadata) {
return this.getCachedService(this.#options.hydrator, metadata, this.#platform, this);
}
/**
* Gets instance of Comparator. (cached)
*/
getComparator(metadata) {
return this.getCachedService(EntityComparator, metadata, this.#platform, this);
}
/**
* Gets instance of MetadataProvider. (cached)
*/
getMetadataProvider() {
return this.getCachedService(this.#options.metadataProvider, this);
}
/**
* Gets instance of metadata CacheAdapter. (cached)
*/
getMetadataCacheAdapter() {
return this.getCachedService(this.#options.metadataCache.adapter, this.#options.metadataCache.options, this.#options.baseDir, this.#options.metadataCache.pretty);
}
/**
* Gets instance of CacheAdapter for result cache. (cached)
*/
getResultCacheAdapter() {
return this.getCachedService(this.#options.resultCache.adapter, {
expiration: this.#options.resultCache.expiration,
...this.#options.resultCache.options,
});
}
/**
* Gets EntityRepository class to be instantiated.
*/
getRepositoryClass(repository) {
if (repository) {
return repository();
}
if (this.#options.entityRepository) {
return this.#options.entityRepository;
}
return this.#platform.getRepositoryClass();
}
/**
* Creates instance of given service and caches it.
*/
getCachedService(cls, ...args) {
if (!this.#cache.has(cls.name)) {
this.#cache.set(cls.name, new cls(...args));
}
return this.#cache.get(cls.name);
}
/** Clears the cached service instances, forcing re-creation on next access. */
resetServiceCache() {
this.#cache.clear();
}
init(validate) {
const useCache = this.getMetadataProvider().useCache();
const metadataCache = this.#options.metadataCache;
if (!useCache) {
metadataCache.adapter = NullCacheAdapter;
}
metadataCache.enabled ??= useCache;
this.#options.clientUrl ??= this.#platform.getDefaultClientUrl();
this.#options.implicitTransactions ??= this.#platform.usesImplicitTransactions();
// Eagerly trigger routine validation so errors surface at init time, not on first call.
if (validate) {
this.getRoutines();
}
if (validate && metadataCache.enabled && !metadataCache.adapter) {
throw new Error('No metadata cache adapter specified, please fill in `metadataCache.adapter` option or use the async MikroORM.init() method which can autoload it.');
}
try {
const url = new URL(this.#options.clientUrl);
if (url.pathname) {
this.#options.dbName = this.get('dbName', decodeURIComponent(url.pathname).substring(1));
}
}
catch {
const url = /:\/\/.*\/([^?]+)/.exec(this.#options.clientUrl);
if (url) {
this.#options.dbName = this.get('dbName', decodeURIComponent(url[1]));
}
}
if (validate && !this.#options.dbName && this.#options.clientUrl) {
throw new Error("No database specified, `clientUrl` option provided but it's missing the pathname.");
}
this.#options.schema ??= this.#platform.getDefaultSchemaName();
this.#options.charset ??= this.#platform.getDefaultCharset();
Object.keys(this.#options.filters).forEach(key => {
this.#options.filters[key].default ??= true;
});
if (!this.#options.filtersOnRelations) {
this.#options.autoJoinRefsForFilters ??= false;
}
this.#options.subscribers = [...this.#options.subscribers].map(subscriber => {
return subscriber.constructor.name === 'Function' ? new subscriber() : subscriber;
});
this.sync();
if (!colors.enabled()) {
this.#options.highlighter = new NullHighlighter();
}
}
sync() {
setEnv('MIKRO_ORM_COLORS', this.#options.colors);
this.#logger.setDebugMode(this.#options.debug);
this.#slowQueryLogger = undefined;
}
validateOptions() {
/* v8 ignore next */
if ('type' in this.#options) {
throw new Error("The `type` option has been removed in v6, please fill in the `driver` option instead or use `defineConfig` helper (to define your ORM config) or `MikroORM` class (to call the `init` method) exported from the driver package (e.g. `import { defineConfig } from '@mikro-orm/mysql'; export default defineConfig({ ... })`).");
}
if (!this.#options.driver) {
throw new Error("No driver specified, please fill in the `driver` option or use `defineConfig` helper (to define your ORM config) or `MikroORM` class (to call the `init` method) exported from the driver package (e.g. `import { defineConfig } from '@mikro-orm/mysql'; export defineConfig({ ... })`).");
}
if (!this.#options.dbName && !this.#options.clientUrl) {
throw new Error('No database specified, please fill in `dbName` or `clientUrl` option');
}
if (this.#options.entities.length === 0 && this.#options.discovery.warnWhenNoEntities) {
throw new Error('No entities found, please use `entities` option');
}
if (typeof this.#options.driverOptions === 'function' &&
this.#options.driverOptions.constructor.name === 'AsyncFunction') {
throw new Error('`driverOptions` callback cannot be async');
}
}
}
/**
* Type helper to make it easier to use `mikro-orm.config.js`.
*/
export function defineConfig(options) {
return options;
}