Skip to content
Open
Show file tree
Hide file tree
Changes from 9 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