@futdevpro/fsm-dynamo 1.15.12 → 1.15.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,43 @@
1
+ # BACKLOG.md
2
+
3
+ > STRICT PATTERN (MUST FOLLOW EXACTLY)
4
+ > - Entry header: "- [TYPE] (BL-YYYYMMDD-###) TITLE"
5
+ > - Immediately followed by (exact keys, colon-space, indented 2 spaces):
6
+ > - " status: <EMOJI status-key>"
7
+ > - " priority: <low|medium|high|urgent>"
8
+ > - " source: <user|system|assistant>"
9
+ > - " area: <ui|backend|infra|docs|tests|ux|general>"
10
+ > - " details: <single-line summary>"
11
+ > - NO blank line between header and fields (parser reads i+1..i+5).
12
+ > - One blank line between entries (readability).
13
+ > - TYPE values: FEATURE, BUG, IMPROVEMENT, QUESTION, RESEARCH, TASK
14
+ > - STATUS EMOJI: ❌ ⏳ 🔄 ✅ ⚠️ ❓
15
+ > - IDs: unique, format BL-YYYYMMDD-### (increment ###).
16
+
17
+ - [IMPROVEMENT] (BL-20260518-001) DyFM_StateMachine: explicit `_FailureResult` return type a `_failure()` helper-en (TS narrowing)
18
+ status: ⏳ pending
19
+ priority: low
20
+ source: assistant
21
+ area: backend
22
+ details: A `DyFM_StateMachine._failure()` jelenleg `DyFM_StateMachineTransition_Result<TState>` union-t ad vissza. A TS inference emiatt nem narrow-ol az `if (!r.ok) {...}` blokkban (a private helper visszatérése a union mind két ágát beilleszti, és a `_failure()` belső object literal `{ ok: false, ... }` típusa nem narrow-ódik le `false` literal-re). Konzumens kódban `as DyFM_StateMachineTransition_FailureResult<TState>` cast szükséges. Fix: `_failure()` return type-ja explicit `DyFM_StateMachineTransition_FailureResult<TState>` (NEM union). 1-line type-annotation change, 0 viselkedés-változás.
23
+
24
+ - [IMPROVEMENT] (BL-20260518-002) DyFM_StateMachine: `DyFM_Error` wrapping a result.error mezőben
25
+ status: ⏳ pending
26
+ priority: low
27
+ source: assistant
28
+ area: backend
29
+ details: A jelenlegi `DyFM_StateMachineTransition_FailureResult.error` `DyFM_Error | Error` típusú, de a `_failure()` raw `Error`-t propagál (callback throw-jából). Konzisztencia a többi `DyFM_*` modullal: `DyFM_Error({ status: 422, errorCode: 'DyFM-SM-<subcode>', message, error: originalError, issuerService: 'DyFM_StateMachine' })` wrappingot adni a 4 throw-elhető helyen (guard/onLeave/onEnter/persist). Subcode-ok pl. 'DyFM-SM-GR' (guard-rejected throw), 'DyFM-SM-OL' (on-leave), 'DyFM-SM-OE' (on-enter), 'DyFM-SM-PE' (persist). Spec: minden 4 error-path-on `DyFM_Error.getErrorCode(f.error)` assertion.
30
+
31
+ - [TASK] (BL-20260518-003) DyFM_StateMachine: guard-throw + self-loop spec coverage
32
+ status: ⏳ pending
33
+ priority: low
34
+ source: assistant
35
+ area: tests
36
+ details: Két explicit spec hiányzik a FR-006 (52354ba) 22-spec szettjéből: (1) guard callback throw esetén `transition()` `guard-rejected` reason-t ad-e, és a `canTransition()` `false`-t (mindkét helyen catch-eli a impl, de NEM bizonyított teszttel); (2) self-loop transition (`from === to`) explicit spec — a `_findTransition` matchel rá, de nincs lefedve. ~30 LOC spec, no impl change.
37
+
38
+ - [FEATURE] (BL-20260518-004) DyFM_StateMachine: Mongoose persist adapter
39
+ status: ⏳ pending
40
+ priority: medium
41
+ source: assistant
42
+ area: backend
43
+ details: Az MVP-ben (FR-006 / 52354ba) a `persist` callback caller-supplied (CCAP MP1-D maga írja a Mongo updateOne logikát stateVersion optimistic-lock-kal). Egy genericikus adapter ami `DyFM_dataModel`-kötött (Mongoose ControlModel) state-document-et frissít automatikusan, kevesebb boilerplate-tel. API: `createMongoosePersistAdapter({ collection: Model<T>, docFilter: { _id: 'rag-status' } }): DyFM_StateMachineConfig['persist']`. Optimistic-lock: `findOneAndUpdate({ ...filter, stateVersion: prev }, { $set: { state, stateVersion: next, lastTransitionAt: timestamp } })` — match nélkül `'persist-failed'`. Tervek szerint a consumer real-world tapasztalat (MP1-D landolása) után írandó, hogy a tényleges minta visszahatáson alapuljon.
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Options for `DyFM_requireEnv`. See the helper's JSDoc for usage.
3
+ */
4
+ export interface DyFM_RequireEnv_Options {
5
+ /**
6
+ * Stable error code emitted on the DyFM_Error when the env-var is missing
7
+ * and no fallback is configured. Convention: `<PACKAGE>-ENV-MISSING-<KEY>`,
8
+ * e.g. `DyNM-ENV-MISSING-STORAGE-KEY`, `FDP-ENV-MISSING-AUTH-KEY`.
9
+ */
10
+ errorCode: string;
11
+ /**
12
+ * Short-code name of the package responsible for the env-var. Used as the
13
+ * DyFM_Error `___issuerService` for downstream filtering / dashboards.
14
+ */
15
+ issuerService: string;
16
+ /**
17
+ * Transition fallback. When set, returned if BOTH `process.env[name]` AND
18
+ * `DyFM_global_settings.envOverrides[name]` are missing.
19
+ *
20
+ * Use only during the additive phase of an env-var migration to keep the
21
+ * package backward-compatible while every deploy target rolls the new
22
+ * env-var out. Remove the `fallback` argument once the rollout is verified
23
+ * everywhere — then a missing env-var fail-fast crashes the consumer at
24
+ * first access with a structured DyFM_Error.
25
+ */
26
+ fallback?: string;
27
+ /**
28
+ * Admin-actionable user-facing message attached to the DyFM_Error. Defaults
29
+ * to a generic config-missing string referencing the env-var name.
30
+ */
31
+ userMessage?: string;
32
+ }
33
+ /**
34
+ * FR-015 Phase 2 — isomorphic env-var requirement helper.
35
+ *
36
+ * Universal source of truth for "read this configuration key from an env-var
37
+ * across Node and browser". Replaces per-package `requireEnv` helpers (the
38
+ * Phase 1 local implementation in fdp-templates-nts is being refactored to
39
+ * delegate here).
40
+ *
41
+ * Lookup order:
42
+ * 1. `process.env[envVarName]` — Node-side (server const files at module load).
43
+ * 2. `DyFM_global_settings.envOverrides[envVarName]` — browser-side, populated
44
+ * by the consumer at app bootstrap from its `environment.ts`. See
45
+ * DyFM_Global_Settings.envOverrides docs for the pattern.
46
+ * 3. `options.fallback` if provided — used during migration's additive phase.
47
+ * 4. throw a structured DyFM_Error with the full deploy-target checklist.
48
+ *
49
+ * The throw shape is rich on purpose (per the rich-error rule):
50
+ * - `___status` = 500 (configuration error)
51
+ * - `_errorCode` = caller-supplied stable code
52
+ * - `_message` = debug-level description with the env-var name + deploy-target
53
+ * checklist (host .env, docker-compose env-block, Overseer secrets-store,
54
+ * client environment.ts → bootstrap envOverrides)
55
+ * - `__userMessage` = admin-actionable message
56
+ * - `___issuerService` = caller-supplied package short-code
57
+ *
58
+ * @example Node server const file (module load):
59
+ * ```ts
60
+ * import { DyFM_requireEnv } from '@futdevpro/fsm-dynamo';
61
+ *
62
+ * export const FDP_keysEnv_settingsBase = {
63
+ * authKey: DyFM_requireEnv('FDP_AUTH_KEY', {
64
+ * errorCode: 'FDP-ENV-MISSING-AUTH-KEY',
65
+ * issuerService: 'fdp-templates',
66
+ * fallback: '<inherit literal>', // remove for Wave 3
67
+ * }),
68
+ * };
69
+ * ```
70
+ *
71
+ * @example Browser-side lazy getter (deferred to first access, after consumer
72
+ * bootstrap has populated DyFM_global_settings.envOverrides):
73
+ * ```ts
74
+ * let _cached: string | undefined;
75
+ * function _resolveKey(): string {
76
+ * if (_cached !== undefined) return _cached;
77
+ * _cached = DyFM_requireEnv('DyNM_STORAGE_ENCRYPTION_KEY', { ... });
78
+ * return _cached;
79
+ * }
80
+ * export const DyNM_global_settings = {
81
+ * get storageEncryptionKey(): string { return _resolveKey(); },
82
+ * };
83
+ * ```
84
+ */
85
+ export declare function DyFM_requireEnv(envVarName: string, options: DyFM_RequireEnv_Options): string;
86
+ //# sourceMappingURL=require-env.util.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"require-env.util.d.ts","sourceRoot":"","sources":["../../../src/_collections/utils/require-env.util.ts"],"names":[],"mappings":"AAIA;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACtC;;;;OAIG;IACH,SAAS,EAAE,MAAM,CAAC;IAElB;;;OAGG;IACH,aAAa,EAAE,MAAM,CAAC;IAEtB;;;;;;;;;OASG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAGD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AACH,wBAAgB,eAAe,CAC7B,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,uBAAuB,GAC/B,MAAM,CAyCR"}
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DyFM_requireEnv = DyFM_requireEnv;
4
+ const error_control_model_1 = require("../../_models/control-models/error.control-model");
5
+ const global_settings_const_1 = require("../constants/global-settings.const");
6
+ /**
7
+ * FR-015 Phase 2 — isomorphic env-var requirement helper.
8
+ *
9
+ * Universal source of truth for "read this configuration key from an env-var
10
+ * across Node and browser". Replaces per-package `requireEnv` helpers (the
11
+ * Phase 1 local implementation in fdp-templates-nts is being refactored to
12
+ * delegate here).
13
+ *
14
+ * Lookup order:
15
+ * 1. `process.env[envVarName]` — Node-side (server const files at module load).
16
+ * 2. `DyFM_global_settings.envOverrides[envVarName]` — browser-side, populated
17
+ * by the consumer at app bootstrap from its `environment.ts`. See
18
+ * DyFM_Global_Settings.envOverrides docs for the pattern.
19
+ * 3. `options.fallback` if provided — used during migration's additive phase.
20
+ * 4. throw a structured DyFM_Error with the full deploy-target checklist.
21
+ *
22
+ * The throw shape is rich on purpose (per the rich-error rule):
23
+ * - `___status` = 500 (configuration error)
24
+ * - `_errorCode` = caller-supplied stable code
25
+ * - `_message` = debug-level description with the env-var name + deploy-target
26
+ * checklist (host .env, docker-compose env-block, Overseer secrets-store,
27
+ * client environment.ts → bootstrap envOverrides)
28
+ * - `__userMessage` = admin-actionable message
29
+ * - `___issuerService` = caller-supplied package short-code
30
+ *
31
+ * @example Node server const file (module load):
32
+ * ```ts
33
+ * import { DyFM_requireEnv } from '@futdevpro/fsm-dynamo';
34
+ *
35
+ * export const FDP_keysEnv_settingsBase = {
36
+ * authKey: DyFM_requireEnv('FDP_AUTH_KEY', {
37
+ * errorCode: 'FDP-ENV-MISSING-AUTH-KEY',
38
+ * issuerService: 'fdp-templates',
39
+ * fallback: '<inherit literal>', // remove for Wave 3
40
+ * }),
41
+ * };
42
+ * ```
43
+ *
44
+ * @example Browser-side lazy getter (deferred to first access, after consumer
45
+ * bootstrap has populated DyFM_global_settings.envOverrides):
46
+ * ```ts
47
+ * let _cached: string | undefined;
48
+ * function _resolveKey(): string {
49
+ * if (_cached !== undefined) return _cached;
50
+ * _cached = DyFM_requireEnv('DyNM_STORAGE_ENCRYPTION_KEY', { ... });
51
+ * return _cached;
52
+ * }
53
+ * export const DyNM_global_settings = {
54
+ * get storageEncryptionKey(): string { return _resolveKey(); },
55
+ * };
56
+ * ```
57
+ */
58
+ function DyFM_requireEnv(envVarName, options) {
59
+ // Node path: process.env at module-load time. Guarded for browser bundles
60
+ // where `process` may be undefined or shimmed without runtime values.
61
+ if (typeof process !== 'undefined' && process.env && process.env[envVarName]) {
62
+ return process.env[envVarName];
63
+ }
64
+ // Browser path: consumer-populated envOverrides map. Populated at app
65
+ // bootstrap from environment.ts — see DyFM_Global_Settings.envOverrides.
66
+ const override = global_settings_const_1.DyFM_global_settings.envOverrides?.[envVarName];
67
+ if (override) {
68
+ return override;
69
+ }
70
+ // Transition fallback (Wave 1 of an FR-015-style migration).
71
+ if (options.fallback !== undefined) {
72
+ return options.fallback;
73
+ }
74
+ // Missing — fail fast with a rich, debug-friendly DyFM_Error.
75
+ throw new error_control_model_1.DyFM_Error({
76
+ status: 500,
77
+ errorCode: options.errorCode,
78
+ message: `Environment variable '${envVarName}' is required but was not found. ` +
79
+ `Checked: process.env.${envVarName} (Node) and ` +
80
+ `DyFM_global_settings.envOverrides['${envVarName}'] (browser). ` +
81
+ `Both unset, and no transition fallback configured. ` +
82
+ `Set '${envVarName}' on every deploy target before deploying this ` +
83
+ `package version: (1) host .env on every host running a backend, ` +
84
+ `(2) docker-compose env-block on every consumer service, ` +
85
+ `(3) Overseer secrets-store entry for CI builds, ` +
86
+ `(4) client environment.ts plus app-bootstrap copy into ` +
87
+ `DyFM_global_settings.envOverrides for any browser consumer.`,
88
+ userMessage: options.userMessage
89
+ ?? `Configuration error — environment variable '${envVarName}' is missing. `
90
+ + `Contact the responsible operator to set it on this deploy target.`,
91
+ issuerService: options.issuerService,
92
+ });
93
+ }
94
+ //# sourceMappingURL=require-env.util.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"require-env.util.js","sourceRoot":"","sources":["../../../src/_collections/utils/require-env.util.ts"],"names":[],"mappings":";;AA6FA,0CA4CC;AAzID,0FAA8E;AAC9E,8EAA0E;AAwC1E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AACH,SAAgB,eAAe,CAC7B,UAAkB,EAClB,OAAgC;IAEhC,0EAA0E;IAC1E,sEAAsE;IACtE,IAAI,OAAO,OAAO,KAAK,WAAW,IAAI,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC;QAC7E,OAAO,OAAO,CAAC,GAAG,CAAC,UAAU,CAAW,CAAC;IAC3C,CAAC;IAED,sEAAsE;IACtE,yEAAyE;IACzE,MAAM,QAAQ,GACZ,4CAAoB,CAAC,YAAY,EAAE,CAAC,UAAU,CAAC,CAAC;IAClD,IAAI,QAAQ,EAAE,CAAC;QACb,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,6DAA6D;IAC7D,IAAI,OAAO,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;QACnC,OAAO,OAAO,CAAC,QAAQ,CAAC;IAC1B,CAAC;IAED,8DAA8D;IAC9D,MAAM,IAAI,gCAAU,CAAC;QACnB,MAAM,EAAE,GAAG;QACX,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,OAAO,EACL,yBAAyB,UAAU,mCAAmC;YACtE,wBAAwB,UAAU,cAAc;YAChD,sCAAsC,UAAU,gBAAgB;YAChE,qDAAqD;YACrD,QAAQ,UAAU,iDAAiD;YACnE,kEAAkE;YAClE,0DAA0D;YAC1D,kDAAkD;YAClD,yDAAyD;YACzD,6DAA6D;QAC/D,WAAW,EACT,OAAO,CAAC,WAAW;eAChB,+CAA+C,UAAU,gBAAgB;kBACvE,mEAAmE;QAC1E,aAAa,EAAE,OAAO,CAAC,aAAa;KACrC,CAAC,CAAC;AACL,CAAC"}
@@ -17,5 +17,27 @@ export interface DyFM_Global_Settings {
17
17
  * this setting will set which logs will be shown
18
18
  */
19
19
  log_settings: DyFM_GlobalLog_Settings;
20
+ /**
21
+ * FR-015 Phase 2 — isomorphic env-var overrides surface.
22
+ *
23
+ * On Node, `DyFM_requireEnv(name, ...)` reads `process.env[name]` directly.
24
+ * On browser there is no `process.env` at runtime, so consumers populate
25
+ * this map at app bootstrap from `environment.ts`:
26
+ *
27
+ * ```ts
28
+ * import { DyFM_global_settings } from '@futdevpro/fsm-dynamo';
29
+ * import { environment } from './environments/environment';
30
+ *
31
+ * DyFM_global_settings.envOverrides = {
32
+ * FDP_AUTH_KEY: environment.authKey,
33
+ * FDP_EXTRA_AUTH_STORAGE_KEY: environment.extraAuthStorageKey,
34
+ * DyNM_STORAGE_ENCRYPTION_KEY: environment.storageEncryptionKey,
35
+ * };
36
+ * ```
37
+ *
38
+ * `DyFM_requireEnv` checks `process.env` first, then this map, then
39
+ * the caller's optional `fallback`, then throws a structured DyFM_Error.
40
+ */
41
+ envOverrides?: Record<string, string>;
20
42
  }
21
43
  //# sourceMappingURL=global-settings.interface.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"global-settings.interface.d.ts","sourceRoot":"","sources":["../../../../src/_models/interfaces/environment/global-settings.interface.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,uBAAuB,EAAE,MAAM,iCAAiC,CAAC;AAE1E;;;;;;GAMG;AACH,MAAM,WAAW,oBAAoB;IACnC,mBAAmB,EAAE,MAAM,CAAC;IAE5B;;;OAGG;IACH,OAAO,EAAE,MAAM,CAAC;IAEhB;;OAEG;IACH,YAAY,EAAE,uBAAuB,CAAC;CACvC"}
1
+ {"version":3,"file":"global-settings.interface.d.ts","sourceRoot":"","sources":["../../../../src/_models/interfaces/environment/global-settings.interface.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,uBAAuB,EAAE,MAAM,iCAAiC,CAAC;AAE1E;;;;;;GAMG;AACH,MAAM,WAAW,oBAAoB;IACnC,mBAAmB,EAAE,MAAM,CAAC;IAE5B;;;OAGG;IACH,OAAO,EAAE,MAAM,CAAC;IAEhB;;OAEG;IACH,YAAY,EAAE,uBAAuB,CAAC;IAEtC;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACvC"}
package/build/index.d.ts CHANGED
@@ -10,6 +10,7 @@ export * from './_collections/utils/json-error-helper.util';
10
10
  export * from './_collections/utils/log.util';
11
11
  export * from './_collections/utils/round-list.util';
12
12
  export * from './_collections/utils/object.util';
13
+ export * from './_collections/utils/require-env.util';
13
14
  export * from './_collections/utils/stack.util';
14
15
  export * from './_collections/utils/string.util';
15
16
  export * from './_collections/utils/string-case.util';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,cAAc,2CAA2C,CAAC;AAC1D,cAAc,+CAA+C,CAAC;AAC9D,cAAc,gDAAgD,CAAC;AAC/D,cAAc,wCAAwC,CAAC;AACvD,cAAc,sCAAsC,CAAC;AAGrD,cAAc,iCAAiC,CAAC;AAChD,cAAc,iCAAiC,CAAC;AAChD,cAAc,gCAAgC,CAAC;AAC/C,cAAc,6CAA6C,CAAC;AAC5D,cAAc,+BAA+B,CAAC;AAC9C,cAAc,sCAAsC,CAAC;AACrD,cAAc,kCAAkC,CAAC;AACjD,cAAc,iCAAiC,CAAC;AAChD,cAAc,kCAAkC,CAAC;AACjD,cAAc,uCAAuC,CAAC;AACtD,cAAc,gCAAgC,CAAC;AAC/C,cAAc,iDAAiD,CAAC;AAChE,cAAc,qCAAqC,CAAC;AACpD,cAAc,gCAAgC,CAAC;AAG/C,cAAc,2CAA2C,CAAC;AAC1D,cAAc,qCAAqC,CAAC;AACpD,cAAc,uCAAuC,CAAC;AACtD,cAAc,6CAA6C,CAAC;AAC5D,cAAc,wCAAwC,CAAC;AAGvD,cAAc,gDAAgD,CAAC;AAC/D,cAAc,uCAAuC,CAAC;AACtD,cAAc,gDAAgD,CAAC;AAI/D,cAAc,mCAAmC,CAAC;AAClD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,yBAAyB,CAAC;AACxC,cAAc,wCAAwC,CAAC;AAGvD,cAAc,mCAAmC,CAAC;AAClD,cAAc,oCAAoC,CAAC;AACnD,cAAc,uCAAuC,CAAC;AAGtD,cAAc,gCAAgC,CAAC;AAC/C,cAAc,0BAA0B,CAAC;AACzC,cAAc,kCAAkC,CAAC;AAKjD,cAAc,0DAA0D,CAAC;AACzE,cAAc,6DAA6D,CAAC;AAC5E,cAAc,8CAA8C,CAAC;AAC7D,cAAc,6CAA6C,CAAC;AAC5D,cAAc,oDAAoD,CAAC;AACnE,cAAc,sDAAsD,CAAC;AACrE,cAAc,uEAAuE,CAAC;AAGtF,cAAc,iEAAiE,CAAC;AAChF,cAAc,0DAA0D,CAAC;AACzE,cAAc,wDAAwD,CAAC;AAGvE,cAAc,yCAAyC,CAAC;AACxD,cAAc,2CAA2C,CAAC;AAG1D,cAAc,2CAA2C,CAAC;AAC1D,cAAc,+CAA+C,CAAC;AAC9D,cAAc,sCAAsC,CAAC;AACrD,cAAc,8CAA8C,CAAC;AAC7D,cAAc,+CAA+C,CAAC;AAC9D,cAAc,6CAA6C,CAAC;AAC5D,cAAc,8CAA8C,CAAC;AAC7D,cAAc,wDAAwD,CAAC;AACvE,cAAc,wCAAwC,CAAC;AAGvD,cAAc,gEAAgE,CAAC;AAC/E,cAAc,4DAA4D,CAAC;AAG3E,cAAc,gCAAgC,CAAC;AAC/C,cAAc,8BAA8B,CAAC;AAC7C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,8BAA8B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,cAAc,2CAA2C,CAAC;AAC1D,cAAc,+CAA+C,CAAC;AAC9D,cAAc,gDAAgD,CAAC;AAC/D,cAAc,wCAAwC,CAAC;AACvD,cAAc,sCAAsC,CAAC;AAGrD,cAAc,iCAAiC,CAAC;AAChD,cAAc,iCAAiC,CAAC;AAChD,cAAc,gCAAgC,CAAC;AAC/C,cAAc,6CAA6C,CAAC;AAC5D,cAAc,+BAA+B,CAAC;AAC9C,cAAc,sCAAsC,CAAC;AACrD,cAAc,kCAAkC,CAAC;AACjD,cAAc,uCAAuC,CAAC;AACtD,cAAc,iCAAiC,CAAC;AAChD,cAAc,kCAAkC,CAAC;AACjD,cAAc,uCAAuC,CAAC;AACtD,cAAc,gCAAgC,CAAC;AAC/C,cAAc,iDAAiD,CAAC;AAChE,cAAc,qCAAqC,CAAC;AACpD,cAAc,gCAAgC,CAAC;AAG/C,cAAc,2CAA2C,CAAC;AAC1D,cAAc,qCAAqC,CAAC;AACpD,cAAc,uCAAuC,CAAC;AACtD,cAAc,6CAA6C,CAAC;AAC5D,cAAc,wCAAwC,CAAC;AAGvD,cAAc,gDAAgD,CAAC;AAC/D,cAAc,uCAAuC,CAAC;AACtD,cAAc,gDAAgD,CAAC;AAI/D,cAAc,mCAAmC,CAAC;AAClD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,yBAAyB,CAAC;AACxC,cAAc,wCAAwC,CAAC;AAGvD,cAAc,mCAAmC,CAAC;AAClD,cAAc,oCAAoC,CAAC;AACnD,cAAc,uCAAuC,CAAC;AAGtD,cAAc,gCAAgC,CAAC;AAC/C,cAAc,0BAA0B,CAAC;AACzC,cAAc,kCAAkC,CAAC;AAKjD,cAAc,0DAA0D,CAAC;AACzE,cAAc,6DAA6D,CAAC;AAC5E,cAAc,8CAA8C,CAAC;AAC7D,cAAc,6CAA6C,CAAC;AAC5D,cAAc,oDAAoD,CAAC;AACnE,cAAc,sDAAsD,CAAC;AACrE,cAAc,uEAAuE,CAAC;AAGtF,cAAc,iEAAiE,CAAC;AAChF,cAAc,0DAA0D,CAAC;AACzE,cAAc,wDAAwD,CAAC;AAGvE,cAAc,yCAAyC,CAAC;AACxD,cAAc,2CAA2C,CAAC;AAG1D,cAAc,2CAA2C,CAAC;AAC1D,cAAc,+CAA+C,CAAC;AAC9D,cAAc,sCAAsC,CAAC;AACrD,cAAc,8CAA8C,CAAC;AAC7D,cAAc,+CAA+C,CAAC;AAC9D,cAAc,6CAA6C,CAAC;AAC5D,cAAc,8CAA8C,CAAC;AAC7D,cAAc,wDAAwD,CAAC;AACvE,cAAc,wCAAwC,CAAC;AAGvD,cAAc,gEAAgE,CAAC;AAC/E,cAAc,4DAA4D,CAAC;AAG3E,cAAc,gCAAgC,CAAC;AAC/C,cAAc,8BAA8B,CAAC;AAC7C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,8BAA8B,CAAC"}
package/build/index.js CHANGED
@@ -16,6 +16,7 @@ tslib_1.__exportStar(require("./_collections/utils/json-error-helper.util"), exp
16
16
  tslib_1.__exportStar(require("./_collections/utils/log.util"), exports);
17
17
  tslib_1.__exportStar(require("./_collections/utils/round-list.util"), exports);
18
18
  tslib_1.__exportStar(require("./_collections/utils/object.util"), exports);
19
+ tslib_1.__exportStar(require("./_collections/utils/require-env.util"), exports);
19
20
  tslib_1.__exportStar(require("./_collections/utils/stack.util"), exports);
20
21
  tslib_1.__exportStar(require("./_collections/utils/string.util"), exports);
21
22
  tslib_1.__exportStar(require("./_collections/utils/string-case.util"), exports);
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AACA,cAAc;AACd,wBAAwB;AACxB,oFAA0D;AAC1D,wFAA8D;AAC9D,yFAA+D;AAC/D,iFAAuD;AACvD,+EAAqD;AAErD,oBAAoB;AACpB,0EAAgD;AAChD,0EAAgD;AAChD,yEAA+C;AAC/C,sFAA4D;AAC5D,wEAA8C;AAC9C,+EAAqD;AACrD,2EAAiD;AACjD,0EAAgD;AAChD,2EAAiD;AACjD,gFAAsD;AACtD,yEAA+C;AAC/C,0FAAgE;AAChE,8EAAoD;AACpD,yEAA+C;AAE/C,yBAAyB;AACzB,oFAA0D;AAC1D,8EAAoD;AACpD,gFAAsD;AACtD,sFAA4D;AAC5D,iFAAuD;AAEvD,2BAA2B;AAC3B,yFAA+D;AAC/D,gFAAsD;AACtD,yFAA+D;AAG/D,QAAQ;AACR,4EAAkD;AAClD,wEAA8C;AAC9C,yEAA+C;AAC/C,oEAA0C;AAC1C,kEAAwC;AACxC,iFAAuD;AAEvD,aAAa;AACb,4EAAkD;AAClD,6EAAmD;AACnD,gFAAsD;AAEtD,aAAa;AACb,yEAA+C;AAC/C,mEAAyC;AACzC,2EAAiD;AAGjD,SAAS;AACT,wBAAwB;AACxB,mGAAyE;AACzE,sGAA4E;AAC5E,uFAA6D;AAC7D,sFAA4D;AAC5D,6FAAmE;AACnE,+FAAqE;AACrE,gHAAsF;AAEtF,6BAA6B;AAC7B,0GAAgF;AAChF,mGAAyE;AACzE,iGAAuE;AAEvE,qBAAqB;AACrB,kFAAwD;AACxD,oFAA0D;AAE1D,oBAAoB;AACpB,oFAA0D;AAC1D,wFAA8D;AAC9D,+EAAqD;AACrD,uFAA6D;AAC7D,wFAA8D;AAC9D,sFAA4D;AAC5D,uFAA6D;AAC7D,iGAAuE;AACvE,iFAAuD;AAEvD,gCAAgC;AAChC,yGAA+E;AAC/E,qGAA2E;AAE3E,eAAe;AACf,yEAA+C;AAC/C,uEAA6C;AAC7C,yEAA+C;AAC/C,uEAA6C"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AACA,cAAc;AACd,wBAAwB;AACxB,oFAA0D;AAC1D,wFAA8D;AAC9D,yFAA+D;AAC/D,iFAAuD;AACvD,+EAAqD;AAErD,oBAAoB;AACpB,0EAAgD;AAChD,0EAAgD;AAChD,yEAA+C;AAC/C,sFAA4D;AAC5D,wEAA8C;AAC9C,+EAAqD;AACrD,2EAAiD;AACjD,gFAAsD;AACtD,0EAAgD;AAChD,2EAAiD;AACjD,gFAAsD;AACtD,yEAA+C;AAC/C,0FAAgE;AAChE,8EAAoD;AACpD,yEAA+C;AAE/C,yBAAyB;AACzB,oFAA0D;AAC1D,8EAAoD;AACpD,gFAAsD;AACtD,sFAA4D;AAC5D,iFAAuD;AAEvD,2BAA2B;AAC3B,yFAA+D;AAC/D,gFAAsD;AACtD,yFAA+D;AAG/D,QAAQ;AACR,4EAAkD;AAClD,wEAA8C;AAC9C,yEAA+C;AAC/C,oEAA0C;AAC1C,kEAAwC;AACxC,iFAAuD;AAEvD,aAAa;AACb,4EAAkD;AAClD,6EAAmD;AACnD,gFAAsD;AAEtD,aAAa;AACb,yEAA+C;AAC/C,mEAAyC;AACzC,2EAAiD;AAGjD,SAAS;AACT,wBAAwB;AACxB,mGAAyE;AACzE,sGAA4E;AAC5E,uFAA6D;AAC7D,sFAA4D;AAC5D,6FAAmE;AACnE,+FAAqE;AACrE,gHAAsF;AAEtF,6BAA6B;AAC7B,0GAAgF;AAChF,mGAAyE;AACzE,iGAAuE;AAEvE,qBAAqB;AACrB,kFAAwD;AACxD,oFAA0D;AAE1D,oBAAoB;AACpB,oFAA0D;AAC1D,wFAA8D;AAC9D,+EAAqD;AACrD,uFAA6D;AAC7D,wFAA8D;AAC9D,sFAA4D;AAC5D,uFAA6D;AAC7D,iGAAuE;AACvE,iFAAuD;AAEvD,gCAAgC;AAChC,yGAA+E;AAC/E,qGAA2E;AAE3E,eAAe;AACf,yEAA+C;AAC/C,uEAA6C;AAC7C,yEAA+C;AAC/C,uEAA6C"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@futdevpro/fsm-dynamo",
3
- "version": "01.15.12",
3
+ "version": "01.15.14",
4
4
  "description": "Full Stack Model Collection for Dynamic (NodeJS-Typescript) Framework called Dynamo, by Future Development Ltd.",
5
5
  "DyBu_settings": {
6
6
  "packageType": "full-stack-package",
@@ -256,7 +256,7 @@
256
256
  "uuid": "11.1.0"
257
257
  },
258
258
  "devDependencies": {
259
- "@futdevpro/dynamo-eslint": "1.15.9",
259
+ "@futdevpro/dynamo-eslint": "1.15.10",
260
260
  "@types/jasmine": "~4.3.5",
261
261
  "@typescript-eslint/eslint-plugin": "^8.41.0",
262
262
  "@typescript-eslint/parser": "^8.41.0",
@@ -114,7 +114,7 @@ describe('| DyFM_Object', () => {
114
114
  expect(result).toEqual(expected);
115
115
  });
116
116
 
117
- xit('should resolve circular references in data', () => {
117
+ xit('should resolve self-circular root reference', () => {
118
118
  const obj: any = {};
119
119
  obj.self = obj;
120
120
  const expected = { self: 'CIRCULATION:ROOT;Object.self' };
@@ -0,0 +1,231 @@
1
+ import { DyFM_global_settings } from '../constants/global-settings.const';
2
+ import { DyFM_Error } from '../../_models/control-models/error.control-model';
3
+ import { DyFM_requireEnv } from './require-env.util';
4
+
5
+
6
+ /**
7
+ * FR-015 Phase 2 Wave 0 — DyFM_requireEnv specs.
8
+ *
9
+ * Coverage:
10
+ * - Node path: returns process.env value when set
11
+ * - Browser path: returns DyFM_global_settings.envOverrides value when
12
+ * process.env is unset
13
+ * - Fallback path: returns options.fallback when both above unset
14
+ * - Throw path: structured DyFM_Error when nothing resolves and no
15
+ * fallback. Verifies errorCode, message contains the env-var name +
16
+ * deploy-target checklist, userMessage is admin-actionable,
17
+ * issuerService matches.
18
+ * - Override priority: process.env wins over envOverrides; envOverrides
19
+ * wins over fallback.
20
+ */
21
+
22
+
23
+ /**
24
+ * Cleans the test fixture between specs so leak-state from one test doesn't
25
+ * pollute another. We touch the live `DyFM_global_settings.envOverrides`
26
+ * because the spec exercises the same surface the production code reads.
27
+ */
28
+ function clearOverrides(): void {
29
+ DyFM_global_settings.envOverrides = undefined;
30
+ }
31
+
32
+
33
+ describe('| DyFM_requireEnv', (): void => {
34
+
35
+ afterEach((): void => {
36
+ clearOverrides();
37
+ delete process.env['DyFM_TEST_REQUIRE_ENV_KEY'];
38
+ delete process.env['DyFM_TEST_REQUIRE_ENV_OTHER'];
39
+ });
40
+
41
+
42
+ describe('| Node path (process.env)', (): void => {
43
+ it('| returns process.env value when set', (): void => {
44
+ process.env['DyFM_TEST_REQUIRE_ENV_KEY'] = 'node-value';
45
+ const value: string = DyFM_requireEnv('DyFM_TEST_REQUIRE_ENV_KEY', {
46
+ errorCode: 'TEST-001',
47
+ issuerService: 'spec',
48
+ });
49
+ expect(value).toBe('node-value');
50
+ });
51
+
52
+ it('| empty-string process.env value is treated as unset', (): void => {
53
+ // Truthy guard — empty string means "not configured".
54
+ process.env['DyFM_TEST_REQUIRE_ENV_KEY'] = '';
55
+ DyFM_global_settings.envOverrides = { 'DyFM_TEST_REQUIRE_ENV_KEY': 'browser-value' };
56
+ const value: string = DyFM_requireEnv('DyFM_TEST_REQUIRE_ENV_KEY', {
57
+ errorCode: 'TEST-002',
58
+ issuerService: 'spec',
59
+ });
60
+ expect(value).toBe('browser-value');
61
+ });
62
+ });
63
+
64
+
65
+ describe('| browser path (envOverrides)', (): void => {
66
+ it('| returns envOverrides value when process.env unset', (): void => {
67
+ DyFM_global_settings.envOverrides = { 'DyFM_TEST_REQUIRE_ENV_KEY': 'browser-value' };
68
+ const value: string = DyFM_requireEnv('DyFM_TEST_REQUIRE_ENV_KEY', {
69
+ errorCode: 'TEST-003',
70
+ issuerService: 'spec',
71
+ });
72
+ expect(value).toBe('browser-value');
73
+ });
74
+
75
+ it('| handles other entries in the same map without interference', (): void => {
76
+ DyFM_global_settings.envOverrides = {
77
+ 'DyFM_TEST_REQUIRE_ENV_KEY': 'value-a',
78
+ 'DyFM_TEST_REQUIRE_ENV_OTHER': 'value-b',
79
+ };
80
+ expect(DyFM_requireEnv('DyFM_TEST_REQUIRE_ENV_KEY', { errorCode: 'X', issuerService: 's' }))
81
+ .toBe('value-a');
82
+ expect(DyFM_requireEnv('DyFM_TEST_REQUIRE_ENV_OTHER', { errorCode: 'X', issuerService: 's' }))
83
+ .toBe('value-b');
84
+ });
85
+ });
86
+
87
+
88
+ describe('| fallback path', (): void => {
89
+ it('| returns fallback when neither process.env nor envOverrides resolves', (): void => {
90
+ const value: string = DyFM_requireEnv('DyFM_TEST_REQUIRE_ENV_KEY', {
91
+ errorCode: 'TEST-004',
92
+ issuerService: 'spec',
93
+ fallback: 'fallback-value',
94
+ });
95
+ expect(value).toBe('fallback-value');
96
+ });
97
+
98
+ it('| empty-string fallback is honored (explicit empty intent)', (): void => {
99
+ const value: string = DyFM_requireEnv('DyFM_TEST_REQUIRE_ENV_KEY', {
100
+ errorCode: 'TEST-005',
101
+ issuerService: 'spec',
102
+ fallback: '',
103
+ });
104
+ expect(value).toBe('');
105
+ });
106
+ });
107
+
108
+
109
+ describe('| throw path — rich DyFM_Error', (): void => {
110
+ it('| throws DyFM_Error when nothing resolves and no fallback', (): void => {
111
+ let caught: unknown = null;
112
+ try {
113
+ DyFM_requireEnv('DyFM_TEST_REQUIRE_ENV_KEY', {
114
+ errorCode: 'TEST-MISSING-001',
115
+ issuerService: 'dynamo-fsm-spec',
116
+ });
117
+ } catch (error: unknown) {
118
+ caught = error;
119
+ }
120
+ expect(caught).toBeInstanceOf(DyFM_Error);
121
+ });
122
+
123
+ it('| error has the caller-supplied errorCode', (): void => {
124
+ try {
125
+ DyFM_requireEnv('DyFM_TEST_REQUIRE_ENV_KEY', {
126
+ errorCode: 'CUSTOM-CODE-XYZ',
127
+ issuerService: 's',
128
+ });
129
+ fail('should have thrown');
130
+ } catch (error: unknown) {
131
+ const err: DyFM_Error = error as DyFM_Error;
132
+ expect(err._errorCode).toBe('CUSTOM-CODE-XYZ');
133
+ // Also present in the aggregated _errorCodes list.
134
+ expect(err._errorCodes).toContain('CUSTOM-CODE-XYZ');
135
+ }
136
+ });
137
+
138
+ it('| error has status 500 (config error)', (): void => {
139
+ try {
140
+ DyFM_requireEnv('DyFM_TEST_REQUIRE_ENV_KEY', {
141
+ errorCode: 'C', issuerService: 's',
142
+ });
143
+ fail('should have thrown');
144
+ } catch (error: unknown) {
145
+ const err: DyFM_Error = error as DyFM_Error;
146
+ expect(err.___status).toBe(500);
147
+ }
148
+ });
149
+
150
+ it('| error message names the env-var + lists deploy targets', (): void => {
151
+ try {
152
+ DyFM_requireEnv('SOME_SPECIFIC_VAR_NAME', {
153
+ errorCode: 'C', issuerService: 's',
154
+ });
155
+ fail('should have thrown');
156
+ } catch (error: unknown) {
157
+ const err: DyFM_Error = error as DyFM_Error;
158
+ const msg: string = err._message || '';
159
+ // Names the specific var
160
+ expect(msg).toContain('SOME_SPECIFIC_VAR_NAME');
161
+ // Lists each deploy target so an operator knows where to look
162
+ expect(msg).toContain('host .env');
163
+ expect(msg).toContain('docker-compose');
164
+ expect(msg).toContain('Overseer');
165
+ expect(msg).toContain('environment.ts');
166
+ }
167
+ });
168
+
169
+ it('| error has admin-actionable userMessage (custom or default)', (): void => {
170
+ // Default userMessage
171
+ try {
172
+ DyFM_requireEnv('DEFAULT_MSG_VAR', { errorCode: 'C', issuerService: 's' });
173
+ fail('should have thrown');
174
+ } catch (error: unknown) {
175
+ const err: DyFM_Error = error as DyFM_Error;
176
+ expect(err.__userMessage).toContain('DEFAULT_MSG_VAR');
177
+ expect(err.__userMessage).toContain('operator');
178
+ }
179
+ // Custom userMessage
180
+ try {
181
+ DyFM_requireEnv('CUSTOM_MSG_VAR', {
182
+ errorCode: 'C',
183
+ issuerService: 's',
184
+ userMessage: 'My custom admin instruction',
185
+ });
186
+ fail('should have thrown');
187
+ } catch (error: unknown) {
188
+ const err: DyFM_Error = error as DyFM_Error;
189
+ expect(err.__userMessage).toBe('My custom admin instruction');
190
+ }
191
+ });
192
+
193
+ it('| error has issuerService set', (): void => {
194
+ try {
195
+ DyFM_requireEnv('V', { errorCode: 'C', issuerService: 'my-package' });
196
+ fail('should have thrown');
197
+ } catch (error: unknown) {
198
+ const err: DyFM_Error = error as DyFM_Error;
199
+ expect(err.___issuerService).toBe('my-package');
200
+ }
201
+ });
202
+ });
203
+
204
+
205
+ describe('| lookup priority', (): void => {
206
+ it('| process.env wins over envOverrides', (): void => {
207
+ process.env['DyFM_TEST_REQUIRE_ENV_KEY'] = 'from-process';
208
+ DyFM_global_settings.envOverrides = { 'DyFM_TEST_REQUIRE_ENV_KEY': 'from-overrides' };
209
+ const value: string = DyFM_requireEnv('DyFM_TEST_REQUIRE_ENV_KEY', {
210
+ errorCode: 'C', issuerService: 's',
211
+ });
212
+ expect(value).toBe('from-process');
213
+ });
214
+
215
+ it('| envOverrides wins over fallback', (): void => {
216
+ DyFM_global_settings.envOverrides = { 'DyFM_TEST_REQUIRE_ENV_KEY': 'from-overrides' };
217
+ const value: string = DyFM_requireEnv('DyFM_TEST_REQUIRE_ENV_KEY', {
218
+ errorCode: 'C', issuerService: 's', fallback: 'from-fallback',
219
+ });
220
+ expect(value).toBe('from-overrides');
221
+ });
222
+
223
+ it('| process.env wins over fallback', (): void => {
224
+ process.env['DyFM_TEST_REQUIRE_ENV_KEY'] = 'from-process';
225
+ const value: string = DyFM_requireEnv('DyFM_TEST_REQUIRE_ENV_KEY', {
226
+ errorCode: 'C', issuerService: 's', fallback: 'from-fallback',
227
+ });
228
+ expect(value).toBe('from-process');
229
+ });
230
+ });
231
+ });
@@ -0,0 +1,138 @@
1
+ import { DyFM_Error } from '../../_models/control-models/error.control-model';
2
+ import { DyFM_global_settings } from '../constants/global-settings.const';
3
+
4
+
5
+ /**
6
+ * Options for `DyFM_requireEnv`. See the helper's JSDoc for usage.
7
+ */
8
+ export interface DyFM_RequireEnv_Options {
9
+ /**
10
+ * Stable error code emitted on the DyFM_Error when the env-var is missing
11
+ * and no fallback is configured. Convention: `<PACKAGE>-ENV-MISSING-<KEY>`,
12
+ * e.g. `DyNM-ENV-MISSING-STORAGE-KEY`, `FDP-ENV-MISSING-AUTH-KEY`.
13
+ */
14
+ errorCode: string;
15
+
16
+ /**
17
+ * Short-code name of the package responsible for the env-var. Used as the
18
+ * DyFM_Error `___issuerService` for downstream filtering / dashboards.
19
+ */
20
+ issuerService: string;
21
+
22
+ /**
23
+ * Transition fallback. When set, returned if BOTH `process.env[name]` AND
24
+ * `DyFM_global_settings.envOverrides[name]` are missing.
25
+ *
26
+ * Use only during the additive phase of an env-var migration to keep the
27
+ * package backward-compatible while every deploy target rolls the new
28
+ * env-var out. Remove the `fallback` argument once the rollout is verified
29
+ * everywhere — then a missing env-var fail-fast crashes the consumer at
30
+ * first access with a structured DyFM_Error.
31
+ */
32
+ fallback?: string;
33
+
34
+ /**
35
+ * Admin-actionable user-facing message attached to the DyFM_Error. Defaults
36
+ * to a generic config-missing string referencing the env-var name.
37
+ */
38
+ userMessage?: string;
39
+ }
40
+
41
+
42
+ /**
43
+ * FR-015 Phase 2 — isomorphic env-var requirement helper.
44
+ *
45
+ * Universal source of truth for "read this configuration key from an env-var
46
+ * across Node and browser". Replaces per-package `requireEnv` helpers (the
47
+ * Phase 1 local implementation in fdp-templates-nts is being refactored to
48
+ * delegate here).
49
+ *
50
+ * Lookup order:
51
+ * 1. `process.env[envVarName]` — Node-side (server const files at module load).
52
+ * 2. `DyFM_global_settings.envOverrides[envVarName]` — browser-side, populated
53
+ * by the consumer at app bootstrap from its `environment.ts`. See
54
+ * DyFM_Global_Settings.envOverrides docs for the pattern.
55
+ * 3. `options.fallback` if provided — used during migration's additive phase.
56
+ * 4. throw a structured DyFM_Error with the full deploy-target checklist.
57
+ *
58
+ * The throw shape is rich on purpose (per the rich-error rule):
59
+ * - `___status` = 500 (configuration error)
60
+ * - `_errorCode` = caller-supplied stable code
61
+ * - `_message` = debug-level description with the env-var name + deploy-target
62
+ * checklist (host .env, docker-compose env-block, Overseer secrets-store,
63
+ * client environment.ts → bootstrap envOverrides)
64
+ * - `__userMessage` = admin-actionable message
65
+ * - `___issuerService` = caller-supplied package short-code
66
+ *
67
+ * @example Node server const file (module load):
68
+ * ```ts
69
+ * import { DyFM_requireEnv } from '@futdevpro/fsm-dynamo';
70
+ *
71
+ * export const FDP_keysEnv_settingsBase = {
72
+ * authKey: DyFM_requireEnv('FDP_AUTH_KEY', {
73
+ * errorCode: 'FDP-ENV-MISSING-AUTH-KEY',
74
+ * issuerService: 'fdp-templates',
75
+ * fallback: '<inherit literal>', // remove for Wave 3
76
+ * }),
77
+ * };
78
+ * ```
79
+ *
80
+ * @example Browser-side lazy getter (deferred to first access, after consumer
81
+ * bootstrap has populated DyFM_global_settings.envOverrides):
82
+ * ```ts
83
+ * let _cached: string | undefined;
84
+ * function _resolveKey(): string {
85
+ * if (_cached !== undefined) return _cached;
86
+ * _cached = DyFM_requireEnv('DyNM_STORAGE_ENCRYPTION_KEY', { ... });
87
+ * return _cached;
88
+ * }
89
+ * export const DyNM_global_settings = {
90
+ * get storageEncryptionKey(): string { return _resolveKey(); },
91
+ * };
92
+ * ```
93
+ */
94
+ export function DyFM_requireEnv(
95
+ envVarName: string,
96
+ options: DyFM_RequireEnv_Options,
97
+ ): string {
98
+ // Node path: process.env at module-load time. Guarded for browser bundles
99
+ // where `process` may be undefined or shimmed without runtime values.
100
+ if (typeof process !== 'undefined' && process.env && process.env[envVarName]) {
101
+ return process.env[envVarName] as string;
102
+ }
103
+
104
+ // Browser path: consumer-populated envOverrides map. Populated at app
105
+ // bootstrap from environment.ts — see DyFM_Global_Settings.envOverrides.
106
+ const override: string | undefined =
107
+ DyFM_global_settings.envOverrides?.[envVarName];
108
+ if (override) {
109
+ return override;
110
+ }
111
+
112
+ // Transition fallback (Wave 1 of an FR-015-style migration).
113
+ if (options.fallback !== undefined) {
114
+ return options.fallback;
115
+ }
116
+
117
+ // Missing — fail fast with a rich, debug-friendly DyFM_Error.
118
+ throw new DyFM_Error({
119
+ status: 500,
120
+ errorCode: options.errorCode,
121
+ message:
122
+ `Environment variable '${envVarName}' is required but was not found. ` +
123
+ `Checked: process.env.${envVarName} (Node) and ` +
124
+ `DyFM_global_settings.envOverrides['${envVarName}'] (browser). ` +
125
+ `Both unset, and no transition fallback configured. ` +
126
+ `Set '${envVarName}' on every deploy target before deploying this ` +
127
+ `package version: (1) host .env on every host running a backend, ` +
128
+ `(2) docker-compose env-block on every consumer service, ` +
129
+ `(3) Overseer secrets-store entry for CI builds, ` +
130
+ `(4) client environment.ts plus app-bootstrap copy into ` +
131
+ `DyFM_global_settings.envOverrides for any browser consumer.`,
132
+ userMessage:
133
+ options.userMessage
134
+ ?? `Configuration error — environment variable '${envVarName}' is missing. `
135
+ + `Contact the responsible operator to set it on this deploy target.`,
136
+ issuerService: options.issuerService,
137
+ });
138
+ }
@@ -21,4 +21,27 @@ export interface DyFM_Global_Settings {
21
21
  * this setting will set which logs will be shown
22
22
  */
23
23
  log_settings: DyFM_GlobalLog_Settings;
24
+
25
+ /**
26
+ * FR-015 Phase 2 — isomorphic env-var overrides surface.
27
+ *
28
+ * On Node, `DyFM_requireEnv(name, ...)` reads `process.env[name]` directly.
29
+ * On browser there is no `process.env` at runtime, so consumers populate
30
+ * this map at app bootstrap from `environment.ts`:
31
+ *
32
+ * ```ts
33
+ * import { DyFM_global_settings } from '@futdevpro/fsm-dynamo';
34
+ * import { environment } from './environments/environment';
35
+ *
36
+ * DyFM_global_settings.envOverrides = {
37
+ * FDP_AUTH_KEY: environment.authKey,
38
+ * FDP_EXTRA_AUTH_STORAGE_KEY: environment.extraAuthStorageKey,
39
+ * DyNM_STORAGE_ENCRYPTION_KEY: environment.storageEncryptionKey,
40
+ * };
41
+ * ```
42
+ *
43
+ * `DyFM_requireEnv` checks `process.env` first, then this map, then
44
+ * the caller's optional `fallback`, then throws a structured DyFM_Error.
45
+ */
46
+ envOverrides?: Record<string, string>;
24
47
  }
package/src/index.ts CHANGED
@@ -15,6 +15,7 @@ export * from './_collections/utils/json-error-helper.util';
15
15
  export * from './_collections/utils/log.util';
16
16
  export * from './_collections/utils/round-list.util';
17
17
  export * from './_collections/utils/object.util';
18
+ export * from './_collections/utils/require-env.util';
18
19
  export * from './_collections/utils/stack.util';
19
20
  export * from './_collections/utils/string.util';
20
21
  export * from './_collections/utils/string-case.util';
@@ -1,124 +0,0 @@
1
- import { DyFM_Vector2 } from '../../../_models/interfaces/vector2.interface';
2
- import { DyFM_BoxBounds_Util } from './box-bounds.util';
3
- import { DyFM_Vector2_Util } from './vector2.util';
4
-
5
- xdescribe('| DyFM_BoxBounds', () => {
6
- let boxBounds: DyFM_BoxBounds_Util;
7
- const mockPosition = new DyFM_Vector2_Util({ x: 0, y: 0 });
8
- const mockSize = new DyFM_Vector2_Util({ x: 10, y: 10 });
9
-
10
- beforeEach(() => {
11
- boxBounds = new DyFM_BoxBounds_Util(mockPosition, mockSize);
12
- });
13
-
14
- it('| should set position correctly ()', () => {
15
- const newPosition = new DyFM_Vector2_Util({ x: 5, y: 5 });
16
-
17
- boxBounds.pos = newPosition;
18
- expect(boxBounds.pos).toEqual(newPosition);
19
- });
20
-
21
- it('| should get position correctly', () => {
22
- const expectedPosition = new DyFM_Vector2_Util({ x: 5, y: 5 });
23
-
24
- expect(boxBounds.pos).toEqual(expectedPosition);
25
- });
26
-
27
- it('| should set size correctly', () => {
28
- const newSize = new DyFM_Vector2_Util({ x: 20, y: 20 });
29
-
30
- boxBounds.size = newSize;
31
- expect(boxBounds.size).toEqual(newSize);
32
- });
33
-
34
- it('| should get size correctly', () => {
35
- const expectedSize = new DyFM_Vector2_Util({ x: 20, y: 20 });
36
-
37
- expect(boxBounds.size).toEqual(expectedSize);
38
- });
39
-
40
- xit('| should calculate center correctly', () => {
41
- const expectedCenter = new DyFM_Vector2_Util({ x: 10, y: 10 });
42
- expect(boxBounds.center).toEqual(expectedCenter);
43
- });
44
-
45
- it('| should calculate center margin correctly', () => {
46
- const expectedCenterMargin = new DyFM_Vector2_Util({ x: -5, y: -5 });
47
- expect(boxBounds.centerMargin).toEqual(expectedCenterMargin);
48
- });
49
-
50
- it('| should return true for constructed method', () => {
51
- expect(boxBounds.constructed()).toBe(true);
52
- });
53
-
54
- it('| should throw error when newValues has undefined position', () => {
55
- expect(() => {
56
- boxBounds.newValues(undefined as any, { x: 10, y: 10 });
57
- }).toThrow(Error);
58
- });
59
-
60
- it('| should throw error when newValues has undefined size', () => {
61
- expect(() => {
62
- boxBounds.newValues({ x: 0, y: 0 }, undefined as any);
63
- }).toThrow(Error);
64
- });
65
-
66
- it('| should clone and return a new instance', () => {
67
- const clonedBoxBounds = boxBounds.clone();
68
-
69
- expect(clonedBoxBounds).toBeInstanceOf(DyFM_BoxBounds_Util);
70
- expect(clonedBoxBounds).toEqual(boxBounds);
71
- });
72
-
73
-
74
- it('| should initialize with given position and size', () => {
75
- expect(boxBounds.pos.x).toBe(mockPosition.x);
76
- expect(boxBounds.pos.y).toBe(mockPosition.y);
77
- expect(boxBounds.size.x).toBe(mockSize.x);
78
- expect(boxBounds.size.y).toBe(mockSize.y);
79
- });
80
-
81
- it('| should calculate center correctly', () => {
82
- const expectedCenter = DyFM_Vector2_Util.plus(mockPosition, DyFM_Vector2_Util.divide(mockSize, 2));
83
- expect(boxBounds.center.x).toBe(expectedCenter.x);
84
- expect(boxBounds.center.y).toBe(expectedCenter.y);
85
- });
86
-
87
- it('| should update position and size with newValues method', () => {
88
- const newPosition: DyFM_Vector2 = { x: 50, y: 60 };
89
- const newSize: DyFM_Vector2 = { x: 70, y: 80 };
90
- boxBounds.newValues(newPosition, newSize);
91
-
92
- expect(boxBounds.pos.x).toBe(newPosition.x);
93
- expect(boxBounds.pos.y).toBe(newPosition.y);
94
- expect(boxBounds.size.x).toBe(newSize.x);
95
- expect(boxBounds.size.y).toBe(newSize.y);
96
- });
97
-
98
- it('| should throw error if newValues is called with undefined position', () => {
99
- expect(() => boxBounds.newValues(undefined as any, mockSize)).toThrowError('new position is undefined!');
100
- });
101
-
102
- it('| should throw error if newValues is called with undefined size', () => {
103
- expect(() => boxBounds.newValues(mockPosition, undefined as any)).toThrowError('new size is undefined!');
104
- });
105
-
106
- it('| should correctly determine if a position is within bounds', () => {
107
- const insidePosition: DyFM_Vector2 = { x: 20, y: 30 };
108
- const outsidePosition: DyFM_Vector2 = { x: 100, y: 100 };
109
-
110
- expect(boxBounds.bounds(insidePosition)).toBeTrue();
111
- expect(boxBounds.bounds(outsidePosition)).toBeFalse();
112
- });
113
-
114
- it('| should clone itself correctly', () => {
115
- const clone = boxBounds.clone();
116
- expect(clone.pos.x).toBe(boxBounds.pos.x);
117
- expect(clone.pos.y).toBe(boxBounds.pos.y);
118
- expect(clone.size.x).toBe(boxBounds.size.x);
119
- expect(clone.size.y).toBe(boxBounds.size.y);
120
- expect(clone).not.toBe(boxBounds);
121
- });
122
-
123
-
124
- });