Skip to content

Commit

Permalink
feat(core): allow setting logger context on EM level (#5023)
Browse files Browse the repository at this point in the history
The logger context can now be also set on `EntityManager` level, e.g.
via `em.fork()`:

```ts
const fork = em.fork({
  loggerContext: { meaningOfLife: 42 },
});
const res = await fork.findAll(Author);
```

Closes #5022
  • Loading branch information
B4nan committed Dec 15, 2023
1 parent 20151cf commit 7e56104
Show file tree
Hide file tree
Showing 13 changed files with 155 additions and 67 deletions.
15 changes: 12 additions & 3 deletions docs/docs/logging.md
Expand Up @@ -197,10 +197,10 @@ interface LogContext extends Dictionary {

### Providing additional context to a custom logger

If you have implemented your own `LoggerFactory` and need to access additional contextual values inside your customer logger implementation, utilize the `LoggerContext` property of `FindOptions`. Adding additional key/value pairs to that object will make them available inside your custom logger:
If you have implemented your own `LoggerFactory` and need to access additional contextual values inside your customer logger implementation, utilize the `loggerContext` property of `FindOptions`. Adding additional key/value pairs to that object will make them available inside your custom logger:

```ts
const author = await em.findOne(Author, { id: 1 }, { loggerContext: { meaningOfLife: 42 } });
const res = await em.findAll(Author, { loggerContext: { meaningOfLife: 42 } });

// ...

Expand All @@ -210,4 +210,13 @@ class CustomLogger extends DefaultLogger {
// 42
}
}
```
```

The logger context can be also set on `EntityManager` level, e.g. via `em.fork()`:

```ts
const fork = em.fork({
loggerContext: { meaningOfLife: 42 },
});
const res = await fork.findAll(Author); // same as previous example
```
80 changes: 57 additions & 23 deletions packages/core/src/EntityManager.ts
Expand Up @@ -86,10 +86,11 @@ import { EventManager, type FlushEventArgs, TransactionEventBroadcaster } from '
import type { EntityComparator } from './utils/EntityComparator';
import { OptimisticLockError, ValidationError } from './errors';
import type { CacheAdapter } from './cache/CacheAdapter';
import type { LogContext, LoggingOptions } from './logging';

/**
* The EntityManager is the central access point to ORM functionality. It is a facade to all different ORM subsystems
* such as UnitOfWork, Query Language and Repository API.
* such as UnitOfWork, Query Language, and Repository API.
* @template {D} current driver type
*/
export class EntityManager<D extends IDatabaseDriver = IDatabaseDriver> {
Expand All @@ -109,6 +110,7 @@ export class EntityManager<D extends IDatabaseDriver = IDatabaseDriver> {
private readonly resultCache: CacheAdapter;
private filters: Dictionary<FilterDef> = {};
private filterParams: Dictionary<Dictionary> = {};
protected loggerContext?: LoggingOptions;
private transactionContext?: Transaction;
private disableTransactions: boolean;
private flushMode?: FlushMode;
Expand Down Expand Up @@ -207,15 +209,16 @@ export class EntityManager<D extends IDatabaseDriver = IDatabaseDriver> {
}

const em = this.getContext();
options.schema ??= em._schema;
this.prepareOptions(options, em);
await em.tryFlush(entityName, options);
entityName = Utils.className(entityName);
where = await em.processWhere(entityName, where, options, 'read') as FilterQuery<Entity>;
em.validator.validateParams(where);
options.orderBy = options.orderBy || {};
options.populate = em.preparePopulate(entityName, options) as any;
const populate = options.populate as unknown as PopulateOptions<Entity>[];
const cached = await em.tryCache<Entity, Loaded<Entity, Hint, Fields>[]>(entityName, options.cache, [entityName, 'em.find', options, where], options.refresh, true);
const cacheKey = em.cacheKey(entityName, options, 'em.find', where);
const cached = await em.tryCache<Entity, Loaded<Entity, Hint, Fields>[]>(entityName, options.cache, cacheKey, options.refresh, true);

if (cached?.data) {
await em.entityLoader.populate<Entity>(entityName, cached.data as Entity[], populate, {
Expand Down Expand Up @@ -632,7 +635,7 @@ export class EntityManager<D extends IDatabaseDriver = IDatabaseDriver> {

const em = this.getContext();
entityName = Utils.className(entityName);
options.schema ??= em._schema;
this.prepareOptions(options, em);
let entity = em.unitOfWork.tryGetById<Entity>(entityName, where, options.schema);

// query for a not managed entity which is already in the identity map as it
Expand All @@ -656,7 +659,8 @@ export class EntityManager<D extends IDatabaseDriver = IDatabaseDriver> {

em.validator.validateParams(where);
options.populate = em.preparePopulate(entityName, options) as any;
const cached = await em.tryCache<Entity, Loaded<Entity, Hint, Fields>>(entityName, options.cache, [entityName, 'em.findOne', options, where], options.refresh, true);
const cacheKey = em.cacheKey(entityName, options, 'em.findOne', where);
const cached = await em.tryCache<Entity, Loaded<Entity, Hint, Fields>>(entityName, options.cache, cacheKey, options.refresh, true);

if (cached?.data) {
await em.entityLoader.populate<Entity, Fields>(entityName, [cached.data as Entity], options.populate as unknown as PopulateOptions<Entity>[], {
Expand Down Expand Up @@ -755,7 +759,7 @@ export class EntityManager<D extends IDatabaseDriver = IDatabaseDriver> {
*/
async upsert<Entity extends object>(entityNameOrEntity: EntityName<Entity> | Entity, data?: EntityData<Entity> | Entity, options: UpsertOptions<Entity> = {}): Promise<Entity> {
const em = this.getContext(false);
options.schema ??= em._schema;
this.prepareOptions(options, em);

let entityName: EntityName<Entity>;
let where: FilterQuery<Entity>;
Expand Down Expand Up @@ -893,7 +897,7 @@ export class EntityManager<D extends IDatabaseDriver = IDatabaseDriver> {
*/
async upsertMany<Entity extends object>(entityNameOrEntity: EntityName<Entity> | Entity[], data?: (EntityData<Entity> | Entity)[], options: UpsertManyOptions<Entity> = {}): Promise<Entity[]> {
const em = this.getContext(false);
options.schema ??= em._schema;
this.prepareOptions(options, em);

let entityName: string;
let propIndex: number;
Expand Down Expand Up @@ -1137,6 +1141,7 @@ export class EntityManager<D extends IDatabaseDriver = IDatabaseDriver> {
flushMode: options.flushMode,
cloneEventManager: true,
disableTransactions: options.ignoreNestedTransactions,
loggerContext: options.loggerContext,
});
options.ctx ??= em.transactionContext;

Expand Down Expand Up @@ -1231,7 +1236,7 @@ export class EntityManager<D extends IDatabaseDriver = IDatabaseDriver> {
*/
async insert<Entity extends object>(entityNameOrEntity: EntityName<Entity> | Entity, data?: RequiredEntityData<Entity> | Entity, options: NativeInsertUpdateOptions<Entity> = {}): Promise<Primary<Entity>> {
const em = this.getContext(false);
options.schema ??= em._schema;
this.prepareOptions(options, em);

let entityName;

Expand Down Expand Up @@ -1274,7 +1279,7 @@ export class EntityManager<D extends IDatabaseDriver = IDatabaseDriver> {
*/
async insertMany<Entity extends object>(entityNameOrEntities: EntityName<Entity> | Entity[], data?: RequiredEntityData<Entity>[] | Entity[], options: NativeInsertUpdateOptions<Entity> = {}): Promise<Primary<Entity>[]> {
const em = this.getContext(false);
options.schema ??= em._schema;
this.prepareOptions(options, em);

let entityName;

Expand Down Expand Up @@ -1324,7 +1329,7 @@ export class EntityManager<D extends IDatabaseDriver = IDatabaseDriver> {
*/
async nativeUpdate<Entity extends object>(entityName: EntityName<Entity>, where: FilterQuery<Entity>, data: EntityData<Entity>, options: UpdateOptions<Entity> = {}): Promise<number> {
const em = this.getContext(false);
options.schema ??= em._schema;
this.prepareOptions(options, em);

entityName = Utils.className(entityName);
data = QueryHelper.processObjectParams(data);
Expand All @@ -1341,7 +1346,7 @@ export class EntityManager<D extends IDatabaseDriver = IDatabaseDriver> {
*/
async nativeDelete<Entity extends object>(entityName: EntityName<Entity>, where: FilterQuery<Entity>, options: DeleteOptions<Entity> = {}): Promise<number> {
const em = this.getContext(false);
options.schema ??= em._schema;
this.prepareOptions(options, em);

entityName = Utils.className(entityName);
where = await em.processWhere(entityName, where, options, 'delete');
Expand Down Expand Up @@ -1548,7 +1553,8 @@ export class EntityManager<D extends IDatabaseDriver = IDatabaseDriver> {
em.validator.validateParams(where);
delete (options as FindOptions<Entity>).orderBy;

const cached = await em.tryCache<Entity, number>(entityName, options.cache, [entityName, 'em.count', options, where]);
const cacheKey = em.cacheKey(entityName, options, 'em.count', where);
const cached = await em.tryCache<Entity, number>(entityName, options.cache, cacheKey);

if (cached?.data) {
return cached.data as number;
Expand Down Expand Up @@ -1713,9 +1719,9 @@ export class EntityManager<D extends IDatabaseDriver = IDatabaseDriver> {
}

const em = this.getContext();
options.schema ??= em._schema;
this.prepareOptions(options, em);
const entityName = (arr[0] as Dictionary).constructor.name;
const preparedPopulate = em.preparePopulate<Entity, Hint>(entityName, { populate: populate as any });
const preparedPopulate = em.preparePopulate<Entity>(entityName, { populate: populate as any });
await em.entityLoader.populate(entityName, arr, preparedPopulate, options as EntityLoaderOptions<Entity>);

return entities as any;
Expand Down Expand Up @@ -1747,6 +1753,7 @@ export class EntityManager<D extends IDatabaseDriver = IDatabaseDriver> {

fork.filters = { ...em.filters };
fork.filterParams = Utils.copy(em.filterParams);
fork.loggerContext = Utils.merge({}, em.loggerContext, options.loggerContext);
fork._schema = options.schema ?? em._schema;

if (!options.clear) {
Expand Down Expand Up @@ -1895,7 +1902,7 @@ export class EntityManager<D extends IDatabaseDriver = IDatabaseDriver> {
});
}

const preparedPopulate = this.preparePopulate<T, P, F>(meta.className, options);
const preparedPopulate = this.preparePopulate<T>(meta.className, options);
await this.entityLoader.populate(meta.className, [entity], preparedPopulate, {
...options as Dictionary,
...this.getPopulateWhere<T>(where as ObjectQuery<T>, options),
Expand All @@ -1919,11 +1926,7 @@ export class EntityManager<D extends IDatabaseDriver = IDatabaseDriver> {
}, [] as string[]);
}

private preparePopulate<
Entity extends object,
Hint extends string = never,
Fields extends string = '*',
>(entityName: string, options: Pick<FindOptions<Entity, Hint, Fields>, 'populate' | 'strategy' | 'fields' | 'flags'>): PopulateOptions<Entity>[] {
private preparePopulate<Entity extends object>(entityName: string, options: Pick<FindOptions<Entity, any, any>, 'populate' | 'strategy' | 'fields' | 'flags'>): PopulateOptions<Entity>[] {
if (options.populate === false) {
return [];
}
Expand Down Expand Up @@ -2035,6 +2038,35 @@ export class EntityManager<D extends IDatabaseDriver = IDatabaseDriver> {
return !!options.populate;
}

protected prepareOptions(options: FindOptions<any, any, any> | FindOneOptions<any, any, any>, em: this): void {
options.schema ??= em._schema;
options.logging = Utils.merge(
{ id: this.id },
em.loggerContext,
options.loggerContext,
options.logging,
);
}

/**
* @internal
*/
cacheKey<T extends object>(
entityName: string,
options: FindOptions<T, any, any> | FindOneOptions<T, any, any> | CountOptions<T, any>,
method: string,
where: FilterQuery<T>,
): unknown[] {
const { ...opts } = options;

// ignore some irrelevant options, e.g. logger context can contain dynamic data for the same query
for (const k of ['ctx', 'strategy', 'flushMode', 'logging', 'loggerContext']) {
delete opts[k as keyof typeof opts];
}

return [entityName, method, opts, where];
}

/**
* @internal
*/
Expand Down Expand Up @@ -2154,20 +2186,22 @@ export interface MergeOptions {
}

export interface ForkOptions {
/** do we want clear identity map? defaults to true */
/** do we want a clear identity map? defaults to true */
clear?: boolean;
/** use request context? should be used only for top level request scope EM, defaults to false */
useContext?: boolean;
/** do we want to use fresh EventManager instance? defaults to false (global instance) */
freshEventManager?: boolean;
/** do we want to clone current EventManager instance? defaults to false (global instance) */
cloneEventManager?: boolean;
/** use this flag to ignore current async context - this is required if we want to call `em.fork()` inside the `getContext` handler */
/** use this flag to ignore the current async context - this is required if we want to call `em.fork()` inside the `getContext` handler */
disableContextResolution?: boolean;
/** set flush mode for this fork, overrides the global option, can be overridden locally via FindOptions */
/** set flush mode for this fork, overrides the global option can be overridden locally via FindOptions */
flushMode?: FlushMode;
/** disable transactions for this fork */
disableTransactions?: boolean;
/** default schema to use for this fork */
schema?: string;
/** default logger context, can be overridden via {@apilink FindOptions} */
loggerContext?: LogContext;
}
13 changes: 7 additions & 6 deletions packages/core/src/drivers/IDatabaseDriver.ts
Expand Up @@ -94,14 +94,13 @@ export type EntityField<T, P extends string = '*'> = keyof T | '*' | AutoPath<T,

export type OrderDefinition<T> = (QueryOrderMap<T> & { 0?: never }) | QueryOrderMap<T>[];

export interface FindAllOptions<T, P extends string = never, F extends string = never> extends FindOptions<T, P, F> {
export interface FindAllOptions<T, P extends string = never, F extends string = '*'> extends FindOptions<T, P, F> {
where?: FilterQuery<T>;
}

export type FilterOptions = Dictionary<boolean | Dictionary> | string[] | boolean;

export interface FindOptions<T, P extends string = never, F extends string = never> {
where?: FilterQuery<T>;
export interface FindOptions<T, P extends string = never, F extends string = '*'> {
populate?: Populate<T, P>;
populateWhere?: ObjectQuery<T> | PopulateHint | `${PopulateHint}`;
populateOrderBy?: OrderDefinition<T>;
Expand Down Expand Up @@ -148,15 +147,15 @@ export interface FindOptions<T, P extends string = never, F extends string = nev
logging?: LoggingOptions;
}

export interface FindByCursorOptions<T extends object, P extends string = never, F extends string = never> extends Omit<FindOptions<T, P, F>, 'limit' | 'offset'> {
export interface FindByCursorOptions<T extends object, P extends string = never, F extends string = '*'> extends Omit<FindOptions<T, P, F>, 'limit' | 'offset'> {
}

export interface FindOneOptions<T extends object, P extends string = never, F extends string = never> extends Omit<FindOptions<T, P, F>, 'limit' | 'lockMode'> {
export interface FindOneOptions<T extends object, P extends string = never, F extends string = '*'> extends Omit<FindOptions<T, P, F>, 'limit' | 'lockMode'> {
lockMode?: LockMode;
lockVersion?: number | Date;
}

export interface FindOneOrFailOptions<T extends object, P extends string = never, F extends string = never> extends FindOneOptions<T, P, F> {
export interface FindOneOrFailOptions<T extends object, P extends string = never, F extends string = '*'> extends FindOneOptions<T, P, F> {
failHandler?: (entityName: string, where: Dictionary | IPrimaryKey | any) => Error;
strict?: boolean;
}
Expand Down Expand Up @@ -199,6 +198,8 @@ export interface CountOptions<T extends object, P extends string = never> {
comments?: string | string[];
/** sql only */
hintComments?: string | string[];
loggerContext?: LogContext;
logging?: LoggingOptions;
}

export interface UpdateOptions<T> {
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/enums.ts
@@ -1,5 +1,6 @@
import type { Dictionary, EntityKey, ExpandProperty } from './typings';
import type { Transaction } from './connections';
import type { LogContext } from './logging';

export enum FlushMode {
/** The `EntityManager` delays the flush until the current Transaction is committed. */
Expand Down Expand Up @@ -190,6 +191,7 @@ export interface TransactionOptions {
clear?: boolean;
flushMode?: FlushMode;
ignoreNestedTransactions?: boolean;
loggerContext?: LogContext;
}

export abstract class PlainObject {
Expand Down
8 changes: 4 additions & 4 deletions packages/core/src/logging/Logger.ts
Expand Up @@ -57,12 +57,12 @@ export interface LoggerOptions {
}

/**
* Logger options to modify format output and overrides, including a label and additional properties that can be accessed by custom loggers
* Logger options to modify format output and overrides, including a label and additional properties that can be accessed by custom loggers.
*
* Differs from {@link LoggerOptions} in terms of how they are used; this type is primarily a public type meant to be used within methods like `EntityManager.Find`
* Differs from {@apilink LoggerOptions} in terms of how they are used; this type is primarily a public type meant to be used within methods like `em.find()`.
*
* @example
* await em.findOne(User, 1, { loggerContext: { label: 'user middleware' } };
* await em.findOne(User, 1, { logger: { label: 'user middleware' } };
* // [query] (user middleware) select * from user where id = 1;
*/
export type LoggingOptions = Pick<LogContext, 'label' | 'enabled' | 'debugMode'> & Dictionary;
export type LoggingOptions = Pick<LogContext, 'label' | 'enabled' | 'debugMode'>;
8 changes: 5 additions & 3 deletions packages/core/src/utils/RequestContext.ts
@@ -1,8 +1,9 @@
import { AsyncLocalStorage } from 'async_hooks';
import type { EntityManager } from '../EntityManager';
import { type LoggingOptions } from '../logging/Logger';

/**
* Uses `AsyncLocalStorage` to create async context that holds current EM fork.
* Uses `AsyncLocalStorage` to create async context that holds the current EM fork.
*/
export class RequestContext {

Expand Down Expand Up @@ -47,9 +48,9 @@ export class RequestContext {
const forks = new Map<string, EntityManager>();

if (Array.isArray(em)) {
em.forEach(em => forks.set(em.name, em.fork({ useContext: true, schema: options.schema })));
em.forEach(em => forks.set(em.name, em.fork({ useContext: true, ...options })));
} else {
forks.set(em.name, em.fork({ useContext: true, schema: options.schema }));
forks.set(em.name, em.fork({ useContext: true, ...options }));
}

return new RequestContext(forks);
Expand All @@ -59,4 +60,5 @@ export class RequestContext {

export interface CreateContextOptions {
schema?: string;
loggerContext?: LoggingOptions;
}
6 changes: 3 additions & 3 deletions packages/knex/src/AbstractSqlConnection.ts
Expand Up @@ -144,7 +144,7 @@ export abstract class AbstractSqlConnection extends Connection {
}
}

async execute<T extends QueryResult | EntityData<AnyEntity> | EntityData<AnyEntity>[] = EntityData<AnyEntity>[]>(queryOrKnex: string | Knex.QueryBuilder | Knex.Raw, params: unknown[] = [], method: 'all' | 'get' | 'run' = 'all', ctx?: Transaction, logging?: LoggingOptions): Promise<T> {
async execute<T extends QueryResult | EntityData<AnyEntity> | EntityData<AnyEntity>[] = EntityData<AnyEntity>[]>(queryOrKnex: string | Knex.QueryBuilder | Knex.Raw, params: unknown[] = [], method: 'all' | 'get' | 'run' = 'all', ctx?: Transaction, loggerContext?: LoggingOptions): Promise<T> {
await this.ensureConnection();

if (Utils.isObject<Knex.QueryBuilder | Knex.Raw>(queryOrKnex)) {
Expand All @@ -155,7 +155,7 @@ export abstract class AbstractSqlConnection extends Connection {
}

const formatted = this.platform.formatQuery(queryOrKnex, params);
const sql = this.getSql(queryOrKnex, formatted, logging);
const sql = this.getSql(queryOrKnex, formatted, loggerContext);
return this.executeQuery<T>(sql, async () => {
const query = this.getKnex().raw(formatted);

Expand All @@ -165,7 +165,7 @@ export abstract class AbstractSqlConnection extends Connection {

const res = await query;
return this.transformRawResult<T>(res, method);
}, { query: queryOrKnex, params, ...logging });
}, { query: queryOrKnex, params, ...loggerContext });
}

/**
Expand Down

0 comments on commit 7e56104

Please sign in to comment.