@wheeparam/library 0.0.4 → 0.0.6

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.
@@ -72,6 +72,18 @@ type Route<Path extends string = string> = {
72
72
  * ] as const)
73
73
  */
74
74
  declare function defineRoutes<const R extends readonly Route<string>[]>(routes: R): R;
75
+ type ApiProps = {
76
+ params: Promise<{
77
+ slug?: string[];
78
+ }>;
79
+ };
80
+ type ListResponse<T> = {
81
+ result: T[];
82
+ totalCount: number;
83
+ };
84
+ type RowResponse<T> = {
85
+ result: T;
86
+ };
75
87
 
76
88
  /**
77
89
  * @packageDocumentation
@@ -121,4 +133,4 @@ declare function matchRoute(path: string, slug: string[]): MatchResult | null;
121
133
  */
122
134
  declare function routeResponse(slug: string[], routes: readonly Route<string>[], request: Request): Promise<Response>;
123
135
 
124
- export { type ParamsFromPath, type Route, type RouteHandler, defineRoutes, matchRoute, routeResponse };
136
+ export { type ApiProps, type ListResponse, type ParamsFromPath, type Route, type RouteHandler, type RowResponse, defineRoutes, matchRoute, routeResponse };
@@ -72,6 +72,18 @@ type Route<Path extends string = string> = {
72
72
  * ] as const)
73
73
  */
74
74
  declare function defineRoutes<const R extends readonly Route<string>[]>(routes: R): R;
75
+ type ApiProps = {
76
+ params: Promise<{
77
+ slug?: string[];
78
+ }>;
79
+ };
80
+ type ListResponse<T> = {
81
+ result: T[];
82
+ totalCount: number;
83
+ };
84
+ type RowResponse<T> = {
85
+ result: T;
86
+ };
75
87
 
76
88
  /**
77
89
  * @packageDocumentation
@@ -121,4 +133,4 @@ declare function matchRoute(path: string, slug: string[]): MatchResult | null;
121
133
  */
122
134
  declare function routeResponse(slug: string[], routes: readonly Route<string>[], request: Request): Promise<Response>;
123
135
 
124
- export { type ParamsFromPath, type Route, type RouteHandler, defineRoutes, matchRoute, routeResponse };
136
+ export { type ApiProps, type ListResponse, type ParamsFromPath, type Route, type RouteHandler, type RowResponse, defineRoutes, matchRoute, routeResponse };
@@ -34,169 +34,306 @@ __export(client_exports, {
34
34
  });
35
35
  module.exports = __toCommonJS(client_exports);
36
36
 
37
- // src/client/axios.ts
37
+ // src/client/createAxiosClient.ts
38
38
  var import_axios = __toESM(require("axios"), 1);
39
- function createLocalStorageTokenAccess(keys) {
40
- const accessTokenKey = keys?.accessTokenKey ?? "accessToken";
41
- const refreshTokenKey = keys?.refreshTokenKey ?? "refreshToken";
39
+
40
+ // src/client/error.ts
41
+ var defaultShouldRefresh = (error) => {
42
+ return error.response?.status === 401;
43
+ };
44
+ function defaultParseRefreshResponse(data) {
45
+ const source = typeof data === "object" && data !== null ? data : {};
46
+ const accessToken = typeof source.result?.accessToken === "string" && source.result.accessToken || typeof source.accessToken === "string" && source.accessToken || typeof source.result?.token === "string" && source.result.token || typeof source.token === "string" && source.token || "";
47
+ const refreshToken = typeof source.result?.refreshToken === "string" && source.result.refreshToken || typeof source.refreshToken === "string" && source.refreshToken || void 0;
48
+ if (!accessToken) {
49
+ throw new Error("Refresh response does not contain accessToken");
50
+ }
42
51
  return {
43
- get() {
44
- return {
45
- accessToken: window.localStorage.getItem(accessTokenKey),
46
- refreshToken: window.localStorage.getItem(refreshTokenKey)
47
- };
52
+ accessToken,
53
+ refreshToken
54
+ };
55
+ }
56
+ function getDefaultErrorMessage(error) {
57
+ const axiosError = error;
58
+ const status = axiosError.response?.status;
59
+ const serverMessage = axiosError.response?.data?.error;
60
+ if (serverMessage && serverMessage.trim().length > 0) {
61
+ return serverMessage;
62
+ }
63
+ switch (status) {
64
+ case 0:
65
+ return "REST API \uC11C\uBC84\uC5D0 \uC811\uADFC\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.\n\uC11C\uBC84 \uAD00\uB9AC\uC790\uC5D0\uAC8C \uBB38\uC758\uD558\uC138\uC694.";
66
+ case 400:
67
+ return "\uC798\uBABB\uB41C \uC694\uCCAD\uC785\uB2C8\uB2E4.";
68
+ case 401:
69
+ return "\uC778\uC99D\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.";
70
+ case 403:
71
+ return "\uC811\uADFC \uAD8C\uD55C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.";
72
+ case 404:
73
+ return "[404] REST API \uC694\uCCAD\uC5D0 \uC2E4\uD328\uD558\uC600\uC2B5\uB2C8\uB2E4.";
74
+ case 500:
75
+ return "\uC11C\uBC84\uC5D0\uC11C \uCC98\uB9AC \uC911 \uC624\uB958\uAC00 \uBC1C\uC0DD\uD558\uC600\uC2B5\uB2C8\uB2E4.";
76
+ default:
77
+ return "";
78
+ }
79
+ }
80
+
81
+ // src/client/refreshManager.ts
82
+ var RefreshManager = class {
83
+ constructor() {
84
+ this.isRefreshing = false;
85
+ this.subscribers = [];
86
+ }
87
+ /**
88
+ * 현재 refresh 진행 중인지 반환합니다.
89
+ */
90
+ get refreshing() {
91
+ return this.isRefreshing;
92
+ }
93
+ /**
94
+ * refresh 시작 상태로 전환합니다.
95
+ */
96
+ begin() {
97
+ this.isRefreshing = true;
98
+ }
99
+ /**
100
+ * refresh 종료 상태로 전환합니다.
101
+ */
102
+ end() {
103
+ this.isRefreshing = false;
104
+ }
105
+ /**
106
+ * refresh 완료를 기다리는 subscriber를 등록합니다.
107
+ *
108
+ * @param callback refresh 완료 후 호출될 함수
109
+ */
110
+ subscribe(callback) {
111
+ this.subscribers.push(callback);
112
+ }
113
+ /**
114
+ * 대기 중인 subscriber들에게 결과를 알리고 큐를 초기화합니다.
115
+ *
116
+ * @param token 새 access token
117
+ */
118
+ notify(token) {
119
+ this.subscribers.forEach((callback) => callback(token));
120
+ this.subscribers = [];
121
+ }
122
+ };
123
+ async function requestRefresh(params) {
124
+ const {
125
+ refreshUrl,
126
+ withCredentials,
127
+ refreshStrategy,
128
+ refreshToken,
129
+ parseRefreshResponse
130
+ } = params;
131
+ const requestInit = {
132
+ method: "POST",
133
+ headers: {
134
+ "Content-Type": "application/json"
48
135
  },
49
- set(data) {
50
- if (data.accessToken !== void 0) {
51
- window.localStorage.setItem(accessTokenKey, data.accessToken ?? "");
136
+ credentials: withCredentials ? "include" : "same-origin"
137
+ };
138
+ if (refreshStrategy === "body") {
139
+ if (!refreshToken) {
140
+ throw new Error("Refresh token not found");
141
+ }
142
+ requestInit.body = JSON.stringify({ refreshToken });
143
+ }
144
+ const response = await fetch(refreshUrl, requestInit);
145
+ if (!response.ok) {
146
+ throw new Error("Token refresh failed");
147
+ }
148
+ const json = await response.json();
149
+ return parseRefreshResponse(json);
150
+ }
151
+
152
+ // src/client/tokenStorage.ts
153
+ function createAccessTokenAccess(mode, key = "accessToken") {
154
+ let memoryToken = null;
155
+ if (mode === "localStorage") {
156
+ return {
157
+ get: () => window.localStorage.getItem(key),
158
+ set: (token) => {
159
+ if (!token) {
160
+ window.localStorage.removeItem(key);
161
+ } else {
162
+ window.localStorage.setItem(key, token);
163
+ }
164
+ },
165
+ clear: () => {
166
+ window.localStorage.removeItem(key);
52
167
  }
53
- if (data.refreshToken !== void 0) {
54
- window.localStorage.setItem(refreshTokenKey, data.refreshToken ?? "");
168
+ };
169
+ }
170
+ if (mode === "memory") {
171
+ return {
172
+ get: () => memoryToken,
173
+ set: (token) => {
174
+ memoryToken = token;
175
+ },
176
+ clear: () => {
177
+ memoryToken = null;
55
178
  }
179
+ };
180
+ }
181
+ return {
182
+ get: () => null,
183
+ set: () => {
56
184
  },
57
- clear() {
58
- window.localStorage.removeItem(accessTokenKey);
59
- window.localStorage.removeItem(refreshTokenKey);
185
+ clear: () => {
60
186
  }
61
187
  };
62
188
  }
63
- function createCookieModeTokenAccess(keys) {
64
- const refreshTokenKey = keys?.refreshTokenKey ?? "refreshToken";
189
+ function createRefreshTokenAccess(key = "refreshToken") {
65
190
  return {
66
- get() {
67
- return {
68
- accessToken: null,
69
- // 쿠키 기반이면 JS에서 accessToken을 직접 읽지 않는 전제를 둔다.
70
- refreshToken: window.localStorage.getItem(refreshTokenKey)
71
- };
72
- },
73
- set(data) {
74
- if (data.refreshToken !== void 0) {
75
- window.localStorage.setItem(refreshTokenKey, data.refreshToken ?? "");
191
+ get: () => window.localStorage.getItem(key),
192
+ set: (token) => {
193
+ if (!token) {
194
+ window.localStorage.removeItem(key);
195
+ } else {
196
+ window.localStorage.setItem(key, token);
76
197
  }
77
198
  },
78
- clear() {
79
- window.localStorage.removeItem(refreshTokenKey);
199
+ clear: () => {
200
+ window.localStorage.removeItem(key);
80
201
  }
81
202
  };
82
203
  }
204
+
205
+ // src/client/createAxiosClient.ts
206
+ function setAuthorizationHeader(config, accessToken) {
207
+ const headers = new import_axios.AxiosHeaders();
208
+ if (config.headers) {
209
+ const current = config.headers instanceof import_axios.AxiosHeaders ? config.headers.toJSON() : config.headers;
210
+ for (const [key, value] of Object.entries(current)) {
211
+ if (value !== void 0) {
212
+ headers.set(key, String(value));
213
+ }
214
+ }
215
+ }
216
+ headers.set("Authorization", `Bearer ${accessToken}`);
217
+ config.headers = headers;
218
+ return config;
219
+ }
83
220
  function createAxiosClient(options) {
84
221
  const {
85
222
  baseURL,
86
223
  timeout = 1e4,
87
224
  withCredentials = true,
88
225
  refreshUrl,
89
- storage = "localStorage",
226
+ accessTokenStorage = "localStorage",
227
+ refreshStrategy = "body",
90
228
  storageKeys,
91
229
  onAuthStateChange,
92
230
  onError,
93
- getErrorMessage
231
+ getErrorMessage,
232
+ shouldRefresh = defaultShouldRefresh,
233
+ parseRefreshResponse = defaultParseRefreshResponse
94
234
  } = options;
95
- const instance = import_axios.default.create({ baseURL, timeout, withCredentials });
96
- const tokenAccess = storage === "localStorage" ? createLocalStorageTokenAccess(storageKeys) : createCookieModeTokenAccess({ refreshTokenKey: storageKeys?.refreshTokenKey });
97
- let isRefreshing = false;
98
- let subscribers = [];
99
- const notifySubscribers = (token) => {
100
- subscribers.forEach((cb) => cb(token));
101
- subscribers = [];
102
- };
235
+ const accessTokenKey = storageKeys?.accessTokenKey ?? "accessToken";
236
+ const refreshTokenKey = storageKeys?.refreshTokenKey ?? "refreshToken";
237
+ const instance = import_axios.default.create({
238
+ baseURL,
239
+ timeout,
240
+ withCredentials
241
+ });
242
+ const accessTokenAccess = createAccessTokenAccess(accessTokenStorage, accessTokenKey);
243
+ const refreshTokenAccess = createRefreshTokenAccess(refreshTokenKey);
244
+ const refreshManager = new RefreshManager();
103
245
  const clearAuth = () => {
104
- tokenAccess.clear();
246
+ accessTokenAccess.clear();
247
+ if (refreshStrategy === "body") {
248
+ refreshTokenAccess.clear();
249
+ }
105
250
  onAuthStateChange?.("unauthenticated");
106
251
  };
107
- const requestRefresh = async (refreshToken) => {
108
- const res = await fetch(refreshUrl, {
109
- method: "POST",
110
- headers: { "Content-Type": "application/json" },
111
- body: JSON.stringify({ refreshToken }),
112
- credentials: withCredentials ? "include" : "same-origin"
113
- });
114
- if (!res.ok) {
115
- throw new Error("Token refresh failed");
252
+ const getToken = () => {
253
+ return {
254
+ accessToken: accessTokenAccess.get(),
255
+ refreshToken: refreshStrategy === "body" ? refreshTokenAccess.get() : null
256
+ };
257
+ };
258
+ const setToken = (data) => {
259
+ if (data.accessToken !== void 0) {
260
+ accessTokenAccess.set(data.accessToken ?? null);
261
+ }
262
+ if (refreshStrategy === "body" && data.refreshToken !== void 0) {
263
+ refreshTokenAccess.set(data.refreshToken ?? null);
116
264
  }
117
- return await res.json();
118
265
  };
119
266
  instance.interceptors.request.use(
120
267
  (config) => {
121
- const { accessToken } = tokenAccess.get();
268
+ const accessToken = accessTokenAccess.get();
122
269
  if (accessToken) {
123
- config.headers = config.headers ?? {};
124
- config.headers.Authorization = `Bearer ${accessToken}`;
270
+ setAuthorizationHeader(config, accessToken);
125
271
  }
126
272
  return config;
127
273
  },
128
- (e) => Promise.reject(e)
274
+ (error) => Promise.reject(error)
129
275
  );
130
276
  instance.interceptors.response.use(
131
- (res) => res,
277
+ (response) => response,
132
278
  async (error) => {
133
- const response = error.response;
134
- const status = response?.status;
135
279
  const originalRequest = error.config ?? {};
136
- if (status !== 401) {
137
- let message = getErrorMessage?.(error) ?? response?.data?.error ?? "";
138
- if (!message && typeof status === "number") {
139
- switch (status) {
140
- case 0:
141
- message = "REST API \uC11C\uBC84\uC5D0 \uC811\uADFC\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4\n\uC11C\uBC84 \uAD00\uB9AC\uC790\uC5D0\uAC8C \uBB38\uC758\uD558\uC138\uC694";
142
- break;
143
- case 400:
144
- message = "\uC798\uBABB\uB41C \uC694\uCCAD\uC785\uB2C8\uB2E4.";
145
- break;
146
- case 404:
147
- message = "[404] REST API \uC694\uCCAD\uC5D0 \uC2E4\uD328\uD558\uC600\uC2B5\uB2C8\uB2E4";
148
- break;
149
- case 500:
150
- message = "\uC11C\uBC84\uC5D0\uC11C \uCC98\uB9AC \uC911 \uC624\uB958\uAC00 \uBC1C\uC0DD\uD558\uC600\uC2B5\uB2C8\uB2E4.";
151
- break;
152
- }
280
+ if (!shouldRefresh(error)) {
281
+ const message = getErrorMessage?.(error) ?? getDefaultErrorMessage(error);
282
+ if (message) {
283
+ onError?.(message, error);
153
284
  }
154
- if (message) onError?.(message, error);
155
285
  return Promise.reject(error);
156
286
  }
157
- if (originalRequest._retry) {
287
+ const requestUrl = originalRequest.url ?? "";
288
+ if (typeof requestUrl === "string" && requestUrl.includes(refreshUrl)) {
158
289
  clearAuth();
159
290
  return Promise.reject(error);
160
291
  }
161
- originalRequest._retry = true;
162
- const { refreshToken } = tokenAccess.get();
163
- if (!refreshToken) {
292
+ if (originalRequest._retry) {
164
293
  clearAuth();
165
294
  return Promise.reject(error);
166
295
  }
167
- if (isRefreshing) {
296
+ originalRequest._retry = true;
297
+ if (refreshManager.refreshing) {
168
298
  return new Promise((resolve, reject) => {
169
- subscribers.push((newToken) => {
170
- if (!newToken) return reject(error);
171
- originalRequest.headers = originalRequest.headers ?? {};
172
- originalRequest.headers.Authorization = `Bearer ${newToken}`;
173
- resolve(instance(originalRequest));
299
+ refreshManager.subscribe((newAccessToken) => {
300
+ if (!newAccessToken) {
301
+ reject(error);
302
+ return;
303
+ }
304
+ const retryRequest = setAuthorizationHeader(originalRequest, newAccessToken);
305
+ resolve(instance(retryRequest));
174
306
  });
175
307
  });
176
308
  }
177
- isRefreshing = true;
309
+ refreshManager.begin();
178
310
  try {
179
- const newTokens = await requestRefresh(refreshToken);
180
- tokenAccess.set(newTokens);
311
+ const newTokens = await requestRefresh({
312
+ refreshUrl,
313
+ withCredentials,
314
+ refreshStrategy,
315
+ refreshToken: refreshStrategy === "body" ? refreshTokenAccess.get() : null,
316
+ parseRefreshResponse
317
+ });
318
+ setToken(newTokens);
181
319
  onAuthStateChange?.("authenticated");
182
- isRefreshing = false;
183
- notifySubscribers(newTokens.accessToken);
184
- originalRequest.headers = originalRequest.headers ?? {};
185
- originalRequest.headers.Authorization = `Bearer ${newTokens.accessToken}`;
186
- return instance(originalRequest);
187
- } catch (e) {
188
- isRefreshing = false;
189
- notifySubscribers(null);
320
+ refreshManager.end();
321
+ refreshManager.notify(newTokens.accessToken);
322
+ const retryRequest = setAuthorizationHeader(originalRequest, newTokens.accessToken);
323
+ return instance(retryRequest);
324
+ } catch (refreshError) {
325
+ refreshManager.end();
326
+ refreshManager.notify(null);
190
327
  clearAuth();
191
- return Promise.reject(error);
328
+ return Promise.reject(refreshError);
192
329
  }
193
330
  }
194
331
  );
195
332
  return {
196
333
  axios: instance,
197
- setToken: (data) => tokenAccess.set(data),
198
- getToken: () => tokenAccess.get(),
199
- clearToken: () => clearAuth()
334
+ setToken,
335
+ getToken,
336
+ clearToken: clearAuth
200
337
  };
201
338
  }
202
339
  // Annotate the CommonJS export names for ESM import in node: