-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #78 from openkfw/77-refactor-env-config
fix: fix env variable validation
- Loading branch information
Showing
6 changed files
with
591 additions
and
309 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import { z } from 'zod'; | ||
|
||
type EnvConfig<T extends EnvVariablesConfiguration> = { | ||
variables: T; | ||
groups: EnvVariableGroupConfiguration<T>[]; | ||
}; | ||
|
||
// Environment variable specific configuration | ||
type EnvVariablesConfiguration = { | ||
[key: string]: { | ||
defaultRule: z.ZodTypeAny; | ||
required?: boolean; | ||
stages?: ('development' | 'test' | 'build' | 'production' | 'lint')[]; | ||
regex?: RegExp; | ||
}; | ||
}; | ||
|
||
// Configurations for a collection of environment variables | ||
type EnvVariableGroupConfiguration<T> = { | ||
variables: (keyof T)[]; | ||
mode: 'none_or_all' | 'at_least_one'; | ||
stages?: ('development' | 'test' | 'build' | 'production' | 'lint')[]; | ||
errorMessage: string; | ||
}; | ||
|
||
// Zod schema that is created based on a environment config | ||
type ZodSchemaType<TVariables extends EnvVariablesConfiguration> = z.ZodObject<{ | ||
[K in keyof TVariables]: TVariables[K]['defaultRule']; | ||
}>; | ||
|
||
export declare const createEnvConfig: <T extends EnvVariablesConfiguration>(props: { | ||
variables: T; | ||
groups: EnvConfig<T>['groups']; | ||
}) => EnvConfig<T>; | ||
|
||
export declare const createZodSchemaFromEnvConfig: <T extends EnvVariablesConfiguration>( | ||
envConfig: EnvConfig<T>, | ||
) => ReturnType<ZodSchemaType<T>['superRefine']>; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
/* eslint-disable @typescript-eslint/no-var-requires */ | ||
|
||
const { z } = require('zod'); | ||
|
||
// Returns an environment config | ||
const createEnvConfig = (props) => { | ||
return { groups: props.groups, variables: props.variables }; | ||
}; | ||
|
||
// Creates a zod schema (z.Object({...})) based on an environment configuration | ||
const createZodSchemaFromEnvConfig = (config) => { | ||
const zodSchema = {}; | ||
|
||
Object.keys(config.variables).map((variable) => { | ||
const key = variable; | ||
const variableConfig = config.variables[key]; | ||
const defaultRule = variableConfig.defaultRule; | ||
zodSchema[variable] = defaultRule; | ||
}); | ||
|
||
return z.object(zodSchema); | ||
}; | ||
|
||
// Adds stage (build/lint/...) specific environment variable validation to a zod schema | ||
const addVariableValidation = (schema, envConfig) => { | ||
const valueExists = (value) => (value?.toString().trim().length ?? 0) > 0; | ||
|
||
return schema.superRefine((values, ctx) => { | ||
const configKeys = Object.keys(envConfig.variables); | ||
const currentStage = values.STAGE; // lint/build/production/... | ||
|
||
// Validate single environment variables one-by-one | ||
configKeys.forEach((configKey) => { | ||
const variableConfig = envConfig.variables[configKey]; | ||
|
||
// Check if env variable should be checked at this stage | ||
const stagesApplies = variableConfig.stages?.some((stage) => stage === currentStage) ?? true; | ||
|
||
if (!stagesApplies) { | ||
return; | ||
} | ||
|
||
const value = values[configKey]; | ||
const valueIsSet = valueExists(value); | ||
|
||
// Optional env variables may be omitted | ||
if (!variableConfig.required && !valueIsSet) { | ||
return; | ||
} | ||
|
||
// Check that required env variables are set | ||
if (!valueIsSet) { | ||
ctx.addIssue({ | ||
message: `Environment variable '${String(configKey)}' is required, but not set`, | ||
code: z.ZodIssueCode.custom, | ||
fatal: variableConfig.required, | ||
path: [configKey], | ||
}); | ||
return; | ||
} | ||
|
||
// Check env variable against regex if configured | ||
if (!variableConfig.regex) { | ||
return; | ||
} | ||
|
||
const matchesRegex = variableConfig.regex.test(value); | ||
|
||
if (!matchesRegex) { | ||
ctx.addIssue({ | ||
message: `Environment variable '${String(configKey)}' has an invalid format`, | ||
code: z.ZodIssueCode.custom, | ||
path: [configKey], | ||
}); | ||
} | ||
}); | ||
|
||
// Validate env variable groups | ||
envConfig.groups.forEach((group) => { | ||
// Check if env variable group should be checked at this stage | ||
const stageApplies = group.stages?.some((stage) => stage === currentStage) ?? true; | ||
|
||
if (!stageApplies) { | ||
return; | ||
} | ||
|
||
const groupValues = group.variables.map((variable) => values[variable]); | ||
const noneAreSet = groupValues.every((value) => !valueExists(value)); | ||
const variableNames = group.variables.map((envVariable) => envVariable); | ||
|
||
if (group.mode === 'at_least_one' && noneAreSet) { | ||
ctx.addIssue({ message: group.errorMessage, code: z.ZodIssueCode.custom, path: variableNames }); | ||
return; | ||
} | ||
|
||
const someAreSet = !noneAreSet; | ||
const someAreNotSet = groupValues.some((value) => !valueExists(value)); | ||
|
||
if (group.mode === 'none_or_all' && someAreSet && someAreNotSet) { | ||
ctx.addIssue({ message: group.errorMessage, code: z.ZodIssueCode.custom, path: variableNames }); | ||
return; | ||
} | ||
}); | ||
}); | ||
}; | ||
|
||
const createValidatingZodSchemaFromEnvConfig = (envConfig) => { | ||
const zodSchema = createZodSchemaFromEnvConfig(envConfig); | ||
const validatingZodSchema = addVariableValidation(zodSchema, envConfig); | ||
return validatingZodSchema; | ||
}; | ||
|
||
module.exports = { | ||
createEnvConfig, | ||
createZodSchemaFromEnvConfig: createValidatingZodSchemaFromEnvConfig, | ||
}; |
Oops, something went wrong.