Skip to content

Instantly share code, notes, and snippets.

@vabatta
Last active May 15, 2025 19:13
Show Gist options
  • Save vabatta/9f70de3d945c6fcd35d7826659729249 to your computer and use it in GitHub Desktop.
Save vabatta/9f70de3d945c6fcd35d7826659729249 to your computer and use it in GitHub Desktop.
NestJS Config service with zod schema and file support
import { ConfigModule, ConfigService } from '@nestjs/config';
import { Test } from '@nestjs/testing';
import { z } from 'zod';
import { registerConfig } from './config';
describe('config', () => {
it('should register config with ConfigModule.forFeature', async () => {
const config = registerConfig('app', z.object({}), {});
const moduleRef = await Test.createTestingModule({
imports: [ConfigModule.forFeature(config)],
}).compile();
const configService = moduleRef.get(ConfigService);
expect(configService).toBeDefined();
});
it('should return the correct config', async () => {
const config = registerConfig(
'app',
z.object({
port: z.coerce
.number()
.positive()
.min(0)
.max(65535)
.default(9556)
.describe('The local HTTP port to bind the server to'),
}),
{},
);
const moduleRef = await Test.createTestingModule({
imports: [ConfigModule.forFeature(config)],
}).compile();
const configService = moduleRef.get(ConfigService);
expect(configService.get('app')).toEqual({
port: 9556,
});
});
it('should throw an error if the config is invalid', async () => {
const config = registerConfig(
'app',
z.object({
missing: z.string().describe('This is a required field'),
}),
{
APP_NOT_MISSING: 'not missing',
},
);
const moduleRef = Test.createTestingModule({
imports: [
ConfigModule.forRoot({ ignoreEnvFile: true, load: [] }),
ConfigModule.forFeature(config),
],
}).compile();
await expect(moduleRef).rejects.toThrow();
});
});
import process from 'node:process';
import { ConfigObject, ConfigType as NestConfigType, registerAs } from '@nestjs/config';
import { merge } from 'lodash';
import { CamelCase, JsonValue } from 'type-fest';
import { z, ZodType, ZodTypeDef } from 'zod';
import { decodeConfig, decodeVariables, zodErrorToTypeError } from './internal';
/**
* Simple type alias for config namespace.
*/
type ConfigNamespace<T extends string> = CamelCase<T>;
/**
* The flattened type of the config.
*
* @example
* const config = registerConfig('app', z.object({}));
* type AppConfig = ConfigType<typeof config>;
* // ^? { port: number }
*/
export type ConfigType<T extends (...args: unknown[]) => unknown> = NestConfigType<T>;
/**
* The type of the config for a given namespace.
*
* @example
* const config = registerConfig('app', z.object({}));
* type AppConfig = NamespacedConfigType<typeof config>;
* // ^? { app: { port: number } }
*/
export type NamespacedConfigType<
// needed to prevent circular dependency during infer of the config shape
// eslint-disable-next-line @typescript-eslint/no-explicit-any
R extends ReturnType<typeof registerConfig<string, any, any>>,
> = {
[K in R['NAMESPACE']]: NestConfigType<R>;
};
/**
* Registers a config with the `ConfigModule.forFeature` for partial configuration under the provided namespace.
*
* This function handles configuration by validating and merging values from multiple sources:
* - **Environment Variables:** Reads environment variables, using the namespace prefix and validating them against the provided schema.
* - **Configuration File:** Optionally reads configuration from a file or inline YAML content, specified by the `CONFIG_FILE` or `CONFIG_CONTENT` environment variables respectively.
*
* **Merge order is Config File < Enviornment Variables** thus enviornment variables _override_ whatever the config sets.
*
* **NOTE:** Be mindful that your schema will be validated against data coming both from environment variables and/or config file, so design your schema accoringly to handle types correctly.
*
* @param namespace - The namespace of the config
* @param configSchema - The schema of the config
* @param variables - The environment variables - defaults to `process.env`
*
* @throws {TypeError} If the environment variables or configuration file content do not match the schema.
*
* @example
* const ConfigSchema = z.object({
* port: z.number().int().min(0).max(65535).default(9558).describe('The local HTTP port to bind the server to'),
* });
*
* export const appConfig = registerConfig('app', ConfigSchema);
*
* export type AppConfigNamespaced = NamespacedConfigType<typeof appConfig>;
* export type AppConfig = ConfigType<typeof appConfig>;
*/
export function registerConfig<N extends string, C extends ConfigObject, I extends JsonValue>(
namespace: ConfigNamespace<N>,
configSchema: ZodType<C, ZodTypeDef, I>,
variables: Record<string, string | undefined> = process.env,
) {
// needed to carry the namespace to the type system
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const service = registerAs(namespace, async () => {
const [decodedEnv, envKeys] = decodeVariables(variables, namespace);
const decodedConfig = decodeConfig(variables.CONFIG_CONTENT, variables.CONFIG_FILE);
const namespacedSchema = z.object({
[namespace]: configSchema,
});
const parsedConfig = await namespacedSchema.safeParseAsync(
merge({ [namespace]: {} }, decodedConfig, decodedEnv),
);
if (!parsedConfig.success)
throw zodErrorToTypeError(parsedConfig.error, namespacedSchema, namespace, envKeys);
const config: C = parsedConfig.data[namespace];
return config;
}) as ReturnType<typeof registerAs<C>> & { NAMESPACE: N };
// we add the namespace to the object itself for runtime access too
return Object.defineProperty(service, 'NAMESPACE', { value: namespace, writable: false });
}
export default registerConfig;
import fs from 'node:fs';
import {
camelCase,
get,
head,
isEmpty,
isObjectLike,
isString,
isUndefined,
pickBy,
reduce,
set,
snakeCase,
tail,
} from 'lodash';
import { JsonValue } from 'type-fest';
import yaml from 'yaml';
import { z, ZodEffects, ZodObject, ZodTransformer, ZodTypeAny } from 'zod';
/**
* Raw content of the config file as UTF-8.
*/
let cachedConfigFileContent: string | undefined;
/**
* Parsed YAML file - unknown as we don't know what shape it will have until we validate it.
*/
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
let parsedConfigFileContent: unknown | undefined;
/**
* Parses a string content into a JavaScript object using YAML parser.
* Uses a cached result if the content has been parsed before.
*
* @param content - The string content in YAML format to parse
* @returns The parsed JavaScript object representation of the YAML content
*/
function parseConfig(content: string): unknown {
if (!isUndefined(parsedConfigFileContent)) return parsedConfigFileContent;
parsedConfigFileContent = yaml.parse(content);
return parsedConfigFileContent;
}
/**
* Reads and parses a configuration file from the filesystem.
* Uses cached file content if available to prevent multiple filesystem reads.
*
* @param path - The path to the configuration file
* @returns The parsed JavaScript object representation of the file
* @throws {Error} if the file cannot be read or parsed
*/
function parseConfigFile(path: string) {
try {
if (!isString(cachedConfigFileContent))
cachedConfigFileContent = fs.readFileSync(path).toString('utf8');
return parseConfig(cachedConfigFileContent);
} catch (err) {
const message = err instanceof Error ? err.message : new String(err).toString();
throw new Error(`Unable to open the config file provided: ${message}`, { cause: err });
}
}
/**
* Reads configuration from either a string content or a file path.
* String content takes precedence over file path to avoid unnecessary filesystem operations.
*
* @param content - Optional string content in YAML format
* @param file - Optional path to a configuration file
* @returns The parsed configuration as a record of string keys to JSON values
* @throws {Error} if neither content nor file contains a valid configuration object
*/
export function decodeConfig(content?: string, file?: string) {
let readConfig: unknown = {};
// NOTE: `content` takes precedence over a `file` (to avoid a file system read in case of both)
if (isString(content)) readConfig = parseConfig(content);
else if (isString(file)) readConfig = parseConfigFile(file);
if (!isObjectLike(readConfig))
throw new Error(`Config file provided must contain the JSON configuration object`);
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return readConfig as Record<string, JsonValue>;
}
/**
* Transforms environment variable keys into nested object notation starting from the provided namespace.
* For example, transforms "APP_SERVER__HOST" into "app.server.host" using camelCase ("APP" was the env namespace).
*
* @param envKey - The environment variable key to transform
* @param envNamespace - The namespace prefix to replace with a dot notation
* @returns A string in dot notation following camelCase conventions
*/
function nestedConventionNamespaced(envKey: string, envNamespace: string): string {
return envKey
.replace(`${envNamespace}_`, `${envNamespace}.`)
.toLowerCase()
.replace(/__/g, '.')
.replace(/[a-z_]+/g, (word) => camelCase(word));
}
/**
* Attempts to parse a string value as JSON, falling back to the original string if parsing fails.
* Used for converting environment variables that might contain JSON values to mimic config file read from ENVs as well.
*
* @param value - The string value to parse as JSON
* @returns The parsed JSON value or the original string if parsing fails
*/
function jsonify(value: string | undefined): JsonValue | undefined {
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return JSON.parse(value ?? '') as JsonValue;
} catch {
return value;
}
}
/**
* Transforms environment variables from a flat structure with a specific namespace prefix
* into a nested object structure with camelCase keys, while preserving the original keys
* for error reporting.
*
* For example, transforms:
* {
* "MY_APP_SERVER__HOST": "localhost",
* "MY_APP_DATABASE__PORT": "5432",
* "UNRELATED_VAR": "value"
* }
*
* With namespace "myApp" into:
* {
* "my_app": {
* "server": {
* "host": "localhost"
* },
* "database": {
* "port": 5432 // Note: jsonify attempts to parse values
* }
* }
* }
*
* @param variables - Record of environment variables with string keys and string values
* @param namespace - The namespace prefix to filter variables (will be converted to SNAKE_CASE)
* @returns A tuple containing:
* 1. The transformed nested configuration object
* 2. A Map relating the transformed path keys to original environment variable names for error reporting
*/
export function decodeVariables(
variables: Record<string, string | undefined>,
namespace: string,
): readonly [Record<string, JsonValue | undefined>, Map<string, string>] {
const envKeys = new Map<string, string>();
const envNamespace = snakeCase(namespace).toUpperCase();
const relevantEnv = pickBy(variables, (value, key) => key.startsWith(envNamespace));
const decodedEnv = reduce(
relevantEnv,
(env, value, key) => {
const newKey = nestedConventionNamespaced(key, envNamespace);
envKeys.set(newKey, key);
const newValue = jsonify(value);
return set(env, newKey, newValue);
},
{} as Record<string, JsonValue | undefined>,
);
return [decodedEnv, envKeys];
}
/**
* Recursively navigates through a Zod schema to find description metadata at a specific path.
* Handles various Zod schema types including effects, transformers, and objects.
*
* @param path - Array of string keys representing the path in the schema
* @param schema - The Zod schema to search through
* @returns The description string if found, otherwise undefined
*/
function findDescriptionInSchemaByPath(path: string[], schema: ZodTypeAny) {
// we have to work with ZodTypeAny to accept anything, so we disable the unsafe argument check
if (isEmpty(path)) return schema?.description;
else if (schema instanceof ZodEffects)
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
return findDescriptionInSchemaByPath(path, schema.innerType());
else if (schema instanceof ZodTransformer)
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
return findDescriptionInSchemaByPath(path, schema.innerType());
else if (schema instanceof ZodObject)
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
return findDescriptionInSchemaByPath(tail(path), get(schema.shape, head(path) ?? ''));
else return schema?.description;
}
/**
* Converts Zod validation errors into a more descriptive TypeError.
* Enhances error messages with schema descriptions when available.
*
* @param error - The Zod error containing validation issues
* @param schema - The namespaced schema that was used for validation
* @param namespace - The configuration namespace for context in error messages
* @param keys - Map of path strings to user-friendly key names for better error reporting
* @returns A TypeError with formatted error messages for each validation issue
*/
export function zodErrorToTypeError(
error: z.ZodError,
schema: ZodTypeAny,
namespace: string,
keys: Map<string, string>,
isSchemaNamespaced: boolean = true,
) {
const errorMessages = error.issues.map((err) => {
const pathNotation = err.path.join('.');
const path = keys.get(pathNotation) ?? pathNotation;
const message = err.message;
const description = findDescriptionInSchemaByPath(
err.path.map((v) => v.toString()),
schema,
);
return { path, message, description };
});
return new ZodConfigError(
{
name: namespace,
description: findDescriptionInSchemaByPath(isSchemaNamespaced ? [namespace] : [], schema),
},
errorMessages,
);
}
/**
* Simple error wrapper for expressing an invalid config from a zod schema.
*/
export class ZodConfigError extends TypeError {
public constructor(
public readonly namespace: { name: string; description: string | undefined },
public readonly configErrors: readonly {
path: string;
message: string;
description: string | undefined;
}[],
options?: ErrorOptions,
) {
const spacing = ' ';
const title = `Invalid config for "${namespace.name}":\n`;
const description = isUndefined(namespace.description)
? ''
: `${spacing}${namespace.description}\n`;
const errors = configErrors.map((v) => ZodConfigError.mapError(v, spacing)).join('\n');
super(title + description + errors, options);
}
private static mapError(
error: { path: string; message: string; description: string | undefined },
spacing: string,
) {
let message = `${spacing}- ${error.path}: ${error.message}`;
message += isUndefined(error.description) ? '' : `\n${spacing}${error.description}`;
return message;
}
}
import { Module } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import { z } from 'zod';
import { ZodConfigurableModuleBuilder } from './module';
describe('module', () => {
const MyOptionSchema = z
.object({
useFlags: z.boolean().describe('Whether the use of flag is considered when connecting'),
clientName: z.string().min(5).describe('The client name used for connecting to the universe'),
})
.describe('The options for connecting using the dynamic module')
.transform((v) => ({ ...v, supportsTransforms: false }))
// eslint-disable-next-line @typescript-eslint/require-await
.transform(async (v) => ({ ...v, supportsTransforms: true }));
type MyOption = z.infer<typeof MyOptionSchema>;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN, ASYNC_OPTIONS_TYPE, OPTIONS_TYPE } =
new ZodConfigurableModuleBuilder(MyOptionSchema)
.setExtras(
{
isGlobal: false,
},
(definition, extras) => ({
...definition,
global: extras.isGlobal,
}),
)
.setClassMethodName('forRoot')
.build();
@Module({})
class DynModule extends ConfigurableModuleClass {}
it('should register correct options with static synchronous method', async () => {
const moduleRef = await Test.createTestingModule({
imports: [
DynModule.forRoot({
useFlags: true,
clientName: 'some long enough client name',
}),
],
}).compile();
const options = moduleRef.get<MyOption>(MODULE_OPTIONS_TOKEN);
expect(options).toMatchObject({
useFlags: true,
clientName: 'some long enough client name',
supportsTransforms: true,
});
});
it('should register correct options with asynchronous method', async () => {
const DEFAULTS_TOKEN = 'DEFAULT_OPTIONS';
const defaultsValue = {
useFlags: false,
};
const moduleRef = await Test.createTestingModule({
imports: [
DynModule.forRootAsync({
provideInjectionTokensFrom: [{ provide: DEFAULTS_TOKEN, useValue: defaultsValue }],
useFactory: (defaults: typeof defaultsValue) => {
return {
...defaults,
clientName: 'some long enough client name',
};
},
inject: [{ token: DEFAULTS_TOKEN, optional: false }],
}),
],
}).compile();
const options = moduleRef.get<MyOption>(MODULE_OPTIONS_TOKEN);
expect(options).toMatchObject({
useFlags: false,
clientName: 'some long enough client name',
supportsTransforms: true,
});
});
it('should throw an error when clientName is too short', async () => {
await expect(
Test.createTestingModule({
imports: [
DynModule.forRoot({
useFlags: true,
clientName: 'shor', // Less than 5 characters
}),
],
}).compile(),
).rejects.toThrow();
});
it('should throw an error when required options are missing', async () => {
await expect(
Test.createTestingModule({
imports: [
// @ts-expect-error: we are passing what could be a complete wrong and unchecked object
DynModule.forRoot({
// Missing clientName
useFlags: true,
} as unknown),
],
}).compile(),
).rejects.toThrow();
});
it('should throw an error when async factory returns invalid options', async () => {
const DEFAULTS_TOKEN = 'INVALID_DEFAULTS';
const defaultsValue = {
useFlags: false,
};
await expect(
Test.createTestingModule({
imports: [
DynModule.forRootAsync({
provideInjectionTokensFrom: [{ provide: DEFAULTS_TOKEN, useValue: defaultsValue }],
useFactory: (defaults: typeof defaultsValue) => {
return {
...defaults,
clientName: 'bad', // Too short
};
},
inject: [{ token: DEFAULTS_TOKEN, optional: false }],
}),
],
}).compile(),
).rejects.toThrow();
});
it('should throw an error when invalid option types are provided', async () => {
await expect(
Test.createTestingModule({
imports: [
DynModule.forRoot({
// @ts-expect-error: we are passing a wrong and unchecked field
useFlags: 'not-a-boolean' as unknown, // Wrong type
clientName: 'some long enough client name',
}),
],
}).compile(),
).rejects.toThrow();
});
it('should throw an error when injected token is not found and not optional', async () => {
const NON_EXISTENT_TOKEN = 'NON_EXISTENT_TOKEN';
await expect(
Test.createTestingModule({
imports: [
DynModule.forRootAsync({
useFactory: () => ({
useFlags: true,
clientName: 'some long enough client name',
}),
inject: [{ token: NON_EXISTENT_TOKEN, optional: false }],
}),
],
}).compile(),
).rejects.toThrow();
});
});
import {
ConfigurableModuleAsyncOptions,
ConfigurableModuleBuilder,
ConfigurableModuleBuilderOptions,
DynamicModule,
Provider,
} from '@nestjs/common';
import {
DEFAULT_FACTORY_CLASS_METHOD_KEY,
DEFAULT_METHOD_KEY,
} from '@nestjs/common/module-utils/constants';
import { isObject, keys, omit } from 'lodash';
import { ZodError, ZodType, ZodTypeDef } from 'zod';
import { zodErrorToTypeError } from './internal';
export class ZodConfigurableModuleBuilder<
ModuleOptions,
ModuleOptionsInput = unknown,
StaticMethodKey extends string = typeof DEFAULT_METHOD_KEY,
FactoryClassMethodKey extends string = typeof DEFAULT_FACTORY_CLASS_METHOD_KEY,
ExtraModuleDefinitionOptions = object,
> {
protected base: ConfigurableModuleBuilder<
ModuleOptionsInput,
StaticMethodKey,
FactoryClassMethodKey,
ExtraModuleDefinitionOptions
>;
private transformSet = false;
public constructor(
protected readonly schema: ZodType<ModuleOptions, ZodTypeDef, ModuleOptionsInput>,
protected readonly options: ConfigurableModuleBuilderOptions = {},
parentBuilder?: ConfigurableModuleBuilder<ModuleOptionsInput>,
) {
this.base = new ConfigurableModuleBuilder(options, parentBuilder);
}
public setExtras<ExtraModuleDefinitionOptions>(
extras: ExtraModuleDefinitionOptions,
transformDefinition: (
definition: DynamicModule,
extras: ExtraModuleDefinitionOptions,
) => DynamicModule = (def) => def,
) {
const builder = new ZodConfigurableModuleBuilder<
ModuleOptions,
ModuleOptionsInput,
StaticMethodKey,
FactoryClassMethodKey,
ExtraModuleDefinitionOptions
// eslint-disable-next-line
>(this.schema, this.options, this as any);
// this was called and we patched
this.transformSet = true;
builder.base = this.patchTransform(extras, transformDefinition);
return builder;
}
private patchTransform<ExtraModuleDefinitionOptions>(
extras: ExtraModuleDefinitionOptions,
transformDefinition: (
definition: DynamicModule,
extras: ExtraModuleDefinitionOptions,
) => DynamicModule = (def) => def,
) {
transformDefinition ??= (definition) => definition;
const existingTransform = transformDefinition;
transformDefinition = (definition, extraOptions) => {
const result = existingTransform(definition, extraOptions);
return this.transformModuleDefinitionWithSchema(result, extraOptions, extras);
};
return this.base.setExtras(extras, transformDefinition);
}
public setClassMethodName<StaticMethodKey extends string>(key: StaticMethodKey) {
const builder = new ZodConfigurableModuleBuilder<
ModuleOptions,
ModuleOptionsInput,
StaticMethodKey,
FactoryClassMethodKey,
ExtraModuleDefinitionOptions
// eslint-disable-next-line
>(this.schema, this.options, this as any);
builder.base = builder.base.setClassMethodName(key);
return builder;
}
public setFactoryMethodName<FactoryClassMethodKey extends string>(key: FactoryClassMethodKey) {
const builder = new ZodConfigurableModuleBuilder<
ModuleOptions,
ModuleOptionsInput,
StaticMethodKey,
FactoryClassMethodKey,
ExtraModuleDefinitionOptions
// eslint-disable-next-line
>(this.schema, this.options, this as any);
builder.base = builder.base.setFactoryMethodName(key);
return builder;
}
public build(): ZodConfigurableModuleHost<
ModuleOptions,
ModuleOptionsInput,
StaticMethodKey,
FactoryClassMethodKey,
ExtraModuleDefinitionOptions
> {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
if (!this.transformSet) this.base = this.patchTransform({} as ExtraModuleDefinitionOptions);
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return this.base.build() as unknown as ZodConfigurableModuleHost<
ModuleOptions,
ModuleOptionsInput,
StaticMethodKey,
FactoryClassMethodKey,
ExtraModuleDefinitionOptions
>;
}
private transformModuleDefinitionWithSchema<ExtraModuleDefinitionOptions>(
definition: DynamicModule,
extraOptions: ExtraModuleDefinitionOptions,
extras: unknown,
): DynamicModule {
const isOptionProvider = (provider: Provider) =>
'provide' in provider && provider.provide === this.options.optionsInjectionToken;
definition.providers = definition.providers?.map((provider) => {
if (isOptionProvider(provider)) {
if ('useFactory' in provider) {
const existingFactory = provider.useFactory;
provider.useFactory = async (...args) => {
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
return await this.schema.parseAsync(existingFactory(...args));
} catch (err) {
if (err instanceof ZodError) {
throw zodErrorToTypeError(
err,
this.schema,
definition.module.name,
new Map(),
false,
);
}
throw err;
}
};
} else {
return {
provide: this.options.optionsInjectionToken!,
useFactory: async () => {
const finalOptions = isObject(extraOptions)
? omit(extraOptions, keys(extras))
: extraOptions;
try {
return await this.schema.parseAsync(finalOptions);
} catch (err) {
if (err instanceof ZodError) {
throw zodErrorToTypeError(
err,
this.schema,
definition.module.name,
new Map(),
false,
);
}
throw err;
}
},
};
}
}
return provider;
});
return definition;
}
}
type ZodConfigurableModuleCls<
// eslint-disable-next-line @typescript-eslint/no-unused-vars
ModuleOptions,
ModuleOptionsInput = unknown,
MethodKey extends string = typeof DEFAULT_METHOD_KEY,
FactoryClassMethodKey extends string = typeof DEFAULT_FACTORY_CLASS_METHOD_KEY,
ExtraModuleDefinitionOptions = object,
> = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
new (): any;
} & Record<
`${MethodKey}`,
(options: ModuleOptionsInput & Partial<ExtraModuleDefinitionOptions>) => DynamicModule
> &
Record<
`${MethodKey}Async`,
(
options: ConfigurableModuleAsyncOptions<ModuleOptionsInput, FactoryClassMethodKey> &
Partial<ExtraModuleDefinitionOptions>,
) => DynamicModule
>;
interface ZodConfigurableModuleHost<
ModuleOptions = Record<string, unknown>,
ModuleOptionsInput = unknown,
MethodKey extends string = string,
FactoryClassMethodKey extends string = string,
ExtraModuleDefinitionOptions = object,
> {
/**
* Class that represents a blueprint/prototype for a configurable Nest module.
* This class provides static methods for constructing dynamic modules. Their names
* can be controlled through the "MethodKey" type argument.
*
* Your module class should inherit from this class to make the static methods available.
*
* @example
* ```typescript
* @Module({})
* class IntegrationModule extends ConfigurableModuleCls {
* // ...
* }
* ```
*/
ConfigurableModuleClass: ZodConfigurableModuleCls<
ModuleOptions,
ModuleOptionsInput,
MethodKey,
FactoryClassMethodKey,
ExtraModuleDefinitionOptions
>;
/**
* Module options provider token. Can be used to inject the "options object" to
* providers registered within the host module.
*/
MODULE_OPTIONS_TOKEN: string | symbol;
/**
* Can be used to auto-infer the compound "async module options" type.
* Note: this property is not supposed to be used as a value.
*
* @example
* ```typescript
* @Module({})
* class IntegrationModule extends ConfigurableModuleCls {
* static module = initializer(IntegrationModule);
*
* static registerAsync(options: typeof ASYNC_OPTIONS_TYPE): DynamicModule {
* return super.registerAsync(options);
* }
* ```
*/
OPTIONS_TYPE: ModuleOptions & Partial<ExtraModuleDefinitionOptions>;
/**
* Can be used to auto-infer the compound "module options" type (options interface + extra module definition options).
* Note: this property is not supposed to be used as a value.
*
* @example
* ```typescript
* @Module({})
* class IntegrationModule extends ConfigurableModuleCls {
* static module = initializer(IntegrationModule);
*
* static register(options: typeof OPTIONS_TYPE): DynamicModule {
* return super.register(options);
* }
* ```
*/
ASYNC_OPTIONS_TYPE: ConfigurableModuleAsyncOptions<ModuleOptions, FactoryClassMethodKey> &
Partial<ExtraModuleDefinitionOptions>;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment