feat(ADM-018): completed feature
This commit is contained in:
78
legacy/specs-old/F-004-config-flags/DESIGN.md
Normal file
78
legacy/specs-old/F-004-config-flags/DESIGN.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# DESIGN — F-004 Typed config and feature flags
|
||||
|
||||
## Layering
|
||||
- `src/infrastructure/config/config.ts` — pure config loader. Owns env parsing and
|
||||
validation. No side effects on `process.env`; takes an env object so tests are
|
||||
deterministic. Throws a single `ConfigError` listing every problem found.
|
||||
- `src/modules/flags/` — the feature-flag module. Business-facing, boundary-checked.
|
||||
Exposes only `index.ts`. Contains the `FeatureFlagProvider` interface and a simple
|
||||
in-memory store. Does NOT depend on infrastructure/config (kept independent so it
|
||||
can later be backed by a DB/external service without touching config).
|
||||
|
||||
## Config loader (infrastructure)
|
||||
```ts
|
||||
export interface AppConfig {
|
||||
nodeEnv: 'development' | 'test' | 'production';
|
||||
port: number;
|
||||
host: string;
|
||||
logLevel: string;
|
||||
databaseUrl: string; // required
|
||||
redisUrl?: string; // optional
|
||||
flags: Record<string, boolean>; // parsed FLAG_* vars (initial seed)
|
||||
}
|
||||
export class ConfigError extends Error { problems: string[] }
|
||||
export function loadConfig(env: StringRecord): AppConfig
|
||||
```
|
||||
- `DATABASE_URL` required: missing -> ConfigError naming the var.
|
||||
- `PORT` parsed to int, validated 1..65535.
|
||||
- `NODE_ENV` whitelisted, default `development`.
|
||||
- `FLAG_<NAME>` vars: value `true`/`false` (case-insensitive) -> boolean; other values
|
||||
-> ConfigError naming the var. Names lowercased after stripping `FLAG_` prefix.
|
||||
- Accumulate all problems, then throw once (fail fast AND clear).
|
||||
|
||||
## Feature flag module
|
||||
```ts
|
||||
export interface FeatureFlagProvider {
|
||||
isEnabled(name: string): boolean;
|
||||
}
|
||||
export class InMemoryFeatureFlagStore implements FeatureFlagProvider {
|
||||
constructor(initial?: Record<string, boolean>)
|
||||
isEnabled(name): boolean // unknown flag -> false (fail-safe default)
|
||||
setEnabled(name, enabled): void // runtime mutation, no redeploy needed
|
||||
}
|
||||
export function createFlagStore(initial?): InMemoryFeatureFlagStore
|
||||
```
|
||||
- Unknown flags are OFF by default (fail-safe: risky paths stay skipped).
|
||||
- `setEnabled` mutates live state: deployment ≠ activation.
|
||||
- Guarded-path helper kept trivial; consumers do `if (flags.isEnabled('x')) { ... }`.
|
||||
|
||||
## Wiring (composition root)
|
||||
- `src/app/build-app.ts` stays DB-free for now; but `buildApp` accepts an optional
|
||||
`flags` dep so routes can guard paths. Default: empty store (all flags off).
|
||||
- `src/infrastructure/http/server.ts` calls `loadConfig(process.env)`, uses typed
|
||||
`port`/`host`, and passes `config.flags` into the app. On ConfigError it logs the
|
||||
problems and exits non-zero.
|
||||
- `pool.ts` keeps `createPoolFromEnv` but gains an optional typed overload note; no
|
||||
breaking change needed this ticket (config owns DATABASE_URL validation at startup).
|
||||
|
||||
## Boundary notes
|
||||
- `flags` module: only imports from `shared` (currently nothing needed). It must not
|
||||
import `infrastructure/config`. Config parses FLAG_* into a plain record and hands it
|
||||
to the module at composition time — inversion keeps the module clean.
|
||||
- Boundary checker will enforce this automatically.
|
||||
|
||||
## Files
|
||||
| File | Role |
|
||||
|---|---|
|
||||
| src/infrastructure/config/config.ts | typed loader + ConfigError |
|
||||
| src/infrastructure/config/tests/config.test.ts | loader unit tests |
|
||||
| src/modules/flags/index.ts | module public API |
|
||||
| src/modules/flags/domain/feature-flag-store.ts | interface + store |
|
||||
| src/modules/flags/tests/feature-flag-store.test.ts | store unit tests |
|
||||
| src/app/build-app.ts | optional flags dep injection |
|
||||
| src/infrastructure/http/server.ts | loadConfig + typed listen + ConfigError exit |
|
||||
|
||||
## Risks / mitigations
|
||||
- Silent flag default: unknown -> false, documented; tested.
|
||||
- ConfigError leaking secrets: messages name var NAMES only, never values.
|
||||
- Boundary violation (flags importing config): prevented by composition-time injection + checker.
|
||||
41
legacy/specs-old/F-004-config-flags/SPEC.md
Normal file
41
legacy/specs-old/F-004-config-flags/SPEC.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# SPEC — F-004 Typed config and feature flags
|
||||
|
||||
## Problem
|
||||
Risky features need activation separate from deployment; env access must be typed.
|
||||
Today env access is ad-hoc (`process.env.X ?? fallback` scattered in entrypoints)
|
||||
and there is no activation mechanism distinct from deploy.
|
||||
|
||||
## Goal
|
||||
Fail-fast typed config plus a simple feature flag module behind an interface.
|
||||
|
||||
## Scope IN
|
||||
- Typed env config loader in `src/infrastructure/config`, fail fast on missing/invalid required vars (all problems reported in one clear message)
|
||||
- `FeatureFlagProvider` interface with simple in-memory store implementation (`src/modules/flags`)
|
||||
- Deployment ≠ activation: flags seed from `FLAG_*` env vars at boot and can be mutated at runtime without redeploy
|
||||
- DB pool refactor to consume config instead of sniffing env itself
|
||||
|
||||
## Scope OUT
|
||||
- No external flag service
|
||||
- No per-user segmentation
|
||||
- No admin HTTP endpoint for flags yet (first consumer decides shape; store API is runtime-mutable already)
|
||||
|
||||
## Required vs optional vars
|
||||
| Var | Required | Default |
|
||||
|---|---|---|
|
||||
| DATABASE_URL | yes | — |
|
||||
| PORT | no | 3000 (valid: integer 1–65535) |
|
||||
| HOST | no | 0.0.0.0 |
|
||||
| LOG_LEVEL | no | info |
|
||||
| NODE_ENV | no | development (valid: development/test/production) |
|
||||
| REDIS_URL | no | undefined |
|
||||
| FLAG_<NAME> | no | parsed as true/false |
|
||||
|
||||
## Acceptance criteria
|
||||
1. Given a missing required env var When the app starts Then startup fails with a clear message naming the var.
|
||||
2. Given flag off When a code path guarded by the flag runs Then the path is skipped.
|
||||
3. Flag state change does not require redeploy (runtime `setEnabled` on the store).
|
||||
4. `./scripts/verify.sh` green.
|
||||
|
||||
## Non-functional
|
||||
- No new dependencies: hand-rolled validation is small, explicit and boring.
|
||||
- Config loader is a pure function of an env object (testable without touching process.env).
|
||||
10
legacy/specs-old/F-004-config-flags/TASKS.md
Normal file
10
legacy/specs-old/F-004-config-flags/TASKS.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# TASKS — F-004 Typed config and feature flags
|
||||
|
||||
- [ ] TASK-001 src/infrastructure/config/config.ts: AppConfig, ConfigError, loadConfig (pure over env object)
|
||||
- [ ] TASK-002 src/infrastructure/config/tests/config.test.ts: required/optional/invalid cases
|
||||
- [ ] TASK-003 src/modules/flags/domain/feature-flag-store.ts: FeatureFlagProvider + InMemoryFeatureFlagStore + createFlagStore
|
||||
- [ ] TASK-004 src/modules/flags/index.ts: public API
|
||||
- [ ] TASK-005 src/modules/flags/tests/feature-flag-store.test.ts: guard + runtime mutation
|
||||
- [ ] TASK-006 src/app/build-app.ts: optional flags dep
|
||||
- [ ] TASK-007 src/infrastructure/http/server.ts: loadConfig, typed listen, ConfigError exit
|
||||
- [ ] TASK-008 run lint + boundaries + typecheck + build + test + verify.sh
|
||||
22
legacy/specs-old/F-004-config-flags/TESTS.md
Normal file
22
legacy/specs-old/F-004-config-flags/TESTS.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# TESTS — F-004 Typed config and feature flags
|
||||
|
||||
## Unit — src/infrastructure/config/tests/config.test.ts
|
||||
1. Valid full env -> typed AppConfig (port number, nodeEnv enum, flags parsed)
|
||||
2. Missing DATABASE_URL -> ConfigError naming DATABASE_URL
|
||||
3. Invalid PORT (non-numeric / out of range) -> ConfigError naming PORT
|
||||
4. Invalid NODE_ENV -> ConfigError
|
||||
5. FLAG_* true/false parsed; unknown flag value -> ConfigError naming the var
|
||||
6. Multiple problems -> single ConfigError listing all of them
|
||||
|
||||
## Unit — src/modules/flags/tests/feature-flag-store.test.ts
|
||||
1. Known flag on -> isEnabled true; known flag off -> false
|
||||
2. Unknown flag -> false (fail-safe)
|
||||
3. setEnabled flips state at runtime (no new store / no redeploy)
|
||||
4. createFlagStore seeds initial state
|
||||
|
||||
## Composition
|
||||
5. buildApp with flags dep: guarded path skipped when flag off, runs when on
|
||||
|
||||
## Manual / QA
|
||||
- Start server without DATABASE_URL -> clear error naming the var, non-zero exit.
|
||||
- verify.sh green.
|
||||
Reference in New Issue
Block a user