Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/conditional.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export class ConditionalModule {
options?: { timeout?: number; debug?: boolean },
) {
const { timeout = 5000, debug = true } = options ?? {};
// eslint-disable-next-line @typescript-eslint/no-base-to-string
const moduleName = getInstanceName(module) || module.toString();

const timer = setTimeout(() => {
Expand Down
21 changes: 18 additions & 3 deletions lib/config.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@ import {
VALIDATED_ENV_PROPNAME,
} from './config.constants';
import { ConfigService } from './config.service';
import { ConfigFactory, ConfigModuleOptions } from './interfaces';
import type {
ConfigFactory,
ConfigModuleOptions,
ValidationSchema,
} from './interfaces';
import { ValidatorFactory, Validator } from './validators';
import { ConfigFactoryKeyHost } from './utils';
import { createConfigProvider } from './utils/create-config-factory.util';
import { getRegistrationToken } from './utils/get-registration-token.util';
Expand Down Expand Up @@ -78,8 +83,13 @@ export class ConfigModule {
this.assignVariablesToProcess(validatedConfig);
} else if (options.validationSchema) {
const validationOptions = this.getSchemaValidationOptions(options);
const { error, value: validatedConfig } =
options.validationSchema.validate(config, validationOptions);

// Create validator from schema
const validator = this.createValidator(options.validationSchema);
const { error, value: validatedConfig } = validator.validate(
config,
validationOptions,
);

if (error) {
throw new Error(`Config validation error: ${error.message}`);
Expand Down Expand Up @@ -253,4 +263,9 @@ export class ConfigModule {
allowUnknown: true,
};
}

private static createValidator(schema: ValidationSchema): Validator {
// Use the factory to create the appropriate validator
return ValidatorFactory.createValidator(schema);
}
}
1 change: 1 addition & 0 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from './config.service';
export * from './types';
export * from './utils';
export * from './interfaces';
export * from './validators';
5 changes: 3 additions & 2 deletions lib/interfaces/config-module-options.interface.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { DotenvExpandOptions } from 'dotenv-expand';
import { ConfigFactory } from './config-factory.interface';
import { ValidationSchema } from './validation-schema.interface';

/**
* @publicApi
Expand Down Expand Up @@ -61,9 +62,9 @@ export interface ConfigModuleOptions<
skipProcessEnv?: boolean;

/**
* Environment variables validation schema (Joi).
* Environment variables validation schema (Joi, Standard Schema).
*/
validationSchema?: any;
validationSchema?: ValidationSchema;

/**
* Schema validation options.
Expand Down
1 change: 1 addition & 0 deletions lib/interfaces/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './config-change-event.interface';
export * from './config-factory.interface';
export * from './config-module-options.interface';
export * from './validation-schema.interface';
39 changes: 39 additions & 0 deletions lib/interfaces/validation-schema.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { type StandardSchemaV1 } from '@standard-schema/spec';
import { type Schema as JoiSchema } from 'joi';

/**
* @publicApi
*/
export { StandardSchemaV1 };

/**
* @publicApi
*/
export { JoiSchema };

/**
* @publicApi
*/
export type ValidationSchema = JoiSchema | StandardSchemaV1;

/**
* @publicApi
*/
export interface ValidationOptions {
/**
* [Joi] Whether to allow unknown properties
* @default true
*/
allowUnknown?: boolean;

/**
* [Joi] Whether to abort validation on first error
* @default false
*/
abortEarly?: boolean;

/**
* Additional validation options specific to the validation library
*/
[key: string]: any;
}
36 changes: 36 additions & 0 deletions lib/validators/abstract.validator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* @publicApi
*/
export abstract class Validator {
validate(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_config: Record<string, any>,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_options?: Record<string, any>,
): { error?: Error; value: Record<string, any> } {
throw new Error('Please implement the validate method');
}

succeed(result: unknown): {
error?: Error;
value: Record<string, any>;
} {
return {
value: result as Record<string, any>,
error: undefined,
};
}

failed(
error: Error,
config: Record<string, any>,
): {
error: Error;
value: Record<string, any>;
} {
return {
error,
value: config,
};
}
}
4 changes: 4 additions & 0 deletions lib/validators/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './abstract.validator';
export * from './joi.validator';
export * from './standard.validator';
export * from './validator.factory';
28 changes: 28 additions & 0 deletions lib/validators/joi.validator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type {
ValidationOptions,
JoiSchema,
} from '../interfaces/validation-schema.interface';
import { Validator } from './abstract.validator';

/**
* Joi validation schema adapter
* @see https://joi.dev/api
* @publicApi
*/
export class JoiValidator extends Validator {
constructor(private schema: JoiSchema) {
super();
}

validate(
config: Record<string, any>,
validationOptions?: ValidationOptions,
): { error?: Error; value: Record<string, any> } {
const { error, value } = this.schema.validate(config, validationOptions);
if (error) {
return this.failed(error, config);
}

return this.succeed(value);
}
}
31 changes: 31 additions & 0 deletions lib/validators/standard.validator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { StandardSchemaV1 } from '@standard-schema/spec';
import { Validator } from './abstract.validator';

/**
* Standard Schema adapter
* @see https://standardschema.dev/
* @publicApi
*/
export class StandardValidator extends Validator {
constructor(private schema: StandardSchemaV1<any, any>) {
super();
}

validate(config: StandardSchemaV1): {
error?: Error;
value: Record<string, any>;
} {
const result = this.schema['~standard'].validate(config);
if (result instanceof Promise) {
throw new Error('Expected sync result');
}
if ('value' in result) {
return this.succeed(result.value);
}

return this.failed(
new Error(JSON.stringify(result.issues, null, 2)),
config,
);
}
}
63 changes: 63 additions & 0 deletions lib/validators/validator.factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import type {
StandardSchemaV1,
ValidationSchema,
JoiSchema,
} from '../interfaces/validation-schema.interface';
import { Validator } from './abstract.validator';
import { JoiValidator } from './joi.validator';
import { StandardValidator } from './standard.validator';

/**
* Factory for creating validation schemas
* @publicApi
*/
export class ValidatorFactory {
/**
* Creates a Joi validator
* @param schema Joi schema
* @returns JoiValidator instance
*/
static createJoiValidator(schema: JoiSchema): Validator {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not put these factory methods in validators themselves?

return new JoiValidator(schema);
}

/**
* Creates a standard schema validator
* @param schema Standard schema
* @returns StandardValidator instance
*/
static createStandardValidator(schema: StandardSchemaV1): Validator {
return new StandardValidator(schema);
}

/**
* Creates a validator from a schema object
* Automatically detects the schema type based on the schema object
* @param schema Schema object (Joi or a schema that conforms to @standard-schema/spec)
* @returns Validator instance
*/
static createValidator(schema: ValidationSchema): Validator {
if (schema instanceof Validator) {
return schema;
}

// Detect Joi schema by checking for the Joi-specific symbol
// Joi schemas have a special symbol that identifies them as Joi schemas
// Reference: https://github.com/hapijs/joi/blob/1b923c1336fb3957733b920a8290c2e2ac68dc88/lib/common.js#L124
if (schema && !!(schema as any)[Symbol.for('@hapi/joi/schema')]) {
return this.createJoiValidator(schema as JoiSchema);
}

// Detect Standard Schema by checking for the '~standard' property
// Standard schemas conform to the @standard-schema/spec specification
// and contain a '~standard' property with validation logic
if (schema && typeof schema === 'object' && '~standard' in schema) {
return this.createStandardValidator(schema as StandardSchemaV1);
}

// If no valid schema type is detected, throw an error
throw new Error(
'Unsupported schema type. Please use Joi schema, Standard Schema, or implement a custom validator.',
);
}
}
Loading