UNPKG

@catbee/utils

Version:

A modular, production-grade utility toolkit for Node.js and TypeScript, designed for robust, scalable applications (including Express-based services). All utilities are tree-shakable and can be imported independently.

794 lines (789 loc) 26.9 kB
/* * The MIT License * * Copyright (c) 2026 Catbee Technologies. https://catbee.in/license * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ 'use strict'; var fs = require('fs'); var path = require('path'); var date = require('@catbee/utils/date'); var validation = require('@catbee/utils/validation'); var __defProp = Object.defineProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); var Environment = /* @__PURE__ */ (function(Environment2) { Environment2["PRODUCTION"] = "production"; Environment2["DEVELOPMENT"] = "development"; Environment2["STAGING"] = "staging"; Environment2["TESTING"] = "testing"; return Environment2; })({}); var Env = class _Env { static { __name(this, "Env"); } // Cache for parsed values to avoid repeated parsing of complex values static cache = /* @__PURE__ */ new Map(); /** * Checks if the current NODE_ENV is 'development'. * * @returns {boolean} `true` if NODE_ENV is 'development', else `false`. */ static isDev() { return _Env.get("NODE_ENV", "development") === "development"; } /** * Checks if the current NODE_ENV is 'production'. * * @returns {boolean} `true` if NODE_ENV is 'production', else `false`. */ static isProd() { return _Env.get("NODE_ENV", "development") === "production"; } /** * Checks if the current NODE_ENV is 'testing'. * * @returns {boolean} `true` if NODE_ENV is 'testing', else `false`. */ static isTest() { return _Env.get("NODE_ENV", "development") === "testing"; } /** * Checks if the current NODE_ENV is 'staging'. * * @returns {boolean} `true` if NODE_ENV is 'staging', else `false`. */ static isStaging() { return _Env.get("NODE_ENV", "development") === "staging"; } /** * Sets an environment variable (only affects runtime memory). * * @param {string} key - The environment variable key. * @param {string} value - The value to set. */ static set(key, value) { process.env[key] = value; this.cache.delete(key); for (const cacheKey of this.cache.keys()) { if (cacheKey.includes(`:${key}`)) { this.cache.delete(cacheKey); } } } /** * Returns all environment variables as an object. * * @returns {object} The current `process.env` object. */ static getAll() { return process.env; } /** * Retrieves a string environment variable with a fallback default. * Supports variable expansion with ${VAR_NAME} syntax. * * @param {string} key - The environment variable key. * @param {string} [defaultValue] - Value to return if the key is missing. * @returns {string} The env value or the fallback. * * @example * // If DATABASE_URL is "postgres://localhost:5432/${DB_NAME}" * // and DB_NAME is "myapp" * const url = Env.get('DATABASE_URL', ''); // "postgres://localhost:5432/myapp" */ static get(key, defaultValue) { let value = process.env[key] ?? defaultValue; if (value?.includes("${")) { value = value.replace(/\${([A-Za-z0-9_]+)}/g, (_, varName) => { return process.env[varName] ?? ""; }); } return value; } /** * Retrieves a string environment variable and throws if it's missing. * * @param {string} key - The environment variable key. * @returns {string} The environment variable's value. * @throws {Error} If the variable is not defined. */ static getRequired(key) { const value = process.env[key]; if (value === void 0) { throw new Error(`Required environment variable '${key}' is missing`); } return _Env.get(key, ""); } /** * Retrieves a value using a default generating function if the key doesn't exist. * Useful for expensive default calculations. * * @param {string} key - The environment variable key. * @param {() => string} defaultFn - Function that generates default value. * @returns {string} The environment value or generated default. * * @example * const hostname = Env.getWithDefault('HOSTNAME', () => { * // Only called if HOSTNAME is not set * return require('os').hostname(); * }); */ static getWithDefault(key, defaultFn) { if (_Env.has(key)) { return _Env.get(key, ""); } return defaultFn(); } /** * Retrieves an environment variable as a number, or returns a default. * * @param {string} key - The environment variable key. * @param {number} defaultValue - Fallback number if key is not present. * @returns {number} Parsed numeric value or default. * @throws {Error} If the value is not a valid number. */ static getNumber(key, defaultValue) { if (this.cache.has(`number:${key}`)) { return this.cache.get(`number:${key}`); } const value = process.env[key]; if (value === void 0) { return defaultValue; } const numberValue = Number(value); if (Number.isNaN(numberValue)) { throw new Error(`Environment variable '${key}' is not a valid number, got: "${value}"`); } this.cache.set(`number:${key}`, numberValue); return numberValue; } /** * Retrieves a required environment variable as a number. * * @param {string} key - The environment variable key. * @returns {number} Parsed number. * @throws {Error} If the value is missing or not a number. */ static getNumberRequired(key) { const value = process.env[key]; if (value === void 0) { throw new Error(`Required environment variable '${key}' is missing`); } return _Env.getNumber(key, 0); } /** * Retrieves an integer environment variable and validates it. * * @param {string} key - The environment variable key. * @param {number} defaultValue - Fallback number if key is not present. * @param {object} [options] - Validation options. * @param {number} [options.min] - Minimum allowed value. * @param {number} [options.max] - Maximum allowed value. * @returns {number} The parsed integer. * * @example * // Require PORT to be between 1000 and 9999 * const port = Env.getInteger('PORT', 3000, { min: 1000, max: 9999 }); */ static getInteger(key, defaultValue, options = {}) { const num = _Env.getNumber(key, defaultValue); const intValue = Math.floor(num); if (intValue !== num) { throw new Error(`Environment variable '${key}' must be an integer, got: ${num}`); } if (options.min !== void 0 && intValue < options.min) { throw new Error(`Environment variable '${key}' must be at least ${options.min}, got: ${intValue}`); } if (options.max !== void 0 && intValue > options.max) { throw new Error(`Environment variable '${key}' must be at most ${options.max}, got: ${intValue}`); } return intValue; } /** * Retrieves an environment variable as a boolean. * Accepts `true`, `1`, `yes`, `on` as true; `false`, `0`, `no`, `off` as false. * * @param {string} key - The environment variable key. * @param {boolean} [defaultValue=false] - Optional fallback value if key is missing. * @returns {boolean} Parsed boolean. * @throws {Error} If the value is not a recognized boolean string. * * @example * // If DEBUG=yes * const isDebug = Env.getBoolean('DEBUG', false); // true */ static getBoolean(key, defaultValue = false) { if (this.cache.has(`bool:${key}`)) { return this.cache.get(`bool:${key}`); } const value = process.env[key]; if (value === void 0) { return defaultValue; } const lowerValue = value.toLowerCase(); if ([ "true", "1", "yes", "on" ].includes(lowerValue)) { this.cache.set(`bool:${key}`, true); return true; } if ([ "false", "0", "no", "off" ].includes(lowerValue)) { this.cache.set(`bool:${key}`, false); return false; } throw new Error(`Environment variable '${key}' is not a valid boolean, got: "${value}". Use true/false, yes/no, 1/0, or on/off.`); } /** * Retrieves a required environment variable as a boolean. * * @param {string} key - The environment variable key. * @returns {boolean} Parsed boolean value. * @throws {Error} If missing or invalid. */ static getBooleanRequired(key) { const value = process.env[key]; if (value === void 0) { throw new Error(`Required environment variable '${key}' is missing`); } return _Env.getBoolean(key); } /** * Parses a stringified JSON object from an environment variable. * * @typeParam T - The type to parse as (defaults to `object`). * @param {string} key - The environment variable key. * @param {T} defaultValue - Value to return if key is missing. * @returns {T} Parsed object or default. * @throws {Error} If the value is not valid JSON. * * @example * // If CONFIG='{"debug":true,"api":{"url":"https://api.example.com"}}' * const config = Env.getJSON('CONFIG', { debug: false }); * // { debug: true, api: { url: "https://api.example.com" }} */ static getJSON(key, defaultValue) { if (this.cache.has(`json:${key}`)) { return this.cache.get(`json:${key}`); } const v = process.env[key]; if (v === void 0) { return defaultValue; } try { const parsed = JSON.parse(v); this.cache.set(`json:${key}`, parsed); return parsed; } catch (error) { throw new Error(`Environment variable '${key}' is not valid JSON: ${error.message}`); } } /** * Parses a comma-separated string as an array. * * @typeParam T - The item type (optional, defaults to string). * @param {string} key - The environment variable key. * @param {T[]} [defaultValue=[]] - Array to return if value is empty or missing. * @param {string} [splitter=','] - Delimiter to split on. * @param {(item: string) => T} [transform] - Optional function to transform each item. * @returns {T[]} An array of items. * * @example * // If ALLOWED_IPS=127.0.0.1,192.168.1.1,10.0.0.1 * const ips = Env.getArray('ALLOWED_IPS'); * // ["127.0.0.1", "192.168.1.1", "10.0.0.1"] * * // With transformation function * const ports = Env.getArray('PORTS', [], ',', (p) => parseInt(p, 10)); */ static getArray(key, defaultValue = [], splitter = ",", transform) { const cacheKey = `array:${key}:${splitter}:${transform ? "transformed" : "raw"}`; if (this.cache.has(cacheKey)) { return this.cache.get(cacheKey); } const value = process.env[key]; if (!value || value.trim() === "") { return defaultValue; } const items = value.split(splitter).map((item) => item.trim()).filter((item) => item.length > 0); if (transform) { try { const result2 = items.map(transform); this.cache.set(cacheKey, result2); return result2; } catch (error) { throw new Error(`Failed to transform items in '${key}': ${error.message}`); } } const result = items; this.cache.set(cacheKey, result); return result; } /** * Parses a comma-separated list of numbers. * * @param {string} key - The environment variable key. * @param {number[]} [defaultValue=[]] - Default value if not present. * @param {string} [splitter=','] - Delimiter to split on. * @returns {number[]} Array of parsed numbers. * * @example * // If ALLOWED_PORTS=80,443,3000,8080 * const ports = Env.getNumberArray('ALLOWED_PORTS'); * // [80, 443, 3000, 8080] */ static getNumberArray(key, defaultValue = [], splitter = ",") { return _Env.getArray(key, defaultValue, splitter, (item) => { const num = Number(item); if (Number.isNaN(num)) { throw new Error(`Value "${item}" in array '${key}' is not a valid number`); } return num; }); } /** * Retrieves an enum-like environment variable value, validating against allowed values. * * @typeParam T - The allowed value type (string literal types). * @param {string} key - The environment variable key. * @param {T} [defaultValue] - Optional fallback value. * @param {T[]} allowedValues - Array of accepted string values. * @returns {T} The validated environment value. * @throws {Error} If missing or invalid. * * @example * // If LOG_LEVEL=debug * const level = Env.getEnum('LOG_LEVEL', 'info', ['debug', 'info', 'warn', 'error'] as const); * // 'debug' (typed as 'debug' | 'info' | 'warn' | 'error') */ static getEnum(key, defaultValue, allowedValues) { const value = process.env[key]; if (!value) { return defaultValue; } if (!allowedValues.includes(value)) { throw new Error(`Environment variable '${key}' must be one of: ${allowedValues.join(", ")}. Received: "${value}"`); } return value; } /** * Retrieves an enum-like numeric environment variable. * * @param {string} key - The environment variable key. * @param {number} defaultValue - Default value if not present. * @param {number[]} allowedValues - Array of accepted values. * @returns {number} The validated value. * * @example * // If NODE_VERSION=16 * const version = Env.getNumberEnum('NODE_VERSION', 16, [14, 16, 18]); */ static getNumberEnum(key, defaultValue, allowedValues) { const value = _Env.getNumber(key, defaultValue); if (!allowedValues.includes(value)) { throw new Error(`Environment variable '${key}' must be one of: ${allowedValues.join(", ")}. Received: ${value}`); } return value; } /** * Retrieves a URL environment variable and validates it. * * @param {string} key - The environment variable key. * @param {string} [defaultValue] - Optional fallback value. * @param {UrlOptions} [options] - Validation options. * @returns {string} The validated URL. * @throws {Error} If URL is invalid. * * @example * // Validate API URL requires HTTPS * const apiUrl = Env.getUrl('API_URL', 'https://api.example.com', { * protocols: ['https'], * requireTld: true, * allowIp: false * }); */ static getUrl(key, defaultValue, options = {}) { const cacheKey = `url:${key}:${JSON.stringify(options)}`; if (this.cache.has(cacheKey)) { return this.cache.get(cacheKey); } const value = _Env.get(key, defaultValue); if (!value) { throw new Error(`URL environment variable '${key}' is missing or empty`); } const url = this.parseUrl(value, key); this.validateProtocol(url, key, options); this.validateHostname(url, key, options); this.cache.set(cacheKey, value); return value; } static parseUrl(value, key) { try { return new URL(value); } catch { throw new Error(`Environment variable '${key}' is not a valid URL: "${value}"`); } } static validateProtocol(url, key, options) { if (options.protocols && options.protocols.length > 0) { const protocol = url.protocol.replace(":", ""); if (!options.protocols.includes(protocol)) { throw new Error(`Environment variable '${key}' must use one of the protocols: ${options.protocols.join(", ")}. Got: ${protocol}`); } } } static isIp(hostname) { return /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/.test(hostname); } static validateHostname(url, key, options) { const { hostname } = url; const isIp = _Env.isIp(hostname); const isLocalhost = hostname === "localhost"; if (isIp && options.allowIp === false) { throw new Error(`Environment variable '${key}' cannot be an IP address: "${hostname}"`); } if (isLocalhost && options.allowLocalhost === false) { throw new Error(`Environment variable '${key}' cannot be localhost`); } if (options.requireTld === true) { if (isLocalhost && options.allowLocalhost !== false) ; else if (isIp && options.allowIp !== false) ; else if (!hostname.includes(".") || hostname.endsWith(".")) { throw new Error(`Environment variable '${key}' must have a valid host with TLD: "${hostname}"`); } } } /** * Retrieves an email environment variable and validates it. * * @param {string} key - The environment variable key. * @param {string} [defaultValue] - Optional fallback value. * @returns {string} The validated email address. * @throws {Error} If email is invalid. * * @example * const supportEmail = Env.getEmail('SUPPORT_EMAIL', 'support@example.com'); */ static getEmail(key, defaultValue) { if (this.cache.has(`email:${key}`)) { return this.cache.get(`email:${key}`); } const value = _Env.get(key, defaultValue); if (!value) { if (defaultValue === void 0) { throw new Error(`Email environment variable '${key}' is missing`); } return defaultValue; } if (!validation.isEmail(value)) { throw new Error(`Environment variable '${key}' is not a valid email address: "${value}"`); } this.cache.set(`email:${key}`, value); return value; } /** * Retrieves a path environment variable and validates it exists. * * @param {string} key - The environment variable key. * @param {string} [defaultValue] - Optional fallback value. * @param {PathOptions} [options] - Validation options. * @returns {string} The validated path. * @throws {Error} If path is invalid. * * @example * // Require that the path exists and is a .json file * const configPath = Env.getPath('CONFIG_PATH', './config.json', { * mustExist: true, * allowedExtensions: ['.json', '.yaml'] * }); */ static getPath(key, defaultValue, options = {}) { const cacheKey = `path:${key}:${JSON.stringify(options)}`; if (this.cache.has(cacheKey)) { return this.cache.get(cacheKey); } const value = _Env.get(key, defaultValue); if (!value) { if (defaultValue === void 0) { throw new Error(`Path environment variable '${key}' is missing`); } return defaultValue; } const path$1 = options.makeAbsolute !== false && !path.isAbsolute(value) ? path.resolve(process.cwd(), value) : value; if (options.mustExist && !fs.existsSync(path$1)) { throw new Error(`Path in environment variable '${key}' does not exist: "${path$1}"`); } if (options.allowedExtensions && options.allowedExtensions.length > 0) { const hasValidExtension = options.allowedExtensions.some((ext) => path$1.toLowerCase().endsWith(ext.toLowerCase())); if (!hasValidExtension) { throw new Error(`Path in environment variable '${key}' must have one of these extensions: ${options.allowedExtensions.join(", ")}. Got: "${path$1}"`); } } this.cache.set(cacheKey, path$1); return path$1; } /** * Retrieves a port environment variable and validates it. * * @param {string} key - The environment variable key. * @param {number} [defaultValue] - Optional fallback value. * @returns {number} The validated port number. * @throws {Error} If port is invalid. * * @example * const serverPort = Env.getPort('PORT', 3000); */ static getPort(key, defaultValue) { try { return _Env.getInteger(key, defaultValue, { min: 0, max: 65535 }); } catch (error) { if (error.message.includes("must be at most 65535")) { throw new Error(`Environment variable '${key}' must be a valid port number (0-65535)`); } throw error; } } /** * Retrieves an ISO date string and converts it to a Date object. * * @param {string} key - The environment variable key. * @param {string|Date} [defaultValue] - Optional fallback value. * @returns {Date} The parsed Date object. * * @example * // If EXPIRY_DATE=2023-12-31T23:59:59Z * const expiryDate = Env.getDate('EXPIRY_DATE', new Date()); */ static getDate(key, defaultValue = /* @__PURE__ */ new Date()) { const value = _Env.get(key, defaultValue instanceof Date ? defaultValue.toISOString() : defaultValue); if (!value) { return defaultValue instanceof Date ? defaultValue : /* @__PURE__ */ new Date(); } const date = new Date(value); if (Number.isNaN(date.getTime())) { throw new Error(`Environment variable '${key}' is not a valid date: "${value}"`); } return date; } /** * Retrieves a duration string and converts it to milliseconds. * Supports formats like "1d", "2h", "30m", "45s", "100ms" or combinations like "1h30m". * * @param {string} key - The environment variable key. * @param {string|number} [defaultValue='0'] - Optional fallback value. * @returns {number} The duration in milliseconds. * @throws {Error} If duration format is invalid. * * @example * // If CACHE_TTL=2h30m * const cacheTtlMs = Env.getDuration('CACHE_TTL', '1h'); * // 9000000 (2.5 hours in milliseconds) */ static getDuration(key, defaultValue = "0") { const cacheKey = `duration:${key}`; if (this.cache.has(cacheKey)) { return this.cache.get(cacheKey); } const raw = _Env.get(key, String(defaultValue)); if (!raw) return 0; const ms = date.parseDuration(raw); this.cache.set(cacheKey, ms); return ms; } /** * Gets all environment variables with sensitive values masked for safe logging. * * @param {string[]} [sensitiveKeys=['password', 'secret', 'key', 'token', 'auth']] - Keys to mask. * @returns {Record<string, string>} Environment variables with sensitive values masked. * * @example * console.log(Env.getSafeEnv(['password', 'secret', 'key'])); * // { DATABASE_URL: "postgres://...", API_KEY: "******", ... } */ static getSafeEnv(sensitiveKeys = [ "password", "secret", "key", "token", "auth" ]) { const safeEnv = {}; for (const [key, value] of Object.entries(process.env)) { if (!value) continue; const isSensitive = sensitiveKeys.some((sensitiveKey) => key.toLowerCase().includes(sensitiveKey.toLowerCase())); safeEnv[key] = isSensitive ? "******" : value; } return safeEnv; } /** * Loads environment variables from a .env file. * Does not override existing variables. * * @param {string} [path='.env'] - Path to the .env file. * @returns {Record<string, string>} Loaded environment variables. * * @example * // Load variables from .env.development * Env.loadFromFile('.env.development'); */ static loadFromFile(path = ".env") { if (!fs.existsSync(path)) { throw new Error(`Environment file not found: "${path}"`); } const content = fs.readFileSync(path, "utf8"); const variables = this.parseEnvContent(content); for (const [k, v] of Object.entries(variables)) { if (!process.env[k]) { process.env[k] = v; } } return variables; } static parseEnvContent(content) { const variables = {}; const lines = content.split(/\r?\n/); let i = 0; while (i < lines.length) { let line = lines[i].trim(); if (this.isCommentOrEmpty(line)) { i++; continue; } const match = this.extractKeyValue(line); if (!match) { i++; continue; } const key = match[1].trim(); let value = match[2].trim(); if (!this.isQuoted(value)) { value = this.stripInlineComment(value); } if (this.isQuoted(value) && value.length > 1) { const result = this.parseQuotedValue(lines, i, value); value = result.value; i = result.nextIndex; } else { const result = this.parseUnquotedMultiline(lines, i, value); value = result.value; i = result.nextIndex; } if (!process.env[key]) { variables[key] = value; } i++; } return variables; } static isCommentOrEmpty(line) { return !line || line.startsWith("#"); } static extractKeyValue(line) { return line.match(/^([^=]+)=(.*)$/); } static isQuoted(value) { return /^['"]/.test(value); } static stripInlineComment(value) { const hashIndex = value.indexOf(" #"); if (hashIndex !== -1) { return value.slice(0, hashIndex).trim(); } return value; } static parseQuotedValue(lines, currentIndex, value) { const quote = value[0]; if (!value.endsWith(quote) || value.length === 1) { let multilineValue = value.slice(1); let i = currentIndex + 1; while (i < lines.length) { const nextLine = lines[i]; if (nextLine.endsWith(quote)) { multilineValue += "\n" + nextLine.slice(0, -1); break; } else { multilineValue += "\n" + nextLine; } i++; } return { value: multilineValue, nextIndex: i }; } return { value: value.slice(1, -1), nextIndex: currentIndex }; } static parseUnquotedMultiline(lines, currentIndex, value) { let i = currentIndex + 1; while (i < lines.length && !lines[i].includes("=") && lines[i].trim() !== "") { value += "\n" + lines[i]; i++; } return { value, nextIndex: i - 1 }; } /** * Checks whether the specified environment variable exists. * * @param {string} key - The environment variable key. * @returns {boolean} `true` if the variable is defined, otherwise `false`. */ static has(key) { return process.env[key] !== void 0; } /** * Deletes the given environment variable (useful in tests). * * @param {string} key - The environment variable key to delete. * @returns {void} */ static delete(key) { delete process.env[key]; this.cache.delete(key); for (const cacheKey of this.cache.keys()) { if (cacheKey.includes(`:${key}`)) { this.cache.delete(cacheKey); } } } /** * Clears the internal cache of parsed environment values. * Useful for testing or when environment variables might change. */ static clearCache() { this.cache.clear(); } }; exports.Env = Env; exports.Environment = Environment;