@seedgrid/fe-core 2026.6.3 → 2026.6.4
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.
|
@@ -661,7 +661,7 @@
|
|
|
661
661
|
"displayName": "API Error Message Extractor",
|
|
662
662
|
"category": "utility",
|
|
663
663
|
"subcategory": "http",
|
|
664
|
-
"description": "Reads API problem payloads and extracts the best user-facing message, prioritizing RFC7807 violations before
|
|
664
|
+
"description": "Reads API problem payloads and extracts the best user-facing message, prioritizing RFC7807 violations before userMessage/detail/title. Walks the whole error chain (responseBody/cause/error/data/body) so it works even when the problem payload is nested, rewrapped, or has crossed a bundle boundary.",
|
|
665
665
|
"tags": [
|
|
666
666
|
"api",
|
|
667
667
|
"http",
|
|
@@ -673,8 +673,10 @@
|
|
|
673
673
|
"api-client-error",
|
|
674
674
|
"rfc7807",
|
|
675
675
|
"violations",
|
|
676
|
+
"user-message",
|
|
676
677
|
"error-message",
|
|
677
|
-
"json-string-body"
|
|
678
|
+
"json-string-body",
|
|
679
|
+
"nested-error-chain"
|
|
678
680
|
],
|
|
679
681
|
"fieldSemantics": [
|
|
680
682
|
"error",
|
|
@@ -1,9 +1,29 @@
|
|
|
1
|
-
|
|
1
|
+
type ApiClientErrorLike = {
|
|
2
|
+
cause?: unknown;
|
|
3
|
+
body?: unknown;
|
|
4
|
+
data?: unknown;
|
|
5
|
+
error?: unknown;
|
|
2
6
|
message?: unknown;
|
|
3
|
-
responseBody?: unknown;
|
|
4
7
|
detail?: unknown;
|
|
5
8
|
status?: unknown;
|
|
9
|
+
responseBody?: unknown;
|
|
6
10
|
};
|
|
11
|
+
/**
|
|
12
|
+
* Type guard mantido por compatibilidade: qualquer objeto pode carregar um
|
|
13
|
+
* `responseBody`/`detail` de onde extrair a mensagem.
|
|
14
|
+
*/
|
|
15
|
+
export declare function isApiClientErrorLike(error: unknown): error is ApiClientErrorLike;
|
|
16
|
+
/**
|
|
17
|
+
* Melhor mensagem amigável a partir de QUALQUER valor lançado por uma chamada de
|
|
18
|
+
* API. Varre a cadeia do erro, então acha o problem+json mesmo aninhado/embrulhado.
|
|
19
|
+
* Cai no `.message` do próprio erro só quando não há payload de problema.
|
|
20
|
+
*/
|
|
7
21
|
export declare function extractApiErrorMessage(error: unknown): string | null;
|
|
22
|
+
/**
|
|
23
|
+
* Mensagem a partir de UM corpo já em mãos (objeto problem+json, ou string JSON que
|
|
24
|
+
* parseie em um). Texto simples não-JSON -> o próprio texto; objeto sem mensagem
|
|
25
|
+
* útil -> null (nunca o JSON cru).
|
|
26
|
+
*/
|
|
8
27
|
export declare function readApiErrorMessage(body: unknown): string | null;
|
|
28
|
+
export {};
|
|
9
29
|
//# sourceMappingURL=api-errors.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-errors.d.ts","sourceRoot":"","sources":["../../src/http/api-errors.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"api-errors.d.ts","sourceRoot":"","sources":["../../src/http/api-errors.ts"],"names":[],"mappings":"AAiCA,KAAK,kBAAkB,GAAG;IACxB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB,CAAC;AAEF;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,OAAO,GACb,KAAK,IAAI,kBAAkB,CAE7B;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAapE;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAMhE"}
|
package/dist/http/api-errors.js
CHANGED
|
@@ -1,75 +1,149 @@
|
|
|
1
1
|
// seedgrid:managed
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Type guard mantido por compatibilidade: qualquer objeto pode carregar um
|
|
4
|
+
* `responseBody`/`detail` de onde extrair a mensagem.
|
|
5
|
+
*/
|
|
3
6
|
export function isApiClientErrorLike(error) {
|
|
4
7
|
return Boolean(error && typeof error === "object");
|
|
5
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* Melhor mensagem amigável a partir de QUALQUER valor lançado por uma chamada de
|
|
11
|
+
* API. Varre a cadeia do erro, então acha o problem+json mesmo aninhado/embrulhado.
|
|
12
|
+
* Cai no `.message` do próprio erro só quando não há payload de problema.
|
|
13
|
+
*/
|
|
6
14
|
export function extractApiErrorMessage(error) {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
15
|
+
const fromProblem = readProblemMessage(findProblemDetails(error));
|
|
16
|
+
if (fromProblem) {
|
|
17
|
+
return fromProblem;
|
|
10
18
|
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
return pickString(error.message);
|
|
19
|
+
// Sem payload de problema útil: cai no `.message` do erro APENAS quando não é um
|
|
20
|
+
// erro do httpclient. O ApiClientError carrega `responseBody` e tem `.message`
|
|
21
|
+
// genérico ("API request failed with status N") — devolver isso vazaria texto
|
|
22
|
+
// técnico e impediria o fallback amigável por-ação do chamador; então retorna null.
|
|
23
|
+
if (isRecord(error) && "responseBody" in error) {
|
|
24
|
+
return null;
|
|
18
25
|
}
|
|
19
|
-
return
|
|
26
|
+
return pickString(readErrorMessage(error));
|
|
20
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Mensagem a partir de UM corpo já em mãos (objeto problem+json, ou string JSON que
|
|
30
|
+
* parseie em um). Texto simples não-JSON -> o próprio texto; objeto sem mensagem
|
|
31
|
+
* útil -> null (nunca o JSON cru).
|
|
32
|
+
*/
|
|
21
33
|
export function readApiErrorMessage(body) {
|
|
22
|
-
const
|
|
23
|
-
if (!
|
|
24
|
-
return pickString(
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
return violationMessage;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
+
const normalized = normalizeApiBody(body);
|
|
35
|
+
if (!isRecord(normalized)) {
|
|
36
|
+
return pickString(normalized);
|
|
37
|
+
}
|
|
38
|
+
return readProblemMessage(normalized);
|
|
39
|
+
}
|
|
40
|
+
function readProblemMessage(problem) {
|
|
41
|
+
if (!problem) {
|
|
42
|
+
return null;
|
|
34
43
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
44
|
+
return (readViolationMessage(problem.violations) ??
|
|
45
|
+
readViolationMessage(problem.errors) ??
|
|
46
|
+
pickString(problem.userMessage) ??
|
|
47
|
+
pickString(problem.detail) ??
|
|
48
|
+
pickString(problem.message) ??
|
|
49
|
+
pickString(problem.error) ??
|
|
50
|
+
pickString(problem.title));
|
|
51
|
+
}
|
|
52
|
+
function findProblemDetails(error) {
|
|
53
|
+
const queue = [error];
|
|
54
|
+
const visited = new Set();
|
|
55
|
+
while (queue.length > 0) {
|
|
56
|
+
const current = queue.shift();
|
|
57
|
+
if (current == null) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
// Uma string pode ser ela mesma o payload problem+json (corpo não desserializado).
|
|
61
|
+
if (typeof current === "string") {
|
|
62
|
+
const parsed = parseProblemDetails(current);
|
|
63
|
+
if (parsed && hasProblemSignals(parsed)) {
|
|
64
|
+
return parsed;
|
|
40
65
|
}
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (typeof current !== "object" || visited.has(current)) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
visited.add(current);
|
|
72
|
+
// Pula o `.message` genérico de Error pra alcançar o responseBody real — senão a
|
|
73
|
+
// mensagem genérica ("Request failed with 400") mascararia o detail do RFC7807.
|
|
74
|
+
if (current instanceof Error) {
|
|
75
|
+
const candidate = current;
|
|
76
|
+
queue.push(candidate.responseBody, candidate.cause, candidate.data, candidate.body);
|
|
77
|
+
continue;
|
|
41
78
|
}
|
|
79
|
+
if (hasProblemSignals(current)) {
|
|
80
|
+
return current;
|
|
81
|
+
}
|
|
82
|
+
const candidate = current;
|
|
83
|
+
queue.push(candidate.responseBody, candidate.cause, candidate.error, candidate.data, candidate.body);
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
function hasProblemSignals(problem) {
|
|
88
|
+
return readProblemMessage(problem) != null;
|
|
89
|
+
}
|
|
90
|
+
function parseProblemDetails(value) {
|
|
91
|
+
if (isRecord(value)) {
|
|
92
|
+
return value;
|
|
93
|
+
}
|
|
94
|
+
if (typeof value !== "string") {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
const parsed = JSON.parse(value);
|
|
99
|
+
return isRecord(parsed) ? parsed : null;
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return null;
|
|
42
103
|
}
|
|
43
|
-
return (pickString(record.userMessage) ??
|
|
44
|
-
pickString(record.detail) ??
|
|
45
|
-
pickString(record.message) ??
|
|
46
|
-
pickString(record.error) ??
|
|
47
|
-
pickString(record.title));
|
|
48
104
|
}
|
|
49
105
|
function normalizeApiBody(body) {
|
|
50
106
|
if (typeof body !== "string") {
|
|
51
107
|
return body;
|
|
52
108
|
}
|
|
53
|
-
const
|
|
54
|
-
if (!
|
|
109
|
+
const trimmed = body.trim();
|
|
110
|
+
if (!trimmed) {
|
|
55
111
|
return body;
|
|
56
112
|
}
|
|
57
113
|
try {
|
|
58
|
-
return JSON.parse(
|
|
114
|
+
return JSON.parse(trimmed);
|
|
59
115
|
}
|
|
60
116
|
catch {
|
|
61
117
|
return body;
|
|
62
118
|
}
|
|
63
119
|
}
|
|
64
|
-
function
|
|
65
|
-
if (!value
|
|
120
|
+
function readViolationMessage(value) {
|
|
121
|
+
if (!Array.isArray(value)) {
|
|
66
122
|
return null;
|
|
67
123
|
}
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
124
|
+
for (const violation of value) {
|
|
125
|
+
if (!isRecord(violation)) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const entry = violation;
|
|
129
|
+
const message = pickString(entry.message) ??
|
|
130
|
+
pickString(entry.userMessage) ??
|
|
131
|
+
pickString(entry.error) ??
|
|
132
|
+
pickString(entry.description);
|
|
133
|
+
if (message) {
|
|
134
|
+
return message;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
function readErrorMessage(error) {
|
|
140
|
+
if (!isRecord(error) || !("message" in error)) {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
return error.message ?? null;
|
|
144
|
+
}
|
|
145
|
+
function isRecord(value) {
|
|
146
|
+
return typeof value === "object" && value !== null;
|
|
73
147
|
}
|
|
74
148
|
function pickString(value) {
|
|
75
149
|
return typeof value === "string" && value.trim().length > 0
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-errors.meta.d.ts","sourceRoot":"","sources":["../../src/http/api-errors.meta.ts"],"names":[],"mappings":"AAAA,KAAK,YAAY,GAAG;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EACT,OAAO,GACP,OAAO,GACP,YAAY,GACZ,UAAU,GACV,YAAY,GACZ,OAAO,GACP,MAAM,CAAC;IACX,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;AAEF,KAAK,QAAQ,GAAG;IACd,OAAO,EAAE,KAAK,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,KAAK,CAAC,EAAE,YAAY,EAAE,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB,CAAC;AAEF,KAAK,WAAW,GAAG;IACjB,OAAO,EAAE,KAAK,CAAC;IACf,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC/B,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC5B,cAAc,CAAC,EAAE;QACf,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,gBAAgB,CAAC,EAAE,MAAM,CAAC;QAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,WAAW,CAAC,EAAE,MAAM,CAAC;KACtB,CAAC;CACH,CAAC;AAEF,eAAO,MAAM,MAAM,EAAE,
|
|
1
|
+
{"version":3,"file":"api-errors.meta.d.ts","sourceRoot":"","sources":["../../src/http/api-errors.meta.ts"],"names":[],"mappings":"AAAA,KAAK,YAAY,GAAG;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EACT,OAAO,GACP,OAAO,GACP,YAAY,GACZ,UAAU,GACV,YAAY,GACZ,OAAO,GACP,MAAM,CAAC;IACX,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;AAEF,KAAK,QAAQ,GAAG;IACd,OAAO,EAAE,KAAK,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,KAAK,CAAC,EAAE,YAAY,EAAE,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB,CAAC;AAEF,KAAK,WAAW,GAAG;IACjB,OAAO,EAAE,KAAK,CAAC;IACf,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC/B,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC5B,cAAc,CAAC,EAAE;QACf,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,gBAAgB,CAAC,EAAE,MAAM,CAAC;QAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,WAAW,CAAC,EAAE,MAAM,CAAC;KACtB,CAAC;CACH,CAAC;AAEF,eAAO,MAAM,MAAM,EAAE,QAiCpB,CAAC;AAEF,eAAO,MAAM,OAAO,EAAE,WA6BrB,CAAC"}
|
|
@@ -7,14 +7,16 @@ export const sgMeta = {
|
|
|
7
7
|
displayName: "API Error Message Extractor",
|
|
8
8
|
category: "utility",
|
|
9
9
|
subcategory: "http",
|
|
10
|
-
description: "Reads API problem payloads and extracts the best user-facing message, prioritizing RFC7807 violations before
|
|
10
|
+
description: "Reads API problem payloads and extracts the best user-facing message, prioritizing RFC7807 violations before userMessage/detail/title. Walks the whole error chain (responseBody/cause/error/data/body) so it works even when the problem payload is nested, rewrapped, or has crossed a bundle boundary.",
|
|
11
11
|
tags: ["api", "http", "error", "rfc7807", "validation"],
|
|
12
12
|
capabilities: [
|
|
13
13
|
"api-client-error",
|
|
14
14
|
"rfc7807",
|
|
15
15
|
"violations",
|
|
16
|
+
"user-message",
|
|
16
17
|
"error-message",
|
|
17
18
|
"json-string-body",
|
|
19
|
+
"nested-error-chain",
|
|
18
20
|
],
|
|
19
21
|
fieldSemantics: ["error", "validation", "problem-details", "http"],
|
|
20
22
|
props: [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seedgrid/fe-core",
|
|
3
|
-
"version": "2026.6.
|
|
3
|
+
"version": "2026.6.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
},
|
|
30
30
|
"scripts": {
|
|
31
31
|
"build": "tsc -p tsconfig.json && node scripts/build-ai-manifest.mjs",
|
|
32
|
-
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
32
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
33
|
+
"test": "pnpm run build && node --test tests/api-errors.test.mjs"
|
|
33
34
|
}
|
|
34
35
|
}
|