@chidchanun/bcp 0.1.19 → 0.1.21
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/README.md +35 -4
- package/docs/README.md +70 -7
- package/docs/error-handling.md +457 -0
- package/docs/hydration.md +190 -0
- package/docs/releases/0.1.20.md +124 -0
- package/docs/releases/0.1.21.md +92 -0
- package/package.json +5 -1
- package/packages/bundler/src/index.ts +46 -33
- package/packages/client/src/http-error.ts +516 -0
- package/packages/client/src/server.ts +22 -0
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
export interface HttpErrorOptions {
|
|
2
|
+
code?: string;
|
|
3
|
+
message?: string;
|
|
4
|
+
details?: unknown;
|
|
5
|
+
headers?: HeadersInit;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface HttpErrorPayload {
|
|
9
|
+
error: {
|
|
10
|
+
status: number;
|
|
11
|
+
code: string;
|
|
12
|
+
message: string;
|
|
13
|
+
details?: unknown;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface HttpErrorLike {
|
|
18
|
+
name: string;
|
|
19
|
+
status: number;
|
|
20
|
+
code: string;
|
|
21
|
+
message: string;
|
|
22
|
+
details?: unknown;
|
|
23
|
+
headers?: unknown;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class HttpError
|
|
27
|
+
extends Error {
|
|
28
|
+
readonly status: number;
|
|
29
|
+
readonly code: string;
|
|
30
|
+
readonly details: unknown;
|
|
31
|
+
readonly headers: Headers;
|
|
32
|
+
|
|
33
|
+
constructor(
|
|
34
|
+
status: number,
|
|
35
|
+
options: HttpErrorOptions = {}
|
|
36
|
+
) {
|
|
37
|
+
assertHttpErrorStatus(
|
|
38
|
+
status
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const defaults =
|
|
42
|
+
getHttpErrorDefaults(
|
|
43
|
+
status
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
super(
|
|
47
|
+
options.message ??
|
|
48
|
+
defaults.message
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
this.name =
|
|
52
|
+
"HttpError";
|
|
53
|
+
this.status =
|
|
54
|
+
status;
|
|
55
|
+
this.code =
|
|
56
|
+
normalizeErrorCode(
|
|
57
|
+
options.code ??
|
|
58
|
+
defaults.code
|
|
59
|
+
);
|
|
60
|
+
this.details =
|
|
61
|
+
options.details;
|
|
62
|
+
this.headers =
|
|
63
|
+
new Headers(
|
|
64
|
+
options.headers
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function createHttpError(
|
|
70
|
+
status: number,
|
|
71
|
+
options: HttpErrorOptions = {}
|
|
72
|
+
): HttpError {
|
|
73
|
+
return new HttpError(
|
|
74
|
+
status,
|
|
75
|
+
options
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function throwHttpError(
|
|
80
|
+
status: number,
|
|
81
|
+
options: HttpErrorOptions = {}
|
|
82
|
+
): never {
|
|
83
|
+
throw createHttpError(
|
|
84
|
+
status,
|
|
85
|
+
options
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function isHttpError(
|
|
90
|
+
error: unknown
|
|
91
|
+
): error is HttpErrorLike {
|
|
92
|
+
return (
|
|
93
|
+
error instanceof
|
|
94
|
+
HttpError ||
|
|
95
|
+
(
|
|
96
|
+
typeof error ===
|
|
97
|
+
"object" &&
|
|
98
|
+
error !== null &&
|
|
99
|
+
"name" in error &&
|
|
100
|
+
error.name ===
|
|
101
|
+
"HttpError" &&
|
|
102
|
+
"status" in error &&
|
|
103
|
+
typeof error.status ===
|
|
104
|
+
"number" &&
|
|
105
|
+
Number.isInteger(
|
|
106
|
+
error.status
|
|
107
|
+
) &&
|
|
108
|
+
error.status >= 400 &&
|
|
109
|
+
error.status <= 599 &&
|
|
110
|
+
"code" in error &&
|
|
111
|
+
typeof error.code ===
|
|
112
|
+
"string" &&
|
|
113
|
+
"message" in error &&
|
|
114
|
+
typeof error.message ===
|
|
115
|
+
"string"
|
|
116
|
+
)
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function toErrorResponse(
|
|
121
|
+
error: unknown,
|
|
122
|
+
fallback: HttpErrorOptions = {}
|
|
123
|
+
): Response {
|
|
124
|
+
const httpError =
|
|
125
|
+
isHttpError(
|
|
126
|
+
error
|
|
127
|
+
)
|
|
128
|
+
? normalizeHttpError(
|
|
129
|
+
error
|
|
130
|
+
)
|
|
131
|
+
: createHttpError(
|
|
132
|
+
500,
|
|
133
|
+
fallback
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
return createHttpErrorResponse(
|
|
137
|
+
httpError
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function errorResponse(
|
|
142
|
+
status: number,
|
|
143
|
+
options: HttpErrorOptions = {}
|
|
144
|
+
): Response {
|
|
145
|
+
return createHttpErrorResponse(
|
|
146
|
+
createHttpError(
|
|
147
|
+
status,
|
|
148
|
+
options
|
|
149
|
+
)
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function badRequest(
|
|
154
|
+
message = "Bad Request",
|
|
155
|
+
details?: unknown
|
|
156
|
+
): Response {
|
|
157
|
+
return errorResponse(
|
|
158
|
+
400,
|
|
159
|
+
{
|
|
160
|
+
code:
|
|
161
|
+
"bad_request",
|
|
162
|
+
message,
|
|
163
|
+
details,
|
|
164
|
+
}
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function unauthorized(
|
|
169
|
+
message = "Unauthorized",
|
|
170
|
+
details?: unknown
|
|
171
|
+
): Response {
|
|
172
|
+
return errorResponse(
|
|
173
|
+
401,
|
|
174
|
+
{
|
|
175
|
+
code:
|
|
176
|
+
"unauthorized",
|
|
177
|
+
message,
|
|
178
|
+
details,
|
|
179
|
+
}
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function forbidden(
|
|
184
|
+
message = "Forbidden",
|
|
185
|
+
details?: unknown
|
|
186
|
+
): Response {
|
|
187
|
+
return errorResponse(
|
|
188
|
+
403,
|
|
189
|
+
{
|
|
190
|
+
code:
|
|
191
|
+
"forbidden",
|
|
192
|
+
message,
|
|
193
|
+
details,
|
|
194
|
+
}
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function notFoundResponse(
|
|
199
|
+
message = "Not Found",
|
|
200
|
+
details?: unknown
|
|
201
|
+
): Response {
|
|
202
|
+
return errorResponse(
|
|
203
|
+
404,
|
|
204
|
+
{
|
|
205
|
+
code:
|
|
206
|
+
"not_found",
|
|
207
|
+
message,
|
|
208
|
+
details,
|
|
209
|
+
}
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function conflict(
|
|
214
|
+
message = "Conflict",
|
|
215
|
+
details?: unknown
|
|
216
|
+
): Response {
|
|
217
|
+
return errorResponse(
|
|
218
|
+
409,
|
|
219
|
+
{
|
|
220
|
+
code:
|
|
221
|
+
"conflict",
|
|
222
|
+
message,
|
|
223
|
+
details,
|
|
224
|
+
}
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function unprocessableEntity(
|
|
229
|
+
message = "Unprocessable Entity",
|
|
230
|
+
details?: unknown
|
|
231
|
+
): Response {
|
|
232
|
+
return errorResponse(
|
|
233
|
+
422,
|
|
234
|
+
{
|
|
235
|
+
code:
|
|
236
|
+
"unprocessable_entity",
|
|
237
|
+
message,
|
|
238
|
+
details,
|
|
239
|
+
}
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function tooManyRequests(
|
|
244
|
+
message = "Too Many Requests",
|
|
245
|
+
options: {
|
|
246
|
+
details?: unknown;
|
|
247
|
+
retryAfter?:
|
|
248
|
+
| number
|
|
249
|
+
| string;
|
|
250
|
+
} = {}
|
|
251
|
+
): Response {
|
|
252
|
+
const headers =
|
|
253
|
+
new Headers();
|
|
254
|
+
|
|
255
|
+
if (
|
|
256
|
+
options.retryAfter !==
|
|
257
|
+
undefined
|
|
258
|
+
) {
|
|
259
|
+
headers.set(
|
|
260
|
+
"Retry-After",
|
|
261
|
+
String(
|
|
262
|
+
options.retryAfter
|
|
263
|
+
)
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return errorResponse(
|
|
268
|
+
429,
|
|
269
|
+
{
|
|
270
|
+
code:
|
|
271
|
+
"too_many_requests",
|
|
272
|
+
message,
|
|
273
|
+
details:
|
|
274
|
+
options.details,
|
|
275
|
+
headers,
|
|
276
|
+
}
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export function internalServerError(
|
|
281
|
+
message = "Internal Server Error"
|
|
282
|
+
): Response {
|
|
283
|
+
return errorResponse(
|
|
284
|
+
500,
|
|
285
|
+
{
|
|
286
|
+
code:
|
|
287
|
+
"internal_server_error",
|
|
288
|
+
message,
|
|
289
|
+
}
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export function serviceUnavailable(
|
|
294
|
+
message = "Service Unavailable",
|
|
295
|
+
details?: unknown
|
|
296
|
+
): Response {
|
|
297
|
+
return errorResponse(
|
|
298
|
+
503,
|
|
299
|
+
{
|
|
300
|
+
code:
|
|
301
|
+
"service_unavailable",
|
|
302
|
+
message,
|
|
303
|
+
details,
|
|
304
|
+
}
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function createHttpErrorResponse(
|
|
309
|
+
error: HttpError
|
|
310
|
+
): Response {
|
|
311
|
+
const payload:
|
|
312
|
+
HttpErrorPayload = {
|
|
313
|
+
error: {
|
|
314
|
+
status:
|
|
315
|
+
error.status,
|
|
316
|
+
code:
|
|
317
|
+
error.code,
|
|
318
|
+
message:
|
|
319
|
+
error.message,
|
|
320
|
+
},
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
if (
|
|
324
|
+
error.details !==
|
|
325
|
+
undefined
|
|
326
|
+
) {
|
|
327
|
+
payload.error.details =
|
|
328
|
+
error.details;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const headers =
|
|
332
|
+
new Headers(
|
|
333
|
+
error.headers
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
if (
|
|
337
|
+
!headers.has(
|
|
338
|
+
"Cache-Control"
|
|
339
|
+
)
|
|
340
|
+
) {
|
|
341
|
+
headers.set(
|
|
342
|
+
"Cache-Control",
|
|
343
|
+
"no-store"
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return Response.json(
|
|
348
|
+
payload,
|
|
349
|
+
{
|
|
350
|
+
status:
|
|
351
|
+
error.status,
|
|
352
|
+
headers,
|
|
353
|
+
}
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function normalizeHttpError(
|
|
358
|
+
error: HttpErrorLike
|
|
359
|
+
): HttpError {
|
|
360
|
+
if (
|
|
361
|
+
error instanceof
|
|
362
|
+
HttpError
|
|
363
|
+
) {
|
|
364
|
+
return error;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return createHttpError(
|
|
368
|
+
error.status,
|
|
369
|
+
{
|
|
370
|
+
code:
|
|
371
|
+
error.code,
|
|
372
|
+
message:
|
|
373
|
+
error.message,
|
|
374
|
+
details:
|
|
375
|
+
error.details,
|
|
376
|
+
headers:
|
|
377
|
+
error.headers instanceof
|
|
378
|
+
Headers
|
|
379
|
+
? error.headers
|
|
380
|
+
: undefined,
|
|
381
|
+
}
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function assertHttpErrorStatus(
|
|
386
|
+
status: number
|
|
387
|
+
): void {
|
|
388
|
+
if (
|
|
389
|
+
!Number.isInteger(
|
|
390
|
+
status
|
|
391
|
+
) ||
|
|
392
|
+
status < 400 ||
|
|
393
|
+
status > 599
|
|
394
|
+
) {
|
|
395
|
+
throw new RangeError(
|
|
396
|
+
`BCP Framework: HTTP error status must be an integer from 400 to 599, received ${status}.`
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function normalizeErrorCode(
|
|
402
|
+
value: string
|
|
403
|
+
): string {
|
|
404
|
+
const normalized =
|
|
405
|
+
value
|
|
406
|
+
.trim()
|
|
407
|
+
.toLowerCase()
|
|
408
|
+
.replace(
|
|
409
|
+
/[^a-z0-9]+/g,
|
|
410
|
+
"_"
|
|
411
|
+
)
|
|
412
|
+
.replace(
|
|
413
|
+
/^_+|_+$/g,
|
|
414
|
+
""
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
if (!normalized) {
|
|
418
|
+
throw new Error(
|
|
419
|
+
"BCP Framework: HTTP error code must not be empty."
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return normalized;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function getHttpErrorDefaults(
|
|
427
|
+
status: number
|
|
428
|
+
): {
|
|
429
|
+
code: string;
|
|
430
|
+
message: string;
|
|
431
|
+
} {
|
|
432
|
+
switch (status) {
|
|
433
|
+
case 400:
|
|
434
|
+
return {
|
|
435
|
+
code: "bad_request",
|
|
436
|
+
message: "Bad Request",
|
|
437
|
+
};
|
|
438
|
+
case 401:
|
|
439
|
+
return {
|
|
440
|
+
code: "unauthorized",
|
|
441
|
+
message: "Unauthorized",
|
|
442
|
+
};
|
|
443
|
+
case 403:
|
|
444
|
+
return {
|
|
445
|
+
code: "forbidden",
|
|
446
|
+
message: "Forbidden",
|
|
447
|
+
};
|
|
448
|
+
case 404:
|
|
449
|
+
return {
|
|
450
|
+
code: "not_found",
|
|
451
|
+
message: "Not Found",
|
|
452
|
+
};
|
|
453
|
+
case 405:
|
|
454
|
+
return {
|
|
455
|
+
code: "method_not_allowed",
|
|
456
|
+
message: "Method Not Allowed",
|
|
457
|
+
};
|
|
458
|
+
case 409:
|
|
459
|
+
return {
|
|
460
|
+
code: "conflict",
|
|
461
|
+
message: "Conflict",
|
|
462
|
+
};
|
|
463
|
+
case 413:
|
|
464
|
+
return {
|
|
465
|
+
code: "payload_too_large",
|
|
466
|
+
message: "Payload Too Large",
|
|
467
|
+
};
|
|
468
|
+
case 415:
|
|
469
|
+
return {
|
|
470
|
+
code: "unsupported_media_type",
|
|
471
|
+
message: "Unsupported Media Type",
|
|
472
|
+
};
|
|
473
|
+
case 422:
|
|
474
|
+
return {
|
|
475
|
+
code: "unprocessable_entity",
|
|
476
|
+
message: "Unprocessable Entity",
|
|
477
|
+
};
|
|
478
|
+
case 429:
|
|
479
|
+
return {
|
|
480
|
+
code: "too_many_requests",
|
|
481
|
+
message: "Too Many Requests",
|
|
482
|
+
};
|
|
483
|
+
case 500:
|
|
484
|
+
return {
|
|
485
|
+
code: "internal_server_error",
|
|
486
|
+
message: "Internal Server Error",
|
|
487
|
+
};
|
|
488
|
+
case 501:
|
|
489
|
+
return {
|
|
490
|
+
code: "not_implemented",
|
|
491
|
+
message: "Not Implemented",
|
|
492
|
+
};
|
|
493
|
+
case 502:
|
|
494
|
+
return {
|
|
495
|
+
code: "bad_gateway",
|
|
496
|
+
message: "Bad Gateway",
|
|
497
|
+
};
|
|
498
|
+
case 503:
|
|
499
|
+
return {
|
|
500
|
+
code: "service_unavailable",
|
|
501
|
+
message: "Service Unavailable",
|
|
502
|
+
};
|
|
503
|
+
case 504:
|
|
504
|
+
return {
|
|
505
|
+
code: "gateway_timeout",
|
|
506
|
+
message: "Gateway Timeout",
|
|
507
|
+
};
|
|
508
|
+
default:
|
|
509
|
+
return {
|
|
510
|
+
code:
|
|
511
|
+
`http_${status}`,
|
|
512
|
+
message:
|
|
513
|
+
`HTTP ${status}`,
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
}
|
|
@@ -22,6 +22,28 @@ export {
|
|
|
22
22
|
type RedirectStatus,
|
|
23
23
|
} from "../../server/src/server-response.js";
|
|
24
24
|
|
|
25
|
+
export {
|
|
26
|
+
badRequest,
|
|
27
|
+
conflict,
|
|
28
|
+
createHttpError,
|
|
29
|
+
errorResponse,
|
|
30
|
+
forbidden,
|
|
31
|
+
HttpError,
|
|
32
|
+
internalServerError,
|
|
33
|
+
isHttpError,
|
|
34
|
+
notFoundResponse,
|
|
35
|
+
serviceUnavailable,
|
|
36
|
+
throwHttpError,
|
|
37
|
+
tooManyRequests,
|
|
38
|
+
toErrorResponse,
|
|
39
|
+
unauthorized,
|
|
40
|
+
unprocessableEntity,
|
|
41
|
+
|
|
42
|
+
type HttpErrorLike,
|
|
43
|
+
type HttpErrorOptions,
|
|
44
|
+
type HttpErrorPayload,
|
|
45
|
+
} from "./http-error.js";
|
|
46
|
+
|
|
25
47
|
export {
|
|
26
48
|
createSession,
|
|
27
49
|
createSessionToken,
|