Docs/configurationv1.0.0-rc.1

Schema Decorators

Schema decorators are the entry point for the UDIOT configuration system. You annotate a plain TypeScript class with @AIConfig and its properties with @AIEnv; the framework reads those annotations at runtime to build a ConfigSchema that drives loading, validation, and provenance tracking.

reflect-metadata must be imported once in your entry point (or tsconfig emitDecoratorMetadata must be enabled) for type inference to work correctly.

Key Concepts

  • ConfigSchema — the complete description of one configuration namespace, built by buildConfigSchema(MyClass).
  • ConfigFieldSchema — per-property metadata stored for each @AIEnv-decorated field.
  • ConfigPolicy — numeric or regex constraints evaluated during the load phase.
  • Namespace — derived automatically from the class name (e.g. DatabaseConfig → database). Override with @AIConfig({ namespace: 'db' }).
  • Environment variable name — derived as NAMESPACE_FIELDKEY in SCREAMING_SNAKE_CASE. Override with @AIEnv({ env: 'MY_CUSTOM_VAR' }).

@AIConfig(options) — Class Decorator

Marks a class as a configuration schema and registers metadata.

import { AIConfig } from '@tagna/udiot';

@AIConfig({
  name?: string;         // Human-readable name (defaults to class name)
  namespace?: string;    // Key used to scope values (auto-derived from class name)
  profile?: string[];    // Active profiles; default ['local', 'staging', 'prod']
  validate?: boolean;    // Run validation on load; default true
  secrets?: 'auto' | 'manual'; // 'auto' fetches all sensitive fields; default 'auto'
  optimize?: boolean;    // Hint to enable optimizations
  description?: string;  // Documentation string
  version?: string;      // Schema version for drift tracking
})
class MyConfig {}

When namespace is omitted, the class name is converted to kebab-case and the Config suffix is stripped: DatabaseConfig → database, CacheServiceConfig → cache-service.

@AIEnv(options) — Property Decorator

Describes a single configuration field. All options are optional.

import { AIEnv } from '@tagna/udiot';

class MyConfig {
  @AIEnv({
    key?: string;                        // Override the field key (defaults to property name)
    path?: string;                       // Dot-notation path inside the namespace object
    env?: string;                        // Override the environment variable name
    type?: 'string' | 'number' | 'boolean' | 'object' | 'array';
                                         // Inferred from TypeScript design type when omitted
    required?: boolean;                  // Missing value → added to missingRequired list
    default?: unknown;                   // Fallback value when no layer provides one
    description?: string;                // Documentation string
    sensitive?: boolean;                 // Redact in logs and provenance output
    provider?: string;                   // Named secret provider to use
    dependsOn?: string[];                // Keys that must be present for this field to be valid
    conflictsWith?: string[];            // Keys that must NOT be present alongside this field
    validate?: string | string[];        // Named validation rules
    optimize?: boolean;                  // Field-level optimization hint
    observe?: boolean;                   // Emit change events when this value is overridden
    enum?: Array<string | number>;       // Allowed values; violation → policy error
    policy?: ConfigPolicy;               // Numeric/regex constraints (see below)
    owner?: string;                      // Team or person responsible for this field
    tags?: string[];                     // Arbitrary labels for grouping
  })
  myField!: string;
}

Type inference

When type is not provided, the decorator reads the TypeScript design-time type via reflect-metadata. String → 'string', Number → 'number', Boolean → 'boolean', Array → 'array', Object → 'object'.

Path notation

Setting path: 'server.port' nests the value under values[namespace].server.port. Alternatively, use double underscores in the property name (server__port) and the path is inferred as server.port automatically.

ConfigFieldSchema Interface

The resolved metadata stored per decorated property. This is what buildConfigSchema produces.

interface ConfigFieldSchema {
  propertyKey: string;           // TypeScript property name on the class
  key: string;                   // Field key (may differ from propertyKey)
  path: string;                  // Dot-notation path inside the namespace object
  type: ConfigValueType;         // 'string' | 'number' | 'boolean' | 'object' | 'array'
  required?: boolean;
  default?: unknown;
  description?: string;
  sensitive?: boolean;
  env?: string;                  // Computed or explicit environment variable name
  provider?: string;
  dependsOn?: string[];
  conflictsWith?: string[];
  validate?: string | string[];
  optimize?: boolean;
  observe?: boolean;
  enum?: Array<string | number>;
  policy?: ConfigPolicy;
  owner?: string;
  tags?: string[];
  suggestion?: string;           // Set by @AISuggested
}

ConfigPolicy Interface

Numeric or pattern constraints evaluated when a value is loaded.

interface ConfigPolicy {
  min?: number;      // Value must be >= min (applies to number type)
  max?: number;      // Value must be <= max (applies to number type)
  pattern?: string;  // Value must match this regular expression (applies to string type)
}

A violation produces a ConfigPolicyViolation in the ConfigLoadResult returned by EnvironmentConfigManager.load().

@AISuggested(reason?) — Property Decorator

Attaches an AI-hint string to a field. The suggestion is surfaced in scaffolded .env.example files and tooling output to guide developers on what value to provide.

import { AIEnv, AISuggested } from '@tagna/udiot';

class MyConfig {
  @AISuggested('Set to the connection string from your cloud provider dashboard')
  @AIEnv({ required: true, sensitive: true })
  databaseUrl!: string;
}

The reason parameter is optional. When omitted the property is still flagged as AI-suggested; the suggestion string is undefined.

API Reference

Function Signature Description
AIConfig (options?: AIConfigOptions) => ClassDecorator Registers class as a config schema
AIEnv (options?: AIEnvOptions) => PropertyDecorator Describes a config field
AISuggested (reason?: string) => PropertyDecorator Attaches an AI hint to a field
buildConfigSchema <T>(target: ConfigClass<T>) => ConfigSchema Builds the resolved schema for a class
extractSchemasFromModule (moduleExports: unknown) => ConfigSchema[] Finds all @AIConfig classes in a module export
hasConfigDecorator (target: Function) => boolean Returns true if the class has @AIConfig

Basic Usage Example

import 'reflect-metadata';
import { AIConfig, AIEnv, AISuggested, buildConfigSchema } from '@tagna/udiot';

@AIConfig({
  namespace: 'database',
  description: 'PostgreSQL connection settings',
  version: '1.0.0',
})
class DatabaseConfig {
  @AIEnv({ required: true, description: 'Database host name' })
  host!: string;

  @AIEnv({ type: 'number', default: 5432, policy: { min: 1, max: 65535 } })
  port!: number;

  @AISuggested('Retrieve from your secrets manager or Vault')
  @AIEnv({ sensitive: true, provider: 'vault', required: true })
  password!: string;

  @AIEnv({
    type: 'number',
    default: 10,
    policy: { min: 1, max: 100 },
    dependsOn: ['host'],
    description: 'Maximum number of connections in the pool',
  })
  maxConnections!: number;
}

const schema = buildConfigSchema(DatabaseConfig);
console.log(schema.namespace);  // 'database'
console.log(schema.fields.port.env);  // 'DATABASE_PORT'

Common Patterns

Enum validation

@AIEnv({ enum: ['debug', 'info', 'warn', 'error'], default: 'info' })
logLevel!: string;

Nested paths via double-underscore

@AIEnv({ type: 'number', default: 30 })
server__timeoutSeconds!: number;
// resolves to values['myapp'].server.timeoutSeconds

Profile-scoped schemas

@AIConfig({ profile: ['prod'], secrets: 'auto' })
class ProductionOnlyConfig { /* … */ }

Fields on this schema are only validated when the active environment profile matches 'prod'.

Integration Notes

  • buildConfigSchema instantiates the class with new target() to read property initialisers as default values. Ensure the constructor requires no mandatory arguments.
  • When using module bundlers or tree-shakers, import decorated classes explicitly to prevent decorator side-effects from being stripped.
  • extractSchemasFromModule accepts the result of a dynamic import() call and returns all @AIConfig classes found in the export object, making it easy to load schemas from arbitrary files.

Pitfalls

  • Forgetting import 'reflect-metadata' causes type inference to return 'string' for every field regardless of the TypeScript type annotation.
  • @AISuggested must be applied before @AIEnv in source order (decorators are applied bottom-up in TypeScript).
  • A field with sensitive: true that also has a default value will have its default exposed in the schema object. Do not set defaults for genuinely secret fields.
From the canonical UDIOT public documentation.Try it in the playground ↗