@app-config/extensions
Advanced tools
Comparing version
import { ParsingExtension } from '@app-config/core'; | ||
import { EnvironmentAliases } from '@app-config/node'; | ||
export { tryDirective } from './try-directive'; | ||
export { ifDirective } from './if-directive'; | ||
export { eqDirective } from './eq-directive'; | ||
export { hiddenDirective } from './hidden-directive'; | ||
export { envDirective } from './env-directive'; | ||
export { extendsDirective, extendsSelfDirective, overrideDirective } from './extends-directive'; | ||
export { timestampDirective } from './timestamp-directive'; | ||
export { envVarDirective } from './env-var-directive'; | ||
export { substituteDirective } from './substitute-directive'; | ||
export { substituteDirective as environmentVariableSubstitution } from './substitute-directive'; | ||
export { parseDirective } from './parse-directive'; | ||
/** Marks all values recursively as fromSecrets, so they do not trigger schema errors */ | ||
@@ -7,24 +17,1 @@ export declare function markAllValuesAsSecret(): ParsingExtension; | ||
export declare function unescape$Directives(): ParsingExtension; | ||
/** Try an operation, with a fallback ($try, $value and $fallback) */ | ||
export declare function tryDirective(): ParsingExtension; | ||
/** Checks a condition, uses then/else */ | ||
export declare function ifDirective(): ParsingExtension; | ||
/** Checks if two values are equal */ | ||
export declare function eqDirective(): ParsingExtension; | ||
/** Prpoerties that are removed, used by references */ | ||
export declare function hiddenDirective(): 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; | ||
/** Lookup a property in the same file, and "copy" it */ | ||
export declare function extendsSelfDirective(): ParsingExtension; | ||
/** Looks up an environment-specific value ($env) */ | ||
export declare function envDirective(aliases?: EnvironmentAliases, environmentOverride?: string, environmentSourceNames?: string[] | string): ParsingExtension; | ||
/** Provides the current timestamp using { $timestamp: true } */ | ||
export declare function timestampDirective(dateSource?: () => Date): ParsingExtension; | ||
/** Substitues environment variables */ | ||
export declare function envVarDirective(aliases?: EnvironmentAliases, environmentOverride?: string, environmentSourceNames?: string[] | string): ParsingExtension; | ||
/** Substitues environment variables found in strings (similar to bash variable substitution) */ | ||
export declare function substituteDirective(aliases?: EnvironmentAliases, environmentOverride?: string, environmentSourceNames?: string[] | string): ParsingExtension; | ||
export declare const environmentVariableSubstitution: typeof substituteDirective; |
@@ -1,13 +0,20 @@ | ||
import isEqual from 'lodash.isequal'; | ||
import { forKey, keysToPath, validateOptions, validationFunction, } from '@app-config/extension-utils'; | ||
import { ParsedValue, AppConfigError, NotFoundError, FailedToSelectSubObject, Fallbackable, InObject, } from '@app-config/core'; | ||
import { currentEnvironment, defaultAliases, resolveFilepath, FileSource, } from '@app-config/node'; | ||
import { logger } from '@app-config/logging'; | ||
import { named } from '@app-config/extension-utils'; | ||
export { tryDirective } from './try-directive'; | ||
export { ifDirective } from './if-directive'; | ||
export { eqDirective } from './eq-directive'; | ||
export { hiddenDirective } from './hidden-directive'; | ||
export { envDirective } from './env-directive'; | ||
export { extendsDirective, extendsSelfDirective, overrideDirective } from './extends-directive'; | ||
export { timestampDirective } from './timestamp-directive'; | ||
export { envVarDirective } from './env-var-directive'; | ||
export { substituteDirective } from './substitute-directive'; | ||
export { substituteDirective as environmentVariableSubstitution } from './substitute-directive'; | ||
export { parseDirective } from './parse-directive'; | ||
/** Marks all values recursively as fromSecrets, so they do not trigger schema errors */ | ||
export function markAllValuesAsSecret() { | ||
return (value) => (parse) => parse(value, { fromSecrets: true }); | ||
return named('markAllValuesAsSecret', (value) => (parse) => parse(value, { fromSecrets: 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]) => { | ||
return named('unescape$', (value, [_, key]) => { | ||
if (typeof key === 'string' && key.startsWith('$$')) { | ||
@@ -19,363 +26,4 @@ return async (parse) => { | ||
return false; | ||
}; | ||
} | ||
/** Try an operation, with a fallback ($try, $value and $fallback) */ | ||
export function tryDirective() { | ||
return forKey('$try', validateOptions((SchemaBuilder) => SchemaBuilder.emptySchema() | ||
.addProperty('$value', SchemaBuilder.fromJsonSchema({})) | ||
.addProperty('$fallback', SchemaBuilder.fromJsonSchema({})) | ||
.addBoolean('$unsafe', {}, false), (value) => async (parse) => { | ||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment | ||
const { $value, $fallback, $unsafe } = value; | ||
try { | ||
return await parse($value, { shouldFlatten: true }); | ||
} | ||
catch (error) { | ||
if (error instanceof Fallbackable || $unsafe) { | ||
return parse($fallback, { shouldFlatten: true }); | ||
} | ||
throw error; | ||
} | ||
}, { lazy: true })); | ||
} | ||
/** Checks a condition, uses then/else */ | ||
export function ifDirective() { | ||
return forKey('$if', validateOptions((SchemaBuilder) => SchemaBuilder.emptySchema() | ||
.addProperty('$check', SchemaBuilder.fromJsonSchema({})) | ||
.addProperty('$then', SchemaBuilder.fromJsonSchema({})) | ||
.addProperty('$else', SchemaBuilder.fromJsonSchema({})), (value) => async (parse) => { | ||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment | ||
const { $check, $then, $else } = value; | ||
const condition = (await parse($check)).toJSON(); | ||
if (condition) { | ||
return parse($then, { shouldFlatten: true }); | ||
} | ||
return parse($else, { shouldFlatten: true }); | ||
}, { lazy: true })); | ||
} | ||
/** Checks if two values are equal */ | ||
export function eqDirective() { | ||
return forKey('$eq', validateOptions((SchemaBuilder) => SchemaBuilder.arraySchema(SchemaBuilder.fromJsonSchema({})), (values) => async (parse) => { | ||
for (const a of values) { | ||
for (const b of values) { | ||
if (a === b) | ||
continue; | ||
if (isEqual(a, b)) | ||
continue; | ||
return parse(false, { shouldFlatten: true }); | ||
} | ||
} | ||
return parse(true, { shouldFlatten: true }); | ||
})); | ||
} | ||
/** Prpoerties that are removed, used by references */ | ||
export function hiddenDirective() { | ||
return forKey('$hidden', () => async (parse) => { | ||
return parse({}, { shouldMerge: true }); | ||
}); | ||
} | ||
/** Uses another file as overriding values, layering them on top of current file */ | ||
export function overrideDirective() { | ||
return fileReferenceDirective('$override', { shouldOverride: true }); | ||
} | ||
/** 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() { | ||
const validate = validationFunction(({ stringSchema }) => stringSchema()); | ||
return forKey('$extendsSelf', (input, key, ctx) => async (parse, _, __, ___, root) => { | ||
const value = (await parse(input)).toJSON(); | ||
validate(value, [...ctx, key]); | ||
// 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 }); | ||
}); | ||
} | ||
/** Looks up an environment-specific value ($env) */ | ||
export function envDirective(aliases = defaultAliases, environmentOverride, environmentSourceNames) { | ||
const environment = environmentOverride ?? currentEnvironment(aliases, environmentSourceNames); | ||
const metadata = { shouldOverride: true }; | ||
return forKey('$env', validateOptions((SchemaBuilder) => SchemaBuilder.emptySchema().addAdditionalProperties(), (value, _, ctx) => (parse) => { | ||
if (!environment) { | ||
if ('none' in value) { | ||
return parse(value.none, metadata); | ||
} | ||
if ('default' in value) { | ||
return parse(value.default, metadata); | ||
} | ||
throw new AppConfigError(`An $env directive was used (in ${keysToPath(ctx)}), 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 (in ${keysToPath(ctx)}), but none matched the current environment (wanted ${environment}, saw [${found}])`); | ||
}, | ||
// $env is lazy so that non-applicable envs don't get evaluated | ||
{ lazy: true })); | ||
} | ||
/** Provides the current timestamp using { $timestamp: true } */ | ||
export function timestampDirective(dateSource = () => new Date()) { | ||
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(); | ||
} | ||
else if (typeof value === 'object') { | ||
const { locale, ...options } = value; | ||
formatted = date.toLocaleDateString(locale, options); | ||
} | ||
else { | ||
throw new AppConfigError('$timestamp was provided an invalid option'); | ||
} | ||
return parse(formatted, { shouldFlatten: true }); | ||
})); | ||
} | ||
/** Substitues environment variables */ | ||
export function envVarDirective(aliases = defaultAliases, environmentOverride, environmentSourceNames) { | ||
const envType = environmentOverride ?? currentEnvironment(aliases, environmentSourceNames); | ||
return forKey('$envVar', (value, key, ctx) => async (parse) => { | ||
let name; | ||
let parseInt = false; | ||
let parseFloat = false; | ||
let parseBool = false; | ||
if (typeof value === 'string') { | ||
name = value; | ||
} | ||
else { | ||
validateObject(value, [...ctx, key]); | ||
if (Array.isArray(value)) | ||
throw new AppConfigError('$envVar was given an array'); | ||
const resolved = (await parse(value.name)).toJSON(); | ||
validateString(resolved, [...ctx, key, [InObject, 'name']]); | ||
parseInt = !!(await parse(value.parseInt)).toJSON(); | ||
parseFloat = !!(await parse(value.parseFloat)).toJSON(); | ||
parseBool = !!(await parse(value.parseBool)).toJSON(); | ||
name = resolved; | ||
} | ||
const parseValue = (strValue) => { | ||
if (parseInt) { | ||
const parsed = Number.parseInt(strValue, 10); | ||
if (Number.isNaN(parsed)) { | ||
throw new AppConfigError(`Failed to parseInt(${strValue})`); | ||
} | ||
return parse(parsed, { shouldFlatten: true }); | ||
} | ||
if (parseFloat) { | ||
const parsed = Number.parseFloat(strValue); | ||
if (Number.isNaN(parsed)) { | ||
throw new AppConfigError(`Failed to parseFloat(${strValue})`); | ||
} | ||
return parse(parsed, { shouldFlatten: true }); | ||
} | ||
if (parseBool) { | ||
const parsed = strValue.toLowerCase() !== 'false' && strValue !== '0'; | ||
return parse(parsed, { shouldFlatten: true }); | ||
} | ||
return parse(strValue, { shouldFlatten: true }); | ||
}; | ||
let resolvedValue = process.env[name]; | ||
if (!resolvedValue && name === 'APP_CONFIG_ENV') { | ||
resolvedValue = envType; | ||
} | ||
if (resolvedValue) { | ||
return parseValue(resolvedValue); | ||
} | ||
if (typeof value === 'object' && value.fallback !== undefined) { | ||
const fallback = (await parse(value.fallback)).toJSON(); | ||
const allowNull = (await parse(value.allowNull)).toJSON(); | ||
if (allowNull) { | ||
validateStringOrNull(fallback, [...ctx, key, [InObject, 'fallback']]); | ||
} | ||
else { | ||
validateString(fallback, [...ctx, key, [InObject, 'fallback']]); | ||
} | ||
return parseValue(fallback); | ||
} | ||
throw new AppConfigError(`$envVar could not find ${name} environment variable`); | ||
}); | ||
} | ||
/** Substitues environment variables found in strings (similar to bash variable substitution) */ | ||
export function substituteDirective(aliases = defaultAliases, environmentOverride, environmentSourceNames) { | ||
const envType = environmentOverride ?? currentEnvironment(aliases, environmentSourceNames); | ||
return forKey(['$substitute', '$subs'], (value, key, ctx) => async (parse) => { | ||
if (typeof value === 'string') { | ||
return parse(performAllSubstitutions(value, envType), { shouldFlatten: true }); | ||
} | ||
validateObject(value, [...ctx, key]); | ||
if (Array.isArray(value)) | ||
throw new AppConfigError('$substitute was given an array'); | ||
const name = (await parse(selectDefined(value.name, value.$name))).toJSON(); | ||
validateString(name, [...ctx, key, [InObject, 'name']]); | ||
const parseValue = async (strValue) => { | ||
const parseInt = (await parse(selectDefined(value.parseInt, value.$parseInt))).toJSON(); | ||
if (parseInt) { | ||
const parsed = Number.parseInt(strValue, 10); | ||
if (Number.isNaN(parsed)) { | ||
throw new AppConfigError(`Failed to parseInt(${strValue})`); | ||
} | ||
return parse(parsed, { shouldFlatten: true }); | ||
} | ||
const parseFloat = (await parse(selectDefined(value.parseFloat, value.$parseFloat))).toJSON(); | ||
if (parseFloat) { | ||
const parsed = Number.parseFloat(strValue); | ||
if (Number.isNaN(parsed)) { | ||
throw new AppConfigError(`Failed to parseFloat(${strValue})`); | ||
} | ||
return parse(parsed, { shouldFlatten: true }); | ||
} | ||
const parseBool = (await parse(selectDefined(value.parseBool, value.$parseBool))).toJSON(); | ||
if (parseBool) { | ||
const parsed = strValue.toLowerCase() !== 'false' && strValue !== '0'; | ||
return parse(parsed, { shouldFlatten: true }); | ||
} | ||
return parse(strValue, { shouldFlatten: true }); | ||
}; | ||
let resolvedValue = process.env[name]; | ||
if (!resolvedValue && name === 'APP_CONFIG_ENV') { | ||
resolvedValue = envType; | ||
} | ||
if (resolvedValue) { | ||
return parseValue(resolvedValue); | ||
} | ||
if (value.fallback !== undefined || value.$fallback !== undefined) { | ||
const fallback = (await parse(selectDefined(value.fallback, value.$fallback))).toJSON(); | ||
const allowNull = (await parse(selectDefined(value.allowNull, value.$allowNull))).toJSON(); | ||
if (allowNull) { | ||
validateStringOrNull(fallback, [...ctx, key, [InObject, 'fallback']]); | ||
} | ||
else { | ||
validateString(fallback, [...ctx, key, [InObject, 'fallback']]); | ||
} | ||
return parseValue(fallback); | ||
} | ||
throw new AppConfigError(`$substitute could not find ${name} environment variable`); | ||
}); | ||
} | ||
export const environmentVariableSubstitution = substituteDirective; | ||
// common logic for $extends and $override | ||
function fileReferenceDirective(keyName, meta) { | ||
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 (_, __, context, extensions) => { | ||
const retrieveFile = async (filepath, subselector, isOptional = false) => { | ||
const resolvedPath = resolveFilepath(context, filepath); | ||
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; | ||
} | ||
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)); | ||
} | ||
else { | ||
const { path, optional, select } = ext; | ||
parsed = ParsedValue.merge(parsed, await retrieveFile(path, select, optional)); | ||
} | ||
} | ||
} | ||
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}`); | ||
} | ||
// 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}`); | ||
} | ||
} | ||
} | ||
logger.verbose(`Performed $substitute for "${text}" -> "${output}"`); | ||
return output; | ||
} | ||
function selectDefined(...args) { | ||
for (const a of args) { | ||
if (a !== undefined) | ||
return a; | ||
} | ||
return undefined; | ||
} | ||
const validateObject = validationFunction(({ emptySchema }) => emptySchema().addAdditionalProperties()); | ||
const validateString = validationFunction(({ stringSchema }) => stringSchema()); | ||
const validateStringOrNull = validationFunction(({ fromJsonSchema }) => fromJsonSchema({ type: ['null', 'string'] })); | ||
//# sourceMappingURL=index.js.map |
import { ParsingExtension } from '@app-config/core'; | ||
import { EnvironmentAliases } from '@app-config/node'; | ||
export { tryDirective } from './try-directive'; | ||
export { ifDirective } from './if-directive'; | ||
export { eqDirective } from './eq-directive'; | ||
export { hiddenDirective } from './hidden-directive'; | ||
export { envDirective } from './env-directive'; | ||
export { extendsDirective, extendsSelfDirective, overrideDirective } from './extends-directive'; | ||
export { timestampDirective } from './timestamp-directive'; | ||
export { envVarDirective } from './env-var-directive'; | ||
export { substituteDirective } from './substitute-directive'; | ||
export { substituteDirective as environmentVariableSubstitution } from './substitute-directive'; | ||
export { parseDirective } from './parse-directive'; | ||
/** Marks all values recursively as fromSecrets, so they do not trigger schema errors */ | ||
@@ -7,24 +17,1 @@ export declare function markAllValuesAsSecret(): ParsingExtension; | ||
export declare function unescape$Directives(): ParsingExtension; | ||
/** Try an operation, with a fallback ($try, $value and $fallback) */ | ||
export declare function tryDirective(): ParsingExtension; | ||
/** Checks a condition, uses then/else */ | ||
export declare function ifDirective(): ParsingExtension; | ||
/** Checks if two values are equal */ | ||
export declare function eqDirective(): ParsingExtension; | ||
/** Prpoerties that are removed, used by references */ | ||
export declare function hiddenDirective(): 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; | ||
/** Lookup a property in the same file, and "copy" it */ | ||
export declare function extendsSelfDirective(): ParsingExtension; | ||
/** Looks up an environment-specific value ($env) */ | ||
export declare function envDirective(aliases?: EnvironmentAliases, environmentOverride?: string, environmentSourceNames?: string[] | string): ParsingExtension; | ||
/** Provides the current timestamp using { $timestamp: true } */ | ||
export declare function timestampDirective(dateSource?: () => Date): ParsingExtension; | ||
/** Substitues environment variables */ | ||
export declare function envVarDirective(aliases?: EnvironmentAliases, environmentOverride?: string, environmentSourceNames?: string[] | string): ParsingExtension; | ||
/** Substitues environment variables found in strings (similar to bash variable substitution) */ | ||
export declare function substituteDirective(aliases?: EnvironmentAliases, environmentOverride?: string, environmentSourceNames?: string[] | string): ParsingExtension; | ||
export declare const environmentVariableSubstitution: typeof substituteDirective; |
"use strict"; | ||
var __rest = (this && this.__rest) || function (s, e) { | ||
var t = {}; | ||
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) | ||
t[p] = s[p]; | ||
if (s != null && typeof Object.getOwnPropertySymbols === "function") | ||
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { | ||
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) | ||
t[p[i]] = s[p[i]]; | ||
} | ||
return t; | ||
}; | ||
var __importDefault = (this && this.__importDefault) || function (mod) { | ||
return (mod && mod.__esModule) ? mod : { "default": mod }; | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.environmentVariableSubstitution = exports.substituteDirective = exports.envVarDirective = exports.timestampDirective = exports.envDirective = exports.extendsSelfDirective = exports.extendsDirective = exports.overrideDirective = exports.hiddenDirective = exports.eqDirective = exports.ifDirective = exports.tryDirective = exports.unescape$Directives = exports.markAllValuesAsSecret = void 0; | ||
const lodash_isequal_1 = __importDefault(require("lodash.isequal")); | ||
exports.unescape$Directives = exports.markAllValuesAsSecret = exports.parseDirective = exports.environmentVariableSubstitution = exports.substituteDirective = exports.envVarDirective = exports.timestampDirective = exports.overrideDirective = exports.extendsSelfDirective = exports.extendsDirective = exports.envDirective = exports.hiddenDirective = exports.eqDirective = exports.ifDirective = exports.tryDirective = void 0; | ||
const extension_utils_1 = require("@app-config/extension-utils"); | ||
const core_1 = require("@app-config/core"); | ||
const node_1 = require("@app-config/node"); | ||
const logging_1 = require("@app-config/logging"); | ||
var try_directive_1 = require("./try-directive"); | ||
Object.defineProperty(exports, "tryDirective", { enumerable: true, get: function () { return try_directive_1.tryDirective; } }); | ||
var if_directive_1 = require("./if-directive"); | ||
Object.defineProperty(exports, "ifDirective", { enumerable: true, get: function () { return if_directive_1.ifDirective; } }); | ||
var eq_directive_1 = require("./eq-directive"); | ||
Object.defineProperty(exports, "eqDirective", { enumerable: true, get: function () { return eq_directive_1.eqDirective; } }); | ||
var hidden_directive_1 = require("./hidden-directive"); | ||
Object.defineProperty(exports, "hiddenDirective", { enumerable: true, get: function () { return hidden_directive_1.hiddenDirective; } }); | ||
var env_directive_1 = require("./env-directive"); | ||
Object.defineProperty(exports, "envDirective", { enumerable: true, get: function () { return env_directive_1.envDirective; } }); | ||
var extends_directive_1 = require("./extends-directive"); | ||
Object.defineProperty(exports, "extendsDirective", { enumerable: true, get: function () { return extends_directive_1.extendsDirective; } }); | ||
Object.defineProperty(exports, "extendsSelfDirective", { enumerable: true, get: function () { return extends_directive_1.extendsSelfDirective; } }); | ||
Object.defineProperty(exports, "overrideDirective", { enumerable: true, get: function () { return extends_directive_1.overrideDirective; } }); | ||
var timestamp_directive_1 = require("./timestamp-directive"); | ||
Object.defineProperty(exports, "timestampDirective", { enumerable: true, get: function () { return timestamp_directive_1.timestampDirective; } }); | ||
var env_var_directive_1 = require("./env-var-directive"); | ||
Object.defineProperty(exports, "envVarDirective", { enumerable: true, get: function () { return env_var_directive_1.envVarDirective; } }); | ||
var substitute_directive_1 = require("./substitute-directive"); | ||
Object.defineProperty(exports, "substituteDirective", { enumerable: true, get: function () { return substitute_directive_1.substituteDirective; } }); | ||
var substitute_directive_2 = require("./substitute-directive"); | ||
Object.defineProperty(exports, "environmentVariableSubstitution", { enumerable: true, get: function () { return substitute_directive_2.substituteDirective; } }); | ||
var parse_directive_1 = require("./parse-directive"); | ||
Object.defineProperty(exports, "parseDirective", { enumerable: true, get: function () { return parse_directive_1.parseDirective; } }); | ||
/** Marks all values recursively as fromSecrets, so they do not trigger schema errors */ | ||
function markAllValuesAsSecret() { | ||
return (value) => (parse) => parse(value, { fromSecrets: true }); | ||
return extension_utils_1.named('markAllValuesAsSecret', (value) => (parse) => parse(value, { fromSecrets: true })); | ||
} | ||
@@ -30,3 +36,3 @@ exports.markAllValuesAsSecret = markAllValuesAsSecret; | ||
function unescape$Directives() { | ||
return (value, [_, key]) => { | ||
return extension_utils_1.named('unescape$', (value, [_, key]) => { | ||
if (typeof key === 'string' && key.startsWith('$$')) { | ||
@@ -38,375 +44,5 @@ return async (parse) => { | ||
return false; | ||
}; | ||
}); | ||
} | ||
exports.unescape$Directives = unescape$Directives; | ||
/** Try an operation, with a fallback ($try, $value and $fallback) */ | ||
function tryDirective() { | ||
return extension_utils_1.forKey('$try', extension_utils_1.validateOptions((SchemaBuilder) => SchemaBuilder.emptySchema() | ||
.addProperty('$value', SchemaBuilder.fromJsonSchema({})) | ||
.addProperty('$fallback', SchemaBuilder.fromJsonSchema({})) | ||
.addBoolean('$unsafe', {}, false), (value) => async (parse) => { | ||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment | ||
const { $value, $fallback, $unsafe } = value; | ||
try { | ||
return await parse($value, { shouldFlatten: true }); | ||
} | ||
catch (error) { | ||
if (error instanceof core_1.Fallbackable || $unsafe) { | ||
return parse($fallback, { shouldFlatten: true }); | ||
} | ||
throw error; | ||
} | ||
}, { lazy: true })); | ||
} | ||
exports.tryDirective = tryDirective; | ||
/** Checks a condition, uses then/else */ | ||
function ifDirective() { | ||
return extension_utils_1.forKey('$if', extension_utils_1.validateOptions((SchemaBuilder) => SchemaBuilder.emptySchema() | ||
.addProperty('$check', SchemaBuilder.fromJsonSchema({})) | ||
.addProperty('$then', SchemaBuilder.fromJsonSchema({})) | ||
.addProperty('$else', SchemaBuilder.fromJsonSchema({})), (value) => async (parse) => { | ||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment | ||
const { $check, $then, $else } = value; | ||
const condition = (await parse($check)).toJSON(); | ||
if (condition) { | ||
return parse($then, { shouldFlatten: true }); | ||
} | ||
return parse($else, { shouldFlatten: true }); | ||
}, { lazy: true })); | ||
} | ||
exports.ifDirective = ifDirective; | ||
/** Checks if two values are equal */ | ||
function eqDirective() { | ||
return extension_utils_1.forKey('$eq', extension_utils_1.validateOptions((SchemaBuilder) => SchemaBuilder.arraySchema(SchemaBuilder.fromJsonSchema({})), (values) => async (parse) => { | ||
for (const a of values) { | ||
for (const b of values) { | ||
if (a === b) | ||
continue; | ||
if (lodash_isequal_1.default(a, b)) | ||
continue; | ||
return parse(false, { shouldFlatten: true }); | ||
} | ||
} | ||
return parse(true, { shouldFlatten: true }); | ||
})); | ||
} | ||
exports.eqDirective = eqDirective; | ||
/** Prpoerties that are removed, used by references */ | ||
function hiddenDirective() { | ||
return extension_utils_1.forKey('$hidden', () => async (parse) => { | ||
return parse({}, { shouldMerge: true }); | ||
}); | ||
} | ||
exports.hiddenDirective = hiddenDirective; | ||
/** Uses another file as overriding values, layering them on top of current file */ | ||
function overrideDirective() { | ||
return fileReferenceDirective('$override', { shouldOverride: true }); | ||
} | ||
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() { | ||
const validate = extension_utils_1.validationFunction(({ stringSchema }) => stringSchema()); | ||
return extension_utils_1.forKey('$extendsSelf', (input, key, ctx) => async (parse, _, __, ___, root) => { | ||
const value = (await parse(input)).toJSON(); | ||
validate(value, [...ctx, key]); | ||
// 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 }); | ||
}); | ||
} | ||
exports.extendsSelfDirective = extendsSelfDirective; | ||
/** Looks up an environment-specific value ($env) */ | ||
function envDirective(aliases = node_1.defaultAliases, environmentOverride, environmentSourceNames) { | ||
const environment = environmentOverride !== null && environmentOverride !== void 0 ? environmentOverride : node_1.currentEnvironment(aliases, environmentSourceNames); | ||
const metadata = { shouldOverride: true }; | ||
return extension_utils_1.forKey('$env', extension_utils_1.validateOptions((SchemaBuilder) => SchemaBuilder.emptySchema().addAdditionalProperties(), (value, _, ctx) => (parse) => { | ||
if (!environment) { | ||
if ('none' in value) { | ||
return parse(value.none, metadata); | ||
} | ||
if ('default' in value) { | ||
return parse(value.default, metadata); | ||
} | ||
throw new core_1.AppConfigError(`An $env directive was used (in ${extension_utils_1.keysToPath(ctx)}), 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 (in ${extension_utils_1.keysToPath(ctx)}), but none matched the current environment (wanted ${environment}, saw [${found}])`); | ||
}, | ||
// $env is lazy so that non-applicable envs don't get evaluated | ||
{ lazy: true })); | ||
} | ||
exports.envDirective = envDirective; | ||
/** Provides the current timestamp using { $timestamp: true } */ | ||
function timestampDirective(dateSource = () => new Date()) { | ||
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(); | ||
} | ||
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; | ||
/** Substitues environment variables */ | ||
function envVarDirective(aliases = node_1.defaultAliases, environmentOverride, environmentSourceNames) { | ||
const envType = environmentOverride !== null && environmentOverride !== void 0 ? environmentOverride : node_1.currentEnvironment(aliases, environmentSourceNames); | ||
return extension_utils_1.forKey('$envVar', (value, key, ctx) => async (parse) => { | ||
let name; | ||
let parseInt = false; | ||
let parseFloat = false; | ||
let parseBool = false; | ||
if (typeof value === 'string') { | ||
name = value; | ||
} | ||
else { | ||
validateObject(value, [...ctx, key]); | ||
if (Array.isArray(value)) | ||
throw new core_1.AppConfigError('$envVar was given an array'); | ||
const resolved = (await parse(value.name)).toJSON(); | ||
validateString(resolved, [...ctx, key, [core_1.InObject, 'name']]); | ||
parseInt = !!(await parse(value.parseInt)).toJSON(); | ||
parseFloat = !!(await parse(value.parseFloat)).toJSON(); | ||
parseBool = !!(await parse(value.parseBool)).toJSON(); | ||
name = resolved; | ||
} | ||
const parseValue = (strValue) => { | ||
if (parseInt) { | ||
const parsed = Number.parseInt(strValue, 10); | ||
if (Number.isNaN(parsed)) { | ||
throw new core_1.AppConfigError(`Failed to parseInt(${strValue})`); | ||
} | ||
return parse(parsed, { shouldFlatten: true }); | ||
} | ||
if (parseFloat) { | ||
const parsed = Number.parseFloat(strValue); | ||
if (Number.isNaN(parsed)) { | ||
throw new core_1.AppConfigError(`Failed to parseFloat(${strValue})`); | ||
} | ||
return parse(parsed, { shouldFlatten: true }); | ||
} | ||
if (parseBool) { | ||
const parsed = strValue.toLowerCase() !== 'false' && strValue !== '0'; | ||
return parse(parsed, { shouldFlatten: true }); | ||
} | ||
return parse(strValue, { shouldFlatten: true }); | ||
}; | ||
let resolvedValue = process.env[name]; | ||
if (!resolvedValue && name === 'APP_CONFIG_ENV') { | ||
resolvedValue = envType; | ||
} | ||
if (resolvedValue) { | ||
return parseValue(resolvedValue); | ||
} | ||
if (typeof value === 'object' && value.fallback !== undefined) { | ||
const fallback = (await parse(value.fallback)).toJSON(); | ||
const allowNull = (await parse(value.allowNull)).toJSON(); | ||
if (allowNull) { | ||
validateStringOrNull(fallback, [...ctx, key, [core_1.InObject, 'fallback']]); | ||
} | ||
else { | ||
validateString(fallback, [...ctx, key, [core_1.InObject, 'fallback']]); | ||
} | ||
return parseValue(fallback); | ||
} | ||
throw new core_1.AppConfigError(`$envVar could not find ${name} environment variable`); | ||
}); | ||
} | ||
exports.envVarDirective = envVarDirective; | ||
/** Substitues environment variables found in strings (similar to bash variable substitution) */ | ||
function substituteDirective(aliases = node_1.defaultAliases, environmentOverride, environmentSourceNames) { | ||
const envType = environmentOverride !== null && environmentOverride !== void 0 ? environmentOverride : node_1.currentEnvironment(aliases, environmentSourceNames); | ||
return extension_utils_1.forKey(['$substitute', '$subs'], (value, key, ctx) => async (parse) => { | ||
if (typeof value === 'string') { | ||
return parse(performAllSubstitutions(value, envType), { shouldFlatten: true }); | ||
} | ||
validateObject(value, [...ctx, key]); | ||
if (Array.isArray(value)) | ||
throw new core_1.AppConfigError('$substitute was given an array'); | ||
const name = (await parse(selectDefined(value.name, value.$name))).toJSON(); | ||
validateString(name, [...ctx, key, [core_1.InObject, 'name']]); | ||
const parseValue = async (strValue) => { | ||
const parseInt = (await parse(selectDefined(value.parseInt, value.$parseInt))).toJSON(); | ||
if (parseInt) { | ||
const parsed = Number.parseInt(strValue, 10); | ||
if (Number.isNaN(parsed)) { | ||
throw new core_1.AppConfigError(`Failed to parseInt(${strValue})`); | ||
} | ||
return parse(parsed, { shouldFlatten: true }); | ||
} | ||
const parseFloat = (await parse(selectDefined(value.parseFloat, value.$parseFloat))).toJSON(); | ||
if (parseFloat) { | ||
const parsed = Number.parseFloat(strValue); | ||
if (Number.isNaN(parsed)) { | ||
throw new core_1.AppConfigError(`Failed to parseFloat(${strValue})`); | ||
} | ||
return parse(parsed, { shouldFlatten: true }); | ||
} | ||
const parseBool = (await parse(selectDefined(value.parseBool, value.$parseBool))).toJSON(); | ||
if (parseBool) { | ||
const parsed = strValue.toLowerCase() !== 'false' && strValue !== '0'; | ||
return parse(parsed, { shouldFlatten: true }); | ||
} | ||
return parse(strValue, { shouldFlatten: true }); | ||
}; | ||
let resolvedValue = process.env[name]; | ||
if (!resolvedValue && name === 'APP_CONFIG_ENV') { | ||
resolvedValue = envType; | ||
} | ||
if (resolvedValue) { | ||
return parseValue(resolvedValue); | ||
} | ||
if (value.fallback !== undefined || value.$fallback !== undefined) { | ||
const fallback = (await parse(selectDefined(value.fallback, value.$fallback))).toJSON(); | ||
const allowNull = (await parse(selectDefined(value.allowNull, value.$allowNull))).toJSON(); | ||
if (allowNull) { | ||
validateStringOrNull(fallback, [...ctx, key, [core_1.InObject, 'fallback']]); | ||
} | ||
else { | ||
validateString(fallback, [...ctx, key, [core_1.InObject, 'fallback']]); | ||
} | ||
return parseValue(fallback); | ||
} | ||
throw new core_1.AppConfigError(`$substitute could not find ${name} environment variable`); | ||
}); | ||
} | ||
exports.substituteDirective = substituteDirective; | ||
exports.environmentVariableSubstitution = substituteDirective; | ||
// common logic for $extends and $override | ||
function fileReferenceDirective(keyName, meta) { | ||
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 (_, __, context, extensions) => { | ||
const retrieveFile = async (filepath, subselector, isOptional = false) => { | ||
const resolvedPath = node_1.resolveFilepath(context, filepath); | ||
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; | ||
} | ||
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)); | ||
} | ||
else { | ||
const { path, optional, select } = ext; | ||
parsed = core_1.ParsedValue.merge(parsed, await retrieveFile(path, select, optional)); | ||
} | ||
} | ||
} | ||
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}`); | ||
} | ||
// 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}`); | ||
} | ||
} | ||
} | ||
logging_1.logger.verbose(`Performed $substitute for "${text}" -> "${output}"`); | ||
return output; | ||
} | ||
function selectDefined(...args) { | ||
for (const a of args) { | ||
if (a !== undefined) | ||
return a; | ||
} | ||
return undefined; | ||
} | ||
const validateObject = extension_utils_1.validationFunction(({ emptySchema }) => emptySchema().addAdditionalProperties()); | ||
const validateString = extension_utils_1.validationFunction(({ stringSchema }) => stringSchema()); | ||
const validateStringOrNull = extension_utils_1.validationFunction(({ fromJsonSchema }) => fromJsonSchema({ type: ['null', 'string'] })); | ||
//# sourceMappingURL=index.js.map |
{ | ||
"name": "@app-config/extensions", | ||
"description": "Common parsing extensions for @app-config", | ||
"version": "2.4.6", | ||
"version": "2.5.0", | ||
"license": "MPL-2.0", | ||
@@ -33,11 +33,11 @@ "author": { | ||
"dependencies": { | ||
"@app-config/core": "^2.4.6", | ||
"@app-config/extension-utils": "^2.4.6", | ||
"@app-config/logging": "^2.4.6", | ||
"@app-config/node": "^2.4.6", | ||
"@app-config/utils": "^2.4.6", | ||
"@app-config/core": "^2.5.0", | ||
"@app-config/extension-utils": "^2.5.0", | ||
"@app-config/logging": "^2.5.0", | ||
"@app-config/node": "^2.5.0", | ||
"@app-config/utils": "^2.5.0", | ||
"lodash.isequal": "4" | ||
}, | ||
"devDependencies": { | ||
"@app-config/test-utils": "^2.4.6", | ||
"@app-config/test-utils": "^2.5.0", | ||
"@types/lodash.isequal": "4" | ||
@@ -44,0 +44,0 @@ }, |
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
107111
43.29%68
750%1177
38.96%1
Infinity%Updated
Updated
Updated
Updated