@web-ts-toolkit/express-response-handler 0.32.0 → 0.34.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/README.md +50 -2
- package/create-handler.d.mts +2 -1
- package/create-handler.d.ts +2 -1
- package/create-handler.js +358 -125
- package/create-handler.mjs +361 -126
- package/error-format.d.mts +10 -3
- package/error-format.d.ts +10 -3
- package/error-format.js +69 -15
- package/error-format.mjs +65 -15
- package/http-response.d.mts +2 -6
- package/http-response.d.ts +2 -6
- package/http-response.js +140 -13
- package/http-response.mjs +140 -13
- package/index.d.mts +5 -3
- package/index.d.ts +5 -3
- package/index.js +365 -129
- package/index.mjs +366 -129
- package/llms.txt +6 -0
- package/package.json +8 -4
- package/public-types.d.mts +13 -17
- package/public-types.d.ts +13 -17
- package/responses/csv.d.mts +12 -6
- package/responses/csv.d.ts +12 -6
- package/responses/csv.js +133 -12
- package/responses/csv.mjs +133 -12
- package/responses/index.d.mts +2 -1
- package/responses/index.d.ts +2 -1
- package/responses/index.js +11 -3
- package/responses/index.mjs +9 -2
- package/responses/success.js +7 -1
- package/responses/success.mjs +7 -1
package/index.mjs
CHANGED
|
@@ -5,6 +5,41 @@ import { isArray as isArray2, isFunction, isPromise } from "@web-ts-toolkit/util
|
|
|
5
5
|
// src/responses/csv.ts
|
|
6
6
|
import { format } from "@fast-csv/format";
|
|
7
7
|
import { castArray, isBoolean, isPlainObject } from "@web-ts-toolkit/utils";
|
|
8
|
+
import { once } from "events";
|
|
9
|
+
var CONTROL_CHARACTER_PATTERN = /[\u0000-\u001f\u007f]/u;
|
|
10
|
+
var isIterable = (value) => typeof value === "object" && value !== null && Symbol.iterator in value;
|
|
11
|
+
var isAsyncIterable = (value) => typeof value === "object" && value !== null && Symbol.asyncIterator in value;
|
|
12
|
+
var isLazyCsvSource = (value) => !Array.isArray(value) && (isIterable(value) || isAsyncIterable(value));
|
|
13
|
+
var createAsyncIterator = (source) => {
|
|
14
|
+
if (isAsyncIterable(source)) {
|
|
15
|
+
return source[Symbol.asyncIterator]();
|
|
16
|
+
}
|
|
17
|
+
const iterator = (isIterable(source) ? source : castArray(source))[Symbol.iterator]();
|
|
18
|
+
return {
|
|
19
|
+
next: () => Promise.resolve(iterator.next()),
|
|
20
|
+
return: (value) => {
|
|
21
|
+
if (iterator.return) {
|
|
22
|
+
return Promise.resolve(iterator.return(value));
|
|
23
|
+
}
|
|
24
|
+
return Promise.resolve({ done: true, value });
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
var encodeRFC5987Value = (value) => encodeURIComponent(value).replace(/[!'()*]/g, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
29
|
+
var createQuotedFilenameFallback = (filename) => {
|
|
30
|
+
const fallback = Array.from(filename.replace(/[\\/]/g, "_"), (char) => {
|
|
31
|
+
const codePoint = char.codePointAt(0) ?? 0;
|
|
32
|
+
return codePoint >= 32 && codePoint <= 126 ? char : "_";
|
|
33
|
+
}).join("");
|
|
34
|
+
return fallback.replace(/(["\\])/g, "\\$1");
|
|
35
|
+
};
|
|
36
|
+
var createAttachmentContentDisposition = (filename) => {
|
|
37
|
+
if (CONTROL_CHARACTER_PATTERN.test(filename)) {
|
|
38
|
+
throw new TypeError("CSV filename cannot contain control characters");
|
|
39
|
+
}
|
|
40
|
+
const fallback = createQuotedFilenameFallback(filename);
|
|
41
|
+
return `attachment; filename="${fallback}"; filename*=UTF-8''${encodeRFC5987Value(filename)}`;
|
|
42
|
+
};
|
|
8
43
|
var csvResponseBrand = /* @__PURE__ */ Symbol.for("@web-ts-toolkit/express-response-handler.csv-response");
|
|
9
44
|
var isCSVResponse = (value) => {
|
|
10
45
|
if (value instanceof CSVResponse) {
|
|
@@ -21,35 +56,127 @@ _a = csvResponseBrand;
|
|
|
21
56
|
var CSVResponse = class {
|
|
22
57
|
constructor(dataset = [], options = {}) {
|
|
23
58
|
this[_a] = true;
|
|
24
|
-
this.dataset =
|
|
59
|
+
this.dataset = dataset;
|
|
25
60
|
this.filename = options.filename || "download.csv";
|
|
26
61
|
this.processor = options.processor || ((value) => value);
|
|
27
|
-
if (isBoolean(options.headers)) {
|
|
62
|
+
if (isBoolean(options.headers) || Array.isArray(options.headers)) {
|
|
28
63
|
this.headers = options.headers;
|
|
29
|
-
} else if (this.dataset.length > 0) {
|
|
64
|
+
} else if (Array.isArray(this.dataset) && this.dataset.length > 0) {
|
|
30
65
|
this.headers = isPlainObject(this.dataset[0]);
|
|
66
|
+
} else if (isLazyCsvSource(this.dataset)) {
|
|
67
|
+
throw new TypeError("CSV lazy sources require an explicit headers option");
|
|
68
|
+
} else {
|
|
69
|
+
this.headers = isPlainObject(this.dataset);
|
|
31
70
|
}
|
|
32
71
|
}
|
|
33
|
-
streamCsv(res) {
|
|
72
|
+
streamCsv(res, onBeforeOutputError) {
|
|
34
73
|
const stream = format({ headers: this.headers });
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
74
|
+
let outputStarted = false;
|
|
75
|
+
let failed = false;
|
|
76
|
+
let finished = false;
|
|
77
|
+
let activeIterator = null;
|
|
78
|
+
let abortStreaming = () => void 0;
|
|
79
|
+
const abortSignal = new Promise((_, reject) => {
|
|
80
|
+
abortStreaming = reject;
|
|
81
|
+
});
|
|
82
|
+
abortSignal.catch(() => void 0);
|
|
83
|
+
const cleanup = () => {
|
|
84
|
+
stream.off("error", fail);
|
|
85
|
+
res.off?.("error", fail);
|
|
86
|
+
res.off?.("close", abort);
|
|
87
|
+
res.off?.("finish", markFinished);
|
|
88
|
+
};
|
|
89
|
+
const closeActiveIterator = () => {
|
|
90
|
+
const iterator = activeIterator;
|
|
91
|
+
activeIterator = null;
|
|
92
|
+
if (iterator?.return) {
|
|
93
|
+
void Promise.resolve(iterator.return()).catch(() => void 0);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
const closeResponse = (error) => {
|
|
39
97
|
if (res.destroy) {
|
|
40
98
|
res.destroy(error);
|
|
41
99
|
return;
|
|
42
100
|
}
|
|
43
101
|
res.end();
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
102
|
+
};
|
|
103
|
+
const fail = (error) => {
|
|
104
|
+
if (failed) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
failed = true;
|
|
108
|
+
cleanup();
|
|
109
|
+
const normalizedError = error instanceof Error ? error : new Error("CSV response streaming failed");
|
|
110
|
+
abortStreaming(normalizedError);
|
|
111
|
+
closeActiveIterator();
|
|
112
|
+
if (!outputStarted && !res.headersSent) {
|
|
113
|
+
onBeforeOutputError?.(normalizedError);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
stream.destroy();
|
|
117
|
+
closeResponse(normalizedError);
|
|
118
|
+
};
|
|
119
|
+
const abort = () => {
|
|
120
|
+
if (!finished) {
|
|
121
|
+
fail(new Error("CSV response streaming aborted"));
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
const markFinished = () => {
|
|
125
|
+
finished = true;
|
|
126
|
+
};
|
|
127
|
+
const writeRow = async (row) => {
|
|
128
|
+
outputStarted = true;
|
|
129
|
+
if (!stream.write(row)) {
|
|
130
|
+
await Promise.race([once(stream, "drain"), abortSignal]);
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
stream.on("error", fail);
|
|
134
|
+
stream.on("end", cleanup);
|
|
135
|
+
res.on?.("error", fail);
|
|
136
|
+
res.on?.("close", abort);
|
|
137
|
+
res.on?.("finish", markFinished);
|
|
138
|
+
const pump = async () => {
|
|
139
|
+
try {
|
|
140
|
+
const iterator = createAsyncIterator(this.dataset);
|
|
141
|
+
activeIterator = iterator;
|
|
142
|
+
const first = await iterator.next();
|
|
143
|
+
const firstRow = first.done ? void 0 : this.processor(first.value);
|
|
144
|
+
const contentDisposition = createAttachmentContentDisposition(this.filename);
|
|
145
|
+
res.set("Content-Type", "text/csv");
|
|
146
|
+
res.set("Content-Disposition", contentDisposition);
|
|
147
|
+
stream.pipe(res);
|
|
148
|
+
if (!first.done) {
|
|
149
|
+
await writeRow(firstRow);
|
|
150
|
+
}
|
|
151
|
+
while (!failed) {
|
|
152
|
+
const row = await Promise.race([iterator.next(), abortSignal]);
|
|
153
|
+
if (row.done) {
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
await writeRow(this.processor(row.value));
|
|
157
|
+
}
|
|
158
|
+
if (!failed) {
|
|
159
|
+
finished = true;
|
|
160
|
+
stream.end();
|
|
161
|
+
}
|
|
162
|
+
} catch (error) {
|
|
163
|
+
fail(error);
|
|
164
|
+
} finally {
|
|
165
|
+
activeIterator = null;
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
void pump();
|
|
48
169
|
}
|
|
49
170
|
};
|
|
50
171
|
|
|
51
172
|
// src/responses/index.ts
|
|
52
173
|
var responseBrand = /* @__PURE__ */ Symbol.for("@web-ts-toolkit/express-response-handler.response");
|
|
174
|
+
var validateHttpStatusCode = (statusCode) => {
|
|
175
|
+
if (typeof statusCode !== "number" || !Number.isFinite(statusCode) || !Number.isInteger(statusCode) || statusCode < 100 || statusCode > 599) {
|
|
176
|
+
throw new TypeError("statusCode must be an integer HTTP status code between 100 and 599");
|
|
177
|
+
}
|
|
178
|
+
return statusCode;
|
|
179
|
+
};
|
|
53
180
|
var isResponse = (value) => {
|
|
54
181
|
if (value instanceof Response) {
|
|
55
182
|
return true;
|
|
@@ -65,7 +192,7 @@ _a2 = responseBrand;
|
|
|
65
192
|
var Response = class {
|
|
66
193
|
constructor(statusCode = 200, data) {
|
|
67
194
|
this[_a2] = true;
|
|
68
|
-
this.statusCode = statusCode;
|
|
195
|
+
this.statusCode = validateHttpStatusCode(statusCode);
|
|
69
196
|
this.data = data;
|
|
70
197
|
}
|
|
71
198
|
};
|
|
@@ -198,15 +325,23 @@ var HttpResponse = {
|
|
|
198
325
|
import {
|
|
199
326
|
createAip193ErrorInfoDetail,
|
|
200
327
|
getCanonicalStatus,
|
|
328
|
+
isHttpErrorStatusCode,
|
|
201
329
|
toAip193ErrorPayload,
|
|
202
|
-
toRfc9457ErrorPayload
|
|
330
|
+
toRfc9457ErrorPayload,
|
|
331
|
+
validateHttpErrorStatusCode
|
|
203
332
|
} from "@web-ts-toolkit/http-errors";
|
|
204
333
|
import { isArray, isPlainObject as isPlainObject2, isString, toStringRecord } from "@web-ts-toolkit/utils";
|
|
205
|
-
var
|
|
206
|
-
|
|
207
|
-
|
|
334
|
+
var FALLBACK_ERROR_STATUS = 500;
|
|
335
|
+
var FALLBACK_ERROR_MESSAGE = "Internal Server Error";
|
|
336
|
+
var validateErrorStatusCode = (statusCode, source) => {
|
|
337
|
+
return validateHttpErrorStatusCode(statusCode, source);
|
|
338
|
+
};
|
|
339
|
+
var toErrorStatusCode = (status, code, fallbackStatusCode = FALLBACK_ERROR_STATUS) => {
|
|
340
|
+
const statusCode = typeof status === "number" ? status : code;
|
|
341
|
+
if (statusCode === void 0) {
|
|
342
|
+
return fallbackStatusCode;
|
|
208
343
|
}
|
|
209
|
-
return
|
|
344
|
+
return validateErrorStatusCode(statusCode, "error status");
|
|
210
345
|
};
|
|
211
346
|
var toOptionalString = (value) => typeof value === "string" ? value : void 0;
|
|
212
347
|
var toMetadata = (value) => {
|
|
@@ -218,6 +353,41 @@ var toArray = (value) => {
|
|
|
218
353
|
}
|
|
219
354
|
return isArray(value) ? value : [value];
|
|
220
355
|
};
|
|
356
|
+
var describeThrown = (value) => {
|
|
357
|
+
try {
|
|
358
|
+
if (value === null) {
|
|
359
|
+
return "null";
|
|
360
|
+
}
|
|
361
|
+
if (value === void 0) {
|
|
362
|
+
return "undefined";
|
|
363
|
+
}
|
|
364
|
+
if (typeof value === "symbol") {
|
|
365
|
+
return value.toString();
|
|
366
|
+
}
|
|
367
|
+
const stringified = typeof value === "object" && value !== null ? value.message : void 0;
|
|
368
|
+
if (typeof stringified === "string" && stringified.length > 0) {
|
|
369
|
+
return stringified;
|
|
370
|
+
}
|
|
371
|
+
return String(value);
|
|
372
|
+
} catch {
|
|
373
|
+
return FALLBACK_ERROR_MESSAGE;
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
var normalizeThrownError = (value) => {
|
|
377
|
+
if (value instanceof Error) {
|
|
378
|
+
return value;
|
|
379
|
+
}
|
|
380
|
+
const message = describeThrown(value);
|
|
381
|
+
if (isPlainObject2(value)) {
|
|
382
|
+
const record = value;
|
|
383
|
+
const normalized = {
|
|
384
|
+
...record,
|
|
385
|
+
message
|
|
386
|
+
};
|
|
387
|
+
return normalized;
|
|
388
|
+
}
|
|
389
|
+
return { message };
|
|
390
|
+
};
|
|
221
391
|
var toHttpErrorShape = (error, fallbackDomain) => ({
|
|
222
392
|
statusCode: error.statusCode ?? 500,
|
|
223
393
|
status: error.status,
|
|
@@ -238,8 +408,8 @@ var toProblemDetailsSource = (result) => {
|
|
|
238
408
|
return isPlainObject2(result) ? result : void 0;
|
|
239
409
|
};
|
|
240
410
|
var defaultErrorMessageProvider = function(error) {
|
|
241
|
-
|
|
242
|
-
return
|
|
411
|
+
void error;
|
|
412
|
+
return FALLBACK_ERROR_MESSAGE;
|
|
243
413
|
};
|
|
244
414
|
var toSimpleErrorPayload = (result) => isString(result) ? { message: result } : { ...result };
|
|
245
415
|
var toStructuredHttpErrorPayload = (error, errorDomain) => toAip193ErrorPayload(toHttpErrorShape(error, errorDomain), errorDomain);
|
|
@@ -247,7 +417,7 @@ var toRfc9457HttpErrorPayload = (error, errorDomain) => toRfc9457ErrorPayload(to
|
|
|
247
417
|
var toStructuredGenericErrorPayload = (result, errorDomain) => {
|
|
248
418
|
if (isPlainObject2(result) && isPlainObject2(result.error)) {
|
|
249
419
|
const error = result.error;
|
|
250
|
-
const statusCode =
|
|
420
|
+
const statusCode = toErrorStatusCode(error.code, void 0);
|
|
251
421
|
const status = typeof error.status === "string" ? error.status : getCanonicalStatus(statusCode);
|
|
252
422
|
const message = typeof error.message === "string" ? error.message : "";
|
|
253
423
|
const details = toArray(error.details);
|
|
@@ -261,7 +431,7 @@ var toStructuredGenericErrorPayload = (result, errorDomain) => {
|
|
|
261
431
|
};
|
|
262
432
|
}
|
|
263
433
|
if (isPlainObject2(result)) {
|
|
264
|
-
const statusCode =
|
|
434
|
+
const statusCode = toErrorStatusCode(result.code, void 0);
|
|
265
435
|
const status = typeof result.status === "string" ? result.status : getCanonicalStatus(statusCode);
|
|
266
436
|
const message = typeof result.message === "string" ? result.message : "";
|
|
267
437
|
const details = toArray(result.details) || [createAip193ErrorInfoDetail(status, errorDomain)];
|
|
@@ -276,17 +446,17 @@ var toStructuredGenericErrorPayload = (result, errorDomain) => {
|
|
|
276
446
|
}
|
|
277
447
|
return {
|
|
278
448
|
error: {
|
|
279
|
-
code:
|
|
280
|
-
status: getCanonicalStatus(
|
|
449
|
+
code: FALLBACK_ERROR_STATUS,
|
|
450
|
+
status: getCanonicalStatus(FALLBACK_ERROR_STATUS),
|
|
281
451
|
message: String(result),
|
|
282
|
-
details: [createAip193ErrorInfoDetail(getCanonicalStatus(
|
|
452
|
+
details: [createAip193ErrorInfoDetail(getCanonicalStatus(FALLBACK_ERROR_STATUS), errorDomain)]
|
|
283
453
|
}
|
|
284
454
|
};
|
|
285
455
|
};
|
|
286
456
|
var toRfc9457GenericErrorPayload = (result) => {
|
|
287
457
|
const problem = toProblemDetailsSource(result);
|
|
288
458
|
if (problem) {
|
|
289
|
-
const statusCode =
|
|
459
|
+
const statusCode = toErrorStatusCode(problem.status, problem.code);
|
|
290
460
|
return toRfc9457ErrorPayload({
|
|
291
461
|
statusCode,
|
|
292
462
|
message: toOptionalString(problem.detail) ?? toOptionalString(problem.message) ?? "",
|
|
@@ -297,7 +467,7 @@ var toRfc9457GenericErrorPayload = (result) => {
|
|
|
297
467
|
});
|
|
298
468
|
}
|
|
299
469
|
return toRfc9457ErrorPayload({
|
|
300
|
-
statusCode:
|
|
470
|
+
statusCode: FALLBACK_ERROR_STATUS,
|
|
301
471
|
message: String(result)
|
|
302
472
|
});
|
|
303
473
|
};
|
|
@@ -310,11 +480,9 @@ var ErrorFormats = {
|
|
|
310
480
|
};
|
|
311
481
|
|
|
312
482
|
// src/create-handler.ts
|
|
313
|
-
var promisify = (fn) => (value) => Promise.resolve().then(() => fn(value));
|
|
314
|
-
var invokePostHook = (hook, value) => {
|
|
315
|
-
void hook(value).catch(() => void 0);
|
|
316
|
-
};
|
|
483
|
+
var promisify = (fn) => (value) => Promise.resolve().then(() => fn(value)).then(() => void 0);
|
|
317
484
|
var RFC_9457_CONTENT_TYPE = "application/problem+json";
|
|
485
|
+
var SUPPORTED_RFC_9457_CONTENT_TYPES = /* @__PURE__ */ new Set(["application/problem+json", "application/json"]);
|
|
318
486
|
var shouldSkipResponse = (res, event) => res.headersSent || event.canceled;
|
|
319
487
|
var sendProblemJson = (res, statusCode, payload, contentType) => {
|
|
320
488
|
res.status(statusCode);
|
|
@@ -323,30 +491,34 @@ var sendProblemJson = (res, statusCode, payload, contentType) => {
|
|
|
323
491
|
};
|
|
324
492
|
var sendHttpErrorByFormat = {
|
|
325
493
|
[ErrorFormats.simple]: (res, error) => {
|
|
494
|
+
const statusCode = validateErrorStatusCode(error.statusCode ?? FALLBACK_ERROR_STATUS, "error.statusCode");
|
|
326
495
|
const payload = { message: error.message ?? "" };
|
|
327
496
|
if (error.errors !== void 0) {
|
|
328
497
|
payload.errors = error.errors;
|
|
329
498
|
}
|
|
330
|
-
res.status(
|
|
499
|
+
res.status(statusCode).send(payload);
|
|
331
500
|
},
|
|
332
501
|
[ErrorFormats.aip193]: (res, error, domain) => {
|
|
333
|
-
|
|
502
|
+
const statusCode = validateErrorStatusCode(error.statusCode ?? FALLBACK_ERROR_STATUS, "error.statusCode");
|
|
503
|
+
res.status(statusCode).send(toStructuredHttpErrorPayload(error, domain));
|
|
334
504
|
},
|
|
335
505
|
[ErrorFormats.rfc9457]: (res, error, domain) => {
|
|
336
|
-
|
|
506
|
+
const statusCode = validateErrorStatusCode(error.statusCode ?? FALLBACK_ERROR_STATUS, "error.statusCode");
|
|
507
|
+
sendProblemJson(res, statusCode, toRfc9457HttpErrorPayload(error, domain), RFC_9457_CONTENT_TYPE);
|
|
337
508
|
}
|
|
338
509
|
};
|
|
339
510
|
var sendGenericErrorByFormat = {
|
|
340
511
|
[ErrorFormats.simple]: (res, result) => {
|
|
341
|
-
res.status(
|
|
512
|
+
res.status(FALLBACK_ERROR_STATUS).send(toSimpleErrorPayload(result));
|
|
342
513
|
},
|
|
343
514
|
[ErrorFormats.aip193]: (res, result, domain) => {
|
|
344
515
|
const payload = toStructuredGenericErrorPayload(result, domain);
|
|
345
|
-
res.status(payload.error.code).send(payload);
|
|
516
|
+
res.status(validateErrorStatusCode(payload.error.code, "error.code")).send(payload);
|
|
346
517
|
},
|
|
347
518
|
[ErrorFormats.rfc9457]: (res, result) => {
|
|
348
519
|
const payload = toRfc9457GenericErrorPayload(result);
|
|
349
|
-
|
|
520
|
+
const statusCode = payload.status ?? FALLBACK_ERROR_STATUS;
|
|
521
|
+
sendProblemJson(res, validateErrorStatusCode(statusCode, "problem.status"), payload, RFC_9457_CONTENT_TYPE);
|
|
350
522
|
}
|
|
351
523
|
};
|
|
352
524
|
var assertMiddleware = (fn) => {
|
|
@@ -366,10 +538,23 @@ var normalizeMiddlewareList = (fns) => {
|
|
|
366
538
|
assertMiddleware(fns[0]);
|
|
367
539
|
return [fns[0]];
|
|
368
540
|
};
|
|
369
|
-
|
|
541
|
+
var validateHandlerConfig = (options) => {
|
|
370
542
|
const errorFormat = options.errorFormat ?? ErrorFormats.simple;
|
|
371
543
|
const errorDomain = options.errorDomain ?? "express-response-handler";
|
|
372
544
|
const rfc9457ContentType = options.rfc9457ContentType ?? RFC_9457_CONTENT_TYPE;
|
|
545
|
+
assert.ok(
|
|
546
|
+
Object.values(ErrorFormats).includes(errorFormat),
|
|
547
|
+
`errorFormat must be one of: ${Object.values(ErrorFormats).join(", ")}`
|
|
548
|
+
);
|
|
549
|
+
assert.ok(typeof errorDomain === "string" && errorDomain.length > 0, "errorDomain must be a non-empty string");
|
|
550
|
+
assert.ok(
|
|
551
|
+
SUPPORTED_RFC_9457_CONTENT_TYPES.has(rfc9457ContentType),
|
|
552
|
+
"rfc9457ContentType must be one of: application/problem+json, application/json"
|
|
553
|
+
);
|
|
554
|
+
return Object.freeze({ errorFormat, errorDomain, rfc9457ContentType });
|
|
555
|
+
};
|
|
556
|
+
function createHandler(options = {}) {
|
|
557
|
+
const config = validateHandlerConfig(options);
|
|
373
558
|
let errorMessageProvider = defaultErrorMessageProvider;
|
|
374
559
|
let preJson = null;
|
|
375
560
|
let postJson = null;
|
|
@@ -389,153 +574,202 @@ function createHandler(options = {}) {
|
|
|
389
574
|
setState(fn, promisify(fn));
|
|
390
575
|
rebuild();
|
|
391
576
|
};
|
|
392
|
-
const
|
|
577
|
+
const noopRebuild = () => {
|
|
578
|
+
};
|
|
579
|
+
const sendBaseJson = function(res, data, event, onBeforeOutputError) {
|
|
393
580
|
if (shouldSkipResponse(res, event)) {
|
|
394
|
-
return;
|
|
581
|
+
return false;
|
|
395
582
|
}
|
|
396
583
|
if (data === void 0) {
|
|
397
|
-
return;
|
|
584
|
+
return false;
|
|
398
585
|
}
|
|
399
586
|
if (isResponse(data)) {
|
|
400
|
-
res.status(data.statusCode).json(data.data);
|
|
401
|
-
return;
|
|
587
|
+
res.status(validateHttpStatusCode(data.statusCode)).json(data.data);
|
|
588
|
+
return true;
|
|
402
589
|
}
|
|
403
590
|
if (isCSVResponse(data)) {
|
|
404
|
-
data.streamCsv(res);
|
|
405
|
-
return;
|
|
591
|
+
data.streamCsv(res, onBeforeOutputError);
|
|
592
|
+
return true;
|
|
406
593
|
}
|
|
407
594
|
res.json(data);
|
|
595
|
+
return true;
|
|
408
596
|
};
|
|
409
597
|
const sendBaseError = function(res, err, event) {
|
|
410
598
|
if (shouldSkipResponse(res, event)) {
|
|
411
|
-
return;
|
|
599
|
+
return false;
|
|
412
600
|
}
|
|
413
|
-
const error = err;
|
|
414
|
-
if (error.statusCode) {
|
|
415
|
-
|
|
601
|
+
const error = normalizeThrownError(err);
|
|
602
|
+
if (error.statusCode !== void 0) {
|
|
603
|
+
const statusCode = validateErrorStatusCode(error.statusCode, "error.statusCode");
|
|
604
|
+
if (config.errorFormat === ErrorFormats.rfc9457) {
|
|
416
605
|
sendProblemJson(
|
|
417
606
|
res,
|
|
418
|
-
|
|
419
|
-
toRfc9457HttpErrorPayload(error, errorDomain),
|
|
420
|
-
rfc9457ContentType
|
|
607
|
+
statusCode,
|
|
608
|
+
toRfc9457HttpErrorPayload(error, config.errorDomain),
|
|
609
|
+
config.rfc9457ContentType
|
|
421
610
|
);
|
|
422
|
-
return;
|
|
611
|
+
return true;
|
|
423
612
|
}
|
|
424
|
-
sendHttpErrorByFormat[errorFormat](res, error, errorDomain);
|
|
425
|
-
return;
|
|
613
|
+
sendHttpErrorByFormat[config.errorFormat](res, error, config.errorDomain);
|
|
614
|
+
return true;
|
|
426
615
|
}
|
|
427
616
|
const result = errorMessageProvider(err);
|
|
428
|
-
if (errorFormat === ErrorFormats.rfc9457) {
|
|
617
|
+
if (config.errorFormat === ErrorFormats.rfc9457) {
|
|
429
618
|
const payload = toRfc9457GenericErrorPayload(result);
|
|
430
|
-
|
|
431
|
-
|
|
619
|
+
const statusCode = payload.status ?? FALLBACK_ERROR_STATUS;
|
|
620
|
+
sendProblemJson(res, validateErrorStatusCode(statusCode, "problem.status"), payload, config.rfc9457ContentType);
|
|
621
|
+
return true;
|
|
432
622
|
}
|
|
433
|
-
sendGenericErrorByFormat[errorFormat](res, result, errorDomain);
|
|
623
|
+
sendGenericErrorByFormat[config.errorFormat](res, result, config.errorDomain);
|
|
624
|
+
return true;
|
|
625
|
+
};
|
|
626
|
+
const invokePostHook = (hook, value, onFailure) => {
|
|
627
|
+
hook(value).then(
|
|
628
|
+
() => void 0,
|
|
629
|
+
(err) => onFailure(err)
|
|
630
|
+
);
|
|
434
631
|
};
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
if (!preJsonHook && !postJsonHook) {
|
|
439
|
-
sendJson = sendBaseJson;
|
|
632
|
+
const invokePostHookOnFinish = (res, hook, value, onFailure) => {
|
|
633
|
+
if (isFunction(res.once)) {
|
|
634
|
+
res.once("finish", () => invokePostHook(hook, value, onFailure));
|
|
440
635
|
return;
|
|
441
636
|
}
|
|
442
|
-
|
|
443
|
-
const finalize = () => {
|
|
444
|
-
try {
|
|
445
|
-
sendBaseJson(res, data, event);
|
|
446
|
-
} catch (err) {
|
|
447
|
-
sendError(res, err, event);
|
|
448
|
-
return;
|
|
449
|
-
}
|
|
450
|
-
if (postJsonHook) {
|
|
451
|
-
invokePostHook(postJsonHook, data);
|
|
452
|
-
}
|
|
453
|
-
};
|
|
454
|
-
if (preJsonHook) {
|
|
455
|
-
preJsonHook(data).then(finalize, (err) => sendError(res, err, event));
|
|
456
|
-
return;
|
|
457
|
-
}
|
|
458
|
-
finalize();
|
|
459
|
-
};
|
|
637
|
+
invokePostHook(hook, value, onFailure);
|
|
460
638
|
};
|
|
461
|
-
const
|
|
462
|
-
if (
|
|
463
|
-
sendError = sendBaseError;
|
|
639
|
+
const terminalErrorBoundary = (res, next, terminalError, event) => {
|
|
640
|
+
if (event.canceled) {
|
|
464
641
|
return;
|
|
465
642
|
}
|
|
466
|
-
|
|
467
|
-
const finalize = () => {
|
|
468
|
-
sendBaseError(res, err, event);
|
|
469
|
-
if (postErrorHook) {
|
|
470
|
-
invokePostHook(postErrorHook, err);
|
|
471
|
-
}
|
|
472
|
-
};
|
|
473
|
-
if (preErrorHook) {
|
|
474
|
-
preErrorHook(err).then(finalize, (hookErr) => sendBaseError(res, hookErr, event));
|
|
475
|
-
return;
|
|
476
|
-
}
|
|
477
|
-
finalize();
|
|
478
|
-
};
|
|
643
|
+
next(terminalError);
|
|
479
644
|
};
|
|
480
|
-
const
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
645
|
+
const dispatchError = (res, next, err, event) => {
|
|
646
|
+
const reportFailure = (failure) => {
|
|
647
|
+
terminalErrorBoundary(res, next, failure, event);
|
|
648
|
+
};
|
|
649
|
+
const sendFormatted = (failure) => {
|
|
650
|
+
let didSend;
|
|
651
|
+
try {
|
|
652
|
+
didSend = sendBaseError(res, failure, event);
|
|
653
|
+
} catch (senderFailure) {
|
|
654
|
+
reportFailure(senderFailure === void 0 ? failure : senderFailure);
|
|
484
655
|
return;
|
|
485
656
|
}
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
if (
|
|
657
|
+
if (didSend && postErrorHook) {
|
|
658
|
+
invokePostHookOnFinish(res, postErrorHook, failure, reportFailure);
|
|
659
|
+
}
|
|
660
|
+
};
|
|
661
|
+
if (event.canceled) {
|
|
491
662
|
return;
|
|
492
663
|
}
|
|
493
|
-
|
|
494
|
-
|
|
664
|
+
const runSender = () => sendFormatted(err);
|
|
665
|
+
if (preErrorHook) {
|
|
666
|
+
preErrorHook(err).then(
|
|
667
|
+
() => sendFormatted(err),
|
|
668
|
+
(hookErr) => sendFormatted(hookErr === void 0 ? err : hookErr)
|
|
669
|
+
);
|
|
495
670
|
return;
|
|
496
671
|
}
|
|
497
|
-
|
|
498
|
-
|
|
672
|
+
runSender();
|
|
673
|
+
};
|
|
674
|
+
const dispatchValue = (res, next, data, event, onSerialized) => {
|
|
675
|
+
const reportFailure = (failure) => {
|
|
676
|
+
terminalErrorBoundary(res, next, failure, event);
|
|
677
|
+
};
|
|
678
|
+
const sendFormattedError = (failure) => {
|
|
679
|
+
try {
|
|
680
|
+
sendBaseError(res, failure, event);
|
|
681
|
+
} catch (senderFailure) {
|
|
682
|
+
reportFailure(senderFailure === void 0 ? failure : senderFailure);
|
|
683
|
+
}
|
|
684
|
+
};
|
|
685
|
+
const runSender = () => {
|
|
686
|
+
let didSend;
|
|
687
|
+
try {
|
|
688
|
+
didSend = sendBaseJson(res, data, event, sendFormattedError);
|
|
689
|
+
} catch (senderFailure) {
|
|
690
|
+
const failure = senderFailure === void 0 ? new Error("response serialization failed") : senderFailure;
|
|
691
|
+
if (res.headersSent) {
|
|
692
|
+
reportFailure(failure);
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
sendFormattedError(failure);
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
if (didSend && postJsonHook) {
|
|
699
|
+
invokePostHookOnFinish(res, postJsonHook, data, reportFailure);
|
|
700
|
+
}
|
|
701
|
+
onSerialized();
|
|
702
|
+
};
|
|
703
|
+
if (preJsonHook) {
|
|
704
|
+
preJsonHook(data).then(
|
|
705
|
+
runSender,
|
|
706
|
+
(hookErr) => sendFormattedError(hookErr === void 0 ? new Error("pre-json hook failed") : hookErr)
|
|
707
|
+
);
|
|
499
708
|
return;
|
|
500
709
|
}
|
|
501
|
-
|
|
710
|
+
runSender();
|
|
502
711
|
};
|
|
503
712
|
const nextFn = function(event, next) {
|
|
504
|
-
return function(
|
|
505
|
-
if (
|
|
713
|
+
return function(error) {
|
|
714
|
+
if (event.canceled) {
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
if (error === void 0) {
|
|
506
718
|
event.canceled = true;
|
|
507
719
|
next();
|
|
508
720
|
return;
|
|
509
721
|
}
|
|
510
|
-
|
|
511
|
-
|
|
722
|
+
event.canceled = true;
|
|
723
|
+
next(error);
|
|
724
|
+
};
|
|
725
|
+
};
|
|
726
|
+
const runLifecycle = (res, next, result, event) => {
|
|
727
|
+
const finalize = (resolved) => {
|
|
728
|
+
if (event.canceled) {
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
if (event.nextError) {
|
|
732
|
+
dispatchError(res, next, event.nextError, event);
|
|
512
733
|
return;
|
|
513
734
|
}
|
|
514
|
-
|
|
735
|
+
dispatchValue(res, next, resolved, event, () => void 0);
|
|
515
736
|
};
|
|
737
|
+
if (isPromise(result)) {
|
|
738
|
+
Promise.resolve(result).then(
|
|
739
|
+
(resolved) => finalize(resolved),
|
|
740
|
+
(err) => {
|
|
741
|
+
if (res.headersSent) {
|
|
742
|
+
next(err);
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
dispatchError(res, next, err, event);
|
|
746
|
+
}
|
|
747
|
+
);
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
finalize(result);
|
|
516
751
|
};
|
|
517
752
|
const routerFn = function(fn) {
|
|
518
753
|
return function(req, res, next) {
|
|
519
754
|
const event = { canceled: false, nextError: null };
|
|
520
755
|
try {
|
|
521
756
|
const result = fn(req, res, nextFn(event, next));
|
|
522
|
-
|
|
757
|
+
runLifecycle(res, next, result, event);
|
|
523
758
|
} catch (err) {
|
|
524
759
|
if (res.headersSent) {
|
|
760
|
+
next(err);
|
|
525
761
|
return;
|
|
526
762
|
}
|
|
527
|
-
|
|
763
|
+
dispatchError(res, next, err, event);
|
|
528
764
|
}
|
|
529
765
|
};
|
|
530
766
|
};
|
|
531
|
-
const
|
|
767
|
+
const handleResponse2 = function(...fns) {
|
|
532
768
|
const middlewares = normalizeMiddlewareList(fns);
|
|
533
769
|
return middlewares.length === 1 ? routerFn(middlewares[0]) : middlewares.map(routerFn);
|
|
534
770
|
};
|
|
535
|
-
|
|
536
|
-
handleResponse,
|
|
537
|
-
handleResult,
|
|
538
|
-
handlePromise,
|
|
771
|
+
const handler = {
|
|
772
|
+
handleResponse: handleResponse2,
|
|
539
773
|
HttpResponse,
|
|
540
774
|
createHandler,
|
|
541
775
|
get errorMessageProvider() {
|
|
@@ -556,7 +790,7 @@ function createHandler(options = {}) {
|
|
|
556
790
|
preJson = syncHook;
|
|
557
791
|
preJsonHook = asyncHook;
|
|
558
792
|
},
|
|
559
|
-
|
|
793
|
+
noopRebuild
|
|
560
794
|
);
|
|
561
795
|
},
|
|
562
796
|
get postJson() {
|
|
@@ -570,7 +804,7 @@ function createHandler(options = {}) {
|
|
|
570
804
|
postJson = syncHook;
|
|
571
805
|
postJsonHook = asyncHook;
|
|
572
806
|
},
|
|
573
|
-
|
|
807
|
+
noopRebuild
|
|
574
808
|
);
|
|
575
809
|
},
|
|
576
810
|
get preError() {
|
|
@@ -584,7 +818,7 @@ function createHandler(options = {}) {
|
|
|
584
818
|
preError = syncHook;
|
|
585
819
|
preErrorHook = asyncHook;
|
|
586
820
|
},
|
|
587
|
-
|
|
821
|
+
noopRebuild
|
|
588
822
|
);
|
|
589
823
|
},
|
|
590
824
|
get postError() {
|
|
@@ -598,14 +832,16 @@ function createHandler(options = {}) {
|
|
|
598
832
|
postError = syncHook;
|
|
599
833
|
postErrorHook = asyncHook;
|
|
600
834
|
},
|
|
601
|
-
|
|
835
|
+
noopRebuild
|
|
602
836
|
);
|
|
603
837
|
}
|
|
604
838
|
};
|
|
839
|
+
return handler;
|
|
605
840
|
}
|
|
606
841
|
|
|
607
842
|
// src/index.ts
|
|
608
843
|
var apiHandler = createHandler();
|
|
844
|
+
var { handleResponse } = apiHandler;
|
|
609
845
|
var index_default = apiHandler;
|
|
610
846
|
export {
|
|
611
847
|
Accepted,
|
|
@@ -623,5 +859,6 @@ export {
|
|
|
623
859
|
ResetContent,
|
|
624
860
|
Response,
|
|
625
861
|
createHandler,
|
|
626
|
-
index_default as default
|
|
862
|
+
index_default as default,
|
|
863
|
+
handleResponse
|
|
627
864
|
};
|