@expressots/core 4.0.0-preview.1 → 4.0.0-preview.3

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.
Files changed (134) hide show
  1. package/LICENSE.md +21 -21
  2. package/README.md +66 -66
  3. package/lib/CHANGELOG.md +774 -774
  4. package/lib/README.md +66 -66
  5. package/lib/cjs/application/application-factory.js +6 -0
  6. package/lib/cjs/application/bootstrap.js +117 -213
  7. package/lib/cjs/config/define-config.js +1 -1
  8. package/lib/cjs/config/env-field-builders.js +47 -0
  9. package/lib/cjs/config/index.js +7 -1
  10. package/lib/cjs/framework-version.js +10 -0
  11. package/lib/cjs/lazy-loading/index.js +5 -1
  12. package/lib/cjs/lazy-loading/lazy-module-helpers.js +49 -0
  13. package/lib/cjs/middleware/index.js +8 -9
  14. package/lib/cjs/middleware/middleware-service.js +68 -12
  15. package/lib/cjs/middleware/presets-standalone.js +93 -0
  16. package/lib/cjs/provider/db-in-memory/adapter/in-memory.adapter.js +23 -0
  17. package/lib/cjs/provider/db-in-memory/index.js +11 -1
  18. package/lib/cjs/provider/db-in-memory/query/query-engine.js +28 -0
  19. package/lib/cjs/provider/db-in-memory/schema/decorators.js +18 -0
  20. package/lib/cjs/provider/db-in-memory/storage/index.js +3 -1
  21. package/lib/cjs/provider/db-in-memory/storage/memory-store.js +72 -1
  22. package/lib/cjs/provider/logger/logger.banner.js +40 -31
  23. package/lib/cjs/provider/logger/logger.config.js +11 -1
  24. package/lib/cjs/provider/logger/logger.formatter.js +22 -1
  25. package/lib/cjs/provider/logger/logger.provider.js +59 -9
  26. package/lib/cjs/provider/logger/transports/console.transport.js +69 -6
  27. package/lib/cjs/provider/logger/transports/file.transport.js +27 -18
  28. package/lib/cjs/provider/logger/utils/log-levels.js +6 -5
  29. package/lib/cjs/provider/validation/adapters/index.js +12 -5
  30. package/lib/cjs/provider/validation/adapters/yup.adapter.js +118 -0
  31. package/lib/cjs/provider/validation/adapters/zod.adapter.js +137 -0
  32. package/lib/cjs/provider/validation/index.js +5 -1
  33. package/lib/cjs/render/adapters/react-adapter.js +14 -14
  34. package/lib/cjs/render/features/type-generator.js +30 -30
  35. package/lib/cjs/render/features/view-debugger.js +75 -55
  36. package/lib/cjs/testing/fluent-request.js +7 -0
  37. package/lib/cjs/testing/snapshot-request.js +2 -0
  38. package/lib/cjs/types/application/application-factory.d.ts +6 -0
  39. package/lib/cjs/types/application/bootstrap.d.ts +196 -24
  40. package/lib/cjs/types/config/config.interfaces.d.ts +7 -1
  41. package/lib/cjs/types/config/env-field-builders.d.ts +39 -0
  42. package/lib/cjs/types/config/index.d.ts +1 -1
  43. package/lib/cjs/types/framework-version.d.ts +7 -0
  44. package/lib/cjs/types/lazy-loading/index.d.ts +1 -0
  45. package/lib/cjs/types/lazy-loading/lazy-module-helpers.d.ts +42 -0
  46. package/lib/cjs/types/middleware/index.d.ts +1 -1
  47. package/lib/cjs/types/middleware/middleware-service.d.ts +21 -0
  48. package/lib/cjs/types/middleware/presets-standalone.d.ts +75 -0
  49. package/lib/cjs/types/provider/db-in-memory/adapter/in-memory.adapter.d.ts +2 -0
  50. package/lib/cjs/types/provider/db-in-memory/index.d.ts +9 -1
  51. package/lib/cjs/types/provider/db-in-memory/query/query-engine.d.ts +14 -1
  52. package/lib/cjs/types/provider/db-in-memory/schema/decorators.d.ts +14 -0
  53. package/lib/cjs/types/provider/db-in-memory/storage/index.d.ts +1 -1
  54. package/lib/cjs/types/provider/db-in-memory/storage/memory-store.d.ts +34 -0
  55. package/lib/cjs/types/provider/logger/logger.banner.d.ts +1 -1
  56. package/lib/cjs/types/provider/logger/logger.config.d.ts +7 -0
  57. package/lib/cjs/types/provider/logger/logger.formatter.d.ts +10 -0
  58. package/lib/cjs/types/provider/logger/logger.provider.d.ts +32 -1
  59. package/lib/cjs/types/provider/logger/transports/console.transport.d.ts +7 -0
  60. package/lib/cjs/types/provider/logger/utils/log-levels.d.ts +3 -3
  61. package/lib/cjs/types/provider/validation/adapters/index.d.ts +7 -4
  62. package/lib/cjs/types/provider/validation/adapters/yup.adapter.d.ts +65 -0
  63. package/lib/cjs/types/provider/validation/adapters/zod.adapter.d.ts +84 -0
  64. package/lib/cjs/types/provider/validation/index.d.ts +1 -1
  65. package/lib/cjs/types/render/features/view-debugger.d.ts +10 -0
  66. package/lib/cjs/types/testing/testing.interfaces.d.ts +31 -6
  67. package/lib/esm/application/application-factory.js +6 -0
  68. package/lib/esm/application/bootstrap.js +117 -213
  69. package/lib/esm/config/define-config.js +1 -1
  70. package/lib/esm/config/env-field-builders.js +48 -0
  71. package/lib/esm/config/index.js +6 -1
  72. package/lib/esm/framework-version.js +7 -0
  73. package/lib/esm/lazy-loading/index.js +2 -0
  74. package/lib/esm/lazy-loading/lazy-module-helpers.js +45 -0
  75. package/lib/esm/middleware/index.js +3 -2
  76. package/lib/esm/middleware/middleware-service.js +68 -12
  77. package/lib/esm/middleware/presets-standalone.js +87 -0
  78. package/lib/esm/provider/db-in-memory/adapter/in-memory.adapter.js +23 -0
  79. package/lib/esm/provider/db-in-memory/index.js +9 -1
  80. package/lib/esm/provider/db-in-memory/query/query-engine.js +28 -0
  81. package/lib/esm/provider/db-in-memory/schema/decorators.js +18 -0
  82. package/lib/esm/provider/db-in-memory/storage/index.js +1 -1
  83. package/lib/esm/provider/db-in-memory/storage/memory-store.js +75 -0
  84. package/lib/esm/provider/logger/logger.banner.js +40 -31
  85. package/lib/esm/provider/logger/logger.config.js +12 -2
  86. package/lib/esm/provider/logger/logger.formatter.js +22 -1
  87. package/lib/esm/provider/logger/logger.provider.js +61 -10
  88. package/lib/esm/provider/logger/transports/console.transport.js +69 -6
  89. package/lib/esm/provider/logger/transports/file.transport.js +27 -18
  90. package/lib/esm/provider/logger/utils/log-levels.js +6 -5
  91. package/lib/esm/provider/validation/adapters/index.js +7 -4
  92. package/lib/esm/provider/validation/adapters/yup.adapter.js +111 -0
  93. package/lib/esm/provider/validation/adapters/zod.adapter.js +130 -0
  94. package/lib/esm/provider/validation/index.js +1 -1
  95. package/lib/esm/render/adapters/react-adapter.js +14 -14
  96. package/lib/esm/render/features/type-generator.js +30 -30
  97. package/lib/esm/render/features/view-debugger.js +75 -55
  98. package/lib/esm/testing/fluent-request.js +7 -0
  99. package/lib/esm/testing/snapshot-request.js +2 -0
  100. package/lib/esm/types/application/application-factory.d.ts +6 -0
  101. package/lib/esm/types/application/bootstrap.d.ts +196 -24
  102. package/lib/esm/types/config/config.interfaces.d.ts +7 -1
  103. package/lib/esm/types/config/env-field-builders.d.ts +39 -0
  104. package/lib/esm/types/config/index.d.ts +1 -1
  105. package/lib/esm/types/framework-version.d.ts +7 -0
  106. package/lib/esm/types/lazy-loading/index.d.ts +1 -0
  107. package/lib/esm/types/lazy-loading/lazy-module-helpers.d.ts +42 -0
  108. package/lib/esm/types/middleware/index.d.ts +1 -1
  109. package/lib/esm/types/middleware/middleware-service.d.ts +21 -0
  110. package/lib/esm/types/middleware/presets-standalone.d.ts +75 -0
  111. package/lib/esm/types/provider/db-in-memory/adapter/in-memory.adapter.d.ts +2 -0
  112. package/lib/esm/types/provider/db-in-memory/index.d.ts +9 -1
  113. package/lib/esm/types/provider/db-in-memory/query/query-engine.d.ts +14 -1
  114. package/lib/esm/types/provider/db-in-memory/schema/decorators.d.ts +14 -0
  115. package/lib/esm/types/provider/db-in-memory/storage/index.d.ts +1 -1
  116. package/lib/esm/types/provider/db-in-memory/storage/memory-store.d.ts +34 -0
  117. package/lib/esm/types/provider/logger/logger.banner.d.ts +1 -1
  118. package/lib/esm/types/provider/logger/logger.config.d.ts +7 -0
  119. package/lib/esm/types/provider/logger/logger.formatter.d.ts +10 -0
  120. package/lib/esm/types/provider/logger/logger.provider.d.ts +32 -1
  121. package/lib/esm/types/provider/logger/transports/console.transport.d.ts +7 -0
  122. package/lib/esm/types/provider/logger/utils/log-levels.d.ts +3 -3
  123. package/lib/esm/types/provider/validation/adapters/index.d.ts +7 -4
  124. package/lib/esm/types/provider/validation/adapters/yup.adapter.d.ts +65 -0
  125. package/lib/esm/types/provider/validation/adapters/zod.adapter.d.ts +84 -0
  126. package/lib/esm/types/provider/validation/index.d.ts +1 -1
  127. package/lib/esm/types/render/features/view-debugger.d.ts +10 -0
  128. package/lib/esm/types/testing/testing.interfaces.d.ts +31 -6
  129. package/lib/package.json +23 -8
  130. package/package.json +23 -8
  131. package/lib/cjs/middleware/middleware-presets.js +0 -294
  132. package/lib/cjs/types/middleware/middleware-presets.d.ts +0 -90
  133. package/lib/esm/middleware/middleware-presets.js +0 -286
  134. package/lib/esm/types/middleware/middleware-presets.d.ts +0 -90
@@ -1,6 +1,17 @@
1
1
  import { LogLevel, logLevelToString } from "./utils/log-levels.js";
2
2
  import { colorCodes } from "../../console/color-codes.js";
3
3
  import { getGlobalRedactor } from "./logger.redaction.js";
4
+ /**
5
+ * Strip ANSI escape sequences (SGR colors, cursor moves, etc.) from a
6
+ * string. Cheap fast-path bails out when no escape character is present.
7
+ */
8
+ // eslint-disable-next-line no-control-regex
9
+ const ANSI_ESCAPE_REGEX = /\u001b\[[\d;?]*[ -/]*[@-~]/g;
10
+ function stripAnsiEscapes(input) {
11
+ if (!input || input.indexOf("\u001b") === -1)
12
+ return input;
13
+ return input.replace(ANSI_ESCAPE_REGEX, "");
14
+ }
4
15
  /**
5
16
  * Format log entry for development (human-readable, colored).
6
17
  * @param entry - Log entry to format
@@ -44,6 +55,13 @@ export function formatDev(entry, options) {
44
55
  if (processedEntry.flow) {
45
56
  output += formatFlow(processedEntry.flow, 2);
46
57
  }
58
+ // Strip ANSI escapes once at the end when colors are disabled. This is
59
+ // far simpler than threading a "useColors" flag through every nested
60
+ // `colorText(...)` call site and the perf cost (one regex pass over a
61
+ // log line) is negligible relative to the I/O that follows.
62
+ if (options?.colors === false) {
63
+ return stripAnsiEscapes(output);
64
+ }
47
65
  return output;
48
66
  }
49
67
  /**
@@ -106,7 +124,7 @@ function formatTimestamp(date) {
106
124
  */
107
125
  function getLevelColor(level) {
108
126
  switch (level) {
109
- case LogLevel.TRACE:
127
+ case LogLevel.ALL:
110
128
  return "white";
111
129
  case LogLevel.DEBUG:
112
130
  return "blue";
@@ -331,6 +349,9 @@ export function formatGroupedDev(groupedEntry, options) {
331
349
  }
332
350
  }
333
351
  }
352
+ if (options?.colors === false) {
353
+ return stripAnsiEscapes(output);
354
+ }
334
355
  return output;
335
356
  }
336
357
  /**
@@ -25,11 +25,12 @@ import { LogQueryManager, LogQuery, exportToMarkdown, } from "./logger.query.js"
25
25
  * - Automatic context detection (class/method names)
26
26
  * - Request-scoped context via AsyncLocalStorage
27
27
  * - Child logger creation with inherited context
28
- * - Multiple log levels (TRACE, DEBUG, INFO, WARN, ERROR, FATAL)
28
+ * - Multiple log levels (ALL, DEBUG, INFO, WARN, ERROR, FATAL, SILENT)
29
29
  * - Pluggable transports (console, file, HTTP)
30
30
  * @public API
31
31
  */
32
- let Logger = Logger_1 = class Logger {
32
+ let Logger = class Logger {
33
+ static { Logger_1 = this; }
33
34
  pid;
34
35
  config;
35
36
  transports = [];
@@ -39,21 +40,40 @@ let Logger = Logger_1 = class Logger {
39
40
  groupingManager = null;
40
41
  healthMonitor = null;
41
42
  queryManager = null;
43
+ /**
44
+ * Module-level overrides applied by `Logger.configure(...)` on top of the
45
+ * built-in defaults. Used by the constructor of every future `Logger`
46
+ * instance so application-wide config (e.g. log level, transports) can be
47
+ * set once before bootstrap, before any DI container exists.
48
+ */
49
+ static defaultOverrides = {};
42
50
  name = "Logger Provider";
43
51
  version = "4.1.0";
44
52
  author = "Richard Zampieri";
45
53
  repo = "https://github.com/expressots/expressots";
46
54
  constructor() {
47
55
  this.pid = process.pid;
48
- this.config = getDefaultLoggerConfig();
49
- // Initialize with default console transport if none provided
56
+ // Merge module-level overrides on top of the built-in defaults so values
57
+ // set via Logger.configure(...) before container creation propagate to
58
+ // every instance. Caller-supplied transports replace the defaults, level
59
+ // is forwarded to the auto-installed console transport below.
60
+ const baseConfig = getDefaultLoggerConfig();
61
+ this.config = {
62
+ ...baseConfig,
63
+ ...Logger_1.defaultOverrides,
64
+ };
65
+ // Initialize with default console transport if none provided.
66
+ // Aligning the transport's level with the resolved logger level
67
+ // (which already honours `process.env.LOG_LEVEL`) keeps both
68
+ // filters consistent for messages emitted before any explicit
69
+ // `logger.configure({ level })` call.
50
70
  if (this.config.transports.length === 0) {
51
71
  const isDevelopment = process.env.NODE_ENV !== "production";
52
- this.config.transports = [
53
- isDevelopment
54
- ? ConsoleTransport.forDevelopment()
55
- : ConsoleTransport.forProduction(),
56
- ];
72
+ const transport = isDevelopment
73
+ ? ConsoleTransport.forDevelopment()
74
+ : ConsoleTransport.forProduction();
75
+ transport.level = parseLogLevel(this.config.level);
76
+ this.config.transports = [transport];
57
77
  }
58
78
  this.transports = this.config.transports;
59
79
  // Initialize grouping manager with default config
@@ -61,6 +81,37 @@ let Logger = Logger_1 = class Logger {
61
81
  // Initialize query manager with default config
62
82
  this.queryManager = new LogQueryManager(this.config.query);
63
83
  }
84
+ /**
85
+ * Configure the default LoggerConfig used by every future `Logger`
86
+ * instance constructed in this process.
87
+ *
88
+ * Use this from application config files **before** bootstrap so the DI
89
+ * container's Logger picks up the correct level / transports / grouping /
90
+ * health / redaction settings without needing to inject and reconfigure it.
91
+ * Calls are merged: later calls override earlier ones, but unspecified
92
+ * keys keep their previously-set values.
93
+ *
94
+ * Already-constructed Logger instances are NOT mutated by this call —
95
+ * use the instance method `logger.configure(...)` to update a live one.
96
+ *
97
+ * @param config - Partial configuration to merge with the defaults.
98
+ * @public API
99
+ */
100
+ static configure(config) {
101
+ Logger_1.defaultOverrides = {
102
+ ...Logger_1.defaultOverrides,
103
+ ...config,
104
+ };
105
+ }
106
+ /**
107
+ * Reset the module-level overrides set by `Logger.configure`. Useful in
108
+ * tests to undo cross-test pollution.
109
+ *
110
+ * @public API
111
+ */
112
+ static resetConfigure() {
113
+ Logger_1.defaultOverrides = {};
114
+ }
64
115
  /**
65
116
  * Configure the logger.
66
117
  * @param config - Partial configuration to merge with defaults
@@ -132,7 +183,7 @@ let Logger = Logger_1 = class Logger {
132
183
  * @public API
133
184
  */
134
185
  trace(message, data) {
135
- this.log(LogLevel.TRACE, message, { data });
186
+ this.log(LogLevel.ALL, message, { data });
136
187
  }
137
188
  /**
138
189
  * Log a debug message (detailed diagnostic).
@@ -1,5 +1,34 @@
1
1
  import { LogLevel, shouldLog } from "../utils/log-levels.js";
2
2
  import { formatDev, formatProd } from "../logger.formatter.js";
3
+ /**
4
+ * Decide whether ANSI color codes should be emitted by default.
5
+ *
6
+ * Order of precedence (industry standard, matches chalk / supports-color):
7
+ * 1. `NO_COLOR` env var (any non-empty value) → never colors
8
+ * 2. `FORCE_COLOR` env var (any non-"0"/"false" value) → always colors
9
+ * 3. Process running in production → never colors
10
+ * 4. Stdout is a TTY → colors
11
+ * 5. Otherwise (pipes, redirects, cloud log capture) → no colors
12
+ *
13
+ * This means cloud platforms like Azure App Service, AWS CloudWatch,
14
+ * Heroku, Docker, Kubernetes, etc. — which all capture stdout into a
15
+ * non-TTY pipe — receive plain text by default, without users having
16
+ * to configure anything.
17
+ */
18
+ function detectColorsDefault() {
19
+ const noColor = process.env.NO_COLOR;
20
+ if (noColor !== undefined && noColor !== "")
21
+ return false;
22
+ const forceColor = process.env.FORCE_COLOR;
23
+ if (forceColor !== undefined &&
24
+ forceColor !== "0" &&
25
+ forceColor !== "false") {
26
+ return true;
27
+ }
28
+ if (process.env.NODE_ENV === "production")
29
+ return false;
30
+ return Boolean(process.stdout && process.stdout.isTTY);
31
+ }
3
32
  /**
4
33
  * Console transport for logging to stdout/stderr.
5
34
  * @public API
@@ -22,7 +51,7 @@ export class ConsoleTransport {
22
51
  this.enabled = options?.enabled ?? true;
23
52
  this.structured =
24
53
  options?.structured ?? process.env.NODE_ENV === "production";
25
- this.colors = options?.colors ?? process.env.NODE_ENV !== "production";
54
+ this.colors = options?.colors ?? detectColorsDefault();
26
55
  this.pretty = options?.pretty ?? process.env.NODE_ENV !== "production";
27
56
  // Enable redaction by default in production
28
57
  this.redact = options?.redact ?? process.env.NODE_ENV === "production";
@@ -30,6 +59,13 @@ export class ConsoleTransport {
30
59
  }
31
60
  /**
32
61
  * Create a console transport optimized for development.
62
+ *
63
+ * Colors are auto-detected: they're emitted when stdout is a TTY
64
+ * (your terminal) and disabled when stdout is piped or captured by
65
+ * a cloud log collector (Azure App Service, AWS CloudWatch, Heroku,
66
+ * Docker, Kubernetes, etc.). Respects the standard `NO_COLOR` and
67
+ * `FORCE_COLOR` environment variables.
68
+ *
33
69
  * @param redact - Enable redaction (default: false for development)
34
70
  * @returns ConsoleTransport configured for development
35
71
  * @public API
@@ -38,7 +74,7 @@ export class ConsoleTransport {
38
74
  return new ConsoleTransport({
39
75
  level: LogLevel.DEBUG,
40
76
  structured: false,
41
- colors: true,
77
+ // Omit `colors` so the constructor's auto-detect kicks in.
42
78
  pretty: true,
43
79
  redact, // Usually disabled in development for easier debugging
44
80
  });
@@ -62,17 +98,44 @@ export class ConsoleTransport {
62
98
  if (!this.enabled || !shouldLog(entry.level, this.level)) {
63
99
  return;
64
100
  }
65
- // Build format options with redaction settings
101
+ // Build format options with redaction + color settings. The color
102
+ // flag is honoured by `formatDev` / `formatGroupedDev`; `formatProd`
103
+ // (structured JSON) never emits color regardless.
66
104
  const formatOptions = {
67
105
  redact: this.redact,
68
106
  redactor: this.redactor,
107
+ colors: this.colors,
69
108
  };
70
109
  const formatted = this.structured
71
110
  ? formatProd(entry, formatOptions)
72
111
  : formatDev(entry, formatOptions);
73
- // Use process.stdout/stderr directly to allow runtime interception
74
- const stream = entry.level >= LogLevel.ERROR ? process.stderr : process.stdout;
75
- stream.write(formatted);
112
+ // Strip trailing newline since console.* methods append their own.
113
+ const msg = formatted.endsWith("\n") ? formatted.slice(0, -1) : formatted;
114
+ // Route through console methods so Studio's LogCapture (which
115
+ // intercepts console.*) can forward these entries to the Live Logs UI.
116
+ // The level mapping mirrors the level chips Studio renders so messages
117
+ // land under the expected filter. We pass a single pre-formatted string
118
+ // so util.format never tries to interpret stray "%s"/"%d" tokens in
119
+ // user-supplied messages.
120
+ switch (entry.level) {
121
+ case LogLevel.DEBUG:
122
+ case LogLevel.ALL:
123
+ console.debug(msg);
124
+ break;
125
+ case LogLevel.INFO:
126
+ console.info(msg);
127
+ break;
128
+ case LogLevel.WARN:
129
+ console.warn(msg);
130
+ break;
131
+ case LogLevel.ERROR:
132
+ case LogLevel.FATAL:
133
+ console.error(msg);
134
+ break;
135
+ default:
136
+ console.log(msg);
137
+ break;
138
+ }
76
139
  }
77
140
  flush() {
78
141
  // Console doesn't need flushing
@@ -92,29 +92,34 @@ export class FileTransport {
92
92
  if (!this.enabled || !shouldLog(entry.level, this.level)) {
93
93
  return;
94
94
  }
95
- const filename = this.getFilename();
96
- const shouldRotate = this.shouldRotate(filename);
97
- if (shouldRotate) {
98
- await this.rotateFile(filename);
99
- }
100
- if (filename !== this.currentFilename) {
101
- this.currentFilename = filename;
102
- this.currentFileSize = await this.getFileSize(filename);
103
- }
104
- await this.ensureDirectory();
105
- const filePath = join(this.directory, this.currentFilename);
106
- const formatted = formatProd(entry) + "\n";
107
- const entrySize = Buffer.byteLength(formatted, "utf8");
95
+ // Wrap the entire write pipeline in a single try/catch.
96
+ //
97
+ // FileTransport must NEVER throw out of `log()` — a logger that crashes
98
+ // the request that just wrote to it would be worse than a silent drop.
99
+ // We log a single line to stderr and move on; this matches the
100
+ // ConsoleTransport contract and keeps the framework's "logging never
101
+ // takes down the app" invariant.
108
102
  try {
103
+ const filename = this.getFilename();
104
+ const shouldRotate = this.shouldRotate(filename);
105
+ if (shouldRotate) {
106
+ await this.rotateFile(filename);
107
+ }
108
+ if (filename !== this.currentFilename) {
109
+ this.currentFilename = filename;
110
+ this.currentFileSize = await this.getFileSize(filename);
111
+ }
112
+ await this.ensureDirectory();
113
+ const filePath = join(this.directory, this.currentFilename);
114
+ const formatted = formatProd(entry) + "\n";
115
+ const entrySize = Buffer.byteLength(formatted, "utf8");
109
116
  await fs.appendFile(filePath, formatted, "utf8");
110
117
  this.currentFileSize += entrySize;
111
- // Check if we need to rotate after this write
112
118
  if (this.maxSize > 0 && this.currentFileSize >= this.maxSize) {
113
119
  await this.rotateFile(this.currentFilename);
114
120
  }
115
121
  }
116
122
  catch (error) {
117
- // Silently fail to avoid log loops
118
123
  console.error(`[FileTransport] Failed to write log:`, error);
119
124
  }
120
125
  }
@@ -222,13 +227,17 @@ export class FileTransport {
222
227
  }
223
228
  }
224
229
  startCleanupInterval() {
225
- // Run cleanup every hour
226
230
  this.cleanupInterval = setInterval(() => {
227
231
  this.cleanupOldFiles().catch((error) => {
228
232
  console.error(`[FileTransport] Cleanup failed:`, error);
229
233
  });
230
- }, 60 * 60 * 1000); // 1 hour
231
- // Run initial cleanup
234
+ }, 60 * 60 * 1000);
235
+ // unref() so the cleanup timer never keeps the event loop alive on its own.
236
+ // Without this a short-lived script that uses FileTransport would hang
237
+ // for an hour at exit waiting on the next tick.
238
+ if (typeof this.cleanupInterval.unref === "function") {
239
+ this.cleanupInterval.unref();
240
+ }
232
241
  this.cleanupOldFiles().catch((error) => {
233
242
  console.error(`[FileTransport] Initial cleanup failed:`, error);
234
243
  });
@@ -5,8 +5,8 @@
5
5
  */
6
6
  export var LogLevel;
7
7
  (function (LogLevel) {
8
- /** Ultra-detailed diagnostic information (function entry/exit) */
9
- LogLevel[LogLevel["TRACE"] = 0] = "TRACE";
8
+ /** Show all logs (ultra-detailed diagnostic information, function entry/exit) */
9
+ LogLevel[LogLevel["ALL"] = 0] = "ALL";
10
10
  /** Detailed diagnostic information for debugging */
11
11
  LogLevel[LogLevel["DEBUG"] = 1] = "DEBUG";
12
12
  /** General informational messages */
@@ -41,8 +41,9 @@ export function parseLogLevel(level) {
41
41
  return LogLevel.INFO;
42
42
  }
43
43
  switch (upperLevel) {
44
+ case "ALL":
44
45
  case "TRACE":
45
- return LogLevel.TRACE;
46
+ return LogLevel.ALL;
46
47
  case "DEBUG":
47
48
  return LogLevel.DEBUG;
48
49
  case "INFO":
@@ -67,8 +68,8 @@ export function parseLogLevel(level) {
67
68
  */
68
69
  export function logLevelToString(level) {
69
70
  switch (level) {
70
- case LogLevel.TRACE:
71
- return "TRACE";
71
+ case LogLevel.ALL:
72
+ return "ALL";
72
73
  case LogLevel.DEBUG:
73
74
  return "DEBUG";
74
75
  case LogLevel.INFO:
@@ -2,9 +2,12 @@
2
2
  * Validation Adapters
3
3
  * @module @expressots/core/validation/adapters
4
4
  *
5
- * Built-in adapters for validation libraries.
6
- * Additional adapters (Zod, Yup, Joi) available as separate packages:
7
- * - @expressots/validator-zod
8
- * - @expressots/validator-yup
5
+ * Built-in adapters for validation libraries. The validation libraries
6
+ * themselves (`class-validator`, `zod`, `yup`) are *optional peer
7
+ * dependencies* — install only the one(s) you actually use.
8
+ *
9
+ * Additional Joi adapter is on the roadmap.
9
10
  */
10
11
  export { ClassValidatorAdapter } from "./class-validator.adapter.js";
12
+ export { ZodValidatorAdapter, createZodValidator } from "./zod.adapter.js";
13
+ export { YupValidatorAdapter, createYupValidator } from "./yup.adapter.js";
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Yup Validation Adapter
3
+ * @module @expressots/core/validation
4
+ *
5
+ * Built-in adapter for the [yup](https://github.com/jquense/yup) validation
6
+ * library. `yup` is declared as an *optional* peer dependency: install it
7
+ * explicitly (`npm i yup`) to enable this adapter.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * import * as yup from "yup";
12
+ * import { validationRegistry, createYupValidator } from "@expressots/core";
13
+ *
14
+ * validationRegistry.register(createYupValidator());
15
+ *
16
+ * const CreateUserSchema = yup.object({
17
+ * email: yup.string().email().required(),
18
+ * age: yup.number().integer().min(18).required(),
19
+ * });
20
+ *
21
+ * @controller("/users")
22
+ * class UsersController {
23
+ * @Post("/")
24
+ * create(@body(CreateUserSchema) input: yup.InferType<typeof CreateUserSchema>) {
25
+ * // input is fully typed and validated
26
+ * }
27
+ * }
28
+ * ```
29
+ */
30
+ /**
31
+ * Adapter for [yup](https://github.com/jquense/yup). Detects yup schemas at
32
+ * runtime by checking for the `__isYupSchema__` brand or the presence of a
33
+ * `validate` + `cast` method pair.
34
+ *
35
+ * @public API
36
+ */
37
+ export class YupValidatorAdapter {
38
+ name = "yup";
39
+ priority = 80;
40
+ canHandle(schema) {
41
+ if (schema === null || typeof schema !== "object")
42
+ return false;
43
+ const candidate = schema;
44
+ if (candidate.__isYupSchema__ === true)
45
+ return true;
46
+ return (typeof candidate.validate === "function" &&
47
+ typeof candidate.cast === "function");
48
+ }
49
+ async validate(data, schema, options) {
50
+ if (typeof schema.validate !== "function") {
51
+ return {
52
+ success: false,
53
+ errors: [
54
+ {
55
+ path: "",
56
+ message: "YupValidatorAdapter received a schema with no validate() method.",
57
+ code: "invalid_schema",
58
+ },
59
+ ],
60
+ };
61
+ }
62
+ try {
63
+ const yupOptions = {
64
+ abortEarly: options?.abortEarly ?? false,
65
+ stripUnknown: options?.stripUnknown ?? false,
66
+ };
67
+ const validated = await schema.validate(data, yupOptions);
68
+ return { success: true, data: validated };
69
+ }
70
+ catch (error) {
71
+ const yupError = error;
72
+ // yup batches per-field issues into `inner` when abortEarly is false,
73
+ // and emits a single message otherwise.
74
+ const issues = yupError?.inner && yupError.inner.length > 0
75
+ ? yupError.inner
76
+ : [yupError];
77
+ return {
78
+ success: false,
79
+ errors: this.mapIssues(issues),
80
+ };
81
+ }
82
+ }
83
+ async transform(data, schema) {
84
+ if (typeof schema.cast === "function") {
85
+ try {
86
+ return schema.cast(data);
87
+ }
88
+ catch {
89
+ return data;
90
+ }
91
+ }
92
+ return data;
93
+ }
94
+ mapIssues(issues) {
95
+ return issues.map((issue) => ({
96
+ path: issue.path ?? "",
97
+ message: issue.message ?? "Validation failed",
98
+ code: issue.type ?? "validation_error",
99
+ received: issue.value,
100
+ }));
101
+ }
102
+ }
103
+ /**
104
+ * Convenience factory matching the `createXxxValidator` naming used by the
105
+ * other adapters. Equivalent to `new YupValidatorAdapter()`.
106
+ *
107
+ * @public API
108
+ */
109
+ export function createYupValidator() {
110
+ return new YupValidatorAdapter();
111
+ }
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Zod Validation Adapter
3
+ * @module @expressots/core/validation
4
+ *
5
+ * Built-in adapter for the [zod](https://zod.dev) validation library.
6
+ * `zod` is declared as an *optional* peer dependency: install it explicitly
7
+ * (`npm i zod`) to enable this adapter; otherwise validation falls back to
8
+ * the next registered adapter.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * import { z } from "zod";
13
+ * import { validationRegistry, ZodValidatorAdapter, createZodValidator } from "@expressots/core";
14
+ *
15
+ * validationRegistry.register(createZodValidator());
16
+ *
17
+ * const CreateUserSchema = z.object({
18
+ * email: z.string().email(),
19
+ * age: z.number().int().min(18),
20
+ * });
21
+ *
22
+ * @controller("/users")
23
+ * class UsersController {
24
+ * @Post("/")
25
+ * create(@body(CreateUserSchema) input: z.infer<typeof CreateUserSchema>) {
26
+ * // input is fully typed and validated
27
+ * }
28
+ * }
29
+ * ```
30
+ */
31
+ /**
32
+ * Adapter for [zod](https://zod.dev). Picks up any zod schema (`z.object`,
33
+ * `z.string`, ...) at runtime by structural detection — no compile-time
34
+ * dependency on `zod` is required.
35
+ *
36
+ * @public API
37
+ */
38
+ export class ZodValidatorAdapter {
39
+ name = "zod";
40
+ priority = 90;
41
+ canHandle(schema) {
42
+ if (schema === null || typeof schema !== "object")
43
+ return false;
44
+ const candidate = schema;
45
+ return (typeof candidate.safeParseAsync === "function" ||
46
+ typeof candidate.safeParse === "function" ||
47
+ typeof candidate.parseAsync === "function" ||
48
+ typeof candidate.parse === "function");
49
+ }
50
+ async validate(data, schema,
51
+ // ValidationOptions accepted for interface compliance; Zod surfaces its own
52
+ // configuration through the schema itself, so options are not consumed here.
53
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
54
+ _options) {
55
+ try {
56
+ // Prefer the safe variants because they encode the failure mode in the
57
+ // result rather than throwing.
58
+ if (typeof schema.safeParseAsync === "function") {
59
+ const r = await schema.safeParseAsync(data);
60
+ return r.success
61
+ ? { success: true, data: r.data }
62
+ : { success: false, errors: this.mapIssues(r.error?.issues ?? []) };
63
+ }
64
+ if (typeof schema.safeParse === "function") {
65
+ const r = schema.safeParse(data);
66
+ return r.success
67
+ ? { success: true, data: r.data }
68
+ : { success: false, errors: this.mapIssues(r.error?.issues ?? []) };
69
+ }
70
+ if (typeof schema.parseAsync === "function") {
71
+ const parsed = await schema.parseAsync(data);
72
+ return { success: true, data: parsed };
73
+ }
74
+ if (typeof schema.parse === "function") {
75
+ const parsed = schema.parse(data);
76
+ return { success: true, data: parsed };
77
+ }
78
+ return {
79
+ success: false,
80
+ errors: [
81
+ {
82
+ path: "",
83
+ message: "ZodValidatorAdapter received a schema with no parse/safeParse method.",
84
+ code: "invalid_schema",
85
+ },
86
+ ],
87
+ };
88
+ }
89
+ catch (error) {
90
+ // zod throws ZodError for `parse`/`parseAsync`. Pull issues if present.
91
+ const maybeIssues = error?.issues;
92
+ if (Array.isArray(maybeIssues)) {
93
+ return { success: false, errors: this.mapIssues(maybeIssues) };
94
+ }
95
+ return {
96
+ success: false,
97
+ errors: [
98
+ {
99
+ path: "",
100
+ message: error instanceof Error
101
+ ? error.message
102
+ : "Validation failed unexpectedly",
103
+ code: "validation_error",
104
+ },
105
+ ],
106
+ };
107
+ }
108
+ }
109
+ async transform(data, schema) {
110
+ const result = await this.validate(data, schema);
111
+ return result.success ? result.data : data;
112
+ }
113
+ mapIssues(issues) {
114
+ return issues.map((issue) => ({
115
+ path: issue.path.join("."),
116
+ message: issue.message,
117
+ code: issue.code ?? "validation_error",
118
+ received: issue.received,
119
+ }));
120
+ }
121
+ }
122
+ /**
123
+ * Convenience factory matching the `createXxxValidator` naming used by the
124
+ * other adapters. Equivalent to `new ZodValidatorAdapter()`.
125
+ *
126
+ * @public API
127
+ */
128
+ export function createZodValidator() {
129
+ return new ZodValidatorAdapter();
130
+ }
@@ -18,4 +18,4 @@ export { HelpfulErrorFormatter, } from "./helpful-error-formatter.js";
18
18
  // Type inference utilities
19
19
  export { getParameterType, getAllParameterTypes, getClassProperties, hasClassValidatorDecorators, isZodSchema, isClassConstructor, detectSchemaType, } from "./type-inference.js";
20
20
  // Built-in adapters
21
- export { ClassValidatorAdapter } from "./adapters/index.js";
21
+ export { ClassValidatorAdapter, ZodValidatorAdapter, createZodValidator, YupValidatorAdapter, createYupValidator, } from "./adapters/index.js";