@iyulab/enterprise 0.13.0 → 0.15.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,62 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.15.0] - 2026-09-22
4
+
5
+ ### Fixed
6
+
7
+ - **`fetchRaw` now matches its own declaration.** It was documented as returning the raw
8
+ response, but the implementation ran every response through `throwIfError` and threw on any
9
+ non-2xx — and it invoked the session hook while doing so. A connection probe that only asks
10
+ "did a response arrive at all" therefore counted a `401` as *unreachable*, when the server was
11
+ in fact alive and refusing, so every signed-out user saw a permanent "cannot reach the server".
12
+ The declaration was the right one, so the implementation changed to match it: `fetchRaw` returns
13
+ the response as-is and calls no session hook.
14
+ ⚠ **If you relied on the throw**, check `res.ok` yourself, or call `apiGet` when you want the
15
+ error policy applied. Nothing else in the service changed its throwing behavior.
16
+
17
+ ### Added
18
+
19
+ - **Per-call control over the `401` hook.** `onUnauthorized` was all-or-nothing for the whole
20
+ service, but a sign-in call is the one place where `401` means "wrong credentials" rather than
21
+ "your session expired" — so a failed sign-in fired the global hook and threw a message about an
22
+ expired session, leaving the screen to invent a reason for a failure it had been told the wrong
23
+ cause of. Every request method now takes an optional trailing `ODataRequestOptions`; passing
24
+ `{ onUnauthorized: false }` keeps that call out of the global hook and throws with the message
25
+ the server actually sent. Omitting it changes nothing.
26
+ - The option is accepted by **all** request methods, not only the ones with a use for it today.
27
+ The failure being fixed is precisely that a single method had no way to express this, and drawing
28
+ the line again one method further along would reproduce it.
29
+
30
+ ### Notes
31
+
32
+ - **No `authenticate()` primitive was added**, though one was proposed. That name would have to
33
+ assume an endpoint path, a request body shape and a token convention — none of which this
34
+ library owns — so it would encode one caller's server into a general client.
35
+
36
+ ## [0.14.0] - 2026-09-20
37
+
38
+ ### Fixed
39
+
40
+ - **A failed `api*` write told the user nothing.** `notify.error` was wired into the OData writes
41
+ only, so a custom REST write that came back 4xx threw an `ApiError` and left the screen exactly as
42
+ it was — indistinguishable from nothing having happened, even when the server had returned a
43
+ precise reason. The four `api*` writes now notify on failure and rethrow, on the same terms as
44
+ their OData siblings (401 excluded, since `onUnauthorized` already covers it). Reads are unchanged
45
+ and still silent on both sides.
46
+
47
+ ### Added
48
+
49
+ - **`apiPostQuiet`, `apiPutQuiet`, `apiPatchQuiet`, `apiDeleteQuiet`.** The opt-out for the above,
50
+ matching the existing `odata*Quiet` naming: use them when one user action issues several requests,
51
+ or when the calling code already reports failures itself and would otherwise show two messages.
52
+
53
+ ### Documentation
54
+
55
+ - **The README said which hooks exist but not where they apply**, which read as "wire `notify` and
56
+ failures become visible". It now carries a per-method table and states the axis: writes notify,
57
+ reads do not, `*Quiet` opts out, 401 never notifies, and `api*` has no success message because the
58
+ library cannot invent wording for an arbitrary endpoint.
59
+
3
60
  ## 0.13.0
4
61
 
5
62
  ### Changed
package/README.md CHANGED
@@ -107,6 +107,14 @@ await svc.apiPut<Order>('orders/7', { name: 'A (revised)' }) // 리소스 전
107
107
  await svc.apiPatch<Order>('orders/7', { note: 'urgent' })
108
108
  await svc.apiDelete('orders/7') // 204 안전
109
109
 
110
+ // 로그인 — 401 이 「세션 만료」가 아니라 「자격 증명 틀림」인 유일한 호출이다.
111
+ // 전역 onUnauthorized 를 이 호출에서만 끄면, 서버가 준 사유가 그대로 ApiError.message 로 온다.
112
+ await svc.apiPost('auth/login', creds, { onUnauthorized: false })
113
+
114
+ // 연결 프로브 — 「응답이 왔는가」만 묻는다. fetchRaw 는 던지지도, 401 훅을 부르지도 않는다.
115
+ const probe = await svc.fetchRaw(svc.apiUrl('ping'))
116
+ const reachable = true // 여기 닿았다는 것 자체가 답이다(probe.status 는 볼 필요 없다)
117
+
110
118
  // body가 FormData 인스턴스면 그대로(직렬화 없이) 멀티파트로 전송된다 —
111
119
  // Content-Type은 브라우저가 boundary와 함께 자동 설정한다. apiPost/apiPut/apiPatch 전부 동일.
112
120
  const form = new FormData()
@@ -121,10 +129,61 @@ await svc.apiPost<Order>('orders/7/attachments', form)
121
129
  | `baseUrl` | 모든 요청의 오리진 (필수) |
122
130
  | `odataPrefix` / `apiPrefix` | 엔드포인트 prefix (기본 `$data` / `api`) |
123
131
  | `onUnauthorized(status)` | 401 시 호출 — 리다이렉트/재진입 가드는 앱이 처리 |
124
- | `notify.success/error` | 토스트 훅 (생략 시 토스트 없음 — 순수) |
132
+ | `notify.success/error` | 토스트 훅 (생략 시 토스트 없음 — 순수). **메서드마다 걸리는 방식이 다르다 — 바로 아래 표 참조** |
125
133
  | `messages` | 사용자 대면 문구 (기본 영어, 지정 키만 대체) |
126
134
  | `formatError(info)` | 에러 메시지 포매팅 오버라이드 (앱별 정책) — `info` 는 `status`/`statusText`/`rawMessage`/`details`(검증된 `error.details`)/`body` 를 받는다 |
127
135
 
136
+ #### `notify` 가 걸리는 자리 — 축은 «쓰기 ↔ 읽기» 다
137
+
138
+ | 메서드 | 실패 시 `error` | 성공 시 `success` |
139
+ |---|---|---|
140
+ | `odataPost` · `odataPatch` · `odataDelete` | ✅ (401 제외) | ✅ `saved`·`updated`·`deleted` |
141
+ | `apiPost` · `apiPut` · `apiPatch` · `apiDelete` | ✅ (401 제외) | ❌ |
142
+ | `*Quiet` 전부 (`odataPostQuiet` … `apiDeleteQuiet`) | ❌ | ❌ |
143
+ | `odataGet` · `odataGetById` · `odataCount` · `apiGet` · `fetchRaw` | ❌ | ❌ |
144
+
145
+ - **조회는 통지하지 않는다.** 빈 화면 자체가 신호이고, 목록을 열 때마다 토스트가 뜨면 읽을 수 없다.
146
+ - **쓰기는 통지한다.** 결과가 화면에 안 보일 수 있기 때문이다 — 서버가 409 와 사유를 돌려줘도,
147
+ 통지가 없으면 아무 일도 일어나지 않은 화면과 구별되지 않는다.
148
+ - **401 은 통지하지 않는다** — `onUnauthorized` 가 이미 안내하므로 겹친다.
149
+ - **`api*` 에는 성공 토스트가 없다.** 임의의 RPC(상태 전이·발행·업로드)를 태우는 경로라
150
+ «저장되었습니다» 같은 문구를 라이브러리가 지어낼 수 없다. 실패 메시지는 서버가 주므로 어느
151
+ 엔드포인트에서나 뜻이 통하지만, 성공 문구는 그렇지 않다 — 필요하면 호출한 쪽이 띄운다.
152
+ - **이미 자기 래퍼로 통지하고 있다면 `*Quiet` 로 바꾼다.** 이중 토스트를 막는 탈출구이고,
153
+ `odata*Quiet` 와 같은 관용구다(한 사용자 액션이 여러 요청을 낼 때도 같은 것을 쓴다).
154
+
155
+ #### 401 축 — 전역 정책과 그 호출 단위 예외
156
+
157
+ `onUnauthorized` 는 **서비스 전역 정책**이다: 401 이 오면 훅을 부르고 `messages.sessionExpired`
158
+ 로 단락한다. 이 기본값은 거의 항상 옳지만 **두 자리에서 틀린다.**
159
+
160
+ | 자리 | 왜 틀리는가 | 해법 |
161
+ |---|---|---|
162
+ | **로그인 자체** | 401 이 「세션이 끊겼다」가 아니라 「자격 증명이 틀렸다」를 뜻한다. 전역 훅이 발화하면 로그인 화면에서 로그인 화면으로 리다이렉트되고, 던져지는 메시지가 `sessionExpired` 라 화면이 실패 사유를 **지어내야** 한다 | `{ onUnauthorized: false }` |
163
+ | **연결 프로브** | *「응답이 왔는가」* 만 묻는 호출이라, 401 은 **서버가 살아서 거절한 것** = 「닿았다」다. 훅+예외를 타면 로그인하지 않은 사용자에게 늘 「서버에 연결할 수 없습니다」가 뜬다 | `fetchRaw` |
164
+
165
+ ```ts
166
+ // 401 을 «그 호출의 결과» 로 받는다 — 훅 없음, 메시지 덮어쓰기 없음.
167
+ await svc.apiPost('auth/login', creds, { onUnauthorized: false })
168
+ ```
169
+
170
+ - **모든 요청 메서드가 이 옵션을 받는다**(마지막 선택 인자). 부분집합으로 두면 다음 소비자가
171
+ 다른 메서드에서 같은 벽을 만난다.
172
+ - **끄는 축만 있다.** 새 동작을 켜는 스위치가 아니라 전역 정책의 탈출구이므로, 옵션을 생략한
173
+ 호출은 이 옵션이 생기기 전과 **한 글자도 다르게 동작하지 않는다.**
174
+ - **`authenticate()` 같은 이름 있는 프리미티브는 두지 않는다** — 그것은 엔드포인트 경로·바디
175
+ 모양·토큰 처리 규약을 라이브러리가 안다고 가정하는 **도메인 이름**이다. 그 규약은 앱마다
176
+ 다르므로 adapter 에 남기고, 라이브러리는 범용 축만 연다.
177
+
178
+ #### `fetchRaw` — 「raw」 는 정책을 태우지 않는다는 뜻이다
179
+
180
+ 응답을 **그대로** 돌려준다. **비-2xx 에도 던지지 않고 `onUnauthorized` 도 부르지 않는다** —
181
+ 상태 판단은 호출부의 몫이다. 상태에 따라 예외·토스트·세션 처리를 원하면 `apiGet` 을 쓴다.
182
+
183
+ > ⚠**0.15.0 이전에는 이 문장이 선언에 적혀 있으면서 실제로는 비-2xx 에 던졌다.** 선언이 옳고
184
+ > 구현이 틀렸던 자리라 구현을 고쳤다. `fetchRaw` 의 throw 에 기대고 있었다면 호출부에서
185
+ > `if (!res.ok) throw …` 로 바꾸거나 `apiGet` 으로 옮긴다.
186
+
128
187
  > 도메인 액션(상태 전이 등)·엔티티 목록·권한 코드는 라이브러리에 넣지 말고 앱 adapter 에 둔다.
129
188
 
130
189
  #### 실패 응답 — `ApiError`