first commit
This commit is contained in:
184
docs/foundation/core/config/README.md
Normal file
184
docs/foundation/core/config/README.md
Normal file
@@ -0,0 +1,184 @@
|
||||
# @suaveplan/config - Layered configuration management: defaults, file, object, env, CLI layers merged in priority order with deep-merge, Zod validation, OTel instrumentation, and event-bus watch support.
|
||||
|
||||
## Summary
|
||||
|
||||
Layered configuration management for Suaveplan. Resolves an ordered set of typed layers (defaults, file, object, env, CLI) in priority order, deep-merges the results, applies per-environment overrides, validates via a caller-supplied Zod schema or custom function, and exposes an immutable `ConfigStore` with live reload and event-bus change propagation.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [config-builder](./src/core/config-builder/config-builder.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [config-builder](./src/core/config-builder/config-builder.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [config-store](./src/core/config-store/config-store.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [config-store](./src/core/config-store/config-store.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [cli-layer](./src/layers/cli-layer/cli-layer.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [cli-layer](./src/layers/cli-layer/cli-layer.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [defaults-layer](./src/layers/defaults-layer/defaults-layer.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [defaults-layer](./src/layers/defaults-layer/defaults-layer.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [env-layer](./src/layers/env-layer/env-layer.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [env-layer](./src/layers/env-layer/env-layer.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [file-layer](./src/layers/file-layer/file-layer.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [file-layer](./src/layers/file-layer/file-layer.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [object-layer](./src/layers/object-layer/object-layer.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [object-layer](./src/layers/object-layer/object-layer.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [common](./src/types/common.md) — This module defines every public TypeScript interface and type alias used across the `@suaveplan/config` package. It is…
|
||||
- [schemas](./src/types/schemas.md) — This module exposes Zod schemas for every public options interface in `@suaveplan/config`. Using these schemas, callers…
|
||||
- [mask](./src/utils/mask.md) — This module provides low-level dot-notation path traversal utilities (`getByPath`, `setByPath`, `hasByPath`) and the `m…
|
||||
- [merge](./src/utils/merge.md) — This module provides two functions that form the merge backbone of the config pipeline: `deepMerge` performs a structur…
|
||||
<!-- end:auto-docs --> - [config-builder](./src/core/config-builder/config-builder.md) — <!-- begin:auto-docs --> _(no submodules)_- [config-store](./src/core/config-store/config-store.md) — <!-- begin:auto-docs --> _(no submodules)_- [config-store](./src/core/config-store/config-store.md) — <!-- begin:auto-docs --> _(no submodules)_- [cli-layer](./src/layers/cli-layer/cli-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [cli-layer](./src/layers/cli-layer/cli-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [defaults-layer](./src/layers/defaults-layer/defaults-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [defaults-layer](./src/layers/defaults-layer/defaults-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [env-layer](./src/layers/env-layer/env-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [env-layer](./src/layers/env-layer/env-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [file-layer](./src/layers/file-layer/file-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [file-layer](./src/layers/file-layer/file-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [object-layer](./src/layers/object-layer/object-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [object-layer](./src/layers/object-layer/object-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [common](./src/types/common.md) — This module defines every public TypeScript interface and type alias used across the `@suaveplan/config` package. It is…
|
||||
- [schemas](./src/types/schemas.md) — This module exposes Zod schemas for every public options interface in `@suaveplan/config`. Using these schemas, callers…
|
||||
- [mask](./src/utils/mask.md) — This module provides low-level dot-notation path traversal utilities (`getByPath`, `setByPath`, `hasByPath`) and the `m…
|
||||
- [merge](./src/utils/merge.md) — This module provides two functions that form the merge backbone of the config pipeline: `deepMerge` performs a structur…
|
||||
- [config-builder](./src/core/config-builder/config-builder.md) — <!-- begin:auto-docs --> _(no submodules)_- [config-store](./src/core/config-store/config-store.md) — <!-- begin:auto-docs --> _(no submodules)_- [config-store](./src/core/config-store/config-store.md) — <!-- begin:auto-docs --> _(no submodules)_- [cli-layer](./src/layers/cli-layer/cli-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [cli-layer](./src/layers/cli-layer/cli-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [defaults-layer](./src/layers/defaults-layer/defaults-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [defaults-layer](./src/layers/defaults-layer/defaults-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [env-layer](./src/layers/env-layer/env-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [env-layer](./src/layers/env-layer/env-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [file-layer](./src/layers/file-layer/file-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [file-layer](./src/layers/file-layer/file-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [object-layer](./src/layers/object-layer/object-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [object-layer](./src/layers/object-layer/object-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [common](./src/types/common.md) — This module defines every public TypeScript interface and type alias used across the `@suaveplan/config` package. It is…
|
||||
- [schemas](./src/types/schemas.md) — This module exposes Zod schemas for every public options interface in `@suaveplan/config`. Using these schemas, callers…
|
||||
- [mask](./src/utils/mask.md) — This module provides low-level dot-notation path traversal utilities (`getByPath`, `setByPath`, `hasByPath`) and the `m…
|
||||
- [merge](./src/utils/merge.md) — This module provides two functions that form the merge backbone of the config pipeline: `deepMerge` performs a structur…
|
||||
- [config-builder](./src/core/config-builder/config-builder.md) — <!-- begin:auto-docs --> _(no submodules)_- [config-store](./src/core/config-store/config-store.md) — <!-- begin:auto-docs --> _(no submodules)_- [config-store](./src/core/config-store/config-store.md) — <!-- begin:auto-docs --> _(no submodules)_- [cli-layer](./src/layers/cli-layer/cli-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [cli-layer](./src/layers/cli-layer/cli-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [defaults-layer](./src/layers/defaults-layer/defaults-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [defaults-layer](./src/layers/defaults-layer/defaults-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [env-layer](./src/layers/env-layer/env-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [env-layer](./src/layers/env-layer/env-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [file-layer](./src/layers/file-layer/file-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [file-layer](./src/layers/file-layer/file-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [object-layer](./src/layers/object-layer/object-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [object-layer](./src/layers/object-layer/object-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [common](./src/types/common.md) — This module defines every public TypeScript interface and type alias used across the `@suaveplan/config` package. It is…
|
||||
- [schemas](./src/types/schemas.md) — This module exposes Zod schemas for every public options interface in `@suaveplan/config`. Using these schemas, callers…
|
||||
- [mask](./src/utils/mask.md) — This module provides low-level dot-notation path traversal utilities (`getByPath`, `setByPath`, `hasByPath`) and the `m…
|
||||
- [merge](./src/utils/merge.md) — This module provides two functions that form the merge backbone of the config pipeline: `deepMerge` performs a structur…
|
||||
- [config-builder](./src/core/config-builder/config-builder.md) — <!-- begin:auto-docs --> _(no submodules)_- [config-store](./src/core/config-store/config-store.md) — <!-- begin:auto-docs --> _(no submodules)_ - [config-store](./src/core/config-store/config-store.md) — <!-- begin:auto-docs --> _(no submodules)_- [cli-layer](./src/layers/cli-layer/cli-layer.md) — <!-- begin:auto-docs --> _(no submodules)_ - [cli-layer](./src/layers/cli-layer/cli-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [defaults-layer](./src/layers/defaults-layer/defaults-layer.md) — <!-- begin:auto-docs --> _(no submodules)_ - [defaults-layer](./src/layers/defaults-layer/defaults-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [env-layer](./src/layers/env-layer/env-layer.md) — <!-- begin:auto-docs --> _(no submodules)_ - [env-layer](./src/layers/env-layer/env-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [file-layer](./src/layers/file-layer/file-layer.md) — <!-- begin:auto-docs --> _(no submodules)_ - [file-layer](./src/layers/file-layer/file-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [object-layer](./src/layers/object-layer/object-layer.md) — <!-- begin:auto-docs --> _(no submodules)_ - [object-layer](./src/layers/object-layer/object-layer.md) — <!-- begin:auto-docs --> _(no submodules)_- [common](./src/types/common.md) — This module defines every public TypeScript interface and type alias used across the `@suaveplan/config` package. It is…
|
||||
- [schemas](./src/types/schemas.md) — This module exposes Zod schemas for every public options interface in `@suaveplan/config`. Using these schemas, callers…
|
||||
- [mask](./src/utils/mask.md) — This module provides low-level dot-notation path traversal utilities (`getByPath`, `setByPath`, `hasByPath`) and the `m…
|
||||
- [merge](./src/utils/merge.md) — This module provides two functions that form the merge backbone of the config pipeline: `deepMerge` performs a structur…
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
bun add @suaveplan/config
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```ts
|
||||
import { createConfig, defaultsLayer, fileLayer, envLayer, cliLayer } from '@suaveplan/config';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import z from 'zod';
|
||||
|
||||
const Schema = z.object({
|
||||
server: z.object({ port: z.number(), host: z.string() }),
|
||||
db: z.object({ url: z.string(), pool: z.number(), password: z.string() }),
|
||||
log: z.object({ level: z.string() }),
|
||||
});
|
||||
type AppConfig = z.infer<typeof Schema>;
|
||||
|
||||
const store = createConfig<AppConfig>({
|
||||
layers: [
|
||||
defaultsLayer({ server: { port: 3000, host: '0.0.0.0' }, db: { pool: 5, password: '' }, log: { level: 'info' } }),
|
||||
fileLayer({ content: JSON.parse(readFileSync('./config.json', 'utf-8')) }),
|
||||
envLayer({ prefix: 'APP_', mapping: { PORT: 'server.port', DB_URL: 'db.url', DB_PASS: 'db.password' } }),
|
||||
cliLayer({ args: process.argv.slice(2), mapping: { port: 'server.port' } }),
|
||||
],
|
||||
validate: (raw) => Schema.parse(raw),
|
||||
sensitive: ['db.password', 'db.url'],
|
||||
environments: { production: { log: { level: 'warn' } }, development: { log: { level: 'debug' } } },
|
||||
environment: process.env.NODE_ENV ?? 'development',
|
||||
});
|
||||
|
||||
console.log(store.get('server.port')); // number
|
||||
console.log(store.toMasked()); // db.password = '****'
|
||||
|
||||
// Live reload
|
||||
await store.reload();
|
||||
|
||||
// Subscribe to changes
|
||||
const unsub = store.subscribe((next) => console.log('Config updated:', next));
|
||||
```
|
||||
|
||||
## Module Documentation
|
||||
|
||||
| Module | Description |
|
||||
|--------|-------------|
|
||||
| [`src/types/common.md`](src/types/common.md) | All public TypeScript interfaces and type aliases |
|
||||
| [`src/types/schemas.md`](src/types/schemas.md) | Zod option schemas for runtime validation |
|
||||
| [`src/core/config-builder/config-builder.md`](src/core/config-builder/config-builder.md) | `createConfig` orchestrator |
|
||||
| [`src/core/config-store/config-store.md`](src/core/config-store/config-store.md) | `createConfigStore` static store factory |
|
||||
| [`src/layers/defaults-layer/defaults-layer.md`](src/layers/defaults-layer/defaults-layer.md) | Static default values layer |
|
||||
| [`src/layers/object-layer/object-layer.md`](src/layers/object-layer/object-layer.md) | In-memory config object layer |
|
||||
| [`src/layers/env-layer/env-layer.md`](src/layers/env-layer/env-layer.md) | Environment variable layer + `coerceValue` |
|
||||
| [`src/layers/file-layer/file-layer.md`](src/layers/file-layer/file-layer.md) | Pre-parsed file content layer with watch mode |
|
||||
| [`src/layers/cli-layer/cli-layer.md`](src/layers/cli-layer/cli-layer.md) | CLI argument layer + `parseArgs` |
|
||||
| [`src/utils/merge.md`](src/utils/merge.md) | `deepMerge` and `mergeLayers` utilities |
|
||||
| [`src/utils/mask.md`](src/utils/mask.md) | Path utilities and `maskSensitive` |
|
||||
|
||||
## Layer Priorities
|
||||
|
||||
| Layer | Priority | Description |
|
||||
|-------|----------|-------------|
|
||||
| `defaultsLayer` | 0 | Static baseline — always overridden |
|
||||
| `fileLayer` | 10 | Pre-parsed file content |
|
||||
| `objectLayer` | 20 | In-memory programmatic overrides |
|
||||
| `envLayer` | 30 | Environment variables |
|
||||
| `cliLayer` | 40 | CLI arguments — highest priority |
|
||||
|
||||
## API
|
||||
|
||||
### Core
|
||||
|
||||
| Export | Description |
|
||||
|--------|-------------|
|
||||
| `createConfig(options)` | Resolves layers, merges, validates, returns live store |
|
||||
| `createConfigStore(options)` | Low-level static store factory |
|
||||
|
||||
### Layers
|
||||
|
||||
| Export | Description |
|
||||
|--------|-------------|
|
||||
| `defaultsLayer(defaults)` | Static defaults at priority 0 |
|
||||
| `fileLayer(options)` | Pre-parsed file content at priority 10 |
|
||||
| `objectLayer(data, priority?)` | In-memory object at priority 20 |
|
||||
| `envLayer(options)` | Environment variables at priority 30 |
|
||||
| `cliLayer(options)` | CLI arguments at priority 40 |
|
||||
| `parseArgs(args, mapping)` | Parse raw argv into flag→value map |
|
||||
|
||||
### Utilities
|
||||
|
||||
| Export | Description |
|
||||
|--------|-------------|
|
||||
| `deepMerge(base, override)` | Recursive object merge |
|
||||
| `mergeLayers(layers)` | Fold ordered layers with deepMerge |
|
||||
| `getByPath(obj, path)` | Read dot-notation path |
|
||||
| `setByPath(obj, path, value)` | Write dot-notation path |
|
||||
| `hasByPath(obj, path)` | Check dot-notation path existence |
|
||||
| `maskSensitive(data, paths, mask?)` | Replace sensitive paths |
|
||||
| `coerceValue(raw)` | Convert env-var string to typed value |
|
||||
|
||||
### Error Codes
|
||||
|
||||
| Code | Description |
|
||||
|------|-------------|
|
||||
| `CONFIG.MISSING` | Required config key not found |
|
||||
| `CONFIG.INVALID_TYPE` | Value has wrong type |
|
||||
| `CONFIG.PARSE_FAILED` | Layer resolution threw |
|
||||
| `CONFIG.FILE_NOT_FOUND` | File layer file not found |
|
||||
| `CONFIG.ENV_NOT_SET` | Required env var not set |
|
||||
| `CONFIG.RELOAD_FAILED` | Reload aborted or layer threw |
|
||||
| `CONFIG.VALIDATION_FAILED` | Validator threw |
|
||||
|
||||
## Dependencies
|
||||
|
||||
| Package | Role |
|
||||
|---------|------|
|
||||
| `@suaveplan/error` | `SuaveplanError` base, `defineCodes` |
|
||||
| `@suaveplan/types` | `LoggerLike`, `EventBusLike` |
|
||||
| `@suaveplan/events` | `EventBus` for file-layer watch mode |
|
||||
| `@suaveplan/env` | Coercion functions for env-var values |
|
||||
| `zod` | Option schemas |
|
||||
| `@opentelemetry/api` | Spans, counters, histograms |
|
||||
| `tslib` | TypeScript runtime helpers |
|
||||
|
||||
## Benchmarks
|
||||
|
||||
Run with `bun run bench`.
|
||||
|
||||
| Benchmark | Operations / sec (approx.) |
|
||||
|-----------|---------------------------|
|
||||
| `createConfig` 5 layers (cold) | TBD |
|
||||
| `store.get` 100x deeply nested key | TBD |
|
||||
| `mergeLayers` 2 layers | TBD |
|
||||
| `mergeLayers` 5 layers | TBD |
|
||||
| `mergeLayers` 10 layers | TBD |
|
||||
| `mergeLayers` 20 layers | TBD |
|
||||
|
||||
_Run `bun run bench` to populate these numbers on your hardware._
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
# config-builder — `createConfig` orchestrator for `@suaveplan/config`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`createConfig` is the primary entry point to `@suaveplan/config`. It accepts
|
||||
a list of `ConfigLayer` objects, resolves each in turn, sorts them by priority,
|
||||
deep-merges the results, applies any per-environment overrides, runs the
|
||||
caller-supplied validator, and returns a live `ConfigStore`. The live store
|
||||
supports `reload()` to re-run the full pipeline and `subscribe()` to receive
|
||||
change notifications after each successful reload. All lifecycle events are
|
||||
traced via OTel spans and optionally logged via the injected `LoggerLike`.
|
||||
|
||||
## Features
|
||||
|
||||
- Sorts layers ascending by `priority` before merging
|
||||
- Wraps each layer's `resolve()` in a try/catch and rethrows as
|
||||
`ConfigError(CONFIG.PARSE_FAILED)`
|
||||
- Applies `environments[environment]` override after merging (if present)
|
||||
- Calls `options.validate` if provided; wraps throws as
|
||||
`ConfigError(CONFIG.VALIDATION_FAILED)`
|
||||
- Returns a live store with `reload(signal?)` and `subscribe(listener)`
|
||||
- `reload` re-runs full pipeline; notifies all subscribers on success
|
||||
- `subscribe` returns an unsubscribe function
|
||||
- OTel span `config.create` and counter `config.get_total` / `config.reload_total`
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { createConfig, defaultsLayer, envLayer } from '@suaveplan/config';
|
||||
|
||||
const store = createConfig({
|
||||
layers: [
|
||||
defaultsLayer({ port: 3000, debug: false }),
|
||||
envLayer({ mapping: { PORT: 'port', DEBUG: 'debug' } }),
|
||||
],
|
||||
});
|
||||
|
||||
store.get('port'); // value from env or 3000
|
||||
store.get('debug'); // true/false from env or false
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { createConfig, defaultsLayer, fileLayer, envLayer, cliLayer } from '@suaveplan/config';
|
||||
import z from 'zod';
|
||||
|
||||
const Schema = z.object({
|
||||
port: z.number().int().min(1).max(65535),
|
||||
debug: z.boolean(),
|
||||
db: z.object({ url: z.string().url(), pool: z.number().int().positive() }),
|
||||
});
|
||||
type AppConfig = z.infer<typeof Schema>;
|
||||
|
||||
const store = createConfig<AppConfig>({
|
||||
layers: [
|
||||
defaultsLayer({ port: 3000, debug: false, db: { url: 'postgres://localhost/app', pool: 5 } }),
|
||||
fileLayer({ content: JSON.parse(readFileSync('./config.json', 'utf-8')) }),
|
||||
envLayer({ prefix: 'APP_', mapping: { PORT: 'port', DB_URL: 'db.url' } }),
|
||||
cliLayer({ args: process.argv.slice(2), mapping: { port: 'port', debug: 'debug' } }),
|
||||
],
|
||||
validate: (raw) => Schema.parse(raw),
|
||||
sensitive: ['db.url'],
|
||||
environments: { production: { debug: false }, development: { debug: true } },
|
||||
environment: process.env.NODE_ENV ?? 'development',
|
||||
});
|
||||
|
||||
// Live reload on SIGHUP
|
||||
process.on('SIGHUP', () => store.reload());
|
||||
|
||||
// Subscribe to config changes
|
||||
const unsub = store.subscribe((next) => {
|
||||
console.log('Config updated, new port:', next.port);
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `createConfig<T extends ConfigData>(options: CreateConfigOptions<T>): ConfigStore<T>`
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `options.layers` | `ConfigLayer[]` | Layers to resolve; sorted by priority |
|
||||
| `options.validate` | `ConfigValidator<T>` | Optional post-merge validator |
|
||||
| `options.sensitive` | `string[]` | Paths to mask in `toMasked()` |
|
||||
| `options.environments` | `EnvironmentOverrides<T>` | Per-env overrides |
|
||||
| `options.environment` | `string` | Active environment name |
|
||||
| `options.logger` | `LoggerLike` | Optional structured logger |
|
||||
|
||||
**Returns** a live `ConfigStore<T>` with real `reload` and `subscribe`.
|
||||
|
||||
**Throws:**
|
||||
|
||||
- `ConfigError(CONFIG.PARSE_FAILED)` — a layer's `resolve()` threw
|
||||
- `ConfigError(CONFIG.VALIDATION_FAILED)` — the validator threw
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Layer resolution is synchronous — all layer `resolve()` methods must be
|
||||
synchronous. The `reload()` method is async (returns a `Promise<void>`) to
|
||||
accommodate future async layer support and to allow AbortSignal handling.
|
||||
The store's internal `frozenData` reference is atomically swapped after a
|
||||
successful reload; subscribers see only fully validated data. Partial failures
|
||||
(e.g. one layer throws during reload) leave the store at the last good state
|
||||
and propagate the error to the `reload()` caller.
|
||||
@@ -0,0 +1,114 @@
|
||||
# config-store — Immutable config store factory for `@suaveplan/config`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`createConfigStore` constructs a static, immutable view of a pre-resolved
|
||||
config object. It deep-freezes the data (including all nested objects) so that
|
||||
accidental runtime mutations are caught in strict mode. The store provides
|
||||
`get`, `has`, `toJSON`, and `toMasked` methods for reading config values, plus
|
||||
no-op stubs for `reload` and `subscribe` that are replaced with live
|
||||
implementations by `createConfig` when a full config pipeline is constructed.
|
||||
|
||||
## Features
|
||||
|
||||
- `structuredClone` + `deepFreeze` on construction — data is immutable
|
||||
- `get(key)` — dot-notation path traversal via `getByPath`
|
||||
- `has(key)` — presence check via `hasByPath`
|
||||
- `toJSON()` — returns the frozen data reference
|
||||
- `toMasked()` — returns a frozen deep clone with sensitive paths replaced
|
||||
- `reload()` — no-op returning `Promise.resolve()` (stub for static stores)
|
||||
- `subscribe()` — no-op returning a no-op unsubscribe (stub for static stores)
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { createConfigStore } from '@suaveplan/config/core/config-store/config-store';
|
||||
|
||||
const store = createConfigStore({
|
||||
data: { server: { port: 3000 }, db: { password: 'secret' } },
|
||||
sensitive: ['db.password'],
|
||||
});
|
||||
|
||||
store.get('server.port'); // 3000
|
||||
store.has('server.port'); // true
|
||||
store.has('missing.key'); // false
|
||||
store.toMasked();
|
||||
// { server: { port: 3000 }, db: { password: '****' } }
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { createConfigStore } from '@suaveplan/config/core/config-store/config-store';
|
||||
|
||||
interface AppConfig {
|
||||
server: { port: number; host: string };
|
||||
db: { url: string; pool: number; password: string };
|
||||
log: { level: string };
|
||||
}
|
||||
|
||||
const store = createConfigStore<AppConfig>({
|
||||
data: {
|
||||
server: { port: 4000, host: '0.0.0.0' },
|
||||
db: { url: 'postgres://...', pool: 10, password: 'hunter2' },
|
||||
log: { level: 'info' },
|
||||
},
|
||||
sensitive: ['db.password', 'db.url'],
|
||||
});
|
||||
|
||||
// JSON-serialise the full config (includes sensitive values — do not log):
|
||||
JSON.stringify(store.toJSON());
|
||||
|
||||
// Safe for logging — sensitive paths are masked:
|
||||
console.log(store.toMasked());
|
||||
// { server: ..., db: { url: '****', pool: 10, password: '****' }, log: ... }
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `createConfigStore<T extends ConfigData>(options: ConfigStoreOptions<T>): ConfigStore<T>`
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `options.data` | `T` | Resolved config data |
|
||||
| `options.sensitive` | `string[]` | Dot-notation paths to mask in `toMasked()` |
|
||||
|
||||
**Returns** a `ConfigStore<T>` with all methods implemented.
|
||||
|
||||
#### `store.get<K extends string>(key: K): unknown`
|
||||
|
||||
Returns the value at the dot-notation `key` path, or `undefined` if not found.
|
||||
|
||||
#### `store.has(key: string): boolean`
|
||||
|
||||
Returns `true` if the dot-notation path exists and is not `undefined`.
|
||||
|
||||
#### `store.toJSON(): Readonly<T>`
|
||||
|
||||
Returns the frozen config data reference. This is the full unmasked data; do
|
||||
not log this value if it contains sensitive fields.
|
||||
|
||||
#### `store.toMasked(): Readonly<T>`
|
||||
|
||||
Returns a frozen deep clone with all `sensitive` paths replaced by `"****"`.
|
||||
Safe for logging and telemetry.
|
||||
|
||||
#### `store.reload(signal?: AbortSignal): Promise<void>`
|
||||
|
||||
No-op stub. Returns `Promise.resolve()`. Live reload is provided by `createConfig`.
|
||||
|
||||
#### `store.subscribe(listener): () => void`
|
||||
|
||||
No-op stub. Returns a no-op unsubscribe function. Live subscriptions are
|
||||
provided by `createConfig`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`deepFreeze` is applied recursively after `structuredClone` to prevent both
|
||||
shallow and deep mutations. The frozen data object is returned directly from
|
||||
`toJSON()` (no additional clone) for performance — callers who need a mutable
|
||||
copy must clone it themselves. `toMasked()` calls `structuredClone` internally
|
||||
via `maskSensitive`, producing a new mutable copy before re-freezing it.
|
||||
101
docs/foundation/core/config/src/layers/cli-layer/cli-layer.md
Normal file
101
docs/foundation/core/config/src/layers/cli-layer/cli-layer.md
Normal file
@@ -0,0 +1,101 @@
|
||||
# cli-layer — CLI argument config layer for `@suaveplan/config`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`cliLayer` creates a config layer that reads values from command-line arguments
|
||||
and maps them to dot-notation config paths. It is assigned the highest built-in
|
||||
priority (`CLI = 40`) so that operator-supplied flags always win over defaults,
|
||||
file content, environment variables, and programmatic object overrides. The
|
||||
companion `parseArgs` function handles the full range of common CLI flag forms
|
||||
and is exported for standalone use in argument-parsing utilities.
|
||||
|
||||
## Features
|
||||
|
||||
- Priority `40` (`LAYER_PRIORITIES.CLI`) — overrides all other built-in layers
|
||||
- `parseArgs` handles `--key=value`, `--key value`, `--flag` (boolean), `-k value`
|
||||
- Unknown flags not in `mapping` are silently ignored
|
||||
- Values are coerced via `coerceValue` — numerics, booleans, and JSON objects
|
||||
are automatically converted from their string representations
|
||||
- Writes to arbitrary dot-notation paths via `setByPath`
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { cliLayer, createConfig } from '@suaveplan/config';
|
||||
|
||||
const store = createConfig({
|
||||
layers: [
|
||||
cliLayer({
|
||||
args: process.argv.slice(2),
|
||||
mapping: { port: 'server.port', debug: 'log.debug' },
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
// When invoked as: node app.js --port 9000 --debug
|
||||
store.get('server.port'); // 9000 (number)
|
||||
store.get('log.debug'); // true (boolean)
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { parseArgs } from '@suaveplan/config';
|
||||
|
||||
// Use parseArgs standalone to inspect raw flag values before coercion:
|
||||
const raw = parseArgs(
|
||||
['--host=prod.example.com', '--port', '443', '--tls'],
|
||||
{ host: 'server.host', port: 'server.port', tls: 'server.tls', p: 'server.port' }
|
||||
);
|
||||
// raw = { host: 'prod.example.com', port: '443', tls: 'true' }
|
||||
|
||||
// Short flags:
|
||||
const short = parseArgs(['-p', '8080'], { p: 'server.port' });
|
||||
// short = { p: '8080' }
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `cliLayer(options: CliLayerOptions): ConfigLayer`
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `options.args` | `string[]` | Raw argv array (e.g. `process.argv.slice(2)`) |
|
||||
| `options.mapping` | `Record<string, string>` | Flag name → config path map |
|
||||
|
||||
**Returns** a `ConfigLayer` with:
|
||||
|
||||
- `name`: `"cli"`
|
||||
- `priority`: `40` (`LAYER_PRIORITIES.CLI`)
|
||||
- `resolve()`: parses `args`, coerces values, writes to config paths
|
||||
|
||||
### `parseArgs(args: string[], mapping: Record<string, string>): Record<string, string>`
|
||||
|
||||
Parses raw argv tokens into a flat `{ flagName: rawStringValue }` record.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `args` | `string[]` | Raw argument tokens |
|
||||
| `mapping` | `Record<string, string>` | Allowed flag names (without prefix) |
|
||||
|
||||
**Returns** `Record<string, string>` — only flags present in `mapping` are included.
|
||||
|
||||
**Supported forms:**
|
||||
|
||||
| Form | Example | Key | Value |
|
||||
|------|---------|-----|-------|
|
||||
| `--key=value` | `--port=4000` | `port` | `"4000"` |
|
||||
| `--key value` | `--port 4000` | `port` | `"4000"` |
|
||||
| `--flag` | `--debug` | `debug` | `"true"` |
|
||||
| `-k value` | `-p 4000` | `p` | `"4000"` |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Short flags (`-k`) are limited to single-character names. Multi-character short
|
||||
options (e.g. `-vv`) are not supported and are silently skipped. The parser
|
||||
does not validate flag values — coercion happens later in `cliLayer.resolve()`
|
||||
via `coerceValue`. Boolean flags are mapped to the string `"true"` so they
|
||||
flow through the coercion pipeline consistently.
|
||||
@@ -0,0 +1,91 @@
|
||||
# defaults-layer — Static default values layer for `@suaveplan/config`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`defaultsLayer` creates a configuration layer that supplies baseline default
|
||||
values for every key in the config schema. It is assigned the lowest possible
|
||||
priority (`LAYER_PRIORITIES.DEFAULTS = 0`) so that every other layer in the
|
||||
stack — file, object, env, or CLI — can override it without any special
|
||||
configuration. This makes it the natural first layer to register when building
|
||||
a `createConfig` call: supply all your application defaults here, and trust that
|
||||
higher-priority layers will selectively override them.
|
||||
|
||||
## Features
|
||||
|
||||
- Priority `0` — always the lowest; all other built-in layers override it
|
||||
- Returns a shallow copy of the provided defaults on `resolve()` — external
|
||||
mutations to the original object do not affect subsequent resolutions
|
||||
- Generic `<T extends ConfigData>` so TypeScript can infer the shape
|
||||
- Named `"defaults"` for clear identification in error messages and logs
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { defaultsLayer, createConfig } from '@suaveplan/config';
|
||||
|
||||
interface AppConfig {
|
||||
port: number;
|
||||
debug: boolean;
|
||||
logLevel: string;
|
||||
}
|
||||
|
||||
const store = createConfig<AppConfig>({
|
||||
layers: [
|
||||
defaultsLayer<AppConfig>({ port: 3000, debug: false, logLevel: 'info' }),
|
||||
],
|
||||
});
|
||||
|
||||
store.get('port'); // 3000
|
||||
store.get('debug'); // false
|
||||
store.get('logLevel'); // 'info'
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { defaultsLayer, envLayer, cliLayer, createConfig } from '@suaveplan/config';
|
||||
|
||||
const defaults = defaultsLayer({
|
||||
server: { port: 3000, host: '0.0.0.0' },
|
||||
db: { pool: 5, timeout: 30_000 },
|
||||
log: { level: 'info', pretty: true },
|
||||
});
|
||||
|
||||
// Override only what differs per environment:
|
||||
const env = envLayer({
|
||||
mapping: { PORT: 'server.port', DB_POOL: 'db.pool', LOG_LEVEL: 'log.level' },
|
||||
});
|
||||
|
||||
const cli = cliLayer({
|
||||
args: process.argv.slice(2),
|
||||
mapping: { port: 'server.port', debug: 'log.pretty' },
|
||||
});
|
||||
|
||||
const store = createConfig({ layers: [defaults, env, cli] });
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `defaultsLayer<T extends ConfigData>(defaults: T): ConfigLayer`
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `defaults` | `T` | Plain object containing default config values |
|
||||
|
||||
**Returns** a `ConfigLayer` with:
|
||||
|
||||
- `name`: `"defaults"`
|
||||
- `priority`: `0` (`LAYER_PRIORITIES.DEFAULTS`)
|
||||
- `resolve()`: returns `{ ...defaults }` (shallow copy)
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The shallow copy returned by `resolve()` prevents the caller from mutating
|
||||
the internal defaults reference across successive `reload()` calls. For deeply
|
||||
nested mutable objects, callers should pass plain JSON-serialisable data to
|
||||
guarantee immutability. The function is intentionally minimal — no async
|
||||
resolution, no file I/O, no validation — because defaults are static values
|
||||
known at build time.
|
||||
107
docs/foundation/core/config/src/layers/env-layer/env-layer.md
Normal file
107
docs/foundation/core/config/src/layers/env-layer/env-layer.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# env-layer — Environment variable config layer for `@suaveplan/config`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`envLayer` creates a config layer that reads environment variables and maps
|
||||
them to dot-notation config paths. It handles prefix stripping, type coercion
|
||||
via `coerceValue`, and missing-variable safety (keys absent from the environment
|
||||
source are simply excluded from the resolved data). With priority
|
||||
`LAYER_PRIORITIES.ENV = 30`, it overrides defaults, file, and object layers but
|
||||
is itself overridden by the CLI layer at priority 40.
|
||||
|
||||
## Features
|
||||
|
||||
- Maps env-var names to arbitrary dot-notation config paths via `mapping`
|
||||
- Optional `prefix` prepended to each env-var name before lookup
|
||||
- `source` override for testing without polluting `process.env`
|
||||
- `coerceValue` exported for standalone use — converts string tokens to the
|
||||
most specific JS type (boolean, number, parsed JSON, or string)
|
||||
- Missing or empty env vars are silently skipped (no error thrown)
|
||||
- Priority `30` — overrides defaults (0), file (10), and object (20) layers
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { envLayer, createConfig } from '@suaveplan/config';
|
||||
|
||||
const store = createConfig({
|
||||
layers: [
|
||||
envLayer({
|
||||
mapping: { PORT: 'server.port', DEBUG: 'log.debug' },
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
// If PORT=4000 and DEBUG=true are set:
|
||||
store.get('server.port'); // 4000 (number, not string)
|
||||
store.get('log.debug'); // true (boolean)
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { envLayer, defaultsLayer, createConfig } from '@suaveplan/config';
|
||||
|
||||
// With prefix — all env vars are prefixed with "APP_"
|
||||
const store = createConfig({
|
||||
layers: [
|
||||
defaultsLayer({ server: { port: 3000 }, db: { pool: 5 } }),
|
||||
envLayer({
|
||||
prefix: 'APP_',
|
||||
mapping: {
|
||||
PORT: 'server.port',
|
||||
DB_POOL: 'db.pool',
|
||||
DB_URL: 'db.url',
|
||||
},
|
||||
// Can override source for testing:
|
||||
source: { APP_PORT: '8080', APP_DB_POOL: '20', APP_DB_URL: '{"host":"db"}' },
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
store.get('server.port'); // 8080 (number)
|
||||
store.get('db.pool'); // 20 (number)
|
||||
store.get('db.url'); // { host: 'db' } (parsed JSON)
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `envLayer(options: EnvLayerOptions): ConfigLayer`
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `options.mapping` | `Record<string, string>` | Env-var name → config path map |
|
||||
| `options.prefix` | `string` | Optional prefix prepended to each env-var name |
|
||||
| `options.source` | `Record<string, string \| undefined>` | Override for `process.env` |
|
||||
| `options.watchBus` | `EventBusLike` | Optional watch bus (reserved for future use) |
|
||||
| `options.watchEvent` | `string` | Watch event name; default `'config:env:changed'` |
|
||||
|
||||
**Returns** a `ConfigLayer` with:
|
||||
|
||||
- `name`: `"env"`
|
||||
- `priority`: `30` (`LAYER_PRIORITIES.ENV`)
|
||||
|
||||
### `coerceValue(raw: string): unknown`
|
||||
|
||||
Converts a raw environment-variable string to the most specific JavaScript type.
|
||||
|
||||
| Input | Output |
|
||||
|-------|--------|
|
||||
| `""` | `""` (empty string returned as-is) |
|
||||
| `"true"`, `"1"`, `"yes"`, `"on"`, `"enabled"` | `true` |
|
||||
| `"false"`, `"0"`, `"no"`, `"off"`, `"disabled"` | `false` |
|
||||
| Finite numeric string (`"42"`, `"3.14"`) | `number` |
|
||||
| JSON object/array string (`'{"a":1}'`) | parsed value |
|
||||
| Anything else | original `string` |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Coercion is case-insensitive for boolean tokens. Numeric coercion uses
|
||||
`Number()` + `isFinite` guard to avoid accepting `Infinity` or `NaN`.
|
||||
JSON parsing is attempted only when the trimmed string starts with `{` or `[`
|
||||
to avoid unnecessary `try/catch` overhead on every string value. The `source`
|
||||
parameter defaults to `process.env` in Node/Bun environments and falls back to
|
||||
`{}` in browser environments where `process` is not defined.
|
||||
@@ -0,0 +1,94 @@
|
||||
# file-layer — Pre-parsed file content config layer for `@suaveplan/config`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`fileLayer` creates a config layer that serves pre-parsed configuration data
|
||||
read from a file. The layer itself does not perform file I/O — that is the
|
||||
caller's responsibility. This separation keeps the layer synchronous and
|
||||
testable without file-system access. When an optional `watchBus` event bus is
|
||||
provided, the layer subscribes to a configurable event name and triggers a
|
||||
parent-store reload when the event fires, enabling live config reloading
|
||||
without polling.
|
||||
|
||||
## Features
|
||||
|
||||
- Priority `10` (`LAYER_PRIORITIES.FILE`) — above defaults, below object/env/CLI
|
||||
- Serves `structuredClone`-safe shallow copy of provided `content` on `resolve()`
|
||||
- Optional watch mode via `EventBusLike` + configurable event name
|
||||
- `teardown()` method unsubscribes the watch listener to prevent memory leaks
|
||||
- File I/O is the caller's responsibility; this layer is format-agnostic
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { fileLayer, createConfig } from '@suaveplan/config';
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
const content = JSON.parse(readFileSync('./config.json', 'utf-8'));
|
||||
|
||||
const store = createConfig({
|
||||
layers: [fileLayer({ content })],
|
||||
});
|
||||
|
||||
store.get('server.port'); // value from config.json
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { fileLayer, defaultsLayer, createConfig } from '@suaveplan/config';
|
||||
import { EventBus } from '@suaveplan/events';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { watch } from 'node:fs';
|
||||
|
||||
let content = JSON.parse(readFileSync('./app.json', 'utf-8'));
|
||||
const watchBus = new EventBus();
|
||||
|
||||
// Re-parse the file when it changes and signal the bus
|
||||
watch('./app.json', () => {
|
||||
content = JSON.parse(readFileSync('./app.json', 'utf-8'));
|
||||
watchBus.emit('config:file:changed', {});
|
||||
});
|
||||
|
||||
const fileL = fileLayer({
|
||||
content,
|
||||
watchBus,
|
||||
watchEvent: 'config:file:changed',
|
||||
});
|
||||
|
||||
const store = createConfig({
|
||||
layers: [defaultsLayer({ debug: false }), fileL],
|
||||
});
|
||||
|
||||
// Reloads automatically when app.json changes
|
||||
await store.reload();
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `fileLayer(options: FileLayerOptions): ConfigLayer`
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `options.content` | `ConfigData` | Pre-parsed config object |
|
||||
| `options.watchBus` | `EventBusLike` | Optional event bus for watch mode |
|
||||
| `options.watchEvent` | `string` | Event name to listen for; default `'config:file:changed'` |
|
||||
|
||||
**Returns** a `ConfigLayer` with:
|
||||
|
||||
- `name`: `"file"`
|
||||
- `priority`: `10` (`LAYER_PRIORITIES.FILE`)
|
||||
- `resolve()`: returns `{ ...options.content }` (shallow copy)
|
||||
- `teardown()`: unsubscribes from the watch bus if one was provided
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The watch mechanism is intentionally passive — the layer subscribes to an
|
||||
event but does not read files itself. The event payload is ignored; only its
|
||||
arrival matters. The parent `createConfig` live store injects a `_onReload`
|
||||
callback into the layer after construction to close the loop. If `teardown()`
|
||||
is not called, the subscription remains active; callers responsible for
|
||||
application shutdown should ensure all layer teardowns are invoked.
|
||||
@@ -0,0 +1,83 @@
|
||||
# object-layer — In-memory config object layer for `@suaveplan/config`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`objectLayer` wraps a plain JavaScript object as a named config layer, making it
|
||||
easy to inject programmatically constructed configuration into the `createConfig`
|
||||
pipeline. It is the simplest way to supply test overrides, feature-flag-driven
|
||||
values, or dynamically computed config blobs without resorting to the full
|
||||
`fileLayer` or `envLayer` machinery. The default priority (`OBJECT = 20`) places
|
||||
it above the file layer and defaults layer but below environment variable and CLI
|
||||
layers.
|
||||
|
||||
## Features
|
||||
|
||||
- Default priority `20` (`LAYER_PRIORITIES.OBJECT`) — overrides file and defaults
|
||||
- Configurable priority via the optional second argument
|
||||
- Returns a shallow copy on `resolve()` for mutation safety
|
||||
- Named `"object"` for clear identification in logs and error messages
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { objectLayer, createConfig } from '@suaveplan/config';
|
||||
|
||||
const store = createConfig({
|
||||
layers: [
|
||||
objectLayer({ port: 4000, debug: true }),
|
||||
],
|
||||
});
|
||||
|
||||
store.get('port'); // 4000
|
||||
store.get('debug'); // true
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { defaultsLayer, objectLayer, envLayer, createConfig } from '@suaveplan/config';
|
||||
import { LAYER_PRIORITIES } from '@suaveplan/config/constants';
|
||||
|
||||
// Two object layers at different priorities:
|
||||
const baseObject = objectLayer({ db: { pool: 5, timeout: 30_000 } });
|
||||
const testOverride = objectLayer(
|
||||
{ db: { pool: 1 } },
|
||||
LAYER_PRIORITIES.ENV + 5 // higher than ENV, used in tests
|
||||
);
|
||||
|
||||
const store = createConfig({
|
||||
layers: [
|
||||
defaultsLayer({ port: 3000 }),
|
||||
baseObject,
|
||||
testOverride,
|
||||
],
|
||||
});
|
||||
|
||||
store.get('db.pool'); // 1 (test override wins)
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `objectLayer(data: ConfigData, priority?: number): ConfigLayer`
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `data` | `ConfigData` | Plain object to expose as config values |
|
||||
| `priority` | `number` | Optional priority; defaults to `LAYER_PRIORITIES.OBJECT` (20) |
|
||||
|
||||
**Returns** a `ConfigLayer` with:
|
||||
|
||||
- `name`: `"object"`
|
||||
- `priority`: provided or `20`
|
||||
- `resolve()`: returns `{ ...data }` (shallow copy)
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The shallow copy in `resolve()` means that top-level key mutations to the
|
||||
returned object do not affect subsequent resolutions. Callers who supply deeply
|
||||
nested mutable objects should be aware that nested references are not deep-cloned.
|
||||
If immutability at all depths is required, pass `structuredClone(data)` as the
|
||||
argument. The layer carries no teardown logic since it holds no external resources.
|
||||
181
docs/foundation/core/config/src/types/common.md
Normal file
181
docs/foundation/core/config/src/types/common.md
Normal file
@@ -0,0 +1,181 @@
|
||||
# common — Public TypeScript types for `@suaveplan/config`
|
||||
|
||||
## Purpose
|
||||
|
||||
This module defines every public TypeScript interface and type alias used across
|
||||
the `@suaveplan/config` package. It is the single source of truth for the
|
||||
package's public API surface, ensuring that all layer implementations, the
|
||||
config builder, and the config store agree on the same type signatures. By
|
||||
keeping types in a dedicated file, downstream consumers can import only the
|
||||
types they need (zero runtime cost) while benefiting from full IntelliSense
|
||||
support and strict TypeScript narrowing.
|
||||
|
||||
## Features
|
||||
|
||||
- `ConfigData` — base record type for all config objects
|
||||
- `ConfigLayer` — contract every layer implementation must satisfy, with optional
|
||||
`teardown` hook for resource cleanup
|
||||
- `ConfigValidator<T>` — typed validation/transformation callback
|
||||
- `EnvironmentOverrides<T>` — per-environment partial override map
|
||||
- `CreateConfigOptions<T>` — full orchestrator options including `logger` injection
|
||||
- `ConfigStore<T>` — immutable runtime view with `get`, `has`, `toJSON`, `toMasked`,
|
||||
`reload`, and `subscribe`
|
||||
- `ConfigStoreOptions<T>` — low-level static store factory options
|
||||
- `EnvLayerOptions` — env-var source, prefix, mapping, and optional watch bus
|
||||
- `CliLayerOptions` — argv array and flag-to-path mapping
|
||||
- `FileLayerOptions` — pre-parsed content and optional watch bus
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import type { ConfigData, ConfigLayer, ConfigStore } from '@suaveplan/config/types/common';
|
||||
|
||||
interface AppConfig extends ConfigData {
|
||||
port: number;
|
||||
debug: boolean;
|
||||
}
|
||||
|
||||
const layer: ConfigLayer = {
|
||||
name: 'app-defaults',
|
||||
priority: 0,
|
||||
resolve: () => ({ port: 3000, debug: false }),
|
||||
};
|
||||
|
||||
function display(store: ConfigStore<AppConfig>): void {
|
||||
const port = store.get('port');
|
||||
console.log(`Listening on port ${port}`);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import type {
|
||||
CreateConfigOptions,
|
||||
ConfigStore,
|
||||
ConfigData,
|
||||
ConfigValidator,
|
||||
EnvironmentOverrides,
|
||||
} from '@suaveplan/config/types/common';
|
||||
import z from 'zod';
|
||||
|
||||
const Schema = z.object({ port: z.number(), debug: z.boolean() });
|
||||
type AppConfig = z.infer<typeof Schema> & ConfigData;
|
||||
|
||||
const validate: ConfigValidator<AppConfig> = (raw) => Schema.parse(raw);
|
||||
|
||||
const overrides: EnvironmentOverrides<AppConfig> = {
|
||||
production: { debug: false },
|
||||
development: { debug: true },
|
||||
};
|
||||
|
||||
const opts: CreateConfigOptions<AppConfig> = {
|
||||
layers: [],
|
||||
validate,
|
||||
sensitive: ['db.password'],
|
||||
environments: overrides,
|
||||
environment: process.env.NODE_ENV ?? 'development',
|
||||
logger: console,
|
||||
};
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `ConfigData`
|
||||
|
||||
```ts
|
||||
type ConfigData = Record<string, unknown>
|
||||
```
|
||||
|
||||
Base type for all config data objects. Allows arbitrary string keys and any
|
||||
value type, including nested objects.
|
||||
|
||||
### `ConfigLayer`
|
||||
|
||||
```ts
|
||||
interface ConfigLayer {
|
||||
readonly name: string;
|
||||
readonly priority: number;
|
||||
resolve(): ConfigData;
|
||||
teardown?(): void;
|
||||
}
|
||||
```
|
||||
|
||||
The contract every config layer must implement. `teardown` is optional and is
|
||||
called when the parent store is torn down (e.g. to unsubscribe from an event bus).
|
||||
|
||||
### `ConfigValidator<T>`
|
||||
|
||||
```ts
|
||||
type ConfigValidator<T extends ConfigData = ConfigData> =
|
||||
(data: ConfigData) => T
|
||||
```
|
||||
|
||||
A synchronous function that validates and transforms raw merged config data. Must
|
||||
throw on invalid input; the error is caught and wrapped as `CONFIG.VALIDATION_FAILED`.
|
||||
|
||||
### `EnvironmentOverrides<T>`
|
||||
|
||||
```ts
|
||||
type EnvironmentOverrides<T extends ConfigData = ConfigData> =
|
||||
Record<string, Partial<T>>
|
||||
```
|
||||
|
||||
Maps environment names to partial config overrides. Applied on top of the merged
|
||||
layer data when the active `environment` key matches.
|
||||
|
||||
### `CreateConfigOptions<T>`
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `layers` | `ConfigLayer[]` | Yes | Layers to resolve and merge |
|
||||
| `validate` | `ConfigValidator<T>` | No | Post-merge validator |
|
||||
| `sensitive` | `string[]` | No | Paths to mask in `toMasked()` |
|
||||
| `environments` | `EnvironmentOverrides<T>` | No | Per-env overrides |
|
||||
| `environment` | `string` | No | Active environment name |
|
||||
| `logger` | `LoggerLike` | No | Structured logger for lifecycle events |
|
||||
|
||||
### `ConfigStore<T>`
|
||||
|
||||
| Method | Returns | Description |
|
||||
|--------|---------|-------------|
|
||||
| `get(key)` | `unknown` | Value at dot-notation path, or `undefined` |
|
||||
| `has(key)` | `boolean` | `true` if path exists |
|
||||
| `toJSON()` | `Readonly<T>` | Frozen resolved config |
|
||||
| `toMasked()` | `Readonly<T>` | Config with sensitive paths replaced |
|
||||
| `reload(signal?)` | `Promise<void>` | Re-resolves all layers |
|
||||
| `subscribe(fn)` | `() => void` | Listen for reload events; returns unsubscribe |
|
||||
|
||||
### `EnvLayerOptions`
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `mapping` | `Record<string, string>` | Yes | Env-var name → config path |
|
||||
| `prefix` | `string` | No | Prefix stripped before mapping |
|
||||
| `source` | `Record<string, string \| undefined>` | No | Override for `process.env` |
|
||||
| `watchBus` | `EventBusLike` | No | Event bus for watch-mode |
|
||||
| `watchEvent` | `string` | No | Event name (default: `'config:env:changed'`) |
|
||||
|
||||
### `CliLayerOptions`
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `args` | `string[]` | Yes | Raw argv (e.g. `process.argv.slice(2)`) |
|
||||
| `mapping` | `Record<string, string>` | Yes | Flag name → config path |
|
||||
|
||||
### `FileLayerOptions`
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `content` | `ConfigData` | Yes | Pre-parsed file content |
|
||||
| `watchBus` | `EventBusLike` | No | Event bus for watch-mode |
|
||||
| `watchEvent` | `string` | No | Event name (default: `'config:file:changed'`) |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
All types in this module are pure TypeScript interfaces and type aliases — they
|
||||
produce zero runtime output. The `ConfigLayer.teardown` optional method was added
|
||||
to support watch-mode layers that hold event-bus subscriptions. The
|
||||
`ConfigStore.reload` and `ConfigStore.subscribe` methods are optional on the
|
||||
interface for backward compatibility but are always present on concrete stores
|
||||
returned by `createConfig`.
|
||||
110
docs/foundation/core/config/src/types/schemas.md
Normal file
110
docs/foundation/core/config/src/types/schemas.md
Normal file
@@ -0,0 +1,110 @@
|
||||
# schemas — Zod option schemas for `@suaveplan/config`
|
||||
|
||||
## Purpose
|
||||
|
||||
This module exposes Zod schemas for every public options interface in
|
||||
`@suaveplan/config`. Using these schemas, callers can validate externally
|
||||
sourced configuration objects at runtime — for instance, when reading layer
|
||||
options from a database row, an HTTP request body, or an untrusted JSON file.
|
||||
The schemas complement the TypeScript interfaces in `types/common.ts` by adding
|
||||
runtime validation without duplicating the structural definitions.
|
||||
|
||||
## Features
|
||||
|
||||
- `EnvLayerOptionsSchema` — validates `EnvLayerOptions` (mapping required, prefix optional)
|
||||
- `CliLayerOptionsSchema` — validates `CliLayerOptions` (args array + mapping required)
|
||||
- `FileLayerOptionsSchema` — validates `FileLayerOptions` (content record required)
|
||||
- `ConfigStoreOptionsSchema` — validates `ConfigStoreOptions` (data + sensitive array)
|
||||
- `CreateConfigOptionsSchema` — validates the shape-checkable subset of `CreateConfigOptions`
|
||||
(functions and loggers are excluded, layer structural fields only)
|
||||
- Inferred companion types (`EnvLayerOptionsInput`, etc.) for consumers that want
|
||||
the Zod-inferred type rather than the hand-written TS interface
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { EnvLayerOptionsSchema } from '@suaveplan/config/types/schemas';
|
||||
|
||||
const raw = JSON.parse(process.env.ENV_LAYER_CONFIG ?? '{}');
|
||||
const opts = EnvLayerOptionsSchema.parse(raw);
|
||||
// opts is now typed as EnvLayerOptionsInput
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { CreateConfigOptionsSchema } from '@suaveplan/config/types/schemas';
|
||||
import z from 'zod';
|
||||
|
||||
// Validate only the serialisable portion of options coming from a config API.
|
||||
function parseRemoteConfigOptions(body: unknown) {
|
||||
const result = CreateConfigOptionsSchema.safeParse(body);
|
||||
if (!result.success) {
|
||||
throw new Error(`Invalid config options: ${result.error.message}`);
|
||||
}
|
||||
return result.data;
|
||||
}
|
||||
|
||||
// Then merge the validated shape with local layer/validate functions before
|
||||
// passing to createConfig.
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `EnvLayerOptionsSchema`
|
||||
|
||||
Validates `{ mapping: Record<string, string>, prefix?: string, source?: Record<string, string|undefined>, watchEvent?: string }`.
|
||||
|
||||
- `mapping` — required; record of env-var names to dot-notation config paths
|
||||
- `prefix` — optional string stripped from env-var names before mapping
|
||||
- `source` — optional environment source override
|
||||
- `watchEvent` — optional event name for watch-mode
|
||||
|
||||
**Inferred type**: `EnvLayerOptionsInput`
|
||||
|
||||
### `CliLayerOptionsSchema`
|
||||
|
||||
Validates `{ args: string[], mapping: Record<string, string> }`.
|
||||
|
||||
- `args` — required array of raw CLI arguments
|
||||
- `mapping` — required; record of flag names to config paths
|
||||
|
||||
**Inferred type**: `CliLayerOptionsInput`
|
||||
|
||||
### `FileLayerOptionsSchema`
|
||||
|
||||
Validates `{ content: Record<string, unknown>, watchEvent?: string }`.
|
||||
|
||||
- `content` — required; pre-parsed file content
|
||||
- `watchEvent` — optional event name for watch-mode
|
||||
|
||||
**Inferred type**: `FileLayerOptionsInput`
|
||||
|
||||
### `ConfigStoreOptionsSchema`
|
||||
|
||||
Validates `{ data: Record<string, unknown>, sensitive: string[] }`.
|
||||
|
||||
- `data` — required; the resolved config data
|
||||
- `sensitive` — required; list of dot-notation paths to mask
|
||||
|
||||
**Inferred type**: `ConfigStoreOptionsInput`
|
||||
|
||||
### `CreateConfigOptionsSchema`
|
||||
|
||||
Validates the serialisable subset of `CreateConfigOptions`:
|
||||
`{ layers: Array<{ name: string, priority: number }>, sensitive?: string[], environments?: Record<string, Record<string, unknown>>, environment?: string }`.
|
||||
|
||||
Functions (`validate`, `logger`) and layer `resolve` methods are intentionally
|
||||
excluded — they cannot be serialised to JSON or validated with Zod.
|
||||
|
||||
**Inferred type**: `CreateConfigOptionsInput`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The `CreateConfigOptionsSchema` deliberately omits the `validate` callback and
|
||||
`logger` fields from the Zod schema. These are functions and cannot be round-
|
||||
tripped through a serialisation boundary. Callers who build `CreateConfigOptions`
|
||||
programmatically should merge the validated structural fields with their local
|
||||
function references before calling `createConfig`. The `watchBus` fields on
|
||||
`EnvLayerOptionsSchema` and `FileLayerOptionsSchema` are also excluded because
|
||||
`EventBusLike` instances are not serialisable; pass them directly in code.
|
||||
115
docs/foundation/core/config/src/utils/mask.md
Normal file
115
docs/foundation/core/config/src/utils/mask.md
Normal file
@@ -0,0 +1,115 @@
|
||||
# mask — Path utilities and sensitive-key masking for `@suaveplan/config`
|
||||
|
||||
## Purpose
|
||||
|
||||
This module provides low-level dot-notation path traversal utilities
|
||||
(`getByPath`, `setByPath`, `hasByPath`) and the `maskSensitive` function that
|
||||
uses them to redact sensitive keys from config snapshots before they are logged
|
||||
or transmitted. The path utilities are also used internally by `envLayer` and
|
||||
`cliLayer` to write coerced values into the result object without requiring
|
||||
the caller to construct deeply nested structures manually.
|
||||
|
||||
## Features
|
||||
|
||||
- `getByPath` — safely reads a value at any dot-notation depth; returns `undefined`
|
||||
on any missing or non-object intermediate segment
|
||||
- `hasByPath` — `true` if `getByPath` resolves to a non-`undefined` value
|
||||
- `setByPath` — writes a value at a dot-notation path, creating intermediate objects
|
||||
as needed; mutates the target and returns it
|
||||
- `maskSensitive` — `structuredClone` + `setByPath` each sensitive path with
|
||||
`MASK_STRING` (`"****"`) or a custom mask string
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { getByPath, setByPath, maskSensitive } from '@suaveplan/config/utils/mask';
|
||||
|
||||
const cfg = { db: { host: 'localhost', password: 'secret' } };
|
||||
|
||||
getByPath(cfg, 'db.host'); // 'localhost'
|
||||
getByPath(cfg, 'db.missing'); // undefined
|
||||
|
||||
const masked = maskSensitive(cfg, ['db.password']);
|
||||
// { db: { host: 'localhost', password: '****' } }
|
||||
// cfg is unchanged
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { setByPath, getByPath, maskSensitive } from '@suaveplan/config/utils/mask';
|
||||
|
||||
// Build a config object from flat CLI flags via dot paths
|
||||
const result = {};
|
||||
setByPath(result, 'server.port', 4000);
|
||||
setByPath(result, 'server.tls.enabled', true);
|
||||
setByPath(result, 'db.password', 'hunter2');
|
||||
|
||||
// result = { server: { port: 4000, tls: { enabled: true } }, db: { password: 'hunter2' } }
|
||||
|
||||
// Multi-path masking with custom mask string
|
||||
const safe = maskSensitive(result, ['db.password', 'server.tls.enabled'], '[REDACTED]');
|
||||
// { server: { port: 4000, tls: { enabled: '[REDACTED]' } }, db: { password: '[REDACTED]' } }
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `getByPath(obj: ConfigData, path: string): unknown`
|
||||
|
||||
Traverses `obj` by splitting `path` on `"."` and descending at each segment.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `obj` | `ConfigData` | Root object to traverse |
|
||||
| `path` | `string` | Dot-notation path, e.g. `"db.host"` |
|
||||
|
||||
**Returns** the resolved value or `undefined` if any segment is missing or an
|
||||
intermediate value is not a plain object (e.g. is `null`, an array, or a scalar).
|
||||
|
||||
### `hasByPath(obj: ConfigData, path: string): boolean`
|
||||
|
||||
Returns `getByPath(obj, path) !== undefined`.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `obj` | `ConfigData` | Root object |
|
||||
| `path` | `string` | Dot-notation path |
|
||||
|
||||
### `setByPath(obj: ConfigData, path: string, value: unknown): ConfigData`
|
||||
|
||||
Writes `value` at the specified dot-notation path, creating intermediate plain
|
||||
objects as needed. Mutates `obj` in place.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `obj` | `ConfigData` | Root object to mutate |
|
||||
| `path` | `string` | Dot-notation path |
|
||||
| `value` | `unknown` | Value to write |
|
||||
|
||||
**Returns** `obj` (same reference, mutated).
|
||||
|
||||
**Side effect:** Any intermediate segment that is not a plain object (null,
|
||||
array, scalar) is replaced with a new `{}`.
|
||||
|
||||
### `maskSensitive(data: ConfigData, sensitive: string[], mask?: string): ConfigData`
|
||||
|
||||
Returns a deep clone of `data` with every path in `sensitive` replaced by
|
||||
`mask`. Does not modify the original `data`.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `data` | `ConfigData` | Source config object |
|
||||
| `sensitive` | `string[]` | Dot-notation paths to redact |
|
||||
| `mask` | `string` | Replacement string; defaults to `"****"` |
|
||||
|
||||
**Returns** a new `ConfigData` with sensitive values replaced. Paths that do
|
||||
not exist in `data` are silently skipped.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`structuredClone` is used in `maskSensitive` to produce a deep copy before
|
||||
mutation. This is safe for all JSON-serialisable values (the only kind that
|
||||
should appear in config data). The function is idempotent: applying it twice
|
||||
with the same arguments yields the same result as applying it once, because the
|
||||
mask string `"****"` is itself a valid value that survives the next clone-and-
|
||||
replace cycle unchanged.
|
||||
90
docs/foundation/core/config/src/utils/merge.md
Normal file
90
docs/foundation/core/config/src/utils/merge.md
Normal file
@@ -0,0 +1,90 @@
|
||||
# merge — Deep-merge and layer-folding utilities for `@suaveplan/config`
|
||||
|
||||
## Purpose
|
||||
|
||||
This module provides two functions that form the merge backbone of the config
|
||||
pipeline: `deepMerge` performs a structural recursive merge of two plain-object
|
||||
trees, and `mergeLayers` folds an ordered array of named config layers using
|
||||
`deepMerge`. These utilities are deliberately small and self-contained — they
|
||||
carry no external runtime dependencies beyond the OTel API no-op SDK, and are
|
||||
therefore safe to import in any environment.
|
||||
|
||||
## Features
|
||||
|
||||
- `deepMerge` — recursively merges two plain objects; arrays replaced, `undefined`
|
||||
override values skipped, non-object scalars overwrite
|
||||
- `mergeLayers` — folds a pre-sorted list of `{ name, data }` entries using
|
||||
`deepMerge`; records merge duration to `config.merge_duration_ms` OTel histogram
|
||||
- No mutation of input objects — both functions return new objects
|
||||
- TypeScript generics preserve the inferred type of the base object
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { deepMerge } from '@suaveplan/config/utils/merge';
|
||||
|
||||
const base = { port: 3000, db: { host: 'localhost', port: 5432 } };
|
||||
const override = { db: { host: 'prod.db.example.com' } };
|
||||
|
||||
const merged = deepMerge(base, override);
|
||||
// { port: 3000, db: { host: 'prod.db.example.com', port: 5432 } }
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { mergeLayers } from '@suaveplan/config/utils/merge';
|
||||
|
||||
const layers = [
|
||||
{ name: 'defaults', data: { port: 3000, debug: true, db: { pool: 5 } } },
|
||||
{ name: 'file', data: { port: 4000, db: { pool: 10 } } },
|
||||
{ name: 'env', data: { debug: false } },
|
||||
];
|
||||
|
||||
const result = mergeLayers(layers);
|
||||
// { port: 4000, debug: false, db: { pool: 10 } }
|
||||
// Note: layers must be pre-sorted by priority — mergeLayers does not sort.
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `deepMerge<T extends ConfigData>(base: T, override: Partial<T>): T`
|
||||
|
||||
Recursively merges `override` into `base` and returns a new object.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `base` | `T` | The base object to merge into |
|
||||
| `override` | `Partial<T>` | Values to apply on top of `base` |
|
||||
|
||||
**Returns** a new `T`-typed object; neither argument is mutated.
|
||||
|
||||
**Merge rules:**
|
||||
|
||||
- Plain objects (non-array, non-null, prototype === `Object.prototype`) are merged recursively.
|
||||
- Arrays in `override` replace the corresponding value in `base`.
|
||||
- Keys with `undefined` values in `override` are skipped (base value preserved).
|
||||
- All other scalar values in `override` overwrite the base value.
|
||||
|
||||
### `mergeLayers(layers: Array<{ name: string; data: ConfigData }>): ConfigData`
|
||||
|
||||
Folds `layers` left-to-right using `deepMerge`.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `layers` | `Array<{ name: string; data: ConfigData }>` | Pre-sorted layer descriptors |
|
||||
|
||||
**Returns** the merged `ConfigData`. Layers must be sorted by priority before
|
||||
calling this function; `mergeLayers` itself does not sort.
|
||||
|
||||
**OTel:** Records the total duration to the `config.merge_duration_ms` histogram
|
||||
with a `config.layer_count` attribute. This is automatically a no-op when no OTel
|
||||
SDK is installed.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`isPlainObject` guards against merging class instances, `Date` objects, `Map`,
|
||||
`Set`, and other non-plain-object values — those are replaced rather than merged.
|
||||
This is intentional: config values should be JSON-serialisable plain data. The
|
||||
`deepMerge` function does not handle circular references; config objects are
|
||||
assumed to be acyclic (a reasonable constraint for any serialisable config tree).
|
||||
Reference in New Issue
Block a user