@spinajs/configuration-db-source
v3.0.26
Published
> TODO: description
Readme
@spinajs/configuration-db-source
A SpinaJS configuration source that loads application configuration from a
database table and lets @Config-decorated values be exposed to the
database so they can be administered at runtime — optionally watched for
live updates without a restart.
Documentation
Full documentation lives in docs/configuration-db-source:
- Database configuration source
- Exposing options
- Watching options
- Entry types and the value converter
- File entries and validators
- Running under authz
- Custom configuration source
- Custom value converter
- Customizing the bootstrapper
Installation
npm install @spinajs/configuration-db-sourceImporting the package is enough to register everything it provides:
- a Bootstrapper that persists exposed options and runs the watch timer,
- a ConfigurationSource (load order
999, so it runs after file sources) that reads the configuration table and merges every exposed row into the config, - the
DbConfigmodel and the migration that creates the table.
import '@spinajs/configuration-db-source';Examples
Runnable, self-contained examples live in examples/ — see examples/README.md for the index:
- getting started — connection, migration, loading rows
- exposing config to the db —
expose/exposeOptions - watching for live updates —
watch+ poll interval - entry types — stored text formats and parsed values
- custom connection / table — with a custom migration
How it works
- Provide a db connection. The source loads last and reads the
db.Connectionsthat an earlier source (a config file, usually) produced. - Create the table. Run the bundled migration — it creates the
configurationtable on the default connection (eg. setMigration: { OnStartup: true }on the connection). - Load. On
Configuration.load()the source reads every row whoseExposedflag is set and merges it into the config under itsSlug. - Expose & watch (optional).
@Configproperties markedexpose: trueare inserted (InsertOrIgnore) on first run; those also markedwatch: trueare periodically refreshed into the live configuration.
import { Config } from '@spinajs/configuration';
export class MailService {
@Config('mailer.fromAddress', {
defaultValue: '[email protected]',
expose: true,
exposeOptions: { type: 'string', group: 'mailer', label: 'From address', watch: true },
})
protected FromAddress: string;
}When it runs — gated on the configuration table
This package writes to the database only once the configuration table is ready.
The bootstrapper does no db work when it runs; it hooks DI's resolve events and
defers everything until then:
di.resolved.Orm, when the table exists: theOrmresolves after itsMigration.OnStartupmigrations, which include the bundled one, and a database migrated bymigrate-uphas it already.
The first such event (1) inserts every exposed @Config option into the
table (InsertOrIgnore) and (2) starts the watch timer. @Config properties
registered after that are persisted/loaded live by a
di.registered.__configuration_property__ handler; before it, the sweep handles them.
Implication: resolve the Orm
somewhere in your app for exposed options to be written and watched. Reading existing
rows is independent of this — it happens during Configuration.load() via the
source, which uses the resolved ORM connection when one is available.
Configuration
Set under the configuration_db_source key (an earlier source must provide it):
| Key | Default | Description |
| --- | --- | --- |
| connection | default | name of the db.Connections entry to read from (default resolves to db.DefaultConnection) |
| table | configuration | table to read configuration rows from |
The watch poll interval defaults to 3 minutes. Override it by registering the
__config_watch_interval__ DI value (milliseconds) before the ORM resolves:
DI.register({ value: 30_000 }).asValue('__config_watch_interval__');The
connection/tableoptions affect the load path only. Exposing and watching use theDbConfigmodel, which is bound to theconfigurationtable on thedefaultconnection — see 05-custom-connection.ts.
Entry types
Each row stores its Value as text plus a Type that decides how it is
converted. Values are stored in canonical form (ISO dates/times, decimal
numbers, true/false booleans) — there is no legacy-format tolerance.
| Type | Stored text | Runtime value |
| --- | --- | --- |
| string, file, oneOf | as-is | string |
| number | integer text | number |
| float, range | decimal text | number |
| boolean | true / false | boolean |
| json | JSON | parsed object/array |
| manyOf | JSON array | string[] |
| date | ISO date (yyyy-MM-dd) | luxon DateTime |
| time | ISO time (HH:mm:ss) | luxon DateTime |
| datetime | ISO 8601 | luxon DateTime |
| date-range / time-range / datetime-range | two ISO values joined by ; | DateTime[] |
Conversion is handled by DbConfigValueConverter, a subclass of the ORM's
UniversalValueConverter (the universal primitives live in the shared converter;
the config-only composite types — oneOf, manyOf, file, range and the
*-range types — live in the subclass). See
04-entry-types.ts for a worked example.
File entries
An entry of type: 'file' stores a file name in Value. meta.file says where the
file lives and what may be uploaded through @spinajs/configuration-http:
import { FileTypeEnum } from '@spinajs/http';
@Config('yourscreen.kalkulator.template', {
defaultValue: 'kalkulator.xlsx',
expose: true,
exposeOptions: {
type: 'file',
group: 'templates',
label: 'Offer calculator template',
meta: {
file: {
fs: 'fs-excel-templates', // @spinajs/fs provider name, required
extensions: ['xlsx'], // lowercase, without the dot
mimeTypes: [FileTypeEnum.xlsx], // checked against the type detected from content
maxSize: 10 * 1024 * 1024, // bytes, default CONFIG_FILE_DEFAULT_MAX_SIZE ( 10 MB )
validator: OfferTemplateValidator, // optional, class or name
},
},
},
})
protected Template: string;- A
type: 'file'entry withoutmeta.file.fsthrows when it is registered, with the slug in the message. validatoris stored as a name ( the class name for a class ). A validator extendsConfigFileValidator, is registered with@Injectable(ConfigFileValidator)and rejects a file by throwingValidationFailed; its message is returned to the client. It is looked up by instanceServiceNameor class name (resolveConfigFileValidator).- A class
validatormust be declared before the class holding the decorated property in module load order - otherwise the decorator reads it in its temporal dead zone ( ESM ) and throws. Use the string form (validator: 'OfferTemplateValidator') when that order cannot be guaranteed.
@Injectable(ConfigFileValidator)
export class OfferTemplateValidator extends ConfigFileValidator {
public async validate(file: IConfigFileCandidate, entry: DbConfig): Promise<void> {
// file.localPath, file.originalName, file.size, file.mimeType
throw new ValidationFailed('Template is missing the Offer sheet', []);
}
}Key concepts
Slugis the config path — exactly what you read withConfiguration.get(...)/ pass to@Config(...).Groupis display-only metadata and is not part of the path.Exposedgates loading — only exposed rows reach the running app.
Under authz
An app that guards DbConfig (for example through @spinajs/configuration-http) calls
requireSystemPrincipal(). From then on, persistence of exposed options and the watch poll run as SYSTEM
(runAs(SYSTEM, 'config-poll')). They start once the root authz service boots (the http server does it). An app without an http server
waits 15 s, logs a warning and boots the service itself; an http app slower than that would lose the
boot-time check of @Access permissions of controllers registered later.
Without that call, queries run as before.
