@futdevpro/fsm-dynamo 1.15.14 → 1.15.16
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.
- package/build/_collections/utils/extract-error-message.util.d.ts +71 -0
- package/build/_collections/utils/extract-error-message.util.d.ts.map +1 -0
- package/build/_collections/utils/extract-error-message.util.js +186 -0
- package/build/_collections/utils/extract-error-message.util.js.map +1 -0
- package/build/_collections/utils/require-env.util.d.ts +3 -3
- package/build/_collections/utils/require-env.util.js +2 -2
- package/build/_models/interfaces/environment/global-settings.interface.d.ts +1 -1
- package/build/index.d.ts +1 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -0
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/_collections/utils/extract-error-message.util.spec.ts +204 -0
- package/src/_collections/utils/extract-error-message.util.ts +223 -0
- package/src/_collections/utils/require-env.util.ts +3 -3
- package/src/_models/interfaces/environment/global-settings.interface.ts +1 -1
- package/src/index.ts +1 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { DyFM_AnyError } from '../../_models/control-models/error.control-model';
|
|
2
|
+
/**
|
|
3
|
+
* Options for `DyFM_extractErrorMessage`.
|
|
4
|
+
*/
|
|
5
|
+
export interface DyFM_ExtractErrorMessage_Options {
|
|
6
|
+
/**
|
|
7
|
+
* Optional prefix prepended to the result with ': '. e.g. `prefix: 'Login'`
|
|
8
|
+
* → `'Login: <message>'`. Useful for distinguishing the UI surface.
|
|
9
|
+
*/
|
|
10
|
+
prefix?: string;
|
|
11
|
+
/**
|
|
12
|
+
* Fallback string when no usable message can be extracted. Defaults to
|
|
13
|
+
* a human-readable diagnostic that mentions checking server logs.
|
|
14
|
+
*/
|
|
15
|
+
fallback?: string;
|
|
16
|
+
/**
|
|
17
|
+
* When `true`, the returned string includes HTTP status + statusText after
|
|
18
|
+
* the main message (in parens) for HTTP errors. Default: `true`.
|
|
19
|
+
*/
|
|
20
|
+
includeHttpStatus?: boolean;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Universal error → human-readable string extractor. **Never** returns the
|
|
24
|
+
* literal string `'undefined'` (or `'undefined undefined'`), `'[object Object]'`,
|
|
25
|
+
* or any other JS-rendering artifact. Walks the full DyFM_Error pipeline plus
|
|
26
|
+
* common Angular HttpErrorResponse + plain Error shapes, falling back to a
|
|
27
|
+
* meaningful diagnostic string when the input is malformed.
|
|
28
|
+
*
|
|
29
|
+
* Order of attempts (first non-empty wins):
|
|
30
|
+
* 1. `err.__userMessage` (DyFM_Error admin-actionable user message)
|
|
31
|
+
* 2. `err._message` (DyFM_Error aggregated debug message)
|
|
32
|
+
* 3. `err.message` (plain Error.message)
|
|
33
|
+
* 4. `err.userMessage` (deprecated DyFM_Error_Settings field, still seen
|
|
34
|
+
* on some payloads)
|
|
35
|
+
* 5. `err.error.__userMessage` (HttpErrorResponse wrapping a DyFM_Error in
|
|
36
|
+
* its body)
|
|
37
|
+
* 6. `err.error._message`
|
|
38
|
+
* 7. `err.error.message`
|
|
39
|
+
* 8. `err.error.userMessage`
|
|
40
|
+
* 9. If `err` itself is a string, use as-is
|
|
41
|
+
* 10. JSON-stringified payload (circular-safe; first 200 chars) as a last
|
|
42
|
+
* resort, prefixed with 'Malformed error: '
|
|
43
|
+
* 11. `options.fallback` (or its default)
|
|
44
|
+
*
|
|
45
|
+
* When `includeHttpStatus` is true (default) and the input looks like a
|
|
46
|
+
* HttpErrorResponse, the status + statusText are appended in parens: e.g.
|
|
47
|
+
* `'Bad credentials (HTTP 401 Unauthorized)'`.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```ts
|
|
51
|
+
* try {
|
|
52
|
+
* await auth_CS.login(account);
|
|
53
|
+
* } catch (err) {
|
|
54
|
+
* this.formError = DyFM_extractErrorMessage(err, { prefix: 'Login' });
|
|
55
|
+
* // → 'Login: Bad credentials (HTTP 401 Unauthorized)'
|
|
56
|
+
* // OR
|
|
57
|
+
* // → 'Login: Network unreachable — check connection. Server logs may have detail.'
|
|
58
|
+
* // never 'Login: undefined undefined'
|
|
59
|
+
* }
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
export declare function DyFM_extractErrorMessage(err: DyFM_AnyError | unknown, options?: DyFM_ExtractErrorMessage_Options): string;
|
|
63
|
+
/**
|
|
64
|
+
* Convenience: same as DyFM_extractErrorMessage but always includes a default
|
|
65
|
+
* prefix derived from the calling context, useful for adding diagnostic
|
|
66
|
+
* breadcrumbs without per-call boilerplate.
|
|
67
|
+
*/
|
|
68
|
+
export declare function DyFM_formatError(err: DyFM_AnyError | unknown, prefix: string): string;
|
|
69
|
+
export declare function DyFM_registerMalformedErrorSink(fn: (err: unknown, context?: string) => void): void;
|
|
70
|
+
export declare function DyFM_reportMalformedError(err: unknown, context?: string): void;
|
|
71
|
+
//# sourceMappingURL=extract-error-message.util.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extract-error-message.util.d.ts","sourceRoot":"","sources":["../../../src/_collections/utils/extract-error-message.util.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAc,MAAM,kDAAkD,CAAC;AAG7F;;GAEG;AACH,MAAM,WAAW,gCAAgC;IAC/C;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;;OAGG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAGD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,wBAAgB,wBAAwB,CACtC,GAAG,EAAE,aAAa,GAAG,OAAO,EAC5B,OAAO,CAAC,EAAE,gCAAgC,GACzC,MAAM,CA2DR;AAqCD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,aAAa,GAAG,OAAO,EAC5B,MAAM,EAAE,MAAM,GACb,MAAM,CAER;AAiBD,wBAAgB,+BAA+B,CAC7C,EAAE,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,MAAM,KAAK,IAAI,GAC3C,IAAI,CAEN;AAED,wBAAgB,yBAAyB,CACvC,GAAG,EAAE,OAAO,EACZ,OAAO,CAAC,EAAE,MAAM,GACf,IAAI,CAoBN"}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DyFM_extractErrorMessage = DyFM_extractErrorMessage;
|
|
4
|
+
exports.DyFM_formatError = DyFM_formatError;
|
|
5
|
+
exports.DyFM_registerMalformedErrorSink = DyFM_registerMalformedErrorSink;
|
|
6
|
+
exports.DyFM_reportMalformedError = DyFM_reportMalformedError;
|
|
7
|
+
/**
|
|
8
|
+
* Universal error → human-readable string extractor. **Never** returns the
|
|
9
|
+
* literal string `'undefined'` (or `'undefined undefined'`), `'[object Object]'`,
|
|
10
|
+
* or any other JS-rendering artifact. Walks the full DyFM_Error pipeline plus
|
|
11
|
+
* common Angular HttpErrorResponse + plain Error shapes, falling back to a
|
|
12
|
+
* meaningful diagnostic string when the input is malformed.
|
|
13
|
+
*
|
|
14
|
+
* Order of attempts (first non-empty wins):
|
|
15
|
+
* 1. `err.__userMessage` (DyFM_Error admin-actionable user message)
|
|
16
|
+
* 2. `err._message` (DyFM_Error aggregated debug message)
|
|
17
|
+
* 3. `err.message` (plain Error.message)
|
|
18
|
+
* 4. `err.userMessage` (deprecated DyFM_Error_Settings field, still seen
|
|
19
|
+
* on some payloads)
|
|
20
|
+
* 5. `err.error.__userMessage` (HttpErrorResponse wrapping a DyFM_Error in
|
|
21
|
+
* its body)
|
|
22
|
+
* 6. `err.error._message`
|
|
23
|
+
* 7. `err.error.message`
|
|
24
|
+
* 8. `err.error.userMessage`
|
|
25
|
+
* 9. If `err` itself is a string, use as-is
|
|
26
|
+
* 10. JSON-stringified payload (circular-safe; first 200 chars) as a last
|
|
27
|
+
* resort, prefixed with 'Malformed error: '
|
|
28
|
+
* 11. `options.fallback` (or its default)
|
|
29
|
+
*
|
|
30
|
+
* When `includeHttpStatus` is true (default) and the input looks like a
|
|
31
|
+
* HttpErrorResponse, the status + statusText are appended in parens: e.g.
|
|
32
|
+
* `'Bad credentials (HTTP 401 Unauthorized)'`.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```ts
|
|
36
|
+
* try {
|
|
37
|
+
* await auth_CS.login(account);
|
|
38
|
+
* } catch (err) {
|
|
39
|
+
* this.formError = DyFM_extractErrorMessage(err, { prefix: 'Login' });
|
|
40
|
+
* // → 'Login: Bad credentials (HTTP 401 Unauthorized)'
|
|
41
|
+
* // OR
|
|
42
|
+
* // → 'Login: Network unreachable — check connection. Server logs may have detail.'
|
|
43
|
+
* // never 'Login: undefined undefined'
|
|
44
|
+
* }
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
function DyFM_extractErrorMessage(err, options) {
|
|
48
|
+
const includeHttpStatus = options?.includeHttpStatus !== false;
|
|
49
|
+
const fallback = options?.fallback
|
|
50
|
+
?? 'Unexpected error (no detail in payload — check server logs).';
|
|
51
|
+
const prefix = options?.prefix ? `${options.prefix}: ` : '';
|
|
52
|
+
// Null/undefined/empty
|
|
53
|
+
if (err === null || err === undefined) {
|
|
54
|
+
return `${prefix}${fallback}`;
|
|
55
|
+
}
|
|
56
|
+
// String → use as-is
|
|
57
|
+
if (typeof err === 'string') {
|
|
58
|
+
return `${prefix}${err}`;
|
|
59
|
+
}
|
|
60
|
+
// Walk the candidate fields in order. Cast to any to access optional
|
|
61
|
+
// properties without forcing each branch into a separate type guard.
|
|
62
|
+
const e = err;
|
|
63
|
+
const innerError = e?.error;
|
|
64
|
+
const candidate = _nonEmptyString(e?.__userMessage)
|
|
65
|
+
?? _nonEmptyString(e?._message)
|
|
66
|
+
?? _nonEmptyString(e?.message)
|
|
67
|
+
?? _nonEmptyString(e?.userMessage)
|
|
68
|
+
?? _nonEmptyString(innerError?.__userMessage)
|
|
69
|
+
?? _nonEmptyString(innerError?._message)
|
|
70
|
+
?? _nonEmptyString(innerError?.message)
|
|
71
|
+
?? _nonEmptyString(innerError?.userMessage);
|
|
72
|
+
// HTTP status suffix (always-on by default)
|
|
73
|
+
const status = e?.___status
|
|
74
|
+
?? e?.status
|
|
75
|
+
?? innerError?.___status
|
|
76
|
+
?? innerError?.status;
|
|
77
|
+
const statusText = _nonEmptyString(e?.statusText)
|
|
78
|
+
?? _nonEmptyString(innerError?.statusText);
|
|
79
|
+
const httpSuffix = (includeHttpStatus && (status !== undefined || statusText !== undefined))
|
|
80
|
+
? ` (HTTP ${status ?? '?'}${statusText ? ' ' + statusText : ''})`
|
|
81
|
+
: '';
|
|
82
|
+
if (candidate !== undefined) {
|
|
83
|
+
return `${prefix}${candidate}${httpSuffix}`;
|
|
84
|
+
}
|
|
85
|
+
// No usable text field. Try a JSON-stringify of the raw payload (circular-safe).
|
|
86
|
+
const jsonAttempt = _safeJsonStringify(err);
|
|
87
|
+
if (jsonAttempt !== undefined && jsonAttempt !== '{}' && jsonAttempt !== '[]') {
|
|
88
|
+
const trimmed = jsonAttempt.length > 200
|
|
89
|
+
? jsonAttempt.slice(0, 200) + '…'
|
|
90
|
+
: jsonAttempt;
|
|
91
|
+
return `${prefix}Malformed error: ${trimmed}${httpSuffix}`;
|
|
92
|
+
}
|
|
93
|
+
// Total fallback.
|
|
94
|
+
return `${prefix}${fallback}${httpSuffix}`;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Returns the value if it's a non-empty string, else undefined.
|
|
98
|
+
* Trims and treats whitespace-only as empty. Internal helper.
|
|
99
|
+
*/
|
|
100
|
+
function _nonEmptyString(v) {
|
|
101
|
+
if (typeof v !== 'string') {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
const trimmed = v.trim();
|
|
105
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* JSON.stringify wrapper that survives circular references + non-serializable
|
|
109
|
+
* values. Returns undefined if even the safe fallback fails (e.g. a Proxy
|
|
110
|
+
* whose getter throws).
|
|
111
|
+
*/
|
|
112
|
+
function _safeJsonStringify(v) {
|
|
113
|
+
try {
|
|
114
|
+
const seen = new WeakSet();
|
|
115
|
+
return JSON.stringify(v, (_key, value) => {
|
|
116
|
+
if (typeof value === 'object' && value !== null) {
|
|
117
|
+
if (seen.has(value)) {
|
|
118
|
+
return '[Circular]';
|
|
119
|
+
}
|
|
120
|
+
seen.add(value);
|
|
121
|
+
}
|
|
122
|
+
if (typeof value === 'function') {
|
|
123
|
+
return '[Function]';
|
|
124
|
+
}
|
|
125
|
+
if (typeof value === 'undefined') {
|
|
126
|
+
return '[undefined]';
|
|
127
|
+
}
|
|
128
|
+
return value;
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
try {
|
|
133
|
+
return String(v);
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Convenience: same as DyFM_extractErrorMessage but always includes a default
|
|
142
|
+
* prefix derived from the calling context, useful for adding diagnostic
|
|
143
|
+
* breadcrumbs without per-call boilerplate.
|
|
144
|
+
*/
|
|
145
|
+
function DyFM_formatError(err, prefix) {
|
|
146
|
+
return DyFM_extractErrorMessage(err, { prefix: prefix });
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Reports a raw error payload via the universal client-error capture system
|
|
150
|
+
* (FR-029-style). Implementation is delegated to a consumer-registered
|
|
151
|
+
* handler so dynamo-fsm stays Angular-agnostic; if no handler is registered
|
|
152
|
+
* the error is logged via `DyFM_Log.error` (which is the default no-op-safe
|
|
153
|
+
* sink). Consumers (e.g. A_ErrorHandler_ControlService) call
|
|
154
|
+
* `DyFM_registerMalformedErrorSink(fn)` once at bootstrap.
|
|
155
|
+
*
|
|
156
|
+
* The intent: when DyFM_extractErrorMessage falls back to 'Malformed error:'
|
|
157
|
+
* or the generic fallback, the raw payload should also be captured for
|
|
158
|
+
* post-mortem analysis (server error-log via FR-029 pipeline).
|
|
159
|
+
*/
|
|
160
|
+
let _malformedErrorSink;
|
|
161
|
+
function DyFM_registerMalformedErrorSink(fn) {
|
|
162
|
+
_malformedErrorSink = fn;
|
|
163
|
+
}
|
|
164
|
+
function DyFM_reportMalformedError(err, context) {
|
|
165
|
+
if (_malformedErrorSink !== undefined) {
|
|
166
|
+
try {
|
|
167
|
+
_malformedErrorSink(err, context);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
/* sink threw — fall through to default */
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// Default: best-effort console.error wrapped in DyFM_Error for stack capture.
|
|
175
|
+
try {
|
|
176
|
+
// Lazy require to avoid a hard cycle through error.control-model -> log.util.
|
|
177
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
178
|
+
const { DyFM_Log } = require('./log.util');
|
|
179
|
+
DyFM_Log.error(`[DyFM_reportMalformedError] ${context ?? 'unknown context'}`, err);
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
// Last-ditch — should never happen since log.util has no further deps.
|
|
183
|
+
/* swallow */
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
//# sourceMappingURL=extract-error-message.util.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extract-error-message.util.js","sourceRoot":"","sources":["../../../src/_collections/utils/extract-error-message.util.ts"],"names":[],"mappings":"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"}
|
|
@@ -5,7 +5,7 @@ export interface DyFM_RequireEnv_Options {
|
|
|
5
5
|
/**
|
|
6
6
|
* Stable error code emitted on the DyFM_Error when the env-var is missing
|
|
7
7
|
* and no fallback is configured. Convention: `<PACKAGE>-ENV-MISSING-<KEY>`,
|
|
8
|
-
* e.g. `DyNM-ENV-MISSING-STORAGE-KEY`, `FDP-ENV-MISSING-AUTH-KEY`.
|
|
8
|
+
* e.g. `DyNM-ENV-MISSING-STORAGE-KEY`, `FDP-ENV-MISSING-CORE-FE-AUTH-KEY`.
|
|
9
9
|
*/
|
|
10
10
|
errorCode: string;
|
|
11
11
|
/**
|
|
@@ -60,8 +60,8 @@ export interface DyFM_RequireEnv_Options {
|
|
|
60
60
|
* import { DyFM_requireEnv } from '@futdevpro/fsm-dynamo';
|
|
61
61
|
*
|
|
62
62
|
* export const FDP_keysEnv_settingsBase = {
|
|
63
|
-
* authKey: DyFM_requireEnv('
|
|
64
|
-
* errorCode: 'FDP-ENV-MISSING-AUTH-KEY',
|
|
63
|
+
* authKey: DyFM_requireEnv('FDP_CORE_FE_AUTH_KEY', {
|
|
64
|
+
* errorCode: 'FDP-ENV-MISSING-CORE-FE-AUTH-KEY',
|
|
65
65
|
* issuerService: 'fdp-templates',
|
|
66
66
|
* fallback: '<inherit literal>', // remove for Wave 3
|
|
67
67
|
* }),
|
|
@@ -33,8 +33,8 @@ const global_settings_const_1 = require("../constants/global-settings.const");
|
|
|
33
33
|
* import { DyFM_requireEnv } from '@futdevpro/fsm-dynamo';
|
|
34
34
|
*
|
|
35
35
|
* export const FDP_keysEnv_settingsBase = {
|
|
36
|
-
* authKey: DyFM_requireEnv('
|
|
37
|
-
* errorCode: 'FDP-ENV-MISSING-AUTH-KEY',
|
|
36
|
+
* authKey: DyFM_requireEnv('FDP_CORE_FE_AUTH_KEY', {
|
|
37
|
+
* errorCode: 'FDP-ENV-MISSING-CORE-FE-AUTH-KEY',
|
|
38
38
|
* issuerService: 'fdp-templates',
|
|
39
39
|
* fallback: '<inherit literal>', // remove for Wave 3
|
|
40
40
|
* }),
|
|
@@ -29,7 +29,7 @@ export interface DyFM_Global_Settings {
|
|
|
29
29
|
* import { environment } from './environments/environment';
|
|
30
30
|
*
|
|
31
31
|
* DyFM_global_settings.envOverrides = {
|
|
32
|
-
*
|
|
32
|
+
* FDP_CORE_FE_AUTH_KEY: environment.authKey,
|
|
33
33
|
* FDP_EXTRA_AUTH_STORAGE_KEY: environment.extraAuthStorageKey,
|
|
34
34
|
* DyNM_STORAGE_ENCRYPTION_KEY: environment.storageEncryptionKey,
|
|
35
35
|
* };
|
package/build/index.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export * from './_collections/constants/times.const';
|
|
|
6
6
|
export * from './_collections/utils/array.util';
|
|
7
7
|
export * from './_collections/utils/async.util';
|
|
8
8
|
export * from './_collections/utils/data.util';
|
|
9
|
+
export * from './_collections/utils/extract-error-message.util';
|
|
9
10
|
export * from './_collections/utils/json-error-helper.util';
|
|
10
11
|
export * from './_collections/utils/log.util';
|
|
11
12
|
export * from './_collections/utils/round-list.util';
|
package/build/index.d.ts.map
CHANGED
|
@@ -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,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"}
|
|
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,iDAAiD,CAAC;AAChE,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
|
@@ -12,6 +12,7 @@ tslib_1.__exportStar(require("./_collections/constants/times.const"), exports);
|
|
|
12
12
|
tslib_1.__exportStar(require("./_collections/utils/array.util"), exports);
|
|
13
13
|
tslib_1.__exportStar(require("./_collections/utils/async.util"), exports);
|
|
14
14
|
tslib_1.__exportStar(require("./_collections/utils/data.util"), exports);
|
|
15
|
+
tslib_1.__exportStar(require("./_collections/utils/extract-error-message.util"), exports);
|
|
15
16
|
tslib_1.__exportStar(require("./_collections/utils/json-error-helper.util"), exports);
|
|
16
17
|
tslib_1.__exportStar(require("./_collections/utils/log.util"), exports);
|
|
17
18
|
tslib_1.__exportStar(require("./_collections/utils/round-list.util"), exports);
|
package/build/index.js.map
CHANGED
|
@@ -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,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"}
|
|
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,0FAAgE;AAChE,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.
|
|
3
|
+
"version": "01.15.16",
|
|
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",
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { DyFM_Error } from '../../_models/control-models/error.control-model';
|
|
2
|
+
import {
|
|
3
|
+
DyFM_extractErrorMessage,
|
|
4
|
+
DyFM_formatError,
|
|
5
|
+
DyFM_registerMalformedErrorSink,
|
|
6
|
+
DyFM_reportMalformedError,
|
|
7
|
+
} from './extract-error-message.util';
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* FR-015-P0-fix — DyFM_extractErrorMessage specs.
|
|
12
|
+
*
|
|
13
|
+
* Coverage:
|
|
14
|
+
* - DyFM_Error with __userMessage / _message / both
|
|
15
|
+
* - HttpErrorResponse-shaped (status + statusText + error body)
|
|
16
|
+
* - Plain Error.message
|
|
17
|
+
* - String input
|
|
18
|
+
* - null / undefined → fallback (never 'undefined undefined')
|
|
19
|
+
* - Empty object → 'Malformed error: {}' OR fallback (depending on json)
|
|
20
|
+
* - Circular reference handled (no infinite loop / no crash)
|
|
21
|
+
* - prefix option
|
|
22
|
+
* - includeHttpStatus toggle
|
|
23
|
+
* - DyFM_formatError convenience wrapper
|
|
24
|
+
* - DyFM_reportMalformedError sink registration
|
|
25
|
+
*
|
|
26
|
+
* MUST never emit:
|
|
27
|
+
* - literal 'undefined' anywhere in the returned string
|
|
28
|
+
* - '[object Object]'
|
|
29
|
+
* - bare empty string
|
|
30
|
+
*/
|
|
31
|
+
describe('| DyFM_extractErrorMessage', (): void => {
|
|
32
|
+
|
|
33
|
+
describe('| DyFM_Error inputs', (): void => {
|
|
34
|
+
it('| uses __userMessage when present', (): void => {
|
|
35
|
+
const err: DyFM_Error = new DyFM_Error({
|
|
36
|
+
status: 401,
|
|
37
|
+
message: 'Internal debug',
|
|
38
|
+
userMessage: 'Bad credentials',
|
|
39
|
+
errorCode: 'AUTH-001',
|
|
40
|
+
});
|
|
41
|
+
// userMessage from settings maps to __userMessage on the DyFM_Error instance
|
|
42
|
+
// (deprecated but still copied).
|
|
43
|
+
const result: string = DyFM_extractErrorMessage(err);
|
|
44
|
+
expect(result).toContain('Bad credentials');
|
|
45
|
+
expect(result).not.toContain('undefined');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('| falls back to _message when no __userMessage', (): void => {
|
|
49
|
+
const err: DyFM_Error = new DyFM_Error({
|
|
50
|
+
status: 500,
|
|
51
|
+
message: 'Database connection lost',
|
|
52
|
+
errorCode: 'DB-001',
|
|
53
|
+
});
|
|
54
|
+
const result: string = DyFM_extractErrorMessage(err);
|
|
55
|
+
expect(result).toContain('Database connection lost');
|
|
56
|
+
expect(result).not.toContain('undefined');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('| appends HTTP status when present', (): void => {
|
|
60
|
+
const err: DyFM_Error = new DyFM_Error({
|
|
61
|
+
status: 401,
|
|
62
|
+
message: 'Token expired',
|
|
63
|
+
errorCode: 'AUTH-002',
|
|
64
|
+
});
|
|
65
|
+
const result: string = DyFM_extractErrorMessage(err);
|
|
66
|
+
expect(result).toContain('Token expired');
|
|
67
|
+
expect(result).toContain('401');
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('| HttpErrorResponse-shaped inputs', (): void => {
|
|
72
|
+
it('| extracts from nested .error.__userMessage', (): void => {
|
|
73
|
+
const httpErr: object = {
|
|
74
|
+
status: 401,
|
|
75
|
+
statusText: 'Unauthorized',
|
|
76
|
+
error: {
|
|
77
|
+
__userMessage: 'Invalid username or password',
|
|
78
|
+
_errorCode: 'FAS-ACS-LOG1',
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
const result: string = DyFM_extractErrorMessage(httpErr);
|
|
82
|
+
expect(result).toContain('Invalid username or password');
|
|
83
|
+
expect(result).toContain('401');
|
|
84
|
+
expect(result).toContain('Unauthorized');
|
|
85
|
+
expect(result).not.toContain('undefined');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('| extracts from nested .error.message (no DyFM extras)', (): void => {
|
|
89
|
+
const httpErr: object = {
|
|
90
|
+
status: 500,
|
|
91
|
+
statusText: 'Internal Server Error',
|
|
92
|
+
error: { message: 'ECONNREFUSED' },
|
|
93
|
+
};
|
|
94
|
+
const result: string = DyFM_extractErrorMessage(httpErr);
|
|
95
|
+
expect(result).toContain('ECONNREFUSED');
|
|
96
|
+
expect(result).toContain('500');
|
|
97
|
+
expect(result).not.toContain('undefined');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('| status without statusText still rendered cleanly', (): void => {
|
|
101
|
+
const httpErr: object = {
|
|
102
|
+
status: 502,
|
|
103
|
+
error: { __userMessage: 'Upstream down' },
|
|
104
|
+
};
|
|
105
|
+
const result: string = DyFM_extractErrorMessage(httpErr);
|
|
106
|
+
expect(result).toContain('Upstream down');
|
|
107
|
+
expect(result).toContain('502');
|
|
108
|
+
expect(result).not.toContain('undefined');
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe('| primitive + edge inputs (the "undefined undefined" guard)', (): void => {
|
|
113
|
+
it('| null → fallback, never literal "undefined"', (): void => {
|
|
114
|
+
const result: string = DyFM_extractErrorMessage(null);
|
|
115
|
+
expect(result).not.toContain('undefined');
|
|
116
|
+
expect(result.length).toBeGreaterThan(0);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('| undefined → fallback', (): void => {
|
|
120
|
+
const result: string = DyFM_extractErrorMessage(undefined);
|
|
121
|
+
expect(result).not.toContain('undefined');
|
|
122
|
+
expect(result.length).toBeGreaterThan(0);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('| string input returned as-is', (): void => {
|
|
126
|
+
const result: string = DyFM_extractErrorMessage('Network unreachable');
|
|
127
|
+
expect(result).toBe('Network unreachable');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('| plain Error.message', (): void => {
|
|
131
|
+
const result: string = DyFM_extractErrorMessage(new Error('boom'));
|
|
132
|
+
expect(result).toContain('boom');
|
|
133
|
+
expect(result).not.toContain('undefined');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('| empty object → "Malformed error: {}" or fallback, NEVER bare undefined', (): void => {
|
|
137
|
+
const result: string = DyFM_extractErrorMessage({});
|
|
138
|
+
expect(result.length).toBeGreaterThan(0);
|
|
139
|
+
expect(result).not.toContain('undefined undefined');
|
|
140
|
+
// Either Malformed-error report or fallback — both are acceptable.
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('| HttpErrorResponse with ALL undefined fields → fallback, never literal "undefined undefined"', (): void => {
|
|
144
|
+
// This is the EXACT shape that produced the bug today:
|
|
145
|
+
// errorRes?.status, errorRes?.error?.status, errorRes?.statusText,
|
|
146
|
+
// errorRes?.error?.statusText all undefined.
|
|
147
|
+
const httpErr: object = { error: {} };
|
|
148
|
+
const result: string = DyFM_extractErrorMessage(httpErr, { prefix: 'Login' });
|
|
149
|
+
expect(result.startsWith('Login:')).toBeTrue();
|
|
150
|
+
expect(result).not.toContain('undefined');
|
|
151
|
+
expect(result).not.toContain('[object Object]');
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe('| circular reference safety', (): void => {
|
|
156
|
+
it('| does not crash + does not loop', (): void => {
|
|
157
|
+
const a: { name: string; self?: object } = { name: 'a' };
|
|
158
|
+
a.self = a;
|
|
159
|
+
const result: string = DyFM_extractErrorMessage(a);
|
|
160
|
+
expect(result.length).toBeGreaterThan(0);
|
|
161
|
+
expect(result).not.toContain('undefined');
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe('| prefix + includeHttpStatus options', (): void => {
|
|
166
|
+
it('| prefix prepends with ": "', (): void => {
|
|
167
|
+
const result: string = DyFM_extractErrorMessage('boom', { prefix: 'Login' });
|
|
168
|
+
expect(result).toBe('Login: boom');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('| includeHttpStatus=false skips the (HTTP …) suffix', (): void => {
|
|
172
|
+
const httpErr: object = { status: 500, statusText: 'X', error: { message: 'm' } };
|
|
173
|
+
const result: string = DyFM_extractErrorMessage(httpErr, { includeHttpStatus: false });
|
|
174
|
+
expect(result).toContain('m');
|
|
175
|
+
expect(result).not.toContain('HTTP');
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('| custom fallback honored', (): void => {
|
|
179
|
+
const result: string = DyFM_extractErrorMessage(null, { fallback: 'Custom retry message' });
|
|
180
|
+
expect(result).toContain('Custom retry message');
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe('| DyFM_formatError convenience wrapper', (): void => {
|
|
185
|
+
it('| applies prefix', (): void => {
|
|
186
|
+
const result: string = DyFM_formatError('boom', 'Register');
|
|
187
|
+
expect(result).toBe('Register: boom');
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
describe('| DyFM_reportMalformedError sink', (): void => {
|
|
192
|
+
it('| calls registered sink with err + context', (): void => {
|
|
193
|
+
const calls: Array<{ err: unknown; context?: string }> = [];
|
|
194
|
+
DyFM_registerMalformedErrorSink((err: unknown, context?: string): void => {
|
|
195
|
+
calls.push({ err: err, context: context });
|
|
196
|
+
});
|
|
197
|
+
DyFM_reportMalformedError({ weirdShape: true }, 'login-flow');
|
|
198
|
+
expect(calls.length).toBe(1);
|
|
199
|
+
expect(calls[0].context).toBe('login-flow');
|
|
200
|
+
// Reset sink (no public unregister; pass an explicit no-op for cleanup).
|
|
201
|
+
DyFM_registerMalformedErrorSink((): void => { /* no-op */ });
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
});
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { DyFM_AnyError, DyFM_Error } from '../../_models/control-models/error.control-model';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Options for `DyFM_extractErrorMessage`.
|
|
6
|
+
*/
|
|
7
|
+
export interface DyFM_ExtractErrorMessage_Options {
|
|
8
|
+
/**
|
|
9
|
+
* Optional prefix prepended to the result with ': '. e.g. `prefix: 'Login'`
|
|
10
|
+
* → `'Login: <message>'`. Useful for distinguishing the UI surface.
|
|
11
|
+
*/
|
|
12
|
+
prefix?: string;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Fallback string when no usable message can be extracted. Defaults to
|
|
16
|
+
* a human-readable diagnostic that mentions checking server logs.
|
|
17
|
+
*/
|
|
18
|
+
fallback?: string;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* When `true`, the returned string includes HTTP status + statusText after
|
|
22
|
+
* the main message (in parens) for HTTP errors. Default: `true`.
|
|
23
|
+
*/
|
|
24
|
+
includeHttpStatus?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Universal error → human-readable string extractor. **Never** returns the
|
|
30
|
+
* literal string `'undefined'` (or `'undefined undefined'`), `'[object Object]'`,
|
|
31
|
+
* or any other JS-rendering artifact. Walks the full DyFM_Error pipeline plus
|
|
32
|
+
* common Angular HttpErrorResponse + plain Error shapes, falling back to a
|
|
33
|
+
* meaningful diagnostic string when the input is malformed.
|
|
34
|
+
*
|
|
35
|
+
* Order of attempts (first non-empty wins):
|
|
36
|
+
* 1. `err.__userMessage` (DyFM_Error admin-actionable user message)
|
|
37
|
+
* 2. `err._message` (DyFM_Error aggregated debug message)
|
|
38
|
+
* 3. `err.message` (plain Error.message)
|
|
39
|
+
* 4. `err.userMessage` (deprecated DyFM_Error_Settings field, still seen
|
|
40
|
+
* on some payloads)
|
|
41
|
+
* 5. `err.error.__userMessage` (HttpErrorResponse wrapping a DyFM_Error in
|
|
42
|
+
* its body)
|
|
43
|
+
* 6. `err.error._message`
|
|
44
|
+
* 7. `err.error.message`
|
|
45
|
+
* 8. `err.error.userMessage`
|
|
46
|
+
* 9. If `err` itself is a string, use as-is
|
|
47
|
+
* 10. JSON-stringified payload (circular-safe; first 200 chars) as a last
|
|
48
|
+
* resort, prefixed with 'Malformed error: '
|
|
49
|
+
* 11. `options.fallback` (or its default)
|
|
50
|
+
*
|
|
51
|
+
* When `includeHttpStatus` is true (default) and the input looks like a
|
|
52
|
+
* HttpErrorResponse, the status + statusText are appended in parens: e.g.
|
|
53
|
+
* `'Bad credentials (HTTP 401 Unauthorized)'`.
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```ts
|
|
57
|
+
* try {
|
|
58
|
+
* await auth_CS.login(account);
|
|
59
|
+
* } catch (err) {
|
|
60
|
+
* this.formError = DyFM_extractErrorMessage(err, { prefix: 'Login' });
|
|
61
|
+
* // → 'Login: Bad credentials (HTTP 401 Unauthorized)'
|
|
62
|
+
* // OR
|
|
63
|
+
* // → 'Login: Network unreachable — check connection. Server logs may have detail.'
|
|
64
|
+
* // never 'Login: undefined undefined'
|
|
65
|
+
* }
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
export function DyFM_extractErrorMessage(
|
|
69
|
+
err: DyFM_AnyError | unknown,
|
|
70
|
+
options?: DyFM_ExtractErrorMessage_Options,
|
|
71
|
+
): string {
|
|
72
|
+
const includeHttpStatus: boolean = options?.includeHttpStatus !== false;
|
|
73
|
+
const fallback: string = options?.fallback
|
|
74
|
+
?? 'Unexpected error (no detail in payload — check server logs).';
|
|
75
|
+
const prefix: string = options?.prefix ? `${options.prefix}: ` : '';
|
|
76
|
+
|
|
77
|
+
// Null/undefined/empty
|
|
78
|
+
if (err === null || err === undefined) {
|
|
79
|
+
return `${prefix}${fallback}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// String → use as-is
|
|
83
|
+
if (typeof err === 'string') {
|
|
84
|
+
return `${prefix}${err}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Walk the candidate fields in order. Cast to any to access optional
|
|
88
|
+
// properties without forcing each branch into a separate type guard.
|
|
89
|
+
const e: any = err as any;
|
|
90
|
+
const innerError: any = e?.error;
|
|
91
|
+
|
|
92
|
+
const candidate: string | undefined =
|
|
93
|
+
_nonEmptyString(e?.__userMessage)
|
|
94
|
+
?? _nonEmptyString(e?._message)
|
|
95
|
+
?? _nonEmptyString(e?.message)
|
|
96
|
+
?? _nonEmptyString(e?.userMessage)
|
|
97
|
+
?? _nonEmptyString(innerError?.__userMessage)
|
|
98
|
+
?? _nonEmptyString(innerError?._message)
|
|
99
|
+
?? _nonEmptyString(innerError?.message)
|
|
100
|
+
?? _nonEmptyString(innerError?.userMessage);
|
|
101
|
+
|
|
102
|
+
// HTTP status suffix (always-on by default)
|
|
103
|
+
const status: number | string | undefined =
|
|
104
|
+
e?.___status
|
|
105
|
+
?? e?.status
|
|
106
|
+
?? innerError?.___status
|
|
107
|
+
?? innerError?.status;
|
|
108
|
+
const statusText: string | undefined =
|
|
109
|
+
_nonEmptyString(e?.statusText)
|
|
110
|
+
?? _nonEmptyString(innerError?.statusText);
|
|
111
|
+
const httpSuffix: string = (includeHttpStatus && (status !== undefined || statusText !== undefined))
|
|
112
|
+
? ` (HTTP ${status ?? '?'}${statusText ? ' ' + statusText : ''})`
|
|
113
|
+
: '';
|
|
114
|
+
|
|
115
|
+
if (candidate !== undefined) {
|
|
116
|
+
return `${prefix}${candidate}${httpSuffix}`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// No usable text field. Try a JSON-stringify of the raw payload (circular-safe).
|
|
120
|
+
const jsonAttempt: string | undefined = _safeJsonStringify(err);
|
|
121
|
+
if (jsonAttempt !== undefined && jsonAttempt !== '{}' && jsonAttempt !== '[]') {
|
|
122
|
+
const trimmed: string = jsonAttempt.length > 200
|
|
123
|
+
? jsonAttempt.slice(0, 200) + '…'
|
|
124
|
+
: jsonAttempt;
|
|
125
|
+
return `${prefix}Malformed error: ${trimmed}${httpSuffix}`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Total fallback.
|
|
129
|
+
return `${prefix}${fallback}${httpSuffix}`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Returns the value if it's a non-empty string, else undefined.
|
|
135
|
+
* Trims and treats whitespace-only as empty. Internal helper.
|
|
136
|
+
*/
|
|
137
|
+
function _nonEmptyString(v: unknown): string | undefined {
|
|
138
|
+
if (typeof v !== 'string') { return undefined; }
|
|
139
|
+
const trimmed: string = v.trim();
|
|
140
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* JSON.stringify wrapper that survives circular references + non-serializable
|
|
146
|
+
* values. Returns undefined if even the safe fallback fails (e.g. a Proxy
|
|
147
|
+
* whose getter throws).
|
|
148
|
+
*/
|
|
149
|
+
function _safeJsonStringify(v: unknown): string | undefined {
|
|
150
|
+
try {
|
|
151
|
+
const seen: WeakSet<object> = new WeakSet<object>();
|
|
152
|
+
return JSON.stringify(v, (_key: string, value: unknown): unknown => {
|
|
153
|
+
if (typeof value === 'object' && value !== null) {
|
|
154
|
+
if (seen.has(value as object)) { return '[Circular]'; }
|
|
155
|
+
seen.add(value as object);
|
|
156
|
+
}
|
|
157
|
+
if (typeof value === 'function') { return '[Function]'; }
|
|
158
|
+
if (typeof value === 'undefined') { return '[undefined]'; }
|
|
159
|
+
return value;
|
|
160
|
+
});
|
|
161
|
+
} catch {
|
|
162
|
+
try { return String(v); } catch { return undefined; }
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Convenience: same as DyFM_extractErrorMessage but always includes a default
|
|
169
|
+
* prefix derived from the calling context, useful for adding diagnostic
|
|
170
|
+
* breadcrumbs without per-call boilerplate.
|
|
171
|
+
*/
|
|
172
|
+
export function DyFM_formatError(
|
|
173
|
+
err: DyFM_AnyError | unknown,
|
|
174
|
+
prefix: string,
|
|
175
|
+
): string {
|
|
176
|
+
return DyFM_extractErrorMessage(err, { prefix: prefix });
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Reports a raw error payload via the universal client-error capture system
|
|
182
|
+
* (FR-029-style). Implementation is delegated to a consumer-registered
|
|
183
|
+
* handler so dynamo-fsm stays Angular-agnostic; if no handler is registered
|
|
184
|
+
* the error is logged via `DyFM_Log.error` (which is the default no-op-safe
|
|
185
|
+
* sink). Consumers (e.g. A_ErrorHandler_ControlService) call
|
|
186
|
+
* `DyFM_registerMalformedErrorSink(fn)` once at bootstrap.
|
|
187
|
+
*
|
|
188
|
+
* The intent: when DyFM_extractErrorMessage falls back to 'Malformed error:'
|
|
189
|
+
* or the generic fallback, the raw payload should also be captured for
|
|
190
|
+
* post-mortem analysis (server error-log via FR-029 pipeline).
|
|
191
|
+
*/
|
|
192
|
+
let _malformedErrorSink: ((err: unknown, context?: string) => void) | undefined;
|
|
193
|
+
|
|
194
|
+
export function DyFM_registerMalformedErrorSink(
|
|
195
|
+
fn: (err: unknown, context?: string) => void,
|
|
196
|
+
): void {
|
|
197
|
+
_malformedErrorSink = fn;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function DyFM_reportMalformedError(
|
|
201
|
+
err: unknown,
|
|
202
|
+
context?: string,
|
|
203
|
+
): void {
|
|
204
|
+
if (_malformedErrorSink !== undefined) {
|
|
205
|
+
try {
|
|
206
|
+
_malformedErrorSink(err, context);
|
|
207
|
+
return;
|
|
208
|
+
} catch {
|
|
209
|
+
/* sink threw — fall through to default */
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
// Default: best-effort console.error wrapped in DyFM_Error for stack capture.
|
|
213
|
+
try {
|
|
214
|
+
// Lazy require to avoid a hard cycle through error.control-model -> log.util.
|
|
215
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
216
|
+
const { DyFM_Log }: { DyFM_Log: { error(msg: string, ...args: unknown[]): void } } =
|
|
217
|
+
require('./log.util');
|
|
218
|
+
DyFM_Log.error(`[DyFM_reportMalformedError] ${context ?? 'unknown context'}`, err);
|
|
219
|
+
} catch {
|
|
220
|
+
// Last-ditch — should never happen since log.util has no further deps.
|
|
221
|
+
/* swallow */
|
|
222
|
+
}
|
|
223
|
+
}
|
|
@@ -9,7 +9,7 @@ export interface DyFM_RequireEnv_Options {
|
|
|
9
9
|
/**
|
|
10
10
|
* Stable error code emitted on the DyFM_Error when the env-var is missing
|
|
11
11
|
* and no fallback is configured. Convention: `<PACKAGE>-ENV-MISSING-<KEY>`,
|
|
12
|
-
* e.g. `DyNM-ENV-MISSING-STORAGE-KEY`, `FDP-ENV-MISSING-AUTH-KEY`.
|
|
12
|
+
* e.g. `DyNM-ENV-MISSING-STORAGE-KEY`, `FDP-ENV-MISSING-CORE-FE-AUTH-KEY`.
|
|
13
13
|
*/
|
|
14
14
|
errorCode: string;
|
|
15
15
|
|
|
@@ -69,8 +69,8 @@ export interface DyFM_RequireEnv_Options {
|
|
|
69
69
|
* import { DyFM_requireEnv } from '@futdevpro/fsm-dynamo';
|
|
70
70
|
*
|
|
71
71
|
* export const FDP_keysEnv_settingsBase = {
|
|
72
|
-
* authKey: DyFM_requireEnv('
|
|
73
|
-
* errorCode: 'FDP-ENV-MISSING-AUTH-KEY',
|
|
72
|
+
* authKey: DyFM_requireEnv('FDP_CORE_FE_AUTH_KEY', {
|
|
73
|
+
* errorCode: 'FDP-ENV-MISSING-CORE-FE-AUTH-KEY',
|
|
74
74
|
* issuerService: 'fdp-templates',
|
|
75
75
|
* fallback: '<inherit literal>', // remove for Wave 3
|
|
76
76
|
* }),
|
|
@@ -34,7 +34,7 @@ export interface DyFM_Global_Settings {
|
|
|
34
34
|
* import { environment } from './environments/environment';
|
|
35
35
|
*
|
|
36
36
|
* DyFM_global_settings.envOverrides = {
|
|
37
|
-
*
|
|
37
|
+
* FDP_CORE_FE_AUTH_KEY: environment.authKey,
|
|
38
38
|
* FDP_EXTRA_AUTH_STORAGE_KEY: environment.extraAuthStorageKey,
|
|
39
39
|
* DyNM_STORAGE_ENCRYPTION_KEY: environment.storageEncryptionKey,
|
|
40
40
|
* };
|
package/src/index.ts
CHANGED
|
@@ -11,6 +11,7 @@ export * from './_collections/constants/times.const';
|
|
|
11
11
|
export * from './_collections/utils/array.util';
|
|
12
12
|
export * from './_collections/utils/async.util';
|
|
13
13
|
export * from './_collections/utils/data.util';
|
|
14
|
+
export * from './_collections/utils/extract-error-message.util';
|
|
14
15
|
export * from './_collections/utils/json-error-helper.util';
|
|
15
16
|
export * from './_collections/utils/log.util';
|
|
16
17
|
export * from './_collections/utils/round-list.util';
|