@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.
@@ -1,11 +1,18 @@
1
1
  import { Rfc9457ErrorPayload, Aip193ErrorPayload } from '@web-ts-toolkit/http-errors';
2
- import { ErrorMessageProvider, ErrorMessageResult, ErrorWithPayload } from './public-types.mjs';
2
+ import { ErrorMessageProvider, ErrorWithPayload, ErrorMessageResult } from './public-types.mjs';
3
+ import 'express-serve-static-core';
3
4
  import './http-response.mjs';
5
+ import './responses/csv.mjs';
4
6
  import './responses/success.mjs';
5
7
  import './responses/index.mjs';
6
- import './responses/csv.mjs';
7
8
  import './error-formats.mjs';
8
9
 
10
+ declare const FALLBACK_ERROR_STATUS = 500;
11
+ declare const FALLBACK_ERROR_MESSAGE = "Internal Server Error";
12
+ declare const isValidErrorStatusCode: (statusCode: unknown) => statusCode is number;
13
+ declare const validateErrorStatusCode: (statusCode: unknown, source: string) => number;
14
+ declare const toErrorStatusCode: (status: unknown, code: unknown, fallbackStatusCode?: number) => number;
15
+ declare const normalizeThrownError: (value: unknown) => ErrorWithPayload;
9
16
  declare const defaultErrorMessageProvider: ErrorMessageProvider;
10
17
  declare const toSimpleErrorPayload: (result: ErrorMessageResult) => Record<string, unknown>;
11
18
  declare const toStructuredHttpErrorPayload: (error: ErrorWithPayload, errorDomain: string) => Aip193ErrorPayload;
@@ -13,4 +20,4 @@ declare const toRfc9457HttpErrorPayload: (error: ErrorWithPayload, errorDomain:
13
20
  declare const toStructuredGenericErrorPayload: (result: ErrorMessageResult, errorDomain: string) => Aip193ErrorPayload;
14
21
  declare const toRfc9457GenericErrorPayload: (result: ErrorMessageResult) => Rfc9457ErrorPayload;
15
22
 
16
- export { defaultErrorMessageProvider, toRfc9457GenericErrorPayload, toRfc9457HttpErrorPayload, toSimpleErrorPayload, toStructuredGenericErrorPayload, toStructuredHttpErrorPayload };
23
+ export { FALLBACK_ERROR_MESSAGE, FALLBACK_ERROR_STATUS, defaultErrorMessageProvider, isValidErrorStatusCode, normalizeThrownError, toErrorStatusCode, toRfc9457GenericErrorPayload, toRfc9457HttpErrorPayload, toSimpleErrorPayload, toStructuredGenericErrorPayload, toStructuredHttpErrorPayload, validateErrorStatusCode };
package/error-format.d.ts CHANGED
@@ -1,11 +1,18 @@
1
1
  import { Rfc9457ErrorPayload, Aip193ErrorPayload } from '@web-ts-toolkit/http-errors';
2
- import { ErrorMessageProvider, ErrorMessageResult, ErrorWithPayload } from './public-types.js';
2
+ import { ErrorMessageProvider, ErrorWithPayload, ErrorMessageResult } from './public-types.js';
3
+ import 'express-serve-static-core';
3
4
  import './http-response.js';
5
+ import './responses/csv.js';
4
6
  import './responses/success.js';
5
7
  import './responses/index.js';
6
- import './responses/csv.js';
7
8
  import './error-formats.js';
8
9
 
10
+ declare const FALLBACK_ERROR_STATUS = 500;
11
+ declare const FALLBACK_ERROR_MESSAGE = "Internal Server Error";
12
+ declare const isValidErrorStatusCode: (statusCode: unknown) => statusCode is number;
13
+ declare const validateErrorStatusCode: (statusCode: unknown, source: string) => number;
14
+ declare const toErrorStatusCode: (status: unknown, code: unknown, fallbackStatusCode?: number) => number;
15
+ declare const normalizeThrownError: (value: unknown) => ErrorWithPayload;
9
16
  declare const defaultErrorMessageProvider: ErrorMessageProvider;
10
17
  declare const toSimpleErrorPayload: (result: ErrorMessageResult) => Record<string, unknown>;
11
18
  declare const toStructuredHttpErrorPayload: (error: ErrorWithPayload, errorDomain: string) => Aip193ErrorPayload;
@@ -13,4 +20,4 @@ declare const toRfc9457HttpErrorPayload: (error: ErrorWithPayload, errorDomain:
13
20
  declare const toStructuredGenericErrorPayload: (result: ErrorMessageResult, errorDomain: string) => Aip193ErrorPayload;
14
21
  declare const toRfc9457GenericErrorPayload: (result: ErrorMessageResult) => Rfc9457ErrorPayload;
15
22
 
16
- export { defaultErrorMessageProvider, toRfc9457GenericErrorPayload, toRfc9457HttpErrorPayload, toSimpleErrorPayload, toStructuredGenericErrorPayload, toStructuredHttpErrorPayload };
23
+ export { FALLBACK_ERROR_MESSAGE, FALLBACK_ERROR_STATUS, defaultErrorMessageProvider, isValidErrorStatusCode, normalizeThrownError, toErrorStatusCode, toRfc9457GenericErrorPayload, toRfc9457HttpErrorPayload, toSimpleErrorPayload, toStructuredGenericErrorPayload, toStructuredHttpErrorPayload, validateErrorStatusCode };
package/error-format.js CHANGED
@@ -20,21 +20,34 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/error-format.ts
21
21
  var error_format_exports = {};
22
22
  __export(error_format_exports, {
23
+ FALLBACK_ERROR_MESSAGE: () => FALLBACK_ERROR_MESSAGE,
24
+ FALLBACK_ERROR_STATUS: () => FALLBACK_ERROR_STATUS,
23
25
  defaultErrorMessageProvider: () => defaultErrorMessageProvider,
26
+ isValidErrorStatusCode: () => isValidErrorStatusCode,
27
+ normalizeThrownError: () => normalizeThrownError,
28
+ toErrorStatusCode: () => toErrorStatusCode,
24
29
  toRfc9457GenericErrorPayload: () => toRfc9457GenericErrorPayload,
25
30
  toRfc9457HttpErrorPayload: () => toRfc9457HttpErrorPayload,
26
31
  toSimpleErrorPayload: () => toSimpleErrorPayload,
27
32
  toStructuredGenericErrorPayload: () => toStructuredGenericErrorPayload,
28
- toStructuredHttpErrorPayload: () => toStructuredHttpErrorPayload
33
+ toStructuredHttpErrorPayload: () => toStructuredHttpErrorPayload,
34
+ validateErrorStatusCode: () => validateErrorStatusCode
29
35
  });
30
36
  module.exports = __toCommonJS(error_format_exports);
31
37
  var import_http_errors = require("@web-ts-toolkit/http-errors");
32
38
  var import_utils = require("@web-ts-toolkit/utils");
33
- var toStatusCode = (status, code, fallbackStatusCode) => {
34
- if (typeof status === "number") {
35
- return status;
39
+ var FALLBACK_ERROR_STATUS = 500;
40
+ var FALLBACK_ERROR_MESSAGE = "Internal Server Error";
41
+ var isValidErrorStatusCode = import_http_errors.isHttpErrorStatusCode;
42
+ var validateErrorStatusCode = (statusCode, source) => {
43
+ return (0, import_http_errors.validateHttpErrorStatusCode)(statusCode, source);
44
+ };
45
+ var toErrorStatusCode = (status, code, fallbackStatusCode = FALLBACK_ERROR_STATUS) => {
46
+ const statusCode = typeof status === "number" ? status : code;
47
+ if (statusCode === void 0) {
48
+ return fallbackStatusCode;
36
49
  }
37
- return typeof code === "number" ? code : fallbackStatusCode;
50
+ return validateErrorStatusCode(statusCode, "error status");
38
51
  };
39
52
  var toOptionalString = (value) => typeof value === "string" ? value : void 0;
40
53
  var toMetadata = (value) => {
@@ -46,6 +59,41 @@ var toArray = (value) => {
46
59
  }
47
60
  return (0, import_utils.isArray)(value) ? value : [value];
48
61
  };
62
+ var describeThrown = (value) => {
63
+ try {
64
+ if (value === null) {
65
+ return "null";
66
+ }
67
+ if (value === void 0) {
68
+ return "undefined";
69
+ }
70
+ if (typeof value === "symbol") {
71
+ return value.toString();
72
+ }
73
+ const stringified = typeof value === "object" && value !== null ? value.message : void 0;
74
+ if (typeof stringified === "string" && stringified.length > 0) {
75
+ return stringified;
76
+ }
77
+ return String(value);
78
+ } catch {
79
+ return FALLBACK_ERROR_MESSAGE;
80
+ }
81
+ };
82
+ var normalizeThrownError = (value) => {
83
+ if (value instanceof Error) {
84
+ return value;
85
+ }
86
+ const message = describeThrown(value);
87
+ if ((0, import_utils.isPlainObject)(value)) {
88
+ const record = value;
89
+ const normalized = {
90
+ ...record,
91
+ message
92
+ };
93
+ return normalized;
94
+ }
95
+ return { message };
96
+ };
49
97
  var toHttpErrorShape = (error, fallbackDomain) => ({
50
98
  statusCode: error.statusCode ?? 500,
51
99
  status: error.status,
@@ -66,8 +114,8 @@ var toProblemDetailsSource = (result) => {
66
114
  return (0, import_utils.isPlainObject)(result) ? result : void 0;
67
115
  };
68
116
  var defaultErrorMessageProvider = function(error) {
69
- const errorLike = error;
70
- return errorLike.message ?? errorLike._message ?? String(error);
117
+ void error;
118
+ return FALLBACK_ERROR_MESSAGE;
71
119
  };
72
120
  var toSimpleErrorPayload = (result) => (0, import_utils.isString)(result) ? { message: result } : { ...result };
73
121
  var toStructuredHttpErrorPayload = (error, errorDomain) => (0, import_http_errors.toAip193ErrorPayload)(toHttpErrorShape(error, errorDomain), errorDomain);
@@ -75,7 +123,7 @@ var toRfc9457HttpErrorPayload = (error, errorDomain) => (0, import_http_errors.t
75
123
  var toStructuredGenericErrorPayload = (result, errorDomain) => {
76
124
  if ((0, import_utils.isPlainObject)(result) && (0, import_utils.isPlainObject)(result.error)) {
77
125
  const error = result.error;
78
- const statusCode = typeof error.code === "number" ? error.code : 422;
126
+ const statusCode = toErrorStatusCode(error.code, void 0);
79
127
  const status = typeof error.status === "string" ? error.status : (0, import_http_errors.getCanonicalStatus)(statusCode);
80
128
  const message = typeof error.message === "string" ? error.message : "";
81
129
  const details = toArray(error.details);
@@ -89,7 +137,7 @@ var toStructuredGenericErrorPayload = (result, errorDomain) => {
89
137
  };
90
138
  }
91
139
  if ((0, import_utils.isPlainObject)(result)) {
92
- const statusCode = typeof result.code === "number" ? result.code : 422;
140
+ const statusCode = toErrorStatusCode(result.code, void 0);
93
141
  const status = typeof result.status === "string" ? result.status : (0, import_http_errors.getCanonicalStatus)(statusCode);
94
142
  const message = typeof result.message === "string" ? result.message : "";
95
143
  const details = toArray(result.details) || [(0, import_http_errors.createAip193ErrorInfoDetail)(status, errorDomain)];
@@ -104,17 +152,17 @@ var toStructuredGenericErrorPayload = (result, errorDomain) => {
104
152
  }
105
153
  return {
106
154
  error: {
107
- code: 422,
108
- status: (0, import_http_errors.getCanonicalStatus)(422),
155
+ code: FALLBACK_ERROR_STATUS,
156
+ status: (0, import_http_errors.getCanonicalStatus)(FALLBACK_ERROR_STATUS),
109
157
  message: String(result),
110
- details: [(0, import_http_errors.createAip193ErrorInfoDetail)((0, import_http_errors.getCanonicalStatus)(422), errorDomain)]
158
+ details: [(0, import_http_errors.createAip193ErrorInfoDetail)((0, import_http_errors.getCanonicalStatus)(FALLBACK_ERROR_STATUS), errorDomain)]
111
159
  }
112
160
  };
113
161
  };
114
162
  var toRfc9457GenericErrorPayload = (result) => {
115
163
  const problem = toProblemDetailsSource(result);
116
164
  if (problem) {
117
- const statusCode = toStatusCode(problem.status, problem.code, 422);
165
+ const statusCode = toErrorStatusCode(problem.status, problem.code);
118
166
  return (0, import_http_errors.toRfc9457ErrorPayload)({
119
167
  statusCode,
120
168
  message: toOptionalString(problem.detail) ?? toOptionalString(problem.message) ?? "",
@@ -125,16 +173,22 @@ var toRfc9457GenericErrorPayload = (result) => {
125
173
  });
126
174
  }
127
175
  return (0, import_http_errors.toRfc9457ErrorPayload)({
128
- statusCode: 422,
176
+ statusCode: FALLBACK_ERROR_STATUS,
129
177
  message: String(result)
130
178
  });
131
179
  };
132
180
  // Annotate the CommonJS export names for ESM import in node:
133
181
  0 && (module.exports = {
182
+ FALLBACK_ERROR_MESSAGE,
183
+ FALLBACK_ERROR_STATUS,
134
184
  defaultErrorMessageProvider,
185
+ isValidErrorStatusCode,
186
+ normalizeThrownError,
187
+ toErrorStatusCode,
135
188
  toRfc9457GenericErrorPayload,
136
189
  toRfc9457HttpErrorPayload,
137
190
  toSimpleErrorPayload,
138
191
  toStructuredGenericErrorPayload,
139
- toStructuredHttpErrorPayload
192
+ toStructuredHttpErrorPayload,
193
+ validateErrorStatusCode
140
194
  });
package/error-format.mjs CHANGED
@@ -2,15 +2,24 @@
2
2
  import {
3
3
  createAip193ErrorInfoDetail,
4
4
  getCanonicalStatus,
5
+ isHttpErrorStatusCode,
5
6
  toAip193ErrorPayload,
6
- toRfc9457ErrorPayload
7
+ toRfc9457ErrorPayload,
8
+ validateHttpErrorStatusCode
7
9
  } from "@web-ts-toolkit/http-errors";
8
10
  import { isArray, isPlainObject, isString, toStringRecord } from "@web-ts-toolkit/utils";
9
- var toStatusCode = (status, code, fallbackStatusCode) => {
10
- if (typeof status === "number") {
11
- return status;
11
+ var FALLBACK_ERROR_STATUS = 500;
12
+ var FALLBACK_ERROR_MESSAGE = "Internal Server Error";
13
+ var isValidErrorStatusCode = isHttpErrorStatusCode;
14
+ var validateErrorStatusCode = (statusCode, source) => {
15
+ return validateHttpErrorStatusCode(statusCode, source);
16
+ };
17
+ var toErrorStatusCode = (status, code, fallbackStatusCode = FALLBACK_ERROR_STATUS) => {
18
+ const statusCode = typeof status === "number" ? status : code;
19
+ if (statusCode === void 0) {
20
+ return fallbackStatusCode;
12
21
  }
13
- return typeof code === "number" ? code : fallbackStatusCode;
22
+ return validateErrorStatusCode(statusCode, "error status");
14
23
  };
15
24
  var toOptionalString = (value) => typeof value === "string" ? value : void 0;
16
25
  var toMetadata = (value) => {
@@ -22,6 +31,41 @@ var toArray = (value) => {
22
31
  }
23
32
  return isArray(value) ? value : [value];
24
33
  };
34
+ var describeThrown = (value) => {
35
+ try {
36
+ if (value === null) {
37
+ return "null";
38
+ }
39
+ if (value === void 0) {
40
+ return "undefined";
41
+ }
42
+ if (typeof value === "symbol") {
43
+ return value.toString();
44
+ }
45
+ const stringified = typeof value === "object" && value !== null ? value.message : void 0;
46
+ if (typeof stringified === "string" && stringified.length > 0) {
47
+ return stringified;
48
+ }
49
+ return String(value);
50
+ } catch {
51
+ return FALLBACK_ERROR_MESSAGE;
52
+ }
53
+ };
54
+ var normalizeThrownError = (value) => {
55
+ if (value instanceof Error) {
56
+ return value;
57
+ }
58
+ const message = describeThrown(value);
59
+ if (isPlainObject(value)) {
60
+ const record = value;
61
+ const normalized = {
62
+ ...record,
63
+ message
64
+ };
65
+ return normalized;
66
+ }
67
+ return { message };
68
+ };
25
69
  var toHttpErrorShape = (error, fallbackDomain) => ({
26
70
  statusCode: error.statusCode ?? 500,
27
71
  status: error.status,
@@ -42,8 +86,8 @@ var toProblemDetailsSource = (result) => {
42
86
  return isPlainObject(result) ? result : void 0;
43
87
  };
44
88
  var defaultErrorMessageProvider = function(error) {
45
- const errorLike = error;
46
- return errorLike.message ?? errorLike._message ?? String(error);
89
+ void error;
90
+ return FALLBACK_ERROR_MESSAGE;
47
91
  };
48
92
  var toSimpleErrorPayload = (result) => isString(result) ? { message: result } : { ...result };
49
93
  var toStructuredHttpErrorPayload = (error, errorDomain) => toAip193ErrorPayload(toHttpErrorShape(error, errorDomain), errorDomain);
@@ -51,7 +95,7 @@ var toRfc9457HttpErrorPayload = (error, errorDomain) => toRfc9457ErrorPayload(to
51
95
  var toStructuredGenericErrorPayload = (result, errorDomain) => {
52
96
  if (isPlainObject(result) && isPlainObject(result.error)) {
53
97
  const error = result.error;
54
- const statusCode = typeof error.code === "number" ? error.code : 422;
98
+ const statusCode = toErrorStatusCode(error.code, void 0);
55
99
  const status = typeof error.status === "string" ? error.status : getCanonicalStatus(statusCode);
56
100
  const message = typeof error.message === "string" ? error.message : "";
57
101
  const details = toArray(error.details);
@@ -65,7 +109,7 @@ var toStructuredGenericErrorPayload = (result, errorDomain) => {
65
109
  };
66
110
  }
67
111
  if (isPlainObject(result)) {
68
- const statusCode = typeof result.code === "number" ? result.code : 422;
112
+ const statusCode = toErrorStatusCode(result.code, void 0);
69
113
  const status = typeof result.status === "string" ? result.status : getCanonicalStatus(statusCode);
70
114
  const message = typeof result.message === "string" ? result.message : "";
71
115
  const details = toArray(result.details) || [createAip193ErrorInfoDetail(status, errorDomain)];
@@ -80,17 +124,17 @@ var toStructuredGenericErrorPayload = (result, errorDomain) => {
80
124
  }
81
125
  return {
82
126
  error: {
83
- code: 422,
84
- status: getCanonicalStatus(422),
127
+ code: FALLBACK_ERROR_STATUS,
128
+ status: getCanonicalStatus(FALLBACK_ERROR_STATUS),
85
129
  message: String(result),
86
- details: [createAip193ErrorInfoDetail(getCanonicalStatus(422), errorDomain)]
130
+ details: [createAip193ErrorInfoDetail(getCanonicalStatus(FALLBACK_ERROR_STATUS), errorDomain)]
87
131
  }
88
132
  };
89
133
  };
90
134
  var toRfc9457GenericErrorPayload = (result) => {
91
135
  const problem = toProblemDetailsSource(result);
92
136
  if (problem) {
93
- const statusCode = toStatusCode(problem.status, problem.code, 422);
137
+ const statusCode = toErrorStatusCode(problem.status, problem.code);
94
138
  return toRfc9457ErrorPayload({
95
139
  statusCode,
96
140
  message: toOptionalString(problem.detail) ?? toOptionalString(problem.message) ?? "",
@@ -101,15 +145,21 @@ var toRfc9457GenericErrorPayload = (result) => {
101
145
  });
102
146
  }
103
147
  return toRfc9457ErrorPayload({
104
- statusCode: 422,
148
+ statusCode: FALLBACK_ERROR_STATUS,
105
149
  message: String(result)
106
150
  });
107
151
  };
108
152
  export {
153
+ FALLBACK_ERROR_MESSAGE,
154
+ FALLBACK_ERROR_STATUS,
109
155
  defaultErrorMessageProvider,
156
+ isValidErrorStatusCode,
157
+ normalizeThrownError,
158
+ toErrorStatusCode,
110
159
  toRfc9457GenericErrorPayload,
111
160
  toRfc9457HttpErrorPayload,
112
161
  toSimpleErrorPayload,
113
162
  toStructuredGenericErrorPayload,
114
- toStructuredHttpErrorPayload
163
+ toStructuredHttpErrorPayload,
164
+ validateErrorStatusCode
115
165
  };
@@ -1,7 +1,7 @@
1
+ import { CsvResponseOptions, CSVResponse } from './responses/csv.mjs';
1
2
  import * as _web_ts_toolkit_http_errors from '@web-ts-toolkit/http-errors';
2
3
  import { BadRequestError, UnauthorizedError, ForbiddenError, NotFoundError, MethodNotAllowedError, NotAcceptableError, ProxyAuthRequiredError, RequestTimeoutError, ConflictError, GoneError, LengthRequiredError, PreconditionFailedError, PayloadTooLargeError, UriTooLongError, UnsupportedMediaTypeError, RequestedRangeNotSatisfiableError, ExpectationFailedError, TeapotError, MisdirectedRequestError, UnprocessableEntityError, LockedError, FailedDependencyError, UpgradeRequiredError, PreconditionRequiredError, TooManyRequestsError, RequestHeaderFieldsTooLargeError, UnavailableForLegalReasonsError } from '@web-ts-toolkit/http-errors';
3
4
  import { OK, Created, Accepted, NonAuthoritativeInfo, NoContent, ResetContent, PartialContent, MultiStatus, AlreadyReported, IMUsed } from './responses/success.mjs';
4
- import { CSVResponse } from './responses/csv.mjs';
5
5
  import './responses/index.mjs';
6
6
 
7
7
  declare const HttpResponse: {
@@ -43,11 +43,7 @@ declare const HttpResponse: {
43
43
  requestHeaderFieldsTooLarge: (message?: string | undefined, options?: _web_ts_toolkit_http_errors.HttpErrorOptions | undefined) => RequestHeaderFieldsTooLargeError;
44
44
  unavailableForLegalReasons: (message?: string | undefined, options?: _web_ts_toolkit_http_errors.HttpErrorOptions | undefined) => UnavailableForLegalReasonsError;
45
45
  json: (data: unknown) => OK<unknown>;
46
- csv: (dataset?: unknown, options?: {
47
- filename?: string;
48
- headers?: boolean;
49
- processor?: (value: unknown) => unknown;
50
- } | undefined) => CSVResponse;
46
+ csv: (dataset?: unknown, options?: CsvResponseOptions | undefined) => CSVResponse;
51
47
  };
52
48
  type HttpResponseHelpers = typeof HttpResponse;
53
49
 
@@ -1,7 +1,7 @@
1
+ import { CsvResponseOptions, CSVResponse } from './responses/csv.js';
1
2
  import * as _web_ts_toolkit_http_errors from '@web-ts-toolkit/http-errors';
2
3
  import { BadRequestError, UnauthorizedError, ForbiddenError, NotFoundError, MethodNotAllowedError, NotAcceptableError, ProxyAuthRequiredError, RequestTimeoutError, ConflictError, GoneError, LengthRequiredError, PreconditionFailedError, PayloadTooLargeError, UriTooLongError, UnsupportedMediaTypeError, RequestedRangeNotSatisfiableError, ExpectationFailedError, TeapotError, MisdirectedRequestError, UnprocessableEntityError, LockedError, FailedDependencyError, UpgradeRequiredError, PreconditionRequiredError, TooManyRequestsError, RequestHeaderFieldsTooLargeError, UnavailableForLegalReasonsError } from '@web-ts-toolkit/http-errors';
3
4
  import { OK, Created, Accepted, NonAuthoritativeInfo, NoContent, ResetContent, PartialContent, MultiStatus, AlreadyReported, IMUsed } from './responses/success.js';
4
- import { CSVResponse } from './responses/csv.js';
5
5
  import './responses/index.js';
6
6
 
7
7
  declare const HttpResponse: {
@@ -43,11 +43,7 @@ declare const HttpResponse: {
43
43
  requestHeaderFieldsTooLarge: (message?: string | undefined, options?: _web_ts_toolkit_http_errors.HttpErrorOptions | undefined) => RequestHeaderFieldsTooLargeError;
44
44
  unavailableForLegalReasons: (message?: string | undefined, options?: _web_ts_toolkit_http_errors.HttpErrorOptions | undefined) => UnavailableForLegalReasonsError;
45
45
  json: (data: unknown) => OK<unknown>;
46
- csv: (dataset?: unknown, options?: {
47
- filename?: string;
48
- headers?: boolean;
49
- processor?: (value: unknown) => unknown;
50
- } | undefined) => CSVResponse;
46
+ csv: (dataset?: unknown, options?: CsvResponseOptions | undefined) => CSVResponse;
51
47
  };
52
48
  type HttpResponseHelpers = typeof HttpResponse;
53
49
 
package/http-response.js CHANGED
@@ -26,12 +26,18 @@ module.exports = __toCommonJS(http_response_exports);
26
26
 
27
27
  // src/responses/index.ts
28
28
  var responseBrand = /* @__PURE__ */ Symbol.for("@web-ts-toolkit/express-response-handler.response");
29
+ var validateHttpStatusCode = (statusCode) => {
30
+ if (typeof statusCode !== "number" || !Number.isFinite(statusCode) || !Number.isInteger(statusCode) || statusCode < 100 || statusCode > 599) {
31
+ throw new TypeError("statusCode must be an integer HTTP status code between 100 and 599");
32
+ }
33
+ return statusCode;
34
+ };
29
35
  var _a;
30
36
  _a = responseBrand;
31
37
  var Response = class {
32
38
  constructor(statusCode = 200, data) {
33
39
  this[_a] = true;
34
- this.statusCode = statusCode;
40
+ this.statusCode = validateHttpStatusCode(statusCode);
35
41
  this.data = data;
36
42
  }
37
43
  };
@@ -91,36 +97,157 @@ var IMUsed = class extends Response {
91
97
  // src/responses/csv.ts
92
98
  var import_format = require("@fast-csv/format");
93
99
  var import_utils = require("@web-ts-toolkit/utils");
100
+ var import_events = require("events");
101
+ var CONTROL_CHARACTER_PATTERN = /[\u0000-\u001f\u007f]/u;
102
+ var isIterable = (value) => typeof value === "object" && value !== null && Symbol.iterator in value;
103
+ var isAsyncIterable = (value) => typeof value === "object" && value !== null && Symbol.asyncIterator in value;
104
+ var isLazyCsvSource = (value) => !Array.isArray(value) && (isIterable(value) || isAsyncIterable(value));
105
+ var createAsyncIterator = (source) => {
106
+ if (isAsyncIterable(source)) {
107
+ return source[Symbol.asyncIterator]();
108
+ }
109
+ const iterator = (isIterable(source) ? source : (0, import_utils.castArray)(source))[Symbol.iterator]();
110
+ return {
111
+ next: () => Promise.resolve(iterator.next()),
112
+ return: (value) => {
113
+ if (iterator.return) {
114
+ return Promise.resolve(iterator.return(value));
115
+ }
116
+ return Promise.resolve({ done: true, value });
117
+ }
118
+ };
119
+ };
120
+ var encodeRFC5987Value = (value) => encodeURIComponent(value).replace(/[!'()*]/g, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`);
121
+ var createQuotedFilenameFallback = (filename) => {
122
+ const fallback = Array.from(filename.replace(/[\\/]/g, "_"), (char) => {
123
+ const codePoint = char.codePointAt(0) ?? 0;
124
+ return codePoint >= 32 && codePoint <= 126 ? char : "_";
125
+ }).join("");
126
+ return fallback.replace(/(["\\])/g, "\\$1");
127
+ };
128
+ var createAttachmentContentDisposition = (filename) => {
129
+ if (CONTROL_CHARACTER_PATTERN.test(filename)) {
130
+ throw new TypeError("CSV filename cannot contain control characters");
131
+ }
132
+ const fallback = createQuotedFilenameFallback(filename);
133
+ return `attachment; filename="${fallback}"; filename*=UTF-8''${encodeRFC5987Value(filename)}`;
134
+ };
94
135
  var csvResponseBrand = /* @__PURE__ */ Symbol.for("@web-ts-toolkit/express-response-handler.csv-response");
95
136
  var _a2;
96
137
  _a2 = csvResponseBrand;
97
138
  var CSVResponse = class {
98
139
  constructor(dataset = [], options = {}) {
99
140
  this[_a2] = true;
100
- this.dataset = (0, import_utils.castArray)(dataset);
141
+ this.dataset = dataset;
101
142
  this.filename = options.filename || "download.csv";
102
143
  this.processor = options.processor || ((value) => value);
103
- if ((0, import_utils.isBoolean)(options.headers)) {
144
+ if ((0, import_utils.isBoolean)(options.headers) || Array.isArray(options.headers)) {
104
145
  this.headers = options.headers;
105
- } else if (this.dataset.length > 0) {
146
+ } else if (Array.isArray(this.dataset) && this.dataset.length > 0) {
106
147
  this.headers = (0, import_utils.isPlainObject)(this.dataset[0]);
148
+ } else if (isLazyCsvSource(this.dataset)) {
149
+ throw new TypeError("CSV lazy sources require an explicit headers option");
150
+ } else {
151
+ this.headers = (0, import_utils.isPlainObject)(this.dataset);
107
152
  }
108
153
  }
109
- streamCsv(res) {
154
+ streamCsv(res, onBeforeOutputError) {
110
155
  const stream = (0, import_format.format)({ headers: this.headers });
111
- res.set("Content-Type", "text/csv");
112
- res.set("Content-Disposition", `attachment;filename=${this.filename}`);
113
- stream.pipe(res);
114
- stream.on("error", (error) => {
156
+ let outputStarted = false;
157
+ let failed = false;
158
+ let finished = false;
159
+ let activeIterator = null;
160
+ let abortStreaming = () => void 0;
161
+ const abortSignal = new Promise((_, reject) => {
162
+ abortStreaming = reject;
163
+ });
164
+ abortSignal.catch(() => void 0);
165
+ const cleanup = () => {
166
+ stream.off("error", fail);
167
+ res.off?.("error", fail);
168
+ res.off?.("close", abort);
169
+ res.off?.("finish", markFinished);
170
+ };
171
+ const closeActiveIterator = () => {
172
+ const iterator = activeIterator;
173
+ activeIterator = null;
174
+ if (iterator?.return) {
175
+ void Promise.resolve(iterator.return()).catch(() => void 0);
176
+ }
177
+ };
178
+ const closeResponse = (error) => {
115
179
  if (res.destroy) {
116
180
  res.destroy(error);
117
181
  return;
118
182
  }
119
183
  res.end();
120
- });
121
- stream.on("end", () => res.end());
122
- this.dataset.forEach((value) => stream.write(this.processor(value)));
123
- stream.end();
184
+ };
185
+ const fail = (error) => {
186
+ if (failed) {
187
+ return;
188
+ }
189
+ failed = true;
190
+ cleanup();
191
+ const normalizedError = error instanceof Error ? error : new Error("CSV response streaming failed");
192
+ abortStreaming(normalizedError);
193
+ closeActiveIterator();
194
+ if (!outputStarted && !res.headersSent) {
195
+ onBeforeOutputError?.(normalizedError);
196
+ return;
197
+ }
198
+ stream.destroy();
199
+ closeResponse(normalizedError);
200
+ };
201
+ const abort = () => {
202
+ if (!finished) {
203
+ fail(new Error("CSV response streaming aborted"));
204
+ }
205
+ };
206
+ const markFinished = () => {
207
+ finished = true;
208
+ };
209
+ const writeRow = async (row) => {
210
+ outputStarted = true;
211
+ if (!stream.write(row)) {
212
+ await Promise.race([(0, import_events.once)(stream, "drain"), abortSignal]);
213
+ }
214
+ };
215
+ stream.on("error", fail);
216
+ stream.on("end", cleanup);
217
+ res.on?.("error", fail);
218
+ res.on?.("close", abort);
219
+ res.on?.("finish", markFinished);
220
+ const pump = async () => {
221
+ try {
222
+ const iterator = createAsyncIterator(this.dataset);
223
+ activeIterator = iterator;
224
+ const first = await iterator.next();
225
+ const firstRow = first.done ? void 0 : this.processor(first.value);
226
+ const contentDisposition = createAttachmentContentDisposition(this.filename);
227
+ res.set("Content-Type", "text/csv");
228
+ res.set("Content-Disposition", contentDisposition);
229
+ stream.pipe(res);
230
+ if (!first.done) {
231
+ await writeRow(firstRow);
232
+ }
233
+ while (!failed) {
234
+ const row = await Promise.race([iterator.next(), abortSignal]);
235
+ if (row.done) {
236
+ break;
237
+ }
238
+ await writeRow(this.processor(row.value));
239
+ }
240
+ if (!failed) {
241
+ finished = true;
242
+ stream.end();
243
+ }
244
+ } catch (error) {
245
+ fail(error);
246
+ } finally {
247
+ activeIterator = null;
248
+ }
249
+ };
250
+ void pump();
124
251
  }
125
252
  };
126
253