UNPKG

env-schema

Version:

Validate your env variables using Ajv with .env file support using Node.js built-in parseEnv

160 lines (135 loc) 4.05 kB
'use strict' const Ajv = require('ajv') const { parseEnv } = require('node:util') const { readFileSync } = require('node:fs') const { resolve } = require('node:path') const separator = { keyword: 'separator', type: 'string', metaSchema: { type: 'string', description: 'value separator' }, modifying: true, valid: true, errors: false, compile: (schema) => (data, { parentData: pData, parentDataProperty: pDataProperty }) => { pData[pDataProperty] = data === '' ? [] : data.split(schema) } } function expandVariables (obj) { // Expand environment variables in the format $VAR or ${VAR} for (const key in obj) { const value = obj[key] if (typeof value === 'string') { obj[key] = value.replace(/\$\{?([A-Z_][A-Z0-9_]*)\}?/gi, (match, varName) => { return obj[varName] !== undefined ? obj[varName] : match }) } } } const optsSchema = { type: 'object', required: ['schema'], properties: { schema: { type: 'object', additionalProperties: true }, data: { oneOf: [ { type: 'array', items: { type: 'object' }, minItems: 1 }, { type: 'object' } ], default: {} }, env: { type: 'boolean', default: true }, dotenv: { type: ['boolean', 'object'], default: false }, expandEnv: { type: ['boolean'], default: false }, ajv: { type: 'object', additionalProperties: true } } } const sharedAjvInstance = getDefaultInstance() const optsSchemaValidator = sharedAjvInstance.compile(optsSchema) function envSchema (_opts) { const opts = Object.assign({}, _opts) if (opts.schema?.[Symbol.for('fluent-schema-object')]) { opts.schema = opts.schema.valueOf() } const isOptionValid = optsSchemaValidator(opts) if (!isOptionValid) { const error = new Error(sharedAjvInstance.errorsText(optsSchemaValidator.errors, { dataVar: 'opts' })) error.errors = optsSchemaValidator.errors throw error } const { schema } = opts schema.additionalProperties = false let { data, dotenv, env, expandEnv } = opts if (!Array.isArray(data)) { data = [data] } let parsedEnv if (dotenv) { const dotenvOpts = typeof dotenv === 'object' ? dotenv : {} const path = dotenvOpts.path || '.env' const encoding = dotenvOpts.encoding || 'utf8' try { const envFileContent = readFileSync(resolve(path), encoding) parsedEnv = parseEnv(envFileContent) } catch (err) { // Silently ignore if file doesn't exist if (err.code !== 'ENOENT') { throw err } parsedEnv = {} } } /* istanbul ignore else */ if (env) { data.unshift(process.env) } if (parsedEnv) { data.unshift(parsedEnv) } const merge = {} data.forEach(d => Object.assign(merge, d)) if (expandEnv) { expandVariables(merge) } const ajv = chooseAjvInstance(sharedAjvInstance, opts.ajv) const valid = ajv.validate(schema, merge) if (!valid) { const error = new Error(ajv.errorsText(ajv.errors, { dataVar: 'env' })) error.errors = ajv.errors throw error } return merge } function chooseAjvInstance (defaultInstance, ajvOpts) { if (ajvOpts instanceof Ajv) { return ajvOpts } let ajv = defaultInstance if (typeof ajvOpts === 'object' && typeof ajvOpts.customOptions === 'function') { ajv = ajvOpts.customOptions(getDefaultInstance()) if (!(ajv instanceof Ajv)) { throw new TypeError('customOptions function must return an instance of Ajv') } } else if (typeof ajvOpts === 'object' && typeof ajvOpts.customOptions === 'object') { ajv = getDefaultInstance(ajvOpts.customOptions) } return ajv } function getDefaultInstance (overrideOpts = {}) { return new Ajv({ allErrors: true, removeAdditional: true, useDefaults: true, coerceTypes: true, allowUnionTypes: true, addUsedSchema: false, keywords: [separator], ...overrideOpts }) } envSchema.keywords = { separator } module.exports = envSchema module.exports.default = envSchema module.exports.envSchema = envSchema