@catbee/utils 2.0.0 → 2.0.2

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/server/index.cjs CHANGED
@@ -57,17 +57,29 @@ var ServerConfigBuilder = class {
57
57
  *
58
58
  * @private
59
59
  * @param port - The port number to validate
60
- * @throws {Error} If port is not an integer or is outside the valid range (1-65535)
60
+ * @throws {Error} If port is not an integer or is outside the valid range (0-65535)
61
61
  */
62
62
  validatePort(port) {
63
- if (!validation.isPort(port)) {
64
- throw new Error(`Port must be a valid number between 1 and 65535, got: ${port}`);
63
+ if (!validation.isPort(port, true)) {
64
+ throw new Error(`Port must be a valid number between 0 and 65535, got: ${port}`);
65
+ }
66
+ }
67
+ /**
68
+ * Validates that a hostname is valid.
69
+ *
70
+ * @private
71
+ * @param host - The hostname to validate
72
+ * @throws {Error} If hostname is invalid
73
+ */
74
+ validateHost(host) {
75
+ if (!validation.isHostname(host)) {
76
+ throw new Error(`Host must be a valid hostname or IP address, got: ${host}`);
65
77
  }
66
78
  }
67
79
  /**
68
80
  * Sets the port the server will listen on.
69
81
  *
70
- * @param port - The port number (1-65535)
82
+ * @param port - The port number (0-65535). Use 0 for dynamic port assignment.
71
83
  * @returns The builder instance for chaining
72
84
  * @throws {Error} If port is invalid
73
85
  * @default 3000 (can be overridden via PORT env variable)
@@ -87,14 +99,17 @@ var ServerConfigBuilder = class {
87
99
  *
88
100
  * @param host - The hostname (e.g., 'localhost', '0.0.0.0', '127.0.0.1')
89
101
  * @returns The builder instance for chaining
102
+ * @throws {Error} If hostname is invalid
90
103
  * @default '0.0.0.0' (can be overridden via HOST env variable)
91
104
  *
92
105
  * @example
93
106
  * ```typescript
94
107
  * builder.withHost('0.0.0.0') // Listen on all interfaces
108
+ * builder.withHost('localhost') // Listen on localhost only
95
109
  * ```
96
110
  */
97
111
  withHost(host) {
112
+ this.validateHost(host);
98
113
  this.config.host = host;
99
114
  return this;
100
115
  }
@@ -505,12 +520,23 @@ var ServerConfigBuilder = class {
505
520
  * ```
506
521
  */
507
522
  withBodyParser(opts) {
523
+ if (opts === true) {
524
+ this.config.bodyParser = config.getCatbeeServerGlobalConfig().bodyParser;
525
+ return this;
526
+ } else if (opts === false) {
527
+ this.config.bodyParser = false;
528
+ return this;
529
+ }
508
530
  this.config.bodyParser = {
509
531
  ...this.config.bodyParser,
510
532
  ...opts
511
533
  };
512
534
  return this;
513
535
  }
536
+ disableBodyParser() {
537
+ this.config.bodyParser = false;
538
+ return this;
539
+ }
514
540
  /**
515
541
  * Configures cookie parsing middleware.
516
542
  *
@@ -661,7 +687,7 @@ var ServerConfigBuilder = class {
661
687
  });
662
688
  }
663
689
  mergeConfig(key, value) {
664
- const current = this.config[key] && typeof this.config[key] === "object" ? object.deepClone(this.config[key]) : {};
690
+ const current = object.isPlainObject(this.config[key]) ? object.deepClone(this.config[key]) : {};
665
691
  this.config[key] = object.deepObjMerge({}, current, value);
666
692
  }
667
693
  setEnabled(key, enable, overrides = {}) {
@@ -742,12 +768,15 @@ var ExpressServer = class {
742
768
  */
743
769
  constructor(config$1, hooks = {}) {
744
770
  if (this.hasBuildMarker(config$1)) {
745
- this.config = config$1;
771
+ this.config = object.deepObjMerge({}, config$1);
746
772
  } else {
747
773
  this.config = object.deepObjMerge({}, config.getCatbeeServerGlobalConfig(), config$1);
748
774
  }
749
- if (!validation.isPort(this.config.port)) {
750
- const msg = `Port must be a valid number between 1 and 65535, got: ${this.config.port}`;
775
+ if (this.config.host) {
776
+ this.config.host = this.normalizeHost(this.config.host);
777
+ }
778
+ if (!validation.isPort(this.config.port, true)) {
779
+ const msg = `Port must be a valid number between 0 and 65535, got: ${this.config.port}`;
751
780
  logger.getLogger().error(msg);
752
781
  throw new Error(msg);
753
782
  }
@@ -860,6 +889,7 @@ var ExpressServer = class {
860
889
  async initialize() {
861
890
  await this.runHook("beforeInit", this);
862
891
  await this.setupMiddleware();
892
+ await this.runHook("beforeRoutes", this.app);
863
893
  await this.setupRoutes();
864
894
  await this.runHook("afterInit", this);
865
895
  }
@@ -1025,7 +1055,6 @@ var ExpressServer = class {
1025
1055
  }
1026
1056
  const logger$1 = logger.getLogger();
1027
1057
  const incomingRequestMetaData = {
1028
- requestId: req.id,
1029
1058
  method: req.method,
1030
1059
  url: req.originalUrl || req.url,
1031
1060
  ip: req.ip
@@ -1075,13 +1104,23 @@ var ExpressServer = class {
1075
1104
  * Set up body parsing middleware.
1076
1105
  */
1077
1106
  setupBodyParsingMiddleware() {
1078
- if (this.config.bodyParser) {
1107
+ if (object.isPlainObject(this.config.bodyParser)) {
1079
1108
  if (this.config.bodyParser.json) {
1080
1109
  this.app.use(express__default.default.json(this.config.bodyParser.json));
1081
1110
  }
1082
1111
  if (this.config.bodyParser.urlencoded) {
1083
1112
  this.app.use(express__default.default.urlencoded(this.config.bodyParser.urlencoded));
1084
1113
  }
1114
+ } else if (this.config.bodyParser === true) {
1115
+ const globalBodyParserConfig = config.getCatbeeServerGlobalConfig().bodyParser;
1116
+ if (object.isPlainObject(globalBodyParserConfig)) {
1117
+ if (globalBodyParserConfig.json) {
1118
+ this.app.use(express__default.default.json(globalBodyParserConfig.json));
1119
+ }
1120
+ if (globalBodyParserConfig.urlencoded) {
1121
+ this.app.use(express__default.default.urlencoded(globalBodyParserConfig.urlencoded));
1122
+ }
1123
+ }
1085
1124
  }
1086
1125
  }
1087
1126
  /**
@@ -1208,6 +1247,7 @@ var ExpressServer = class {
1208
1247
  }
1209
1248
  const routerToUse = this.externalRouter || this.rootRouter;
1210
1249
  this.app.use(this.globalPrefix, routerToUse);
1250
+ await this.runHook("afterRoutes", this.app);
1211
1251
  this.app.use((req, res) => {
1212
1252
  const status = httpStatusCodes.HttpStatusCodes.NOT_FOUND;
1213
1253
  const response$1 = response.createFinalErrorResponse(req, status, `Route ${req.method.toUpperCase()} ${req.path} not found`);
@@ -1361,6 +1401,7 @@ var ExpressServer = class {
1361
1401
  resolve(this.server);
1362
1402
  }, "onListening");
1363
1403
  this.server = this.createServerInstance(onListening);
1404
+ this.runHook("onServerCreated", this.server);
1364
1405
  this.setupConnectionTracking();
1365
1406
  this.setupServerErrorHandling(reject);
1366
1407
  } catch (error) {
@@ -1418,7 +1459,9 @@ var ExpressServer = class {
1418
1459
  */
1419
1460
  logServerStartInfo() {
1420
1461
  const protocol = this.config.https ? "https" : "http";
1421
- const url = `${protocol}://${this.config.host}:${this.config.port}`;
1462
+ const port = this.getPort();
1463
+ const host = this.formatHostForUrl(this.config.host || "localhost");
1464
+ const url = `${protocol}://${host}:${port}`;
1422
1465
  logger.getLogger().info(`Server running on ${url}`);
1423
1466
  if (this.config.healthCheck?.path) {
1424
1467
  logger.getLogger().info(`Health check available at ${url}${this.normalizePath(this.config.healthCheck.path, this.config.healthCheck.withGlobalPrefix)}`);
@@ -1643,12 +1686,136 @@ var ExpressServer = class {
1643
1686
  return this.config;
1644
1687
  }
1645
1688
  /**
1689
+ * Get the port the server is listening on.
1690
+ * Returns the actual port if server is running (useful when config.port was 0),
1691
+ * otherwise returns the configured port.
1692
+ *
1693
+ * @returns The port number
1694
+ */
1695
+ getPort() {
1696
+ const address = this.server?.address();
1697
+ if (address && typeof address === "object" && "port" in address) {
1698
+ return address.port;
1699
+ }
1700
+ return this.config.port;
1701
+ }
1702
+ /**
1703
+ * Get the full URL the server is running on.
1704
+ * Returns the actual URL if server is running (useful when config.port was 0),
1705
+ * otherwise returns the configured URL.
1706
+ *
1707
+ * @returns The full server URL (e.g., "http://localhost:3000")
1708
+ */
1709
+ getUrl() {
1710
+ const protocol = this.config.https ? "https" : "http";
1711
+ const port = this.getPort();
1712
+ const host = this.formatHostForUrl(this.config.host || "localhost");
1713
+ return `${protocol}://${host}:${port}`;
1714
+ }
1715
+ /**
1716
+ * Check if the server is configured for dynamic port assignment.
1717
+ * Returns true if the original port configuration was 0.
1718
+ *
1719
+ * @returns True if using dynamic port assignment, false otherwise
1720
+ */
1721
+ isPortDynamic() {
1722
+ return this.config.port === 0;
1723
+ }
1724
+ /**
1725
+ * Check if the server is currently running and listening for requests.
1726
+ *
1727
+ * @returns True if server is running, false otherwise
1728
+ */
1729
+ isRunning() {
1730
+ return this.server !== null && this.server.listening;
1731
+ }
1732
+ /**
1733
+ * Get the host address the server is bound to.
1734
+ *
1735
+ * @returns The host address
1736
+ */
1737
+ getHost() {
1738
+ return this.config.host || "localhost";
1739
+ }
1740
+ /**
1741
+ * Get the protocol the server is using ('http' or 'https').
1742
+ *
1743
+ * @returns The protocol string
1744
+ */
1745
+ getProtocol() {
1746
+ return this.config.https ? "https" : "http";
1747
+ }
1748
+ /**
1749
+ * Check if the server is configured to use HTTPS.
1750
+ *
1751
+ * @returns True if using HTTPS, false otherwise
1752
+ */
1753
+ isHttps() {
1754
+ return this.config.https !== void 0;
1755
+ }
1756
+ /**
1757
+ * Set a new port for the server.
1758
+ * Can only be called before the server starts listening.
1759
+ * Useful for testing scenarios where you need to change the port dynamically.
1760
+ *
1761
+ * @param port - The new port number (0-65535)
1762
+ * @throws Error if server is already running or port is invalid
1763
+ */
1764
+ setPort(port) {
1765
+ if (this.server) {
1766
+ throw new Error("Cannot change port after server has started");
1767
+ }
1768
+ if (!validation.isPort(port, true)) {
1769
+ throw new Error(`Port must be a valid number between 0 and 65535, got: ${port}`);
1770
+ }
1771
+ this.config.port = port;
1772
+ }
1773
+ /**
1774
+ * Set a new host for the server.
1775
+ * Can only be called before the server starts listening.
1776
+ * Useful for testing scenarios where you need to change the host dynamically.
1777
+ *
1778
+ * @param host - The new host (e.g., "localhost", "0.0.0.0", "127.0.0.1", "::1", or "[::1]")
1779
+ * @throws {Error} If server is already running or host is invalid
1780
+ */
1781
+ setHost(host) {
1782
+ if (this.server) {
1783
+ throw new Error("Cannot change host after server has started");
1784
+ }
1785
+ const normalizedHost = this.normalizeHost(host);
1786
+ if (!validation.isHostname(normalizedHost)) {
1787
+ throw new Error(`Host must be a valid hostname or IP address, got: ${host}`);
1788
+ }
1789
+ this.config.host = normalizedHost;
1790
+ }
1791
+ /**
1646
1792
  * Wait until server initialization (middleware + routes) has completed.
1647
1793
  * Useful for integration tests that inspect app before starting.
1648
1794
  */
1649
1795
  async waitUntilReady() {
1650
1796
  await this.initPromise;
1651
1797
  }
1798
+ /**
1799
+ * Normalize host by stripping surrounding brackets from IPv6 addresses.
1800
+ * This ensures the host value is compatible with server.listen().
1801
+ * Brackets are URL syntax only and must be removed for Node.js binding.
1802
+ */
1803
+ normalizeHost(host) {
1804
+ if (host.startsWith("[") && host.endsWith("]")) {
1805
+ return host.slice(1, -1);
1806
+ }
1807
+ return host;
1808
+ }
1809
+ /**
1810
+ * Format host for use in URLs.
1811
+ * Wraps IPv6 addresses in brackets per RFC 3986.
1812
+ */
1813
+ formatHostForUrl(host) {
1814
+ if (host.includes(":")) {
1815
+ return `[${host}]`;
1816
+ }
1817
+ return host;
1818
+ }
1652
1819
  normalizePath(path, withGlobalPrefix = false) {
1653
1820
  const sanitize = /* @__PURE__ */ __name((p) => {
1654
1821
  return "/" + p.trim().replace(/^\/+/, "").replace(/\/{2,}/g, "/").replace(/\/+$/, "");
package/server/index.d.ts CHANGED
@@ -363,11 +363,87 @@ declare class ExpressServer {
363
363
  * @return {*} {CatbeeServerConfig}
364
364
  */
365
365
  getConfig(): CatbeeServerConfig;
366
+ /**
367
+ * Get the port the server is listening on.
368
+ * Returns the actual port if server is running (useful when config.port was 0),
369
+ * otherwise returns the configured port.
370
+ *
371
+ * @returns The port number
372
+ */
373
+ getPort(): number;
374
+ /**
375
+ * Get the full URL the server is running on.
376
+ * Returns the actual URL if server is running (useful when config.port was 0),
377
+ * otherwise returns the configured URL.
378
+ *
379
+ * @returns The full server URL (e.g., "http://localhost:3000")
380
+ */
381
+ getUrl(): string;
382
+ /**
383
+ * Check if the server is configured for dynamic port assignment.
384
+ * Returns true if the original port configuration was 0.
385
+ *
386
+ * @returns True if using dynamic port assignment, false otherwise
387
+ */
388
+ isPortDynamic(): boolean;
389
+ /**
390
+ * Check if the server is currently running and listening for requests.
391
+ *
392
+ * @returns True if server is running, false otherwise
393
+ */
394
+ isRunning(): boolean;
395
+ /**
396
+ * Get the host address the server is bound to.
397
+ *
398
+ * @returns The host address
399
+ */
400
+ getHost(): string;
401
+ /**
402
+ * Get the protocol the server is using ('http' or 'https').
403
+ *
404
+ * @returns The protocol string
405
+ */
406
+ getProtocol(): string;
407
+ /**
408
+ * Check if the server is configured to use HTTPS.
409
+ *
410
+ * @returns True if using HTTPS, false otherwise
411
+ */
412
+ isHttps(): boolean;
413
+ /**
414
+ * Set a new port for the server.
415
+ * Can only be called before the server starts listening.
416
+ * Useful for testing scenarios where you need to change the port dynamically.
417
+ *
418
+ * @param port - The new port number (0-65535)
419
+ * @throws Error if server is already running or port is invalid
420
+ */
421
+ setPort(port: number): void;
422
+ /**
423
+ * Set a new host for the server.
424
+ * Can only be called before the server starts listening.
425
+ * Useful for testing scenarios where you need to change the host dynamically.
426
+ *
427
+ * @param host - The new host (e.g., "localhost", "0.0.0.0", "127.0.0.1", "::1", or "[::1]")
428
+ * @throws {Error} If server is already running or host is invalid
429
+ */
430
+ setHost(host: string): void;
366
431
  /**
367
432
  * Wait until server initialization (middleware + routes) has completed.
368
433
  * Useful for integration tests that inspect app before starting.
369
434
  */
370
435
  waitUntilReady(): Promise<void>;
436
+ /**
437
+ * Normalize host by stripping surrounding brackets from IPv6 addresses.
438
+ * This ensures the host value is compatible with server.listen().
439
+ * Brackets are URL syntax only and must be removed for Node.js binding.
440
+ */
441
+ private normalizeHost;
442
+ /**
443
+ * Format host for use in URLs.
444
+ * Wraps IPv6 addresses in brackets per RFC 3986.
445
+ */
446
+ private formatHostForUrl;
371
447
  private normalizePath;
372
448
  private normalizeRouteForMetrics;
373
449
  /**
@@ -403,13 +479,21 @@ declare class ServerConfigBuilder {
403
479
  *
404
480
  * @private
405
481
  * @param port - The port number to validate
406
- * @throws {Error} If port is not an integer or is outside the valid range (1-65535)
482
+ * @throws {Error} If port is not an integer or is outside the valid range (0-65535)
407
483
  */
408
484
  private validatePort;
485
+ /**
486
+ * Validates that a hostname is valid.
487
+ *
488
+ * @private
489
+ * @param host - The hostname to validate
490
+ * @throws {Error} If hostname is invalid
491
+ */
492
+ private validateHost;
409
493
  /**
410
494
  * Sets the port the server will listen on.
411
495
  *
412
- * @param port - The port number (1-65535)
496
+ * @param port - The port number (0-65535). Use 0 for dynamic port assignment.
413
497
  * @returns The builder instance for chaining
414
498
  * @throws {Error} If port is invalid
415
499
  * @default 3000 (can be overridden via PORT env variable)
@@ -425,11 +509,13 @@ declare class ServerConfigBuilder {
425
509
  *
426
510
  * @param host - The hostname (e.g., 'localhost', '0.0.0.0', '127.0.0.1')
427
511
  * @returns The builder instance for chaining
512
+ * @throws {Error} If hostname is invalid
428
513
  * @default '0.0.0.0' (can be overridden via HOST env variable)
429
514
  *
430
515
  * @example
431
516
  * ```typescript
432
517
  * builder.withHost('0.0.0.0') // Listen on all interfaces
518
+ * builder.withHost('localhost') // Listen on localhost only
433
519
  * ```
434
520
  */
435
521
  withHost(host: string): this;
@@ -768,6 +854,7 @@ declare class ServerConfigBuilder {
768
854
  * ```
769
855
  */
770
856
  withBodyParser(opts: NonNullable<CatbeeServerConfig['bodyParser']>): this;
857
+ disableBodyParser(): this;
771
858
  /**
772
859
  * Configures cookie parsing middleware.
773
860
  *