nestjs-log-decorator 1.4.0 → 1.5.0

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.
package/README.md CHANGED
@@ -25,14 +25,15 @@ TypeScript decorators that eliminate logging boilerplate from NestJS application
25
25
 
26
26
  ## Description
27
27
 
28
- `@Log()` decorator replaces the try-catch logging pattern in NestJS service methods by automatically logging method invocation on return or error.
28
+ `@Log()` decorator replaces the try-catch logging pattern in NestJS service methods by automatically logging method success and errors, with optional invocation and result logging.
29
29
 
30
30
  **Key Features**
31
31
 
32
32
  - By default uses structured output
33
33
  - Prettifies Axios errors
34
34
  - Zero configuration
35
- - Zero dependencies
35
+ - Minimal dependencies
36
+ - Uses default `@nestjs/common` Logger instance
36
37
 
37
38
  ## Installation
38
39
 
@@ -42,19 +43,17 @@ npm install nestjs-log-decorator @nestjs/common
42
43
 
43
44
  ## Quick Start
44
45
 
46
+ Simply apply `@Log()` to your class or method:
47
+
45
48
  ```typescript
46
- import { Logger } from '@nestjs/common';
47
49
  import { Log } from 'nestjs-log-decorator';
48
50
 
49
51
  class UserService {
50
- readonly logger = new Logger(UserService.name);
51
-
52
52
  @Log()
53
53
  createUser(name: string, email: string) {
54
54
  return { id: 1, name, email };
55
55
  }
56
56
  }
57
-
58
57
  ```
59
58
 
60
59
  Once a service method is called, it will log the method invocation with all arguments.
@@ -104,18 +103,43 @@ const result = await resultPromise;
104
103
  // [UserService] { method: 'createUser', state: 'success', args: { name: 'John' } }
105
104
  ```
106
105
 
106
+ ### Result Logging
107
+
108
+ If you want to include method results in success logs, use the `result` option.
109
+
110
+ ```typescript
111
+ class UserService {
112
+ // Logs result as-is
113
+ @Log({ result: true })
114
+ findUser(id: number) {
115
+ return { id, name: 'John', email: 'john@example.com' };
116
+ }
117
+
118
+ // Logs formatted result
119
+ @Log({
120
+ result: (res: { id: number; name: string; email: string }) => ({ id: res.id, name: res.name }),
121
+ })
122
+ findPublicUser(id: number) {
123
+ return { id, name: 'John', email: 'john@example.com' };
124
+ }
125
+ }
126
+ ```
127
+
128
+ Example success output:
129
+
130
+ ```typescript
131
+ // [UserService] { method: 'findUser', state: 'success', args: { id: 1 }, result: { id: 1, name: 'John', email: 'john@example.com' } }
132
+ // [UserService] { method: 'findPublicUser', state: 'success', args: { id: 1 }, result: { id: 1, name: 'John' } }
133
+ ```
134
+
107
135
  ### Complete Example
108
136
 
109
- After installation, no additional configuration is needed, just make sure that your class has a public `logger` property that implements the default NestJS Logger interface.
137
+ After installation, no additional configuration is needed. If the class has `logger` properly, the `@Log()` decorator will use it log method. If the logger is missing, the decorator will inject `@nestjs/common` Logger instance using the class name as the context.
110
138
 
111
139
  ```typescript
112
- import { Logger } from '@nestjs/common';
113
140
  import { Log } from 'nestjs-log-decorator';
114
141
 
115
-
116
142
  class PaymentService {
117
- // `logger` property will be used by decorator
118
- readonly logger = new Logger(PaymentService.name);
119
143
 
120
144
  @Log()
121
145
  async processPayment(amount: number, currency: string) {
@@ -130,7 +154,25 @@ class PaymentService {
130
154
  }
131
155
  ```
132
156
 
133
- The `logger` property is public due to TypeScript validation limitations. TS cannot check if a private property is valid, but it should still work even if the property is private.
157
+ #### Explicit Logger (Optional)
158
+
159
+ If you need a custom logger (e.g., for testing or a different context), you can still define your own:
160
+
161
+ ```typescript
162
+ import { Logger } from '@nestjs/common';
163
+ import { Log } from 'nestjs-log-decorator';
164
+
165
+ @Log()
166
+ class PaymentService {
167
+ // Explicit logger takes precedence over auto-injected one
168
+ readonly logger = new Logger('CustomPaymentContext');
169
+
170
+ async processPayment(amount: number, currency: string) {
171
+ // Logs using the explicit logger with 'CustomPaymentContext' context
172
+ return await this.gateway.processPayment(amount, currency);
173
+ }
174
+ }
175
+ ```
134
176
 
135
177
  ## How It Works
136
178
 
@@ -141,38 +183,38 @@ The `@Log()` decorator wraps your methods with automatic try-catch logging. It e
141
183
  │ @Log() Decorator Flow │
142
184
  └─────────────────────────────────────────────────────────────────────────────┘
143
185
 
144
- Method Call
145
-
146
-
147
- ┌──────────────────┐
148
- │ Extract Args │ ──▶ { id: 1, name: 'John' }
149
- │ (auto or custom) │
150
- └────────┬─────────┘
151
-
152
-
153
- ┌──────────────────┐ ┌─────────────────────────────────────┐
154
- │ onInvoke: true? │──YES─▶│ logger.log({ state: 'invoked' }) │
155
- └────────┬─────────┘ └─────────────────────────────────────┘
156
- │ NO
157
-
158
- ┌──────────────────────┐
159
- │ Execute Original │
160
- │ Method (sync/async) │
161
- └────────┬─────────────┘
162
-
163
- ┌────┴────┐
164
- ▼ ▼
165
- SUCCESS ERROR
166
- │ │
167
- ▼ ▼
168
- ┌────────┐ ┌──────────────────────────────────────────────────────┐
169
- │log() │ │ logger.error({ state: 'error', error: prettify(e) })│
170
- │success │ │ (Axios errors auto-prettified) │
171
- └────────┘ └──────────────────────────────────────────────────────┘
172
- │ │
173
- ▼ ▼
174
- Return Re-throw
175
- Result Error
186
+ Method Call
187
+
188
+
189
+ ┌──────────────────┐
190
+ │ Extract Args │ ──▶ { id: 1, name: 'John' }
191
+ │ (auto or custom) │
192
+ └────────┬─────────┘
193
+
194
+
195
+ ┌──────────────────┐ ┌─────────────────────────────────────┐
196
+ │ onInvoke: true? │──YES─▶│ logger.log({ state: 'invoked' }) │
197
+ └────────┬─────────┘ └─────────────────────────────────────┘
198
+ │ NO
199
+
200
+ ┌──────────────────────┐
201
+ │ Execute Original │
202
+ │ Method (sync/async) │
203
+ └────────┬─────────────┘
204
+
205
+ ┌────┴────┐
206
+ ▼ ▼
207
+ SUCCESS ERROR
208
+ │ │
209
+ ▼ ▼
210
+ ┌────────┐ ┌──────────────────────────────────────────────────────┐
211
+ │log() │ │ logger.error({ state: 'error', error: prettify(e) })│
212
+ │success │ │ (Axios errors auto-prettified) │
213
+ └────────┘ └──────────────────────────────────────────────────────┘
214
+ │ │
215
+ ▼ ▼
216
+ Return Re-throw
217
+ Result Error
176
218
  ```
177
219
 
178
220
  ## Usage
@@ -182,9 +224,9 @@ The `@Log()` decorator wraps your methods with automatic try-catch logging. It e
182
224
  Apply `@Log()` to specific methods for granular control:
183
225
 
184
226
  ```typescript
185
- class DataService {
186
- readonly logger = new Logger(DataService.name);
227
+ import { Log } from 'nestjs-log-decorator';
187
228
 
229
+ class DataService {
188
230
  @Log()
189
231
  async fetchData(id: number) {
190
232
  // This method is logged
@@ -203,14 +245,11 @@ class DataService {
203
245
  If you want to log all methods in a class, use the `@Log()` decorator on its definition:
204
246
 
205
247
  ```typescript
206
- import { Logger } from '@nestjs/common';
207
248
  import { Log } from 'nestjs-log-decorator';
208
249
 
209
250
  @Log()
210
251
  @Injectable()
211
252
  class PaymentService {
212
- readonly logger = new Logger(PaymentService.name);
213
-
214
253
  processPayment(amount: number, currency: string) {
215
254
  // Automatically logged on success or error
216
255
  return { status: 'completed', amount, currency };
@@ -232,8 +271,6 @@ import { Log, NoLog } from 'nestjs-log-decorator';
232
271
 
233
272
  @Log()
234
273
  class UserService {
235
- readonly logger = new Logger(UserService.name);
236
-
237
274
  createUser(name: string) {
238
275
  // Logged
239
276
  return { name };
@@ -268,8 +305,6 @@ Class-level with `onInvoke`:
268
305
  ```typescript
269
306
  @Log({ onInvoke: true })
270
307
  class ApiService {
271
- readonly logger = new Logger(ApiService.name);
272
-
273
308
  // All methods will log invocation + completion
274
309
  }
275
310
  ```
@@ -289,8 +324,6 @@ interface LargePayload {
289
324
  }
290
325
 
291
326
  class SyncService {
292
- readonly logger = new Logger(SyncService.name);
293
-
294
327
  // Only log the ID, exclude the large payload
295
328
  @Log({ args: (id: number, _payload: LargePayload) => ({ id }) })
296
329
  async syncData(id: number, payload: LargePayload) {
@@ -319,6 +352,31 @@ class SyncService {
319
352
  [SyncService] { method: 'lookupUser', state: 'success', args: '1:John' }
320
353
  ```
321
354
 
355
+ ### `result` — Success Result Logging
356
+
357
+ Control how return values are included in success logs:
358
+
359
+ - `result: true` logs the raw return value
360
+ - `result: (value) => ...` logs a formatted value
361
+
362
+ ```typescript
363
+ class PaymentService {
364
+ // Log full return value
365
+ @Log({ result: true })
366
+ createPayment(id: number) {
367
+ return { id, status: 'success', cardToken: 'tok_123' };
368
+ }
369
+
370
+ // Log only safe result fields
371
+ @Log({
372
+ result: (res: { id: number; status: string; cardToken: string }) => ({ id: res.id, status: res.status }),
373
+ })
374
+ createPaymentSafe(id: number) {
375
+ return { id, status: 'success', cardToken: 'tok_123' };
376
+ }
377
+ }
378
+ ```
379
+
322
380
  ## Log Format
323
381
 
324
382
  All logs are structured JSON objects:
@@ -329,7 +387,9 @@ All logs are structured JSON objects:
329
387
  {
330
388
  method: 'methodName',
331
389
  state: 'success',
332
- args: { param1: value1, param2: value2 }
390
+ args: { param1: value1, param2: value2 },
391
+ // Present only when `result` option is configured
392
+ result: { any: 'value' }
333
393
  }
334
394
  ```
335
395
 
@@ -437,12 +497,13 @@ async fetchData(url: string) {
437
497
 
438
498
  ### `Log(options?)`
439
499
 
440
- Decorator that can be applied to classes or methods.
500
+ Decorator that can be applied to classes or methods. When applied to a class, by default all methods are logged.
441
501
 
442
502
  | Option | Type | Default | Description |
443
503
  |--------|------|---------|-------------|
444
504
  | `onInvoke` | `boolean` | `false` | Log method invocation before execution |
445
505
  | `args` | `(...args) => any` | `undefined` | Custom function to format logged arguments |
506
+ | `result` | `true \| (result) => any` | `undefined` | Include and optionally format successful method result |
446
507
 
447
508
  ### `NoLog()`
448
509
 
@@ -459,7 +520,7 @@ import { Log, NoLog, LogOptions, Loggable, isLoggable } from 'nestjs-log-decorat
459
520
  | `Log` | Decorator | Main logging decorator |
460
521
  | `NoLog` | Decorator | Exclude method from logging |
461
522
  | `LogOptions` | Interface | Options for `@Log()` decorator |
462
- | `Loggable` | Interface | Interface for classes with a `logger` property |
523
+ | `Loggable` | Interface | Interface for classes with a `logger` property (optional) |
463
524
  | `isLoggable` | Function | Type guard to check if instance has logger |
464
525
 
465
526
  ## Advanced Example
@@ -471,6 +532,7 @@ import { Log, NoLog } from 'nestjs-log-decorator';
471
532
  @Log()
472
533
  @Injectable()
473
534
  export class OrderService {
535
+ // Optional: explicit logger takes precedence over auto-injected one
474
536
  readonly logger = new Logger(OrderService.name);
475
537
 
476
538
  constructor(
@@ -485,9 +547,9 @@ export class OrderService {
485
547
  }
486
548
 
487
549
  // Logged with invocation + custom args (exclude sensitive card data)
488
- @Log({
489
- onInvoke: true,
490
- args: (orderId: number, _cardDetails: CardDetails) => ({ orderId })
550
+ @Log({
551
+ onInvoke: true,
552
+ args: (orderId: number, _cardDetails: CardDetails) => ({ orderId })
491
553
  })
492
554
  async processPayment(orderId: number, cardDetails: CardDetails) {
493
555
  const result = await this.paymentGateway.charge(orderId, cardDetails);
package/dist/index.cjs CHANGED
@@ -1,4 +1,5 @@
1
- require("@nestjs/common");
1
+ let base_decorators = require("base-decorators");
2
+ let _nestjs_common = require("@nestjs/common");
2
3
 
3
4
  //#region src/axios/axios.stub.ts
4
5
  /**
@@ -75,12 +76,13 @@ var LogWrapper = class {
75
76
  ...message ? { message } : {}
76
77
  });
77
78
  }
78
- success(message) {
79
+ success(message, additionalFields = {}) {
79
80
  this.logger.log({
80
81
  method: this.method,
81
82
  state: "success",
82
83
  args: this.args,
83
- ...message ? { message } : {}
84
+ ...message ? { message } : {},
85
+ ...additionalFields
84
86
  });
85
87
  }
86
88
  error(error, message) {
@@ -108,95 +110,24 @@ const isLoggable = (instance) => {
108
110
  return typeof instance === "object" && instance !== null && "logger" in instance;
109
111
  };
110
112
  /**
111
- * Creates a LogWrapper instance with validated logger and built args object.
112
- * @internal
113
- */
114
- const createLogWrapper = (instance, className, methodName, argsObject) => {
115
- if (!isLoggable(instance)) throw new Error(`Logger not found in ${className}. Please add: readonly logger = new Logger(${className}.name)`);
116
- return new LogWrapper(instance.logger, methodName, argsObject);
117
- };
118
- /**
119
- * Builds an object mapping parameter names to their values.
120
- *
121
- * Creates a record where keys are parameter names and values are the
122
- * corresponding argument values passed to the function.
123
- *
124
- * @param parameterNames - Array of parameter names
125
- * @param args - Array of argument values
126
- * @returns Object mapping parameter names to values
127
- *
128
- * @example
129
- * buildArgsObject(['id', 'name'], [1, 'John'])
130
- * // Returns: { id: 1, name: 'John' }
131
- *
132
- * @internal
133
- */
134
- const buildArgsObject = (parameterNames, args) => {
135
- if (args.length === 0 && parameterNames.length === 0) return;
136
- const argsObject = {};
137
- parameterNames.forEach((paramName, index) => {
138
- if (index < args.length) argsObject[paramName] = args[index];
139
- });
140
- return argsObject;
141
- };
142
-
143
- //#endregion
144
- //#region src/decorate/applyToMethod.ts
145
- /**
146
- * Applies logging to a single method.
147
- * @internal
148
- */
149
- const applyToMethod = (target, propertyKey, descriptor, { onInvoke: shouldLogInvoke = false, args: formatArgs } = {}) => {
150
- const originalMethod = descriptor.value;
151
- const parameterNames = getParameterNames(originalMethod);
152
- descriptor.value = function(...args) {
153
- const argsObject = formatArgs ? formatArgs(...args) : buildArgsObject(parameterNames, args);
154
- const logWrapper = createLogWrapper(this, target.constructor.name, propertyKey, argsObject);
155
- if (shouldLogInvoke) logWrapper.invoked();
156
- try {
157
- const result = originalMethod.apply(this, args);
158
- if (result instanceof Promise) return handleAsyncExecution(result, logWrapper);
159
- logWrapper.success();
160
- return result;
161
- } catch (error) {
162
- logWrapper.error(error);
163
- throw error;
164
- }
165
- };
166
- return descriptor;
167
- };
168
- /**
169
- * Extracts parameter names from a function signature.
170
- *
171
- * This function parses the function's string representation to extract
172
- * parameter names, handling TypeScript type annotations and default values.
113
+ * Creates a LogWrapper instance for structured method logging.
173
114
  *
174
- * @param func - The function to extract parameter names from
175
- * @returns Array of parameter names
115
+ * When the target instance already has a `logger` property (i.e., satisfies
116
+ * the {@link Loggable} interface), that logger is used directly. Otherwise,
117
+ * a new `Logger` from `@nestjs/common` is created with `className` as its
118
+ * context and assigned to `instance.logger`, so subsequent calls reuse it.
176
119
  *
177
- * @example
178
- * function example(id: number, name: string = 'default') {}
179
- * getParameterNames(example) // Returns: ['id', 'name']
120
+ * @param instance - The class instance whose method is being logged
121
+ * @param className - The name of the class (used as Logger context if auto-injected)
122
+ * @param methodName - The name of the method being logged
123
+ * @param argsObject - Formatted arguments to include in the log entry
124
+ * @returns A configured {@link LogWrapper} ready for invoked/success/error calls
180
125
  *
181
126
  * @internal
182
127
  */
183
- const getParameterNames = (func) => {
184
- const match = func.toString().match(/\(([^)]*)\)/);
185
- if (!match?.[1]) return [];
186
- return match[1].split(",").map((param) => param.trim().split(/[=:]/)[0].trim()).filter((param) => param.length > 0);
187
- };
188
- /**
189
- * Handles execution of async methods with proper logging.
190
- * @internal
191
- */
192
- const handleAsyncExecution = (result, logWrapper) => {
193
- return result.then((value) => {
194
- logWrapper.success();
195
- return value;
196
- }).catch((error) => {
197
- logWrapper.error(error);
198
- throw error;
199
- });
128
+ const createLogWrapper = (instance, className, methodName, argsObject) => {
129
+ if (!isLoggable(instance)) instance.logger = new _nestjs_common.Logger(className);
130
+ return new LogWrapper(instance.logger, methodName, argsObject);
200
131
  };
201
132
 
202
133
  //#endregion
@@ -207,23 +138,6 @@ const handleAsyncExecution = (result, logWrapper) => {
207
138
  */
208
139
  const NO_LOG_METADATA_KEY = Symbol("noLog");
209
140
 
210
- //#endregion
211
- //#region src/decorate/applyToClass.ts
212
- /**
213
- * Applies logging to all methods in a class.
214
- * @internal
215
- */
216
- const applyToClass = (target, options) => {
217
- Object.getOwnPropertyNames(target.prototype).forEach((propertyName) => {
218
- if (propertyName === "constructor") return;
219
- const descriptor = Object.getOwnPropertyDescriptor(target.prototype, propertyName);
220
- if (!descriptor || typeof descriptor.value !== "function") return;
221
- if (descriptor.value[NO_LOG_METADATA_KEY] === true) return;
222
- applyToMethod(target.prototype, propertyName, descriptor, options);
223
- Object.defineProperty(target.prototype, propertyName, descriptor);
224
- });
225
- };
226
-
227
141
  //#endregion
228
142
  //#region src/log.decorator.ts
229
143
  /**
@@ -233,6 +147,7 @@ const applyToClass = (target, options) => {
233
147
  *
234
148
  * **Usage on Classes:**
235
149
  * When applied to a class, wraps all methods in the class with logging.
150
+ * A `logger` property is auto-injected if not already defined.
236
151
  *
237
152
  * **Usage on Methods:**
238
153
  * When applied to a method, wraps only that specific method with logging.
@@ -242,13 +157,10 @@ const applyToClass = (target, options) => {
242
157
  * - Successful completion with arguments
243
158
  * - Errors with arguments and error details (Axios errors are automatically prettified)
244
159
  *
245
- * **Requirements:**
246
- * The class must have a `logger` property (typically a NestJS Logger instance).
247
- *
248
160
  * **Log Format:**
249
161
  * All logs are output as structured objects with the following format:
250
162
  * - Invocation: `{ method: string, state: 'invoked', args: Record<string, any> }` (only when `onInvoke: true`)
251
- * - Success: `{ method: string, state: 'success', args: Record<string, any> }`
163
+ * - Success: `{ method: string, state: 'success', args: Record<string, any>, result?: unknown }`
252
164
  * - Error: `{ method: string, state: 'error', args: Record<string, any>, error: Error | PrettifiedAxiosError }`
253
165
  *
254
166
  * **Custom Argument Formatting:**
@@ -257,6 +169,11 @@ const applyToClass = (target, options) => {
257
169
  * - Logging only specific arguments
258
170
  * - Transforming sensitive data before logging
259
171
  *
172
+ * **Result Logging:**
173
+ * Use the `result` option to include successful return values in success logs.
174
+ * - `result: true` logs the raw returned value
175
+ * - `result: (value) => ...` logs a formatted value
176
+ *
260
177
  * **Error Handling:**
261
178
  * - Axios errors are automatically prettified using `prettifyAxiosError` to provide structured error information
262
179
  * - Regular errors are logged as-is
@@ -269,11 +186,9 @@ const applyToClass = (target, options) => {
269
186
  * - Methods with no arguments
270
187
  *
271
188
  * @example
272
- * // Class-level usage - logs all methods
189
+ * // Class-level usage - logs all methods, logger auto-injected
273
190
  * @Log()
274
191
  * class UserService {
275
- * readonly logger = new Logger(UserService.name)
276
- *
277
192
  * createUser(name: string, email: string) {
278
193
  * return { id: 1, name, email }
279
194
  * }
@@ -286,8 +201,6 @@ const applyToClass = (target, options) => {
286
201
  * @example
287
202
  * // Method-level usage - logs only specific method
288
203
  * class DataService {
289
- * readonly logger = new Logger(DataService.name)
290
- *
291
204
  * @Log({ onInvoke: true })
292
205
  * async fetchData(id: number) {
293
206
  * const data = await this.repository.findById(id)
@@ -299,8 +212,7 @@ const applyToClass = (target, options) => {
299
212
  * return 'helper'
300
213
  * }
301
214
  * }
302
- *
303
- * @example
215
+ *
304
216
  * // Error handling with regular errors
305
217
  * class PaymentService {
306
218
  * readonly logger = new Logger(PaymentService.name)
@@ -346,17 +258,15 @@ const applyToClass = (target, options) => {
346
258
  * @example
347
259
  * // Custom argument formatting - exclude large objects from logs
348
260
  * class SyncService {
349
- * readonly logger = new Logger(SyncService.name)
350
- *
351
261
  * @Log({ args: (loanId: number) => ({ loanId }) })
352
- * async syncLoan(loanId: number, loanData?: CloudbankinLoan) {
262
+ * async syncLoan(loanId: number, loanData?: unknown) {
353
263
  * // loanData is excluded from logs due to large size
354
264
  * // Only loanId will be logged
355
265
  * return this.processLoan(loanId, loanData)
356
266
  * }
357
- *
267
+ *
358
268
  * @Log({ args: (loanId: number, transactionId: number) => ({ loanId, transactionId }) })
359
- * async syncPayment(loanId: number, transactionId: number, loanData?: CloudbankinLoan) {
269
+ * async syncPayment(loanId: number, transactionId: number, loanData?: unknown) {
360
270
  * // Only loanId and transactionId are logged, loanData is excluded
361
271
  * return this.processPayment(loanId, transactionId, loanData)
362
272
  * }
@@ -365,22 +275,26 @@ const applyToClass = (target, options) => {
365
275
  * // Logs output:
366
276
  * // [SyncService] { method: 'syncLoan', state: 'success', args: { loanId: 123 } }
367
277
  * // [SyncService] { method: 'syncPayment', state: 'success', args: { loanId: 123, transactionId: 456 } }
368
- *
278
+ *
369
279
  * @param options - Configuration options for the decorator
370
- * @throws {Error} If the logger property is not found in the class instance
371
- *
372
280
  * @returns Decorator function that can be applied to classes or methods
373
281
  */
374
- const Log = (options = {}) => {
375
- return ((target, propertyKey, descriptor) => {
376
- if (propertyKey === void 0) {
377
- applyToClass(target, options);
378
- return target;
282
+ const Log = ({ onInvoke: shouldLogInvoke = false, args: formatArgs, result: formatResult } = {}) => (0, base_decorators.Effect)(({ args, argsObject, target, propertyKey, className }) => {
283
+ const formattedArgs = formatArgs ? formatArgs(...args) : argsObject;
284
+ const resultFormatter = typeof formatResult === "function" ? formatResult : (value) => value;
285
+ const logger = createLogWrapper(target, className, String(propertyKey), formattedArgs);
286
+ return {
287
+ onInvoke: shouldLogInvoke ? () => logger.invoked() : void 0,
288
+ onReturn: ({ result }) => {
289
+ logger.success(void 0, formatResult ? { result: resultFormatter(result) } : void 0);
290
+ return result;
291
+ },
292
+ onError: ({ error }) => {
293
+ logger.error(error);
294
+ throw error;
379
295
  }
380
- if (descriptor !== void 0) return applyToMethod(target, propertyKey, descriptor, options);
381
- throw new Error("Log decorator can only be applied to classes or methods");
382
- });
383
- };
296
+ };
297
+ }, NO_LOG_METADATA_KEY);
384
298
  /**
385
299
  * Method decorator that prevents logging when used with class-level @Log()
386
300
  *
@@ -392,8 +306,6 @@ const Log = (options = {}) => {
392
306
  * @example
393
307
  * @Log()
394
308
  * class UserService {
395
- * readonly logger = new Logger(UserService.name)
396
- *
397
309
  * createUser(name: string) {
398
310
  * // This will be logged
399
311
  * return { name }
@@ -429,27 +341,23 @@ const Log = (options = {}) => {
429
341
  * return 'helper'
430
342
  * }
431
343
  * }
432
- *
344
+ *
433
345
  * @returns {MethodDecorator} The method decorator function
434
346
  */
435
- const NoLog = () => {
436
- return (_target, _propertyKey, descriptor) => {
437
- const value = descriptor.value;
438
- value[NO_LOG_METADATA_KEY] = true;
439
- return descriptor;
440
- };
441
- };
347
+ const NoLog = () => (0, base_decorators.SetMeta)(NO_LOG_METADATA_KEY, true);
348
+
349
+ //#endregion
350
+ //#region src/axios/isTimoutError.ts
351
+ function isTimeoutError(error) {
352
+ return error.code === "ECONNABORTED" || error.code === "ETIMEDOUT";
353
+ }
442
354
 
443
355
  //#endregion
444
356
  exports.Log = Log;
445
357
  exports.LogWrapper = LogWrapper;
446
358
  exports.NO_LOG_METADATA_KEY = NO_LOG_METADATA_KEY;
447
359
  exports.NoLog = NoLog;
448
- exports.applyToClass = applyToClass;
449
- exports.applyToMethod = applyToMethod;
450
- exports.buildArgsObject = buildArgsObject;
451
360
  exports.createLogWrapper = createLogWrapper;
452
- exports.getParameterNames = getParameterNames;
453
- exports.handleAsyncExecution = handleAsyncExecution;
454
361
  exports.isLoggable = isLoggable;
362
+ exports.isTimeoutError = isTimeoutError;
455
363
  exports.prettifyAxiosError = prettifyAxiosError;
package/dist/index.d.cts CHANGED
@@ -2,10 +2,11 @@ import { Logger } from "@nestjs/common";
2
2
 
3
3
  //#region src/types.d.ts
4
4
  type LogArgsFormatter<TArgs extends unknown[]> = (...args: TArgs) => string | number | Record<string, unknown> | undefined;
5
+ type LogResultFormatter<TResult = unknown> = (result: TResult) => unknown;
5
6
  /**
6
7
  * Configuration options for the Log decorator.
7
8
  */
8
- interface LogOptions<TArgs extends unknown[] = unknown[]> {
9
+ interface LogOptions<TArgs extends unknown[] = unknown[], TResult = unknown> {
9
10
  /**
10
11
  * When true, logs method invocation with arguments.
11
12
  * When false or not set, only logs success and error states.
@@ -28,6 +29,21 @@ interface LogOptions<TArgs extends unknown[] = unknown[]> {
28
29
  * @returns Formatted arguments as string, number, object, or undefined
29
30
  */
30
31
  args?: LogArgsFormatter<TArgs>;
32
+ /**
33
+ * Controls how successful method result is logged.
34
+ *
35
+ * - `true`: logs the returned result value as-is
36
+ * - formatter function: receives the method result and returns a formatted value
37
+ *
38
+ * @example
39
+ * // Log raw result
40
+ * @Log({ result: true })
41
+ *
42
+ * @example
43
+ * // Log only selected result fields
44
+ * @Log({ result: (res: { id: number; data: unknown }) => ({ id: res.id }) })
45
+ */
46
+ result?: true | LogResultFormatter<TResult>;
31
47
  }
32
48
  /**
33
49
  * Symbol used to mark methods that should not be logged
@@ -35,59 +51,23 @@ interface LogOptions<TArgs extends unknown[] = unknown[]> {
35
51
  */
36
52
  declare const NO_LOG_METADATA_KEY: unique symbol;
37
53
  //#endregion
38
- //#region src/LogWrapper.d.ts
39
- /**
40
- * Wrapper class for logging operations with consistent format.
41
- * @internal
42
- */
43
- declare class LogWrapper {
44
- private readonly logger;
45
- private readonly method;
46
- private readonly args;
47
- constructor(logger: Logger, method: string, args: string | number | Record<string, unknown> | undefined);
48
- invoked(message?: string): void;
49
- success(message?: string): void;
50
- error(error: unknown, message?: string): void;
51
- }
52
- /** If you see it, then you probably forgot to add `readonly logger = new Logger(YourClass.name)` to your class */
53
- interface Loggable {
54
- logger: Logger;
55
- }
56
- declare const isLoggable: (instance: unknown) => instance is Loggable;
57
- /**
58
- * Creates a LogWrapper instance with validated logger and built args object.
59
- * @internal
60
- */
61
- declare const createLogWrapper: (instance: unknown, className: string, methodName: string, argsObject: string | number | Record<string, unknown> | undefined) => LogWrapper;
62
- /**
63
- * Builds an object mapping parameter names to their values.
64
- *
65
- * Creates a record where keys are parameter names and values are the
66
- * corresponding argument values passed to the function.
67
- *
68
- * @param parameterNames - Array of parameter names
69
- * @param args - Array of argument values
70
- * @returns Object mapping parameter names to values
71
- *
72
- * @example
73
- * buildArgsObject(['id', 'name'], [1, 'John'])
74
- * // Returns: { id: 1, name: 'John' }
75
- *
76
- * @internal
77
- */
78
- declare const buildArgsObject: (parameterNames: string[], args: unknown[]) => Record<string, unknown> | undefined;
79
- //#endregion
80
54
  //#region src/log.decorator.d.ts
81
- type LoggableConstructor = new (...args: any[]) => Loggable;
82
55
  /**
83
56
  * Decorator function that can be applied to classes or methods.
84
57
  * Uses overloads to provide type checking for class decorators while
85
58
  * keeping method decorators flexible.
86
59
  */
87
60
  interface LogDecorator {
88
- /** Class decorator - enforces that class instances have a `logger` property */
89
- <T extends LoggableConstructor>(target: T): T;
90
- /** Method decorator - no compile-time constraint on `this` */
61
+ /**
62
+ * Class decorator - applies logging to all methods in the class.
63
+ * The class no longer needs to define a `logger` property; one will
64
+ * be auto-injected if missing.
65
+ */
66
+ <T extends new (...args: any[]) => unknown>(target: T): T;
67
+ /**
68
+ * Method decorator - no compile-time constraint on `this`.
69
+ * Wraps only the specific method with logging.
70
+ */
91
71
  (target: object, propertyKey: string | symbol, descriptor: PropertyDescriptor): PropertyDescriptor;
92
72
  }
93
73
  /**
@@ -97,6 +77,7 @@ interface LogDecorator {
97
77
  *
98
78
  * **Usage on Classes:**
99
79
  * When applied to a class, wraps all methods in the class with logging.
80
+ * A `logger` property is auto-injected if not already defined.
100
81
  *
101
82
  * **Usage on Methods:**
102
83
  * When applied to a method, wraps only that specific method with logging.
@@ -106,13 +87,10 @@ interface LogDecorator {
106
87
  * - Successful completion with arguments
107
88
  * - Errors with arguments and error details (Axios errors are automatically prettified)
108
89
  *
109
- * **Requirements:**
110
- * The class must have a `logger` property (typically a NestJS Logger instance).
111
- *
112
90
  * **Log Format:**
113
91
  * All logs are output as structured objects with the following format:
114
92
  * - Invocation: `{ method: string, state: 'invoked', args: Record<string, any> }` (only when `onInvoke: true`)
115
- * - Success: `{ method: string, state: 'success', args: Record<string, any> }`
93
+ * - Success: `{ method: string, state: 'success', args: Record<string, any>, result?: unknown }`
116
94
  * - Error: `{ method: string, state: 'error', args: Record<string, any>, error: Error | PrettifiedAxiosError }`
117
95
  *
118
96
  * **Custom Argument Formatting:**
@@ -121,6 +99,11 @@ interface LogDecorator {
121
99
  * - Logging only specific arguments
122
100
  * - Transforming sensitive data before logging
123
101
  *
102
+ * **Result Logging:**
103
+ * Use the `result` option to include successful return values in success logs.
104
+ * - `result: true` logs the raw returned value
105
+ * - `result: (value) => ...` logs a formatted value
106
+ *
124
107
  * **Error Handling:**
125
108
  * - Axios errors are automatically prettified using `prettifyAxiosError` to provide structured error information
126
109
  * - Regular errors are logged as-is
@@ -133,11 +116,9 @@ interface LogDecorator {
133
116
  * - Methods with no arguments
134
117
  *
135
118
  * @example
136
- * // Class-level usage - logs all methods
119
+ * // Class-level usage - logs all methods, logger auto-injected
137
120
  * @Log()
138
121
  * class UserService {
139
- * readonly logger = new Logger(UserService.name)
140
- *
141
122
  * createUser(name: string, email: string) {
142
123
  * return { id: 1, name, email }
143
124
  * }
@@ -150,8 +131,6 @@ interface LogDecorator {
150
131
  * @example
151
132
  * // Method-level usage - logs only specific method
152
133
  * class DataService {
153
- * readonly logger = new Logger(DataService.name)
154
- *
155
134
  * @Log({ onInvoke: true })
156
135
  * async fetchData(id: number) {
157
136
  * const data = await this.repository.findById(id)
@@ -164,7 +143,6 @@ interface LogDecorator {
164
143
  * }
165
144
  * }
166
145
  *
167
- * @example
168
146
  * // Error handling with regular errors
169
147
  * class PaymentService {
170
148
  * readonly logger = new Logger(PaymentService.name)
@@ -210,17 +188,15 @@ interface LogDecorator {
210
188
  * @example
211
189
  * // Custom argument formatting - exclude large objects from logs
212
190
  * class SyncService {
213
- * readonly logger = new Logger(SyncService.name)
214
- *
215
191
  * @Log({ args: (loanId: number) => ({ loanId }) })
216
- * async syncLoan(loanId: number, loanData?: CloudbankinLoan) {
192
+ * async syncLoan(loanId: number, loanData?: unknown) {
217
193
  * // loanData is excluded from logs due to large size
218
194
  * // Only loanId will be logged
219
195
  * return this.processLoan(loanId, loanData)
220
196
  * }
221
197
  *
222
198
  * @Log({ args: (loanId: number, transactionId: number) => ({ loanId, transactionId }) })
223
- * async syncPayment(loanId: number, transactionId: number, loanData?: CloudbankinLoan) {
199
+ * async syncPayment(loanId: number, transactionId: number, loanData?: unknown) {
224
200
  * // Only loanId and transactionId are logged, loanData is excluded
225
201
  * return this.processPayment(loanId, transactionId, loanData)
226
202
  * }
@@ -231,11 +207,13 @@ interface LogDecorator {
231
207
  * // [SyncService] { method: 'syncPayment', state: 'success', args: { loanId: 123, transactionId: 456 } }
232
208
  *
233
209
  * @param options - Configuration options for the decorator
234
- * @throws {Error} If the logger property is not found in the class instance
235
- *
236
210
  * @returns Decorator function that can be applied to classes or methods
237
211
  */
238
- declare const Log: <TArgs extends unknown[]>(options?: LogOptions<TArgs>) => LogDecorator;
212
+ declare const Log: <TArgs extends unknown[], TResult = unknown>({
213
+ onInvoke: shouldLogInvoke,
214
+ args: formatArgs,
215
+ result: formatResult
216
+ }?: LogOptions<TArgs, TResult>) => LogDecorator;
239
217
  /**
240
218
  * Method decorator that prevents logging when used with class-level @Log()
241
219
  *
@@ -247,8 +225,6 @@ declare const Log: <TArgs extends unknown[]>(options?: LogOptions<TArgs>) => Log
247
225
  * @example
248
226
  * @Log()
249
227
  * class UserService {
250
- * readonly logger = new Logger(UserService.name)
251
- *
252
228
  * createUser(name: string) {
253
229
  * // This will be logged
254
230
  * return { name }
@@ -287,51 +263,91 @@ declare const Log: <TArgs extends unknown[]>(options?: LogOptions<TArgs>) => Log
287
263
  *
288
264
  * @returns {MethodDecorator} The method decorator function
289
265
  */
290
- declare const NoLog: () => (_target: unknown, _propertyKey: string, descriptor: PropertyDescriptor) => PropertyDescriptor;
266
+ declare const NoLog: () => MethodDecorator;
291
267
  //#endregion
292
- //#region src/axios/axios.logger.d.ts
293
- declare function prettifyAxiosError(error: unknown): unknown;
294
- //#endregion
295
- //#region src/decorate/applyToMethod.d.ts
296
- type MethodFunction = (...args: unknown[]) => unknown;
268
+ //#region src/LogWrapper.d.ts
297
269
  /**
298
- * Applies logging to a single method.
270
+ * Wrapper class for logging operations with consistent format.
299
271
  * @internal
300
272
  */
301
- declare const applyToMethod: (target: object, propertyKey: string, descriptor: PropertyDescriptor, {
302
- onInvoke: shouldLogInvoke,
303
- args: formatArgs
304
- }?: LogOptions) => PropertyDescriptor;
273
+ declare class LogWrapper {
274
+ private readonly logger;
275
+ private readonly method;
276
+ private readonly args;
277
+ constructor(logger: Logger, method: string, args: string | number | Record<string, unknown> | undefined);
278
+ invoked(message?: string): void;
279
+ success(message?: string, additionalFields?: Record<string, unknown>): void;
280
+ error(error: unknown, message?: string): void;
281
+ }
305
282
  /**
306
- * Extracts parameter names from a function signature.
307
- *
308
- * This function parses the function's string representation to extract
309
- * parameter names, handling TypeScript type annotations and default values.
283
+ * Interface for classes that expose a NestJS Logger instance.
310
284
  *
311
- * @param func - The function to extract parameter names from
312
- * @returns Array of parameter names
285
+ * Implementing this interface is optional when using the `@Log()` decorator.
286
+ * If a class does not define a `logger` property, the decorator will
287
+ * automatically inject a `new Logger(ClassName)` instance at runtime.
313
288
  *
314
- * @example
315
- * function example(id: number, name: string = 'default') {}
316
- * getParameterNames(example) // Returns: ['id', 'name']
317
- *
318
- * @internal
289
+ * You may still implement this interface explicitly to provide a custom logger
290
+ * (e.g., a mock for testing, or a logger with a non-default context).
319
291
  */
320
- declare const getParameterNames: (func: MethodFunction) => string[];
292
+ interface Loggable {
293
+ logger: Logger;
294
+ }
295
+ declare const isLoggable: (instance: unknown) => instance is Loggable;
321
296
  /**
322
- * Handles execution of async methods with proper logging.
297
+ * Creates a LogWrapper instance for structured method logging.
298
+ *
299
+ * When the target instance already has a `logger` property (i.e., satisfies
300
+ * the {@link Loggable} interface), that logger is used directly. Otherwise,
301
+ * a new `Logger` from `@nestjs/common` is created with `className` as its
302
+ * context and assigned to `instance.logger`, so subsequent calls reuse it.
303
+ *
304
+ * @param instance - The class instance whose method is being logged
305
+ * @param className - The name of the class (used as Logger context if auto-injected)
306
+ * @param methodName - The name of the method being logged
307
+ * @param argsObject - Formatted arguments to include in the log entry
308
+ * @returns A configured {@link LogWrapper} ready for invoked/success/error calls
309
+ *
323
310
  * @internal
324
311
  */
325
- declare const handleAsyncExecution: (result: Promise<unknown>, logWrapper: LogWrapper) => Promise<unknown>;
312
+ declare const createLogWrapper: (instance: unknown, className: string, methodName: string, argsObject: string | number | Record<string, unknown> | undefined) => LogWrapper;
326
313
  //#endregion
327
- //#region src/decorate/applyToClass.d.ts
328
- interface Constructor {
329
- prototype: Record<string, unknown>;
330
- }
314
+ //#region src/axios/axios.logger.d.ts
315
+ declare function prettifyAxiosError(error: unknown): unknown;
316
+ //#endregion
317
+ //#region src/axios/axios.stub.d.ts
331
318
  /**
332
- * Applies logging to all methods in a class.
333
- * @internal
334
- */
335
- declare const applyToClass: (target: Constructor, options: LogOptions) => void;
319
+ * Stub for axios types and utils, to avoid ading axios as a dependency
320
+ *
321
+ * Please avoid adding business logic to this file.
322
+ * */
323
+ interface AxiosError extends Error {
324
+ name: string;
325
+ message: string;
326
+ stack?: string;
327
+ isAxiosError: true;
328
+ config: AxiosRequestConfig;
329
+ response: AxiosResponse;
330
+ code?: string;
331
+ request?: any;
332
+ status?: number;
333
+ }
334
+ interface AxiosRequestConfig {
335
+ url?: string;
336
+ method?: string;
337
+ baseURL?: string;
338
+ path?: string;
339
+ headers?: unknown;
340
+ data?: unknown;
341
+ params?: unknown;
342
+ }
343
+ interface AxiosResponse {
344
+ status: number;
345
+ statusText: string;
346
+ data: unknown;
347
+ headers: unknown;
348
+ }
349
+ //#endregion
350
+ //#region src/axios/isTimoutError.d.ts
351
+ declare function isTimeoutError(error: AxiosError): boolean;
336
352
  //#endregion
337
- export { Log, LogArgsFormatter, LogOptions, LogWrapper, Loggable, NO_LOG_METADATA_KEY, NoLog, applyToClass, applyToMethod, buildArgsObject, createLogWrapper, getParameterNames, handleAsyncExecution, isLoggable, prettifyAxiosError };
353
+ export { Log, LogArgsFormatter, LogOptions, LogResultFormatter, LogWrapper, Loggable, NO_LOG_METADATA_KEY, NoLog, createLogWrapper, isLoggable, isTimeoutError, prettifyAxiosError };
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "nestjs-log-decorator",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "Decorator that removes try catch boilerplate logging from NestJS service methods",
5
- "author": "Vlad Goncharov <leovs010@gmail.com>",
5
+ "author": "Vlad Goncharov <vlad.goncharov@neolab.com>",
6
6
  "license": "AGPL-3.0",
7
7
  "homepage": "https://github.com/NeoLabHQ/nestjs-log-decorator#readme",
8
8
  "repository": {
@@ -24,13 +24,18 @@
24
24
  "scripts": {
25
25
  "build": "tsdown",
26
26
  "build:watch": "tsdown --watch",
27
- "dev": "concurrently -c auto --names build,test npm:build:watch npm:test",
28
- "test": "vitest",
27
+ "dev": "concurrently -c auto --names build,test npm:build:watch npm:test:watch",
28
+ "lint": "npm run typecheck",
29
+ "test": "vitest --run",
30
+ "test:watch": "vitest",
29
31
  "typecheck": "tsc --noEmit",
30
32
  "prepublishOnly": "npm run build",
31
33
  "commit": "cz help",
32
34
  "cz": "git add . && cz"
33
35
  },
36
+ "dependencies": {
37
+ "base-decorators": "^0.1.1"
38
+ },
34
39
  "devDependencies": {
35
40
  "@semantic-release/git": "^10.0.1",
36
41
  "@types/node": "^25.0.3",
@@ -49,5 +54,14 @@
49
54
  "commitizen": {
50
55
  "path": "./node_modules/cz-conventional-changelog"
51
56
  }
52
- }
57
+ },
58
+ "keywords": [
59
+ "decorator",
60
+ "decorators",
61
+ "library",
62
+ "nestjs",
63
+ "typescript",
64
+ "utility",
65
+ "logger"
66
+ ]
53
67
  }