@biffo/cli 0.253.4 → 0.253.5

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.
@@ -34,11 +34,52 @@ describe('extractErrorMessage', () => {
34
34
  expect(extractErrorMessage('{"message":"nope"}', 'Bad Request')).toBe('{"message":"nope"}')
35
35
  })
36
36
 
37
- it('falls back to the raw text when detail is not a string', () => {
37
+ // Renamed from "falls back to the raw text when detail is not a string"
38
+ // that name generalised a decision only ever made about a LIST `detail`
39
+ // (biffo-template#1350). It is no longer true of every non-string detail: a
40
+ // dict carrying a string `message` (below) now unwraps instead of falling
41
+ // back. FastAPI's own 422 is the case this test still pins.
42
+ it('falls back to the raw text when detail is a list (FastAPI 422 field errors)', () => {
38
43
  const body = '{"detail":[{"loc":["body","x"],"msg":"bad"}]}'
39
44
  expect(extractErrorMessage(body, 'Unprocessable Entity')).toBe(body)
40
45
  })
41
46
 
47
+ // The browser-side twin of `_extract_detail`'s identical rule in
48
+ // core_client.py. Core's generic CRUD layer answers an integrity error with
49
+ // `{"detail": {"message": "...", "constraint": "..."}}`
50
+ // (`routing/crud_handlers._integrity_error_response`); until this rule
51
+ // existed here, that shape fell straight through to the raw JSON blob, even
52
+ // in a sibling whose BFF had already unwrapped it server-side
53
+ // (biffo-template#1350, tabsii-crm#272).
54
+ it('unwraps a dict detail carrying a string message', () => {
55
+ const body =
56
+ '{"detail":{"message":"course c1 cannot be deleted: 3 enrolment(s) depend on it.","constraint":"fk_enrolments_course_id"}}'
57
+ expect(extractErrorMessage(body, 'Conflict')).toBe(
58
+ 'course c1 cannot be deleted: 3 enrolment(s) depend on it.',
59
+ )
60
+ })
61
+
62
+ // `constraint` is deliberately dropped, not appended, in EVERY case — a
63
+ // database object name is schema reconnaissance (tabsii-platform#473) and
64
+ // says nothing to the person reading the sentence. This asserts the drop
65
+ // rather than just the unwrap above, so a future "helpfully" append would
66
+ // fail here even if it left the previous test passing.
67
+ it('drops constraint rather than appending it to the message', () => {
68
+ const body = '{"detail":{"message":"cannot delete","constraint":"fk_enrolments_course_id"}}'
69
+ const message = extractErrorMessage(body, 'Conflict')
70
+ expect(message).toBe('cannot delete')
71
+ expect(message).not.toContain('fk_enrolments_course_id')
72
+ })
73
+
74
+ // Only the declared `message` key is trusted. A dict `detail` with no
75
+ // `message` is a shape nobody has declared, so it falls back to the raw
76
+ // text — same posture as the list case above — rather than inventing a
77
+ // summary that could hide information the caller had before.
78
+ it('falls back to the raw text when a dict detail has no message', () => {
79
+ const body = '{"detail":{"constraint":"fk_enrolments_course_id"}}'
80
+ expect(extractErrorMessage(body, 'Conflict')).toBe(body)
81
+ })
82
+
42
83
  it('uses the status text when the body is empty', () => {
43
84
  expect(extractErrorMessage('', 'Internal Server Error')).toBe('Internal Server Error')
44
85
  })
@@ -30,9 +30,22 @@ export class ApiError extends Error {
30
30
  * - **Parsed, never assumed.** A non-JSON body (a proxy timeout page, a
31
31
  * CloudFront error), or JSON with no `detail`, falls back to the raw text
32
32
  * unchanged — so this can never hide information the caller had before.
33
- * - **A non-string `detail` also falls back.** FastAPI's own 422 makes
34
- * `detail` a list of field errors; picking something out of it would just
35
- * move the problem rather than fix it.
33
+ * - **A dict `detail` carrying a string `message` unwraps to that message.**
34
+ * Core's generic CRUD layer answers an integrity error with
35
+ * `{"detail": {"message": "...", "constraint": "..."}}`
36
+ * (`routing/crud_handlers._integrity_error_response`). Until this rule
37
+ * existed here, a sibling whose BFF had already unwrapped this shape
38
+ * (`core_client.py`) still rendered the raw JSON blob in the browser,
39
+ * because this layer only knew how to unwrap a *string* `detail`
40
+ * (biffo-template#1350). `constraint` is deliberately dropped, not
41
+ * appended — it is a database object name, which is schema
42
+ * reconnaissance (tabsii-platform#473) and says nothing to the person
43
+ * reading the sentence.
44
+ * - **Any other non-string, non-message-bearing `detail` still falls
45
+ * back.** FastAPI's own 422 makes `detail` a list of field errors, and a
46
+ * dict `detail` with no `message` key is a shape nobody has declared —
47
+ * picking something out of either would move the problem rather than fix
48
+ * it.
36
49
  *
37
50
  * The one addition over the Python version: an empty body yields the HTTP
38
51
  * status text, because a browser rendering `''` shows the user nothing at all.
@@ -66,10 +79,25 @@ function parsedDetail(body: string): string | null {
66
79
  }
67
80
  if (typeof parsed === 'object' && parsed !== null && 'detail' in parsed) {
68
81
  const { detail } = parsed
69
- // A non-string `detail` also falls back. FastAPI's own 422 makes it a list
70
- // of field errors; picking something out of it would move the problem
71
- // rather than fix it.
72
82
  if (typeof detail === 'string') return detail
83
+ // A dict `detail` carrying a string `message` unwraps to that message —
84
+ // the browser-side twin of `_extract_detail`'s identical rule in
85
+ // core_client.py (biffo-template#1350). `constraint` is deliberately
86
+ // dropped, not appended: it is a database object name (schema
87
+ // reconnaissance, tabsii-platform#473) and says nothing to the person
88
+ // reading the sentence. Only the declared `message` key is trusted — a
89
+ // dict `detail` with anything else (no `message`, or a non-string one),
90
+ // or a non-dict non-string `detail` (FastAPI's own 422 makes it a list
91
+ // of field errors), falls through to the raw text below rather than
92
+ // inventing a summary from a shape nobody has declared.
93
+ if (
94
+ typeof detail === 'object' &&
95
+ detail !== null &&
96
+ !Array.isArray(detail) &&
97
+ typeof (detail as { message?: unknown }).message === 'string'
98
+ ) {
99
+ return (detail as { message: string }).message
100
+ }
73
101
  }
74
102
  return null
75
103
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@biffo/cli",
3
- "version": "0.253.4",
3
+ "version": "0.253.5",
4
4
  "description": "Biffo project scaffolding CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",