UNPKG

@nestjs/config

Version:

Configuration for NestJS: load .env files and custom configuration, validate it at startup, and read it through an injectable ConfigService.

106 lines (105 loc) • 3.94 kB
import { getProperty } from './property-path.util.js'; /** * Joi defaults to `allowUnknown: false` and `abortEarly: true`, which would * reject the unrelated variables that are always present in `process.env` and * report only the first failure. Previous versions of this package overrode * both, so they remain the defaults for Joi schemas unless the user opts out. * * Joi's `pattern()` messages also quote the rejected value (`with value * {:[.]}`), after any conversion the schema applied to it. The default * templates below leave it out, so that a secret that fails its format rule * doesn't end up in the bootstrap error (and the logs). */ const JOI_DEFAULT_LIBRARY_OPTIONS = { abortEarly: false, allowUnknown: true, }; const JOI_DEFAULT_MESSAGES = { 'string.pattern.base': '{{#label}} fails to match the required pattern: {{#regex}}', 'string.pattern.name': '{{#label}} fails to match the {{#name}} pattern', 'string.pattern.invert.base': '{{#label}} matches the inverted pattern: {{#regex}}', 'string.pattern.invert.name': '{{#label}} matches the inverted {{#name}} pattern', }; const REDACTED = '[redacted]'; /** * Validates config using a Standard Schema (e.g. Zod, Arktype). * @see https://standardschema.dev/ */ export async function validateWithStandardSchema(schema, config, options) { const result = await schema['~standard'].validate(config, getLibraryOptions(schema, options)); // A falsy "issues" value indicates success, see https://standardschema.dev/ if (result.issues) { return { error: new Error(formatIssues(result.issues, config)), value: config, }; } return { value: result.value }; } /** * Returns the names of the variables an object schema declares, or * `undefined` if they can't be told from the schema (e.g. a Zod object * wrapped in a "transform"). */ export function getDeclaredKeys(schema) { const untypedSchema = schema; try { const keys = schema['~standard'].vendor === 'joi' ? untypedSchema.describe?.().keys : // Zod and Valibot objects (untypedSchema.shape ?? untypedSchema.entries); return keys && typeof keys === 'object' ? new Set(Object.keys(keys)) : undefined; } catch { return undefined; } } function getLibraryOptions(schema, options) { if (schema['~standard'].vendor !== 'joi') { return options; } const libraryOptions = options?.libraryOptions; return { ...options, libraryOptions: { ...JOI_DEFAULT_LIBRARY_OPTIONS, ...libraryOptions, messages: { ...JOI_DEFAULT_MESSAGES, ...libraryOptions?.messages }, }, }; } function formatIssues(issues, config) { return issues .map(issue => { const message = redactValue(issue.message, getIssueValue(issue, config)); const path = formatIssuePath(issue.path); return path ? `${path}: ${message}` : message; }) .join('\n'); } function getIssueValue(issue, config) { let value = config; for (const segment of issue.path ?? []) { value = getProperty(value, typeof segment === 'object' && segment !== null ? segment.key : segment); } return issue.path?.length ? value : undefined; } /** * Takes the rejected value out of the message (some schema libraries quote * it), as it may be a secret that only has a typo */ function redactValue(message, value) { if (typeof value !== 'string' || value === '') { return message; } return [JSON.stringify(value), `"${value}"`, `'${value}'`].reduce((result, quoted) => result.split(quoted).join(REDACTED), message); } function formatIssuePath(path) { return (path ?? []) .map(segment => typeof segment === 'object' && segment !== null ? String(segment.key) : String(segment)) .join('.'); }