diff --git a/README.md b/README.md index 0cf7242..5b43196 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,18 @@ env.list('ALLOWED_ORIGINS'); // Shorthand for getList() // Get a numeric list env.list('VALUES', { cast: 'number' }); // [1, 2, 3] (converts from '1,2,3') + +// Get a duration in milliseconds +// CACHE_TTL=5m +env.getDuration('CACHE_TTL'); // 300000 +env.duration('CACHE_TTL'); // Shorthand for getDuration() + +// With a default — strings are parsed, numbers are treated as ms +env.getDuration('CACHE_TTL', '30s'); // 30000 if CACHE_TTL is not set +env.getDuration('CACHE_TTL', 1000); // 1000 if CACHE_TTL is not set + +// Supported units: ms, s, m, h, d, w (case-insensitive, decimals allowed) +// Returns null if the value and default are both unparseable. ``` #### URLs and IPs diff --git a/package.json b/package.json index 803e143..04e2b48 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "good-env", - "version": "7.6.3", + "version": "7.7.0", "description": "Better environment variable handling for Twelve-Factor node apps", "main": "src/index.js", "scripts": { diff --git a/src/index.d.ts b/src/index.d.ts index a4760c1..7652a84 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -125,6 +125,24 @@ declare module "good-env" { * */ export const num: (key: any, defaultVal?: any) => any; + /** + * @description Fetches the value at the given key and parses it as a + * duration string (e.g. "500ms", "30s", "5m", "2h", "1d", "1w") into + * milliseconds. The unit is case-insensitive and whitespace between the + * number and unit is allowed. If the env value is invalid, the default + * is parsed instead. Numeric defaults are treated as milliseconds. + * Returns null if nothing can be resolved. + * + * @param {string} key - A unique key + * @param {(string|number)} defaultVal - A duration string or number of ms + * + */ + export const getDuration: (key: string, defaultVal?: string | number) => number | null; + /** + * @description An alias function for getDuration() + * + */ + export const duration: (key: string, defaultVal?: string | number) => number | null; /** * @description Fetches the value at the given key and attempts to * coherse it into a list of literal values diff --git a/src/index.js b/src/index.js index 950ad2b..c2300c6 100644 --- a/src/index.js +++ b/src/index.js @@ -12,6 +12,22 @@ const isObject = x => is(x) === '[object Object]'; const isFunction = x => is(x) === '[object Function]'; const mapNums = items => items.map(t => parseFloat(t)); const validType = item => ['number', 'boolean', 'string'].includes(item); +const DURATION_UNITS = { + ms: 1, + s: 1000, + m: 60 * 1000, + h: 60 * 60 * 1000, + d: 24 * 60 * 60 * 1000, + w: 7 * 24 * 60 * 60 * 1000 +}; +const DURATION_RE = /^(\d+(?:\.\d+)?)\s*(ms|s|m|h|d|w)$/i; +const parseDuration = input => { + if (isNumber(input)) return Number.isFinite(input) ? input : null; + if (!isString(input)) return null; + const match = input.trim().match(DURATION_RE); + if (!match) return null; + return parseFloat(match[1]) * DURATION_UNITS[match[2].toLowerCase()]; +}; const store = { ...process.env }; module.exports = Object @@ -339,6 +355,34 @@ module.exports = Object return this.getNumber(key, defaultVal); }, + /** + * @description Fetches the value at the given key and parses it as a + * duration string (e.g. "500ms", "30s", "5m", "2h", "1d", "1w") into + * milliseconds. The unit is case-insensitive and whitespace between + * the number and unit is allowed. If the env value is invalid, the + * default is parsed instead. Numeric defaults are treated as + * milliseconds. Returns null if nothing can be resolved. + * + * @param {string} key - A unique key + * @param {(string|number)} defaultVal - A duration string or a number of ms + * + */ + getDuration (key, defaultVal) { + const parsed = parseDuration(this.get(key)); + if (parsed !== null) return parsed; + const parsedDefault = parseDuration(defaultVal); + if (parsedDefault !== null) return parsedDefault; + return null; + }, + + /** + * @description An alias function for getDuration() + * + */ + duration (key, defaultVal) { + return this.getDuration(key, defaultVal); + }, + /** * @description Fetches the value at the given key and attempts to * coerce it into a list of literal values diff --git a/test/test.env b/test/test.env index 07eca01..46be7d5 100644 --- a/test/test.env +++ b/test/test.env @@ -20,3 +20,13 @@ AWS_REGION=us-east-1 AWS_SESSION_TOKEN=session VALID_IP=192.168.1.60 INVALID_IP=nope +DURATION_MS=500ms +DURATION_S=30s +DURATION_M=5m +DURATION_H=2h +DURATION_D=1d +DURATION_W=1w +DURATION_FLOAT=1.5h +DURATION_UPPER=10S +DURATION_SPACED=5 m +DURATION_INVALID=nope diff --git a/test/test.js b/test/test.js index 43ec26e..e66f864 100644 --- a/test/test.js +++ b/test/test.js @@ -279,6 +279,52 @@ test('returns undefined for exequalsting non-number', (t) => { t.end(); }); +test('getDuration parses every supported unit', (t) => { + t.equals(env.getDuration('DURATION_MS'), 500); + t.equals(env.getDuration('DURATION_S'), 30 * 1000); + t.equals(env.getDuration('DURATION_M'), 5 * 60 * 1000); + t.equals(env.getDuration('DURATION_H'), 2 * 60 * 60 * 1000); + t.equals(env.getDuration('DURATION_D'), 24 * 60 * 60 * 1000); + t.equals(env.getDuration('DURATION_W'), 7 * 24 * 60 * 60 * 1000); + t.end(); +}); + +test('getDuration parses decimals, is case-insensitive, allows whitespace', (t) => { + t.equals(env.getDuration('DURATION_FLOAT'), 1.5 * 60 * 60 * 1000); + t.equals(env.getDuration('DURATION_UPPER'), 10 * 1000); + t.equals(env.getDuration('DURATION_SPACED'), 5 * 60 * 1000); + t.end(); +}); + +test('getDuration returns null for invalid env values', (t) => { + t.equals(env.getDuration('DURATION_INVALID'), null); + t.end(); +}); + +test('getDuration falls back to default when env value is invalid', (t) => { + t.equals(env.getDuration('DURATION_INVALID', '2s'), 2000); + t.end(); +}); + +test('getDuration resolves defaults for missing keys', (t) => { + t.equals(env.getDuration('DURATION_NOPE'), null); + t.equals(env.getDuration('DURATION_NOPE', '1m'), 60 * 1000); + t.equals(env.getDuration('DURATION_NOPE', 1234), 1234); + t.end(); +}); + +test('getDuration returns null for unparseable defaults', (t) => { + t.equals(env.getDuration('DURATION_NOPE', 'garbage'), null); + t.equals(env.getDuration('DURATION_NOPE', NaN), null); + t.equals(env.getDuration('DURATION_NOPE', {}), null); + t.end(); +}); + +test('duration() is an alias for getDuration()', (t) => { + t.equals(env.duration('DURATION_M'), 5 * 60 * 1000); + t.end(); +}); + test('returns a list of values', (t) => { let result = env.getList('MY_LIST'); t.equals(result.length, 3);