@web-ts-toolkit/express-response-handler 0.31.5 → 0.33.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/http-response.mjs
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
// src/responses/index.ts
|
|
2
2
|
var responseBrand = /* @__PURE__ */ Symbol.for("@web-ts-toolkit/express-response-handler.response");
|
|
3
|
+
var validateHttpStatusCode = (statusCode) => {
|
|
4
|
+
if (typeof statusCode !== "number" || !Number.isFinite(statusCode) || !Number.isInteger(statusCode) || statusCode < 100 || statusCode > 599) {
|
|
5
|
+
throw new TypeError("statusCode must be an integer HTTP status code between 100 and 599");
|
|
6
|
+
}
|
|
7
|
+
return statusCode;
|
|
8
|
+
};
|
|
3
9
|
var _a;
|
|
4
10
|
_a = responseBrand;
|
|
5
11
|
var Response = class {
|
|
6
12
|
constructor(statusCode = 200, data) {
|
|
7
13
|
this[_a] = true;
|
|
8
|
-
this.statusCode = statusCode;
|
|
14
|
+
this.statusCode = validateHttpStatusCode(statusCode);
|
|
9
15
|
this.data = data;
|
|
10
16
|
}
|
|
11
17
|
};
|
|
@@ -65,36 +71,157 @@ var IMUsed = class extends Response {
|
|
|
65
71
|
// src/responses/csv.ts
|
|
66
72
|
import { format } from "@fast-csv/format";
|
|
67
73
|
import { castArray, isBoolean, isPlainObject } from "@web-ts-toolkit/utils";
|
|
74
|
+
import { once } from "events";
|
|
75
|
+
var CONTROL_CHARACTER_PATTERN = /[\u0000-\u001f\u007f]/u;
|
|
76
|
+
var isIterable = (value) => typeof value === "object" && value !== null && Symbol.iterator in value;
|
|
77
|
+
var isAsyncIterable = (value) => typeof value === "object" && value !== null && Symbol.asyncIterator in value;
|
|
78
|
+
var isLazyCsvSource = (value) => !Array.isArray(value) && (isIterable(value) || isAsyncIterable(value));
|
|
79
|
+
var createAsyncIterator = (source) => {
|
|
80
|
+
if (isAsyncIterable(source)) {
|
|
81
|
+
return source[Symbol.asyncIterator]();
|
|
82
|
+
}
|
|
83
|
+
const iterator = (isIterable(source) ? source : castArray(source))[Symbol.iterator]();
|
|
84
|
+
return {
|
|
85
|
+
next: () => Promise.resolve(iterator.next()),
|
|
86
|
+
return: (value) => {
|
|
87
|
+
if (iterator.return) {
|
|
88
|
+
return Promise.resolve(iterator.return(value));
|
|
89
|
+
}
|
|
90
|
+
return Promise.resolve({ done: true, value });
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
};
|
|
94
|
+
var encodeRFC5987Value = (value) => encodeURIComponent(value).replace(/[!'()*]/g, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
95
|
+
var createQuotedFilenameFallback = (filename) => {
|
|
96
|
+
const fallback = Array.from(filename.replace(/[\\/]/g, "_"), (char) => {
|
|
97
|
+
const codePoint = char.codePointAt(0) ?? 0;
|
|
98
|
+
return codePoint >= 32 && codePoint <= 126 ? char : "_";
|
|
99
|
+
}).join("");
|
|
100
|
+
return fallback.replace(/(["\\])/g, "\\$1");
|
|
101
|
+
};
|
|
102
|
+
var createAttachmentContentDisposition = (filename) => {
|
|
103
|
+
if (CONTROL_CHARACTER_PATTERN.test(filename)) {
|
|
104
|
+
throw new TypeError("CSV filename cannot contain control characters");
|
|
105
|
+
}
|
|
106
|
+
const fallback = createQuotedFilenameFallback(filename);
|
|
107
|
+
return `attachment; filename="${fallback}"; filename*=UTF-8''${encodeRFC5987Value(filename)}`;
|
|
108
|
+
};
|
|
68
109
|
var csvResponseBrand = /* @__PURE__ */ Symbol.for("@web-ts-toolkit/express-response-handler.csv-response");
|
|
69
110
|
var _a2;
|
|
70
111
|
_a2 = csvResponseBrand;
|
|
71
112
|
var CSVResponse = class {
|
|
72
113
|
constructor(dataset = [], options = {}) {
|
|
73
114
|
this[_a2] = true;
|
|
74
|
-
this.dataset =
|
|
115
|
+
this.dataset = dataset;
|
|
75
116
|
this.filename = options.filename || "download.csv";
|
|
76
117
|
this.processor = options.processor || ((value) => value);
|
|
77
|
-
if (isBoolean(options.headers)) {
|
|
118
|
+
if (isBoolean(options.headers) || Array.isArray(options.headers)) {
|
|
78
119
|
this.headers = options.headers;
|
|
79
|
-
} else if (this.dataset.length > 0) {
|
|
120
|
+
} else if (Array.isArray(this.dataset) && this.dataset.length > 0) {
|
|
80
121
|
this.headers = isPlainObject(this.dataset[0]);
|
|
122
|
+
} else if (isLazyCsvSource(this.dataset)) {
|
|
123
|
+
throw new TypeError("CSV lazy sources require an explicit headers option");
|
|
124
|
+
} else {
|
|
125
|
+
this.headers = isPlainObject(this.dataset);
|
|
81
126
|
}
|
|
82
127
|
}
|
|
83
|
-
streamCsv(res) {
|
|
128
|
+
streamCsv(res, onBeforeOutputError) {
|
|
84
129
|
const stream = format({ headers: this.headers });
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
130
|
+
let outputStarted = false;
|
|
131
|
+
let failed = false;
|
|
132
|
+
let finished = false;
|
|
133
|
+
let activeIterator = null;
|
|
134
|
+
let abortStreaming = () => void 0;
|
|
135
|
+
const abortSignal = new Promise((_, reject) => {
|
|
136
|
+
abortStreaming = reject;
|
|
137
|
+
});
|
|
138
|
+
abortSignal.catch(() => void 0);
|
|
139
|
+
const cleanup = () => {
|
|
140
|
+
stream.off("error", fail);
|
|
141
|
+
res.off?.("error", fail);
|
|
142
|
+
res.off?.("close", abort);
|
|
143
|
+
res.off?.("finish", markFinished);
|
|
144
|
+
};
|
|
145
|
+
const closeActiveIterator = () => {
|
|
146
|
+
const iterator = activeIterator;
|
|
147
|
+
activeIterator = null;
|
|
148
|
+
if (iterator?.return) {
|
|
149
|
+
void Promise.resolve(iterator.return()).catch(() => void 0);
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
const closeResponse = (error) => {
|
|
89
153
|
if (res.destroy) {
|
|
90
154
|
res.destroy(error);
|
|
91
155
|
return;
|
|
92
156
|
}
|
|
93
157
|
res.end();
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
158
|
+
};
|
|
159
|
+
const fail = (error) => {
|
|
160
|
+
if (failed) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
failed = true;
|
|
164
|
+
cleanup();
|
|
165
|
+
const normalizedError = error instanceof Error ? error : new Error("CSV response streaming failed");
|
|
166
|
+
abortStreaming(normalizedError);
|
|
167
|
+
closeActiveIterator();
|
|
168
|
+
if (!outputStarted && !res.headersSent) {
|
|
169
|
+
onBeforeOutputError?.(normalizedError);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
stream.destroy();
|
|
173
|
+
closeResponse(normalizedError);
|
|
174
|
+
};
|
|
175
|
+
const abort = () => {
|
|
176
|
+
if (!finished) {
|
|
177
|
+
fail(new Error("CSV response streaming aborted"));
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
const markFinished = () => {
|
|
181
|
+
finished = true;
|
|
182
|
+
};
|
|
183
|
+
const writeRow = async (row) => {
|
|
184
|
+
outputStarted = true;
|
|
185
|
+
if (!stream.write(row)) {
|
|
186
|
+
await Promise.race([once(stream, "drain"), abortSignal]);
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
stream.on("error", fail);
|
|
190
|
+
stream.on("end", cleanup);
|
|
191
|
+
res.on?.("error", fail);
|
|
192
|
+
res.on?.("close", abort);
|
|
193
|
+
res.on?.("finish", markFinished);
|
|
194
|
+
const pump = async () => {
|
|
195
|
+
try {
|
|
196
|
+
const iterator = createAsyncIterator(this.dataset);
|
|
197
|
+
activeIterator = iterator;
|
|
198
|
+
const first = await iterator.next();
|
|
199
|
+
const firstRow = first.done ? void 0 : this.processor(first.value);
|
|
200
|
+
const contentDisposition = createAttachmentContentDisposition(this.filename);
|
|
201
|
+
res.set("Content-Type", "text/csv");
|
|
202
|
+
res.set("Content-Disposition", contentDisposition);
|
|
203
|
+
stream.pipe(res);
|
|
204
|
+
if (!first.done) {
|
|
205
|
+
await writeRow(firstRow);
|
|
206
|
+
}
|
|
207
|
+
while (!failed) {
|
|
208
|
+
const row = await Promise.race([iterator.next(), abortSignal]);
|
|
209
|
+
if (row.done) {
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
await writeRow(this.processor(row.value));
|
|
213
|
+
}
|
|
214
|
+
if (!failed) {
|
|
215
|
+
finished = true;
|
|
216
|
+
stream.end();
|
|
217
|
+
}
|
|
218
|
+
} catch (error) {
|
|
219
|
+
fail(error);
|
|
220
|
+
} finally {
|
|
221
|
+
activeIterator = null;
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
void pump();
|
|
98
225
|
}
|
|
99
226
|
};
|
|
100
227
|
|
package/index.d.mts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import { ExpressResponseHandler } from './public-types.mjs';
|
|
2
|
-
export { AsyncHook, CreateHandler, ErrorFormat, ErrorMessageProvider, ErrorMessageResult, ErrorWithPayload,
|
|
1
|
+
import { ExpressResponseHandler, HandleResponse } from './public-types.mjs';
|
|
2
|
+
export { AsyncHook, CreateHandler, ErrorFormat, ErrorMessageProvider, ErrorMessageResult, ErrorWithPayload, ExpressResponseHandlerOptions, Hook, MaybePromise, MiddlewareFunction, NextFunction, ResponseLike, RouterFunction } from './public-types.mjs';
|
|
3
3
|
export { createHandler } from './create-handler.mjs';
|
|
4
4
|
export { ErrorFormats } from './error-formats.mjs';
|
|
5
5
|
export { HttpResponse, HttpResponseHelpers } from './http-response.mjs';
|
|
6
6
|
export { CSVResponse } from './responses/csv.mjs';
|
|
7
7
|
export { Response } from './responses/index.mjs';
|
|
8
8
|
export { Accepted, AlreadyReported, Created, IMUsed, MultiStatus, NoContent, NonAuthoritativeInfo, OK, PartialContent, ResetContent } from './responses/success.mjs';
|
|
9
|
+
import 'express-serve-static-core';
|
|
9
10
|
import '@web-ts-toolkit/http-errors';
|
|
10
11
|
|
|
11
12
|
declare const apiHandler: ExpressResponseHandler;
|
|
13
|
+
declare const handleResponse: HandleResponse;
|
|
12
14
|
|
|
13
|
-
export { ExpressResponseHandler, apiHandler as default };
|
|
15
|
+
export { ExpressResponseHandler, HandleResponse, apiHandler as default, handleResponse };
|
package/index.d.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import { ExpressResponseHandler } from './public-types.js';
|
|
2
|
-
export { AsyncHook, CreateHandler, ErrorFormat, ErrorMessageProvider, ErrorMessageResult, ErrorWithPayload,
|
|
1
|
+
import { ExpressResponseHandler, HandleResponse } from './public-types.js';
|
|
2
|
+
export { AsyncHook, CreateHandler, ErrorFormat, ErrorMessageProvider, ErrorMessageResult, ErrorWithPayload, ExpressResponseHandlerOptions, Hook, MaybePromise, MiddlewareFunction, NextFunction, ResponseLike, RouterFunction } from './public-types.js';
|
|
3
3
|
export { createHandler } from './create-handler.js';
|
|
4
4
|
export { ErrorFormats } from './error-formats.js';
|
|
5
5
|
export { HttpResponse, HttpResponseHelpers } from './http-response.js';
|
|
6
6
|
export { CSVResponse } from './responses/csv.js';
|
|
7
7
|
export { Response } from './responses/index.js';
|
|
8
8
|
export { Accepted, AlreadyReported, Created, IMUsed, MultiStatus, NoContent, NonAuthoritativeInfo, OK, PartialContent, ResetContent } from './responses/success.js';
|
|
9
|
+
import 'express-serve-static-core';
|
|
9
10
|
import '@web-ts-toolkit/http-errors';
|
|
10
11
|
|
|
11
12
|
declare const apiHandler: ExpressResponseHandler;
|
|
13
|
+
declare const handleResponse: HandleResponse;
|
|
12
14
|
|
|
13
|
-
export { ExpressResponseHandler, apiHandler as default };
|
|
15
|
+
export { ExpressResponseHandler, HandleResponse, apiHandler as default, handleResponse };
|