@nestjs-mod/common
Version:
A collection of utilities for unifying NestJS applications and modules
258 lines • 11.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.envTransform = envTransform;
exports.getEnvModelMetadata = getEnvModelMetadata;
const common_1 = require("@nestjs/common");
const load_package_1 = require("../utils/load-package");
const constants_1 = require("./constants");
const errors_1 = require("./errors");
// ============= Validation Helpers =============
/**
* Loads the class-validator package, using provided package or loading it dynamically
*/
function loadValidator(validatorPackage) {
return validatorPackage || (0, load_package_1.loadPackage)('class-validator', () => require('class-validator'));
}
// ============= Main Transformation Function =============
/**
* Transforms and validates environment data based on model metadata
*/
async function envTransform({ model, data, rootOptions, }) {
const options = {
logger: new common_1.ConsoleLogger('envTransform'),
...rootOptions,
};
const validator = loadValidator(options?.validatorPackage);
const { modelPropertyOptions, modelOptions } = getEnvModelMetadata(model);
const info = {
modelPropertyOptions,
modelOptions: modelOptions || {},
validations: {},
};
const propertyNameFormatters = options?.propertyNameFormatters || modelOptions?.propertyNameFormatters || [];
// Process each property with formatters and extractors
for (const propertyOptions of modelPropertyOptions) {
processPropertyWithFormatters(propertyOptions, data, propertyNameFormatters, options, modelOptions, info);
}
// Transform property values
transformPropertyValues(data, modelPropertyOptions, options, modelOptions, info);
// Create model instances for validation
const { emptyInstance, dataInstance } = createModelInstances(model, data);
// Validate the environment configuration
await validateEnvironment(emptyInstance, dataInstance, validator, options, modelOptions, info);
// Log debug information if enabled
logDebugInfo(options, modelOptions, info);
// Copy validated data back to original data object
copyValidatedData(data, dataInstance, modelPropertyOptions);
return { data, info };
}
// ============= Property Processing =============
/**
* Processes a single property with all formatters and extractors
*/
function processPropertyWithFormatters(propertyOptions, data, propertyNameFormatters, rootOptions, modelOptions, info) {
const demoMode = rootOptions?.demoMode || modelOptions?.demoMode || false;
for (const propertyNameFormatter of propertyNameFormatters) {
const formattedPropertyExample = propertyNameFormatter.example({
modelRootOptions: rootOptions,
modelOptions: modelOptions || {},
propertyOptions,
});
const formattedPropertyName = propertyNameFormatter.format({
modelRootOptions: rootOptions,
modelOptions: modelOptions || {},
propertyOptions,
});
// Extract property values from various sources
const propertyValueExtractors = extractPropertyValues(data, propertyOptions, formattedPropertyName, demoMode, rootOptions, modelOptions);
// Initialize validation info for this property
initializeValidationInfo(propertyOptions, info);
// Add formatter and extractor info to validation
info.validations[propertyOptions.originalName].propertyNameFormatters.push({
name: propertyNameFormatter.name,
value: formattedPropertyName,
example: formattedPropertyExample,
});
info.validations[propertyOptions.originalName].propertyValueExtractors = [
...info.validations[propertyOptions.originalName].propertyValueExtractors,
...propertyValueExtractors,
];
}
}
/**
* Extracts property values from all configured extractors
*/
function extractPropertyValues(data, propertyOptions, formattedPropertyName, demoMode, rootOptions, modelOptions) {
const propertyValueExtractors = (rootOptions?.propertyValueExtractors ??
modelOptions?.propertyValueExtractors ??
[]).map((extractor) => {
// Set demo mode if extractor supports it
if (extractor.setDemoMode) {
extractor.setDemoMode(demoMode);
}
const result = {
name: extractor.name,
example: extractor.example({
obj: data,
modelRootOptions: rootOptions,
modelOptions: modelOptions || {},
propertyOptions,
formattedPropertyName,
}),
demoMode,
value: extractor.extract({
obj: data,
modelRootOptions: rootOptions,
modelOptions: modelOptions || {},
propertyOptions,
formattedPropertyName,
}),
};
// Reset demo mode
if (extractor.setDemoMode) {
extractor.setDemoMode(false);
}
return result;
});
return propertyValueExtractors;
}
/**
* Initializes validation info for a property if not already present
*/
function initializeValidationInfo(propertyOptions, info) {
if (info.validations[propertyOptions.originalName] === undefined) {
info.validations[propertyOptions.originalName] = {
constraints: {},
value: undefined,
propertyNameFormatters: [],
propertyValueExtractors: [],
};
}
}
// ============= Value Transformation =============
/**
* Transforms property values using transformers or extracted values
*/
function transformPropertyValues(data, modelPropertyOptions, rootOptions, modelOptions, info) {
for (const propertyOptions of modelPropertyOptions) {
const extractedValue = info.validations[propertyOptions.originalName].propertyValueExtractors.find((e) => e.value !== undefined)?.value;
const currentValue = data[propertyOptions.originalName];
const value = extractedValue !== undefined ? extractedValue : currentValue;
if (propertyOptions.transform?.transform) {
data[propertyOptions.originalName] =
value !== undefined
? propertyOptions.transform.transform({
modelRootOptions: rootOptions,
modelOptions: modelOptions || {},
obj: data,
options: propertyOptions,
value,
})
: propertyOptions.default;
}
else {
data[propertyOptions.originalName] = value !== undefined ? value : propertyOptions.default;
}
info.validations[propertyOptions.originalName].value = data[propertyOptions.originalName];
}
}
// ============= Validation Logic =============
/**
* Creates empty and data-filled model instances
*/
function createModelInstances(model, data) {
try {
const emptyInstance = new model();
const dataInstance = Object.assign(new model(), data);
return { emptyInstance, dataInstance };
}
catch {
return { emptyInstance: {}, dataInstance: {} };
}
}
/**
* Validates the environment configuration
*/
async function validateEnvironment(emptyInstance, dataInstance, validator, rootOptions, modelOptions, info) {
const shouldSkipValidation = rootOptions?.skipValidation || modelOptions?.skipValidation;
if (shouldSkipValidation) {
return;
}
const validatorOptions = rootOptions?.validatorOptions || modelOptions?.validatorOptions || constants_1.ENV_MODEL_CLASS_VALIDATOR_OPTIONS;
// Validate data instance for errors
const validationErrors = (await validator.validate(dataInstance, validatorOptions)).filter((error) => error.property);
// Validate empty instance to collect constraints for documentation
const emptyValidationErrors = (await validator.validate(emptyInstance, validatorOptions)).filter((error) => error.property);
// Collect constraints from empty instance validation
for (const error of emptyValidationErrors) {
if (info.validations[error.property]) {
info.validations[error.property].constraints = error?.constraints || {};
}
}
// Throw error if validation failed
if (validationErrors.length > 0) {
throwError(validationErrors, info, rootOptions, modelOptions);
}
}
/**
* Throws validation error with proper logging
*/
function throwError(errors, info, rootOptions, modelOptions) {
const error = new errors_1.EnvModelValidationErrors(errors, info);
const logger = rootOptions?.logger || modelOptions?.logger;
if (logger?.error) {
const debug = rootOptions?.debug || modelOptions?.debug || process.env['DEBUG'];
const visibleValidations = buildVisibleValidations(info);
logger.error(!debug
? error.message
: {
...info,
validations: visibleValidations,
modelPropertyOptions: info.modelPropertyOptions.filter((o) => !o.hideValueFromOutputs && visibleValidations[o.originalName]),
});
}
throw error;
}
/**
* Builds visible validations (excluding hidden values)
*/
function buildVisibleValidations(info) {
return Object.fromEntries(Object.entries(info.validations).filter(([key, value]) => {
return (Object.keys(value.constraints || {}).length > 0 &&
!info.modelPropertyOptions.some((o) => o.hideValueFromOutputs && o.originalName === key));
}));
}
/**
* Logs debug information if debug mode is enabled
*/
function logDebugInfo(rootOptions, modelOptions, info) {
const debug = rootOptions?.debug || modelOptions?.debug || process.env['DEBUG'];
const logger = rootOptions?.logger || modelOptions?.logger;
if (debug && logger?.debug) {
logger.debug(JSON.stringify({
...info,
validations: Object.fromEntries(Object.entries(info.validations).filter(([key]) => {
return !info.modelPropertyOptions.some((o) => o.hideValueFromOutputs && o.originalName === key);
})),
modelPropertyOptions: info.modelPropertyOptions.filter((o) => !o.hideValueFromOutputs),
}));
}
}
/**
* Copies validated data from instance back to original data object
*/
function copyValidatedData(data, dataInstance, modelPropertyOptions) {
for (const propertyMetadata of modelPropertyOptions) {
data[propertyMetadata.originalName] = dataInstance[propertyMetadata.originalName];
}
}
// ============= Metadata Extraction =============
/**
* Extracts environment model metadata from the model class
*/
function getEnvModelMetadata(model) {
const modelPropertyOptions = Reflect.getMetadata(constants_1.ENV_MODEL_PROPERTIES_METADATA, model) || [];
const modelOptions = Reflect.getMetadata(constants_1.ENV_MODEL_METADATA, model);
return { modelPropertyOptions, modelOptions };
}
//# sourceMappingURL=utils.js.map