@spinajs/configuration-db-source
v2.0.486
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.
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 ORM resolution
This package writes to the database only after the ORM module is resolved. The bootstrapper does no db work when it runs; instead it hooks DI's resolve event and defers everything until the ORM is available:
- It subscribes with
DI.once('di.resolved.Orm', …).@spinajs/diemits adi.resolved.<Type>event the first time a type is resolved/cached (see the container cache in@spinajs/di), so this handler fires the first timeOrmis resolved. At that point the package (1) inserts every exposed@Configoption into the table (InsertOrIgnore) and (2) starts the watch timer. @Configproperties registered after the ORM is up are persisted/loaded live by adi.registered.__configuration_property__handler — but it is guarded byDI.has(Orm), so before the ORM exists it does nothing (those vars are handled by thedi.resolved.Ormstep above).
Implication: the ORM must actually be resolved somewhere in your app
(injected, or await DI.resolve(Orm)) for exposed options to be written and
watched. If nothing resolves the ORM, those hooks never fire. 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.
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.
