@app-config/extensions
Advanced tools
Comparing version 2.1.5 to 2.1.6
@@ -5,6 +5,8 @@ import { ParsingExtension } from '@app-config/core'; | ||
export declare function markAllValuesAsSecret(): ParsingExtension; | ||
/** When a key $$foo is seen, change it to be $foo and mark with meta property fromEscapedDirective */ | ||
export declare function unescape$Directives(): ParsingExtension; | ||
/** Uses another file as overriding values, layering them on top of current file */ | ||
export declare function overrideDirective(): ParsingExtension; | ||
/** Uses another file as a "base", and extends on top of it */ | ||
export declare function extendsDirective(): ParsingExtension; | ||
/** Uses another file as overriding values, layering them on top of current file */ | ||
export declare function overrideDirective(): ParsingExtension; | ||
/** Lookup a property in the same file, and "copy" it */ | ||
@@ -16,5 +18,3 @@ export declare function extendsSelfDirective(): ParsingExtension; | ||
export declare function timestampDirective(dateSource?: () => Date): ParsingExtension; | ||
/** When a key $$foo is seen, change it to be $foo and mark with meta property fromEscapedDirective */ | ||
export declare function unescape$Directives(): ParsingExtension; | ||
/** Substitues environment variables found in strings (similar to bash variable substitution) */ | ||
export declare function environmentVariableSubstitution(aliases?: EnvironmentAliases, environmentOverride?: string, environmentSourceNames?: string[] | string): ParsingExtension; |
import { join, dirname, resolve, isAbsolute } from 'path'; | ||
import { isObject } from '@app-config/utils'; | ||
import { forKey, validateOptions } from '@app-config/extension-utils'; | ||
import { ParsedValue, AppConfigError, NotFoundError, FailedToSelectSubObject, } from '@app-config/core'; | ||
@@ -10,5 +10,12 @@ import { currentEnvironment, defaultAliases, FileSource, } from '@app-config/node'; | ||
} | ||
/** Uses another file as a "base", and extends on top of it */ | ||
export function extendsDirective() { | ||
return fileReferenceDirective('$extends', { shouldMerge: true }); | ||
/** When a key $$foo is seen, change it to be $foo and mark with meta property fromEscapedDirective */ | ||
export function unescape$Directives() { | ||
return (value, [_, key]) => { | ||
if (typeof key === 'string' && key.startsWith('$$')) { | ||
return async (parse) => { | ||
return parse(value, { rewriteKey: key.slice(1), fromEscapedDirective: true }); | ||
}; | ||
} | ||
return false; | ||
}; | ||
} | ||
@@ -19,23 +26,19 @@ /** Uses another file as overriding values, layering them on top of current file */ | ||
} | ||
/** Uses another file as a "base", and extends on top of it */ | ||
export function extendsDirective() { | ||
return fileReferenceDirective('$extends', { shouldMerge: true }); | ||
} | ||
/** Lookup a property in the same file, and "copy" it */ | ||
export function extendsSelfDirective() { | ||
return (value, [_, key]) => { | ||
if (key !== '$extendsSelf') | ||
return false; | ||
return async (parse, _, __, ___, root) => { | ||
const selector = (await parse(value)).toJSON(); | ||
if (typeof selector !== 'string') { | ||
throw new AppConfigError(`$extendsSelf was provided a non-string value`); | ||
} | ||
// we temporarily use a ParsedValue literal so that we get the same property lookup semantics | ||
const selected = ParsedValue.literal(root).property(selector.split('.')); | ||
if (selected === undefined) { | ||
throw new AppConfigError(`$extendsSelf selector was not found (${selector})`); | ||
} | ||
if (selected.asObject() !== undefined) { | ||
return parse(selected.toJSON(), { shouldMerge: true }); | ||
} | ||
return parse(selected.toJSON(), { shouldFlatten: true }); | ||
}; | ||
}; | ||
return forKey('$extendsSelf', validateOptions((SchemaBuilder) => SchemaBuilder.stringSchema(), (value) => async (parse, _, __, ___, root) => { | ||
// we temporarily use a ParsedValue literal so that we get the same property lookup semantics | ||
const selected = ParsedValue.literal(root).property(value.split('.')); | ||
if (selected === undefined) { | ||
throw new AppConfigError(`$extendsSelf selector was not found (${value})`); | ||
} | ||
if (selected.asObject() !== undefined) { | ||
return parse(selected.toJSON(), { shouldMerge: true }); | ||
} | ||
return parse(selected.toJSON(), { shouldFlatten: true }); | ||
})); | ||
} | ||
@@ -46,209 +49,165 @@ /** Looks up an environment-specific value ($env) */ | ||
const metadata = { shouldOverride: true }; | ||
return (value, [_, key]) => { | ||
if (key === '$env') { | ||
return (parse) => { | ||
if (!isObject(value)) { | ||
throw new AppConfigError('An $env directive was used with a non-object value'); | ||
} | ||
if (!environment) { | ||
if (value.default) | ||
return parse(value.default, metadata); | ||
throw new AppConfigError(`An $env directive was used, but current environment (eg. NODE_ENV) is undefined`); | ||
} | ||
for (const [envName, envValue] of Object.entries(value)) { | ||
if (envName === environment || aliases[envName] === environment) { | ||
return parse(envValue, metadata); | ||
} | ||
} | ||
if ('default' in value) { | ||
return parse(value.default, metadata); | ||
} | ||
const found = Object.keys(value).join(', '); | ||
throw new AppConfigError(`An $env directive was used, but none matched the current environment (wanted ${environment}, saw [${found}])`); | ||
}; | ||
return forKey('$env', validateOptions((SchemaBuilder) => SchemaBuilder.emptySchema().addAdditionalProperties(), (value) => (parse) => { | ||
if (!environment) { | ||
if ('default' in value) { | ||
return parse(value.default, metadata); | ||
} | ||
throw new AppConfigError(`An $env directive was used, but current environment (eg. NODE_ENV) is undefined`); | ||
} | ||
return false; | ||
}; | ||
for (const [envName, envValue] of Object.entries(value)) { | ||
if (envName === environment || aliases[envName] === environment) { | ||
return parse(envValue, metadata); | ||
} | ||
} | ||
if ('default' in value) { | ||
return parse(value.default, metadata); | ||
} | ||
const found = Object.keys(value).join(', '); | ||
throw new AppConfigError(`An $env directive was used, but none matched the current environment (wanted ${environment}, saw [${found}])`); | ||
}, { lazy: true })); | ||
} | ||
/** Provides the current timestamp using { $timestamp: true } */ | ||
export function timestampDirective(dateSource = () => new Date()) { | ||
return (value, [_, key]) => { | ||
if (key === '$timestamp') { | ||
return async (parse) => { | ||
let formatted; | ||
const date = dateSource(); | ||
if (value === true) { | ||
formatted = date.toISOString(); | ||
} | ||
else if (value && typeof value === 'object' && !Array.isArray(value)) { | ||
const { locale, ...options } = value; | ||
if (typeof locale !== 'string') { | ||
throw new AppConfigError('$timestamp was provided a non-string locale'); | ||
} | ||
formatted = date.toLocaleDateString(locale, options); | ||
} | ||
else { | ||
throw new AppConfigError('$timestamp was provided an invalid option'); | ||
} | ||
return parse(formatted, { shouldFlatten: true }); | ||
}; | ||
return forKey('$timestamp', validateOptions((SchemaBuilder) => SchemaBuilder.oneOf(SchemaBuilder.booleanSchema(), SchemaBuilder.emptySchema() | ||
.addString('day', {}, false) | ||
.addString('month', {}, false) | ||
.addString('year', {}, false) | ||
.addString('weekday', {}, false) | ||
.addString('locale', {}, false) | ||
.addString('timeZone', {}, false) | ||
.addString('timeZoneName', {}, false)), (value) => (parse) => { | ||
let formatted; | ||
const date = dateSource(); | ||
if (value === true) { | ||
formatted = date.toISOString(); | ||
} | ||
return false; | ||
}; | ||
} | ||
/** When a key $$foo is seen, change it to be $foo and mark with meta property fromEscapedDirective */ | ||
export function unescape$Directives() { | ||
return (value, [_, key]) => { | ||
if (typeof key === 'string' && key.startsWith('$$')) { | ||
return async (parse) => { | ||
return parse(value, { rewriteKey: key.slice(1), fromEscapedDirective: true }); | ||
}; | ||
else if (typeof value === 'object') { | ||
const { locale, ...options } = value; | ||
formatted = date.toLocaleDateString(locale, options); | ||
} | ||
return false; | ||
}; | ||
else { | ||
throw new AppConfigError('$timestamp was provided an invalid option'); | ||
} | ||
return parse(formatted, { shouldFlatten: true }); | ||
})); | ||
} | ||
/** Substitues environment variables found in strings (similar to bash variable substitution) */ | ||
export function environmentVariableSubstitution(aliases = defaultAliases, environmentOverride, environmentSourceNames) { | ||
const performAllSubstitutions = (text) => { | ||
let output = text; | ||
/* eslint-disable-next-line no-constant-condition */ | ||
while (true) { | ||
// this regex matches: | ||
// $FOO | ||
// ${FOO} | ||
// ${FOO:-fallback} | ||
// ${FOO:-${FALLBACK}} | ||
// | ||
// var name is group 1 || 2 | ||
// fallback value is group 3 | ||
// https://regex101.com/r/6ZMmx7/3 | ||
const match = /\$(?:([a-zA-Z_]\w+)|(?:{([a-zA-Z_]\w+)(?::- *(.*?) *)?}))/g.exec(output); | ||
if (!match) | ||
break; | ||
const fullMatch = match[0]; | ||
const varName = match[1] || match[2]; | ||
const fallback = match[3]; | ||
if (varName) { | ||
const env = process.env[varName]; | ||
if (env !== undefined) { | ||
output = output.replace(fullMatch, env); | ||
} | ||
else if (fallback !== undefined) { | ||
// we'll recurse again, so that ${FOO:-${FALLBACK}} -> ${FALLBACK} -> value | ||
output = performAllSubstitutions(output.replace(fullMatch, fallback)); | ||
} | ||
else if (varName === 'APP_CONFIG_ENV') { | ||
const envType = environmentOverride ?? currentEnvironment(aliases, environmentSourceNames); | ||
if (!envType) { | ||
throw new AppConfigError(`Could not find environment variable ${varName}`); | ||
} | ||
// there's a special case for APP_CONFIG_ENV, which is always the envType | ||
output = output.replace(fullMatch, envType); | ||
} | ||
else { | ||
throw new AppConfigError(`Could not find environment variable ${varName}`); | ||
} | ||
const envType = environmentOverride ?? currentEnvironment(aliases, environmentSourceNames); | ||
return forKey(['$substitute', '$subs'], validateOptions((SchemaBuilder) => SchemaBuilder.oneOf(SchemaBuilder.stringSchema(), SchemaBuilder.emptySchema().addString('$name').addString('$fallback', {}, false)), (value) => (parse) => { | ||
if (typeof value === 'object') { | ||
const { $name: variableName, $fallback: fallback } = value; | ||
const resolvedValue = process.env[variableName]; | ||
if (fallback !== undefined) { | ||
return parse(resolvedValue || fallback, { shouldFlatten: true }); | ||
} | ||
if (!resolvedValue) { | ||
throw new AppConfigError(`$substitute could not find ${variableName} environment variable`); | ||
} | ||
return parse(resolvedValue, { shouldFlatten: true }); | ||
} | ||
logger.verbose(`Performed $substitute for "${text}" -> "${output}"`); | ||
return output; | ||
}; | ||
return (value, [_, key]) => { | ||
if (key === '$subsitute') | ||
logger.warn('Noticed a typo! Key of $subsitute was found.'); | ||
if (key === '$substitute' || key === '$subs') { | ||
return (parse) => { | ||
if (isObject(value)) { | ||
if (!value.$name) { | ||
throw new AppConfigError('$substitute was provided an object without $name'); | ||
} | ||
if (typeof value.$name !== 'string') { | ||
throw new AppConfigError('$substitute was provided an object without a string $name'); | ||
} | ||
const variableName = value.$name; | ||
const fallback = value.$fallback; | ||
const resolvedValue = process.env[variableName]; | ||
if (fallback !== undefined) { | ||
return parse(resolvedValue || fallback, { shouldFlatten: true }); | ||
} | ||
if (!resolvedValue) { | ||
throw new AppConfigError(`$substitute could not find ${variableName} environment variable`); | ||
} | ||
return parse(resolvedValue, { shouldFlatten: true }); | ||
} | ||
if (typeof value !== 'string') { | ||
throw new AppConfigError('$substitute expects a string value'); | ||
} | ||
return parse(performAllSubstitutions(value), { shouldFlatten: true }); | ||
}; | ||
} | ||
return false; | ||
}; | ||
return parse(performAllSubstitutions(value, envType), { shouldFlatten: true }); | ||
})); | ||
} | ||
// common logic for $extends and $override | ||
function fileReferenceDirective(keyName, meta) { | ||
return (value, [_, key]) => { | ||
if (key !== keyName) | ||
return false; | ||
return async (parse, _, context, extensions) => { | ||
const retrieveFile = async (filepath, subselector, isOptional = false) => { | ||
let resolvedPath = filepath; | ||
// resolve filepaths that are relative to the current FileSource | ||
if (!isAbsolute(filepath) && context instanceof FileSource) { | ||
resolvedPath = join(dirname(context.filePath), filepath); | ||
if (resolve(context.filePath) === resolvedPath) { | ||
throw new AppConfigError(`A ${keyName} directive resolved to it's own file (${resolvedPath}). Please use $extendsSelf instead.`); | ||
} | ||
return forKey(keyName, validateOptions((SchemaBuilder) => { | ||
const reference = SchemaBuilder.oneOf(SchemaBuilder.stringSchema(), SchemaBuilder.emptySchema() | ||
.addString('path') | ||
.addBoolean('optional', {}, false) | ||
.addString('select', {}, false)); | ||
return SchemaBuilder.oneOf(reference, SchemaBuilder.arraySchema(reference)); | ||
}, (value) => async (parse, _, context, extensions) => { | ||
const retrieveFile = async (filepath, subselector, isOptional = false) => { | ||
let resolvedPath = filepath; | ||
// resolve filepaths that are relative to the current FileSource | ||
if (!isAbsolute(filepath) && context instanceof FileSource) { | ||
resolvedPath = join(dirname(context.filePath), filepath); | ||
if (resolve(context.filePath) === resolvedPath) { | ||
throw new AppConfigError(`A ${keyName} directive resolved to it's own file (${resolvedPath}). Please use $extendsSelf instead.`); | ||
} | ||
logger.verbose(`Loading file for ${keyName}: ${resolvedPath}`); | ||
const source = new FileSource(resolvedPath); | ||
const parsed = await source.read(extensions).catch((error) => { | ||
if (error instanceof NotFoundError && isOptional) { | ||
return ParsedValue.literal({}); | ||
} | ||
throw error; | ||
}); | ||
if (subselector) { | ||
const found = parsed.property(subselector.split('.')); | ||
if (!found) { | ||
throw new FailedToSelectSubObject(`Failed to select ${subselector} in ${resolvedPath}`); | ||
} | ||
return found; | ||
} | ||
logger.verbose(`Loading file for ${keyName}: ${resolvedPath}`); | ||
const source = new FileSource(resolvedPath); | ||
const parsed = await source.read(extensions).catch((error) => { | ||
if (error instanceof NotFoundError && isOptional) { | ||
return ParsedValue.literal({}); | ||
} | ||
return parsed; | ||
}; | ||
const forOptions = async (options) => { | ||
const parsed = (await parse(options)).toJSON(); | ||
if (typeof parsed === 'string') { | ||
return retrieveFile(parsed); | ||
throw error; | ||
}); | ||
if (subselector) { | ||
const found = parsed.property(subselector.split('.')); | ||
if (!found) { | ||
throw new FailedToSelectSubObject(`Failed to select ${subselector} in ${resolvedPath}`); | ||
} | ||
if (!isObject(parsed)) { | ||
throw new AppConfigError(`${keyName} was provided an invalid option`); | ||
return found; | ||
} | ||
return parsed; | ||
}; | ||
let parsed; | ||
if (typeof value === 'string') { | ||
parsed = await retrieveFile(value); | ||
} | ||
else if (Array.isArray(value)) { | ||
parsed = ParsedValue.literal({}); | ||
for (const ext of value) { | ||
if (typeof ext === 'string') { | ||
parsed = ParsedValue.merge(parsed, await retrieveFile(ext)); | ||
} | ||
const { path, optional, select } = parsed; | ||
if (!path || typeof path !== 'string') { | ||
throw new AppConfigError(`Invalid ${keyName} filepath found`); | ||
else { | ||
const { path, optional, select } = ext; | ||
parsed = ParsedValue.merge(parsed, await retrieveFile(path, select, optional)); | ||
} | ||
if (select !== undefined && typeof select !== 'string') { | ||
throw new AppConfigError(`Invalid ${keyName} select found`); | ||
} | ||
} | ||
else { | ||
const { path, optional, select } = value; | ||
parsed = await retrieveFile(path, select, optional); | ||
} | ||
return parsed.assignMeta(meta); | ||
})); | ||
} | ||
function performAllSubstitutions(text, envType) { | ||
let output = text; | ||
/* eslint-disable-next-line no-constant-condition */ | ||
while (true) { | ||
// this regex matches: | ||
// $FOO | ||
// ${FOO} | ||
// ${FOO:-fallback} | ||
// ${FOO:-${FALLBACK}} | ||
// | ||
// var name is group 1 || 2 | ||
// fallback value is group 3 | ||
// https://regex101.com/r/6ZMmx7/3 | ||
const match = /\$(?:([a-zA-Z_]\w+)|(?:{([a-zA-Z_]\w+)(?::- *(.*?) *)?}))/g.exec(output); | ||
if (!match) | ||
break; | ||
const fullMatch = match[0]; | ||
const varName = match[1] || match[2]; | ||
const fallback = match[3]; | ||
if (varName) { | ||
const env = process.env[varName]; | ||
if (env !== undefined) { | ||
output = output.replace(fullMatch, env); | ||
} | ||
else if (fallback !== undefined) { | ||
// we'll recurse again, so that ${FOO:-${FALLBACK}} -> ${FALLBACK} -> value | ||
output = performAllSubstitutions(output.replace(fullMatch, fallback), envType); | ||
} | ||
else if (varName === 'APP_CONFIG_ENV') { | ||
if (!envType) { | ||
throw new AppConfigError(`Could not find environment variable ${varName}`); | ||
} | ||
if (optional !== undefined && typeof optional !== 'boolean') { | ||
throw new AppConfigError(`Invalid ${keyName} optional found`); | ||
} | ||
return retrieveFile(path, select, optional); | ||
}; | ||
let parsed; | ||
if (Array.isArray(value)) { | ||
parsed = ParsedValue.literal({}); | ||
for (const ext of value) { | ||
parsed = ParsedValue.merge(parsed, await forOptions(ext)); | ||
} | ||
// there's a special case for APP_CONFIG_ENV, which is always the envType | ||
output = output.replace(fullMatch, envType); | ||
} | ||
else { | ||
parsed = await forOptions(value); | ||
throw new AppConfigError(`Could not find environment variable ${varName}`); | ||
} | ||
return parsed.assignMeta(meta); | ||
}; | ||
}; | ||
} | ||
} | ||
logger.verbose(`Performed $substitute for "${text}" -> "${output}"`); | ||
return output; | ||
} | ||
//# sourceMappingURL=index.js.map |
@@ -5,6 +5,8 @@ import { ParsingExtension } from '@app-config/core'; | ||
export declare function markAllValuesAsSecret(): ParsingExtension; | ||
/** When a key $$foo is seen, change it to be $foo and mark with meta property fromEscapedDirective */ | ||
export declare function unescape$Directives(): ParsingExtension; | ||
/** Uses another file as overriding values, layering them on top of current file */ | ||
export declare function overrideDirective(): ParsingExtension; | ||
/** Uses another file as a "base", and extends on top of it */ | ||
export declare function extendsDirective(): ParsingExtension; | ||
/** Uses another file as overriding values, layering them on top of current file */ | ||
export declare function overrideDirective(): ParsingExtension; | ||
/** Lookup a property in the same file, and "copy" it */ | ||
@@ -16,5 +18,3 @@ export declare function extendsSelfDirective(): ParsingExtension; | ||
export declare function timestampDirective(dateSource?: () => Date): ParsingExtension; | ||
/** When a key $$foo is seen, change it to be $foo and mark with meta property fromEscapedDirective */ | ||
export declare function unescape$Directives(): ParsingExtension; | ||
/** Substitues environment variables found in strings (similar to bash variable substitution) */ | ||
export declare function environmentVariableSubstitution(aliases?: EnvironmentAliases, environmentOverride?: string, environmentSourceNames?: string[] | string): ParsingExtension; |
@@ -14,5 +14,5 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.environmentVariableSubstitution = exports.unescape$Directives = exports.timestampDirective = exports.envDirective = exports.extendsSelfDirective = exports.overrideDirective = exports.extendsDirective = exports.markAllValuesAsSecret = void 0; | ||
exports.environmentVariableSubstitution = exports.timestampDirective = exports.envDirective = exports.extendsSelfDirective = exports.extendsDirective = exports.overrideDirective = exports.unescape$Directives = exports.markAllValuesAsSecret = void 0; | ||
const path_1 = require("path"); | ||
const utils_1 = require("@app-config/utils"); | ||
const extension_utils_1 = require("@app-config/extension-utils"); | ||
const core_1 = require("@app-config/core"); | ||
@@ -26,7 +26,14 @@ const node_1 = require("@app-config/node"); | ||
exports.markAllValuesAsSecret = markAllValuesAsSecret; | ||
/** Uses another file as a "base", and extends on top of it */ | ||
function extendsDirective() { | ||
return fileReferenceDirective('$extends', { shouldMerge: true }); | ||
/** When a key $$foo is seen, change it to be $foo and mark with meta property fromEscapedDirective */ | ||
function unescape$Directives() { | ||
return (value, [_, key]) => { | ||
if (typeof key === 'string' && key.startsWith('$$')) { | ||
return async (parse) => { | ||
return parse(value, { rewriteKey: key.slice(1), fromEscapedDirective: true }); | ||
}; | ||
} | ||
return false; | ||
}; | ||
} | ||
exports.extendsDirective = extendsDirective; | ||
exports.unescape$Directives = unescape$Directives; | ||
/** Uses another file as overriding values, layering them on top of current file */ | ||
@@ -37,23 +44,20 @@ function overrideDirective() { | ||
exports.overrideDirective = overrideDirective; | ||
/** Uses another file as a "base", and extends on top of it */ | ||
function extendsDirective() { | ||
return fileReferenceDirective('$extends', { shouldMerge: true }); | ||
} | ||
exports.extendsDirective = extendsDirective; | ||
/** Lookup a property in the same file, and "copy" it */ | ||
function extendsSelfDirective() { | ||
return (value, [_, key]) => { | ||
if (key !== '$extendsSelf') | ||
return false; | ||
return async (parse, _, __, ___, root) => { | ||
const selector = (await parse(value)).toJSON(); | ||
if (typeof selector !== 'string') { | ||
throw new core_1.AppConfigError(`$extendsSelf was provided a non-string value`); | ||
} | ||
// we temporarily use a ParsedValue literal so that we get the same property lookup semantics | ||
const selected = core_1.ParsedValue.literal(root).property(selector.split('.')); | ||
if (selected === undefined) { | ||
throw new core_1.AppConfigError(`$extendsSelf selector was not found (${selector})`); | ||
} | ||
if (selected.asObject() !== undefined) { | ||
return parse(selected.toJSON(), { shouldMerge: true }); | ||
} | ||
return parse(selected.toJSON(), { shouldFlatten: true }); | ||
}; | ||
}; | ||
return extension_utils_1.forKey('$extendsSelf', extension_utils_1.validateOptions((SchemaBuilder) => SchemaBuilder.stringSchema(), (value) => async (parse, _, __, ___, root) => { | ||
// we temporarily use a ParsedValue literal so that we get the same property lookup semantics | ||
const selected = core_1.ParsedValue.literal(root).property(value.split('.')); | ||
if (selected === undefined) { | ||
throw new core_1.AppConfigError(`$extendsSelf selector was not found (${value})`); | ||
} | ||
if (selected.asObject() !== undefined) { | ||
return parse(selected.toJSON(), { shouldMerge: true }); | ||
} | ||
return parse(selected.toJSON(), { shouldFlatten: true }); | ||
})); | ||
} | ||
@@ -65,27 +69,20 @@ exports.extendsSelfDirective = extendsSelfDirective; | ||
const metadata = { shouldOverride: true }; | ||
return (value, [_, key]) => { | ||
if (key === '$env') { | ||
return (parse) => { | ||
if (!utils_1.isObject(value)) { | ||
throw new core_1.AppConfigError('An $env directive was used with a non-object value'); | ||
} | ||
if (!environment) { | ||
if (value.default) | ||
return parse(value.default, metadata); | ||
throw new core_1.AppConfigError(`An $env directive was used, but current environment (eg. NODE_ENV) is undefined`); | ||
} | ||
for (const [envName, envValue] of Object.entries(value)) { | ||
if (envName === environment || aliases[envName] === environment) { | ||
return parse(envValue, metadata); | ||
} | ||
} | ||
if ('default' in value) { | ||
return parse(value.default, metadata); | ||
} | ||
const found = Object.keys(value).join(', '); | ||
throw new core_1.AppConfigError(`An $env directive was used, but none matched the current environment (wanted ${environment}, saw [${found}])`); | ||
}; | ||
return extension_utils_1.forKey('$env', extension_utils_1.validateOptions((SchemaBuilder) => SchemaBuilder.emptySchema().addAdditionalProperties(), (value) => (parse) => { | ||
if (!environment) { | ||
if ('default' in value) { | ||
return parse(value.default, metadata); | ||
} | ||
throw new core_1.AppConfigError(`An $env directive was used, but current environment (eg. NODE_ENV) is undefined`); | ||
} | ||
return false; | ||
}; | ||
for (const [envName, envValue] of Object.entries(value)) { | ||
if (envName === environment || aliases[envName] === environment) { | ||
return parse(envValue, metadata); | ||
} | ||
} | ||
if ('default' in value) { | ||
return parse(value.default, metadata); | ||
} | ||
const found = Object.keys(value).join(', '); | ||
throw new core_1.AppConfigError(`An $env directive was used, but none matched the current environment (wanted ${environment}, saw [${found}])`); | ||
}, { lazy: true })); | ||
} | ||
@@ -95,116 +92,43 @@ exports.envDirective = envDirective; | ||
function timestampDirective(dateSource = () => new Date()) { | ||
return (value, [_, key]) => { | ||
if (key === '$timestamp') { | ||
return async (parse) => { | ||
let formatted; | ||
const date = dateSource(); | ||
if (value === true) { | ||
formatted = date.toISOString(); | ||
} | ||
else if (value && typeof value === 'object' && !Array.isArray(value)) { | ||
const { locale } = value, options = __rest(value, ["locale"]); | ||
if (typeof locale !== 'string') { | ||
throw new core_1.AppConfigError('$timestamp was provided a non-string locale'); | ||
} | ||
formatted = date.toLocaleDateString(locale, options); | ||
} | ||
else { | ||
throw new core_1.AppConfigError('$timestamp was provided an invalid option'); | ||
} | ||
return parse(formatted, { shouldFlatten: true }); | ||
}; | ||
return extension_utils_1.forKey('$timestamp', extension_utils_1.validateOptions((SchemaBuilder) => SchemaBuilder.oneOf(SchemaBuilder.booleanSchema(), SchemaBuilder.emptySchema() | ||
.addString('day', {}, false) | ||
.addString('month', {}, false) | ||
.addString('year', {}, false) | ||
.addString('weekday', {}, false) | ||
.addString('locale', {}, false) | ||
.addString('timeZone', {}, false) | ||
.addString('timeZoneName', {}, false)), (value) => (parse) => { | ||
let formatted; | ||
const date = dateSource(); | ||
if (value === true) { | ||
formatted = date.toISOString(); | ||
} | ||
return false; | ||
}; | ||
else if (typeof value === 'object') { | ||
const { locale } = value, options = __rest(value, ["locale"]); | ||
formatted = date.toLocaleDateString(locale, options); | ||
} | ||
else { | ||
throw new core_1.AppConfigError('$timestamp was provided an invalid option'); | ||
} | ||
return parse(formatted, { shouldFlatten: true }); | ||
})); | ||
} | ||
exports.timestampDirective = timestampDirective; | ||
/** When a key $$foo is seen, change it to be $foo and mark with meta property fromEscapedDirective */ | ||
function unescape$Directives() { | ||
return (value, [_, key]) => { | ||
if (typeof key === 'string' && key.startsWith('$$')) { | ||
return async (parse) => { | ||
return parse(value, { rewriteKey: key.slice(1), fromEscapedDirective: true }); | ||
}; | ||
} | ||
return false; | ||
}; | ||
} | ||
exports.unescape$Directives = unescape$Directives; | ||
/** Substitues environment variables found in strings (similar to bash variable substitution) */ | ||
function environmentVariableSubstitution(aliases = node_1.defaultAliases, environmentOverride, environmentSourceNames) { | ||
const performAllSubstitutions = (text) => { | ||
let output = text; | ||
/* eslint-disable-next-line no-constant-condition */ | ||
while (true) { | ||
// this regex matches: | ||
// $FOO | ||
// ${FOO} | ||
// ${FOO:-fallback} | ||
// ${FOO:-${FALLBACK}} | ||
// | ||
// var name is group 1 || 2 | ||
// fallback value is group 3 | ||
// https://regex101.com/r/6ZMmx7/3 | ||
const match = /\$(?:([a-zA-Z_]\w+)|(?:{([a-zA-Z_]\w+)(?::- *(.*?) *)?}))/g.exec(output); | ||
if (!match) | ||
break; | ||
const fullMatch = match[0]; | ||
const varName = match[1] || match[2]; | ||
const fallback = match[3]; | ||
if (varName) { | ||
const env = process.env[varName]; | ||
if (env !== undefined) { | ||
output = output.replace(fullMatch, env); | ||
} | ||
else if (fallback !== undefined) { | ||
// we'll recurse again, so that ${FOO:-${FALLBACK}} -> ${FALLBACK} -> value | ||
output = performAllSubstitutions(output.replace(fullMatch, fallback)); | ||
} | ||
else if (varName === 'APP_CONFIG_ENV') { | ||
const envType = environmentOverride !== null && environmentOverride !== void 0 ? environmentOverride : node_1.currentEnvironment(aliases, environmentSourceNames); | ||
if (!envType) { | ||
throw new core_1.AppConfigError(`Could not find environment variable ${varName}`); | ||
} | ||
// there's a special case for APP_CONFIG_ENV, which is always the envType | ||
output = output.replace(fullMatch, envType); | ||
} | ||
else { | ||
throw new core_1.AppConfigError(`Could not find environment variable ${varName}`); | ||
} | ||
const envType = environmentOverride !== null && environmentOverride !== void 0 ? environmentOverride : node_1.currentEnvironment(aliases, environmentSourceNames); | ||
return extension_utils_1.forKey(['$substitute', '$subs'], extension_utils_1.validateOptions((SchemaBuilder) => SchemaBuilder.oneOf(SchemaBuilder.stringSchema(), SchemaBuilder.emptySchema().addString('$name').addString('$fallback', {}, false)), (value) => (parse) => { | ||
if (typeof value === 'object') { | ||
const { $name: variableName, $fallback: fallback } = value; | ||
const resolvedValue = process.env[variableName]; | ||
if (fallback !== undefined) { | ||
return parse(resolvedValue || fallback, { shouldFlatten: true }); | ||
} | ||
if (!resolvedValue) { | ||
throw new core_1.AppConfigError(`$substitute could not find ${variableName} environment variable`); | ||
} | ||
return parse(resolvedValue, { shouldFlatten: true }); | ||
} | ||
logging_1.logger.verbose(`Performed $substitute for "${text}" -> "${output}"`); | ||
return output; | ||
}; | ||
return (value, [_, key]) => { | ||
if (key === '$subsitute') | ||
logging_1.logger.warn('Noticed a typo! Key of $subsitute was found.'); | ||
if (key === '$substitute' || key === '$subs') { | ||
return (parse) => { | ||
if (utils_1.isObject(value)) { | ||
if (!value.$name) { | ||
throw new core_1.AppConfigError('$substitute was provided an object without $name'); | ||
} | ||
if (typeof value.$name !== 'string') { | ||
throw new core_1.AppConfigError('$substitute was provided an object without a string $name'); | ||
} | ||
const variableName = value.$name; | ||
const fallback = value.$fallback; | ||
const resolvedValue = process.env[variableName]; | ||
if (fallback !== undefined) { | ||
return parse(resolvedValue || fallback, { shouldFlatten: true }); | ||
} | ||
if (!resolvedValue) { | ||
throw new core_1.AppConfigError(`$substitute could not find ${variableName} environment variable`); | ||
} | ||
return parse(resolvedValue, { shouldFlatten: true }); | ||
} | ||
if (typeof value !== 'string') { | ||
throw new core_1.AppConfigError('$substitute expects a string value'); | ||
} | ||
return parse(performAllSubstitutions(value), { shouldFlatten: true }); | ||
}; | ||
} | ||
return false; | ||
}; | ||
return parse(performAllSubstitutions(value, envType), { shouldFlatten: true }); | ||
})); | ||
} | ||
@@ -214,66 +138,101 @@ exports.environmentVariableSubstitution = environmentVariableSubstitution; | ||
function fileReferenceDirective(keyName, meta) { | ||
return (value, [_, key]) => { | ||
if (key !== keyName) | ||
return false; | ||
return async (parse, _, context, extensions) => { | ||
const retrieveFile = async (filepath, subselector, isOptional = false) => { | ||
let resolvedPath = filepath; | ||
// resolve filepaths that are relative to the current FileSource | ||
if (!path_1.isAbsolute(filepath) && context instanceof node_1.FileSource) { | ||
resolvedPath = path_1.join(path_1.dirname(context.filePath), filepath); | ||
if (path_1.resolve(context.filePath) === resolvedPath) { | ||
throw new core_1.AppConfigError(`A ${keyName} directive resolved to it's own file (${resolvedPath}). Please use $extendsSelf instead.`); | ||
} | ||
return extension_utils_1.forKey(keyName, extension_utils_1.validateOptions((SchemaBuilder) => { | ||
const reference = SchemaBuilder.oneOf(SchemaBuilder.stringSchema(), SchemaBuilder.emptySchema() | ||
.addString('path') | ||
.addBoolean('optional', {}, false) | ||
.addString('select', {}, false)); | ||
return SchemaBuilder.oneOf(reference, SchemaBuilder.arraySchema(reference)); | ||
}, (value) => async (parse, _, context, extensions) => { | ||
const retrieveFile = async (filepath, subselector, isOptional = false) => { | ||
let resolvedPath = filepath; | ||
// resolve filepaths that are relative to the current FileSource | ||
if (!path_1.isAbsolute(filepath) && context instanceof node_1.FileSource) { | ||
resolvedPath = path_1.join(path_1.dirname(context.filePath), filepath); | ||
if (path_1.resolve(context.filePath) === resolvedPath) { | ||
throw new core_1.AppConfigError(`A ${keyName} directive resolved to it's own file (${resolvedPath}). Please use $extendsSelf instead.`); | ||
} | ||
logging_1.logger.verbose(`Loading file for ${keyName}: ${resolvedPath}`); | ||
const source = new node_1.FileSource(resolvedPath); | ||
const parsed = await source.read(extensions).catch((error) => { | ||
if (error instanceof core_1.NotFoundError && isOptional) { | ||
return core_1.ParsedValue.literal({}); | ||
} | ||
throw error; | ||
}); | ||
if (subselector) { | ||
const found = parsed.property(subselector.split('.')); | ||
if (!found) { | ||
throw new core_1.FailedToSelectSubObject(`Failed to select ${subselector} in ${resolvedPath}`); | ||
} | ||
return found; | ||
} | ||
logging_1.logger.verbose(`Loading file for ${keyName}: ${resolvedPath}`); | ||
const source = new node_1.FileSource(resolvedPath); | ||
const parsed = await source.read(extensions).catch((error) => { | ||
if (error instanceof core_1.NotFoundError && isOptional) { | ||
return core_1.ParsedValue.literal({}); | ||
} | ||
return parsed; | ||
}; | ||
const forOptions = async (options) => { | ||
const parsed = (await parse(options)).toJSON(); | ||
if (typeof parsed === 'string') { | ||
return retrieveFile(parsed); | ||
throw error; | ||
}); | ||
if (subselector) { | ||
const found = parsed.property(subselector.split('.')); | ||
if (!found) { | ||
throw new core_1.FailedToSelectSubObject(`Failed to select ${subselector} in ${resolvedPath}`); | ||
} | ||
if (!utils_1.isObject(parsed)) { | ||
throw new core_1.AppConfigError(`${keyName} was provided an invalid option`); | ||
return found; | ||
} | ||
return parsed; | ||
}; | ||
let parsed; | ||
if (typeof value === 'string') { | ||
parsed = await retrieveFile(value); | ||
} | ||
else if (Array.isArray(value)) { | ||
parsed = core_1.ParsedValue.literal({}); | ||
for (const ext of value) { | ||
if (typeof ext === 'string') { | ||
parsed = core_1.ParsedValue.merge(parsed, await retrieveFile(ext)); | ||
} | ||
const { path, optional, select } = parsed; | ||
if (!path || typeof path !== 'string') { | ||
throw new core_1.AppConfigError(`Invalid ${keyName} filepath found`); | ||
else { | ||
const { path, optional, select } = ext; | ||
parsed = core_1.ParsedValue.merge(parsed, await retrieveFile(path, select, optional)); | ||
} | ||
if (select !== undefined && typeof select !== 'string') { | ||
throw new core_1.AppConfigError(`Invalid ${keyName} select found`); | ||
} | ||
} | ||
else { | ||
const { path, optional, select } = value; | ||
parsed = await retrieveFile(path, select, optional); | ||
} | ||
return parsed.assignMeta(meta); | ||
})); | ||
} | ||
function performAllSubstitutions(text, envType) { | ||
let output = text; | ||
/* eslint-disable-next-line no-constant-condition */ | ||
while (true) { | ||
// this regex matches: | ||
// $FOO | ||
// ${FOO} | ||
// ${FOO:-fallback} | ||
// ${FOO:-${FALLBACK}} | ||
// | ||
// var name is group 1 || 2 | ||
// fallback value is group 3 | ||
// https://regex101.com/r/6ZMmx7/3 | ||
const match = /\$(?:([a-zA-Z_]\w+)|(?:{([a-zA-Z_]\w+)(?::- *(.*?) *)?}))/g.exec(output); | ||
if (!match) | ||
break; | ||
const fullMatch = match[0]; | ||
const varName = match[1] || match[2]; | ||
const fallback = match[3]; | ||
if (varName) { | ||
const env = process.env[varName]; | ||
if (env !== undefined) { | ||
output = output.replace(fullMatch, env); | ||
} | ||
else if (fallback !== undefined) { | ||
// we'll recurse again, so that ${FOO:-${FALLBACK}} -> ${FALLBACK} -> value | ||
output = performAllSubstitutions(output.replace(fullMatch, fallback), envType); | ||
} | ||
else if (varName === 'APP_CONFIG_ENV') { | ||
if (!envType) { | ||
throw new core_1.AppConfigError(`Could not find environment variable ${varName}`); | ||
} | ||
if (optional !== undefined && typeof optional !== 'boolean') { | ||
throw new core_1.AppConfigError(`Invalid ${keyName} optional found`); | ||
} | ||
return retrieveFile(path, select, optional); | ||
}; | ||
let parsed; | ||
if (Array.isArray(value)) { | ||
parsed = core_1.ParsedValue.literal({}); | ||
for (const ext of value) { | ||
parsed = core_1.ParsedValue.merge(parsed, await forOptions(ext)); | ||
} | ||
// there's a special case for APP_CONFIG_ENV, which is always the envType | ||
output = output.replace(fullMatch, envType); | ||
} | ||
else { | ||
parsed = await forOptions(value); | ||
throw new core_1.AppConfigError(`Could not find environment variable ${varName}`); | ||
} | ||
return parsed.assignMeta(meta); | ||
}; | ||
}; | ||
} | ||
} | ||
logging_1.logger.verbose(`Performed $substitute for "${text}" -> "${output}"`); | ||
return output; | ||
} | ||
//# sourceMappingURL=index.js.map |
{ | ||
"name": "@app-config/extensions", | ||
"description": "Common parsing extensions for @app-config", | ||
"version": "2.1.5", | ||
"version": "2.1.6", | ||
"license": "MPL-2.0", | ||
@@ -33,9 +33,10 @@ "author": { | ||
"dependencies": { | ||
"@app-config/core": "^2.1.5", | ||
"@app-config/logging": "^2.1.5", | ||
"@app-config/node": "^2.1.5", | ||
"@app-config/utils": "^2.1.5" | ||
"@app-config/core": "^2.1.6", | ||
"@app-config/extension-utils": "^2.1.6", | ||
"@app-config/logging": "^2.1.6", | ||
"@app-config/node": "^2.1.6", | ||
"@app-config/utils": "^2.1.6" | ||
}, | ||
"devDependencies": { | ||
"@app-config/test-utils": "^2.1.5" | ||
"@app-config/test-utils": "^2.1.6" | ||
}, | ||
@@ -42,0 +43,0 @@ "prettier": "@lcdev/prettier", |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
42776
5
476
1
+ Added@app-config/extension-utils@2.8.7(transitive)
+ Added@serafin/schema-builder@0.14.2(transitive)
+ Addedajv@6.12.6(transitive)
+ Addedassert-plus@1.0.0(transitive)
+ Addedcore-util-is@1.0.2(transitive)
+ Addedextsprintf@1.4.1(transitive)
+ Addedfast-deep-equal@3.1.3(transitive)
+ Addedfast-json-stable-stringify@2.1.0(transitive)
+ Addedjson-schema-traverse@0.4.1(transitive)
+ Addedpunycode@2.3.1(transitive)
+ Addeduri-js@4.4.1(transitive)
+ Addedverror@1.10.1(transitive)
Updated@app-config/core@^2.1.6
Updated@app-config/logging@^2.1.6
Updated@app-config/node@^2.1.6
Updated@app-config/utils@^2.1.6