@nestbolt/settings
Seeding
Pre-populate the settings table at startup with defaults and INSERT ... OR IGNORE.
Why Seed?
When a new app instance boots against an empty (or partially populated) database, you want known keys -- app.name, app.maintenance, mail.host -- to exist with sensible defaults so the rest of the application can read them safely. defaults lets you express that without writing a custom migration.
Declaring Defaults
Pass an array of SettingDefinition objects to SettingsModule.forRoot():
import { Module } from "@nestjs/common";
import { SettingsModule } from "@nestbolt/settings";
@Module({
imports: [
SettingsModule.forRoot({
defaults: [
{ key: "app.name", value: "Nestbolt", type: "string" },
{ key: "app.port", value: 3000, type: "number" },
{ key: "app.maintenance", value: false, type: "boolean" },
{ key: "mail.host", value: "smtp.example.com", group: "mail" },
{ key: "mail.port", value: 587, type: "number", group: "mail" },
],
}),
],
})
export class AppModule {}Each definition supports:
| Field | Type | Required | Description |
|---|---|---|---|
key | string | yes | Setting key. Must be a non-empty string ≤255 chars with no control characters. |
value | unknown | yes | The default value. Used by the seed step and as the fallback for @Setting(key) consumers when the key is missing. |
type | "string" | "number" | "boolean" | "json" | no | Explicit type. Inferred from value when omitted. |
group | string | no | Optional group tag. |
description | string | no | Optional human-readable description. |
How Seeding Runs
When autoSeed !== false (the default), onModuleInit() runs a single bulk insert:
await repo
.createQueryBuilder()
.insert()
.into(SettingEntity)
.values(rows)
.orIgnore()
.execute();Translated by TypeORM to your dialect's "insert and skip duplicates" form -- INSERT ... ON CONFLICT DO NOTHING on Postgres / SQLite, INSERT IGNORE on MySQL.
This means:
- Existing rows are never overwritten. Operators can change a setting in production without the next deploy resetting it.
- The seed is race-safe. Two app instances starting simultaneously won't fight for the same row -- the first commit wins, the second is silently ignored.
- The seed is one round-trip. All defaults are inserted in a single statement, regardless of how many entries are in the array.
Disabling the Seed
Set autoSeed: false to skip the database write while still exposing SETTING_<key> providers:
SettingsModule.forRoot({
autoSeed: false,
defaults: [
{ key: "app.name", value: "Nestbolt", type: "string" },
],
});With autoSeed: false:
- Nothing is written to the database at module init.
@Setting(key)still works: it callsservice.get(key, def.value)at provider construction, returning the live DB value if it exists or the default otherwise.
This is useful when you want @Setting(key) for typed constructor injection but you maintain settings rows yourself via migrations or an admin UI.
Re-Seeding Behavior
The seed always uses INSERT ... OR IGNORE. Calling it twice (e.g., bouncing the process) writes nothing the second time -- existing rows are preserved as-is.
If you need to reset a setting back to its default, do it explicitly:
await this.settings.forget("app.name");
// Next read will return undefined or the @Setting() default
// Or, to write the default explicitly:
await this.settings.set("app.name", "Nestbolt", { type: "string" });Seeding with forRootAsync
forRootAsync() honors defaults and autoSeed the same way:
SettingsModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
defaults: [
{ key: "app.name", value: config.get("APP_NAME", "Nestbolt") },
],
}),
});The one difference: the async form does not create per-key SETTING_<key> providers (the keys aren't known until the factory runs). If you need @Setting(key) injection, use the synchronous forRoot() instead. See Decorator -- Limitations for the full rationale.
Validation at Seed Time
Every key in defaults is validated up front before any DB work. A bad key fails fast at module init with a clear error:
SettingsModule.forRoot({
defaults: [
{ key: "", value: "x" }, // throws: key must be a non-empty string
],
});