npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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:

Installation

npm install @spinajs/configuration-db-source

Importing 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 DbConfig model 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:

How it works

  1. Provide a db connection. The source loads last and reads the db.Connections that an earlier source (a config file, usually) produced.
  2. Create the table. Run the bundled migration — it creates the configuration table on the default connection (eg. set Migration: { OnStartup: true } on the connection).
  3. Load. On Configuration.load() the source reads every row whose Exposed flag is set and merges it into the config under its Slug.
  4. Expose & watch (optional). @Config properties marked expose: true are inserted (InsertOrIgnore) on first run; those also marked watch: true are 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: the Orm resolves after its Migration.OnStartup migrations, which include the bundled one, and a database migrated by migrate-up has 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 / table options affect the load path only. Exposing and watching use the DbConfig model, which is bound to the configuration table on the default connection — 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 without meta.file.fs throws when it is registered, with the slug in the message.
  • validator is stored as a name ( the class name for a class ). A validator extends ConfigFileValidator, is registered with @Injectable(ConfigFileValidator) and rejects a file by throwing ValidationFailed; its message is returned to the client. It is looked up by instance ServiceName or class name ( resolveConfigFileValidator ).
  • A class validator must 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

  • Slug is the config path — exactly what you read with Configuration.get(...) / pass to @Config(...). Group is display-only metadata and is not part of the path.
  • Exposed gates 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.