The validation system provides a framework for adding custom validation logic to constructs. Validators can check construct configuration, relationships, and state to ensure correctness before synthesis or deployment.
Interface that must be implemented by validation objects to participate in the construct validation system.
/**
* Implement this interface in order for the construct to be able to validate itself.
*/
interface IValidation {
/**
* Validate the current construct.
*
* This method can be implemented by derived constructs in order to perform
* validation logic. It is called on all constructs before synthesis.
*
* @returns An array of validation error messages, or an empty array if there the construct is valid.
*/
validate(): string[];
}Methods provided by the Node class for adding and executing validations.
/**
* Adds a validation to this construct.
*
* When `node.validate()` is called, the `validate()` method will be called on
* all validations and all errors will be returned.
*
* @param validation The validation object
*/
addValidation(validation: IValidation): void;
/**
* Validates this construct.
*
* Invokes the `validate()` method on all validations added through
* `addValidation()`.
*
* @returns an array of validation error messages associated with this
* construct.
*/
validate(): string[];Usage Examples:
import { Construct, RootConstruct, IValidation } from "constructs";
const app = new RootConstruct("App");
const database = new Construct(app, "Database");
// Simple validation class
class DatabaseConfigValidation implements IValidation {
constructor(private construct: Construct) {}
validate(): string[] {
const errors: string[] = [];
// Check required context
const dbType = this.construct.node.tryGetContext("databaseType");
if (!dbType) {
errors.push("Database type must be specified in context");
}
const backupEnabled = this.construct.node.tryGetContext("backupEnabled");
if (backupEnabled === undefined) {
errors.push("Backup configuration must be specified");
}
return errors;
}
}
// Add validation to construct
database.node.addValidation(new DatabaseConfigValidation(database));
// Run validation (will return errors)
let errors = database.node.validate();
console.log(errors);
// ["Database type must be specified in context", "Backup configuration must be specified"]
// Fix configuration
database.node.setContext("databaseType", "postgresql");
database.node.setContext("backupEnabled", true);
// Run validation again (should pass)
errors = database.node.validate();
console.log(errors); // []Advanced validation patterns for checking construct relationships, configuration consistency, and business rules.
Usage Examples:
import { Construct, RootConstruct, IValidation } from "constructs";
const app = new RootConstruct("App");
const vpc = new Construct(app, "VPC");
const database = new Construct(vpc, "Database");
const api = new Construct(vpc, "API");
// Validation that checks construct relationships
class DependencyValidation implements IValidation {
constructor(private construct: Construct) {}
validate(): string[] {
const errors: string[] = [];
const dependencies = this.construct.node.dependencies;
// API must depend on database
if (this.construct.node.id === "API") {
const dependsOnDatabase = dependencies.some(dep =>
dep.node.id === "Database"
);
if (!dependsOnDatabase) {
errors.push("API must declare dependency on Database");
}
}
return errors;
}
}
// Validation for environment-specific rules
class EnvironmentValidation implements IValidation {
constructor(private construct: Construct) {}
validate(): string[] {
const errors: string[] = [];
const environment = this.construct.node.tryGetContext("environment");
if (environment === "production") {
// Production-specific validations
const monitoring = this.construct.node.tryGetContext("monitoring");
if (!monitoring?.enabled) {
errors.push("Monitoring must be enabled in production");
}
const backup = this.construct.node.tryGetContext("backup");
if (!backup?.enabled) {
errors.push("Backup must be enabled in production");
}
// Check for security metadata
const hasSecurityMeta = this.construct.node.metadata.some(m =>
m.type.startsWith("security:")
);
if (!hasSecurityMeta) {
errors.push("Security metadata required for production constructs");
}
}
return errors;
}
}
// Configuration consistency validation
class ConfigValidation implements IValidation {
constructor(private construct: Construct) {}
validate(): string[] {
const errors: string[] = [];
const dbType = this.construct.node.tryGetContext("databaseType");
const dbVersion = this.construct.node.tryGetContext("databaseVersion");
// Version compatibility checks
if (dbType === "postgresql" && dbVersion) {
const version = parseFloat(dbVersion);
if (version < 12) {
errors.push("PostgreSQL version must be 12 or higher");
}
}
if (dbType === "mysql" && dbVersion) {
const version = parseFloat(dbVersion);
if (version < 8) {
errors.push("MySQL version must be 8.0 or higher");
}
}
return errors;
}
}
// Add validations to constructs
api.node.addValidation(new DependencyValidation(api));
database.node.addValidation(new EnvironmentValidation(database));
database.node.addValidation(new ConfigValidation(database));
// Set up test scenario
app.node.setContext("environment", "production");
database.node.setContext("databaseType", "postgresql");
database.node.setContext("databaseVersion", "11"); // Too old
database.node.setContext("monitoring", { enabled: false }); // Should be true for prod
// Run validations
const apiErrors = api.node.validate();
console.log("API errors:", apiErrors);
// ["API must declare dependency on Database"]
const dbErrors = database.node.validate();
console.log("Database errors:", dbErrors);
// [
// "Monitoring must be enabled in production",
// "Backup must be enabled in production",
// "Security metadata required for production constructs",
// "PostgreSQL version must be 12 or higher"
// ]
// Fix issues
api.node.addDependency(database);
database.node.setContext("databaseVersion", "13");
database.node.setContext("monitoring", { enabled: true });
database.node.setContext("backup", { enabled: true });
database.node.addMetadata("security:classification", "internal");
// Validate again
console.log("API errors after fix:", api.node.validate()); // []
console.log("Database errors after fix:", database.node.validate()); // []Adding multiple validators to a single construct for different validation concerns.
Usage Examples:
import { Construct, RootConstruct, IValidation } from "constructs";
const app = new RootConstruct("App");
const service = new Construct(app, "UserService");
// Separate validations for different concerns
class NamingValidation implements IValidation {
constructor(private construct: Construct) {}
validate(): string[] {
const errors: string[] = [];
const id = this.construct.node.id;
// Naming conventions
if (!id.match(/^[A-Z][a-zA-Z0-9]*$/)) {
errors.push(`Construct ID '${id}' must be PascalCase`);
}
if (id.length > 50) {
errors.push(`Construct ID '${id}' must be 50 characters or less`);
}
return errors;
}
}
class ResourceValidation implements IValidation {
constructor(private construct: Construct) {}
validate(): string[] {
const errors: string[] = [];
// Resource limits
const children = this.construct.node.children;
if (children.length > 20) {
errors.push(`Construct has ${children.length} children, maximum is 20`);
}
// Check for required child constructs
const hasDatabase = children.some(child =>
child.node.id.toLowerCase().includes("database")
);
const hasApi = children.some(child =>
child.node.id.toLowerCase().includes("api")
);
if (!hasDatabase && !hasApi) {
errors.push("Service must contain either Database or API construct");
}
return errors;
}
}
class SecurityValidation implements IValidation {
constructor(private construct: Construct) {}
validate(): string[] {
const errors: string[] = [];
// Security requirements
const metadata = this.construct.node.metadata;
const hasSecurityReview = metadata.some(m =>
m.type === "security:review" && m.data.approved === true
);
if (!hasSecurityReview) {
errors.push("Construct requires security review approval");
}
// Check for sensitive data handling
const handlesUserData = this.construct.node.tryGetContext("handlesUserData");
if (handlesUserData) {
const hasEncryption = this.construct.node.tryGetContext("encryption");
if (!hasEncryption?.enabled) {
errors.push("Constructs handling user data must have encryption enabled");
}
}
return errors;
}
}
// Add all validations
service.node.addValidation(new NamingValidation(service));
service.node.addValidation(new ResourceValidation(service));
service.node.addValidation(new SecurityValidation(service));
// Add child constructs
const database = new Construct(service, "Database");
const api = new Construct(service, "API");
// Configure for testing
service.node.setContext("handlesUserData", true);
service.node.addMetadata("security:review", {
approved: true,
reviewer: "security-team",
date: "2023-10-01"
});
// Run all validations
const allErrors = service.node.validate();
console.log("All validation errors:", allErrors);
// ["Constructs handling user data must have encryption enabled"]
// Fix remaining issue
service.node.setContext("encryption", { enabled: true, algorithm: "AES-256" });
// Validate again - should pass all validations
console.log("Final validation:", service.node.validate()); // []Patterns for validating entire construct trees and inheriting validation rules.
Usage Examples:
import { Construct, RootConstruct, IValidation } from "constructs";
const app = new RootConstruct("App");
// Recursive validation helper
function validateTree(construct: Construct): string[] {
const allErrors: string[] = [];
// Validate current construct
const errors = construct.node.validate();
allErrors.push(...errors.map(error =>
`${construct.node.path || '<root>'}: ${error}`
));
// Recursively validate children
for (const child of construct.node.children) {
if (Construct.isConstruct(child)) {
allErrors.push(...validateTree(child as Construct));
}
}
return allErrors;
}
// Base validation that can be inherited
class BaseValidation implements IValidation {
constructor(protected construct: Construct) {}
validate(): string[] {
const errors: string[] = [];
// Common validation rules
const metadata = this.construct.node.metadata;
const hasOwner = metadata.some(m => m.type === "owner");
if (!hasOwner) {
errors.push("Construct must have owner metadata");
}
return errors;
}
}
// Environment-specific validation extending base
class ProductionValidation extends BaseValidation {
validate(): string[] {
const errors = super.validate(); // Get base validation errors
const environment = this.construct.node.tryGetContext("environment");
if (environment === "production") {
// Additional production checks
const hasMonitoring = this.construct.node.metadata.some(m =>
m.type === "monitoring"
);
if (!hasMonitoring) {
errors.push("Production constructs must have monitoring metadata");
}
}
return errors;
}
}
// Set up test tree
const production = new Construct(app, "Production");
const service1 = new Construct(production, "Service1");
const service2 = new Construct(production, "Service2");
production.node.setContext("environment", "production");
// Add inherited validations
service1.node.addValidation(new ProductionValidation(service1));
service2.node.addValidation(new ProductionValidation(service2));
// Add partial metadata
service1.node.addMetadata("owner", "team-alpha");
service2.node.addMetadata("owner", "team-beta");
service2.node.addMetadata("monitoring", { enabled: true });
// Validate entire tree
const treeErrors = validateTree(app);
console.log("Tree validation errors:");
treeErrors.forEach(error => console.log(` ${error}`));
// Production/Service1: Production constructs must have monitoring metadata
// Fix remaining issues
service1.node.addMetadata("monitoring", { enabled: true });
// Validate again
const finalErrors = validateTree(app);
console.log("Final tree validation:", finalErrors); // []