NestboltNestbolt

@nestbolt/settings

Events

Listen to settings.created, settings.updated, and settings.deleted lifecycle events.

@nestbolt/settings emits lifecycle events when a setting is written, updated, or deleted. Events are emitted via @nestjs/event-emitter when the package is installed; otherwise they're silently no-op.

Setup

Install @nestjs/event-emitter and register it in your application:

npm install @nestjs/event-emitter
import { Module } from "@nestjs/common";
import { EventEmitterModule } from "@nestjs/event-emitter";
import { SettingsModule } from "@nestbolt/settings";

@Module({
  imports: [
    EventEmitterModule.forRoot(),
    SettingsModule.forRoot(),
  ],
})
export class AppModule {}

If the package isn't installed or EventEmitterModule isn't registered, SettingsService works normally and silently skips event emission.

Event Reference

The package exports a SETTINGS_EVENTS constant for use with @OnEvent():

import { SETTINGS_EVENTS } from "@nestbolt/settings";

SETTINGS_EVENTS.CREATED; // "settings.created"
SETTINGS_EVENTS.UPDATED; // "settings.updated"
SETTINGS_EVENTS.DELETED; // "settings.deleted"

settings.created

Emitted by service.set(key, value, options?) when no row existed for key before the write.

import type { SettingCreatedEvent } from "@nestbolt/settings";

interface SettingCreatedEvent {
  key: string;
  value: unknown;
  type: "string" | "number" | "boolean" | "json";
  group: string | null;
}
import { OnEvent } from "@nestjs/event-emitter";
import { SETTINGS_EVENTS, SettingCreatedEvent } from "@nestbolt/settings";

@Injectable()
export class SettingsAuditor {
  @OnEvent(SETTINGS_EVENTS.CREATED)
  onCreated(event: SettingCreatedEvent) {
    console.log(`Setting created: ${event.key} = ${JSON.stringify(event.value)}`);
  }
}

settings.updated

Emitted by service.set(key, value, options?) when a row already existed for key. The payload includes both oldValue and newValue:

import type { SettingUpdatedEvent } from "@nestbolt/settings";

interface SettingUpdatedEvent {
  key: string;
  oldValue: unknown;
  newValue: unknown;
  type: "string" | "number" | "boolean" | "json";
}
@OnEvent(SETTINGS_EVENTS.UPDATED)
onUpdated(event: SettingUpdatedEvent) {
  this.audit.record({
    action: "setting-changed",
    key: event.key,
    before: event.oldValue,
    after: event.newValue,
  });
}

The oldValue is the deserialized value as it existed before the write, reconstructed from the stored type column.

settings.deleted

Emitted by service.forget(key) -- but only when the key actually existed. Forgetting a non-existent key is a no-op and emits nothing.

import type { SettingDeletedEvent } from "@nestbolt/settings";

interface SettingDeletedEvent {
  key: string;
  lastValue: unknown;
}
@OnEvent(SETTINGS_EVENTS.DELETED)
onDeleted(event: SettingDeletedEvent) {
  this.cacheBus.broadcast("settings:invalidate", event.key);
}

Use Cases

Cross-Process Cache Invalidation

The in-memory cache is process-local. When one Node process calls service.set("app.name", "X"), only its own cache is updated -- siblings won't see the change until their entry expires. For tighter consistency, pipe the events to your message broker:

@Injectable()
export class CrossProcessInvalidator {
  constructor(private readonly broker: MessageBroker) {}

  @OnEvent(SETTINGS_EVENTS.UPDATED)
  @OnEvent(SETTINGS_EVENTS.DELETED)
  onChange(event: { key: string }) {
    this.broker.publish("settings:invalidate", { key: event.key });
  }
}

@Injectable()
export class CacheBusListener {
  constructor(private readonly settings: SettingsService) {}

  @OnEvent("settings:invalidate-received")
  invalidate({ key }: { key: string }) {
    this.settings.flushCache(); // or invalidate just `key` via your own logic
  }
}

Audit Logging

Record every change to sensitive settings:

@OnEvent(SETTINGS_EVENTS.UPDATED)
async onUpdated(event: SettingUpdatedEvent) {
  await this.auditLog.record({
    actor: this.requestContext.userId,
    action: "settings.update",
    key: event.key,
    before: event.oldValue,
    after: event.newValue,
  });
}

Reactive Re-Configuration

Re-bootstrap a service when its setting changes:

@OnEvent(SETTINGS_EVENTS.UPDATED, { async: true })
async onUpdated(event: SettingUpdatedEvent) {
  if (event.key === "mail.host" || event.key === "mail.port") {
    await this.mailer.reconnect();
  }
}

Emission Timing

Events are emitted after the database transaction commits but before the call returns. If a listener throws, the exception propagates back to the caller of set() / forget(). Wrap listener bodies in try/catch (or use the async listener pattern) when failures should be isolated from the write path.

The event emitter is resolved as an @Optional() provider with the token "EventEmitter2". When @nestjs/event-emitter isn't installed, the dependency is undefined and emission is skipped silently -- no error, no warning.