File

src/shared/utils/config-import/config-import-orchestrator.service.ts

Description

Centralized orchestrator for configuration imports. Imports are processed tenant-by-tenant to provide better log clarity and isolation - if one tenant fails, others can still be imported.

Flow:

  1. Discover all tenant folders
  2. For each tenant: a. Setup tenant (create if needed) b. Run all import phases in order (CORE → CONFIGURATION → REFERENCES → FINAL)
  3. Continue with next tenant even if current tenant fails

Services should register their import functions during construction. The orchestrator automatically runs imports during onApplicationBootstrap.

Index

Properties
Methods

Constructor

constructor(logger: PinoLogger, configService: ConfigService)
Parameters :
Name Type Optional
logger PinoLogger No
configService ConfigService No

Methods

Private discoverTenants
discoverTenants()

Discover all tenant folders in the config directory.

Returns : string[]
Private Async executeImports
executeImports()
Returns : Promise<void>
Async onApplicationBootstrap
onApplicationBootstrap()

Lifecycle hook - automatically triggers import orchestration.

Returns : any
register
register(name: string, phase: ImportPhase, importFn: TenantImportFn)

Register an import function for orchestration.

Parameters :
Name Type Optional Description
name string No
  • Human-readable name for logging
phase ImportPhase No
  • The import phase (determines order within tenant)
importFn TenantImportFn No
  • The import function to call (receives tenantId)
Returns : void
registerTenantSetup
registerTenantSetup(name: string, setupFn: (tenantId: string) => void)

Register a tenant setup function. This is called first for each tenant to ensure the tenant exists.

Parameters :
Name Type Optional Description
name string No
  • Human-readable name for logging
setupFn function No
  • Function that creates/verifies tenant, returns true if tenant is valid
Returns : void
Async runImports
runImports()

Execute all registered imports tenant-by-tenant. Safe to call multiple times - only runs once. Returns the same promise if called while running.

Returns : Promise<void>

Properties

Private hasRun
Type : unknown
Default value : false
Private Readonly importers
Type : RegisteredImporter[]
Default value : []
Private runPromise
Type : Promise<void> | null
Default value : null
Private tenantSetup
Type : TenantSetupFn | null
Default value : null
import { existsSync, readdirSync } from "node:fs";
import { Injectable, OnApplicationBootstrap } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { PinoLogger } from "nestjs-pino";

/**
 * Interface for services that can be registered with the import orchestrator.
 */
export interface ImportableService {
    /**
     * Import method that will be called by the orchestrator.
     */
    import(): Promise<void>;
}

/**
 * Import phase definitions with their order.
 * Lower numbers run first.
 */
export enum ImportPhase {
    /** Core infrastructure (keys, certificates) */
    CORE = 10,
    /** Configuration (issuance, credential configs) */
    CONFIGURATION = 20,
    /** References (presentation configs that may reference certs) */
    REFERENCES = 30,
    /** Final phase (status lists, trust lists) */
    FINAL = 40,
}

/**
 * Type for tenant-aware import functions.
 * Each function receives the tenant ID and processes imports for that specific tenant.
 */
export type TenantImportFn = (tenantId: string) => Promise<void>;

interface RegisteredImporter {
    name: string;
    phase: ImportPhase;
    importForTenant: TenantImportFn;
}

interface TenantSetupFn {
    name: string;
    setup: (tenantId: string) => Promise<boolean>;
}

/**
 * Centralized orchestrator for configuration imports.
 * Imports are processed tenant-by-tenant to provide better log clarity
 * and isolation - if one tenant fails, others can still be imported.
 *
 * Flow:
 * 1. Discover all tenant folders
 * 2. For each tenant:
 *    a. Setup tenant (create if needed)
 *    b. Run all import phases in order (CORE → CONFIGURATION → REFERENCES → FINAL)
 * 3. Continue with next tenant even if current tenant fails
 *
 * Services should register their import functions during construction.
 * The orchestrator automatically runs imports during onApplicationBootstrap.
 */
@Injectable()
export class ConfigImportOrchestratorService implements OnApplicationBootstrap {
    private readonly importers: RegisteredImporter[] = [];
    private tenantSetup: TenantSetupFn | null = null;
    private hasRun = false;
    private runPromise: Promise<void> | null = null;

    constructor(
        private readonly logger: PinoLogger,
        private readonly configService: ConfigService,
    ) {}

    /**
     * Lifecycle hook - automatically triggers import orchestration.
     */
    async onApplicationBootstrap() {
        await this.runImports();
    }

    /**
     * Register a tenant setup function.
     * This is called first for each tenant to ensure the tenant exists.
     * @param name - Human-readable name for logging
     * @param setupFn - Function that creates/verifies tenant, returns true if tenant is valid
     */
    registerTenantSetup(
        name: string,
        setupFn: (tenantId: string) => Promise<boolean>,
    ): void {
        if (this.hasRun) {
            this.logger.warn(
                `Tenant setup "${name}" registered after orchestration already ran`,
            );
        }
        this.tenantSetup = { name, setup: setupFn };
    }

    /**
     * Register an import function for orchestration.
     * @param name - Human-readable name for logging
     * @param phase - The import phase (determines order within tenant)
     * @param importFn - The import function to call (receives tenantId)
     */
    register(name: string, phase: ImportPhase, importFn: TenantImportFn): void {
        if (this.hasRun) {
            this.logger.warn(
                `Importer "${name}" registered after orchestration already ran`,
            );
        }
        this.importers.push({ name, phase, importForTenant: importFn });
    }

    /**
     * Execute all registered imports tenant-by-tenant.
     * Safe to call multiple times - only runs once.
     * Returns the same promise if called while running.
     */
    async runImports(): Promise<void> {
        // If already running, return the existing promise
        if (this.runPromise) {
            return this.runPromise;
        }

        // If already completed, return immediately
        if (this.hasRun) {
            return;
        }

        // Start the import process
        this.runPromise = this.executeImports();
        return this.runPromise;
    }

    /**
     * Discover all tenant folders in the config directory.
     */
    private discoverTenants(): string[] {
        const configPath = this.configService.get<string>("CONFIG_FOLDER");
        if (!configPath || !existsSync(configPath)) {
            return [];
        }

        return readdirSync(configPath, { withFileTypes: true })
            .filter((entry) => entry.isDirectory())
            .map((entry) => entry.name);
    }

    private async executeImports(): Promise<void> {
        if (!this.configService.get<boolean>("CONFIG_IMPORT")) {
            this.hasRun = true;
            this.logger.info("Config import is disabled");
            return;
        }

        // Sort importers by phase
        const sortedImporters = [...this.importers].sort(
            (a, b) => a.phase - b.phase,
        );

        // Discover tenants
        const tenants = this.discoverTenants();

        this.logger.info(
            `Starting config import for ${tenants.length} tenant(s)`,
        );

        const failedTenants: string[] = [];

        for (const tenantId of tenants) {
            this.logger.debug(`[${tenantId}] Starting import`);

            try {
                // Step 1: Setup tenant (create if needed)
                if (this.tenantSetup) {
                    const isValid = await this.tenantSetup.setup(tenantId);
                    if (!isValid) {
                        this.logger.warn(
                            `[${tenantId}] Tenant setup returned invalid, skipping`,
                        );
                        continue;
                    }
                }

                // Step 2: Run all import phases for this tenant
                for (const importer of sortedImporters) {
                    this.logger.debug(
                        `[${tenantId}] Running ${importer.name} (phase ${importer.phase})`,
                    );
                    try {
                        await importer.importForTenant(tenantId);
                    } catch (error: any) {
                        this.logger.error(
                            { error: error.message },
                            `[${tenantId}] Failed to import ${importer.name}: ${error.message}`,
                        );
                        // Continue with next importer for this tenant
                    }
                }

                this.logger.info(`[${tenantId}] Import completed`);
            } catch (error: any) {
                this.logger.error(
                    { error: error.message },
                    `[${tenantId}] Failed to import tenant: ${error.message}`,
                );
                failedTenants.push(tenantId);
                // Continue with next tenant
            }
        }

        this.hasRun = true;

        if (failedTenants.length > 0) {
            this.logger.warn(
                `Config import completed with ${failedTenants.length} failed tenant(s): ${failedTenants.join(", ")}`,
            );
        } else {
            this.logger.info(
                `Config import completed for ${tenants.length} tenant(s)`,
            );
        }
    }
}

results matching ""

    No results matching ""