@iyulab/enterprise 0.13.0 → 0.14.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 +24 -0
- package/README.md +20 -1
- package/dist/data/ODataService.d.ts +18 -6
- package/dist/index.js +49 -19
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.14.0] - 2026-09-20
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **A failed `api*` write told the user nothing.** `notify.error` was wired into the OData writes
|
|
8
|
+
only, so a custom REST write that came back 4xx threw an `ApiError` and left the screen exactly as
|
|
9
|
+
it was — indistinguishable from nothing having happened, even when the server had returned a
|
|
10
|
+
precise reason. The four `api*` writes now notify on failure and rethrow, on the same terms as
|
|
11
|
+
their OData siblings (401 excluded, since `onUnauthorized` already covers it). Reads are unchanged
|
|
12
|
+
and still silent on both sides.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **`apiPostQuiet`, `apiPutQuiet`, `apiPatchQuiet`, `apiDeleteQuiet`.** The opt-out for the above,
|
|
17
|
+
matching the existing `odata*Quiet` naming: use them when one user action issues several requests,
|
|
18
|
+
or when the calling code already reports failures itself and would otherwise show two messages.
|
|
19
|
+
|
|
20
|
+
### Documentation
|
|
21
|
+
|
|
22
|
+
- **The README said which hooks exist but not where they apply**, which read as "wire `notify` and
|
|
23
|
+
failures become visible". It now carries a per-method table and states the axis: writes notify,
|
|
24
|
+
reads do not, `*Quiet` opts out, 401 never notifies, and `api*` has no success message because the
|
|
25
|
+
library cannot invent wording for an arbitrary endpoint.
|
|
26
|
+
|
|
3
27
|
## 0.13.0
|
|
4
28
|
|
|
5
29
|
### Changed
|
package/README.md
CHANGED
|
@@ -121,10 +121,29 @@ await svc.apiPost<Order>('orders/7/attachments', form)
|
|
|
121
121
|
| `baseUrl` | 모든 요청의 오리진 (필수) |
|
|
122
122
|
| `odataPrefix` / `apiPrefix` | 엔드포인트 prefix (기본 `$data` / `api`) |
|
|
123
123
|
| `onUnauthorized(status)` | 401 시 호출 — 리다이렉트/재진입 가드는 앱이 처리 |
|
|
124
|
-
| `notify.success/error` | 토스트 훅 (생략 시 토스트 없음 — 순수) |
|
|
124
|
+
| `notify.success/error` | 토스트 훅 (생략 시 토스트 없음 — 순수). **메서드마다 걸리는 방식이 다르다 — 바로 아래 표 참조** |
|
|
125
125
|
| `messages` | 사용자 대면 문구 (기본 영어, 지정 키만 대체) |
|
|
126
126
|
| `formatError(info)` | 에러 메시지 포매팅 오버라이드 (앱별 정책) — `info` 는 `status`/`statusText`/`rawMessage`/`details`(검증된 `error.details`)/`body` 를 받는다 |
|
|
127
127
|
|
|
128
|
+
#### `notify` 가 걸리는 자리 — 축은 «쓰기 ↔ 읽기» 다
|
|
129
|
+
|
|
130
|
+
| 메서드 | 실패 시 `error` | 성공 시 `success` |
|
|
131
|
+
|---|---|---|
|
|
132
|
+
| `odataPost` · `odataPatch` · `odataDelete` | ✅ (401 제외) | ✅ `saved`·`updated`·`deleted` |
|
|
133
|
+
| `apiPost` · `apiPut` · `apiPatch` · `apiDelete` | ✅ (401 제외) | ❌ |
|
|
134
|
+
| `*Quiet` 전부 (`odataPostQuiet` … `apiDeleteQuiet`) | ❌ | ❌ |
|
|
135
|
+
| `odataGet` · `odataGetById` · `odataCount` · `apiGet` · `fetchRaw` | ❌ | ❌ |
|
|
136
|
+
|
|
137
|
+
- **조회는 통지하지 않는다.** 빈 화면 자체가 신호이고, 목록을 열 때마다 토스트가 뜨면 읽을 수 없다.
|
|
138
|
+
- **쓰기는 통지한다.** 결과가 화면에 안 보일 수 있기 때문이다 — 서버가 409 와 사유를 돌려줘도,
|
|
139
|
+
통지가 없으면 아무 일도 일어나지 않은 화면과 구별되지 않는다.
|
|
140
|
+
- **401 은 통지하지 않는다** — `onUnauthorized` 가 이미 안내하므로 겹친다.
|
|
141
|
+
- **`api*` 에는 성공 토스트가 없다.** 임의의 RPC(상태 전이·발행·업로드)를 태우는 경로라
|
|
142
|
+
«저장되었습니다» 같은 문구를 라이브러리가 지어낼 수 없다. 실패 메시지는 서버가 주므로 어느
|
|
143
|
+
엔드포인트에서나 뜻이 통하지만, 성공 문구는 그렇지 않다 — 필요하면 호출한 쪽이 띄운다.
|
|
144
|
+
- **이미 자기 래퍼로 통지하고 있다면 `*Quiet` 로 바꾼다.** 이중 토스트를 막는 탈출구이고,
|
|
145
|
+
`odata*Quiet` 와 같은 관용구다(한 사용자 액션이 여러 요청을 낼 때도 같은 것을 쓴다).
|
|
146
|
+
|
|
128
147
|
> 도메인 액션(상태 전이 등)·엔티티 목록·권한 코드는 라이브러리에 넣지 말고 앱 adapter 에 둔다.
|
|
129
148
|
|
|
130
149
|
#### 실패 응답 — `ApiError`
|
|
@@ -94,16 +94,28 @@ export interface ODataService {
|
|
|
94
94
|
odataDelete(entity: string, id: string): Promise<void>;
|
|
95
95
|
/** custom REST GET — 204 등 빈 바디를 안전 파싱. */
|
|
96
96
|
apiGet<T>(path: string): Promise<T>;
|
|
97
|
-
/** custom REST POST
|
|
98
|
-
* 전송된다 — `@iyulab/http-client`가
|
|
99
|
-
* JSON 직렬화 분기를 타지 않는다.
|
|
97
|
+
/** custom REST POST — 실패 시 `error` 토스트 후 rethrow(401 제외). `body`가 `FormData`
|
|
98
|
+
* 인스턴스면 그대로(직렬화 없이) 멀티파트로 전송된다 — `@iyulab/http-client`가
|
|
99
|
+
* Content-Type을 브라우저 자동 설정에 맡기고 JSON 직렬화 분기를 타지 않는다.
|
|
100
|
+
* `apiPut`/`apiPatch`도 동일하게 동작한다. */
|
|
100
101
|
apiPost<T>(path: string, body?: unknown): Promise<T>;
|
|
101
|
-
/** custom REST
|
|
102
|
+
/** custom REST POST — 토스트 없이 결과만(`odataPostQuiet`와 같은 이유: 한 사용자 액션이
|
|
103
|
+
* 여러 요청을 내거나, 소비자가 자기 래퍼로 이미 통지할 때). */
|
|
104
|
+
apiPostQuiet<T>(path: string, body?: unknown): Promise<T>;
|
|
105
|
+
/** custom REST PUT(리소스 전체 교체/생성) — 실패 시 `error` 토스트 후 rethrow(401 제외).
|
|
106
|
+
* `body`의 `FormData` 처리는 `apiPost` 참조. */
|
|
102
107
|
apiPut<T>(path: string, body?: unknown): Promise<T>;
|
|
103
|
-
/** custom REST
|
|
108
|
+
/** custom REST PUT — 토스트 없이 결과만. */
|
|
109
|
+
apiPutQuiet<T>(path: string, body?: unknown): Promise<T>;
|
|
110
|
+
/** custom REST PATCH — 실패 시 `error` 토스트 후 rethrow(401 제외). `body`의 `FormData`
|
|
111
|
+
* 처리는 `apiPost` 참조. */
|
|
104
112
|
apiPatch<T>(path: string, body?: unknown): Promise<T>;
|
|
105
|
-
/** custom REST
|
|
113
|
+
/** custom REST PATCH — 토스트 없이 결과만. */
|
|
114
|
+
apiPatchQuiet<T>(path: string, body?: unknown): Promise<T>;
|
|
115
|
+
/** custom REST DELETE — 실패 시 `error` 토스트 후 rethrow(401 제외). 대부분 204 No Content. */
|
|
106
116
|
apiDelete<T = void>(path: string): Promise<T>;
|
|
117
|
+
/** custom REST DELETE — 토스트 없이 결과만. */
|
|
118
|
+
apiDeleteQuiet<T = void>(path: string): Promise<T>;
|
|
107
119
|
/** URL 을 직접 조립한 커스텀 조회(csv-export 등)를 위해 raw 응답을 반환. */
|
|
108
120
|
fetchRaw(url: string): Promise<HttpResponse>;
|
|
109
121
|
/**
|
package/dist/index.js
CHANGED
|
@@ -774,44 +774,58 @@ function createODataService(config) {
|
|
|
774
774
|
await throwIfError(res);
|
|
775
775
|
return (await res.json())["@odata.count"] ?? 0;
|
|
776
776
|
}
|
|
777
|
+
/**
|
|
778
|
+
* 쓰기 실패를 사용자에게 알린 뒤 그대로 다시 던진다.
|
|
779
|
+
*
|
|
780
|
+
* 🔴**이 서비스의 통지 축은 «쓰기 ↔ 읽기» 이지 «odata ↔ api» 가 아니다.** 조회
|
|
781
|
+
* (`odataGet`·`odataGetById`·`odataCount`·`apiGet`·`fetchRaw`)는 어느 쪽도 통지하지
|
|
782
|
+
* 않는다 — 화면이 비어 있는 것 자체가 신호이고, 목록 조회마다 토스트를 띄우면 읽을 수
|
|
783
|
+
* 없다. 쓰기는 **결과가 화면에 안 보일 수 있으므로** 통지한다: 서버가 409 와 사유를
|
|
784
|
+
* 돌려줘도 아무 일도 일어나지 않은 화면과 구별되지 않는다.
|
|
785
|
+
*
|
|
786
|
+
* ⚠**401 은 제외한다** — `onUnauthorized` 가 이미 안내하므로 토스트가 겹친다.
|
|
787
|
+
*
|
|
788
|
+
* ⚠성공 토스트는 **`odata*` 쓰기에만** 있다. `api*` 는 임의의 RPC(상태 전이·발행·업로드)를
|
|
789
|
+
* 태우는 경로라 «저장되었습니다» 같은 문구를 우리가 지어낼 수 없다 — 실패 메시지는 서버가
|
|
790
|
+
* 주므로 어느 엔드포인트에서나 뜻이 통하지만, 성공 문구는 그렇지 않다.
|
|
791
|
+
*/
|
|
792
|
+
async function notifyingWrite(run) {
|
|
793
|
+
try {
|
|
794
|
+
return await run();
|
|
795
|
+
} catch (e) {
|
|
796
|
+
if (!(e instanceof ApiError && e.status === 401)) notifyError?.(e instanceof Error ? e.message : messages.requestFailed);
|
|
797
|
+
throw e;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
777
800
|
async function odataPostQuiet(entity, body) {
|
|
778
801
|
const res = await client.post(odataUrl(entity), normalizeBody(body));
|
|
779
802
|
await throwIfError(res);
|
|
780
803
|
return res.json();
|
|
781
804
|
}
|
|
782
805
|
async function odataPost(entity, body) {
|
|
783
|
-
|
|
806
|
+
return notifyingWrite(async () => {
|
|
784
807
|
const result = await odataPostQuiet(entity, body);
|
|
785
808
|
notifySuccess?.(messages.saved);
|
|
786
809
|
return result;
|
|
787
|
-
}
|
|
788
|
-
if (!(e instanceof ApiError && e.status === 401)) notifyError?.(e instanceof Error ? e.message : messages.requestFailed);
|
|
789
|
-
throw e;
|
|
790
|
-
}
|
|
810
|
+
});
|
|
791
811
|
}
|
|
792
812
|
async function odataPatchQuiet(entity, id, body) {
|
|
793
813
|
await throwIfError(await client.patch(`${odataUrl(entity)}(${id})`, normalizeBody(body)));
|
|
794
814
|
}
|
|
795
815
|
async function odataPatch(entity, id, body) {
|
|
796
|
-
|
|
816
|
+
return notifyingWrite(async () => {
|
|
797
817
|
await odataPatchQuiet(entity, id, body);
|
|
798
818
|
notifySuccess?.(messages.updated);
|
|
799
|
-
}
|
|
800
|
-
if (!(e instanceof ApiError && e.status === 401)) notifyError?.(e instanceof Error ? e.message : messages.requestFailed);
|
|
801
|
-
throw e;
|
|
802
|
-
}
|
|
819
|
+
});
|
|
803
820
|
}
|
|
804
821
|
async function odataDeleteQuiet(entity, id) {
|
|
805
822
|
await throwIfError(await client.delete(`${odataUrl(entity)}(${id})`));
|
|
806
823
|
}
|
|
807
824
|
async function odataDelete(entity, id) {
|
|
808
|
-
|
|
825
|
+
return notifyingWrite(async () => {
|
|
809
826
|
await odataDeleteQuiet(entity, id);
|
|
810
827
|
notifySuccess?.(messages.deleted);
|
|
811
|
-
}
|
|
812
|
-
if (!(e instanceof ApiError && e.status === 401)) notifyError?.(e instanceof Error ? e.message : messages.requestFailed);
|
|
813
|
-
throw e;
|
|
814
|
-
}
|
|
828
|
+
});
|
|
815
829
|
}
|
|
816
830
|
async function apiGet(path) {
|
|
817
831
|
const [p, ...q] = path.split("?");
|
|
@@ -820,26 +834,38 @@ function createODataService(config) {
|
|
|
820
834
|
await throwIfError(res);
|
|
821
835
|
return parseJsonBody(res);
|
|
822
836
|
}
|
|
823
|
-
async function
|
|
837
|
+
async function apiPostQuiet(path, body) {
|
|
824
838
|
const res = await client.post(apiUrl(path), body ?? {});
|
|
825
839
|
await throwIfError(res);
|
|
826
840
|
return parseJsonBody(res);
|
|
827
841
|
}
|
|
828
|
-
async function
|
|
842
|
+
async function apiPost(path, body) {
|
|
843
|
+
return notifyingWrite(() => apiPostQuiet(path, body));
|
|
844
|
+
}
|
|
845
|
+
async function apiPutQuiet(path, body) {
|
|
829
846
|
const res = await client.put(apiUrl(path), body ?? {});
|
|
830
847
|
await throwIfError(res);
|
|
831
848
|
return parseJsonBody(res);
|
|
832
849
|
}
|
|
833
|
-
async function
|
|
850
|
+
async function apiPut(path, body) {
|
|
851
|
+
return notifyingWrite(() => apiPutQuiet(path, body));
|
|
852
|
+
}
|
|
853
|
+
async function apiPatchQuiet(path, body) {
|
|
834
854
|
const res = await client.patch(apiUrl(path), body ?? {});
|
|
835
855
|
await throwIfError(res);
|
|
836
856
|
return parseJsonBody(res);
|
|
837
857
|
}
|
|
838
|
-
async function
|
|
858
|
+
async function apiPatch(path, body) {
|
|
859
|
+
return notifyingWrite(() => apiPatchQuiet(path, body));
|
|
860
|
+
}
|
|
861
|
+
async function apiDeleteQuiet(path) {
|
|
839
862
|
const res = await client.delete(apiUrl(path));
|
|
840
863
|
await throwIfError(res);
|
|
841
864
|
return parseJsonBody(res);
|
|
842
865
|
}
|
|
866
|
+
async function apiDelete(path) {
|
|
867
|
+
return notifyingWrite(() => apiDeleteQuiet(path));
|
|
868
|
+
}
|
|
843
869
|
async function fetchRaw(url) {
|
|
844
870
|
const res = await client.get(url);
|
|
845
871
|
await throwIfError(res);
|
|
@@ -859,9 +885,13 @@ function createODataService(config) {
|
|
|
859
885
|
odataDelete,
|
|
860
886
|
apiGet,
|
|
861
887
|
apiPost,
|
|
888
|
+
apiPostQuiet,
|
|
862
889
|
apiPut,
|
|
890
|
+
apiPutQuiet,
|
|
863
891
|
apiPatch,
|
|
892
|
+
apiPatchQuiet,
|
|
864
893
|
apiDelete,
|
|
894
|
+
apiDeleteQuiet,
|
|
865
895
|
fetchRaw,
|
|
866
896
|
sourceDefaults: {
|
|
867
897
|
baseUrl,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iyulab/enterprise",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Enterprise utilities and components for iyulab framework",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"enterprise",
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"typescript": "^6.0.2",
|
|
62
62
|
"vite": "^8.1.4",
|
|
63
63
|
"vite-plugin-dts": "^5.0.3",
|
|
64
|
-
"vitest": "^
|
|
64
|
+
"vitest": "^5.0.0"
|
|
65
65
|
},
|
|
66
66
|
"peerDependencies": {
|
|
67
67
|
"react": ">=18",
|