@mikro-orm/core 7.1.0-dev.43 → 7.1.0-dev.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,7 +7,8 @@ import { EntityLoader, type EntityLoaderOptions } from './entity/EntityLoader.js
7
7
  import { Reference } from './entity/Reference.js';
8
8
  import { UnitOfWork } from './unit-of-work/UnitOfWork.js';
9
9
  import type { CountByOptions, CountOptions, DeleteOptions, FilterOptions, FindAllOptions, FindByCursorOptions, FindOneOptions, FindOneOrFailOptions, FindOptions, GetReferenceOptions, IDatabaseDriver, LockOptions, NativeInsertUpdateOptions, StreamOptions, UpdateOptions, UpsertManyOptions, UpsertOptions } from './drivers/IDatabaseDriver.js';
10
- import type { AnyString, ArrayElement, AutoPath, ConnectionType, Dictionary, EntityClass, EntityData, EntityDictionary, EntityDTO, EntityKey, EntityMetadata, EntityName, FilterDef, FilterQuery, FromEntityType, GetRepository, IHydrator, IsSubset, Loaded, MergeLoaded, MergeSelected, ObjectQuery, PopulateOptions, Primary, Ref, RequiredEntityData, UnboxArray, IndexFilterQuery, WithUsingOptions } from './typings.js';
10
+ import type { AnyString, ArrayElement, AutoPath, ConnectionType, Dictionary, EntityClass, EntityData, EntityDictionary, EntityDTO, EntityKey, EntityMetadata, EntityName, FilterDef, FilterQuery, FromEntityType, GetRepository, IHydrator, IsSubset, Loaded, MergeLoaded, MergeSelected, ObjectQuery, PopulateOptions, Primary, Ref, RequiredEntityData, RoutineArgs, RoutineReturn, UnboxArray, IndexFilterQuery, WithUsingOptions } from './typings.js';
11
+ import type { Routine } from './metadata/Routine.js';
11
12
  import { FlushMode, LockMode, PopulatePath, type TransactionOptions } from './enums.js';
12
13
  import type { MetadataStorage } from './metadata/MetadataStorage.js';
13
14
  import type { AbortQueryOptions, InflightQueryAbortStrategy, Transaction } from './connections/Connection.js';
@@ -346,6 +347,29 @@ export declare class EntityManager<Driver extends IDatabaseDriver = IDatabaseDri
346
347
  * Fires native update query. Calling this has no side effects on the context (identity map).
347
348
  */
348
349
  nativeUpdate<Entity extends object>(entityName: EntityName<Entity>, where: FilterQuery<NoInfer<Entity>>, data: EntityData<Entity>, options?: UpdateOptions<Entity>): Promise<number>;
350
+ /**
351
+ * Invokes a stored procedure or function declared via the {@link Routine} class. Arg and return
352
+ * types are inferred from the literal config; use {@link Routine.create} to refine when needed.
353
+ *
354
+ * Procedures emitting result sets (MySQL `SELECT`s, PG/Oracle refcursor OUT params) return
355
+ * `Dictionary[][]`; MSSQL multi-result procs aren't exposed here, use `em.getConnection().execute()`.
356
+ *
357
+ * Mongo throws. SQLite bridges `bodyJs` functions via better-sqlite3 UDFs (procedures and
358
+ * function-without-bodyJs throw); libSQL throws unconditionally. Oracle calls run on their own
359
+ * pool connection with `autoCommit: true`, so wrapping in `em.transactional(...)` throws.
360
+ *
361
+ * @example
362
+ * ```ts
363
+ * const hash = await em.callRoutine(HashUser, { name: 'jon', salt: 'pepper' });
364
+ *
365
+ * const hashRef = new ScalarReference<string>();
366
+ * await em.callRoutine(AddRecord, { p_name: 'jon', p_age: 30, p_hash: hashRef });
367
+ *
368
+ * const TwoCursors = Routine.create<Record<string, never>, unknown[][]>({ ... });
369
+ * const [users, books] = await em.transactional(em => em.callRoutine(TwoCursors, {}));
370
+ * ```
371
+ */
372
+ callRoutine<R extends Routine>(routine: R, args: RoutineArgs<R>): Promise<RoutineReturn<R>>;
349
373
  /**
350
374
  * Fires native delete query. Calling this has no side effects on the context (identity map).
351
375
  */
package/EntityManager.js CHANGED
@@ -1345,6 +1345,37 @@ export class EntityManager {
1345
1345
  });
1346
1346
  return res.affectedRows;
1347
1347
  }
1348
+ /**
1349
+ * Invokes a stored procedure or function declared via the {@link Routine} class. Arg and return
1350
+ * types are inferred from the literal config; use {@link Routine.create} to refine when needed.
1351
+ *
1352
+ * Procedures emitting result sets (MySQL `SELECT`s, PG/Oracle refcursor OUT params) return
1353
+ * `Dictionary[][]`; MSSQL multi-result procs aren't exposed here, use `em.getConnection().execute()`.
1354
+ *
1355
+ * Mongo throws. SQLite bridges `bodyJs` functions via better-sqlite3 UDFs (procedures and
1356
+ * function-without-bodyJs throw); libSQL throws unconditionally. Oracle calls run on their own
1357
+ * pool connection with `autoCommit: true`, so wrapping in `em.transactional(...)` throws.
1358
+ *
1359
+ * @example
1360
+ * ```ts
1361
+ * const hash = await em.callRoutine(HashUser, { name: 'jon', salt: 'pepper' });
1362
+ *
1363
+ * const hashRef = new ScalarReference<string>();
1364
+ * await em.callRoutine(AddRecord, { p_name: 'jon', p_age: 30, p_hash: hashRef });
1365
+ *
1366
+ * const TwoCursors = Routine.create<Record<string, never>, unknown[][]>({ ... });
1367
+ * const [users, books] = await em.transactional(em => em.callRoutine(TwoCursors, {}));
1368
+ * ```
1369
+ */
1370
+ async callRoutine(routine, args) {
1371
+ const em = this.getContext(false);
1372
+ // Identity by reference so the user-held import survives bundling and tree-shakes unused routines.
1373
+ if (!em.config.hasRoutine(routine)) {
1374
+ throw new Error(`Routine '${routine.name}' is not registered in the 'routines' config option.`);
1375
+ }
1376
+ const conn = em.driver.getConnection('write');
1377
+ return conn.callRoutine(routine, args, em.#transactionContext);
1378
+ }
1348
1379
  /**
1349
1380
  * Fires native delete query. Calling this has no side effects on the context (identity map).
1350
1381
  */
@@ -1,8 +1,10 @@
1
1
  import { type Configuration, type ConnectionOptions } from '../utils/Configuration.js';
2
2
  import type { LogContext, Logger } from '../logging/Logger.js';
3
3
  import type { MetadataStorage } from '../metadata/MetadataStorage.js';
4
- import type { ConnectionType, Dictionary, MaybePromise, Primary } from '../typings.js';
4
+ import type { ConnectionType, Dictionary, MaybePromise, Primary, RoutineProperty } from '../typings.js';
5
+ import type { Routine } from '../metadata/Routine.js';
5
6
  import type { Platform } from '../platforms/Platform.js';
7
+ import type { Type } from '../types/Type.js';
6
8
  import type { TransactionEventBroadcaster } from '../events/TransactionEventBroadcaster.js';
7
9
  import type { IsolationLevel } from '../enums.js';
8
10
  /** Abstract base class for database connections, providing transaction and query execution support. */
@@ -72,6 +74,41 @@ export declare abstract class Connection {
72
74
  rollback(ctx: Transaction, eventBroadcaster?: TransactionEventBroadcaster, loggerContext?: LogContext): Promise<void>;
73
75
  /** Executes a raw query and returns the result. */
74
76
  abstract execute<T>(query: string, params?: any[], method?: 'all' | 'get' | 'run', ctx?: Transaction): Promise<QueryResult<T> | any | any[]>;
77
+ /** @internal — public callers go through {@link EntityManager.callRoutine}. */
78
+ callRoutine<T>(routine: Routine, args: Record<string, unknown>, ctx?: Transaction): Promise<T>;
79
+ /**
80
+ * Unwraps a routine argument (resolving any `ScalarReference` wrapper) and, when the param
81
+ * declares a `customType`, marshals it through `convertToDatabaseValue`. `undefined` is
82
+ * normalised to `null` so every driver sees the same shape.
83
+ *
84
+ * @internal
85
+ */
86
+ protected convertRoutineInbound(value: unknown, param: RoutineProperty | undefined): unknown;
87
+ /**
88
+ * Converts a raw database value to its JS representation via the supplied `customType`, when
89
+ * one is declared. Used to marshal scalar function returns and OUT/INOUT values back to the
90
+ * caller before they land in a `ScalarReference` or `em.callRoutine`'s return value.
91
+ *
92
+ * @internal
93
+ */
94
+ protected convertRoutineOutbound<T>(value: unknown, customType: Type<unknown> | undefined): T;
95
+ /**
96
+ * Executes a scalar function routine as `select <qualified>(?, ?, ...) as value`, marshalling
97
+ * IN params on the way in and the return value on the way out. The qualified name is built
98
+ * from `routine.schema`, falling back to the platform's default schema (e.g. `dbo` on MSSQL),
99
+ * which gives MySQL/SQLite a bare name and MSSQL/Oracle the mandatory `schema.name` form.
100
+ *
101
+ * @internal
102
+ */
103
+ protected callRoutineFunction<T>(routine: Routine, args: Record<string, unknown>, ctx?: Transaction): Promise<T>;
104
+ /**
105
+ * Walks a result row produced by an OUT/INOUT-param SELECT and writes each value into the
106
+ * caller's `ScalarReference` slot. Non-reference args are ignored (the user opted out of
107
+ * receiving the OUT value).
108
+ *
109
+ * @internal
110
+ */
111
+ protected applyRoutineOutParams(row: Dictionary, outParams: RoutineProperty[], args: Record<string, unknown>): void;
75
112
  /** Parses and returns the resolved connection configuration (host, port, user, etc.). */
76
113
  getConnectionOptions(): ConnectionConfig;
77
114
  /** Sets the metadata storage on this connection. */
@@ -1,4 +1,5 @@
1
1
  import { Utils } from '../utils/Utils.js';
2
+ import { ScalarReference } from '../entity/Reference.js';
2
3
  /** Abstract base class for database connections, providing transaction and query execution support. */
3
4
  export class Connection {
4
5
  config;
@@ -92,6 +93,69 @@ export class Connection {
92
93
  async rollback(ctx, eventBroadcaster, loggerContext) {
93
94
  throw new Error(`Transactions are not supported by current driver`);
94
95
  }
96
+ /** @internal — public callers go through {@link EntityManager.callRoutine}. */
97
+ async callRoutine(routine, args, ctx) {
98
+ throw new Error(`Stored routines are not supported by the current driver`);
99
+ }
100
+ /**
101
+ * Unwraps a routine argument (resolving any `ScalarReference` wrapper) and, when the param
102
+ * declares a `customType`, marshals it through `convertToDatabaseValue`. `undefined` is
103
+ * normalised to `null` so every driver sees the same shape.
104
+ *
105
+ * @internal
106
+ */
107
+ convertRoutineInbound(value, param) {
108
+ const resolved = value instanceof ScalarReference ? value.unwrap() : value;
109
+ const coerced = resolved === undefined ? null : resolved;
110
+ if (coerced === null || !param?.customType) {
111
+ return coerced;
112
+ }
113
+ return param.customType.convertToDatabaseValue(coerced, this.platform);
114
+ }
115
+ /**
116
+ * Converts a raw database value to its JS representation via the supplied `customType`, when
117
+ * one is declared. Used to marshal scalar function returns and OUT/INOUT values back to the
118
+ * caller before they land in a `ScalarReference` or `em.callRoutine`'s return value.
119
+ *
120
+ * @internal
121
+ */
122
+ convertRoutineOutbound(value, customType) {
123
+ if (value === null || value === undefined || !customType) {
124
+ return value;
125
+ }
126
+ return customType.convertToJSValue(value, this.platform);
127
+ }
128
+ /**
129
+ * Executes a scalar function routine as `select <qualified>(?, ?, ...) as value`, marshalling
130
+ * IN params on the way in and the return value on the way out. The qualified name is built
131
+ * from `routine.schema`, falling back to the platform's default schema (e.g. `dbo` on MSSQL),
132
+ * which gives MySQL/SQLite a bare name and MSSQL/Oracle the mandatory `schema.name` form.
133
+ *
134
+ * @internal
135
+ */
136
+ async callRoutineFunction(routine, args, ctx) {
137
+ const schema = routine.schema ?? this.platform.getDefaultSchemaName();
138
+ const qualified = (schema ? `${this.platform.quoteIdentifier(schema)}.` : '') + this.platform.quoteIdentifier(routine.name);
139
+ const placeholders = routine.params.map(() => '?').join(', ');
140
+ const positional = routine.params.map(p => this.convertRoutineInbound(args[p.name], p));
141
+ const rows = (await this.execute(`select ${qualified}(${placeholders}) as value`, positional, 'all', ctx));
142
+ return this.convertRoutineOutbound(rows[0]?.value, routine.returnCustomType);
143
+ }
144
+ /**
145
+ * Walks a result row produced by an OUT/INOUT-param SELECT and writes each value into the
146
+ * caller's `ScalarReference` slot. Non-reference args are ignored (the user opted out of
147
+ * receiving the OUT value).
148
+ *
149
+ * @internal
150
+ */
151
+ applyRoutineOutParams(row, outParams, args) {
152
+ for (const param of outParams) {
153
+ const ref = args[param.name];
154
+ if (ref instanceof ScalarReference) {
155
+ ref.set(this.convertRoutineOutbound(row[param.name], param.customType));
156
+ }
157
+ }
158
+ }
95
159
  /** Parses and returns the resolved connection configuration (host, port, user, etc.). */
96
160
  getConnectionOptions() {
97
161
  const ret = {};
package/index.d.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * @module core
4
4
  */
5
5
  export { EntityMetadata, PrimaryKeyProp, EntityRepositoryType, OptionalProps, EagerProps, HiddenProps, Config, EntityName, IndexHints, } from './typings.js';
6
- export type { CompiledFunctions, Constructor, ConnectionType, Dictionary, Primary, IPrimaryKey, ObjectQuery, FilterQuery, IWrappedEntity, InferEntityName, EntityData, Highlighter, MaybePromise, AnyEntity, EntityClass, EntityProperty, PopulateOptions, Populate, Loaded, New, LoadedReference, LoadedCollection, IMigrator, IMigrationGenerator, MigratorEvent, GetRepository, MigrationObject, DeepPartial, PrimaryProperty, Cast, IsUnknown, EntityDictionary, EntityDTO, EntityDTOFlat, EntityDTOProp, SerializeDTO, MigrationDiff, GenerateOptions, FilterObject, IndexFilterQuery, ExtractIndexHints, IndexName, IndexColumns, WithUsingOptions, IMigrationRunner, IEntityGenerator, ISeedManager, SeederObject, IMigratorStorage, RequiredEntityData, CheckCallback, TriggerCallback, IndexCallback, FormulaCallback, FormulaColumns, FormulaTable, SchemaTable, SchemaColumns, SchemaColumnRef, EntityDataPropValue, SimpleColumnMeta, Rel, Ref, LazyRef, ScalarRef, EntityRef, ISchemaGenerator, MigrationInfo, MigrateOptions, MigrationResult, MigrationRow, EntityKey, EntityValue, EntityDataValue, FilterKey, EntityType, FromEntityType, Selected, IsSubset, EntityProps, ExpandProperty, ExpandScalar, FilterItemValue, ExpandQuery, Scalar, ExpandHint, FilterValue, MergeLoaded, MergeSelected, TypeConfig, AnyString, ClearDatabaseOptions, CreateSchemaOptions, EnsureDatabaseOptions, UpdateSchemaOptions, DropSchemaOptions, RefreshDatabaseOptions, AutoPath, UnboxArray, MetadataProcessor, ImportsResolver, RequiredNullable, DefineConfig, Opt, Hidden, EntitySchemaWithMeta, InferEntity, CheckConstraint, TriggerDef, GeneratedColumnCallback, FilterDef, EntityCtor, Subquery, PopulateHintOptions, Prefixes, } from './typings.js';
6
+ export type { CompiledFunctions, Constructor, ConnectionType, Dictionary, Primary, IPrimaryKey, ObjectQuery, FilterQuery, IWrappedEntity, InferEntityName, EntityData, Highlighter, MaybePromise, AnyEntity, EntityClass, EntityProperty, PopulateOptions, Populate, Loaded, New, LoadedReference, LoadedCollection, IMigrator, IMigrationGenerator, MigratorEvent, GetRepository, MigrationObject, DeepPartial, PrimaryProperty, Cast, IsUnknown, EntityDictionary, EntityDTO, EntityDTOFlat, EntityDTOProp, SerializeDTO, MigrationDiff, GenerateOptions, FilterObject, IndexFilterQuery, ExtractIndexHints, IndexName, IndexColumns, WithUsingOptions, IMigrationRunner, IEntityGenerator, ISeedManager, SeederObject, IMigratorStorage, RequiredEntityData, CheckCallback, TriggerCallback, IndexCallback, FormulaCallback, FormulaColumns, FormulaTable, SchemaTable, SchemaColumns, SchemaColumnRef, EntityDataPropValue, SimpleColumnMeta, Rel, Ref, LazyRef, ScalarRef, EntityRef, ISchemaGenerator, MigrationInfo, MigrateOptions, MigrationResult, MigrationRow, EntityKey, EntityValue, EntityDataValue, FilterKey, EntityType, FromEntityType, Selected, IsSubset, EntityProps, ExpandProperty, ExpandScalar, FilterItemValue, ExpandQuery, Scalar, ExpandHint, FilterValue, MergeLoaded, MergeSelected, TypeConfig, AnyString, ClearDatabaseOptions, CreateSchemaOptions, EnsureDatabaseOptions, UpdateSchemaOptions, DropSchemaOptions, RefreshDatabaseOptions, AutoPath, UnboxArray, MetadataProcessor, ImportsResolver, RequiredNullable, DefineConfig, Opt, Hidden, EntitySchemaWithMeta, InferEntity, CheckConstraint, TriggerDef, RoutineReturns, RoutineBodyCallback, RoutineJsBody, RoutineIgnoreField, RoutineParamConfig, RoutineConfig, RoutineRuntimeType, RoutineArgs, RoutineReturn, GeneratedColumnCallback, FilterDef, EntityCtor, Subquery, PopulateHintOptions, Prefixes, } from './typings.js';
7
7
  export * from './enums.js';
8
8
  export * from './errors.js';
9
9
  export * from './exceptions.js';
@@ -1,4 +1,5 @@
1
1
  import type { EntityMetadata, EntityName } from '../typings.js';
2
+ import type { Routine } from './Routine.js';
2
3
  import { type MetadataDiscoveryOptions } from '../utils/Configuration.js';
3
4
  import type { MetadataStorage } from './MetadataStorage.js';
4
5
  /**
@@ -6,6 +7,7 @@ import type { MetadataStorage } from './MetadataStorage.js';
6
7
  */
7
8
  export declare class MetadataValidator {
8
9
  validateEntityDefinition<T>(metadata: MetadataStorage, name: EntityName<T>, options: MetadataDiscoveryOptions): void;
10
+ validateRoutineDefinition(routine: Routine): void;
9
11
  validateDiscovered(discovered: EntityMetadata[], options: MetadataDiscoveryOptions): void;
10
12
  private validatePartitioning;
11
13
  /**
@@ -62,6 +62,41 @@ export class MetadataValidator {
62
62
  }
63
63
  }
64
64
  }
65
+ validateRoutineDefinition(routine) {
66
+ if (typeof routine.name !== 'string' || routine.name.trim() === '') {
67
+ throw new MetadataError(`Routine is missing the required 'name' option.`);
68
+ }
69
+ if (!routine.type) {
70
+ throw new MetadataError(`Routine ${routine.name} is missing the required 'type' option ('procedure' | 'function').`);
71
+ }
72
+ if (routine.body != null && routine.expression != null) {
73
+ throw new MetadataError(`Routine ${routine.name} defines both 'body' and 'expression'. Use one or the other.`);
74
+ }
75
+ if (routine.body == null && routine.expression == null && routine.bodyJs == null) {
76
+ throw new MetadataError(`Routine ${routine.name} must define a 'body', 'expression', or 'bodyJs'.`);
77
+ }
78
+ if (routine.type === 'function' && routine.returns == null) {
79
+ throw new MetadataError(`Function routine ${routine.name} must declare a 'returns' option.`);
80
+ }
81
+ if (routine.type === 'procedure' && routine.bodyJs != null) {
82
+ throw new MetadataError(`Routine ${routine.name} declares 'bodyJs' on a procedure. JS fallbacks are only supported for functions — SQLite has no analog for stored procedures.`);
83
+ }
84
+ for (const param of routine.params) {
85
+ const dir = param.direction;
86
+ if (dir !== 'in' && dir !== 'out' && dir !== 'inout') {
87
+ throw new MetadataError(`Routine ${routine.name}.${param.name} has invalid direction '${dir}'. Expected 'in', 'out', or 'inout'.`);
88
+ }
89
+ if ((dir === 'out' || dir === 'inout') && !param.ref) {
90
+ throw new MetadataError(`Routine ${routine.name}.${param.name} is declared as '${dir}' but missing 'ref: true'. OUT/INOUT parameters must be passed as ScalarReference.`);
91
+ }
92
+ if (dir === 'in' && param.ref) {
93
+ throw new MetadataError(`Routine ${routine.name}.${param.name} declares 'ref: true' on an IN parameter. ScalarReference wrapping is only meaningful for OUT/INOUT parameters.`);
94
+ }
95
+ if (routine.type === 'function' && dir !== 'in') {
96
+ throw new MetadataError(`Function routine ${routine.name}.${param.name} declares direction '${dir}'. Functions only support IN parameters — use a procedure for OUT/INOUT semantics.`);
97
+ }
98
+ }
99
+ }
65
100
  validateDiscovered(discovered, options) {
66
101
  if (discovered.length === 0 && options.warnWhenNoEntities) {
67
102
  throw MetadataError.noEntityDiscovered();
@@ -0,0 +1,74 @@
1
+ import type { RoutineArgsOf, RoutineConfig, RoutineDataAccess, RoutineIgnoreField, RoutineKind, RoutineParamMap, RoutineProperty, RoutineReturnOf, RoutineReturns, RoutineSecurity } from '../typings.js';
2
+ import type { Type } from '../types/Type.js';
3
+ import type { Raw } from '../utils/RawQueryFragment.js';
4
+ /**
5
+ * Stored procedure or function declaration. Register instances via the `routines` config option
6
+ * passed to `MikroORM.init`, then call them through `em.callRoutine(routine, args)`.
7
+ *
8
+ * `TArgs` and `TReturn` are inferred from the literal config, so `em.callRoutine` is fully typed
9
+ * without threading generics through the call site. Reach for {@link Routine.create} when the
10
+ * inferred type is too loose — typically an `object` return that should be a concrete shape
11
+ * rather than `Dictionary`.
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * const HashUser = new Routine({
16
+ * name: 'hash_user',
17
+ * type: 'function',
18
+ * params: {
19
+ * name: { type: 'varchar(255)' },
20
+ * salt: { type: 'varchar(255)' },
21
+ * },
22
+ * returns: { runtimeType: 'string', columnType: 'char(40)' },
23
+ * body: 'SELECT SHA1(CONCAT(name, salt))',
24
+ * });
25
+ *
26
+ * await MikroORM.init({ entities: [User], routines: [HashUser] });
27
+ *
28
+ * // args typed as `{ name: string; salt: string }`, result typed as `string`:
29
+ * const hash = await em.callRoutine(HashUser, { name: 'jon', salt: 'pepper' });
30
+ * ```
31
+ */
32
+ export declare class Routine<const TConfig extends RoutineConfig = RoutineConfig, TArgs = RoutineArgsOf<TConfig>, TReturn = RoutineReturnOf<TConfig>> {
33
+ readonly name: string;
34
+ readonly type: RoutineKind;
35
+ readonly schema?: string;
36
+ readonly comment?: string;
37
+ readonly security?: RoutineSecurity;
38
+ readonly definer?: string;
39
+ readonly deterministic?: boolean;
40
+ readonly dataAccess?: RoutineDataAccess;
41
+ readonly language?: string;
42
+ readonly body?: string | Raw | ((params: RoutineParamMap<any>, em: any) => string | Raw);
43
+ readonly expression?: string;
44
+ readonly bodyJs?: (params: any) => unknown;
45
+ readonly returns?: RoutineReturns;
46
+ readonly returnCustomType?: Type<unknown>;
47
+ readonly ignoreSchemaChanges?: RoutineIgnoreField[];
48
+ readonly params: RoutineProperty[];
49
+ constructor(config: TConfig);
50
+ /** @internal */
51
+ createParamMappingObject(): Record<string, string>;
52
+ /**
53
+ * Overrides the inferred TArgs/TReturn. Omit a generic to keep inference; pass `never` in the
54
+ * args slot to refine only the return type.
55
+ *
56
+ * @example
57
+ * ```ts
58
+ * interface UserStats { totalOrders: number; lastOrderAt: Date }
59
+ * const GetStats = Routine.create<never, UserStats>({
60
+ * name: 'get_user_stats',
61
+ * type: 'function',
62
+ * params: { user_id: { type: 'int', runtimeType: 'number' } },
63
+ * returns: { runtimeType: 'object', columnType: 'json' },
64
+ * body: '...',
65
+ * });
66
+ * ```
67
+ */
68
+ static create<TArgs = never, TReturn = never, const TConfig extends RoutineConfig = RoutineConfig>(config: TConfig): Routine<TConfig, [
69
+ TArgs
70
+ ] extends [never] ? RoutineArgsOf<TConfig> : TArgs, [
71
+ TReturn
72
+ ] extends [never] ? RoutineReturnOf<TConfig> : TReturn>;
73
+ static is(item: unknown): item is Routine;
74
+ }
@@ -0,0 +1,140 @@
1
+ // Resolve a `Type<unknown> | Constructor<Type<unknown>>` to an instance. Each built-in `Type` has
2
+ // `__mappedType` on its prototype so any constructed instance carries it; a missing brand means a
3
+ // raw constructor that we instantiate.
4
+ const toTypeInstance = (value) => {
5
+ return value.__mappedType ? value : new value();
6
+ };
7
+ /**
8
+ * Stored procedure or function declaration. Register instances via the `routines` config option
9
+ * passed to `MikroORM.init`, then call them through `em.callRoutine(routine, args)`.
10
+ *
11
+ * `TArgs` and `TReturn` are inferred from the literal config, so `em.callRoutine` is fully typed
12
+ * without threading generics through the call site. Reach for {@link Routine.create} when the
13
+ * inferred type is too loose — typically an `object` return that should be a concrete shape
14
+ * rather than `Dictionary`.
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * const HashUser = new Routine({
19
+ * name: 'hash_user',
20
+ * type: 'function',
21
+ * params: {
22
+ * name: { type: 'varchar(255)' },
23
+ * salt: { type: 'varchar(255)' },
24
+ * },
25
+ * returns: { runtimeType: 'string', columnType: 'char(40)' },
26
+ * body: 'SELECT SHA1(CONCAT(name, salt))',
27
+ * });
28
+ *
29
+ * await MikroORM.init({ entities: [User], routines: [HashUser] });
30
+ *
31
+ * // args typed as `{ name: string; salt: string }`, result typed as `string`:
32
+ * const hash = await em.callRoutine(HashUser, { name: 'jon', salt: 'pepper' });
33
+ * ```
34
+ */
35
+ export class Routine {
36
+ name;
37
+ type;
38
+ schema;
39
+ comment;
40
+ security;
41
+ definer;
42
+ deterministic;
43
+ dataAccess;
44
+ language;
45
+ body;
46
+ expression;
47
+ bodyJs;
48
+ returns;
49
+ returnCustomType;
50
+ ignoreSchemaChanges;
51
+ params;
52
+ constructor(config) {
53
+ this.name = config.name;
54
+ this.type = config.type;
55
+ this.schema = config.schema;
56
+ this.comment = config.comment;
57
+ this.security = config.security;
58
+ this.definer = config.definer;
59
+ this.deterministic = config.deterministic;
60
+ this.dataAccess = config.dataAccess;
61
+ this.language = config.language;
62
+ this.body = config.body;
63
+ this.expression = config.expression;
64
+ this.bodyJs = config.bodyJs;
65
+ this.returns = config.returns;
66
+ this.ignoreSchemaChanges = config.ignoreSchemaChanges;
67
+ this.params = Object.entries(config.params ?? {}).map(([name, opts], index) => {
68
+ const explicitCustom = opts.customType ? toTypeInstance(opts.customType) : undefined;
69
+ const rawType = opts.type;
70
+ let type;
71
+ let customType;
72
+ if (rawType == null || typeof rawType === 'string') {
73
+ type = rawType ?? 'string';
74
+ customType = explicitCustom;
75
+ }
76
+ else {
77
+ // A `Type` instance or constructor at `type` both defines the column (via
78
+ // `getColumnType` at schema-gen time) and acts as the default `customType` for
79
+ // marshalling. An explicit `customType` on the same param overrides the marshalling side.
80
+ const instance = toTypeInstance(rawType);
81
+ type = instance;
82
+ customType = explicitCustom ?? instance;
83
+ }
84
+ return {
85
+ name,
86
+ direction: opts.direction ?? 'in',
87
+ type,
88
+ runtimeType: opts.runtimeType ?? 'any',
89
+ columnTypes: [typeof type === 'string' ? type : ''],
90
+ customType,
91
+ ref: opts.ref,
92
+ length: opts.length,
93
+ precision: opts.precision,
94
+ scale: opts.scale,
95
+ nullable: opts.nullable,
96
+ defaultRaw: opts.defaultRaw,
97
+ index,
98
+ };
99
+ });
100
+ if (config.returns && typeof config.returns === 'object') {
101
+ // `returns: { type: SomeType }` is the canonical scalar-return shape — the Type drives
102
+ // both the column type (via `getColumnType` at schema-gen time) and the marshalling.
103
+ if ('type' in config.returns && config.returns.type) {
104
+ this.returnCustomType = toTypeInstance(config.returns.type);
105
+ }
106
+ else if ('customType' in config.returns && config.returns.customType) {
107
+ this.returnCustomType = toTypeInstance(config.returns.customType);
108
+ }
109
+ }
110
+ }
111
+ /** @internal */
112
+ createParamMappingObject() {
113
+ return this.params.reduce((o, p) => {
114
+ o[p.name] = p.name;
115
+ return o;
116
+ }, {});
117
+ }
118
+ /**
119
+ * Overrides the inferred TArgs/TReturn. Omit a generic to keep inference; pass `never` in the
120
+ * args slot to refine only the return type.
121
+ *
122
+ * @example
123
+ * ```ts
124
+ * interface UserStats { totalOrders: number; lastOrderAt: Date }
125
+ * const GetStats = Routine.create<never, UserStats>({
126
+ * name: 'get_user_stats',
127
+ * type: 'function',
128
+ * params: { user_id: { type: 'int', runtimeType: 'number' } },
129
+ * returns: { runtimeType: 'object', columnType: 'json' },
130
+ * body: '...',
131
+ * });
132
+ * ```
133
+ */
134
+ static create(config) {
135
+ return new Routine(config);
136
+ }
137
+ static is(item) {
138
+ return item instanceof Routine;
139
+ }
140
+ }
@@ -1,5 +1,6 @@
1
1
  export type * from './types.js';
2
2
  export * from './EntitySchema.js';
3
+ export * from './Routine.js';
3
4
  export * from './MetadataDiscovery.js';
4
5
  export * from './MetadataStorage.js';
5
6
  export * from './MetadataProvider.js';
package/metadata/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './EntitySchema.js';
2
+ export * from './Routine.js';
2
3
  export * from './MetadataDiscovery.js';
3
4
  export * from './MetadataStorage.js';
4
5
  export * from './MetadataProvider.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikro-orm/core",
3
- "version": "7.1.0-dev.43",
3
+ "version": "7.1.0-dev.45",
4
4
  "description": "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.",
5
5
  "keywords": [
6
6
  "data-mapper",
package/typings.d.ts CHANGED
@@ -10,6 +10,7 @@ import { Reference, type ScalarReference } from './entity/Reference.js';
10
10
  import type { SerializationContext } from './serialization/SerializationContext.js';
11
11
  import type { SerializeOptions } from './serialization/EntitySerializer.js';
12
12
  import type { MetadataStorage } from './metadata/MetadataStorage.js';
13
+ import type { Routine } from './metadata/Routine.js';
13
14
  import type { EntitySchema } from './metadata/EntitySchema.js';
14
15
  import type { EntityPartitionBy, IndexColumnOptions } from './metadata/types.js';
15
16
  import type { Type, types } from './types/index.js';
@@ -730,6 +731,197 @@ export interface TriggerDef<T = any> {
730
731
  /** Raw DDL escape hatch — full CREATE TRIGGER statement. Mutually exclusive with `body`. */
731
732
  expression?: string;
732
733
  }
734
+ export type RoutineParamDirection = 'in' | 'out' | 'inout';
735
+ /** Mirrors MySQL/MariaDB's `SQL DATA ACCESS` clause; ignored by other dialects. */
736
+ export type RoutineDataAccess = 'contains-sql' | 'no-sql' | 'reads-sql-data' | 'modifies-sql-data';
737
+ export type RoutineSecurity = 'invoker' | 'definer';
738
+ export type RoutineKind = 'procedure' | 'function';
739
+ export type RoutineIgnoreField = 'body' | 'comment' | 'security' | 'deterministic' | 'definer';
740
+ export type RoutineParamMap<T> = Record<PropertyName<T>, string>;
741
+ export type RoutineBodyCallback<T> = (params: RoutineParamMap<T>, em: any) => string | Raw;
742
+ /**
743
+ * Routine return shape:
744
+ * - omitted/`void`: procedures with no result set
745
+ * - `() => Entity` (single or tuple): hydrated row arrays, one per result set
746
+ * - `{ type: Type<...> }`: scalar function return — runtime/column types both inferred
747
+ * from the {@link Type} generics
748
+ * - `{ runtimeType, columnType }`: scalar function return with explicit SQL+TS types
749
+ * - `{ hydrate }`: fully custom row mapping
750
+ */
751
+ export type RoutineReturns<T = unknown> = (() => EntityName<any>) | (() => EntityName<any>)[] | {
752
+ type: Type<unknown> | Constructor<Type<unknown>>;
753
+ nullable?: boolean;
754
+ } | {
755
+ runtimeType: RoutineRuntimeType;
756
+ columnType?: string;
757
+ nullable?: boolean;
758
+ customType?: Type<unknown> | Constructor<Type<unknown>>;
759
+ } | {
760
+ hydrate: (rows: Dictionary[][], args: T, em: any) => unknown;
761
+ };
762
+ /** JS fallback registered as a UDF on SQLite (better-sqlite3). Functions only. */
763
+ export type RoutineJsBody<T> = (params: T) => unknown;
764
+ export interface RoutineDef<T = any> {
765
+ type: RoutineKind;
766
+ name?: string;
767
+ schema?: string;
768
+ comment?: string;
769
+ security?: RoutineSecurity;
770
+ /** User the routine runs as when `security: 'definer'`. */
771
+ definer?: string;
772
+ deterministic?: boolean;
773
+ /** MySQL/MariaDB only. */
774
+ dataAccess?: RoutineDataAccess;
775
+ language?: 'sql' | 'plpgsql' | 'tsql' | 'plsql' | AnyString;
776
+ /** Mutually exclusive with `expression`. */
777
+ body?: string | Raw | RoutineBodyCallback<T>;
778
+ /** Raw `CREATE PROCEDURE`/`FUNCTION` escape hatch — schema diff can't detect changes. Prefer `body`. */
779
+ expression?: string;
780
+ /** JS fallback registered as a UDF on SQLite (better-sqlite3). Functions only. */
781
+ bodyJs?: RoutineJsBody<T>;
782
+ returns?: RoutineReturns<T>;
783
+ /** Skip selected fields in schema diff; useful when the engine normalises the body differently. */
784
+ ignoreSchemaChanges?: RoutineIgnoreField[];
785
+ }
786
+ export interface RoutineProperty<Owner = any> {
787
+ name: EntityKey<Owner>;
788
+ /** SQL string, or a `Type` instance (resolved to the dialect column type at schema-gen time via `getColumnType`). */
789
+ type: keyof typeof types | AnyString | Type<unknown>;
790
+ runtimeType: EntityProperty['runtimeType'];
791
+ /** Singleton array to align with EntityProperty's `columnTypes`/Type API surface. */
792
+ columnTypes: string[];
793
+ customType?: Type<unknown>;
794
+ direction: RoutineParamDirection;
795
+ /** Required for OUT/INOUT params — wraps the JS-side value in `ScalarReference`. */
796
+ ref?: boolean;
797
+ length?: number;
798
+ precision?: number;
799
+ scale?: number;
800
+ nullable?: boolean;
801
+ /** Default expression used when the caller omits the parameter, where the dialect supports it. */
802
+ defaultRaw?: string;
803
+ /** Original declaration order, used to rebuild positional argument lists. */
804
+ index: number;
805
+ }
806
+ export interface RoutineParamConfig {
807
+ runtimeType?: RoutineRuntimeType;
808
+ type?: keyof typeof types | AnyString | Type<any> | Constructor<Type<any>>;
809
+ direction?: RoutineParamDirection;
810
+ ref?: boolean;
811
+ length?: number;
812
+ precision?: number;
813
+ scale?: number;
814
+ nullable?: boolean;
815
+ defaultRaw?: string;
816
+ /** Marshals the param via `Type.convertToDatabaseValue` inbound and `convertToJSValue` outbound. */
817
+ customType?: Type<unknown> | Constructor<Type<unknown>>;
818
+ }
819
+ /** Routine declaration shape accepted by the {@link Routine} class. */
820
+ export interface RoutineConfig<T = any> extends Omit<RoutineDef<T>, 'name'> {
821
+ name: string;
822
+ /** Order is preserved from `Object.keys`. */
823
+ params?: Record<string, RoutineParamConfig>;
824
+ }
825
+ export type RoutineRuntimeTypeMap = {
826
+ string: string;
827
+ number: number;
828
+ boolean: boolean;
829
+ bigint: bigint;
830
+ Buffer: Buffer;
831
+ Date: Date;
832
+ object: Dictionary;
833
+ any: any;
834
+ };
835
+ /** Narrow union (vs `EntityProperty['runtimeType']`) so literals survive `const` inference and feed args/return inference. */
836
+ export type RoutineRuntimeType = keyof RoutineRuntimeTypeMap;
837
+ type RoutineRuntimeOf<R> = R extends keyof RoutineRuntimeTypeMap ? RoutineRuntimeTypeMap[R] : unknown;
838
+ type Nullify<P, V> = P extends {
839
+ nullable: true;
840
+ } ? V | null : V;
841
+ type StripSqlTypeArgs<S extends string> = S extends `${infer Base}(${string}` ? Base : S;
842
+ type SqlStringTypes = 'varchar' | 'char' | 'character' | 'nvarchar' | 'nchar' | 'text' | 'mediumtext' | 'longtext' | 'tinytext' | 'ntext' | 'clob' | 'nclob' | 'citext' | 'xml' | 'uuid' | 'string' | 'decimal' | 'numeric' | 'money' | 'bigint' | 'int8' | 'bigserial';
843
+ type SqlNumberTypes = 'int' | 'integer' | 'smallint' | 'tinyint' | 'mediumint' | 'int2' | 'int4' | 'serial' | 'smallserial' | 'real' | 'float' | 'float4' | 'float8' | 'double';
844
+ type SqlBooleanTypes = 'boolean' | 'bool' | 'bit';
845
+ type SqlDateTypes = 'date' | 'datetime' | 'datetime2' | 'smalldatetime' | 'timestamp' | 'timestamptz' | 'time' | 'timetz';
846
+ type SqlJsonTypes = 'json' | 'jsonb';
847
+ type SqlBufferTypes = 'blob' | 'tinyblob' | 'mediumblob' | 'longblob' | 'binary' | 'varbinary' | 'bytea' | 'bytes' | 'image' | 'raw';
848
+ type SqlTypeMap = {
849
+ [K in SqlStringTypes]: string;
850
+ } & {
851
+ [K in SqlNumberTypes]: number;
852
+ } & {
853
+ [K in SqlBooleanTypes]: boolean;
854
+ } & {
855
+ [K in SqlDateTypes]: Date;
856
+ } & {
857
+ [K in SqlJsonTypes]: Dictionary;
858
+ } & {
859
+ [K in SqlBufferTypes]: Buffer;
860
+ } & {
861
+ 'character varying': string;
862
+ 'double precision': number;
863
+ 'time with time zone': Date;
864
+ 'timestamp with time zone': Date;
865
+ 'long raw': Buffer;
866
+ };
867
+ type LookupSqlType<K> = K extends keyof SqlTypeMap ? SqlTypeMap[K] : any;
868
+ /**
869
+ * Maps a SQL-flavoured `type` string (e.g. `'varchar(255)'`, `'int'`, `'timestamp'`) to a TS
870
+ * runtime type, used as a fallback when {@link RoutineParamConfig} does not declare an explicit
871
+ * `runtimeType`. Length/precision arguments are stripped, the lowercase token is matched
872
+ * against {@link SqlTypeMap}. `decimal`/`numeric`/`money`/`bigint` default to `string` since
873
+ * drivers typically return them as strings to preserve precision; opt in to `number` or
874
+ * `bigint` via `runtimeType` when the value range is safe. Unrecognised or genuinely
875
+ * ambiguous types (`refcursor`, `sys_refcursor`, …) fall through to `any`.
876
+ */
877
+ export type SqlTypeToTs<S> = S extends string ? Lowercase<S> extends `tinyint(1)${string}` ? boolean : LookupSqlType<StripSqlTypeArgs<Lowercase<S>>> : any;
878
+ /** Strips `null | undefined` from a `Type<JSType, _>` JSType, so e.g. `StringType extends Type<string | null | undefined>` resolves to `string`. */
879
+ type TypeJsType<T> = T extends Type<infer J, any> ? Exclude<J, null | undefined> : T extends new (...args: any[]) => Type<infer J, any> ? Exclude<J, null | undefined> : never;
880
+ /**
881
+ * TS value type for a single routine parameter. Resolution order:
882
+ *
883
+ * 1. explicit `runtimeType`
884
+ * 2. `type` set to a {@link Type} class or instance — JSType is extracted from the Type generic
885
+ * 3. `type` set to a SQL string — mapped via {@link SqlTypeToTs}
886
+ * 4. fallback `any`
887
+ *
888
+ * `ref: true` wraps the result in `ScalarReference`; `nullable: true` adds `| null`.
889
+ */
890
+ export type RoutineParamValue<P> = P extends {
891
+ runtimeType: infer R;
892
+ } ? P extends {
893
+ ref: true;
894
+ } ? ScalarReference<Nullify<P, RoutineRuntimeOf<R>>> : Nullify<P, RoutineRuntimeOf<R>> : P extends {
895
+ type: infer T;
896
+ } ? [TypeJsType<T>] extends [never] ? T extends string ? P extends {
897
+ ref: true;
898
+ } ? ScalarReference<Nullify<P, SqlTypeToTs<T>>> : Nullify<P, SqlTypeToTs<T>> : P extends {
899
+ ref: true;
900
+ } ? ScalarReference<any> : any : P extends {
901
+ ref: true;
902
+ } ? ScalarReference<Nullify<P, TypeJsType<T>>> : Nullify<P, TypeJsType<T>> : P extends {
903
+ ref: true;
904
+ } ? ScalarReference<any> : any;
905
+ /** Default `TArgs` for {@link Routine}; consumers use {@link RoutineArgs}, which honours overrides from {@link Routine.create}. */
906
+ export type RoutineArgsOf<Config> = Config extends {
907
+ params: infer P;
908
+ } ? P extends Record<string, RoutineParamConfig> ? {
909
+ [K in keyof P]: RoutineParamValue<P[K]>;
910
+ } : Record<string, unknown> : Record<string, unknown>;
911
+ /** Default `TReturn` for {@link Routine}; consumers use {@link RoutineReturn}, which honours overrides from {@link Routine.create}. */
912
+ export type RoutineReturnOf<Config> = Config extends {
913
+ returns: infer R;
914
+ } ? R extends {
915
+ runtimeType: infer RT;
916
+ } ? Nullify<R, RoutineRuntimeOf<RT>> : R extends {
917
+ type: infer T;
918
+ } ? [TypeJsType<T>] extends [never] ? unknown : Nullify<R, TypeJsType<T>> : R extends readonly (() => EntityName<any>)[] ? {
919
+ [K in keyof R]: R[K] extends () => EntityName<infer E> ? E[] : never;
920
+ } : R extends () => EntityName<infer E> ? E[] : R extends {
921
+ hydrate: (...args: any[]) => infer H;
922
+ } ? Awaited<H> : void : void;
923
+ export type RoutineArgs<R> = R extends Routine<any, infer A, any> ? A : Record<string, unknown>;
924
+ export type RoutineReturn<R> = R extends Routine<any, any, infer Re> ? Re : unknown;
733
925
  /** Branded string that accepts any string value while preserving autocompletion for known literals. */
734
926
  export type AnyString = string & {};
735
927
  /** Describes a single property (column, relation, or embedded) within an entity's metadata. */
@@ -6,6 +6,7 @@ import { type Logger, type LoggerNamespace, type LoggerOptions } from '../loggin
6
6
  import type { EntityManager } from '../EntityManager.js';
7
7
  import type { Platform } from '../platforms/Platform.js';
8
8
  import type { EntitySchema } from '../metadata/EntitySchema.js';
9
+ import { Routine } from '../metadata/Routine.js';
9
10
  import { MetadataProvider } from '../metadata/MetadataProvider.js';
10
11
  import type { MetadataStorage } from '../metadata/MetadataStorage.js';
11
12
  import type { EventSubscriber } from '../events/EventSubscriber.js';
@@ -27,6 +28,10 @@ export declare class Configuration<D extends IDatabaseDriver = IDatabaseDriver,
27
28
  get<T extends keyof Options<D, EM>, U extends Options<D, EM>[T]>(key: T, defaultValue?: U): U;
28
29
  /** Returns all configuration options. */
29
30
  getAll(): Options<D, EM>;
31
+ /** Validates the `routines` config option on first access and throws on duplicate `(schema, name)` pairs or non-Routine entries. */
32
+ getRoutines(): readonly Routine[];
33
+ hasRoutine(routine: Routine): boolean;
34
+ private normaliseRoutines;
30
35
  /**
31
36
  * Overrides specified configuration value.
32
37
  */
@@ -408,6 +413,13 @@ export interface Options<Driver extends IDatabaseDriver = IDatabaseDriver, EM ex
408
413
  * Can be class references or instances.
409
414
  */
410
415
  subscribers: Iterable<EventSubscriber | Constructor<EventSubscriber>>;
416
+ /**
417
+ * Stored procedures and functions, declared as {@link Routine} instances.
418
+ *
419
+ * @example
420
+ * routines: [HashUser, AddRecord]
421
+ */
422
+ routines: Iterable<Routine>;
411
423
  /**
412
424
  * Global entity filters to apply.
413
425
  * Filters are applied by default unless explicitly disabled.
@@ -4,6 +4,8 @@ import { NullHighlighter } from '../utils/NullHighlighter.js';
4
4
  import { DefaultLogger } from '../logging/DefaultLogger.js';
5
5
  import { colors } from '../logging/colors.js';
6
6
  import { Utils } from '../utils/Utils.js';
7
+ import { Routine } from '../metadata/Routine.js';
8
+ import { MetadataValidator } from '../metadata/MetadataValidator.js';
7
9
  import { MetadataProvider } from '../metadata/MetadataProvider.js';
8
10
  import { NotFoundError } from '../errors.js';
9
11
  import { RequestContext } from './RequestContext.js';
@@ -17,6 +19,7 @@ const DEFAULTS = {
17
19
  entitiesTs: [],
18
20
  extensions: [],
19
21
  subscribers: [],
22
+ routines: [],
20
23
  filters: {},
21
24
  discovery: {
22
25
  warnWhenNoEntities: true,
@@ -137,6 +140,8 @@ export class Configuration {
137
140
  #platform;
138
141
  #cache = new Map();
139
142
  #extensions = new Map();
143
+ #routines = [];
144
+ #routinesNormalised = false;
140
145
  constructor(options, validate = true) {
141
146
  if (options.dynamicImportProvider) {
142
147
  globalThis.dynamicImportProvider = options.dynamicImportProvider;
@@ -181,6 +186,42 @@ export class Configuration {
181
186
  getAll() {
182
187
  return this.#options;
183
188
  }
189
+ /** Validates the `routines` config option on first access and throws on duplicate `(schema, name)` pairs or non-Routine entries. */
190
+ getRoutines() {
191
+ this.normaliseRoutines();
192
+ return this.#routines;
193
+ }
194
+ hasRoutine(routine) {
195
+ this.normaliseRoutines();
196
+ return this.#routines.includes(routine);
197
+ }
198
+ normaliseRoutines() {
199
+ if (this.#routinesNormalised) {
200
+ return;
201
+ }
202
+ const validator = new MetadataValidator();
203
+ const seenKeys = new Set();
204
+ // Stage in a local array so a mid-loop throw doesn't leave `#routines` partially populated;
205
+ // a retry after the user fixes the offending entry would otherwise duplicate earlier ones.
206
+ const collected = [];
207
+ for (const item of this.#options.routines ?? []) {
208
+ if (!Routine.is(item)) {
209
+ throw new Error(`'routines' entry is not a stored routine declaration. Use a Routine class instance.`);
210
+ }
211
+ validator.validateRoutineDefinition(item);
212
+ // Case-fold to match `SchemaComparator.compareRoutines`, which lower-cases the key so
213
+ // dialect-specific folding (Oracle uppercases, PG lowercases) round-trips; otherwise a
214
+ // user could register `'Foo'` and `'foo'` here and have the comparator silently collapse them.
215
+ const key = ((item.schema ? `${item.schema}.` : '') + item.name).toLowerCase();
216
+ if (seenKeys.has(key)) {
217
+ throw new Error(`Duplicate routine '${key}' declared more than once in the 'routines' config. Routine names must be unique within a schema.`);
218
+ }
219
+ seenKeys.add(key);
220
+ collected.push(item);
221
+ }
222
+ this.#routines.push(...collected);
223
+ this.#routinesNormalised = true;
224
+ }
184
225
  /**
185
226
  * Overrides specified configuration value.
186
227
  */
@@ -324,6 +365,10 @@ export class Configuration {
324
365
  metadataCache.enabled ??= useCache;
325
366
  this.#options.clientUrl ??= this.#platform.getDefaultClientUrl();
326
367
  this.#options.implicitTransactions ??= this.#platform.usesImplicitTransactions();
368
+ // Eagerly trigger routine validation so errors surface at init time, not on first call.
369
+ if (validate) {
370
+ this.getRoutines();
371
+ }
327
372
  if (validate && metadataCache.enabled && !metadataCache.adapter) {
328
373
  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.');
329
374
  }
package/utils/Utils.js CHANGED
@@ -141,7 +141,7 @@ export function parseJsonSafe(value) {
141
141
  /** Collection of general-purpose utility methods used throughout the ORM. */
142
142
  export class Utils {
143
143
  static PK_SEPARATOR = '~~~';
144
- static #ORM_VERSION = '7.1.0-dev.43';
144
+ static #ORM_VERSION = '7.1.0-dev.45';
145
145
  /**
146
146
  * Checks if the argument is instance of `Object`. Returns false for arrays.
147
147
  */