src/shared/utils/config-import/config-import-orchestrator.service.ts
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:
Services should register their import functions during construction. The orchestrator automatically runs imports during onApplicationBootstrap.
Properties |
|
Methods |
|
constructor(logger: PinoLogger, configService: ConfigService)
|
|||||||||
|
Parameters :
|
| 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 :
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 :
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>
|
| 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)`,
);
}
}
}