Skip to content

Commit c266d8d

Browse files
committed
feat: add Zod validation support for configuration schema
1 parent a3a5c61 commit c266d8d

15 files changed

+1295
-9307
lines changed

lib/conditional.module.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export class ConditionalModule {
3737
options?: { timeout?: number; debug?: boolean },
3838
) {
3939
const { timeout = 5000, debug = true } = options ?? {};
40+
// eslint-disable-next-line @typescript-eslint/no-base-to-string
4041
const moduleName = getInstanceName(module) || module.toString();
4142

4243
const timer = setTimeout(() => {

lib/config.module.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,12 @@ import {
1414
VALIDATED_ENV_PROPNAME,
1515
} from './config.constants';
1616
import { ConfigService } from './config.service';
17-
import { ConfigFactory, ConfigModuleOptions } from './interfaces';
17+
import type {
18+
ConfigFactory,
19+
ConfigModuleOptions,
20+
ValidationSchema,
21+
} from './interfaces';
22+
import { ValidatorFactory, Validator } from './validators';
1823
import { ConfigFactoryKeyHost } from './utils';
1924
import { createConfigProvider } from './utils/create-config-factory.util';
2025
import { getRegistrationToken } from './utils/get-registration-token.util';
@@ -78,8 +83,13 @@ export class ConfigModule {
7883
this.assignVariablesToProcess(validatedConfig);
7984
} else if (options.validationSchema) {
8085
const validationOptions = this.getSchemaValidationOptions(options);
81-
const { error, value: validatedConfig } =
82-
options.validationSchema.validate(config, validationOptions);
86+
87+
// Create validator from schema
88+
const validator = this.createValidator(options.validationSchema);
89+
const { error, value: validatedConfig } = validator.validate(
90+
config,
91+
validationOptions,
92+
);
8393

8494
if (error) {
8595
throw new Error(`Config validation error: ${error.message}`);
@@ -253,4 +263,9 @@ export class ConfigModule {
253263
allowUnknown: true,
254264
};
255265
}
266+
267+
private static createValidator(schema: ValidationSchema): Validator {
268+
// Use the factory to create the appropriate validator
269+
return ValidatorFactory.createValidator(schema);
270+
}
256271
}

lib/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ export * from './config.service';
44
export * from './types';
55
export * from './utils';
66
export * from './interfaces';
7+
export * from './validators';

lib/interfaces/config-module-options.interface.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { DotenvExpandOptions } from 'dotenv-expand';
22
import { ConfigFactory } from './config-factory.interface';
3+
import { ValidationSchema } from './validation-schema.interface';
34

45
/**
56
* @publicApi
@@ -61,9 +62,9 @@ export interface ConfigModuleOptions<
6162
skipProcessEnv?: boolean;
6263

6364
/**
64-
* Environment variables validation schema (Joi).
65+
* Environment variables validation schema (Joi, Zod, or custom ValidationSchema).
6566
*/
66-
validationSchema?: any;
67+
validationSchema?: ValidationSchema;
6768

6869
/**
6970
* Schema validation options.

lib/interfaces/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * from './config-change-event.interface';
22
export * from './config-factory.interface';
33
export * from './config-module-options.interface';
4+
export * from './validation-schema.interface';
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/**
2+
* @publicApi
3+
*/
4+
export interface ValidationSchema {
5+
/**
6+
* Validates the given configuration object
7+
* @param config The configuration object to validate
8+
* @param options Optional validation options
9+
* @returns An object containing error (if any) and validated value
10+
*/
11+
validate?(
12+
config: Record<string, any>,
13+
options?: Record<string, any>,
14+
): { error?: Error; value: Record<string, any> };
15+
16+
/**
17+
* Parses and validates the given configuration object
18+
* @param config The configuration object to parse and validate
19+
* @param options Optional validation options
20+
* @returns The validated configuration object
21+
* @throws Error if validation fails
22+
*/
23+
parse?(
24+
config: Record<string, any>,
25+
options?: Record<string, any>,
26+
): Record<string, any>;
27+
}
28+
29+
/**
30+
* @publicApi
31+
*/
32+
export interface ValidationOptions {
33+
/**
34+
* [Joi] Whether to allow unknown properties
35+
* @default true
36+
*/
37+
allowUnknown?: boolean;
38+
39+
/**
40+
* [Joi] Whether to abort validation on first error
41+
* @default false
42+
*/
43+
abortEarly?: boolean;
44+
45+
/**
46+
* Additional validation options specific to the validation library
47+
*/
48+
[key: string]: any;
49+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { ValidationSchema } from '../interfaces/validation-schema.interface';
2+
3+
/**
4+
* @publicApi
5+
*/
6+
export abstract class Validator implements ValidationSchema {
7+
validate(
8+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
9+
_config: Record<string, any>,
10+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
11+
_options?: Record<string, any>,
12+
): { error?: Error; value: Record<string, any> } {
13+
throw new Error('Please implement the validate method');
14+
}
15+
16+
succeed(result: unknown): {
17+
error?: Error;
18+
value?: Record<string, any>;
19+
} {
20+
return {
21+
value: result as Record<string, any>,
22+
error: undefined,
23+
};
24+
}
25+
26+
failed(
27+
error: Error,
28+
config: Record<string, any>,
29+
): {
30+
error: Error;
31+
value: Record<string, any>;
32+
} {
33+
return {
34+
error: error instanceof Error ? error : new Error(String(error)),
35+
value: config,
36+
};
37+
}
38+
}

lib/validators/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export * from './abstract.validator';
2+
export * from './joi.validator';
3+
export * from './zod.validator';
4+
export * from './validator.factory';

lib/validators/joi.validator.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import type { Schema as JoiSchema, ValidationResult } from 'joi';
2+
import { ValidationOptions } from '../interfaces/validation-schema.interface';
3+
import { Validator } from './abstract.validator';
4+
5+
/**
6+
* Joi validation schema adapter with validate method
7+
*
8+
* If you need parse method instead, implement the parse method:
9+
*
10+
* parse(config: Record<string, any>, options?: ValidationOptions): Record<string, any> {
11+
* const result = this.schema.validate(config, { ...this.options, ...options });
12+
* if (result.error) throw result.error;
13+
* return result.value;
14+
* }
15+
*
16+
* @publicApi
17+
*/
18+
export class JoiValidator extends Validator {
19+
constructor(private schema: JoiSchema) {
20+
super();
21+
}
22+
23+
validate(
24+
config: Record<string, any>,
25+
options?: ValidationOptions,
26+
): { error?: Error; value: Record<string, any> } {
27+
const validationOptions = {
28+
abortEarly: false,
29+
allowUnknown: true,
30+
...options,
31+
};
32+
33+
try {
34+
const result = this.schema.validate(config, validationOptions);
35+
return this.succeed(result);
36+
} catch (error) {
37+
return this.failed(error, config);
38+
}
39+
}
40+
41+
succeed(result: ValidationResult): {
42+
error?: Error;
43+
value: Record<string, any>;
44+
} {
45+
return {
46+
error: result.error || undefined,
47+
value: result.value,
48+
};
49+
}
50+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { type Schema as JoiSchema } from 'joi';
2+
import { type ZodType } from 'zod';
3+
import { type ValidationSchema } from '../interfaces/validation-schema.interface';
4+
import { Validator } from './abstract.validator';
5+
import { JoiValidator } from './joi.validator';
6+
import { ZodValidator } from './zod.validator';
7+
8+
/**
9+
* Factory for creating validation schemas
10+
* @publicApi
11+
*/
12+
export class ValidatorFactory {
13+
/**
14+
* Creates a Joi validator
15+
* @param schema Joi schema
16+
* @returns JoiValidator instance
17+
*/
18+
static createJoiValidator(schema: JoiSchema): Validator {
19+
return new JoiValidator(schema);
20+
}
21+
22+
/**
23+
* Creates a Zod validator
24+
* @param schema Zod schema
25+
* @returns ZodValidator instance
26+
*/
27+
static createZodValidator(schema: ZodType): Validator {
28+
return new ZodValidator(schema);
29+
}
30+
31+
/**
32+
* Creates a validator from a schema object
33+
* Automatically detects the schema type based on the schema object
34+
* @param schema Schema object (Joi or Zod)
35+
* @returns ValidationSchema instance
36+
*/
37+
static createValidator(schema: ValidationSchema): Validator {
38+
// Check if it's a validator instance
39+
if (schema instanceof Validator) {
40+
return schema;
41+
}
42+
43+
// Check if it's a Joi schema first
44+
if (
45+
schema &&
46+
typeof schema === 'object' &&
47+
'validate' in schema &&
48+
typeof schema.validate === 'function'
49+
) {
50+
return this.createJoiValidator(schema as JoiSchema);
51+
}
52+
53+
// Check if it's a Zod schema
54+
if (
55+
schema &&
56+
typeof schema === 'object' &&
57+
'parse' in schema &&
58+
typeof schema.parse === 'function'
59+
) {
60+
return this.createZodValidator(schema as ZodType);
61+
}
62+
63+
throw new Error(
64+
'Unsupported schema type. Please use Joi or Zod schema or implement the validator directly.',
65+
);
66+
}
67+
}

0 commit comments

Comments
 (0)